ocuclaw 1.3.2 → 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 +93 -0
- 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 +657 -271
- package/dist/runtime/relay-service.js +40 -36
- 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 +109 -39
- package/dist/runtime/relay-worker-transport.js +157 -15
- 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 +58 -63
- 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 +22 -34
- package/dist/tools/glasses-ui-recipes.js +92 -101
- package/dist/tools/glasses-ui-surfaces.js +295 -100
- 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 +475 -331
- package/dist/tools/glasses-ui-voicemail.js +242 -0
- package/dist/tools/glasses-ui-wake.js +195 -0
- 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/skills/glasses-ui/SKILL.md +19 -3
- 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();
|
|
@@ -447,6 +426,31 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
447
426
|
}
|
|
448
427
|
return false;
|
|
449
428
|
},
|
|
429
|
+
isGlassesSendBufferOverHighWater() {
|
|
430
|
+
|
|
431
|
+
const liveRelay = resolveLiveRelay();
|
|
432
|
+
if (liveRelay && typeof liveRelay.isGlassesSendBufferOverHighWater === "function") {
|
|
433
|
+
return liveRelay.isGlassesSendBufferOverHighWater();
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
dispatchGlassesWake(params) {
|
|
439
|
+
const liveRelay = resolveLiveRelay();
|
|
440
|
+
if (!liveRelay || typeof liveRelay.dispatchGlassesWake !== "function") {
|
|
441
|
+
|
|
442
|
+
return Promise.reject(new Error("ocuclaw relay not started"));
|
|
443
|
+
}
|
|
444
|
+
return liveRelay.dispatchGlassesWake(params);
|
|
445
|
+
},
|
|
446
|
+
isAgentTurnBusy(sessionKey) {
|
|
447
|
+
const liveRelay = resolveLiveRelay();
|
|
448
|
+
if (liveRelay && typeof liveRelay.isAgentTurnBusy === "function") {
|
|
449
|
+
return liveRelay.isAgentTurnBusy(sessionKey);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return false;
|
|
453
|
+
},
|
|
450
454
|
start,
|
|
451
455
|
stop,
|
|
452
456
|
};
|
|
@@ -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) {
|
|
@@ -777,6 +820,13 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
777
820
|
postMainFrame("unicast", frame, pending.requesterClientId);
|
|
778
821
|
return;
|
|
779
822
|
}
|
|
823
|
+
if (message.kind === "worker.backpressure") {
|
|
824
|
+
|
|
825
|
+
if (typeof options.onWorkerBackpressure === "function") {
|
|
826
|
+
options.onWorkerBackpressure(message);
|
|
827
|
+
}
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
780
830
|
if (message.kind === "debug" && typeof options.emitDebug === "function") {
|
|
781
831
|
options.emitDebug(
|
|
782
832
|
message.category || "relay.worker.health",
|
|
@@ -802,20 +852,21 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
802
852
|
nextWorker.on("message", handleMessage);
|
|
803
853
|
nextWorker.on("error", (err) => {
|
|
804
854
|
logger.error(`[relay-worker] worker error: ${err && err.message ? err.message : err}`);
|
|
805
|
-
|
|
855
|
+
|
|
806
856
|
wssEvents.emit("error", err);
|
|
807
857
|
});
|
|
808
858
|
const startedWorker = nextWorker;
|
|
809
859
|
nextWorker.on("exit", (code) => {
|
|
810
|
-
|
|
811
|
-
|
|
860
|
+
|
|
861
|
+
const wasActive = worker === startedWorker;
|
|
862
|
+
if (!wasActive) return;
|
|
863
|
+
worker = null;
|
|
812
864
|
stopMainHeartbeat();
|
|
813
|
-
|
|
865
|
+
clearWorkerReadyWatchdog();
|
|
866
|
+
if (!closing) {
|
|
814
867
|
const err = new Error(`relay worker exited with code ${code}`);
|
|
815
868
|
logger.warn(`[relay-worker] ${err.message}`);
|
|
816
|
-
|
|
817
|
-
resolveReady = null;
|
|
818
|
-
rejectReady = null;
|
|
869
|
+
const wasPreReady = Boolean(rejectReady);
|
|
819
870
|
addressValue = null;
|
|
820
871
|
clients.clear();
|
|
821
872
|
pendingReadinessProbeRequests.clear();
|
|
@@ -823,18 +874,43 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
823
874
|
if (wssEvents.listenerCount("error") > 0) {
|
|
824
875
|
wssEvents.emit("error", err);
|
|
825
876
|
}
|
|
826
|
-
|
|
827
|
-
|
|
877
|
+
|
|
878
|
+
if (!wasPreReady) {
|
|
879
|
+
resetReadyPromise();
|
|
880
|
+
}
|
|
881
|
+
scheduleWorkerRestart();
|
|
828
882
|
}
|
|
829
883
|
});
|
|
830
884
|
const manifest = buildManifest();
|
|
831
885
|
postToWorker(manifest);
|
|
832
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();
|
|
833
898
|
return nextWorker;
|
|
834
899
|
}
|
|
835
900
|
|
|
836
901
|
function close() {
|
|
837
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();
|
|
838
914
|
stopMainHeartbeat();
|
|
839
915
|
if (!worker) {
|
|
840
916
|
startPromise = null;
|
|
@@ -936,12 +1012,6 @@ export function createRelayWorkerSupervisor(options = {}) {
|
|
|
936
1012
|
});
|
|
937
1013
|
}
|
|
938
1014
|
|
|
939
|
-
// The WebUI's reconnect hello embeds its readiness snapshot WITHOUT
|
|
940
|
-
// emittedAtMs (toProtocolHelloReadinessSnapshotJson omits it), but the
|
|
941
|
-
// automation-state gate requires a finite emittedAtMs to count the client
|
|
942
|
-
// as published. Stamp ingest time so a reconnect hello counts as a
|
|
943
|
-
// publication — otherwise every relay restart leaves `inspect state`
|
|
944
|
-
// returning snapshot_unavailable until the sim/app client is cycled.
|
|
945
1015
|
function normalizeIngestedReadinessSnapshot(snapshot) {
|
|
946
1016
|
if (!snapshot || typeof snapshot !== "object") {
|
|
947
1017
|
return null;
|