Files
orca/src/shared/structured-agent-session-create.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

68 lines
2.4 KiB
TypeScript

import type { AgentSessionHandleProvider } from './agent-session-provider-handle'
import type { AgentSessionMutationEnvelope } from './agent-session-wire'
import {
createStructuredAgentSessionOperationId,
structuredAgentSessionCreateFingerprint
} from './structured-agent-session-mutation'
/**
* The conversation a create adopts instead of starting a fresh one.
*
* Deliberately carries an identity and nothing else. The transcript file and the account home it
* lives under are derived by the executing host, never sent: `agentSession.create` is reachable by
* paired mobile clients, and a client-supplied path would let one choose which file the host reads
* into a journal and which credential directory the provider child launches against.
*/
export type StructuredAgentSessionResumeSource = {
/** claude: the session id. codex: the thread id. */
providerSessionId: string
}
export type StructuredAgentSessionCreateParams = {
envelope: AgentSessionMutationEnvelope
worktree: string
agent: AgentSessionHandleProvider
resumeFrom?: StructuredAgentSessionResumeSource
}
/** Provider-prefixed so a session id names its lane on sight, and underscore-only
* so the id stays a single token everywhere it is embedded (tab ids, log keys). */
export function createStructuredAgentSessionId(
agent: AgentSessionHandleProvider,
randomUuid: () => string
): string {
return `${agent}_${randomUuid().replaceAll('-', '_')}`
}
/**
* The durable `agentSession.create` envelope every client replays on an ambiguous
* transport failure. The fingerprint must be computed over the same fields the host
* recomputes, so both clients build it here rather than each assembling their own.
*/
export function structuredAgentSessionCreateParams(args: {
sessionId: string
worktree: string
agent: AgentSessionHandleProvider
resumeFrom?: StructuredAgentSessionResumeSource
randomUuid: () => string
now?: number
}): StructuredAgentSessionCreateParams {
const fields = {
worktree: args.worktree,
agent: args.agent,
...(args.resumeFrom ? { resumeFrom: args.resumeFrom } : {})
}
return {
envelope: {
sessionId: args.sessionId,
clientOperationId: createStructuredAgentSessionOperationId(args.randomUuid, args.now),
expectedRuntimeFence: null,
payloadFingerprint: structuredAgentSessionCreateFingerprint({
sessionId: args.sessionId,
...fields
})
},
...fields
}
}