mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 16:02:55 +00:00
fix(agent-status): persist Claude idle-row hydration cleanup
This commit is contained in:
@@ -6093,6 +6093,10 @@ describe('Last-status persistence', () => {
|
||||
subagents: undefined
|
||||
})
|
||||
])
|
||||
// Why: make the migration one-time; otherwise every launch reparses and
|
||||
// re-prunes the same persisted idle rows.
|
||||
const persisted = JSON.parse(readFileSync(lastStatusPath(), 'utf8'))
|
||||
expect(persisted.entries[PANE].payload.subagents).toBeUndefined()
|
||||
} finally {
|
||||
server.stop()
|
||||
}
|
||||
|
||||
@@ -244,11 +244,10 @@ function sanitizeHydratedEntry(
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
const normalizedPayload = normalizeAgentStatusPayload(record.payload)
|
||||
if (!normalizedPayload) {
|
||||
const payload = normalizeAgentStatusPayload(record.payload)
|
||||
if (!payload) {
|
||||
return null
|
||||
}
|
||||
const payload = dropHydratedIdleClaudeSubagents(normalizedPayload)
|
||||
return {
|
||||
paneKey,
|
||||
launchToken: typeof record.launchToken === 'string' ? record.launchToken : undefined,
|
||||
@@ -1777,6 +1776,7 @@ export class AgentHookServer {
|
||||
}
|
||||
let hydrated = 0
|
||||
let dropped = 0
|
||||
let prunedLegacyClaudeSubagents = 0
|
||||
// Why: bound disk growth — drop anything older than HYDRATE_MAX_AGE_MS so
|
||||
// entries from worktrees archived weeks ago do not pile up forever. Use
|
||||
// Date.now() once to keep the cutoff consistent across all entries this
|
||||
@@ -1790,11 +1790,15 @@ export class AgentHookServer {
|
||||
: { ...(rawEntry as Record<string, unknown>), paneKey: resolvedPaneKey }
|
||||
const entry = sanitizeHydratedEntry(resolvedPaneKey, rawResolvedEntry)
|
||||
if (entry && entry.receivedAt >= ttlCutoff) {
|
||||
const hydratedPayload = dropHydratedIdleClaudeSubagents(entry.payload)
|
||||
if (hydratedPayload !== entry.payload) {
|
||||
prunedLegacyClaudeSubagents +=
|
||||
(entry.payload.subagents?.length ?? 0) - (hydratedPayload.subagents?.length ?? 0)
|
||||
entry.payload = hydratedPayload
|
||||
}
|
||||
this.state.lastStatusByPaneKey.set(resolvedPaneKey, entry)
|
||||
// Why: the in-memory subagent roster died with the previous process.
|
||||
// Reseed it from the persisted snapshot so the next teammate-bearing
|
||||
// Stop (whose task ids never match lifecycle ids) doesn't silently
|
||||
// drop the replayed child rows.
|
||||
// Why: preserve only working children across restart. Live activity
|
||||
// confirms them; a later complete inventory may reap stale seeds.
|
||||
if (entry.payload.subagents) {
|
||||
seedClaudeSubagentRosterFromSnapshots(
|
||||
this.state,
|
||||
@@ -1812,18 +1816,16 @@ export class AgentHookServer {
|
||||
`[agent-hooks] last-status hydrate dropped ${dropped} entries (kept ${hydrated})`
|
||||
)
|
||||
}
|
||||
if (hydrated > 0 && dropped === 0) {
|
||||
if (dropped > 0 || prunedLegacyClaudeSubagents > 0) {
|
||||
// Why: persist load-time pruning once so legacy idle rows do not consume
|
||||
// parse/filter work again on every launch.
|
||||
this.runStatusPersist()
|
||||
} else if (hydrated > 0) {
|
||||
// Why: prime from the raw on-disk bytes (not a re-serialization) so the
|
||||
// dedup is robust against future shape drift in serializeStatusFile.
|
||||
// Only prime when hydration was lossless — if entries were dropped
|
||||
// during sanitize, the in-memory map diverges from the on-disk bytes.
|
||||
this.lastWrittenJson = raw
|
||||
} else if (dropped > 0) {
|
||||
// Why: clean the stale on-disk file now so a user who has not run an
|
||||
// agent in 8+ days does not re-drop the same entries on every cold
|
||||
// boot. Synchronous variant is safe at start time and avoids
|
||||
// unref'd-timer-during-quit edge cases.
|
||||
this.runStatusPersist()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2477,11 +2477,9 @@ export function markClaudeLeadTurnInterrupted(state: HookListenerState, paneKey:
|
||||
state.claudeLeadStateByPaneKey.set(paneKey, { state: 'done', interrupted: true })
|
||||
}
|
||||
|
||||
/** Rebuild a pane's roster from a persisted status snapshot during hydration.
|
||||
* Restart loses the in-memory roster while the renderer replays the child
|
||||
* rows from disk; without reseeding, the next teammate-bearing Stop (whose
|
||||
* task ids never match lifecycle ids) would silently drop those rows. Stale
|
||||
* seeds self-heal: an empty background_tasks clears, activity refreshes. */
|
||||
/** Rebuild a pane's working roster from a persisted status snapshot. Live
|
||||
* activity confirms a seed after restart; a complete task inventory may reap
|
||||
* an unconfirmed seed whose finish hook arrived while Orca was offline. */
|
||||
export function seedClaudeSubagentRosterFromSnapshots(
|
||||
state: HookListenerState,
|
||||
paneKey: string,
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('claude-subagent-roster', () => {
|
||||
expect(roster.has('aprobe1-6d3cb5b5')).toBe(false)
|
||||
})
|
||||
|
||||
it('re-adds a resumed agent as working, keeping its original startedAt', () => {
|
||||
it('re-adds a resumed agent as working with a fresh startedAt', () => {
|
||||
const roster: ClaudeSubagentRoster = new Map()
|
||||
upsertWorkingClaudeSubagent(roster, 'aprobe1-6d3cb5b5', { agentType: 'probe1' }, 100)
|
||||
finishClaudeSubagent(roster, 'aprobe1-6d3cb5b5')
|
||||
|
||||
Reference in New Issue
Block a user