@@ -5123,93 +5123,7 @@ describe('SessionManager permission mode updates', () => {
51235123 expect(builtPermissionModes).toEqual(['ask', 'bypass']);
51245124 });
51255125
5126- test('a boundary revision bump without backend disposal rebuilds on the next activation', async () => {
5127- const store = new MemorySessionStore();
5128- const runStore = new MemoryAgentRunStore();
5129- const backends = new BackendRegistry();
5130- let builds = 0;
5131- backends.register('ai-sdk', (ctx) => {
5132- builds += 1;
5133- return new TestBackend(ctx);
5134- });
5135- const manager = new SessionManager({
5136- store,
5137- runStore,
5138- runtimeEventStore: runStore,
5139- backends,
5140- newId: nextId(),
5141- now: nextNow(8_000),
5142- });
5143- const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
5144-
5145- await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'one' }));
5146- expect(builds).toBe(1);
5147-
5148- // A write path that skips backend disposal bumps the durable boundary
5149- // while the generation stays alive.
5150- store.forceExecutionBoundary(session.id, { kind: 'bypass', revision: 5 });
5151-
5152- await drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'two' }));
5153- expect(builds).toBe(2);
5154- expect(store.disposeCount).toBe(1);
5155-
5156- // Once rebuilt against the current revision, the generation is reused again.
5157- await drain(manager.sendMessage(session.id, { turnId: 'turn-3', text: 'three' }));
5158- expect(builds).toBe(2);
5159- });
5160-
5161- test('a stale generation with live runs flushes after they exit instead of disposing underneath them', async () => {
5162- const store = new MemorySessionStore();
5163- const runStore = new MemoryAgentRunStore();
5164- const backends = new BackendRegistry();
5165- const gates: Gate[] = [];
5166- let builds = 0;
5167- backends.register('ai-sdk', (ctx) => {
5168- builds += 1;
5169- const gate = makeGate();
5170- gates.push(gate);
5171- return new TestBackend(ctx, gate);
5172- });
5173- const manager = new SessionManager({
5174- store,
5175- runStore,
5176- runtimeEventStore: runStore,
5177- backends,
5178- newId: nextId(),
5179- now: nextNow(8_000),
5180- });
5181- const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
5182-
5183- const first = manager
5184- .sendMessage(session.id, { turnId: 'turn-1', text: 'one' })
5185- [Symbol.asyncIterator]();
5186- expect((await first.next()).value?.type).toBe('text_delta');
5187- expect(builds).toBe(1);
51885126
5189- store.forceExecutionBoundary(session.id, { kind: 'bypass', revision: 5 });
5190-
5191- // An overlapping activation while turn 1 is live must not dispose the
5192- // generation underneath it: the generation is marked, reused for this
5193- // turn, and flushed once both runs exit. Both turns share the reused
5194- // backend, so a single gate holds them both.
5195- const second = manager
5196- .sendMessage(session.id, { turnId: 'turn-2', text: 'two' })
5197- [Symbol.asyncIterator]();
5198- expect((await second.next()).value?.type).toBe('text_delta');
5199- expect(builds).toBe(1);
5200-
5201- gates[0]!.release();
5202- while (!(await first.next()).done) {}
5203- while (!(await second.next()).done) {}
5204-
5205- const third = manager
5206- .sendMessage(session.id, { turnId: 'turn-3', text: 'three' })
5207- [Symbol.asyncIterator]();
5208- expect((await third.next()).value?.type).toBe('text_delta');
5209- gates[1]!.release();
5210- while (!(await third.next()).done) {}
5211- expect(builds).toBe(2);
5212- });
52135127
52145128 test('narrowing with an active descendant commits promptly and fences the lineage shells', async () => {
52155129 const store = new MemorySessionStore();
@@ -5519,88 +5433,6 @@ describe('SessionManager permission mode updates', () => {
55195433 expect(dispatch.permissionMode).toBe('bypass');
55205434 });
55215435
5522- test('seeded switch/turn interleavings always observe the committed mode', async () => {
5523- // A fixed-seed PRNG picks the interleaving class per iteration; every
5524- // checkpoint awaits a deterministic event, so the sweep is reproducible.
5525- let seed = 0x3349;
5526- const random = (): number => {
5527- seed = (seed * 1_103_515_245 + 12_345) % 2_147_483_648;
5528- return seed / 2_147_483_648;
5529- };
5530-
5531- const store = new MemorySessionStore();
5532- const runStore = new MemoryAgentRunStore();
5533- const backends = new BackendRegistry();
5534- const gates: Gate[] = [];
5535- const composedModes: SessionHeader['permissionMode'][] = [];
5536- backends.register('ai-sdk', (ctx) => {
5537- const gate = makeGate();
5538- gates.push(gate);
5539- composedModes.push(ctx.header.permissionMode);
5540- return new TestBackend(ctx, gate);
5541- });
5542- const manager = new SessionManager({
5543- store,
5544- runStore,
5545- runtimeEventStore: runStore,
5546- backends,
5547- // Narrowing (bypass → ask) fences shell runs through this authority.
5548- shellRuns: {
5549- async terminateSession() {
5550- return undefined;
5551- },
5552- async commitSessionClose() {},
5553- rollbackSessionClose() {},
5554- resumeSession() {},
5555- } as never,
5556- newId: nextId(),
5557- now: nextNow(9_000),
5558- });
5559- const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
5560-
5561- let expected: 'ask' | 'bypass' = 'ask';
5562- let turnCount = 0;
5563- for (let iteration = 0; iteration < 100; iteration += 1) {
5564- const nextMode: 'ask' | 'bypass' = random() < 0.5 ? 'bypass' : 'ask';
5565- const interleaving = Math.floor(random() * 3);
5566-
5567- if (interleaving === 0) {
5568- // Switch while the session is idle.
5569- await manager.setPermissionMode(session.id, nextMode);
5570- expected = nextMode;
5571- } else {
5572- // Switch requested while a turn is running; the queued commit lands
5573- // in the gap as the turn settles (class 1 requests it mid-flight,
5574- // class 2 races it with the gate release).
5575- turnCount += 1;
5576- const turn = manager
5577- .sendMessage(session.id, { turnId: `turn-${turnCount}`, text: `t${turnCount}` })
5578- [Symbol.asyncIterator]();
5579- expect((await turn.next()).value?.type).toBe('text_delta');
5580- const switching = manager.setPermissionMode(session.id, nextMode);
5581- if (interleaving === 2) gates[gates.length - 1]!.release();
5582- if (interleaving === 1) gates[gates.length - 1]!.release();
5583- while (!(await turn.next()).done) {}
5584- await switching;
5585- expected = nextMode;
5586- }
5587-
5588- // Invariant: every turn started after the switch resolved is composed
5589- // from the committed mode, and a tool call against the committed store
5590- // derives the same mode.
5591- turnCount += 1;
5592- const verify = manager
5593- .sendMessage(session.id, { turnId: `turn-${turnCount}`, text: `t${turnCount}` })
5594- [Symbol.asyncIterator]();
5595- expect((await verify.next()).value?.type).toBe('text_delta');
5596- expect(composedModes[composedModes.length - 1]).toBe(expected);
5597- const dispatch = await dispatchProbeTool(store, session.id);
5598- expect(dispatch.boundaryKind).toBe(expected === 'bypass' ? 'bypass' : 'managed');
5599- expect(dispatch.permissionMode).toBe(expected);
5600- gates[gates.length - 1]!.release();
5601- while (!(await verify.next()).done) {}
5602- }
5603- });
56045436
56055437 test('leaving explore clears the deep research label so visible read-only copy stays truthful', async () => {
56065438 const store = new MemorySessionStore();
@@ -17218,11 +17050,6 @@ class MemorySessionStore implements SessionStore {
1721817050 return boundary;
1721917051 }
1722017052
17221- /** Simulates a config write path that bumps the boundary without disposing backends. */
17222- forceExecutionBoundary(sessionId: string, boundary: ExecutionBoundary): void {
17223- this.executionBoundaries.set(sessionId, boundary);
17224- }
17225-
1722617053 async createSandboxBoundaryRequest(
1722717054 input: CreateSandboxBoundaryRequest,
1722817055 ): Promise<SandboxBoundaryRequest> {
0 commit comments