mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(agent-status): keep harness-injected turns out of sidebar prompt labels (#7274)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -1,25 +1,12 @@
|
||||
import { isTextBlock, type NativeChatMessage } from '../../../../shared/native-chat-types'
|
||||
import { isHarnessInjectedUserTurnText } from '../../../../shared/harness-injected-user-turns'
|
||||
|
||||
// Why: the harness injects machinery into the agent's conversation as user-role
|
||||
// turns — background task-notifications, system reminders, local-command output,
|
||||
// slash-command envelopes, interruption/compaction notices. These land in the
|
||||
// transcript but are not real user messages, so the chat filters them out (they
|
||||
// were confusingly rendered as the user's own bubbles). Mirrors the mobile
|
||||
// predicate in mobile/src/session/mobile-native-chat-noise.ts.
|
||||
|
||||
const NOISE_PREFIXES = [
|
||||
'<task-notification>',
|
||||
'<system-reminder>',
|
||||
'<local-command-stdout>',
|
||||
'<local-command-caveat>',
|
||||
'<command-name>',
|
||||
'<command-message>',
|
||||
'<command-args>',
|
||||
'<bash-',
|
||||
'[request interrupted',
|
||||
'caveat: the messages below were generated by the user while running local commands',
|
||||
'this session is being continued from a previous conversation'
|
||||
]
|
||||
// Why: harness machinery turns land in the transcript but are not real user
|
||||
// messages, so the chat filters them out (they were confusingly rendered as
|
||||
// the user's own bubbles). The prefix list lives in
|
||||
// src/shared/harness-injected-user-turns.ts, shared with the agent-status
|
||||
// prompt pipeline. Mirrors the mobile predicate in
|
||||
// mobile/src/session/mobile-native-chat-noise.ts.
|
||||
|
||||
function messageText(message: NativeChatMessage): string {
|
||||
return message.blocks
|
||||
@@ -40,11 +27,7 @@ export function isNoiseMessage(message: NativeChatMessage): boolean {
|
||||
if (message.blocks.some((b) => b.type === 'tool-call' || b.type === 'tool-result')) {
|
||||
return false
|
||||
}
|
||||
const text = messageText(message).toLowerCase()
|
||||
if (text.length === 0) {
|
||||
return false
|
||||
}
|
||||
return NOISE_PREFIXES.some((prefix) => text.startsWith(prefix))
|
||||
return isHarnessInjectedUserTurnText(messageText(message))
|
||||
}
|
||||
|
||||
/** Drop harness-noise messages from a transcript. */
|
||||
|
||||
@@ -808,6 +808,51 @@ describe('shared agent-hook-listener', () => {
|
||||
expect(event).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the cached prompt when a harness-injected turn fires UserPromptSubmit', () => {
|
||||
normalizeHookPayload(
|
||||
state,
|
||||
'claude',
|
||||
{ paneKey: PANE_KEY, payload: { hook_event_name: 'UserPromptSubmit', prompt: 'fix login' } },
|
||||
'production'
|
||||
)
|
||||
// Why: the harness injects background task notifications as user turns;
|
||||
// they must not replace the user's real prompt in status labels.
|
||||
const event = normalizeHookPayload(
|
||||
state,
|
||||
'claude',
|
||||
{
|
||||
paneKey: PANE_KEY,
|
||||
payload: {
|
||||
hook_event_name: 'UserPromptSubmit',
|
||||
prompt: '<task-notification> <task-id>bzthj2b8r</task-id> <tool-use-id>t1</tool-use-id>'
|
||||
}
|
||||
},
|
||||
'production'
|
||||
)
|
||||
expect(event).not.toBeNull()
|
||||
expect(event!.payload.state).toBe('working')
|
||||
expect(event!.payload.prompt).toBe('fix login')
|
||||
expect(event!.hasExplicitPrompt).toBe(false)
|
||||
})
|
||||
|
||||
it('resolves an empty prompt for a harness-injected turn with nothing cached', () => {
|
||||
const event = normalizeHookPayload(
|
||||
state,
|
||||
'claude',
|
||||
{
|
||||
paneKey: PANE_KEY,
|
||||
payload: {
|
||||
hook_event_name: 'UserPromptSubmit',
|
||||
prompt: '<system-reminder>background context</system-reminder>'
|
||||
}
|
||||
},
|
||||
'production'
|
||||
)
|
||||
expect(event).not.toBeNull()
|
||||
expect(event!.payload.prompt).toBe('')
|
||||
expect(event!.hasExplicitPrompt).toBe(false)
|
||||
})
|
||||
|
||||
it('isolates caches between listener instances', () => {
|
||||
const a = createHookListenerState()
|
||||
const b = createHookListenerState()
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
type AgentProviderSessionMetadata
|
||||
} from './agent-session-resume'
|
||||
import { parsePaneKey } from './stable-pane-id'
|
||||
import { isHarnessInjectedUserTurnText } from './harness-injected-user-turns'
|
||||
|
||||
/** Maximum request body size accepted by the listener (1 MB). */
|
||||
export const HOOK_REQUEST_MAX_BYTES = 1_000_000
|
||||
@@ -392,6 +393,12 @@ function resolvePrompt(
|
||||
promptText: string,
|
||||
options?: { resetOnNewTurn?: boolean }
|
||||
): string {
|
||||
// Why: harness-injected turns (task notifications, system reminders) fire
|
||||
// UserPromptSubmit but are not the user's ask — keep the cached real prompt
|
||||
// instead of surfacing raw machinery tags in status labels.
|
||||
if (isHarnessInjectedUserTurnText(promptText)) {
|
||||
return state.lastPromptByPaneKey.get(paneKey) ?? ''
|
||||
}
|
||||
if (options?.resetOnNewTurn) {
|
||||
state.lastPromptByPaneKey.delete(paneKey)
|
||||
}
|
||||
@@ -2085,6 +2092,11 @@ function hasExplicitUserPrompt(
|
||||
if (extractedPrompt.text.length === 0) {
|
||||
return false
|
||||
}
|
||||
// Why: harness-injected machinery turns are not proof of a user submit —
|
||||
// they must not count for prompt-sent telemetry or permission stickiness.
|
||||
if (isHarnessInjectedUserTurnText(extractedPrompt.text)) {
|
||||
return false
|
||||
}
|
||||
// Why: bare `message` fields often contain permission or status copy. They
|
||||
// may update visible status prompts, but they are not proof of user submit.
|
||||
if (extractedPrompt.source === 'message') {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { isHarnessInjectedUserTurnText } from './harness-injected-user-turns'
|
||||
|
||||
describe('isHarnessInjectedUserTurnText', () => {
|
||||
it('matches harness machinery turns by prefix', () => {
|
||||
expect(
|
||||
isHarnessInjectedUserTurnText(
|
||||
'<task-notification> <task-id>bzthj2b8r</task-id> <tool-use-id>toolu_01abc</tool-use-id>'
|
||||
)
|
||||
).toBe(true)
|
||||
expect(isHarnessInjectedUserTurnText('<system-reminder>context</system-reminder>')).toBe(true)
|
||||
expect(isHarnessInjectedUserTurnText('<command-name>/review</command-name>')).toBe(true)
|
||||
expect(isHarnessInjectedUserTurnText('<local-command-stdout>ok</local-command-stdout>')).toBe(
|
||||
true
|
||||
)
|
||||
expect(isHarnessInjectedUserTurnText('[Request interrupted by user]')).toBe(true)
|
||||
expect(
|
||||
isHarnessInjectedUserTurnText('This session is being continued from a previous conversation.')
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('is case-insensitive and ignores surrounding whitespace', () => {
|
||||
expect(isHarnessInjectedUserTurnText(' <TASK-NOTIFICATION> done')).toBe(true)
|
||||
expect(isHarnessInjectedUserTurnText('\n<System-Reminder> hi')).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps real user prompts, including ones that mention the tags', () => {
|
||||
expect(isHarnessInjectedUserTurnText('fix the login bug')).toBe(false)
|
||||
expect(isHarnessInjectedUserTurnText('why does <task-notification> show in the sidebar?')).toBe(
|
||||
false
|
||||
)
|
||||
expect(isHarnessInjectedUserTurnText('')).toBe(false)
|
||||
expect(isHarnessInjectedUserTurnText(' ')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
// Why: agent harnesses (Claude Code and its forks) inject machinery into the
|
||||
// conversation as user-role turns — background task notifications, system
|
||||
// reminders, slash-command envelopes, local-command output, interruption and
|
||||
// compaction notices. These fire user-prompt hooks and land in transcripts,
|
||||
// but they are not something the user typed, so prompt-derived UI must not
|
||||
// surface them.
|
||||
const HARNESS_INJECTED_TURN_PREFIXES = [
|
||||
'<task-notification>',
|
||||
'<system-reminder>',
|
||||
'<local-command-stdout>',
|
||||
'<local-command-caveat>',
|
||||
'<command-name>',
|
||||
'<command-message>',
|
||||
'<command-args>',
|
||||
'<bash-',
|
||||
'[request interrupted',
|
||||
'caveat: the messages below were generated by the user while running local commands',
|
||||
'this session is being continued from a previous conversation'
|
||||
]
|
||||
|
||||
/** True when prompt-like text is a harness-injected machinery turn rather
|
||||
* than something the user typed. Prefix match on trimmed, lowercased text. */
|
||||
export function isHarnessInjectedUserTurnText(text: string): boolean {
|
||||
const normalized = text.trim().toLowerCase()
|
||||
if (normalized.length === 0) {
|
||||
return false
|
||||
}
|
||||
return HARNESS_INJECTED_TURN_PREFIXES.some((prefix) => normalized.startsWith(prefix))
|
||||
}
|
||||
Reference in New Issue
Block a user