ソースを参照

fix(web): invalidate catalog availability when the owning parent is removed

A removed session can no longer be the delivery owner of its continuable
children, but the `host/session-removed` handler only reconciled the
removed row's own activity. `parentAvailable` was updated exclusively from
`refreshSubagents` success, and removal schedules no catalog refresh — so
after the parent's Activation detaches, an addressed child kept a writable
editor against a dead continuation owner until an unrelated refresh (or
forever, for a closed menu).

Flip `parentAvailable` to false on the owned catalog and push
`handleSubagentParentAvailable(false)` to every addressed child Session at
removal time, matching the refresh path's notification. New Session
instances already read `parentAvailable` from the catalog, so they inherit
the invalidated state.

Adds a regression test: removing the catalog's owning parent flips the
snapshot's `parentAvailable` and notifies the addressed child instance.
Tianyi Cui 2 ヶ月 前
親
コミット
8431dbead3

+ 13 - 0
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': {

+ 24 - 0
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', () => {