diff --git a/integration-tests/helpers/hosted-process-crash-driver.ts b/integration-tests/helpers/hosted-process-crash-driver.ts index a1b8556833e..db3accc09b3 100644 --- a/integration-tests/helpers/hosted-process-crash-driver.ts +++ b/integration-tests/helpers/hosted-process-crash-driver.ts @@ -50,6 +50,10 @@ const report = { promptSubmissions: 0, continuationsAfterFailure: 0, postFailureLoads: [] as boolean[], + postFailureCancellationLoads: 0, + // Record non-resuming recovery modes separately from their wire origins. + // Both modes target the original prompt; cleanup alone cannot prove takeover. + postFailureLoadOrigins: [] as string[], lateCancelReplays: 0, originalCancelBody: '', }; @@ -156,8 +160,44 @@ const proxy = createServer(async (req, res) => { `/session/${config.sessionId}/managed-runtime/continue` ) report.continuationsAfterFailure++; - if (injected && target.pathname.endsWith('/load')) - report.postFailureLoads.push(fields.cancellationTakeover === true); + let postFailureLoadIndex: number | undefined; + if (injected && target.pathname.endsWith('/load')) { + const cancellationOnly = + fields.cancellationTakeover === true && + fields.passiveManagedRuntimeRecovery === true && + typeof fields.cancellationPromptId === 'string' && + fields.cancellationPromptId === report.promptId && + fields.recoveryCleanupPromptId === undefined; + const cleanupOnly = + typeof fields.recoveryCleanupPromptId === 'string' && + fields.recoveryCleanupPromptId === report.promptId && + fields.cancellationTakeover !== true && + fields.passiveManagedRuntimeRecovery !== true && + fields.cancellationPromptId === undefined; + const nonDriving = + fields.driveRuntimeRecovery !== true && + fields.mcpManagementOnly !== true && + fields.lifecycleAuthority === undefined; + postFailureLoadIndex = report.postFailureLoads.length; + report.postFailureLoads.push( + nonDriving && (cancellationOnly || cleanupOnly), + ); + if (nonDriving && cancellationOnly) + report.postFailureCancellationLoads++; + report.postFailureLoadOrigins.push( + [ + `takeover=${fields.cancellationTakeover === true}`, + `passive=${fields.passiveManagedRuntimeRecovery === true}`, + `drive=${fields.driveRuntimeRecovery === true}`, + `mcpOnly=${fields.mcpManagementOnly === true}`, + `promptId=${typeof fields.cancellationPromptId === 'string'}`, + `promptMatches=${fields.cancellationPromptId === report.promptId}`, + `cleanup=${typeof fields.recoveryCleanupPromptId === 'string'}`, + `cleanupMatches=${fields.recoveryCleanupPromptId === report.promptId}`, + `lifecycle=${fields.lifecycleAuthority !== undefined}`, + ].join(' '), + ); + } const headers = Object.fromEntries( Object.entries(req.headers) .filter( @@ -181,6 +221,10 @@ const proxy = createServer(async (req, res) => { headers, ...(body.length ? { body } : {}), }); + if (postFailureLoadIndex !== undefined) { + report.postFailureLoadOrigins[postFailureLoadIndex] += + ` status=${upstream.status}`; + } if (target.pathname.endsWith('/events') && upstream.body) { res.writeHead(upstream.status, relayedHeaders(upstream.headers)); const reader = upstream.body.getReader(); @@ -679,10 +723,17 @@ async function runRecoverySettlement() { assert.equal(report.modelCalls, 1); assert.equal(report.promptSubmissions, 1); assert.equal(report.continuationsAfterFailure, 0); - assert(report.postFailureLoads.length > 0); + assert( + report.postFailureLoads.length > 0, + `no post-failure loads were recorded at all: ${JSON.stringify(report.postFailureLoadOrigins)}`, + ); + assert( + report.postFailureCancellationLoads > 0, + 'replacement must use a targeted cancellation takeover load', + ); assert( report.postFailureLoads.every(Boolean), - 'replacement must use cancellation-only loads', + `replacement must use targeted cancellation or cleanup-only loads: ${report.postFailureLoadOrigins.join(' | ')}`, ); await unlink(`${proof}.read-gate`); await waitUntil(async () => { diff --git a/integration-tests/helpers/hosted-sse-gap-driver.ts b/integration-tests/helpers/hosted-sse-gap-driver.ts index b2057752b6c..8ff0377db97 100644 --- a/integration-tests/helpers/hosted-sse-gap-driver.ts +++ b/integration-tests/helpers/hosted-sse-gap-driver.ts @@ -304,15 +304,12 @@ try { .join(''), 'FG6e tool finished once.', ); - // Progress frames materialize as transcript items, not transcript events. - const itemFrames = new Set([ - 'item.output_text.delta', - 'item.tool_call.updated', - ]); + // Message pages carry selected Turn control events; full Session history + // and progress frames are checked against the SQL ledger above. assert.deepEqual( transcript.events, webFrames - .filter((frame) => !itemFrames.has(frame.event)) + .filter((frame) => frame.event === 'turn.completed') .map((frame) => frame.data), ); assert.equal(