@hypercli.com/sdk 2026.6.26 → 2026.8.11

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 (106) hide show
  1. package/README.md +181 -1
  2. package/dist/agent-slots.d.ts +17 -0
  3. package/dist/agent-slots.d.ts.map +1 -0
  4. package/dist/agent-slots.js +18 -0
  5. package/dist/agent-slots.js.map +1 -0
  6. package/dist/agent.d.ts +67 -12
  7. package/dist/agent.d.ts.map +1 -1
  8. package/dist/agent.js +220 -28
  9. package/dist/agent.js.map +1 -1
  10. package/dist/agents.d.ts +690 -82
  11. package/dist/agents.d.ts.map +1 -1
  12. package/dist/agents.js +2595 -225
  13. package/dist/agents.js.map +1 -1
  14. package/dist/browser.d.ts +4 -2
  15. package/dist/browser.d.ts.map +1 -1
  16. package/dist/browser.js +10 -3
  17. package/dist/browser.js.map +1 -1
  18. package/dist/channels.d.ts +156 -0
  19. package/dist/channels.d.ts.map +1 -0
  20. package/dist/channels.js +110 -0
  21. package/dist/channels.js.map +1 -0
  22. package/dist/client.d.ts +2 -0
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +14 -4
  25. package/dist/client.js.map +1 -1
  26. package/dist/config.d.ts +1 -1
  27. package/dist/config.js +2 -2
  28. package/dist/config.js.map +1 -1
  29. package/dist/connectors.d.ts +79 -0
  30. package/dist/connectors.d.ts.map +1 -0
  31. package/dist/connectors.js +2 -0
  32. package/dist/connectors.js.map +1 -0
  33. package/dist/errors.d.ts +4 -1
  34. package/dist/errors.d.ts.map +1 -1
  35. package/dist/errors.js +9 -2
  36. package/dist/errors.js.map +1 -1
  37. package/dist/hermes/gateway.d.ts +194 -0
  38. package/dist/hermes/gateway.d.ts.map +1 -0
  39. package/dist/hermes/gateway.js +209 -0
  40. package/dist/hermes/gateway.js.map +1 -0
  41. package/dist/http.d.ts +16 -3
  42. package/dist/http.d.ts.map +1 -1
  43. package/dist/http.js +196 -63
  44. package/dist/http.js.map +1 -1
  45. package/dist/index.d.ts +15 -5
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +13 -4
  48. package/dist/index.js.map +1 -1
  49. package/dist/job/base.d.ts +6 -0
  50. package/dist/job/base.d.ts.map +1 -1
  51. package/dist/job/base.js +7 -1
  52. package/dist/job/base.js.map +1 -1
  53. package/dist/keys.d.ts +21 -5
  54. package/dist/keys.d.ts.map +1 -1
  55. package/dist/keys.js +62 -3
  56. package/dist/keys.js.map +1 -1
  57. package/dist/openclaw/channels.d.ts +86 -0
  58. package/dist/openclaw/channels.d.ts.map +1 -0
  59. package/dist/openclaw/channels.js +290 -0
  60. package/dist/openclaw/channels.js.map +1 -0
  61. package/dist/openclaw/connectors.d.ts +61 -0
  62. package/dist/openclaw/connectors.d.ts.map +1 -0
  63. package/dist/openclaw/connectors.js +174 -0
  64. package/dist/openclaw/connectors.js.map +1 -0
  65. package/dist/openclaw/gateway.d.ts +682 -16
  66. package/dist/openclaw/gateway.d.ts.map +1 -1
  67. package/dist/openclaw/gateway.js +1794 -187
  68. package/dist/openclaw/gateway.js.map +1 -1
  69. package/dist/openclaw/skills.d.ts +106 -0
  70. package/dist/openclaw/skills.d.ts.map +1 -0
  71. package/dist/openclaw/skills.js +575 -0
  72. package/dist/openclaw/skills.js.map +1 -0
  73. package/dist/openclaw/slack.d.ts +818 -0
  74. package/dist/openclaw/slack.d.ts.map +1 -0
  75. package/dist/openclaw/slack.js +536 -0
  76. package/dist/openclaw/slack.js.map +1 -0
  77. package/dist/openclaw/whatsapp.d.ts +63 -0
  78. package/dist/openclaw/whatsapp.d.ts.map +1 -0
  79. package/dist/openclaw/whatsapp.js +264 -0
  80. package/dist/openclaw/whatsapp.js.map +1 -0
  81. package/dist/renders.js +11 -11
  82. package/dist/renders.js.map +1 -1
  83. package/dist/skills.d.ts +126 -0
  84. package/dist/skills.d.ts.map +1 -0
  85. package/dist/skills.js +2 -0
  86. package/dist/skills.js.map +1 -0
  87. package/dist/user.d.ts +15 -2
  88. package/dist/user.d.ts.map +1 -1
  89. package/dist/user.js +51 -4
  90. package/dist/user.js.map +1 -1
  91. package/dist/voice-session.d.ts +74 -0
  92. package/dist/voice-session.d.ts.map +1 -0
  93. package/dist/voice-session.js +255 -0
  94. package/dist/voice-session.js.map +1 -0
  95. package/dist/voice.d.ts +27 -3
  96. package/dist/voice.d.ts.map +1 -1
  97. package/dist/voice.js +89 -6
  98. package/dist/voice.js.map +1 -1
  99. package/dist/workspaces.d.ts +181 -0
  100. package/dist/workspaces.d.ts.map +1 -0
  101. package/dist/workspaces.js +495 -0
  102. package/dist/workspaces.js.map +1 -0
  103. package/dist/x402.d.ts.map +1 -1
  104. package/dist/x402.js +1 -2
  105. package/dist/x402.js.map +1 -1
  106. package/package.json +42 -2
@@ -8,6 +8,58 @@
8
8
  */
9
9
  import { getPublicKeyAsync, signAsync, utils as edUtils } from "@noble/ed25519";
10
10
  import NodeWebSocket from "ws";
