mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
fix(notifications): gate remote Codex lead Stops too
SSH/relay panes never reach normalizeHookPayload's codex branch — the main process re-derives their roster in reconcileRemoteCodexState, which does its own reap. So the previous commit covered local Codex only, and remote leads kept spamming. Same pre-reap capture, applied on that path.
This commit is contained in:
@@ -7,6 +7,7 @@ import { dispatchTerminalNotification } from './use-notification-dispatch'
|
||||
import {
|
||||
createHookListenerState,
|
||||
normalizeHookPayload,
|
||||
reconcileRemoteCodexState,
|
||||
type HookListenerState
|
||||
} from '../../../../shared/agent-hook-listener'
|
||||
import type { AgentHookSource } from '../../../../shared/agent-hook-relay'
|
||||
@@ -279,6 +280,33 @@ describe('issue #4375 — non-turn-end notification spam', () => {
|
||||
expect(childStop?.payload.leadStopWithLiveSubagents).toBeUndefined()
|
||||
})
|
||||
|
||||
it('gates a lead Stop arriving over SSH/relay, which reconciles on a separate path', () => {
|
||||
// Why: remote panes never reach normalizeHookPayload's codex branch — the main process
|
||||
// re-derives their roster in reconcileRemoteCodexState, which does its own reap. Without the
|
||||
// same pre-reap capture there, the fix would cover local Codex only.
|
||||
const listenerState: HookListenerState = createHookListenerState()
|
||||
const remote = (
|
||||
ev: string,
|
||||
agentId: string | undefined,
|
||||
payload: Parameters<typeof reconcileRemoteCodexState>[4],
|
||||
previous?: Parameters<typeof reconcileRemoteCodexState>[5]
|
||||
): ReturnType<typeof reconcileRemoteCodexState> =>
|
||||
reconcileRemoteCodexState(listenerState, PANE_KEY, ev, agentId, payload, previous)
|
||||
|
||||
const working = { state: 'working', prompt: 'go', agentType: 'codex' } as const
|
||||
remote('UserPromptSubmit', undefined, { ...working })
|
||||
// The relay reports the live child roster alongside the lead's own state.
|
||||
remote('SubagentStart', 'child-1', {
|
||||
...working,
|
||||
subagents: [{ id: 'child-1', state: 'working', agentType: 'reviewer', startedAt: Date.now() }]
|
||||
})
|
||||
|
||||
const leadStop = remote('Stop', undefined, { state: 'done', prompt: 'go', agentType: 'codex' })
|
||||
expect(leadStop.state).toBe('done')
|
||||
expect(leadStop.subagents).toBeUndefined()
|
||||
expect(leadStop.leadStopWithLiveSubagents).toBe(true)
|
||||
})
|
||||
|
||||
// KNOWN-OPEN residual, deliberately out of scope for the roster fix. A bare mid-turn
|
||||
// Stop (no subagent) that resumes AFTER the 1.5s quiet window still notifies. There is
|
||||
// no principled bound to widen the window to, and widening it delays every real Codex
|
||||
|
||||
@@ -3133,12 +3133,17 @@ export function reconcileRemoteCodexState(
|
||||
if (payload.subagents) {
|
||||
seedCodexSubagentRoster(roster, payload.subagents)
|
||||
}
|
||||
let leadStopWithLiveSubagents = false
|
||||
if (agentId) {
|
||||
if (eventName === 'SubagentStop') {
|
||||
finishCodexSubagent(roster, agentId)
|
||||
}
|
||||
} else {
|
||||
const leadState = codexLeadStateForHookEvent(eventName)
|
||||
// Why: same pre-reap capture as the local path — SSH/relay panes reach the notification
|
||||
// coordinator through here, so without it remote Codex leads keep spamming (#4375).
|
||||
leadStopWithLiveSubagents =
|
||||
eventName === 'Stop' && codexRosterEffectiveState(roster, 'done') !== 'done'
|
||||
if (eventName === 'SessionStart' || eventName === 'Stop') {
|
||||
roster.clear()
|
||||
}
|
||||
@@ -3159,7 +3164,8 @@ export function reconcileRemoteCodexState(
|
||||
...payload,
|
||||
state: codexRosterEffectiveState(roster, lead.state),
|
||||
model: lead.model ?? payload.model,
|
||||
subagents: codexRosterToSnapshots(roster)
|
||||
subagents: codexRosterToSnapshots(roster),
|
||||
...(leadStopWithLiveSubagents ? { leadStopWithLiveSubagents: true } : {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user