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
|
@@ -38,13 +38,6 @@ function resolveOpenclawClient(openclawClientOverride, runtimeConfig, logger, st
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// Shared cross-context relay singleton. OpenClaw can load the plugin in
|
|
42
|
-
// multiple isolated registration contexts (gateway startup, agent runs,
|
|
43
|
-
// tool discovery) and each call to createOcuClawRelayService produces a
|
|
44
|
-
// fresh service instance. Only one of those instances actually starts the
|
|
45
|
-
// WebSocket-bearing relay; the others need to reach the same relay so that
|
|
46
|
-
// tools registered in those contexts (e.g. render_glasses_ui) can call
|
|
47
|
-
// sendGlassesUiRender / onGlassesUiResult against the real running relay.
|
|
48
41
|
const SHARED_RELAY_SYMBOL = Symbol.for("ocuclaw.shared.relay");
|
|
49
42
|
|
|
50
43
|
function getSharedRelay() {
|
|
@@ -110,9 +103,13 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
110
103
|
token: config.relayToken,
|
|
111
104
|
sessionLimit: config.sessionLimit,
|
|
112
105
|
sonioxApiKey: config.sonioxApiKey,
|
|
113
|
-
|
|
106
|
+
cartesiaApiKey: config.cartesiaApiKey,
|
|
114
107
|
debugNoisyPolicies: config.debugNoisyPolicies,
|
|
115
108
|
externalDebugToolsEnabled: config.externalDebugToolsEnabled,
|
|
109
|
+
allowDebugUpload: config.allowDebugUpload,
|
|
110
|
+
debugUploadMaxZipBytes: config.debugUploadMaxZipBytes,
|
|
111
|
+
debugUploadCapturePreset: config.debugUploadCapturePreset,
|
|
112
|
+
debugBundleSaveDir: config.debugBundleSaveDir,
|
|
116
113
|
evenAiEnabled: config.evenAiEnabled,
|
|
117
114
|
evenAiToken: config.evenAiToken,
|
|
118
115
|
evenAiSystemPrompt: config.evenAiSystemPrompt,
|
|
@@ -145,10 +142,7 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
145
142
|
}
|
|
146
143
|
}
|
|
147
144
|
if (typeof nextRelay.onDeviceInfoResponse === "function" && pendingDeviceInfoResponseHandlers.length > 0) {
|
|
148
|
-
|
|
149
|
-
// pre-start() callers becomes a safe no-op (it indexOf-s an empty list).
|
|
150
|
-
// After flush, handler lifetime is owned by the live relay's own
|
|
151
|
-
// unsubscribe; the pending buffer is no longer the source of truth.
|
|
145
|
+
|
|
152
146
|
for (const handler of pendingDeviceInfoResponseHandlers.splice(0)) {
|
|
153
147
|
nextRelay.onDeviceInfoResponse(handler);
|
|
154
148
|
}
|
|
@@ -192,11 +186,7 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
192
186
|
}
|
|
193
187
|
|
|
194
188
|
function resolveLiveRelay() {
|
|
195
|
-
|
|
196
|
-
// the cross-context shared relay (started in a sibling plugin-load context
|
|
197
|
-
// such as the gateway main process). This matters because OpenClaw can
|
|
198
|
-
// invoke a plugin's register() in multiple isolated contexts, but only
|
|
199
|
-
// one of them holds the running WebSocket relay.
|
|
189
|
+
|
|
200
190
|
return relay || getSharedRelay();
|
|
201
191
|
}
|
|
202
192
|
return {
|
|
@@ -218,9 +208,7 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
218
208
|
}
|
|
219
209
|
liveRelay.sendGlassesUiSurfaceUpdate(params);
|
|
220
210
|
},
|
|
221
|
-
|
|
222
|
-
// cron pause/resume/tick). No-op until the relay is live; events are only
|
|
223
|
-
// recorded when the category is enabled via debug-set.
|
|
211
|
+
|
|
224
212
|
emitGlassesUiLifecycle(event, severity, data) {
|
|
225
213
|
const liveRelay = resolveLiveRelay();
|
|
226
214
|
if (liveRelay && typeof liveRelay.emitGlassesUiLifecycle === "function") {
|
|
@@ -235,7 +223,7 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
235
223
|
if (liveRelay && typeof liveRelay.onGlassesUiResult === "function") {
|
|
236
224
|
return liveRelay.onGlassesUiResult(handler);
|
|
237
225
|
}
|
|
238
|
-
|
|
226
|
+
|
|
239
227
|
pendingGlassesUiResultHandlers.push(handler);
|
|
240
228
|
return () => {
|
|
241
229
|
const idx = pendingGlassesUiResultHandlers.indexOf(handler);
|
|
@@ -260,7 +248,7 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
260
248
|
if (liveRelay && typeof liveRelay.onAppClientDisconnect === "function") {
|
|
261
249
|
return liveRelay.onAppClientDisconnect(handler);
|
|
262
250
|
}
|
|
263
|
-
|
|
251
|
+
|
|
264
252
|
pendingAppClientDisconnectHandlers.push(handler);
|
|
265
253
|
return () => {
|
|
266
254
|
const idx = pendingAppClientDisconnectHandlers.indexOf(handler);
|
|
@@ -282,7 +270,7 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
282
270
|
if (liveRelay && typeof liveRelay.onDeviceInfoResponse === "function") {
|
|
283
271
|
return liveRelay.onDeviceInfoResponse(handler);
|
|
284
272
|
}
|
|
285
|
-
|
|
273
|
+
|
|
286
274
|
pendingDeviceInfoResponseHandlers.push(handler);
|
|
287
275
|
return () => {
|
|
288
276
|
const idx = pendingDeviceInfoResponseHandlers.indexOf(handler);
|
|
@@ -303,12 +291,7 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
303
291
|
trackedThrowawayKeys: [],
|
|
304
292
|
};
|
|
305
293
|
},
|
|
306
|
-
|
|
307
|
-
// Channel-2 before_prompt_build hook — all of which can run in a sibling
|
|
308
|
-
// plugin-register context whose own `relay` is null. Resolve the live
|
|
309
|
-
// (possibly shared) relay so they reach the running instance, mirroring
|
|
310
|
-
// hasConnectedAppClient(); otherwise the distiller skips every run and the
|
|
311
|
-
// Channel-2 stop-notices never fire.
|
|
294
|
+
|
|
312
295
|
getSessionTitle(sessionKey) {
|
|
313
296
|
const liveRelay = resolveLiveRelay();
|
|
314
297
|
if (liveRelay && typeof liveRelay.getSessionTitle === "function") {
|
|
@@ -321,7 +304,7 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
321
304
|
if (liveRelay && typeof liveRelay.hasRecordedUserMessage === "function") {
|
|
322
305
|
return liveRelay.hasRecordedUserMessage(sessionKey);
|
|
323
306
|
}
|
|
324
|
-
|
|
307
|
+
|
|
325
308
|
return false;
|
|
326
309
|
},
|
|
327
310
|
isNeuralSessionNamesEnabled(sessionKey) {
|
|
@@ -352,9 +335,7 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
352
335
|
}
|
|
353
336
|
return { emoji: false, pace: false };
|
|
354
337
|
},
|
|
355
|
-
|
|
356
|
-
// register-context's shared relay) since the distiller may fire from a
|
|
357
|
-
// context whose own `relay` is null.
|
|
338
|
+
|
|
358
339
|
getSessionTitleRecord(sessionKey) {
|
|
359
340
|
const liveRelay = resolveLiveRelay();
|
|
360
341
|
if (liveRelay && typeof liveRelay.getSessionTitleRecord === "function") {
|
|
@@ -419,9 +400,7 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
419
400
|
return () => {};
|
|
420
401
|
},
|
|
421
402
|
peekSessionKey() {
|
|
422
|
-
|
|
423
|
-
// session-title accessors it must reach the live (possibly shared) relay,
|
|
424
|
-
// or an explicit rename from a sibling tool context fails no_active_session.
|
|
403
|
+
|
|
425
404
|
const liveRelay = resolveLiveRelay();
|
|
426
405
|
if (liveRelay && typeof liveRelay.peekSessionKey === "function") {
|
|
427
406
|
return liveRelay.peekSessionKey();
|
|
@@ -448,24 +427,18 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
448
427
|
return false;
|
|
449
428
|
},
|
|
450
429
|
isGlassesSendBufferOverHighWater() {
|
|
451
|
-
|
|
452
|
-
// already consults this; false = no shedding, the safe default).
|
|
430
|
+
|
|
453
431
|
const liveRelay = resolveLiveRelay();
|
|
454
432
|
if (liveRelay && typeof liveRelay.isGlassesSendBufferOverHighWater === "function") {
|
|
455
433
|
return liveRelay.isGlassesSendBufferOverHighWater();
|
|
456
434
|
}
|
|
457
435
|
return false;
|
|
458
436
|
},
|
|
459
|
-
|
|
460
|
-
// probes these at plugin register() time — BEFORE start() — and wires
|
|
461
|
-
// dispatchWake to null when the probe fails, so they must exist on the
|
|
462
|
-
// facade unconditionally and resolve the live (possibly shared) relay at
|
|
463
|
-
// call time, like every other passthrough above.
|
|
437
|
+
|
|
464
438
|
dispatchGlassesWake(params) {
|
|
465
439
|
const liveRelay = resolveLiveRelay();
|
|
466
440
|
if (!liveRelay || typeof liveRelay.dispatchGlassesWake !== "function") {
|
|
467
|
-
|
|
468
|
-
// durable outbox with a warn lifecycle — never silent after the ✓-ack.
|
|
441
|
+
|
|
469
442
|
return Promise.reject(new Error("ocuclaw relay not started"));
|
|
470
443
|
}
|
|
471
444
|
return liveRelay.dispatchGlassesWake(params);
|
|
@@ -475,8 +448,7 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
475
448
|
if (liveRelay && typeof liveRelay.isAgentTurnBusy === "function") {
|
|
476
449
|
return liveRelay.isAgentTurnBusy(sessionKey);
|
|
477
450
|
}
|
|
478
|
-
|
|
479
|
-
// path's failure handling, never suppress wakes as phantom-busy.
|
|
451
|
+
|
|
480
452
|
return false;
|
|
481
453
|
},
|
|
482
454
|
start,
|
|
@@ -10,7 +10,7 @@ export function createApprovalReplayCache(options = {}) {
|
|
|
10
10
|
const now = typeof options.now === "function" ? options.now : () => Date.now();
|
|
11
11
|
const ttlMs = normalizeNonNegativeInteger(options.ttlMs, DEFAULT_TTL_MS);
|
|
12
12
|
const maxEntries = normalizeNonNegativeInteger(options.maxEntries, DEFAULT_MAX_ENTRIES);
|
|
13
|
-
const entries = new Map();
|
|
13
|
+
const entries = new Map();
|
|
14
14
|
|
|
15
15
|
function nowMs() {
|
|
16
16
|
return normalizeNonNegativeInteger(now(), Date.now());
|
|
@@ -27,8 +27,7 @@ const transport = createRelayWorkerTransport({
|
|
|
27
27
|
postToMain(message) {
|
|
28
28
|
parentPort.postMessage(message);
|
|
29
29
|
},
|
|
30
|
-
|
|
31
|
-
// forward log lines to the supervisor, which owns the real plugin logger.
|
|
30
|
+
|
|
32
31
|
logger: {
|
|
33
32
|
info(...args) { postWorkerLog("info", args); },
|
|
34
33
|
warn(...args) { postWorkerLog("warn", args); },
|
|
@@ -167,8 +167,7 @@ export function createRelayWorkerHealthMonitor(options = {}) {
|
|
|
167
167
|
return true;
|
|
168
168
|
}
|
|
169
169
|
if (loopLagP95Ms !== null) {
|
|
170
|
-
|
|
171
|
-
// until lag drops below the (lower) recovered threshold.
|
|
170
|
+
|
|
172
171
|
const enterThreshold =
|
|
173
172
|
status === "degraded"
|
|
174
173
|
? thresholds.loopLagRecoveredP95Ms
|
|
@@ -179,14 +178,7 @@ export function createRelayWorkerHealthMonitor(options = {}) {
|
|
|
179
178
|
}
|
|
180
179
|
|
|
181
180
|
function computeStatus() {
|
|
182
|
-
|
|
183
|
-
// `main_disconnected`. On a live worker the two cannot co-occur — live
|
|
184
|
-
// message.send queue depth tops at 31 (enqueue rejects at the cap before set)
|
|
185
|
-
// and `main_disconnected` is only the pre-first-heartbeat boot state — so this
|
|
186
|
-
// order is never observably wrong. Any future reorder is conditional on BOTH a
|
|
187
|
-
// product-copy reason to prefer a "waiting" label during a disconnect AND a
|
|
188
|
-
// degraded condition becoming live-reachable, and must ship with a degraded
|
|
189
|
-
// secondary-text line + distinct badge in the phone UI.
|
|
181
|
+
|
|
190
182
|
if (isDegraded()) {
|
|
191
183
|
return "degraded";
|
|
192
184
|
}
|
|
@@ -61,6 +61,8 @@ export const DEFAULT_WORKER_RPC_LIMITS = Object.freeze({
|
|
|
61
61
|
httpRequestTimeoutMs: 60_000,
|
|
62
62
|
httpMaxBodyBytes: 65_536,
|
|
63
63
|
httpMaxResponseBytes: 262_144,
|
|
64
|
+
|
|
65
|
+
wsMaxMessageBytes: 25 * 1024 * 1024,
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
const ALLOWED_WORKER_STATUSES = new Set([
|
|
@@ -196,11 +198,14 @@ export function formatMainOperationReceived(data) {
|
|
|
196
198
|
});
|
|
197
199
|
}
|
|
198
200
|
|
|
199
|
-
export function formatSendAck(requestId, status, error, errorCode) {
|
|
201
|
+
export function formatSendAck(requestId, status, error, errorCode, data = {}) {
|
|
200
202
|
const id = normalizeRequestId(requestId);
|
|
201
203
|
const msg = { type: APP_PROTOCOL.messageSendAck, requestId: id, status };
|
|
202
204
|
if (error !== undefined) msg.error = error;
|
|
203
205
|
if (errorCode !== undefined) msg.errorCode = errorCode;
|
|
206
|
+
if (data && typeof data.runId === "string" && data.runId.trim()) {
|
|
207
|
+
msg.runId = data.runId.trim();
|
|
208
|
+
}
|
|
204
209
|
return JSON.stringify(msg);
|
|
205
210
|
}
|
|
206
211
|
|
|
@@ -109,6 +109,9 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
109
109
|
let closing = false;
|
|
110
110
|
let activeOperationBarrier = null;
|
|
111
111
|
let mainHeartbeatTimer = null;
|
|
112
|
+
let restartTimer = null;
|
|
113
|
+
let restartAttempt = 0;
|
|
114
|
+
let workerReadyWatchdog = null;
|
|
112
115
|
const clients = new Map();
|
|
113
116
|
const pendingReadinessProbeRequests = new Map();
|
|
114
117
|
const pendingAutomationStateRequests = new Map();
|
|
@@ -133,6 +136,8 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
133
136
|
resolveReady = resolve;
|
|
134
137
|
rejectReady = reject;
|
|
135
138
|
});
|
|
139
|
+
|
|
140
|
+
readyPromise.catch(() => {});
|
|
136
141
|
startPromise = readyPromise;
|
|
137
142
|
}
|
|
138
143
|
|
|
@@ -181,6 +186,57 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
181
186
|
if (typeof mainHeartbeatTimer.unref === "function") mainHeartbeatTimer.unref();
|
|
182
187
|
}
|
|
183
188
|
|
|
189
|
+
function workerRestartBackoffBaseMs() {
|
|
190
|
+
return Number.isFinite(options.workerRestartBackoffBaseMs)
|
|
191
|
+
? Math.max(0, Math.floor(options.workerRestartBackoffBaseMs))
|
|
192
|
+
: 250;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function workerRestartBackoffMaxMs() {
|
|
196
|
+
return Number.isFinite(options.workerRestartBackoffMaxMs)
|
|
197
|
+
? Math.max(0, Math.floor(options.workerRestartBackoffMaxMs))
|
|
198
|
+
: 30000;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function workerReadyWatchdogMs() {
|
|
202
|
+
|
|
203
|
+
return Number.isFinite(options.workerReadyWatchdogMs) && options.workerReadyWatchdogMs > 0
|
|
204
|
+
? Math.floor(options.workerReadyWatchdogMs)
|
|
205
|
+
: 10000;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function clearWorkerReadyWatchdog() {
|
|
209
|
+
if (workerReadyWatchdog) {
|
|
210
|
+
clearTimeout(workerReadyWatchdog);
|
|
211
|
+
workerReadyWatchdog = null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function scheduleWorkerRestart() {
|
|
216
|
+
if (restartTimer || closing) return;
|
|
217
|
+
const base = Math.min(
|
|
218
|
+
workerRestartBackoffMaxMs(),
|
|
219
|
+
workerRestartBackoffBaseMs() * 2 ** Math.min(restartAttempt, 7),
|
|
220
|
+
);
|
|
221
|
+
const delay = base / 2 + Math.random() * (base / 2);
|
|
222
|
+
restartAttempt += 1;
|
|
223
|
+
if (typeof options.emitDebug === "function") {
|
|
224
|
+
options.emitDebug(
|
|
225
|
+
"relay.worker.health",
|
|
226
|
+
"worker_restart_backoff",
|
|
227
|
+
"warn",
|
|
228
|
+
null,
|
|
229
|
+
() => ({ attempt: restartAttempt, delayMs: Math.round(delay) }),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
restartTimer = setTimeout(() => {
|
|
233
|
+
restartTimer = null;
|
|
234
|
+
if (closing) return;
|
|
235
|
+
startWorker();
|
|
236
|
+
}, delay);
|
|
237
|
+
if (typeof restartTimer.unref === "function") restartTimer.unref();
|
|
238
|
+
}
|
|
239
|
+
|
|
184
240
|
function buildManifest() {
|
|
185
241
|
workerEpoch += 1;
|
|
186
242
|
return {
|
|
@@ -307,9 +363,7 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
307
363
|
|
|
308
364
|
function postMainFrame(target, frame, clientId) {
|
|
309
365
|
if (typeof frame !== "string") return;
|
|
310
|
-
|
|
311
|
-
// so deriving type from a single parseFrame avoids a second parse on the hot
|
|
312
|
-
// pages/status path below (and is neutral — still one parse — on other frames).
|
|
366
|
+
|
|
313
367
|
const parsed = parseFrame(frame);
|
|
314
368
|
const type = parsed && typeof parsed.type === "string" ? parsed.type : null;
|
|
315
369
|
const message = {
|
|
@@ -329,16 +383,7 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
329
383
|
: { statusRevision: revision };
|
|
330
384
|
}
|
|
331
385
|
}
|
|
332
|
-
|
|
333
|
-
// preserve send-ordering in the worker path, but it used to divert EVERY
|
|
334
|
-
// broadcast/broadcastApp frame for the whole send→ack window — stalling
|
|
335
|
-
// unrelated live broadcasts (streaming deltas / activity / typing / status
|
|
336
|
-
// from a prior, still-streaming turn) until flush. Only hold a broadcast
|
|
337
|
-
// that is causally tied to the in-flight barrier.requestId; let everything
|
|
338
|
-
// else pass straight through. The send's own operation-received and ack are
|
|
339
|
-
// unicast (never diverted) and the op-received is posted synchronously
|
|
340
|
-
// ahead of the async ack, so "operation-received precedes that op's ack"
|
|
341
|
-
// holds independent of this barrier.
|
|
386
|
+
|
|
342
387
|
if (
|
|
343
388
|
activeOperationBarrier &&
|
|
344
389
|
(target === "broadcast" || target === "broadcastApp") &&
|
|
@@ -506,6 +551,8 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
506
551
|
function handleMessage(message) {
|
|
507
552
|
if (!message || typeof message !== "object") return;
|
|
508
553
|
if (message.kind === "worker.ready") {
|
|
554
|
+
restartAttempt = 0;
|
|
555
|
+
clearWorkerReadyWatchdog();
|
|
509
556
|
addressValue = message.address || null;
|
|
510
557
|
wssEvents.emit("listening");
|
|
511
558
|
if (resolveReady) {
|
|
@@ -517,11 +564,14 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
517
564
|
}
|
|
518
565
|
if (message.kind === "worker.error") {
|
|
519
566
|
logger.warn(`[relay-worker] ${message.message || "worker error"}`);
|
|
567
|
+
|
|
568
|
+
if (rejectReady && worker && typeof worker.terminate === "function") {
|
|
569
|
+
worker.terminate();
|
|
570
|
+
}
|
|
520
571
|
return;
|
|
521
572
|
}
|
|
522
573
|
if (message.kind === "worker.log") {
|
|
523
|
-
|
|
524
|
-
// log file, so the worker forwards its log lines here instead.
|
|
574
|
+
|
|
525
575
|
const level =
|
|
526
576
|
message.level === "warn" || message.level === "error" || message.level === "debug"
|
|
527
577
|
? message.level
|
|
@@ -649,11 +699,7 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
649
699
|
disconnectedEntry.clientKind === "app" &&
|
|
650
700
|
typeof options.onAppClientDisconnect === "function"
|
|
651
701
|
) {
|
|
652
|
-
|
|
653
|
-
// app client disconnects (round-2: a same-session duplicate keeps it
|
|
654
|
-
// live; round-6: per-session so A drains independently of B). The
|
|
655
|
-
// disconnecting client is already removed from `clients`, so the
|
|
656
|
-
// excludeClientId is belt-and-suspenders.
|
|
702
|
+
|
|
657
703
|
const drainSessionKey =
|
|
658
704
|
typeof disconnectedEntry.sessionKey === "string" && disconnectedEntry.sessionKey
|
|
659
705
|
? disconnectedEntry.sessionKey
|
|
@@ -705,10 +751,7 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
705
751
|
const pending = requestId ? pendingReadinessProbeRequests.get(requestId) : null;
|
|
706
752
|
if (!pending || pending.targetClientId !== message.clientId) return;
|
|
707
753
|
pendingReadinessProbeRequests.delete(requestId);
|
|
708
|
-
|
|
709
|
-
// Refresh the registry entry with it: the app's standalone snapshot
|
|
710
|
-
// republication can race a reconnecting socket and get lost, leaving the
|
|
711
|
-
// hello-frozen key here until the next app boot (quirk 8's second layer).
|
|
754
|
+
|
|
712
755
|
if (ack && ack.ok !== false && typeof ack.activeSessionKey === "string" && ack.activeSessionKey) {
|
|
713
756
|
const ackEntry = clients.get(message.clientId);
|
|
714
757
|
if (ackEntry && ackEntry.readinessSnapshot) {
|
|
@@ -778,8 +821,7 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
778
821
|
return;
|
|
779
822
|
}
|
|
780
823
|
if (message.kind === "worker.backpressure") {
|
|
781
|
-
|
|
782
|
-
// worker transport; relay-core latches them for the glasses paint shed.
|
|
824
|
+
|
|
783
825
|
if (typeof options.onWorkerBackpressure === "function") {
|
|
784
826
|
options.onWorkerBackpressure(message);
|
|
785
827
|
}
|
|
@@ -810,20 +852,21 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
810
852
|
nextWorker.on("message", handleMessage);
|
|
811
853
|
nextWorker.on("error", (err) => {
|
|
812
854
|
logger.error(`[relay-worker] worker error: ${err && err.message ? err.message : err}`);
|
|
813
|
-
|
|
855
|
+
|
|
814
856
|
wssEvents.emit("error", err);
|
|
815
857
|
});
|
|
816
858
|
const startedWorker = nextWorker;
|
|
817
859
|
nextWorker.on("exit", (code) => {
|
|
818
|
-
|
|
819
|
-
|
|
860
|
+
|
|
861
|
+
const wasActive = worker === startedWorker;
|
|
862
|
+
if (!wasActive) return;
|
|
863
|
+
worker = null;
|
|
820
864
|
stopMainHeartbeat();
|
|
821
|
-
|
|
865
|
+
clearWorkerReadyWatchdog();
|
|
866
|
+
if (!closing) {
|
|
822
867
|
const err = new Error(`relay worker exited with code ${code}`);
|
|
823
868
|
logger.warn(`[relay-worker] ${err.message}`);
|
|
824
|
-
|
|
825
|
-
resolveReady = null;
|
|
826
|
-
rejectReady = null;
|
|
869
|
+
const wasPreReady = Boolean(rejectReady);
|
|
827
870
|
addressValue = null;
|
|
828
871
|
clients.clear();
|
|
829
872
|
pendingReadinessProbeRequests.clear();
|
|
@@ -831,18 +874,43 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
831
874
|
if (wssEvents.listenerCount("error") > 0) {
|
|
832
875
|
wssEvents.emit("error", err);
|
|
833
876
|
}
|
|
834
|
-
|
|
835
|
-
|
|
877
|
+
|
|
878
|
+
if (!wasPreReady) {
|
|
879
|
+
resetReadyPromise();
|
|
880
|
+
}
|
|
881
|
+
scheduleWorkerRestart();
|
|
836
882
|
}
|
|
837
883
|
});
|
|
838
884
|
const manifest = buildManifest();
|
|
839
885
|
postToWorker(manifest);
|
|
840
886
|
startMainHeartbeat(manifest.workerEpoch);
|
|
887
|
+
|
|
888
|
+
clearWorkerReadyWatchdog();
|
|
889
|
+
workerReadyWatchdog = setTimeout(() => {
|
|
890
|
+
workerReadyWatchdog = null;
|
|
891
|
+
if (closing || worker !== startedWorker) return;
|
|
892
|
+
logger.warn(
|
|
893
|
+
`[relay-worker] worker did not report ready within ${workerReadyWatchdogMs()}ms; terminating`,
|
|
894
|
+
);
|
|
895
|
+
if (typeof startedWorker.terminate === "function") startedWorker.terminate();
|
|
896
|
+
}, workerReadyWatchdogMs());
|
|
897
|
+
if (typeof workerReadyWatchdog.unref === "function") workerReadyWatchdog.unref();
|
|
841
898
|
return nextWorker;
|
|
842
899
|
}
|
|
843
900
|
|
|
844
901
|
function close() {
|
|
845
902
|
closing = true;
|
|
903
|
+
|
|
904
|
+
if (rejectReady) {
|
|
905
|
+
rejectReady(new Error("relay worker supervisor closed before ready"));
|
|
906
|
+
resolveReady = null;
|
|
907
|
+
rejectReady = null;
|
|
908
|
+
}
|
|
909
|
+
if (restartTimer) {
|
|
910
|
+
clearTimeout(restartTimer);
|
|
911
|
+
restartTimer = null;
|
|
912
|
+
}
|
|
913
|
+
clearWorkerReadyWatchdog();
|
|
846
914
|
stopMainHeartbeat();
|
|
847
915
|
if (!worker) {
|
|
848
916
|
startPromise = null;
|
|
@@ -944,12 +1012,6 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
944
1012
|
});
|
|
945
1013
|
}
|
|
946
1014
|
|
|
947
|
-
// The WebUI's reconnect hello embeds its readiness snapshot WITHOUT
|
|
948
|
-
// emittedAtMs (toProtocolHelloReadinessSnapshotJson omits it), but the
|
|
949
|
-
// automation-state gate requires a finite emittedAtMs to count the client
|
|
950
|
-
// as published. Stamp ingest time so a reconnect hello counts as a
|
|
951
|
-
// publication — otherwise every relay restart leaves `inspect state`
|
|
952
|
-
// returning snapshot_unavailable until the sim/app client is cycled.
|
|
953
1015
|
function normalizeIngestedReadinessSnapshot(snapshot) {
|
|
954
1016
|
if (!snapshot || typeof snapshot !== "object") {
|
|
955
1017
|
return null;
|