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