Files
orca/src/shared/structured-agent-session-create.test.ts
T
Brennan Benson f0b3f44f10 feat(agent-session): let the host own a chat's tab id and let a create reserve it (#22616)
* feat(agent-session): let the host own a chat's tab id and let a create reserve it

A structured chat's tab id was derived from its session id by every layer that
needed one: the renderer, the host snapshot and the status address each built
their own spelling. The join between a conversation and the tab that shows it
must be a pointer the host owns, not a derivation each client repeats.

The session record now carries surfaceTabId. A create pins it: the tab half of
the pane agent.launch reserved, an optional tabId on agentSession.create, or a
host-minted UUID. Records written before the field existed are backfilled at
open with the string clients derived, in memory at once and on disk with the
store's first transaction, so nothing keyed by it (read state, notification
ids, worker rows) moves on upgrade. A second record under a held id is refused.

Only the record and the two create wires change here. The snapshot still
publishes agent-session:<sid> and the renderer still derives its local id;
those move in the next two changes. agentSession.create is a strict object, so
the field is advertised as a capability a client checks before sending it.

* fix(agent-session): record the derived tab id for an unreserved create

A create that reserved no tab minted a random UUID that no reader uses: the
renderer, status address, worker rows and host-shared read state all still key
by structured-agent-session-<sid>. Persisted, that id would move every chat
created before readers switch to the recorded one, orphaning its read state
and worker rows the way the backfill exists to prevent. An unreserved create
now records the derived id, the same rule the backfill applies, so the record
always matches the prefix every existing key uses; an opaque mint belongs with
the change that moves the last reader.

Also:
- a chat tab id must be a host tab id on the record, the create wire and in
  admission, matching what agent.launch already requires of paneKey; a
  web-surface id would decode as another tab
- the stored launch-result guard checks the structured outcome's tabId
- comments no longer claim a retry naming another tab conflicts; replay keys
  on the attach fingerprint and answers with the recorded id (now pinned)
- the wire refusal test used a non-hex digest, so the schema refused it for
  that reason; it now reaches the tab id rule
- pin that the reload path refills the id without forcing a save

* test(agent-session): correct the tab-id fingerprint comment to match replay
2026-09-24 14:42:12 -07:00

112 lines
4.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
AGENT_SESSION_CREATE_TAB_ID_RUNTIME_CAPABILITY,
RUNTIME_CAPABILITIES
} from './protocol-version'
import { structuredAgentSessionCreateParams } from './structured-agent-session-create'
import {
structuredAgentSessionCreateFingerprint,
structuredAgentSessionPayloadFingerprint
} from './structured-agent-session-mutation'
const SESSION_ID = 'codex_11111111_2222_3333_4444_555555555555'
const RESUME = { providerSessionId: 'thread-abc' }
/** Distinct per call so two envelopes never share an operation id by accident. */
let uuidCounter = 0
function nextUuid(): string {
uuidCounter += 1
return `00000000-0000-4000-8000-${String(uuidCounter).padStart(12, '0')}`
}
function createParams(
overrides: { resumeFrom?: { providerSessionId: string }; tabId?: string } = {}
) {
return structuredAgentSessionCreateParams({
sessionId: SESSION_ID,
worktree: 'id:repo-1::/repo/orca',
agent: 'codex',
...overrides,
randomUuid: nextUuid,
now: 1_800_000_000_000
})
}
describe('structured agent session create params', () => {
it('carries resumeFrom only when the create adopts a conversation', () => {
expect(createParams()).not.toHaveProperty('resumeFrom')
expect(createParams({ resumeFrom: RESUME })).toMatchObject({ resumeFrom: RESUME })
})
it('declares a fingerprint the host can recompute from the same fields', () => {
const params = createParams({ resumeFrom: RESUME })
expect(params.envelope.payloadFingerprint).toBe(
structuredAgentSessionCreateFingerprint({
sessionId: SESSION_ID,
worktree: 'id:repo-1::/repo/orca',
agent: 'codex',
resumeFrom: RESUME
})
)
})
it('separates an adopting create from a blank one and from another row', () => {
const blank = createParams().envelope.payloadFingerprint
const adopted = createParams({ resumeFrom: RESUME }).envelope.payloadFingerprint
const otherRow = createParams({
resumeFrom: { providerSessionId: 'thread-other' }
}).envelope.payloadFingerprint
expect(adopted).not.toBe(blank)
expect(otherRow).not.toBe(adopted)
})
it('gives a replay of the same adoption the same digest under a new operation id', () => {
const first = createParams({ resumeFrom: RESUME })
const second = createParams({ resumeFrom: RESUME })
expect(second.envelope.clientOperationId).not.toBe(first.envelope.clientOperationId)
expect(second.envelope.payloadFingerprint).toBe(first.envelope.payloadFingerprint)
})
it('leaves a blank create byte-identical to the pre-resume digest', () => {
// Pinned literal: a create with no `resumeFrom` must keep the digest older clients and hosts
// already compute, so adding a field to the create fingerprint fails here rather than in the
// field on a mixed-version pair.
expect(createParams().envelope.payloadFingerprint).toBe(
structuredAgentSessionPayloadFingerprint({
method: 'agentSession.create',
sessionId: SESSION_ID,
fields: { worktree: 'id:repo-1::/repo/orca', agent: 'codex' }
})
)
expect(createParams().envelope.payloadFingerprint).toBe(
'56cb15e22414c0f62fd89d77d00d2d6a0a422f16e95edee154fb8b5bf53fbbc3'
)
})
})
describe('the tab id a create reserves', () => {
it('rides the params and the fingerprint together', () => {
const params = createParams({ tabId: 'tab-1' })
expect(params.tabId).toBe('tab-1')
expect(params.envelope.payloadFingerprint).toBe(
structuredAgentSessionCreateFingerprint({
sessionId: SESSION_ID,
worktree: 'id:repo-1::/repo/orca',
agent: 'codex',
tabId: 'tab-1'
})
)
// The declared digest covers the tab; the host still replays a retry on its attach fingerprint.
expect(params.envelope.payloadFingerprint).not.toBe(createParams().envelope.payloadFingerprint)
})
it('is advertised as a capability, because an older host refuses the strict payload', () => {
expect(RUNTIME_CAPABILITIES).toContain(AGENT_SESSION_CREATE_TAB_ID_RUNTIME_CAPABILITY)
expect(AGENT_SESSION_CREATE_TAB_ID_RUNTIME_CAPABILITY).toBe('agentSession.create.tab-id.v1')
})
})