fix(native): let a follow-up wait out the previous turn's warm release instead of failing busy (#20068)

## Thinking Path

- With a warm lifecycle, one session serves consecutive turns on a task.
A follow-up comment wakes the next run, which claims the warm session's
instruction copy and reuses the session.
- #20048 makes warm the default on remote sandboxes. Its merge condition
includes "no restart or follow-up ever leaves a task needing Retry".
- In the #20048 Daytona chaos wave, task t12's follow-up run was created
43 ms after the previous run settled. The previous run was still
releasing its warm session, so `claimWarmNativeInstructionCopy` saw it
busy and failed the run with `native_session_supervisor_busy`. The task
then needed a Retry.
- The release takes seconds and always finishes, so a follow-up should
wait for it rather than fail. A session busy because another run is
preparing or executing a turn is a genuine conflict and must still fail
at once, as existing tests require.

## Linked Issues or Issue Description

Refs #20048

**What happened?**
Wave w-20261108T142906-s9501, t12 (claude, warm): run 5b839da4 succeeded
at 14:46:36.224. The follow-up run f2bf2f05 was created at 36.267 and
failed immediately with `adapter_failed` /
`native_session_supervisor_busy`, from `claimWarmNativeInstructionCopy`
called by `instruction-working-copy.prepare`. Its automatic retry also
failed, and only a user Retry completed the task.

**Steps to reproduce**
1. Run a warm native session turn to completion.
2. While that run is still releasing its warm session (post-turn
instruction check, release, hand-back), admit the next run on the same
session.
3. The next run's instruction-copy claim throws
`native_session_supervisor_busy`.

**Expected behavior**
The follow-up waits for the previous run's release (bounded) and reuses
the session. A session held by a run that is still preparing or
executing fails admission at once, as before.

## What Changed

- `server/src/services/native-runtime/native-session-executor.ts`:
- When the provider turn ends, the executor records the session scope as
settling (`settlingNativeSessionScopes`). The outer `finally` clears the
mark once the run has handed every scope back.
- `claimWarmNativeInstructionCopy` waits, polling every 25 ms for at
most 30 s (`warmSessionReleaseWait`), while the warm entry is busy and
its scope is settling. It then re-reads the entry and runs the existing
checks. If the entry is still busy at the deadline, or busy for any
other reason, it throws `native_session_supervisor_busy` as before.
- No wait happens on the normal path (entry idle), so turn admission
adds no latency.
- `server/src/services/native-runtime/native-session-executor.test.ts`:
two cases.
- A follow-up claimed while the previous turn's post-turn check is held:
it waits, then succeeds once the release finishes. It fails on master
with the wave's error.
  - The release outlasts the bound: it fails busy.

**Previous-release state:** the change touches only in-memory admission
state, so no saved run or session state changes.

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-session-executor.test.ts
src/services/heartbeat/instruction-working-copy.test.ts`: 1301/1301
pass.
- With the wait removed (behaviour on master), the "release finishes"
case fails with `native_session_supervisor_busy`.
- The existing concurrency tests ("fences owned instruction preparation:
concurrent", and the settled-release ownership tests) still fail
admission at once.
- Not yet covered: the Daytona warm wave rerun for #20048.

## Risks

- **A wait that never ends.** The wait is bounded at 30 s and happens
only while the owner is past its turn, which always reaches the outer
`finally`. Every caller of the inner executor runs inside that `try`,
and attachment staging keeps the same session scope.
- **Hiding a real conflict.** Only a settling owner is waited for. A
preparing or executing owner fails at once, as the existing tests
assert.
- **Scope.** I tried a similar wait at execution admission, but it
conflicted with deliberate fences that reject a run during a settled
release (the existing "releases settled codex only under its current
owner" tests). It is not part of this PR.

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`, Anthropic), 1M context window, tool
use and code execution, via Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 authored and GitHub committed 2026-11-08 13:57:32 -06:00
1 parent c3c226d165
commit d007afbf86
2 files changed
+244 -73

No files matched your search

