diff --git a/packages/client/runtime/src/client/sessions/manager.ts b/packages/client/runtime/src/client/sessions/manager.ts index 3b841ae0b2..1c186059de 100644 --- a/packages/client/runtime/src/client/sessions/manager.ts +++ b/packages/client/runtime/src/client/sessions/manager.ts @@ -688,6 +688,19 @@ export class SessionManager { this.pendingBuffers.delete(frame.sessionId) // a removed session's buffered frames must not replay on a future instantiation this.waitingApprovals.delete(frame.sessionId) // a removed session cannot wait on anyone if (!durableSubagent) this.projectionStores.delete(frame.sessionId) + // The removed session can no longer be the delivery owner of its + // catalog: invalidate availability immediately. Removal schedules no + // catalog refresh, and without this an addressed child keeps a + // writable editor against a dead continuation owner until an + // unrelated refresh (or forever, for a closed menu). + const ownedCatalog = this.catalogs.get(frame.sessionId) + if (ownedCatalog !== undefined && ownedCatalog.parentAvailable) { + this.catalogs.set(frame.sessionId, { ...ownedCatalog, parentAvailable: false }) + } + for (const [childId, address] of this.addresses) { + if (address.parentSessionId !== frame.sessionId) continue + this.sessions.get(childId)?.handleSubagentParentAvailable(false) + } return } case 'host/session-status': { diff --git a/packages/client/runtime/tests/manager.spec.ts b/packages/client/runtime/tests/manager.spec.ts index 2c1b8c259d..5d12498edd 100644 --- a/packages/client/runtime/tests/manager.spec.ts +++ b/packages/client/runtime/tests/manager.spec.ts @@ -587,6 +587,30 @@ describe('subagent catalogs', () => { vi.useRealTimers() } }) + + it('invalidates catalog availability when the owning parent is removed', async () => { + const api = new FakeApiClient() + const root = 'fk-root' as SessionId + api.onSubagentList = () => Promise.resolve(ok({ + entries: [{ + kind: 'child', id: S2, mode: 'continuable', label: 'worker', + activity: 'inactive', hasChildren: false, + }] as never[], + parentAvailable: true, + })) + const manager = new SessionManager(api) + await manager.refreshSubagents(root) + manager.selectSubagent({ parentSessionId: root, childSessionId: S2, mode: 'continuable' }) + expect(manager.get(S2).getSnapshot().subagent).toMatchObject({ parentAvailable: true }) + + manager.handleHostEnvelope({ + rpcId: 'parent-removed' as never, + payload: { type: 'host/session-removed', sessionId: root }, + }) + + expect(manager.getListSnapshot().subagentsByParent[root]?.parentAvailable).toBe(false) + expect(manager.get(S2).getSnapshot().subagent).toMatchObject({ parentAvailable: false }) + }) }) describe('remaining branches', () => {