fix(notifications): announce finished only when the agent is fully done (#36992)

This commit is contained in:
Kelvin Amoaba
2026-11-01 16:03:29 +00:00
committed by GitHub
parent 9ce650839c
commit d95e2324dc
37 changed files with 891 additions and 211 deletions
@@ -123,39 +123,47 @@ describe('createProgramStatusProjector', () => {
expect(cleared.subagents).toBeUndefined()
})
it("keeps a finished turn working while a child runs, stamping the turn's own end for both rows", () => {
it('keeps a finished turn working while a child runs, leaving it to the all-clear to announce', () => {
const { feed, tick } = projector()
feed(osc('state=working:app=pi') + osc('state=working:id=subagents/run1:app=pi-subagents'))
tick(10)
expect(feed(osc('state=done:app=pi'))).toMatchObject([
{
state: 'working',
turnCompletedAt: 1010,
mainAgent: { state: 'done', outcome: 'success', stateStartedAt: 1010 },
subagents: [{ id: 'subagents/run1', state: 'working' }]
}
])
const [held] = feed(osc('state=done:app=pi'))
expect(held).toMatchObject({
state: 'working',
mainAgent: { state: 'done', outcome: 'success', stateStartedAt: 1010 },
subagents: [{ id: 'subagents/run1', state: 'working' }]
})
expect(held.turnCompletedAt).toBeUndefined()
tick(10)
const [settled] = feed(osc('state=done:id=subagents/run1:app=pi-subagents'))
expect(settled).toMatchObject({
state: 'done',
turnCompletedAt: 1010,
mainAgent: { state: 'done', outcome: 'success', stateStartedAt: 1010 }
})
expect(settled.turnCompletedAt).toBeUndefined()
expect(settled.subagents).toBeUndefined()
})
it("lapses a hold no child refreshed to the lead's done, keeping its completion stamp", () => {
it("stamps a failed turn's own end while a child still runs, for both rows", () => {
const { feed, tick } = projector()
feed(osc('state=working:app=pi') + osc('state=working:id=subagents/run1:app=pi-subagents'))
tick(10)
expect(feed(osc('state=error:app=pi'))).toMatchObject([
{ state: 'working', turnCompletedAt: 1010, mainAgent: { state: 'done', outcome: 'failure' } }
])
tick(10)
expect(feed(osc('state=done:id=subagents/run1:app=pi-subagents'))).toMatchObject([
{ state: 'done', turnCompletedAt: 1010 }
])
})
it("lapses a hold no child refreshed to the lead's done, keeping its verdict", () => {
const { feed } = projector()
feed(osc('state=working:app=pi') + osc('state=working:id=subagents/run1'))
const [held] = feed(osc('state=done:app=pi'))
expect(lapsedProgramStatusChildHold(held, 0, 999, 1000)).toBeNull()
const lapsed = lapsedProgramStatusChildHold(held, 0, 1000, 1000)
expect(lapsed).toMatchObject({
state: 'done',
turnCompletedAt: held.turnCompletedAt,
mainAgent: held.mainAgent
})
expect(lapsed).toMatchObject({ state: 'done', mainAgent: held.mainAgent })
expect(lapsed?.subagents).toBeUndefined()
const [settled] = feed(osc('state=done:id=subagents/run1'))
expect(lapsedProgramStatusChildHold(settled, 0, 1000, 1000)).toBeNull()
@@ -4,7 +4,10 @@ import type { AgentSubagentSnapshot, AgentSubagentState } from '../agent-status-
import { AGENT_SUBAGENT_ID_MAX_LENGTH } from '../agent-status-subagent-snapshot'
import type { AgentTurnOutcome } from '../agent-turn-outcome'
import { foldMainAgentWithRowChildWork } from '../agent-hook-row-child-work-fold'
import { isAgentStatusHeldOpenByChildWork } from '../agent-lead-status-fold'
import {
heldTurnCountsAsFinished,
isAgentStatusHeldOpenByChildWork
} from '../agent-lead-status-fold'
import { createProgramStatusOscScanner } from './osc-scanner'
import {
programStatusDisplayText,
@@ -210,13 +213,15 @@ export function createProgramStatusProjector(now: () => number = Date.now): Prog
const msg = programStatusDisplayText(root?.msg)
const subagents = snapshotChildren()
// Why: async children outlive a finished turn (pi-subagents); failed/idle children and boundaries hold nothing.
const state =
const held =
projection.state === 'done' && projection.sessionBoundary !== true
? foldMainAgentWithRowChildWork('done', {
payload: { subagents: subagents.filter(isRowHoldingChild) }
}).stateName
: projection.state
rootDoneHeldOpen ||= projection.state === 'done' && state !== 'done'
})
: null
const state = held?.stateName ?? projection.state
rootDoneHeldOpen ||=
held !== null && state !== 'done' && heldTurnCountsAsFinished(held, projection.outcome, false)
const payload: ParsedAgentStatusPayload = {
state,
prompt: '',
@@ -228,7 +233,7 @@ export function createProgramStatusProjector(now: () => number = Date.now): Prog
: {}),
...(projection.sessionBoundary ? { sessionBoundary: true } : {}),
...(subagents.length > 0 ? { subagents } : {}),
// Hook rows announce a held turn's own done now; the all-clear restates it so it is not announced twice.
// A held turn that counts as finished is announced now; the all-clear restates it so it is not announced twice.
...(rootDoneHeldOpen ? { turnCompletedAt: rootStateStartedAt } : {}),
mainAgent: {
state: projection.state,