Files
orca/src/shared/structured-agent-session-create.test.ts
T
Brennan BensonandMerge Sim 1ae7aa8bb4 feat(native-chat): resume an Agent Session History row into a new structured chat (#19176)
* feat(native-chat): resume an Agent Session History row into a new structured chat

A Claude or Codex row in Agent Session History gains "Resume in New Chat": it
opens a new structured native-chat tab that continues that provider
conversation, with the prior turns already in the journal. Until now those rows
could only be resumed into a PTY terminal; the structured branch could reveal a
chat Orca already owned but could not adopt one it had never held.

Almost all of the machinery existed. Both lanes already resume from the record's
provider handle chain, the journal already has a transcript importer, and the
handle chain already models `adopted` as an origin. The gap was that a create
always minted an empty chain, so the adapters started a fresh conversation. This
seeds that chain.

The client names only the conversation. `agentSession.create` is reachable by
paired mobile clients, so the transcript path and the account home are derived
by the executing host and validated against the account homes it recognises —
a client-supplied path would choose which file the host imports and which
credential directory the provider child launches against.

Failure refuses rather than degrades. A transcript that cannot be found refuses
before anything is created; one that fails or decodes empty *after* the provider
has resumed fails the attach, tearing the child down and publishing no tab,
because an empty journal beside a context-carrying agent claims a continuity the
provider never gave.

Codex can resume into any workspace since it is handed the rollout path; Claude
resolves transcripts under a project key derived from the launch cwd, so it is
offered only for the workspace the conversation was recorded in.

* fix(native-chat): widen adopted-home discovery and keep ordinary launches untouched

Three corrections from review of the first commit.

The adoption's account-home candidates now include the extra Codex homes session
discovery already scans. A row this host listed could otherwise refuse to
resume, which reads as the feature being broken rather than as a scope.

Ordinary launches call `createStructuredAgentSessionLaunchIntent` with two
arguments again. Passing the resume source unconditionally appended a trailing
`undefined` that four existing call-site assertions had to absorb; the churn was
the caller's fault, not the tests'.

The transactional adoption guard's comment claimed the self-exemption is what
lets a committed create replay. It is not: replay is settled earlier by the
operation ledger, and an adoption always arrives with a null expected fence, so
a request naming an existing session id is refused a few lines below either way.
The exemption is part of what "another record" means, and the comment now says
that instead.

* fix: preserve history adoption through create and retries

* fix: replay committed history adoption from durable identity

* fix: validate history before claiming adopted sessions

* fix: extract AI vault resume domains

* fix: recognize typed history resume refusals

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 23:24:21 -07:00

83 lines
3.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
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 } } = {}) {
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'
)
})
})