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.
This commit is contained in:
Merge Sim
2026-09-11 01:38:21 -07:00
parent 22d12388a5
commit 1760e70acd
3 changed files with 33 additions and 4 deletions
+3
View File
@@ -109,6 +109,9 @@ const STRUCTURED_RUNTIME_PASSTHROUGH_CODES: ReadonlySet<string> = 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',
@@ -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
}
@@ -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(