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.
Files changed (83) hide show
  1. package/README.md +29 -1
  2. package/dist/config/runtime-config-session-title-model.test.js +0 -3
  3. package/dist/config/runtime-config.js +22 -33
  4. package/dist/domain/activity-status-adapter.js +0 -7
  5. package/dist/domain/activity-status-arbiter.js +3 -27
  6. package/dist/domain/activity-status-labels.js +8 -38
  7. package/dist/domain/code-span-regions.js +4 -24
  8. package/dist/domain/constant-time-equal.js +9 -0
  9. package/dist/domain/constant-time-equal.test.js +28 -0
  10. package/dist/domain/conversation-state.js +27 -138
  11. package/dist/domain/debug-bundle-cache.js +52 -0
  12. package/dist/domain/debug-bundle-format.js +60 -0
  13. package/dist/domain/debug-bundle-preview.js +123 -0
  14. package/dist/domain/debug-bundle-redaction.js +182 -0
  15. package/dist/domain/debug-bundle-save.js +11 -0
  16. package/dist/domain/debug-bundle-zip.js +15 -0
  17. package/dist/domain/debug-bundle.js +97 -0
  18. package/dist/domain/debug-store.js +6 -17
  19. package/dist/domain/debug-upload-preset.js +27 -0
  20. package/dist/domain/glasses-display-system-prompt.js +0 -5
  21. package/dist/domain/glasses-display-system-prompt.test.js +1 -1
  22. package/dist/domain/glasses-ui-content-summary.js +0 -6
  23. package/dist/domain/glasses-ui-system-prompt.test.js +1 -2
  24. package/dist/domain/message-emoji-allowlist.js +0 -7
  25. package/dist/domain/message-emoji-filter.js +3 -9
  26. package/dist/domain/neural-emoji-reactor-tag-config.js +3 -3
  27. package/dist/domain/prompt-channel-fragments.js +1 -10
  28. package/dist/domain/tagged-span-parser.js +3 -26
  29. package/dist/domain/tagged-span-strip.js +0 -7
  30. package/dist/even-ai/even-ai-endpoint.js +77 -24
  31. package/dist/even-ai/even-ai-run-waiter.js +0 -1
  32. package/dist/even-ai/even-ai-settings-store.js +11 -0
  33. package/dist/gateway/gateway-bridge.js +8 -9
  34. package/dist/gateway/gateway-timing-ledger.js +8 -6
  35. package/dist/gateway/openclaw-client.js +97 -297
  36. package/dist/gateway/sanitize-connect-reason.js +10 -0
  37. package/dist/gateway/sanitize-connect-reason.test.js +34 -0
  38. package/dist/index.js +3 -3
  39. package/dist/runtime/channel-two-hook.js +1 -6
  40. package/dist/runtime/container-env.js +1 -5
  41. package/dist/runtime/debug-bundle-handler.js +159 -0
  42. package/dist/runtime/display-toggle-states.js +6 -17
  43. package/dist/runtime/downstream-handler.js +682 -508
  44. package/dist/runtime/glasses-backpressure-latch.js +2 -24
  45. package/dist/runtime/ocuclaw-settings-store.js +10 -1
  46. package/dist/runtime/openclaw-host-version.js +5 -0
  47. package/dist/runtime/plugin-version-service.js +13 -6
  48. package/dist/runtime/provider-usage-select.js +0 -6
  49. package/dist/runtime/register-session-title-distiller.js +14 -16
  50. package/dist/runtime/relay-core.js +601 -290
  51. package/dist/runtime/relay-service.js +19 -47
  52. package/dist/runtime/relay-worker-approval-replay-cache.js +1 -1
  53. package/dist/runtime/relay-worker-entry.js +1 -2
  54. package/dist/runtime/relay-worker-health.js +2 -10
  55. package/dist/runtime/relay-worker-protocol.js +6 -1
  56. package/dist/runtime/relay-worker-supervisor.js +103 -41
  57. package/dist/runtime/relay-worker-transport.js +150 -17
  58. package/dist/runtime/session-context-service.js +5 -45
  59. package/dist/runtime/session-service.js +157 -175
  60. package/dist/runtime/session-title-distiller-budget.js +1 -5
  61. package/dist/runtime/session-title-distiller-helpers.js +14 -24
  62. package/dist/runtime/session-title-distiller.js +109 -122
  63. package/dist/runtime/session-title-record.js +0 -6
  64. package/dist/runtime/stable-prompt-snapshot.js +3 -14
  65. package/dist/runtime/upstream-runtime.js +600 -103
  66. package/dist/tools/device-info-tool.js +4 -21
  67. package/dist/tools/glasses-ui-cron.js +22 -77
  68. package/dist/tools/glasses-ui-descriptors.js +4 -33
  69. package/dist/tools/glasses-ui-limits.js +0 -13
  70. package/dist/tools/glasses-ui-paint-floor.js +5 -39
  71. package/dist/tools/glasses-ui-recipes.js +92 -101
  72. package/dist/tools/glasses-ui-surfaces.js +31 -163
  73. package/dist/tools/glasses-ui-template.js +7 -22
  74. package/dist/tools/glasses-ui-tool-description.test.js +2 -2
  75. package/dist/tools/glasses-ui-tool.js +87 -451
  76. package/dist/tools/glasses-ui-voicemail.js +6 -63
  77. package/dist/tools/glasses-ui-wake.js +9 -76
  78. package/dist/tools/session-title-tool.js +2 -7
  79. package/dist/tools/session-title-tool.test.js +1 -1
  80. package/dist/version.js +3 -2
  81. package/openclaw.plugin.json +60 -13
  82. package/package.json +3 -2
  83. package/dist/runtime/protocol-adapter.js +0 -387
