agent-ecosystem/src/main/services/team/TeamLaunchStateEvaluator.ts
2026-05-21 16:43:00 +03:00

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,
});
}