@robota-sdk/agent-transport 3.0.0-beta.83 → 3.0.0-beta.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +80 -118
  3. package/dist/browser/client.d.ts +1 -1
  4. package/dist/browser/client.js +1 -1
  5. package/dist/browser/index.d.ts +60 -6
  6. package/dist/browser/index.d.ts.map +1 -1
  7. package/dist/browser/index.js +1 -1
  8. package/dist/browser/index.js.map +1 -1
  9. package/dist/browser/observer-messages-CEeisRaF.js +2 -0
  10. package/dist/browser/observer-messages-CEeisRaF.js.map +1 -0
  11. package/dist/browser/{observer-messages-DC_8VHk9.d.ts → observer-messages-Dnff2Tdb.d.ts} +125 -3
  12. package/dist/browser/observer-messages-Dnff2Tdb.d.ts.map +1 -0
  13. package/dist/node/index.cjs +1 -1
  14. package/dist/node/index.d.cts +181 -6
  15. package/dist/node/index.d.cts.map +1 -1
  16. package/dist/node/index.d.ts +181 -6
  17. package/dist/node/index.d.ts.map +1 -1
  18. package/dist/node/index.js +1 -1
  19. package/dist/node/index.js.map +1 -1
  20. package/package.json +12 -11
  21. package/src/__tests__/message-decoders.test.ts +295 -1
  22. package/src/__tests__/project-read-messages.test.ts +118 -0
  23. package/src/__tests__/session-directory-messages.test.ts +130 -0
  24. package/src/__tests__/session-error-classification.test.ts +113 -0
  25. package/src/__tests__/session-message-handler-observe.test.ts +25 -1
  26. package/src/__tests__/session-message-handler.test.ts +57 -3
  27. package/src/__tests__/settings-messages.test.ts +193 -0
  28. package/src/index.ts +4 -0
  29. package/src/message-decoders.ts +157 -3
  30. package/src/observer-messages.ts +11 -0
  31. package/src/project-read-messages.ts +74 -0
  32. package/src/protocol-session.ts +17 -1
  33. package/src/session-directory-messages.ts +78 -1
  34. package/src/session-error-classification.ts +91 -0
  35. package/src/session-events.ts +7 -1
  36. package/src/session-message-handler.ts +58 -8
  37. package/src/session-query-messages.ts +18 -1
  38. package/src/session-resume-bridge.ts +8 -0
  39. package/src/settings-messages.ts +103 -0
  40. package/src/wire-messages.ts +133 -4
  41. package/dist/browser/observer-messages-Bx-xReUQ.js +0 -2
  42. package/dist/browser/observer-messages-Bx-xReUQ.js.map +0 -1
  43. package/dist/browser/observer-messages-DC_8VHk9.d.ts.map +0 -1
