ocuclaw 1.3.1 → 1.3.3
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/dist/config/runtime-config-session-title-model.test.js +22 -0
- package/dist/config/runtime-config.js +7 -1
- package/dist/domain/glasses-display-system-prompt.js +52 -0
- package/dist/domain/glasses-display-system-prompt.test.js +44 -0
- package/dist/domain/glasses-ui-system-prompt.js +6 -22
- package/dist/domain/glasses-ui-system-prompt.test.js +13 -0
- package/dist/domain/prompt-channel-fragments.js +32 -0
- package/dist/domain/prompt-channel-fragments.test.js +70 -0
- package/dist/gateway/gateway-timing-ledger.js +15 -3
- package/dist/gateway/openclaw-client.js +80 -3
- package/dist/index.js +22 -0
- package/dist/runtime/channel-two-hook.js +36 -0
- package/dist/runtime/container-env.js +41 -0
- package/dist/runtime/display-toggle-states.js +98 -0
- package/dist/runtime/glasses-backpressure-latch.js +115 -0
- package/dist/runtime/register-session-title-distiller.js +100 -0
- package/dist/runtime/relay-core.js +284 -33
- package/dist/runtime/relay-service.js +152 -13
- package/dist/runtime/relay-worker-entry.js +26 -0
- package/dist/runtime/relay-worker-supervisor.js +51 -2
- package/dist/runtime/relay-worker-transport.js +51 -1
- package/dist/runtime/session-service.js +136 -12
- package/dist/runtime/session-title-distiller-budget.js +36 -0
- package/dist/runtime/session-title-distiller-helpers.js +130 -0
- package/dist/runtime/session-title-distiller.js +354 -0
- package/dist/runtime/session-title-record.js +21 -0
- package/dist/runtime/stable-prompt-snapshot.js +119 -0
- package/dist/tools/glasses-ui-cron.js +59 -3
- package/dist/tools/glasses-ui-paint-floor.js +33 -4
- package/dist/tools/glasses-ui-surfaces.js +369 -35
- package/dist/tools/glasses-ui-tool-description.test.js +16 -0
- package/dist/tools/glasses-ui-tool.js +662 -80
- package/dist/tools/glasses-ui-voicemail.js +299 -0
- package/dist/tools/glasses-ui-wake.js +262 -0
- package/dist/tools/session-title-tool.js +14 -76
- package/dist/tools/session-title-tool.test.js +53 -0
- package/dist/version.js +2 -2
- package/openclaw.plugin.json +9 -0
- package/package.json +4 -3
- package/skills/glasses-ui/SKILL.md +26 -3
|
@@ -85,7 +85,7 @@ export function createGlassesUiCronEngine(deps) {
|
|
|
85
85
|
return Object.assign({}, outcome, extra);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
function resolveAndClean(state, extra) {
|
|
88
|
+
function resolveAndClean(state, extra, opts) {
|
|
89
89
|
if (state.resolved) return;
|
|
90
90
|
state.resolved = true;
|
|
91
91
|
if (state.nextTickTimer) clearTimeoutFn(state.nextTickTimer);
|
|
@@ -93,6 +93,12 @@ export function createGlassesUiCronEngine(deps) {
|
|
|
93
93
|
state.nextTickTimer = null;
|
|
94
94
|
state.maxDurationTimer = null;
|
|
95
95
|
active.delete(state.surfaceId);
|
|
96
|
+
// silent: tear down timers/state WITHOUT firing onResolve. Used when the
|
|
97
|
+
// cron slot is being RECYCLED (a replace render swapping the surface's
|
|
98
|
+
// content in place) — a synthesized outcome here would reach
|
|
99
|
+
// surfaceStore.resolve with no pending call and LATCH a bogus exit that
|
|
100
|
+
// discards the very render doing the replacing (B7, found 2026-06-11).
|
|
101
|
+
if (opts && opts.silent === true) return;
|
|
96
102
|
try {
|
|
97
103
|
state.onResolve(makeOutcome(state, extra));
|
|
98
104
|
} catch (_) {
|
|
@@ -305,6 +311,12 @@ export function createGlassesUiCronEngine(deps) {
|
|
|
305
311
|
resolved: false,
|
|
306
312
|
nextTickTimer: null,
|
|
307
313
|
maxDurationTimer: null,
|
|
314
|
+
// Pause-aware duration accounting (roadmap 6e): the cap measures
|
|
315
|
+
// ACTIVE time only. remainingMs is banked on pause and re-armed on
|
|
316
|
+
// resume; armedAtMs is monotonic (never wall-clock — the 2026-06-01
|
|
317
|
+
// freeze came from mixing clocks).
|
|
318
|
+
maxDurationRemainingMs: params.refresh.maxDurationMs,
|
|
319
|
+
maxDurationArmedAtMs: null,
|
|
308
320
|
isSmokeTest: false,
|
|
309
321
|
lastTickAt: null,
|
|
310
322
|
generationToken: 0,
|
|
@@ -313,7 +325,14 @@ export function createGlassesUiCronEngine(deps) {
|
|
|
313
325
|
};
|
|
314
326
|
active.set(state.surfaceId, state);
|
|
315
327
|
// Arm the duration cap.
|
|
328
|
+
state.maxDurationArmedAtMs = monotonicNowMs();
|
|
316
329
|
state.maxDurationTimer = setTimeoutFn(() => {
|
|
330
|
+
// Cron-death observability (Wave-1 e2e leg-4 gap): the cap firing was
|
|
331
|
+
// invisible — bounded tick-silence watches were the only evidence.
|
|
332
|
+
emitLifecycle("cron_max_duration_reached", "debug", {
|
|
333
|
+
surfaceId: state.surfaceId,
|
|
334
|
+
sessionKey: state.sessionKey,
|
|
335
|
+
});
|
|
317
336
|
resolveAndClean(state, { result: "timeout" });
|
|
318
337
|
}, params.refresh.maxDurationMs);
|
|
319
338
|
// start() is sync; runSmokeTest is async. Catch any rejection so it
|
|
@@ -326,10 +345,10 @@ export function createGlassesUiCronEngine(deps) {
|
|
|
326
345
|
});
|
|
327
346
|
});
|
|
328
347
|
},
|
|
329
|
-
stop(surfaceId, outcome) {
|
|
348
|
+
stop(surfaceId, outcome, opts) {
|
|
330
349
|
const state = active.get(surfaceId);
|
|
331
350
|
if (!state) return false;
|
|
332
|
-
resolveAndClean(state, outcome || { result: "preempted" });
|
|
351
|
+
resolveAndClean(state, outcome || { result: "preempted" }, opts);
|
|
333
352
|
return true;
|
|
334
353
|
},
|
|
335
354
|
stopAllForSession(sessionKey, outcome) {
|
|
@@ -374,6 +393,20 @@ export function createGlassesUiCronEngine(deps) {
|
|
|
374
393
|
clearTimeoutFn(state.nextTickTimer);
|
|
375
394
|
state.nextTickTimer = null;
|
|
376
395
|
}
|
|
396
|
+
// Pause-aware duration cap (roadmap 6e): stop the wall-clock burn and
|
|
397
|
+
// bank the remaining ACTIVE budget. Pre-6e the cap kept running while
|
|
398
|
+
// the parent hid under a pushed child — hardware-observed 2026-06-11
|
|
399
|
+
// as cron_resume {found:false} → Back landed on a frozen list. Guarded
|
|
400
|
+
// on the timer handle so a double-pause cannot double-deduct.
|
|
401
|
+
if (state.maxDurationTimer) {
|
|
402
|
+
clearTimeoutFn(state.maxDurationTimer);
|
|
403
|
+
state.maxDurationTimer = null;
|
|
404
|
+
state.maxDurationRemainingMs = Math.max(
|
|
405
|
+
0,
|
|
406
|
+
state.maxDurationRemainingMs - (monotonicNowMs() - state.maxDurationArmedAtMs),
|
|
407
|
+
);
|
|
408
|
+
state.maxDurationArmedAtMs = null;
|
|
409
|
+
}
|
|
377
410
|
state.paused = true;
|
|
378
411
|
// Bump so an in-flight tick (started before pause) is discarded when it
|
|
379
412
|
// resolves — it must not patch the now-hidden parent screen.
|
|
@@ -392,6 +425,29 @@ export function createGlassesUiCronEngine(deps) {
|
|
|
392
425
|
});
|
|
393
426
|
return false;
|
|
394
427
|
}
|
|
428
|
+
// Re-arm the duration cap with the banked ACTIVE remainder (roadmap
|
|
429
|
+
// 6e). An exhausted budget resolves terminal timeout here instead of
|
|
430
|
+
// re-arming a dead cron — honest death beats a zombie surface.
|
|
431
|
+
if (!state.maxDurationTimer) {
|
|
432
|
+
if (state.maxDurationRemainingMs <= 0) {
|
|
433
|
+
emitLifecycle("cron_resume", "debug", {
|
|
434
|
+
surfaceId,
|
|
435
|
+
found: true,
|
|
436
|
+
resolved: false,
|
|
437
|
+
branch: "max_duration_exhausted",
|
|
438
|
+
});
|
|
439
|
+
resolveAndClean(state, { result: "timeout" });
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
state.maxDurationArmedAtMs = monotonicNowMs();
|
|
443
|
+
state.maxDurationTimer = setTimeoutFn(() => {
|
|
444
|
+
emitLifecycle("cron_max_duration_reached", "debug", {
|
|
445
|
+
surfaceId: state.surfaceId,
|
|
446
|
+
sessionKey: state.sessionKey,
|
|
447
|
+
});
|
|
448
|
+
resolveAndClean(state, { result: "timeout" });
|
|
449
|
+
}, state.maxDurationRemainingMs);
|
|
450
|
+
}
|
|
395
451
|
state.paused = false;
|
|
396
452
|
if (state.nextTickTimer) {
|
|
397
453
|
clearTimeoutFn(state.nextTickTimer);
|
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Governs ALL plugin->glass sends (initial RebuildPageContainer render and
|
|
4
4
|
// every surface_update patch). Collapses bursts to last-write-wins per field
|
|
5
|
-
// and emits at most one frame per paintFloorMs
|
|
6
|
-
//
|
|
5
|
+
// and emits at most one frame per paintFloorMs, with a leading-edge send + a
|
|
6
|
+
// trailing send carrying the final merged patch.
|
|
7
7
|
//
|
|
8
8
|
// There is NO glass-side paint-ack: the only backpressure signal is
|
|
9
9
|
// relay/BLE transport-side (see the isUnderBackpressure shed in Task 13).
|
|
10
10
|
// Local fake-list textContainerUpgrade scroll-swaps are client-side and never
|
|
11
11
|
// reach this coalescer.
|
|
12
|
+
//
|
|
13
|
+
// 250 is the unconditionally hardware-proven floor. Spike D approved lowering
|
|
14
|
+
// to 150 ONLY once the backpressure shed has a live signal; the shed's
|
|
15
|
+
// isGlassesSendBufferOverHighWater query is implemented nowhere yet, so the
|
|
16
|
+
// shed is inert and 150 would run without its safety condition. Restore 150
|
|
17
|
+
// when the relay-service bridge lands and is validated on hardware
|
|
18
|
+
// (roadmap step 4, docs/superpowers/plans/2026-06-10-glasses-ui-state-reset-and-roadmap.md).
|
|
12
19
|
|
|
13
|
-
export const DEFAULT_PAINT_FLOOR_MS =
|
|
20
|
+
export const DEFAULT_PAINT_FLOOR_MS = 250;
|
|
14
21
|
|
|
15
22
|
export function createPaintFloorCoalescer(deps) {
|
|
16
23
|
const paintFloorMs = Number.isFinite(deps.paintFloorMs) ? deps.paintFloorMs : DEFAULT_PAINT_FLOOR_MS;
|
|
@@ -28,6 +35,14 @@ export function createPaintFloorCoalescer(deps) {
|
|
|
28
35
|
return !!(p && p.__render === true);
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
function markerOf(p) {
|
|
39
|
+
if (!p) return undefined;
|
|
40
|
+
return p.__marker !== undefined ? p.__marker : p.marker;
|
|
41
|
+
}
|
|
42
|
+
function isMarkerOnly(p) {
|
|
43
|
+
return !!p && !isRenderSentinel(p) && Object.keys(p).length === 1 && p.marker !== undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
function mergePatch(base, incoming) {
|
|
32
47
|
// A render sentinel ({ __render, __depth, __spec }, Task 13) and a field
|
|
33
48
|
// patch ({ body }/{ items }/{ title }) must NEVER shallow-merge — a merged
|
|
@@ -36,8 +51,22 @@ export function createPaintFloorCoalescer(deps) {
|
|
|
36
51
|
// earlier one wholesale (a render replaces a queued field patch; a field
|
|
37
52
|
// patch after a queued render replaces that render). Same-kind writes
|
|
38
53
|
// merge last-write-wins per field as before.
|
|
54
|
+
//
|
|
55
|
+
// Marker-only field patch must NEVER drop a queued render (7a): merge the
|
|
56
|
+
// marker onto the render sentinel instead of superseding it wholesale.
|
|
57
|
+
if (isMarkerOnly(incoming) && isRenderSentinel(base)) {
|
|
58
|
+
return { ...base, __marker: incoming.marker };
|
|
59
|
+
}
|
|
39
60
|
if (isRenderSentinel(base) !== isRenderSentinel(incoming)) {
|
|
40
|
-
|
|
61
|
+
// Different write kinds: later supersedes wholesale (a render replaces a
|
|
62
|
+
// queued field patch; a content field patch replaces a queued render).
|
|
63
|
+
// Marker is sticky — carry it forward if the survivor didn't set one.
|
|
64
|
+
const next = incoming && typeof incoming === "object" ? { ...incoming } : {};
|
|
65
|
+
if (markerOf(next) === undefined) {
|
|
66
|
+
const m = markerOf(base);
|
|
67
|
+
if (m !== undefined) { if (isRenderSentinel(next)) next.__marker = m; else next.marker = m; }
|
|
68
|
+
}
|
|
69
|
+
return next;
|
|
41
70
|
}
|
|
42
71
|
const merged = base ? { ...base } : {};
|
|
43
72
|
if (incoming && typeof incoming === "object") {
|