mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(web): preserve removal across stale catalog pulls
The earlier removal fix invalidated parentAvailable immediately and queued a trailing subagent.list request, but it still applied the already in-flight success verbatim. That stale success reopened the composer and became the trailing request baseline. If the trailing request failed, its error snapshot preserved parentAvailable:true indefinitely. Record a false-only parent availability override on the exact in-flight catalog request when the owner removal frame arrives. Successful and failed responses now replay that request-local invalidation before publishing a snapshot, and addressed child Sessions receive the same effective value. The trailing request therefore starts from a false baseline and a later transport or business failure cannot resurrect the removed parent. Strengthen the regression to assert the catalog and selected child remain read-only immediately after a stale parentAvailable:true success, then fail the trailing pull and assert the error snapshot remains unavailable. The complete SessionManager test file passes all 39 tests, and the client runtime TypeScript project builds cleanly.
This commit is contained in:
@@ -59,6 +59,8 @@ interface CatalogInflight {
|
||||
readonly promise: Promise<void>
|
||||
readonly expandableRows: Set<SessionId>
|
||||
readonly activityRows: Map<SessionId, 'running' | 'inactive'>
|
||||
/** Removal-time invalidation replayed over the response this request predates. */
|
||||
parentAvailableOverride: false | undefined
|
||||
}
|
||||
|
||||
type SessionListMutation =
|
||||
@@ -312,22 +314,26 @@ export class SessionManager {
|
||||
try {
|
||||
const { result } = await this.api.subagents.list({ parentSessionId })
|
||||
if (result.ok) {
|
||||
const parentAvailable = this.catalogInflight.get(parentSessionId)?.parentAvailableOverride
|
||||
?? result.value.parentAvailable
|
||||
this.catalogs.set(parentSessionId, {
|
||||
...result.value,
|
||||
entries: this.withCatalogMutations(result.value.entries, expandableRows, activityRows),
|
||||
parentAvailable,
|
||||
state: 'ready',
|
||||
error: null,
|
||||
})
|
||||
for (const [childId, address] of this.addresses) {
|
||||
if (address.parentSessionId !== parentSessionId) continue
|
||||
this.sessions.get(childId)?.handleSubagentParentAvailable(result.value.parentAvailable)
|
||||
this.sessions.get(childId)?.handleSubagentParentAvailable(parentAvailable)
|
||||
}
|
||||
} else {
|
||||
this.catalogs.set(parentSessionId, {
|
||||
entries: this.withCatalogMutations(
|
||||
previous?.entries ?? [], expandableRows, activityRows,
|
||||
),
|
||||
parentAvailable: previous?.parentAvailable ?? false,
|
||||
parentAvailable: this.catalogInflight.get(parentSessionId)?.parentAvailableOverride
|
||||
?? previous?.parentAvailable ?? false,
|
||||
state: 'error',
|
||||
error: result.error,
|
||||
})
|
||||
@@ -338,7 +344,8 @@ export class SessionManager {
|
||||
entries: this.withCatalogMutations(
|
||||
previous?.entries ?? [], expandableRows, activityRows,
|
||||
),
|
||||
parentAvailable: previous?.parentAvailable ?? false,
|
||||
parentAvailable: this.catalogInflight.get(parentSessionId)?.parentAvailableOverride
|
||||
?? previous?.parentAvailable ?? false,
|
||||
state: 'error',
|
||||
error: folded.ok ? null : folded.error,
|
||||
})
|
||||
@@ -351,7 +358,12 @@ export class SessionManager {
|
||||
this.notifier.markDirty()
|
||||
}
|
||||
})()
|
||||
this.catalogInflight.set(parentSessionId, { promise: operation, expandableRows, activityRows })
|
||||
this.catalogInflight.set(parentSessionId, {
|
||||
promise: operation,
|
||||
expandableRows,
|
||||
activityRows,
|
||||
parentAvailableOverride: undefined,
|
||||
})
|
||||
return operation
|
||||
}
|
||||
|
||||
@@ -690,9 +702,14 @@ export class SessionManager {
|
||||
if (!durableSubagent) this.projectionStores.delete(frame.sessionId)
|
||||
// A pull already in flight was requested before this removal and can
|
||||
// carry the pre-removal parentAvailable:true, which would resurrect
|
||||
// the writable editor this invalidation just closed. Queue one
|
||||
// trailing refresh so the post-removal host truth converges.
|
||||
if (this.catalogInflight.has(frame.sessionId)) this.catalogStale.add(frame.sessionId)
|
||||
// the writable editor this invalidation just closed. Replay false over
|
||||
// that response and queue one trailing refresh so the post-removal
|
||||
// host truth converges.
|
||||
const inflightCatalog = this.catalogInflight.get(frame.sessionId)
|
||||
if (inflightCatalog !== undefined) {
|
||||
inflightCatalog.parentAvailableOverride = false
|
||||
this.catalogStale.add(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
|
||||
|
||||
@@ -588,7 +588,7 @@ describe('subagent catalogs', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('does not let a stale in-flight pull resurrect a removed parent\'s availability', async () => {
|
||||
it('keeps removal invalidation across a stale success and failed trailing pull', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const root = 'fk-root' as SessionId
|
||||
const child = () => ({
|
||||
@@ -616,8 +616,16 @@ describe('subagent catalogs', () => {
|
||||
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
|
||||
expect(manager.getListSnapshot().subagentsByParent[root]?.parentAvailable).toBe(false)
|
||||
expect(manager.get(S2).getSnapshot().subagent).toMatchObject({ parentAvailable: false })
|
||||
|
||||
trailing.resolve(err({ code: 'internal', message: 'trailing pull failed', details: {} }))
|
||||
await vi.waitFor(() => {
|
||||
expect(manager.getListSnapshot().subagentsByParent[root]).toMatchObject({
|
||||
state: 'error',
|
||||
parentAvailable: false,
|
||||
})
|
||||
})
|
||||
|
||||
const rootCalls = api.callsOf('subagent.list')
|
||||
.filter(call => (call as { parentSessionId: SessionId }).parentSessionId === root)
|
||||
|
||||
Reference in New Issue
Block a user