From dc77d92680cb8fc303b5f4788176e6c1f267d89d Mon Sep 17 00:00:00 2001 From: dfrysinger <1424648+dfrysinger@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:22:17 -0600 Subject: [PATCH 1/4] Rust SDK: Add authenticated cross-session input producer Add a hand-written typed `Session::admit_authenticated_cross_session_input` for the runtime's private direct-host-only `session.lifecycle.admitAuthenticatedCrossSessionInput` method. The public request types carry only caller-variable fields; the wire `version`, `kind`, `origin`, and `integrity` discriminators are stamped by private wire types during request conversion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b6a7c90-1ad5-47e3-83a3-a87b895fb13c --- rust/src/session.rs | 38 +++++ rust/src/types.rs | 301 +++++++++++++++++++++++++++++++++++++ rust/tests/session_test.rs | 224 ++++++++++++++++++++++++++- 3 files changed, 557 insertions(+), 6 deletions(-) diff --git a/rust/src/session.rs b/rust/src/session.rs index b9d2173055..75366131a7 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -30,6 +30,7 @@ use crate::session_fs::SessionFsProvider; use crate::trace_context::inject_trace_context; use crate::transforms::SystemMessageTransform; use crate::types::{ + AdmitAuthenticatedCrossSessionInputParams, AdmitAuthenticatedCrossSessionInputRequest, CommandContext, CommandDefinition, CommandHandler, CreateSessionResult, ElicitationRequest, ElicitationResult, ExitPlanModeData, GetMessagesResponse, MessageOptions, PermissionRequestData, RequestId, ResumeSessionConfig, ResumeSessionResult, SectionOverride, @@ -500,6 +501,43 @@ impl Session { } } + /// Admit an authenticated cross-session input into this session. + /// + /// Wraps the runtime's private, direct-host-only + /// `session.lifecycle.admitAuthenticatedCrossSessionInput` method. The + /// runtime rejects the call unless it arrives on the local direct + /// connection, so this is only usable by a host embedding the SDK + /// in-process (or over the direct local transport), not by a remote peer. + /// + /// The caller supplies only the variable fields of + /// [`CrossSessionInput`](crate::types::CrossSessionInput). The wire + /// payload's `version`, `kind`, `origin`, and `integrity` discriminators + /// are stamped by the SDK, so a caller cannot assert a different + /// provenance or integrity class for the admitted content. + /// + /// The runtime's response carries no payload this SDK surfaces; errors + /// from the runtime are returned unchanged. + /// + /// # Cancel safety + /// + /// **Cancel-safe.** Single RPC dispatched through the writer-actor (see + /// [`Client::call`](crate::Client::call)). If the caller's future is + /// dropped after the frame is enqueued, the admission still lands and + /// the runtime processes it normally. + pub async fn admit_authenticated_cross_session_input( + &self, + request: AdmitAuthenticatedCrossSessionInputRequest, + ) -> Result<(), Error> { + let params = AdmitAuthenticatedCrossSessionInputParams::new(self.id.clone(), request); + self.client + .call( + "session.lifecycle.admitAuthenticatedCrossSessionInput", + Some(serde_json::to_value(params)?), + ) + .await?; + Ok(()) + } + /// Retrieve the session's timeline events. pub async fn get_events(&self) -> Result, Error> { let result = self diff --git a/rust/src/types.rs b/rust/src/types.rs index ee3ac3df26..2b62989010 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -5424,6 +5424,307 @@ impl From<&String> for MessageOptions { } } +/// Wire `version` stamped on every authenticated cross-session admission +/// request. The runtime rejects any other value. +const CROSS_SESSION_INPUT_VERSION: u32 = 1; + +/// Wire `kind` discriminator stamped on every authenticated cross-session +/// admission request. +const CROSS_SESSION_INPUT_KIND: &str = "authenticated-cross-session"; + +/// Wire `origin` provenance class stamped on every authenticated +/// cross-session admission request. Cross-session input is never human +/// input, so the SDK — not the caller — asserts the non-human origin. +const CROSS_SESSION_INPUT_ORIGIN: &str = "authenticated-cross-session"; + +/// Wire `integrity` class stamped on every authenticated cross-session +/// admission request. Cross-session content is untrusted even when the +/// host authenticated its sender, so the SDK — not the caller — asserts it. +const CROSS_SESSION_INPUT_INTEGRITY: &str = "untrusted"; + +/// Optional private presentation metadata describing the sending session, +/// carried alongside a [`CrossSessionInput`]. +/// +/// Every field is optional and is omitted from the wire payload when `None`. +/// The runtime rejects explicit `null` for any of these fields. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[non_exhaustive] +pub struct CrossSessionPresentation { + /// Identifier of the sending project session. + pub project_session_id: Option, + /// Human-readable name of the sending project session. + pub project_session_name: Option, + /// Display name shown for the sender. + pub display_name: Option, + /// Branch the sending project session is working on. + pub project_session_branch: Option, + /// Repository the sending project session is working in. + pub repository: Option, +} + +impl CrossSessionPresentation { + /// Build an empty presentation block. Every field is omitted until set. + pub fn new() -> Self { + Self::default() + } + + /// Set the sending project session's identifier. + pub fn with_project_session_id(mut self, project_session_id: impl Into) -> Self { + self.project_session_id = Some(project_session_id.into()); + self + } + + /// Set the sending project session's name. + pub fn with_project_session_name(mut self, project_session_name: impl Into) -> Self { + self.project_session_name = Some(project_session_name.into()); + self + } + + /// Set the display name shown for the sender. + pub fn with_display_name(mut self, display_name: impl Into) -> Self { + self.display_name = Some(display_name.into()); + self + } + + /// Set the sending project session's branch. + pub fn with_project_session_branch(mut self, branch: impl Into) -> Self { + self.project_session_branch = Some(branch.into()); + self + } + + /// Set the sending project session's repository. + pub fn with_repository(mut self, repository: impl Into) -> Self { + self.repository = Some(repository.into()); + self + } +} + +/// The caller-supplied half of an authenticated cross-session input. +/// +/// This type deliberately carries only the fields a host may vary. The +/// wire payload's `version`, `kind`, `origin`, and `integrity` +/// discriminators are stamped by the SDK during request conversion and are +/// not reachable from this type, so a caller cannot claim a different +/// provenance or integrity class. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct CrossSessionInput { + /// Stable identifier assigned by the sending host for this message. + pub message_id: String, + /// Authenticated same-user principal supplied by the sending host. + pub sender_principal: String, + /// Session identifier of the sender. + pub sender_session_id: String, + /// Host-stamped immediate sender identity used to check that a reply + /// stays on the established edge. + pub reply_target: String, + /// Raw natural-language message content. + pub content: String, + /// Optional recipient behavior request, distinct from + /// [`delivery_mode`](Self::delivery_mode). Omitted when `None`. + pub requested_mode: Option, + /// Optional private presentation metadata. Omitted when `None`. + pub presentation: Option, + /// Optional scheduling selection. Omitted when `None`, which preserves + /// the recipient session's current default. + pub delivery_mode: Option, +} + +impl CrossSessionInput { + /// Build a cross-session input from its required fields. + pub fn new( + message_id: impl Into, + sender_principal: impl Into, + sender_session_id: impl Into, + reply_target: impl Into, + content: impl Into, + ) -> Self { + Self { + message_id: message_id.into(), + sender_principal: sender_principal.into(), + sender_session_id: sender_session_id.into(), + reply_target: reply_target.into(), + content: content.into(), + requested_mode: None, + presentation: None, + delivery_mode: None, + } + } + + /// Set the optional recipient behavior request. + pub fn with_requested_mode(mut self, requested_mode: impl Into) -> Self { + self.requested_mode = Some(requested_mode.into()); + self + } + + /// Attach optional private presentation metadata. + pub fn with_presentation(mut self, presentation: CrossSessionPresentation) -> Self { + self.presentation = Some(presentation); + self + } + + /// Set the optional delivery mode for this admission. + pub fn with_delivery_mode(mut self, delivery_mode: DeliveryMode) -> Self { + self.delivery_mode = Some(delivery_mode); + self + } +} + +/// Recipient-side per-turn continuation context supplied by the local host. +/// +/// This is never transmitted between sessions and is never populated from a +/// remote sender's payload. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[non_exhaustive] +pub struct CrossSessionRecipientContext { + /// Session identifiers this turn is authorized to continue with. + pub authorized_continuation_targets: Vec, +} + +impl CrossSessionRecipientContext { + /// Build a recipient context from a set of authorized continuation targets. + pub fn new(targets: impl IntoIterator>) -> Self { + Self { + authorized_continuation_targets: targets.into_iter().map(Into::into).collect(), + } + } +} + +/// Request for +/// [`Session::admit_authenticated_cross_session_input`](crate::session::Session::admit_authenticated_cross_session_input). +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct AdmitAuthenticatedCrossSessionInputRequest { + /// The authenticated cross-session input to admit. + pub input: CrossSessionInput, + /// Optional recipient-side continuation context. Omitted when `None`. + pub recipient_context: Option, +} + +impl AdmitAuthenticatedCrossSessionInputRequest { + /// Build a request that admits `input` with no extra recipient context. + pub fn new(input: CrossSessionInput) -> Self { + Self { + input, + recipient_context: None, + } + } + + /// Attach recipient-side continuation context. + pub fn with_recipient_context(mut self, context: CrossSessionRecipientContext) -> Self { + self.recipient_context = Some(context); + self + } +} + +/// Wire params for `session.lifecycle.admitAuthenticatedCrossSessionInput`. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AdmitAuthenticatedCrossSessionInputParams { + session_id: SessionId, + input: CrossSessionInputWire, + #[serde(skip_serializing_if = "Option::is_none")] + recipient_context: Option, +} + +impl AdmitAuthenticatedCrossSessionInputParams { + pub(crate) fn new( + session_id: SessionId, + request: AdmitAuthenticatedCrossSessionInputRequest, + ) -> Self { + Self { + session_id, + input: CrossSessionInputWire::from(request.input), + recipient_context: request.recipient_context.map(Into::into), + } + } +} + +/// Wire form of [`CrossSessionInput`]. The provenance and integrity +/// discriminators are private and stamped here, so no caller-provided value +/// can reach them. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct CrossSessionInputWire { + version: u32, + kind: &'static str, + origin: &'static str, + integrity: &'static str, + message_id: String, + sender_principal: String, + sender_session_id: String, + reply_target: String, + content: String, + #[serde(skip_serializing_if = "Option::is_none")] + requested_mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + presentation: Option, + #[serde(skip_serializing_if = "Option::is_none")] + delivery_mode: Option, +} + +impl From for CrossSessionInputWire { + fn from(input: CrossSessionInput) -> Self { + Self { + version: CROSS_SESSION_INPUT_VERSION, + kind: CROSS_SESSION_INPUT_KIND, + origin: CROSS_SESSION_INPUT_ORIGIN, + integrity: CROSS_SESSION_INPUT_INTEGRITY, + message_id: input.message_id, + sender_principal: input.sender_principal, + sender_session_id: input.sender_session_id, + reply_target: input.reply_target, + content: input.content, + requested_mode: input.requested_mode, + presentation: input.presentation.map(Into::into), + delivery_mode: input.delivery_mode, + } + } +} + +/// Wire form of [`CrossSessionPresentation`]. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct CrossSessionPresentationWire { + #[serde(skip_serializing_if = "Option::is_none")] + project_session_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + project_session_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + display_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + project_session_branch: Option, + #[serde(skip_serializing_if = "Option::is_none")] + repository: Option, +} + +impl From for CrossSessionPresentationWire { + fn from(presentation: CrossSessionPresentation) -> Self { + Self { + project_session_id: presentation.project_session_id, + project_session_name: presentation.project_session_name, + display_name: presentation.display_name, + project_session_branch: presentation.project_session_branch, + repository: presentation.repository, + } + } +} + +/// Wire form of [`CrossSessionRecipientContext`]. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct CrossSessionRecipientContextWire { + authorized_continuation_targets: Vec, +} + +impl From for CrossSessionRecipientContextWire { + fn from(context: CrossSessionRecipientContext) -> Self { + Self { + authorized_continuation_targets: context.authorized_continuation_targets, + } + } +} + /// Response from [`Client::get_status`](crate::Client::get_status). #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index fb16a0f674..300dddd920 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -24,12 +24,14 @@ use github_copilot_sdk::session_events::{ SessionManagedSettingsResolvedData, }; use github_copilot_sdk::types::{ - AskUserVariant, CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, - CommandContext, CommandDefinition, CommandHandler, DeliveryMode, DisableBypassPermissionsModes, - ElicitationRequest, ElicitationResult, ExitPlanModeData, ExtensionInfo, ManagedSettings, - ManagedSettingsPermissions, MessageOptions, PermissionDecisionContext, - PermissionDecisionOutcome, PermissionDecisionSource, PermissionDecisionSurface, RequestId, - SessionConfig, SessionId, SetModelOptions, Tool, ToolInvocation, ToolResult, + AdmitAuthenticatedCrossSessionInputRequest, AskUserVariant, CanvasProviderIdentity, + CloudSessionOptions, CloudSessionRepository, CommandContext, CommandDefinition, CommandHandler, + CrossSessionInput, CrossSessionPresentation, CrossSessionRecipientContext, DeliveryMode, + DisableBypassPermissionsModes, ElicitationRequest, ElicitationResult, ExitPlanModeData, + ExtensionInfo, ManagedSettings, ManagedSettingsPermissions, MessageOptions, + PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource, + PermissionDecisionSurface, RequestId, SessionConfig, SessionId, SetModelOptions, Tool, + ToolInvocation, ToolResult, }; use github_copilot_sdk::{Client, ContextTier, ErrorKind, ProtocolErrorKind, tool}; use serde_json::Value; @@ -1703,6 +1705,216 @@ async fn send_omits_display_prompt_when_unset() { timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); } +#[tokio::test] +async fn admit_authenticated_cross_session_input_serializes_full_request() { + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + + let handle = tokio::spawn({ + let session = session.clone(); + async move { + let presentation = CrossSessionPresentation::new() + .with_project_session_id("project-session-7") + .with_project_session_name("Nexus") + .with_display_name("Alpha") + .with_project_session_branch("dfrysinger/cross-session") + .with_repository("github/copilot-sdk"); + let input = CrossSessionInput::new( + "message-1", + "copilot-user-1", + "sender-session-1", + "reply-target-1", + "Inspect the failure.", + ) + .with_requested_mode("plan") + .with_presentation(presentation) + .with_delivery_mode(DeliveryMode::Immediate); + let request = AdmitAuthenticatedCrossSessionInputRequest::new(input) + .with_recipient_context(CrossSessionRecipientContext::new([ + "sender-session-1", + "reply-target-1", + "parent-session-9", + ])); + session + .admit_authenticated_cross_session_input(request) + .await + } + }); + + let request = server.read_request().await; + assert_eq!( + request["method"], + "session.lifecycle.admitAuthenticatedCrossSessionInput" + ); + + let params = request["params"].as_object().expect("params object"); + assert_eq!(params.len(), 3, "unexpected params: {:?}", params.keys()); + assert_eq!(params["sessionId"], server.session_id); + + let input = params["input"].as_object().expect("input object"); + assert_eq!(input.len(), 12, "unexpected input keys: {:?}", input.keys()); + // Stamped by the SDK's request conversion — `CrossSessionInput` exposes + // no field a caller could use to supply or override these four values. + assert_eq!(input["version"], 1); + assert!( + input["version"].is_u64(), + "version must serialize as an integer, got: {}", + input["version"] + ); + assert_eq!(input["kind"], "authenticated-cross-session"); + assert_eq!(input["origin"], "authenticated-cross-session"); + assert_eq!(input["integrity"], "untrusted"); + assert_eq!(input["messageId"], "message-1"); + assert_eq!(input["senderPrincipal"], "copilot-user-1"); + assert_eq!(input["senderSessionId"], "sender-session-1"); + assert_eq!(input["replyTarget"], "reply-target-1"); + assert_eq!(input["content"], "Inspect the failure."); + assert_eq!(input["requestedMode"], "plan"); + assert_eq!(input["deliveryMode"], "immediate"); + + let presentation = input["presentation"] + .as_object() + .expect("presentation object"); + assert_eq!( + presentation.len(), + 5, + "unexpected presentation keys: {:?}", + presentation.keys() + ); + assert_eq!(presentation["projectSessionId"], "project-session-7"); + assert_eq!(presentation["projectSessionName"], "Nexus"); + assert_eq!(presentation["displayName"], "Alpha"); + assert_eq!( + presentation["projectSessionBranch"], + "dfrysinger/cross-session" + ); + assert_eq!(presentation["repository"], "github/copilot-sdk"); + + let recipient_context = params["recipientContext"] + .as_object() + .expect("recipientContext object"); + assert_eq!( + recipient_context.len(), + 1, + "unexpected recipientContext keys: {:?}", + recipient_context.keys() + ); + assert_eq!( + recipient_context["authorizedContinuationTargets"], + serde_json::json!(["sender-session-1", "reply-target-1", "parent-session-9"]) + ); + + server.respond(&request, serde_json::json!({})).await; + timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); +} + +#[tokio::test] +async fn admit_authenticated_cross_session_input_omits_unset_optional_fields() { + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + + let handle = tokio::spawn({ + let session = session.clone(); + async move { + // Caller-controlled values that *look* like provenance claims must + // not reach the stamped discriminators. + let input = CrossSessionInput::new( + "message-2", + "host_authenticated", + "sender-session-2", + "reply-target-2", + "cross_session", + ); + session + .admit_authenticated_cross_session_input( + AdmitAuthenticatedCrossSessionInputRequest::new(input), + ) + .await + } + }); + + let request = server.read_request().await; + assert_eq!( + request["method"], + "session.lifecycle.admitAuthenticatedCrossSessionInput" + ); + + let params = request["params"].as_object().expect("params object"); + assert_eq!(params.len(), 2, "unexpected params: {:?}", params.keys()); + assert_eq!(params["sessionId"], server.session_id); + assert!( + params.get("recipientContext").is_none(), + "recipientContext should be omitted when unset, got: {}", + request["params"] + ); + + let input = params["input"].as_object().expect("input object"); + assert_eq!(input.len(), 9, "unexpected input keys: {:?}", input.keys()); + assert_eq!(input["version"], 1); + assert_eq!(input["kind"], "authenticated-cross-session"); + assert_eq!(input["origin"], "authenticated-cross-session"); + assert_eq!(input["integrity"], "untrusted"); + assert_eq!(input["messageId"], "message-2"); + assert_eq!(input["senderPrincipal"], "host_authenticated"); + assert_eq!(input["senderSessionId"], "sender-session-2"); + assert_eq!(input["replyTarget"], "reply-target-2"); + assert_eq!(input["content"], "cross_session"); + for omitted in ["requestedMode", "presentation", "deliveryMode"] { + assert!( + input.get(omitted).is_none(), + "{omitted} should be omitted when unset, got: {}", + request["params"]["input"] + ); + } + + server.respond(&request, serde_json::json!({})).await; + timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); +} + +#[tokio::test] +async fn admit_authenticated_cross_session_input_propagates_runtime_errors() { + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + + let handle = tokio::spawn({ + let session = session.clone(); + async move { + session + .admit_authenticated_cross_session_input( + AdmitAuthenticatedCrossSessionInputRequest::new(CrossSessionInput::new( + "message-3", + "copilot-user-1", + "sender-session-3", + "reply-target-3", + "Steer the active turn.", + )), + ) + .await + } + }); + + let request = server.read_request().await; + let id = request["id"].as_u64().unwrap(); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "error": { + "code": -32603, + "message": "authenticated cross-session input admission is local-host only", + }, + }); + write_framed(&mut server.write, &serde_json::to_vec(&response).unwrap()).await; + + let result = timeout(TIMEOUT, handle).await.unwrap().unwrap(); + let error = result.expect_err("runtime rejection must not be swallowed"); + assert!( + error + .to_string() + .contains("authenticated cross-session input admission is local-host only"), + "unexpected error: {error}" + ); +} + #[tokio::test] async fn session_rpc_methods_send_correct_method_names() { let (session, mut server) = create_session_pair().await; From 01396124b646ec00b4380a67112d78d071df776f Mon Sep 17 00:00:00 2001 From: dfrysinger <1424648+dfrysinger@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:44:38 -0600 Subject: [PATCH 2/4] Expose local cross-session discovery and delivery Add session-bound Node and Rust SDK operations for listing active local peers and sending an exact-ID message through the runtime contract. Preserve typed refused, not-delivered, and ambiguous outcomes without retrying an uncertain delivery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ea23a388-a87d-436d-8110-4f7f543141d6 --- nodejs/src/generated/rpc.ts | 473 +++++++++++------- nodejs/src/generated/session-events.ts | 26 +- nodejs/src/index.ts | 16 +- nodejs/src/session.ts | 116 +++++ .../session-list-messageable-sessions.test.ts | 115 +++++ .../test/session-send-session-message.test.ts | 245 +++++++++ nodejs/tsconfig.test.json | 7 +- rust/src/errors.rs | 51 ++ rust/src/generated/api_types.rs | 153 ++++++ rust/src/generated/rpc.rs | 69 +++ rust/src/lib.rs | 5 +- rust/src/session.rs | 92 +++- rust/tests/session_test.rs | 261 +++++++++- scripts/codegen/typescript.ts | 2 +- 14 files changed, 1437 insertions(+), 194 deletions(-) create mode 100644 nodejs/test/session-list-messageable-sessions.test.ts create mode 100644 nodejs/test/session-send-session-message.test.ts diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 812c6b2607..e740427f79 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -2927,6 +2927,20 @@ export type SandboxConfigSource = | "unsupported_host" /** A repository policy selected the sandbox state. */ | "repository_policy"; +/** + * Actual recipient delivery class for an admitted cross-session message. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionMessageDelivery". + */ +/** @experimental */ +export type SessionMessageDelivery = + /** The recipient was idle and the message started a turn. */ + | "idle" + /** The message entered the active turn's safe steering boundary. */ + | "steering" + /** The message was admitted to the recipient queue. */ + | "queued"; /** * Current authentication information, or null when no authentication is active. * @@ -2961,6 +2975,8 @@ export type SessionCapability = | "elicitation" /** Cross-session history tools and session-store SQL prompt/tool metadata. */ | "session-store" + /** First-party local cross-session messaging tool for a root CLI session. */ + | "cross-session-messaging" /** MCP Apps UI passthrough. */ | "mcp-apps" /** Host-provided canvas rendering support. */ @@ -9284,6 +9300,53 @@ export interface InterruptMainTurnResult { */ interrupted: boolean; } +/** + * Optional exact-name query for active local messageable sessions. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ListMessageableSessionsRequest". + */ +/** @experimental */ +export interface ListMessageableSessionsRequest { + /** + * Optional exact session name query. Matching semantics are owned by the local host. + */ + name?: string; +} +/** + * Sanitized active local sessions available for exact-ID messaging selection. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ListMessageableSessionsResult". + */ +/** @experimental */ +export interface ListMessageableSessionsResult { + /** + * Messageable sessions in deterministic session-ID order. + */ + sessions: MessageableSession[]; +} +/** + * Sanitized active local session available for exact-ID messaging selection. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "MessageableSession". + */ +/** @experimental */ +export interface MessageableSession { + /** + * Stable session ID to provide to session.sendSessionMessage. + */ + sessionId: string; + /** + * Current session name when available. + */ + name?: string; + /** + * Current session summary when available. + */ + summary?: string; +} /** * HTTP headers as a map from lowercased header name to a list of values. Multi-valued headers (e.g. Set-Cookie) preserve all values. * @@ -17073,6 +17136,38 @@ export interface SendResult { */ messageId: string; } +/** + * Parameters for sending one authenticated non-user message from the current bound session to an exact active local session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SendSessionMessageRequest". + */ +/** @experimental */ +export interface SendSessionMessageRequest { + /** + * Exact active local recipient session ID. + */ + targetSessionId: string; + /** + * Natural-language message content. + */ + content: string; + delivery?: SendMode; +} +/** + * Recipient admission result for an authenticated cross-session message. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SendSessionMessageResult". + */ +/** @experimental */ +export interface SendSessionMessageResult { + /** + * Unique identifier assigned to the admitted message. + */ + messageId: string; + delivery: SessionMessageDelivery; +} /** * Internal request for sending a system notification. * @@ -23672,7 +23767,29 @@ 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 one authenticated non-user message from the current bound session to an exact active local session. Success reports recipient admission, not delegated-work completion. + * + * @param params Parameters for sending one authenticated non-user message from the current bound session to an exact active local session. + * + * @returns Recipient admission result for an authenticated cross-session message. + * + * @experimental + */ + sendSessionMessage: async (params: SendSessionMessageRequest): Promise => + connection.sendRequest("session.sendSessionMessage", { ...params, sessionId }), + /** + * Lists active local sessions that the current bound session may select by exact ID for cross-session messaging. This discovery result grants no delivery authority. + * + * @param params Optional exact-name query for active local messageable sessions. + * + * @returns Sanitized active local sessions available for exact-ID messaging selection. + * + * @experimental + */ + listMessageableSessions: async (params: ListMessageableSessionsRequest): Promise => + connection.sendRequest("session.listMessageableSessions", { ...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. * @@ -23683,7 +23800,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: { /** @@ -23704,7 +23821,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. * @@ -23715,7 +23832,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. * @@ -23733,7 +23850,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: { /** @@ -23751,7 +23868,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: { @@ -23763,7 +23880,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: { @@ -23789,14 +23906,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: { /** @@ -23807,7 +23924,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 */ @@ -23820,7 +23937,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. * @@ -23829,7 +23946,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. * @@ -23838,7 +23955,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. * @@ -23847,7 +23964,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. * @@ -23856,7 +23973,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. * @@ -23865,7 +23982,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. * @@ -23874,7 +23991,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 }), /** * Records a batch of ordered factory progress lines. * @@ -23883,7 +24000,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. * @@ -23892,7 +24009,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: { /** @@ -23903,7 +24020,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. * @@ -23912,7 +24029,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 */ @@ -23932,7 +24049,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 }), /** * Updates the session's reasoning effort without changing the selected model. * @@ -23941,7 +24058,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. * @@ -23950,7 +24067,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: { @@ -23969,7 +24086,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: { @@ -23986,7 +24103,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. * @@ -23995,7 +24112,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: { @@ -24012,7 +24129,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. */ @@ -24050,7 +24167,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. * @@ -24059,7 +24176,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. * @@ -24075,14 +24192,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. * @@ -24098,7 +24215,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. * @@ -24107,7 +24224,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. * @@ -24116,7 +24233,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. * @@ -24132,7 +24249,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. * @@ -24155,7 +24272,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`. * @@ -24164,7 +24281,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 */ completions: { @@ -24183,7 +24300,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: { @@ -24205,7 +24322,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: { @@ -24217,14 +24334,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. * @@ -24240,7 +24357,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. */ @@ -24264,7 +24381,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. * @@ -24294,7 +24411,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. * @@ -24310,7 +24427,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. * @@ -24326,7 +24443,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. * @@ -24335,7 +24452,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. * @@ -24344,7 +24461,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: { @@ -24368,14 +24485,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. * @@ -24406,21 +24523,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. */ @@ -24441,7 +24558,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. * @@ -24450,7 +24567,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). * @@ -24459,7 +24576,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. * @@ -24473,21 +24590,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. * @@ -24496,7 +24613,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: { /** @@ -24507,14 +24624,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. * @@ -24523,7 +24640,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. * @@ -24532,7 +24649,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. * @@ -24541,7 +24658,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: { @@ -24553,7 +24670,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: { @@ -24565,7 +24682,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"`. * @@ -24574,7 +24691,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`. * @@ -24583,14 +24700,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. * @@ -24606,7 +24723,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: { @@ -24618,7 +24735,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`. * @@ -24627,7 +24744,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`. * @@ -24636,7 +24753,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 */ @@ -24654,7 +24771,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: { @@ -24666,7 +24783,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. * @@ -24675,7 +24792,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: { @@ -24687,7 +24804,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: { @@ -24697,7 +24814,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: { @@ -24714,14 +24831,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. */ @@ -24733,7 +24850,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: { @@ -24745,7 +24862,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. * @@ -24754,7 +24871,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. * @@ -24763,7 +24880,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. * @@ -24772,7 +24889,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. * @@ -24795,7 +24912,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 }), /** * Updates the current session's live subagent settings after user settings change. The persisted user settings remain the source of truth for future sessions. * @@ -24804,7 +24921,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: { @@ -24816,7 +24933,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. * @@ -24825,7 +24942,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. * @@ -24834,7 +24951,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. * @@ -24843,7 +24960,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. * @@ -24852,7 +24969,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. * @@ -24861,7 +24978,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: { @@ -24878,7 +24995,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: { @@ -24890,7 +25007,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. * @@ -24899,7 +25016,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. * @@ -24908,7 +25025,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. * @@ -24917,7 +25034,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. * @@ -24926,7 +25043,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. * @@ -24935,7 +25052,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. * @@ -24944,7 +25061,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. * @@ -24953,7 +25070,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. * @@ -24969,7 +25086,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: { @@ -24981,7 +25098,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. * @@ -24990,7 +25107,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. * @@ -25006,7 +25123,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. * @@ -25015,7 +25132,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. * @@ -25031,7 +25148,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. * @@ -25040,7 +25157,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. * @@ -25049,7 +25166,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. * @@ -25058,7 +25175,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: { /** @@ -25076,7 +25193,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. * @@ -25085,7 +25202,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. * @@ -25094,7 +25211,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. * @@ -25103,7 +25220,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: { @@ -25115,7 +25232,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. * @@ -25124,7 +25241,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. * @@ -25133,7 +25250,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: { @@ -25145,7 +25262,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. * @@ -25154,7 +25271,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: { @@ -25166,7 +25283,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 }), }, }, /** @@ -25179,7 +25296,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: { /** @@ -25211,7 +25328,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. * @@ -25227,7 +25344,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. * @@ -25236,7 +25353,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. * @@ -25245,7 +25362,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. * @@ -25254,7 +25371,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: { @@ -25266,7 +25383,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: { @@ -25278,7 +25395,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. * @@ -25287,7 +25404,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. * @@ -25296,7 +25413,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. * @@ -25305,7 +25422,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: { @@ -25317,7 +25434,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. * @@ -25326,7 +25443,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. * @@ -25342,7 +25459,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. * @@ -25351,7 +25468,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. * @@ -25381,7 +25498,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: { @@ -25400,7 +25517,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. * @@ -25409,7 +25526,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. * @@ -25418,7 +25535,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. * @@ -25427,7 +25544,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. * @@ -25436,14 +25553,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. * @@ -25452,7 +25569,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). * @@ -25476,7 +25593,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. * @@ -25492,7 +25609,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. * @@ -25501,7 +25618,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: { @@ -25523,7 +25640,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: { @@ -25535,7 +25652,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. */ @@ -25549,7 +25666,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: { @@ -25568,7 +25685,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: { @@ -25587,7 +25704,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 }), }, }; } @@ -25607,7 +25724,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: { /** @@ -25639,14 +25756,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. * @@ -25662,7 +25779,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. * @@ -25681,14 +25798,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 */ @@ -25701,7 +25818,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. * @@ -25710,7 +25827,7 @@ 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 }), }, /** @experimental */ model: { @@ -25722,7 +25839,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: { @@ -25734,7 +25851,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. * @@ -25743,21 +25860,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: { @@ -25769,7 +25886,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: { @@ -25788,7 +25905,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: { @@ -25814,7 +25931,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. * @@ -25823,14 +25940,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. * @@ -25839,7 +25956,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. * @@ -25875,7 +25992,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. * @@ -25884,7 +26001,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. * @@ -25893,7 +26010,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. * @@ -25902,7 +26019,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. * @@ -25911,7 +26028,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/generated/session-events.ts b/nodejs/src/generated/session-events.ts index a7049dd9fc..ad1042c0bb 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -395,6 +395,14 @@ export type AttachmentGitHubReferenceType = | "pr" /** GitHub discussion reference. */ | "discussion"; +/** + * Integrity classification retained for replay. Authenticated cross-session content is untrusted even when its sender identity was authenticated by the host. + */ +export type UserMessageInputIntegrity = "untrusted"; +/** + * Non-human input origin retained for replay. This enum is intentionally narrow; authenticated sender identity and routing authority remain private runtime state. + */ +export type UserMessageInputOrigin = "authenticated-cross-session"; /** * How this user message was delivered to the agentic loop, relative to whether the loop was already running. This is the timing axis only; the message's origin (human vs. system/command/schedule/skill/etc.) is carried separately by `source`. A system-injected message has a delivery too — e.g. a background-task notification waking an idle agent is `idle`, the same mechanism as a human starting a fresh turn. */ @@ -3428,7 +3436,7 @@ export interface FusionCompletedData { turnId: string; } /** - * Session event "user.message". Payload of `user.message` with displayed and model-transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. + * Session event "user.message". Payload of `user.message` with displayed and public transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. */ export interface UserMessageEvent { /** @@ -3458,7 +3466,7 @@ export interface UserMessageEvent { type: "user.message"; } /** - * Payload of `user.message` with displayed and model-transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. + * Payload of `user.message` with displayed and public transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. */ export interface UserMessageData { agentMode?: UserMessageAgentMode; @@ -3470,6 +3478,7 @@ export interface UserMessageData { * The user's message text as displayed in the timeline */ content: string; + crossSession?: UserMessageCrossSessionLabel; delivery?: UserMessageDelivery; /** * CAPI interaction ID for correlating this user message with its turn @@ -3496,7 +3505,7 @@ export interface UserMessageData { */ supportedNativeDocumentMimeTypes?: string[]; /** - * Transformed version of the message sent to the model, with XML wrapping, timestamps, and other augmentations for prompt caching + * Public transformed message content with timestamps and other ordinary augmentations. Private authenticated cross-session envelope metadata is excluded. */ transformedContent?: string; /** @@ -3942,6 +3951,17 @@ export interface AttachmentExtensionContext { */ type: "extension_context"; } +/** + * Minimal replay label for authenticated cross-session input. It carries no sender, principal, reply target, message identifier, continuation target, requested mode, or presentation metadata. + */ +export interface UserMessageCrossSessionLabel { + integrity: UserMessageInputIntegrity; + origin: UserMessageInputOrigin; + /** + * Replay-label schema version. + */ + version: 1; +} /** * Session event "pending_messages.modified". Empty payload; the event signals that the pending message queue has changed */ diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 11e742bee7..5573492458 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -11,7 +11,12 @@ 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 SendSessionMessageErrorCode, +} from "./session.js"; export { defineFactory, FactoryResumeError, isFactoryRunTerminal } from "./factory.js"; export { Canvas, @@ -52,6 +57,15 @@ 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 { + ListMessageableSessionsRequest, + ListMessageableSessionsResult, + MessageableSession, + SendMode, + SendSessionMessageRequest, + SendSessionMessageResult, + SessionMessageDelivery, +} from "./generated/rpc.js"; export type { AskUserVariant, CommandContext, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index eb4c6b7561..617950daa1 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -18,6 +18,10 @@ import type { McpOauthPendingRequestResponse, FactoryLogLine, FactoryRunResult as WireFactoryRunResult, + ListMessageableSessionsRequest, + ListMessageableSessionsResult, + SendSessionMessageRequest, + SendSessionMessageResult, } from "./generated/rpc.js"; import { type Canvas, CanvasError } from "./canvas.js"; import type { OpenCanvasInstance } from "./generated/rpc.js"; @@ -382,6 +386,78 @@ function isFactoryFatalError(error: unknown): boolean { /** Assistant message event - the final response from the assistant. */ export type AssistantMessageEvent = Extract; +/** 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"; /** @@ -725,6 +801,46 @@ 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.rpc.listMessageableSessions(params); + } + + /** + * 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.rpc.sendSessionMessage(params); + } 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..2d36272ea7 --- /dev/null +++ b/nodejs/test/session-list-messageable-sessions.test.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { readFileSync } from "node:fs"; +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; + +if (false) { + const session = null as unknown as CopilotSession; + + // @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" }); +} + +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", + }); + }); + + it("keeps the generated wrapper source-bound", () => { + const generatedRpc = readFileSync( + new URL("../src/generated/rpc.ts", import.meta.url), + "utf8" + ); + + expect(generatedRpc).toContain( + "listMessageableSessions: async (params: ListMessageableSessionsRequest): Promise =>" + ); + expect(generatedRpc).toContain( + 'connection.sendRequest("session.listMessageableSessions", { ...params, sessionId })' + ); + }); +}); + +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..2430db944b --- /dev/null +++ b/nodejs/test/session-send-session-message.test.ts @@ -0,0 +1,245 @@ +/*--------------------------------------------------------------------------------------------- + * 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"; + } +>; +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; + +if (false) { + const session = null as unknown as CopilotSession; + 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" }); +} + +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 the generated session wrapper source-bound", () => { + const generatedRpc = readFileSync( + new URL("../src/generated/rpc.ts", import.meta.url), + "utf8" + ); + + expect(generatedRpc).toContain( + "sendSessionMessage: async (params: SendSessionMessageRequest): Promise =>" + ); + expect(generatedRpc).toContain( + 'connection.sendRequest("session.sendSessionMessage", { ...params, sessionId })' + ); + }); +}); + +void requestMatchesPublicContract; +void resultMatchesPublicContract; +void requestDeliveryMatchesPublicContract; +void resultDeliveryMatchesPublicContract; +void errorCodeMatchesPublicContract; diff --git a/nodejs/tsconfig.test.json b/nodejs/tsconfig.test.json index 2957487505..3f6e4e5488 100644 --- a/nodejs/tsconfig.test.json +++ b/nodejs/tsconfig.test.json @@ -5,6 +5,11 @@ "emitDeclarationOnly": false, "types": ["node"] }, - "include": ["src/**/*", "test/session-event-types.test.ts"], + "include": [ + "src/**/*", + "test/session-list-messageable-sessions.test.ts", + "test/session-event-types.test.ts", + "test/session-send-session-message.test.ts" + ], "exclude": ["node_modules", "dist"] } diff --git a/rust/src/errors.rs b/rust/src/errors.rs index 70f4c14ff1..04cd5a63f0 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] @@ -152,6 +176,14 @@ pub enum SessionErrorKind { /// Session ID returned by the CLI. 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, + }, } impl fmt::Display for SessionErrorKind { @@ -186,6 +218,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}") + } } } } @@ -248,6 +283,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>, @@ -264,6 +300,7 @@ impl Error { kind, error: error.into(), }), + rpc_data: None, backtrace: capture_backtrace(), } } @@ -293,10 +330,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. @@ -357,6 +407,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/generated/api_types.rs b/rust/src/generated/api_types.rs index b1d9896f3e..4ffca64dd9 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -201,6 +201,10 @@ pub mod rpc_methods { pub const SESSION_SUSPEND: &str = "session.suspend"; /// `session.send` pub const SESSION_SEND: &str = "session.send"; + /// `session.sendSessionMessage` + pub const SESSION_SENDSESSIONMESSAGE: &str = "session.sendSessionMessage"; + /// `session.listMessageableSessions` + pub const SESSION_LISTMESSAGEABLESESSIONS: &str = "session.listMessageableSessions"; /// `session.sendMessages` pub const SESSION_SENDMESSAGES: &str = "session.sendMessages"; /// `session.sandbox.getEnforcementStatus` @@ -6767,6 +6771,58 @@ pub struct InterruptMainTurnResult { pub interrupted: bool, } +/// Optional exact-name query for active local messageable sessions. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, 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. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageableSession { + /// Current session name when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Stable session ID to provide to session.sendSessionMessage. + pub session_id: SessionId, + /// 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. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListMessageableSessionsResult { + /// Messageable sessions in deterministic session-ID order. + pub sessions: Vec, +} + /// A request body chunk or cancellation signal. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -15221,6 +15277,43 @@ pub struct SendResult { pub message_id: String, } +/// Parameters for sending one authenticated non-user message from the current bound session to an exact active local session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SendSessionMessageRequest { + /// 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, + /// Exact active local recipient session ID. + pub target_session_id: String, +} + +/// Recipient admission result for an authenticated cross-session message. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SendSessionMessageResult { + /// Actual recipient delivery class at admission. + pub delivery: SessionMessageDelivery, + /// Unique identifier assigned to the admitted message. + pub message_id: String, +} + /// Internal request for sending a system notification. /// ///
@@ -22021,6 +22114,38 @@ pub struct SessionSendResult { pub message_id: String, } +/// Recipient admission result for an authenticated cross-session message. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionSendSessionMessageResult { + /// Actual recipient delivery class at admission. + pub delivery: SessionMessageDelivery, + /// Unique identifier assigned to the admitted message. + pub message_id: String, +} + +/// Sanitized active local sessions available for exact-ID messaging selection. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionListMessageableSessionsResult { + /// Messageable sessions in deterministic session-ID order. + pub sessions: Vec, +} + /// Result of sending zero or more user messages /// ///
@@ -32280,6 +32405,31 @@ pub enum SandboxConfigSource { Unknown, } +/// Actual recipient delivery class for an admitted cross-session message. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, 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, +} + /// Session capability enabled for this session /// ///
@@ -32317,6 +32467,9 @@ pub enum SessionCapability { /// Cross-session history tools and session-store SQL prompt/tool metadata. #[serde(rename = "session-store")] SessionStore, + /// First-party local cross-session messaging tool for a root CLI session. + #[serde(rename = "cross-session-messaging")] + CrossSessionMessaging, /// MCP Apps UI passthrough. #[serde(rename = "mcp-apps")] McpApps, diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 6f5895d856..b71b288e07 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -3320,6 +3320,75 @@ impl<'a> SessionRpc<'a> { Ok(serde_json::from_value(_value)?) } + /// Sends one authenticated non-user message from the current bound session to an exact active local session. Success reports recipient admission, not delegated-work completion. + /// + /// Wire method: `session.sendSessionMessage`. + /// + /// # Parameters + /// + /// * `params` - Parameters for sending one authenticated non-user message from the current bound session to an exact active local session. + /// + /// # Returns + /// + /// Recipient admission result for an authenticated cross-session message. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn send_session_message( + &self, + params: SendSessionMessageRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_SENDSESSIONMESSAGE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Lists active local sessions that the current bound session may select by exact ID for cross-session messaging. This discovery result grants no delivery authority. + /// + /// Wire method: `session.listMessageableSessions`. + /// + /// # Parameters + /// + /// * `params` - Optional exact-name query for active local messageable sessions. + /// + /// # Returns + /// + /// Sanitized active local sessions available for exact-ID messaging selection. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn list_messageable_sessions( + &self, + params: ListMessageableSessionsRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_LISTMESSAGEABLESESSIONS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// 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. /// /// Wire method: `session.sendMessages`. diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 32d44ddc0f..37d314937d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -2146,10 +2146,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/session.rs b/rust/src/session.rs index 75366131a7..b60f833ea1 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -12,8 +12,10 @@ use tracing::{Instrument, warn}; use crate::canvas::CanvasHandler; use crate::generated::api_types::{ - LogRequest, ModelSwitchToRequest, OpenCanvasInstance, PermissionDecisionRequest, - RegisterEventInterestParams, ToolsGetCurrentMetadataResult, rpc_methods, + ListMessageableSessionsRequest, ListMessageableSessionsResult, LogRequest, + ModelSwitchToRequest, OpenCanvasInstance, PermissionDecisionRequest, + RegisterEventInterestParams, SendSessionMessageRequest, SendSessionMessageResult, + ToolsGetCurrentMetadataResult, rpc_methods, }; use crate::generated::session_events::{ CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, McpOauthRequiredData, @@ -39,8 +41,8 @@ use crate::types::{ 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 @@ -48,6 +50,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)) +} + /// Bundle of the per-session callbacks the SDK dispatches to. Built from a /// [`SessionConfig`] / [`ResumeSessionConfig`] at /// [`Client::create_session`] / [`Client::resume_session`] time. Each @@ -373,6 +413,50 @@ 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 { + self.rpc() + .list_messageable_sessions(params.unwrap_or_default()) + .await + } + + /// 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 { + match self.rpc().send_session_message(params).await { + 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 300dddd920..5806946131 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -17,7 +17,8 @@ use github_copilot_sdk::handler::{ }; use github_copilot_sdk::rpc::{ CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, - OpenCanvasInstance, + ListMessageableSessionsRequest, OpenCanvasInstance, SendMode, SendSessionMessageRequest, + SessionMessageDelivery, }; use github_copilot_sdk::session_events::{ ManagedSettingsResolvedSource, McpOauthRequiredData, ReasoningSummary, SessionLimitsConfig, @@ -33,7 +34,10 @@ use github_copilot_sdk::types::{ PermissionDecisionSurface, RequestId, SessionConfig, SessionId, SetModelOptions, Tool, ToolInvocation, ToolResult, }; -use github_copilot_sdk::{Client, ContextTier, ErrorKind, ProtocolErrorKind, tool}; +use github_copilot_sdk::{ + Client, ContextTier, ErrorKind, ProtocolErrorKind, SendSessionMessageErrorCode, + SessionErrorKind, tool, +}; use serde_json::Value; use tokio::io::{AsyncWrite, AsyncWriteExt, duplex}; use tokio::time::timeout; @@ -160,6 +164,20 @@ impl FakeServer { write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; } + async fn respond_error(&mut self, request: &Value, message: &str, data: Value) { + let id = request["id"].as_u64().unwrap(); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "error": { + "code": -32603, + "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", @@ -1915,6 +1933,245 @@ async fn admit_authenticated_cross_session_input_propagates_runtime_errors() { ); } +#[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( + &request, + "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( + &request, + "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 4984816d8d..0a3ad4601b 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -994,7 +994,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 }"; } From 403b5c77c83e12830918d6ab6f7591e28ad71d91 Mon Sep 17 00:00:00 2001 From: dfrysinger <1424648+dfrysinger@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:44:00 -0600 Subject: [PATCH 3/4] Keep cross-session APIs compatible with pinned codegen Define unreleased discovery and delivery wire types in the SDK facade while leaving generated bindings reproducible from the pinned CLI schema. Replace unreachable compile assertions with callable type checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ea23a388-a87d-436d-8110-4f7f543141d6 --- nodejs/src/generated/rpc.ts | 139 +----- nodejs/src/generated/session-events.ts | 335 +------------- nodejs/src/index.ts | 16 +- nodejs/src/session.ts | 60 ++- nodejs/src/types.ts | 2 +- .../session-list-messageable-sessions.test.ts | 22 +- .../test/session-send-session-message.test.ts | 16 +- rust/src/generated/api_types.rs | 194 ++------ rust/src/generated/rpc.rs | 69 --- rust/src/generated/session_events.rs | 419 +----------------- rust/src/rpc.rs | 75 ++++ rust/src/session.rs | 29 +- rust/src/types.rs | 5 +- 13 files changed, 222 insertions(+), 1159 deletions(-) diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 8c87590fb0..d33e70d1c5 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -5,7 +5,7 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; -import type { AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, ModelChangeSource, PermissionDecisionSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js"; +import type { AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js"; /** A value that can be represented losslessly on the SDK JSON wire. */ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }; @@ -2643,6 +2643,22 @@ export type PermissionDecisionOutcome = | "autopilot_denied" /** The response came from an interactive user prompt. */ | "prompted_user"; +/** + * Controlled reason or actor responsible for a permission response. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "PermissionDecisionSource". + */ +/** @experimental */ +export type PermissionDecisionSource = + /** The response followed the assisted-approval judge recommendation. */ + | "assisted_approval" + /** A human supplied the response through an interactive prompt. */ + | "human_response" + /** The host applied a standing policy or override rather than a judge recommendation or human decision. */ + | "host_policy" + /** The host denied the request because no interactive user response was available. */ + | "unattended_fallback"; /** * Client surface that submitted a permission response. * @@ -3032,20 +3048,6 @@ export type SandboxConfigSource = | "unsupported_host" /** A repository policy selected the sandbox state. */ | "repository_policy"; -/** - * Actual recipient delivery class for an admitted cross-session message. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SessionMessageDelivery". - */ -/** @experimental */ -export type SessionMessageDelivery = - /** The recipient was idle and the message started a turn. */ - | "idle" - /** The message entered the active turn's safe steering boundary. */ - | "steering" - /** The message was admitted to the recipient queue. */ - | "queued"; /** * Current authentication information, or null when no authentication is active. * @@ -3080,8 +3082,6 @@ export type SessionCapability = | "elicitation" /** Cross-session history tools and session-store SQL prompt/tool metadata. */ | "session-store" - /** First-party local cross-session messaging tool for a root CLI session. */ - | "cross-session-messaging" /** MCP Apps UI passthrough. */ | "mcp-apps" /** Host-provided canvas rendering support. */ @@ -9956,53 +9956,6 @@ export interface InterruptMainTurnResult { */ interrupted: boolean; } -/** - * Optional exact-name query for active local messageable sessions. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ListMessageableSessionsRequest". - */ -/** @experimental */ -export interface ListMessageableSessionsRequest { - /** - * Optional exact session name query. Matching semantics are owned by the local host. - */ - name?: string; -} -/** - * Sanitized active local sessions available for exact-ID messaging selection. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ListMessageableSessionsResult". - */ -/** @experimental */ -export interface ListMessageableSessionsResult { - /** - * Messageable sessions in deterministic session-ID order. - */ - sessions: MessageableSession[]; -} -/** - * Sanitized active local session available for exact-ID messaging selection. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "MessageableSession". - */ -/** @experimental */ -export interface MessageableSession { - /** - * Stable session ID to provide to session.sendSessionMessage. - */ - sessionId: string; - /** - * Current session name when available. - */ - name?: string; - /** - * Current session summary when available. - */ - summary?: string; -} /** * HTTP headers as a map from lowercased header name to a list of values. Multi-valued headers (e.g. Set-Cookie) preserve all values. * @@ -17980,42 +17933,6 @@ export interface SendResult { */ messageId: string; } -/** - * Parameters for sending one authenticated non-user message from the current bound session to an exact active local session. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SendSessionMessageRequest". - */ -/** @experimental */ -export interface SendSessionMessageRequest { - /** - * Exact active local recipient session ID. - */ - targetSessionId: string; - /** - * Natural-language message content. - */ - content: string; - delivery?: SendMode; -} -/** - * Recipient admission result for an authenticated cross-session message. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "SendSessionMessageResult". - */ -/** @experimental */ -export interface SendSessionMessageResult { - /** - * Unique identifier assigned to the admitted message. - */ - messageId: string; - delivery: SessionMessageDelivery; - /** - * Sanitized recipient display name for presentation only. It is never routing authority. - */ - targetDisplayName?: string; -} /** * Internal request for sending a system notification. * @@ -25049,28 +24966,6 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin */ send: async (params: SendRequest): Promise => connection.sendRequest("session.send", { ...params, sessionId }), - /** - * Sends one authenticated non-user message from the current bound session to an exact active local session. Success reports recipient admission, not delegated-work completion. - * - * @param params Parameters for sending one authenticated non-user message from the current bound session to an exact active local session. - * - * @returns Recipient admission result for an authenticated cross-session message. - * - * @experimental - */ - sendSessionMessage: async (params: SendSessionMessageRequest): Promise => - connection.sendRequest("session.sendSessionMessage", { ...params, sessionId }), - /** - * Lists active local sessions that the current bound session may select by exact ID for cross-session messaging. This discovery result grants no delivery authority. - * - * @param params Optional exact-name query for active local messageable sessions. - * - * @returns Sanitized active local sessions available for exact-ID messaging selection. - * - * @experimental - */ - listMessageableSessions: async (params: ListMessageableSessionsRequest): Promise => - connection.sendRequest("session.listMessageableSessions", { ...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. * diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 9bbcd98c71..b33830dc7d 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -91,10 +91,6 @@ export type SessionEvent = | SystemNotificationEvent | PermissionRequestedEvent | PermissionCompletedEvent - | PermissionCarriedForwardEvent - | PermissionMessageAuthorizationEvent - | PermissionMessageAuthorizationReadEvent - | PermissionMessageAuthorizationDegradedEvent | UserInputRequestedEvent | UserInputCompletedEvent | ElicitationRequestedEvent @@ -495,14 +491,6 @@ export type AttachmentGitHubReferenceType = | "pr" /** GitHub discussion reference. */ | "discussion"; -/** - * Integrity classification retained for replay. Authenticated cross-session content is untrusted even when its sender identity was authenticated by the host. - */ -export type UserMessageInputIntegrity = "untrusted"; -/** - * Non-human input origin retained for replay. This enum is intentionally narrow; authenticated sender identity and routing authority remain private runtime state. - */ -export type UserMessageInputOrigin = "authenticated-cross-session"; /** * How this user message was delivered to the agentic loop, relative to whether the loop was already running. This is the timing axis only; the message's origin (human vs. system/command/schedule/skill/etc.) is carried separately by `source`. A system-injected message has a delivery too — e.g. a background-task notification waking an idle agent is `idle`, the same mechanism as a human starting a fresh turn. */ @@ -941,20 +929,6 @@ export type PermissionPromptRequestPathAccessKind = | "shell" /** Write access to a filesystem path. */ | "write"; -/** - * Controlled reason or actor responsible for a permission response. - */ -export type PermissionDecisionSource = - /** The response followed the assisted-approval judge recommendation. */ - | "assisted_approval" - /** A human supplied the response through an interactive prompt. */ - | "human_response" - /** The host applied a standing policy or override rather than a judge recommendation or human decision. */ - | "host_policy" - /** The host denied the request because no interactive user response was available. */ - | "unattended_fallback" - /** A live authorization record from an earlier human decision in this session contained the proposal, so it ran without another prompt. This is not a new human decision and never mints authority of its own. */ - | "authorization_carry_forward"; /** * The result of the permission request */ @@ -982,15 +956,6 @@ export type UserToolSessionApproval = | UserToolSessionApprovalFactory | UserToolSessionApprovalExtensionPermissionAccess | UserToolSessionApprovalExtensionEnvAccess; -/** - * Which direction a message-backed authorization claim moves authority in. - */ -/** @experimental */ -export type PermissionMessageAuthorizationPolarity = - /** The human's words authorized an effect. */ - | "grant" - /** The human's words refused an effect. */ - | "denial"; /** * Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent. */ @@ -3821,7 +3786,7 @@ export interface FusionCompletedData { turnId: string; } /** - * Session event "user.message". Payload of `user.message` with displayed and public transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. + * Session event "user.message". Payload of `user.message` with displayed and model-transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. */ export interface UserMessageEvent { /** @@ -3851,7 +3816,7 @@ export interface UserMessageEvent { type: "user.message"; } /** - * Payload of `user.message` with displayed and public transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. + * Payload of `user.message` with displayed and model-transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. */ export interface UserMessageData { agentMode?: UserMessageAgentMode; @@ -3863,7 +3828,6 @@ export interface UserMessageData { * The user's message text as displayed in the timeline */ content: string; - crossSession?: UserMessageCrossSessionLabel; delivery?: UserMessageDelivery; /** * CAPI interaction ID for correlating this user message with its turn @@ -3894,7 +3858,7 @@ export interface UserMessageData { */ supportedNativeDocumentMimeTypes?: string[]; /** - * Public transformed message content with timestamps and other ordinary augmentations. Authenticated cross-session input may include a sanitized display-name projection; private identity, reply, principal, message, and routing authority are excluded. + * Transformed version of the message sent to the model, with XML wrapping, timestamps, and other augmentations for prompt caching */ transformedContent?: string; /** @@ -4340,17 +4304,6 @@ export interface AttachmentExtensionContext { */ type: "extension_context"; } -/** - * Minimal replay label for authenticated cross-session input. It carries no sender, principal, reply target, message identifier, continuation target, requested mode, or presentation metadata. - */ -export interface UserMessageCrossSessionLabel { - integrity: UserMessageInputIntegrity; - origin: UserMessageInputOrigin; - /** - * Replay-label schema version. - */ - version: 1; -} /** * Session event "pending_messages.modified". Empty payload; the event signals that the pending message queue has changed */ @@ -8156,20 +8109,6 @@ export interface PermissionRequestShell { * True when the requested escalation is a permissive retry rather than a full bypass: the command re-runs inside the sandbox with its file and process restrictions recording instead of blocking, while the network policy stays enforced. Always accompanied by requestSandboxBypass, so hosts that do not recognize this field still treat the request as the escalation it is. Hosts that do recognize it must not describe the command as running outside the sandbox, which would overstate the privilege being granted. */ requestSandboxPermissive?: boolean; - /** - * Runtime-resolved canonical object each possiblePaths entry names, keyed by the requested spelling, used for authorization identity checks. Internal and experimental; clients should continue to display possiblePaths. - * - * @experimental - */ - resolvedPaths?: { - [k: string]: string | undefined; - }; - /** - * Runtime-resolved canonical working directory the command runs in, used for authorization identity checks. Internal and experimental; clients should not display it. - * - * @experimental - */ - resolvedWorkingDirectory?: string; /** * Tool call ID that triggered this permission request */ @@ -8254,12 +8193,6 @@ export interface PermissionRequestWrite { * Justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. */ requestSandboxBypassReason?: string; - /** - * Runtime-resolved canonical path used for authorization identity checks. Internal and experimental; clients should continue to display fileName. - * - * @experimental - */ - resolvedPath?: string; /** * Tool call ID that triggered this permission request */ @@ -8293,12 +8226,6 @@ export interface PermissionRequestRead { * What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. */ requestSandboxBypassReason?: string; - /** - * Runtime-resolved canonical path used for authorization identity checks. Internal and experimental; clients should continue to display path. - * - * @experimental - */ - resolvedPath?: string; /** * Tool call ID that triggered this permission request */ @@ -8729,12 +8656,6 @@ export interface PermissionPromptRequestWrite { * Complete new file contents for newly created files */ newFileContents?: string; - /** - * Runtime-resolved canonical path used for authorization identity checks. Internal and experimental; clients should continue to display fileName. - * - * @experimental - */ - resolvedPath?: string; /** * Tool call ID that triggered this permission request */ @@ -8766,12 +8687,6 @@ export interface PermissionPromptRequestRead { * Path of the file or directory being read */ path: string; - /** - * Runtime-resolved canonical path used for authorization identity checks. Internal and experimental; clients should continue to display path. - * - * @experimental - */ - resolvedPath?: string; /** * Tool call ID that triggered this permission request */ @@ -9181,12 +9096,6 @@ export interface PermissionCompletedEvent { * Permission request completion notification signaling UI dismissal */ export interface PermissionCompletedData { - /** - * Who decided this permission request. Absent on completions recorded before this field existed, which consumers must treat as "not a human decision" rather than assuming one. Authorization records are minted only for `human_response`; an assisted-approval verdict, a host policy, an unattended fallback, and a hook resolution all produce the same `result` a person does, so this is the only field that distinguishes them. - * - * @experimental - */ - decisionSource?: PermissionDecisionSource; /** * Request ID of the resolved permission request; clients should dismiss any UI for this request */ @@ -9469,244 +9378,6 @@ export interface PermissionDeniedByPermissionRequestHook { */ message?: string; } -/** - * Session event "permission.carriedForward". Records that a live authorization record from an earlier human decision in this session contained a permission proposal, so it ran without another prompt. This mints no authority: it accounts for one more effect against the prior grant, which is what lets a replayed session agree with the live one about how much of that grant is left. - */ -/** @experimental */ -export interface PermissionCarriedForwardEvent { - /** - * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. - */ - agentId?: string; - data: PermissionCarriedForwardData; - /** - * When true, the event is transient and not persisted to the session event log on disk - */ - ephemeral?: boolean; - /** - * Unique event identifier (UUID v4), generated when the event is emitted - */ - id: string; - /** - * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. - */ - parentId: string | null; - /** - * ISO 8601 timestamp when the event was created - */ - timestamp: string; - /** - * Type discriminator. Always "permission.carriedForward". - */ - type: "permission.carriedForward"; -} -/** - * Records that a live authorization record from an earlier human decision in this session contained a permission proposal, so it ran without another prompt. This mints no authority: it accounts for one more effect against the prior grant, which is what lets a replayed session agree with the live one about how much of that grant is left. - */ -/** @experimental */ -export interface PermissionCarriedForwardData { - /** - * Always `authorization_carry_forward`. Stated explicitly so a consumer reading this event cannot mistake it for a human, host-policy, or assisted-approval decision. - * - * @experimental - */ - decisionSource: PermissionDecisionSource; - /** - * Identity of the prior authorization record that contained the proposal. - * - * @experimental - */ - recordId: string; - /** - * Authorization edge minted for this admission. Not a prompt id: no prompt was raised, so no client should expect a request with this id. - * - * @experimental - */ - requestId: string; - /** - * Tool call this admission authorizes. Its execution receipts the prior grant, which is how a single-effect approval is spent rather than carried forward again. - * - * @experimental - */ - toolCallId: string; -} -/** - * Session event "permission.messageAuthorization". Freezes one blinded, verbatim-verified authorization claim the runtime minted from a human user message, so a resumed session re-establishes the same grant deterministically instead of re-running the extraction model. This mints no authority on its own: it records what a blinded proposer pointed at and the trusted discriminator the runtime established, and deterministic establishment runs on replay. Persisted so recorded authority survives compaction and process resume. - */ -/** @experimental */ -export interface PermissionMessageAuthorizationEvent { - /** - * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. - */ - agentId?: string; - data: PermissionMessageAuthorizationData; - /** - * When true, the event is transient and not persisted to the session event log on disk - */ - ephemeral?: boolean; - /** - * Unique event identifier (UUID v4), generated when the event is emitted - */ - id: string; - /** - * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. - */ - parentId: string | null; - /** - * ISO 8601 timestamp when the event was created - */ - timestamp: string; - /** - * Type discriminator. Always "permission.messageAuthorization". - */ - type: "permission.messageAuthorization"; -} -/** - * Freezes one blinded, verbatim-verified authorization claim the runtime minted from a human user message, so a resumed session re-establishes the same grant deterministically instead of re-running the extraction model. This mints no authority on its own: it records what a blinded proposer pointed at and the trusted discriminator the runtime established, and deterministic establishment runs on replay. Persisted so recorded authority survives compaction and process resume. - */ -/** @experimental */ -export interface PermissionMessageAuthorizationData { - /** - * The kind of effect authorized, as an action-class identifier. - * - * @experimental - */ - actionClass: string; - /** - * Whether the claim granted or denied authority. - * - * @experimental - */ - polarity: PermissionMessageAuthorizationPolarity; - /** - * Deterministic identity of the record, derived from the turn and span offsets so re-extracting the same span mints nothing new. - * - * @experimental - */ - recordId: string; - /** - * End byte offset of the authorizing span within the turn. - * - * @experimental - */ - spanEnd: number; - /** - * Start byte offset of the authorizing span within the turn. - * - * @experimental - */ - spanStart: number; - /** - * Concrete named targets that appear verbatim inside the span. - * - * @experimental - */ - targetMembers?: string[]; - /** - * The task the permission is scoped to, when the human named one. - * - * @experimental - */ - task?: string; - /** - * The human turn the quoted span was read from. - * - * @experimental - */ - turnIndex: number; - /** - * The trusted version discriminator, when one exists. Exact shell-command grants carry the byte-identical commands grounded in the human span; world-derived classes carry a file object, remote tip, or runner only when that state was captured safely. An opaque object mirroring the runtime's adjacently-tagged resolution. - * - * @experimental - */ - world?: JsonValue; -} -/** - * Session event "permission.messageAuthorizationRead". Records that one human turn has been read by the blinded authorization proposer, whether or not it minted anything, so a resumed session does not re-run the extraction model on a turn the live session already read. Persisted purely to avoid wasted model calls across resume; it is never a correctness mechanism. - */ -/** @experimental */ -export interface PermissionMessageAuthorizationReadEvent { - /** - * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. - */ - agentId?: string; - data: PermissionMessageAuthorizationReadData; - /** - * When true, the event is transient and not persisted to the session event log on disk - */ - ephemeral?: boolean; - /** - * Unique event identifier (UUID v4), generated when the event is emitted - */ - id: string; - /** - * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. - */ - parentId: string | null; - /** - * ISO 8601 timestamp when the event was created - */ - timestamp: string; - /** - * Type discriminator. Always "permission.messageAuthorizationRead". - */ - type: "permission.messageAuthorizationRead"; -} -/** - * Records that one human turn has been read by the blinded authorization proposer, whether or not it minted anything, so a resumed session does not re-run the extraction model on a turn the live session already read. Persisted purely to avoid wasted model calls across resume; it is never a correctness mechanism. - */ -/** @experimental */ -export interface PermissionMessageAuthorizationReadData { - /** - * The human turn that was read by the proposer. - * - * @experimental - */ - turnIndex: number; -} -/** - * Session event "permission.messageAuthorizationDegraded". Records that message-backed authorization could not safely represent one human turn before compaction. The runtime may compact the original message after this marker is durable, but message-derived carry-forward and assisted auto-approval remain disabled for the rest of the session so subsequent commands continue through the ordinary permission prompt. - */ -/** @experimental */ -export interface PermissionMessageAuthorizationDegradedEvent { - /** - * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. - */ - agentId?: string; - data: PermissionMessageAuthorizationDegradedData; - /** - * When true, the event is transient and not persisted to the session event log on disk - */ - ephemeral?: boolean; - /** - * Unique event identifier (UUID v4), generated when the event is emitted - */ - id: string; - /** - * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. - */ - parentId: string | null; - /** - * ISO 8601 timestamp when the event was created - */ - timestamp: string; - /** - * Type discriminator. Always "permission.messageAuthorizationDegraded". - */ - type: "permission.messageAuthorizationDegraded"; -} -/** - * Records that message-backed authorization could not safely represent one human turn before compaction. The runtime may compact the original message after this marker is durable, but message-derived carry-forward and assisted auto-approval remain disabled for the rest of the session so subsequent commands continue through the ordinary permission prompt. - */ -/** @experimental */ -export interface PermissionMessageAuthorizationDegradedData { - /** - * The human turn that could not be represented safely. - * - * @experimental - */ - turnIndex: number; -} /** * Session event "user_input.requested". User input request notification with question and optional predefined choices */ diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 72f6aee268..a8fba33a08 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -15,7 +15,13 @@ 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 { @@ -57,15 +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 { - ListMessageableSessionsRequest, - ListMessageableSessionsResult, - MessageableSession, - SendMode, - SendSessionMessageRequest, - SendSessionMessageResult, - SessionMessageDelivery, -} from "./generated/rpc.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 96113f294a..83f818a6d8 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -18,11 +18,8 @@ import type { McpOauthPendingRequestResponse, FactoryLogLine, FactoryRunResult as WireFactoryRunResult, - ListMessageableSessionsRequest, - ListMessageableSessionsResult, - SendSessionMessageRequest, - SendSessionMessageResult, ModelSwitchAutoTierResult, + SendMode, } from "./generated/rpc.js"; import { type Canvas, CanvasError } from "./canvas.js"; import type { OpenCanvasInstance } from "./generated/rpc.js"; @@ -388,6 +385,51 @@ 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"; @@ -816,7 +858,10 @@ export class CopilotSession { async listMessageableSessions( params: ListMessageableSessionsRequest = {} ): Promise { - return this.rpc.listMessageableSessions(params); + return this.connection.sendRequest("session.listMessageableSessions", { + ...params, + sessionId: this.sessionId, + }); } /** @@ -830,7 +875,10 @@ export class CopilotSession { */ async sendSessionMessage(params: SendSessionMessageRequest): Promise { try { - return await this.rpc.sendSessionMessage(params); + return await this.connection.sendRequest("session.sendSessionMessage", { + ...params, + sessionId: this.sessionId, + }); } catch (error) { if (error instanceof ResponseError) { const translated = parseSendSessionMessageErrorData(error.data); diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 9c4258d9c9..efff9b47df 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -92,10 +92,10 @@ export type { LlmInferenceHeaders } from "./generated/rpc.js"; export type { PermissionDecisionContext, PermissionDecisionOutcome, + PermissionDecisionSource, PermissionDecisionSurface, PermissionResponseCapability, } from "./generated/rpc.js"; -export type { PermissionDecisionSource } from "./generated/session-events.js"; export type { CopilotRequestContext } from "./copilotRequestHandler.js"; export { CopilotRequestHandler, diff --git a/nodejs/test/session-list-messageable-sessions.test.ts b/nodejs/test/session-list-messageable-sessions.test.ts index 2d36272ea7..4c818ea75a 100644 --- a/nodejs/test/session-list-messageable-sessions.test.ts +++ b/nodejs/test/session-list-messageable-sessions.test.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { readFileSync } from "node:fs"; import { describe, expect, it, vi } from "vitest"; import { CopilotSession, @@ -40,14 +39,13 @@ type ResultMatchesPublicContract = AssertEqual< >; const resultMatchesPublicContract: ResultMatchesPublicContract = true; -if (false) { - const session = null as unknown as CopilotSession; - +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 () => { @@ -94,20 +92,6 @@ describe("CopilotSession.listMessageableSessions", () => { name: "Research", }); }); - - it("keeps the generated wrapper source-bound", () => { - const generatedRpc = readFileSync( - new URL("../src/generated/rpc.ts", import.meta.url), - "utf8" - ); - - expect(generatedRpc).toContain( - "listMessageableSessions: async (params: ListMessageableSessionsRequest): Promise =>" - ); - expect(generatedRpc).toContain( - 'connection.sendRequest("session.listMessageableSessions", { ...params, sessionId })' - ); - }); }); void requestMatchesPublicContract; diff --git a/nodejs/test/session-send-session-message.test.ts b/nodejs/test/session-send-session-message.test.ts index 7120c21d3f..9785990ab6 100644 --- a/nodejs/test/session-send-session-message.test.ts +++ b/nodejs/test/session-send-session-message.test.ts @@ -53,8 +53,7 @@ type ErrorCodeMatchesPublicContract = AssertEqual< >; const errorCodeMatchesPublicContract: ErrorCodeMatchesPublicContract = true; -if (false) { - const session = null as unknown as CopilotSession; +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. @@ -69,7 +68,8 @@ if (false) { 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 () => { @@ -224,18 +224,14 @@ describe("CopilotSession.sendSessionMessage", () => { expect(error).not.toBeInstanceOf(SendSessionMessageError); }); - it("keeps the generated session wrapper source-bound", () => { + it("keeps generated session wrappers source-bound", () => { const generatedRpc = readFileSync( new URL("../src/generated/rpc.ts", import.meta.url), "utf8" ); - expect(generatedRpc).toContain( - "sendSessionMessage: async (params: SendSessionMessageRequest): Promise =>" - ); - expect(generatedRpc).toContain( - 'connection.sendRequest("session.sendSessionMessage", { ...params, sessionId })' - ); + expect(generatedRpc).toContain("{ ...params, sessionId }"); + expect(generatedRpc).not.toContain("{ sessionId, ...params }"); }); }); diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 123ff869a7..7500c30745 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -12,10 +12,9 @@ use serde::{Deserialize, Serialize}; use super::session_events::{ AbortReason, AgentModelPolicy, AutoTier, ContextTier, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, - ModelChangeSource, OmittedBinaryOmittedReason, PermissionDecisionSource, PermissionMode, - PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, - SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, - UserToolSessionApproval, Verbosity, + ModelChangeSource, OmittedBinaryOmittedReason, PermissionMode, PermissionPromptRequest, + PermissionRule, ReasoningSummary, RemediationAction, SessionLimitsConfig, SessionMode, + ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity, }; use crate::types::{RequestId, SessionEvent, SessionId}; @@ -210,10 +209,6 @@ pub mod rpc_methods { pub const SESSION_SUSPEND: &str = "session.suspend"; /// `session.send` pub const SESSION_SEND: &str = "session.send"; - /// `session.sendSessionMessage` - pub const SESSION_SENDSESSIONMESSAGE: &str = "session.sendSessionMessage"; - /// `session.listMessageableSessions` - pub const SESSION_LISTMESSAGEABLESESSIONS: &str = "session.listMessageableSessions"; /// `session.sendMessages` pub const SESSION_SENDMESSAGES: &str = "session.sendMessages"; /// `session.sandbox.getEnforcementStatus` @@ -7128,58 +7123,6 @@ pub struct InterruptMainTurnResult { pub interrupted: bool, } -/// Optional exact-name query for active local messageable sessions. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, 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. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MessageableSession { - /// Current session name when available. - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - /// Stable session ID to provide to session.sendSessionMessage. - pub session_id: SessionId, - /// 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. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ListMessageableSessionsResult { - /// Messageable sessions in deterministic session-ID order. - pub sessions: Vec, -} - /// A request body chunk or cancellation signal. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -15824,46 +15767,6 @@ pub struct SendResult { pub message_id: String, } -/// Parameters for sending one authenticated non-user message from the current bound session to an exact active local session. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SendSessionMessageRequest { - /// 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, - /// Exact active local recipient session ID. - pub target_session_id: String, -} - -/// Recipient admission result for an authenticated cross-session message. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SendSessionMessageResult { - /// Actual recipient delivery class at admission. - pub delivery: SessionMessageDelivery, - /// Unique identifier assigned to the admitted message. - pub message_id: String, - /// Sanitized recipient display name for presentation only. It is never routing authority. - #[serde(skip_serializing_if = "Option::is_none")] - pub target_display_name: Option, -} - /// Internal request for sending a system notification. /// ///
@@ -23109,41 +23012,6 @@ pub struct SessionSendResult { pub message_id: String, } -/// Recipient admission result for an authenticated cross-session message. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionSendSessionMessageResult { - /// Actual recipient delivery class at admission. - pub delivery: SessionMessageDelivery, - /// Unique identifier assigned to the admitted message. - pub message_id: String, - /// Sanitized recipient display name for presentation only. It is never routing authority. - #[serde(skip_serializing_if = "Option::is_none")] - pub target_display_name: Option, -} - -/// Sanitized active local sessions available for exact-ID messaging selection. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionListMessageableSessionsResult { - /// Messageable sessions in deterministic session-ID order. - pub sessions: Vec, -} - /// Result of sending zero or more user messages /// ///
@@ -33110,6 +32978,34 @@ pub enum PermissionResponseCapability { Unknown, } +/// Controlled reason or actor responsible for a permission response. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionDecisionSource { + /// The response followed the assisted-approval judge recommendation. + #[serde(rename = "assisted_approval")] + AssistedApproval, + /// A human supplied the response through an interactive prompt. + #[serde(rename = "human_response")] + HumanResponse, + /// The host applied a standing policy or override rather than a judge recommendation or human decision. + #[serde(rename = "host_policy")] + HostPolicy, + /// The host denied the request because no interactive user response was available. + #[serde(rename = "unattended_fallback")] + UnattendedFallback, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Client surface that submitted a permission response. /// ///
@@ -33839,31 +33735,6 @@ pub enum SandboxConfigSource { Unknown, } -/// Actual recipient delivery class for an admitted cross-session message. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, 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, -} - /// Session capability enabled for this session /// ///
@@ -33901,9 +33772,6 @@ pub enum SessionCapability { /// Cross-session history tools and session-store SQL prompt/tool metadata. #[serde(rename = "session-store")] SessionStore, - /// First-party local cross-session messaging tool for a root CLI session. - #[serde(rename = "cross-session-messaging")] - CrossSessionMessaging, /// MCP Apps UI passthrough. #[serde(rename = "mcp-apps")] McpApps, diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index f0b8196295..789514b708 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -3455,75 +3455,6 @@ impl<'a> SessionRpc<'a> { Ok(serde_json::from_value(_value)?) } - /// Sends one authenticated non-user message from the current bound session to an exact active local session. Success reports recipient admission, not delegated-work completion. - /// - /// Wire method: `session.sendSessionMessage`. - /// - /// # Parameters - /// - /// * `params` - Parameters for sending one authenticated non-user message from the current bound session to an exact active local session. - /// - /// # Returns - /// - /// Recipient admission result for an authenticated cross-session message. - /// - ///
- /// - /// **Experimental.** This API is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. Pin both the - /// SDK and CLI versions if your code depends on it. - /// - ///
- pub async fn send_session_message( - &self, - params: SendSessionMessageRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); - let _value = self - .session - .client() - .call(rpc_methods::SESSION_SENDSESSIONMESSAGE, Some(wire_params)) - .await?; - Ok(serde_json::from_value(_value)?) - } - - /// Lists active local sessions that the current bound session may select by exact ID for cross-session messaging. This discovery result grants no delivery authority. - /// - /// Wire method: `session.listMessageableSessions`. - /// - /// # Parameters - /// - /// * `params` - Optional exact-name query for active local messageable sessions. - /// - /// # Returns - /// - /// Sanitized active local sessions available for exact-ID messaging selection. - /// - ///
- /// - /// **Experimental.** This API is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. Pin both the - /// SDK and CLI versions if your code depends on it. - /// - ///
- pub async fn list_messageable_sessions( - &self, - params: ListMessageableSessionsRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); - let _value = self - .session - .client() - .call( - rpc_methods::SESSION_LISTMESSAGEABLESESSIONS, - Some(wire_params), - ) - .await?; - Ok(serde_json::from_value(_value)?) - } - /// 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. /// /// Wire method: `session.sendMessages`. diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 879c7dc716..611f04d40f 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -267,42 +267,6 @@ pub enum SessionEventType { PermissionRequested, #[serde(rename = "permission.completed")] PermissionCompleted, - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "permission.carriedForward")] - PermissionCarriedForward, - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "permission.messageAuthorization")] - PermissionMessageAuthorization, - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "permission.messageAuthorizationRead")] - PermissionMessageAuthorizationRead, - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "permission.messageAuthorizationDegraded")] - PermissionMessageAuthorizationDegraded, #[serde(rename = "user_input.requested")] UserInputRequested, #[serde(rename = "user_input.completed")] @@ -757,42 +721,6 @@ pub enum SessionEventData { PermissionRequested(PermissionRequestedData), #[serde(rename = "permission.completed")] PermissionCompleted(PermissionCompletedData), - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "permission.carriedForward")] - PermissionCarriedForward(PermissionCarriedForwardData), - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "permission.messageAuthorization")] - PermissionMessageAuthorization(PermissionMessageAuthorizationData), - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "permission.messageAuthorizationRead")] - PermissionMessageAuthorizationRead(PermissionMessageAuthorizationReadData), - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "permission.messageAuthorizationDegraded")] - PermissionMessageAuthorizationDegraded(PermissionMessageAuthorizationDegradedData), #[serde(rename = "user_input.requested")] UserInputRequested(UserInputRequestedData), #[serde(rename = "user_input.completed")] @@ -2313,19 +2241,7 @@ pub struct SessionFusionCompletedData { pub turn_id: String, } -/// Minimal replay label for authenticated cross-session input. It carries no sender, principal, reply target, message identifier, continuation target, requested mode, or presentation metadata. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UserMessageCrossSessionLabel { - /// Input integrity class. - pub integrity: UserMessageInputIntegrity, - /// Non-human origin class. - pub origin: UserMessageInputOrigin, - /// Replay-label schema version. - pub version: serde_json::Value, -} - -/// Session event "user.message". Payload of `user.message` with displayed and public transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. +/// Session event "user.message". Payload of `user.message` with displayed and model-transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UserMessageData { @@ -2337,9 +2253,6 @@ pub struct UserMessageData { pub attachments: Option>, /// The user's message text as displayed in the timeline pub content: String, - /// Optional strict replay label for authenticated cross-session input. Sender, reply, requested-mode, presentation, and continuation data are excluded from the event. - #[serde(skip_serializing_if = "Option::is_none")] - pub cross_session: Option, /// How this message was delivered to the agentic loop relative to loop state (idle-start vs. steering/queued while busy). The timing axis; combine with `source` (origin) for the full picture. Used for telemetry attribution. #[serde(skip_serializing_if = "Option::is_none")] pub delivery: Option, @@ -2364,7 +2277,7 @@ pub struct UserMessageData { /// Normalized document MIME types that were sent natively instead of through tagged_files XML #[serde(skip_serializing_if = "Option::is_none")] pub supported_native_document_mime_types: Option>, - /// Public transformed message content with timestamps and other ordinary augmentations. Authenticated cross-session input may include a sanitized display-name projection; private identity, reply, principal, message, and routing authority are excluded. + /// Transformed version of the message sent to the model, with XML wrapping, timestamps, and other augmentations for prompt caching #[serde(skip_serializing_if = "Option::is_none")] pub transformed_content: Option, /// The agent-loop turn ID that consumed this message; absent when no agent-loop turn consumed it @@ -4593,26 +4506,6 @@ pub struct PermissionRequestShell { /// True when the requested escalation is a permissive retry rather than a full bypass: the command re-runs inside the sandbox with its file and process restrictions recording instead of blocking, while the network policy stays enforced. Always accompanied by requestSandboxBypass, so hosts that do not recognize this field still treat the request as the escalation it is. Hosts that do recognize it must not describe the command as running outside the sandbox, which would overstate the privilege being granted. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_permissive: Option, - /// Runtime-resolved canonical object each possiblePaths entry names, keyed by the requested spelling, used for authorization identity checks. Internal and experimental; clients should continue to display possiblePaths. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub resolved_paths: Option>, - /// Runtime-resolved canonical working directory the command runs in, used for authorization identity checks. Internal and experimental; clients should not display it. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub resolved_working_directory: Option, /// Tool call ID that triggered this permission request #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, @@ -4647,16 +4540,6 @@ pub struct PermissionRequestWrite { /// Justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass_reason: Option, - /// Runtime-resolved canonical path used for authorization identity checks. Internal and experimental; clients should continue to display fileName. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub resolved_path: Option, /// Tool call ID that triggered this permission request #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, @@ -4681,16 +4564,6 @@ pub struct PermissionRequestRead { /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass_reason: Option, - /// Runtime-resolved canonical path used for authorization identity checks. Internal and experimental; clients should continue to display path. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub resolved_path: Option, /// Tool call ID that triggered this permission request #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, @@ -5065,16 +4938,6 @@ pub struct PermissionPromptRequestWrite { /// Complete new file contents for newly created files #[serde(skip_serializing_if = "Option::is_none")] pub new_file_contents: Option, - /// Runtime-resolved canonical path used for authorization identity checks. Internal and experimental; clients should continue to display fileName. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub resolved_path: Option, /// Tool call ID that triggered this permission request #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, @@ -5103,16 +4966,6 @@ pub struct PermissionPromptRequestRead { pub managed_approval_required: Option, /// Path of the file or directory being read pub path: String, - /// Runtime-resolved canonical path used for authorization identity checks. Internal and experimental; clients should continue to display path. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub resolved_path: Option, /// Tool call ID that triggered this permission request #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, @@ -5698,16 +5551,6 @@ pub struct PermissionDeniedByPermissionRequestHook { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PermissionCompletedData { - /// Who decided this permission request. Absent on completions recorded before this field existed, which consumers must treat as "not a human decision" rather than assuming one. Authorization records are minted only for `human_response`; an assisted-approval verdict, a host policy, an unattended fallback, and a hook resolution all produce the same `result` a person does, so this is the only field that distinguishes them. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub decision_source: Option, /// Request ID of the resolved permission request; clients should dismiss any UI for this request pub request_id: RequestId, /// The result of the permission request @@ -5717,196 +5560,6 @@ pub struct PermissionCompletedData { pub tool_call_id: Option, } -/// Session event "permission.carriedForward". Records that a live authorization record from an earlier human decision in this session contained a permission proposal, so it ran without another prompt. This mints no authority: it accounts for one more effect against the prior grant, which is what lets a replayed session agree with the live one about how much of that grant is left. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PermissionCarriedForwardData { - /// Always `authorization_carry_forward`. Stated explicitly so a consumer reading this event cannot mistake it for a human, host-policy, or assisted-approval decision. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- pub decision_source: PermissionDecisionSource, - /// Identity of the prior authorization record that contained the proposal. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- pub record_id: String, - /// Authorization edge minted for this admission. Not a prompt id: no prompt was raised, so no client should expect a request with this id. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- pub request_id: RequestId, - /// Tool call this admission authorizes. Its execution receipts the prior grant, which is how a single-effect approval is spent rather than carried forward again. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- pub tool_call_id: String, -} - -/// Session event "permission.messageAuthorization". Freezes one blinded, verbatim-verified authorization claim the runtime minted from a human user message, so a resumed session re-establishes the same grant deterministically instead of re-running the extraction model. This mints no authority on its own: it records what a blinded proposer pointed at and the trusted discriminator the runtime established, and deterministic establishment runs on replay. Persisted so recorded authority survives compaction and process resume. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PermissionMessageAuthorizationData { - /// The kind of effect authorized, as an action-class identifier. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- pub action_class: String, - /// Whether the claim granted or denied authority. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- pub polarity: PermissionMessageAuthorizationPolarity, - /// Deterministic identity of the record, derived from the turn and span offsets so re-extracting the same span mints nothing new. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- pub record_id: String, - /// End byte offset of the authorizing span within the turn. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- pub span_end: i64, - /// Start byte offset of the authorizing span within the turn. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- pub span_start: i64, - /// Concrete named targets that appear verbatim inside the span. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub target_members: Option>, - /// The task the permission is scoped to, when the human named one. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub task: Option, - /// The human turn the quoted span was read from. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- pub turn_index: i64, - /// The trusted version discriminator, when one exists. Exact shell-command grants carry the byte-identical commands grounded in the human span; world-derived classes carry a file object, remote tip, or runner only when that state was captured safely. An opaque object mirroring the runtime's adjacently-tagged resolution. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub world: Option, -} - -/// Session event "permission.messageAuthorizationRead". Records that one human turn has been read by the blinded authorization proposer, whether or not it minted anything, so a resumed session does not re-run the extraction model on a turn the live session already read. Persisted purely to avoid wasted model calls across resume; it is never a correctness mechanism. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PermissionMessageAuthorizationReadData { - /// The human turn that was read by the proposer. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- pub turn_index: i64, -} - -/// Session event "permission.messageAuthorizationDegraded". Records that message-backed authorization could not safely represent one human turn before compaction. The runtime may compact the original message after this marker is durable, but message-derived carry-forward and assisted auto-approval remain disabled for the rest of the session so subsequent commands continue through the ordinary permission prompt. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PermissionMessageAuthorizationDegradedData { - /// The human turn that could not be represented safely. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- pub turn_index: i64, -} - /// Session event "user_input.requested". User input request notification with question and optional predefined choices #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -7610,28 +7263,6 @@ pub enum UserMessageAgentMode { Unknown, } -/// Integrity classification retained for replay. Authenticated cross-session content is untrusted even when its sender identity was authenticated by the host. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum UserMessageInputIntegrity { - #[serde(rename = "untrusted")] - Untrusted, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Non-human input origin retained for replay. This enum is intentionally narrow; authenticated sender identity and routing authority remain private runtime state. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum UserMessageInputOrigin { - #[serde(rename = "authenticated-cross-session")] - AuthenticatedCrossSession, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// How this user message was delivered to the agentic loop, relative to whether the loop was already running. This is the timing axis only; the message's origin (human vs. system/command/schedule/skill/etc.) is carried separately by `source`. A system-injected message has a delivery too — e.g. a background-task notification waking an idle agent is `idle`, the same mechanism as a human starting a fresh turn. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum UserMessageDelivery { @@ -8643,30 +8274,6 @@ pub enum PermissionPromptRequest { ExtensionEnvAccess(PermissionPromptRequestExtensionEnvAccess), } -/// Controlled reason or actor responsible for a permission response. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionDecisionSource { - /// The response followed the assisted-approval judge recommendation. - #[serde(rename = "assisted_approval")] - AssistedApproval, - /// A human supplied the response through an interactive prompt. - #[serde(rename = "human_response")] - HumanResponse, - /// The host applied a standing policy or override rather than a judge recommendation or human decision. - #[serde(rename = "host_policy")] - HostPolicy, - /// The host denied the request because no interactive user response was available. - #[serde(rename = "unattended_fallback")] - UnattendedFallback, - /// A live authorization record from an earlier human decision in this session contained the proposal, so it ran without another prompt. This is not a new human decision and never mints authority of its own. - #[serde(rename = "authorization_carry_forward")] - AuthorizationCarryForward, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// The permission request was approved #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum PermissionApprovedKind { @@ -8852,28 +8459,6 @@ pub enum PermissionResult { DeniedByPermissionRequestHook(PermissionDeniedByPermissionRequestHook), } -/// Which direction a message-backed authorization claim moves authority in. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionMessageAuthorizationPolarity { - /// The human's words authorized an effect. - #[serde(rename = "grant")] - Grant, - /// The human's words refused an effect. - #[serde(rename = "denial")] - Denial, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ElicitationRequestedMode { 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 7b46807c66..c21f031604 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -14,11 +14,9 @@ use tracing::{Instrument, error, warn}; use crate::canvas::CanvasHandler; use crate::generated::api_types::{ - ListMessageableSessionsRequest, ListMessageableSessionsResult, LogRequest, - ModelSwitchAutoTierRequest, ModelSwitchAutoTierResult, ModelSwitchToRequest, + LogRequest, ModelSwitchAutoTierRequest, ModelSwitchAutoTierResult, ModelSwitchToRequest, OpenCanvasInstance, PermissionDecisionRequest, RegisterEventInterestParams, - SendSessionMessageRequest, SendSessionMessageResult, ToolsGetCurrentMetadataResult, - rpc_methods, + ToolsGetCurrentMetadataResult, rpc_methods, }; use crate::generated::session_events::{ CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, McpOauthRequiredData, @@ -31,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; @@ -567,9 +569,13 @@ impl Session { &self, params: Option, ) -> Result { - self.rpc() - .list_messageable_sessions(params.unwrap_or_default()) - .await + 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. @@ -582,7 +588,14 @@ impl Session { &self, params: SendSessionMessageRequest, ) -> Result { - match self.rpc().send_session_message(params).await { + 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)) = diff --git a/rust/src/types.rs b/rust/src/types.rs index eb0bbafb36..8a0de1e7fb 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -6427,10 +6427,9 @@ pub use crate::generated::api_types::{ ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision, ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision, PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome, - PermissionDecisionReject, PermissionDecisionSurface, PermissionDecisionUserNotAvailable, - PermissionResponseCapability, + PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface, + PermissionDecisionUserNotAvailable, PermissionResponseCapability, }; -pub use crate::generated::session_events::PermissionDecisionSource; /// Permission categories the CLI may request approval for. /// From d8aaeed60fee20ec1f33f18d36d00a7e8936d134 Mon Sep 17 00:00:00 2001 From: dfrysinger <1424648+dfrysinger@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:46:12 -0600 Subject: [PATCH 4/4] Revert "Rust SDK: Add authenticated cross-session input producer" This reverts commit dc77d92680cb8fc303b5f4788176e6c1f267d89d. --- rust/src/session.rs | 38 ----- rust/src/types.rs | 301 ------------------------------------- rust/tests/session_test.rs | 224 +-------------------------- 3 files changed, 6 insertions(+), 557 deletions(-) diff --git a/rust/src/session.rs b/rust/src/session.rs index c21f031604..2dfd8cfce2 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -37,7 +37,6 @@ use crate::session_fs::SessionFsProvider; use crate::trace_context::inject_trace_context; use crate::transforms::SystemMessageTransform; use crate::types::{ - AdmitAuthenticatedCrossSessionInputParams, AdmitAuthenticatedCrossSessionInputRequest, AutoTier, AutoTierPreference, CommandContext, CommandDefinition, CommandHandler, CreateSessionResult, ElicitationRequest, ElicitationResult, ExitPlanModeData, GetMessagesResponse, MessageOptions, PermissionRequestData, RequestId, ResumeSessionConfig, @@ -746,43 +745,6 @@ impl Session { } } - /// Admit an authenticated cross-session input into this session. - /// - /// Wraps the runtime's private, direct-host-only - /// `session.lifecycle.admitAuthenticatedCrossSessionInput` method. The - /// runtime rejects the call unless it arrives on the local direct - /// connection, so this is only usable by a host embedding the SDK - /// in-process (or over the direct local transport), not by a remote peer. - /// - /// The caller supplies only the variable fields of - /// [`CrossSessionInput`](crate::types::CrossSessionInput). The wire - /// payload's `version`, `kind`, `origin`, and `integrity` discriminators - /// are stamped by the SDK, so a caller cannot assert a different - /// provenance or integrity class for the admitted content. - /// - /// The runtime's response carries no payload this SDK surfaces; errors - /// from the runtime are returned unchanged. - /// - /// # Cancel safety - /// - /// **Cancel-safe.** Single RPC dispatched through the writer-actor (see - /// [`Client::call`](crate::Client::call)). If the caller's future is - /// dropped after the frame is enqueued, the admission still lands and - /// the runtime processes it normally. - pub async fn admit_authenticated_cross_session_input( - &self, - request: AdmitAuthenticatedCrossSessionInputRequest, - ) -> Result<(), Error> { - let params = AdmitAuthenticatedCrossSessionInputParams::new(self.id.clone(), request); - self.client - .call( - "session.lifecycle.admitAuthenticatedCrossSessionInput", - Some(serde_json::to_value(params)?), - ) - .await?; - Ok(()) - } - /// Retrieve the session's timeline events. pub async fn get_events(&self) -> Result, Error> { let result = self diff --git a/rust/src/types.rs b/rust/src/types.rs index 8a0de1e7fb..332a48d18c 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -5605,307 +5605,6 @@ impl From<&String> for MessageOptions { } } -/// Wire `version` stamped on every authenticated cross-session admission -/// request. The runtime rejects any other value. -const CROSS_SESSION_INPUT_VERSION: u32 = 1; - -/// Wire `kind` discriminator stamped on every authenticated cross-session -/// admission request. -const CROSS_SESSION_INPUT_KIND: &str = "authenticated-cross-session"; - -/// Wire `origin` provenance class stamped on every authenticated -/// cross-session admission request. Cross-session input is never human -/// input, so the SDK — not the caller — asserts the non-human origin. -const CROSS_SESSION_INPUT_ORIGIN: &str = "authenticated-cross-session"; - -/// Wire `integrity` class stamped on every authenticated cross-session -/// admission request. Cross-session content is untrusted even when the -/// host authenticated its sender, so the SDK — not the caller — asserts it. -const CROSS_SESSION_INPUT_INTEGRITY: &str = "untrusted"; - -/// Optional private presentation metadata describing the sending session, -/// carried alongside a [`CrossSessionInput`]. -/// -/// Every field is optional and is omitted from the wire payload when `None`. -/// The runtime rejects explicit `null` for any of these fields. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -#[non_exhaustive] -pub struct CrossSessionPresentation { - /// Identifier of the sending project session. - pub project_session_id: Option, - /// Human-readable name of the sending project session. - pub project_session_name: Option, - /// Display name shown for the sender. - pub display_name: Option, - /// Branch the sending project session is working on. - pub project_session_branch: Option, - /// Repository the sending project session is working in. - pub repository: Option, -} - -impl CrossSessionPresentation { - /// Build an empty presentation block. Every field is omitted until set. - pub fn new() -> Self { - Self::default() - } - - /// Set the sending project session's identifier. - pub fn with_project_session_id(mut self, project_session_id: impl Into) -> Self { - self.project_session_id = Some(project_session_id.into()); - self - } - - /// Set the sending project session's name. - pub fn with_project_session_name(mut self, project_session_name: impl Into) -> Self { - self.project_session_name = Some(project_session_name.into()); - self - } - - /// Set the display name shown for the sender. - pub fn with_display_name(mut self, display_name: impl Into) -> Self { - self.display_name = Some(display_name.into()); - self - } - - /// Set the sending project session's branch. - pub fn with_project_session_branch(mut self, branch: impl Into) -> Self { - self.project_session_branch = Some(branch.into()); - self - } - - /// Set the sending project session's repository. - pub fn with_repository(mut self, repository: impl Into) -> Self { - self.repository = Some(repository.into()); - self - } -} - -/// The caller-supplied half of an authenticated cross-session input. -/// -/// This type deliberately carries only the fields a host may vary. The -/// wire payload's `version`, `kind`, `origin`, and `integrity` -/// discriminators are stamped by the SDK during request conversion and are -/// not reachable from this type, so a caller cannot claim a different -/// provenance or integrity class. -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub struct CrossSessionInput { - /// Stable identifier assigned by the sending host for this message. - pub message_id: String, - /// Authenticated same-user principal supplied by the sending host. - pub sender_principal: String, - /// Session identifier of the sender. - pub sender_session_id: String, - /// Host-stamped immediate sender identity used to check that a reply - /// stays on the established edge. - pub reply_target: String, - /// Raw natural-language message content. - pub content: String, - /// Optional recipient behavior request, distinct from - /// [`delivery_mode`](Self::delivery_mode). Omitted when `None`. - pub requested_mode: Option, - /// Optional private presentation metadata. Omitted when `None`. - pub presentation: Option, - /// Optional scheduling selection. Omitted when `None`, which preserves - /// the recipient session's current default. - pub delivery_mode: Option, -} - -impl CrossSessionInput { - /// Build a cross-session input from its required fields. - pub fn new( - message_id: impl Into, - sender_principal: impl Into, - sender_session_id: impl Into, - reply_target: impl Into, - content: impl Into, - ) -> Self { - Self { - message_id: message_id.into(), - sender_principal: sender_principal.into(), - sender_session_id: sender_session_id.into(), - reply_target: reply_target.into(), - content: content.into(), - requested_mode: None, - presentation: None, - delivery_mode: None, - } - } - - /// Set the optional recipient behavior request. - pub fn with_requested_mode(mut self, requested_mode: impl Into) -> Self { - self.requested_mode = Some(requested_mode.into()); - self - } - - /// Attach optional private presentation metadata. - pub fn with_presentation(mut self, presentation: CrossSessionPresentation) -> Self { - self.presentation = Some(presentation); - self - } - - /// Set the optional delivery mode for this admission. - pub fn with_delivery_mode(mut self, delivery_mode: DeliveryMode) -> Self { - self.delivery_mode = Some(delivery_mode); - self - } -} - -/// Recipient-side per-turn continuation context supplied by the local host. -/// -/// This is never transmitted between sessions and is never populated from a -/// remote sender's payload. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -#[non_exhaustive] -pub struct CrossSessionRecipientContext { - /// Session identifiers this turn is authorized to continue with. - pub authorized_continuation_targets: Vec, -} - -impl CrossSessionRecipientContext { - /// Build a recipient context from a set of authorized continuation targets. - pub fn new(targets: impl IntoIterator>) -> Self { - Self { - authorized_continuation_targets: targets.into_iter().map(Into::into).collect(), - } - } -} - -/// Request for -/// [`Session::admit_authenticated_cross_session_input`](crate::session::Session::admit_authenticated_cross_session_input). -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub struct AdmitAuthenticatedCrossSessionInputRequest { - /// The authenticated cross-session input to admit. - pub input: CrossSessionInput, - /// Optional recipient-side continuation context. Omitted when `None`. - pub recipient_context: Option, -} - -impl AdmitAuthenticatedCrossSessionInputRequest { - /// Build a request that admits `input` with no extra recipient context. - pub fn new(input: CrossSessionInput) -> Self { - Self { - input, - recipient_context: None, - } - } - - /// Attach recipient-side continuation context. - pub fn with_recipient_context(mut self, context: CrossSessionRecipientContext) -> Self { - self.recipient_context = Some(context); - self - } -} - -/// Wire params for `session.lifecycle.admitAuthenticatedCrossSessionInput`. -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct AdmitAuthenticatedCrossSessionInputParams { - session_id: SessionId, - input: CrossSessionInputWire, - #[serde(skip_serializing_if = "Option::is_none")] - recipient_context: Option, -} - -impl AdmitAuthenticatedCrossSessionInputParams { - pub(crate) fn new( - session_id: SessionId, - request: AdmitAuthenticatedCrossSessionInputRequest, - ) -> Self { - Self { - session_id, - input: CrossSessionInputWire::from(request.input), - recipient_context: request.recipient_context.map(Into::into), - } - } -} - -/// Wire form of [`CrossSessionInput`]. The provenance and integrity -/// discriminators are private and stamped here, so no caller-provided value -/// can reach them. -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct CrossSessionInputWire { - version: u32, - kind: &'static str, - origin: &'static str, - integrity: &'static str, - message_id: String, - sender_principal: String, - sender_session_id: String, - reply_target: String, - content: String, - #[serde(skip_serializing_if = "Option::is_none")] - requested_mode: Option, - #[serde(skip_serializing_if = "Option::is_none")] - presentation: Option, - #[serde(skip_serializing_if = "Option::is_none")] - delivery_mode: Option, -} - -impl From for CrossSessionInputWire { - fn from(input: CrossSessionInput) -> Self { - Self { - version: CROSS_SESSION_INPUT_VERSION, - kind: CROSS_SESSION_INPUT_KIND, - origin: CROSS_SESSION_INPUT_ORIGIN, - integrity: CROSS_SESSION_INPUT_INTEGRITY, - message_id: input.message_id, - sender_principal: input.sender_principal, - sender_session_id: input.sender_session_id, - reply_target: input.reply_target, - content: input.content, - requested_mode: input.requested_mode, - presentation: input.presentation.map(Into::into), - delivery_mode: input.delivery_mode, - } - } -} - -/// Wire form of [`CrossSessionPresentation`]. -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct CrossSessionPresentationWire { - #[serde(skip_serializing_if = "Option::is_none")] - project_session_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - project_session_name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - display_name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - project_session_branch: Option, - #[serde(skip_serializing_if = "Option::is_none")] - repository: Option, -} - -impl From for CrossSessionPresentationWire { - fn from(presentation: CrossSessionPresentation) -> Self { - Self { - project_session_id: presentation.project_session_id, - project_session_name: presentation.project_session_name, - display_name: presentation.display_name, - project_session_branch: presentation.project_session_branch, - repository: presentation.repository, - } - } -} - -/// Wire form of [`CrossSessionRecipientContext`]. -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct CrossSessionRecipientContextWire { - authorized_continuation_targets: Vec, -} - -impl From for CrossSessionRecipientContextWire { - fn from(context: CrossSessionRecipientContext) -> Self { - Self { - authorized_continuation_targets: context.authorized_continuation_targets, - } - } -} - /// Response from [`Client::get_status`](crate::Client::get_status). #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 39299d888c..282dfa9fa1 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -27,14 +27,12 @@ use github_copilot_sdk::session_events::{ SessionManagedSettingsResolvedData, }; use github_copilot_sdk::types::{ - AdmitAuthenticatedCrossSessionInputRequest, AskUserVariant, CanvasProviderIdentity, - CloudSessionOptions, CloudSessionRepository, CommandContext, CommandDefinition, CommandHandler, - CrossSessionInput, CrossSessionPresentation, CrossSessionRecipientContext, DeliveryMode, - DisableBypassPermissionsModes, ElicitationRequest, ElicitationResult, ExitPlanModeData, - ExtensionInfo, ManagedSettings, ManagedSettingsPermissions, MessageOptions, - PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource, - PermissionDecisionSurface, RequestId, SessionConfig, SessionId, SetModelOptions, Tool, - ToolInvocation, ToolResult, + AskUserVariant, CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, + CommandContext, CommandDefinition, CommandHandler, DeliveryMode, DisableBypassPermissionsModes, + ElicitationRequest, ElicitationResult, ExitPlanModeData, ExtensionInfo, ManagedSettings, + ManagedSettingsPermissions, MessageOptions, PermissionDecisionContext, + PermissionDecisionOutcome, PermissionDecisionSource, PermissionDecisionSurface, RequestId, + SessionConfig, SessionId, SetModelOptions, Tool, ToolInvocation, ToolResult, }; use github_copilot_sdk::{ AgentMode, Attachment, Client, ContextTier, ErrorKind, MessageSource, ProtocolErrorKind, @@ -2435,216 +2433,6 @@ async fn send_omits_display_prompt_when_unset() { timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); } -#[tokio::test] -async fn admit_authenticated_cross_session_input_serializes_full_request() { - let (session, mut server) = create_session_pair().await; - let session = Arc::new(session); - - let handle = tokio::spawn({ - let session = session.clone(); - async move { - let presentation = CrossSessionPresentation::new() - .with_project_session_id("project-session-7") - .with_project_session_name("Nexus") - .with_display_name("Alpha") - .with_project_session_branch("dfrysinger/cross-session") - .with_repository("github/copilot-sdk"); - let input = CrossSessionInput::new( - "message-1", - "copilot-user-1", - "sender-session-1", - "reply-target-1", - "Inspect the failure.", - ) - .with_requested_mode("plan") - .with_presentation(presentation) - .with_delivery_mode(DeliveryMode::Immediate); - let request = AdmitAuthenticatedCrossSessionInputRequest::new(input) - .with_recipient_context(CrossSessionRecipientContext::new([ - "sender-session-1", - "reply-target-1", - "parent-session-9", - ])); - session - .admit_authenticated_cross_session_input(request) - .await - } - }); - - let request = server.read_request().await; - assert_eq!( - request["method"], - "session.lifecycle.admitAuthenticatedCrossSessionInput" - ); - - let params = request["params"].as_object().expect("params object"); - assert_eq!(params.len(), 3, "unexpected params: {:?}", params.keys()); - assert_eq!(params["sessionId"], server.session_id); - - let input = params["input"].as_object().expect("input object"); - assert_eq!(input.len(), 12, "unexpected input keys: {:?}", input.keys()); - // Stamped by the SDK's request conversion — `CrossSessionInput` exposes - // no field a caller could use to supply or override these four values. - assert_eq!(input["version"], 1); - assert!( - input["version"].is_u64(), - "version must serialize as an integer, got: {}", - input["version"] - ); - assert_eq!(input["kind"], "authenticated-cross-session"); - assert_eq!(input["origin"], "authenticated-cross-session"); - assert_eq!(input["integrity"], "untrusted"); - assert_eq!(input["messageId"], "message-1"); - assert_eq!(input["senderPrincipal"], "copilot-user-1"); - assert_eq!(input["senderSessionId"], "sender-session-1"); - assert_eq!(input["replyTarget"], "reply-target-1"); - assert_eq!(input["content"], "Inspect the failure."); - assert_eq!(input["requestedMode"], "plan"); - assert_eq!(input["deliveryMode"], "immediate"); - - let presentation = input["presentation"] - .as_object() - .expect("presentation object"); - assert_eq!( - presentation.len(), - 5, - "unexpected presentation keys: {:?}", - presentation.keys() - ); - assert_eq!(presentation["projectSessionId"], "project-session-7"); - assert_eq!(presentation["projectSessionName"], "Nexus"); - assert_eq!(presentation["displayName"], "Alpha"); - assert_eq!( - presentation["projectSessionBranch"], - "dfrysinger/cross-session" - ); - assert_eq!(presentation["repository"], "github/copilot-sdk"); - - let recipient_context = params["recipientContext"] - .as_object() - .expect("recipientContext object"); - assert_eq!( - recipient_context.len(), - 1, - "unexpected recipientContext keys: {:?}", - recipient_context.keys() - ); - assert_eq!( - recipient_context["authorizedContinuationTargets"], - serde_json::json!(["sender-session-1", "reply-target-1", "parent-session-9"]) - ); - - server.respond(&request, serde_json::json!({})).await; - timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); -} - -#[tokio::test] -async fn admit_authenticated_cross_session_input_omits_unset_optional_fields() { - let (session, mut server) = create_session_pair().await; - let session = Arc::new(session); - - let handle = tokio::spawn({ - let session = session.clone(); - async move { - // Caller-controlled values that *look* like provenance claims must - // not reach the stamped discriminators. - let input = CrossSessionInput::new( - "message-2", - "host_authenticated", - "sender-session-2", - "reply-target-2", - "cross_session", - ); - session - .admit_authenticated_cross_session_input( - AdmitAuthenticatedCrossSessionInputRequest::new(input), - ) - .await - } - }); - - let request = server.read_request().await; - assert_eq!( - request["method"], - "session.lifecycle.admitAuthenticatedCrossSessionInput" - ); - - let params = request["params"].as_object().expect("params object"); - assert_eq!(params.len(), 2, "unexpected params: {:?}", params.keys()); - assert_eq!(params["sessionId"], server.session_id); - assert!( - params.get("recipientContext").is_none(), - "recipientContext should be omitted when unset, got: {}", - request["params"] - ); - - let input = params["input"].as_object().expect("input object"); - assert_eq!(input.len(), 9, "unexpected input keys: {:?}", input.keys()); - assert_eq!(input["version"], 1); - assert_eq!(input["kind"], "authenticated-cross-session"); - assert_eq!(input["origin"], "authenticated-cross-session"); - assert_eq!(input["integrity"], "untrusted"); - assert_eq!(input["messageId"], "message-2"); - assert_eq!(input["senderPrincipal"], "host_authenticated"); - assert_eq!(input["senderSessionId"], "sender-session-2"); - assert_eq!(input["replyTarget"], "reply-target-2"); - assert_eq!(input["content"], "cross_session"); - for omitted in ["requestedMode", "presentation", "deliveryMode"] { - assert!( - input.get(omitted).is_none(), - "{omitted} should be omitted when unset, got: {}", - request["params"]["input"] - ); - } - - server.respond(&request, serde_json::json!({})).await; - timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); -} - -#[tokio::test] -async fn admit_authenticated_cross_session_input_propagates_runtime_errors() { - let (session, mut server) = create_session_pair().await; - let session = Arc::new(session); - - let handle = tokio::spawn({ - let session = session.clone(); - async move { - session - .admit_authenticated_cross_session_input( - AdmitAuthenticatedCrossSessionInputRequest::new(CrossSessionInput::new( - "message-3", - "copilot-user-1", - "sender-session-3", - "reply-target-3", - "Steer the active turn.", - )), - ) - .await - } - }); - - let request = server.read_request().await; - let id = request["id"].as_u64().unwrap(); - let response = serde_json::json!({ - "jsonrpc": "2.0", - "id": id, - "error": { - "code": -32603, - "message": "authenticated cross-session input admission is local-host only", - }, - }); - write_framed(&mut server.write, &serde_json::to_vec(&response).unwrap()).await; - - let result = timeout(TIMEOUT, handle).await.unwrap().unwrap(); - let error = result.expect_err("runtime rejection must not be swallowed"); - assert!( - error - .to_string() - .contains("authenticated cross-session input admission is local-host only"), - "unexpected error: {error}" - ); -} - #[tokio::test] async fn list_messageable_sessions_stamps_bound_source_and_preserves_exact_name() { let (session, mut server) = create_session_pair().await;