Files
orca/src/main/runtime/rpc/methods/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

139 lines
5.6 KiB
TypeScript

/**
* Creating a structured session for a worktree: resolve the create intent, attach it under the
* host-computed fingerprint, then publish its tab.
*
* Extracted from `agentSession.create` so orchestration can start a native-born structured worker
* on exactly the same path. `activate` is the only knob the two callers differ on: a chat the user
* asked for takes the surface, a background dispatch must not steal it (the terminal worker path's
* `surfaceOwner: false`).
*
* The prepare/commit split is the pre-commit boundary, not a style choice: nothing before `attach`
* commits a session, so that span answers with a refusal, and nothing after it may be folded back
* in. Both callers run the same two halves, so orchestration gets that guarantee too.
*/
import { computeAgentSessionPayloadFingerprint } from '../../../../shared/agent-session-mutation-envelope'
import type {
AgentSessionAttachResult,
AgentSessionMutationEnvelope,
AgentSessionMutationResult
} from '../../../../shared/agent-session-wire'
import {
attachFingerprintFields,
type AgentSessionAttachParams
} from '../../../native-chat/agent-session-wire/structured-agent-session-attach'
import type { StructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-host'
import type { StructuredAgentSessionCaller } from '../../../native-chat/agent-session-wire/structured-agent-session-host-types'
import type { StructuredAgentSessionResumeSource } from '../../../../shared/structured-agent-session-create'
import type { OrcaRuntimeService } from '../../orca-runtime'
import {
resolveUncommittedStructuredCreate,
type StructuredCreateRefused
} from './structured-agent-session-precommit-refusal'
export type PreparedStructuredAgentSessionCreate = {
host: StructuredAgentSessionHost
attachParams: AgentSessionAttachParams
/** Null when the caller supplied its own location; only a resolved worktree publishes a tab. */
tab: { workspaceId: string; agent: 'claude' | 'codex' } | null
}
/** The pre-commit half. Throws; the caller is expected to run it inside
* `resolveUncommittedStructuredCreate` so a failure reaches the client as a refusal. */
export async function prepareStructuredAgentSessionCreateForWorktree(args: {
runtime: OrcaRuntimeService
/** Installs the host lazily; called at the same point the RPC handler always installed it. */
ensureHost: () => Promise<StructuredAgentSessionHost>
envelope: AgentSessionMutationEnvelope
worktree: string
agent: 'claude' | 'codex'
caller: StructuredAgentSessionCaller
resumeFrom?: StructuredAgentSessionResumeSource
}): Promise<PreparedStructuredAgentSessionCreate> {
// Adoption replay may need the record loaded from disk before source discovery can be skipped.
let host = args.resumeFrom ? await args.ensureHost() : null
const resolved = await args.runtime.resolveStructuredAgentSessionCreateIntent({
envelope: args.envelope,
worktree: args.worktree,
agent: args.agent,
callerKey: args.caller.callerKey,
...(args.resumeFrom ? { resumeFrom: args.resumeFrom } : {})
})
const hostFingerprint = computeAgentSessionPayloadFingerprint({
method: 'agentSession.attach',
sessionId: args.envelope.sessionId,
fields: attachFingerprintFields({ ...resolved, envelope: args.envelope })
})
host ??= await args.ensureHost()
const { agent: _resolvedAgent, provider: _resolvedProvider, ...resolvedAttach } = resolved
return {
host,
attachParams: {
...resolvedAttach,
provider: resolved.provider as 'claude' | 'codex',
agent: resolved.agent as 'claude' | 'codex',
envelope: { ...args.envelope, payloadFingerprint: hostFingerprint }
},
tab: {
workspaceId: resolved.location.workspaceId,
agent: resolved.agent as 'claude' | 'codex'
}
}
}
/** The commit half. Past `attach`, a failure no longer proves the session does not exist. */
export async function commitStructuredAgentSessionCreate(args: {
runtime: OrcaRuntimeService
caller: StructuredAgentSessionCaller
prepared: PreparedStructuredAgentSessionCreate
activate: boolean
}): Promise<AgentSessionMutationResult<AgentSessionAttachResult>> {
const { prepared } = args
const result = await prepared.host.attach(args.caller, prepared.attachParams)
if (!result.ok || !prepared.tab) {
return result
}
try {
await args.runtime.publishStructuredAgentSessionTab({
workspaceId: prepared.tab.workspaceId,
sessionId: result.value.sessionId,
agent: prepared.tab.agent,
activate: args.activate
})
} catch (error) {
console.warn('[agent-session] create committed before tab publication failed', error)
return {
ok: false,
refusal: {
code: 'agent_session_operation_unknown',
message: 'The chat may have been created, but its tab could not be confirmed.'
}
}
}
return result
}
export async function createStructuredAgentSessionForWorktree(args: {
runtime: OrcaRuntimeService
ensureHost: () => Promise<StructuredAgentSessionHost>
caller: StructuredAgentSessionCaller
envelope: AgentSessionMutationEnvelope
worktree: string
agent: 'claude' | 'codex'
activate: boolean
}): Promise<AgentSessionMutationResult<AgentSessionAttachResult>> {
const prepared: PreparedStructuredAgentSessionCreate | StructuredCreateRefused =
await resolveUncommittedStructuredCreate(() =>
prepareStructuredAgentSessionCreateForWorktree(args)
)
if ('refusal' in prepared) {
return { ok: false, refusal: prepared.refusal }
}
return commitStructuredAgentSessionCreate({
runtime: args.runtime,
caller: args.caller,
prepared,
activate: args.activate
})
}