@@ -0,0 +1,182 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ const DEFAULT_SAFE_KEYS = new Set([
4
+ "page", "pageCount", "selectedIndex", "cursor", "index", "count", "lane",
5
+ "slotState", "ms", "durationMs", "seq", "ok", "enabled", "connected",
6
+ "battery", "batteryLevel", "from", "to", "lines", "width", "height", "kind",
7
+ "severity", "state", "phase",
8
+ ]);
9
+
10
+ const SAFE_KEYS = {
11
+ "sdk.frames": new Set([
12
+ "writeSeq", "chars", "lineCount", "selectedLane", "selectedCanonicalIndex",
13
+ "virtualPageCount", "slotIntent", "slotRunId", "indicator", "bodyHash",
14
+ "statusEmojiDecisionBranch", "streamingEmojiVariant", "startsWithNewline",
15
+ "endsWithNewline", "unifiedHeaderChars", "unifiedBodyChars",
16
+ "unifiedLeftTruncated", "payloadLiteMode", "containerName", "containerID",
17
+ "owner", "coalesced", "contentOffset", "contentLength",
18
+ "bridgeCallPrevented",
19
+ ]),
20
+
21
+ "glasses.lifecycle": new Set([
22
+ "surfaceId", "mode", "depth", "kind", "itemsMore",
23
+ ]),
24
+
25
+ "openclaw.message": new Set([
26
+ "runId",
27
+ ]),
28
+
29
+ "evenai": new Set([
30
+ "requestId", "bodyBytes", "messageChars", "model", "extraSystemPromptChars",
31
+ "routingMode", "sessionChanged", "listenEnabled", "dedupWindowMs",
32
+ "activeRequestId", "code", "elapsedMs", "timeoutMs", "textChars",
33
+ ]),
34
+
35
+ "voice.timeline": new Set([
36
+ "voiceSessionId", "trigger", "wasWaiting", "flagSource", "transcriptChars",
37
+ "sdkPackage",
38
+ ]),
39
+
40
+ "voice.transport": new Set([
41
+ "voiceSessionId", "trigger", "state", "transportMode",
42
+ ]),
43
+
44
+ "screen.nav": new Set([
45
+ "activeScreen", "navigationSeq", "inFlightNavigationSeq", "owner", "navSeq",
46
+ "source", "sysType", "textType", "listType", "menuDepth", "pendingCount",
47
+ "deferredDoubleClick", "coalesced", "replacedMenuActivation",
48
+ ]),
49
+
50
+ "render.ownership": new Set([
51
+ "activeScreen", "navigationSeq", "inFlightNavigationSeq", "owner",
52
+ ]),
53
+
54
+ "render.header_animation": new Set([
55
+ "slotRunId", "slotActivityId", "slotSeq", "slotCategory", "slotOrigin",
56
+ "slotPhase", "slotIntent", "expectedScreenLifecycleEpoch",
57
+ ]),
58
+
59
+ "render.virtual_pager.diagnostics": new Set([
60
+ "selectedLane", "selectedCanonicalIndex", "streamPageCount",
61
+ "historyPageCount", "autoFollow", "streamManualBrowseActive",
62
+ "handoffAnchorOffset",
63
+ ]),
64
+
65
+ "relay.protocol": new Set([
66
+ "messageId", "textChars", "hasAttachment", "attachmentBytes",
67
+ "upstreamDispatchMs", "localPublishMs", "onSendSyncMs", "clientId",
68
+ "persisted",
69
+ ]),
70
+
71
+ "relay.worker.health": new Set([
72
+ "workerEpoch", "mainFrameAgeMs", "mainHeartbeatAgeMs",
73
+ "workerMainQueueDepth", "workerQueueDepthByClass",
74
+ ]),
75
+
76
+ "relay.operation": new Set([
77
+ "requestId", "operation", "class", "clientId", "duplicate", "retainedFinal",
78
+ ]),
79
+
80
+ "relay.health": new Set([
81
+ "bufferedAmountBytes", "thresholdBytes", "clientId",
82
+ ]),
83
+ "relay.session": new Set([
84
+ "sessionId", "clientId",
85
+ ]),
86
+ "openclaw.run": new Set([
87
+ "role", "contentBlocks", "textChars", "rawAssistantChars",
88
+ "assistantDeltaChars", "firstGatewayChunk", "gatewayReceivedAtMs",
89
+ "gatewayToRelayIngressMs",
90
+ ]),
91
+
92
+ "app.lifecycle": new Set([
93
+ "requestId", "sinceMs", "flushed", "dropped",
94
+ ]),
95
+ };
96
+
97
+ const SECRET_KEY_RE = /token|secret|auth|apikey|api_key|password|cookie|bearer/i;
98
+ const TOKEN_VALUE_RE = /[?&](?:token|access_token|key)=[^&\s"]+/gi;
99
+
100
+ const SECRET_VALUE_RE = /(?:bearer\s+\S+|authorization\s*[:=]\s*(?:bearer\s+)?\S+|(?:access[_-]?token|token|secret|api[_-]?key|password|passwd|pwd)\s*=\s*\S+)/gi;
101
+
102
+ const URL_AUTHORITY_RE = /\b(wss?|https?):\/\/[^\/\s"'?#]+/gi;
103
+
104
+ const ADDRESS_KEY_RE = /^(?:url|uri|addr|address|host|hostname|endpoint|relay|gateway)(?:[A-Z_]|$)|(?:Url|Uri|Addr|Address|Host|Hostname|Endpoint|Relay|Gateway)(?:[A-Z_]|$)/;
105
+
106
+ export function structuralPlaceholder(s) {
107
+
108
+ return s.replace(/\S/g, "■");
109
+ }
110
+
111
+ function safeKeysFor(cat) {
112
+ const extra = SAFE_KEYS[cat];
113
+ if (!extra) return DEFAULT_SAFE_KEYS;
114
+ return new Set([...DEFAULT_SAFE_KEYS, ...extra]);
115
+ }
116
+
117
+ function redactData(cat, data, mode) {
118
+ if (data == null || typeof data !== "object") return data;
119
+ const safe = safeKeysFor(cat);
120
+ const out = Array.isArray(data) ? [] : {};
121
+ for (const [k, v] of Object.entries(data)) {
122
+
123
+ if (SECRET_KEY_RE.test(k)) continue;
124
+ if (typeof v === "string") {
125
+
126
+ if (ADDRESS_KEY_RE.test(k)) {
127
+ out[k] = "[redacted-address]";
128
+ continue;
129
+ }
130
+
131
+ const cleaned = v
132
+ .replace(TOKEN_VALUE_RE, "[redacted]")
133
+ .replace(SECRET_VALUE_RE, "[redacted-secret]")
134
+ .replace(URL_AUTHORITY_RE, "$1://[redacted-host]");
135
+ if (safe.has(k) || mode === "off") {
136
+ out[k] = cleaned;
137
+ } else if (mode === "full") {
138
+ out[k] = "";
139
+ } else {
140
+ out[k] = structuralPlaceholder(cleaned);
141
+ }
142
+ } else if (v && typeof v === "object") {
143
+
144
+ out[k] = redactData(cat, v, mode);
145
+ } else {
146
+ out[k] = v;
147
+ }
148
+ }
149
+ return out;
150
+ }
151
+
152
+ function hashId(salt, value) {
153
+ return createHash("sha256").update(salt + ":" + value).digest("hex").slice(0, 16);
154
+ }
155
+
156
+ export function redactEvents(events, opts) {
157
+ const mode = opts && opts.mode ? opts.mode : "structural";
158
+ const idSalt = opts && typeof opts.idSalt === "string" ? opts.idSalt : "";
159
+ return events.map((evt) => {
160
+ const out = {
161
+ ts: evt.ts,
162
+ cat: evt.cat,
163
+ event: evt.event,
164
+ severity: evt.severity,
165
+ seq: evt.seq,
166
+ data: redactData(evt.cat, evt.data, mode),
167
+ };
168
+ if (typeof evt.sessionKey === "string" && evt.sessionKey) {
169
+ out.sessionKey = hashId(idSalt, evt.sessionKey);
170
+ }
171
+ if (typeof evt.runId === "string" && evt.runId) {
172
+ out.runId = hashId(idSalt, evt.runId);
173
+ }
174
+ if (typeof evt.screen === "string" && evt.screen) {
175
+
176
+ out.screen = evt.screen;
177
+ }
178
+ return out;
179
+ });
180
+ }
181
+
182
+ export { SAFE_KEYS, DEFAULT_SAFE_KEYS };
@@ -0,0 +1,11 @@
1
+ export function saveBundleToDisk(opts) {
2
+ const { saveDir, bundleId, savedMs, zip, metadataJson, fs, path } = opts;
3
+ const safeId = String(bundleId).replace(/[^A-Za-z0-9-]/g, "");
4
+ const stamp = new Date(savedMs).toISOString().slice(0, 19).replace(/[-:]/g, "") + "Z";
5
+ const base = stamp + "-" + safeId;
6
+ fs.mkdirSync(saveDir, { recursive: true });
7
+ const zipPath = path.join(saveDir, base + ".zip");
8
+ fs.writeFileSync(zipPath, zip);
9
+ fs.writeFileSync(path.join(saveDir, base + ".metadata.json"), metadataJson);
10
+ return { savedPath: zipPath, fileSize: zip.length };
11
+ }
@@ -0,0 +1,15 @@
1
+ import { zipSync, strToU8 } from "fflate";
2
+ import { createHash } from "node:crypto";
3
+
4
+ export function zipFiles(files) {
5
+ const entries = {};
6
+ for (const [name, content] of files) {
7
+ entries[name] = typeof content === "string" ? strToU8(content) : content;
8
+ }
9
+
10
+ return zipSync(entries, { mtime: new Date("1980-01-01") });
11
+ }
12
+
13
+ export function sha256Hex(bytes) {
14
+ return createHash("sha256").update(bytes).digest("hex");
15
+ }
@@ -0,0 +1,97 @@
1
+ import { bucketEventsToFiles, renderBundleReadme } from "./debug-bundle-format.js";
2
+ import { redactEvents } from "./debug-bundle-redaction.js";
3
+ import { zipFiles, sha256Hex } from "./debug-bundle-zip.js";
4
+ import { strToU8 } from "fflate";
5
+
6
+ const LIVEUI_LANE = ["glasses.lifecycle", "openclaw.message", "evenai"];
7
+ const SCHEMA_VERSION = 1;
8
+ const FORMAT_VERSION = 1;
9
+
10
+ export function assembleBundle(dumpResult, opts) {
11
+ const appliedQuery = dumpResult.appliedQuery || {
12
+ categories: dumpResult.categories,
13
+ sinceMs: dumpResult.sinceMs,
14
+ untilMs: dumpResult.untilMs,
15
+ };
16
+
17
+ let events = redactEvents(dumpResult.events, { mode: opts.redactionMode, idSalt: opts.idSalt });
18
+ let ringCapped = opts.ringCappedWindow;
19
+
20
+ const dropOldest = () => {
21
+ events.sort((a, b) => a.ts - b.ts || (a.seq || 0) - (b.seq || 0));
22
+ events = events.slice(Math.ceil(events.length * 0.1));
23
+ ringCapped = true;
24
+ };
25
+
26
+ let built = buildArtifacts(events, dumpResult, appliedQuery, opts, ringCapped);
27
+
28
+ if (typeof opts.maxZipBytes === "number" && opts.maxZipBytes > 0) {
29
+ while (built.zip.length > opts.maxZipBytes && events.length > 0) {
30
+ dropOldest();
31
+ built = buildArtifacts(events, dumpResult, appliedQuery, opts, ringCapped);
32
+ }
33
+ }
34
+
35
+ return { zip: built.zip, bundleSha256: built.bundleSha256, metadata: built.metadata, chunks: built.chunks, files: built.files };
36
+ }
37
+
38
+ function buildArtifacts(events, dumpResult, appliedQuery, opts, ringCapped) {
39
+
40
+ const { files, summary } = bucketEventsToFiles({ events, ringEvents: dumpResult.ringEvents, ringCapacity: dumpResult.ringCapacity, appliedQuery });
41
+
42
+ const lane = events
43
+ .filter((e) => LIVEUI_LANE.includes(e.cat))
44
+ .sort((a, b) => a.ts - b.ts || (a.seq || 0) - (b.seq || 0));
45
+ if (lane.length) {
46
+ files.set("correlation-liveui.jsonl", lane.map((e) => JSON.stringify(e) + "\n").join(""));
47
+ }
48
+
49
+ files.set("README.md", renderBundleReadme(summary));
50
+
51
+ const contentNames = [...files.keys()].filter((n) => n !== "metadata.json").sort();
52
+ const concat = contentNames.map((n) => files.get(n)).join("");
53
+ const contentSha256 = sha256Hex(strToU8(concat));
54
+
55
+ const metadata = {
56
+ schemaVersion: SCHEMA_VERSION,
57
+ formatVersion: FORMAT_VERSION,
58
+ kind: "ocuclaw-debug-bundle",
59
+ capturedAtMs: dumpResult.nowMs,
60
+ window: {
61
+ fromMs: summary.timeRange ? summary.timeRange.fromMs : null,
62
+ toMs: summary.timeRange ? summary.timeRange.toMs : null,
63
+ spanMs: summary.timeRange ? summary.timeRange.spanMs : null,
64
+ ringCappedWindow: ringCapped,
65
+ },
66
+ ring: { events: dumpResult.ringEvents, capacity: dumpResult.ringCapacity },
67
+ totalBytes: summary.totalBytes,
68
+ contentSha256,
69
+ build: opts.build,
70
+ installId: opts.installId,
71
+ redactionMode: opts.redactionMode,
72
+ secretsStripped: true,
73
+ categories: summary.categories,
74
+ appliedQuery,
75
+ timeRange: summary.timeRange,
76
+ notes: { byteCountsArePostRedaction: true, appliedQueryIsPreExpansion: true, crossCategoryMergeKey: ["ts", "seq"] },
77
+ ticket: { id: null, reporter: null, note: opts.note || null, deviceModel: "G2" },
78
+ };
79
+ files.set("metadata.json", JSON.stringify(metadata, null, 2) + "\n");
80
+
81
+ const zip = zipFiles(files);
82
+ const bundleSha256 = sha256Hex(zip);
83
+ const chunks = chunkZip(zip, opts.chunkBytes);
84
+
85
+ return { files, summary, metadata, zip, bundleSha256, chunks };
86
+ }
87
+
88
+ export function chunkZip(zip, chunkBytes) {
89
+ const safeChunkBytes = Math.max(1, chunkBytes | 0);
90
+ const partCount = Math.max(1, Math.ceil(zip.length / safeChunkBytes));
91
+ const chunks = [];
92
+ for (let i = 0; i < partCount; i++) {
93
+ const slice = zip.subarray(i * safeChunkBytes, (i + 1) * safeChunkBytes);
94
+ chunks.push({ partIndex: i, partCount, partBase64: Buffer.from(slice).toString("base64") });
95
+ }
96
+ return chunks;
97
+ }
@@ -82,12 +82,7 @@ const DEFAULT_NOISY_CATEGORY_POLICIES = Object.freeze({
82
82
  ]),
83
83
  }),
