fix(team): recover process table launch failures

This commit is contained in:
777genius 2026-05-22 13:34:39 +03:00
parent 3eaa9889ca
commit 169e34ce7c
2 changed files with 329 additions and 8 deletions

View file

@ -3513,6 +3513,36 @@ function isRegisteredRuntimeMetadataFailureReason(reason?: string): boolean {
return reason?.trim() === 'registered runtime metadata without live process';
}
function isProcessTableUnavailableFailureReason(reason?: string): boolean {
const text = reason?.trim();
if (!text || !mentionsProcessTableUnavailable(text)) {
return false;
}
return (
/^process table (?:is )?unavailable$/i.test(text) ||
/^runtime pid could not be verified because process table (?:is )?unavailable$/i.test(text)
);
}
function stripProcessTableUnavailableDiagnosticSuffix(reason: string): string | null {
const match = /^(.*?);\s*process table (?:is )?unavailable$/i.exec(reason.trim());
const baseReason = match?.[1]?.trim();
return baseReason && baseReason.length > 0 ? baseReason : null;
}
function isBaseAutoClearableLaunchFailureReason(reason?: string): boolean {
return (
isNeverSpawnedDuringLaunchReason(reason) ||
isLaunchGraceWindowFailureReason(reason) ||
isConfigRegistrationFailureReason(reason) ||
isRegisteredRuntimeMetadataFailureReason(reason) ||
isOpenCodeBridgeLaunchFailureReason(reason) ||
isBootstrapMcpResourceReadFailureReason(reason) ||
isBootstrapCheckInTimeoutFailureReason(reason) ||
isBootstrapInstructionPromptFailureReason(reason)
);
}
function isBootstrapMcpResourceReadFailureReason(reason?: string): boolean {
const text = reason?.trim().toLowerCase() ?? '';
return (
@ -3539,15 +3569,15 @@ function isTmuxNoServerRunningError(error: unknown): boolean {
}
function isAutoClearableLaunchFailureReason(reason?: string): boolean {
const text = reason?.trim();
if (!text) {
return false;
}
const baseReason = stripProcessTableUnavailableDiagnosticSuffix(text);
return (
isNeverSpawnedDuringLaunchReason(reason) ||
isLaunchGraceWindowFailureReason(reason) ||
isConfigRegistrationFailureReason(reason) ||
isRegisteredRuntimeMetadataFailureReason(reason) ||
isOpenCodeBridgeLaunchFailureReason(reason) ||
isBootstrapMcpResourceReadFailureReason(reason) ||
isBootstrapCheckInTimeoutFailureReason(reason) ||
isBootstrapInstructionPromptFailureReason(reason)
isBaseAutoClearableLaunchFailureReason(text) ||
isProcessTableUnavailableFailureReason(text) ||
(baseReason != null && isBaseAutoClearableLaunchFailureReason(baseReason))
);
}

View file

@ -583,6 +583,26 @@ function createMemberSpawnStatusEntry(
};
}
type TeamProvisioningServicePrivateHarness = {
getLiveTeamAgentRuntimeMetadata: (
teamName: string
) => Promise<Map<string, Record<string, unknown>>>;
attachLiveRuntimeMetadataToStatuses: (
teamName: string,
statuses: Record<string, Record<string, unknown>>,
options?: Record<string, unknown>
) => Promise<Record<string, Record<string, unknown>>>;
applyBootstrapTranscriptEvidenceOverlay: (
snapshot: ReturnType<typeof createPersistedLaunchSnapshot> | null
) => Promise<ReturnType<typeof createPersistedLaunchSnapshot> | null>;
};
function privateHarness(
svc: TeamProvisioningService
): TeamProvisioningServicePrivateHarness {
return svc as unknown as TeamProvisioningServicePrivateHarness;
}
function createMemberSpawnRun(params?: {
runId?: string;
teamName?: string;
@ -19594,6 +19614,95 @@ describe('TeamProvisioningService', () => {
});
});
it('heals process-table unavailable launch-state failures from runtime bootstrap proof', async () => {
allowConsoleLogs();
const teamName = 'zz-unit-process-table-unavailable-proof-heals';
const leadSessionId = 'lead-session';
const projectPath = '/Users/test/proj';
const acceptedAt = new Date(Date.now() - 90_000).toISOString();
const proofAt = new Date(Date.now() - 60_000).toISOString();
const proofToken = 'proof-token-alice';
const bootstrapRunId = 'run-process-table-unavailable';
const runtimePid = 35906;
const runtimeEventsPath = path.join(tempTeamsBase, teamName, 'runtime', 'alice.runtime.jsonl');
const processTableReason =
'runtime pid could not be verified because process table is unavailable';
writeLaunchConfig(teamName, projectPath, leadSessionId, ['alice']);
const configPath = path.join(tempTeamsBase, teamName, 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8')) as {
members: Array<Record<string, unknown>>;
};
config.members = config.members.map((member) =>
member.name === 'alice'
? {
...member,
agentId: `alice@${teamName}`,
backendType: 'process',
tmuxPaneId: `process:${runtimePid}`,
runtimePid,
bootstrapExpectedAfter: acceptedAt,
bootstrapProofToken: proofToken,
bootstrapRunId,
bootstrapRuntimeEventsPath: runtimeEventsPath,
}
: member
);
fs.writeFileSync(configPath, JSON.stringify(config), 'utf8');
const snapshot = createPersistedLaunchSnapshot({
teamName,
leadSessionId,
launchPhase: 'finished',
expectedMembers: ['alice'],
members: {
alice: {
name: 'alice',
launchState: 'failed_to_start',
agentToolAccepted: true,
runtimeAlive: false,
runtimePid,
runtimeRunId: bootstrapRunId,
bootstrapConfirmed: false,
hardFailure: true,
hardFailureReason: processTableReason,
livenessKind: 'registered_only',
runtimeDiagnostic: processTableReason,
runtimeDiagnosticSeverity: 'warning',
firstSpawnAcceptedAt: acceptedAt,
lastEvaluatedAt: acceptedAt,
},
},
});
fs.mkdirSync(path.dirname(runtimeEventsPath), { recursive: true });
fs.writeFileSync(
runtimeEventsPath,
`${JSON.stringify({
version: 1,
type: 'bootstrap_confirmed',
timestamp: proofAt,
pid: runtimePid,
teamName,
agentName: 'alice',
agentId: `alice@${teamName}`,
bootstrapRunId,
source: 'member_briefing_tool_success',
bootstrapProofToken: proofToken,
})}\n`,
'utf8'
);
const svc = new TeamProvisioningService();
const result = await privateHarness(svc).applyBootstrapTranscriptEvidenceOverlay(snapshot);
expect(result?.members.alice).toMatchObject({
launchState: 'confirmed_alive',
bootstrapConfirmed: true,
runtimeAlive: true,
hardFailure: false,
hardFailureReason: undefined,
});
});
it('does not heal terminal bootstrap-state failures from native app-managed proof with mismatched hashes', async () => {
allowConsoleLogs();
const teamName = 'zz-unit-bootstrap-state-native-runtime-proof-hash-mismatch';
@ -22418,6 +22527,141 @@ describe('TeamProvisioningService', () => {
});
});
it('clears registered-only stale failure with process-table diagnostic suffix', async () => {
const svc = new TeamProvisioningService();
const harness = privateHarness(svc);
harness.getLiveTeamAgentRuntimeMetadata = vi.fn(
async () =>
new Map([
[
'tom',
{
alive: true,
model: 'gpt-5.4',
livenessKind: 'runtime_process',
runtimeDiagnostic: 'verified runtime process detected',
},
],
])
);
const reason = 'registered runtime metadata without live process; process table unavailable';
const result = await harness.attachLiveRuntimeMetadataToStatuses('forge-labs-10', {
tom: createMemberSpawnStatusEntry({
status: 'error',
launchState: 'failed_to_start',
error: reason,
hardFailure: true,
hardFailureReason: reason,
livenessKind: 'registered_only',
runtimeDiagnostic: reason,
}),
});
expect(result.tom).toMatchObject({
status: 'online',
launchState: 'runtime_pending_bootstrap',
runtimeAlive: true,
hardFailure: false,
hardFailureReason: undefined,
error: undefined,
runtimeModel: 'gpt-5.4',
livenessKind: 'runtime_process',
runtimeDiagnostic: 'verified runtime process detected',
livenessSource: 'process',
});
});
it('clears process-table unavailable failure when a verified runtime process appears later', async () => {
const svc = new TeamProvisioningService();
const harness = privateHarness(svc);
harness.getLiveTeamAgentRuntimeMetadata = vi.fn(
async () =>
new Map([
[
'alice',
{
alive: true,
model: 'gpt-5.3-codex',
livenessKind: 'runtime_process',
runtimeDiagnostic: 'verified runtime process detected',
},
],
])
);
const result = await harness.attachLiveRuntimeMetadataToStatuses('signal-ops-10', {
alice: createMemberSpawnStatusEntry({
status: 'error',
launchState: 'failed_to_start',
error: 'runtime pid could not be verified because process table is unavailable',
hardFailure: true,
hardFailureReason: 'runtime pid could not be verified because process table is unavailable',
livenessKind: 'registered_only',
runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable',
runtimeDiagnosticSeverity: 'warning',
}),
});
expect(result.alice).toMatchObject({
status: 'online',
launchState: 'runtime_pending_bootstrap',
runtimeAlive: true,
hardFailure: false,
hardFailureReason: undefined,
error: undefined,
runtimeModel: 'gpt-5.3-codex',
livenessKind: 'runtime_process',
runtimeDiagnostic: 'verified runtime process detected',
livenessSource: 'process',
});
});
it('keeps process-table unavailable failure failed when only weak metadata is available', async () => {
const svc = new TeamProvisioningService();
const harness = privateHarness(svc);
harness.getLiveTeamAgentRuntimeMetadata = vi.fn(
async () =>
new Map([
[
'alice',
{
alive: false,
livenessKind: 'registered_only',
runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable',
runtimeDiagnosticSeverity: 'warning',
},
],
])
);
const result = await harness.attachLiveRuntimeMetadataToStatuses('signal-ops-10', {
alice: createMemberSpawnStatusEntry({
status: 'error',
launchState: 'failed_to_start',
error: 'runtime pid could not be verified because process table is unavailable',
hardFailure: true,
hardFailureReason: 'runtime pid could not be verified because process table is unavailable',
livenessKind: 'registered_only',
runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable',
runtimeDiagnosticSeverity: 'warning',
}),
});
expect(result.alice).toMatchObject({
status: 'error',
launchState: 'failed_to_start',
runtimeAlive: false,
hardFailure: true,
hardFailureReason: 'runtime pid could not be verified because process table is unavailable',
error: 'runtime pid could not be verified because process table is unavailable',
livenessKind: 'registered_only',
runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable',
runtimeDiagnosticSeverity: 'warning',
livenessSource: undefined,
});
});
it('does not clear OpenCode bridge launch failure from process-only liveness', async () => {
const svc = new TeamProvisioningService();
(svc as any).getLiveTeamAgentRuntimeMetadata = vi.fn(
@ -22464,6 +22708,53 @@ describe('TeamProvisioningService', () => {
});
});
it('does not clear concrete OpenCode bridge failures even when a process is visible', async () => {
const svc = new TeamProvisioningService();
const harness = privateHarness(svc);
harness.getLiveTeamAgentRuntimeMetadata = vi.fn(
async () =>
new Map([
[
'bob',
{
alive: true,
model: 'openrouter/minimax/minimax-m2.5',
livenessKind: 'runtime_process',
providerId: 'opencode',
runtimeDiagnostic: 'OpenCode runtime process detected',
runtimeDiagnosticSeverity: 'info',
},
],
])
);
const reason =
'OpenCode bridge failed: OpenCode app-managed bootstrap launch requires a fresh capability snapshot before state-changing launch; process table unavailable';
const result = await harness.attachLiveRuntimeMetadataToStatuses('signal-ops-10', {
bob: createMemberSpawnStatusEntry({
status: 'error',
launchState: 'failed_to_start',
error: reason,
hardFailure: true,
hardFailureReason: reason,
}),
});
expect(result.bob).toMatchObject({
status: 'error',
launchState: 'failed_to_start',
runtimeAlive: false,
hardFailure: true,
hardFailureReason: reason,
error: reason,
runtimeModel: 'openrouter/minimax/minimax-m2.5',
livenessKind: 'runtime_process',
runtimeDiagnostic: 'OpenCode runtime process detected',
runtimeDiagnosticSeverity: 'info',
livenessSource: undefined,
});
});
it('maps suffixed live runtime metadata keys back onto canonical spawn statuses', async () => {
const svc = new TeamProvisioningService();
(svc as any).getLiveTeamAgentRuntimeMetadata = vi.fn(