From 1760e70acd2e05eb87b85355f9b1b9e71036d53e Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Fri, 11 Sep 2026 01:38:21 -0700 Subject: [PATCH] fix(runtime): give the structured-session refusal its own error code The `agentSession.*` gate refused with a bare `Error` whose message was the magic token, so the RPC envelope flattened it to `runtime_error` and a client could not structurally tell "this host refuses structured chat" from any other runtime fault. The gate now throws the repo's coded-error shape and the code is registered for envelope passthrough; the message is unchanged, so clients that match on the token keep classifying exactly as before. --- src/main/runtime/rpc/errors.ts | 3 +++ .../methods/structured-agent-session-gate.ts | 20 +++++++++++++++---- .../methods/structured-agent-session.test.ts | 14 +++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/main/runtime/rpc/errors.ts b/src/main/runtime/rpc/errors.ts index ef4b0b3024d..9483a72bd15 100644 --- a/src/main/runtime/rpc/errors.ts +++ b/src/main/runtime/rpc/errors.ts @@ -109,6 +109,9 @@ const STRUCTURED_RUNTIME_PASSTHROUGH_CODES: ReadonlySet = new Set([ 'runtime_timeout', 'invalid_runtime_response', 'capability_unsupported', + // The whole `agentSession.*` gate refusal. Coded so a client can route on it without matching + // the message; the message is unchanged so older clients still classify. + 'structured_agent_session_unsupported', 'relay_quota_exceeded', 'dispatch_capability_invalid', 'agent_unconfigured', diff --git a/src/main/runtime/rpc/methods/structured-agent-session-gate.ts b/src/main/runtime/rpc/methods/structured-agent-session-gate.ts index 60b28425057..5b23a648f8c 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-gate.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-gate.ts @@ -8,6 +8,7 @@ // for old mobile clients while structured chat is enabled so they receive a fallback row, and that // path constructs the host. `agentSession.*` stays refused either way, which is what this gate is for. +import { OrchestrationError } from '../../orchestration/orchestration-error' import { getStructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-registry' 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' @@ -17,6 +18,17 @@ import { supportsStructuredAgentSessions } from './structured-agent-session-policy' +/** + * Carries a code so a client can tell "this host refuses structured chat" apart from any other + * runtime fault. The message stays the bare token: older clients classify by matching it. + */ +function structuredSessionsUnsupported(): OrchestrationError { + return new OrchestrationError( + 'structured_agent_session_unsupported', + 'structured_agent_session_unsupported' + ) +} + /** * In-process callers are the same build as the host, so they carry no negotiated * capability list; every remote client must say it can read structured sessions. @@ -27,7 +39,7 @@ export function supportsStructuredSessions(ctx: RpcContext): boolean { export function requireStructuredCapability(ctx: RpcContext): void { if (!supportsStructuredSessions(ctx)) { - throw new Error('structured_agent_session_unsupported') + throw structuredSessionsUnsupported() } } @@ -35,7 +47,7 @@ export function requireStructuredHost(ctx: RpcContext): StructuredAgentSessionHo requireStructuredCapability(ctx) const host = getStructuredAgentSessionHost() if (!host) { - throw new Error('structured_agent_session_unsupported') + throw structuredSessionsUnsupported() } return host } @@ -64,11 +76,11 @@ export function requireStructuredHost(ctx: RpcContext): StructuredAgentSessionHo */ export function requireStructuredCleanupHost(ctx: RpcContext): StructuredAgentSessionHost { if (!supportsStructuredAgentSessionCapability(ctx)) { - throw new Error('structured_agent_session_unsupported') + throw structuredSessionsUnsupported() } const host = getStructuredAgentSessionHost() if (!host) { - throw new Error('structured_agent_session_unsupported') + throw structuredSessionsUnsupported() } return host } diff --git a/src/main/runtime/rpc/methods/structured-agent-session.test.ts b/src/main/runtime/rpc/methods/structured-agent-session.test.ts index c2d46b09818..bec7827f0cb 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session.test.ts @@ -177,6 +177,20 @@ describe('capability gating', () => { expect(hostCalls.send).not.toHaveBeenCalled() }) + it('refuses with a code a client can route on, not just a message it must parse', async () => { + const response = await call('agentSession.send', sendParams(), { + clientKind: 'runtime', + clientCapabilities: ['terminal.stream.v1'] + }) + expect(response).toMatchObject({ + ok: false, + error: { + code: 'structured_agent_session_unsupported', + message: 'structured_agent_session_unsupported' + } + }) + }) + it('rejects create intent before resolving host-owned fields for an old client', async () => { const worktree = 'id:workspace-1' const response = await call(