Fix noisy Grok tool notifications (#6306)

This commit is contained in:
Neil 2026-06-26 02:19:25 -07:00 committed by GitHub
parent 19ab395a26
commit fa00464df9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 284 additions and 1 deletions

View File

@ -1,6 +1,7 @@
/* eslint-disable max-lines -- Why: notification edge cases share one module-scoped coordinator, so keeping setup and regression cases together prevents brittle cross-file mock resets. */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { ParsedAgentStatusPayload } from '../../../shared/agent-status-types'
import { createHookListenerState, normalizeHookPayload } from '../../../shared/agent-hook-listener'
const dispatchTerminalNotification = vi.fn()
@ -414,6 +415,83 @@ describe('agent hook completion notifications', () => {
expect(dispatchTerminalNotification).not.toHaveBeenCalled()
})
it('does not notify on Grok routine permission prompt notifications during tool use', async () => {
const { observeAgentHookCompletionForNotification } =
await import('./agent-hook-completion-notifications')
const listenerState = createHookListenerState()
const observeGrokHook = (payload: Record<string, unknown>): void => {
const event = normalizeHookPayload(
listenerState,
'grok',
{
paneKey,
tabId: 'tab-1',
worktreeId: 'wt-1',
payload
},
'production'
)
if (!event) {
return
}
observeAgentHookCompletionForNotification({
paneKey: event.paneKey,
worktreeId: event.worktreeId ?? 'wt-1',
payload: event.payload
})
}
observeGrokHook({
hookEventName: 'user_prompt_submit',
prompt: 'run shell and glob'
})
observeGrokHook({
hookEventName: 'pre_tool_use',
toolName: 'Shell',
toolInput: { command: 'echo hi' }
})
observeGrokHook({
hookEventName: 'notification',
notificationType: 'permission_prompt',
message: 'Tool permission requested',
level: 'info'
})
observeGrokHook({
hookEventName: 'pre_tool_use',
toolName: 'Glob',
toolInput: { pattern: '**/package.json' }
})
observeGrokHook({
hookEventName: 'notification',
notificationType: 'permission_prompt',
message: 'Tool permission requested',
level: 'info'
})
expect(dispatchTerminalNotification).not.toHaveBeenCalled()
observeGrokHook({
hookEventName: 'stop',
lastAssistantMessage: 'Done.'
})
vi.advanceTimersByTime(HOOK_DONE_QUIET_MS)
expect(dispatchTerminalNotification).toHaveBeenCalledTimes(1)
expect(dispatchTerminalNotification).toHaveBeenCalledWith(
'wt-1',
expect.objectContaining({
source: 'agent-task-complete',
paneKey,
agentStatusSnapshot: expect.objectContaining({
state: 'done',
agentType: 'grok',
prompt: 'run shell and glob',
lastAssistantMessage: 'Done.'
})
})
)
})
it('suppresses an internal milestone completion when hook work resumes before quiet', async () => {
const { observeAgentHookCompletionForNotification } =
await import('./agent-hook-completion-notifications')

View File

@ -1303,6 +1303,45 @@ describe('shared agent-hook-listener', () => {
})
})
it('ignores Grok routine permission prompt notifications during tool use', () => {
normalizeHookPayload(
state,
'grok',
{ paneKey: PANE_KEY, payload: { hookEventName: 'UserPromptSubmit', prompt: 'ship it' } },
'production'
)
normalizeHookPayload(
state,
'grok',
{
paneKey: PANE_KEY,
payload: {
hookEventName: 'PreToolUse',
toolName: 'Shell',
toolInput: { command: 'echo hi' }
}
},
'production'
)
const event = normalizeHookPayload(
state,
'grok',
{
paneKey: PANE_KEY,
payload: {
hookEventName: 'Notification',
notificationType: 'permission_prompt',
message: 'Tool permission requested',
level: 'info'
}
},
'production'
)
expect(event).toBeNull()
})
it('reads Grok final assistant text from chat history on Stop', () => {
const tmpDir = mkdtempSync(join(tmpdir(), 'orca-grok-session-'))
const sessionId = '019e37f4-5135-7b63-a4ab-6d13aa6bf528'

View File

@ -1864,6 +1864,28 @@ function isGrokPermissionNotification(message: string | undefined): boolean {
)
}
function getGrokNotificationType(hookPayload: Record<string, unknown>): string | undefined {
return (
readString(hookPayload, 'notificationType') ??
readString(hookPayload, 'notification_type') ??
readString(hookPayload, 'type')
)
}
function isGrokRoutinePermissionPromptNotification(
notificationType: string | undefined,
message: string | undefined,
level: string | undefined
): boolean {
// Why: Grok emits this info notification before each tool even under
// bypassPermissions; PreToolUse already captures progress without paging users.
return (
isGrokEvent(notificationType, 'permission_prompt') &&
message?.trim().toLowerCase() === 'tool permission requested' &&
(!level || level.trim().toLowerCase() === 'info')
)
}
function isGrokIdleNotification(message: string | undefined): boolean {
if (!message) {
return false
@ -2860,6 +2882,8 @@ function normalizeGrokEvent(
}
const notificationMessage = readString(hookPayload, 'message')
const notificationType = getGrokNotificationType(hookPayload)
const notificationLevel = readString(hookPayload, 'level')
let stateName: 'working' | 'waiting' | 'done' | null = null
if (
isGrokEvent(
@ -2873,6 +2897,15 @@ function normalizeGrokEvent(
stateName = 'working'
} else if (isGrokEvent(eventName, 'stop', 'session_end')) {
stateName = 'done'
} else if (
isGrokEvent(eventName, 'notification') &&
isGrokRoutinePermissionPromptNotification(
notificationType,
notificationMessage,
notificationLevel
)
) {
return null
} else if (
isGrokEvent(eventName, 'notification') &&
isGrokPermissionNotification(notificationMessage)

View File

@ -10,7 +10,11 @@ import {
waitForTerminalOutput
} from './helpers/terminal'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import { emitCodexHookStatus, readHookEndpoint } from './helpers/agent-hook-endpoint'
import {
emitCodexHookStatus,
emitGrokHookPayload,
readHookEndpoint
} from './helpers/agent-hook-endpoint'
type NotificationDispatch = {
source?: string
@ -253,6 +257,106 @@ test.describe('Droid notifications', () => {
.toBe(true)
})
test('Grok routine permission prompt hooks stay working and do not notify', async ({
orcaPage,
electronApp
}) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
await installMainProcessNotificationDispatchSpy(electronApp)
const endpoint = await readHookEndpoint(electronApp)
const ptyId = await waitForActivePanePtyId(orcaPage)
const readyMarker = `__GROK_HOOK_NOTIFY_READY_${Date.now()}__`
await sendToTerminal(orcaPage, ptyId, `printf '${readyMarker}\\n'\r`)
await waitForTerminalOutput(orcaPage, readyMarker)
const { paneKey, worktreeId } = await waitForActivePaneHookDescriptor(orcaPage)
const prompt = `grok-hook-notify-${Date.now()}`
await emitGrokHookPayload(endpoint, {
paneKey,
worktreeId,
payload: {
hookEventName: 'user_prompt_submit',
prompt
}
})
await expect
.poll(
async () =>
(await getAgentStatuses(orcaPage)).some(
(status) =>
status.agentType === 'grok' && status.state === 'working' && status.prompt === prompt
),
{
timeout: 30_000,
message: 'Grok UserPromptSubmit hook did not reach renderer agent status'
}
)
.toBe(true)
await emitGrokHookPayload(endpoint, {
paneKey,
worktreeId,
payload: {
hookEventName: 'pre_tool_use',
toolName: 'Shell',
toolInput: { command: 'echo hi' }
}
})
await emitGrokHookPayload(endpoint, {
paneKey,
worktreeId,
payload: {
hookEventName: 'notification',
notificationType: 'permission_prompt',
message: 'Tool permission requested',
level: 'info'
}
})
await orcaPage.waitForTimeout(500)
expect(
(await getAgentStatuses(orcaPage)).some(
(status) =>
status.agentType === 'grok' && status.prompt === prompt && status.state === 'waiting'
)
).toBe(false)
expect(
(await getNotificationDispatches(electronApp)).filter(
(dispatch) => dispatch.source === 'agent-task-complete'
)
).toEqual([])
const finalMessage = `Grok hook completed ${Date.now()}`
await emitGrokHookPayload(endpoint, {
paneKey,
worktreeId,
payload: {
hookEventName: 'stop',
lastAssistantMessage: finalMessage
}
})
await expect
.poll(
async () =>
(await getAgentStatuses(orcaPage)).some(
(status) =>
status.agentType === 'grok' &&
status.state === 'done' &&
status.prompt === prompt &&
status.lastAssistantMessage === finalMessage
),
{
timeout: 30_000,
message: 'Grok Stop hook did not reach renderer agent status'
}
)
.toBe(true)
})
test('recognized agent title completion dispatches one task-complete notification', async ({
orcaPage,
electronApp

View File

@ -89,3 +89,32 @@ export async function emitCodexHookStatus(
throw new Error(`Codex hook POST returned ${response.status}`)
}
}
export async function emitGrokHookPayload(
endpoint: AgentHookEndpoint,
event: {
paneKey: string
worktreeId: string
payload: Record<string, unknown>
}
): Promise<void> {
const [tabId] = event.paneKey.split(':')
const response = await fetch(`http://127.0.0.1:${endpoint.port}/hook/grok`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Orca-Agent-Hook-Token': endpoint.token
},
body: JSON.stringify({
paneKey: event.paneKey,
tabId,
worktreeId: event.worktreeId,
env: endpoint.env,
version: endpoint.version,
payload: event.payload
})
})
if (response.status !== 204) {
throw new Error(`Grok hook POST returned ${response.status}`)
}
}