@@ -15,9 +15,14 @@ import {
15
15
  import { handleLoopControlMessage, isLoopControlMessage } from './loop-control-messages.js';
16
16
  import { parseClientMessage } from './message-parser.js';
17
17
  import { isObserverMessageType } from './observer-messages.js';
18
+ import { handleProjectReadMessage, isProjectReadMessage } from './project-read-messages.js';
18
19
  import {
20
+ handleSessionDeleteMessage,
19
21
  handleSessionDirectoryMessage,
22
+ handleSessionRenameMessage,
23
+ isSessionDeleteMessage,
20
24
  isSessionDirectoryMessage,
25
+ isSessionRenameMessage,
21
26
  } from './session-directory-messages.js';
22
27
  import { subscribeSessionEvents } from './session-events.js';
23
28
  import {
@@ -25,12 +30,15 @@ import {
25
30
  isSessionQueryMessage,
26
31
  pendingFrame,
27
32
  } from './session-query-messages.js';
33
+ import { handleSettingsMessage, isSettingsMessage } from './settings-messages.js';
28
34
  import { handleUsageQueryMessage } from './usage-messages.js';
29
35
 
30
36
  import type { TOutboundDeliver } from './outbound-delivery.js';
31
- import type { IProtocolSession } from './protocol-session.js';
37
+ import type { IProtocolSession, TProjectReadCapableSession } from './protocol-session.js';
38
+ import type { ISettingsReporter } from './settings-messages.js';
32
39
  import type { IUsageQueryReporters } from './usage-messages.js';
33
40
  import type { TClientMessage } from './wire-messages.js';
41
+ import type { TCommandSurfaceLocality } from '@robota-sdk/agent-interface-command';
34
42
  import type { TUsageSurface } from '@robota-sdk/agent-interface-analytics';
35
43
  import type { ISessionDirectory, TDriverId } from '@robota-sdk/agent-interface-session';
36
44
 
@@ -48,8 +56,8 @@ export { parseClientMessage } from './message-parser.js';
48
56
  export type TSessionSurfaceRole = 'drive' | 'observe';
49
57
 
50
58
  export interface ISessionMessageHandlerOptions {
51
- /** IProtocolSession to expose. */
52
- session: IProtocolSession;
59
+ /** IProtocolSession to expose — `Partial<ISessionProjectRead>` is probed, not assumed (#3282 §4c). */
60
+ session: TProjectReadCapableSession;
53
61
  /**
54
62
  * ARCH-030: the CARRIER's connection-scoped outbound delivery boundary — not a raw `send`, and not a
55
63
  * `send` plus an error callback for this handler to assemble into one. The carrier owns both the sink
@@ -76,6 +84,15 @@ export interface ISessionMessageHandlerOptions {
76
84
  storedSessionUsageReporter?: NonNullable<IUsageQueryReporters['storedSessionUsageReporter']>;
77
85
  /** Host-owned session directory (#3189): list, start and switch the host's sessions. */
78
86
  sessionDirectory?: ISessionDirectory;
87
+ /** #3282 §4a: host-owned read/write for the GUI Settings screen. */
88
+ settingsReporter?: ISettingsReporter;
89
+ /**
90
+ * #3282 §4 part b-2: carrier-decided, like `role` — whether THIS carrier's connections are
91
+ * provably on this machine. The loopback WS carrier is `'local'`; the device-mesh/WebRTC carrier
92
+ * sets `'remote'`. Absent → `'local'`, forwarded to `executeCommand` and the settings reporter so a
93
+ * command that runs code from outside the session (installing a plugin) can refuse a remote device.
94
+ */
95
+ commandSurfaceLocality?: TCommandSurfaceLocality;
79
96
  }
80
97
 
81
98
  /**
@@ -118,19 +135,23 @@ export function createSessionMessageHandler(options: ISessionMessageHandlerOptio
118
135
  options.surface,
119
136
  role,
120
137
  options.sessionDirectory,
138
+ options.settingsReporter,
139
+ options.commandSurfaceLocality,
121
140
  );
122
141
 
123
142
  return { onMessage, cleanup };
124
143
  }
125
144
 
126
145
  function createMessageHandler(
127
- session: IProtocolSession,
146
+ session: TProjectReadCapableSession,
128
147
  deliver: TOutboundDeliver,
129
148
  driverId?: TDriverId,
130
149
  reporters: IUsageQueryReporters = EMPTY_USAGE_REPORTERS,
131
150
  surface?: TUsageSurface,
132
151
  role: TSessionSurfaceRole = 'drive',
133
152
  sessionDirectory?: ISessionDirectory,
153
+ settingsReporter?: ISettingsReporter,
154
+ commandSurfaceLocality?: TCommandSurfaceLocality,
134
155
  ): (data: string) => void {
135
156
  return (data: string): void => {
136
157
  const msg = parseClientMessage(data, deliver);
@@ -139,7 +160,17 @@ function createMessageHandler(
139
160
  deliver({ type: 'protocol_error', message: `Not permitted for an observer: ${msg.type}` });
140
161
  return;
141
162
  }
142
- handleClientMessage(session, deliver, msg, driverId, reporters, surface, sessionDirectory);
163
+ handleClientMessage(
164
+ session,
165
+ deliver,
166
+ msg,
167
+ driverId,
168
+ reporters,
169
+ surface,
170
+ sessionDirectory,
171
+ settingsReporter,
172
+ commandSurfaceLocality,
173
+ );
143
174
  };
144
175
  }
145
176
 
@@ -154,13 +185,15 @@ const EMPTY_USAGE_REPORTERS: IUsageQueryReporters = {
154
185
  * the {@link SessionResumeBridge} intercepts `resume`/`ack` itself and delegates everything else here.
155
186
  */
156
187
  export function handleClientMessage(
157
- session: IProtocolSession,
188
+ session: TProjectReadCapableSession,
158
189
  deliver: TOutboundDeliver,
159
190
  msg: TClientMessage,
160
191
  driverId?: TDriverId,
161
192
  reporters: IUsageQueryReporters = EMPTY_USAGE_REPORTERS,
162
193
  surface?: TUsageSurface,
163
194
  sessionDirectory?: ISessionDirectory,
195
+ settingsReporter?: ISettingsReporter,
196
+ commandSurfaceLocality?: TCommandSurfaceLocality,
164
197
  ): void {
165
198
  if (handleUsageQueryMessage(session, deliver, msg, reporters)) {
166
199
  return;
@@ -169,8 +202,20 @@ export function handleClientMessage(
169
202
  handleSessionDirectoryMessage(deliver, msg, sessionDirectory);
170
203
  return;
171
204
  }
205
+ if (isSettingsMessage(msg)) {
206
+ handleSettingsMessage(session, deliver, msg, settingsReporter, commandSurfaceLocality);
207
+ return;
208
+ }
209
+ if (isSessionRenameMessage(msg)) {
210
+ handleSessionRenameMessage(deliver, msg, sessionDirectory);
211
+ return;
212
+ }
213
+ if (isSessionDeleteMessage(msg)) {
214
+ handleSessionDeleteMessage(deliver, msg, sessionDirectory);
215
+ return;
216
+ }
172
217
  if (isSessionControlMessage(msg)) {
173
- handleSessionControlMessage(session, deliver, msg, driverId, surface);
218
+ handleSessionControlMessage(session, deliver, msg, driverId, surface, commandSurfaceLocality);
174
219
  return;
175
220
  }
176
221
  if (isLoopControlMessage(msg)) {
@@ -181,6 +226,10 @@ export function handleClientMessage(
181
226
  handleSessionQueryMessage(session, deliver, msg);
182
227
  return;
183
228
  }
229
+ if (isProjectReadMessage(msg)) {
230
+ handleProjectReadMessage(session, deliver, msg);
231
+ return;
232
+ }
184
233
  if (isBackgroundQueryMessage(msg)) {
185
234
  handleBackgroundQueryMessage(session, deliver, msg);
186
235
  return;
@@ -268,6 +317,7 @@ function handleSessionControlMessage(
268
317
  msg: Extract<TClientMessage, { type: 'submit' | 'command' | 'abort' | 'cancel-queue' }>,
269
318
  driverId?: TDriverId,
270
319
  surface?: TUsageSurface,
320
+ commandSurfaceLocality?: TCommandSurfaceLocality,
271
321
  ): void {
272
322
  if (msg.type === 'submit') {
273
323
  // TRANS-008 (issue #2045). A TYPE check, not a falsy one: `{}`, `[]`, `42` and `true` are truthy
@@ -304,7 +354,7 @@ function handleSessionControlMessage(
304
354
  }
305
355
  // REMOTE-003: a transport-origin command is tagged `'remote'` (optional policy, allow-by-default;
306
356
  // REMOTE-006). CMD-004: the SERVER-ASSIGNED driver id (E5) is the command origin — intents route back here.
307
- session.executeCommand(msg.name, msg.args ?? '', 'remote', driverId).then(
357
+ session.executeCommand(msg.name, msg.args ?? '', 'remote', driverId, commandSurfaceLocality).then(
308
358
  (result) => {
309
359
  deliver({
310
360
  type: 'command_result',
@@ -14,6 +14,8 @@ type TSessionQueryMessage = Extract<
14
14
  | 'get-context'
15
15
  | 'get-commands'
16
16
  | 'get-status'
17
+ | 'list-models'
18
+ | 'get-agent-definitions'
17
19
  | 'get-executing'
18
20
  | 'get-pending'
19
21
  | 'get-execution-workspace'
@@ -38,6 +40,8 @@ export function isSessionQueryMessage(msg: TClientMessage): msg is TSessionQuery
38
40
  msg.type === 'get-context' ||
39
41
  msg.type === 'get-commands' ||
40
42
  msg.type === 'get-status' ||
43
+ msg.type === 'list-models' ||
44
+ msg.type === 'get-agent-definitions' ||
41
45
  msg.type === 'get-executing' ||
42
46
  msg.type === 'get-pending' ||
43
47
  msg.type === 'get-execution-workspace' ||
@@ -51,7 +55,10 @@ export function handleSessionQueryMessage(
51
55
  msg: TSessionQueryMessage,
52
56
  ): void {
53
57
  if (msg.type === 'get-messages') {
54
- deliver({ type: 'messages', messages: session.getMessages() });
58
+ // #3288 §2: `display` rides the same frame as `messages` — a reload/reconnect/resume replay
59
+ // needs both in one round trip, and there is no reason a client would ever want one without
60
+ // the other (the raw messages already have to be fetched to build the projection).
61
+ deliver({ type: 'messages', messages: session.getMessages(), display: session.getMessagesDisplay() });
55
62
  } else if (msg.type === 'get-history') {
56
63
  deliver(historyPage(session, msg.fromIndex ?? 0));
57
64
  } else if (msg.type === 'get-prompts') {
@@ -62,6 +69,16 @@ export function handleSessionQueryMessage(
62
69
  deliver({ type: 'commands', commands: session.listCommands(), skills: session.listSkills() });
63
70
  } else if (msg.type === 'get-status') {
64
71
  deliver({ type: 'session_status', status: session.getStatusSnapshot() });
72
+ } else if (msg.type === 'list-models') {
73
+ const snapshot = session.listModels();
74
+ deliver({ type: 'model_list', requestId: msg.requestId, ...snapshot });
75
+ } else if (msg.type === 'get-agent-definitions') {
76
+ deliver({
77
+ type: 'agent_definitions',
78
+ requestId: msg.requestId,
79
+ agents: session.listAgentDefinitions(),
80
+ current: session.getDefaultAgentType(),
81
+ });
65
82
  } else if (msg.type === 'get-executing') {
66
83
  deliver({ type: 'executing', executing: session.isExecuting() });
67
84
  } else if (msg.type === 'get-execution-workspace') {
@@ -28,6 +28,7 @@ import type { TOutboundDeliver } from './outbound-delivery.js';
28
28
  import type { IProtocolSession } from './protocol-session.js';
29
29
  import type { IUsageQueryReporters } from './usage-messages.js';
30
30
  import type { TSeqServerMessage, TServerMessage } from './wire-messages.js';
31
+ import type { TCommandSurfaceLocality } from '@robota-sdk/agent-interface-command';
31
32
  import type { TUsageSurface } from '@robota-sdk/agent-interface-analytics';
32
33
  import type { TDriverId } from '@robota-sdk/agent-interface-session';
33
34
 
@@ -65,6 +66,8 @@ export interface ISessionResumeBridgeOptions {
65
66
  readonly personalUsageReporter?: IUsageQueryReporters['personalUsageReporter'];
66
67
  readonly usageReporter?: IUsageQueryReporters['usageReporter'];
67
68
  readonly storedSessionUsageReporter?: IUsageQueryReporters['storedSessionUsageReporter'];
69
+ /** #3282 §4 part b-2: this bridge is the reconnectable WebRTC path, so it is always `'remote'`. */
70
+ readonly commandSurfaceLocality?: TCommandSurfaceLocality;
68
71
  }
69
72
 
70
73
  export class SessionResumeBridge {
@@ -73,6 +76,7 @@ export class SessionResumeBridge {
73
76
  private driverId?: TDriverId;
74
77
  private readonly reporters: IUsageQueryReporters;
75
78
  private readonly surface?: TUsageSurface;
79
+ private readonly commandSurfaceLocality?: TCommandSurfaceLocality;
76
80
  private readonly unsubscribe: () => void;
77
81
  private onDeliveryError?: IAttachOptions['onDeliveryError'];
78
82
  /**
@@ -105,6 +109,7 @@ export class SessionResumeBridge {
105
109
  this.buffer = new ResumeBuffer(options.buffer);
106
110
  this.driverId = options.driverId;
107
111
  this.surface = options.surface;
112
+ this.commandSurfaceLocality = options.commandSurfaceLocality;
108
113
  this.reporters = {
109
114
  personalUsageReporter: options.personalUsageReporter,
110
115
  usageReporter: options.usageReporter,
@@ -178,6 +183,9 @@ export class SessionResumeBridge {
178
183
  this.driverId,
179
184
  this.reporters,
180
185
  this.surface,
186
+ undefined,
187
+ undefined,
188
+ this.commandSurfaceLocality,
181
189
  );
182
190
  }
183
191
 
@@ -0,0 +1,103 @@
1
+ import type { TOutboundDeliver } from './outbound-delivery.js';
2
+ import type { IProtocolSession } from './protocol-session.js';
3
+ import type { TClientMessage } from './wire-messages.js';
4
+ import type { TCommandSurfaceLocality } from '@robota-sdk/agent-interface-command';
5
+ import type { ISettingsSnapshot, TSettingsPatch } from '@robota-sdk/agent-interface-session';
6
+
7
+ type TSettingsMessage = Extract<TClientMessage, { type: 'get-settings' | 'update-settings' }>;
8
+
9
+ export function isSettingsMessage(msg: TClientMessage): msg is TSettingsMessage {
10
+ return msg.type === 'get-settings' || msg.type === 'update-settings';
11
+ }
12
+
13
+ /** What applying a patch did: the fresh snapshot, or why nothing was written. */
14
+ export type TSettingsUpdateOutcome =
15
+ | { readonly ok: true; readonly settings: ISettingsSnapshot }
16
+ | {
17
+ readonly ok: false;
18
+ readonly code: 'not_available' | 'invalid' | 'refused' | 'update_failed';
19
+ readonly message: string;
20
+ };
21
+
22
+ /**
23
+ * #3282 §4a: host-owned read/write for the Settings screen, supplied by the composition root the
24
+ * same way `IUsageQueryReporters` supplies usage reads — this package only correlates the request
25
+ * and carries the result. `updateSettings` applies through the SAME function the matching slash
26
+ * command uses (issue #3282 §4: "one code path, so the behavior matches the TUI"), so whatever a
27
+ * command would refuse on this surface is refused here too.
28
+ */
29
+ export interface ISettingsReporter {
30
+ getSettings(
31
+ session: IProtocolSession,
32
+ locality?: TCommandSurfaceLocality,
33
+ ): ISettingsSnapshot | Promise<ISettingsSnapshot>;
34
+ /**
35
+ * Applies through the SAME function the matching slash command uses. `locality` (#3282 §4 part
36
+ * b-2) is forwarded to that command exactly as the transport received it, so a command that
37
+ * refuses a remote surface (installing a plugin) refuses identically here.
38
+ */
39
+ updateSettings(
40
+ session: IProtocolSession,
41
+ patch: TSettingsPatch,
42
+ locality?: TCommandSurfaceLocality,
43
+ ): TSettingsUpdateOutcome | Promise<TSettingsUpdateOutcome>;
44
+ }
45
+
46
+ export function handleSettingsMessage(
47
+ session: IProtocolSession,
48
+ deliver: TOutboundDeliver,
49
+ msg: TSettingsMessage,
50
+ reporter: ISettingsReporter | undefined,
51
+ locality?: TCommandSurfaceLocality,
52
+ ): void {
53
+ if (!reporter) {
54
+ deliverSettingsError(
55
+ deliver,
56
+ msg.requestId,
57
+ 'not_available',
58
+ 'Settings are not available on this host.',
59
+ );
60
+ return;
61
+ }
62
+ try {
63
+ if (msg.type === 'get-settings') {
64
+ void Promise.resolve(reporter.getSettings(session, locality)).then(
65
+ (settings) => deliver({ type: 'settings', requestId: msg.requestId, settings }),
66
+ (error) =>
67
+ deliverSettingsError(deliver, msg.requestId, 'update_failed', failureMessage(error)),
68
+ );
69
+ return;
70
+ }
71
+ void Promise.resolve(reporter.updateSettings(session, msg.patch, locality)).then(
72
+ (outcome) => {
73
+ if (outcome.ok) {
74
+ deliver({ type: 'settings', requestId: msg.requestId, settings: outcome.settings });
75
+ } else {
76
+ deliverSettingsError(deliver, msg.requestId, outcome.code, outcome.message);
77
+ }
78
+ },
79
+ (error) =>
80
+ deliverSettingsError(deliver, msg.requestId, 'update_failed', failureMessage(error)),
81
+ );
82
+ } catch (error) {
83
+ deliverSettingsError(
84
+ deliver,
85
+ msg.requestId,
86
+ 'update_failed',
87
+ failureMessage(error instanceof Error ? error : String(error)),
88
+ );
89
+ }
90
+ }
91
+
92
+ function deliverSettingsError(
93
+ deliver: TOutboundDeliver,
94
+ requestId: string,
95
+ code: 'not_available' | 'invalid' | 'refused' | 'update_failed',
96
+ message: string,
97
+ ): void {
98
+ deliver({ type: 'settings_error', requestId, code, message });
99
+ }
100
+
101
+ function failureMessage(error: Error | string): string {
102
+ return error instanceof Error ? error.message : String(error);
103
+ }
@@ -28,17 +28,24 @@ import type {
28
28
  IBranchEvent,
29
29
  IExecutionResult,
30
30
  IContextFileRefreshedEvent,
31
+ IModelListGroup,
31
32
  IPermissionRequestEvent,
32
33
  IPromptResolvedEvent,
33
34
  IPlanApprovalEvent,
34
35
  ISessionRenamedEvent,
36
+ ISettingsSnapshot,
35
37
  IToolState,
36
38
  ISessionListing,
37
39
  ISessionStatusSnapshot,
38
40
  ISessionSwitchedEvent,
39
41
  TSessionChangeRefusalCode,
42
+ TSessionDeleteRefusalCode,
43
+ TSettingsPatch,
40
44
  IUiIntentEvent,
41
45
  TPermissionResultValue,
46
+ TProjectDiffRead,
47
+ TProjectMemoryRead,
48
+ TProjectStatusRead,
42
49
  TWaitingLoopStopOutcome,
43
50
  } from '@robota-sdk/agent-interface-session';
44
51
  import type {
@@ -52,6 +59,16 @@ export type TBackgroundControlAction = 'cancel' | 'close' | 'send';
52
59
 
53
60
  type THistoryEntry = ReturnType<ISessionConversationRead['getFullHistory']>[number];
54
61
 
62
+ /**
63
+ * #3282 §4: one agent the switcher can offer. `definedIn` is a plain-words location — a discovered
64
+ * file's path, or "Built-in" — never a raw internal id.
65
+ */
66
+ export interface IWireAgentDefinitionSummary {
67
+ name: string;
68
+ description: string;
69
+ definedIn: string;
70
+ }
71
+
55
72
  /**
56
73
  * #3189: one entry of the session's full history as it crosses the wire. The same record the session
57
74
  * keeps, except that `timestamp` is an ISO 8601 string: a `Date` does not survive JSON, and a
@@ -89,11 +106,25 @@ export type TClientMessage =
89
106
  // (a `/` menu), and the session's status (model, permission mode, effort, context).
90
107
  | { type: 'get-commands' }
91
108
  | { type: 'get-status' }
109
+ // #3282 §2: the models the GUI's model menu offers, grouped by configured provider profile — so
110
+ // the GUI never scrapes `/model`'s picker text. Choosing one sends `command` with `name: 'model'`,
111
+ // the SAME path `/model <id>` runs; this message only reads the choices.
112
+ | { type: 'list-models'; requestId: string }
113
+ // #3282 §4: the agent switcher's roster and its currently-selected default — so the GUI never
114
+ // scrapes `/agent`'s text. Choosing one sends `command` with `name: 'agent'`, the SAME path
115
+ // `/agent <name>` runs; this message only reads the roster and the current selection.
116
+ | { type: 'get-agent-definitions'; requestId: string }
92
117
  // #3189: the host's sessions — list them, start a new one, make another current. A refused change
93
118
  // answers `session_change_failed` with the same `requestId`.
94
119
  | { type: 'list-sessions'; requestId: string }
95
120
  | { type: 'new-session'; requestId?: string }
96
121
  | { type: 'switch-session'; sessionId: string; requestId?: string }
122
+ // #3289 §1: rename a session from the list — current or not. Answered by `session_renamed_in_list`
123
+ // or `session_rename_failed`, both carrying the same `requestId`.
124
+ | { type: 'rename-session'; sessionId: string; name: string; requestId: string }
125
+ // #3289 §1: remove a stored session's record for good. Answered by `session_deleted` or
126
+ // `session_delete_failed`, both carrying the same `requestId`.
127
+ | { type: 'delete-session'; sessionId: string; requestId: string }
97
128
  // SELFHOST-004: request the assembled trace/cost read-model (spans + cost-by-source) for the run.
98
129
  | { type: 'get-usage-report' }
99
130
  | {
@@ -116,6 +147,16 @@ export type TClientMessage =
116
147
  }
117
148
  // Stop the self-paced loop that is waiting for its next wake; answered by `waiting_loop_stop`.
118
149
  | { type: 'stop-waiting-loop'; requestId: string }
150
+ // #3282 §4c: the Project panel's "Changes" section — this workspace's git status. Answered by
151
+ // `project_status`, echoing `requestId`.
152
+ | { type: 'project-status'; requestId: string }
153
+ // The Project panel's "File diff" section for one file `project_status` reported — `path` exactly
154
+ // as reported (workspace-relative; the server re-checks it stays inside the workspace). Answered by
155
+ // `project_diff`.
156
+ | { type: 'project-diff'; requestId: string; path: string }
157
+ // The Project panel's "Memory" section — project memory as readable text. Answered by
158
+ // `project_memory`.
159
+ | { type: 'project-memory'; requestId: string }
119
160
  | { type: 'get-background-tasks'; filter?: IBackgroundTaskListFilter }
120
161
  | { type: 'get-background-task'; taskId: string }
121
162
  | { type: 'get-background-job-groups' }
@@ -131,7 +172,12 @@ export type TClientMessage =
131
172
  // REMOTE-013 E4 session-resume: `resume` asks the host to replay the tail after `lastSeq` (the last seq the
132
173
  // client applied); `ack` lets the host free its un-acked buffer up to `seq`. Only meaningful post-E3-accept.
133
174
  | { type: 'resume'; lastSeq: number }
134
- | { type: 'ack'; seq: number };
175
+ | { type: 'ack'; seq: number }
176
+ // #3282 §4a: the GUI Settings screen. A snapshot fetch and a discriminated single-field patch,
177
+ // never command text — the server applies a patch through the same function its slash command
178
+ // uses, so the two paths cannot drift. Answered by `settings` or `settings_error`.
179
+ | { type: 'get-settings'; requestId: string }
180
+ | { type: 'update-settings'; requestId: string; patch: TSettingsPatch };
135
181
 
136
182
  /** Outbound message from server to client. */
137
183
  export type TServerMessage =
@@ -145,7 +191,28 @@ export type TServerMessage =
145
191
  | { type: 'thinking'; isThinking: boolean; driverId?: TDriverId }
146
192
  | { type: 'complete'; result: TWireExecutionResult; driverId?: TDriverId }
147
193
  | { type: 'interrupted'; result: TWireExecutionResult; driverId?: TDriverId }
148
- | { type: 'error'; message: string; driverId?: TDriverId }
194
+ | {
195
+ type: 'error';
196
+ /** The raw failure text — kept for a "Details" disclosure; never the whole story on its own. */
197
+ message: string;
198
+ /**
199
+ * What kind of failure this was, when it's known — so a renderer can say what happened and
200
+ * what to do next in plain words instead of showing `message` verbatim (#3289 §3). Additive:
201
+ * absent for a session error this classification does not recognize (unchanged behavior).
202
+ */
203
+ code?: 'auth' | 'rate_limit' | 'model_unavailable' | 'network' | 'provider';
204
+ /** The provider/profile name the failure came from, when `code` names one. */
205
+ provider?: string;
206
+ /** For `code: 'rate_limit'`, how long until a retry may succeed, when the provider said so. */
207
+ retryAfterSeconds?: number;
208
+ /**
209
+ * For `code: 'model_unavailable'`, the model the failed request tried — captured at the moment
210
+ * of failure so a renderer never has to fall back to whatever model is live by the time it
211
+ * draws this notice (which may already be a different one the person switched to).
212
+ */
213
+ model?: string;
214
+ driverId?: TDriverId;
215
+ }
149
216
  | {
150
217
  type: 'command_result';
151
218
  name: string;
@@ -155,7 +222,24 @@ export type TServerMessage =
155
222
  /** The `requestId` of the `command` this answers, when it carried one. */
156
223
  requestId?: string;
157
224
  }
158
- | { type: 'messages'; messages: ReturnType<ISessionConversationRead['getMessages']> }
225
+ | {
226
+ type: 'messages';
227
+ messages: ReturnType<ISessionConversationRead['getMessages']>;
228
+ /**
229
+ * #3288 §2: the SAME history, projected into display segments (text runs and finished tool
230
+ * calls, diffs included) — so a reload/reconnect/resume replay shows tool rows instead of bare
231
+ * text. Optional so an older host that has not grown `getMessagesDisplay()` yet still sends a
232
+ * valid frame; a client without this field falls back to `messages`-only (text bubbles), the
233
+ * same as before this existed.
234
+ */
235
+ display?: ReturnType<ISessionConversationRead['getMessagesDisplay']>;
236
+ /**
237
+ * #3289 §3: this connection's own server-assigned driver id, carried on the first frame every
238
+ * connection already gets — so a client can tell its OWN turns from a co-driver's instead of
239
+ * treating everything but the literal `'owner'` id as someone else (REMOTE-014 E5 display-only).
240
+ */
241
+ driverId?: TDriverId;
242
+ }
159
243
  // #3189: one page of the full history. `entries` start at `startIndex` of the `total` the session
160
244
  // holds now, and stop before a page grows past a bounded size (a single larger entry is sent alone).
161
245
  | { type: 'history'; startIndex: number; total: number; entries: IWireHistoryEntry[] }
@@ -169,6 +253,23 @@ export type TServerMessage =
169
253
  | { type: 'commands'; commands: ICommandListEntry[]; skills: ICommandSkillListEntry[] }
170
254
  // Sent in reply to `get-status`, and pushed whenever the session's status changes.
171
255
  | { type: 'session_status'; status: ISessionStatusSnapshot }
256
+ // #3282 §2: reply to `list-models`, echoing its `requestId`. `groups` is current-profile first;
257
+ // `currentProfile` is absent only when no provider profile is configured at all.
258
+ | {
259
+ type: 'model_list';
260
+ requestId: string;
261
+ groups: readonly IModelListGroup[];
262
+ currentProfile?: string;
263
+ currentModel: string;
264
+ }
265
+ // #3282 §4: reply to `get-agent-definitions`, echoing its `requestId`. `current` is always one of
266
+ // `agents`' names (the fallback default when nothing was ever selected).
267
+ | {
268
+ type: 'agent_definitions';
269
+ requestId: string;
270
+ agents: readonly IWireAgentDefinitionSummary[];
271
+ current: string;
272
+ }
172
273
  | { type: 'sessions'; requestId: string; listing: ISessionListing }
173
274
  | {
174
275
  type: 'sessions_error';
@@ -186,6 +287,18 @@ export type TServerMessage =
186
287
  message: string;
187
288
  requestId?: string;
188
289
  }
290
+ // #3289 §1: a `rename-session` succeeded (current session or not); the client refreshes its listing.
291
+ | { type: 'session_renamed_in_list'; requestId: string; sessionId: string; name: string }
292
+ | { type: 'session_rename_failed'; requestId: string; message: string }
293
+ // #3289 §1: a `delete-session` succeeded; the client refreshes its listing and, if it was showing
294
+ // the deleted session, re-reads what it now shows (the host has already switched it away).
295
+ | { type: 'session_deleted'; requestId: string; sessionId: string }
296
+ | {
297
+ type: 'session_delete_failed';
298
+ requestId: string;
299
+ code: TSessionDeleteRefusalCode;
300
+ message: string;
301
+ }
189
302
  // SELFHOST-004 (P5, TC-08): carry the assembled trace/cost read-model (per-op span timeline +
190
303
  // cost-by-source) across the sidecar boundary — no existing variant carries per-op `durationMs` or
191
304
  // per-source `costUsd`. The GUI renders it renderer-side.
@@ -218,6 +331,13 @@ export type TServerMessage =
218
331
  | { type: 'execution_detail'; requestId: string; page: IExecutionDetailPage }
219
332
  | { type: 'execution_detail_error'; requestId: string; message: string }
220
333
  | { type: 'waiting_loop_stop'; requestId: string; outcome: TWaitingLoopStopOutcome }
334
+ // #3282 §4c: the Project panel's reads. Each result crosses the wire UNCHANGED as the session
335
+ // method's own return value (same convention as `waiting_loop_stop`'s `outcome` above) — the
336
+ // `kind` discriminant carries the non-repository / outside-workspace / unavailable / failed cases,
337
+ // so there is no separate `_error` sibling to keep in sync with these.
338
+ | { type: 'project_status'; requestId: string; result: TProjectStatusRead }
339
+ | { type: 'project_diff'; requestId: string; result: TProjectDiffRead }
340
+ | { type: 'project_memory'; requestId: string; result: TProjectMemoryRead }
221
341
  | { type: 'background_task_event'; event: TBackgroundTaskEvent }
222
342
  | { type: 'background_job_group_event'; event: TBackgroundJobGroupEvent }
223
343
  | { type: 'plan_event'; event: IPlanApprovalEvent }
@@ -252,7 +372,16 @@ export type TServerMessage =
252
372
  | { type: 'protocol_error'; message: string; requestId?: string }
253
373
  // REMOTE-013 E4: sent instead of a replay when the client's `lastSeq` predates the host's retained buffer
254
374
  // (overrun) — the client must do a full `get-messages` refresh rather than accept a silent gap.
255
- | { type: 'resume_gap' };
375
+ | { type: 'resume_gap' }
376
+ // #3282 §4a: the Settings screen's snapshot, in reply to `get-settings` or `update-settings`. The
377
+ // same type answers both, the way `session_status` answers `get-status` and a status change alike.
378
+ | { type: 'settings'; requestId: string; settings: ISettingsSnapshot }
379
+ | {
380
+ type: 'settings_error';
381
+ requestId: string;
382
+ code: 'not_available' | 'invalid' | 'refused' | 'update_failed';
383
+ message: string;
384
+ };
256
385
 
257
386
  /**
258
387
  * REMOTE-013 E4: a server message stamped with its monotonic session sequence number (added by the
@@ -1,2 +0,0 @@
1
- import{SESSION_CHANGE_REFUSAL_CODES as e}from"@robota-sdk/agent-interface-session";const t=4*1024*1024;function n(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}const r=e=>typeof e==`string`,i=e=>typeof e==`string`&&e.length>0,a=e=>typeof e==`boolean`,o=e=>typeof e==`number`&&Number.isFinite(e),s=e=>Number.isSafeInteger(e)&&e>=0,c=e=>t=>t===void 0||e(t),l=e=>Array.isArray(e)&&e.every(n),u=e=>e===null||n(e),d=e=>t=>typeof t==`string`&&e.includes(t),f={user:!0,"agent-wakeup":!0,peer:!0,external:!0},p=e=>n(e)&&r(e.id)&&r(e.timestamp)&&r(e.category)&&r(e.type),m=e=>Array.isArray(e)&&e.every(p),h=[`agent`,`process`,`scheduled`],g=[`foreground`,`background`],_=[`queued`,`running`,`waiting_permission`,`sleeping`,`paused`,`completed`,`failed`,`cancelled`],v=[`cancel`,`close`,`send`],y=e=>n(e)&&c(d(h))(e.kind)&&c(d(_))(e.status)&&c(d(g))(e.mode)&&c(a)(e.includeClosed),b=e=>n(e)&&c(r)(e.prompt)&&c(r)(e.stdin),x=e=>n(e)&&o(e.offset),S=e=>n(e)&&s(e.offset),C=e=>a(e)||e===`allow-session`||e===`allow-project`,w=e=>n(e)&&(e.type===`answer`&&Array.isArray(e.values)&&e.values.every(r)&&c(r)(e.text)||e.type===`cancelled`&&e.values===void 0&&e.text===void 0),T={none:{},several:{message:r},stopped:{loopId:i,message:c(r)},failed:{loopId:c(i),message:r}},E=e=>{if(!n(e)||!r(e.kind))return!1;let t=e.kind;if(!Object.prototype.hasOwnProperty.call(T,t))return!1;let i=T[t];return Object.entries(i).every(([t,n])=>n(e[t]))},D={submit:{prompt:i},command:{name:i,args:c(r),requestId:c(i)},abort:{},"cancel-queue":{},"get-messages":{},"get-history":{fromIndex:c(s)},"get-prompts":{},"get-context":{},"get-commands":{},"get-status":{},"list-sessions":{requestId:i},"new-session":{requestId:c(i)},"switch-session":{sessionId:i,requestId:c(i)},"get-usage-report":{},"get-personal-usage-report":{requestId:i,period:d([`7d`,`30d`]),timezone:i},"get-stored-session-usage-report":{requestId:i,sessionId:i},"get-executing":{},"get-pending":{},"get-execution-workspace":{},"read-execution-detail":{requestId:i,entryId:i,cursor:c(S)},"stop-waiting-loop":{requestId:i},"get-background-tasks":{filter:c(y)},"get-background-task":{taskId:i},"get-background-job-groups":{},"get-background-job-group":{groupId:i},"wait-background-job-group":{groupId:i},"cancel-background-task":{taskId:i,reason:c(r)},"close-background-task":{taskId:i},"send-background-task":{taskId:i,input:b},"read-background-task-log":{taskId:i,cursor:c(x)},"permission-response":{id:i,result:C},"ask-response":{id:i,response:w},resume:{lastSeq:o},ack:{seq:o}},O={driverId:c(r)},k={text_delta:{delta:r,...O},user_message:{content:r,...O},tool_start:{state:n,...O},tool_end:{state:n,...O},thinking:{isThinking:a,...O},complete:{result:n,...O},interrupted:{result:n,...O},error:{message:r,...O},command_result:{name:r,message:r,success:a,requestId:c(r)},messages:{messages:l},history:{startIndex:s,total:s,entries:m},context:{state:n},history_changed:{},turn_source:{source:d(Object.keys(f))},commands:{commands:l,skills:l},session_status:{status:n},sessions:{requestId:i,listing:n},sessions_error:{requestId:i,code:d([`not_available`,`list_failed`]),message:r},session_switched:{event:n},session_change_failed:{code:d(e),message:r,requestId:c(r)},usage_report:{report:n},personal_usage_report:{requestId:i,report:n},personal_usage_report_error:{requestId:i,code:d([`not_available`,`report_failed`]),message:r},stored_session_usage_report:{requestId:i,sessionId:i,report:n},stored_session_usage_report_error:{requestId:i,sessionId:i,code:d([`not_available`,`report_failed`]),message:r},executing:{executing:a},pending:{pending:e=>e===null||r(e),pendingCount:c(s)},execution_workspace_event:{snapshot:n},execution_detail:{requestId:i,page:n},execution_detail_error:{requestId:i,message:r},waiting_loop_stop:{requestId:i,outcome:E},background_task_event:{event:n},background_job_group_event:{event:n},plan_event:{event:n},context_file_refreshed:{event:n},branch_event:{event:n},background_tasks:{tasks:l},background_task:{taskId:r,task:u},background_job_groups:{groups:l},background_job_group:{groupId:r,group:u},background_task_log:{taskId:r,page:n},permission_request:{event:n},ask_request:{event:n},prompt_resolved:{event:n},ui_intent:{event:n},session_renamed:{event:n},history_cleared:{},background_task_control_result:{action:d(v),taskId:r,success:a,message:c(r)},protocol_error:{message:r,requestId:c(r)},resume_gap:{}};function A(e,t,i){if(!n(e))return{ok:!1,reason:`message must be a JSON object`};let a=e.type;if(!r(a))return{ok:!1,reason:`message type must be a string`};let o=Object.prototype.hasOwnProperty.call(t,a)?t[a]:void 0;if(o===void 0)return{ok:!1,reason:`Unknown message type: ${a}`};for(let[t,n]of Object.entries(o))if(!n(e[t]))return{ok:!1,reason:i(t)};return{ok:!0,message:e}}function j(e){return A(e,D,e=>e===`prompt`?`prompt must be a non-empty string`:`${e} has an invalid shape`)}function M(e){return A(e,k,e=>`${e} has an invalid shape`)}function N(e,t){if(e.length>4194304)return{ok:!1,reason:`Frame too large`};let n;try{n=JSON.parse(e)}catch{return{ok:!1,reason:`Invalid JSON`}}return t(n)}const P=new Set([`get-messages`,`get-history`,`get-context`,`get-status`,`get-commands`,`get-executing`,`get-pending`,`get-execution-workspace`,`read-execution-detail`,`get-usage-report`,`list-sessions`,`get-background-tasks`,`get-background-task`,`get-background-job-groups`,`get-background-job-group`,`wait-background-job-group`,`read-background-task-log`]);function F(e){return P.has(e)}export{M as a,N as i,t as n,j as r,F as t};
2
- //# sourceMappingURL=observer-messages-Bx-xReUQ.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"observer-messages-Bx-xReUQ.js","names":[],"sources":["../../src/message-decoders.ts","../../src/observer-messages.ts"],"sourcesContent":["/**\n * Owner-side runtime decoders for the wire message unions (issue #2045 / TRANS-005 adjacent).\n *\n * `wire-messages.ts` owns the TypeScript unions; this file owns turning `JSON.parse` output into one\n * of them. Before it, the server did `JSON.parse(data) as TClientMessage` and each client did the\n * same for `TServerMessage`, so valid JSON with an invalid shape crossed the boundary wearing a\n * protocol type. Carriers now implement `raw string → owner decoder → typed message` and answer a\n * refusal with the protocol's own `protocol_error` (server side) or client error (client side).\n *\n * `decodeClientMessage` is TOTAL: every variant and every nested field it carries is validated.\n * `decodeServerMessage` validates every variant's discriminator and declared top-level fields; a\n * payload OWNED BY ANOTHER PACKAGE (`IToolState`, `IExecutionResult`, background events, …) is\n * checked as a non-array record (or array of records) here — its own total decoding belongs to its\n * owner and is tracked there, not duplicated in a transport package.\n */\n\nimport { SESSION_CHANGE_REFUSAL_CODES } from '@robota-sdk/agent-interface-session';\n\nimport type { TBackgroundControlAction, TClientMessage, TServerMessage } from './wire-messages.js';\n\nexport type TMessageDecodeResult<TMessage> =\n | { readonly ok: true; readonly message: TMessage }\n | { readonly ok: false; readonly reason: string };\n\n/** Serialized frames larger than this are refused before any parse (a bounded inbound budget). */\nexport const MAX_INBOUND_FRAME_BYTES = 4 * 1024 * 1024;\n\ntype TRecord = Record<string, unknown>;\n\nfunction isRecord(value: unknown): value is TRecord {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\nconst isString = (v: unknown): v is string => typeof v === 'string';\nconst isNonEmptyString = (v: unknown): v is string => typeof v === 'string' && v.length > 0;\nconst isBoolean = (v: unknown): v is boolean => typeof v === 'boolean';\nconst isFiniteNumber = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\nconst isCount = (v: unknown): v is number => Number.isSafeInteger(v) && (v as number) >= 0;\nconst isOptional =\n (check: (v: unknown) => boolean) =>\n (v: unknown): boolean =>\n v === undefined || check(v);\nconst isRecordArray = (v: unknown): boolean => Array.isArray(v) && v.every(isRecord);\nconst isNullableRecord = (v: unknown): boolean => v === null || isRecord(v);\nconst oneOf =\n (members: readonly string[]) =>\n (v: unknown): boolean =>\n typeof v === 'string' && members.includes(v);\n/** A record, not an array, so a turn source added to the union without one here fails to compile. */\nconst TURN_SOURCES: Readonly<\n Record<Extract<TServerMessage, { type: 'turn_source' }>['source'], true>\n> = { user: true, 'agent-wakeup': true, peer: true, external: true };\n/** A history entry's envelope is this package's (`IWireHistoryEntry`); its `data` is the recorder's. */\nconst isWireHistoryEntry = (v: unknown): boolean =>\n isRecord(v) &&\n isString(v['id']) &&\n isString(v['timestamp']) &&\n isString(v['category']) &&\n isString(v['type']);\nconst isWireHistoryEntries = (v: unknown): boolean =>\n Array.isArray(v) && v.every(isWireHistoryEntry);\n\ntype TFieldCheck = (value: unknown) => boolean;\ntype TVariantShape = Readonly<Record<string, TFieldCheck>>;\n\nconst BACKGROUND_TASK_KINDS = ['agent', 'process', 'scheduled'] as const;\nconst BACKGROUND_TASK_MODES = ['foreground', 'background'] as const;\nconst BACKGROUND_TASK_STATUSES = [\n 'queued',\n 'running',\n 'waiting_permission',\n 'sleeping',\n 'paused',\n 'completed',\n 'failed',\n 'cancelled',\n] as const;\nconst BACKGROUND_CONTROL_ACTIONS: readonly TBackgroundControlAction[] = ['cancel', 'close', 'send'];\n\nconst isBackgroundTaskListFilter: TFieldCheck = (v) =>\n isRecord(v) &&\n isOptional(oneOf(BACKGROUND_TASK_KINDS))(v['kind']) &&\n isOptional(oneOf(BACKGROUND_TASK_STATUSES))(v['status']) &&\n isOptional(oneOf(BACKGROUND_TASK_MODES))(v['mode']) &&\n isOptional(isBoolean)(v['includeClosed']);\nconst isBackgroundTaskInput: TFieldCheck = (v) =>\n isRecord(v) && isOptional(isString)(v['prompt']) && isOptional(isString)(v['stdin']);\nconst isBackgroundTaskLogCursor: TFieldCheck = (v) => isRecord(v) && isFiniteNumber(v['offset']);\nconst isExecutionDetailCursor: TFieldCheck = (v) => isRecord(v) && isCount(v['offset']);\nconst isPermissionResultValue: TFieldCheck = (v) =>\n isBoolean(v) || v === 'allow-session' || v === 'allow-project';\nconst isActionResponse: TFieldCheck = (v) =>\n isRecord(v) &&\n ((v['type'] === 'answer' &&\n Array.isArray(v['values']) &&\n v['values'].every(isString) &&\n isOptional(isString)(v['text'])) ||\n (v['type'] === 'cancelled' && v['values'] === undefined && v['text'] === undefined));\n\ntype TWaitingLoopStopOutcome = Extract<TServerMessage, { type: 'waiting_loop_stop' }>['outcome'];\n/** Keyed by `kind`, so an outcome added to the union without a shape here fails to compile. */\nconst WAITING_LOOP_STOP_OUTCOME_SHAPES: Readonly<\n Record<TWaitingLoopStopOutcome['kind'], TVariantShape>\n> = {\n none: {},\n several: { message: isString },\n stopped: { loopId: isNonEmptyString, message: isOptional(isString) },\n failed: { loopId: isOptional(isNonEmptyString), message: isString },\n};\nconst isWaitingLoopStopOutcome: TFieldCheck = (v) => {\n if (!isRecord(v) || !isString(v['kind'])) return false;\n const kind = v['kind'];\n if (!Object.prototype.hasOwnProperty.call(WAITING_LOOP_STOP_OUTCOME_SHAPES, kind)) return false;\n const shape = WAITING_LOOP_STOP_OUTCOME_SHAPES[kind as TWaitingLoopStopOutcome['kind']];\n return Object.entries(shape).every(([field, check]) => check(v[field]));\n};\n\n/** One entry per `TClientMessage` variant — a variant added to the union without one fails the test. */\nexport const CLIENT_MESSAGE_SHAPES: Readonly<Record<TClientMessage['type'], TVariantShape>> = {\n submit: { prompt: isNonEmptyString },\n command: {\n name: isNonEmptyString,\n args: isOptional(isString),\n requestId: isOptional(isNonEmptyString),\n },\n abort: {},\n 'cancel-queue': {},\n 'get-messages': {},\n 'get-history': { fromIndex: isOptional(isCount) },\n 'get-prompts': {},\n 'get-context': {},\n 'get-commands': {},\n 'get-status': {},\n 'list-sessions': { requestId: isNonEmptyString },\n 'new-session': { requestId: isOptional(isNonEmptyString) },\n 'switch-session': { sessionId: isNonEmptyString, requestId: isOptional(isNonEmptyString) },\n 'get-usage-report': {},\n 'get-personal-usage-report': {\n requestId: isNonEmptyString,\n period: oneOf(['7d', '30d']),\n timezone: isNonEmptyString,\n },\n 'get-stored-session-usage-report': {\n requestId: isNonEmptyString,\n sessionId: isNonEmptyString,\n },\n 'get-executing': {},\n 'get-pending': {},\n 'get-execution-workspace': {},\n 'read-execution-detail': {\n requestId: isNonEmptyString,\n entryId: isNonEmptyString,\n cursor: isOptional(isExecutionDetailCursor),\n },\n 'stop-waiting-loop': { requestId: isNonEmptyString },\n 'get-background-tasks': { filter: isOptional(isBackgroundTaskListFilter) },\n 'get-background-task': { taskId: isNonEmptyString },\n 'get-background-job-groups': {},\n 'get-background-job-group': { groupId: isNonEmptyString },\n 'wait-background-job-group': { groupId: isNonEmptyString },\n 'cancel-background-task': { taskId: isNonEmptyString, reason: isOptional(isString) },\n 'close-background-task': { taskId: isNonEmptyString },\n 'send-background-task': { taskId: isNonEmptyString, input: isBackgroundTaskInput },\n 'read-background-task-log': {\n taskId: isNonEmptyString,\n cursor: isOptional(isBackgroundTaskLogCursor),\n },\n 'permission-response': { id: isNonEmptyString, result: isPermissionResultValue },\n 'ask-response': { id: isNonEmptyString, response: isActionResponse },\n resume: { lastSeq: isFiniteNumber },\n ack: { seq: isFiniteNumber },\n};\n\nconst authored: TVariantShape = { driverId: isOptional(isString) };\n\n/** One entry per `TServerMessage` variant. Foreign-owned payloads are checked as records (see header). */\nexport const SERVER_MESSAGE_SHAPES: Readonly<Record<TServerMessage['type'], TVariantShape>> = {\n text_delta: { delta: isString, ...authored },\n user_message: { content: isString, ...authored },\n tool_start: { state: isRecord, ...authored },\n tool_end: { state: isRecord, ...authored },\n thinking: { isThinking: isBoolean, ...authored },\n complete: { result: isRecord, ...authored },\n interrupted: { result: isRecord, ...authored },\n error: { message: isString, ...authored },\n command_result: {\n name: isString,\n message: isString,\n success: isBoolean,\n requestId: isOptional(isString),\n },\n messages: { messages: isRecordArray },\n history: { startIndex: isCount, total: isCount, entries: isWireHistoryEntries },\n context: { state: isRecord },\n history_changed: {},\n turn_source: { source: oneOf(Object.keys(TURN_SOURCES)) },\n commands: { commands: isRecordArray, skills: isRecordArray },\n session_status: { status: isRecord },\n sessions: { requestId: isNonEmptyString, listing: isRecord },\n sessions_error: {\n requestId: isNonEmptyString,\n code: oneOf(['not_available', 'list_failed']),\n message: isString,\n },\n session_switched: { event: isRecord },\n session_change_failed: {\n code: oneOf(SESSION_CHANGE_REFUSAL_CODES),\n message: isString,\n requestId: isOptional(isString),\n },\n usage_report: { report: isRecord },\n personal_usage_report: { requestId: isNonEmptyString, report: isRecord },\n personal_usage_report_error: {\n requestId: isNonEmptyString,\n code: oneOf(['not_available', 'report_failed']),\n message: isString,\n },\n stored_session_usage_report: {\n requestId: isNonEmptyString,\n sessionId: isNonEmptyString,\n report: isRecord,\n },\n stored_session_usage_report_error: {\n requestId: isNonEmptyString,\n sessionId: isNonEmptyString,\n code: oneOf(['not_available', 'report_failed']),\n message: isString,\n },\n executing: { executing: isBoolean },\n // An older host sends no count; a client then counts the one prompt it can see.\n pending: { pending: (v) => v === null || isString(v), pendingCount: isOptional(isCount) },\n execution_workspace_event: { snapshot: isRecord },\n execution_detail: { requestId: isNonEmptyString, page: isRecord },\n execution_detail_error: { requestId: isNonEmptyString, message: isString },\n waiting_loop_stop: { requestId: isNonEmptyString, outcome: isWaitingLoopStopOutcome },\n background_task_event: { event: isRecord },\n background_job_group_event: { event: isRecord },\n plan_event: { event: isRecord },\n context_file_refreshed: { event: isRecord },\n branch_event: { event: isRecord },\n background_tasks: { tasks: isRecordArray },\n background_task: { taskId: isString, task: isNullableRecord },\n background_job_groups: { groups: isRecordArray },\n background_job_group: { groupId: isString, group: isNullableRecord },\n background_task_log: { taskId: isString, page: isRecord },\n permission_request: { event: isRecord },\n ask_request: { event: isRecord },\n prompt_resolved: { event: isRecord },\n ui_intent: { event: isRecord },\n session_renamed: { event: isRecord },\n history_cleared: {},\n background_task_control_result: {\n action: oneOf(BACKGROUND_CONTROL_ACTIONS),\n taskId: isString,\n success: isBoolean,\n message: isOptional(isString),\n },\n protocol_error: { message: isString, requestId: isOptional(isString) },\n resume_gap: {},\n};\n\nfunction decodeVariant<TMessage extends { type: string }>(\n value: unknown,\n shapes: Readonly<Record<string, TVariantShape>>,\n reasonFor: (field: string) => string,\n): TMessageDecodeResult<TMessage> {\n if (!isRecord(value)) return { ok: false, reason: 'message must be a JSON object' };\n const type = value['type'];\n if (!isString(type)) return { ok: false, reason: 'message type must be a string' };\n const shape = Object.prototype.hasOwnProperty.call(shapes, type) ? shapes[type] : undefined;\n if (shape === undefined) return { ok: false, reason: `Unknown message type: ${type}` };\n for (const [field, check] of Object.entries(shape)) {\n if (!check(value[field])) return { ok: false, reason: reasonFor(field) };\n }\n return { ok: true, message: value as unknown as TMessage };\n}\n\n/** Total decode of a parsed client frame. */\nexport function decodeClientMessage(value: unknown): TMessageDecodeResult<TClientMessage> {\n return decodeVariant<TClientMessage>(value, CLIENT_MESSAGE_SHAPES, (field) =>\n field === 'prompt' ? 'prompt must be a non-empty string' : `${field} has an invalid shape`,\n );\n}\n\n/** Variant-level decode of a parsed server frame (foreign-owned payloads as records, see header). */\nexport function decodeServerMessage(value: unknown): TMessageDecodeResult<TServerMessage> {\n return decodeVariant<TServerMessage>(\n value,\n SERVER_MESSAGE_SHAPES,\n (field) => `${field} has an invalid shape`,\n );\n}\n\n/** `raw string → JSON → owner decoder`, the one path a carrier implements. */\nexport function decodeFrame<TMessage>(\n data: string,\n decode: (value: unknown) => TMessageDecodeResult<TMessage>,\n): TMessageDecodeResult<TMessage> {\n if (data.length > MAX_INBOUND_FRAME_BYTES) return { ok: false, reason: 'Frame too large' };\n let parsed: unknown;\n try {\n parsed = JSON.parse(data);\n // allow-fallback: the parse failure is returned as a typed refusal the carrier answers on the wire\n } catch {\n return { ok: false, reason: 'Invalid JSON' };\n }\n return decode(parsed);\n}\n","import type { TClientMessage } from './wire-messages.js';\n\n/**\n * The only inbound messages an observer may send: reads of this session. An allowlist, so a message\n * type added later is refused to observers until someone decides it is a read. `get-prompts` is left\n * out: an observer never receives prompts, open ones included.\n *\n * The host refuses anything else from an observer, and a client in observe mode sends only these, so\n * both sides read this one list.\n */\nexport const OBSERVER_MESSAGES: ReadonlySet<TClientMessage['type']> = new Set<\n TClientMessage['type']\n>([\n 'get-messages',\n 'get-history',\n 'get-context',\n 'get-status',\n 'get-commands',\n 'get-executing',\n 'get-pending',\n 'get-execution-workspace',\n 'read-execution-detail',\n 'get-usage-report',\n 'list-sessions',\n 'get-background-tasks',\n 'get-background-task',\n 'get-background-job-groups',\n 'get-background-job-group',\n 'wait-background-job-group',\n 'read-background-task-log',\n]);\n\n/** Whether an observer may send a message of this type. */\nexport function isObserverMessageType(type: string): boolean {\n return (OBSERVER_MESSAGES as ReadonlySet<string>).has(type);\n}\n"],"mappings":"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"}