mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
fix(notifications): restore the blocked-child waiting state on non-Stop lead events
The lead-Stop gate revert (84880f2711) dropped the effective-state read from
the local lead path entirely, not just from the Stop case. That regressed a
separate contract: a child blocked on a permission prompt no longer held the
pane at `waiting` when the lead kept working, so the pane read `working` and
the user never saw what it was blocked on.
Restore the read, but AFTER the roster reap rather than before it. Post-reap,
a lead Stop sees an empty roster and still reports `done`, which is what
agent-hooks/server.ts requires; only non-Stop events observe live children.
The pre-reap `leadStopWithLiveSubagents` flag is unchanged and still carries
the "children were live at Stop" signal for the notification gate.
Both orderings are now pinned by tests: reading pre-reap breaks the Stop
contract, dropping the read breaks the blocked-child contract.
This commit is contained in:
@@ -3316,8 +3316,15 @@ function normalizeCodexEvent(
|
||||
normalizeOptionalField(hookPayload['model'], AGENT_MODEL_MAX_LENGTH) ??
|
||||
(eventName === 'SessionStart' ? undefined : previousLead?.model)
|
||||
})
|
||||
// Why: read the roster AFTER the reap. A blocked child must still hold the pane at `waiting` on
|
||||
// non-Stop lead events, but on Stop the roster is already empty so this stays `done` — the root
|
||||
// Stop contract in agent-hooks/server.ts depends on that.
|
||||
const effectiveState = codexRosterEffectiveState(
|
||||
state.codexSubagentRosterByPaneKey.get(paneKey),
|
||||
stateName
|
||||
)
|
||||
return buildCodexStatusPayload(state, eventName, promptText, paneKey, hookPayload, {
|
||||
stateName,
|
||||
stateName: effectiveState,
|
||||
updateLead: true,
|
||||
leadStopWithLiveSubagents
|
||||
})
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createHookListenerState, normalizeHookPayload } from './agent-hook-listener'
|
||||
import { makePaneKey } from './stable-pane-id'
|
||||
|
||||
const PANE_KEY = makePaneKey('tab-1', '11111111-1111-4111-8111-111111111111')
|
||||
let state: ReturnType<typeof createHookListenerState>
|
||||
|
||||
function codexEvent(payload: Record<string, unknown>) {
|
||||
return normalizeHookPayload(state, 'codex', { paneKey: PANE_KEY, payload }, 'production')
|
||||
}
|
||||
|
||||
function withBlockedChild() {
|
||||
codexEvent({ hook_event_name: 'SessionStart' })
|
||||
codexEvent({ hook_event_name: 'SubagentStart', agent_id: 'child-1', agent_type: 'explore' })
|
||||
codexEvent({ hook_event_name: 'PermissionRequest', agent_id: 'child-1' })
|
||||
}
|
||||
|
||||
// Why: the roster reap and the effective-state read must stay in this order. Swapping them
|
||||
// breaks one of the two contracts below, and each was a shipped bug at some point (#4375).
|
||||
describe('Codex lead state vs. a live subagent roster', () => {
|
||||
beforeEach(() => {
|
||||
state = createHookListenerState()
|
||||
})
|
||||
|
||||
it('holds the pane at waiting on a non-Stop lead event while a child is blocked', () => {
|
||||
withBlockedChild()
|
||||
// Read pre-reap: a lead that keeps working must not paper over a child's permission prompt,
|
||||
// or the pane looks busy and the user never sees what it is blocked on.
|
||||
expect(codexEvent({ hook_event_name: 'PostToolUse', tool_name: 'read' })?.payload.state).toBe(
|
||||
'waiting'
|
||||
)
|
||||
})
|
||||
|
||||
it('still reports done on a lead Stop, flagging that children were live', () => {
|
||||
withBlockedChild()
|
||||
const stopped = codexEvent({ hook_event_name: 'Stop' })
|
||||
// Read post-reap: agent-hooks/server.ts retires child rows on a root Stop and reports 'done'.
|
||||
// The pre-reap flag carries the "children were live" signal instead of downgrading the state.
|
||||
expect(stopped?.payload.state).toBe('done')
|
||||
expect(stopped?.payload.leadStopWithLiveSubagents).toBe(true)
|
||||
})
|
||||
|
||||
it('omits the flag on a lead Stop with no live children', () => {
|
||||
codexEvent({ hook_event_name: 'SessionStart' })
|
||||
codexEvent({ hook_event_name: 'SubagentStart', agent_id: 'child-1', agent_type: 'explore' })
|
||||
codexEvent({ hook_event_name: 'SubagentStop', agent_id: 'child-1' })
|
||||
const stopped = codexEvent({ hook_event_name: 'Stop' })
|
||||
expect(stopped?.payload.state).toBe('done')
|
||||
expect(stopped?.payload.leadStopWithLiveSubagents).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user