From 078d30049c6e0c384fceb48670b75f9add1fdbfd Mon Sep 17 00:00:00 2001 From: testikun Date: Thu, 3 Sep 2026 14:46:01 +0800 Subject: [PATCH 1/3] fix(runtime-host): detach sandbox graph wake reconciliation Generated-by: OpenAI Codex --- .../__tests__/interaction-coordinator.test.ts | 88 +++++++++++++++++++ .../src/server/interaction-coordinator.ts | 11 ++- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts b/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts index 52ebb394de..91733e905f 100644 --- a/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts @@ -388,6 +388,94 @@ describe('HostInteractionCoordinator', () => { }); }); + test('does not hold Session admission while graph wake reconciliation waits', async () => { + await withStore(async ({ owner, store, stores }) => { + const workspace = join(owner.capability.canonicalPath, 'wake-workspace'); + await mkdir(workspace); + const session = await stores.sessionStore.create({ + cwd: workspace, + llmConnectionId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc', + llmConnectionSlug: 'fake', + model: 'fake-model', + permissionMode: 'ask', + }); + const identity = { ...RUN, sessionId: session.id }; + const wakeStarted = deferred(); + const releaseWake = deferred(); + const wakeFinished = deferred(); + const coordinator = new HostInteractionCoordinator({ + store, + sandboxBoundaries: stores.sessionStore, + sessionAdmission: new SessionAdmissionGate(), + sessions: stores.sessionStore, + preflightSessionSnapshot: () => true, + refreshCanonicalContinuity: async () => {}, + onSandboxBoundarySettled: async () => { + wakeStarted.resolve(); + await releaseWake.promise; + wakeFinished.resolve(); + }, + onPoison: () => {}, + }); + const binding = coordinator.bindRun(identity); + const request = sandboxBoundaryEvent({ + sessionId: session.id, + requestId: 'boundary_wake_wait', + status: 'pending', + baseRevision: 0, + turnId: identity.turnId, + runId: identity.runId, + expansion: { network: { enabled: true } }, + justification: 'Connect to the requested service.', + createdAt: 1, + }); + await binding.acceptSandboxBoundaryRequest({ + request, + continuation: sandboxBoundaryContinuation(identity, request.requestId), + }); + + let answerSettled = false; + let answerResult: + | Awaited> + | undefined; + const answer = coordinator.handlers['interaction.answer']( + { + sessionId: session.id, + interactionId: request.requestId, + answer: { kind: 'sandbox_boundary', decision: 'allow' }, + }, + connection(), + ); + void answer.then( + (result) => { + answerResult = result; + answerSettled = true; + }, + () => { + answerSettled = true; + }, + ); + await wakeStarted.promise; + try { + await new Promise((resolve) => setImmediate(resolve)); + assert.equal( + answerSettled, + true, + 'interaction answer waited for graph wake reconciliation', + ); + assert.equal(answerResult?.ok, true); + if (answerResult?.ok) assert.equal(answerResult.result.status, 'answered'); + } finally { + releaseWake.resolve(); + await wakeFinished.promise; + await answer; + } + await binding.close('turn_terminal'); + binding.release(); + await coordinator.close(); + }); + }); + test('a queued stop waits for sandbox boundary publication before closing its Run', async () => { await withStore(async ({ owner, store, stores }) => { const workspace = join(owner.capability.canonicalPath, 'publication-workspace'); diff --git a/packages/runtime-host/src/server/interaction-coordinator.ts b/packages/runtime-host/src/server/interaction-coordinator.ts index be899c983f..10f0940437 100644 --- a/packages/runtime-host/src/server/interaction-coordinator.ts +++ b/packages/runtime-host/src/server/interaction-coordinator.ts @@ -886,7 +886,16 @@ export class HostInteractionCoordinator implements RuntimeInteractionAuthority { await this.#refreshCanonicalContinuity(request.sessionId, admission); this.#throwIfPoisoned(); await this.#applySandboxBoundaryDecisionAndDelete(entry, settlement); - await this.#onSandboxBoundarySettled(request.sessionId); + // The answer owns Session admission. Graph-wake reconciliation may need to + // acquire the activity lease held by the wake turn that is parked on this + // very answer, so awaiting it here deadlocks the Session (#3328, #3866). + // Start it after the durable answer is applied, but keep failures visible + // to the Host's fail-stop path instead of creating an unhandled rejection. + void Promise.resolve() + .then(() => this.#onSandboxBoundarySettled(request.sessionId)) + .catch((error: unknown) => { + this.#poison(error); + }); const result = projectSandboxBoundaryInteraction(settlement.request); if (result.status !== 'answered') { throw this.#poison( From c45bde5b35a0edc38f8a39517fca6369566df082 Mon Sep 17 00:00:00 2001 From: testikun Date: Fri, 4 Sep 2026 11:14:16 +0800 Subject: [PATCH 2/3] fix(runtime-host): resolve sandbox wake lineage under admission --- .../__tests__/interaction-coordinator.test.ts | 69 ++++++++++++++++++- .../src/server/execution-composition.ts | 22 +++--- .../src/server/interaction-coordinator.ts | 24 +++++-- .../src/server/sandbox-boundary-graph-wake.ts | 13 +++- 4 files changed, 109 insertions(+), 19 deletions(-) diff --git a/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts b/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts index 91733e905f..7cc94ce75e 100644 --- a/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts @@ -403,6 +403,7 @@ describe('HostInteractionCoordinator', () => { const wakeStarted = deferred(); const releaseWake = deferred(); const wakeFinished = deferred(); + let resolvedRootSessionId: string | undefined; const coordinator = new HostInteractionCoordinator({ store, sandboxBoundaries: stores.sessionStore, @@ -410,7 +411,12 @@ describe('HostInteractionCoordinator', () => { sessions: stores.sessionStore, preflightSessionSnapshot: () => true, refreshCanonicalContinuity: async () => {}, - onSandboxBoundarySettled: async () => { + resolveSandboxBoundaryGraphWake: async (sessionId) => { + resolvedRootSessionId = sessionId; + return session.id; + }, + onSandboxBoundarySettled: async (rootSessionId) => { + assert.equal(rootSessionId, session.id); wakeStarted.resolve(); await releaseWake.promise; wakeFinished.resolve(); @@ -465,6 +471,7 @@ describe('HostInteractionCoordinator', () => { ); assert.equal(answerResult?.ok, true); if (answerResult?.ok) assert.equal(answerResult.result.status, 'answered'); + assert.equal(resolvedRootSessionId, session.id); } finally { releaseWake.resolve(); await wakeFinished.promise; @@ -476,6 +483,66 @@ describe('HostInteractionCoordinator', () => { }); }); + test('poisons when detached graph wake notification rejects', async () => { + await withStore(async ({ owner, store, stores }) => { + const workspace = join(owner.capability.canonicalPath, 'wake-rejection-workspace'); + await mkdir(workspace); + const session = await stores.sessionStore.create({ + cwd: workspace, + llmConnectionId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc', + llmConnectionSlug: 'fake', + model: 'fake-model', + permissionMode: 'ask', + }); + const identity = { ...RUN, sessionId: session.id }; + const poison: RuntimeInteractionFailStopError[] = []; + const coordinator = new HostInteractionCoordinator({ + store, + sandboxBoundaries: stores.sessionStore, + sessionAdmission: new SessionAdmissionGate(), + sessions: stores.sessionStore, + preflightSessionSnapshot: () => true, + refreshCanonicalContinuity: async () => {}, + resolveSandboxBoundaryGraphWake: async () => session.id, + onSandboxBoundarySettled: async () => { + throw new Error('graph wake notification failed'); + }, + onPoison: (error) => poison.push(error), + }); + const binding = coordinator.bindRun(identity); + const request = sandboxBoundaryEvent({ + sessionId: session.id, + requestId: 'boundary_wake_rejection', + status: 'pending', + baseRevision: 0, + turnId: identity.turnId, + runId: identity.runId, + expansion: { network: { enabled: true } }, + justification: 'Connect to the requested service.', + createdAt: 1, + }); + await binding.acceptSandboxBoundaryRequest({ + request, + continuation: sandboxBoundaryContinuation(identity, request.requestId), + }); + + const answerResult = await coordinator.handlers['interaction.answer']( + { + sessionId: session.id, + interactionId: request.requestId, + answer: { kind: 'sandbox_boundary', decision: 'allow' }, + }, + connection(), + ); + assert.equal(answerResult.ok, true); + await new Promise((resolve) => setImmediate(resolve)); + assert.equal(poison.length, 1); + assert.equal(coordinator.isPoisoned(), true); + await assert.rejects(binding.close('turn_terminal'), poison[0]); + await assert.rejects(coordinator.close(), poison[0]); + }); + }); + test('a queued stop waits for sandbox boundary publication before closing its Run', async () => { await withStore(async ({ owner, store, stores }) => { const workspace = join(owner.capability.canonicalPath, 'publication-workspace'); diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts index 2e38c38f8d..d624048ed8 100644 --- a/packages/runtime-host/src/server/execution-composition.ts +++ b/packages/runtime-host/src/server/execution-composition.ts @@ -164,7 +164,7 @@ import { HostPluginPlatform } from './plugin-platform.js'; import { RootAdmissionOwner } from './root-admission-owner.js'; import { RootTurnCoordinator } from './root-turn-coordinator.js'; import { RuntimePolicyActivationGate } from './runtime-policy-activation-gate.js'; -import { notifySandboxBoundaryGraphWake } from './sandbox-boundary-graph-wake.js'; +import { resolveSandboxBoundaryGraphWake } from './sandbox-boundary-graph-wake.js'; import { HostRuntimePolicyCoordinator } from './runtime-policy-coordinator.js'; import { startHostModelMetadataRefresh } from './model-metadata-refresh.js'; import { HostRuntimeResourceCoordinator } from './runtime-resource-coordinator.js'; @@ -680,17 +680,19 @@ export async function createExecutionRuntimeHostComposition( beginDrain(); context.requestDrain(); }, - onSandboxBoundarySettled: (sessionId) => - notifySandboxBoundaryGraphWake( - sessionId, - stores.sessionStore, - { + resolveSandboxBoundaryGraphWake: async (sessionId) => { + try { + return await resolveSandboxBoundaryGraphWake(sessionId, stores.sessionStore, { listGraphIds: (rootSessionId) => requireGraphCoordinator(graphCoordinator).listGraphIds(rootSessionId), - }, - (rootSessionId) => - requireGraphSupervisorWake(graphSupervisorWake).notifyPermissionResponse(rootSessionId), - ), + }); + } catch (error) { + if (isSessionNotFoundError(error)) return undefined; + throw error; + } + }, + onSandboxBoundarySettled: (sessionId) => + requireGraphSupervisorWake(graphSupervisorWake).notifyPermissionResponse(sessionId), }); memory = new HostMemoryCoordinator({ store: memoryStore, diff --git a/packages/runtime-host/src/server/interaction-coordinator.ts b/packages/runtime-host/src/server/interaction-coordinator.ts index 10f0940437..29721e7b3e 100644 --- a/packages/runtime-host/src/server/interaction-coordinator.ts +++ b/packages/runtime-host/src/server/interaction-coordinator.ts @@ -108,6 +108,11 @@ export interface HostInteractionCoordinatorOptions { admission: SessionAdmissionLease, ) => Promise; readonly onPoison: (error: RuntimeInteractionFailStopError) => void; + /** Resolve graph-wake lineage while the settled Session still holds admission. */ + readonly resolveSandboxBoundaryGraphWake?: ( + sessionId: string, + admission: SessionAdmissionLease, + ) => Promise | string | undefined; readonly onSandboxBoundarySettled: (sessionId: string) => Promise | void; } @@ -205,6 +210,7 @@ export class HostInteractionCoordinator implements RuntimeInteractionAuthority { readonly #preflightSessionSnapshot: HostInteractionCoordinatorOptions['preflightSessionSnapshot']; readonly #refreshCanonicalContinuity: HostInteractionCoordinatorOptions['refreshCanonicalContinuity']; readonly #onPoison: HostInteractionCoordinatorOptions['onPoison']; + readonly #resolveSandboxBoundaryGraphWake: HostInteractionCoordinatorOptions['resolveSandboxBoundaryGraphWake']; readonly #onSandboxBoundarySettled: HostInteractionCoordinatorOptions['onSandboxBoundarySettled']; readonly #runs = new Map(); readonly #live = new Map(); @@ -220,6 +226,7 @@ export class HostInteractionCoordinator implements RuntimeInteractionAuthority { this.#preflightSessionSnapshot = options.preflightSessionSnapshot; this.#refreshCanonicalContinuity = options.refreshCanonicalContinuity; this.#onPoison = options.onPoison; + this.#resolveSandboxBoundaryGraphWake = options.resolveSandboxBoundaryGraphWake; this.#onSandboxBoundarySettled = options.onSandboxBoundarySettled; } @@ -886,13 +893,18 @@ export class HostInteractionCoordinator implements RuntimeInteractionAuthority { await this.#refreshCanonicalContinuity(request.sessionId, admission); this.#throwIfPoisoned(); await this.#applySandboxBoundaryDecisionAndDelete(entry, settlement); - // The answer owns Session admission. Graph-wake reconciliation may need to - // acquire the activity lease held by the wake turn that is parked on this - // very answer, so awaiting it here deadlocks the Session (#3328, #3866). - // Start it after the durable answer is applied, but keep failures visible - // to the Host's fail-stop path instead of creating an unhandled rejection. + // The answer owns Session admission. Resolve graph-wake lineage while that + // admission is held, then detach only the notification which may need to + // acquire the activity lease held by the wake turn parked on this answer. + // Awaiting that notification here deadlocks the Session (#3328, #3866). + const resolvedRootSessionId = this.#resolveSandboxBoundaryGraphWake + ? await this.#resolveSandboxBoundaryGraphWake(request.sessionId, admission) + : undefined; void Promise.resolve() - .then(() => this.#onSandboxBoundarySettled(request.sessionId)) + .then(() => { + if (this.#resolveSandboxBoundaryGraphWake && !resolvedRootSessionId) return; + return this.#onSandboxBoundarySettled(resolvedRootSessionId ?? request.sessionId); + }) .catch((error: unknown) => { this.#poison(error); }); diff --git a/packages/runtime-host/src/server/sandbox-boundary-graph-wake.ts b/packages/runtime-host/src/server/sandbox-boundary-graph-wake.ts index 03505b0c75..cf2c12b21f 100644 --- a/packages/runtime-host/src/server/sandbox-boundary-graph-wake.ts +++ b/packages/runtime-host/src/server/sandbox-boundary-graph-wake.ts @@ -39,6 +39,16 @@ export async function sandboxBoundaryGraphWakeRoot( return parent.parentSessionId; } +/** Resolve the durable lineage for a settled sandbox boundary. */ +export async function resolveSandboxBoundaryGraphWake( + sessionId: string, + sessions: SandboxBoundaryGraphWakeHeaderReader, + graphIds: { listGraphIds(rootSessionId: string): Promise }, +): Promise { + const header = await sessions.readHeaderSnapshot(sessionId); + return sandboxBoundaryGraphWakeRoot(header, graphIds); +} + /** Resolve durable lineage before notifying the root graph supervisor. */ export async function notifySandboxBoundaryGraphWake( sessionId: string, @@ -46,7 +56,6 @@ export async function notifySandboxBoundaryGraphWake( graphIds: { listGraphIds(rootSessionId: string): Promise }, notifyPermissionResponse: (rootSessionId: string) => Promise | void, ): Promise { - const header = await sessions.readHeaderSnapshot(sessionId); - const rootSessionId = await sandboxBoundaryGraphWakeRoot(header, graphIds); + const rootSessionId = await resolveSandboxBoundaryGraphWake(sessionId, sessions, graphIds); if (rootSessionId) await notifyPermissionResponse(rootSessionId); } From 3a939fb3f0c362c241b4dda0cb570ed7222122c3 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 5 Sep 2026 12:25:37 +0800 Subject: [PATCH 3/3] test(runtime-host): pin sandbox wake admission --- ...t-capability-admission-integration.test.ts | 3 +- .../src/__tests__/goal-root-authority.test.ts | 3 +- .../__tests__/interaction-coordinator.test.ts | 65 +++++++++++++++---- .../__tests__/root-turn-coordinator.test.ts | 6 +- .../sandbox-boundary-graph-wake.test.ts | 20 +++--- .../src/server/execution-composition.ts | 10 +-- .../src/server/interaction-coordinator.ts | 24 ++++--- .../src/server/sandbox-boundary-graph-wake.ts | 13 +--- 8 files changed, 88 insertions(+), 56 deletions(-) diff --git a/packages/runtime-host/src/__tests__/client-capability-admission-integration.test.ts b/packages/runtime-host/src/__tests__/client-capability-admission-integration.test.ts index a68e1d6d71..4191e67b69 100644 --- a/packages/runtime-host/src/__tests__/client-capability-admission-integration.test.ts +++ b/packages/runtime-host/src/__tests__/client-capability-admission-integration.test.ts @@ -355,7 +355,8 @@ function createInteractionCoordinator( preflightSessionSnapshot: () => true, refreshCanonicalContinuity: async () => undefined, onPoison: () => undefined, - onSandboxBoundarySettled: async () => undefined, + resolveSandboxBoundaryRootSession: async () => undefined, + onSandboxBoundaryGraphWake: async () => undefined, }; return new HostInteractionCoordinator(options); } diff --git a/packages/runtime-host/src/__tests__/goal-root-authority.test.ts b/packages/runtime-host/src/__tests__/goal-root-authority.test.ts index c6e89e8376..98acd01f34 100644 --- a/packages/runtime-host/src/__tests__/goal-root-authority.test.ts +++ b/packages/runtime-host/src/__tests__/goal-root-authority.test.ts @@ -617,7 +617,8 @@ async function createFixture(options: { recoverAdmissions?: boolean } = {}): Pro onPoison: () => { requestedDrain = true; }, - onSandboxBoundarySettled: async () => {}, + resolveSandboxBoundaryRootSession: async () => undefined, + onSandboxBoundaryGraphWake: async () => {}, }); const backends = new BackendRegistry(); backends.register('ai-sdk', (context) => new FakeBackend(context)); diff --git a/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts b/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts index 7cc94ce75e..eac022b0b7 100644 --- a/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { deferred } from '@maka/core/test-only/async-primitives'; +import { deferred, withTimeout } from '@maka/core/test-only/async-primitives'; import assert from 'node:assert/strict'; import { mkdir, mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; @@ -294,7 +294,8 @@ describe('HostInteractionCoordinator', () => { return true; }, refreshCanonicalContinuity: async () => {}, - onSandboxBoundarySettled: async (sessionId) => { + resolveSandboxBoundaryRootSession: async (sessionId) => sessionId, + onSandboxBoundaryGraphWake: async (sessionId) => { assert.equal(sessionId, session.id); graphWakes += 1; }, @@ -403,7 +404,10 @@ describe('HostInteractionCoordinator', () => { const wakeStarted = deferred(); const releaseWake = deferred(); const wakeFinished = deferred(); + const resolverStarted = deferred(); + const releaseResolver = deferred(); let resolvedRootSessionId: string | undefined; + let wakeNotificationStarted = false; const coordinator = new HostInteractionCoordinator({ store, sandboxBoundaries: stores.sessionStore, @@ -411,12 +415,15 @@ describe('HostInteractionCoordinator', () => { sessions: stores.sessionStore, preflightSessionSnapshot: () => true, refreshCanonicalContinuity: async () => {}, - resolveSandboxBoundaryGraphWake: async (sessionId) => { + resolveSandboxBoundaryRootSession: async (sessionId) => { resolvedRootSessionId = sessionId; + resolverStarted.resolve(); + await releaseResolver.promise; return session.id; }, - onSandboxBoundarySettled: async (rootSessionId) => { + onSandboxBoundaryGraphWake: async (rootSessionId) => { assert.equal(rootSessionId, session.id); + wakeNotificationStarted = true; wakeStarted.resolve(); await releaseWake.promise; wakeFinished.resolve(); @@ -461,9 +468,39 @@ describe('HostInteractionCoordinator', () => { answerSettled = true; }, ); - await wakeStarted.promise; + let querySettled = false; + let query: ReturnType<(typeof coordinator.handlers)['interaction.query']> | undefined; try { + await withTimeout( + resolverStarted.promise, + 5_000, + 'sandbox boundary root-session resolver did not start', + ); + query = coordinator.handlers['interaction.query']( + { sessionId: session.id, interactionId: request.requestId }, + connection(), + ); + void query.then( + () => { + querySettled = true; + }, + () => { + querySettled = true; + }, + ); await new Promise((resolve) => setImmediate(resolve)); + assert.equal( + querySettled, + false, + 'interaction query bypassed the resolver admission lease', + ); + releaseResolver.resolve(); + await answer; + assert.ok(query); + const queryResult = await query; + assert.equal(queryResult.ok, true); + assert.equal(querySettled, true); + await withTimeout(wakeStarted.promise, 5_000, 'sandbox boundary graph wake did not start'); assert.equal( answerSettled, true, @@ -473,9 +510,10 @@ describe('HostInteractionCoordinator', () => { if (answerResult?.ok) assert.equal(answerResult.result.status, 'answered'); assert.equal(resolvedRootSessionId, session.id); } finally { + releaseResolver.resolve(); releaseWake.resolve(); - await wakeFinished.promise; - await answer; + if (wakeNotificationStarted) await wakeFinished.promise; + await answer.catch(() => undefined); } await binding.close('turn_terminal'); binding.release(); @@ -503,8 +541,8 @@ describe('HostInteractionCoordinator', () => { sessions: stores.sessionStore, preflightSessionSnapshot: () => true, refreshCanonicalContinuity: async () => {}, - resolveSandboxBoundaryGraphWake: async () => session.id, - onSandboxBoundarySettled: async () => { + resolveSandboxBoundaryRootSession: async () => session.id, + onSandboxBoundaryGraphWake: async () => { throw new Error('graph wake notification failed'); }, onPoison: (error) => poison.push(error), @@ -572,7 +610,8 @@ describe('HostInteractionCoordinator', () => { await releaseAdmissionRefresh.promise; }, onPoison: () => {}, - onSandboxBoundarySettled: async () => {}, + resolveSandboxBoundaryRootSession: async () => undefined, + onSandboxBoundaryGraphWake: async () => {}, }); const binding = await bindRuntimeInteractionRun(coordinator, identity); const request = sandboxBoundaryEvent({ @@ -656,7 +695,8 @@ describe('HostInteractionCoordinator', () => { preflightSessionSnapshot: () => false, refreshCanonicalContinuity: async () => {}, onPoison: () => {}, - onSandboxBoundarySettled: async () => {}, + resolveSandboxBoundaryRootSession: async () => undefined, + onSandboxBoundaryGraphWake: async () => {}, }); const ownerRun = coordinator.bindRun(identity); @@ -1000,7 +1040,8 @@ function createCoordinator( preflightSessionSnapshot: () => true, refreshCanonicalContinuity: async () => {}, onPoison: () => {}, - onSandboxBoundarySettled: async () => {}, + resolveSandboxBoundaryRootSession: async () => undefined, + onSandboxBoundaryGraphWake: async () => {}, ...overrides, }); } diff --git a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts index 553d5d1d4b..535a25837c 100644 --- a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts @@ -2717,7 +2717,8 @@ test('hosted linked child roots share admission, message, terminal, and stop aut onPoison: () => { drainRequested = true; }, - onSandboxBoundarySettled: async () => {}, + resolveSandboxBoundaryRootSession: async () => undefined, + onSandboxBoundaryGraphWake: async () => {}, }); const interactionAuthority: RuntimeInteractionAuthority = { bindRun: (identity) => { @@ -5286,7 +5287,8 @@ async function createFailureFixture(options: { refreshCanonicalContinuity: (sessionId, admission) => requireContinuity(continuity).refreshCanonical(sessionId, admission), onPoison: requestDrain, - onSandboxBoundarySettled: async () => {}, + resolveSandboxBoundaryRootSession: async () => undefined, + onSandboxBoundaryGraphWake: async () => {}, }) : undefined; const backends = new BackendRegistry(); diff --git a/packages/runtime-host/src/__tests__/sandbox-boundary-graph-wake.test.ts b/packages/runtime-host/src/__tests__/sandbox-boundary-graph-wake.test.ts index 20b56508e0..61269a9c14 100644 --- a/packages/runtime-host/src/__tests__/sandbox-boundary-graph-wake.test.ts +++ b/packages/runtime-host/src/__tests__/sandbox-boundary-graph-wake.test.ts @@ -21,7 +21,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { agentGraphIdForRootSession } from '@maka/runtime/stream-graph-coordinator'; import { - notifySandboxBoundaryGraphWake, + resolveSandboxBoundaryRootSession, sandboxBoundaryGraphWakeRoot, } from '../server/sandbox-boundary-graph-wake.js'; @@ -78,9 +78,8 @@ test('rejects graph operator lineage that is not owned by its parent Session', a ); }); -test('reads durable operator lineage before notifying only its root graph', async () => { +test('resolves durable operator lineage for only its root graph', async () => { const reads: string[] = []; - const wakes: string[] = []; const headers = new Map([ [ 'graph-operator', @@ -106,16 +105,17 @@ test('reads durable operator lineage before notifying only its root graph', asyn return header; }, }; - const notify = async (sessionId: string) => { - wakes.push(sessionId); - }; - const graphIds = idsFor('root-session'); - await notifySandboxBoundaryGraphWake('graph-operator', reader, graphIds, notify); - await notifySandboxBoundaryGraphWake('ordinary-child', reader, graphIds, notify); + assert.equal( + await resolveSandboxBoundaryRootSession('graph-operator', reader, graphIds), + 'root-session', + ); + assert.equal( + await resolveSandboxBoundaryRootSession('ordinary-child', reader, graphIds), + undefined, + ); assert.deepEqual(reads, ['graph-operator', 'ordinary-child']); - assert.deepEqual(wakes, ['root-session']); }); function idsFor(rootSessionId: string) { diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts index d624048ed8..6d83056a07 100644 --- a/packages/runtime-host/src/server/execution-composition.ts +++ b/packages/runtime-host/src/server/execution-composition.ts @@ -164,7 +164,7 @@ import { HostPluginPlatform } from './plugin-platform.js'; import { RootAdmissionOwner } from './root-admission-owner.js'; import { RootTurnCoordinator } from './root-turn-coordinator.js'; import { RuntimePolicyActivationGate } from './runtime-policy-activation-gate.js'; -import { resolveSandboxBoundaryGraphWake } from './sandbox-boundary-graph-wake.js'; +import { resolveSandboxBoundaryRootSession } from './sandbox-boundary-graph-wake.js'; import { HostRuntimePolicyCoordinator } from './runtime-policy-coordinator.js'; import { startHostModelMetadataRefresh } from './model-metadata-refresh.js'; import { HostRuntimeResourceCoordinator } from './runtime-resource-coordinator.js'; @@ -680,9 +680,9 @@ export async function createExecutionRuntimeHostComposition( beginDrain(); context.requestDrain(); }, - resolveSandboxBoundaryGraphWake: async (sessionId) => { + resolveSandboxBoundaryRootSession: async (sessionId) => { try { - return await resolveSandboxBoundaryGraphWake(sessionId, stores.sessionStore, { + return await resolveSandboxBoundaryRootSession(sessionId, stores.sessionStore, { listGraphIds: (rootSessionId) => requireGraphCoordinator(graphCoordinator).listGraphIds(rootSessionId), }); @@ -691,8 +691,8 @@ export async function createExecutionRuntimeHostComposition( throw error; } }, - onSandboxBoundarySettled: (sessionId) => - requireGraphSupervisorWake(graphSupervisorWake).notifyPermissionResponse(sessionId), + onSandboxBoundaryGraphWake: (rootSessionId) => + requireGraphSupervisorWake(graphSupervisorWake).notifyPermissionResponse(rootSessionId), }); memory = new HostMemoryCoordinator({ store: memoryStore, diff --git a/packages/runtime-host/src/server/interaction-coordinator.ts b/packages/runtime-host/src/server/interaction-coordinator.ts index 29721e7b3e..e3f4451745 100644 --- a/packages/runtime-host/src/server/interaction-coordinator.ts +++ b/packages/runtime-host/src/server/interaction-coordinator.ts @@ -108,12 +108,12 @@ export interface HostInteractionCoordinatorOptions { admission: SessionAdmissionLease, ) => Promise; readonly onPoison: (error: RuntimeInteractionFailStopError) => void; - /** Resolve graph-wake lineage while the settled Session still holds admission. */ - readonly resolveSandboxBoundaryGraphWake?: ( + /** Resolve the root Session while the settled Session still holds admission. */ + readonly resolveSandboxBoundaryRootSession: ( sessionId: string, - admission: SessionAdmissionLease, ) => Promise | string | undefined; - readonly onSandboxBoundarySettled: (sessionId: string) => Promise | void; + /** Notify the root graph supervisor after the answer releases admission. */ + readonly onSandboxBoundaryGraphWake: (rootSessionId: string) => Promise | void; } interface RunClosure { @@ -210,8 +210,8 @@ export class HostInteractionCoordinator implements RuntimeInteractionAuthority { readonly #preflightSessionSnapshot: HostInteractionCoordinatorOptions['preflightSessionSnapshot']; readonly #refreshCanonicalContinuity: HostInteractionCoordinatorOptions['refreshCanonicalContinuity']; readonly #onPoison: HostInteractionCoordinatorOptions['onPoison']; - readonly #resolveSandboxBoundaryGraphWake: HostInteractionCoordinatorOptions['resolveSandboxBoundaryGraphWake']; - readonly #onSandboxBoundarySettled: HostInteractionCoordinatorOptions['onSandboxBoundarySettled']; + readonly #resolveSandboxBoundaryRootSession: HostInteractionCoordinatorOptions['resolveSandboxBoundaryRootSession']; + readonly #onSandboxBoundaryGraphWake: HostInteractionCoordinatorOptions['onSandboxBoundaryGraphWake']; readonly #runs = new Map(); readonly #live = new Map(); #accepting = true; @@ -226,8 +226,8 @@ export class HostInteractionCoordinator implements RuntimeInteractionAuthority { this.#preflightSessionSnapshot = options.preflightSessionSnapshot; this.#refreshCanonicalContinuity = options.refreshCanonicalContinuity; this.#onPoison = options.onPoison; - this.#resolveSandboxBoundaryGraphWake = options.resolveSandboxBoundaryGraphWake; - this.#onSandboxBoundarySettled = options.onSandboxBoundarySettled; + this.#resolveSandboxBoundaryRootSession = options.resolveSandboxBoundaryRootSession; + this.#onSandboxBoundaryGraphWake = options.onSandboxBoundaryGraphWake; } bindRun(identity: RuntimeInteractionRunIdentity): RuntimeInteractionRunOwner { @@ -897,13 +897,11 @@ export class HostInteractionCoordinator implements RuntimeInteractionAuthority { // admission is held, then detach only the notification which may need to // acquire the activity lease held by the wake turn parked on this answer. // Awaiting that notification here deadlocks the Session (#3328, #3866). - const resolvedRootSessionId = this.#resolveSandboxBoundaryGraphWake - ? await this.#resolveSandboxBoundaryGraphWake(request.sessionId, admission) - : undefined; + const resolvedRootSessionId = await this.#resolveSandboxBoundaryRootSession(request.sessionId); void Promise.resolve() .then(() => { - if (this.#resolveSandboxBoundaryGraphWake && !resolvedRootSessionId) return; - return this.#onSandboxBoundarySettled(resolvedRootSessionId ?? request.sessionId); + if (!resolvedRootSessionId) return; + return this.#onSandboxBoundaryGraphWake(resolvedRootSessionId); }) .catch((error: unknown) => { this.#poison(error); diff --git a/packages/runtime-host/src/server/sandbox-boundary-graph-wake.ts b/packages/runtime-host/src/server/sandbox-boundary-graph-wake.ts index cf2c12b21f..99ec41e8d8 100644 --- a/packages/runtime-host/src/server/sandbox-boundary-graph-wake.ts +++ b/packages/runtime-host/src/server/sandbox-boundary-graph-wake.ts @@ -40,7 +40,7 @@ export async function sandboxBoundaryGraphWakeRoot( } /** Resolve the durable lineage for a settled sandbox boundary. */ -export async function resolveSandboxBoundaryGraphWake( +export async function resolveSandboxBoundaryRootSession( sessionId: string, sessions: SandboxBoundaryGraphWakeHeaderReader, graphIds: { listGraphIds(rootSessionId: string): Promise }, @@ -48,14 +48,3 @@ export async function resolveSandboxBoundaryGraphWake( const header = await sessions.readHeaderSnapshot(sessionId); return sandboxBoundaryGraphWakeRoot(header, graphIds); } - -/** Resolve durable lineage before notifying the root graph supervisor. */ -export async function notifySandboxBoundaryGraphWake( - sessionId: string, - sessions: SandboxBoundaryGraphWakeHeaderReader, - graphIds: { listGraphIds(rootSessionId: string): Promise }, - notifyPermissionResponse: (rootSessionId: string) => Promise | void, -): Promise { - const rootSessionId = await resolveSandboxBoundaryGraphWake(sessionId, sessions, graphIds); - if (rootSessionId) await notifyPermissionResponse(rootSessionId); -}