ocuclaw 0.1.0 → 1.3.0

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.
Files changed (59) hide show
  1. package/README.md +63 -8
  2. package/dist/config/runtime-config.js +81 -3
  3. package/dist/domain/activity-status-adapter.js +138 -605
  4. package/dist/domain/activity-status-arbiter.js +109 -0
  5. package/dist/domain/activity-status-labels.js +906 -0
  6. package/dist/domain/code-span-regions.js +103 -0
  7. package/dist/domain/conversation-state.js +14 -1
  8. package/dist/domain/debug-store.js +41 -184
  9. package/dist/domain/glasses-ui-content-summary.js +62 -0
  10. package/dist/domain/glasses-ui-system-prompt.js +28 -0
  11. package/dist/domain/message-emoji-allowlist.js +16 -0
  12. package/dist/domain/message-emoji-filter.js +33 -55
  13. package/dist/domain/neural-emoji-reactor-system-prompt.js +43 -0
  14. package/dist/domain/neural-emoji-reactor-tag-config.js +56 -0
  15. package/dist/domain/neural-pace-modulator-system-prompt.js +32 -0
  16. package/dist/domain/neural-pace-modulator-tag-config.js +51 -0
  17. package/dist/domain/tagged-span-parser.js +121 -0
  18. package/dist/domain/tagged-span-strip.js +38 -0
  19. package/dist/even-ai/even-ai-endpoint.js +91 -0
  20. package/dist/even-ai/even-ai-run-waiter.js +14 -0
  21. package/dist/even-ai/even-ai-settings-store.js +14 -0
  22. package/dist/gateway/gateway-bridge.js +14 -2
  23. package/dist/gateway/gateway-timing-ledger.js +457 -0
  24. package/dist/gateway/openclaw-client.js +462 -38
  25. package/dist/index.js +28 -1
  26. package/dist/runtime/downstream-handler.js +909 -68
  27. package/dist/runtime/downstream-server.js +1004 -512
  28. package/dist/runtime/ocuclaw-settings-store.js +74 -31
  29. package/dist/runtime/plugin-update-service.js +216 -0
  30. package/dist/runtime/protocol-adapter.js +9 -0
  31. package/dist/runtime/provider-usage-select.js +168 -0
  32. package/dist/runtime/relay-client-nudge-controller.js +553 -0
  33. package/dist/runtime/relay-core.js +1357 -210
  34. package/dist/runtime/relay-health-monitor.js +172 -0
  35. package/dist/runtime/relay-operation-registry.js +263 -0
  36. package/dist/runtime/relay-service.js +201 -1
  37. package/dist/runtime/relay-worker-approval-replay-cache.js +68 -0
  38. package/dist/runtime/relay-worker-entry.js +32 -0
  39. package/dist/runtime/relay-worker-health.js +272 -0
  40. package/dist/runtime/relay-worker-protocol.js +285 -0
  41. package/dist/runtime/relay-worker-queue.js +202 -0
  42. package/dist/runtime/relay-worker-supervisor.js +1081 -0
  43. package/dist/runtime/relay-worker-transport.js +1051 -0
  44. package/dist/runtime/session-context-service.js +189 -0
  45. package/dist/runtime/session-service.js +656 -38
  46. package/dist/runtime/upstream-runtime.js +1167 -60
  47. package/dist/tools/device-info-tool.js +242 -0
  48. package/dist/tools/glasses-ui-cron.js +427 -0
  49. package/dist/tools/glasses-ui-descriptors.js +261 -0
  50. package/dist/tools/glasses-ui-limits.js +21 -0
  51. package/dist/tools/glasses-ui-paint-floor.js +99 -0
  52. package/dist/tools/glasses-ui-recipes.js +746 -0
  53. package/dist/tools/glasses-ui-surfaces.js +278 -0
  54. package/dist/tools/glasses-ui-template.js +182 -0
  55. package/dist/tools/glasses-ui-tool.js +1147 -0
  56. package/dist/tools/session-title-tool.js +209 -0
  57. package/dist/version.js +2 -0
  58. package/openclaw.plugin.json +163 -15
  59. package/package.json +12 -4
