diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 978608c9..72fdcb8b 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -917,6 +917,9 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct } } return ReduceOutcomeNoOp + case *ahptypes.SessionStateReplacedAction: + *state = a.State + return ReduceOutcomeApplied case *ahptypes.SessionCustomizationsChangedAction: state.Customizations = append([]ahptypes.Customization(nil), a.Customizations...) return ReduceOutcomeApplied diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 4071bd50..1184f6e2 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -23,6 +23,7 @@ const ( ActionTypeRootActiveSessionsChanged ActionType = "root/activeSessionsChanged" ActionTypeSessionReady ActionType = "session/ready" ActionTypeSessionCreationFailed ActionType = "session/creationFailed" + ActionTypeSessionStateReplaced ActionType = "session/stateReplaced" ActionTypeSessionChatAdded ActionType = "session/chatAdded" ActionTypeSessionChatRemoved ActionType = "session/chatRemoved" ActionTypeSessionChatUpdated ActionType = "session/chatUpdated" @@ -972,6 +973,17 @@ type SessionInputNeededRemovedAction struct { Id string `json:"id"` } +// The authoritative state for this session was replaced while the channel +// remained subscribed. +// +// Full-replacement semantics: the supplied state replaces the previous session +// state entirely. +type SessionStateReplacedAction struct { + Type ActionType `json:"type"` + // Complete authoritative state for the session. + State SessionState `json:"state"` +} + // The session's customizations have changed. // // Full-replacement semantics: the `customizations` array replaces the @@ -1541,6 +1553,7 @@ func (*ChatWorkingDirectorySetAction) isStateAction() {} func (*ChatWorkingDirectoryRemovedAction) isStateAction() {} func (*SessionInputNeededSetAction) isStateAction() {} func (*SessionInputNeededRemovedAction) isStateAction() {} +func (*SessionStateReplacedAction) isStateAction() {} func (*SessionCustomizationsChangedAction) isStateAction() {} func (*SessionCustomizationToggledAction) isStateAction() {} func (*SessionCustomizationUpdatedAction) isStateAction() {} @@ -1891,6 +1904,12 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value + case "session/stateReplaced": + var value SessionStateReplacedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value case "session/customizationsChanged": var value SessionCustomizationsChangedAction if err := json.Unmarshal(data, &value); err != nil { diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt index b62d7a61..725b4624 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -671,6 +671,8 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } } + is StateActionSessionStateReplaced -> action.value.state + is StateActionSessionCustomizationsChanged -> state.copy(customizations = action.value.customizations) is StateActionSessionCustomizationToggled -> { diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt index f696130b..5465f9e5 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt @@ -34,6 +34,8 @@ enum class ActionType { SESSION_READY, @SerialName("session/creationFailed") SESSION_CREATION_FAILED, + @SerialName("session/stateReplaced") + SESSION_STATE_REPLACED, @SerialName("session/chatAdded") SESSION_CHAT_ADDED, @SerialName("session/chatRemoved") @@ -1045,6 +1047,15 @@ data class ChatInputCompletedAction( val answers: Map? = null ) +@Serializable +data class SessionStateReplacedAction( + val type: ActionType, + /** + * Complete authoritative state for the session. + */ + val state: SessionState +) + @Serializable data class SessionCustomizationsChangedAction( val type: ActionType, @@ -1584,6 +1595,7 @@ sealed interface StateAction @JvmInline value class StateActionChatInputRequested(val value: ChatInputRequestedAction) : StateAction @JvmInline value class StateActionChatInputAnswerChanged(val value: ChatInputAnswerChangedAction) : StateAction @JvmInline value class StateActionChatInputCompleted(val value: ChatInputCompletedAction) : StateAction +@JvmInline value class StateActionSessionStateReplaced(val value: SessionStateReplacedAction) : StateAction @JvmInline value class StateActionSessionCustomizationsChanged(val value: SessionCustomizationsChangedAction) : StateAction @JvmInline value class StateActionSessionCustomizationToggled(val value: SessionCustomizationToggledAction) : StateAction @JvmInline value class StateActionSessionCustomizationUpdated(val value: SessionCustomizationUpdatedAction) : StateAction @@ -1684,6 +1696,7 @@ internal object StateActionSerializer : KSerializer { "chat/inputRequested" -> StateActionChatInputRequested(input.json.decodeFromJsonElement(ChatInputRequestedAction.serializer(), element)) "chat/inputAnswerChanged" -> StateActionChatInputAnswerChanged(input.json.decodeFromJsonElement(ChatInputAnswerChangedAction.serializer(), element)) "chat/inputCompleted" -> StateActionChatInputCompleted(input.json.decodeFromJsonElement(ChatInputCompletedAction.serializer(), element)) + "session/stateReplaced" -> StateActionSessionStateReplaced(input.json.decodeFromJsonElement(SessionStateReplacedAction.serializer(), element)) "session/customizationsChanged" -> StateActionSessionCustomizationsChanged(input.json.decodeFromJsonElement(SessionCustomizationsChangedAction.serializer(), element)) "session/customizationToggled" -> StateActionSessionCustomizationToggled(input.json.decodeFromJsonElement(SessionCustomizationToggledAction.serializer(), element)) "session/customizationUpdated" -> StateActionSessionCustomizationUpdated(input.json.decodeFromJsonElement(SessionCustomizationUpdatedAction.serializer(), element)) @@ -1777,6 +1790,7 @@ internal object StateActionSerializer : KSerializer { is StateActionChatInputRequested -> output.json.encodeToJsonElement(ChatInputRequestedAction.serializer(), value.value) is StateActionChatInputAnswerChanged -> output.json.encodeToJsonElement(ChatInputAnswerChangedAction.serializer(), value.value) is StateActionChatInputCompleted -> output.json.encodeToJsonElement(ChatInputCompletedAction.serializer(), value.value) + is StateActionSessionStateReplaced -> output.json.encodeToJsonElement(SessionStateReplacedAction.serializer(), value.value) is StateActionSessionCustomizationsChanged -> output.json.encodeToJsonElement(SessionCustomizationsChangedAction.serializer(), value.value) is StateActionSessionCustomizationToggled -> output.json.encodeToJsonElement(SessionCustomizationToggledAction.serializer(), value.value) is StateActionSessionCustomizationUpdated -> output.json.encodeToJsonElement(SessionCustomizationUpdatedAction.serializer(), value.value) diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 8cd2c8f4..33248806 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -18,9 +18,9 @@ use crate::state::{ ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ChatSummary, ConfirmationOption, Customization, ErrorInfo, McpAuthRequirement, McpServerState, Message, ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputRequest, - SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallCancellationReason, - ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, - ToolDefinition, ToolResultContent, Turn, UsageInfo, + SessionState, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, + ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, + ToolCallRiskAssessment, ToolDefinition, ToolResultContent, Turn, UsageInfo, }; // ─── ActionType ────────────────────────────────────────────────────── @@ -36,6 +36,8 @@ pub enum ActionType { SessionReady, #[serde(rename = "session/creationFailed")] SessionCreationFailed, + #[serde(rename = "session/stateReplaced")] + SessionStateReplaced, #[serde(rename = "session/chatAdded")] SessionChatAdded, #[serde(rename = "session/chatRemoved")] @@ -1146,6 +1148,18 @@ pub struct ChatInputCompletedAction { pub answers: Option>, } +/// The authoritative state for this session was replaced while the channel +/// remained subscribed. +/// +/// Full-replacement semantics: the supplied state replaces the previous session +/// state entirely. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionStateReplacedAction { + /// Complete authoritative state for the session. + pub state: SessionState, +} + /// The session's customizations have changed. /// /// Full-replacement semantics: the `customizations` array replaces the @@ -1874,6 +1888,8 @@ pub enum StateAction { ChatInputAnswerChanged(ChatInputAnswerChangedAction), #[serde(rename = "chat/inputCompleted")] ChatInputCompleted(ChatInputCompletedAction), + #[serde(rename = "session/stateReplaced")] + SessionStateReplaced(Box), #[serde(rename = "session/customizationsChanged")] SessionCustomizationsChanged(SessionCustomizationsChangedAction), #[serde(rename = "session/customizationToggled")] diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 59c62518..e765913d 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -821,6 +821,10 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - state.status = new_status; ReduceOutcome::Applied } + StateAction::SessionStateReplaced(a) => { + *state = a.state.clone(); + ReduceOutcome::Applied + } StateAction::SessionCustomizationsChanged(a) => { state.customizations = Some(a.customizations.clone()); ReduceOutcome::Applied diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index 809dd13b..2adf88f9 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -10,6 +10,7 @@ public enum ActionType: String, Codable, Sendable { case rootActiveSessionsChanged = "root/activeSessionsChanged" case sessionReady = "session/ready" case sessionCreationFailed = "session/creationFailed" + case sessionStateReplaced = "session/stateReplaced" case sessionChatAdded = "session/chatAdded" case sessionChatRemoved = "session/chatRemoved" case sessionChatUpdated = "session/chatUpdated" @@ -1338,6 +1339,20 @@ public struct ChatInputCompletedAction: Codable, Sendable { } } +public struct SessionStateReplacedAction: Codable, Sendable { + public var type: ActionType + /// Complete authoritative state for the session. + public var state: SessionState + + public init( + type: ActionType, + state: SessionState + ) { + self.type = type + self.state = state + } +} + public struct SessionCustomizationsChangedAction: Codable, Sendable { public var type: ActionType /// Updated customization list (full replacement). @@ -2064,6 +2079,7 @@ public enum StateAction: Codable, Sendable { case chatInputRequested(ChatInputRequestedAction) case chatInputAnswerChanged(ChatInputAnswerChangedAction) case chatInputCompleted(ChatInputCompletedAction) + case sessionStateReplaced(SessionStateReplacedAction) case sessionCustomizationsChanged(SessionCustomizationsChangedAction) case sessionCustomizationToggled(SessionCustomizationToggledAction) case sessionCustomizationUpdated(SessionCustomizationUpdatedAction) @@ -2208,6 +2224,8 @@ public enum StateAction: Codable, Sendable { self = .chatInputAnswerChanged(try ChatInputAnswerChangedAction(from: decoder)) case "chat/inputCompleted": self = .chatInputCompleted(try ChatInputCompletedAction(from: decoder)) + case "session/stateReplaced": + self = .sessionStateReplaced(try SessionStateReplacedAction(from: decoder)) case "session/customizationsChanged": self = .sessionCustomizationsChanged(try SessionCustomizationsChangedAction(from: decoder)) case "session/customizationToggled": @@ -2338,6 +2356,7 @@ public enum StateAction: Codable, Sendable { case .chatInputRequested(let v): try v.encode(to: encoder) case .chatInputAnswerChanged(let v): try v.encode(to: encoder) case .chatInputCompleted(let v): try v.encode(to: encoder) + case .sessionStateReplaced(let v): try v.encode(to: encoder) case .sessionCustomizationsChanged(let v): try v.encode(to: encoder) case .sessionCustomizationToggled(let v): try v.encode(to: encoder) case .sessionCustomizationUpdated(let v): try v.encode(to: encoder) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index fc0f03fb..e6812cf9 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -789,6 +789,9 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS // ── Customizations ────────────────────────────────────────────────── + case .sessionStateReplaced(let a): + return a.state + case .sessionCustomizationsChanged(let a): var next = state next.customizations = a.customizations diff --git a/docs/.changes/20260731-session-state-replaced.json b/docs/.changes/20260731-session-state-replaced.json new file mode 100644 index 00000000..e9fc34c5 --- /dev/null +++ b/docs/.changes/20260731-session-state-replaced.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "`session/stateReplaced` action for replacing session state while its channel remains subscribed." +} diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 9a046769..8b2e2a8a 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -157,6 +157,23 @@ "error" ] }, + "SessionStateReplacedAction": { + "type": "object", + "description": "The authoritative state for this session was replaced while the channel\nremained subscribed.\n\nFull-replacement semantics: the supplied state replaces the previous session\nstate entirely.", + "properties": { + "type": { + "const": "session/stateReplaced" + }, + "state": { + "$ref": "#/$defs/SessionState", + "description": "Complete authoritative state for the session." + } + }, + "required": [ + "type", + "state" + ] + }, "SessionChatAddedAction": { "type": "object", "description": "A chat was added to this session's catalog. Upsert semantics: if a chat\nwith the same `summary.resource` already exists, the existing entry is\nreplaced.\n\nMirrors the root-channel `root/sessionAdded` notification.", @@ -7526,6 +7543,9 @@ { "$ref": "#/$defs/SessionCreationFailedAction" }, + { + "$ref": "#/$defs/SessionStateReplacedAction" + }, { "$ref": "#/$defs/SessionChatAddedAction" }, diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 775ee128..ab8c01bf 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -6397,6 +6397,23 @@ "error" ] }, + "SessionStateReplacedAction": { + "type": "object", + "description": "The authoritative state for this session was replaced while the channel\nremained subscribed.\n\nFull-replacement semantics: the supplied state replaces the previous session\nstate entirely.", + "properties": { + "type": { + "const": "session/stateReplaced" + }, + "state": { + "$ref": "#/$defs/SessionState", + "description": "Complete authoritative state for the session." + } + }, + "required": [ + "type", + "state" + ] + }, "SessionChatAddedAction": { "type": "object", "description": "A chat was added to this session's catalog. Upsert semantics: if a chat\nwith the same `summary.resource` already exists, the existing entry is\nreplaced.\n\nMirrors the root-channel `root/sessionAdded` notification.", @@ -8357,6 +8374,9 @@ { "$ref": "#/$defs/SessionCreationFailedAction" }, + { + "$ref": "#/$defs/SessionStateReplacedAction" + }, { "$ref": "#/$defs/SessionChatAddedAction" }, diff --git a/schema/errors.schema.json b/schema/errors.schema.json index c770002f..b600bdc0 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -6952,6 +6952,9 @@ { "$ref": "#/$defs/SessionCreationFailedAction" }, + { + "$ref": "#/$defs/SessionStateReplacedAction" + }, { "$ref": "#/$defs/SessionChatAddedAction" }, @@ -7413,6 +7416,23 @@ "error" ] }, + "SessionStateReplacedAction": { + "type": "object", + "description": "The authoritative state for this session was replaced while the channel\nremained subscribed.\n\nFull-replacement semantics: the supplied state replaces the previous session\nstate entirely.", + "properties": { + "type": { + "const": "session/stateReplaced" + }, + "state": { + "$ref": "#/$defs/SessionState", + "description": "Complete authoritative state for the session." + } + }, + "required": [ + "type", + "state" + ] + }, "SessionChatAddedAction": { "type": "object", "description": "A chat was added to this session's catalog. Upsert semantics: if a chat\nwith the same `summary.resource` already exists, the existing entry is\nreplaced.\n\nMirrors the root-channel `root/sessionAdded` notification.", diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 33d7009c..0ae966f4 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -1381,6 +1381,7 @@ const ACTION_VARIANTS: { { type: 'chat/workingDirectoryRemoved', variantName: 'ChatWorkingDirectoryRemoved', tsInterface: 'ChatWorkingDirectoryRemovedAction' }, { type: 'session/inputNeededSet', variantName: 'SessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction' }, { type: 'session/inputNeededRemoved', variantName: 'SessionInputNeededRemoved', tsInterface: 'SessionInputNeededRemovedAction' }, + { type: 'session/stateReplaced', variantName: 'SessionStateReplaced', tsInterface: 'SessionStateReplacedAction' }, { type: 'session/customizationsChanged', variantName: 'SessionCustomizationsChanged', tsInterface: 'SessionCustomizationsChangedAction' }, { type: 'session/customizationToggled', variantName: 'SessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, { type: 'session/customizationUpdated', variantName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' }, diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 4fad6f13..115cfa43 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -1308,6 +1308,7 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'chat/inputRequested', caseName: 'ChatInputRequested', tsInterface: 'ChatInputRequestedAction' }, { type: 'chat/inputAnswerChanged', caseName: 'ChatInputAnswerChanged', tsInterface: 'ChatInputAnswerChangedAction' }, { type: 'chat/inputCompleted', caseName: 'ChatInputCompleted', tsInterface: 'ChatInputCompletedAction' }, + { type: 'session/stateReplaced', caseName: 'SessionStateReplaced', tsInterface: 'SessionStateReplacedAction' }, { type: 'session/customizationsChanged', caseName: 'SessionCustomizationsChanged', tsInterface: 'SessionCustomizationsChangedAction' }, { type: 'session/customizationToggled', caseName: 'SessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, { type: 'session/customizationUpdated', caseName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' }, diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index eaba7592..86e43010 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -1241,6 +1241,7 @@ const ACTION_VARIANTS: { { type: 'chat/inputRequested', variantName: 'ChatInputRequested', tsInterface: 'ChatInputRequestedAction' }, { type: 'chat/inputAnswerChanged', variantName: 'ChatInputAnswerChanged', tsInterface: 'ChatInputAnswerChangedAction' }, { type: 'chat/inputCompleted', variantName: 'ChatInputCompleted', tsInterface: 'ChatInputCompletedAction' }, + { type: 'session/stateReplaced', variantName: 'SessionStateReplaced', tsInterface: 'SessionStateReplacedAction', boxed: true }, { type: 'session/customizationsChanged', variantName: 'SessionCustomizationsChanged', tsInterface: 'SessionCustomizationsChangedAction' }, { type: 'session/customizationToggled', variantName: 'SessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, { type: 'session/customizationUpdated', variantName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction', boxed: true }, @@ -1316,7 +1317,7 @@ pub struct ${scope}ToolCallConfirmedAction { function generateActionsFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; lines.push('#[allow(unused_imports)]'); - lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, Customization, ErrorInfo, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); + lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, Customization, ErrorInfo, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, SessionState, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); lines.push(''); // ActionType enum diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 88d307d6..9c86f45a 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -1219,6 +1219,7 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'chat/inputRequested', caseName: 'chatInputRequested', tsInterface: 'ChatInputRequestedAction' }, { type: 'chat/inputAnswerChanged', caseName: 'chatInputAnswerChanged', tsInterface: 'ChatInputAnswerChangedAction' }, { type: 'chat/inputCompleted', caseName: 'chatInputCompleted', tsInterface: 'ChatInputCompletedAction' }, + { type: 'session/stateReplaced', caseName: 'sessionStateReplaced', tsInterface: 'SessionStateReplacedAction' }, { type: 'session/customizationsChanged', caseName: 'sessionCustomizationsChanged', tsInterface: 'SessionCustomizationsChangedAction' }, { type: 'session/customizationToggled', caseName: 'sessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, { type: 'session/customizationUpdated', caseName: 'sessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' }, diff --git a/types/action-origin.generated.ts b/types/action-origin.generated.ts index 92cffe48..3a4e27bb 100644 --- a/types/action-origin.generated.ts +++ b/types/action-origin.generated.ts @@ -9,6 +9,7 @@ import type { RootConfigChangedAction, SessionReadyAction, SessionCreationFailedAction, + SessionStateReplacedAction, SessionChatAddedAction, SessionChatRemovedAction, SessionChatUpdatedAction, @@ -118,6 +119,7 @@ export type ServerRootAction = export type SessionAction = | SessionReadyAction | SessionCreationFailedAction + | SessionStateReplacedAction | SessionChatAddedAction | SessionChatRemovedAction | SessionChatUpdatedAction @@ -164,6 +166,7 @@ export type ClientSessionAction = export type ServerSessionAction = | SessionReadyAction | SessionCreationFailedAction + | SessionStateReplacedAction | SessionChatAddedAction | SessionChatRemovedAction | SessionChatUpdatedAction @@ -363,6 +366,7 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.RootConfigChanged]: true, [ActionType.SessionReady]: false, [ActionType.SessionCreationFailed]: false, + [ActionType.SessionStateReplaced]: false, [ActionType.SessionChatAdded]: false, [ActionType.SessionChatRemoved]: false, [ActionType.SessionChatUpdated]: false, diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index 5ba4ef05..92a32b34 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -12,6 +12,7 @@ import type { SessionInputRequest, Customization, McpServerState, + SessionState, } from './state.js'; import type { URI } from '../common/state.js'; import type { Changeset } from '../channels-changeset/state.js'; @@ -41,6 +42,22 @@ export interface SessionCreationFailedAction { error: ErrorInfo; } +/** + * The authoritative state for this session was replaced while the channel + * remained subscribed. + * + * Full-replacement semantics: the supplied state replaces the previous session + * state entirely. + * + * @category Session Actions + * @version 1 + */ +export interface SessionStateReplacedAction { + type: ActionType.SessionStateReplaced; + /** Complete authoritative state for the session. */ + state: SessionState; +} + /** * A chat was added to this session's catalog. Upsert semantics: if a chat * with the same `summary.resource` already exists, the existing entry is diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts index 3847c9f1..aa846cb0 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -118,6 +118,9 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: creationError: action.error, }; + case ActionType.SessionStateReplaced: + return { ...action.state }; + case ActionType.SessionChatAdded: { const list = state.chats; const idx = list.findIndex(c => c.resource === action.summary.resource); diff --git a/types/common/actions.ts b/types/common/actions.ts index d0716412..7264309f 100644 --- a/types/common/actions.ts +++ b/types/common/actions.ts @@ -18,6 +18,7 @@ import type { import type { SessionReadyAction, SessionCreationFailedAction, + SessionStateReplacedAction, SessionChatAddedAction, SessionChatRemovedAction, SessionChatUpdatedAction, @@ -126,6 +127,7 @@ export const enum ActionType { RootActiveSessionsChanged = 'root/activeSessionsChanged', SessionReady = 'session/ready', SessionCreationFailed = 'session/creationFailed', + SessionStateReplaced = 'session/stateReplaced', SessionChatAdded = 'session/chatAdded', SessionChatRemoved = 'session/chatRemoved', SessionChatUpdated = 'session/chatUpdated', @@ -249,6 +251,7 @@ export type StateAction = | RootConfigChangedAction | SessionReadyAction | SessionCreationFailedAction + | SessionStateReplacedAction | SessionChatAddedAction | SessionChatRemovedAction | SessionChatUpdatedAction diff --git a/types/index.ts b/types/index.ts index b91e0dd4..f886e7f5 100644 --- a/types/index.ts +++ b/types/index.ts @@ -161,6 +161,7 @@ export type { RootActiveSessionsChangedAction, SessionReadyAction, SessionCreationFailedAction, + SessionStateReplacedAction, SessionChatAddedAction, SessionChatRemovedAction, SessionChatUpdatedAction, diff --git a/types/test-cases/reducers/258-session-statereplaced.json b/types/test-cases/reducers/258-session-statereplaced.json new file mode 100644 index 00000000..4a55e759 --- /dev/null +++ b/types/test-cases/reducers/258-session-statereplaced.json @@ -0,0 +1,51 @@ +{ + "description": "session/stateReplaced replaces the complete session state", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Old Session", + "status": 1, + "lifecycle": "creating", + "workingDirectories": [ + "file:///old" + ], + "customizations": [ + { + "type": "plugin", + "id": "plugin-old", + "uri": "file:///plugin-old", + "name": "Old", + "enabled": true + } + ], + "activeClients": [], + "chats": [] + }, + "actions": [ + { + "type": "session/stateReplaced", + "state": { + "provider": "copilot", + "title": "New Session", + "status": 1, + "lifecycle": "creating", + "workingDirectories": [ + "file:///new" + ], + "activeClients": [], + "chats": [] + } + } + ], + "expected": { + "provider": "copilot", + "title": "New Session", + "status": 1, + "lifecycle": "creating", + "workingDirectories": [ + "file:///new" + ], + "activeClients": [], + "chats": [] + } +} diff --git a/types/version/registry.ts b/types/version/registry.ts index e2ea8314..82b75954 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -82,6 +82,7 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.RootActiveSessionsChanged]: '0.1.0', [ActionType.SessionReady]: '0.1.0', [ActionType.SessionCreationFailed]: '0.1.0', + [ActionType.SessionStateReplaced]: '0.7.0', [ActionType.SessionChatAdded]: '0.4.0', [ActionType.SessionChatRemoved]: '0.4.0', [ActionType.SessionChatUpdated]: '0.4.0',