mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
fix structured provider error visibility
This commit is contained in:
@@ -469,6 +469,39 @@ describe('codex journal translation', () => {
|
||||
expect(timeline).toEqual([])
|
||||
})
|
||||
|
||||
it('renders a system error carried by a suppressed status kind', () => {
|
||||
const { translator, tap } = translatorWith()
|
||||
|
||||
translator.handle(
|
||||
notification('thread/status/changed', {
|
||||
threadId: THREAD_ID,
|
||||
status: { type: 'systemError' }
|
||||
})
|
||||
)
|
||||
|
||||
const timeline = projectStructuredItemsToNativeChat(
|
||||
tap.rows.map((row, index) => ({
|
||||
itemId: row.key,
|
||||
revision: 1,
|
||||
sequence: index + 1,
|
||||
observedAt: index + 1,
|
||||
body: row.body
|
||||
}))
|
||||
)
|
||||
expect(timeline).toEqual([
|
||||
expect.objectContaining({
|
||||
role: 'system',
|
||||
blocks: [
|
||||
expect.objectContaining({
|
||||
providerFrame: expect.objectContaining({
|
||||
kind: 'notification:thread/status/changed'
|
||||
})
|
||||
})
|
||||
]
|
||||
})
|
||||
])
|
||||
})
|
||||
|
||||
it('writes nothing more after dispose', () => {
|
||||
const { translator, tap, window } = translatorWith()
|
||||
|
||||
|
||||
@@ -1,5 +1,50 @@
|
||||
export type ProviderFrameDisposition = 'debug-only' | 'timeline'
|
||||
|
||||
const ERROR_VARIANT_KEYS = new Set(['type', 'status', 'state', 'subtype', 'outcome'])
|
||||
const ERROR_VALUE_KEYS = new Set(['error', 'failureReason', 'failure_reason'])
|
||||
|
||||
function isErrorVariant(value: unknown): boolean {
|
||||
if (typeof value !== 'string') {
|
||||
return false
|
||||
}
|
||||
const normalized = value.replace(/[_\s-]/g, '').toLowerCase()
|
||||
return (
|
||||
normalized.startsWith('error') || normalized.startsWith('fail') || normalized === 'systemerror'
|
||||
)
|
||||
}
|
||||
|
||||
function hasProviderError(payload: unknown): boolean {
|
||||
const pending = [payload]
|
||||
const seen = new WeakSet<object>()
|
||||
while (pending.length > 0) {
|
||||
const value = pending.pop()
|
||||
if (typeof value !== 'object' || value === null || seen.has(value)) {
|
||||
continue
|
||||
}
|
||||
seen.add(value)
|
||||
if (Array.isArray(value)) {
|
||||
pending.push(...value)
|
||||
continue
|
||||
}
|
||||
for (const [key, nested] of Object.entries(value)) {
|
||||
if ((key === 'isError' || key === 'is_error') && nested === true) {
|
||||
return true
|
||||
}
|
||||
if (key === 'success' && nested === false) {
|
||||
return true
|
||||
}
|
||||
if (ERROR_VARIANT_KEYS.has(key) && isErrorVariant(nested)) {
|
||||
return true
|
||||
}
|
||||
if (ERROR_VALUE_KEYS.has(key) && nested !== null && nested !== false && nested !== '') {
|
||||
return true
|
||||
}
|
||||
pending.push(nested)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function notificationKind(kind: string): string {
|
||||
return kind.startsWith('notification:') ? kind.slice('notification:'.length) : kind
|
||||
}
|
||||
@@ -27,11 +72,25 @@ function isClaudeStatusFrame(kind: string): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
export function classifyProviderFrame(provider: string, kind: string): ProviderFrameDisposition {
|
||||
export function classifyProviderFrame(
|
||||
provider: string,
|
||||
kind: string,
|
||||
payload: unknown
|
||||
): ProviderFrameDisposition {
|
||||
if (hasProviderError(payload)) {
|
||||
return 'timeline'
|
||||
}
|
||||
if (provider === 'codex' && isCodexStatusFrame(kind)) {
|
||||
return 'debug-only'
|
||||
}
|
||||
if (provider === 'claude' && isClaudeStatusFrame(kind)) {
|
||||
if (kind === 'message:result') {
|
||||
const record =
|
||||
typeof payload === 'object' && payload !== null
|
||||
? (payload as Record<string, unknown>)
|
||||
: null
|
||||
return record?.subtype === 'success' ? 'debug-only' : 'timeline'
|
||||
}
|
||||
return 'debug-only'
|
||||
}
|
||||
return 'timeline'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { projectStructuredItemToNativeChat } from '../../../shared/structured-agent-session-projection'
|
||||
import { unhandledProviderFrameJournalItem } from './unhandled-provider-frame'
|
||||
|
||||
describe('unhandled provider frame journal fallback', () => {
|
||||
@@ -63,7 +64,72 @@ describe('unhandled provider frame journal fallback', () => {
|
||||
unhandledProviderFrameJournalItem('codex', 'notification:thread/goal/cleared', {})
|
||||
).toBeNull()
|
||||
expect(unhandledProviderFrameJournalItem('claude', 'message:system:init', {})).toBeNull()
|
||||
expect(unhandledProviderFrameJournalItem('claude', 'message:result', {})).toBeNull()
|
||||
expect(
|
||||
unhandledProviderFrameJournalItem('claude', 'message:result', {
|
||||
subtype: 'success',
|
||||
is_error: false
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('renders codex systemError and Claude error result variants', () => {
|
||||
const codex = unhandledProviderFrameJournalItem('codex', 'notification:thread/status/changed', {
|
||||
threadId: 'thread-1',
|
||||
status: { type: 'systemError' }
|
||||
})
|
||||
const claude = unhandledProviderFrameJournalItem('claude', 'message:result', {
|
||||
subtype: 'error_during_execution',
|
||||
is_error: true,
|
||||
result: 'Provider request failed'
|
||||
})
|
||||
|
||||
expect(codex?.body.providerFrame).toMatchObject({
|
||||
provider: 'codex',
|
||||
kind: 'notification:thread/status/changed'
|
||||
})
|
||||
expect(claude?.body.providerFrame).toMatchObject({
|
||||
provider: 'claude',
|
||||
kind: 'message:result'
|
||||
})
|
||||
expect(
|
||||
claude
|
||||
? projectStructuredItemToNativeChat({
|
||||
itemId: 'claude-error',
|
||||
revision: 1,
|
||||
sequence: 1,
|
||||
observedAt: 1,
|
||||
body: claude.body
|
||||
})
|
||||
: null
|
||||
).toMatchObject({
|
||||
role: 'system',
|
||||
blocks: [
|
||||
expect.objectContaining({
|
||||
providerFrame: expect.objectContaining({ kind: 'message:result' })
|
||||
})
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps failed startup variants visible while suppressing startup progress', () => {
|
||||
const kind = 'notification:mcpServer/startupStatus/updated'
|
||||
|
||||
expect(
|
||||
unhandledProviderFrameJournalItem('codex', kind, {
|
||||
name: 'filesystem',
|
||||
status: 'starting',
|
||||
error: null,
|
||||
failureReason: null
|
||||
})
|
||||
).toBeNull()
|
||||
expect(
|
||||
unhandledProviderFrameJournalItem('codex', kind, {
|
||||
name: 'filesystem',
|
||||
status: 'failed',
|
||||
error: 'server exited',
|
||||
failureReason: null
|
||||
})
|
||||
).not.toBeNull()
|
||||
})
|
||||
|
||||
it('keeps unknown substantive frames visible for both providers', () => {
|
||||
|
||||
@@ -27,7 +27,7 @@ export function unhandledProviderFrameJournalItem(
|
||||
payload: unknown,
|
||||
limits: JournalPayloadLimits = DEFAULT_JOURNAL_PAYLOAD_LIMITS
|
||||
): UnhandledProviderFrameJournalItem | null {
|
||||
if (classifyProviderFrame(provider, kind) === 'debug-only') {
|
||||
if (classifyProviderFrame(provider, kind, payload) === 'debug-only') {
|
||||
return null
|
||||
}
|
||||
const serialized = serializeProviderPayload(payload)
|
||||
|
||||
Reference in New Issue
Block a user