84
84
  "sdk.frames": Object.freeze({
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.
85
+
91
86
  sampleEvery: 1,
92
87
  dedupeWindowMs: 150,
93
88
  alwaysAllow: Object.freeze([
@@ -154,16 +149,9 @@ function createDebugStore(opts) {
154
149
  ...DEFAULT_NOISY_CATEGORY_POLICIES,
155
150
  ...(options.noisyPolicies || {}),
156
151
  };
157
- /** @type {Map<string, number>} */
152
+
158
153
  const enabledUntil = new Map();
159
154
 
160
- // Rehydrate the arm from a persisted snapshot. relay-core reads debug-arm.json
161
- // at construction and passes it here as options.initialEnabled, whose entries are
162
- // the exact shape getSnapshot().enabled emits. That snapshot is already pruned of
163
- // expired categories (getEnabledCategories -> pruneExpired), so the `> seedNow`
164
- // re-check below is defensive belt-and-suspenders. Expired/unknown categories are
165
- // SILENTLY skipped (do not log or warn on skip). Restoring the ORIGINAL absolute
166
- // expiresAtMs makes a relay restart transparent without refreshing the TTL window.
167
155
  if (Array.isArray(options.initialEnabled)) {
168
156
  const seedNow = nowFn();
169
157
  for (const entry of options.initialEnabled) {
@@ -175,13 +163,10 @@ function createDebugStore(opts) {
175
163
  }
176
164
  }
177
165
 
178
- /** @type {Map<string, number>} */
179
166
  const noisyCounters = new Map();
180
167
 
181
- /** @type {Map<string, { key: string, ts: number }>} */
182
168
  const noisyLast = new Map();
183
169
 
184
- /** @type {Array<object>} */
185
170
  const ring = new Array(capacity);
186
171
  let ringWrite = 0;
187
172
  let ringSize = 0;
@@ -509,9 +494,12 @@ function createDebugStore(opts) {
509
494
  const categorySet =
510
495
  categoriesFilter.length > 0 ? new Set(categoriesFilter) : null;
511
496
  const filtered = [];
497
+
498
+ let oldestMatchedMs = null;
512
499
  const all = getAllEvents();
513
500
  for (const evt of all) {
514
501
  if (categorySet && !categorySet.has(evt.cat)) continue;
502
+ if (oldestMatchedMs === null || evt.ts < oldestMatchedMs) oldestMatchedMs = evt.ts;
515
503
  if (sinceMs !== null && evt.ts < sinceMs) continue;
516
504
  if (untilMs !== null && evt.ts > untilMs) continue;
517
505
  filtered.push(evt);
@@ -537,6 +525,7 @@ function createDebugStore(opts) {
537
525
  events: formattedEvents,
538
526
  ringEvents: ringSize,
539
527
  ringCapacity: capacity,
528
+ oldestMatchedMs,
540
529
  };
541
530
  }
542
531
 
@@ -0,0 +1,27 @@
1
+ export const UPLOAD_CAPTURE_PRESET = [
2
+ "sdk.frames", "render.header_animation", "render.virtual_pager.diagnostics", "render.ownership",
3
+ "screen.nav", "app.lifecycle", "voice.timeline", "voice.transport",
4
+ "relay.session", "relay.protocol", "relay.health", "relay.worker.health", "relay.operation", "relay.transport",
5
+ "glasses.lifecycle", "openclaw.run", "openclaw.message", "evenai",
6
+ ];
7
+
8
+ export function startUploadCaptureArming(deps) {
9
+ if (!deps.gatesOn()) return () => {};
10
+
11
+ const preset =
12
+ deps.preset && Array.isArray(deps.preset) && deps.preset.length ? deps.preset : UPLOAD_CAPTURE_PRESET;
13
+
14
+ const armSafely = () => {
15
+ try {
16
+ deps.armCategories(preset, deps.maxTtlMs);
17
+ } catch (err) {
18
+ if (deps.onArmError) deps.onArmError(err);
19
+ }
20
+ };
21
+ armSafely();
22
+ const handle = deps.setInterval(() => {
23
+ if (deps.gatesOn()) armSafely();
24
+ }, Math.round(0.8 * deps.maxTtlMs));
25
+ handle.unref();
26
+ return () => deps.clearInterval(handle);
27
+ }
@@ -31,11 +31,6 @@ const SHARED_RULES =
31
31
  const ALLOWLIST_BLOCK =
32
32
  "Allowed emoji (copy exactly one per span):\n" + ALLOWLIST_LINES;
33
33
 
34
- /**
35
- * Compose the Channel-1 consolidated display-tags block.
36
- * @param {{emoji: boolean, pace: boolean}} opts - features ENABLED AT SESSION START
37
- * @returns {string} the block, or "" when neither feature is enabled
38
- */
39
34
  export function composeGlassesDisplaySystemPrompt(opts) {
40
35
  const emoji = !!(opts && opts.emoji);
41
36
  const pace = !!(opts && opts.pace);
@@ -25,7 +25,7 @@ test("pace-only includes dwell/skim, omits emoji + allowlist", () => {
25
25
  assert.match(out, /<dwell>/);
26
26
  assert.match(out, /<skim>/);
27
27
  assert.doesNotMatch(out, /<emoji:X>/);
28
- // allowlist must not leak when emoji is off
28
+
29
29
  assert.ok(!out.includes(MESSAGE_EMOJI_ALLOWLIST[0]));
30
30
  });
31
31
 
@@ -10,11 +10,6 @@ function truncate(value, max) {
10
10
  return value.slice(0, Math.max(0, max - 1)) + "…";
11
11
  }
12
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
13
  function summarizeGlassesUiContent(specOrPatch) {
19
14
  const o = specOrPatch && typeof specOrPatch === "object" ? specOrPatch : {};
20
15
  const rawItems = Array.isArray(o.items) ? o.items : null;
@@ -46,7 +41,6 @@ function summarizeGlassesUiContent(specOrPatch) {
46
41
 
47
42
  if (typeof o.body === "string") out.body = truncate(o.body, BODY_MAX);
48
43
 
49
- // Defensive cap: drop trailing items until under SUMMARY_MAX.
50
44
  while (
51
45
  Array.isArray(out.items) &&
52
46
  out.items.length > 1 &&
@@ -6,8 +6,7 @@ test("pointer is short and references the tool + 'see its description'", () => {
6
6
  const out = composeGlassesUiNudgeSystemPrompt();
7
7
  assert.match(out, /render_glasses_ui/);
8
8
  assert.match(out, /description/i);
9
- // ef8a821e carry: the deferred-tool search hint rides in the pointer so
10
- // Codex-harness runs (tool not in the initial list) still find the tool.
9
+
11
10
  assert.match(out, /available\/deferred tools/);
12
11
  assert.ok(out.length < 420, `pointer should stay lean, got ${out.length}`);
13
12
  });
@@ -1,10 +1,3 @@
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
1
  export const MESSAGE_EMOJI_ALLOWLIST = [
9
2
  "😂", "❤️", "🤣", "👍", "😭", "🙏", "😘", "🥰", "😍", "😊",
10
3
  "🎉", "😁", "💕", "🥺", "😅", "🔥", "☺️", "🤦", "🤷", "🙄",
@@ -6,9 +6,7 @@ import {
6
6
  const EMOJI_CLUSTER_SEGMENTER = new Intl.Segmenter(undefined, {
7
7
  granularity: "grapheme",
8
8
  });
9
- // All emoji codepoints live above U+007F, so pure-ASCII text cannot contain
10
- // any emoji and the segmenter walk is pure overhead. This regex is the
11
- // gatekeeper for the fast path below.
9
+
12
10
  const NON_ASCII_RE = /[^\x00-\x7F]/;
13
11
  const RGI_EMOJI_SEQUENCE_RE = createOptionalRegex("^\\p{RGI_Emoji}$", "v");
14
12
  const EMOJI_CLUSTER_FALLBACK_RE =
@@ -103,10 +101,7 @@ function emitGapForTextEnd(mode, beforeRemoved, afterRemoved, removedEmojiInGap)
103
101
  }
104
102
 
105
103
  function filterAsciiDisplayFast(text) {
106
- // Mirrors the segmenter walk for pure-ASCII display mode:
107
- // - whitespace immediately before a newline is dropped
108
- // - other [ \t]+ runs collapse to a single space
109
- // - trailing horizontal whitespace at end-of-text is stripped
104
+
110
105
  return text
111
106
  .replace(/[ \t]+(\r\n|\r|\n)/g, "$1")
112
107
  .replace(/[ \t]+/g, " ")
@@ -114,8 +109,7 @@ function filterAsciiDisplayFast(text) {
114
109
  }
115
110
 
116
111
  function filterAsciiRawFast(text) {
117
- // Raw mode preserves inline whitespace; only the post-loop trailing-trim
118
- // remains for pure-ASCII input.
112
+
119
113
  return text.replace(/[ \t]+$/g, "");
120
114
  }
121
115
 
@@ -35,18 +35,18 @@ export const EMOJI_TAG_FAMILY_CONFIG = {
35
35
 
36
36
  matchTrailingPartial(input, _suffixStart) {
37
37
  const n = input.length;
38
- // 1) Trailing prefix of OPEN_PREFIX
38
+
39
39
  for (let len = Math.min(n, OPEN_PREFIX.length - 1); len > 0; len--) {
40
40
  const tail = input.slice(n - len);
41
41
  if (OPEN_PREFIX.startsWith(tail)) return len;
42
42
  }
43
- // 2) Trailing OPEN_PREFIX + bytes not yet terminated by `>`
43
+
44
44
  const lastOpen = input.lastIndexOf(OPEN_PREFIX);
45
45
  if (lastOpen !== -1) {
46
46
  const after = input.indexOf(">", lastOpen + OPEN_PREFIX.length);
47
47
  if (after === -1) return n - lastOpen;
48
48
  }
49
- // 3) Trailing prefix of CLOSE_TAG
49
+
50
50
  for (let len = Math.min(n, CLOSE_TAG.length - 1); len > 0; len--) {
51
51
  const tail = input.slice(n - len);
52
52
  if (CLOSE_TAG.startsWith(tail)) return len;
@@ -7,22 +7,13 @@ const PACE_STOP =
7
7
  const RENDER_GATE =
8
8
  "No glasses display is connected right now; do not call render_glasses_ui.";
9
9
 
10
- /**
11
- * Compose the Channel-2 (before_prompt_build → appendSystemContext) fragment.
12
- * Returns undefined when nothing needs saying (the common case).
13
- *
14
- * @param {{startEnabled:{emoji:boolean,pace:boolean},
15
- * currentEnabled:{emoji:boolean,pace:boolean},
16
- * glassesConnected:boolean}} input
17
- * @returns {string|undefined}
18
- */
19
10
  export function composeChannelTwoFragment(input) {
20
11
  const start = (input && input.startEnabled) || { emoji: false, pace: false };
21
12
  const current = (input && input.currentEnabled) || { emoji: false, pace: false };
22
13
  const glassesConnected = !!(input && input.glassesConnected);
23
14
 
24
15
  const parts = [];
25
- // Stop-notice only when a feature was ENABLED at start and is now OFF.
16
+
26
17
  if (start.emoji && !current.emoji) parts.push(EMOJI_STOP);
27
18
  if (start.pace && !current.pace) parts.push(PACE_STOP);
28
19
  if (!glassesConnected) parts.push(RENDER_GATE);
@@ -1,30 +1,10 @@
1
1
  import { computeCodeSpanRegions } from "./code-span-regions.js";
2
2
 
3
- /**
4
- * @typedef {Object} TagFamilyConfig
5
- * @property {string} name
6
- * @property {ReadonlyArray<string>} closeLiterals
7
- * @property {(input: string, atOffset: number) => { consumed: number, spanInit: object } | null} matchOpen
8
- * @property {(input: string, atOffset: number) => { consumed: number, closeKind: any } | null} matchClose
9
- * @property {(activeOpen: object, closeKind: any) => boolean} closeMatches
10
- * @property {(input: string, suffixStart: number) => number} matchTrailingPartial
11
- * @property {(spanInit: object) => boolean} [validateOpen]
12
- * @property {(spanInit: object) => void} [onRejected]
13
- */
14
-
15
- /**
16
- * @param {string} accumulatedText
17
- * @param {ReadonlyArray<TagFamilyConfig>} families
18
- * @returns {{ cleanText: string, spansByFamily: Record<string, object[]>, trailingPartialTag: boolean }}
19
- */
20
3
  export function parseTaggedSpans(accumulatedText, families) {
21
4
  const spansByFamily = {};
22
5
  const activeOpens = new Map();
23
6
  for (const fam of families) spansByFamily[fam.name] = [];
24
7
 
25
- // Compute holdback once: each family declares how many trailing bytes look
26
- // like the start of one of its tags; max wins. Held-back bytes are kept
27
- // out of the parse loop so they neither become spans nor leak verbatim.
28
8
  let holdback = 0;
29
9
  for (const fam of families) {
30
10
  const vote = fam.matchTrailingPartial(accumulatedText, 0);
@@ -32,8 +12,6 @@ export function parseTaggedSpans(accumulatedText, families) {
32
12
  }
33
13
  const scanEnd = accumulatedText.length - holdback;
34
14
 
35
- // Tags quoted inside markdown code (inline backticks / fenced blocks) are
36
- // literal text, not live tags — copy those regions through verbatim.
37
15
  const codeRegions = computeCodeSpanRegions(accumulatedText);
38
16
  let codeRegionIdx = 0;
39
17
 
@@ -58,7 +36,7 @@ export function parseTaggedSpans(accumulatedText, families) {
58
36
  continue;
59
37
  }
60
38
  if (accumulatedText[i] === "<") {
61
- // Try each family's matchClose, then matchOpen, in registration order.
39
+
62
40
  for (const fam of families) {
63
41
  const close = fam.matchClose(accumulatedText, i);
64
42
  if (close) {
@@ -71,7 +49,7 @@ export function parseTaggedSpans(accumulatedText, families) {
71
49
  });
72
50
  activeOpens.delete(fam.name);
73
51
  }
74
- // mismatched or dangling close → drop silently
52
+
75
53
  i += close.consumed;
76
54
  continue outer;
77
55
  }
@@ -79,7 +57,7 @@ export function parseTaggedSpans(accumulatedText, families) {
79
57
  for (const fam of families) {
80
58
  const open = fam.matchOpen(accumulatedText, i);
81
59
  if (open) {
82
- // Latest-wins same-family: close any active span at current offset.
60
+
83
61
  const prior = activeOpens.get(fam.name);
84
62
  if (prior) {
85
63
  spansByFamily[fam.name].push({
@@ -104,7 +82,6 @@ export function parseTaggedSpans(accumulatedText, families) {
104
82
  i += 1;
105
83
  }
106
84
 
107
- // Unclosed spans → run to cleanText end.
108
85
  for (const fam of families) {
109
86
  const active = activeOpens.get(fam.name);
110
87
  if (active) {
@@ -1,8 +1,3 @@
1
- // Syntactic strip for all tagged-span grammar families.
2
- // Removes well-formed openers and closers; keeps wrapped content.
3
- // Removes orphan halves of either. Idempotent. No allowlist enforcement.
4
- // Tags quoted inside markdown code (inline backticks / fenced blocks) are
5
- // preserved verbatim so agents can talk about the tag grammar literally.
6
1
  import { computeCodeSpanRegions } from "./code-span-regions.js";
7
2
 
8
3
  const EMOJI_OPEN_RE = /<emoji:[^<>\s]+?>/g;
@@ -10,8 +5,6 @@ const EMOJI_CLOSE_RE = /<\/emoji>/g;
10
5
  const PACE_OPEN_RE = /<(?:dwell|skim)>/g;
11
6
  const PACE_CLOSE_RE = /<\/(?:dwell|skim)>/g;
12
7
 
13
- // Single alternation so one pass over the original string keeps match
14
- // offsets valid for the code-region exemption check.
15
8
  const ALL_TAGS_RE = new RegExp(
16
9
  [
17
10
  EMOJI_OPEN_RE.source,