mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 08:02:54 +00:00
fix(agent-completion): stop mid-turn permission/elicitation hooks from firing false completion notifications
isCompletionHookState() treated 'waiting' (PermissionRequest) and 'blocked' (Copilot elicitation dialog) as turn-completion states, so observeHookStatus() called dispatchCompletion() on those intermediate, mid-turn hook events. That fired false agent-task-complete desktop notifications during permission prompts and tool calls instead of only at terminal turn end, spamming the user. Narrow isCompletionHookState to 'done' only, add isAttentionHookState covering 'waiting'/'blocked', and early-return those states in observeHookStatus after clearing any pending hook-done quiet-window timer. This prevents a permission prompt arriving within the 1.5s quiet window after a 'done' from firing a delayed false completion. The sidebar smart-attention indicator still surfaces 'waiting'/'blocked' inline, so the attention signal is not lost. Supersedes community PR #6316. Fixes #4375 Co-authored-by: Rod Boev <rodboev@users.noreply.github.com>
This commit is contained in:
@@ -1054,7 +1054,7 @@ describe('agent completion coordinator', () => {
|
||||
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('would spam Cursor notifications if shell hooks still mapped to waiting', () => {
|
||||
it('does not dispatch completion when waiting states arrive mid-turn', () => {
|
||||
const dispatchCompletion = vi.fn()
|
||||
const coordinator = createAgentCompletionCoordinator({
|
||||
paneKey: 'tab-1:leaf-1',
|
||||
@@ -1090,7 +1090,64 @@ describe('agent completion coordinator', () => {
|
||||
toolInput: 'git status'
|
||||
})
|
||||
|
||||
expect(dispatchCompletion).toHaveBeenCalledTimes(2)
|
||||
expect(dispatchCompletion).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('does not dispatch completion when blocked state arrives mid-turn', () => {
|
||||
const dispatchCompletion = vi.fn()
|
||||
const coordinator = createAgentCompletionCoordinator({
|
||||
paneKey: 'tab-1:leaf-1',
|
||||
getPtyId: () => 'pty-1',
|
||||
getSettings: () => null,
|
||||
inspectProcess: vi.fn(),
|
||||
dispatchCompletion,
|
||||
isLive: () => true
|
||||
})
|
||||
|
||||
const turn = {
|
||||
prompt: 'fix the bug',
|
||||
agentType: 'copilot' as const
|
||||
}
|
||||
|
||||
coordinator.observeHookStatus({ state: 'working', ...turn })
|
||||
coordinator.observeHookStatus({
|
||||
state: 'blocked',
|
||||
...turn,
|
||||
toolName: 'Shell',
|
||||
toolInput: 'npm install'
|
||||
})
|
||||
|
||||
expect(dispatchCompletion).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('cancels a pending done timer when a waiting state arrives 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
|
||||
})
|
||||
|
||||
const turn = {
|
||||
prompt: 'fix the bug',
|
||||
agentType: 'cursor' as const
|
||||
}
|
||||
|
||||
coordinator.observeHookStatus({ state: 'working', ...turn })
|
||||
coordinator.observeHookStatus({ state: 'done', ...turn, lastAssistantMessage: 'Done.' })
|
||||
// waiting arrives before the 1.5s quiet window expires; must cancel the timer
|
||||
coordinator.observeHookStatus({
|
||||
state: 'waiting',
|
||||
...turn,
|
||||
toolName: 'Shell',
|
||||
toolInput: 'pnpm test'
|
||||
})
|
||||
vi.advanceTimersByTime(HOOK_DONE_QUIET_MS)
|
||||
|
||||
expect(dispatchCompletion).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('keeps a generic title completion pending long enough for the first remote inspection', async () => {
|
||||
|
||||
@@ -43,7 +43,13 @@ const COMPLETION_REPLAY_GUARD_MS = 1_000
|
||||
const HOOK_DONE_QUIET_MS = 1_500
|
||||
|
||||
function isCompletionHookState(state: ParsedAgentStatusPayload['state']): boolean {
|
||||
return state === 'done' || state === 'waiting' || state === 'blocked'
|
||||
return state === 'done'
|
||||
}
|
||||
|
||||
function isAttentionHookState(state: ParsedAgentStatusPayload['state']): boolean {
|
||||
// 'waiting' = PermissionRequest; 'blocked' = Copilot elicitation dialog.
|
||||
// Agent is alive but paused; not a completion signal.
|
||||
return state === 'waiting' || state === 'blocked'
|
||||
}
|
||||
|
||||
export function createAgentCompletionCoordinator(
|
||||
@@ -622,10 +628,11 @@ export function createAgentCompletionCoordinator(
|
||||
dropPendingTitle()
|
||||
return
|
||||
}
|
||||
if (isAttentionHookState(payload.state)) {
|
||||
clearPendingHookDone()
|
||||
return
|
||||
}
|
||||
if (isCompletionHookState(payload.state)) {
|
||||
if (payload.state !== 'done') {
|
||||
clearPendingHookDone()
|
||||
}
|
||||
if (isRecognizedAgentType(payload.agentType)) {
|
||||
establishAgentEvidence()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user