1057 lines
36 KiB
TypeScript
1057 lines
36 KiB
TypeScript
import { isLeadMember } from '@shared/utils/leadDetection';
|
|
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
|
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
|
|
|
import type {
|
|
MemberLaunchState,
|
|
MemberSpawnLivenessSource,
|
|
MemberSpawnStatusEntry,
|
|
OpenCodeAppManagedBootstrapCandidate,
|
|
PersistedTeamLaunchMemberSources,
|
|
PersistedTeamLaunchMemberState,
|
|
PersistedTeamLaunchPhase,
|
|
PersistedTeamLaunchSnapshot,
|
|
PersistedTeamLaunchSummary,
|
|
ProviderModelLaunchIdentity,
|
|
TeamAgentRuntimeDiagnosticSeverity,
|
|
TeamAgentRuntimeLivenessKind,
|
|
TeamAgentRuntimePidSource,
|
|
TeamLaunchAggregateState,
|
|
} from '@shared/types';
|
|
|
|
interface LegacyPartialLaunchStateFile {
|
|
version?: unknown;
|
|
state?: unknown;
|
|
updatedAt?: unknown;
|
|
leadSessionId?: unknown;
|
|
expectedMembers?: unknown;
|
|
confirmedMembers?: unknown;
|
|
missingMembers?: unknown;
|
|
}
|
|
|
|
type RuntimeMemberSpawnState = Pick<
|
|
MemberSpawnStatusEntry,
|
|
| 'launchState'
|
|
| 'status'
|
|
| 'error'
|
|
| 'hardFailureReason'
|
|
| 'livenessSource'
|
|
| 'agentToolAccepted'
|
|
| 'runtimeAlive'
|
|
| 'bootstrapConfirmed'
|
|
| 'hardFailure'
|
|
| 'skippedForLaunch'
|
|
| 'skipReason'
|
|
| 'skippedAt'
|
|
| 'pendingPermissionRequestIds'
|
|
| 'livenessKind'
|
|
| 'runtimeDiagnostic'
|
|
| 'runtimeDiagnosticSeverity'
|
|
| 'bootstrapStalled'
|
|
| 'livenessLastCheckedAt'
|
|
| 'firstSpawnAcceptedAt'
|
|
| 'lastHeartbeatAt'
|
|
| 'runtimeModel'
|
|
| 'updatedAt'
|
|
>;
|
|
|
|
function normalizePendingPermissionRequestIds(value: unknown): string[] | undefined {
|
|
if (!Array.isArray(value)) {
|
|
return undefined;
|
|
}
|
|
const normalized = value
|
|
.filter((item): item is string => typeof item === 'string')
|
|
.map((item) => item.trim())
|
|
.filter((item) => item.length > 0);
|
|
return normalized.length > 0 ? Array.from(new Set(normalized)) : undefined;
|
|
}
|
|
|
|
function normalizeRuntimePid(value: unknown): number | undefined {
|
|
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
? Math.trunc(value)
|
|
: undefined;
|
|
}
|
|
|
|
function normalizeLivenessKind(value: unknown): TeamAgentRuntimeLivenessKind | undefined {
|
|
return value === 'confirmed_bootstrap' ||
|
|
value === 'runtime_process' ||
|
|
value === 'runtime_process_candidate' ||
|
|
value === 'permission_blocked' ||
|
|
value === 'shell_only' ||
|
|
value === 'registered_only' ||
|
|
value === 'stale_metadata' ||
|
|
value === 'not_found'
|
|
? value
|
|
: undefined;
|
|
}
|
|
|
|
function preservesStrongRuntimeAlive(
|
|
value:
|
|
| {
|
|
runtimeAlive?: boolean;
|
|
bootstrapConfirmed?: boolean;
|
|
livenessKind?: TeamAgentRuntimeLivenessKind;
|
|
}
|
|
| undefined
|
|
): boolean {
|
|
return (
|
|
value?.runtimeAlive === true &&
|
|
(value.bootstrapConfirmed === true ||
|
|
value.livenessKind === 'confirmed_bootstrap' ||
|
|
value.livenessKind === 'runtime_process')
|
|
);
|
|
}
|
|
|
|
function isOpenCodeSecondaryBootstrapPending(
|
|
member: Pick<
|
|
PersistedTeamLaunchMemberState,
|
|
| 'providerId'
|
|
| 'laneKind'
|
|
| 'laneOwnerProviderId'
|
|
| 'launchState'
|
|
| 'bootstrapConfirmed'
|
|
| 'hardFailure'
|
|
>
|
|
): boolean {
|
|
return (
|
|
member.providerId === 'opencode' &&
|
|
member.laneKind === 'secondary' &&
|
|
member.laneOwnerProviderId === 'opencode' &&
|
|
member.launchState === 'runtime_pending_bootstrap' &&
|
|
member.bootstrapConfirmed !== true &&
|
|
member.hardFailure !== true
|
|
);
|
|
}
|
|
|
|
function isPersistedBootstrapStalled(
|
|
member: Pick<
|
|
PersistedTeamLaunchMemberState,
|
|
| 'providerId'
|
|
| 'laneKind'
|
|
| 'laneOwnerProviderId'
|
|
| 'launchState'
|
|
| 'runtimeAlive'
|
|
| 'bootstrapConfirmed'
|
|
| 'hardFailure'
|
|
| 'bootstrapStalled'
|
|
| 'runtimePid'
|
|
| 'runtimeSessionId'
|
|
| 'livenessKind'
|
|
>
|
|
): boolean {
|
|
const hasMaterializedOpenCodeRuntimeMarker =
|
|
member.runtimeAlive === true ||
|
|
(typeof member.runtimePid === 'number' &&
|
|
Number.isFinite(member.runtimePid) &&
|
|
member.runtimePid > 0) ||
|
|
(typeof member.runtimeSessionId === 'string' && member.runtimeSessionId.trim().length > 0) ||
|
|
member.livenessKind === 'runtime_process' ||
|
|
member.livenessKind === 'runtime_process_candidate' ||
|
|
member.livenessKind === 'registered_only';
|
|
return (
|
|
member.bootstrapStalled === true &&
|
|
isOpenCodeSecondaryBootstrapPending(member) &&
|
|
hasMaterializedOpenCodeRuntimeMarker
|
|
);
|
|
}
|
|
|
|
function normalizePidSource(value: unknown): TeamAgentRuntimePidSource | undefined {
|
|
return value === 'lead_process' ||
|
|
value === 'tmux_pane' ||
|
|
value === 'tmux_child' ||
|
|
value === 'agent_process_table' ||
|
|
value === 'opencode_bridge' ||
|
|
value === 'runtime_bootstrap' ||
|
|
value === 'persisted_metadata'
|
|
? value
|
|
: undefined;
|
|
}
|
|
|
|
function normalizeDiagnosticSeverity(
|
|
value: unknown
|
|
): TeamAgentRuntimeDiagnosticSeverity | undefined {
|
|
return value === 'info' || value === 'warning' || value === 'error' ? value : undefined;
|
|
}
|
|
|
|
function normalizeOptionalString(value: unknown): string | undefined {
|
|
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
}
|
|
|
|
function normalizeOpenCodeAppManagedBootstrapCandidate(
|
|
value: unknown
|
|
): OpenCodeAppManagedBootstrapCandidate | undefined {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
return undefined;
|
|
}
|
|
const record = value as Record<string, unknown>;
|
|
if (record.schemaVersion !== 1 || record.source !== 'app_managed_bootstrap') {
|
|
return undefined;
|
|
}
|
|
const teamName = normalizeOptionalString(record.teamName);
|
|
const memberName = normalizeOptionalString(record.memberName);
|
|
const runId = normalizeOptionalString(record.runId);
|
|
const laneId = normalizeOptionalString(record.laneId);
|
|
const runtimeSessionId = normalizeOptionalString(record.runtimeSessionId);
|
|
const messageID = normalizeOptionalString(record.messageID);
|
|
const contextHash = normalizeOptionalString(record.contextHash);
|
|
const briefingHash = normalizeOptionalString(record.briefingHash);
|
|
const injectionVerifiedAt = normalizeOptionalString(record.injectionVerifiedAt);
|
|
const candidateAt = normalizeOptionalString(record.candidateAt);
|
|
if (
|
|
!teamName ||
|
|
!memberName ||
|
|
!runId ||
|
|
!laneId ||
|
|
!runtimeSessionId ||
|
|
!messageID ||
|
|
!contextHash ||
|
|
!briefingHash ||
|
|
!injectionVerifiedAt ||
|
|
!candidateAt
|
|
) {
|
|
return undefined;
|
|
}
|
|
const model = normalizeOptionalString(record.model);
|
|
const agent = normalizeOptionalString(record.agent);
|
|
return {
|
|
schemaVersion: 1,
|
|
source: 'app_managed_bootstrap',
|
|
teamName,
|
|
memberName,
|
|
runId,
|
|
laneId,
|
|
runtimeSessionId,
|
|
messageID,
|
|
contextHash,
|
|
briefingHash,
|
|
injectionVerifiedAt,
|
|
candidateAt,
|
|
...(model ? { model } : {}),
|
|
...(agent ? { agent } : {}),
|
|
};
|
|
}
|
|
|
|
function decodeJsonStringLiteral(value: string): string {
|
|
try {
|
|
return JSON.parse(`"${value}"`) as string;
|
|
} catch {
|
|
return value.replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\\\/g, '\\');
|
|
}
|
|
}
|
|
|
|
function extractLooseJsonStringField(text: string, fieldName: string): string | undefined {
|
|
const strictMatch = new RegExp(`"${fieldName}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`).exec(text);
|
|
if (strictMatch?.[1]) {
|
|
return decodeJsonStringLiteral(strictMatch[1]).trim() || undefined;
|
|
}
|
|
|
|
const looseMatch = new RegExp(`"${fieldName}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)$`).exec(text);
|
|
return looseMatch?.[1] ? decodeJsonStringLiteral(looseMatch[1]).trim() || undefined : undefined;
|
|
}
|
|
|
|
function joinUniqueReasonParts(parts: readonly (string | undefined)[]): string | undefined {
|
|
const uniqueParts = Array.from(
|
|
new Set(parts.map((part) => part?.trim()).filter((part): part is string => !!part))
|
|
);
|
|
return uniqueParts.length > 0 ? uniqueParts.join(': ') : undefined;
|
|
}
|
|
|
|
function extractMessageSendRoutingReason(text: string): string | undefined {
|
|
const trimmed = text.trim();
|
|
if (!trimmed.includes('Message sent to') && !trimmed.includes('"routing"')) {
|
|
return undefined;
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(trimmed) as {
|
|
success?: unknown;
|
|
message?: unknown;
|
|
routing?: { summary?: unknown; content?: unknown };
|
|
};
|
|
if (parsed.success === true && parsed.routing && typeof parsed.routing === 'object') {
|
|
return joinUniqueReasonParts([
|
|
typeof parsed.routing.summary === 'string' ? parsed.routing.summary : undefined,
|
|
typeof parsed.routing.content === 'string' ? parsed.routing.content : undefined,
|
|
]);
|
|
}
|
|
} catch {
|
|
// Fall through to loose extraction for persisted reasons truncated by older builds.
|
|
}
|
|
|
|
return joinUniqueReasonParts([
|
|
extractLooseJsonStringField(trimmed, 'summary'),
|
|
extractLooseJsonStringField(trimmed, 'content'),
|
|
]);
|
|
}
|
|
|
|
export function normalizeLaunchFailureReasonText(value: unknown): string | undefined {
|
|
const text = normalizeOptionalString(value);
|
|
if (!text) {
|
|
return undefined;
|
|
}
|
|
return extractMessageSendRoutingReason(text) ?? text;
|
|
}
|
|
|
|
function normalizeMemberName(name: string): string {
|
|
return name.trim();
|
|
}
|
|
|
|
function buildDiagnostics(
|
|
member: Pick<
|
|
PersistedTeamLaunchMemberState,
|
|
| 'agentToolAccepted'
|
|
| 'runtimeAlive'
|
|
| 'bootstrapConfirmed'
|
|
| 'hardFailure'
|
|
| 'hardFailureReason'
|
|
| 'skippedForLaunch'
|
|
| 'skipReason'
|
|
| 'sources'
|
|
| 'pendingPermissionRequestIds'
|
|
| 'bootstrapStalled'
|
|
>
|
|
): string[] {
|
|
const diagnostics: string[] = [];
|
|
if (member.agentToolAccepted) diagnostics.push('spawn accepted');
|
|
if (member.runtimeAlive) diagnostics.push('runtime alive');
|
|
if (member.bootstrapConfirmed) diagnostics.push('late heartbeat received');
|
|
if ((member.pendingPermissionRequestIds?.length ?? 0) > 0) {
|
|
diagnostics.push('waiting for permission approval');
|
|
} else if (member.bootstrapStalled) {
|
|
diagnostics.push('opencode_bootstrap_stalled');
|
|
} else if (member.hardFailure && member.runtimeAlive && !member.bootstrapConfirmed) {
|
|
diagnostics.push('bootstrap failed while runtime process was still alive');
|
|
} else if (member.runtimeAlive && !member.bootstrapConfirmed) {
|
|
diagnostics.push('waiting for teammate check-in');
|
|
}
|
|
const hardFailureReason = normalizeLaunchFailureReasonText(member.hardFailureReason);
|
|
if (hardFailureReason) diagnostics.push(`hard failure reason: ${hardFailureReason}`);
|
|
if (member.skippedForLaunch) {
|
|
diagnostics.push(
|
|
member.skipReason
|
|
? `skipped for this launch: ${member.skipReason}`
|
|
: 'skipped for this launch'
|
|
);
|
|
}
|
|
if (member.sources?.duplicateRespawnBlocked) diagnostics.push('respawn blocked as duplicate');
|
|
if (member.sources?.configDrift) diagnostics.push('config drift detected');
|
|
return diagnostics;
|
|
}
|
|
|
|
export function deriveTeamLaunchAggregateState(
|
|
summary: PersistedTeamLaunchSummary
|
|
): TeamLaunchAggregateState {
|
|
if (summary.failedCount > 0) {
|
|
return 'partial_failure';
|
|
}
|
|
if (summary.pendingCount > 0) {
|
|
return 'partial_pending';
|
|
}
|
|
if ((summary.skippedCount ?? 0) > 0) {
|
|
return 'partial_skipped';
|
|
}
|
|
return 'clean_success';
|
|
}
|
|
|
|
export function summarizePersistedLaunchMembers(
|
|
expectedMembers: readonly string[],
|
|
members: Record<string, PersistedTeamLaunchMemberState>
|
|
): PersistedTeamLaunchSummary {
|
|
let confirmedCount = 0;
|
|
let pendingCount = 0;
|
|
let failedCount = 0;
|
|
let skippedCount = 0;
|
|
let runtimeAlivePendingCount = 0;
|
|
let shellOnlyPendingCount = 0;
|
|
let runtimeProcessPendingCount = 0;
|
|
let runtimeCandidatePendingCount = 0;
|
|
let noRuntimePendingCount = 0;
|
|
let permissionPendingCount = 0;
|
|
const normalizedExpected = expectedMembers.map(normalizeMemberName).filter(Boolean);
|
|
const memberNames = Array.from(
|
|
new Set([
|
|
...normalizedExpected,
|
|
...Object.keys(members).map(normalizeMemberName).filter(Boolean),
|
|
])
|
|
);
|
|
|
|
for (const memberName of memberNames) {
|
|
const entry = members[memberName];
|
|
if (!entry) {
|
|
pendingCount += 1;
|
|
continue;
|
|
}
|
|
if (entry.launchState === 'confirmed_alive') {
|
|
confirmedCount += 1;
|
|
continue;
|
|
}
|
|
if (entry.launchState === 'skipped_for_launch' || entry.skippedForLaunch === true) {
|
|
skippedCount += 1;
|
|
continue;
|
|
}
|
|
if (entry.launchState === 'failed_to_start') {
|
|
failedCount += 1;
|
|
continue;
|
|
}
|
|
pendingCount += 1;
|
|
if (preservesStrongRuntimeAlive(entry)) {
|
|
runtimeAlivePendingCount += 1;
|
|
}
|
|
if (entry.launchState === 'runtime_pending_permission') {
|
|
permissionPendingCount += 1;
|
|
}
|
|
if (entry.livenessKind === 'shell_only') {
|
|
shellOnlyPendingCount += 1;
|
|
} else if (entry.livenessKind === 'runtime_process') {
|
|
runtimeProcessPendingCount += 1;
|
|
} else if (entry.livenessKind === 'runtime_process_candidate') {
|
|
runtimeCandidatePendingCount += 1;
|
|
} else if (
|
|
entry.livenessKind === 'not_found' ||
|
|
entry.livenessKind === 'stale_metadata' ||
|
|
entry.livenessKind === 'registered_only'
|
|
) {
|
|
noRuntimePendingCount += 1;
|
|
}
|
|
}
|
|
|
|
return {
|
|
confirmedCount,
|
|
pendingCount,
|
|
failedCount,
|
|
skippedCount,
|
|
runtimeAlivePendingCount,
|
|
shellOnlyPendingCount,
|
|
runtimeProcessPendingCount,
|
|
runtimeCandidatePendingCount,
|
|
noRuntimePendingCount,
|
|
permissionPendingCount,
|
|
};
|
|
}
|
|
|
|
export function hasMixedPersistedLaunchMetadata(
|
|
snapshot: PersistedTeamLaunchSnapshot | null | undefined
|
|
): boolean {
|
|
if (!snapshot) {
|
|
return false;
|
|
}
|
|
if (
|
|
Array.isArray(snapshot.bootstrapExpectedMembers) &&
|
|
snapshot.bootstrapExpectedMembers.join('\u0000') !== snapshot.expectedMembers.join('\u0000')
|
|
) {
|
|
return true;
|
|
}
|
|
return Object.values(snapshot.members).some(
|
|
(member) =>
|
|
Boolean(member?.laneId) ||
|
|
Boolean(member?.laneKind) ||
|
|
Boolean(member?.laneOwnerProviderId) ||
|
|
Boolean(member?.launchIdentity)
|
|
);
|
|
}
|
|
|
|
function deriveMemberLaunchState(
|
|
member: Pick<
|
|
PersistedTeamLaunchMemberState,
|
|
| 'hardFailure'
|
|
| 'bootstrapConfirmed'
|
|
| 'runtimeAlive'
|
|
| 'agentToolAccepted'
|
|
| 'skippedForLaunch'
|
|
| 'pendingPermissionRequestIds'
|
|
>
|
|
): MemberLaunchState {
|
|
if (member.skippedForLaunch) {
|
|
return 'skipped_for_launch';
|
|
}
|
|
if (member.hardFailure) {
|
|
return 'failed_to_start';
|
|
}
|
|
if (member.bootstrapConfirmed) {
|
|
return 'confirmed_alive';
|
|
}
|
|
if ((member.pendingPermissionRequestIds?.length ?? 0) > 0) {
|
|
return 'runtime_pending_permission';
|
|
}
|
|
if (member.runtimeAlive || member.agentToolAccepted) {
|
|
return 'runtime_pending_bootstrap';
|
|
}
|
|
return 'starting';
|
|
}
|
|
|
|
function toBoolean(value: unknown): boolean {
|
|
return value === true;
|
|
}
|
|
|
|
function normalizeFastMode(value: unknown): PersistedTeamLaunchMemberState['selectedFastMode'] {
|
|
return value === 'inherit' || value === 'on' || value === 'off' ? value : undefined;
|
|
}
|
|
|
|
function normalizeLaunchIdentity(
|
|
value: unknown,
|
|
fallbackProviderId?: PersistedTeamLaunchMemberState['providerId']
|
|
): ProviderModelLaunchIdentity | undefined {
|
|
if (!value || typeof value !== 'object') {
|
|
return undefined;
|
|
}
|
|
const raw = value as Record<string, unknown>;
|
|
const providerId =
|
|
normalizeOptionalTeamProviderId(raw.providerId) ??
|
|
normalizeOptionalTeamProviderId(fallbackProviderId);
|
|
if (!providerId) {
|
|
return undefined;
|
|
}
|
|
const selectedModelKind =
|
|
raw.selectedModelKind === 'explicit' || raw.selectedModelKind === 'default'
|
|
? raw.selectedModelKind
|
|
: 'default';
|
|
const catalogSource =
|
|
raw.catalogSource === 'anthropic-models-api' ||
|
|
raw.catalogSource === 'anthropic-compatible-api' ||
|
|
raw.catalogSource === 'app-server' ||
|
|
raw.catalogSource === 'static-fallback' ||
|
|
raw.catalogSource === 'runtime' ||
|
|
raw.catalogSource === 'unavailable'
|
|
? raw.catalogSource
|
|
: 'unavailable';
|
|
return {
|
|
providerId,
|
|
providerBackendId:
|
|
migrateProviderBackendId(
|
|
providerId,
|
|
typeof raw.providerBackendId === 'string' ? raw.providerBackendId : undefined
|
|
) ?? null,
|
|
selectedModel: typeof raw.selectedModel === 'string' ? raw.selectedModel.trim() || null : null,
|
|
selectedModelKind,
|
|
resolvedLaunchModel:
|
|
typeof raw.resolvedLaunchModel === 'string' ? raw.resolvedLaunchModel.trim() || null : null,
|
|
catalogId: typeof raw.catalogId === 'string' ? raw.catalogId.trim() || null : null,
|
|
catalogSource,
|
|
catalogFetchedAt:
|
|
typeof raw.catalogFetchedAt === 'string' ? raw.catalogFetchedAt.trim() || null : null,
|
|
selectedEffort:
|
|
raw.selectedEffort === 'none' ||
|
|
raw.selectedEffort === 'minimal' ||
|
|
raw.selectedEffort === 'low' ||
|
|
raw.selectedEffort === 'medium' ||
|
|
raw.selectedEffort === 'high' ||
|
|
raw.selectedEffort === 'xhigh' ||
|
|
raw.selectedEffort === 'max'
|
|
? raw.selectedEffort
|
|
: null,
|
|
resolvedEffort:
|
|
raw.resolvedEffort === 'none' ||
|
|
raw.resolvedEffort === 'minimal' ||
|
|
raw.resolvedEffort === 'low' ||
|
|
raw.resolvedEffort === 'medium' ||
|
|
raw.resolvedEffort === 'high' ||
|
|
raw.resolvedEffort === 'xhigh' ||
|
|
raw.resolvedEffort === 'max'
|
|
? raw.resolvedEffort
|
|
: null,
|
|
selectedFastMode:
|
|
raw.selectedFastMode === 'inherit' ||
|
|
raw.selectedFastMode === 'on' ||
|
|
raw.selectedFastMode === 'off'
|
|
? raw.selectedFastMode
|
|
: null,
|
|
resolvedFastMode: typeof raw.resolvedFastMode === 'boolean' ? raw.resolvedFastMode : null,
|
|
fastResolutionReason:
|
|
typeof raw.fastResolutionReason === 'string' ? raw.fastResolutionReason.trim() || null : null,
|
|
};
|
|
}
|
|
|
|
function normalizeSources(value: unknown): PersistedTeamLaunchMemberSources | undefined {
|
|
if (!value || typeof value !== 'object') {
|
|
return undefined;
|
|
}
|
|
const source = value as Record<string, unknown>;
|
|
const normalized: PersistedTeamLaunchMemberSources = {
|
|
inboxHeartbeat: toBoolean(source.inboxHeartbeat),
|
|
nativeHeartbeat: toBoolean(source.nativeHeartbeat),
|
|
processAlive: toBoolean(source.processAlive),
|
|
configRegistered: toBoolean(source.configRegistered),
|
|
configDrift: toBoolean(source.configDrift),
|
|
hardFailureSignal: toBoolean(source.hardFailureSignal),
|
|
duplicateRespawnBlocked: toBoolean(source.duplicateRespawnBlocked),
|
|
};
|
|
return Object.values(normalized).some(Boolean) ? normalized : undefined;
|
|
}
|
|
|
|
function normalizePersistedMemberState(
|
|
memberName: string,
|
|
value: unknown,
|
|
updatedAtFallback: string
|
|
): PersistedTeamLaunchMemberState | null {
|
|
if (!value || typeof value !== 'object') {
|
|
return null;
|
|
}
|
|
const parsed = value as Record<string, unknown>;
|
|
const normalizedName = normalizeMemberName(memberName);
|
|
if (!normalizedName || normalizedName === 'user' || isLeadMember({ name: normalizedName })) {
|
|
return null;
|
|
}
|
|
const providerId = normalizeOptionalTeamProviderId(parsed.providerId);
|
|
const skippedForLaunch =
|
|
toBoolean(parsed.skippedForLaunch) || parsed.launchState === 'skipped_for_launch';
|
|
const bootstrapConfirmed =
|
|
!skippedForLaunch &&
|
|
(toBoolean(parsed.bootstrapConfirmed) || parsed.launchState === 'confirmed_alive');
|
|
const livenessKind = normalizeLivenessKind(parsed.livenessKind);
|
|
const runtimeAlive = skippedForLaunch
|
|
? false
|
|
: preservesStrongRuntimeAlive({
|
|
runtimeAlive: toBoolean(parsed.runtimeAlive),
|
|
bootstrapConfirmed,
|
|
livenessKind,
|
|
});
|
|
const hardFailure = skippedForLaunch
|
|
? false
|
|
: toBoolean(parsed.hardFailure) || parsed.launchState === 'failed_to_start';
|
|
const sources = normalizeSources(parsed.sources) ?? {};
|
|
if (!runtimeAlive) {
|
|
sources.processAlive = undefined;
|
|
}
|
|
const next: PersistedTeamLaunchMemberState = {
|
|
name: normalizedName,
|
|
providerId,
|
|
providerBackendId: migrateProviderBackendId(
|
|
providerId,
|
|
typeof parsed.providerBackendId === 'string' ? parsed.providerBackendId : undefined
|
|
),
|
|
model: typeof parsed.model === 'string' ? parsed.model.trim() || undefined : undefined,
|
|
effort:
|
|
parsed.effort === 'none' ||
|
|
parsed.effort === 'minimal' ||
|
|
parsed.effort === 'low' ||
|
|
parsed.effort === 'medium' ||
|
|
parsed.effort === 'high' ||
|
|
parsed.effort === 'xhigh' ||
|
|
parsed.effort === 'max'
|
|
? parsed.effort
|
|
: undefined,
|
|
selectedFastMode: normalizeFastMode(parsed.selectedFastMode),
|
|
resolvedFastMode:
|
|
typeof parsed.resolvedFastMode === 'boolean' ? parsed.resolvedFastMode : undefined,
|
|
laneId: typeof parsed.laneId === 'string' ? parsed.laneId.trim() || undefined : undefined,
|
|
laneKind:
|
|
parsed.laneKind === 'primary' || parsed.laneKind === 'secondary'
|
|
? parsed.laneKind
|
|
: undefined,
|
|
laneOwnerProviderId: normalizeOptionalTeamProviderId(parsed.laneOwnerProviderId),
|
|
launchIdentity: normalizeLaunchIdentity(parsed.launchIdentity, providerId),
|
|
launchState: 'starting',
|
|
skippedForLaunch,
|
|
skipReason: normalizeOptionalString(parsed.skipReason),
|
|
skippedAt: normalizeOptionalString(parsed.skippedAt),
|
|
agentToolAccepted: skippedForLaunch ? false : toBoolean(parsed.agentToolAccepted),
|
|
runtimeAlive,
|
|
bootstrapConfirmed,
|
|
hardFailure,
|
|
hardFailureReason: !hardFailure
|
|
? undefined
|
|
: normalizeLaunchFailureReasonText(parsed.hardFailureReason),
|
|
pendingPermissionRequestIds: normalizePendingPermissionRequestIds(
|
|
parsed.pendingPermissionRequestIds
|
|
),
|
|
runtimePid: normalizeRuntimePid(parsed.runtimePid),
|
|
runtimeRunId: normalizeOptionalString(parsed.runtimeRunId),
|
|
runtimeSessionId: normalizeOptionalString(parsed.runtimeSessionId),
|
|
bootstrapEvidenceSource:
|
|
parsed.bootstrapEvidenceSource === 'runtime_bootstrap_checkin' ||
|
|
parsed.bootstrapEvidenceSource === 'app_managed_bootstrap'
|
|
? parsed.bootstrapEvidenceSource
|
|
: undefined,
|
|
bootstrapMode:
|
|
parsed.bootstrapMode === 'model_tool_checkin' ||
|
|
parsed.bootstrapMode === 'app_managed_context'
|
|
? parsed.bootstrapMode
|
|
: undefined,
|
|
appManagedBootstrapCandidate: normalizeOpenCodeAppManagedBootstrapCandidate(
|
|
parsed.appManagedBootstrapCandidate
|
|
),
|
|
livenessKind,
|
|
pidSource: normalizePidSource(parsed.pidSource),
|
|
runtimeDiagnostic: normalizeOptionalString(parsed.runtimeDiagnostic),
|
|
runtimeDiagnosticSeverity: normalizeDiagnosticSeverity(parsed.runtimeDiagnosticSeverity),
|
|
bootstrapStalled: toBoolean(parsed.bootstrapStalled),
|
|
runtimeLastSeenAt: normalizeOptionalString(parsed.runtimeLastSeenAt),
|
|
firstSpawnAcceptedAt:
|
|
typeof parsed.firstSpawnAcceptedAt === 'string' ? parsed.firstSpawnAcceptedAt : undefined,
|
|
lastHeartbeatAt:
|
|
typeof parsed.lastHeartbeatAt === 'string' ? parsed.lastHeartbeatAt : undefined,
|
|
lastRuntimeAliveAt:
|
|
typeof parsed.lastRuntimeAliveAt === 'string' ? parsed.lastRuntimeAliveAt : undefined,
|
|
lastEvaluatedAt:
|
|
typeof parsed.lastEvaluatedAt === 'string' ? parsed.lastEvaluatedAt : updatedAtFallback,
|
|
sources: Object.values(sources).some(Boolean) ? sources : undefined,
|
|
diagnostics: Array.isArray(parsed.diagnostics)
|
|
? parsed.diagnostics.filter(
|
|
(item): item is string => typeof item === 'string' && item.trim().length > 0
|
|
)
|
|
: undefined,
|
|
};
|
|
const launchState =
|
|
parsed.launchState === 'starting' ||
|
|
parsed.launchState === 'runtime_pending_bootstrap' ||
|
|
parsed.launchState === 'runtime_pending_permission' ||
|
|
parsed.launchState === 'confirmed_alive' ||
|
|
parsed.launchState === 'failed_to_start' ||
|
|
parsed.launchState === 'skipped_for_launch'
|
|
? parsed.launchState
|
|
: deriveMemberLaunchState(next);
|
|
next.launchState = launchState;
|
|
if (!isPersistedBootstrapStalled(next)) {
|
|
next.bootstrapStalled = undefined;
|
|
}
|
|
next.diagnostics = next.diagnostics?.length ? next.diagnostics : buildDiagnostics(next);
|
|
return next;
|
|
}
|
|
|
|
export function createPersistedLaunchSnapshot(params: {
|
|
teamName: string;
|
|
expectedMembers: readonly string[];
|
|
bootstrapExpectedMembers?: readonly string[];
|
|
leadSessionId?: string;
|
|
launchPhase?: PersistedTeamLaunchPhase;
|
|
members?: Record<string, PersistedTeamLaunchMemberState>;
|
|
updatedAt?: string;
|
|
}): PersistedTeamLaunchSnapshot {
|
|
const updatedAt = params.updatedAt ?? new Date().toISOString();
|
|
const expectedMembers = Array.from(
|
|
new Set(
|
|
params.expectedMembers
|
|
.map(normalizeMemberName)
|
|
.filter((name) => name.length > 0 && name !== 'user' && !isLeadMember({ name }))
|
|
)
|
|
);
|
|
const bootstrapExpectedMembers = Array.from(
|
|
new Set(
|
|
(params.bootstrapExpectedMembers ?? expectedMembers)
|
|
.map(normalizeMemberName)
|
|
.filter((name) => name.length > 0 && name !== 'user' && !isLeadMember({ name }))
|
|
)
|
|
);
|
|
const members = params.members ?? {};
|
|
const launchPhase = params.launchPhase ?? 'active';
|
|
|
|
for (const name of expectedMembers) {
|
|
if (members[name]) {
|
|
continue;
|
|
}
|
|
members[name] = {
|
|
name,
|
|
launchState: 'starting',
|
|
skippedForLaunch: false,
|
|
agentToolAccepted: false,
|
|
runtimeAlive: false,
|
|
bootstrapConfirmed: false,
|
|
hardFailure: false,
|
|
lastEvaluatedAt: updatedAt,
|
|
diagnostics: [],
|
|
};
|
|
}
|
|
|
|
// When the launch is over (finished/reconciled), members still in 'starting' state
|
|
// (never spawned — agentToolAccepted is false) are unreachable and should be marked
|
|
// as failed. Without this, they stay as 'pending' forever, causing the UI to show
|
|
// "Last launch is still reconciling" indefinitely after a crash or incomplete launch.
|
|
if (launchPhase !== 'active') {
|
|
for (const name of expectedMembers) {
|
|
const member = members[name];
|
|
const isRecoverableOpenCodeSecondaryLane =
|
|
member?.laneKind === 'secondary' &&
|
|
member.laneOwnerProviderId === 'opencode' &&
|
|
typeof member.laneId === 'string' &&
|
|
member.laneId.trim().length > 0;
|
|
if (
|
|
member?.launchState === 'starting' &&
|
|
!member.agentToolAccepted &&
|
|
!member.runtimeAlive &&
|
|
!member.bootstrapConfirmed &&
|
|
!member.hardFailure &&
|
|
!isRecoverableOpenCodeSecondaryLane
|
|
) {
|
|
member.hardFailure = true;
|
|
member.hardFailureReason =
|
|
member.hardFailureReason ?? 'Teammate was never spawned during launch.';
|
|
member.launchState = deriveMemberLaunchState(member);
|
|
member.diagnostics = buildDiagnostics(member);
|
|
}
|
|
}
|
|
}
|
|
|
|
const summary = summarizePersistedLaunchMembers(expectedMembers, members);
|
|
return {
|
|
version: 2,
|
|
teamName: params.teamName,
|
|
updatedAt,
|
|
...(params.leadSessionId ? { leadSessionId: params.leadSessionId } : {}),
|
|
launchPhase,
|
|
expectedMembers,
|
|
...(bootstrapExpectedMembers.length > 0 &&
|
|
bootstrapExpectedMembers.join('\u0000') !== expectedMembers.join('\u0000')
|
|
? { bootstrapExpectedMembers }
|
|
: {}),
|
|
members,
|
|
summary,
|
|
teamLaunchState: deriveTeamLaunchAggregateState(summary),
|
|
};
|
|
}
|
|
|
|
export function snapshotFromRuntimeMemberStatuses(params: {
|
|
teamName: string;
|
|
expectedMembers: readonly string[];
|
|
leadSessionId?: string;
|
|
launchPhase?: PersistedTeamLaunchPhase;
|
|
statuses: Record<string, RuntimeMemberSpawnState>;
|
|
updatedAt?: string;
|
|
}): PersistedTeamLaunchSnapshot {
|
|
const updatedAt = params.updatedAt ?? new Date().toISOString();
|
|
const members: Record<string, PersistedTeamLaunchMemberState> = {};
|
|
|
|
for (const expected of params.expectedMembers) {
|
|
const name = normalizeMemberName(expected);
|
|
if (!name || name === 'user' || isLeadMember({ name })) continue;
|
|
const runtime = params.statuses[name];
|
|
const sources: PersistedTeamLaunchMemberSources = {};
|
|
if (runtime?.livenessSource === 'heartbeat') {
|
|
sources.nativeHeartbeat = true;
|
|
sources.inboxHeartbeat = true;
|
|
}
|
|
const skippedForLaunch =
|
|
runtime?.skippedForLaunch === true || runtime?.launchState === 'skipped_for_launch';
|
|
const runtimeAlive = skippedForLaunch ? false : preservesStrongRuntimeAlive(runtime);
|
|
if (runtime?.livenessSource === 'process' && runtimeAlive) {
|
|
sources.processAlive = true;
|
|
}
|
|
const launchState = runtime?.launchState ?? 'starting';
|
|
const hardFailure =
|
|
runtime?.launchState === 'skipped_for_launch'
|
|
? false
|
|
: runtime?.hardFailure === true || launchState === 'failed_to_start';
|
|
const entry: PersistedTeamLaunchMemberState = {
|
|
name,
|
|
launchState,
|
|
skippedForLaunch,
|
|
skipReason: runtime?.skipReason,
|
|
skippedAt: runtime?.skippedAt,
|
|
agentToolAccepted: skippedForLaunch ? false : runtime?.agentToolAccepted === true,
|
|
runtimeAlive,
|
|
bootstrapConfirmed: skippedForLaunch ? false : runtime?.bootstrapConfirmed === true,
|
|
hardFailure,
|
|
hardFailureReason: hardFailure ? (runtime?.hardFailureReason ?? runtime?.error) : undefined,
|
|
pendingPermissionRequestIds: runtime?.pendingPermissionRequestIds?.length
|
|
? [...new Set(runtime.pendingPermissionRequestIds)]
|
|
: undefined,
|
|
livenessKind: runtime?.livenessKind,
|
|
runtimeDiagnostic: runtime?.runtimeDiagnostic,
|
|
runtimeDiagnosticSeverity: runtime?.runtimeDiagnosticSeverity,
|
|
bootstrapStalled: runtime?.bootstrapStalled === true,
|
|
runtimeLastSeenAt: runtime?.livenessLastCheckedAt,
|
|
firstSpawnAcceptedAt: runtime?.firstSpawnAcceptedAt,
|
|
lastHeartbeatAt: runtime?.lastHeartbeatAt,
|
|
lastRuntimeAliveAt: runtimeAlive ? updatedAt : undefined,
|
|
lastEvaluatedAt: runtime?.updatedAt ?? updatedAt,
|
|
sources: Object.values(sources).some(Boolean) ? sources : undefined,
|
|
diagnostics: undefined,
|
|
};
|
|
entry.launchState = deriveMemberLaunchState(entry);
|
|
if (!isPersistedBootstrapStalled(entry)) {
|
|
entry.bootstrapStalled = undefined;
|
|
}
|
|
entry.diagnostics = buildDiagnostics(entry);
|
|
members[name] = entry;
|
|
}
|
|
|
|
return createPersistedLaunchSnapshot({
|
|
teamName: params.teamName,
|
|
expectedMembers: params.expectedMembers,
|
|
leadSessionId: params.leadSessionId,
|
|
launchPhase: params.launchPhase,
|
|
members,
|
|
updatedAt,
|
|
});
|
|
}
|
|
|
|
export function snapshotToMemberSpawnStatuses(
|
|
snapshot: PersistedTeamLaunchSnapshot | null
|
|
): Record<string, MemberSpawnStatusEntry> {
|
|
if (!snapshot) return {};
|
|
const statuses: Record<string, MemberSpawnStatusEntry> = {};
|
|
const memberNames = Array.from(
|
|
new Set([
|
|
...snapshot.expectedMembers.map(normalizeMemberName).filter(Boolean),
|
|
...Object.keys(snapshot.members).map(normalizeMemberName).filter(Boolean),
|
|
])
|
|
);
|
|
for (const memberName of memberNames) {
|
|
const entry = snapshot.members[memberName];
|
|
if (!entry) continue;
|
|
let status: MemberSpawnStatusEntry['status'] = 'offline';
|
|
let livenessSource: MemberSpawnLivenessSource | undefined;
|
|
const skippedForLaunch =
|
|
entry.launchState === 'skipped_for_launch' || entry.skippedForLaunch === true;
|
|
const runtimeAlive = skippedForLaunch ? false : preservesStrongRuntimeAlive(entry);
|
|
const openCodeBootstrapPending = isOpenCodeSecondaryBootstrapPending(entry);
|
|
const bootstrapStalled = isPersistedBootstrapStalled(entry);
|
|
if (entry.launchState === 'failed_to_start') {
|
|
status = 'error';
|
|
} else if (entry.launchState === 'skipped_for_launch') {
|
|
status = 'skipped';
|
|
} else if (entry.launchState === 'confirmed_alive') {
|
|
status = 'online';
|
|
livenessSource = 'heartbeat';
|
|
} else if (
|
|
entry.launchState === 'runtime_pending_permission' ||
|
|
entry.launchState === 'runtime_pending_bootstrap'
|
|
) {
|
|
status =
|
|
runtimeAlive && !openCodeBootstrapPending && !bootstrapStalled ? 'online' : 'waiting';
|
|
livenessSource =
|
|
runtimeAlive && !openCodeBootstrapPending && !bootstrapStalled ? 'process' : undefined;
|
|
} else {
|
|
status = entry.agentToolAccepted ? 'waiting' : 'spawning';
|
|
}
|
|
statuses[memberName] = {
|
|
status,
|
|
launchState: entry.launchState,
|
|
error: entry.hardFailure ? entry.hardFailureReason : undefined,
|
|
hardFailureReason: entry.hardFailureReason,
|
|
skippedForLaunch: entry.skippedForLaunch,
|
|
skipReason: entry.skipReason,
|
|
skippedAt: entry.skippedAt,
|
|
livenessSource,
|
|
agentToolAccepted: skippedForLaunch ? false : entry.agentToolAccepted,
|
|
runtimeAlive,
|
|
bootstrapConfirmed: skippedForLaunch ? false : entry.bootstrapConfirmed,
|
|
hardFailure: skippedForLaunch ? false : entry.hardFailure,
|
|
pendingPermissionRequestIds: entry.pendingPermissionRequestIds,
|
|
bootstrapEvidenceSource: entry.bootstrapEvidenceSource,
|
|
bootstrapMode: entry.bootstrapMode,
|
|
appManagedBootstrapCandidate: entry.appManagedBootstrapCandidate,
|
|
livenessKind: entry.livenessKind,
|
|
runtimeDiagnostic: entry.runtimeDiagnostic,
|
|
runtimeDiagnosticSeverity: entry.runtimeDiagnosticSeverity,
|
|
bootstrapStalled,
|
|
livenessLastCheckedAt: entry.runtimeLastSeenAt ?? entry.lastEvaluatedAt,
|
|
firstSpawnAcceptedAt: entry.firstSpawnAcceptedAt,
|
|
lastHeartbeatAt: entry.lastHeartbeatAt,
|
|
updatedAt: entry.lastEvaluatedAt,
|
|
};
|
|
}
|
|
return statuses;
|
|
}
|
|
|
|
export function normalizePersistedLaunchSnapshot(
|
|
teamName: string,
|
|
parsed: unknown
|
|
): PersistedTeamLaunchSnapshot | null {
|
|
if (!parsed || typeof parsed !== 'object') {
|
|
return null;
|
|
}
|
|
|
|
const maybeLegacy = parsed as LegacyPartialLaunchStateFile;
|
|
if (maybeLegacy.state === 'partial_launch_failure') {
|
|
const expectedMembers = Array.isArray(maybeLegacy.expectedMembers)
|
|
? maybeLegacy.expectedMembers.filter(
|
|
(name): name is string => typeof name === 'string' && normalizeMemberName(name).length > 0
|
|
)
|
|
: [];
|
|
const confirmedMembers = Array.isArray(maybeLegacy.confirmedMembers)
|
|
? maybeLegacy.confirmedMembers.filter(
|
|
(name): name is string => typeof name === 'string' && normalizeMemberName(name).length > 0
|
|
)
|
|
: [];
|
|
const missingMembers = Array.isArray(maybeLegacy.missingMembers)
|
|
? maybeLegacy.missingMembers.filter(
|
|
(name): name is string => typeof name === 'string' && normalizeMemberName(name).length > 0
|
|
)
|
|
: [];
|
|
if (expectedMembers.length === 0 || missingMembers.length === 0) {
|
|
return null;
|
|
}
|
|
const updatedAt =
|
|
typeof maybeLegacy.updatedAt === 'string' ? maybeLegacy.updatedAt : new Date().toISOString();
|
|
const members: Record<string, PersistedTeamLaunchMemberState> = {};
|
|
for (const name of expectedMembers) {
|
|
const failed = missingMembers.includes(name);
|
|
const confirmed = confirmedMembers.includes(name);
|
|
const entry: PersistedTeamLaunchMemberState = {
|
|
name,
|
|
launchState: failed ? 'failed_to_start' : confirmed ? 'confirmed_alive' : 'starting',
|
|
agentToolAccepted: true,
|
|
runtimeAlive: false,
|
|
bootstrapConfirmed: confirmed,
|
|
hardFailure: failed,
|
|
hardFailureReason: failed
|
|
? 'Legacy partial launch marker reported teammate missing.'
|
|
: undefined,
|
|
lastEvaluatedAt: updatedAt,
|
|
diagnostics: undefined,
|
|
};
|
|
entry.diagnostics = buildDiagnostics(entry);
|
|
members[name] = entry;
|
|
}
|
|
return createPersistedLaunchSnapshot({
|
|
teamName,
|
|
expectedMembers,
|
|
leadSessionId:
|
|
typeof maybeLegacy.leadSessionId === 'string' && maybeLegacy.leadSessionId.trim().length > 0
|
|
? maybeLegacy.leadSessionId.trim()
|
|
: undefined,
|
|
launchPhase: 'reconciled',
|
|
members,
|
|
updatedAt,
|
|
});
|
|
}
|
|
|
|
const record = parsed as Record<string, unknown>;
|
|
if (record.version !== 2) {
|
|
return null;
|
|
}
|
|
const expectedMembers = Array.isArray(record.expectedMembers)
|
|
? record.expectedMembers.filter(
|
|
(name): name is string => typeof name === 'string' && normalizeMemberName(name).length > 0
|
|
)
|
|
: [];
|
|
const bootstrapExpectedMembers = Array.isArray(record.bootstrapExpectedMembers)
|
|
? record.bootstrapExpectedMembers.filter(
|
|
(name): name is string => typeof name === 'string' && normalizeMemberName(name).length > 0
|
|
)
|
|
: undefined;
|
|
const updatedAt =
|
|
typeof record.updatedAt === 'string' && record.updatedAt.trim().length > 0
|
|
? record.updatedAt
|
|
: new Date().toISOString();
|
|
const normalizedMembers: Record<string, PersistedTeamLaunchMemberState> = {};
|
|
const rawMembers =
|
|
record.members && typeof record.members === 'object'
|
|
? (record.members as Record<string, unknown>)
|
|
: {};
|
|
for (const [memberName, value] of Object.entries(rawMembers)) {
|
|
const normalized = normalizePersistedMemberState(memberName, value, updatedAt);
|
|
if (!normalized) continue;
|
|
normalizedMembers[normalized.name] = normalized;
|
|
}
|
|
return createPersistedLaunchSnapshot({
|
|
teamName:
|
|
typeof record.teamName === 'string' && record.teamName.trim().length > 0
|
|
? record.teamName.trim()
|
|
: teamName,
|
|
expectedMembers,
|
|
leadSessionId:
|
|
typeof record.leadSessionId === 'string' && record.leadSessionId.trim().length > 0
|
|
? record.leadSessionId.trim()
|
|
: undefined,
|
|
launchPhase:
|
|
record.launchPhase === 'active' ||
|
|
record.launchPhase === 'finished' ||
|
|
record.launchPhase === 'reconciled'
|
|
? record.launchPhase
|
|
: 'finished',
|
|
bootstrapExpectedMembers,
|
|
members: normalizedMembers,
|
|
updatedAt,
|
|
});
|
|
}
|