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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|