mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
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:
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 {
|
||||
|
||||
Reference in new issue
Block a user