@@ -0,0 +1,103 @@
1
+ // Markdown code-region scanner for the tagged-span grammar passes.
2
+ // Leaf module by design: no imports (CJS emitter import-cycle hazard).
3
+ //
4
+ // Computes [start, end) regions of text covered by markdown code so that
5
+ // tagged-span grammar (<emoji:…>, <dwell>, <skim>) quoted inside backticks
6
+ // or fenced blocks is treated as literal text instead of live tags.
7
+ //
8
+ // Streaming-partial semantics: an unclosed fence runs to end-of-text
9
+ // (CommonMark), while an unclosed inline backtick stays literal until its
10
+ // closer arrives — the cumulative re-parse on the next flush re-interprets,
11
+ // matching how the markdown pass already behaves on partial text.
12
+
13
+ /**
14
+ * @param {string} text
15
+ * @returns {Array<[number, number]>} sorted, non-overlapping [start, end) regions
16
+ */
17
+ export function computeCodeSpanRegions(text) {
18
+ if (typeof text !== "string" || !text) return [];
19
+ const n = text.length;
20
+ const regions = [];
21
+
22
+ // --- Pass 1: fenced code blocks (line-oriented, ``` or ~~~) ---
23
+ const FENCE_OPEN_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
24
+ const FENCE_CLOSE_RE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
25
+ let fence = null; // { char, len, start }
26
+ let lineStart = 0;
27
+ while (lineStart < n) {
28
+ const nl = text.indexOf("\n", lineStart);
29
+ const lineEnd = nl === -1 ? n : nl;
30
+ const line = text.slice(lineStart, lineEnd);
31
+ if (!fence) {
32
+ const open = FENCE_OPEN_RE.exec(line);
33
+ // Backtick fence info strings must not contain backticks (CommonMark).
34
+ if (open && !(open[1][0] === "`" && open[2].includes("`"))) {
35
+ fence = { char: open[1][0], len: open[1].length, start: lineStart };
36
+ }
37
+ } else {
38
+ const close = FENCE_CLOSE_RE.exec(line);
39
+ if (close && close[1][0] === fence.char && close[1].length >= fence.len) {
40
+ regions.push([fence.start, nl === -1 ? n : nl + 1]);
41
+ fence = null;
42
+ }
43
+ }
44
+ if (nl === -1) break;
45
+ lineStart = nl + 1;
46
+ }
47
+ if (fence) regions.push([fence.start, n]);
48
+
49
+ const inFence = (pos) => {
50
+ for (const [s, e] of regions) {
51
+ if (pos >= s && pos < e) return true;
52
+ }
53
+ return false;
54
+ };
55
+
56
+ // --- Pass 2: inline backtick code spans outside fences ---
57
+ // CommonMark: a run of N backticks closes only on the next run of exactly
58
+ // N backticks, and a code span cannot cross a blank line.
59
+ let i = 0;
60
+ while (i < n) {
61
+ if (text[i] !== "`" || inFence(i)) {
62
+ i += 1;
63
+ continue;
64
+ }
65
+ let runEnd = i;
66
+ while (runEnd < n && text[runEnd] === "`") runEnd += 1;
67
+ const runLen = runEnd - i;
68
+
69
+ let close = -1;
70
+ let k = runEnd;
71
+ scan: while (k < n) {
72
+ const ch = text[k];
73
+ if (ch === "`" && !inFence(k)) {
74
+ let ke = k;
75
+ while (ke < n && text[ke] === "`") ke += 1;
76
+ if (ke - k === runLen) {
77
+ close = k;
78
+ break;
79
+ }
80
+ k = ke;
81
+ continue;
82
+ }
83
+ if (ch === "\n") {
84
+ // Blank line ends the paragraph — no closer for this opener.
85
+ let p = k + 1;
86
+ while (p < n && (text[p] === " " || text[p] === "\t")) p += 1;
87
+ if (p < n && text[p] === "\n") break scan;
88
+ }
89
+ k += 1;
90
+ }
91
+
92
+ if (close === -1) {
93
+ // Unmatched run: literal backticks, keep scanning after the run.
94
+ i = runEnd;
95
+ continue;
96
+ }
97
+ regions.push([i, close + runLen]);
98
+ i = close + runLen;
99
+ }
100
+
101
+ regions.sort((a, b) => a[0] - b[0]);
102
+ return regions;
103
+ }
@@ -1,4 +1,5 @@
1
1
  import { filterDisplayEmojiText } from "./message-emoji-filter.js";
