Files
orca/src/main/codex/codex-structured-dispatch-echo.test.ts
T
Brennan Benson 9def4b9ba1 feat(native-chat): publish where each submission was sent and the turn a stopped one was answered into (#25073)
* feat(native-chat): publish each submission's journal positions

A client never learned where in the journal a send was accepted or taken back,
so drawing a stopped send had to guess from its own row and from clocks, and the
guesses misfired after restarts. Each projected submission now carries
`submittedSequence` (its submission row) and `resolvedSequence` (the dispatch row
that resolved it; absent while pending): two optional fields, computed on every
fold from rows already stored, never stored themselves. Older clients ignore
them. The failure-fact schema moves to its own module to keep the journal schema
file within its size limit.

* test(native-chat): pin a submission's resolved position on the provider-echo accept path

Also say which rows can resolve a submission, and that the submission schema, not the shared type, omits acceptedSequence.

* refactor(native-chat): publish only where each submission was sent

A rejected send's own row now moves to its rejection (#24710), so the row that
resolved a send is the item's own position and needs no second copy. Keep
submittedSequence, the one journal-order record of where a send was sent.

* feat(native-chat): publish the turn a withdrawn Codex send was answered into

A send Codex answered into a turn that then ended without taking it is
rejected by that turn's end. The rejection row now names that turn's
record, and the submission carries it as answeredInTurnItemId, so a
client can tell which turn the send belonged to without guessing from
journal order or clocks. Absent on every other send.

* feat(native-chat): say how a withdrawn Codex send joined the turn it names

The answered turn becomes { turnItemId, via }: 'start' when Codex
answered the send's turn/start with that turn, 'steer' when Orca steered
it into that running turn. A client can then tell the turn's opener from
a send steered into it even when the opener is on an older page.

* feat(native-chat): state on every rejection whether it names a turn

A rejection a host writes now always carries answeredInTurn: the turn
Codex answered the send into, or null for none. A reader can then tell
'answered into no turn' from a row written before the field existed,
which keeps no answer and is placed by the older rules.

* refactor(native-chat): give the answered-turn schema its own module

Keeps the journal schema file within its line limit.

* docs(native-chat): say an unreadable answered turn reads as none
2026-10-05 15:56:55 -07:00

182 lines
6.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { AgentJournalItemIdentity } from '../../shared/agent-session-journal-types'
import {
createCodexDispatchEchoes,
readCodexDispatchEcho,
MAX_CODEX_PENDING_DISPATCH_ECHOES
} from './codex-structured-dispatch-echo'
const CODEX_IDENTITY: AgentJournalItemIdentity = {
provider: 'codex',
threadId: 'thread-1',
turnId: 'turn-1',
ordinal: 3
}
describe('codex dispatch echoes', () => {
it('settles by client message id rather than arrival order', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
echoes.arm('client-2')
// Codex coalesces both sends into one turn, and the second can be echoed
// first. Queue position would settle the wrong submission here.
expect(echoes.settle('client-2')).toBe(true)
expect(echoes.settle('client-1')).toBe(true)
expect(echoes.size).toBe(0)
})
it('reads each submission instant by client message id', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('stale-unknown', 100)
echoes.arm('later-turn', 200)
expect(echoes.requestOrigin('later-turn')).toEqual({ requestedAt: 200, sequence: 1 })
expect(echoes.requestOrigin('stale-unknown')).toEqual({ requestedAt: 100, sequence: 0 })
expect(echoes.requestOrigin('never-armed')).toBeNull()
expect(echoes.latestSequence()).toBe(1)
})
it('keeps one causal sequence when an unconfirmed send retries', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1', 100)
echoes.arm('client-1', 200)
expect(echoes.requestOrigin('client-1')).toEqual({ requestedAt: 100, sequence: 0 })
expect(echoes.latestSequence()).toBe(0)
})
it('refuses an echo this session never armed', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
expect(echoes.settle('client-from-history')).toBe(false)
expect(echoes.size).toBe(1)
})
it('settles a send exactly once', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
expect(echoes.settle('client-1')).toBe(true)
expect(echoes.settle('client-1')).toBe(false)
})
it('drops a send whose write never reached the provider', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
echoes.disarm('client-1')
expect(echoes.settle('client-1')).toBe(false)
})
it('clears every armed send', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
echoes.arm('client-2')
echoes.clear()
expect(echoes.size).toBe(0)
expect(echoes.settle('client-1')).toBe(false)
})
it('refuses new correlations at capacity without dropping an older send', () => {
const echoes = createCodexDispatchEchoes()
for (let index = 0; index < MAX_CODEX_PENDING_DISPATCH_ECHOES; index += 1) {
expect(echoes.arm(`client-${index}`)).toBe(true)
}
expect(echoes.arm(`client-${MAX_CODEX_PENDING_DISPATCH_ECHOES}`)).toBe(false)
expect(echoes.size).toBe(MAX_CODEX_PENDING_DISPATCH_ECHOES)
expect(echoes.settle('client-0')).toBe(true)
expect(echoes.settle(`client-${MAX_CODEX_PENDING_DISPATCH_ECHOES}`)).toBe(false)
})
})
describe('readCodexDispatchEcho', () => {
it('reads the client message id off a user message', () => {
expect(
readCodexDispatchEcho(
{ type: 'userMessage', id: 'item-1', clientId: 'client-1' },
CODEX_IDENTITY
)
).toEqual({ clientMessageId: 'client-1', providerIdentity: CODEX_IDENTITY })
})
it('ignores an item that is not a user message', () => {
expect(
readCodexDispatchEcho(
{ type: 'agentMessage', id: 'item-1', clientId: 'client-1' },
CODEX_IDENTITY
)
).toBeNull()
})
it('ignores a user message Codex did not correlate', () => {
expect(readCodexDispatchEcho({ type: 'userMessage', id: 'item-1' }, CODEX_IDENTITY)).toBeNull()
})
it('ignores an item with no durable Codex identity', () => {
expect(
readCodexDispatchEcho(
{ type: 'userMessage', id: 'item-1', clientId: 'client-1' },
{ provider: 'orca', clientMessageId: 'codex-item:thread-1:item-1' }
)
).toBeNull()
})
})
describe('the turn Codex answered a send into but has not opened', () => {
const NONE_OPEN = new Set<string>()
it('is the turn the latest armed send was answered into', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
echoes.bindTurn('client-1', 'thread-1', 'turn-1', 'start')
echoes.arm('client-2')
echoes.bindTurn('client-2', 'thread-1', 'turn-2', 'start')
expect(echoes.answeredUnopenedTurn('thread-1', NONE_OPEN)).toBe('turn-2')
})
it('is none once Codex opened that turn', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
echoes.bindTurn('client-1', 'thread-1', 'turn-1', 'start')
expect(echoes.answeredUnopenedTurn('thread-1', new Set(['turn-1']))).toBeNull()
})
it('is none once that turn ended, even with its send still armed for an echo', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
echoes.bindTurn('client-1', 'thread-1', 'turn-1', 'start')
// A completed end leaves its unechoed send armed.
expect(echoes.endTurn('thread-1', 'turn-1', { status: 'completed' })).toEqual([])
expect(echoes.answeredUnopenedTurn('thread-1', NONE_OPEN)).toBeNull()
})
it('skips a turn a wait left unopened, and still names an earlier one', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
echoes.bindTurn('client-1', 'thread-1', 'turn-1', 'start')
echoes.arm('client-2')
echoes.bindTurn('client-2', 'thread-1', 'turn-2', 'start')
echoes.leftUnopened('thread-1', 'turn-2')
expect(echoes.answeredUnopenedTurn('thread-1', NONE_OPEN)).toBe('turn-1')
})
it('is none for a send not yet answered, or answered on another thread', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
echoes.arm('client-2')
echoes.bindTurn('client-2', 'thread-2', 'turn-2', 'start')
expect(echoes.answeredUnopenedTurn('thread-1', NONE_OPEN)).toBeNull()
})
})