@@ -393,6 +393,7 @@ import {
cancelNativeSession,
closeWarmNativeSessionsForEnvironment,
claimWarmNativeInstructionCopy,
warmSessionReleaseWait,
nativeSessionWorkspaceScope,
reserveWarmNativeInstructionDirectory,
closeIdleWarmNativeSessionsForRestart,
@@ -10460,6 +10461,137 @@ describe("native warm session supervision", () => {
}
}, 90_000);
it.each([["finishes", true], ["outlasts the wait", false]] as const)(
"a follow-up admitted while the previous turn's warm release is still running waits for it when the release %s",
async (_label, releaseFinishes) => {
const identity = `warm-release-wait-${releaseFinishes}`;
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 armPostTurnHold = false, holdPostTurnCheck = false;
state.execute.mockReset().mockImplementation(async (options) => {
await options.onSession?.(session);
// Only checks after the provider turn are held: that is the release window.
if (armPostTurnHold) holdPostTurnCheck = true;
return { result: { summary: "completed" }, terminal: { runTerminalState: "succeeded" },
turnId: identity, normalizedSessionId: identity, providerSessionId: identity,
driverKind: "test", driverVersion: "1", nativeEventCount: 1, highestContiguousSourceSeq: 1, usage: null };
});
let releaseGate!: () => void, settling!: () => void;
const gate = new Promise<void>(resolve => { releaseGate = resolve; });
const reachedSettle = new Promise<void>(resolve => { settling = resolve; });
const copy = (runId: string, slow = false) => ({ runId, root: `/tmp/${identity}`, preparationKey: "same",
// The previous turn's post-turn check runs while it still holds the session.
hasChanges: async () => { if (slow && holdPostTurnCheck) { settling(); await gate; } return false; },
collectStopped: async () => undefined });
const original = { ...warmSessionReleaseWait };
warmSessionReleaseWait.timeoutMs = releaseFinishes ? 5_000 : 200;
try {
await executePaperclipNativeSession({ db: leaseDb(warmExecution), execution: warmExecution, runnerInstanceId: identity,
runnerExecutionTarget: { kind: "remote", transport: "sandbox", environmentId: identity, remoteCwd: `/tmp/${identity}` },
instructionWorkingCopy: copy(identity) });
const second = { ...warmExecution, binding: { ...warmExecution.binding, runId: `${identity}-second` } };
const secondCopy = copy(second.binding.runId, true);
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");
armPostTurnHold = true;
const previous = executePaperclipNativeSession({ db: leaseDb(second), execution: second, runnerInstanceId: identity,
runnerExecutionTarget: { kind: "remote", transport: "sandbox", environmentId: identity, remoteCwd: `/tmp/${identity}` },
instructionWorkingCopy: secondCopy });
await reachedSettle;
// A follow-up wakes 43 ms after the turn settled, while its release runs.
const followUpRunId = `${identity}-follow-up`;
const followUp = 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 () => copy(followUpRunId) });
if (releaseFinishes) {
setTimeout(releaseGate, 100);
const release = await followUp;
expect(release).toBeTypeOf("function");
// The follow-up's own execution is admitted at once: the previous owner
// handed every scope back before the claim returned.
const followUpExecution = { ...second, binding: { ...second.binding, runId: followUpRunId } };
await expect(executePaperclipNativeSession({ db: leaseDb(followUpExecution), execution: followUpExecution,
runnerInstanceId: identity, runnerExecutionTarget: { kind: "remote", transport: "sandbox", environmentId: identity,
remoteCwd: `/tmp/${identity}` }, instructionWorkingCopy: copy(followUpRunId) })).resolves.toBeDefined();
await previous;
await release!();
} else {
await expect(followUp).rejects.toThrow("native_session_supervisor_busy");
releaseGate();
}
await previous;
} finally {
Object.assign(warmSessionReleaseWait, original);
releaseGate();
await closeWarmNativeSessionsForEnvironment({ environmentId: identity, reason: "test cleanup" });
}
});
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");
@@ -738,6 +738,26 @@ export function readNativeSessionIdleWork(): IdleLocalWork {
}
registerIdleLocalWorkProbe(readNativeSessionIdleWork);
/**
* How long a new run's admission waits for the warm session to stop being busy.
* A follow-up comment can wake the next run within milliseconds of the previous
* run settling, while that run's warm release is still finishing. Waiting out
* the release lets the follow-up reuse the session instead of failing. A
* session still busy at the deadline is a real concurrent owner and fails as
* before.
*/
export const warmSessionReleaseWait = { timeoutMs: 30_000, pollMs: 25 };
async function waitForWarmNativeSessionSettled(scope: string): Promise<void> {
const deadline = Date.now() + warmSessionReleaseWait.timeoutMs;
// Wait while the owner's turn is over and it is releasing the session, until
// it has handed every scope back (its execution's final cleanup), so the
// follow-up's own admission does not meet the old owner's scopes either.
while (settlingNativeSessionScopes.has(scope) && Date.now() < deadline) {
await new Promise(resolve => setTimeout(resolve, warmSessionReleaseWait.pollMs));
}
}
/** Reserve the existing physical copy before heartbeat composes AGENT_HOME and
* task guidance. The final execution/configuration fence still owns admission. */
export async function claimWarmNativeInstructionCopy(input: {
@@ -759,6 +779,9 @@ export async function claimWarmNativeInstructionCopy(input: {
binding: { ...prior.binding, runId: input.runId, executionWorkspaceId: input.executionWorkspaceId }, workspace: input.workspace,
})) || prior.workspace.cwd !== input.workspace.cwd) return null;
const scope = nativeSessionScopeKey(prior);
const busyEntry = warmNativeSessions.get(scope);
if (busyEntry?.busy || settlingNativeSessionScopes.has(scope)) await waitForWarmNativeSessionSettled(scope);
// Re-read after the wait: the release may have retired or replaced the session.
const entry = warmNativeSessions.get(scope);
if (!entry || !entry.instructionWorkingCopy?.runId || entry.environmentId !== input.environmentId) return null;
if (entry.busy) throw new Error("native_session_supervisor_busy");
@@ -1982,6 +2005,9 @@ const initializingSessionToolAuthorities = new Set<string>();
const executingRunnerdSessionScopes = new Map<string, string>();
const executingNativeOwnerScopes = new Map<string, symbol>();
const executingNativeIngressAllocations = new Map<string, symbol>();
/** Session scope -> the run releasing it after its turn (warm release and
* hand-back), which a follow-up's instruction claim may wait for. */
const settlingNativeSessionScopes = new Map<string, string>();
export function nativeSessionScopeDigestIsBusy(digest: string): boolean {
const scopes = [
@@ -9366,84 +9392,90 @@ export async function executePaperclipNativeSession(input: {
}
throw error;
} finally {
startup.providerPackSourceAbort.abort(new Error("native_execution_startup_completed"));
for (const lease of diagnosticLeases) lease.retire();
let startupUnsettled = false;
if (startup.executionContext) {
if (startup.executionContext.active) startup.executionContext.abort(new Error("native_execution_startup_completed"));
startupUnsettled = !(await startup.executionContext.drain());
startup.executionContext.finish();
}
// Join preparation cleanup before release; keep cancellation reachable
// until the exact retained owner has finished the handover.
await startup.executionLeaseRenewal?.stop().catch(() => undefined);
if (startup.unstartedClaim && !nativeRunsDetachingForRestart.has(runId)) {
await releaseUnstartedNativeExecutionClaim({ db: input.db, runId, companyId: input.execution.binding.companyId,
issueId: input.execution.binding.issueId, ...startup.unstartedClaim }).catch(error => {
logger.warn({ err: error, runId }, "Native preparation claim release failed; the control deadline will settle it");
});
}
let stagedCleanupFailed = false;
try {
await cleanupStagedAttachments();
} catch (cleanupError) {
stagedCleanupFailed = true;
// Cleanup is a confidentiality incident, but it occurs after the native
// provider may already have completed the turn. Reclassifying that turn
// as failed could replay provider side effects. Emit a private runtime
// health event while preserving the provider result/error disposition.
await input
.onEvent?.({
eventType: "native.attachment_staging_cleanup_failed",
level: "error",
message: "Native inbound attachment staging cleanup failed.",
payload: {
runId: input.execution.binding.runId,
issueId: input.execution.binding.issueId,
executionAlreadyFailed: executionFailure !== undefined,
},
})
.catch(() => undefined);
}
releaseRetainedNativeSessionCleanup(input.db, {
companyId: input.execution.binding.companyId, agentId: input.execution.binding.agentId,
}, { companyId: input.execution.binding.companyId, runId: input.execution.binding.runId,
issueId: input.execution.binding.issueId });
const pendingRelease = ownsSessionScope && sessionScopeId
&& executingRunnerdSessionScopes.get(sessionScopeId) === runId
? warmNativeSessions.get(sessionScopeId) : undefined;
try {
if (pendingRelease?.runId === runId && pendingRelease.finishRelease) {
if (executionFailure !== undefined || startupUnsettled || stagedCleanupFailed) {
pendingRelease.closeOnReleaseReason = "warm native session final cleanup failed";
startup.providerPackSourceAbort.abort(new Error("native_execution_startup_completed"));
for (const lease of diagnosticLeases) lease.retire();
let startupUnsettled = false;
if (startup.executionContext) {
if (startup.executionContext.active) startup.executionContext.abort(new Error("native_execution_startup_completed"));
startupUnsettled = !(await startup.executionContext.drain());
startup.executionContext.finish();
}
// Join preparation cleanup before release; keep cancellation reachable
// until the exact retained owner has finished the handover.
await startup.executionLeaseRenewal?.stop().catch(() => undefined);
if (startup.unstartedClaim && !nativeRunsDetachingForRestart.has(runId)) {
await releaseUnstartedNativeExecutionClaim({ db: input.db, runId, companyId: input.execution.binding.companyId,
issueId: input.execution.binding.issueId, ...startup.unstartedClaim }).catch(error => {
logger.warn({ err: error, runId }, "Native preparation claim release failed; the control deadline will settle it");
});
}
let stagedCleanupFailed = false;
try {
await cleanupStagedAttachments();
} catch (cleanupError) {
stagedCleanupFailed = true;
// Cleanup is a confidentiality incident, but it occurs after the native
// provider may already have completed the turn. Reclassifying that turn
// as failed could replay provider side effects. Emit a private runtime
// health event while preserving the provider result/error disposition.
await input
.onEvent?.({
eventType: "native.attachment_staging_cleanup_failed",
level: "error",
message: "Native inbound attachment staging cleanup failed.",
payload: {
runId: input.execution.binding.runId,
issueId: input.execution.binding.issueId,
executionAlreadyFailed: executionFailure !== undefined,
},
})
.catch(() => undefined);
}
releaseRetainedNativeSessionCleanup(input.db, {
companyId: input.execution.binding.companyId, agentId: input.execution.binding.agentId,
}, { companyId: input.execution.binding.companyId, runId: input.execution.binding.runId,
issueId: input.execution.binding.issueId });
const pendingRelease = ownsSessionScope && sessionScopeId
&& executingRunnerdSessionScopes.get(sessionScopeId) === runId
? warmNativeSessions.get(sessionScopeId) : undefined;
try {
if (pendingRelease?.runId === runId && pendingRelease.finishRelease) {
if (executionFailure !== undefined || startupUnsettled || stagedCleanupFailed) {
pendingRelease.closeOnReleaseReason = "warm native session final cleanup failed";
}
await pendingRelease.finishRelease();
}
} finally {
if (ownsPhysicalIngress && physicalIngressKey && executingNativeIngressAllocations.get(physicalIngressKey) === ownerToken) {
executingNativeIngressAllocations.delete(physicalIngressKey);
}
if (executingNativeOwnerScopes.get(ownerScope) === ownerToken) {
executingNativeOwnerScopes.delete(ownerScope);
}
if (
ownsSessionScope &&
sessionScopeId !== null &&
executingRunnerdSessionScopes.get(sessionScopeId) ===
input.execution.binding.runId
) {
executingRunnerdSessionScopes.delete(sessionScopeId);
}
startup.resolve(null);
startup.settle();
if (nativeSessionStartups.get(runId) === startup) {
nativeSessionStartups.delete(runId);
setNativeExecutionActive(runId, false);
}
await pendingRelease.finishRelease();
}
if (startupUnsettled) throw new NativeStartupCleanupPendingError(startup.executionContext!.id,
startup.executionContext!.deadlineAtMs, startup.leaseOwner!, startup.executionContext!.pluginSupported,
input.runnerExecutionTarget?.kind === "remote" ? input.runnerExecutionTarget.leaseId ?? undefined : undefined);
} finally {
if (ownsPhysicalIngress && physicalIngressKey && executingNativeIngressAllocations.get(physicalIngressKey) === ownerToken) {
executingNativeIngressAllocations.delete(physicalIngressKey);
}
if (executingNativeOwnerScopes.get(ownerScope) === ownerToken) {
executingNativeOwnerScopes.delete(ownerScope);
}
if (
ownsSessionScope &&
sessionScopeId !== null &&
executingRunnerdSessionScopes.get(sessionScopeId) ===
input.execution.binding.runId
) {
executingRunnerdSessionScopes.delete(sessionScopeId);
}
startup.resolve(null);
startup.settle();
if (nativeSessionStartups.get(runId) === startup) {
nativeSessionStartups.delete(runId);
setNativeExecutionActive(runId, false);
}
// Always hand the settling mark back, even if a cleanup step throws.
const settledScope = nativeSessionScopeKey(input.execution);
if (settlingNativeSessionScopes.get(settledScope) === runId) settlingNativeSessionScopes.delete(settledScope);
}
if (startupUnsettled) throw new NativeStartupCleanupPendingError(startup.executionContext!.id,
startup.executionContext!.deadlineAtMs, startup.leaseOwner!, startup.executionContext!.pluginSupported,
input.runnerExecutionTarget?.kind === "remote" ? input.runnerExecutionTarget.leaseId ?? undefined : undefined);
}
}
@@ -11362,6 +11394,9 @@ async function executePaperclipNativeSessionWithinScope(
clearSteeringDeliveries(input.execution.binding.runId);
clearNativeRuntimeRequestResolutions(input.execution.binding.runId);
} catch (error) {
// A failed turn releases its warm session here, before the finally below:
// mark the scope settling first so a follow-up waits for that release too.
settlingNativeSessionScopes.set(nativeSessionScopeKey(input.execution), input.execution.binding.runId);
executionContext?.abort(error instanceof Error ? error : new Error("native_execution_startup_failed"));
const startupUnsettled = executionContext ? !(await executionContext.drain()) : false;
if (startupUnsettled) throw new NativeStartupCleanupPendingError(executionContext!.id, executionContext!.deadlineAtMs,
@@ -12024,6 +12059,10 @@ 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());