ocuclaw 1.3.3 → 1.3.4
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 +29 -1
- package/dist/config/runtime-config-session-title-model.test.js +0 -3
- package/dist/config/runtime-config.js +22 -33
- package/dist/domain/activity-status-adapter.js +0 -7
- package/dist/domain/activity-status-arbiter.js +3 -27
- package/dist/domain/activity-status-labels.js +8 -38
- package/dist/domain/code-span-regions.js +4 -24
- package/dist/domain/constant-time-equal.js +9 -0
- package/dist/domain/constant-time-equal.test.js +28 -0
- package/dist/domain/conversation-state.js +27 -138
- package/dist/domain/debug-bundle-cache.js +52 -0
- package/dist/domain/debug-bundle-format.js +60 -0
- package/dist/domain/debug-bundle-preview.js +123 -0
- package/dist/domain/debug-bundle-redaction.js +182 -0
- package/dist/domain/debug-bundle-save.js +11 -0
- package/dist/domain/debug-bundle-zip.js +15 -0
- package/dist/domain/debug-bundle.js +97 -0
- package/dist/domain/debug-store.js +6 -17
- package/dist/domain/debug-upload-preset.js +27 -0
- package/dist/domain/glasses-display-system-prompt.js +0 -5
- package/dist/domain/glasses-display-system-prompt.test.js +1 -1
- package/dist/domain/glasses-ui-content-summary.js +0 -6
- package/dist/domain/glasses-ui-system-prompt.test.js +1 -2
- package/dist/domain/message-emoji-allowlist.js +0 -7
- package/dist/domain/message-emoji-filter.js +3 -9
- package/dist/domain/neural-emoji-reactor-tag-config.js +3 -3
- package/dist/domain/prompt-channel-fragments.js +1 -10
- package/dist/domain/tagged-span-parser.js +3 -26
- package/dist/domain/tagged-span-strip.js +0 -7
- package/dist/even-ai/even-ai-endpoint.js +77 -24
- package/dist/even-ai/even-ai-run-waiter.js +0 -1
- package/dist/even-ai/even-ai-settings-store.js +11 -0
- package/dist/gateway/gateway-bridge.js +8 -9
- package/dist/gateway/gateway-timing-ledger.js +8 -6
- package/dist/gateway/openclaw-client.js +97 -297
- package/dist/gateway/sanitize-connect-reason.js +10 -0
- package/dist/gateway/sanitize-connect-reason.test.js +34 -0
- package/dist/index.js +3 -3
- package/dist/runtime/channel-two-hook.js +1 -6
- package/dist/runtime/container-env.js +1 -5
- package/dist/runtime/debug-bundle-handler.js +159 -0
- package/dist/runtime/display-toggle-states.js +6 -17
- package/dist/runtime/downstream-handler.js +682 -508
- package/dist/runtime/glasses-backpressure-latch.js +2 -24
- package/dist/runtime/ocuclaw-settings-store.js +10 -1
- package/dist/runtime/openclaw-host-version.js +5 -0
- package/dist/runtime/plugin-version-service.js +13 -6
- package/dist/runtime/provider-usage-select.js +0 -6
- package/dist/runtime/register-session-title-distiller.js +14 -16
- package/dist/runtime/relay-core.js +601 -290
- package/dist/runtime/relay-service.js +19 -47
- package/dist/runtime/relay-worker-approval-replay-cache.js +1 -1
- package/dist/runtime/relay-worker-entry.js +1 -2
- package/dist/runtime/relay-worker-health.js +2 -10
- package/dist/runtime/relay-worker-protocol.js +6 -1
- package/dist/runtime/relay-worker-supervisor.js +103 -41
- package/dist/runtime/relay-worker-transport.js +150 -17
- package/dist/runtime/session-context-service.js +5 -45
- package/dist/runtime/session-service.js +157 -175
- package/dist/runtime/session-title-distiller-budget.js +1 -5
- package/dist/runtime/session-title-distiller-helpers.js +14 -24
- package/dist/runtime/session-title-distiller.js +109 -122
- package/dist/runtime/session-title-record.js +0 -6
- package/dist/runtime/stable-prompt-snapshot.js +3 -14
- package/dist/runtime/upstream-runtime.js +600 -103
- package/dist/tools/device-info-tool.js +4 -21
- package/dist/tools/glasses-ui-cron.js +22 -77
- package/dist/tools/glasses-ui-descriptors.js +4 -33
- package/dist/tools/glasses-ui-limits.js +0 -13
- package/dist/tools/glasses-ui-paint-floor.js +5 -39
- package/dist/tools/glasses-ui-recipes.js +92 -101
- package/dist/tools/glasses-ui-surfaces.js +31 -163
- package/dist/tools/glasses-ui-template.js +7 -22
- package/dist/tools/glasses-ui-tool-description.test.js +2 -2
- package/dist/tools/glasses-ui-tool.js +87 -451
- package/dist/tools/glasses-ui-voicemail.js +6 -63
- package/dist/tools/glasses-ui-wake.js +9 -76
- package/dist/tools/session-title-tool.js +2 -7
- package/dist/tools/session-title-tool.test.js +1 -1
- package/dist/version.js +3 -2
- package/openclaw.plugin.json +60 -13
- package/package.json +3 -2
- package/dist/runtime/protocol-adapter.js +0 -387
|
@@ -4,8 +4,7 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import WebSocket from "ws";
|
|
6
6
|
import { createGatewayTimingLedger } from "./gateway-timing-ledger.js";
|
|
7
|
-
|
|
8
|
-
// --- Constants ---
|
|
7
|
+
import { sanitizeConnectReason } from "./sanitize-connect-reason.js";
|
|
9
8
|
|
|
10
9
|
const DEVICE_KEY_FILE = "ocuclaw-device-key.json";
|
|
11
10
|
const DEVICE_TOKEN_FILE = "ocuclaw-device-token.json";
|
|
@@ -24,11 +23,14 @@ const MIN_PROTOCOL_VERSION = 3;
|
|
|
24
23
|
const MAX_PROTOCOL_VERSION = 4;
|
|
25
24
|
const HISTORY_ACTIVITY_POLL_INTERVAL_MS = 500;
|
|
26
25
|
const HISTORY_ACTIVITY_POLL_LIMIT = 40;
|
|
27
|
-
|
|
28
|
-
// during a mid-turn run. Comfortably above normal ack latency but below the
|
|
29
|
-
// ~60s coarse tick-watch run backstop. Disarmed when an accepted ack arrives.
|
|
26
|
+
|
|
30
27
|
const RPC_ACK_TIMEOUT_MS = 15000;
|
|
31
28
|
|
|
29
|
+
const ESTABLISH_TIMEOUT_MS = 10000;
|
|
30
|
+
|
|
31
|
+
const TICK_WATCH_MAX_INTERVAL_MS = 60000;
|
|
32
|
+
const TICK_STALE_MULTIPLIER = 1.5;
|
|
33
|
+
|
|
32
34
|
const THINKING_SUMMARY_KEYS = [
|
|
33
35
|
"summary",
|
|
34
36
|
"thinkingSummary",
|
|
@@ -110,12 +112,10 @@ function writeJsonFile(filePath, data) {
|
|
|
110
112
|
try {
|
|
111
113
|
fs.chmodSync(filePath, 0o600);
|
|
112
114
|
} catch {
|
|
113
|
-
|
|
115
|
+
|
|
114
116
|
}
|
|
115
117
|
}
|
|
116
118
|
|
|
117
|
-
// --- Base64url helpers ---
|
|
118
|
-
|
|
119
119
|
function base64UrlEncode(buf) {
|
|
120
120
|
return buf
|
|
121
121
|
.toString("base64")
|
|
@@ -124,12 +124,6 @@ function base64UrlEncode(buf) {
|
|
|
124
124
|
.replace(/=+$/g, "");
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
// --- Device identity ---
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Extract raw 32-byte Ed25519 public key from SPKI DER.
|
|
131
|
-
* Strips the standard 12-byte SPKI prefix for Ed25519 keys.
|
|
132
|
-
*/
|
|
133
127
|
function derivePublicKeyRaw(publicKeyPem) {
|
|
134
128
|
const key = crypto.createPublicKey(publicKeyPem);
|
|
135
129
|
const spki = key.export({ type: "spki", format: "der" });
|
|
@@ -142,17 +136,11 @@ function derivePublicKeyRaw(publicKeyPem) {
|
|
|
142
136
|
return spki;
|
|
143
137
|
}
|
|
144
138
|
|
|
145
|
-
/**
|
|
146
|
-
* SHA-256 hex hash of the raw 32-byte public key.
|
|
147
|
-
*/
|
|
148
139
|
function fingerprintPublicKey(publicKeyPem) {
|
|
149
140
|
const raw = derivePublicKeyRaw(publicKeyPem);
|
|
150
141
|
return crypto.createHash("sha256").update(raw).digest("hex");
|
|
151
142
|
}
|
|
152
143
|
|
|
153
|
-
/**
|
|
154
|
-
* Generate a new Ed25519 keypair.
|
|
155
|
-
*/
|
|
156
144
|
function generateIdentity() {
|
|
157
145
|
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
158
146
|
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
@@ -161,12 +149,9 @@ function generateIdentity() {
|
|
|
161
149
|
return { deviceId, publicKeyPem, privateKeyPem };
|
|
162
150
|
}
|
|
163
151
|
|
|
164
|
-
/**
|
|
165
|
-
* Load device identity from disk, or generate and persist a new one.
|
|
166
|
-
*/
|
|
167
152
|
function loadOrCreateDeviceIdentity(persistencePaths, logger) {
|
|
168
153
|
const deviceKeyPath = persistencePaths && persistencePaths.deviceKeyPath;
|
|
169
|
-
|
|
154
|
+
|
|
170
155
|
try {
|
|
171
156
|
if (deviceKeyPath && fs.existsSync(deviceKeyPath)) {
|
|
172
157
|
const raw = fs.readFileSync(deviceKeyPath, "utf8");
|
|
@@ -178,10 +163,10 @@ function loadOrCreateDeviceIdentity(persistencePaths, logger) {
|
|
|
178
163
|
typeof parsed.publicKeyPem === "string" &&
|
|
179
164
|
typeof parsed.privateKeyPem === "string"
|
|
180
165
|
) {
|
|
181
|
-
|
|
166
|
+
|
|
182
167
|
const derivedId = fingerprintPublicKey(parsed.publicKeyPem);
|
|
183
168
|
if (derivedId && derivedId !== parsed.deviceId) {
|
|
184
|
-
|
|
169
|
+
|
|
185
170
|
const updated = { ...parsed, deviceId: derivedId };
|
|
186
171
|
writeJsonFile(deviceKeyPath, updated);
|
|
187
172
|
logger.info(
|
|
@@ -204,10 +189,9 @@ function loadOrCreateDeviceIdentity(persistencePaths, logger) {
|
|
|
204
189
|
}
|
|
205
190
|
}
|
|
206
191
|
} catch {
|
|
207
|
-
|
|
192
|
+
|
|
208
193
|
}
|
|
209
194
|
|
|
210
|
-
// Generate new identity
|
|
211
195
|
const identity = generateIdentity();
|
|
212
196
|
if (!deviceKeyPath) {
|
|
213
197
|
logger.info(
|
|
@@ -229,12 +213,6 @@ function loadOrCreateDeviceIdentity(persistencePaths, logger) {
|
|
|
229
213
|
return identity;
|
|
230
214
|
}
|
|
231
215
|
|
|
232
|
-
// --- Device token cache ---
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Load cached device token from disk.
|
|
236
|
-
* Returns token string or null.
|
|
237
|
-
*/
|
|
238
216
|
function loadDeviceToken(deviceId, persistencePaths) {
|
|
239
217
|
const deviceTokenPath = persistencePaths && persistencePaths.deviceTokenPath;
|
|
240
218
|
try {
|
|
@@ -255,9 +233,6 @@ function loadDeviceToken(deviceId, persistencePaths) {
|
|
|
255
233
|
}
|
|
256
234
|
}
|
|
257
235
|
|
|
258
|
-
/**
|
|
259
|
-
* Store device token to disk.
|
|
260
|
-
*/
|
|
261
236
|
function storeDeviceToken(deviceId, token, role, scopes, persistencePaths) {
|
|
262
237
|
const deviceTokenPath = persistencePaths && persistencePaths.deviceTokenPath;
|
|
263
238
|
if (!deviceTokenPath) return;
|
|
@@ -272,9 +247,6 @@ function storeDeviceToken(deviceId, token, role, scopes, persistencePaths) {
|
|
|
272
247
|
writeJsonFile(deviceTokenPath, data);
|
|
273
248
|
}
|
|
274
249
|
|
|
275
|
-
/**
|
|
276
|
-
* Clear cached device token.
|
|
277
|
-
*/
|
|
278
250
|
function clearDeviceToken(persistencePaths) {
|
|
279
251
|
const deviceTokenPath = persistencePaths && persistencePaths.deviceTokenPath;
|
|
280
252
|
try {
|
|
@@ -282,16 +254,10 @@ function clearDeviceToken(persistencePaths) {
|
|
|
282
254
|
fs.unlinkSync(deviceTokenPath);
|
|
283
255
|
}
|
|
284
256
|
} catch {
|
|
285
|
-
|
|
257
|
+
|
|
286
258
|
}
|
|
287
259
|
}
|
|
288
260
|
|
|
289
|
-
// --- Auth payload ---
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Build the pipe-delimited device auth payload string.
|
|
293
|
-
* Format: v2|{deviceId}|{clientId}|{clientMode}|{role}|{scopes}|{signedAtMs}|{token}|{nonce}
|
|
294
|
-
*/
|
|
295
261
|
function buildDeviceAuthPayload(params) {
|
|
296
262
|
const version = params.nonce ? "v2" : "v1";
|
|
297
263
|
const scopes = params.scopes.join(",");
|
|
@@ -312,18 +278,12 @@ function buildDeviceAuthPayload(params) {
|
|
|
312
278
|
return parts.join("|");
|
|
313
279
|
}
|
|
314
280
|
|
|
315
|
-
/**
|
|
316
|
-
* Sign a payload string with Ed25519 private key, return base64url signature.
|
|
317
|
-
*/
|
|
318
281
|
function signPayload(privateKeyPem, payload) {
|
|
319
282
|
const key = crypto.createPrivateKey(privateKeyPem);
|
|
320
283
|
const sig = crypto.sign(null, Buffer.from(payload, "utf8"), key);
|
|
321
284
|
return base64UrlEncode(sig);
|
|
322
285
|
}
|
|
323
286
|
|
|
324
|
-
/**
|
|
325
|
-
* Get the raw public key as base64url from PEM.
|
|
326
|
-
*/
|
|
327
287
|
function publicKeyRawBase64Url(publicKeyPem) {
|
|
328
288
|
return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
|
|
329
289
|
}
|
|
@@ -389,7 +349,7 @@ function pickFirstStringEntry(obj, keys) {
|
|
|
389
349
|
|
|
390
350
|
function normalizeThinkingText(raw) {
|
|
391
351
|
if (typeof raw !== "string") return null;
|
|
392
|
-
|
|
352
|
+
|
|
393
353
|
const cleaned = raw
|
|
394
354
|
.replace(/\*\*/g, "")
|
|
395
355
|
.trim();
|
|
@@ -618,10 +578,7 @@ function buildTerminalErrorActivity(data, fallbackRunId, fallbackSessionKey, fal
|
|
|
618
578
|
if (label) activity.label = label;
|
|
619
579
|
if (detail) activity.detail = detail;
|
|
620
580
|
else if (label) activity.detail = label;
|
|
621
|
-
|
|
622
|
-
// trigger a profile rotation or fallback retry. Surface it as a hint so the
|
|
623
|
-
// WebUI can suppress sticky failure feedback for runs that will retry on
|
|
624
|
-
// another auth profile or model rather than ending the user-visible turn.
|
|
581
|
+
|
|
625
582
|
if (pickTrimmedString(source.failoverReason)) {
|
|
626
583
|
activity.failoverPending = true;
|
|
627
584
|
}
|
|
@@ -770,8 +727,6 @@ function hashThinkingKey(seed) {
|
|
|
770
727
|
return crypto.createHash("sha1").update(seed).digest("hex").slice(0, 16);
|
|
771
728
|
}
|
|
772
729
|
|
|
773
|
-
// --- OpenClaw Gateway Client ---
|
|
774
|
-
|
|
775
730
|
class OpenClawClient extends EventEmitter {
|
|
776
731
|
constructor(opts = {}) {
|
|
777
732
|
super();
|
|
@@ -786,50 +741,38 @@ class OpenClawClient extends EventEmitter {
|
|
|
786
741
|
this._persistencePaths = resolvePersistencePaths(opts.stateDir);
|
|
787
742
|
this._ws = null;
|
|
788
743
|
this._stopped = false;
|
|
789
|
-
this._pending = new Map();
|
|
744
|
+
this._pending = new Map();
|
|
790
745
|
this._identity = null;
|
|
791
746
|
this._connectNonce = null;
|
|
792
747
|
this._connectSent = false;
|
|
793
748
|
this._connectTimer = null;
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
// A fresh socket is automatically "handshake-incomplete" (its generation is
|
|
798
|
-
// strictly greater than the watermark) with no extra reset bookkeeping —
|
|
799
|
-
// mirrors the existing _activeRunGeneration idiom. request() refuses any
|
|
800
|
-
// non-connect frame on a real ws socket whose generation has not yet
|
|
801
|
-
// handshaked, which is what enforces the gateway's "first frame must be
|
|
802
|
-
// connect" invariant across reconnect churn.
|
|
749
|
+
|
|
750
|
+
this._establishTimer = null;
|
|
751
|
+
|
|
803
752
|
this._socketGeneration = 0;
|
|
804
753
|
this._handshakeGeneration = -1;
|
|
805
754
|
this._tickIntervalMs = 30000;
|
|
806
|
-
this._deviceToken = null;
|
|
755
|
+
this._deviceToken = null;
|
|
807
756
|
|
|
808
|
-
// --- Reconnection (step 7) ---
|
|
809
757
|
this._backoffMs = 1000;
|
|
810
758
|
this._reconnectTimer = null;
|
|
811
759
|
|
|
812
|
-
// --- Tick watch (step 7) ---
|
|
813
760
|
this._lastTick = null;
|
|
814
761
|
this._tickWatchTimer = null;
|
|
815
762
|
|
|
816
|
-
// --- Agent run state (steps 4-5) ---
|
|
817
763
|
this._activeRunId = null;
|
|
818
764
|
this._activeRunSessionKey = null;
|
|
819
765
|
this._activeRunStartedAtMs = null;
|
|
820
766
|
this._activeRunGeneration = 0;
|
|
821
|
-
this._runTextBuffer = "";
|
|
767
|
+
this._runTextBuffer = "";
|
|
822
768
|
|
|
823
|
-
// --- Agent identity (step 6) ---
|
|
824
769
|
this._agentIdentity = null;
|
|
825
770
|
|
|
826
|
-
// --- Sequence tracking (step 8) ---
|
|
827
771
|
this._lastSeq = null;
|
|
828
|
-
this._gapDuringRun = false;
|
|
772
|
+
this._gapDuringRun = false;
|
|
829
773
|
|
|
830
|
-
// --- History hydration (step 9) ---
|
|
831
774
|
this._historyResolved = false;
|
|
832
|
-
this._eventQueue = [];
|
|
775
|
+
this._eventQueue = [];
|
|
833
776
|
this._historyActivityPollTimer = null;
|
|
834
777
|
this._historyActivityPollInFlightGeneration = null;
|
|
835
778
|
this._seenThinkingSummaryIds = new Set();
|
|
@@ -840,20 +783,15 @@ class OpenClawClient extends EventEmitter {
|
|
|
840
783
|
this._timingLedger.setLogger(this._logger);
|
|
841
784
|
}
|
|
842
785
|
|
|
843
|
-
/**
|
|
844
|
-
* Begin connecting to the gateway (non-blocking).
|
|
845
|
-
*/
|
|
846
786
|
start() {
|
|
847
787
|
if (this._stopped) return;
|
|
848
788
|
|
|
849
|
-
// Load or create device identity (only on first start)
|
|
850
789
|
if (!this._identity) {
|
|
851
790
|
this._identity = loadOrCreateDeviceIdentity(
|
|
852
791
|
this._persistencePaths,
|
|
853
792
|
this._logger,
|
|
854
793
|
);
|
|
855
794
|
|
|
856
|
-
// Load cached device token
|
|
857
795
|
this._deviceToken = loadDeviceToken(
|
|
858
796
|
this._identity.deviceId,
|
|
859
797
|
this._persistencePaths,
|
|
@@ -866,15 +804,13 @@ class OpenClawClient extends EventEmitter {
|
|
|
866
804
|
this._connect();
|
|
867
805
|
}
|
|
868
806
|
|
|
869
|
-
/**
|
|
870
|
-
* Disconnect and stop.
|
|
871
|
-
*/
|
|
872
807
|
stop() {
|
|
873
808
|
this._stopped = true;
|
|
874
809
|
if (this._connectTimer) {
|
|
875
810
|
clearTimeout(this._connectTimer);
|
|
876
811
|
this._connectTimer = null;
|
|
877
812
|
}
|
|
813
|
+
this._clearEstablishTimer();
|
|
878
814
|
if (this._reconnectTimer) {
|
|
879
815
|
clearTimeout(this._reconnectTimer);
|
|
880
816
|
this._reconnectTimer = null;
|
|
@@ -890,31 +826,15 @@ class OpenClawClient extends EventEmitter {
|
|
|
890
826
|
this.emit("status", "stopped");
|
|
891
827
|
}
|
|
892
828
|
|
|
893
|
-
/**
|
|
894
|
-
* Send a request to the gateway. Returns a promise that resolves with the response payload.
|
|
895
|
-
* @param {string} method
|
|
896
|
-
* @param {object} [params]
|
|
897
|
-
* @param {{ expectFinal?: boolean }} [opts] - If expectFinal is true, skip intermediate
|
|
898
|
-
* acks (status: "accepted") and resolve only on the final response.
|
|
899
|
-
*/
|
|
900
829
|
request(method, params, opts) {
|
|
901
|
-
|
|
902
|
-
// this._ws between entry and the synchronous send can never land this frame
|
|
903
|
-
// on a successor socket (kills the successor-socket race variant).
|
|
830
|
+
|
|
904
831
|
const ws = this._ws;
|
|
905
832
|
const gen = this._socketGeneration;
|
|
906
833
|
if (!ws || ws !== this._ws || ws.readyState !== WebSocket.OPEN) {
|
|
907
|
-
|
|
908
|
-
// no-usable-socket case. The handshake gate below uses a DISTINCT message.
|
|
834
|
+
|
|
909
835
|
return Promise.reject(new Error("gateway not connected"));
|
|
910
836
|
}
|
|
911
|
-
|
|
912
|
-
// on a freshly-OPEN-but-unregistered socket (gateway emits 1008). connect
|
|
913
|
-
// BYPASSES the gate (it IS the first allowed frame). The gate only applies
|
|
914
|
-
// to real `ws.WebSocket` sockets — the unit harnesses inject plain-object
|
|
915
|
-
// fakes ({ readyState: 1, send }) which are intentionally treated as
|
|
916
|
-
// already-handshaken so their request("agent"/"ping"/"chat.history") still
|
|
917
|
-
// sends with no harness changes.
|
|
837
|
+
|
|
918
838
|
if (
|
|
919
839
|
method !== "connect" &&
|
|
920
840
|
ws instanceof WebSocket &&
|
|
@@ -932,9 +852,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
932
852
|
const promise = new Promise((resolve, reject) => {
|
|
933
853
|
this._pending.set(id, { resolve, reject, expectFinal, method, diagnostic });
|
|
934
854
|
});
|
|
935
|
-
|
|
936
|
-
// when an accepted ack arrives (keepPending branch) so a legitimately
|
|
937
|
-
// long-running mid-turn run is NOT killed by this timeout.
|
|
855
|
+
|
|
938
856
|
const timer = setTimeout(() => {
|
|
939
857
|
const pendingEntry = this._pending.get(id);
|
|
940
858
|
if (!pendingEntry) return;
|
|
@@ -964,15 +882,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
964
882
|
return promise;
|
|
965
883
|
}
|
|
966
884
|
|
|
967
|
-
// --- Public: messaging (step 4) ---
|
|
968
|
-
|
|
969
|
-
/**
|
|
970
|
-
* Send a user message to the OpenClaw agent.
|
|
971
|
-
* Fire-and-forget: sends the request, streaming events arrive via event handlers.
|
|
972
|
-
* @param {string} text - Message text
|
|
973
|
-
* @param {string} [sessionKey="main"] - Session key
|
|
974
|
-
* @param {object|null} [attachment] - Optional image attachment payload
|
|
975
|
-
*/
|
|
976
885
|
sendMessage(text, sessionKey, attachment) {
|
|
977
886
|
const key = sessionKey || "main";
|
|
978
887
|
const idempotencyKey = crypto.randomUUID();
|
|
@@ -993,8 +902,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
993
902
|
];
|
|
994
903
|
}
|
|
995
904
|
|
|
996
|
-
// Resolve on the initial ack (accepted/queued) for immediate feedback.
|
|
997
|
-
// Agent streaming events arrive independently via event handlers.
|
|
998
905
|
return this.request(
|
|
999
906
|
"agent",
|
|
1000
907
|
params,
|
|
@@ -1012,13 +919,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1012
919
|
});
|
|
1013
920
|
}
|
|
1014
921
|
|
|
1015
|
-
// --- Public: agent identity (step 6) ---
|
|
1016
|
-
|
|
1017
|
-
/**
|
|
1018
|
-
* Fetch agent identity from the gateway. Caches the result.
|
|
1019
|
-
* @param {string} [sessionKey] - Optional session key
|
|
1020
|
-
* @returns {Promise<{agentId, name, emoji, avatar}>}
|
|
1021
|
-
*/
|
|
1022
922
|
async fetchAgentIdentity(sessionKey) {
|
|
1023
923
|
const params = sessionKey ? { sessionKey } : {};
|
|
1024
924
|
const result = await this.request("agent.identity.get", params);
|
|
@@ -1028,12 +928,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1028
928
|
return result;
|
|
1029
929
|
}
|
|
1030
930
|
|
|
1031
|
-
/**
|
|
1032
|
-
* Resolve an approval request.
|
|
1033
|
-
* @param {string} id - Approval request ID
|
|
1034
|
-
* @param {string} decision - "allow-once", "allow-always", or "deny"
|
|
1035
|
-
* @returns {Promise}
|
|
1036
|
-
*/
|
|
1037
931
|
resolveApproval(id, decision) {
|
|
1038
932
|
const method =
|
|
1039
933
|
typeof id === "string" && id.startsWith("plugin:")
|
|
@@ -1076,28 +970,21 @@ class OpenClawClient extends EventEmitter {
|
|
|
1076
970
|
);
|
|
1077
971
|
}
|
|
1078
972
|
|
|
1079
|
-
// --- Internal: connection ---
|
|
1080
|
-
|
|
1081
973
|
_connect() {
|
|
1082
974
|
if (this._stopped) return;
|
|
1083
975
|
|
|
1084
|
-
// Close any existing WebSocket to prevent parallel connections
|
|
1085
|
-
// (e.g., from shutdown handler scheduling reconnect before close fires)
|
|
1086
976
|
if (this._ws) {
|
|
1087
|
-
try { this._ws.close(); } catch {
|
|
977
|
+
try { this._ws.close(); } catch { }
|
|
1088
978
|
this._ws = null;
|
|
1089
979
|
}
|
|
1090
980
|
|
|
1091
|
-
// Clear a stale 750ms connect-fallback timer left armed by a prior
|
|
1092
|
-
// generation that opened then closed before it fired. _connect() resets
|
|
1093
|
-
// _connectSent=false below, so a stale timer firing here would re-enter
|
|
1094
|
-
// _sendConnect() on the NEW socket — harmless (still a connect frame, never
|
|
1095
|
-
// a 1008), but it can produce a redundant connect. Clearing closes the edge.
|
|
1096
981
|
if (this._connectTimer) {
|
|
1097
982
|
clearTimeout(this._connectTimer);
|
|
1098
983
|
this._connectTimer = null;
|
|
1099
984
|
}
|
|
1100
985
|
|
|
986
|
+
this._clearEstablishTimer();
|
|
987
|
+
|
|
1101
988
|
const url = this._gatewayUrl;
|
|
1102
989
|
this.emit("status", "connecting");
|
|
1103
990
|
this._logger.info(`[openclaw] Connecting to ${url}`);
|
|
@@ -1105,14 +992,8 @@ class OpenClawClient extends EventEmitter {
|
|
|
1105
992
|
this._connectNonce = null;
|
|
1106
993
|
this._connectSent = false;
|
|
1107
994
|
|
|
1108
|
-
// Bump the socket generation BEFORE constructing the new socket. The
|
|
1109
|
-
// handshake watermark (_handshakeGeneration) intentionally STAYS at the
|
|
1110
|
-
// prior value, so the about-to-be-created socket is automatically
|
|
1111
|
-
// handshake-incomplete (its generation > watermark) until its own connect
|
|
1112
|
-
// resolves. No watermark reset is needed.
|
|
1113
995
|
this._socketGeneration += 1;
|
|
1114
996
|
|
|
1115
|
-
// Reset per-connection state
|
|
1116
997
|
this._timingLedger.clear("connect_reset");
|
|
1117
998
|
this._lastSeq = null;
|
|
1118
999
|
this._lastTick = null;
|
|
@@ -1123,10 +1004,13 @@ class OpenClawClient extends EventEmitter {
|
|
|
1123
1004
|
const ws = new WebSocket(url, { maxPayload: 25 * 1024 * 1024 });
|
|
1124
1005
|
this._ws = ws;
|
|
1125
1006
|
|
|
1007
|
+
this._armEstablishTimer(ws);
|
|
1008
|
+
|
|
1126
1009
|
ws.on("open", () => {
|
|
1010
|
+
|
|
1011
|
+
this._clearEstablishTimer();
|
|
1127
1012
|
this._logger.info("[openclaw] WebSocket open, waiting for challenge...");
|
|
1128
|
-
|
|
1129
|
-
// (mirrors the reference client's queueConnect fallback)
|
|
1013
|
+
|
|
1130
1014
|
this._connectTimer = setTimeout(() => {
|
|
1131
1015
|
this._sendConnect();
|
|
1132
1016
|
}, 750);
|
|
@@ -1140,14 +1024,14 @@ class OpenClawClient extends EventEmitter {
|
|
|
1140
1024
|
const reasonText = reason ? reason.toString() : "";
|
|
1141
1025
|
this._logger.info(`[openclaw] WebSocket closed: ${code} ${reasonText}`);
|
|
1142
1026
|
this._ws = null;
|
|
1027
|
+
this._clearEstablishTimer();
|
|
1143
1028
|
this._stopTickWatch();
|
|
1144
1029
|
this._stopHistoryActivityPolling();
|
|
1145
1030
|
this._timingLedger.clear("disconnect");
|
|
1146
1031
|
this._flushPendingErrors(new Error(`gateway closed (${code}): ${reasonText}`));
|
|
1147
1032
|
this.emit("disconnected", { code, reason: reasonText });
|
|
1148
1033
|
this.emit("status", "disconnected");
|
|
1149
|
-
|
|
1150
|
-
// (e.g., shutdown handler may have already scheduled with a specific delay)
|
|
1034
|
+
|
|
1151
1035
|
if (!this._reconnectTimer) {
|
|
1152
1036
|
this._scheduleReconnect();
|
|
1153
1037
|
}
|
|
@@ -1161,7 +1045,33 @@ class OpenClawClient extends EventEmitter {
|
|
|
1161
1045
|
});
|
|
1162
1046
|
}
|
|
1163
1047
|
|
|
1164
|
-
|
|
1048
|
+
_armEstablishTimer(ws) {
|
|
1049
|
+
const establishGeneration = this._socketGeneration;
|
|
1050
|
+
this._establishTimer = setTimeout(() => {
|
|
1051
|
+
this._establishTimer = null;
|
|
1052
|
+
if (this._stopped || ws !== this._ws || establishGeneration !== this._socketGeneration) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
this._logger.warn(
|
|
1056
|
+
`[openclaw] Connect establishment timeout (${ESTABLISH_TIMEOUT_MS}ms), terminating socket`
|
|
1057
|
+
);
|
|
1058
|
+
try {
|
|
1059
|
+
ws.terminate();
|
|
1060
|
+
} catch {
|
|
1061
|
+
|
|
1062
|
+
}
|
|
1063
|
+
}, ESTABLISH_TIMEOUT_MS);
|
|
1064
|
+
if (this._establishTimer.unref) {
|
|
1065
|
+
this._establishTimer.unref();
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
_clearEstablishTimer() {
|
|
1070
|
+
if (this._establishTimer) {
|
|
1071
|
+
clearTimeout(this._establishTimer);
|
|
1072
|
+
this._establishTimer = null;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1165
1075
|
|
|
1166
1076
|
_handleMessage(raw) {
|
|
1167
1077
|
let parsed;
|
|
@@ -1172,21 +1082,17 @@ class OpenClawClient extends EventEmitter {
|
|
|
1172
1082
|
return;
|
|
1173
1083
|
}
|
|
1174
1084
|
|
|
1175
|
-
// Emit protocol event for every incoming frame (step 10)
|
|
1176
1085
|
this.emit("protocol", { direction: "in", frame: parsed });
|
|
1177
1086
|
|
|
1178
|
-
// Event frames: { type: "event", event: "...", payload: ... }
|
|
1179
1087
|
if (parsed.type === "event") {
|
|
1180
1088
|
this._handleEvent(parsed);
|
|
1181
1089
|
return;
|
|
1182
1090
|
}
|
|
1183
1091
|
|
|
1184
|
-
// Response frames: { type: "res", id: "...", ok: true/false, payload: ... }
|
|
1185
1092
|
if (parsed.type === "res") {
|
|
1186
1093
|
const pending = this._pending.get(parsed.id);
|
|
1187
1094
|
if (!pending) return;
|
|
1188
1095
|
|
|
1189
|
-
// If expectFinal, skip intermediate acks (status: "accepted") (step 4)
|
|
1190
1096
|
const payload = parsed.payload;
|
|
1191
1097
|
const status = payload && payload.status;
|
|
1192
1098
|
const keepPending =
|
|
@@ -1200,21 +1106,19 @@ class OpenClawClient extends EventEmitter {
|
|
|
1200
1106
|
keepPending,
|
|
1201
1107
|
});
|
|
1202
1108
|
if (keepPending) {
|
|
1203
|
-
|
|
1204
|
-
// disarm the ACK timeout (the coarse tick-watch remains the run backstop).
|
|
1109
|
+
|
|
1205
1110
|
if (pending.timer) {
|
|
1206
1111
|
clearTimeout(pending.timer);
|
|
1207
1112
|
pending.timer = null;
|
|
1208
1113
|
}
|
|
1209
|
-
|
|
1114
|
+
|
|
1210
1115
|
if (payload.runId) {
|
|
1211
1116
|
this._activeRunId = payload.runId;
|
|
1212
1117
|
this._logger.info(`[openclaw] Agent run accepted: ${payload.runId}`);
|
|
1213
1118
|
}
|
|
1214
|
-
return;
|
|
1119
|
+
return;
|
|
1215
1120
|
}
|
|
1216
1121
|
|
|
1217
|
-
// Final settle: clear the ACK timeout before resolving/rejecting.
|
|
1218
1122
|
if (pending.timer) {
|
|
1219
1123
|
clearTimeout(pending.timer);
|
|
1220
1124
|
pending.timer = null;
|
|
@@ -1232,7 +1136,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
1232
1136
|
if (parsed.error && parsed.error.data !== undefined) {
|
|
1233
1137
|
err.data = parsed.error.data;
|
|
1234
1138
|
}
|
|
1235
|
-
|
|
1139
|
+
|
|
1236
1140
|
if (parsed.error && parsed.error.retryable && parsed.error.retryAfterMs) {
|
|
1237
1141
|
err.retryAfterMs = parsed.error.retryAfterMs;
|
|
1238
1142
|
}
|
|
@@ -1242,10 +1146,8 @@ class OpenClawClient extends EventEmitter {
|
|
|
1242
1146
|
}
|
|
1243
1147
|
}
|
|
1244
1148
|
|
|
1245
|
-
// --- Internal: event routing ---
|
|
1246
|
-
|
|
1247
1149
|
_handleEvent(evt) {
|
|
1248
|
-
|
|
1150
|
+
|
|
1249
1151
|
if (evt.event === "connect.challenge") {
|
|
1250
1152
|
const nonce =
|
|
1251
1153
|
evt.payload && typeof evt.payload.nonce === "string" ? evt.payload.nonce : null;
|
|
@@ -1257,7 +1159,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1257
1159
|
return;
|
|
1258
1160
|
}
|
|
1259
1161
|
|
|
1260
|
-
// --- Sequence tracking (step 8) ---
|
|
1261
1162
|
const seq = typeof evt.seq === "number" ? evt.seq : null;
|
|
1262
1163
|
if (seq !== null) {
|
|
1263
1164
|
if (this._lastSeq !== null && seq > this._lastSeq + 1) {
|
|
@@ -1266,7 +1167,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
1266
1167
|
`[openclaw] Sequence gap: expected ${gapInfo.expected}, received ${gapInfo.received}`
|
|
1267
1168
|
);
|
|
1268
1169
|
this.emit("gap", gapInfo);
|
|
1269
|
-
|
|
1170
|
+
|
|
1270
1171
|
if (this._activeRunId) {
|
|
1271
1172
|
this._gapDuringRun = true;
|
|
1272
1173
|
}
|
|
@@ -1274,29 +1175,25 @@ class OpenClawClient extends EventEmitter {
|
|
|
1274
1175
|
this._lastSeq = seq;
|
|
1275
1176
|
}
|
|
1276
1177
|
|
|
1277
|
-
// --- Tick handling (step 7) ---
|
|
1278
1178
|
if (evt.event === "tick") {
|
|
1279
1179
|
this._lastTick = Date.now();
|
|
1280
1180
|
return;
|
|
1281
1181
|
}
|
|
1282
1182
|
|
|
1283
|
-
// --- Shutdown handling (step 7) ---
|
|
1284
1183
|
if (evt.event === "shutdown") {
|
|
1285
1184
|
const payload = evt.payload || {};
|
|
1286
1185
|
const restartMs = typeof payload.restartExpectedMs === "number" ? payload.restartExpectedMs : 5000;
|
|
1287
1186
|
this._logger.info(`[openclaw] Gateway shutdown, reconnecting in ${restartMs}ms`);
|
|
1288
1187
|
this.emit("status", "shutdown");
|
|
1289
|
-
|
|
1188
|
+
|
|
1290
1189
|
this._scheduleReconnect(restartMs);
|
|
1291
|
-
|
|
1292
|
-
// a second reconnect with normal backoff (double-reconnect race).
|
|
1190
|
+
|
|
1293
1191
|
if (this._ws) {
|
|
1294
1192
|
this._ws.close(1000, "shutdown");
|
|
1295
1193
|
}
|
|
1296
1194
|
return;
|
|
1297
1195
|
}
|
|
1298
1196
|
|
|
1299
|
-
// --- Approval events ---
|
|
1300
1197
|
if (evt.event === "exec.approval.requested") {
|
|
1301
1198
|
this.emit("approval", evt.payload);
|
|
1302
1199
|
return;
|
|
@@ -1317,7 +1214,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1317
1214
|
return;
|
|
1318
1215
|
}
|
|
1319
1216
|
|
|
1320
|
-
// --- Agent events (step 5) ---
|
|
1321
1217
|
if (evt.event === "agent") {
|
|
1322
1218
|
const payload = evt.payload || {};
|
|
1323
1219
|
const data = payload.data || {};
|
|
@@ -1330,25 +1226,10 @@ class OpenClawClient extends EventEmitter {
|
|
|
1330
1226
|
phase: data.phase,
|
|
1331
1227
|
data,
|
|
1332
1228
|
});
|
|
1333
|
-
|
|
1334
|
-
// mutates the persistent message list downstream (lifecycle:end ->
|
|
1335
|
-
// emit("message") -> conversationState.addMessage, a blind push), and the
|
|
1336
|
-
// history hydrate is a DESTRUCTIVE replace-all (conversation-state.ts
|
|
1337
|
-
// hydrate()). To avoid the commit being clobbered/duplicated by a later
|
|
1338
|
-
// hydrate, the commit must still run AFTER history resolves. Every other
|
|
1339
|
-
// agent event (lifecycle:start, assistant/streaming, tool, error) emits
|
|
1340
|
-
// only transient/live effects (activity/streaming/error) that hydrate
|
|
1341
|
-
// never touches, so processing them immediately is safe and removes the
|
|
1342
|
-
// reconnect responsiveness delay. The intact commit event is queued and
|
|
1343
|
-
// replayed via _drainEventQueue, preserving the de-dup contract exactly.
|
|
1229
|
+
|
|
1344
1230
|
const isCommitEvent = data.phase === "end" && payload.stream === "lifecycle";
|
|
1345
1231
|
if (isCommitEvent && !this._historyResolved) {
|
|
1346
|
-
|
|
1347
|
-
// lifecycle:end branch reads _runTextBuffer / _activeRunId / _gapDuringRun
|
|
1348
|
-
// (and _activeRunSessionKey as the session-key fallback) — all of which a
|
|
1349
|
-
// LATER run's lifecycle:start (_beginActiveRun) or an _invalidateActiveRun
|
|
1350
|
-
// mutates before the queue drains. Capturing now keeps the deferred commit
|
|
1351
|
-
// contemporaneous with its own run, restoring pre-F18 replay semantics.
|
|
1232
|
+
|
|
1352
1233
|
this._eventQueue.push({
|
|
1353
1234
|
payload: evt.payload,
|
|
1354
1235
|
capturedCommit: {
|
|
@@ -1365,12 +1246,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1365
1246
|
}
|
|
1366
1247
|
}
|
|
1367
1248
|
|
|
1368
|
-
// --- Internal: agent streaming (step 5) ---
|
|
1369
|
-
|
|
1370
|
-
/**
|
|
1371
|
-
* Handle an agent event payload.
|
|
1372
|
-
* Buffers assistant text deltas, emits activity/message events.
|
|
1373
|
-
*/
|
|
1374
1249
|
_handleAgentEvent(payload, capturedCommit) {
|
|
1375
1250
|
if (!payload) return;
|
|
1376
1251
|
|
|
@@ -1430,19 +1305,14 @@ class OpenClawClient extends EventEmitter {
|
|
|
1430
1305
|
break;
|
|
1431
1306
|
|
|
1432
1307
|
case "end": {
|
|
1433
|
-
|
|
1434
|
-
// defer). When the end event is processed IMMEDIATELY (history already
|
|
1435
|
-
// resolved), capturedCommit is undefined and live instance state is read
|
|
1436
|
-
// exactly as before — byte-for-byte unchanged. When DEFERRED, a later
|
|
1437
|
-
// run's start (or an invalidate) may have already clobbered these shared
|
|
1438
|
-
// fields, so the snapshot keeps the commit contemporaneous with its run.
|
|
1308
|
+
|
|
1439
1309
|
const committedActiveRunId = capturedCommit
|
|
1440
1310
|
? capturedCommit.activeRunId
|
|
1441
1311
|
: this._activeRunId;
|
|
1442
1312
|
const committedActiveRunSessionKey = capturedCommit
|
|
1443
1313
|
? capturedCommit.activeRunSessionKey
|
|
1444
1314
|
: this._activeRunSessionKey;
|
|
1445
|
-
|
|
1315
|
+
|
|
1446
1316
|
const fullText = capturedCommit ? capturedCommit.fullText : this._runTextBuffer;
|
|
1447
1317
|
const completedRunId = normalizeRunId(committedActiveRunId) || normalizeRunId(runId);
|
|
1448
1318
|
const completedSessionKey =
|
|
@@ -1451,22 +1321,10 @@ class OpenClawClient extends EventEmitter {
|
|
|
1451
1321
|
null;
|
|
1452
1322
|
const gapDuringRun = capturedCommit ? capturedCommit.gapDuringRun : this._gapDuringRun;
|
|
1453
1323
|
|
|
1454
|
-
// Invalidate run state before emitting terminal idle so late history polls
|
|
1455
|
-
// cannot reopen thinking for a completed run.
|
|
1456
1324
|
this._timingLedger.recordRunTerminal({
|
|
1457
1325
|
runId: completedRunId,
|
|
1458
1326
|
});
|
|
1459
|
-
|
|
1460
|
-
// commit (capturedCommit present) drains AFTER its run ended, and a
|
|
1461
|
-
// LATER run's lifecycle:start may already have become the live active
|
|
1462
|
-
// run (set _activeRunId, armed history polling, started buffering its
|
|
1463
|
-
// own text) before drain. An unconditional invalidate here would tear
|
|
1464
|
-
// down that live successor — stop its thinking-summary polling, null
|
|
1465
|
-
// its _activeRunId, and clear its _runTextBuffer — wiping state that
|
|
1466
|
-
// belongs to a run that has NOT ended. Only invalidate when this commit
|
|
1467
|
-
// IS the live active run (its own run, or a non-deferred/live commit
|
|
1468
|
-
// where the snapshot is absent). When _activeRunId is already null there
|
|
1469
|
-
// is nothing live to protect, so skipping the invalidate is a no-op.
|
|
1327
|
+
|
|
1470
1328
|
const commitIsLiveActiveRun =
|
|
1471
1329
|
normalizeRunId(this._activeRunId) === normalizeRunId(committedActiveRunId);
|
|
1472
1330
|
if (!capturedCommit || commitIsLiveActiveRun) {
|
|
@@ -1490,7 +1348,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1490
1348
|
`[openclaw] Agent run ended: ${completedRunId} (${fullText.length} chars)`
|
|
1491
1349
|
);
|
|
1492
1350
|
|
|
1493
|
-
// If there was a gap during this run, re-fetch history (step 8)
|
|
1494
1351
|
if (gapDuringRun) {
|
|
1495
1352
|
this._logger.info("[openclaw] Gap detected during run, re-fetching history");
|
|
1496
1353
|
this._fetchHistory(completedSessionKey || "main").catch((err) => {
|
|
@@ -1542,7 +1399,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1542
1399
|
"assistant_event",
|
|
1543
1400
|
);
|
|
1544
1401
|
|
|
1545
|
-
// Gateway sends accumulated text (full text so far), not deltas
|
|
1546
1402
|
if (typeof data.text === "string") {
|
|
1547
1403
|
const previousTextLength = this._runTextBuffer.length;
|
|
1548
1404
|
const gatewayReceivedAtMs = Date.now();
|
|
@@ -1602,10 +1458,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
1602
1458
|
|
|
1603
1459
|
const poll = () => {
|
|
1604
1460
|
this._pollHistoryActivity().catch((err) => {
|
|
1605
|
-
|
|
1606
|
-
// no-socket "gateway not connected" case AND the new handshake-gate
|
|
1607
|
-
// reject (handshake_pending / "gateway handshake in flight"), so the
|
|
1608
|
-
// 1008 fix does not emit spurious poll-failed warnings in its own window.
|
|
1461
|
+
|
|
1609
1462
|
const benignTransient =
|
|
1610
1463
|
!!err &&
|
|
1611
1464
|
(err.code === "handshake_pending" ||
|
|
@@ -1621,8 +1474,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1621
1474
|
});
|
|
1622
1475
|
};
|
|
1623
1476
|
|
|
1624
|
-
// Let setInterval fire the first poll one interval out so the initial
|
|
1625
|
-
// chat.history fetch doesn't contend with the first streaming chunk landing.
|
|
1626
1477
|
this._historyActivityPollTimer = setInterval(
|
|
1627
1478
|
poll,
|
|
1628
1479
|
HISTORY_ACTIVITY_POLL_INTERVAL_MS,
|
|
@@ -1771,8 +1622,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1771
1622
|
});
|
|
1772
1623
|
}
|
|
1773
1624
|
|
|
1774
|
-
// --- Internal: handshake ---
|
|
1775
|
-
|
|
1776
1625
|
_sendConnect() {
|
|
1777
1626
|
if (this._connectSent) return;
|
|
1778
1627
|
this._connectSent = true;
|
|
@@ -1788,14 +1637,12 @@ class OpenClawClient extends EventEmitter {
|
|
|
1788
1637
|
return;
|
|
1789
1638
|
}
|
|
1790
1639
|
|
|
1791
|
-
// Choose auth token: prefer cached device token, fall back to gateway token
|
|
1792
1640
|
const authToken = this._deviceToken || this._gatewayToken || undefined;
|
|
1793
1641
|
const canFallback = Boolean(this._deviceToken && this._gatewayToken);
|
|
1794
1642
|
|
|
1795
1643
|
const signedAtMs = Date.now();
|
|
1796
1644
|
const nonce = this._connectNonce || undefined;
|
|
1797
1645
|
|
|
1798
|
-
// Build device auth payload
|
|
1799
1646
|
const payload = buildDeviceAuthPayload({
|
|
1800
1647
|
deviceId: identity.deviceId,
|
|
1801
1648
|
clientId: CLIENT_ID,
|
|
@@ -1807,10 +1654,8 @@ class OpenClawClient extends EventEmitter {
|
|
|
1807
1654
|
nonce,
|
|
1808
1655
|
});
|
|
1809
1656
|
|
|
1810
|
-
// Sign with Ed25519 private key
|
|
1811
1657
|
const signature = signPayload(identity.privateKeyPem, payload);
|
|
1812
1658
|
|
|
1813
|
-
// Build connect request params
|
|
1814
1659
|
const params = {
|
|
1815
1660
|
minProtocol: MIN_PROTOCOL_VERSION,
|
|
1816
1661
|
maxProtocol: MAX_PROTOCOL_VERSION,
|
|
@@ -1835,10 +1680,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1835
1680
|
|
|
1836
1681
|
this._logger.info("[openclaw] Sending connect request...");
|
|
1837
1682
|
|
|
1838
|
-
// Capture the generation this connect is being sent for. The resolve below
|
|
1839
|
-
// must only mark THIS generation handshaken — if a disconnect+reconnect
|
|
1840
|
-
// bumped _socketGeneration before the connect resolved, this resolve is
|
|
1841
|
-
// stale and must NOT open the gate for the new in-flight socket.
|
|
1842
1683
|
const connectGeneration = this._socketGeneration;
|
|
1843
1684
|
|
|
1844
1685
|
this.request("connect", params)
|
|
@@ -1848,19 +1689,13 @@ class OpenClawClient extends EventEmitter {
|
|
|
1848
1689
|
`tick=${helloOk.policy && helloOk.policy.tickIntervalMs}ms`
|
|
1849
1690
|
);
|
|
1850
1691
|
|
|
1851
|
-
// Reset backoff on successful connect (step 7)
|
|
1852
1692
|
this._backoffMs = 1000;
|
|
1853
1693
|
|
|
1854
|
-
|
|
1855
|
-
if (helloOk.policy && typeof helloOk.policy.tickIntervalMs === "number") {
|
|
1856
|
-
this._tickIntervalMs = helloOk.policy.tickIntervalMs;
|
|
1857
|
-
}
|
|
1694
|
+
this._applyConnectPolicy(helloOk.policy);
|
|
1858
1695
|
|
|
1859
|
-
// Start tick watch (step 7)
|
|
1860
1696
|
this._lastTick = Date.now();
|
|
1861
1697
|
this._startTickWatch();
|
|
1862
1698
|
|
|
1863
|
-
// Cache device token if provided
|
|
1864
1699
|
if (helloOk.auth && helloOk.auth.deviceToken) {
|
|
1865
1700
|
this._deviceToken = helloOk.auth.deviceToken;
|
|
1866
1701
|
storeDeviceToken(
|
|
@@ -1873,12 +1708,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1873
1708
|
this._logger.info("[openclaw] Device token cached");
|
|
1874
1709
|
}
|
|
1875
1710
|
|
|
1876
|
-
// Open the handshake gate for THIS socket's generation BEFORE emitting
|
|
1877
|
-
// "connected" (so refreshUpstreamBootstrap, fired on the connected event,
|
|
1878
|
-
// sees an open gate). Guard against a stale resolve: if a reconnect
|
|
1879
|
-
// already superseded this socket, _socketGeneration has advanced past
|
|
1880
|
-
// connectGeneration and we must NOT mark the new in-flight socket
|
|
1881
|
-
// handshaken — that would re-admit the very 1008 race this fix closes.
|
|
1882
1711
|
if (connectGeneration === this._socketGeneration) {
|
|
1883
1712
|
this._handshakeGeneration = connectGeneration;
|
|
1884
1713
|
}
|
|
@@ -1889,7 +1718,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1889
1718
|
});
|
|
1890
1719
|
this.emit("status", "connected");
|
|
1891
1720
|
|
|
1892
|
-
// Post-connect: fetch agent identity (step 6) and chat history (step 9)
|
|
1893
1721
|
this._postConnect().catch((err) => {
|
|
1894
1722
|
this._logger.error(`[openclaw] Post-connect setup failed: ${err.message}`);
|
|
1895
1723
|
this.emit("error", err);
|
|
@@ -1898,7 +1726,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1898
1726
|
.catch((err) => {
|
|
1899
1727
|
this._logger.error(`[openclaw] Connect failed: ${err.message}`);
|
|
1900
1728
|
|
|
1901
|
-
// If we were using a cached device token and have a fallback, clear and retry
|
|
1902
1729
|
if (canFallback) {
|
|
1903
1730
|
this._logger.info(
|
|
1904
1731
|
"[openclaw] Clearing cached device token, will use gateway token on next connect"
|
|
@@ -1907,6 +1734,11 @@ class OpenClawClient extends EventEmitter {
|
|
|
1907
1734
|
clearDeviceToken(this._persistencePaths);
|
|
1908
1735
|
}
|
|
1909
1736
|
|
|
1737
|
+
this.emit("connectFailed", {
|
|
1738
|
+
reason: sanitizeConnectReason(err && err.message),
|
|
1739
|
+
minProtocol: MIN_PROTOCOL_VERSION,
|
|
1740
|
+
maxProtocol: MAX_PROTOCOL_VERSION,
|
|
1741
|
+
});
|
|
1910
1742
|
this.emit("error", err);
|
|
1911
1743
|
if (this._ws) {
|
|
1912
1744
|
this._ws.close(1008, "connect failed");
|
|
@@ -1914,32 +1746,22 @@ class OpenClawClient extends EventEmitter {
|
|
|
1914
1746
|
});
|
|
1915
1747
|
}
|
|
1916
1748
|
|
|
1917
|
-
// --- Internal: post-connect setup (steps 6, 9) ---
|
|
1918
|
-
|
|
1919
1749
|
async _postConnect() {
|
|
1920
|
-
|
|
1750
|
+
|
|
1921
1751
|
this.fetchAgentIdentity().catch((err) => {
|
|
1922
1752
|
this._logger.error(`[openclaw] Agent identity fetch failed: ${err.message}`);
|
|
1923
1753
|
});
|
|
1924
1754
|
|
|
1925
|
-
// Fetch chat history (step 9) — blocks agent event processing until done
|
|
1926
1755
|
try {
|
|
1927
1756
|
await this._fetchHistory("main");
|
|
1928
1757
|
} catch (err) {
|
|
1929
1758
|
this._logger.error(`[openclaw] Chat history fetch failed: ${err.message}`);
|
|
1930
1759
|
}
|
|
1931
1760
|
|
|
1932
|
-
// Mark history as resolved and drain queued events
|
|
1933
1761
|
this._historyResolved = true;
|
|
1934
1762
|
this._drainEventQueue();
|
|
1935
1763
|
}
|
|
1936
1764
|
|
|
1937
|
-
// --- Internal: chat history (step 9) ---
|
|
1938
|
-
|
|
1939
|
-
/**
|
|
1940
|
-
* Fetch chat history from the gateway.
|
|
1941
|
-
* @param {string} sessionKey
|
|
1942
|
-
*/
|
|
1943
1765
|
async _fetchHistory(sessionKey) {
|
|
1944
1766
|
const result = await this.request("chat.history", {
|
|
1945
1767
|
sessionKey,
|
|
@@ -1957,9 +1779,6 @@ class OpenClawClient extends EventEmitter {
|
|
|
1957
1779
|
return result;
|
|
1958
1780
|
}
|
|
1959
1781
|
|
|
1960
|
-
/**
|
|
1961
|
-
* Drain queued agent events that arrived before history resolved.
|
|
1962
|
-
*/
|
|
1963
1782
|
_drainEventQueue() {
|
|
1964
1783
|
const queue = this._eventQueue;
|
|
1965
1784
|
this._eventQueue = [];
|
|
@@ -1968,28 +1787,15 @@ class OpenClawClient extends EventEmitter {
|
|
|
1968
1787
|
}
|
|
1969
1788
|
}
|
|
1970
1789
|
|
|
1971
|
-
// --- Internal: reconnection (step 7) ---
|
|
1972
|
-
|
|
1973
|
-
/**
|
|
1974
|
-
* Schedule a reconnect attempt with exponential backoff.
|
|
1975
|
-
* @param {number} [delayOverride] - Override the backoff delay (e.g., for shutdown events)
|
|
1976
|
-
*/
|
|
1977
1790
|
_scheduleReconnect(delayOverride) {
|
|
1978
1791
|
if (this._stopped) return;
|
|
1979
1792
|
|
|
1980
|
-
// Capture the current base before advancing so jitter is computed from the
|
|
1981
|
-
// same epoch value that would previously have been used as the raw delay.
|
|
1982
1793
|
const base = this._backoffMs;
|
|
1983
1794
|
|
|
1984
|
-
// Advance backoff for next time (unless overridden)
|
|
1985
1795
|
if (typeof delayOverride !== "number") {
|
|
1986
1796
|
this._backoffMs = Math.min(this._backoffMs * 2, 30000);
|
|
1987
1797
|
}
|
|
1988
1798
|
|
|
1989
|
-
// Apply equal jitter to the scheduled delay so concurrent reconnect storms
|
|
1990
|
-
// don't all fire at the same instant. The stored _backoffMs base is NOT
|
|
1991
|
-
// mutated — doubling/cap/reset invariants are preserved.
|
|
1992
|
-
// delayOverride bypasses jitter (used for e.g. shutdown-driven reconnects).
|
|
1993
1799
|
const delay =
|
|
1994
1800
|
typeof delayOverride === "number"
|
|
1995
1801
|
? delayOverride
|
|
@@ -2006,26 +1812,26 @@ class OpenClawClient extends EventEmitter {
|
|
|
2006
1812
|
this._reconnectTimer = null;
|
|
2007
1813
|
this.start();
|
|
2008
1814
|
}, delay);
|
|
2009
|
-
|
|
1815
|
+
|
|
2010
1816
|
if (this._reconnectTimer.unref) {
|
|
2011
1817
|
this._reconnectTimer.unref();
|
|
2012
1818
|
}
|
|
2013
1819
|
}
|
|
2014
1820
|
|
|
2015
|
-
|
|
1821
|
+
_applyConnectPolicy(policy) {
|
|
1822
|
+
if (policy && typeof policy.tickIntervalMs === "number") {
|
|
1823
|
+
this._tickIntervalMs = Math.min(policy.tickIntervalMs, TICK_WATCH_MAX_INTERVAL_MS);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
2016
1826
|
|
|
2017
|
-
/**
|
|
2018
|
-
* Start watching for stale connections via tick timeout.
|
|
2019
|
-
* If no tick received within 2x tickIntervalMs, close and reconnect.
|
|
2020
|
-
*/
|
|
2021
1827
|
_startTickWatch() {
|
|
2022
1828
|
this._stopTickWatch();
|
|
2023
|
-
const
|
|
1829
|
+
const pollMs = Math.max(Math.floor(this._tickIntervalMs / 4), 1000);
|
|
2024
1830
|
this._tickWatchTimer = setInterval(() => {
|
|
2025
1831
|
if (this._stopped) return;
|
|
2026
1832
|
if (!this._lastTick) return;
|
|
2027
1833
|
const elapsed = Date.now() - this._lastTick;
|
|
2028
|
-
if (elapsed > this._tickIntervalMs *
|
|
1834
|
+
if (elapsed > this._tickIntervalMs * TICK_STALE_MULTIPLIER) {
|
|
2029
1835
|
this._logger.warn(
|
|
2030
1836
|
`[openclaw] Tick timeout (${elapsed}ms since last tick), closing connection`
|
|
2031
1837
|
);
|
|
@@ -2033,16 +1839,13 @@ class OpenClawClient extends EventEmitter {
|
|
|
2033
1839
|
this._ws.close(4000, "tick timeout");
|
|
2034
1840
|
}
|
|
2035
1841
|
}
|
|
2036
|
-
},
|
|
2037
|
-
|
|
1842
|
+
}, pollMs);
|
|
1843
|
+
|
|
2038
1844
|
if (this._tickWatchTimer.unref) {
|
|
2039
1845
|
this._tickWatchTimer.unref();
|
|
2040
1846
|
}
|
|
2041
1847
|
}
|
|
2042
1848
|
|
|
2043
|
-
/**
|
|
2044
|
-
* Stop the tick watch timer.
|
|
2045
|
-
*/
|
|
2046
1849
|
_stopTickWatch() {
|
|
2047
1850
|
if (this._tickWatchTimer) {
|
|
2048
1851
|
clearInterval(this._tickWatchTimer);
|
|
@@ -2050,12 +1853,9 @@ class OpenClawClient extends EventEmitter {
|
|
|
2050
1853
|
}
|
|
2051
1854
|
}
|
|
2052
1855
|
|
|
2053
|
-
// --- Internal: helpers ---
|
|
2054
|
-
|
|
2055
1856
|
_flushPendingErrors(err) {
|
|
2056
1857
|
for (const [, pending] of this._pending) {
|
|
2057
|
-
|
|
2058
|
-
// late reject after the map is cleared.
|
|
1858
|
+
|
|
2059
1859
|
if (pending.timer) {
|
|
2060
1860
|
clearTimeout(pending.timer);
|
|
2061
1861
|
pending.timer = null;
|