mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 00:02:19 +00:00
* 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
80 lines
3.0 KiB
TypeScript
80 lines
3.0 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
|
|
/** Sent only to a host advertising `AGENT_SESSION_CREATE_TAB_ID_RUNTIME_CAPABILITY`. */
|
|
tabId?: string
|
|
}
|
|
|
|
/** 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('-', '_')}`
|
|
}
|
|
|
|
/** 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: AgentSessionHandleProvider
|
|
resumeFrom?: StructuredAgentSessionResumeSource
|
|
tabId?: string
|
|
randomUuid: () => string
|
|
now?: number
|
|
}): StructuredAgentSessionCreateParams {
|
|
const fields = {
|
|
worktree: args.worktree,
|
|
agent: args.agent,
|
|
...(args.resumeFrom ? { resumeFrom: args.resumeFrom } : {}),
|
|
...(args.tabId ? { tabId: args.tabId } : {})
|
|
}
|
|
return {
|
|
envelope: {
|
|
sessionId: args.sessionId,
|
|
clientOperationId: createStructuredAgentSessionOperationId(args.randomUuid, args.now),
|
|
expectedRuntimeFence: null,
|
|
payloadFingerprint: structuredAgentSessionCreateFingerprint({
|
|
sessionId: args.sessionId,
|
|
...fields
|
|
})
|
|
},
|
|
...fields
|
|
}
|
|
}
|