Quiet-window Pi/OMP intermediate done so resumed work cancels notification (#6361) (#6558)

Pi (pi.dev) and OMP are goal/mission agents whose normalizePiCompatibleEvent
maps milestone agent_end -> hook state 'done' while they are still working.
observeHookStatus gated the quiet window on `workingStatusObserved`, so these
intermediate 'done' events (workingStatusObserved === false) fell through to an
immediate "agent finished" notification while the TUI kept spinning, and a
follow-up working event could not cancel it.

Route Pi/OMP 'done' through the existing quiet window via a new
doneShouldUseQuietWindow() predicate (delegating to a shared
isPiCompatibleAgentType on the canonical PiAgentKind), so resumed work cancels
the premature notification. Codex and other turn-end-only producers keep their
immediate dispatch.

Also harden the process-exit backstop: skip the agent-evidence teardown while a
quiet-window 'done' is pending, otherwise a poll that finds the agent gone would
clear hasAgentRunEvidence and the timer would silently drop the real completion.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil 2026-06-28 16:52:55 -07:00 committed by GitHub
parent 8c51ab12fa
commit 6154aded34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 151 additions and 3 deletions

View File

@ -983,8 +983,6 @@ describe('agent completion coordinator', () => {
'gemini',
'opencode',
'cursor',
'pi',
'omp',
'droid',
'grok',
'devin',
@ -1010,6 +1008,132 @@ describe('agent completion coordinator', () => {
expect(dispatchCompletion).toHaveBeenCalledWith(agentType)
})
it.each(['pi', 'omp'])(
'defers a %s milestone done without prior working through the quiet window',
(agentType) => {
const dispatchCompletion = vi.fn()
const coordinator = createAgentCompletionCoordinator({
paneKey: 'tab-1:leaf-1',
getPtyId: () => 'pty-1',
getSettings: () => null,
inspectProcess: vi.fn(),
dispatchCompletion,
isLive: () => true
})
// Pi/OMP emit agent_end ('done') between milestones with no prior 'working';
// the done must wait out the quiet window instead of firing immediately.
coordinator.observeHookStatus({
state: 'done',
prompt: 'run the mission',
agentType
})
expect(coordinator.hasPendingHookDoneCompletion()).toBe(true)
vi.advanceTimersByTime(HOOK_DONE_QUIET_MS - 1)
expect(dispatchCompletion).not.toHaveBeenCalled()
vi.advanceTimersByTime(1)
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
expect(dispatchCompletion).toHaveBeenCalledWith(
agentType,
expect.objectContaining({ source: 'hook', quietedHookDone: true })
)
}
)
it('suppresses a Pi milestone done when work resumes before the quiet window', () => {
const dispatchCompletion = vi.fn()
const coordinator = createAgentCompletionCoordinator({
paneKey: 'tab-1:leaf-1',
getPtyId: () => 'pty-1',
getSettings: () => null,
inspectProcess: vi.fn(),
dispatchCompletion,
isLive: () => true
})
coordinator.observeHookStatus({
state: 'done',
prompt: 'run the mission',
agentType: 'pi'
})
expect(coordinator.hasPendingHookDoneCompletion()).toBe(true)
// Pi resumes (a tool_call mapped to 'working') before the window elapses,
// which must cancel the premature "finished".
coordinator.observeHookStatus({
state: 'working',
prompt: 'run the mission',
agentType: 'pi'
})
expect(coordinator.hasPendingHookDoneCompletion()).toBe(false)
vi.advanceTimersByTime(HOOK_DONE_QUIET_MS)
expect(dispatchCompletion).not.toHaveBeenCalled()
})
it('still dispatches a Codex done-without-prior-working immediately', () => {
const dispatchCompletion = vi.fn()
const coordinator = createAgentCompletionCoordinator({
paneKey: 'tab-1:leaf-1',
getPtyId: () => 'pty-1',
getSettings: () => null,
inspectProcess: vi.fn(),
dispatchCompletion,
isLive: () => true
})
// Codex only emits 'done' at turn end, so it must keep its immediate dispatch.
coordinator.observeHookStatus({
state: 'done',
prompt: 'fix the bug',
agentType: 'codex'
})
expect(coordinator.hasPendingHookDoneCompletion()).toBe(false)
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
})
it('still fires a pending Pi done when process inspection sees the agent exit first', async () => {
// Why: a process-exit probe landing inside the quiet window must not tear
// down agent evidence, or the pending hook 'done' would be silently dropped.
let foregroundProcess: string | null = 'pi'
const dispatchCompletion = vi.fn()
const coordinator = createAgentCompletionCoordinator({
paneKey: 'tab-1:leaf-1',
getPtyId: () => 'pty-1',
getSettings: () => null,
inspectProcess: vi.fn(async () => processResult(foregroundProcess)),
dispatchCompletion,
isLive: () => true
})
coordinator.startProcessTracking()
vi.advanceTimersByTime(2_000)
await flushAsyncTicks()
coordinator.observeHookStatus({
state: 'done',
prompt: 'run the mission',
agentType: 'pi'
})
expect(coordinator.hasPendingHookDoneCompletion()).toBe(true)
// The agent process disappears mid-window; the cadence poll must not drop
// the pending completion.
foregroundProcess = null
vi.advanceTimersByTime(750)
await flushAsyncTicks()
expect(dispatchCompletion).not.toHaveBeenCalled()
vi.advanceTimersByTime(HOOK_DONE_QUIET_MS)
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
expect(dispatchCompletion).toHaveBeenCalledWith(
'pi',
expect.objectContaining({ source: 'hook' })
)
})
it('notifies once after a Cursor tool-heavy turn, not on each shell hook', () => {
const dispatchCompletion = vi.fn()
const coordinator = createAgentCompletionCoordinator({

View File

@ -16,6 +16,7 @@ import type {
AgentCompletionStatusSnapshot
} from './agent-completion-coordinator-types'
import type { RuntimeTerminalProcessInspection } from '@/runtime/runtime-terminal-inspection'
import { isPiCompatibleAgentType } from '../../../../shared/pi-agent-kind'
import {
titleHasExplicitAgentIdentity,
titleIsInconclusiveNativeDroidTitle
@ -157,6 +158,12 @@ export function createAgentCompletionCoordinator(
return payload.agentType?.trim().toLowerCase() || null
}
function doneShouldUseQuietWindow(payload: AgentCompletionStatusSnapshot): boolean {
// Why: Pi/OMP emit milestone 'done' while still working, so route their done
// through the quiet window (like a resumed turn) so later work can cancel it.
return workingStatusObserved || isPiCompatibleAgentType(hookCompletionAgentIdentity(payload))
}
function hookAttentionToken(payload: AgentCompletionStatusSnapshot): string {
const identity = hookCompletionIdentity(payload)
if (identity) {
@ -416,6 +423,12 @@ export function createAgentCompletionCoordinator(
handleRecognizedProcess(recognized)
return true
}
if (pendingHookDoneTimer !== null) {
// Why: a pending quiet-window 'done' is the authoritative completion;
// tearing down agent evidence here would make the timer drop it.
scheduleNextPoll()
return false
}
if (lastForegroundAgent && hasAgentRunEvidence) {
if (result.hasChildProcesses) {
// Why: Codex can briefly report a shell/null foreground while its TUI or
@ -708,7 +721,7 @@ export function createAgentCompletionCoordinator(
// backstops duplicate the same completion.
currentTurn += 1
}
if (payload.state === 'done' && workingStatusObserved) {
if (payload.state === 'done' && doneShouldUseQuietWindow(payload)) {
lastCompletionIdentity = hookIdentity
? {
source: 'hook',

View File

@ -13,6 +13,17 @@ import { getCommandTokenPathBasename, getFirstCommandToken } from './command-tok
*/
export type PiAgentKind = 'pi' | 'omp'
/**
* True when `agentType` names a Pi-compatible (goal/mission) kind. These agents
* emit milestone `agent_end` events between steps while still working, so they
* are treated differently from agents that only signal completion at turn end.
*/
export function isPiCompatibleAgentType(
agentType: string | null | undefined
): agentType is PiAgentKind {
return agentType === 'pi' || agentType === 'omp'
}
const OMP_LAUNCH_CMD = TUI_AGENT_CONFIG.omp.launchCmd
// Why: regex carved to avoid matching `pi` inside `pip`, `mpi`, `api`,