diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index b2ce8c05e9..d33e70d1c5 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -24965,7 +24965,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @experimental */ send: async (params: SendRequest): Promise => - connection.sendRequest("session.send", { sessionId, ...params }), + connection.sendRequest("session.send", { ...params, sessionId }), /** * Sends zero or more user messages to the session in a single turn and returns their message IDs. All provided messages are appended to the conversation in order, then exactly one agent turn runs over the resulting history. When the list is empty, one turn runs over the existing history with no new user message. Remote-backed (Mission Control) sessions do not support this method and will return an error. * @@ -24976,7 +24976,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @experimental */ sendMessages: async (params: SendMessagesRequest): Promise => - connection.sendRequest("session.sendMessages", { sessionId, ...params }), + connection.sendRequest("session.sendMessages", { ...params, sessionId }), /** @experimental */ sandbox: { /** @@ -24994,7 +24994,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of attempting to disable sandboxing for the current session. */ disableForSession: async (params: SandboxDisableForSessionRequest): Promise => - connection.sendRequest("session.sandbox.disableForSession", { sessionId, ...params }), + connection.sendRequest("session.sandbox.disableForSession", { ...params, sessionId }), }, /** * Aborts the current agent turn. @@ -25006,7 +25006,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @experimental */ abort: async (params: AbortRequest): Promise => - connection.sendRequest("session.abort", { sessionId, ...params }), + connection.sendRequest("session.abort", { ...params, sessionId }), /** * Interrupts the current main agent turn while leaving running background work (subagents, sidekicks, and promoted attached shells) alive. No-op when the main loop is not processing. * @@ -25017,7 +25017,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @experimental */ interruptMainTurn: async (params: InterruptMainTurnRequest): Promise => - connection.sendRequest("session.interruptMainTurn", { sessionId, ...params }), + connection.sendRequest("session.interruptMainTurn", { ...params, sessionId }), /** * Cancels every running background agent (task-registry subagents plus sidekick agents) without interrupting the main agent loop. Promoted attached shells are left running. * @@ -25035,7 +25035,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @experimental */ shutdown: async (params: ShutdownRequest): Promise => - connection.sendRequest("session.shutdown", { sessionId, ...params }), + connection.sendRequest("session.shutdown", { ...params, sessionId }), /** @experimental */ gitHubAuth: { /** @@ -25053,7 +25053,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the credential update succeeded. */ setCredentials: async (params: SessionSetCredentialsParams): Promise => - connection.sendRequest("session.gitHubAuth.setCredentials", { sessionId, ...params }), + connection.sendRequest("session.gitHubAuth.setCredentials", { ...params, sessionId }), }, /** @experimental */ debug: { @@ -25065,7 +25065,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of collecting a redacted debug bundle. */ collectLogs: async (params: DebugCollectLogsRequest): Promise => - connection.sendRequest("session.debug.collectLogs", { sessionId, ...params }), + connection.sendRequest("session.debug.collectLogs", { ...params, sessionId }), }, /** @experimental */ canvas: { @@ -25091,14 +25091,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Open canvas instance snapshot. */ open: async (params: CanvasOpenRequest): Promise => - connection.sendRequest("session.canvas.open", { sessionId, ...params }), + connection.sendRequest("session.canvas.open", { ...params, sessionId }), /** * Closes an open canvas instance. * * @param params Canvas close parameters. */ close: async (params: CanvasCloseRequest): Promise => - connection.sendRequest("session.canvas.close", { sessionId, ...params }), + connection.sendRequest("session.canvas.close", { ...params, sessionId }), /** @experimental */ action: { /** @@ -25109,7 +25109,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Canvas action invocation result. */ invoke: async (params: CanvasActionInvokeRequest): Promise => - connection.sendRequest("session.canvas.action.invoke", { sessionId, ...params }), + connection.sendRequest("session.canvas.action.invoke", { ...params, sessionId }), }, }, /** @experimental */ @@ -25122,7 +25122,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Complete current or terminal factory run envelope. */ run: async (params: FactoryRunRequest): Promise => - connection.sendRequest("session.factory.run", { sessionId, ...params }), + connection.sendRequest("session.factory.run", { ...params, sessionId }), /** * Resumes a factory run using its persisted name, arguments, journal, and accounting. * @@ -25131,7 +25131,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Resolved persisted factory identity and resumed run envelope. */ resume: async (params: FactoryResumeRequest): Promise => - connection.sendRequest("session.factory.resume", { sessionId, ...params }), + connection.sendRequest("session.factory.resume", { ...params, sessionId }), /** * Gets the current or settled envelope for a factory run. * @@ -25140,7 +25140,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Complete current or terminal factory run envelope. */ getRun: async (params: FactoryGetRunRequest): Promise => - connection.sendRequest("session.factory.getRun", { sessionId, ...params }), + connection.sendRequest("session.factory.getRun", { ...params, sessionId }), /** * Lists durable factory runs for this session in creation order. * @@ -25149,7 +25149,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns A page of factory runs in durable creation order. */ listRuns: async (params: FactoryListRunsRequest): Promise => - connection.sendRequest("session.factory.listRuns", { sessionId, ...params }), + connection.sendRequest("session.factory.listRuns", { ...params, sessionId }), /** * Gets durable and live observability detail for one factory run. * @@ -25158,7 +25158,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Full factory run observability detail. */ getRunDetail: async (params: FactoryGetRunRequest): Promise => - connection.sendRequest("session.factory.getRunDetail", { sessionId, ...params }), + connection.sendRequest("session.factory.getRunDetail", { ...params, sessionId }), /** * Pages durable progress for one factory run. * @@ -25167,7 +25167,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns A bidirectional page of factory progress. */ getRunProgress: async (params: FactoryGetRunProgressRequest): Promise => - connection.sendRequest("session.factory.getRunProgress", { sessionId, ...params }), + connection.sendRequest("session.factory.getRunProgress", { ...params, sessionId }), /** * Requests cancellation of a factory run and returns its run envelope. * @@ -25176,7 +25176,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Complete current or terminal factory run envelope. */ cancel: async (params: FactoryCancelRequest): Promise => - connection.sendRequest("session.factory.cancel", { sessionId, ...params }), + connection.sendRequest("session.factory.cancel", { ...params, sessionId }), /** * Pauses a running factory and returns its settled run envelope. * @@ -25185,7 +25185,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Complete current or terminal factory run envelope. */ pause: async (params: FactoryPauseRequest): Promise => - connection.sendRequest("session.factory.pause", { sessionId, ...params }), + connection.sendRequest("session.factory.pause", { ...params, sessionId }), /** * Records a batch of ordered factory progress lines. * @@ -25194,7 +25194,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Acknowledgement that a factory request was accepted. */ log: async (params: FactoryLogRequest): Promise => - connection.sendRequest("session.factory.log", { sessionId, ...params }), + connection.sendRequest("session.factory.log", { ...params, sessionId }), /** * Runs one factory-scoped subagent and returns its result. * @@ -25203,7 +25203,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of one factory-scoped subagent call. */ agent: async (params: FactoryAgentRequest): Promise => - connection.sendRequest("session.factory.agent", { sessionId, ...params }), + connection.sendRequest("session.factory.agent", { ...params, sessionId }), /** @experimental */ journal: { /** @@ -25214,7 +25214,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of reading a factory journal entry. */ get: async (params: FactoryJournalGetRequest): Promise => - connection.sendRequest("session.factory.journal.get", { sessionId, ...params }), + connection.sendRequest("session.factory.journal.get", { ...params, sessionId }), /** * Stores a memoized factory journal entry. * @@ -25223,7 +25223,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Acknowledgement that a factory request was accepted. */ put: async (params: FactoryJournalPutRequest): Promise => - connection.sendRequest("session.factory.journal.put", { sessionId, ...params }), + connection.sendRequest("session.factory.journal.put", { ...params, sessionId }), }, }, /** @experimental */ @@ -25243,7 +25243,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The model identifier active on the session after the switch. */ switchTo: async (params: ModelSwitchToRequest): Promise => - connection.sendRequest("session.model.switchTo", { sessionId, ...params }), + connection.sendRequest("session.model.switchTo", { ...params, sessionId }), /** * Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. * @@ -25252,7 +25252,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. */ switchAutoTier: async (params: ModelSwitchAutoTierRequest): Promise => - connection.sendRequest("session.model.switchAutoTier", { sessionId, ...params }), + connection.sendRequest("session.model.switchAutoTier", { ...params, sessionId }), /** * Replaces or clears the host-supplied model allowlist for a running session. * @@ -25261,7 +25261,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The applied host allowlist and effective session model policy after intersection. */ setAllowedModels: async (params: ModelSetAllowedModelsRequest): Promise => - connection.sendRequest("session.model.setAllowedModels", { sessionId, ...params }), + connection.sendRequest("session.model.setAllowedModels", { ...params, sessionId }), /** * Updates the session's reasoning effort without changing the selected model. * @@ -25270,7 +25270,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Update the session's reasoning effort without changing the selected model. Use `switchTo` instead when you also need to change the model. The runtime stores the effort on the session and applies it to subsequent turns. */ setReasoningEffort: async (params: ModelSetReasoningEffortRequest): Promise => - connection.sendRequest("session.model.setReasoningEffort", { sessionId, ...params }), + connection.sendRequest("session.model.setReasoningEffort", { ...params, sessionId }), /** * Lists models available to this session using its own auth and integration context. Connected hosts (CLI TUI, GitHub App) should call this through the session client so remote sessions return the remote CLI's available models rather than the caller's. * @@ -25279,7 +25279,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The list of models available to this session. */ list: async (params?: SessionModelListRequest): Promise => - connection.sendRequest("session.model.list", { sessionId, ...params }), + connection.sendRequest("session.model.list", { ...params, sessionId }), }, /** @experimental */ mode: { @@ -25298,7 +25298,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Outcome of a session mode change, including any model switch it triggered and follow-up the host must perform. */ set: async (params: ModeSetRequest): Promise => - connection.sendRequest("session.mode.set", { sessionId, ...params }), + connection.sendRequest("session.mode.set", { ...params, sessionId }), }, /** @experimental */ name: { @@ -25315,7 +25315,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params New friendly name to apply to the session. */ set: async (params: NameSetRequest): Promise => - connection.sendRequest("session.name.set", { sessionId, ...params }), + connection.sendRequest("session.name.set", { ...params, sessionId }), /** * Persists an auto-generated session summary as the session's name when no user-set name exists. * @@ -25324,7 +25324,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the auto-generated summary was applied as the session's name. */ setAuto: async (params: NameSetAutoRequest): Promise => - connection.sendRequest("session.name.setAuto", { sessionId, ...params }), + connection.sendRequest("session.name.setAuto", { ...params, sessionId }), }, /** @experimental */ plan: { @@ -25341,7 +25341,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Replacement contents to write to the session plan file. */ update: async (params: PlanUpdateRequest): Promise => - connection.sendRequest("session.plan.update", { sessionId, ...params }), + connection.sendRequest("session.plan.update", { ...params, sessionId }), /** * Deletes the session plan file from the workspace. */ @@ -25379,7 +25379,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Current workspace metadata for the session, including its absolute filesystem path when available. */ updateMetadata: async (params: WorkspacesUpdateMetadataRequest): Promise => - connection.sendRequest("session.workspaces.updateMetadata", { sessionId, ...params }), + connection.sendRequest("session.workspaces.updateMetadata", { ...params, sessionId }), /** * Ensures a local session workspace exists and returns it. * @@ -25388,7 +25388,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Current workspace metadata for the session, including its absolute filesystem path when available. */ ensure: async (params: WorkspacesEnsureRequest): Promise => - connection.sendRequest("session.workspaces.ensure", { sessionId, ...params }), + connection.sendRequest("session.workspaces.ensure", { ...params, sessionId }), /** * Lists files stored in the session workspace files directory. * @@ -25404,14 +25404,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Contents of the requested workspace file as a UTF-8 string. */ readFile: async (params: WorkspacesReadFileRequest): Promise => - connection.sendRequest("session.workspaces.readFile", { sessionId, ...params }), + connection.sendRequest("session.workspaces.readFile", { ...params, sessionId }), /** * Creates or overwrites a file in the session workspace files directory. * * @param params Relative path and UTF-8 content for the workspace file to create or overwrite. */ createFile: async (params: WorkspacesCreateFileRequest): Promise => - connection.sendRequest("session.workspaces.createFile", { sessionId, ...params }), + connection.sendRequest("session.workspaces.createFile", { ...params, sessionId }), /** * Lists workspace checkpoints in chronological order. * @@ -25427,7 +25427,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing. */ readCheckpoint: async (params: WorkspacesReadCheckpointRequest): Promise => - connection.sendRequest("session.workspaces.readCheckpoint", { sessionId, ...params }), + connection.sendRequest("session.workspaces.readCheckpoint", { ...params, sessionId }), /** * Adds a compaction summary checkpoint to the local session workspace. * @@ -25436,7 +25436,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Persisted summary metadata and refreshed workspace metadata. */ addSummary: async (params: WorkspacesAddSummaryRequest): Promise => - connection.sendRequest("session.workspaces.addSummary", { sessionId, ...params }), + connection.sendRequest("session.workspaces.addSummary", { ...params, sessionId }), /** * Truncates local workspace compaction summaries after a rollback. * @@ -25445,7 +25445,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Current workspace metadata for the session, including its absolute filesystem path when available. */ truncateSummaries: async (params: WorkspacesTruncateSummariesRequest): Promise => - connection.sendRequest("session.workspaces.truncateSummaries", { sessionId, ...params }), + connection.sendRequest("session.workspaces.truncateSummaries", { ...params, sessionId }), /** * Reads the autopilot objective state file from the local session workspace. * @@ -25461,7 +25461,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of writing the autopilot objective file. */ writeAutopilotObjective: async (params: WorkspacesWriteAutopilotObjectiveRequest): Promise => - connection.sendRequest("session.workspaces.writeAutopilotObjective", { sessionId, ...params }), + connection.sendRequest("session.workspaces.writeAutopilotObjective", { ...params, sessionId }), /** * Deletes the autopilot objective state file from the local session workspace. * @@ -25484,7 +25484,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Descriptor for the saved paste file, or null when the workspace is unavailable. */ saveLargePaste: async (params: WorkspacesSaveLargePasteRequest): Promise => - connection.sendRequest("session.workspaces.saveLargePaste", { sessionId, ...params }), + connection.sendRequest("session.workspaces.saveLargePaste", { ...params, sessionId }), /** * Computes a diff for the session workspace. Never rejects for a busy session: a `session`-mode diff that cannot read the session's file-change captures falls back to an unstaged git diff with `isFallback: true` and reports why in `unavailableReason`. * @@ -25493,7 +25493,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Workspace diff result for the requested mode. */ diff: async (params: WorkspacesDiffRequest): Promise => - connection.sendRequest("session.workspaces.diff", { sessionId, ...params }), + connection.sendRequest("session.workspaces.diff", { ...params, sessionId }), }, /** @experimental */ autopilotObjective: { @@ -25522,7 +25522,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Host-driven completion items for the current composer input. Empty when the host returns no items or does not support completions. */ request: async (params: CompletionsRequestRequest): Promise => - connection.sendRequest("session.completions.request", { sessionId, ...params }), + connection.sendRequest("session.completions.request", { ...params, sessionId }), }, /** @experimental */ instructions: { @@ -25544,7 +25544,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether fleet mode was successfully activated. */ start: async (params: FleetStartRequest): Promise => - connection.sendRequest("session.fleet.start", { sessionId, ...params }), + connection.sendRequest("session.fleet.start", { ...params, sessionId }), }, /** @experimental */ agent: { @@ -25556,14 +25556,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Agents available to the session. */ list: async (params?: SessionAgentListRequest): Promise => - connection.sendRequest("session.agent.list", { sessionId, ...params }), + connection.sendRequest("session.agent.list", { ...params, sessionId }), /** * Sets an in-memory authored prompt override for an available agent. For built-in agents, this replaces only the static base prompt while preserving runtime-owned dynamic prompt composition and behavior. The special `general-purpose` agent is not overrideable. Overrides are not persisted; resumed and forked sessions start without them, so the host must re-apply them. * * @param params An in-memory authored prompt override for an available agent. */ setPrompt: async (params: AgentSetPromptRequest): Promise => - connection.sendRequest("session.agent.setPrompt", { sessionId, ...params }), + connection.sendRequest("session.agent.setPrompt", { ...params, sessionId }), /** * Gets the currently selected custom agent for the session. * @@ -25579,7 +25579,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The newly selected custom agent. */ select: async (params: AgentSelectRequest): Promise => - connection.sendRequest("session.agent.select", { sessionId, ...params }), + connection.sendRequest("session.agent.select", { ...params, sessionId }), /** * Clears the selected custom agent and returns the session to the default agent. */ @@ -25603,7 +25603,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Identifier assigned to the newly started background agent task. */ startAgent: async (params: TasksStartAgentRequest): Promise => - connection.sendRequest("session.tasks.startAgent", { sessionId, ...params }), + connection.sendRequest("session.tasks.startAgent", { ...params, sessionId }), /** * Lists background tasks tracked by the session. * @@ -25619,7 +25619,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of registering or reclaiming a client-owned task. */ register: async (params: TasksRegisterRequest): Promise => - connection.sendRequest("session.tasks.register", { sessionId, ...params }), + connection.sendRequest("session.tasks.register", { ...params, sessionId }), /** * Publishes generic progress or a terminal outcome for a client-owned task. * @@ -25628,7 +25628,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of publishing a client-owned task update. */ update: async (params: TasksUpdateRequest): Promise => - connection.sendRequest("session.tasks.update", { sessionId, ...params }), + connection.sendRequest("session.tasks.update", { ...params, sessionId }), /** * Refreshes metadata for any detached background shells the runtime knows about. * @@ -25651,7 +25651,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Progress information for the task, or null when no task with that ID is tracked. */ getProgress: async (params: TasksGetProgressRequest): Promise => - connection.sendRequest("session.tasks.getProgress", { sessionId, ...params }), + connection.sendRequest("session.tasks.getProgress", { ...params, sessionId }), /** * Returns the first sync-waiting task that can currently be promoted to background mode. * @@ -25667,7 +25667,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the task was successfully promoted to background mode. */ promoteToBackground: async (params: TasksPromoteToBackgroundRequest): Promise => - connection.sendRequest("session.tasks.promoteToBackground", { sessionId, ...params }), + connection.sendRequest("session.tasks.promoteToBackground", { ...params, sessionId }), /** * Atomically promotes the first promotable sync-waiting task to background mode and returns it. * @@ -25683,7 +25683,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the background task was successfully cancelled. */ cancel: async (params: TasksCancelRequest): Promise => - connection.sendRequest("session.tasks.cancel", { sessionId, ...params }), + connection.sendRequest("session.tasks.cancel", { ...params, sessionId }), /** * Removes a completed or cancelled background task from tracking. * @@ -25692,7 +25692,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the task was removed. False when the task does not exist or is still running/idle. */ remove: async (params: TasksRemoveRequest): Promise => - connection.sendRequest("session.tasks.remove", { sessionId, ...params }), + connection.sendRequest("session.tasks.remove", { ...params, sessionId }), /** * Sends a message to a background agent task. * @@ -25701,7 +25701,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the message was delivered, with an error message when delivery failed. */ sendMessage: async (params: TasksSendMessageRequest): Promise => - connection.sendRequest("session.tasks.sendMessage", { sessionId, ...params }), + connection.sendRequest("session.tasks.sendMessage", { ...params, sessionId }), }, /** @experimental */ skills: { @@ -25725,14 +25725,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Name of the skill to enable for the session. */ enable: async (params: SkillsEnableRequest): Promise => - connection.sendRequest("session.skills.enable", { sessionId, ...params }), + connection.sendRequest("session.skills.enable", { ...params, sessionId }), /** * Disables a skill for the session. * * @param params Name of the skill to disable for the session. */ disable: async (params: SkillsDisableRequest): Promise => - connection.sendRequest("session.skills.disable", { sessionId, ...params }), + connection.sendRequest("session.skills.disable", { ...params, sessionId }), /** * Reloads skill definitions for the session. * @@ -25763,21 +25763,21 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Tools exposed by the connected MCP server. Throws when the server is not connected. */ listTools: async (params: McpListToolsRequest): Promise => - connection.sendRequest("session.mcp.listTools", { sessionId, ...params }), + connection.sendRequest("session.mcp.listTools", { ...params, sessionId }), /** * Enables an MCP server for the session. * * @param params Name of the MCP server to enable for the session. */ enable: async (params: McpEnableRequest): Promise => - connection.sendRequest("session.mcp.enable", { sessionId, ...params }), + connection.sendRequest("session.mcp.enable", { ...params, sessionId }), /** * Disables an MCP server for the session. * * @param params Name of the MCP server to disable for the session. */ disable: async (params: McpDisableRequest): Promise => - connection.sendRequest("session.mcp.disable", { sessionId, ...params }), + connection.sendRequest("session.mcp.disable", { ...params, sessionId }), /** * Reloads MCP server connections for the session. */ @@ -25798,7 +25798,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Outcome of an MCP sampling execution: success result, failure error, or cancellation. */ executeSampling: async (params: McpExecuteSamplingParams): Promise => - connection.sendRequest("session.mcp.executeSampling", { sessionId, ...params }), + connection.sendRequest("session.mcp.executeSampling", { ...params, sessionId }), /** * Cancels an in-flight MCP sampling execution by request ID. * @@ -25807,7 +25807,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether an in-flight sampling execution with the given requestId was found and cancelled. */ cancelSamplingExecution: async (params: McpCancelSamplingExecutionParams): Promise => - connection.sendRequest("session.mcp.cancelSamplingExecution", { sessionId, ...params }), + connection.sendRequest("session.mcp.cancelSamplingExecution", { ...params, sessionId }), /** * Sets how environment-variable values supplied to MCP servers are resolved (direct or indirect). * @@ -25816,7 +25816,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Env-value mode recorded on the session after the update. */ setEnvValueMode: async (params: McpSetEnvValueModeParams): Promise => - connection.sendRequest("session.mcp.setEnvValueMode", { sessionId, ...params }), + connection.sendRequest("session.mcp.setEnvValueMode", { ...params, sessionId }), /** * Removes the auto-managed `github` MCP server when present. * @@ -25830,21 +25830,21 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Server name and optional configuration for an individual MCP server start. Omit `config` for a config-free start-by-name of an already-configured server. */ startServer: async (params: McpStartServerRequest): Promise => - connection.sendRequest("session.mcp.startServer", { sessionId, ...params }), + connection.sendRequest("session.mcp.startServer", { ...params, sessionId }), /** * Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`). * * @param params Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. */ restartServer: async (params: McpRestartServerRequest): Promise => - connection.sendRequest("session.mcp.restartServer", { sessionId, ...params }), + connection.sendRequest("session.mcp.restartServer", { ...params, sessionId }), /** * Stops an individual MCP server on the session's host. * * @param params Server name for an individual MCP server stop. */ stopServer: async (params: McpStopServerRequest): Promise => - connection.sendRequest("session.mcp.stopServer", { sessionId, ...params }), + connection.sendRequest("session.mcp.stopServer", { ...params, sessionId }), /** * Checks whether a named MCP server is currently running on the session's host. * @@ -25853,7 +25853,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Whether the named MCP server is running. */ isServerRunning: async (params: McpIsServerRunningRequest): Promise => - connection.sendRequest("session.mcp.isServerRunning", { sessionId, ...params }), + connection.sendRequest("session.mcp.isServerRunning", { ...params, sessionId }), /** @experimental */ oauth: { /** @@ -25864,14 +25864,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending MCP OAuth response was accepted. */ handlePendingRequest: async (params: McpOauthHandlePendingRequest): Promise => - connection.sendRequest("session.mcp.oauth.handlePendingRequest", { sessionId, ...params }), + connection.sendRequest("session.mcp.oauth.handlePendingRequest", { ...params, sessionId }), /** * Notifies the session that MCP OAuth authentication succeeded and updated credentials were persisted, so cached tool definitions can be refreshed. * * @param params Identifies the MCP server whose persisted OAuth credentials were updated. */ authenticationStateChanged: async (params: McpOauthAuthenticationStateChangedRequest): Promise => - connection.sendRequest("session.mcp.oauth.authenticationStateChanged", { sessionId, ...params }), + connection.sendRequest("session.mcp.oauth.authenticationStateChanged", { ...params, sessionId }), /** * Starts OAuth authentication for a remote MCP server. * @@ -25880,7 +25880,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. */ login: async (params: McpOauthLoginRequest): Promise => - connection.sendRequest("session.mcp.oauth.login", { sessionId, ...params }), + connection.sendRequest("session.mcp.oauth.login", { ...params, sessionId }), /** * Passively probes a configured remote MCP server to classify whether OAuth is required or a cached/override token is accepted. Does not start OAuth, emit pending OAuth requests, or mutate MCP connection state. * @@ -25889,7 +25889,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Passive MCP OAuth probe result. `authenticated` means the server accepted the probe request while an OAuth-origin access token was attached; it does not prove the server required or independently validated that token. The probe does not make a second unauthenticated request. Failed is an expected probe-domain outcome; JSON-RPC errors are reserved for API-call failures. */ probe: async (params: McpOauthProbeRequest): Promise => - connection.sendRequest("session.mcp.oauth.probe", { sessionId, ...params }), + connection.sendRequest("session.mcp.oauth.probe", { ...params, sessionId }), /** * Responds to a pending MCP OAuth authorization request by its request id. * @@ -25898,7 +25898,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending MCP OAuth response was accepted. */ respond: async (params: McpOauthRespondRequest): Promise => - connection.sendRequest("session.mcp.oauth.respond", { sessionId, ...params }), + connection.sendRequest("session.mcp.oauth.respond", { ...params, sessionId }), }, /** @experimental */ headers: { @@ -25910,7 +25910,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending MCP headers refresh response was accepted. */ handlePendingHeadersRefreshRequest: async (params: McpHeadersHandlePendingHeadersRefreshRequestRequest): Promise => - connection.sendRequest("session.mcp.headers.handlePendingHeadersRefreshRequest", { sessionId, ...params }), + connection.sendRequest("session.mcp.headers.handlePendingHeadersRefreshRequest", { ...params, sessionId }), }, /** @experimental */ apps: { @@ -25922,7 +25922,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Resource contents returned by the MCP server. */ readResource: async (params: McpAppsReadResourceRequest): Promise => - connection.sendRequest("session.mcp.apps.readResource", { sessionId, ...params }), + connection.sendRequest("session.mcp.apps.readResource", { ...params, sessionId }), /** * List tools that an MCP App view is allowed to call (SEP-1865 visibility filter). Returns tools whose `_meta.ui.visibility` is unset (default `["model","app"]`) or includes `"app"`. * @@ -25931,7 +25931,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns App-callable tools from the named MCP server. */ listTools: async (params: McpAppsListToolsRequest): Promise => - connection.sendRequest("session.mcp.apps.listTools", { sessionId, ...params }), + connection.sendRequest("session.mcp.apps.listTools", { ...params, sessionId }), /** * Call an MCP tool from an MCP App view (SEP-1865). Enforces the visibility check that prevents an app iframe from invoking model-only tools. Returns the standard MCP `CallToolResult`. * @@ -25940,14 +25940,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Standard MCP CallToolResult */ callTool: async (params: McpAppsCallToolRequest): Promise => - connection.sendRequest("session.mcp.apps.callTool", { sessionId, ...params }), + connection.sendRequest("session.mcp.apps.callTool", { ...params, sessionId }), /** * Replace the host context returned to MCP App guests on `ui/initialize`. Hosts use this to advertise theme, locale, or other metadata to the guest UI. * * @param params Host context to advertise to MCP App guests. */ setHostContext: async (params: McpAppsSetHostContextRequest): Promise => - connection.sendRequest("session.mcp.apps.setHostContext", { sessionId, ...params }), + connection.sendRequest("session.mcp.apps.setHostContext", { ...params, sessionId }), /** * Read the current host context advertised to MCP App guests. * @@ -25963,7 +25963,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Diagnostic snapshot of MCP Apps wiring for the named server. */ diagnose: async (params: McpAppsDiagnoseRequest): Promise => - connection.sendRequest("session.mcp.apps.diagnose", { sessionId, ...params }), + connection.sendRequest("session.mcp.apps.diagnose", { ...params, sessionId }), }, /** @experimental */ resources: { @@ -25975,7 +25975,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Resource contents returned by the MCP server. */ read: async (params: McpResourcesReadRequest): Promise => - connection.sendRequest("session.mcp.resources.read", { sessionId, ...params }), + connection.sendRequest("session.mcp.resources.read", { ...params, sessionId }), /** * Enumerate one page of resources a connected MCP server exposes (proxies MCP `resources/list`). Pass `cursor` to continue from a prior result's `nextCursor`. * @@ -25984,7 +25984,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns One page of resources advertised by the named MCP server. */ list: async (params: McpResourcesListRequest): Promise => - connection.sendRequest("session.mcp.resources.list", { sessionId, ...params }), + connection.sendRequest("session.mcp.resources.list", { ...params, sessionId }), /** * Enumerate one page of resource templates a connected MCP server exposes (proxies MCP `resources/templates/list`). Pass `cursor` to continue from a prior result's `nextCursor`. * @@ -25993,7 +25993,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns One page of resource templates advertised by the named MCP server. */ listTemplates: async (params: McpResourcesListTemplatesRequest): Promise => - connection.sendRequest("session.mcp.resources.listTemplates", { sessionId, ...params }), + connection.sendRequest("session.mcp.resources.listTemplates", { ...params, sessionId }), }, }, /** @experimental */ @@ -26011,7 +26011,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Optional flags controlling which side effects the reload performs. */ reload: async (params?: SessionPluginsReloadRequest): Promise => - connection.sendRequest("session.plugins.reload", { sessionId, ...params }), + connection.sendRequest("session.plugins.reload", { ...params, sessionId }), }, /** @experimental */ provider: { @@ -26023,7 +26023,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns A snapshot of the provider endpoint the session is currently configured to talk to. */ getEndpoint: async (params?: SessionProviderGetEndpointRequest): Promise => - connection.sendRequest("session.provider.getEndpoint", { sessionId, ...params }), + connection.sendRequest("session.provider.getEndpoint", { ...params, sessionId }), /** * Adds BYOK providers and/or models to the session's registry at runtime, extending the additive registry built from the session's `providers`/`models` options. Both fields are optional, so a call may add providers only, models only, or both. Within a single call providers are registered before models, so a model may reference a provider added in the same call; across calls a model may reference any provider already registered (from session creation or a prior add). A model whose referenced provider is not registered by the end of the call is rejected. Newly added models become selectable via `model.list` / `model.switchTo` and are inherited by sub-agents spawned afterwards. * @@ -26032,7 +26032,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The selectable model entries synthesized for the models added by this call. */ add: async (params: ProviderAddRequest): Promise => - connection.sendRequest("session.provider.add", { sessionId, ...params }), + connection.sendRequest("session.provider.add", { ...params, sessionId }), }, /** @experimental */ options: { @@ -26044,7 +26044,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the session options patch was applied successfully. */ update: async (params: SessionUpdateOptionsParams): Promise => - connection.sendRequest("session.options.update", { sessionId, ...params }), + connection.sendRequest("session.options.update", { ...params, sessionId }), }, /** @experimental */ lsp: { @@ -26054,7 +26054,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Parameters for (re)loading the merged LSP configuration set. */ initialize: async (params: LspInitializeRequest): Promise => - connection.sendRequest("session.lsp.initialize", { sessionId, ...params }), + connection.sendRequest("session.lsp.initialize", { ...params, sessionId }), }, /** @experimental */ extensions: { @@ -26071,14 +26071,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Source-qualified extension identifier to enable for the session. */ enable: async (params: ExtensionsEnableRequest): Promise => - connection.sendRequest("session.extensions.enable", { sessionId, ...params }), + connection.sendRequest("session.extensions.enable", { ...params, sessionId }), /** * Disables an extension for the session. * * @param params Source-qualified extension identifier to disable for the session. */ disable: async (params: ExtensionsDisableRequest): Promise => - connection.sendRequest("session.extensions.disable", { sessionId, ...params }), + connection.sendRequest("session.extensions.disable", { ...params, sessionId }), /** * Reloads extension definitions and processes for the session. */ @@ -26090,7 +26090,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Parameters for session.extensions.sendAttachmentsToMessage. */ sendAttachmentsToMessage: async (params: SendAttachmentsToMessageParams): Promise => - connection.sendRequest("session.extensions.sendAttachmentsToMessage", { sessionId, ...params }), + connection.sendRequest("session.extensions.sendAttachmentsToMessage", { ...params, sessionId }), }, /** @experimental */ tools: { @@ -26102,7 +26102,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Canonical result returned by a session tool. */ execute: async (params: ToolsExecuteRequest): Promise => - connection.sendRequest("session.tools.execute", { sessionId, ...params }), + connection.sendRequest("session.tools.execute", { ...params, sessionId }), /** * Returns the Rust-owned built-in tool descriptors used to construct the session's offered tool set. * @@ -26111,7 +26111,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Rust-owned built-in tool descriptors for the session. */ getBuiltinDescriptors: async (params: ToolsGetBuiltinDescriptorsRequest): Promise => - connection.sendRequest("session.tools.getBuiltinDescriptors", { sessionId, ...params }), + connection.sendRequest("session.tools.getBuiltinDescriptors", { ...params, sessionId }), /** * Projects a completed task_complete tool call into its label-safe session event payload. * @@ -26120,7 +26120,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Task completion notification with summary from the agent */ taskCompleteEventData: async (params: ToolsTaskCompleteEventDataRequest): Promise => - connection.sendRequest("session.tools.taskCompleteEventData", { sessionId, ...params }), + connection.sendRequest("session.tools.taskCompleteEventData", { ...params, sessionId }), /** * Provides the result for a pending external tool call. * @@ -26129,7 +26129,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the external tool call result was handled successfully. */ handlePendingToolCall: async (params: HandlePendingToolCallRequest): Promise => - connection.sendRequest("session.tools.handlePendingToolCall", { sessionId, ...params }), + connection.sendRequest("session.tools.handlePendingToolCall", { ...params, sessionId }), /** * Resolves, builds, and validates the runtime tool list for the session. * @@ -26152,7 +26152,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Empty result after replacing the calling connection's externally implemented tools. */ set: async (params: ToolsSetRequest): Promise => - connection.sendRequest("session.tools.set", { sessionId, ...params }), + connection.sendRequest("session.tools.set", { ...params, sessionId }), /** * Sets the current session's live subagent settings override, which takes precedence over persisted user settings until cleared. Persisted user settings remain the source of truth for future sessions. * @@ -26161,7 +26161,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Empty result after applying subagent settings */ updateSubagentSettings: async (params: UpdateSubagentSettingsRequest): Promise => - connection.sendRequest("session.tools.updateSubagentSettings", { sessionId, ...params }), + connection.sendRequest("session.tools.updateSubagentSettings", { ...params, sessionId }), }, /** @experimental */ commands: { @@ -26173,7 +26173,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Slash commands available in the session, after applying any include/exclude filters. */ list: async (params?: SessionCommandsListRequest): Promise => - connection.sendRequest("session.commands.list", { sessionId, ...params }), + connection.sendRequest("session.commands.list", { ...params, sessionId }), /** * Invokes a slash command in the session. * @@ -26182,7 +26182,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). */ invoke: async (params: CommandsInvokeRequest): Promise => - connection.sendRequest("session.commands.invoke", { sessionId, ...params }), + connection.sendRequest("session.commands.invoke", { ...params, sessionId }), /** * Reports completion of a pending client-handled slash command. * @@ -26191,7 +26191,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending client-handled command was completed successfully. */ handlePendingCommand: async (params: CommandsHandlePendingCommandRequest): Promise => - connection.sendRequest("session.commands.handlePendingCommand", { sessionId, ...params }), + connection.sendRequest("session.commands.handlePendingCommand", { ...params, sessionId }), /** * Executes a slash command synchronously and returns any error. * @@ -26200,7 +26200,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Error message produced while executing the command, if any. */ execute: async (params: ExecuteCommandParams): Promise => - connection.sendRequest("session.commands.execute", { sessionId, ...params }), + connection.sendRequest("session.commands.execute", { ...params, sessionId }), /** * Enqueues a slash command for FIFO processing on the local session. * @@ -26209,7 +26209,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the command was accepted into the local execution queue. */ enqueue: async (params: EnqueueCommandParams): Promise => - connection.sendRequest("session.commands.enqueue", { sessionId, ...params }), + connection.sendRequest("session.commands.enqueue", { ...params, sessionId }), /** * Reports whether the host actually executed a queued command and whether to continue processing. * @@ -26218,7 +26218,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the queued-command response was matched to a pending request. */ respondToQueuedCommand: async (params: CommandsRespondToQueuedCommandRequest): Promise => - connection.sendRequest("session.commands.respondToQueuedCommand", { sessionId, ...params }), + connection.sendRequest("session.commands.respondToQueuedCommand", { ...params, sessionId }), }, /** @experimental */ telemetry: { @@ -26235,7 +26235,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Feature override key/value pairs to attach to subsequent telemetry events from this session. */ setFeatureOverrides: async (params: TelemetrySetFeatureOverridesRequest): Promise => - connection.sendRequest("session.telemetry.setFeatureOverrides", { sessionId, ...params }), + connection.sendRequest("session.telemetry.setFeatureOverrides", { ...params, sessionId }), }, /** @experimental */ ui: { @@ -26247,7 +26247,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Completed transient query. Ordered chunks and the terminal outcome are also delivered through `ui.ephemeral_query` session events while it runs. */ ephemeralQuery: async (params: UIEphemeralQueryRequest): Promise => - connection.sendRequest("session.ui.ephemeralQuery", { sessionId, ...params }), + connection.sendRequest("session.ui.ephemeralQuery", { ...params, sessionId }), /** * Requests structured input from a UI-capable client. * @@ -26256,7 +26256,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The elicitation response (accept with form values, decline, or cancel) */ elicitation: async (params: UIElicitationRequest): Promise => - connection.sendRequest("session.ui.elicitation", { sessionId, ...params }), + connection.sendRequest("session.ui.elicitation", { ...params, sessionId }), /** * Provides the user response for a pending elicitation request. * @@ -26265,7 +26265,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the elicitation response was accepted; false if it was already resolved by another client. */ handlePendingElicitation: async (params: UIHandlePendingElicitationRequest): Promise => - connection.sendRequest("session.ui.handlePendingElicitation", { sessionId, ...params }), + connection.sendRequest("session.ui.handlePendingElicitation", { ...params, sessionId }), /** * Resolves a pending `user_input.requested` event with the user's response. * @@ -26274,7 +26274,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending UI request was resolved by this call. */ handlePendingUserInput: async (params: UIHandlePendingUserInputRequest): Promise => - connection.sendRequest("session.ui.handlePendingUserInput", { sessionId, ...params }), + connection.sendRequest("session.ui.handlePendingUserInput", { ...params, sessionId }), /** * Resolves a pending `sampling.requested` event with a sampling result, or rejects it. * @@ -26283,7 +26283,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending UI request was resolved by this call. */ handlePendingSampling: async (params: UIHandlePendingSamplingRequest): Promise => - connection.sendRequest("session.ui.handlePendingSampling", { sessionId, ...params }), + connection.sendRequest("session.ui.handlePendingSampling", { ...params, sessionId }), /** * Resolves a pending `auto_mode_switch.requested` event with the user's accept/decline decision. * @@ -26292,7 +26292,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending UI request was resolved by this call. */ handlePendingAutoModeSwitch: async (params: UIHandlePendingAutoModeSwitchRequest): Promise => - connection.sendRequest("session.ui.handlePendingAutoModeSwitch", { sessionId, ...params }), + connection.sendRequest("session.ui.handlePendingAutoModeSwitch", { ...params, sessionId }), /** * Resolves a pending `session_limits_exhausted.requested` event with the user's selected limit action. * @@ -26301,7 +26301,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending UI request was resolved by this call. */ handlePendingSessionLimitsExhausted: async (params: UIHandlePendingSessionLimitsExhaustedRequest): Promise => - connection.sendRequest("session.ui.handlePendingSessionLimitsExhausted", { sessionId, ...params }), + connection.sendRequest("session.ui.handlePendingSessionLimitsExhausted", { ...params, sessionId }), /** * Resolves a pending `exit_plan_mode.requested` event with the user's response. * @@ -26310,7 +26310,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending UI request was resolved by this call. */ handlePendingExitPlanMode: async (params: UIHandlePendingExitPlanModeRequest): Promise => - connection.sendRequest("session.ui.handlePendingExitPlanMode", { sessionId, ...params }), + connection.sendRequest("session.ui.handlePendingExitPlanMode", { ...params, sessionId }), /** * Registers an in-process handler for auto-mode-switch requests so the server bridge skips dispatch. * @@ -26326,7 +26326,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the handle was active and the registration count was decremented. */ unregisterDirectAutoModeSwitchHandler: async (params: UIUnregisterDirectAutoModeSwitchHandlerRequest): Promise => - connection.sendRequest("session.ui.unregisterDirectAutoModeSwitchHandler", { sessionId, ...params }), + connection.sendRequest("session.ui.unregisterDirectAutoModeSwitchHandler", { ...params, sessionId }), }, /** @experimental */ permissions: { @@ -26338,7 +26338,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ configure: async (params: PermissionsConfigureParams): Promise => - connection.sendRequest("session.permissions.configure", { sessionId, ...params }), + connection.sendRequest("session.permissions.configure", { ...params, sessionId }), /** * Provides a decision for a pending tool permission request. * @@ -26347,7 +26347,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the permission decision was applied; false when the request was already resolved. */ handlePendingPermissionRequest: async (params: PermissionDecisionRequest): Promise => - connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params }), + connection.sendRequest("session.permissions.handlePendingPermissionRequest", { ...params, sessionId }), /** * Reconstructs the set of pending tool permission requests from the session's event history. * @@ -26363,7 +26363,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ setApproveAll: async (params: PermissionsSetApproveAllRequest): Promise => - connection.sendRequest("session.permissions.setApproveAll", { sessionId, ...params }), + connection.sendRequest("session.permissions.setApproveAll", { ...params, sessionId }), /** * Sets the permission mode for the session. `manual` follows the normal approval flow, `assisted` attaches LLM safety recommendations, and `allow-all` automatically approves permission requests. The result returns the authoritative post-mutation mode so callers can update local state without racing the `session.permissions_changed` notification. * @@ -26372,7 +26372,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the requested permission mode was applied and reports the authoritative post-mutation mode. */ setMode: async (params: PermissionsSetModeRequest): Promise => - connection.sendRequest("session.permissions.setMode", { sessionId, ...params }), + connection.sendRequest("session.permissions.setMode", { ...params, sessionId }), /** * Returns the current permission mode for the session. * @@ -26388,7 +26388,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ modifyRules: async (params: PermissionsModifyRulesParams): Promise => - connection.sendRequest("session.permissions.modifyRules", { sessionId, ...params }), + connection.sendRequest("session.permissions.modifyRules", { ...params, sessionId }), /** * Sets whether the client wants permission prompts bridged into session events. * @@ -26397,7 +26397,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ setRequired: async (params: PermissionsSetRequiredRequest): Promise => - connection.sendRequest("session.permissions.setRequired", { sessionId, ...params }), + connection.sendRequest("session.permissions.setRequired", { ...params, sessionId }), /** * Clears session-scoped tool permission approvals. * @@ -26406,7 +26406,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ resetSessionApprovals: async (params: PermissionsResetSessionApprovalsRequest): Promise => - connection.sendRequest("session.permissions.resetSessionApprovals", { sessionId, ...params }), + connection.sendRequest("session.permissions.resetSessionApprovals", { ...params, sessionId }), /** * Notifies the runtime that a permission prompt UI has been shown to the user. * @@ -26415,7 +26415,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ notifyPromptShown: async (params: PermissionPromptShownNotification): Promise => - connection.sendRequest("session.permissions.notifyPromptShown", { sessionId, ...params }), + connection.sendRequest("session.permissions.notifyPromptShown", { ...params, sessionId }), /** @experimental */ paths: { /** @@ -26433,7 +26433,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ add: async (params: PermissionPathsAddParams): Promise => - connection.sendRequest("session.permissions.paths.add", { sessionId, ...params }), + connection.sendRequest("session.permissions.paths.add", { ...params, sessionId }), /** * Updates the session's primary working directory used by the permission policy. * @@ -26442,7 +26442,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ updatePrimary: async (params: PermissionPathsUpdatePrimaryParams): Promise => - connection.sendRequest("session.permissions.paths.updatePrimary", { sessionId, ...params }), + connection.sendRequest("session.permissions.paths.updatePrimary", { ...params, sessionId }), /** * Reports whether a path falls within any of the session's allowed directories. * @@ -26451,7 +26451,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the supplied path is within the session's allowed directories. */ isPathWithinAllowedDirectories: async (params: PermissionPathsAllowedCheckParams): Promise => - connection.sendRequest("session.permissions.paths.isPathWithinAllowedDirectories", { sessionId, ...params }), + connection.sendRequest("session.permissions.paths.isPathWithinAllowedDirectories", { ...params, sessionId }), /** * Reports whether a path falls within the session's workspace (primary) directory. * @@ -26460,7 +26460,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the supplied path is within the session's workspace directory. */ isPathWithinWorkspace: async (params: PermissionPathsWorkspaceCheckParams): Promise => - connection.sendRequest("session.permissions.paths.isPathWithinWorkspace", { sessionId, ...params }), + connection.sendRequest("session.permissions.paths.isPathWithinWorkspace", { ...params, sessionId }), }, /** @experimental */ locations: { @@ -26472,7 +26472,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Resolved location-permissions key and type. */ resolve: async (params: PermissionLocationResolveParams): Promise => - connection.sendRequest("session.permissions.locations.resolve", { sessionId, ...params }), + connection.sendRequest("session.permissions.locations.resolve", { ...params, sessionId }), /** * Applies persisted location-scoped tool approvals and allowed directories for a working directory to this session's permission service. * @@ -26481,7 +26481,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Summary of persisted location permissions applied to the session. */ apply: async (params: PermissionLocationApplyParams): Promise => - connection.sendRequest("session.permissions.locations.apply", { sessionId, ...params }), + connection.sendRequest("session.permissions.locations.apply", { ...params, sessionId }), /** * Persists a tool approval for a permission location and applies its rules to this session's live permission service. * @@ -26490,7 +26490,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ addToolApproval: async (params: PermissionLocationAddToolApprovalParams): Promise => - connection.sendRequest("session.permissions.locations.addToolApproval", { sessionId, ...params }), + connection.sendRequest("session.permissions.locations.addToolApproval", { ...params, sessionId }), }, /** @experimental */ folderTrust: { @@ -26502,7 +26502,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Folder trust check result. */ isTrusted: async (params: FolderTrustCheckParams): Promise => - connection.sendRequest("session.permissions.folderTrust.isTrusted", { sessionId, ...params }), + connection.sendRequest("session.permissions.folderTrust.isTrusted", { ...params, sessionId }), /** * Adds a folder to the user's trusted folders list. * @@ -26511,7 +26511,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ addTrusted: async (params: FolderTrustAddParams): Promise => - connection.sendRequest("session.permissions.folderTrust.addTrusted", { sessionId, ...params }), + connection.sendRequest("session.permissions.folderTrust.addTrusted", { ...params, sessionId }), }, /** @experimental */ urls: { @@ -26523,7 +26523,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ setUnrestrictedMode: async (params: PermissionUrlsSetUnrestrictedModeParams): Promise => - connection.sendRequest("session.permissions.urls.setUnrestrictedMode", { sessionId, ...params }), + connection.sendRequest("session.permissions.urls.setUnrestrictedMode", { ...params, sessionId }), }, }, /** @@ -26536,7 +26536,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @experimental */ log: async (params: LogRequest): Promise => - connection.sendRequest("session.log", { sessionId, ...params }), + connection.sendRequest("session.log", { ...params, sessionId }), /** @experimental */ metadata: { /** @@ -26561,7 +26561,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Client-owned, case-sensitive string metadata persisted with a local session. Clients should namespace keys by owner. Keys must be non-empty and at most 256 UTF-8 bytes; keys under `copilot/` and `github/` are reserved. Values may contain at most 16 KiB of UTF-8 data. A bag may contain at most 128 entries and its serialized sidecar may contain at most 64 KiB. The runtime stores but never interprets these values. */ updateClientMetadata: async (params: MetadataUpdateClientMetadataRequest): Promise => - connection.sendRequest("session.metadata.updateClientMetadata", { sessionId, ...params }), + connection.sendRequest("session.metadata.updateClientMetadata", { ...params, sessionId }), /** * Reports whether the local session is currently processing user/agent messages. * @@ -26584,7 +26584,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Token breakdown for the session's current context window, or null if uninitialized. */ contextInfo: async (params: MetadataContextInfoRequest): Promise => - connection.sendRequest("session.metadata.contextInfo", { sessionId, ...params }), + connection.sendRequest("session.metadata.contextInfo", { ...params, sessionId }), /** * Returns the experimental per-source attribution breakdown of the session's current context window as a flat list of entries (skills, subagents, MCP servers, built-in tools, plugin rollups, system/tool-definition costs, with nesting via parentId), plus the successful compaction count. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. Returns null until the session has initialized its system prompt and tool metadata. * @@ -26600,7 +26600,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The heaviest individual messages in the session's context window, most-expensive first. */ getContextHeaviestMessages: async (params: MetadataContextHeaviestMessagesRequest): Promise => - connection.sendRequest("session.metadata.getContextHeaviestMessages", { sessionId, ...params }), + connection.sendRequest("session.metadata.getContextHeaviestMessages", { ...params, sessionId }), /** * Records a working-directory/git context change and emits a `session.context_changed` event. For a local session, a report whose `cwd` diverges from the session's current working directory is ignored (the call still succeeds but records nothing and emits no event): a local session's working directory is authoritative and is moved via `metadata.setWorkingDirectory` (or an SDK `session.resume` that supplies a `workingDirectory`), not by this method. * @@ -26609,7 +26609,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Notify the session that its working directory context has changed. Emits a `session.context_changed` event so consumers (telemetry, OTel tracker, ACP, the timeline UI) can react. Use this when the host has detected a cwd/branch/repo change outside the session's normal lifecycle (e.g., after a shell command in interactive mode). For a local session, a report whose `cwd` diverges from the session's current working directory is ignored (the call still succeeds but records nothing and emits no event); move a local session's working directory via `metadata.setWorkingDirectory` instead. */ recordContextChange: async (params: MetadataRecordContextChangeRequest): Promise => - connection.sendRequest("session.metadata.recordContextChange", { sessionId, ...params }), + connection.sendRequest("session.metadata.recordContextChange", { ...params, sessionId }), /** * Updates the session's working directory. For local sessions the target is validated first (an absolute path that exists on disk) and the permission primary directory is re-based; a rejected validation fails the call before any session state changes. * @@ -26618,7 +26618,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. */ setWorkingDirectory: async (params: MetadataSetWorkingDirectoryRequest): Promise => - connection.sendRequest("session.metadata.setWorkingDirectory", { sessionId, ...params }), + connection.sendRequest("session.metadata.setWorkingDirectory", { ...params, sessionId }), /** * Re-tokenizes the session's existing messages against a model and returns aggregate token totals. * @@ -26627,7 +26627,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Re-tokenize the session's existing messages against `modelId` and return the token totals. Useful for hosts that want an initial estimate of context usage on session resume, before the next agent turn fires `session.context_info_changed` events. Returns zeros for an empty session. */ recomputeContextTokens: async (params: MetadataRecomputeContextTokensRequest): Promise => - connection.sendRequest("session.metadata.recomputeContextTokens", { sessionId, ...params }), + connection.sendRequest("session.metadata.recomputeContextTokens", { ...params, sessionId }), }, /** @experimental */ contentExclusion: { @@ -26639,7 +26639,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. */ checkPaths: async (params: ContentExclusionCheckPathsRequest): Promise => - connection.sendRequest("session.contentExclusion.checkPaths", { sessionId, ...params }), + connection.sendRequest("session.contentExclusion.checkPaths", { ...params, sessionId }), }, /** @experimental */ shell: { @@ -26651,7 +26651,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Identifier of the spawned process, used to correlate streamed output and exit notifications. */ exec: async (params: ShellExecRequest): Promise => - connection.sendRequest("session.shell.exec", { sessionId, ...params }), + connection.sendRequest("session.shell.exec", { ...params, sessionId }), /** * Sends a signal to a shell process previously started via "shell.exec". The signal targets the command's whole process group (POSIX) or job object (Windows), so descendants still in that group are signalled too, not just the direct child. On POSIX a descendant that moved itself into a new session or process group (for example via "setsid") is no longer in the signalled group and survives. * @@ -26660,7 +26660,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the signal was delivered; false if the process was unknown or already exited. */ kill: async (params: ShellKillRequest): Promise => - connection.sendRequest("session.shell.kill", { sessionId, ...params }), + connection.sendRequest("session.shell.kill", { ...params, sessionId }), /** * Executes a user-requested shell command through the session runtime. * @@ -26669,7 +26669,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of a user-requested shell command. */ executeUserRequested: async (params: ShellExecuteUserRequestedRequest): Promise => - connection.sendRequest("session.shell.executeUserRequested", { sessionId, ...params }), + connection.sendRequest("session.shell.executeUserRequested", { ...params, sessionId }), /** * Cancels a user-requested shell command by request ID. * @@ -26678,7 +26678,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Cancellation result for a user-requested shell command. */ cancelUserRequested: async (params: ShellCancelUserRequestedRequest): Promise => - connection.sendRequest("session.shell.cancelUserRequested", { sessionId, ...params }), + connection.sendRequest("session.shell.cancelUserRequested", { ...params, sessionId }), }, /** @experimental */ history: { @@ -26690,7 +26690,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. */ compact: async (params?: SessionHistoryCompactRequest): Promise => - connection.sendRequest("session.history.compact", { sessionId, ...params }), + connection.sendRequest("session.history.compact", { ...params, sessionId }), /** * Truncates persisted session history to a specific event. * @@ -26699,7 +26699,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Number of events that were removed by the truncation. */ truncate: async (params: HistoryTruncateRequest): Promise => - connection.sendRequest("session.history.truncate", { sessionId, ...params }), + connection.sendRequest("session.history.truncate", { ...params, sessionId }), /** * Lists the user turns that the session can rewind to. Never rejects for a busy session: rewind reads need the session's file-change captures to be settled, so a session that still holds active work answers with `unavailableReason: "session-busy"` and no points, which the caller can retry. * @@ -26715,7 +26715,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Files and aggregate changes for a prospective rewind. */ previewRewind: async (params: HistoryPreviewRewindRequest): Promise => - connection.sendRequest("session.history.previewRewind", { sessionId, ...params }), + connection.sendRequest("session.history.previewRewind", { ...params, sessionId }), /** * Rewinds the session conversation, optionally restoring files changed by the discarded turns. Not crash-atomic: file restore and conversation truncation are separate stores, applied in that order, so a process crash between them can leave the workspace rewound while the conversation still contains the discarded turns. There is no recovery journal; re-running the same rewind is the recovery path for a crash before truncation lands, since file restore is idempotent (already-restored files are reported as skipped) and truncation is re-derived from the still-retained boundary event. After truncation lands that boundary no longer exists, so the same request is rejected; the only stage that can still be outstanding is snapshot pruning, whose failure leaves orphan snapshots the capture store tolerates. The reverse inconsistency cannot occur, because truncation is never applied before file restore succeeds. * @@ -26724,7 +26724,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Structured outcome of a rewind request. */ rewind: async (params: HistoryRewindRequest): Promise => - connection.sendRequest("session.history.rewind", { sessionId, ...params }), + connection.sendRequest("session.history.rewind", { ...params, sessionId }), /** * Cancels any in-progress background compaction on a local session. * @@ -26754,7 +26754,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns What a successful clear removed. A clear that could not be applied rejects instead of reporting a count. */ clearContext: async (params: HistoryClearContextRequest): Promise => - connection.sendRequest("session.history.clearContext", { sessionId, ...params }), + connection.sendRequest("session.history.clearContext", { ...params, sessionId }), }, /** @experimental */ queue: { @@ -26773,7 +26773,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of moving a queued item. */ moveItem: async (params: QueueMoveItemRequest): Promise => - connection.sendRequest("session.queue.moveItem", { sessionId, ...params }), + connection.sendRequest("session.queue.moveItem", { ...params, sessionId }), /** * Inserts a new queued message at a public visible position. * @@ -26782,7 +26782,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of inserting a queued message. */ insertAt: async (params: QueueInsertAtRequest): Promise => - connection.sendRequest("session.queue.insertAt", { sessionId, ...params }), + connection.sendRequest("session.queue.insertAt", { ...params, sessionId }), /** * Removes an addressable queued item by its stable id. * @@ -26791,7 +26791,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of removing a queued item. */ removeAt: async (params: QueueRemoveAtRequest): Promise => - connection.sendRequest("session.queue.removeAt", { sessionId, ...params }), + connection.sendRequest("session.queue.removeAt", { ...params, sessionId }), /** * Updates the text of an addressable single-message queue item. * @@ -26800,7 +26800,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of editing a queued message. */ updateText: async (params: QueueUpdateTextRequest): Promise => - connection.sendRequest("session.queue.updateText", { sessionId, ...params }), + connection.sendRequest("session.queue.updateText", { ...params, sessionId }), /** * Duplicates an addressable queued item immediately after its source. * @@ -26809,14 +26809,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of duplicating a queued item. */ duplicateAt: async (params: QueueDuplicateAtRequest): Promise => - connection.sendRequest("session.queue.duplicateAt", { sessionId, ...params }), + connection.sendRequest("session.queue.duplicateAt", { ...params, sessionId }), /** * Acquires or releases the queued-lane drain pause. * * @param params Parameters for acquiring or releasing the queued-lane drain pause. Acquisition is exclusive and non-idempotent: `paused: true` against an already-paused session fails with `queue_already_paused`. The pause is never released automatically — it is not tied to the caller's lifetime, so a client that exits without sending `paused: false` leaves the lane frozen. Release is unowned: `paused: false` clears the pause for any caller, including one that never acquired it. */ setDrainPaused: async (params: QueueSetDrainPausedRequest): Promise => - connection.sendRequest("session.queue.setDrainPaused", { sessionId, ...params }), + connection.sendRequest("session.queue.setDrainPaused", { ...params, sessionId }), /** * Moves an addressable queued message into the live turn's steering lane. * @@ -26825,7 +26825,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of trying to steer a queued message into a live turn. */ sendNow: async (params: QueueSendNowRequest): Promise => - connection.sendRequest("session.queue.sendNow", { sessionId, ...params }), + connection.sendRequest("session.queue.sendNow", { ...params, sessionId }), /** * Removes the most recently queued user-facing item (LIFO). * @@ -26849,7 +26849,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Batch of session events returned by a read, with cursor and continuation metadata. */ read: async (params: EventLogReadRequest): Promise => - connection.sendRequest("session.eventLog.read", { sessionId, ...params }), + connection.sendRequest("session.eventLog.read", { ...params, sessionId }), /** * Returns a snapshot of the current tail cursor without consuming events. * @@ -26865,7 +26865,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Opaque handle representing an event-type interest registration. */ registerInterest: async (params: RegisterEventInterestParams): Promise => - connection.sendRequest("session.eventLog.registerInterest", { sessionId, ...params }), + connection.sendRequest("session.eventLog.registerInterest", { ...params, sessionId }), /** * Releases a consumer's previously-registered interest in an event type. * @@ -26874,7 +26874,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ releaseInterest: async (params: ReleaseEventInterestParams): Promise => - connection.sendRequest("session.eventLog.releaseInterest", { sessionId, ...params }), + connection.sendRequest("session.eventLog.releaseInterest", { ...params, sessionId }), }, /** @experimental */ usage: { @@ -26896,7 +26896,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Prediction result. Available results include prediction details; unavailable results include an explicit reason. */ predict: async (params?: SessionLimitPredictionPredictRequest): Promise => - connection.sendRequest("session.limitPrediction.predict", { sessionId, ...params }), + connection.sendRequest("session.limitPrediction.predict", { ...params, sessionId }), }, /** @experimental */ remote: { @@ -26908,7 +26908,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns GitHub URL for the session and a flag indicating whether remote steering is enabled. */ enable: async (params: RemoteEnableRequest): Promise => - connection.sendRequest("session.remote.enable", { sessionId, ...params }), + connection.sendRequest("session.remote.enable", { ...params, sessionId }), /** * Disables remote session export and steering. */ @@ -26922,7 +26922,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Persist a steerability change as a `session.remote_steerable_changed` event. Used by the host (CLI / SDK consumer) when it has just finished enabling or disabling steering on a remote exporter that the runtime does not directly own. */ notifySteerableChanged: async (params: RemoteNotifySteerableChangedRequest): Promise => - connection.sendRequest("session.remote.notifySteerableChanged", { sessionId, ...params }), + connection.sendRequest("session.remote.notifySteerableChanged", { ...params, sessionId }), }, /** @experimental */ visibility: { @@ -26941,7 +26941,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Effective sharing status and shareable GitHub URL after updating session visibility. */ set: async (params: VisibilitySetRequest): Promise => - connection.sendRequest("session.visibility.set", { sessionId, ...params }), + connection.sendRequest("session.visibility.set", { ...params, sessionId }), }, /** @experimental */ schedule: { @@ -26960,7 +26960,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Remove a scheduled prompt by id. The result entry is omitted if the id was unknown. */ stop: async (params: ScheduleStopRequest): Promise => - connection.sendRequest("session.schedule.stop", { sessionId, ...params }), + connection.sendRequest("session.schedule.stop", { ...params, sessionId }), }, }; } @@ -26980,7 +26980,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @experimental */ sendSystemNotification: async (params: SendSystemNotificationRequest): Promise => - connection.sendRequest("session.sendSystemNotification", { sessionId, ...params }), + connection.sendRequest("session.sendSystemNotification", { ...params, sessionId }), /** @experimental */ gitHubAuth: { /** @@ -27012,14 +27012,14 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Authentication credentials accepted only at native protocol ingress. Runtime outputs use credential-free `AuthIdentity` metadata. */ login: async (params: SessionAuthLoginRequest): Promise => - connection.sendRequest("session.gitHubAuth.login", { sessionId, ...params }), + connection.sendRequest("session.gitHubAuth.login", { ...params, sessionId }), /** * Switches the session to another available authentication. * * @param params Parameters for switching the session's active authentication. */ switchToAuth: async (params: SessionAuthSwitchRequest): Promise => - connection.sendRequest("session.gitHubAuth.switchToAuth", { sessionId, ...params }), + connection.sendRequest("session.gitHubAuth.switchToAuth", { ...params, sessionId }), /** * Logs out the session's current GitHub authentication. * @@ -27035,7 +27035,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Whether the requested authentication was logged out. */ logoutUser: async (params: SessionAuthLogoutUserRequest): Promise => - connection.sendRequest("session.gitHubAuth.logoutUser", { sessionId, ...params }), + connection.sendRequest("session.gitHubAuth.logoutUser", { ...params, sessionId }), /** * Gets validation errors from the most recent authentication attempt. * @@ -27054,14 +27054,14 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @param params Internal canvas provider registration parameters. */ register: async (params: CanvasProviderRegisterRequest): Promise => - connection.sendRequest("session.canvas.provider.register", { sessionId, ...params }), + connection.sendRequest("session.canvas.provider.register", { ...params, sessionId }), /** * Unregisters an internal canvas provider connection. * * @param params Internal canvas provider unregistration parameters. */ unregister: async (params: CanvasProviderUnregisterRequest): Promise => - connection.sendRequest("session.canvas.provider.unregister", { sessionId, ...params }), + connection.sendRequest("session.canvas.provider.unregister", { ...params, sessionId }), }, }, /** @experimental */ @@ -27074,7 +27074,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Complete current or terminal factory run envelope. */ runFromTool: async (params: FactoryToolRunRequest): Promise => - connection.sendRequest("session.factory.runFromTool", { sessionId, ...params }), + connection.sendRequest("session.factory.runFromTool", { ...params, sessionId }), /** * Internal tool-originated factory resume. * @@ -27083,14 +27083,14 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Resolved persisted factory identity and resumed run envelope. */ resumeFromTool: async (params: FactoryToolResumeRequest): Promise => - connection.sendRequest("session.factory.resumeFromTool", { sessionId, ...params }), + connection.sendRequest("session.factory.resumeFromTool", { ...params, sessionId }), /** * Atomically pauses an owned factory attempt at a durable checkpoint. * * @param params Parameters for an owned durable pause checkpoint. */ pauseAtCheckpoint: async (params: FactoryPauseCheckpointRequest): Promise => - connection.sendRequest("session.factory.pauseAtCheckpoint", { sessionId, ...params }), + connection.sendRequest("session.factory.pauseAtCheckpoint", { ...params, sessionId }), }, /** @experimental */ model: { @@ -27102,7 +27102,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns The model identifier active on the session after the switch. */ applyStartupOverlay: async (params: ModelApplyStartupOverlayRequest): Promise => - connection.sendRequest("session.model.applyStartupOverlay", { sessionId, ...params }), + connection.sendRequest("session.model.applyStartupOverlay", { ...params, sessionId }), }, /** @experimental */ mcp: { @@ -27114,7 +27114,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns MCP server startup filtering result. */ reloadWithConfig: async (params: McpReloadWithConfigRequest): Promise => - connection.sendRequest("session.mcp.reloadWithConfig", { sessionId, ...params }), + connection.sendRequest("session.mcp.reloadWithConfig", { ...params, sessionId }), /** * Configures the built-in GitHub MCP server for the session's current auth context. * @@ -27123,21 +27123,21 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of configuring GitHub MCP. */ configureGitHub: async (params: McpConfigureGitHubRequest): Promise => - connection.sendRequest("session.mcp.configureGitHub", { sessionId, ...params }), + connection.sendRequest("session.mcp.configureGitHub", { ...params, sessionId }), /** * Registers a pre-connected external MCP client (e.g. IDE) on the session's host. The caller retains lifecycle ownership of the client and transport. Marked internal because the `client` and `transport` arguments are in-process MCP SDK instances that cannot be serialized across the JSON-RPC boundary; once the CLI moves on top of the SDK, external clients will be expressed as transport configs the runtime can construct itself. * * @param params Registration parameters for an external MCP client. */ registerExternalClient: async (params: McpRegisterExternalClientRequest): Promise => - connection.sendRequest("session.mcp.registerExternalClient", { sessionId, ...params }), + connection.sendRequest("session.mcp.registerExternalClient", { ...params, sessionId }), /** * Unregisters a previously registered external MCP client by server name. Marked internal as the paired companion of `registerExternalClient`: only in-process callers that registered a client this way can meaningfully unregister it. Disappears alongside `registerExternalClient`: once external clients are described to the runtime as config rather than handed in as instances, lifecycle (including deregistration) is owned entirely by the runtime. * * @param params Server name identifying the external client to remove. */ unregisterExternalClient: async (params: McpUnregisterExternalClientRequest): Promise => - connection.sendRequest("session.mcp.unregisterExternalClient", { sessionId, ...params }), + connection.sendRequest("session.mcp.unregisterExternalClient", { ...params, sessionId }), }, /** @experimental */ commands: { @@ -27149,7 +27149,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Whether finalizing the invocation effect succeeded, and the failure reason when it did not. */ finalizeInvocationEffect: async (params: CommandsFinalizeInvocationEffectRequest): Promise => - connection.sendRequest("session.commands.finalizeInvocationEffect", { sessionId, ...params }), + connection.sendRequest("session.commands.finalizeInvocationEffect", { ...params, sessionId }), }, /** @experimental */ settings: { @@ -27168,7 +27168,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of evaluating a Rust-owned settings predicate. */ evaluatePredicate: async (params: SessionSettingsEvaluatePredicateRequest): Promise => - connection.sendRequest("session.settings.evaluatePredicate", { sessionId, ...params }), + connection.sendRequest("session.settings.evaluatePredicate", { ...params, sessionId }), }, /** @experimental */ queue: { @@ -27194,7 +27194,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Whether a deferred-idle drain should run. */ beginDeferredIdleDrain: async (params: QueueBeginDeferredIdleDrainRequest): Promise => - connection.sendRequest("session.queue.beginDeferredIdleDrain", { sessionId, ...params }), + connection.sendRequest("session.queue.beginDeferredIdleDrain", { ...params, sessionId }), /** * Finishes a native deferred-idle drain and reports whether to drain queue work or emit idle. * @@ -27203,14 +27203,14 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Action selected by the native deferred-idle drain. */ finishDeferredIdleDrain: async (params: QueueFinishDeferredIdleDrainRequest): Promise => - connection.sendRequest("session.queue.finishDeferredIdleDrain", { sessionId, ...params }), + connection.sendRequest("session.queue.finishDeferredIdleDrain", { ...params, sessionId }), /** * Marks session.idle as deferred by native background work state. * * @param params Inputs for marking session.idle deferred in native state. */ deferSessionIdle: async (params: QueueDeferSessionIdleRequest): Promise => - connection.sendRequest("session.queue.deferSessionIdle", { sessionId, ...params }), + connection.sendRequest("session.queue.deferSessionIdle", { ...params, sessionId }), /** * Consumes queued native system notifications matching an internal filter. * @@ -27219,7 +27219,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Indicates whether a user-facing pending item was removed. */ consumeSystemNotifications: async (params: QueueConsumeSystemNotificationsRequest): Promise => - connection.sendRequest("session.queue.consumeSystemNotifications", { sessionId, ...params }), + connection.sendRequest("session.queue.consumeSystemNotifications", { ...params, sessionId }), /** * Enqueues the internal resume-pending wake item when orphan handling needs a follow-up turn. * @@ -27255,7 +27255,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of registering or re-arming a scheduled prompt. */ add: async (params: ScheduleAddRequest): Promise => - connection.sendRequest("session.schedule.add", { sessionId, ...params }), + connection.sendRequest("session.schedule.add", { ...params, sessionId }), /** * Registers a recurring cron scheduled prompt. * @@ -27264,7 +27264,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of registering or re-arming a scheduled prompt. */ addCron: async (params: ScheduleAddCronRequest): Promise => - connection.sendRequest("session.schedule.addCron", { sessionId, ...params }), + connection.sendRequest("session.schedule.addCron", { ...params, sessionId }), /** * Registers an absolute-time scheduled prompt. * @@ -27273,7 +27273,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of registering or re-arming a scheduled prompt. */ addAt: async (params: ScheduleAddAtRequest): Promise => - connection.sendRequest("session.schedule.addAt", { sessionId, ...params }), + connection.sendRequest("session.schedule.addAt", { ...params, sessionId }), /** * Registers a self-paced scheduled prompt. * @@ -27282,7 +27282,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of registering or re-arming a scheduled prompt. */ addSelfPaced: async (params: ScheduleAddSelfPacedRequest): Promise => - connection.sendRequest("session.schedule.addSelfPaced", { sessionId, ...params }), + connection.sendRequest("session.schedule.addSelfPaced", { ...params, sessionId }), /** * Re-arms an active self-paced scheduled prompt. * @@ -27291,7 +27291,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of registering or re-arming a scheduled prompt. */ rearmSelfPaced: async (params: ScheduleRearmSelfPacedRequest): Promise => - connection.sendRequest("session.schedule.rearmSelfPaced", { sessionId, ...params }), + connection.sendRequest("session.schedule.rearmSelfPaced", { ...params, sessionId }), }, }; } diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 6251df4fc7..a8fba33a08 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -11,7 +11,18 @@ export { CopilotClient } from "./client.js"; export { DisableBypassPermissionsModes, RuntimeConnection } from "./types.js"; export { BuiltInTools, ToolSet } from "./toolSet.js"; -export { CopilotSession, type AssistantMessageEvent } from "./session.js"; +export { + CopilotSession, + SendSessionMessageError, + type AssistantMessageEvent, + type ListMessageableSessionsRequest, + type ListMessageableSessionsResult, + type MessageableSession, + type SendSessionMessageRequest, + type SendSessionMessageResult, + type SendSessionMessageErrorCode, + type SessionMessageDelivery, +} from "./session.js"; export { defineFactory, FactoryResumeError, isFactoryRunTerminal } from "./factory.js"; export { Canvas, @@ -52,6 +63,7 @@ export { // shadow the names arriving via `export type *`, so the hand-authored public API // surface for those six identifiers is preserved unchanged. export type * from "./generated/session-events.js"; +export type { SendMode } from "./generated/rpc.js"; export type { AskUserVariant, CommandContext, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 4c2be14299..83f818a6d8 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -19,6 +19,7 @@ import type { FactoryLogLine, FactoryRunResult as WireFactoryRunResult, ModelSwitchAutoTierResult, + SendMode, } from "./generated/rpc.js"; import { type Canvas, CanvasError } from "./canvas.js"; import type { OpenCanvasInstance } from "./generated/rpc.js"; @@ -384,6 +385,123 @@ function isFactoryFatalError(error: unknown): boolean { /** Assistant message event - the final response from the assistant. */ export type AssistantMessageEvent = Extract; +/** Optional exact-name query for active local messageable sessions. */ +export interface ListMessageableSessionsRequest { + /** Optional exact session name query. Matching semantics are owned by the local host. */ + name?: string; +} + +/** Sanitized active local session available for exact-ID messaging selection. */ +export interface MessageableSession { + /** Stable session ID to provide to {@link CopilotSession.sendSessionMessage}. */ + sessionId: string; + /** Current session name when available. */ + name?: string; + /** Current session summary when available. */ + summary?: string; +} + +/** Sanitized active local sessions available for exact-ID messaging selection. */ +export interface ListMessageableSessionsResult { + /** Messageable sessions in deterministic session-ID order. */ + sessions: MessageableSession[]; +} + +/** Actual recipient delivery class for an admitted cross-session message. */ +export type SessionMessageDelivery = "idle" | "steering" | "queued"; + +/** Parameters for one authenticated exact-target cross-session message. */ +export interface SendSessionMessageRequest { + /** Exact active local recipient session ID. */ + targetSessionId: string; + /** Natural-language message content. */ + content: string; + /** Requested delivery mode. The host applies its existing default when omitted. */ + delivery?: SendMode; +} + +/** Recipient admission result for an authenticated cross-session message. */ +export interface SendSessionMessageResult { + /** Unique identifier assigned to the admitted message. */ + messageId: string; + /** Actual recipient delivery class at admission. */ + delivery: SessionMessageDelivery; + /** Sanitized recipient display name for presentation only. */ + targetDisplayName?: string; +} + +/** Stable public outcomes for a failed cross-session message send. */ +export type SendSessionMessageErrorCode = "refused" | "not-delivered" | "ambiguous"; + +/** + * Error returned when the runtime reaches a recognized terminal cross-session + * message outcome. + * + * @experimental + */ +export class SendSessionMessageError extends Error { + constructor( + public readonly code: SendSessionMessageErrorCode, + message: string, + public readonly messageId?: string + ) { + super(message); + this.name = "SendSessionMessageError"; + } +} + +function parseSendSessionMessageErrorData( + data: unknown +): { code: SendSessionMessageErrorCode; messageId?: string } | undefined { + if (typeof data !== "object" || data === null) { + return undefined; + } + + const envelope = data as { kind?: unknown; code?: unknown; messageId?: unknown }; + if ( + typeof envelope.code !== "string" || + (envelope.messageId !== undefined && typeof envelope.messageId !== "string") + ) { + return undefined; + } + + let code: SendSessionMessageErrorCode; + switch (envelope.kind) { + case "session_message_refused": + if ( + ![ + "target-not-active", + "target-generation-changed", + "source-not-active", + "self-send", + "request-invalid", + "recipient-refused", + "transport-unavailable", + ].includes(envelope.code) + ) { + return undefined; + } + code = "refused"; + break; + case "session_message_not_delivered": + if (envelope.code !== "not-delivered") { + return undefined; + } + code = "not-delivered"; + break; + case "session_message_ambiguous": + if (envelope.code !== "ambiguous") { + return undefined; + } + code = "ambiguous"; + break; + default: + return undefined; + } + + return envelope.messageId === undefined ? { code } : { code, messageId: envelope.messageId }; +} + const TOOL_SEARCH_TOOL_NAME = "tool_search_tool"; /** @@ -730,6 +848,52 @@ export class CopilotSession { return (response as { messageId: string }).messageId; } + /** + * Lists active local sessions that this bound session can select by exact + * ID for cross-session messaging. The result grants no delivery authority; + * call {@link sendSessionMessage} with a selected `sessionId`. + * + * @experimental + */ + async listMessageableSessions( + params: ListMessageableSessionsRequest = {} + ): Promise { + return this.connection.sendRequest("session.listMessageableSessions", { + ...params, + sessionId: this.sessionId, + }); + } + + /** + * Sends one authenticated non-user message from this bound session to an + * exact active local session. + * + * Success reports recipient admission, not completion of delegated work. + * An ambiguous error means delivery may have started and is never retried. + * + * @experimental + */ + async sendSessionMessage(params: SendSessionMessageRequest): Promise { + try { + return await this.connection.sendRequest("session.sendSessionMessage", { + ...params, + sessionId: this.sessionId, + }); + } catch (error) { + if (error instanceof ResponseError) { + const translated = parseSendSessionMessageErrorData(error.data); + if (translated) { + throw new SendSessionMessageError( + translated.code, + error.message, + translated.messageId + ); + } + } + throw error; + } + } + /** * Sends a message to this session and waits until the session becomes idle. * diff --git a/nodejs/test/session-list-messageable-sessions.test.ts b/nodejs/test/session-list-messageable-sessions.test.ts new file mode 100644 index 0000000000..4c818ea75a --- /dev/null +++ b/nodejs/test/session-list-messageable-sessions.test.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it, vi } from "vitest"; +import { + CopilotSession, + type ListMessageableSessionsRequest, + type ListMessageableSessionsResult, + type MessageableSession, +} from "../src/index.js"; + +type AssertEqual = + (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 ? true : false; + +type RequestMatchesPublicContract = AssertEqual< + ListMessageableSessionsRequest, + { + name?: string; + } +>; +const requestMatchesPublicContract: RequestMatchesPublicContract = true; + +type CandidateMatchesPublicContract = AssertEqual< + MessageableSession, + { + sessionId: string; + name?: string; + summary?: string; + } +>; +const candidateMatchesPublicContract: CandidateMatchesPublicContract = true; + +type ResultMatchesPublicContract = AssertEqual< + ListMessageableSessionsResult, + { + sessions: MessageableSession[]; + } +>; +const resultMatchesPublicContract: ResultMatchesPublicContract = true; + +const assertRejectedListInputs = (session: CopilotSession): void => { + // @ts-expect-error Source identity is derived from the bound session. + void session.listMessageableSessions({ sourceSessionId: "forged" }); + // @ts-expect-error Discovery never accepts a delivery target. + void session.listMessageableSessions({ targetSessionId: "target-session" }); +}; +void assertRejectedListInputs; + +describe("CopilotSession.listMessageableSessions", () => { + it("lists all candidates when no name is supplied", async () => { + const result = { + sessions: [ + { sessionId: "session-a", name: "Research" }, + { sessionId: "session-b", summary: "Research" }, + ], + }; + const sendRequest = vi.fn(async () => result); + const session = new CopilotSession("source-session", { sendRequest } as never); + + await expect(session.listMessageableSessions()).resolves.toEqual(result); + expect(sendRequest).toHaveBeenCalledExactlyOnceWith("session.listMessageableSessions", { + sessionId: "source-session", + }); + }); + + it("forwards the exact-name query without rewriting it", async () => { + const result = { sessions: [{ sessionId: "session-a", name: "Research" }] }; + const sendRequest = vi.fn(async () => result); + const session = new CopilotSession("source-session", { sendRequest } as never); + + await expect(session.listMessageableSessions({ name: " ReSeArCh " })).resolves.toEqual( + result + ); + expect(sendRequest).toHaveBeenCalledExactlyOnceWith("session.listMessageableSessions", { + sessionId: "source-session", + name: " ReSeArCh ", + }); + }); + + it("does not allow untyped input to override the bound source session", async () => { + const result = { sessions: [] }; + const sendRequest = vi.fn(async () => result); + const session = new CopilotSession("source-session", { sendRequest } as never); + const params = JSON.parse( + '{"sessionId":"forged-session","name":"Research"}' + ) as ListMessageableSessionsRequest; + + await expect(session.listMessageableSessions(params)).resolves.toEqual(result); + expect(sendRequest).toHaveBeenCalledExactlyOnceWith("session.listMessageableSessions", { + sessionId: "source-session", + name: "Research", + }); + }); +}); + +void requestMatchesPublicContract; +void candidateMatchesPublicContract; +void resultMatchesPublicContract; diff --git a/nodejs/test/session-send-session-message.test.ts b/nodejs/test/session-send-session-message.test.ts new file mode 100644 index 0000000000..9785990ab6 --- /dev/null +++ b/nodejs/test/session-send-session-message.test.ts @@ -0,0 +1,242 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { readFileSync } from "node:fs"; +import { describe, expect, it, vi } from "vitest"; +import { ResponseError } from "vscode-jsonrpc/node.js"; +import { + CopilotSession, + SendSessionMessageError, + type SendMode, + type SendSessionMessageErrorCode, + type SendSessionMessageRequest, + type SendSessionMessageResult, + type SessionMessageDelivery, +} from "../src/index.js"; + +type AssertEqual = + (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 ? true : false; + +type RequestMatchesPublicContract = AssertEqual< + SendSessionMessageRequest, + { + targetSessionId: string; + content: string; + delivery?: "immediate" | "enqueue"; + } +>; +const requestMatchesPublicContract: RequestMatchesPublicContract = true; + +type ResultMatchesPublicContract = AssertEqual< + SendSessionMessageResult, + { + messageId: string; + delivery: "idle" | "steering" | "queued"; + targetDisplayName?: string; + } +>; +const resultMatchesPublicContract: ResultMatchesPublicContract = true; + +type RequestDeliveryMatchesPublicContract = AssertEqual; +const requestDeliveryMatchesPublicContract: RequestDeliveryMatchesPublicContract = true; + +type ResultDeliveryMatchesPublicContract = AssertEqual< + SessionMessageDelivery, + "idle" | "steering" | "queued" +>; +const resultDeliveryMatchesPublicContract: ResultDeliveryMatchesPublicContract = true; + +type ErrorCodeMatchesPublicContract = AssertEqual< + SendSessionMessageErrorCode, + "refused" | "not-delivered" | "ambiguous" +>; +const errorCodeMatchesPublicContract: ErrorCodeMatchesPublicContract = true; + +const assertRejectedSendInputs = (session: CopilotSession): void => { + const base = { targetSessionId: "target-session", content: "Please inspect this." }; + + // @ts-expect-error Source identity is derived from the bound session. + void session.sendSessionMessage({ ...base, source: "source-session" }); + // @ts-expect-error Reply identity is derived by the recipient runtime. + void session.sendSessionMessage({ ...base, reply: "source-session" }); + // @ts-expect-error Provenance is host-derived and cannot be caller supplied. + void session.sendSessionMessage({ ...base, provenance: "authenticated" }); + // @ts-expect-error Presentation metadata is not part of the public request. + void session.sendSessionMessage({ ...base, presentation: { label: "sender" } }); + // @ts-expect-error Continuation authority is derived by the recipient runtime. + void session.sendSessionMessage({ ...base, continuation: ["source-session"] }); + // @ts-expect-error Message IDs are assigned by the host. + void session.sendSessionMessage({ ...base, messageId: "caller-selected" }); +}; +void assertRejectedSendInputs; + +describe("CopilotSession.sendSessionMessage", () => { + it("omits delivery when the caller does not provide it and returns the admission result", async () => { + const result = { messageId: "message-1", delivery: "idle" as const }; + const sendRequest = vi.fn(async () => result); + const session = new CopilotSession("source-session", { sendRequest } as never); + + await expect( + session.sendSessionMessage({ + targetSessionId: "target-session", + content: "Please inspect this.", + }) + ).resolves.toEqual(result); + + expect(sendRequest).toHaveBeenCalledExactlyOnceWith("session.sendSessionMessage", { + sessionId: "source-session", + targetSessionId: "target-session", + content: "Please inspect this.", + }); + }); + + it.each(["immediate", "enqueue"] as const)( + "forwards explicit %s delivery without rewriting it", + async (delivery) => { + const sendRequest = vi.fn(async () => ({ + messageId: `message-${delivery}`, + delivery: delivery === "immediate" ? ("steering" as const) : ("queued" as const), + })); + const session = new CopilotSession("source-session", { sendRequest } as never); + + await session.sendSessionMessage({ + targetSessionId: "target-session", + content: "Please inspect this.", + delivery, + }); + + expect(sendRequest).toHaveBeenCalledExactlyOnceWith("session.sendSessionMessage", { + sessionId: "source-session", + targetSessionId: "target-session", + content: "Please inspect this.", + delivery, + }); + } + ); + + it("does not allow untyped input to override the bound source session", async () => { + const result = { messageId: "message-1", delivery: "idle" as const }; + const sendRequest = vi.fn(async () => result); + const session = new CopilotSession("source-session", { sendRequest } as never); + const params = JSON.parse( + '{"sessionId":"forged-session","targetSessionId":"target-session","content":"Please inspect this."}' + ) as SendSessionMessageRequest; + + await expect(session.sendSessionMessage(params)).resolves.toEqual(result); + expect(sendRequest).toHaveBeenCalledExactlyOnceWith("session.sendSessionMessage", { + sessionId: "source-session", + targetSessionId: "target-session", + content: "Please inspect this.", + }); + }); + + it.each([ + { + kind: "session_message_refused", + publicCode: "refused", + runtimeCode: "target-not-active", + messageId: "message-refused", + }, + { + kind: "session_message_refused", + publicCode: "refused", + runtimeCode: "transport-unavailable", + messageId: "message-unsupported-platform", + }, + { + kind: "session_message_not_delivered", + publicCode: "not-delivered", + runtimeCode: "not-delivered", + messageId: "message-not-delivered", + }, + { + kind: "session_message_ambiguous", + publicCode: "ambiguous", + runtimeCode: "ambiguous", + messageId: "message-ambiguous", + }, + ] as const)( + "translates $kind to the stable $publicCode outcome", + async ({ kind, publicCode, runtimeCode, messageId }) => { + const responseError = new ResponseError(-32603, `send failed: ${publicCode}`, { + kind, + code: runtimeCode, + messageId, + }); + const sendRequest = vi.fn(async () => { + throw responseError; + }); + const session = new CopilotSession("source-session", { sendRequest } as never); + + const error = await session + .sendSessionMessage({ + targetSessionId: "target-session", + content: "Please inspect this.", + }) + .catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(SendSessionMessageError); + expect(error).toMatchObject({ + name: "SendSessionMessageError", + code: publicCode, + message: responseError.message, + messageId, + }); + expect(sendRequest).toHaveBeenCalledTimes(1); + } + ); + + it.each([ + ["non-object data", "not-an-envelope"], + ["missing kind", { code: "target-not-active" }], + ["missing runtime code", { kind: "session_message_refused" }], + ["unknown kind", { kind: "session_message_unknown", code: "target-not-active" }], + ["unknown refusal code", { kind: "session_message_refused", code: "unknown-refusal" }], + [ + "mismatched outcome and code", + { kind: "session_message_ambiguous", code: "not-delivered" }, + ], + [ + "non-string message ID", + { + kind: "session_message_ambiguous", + code: "ambiguous", + messageId: 42, + }, + ], + ])("leaves %s as the original ResponseError", async (_label, data) => { + const responseError = new ResponseError(-32603, "raw runtime failure", data); + const sendRequest = vi.fn(async () => { + throw responseError; + }); + const session = new CopilotSession("source-session", { sendRequest } as never); + + const error = await session + .sendSessionMessage({ + targetSessionId: "target-session", + content: "Please inspect this.", + }) + .catch((caught: unknown) => caught); + + expect(error).toBe(responseError); + expect(error).toBeInstanceOf(ResponseError); + expect(error).not.toBeInstanceOf(SendSessionMessageError); + }); + + it("keeps generated session wrappers source-bound", () => { + const generatedRpc = readFileSync( + new URL("../src/generated/rpc.ts", import.meta.url), + "utf8" + ); + + expect(generatedRpc).toContain("{ ...params, sessionId }"); + expect(generatedRpc).not.toContain("{ sessionId, ...params }"); + }); +}); + +void requestMatchesPublicContract; +void resultMatchesPublicContract; +void requestDeliveryMatchesPublicContract; +void resultDeliveryMatchesPublicContract; +void errorCodeMatchesPublicContract; diff --git a/nodejs/tsconfig.test.json b/nodejs/tsconfig.test.json index 03d23317f7..33309469d2 100644 --- a/nodejs/tsconfig.test.json +++ b/nodejs/tsconfig.test.json @@ -5,6 +5,12 @@ "emitDeclarationOnly": false, "types": ["node"] }, - "include": ["src/**/*", "test/session-event-types.test.ts", "test/message-source.test.ts"], + "include": [ + "src/**/*", + "test/session-list-messageable-sessions.test.ts", + "test/session-event-types.test.ts", + "test/session-send-session-message.test.ts", + "test/message-source.test.ts" + ], "exclude": ["node_modules", "dist"] } diff --git a/rust/src/errors.rs b/rust/src/errors.rs index 3bf5becbda..c87cadb278 100644 --- a/rust/src/errors.rs +++ b/rust/src/errors.rs @@ -5,6 +5,8 @@ use std::borrow::{Borrow, Cow}; use std::fmt; use std::time::Duration; +use serde_json::Value; + use crate::types::SessionId; /// Crate-specific [`Result`](std::result::Result). @@ -112,6 +114,28 @@ impl fmt::Display for ProtocolErrorKind { // ── SessionErrorKind ─────────────────────────────────────────── +/// Stable outcome classification for a failed cross-session message send. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum SendSessionMessageErrorCode { + /// The runtime refused the request before delivery could be admitted. + Refused, + /// The runtime established that the message was not delivered. + NotDelivered, + /// Delivery may have started, so the caller must not retry automatically. + Ambiguous, +} + +impl fmt::Display for SendSessionMessageErrorCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Refused => f.write_str("refused"), + Self::NotDelivered => f.write_str("not-delivered"), + Self::Ambiguous => f.write_str("ambiguous"), + } + } +} + /// Session-scoped error kind. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] @@ -153,6 +177,14 @@ pub enum SessionErrorKind { returned: SessionId, }, + /// A cross-session message reached a recognized terminal failure outcome. + SendSessionMessage { + /// Stable outcome classification. + code: SendSessionMessageErrorCode, + /// Runtime-assigned message ID, when admission progressed far enough to assign one. + message_id: Option, + }, + /// The CLI could not detach the session. DetachFailed, } @@ -189,6 +221,9 @@ impl fmt::Display for SessionErrorKind { f, "CLI returned session ID {returned} after SDK registered {requested}" ), + SessionErrorKind::SendSessionMessage { code, .. } => { + write!(f, "cross-session message {code}") + } SessionErrorKind::DetachFailed => write!(f, "failed to detach session"), } } @@ -252,6 +287,7 @@ impl fmt::Display for ErrorKind { /// Errors returned by the SDK. pub struct Error { repr: Repr, + rpc_data: Option, // Only `Some` when `RUST_BACKTRACE` is set; boxed so the `Some` variant // doesn't inflate `Error` beyond `clippy::result_large_err` limits. backtrace: Option>, @@ -268,6 +304,7 @@ impl Error { kind, error: error.into(), }), + rpc_data: None, backtrace: capture_backtrace(), } } @@ -297,10 +334,23 @@ impl Error { { Self { repr: Repr::SimpleMessage(kind, message.into()), + rpc_data: None, + backtrace: capture_backtrace(), + } + } + + pub(crate) fn from_rpc_error(code: i32, message: String, data: Option) -> Self { + Self { + repr: Repr::SimpleMessage(ErrorKind::Rpc { code }, message.into()), + rpc_data: data, backtrace: capture_backtrace(), } } + pub(crate) fn rpc_data(&self) -> Option<&Value> { + self.rpc_data.as_ref() + } + /// Returns `true` if this error indicates the transport is broken — the CLI /// process exited, the connection was lost, or an I/O failure occurred. /// Callers should discard the client and create a fresh one. @@ -361,6 +411,7 @@ impl From for Error { fn from(kind: ErrorKind) -> Self { Self { repr: Repr::Simple(kind), + rpc_data: None, backtrace: capture_backtrace(), } } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index c95ed2087a..24e908406d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -2321,10 +2321,7 @@ impl Client { )) .into()); } - return Err(Error::with_message( - ErrorKind::Rpc { code: err.code }, - err.message, - )); + return Err(Error::from_rpc_error(err.code, err.message, err.data)); } Ok(response.result.unwrap_or(serde_json::Value::Null)) } diff --git a/rust/src/rpc.rs b/rust/src/rpc.rs index 227d2770ef..61afc1a3aa 100644 --- a/rust/src/rpc.rs +++ b/rust/src/rpc.rs @@ -11,6 +11,81 @@ pub use crate::generated::api_types::*; pub use crate::generated::rpc::*; +/// Optional exact-name query for active local messageable sessions. +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListMessageableSessionsRequest { + /// Optional exact session name query. Matching semantics are owned by the local host. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +/// Sanitized active local session available for exact-ID messaging selection. +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageableSession { + /// Stable session ID to provide to `session.sendSessionMessage`. + pub session_id: crate::SessionId, + /// Current session name when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Current session summary when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, +} + +/// Sanitized active local sessions available for exact-ID messaging selection. +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListMessageableSessionsResult { + /// Messageable sessions in deterministic session-ID order. + pub sessions: Vec, +} + +/// Actual recipient delivery class for an admitted cross-session message. +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SessionMessageDelivery { + /// The recipient was idle and the message started a turn. + #[serde(rename = "idle")] + Idle, + /// The message entered the active turn's safe steering boundary. + #[serde(rename = "steering")] + Steering, + /// The message was admitted to the recipient queue. + #[serde(rename = "queued")] + Queued, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Parameters for one authenticated exact-target cross-session message. +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SendSessionMessageRequest { + /// Exact active local recipient session ID. + pub target_session_id: String, + /// Natural-language message content. + pub content: String, + /// Requested delivery mode. The host applies its existing default when omitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub delivery: Option, +} + +/// Recipient admission result for an authenticated cross-session message. +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SendSessionMessageResult { + /// Unique identifier assigned to the admitted message. + pub message_id: String, + /// Actual recipient delivery class at admission. + pub delivery: SessionMessageDelivery, + /// Sanitized recipient display name for presentation only. + #[serde(skip_serializing_if = "Option::is_none")] + pub target_display_name: Option, +} + impl SendRequest { /// Set the message provenance without changing other request options. /// diff --git a/rust/src/session.rs b/rust/src/session.rs index 82e97b6606..2dfd8cfce2 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -29,6 +29,10 @@ use crate::handler::{ }; use crate::hooks::SessionHooks; use crate::provider_token::BearerTokenProvider; +use crate::rpc::{ + ListMessageableSessionsRequest, ListMessageableSessionsResult, SendSessionMessageRequest, + SendSessionMessageResult, +}; use crate::session_fs::SessionFsProvider; use crate::trace_context::inject_trace_context; use crate::transforms::SystemMessageTransform; @@ -41,8 +45,8 @@ use crate::types::{ ToolResultExpanded, TraceContext, UiInputOptions, ensure_attachment_display_names, }; use crate::{ - Client, Error, ErrorKind, JsonRpcResponse, SessionErrorKind, SessionEventNotification, - error_codes, + Client, Error, ErrorKind, JsonRpcResponse, SendSessionMessageErrorCode, SessionErrorKind, + SessionEventNotification, error_codes, }; /// Fixed name of the runtime's built-in tool-search tool. A client can replace @@ -50,6 +54,44 @@ use crate::{ /// `overrides_built_in_tool` set to `true`. const TOOL_SEARCH_TOOL_NAME: &str = "tool_search_tool"; +fn parse_send_session_message_error_data( + data: Option<&Value>, +) -> Option<(SendSessionMessageErrorCode, Option)> { + let envelope = data?.as_object()?; + let kind = envelope.get("kind")?.as_str()?; + let runtime_code = envelope.get("code")?.as_str()?; + let message_id = match envelope.get("messageId") { + Some(value) => Some(value.as_str()?.to_string()), + None => None, + }; + + let code = match kind { + "session_message_refused" + if matches!( + runtime_code, + "target-not-active" + | "target-generation-changed" + | "source-not-active" + | "self-send" + | "request-invalid" + | "recipient-refused" + | "transport-unavailable" + ) => + { + SendSessionMessageErrorCode::Refused + } + "session_message_not_delivered" if runtime_code == "not-delivered" => { + SendSessionMessageErrorCode::NotDelivered + } + "session_message_ambiguous" if runtime_code == "ambiguous" => { + SendSessionMessageErrorCode::Ambiguous + } + _ => return None, + }; + + Some((code, message_id)) +} + /// Default capacity of the per-session event broadcast buffer backing /// [`Session::subscribe`] and [`PreparedSession::subscribe`]. /// @@ -517,6 +559,61 @@ impl Session { self.send_inner(opts.into()).await } + /// Lists active local sessions available for exact-ID cross-session messaging. + /// + /// The runtime derives the source identity from this bound session. Pass + /// `None` to list all messageable sessions or a request containing an + /// exact-name filter. Discovery grants no delivery authority. + pub async fn list_messageable_sessions( + &self, + params: Option, + ) -> Result { + let mut wire_params = serde_json::to_value(params.unwrap_or_default())?; + wire_params["sessionId"] = Value::String(self.id.to_string()); + let value = self + .client + .call("session.listMessageableSessions", Some(wire_params)) + .await?; + Ok(serde_json::from_value(value)?) + } + + /// Sends one authenticated non-user message to an exact active local session. + /// + /// The runtime derives the source identity from this bound session. Success + /// reports recipient admission, not completion of delegated work. An + /// [`SendSessionMessageErrorCode::Ambiguous`] error means delivery may have + /// started and must not be retried automatically. + pub async fn send_session_message( + &self, + params: SendSessionMessageRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = Value::String(self.id.to_string()); + match self + .client + .call("session.sendSessionMessage", Some(wire_params)) + .await + .and_then(|value| serde_json::from_value(value).map_err(Error::from)) + { + Ok(result) => Ok(result), + Err(error) => { + let Some((code, message_id)) = + parse_send_session_message_error_data(error.rpc_data()) + else { + return Err(error); + }; + let message = error + .message() + .map(str::to_owned) + .unwrap_or_else(|| error.to_string()); + Err(Error::with_message( + ErrorKind::Session(SessionErrorKind::SendSessionMessage { code, message_id }), + message, + )) + } + } + } + async fn send_inner(&self, opts: MessageOptions) -> Result { let mut params = serde_json::json!({ "sessionId": self.id, diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 9c8eda07f7..282dfa9fa1 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -19,7 +19,8 @@ use github_copilot_sdk::handler::{ }; use github_copilot_sdk::rpc::{ CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, - OpenCanvasInstance, SendAgentMode, SendMode, SendRequest, + ListMessageableSessionsRequest, OpenCanvasInstance, SendAgentMode, SendMode, SendRequest, + SendSessionMessageRequest, SessionMessageDelivery, }; use github_copilot_sdk::session_events::{ ManagedSettingsResolvedSource, McpOauthRequiredData, ReasoningSummary, SessionLimitsConfig, @@ -34,7 +35,8 @@ use github_copilot_sdk::types::{ SessionConfig, SessionId, SetModelOptions, Tool, ToolInvocation, ToolResult, }; use github_copilot_sdk::{ - AgentMode, Attachment, Client, ContextTier, ErrorKind, MessageSource, ProtocolErrorKind, tool, + AgentMode, Attachment, Client, ContextTier, ErrorKind, MessageSource, ProtocolErrorKind, + SendSessionMessageErrorCode, SessionErrorKind, tool, }; use serde_json::Value; use tokio::io::{AsyncWrite, AsyncWriteExt, duplex}; @@ -333,6 +335,26 @@ impl FakeServer { write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; } + async fn respond_error_with_data( + &mut self, + request: &Value, + code: i64, + message: &str, + data: Value, + ) { + let id = request["id"].as_u64().unwrap(); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "error": { + "code": code, + "message": message, + "data": data, + }, + }); + write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; + } + async fn send_notification(&mut self, method: &str, params: Value) { let notification = serde_json::json!({ "jsonrpc": "2.0", @@ -2411,6 +2433,247 @@ async fn send_omits_display_prompt_when_unset() { timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); } +#[tokio::test] +async fn list_messageable_sessions_stamps_bound_source_and_preserves_exact_name() { + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + let handle = tokio::spawn({ + let session = session.clone(); + async move { + session + .list_messageable_sessions(Some(ListMessageableSessionsRequest { + name: Some(" ReSeArCh ".to_string()), + })) + .await + } + }); + + let request = server.read_request().await; + assert_eq!(request["method"], "session.listMessageableSessions"); + assert_eq!( + request["params"], + serde_json::json!({ + "sessionId": server.session_id, + "name": " ReSeArCh ", + }) + ); + server + .respond( + &request, + serde_json::json!({ + "sessions": [ + { + "sessionId": "target-session", + "name": "Research", + "summary": "Inspect the runtime." + } + ] + }), + ) + .await; + + let result = timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); + assert_eq!(result.sessions.len(), 1); + assert_eq!(result.sessions[0].session_id, "target-session"); + assert_eq!(result.sessions[0].name.as_deref(), Some("Research")); + assert_eq!( + result.sessions[0].summary.as_deref(), + Some("Inspect the runtime.") + ); +} + +#[tokio::test] +async fn list_messageable_sessions_omits_absent_name() { + let (session, mut server) = create_session_pair().await; + let handle = tokio::spawn(async move { session.list_messageable_sessions(None).await }); + + let request = server.read_request().await; + assert_eq!(request["method"], "session.listMessageableSessions"); + assert_eq!( + request["params"], + serde_json::json!({ "sessionId": server.session_id }) + ); + server + .respond(&request, serde_json::json!({ "sessions": [] })) + .await; + + let result = timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); + assert!(result.sessions.is_empty()); +} + +#[tokio::test] +async fn send_session_message_stamps_bound_source_and_returns_admission_result() { + let (session, mut server) = create_session_pair().await; + let handle = tokio::spawn(async move { + session + .send_session_message(SendSessionMessageRequest { + target_session_id: "target-session".to_string(), + content: "Please inspect this.".to_string(), + delivery: None, + }) + .await + }); + + let request = server.read_request().await; + assert_eq!(request["method"], "session.sendSessionMessage"); + assert_eq!( + request["params"], + serde_json::json!({ + "sessionId": server.session_id, + "targetSessionId": "target-session", + "content": "Please inspect this.", + }) + ); + server + .respond( + &request, + serde_json::json!({ + "messageId": "message-1", + "delivery": "idle", + }), + ) + .await; + + let result = timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); + assert_eq!(result.message_id, "message-1"); + assert_eq!(result.delivery, SessionMessageDelivery::Idle); +} + +#[tokio::test] +async fn send_session_message_forwards_explicit_delivery() { + let (session, mut server) = create_session_pair().await; + let handle = tokio::spawn(async move { + session + .send_session_message(SendSessionMessageRequest { + target_session_id: "target-session".to_string(), + content: "Please inspect this.".to_string(), + delivery: Some(SendMode::Immediate), + }) + .await + }); + + let request = server.read_request().await; + assert_eq!(request["method"], "session.sendSessionMessage"); + assert_eq!( + request["params"], + serde_json::json!({ + "sessionId": server.session_id, + "targetSessionId": "target-session", + "content": "Please inspect this.", + "delivery": "immediate", + }) + ); + server + .respond( + &request, + serde_json::json!({ + "messageId": "message-immediate", + "delivery": "steering", + }), + ) + .await; + + let result = timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); + assert_eq!(result.message_id, "message-immediate"); + assert_eq!(result.delivery, SessionMessageDelivery::Steering); +} + +#[tokio::test] +async fn send_session_message_preserves_typed_terminal_outcomes_and_message_ids() { + for (kind, runtime_code, expected_code) in [ + ( + "session_message_refused", + "target-not-active", + SendSessionMessageErrorCode::Refused, + ), + ( + "session_message_not_delivered", + "not-delivered", + SendSessionMessageErrorCode::NotDelivered, + ), + ( + "session_message_ambiguous", + "ambiguous", + SendSessionMessageErrorCode::Ambiguous, + ), + ] { + let (session, mut server) = create_session_pair().await; + let handle = tokio::spawn(async move { + session + .send_session_message(SendSessionMessageRequest { + target_session_id: "target-session".to_string(), + content: "Please inspect this.".to_string(), + delivery: Some(SendMode::Enqueue), + }) + .await + }); + + let request = server.read_request().await; + server + .respond_error_with_data( + &request, + -32603, + "cross-session send failed", + serde_json::json!({ + "kind": kind, + "code": runtime_code, + "messageId": "message-terminal", + }), + ) + .await; + + let error = timeout(TIMEOUT, handle) + .await + .unwrap() + .unwrap() + .unwrap_err(); + let ErrorKind::Session(SessionErrorKind::SendSessionMessage { code, message_id }) = + error.kind() + else { + panic!("unexpected error kind: {:?}", error.kind()); + }; + assert_eq!(*code, expected_code); + assert_eq!(message_id.as_deref(), Some("message-terminal")); + assert_eq!(error.message(), Some("cross-session send failed")); + } +} + +#[tokio::test] +async fn send_session_message_leaves_unrecognized_error_envelopes_as_rpc_errors() { + let (session, mut server) = create_session_pair().await; + let handle = tokio::spawn(async move { + session + .send_session_message(SendSessionMessageRequest { + target_session_id: "target-session".to_string(), + content: "Please inspect this.".to_string(), + delivery: None, + }) + .await + }); + + let request = server.read_request().await; + server + .respond_error_with_data( + &request, + -32603, + "raw runtime failure", + serde_json::json!({ + "kind": "session_message_ambiguous", + "code": "not-delivered", + "messageId": "message-mismatched", + }), + ) + .await; + + let error = timeout(TIMEOUT, handle) + .await + .unwrap() + .unwrap() + .unwrap_err(); + assert_eq!(error.rpc_code(), Some(-32603)); + assert_eq!(error.message(), Some("raw runtime failure")); +} + #[tokio::test] async fn session_rpc_methods_send_correct_method_names() { let (session, mut server) = create_session_pair().await; diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index f5e8acb146..95c2534551 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -995,7 +995,7 @@ function emitGroup( // sessionId is already stripped from the generated type definition, // so no need for Omit<..., "sessionId"> sigParams.push(`params${optMark}: ${paramsType}`); - bodyArg = "{ sessionId, ...params }"; + bodyArg = "{ ...params, sessionId }"; } else { bodyArg = "{ sessionId }"; }