mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 00:03:08 +00:00
fix(native-chat): avoid hiding user markup
This commit is contained in:
@@ -22,17 +22,19 @@ export function decodeClaudeTranscriptLine(
|
||||
return null
|
||||
}
|
||||
const message = asRecord(record.message)
|
||||
const blocks = claudeContentBlocks(message?.content)
|
||||
if (blocks.length === 0) {
|
||||
const decodedBlocks = claudeContentBlocks(message?.content)
|
||||
if (decodedBlocks.length === 0) {
|
||||
return null
|
||||
}
|
||||
// Why: Claude structurally marks injected turns, but tool-result records are
|
||||
// genuine output and must remain visible even when the containing turn is meta.
|
||||
if (
|
||||
const isInjectedUserTurn =
|
||||
role === 'user' &&
|
||||
(record.isMeta === true || record.isSynthetic === true || record.isCompactSummary === true) &&
|
||||
!blocks.some((block) => block.type === 'tool-result')
|
||||
) {
|
||||
(record.isMeta === true || record.isSynthetic === true || record.isCompactSummary === true)
|
||||
const blocks = isInjectedUserTurn
|
||||
? decodedBlocks.filter((block) => block.type === 'tool-result')
|
||||
: decodedBlocks
|
||||
if (blocks.length === 0) {
|
||||
return null
|
||||
}
|
||||
const messageId = extractString(record.uuid) ?? extractString(message?.id)
|
||||
|
||||
@@ -124,14 +124,35 @@ describe('readNativeChatTranscript (claude)', () => {
|
||||
role: 'user',
|
||||
content: [{ type: 'tool_result', content: 'ok', is_error: false }]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'user',
|
||||
uuid: 'u-meta-mixed',
|
||||
isMeta: true,
|
||||
timestamp: '2026-06-01T10:00:04.000Z',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: '<system-reminder>hidden machinery' },
|
||||
{ type: 'tool_result', content: 'mixed result', is_error: false }
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
||||
const result = await readNativeChatTranscript('claude', 'sess', { filePath })
|
||||
if (!('messages' in result)) {
|
||||
throw new Error('expected messages')
|
||||
}
|
||||
expect(result.messages.map((m) => m.id)).toEqual(['u-real', 'u-meta-toolresult'])
|
||||
expect(result.messages.map((m) => m.id)).toEqual([
|
||||
'u-real',
|
||||
'u-meta-toolresult',
|
||||
'u-meta-mixed'
|
||||
])
|
||||
expect(result.messages[1].role).toBe('tool')
|
||||
expect(result.messages[2]).toMatchObject({
|
||||
role: 'tool',
|
||||
blocks: [{ type: 'tool-result', output: 'mixed result' }]
|
||||
})
|
||||
})
|
||||
|
||||
it('marks thinking-only assistant content as a reasoning surface', async () => {
|
||||
|
||||
@@ -33,6 +33,7 @@ describe('isNoiseMessage', () => {
|
||||
|
||||
it('keeps real user messages', () => {
|
||||
expect(isNoiseMessage(msg('user', 'make it work for codex'))).toBe(false)
|
||||
expect(isNoiseMessage(msg('user', '<my-custom-element>pasted code'))).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps assistant and tool turns', () => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { isTextBlock, type NativeChatMessage } from '../../../../shared/native-chat-types'
|
||||
import { isHarnessInjectedUserTurnText } from '../../../../shared/harness-injected-user-turns'
|
||||
import { isKnownHarnessInjectedUserTurnText } from '../../../../shared/harness-injected-user-turns'
|
||||
|
||||
// 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 classifier lives in
|
||||
// src/shared/harness-injected-user-turns.ts, shared with the agent-status
|
||||
// prompt pipeline; structurally marked turns (isMeta etc.) are already
|
||||
// dropped at decode in src/main/native-chat/transcript-line-decoders.ts.
|
||||
// dropped by the Claude transcript decoder.
|
||||
|
||||
function messageText(message: NativeChatMessage): string {
|
||||
return message.blocks
|
||||
@@ -27,7 +27,7 @@ export function isNoiseMessage(message: NativeChatMessage): boolean {
|
||||
if (message.blocks.some((b) => b.type === 'tool-call' || b.type === 'tool-result')) {
|
||||
return false
|
||||
}
|
||||
return isHarnessInjectedUserTurnText(messageText(message))
|
||||
return isKnownHarnessInjectedUserTurnText(messageText(message))
|
||||
}
|
||||
|
||||
/** Drop harness-noise messages from a transcript. */
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { isHarnessInjectedUserTurnText } from './harness-injected-user-turns'
|
||||
import {
|
||||
isHarnessInjectedUserTurnText,
|
||||
isKnownHarnessInjectedUserTurnText
|
||||
} from './harness-injected-user-turns'
|
||||
|
||||
describe('isHarnessInjectedUserTurnText', () => {
|
||||
it('matches every known harness tag, including attribute-carrying forms', () => {
|
||||
@@ -30,6 +33,7 @@ describe('isHarnessInjectedUserTurnText', () => {
|
||||
]
|
||||
for (const text of injected) {
|
||||
expect(isHarnessInjectedUserTurnText(text), text).toBe(true)
|
||||
expect(isKnownHarnessInjectedUserTurnText(text), text).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -82,4 +86,11 @@ describe('isHarnessInjectedUserTurnText', () => {
|
||||
expect(isHarnessInjectedUserTurnText('<https://example.com/a-b> what is this?')).toBe(false)
|
||||
expect(isHarnessInjectedUserTurnText('<foo-bar@example.com> sent me this')).toBe(false)
|
||||
})
|
||||
|
||||
it('limits destructive filtering to observed harness tags', () => {
|
||||
expect(isKnownHarnessInjectedUserTurnText('<task-notification>done')).toBe(true)
|
||||
expect(isKnownHarnessInjectedUserTurnText('<agent-message from="worker">done')).toBe(true)
|
||||
expect(isKnownHarnessInjectedUserTurnText('<my-custom-element>pasted code')).toBe(false)
|
||||
expect(isKnownHarnessInjectedUserTurnText('<brand-new-harness-tag>unknown')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,9 +10,35 @@
|
||||
// so we match that shape instead of chasing individual tag names — the tag
|
||||
// set grows faster than a literal list stays current. Underscore tags
|
||||
// (<user_query>) are deliberately NOT matched: Grok wraps real typed prompts
|
||||
// in them. A prompt that begins with pasted kebab-tag markup (e.g. a
|
||||
// web-component snippet) is misclassified, costing only a stale status label.
|
||||
// in them. This broad shape is limited to classification that does not remove
|
||||
// transcript content; transcript filtering uses the observed-tag list below.
|
||||
const HARNESS_KEBAB_TAG_PREFIX = /^<[a-z][a-z0-9]*(?:-[a-z0-9]+)+(?:[\s>]|$)/
|
||||
const LEADING_TAG_NAME = /^<([a-z][a-z0-9-]*)(?:[\s>]|$)/
|
||||
|
||||
// Destructive consumers such as transcript filtering must only hide tags we
|
||||
// have observed from harnesses; arbitrary kebab tags can be genuine user code.
|
||||
const KNOWN_HARNESS_TAG_NAMES = new Set([
|
||||
'agent-message',
|
||||
'bash-input',
|
||||
'bash-stderr',
|
||||
'bash-stdout',
|
||||
'channel',
|
||||
'command-args',
|
||||
'command-message',
|
||||
'command-name',
|
||||
'cross-session-message',
|
||||
'fork-boilerplate',
|
||||
'local-command-caveat',
|
||||
'local-command-stderr',
|
||||
'local-command-stdout',
|
||||
'mcp-polling-update',
|
||||
'mcp-resource-update',
|
||||
'system-reminder',
|
||||
'task-notification',
|
||||
'teammate-message',
|
||||
'user-memory-input',
|
||||
'user-prompt-submit-hook'
|
||||
])
|
||||
|
||||
// Injected shapes the kebab-tag rule cannot see: single-word attribute tags
|
||||
// and the prose sentences the harness wraps deliveries and notices in.
|
||||
@@ -38,3 +64,17 @@ export function isHarnessInjectedUserTurnText(text: string): boolean {
|
||||
}
|
||||
return HARNESS_INJECTED_TURN_PREFIXES.some((prefix) => normalized.startsWith(prefix))
|
||||
}
|
||||
|
||||
/** True only for observed harness shapes. Use this when a match removes user
|
||||
* content rather than merely suppressing ephemeral prompt-derived UI. */
|
||||
export function isKnownHarnessInjectedUserTurnText(text: string): boolean {
|
||||
const normalized = text.trim().toLowerCase()
|
||||
if (normalized.length === 0) {
|
||||
return false
|
||||
}
|
||||
const tagName = LEADING_TAG_NAME.exec(normalized)?.[1]
|
||||
if (tagName && KNOWN_HARNESS_TAG_NAMES.has(tagName)) {
|
||||
return true
|
||||
}
|
||||
return HARNESS_INJECTED_TURN_PREFIXES.some((prefix) => normalized.startsWith(prefix))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user