fix(native): mark the scope settling after a failed turn too; test the post-hand-back window

Review follow-up: the settling mark was set only after a successful turn, so a
failed turn's scopes could still meet a follow-up's admission; it is now set in
the finally around the turn. The new test holds the previous run's coordinator
release (after the warm hand-back, before its scopes are released) and fails on
the earlier fix with native_session_supervisor_busy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 committed 2026-11-08 10:56:01 -06:00
1 parent 7ccd659636
commit 9c0562515c
2 files changed
+64 -3

No files matched your search

@@ -10497,6 +10497,66 @@ describe("native warm session supervision", () => {
}
});
it("admits the follow-up's execution only after the previous owner's final cleanup hands its scopes back", async () => {
const identity = "warm-release-scope-handback";
const warmExecution = { ...execution, binding: { ...execution.binding, runId: identity, executionWorkspaceId: `${identity}-workspace` },
session: { ...execution.session, normalizedSessionId: identity, lifecyclePolicy: { mode: "warm" as const, idleTimeoutMs: 60_000 } },
} as NativeExecutionInputV1;
const session = { close: vi.fn(async () => undefined) };
let turnDone = false;
state.execute.mockReset().mockImplementation(async (options) => {
await options.onSession?.(session);
turnDone = true;
return { result: { summary: "completed" }, terminal: { runTerminalState: "succeeded" },
turnId: identity, normalizedSessionId: identity, providerSessionId: identity,
driverKind: "test", driverVersion: "1", nativeEventCount: 1, highestContiguousSourceSeq: 1, usage: null };
});
const copy = (runId: string) => ({ runId, root: `/tmp/${identity}`, preparationKey: "same", hasChanges: async () => false,
collectStopped: async () => undefined });
const target = { kind: "remote" as const, transport: "sandbox" as const, environmentId: identity, remoteCwd: `/tmp/${identity}` };
try {
await executePaperclipNativeSession({ db: leaseDb(warmExecution), execution: warmExecution, runnerInstanceId: identity,
runnerExecutionTarget: target, instructionWorkingCopy: copy(identity) });
const second = { ...warmExecution, binding: { ...warmExecution.binding, runId: `${identity}-second` } };
const secondCopy = copy(second.binding.runId);
expect(await claimWarmNativeInstructionCopy({ priorExecution: warmExecution,
companyId: second.binding.companyId, agentId: second.binding.agentId,
executionWorkspaceId: second.binding.executionWorkspaceId, workspace: second.workspace,
environmentId: identity, runId: second.binding.runId, preparationKey: "same", adopt: async () => secondCopy })).toBeTypeOf("function");
// Hold the previous run's post-turn coordinator release: the warm session
// is already handed back, but the run still holds its execution scopes.
let openGate!: () => void, held!: () => void;
const gate = new Promise<void>(resolve => { openGate = resolve; });
const reachedHold = new Promise<void>(resolve => { held = resolve; });
turnDone = false;
const secondDb = leaseDb(second);
const transaction = secondDb.transaction.bind(secondDb);
(secondDb as unknown as { transaction: typeof transaction }).transaction = (async (...args: Parameters<typeof transaction>) => {
if (turnDone) { held(); await gate; }
return transaction(...args);
}) as typeof transaction;
const previous = executePaperclipNativeSession({ db: secondDb, execution: second, runnerInstanceId: identity,
runnerExecutionTarget: target, instructionWorkingCopy: secondCopy });
await reachedHold;
const followUpRunId = `${identity}-follow-up`;
const followUpCopy = copy(followUpRunId);
const claim = claimWarmNativeInstructionCopy({ priorExecution: second,
companyId: second.binding.companyId, agentId: second.binding.agentId,
executionWorkspaceId: second.binding.executionWorkspaceId, workspace: second.workspace,
environmentId: identity, runId: followUpRunId, preparationKey: "same", adopt: async () => followUpCopy });
setTimeout(openGate, 100);
const release = await claim;
const followUp = { ...second, binding: { ...second.binding, runId: followUpRunId } };
// Admitted at once: no native_session_supervisor_busy from the old owner's scopes.
await expect(executePaperclipNativeSession({ db: leaseDb(followUp), execution: followUp, runnerInstanceId: identity,
runnerExecutionTarget: target, ...(release ? { instructionWorkingCopy: followUpCopy } : {}) })).resolves.toBeDefined();
await previous;
await release?.();
} finally {
await closeWarmNativeSessionsForEnvironment({ environmentId: identity, reason: "test cleanup" });
}
});
it.each(["idle", "edit", "add", "remove", "projectless"])("retains the actual agent_files copy across warm turns, then retires for %s", async ending => {
const root = await realpath(await mkdtemp(join(tmpdir(), "warm-agent-files-")));
const localRoot = join(root, "run", "live");
@@ -12036,14 +12036,15 @@ async function executePaperclipNativeSessionWithinScope(
if (protocolIntegrityFailure !== null) throw protocolIntegrityFailure;
}
} finally {
// The provider turn is over, whether it succeeded or threw: what follows is
// release and hand-back, which a follow-up's admission may wait for
// (`warmSessionReleaseWait`). The outer execution's finally clears it.
settlingNativeSessionScopes.set(nativeSessionScopeKey(input.execution), input.execution.binding.runId);
releaseGitHubRun?.();
if (githubAccess && (!warmSessionId || warmNativeSessions.get(warmSessionId)?.githubAccess !== githubAccess)) {
await timeController("github_stop", () => githubAccess!.stop());
}
}
// The provider turn is over: what follows is release and hand-back, which a
// follow-up's admission may wait for (`warmSessionReleaseWait`).
settlingNativeSessionScopes.set(nativeSessionScopeKey(input.execution), input.execution.binding.runId);
let postExecutionFailed = false;
let postExecutionCleanupFailed = false;
try {