2
+ import { stripAllTaggedSpans } from "./tagged-span-strip.js";
2
3
  import { marked } from "marked";
3
4
 
4
5
  // --- Constants ---
@@ -39,9 +40,13 @@ let transcriptDirty = false;
39
40
  function buildDisplayEntry(msg, options = {}) {
40
41
  if (!msg || (msg.role !== "user" && msg.role !== "assistant")) return null;
41
42
 
42
- const text = extractText(msg.content);
43
+ let text = extractText(msg.content);
43
44
  if (!text) return null;
44
45
 
46
+ if (msg.role === "assistant") {
47
+ text = stripAllTaggedSpans(text);
48
+ }
49
+
45
50
  const { text: plainText } = markdownToPlainText(text, {
46
51
  stripReplyTags: msg.role === "assistant",
47
52
  });
@@ -212,6 +217,14 @@ function renderBlockTokens(tokens) {
212
217
  blocks.push(token.tokens ? renderInlineTokens(token.tokens) : token.text);
213
218
  break;
214
219
 
220
+ case "text":
221
+ // Block-level text token (marked wraps list-item content in these).
222
+ // Without this case it falls through to default, which recurses over
223
+ // the INLINE children as blocks — putting every bold/code span on its
224
+ // own line once the list join("\n") runs.
225
+ blocks.push(token.tokens ? renderInlineTokens(token.tokens) : token.text);
226
+ break;
227
+
215
228
  case "code":
216
229
  blocks.push(token.text);
217
230
  break;
@@ -1,10 +1,14 @@
1
1
  const DEFAULT_DEBUG_CATEGORIES = Object.freeze([
2
2
  "relay.transport",
3
3
  "relay.protocol",
4
+ "relay.health",
5
+ "relay.worker.health",
6
+ "relay.operation",
4
7
  "relay.session",
5
8
  "openclaw.run",
6
9
  "openclaw.seq",
7
10
  "openclaw.history",
11
+ "openclaw.message",
8
12
  "sdk.frames",
9
13
  "sdk.results",
10
14
  "sdk.events",
@@ -16,15 +20,16 @@ const DEFAULT_DEBUG_CATEGORIES = Object.freeze([
16
20
  "probe.runtime.memory",
17
21
  "probe.runtime.bridge",
18
22
  "probe.runtime.bridge_timing",
19
- "probe.runtime.screen_off",
20
23
  "probe.perf.conversation_upgrade",
21
24
  "app.state.diff",
22
25
  "render.ownership",
23
26
  "render.virtual_pager",
24
27
  "render.virtual_pager.summary",
25
28
  "render.virtual_pager.diagnostics",
29
+ "render.header_animation",
26
30
  "screen.nav",
27
31
  "screen.dim",
32
+ "glasses.lifecycle",
28
33
  "probe.webview.trace",
29
34
  "session.timeline",
30
35
  "approvals.timeline",
@@ -47,7 +52,6 @@ const DEBUG_CATEGORY_ALIASES = Object.freeze({
47
52
  "probe.runtime.memory",
48
53
  "probe.runtime.bridge",
49
54
  "probe.runtime.bridge_timing",
50
- "probe.runtime.screen_off",
51
55
  "probe.perf.conversation_upgrade",
52
56
  ]),
53
57
  "voice.timeline": Object.freeze([
@@ -67,8 +71,24 @@ const DEBUG_CATEGORY_ALIASES = Object.freeze({
67
71
  });
68
72
 
69
73
  const DEFAULT_NOISY_CATEGORY_POLICIES = Object.freeze({
74
+ "relay.health": Object.freeze({
75
+ sampleEvery: 1,
76
+ dedupeWindowMs: 250,
77
+ alwaysAllow: Object.freeze([
78
+ "event_loop_lag_spike",
79
+ "gc_pause",
80
+ "ws_send_buffer_high_water",
81
+ "relay_queue_depth",
82
+ ]),
83
+ }),
70
84
  "sdk.frames": Object.freeze({
71
- sampleEvery: 5,
85
+ // sampleEvery: 1 disables the 1-in-N sampler. Every sdk.frames emit from
86
+ // MessageScreenWritePipeline is a sparse pipeline-invariant marker (flush,
87
+ // pipeline_exit, dedup_skipped, etc.) — none are high-rate enough to need
88
+ // sampling, and dropping them breaks flush↔exit pairing analyses that
89
+ // depend on seeing every iteration. dedupeWindowMs stays as a narrow
90
+ // safety net for adjacent same-name/same-prefix bursts.
91
+ sampleEvery: 1,
72
92
  dedupeWindowMs: 150,
73
93
  alwaysAllow: Object.freeze([
74
94
  "coalescing_summary",
@@ -81,21 +101,6 @@ const DEFAULT_NOISY_CATEGORY_POLICIES = Object.freeze({
81
101
  }),
82
102
  });
83
103
 
84
- const REDACTION_SAFE = "safe";
85
- const REDACTION_FULL = "full";
86
- const DEFAULT_REDACTION_MODE = REDACTION_SAFE;
87
- const SUPPORTED_REDACTION_MODES = new Set([
88
- REDACTION_SAFE,
89
- REDACTION_FULL,
90
- ]);
91
- const SENSITIVE_KEY_PATTERN =
92
- /(?:api[-_]?key|auth(?:orization)?|bearer|cookie|credential|jwt|pass(?:word|phrase)?|private[-_]?key|secret|signature|token)/i;
93
- const FULL_ONLY_KEY_PATTERN =
94
- /^(?:renderedtext|fulltext|screentext)$/i;
95
- const SECRET_VALUE_PATTERN =
96
- /(?:^sk-[A-Za-z0-9_-]{8,}$)|(?:^Bearer\s+[^\s]+$)|(?:eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})/;
97
- const MAX_REDACTION_DEPTH = 6;
98
-
99
104
  function clampInt(value, min, max, fallback) {
100
105
  const n = Number(value);
101
106
  if (!Number.isFinite(n)) return fallback;
@@ -132,23 +137,11 @@ function expandCategoryAliases(list) {
132
137
  return Array.from(dedup.values());
133
138
  }
134
139
 
135
- function normalizeRedactionMode(raw) {
136
- if (raw === undefined || raw === null) return DEFAULT_REDACTION_MODE;
137
- if (typeof raw !== "string") return null;
138
- const mode = raw.trim().toLowerCase();
139
- if (!mode) return DEFAULT_REDACTION_MODE;
140
- if (!SUPPORTED_REDACTION_MODES.has(mode)) return null;
141
- return mode;
142
- }
143
-
144
140
  function createDebugStore(opts) {
145
141
  const options = opts || {};
146
- const capacity = clampInt(options.capacity, 1, 50000, 5000);
147
- const payloadMaxBytes = clampInt(options.payloadMaxBytes, 256, 65536, 2048);
142
+ const capacity = clampInt(options.capacity, 1, 100000, 100000);
148
143
  const defaultTtlMs = clampInt(options.defaultTtlMs, 1, 600000, 120000);
149
144
  const maxTtlMs = clampInt(options.maxTtlMs, 1, 3600000, 600000);
150
- const dumpDefaultLimit = clampInt(options.dumpDefaultLimit, 1, 2000, 300);
151
- const dumpMaxLimit = clampInt(options.dumpMaxLimit, 1, 10000, 2000);
152
145
  const nowFn = typeof options.now === "function" ? options.now : () => Date.now();
153
146
 
154
147
  const configuredCategories =
@@ -161,19 +154,6 @@ function createDebugStore(opts) {
161
154
  ...DEFAULT_NOISY_CATEGORY_POLICIES,
162
155
  ...(options.noisyPolicies || {}),
163
156
  };
164
- const safeStringTailChars = clampInt(
165
- options.safeStringTailChars,
166
- 16,
167
- 2048,
168
- Math.max(64, Math.min(payloadMaxBytes, 160)),
169
- );
170
- const fullStringTailChars = clampInt(
171
- options.fullStringTailChars,
172
- safeStringTailChars,
173
- 4096,
174
- Math.max(256, Math.min(payloadMaxBytes * 2, 1024)),
175
- );
176
-
177
157
  /** @type {Map<string, number>} */
178
158
  const enabledUntil = new Map();
179
159
 
@@ -189,102 +169,6 @@ function createDebugStore(opts) {
189
169
  let ringSize = 0;
190
170
  let seq = 0;
191
171
 
192
- function isSensitiveKeyName(keyName) {
193
- if (typeof keyName !== "string" || !keyName) return false;
194
- return SENSITIVE_KEY_PATTERN.test(keyName);
195
- }
196
-
197
- function isFullOnlyKeyName(keyName) {
198
- if (typeof keyName !== "string" || !keyName) return false;
199
- return FULL_ONLY_KEY_PATTERN.test(keyName);
200
- }
201
-
202
- function redactInlineSecrets(value) {
203
- if (typeof value !== "string" || !value) return value;
204
- return value
205
- .replace(/(Bearer\s+)[^\s"']+/gi, "$1[REDACTED]")
206
- .replace(
207
- /((?:api[-_]?key|token|secret|password|passphrase)\s*(?:=|:)\s*)([^,\s"']+)/gi,
208
- "$1[REDACTED]",
209
- );
210
- }
211
-
212
- function redactStringValue(value, mode, keyName) {
213
- const sensitiveByKey = isSensitiveKeyName(keyName);
214
- if (mode === REDACTION_SAFE) {
215
- if (isFullOnlyKeyName(keyName)) {
216
- return {
217
- _fullOnly: true,
218
- _chars: value.length,
219
- };
220
- }
221
- if (sensitiveByKey || SECRET_VALUE_PATTERN.test(value)) {
222
- return "[REDACTED]";
223
- }
224
- const redactedInline = redactInlineSecrets(value);
225
- if (redactedInline.length <= safeStringTailChars) {
226
- return redactedInline;
227
- }
228
- return {
229
- _truncated: true,
230
- _chars: redactedInline.length,
231
- _tail: redactedInline.slice(-safeStringTailChars),
232
- };
233
- }
234
-
235
- if (value.length <= fullStringTailChars) {
236
- return value;
237
- }
238
- return {
239
- _truncated: true,
240
- _chars: value.length,
241
- _tail: value.slice(-fullStringTailChars),
242
- };
243
- }
244
-
245
- function redactDataByMode(value, mode, keyName, depth) {
246
- if (depth > MAX_REDACTION_DEPTH) {
247
- return { _truncated: true, _depth: depth };
248
- }
249
-
250
- if (typeof value === "string") {
251
- return redactStringValue(value, mode, keyName);
252
- }
253
- if (
254
- value === null ||
255
- typeof value === "number" ||
256
- typeof value === "boolean"
257
- ) {
258
- return value;
259
- }
260
- if (Array.isArray(value)) {
261
- return value.map((entry) =>
262
- redactDataByMode(entry, mode, keyName, depth + 1),
263
- );
264
- }
265
- if (typeof value === "object") {
266
- const out = {};
267
- for (const [key, entry] of Object.entries(value)) {
268
- out[key] = redactDataByMode(entry, mode, key, depth + 1);
269
- }
270
- return out;
271
- }
272
-
273
- return { value };
274
- }
275
-
276
- function buildRedactedDataModes(data) {
277
- const safe = redactDataByMode(data, REDACTION_SAFE, null, 0);
278
- const full = redactDataByMode(data, REDACTION_FULL, null, 0);
279
- let safeSerialized;
280
- try {
281
- safeSerialized = JSON.stringify(safe);
282
- } catch {
283
- safeSerialized = "{\"_serializationError\":true}";
284
- }
285
- return { safe, full, safeSerialized };
286
- }
287
-
288
172
  function pruneExpired(nowMs) {
289
173
  for (const [cat, expiresAt] of enabledUntil) {
290
174
  if (expiresAt <= nowMs) {
@@ -403,21 +287,7 @@ function redactStringValue(value, mode, keyName) {
403
287
  serialized = JSON.stringify(normalized);
404
288
  }
405
289
 
406
- const bytes = Buffer.byteLength(serialized, "utf8");
407
- if (bytes <= payloadMaxBytes) {
408
- return { data: normalized, serialized };
409
- }
410
-
411
- const tailMaxChars = Math.max(64, Math.min(payloadMaxBytes, 1024));
412
- const truncated = {
413
- _truncated: true,
414
- _bytes: bytes,
415
- _tail: serialized.slice(-tailMaxChars),
416
- };
417
- return {
418
- data: truncated,
419
- serialized: JSON.stringify(truncated),
420
- };
290
+ return { data: normalized, serialized };
421
291
  }
422
292
 
423
293
  function allowByNoisyPolicy(cat, eventName, serializedData, ts) {
@@ -458,11 +328,12 @@ function redactStringValue(value, mode, keyName) {
458
328
  }
459
329
  }
460
330
 
461
- function emit(event) {
331
+ function emit(event, options) {
462
332
  const raw = event || {};
463
333
  const cat = typeof raw.cat === "string" ? raw.cat.trim() : "";
334
+ const force = !!(options && options.force === true);
464
335
  if (!cat) return false;
465
- if (!isEnabled(cat)) return false;
336
+ if (!force && !isEnabled(cat)) return false;
466
337
 
467
338
  const ts = Number.isFinite(raw.ts) ? Math.floor(raw.ts) : nowFn();
468
339
  const eventName =
@@ -477,8 +348,7 @@ function redactStringValue(value, mode, keyName) {
477
348
  : "debug";
478
349
 
479
350
  const normalized = normalizeData(raw.data);
480
- const redactedDataModes = buildRedactedDataModes(normalized.data);
481
- if (!allowByNoisyPolicy(cat, eventName, redactedDataModes.safeSerialized, ts)) {
351
+ if (!allowByNoisyPolicy(cat, eventName, normalized.serialized, ts)) {
482
352
  return false;
483
353
  }
484
354
 
@@ -488,8 +358,7 @@ function redactStringValue(value, mode, keyName) {
488
358
  event: eventName,
489
359
  severity,
490
360
  seq: ++seq,
491
- dataSafe: redactedDataModes.safe,
492
- dataFull: redactedDataModes.full,
361
+ data: normalized.data,
493
362
  };
494
363
 
495
364
  if (typeof raw.sessionKey === "string" && raw.sessionKey) {
@@ -516,17 +385,14 @@ function redactStringValue(value, mode, keyName) {
516
385
  return out;
517
386
  }
518
387
 
519
- function formatEventForDump(evt, redactionMode) {
388
+ function formatEventForDump(evt) {
520
389
  const out = {
521
390
  ts: evt.ts,
522
391
  cat: evt.cat,
523
392
  event: evt.event,
524
393
  severity: evt.severity,
525
394
  seq: evt.seq,
526
- data:
527
- redactionMode === REDACTION_FULL
528
- ? evt.dataFull
529
- : evt.dataSafe,
395
+ data: evt.data,
530
396
  };
531
397
 
532
398
  if (typeof evt.sessionKey === "string" && evt.sessionKey) {
@@ -545,13 +411,6 @@ function redactStringValue(value, mode, keyName) {
545
411
  function dump(request) {
546
412
  const req = request || {};
547
413
  const nowMs = nowFn();
548
- const redaction = normalizeRedactionMode(req.redaction);
549
- if (!redaction) {
550
- return {
551
- ok: false,
552
- error: "debug-dump redaction must be one of: safe, full",
553
- };
554
- }
555
414
  const categoriesFilter = expandCategoryAliases(
556
415
  normalizeCategoryList(req.categories),
557
416
  );
@@ -603,7 +462,10 @@ function redactStringValue(value, mode, keyName) {
603
462
  };
604
463
  }
605
464
 
606
- const limit = clampInt(req.limit, 1, dumpMaxLimit, dumpDefaultLimit);
465
+ const limit =
466
+ req.limit !== undefined && Number.isFinite(Number(req.limit)) && Number(req.limit) > 0
467
+ ? Math.floor(Number(req.limit))
468
+ : null;
607
469
  const sinceMs =
608
470
  req.sinceMs !== undefined
609
471
  ? Math.floor(Number(req.sinceMs))
@@ -638,26 +500,25 @@ function redactStringValue(value, mode, keyName) {
638
500
  }
639
501
 
640
502
  const events =
641
- filtered.length > limit
503
+ limit !== null && filtered.length > limit
642
504
  ? filtered.slice(filtered.length - limit)
643
505
  : filtered;
644
- const formattedEvents = events.map((evt) =>
645
- formatEventForDump(evt, redaction),
646
- );
506
+ const formattedEvents = events.map((evt) => formatEventForDump(evt));
647
507
 
648
508
  return {
649
509
  ok: true,
650
510
  nowMs,
651
511
  sinceMs,
652
512
  untilMs,
653
- redaction,
654
513
  categories: categoriesFilter,
655
- limit,
514
+ limit: limit === null ? undefined : limit,
656
515
  totalMatched: filtered.length,
657
516
  returned: formattedEvents.length,
658
517
  dropped: Math.max(0, filtered.length - formattedEvents.length),
659
518
  enabled: getEnabledCategories(nowMs),
660
519
  events: formattedEvents,
520
+ ringEvents: ringSize,
521
+ ringCapacity: capacity,
661
522
  };
662
523
  }
663
524
 
@@ -682,12 +543,8 @@ function redactStringValue(value, mode, keyName) {
682
543
  getConfig() {
683
544
  return {
684
545
  capacity,
685
- payloadMaxBytes,
686
546
  defaultTtlMs,
687
547
  maxTtlMs,
688
- dumpDefaultLimit,
689
- dumpMaxLimit,
690
- defaultRedaction: DEFAULT_REDACTION_MODE,
691
548
  };
692
549
  },
693
550
  };
@@ -0,0 +1,62 @@
1
+ const LABEL_MAX = 32;
2
+ const TITLE_MAX = 64;
3
+ const ITEMS_SHOWN = 8;
4
+ const BODY_MAX = 120;
5
+ const SUMMARY_MAX = 400;
6
+
7
+ function truncate(value, max) {
8
+ if (typeof value !== "string") return "";
9
+ if (value.length <= max) return value;
10
+ return value.slice(0, Math.max(0, max - 1)) + "…";
11
+ }
12
+
13
+ /**
14
+ * Summarize a glasses-UI render spec OR surface-update patch into a
15
+ * token-bounded, structured shape: { kind, title?, items?, itemsMore?, body? }.
16
+ * Pure; never throws.
17
+ */
18
+ function summarizeGlassesUiContent(specOrPatch) {
19
+ const o = specOrPatch && typeof specOrPatch === "object" ? specOrPatch : {};
20
+ const rawItems = Array.isArray(o.items) ? o.items : null;
21
+ const out = {};
22
+
23
+ const hasDetail =
24
+ !!rawItems &&
25
+ rawItems.some((i) => i && typeof i === "object" && typeof i.body === "string");
26
+ if (rawItems) out.kind = hasDetail ? "list_with_details" : "list";
27
+ else if (typeof o.body === "string") out.kind = "text";
28
+ else out.kind = "unknown";
29
+
30
+ if (typeof o.title === "string") out.title = truncate(o.title, TITLE_MAX);
31
+
32
+ if (rawItems) {
33
+ const labels = rawItems
34
+ .map((i) =>
35
+ typeof i === "string"
36
+ ? i
37
+ : i && typeof i === "object" && typeof i.label === "string"
38
+ ? i.label
39
+ : "",
40
+ )
41
+ .filter((l) => l.length > 0)
42
+ .map((l) => truncate(l, LABEL_MAX));
43
+ out.items = labels.slice(0, ITEMS_SHOWN);
44
+ if (labels.length > ITEMS_SHOWN) out.itemsMore = labels.length - ITEMS_SHOWN;
45
+ }
46
+
47
+ if (typeof o.body === "string") out.body = truncate(o.body, BODY_MAX);
48
+
49
+ // Defensive cap: drop trailing items until under SUMMARY_MAX.
50
+ while (
51
+ Array.isArray(out.items) &&
52
+ out.items.length > 1 &&
53
+ JSON.stringify(out).length > SUMMARY_MAX
54
+ ) {
55
+ out.items.pop();
56
+ out.itemsMore = (out.itemsMore || 0) + 1;
57
+ }
58
+
59
+ return out;
60
+ }
61
+
62
+ export { summarizeGlassesUiContent };
@@ -0,0 +1,28 @@
1
+ export const GLASSES_UI_NUDGE_SYSTEM_PROMPT = [
2
+ "On the user's Even G2 glasses, prefer calling the render_glasses_ui tool over",
3
+ "a multi-line text reply when the answer is a short list of choices the user",
4
+ "could pick from (use list_surface), a list where each option has a short",
5
+ "detail the user needs to read before choosing (use list_with_details_surface,",
6
+ "with a one-to-two-sentence body per item), or a single formatted block they",
7
+ "should read (use text_surface). The user can double-tap to back up one popup; if a",
8
+ "tool call returns { result: \"back\" }, they want to revise their previous",
9
+ "answer — re-render the previous step or pivot.",
10
+ "",
11
+ "After the tool call resolves, your NEXT output decides what the glasses show",
12
+ "next:",
13
+ " • another render_glasses_ui call → replaces the current surface (use this",
14
+ " for a drill-down or follow-up step in the flow);",
15
+ " • a short text reply → the chat screen takes over and the surface",
16
+ " disappears, so the user sees your text instead of the now-stale list;",
17
+ " • silent run-end (no further output) → the surface lingers on glass until",
18
+ " the user dismisses; only do this if you intentionally want the user to",
19
+ " keep interacting with the same surface.",
20
+ "After result \"selected\", default to either a follow-up render (next step in",
21
+ "the flow) or a brief one-line text ack confirming the choice; avoid ending",
22
+ "the run silently unless the rendered surface is still the right thing to",
23
+ "look at.",
24
+ ].join(" ");
25
+
26
+ export function composeGlassesUiNudgeSystemPrompt() {
27
+ return GLASSES_UI_NUDGE_SYSTEM_PROMPT;
28
+ }
@@ -0,0 +1,16 @@
1
+ // Single source of truth for the 50-emoji allowlist used by the
2
+ // message body-text filter, the Neural Emoji Reactor stream parser,
3
+ // and the Neural Emoji Reactor system-prompt composer.
4
+ //
5
+ // Adding/removing entries here is a wire-protocol change in spirit:
6
+ // the agent's prompt enumerates this list verbatim and the parser
7
+ // rejects any tag with an off-list emoji.
8
+ export const MESSAGE_EMOJI_ALLOWLIST = [
9
+ "😂", "❤️", "🤣", "👍", "😭", "🙏", "😘", "🥰", "😍", "😊",
10
+ "🎉", "😁", "💕", "🥺", "😅", "🔥", "☺️", "🤦", "🤷", "🙄",
11
+ "😆", "🤗", "😉", "🎂", "🤔", "👏", "🙂", "😳", "🥳", "😎",
12
+ "👌", "😔", "💪", "✨", "💖", "💞", "👀", "😋", "😏", "😢",
13
+ "👉", "💗", "😩", "💯", "🌹", "🎈", "😚", "😐", "😒", "😀",
14
+ ];
15
+
16
+ export const MESSAGE_EMOJI_ALLOWLIST_SET = new Set(MESSAGE_EMOJI_ALLOWLIST);