11
+ export const OPENCLAW_INTERNAL_MAIN_SESSION_KEY = "main";
12
+ export const OPENCLAW_SDK_SESSION_PREFIX = "hcli:";
13
+ export const OPENCLAW_DASHBOARD_SESSION_PREFIX = "dashboard:";
14
+ const OPENCLAW_CHAT_HISTORY_TRUNCATION_SUFFIX = "\n...(truncated)...";
15
+ export function createOpenClawSessionKey(existingSessionKeys = [], prefix = OPENCLAW_SDK_SESSION_PREFIX) {
16
+ const resolvedPrefix = prefix.trim();
17
+ const existing = new Set(existingSessionKeys
18
+ .map((key) => key?.trim())
19
+ .filter((key) => Boolean(key)));
20
+ for (let attempt = 0; attempt < 10; attempt += 1) {
21
+ const suffix = typeof globalThis.crypto?.randomUUID === "function"
22
+ ? globalThis.crypto.randomUUID()
23
+ : `local-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
24
+ const key = `${resolvedPrefix}${suffix}`;
25
+ if (!existing.has(key))
26
+ return key;
27
+ }
28
+ return `${resolvedPrefix}local-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
29
+ }
30
+ export function createOpenClawSdkSessionKey(existingSessionKeys = []) {
31
+ return createOpenClawSessionKey(existingSessionKeys, OPENCLAW_SDK_SESSION_PREFIX);
32
+ }
33
+ export function isOpenClawInternalMainSessionKey(sessionKey) {
34
+ const normalized = (sessionKey ?? "").trim().toLowerCase();
35
+ if (!normalized)
36
+ return false;
37
+ const parsed = parseAgentSessionKey(normalized);
38
+ return (parsed?.rest ?? normalized) === OPENCLAW_INTERNAL_MAIN_SESSION_KEY;
39
+ }
40
+ export function isOpenClawSdkSessionKey(sessionKey) {
41
+ const normalized = (sessionKey ?? "").trim().toLowerCase();
42
+ if (!normalized)
43
+ return false;
44
+ const parsed = parseAgentSessionKey(normalized);
45
+ return (parsed?.rest ?? normalized).startsWith(OPENCLAW_SDK_SESSION_PREFIX);
46
+ }
47
+ export class GatewayChatStreamInterruptedError extends Error {
48
+ static code = "GATEWAY_CHAT_STREAM_INTERRUPTED";
49
+ code = GatewayChatStreamInterruptedError.code;
50
+ reason;
51
+ expectedSequence;
52
+ receivedSequence;
53
+ constructor(reason, sequence) {
54
+ super(reason === "sequence-gap" && sequence
55
+ ? `Gateway chat stream interrupted by a sequence gap (expected ${sequence.expected}, received ${sequence.received})`
56
+ : "Gateway chat stream interrupted by an event without run or session identity");
57
+ this.name = "GatewayChatStreamInterruptedError";
58
+ this.reason = reason;
59
+ this.expectedSequence = sequence?.expected;
60
+ this.receivedSequence = sequence?.received;
61
+ }
62
+ }
11
63
  class GatewayRequestError extends Error {
12
64
  gatewayCode;
13
65
  details;
@@ -18,11 +70,24 @@ class GatewayRequestError extends Error {
18
70
  this.details = error.details;
19
71
  }
20
72
  }
21
- const MIN_PROTOCOL_VERSION = 3;
22
- const PROTOCOL_VERSION = 4;
23
- const DEFAULT_TIMEOUT = 15_000;
24
- const CHAT_TIMEOUT = 120_000;
73
+ export function isOpenClawGatewayMethodUnsupported(error, method) {
74
+ const record = error && typeof error === "object" ? error : null;
75
+ const code = typeof record?.gatewayCode === "string" ? record.gatewayCode.trim().toUpperCase() : "";
76
+ if (["METHOD_NOT_FOUND", "NOT_IMPLEMENTED", "UNIMPLEMENTED", "UNSUPPORTED_METHOD"].includes(code))
77
+ return true;
78
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
79
+ if (!/unknown method|method not found|not implemented|unsupported method/i.test(message))
80
+ return false;
81
+ return !method || message.toLowerCase().includes(method.trim().toLowerCase());
82
+ }
83
+ export const MIN_GATEWAY_VERSION = 3;
84
+ export const MAX_GATEWAY_VERSION = 4;
85
+ const DEFAULT_CONNECTION_TIMEOUT = 30_000;
86
+ const WEB_LOGIN_WAIT_TIMEOUT = 120_000;
87
+ const DEFAULT_AGENT_TIMEOUT = 900_000;
88
+ const CHAT_LIFECYCLE_ERROR_FALLBACK_TIMEOUT_MS = 20_000;
25
89
  const SKILLS_MUTATION_TIMEOUT = 300_000;
90
+ const PLUGIN_MUTATION_TIMEOUT = 300_000;
26
91
  const RECONNECT_CLOSE_CODE = 4008;
27
92
  const DEFAULT_CLIENT_ID = "cli";
28
93
  const DEFAULT_CLIENT_MODE = "cli";
@@ -35,6 +100,9 @@ const BACKOFF_MULTIPLIER = 1.7;
35
100
  const OPERATOR_ROLE = "operator";
36
101
  const OPERATOR_SCOPES = ["operator.admin", "operator.approvals", "operator.pairing"];
37
102
  const STORAGE_KEY = "openclaw.device.auth.v1";
103
+ const EPHEMERAL_SESSION_PREFIX = "session-hypercli-ephemeral-";
104
+ const EPHEMERAL_RUN_ID_SUPPRESSION_MS = DEFAULT_AGENT_TIMEOUT + 5 * 60_000;
105
+ const EPHEMERAL_TURN_CLOSE_WAIT_MS = 1_000;
38
106
  const CONNECT_ERROR_PAIRING_REQUIRED = "PAIRING_REQUIRED";
39
107
  const CONNECT_ERROR_AUTH_TOKEN_MISMATCH = "AUTH_TOKEN_MISMATCH";
40
108
  const CONNECT_ERROR_AUTH_RATE_LIMITED = "AUTH_RATE_LIMITED";
@@ -42,13 +110,16 @@ const CONNECT_ERROR_DEVICE_TOKEN_MISMATCH = "AUTH_DEVICE_TOKEN_MISMATCH";
42
110
  const VALID_CLIENT_IDS = new Set([
43
111
  "webchat-ui",
44
112
  "openclaw-control-ui",
113
+ "openclaw-tui",
45
114
  "webchat",
46
115
  "cli",
47
116
  "gateway-client",
48
117
  "openclaw-macos",
49
118
  "openclaw-ios",
119
+ "openclaw-watchos",
50
120
  "openclaw-android",
51
121
  "node-host",
122
+ "openclaw-worker",
52
123
  "test",
53
124
  "fingerprint",
54
125
  "openclaw-probe",
@@ -59,6 +130,7 @@ const VALID_CLIENT_MODES = new Set([
59
130
  "ui",
60
131
  "backend",
61
132
  "node",
133
+ "worker",
62
134
  "probe",
63
135
  "test",
64
136
  ]);
@@ -72,11 +144,94 @@ function makeId() {
72
144
  return (char === "x" ? random : (random & 0x3) | 0x8).toString(16);
73
145
  });
74
146
  }
147
+ function channelConfigPatch(channelId, config, accountId, defaultAccount = false) {
148
+ if (!accountId)
149
+ return { channels: { [channelId]: config } };
150
+ return {
151
+ channels: {
152
+ [channelId]: {
153
+ accounts: { [accountId]: config },
154
+ ...(defaultAccount ? { defaultAccount: accountId } : {}),
155
+ },
156
+ },
157
+ };
158
+ }
75
159
  function asRecord(value) {
76
160
  return value && typeof value === "object" && !Array.isArray(value)
77
161
  ? value
78
162
  : null;
79
163
  }
164
+ function protocolError(code, message, record) {
165
+ const frameType = typeof record?.type === "string" ? record.type : undefined;
166
+ const event = typeof record?.event === "string" ? record.event : undefined;
167
+ const requestId = typeof record?.id === "string" ? record.id : undefined;
168
+ return {
169
+ code,
170
+ message,
171
+ ...(frameType ? { frameType } : {}),
172
+ ...(event ? { event } : {}),
173
+ ...(requestId ? { requestId } : {}),
174
+ };
175
+ }
176
+ function normalizeGatewayResponseError(value) {
177
+ if (typeof value === "string" && value.trim()) {
178
+ return { code: "UNAVAILABLE", message: value.trim() };
179
+ }
180
+ const error = asRecord(value);
181
+ return {
182
+ code: typeof error?.code === "string" && error.code.trim() ? error.code : "UNAVAILABLE",
183
+ message: typeof error?.message === "string" && error.message.trim()
184
+ ? error.message
185
+ : "gateway request failed",
186
+ ...(error?.details !== undefined ? { details: error.details } : {}),
187
+ };
188
+ }
189
+ function decodeGatewayFrame(raw) {
190
+ let value;
191
+ try {
192
+ value = JSON.parse(raw);
193
+ }
194
+ catch {
195
+ return { ok: false, error: protocolError("INVALID_JSON", "gateway frame is not valid JSON") };
196
+ }
197
+ const record = asRecord(value);
198
+ if (!record) {
199
+ return { ok: false, error: protocolError("INVALID_FRAME", "gateway frame must be an object") };
200
+ }
201
+ if (record.type === "event") {
202
+ if (typeof record.event !== "string" || !record.event.trim()) {
203
+ return { ok: false, error: protocolError("INVALID_EVENT", "gateway event requires a non-empty string event", record) };
204
+ }
205
+ if (record.seq !== undefined && (!Number.isSafeInteger(record.seq) || record.seq < 0)) {
206
+ return { ok: false, error: protocolError("INVALID_EVENT", "gateway event seq must be a non-negative safe integer", record) };
207
+ }
208
+ return { ok: true, frame: record };
209
+ }
210
+ if (record.type === "res") {
211
+ if (typeof record.id !== "string" || !record.id.trim()) {
212
+ return { ok: false, error: protocolError("INVALID_RESPONSE", "gateway response requires a non-empty string id", record) };
213
+ }
214
+ if (typeof record.ok !== "boolean") {
215
+ return { ok: false, error: protocolError("INVALID_RESPONSE", "gateway response ok must be a boolean", record) };
216
+ }
217
+ if (record.ok) {
218
+ return { ok: true, frame: { type: "res", id: record.id, ok: true, payload: record.payload } };
219
+ }
220
+ return {
221
+ ok: true,
222
+ frame: {
223
+ type: "res",
224
+ id: record.id,
225
+ ok: false,
226
+ error: normalizeGatewayResponseError(record.error),
227
+ },
228
+ };
229
+ }
230
+ return {
231
+ ok: false,
232
+ error: protocolError("INVALID_FRAME", "gateway frame type must be event or res", record),
233
+ };
234
+ }
80
235
  function asContentItems(value) {
81
236
  if (!Array.isArray(value)) {
82
237
  return [];
@@ -129,7 +284,7 @@ function normalizeToolArgs(value) {
129
284
  }
130
285
  }
131
286
  function stringifyToolResult(value) {
132
- if (value == null) {
287
+ if (value === null || value === undefined) {
133
288
  return undefined;
134
289
  }
135
290
  if (typeof value === "string") {
@@ -144,6 +299,8 @@ function stringifyToolResult(value) {
144
299
  }
145
300
  function gatewayToolCallId(record) {
146
301
  const direct = (typeof record.id === "string" && record.id.trim()) ||
302
+ (typeof record.call_id === "string" && record.call_id.trim()) ||
303
+ (typeof record.callId === "string" && record.callId.trim()) ||
147
304
  (typeof record.toolCallId === "string" && record.toolCallId.trim()) ||
148
305
  (typeof record.tool_call_id === "string" && record.tool_call_id.trim());
149
306
  return direct || undefined;
@@ -171,7 +328,7 @@ function mergeGatewayToolResult(toolCalls, result) {
171
328
  index = cursor;
172
329
  break;
173
330
  }
174
- if (result.name && entry.name === result.name && entry.result == null) {
331
+ if (result.name && entry.name === result.name && (entry.result === null || entry.result === undefined)) {
175
332
  index = cursor;
176
333
  break;
177
334
  }
@@ -213,7 +370,18 @@ export function extractGatewayChatMediaUrls(message) {
213
370
  if (item.type !== "image" && item.type !== "audio" && item.type !== "input_audio" && item.type !== "output_audio") {
214
371
  continue;
215
372
  }
216
- const source = asRecord(item.source);
373
+ const isAudio = item.type === "audio" || item.type === "input_audio" || item.type === "output_audio";
374
+ if (typeof item.url === "string" && item.url.trim()) {
375
+ mediaUrls.push(item.url);
376
+ continue;
377
+ }
378
+ if (typeof item.openUrl === "string" && item.openUrl.trim()) {
379
+ mediaUrls.push(item.openUrl);
380
+ continue;
381
+ }
382
+ const source = asRecord(item.source) ?? (isAudio
383
+ ? asRecord(item.audio) ?? asRecord(item.input_audio) ?? asRecord(item.output_audio) ?? item
384
+ : null);
217
385
  if (!source) {
218
386
  continue;
219
387
  }
@@ -221,13 +389,43 @@ export function extractGatewayChatMediaUrls(message) {
221
389
  mediaUrls.push(source.url);
222
390
  continue;
223
391
  }
224
- if (source.type === "base64" && typeof source.data === "string" && source.data.trim()) {
225
- const mimeType = typeof source.media_type === "string" && source.media_type.trim()
226
- ? source.media_type.trim()
227
- : item.type === "image"
228
- ? "image/png"
229
- : "audio/mpeg";
230
- mediaUrls.push(`data:${mimeType};base64,${source.data}`);
392
+ if (isAudio && typeof source.url === "string" && source.url.trim()) {
393
+ mediaUrls.push(source.url.trim());
394
+ continue;
395
+ }
396
+ if (typeof source.data === "string" && source.data.trim()) {
397
+ if (isAudio && /^data:audio\//i.test(source.data.trim())) {
398
+ mediaUrls.push(source.data.trim());
399
+ continue;
400
+ }
401
+ const rawMimeType = [source.media_type, source.mime_type, item.media_type, item.mime_type]
402
+ .find((value) => typeof value === "string" && value.trim());
403
+ const rawFormat = [source.format, item.format]
404
+ .find((value) => typeof value === "string" && value.trim());
405
+ const format = typeof rawFormat === "string" ? rawFormat.trim().toLowerCase() : "";
406
+ const normalizedMimeType = typeof rawMimeType === "string"
407
+ ? rawMimeType.trim().split(";", 1)[0].trim()
408
+ : "";
409
+ const audioMimeTypes = {
410
+ aac: "audio/aac",
411
+ flac: "audio/flac",
412
+ m4a: "audio/mp4",
413
+ mp3: "audio/mpeg",
414
+ oga: "audio/ogg",
415
+ ogg: "audio/ogg",
416
+ opus: "audio/ogg",
417
+ wav: "audio/wav",
418
+ weba: "audio/webm",
419
+ webm: "audio/webm",
420
+ };
421
+ const mimeType = isAudio
422
+ ? (/^audio\/[A-Za-z0-9.+-]+$/i.test(normalizedMimeType)
423
+ ? normalizedMimeType
424
+ : audioMimeTypes[format] ?? "audio/mpeg")
425
+ : (/^image\/[A-Za-z0-9.+-]+$/i.test(normalizedMimeType)
426
+ ? normalizedMimeType
427
+ : "image/png");
428
+ mediaUrls.push(`data:${mimeType};base64,${source.data.trim()}`);
231
429
  }
232
430
  }
233
431
  if (typeof record.mediaUrl === "string" && record.mediaUrl.trim()) {
@@ -240,7 +438,7 @@ export function extractGatewayChatMediaUrls(message) {
240
438
  }
241
439
  }
242
440
  }
243
- return mediaUrls;
441
+ return Array.from(new Set(mediaUrls));
244
442
  }
245
443
  export function extractGatewayChatToolCalls(message) {
246
444
  const record = asRecord(message);
@@ -256,6 +454,8 @@ export function extractGatewayChatToolCalls(message) {
256
454
  kind === "tool_call" ||
257
455
  kind === "tooluse" ||
258
456
  kind === "tool_use" ||
457
+ kind === "function_call" ||
458
+ kind === "functioncall" ||
259
459
  (name && (item.arguments !== undefined || item.args !== undefined))) {
260
460
  toolCalls.push({
261
461
  ...(id ? { id } : {}),
@@ -264,11 +464,11 @@ export function extractGatewayChatToolCalls(message) {
264
464
  });
265
465
  continue;
266
466
  }
267
- if (kind === "toolresult" || kind === "tool_result") {
467
+ if (kind === "toolresult" || kind === "tool_result" || kind === "function_call_output" || kind === "functioncalloutput") {
268
468
  toolCalls = mergeGatewayToolResult(toolCalls, {
269
469
  ...(id ? { id } : {}),
270
470
  name: name ?? "tool",
271
- result: stringifyToolResult(item.text ?? item.content ?? item.result),
471
+ result: stringifyToolResult(item.text ?? item.content ?? item.result ?? item.output),
272
472
  });
273
473
  }
274
474
  }
@@ -284,6 +484,9 @@ export function extractGatewayChatToolCalls(message) {
284
484
  ...(gatewayToolCallId(tool) ? { id: gatewayToolCallId(tool) } : {}),
285
485
  name,
286
486
  args: normalizeToolArgs(tool.arguments ?? tool.args),
487
+ ...(stringifyToolResult(tool.result ?? tool.output ?? tool.content ?? tool.text) !== undefined
488
+ ? { result: stringifyToolResult(tool.result ?? tool.output ?? tool.content ?? tool.text) }
489
+ : {}),
287
490
  });
288
491
  }
289
492
  }
@@ -292,7 +495,7 @@ export function extractGatewayChatToolCalls(message) {
292
495
  const role = typeof record.role === "string" ? record.role.trim().toLowerCase() : "";
293
496
  if (topLevelToolName &&
294
497
  topLevelResult &&
295
- (role === "toolresult" || role === "tool_result" || record.toolCallId || record.tool_call_id)) {
498
+ (role === "toolresult" || role === "tool_result" || role === "function_call_output" || record.toolCallId || record.tool_call_id || record.call_id || record.callId)) {
296
499
  toolCalls = mergeGatewayToolResult(toolCalls, {
297
500
  ...(gatewayToolCallId(record) ? { id: gatewayToolCallId(record) } : {}),
298
501
  name: topLevelToolName,
@@ -312,6 +515,11 @@ export function normalizeGatewayChatMessage(message) {
312
515
  const mediaUrls = extractGatewayChatMediaUrls(record);
313
516
  const timestamp = typeof record.timestamp === "number" ? record.timestamp : undefined;
314
517
  const role = typeof record.role === "string" && record.role.trim() ? record.role : "assistant";
518
+ const messageId = protocolIdentityString(record.messageId);
519
+ const turnId = protocolIdentityString(record.turnId);
520
+ const runId = extractMessageRunId(record) ?? undefined;
521
+ const sessionKey = chatPayloadSessionKey(record);
522
+ const revision = protocolRevision(record.revision);
315
523
  if (!text && !thinking && toolCalls.length === 0 && mediaUrls.length === 0) {
316
524
  return null;
317
525
  }
@@ -322,6 +530,11 @@ export function normalizeGatewayChatMessage(message) {
322
530
  toolCalls,
323
531
  mediaUrls,
324
532
  ...(timestamp !== undefined ? { timestamp } : {}),
533
+ ...(messageId ? { messageId } : {}),
534
+ ...(turnId ? { turnId } : {}),
535
+ ...(runId ? { runId } : {}),
536
+ ...(sessionKey ? { sessionKey } : {}),
537
+ ...(revision !== undefined ? { revision } : {}),
325
538
  };
326
539
  }
327
540
  function extractMessageText(message) {
@@ -369,7 +582,25 @@ function extractMessageRunId(message) {
369
582
  const metaRunId = typeof meta?.runId === "string" ? meta.runId.trim() : "";
370
583
  return metaRunId || null;
371
584
  }
372
- function latestHistoryAssistantText(messages, acceptedRunIds) {
585
+ function historyAssistantBaseline(messages, priorAssistantTexts = []) {
586
+ const assistantTexts = messages.flatMap((candidate) => {
587
+ const message = asRecord(candidate);
588
+ const role = typeof message?.role === "string" ? message.role.trim().toLowerCase() : "";
589
+ const text = role === "assistant" ? extractMessageText(message)?.trim() : "";
590
+ return text ? [text] : [];
591
+ });
592
+ const useTrackedPrior = priorAssistantTexts.length > assistantTexts.length;
593
+ return {
594
+ count: Math.max(assistantTexts.length, priorAssistantTexts.length),
595
+ latestText: useTrackedPrior
596
+ ? priorAssistantTexts[priorAssistantTexts.length - 1] ?? ""
597
+ : assistantTexts[assistantTexts.length - 1] ?? "",
598
+ };
599
+ }
600
+ function latestHistoryAssistantText(messages, acceptedRunIds, baseline) {
601
+ const currentBaseline = historyAssistantBaseline(messages);
602
+ const hasNewUncorrelatedAssistant = baseline === undefined || Boolean(baseline && (currentBaseline.count > baseline.count ||
603
+ currentBaseline.latestText !== baseline.latestText));
373
604
  for (let index = messages.length - 1; index >= 0; index -= 1) {
374
605
  const message = asRecord(messages[index]);
375
606
  if (!message) {
@@ -383,6 +614,9 @@ function latestHistoryAssistantText(messages, acceptedRunIds) {
383
614
  if (messageRunId && acceptedRunIds.size > 0 && !acceptedRunIds.has(messageRunId)) {
384
615
  continue;
385
616
  }
617
+ if (!messageRunId && !hasNewUncorrelatedAssistant) {
618
+ continue;
619
+ }
386
620
  const text = extractMessageText(message)?.trim();
387
621
  if (text) {
388
622
  return text;
@@ -402,6 +636,67 @@ function streamDelta(previousText, nextText) {
402
636
  }
403
637
  return { delta: nextText, nextText };
404
638
  }
639
+ function appendStreamContent(previousText, text, replace) {
640
+ if (replace)
641
+ return reconcileStreamContent(previousText, text, true);
642
+ if (!text)
643
+ return null;
644
+ return { text, nextText: previousText + text, replace: false };
645
+ }
646
+ function reconcileStreamContent(previousText, snapshot, replace = false) {
647
+ if (replace)
648
+ return { text: snapshot, nextText: snapshot, replace: true };
649
+ if (snapshot === previousText)
650
+ return null;
651
+ if (snapshot.startsWith(previousText)) {
652
+ const text = snapshot.slice(previousText.length);
653
+ return text ? { text, nextText: snapshot, replace: false } : null;
654
+ }
655
+ if (previousText.startsWith(snapshot))
656
+ return null;
657
+ return { text: snapshot, nextText: snapshot, replace: true };
658
+ }
659
+ function reconcileHistoryStreamContent(previousText, snapshot) {
660
+ if (snapshot.endsWith(OPENCLAW_CHAT_HISTORY_TRUNCATION_SUFFIX)) {
661
+ const projectedPrefix = snapshot.slice(0, -OPENCLAW_CHAT_HISTORY_TRUNCATION_SUFFIX.length);
662
+ if (previousText.startsWith(projectedPrefix))
663
+ return null;
664
+ }
665
+ return reconcileStreamContent(previousText, snapshot);
666
+ }
667
+ function protocolIdentityString(value) {
668
+ return typeof value === "string" && value.trim() ? value : undefined;
669
+ }
670
+ function chatPayloadSessionKey(payload) {
671
+ return protocolIdentityString(payload.canonicalSessionKey) ?? protocolIdentityString(payload.sessionKey);
672
+ }
673
+ function protocolRevision(value) {
674
+ return typeof value === "number" && Number.isFinite(value)
675
+ ? value
676
+ : protocolIdentityString(value);
677
+ }
678
+ function chatEventIdentity(payload) {
679
+ const message = asRecord(payload.message);
680
+ const eventId = protocolIdentityString(payload.eventId) ?? protocolIdentityString(message?.eventId);
681
+ const messageId = protocolIdentityString(payload.messageId) ?? protocolIdentityString(message?.messageId);
682
+ const turnId = protocolIdentityString(payload.turnId) ?? protocolIdentityString(message?.turnId);
683
+ const runId = protocolIdentityString(payload.runId) ?? extractMessageRunId(message);
684
+ const sessionKey = chatPayloadSessionKey(payload) ?? (message ? chatPayloadSessionKey(message) : undefined);
685
+ const revision = protocolRevision(payload.revision) ?? protocolRevision(message?.revision);
686
+ return {
687
+ ...(eventId ? { eventId } : {}),
688
+ ...(messageId ? { messageId } : {}),
689
+ ...(turnId ? { turnId } : {}),
690
+ ...(runId ? { runId } : {}),
691
+ ...(sessionKey ? { sessionKey } : {}),
692
+ ...(revision !== undefined ? { revision } : {}),
693
+ };
694
+ }
695
+ function applyChatEventContent(currentText, event) {
696
+ if (event.type !== "content")
697
+ return currentText;
698
+ return event.replace === true ? event.text ?? "" : currentText + (event.text ?? "");
699
+ }
405
700
  function parseAgentSessionKey(sessionKey) {
406
701
  const normalized = (sessionKey ?? "").trim().toLowerCase();
407
702
  if (!normalized) {
@@ -440,6 +735,89 @@ function sameSessionKey(left, right) {
440
735
  }
441
736
  return false;
442
737
  }
738
+ function isReservedEphemeralSessionKey(sessionKey) {
739
+ return /^session-hypercli-ephemeral-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(sessionKey.trim().toLowerCase());
740
+ }
741
+ function validateEphemeralCanonicalKey(requestedKey, canonicalKey) {
742
+ if (!isReservedEphemeralSessionKey(requestedKey))
743
+ return null;
744
+ if (canonicalKey === requestedKey)
745
+ return canonicalKey;
746
+ if (!canonicalKey.startsWith("agent:"))
747
+ return null;
748
+ const agentSeparator = canonicalKey.indexOf(":", "agent:".length);
749
+ if (agentSeparator <= "agent:".length)
750
+ return null;
751
+ if (!canonicalKey.slice("agent:".length, agentSeparator).trim())
752
+ return null;
753
+ return canonicalKey.slice(agentSeparator + 1) === requestedKey ? canonicalKey : null;
754
+ }
755
+ function valueReferencesEphemeralSession(value, activeSessionKeys, seen = new WeakSet()) {
756
+ if (typeof value === "string") {
757
+ const candidate = value.trim();
758
+ const unscoped = parseAgentSessionKey(candidate)?.rest ?? candidate;
759
+ return isReservedEphemeralSessionKey(unscoped) || activeSessionKeys.some((key) => sameSessionKey(candidate, key));
760
+ }
761
+ if (!value || typeof value !== "object" || seen.has(value))
762
+ return false;
763
+ seen.add(value);
764
+ if (Array.isArray(value)) {
765
+ return value.some((item) => valueReferencesEphemeralSession(item, activeSessionKeys, seen));
766
+ }
767
+ return Object.values(value).some((item) => valueReferencesEphemeralSession(item, activeSessionKeys, seen));
768
+ }
769
+ async function promiseSettlesWithin(promise, timeoutMs) {
770
+ let timeout = null;
771
+ try {
772
+ return await Promise.race([
773
+ promise.then(() => true, () => true),
774
+ new Promise((resolve) => {
775
+ timeout = setTimeout(() => resolve(false), timeoutMs);
776
+ }),
777
+ ]);
778
+ }
779
+ finally {
780
+ if (timeout)
781
+ clearTimeout(timeout);
782
+ }
783
+ }
784
+ async function booleanPromiseWithin(promise, timeoutMs) {
785
+ let timeout = null;
786
+ try {
787
+ return await Promise.race([
788
+ promise.catch(() => false),
789
+ new Promise((resolve) => {
790
+ timeout = setTimeout(() => resolve(false), timeoutMs);
791
+ }),
792
+ ]);
793
+ }
794
+ finally {
795
+ if (timeout)
796
+ clearTimeout(timeout);
797
+ }
798
+ }
799
+ function isChatStreamGatewayEvent(event) {
800
+ if (event.event === "chat" || event.event.startsWith("chat."))
801
+ return true;
802
+ if (event.event !== "agent")
803
+ return false;
804
+ const payload = asRecord(event.payload) ?? {};
805
+ const stream = typeof payload.stream === "string" ? payload.stream.toLowerCase() : "";
806
+ return stream === "tool" || stream === "lifecycle";
807
+ }
808
+ function isTerminalChatStreamGatewayEvent(event) {
809
+ if (event.event === "chat.done" || event.event === "chat.error" || event.event === "chat.aborted")
810
+ return true;
811
+ const payload = asRecord(event.payload) ?? {};
812
+ if (event.event === "agent" && String(payload.stream ?? "").toLowerCase() === "lifecycle") {
813
+ const phase = String(asRecord(payload.data)?.phase ?? "").toLowerCase();
814
+ return phase === "end";
815
+ }
816
+ if (event.event !== "chat")
817
+ return false;
818
+ const state = String(payload.state ?? "").toLowerCase();
819
+ return state === "final" || state === "error" || state === "aborted";
820
+ }
443
821
  function splitConfigPath(path) {
444
822
  return path
445
823
  .split(".")
@@ -665,6 +1043,23 @@ function normalizeClientMode(value) {
665
1043
  const normalized = value?.trim().toLowerCase();
666
1044
  return normalized && VALID_CLIENT_MODES.has(normalized) ? normalized : DEFAULT_CLIENT_MODE;
667
1045
  }
1046
+ function normalizeOptionalString(value) {
1047
+ return value?.trim() || undefined;
1048
+ }
1049
+ function normalizeProtocolVersion(value, fallback) {
1050
+ return typeof value === "number" && Number.isFinite(value) && value > 0
1051
+ ? Math.trunc(value)
1052
+ : fallback;
1053
+ }
1054
+ function normalizePermissions(value) {
1055
+ if (!value || typeof value !== "object") {
1056
+ return undefined;
1057
+ }
1058
+ const entries = Object.entries(value)
1059
+ .filter(([key, allowed]) => key.trim() && typeof allowed === "boolean")
1060
+ .map(([key, allowed]) => [key.trim(), allowed]);
1061
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
1062
+ }
668
1063
  function getNavigatorLike() {
669
1064
  const maybeNavigator = globalThis.navigator;
670
1065
  return maybeNavigator ?? null;
@@ -1072,6 +1467,11 @@ export class GatewayClient {
1072
1467
  url;
1073
1468
  token;
1074
1469
  gatewayToken;
1470
+ bootstrapToken;
1471
+ deviceToken;
1472
+ password;
1473
+ approvalRuntimeToken;
1474
+ agentRuntimeIdentityToken;
1075
1475
  deploymentId;
1076
1476
  apiKey;
1077
1477
  apiBase;
@@ -1081,13 +1481,31 @@ export class GatewayClient {
1081
1481
  clientDisplayName;
1082
1482
  clientVersion;
1083
1483
  clientPlatform;
1484
+ clientDeviceFamily;
1084
1485
  clientInstanceId;
1085
1486
  caps;
1487
+ role;
1488
+ scopes;
1489
+ commands;
1490
+ permissions;
1491
+ pathEnv;
1492
+ minProtocol;
1493
+ maxProtocol;
1086
1494
  origin;
1087
1495
  defaultTimeout;
1088
1496
  ws = null;
1089
1497
  pending = new Map();
1090
1498
  eventHandlers = new Set();
1499
+ internalEventHandlers = new Set();
1500
+ internalStreamCloseHandlers = new Set();
1501
+ activeNormalChatStreams = new Set();
1502
+ activeStrictChatStreams = new Set();
1503
+ activeEphemeralSessionKeys = new Map();
1504
+ suppressedEphemeralRunIds = new Map();
1505
+ pendingEphemeralChatAcks = 0;
1506
+ deferredEphemeralCorrelationEvents = new Set();
1507
+ deferredEphemeralEventsCanRepublish = true;
1508
+ suppressUncorrelatedChatEventsUntil = 0;
1091
1509
  connectionStateHandlers = new Set();
1092
1510
  connected = false;
1093
1511
  connectionState = "disconnected";
@@ -1112,37 +1530,58 @@ export class GatewayClient {
1112
1530
  onDisconnect = null;
1113
1531
  constructor(options) {
1114
1532
  this.url = options.url;
1115
- this.token = options.token?.trim() || undefined;
1116
- this.gatewayToken = options.gatewayToken?.trim() || undefined;
1117
- this.deploymentId = options.deploymentId?.trim() || undefined;
1118
- this.apiKey = options.apiKey?.trim() || undefined;
1533
+ this.token = normalizeOptionalString(options.token);
1534
+ this.gatewayToken = normalizeOptionalString(options.gatewayToken);
1535
+ this.bootstrapToken = normalizeOptionalString(options.bootstrapToken);
1536
+ this.deviceToken = normalizeOptionalString(options.deviceToken);
1537
+ this.password = normalizeOptionalString(options.password);
1538
+ this.approvalRuntimeToken = normalizeOptionalString(options.approvalRuntimeToken);
1539
+ this.agentRuntimeIdentityToken = normalizeOptionalString(options.agentRuntimeIdentityToken);
1540
+ this.deploymentId = normalizeOptionalString(options.deploymentId);
1541
+ this.apiKey = normalizeOptionalString(options.apiKey);
1119
1542
  this.apiBase = options.apiBase?.trim().replace(/\/$/, "") || undefined;
1120
1543
  this.autoApprovePairing = options.autoApprovePairing === true;
1121
1544
  this.clientId = normalizeClientId(options.clientId);
1122
1545
  this.clientMode = normalizeClientMode(options.clientMode);
1123
1546
  this.clientVersion = options.clientVersion?.trim() || DEFAULT_CLIENT_VERSION;
1124
1547
  this.clientPlatform = resolvePlatform(options.platform);
1548
+ this.clientDeviceFamily = normalizeOptionalString(options.deviceFamily);
1125
1549
  this.clientDisplayName = resolveClientDisplayName(options.clientDisplayName, this.clientPlatform);
1126
- this.clientInstanceId = options.instanceId?.trim() || makeId();
1550
+ this.clientInstanceId = normalizeOptionalString(options.instanceId) || makeId();
1127
1551
  this.caps = Array.isArray(options.caps)
1128
1552
  ? options.caps.map((cap) => cap.trim()).filter(Boolean)
1129
1553
  : [...DEFAULT_CAPS];
1554
+ this.role = options.role?.trim() || OPERATOR_ROLE;
1555
+ this.scopes = Array.isArray(options.scopes)
1556
+ ? options.scopes.map((scope) => scope.trim()).filter(Boolean)
1557
+ : this.role === OPERATOR_ROLE
1558
+ ? [...OPERATOR_SCOPES]
1559
+ : [];
1560
+ this.commands = Array.isArray(options.commands)
1561
+ ? options.commands.map((command) => command.trim()).filter(Boolean)
1562
+ : undefined;
1563
+ this.permissions = normalizePermissions(options.permissions);
1564
+ this.pathEnv = normalizeOptionalString(options.pathEnv);
1565
+ this.minProtocol = normalizeProtocolVersion(options.minProtocol, MIN_GATEWAY_VERSION);
1566
+ this.maxProtocol = normalizeProtocolVersion(options.maxProtocol, MAX_GATEWAY_VERSION);
1130
1567
  // Non-browser SDK clients should not send Origin by default. OpenClaw
1131
1568
  // treats any Origin header as browser-originated and applies browser
1132
1569
  // origin checks to the connection.
1133
1570
  this.origin = typeof options.origin === "string" && options.origin.trim()
1134
1571
  ? options.origin.trim()
1135
1572
  : undefined;
1136
- this.defaultTimeout = options.timeout ?? DEFAULT_TIMEOUT;
1573
+ this.defaultTimeout = options.timeout ?? DEFAULT_CONNECTION_TIMEOUT;
1137
1574
  this.onHello = options.onHello;
1138
1575
  this.onClose = options.onClose;
1139
1576
  this.onGap = options.onGap;
1577
+ this.onProtocolError = options.onProtocolError;
1140
1578
  this.onPairing = options.onPairing;
1141
- this.pairingState = loadPendingPairing(this.storageScope(), OPERATOR_ROLE);
1579
+ this.pairingState = loadPendingPairing(this.storageScope(), this.role);
1142
1580
  }
1143
1581
  onHello;
1144
1582
  onClose;
1145
1583
  onGap;
1584
+ onProtocolError;
1146
1585
  onPairing;
1147
1586
  get version() {
1148
1587
  return this._version;
@@ -1188,6 +1627,153 @@ export class GatewayClient {
1188
1627
  this.eventHandlers.add(handler);
1189
1628
  return () => this.eventHandlers.delete(handler);
1190
1629
  }
1630
+ currentEphemeralSessionKeys() {
1631
+ const now = Date.now();
1632
+ for (const [sessionKey, expiresAt] of this.activeEphemeralSessionKeys) {
1633
+ if (expiresAt !== null && expiresAt <= now)
1634
+ this.activeEphemeralSessionKeys.delete(sessionKey);
1635
+ }
1636
+ return [...this.activeEphemeralSessionKeys.keys()];
1637
+ }
1638
+ shouldSuppressPublicEvent(event, handledInternally) {
1639
+ if (handledInternally)
1640
+ return true;
1641
+ const payload = asRecord(event.payload) ?? {};
1642
+ const activeSessionKeys = this.currentEphemeralSessionKeys();
1643
+ if (valueReferencesEphemeralSession(payload, activeSessionKeys))
1644
+ return true;
1645
+ const sessionKey = chatPayloadSessionKey(payload)?.trim() ?? "";
1646
+ const unscopedSessionKey = parseAgentSessionKey(sessionKey)?.rest ?? sessionKey;
1647
+ if (isReservedEphemeralSessionKey(unscopedSessionKey))
1648
+ return true;
1649
+ if (sessionKey &&
1650
+ activeSessionKeys.some((activeKey) => sameSessionKey(sessionKey, activeKey))) {
1651
+ return true;
1652
+ }
1653
+ const runId = typeof payload.runId === "string" ? payload.runId.trim() : "";
1654
+ const suppressedRunExpiry = runId ? this.suppressedEphemeralRunIds.get(runId) : undefined;
1655
+ if (suppressedRunExpiry !== undefined) {
1656
+ if (suppressedRunExpiry > Date.now())
1657
+ return true;
1658
+ this.suppressedEphemeralRunIds.delete(runId);
1659
+ }
1660
+ if (runId &&
1661
+ !sessionKey &&
1662
+ isChatStreamGatewayEvent(event) &&
1663
+ this.suppressUncorrelatedChatEventsUntil > Date.now()) {
1664
+ return true;
1665
+ }
1666
+ if (activeSessionKeys.length > 0 && event.event === "activity.log" && !sessionKey && !runId) {
1667
+ return true;
1668
+ }
1669
+ return (activeSessionKeys.length > 0 &&
1670
+ !sessionKey &&
1671
+ !runId &&
1672
+ isChatStreamGatewayEvent(event));
1673
+ }
1674
+ suppressEphemeralRunId(runId) {
1675
+ const normalized = runId.trim();
1676
+ if (!normalized)
1677
+ return;
1678
+ const now = Date.now();
1679
+ for (const [candidate, expiresAt] of this.suppressedEphemeralRunIds) {
1680
+ if (expiresAt <= now)
1681
+ this.suppressedEphemeralRunIds.delete(candidate);
1682
+ }
1683
+ this.suppressedEphemeralRunIds.set(normalized, now + EPHEMERAL_RUN_ID_SUPPRESSION_MS);
1684
+ }
1685
+ beginEphemeralChatAcknowledgement() {
1686
+ if (this.pendingEphemeralChatAcks === 0)
1687
+ this.deferredEphemeralEventsCanRepublish = true;
1688
+ this.pendingEphemeralChatAcks += 1;
1689
+ }
1690
+ deferEphemeralCorrelationEvent(event) {
1691
+ if (this.pendingEphemeralChatAcks > 0)
1692
+ this.deferredEphemeralCorrelationEvents.add(event);
1693
+ }
1694
+ finishEphemeralChatAcknowledgement(canRepublishDeferredEvents) {
1695
+ if (this.pendingEphemeralChatAcks <= 0)
1696
+ return;
1697
+ this.deferredEphemeralEventsCanRepublish &&= canRepublishDeferredEvents;
1698
+ this.pendingEphemeralChatAcks -= 1;
1699
+ if (this.pendingEphemeralChatAcks > 0)
1700
+ return;
1701
+ if (!this.deferredEphemeralEventsCanRepublish) {
1702
+ this.suppressUncorrelatedChatEventsUntil = Math.max(this.suppressUncorrelatedChatEventsUntil, Date.now() + EPHEMERAL_RUN_ID_SUPPRESSION_MS);
1703
+ }
1704
+ const deferredEvents = [...this.deferredEphemeralCorrelationEvents];
1705
+ this.deferredEphemeralCorrelationEvents.clear();
1706
+ const shouldRepublish = this.deferredEphemeralEventsCanRepublish;
1707
+ this.deferredEphemeralEventsCanRepublish = true;
1708
+ if (shouldRepublish)
1709
+ deferredEvents.forEach((event) => this.publishPublicEvent(event));
1710
+ }
1711
+ publishPublicEvent(event, handledInternally = false) {
1712
+ if (this.shouldSuppressPublicEvent(event, handledInternally))
1713
+ return;
1714
+ for (const handler of this.eventHandlers) {
1715
+ try {
1716
+ handler(event);
1717
+ }
1718
+ catch {
1719
+ // Event handlers are isolated from the socket lifecycle.
1720
+ }
1721
+ }
1722
+ }
1723
+ notifyInternalStreamClose(error) {
1724
+ for (const handler of this.internalStreamCloseHandlers) {
1725
+ try {
1726
+ handler(error);
1727
+ }
1728
+ catch {
1729
+ // Stream close handlers are isolated from the socket lifecycle.
1730
+ }
1731
+ }
1732
+ }
1733
+ interruptNormalChatStreams(error) {
1734
+ for (const stream of [...this.activeNormalChatStreams])
1735
+ stream.close(error);
1736
+ }
1737
+ handleForwardSequenceGap(info, receivedEvent) {
1738
+ try {
1739
+ this.onGap?.(info);
1740
+ }
1741
+ catch {
1742
+ // Gap callbacks are observational and must not affect the socket.
1743
+ }
1744
+ queueMicrotask(() => {
1745
+ const error = new GatewayChatStreamInterruptedError("sequence-gap", info);
1746
+ const streams = new Set([
1747
+ ...this.activeNormalChatStreams,
1748
+ ...this.activeStrictChatStreams,
1749
+ ]);
1750
+ for (const stream of streams) {
1751
+ if (stream.terminalEvent !== receivedEvent)
1752
+ stream.close(error);
1753
+ }
1754
+ });
1755
+ }
1756
+ reportProtocolError(info) {
1757
+ try {
1758
+ this.onProtocolError?.(info);
1759
+ }
1760
+ catch {
1761
+ // Protocol diagnostics are observational and must not affect the socket.
1762
+ }
1763
+ }
1764
+ rejectMalformedPendingResponse(info) {
1765
+ if (info.frameType !== "res" || !info.requestId)
1766
+ return;
1767
+ const pending = this.pending.get(info.requestId);
1768
+ if (!pending)
1769
+ return;
1770
+ this.pending.delete(info.requestId);
1771
+ pending.cleanup();
1772
+ pending.reject(new GatewayRequestError({
1773
+ code: "PROTOCOL_ERROR",
1774
+ message: info.message,
1775
+ }));
1776
+ }
1191
1777
  /** Connect and keep reconnecting until stopped */
1192
1778
  connect() {
1193
1779
  return this.start();
@@ -1231,7 +1817,9 @@ export class GatewayClient {
1231
1817
  if (ws) {
1232
1818
  ws.close();
1233
1819
  }
1234
- this.flushPending(new Error("gateway client stopped"));
1820
+ const stoppedError = new Error("gateway client stopped");
1821
+ this.flushPending(stoppedError);
1822
+ this.notifyInternalStreamClose(stoppedError);
1235
1823
  if (this.rejectConnectPromise) {
1236
1824
  this.rejectConnectPromise(new Error("gateway client stopped"));
1237
1825
  }
@@ -1245,7 +1833,7 @@ export class GatewayClient {
1245
1833
  storePendingPairing(pairing, this.storageScope());
1246
1834
  }
1247
1835
  else {
1248
- clearPendingPairing(this.storageScope(), OPERATOR_ROLE);
1836
+ clearPendingPairing(this.storageScope(), this.role);
1249
1837
  }
1250
1838
  this.onPairing?.(pairing);
1251
1839
  }
@@ -1299,6 +1887,7 @@ export class GatewayClient {
1299
1887
  ? new WebSocket(wsUrl)
1300
1888
  : new NodeWebSocket(wsUrl, this.origin ? { headers: { Origin: this.origin } } : undefined);
1301
1889
  this.ws = ws;
1890
+ this.lastSeq = null;
1302
1891
  if (useBrowserSocket) {
1303
1892
  ws.onopen = () => {
1304
1893
  this.queueConnect();
@@ -1361,7 +1950,7 @@ export class GatewayClient {
1361
1950
  }
1362
1951
  flushPending(error) {
1363
1952
  for (const [, pending] of this.pending) {
1364
- clearTimeout(pending.timer);
1953
+ pending.cleanup();
1365
1954
  pending.reject(error);
1366
1955
  }
1367
1956
  this.pending.clear();
@@ -1381,7 +1970,9 @@ export class GatewayClient {
1381
1970
  }
1382
1971
  const error = this.pendingConnectError;
1383
1972
  this.pendingConnectError = null;
1384
- this.flushPending(new Error(`gateway closed (${code}): ${reason || "no reason"}`));
1973
+ const closeError = new Error(`gateway closed (${code}): ${reason || "no reason"}`);
1974
+ this.flushPending(closeError);
1975
+ this.notifyInternalStreamClose(closeError);
1385
1976
  this.onClose?.({ code, reason, error });
1386
1977
  if (!this.closed) {
1387
1978
  this.onDisconnect?.();
@@ -1419,34 +2010,53 @@ export class GatewayClient {
1419
2010
  let storedDeviceToken = null;
1420
2011
  try {
1421
2012
  identity = await loadOrCreateDeviceIdentity();
1422
- storedDeviceToken = loadStoredDeviceToken(identity.deviceId, this.storageScope(), OPERATOR_ROLE)?.token ?? null;
1423
- const authToken = this.gatewayToken ?? storedDeviceToken;
1424
- const authDeviceToken = this.pendingDeviceTokenRetry ? (storedDeviceToken ?? undefined) : undefined;
2013
+ storedDeviceToken = loadStoredDeviceToken(identity.deviceId, this.storageScope(), this.role)?.token ?? null;
2014
+ const resolvedDeviceToken = this.deviceToken ?? storedDeviceToken ?? undefined;
2015
+ const authToken = this.gatewayToken ?? resolvedDeviceToken;
2016
+ const authBootstrapToken = !this.gatewayToken && !resolvedDeviceToken && !this.password
2017
+ ? this.bootstrapToken
2018
+ : undefined;
2019
+ const authDeviceToken = this.deviceToken ?? (this.pendingDeviceTokenRetry ? resolvedDeviceToken : undefined);
2020
+ const signatureToken = authToken ?? authBootstrapToken;
1425
2021
  const signedAtMs = Date.now();
1426
2022
  const payload = buildDeviceAuthPayload({
1427
2023
  deviceId: identity.deviceId,
1428
2024
  clientId: this.clientId,
1429
2025
  clientMode: this.clientMode,
1430
- role: OPERATOR_ROLE,
1431
- scopes: OPERATOR_SCOPES,
2026
+ role: this.role,
2027
+ scopes: this.scopes,
1432
2028
  signedAtMs,
1433
- token: authToken ?? null,
2029
+ token: signatureToken ?? null,
1434
2030
  nonce,
1435
2031
  });
1436
2032
  const signature = await signDevicePayload(identity.privateKey, payload);
2033
+ const auth = {};
2034
+ if (authToken)
2035
+ auth.token = authToken;
2036
+ if (authBootstrapToken)
2037
+ auth.bootstrapToken = authBootstrapToken;
2038
+ if (authDeviceToken)
2039
+ auth.deviceToken = authDeviceToken;
2040
+ if (this.password)
2041
+ auth.password = this.password;
2042
+ if (this.approvalRuntimeToken)
2043
+ auth.approvalRuntimeToken = this.approvalRuntimeToken;
2044
+ if (this.agentRuntimeIdentityToken)
2045
+ auth.agentRuntimeIdentityToken = this.agentRuntimeIdentityToken;
1437
2046
  const params = {
1438
- minProtocol: MIN_PROTOCOL_VERSION,
1439
- maxProtocol: PROTOCOL_VERSION,
2047
+ minProtocol: this.minProtocol,
2048
+ maxProtocol: this.maxProtocol,
1440
2049
  client: {
1441
2050
  id: this.clientId,
1442
2051
  ...(this.clientDisplayName ? { displayName: this.clientDisplayName } : {}),
1443
2052
  version: this.clientVersion,
1444
2053
  platform: this.clientPlatform,
2054
+ ...(this.clientDeviceFamily ? { deviceFamily: this.clientDeviceFamily } : {}),
1445
2055
  mode: this.clientMode,
1446
2056
  ...(this.clientInstanceId ? { instanceId: this.clientInstanceId } : {}),
1447
2057
  },
1448
- role: OPERATOR_ROLE,
1449
- scopes: [...OPERATOR_SCOPES],
2058
+ role: this.role,
2059
+ scopes: [...this.scopes],
1450
2060
  device: {
1451
2061
  id: identity.deviceId,
1452
2062
  publicKey: identity.publicKey,
@@ -1455,30 +2065,21 @@ export class GatewayClient {
1455
2065
  nonce,
1456
2066
  },
1457
2067
  caps: this.caps,
1458
- ...(authToken
1459
- ? {
1460
- auth: {
1461
- ...(authToken ? { token: authToken } : {}),
1462
- ...(authDeviceToken ? { deviceToken: authDeviceToken } : {}),
1463
- },
1464
- }
1465
- : authDeviceToken
1466
- ? {
1467
- auth: {
1468
- deviceToken: authDeviceToken,
1469
- },
1470
- }
1471
- : {}),
2068
+ ...(this.commands ? { commands: this.commands } : {}),
2069
+ ...(this.permissions ? { permissions: this.permissions } : {}),
2070
+ ...(this.pathEnv ? { pathEnv: this.pathEnv } : {}),
2071
+ ...(Object.keys(auth).length ? { auth } : {}),
1472
2072
  ...(resolveUserAgent() ? { userAgent: resolveUserAgent() } : {}),
1473
2073
  ...(resolveLocale() ? { locale: resolveLocale() } : {}),
1474
2074
  };
1475
- const hello = await this.sendRawRequest("connect", params, this.defaultTimeout, true);
2075
+ const hello = await this.sendRawRequest("connect", params, { timeoutMs: this.defaultTimeout }, true);
1476
2076
  if (hello?.auth?.deviceToken) {
2077
+ this.deviceToken = hello.auth.deviceToken;
1477
2078
  storeStoredDeviceToken({
1478
2079
  deviceId: identity.deviceId,
1479
2080
  scope: this.storageScope(),
1480
2081
  gatewayUrl: this.url,
1481
- role: hello.auth.role ?? OPERATOR_ROLE,
2082
+ role: hello.auth.role ?? this.role,
1482
2083
  token: hello.auth.deviceToken,
1483
2084
  scopes: hello.auth.scopes ?? [],
1484
2085
  });
@@ -1510,7 +2111,7 @@ export class GatewayClient {
1510
2111
  // one-shot flag to prevent infinite recursion if the retry also fails.
1511
2112
  if (identity && detailCode === CONNECT_ERROR_DEVICE_TOKEN_MISMATCH && !this.deviceTokenMismatchRetried) {
1512
2113
  this.deviceTokenMismatchRetried = true;
1513
- clearStoredDeviceToken(identity.deviceId, this.storageScope(), OPERATOR_ROLE);
2114
+ clearStoredDeviceToken(identity.deviceId, this.storageScope(), this.role);
1514
2115
  this.connectSent = false;
1515
2116
  this.pendingConnectError = null;
1516
2117
  this.pendingDeviceTokenRetry = false;
@@ -1531,7 +2132,7 @@ export class GatewayClient {
1531
2132
  return;
1532
2133
  }
1533
2134
  if (identity && detailCode === CONNECT_ERROR_PAIRING_REQUIRED) {
1534
- clearStoredDeviceToken(identity.deviceId, this.storageScope(), OPERATOR_ROLE);
2135
+ clearStoredDeviceToken(identity.deviceId, this.storageScope(), this.role);
1535
2136
  }
1536
2137
  if (detailCode === CONNECT_ERROR_PAIRING_REQUIRED && requestId) {
1537
2138
  if (this.canAutoApprovePairing() && !this.autoApproveAttemptedRequestIds.has(requestId)) {
@@ -1556,7 +2157,7 @@ export class GatewayClient {
1556
2157
  this.pendingConnectError = toCloseError(approvalError);
1557
2158
  this.updatePairingState({
1558
2159
  requestId,
1559
- role: OPERATOR_ROLE,
2160
+ role: this.role,
1560
2161
  gatewayUrl: this.url,
1561
2162
  ...(identity ? { deviceId: identity.deviceId } : {}),
1562
2163
  status: "failed",
@@ -1569,7 +2170,7 @@ export class GatewayClient {
1569
2170
  // No auto-approve — surface pairing state so the UI can prompt.
1570
2171
  this.updatePairingState({
1571
2172
  requestId,
1572
- role: OPERATOR_ROLE,
2173
+ role: this.role,
1573
2174
  gatewayUrl: this.url,
1574
2175
  ...(identity ? { deviceId: identity.deviceId } : {}),
1575
2176
  status: "pending",
@@ -1583,19 +2184,18 @@ export class GatewayClient {
1583
2184
  }
1584
2185
  }
1585
2186
  handleMessage(raw) {
1586
- let message;
1587
- try {
1588
- message = JSON.parse(raw);
1589
- }
1590
- catch {
2187
+ const decoded = decodeGatewayFrame(raw);
2188
+ if (!decoded.ok) {
2189
+ this.reportProtocolError(decoded.error);
2190
+ this.rejectMalformedPendingResponse(decoded.error);
1591
2191
  return;
1592
2192
  }
2193
+ const message = decoded.frame;
1593
2194
  if (message.type === "event") {
1594
2195
  const gatewayEvent = message;
1595
2196
  if (gatewayEvent.event === "connect.challenge") {
1596
- const nonce = gatewayEvent.payload && typeof gatewayEvent.payload.nonce === "string"
1597
- ? gatewayEvent.payload.nonce.trim()
1598
- : "";
2197
+ const challenge = asRecord(gatewayEvent.payload);
2198
+ const nonce = typeof challenge?.nonce === "string" ? challenge.nonce.trim() : "";
1599
2199
  if (!nonce) {
1600
2200
  this.pendingConnectError = {
1601
2201
  code: "DEVICE_AUTH_NONCE_REQUIRED",
@@ -1608,67 +2208,161 @@ export class GatewayClient {
1608
2208
  void this.sendConnect();
1609
2209
  return;
1610
2210
  }
2211
+ let sequenceGap = null;
1611
2212
  if (typeof gatewayEvent.seq === "number") {
1612
- if (this.lastSeq !== null && gatewayEvent.seq > this.lastSeq + 1) {
1613
- this.onGap?.({ expected: this.lastSeq + 1, received: gatewayEvent.seq });
2213
+ if (this.lastSeq !== null) {
2214
+ if (gatewayEvent.seq === this.lastSeq) {
2215
+ this.reportProtocolError({
2216
+ code: "DUPLICATE_SEQUENCE",
2217
+ message: "duplicate gateway event sequence was discarded",
2218
+ frameType: "event",
2219
+ event: gatewayEvent.event,
2220
+ });
2221
+ return;
2222
+ }
2223
+ if (gatewayEvent.seq < this.lastSeq) {
2224
+ this.reportProtocolError({
2225
+ code: "OUT_OF_ORDER_SEQUENCE",
2226
+ message: "out-of-order gateway event sequence was discarded",
2227
+ frameType: "event",
2228
+ event: gatewayEvent.event,
2229
+ });
2230
+ return;
2231
+ }
2232
+ if (gatewayEvent.seq > this.lastSeq + 1) {
2233
+ sequenceGap = { expected: this.lastSeq + 1, received: gatewayEvent.seq };
2234
+ }
1614
2235
  }
1615
2236
  this.lastSeq = gatewayEvent.seq;
1616
2237
  }
1617
- for (const handler of this.eventHandlers) {
2238
+ const payload = asRecord(gatewayEvent.payload) ?? {};
2239
+ const identity = chatEventIdentity(payload);
2240
+ if (isChatStreamGatewayEvent(gatewayEvent) &&
2241
+ !identity.runId &&
2242
+ !identity.sessionKey &&
2243
+ this.activeNormalChatStreams.size > 1) {
2244
+ this.reportProtocolError({
2245
+ code: "AMBIGUOUS_CHAT_STREAM_EVENT",
2246
+ message: "chat stream event without run or session identity was discarded",
2247
+ frameType: "event",
2248
+ event: gatewayEvent.event,
2249
+ });
2250
+ this.interruptNormalChatStreams(new GatewayChatStreamInterruptedError("ambiguous-event"));
2251
+ if (sequenceGap)
2252
+ this.handleForwardSequenceGap(sequenceGap, gatewayEvent);
2253
+ return;
2254
+ }
2255
+ let handledInternally = false;
2256
+ for (const handler of this.internalEventHandlers) {
1618
2257
  try {
1619
- handler(gatewayEvent);
2258
+ handledInternally = handler(gatewayEvent) || handledInternally;
1620
2259
  }
1621
2260
  catch {
1622
- // Event handlers are isolated from the socket lifecycle.
2261
+ // Internal stream handlers are isolated from the socket lifecycle.
1623
2262
  }
1624
2263
  }
1625
- return;
1626
- }
1627
- if (message.type !== "res") {
2264
+ this.publishPublicEvent(gatewayEvent, handledInternally);
2265
+ if (sequenceGap)
2266
+ this.handleForwardSequenceGap(sequenceGap, gatewayEvent);
1628
2267
  return;
1629
2268
  }
1630
2269
  const pending = this.pending.get(message.id);
1631
2270
  if (!pending) {
1632
2271
  return;
1633
2272
  }
2273
+ const payload = message.ok ? asRecord(message.payload) : null;
2274
+ if (pending.expectFinal && payload?.status === "accepted") {
2275
+ if (!pending.acceptedNotified) {
2276
+ pending.acceptedNotified = true;
2277
+ try {
2278
+ pending.onAccepted?.(payload);
2279
+ }
2280
+ catch {
2281
+ // Accepted callbacks are observational and must not break the request.
2282
+ }
2283
+ }
2284
+ return;
2285
+ }
1634
2286
  this.pending.delete(message.id);
1635
- clearTimeout(pending.timer);
2287
+ pending.cleanup();
1636
2288
  if (message.ok) {
1637
2289
  pending.resolve(message.payload);
1638
2290
  return;
1639
2291
  }
1640
- pending.reject(new GatewayRequestError({
1641
- code: message.error?.code ?? "UNAVAILABLE",
1642
- message: message.error?.message ?? "gateway request failed",
1643
- details: message.error?.details,
1644
- }));
2292
+ pending.reject(new GatewayRequestError(message.error));
1645
2293
  }
1646
- sendRawRequest(method, params = {}, timeout = this.defaultTimeout, allowBeforeHello = false) {
2294
+ sendRawRequest(method, params = {}, options = {}, allowBeforeHello = false) {
1647
2295
  if (!this.ws || !isSocketOpen(this.ws)) {
1648
2296
  return Promise.reject(new Error("gateway not connected"));
1649
2297
  }
1650
2298
  if (!allowBeforeHello && !this.connected) {
1651
2299
  return Promise.reject(new Error("gateway not connected"));
1652
2300
  }
2301
+ if (options.signal?.aborted) {
2302
+ return Promise.reject(createGatewayRequestAbortError(method));
2303
+ }
1653
2304
  const id = makeId();
1654
2305
  const request = { type: "req", id, method, params };
1655
- return new Promise((resolve, reject) => {
1656
- const timer = setTimeout(() => {
2306
+ const expectFinal = options.expectFinal === true;
2307
+ const timeout = options.timeoutMs === null
2308
+ ? null
2309
+ : typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs)
2310
+ ? Math.max(0, options.timeoutMs)
2311
+ : expectFinal
2312
+ ? null
2313
+ : this.defaultTimeout;
2314
+ const promise = new Promise((resolve, reject) => {
2315
+ const timer = timeout === null
2316
+ ? null
2317
+ : setTimeout(() => {
2318
+ const pending = this.pending.get(id);
2319
+ this.pending.delete(id);
2320
+ pending?.cleanup();
2321
+ reject(new Error(`RPC timeout: ${method}`));
2322
+ }, timeout);
2323
+ const abortHandler = () => {
2324
+ const pending = this.pending.get(id);
1657
2325
  this.pending.delete(id);
1658
- reject(new Error(`RPC timeout: ${method}`));
1659
- }, timeout);
1660
- this.pending.set(id, { resolve, reject, timer });
1661
- this.ws?.send(JSON.stringify(request));
2326
+ pending?.cleanup();
2327
+ reject(createGatewayRequestAbortError(method));
2328
+ };
2329
+ const cleanup = () => {
2330
+ if (timer) {
2331
+ clearTimeout(timer);
2332
+ }
2333
+ options.signal?.removeEventListener("abort", abortHandler);
2334
+ };
2335
+ this.pending.set(id, {
2336
+ resolve,
2337
+ reject,
2338
+ expectFinal,
2339
+ timer,
2340
+ cleanup,
2341
+ onAccepted: options.onAccepted,
2342
+ });
2343
+ options.signal?.addEventListener("abort", abortHandler, { once: true });
1662
2344
  });
2345
+ try {
2346
+ this.ws?.send(JSON.stringify(request));
2347
+ }
2348
+ catch (error) {
2349
+ const pending = this.pending.get(id);
2350
+ this.pending.delete(id);
2351
+ pending?.cleanup();
2352
+ throw error;
2353
+ }
2354
+ return promise;
1663
2355
  }
1664
2356
  // ---------------------------------------------------------------------------
1665
2357
  // RPC
1666
2358
  // ---------------------------------------------------------------------------
1667
2359
  rpc(method, params = {}, timeout) {
1668
- return this.sendRawRequest(method, params, timeout ?? this.defaultTimeout);
2360
+ return this.sendRawRequest(method, params, {
2361
+ timeoutMs: timeout === undefined ? this.defaultTimeout : timeout,
2362
+ });
1669
2363
  }
1670
- request(method, params = {}, timeout) {
1671
- return this.rpc(method, params, timeout);
2364
+ request(method, params = {}, options) {
2365
+ return this.sendRawRequest(method, params, typeof options === "object" && options !== null ? options : { timeoutMs: options });
1672
2366
  }
1673
2367
  // ---------------------------------------------------------------------------
1674
2368
  // Config
@@ -1691,6 +2385,9 @@ export class GatewayClient {
1691
2385
  const res = await this.rpc("config.schema");
1692
2386
  return normalizeOpenClawConfigSchema(res) ?? { schema: {}, uiHints: {} };
1693
2387
  }
2388
+ async configSchemaLookup(path) {
2389
+ return await this.rpc("config.schema.lookup", { path });
2390
+ }
1694
2391
  async configPatch(patch) {
1695
2392
  const { hash, baseHash } = await this.rpc("config.get");
1696
2393
  await this.rpc("config.patch", {
@@ -1712,6 +2409,42 @@ export class GatewayClient {
1712
2409
  baseHash: hash ?? baseHash ?? "",
1713
2410
  });
1714
2411
  }
2412
+ async configureSlackSocket(config, accountId) {
2413
+ await this.configPatch(channelConfigPatch("slack", { ...config, mode: "socket" }, accountId));
2414
+ }
2415
+ async configureSlackHttp(config, accountId) {
2416
+ await this.configPatch(channelConfigPatch("slack", { ...config, mode: "http" }, accountId));
2417
+ }
2418
+ async configureSlackRelay(options, accountId) {
2419
+ if ("relay" in options) {
2420
+ await this.configPatch(channelConfigPatch("slack", {
2421
+ ...options,
2422
+ enterpriseOrgInstall: false,
2423
+ mode: "relay",
2424
+ }, accountId));
2425
+ return;
2426
+ }
2427
+ const relayConfig = {
2428
+ mode: "relay",
2429
+ ...(options.botToken ? { botToken: options.botToken } : {}),
2430
+ relay: {
2431
+ url: options.url,
2432
+ authToken: {
2433
+ source: "env",
2434
+ provider: "default",
2435
+ id: options.authTokenEnv ?? "HYPER_AGENTS_API_KEY",
2436
+ },
2437
+ gatewayId: options.gatewayId,
2438
+ },
2439
+ };
2440
+ await this.configPatch(channelConfigPatch("slack", relayConfig, options.accountId, Boolean(options.accountId)));
2441
+ }
2442
+ async configureTelegram(config, accountId) {
2443
+ await this.configPatch(channelConfigPatch("telegram", config, accountId));
2444
+ }
2445
+ async configureWhatsapp(config, accountId) {
2446
+ await this.configPatch(channelConfigPatch("whatsapp", config, accountId));
2447
+ }
1715
2448
  async modelsList() {
1716
2449
  const res = await this.rpc("models.list");
1717
2450
  return res?.models ?? res ?? [];
@@ -1781,18 +2514,111 @@ export class GatewayClient {
1781
2514
  const detail = lastError instanceof Error ? `: ${lastError.message}` : "";
1782
2515
  throw new Error(`Gateway readiness probe timed out after ${timeoutMs}ms${detail}`);
1783
2516
  }
1784
- async channelsStatus(probe = false, timeoutMs) {
2517
+ async channelsStatus(probe = false, timeoutMs, channel) {
1785
2518
  const params = { probe };
1786
2519
  if (timeoutMs !== undefined)
1787
2520
  params.timeoutMs = timeoutMs;
2521
+ if (channel !== undefined)
2522
+ params.channel = channel;
1788
2523
  return await this.rpc("channels.status", params);
1789
2524
  }
2525
+ async channelsStart(channel, accountId) {
2526
+ const params = { channel };
2527
+ if (accountId !== undefined)
2528
+ params.accountId = accountId;
2529
+ return await this.rpc("channels.start", params);
2530
+ }
2531
+ async channelsStop(channel, accountId) {
2532
+ const params = { channel };
2533
+ if (accountId !== undefined)
2534
+ params.accountId = accountId;
2535
+ return await this.rpc("channels.stop", params);
2536
+ }
1790
2537
  async channelsLogout(channel, accountId) {
1791
2538
  const params = { channel };
1792
- if (accountId)
2539
+ if (accountId !== undefined)
1793
2540
  params.accountId = accountId;
1794
2541
  return await this.rpc("channels.logout", params);
1795
2542
  }
2543
+ async messageAction(params) {
2544
+ const request = {
2545
+ channel: params.channel,
2546
+ action: params.action,
2547
+ params: params.params,
2548
+ idempotencyKey: params.idempotencyKey ?? makeId(),
2549
+ };
2550
+ if (params.accountId !== undefined)
2551
+ request.accountId = params.accountId;
2552
+ if (params.sessionKey !== undefined)
2553
+ request.sessionKey = params.sessionKey;
2554
+ if (params.sessionId !== undefined)
2555
+ request.sessionId = params.sessionId;
2556
+ if (params.inboundTurnKind !== undefined)
2557
+ request.inboundTurnKind = params.inboundTurnKind;
2558
+ if (params.agentId !== undefined)
2559
+ request.agentId = params.agentId;
2560
+ if (params.conversationReadOrigin !== undefined) {
2561
+ request.conversationReadOrigin = params.conversationReadOrigin;
2562
+ }
2563
+ return await this.rpc("message.action", request);
2564
+ }
2565
+ async send(params) {
2566
+ return await this.rpc("send", {
2567
+ ...params,
2568
+ idempotencyKey: params.idempotencyKey ?? makeId(),
2569
+ });
2570
+ }
2571
+ async readMediaBytes(pathOrUrl) {
2572
+ const token = this.gatewayToken ?? this.deviceToken ?? this.password;
2573
+ if (!token) {
2574
+ throw new Error("Gateway media requires an auth token");
2575
+ }
2576
+ if (typeof globalThis.fetch !== "function") {
2577
+ throw new Error("fetch is not available in this runtime");
2578
+ }
2579
+ const base = new URL(this.url);
2580
+ const protocol = base.protocol === "wss:" ? "https:" : base.protocol === "ws:" ? "http:" : base.protocol;
2581
+ const url = pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")
2582
+ ? pathOrUrl
2583
+ : `${protocol}//${base.host}${pathOrUrl.startsWith("/") ? pathOrUrl : `/${pathOrUrl}`}`;
2584
+ const response = await globalThis.fetch(url, {
2585
+ headers: { Authorization: `Bearer ${token}` },
2586
+ });
2587
+ if (!response.ok) {
2588
+ throw new Error(`Gateway media read failed: ${response.status} ${response.statusText}`);
2589
+ }
2590
+ return new Uint8Array(await response.arrayBuffer());
2591
+ }
2592
+ async pluginsList() {
2593
+ return await this.rpc("plugins.list", {});
2594
+ }
2595
+ async pluginsInstall(params) {
2596
+ return await this.rpc("plugins.install", params, Math.max(PLUGIN_MUTATION_TIMEOUT, this.defaultTimeout));
2597
+ }
2598
+ async pluginsSetEnabled(params) {
2599
+ return await this.rpc("plugins.setEnabled", params);
2600
+ }
2601
+ async pluginsUninstall(params) {
2602
+ return await this.rpc("plugins.uninstall", params, Math.max(PLUGIN_MUTATION_TIMEOUT, this.defaultTimeout));
2603
+ }
2604
+ async pluginsRefresh() {
2605
+ return await this.rpc("plugins.refresh", {});
2606
+ }
2607
+ async toolsCatalog(params = {}) {
2608
+ return await this.rpc("tools.catalog", params);
2609
+ }
2610
+ async toolsEffective(params) {
2611
+ return await this.rpc("tools.effective", params);
2612
+ }
2613
+ async toolsInvoke(params) {
2614
+ return await this.rpc("tools.invoke", {
2615
+ ...params,
2616
+ idempotencyKey: params.idempotencyKey ?? makeId(),
2617
+ });
2618
+ }
2619
+ async commandsList(params = {}) {
2620
+ return await this.rpc("commands.list", params);
2621
+ }
1796
2622
  async webLoginStart(options = {}) {
1797
2623
  const params = {};
1798
2624
  if (options.force)
@@ -1811,88 +2637,437 @@ export class GatewayClient {
1811
2637
  params.timeoutMs = options.timeoutMs;
1812
2638
  if (options.accountId)
1813
2639
  params.accountId = options.accountId;
1814
- return await this.rpc("web.login.wait", params, CHAT_TIMEOUT);
2640
+ if (options.currentQrDataUrl)
2641
+ params.currentQrDataUrl = options.currentQrDataUrl;
2642
+ return await this.rpc("web.login.wait", params, WEB_LOGIN_WAIT_TIMEOUT);
1815
2643
  }
1816
2644
  // ---------------------------------------------------------------------------
1817
2645
  // Sessions
1818
2646
  // ---------------------------------------------------------------------------
1819
- async sessionsList() {
2647
+ async sessionsListResult() {
1820
2648
  const res = await this.rpc("sessions.list");
1821
- return res?.sessions ?? res ?? [];
2649
+ if (Array.isArray(res))
2650
+ return { sessions: res };
2651
+ if (!res || typeof res !== "object")
2652
+ return { sessions: [] };
2653
+ return {
2654
+ ...res,
2655
+ sessions: Array.isArray(res.sessions) ? res.sessions : [],
2656
+ };
2657
+ }
2658
+ async sessionsList() {
2659
+ return (await this.sessionsListResult()).sessions;
1822
2660
  }
1823
2661
  async sessionsPreview(sessionKey, limit = 20) {
1824
2662
  const res = await this.rpc("sessions.preview", { keys: [sessionKey], limit });
1825
2663
  return res?.previews?.[0]?.items ?? [];
1826
2664
  }
2665
+ async sessionsSubscribe() {
2666
+ const result = await this.rpc("sessions.subscribe", {});
2667
+ return result?.subscribed === true;
2668
+ }
2669
+ async sessionsCreate(params = {}) {
2670
+ return await this.rpc("sessions.create", params);
2671
+ }
1827
2672
  async sessionsPatch(patch) {
1828
2673
  return await this.rpc("sessions.patch", patch);
1829
2674
  }
1830
- async chatHistory(sessionKey, limit = 50) {
2675
+ async chatHistoryResult(sessionKey, limit = 50) {
1831
2676
  const params = { limit };
1832
2677
  if (sessionKey)
1833
2678
  params.sessionKey = sessionKey;
1834
2679
  const res = await this.rpc("chat.history", params);
1835
- return res?.messages ?? res ?? [];
2680
+ if (Array.isArray(res))
2681
+ return { messages: res };
2682
+ if (asRecord(res) && Array.isArray(res.messages)) {
2683
+ return { ...res, messages: res.messages };
2684
+ }
2685
+ throw new GatewayRequestError({
2686
+ code: "PROTOCOL_ERROR",
2687
+ message: "Gateway protocol error: chat.history response must be an array or an object with an array `messages` property",
2688
+ });
1836
2689
  }
1837
- async chatAbort(sessionKey) {
2690
+ async chatHistory(sessionKey, limit = 50) {
2691
+ return (await this.chatHistoryResult(sessionKey, limit)).messages;
2692
+ }
2693
+ async chatMessageGet(sessionKey, messageId, options = {}) {
2694
+ const params = { sessionKey, messageId };
2695
+ if (options.agentId)
2696
+ params.agentId = options.agentId;
2697
+ if (options.maxChars !== undefined)
2698
+ params.maxChars = options.maxChars;
2699
+ const res = await this.rpc("chat.message.get", params);
2700
+ if (!asRecord(res) || typeof res.ok !== "boolean") {
2701
+ throw new GatewayRequestError({
2702
+ code: "PROTOCOL_ERROR",
2703
+ message: "Gateway protocol error: chat.message.get response must contain a boolean `ok` property",
2704
+ });
2705
+ }
2706
+ return res;
2707
+ }
2708
+ async chatAbort(sessionKey, runId) {
1838
2709
  const params = {};
1839
2710
  if (sessionKey)
1840
2711
  params.sessionKey = sessionKey;
2712
+ if (runId)
2713
+ params.runId = runId;
1841
2714
  await this.rpc("chat.abort", params);
1842
2715
  }
1843
- async sendChat(message, sessionKey = "main", agentId, attachments) {
2716
+ async sendChat(message, sessionKey, agentId, attachments) {
2717
+ const resolvedSessionKey = sessionKey?.trim() || createOpenClawSdkSessionKey();
1844
2718
  const normalizedAttachments = normalizeChatAttachments(attachments);
1845
2719
  const params = {
1846
2720
  message,
1847
2721
  deliver: false,
1848
- sessionKey,
2722
+ sessionKey: resolvedSessionKey,
1849
2723
  idempotencyKey: makeId(),
1850
2724
  };
1851
2725
  if (agentId)
1852
2726
  params.agentId = agentId;
1853
2727
  if (normalizedAttachments)
1854
2728
  params.attachments = normalizedAttachments;
1855
- return this.rpc("chat.send", params, CHAT_TIMEOUT);
2729
+ return this.rpc("chat.send", params, DEFAULT_AGENT_TIMEOUT);
1856
2730
  }
1857
2731
  async sessionsReset(sessionKey, reason) {
1858
2732
  const params = { key: sessionKey };
1859
2733
  if (reason)
1860
2734
  params.reason = reason;
1861
- await this.rpc("sessions.reset", params);
2735
+ const result = await this.rpc("sessions.reset", params);
2736
+ const canonicalKey = result?.key ?? result?.sessionKey ?? result?.session?.key;
2737
+ return typeof canonicalKey === "string" && canonicalKey.trim() ? canonicalKey.trim() : sessionKey;
2738
+ }
2739
+ async createEphemeralChatSession() {
2740
+ if (!this.connected || !this.ws)
2741
+ throw new Error("Not connected");
2742
+ const requestedSessionKey = `${EPHEMERAL_SESSION_PREFIX}${makeId()}`;
2743
+ const canonicalSessionKey = await this.sessionsReset(requestedSessionKey, "new");
2744
+ const sessionKey = validateEphemeralCanonicalKey(requestedSessionKey, canonicalSessionKey);
2745
+ if (!sessionKey) {
2746
+ await this.sessionsReset(requestedSessionKey, "reset").catch(() => undefined);
2747
+ throw new Error(`Gateway returned an unsafe ephemeral session key: ${canonicalSessionKey}`);
2748
+ }
2749
+ this.activeEphemeralSessionKeys.set(sessionKey, null);
2750
+ let closed = false;
2751
+ let activeStream = null;
2752
+ let closePromise = null;
2753
+ let turnsStarted = 0;
2754
+ let backendTurnMayBeActive = false;
2755
+ const priorAssistantTexts = [];
2756
+ const assertOpen = () => {
2757
+ if (closed)
2758
+ throw new Error("Ephemeral chat session is closed.");
2759
+ };
2760
+ const session = {
2761
+ sessionKey,
2762
+ get closed() {
2763
+ return closed;
2764
+ },
2765
+ chatSend: (message, attachments) => {
2766
+ assertOpen();
2767
+ if (activeStream || backendTurnMayBeActive) {
2768
+ throw new Error("Ephemeral chat session already has an active turn.");
2769
+ }
2770
+ normalizeChatAttachments(attachments);
2771
+ const source = this.chatSend(message, sessionKey, attachments, {
2772
+ strictCorrelation: true,
2773
+ ephemeralSession: true,
2774
+ ...(turnsStarted > 0 ? { captureHistoryBaseline: true } : {}),
2775
+ ...(priorAssistantTexts.length > 0 ? { priorAssistantTexts: [...priorAssistantTexts] } : {}),
2776
+ });
2777
+ turnsStarted += 1;
2778
+ backendTurnMayBeActive = true;
2779
+ let turnText = "";
2780
+ let turnRecorded = false;
2781
+ const observeEvent = (event) => {
2782
+ turnText = applyChatEventContent(turnText, event);
2783
+ if (event.type !== "done" && event.type !== "error")
2784
+ return;
2785
+ backendTurnMayBeActive = false;
2786
+ if (event.type === "done" && turnText.trim() && !turnRecorded) {
2787
+ turnRecorded = true;
2788
+ priorAssistantTexts.push(turnText.trim());
2789
+ }
2790
+ };
2791
+ const release = () => {
2792
+ if (activeStream === stream)
2793
+ activeStream = null;
2794
+ };
2795
+ const stream = {
2796
+ async next(value) {
2797
+ try {
2798
+ const result = await source.next(value);
2799
+ if (result.done) {
2800
+ backendTurnMayBeActive = false;
2801
+ release();
2802
+ }
2803
+ else {
2804
+ observeEvent(result.value);
2805
+ }
2806
+ return result;
2807
+ }
2808
+ catch (error) {
2809
+ release();
2810
+ throw error;
2811
+ }
2812
+ },
2813
+ async return(value) {
2814
+ try {
2815
+ return await source.return(value);
2816
+ }
2817
+ finally {
2818
+ release();
2819
+ }
2820
+ },
2821
+ async throw(error) {
2822
+ try {
2823
+ const result = await source.throw(error);
2824
+ if (result.done) {
2825
+ backendTurnMayBeActive = false;
2826
+ release();
2827
+ }
2828
+ else {
2829
+ observeEvent(result.value);
2830
+ }
2831
+ return result;
2832
+ }
2833
+ catch (cause) {
2834
+ release();
2835
+ throw cause;
2836
+ }
2837
+ },
2838
+ [Symbol.asyncIterator]() {
2839
+ return stream;
2840
+ },
2841
+ };
2842
+ activeStream = stream;
2843
+ return stream;
2844
+ },
2845
+ chatHistory: (limit = 50) => {
2846
+ assertOpen();
2847
+ return this.chatHistory(sessionKey, limit);
2848
+ },
2849
+ chatAbort: async () => {
2850
+ assertOpen();
2851
+ const stream = activeStream;
2852
+ await this.chatAbort(sessionKey);
2853
+ backendTurnMayBeActive = false;
2854
+ if (stream && !await promiseSettlesWithin(stream.return(undefined), EPHEMERAL_TURN_CLOSE_WAIT_MS)) {
2855
+ throw new Error("Ephemeral chat turn is still stopping.");
2856
+ }
2857
+ },
2858
+ close: () => {
2859
+ if (closePromise)
2860
+ return closePromise;
2861
+ closed = true;
2862
+ const stream = activeStream;
2863
+ const abortPromise = backendTurnMayBeActive
2864
+ ? this.chatAbort(sessionKey).then(() => {
2865
+ backendTurnMayBeActive = false;
2866
+ return true;
2867
+ }, () => false)
2868
+ : Promise.resolve(true);
2869
+ const streamReturnPromise = stream?.return(undefined);
2870
+ let cleanupComplete = false;
2871
+ const attempt = (async () => {
2872
+ try {
2873
+ const backendStopped = await booleanPromiseWithin(abortPromise, EPHEMERAL_TURN_CLOSE_WAIT_MS);
2874
+ if (!backendStopped)
2875
+ throw new Error("Ephemeral chat turn did not stop before cleanup timed out.");
2876
+ if (streamReturnPromise) {
2877
+ await promiseSettlesWithin(streamReturnPromise, EPHEMERAL_TURN_CLOSE_WAIT_MS);
2878
+ }
2879
+ await this.sessionsReset(sessionKey, "reset");
2880
+ backendTurnMayBeActive = false;
2881
+ cleanupComplete = true;
2882
+ }
2883
+ finally {
2884
+ if (cleanupComplete) {
2885
+ this.activeEphemeralSessionKeys.delete(sessionKey);
2886
+ }
2887
+ else {
2888
+ this.activeEphemeralSessionKeys.set(sessionKey, Date.now() + EPHEMERAL_RUN_ID_SUPPRESSION_MS);
2889
+ }
2890
+ }
2891
+ })();
2892
+ closePromise = attempt;
2893
+ void attempt.catch(() => {
2894
+ if (closePromise === attempt)
2895
+ closePromise = null;
2896
+ });
2897
+ return attempt;
2898
+ },
2899
+ };
2900
+ return session;
2901
+ }
2902
+ // ---------------------------------------------------------------------------
2903
+ // Nodes (operator-side: manage & drive paired nodes)
2904
+ //
2905
+ // A "node" is a device/machine connected to the same gateway with
2906
+ // `role: "node"` that exposes a command surface. An operator lists/approves
2907
+ // node pairings and drives them with `node.invoke`; the node executes the
2908
+ // command locally and returns the result. This is the gateway's built-in
2909
+ // "agent asks a connected machine to do X and use the result" path.
2910
+ // ---------------------------------------------------------------------------
2911
+ /** Paired nodes known to the gateway. */
2912
+ async nodesList() {
2913
+ const res = await this.rpc("node.list");
2914
+ return res?.nodes ?? res ?? [];
2915
+ }
2916
+ /** Detailed metadata for one paired node (caps + supported invoke commands). */
2917
+ async nodeDescribe(nodeId) {
2918
+ return await this.rpc("node.describe", { nodeId });
2919
+ }
2920
+ /** Pending and paired node-pairing requests. */
2921
+ async nodePairList() {
2922
+ const res = await this.rpc("node.pair.list");
2923
+ return { pending: res?.pending ?? [], paired: res?.paired ?? [] };
2924
+ }
2925
+ /**
2926
+ * Approve a pending node pairing (and its declared command surface). This is
2927
+ * the sibling of `device.pair.approve`; it requires `operator.pairing` and,
2928
+ * for command-bearing nodes, `operator.write`/`operator.admin`.
2929
+ */
2930
+ async nodePairApprove(requestId) {
2931
+ return await this.rpc("node.pair.approve", { requestId });
2932
+ }
2933
+ /** Reject a pending node pairing request. */
2934
+ async nodePairReject(requestId) {
2935
+ await this.rpc("node.pair.reject", { requestId });
2936
+ }
2937
+ /** Remove an already-paired node from the gateway trust set. */
2938
+ async nodePairRemove(nodeId) {
2939
+ await this.rpc("node.pair.remove", { nodeId });
2940
+ }
2941
+ /** Rename a paired node while preserving its stable node id. */
2942
+ async nodeRename(nodeId, displayName) {
2943
+ await this.rpc("node.rename", { nodeId, displayName });
2944
+ }
2945
+ /**
2946
+ * Invoke a command on a paired node and return its result. The node must be
2947
+ * connected, have declared `command`, and the command must be allowed by the
2948
+ * gateway policy (declared + approved surface, and/or
2949
+ * `gateway.nodes.allowCommands`).
2950
+ */
2951
+ async nodeInvoke(nodeId, command, params = {}, timeoutMs) {
2952
+ const rpcParams = {
2953
+ nodeId,
2954
+ command,
2955
+ params,
2956
+ idempotencyKey: makeId(),
2957
+ };
2958
+ if (timeoutMs !== undefined)
2959
+ rpcParams.timeoutMs = timeoutMs;
2960
+ return await this.rpc("node.invoke", rpcParams, timeoutMs);
1862
2961
  }
1863
2962
  // ---------------------------------------------------------------------------
1864
2963
  // Chat (streaming via events)
1865
2964
  // ---------------------------------------------------------------------------
1866
- async *chatSend(message, sessionKey, attachments) {
2965
+ async *chatSend(message, sessionKey, attachments, options = {}) {
1867
2966
  if (!this.connected || !this.ws) {
1868
2967
  throw new Error("Not connected");
1869
2968
  }
1870
2969
  const idempotencyKey = makeId();
1871
2970
  const acceptedRunIds = new Set([idempotencyKey]);
2971
+ if (options.ephemeralSession)
2972
+ this.suppressEphemeralRunId(idempotencyKey);
1872
2973
  const queuedEvents = [];
1873
2974
  let resolveWait = null;
1874
- let streamedDisplayText = false;
1875
- let lastLegacyText = "";
2975
+ let correlationReady = false;
2976
+ let emittedDisplayText = "";
2977
+ let fallbackReplacementAfterSeq;
2978
+ let pendingLifecycleError = null;
1876
2979
  let lastThinkingText = "";
1877
2980
  const seenToolCallIds = new Set();
1878
2981
  const seenToolResultIds = new Set();
2982
+ let acknowledgementPending = false;
2983
+ let historyBaseline;
2984
+ let streamCloseError = null;
2985
+ let resolveStreamClose = null;
2986
+ const streamClosed = new Promise((resolve) => {
2987
+ resolveStreamClose = resolve;
2988
+ });
2989
+ let streamState = null;
2990
+ const wakeWaiter = () => {
2991
+ const waiter = resolveWait;
2992
+ resolveWait = null;
2993
+ waiter?.();
2994
+ };
2995
+ const closeHandler = (error) => {
2996
+ if (streamCloseError)
2997
+ return;
2998
+ streamCloseError = error;
2999
+ this.internalEventHandlers.delete(handler);
3000
+ this.internalStreamCloseHandlers.delete(closeHandler);
3001
+ if (streamState) {
3002
+ this.activeNormalChatStreams.delete(streamState);
3003
+ this.activeStrictChatStreams.delete(streamState);
3004
+ }
3005
+ resolveStreamClose?.(error);
3006
+ wakeWaiter();
3007
+ };
3008
+ const stopOnStreamClose = async (promise) => {
3009
+ return await Promise.race([
3010
+ promise,
3011
+ streamClosed.then((error) => Promise.reject(error)),
3012
+ ]);
3013
+ };
1879
3014
  const handler = (evt) => {
1880
- const isAgentStreamEvent = evt.event === "agent" &&
1881
- (() => {
1882
- const payload = asRecord(evt.payload) ?? {};
1883
- const stream = typeof payload.stream === "string" ? payload.stream.toLowerCase() : "";
1884
- return stream === "tool" || stream === "lifecycle";
1885
- })();
1886
- if (evt.event === "chat" || evt.event?.startsWith("chat.") || isAgentStreamEvent) {
1887
- queuedEvents.push(evt);
1888
- const waiter = resolveWait;
1889
- resolveWait = null;
1890
- waiter?.();
3015
+ if (!isChatStreamGatewayEvent(evt))
3016
+ return false;
3017
+ const payload = asRecord(evt.payload) ?? {};
3018
+ const identity = chatEventIdentity(payload);
3019
+ const payloadRunId = identity.runId?.trim() ?? "";
3020
+ const payloadSessionKey = identity.sessionKey?.trim() ?? "";
3021
+ if (payloadSessionKey && !sameSessionKey(payloadSessionKey, sessionKey))
3022
+ return false;
3023
+ if (payloadRunId && correlationReady && !acceptedRunIds.has(payloadRunId))
3024
+ return false;
3025
+ if (options.strictCorrelation) {
3026
+ if (!payloadRunId && !payloadSessionKey)
3027
+ return false;
3028
+ if (options.ephemeralSession &&
3029
+ !correlationReady &&
3030
+ payloadRunId &&
3031
+ !payloadSessionKey &&
3032
+ !acceptedRunIds.has(payloadRunId)) {
3033
+ this.deferEphemeralCorrelationEvent(evt);
3034
+ }
3035
+ }
3036
+ queuedEvents.push(evt);
3037
+ if (correlationReady && isTerminalChatStreamGatewayEvent(evt) && streamState) {
3038
+ streamState.terminalEvent = evt;
1891
3039
  }
3040
+ wakeWaiter();
3041
+ return options.strictCorrelation === true;
1892
3042
  };
1893
- this.eventHandlers.add(handler);
3043
+ streamState = { close: closeHandler, terminalEvent: null };
1894
3044
  try {
3045
+ if (options.captureHistoryBaseline) {
3046
+ try {
3047
+ historyBaseline = historyAssistantBaseline(await this.chatHistory(sessionKey, 20), options.priorAssistantTexts);
3048
+ }
3049
+ catch {
3050
+ historyBaseline = options.priorAssistantTexts?.length
3051
+ ? historyAssistantBaseline([], options.priorAssistantTexts)
3052
+ : null;
3053
+ }
3054
+ }
3055
+ else if (options.priorAssistantTexts) {
3056
+ historyBaseline = historyAssistantBaseline([], options.priorAssistantTexts);
3057
+ }
1895
3058
  const normalizedAttachments = normalizeChatAttachments(attachments);
3059
+ if (options.ephemeralSession) {
3060
+ this.beginEphemeralChatAcknowledgement();
3061
+ acknowledgementPending = true;
3062
+ }
3063
+ this.internalEventHandlers.add(handler);
3064
+ this.internalStreamCloseHandlers.add(closeHandler);
3065
+ if (options.strictCorrelation) {
3066
+ this.activeStrictChatStreams.add(streamState);
3067
+ }
3068
+ else {
3069
+ this.activeNormalChatStreams.add(streamState);
3070
+ }
1896
3071
  const params = {
1897
3072
  message,
1898
3073
  deliver: false,
@@ -1901,26 +3076,85 @@ export class GatewayClient {
1901
3076
  };
1902
3077
  if (normalizedAttachments)
1903
3078
  params.attachments = normalizedAttachments;
1904
- const ack = await this.rpc("chat.send", params, CHAT_TIMEOUT);
3079
+ const ack = await stopOnStreamClose(this.rpc("chat.send", params, DEFAULT_AGENT_TIMEOUT));
1905
3080
  const serverRunId = typeof ack?.runId === "string" ? ack.runId.trim() : "";
1906
3081
  if (serverRunId) {
1907
3082
  acceptedRunIds.add(serverRunId);
3083
+ if (options.ephemeralSession)
3084
+ this.suppressEphemeralRunId(serverRunId);
3085
+ }
3086
+ correlationReady = true;
3087
+ if (queuedEvents.length > 0) {
3088
+ for (let index = queuedEvents.length - 1; index >= 0; index -= 1) {
3089
+ const payload = asRecord(queuedEvents[index]?.payload) ?? {};
3090
+ const identity = chatEventIdentity(payload);
3091
+ const queuedRunId = identity.runId?.trim() ?? "";
3092
+ const queuedSessionKey = identity.sessionKey?.trim() ?? "";
3093
+ const mismatchedSession = Boolean(queuedSessionKey && !sameSessionKey(queuedSessionKey, sessionKey));
3094
+ const unacceptedRun = Boolean(queuedRunId && !acceptedRunIds.has(queuedRunId));
3095
+ if (mismatchedSession || unacceptedRun) {
3096
+ queuedEvents.splice(index, 1);
3097
+ }
3098
+ }
3099
+ streamState.terminalEvent =
3100
+ [...queuedEvents].reverse().find(isTerminalChatStreamGatewayEvent) ?? null;
3101
+ }
3102
+ if (acknowledgementPending) {
3103
+ acknowledgementPending = false;
3104
+ this.finishEphemeralChatAcknowledgement(Boolean(serverRunId));
1908
3105
  }
3106
+ const readLatestHistoryText = async () => {
3107
+ try {
3108
+ return latestHistoryAssistantText(await stopOnStreamClose(this.chatHistory(sessionKey, 20)), acceptedRunIds, historyBaseline) ?? "";
3109
+ }
3110
+ catch (error) {
3111
+ if (streamCloseError)
3112
+ throw error;
3113
+ return "";
3114
+ }
3115
+ };
1909
3116
  const waitForHistoryText = async (timeoutMs = 10_000) => {
1910
- const historyDeadline = Date.now() + Math.min(timeoutMs, CHAT_TIMEOUT);
3117
+ const historyDeadline = Date.now() + Math.min(timeoutMs, DEFAULT_AGENT_TIMEOUT);
1911
3118
  while (true) {
1912
- const historyText = latestHistoryAssistantText(await this.chatHistory(sessionKey, 20), acceptedRunIds);
3119
+ const historyText = await readLatestHistoryText();
1913
3120
  if (historyText)
1914
3121
  return historyText;
1915
3122
  if (Date.now() >= historyDeadline)
1916
3123
  return "";
1917
- await new Promise((resolve) => setTimeout(resolve, 500));
3124
+ await stopOnStreamClose(new Promise((resolve) => setTimeout(resolve, 500)));
1918
3125
  }
1919
3126
  };
1920
- let deadline = Date.now() + CHAT_TIMEOUT;
3127
+ const reconcileHistoryAfterToolActivity = async (streamedText) => {
3128
+ let historyText = await readLatestHistoryText();
3129
+ if (!historyText || historyText !== streamedText)
3130
+ return historyText;
3131
+ const historyDeadline = Date.now() + 1_500;
3132
+ while (Date.now() < historyDeadline) {
3133
+ await stopOnStreamClose(new Promise((resolve) => setTimeout(resolve, 500)));
3134
+ const nextHistoryText = await readLatestHistoryText();
3135
+ if (!nextHistoryText)
3136
+ continue;
3137
+ historyText = nextHistoryText;
3138
+ if (historyText !== streamedText)
3139
+ break;
3140
+ }
3141
+ return historyText;
3142
+ };
3143
+ let deadline = Date.now() + DEFAULT_AGENT_TIMEOUT;
1921
3144
  while (Date.now() < deadline) {
1922
3145
  if (queuedEvents.length === 0) {
1923
- const remainingMs = Math.max(100, Math.min(1000, deadline - Date.now()));
3146
+ if (streamCloseError)
3147
+ throw streamCloseError;
3148
+ if (pendingLifecycleError && Date.now() >= pendingLifecycleError.expiresAt) {
3149
+ yield {
3150
+ type: "error",
3151
+ text: pendingLifecycleError.text,
3152
+ ...pendingLifecycleError.identity,
3153
+ data: pendingLifecycleError.data,
3154
+ };
3155
+ return;
3156
+ }
3157
+ const remainingMs = Math.max(100, Math.min(1000, deadline - Date.now(), pendingLifecycleError ? pendingLifecycleError.expiresAt - Date.now() : Number.POSITIVE_INFINITY));
1924
3158
  await new Promise((resolve) => {
1925
3159
  const timer = setTimeout(() => {
1926
3160
  if (resolveWait === release) {
@@ -1934,24 +3168,49 @@ export class GatewayClient {
1934
3168
  };
1935
3169
  resolveWait = release;
1936
3170
  });
3171
+ if (queuedEvents.length === 0 && streamCloseError)
3172
+ throw streamCloseError;
1937
3173
  continue;
1938
3174
  }
1939
3175
  const evt = queuedEvents.shift();
1940
3176
  const payload = asRecord(evt.payload) ?? {};
1941
- const payloadRunId = typeof payload.runId === "string" ? payload.runId.trim() : "";
1942
- const payloadSessionKey = typeof payload.sessionKey === "string" ? payload.sessionKey.trim() : "";
3177
+ const routingIdentity = chatEventIdentity(payload);
3178
+ const payloadRunId = routingIdentity.runId?.trim() ?? "";
3179
+ const payloadSessionKey = routingIdentity.sessionKey?.trim() ?? "";
1943
3180
  if (payloadRunId && !acceptedRunIds.has(payloadRunId)) {
1944
3181
  continue;
1945
3182
  }
1946
3183
  if (payloadSessionKey && !sameSessionKey(payloadSessionKey, sessionKey)) {
1947
3184
  continue;
1948
3185
  }
1949
- deadline = Date.now() + CHAT_TIMEOUT;
3186
+ const identity = chatEventIdentity(payload);
3187
+ const eventRunSeq = Number.isSafeInteger(payload.seq) ? payload.seq : null;
3188
+ const replacesFailedAttempt = fallbackReplacementAfterSeq !== undefined && (fallbackReplacementAfterSeq === null ||
3189
+ eventRunSeq === null ||
3190
+ eventRunSeq > fallbackReplacementAfterSeq);
3191
+ const eventLifecyclePhase = evt.event === "agent" && String(payload.stream ?? "").toLowerCase() === "lifecycle"
3192
+ ? String(asRecord(payload.data)?.phase ?? "").toLowerCase()
3193
+ : "";
3194
+ const fallbackContinued = replacesFailedAttempt && (evt.event === "chat" ||
3195
+ evt.event === "chat.content" ||
3196
+ Boolean(eventLifecyclePhase && eventLifecyclePhase !== "error"));
3197
+ if (pendingLifecycleError && fallbackContinued) {
3198
+ pendingLifecycleError = null;
3199
+ }
3200
+ deadline = Date.now() + DEFAULT_AGENT_TIMEOUT;
1950
3201
  if (evt.event === "chat.content") {
1951
3202
  const text = typeof payload.text === "string" ? payload.text : "";
1952
- if (text) {
1953
- streamedDisplayText = true;
1954
- yield { type: "content", text };
3203
+ const update = appendStreamContent(emittedDisplayText, text, payload.replace === true || replacesFailedAttempt);
3204
+ if (update) {
3205
+ emittedDisplayText = update.nextText;
3206
+ if (replacesFailedAttempt)
3207
+ fallbackReplacementAfterSeq = undefined;
3208
+ yield {
3209
+ type: "content",
3210
+ text: update.text,
3211
+ ...(update.replace ? { replace: true } : {}),
3212
+ ...identity,
3213
+ };
1955
3214
  }
1956
3215
  continue;
1957
3216
  }
@@ -1964,6 +3223,7 @@ export class GatewayClient {
1964
3223
  seenToolCallIds.add(toolCallId);
1965
3224
  yield {
1966
3225
  type: "tool_call",
3226
+ ...identity,
1967
3227
  data: {
1968
3228
  ...gatewayToolStreamPayload(toolPayload),
1969
3229
  args: toolPayload.args,
@@ -1974,6 +3234,7 @@ export class GatewayClient {
1974
3234
  seenToolResultIds.add(toolCallId);
1975
3235
  yield {
1976
3236
  type: "tool_result",
3237
+ ...identity,
1977
3238
  data: {
1978
3239
  ...gatewayToolStreamPayload(toolPayload),
1979
3240
  result: toolPayload.result ?? toolPayload.meta ?? toolPayload.content ?? toolPayload.text ?? toolPayload.partialResult,
@@ -1988,33 +3249,51 @@ export class GatewayClient {
1988
3249
  const phase = typeof lifecyclePayload.phase === "string" ? lifecyclePayload.phase.toLowerCase() : "";
1989
3250
  if (phase === "end") {
1990
3251
  const hasNonTextActivity = Boolean(lastThinkingText) || seenToolCallIds.size > 0 || seenToolResultIds.size > 0;
1991
- const historyText = hasNonTextActivity
1992
- ? ""
1993
- : streamedDisplayText || lastLegacyText
1994
- ? latestHistoryAssistantText(await this.chatHistory(sessionKey, 20), acceptedRunIds)
1995
- : await waitForHistoryText();
3252
+ const historyText = emittedDisplayText && hasNonTextActivity
3253
+ ? await reconcileHistoryAfterToolActivity(emittedDisplayText)
3254
+ : emittedDisplayText
3255
+ ? await readLatestHistoryText()
3256
+ : hasNonTextActivity
3257
+ ? ""
3258
+ : await waitForHistoryText();
3259
+ if (queuedEvents.length > 0)
3260
+ continue;
1996
3261
  if (historyText) {
1997
- const streamed = streamDelta(lastLegacyText, historyText);
1998
- lastLegacyText = streamed.nextText;
1999
- if (streamed.delta) {
2000
- streamedDisplayText = true;
2001
- yield { type: "content", text: streamed.delta, data: payload };
3262
+ const update = replacesFailedAttempt
3263
+ ? reconcileStreamContent(emittedDisplayText, historyText, true)
3264
+ : reconcileHistoryStreamContent(emittedDisplayText, historyText);
3265
+ if (update) {
3266
+ emittedDisplayText = update.nextText;
3267
+ if (replacesFailedAttempt)
3268
+ fallbackReplacementAfterSeq = undefined;
3269
+ yield {
3270
+ type: "content",
3271
+ text: update.text,
3272
+ ...(update.replace ? { replace: true } : {}),
3273
+ ...identity,
3274
+ data: payload,
3275
+ };
2002
3276
  }
2003
3277
  }
2004
- yield { type: "done", data: payload };
3278
+ yield { type: "done", ...identity, data: payload };
2005
3279
  return;
2006
3280
  }
2007
3281
  if (phase === "error") {
2008
- yield {
2009
- type: "error",
3282
+ // OpenClaw emits lifecycle errors for failed provider attempts, then
3283
+ // may continue the same chat run through a fallback provider.
3284
+ fallbackReplacementAfterSeq = eventRunSeq;
3285
+ pendingLifecycleError = {
2010
3286
  text: typeof lifecyclePayload.error === "string" && lifecyclePayload.error
2011
3287
  ? lifecyclePayload.error
2012
3288
  : typeof payload.errorMessage === "string" && payload.errorMessage
2013
3289
  ? payload.errorMessage
2014
3290
  : phase,
3291
+ identity,
2015
3292
  data: payload,
3293
+ seq: eventRunSeq,
3294
+ expiresAt: Date.now() + CHAT_LIFECYCLE_ERROR_FALLBACK_TIMEOUT_MS,
2016
3295
  };
2017
- return;
3296
+ continue;
2018
3297
  }
2019
3298
  continue;
2020
3299
  }
@@ -2023,38 +3302,58 @@ export class GatewayClient {
2023
3302
  if (text) {
2024
3303
  lastThinkingText += text;
2025
3304
  }
2026
- yield { type: "thinking", text };
3305
+ yield { type: "thinking", text, ...identity };
2027
3306
  continue;
2028
3307
  }
2029
3308
  if (evt.event === "chat.tool_call") {
2030
3309
  const toolCallId = gatewayToolCallId(payload) ??
2031
3310
  `${gatewayToolName(payload) ?? "tool"}:${JSON.stringify(payload.args ?? payload.arguments ?? null)}`;
2032
3311
  seenToolCallIds.add(toolCallId);
2033
- yield { type: "tool_call", data: payload };
3312
+ yield { type: "tool_call", ...identity, data: payload };
2034
3313
  continue;
2035
3314
  }
2036
3315
  if (evt.event === "chat.tool_result") {
2037
3316
  const toolCallId = gatewayToolCallId(payload) ??
2038
3317
  `${gatewayToolName(payload) ?? "tool"}:${JSON.stringify(payload.args ?? payload.arguments ?? null)}`;
2039
3318
  seenToolResultIds.add(toolCallId);
2040
- yield { type: "tool_result", data: payload };
3319
+ yield { type: "tool_result", ...identity, data: payload };
2041
3320
  continue;
2042
3321
  }
2043
3322
  if (evt.event === "chat.done") {
2044
3323
  const hasNonTextActivity = Boolean(lastThinkingText) || seenToolCallIds.size > 0 || seenToolResultIds.size > 0;
2045
- if (!streamedDisplayText && !lastLegacyText && !hasNonTextActivity) {
2046
- const historyText = await waitForHistoryText();
2047
- if (historyText) {
2048
- yield { type: "content", text: historyText, data: payload };
3324
+ const historyText = emittedDisplayText && hasNonTextActivity
3325
+ ? await reconcileHistoryAfterToolActivity(emittedDisplayText)
3326
+ : (!emittedDisplayText && !hasNonTextActivity) || fallbackReplacementAfterSeq !== undefined
3327
+ ? await waitForHistoryText()
3328
+ : "";
3329
+ if (historyText) {
3330
+ const update = replacesFailedAttempt || fallbackReplacementAfterSeq !== undefined
3331
+ ? reconcileStreamContent(emittedDisplayText, historyText, true)
3332
+ : reconcileHistoryStreamContent(emittedDisplayText, historyText);
3333
+ if (update) {
3334
+ emittedDisplayText = update.nextText;
3335
+ fallbackReplacementAfterSeq = undefined;
3336
+ yield {
3337
+ type: "content",
3338
+ text: update.text,
3339
+ ...(update.replace ? { replace: true } : {}),
3340
+ ...identity,
3341
+ data: payload,
3342
+ };
2049
3343
  }
2050
3344
  }
2051
- yield { type: "done", data: payload };
3345
+ yield { type: "done", ...identity, data: payload };
3346
+ return;
3347
+ }
3348
+ if (evt.event === "chat.aborted") {
3349
+ yield { type: "error", text: "aborted", ...identity, data: payload };
2052
3350
  return;
2053
3351
  }
2054
3352
  if (evt.event === "chat.error") {
2055
3353
  yield {
2056
3354
  type: "error",
2057
3355
  text: typeof payload.message === "string" ? payload.message : "Unknown error",
3356
+ ...identity,
2058
3357
  data: payload,
2059
3358
  };
2060
3359
  return;
@@ -2063,24 +3362,25 @@ export class GatewayClient {
2063
3362
  continue;
2064
3363
  }
2065
3364
  const state = typeof payload.state === "string" ? payload.state.trim().toLowerCase() : "";
2066
- const currentText = extractMessageText(payload.message) ?? "";
3365
+ const currentText = extractMessageText(payload.message);
2067
3366
  const currentDeltaText = typeof payload.deltaText === "string" ? payload.deltaText : "";
2068
3367
  const normalizedMessage = normalizeGatewayChatMessage(payload.message);
2069
3368
  if (state === "delta") {
2070
- const streamed = currentText
2071
- ? streamDelta(lastLegacyText, currentText)
2072
- : {
2073
- delta: currentDeltaText,
2074
- nextText: currentDeltaText
2075
- ? payload.replace === true
2076
- ? currentDeltaText
2077
- : lastLegacyText + currentDeltaText
2078
- : lastLegacyText,
3369
+ const replaceContent = payload.replace === true || replacesFailedAttempt;
3370
+ const update = currentText !== null && (currentText || payload.replace === true)
3371
+ ? reconcileStreamContent(emittedDisplayText, currentText, replaceContent)
3372
+ : appendStreamContent(emittedDisplayText, currentDeltaText, replaceContent);
3373
+ if (update) {
3374
+ emittedDisplayText = update.nextText;
3375
+ if (replacesFailedAttempt)
3376
+ fallbackReplacementAfterSeq = undefined;
3377
+ yield {
3378
+ type: "content",
3379
+ text: update.text,
3380
+ ...(update.replace ? { replace: true } : {}),
3381
+ ...identity,
3382
+ data: payload,
2079
3383
  };
2080
- lastLegacyText = streamed.nextText;
2081
- if (streamed.delta) {
2082
- streamedDisplayText = true;
2083
- yield { type: "content", text: streamed.delta, data: payload };
2084
3384
  }
2085
3385
  continue;
2086
3386
  }
@@ -2090,7 +3390,7 @@ export class GatewayClient {
2090
3390
  : { delta: "", nextText: lastThinkingText };
2091
3391
  lastThinkingText = thinkingDelta.nextText;
2092
3392
  if (thinkingDelta.delta) {
2093
- yield { type: "thinking", text: thinkingDelta.delta, data: payload };
3393
+ yield { type: "thinking", text: thinkingDelta.delta, ...identity, data: payload };
2094
3394
  }
2095
3395
  for (const toolCall of normalizedMessage?.toolCalls ?? []) {
2096
3396
  const toolCallKey = toolCall.id?.trim() ||
@@ -2099,6 +3399,7 @@ export class GatewayClient {
2099
3399
  seenToolCallIds.add(toolCallKey);
2100
3400
  yield {
2101
3401
  type: "tool_call",
3402
+ ...identity,
2102
3403
  data: {
2103
3404
  ...(toolCall.id ? { toolCallId: toolCall.id } : {}),
2104
3405
  name: toolCall.name,
@@ -2110,6 +3411,7 @@ export class GatewayClient {
2110
3411
  seenToolResultIds.add(toolCallKey);
2111
3412
  yield {
2112
3413
  type: "tool_result",
3414
+ ...identity,
2113
3415
  data: {
2114
3416
  ...(toolCall.id ? { toolCallId: toolCall.id } : {}),
2115
3417
  name: toolCall.name,
@@ -2118,53 +3420,247 @@ export class GatewayClient {
2118
3420
  };
2119
3421
  }
2120
3422
  }
2121
- if (currentText) {
2122
- const streamed = streamDelta(lastLegacyText, currentText);
2123
- lastLegacyText = streamed.nextText;
2124
- if (streamed.delta) {
2125
- streamedDisplayText = true;
2126
- yield { type: "content", text: streamed.delta, data: payload };
3423
+ if (currentText !== null && (currentText || payload.replace === true)) {
3424
+ const update = reconcileStreamContent(emittedDisplayText, currentText, payload.replace === true || replacesFailedAttempt);
3425
+ if (update) {
3426
+ emittedDisplayText = update.nextText;
3427
+ if (replacesFailedAttempt)
3428
+ fallbackReplacementAfterSeq = undefined;
3429
+ yield {
3430
+ type: "content",
3431
+ text: update.text,
3432
+ ...(update.replace ? { replace: true } : {}),
3433
+ ...identity,
3434
+ data: payload,
3435
+ };
2127
3436
  }
2128
- yield { type: "done", data: payload };
3437
+ yield { type: "done", ...identity, data: payload };
2129
3438
  return;
2130
3439
  }
2131
- if (streamedDisplayText || lastLegacyText) {
2132
- yield { type: "done", data: payload };
3440
+ if (emittedDisplayText && fallbackReplacementAfterSeq === undefined) {
3441
+ yield { type: "done", ...identity, data: payload };
2133
3442
  return;
2134
3443
  }
2135
3444
  if (normalizedMessage?.thinking || (normalizedMessage?.toolCalls.length ?? 0) > 0) {
2136
- yield { type: "done", data: payload };
3445
+ yield { type: "done", ...identity, data: payload };
2137
3446
  return;
2138
3447
  }
2139
3448
  const historyText = await waitForHistoryText();
2140
3449
  if (historyText) {
2141
- yield { type: "content", text: historyText, data: payload };
3450
+ const update = fallbackReplacementAfterSeq !== undefined
3451
+ ? reconcileStreamContent(emittedDisplayText, historyText, true)
3452
+ : reconcileHistoryStreamContent(emittedDisplayText, historyText);
3453
+ if (update) {
3454
+ emittedDisplayText = update.nextText;
3455
+ fallbackReplacementAfterSeq = undefined;
3456
+ yield {
3457
+ type: "content",
3458
+ text: update.text,
3459
+ ...(update.replace ? { replace: true } : {}),
3460
+ ...identity,
3461
+ data: payload,
3462
+ };
3463
+ }
2142
3464
  }
2143
- yield { type: "done", data: payload };
3465
+ yield { type: "done", ...identity, data: payload };
2144
3466
  return;
2145
3467
  }
2146
3468
  if (state === "error" || state === "aborted") {
2147
- if (currentText) {
2148
- const streamed = streamDelta(lastLegacyText, currentText);
2149
- lastLegacyText = streamed.nextText;
2150
- if (streamed.delta) {
2151
- streamedDisplayText = true;
2152
- yield { type: "content", text: streamed.delta, data: payload };
3469
+ if (currentText !== null && (currentText || payload.replace === true)) {
3470
+ const update = reconcileStreamContent(emittedDisplayText, currentText, payload.replace === true);
3471
+ if (update) {
3472
+ emittedDisplayText = update.nextText;
3473
+ yield {
3474
+ type: "content",
3475
+ text: update.text,
3476
+ ...(update.replace ? { replace: true } : {}),
3477
+ ...identity,
3478
+ data: payload,
3479
+ };
2153
3480
  }
2154
3481
  }
2155
3482
  yield {
2156
3483
  type: "error",
2157
3484
  text: typeof payload.errorMessage === "string" ? payload.errorMessage : state,
3485
+ ...identity,
2158
3486
  data: payload,
2159
3487
  };
2160
3488
  return;
2161
3489
  }
2162
3490
  }
3491
+ if (streamCloseError)
3492
+ throw streamCloseError;
2163
3493
  throw new Error("Streaming chat.send timed out");
2164
3494
  }
2165
3495
  finally {
2166
- this.eventHandlers.delete(handler);
3496
+ if (acknowledgementPending)
3497
+ this.finishEphemeralChatAcknowledgement(false);
3498
+ this.internalEventHandlers.delete(handler);
3499
+ this.internalStreamCloseHandlers.delete(closeHandler);
3500
+ this.activeNormalChatStreams.delete(streamState);
3501
+ this.activeStrictChatStreams.delete(streamState);
3502
+ }
3503
+ }
3504
+ async runEphemeralChat(message, options = {}) {
3505
+ const prompt = message.trim();
3506
+ if (!prompt)
3507
+ throw new Error("Ephemeral chat requires a message.");
3508
+ if (!this.connected || !this.ws)
3509
+ throw new Error("Not connected");
3510
+ const timeoutMs = options.timeoutMs ?? DEFAULT_AGENT_TIMEOUT;
3511
+ const maxResponseChars = options.maxResponseChars ?? 128 * 1024;
3512
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > DEFAULT_AGENT_TIMEOUT) {
3513
+ throw new Error(`Ephemeral chat timeout must be between 1 and ${DEFAULT_AGENT_TIMEOUT} milliseconds.`);
3514
+ }
3515
+ if (!Number.isSafeInteger(maxResponseChars) || maxResponseChars <= 0) {
3516
+ throw new Error("Ephemeral chat response limit must be a positive integer.");
3517
+ }
3518
+ let session = null;
3519
+ let terminal = false;
3520
+ let stream = null;
3521
+ let stopError = null;
3522
+ let abortPromise = null;
3523
+ let primaryError = null;
3524
+ let cleanupError = null;
3525
+ let resultContent = null;
3526
+ let resolveStop = null;
3527
+ const stopped = new Promise((resolve) => {
3528
+ resolveStop = resolve;
3529
+ });
3530
+ const requestAbort = (error) => {
3531
+ if (!stopError) {
3532
+ stopError = error;
3533
+ resolveStop?.(error);
3534
+ }
3535
+ if (session && !terminal && !abortPromise) {
3536
+ abortPromise = session.chatAbort().catch(() => undefined);
3537
+ }
3538
+ void stream?.return(undefined).catch(() => undefined);
3539
+ };
3540
+ const dispatchEvent = async (event) => {
3541
+ if (!options.onEvent)
3542
+ return;
3543
+ await Promise.race([
3544
+ Promise.resolve().then(() => options.onEvent?.(event)),
3545
+ stopped.then((error) => Promise.reject(error)),
3546
+ ]);
3547
+ };
3548
+ const abortError = () => {
3549
+ const error = new Error("Ephemeral chat was cancelled.");
3550
+ error.name = "AbortError";
3551
+ return error;
3552
+ };
3553
+ const handleSignalAbort = () => requestAbort(abortError());
3554
+ const timeout = setTimeout(() => requestAbort(new Error("Ephemeral chat timed out.")), timeoutMs);
3555
+ options.signal?.addEventListener("abort", handleSignalAbort, { once: true });
3556
+ try {
3557
+ if (options.signal?.aborted)
3558
+ throw abortError();
3559
+ session = await this.createEphemeralChatSession();
3560
+ if (options.signal?.aborted)
3561
+ throw abortError();
3562
+ if (stopError)
3563
+ throw stopError;
3564
+ if (options.fastMode) {
3565
+ let fastModeTerminal = false;
3566
+ stream = session.chatSend("/fast on");
3567
+ while (true) {
3568
+ const next = await Promise.race([
3569
+ stream.next(),
3570
+ stopped.then((error) => Promise.reject(error)),
3571
+ ]);
3572
+ if (next.done)
3573
+ break;
3574
+ const event = next.value;
3575
+ if (event.type === "error") {
3576
+ fastModeTerminal = true;
3577
+ throw new Error(event.text || "Ephemeral fast mode failed.");
3578
+ }
3579
+ if (event.type === "done") {
3580
+ fastModeTerminal = true;
3581
+ break;
3582
+ }
3583
+ }
3584
+ await stream.return(undefined).catch(() => undefined);
3585
+ stream = null;
3586
+ if (stopError)
3587
+ throw stopError;
3588
+ if (!fastModeTerminal)
3589
+ throw new Error("Ephemeral fast mode ended without completing.");
3590
+ }
3591
+ let content = "";
3592
+ stream = session.chatSend(prompt);
3593
+ while (true) {
3594
+ const next = await Promise.race([
3595
+ stream.next(),
3596
+ stopped.then((error) => Promise.reject(error)),
3597
+ ]);
3598
+ if (next.done)
3599
+ break;
3600
+ const event = next.value;
3601
+ if (stopError)
3602
+ throw stopError;
3603
+ if (event.type === "content") {
3604
+ content = applyChatEventContent(content, event);
3605
+ if (content.length > maxResponseChars) {
3606
+ const error = new Error("Ephemeral chat response exceeds the configured limit.");
3607
+ requestAbort(error);
3608
+ throw error;
3609
+ }
3610
+ }
3611
+ await dispatchEvent(event);
3612
+ if (event.type === "error") {
3613
+ terminal = true;
3614
+ throw new Error(event.text || "Ephemeral chat failed.");
3615
+ }
3616
+ else if (event.type === "done") {
3617
+ terminal = true;
3618
+ }
3619
+ }
3620
+ if (stopError)
3621
+ throw stopError;
3622
+ if (!terminal)
3623
+ throw new Error("Ephemeral chat ended without completing.");
3624
+ if (!content.trim())
3625
+ throw new Error("Ephemeral chat returned an empty response.");
3626
+ resultContent = content;
3627
+ }
3628
+ catch (error) {
3629
+ if (session && !terminal)
3630
+ requestAbort(error instanceof Error ? error : new Error("Ephemeral chat failed."));
3631
+ await abortPromise;
3632
+ primaryError = stopError ?? error;
3633
+ }
3634
+ finally {
3635
+ clearTimeout(timeout);
3636
+ options.signal?.removeEventListener("abort", handleSignalAbort);
3637
+ if (stream && !primaryError) {
3638
+ await promiseSettlesWithin(stream.return(undefined), EPHEMERAL_TURN_CLOSE_WAIT_MS);
3639
+ }
3640
+ if (session) {
3641
+ try {
3642
+ await session.close();
3643
+ }
3644
+ catch (error) {
3645
+ cleanupError = error;
3646
+ }
3647
+ }
2167
3648
  }
3649
+ if (primaryError && cleanupError) {
3650
+ const primaryMessage = primaryError instanceof Error ? primaryError.message : String(primaryError);
3651
+ const cleanupMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
3652
+ const combinedError = new Error(`${primaryMessage} Private chat cleanup also failed: ${cleanupMessage}`);
3653
+ combinedError.name = primaryError instanceof Error ? primaryError.name : "Error";
3654
+ combinedError.cause = cleanupError;
3655
+ throw combinedError;
3656
+ }
3657
+ if (primaryError)
3658
+ throw primaryError;
3659
+ if (cleanupError)
3660
+ throw cleanupError;
3661
+ if (resultContent === null)
3662
+ throw new Error("Ephemeral chat ended without a result.");
3663
+ return resultContent;
2168
3664
  }
2169
3665
  // ---------------------------------------------------------------------------
2170
3666
  // Files (agent workspace files)
@@ -2223,4 +3719,115 @@ export class GatewayClient {
2223
3719
  return this.rpc("status");
2224
3720
  }
2225
3721
  }
3722
+ function createGatewayRequestAbortError(method) {
3723
+ const err = new Error(`gateway request aborted for ${method}`);
3724
+ err.name = "AbortError";
3725
+ return err;
3726
+ }
3727
+ function coerceNodeInvokeRequest(payload) {
3728
+ if (!payload || typeof payload !== "object") {
3729
+ return null;
3730
+ }
3731
+ const obj = payload;
3732
+ const id = typeof obj.id === "string" ? obj.id.trim() : "";
3733
+ const nodeId = typeof obj.nodeId === "string" ? obj.nodeId.trim() : "";
3734
+ const command = typeof obj.command === "string" ? obj.command.trim() : "";
3735
+ if (!id || !nodeId || !command) {
3736
+ return null;
3737
+ }
3738
+ const paramsJSON = typeof obj.paramsJSON === "string"
3739
+ ? obj.paramsJSON
3740
+ : obj.params !== undefined
3741
+ ? JSON.stringify(obj.params)
3742
+ : null;
3743
+ return { id, nodeId, command, paramsJSON };
3744
+ }
3745
+ /**
3746
+ * Serve a node command surface over a gateway connection. Wraps a
3747
+ * {@link GatewayClient} connected as `role: "node"`, subscribes to
3748
+ * `node.invoke.request`, dispatches to the registered handlers, and replies with
3749
+ * `node.invoke.result` (JSON-encoded payload on success, or an error).
3750
+ */
3751
+ export class NodeServer {
3752
+ gateway;
3753
+ handlers;
3754
+ unsubscribe = null;
3755
+ constructor(commands, options) {
3756
+ this.handlers = { ...commands };
3757
+ const nodeId = options.nodeId?.trim() || undefined;
3758
+ this.gateway = new GatewayClient({
3759
+ ...options,
3760
+ clientId: options.clientId ?? "node-host",
3761
+ clientMode: "node",
3762
+ role: "node",
3763
+ scopes: [],
3764
+ caps: options.caps ?? [],
3765
+ commands: Object.keys(this.handlers),
3766
+ ...(nodeId ? { instanceId: nodeId } : {}),
3767
+ });
3768
+ }
3769
+ /** Connect as a node and start answering invoke requests. Resolves once connected. */
3770
+ async start() {
3771
+ if (!this.unsubscribe) {
3772
+ this.unsubscribe = this.gateway.onEvent((event) => {
3773
+ if (event.event === "node.invoke.request") {
3774
+ void this.handleInvoke(event.payload);
3775
+ }
3776
+ });
3777
+ }
3778
+ await this.gateway.connect();
3779
+ }
3780
+ /** Stop answering requests and disconnect. */
3781
+ stop() {
3782
+ this.unsubscribe?.();
3783
+ this.unsubscribe = null;
3784
+ this.gateway.stop();
3785
+ }
3786
+ async handleInvoke(payload) {
3787
+ const frame = coerceNodeInvokeRequest(payload);
3788
+ if (!frame) {
3789
+ return;
3790
+ }
3791
+ const result = await this.dispatch(frame);
3792
+ try {
3793
+ await this.gateway.request("node.invoke.result", {
3794
+ id: frame.id,
3795
+ nodeId: frame.nodeId,
3796
+ ...result,
3797
+ });
3798
+ }
3799
+ catch {
3800
+ // node invoke replies are best-effort; the gateway times out stale ids.
3801
+ }
3802
+ }
3803
+ async dispatch(frame) {
3804
+ const handler = this.handlers[frame.command];
3805
+ if (!handler) {
3806
+ return {
3807
+ ok: false,
3808
+ error: { code: "INVALID_REQUEST", message: `command not supported: ${frame.command}` },
3809
+ };
3810
+ }
3811
+ let params;
3812
+ try {
3813
+ params = frame.paramsJSON ? JSON.parse(frame.paramsJSON) : {};
3814
+ }
3815
+ catch {
3816
+ return {
3817
+ ok: false,
3818
+ error: { code: "INVALID_REQUEST", message: "paramsJSON malformed JSON" },
3819
+ };
3820
+ }
3821
+ try {
3822
+ const result = await handler(params);
3823
+ return { ok: true, payloadJSON: JSON.stringify(result === undefined ? null : result) };
3824
+ }
3825
+ catch (err) {
3826
+ return {
3827
+ ok: false,
3828
+ error: { code: "UNAVAILABLE", message: err instanceof Error ? err.message : String(err) },
3829
+ };
3830
+ }
3831
+ }
3832
+ }
2226
3833
  //# sourceMappingURL=gateway.js.map