ocuclaw 1.2.4 → 1.3.1
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 +21 -6
- package/dist/config/runtime-config.js +84 -3
- package/dist/domain/activity-status-adapter.js +138 -605
- package/dist/domain/activity-status-arbiter.js +109 -0
- package/dist/domain/activity-status-labels.js +906 -0
- package/dist/domain/code-span-regions.js +103 -0
- package/dist/domain/conversation-state.js +14 -1
- package/dist/domain/debug-store.js +56 -182
- package/dist/domain/glasses-ui-content-summary.js +62 -0
- package/dist/domain/glasses-ui-system-prompt.js +28 -0
- package/dist/domain/message-emoji-allowlist.js +16 -0
- package/dist/domain/message-emoji-filter.js +33 -55
- package/dist/domain/neural-emoji-reactor-system-prompt.js +43 -0
- package/dist/domain/neural-emoji-reactor-tag-config.js +56 -0
- package/dist/domain/neural-pace-modulator-system-prompt.js +32 -0
- package/dist/domain/neural-pace-modulator-tag-config.js +51 -0
- package/dist/domain/tagged-span-parser.js +121 -0
- package/dist/domain/tagged-span-strip.js +38 -0
- package/dist/even-ai/even-ai-endpoint.js +91 -0
- package/dist/even-ai/even-ai-run-waiter.js +14 -0
- package/dist/even-ai/even-ai-settings-store.js +14 -0
- package/dist/gateway/gateway-bridge.js +14 -2
- package/dist/gateway/gateway-timing-ledger.js +457 -0
- package/dist/gateway/openclaw-client.js +462 -38
- package/dist/index.js +28 -1
- package/dist/runtime/downstream-handler.js +754 -83
- package/dist/runtime/ocuclaw-settings-store.js +74 -31
- package/dist/runtime/plugin-version-service.js +23 -0
- package/dist/runtime/protocol-adapter.js +9 -0
- package/dist/runtime/provider-usage-select.js +168 -0
- package/dist/runtime/relay-client-nudge-controller.js +553 -0
- package/dist/runtime/relay-core.js +1293 -225
- package/dist/runtime/relay-health-monitor.js +172 -0
- package/dist/runtime/relay-operation-registry.js +263 -0
- package/dist/runtime/relay-service.js +201 -1
- package/dist/runtime/relay-worker-approval-replay-cache.js +68 -0
- package/dist/runtime/relay-worker-entry.js +32 -0
- package/dist/runtime/relay-worker-health.js +272 -0
- package/dist/runtime/relay-worker-protocol.js +281 -0
- package/dist/runtime/relay-worker-queue.js +202 -0
- package/dist/runtime/relay-worker-supervisor.js +1004 -0
- package/dist/runtime/relay-worker-transport.js +1051 -0
- package/dist/runtime/session-context-service.js +189 -0
- package/dist/runtime/session-service.js +638 -27
- package/dist/runtime/upstream-runtime.js +1167 -60
- package/dist/tools/device-info-tool.js +242 -0
- package/dist/tools/glasses-ui-cron.js +427 -0
- package/dist/tools/glasses-ui-descriptors.js +261 -0
- package/dist/tools/glasses-ui-limits.js +21 -0
- package/dist/tools/glasses-ui-paint-floor.js +99 -0
- package/dist/tools/glasses-ui-recipes.js +581 -0
- package/dist/tools/glasses-ui-surfaces.js +278 -0
- package/dist/tools/glasses-ui-template.js +182 -0
- package/dist/tools/glasses-ui-tool.js +1111 -0
- package/dist/tools/session-title-tool.js +209 -0
- package/dist/version.js +2 -0
- package/openclaw.plugin.json +163 -15
- package/package.json +14 -5
- package/skills/glasses-ui/SKILL.md +156 -0
- package/dist/runtime/downstream-server.js +0 -1891
|
@@ -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
|
-
|
|
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:
|
|
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,
|
|
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,22 +154,27 @@ 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
|
|
|
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
|
+
if (Array.isArray(options.initialEnabled)) {
|
|
168
|
+
const seedNow = nowFn();
|
|
169
|
+
for (const entry of options.initialEnabled) {
|
|
170
|
+
if (!entry || typeof entry.cat !== "string") continue;
|
|
171
|
+
if (!categories.has(entry.cat)) continue;
|
|
172
|
+
const expiresAtMs = Number(entry.expiresAtMs);
|
|
173
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= seedNow) continue;
|
|
174
|
+
enabledUntil.set(entry.cat, Math.floor(expiresAtMs));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
180
178
|
/** @type {Map<string, number>} */
|
|
181
179
|
const noisyCounters = new Map();
|
|
182
180
|
|
|
@@ -189,102 +187,6 @@ function createDebugStore(opts) {
|
|
|
189
187
|
let ringSize = 0;
|
|
190
188
|
let seq = 0;
|
|
191
189
|
|
|
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
190
|
function pruneExpired(nowMs) {
|
|
289
191
|
for (const [cat, expiresAt] of enabledUntil) {
|
|
290
192
|
if (expiresAt <= nowMs) {
|
|
@@ -403,21 +305,7 @@ function redactStringValue(value, mode, keyName) {
|
|
|
403
305
|
serialized = JSON.stringify(normalized);
|
|
404
306
|
}
|
|
405
307
|
|
|
406
|
-
|
|
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
|
-
};
|
|
308
|
+
return { data: normalized, serialized };
|
|
421
309
|
}
|
|
422
310
|
|
|
423
311
|
function allowByNoisyPolicy(cat, eventName, serializedData, ts) {
|
|
@@ -478,8 +366,7 @@ function redactStringValue(value, mode, keyName) {
|
|
|
478
366
|
: "debug";
|
|
479
367
|
|
|
480
368
|
const normalized = normalizeData(raw.data);
|
|
481
|
-
|
|
482
|
-
if (!allowByNoisyPolicy(cat, eventName, redactedDataModes.safeSerialized, ts)) {
|
|
369
|
+
if (!allowByNoisyPolicy(cat, eventName, normalized.serialized, ts)) {
|
|
483
370
|
return false;
|
|
484
371
|
}
|
|
485
372
|
|
|
@@ -489,8 +376,7 @@ function redactStringValue(value, mode, keyName) {
|
|
|
489
376
|
event: eventName,
|
|
490
377
|
severity,
|
|
491
378
|
seq: ++seq,
|
|
492
|
-
|
|
493
|
-
dataFull: redactedDataModes.full,
|
|
379
|
+
data: normalized.data,
|
|
494
380
|
};
|
|
495
381
|
|
|
496
382
|
if (typeof raw.sessionKey === "string" && raw.sessionKey) {
|
|
@@ -517,17 +403,14 @@ function redactStringValue(value, mode, keyName) {
|
|
|
517
403
|
return out;
|
|
518
404
|
}
|
|
519
405
|
|
|
520
|
-
function formatEventForDump(evt
|
|
406
|
+
function formatEventForDump(evt) {
|
|
521
407
|
const out = {
|
|
522
408
|
ts: evt.ts,
|
|
523
409
|
cat: evt.cat,
|
|
524
410
|
event: evt.event,
|
|
525
411
|
severity: evt.severity,
|
|
526
412
|
seq: evt.seq,
|
|
527
|
-
data:
|
|
528
|
-
redactionMode === REDACTION_FULL
|
|
529
|
-
? evt.dataFull
|
|
530
|
-
: evt.dataSafe,
|
|
413
|
+
data: evt.data,
|
|
531
414
|
};
|
|
532
415
|
|
|
533
416
|
if (typeof evt.sessionKey === "string" && evt.sessionKey) {
|
|
@@ -546,13 +429,6 @@ function redactStringValue(value, mode, keyName) {
|
|
|
546
429
|
function dump(request) {
|
|
547
430
|
const req = request || {};
|
|
548
431
|
const nowMs = nowFn();
|
|
549
|
-
const redaction = normalizeRedactionMode(req.redaction);
|
|
550
|
-
if (!redaction) {
|
|
551
|
-
return {
|
|
552
|
-
ok: false,
|
|
553
|
-
error: "debug-dump redaction must be one of: safe, full",
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
432
|
const categoriesFilter = expandCategoryAliases(
|
|
557
433
|
normalizeCategoryList(req.categories),
|
|
558
434
|
);
|
|
@@ -604,7 +480,10 @@ function redactStringValue(value, mode, keyName) {
|
|
|
604
480
|
};
|
|
605
481
|
}
|
|
606
482
|
|
|
607
|
-
const limit =
|
|
483
|
+
const limit =
|
|
484
|
+
req.limit !== undefined && Number.isFinite(Number(req.limit)) && Number(req.limit) > 0
|
|
485
|
+
? Math.floor(Number(req.limit))
|
|
486
|
+
: null;
|
|
608
487
|
const sinceMs =
|
|
609
488
|
req.sinceMs !== undefined
|
|
610
489
|
? Math.floor(Number(req.sinceMs))
|
|
@@ -639,26 +518,25 @@ function redactStringValue(value, mode, keyName) {
|
|
|
639
518
|
}
|
|
640
519
|
|
|
641
520
|
const events =
|
|
642
|
-
filtered.length > limit
|
|
521
|
+
limit !== null && filtered.length > limit
|
|
643
522
|
? filtered.slice(filtered.length - limit)
|
|
644
523
|
: filtered;
|
|
645
|
-
const formattedEvents = events.map((evt) =>
|
|
646
|
-
formatEventForDump(evt, redaction),
|
|
647
|
-
);
|
|
524
|
+
const formattedEvents = events.map((evt) => formatEventForDump(evt));
|
|
648
525
|
|
|
649
526
|
return {
|
|
650
527
|
ok: true,
|
|
651
528
|
nowMs,
|
|
652
529
|
sinceMs,
|
|
653
530
|
untilMs,
|
|
654
|
-
redaction,
|
|
655
531
|
categories: categoriesFilter,
|
|
656
|
-
limit,
|
|
532
|
+
limit: limit === null ? undefined : limit,
|
|
657
533
|
totalMatched: filtered.length,
|
|
658
534
|
returned: formattedEvents.length,
|
|
659
535
|
dropped: Math.max(0, filtered.length - formattedEvents.length),
|
|
660
536
|
enabled: getEnabledCategories(nowMs),
|
|
661
537
|
events: formattedEvents,
|
|
538
|
+
ringEvents: ringSize,
|
|
539
|
+
ringCapacity: capacity,
|
|
662
540
|
};
|
|
663
541
|
}
|
|
664
542
|
|
|
@@ -683,12 +561,8 @@ function redactStringValue(value, mode, keyName) {
|
|
|
683
561
|
getConfig() {
|
|
684
562
|
return {
|
|
685
563
|
capacity,
|
|
686
|
-
payloadMaxBytes,
|
|
687
564
|
defaultTtlMs,
|
|
688
565
|
maxTtlMs,
|
|
689
|
-
dumpDefaultLimit,
|
|
690
|
-
dumpMaxLimit,
|
|
691
|
-
defaultRedaction: DEFAULT_REDACTION_MODE,
|
|
692
566
|
};
|
|
693
567
|
},
|
|
694
568
|
};
|
|
@@ -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);
|