fix(agent-session): refuse a pre-commit structured create with an envelope

The create route refused by throwing, which reaches a client as a generic
transport error indistinguishable from a lost answer — so desktop parked the
launch as visibility-unknown with no chat and no terminal. Convert the whole
pre-commit span, everything before `attach`, into a refusal envelope carrying a
code, and name the definitive-refusal allowlist the fallback decision needs.
This commit is contained in:
Merge Sim
2026-09-04 13:24:40 -07:00
parent b0253673c9
commit 730e4c9ab0
5 changed files with 447 additions and 45 deletions
@@ -0,0 +1,235 @@
// The create route's pre-commit boundary: a failure before `attach` must reach the client as a
// refusal it can classify, and a failure at or after `attach` must not.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { StructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-host'
import { setStructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-registry'
import { computeAgentSessionPayloadFingerprint } from '../../../../shared/agent-session-mutation-envelope'
import { isDefinitiveAgentSessionCreateRefusal } from '../../../../shared/agent-session-definitive-refusal'
import { STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
import type { OrcaRuntimeService } from '../../orca-runtime'
import type { RpcResponse } from '../core'
import { RpcDispatcher } from '../dispatcher'
import { STRUCTURED_AGENT_SESSION_METHODS } from './structured-agent-session'
const SESSION = 'session-alpha'
const OPERATION = '1800000000000-00000000000000000000000000000001'
const WORKTREE = 'id:workspace-1'
const STRUCTURED_CLIENT = {
clientKind: 'runtime' as const,
clientCapabilities: [STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY]
}
function createParams(overrides: Record<string, unknown> = {}) {
return {
envelope: {
sessionId: SESSION,
clientOperationId: OPERATION,
expectedRuntimeFence: null,
payloadFingerprint: computeAgentSessionPayloadFingerprint({
method: 'agentSession.create',
sessionId: SESSION,
fields: { worktree: WORKTREE, agent: 'codex' }
}),
...(overrides.envelope as Record<string, unknown> | undefined)
},
worktree: WORKTREE,
agent: 'codex'
}
}
let attach: ReturnType<typeof vi.fn>
function hostStub(): StructuredAgentSessionHost {
attach = vi.fn(async () => ({
ok: true,
replayed: false,
fence: 1,
cursor: { epoch: 'epoch-a', sequence: 0 },
value: { sessionId: SESSION, fence: 1, page: {}, unconfirmedClientMessageIds: [] }
}))
return { attach } as unknown as StructuredAgentSessionHost
}
const resolvedIntent = {
location: {
executionHostId: 'local',
wslDistro: null,
workspaceId: 'workspace-1',
workspaceKind: 'git-worktree'
},
provider: 'codex',
agent: 'codex',
accountHome: { variable: 'CODEX_HOME', path: '/host/.codex' },
runtimeKind: 'native'
}
async function create(
runtimeOverrides: Record<string, unknown> = {},
params: unknown = createParams()
): Promise<RpcResponse> {
const runtime = {
getRuntimeId: () => 'runtime-1',
registerSubscriptionCleanup: vi.fn(),
cleanupSubscription: vi.fn(),
cleanupSubscriptionsByPrefix: vi.fn(),
ensureStructuredAgentSessionHost: vi.fn(async () => undefined),
resolveStructuredAgentSessionCreateIntent: vi.fn(async (input: { envelope: unknown }) => ({
envelope: input.envelope,
...resolvedIntent
})),
publishStructuredAgentSessionTab: vi.fn(async () => undefined),
...runtimeOverrides
}
const replies: RpcResponse[] = []
await new RpcDispatcher({
runtime: runtime as unknown as OrcaRuntimeService,
methods: STRUCTURED_AGENT_SESSION_METHODS
}).dispatchStreaming(
{ id: 'request-1', authToken: 'token', method: 'agentSession.create', params },
(raw) => replies.push(JSON.parse(raw) as RpcResponse),
STRUCTURED_CLIENT
)
const first = replies[0]
if (!first) {
throw new Error('no reply for agentSession.create')
}
return first
}
/** The refusal a client can act on, or null when the reply was not one. */
function refusalOf(response: RpcResponse): { code: string; message: string } | null {
if (!response.ok) {
return null
}
const result = response.result as { ok: boolean; refusal?: { code: string; message: string } }
return result.ok ? null : (result.refusal ?? null)
}
beforeEach(() => {
setStructuredAgentSessionHost(hostStub())
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
})
afterEach(() => {
setStructuredAgentSessionHost(null)
vi.restoreAllMocks()
})
describe('a create refused before it commits', () => {
it('answers a code-carrying refusal as a definitive envelope', async () => {
const response = await create({
resolveStructuredAgentSessionCreateIntent: vi.fn(async () => {
throw new Error('structured_agent_session_unsupported')
})
})
const refusal = refusalOf(response)
expect(refusal?.code).toBe('structured_agent_session_unsupported')
expect(isDefinitiveAgentSessionCreateRefusal(refusal?.code)).toBe(true)
expect(attach).not.toHaveBeenCalled()
})
it('answers a code-less failure as a definitive envelope too, keeping the cause in the message', async () => {
// The class no per-site conversion catches: an unresolvable worktree throws prose, not a code.
const response = await create({
resolveStructuredAgentSessionCreateIntent: vi.fn(async () => {
throw new Error('No worktree matches id:workspace-1')
})
})
const refusal = refusalOf(response)
expect(isDefinitiveAgentSessionCreateRefusal(refusal?.code)).toBe(true)
expect(refusal?.message).toContain('No worktree matches id:workspace-1')
expect(attach).not.toHaveBeenCalled()
})
it('answers a host that will not install as a definitive envelope', async () => {
setStructuredAgentSessionHost(null)
const response = await create({
ensureStructuredAgentSessionHost: vi.fn(async () => {
throw new Error('EACCES: could not open the session store')
})
})
const refusal = refusalOf(response)
expect(isDefinitiveAgentSessionCreateRefusal(refusal?.code)).toBe(true)
expect(refusal?.message).toContain('could not open the session store')
})
it('answers a missing host as a definitive envelope rather than a thrown code', async () => {
setStructuredAgentSessionHost(null)
const response = await create()
const refusal = refusalOf(response)
expect(refusal?.code).toBe('structured_agent_session_unsupported')
expect(isDefinitiveAgentSessionCreateRefusal(refusal?.code)).toBe(true)
})
it('still refuses a fingerprint conflict with its own code, not a pre-commit one', async () => {
const response = await create(
{},
createParams({ envelope: { payloadFingerprint: 'a'.repeat(64) } })
)
expect(refusalOf(response)?.code).toBe('agent_session_operation_conflict')
expect(attach).not.toHaveBeenCalled()
})
})
describe('the boundary the envelope stops at', () => {
it('leaves a failure at attach unknown, because it may have committed', async () => {
attach.mockRejectedValueOnce(new Error('attach exploded'))
const response = await create()
expect(response).toMatchObject({ ok: false, error: { code: 'runtime_error' } })
expect(refusalOf(response)).toBeNull()
})
it('leaves a committed create whose tab could not be published unknown', async () => {
const response = await create({
publishStructuredAgentSessionTab: vi.fn(async () => {
throw new Error('publish failed')
})
})
const refusal = refusalOf(response)
expect(refusal?.code).toBe('agent_session_operation_unknown')
expect(isDefinitiveAgentSessionCreateRefusal(refusal?.code)).toBe(false)
})
it('keeps hiding the surface from a client that never advertised it', async () => {
const replies: RpcResponse[] = []
await new RpcDispatcher({
runtime: { getRuntimeId: () => 'runtime-1' } as unknown as OrcaRuntimeService,
methods: STRUCTURED_AGENT_SESSION_METHODS
}).dispatchStreaming(
{
id: 'request-1',
authToken: 'token',
method: 'agentSession.create',
params: createParams()
},
(raw) => replies.push(JSON.parse(raw) as RpcResponse),
{ clientKind: 'runtime', clientCapabilities: [] }
)
expect(replies[0]).toMatchObject({
ok: false,
error: { message: expect.stringContaining('structured_agent_session_unsupported') }
})
})
it('keeps a client-declared fence a programming error, not a refusal', async () => {
const response = await create({}, createParams({ envelope: { expectedRuntimeFence: 1 } }))
expect(response).toMatchObject({
ok: false,
error: { code: 'agent_session_operation_invalid' }
})
})
})
@@ -0,0 +1,71 @@
// Nothing before `attach` commits a session, so every failure in that span definitively created
// nothing. Thrown, it reaches a remote client as a generic transport error, indistinguishable from
// an answer that was lost on the way back — and a client that cannot tell those apart either
// strands the user with no chat and no terminal, or spawns a sibling beside a session that may
// already exist. So the whole span answers with a refusal envelope carrying a code, whatever it
// failed on.
//
// Converting the span rather than each throw site is deliberate: alongside the throws that carry a
// code there is a code-less class — an unresolvable worktree, a store that will not open, a host
// that will not install — that no per-site list catches, and it is exactly the class that reaches
// the user as nothing at all.
import {
AGENT_SESSION_WIRE_REFUSAL_CODES,
type AgentSessionWireRefusal,
type AgentSessionWireRefusalCode
} from '../../../../shared/agent-session-wire'
export type StructuredCreateRefused = { refusal: AgentSessionWireRefusal }
/** A pre-commit failure with no code of its own still proves the host could not serve a structured
* session for this request and did not create one, which is what `unsupported` says on the wire.
* A new code would say it more precisely, but only to clients new enough to know it. */
const UNCODED_PRECOMMIT_REFUSAL_CODE: AgentSessionWireRefusalCode =
'structured_agent_session_unsupported'
function wireRefusalCode(error: unknown): AgentSessionWireRefusalCode | null {
const candidates = [
error instanceof Error && 'code' in error ? (error as { code: unknown }).code : undefined,
error instanceof Error ? error.message : String(error)
]
for (const candidate of candidates) {
if (
typeof candidate === 'string' &&
(AGENT_SESSION_WIRE_REFUSAL_CODES as readonly string[]).includes(candidate)
) {
return candidate as AgentSessionWireRefusalCode
}
}
return null
}
function precommitRefusal(error: unknown): AgentSessionWireRefusal {
const code = wireRefusalCode(error)
if (code) {
return { code, message: 'Orca cannot open a structured Codex chat for this workspace.' }
}
const message = error instanceof Error ? error.message : String(error)
// A code-less failure here is often a defect, not a policy answer; the refusal keeps the user
// moving, the log keeps the cause findable.
console.warn('[agent-session] create refused before it committed anything', error)
return {
code: UNCODED_PRECOMMIT_REFUSAL_CODE,
message: `Orca could not prepare a Codex chat for this workspace: ${message}`
}
}
/**
* Runs the pre-commit half of a create. Anything it throws becomes a refusal; a refusal it returns
* itself passes through. Must not wrap `attach` or anything after it — past that point a failure no
* longer proves the session does not exist.
*/
export async function resolveUncommittedStructuredCreate<TPrepared>(
prepare: () => Promise<TPrepared | StructuredCreateRefused>
): Promise<TPrepared | StructuredCreateRefused> {
try {
return await prepare()
} catch (error) {
return { refusal: precommitRefusal(error) }
}
}
@@ -19,6 +19,7 @@ import {
supportsStructuredSessions
} from './structured-agent-session-gate'
import { STRUCTURED_AGENT_SESSION_HOLD_METHODS } from './structured-agent-session-hold'
import { resolveUncommittedStructuredCreate } from './structured-agent-session-precommit-refusal'
import {
AttachParams,
CancelParams,
@@ -62,57 +63,69 @@ export const STRUCTURED_AGENT_SESSION_METHODS: RpcAnyMethod[] = [
if (params.envelope.expectedRuntimeFence !== null) {
throw new Error('agent_session_operation_invalid')
}
if ('worktree' in params) {
const intentFingerprint = computeAgentSessionPayloadFingerprint({
method: 'agentSession.create',
sessionId: params.envelope.sessionId,
fields: { worktree: params.worktree, agent: params.agent }
})
const conflict = agentSessionFingerprintConflict(params.envelope, intentFingerprint)
if (conflict) {
return { ok: false, refusal: conflict }
}
const resolved = await ctx.runtime.resolveStructuredAgentSessionCreateIntent(params)
const hostFingerprint = computeAgentSessionPayloadFingerprint({
method: 'agentSession.attach',
sessionId: params.envelope.sessionId,
fields: {
location: resolved.location,
provider: resolved.provider,
agent: resolved.agent,
accountHome: resolved.accountHome,
runtimeKind: resolved.runtimeKind,
expectedRuntimeFence: null
// Everything up to `attach` is pre-commit, and answers with a refusal rather than a throw so
// a client can tell "nothing was created" from "the outcome is unknown".
const prepared = await resolveUncommittedStructuredCreate(async () => {
if ('worktree' in params) {
const intentFingerprint = computeAgentSessionPayloadFingerprint({
method: 'agentSession.create',
sessionId: params.envelope.sessionId,
fields: { worktree: params.worktree, agent: params.agent }
})
const conflict = agentSessionFingerprintConflict(params.envelope, intentFingerprint)
if (conflict) {
return { refusal: conflict }
}
})
const resolved = await ctx.runtime.resolveStructuredAgentSessionCreateIntent(params)
const hostFingerprint = computeAgentSessionPayloadFingerprint({
method: 'agentSession.attach',
sessionId: params.envelope.sessionId,
fields: {
location: resolved.location,
provider: resolved.provider,
agent: resolved.agent,
accountHome: resolved.accountHome,
runtimeKind: resolved.runtimeKind,
expectedRuntimeFence: null
}
})
await ensureHostInstalled(ctx)
return {
host: requireHost(ctx),
attachParams: {
...resolved,
envelope: { ...params.envelope, payloadFingerprint: hostFingerprint }
},
tab: resolved.agent === 'codex' ? { workspaceId: resolved.location.workspaceId } : null
}
}
await ensureHostInstalled(ctx)
const result = await requireHost(ctx).attach(callerFor(ctx), {
...resolved,
envelope: { ...params.envelope, payloadFingerprint: hostFingerprint }
})
if (result.ok && resolved.agent === 'codex') {
try {
await ctx.runtime.publishStructuredAgentSessionTab({
workspaceId: resolved.location.workspaceId,
sessionId: result.value.sessionId,
agent: 'codex',
activate: true
})
} catch (error) {
console.warn('[agent-session] create committed before tab publication failed', error)
return {
ok: false,
refusal: {
code: 'agent_session_operation_unknown',
message: 'The Codex chat may have been created, but its tab could not be confirmed.'
}
return { host: requireHost(ctx), attachParams: params, tab: null }
})
if ('refusal' in prepared) {
return { ok: false, refusal: prepared.refusal }
}
const result = await prepared.host.attach(callerFor(ctx), prepared.attachParams)
if (result.ok && prepared.tab) {
try {
await ctx.runtime.publishStructuredAgentSessionTab({
workspaceId: prepared.tab.workspaceId,
sessionId: result.value.sessionId,
agent: 'codex',
activate: true
})
} catch (error) {
console.warn('[agent-session] create committed before tab publication failed', error)
return {
ok: false,
refusal: {
code: 'agent_session_operation_unknown',
message: 'The Codex chat may have been created, but its tab could not be confirmed.'
}
}
}
return result
}
await ensureHostInstalled(ctx)
return requireHost(ctx).attach(callerFor(ctx), params)
return result
}
}),
defineMethod({
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest'
import { AGENT_SESSION_WIRE_REFUSAL_CODES } from './agent-session-wire'
import { agentSessionRefusalOperationState } from './agent-session-refusal-retry'
import { isDefinitiveAgentSessionCreateRefusal } from './agent-session-definitive-refusal'
describe('definitive agent-session create refusals', () => {
it('treats an unsupported structured session as definitive', () => {
expect(isDefinitiveAgentSessionCreateRefusal('structured_agent_session_unsupported')).toBe(true)
})
it('never treats an unproven outcome as definitive', () => {
expect(isDefinitiveAgentSessionCreateRefusal('agent_session_operation_unknown')).toBe(false)
expect(isDefinitiveAgentSessionCreateRefusal('agent_session_ownership_unknown')).toBe(false)
})
it('leaves transport failures, timeouts and a missing code unknown', () => {
expect(isDefinitiveAgentSessionCreateRefusal('runtime_error')).toBe(false)
expect(isDefinitiveAgentSessionCreateRefusal('remote_runtime_unavailable')).toBe(false)
expect(isDefinitiveAgentSessionCreateRefusal('timeout')).toBe(false)
expect(isDefinitiveAgentSessionCreateRefusal('runtime_timeout')).toBe(false)
expect(isDefinitiveAgentSessionCreateRefusal(undefined)).toBe(false)
expect(isDefinitiveAgentSessionCreateRefusal(null)).toBe(false)
expect(isDefinitiveAgentSessionCreateRefusal('')).toBe(false)
})
it('counts a method an old host never registered as definitive', () => {
expect(isDefinitiveAgentSessionCreateRefusal('method_not_found')).toBe(true)
})
it('is an allowlist: every other wire refusal code is unknown', () => {
const definitive = AGENT_SESSION_WIRE_REFUSAL_CODES.filter((code) =>
isDefinitiveAgentSessionCreateRefusal(code)
)
expect(definitive).toEqual(['structured_agent_session_unsupported'])
})
it('does not answer the durable-settlement question, which disagrees on the one code that matters', () => {
// Guards the reuse this allowlist exists to avoid: settlement state calls the definitive
// refusal pending-admission, which would rule out the fallback it is meant to allow.
expect(
agentSessionRefusalOperationState(
'agentSession.create',
'structured_agent_session_unsupported'
)
).toBe('pending-admission')
expect(isDefinitiveAgentSessionCreateRefusal('structured_agent_session_unsupported')).toBe(true)
})
})
@@ -0,0 +1,35 @@
/**
* "May a caller create something else instead?" — the fallback question.
*
* Deliberately NOT `agentSessionRefusalOperationState`: that answers "did this operation durably
* settle?", and for that question `structured_agent_session_unsupported` is correctly
* pending-admission. Reused here it would rule out a fallback on the one refusal that most needs
* one. The two questions only look alike.
*
* An allowlist, never a negation: falling back on an outcome the host could not describe is how a
* user ends up with two sessions for one intent. Everything absent — transport failures, timeouts,
* `agent_session_operation_unknown`, `agent_session_ownership_unknown` — is unknown, and unknown
* never falls back.
*/
import type { AgentSessionWireRefusalCode } from './agent-session-wire'
/** Proves the host neither created a session nor will on a retry. */
const DEFINITIVE_REFUSAL_CODES: ReadonlySet<string> = new Set<AgentSessionWireRefusalCode>([
'structured_agent_session_unsupported'
])
/** A dispatcher that never registered the method ran no handler at all, which is as definitive as
* a refusal — and the only transport-level answer that is. */
const DEFINITIVE_RPC_ERROR_CODES: ReadonlySet<string> = new Set(['method_not_found'])
/**
* True only when the code proves nothing was created. Accepts a wire refusal code or an RPC error
* code; the two namespaces are disjoint.
*/
export function isDefinitiveAgentSessionCreateRefusal(code: string | null | undefined): boolean {
if (typeof code !== 'string') {
return false
}
return DEFINITIVE_REFUSAL_CODES.has(code) || DEFINITIVE_RPC_ERROR_CODES.has(code)
}