diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts index 91e9ac91fa1..9d50adaaa1a 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts @@ -193,3 +193,25 @@ export async function settleStructuredAgentSessionLateDispatch( }) context.publish(input.sessionId, session.journal) } + +/** The host's thin mutation surface. Each call re-reads the context, so a session + * map or fence that moves between calls is never captured by a stale closure. */ +export function structuredAgentSessionMutationDelegates( + context: () => StructuredAgentSessionMutationContext +) { + return { + cancel: ( + caller: StructuredAgentSessionCaller, + params: Parameters[2] + ) => cancelStructuredAgentSessionTurn(context(), caller, params), + respondToPrompt: ( + caller: StructuredAgentSessionCaller, + params: Parameters[2] + ) => respondToStructuredAgentSessionPrompt(context(), caller, params), + setOption: ( + caller: StructuredAgentSessionCaller, + params: Parameters[2] + ) => setStructuredAgentSessionOption(context(), caller, params), + readOptions: (sessionId: string) => readStructuredAgentSessionOptions(context(), sessionId) + } +} diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts index 7ac76de700a..097cc5ecce7 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts @@ -36,10 +36,7 @@ import type { import type { StructuredAgentSessionAttachContext } from './structured-agent-session-attach-context' import { listStructuredAgentSessionTabs } from './structured-agent-session-host-tabs' import { - cancelStructuredAgentSessionTurn, - readStructuredAgentSessionOptions, - respondToStructuredAgentSessionPrompt, - setStructuredAgentSessionOption, + structuredAgentSessionMutationDelegates, settleStructuredAgentSessionLateDispatch, type StructuredAgentSessionMutationContext } from './structured-agent-session-host-mutations' @@ -283,23 +280,11 @@ export class StructuredAgentSessionHost { send = (...args: Parameters) => this.conversationCommands.send(...args) - cancel = ( - caller: StructuredAgentSessionCaller, - params: Parameters[2] - ): ReturnType => - cancelStructuredAgentSessionTurn(this.mutationContext(), caller, params) - - respondToPrompt = ( - caller: StructuredAgentSessionCaller, - params: Parameters[2] - ): ReturnType => - respondToStructuredAgentSessionPrompt(this.mutationContext(), caller, params) - - setOption = ( - caller: StructuredAgentSessionCaller, - params: Parameters[2] - ): ReturnType => - setStructuredAgentSessionOption(this.mutationContext(), caller, params) + private mutations = structuredAgentSessionMutationDelegates(() => this.mutationContext()) + cancel = this.mutations.cancel + respondToPrompt = this.mutations.respondToPrompt + setOption = this.mutations.setOption + readOptions = this.mutations.readOptions requestHandoff = ( caller: StructuredAgentSessionCaller, @@ -307,9 +292,6 @@ export class StructuredAgentSessionHost { ): Promise> => this.handoffs.request(caller.callerKey, params) - readOptions = (sessionId: string): Promise => - readStructuredAgentSessionOptions(this.mutationContext(), sessionId) - rewind = (caller: StructuredAgentSessionCaller, params: AgentSessionRewindParams) => rewindStructuredAgentSession(this.mutationContext(), this.attachContext(), caller, params) diff --git a/src/shared/agent-session-wire-refusals.ts b/src/shared/agent-session-wire-refusals.ts new file mode 100644 index 00000000000..040c3e63d5c --- /dev/null +++ b/src/shared/agent-session-wire-refusals.ts @@ -0,0 +1,46 @@ +// How the host declines an `agentSession.*` mutation: the closed code list, its +// narrowing guard, and the refusal body a client reads. + +import type { AgentJournalResolution } from './agent-session-journal-types' +import type { AgentSessionRewindReason } from './agent-session-rewind' + +export const AGENT_SESSION_WIRE_REFUSAL_CODES = [ + 'structured_agent_session_unsupported', + 'agent_session_checkpoint_stale', + 'agent_session_conflict', + 'agent_session_ownership_unknown', + 'agent_session_operation_conflict', + 'agent_session_operation_expired', + 'agent_session_operation_capacity', + 'agent_session_operation_invalid', + 'agent_session_operation_unknown', + 'agent_session_item_revision_stale', + 'agent_session_already_resolved', + 'agent_session_identity_required', + 'agent_session_journal_unreadable', + 'execution_owner_reconciling' +] as const +export type AgentSessionWireRefusalCode = (typeof AGENT_SESSION_WIRE_REFUSAL_CODES)[number] + +/** For a host path that raises its refusal as the thrown code. Narrowing through this keeps an + * unrelated fault from being reported to the client as a tidy, wrong refusal. */ +export function isAgentSessionWireRefusalCode( + value: unknown +): value is AgentSessionWireRefusalCode { + return ( + typeof value === 'string' && + (AGENT_SESSION_WIRE_REFUSAL_CODES as readonly string[]).includes(value) + ) +} + +export type AgentSessionWireRefusal = { + rewindReason?: AgentSessionRewindReason + code: AgentSessionWireRefusalCode + message: string + /** On a stale fence, so the client can retry without another round trip. */ + currentFence?: number + /** On a lost compare-and-set: the winning answer and who gave it. */ + resolution?: AgentJournalResolution + /** On a lost compare-and-set: the revision the host actually holds. */ + currentRevision?: number +} diff --git a/src/shared/agent-session-wire.ts b/src/shared/agent-session-wire.ts index cb623dc01b0..1c85b6a4445 100644 --- a/src/shared/agent-session-wire.ts +++ b/src/shared/agent-session-wire.ts @@ -1,4 +1,7 @@ import type { AgentSessionRewindReason, AgentSessionRewindSupport } from './agent-session-rewind' +import type { AgentSessionWireRefusal } from './agent-session-wire-refusals' + +export * from './agent-session-wire-refusals' import type { AgentSessionConversationCommand } from './agent-session-conversation-command' // ─── Structured agent-session wire contract ───────────────────────────────── // The shapes `agentSession.*` accepts and publishes. Phase 2 builds provider @@ -302,47 +305,6 @@ export type AgentSessionMutationEnvelope = { payloadFingerprint: string } -export const AGENT_SESSION_WIRE_REFUSAL_CODES = [ - 'structured_agent_session_unsupported', - 'agent_session_checkpoint_stale', - 'agent_session_conflict', - 'agent_session_ownership_unknown', - 'agent_session_operation_conflict', - 'agent_session_operation_expired', - 'agent_session_operation_capacity', - 'agent_session_operation_invalid', - 'agent_session_operation_unknown', - 'agent_session_item_revision_stale', - 'agent_session_already_resolved', - 'agent_session_identity_required', - 'agent_session_journal_unreadable', - 'execution_owner_reconciling' -] as const -export type AgentSessionWireRefusalCode = (typeof AGENT_SESSION_WIRE_REFUSAL_CODES)[number] - -/** For a host path that raises its refusal as the thrown code. Narrowing through this keeps an - * unrelated fault from being reported to the client as a tidy, wrong refusal. */ -export function isAgentSessionWireRefusalCode( - value: unknown -): value is AgentSessionWireRefusalCode { - return ( - typeof value === 'string' && - (AGENT_SESSION_WIRE_REFUSAL_CODES as readonly string[]).includes(value) - ) -} - -export type AgentSessionWireRefusal = { - rewindReason?: AgentSessionRewindReason - code: AgentSessionWireRefusalCode - message: string - /** On a stale fence, so the client can retry without another round trip. */ - currentFence?: number - /** On a lost compare-and-set: the winning answer and who gave it. */ - resolution?: AgentJournalResolution - /** On a lost compare-and-set: the revision the host actually holds. */ - currentRevision?: number -} - export type AgentSessionMutationResult = | { ok: true