fix(web): keep removal-time availability invalidation across an in-flight pull
The `host/session-removed` invalidation flipped the owned catalog and addressed children to `parentAvailable:false`, but a `subagent.list` pull already in flight was requested before the removal and its ok-response carries the pre-removal `parentAvailable:true` — the response then overwrote both the catalog and every addressed child, resurrecting the writable-editor-against-a-dead-continuation-owner bug the invalidation closes, with no refresh scheduled to converge afterwards. Mark the owner stale when a pull is in flight at removal time, so one trailing refresh runs after the in-flight response settles and the post-removal host truth lands. Adds a regression test: removal mid-pull, stale ok response, trailing pull, final state stays unavailable on the catalog and the addressed child.
This commit is contained in:
@@ -588,6 +588,44 @@ describe('subagent catalogs', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('does not let a stale in-flight pull resurrect a removed parent\'s availability', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const root = 'fk-root' as SessionId
|
||||
const child = () => ({
|
||||
kind: 'child' as const, id: S2, mode: 'continuable' as const, label: 'worker',
|
||||
activity: 'inactive' as const, hasChildren: false,
|
||||
})
|
||||
const first = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
||||
api.onSubagentList = () => first.promise
|
||||
const manager = new SessionManager(api)
|
||||
const refresh = manager.refreshSubagents(root)
|
||||
first.resolve(ok({ entries: [child()] as never[], parentAvailable: true }))
|
||||
await refresh
|
||||
manager.selectSubagent({ parentSessionId: root, childSessionId: S2, mode: 'continuable' })
|
||||
|
||||
// The removal lands while a second pull is in flight: the invalidation
|
||||
// must survive the pre-removal ok response, so one trailing pull runs.
|
||||
const mid = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
||||
api.onSubagentList = () => mid.promise
|
||||
const midRefresh = manager.refreshSubagents(root)
|
||||
manager.handleHostEnvelope({
|
||||
rpcId: 'parent-removed-mid-pull' as never,
|
||||
payload: { type: 'host/session-removed', sessionId: root },
|
||||
})
|
||||
const trailing = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
||||
api.onSubagentList = () => trailing.promise
|
||||
mid.resolve(ok({ entries: [child()] as never[], parentAvailable: true }))
|
||||
await midRefresh
|
||||
trailing.resolve(ok({ entries: [child()] as never[], parentAvailable: false }))
|
||||
await trailing.promise
|
||||
|
||||
const rootCalls = api.callsOf('subagent.list')
|
||||
.filter((call: { parentSessionId: SessionId }) => call.parentSessionId === root)
|
||||
expect(rootCalls).toHaveLength(3)
|
||||
expect(manager.getListSnapshot().subagentsByParent[root]?.parentAvailable).toBe(false)
|
||||
expect(manager.get(S2).getSnapshot().subagent).toMatchObject({ parentAvailable: false })
|
||||
})
|
||||
|
||||
it('invalidates catalog availability when the owning parent is removed', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const root = 'fk-root' as SessionId
|
||||
|
||||
Reference in New Issue
Block a user