mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
fix(notifications): announce finished only when the agent is fully done (#36992)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user