Files
orca/src/shared/structured-agent-session-create.ts
T
Brennan Benson 002320c17d fix(native-chat): verify the actual CLI conversation during resume (#29124)
* fix(native-chat): identify the owner when resume create is refused

* test(native-chat): consolidate transcript refusal cases

* test(native-chat): keep ownership coverage at the launch boundary

* test(native-chat): update adoption fixture for early ownership admission

* test(native-chat): align resume admission assertions with host ownership

* test(native-chat): keep RPC coverage focused on ownership proof

* test(native-chat): assert blank request presence before optional source

* fix(native-chat): admit the actual CLI source during chat creation

* fix(native-chat): satisfy source admission lint checks

* test(native-chat): provide the waiting hook prompt

* test(native-chat): isolate source admission fixtures from host homes

* test(native-chat): canonicalize adoption fixture directories

* test(native-chat): preserve early ownership admission in source stack

* fix(native-chat): preserve resume refusal behavior and honest transcript feedback

* fix(native-chat): retain import read evidence in typed result

* fix(native-chat): preserve admitted source and prepared execution home

* fix(chat): retain source evidence for dispatched adoption

* fix(chat): keep source capture within module limits

* test(chat): allow host evidence in create routing

* fix(native-chat): admit an unsaved transcript under a linked account folder

A missing transcript stayed unresolved while its account root resolved through
symlinks, so containment failed and a not-yet-saved conversation showed a load
error instead of the unsaved notice.

* refactor(native-chat): move the newest-item import window into its own module

Keeps journal-legacy-import.ts under the line limit after the source
identity observer landed on top of the streaming import.

* fix(native-chat): restore the refusal error import lost in the main merge

* fix(native-chat): admit Pi transcripts against the root Session History walks

A PI_CODING_AGENT_DIR that already ends in sessions, or any custom folder,
made admission expect <dir>/sessions while the lookup searched <dir>, so a
found conversation was refused.

* refactor(native-chat): admit and look up CLI transcripts in one store per account

Each agent's import described where an account keeps its conversations twice,
once for the path check and once for the lookup, and the two could disagree
(the linked-folder and Pi custom-root refusals). The import now names one
conversation store per account; the create path resolves it once and both the
hook-path check and the fallback lookup judge that same store. OpenCode's
store is its exact database.
2026-10-16 21:14:03 -07:00

96 lines
3.7 KiB
TypeScript

import type { StructuredAgentId } from './agent-session-provider-handle'
import type { AgentSessionMutationEnvelope } from './agent-session-wire'
import type { z } from 'zod'
import type { SendBody } from './rpc-contract/structured-agent-session-message-params'
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
/** Host-owned pane evidence; send only with the CLI-source create capability. */
cliSource?: { workspaceId: string; paneKey: string }
}
export type StructuredAgentSessionCreateParams = {
envelope: AgentSessionMutationEnvelope
worktree: string
agent: StructuredAgentId
resumeFrom?: StructuredAgentSessionResumeSource
/** Sent only to a host advertising `AGENT_SESSION_CREATE_TAB_ID_RUNTIME_CAPABILITY`. */
tabId?: string
/** Sent only to a host advertising `AGENT_SESSION_CREATE_MESSAGE_RUNTIME_CAPABILITY`. */
firstMessage?: StructuredAgentSessionFirstMessage
options?: Readonly<Record<string, string>>
}
export type StructuredAgentSessionFirstMessage = {
clientMessageId: string
body: z.infer<typeof SendBody>
}
/** 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: StructuredAgentId,
randomUuid: () => string
): string {
return `${agent}_${randomUuid().replaceAll('-', '_')}`
}
/** Whether a caller-minted id keeps the shape `createStructuredAgentSessionId` gives every id:
* named for its agent, then one token. The token alone is checked, so a hyphenated agent name
* is not refused at the wire. */
export function isStructuredAgentSessionIdFor(agent: string, sessionId: string): boolean {
const prefix = `${agent}_`
return sessionId.startsWith(prefix) && /^[A-Za-z0-9_]+$/.test(sessionId.slice(prefix.length))
}
/**
* 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: StructuredAgentId
resumeFrom?: StructuredAgentSessionResumeSource
tabId?: string
firstMessage?: StructuredAgentSessionFirstMessage
options?: Readonly<Record<string, string>>
randomUuid: () => string
now?: number
}): StructuredAgentSessionCreateParams {
const fields = {
worktree: args.worktree,
agent: args.agent,
...(args.resumeFrom ? { resumeFrom: args.resumeFrom } : {}),
...(args.tabId ? { tabId: args.tabId } : {}),
...(args.firstMessage ? { firstMessage: args.firstMessage } : {}),
...(args.options ? { options: args.options } : {})
}
return {
envelope: {
sessionId: args.sessionId,
clientOperationId: createStructuredAgentSessionOperationId(args.randomUuid, args.now),
expectedRuntimeFence: null,
payloadFingerprint: structuredAgentSessionCreateFingerprint({
sessionId: args.sessionId,
...fields
})
},
...fields
}
}