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
|
@@ -1,45 +1,17 @@
|
|
|
1
|
-
// Per-session surface store for the glasses-UI tool. Extracted from
|
|
2
|
-
// glasses-ui-tool.ts (Phase 1) so this stateful logic can be unit-tested in
|
|
3
|
-
// isolation and so the tool file thins toward a schema+wiring layer (spec
|
|
4
|
-
// §Lifecycle detail — Surface/stack lifecycle store).
|
|
5
|
-
//
|
|
6
|
-
// Phase 2 evolves the Phase-1 createPendingRenderMap IN PLACE into
|
|
7
|
-
// createSurfaceStore (SINGLE store — there is never a second store alongside
|
|
8
|
-
// it). The decoupled lifecycle means a surface's lifetime is independent of
|
|
9
|
-
// any one tool call: resolve() settles the in-flight pending call but the
|
|
10
|
-
// surface entry PERSISTS (keyed by surfaceId), and a new register() does NOT
|
|
11
|
-
// preempt the previous surface. The legacy createPendingRenderMap name survives
|
|
12
|
-
// only as an alias (bottom of this file) for the Phase 1 import path.
|
|
13
|
-
|
|
14
|
-
// A terminal outcome ends the surface (teardown / exit). A nonterminal outcome
|
|
15
|
-
// (selected/back) keeps the surface alive and hands the turn to the agent.
|
|
16
1
|
const TERMINAL_RESULTS = new Set(["dismissed", "timeout", "glasses_disconnected", "preempted", "recipe_failed"]);
|
|
17
2
|
|
|
18
3
|
export function isTerminalOutcome(outcome) {
|
|
19
4
|
return !!(outcome && typeof outcome.result === "string" && TERMINAL_RESULTS.has(outcome.result));
|
|
20
5
|
}
|
|
21
6
|
|
|
22
|
-
// Provenance enum for GlassEvents and wake envelopes (roadmap 6b, §2.6).
|
|
23
|
-
// Only "gesture" is an enabled wake origin at launch; the others reserve the
|
|
24
|
-
// event-to-wake categories (schedule/threshold/system) so enabling them later
|
|
25
|
-
// is a policy change, not a schema migration.
|
|
26
7
|
export const GLASS_EVENT_ORIGINS = ["gesture", "schedule", "threshold", "system"];
|
|
27
8
|
|
|
28
|
-
// Session keys reach the store in two forms: the canonical agent ctx form
|
|
29
|
-
// ("agent:<id>:<key>", what registerTool factories and agent_end hooks see)
|
|
30
|
-
// and the stripped relay form ("<key>", what relay-side callbacks like the
|
|
31
|
-
// glasses-disconnect drain carry). The suffix is the identity; the prefix is
|
|
32
|
-
// routing. Normalizing at the store boundary makes every caller agnostic —
|
|
33
|
-
// the same lesson as the 6f busy-tracker key fix (drift #2, `0cc639b9`).
|
|
34
9
|
export function normalizeGlassesSessionKey(key) {
|
|
35
10
|
return typeof key === "string" ? key.replace(/^agent:[^:]+:/, "") : key;
|
|
36
11
|
}
|
|
37
12
|
|
|
38
13
|
export function createSurfaceStore(deps = {}) {
|
|
39
|
-
|
|
40
|
-
// traces can tell WHICH plugin-load context's store handled a render/reap
|
|
41
|
-
// (drift #3, 2026-06-12: a queued wake run's collect landed in a sibling
|
|
42
|
-
// context's empty store and minted a phantom root — invisible without this).
|
|
14
|
+
|
|
43
15
|
const storeId =
|
|
44
16
|
typeof deps.storeId === "string" && deps.storeId
|
|
45
17
|
? deps.storeId
|
|
@@ -58,21 +30,13 @@ export function createSurfaceStore(deps = {}) {
|
|
|
58
30
|
typeof deps.mintUuid === "function"
|
|
59
31
|
? deps.mintUuid
|
|
60
32
|
: () => `su-${Math.random().toString(36).slice(2, 10)}${Math.random().toString(36).slice(2, 6)}`;
|
|
61
|
-
|
|
62
|
-
// state, queuedEvent, exitLatched, uuid, events, queueMode }
|
|
33
|
+
|
|
63
34
|
const bySurface = new Map();
|
|
64
|
-
const stackBySession = new Map();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Reap paths (drain/exit/popBack) hand undelivered events to a per-session
|
|
70
|
-
// dead-letter instead of destroying them: a ✓-acked tap must NEVER silently
|
|
71
|
-
// disappear (the 2026-06-12 panel's fatal). Wake/voicemail legs (6f/7b)
|
|
72
|
-
// drain the dead-letter; this wave only guarantees survival.
|
|
73
|
-
const DEAD_LETTER_EVENT_CAP = 32; // per session, FIFO eviction
|
|
74
|
-
const SURFACE_EVENT_LOG_CAP = 32; // per surface, FIFO eviction
|
|
75
|
-
const deadLetterBySession = new Map(); // sessionKey -> [{surfaceUuid, surfaceId, events, reason, reapedAtMs}]
|
|
35
|
+
const stackBySession = new Map();
|
|
36
|
+
|
|
37
|
+
const DEAD_LETTER_EVENT_CAP = 32;
|
|
38
|
+
const SURFACE_EVENT_LOG_CAP = 32;
|
|
39
|
+
const deadLetterBySession = new Map();
|
|
76
40
|
let eventSeq = 0;
|
|
77
41
|
|
|
78
42
|
function deadLetterFor(sessionKey) {
|
|
@@ -82,8 +46,7 @@ export function createSurfaceStore(deps = {}) {
|
|
|
82
46
|
}
|
|
83
47
|
|
|
84
48
|
function deadLetterEntryEvents(sessionKey, surfaceId, entry, reason) {
|
|
85
|
-
|
|
86
|
-
// nonterminals beneath it are no longer intent to deliver.
|
|
49
|
+
|
|
87
50
|
if (!entry || entry.exitLatched || !entry.events || entry.events.length === 0) return;
|
|
88
51
|
const eventIds = entry.events.map((e) => e.eventId);
|
|
89
52
|
const list = deadLetterFor(sessionKey);
|
|
@@ -93,13 +56,11 @@ export function createSurfaceStore(deps = {}) {
|
|
|
93
56
|
events: entry.events,
|
|
94
57
|
reason,
|
|
95
58
|
reapedAtMs: now(),
|
|
96
|
-
|
|
97
|
-
// events so the 6f/7b consumers (wake/voicemail) can frame age honestly.
|
|
59
|
+
|
|
98
60
|
staleAfterMs: Number.isFinite(entry.staleAfterMs) ? entry.staleAfterMs : null,
|
|
99
61
|
});
|
|
100
62
|
entry.events = [];
|
|
101
|
-
|
|
102
|
-
// dead-letter must be traceable (the contract is "never silently lost").
|
|
63
|
+
|
|
103
64
|
emitLifecycle("dead_letter_appended", "debug", {
|
|
104
65
|
sessionKey,
|
|
105
66
|
surfaceId,
|
|
@@ -128,32 +89,19 @@ export function createSurfaceStore(deps = {}) {
|
|
|
128
89
|
return s;
|
|
129
90
|
}
|
|
130
91
|
|
|
131
|
-
// A fresh entry starts un-latched with an empty queue. A `prior` entry (an
|
|
132
|
-
// in-place content swap of the SAME surfaceId — i.e. `replace`) carries its
|
|
133
|
-
// latched exit / last-wins queued event forward so reattach semantics are
|
|
134
|
-
// MOVE-INDEPENDENT: a terminal latched (or a nonterminal queued) during
|
|
135
|
-
// `visible_awaiting_agent` is honored on the agent's NEXT render whether that
|
|
136
|
-
// render is patch, replace (the schema DEFAULT) or push — never silently
|
|
137
|
-
// dropped by the rebuild. (Without this carry-forward, a `replace` rebuild
|
|
138
|
-
// would reset exitLatched=false / queuedEvent=null and onReattached would
|
|
139
|
-
// return "reattached" instead of "discarded_for_exit", failing to tear down.)
|
|
140
92
|
function makeEntry(sessionKey, kind, prior) {
|
|
141
93
|
return {
|
|
142
94
|
sessionKey, kind: kind || null, pending: null, lastContent: null,
|
|
143
95
|
state: "visible_pending",
|
|
144
96
|
queuedEvent: prior ? prior.queuedEvent : null,
|
|
145
97
|
exitLatched: prior ? !!prior.exitLatched : false,
|
|
146
|
-
|
|
147
|
-
// (same surfaceId, same surface as far as the wearer is concerned).
|
|
98
|
+
|
|
148
99
|
uuid: prior ? prior.uuid : mintUuid(),
|
|
149
100
|
events: prior ? prior.events : [],
|
|
150
101
|
queueMode: prior && prior.queueMode === "log" ? "log" : "latest",
|
|
151
|
-
|
|
152
|
-
// it from each render's meta, so the latest render's spec governs.
|
|
102
|
+
|
|
153
103
|
staleAfterMs: null,
|
|
154
|
-
|
|
155
|
-
// awaitingAgentResponse resets on a fresh entry — a new render opens a new
|
|
156
|
-
// window so markerFor returns "listening" anyway.
|
|
104
|
+
|
|
157
105
|
title: prior ? prior.title : null,
|
|
158
106
|
awaitingAgentResponse: false,
|
|
159
107
|
};
|
|
@@ -164,10 +112,7 @@ export function createSurfaceStore(deps = {}) {
|
|
|
164
112
|
return new Promise((resolve) => {
|
|
165
113
|
const existing = bySurface.get(surfaceId);
|
|
166
114
|
if (existing) {
|
|
167
|
-
|
|
168
|
-
// keep the entry. No preempt of a different surface. Restore the
|
|
169
|
-
// pending state so a re-render returns to visible_pending before
|
|
170
|
-
// onReattached flushes any queued event / latched exit.
|
|
115
|
+
|
|
171
116
|
existing.pending = resolve;
|
|
172
117
|
if (meta && meta.kind) existing.kind = meta.kind;
|
|
173
118
|
if (meta && (meta.queueMode === "log" || meta.queueMode === "latest")) {
|
|
@@ -175,7 +120,7 @@ export function createSurfaceStore(deps = {}) {
|
|
|
175
120
|
}
|
|
176
121
|
existing.staleAfterMs = meta && Number.isFinite(meta.staleAfterMs) ? meta.staleAfterMs : null;
|
|
177
122
|
if (meta && typeof meta.title === "string") existing.title = meta.title;
|
|
178
|
-
existing.awaitingAgentResponse = false;
|
|
123
|
+
existing.awaitingAgentResponse = false;
|
|
179
124
|
existing.sessionKey = sessionKey;
|
|
180
125
|
existing.state = "visible_pending";
|
|
181
126
|
return;
|
|
@@ -191,9 +136,6 @@ export function createSurfaceStore(deps = {}) {
|
|
|
191
136
|
});
|
|
192
137
|
}
|
|
193
138
|
|
|
194
|
-
// Every delivered outcome carries the surface's durable identity (roadmap
|
|
195
|
-
// 6b): agents and the 6f wake/voicemail consumers key on surfaceUuid, never
|
|
196
|
-
// on the plugin-internal surfaceId. Additive only — caller fields win.
|
|
197
139
|
function decorateDelivery(entry, outcome) {
|
|
198
140
|
if (!outcome || typeof outcome !== "object" || Array.isArray(outcome)) return outcome;
|
|
199
141
|
if (outcome.surfaceUuid !== undefined) return outcome;
|
|
@@ -204,14 +146,13 @@ export function createSurfaceStore(deps = {}) {
|
|
|
204
146
|
const entry = bySurface.get(surfaceId);
|
|
205
147
|
if (!entry || !entry.pending) return false;
|
|
206
148
|
const pending = entry.pending;
|
|
207
|
-
entry.pending = null;
|
|
149
|
+
entry.pending = null;
|
|
208
150
|
if (isTerminalOutcome(outcome)) {
|
|
209
151
|
entry.state = "exiting";
|
|
210
152
|
entry.awaitingAgentResponse = false;
|
|
211
153
|
} else {
|
|
212
154
|
entry.state = "visible_awaiting_agent";
|
|
213
|
-
|
|
214
|
-
// a window_expired resolve is the timer, not the wearer → parked.
|
|
155
|
+
|
|
215
156
|
entry.awaitingAgentResponse = !!(outcome && outcome.result !== "window_expired");
|
|
216
157
|
}
|
|
217
158
|
pending(decorateDelivery(entry, outcome));
|
|
@@ -227,12 +168,6 @@ export function createSurfaceStore(deps = {}) {
|
|
|
227
168
|
return !!(entry && entry.pending);
|
|
228
169
|
}
|
|
229
170
|
|
|
230
|
-
// Carried over from createPendingRenderMap (Phase 1). drainSession/drainAll
|
|
231
|
-
// resolve any in-flight pending call; Task 11 extends them to also stop
|
|
232
|
-
// crons and clear the per-session stack. They are the only resolve path that
|
|
233
|
-
// also DELETES surfaces (terminal teardown), unlike resolve() above.
|
|
234
|
-
// Drains are plugin/host-initiated by definition — stamp origin "system"
|
|
235
|
-
// (never a wearer gesture) unless the caller already typed it.
|
|
236
171
|
function decorateDrainOutcome(entry, outcome) {
|
|
237
172
|
if (!outcome || typeof outcome !== "object" || Array.isArray(outcome)) return outcome;
|
|
238
173
|
return decorateDelivery(entry, {
|
|
@@ -255,11 +190,6 @@ export function createSurfaceStore(deps = {}) {
|
|
|
255
190
|
return n;
|
|
256
191
|
}
|
|
257
192
|
|
|
258
|
-
// Settle still-pending calls for a session WITHOUT tearing anything down:
|
|
259
|
-
// entries, parked events, crons and the stack all persist (surfaces are
|
|
260
|
-
// DESIGNED to outlive runs — resolve ≠ teardown). This is the run-teardown
|
|
261
|
-
// safety net's correct scope: no leaked pending call / orphan tool_use,
|
|
262
|
-
// and no destruction of parked wearer intent (drift #3, 2026-06-12).
|
|
263
193
|
function settlePending(rawSessionKey, outcome) {
|
|
264
194
|
const sessionKey = normalizeGlassesSessionKey(rawSessionKey);
|
|
265
195
|
let n = 0;
|
|
@@ -267,8 +197,7 @@ export function createSurfaceStore(deps = {}) {
|
|
|
267
197
|
if (entry.sessionKey !== sessionKey || !entry.pending) continue;
|
|
268
198
|
const pending = entry.pending;
|
|
269
199
|
entry.pending = null;
|
|
270
|
-
|
|
271
|
-
// exactly like the post-window_expired parked state.
|
|
200
|
+
|
|
272
201
|
entry.state = "visible_awaiting_agent";
|
|
273
202
|
pending(decorateDrainOutcome(entry, outcome));
|
|
274
203
|
n += 1;
|
|
@@ -297,26 +226,21 @@ export function createSurfaceStore(deps = {}) {
|
|
|
297
226
|
const entry = bySurface.get(surfaceId);
|
|
298
227
|
if (!entry) return false;
|
|
299
228
|
if (isTerminalOutcome(event)) {
|
|
300
|
-
entry.exitLatched = true;
|
|
229
|
+
entry.exitLatched = true;
|
|
301
230
|
entry.queuedEvent = event;
|
|
302
231
|
return { ok: true, eventId: ++eventSeq, surfaceUuid: entry.uuid, kind: "terminal_latch" };
|
|
303
232
|
}
|
|
304
233
|
if (entry.exitLatched) {
|
|
305
|
-
|
|
306
|
-
// stamped origin:"system" at tool.ts:945) is NOT the wearer ending the
|
|
307
|
-
// surface — revive it. Clear the latch and fall through to the normal append
|
|
308
|
-
// (truthy receipt → ✓-ack → wake; the next render takes onReattached's deliver
|
|
309
|
-
// path, not discarded_for_exit). A GESTURE-origin latch keeps dropping (the
|
|
310
|
-
// wearer really ended it). Mirrors onReattached's forgiveness (surfaces.ts:373-377).
|
|
234
|
+
|
|
311
235
|
const latched = entry.queuedEvent;
|
|
312
236
|
const latchedOrigin = latched && typeof latched.origin === "string" ? latched.origin : "gesture";
|
|
313
237
|
if (latchedOrigin === "gesture") {
|
|
314
|
-
|
|
238
|
+
|
|
315
239
|
return false;
|
|
316
240
|
}
|
|
317
241
|
entry.exitLatched = false;
|
|
318
242
|
entry.queuedEvent = null;
|
|
319
|
-
|
|
243
|
+
|
|
320
244
|
}
|
|
321
245
|
const record = {
|
|
322
246
|
eventId: ++eventSeq,
|
|
@@ -331,7 +255,7 @@ export function createSurfaceStore(deps = {}) {
|
|
|
331
255
|
if (entry.events.length > SURFACE_EVENT_LOG_CAP) {
|
|
332
256
|
entry.events.splice(0, entry.events.length - SURFACE_EVENT_LOG_CAP);
|
|
333
257
|
}
|
|
334
|
-
entry.queuedEvent = event;
|
|
258
|
+
entry.queuedEvent = event;
|
|
335
259
|
return { ok: true, eventId: record.eventId, surfaceUuid: entry.uuid };
|
|
336
260
|
}
|
|
337
261
|
|
|
@@ -340,10 +264,6 @@ export function createSurfaceStore(deps = {}) {
|
|
|
340
264
|
return entry ? entry.title : null;
|
|
341
265
|
}
|
|
342
266
|
|
|
343
|
-
// Plugin-authoritative presence marker (roadmap 7a). Pure function of two
|
|
344
|
-
// store facts: is a listen window open, and is anything in flight (a queued
|
|
345
|
-
// tap OR the agent responding to a just-delivered gesture). Glyphs are
|
|
346
|
-
// mapped client-side; the plugin only emits these enum strings.
|
|
347
267
|
function markerFor(surfaceId) {
|
|
348
268
|
const entry = bySurface.get(surfaceId);
|
|
349
269
|
if (!entry) return null;
|
|
@@ -352,8 +272,6 @@ export function createSurfaceStore(deps = {}) {
|
|
|
352
272
|
return "parked";
|
|
353
273
|
}
|
|
354
274
|
|
|
355
|
-
// 7a: called by the agent_end hook to flip inflight→parked for the session's
|
|
356
|
-
// surfaces when the agent turn ends without a new render (silent end).
|
|
357
275
|
function clearAwaitingResponse(rawSessionKey) {
|
|
358
276
|
const sessionKey = normalizeGlassesSessionKey(rawSessionKey);
|
|
359
277
|
for (const [, entry] of bySurface) {
|
|
@@ -361,8 +279,6 @@ export function createSurfaceStore(deps = {}) {
|
|
|
361
279
|
}
|
|
362
280
|
}
|
|
363
281
|
|
|
364
|
-
// 7a: compose the breadcrumb from the session's stack of titles, separator " › ".
|
|
365
|
-
// Returns null when the session has no stack, is empty, or has no titled surfaces.
|
|
366
282
|
function breadcrumbFor(rawSessionKey) {
|
|
367
283
|
const s = stackBySession.get(normalizeGlassesSessionKey(rawSessionKey));
|
|
368
284
|
if (!s || s.length === 0) return null;
|
|
@@ -382,9 +298,6 @@ export function createSurfaceStore(deps = {}) {
|
|
|
382
298
|
return entry ? [...entry.events] : [];
|
|
383
299
|
}
|
|
384
300
|
|
|
385
|
-
// Read-time delivery reducer (6a): "latest" collapses to the newest
|
|
386
|
-
// nonterminal (the pre-6a delivery semantics); "log" exposes the ordered
|
|
387
|
-
// sequence for consumers that drain it whole (wake/voicemail, v2 collect).
|
|
388
301
|
function reduceForDelivery(surfaceId) {
|
|
389
302
|
const entry = bySurface.get(surfaceId);
|
|
390
303
|
if (!entry) return null;
|
|
@@ -417,17 +330,7 @@ export function createSurfaceStore(deps = {}) {
|
|
|
417
330
|
if (!entry) return "no_surface";
|
|
418
331
|
let staleLatchDropped = false;
|
|
419
332
|
if (entry.exitLatched) {
|
|
420
|
-
|
|
421
|
-
// was by construction queued under a PRIOR call (queueEvent only
|
|
422
|
-
// latches while no call is pending). A SYSTEM-origin terminal — a
|
|
423
|
-
// cron's own maxDuration timeout / recipe_failed summary — is plugin
|
|
424
|
-
// noise relative to the agent's fresh render and must NOT consume the
|
|
425
|
-
// fresh call's window (HW 2026-06-11: a 7-min-stale cron `timeout`
|
|
426
|
-
// ate a fresh 10-min call in 2.2s). Drop it and proceed; the fresh
|
|
427
|
-
// render recycles the slot, which is the teardown the latch wanted.
|
|
428
|
-
// GESTURE terminals (and legacy un-stamped ones — honoring a teardown
|
|
429
|
-
// is the safe default) keep the discard-for-exit contract: the wearer
|
|
430
|
-
// ended the surface and the fresh render must not resurrect it.
|
|
333
|
+
|
|
431
334
|
const latched = entry.queuedEvent;
|
|
432
335
|
const latchedOrigin =
|
|
433
336
|
latched && typeof latched.origin === "string" ? latched.origin : "gesture";
|
|
@@ -436,11 +339,7 @@ export function createSurfaceStore(deps = {}) {
|
|
|
436
339
|
entry.queuedEvent = null;
|
|
437
340
|
staleLatchDropped = true;
|
|
438
341
|
} else {
|
|
439
|
-
|
|
440
|
-
// resolve the freshly-established pending call (the re-attach render's
|
|
441
|
-
// promise) with the latched terminal outcome so the tool call settles
|
|
442
|
-
// instead of hanging, then tear down. Parked nonterminals beneath the
|
|
443
|
-
// latch are dropped (the wearer ended the surface), not dead-lettered.
|
|
342
|
+
|
|
444
343
|
entry.state = "exiting";
|
|
445
344
|
const terminal = entry.queuedEvent || { result: "dismissed" };
|
|
446
345
|
entry.queuedEvent = null;
|
|
@@ -454,12 +353,7 @@ export function createSurfaceStore(deps = {}) {
|
|
|
454
353
|
}
|
|
455
354
|
}
|
|
456
355
|
entry.state = "reattached";
|
|
457
|
-
|
|
458
|
-
// resolves the re-attached call and the log drains. 6b: the delivery is
|
|
459
|
-
// decorated with the log record's identity + provenance + age, and — when
|
|
460
|
-
// the render declared staleAfterMs — an annotate-only stale flag (the tap
|
|
461
|
-
// is still delivered; degrading a stale actuating tap to a re-confirm is
|
|
462
|
-
// the agent's job, taught in the skill).
|
|
356
|
+
|
|
463
357
|
const newest = entry.events.length ? entry.events[entry.events.length - 1] : null;
|
|
464
358
|
let delivered = null;
|
|
465
359
|
if (newest) {
|
|
@@ -485,7 +379,7 @@ export function createSurfaceStore(deps = {}) {
|
|
|
485
379
|
const pending = entry.pending;
|
|
486
380
|
entry.pending = null;
|
|
487
381
|
entry.state = "visible_awaiting_agent";
|
|
488
|
-
entry.awaitingAgentResponse = true;
|
|
382
|
+
entry.awaitingAgentResponse = true;
|
|
489
383
|
pending(delivered);
|
|
490
384
|
}
|
|
491
385
|
return staleLatchDropped ? "reattached_stale_latch_dropped" : "reattached";
|
|
@@ -506,16 +400,11 @@ export function createSurfaceStore(deps = {}) {
|
|
|
506
400
|
return entry ? entry.sessionKey : null;
|
|
507
401
|
}
|
|
508
402
|
|
|
509
|
-
// Derive the target surfaceId from the session's current top. The agent
|
|
510
|
-
// never supplies a surfaceId (spec §Core model — the plugin owns them,
|
|
511
|
-
// keyed by session + stack). Returns { mode, surfaceId } so the caller can
|
|
512
|
-
// render against the bound id.
|
|
513
403
|
function applyRender(rawSessionKey, params) {
|
|
514
404
|
const sessionKey = normalizeGlassesSessionKey(rawSessionKey);
|
|
515
405
|
const stack = stackFor(sessionKey);
|
|
516
406
|
const top = stack[stack.length - 1] || null;
|
|
517
|
-
|
|
518
|
-
// regardless of the requested update (there is nothing to patch/push onto).
|
|
407
|
+
|
|
519
408
|
if (!top) {
|
|
520
409
|
const id = mintSurfaceId();
|
|
521
410
|
stack.push(id);
|
|
@@ -526,9 +415,7 @@ export function createSurfaceStore(deps = {}) {
|
|
|
526
415
|
: params && params.update === "push" ? "push"
|
|
527
416
|
: "replace";
|
|
528
417
|
if (update === "patch") {
|
|
529
|
-
|
|
530
|
-
// caller sends a field patch; the body content slot is updated by the
|
|
531
|
-
// render send, not here.
|
|
418
|
+
|
|
532
419
|
const entry = bySurface.get(top);
|
|
533
420
|
if (entry && params && params.kind) entry.kind = params.kind;
|
|
534
421
|
if (entry) entry.state = "visible_pending";
|
|
@@ -541,22 +428,9 @@ export function createSurfaceStore(deps = {}) {
|
|
|
541
428
|
bySurface.set(id, makeEntry(sessionKey, params && params.kind));
|
|
542
429
|
return { mode: "push", surfaceId: id };
|
|
543
430
|
}
|
|
544
|
-
|
|
545
|
-
// new back-target, depth unchanged). The entry is reset to a fresh
|
|
546
|
-
// pending state; the cron for this slot is reset for new content. CARRY
|
|
547
|
-
// FORWARD the prior entry's latched exit / queued event (same surfaceId)
|
|
548
|
-
// so a terminal/nonterminal recorded during visible_awaiting_agent is
|
|
549
|
-
// still honored by the subsequent onReattached — reattach semantics must
|
|
550
|
-
// be move-independent (replace, the schema DEFAULT, must behave like
|
|
551
|
-
// patch here, not silently drop a latched exit).
|
|
431
|
+
|
|
552
432
|
const priorTop = bySurface.get(top);
|
|
553
|
-
|
|
554
|
-
// a real outcome. A non-silent stop fires the cron's onResolve with a
|
|
555
|
-
// synthesized `preempted`, which (with no pending call at this instant)
|
|
556
|
-
// latches a bogus exit onto the prior entry — carried into makeEntry below,
|
|
557
|
-
// it makes the very render we are applying discard-for-exit on re-attach
|
|
558
|
-
// ("fresh render instantly dismissed", B7 — the real B3 contamination
|
|
559
|
-
// mechanism, found 2026-06-11).
|
|
433
|
+
|
|
560
434
|
stopCron(top, { silent: true });
|
|
561
435
|
bySurface.set(top, makeEntry(sessionKey, params && params.kind, priorTop));
|
|
562
436
|
return { mode: "replace", surfaceId: top };
|
|
@@ -603,10 +477,4 @@ export function createSurfaceStore(deps = {}) {
|
|
|
603
477
|
};
|
|
604
478
|
}
|
|
605
479
|
|
|
606
|
-
// Backwards-compatible alias. Phase 1 extracted createPendingRenderMap; Phase 2
|
|
607
|
-
// evolved it into createSurfaceStore (single store — see the SINGLE-STORE
|
|
608
|
-
// EVOLUTION note in the plan). The old name survives only as an alias so the
|
|
609
|
-
// Phase 1 import path (glasses-ui-tool re-export) keeps resolving. There is
|
|
610
|
-
// never a separate createPendingRenderMap *instance* — both names construct the
|
|
611
|
-
// one evolved store.
|
|
612
480
|
export const createPendingRenderMap = createSurfaceStore;
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// Template engine for glasses UI refresh recipes.
|
|
2
|
-
// Pure functions. No I/O. Parses {{path | filter:arg | filter:arg}} expressions,
|
|
3
|
-
// resolves paths against a data object (plus an optional previous-tick object
|
|
4
|
-
// exposed under {{previous.*}}), and applies a small filter set sized exactly
|
|
5
|
-
// to the spec's stated use cases.
|
|
6
|
-
|
|
7
1
|
const KNOWN_FILTERS = new Set([
|
|
8
2
|
"trim",
|
|
9
3
|
"lower",
|
|
@@ -18,11 +12,10 @@ const KNOWN_FILTERS = new Set([
|
|
|
18
12
|
"plus",
|
|
19
13
|
]);
|
|
20
14
|
|
|
21
|
-
// Filters that require a numeric arg.
|
|
22
15
|
const NUMERIC_ARG_FILTERS = new Set(["round", "truncate"]);
|
|
23
|
-
|
|
16
|
+
|
|
24
17
|
const STRING_ARG_FILTERS = new Set(["default", "prefix"]);
|
|
25
|
-
|
|
18
|
+
|
|
26
19
|
const PATH_ARG_FILTERS = new Set(["minus", "plus"]);
|
|
27
20
|
|
|
28
21
|
function resolvePath(path, data, previous) {
|
|
@@ -51,10 +44,7 @@ function resolvePath(path, data, previous) {
|
|
|
51
44
|
}
|
|
52
45
|
|
|
53
46
|
function parseFilter(filterSrc) {
|
|
54
|
-
|
|
55
|
-
// - a quoted string: `default:"x"` or `prefix:"+"`
|
|
56
|
-
// - a number: `round:2` or `truncate:18`
|
|
57
|
-
// - a path: `minus:previous.value`
|
|
47
|
+
|
|
58
48
|
const colonIdx = filterSrc.indexOf(":");
|
|
59
49
|
const name = (colonIdx === -1 ? filterSrc : filterSrc.slice(0, colonIdx)).trim();
|
|
60
50
|
const rawArg = colonIdx === -1 ? "" : filterSrc.slice(colonIdx + 1).trim();
|
|
@@ -81,7 +71,7 @@ function parseFilter(filterSrc) {
|
|
|
81
71
|
}
|
|
82
72
|
return { ok: true, name, arg: rawArg };
|
|
83
73
|
}
|
|
84
|
-
|
|
74
|
+
|
|
85
75
|
return { ok: true, name, arg: null };
|
|
86
76
|
}
|
|
87
77
|
|
|
@@ -113,10 +103,7 @@ function applyFilter(value, filter, data, previous) {
|
|
|
113
103
|
case "default":
|
|
114
104
|
return value === undefined || value === null || value === "" ? filter.arg : value;
|
|
115
105
|
case "prefix": {
|
|
116
|
-
|
|
117
|
-
// still prepend on positive values; here we treat "truthy" as the
|
|
118
|
-
// value's actual truthiness — agent uses prefix when they want the
|
|
119
|
-
// literal prepended on a non-empty value.
|
|
106
|
+
|
|
120
107
|
if (value === undefined || value === null || value === "") return value;
|
|
121
108
|
if (typeof value === "number" && value === 0) return value;
|
|
122
109
|
return `${filter.arg}${value}`;
|
|
@@ -139,8 +126,6 @@ function stringify(value) {
|
|
|
139
126
|
return String(value);
|
|
140
127
|
}
|
|
141
128
|
|
|
142
|
-
// PUBLIC ----
|
|
143
|
-
|
|
144
129
|
export function substituteTemplate(template, data, opts) {
|
|
145
130
|
if (typeof template !== "string") return "";
|
|
146
131
|
const previous = opts && opts.previous ? opts.previous : null;
|
|
@@ -150,7 +135,7 @@ export function substituteTemplate(template, data, opts) {
|
|
|
150
135
|
let value = resolvePath(path, data, previous);
|
|
151
136
|
for (let i = 1; i < parts.length; i += 1) {
|
|
152
137
|
const f = parseFilter(parts[i]);
|
|
153
|
-
if (!f.ok) return stringify(value);
|
|
138
|
+
if (!f.ok) return stringify(value);
|
|
154
139
|
value = applyFilter(value, f, data, previous);
|
|
155
140
|
}
|
|
156
141
|
return stringify(value);
|
|
@@ -161,7 +146,7 @@ export function validateTemplate(template) {
|
|
|
161
146
|
if (typeof template !== "string") {
|
|
162
147
|
return { ok: false, code: "refresh_template_invalid", message: "template must be a string" };
|
|
163
148
|
}
|
|
164
|
-
|
|
149
|
+
|
|
165
150
|
const openCount = (template.match(/\{\{/g) || []).length;
|
|
166
151
|
const closeCount = (template.match(/\}\}/g) || []).length;
|
|
167
152
|
if (openCount !== closeCount) {
|
|
@@ -7,10 +7,10 @@ test("description now carries the follow-up + back/selected usage rules", () =>
|
|
|
7
7
|
assert.match(d, /text_surface/);
|
|
8
8
|
assert.match(d, /list_surface/);
|
|
9
9
|
assert.match(d, /list_with_details_surface/);
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
assert.match(d, /NEXT output|next output/);
|
|
12
12
|
assert.match(d, /back/i);
|
|
13
13
|
assert.match(d, /selected/i);
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
assert.match(d, /glasses-ui/);
|
|
16
16
|
});
|