ocuclaw 1.3.2 → 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 +93 -0
- 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 +657 -271
- package/dist/runtime/relay-service.js +40 -36
- 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 +109 -39
- package/dist/runtime/relay-worker-transport.js +157 -15
- 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 +58 -63
- 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 +22 -34
- package/dist/tools/glasses-ui-recipes.js +92 -101
- package/dist/tools/glasses-ui-surfaces.js +295 -100
- 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 +475 -331
- package/dist/tools/glasses-ui-voicemail.js +242 -0
- package/dist/tools/glasses-ui-wake.js +195 -0
- 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/skills/glasses-ui/SKILL.md +19 -3
- package/dist/runtime/protocol-adapter.js +0 -387
|
@@ -2,8 +2,6 @@ import { filterDisplayEmojiText } from "./message-emoji-filter.js";
|
|
|
2
2
|
import { stripAllTaggedSpans } from "./tagged-span-strip.js";
|
|
3
3
|
import { marked } from "marked";
|
|
4
4
|
|
|
5
|
-
// --- Constants ---
|
|
6
|
-
|
|
7
5
|
const DEFAULT_AGENT_NAME = "Agent";
|
|
8
6
|
const REPLY_DIRECTIVE_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:\s*[^\]\n]+)\s*\]\]/gi;
|
|
9
7
|
const REPLY_DIRECTIVE_SENTINEL = "\u0000";
|
|
@@ -22,7 +20,6 @@ const SYNTHETIC_SESSION_INSTRUCTION_PATTERNS = [
|
|
|
22
20
|
/\bdo not mention\b/,
|
|
23
21
|
/\binternal\b.*\b(?:steps|files|tools|reasoning)\b/,
|
|
24
22
|
];
|
|
25
|
-
// --- State ---
|
|
26
23
|
|
|
27
24
|
let messages = [];
|
|
28
25
|
let agentName = DEFAULT_AGENT_NAME;
|
|
@@ -30,13 +27,6 @@ let displayEntries = [];
|
|
|
30
27
|
let cachedTranscript = "";
|
|
31
28
|
let transcriptDirty = false;
|
|
32
29
|
|
|
33
|
-
/**
|
|
34
|
-
* Build a display entry for a single message, or null if filtered out.
|
|
35
|
-
*
|
|
36
|
-
* @param {{role: string, content: string|Array, name?: string}} msg
|
|
37
|
-
* @param {{isFirstVisibleEntry?: boolean}} [options]
|
|
38
|
-
* @returns {{role: "user"|"assistant", text: string, name: string|null}|null}
|
|
39
|
-
*/
|
|
40
30
|
function buildDisplayEntry(msg, options = {}) {
|
|
41
31
|
if (!msg || (msg.role !== "user" && msg.role !== "assistant")) return null;
|
|
42
32
|
|
|
@@ -83,10 +73,6 @@ function countSyntheticSessionInstructionSignals(normalizedText) {
|
|
|
83
73
|
return count;
|
|
84
74
|
}
|
|
85
75
|
|
|
86
|
-
/**
|
|
87
|
-
* Detect OpenClaw's synthetic bare /new or /reset starter prompt.
|
|
88
|
-
* This is heuristic-based (signals + shape), not exact whole-string matching.
|
|
89
|
-
*/
|
|
90
76
|
function isLikelySyntheticSessionStarterPrompt(text) {
|
|
91
77
|
const normalized = normalizeSessionStarterCandidate(text);
|
|
92
78
|
if (!normalized) return false;
|
|
@@ -100,21 +86,12 @@ function isLikelySyntheticSessionStarterPrompt(text) {
|
|
|
100
86
|
return countSyntheticSessionInstructionSignals(normalized) >= 2;
|
|
101
87
|
}
|
|
102
88
|
|
|
103
|
-
/**
|
|
104
|
-
* Format a display entry with role prefix.
|
|
105
|
-
*
|
|
106
|
-
* @param {{role: "user"|"assistant", text: string, name: string|null}} entry
|
|
107
|
-
* @returns {string}
|
|
108
|
-
*/
|
|
109
89
|
function formatEntry(entry) {
|
|
110
90
|
if (entry.role === "user") return `• ${entry.text}`;
|
|
111
91
|
const name = entry.name || agentName;
|
|
112
92
|
return `${name}: ${entry.text}`;
|
|
113
93
|
}
|
|
114
94
|
|
|
115
|
-
/**
|
|
116
|
-
* Rebuild derived display cache from raw messages.
|
|
117
|
-
*/
|
|
118
95
|
function rebuildDisplayCache() {
|
|
119
96
|
displayEntries = [];
|
|
120
97
|
for (const msg of messages) {
|
|
@@ -126,11 +103,6 @@ function rebuildDisplayCache() {
|
|
|
126
103
|
transcriptDirty = true;
|
|
127
104
|
}
|
|
128
105
|
|
|
129
|
-
/**
|
|
130
|
-
* Return the current transcript string, rebuilding lazily if needed.
|
|
131
|
-
*
|
|
132
|
-
* @returns {string}
|
|
133
|
-
*/
|
|
134
106
|
function getTranscript() {
|
|
135
107
|
if (!transcriptDirty) return cachedTranscript;
|
|
136
108
|
cachedTranscript = displayEntries.map((entry) => formatEntry(entry)).join("\n\n");
|
|
@@ -138,12 +110,6 @@ function getTranscript() {
|
|
|
138
110
|
return cachedTranscript;
|
|
139
111
|
}
|
|
140
112
|
|
|
141
|
-
// --- Markdown Pipeline ---
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Remove OpenClaw inline reply directives from assistant text before display.
|
|
145
|
-
* These control transport threading and should never be user-visible.
|
|
146
|
-
*/
|
|
147
113
|
function stripReplyDirectives(text) {
|
|
148
114
|
if (!text) return "";
|
|
149
115
|
|
|
@@ -162,9 +128,6 @@ function stripReplyDirectives(text) {
|
|
|
162
128
|
.trimEnd();
|
|
163
129
|
}
|
|
164
130
|
|
|
165
|
-
/**
|
|
166
|
-
* Extract plain text from an array of marked inline tokens.
|
|
167
|
-
*/
|
|
168
131
|
function renderInlineTokens(tokens) {
|
|
169
132
|
let out = "";
|
|
170
133
|
for (const token of tokens) {
|
|
@@ -186,13 +149,13 @@ function renderInlineTokens(tokens) {
|
|
|
186
149
|
out += token.text || "";
|
|
187
150
|
break;
|
|
188
151
|
case "html":
|
|
189
|
-
|
|
152
|
+
|
|
190
153
|
break;
|
|
191
154
|
case "image":
|
|
192
155
|
out += token.text || token.title || "";
|
|
193
156
|
break;
|
|
194
157
|
default:
|
|
195
|
-
|
|
158
|
+
|
|
196
159
|
if (token.text) out += token.text;
|
|
197
160
|
break;
|
|
198
161
|
}
|
|
@@ -200,10 +163,6 @@ function renderInlineTokens(tokens) {
|
|
|
200
163
|
return out;
|
|
201
164
|
}
|
|
202
165
|
|
|
203
|
-
/**
|
|
204
|
-
* Extract plain text from an array of marked block tokens.
|
|
205
|
-
* Returns an array of text blocks (each block is a paragraph-level chunk).
|
|
206
|
-
*/
|
|
207
166
|
function renderBlockTokens(tokens) {
|
|
208
167
|
const blocks = [];
|
|
209
168
|
|
|
@@ -218,10 +177,7 @@ function renderBlockTokens(tokens) {
|
|
|
218
177
|
break;
|
|
219
178
|
|
|
220
179
|
case "text":
|
|
221
|
-
|
|
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.
|
|
180
|
+
|
|
225
181
|
blocks.push(token.tokens ? renderInlineTokens(token.tokens) : token.text);
|
|
226
182
|
break;
|
|
227
183
|
|
|
@@ -251,11 +207,11 @@ function renderBlockTokens(tokens) {
|
|
|
251
207
|
|
|
252
208
|
case "table": {
|
|
253
209
|
const rows = [];
|
|
254
|
-
|
|
210
|
+
|
|
255
211
|
rows.push(
|
|
256
212
|
token.header.map((cell) => renderInlineTokens(cell.tokens)).join(" | ")
|
|
257
213
|
);
|
|
258
|
-
|
|
214
|
+
|
|
259
215
|
for (const row of token.rows) {
|
|
260
216
|
rows.push(
|
|
261
217
|
row.map((cell) => renderInlineTokens(cell.tokens)).join(" | ")
|
|
@@ -266,19 +222,19 @@ function renderBlockTokens(tokens) {
|
|
|
266
222
|
}
|
|
267
223
|
|
|
268
224
|
case "hr":
|
|
269
|
-
|
|
225
|
+
|
|
270
226
|
break;
|
|
271
227
|
|
|
272
228
|
case "space":
|
|
273
|
-
|
|
229
|
+
|
|
274
230
|
break;
|
|
275
231
|
|
|
276
232
|
case "html":
|
|
277
|
-
|
|
233
|
+
|
|
278
234
|
break;
|
|
279
235
|
|
|
280
236
|
default:
|
|
281
|
-
|
|
237
|
+
|
|
282
238
|
if (token.tokens) {
|
|
283
239
|
blocks.push(...renderBlockTokens(token.tokens));
|
|
284
240
|
} else if (token.text) {
|
|
@@ -297,14 +253,6 @@ function cleanupDisplayWhitespace(text) {
|
|
|
297
253
|
.replace(/[ \t]+$/gm, "");
|
|
298
254
|
}
|
|
299
255
|
|
|
300
|
-
/**
|
|
301
|
-
* Convert Markdown text to plain text.
|
|
302
|
-
* Uses marked.lexer to parse, then walks the AST to extract text.
|
|
303
|
-
*
|
|
304
|
-
* @param {string} markdown
|
|
305
|
-
* @param {{ stripReplyTags?: boolean }} [options]
|
|
306
|
-
* @returns {{ text: string }}
|
|
307
|
-
*/
|
|
308
256
|
function markdownToPlainText(markdown, options = {}) {
|
|
309
257
|
if (!markdown) return { text: "" };
|
|
310
258
|
const source = options.stripReplyTags ? stripReplyDirectives(markdown) : markdown;
|
|
@@ -319,15 +267,6 @@ function markdownToPlainText(markdown, options = {}) {
|
|
|
319
267
|
return { text };
|
|
320
268
|
}
|
|
321
269
|
|
|
322
|
-
// --- Content Extraction ---
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Extract displayable text from a message's content field.
|
|
326
|
-
* Content can be a string or an array of content blocks.
|
|
327
|
-
*
|
|
328
|
-
* @param {string|Array} content
|
|
329
|
-
* @returns {string|null} Text content, or null if no displayable text
|
|
330
|
-
*/
|
|
331
270
|
function extractText(content) {
|
|
332
271
|
if (typeof content === "string") {
|
|
333
272
|
return content || null;
|
|
@@ -352,12 +291,6 @@ function extractText(content) {
|
|
|
352
291
|
return `[Image] ${text}`;
|
|
353
292
|
}
|
|
354
293
|
|
|
355
|
-
// --- Message Filtering ---
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Filter messages for display and format them with name prefixes.
|
|
359
|
-
* Returns an array of formatted message strings.
|
|
360
|
-
*/
|
|
361
294
|
function filterAndFormat() {
|
|
362
295
|
return displayEntries.map((entry) => ({
|
|
363
296
|
text: formatEntry(entry),
|
|
@@ -365,16 +298,6 @@ function filterAndFormat() {
|
|
|
365
298
|
}));
|
|
366
299
|
}
|
|
367
300
|
|
|
368
|
-
// --- Turn Grouping ---
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Group chronological formatted messages into turns.
|
|
372
|
-
* Each user message starts a new turn. Assistant messages before the first
|
|
373
|
-
* user message form their own turn.
|
|
374
|
-
*
|
|
375
|
-
* @param {Array<{text: string, role: string}>} formatted
|
|
376
|
-
* @returns {Array<Array<{text: string, role: string}>>}
|
|
377
|
-
*/
|
|
378
301
|
function groupIntoTurns(formatted) {
|
|
379
302
|
const turns = [];
|
|
380
303
|
let current = [];
|
|
@@ -394,49 +317,22 @@ function groupIntoTurns(formatted) {
|
|
|
394
317
|
return turns;
|
|
395
318
|
}
|
|
396
319
|
|
|
397
|
-
// --- Pagination ---
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Paginate conversation into chronological rolling pages.
|
|
401
|
-
* All messages are joined in order, with newest content at the tail of the
|
|
402
|
-
* resulting string.
|
|
403
|
-
*
|
|
404
|
-
* Relay intentionally does not char-split the transcript. Client render code
|
|
405
|
-
* owns virtual-page splitting.
|
|
406
|
-
*
|
|
407
|
-
* @returns {Array<{content: string, subPage: [number, number]|null, turn: null}>}
|
|
408
|
-
*/
|
|
409
320
|
function paginate() {
|
|
410
321
|
if (displayEntries.length === 0) return [];
|
|
411
322
|
|
|
412
|
-
// Join all messages chronologically (no reversal)
|
|
413
323
|
const allText = getTranscript();
|
|
414
324
|
|
|
415
325
|
return [{ content: allText, subPage: null, turn: null }];
|
|
416
326
|
}
|
|
417
327
|
|
|
418
|
-
// --- Public API ---
|
|
419
|
-
|
|
420
328
|
const conversationState = {
|
|
421
|
-
|
|
422
|
-
* Bulk load messages from chat.history and set agent name.
|
|
423
|
-
*
|
|
424
|
-
* @param {Array<{role: string, content: string|Array}>} msgs
|
|
425
|
-
* @param {string} [name] - Agent name for display prefix
|
|
426
|
-
*/
|
|
329
|
+
|
|
427
330
|
hydrate(msgs, name) {
|
|
428
331
|
messages = Array.isArray(msgs) ? [...msgs] : [];
|
|
429
332
|
if (name) agentName = name;
|
|
430
333
|
rebuildDisplayCache();
|
|
431
334
|
},
|
|
432
335
|
|
|
433
|
-
/**
|
|
434
|
-
* Append a single message.
|
|
435
|
-
*
|
|
436
|
-
* @param {string} role
|
|
437
|
-
* @param {string|Array} content
|
|
438
|
-
* @param {string} [name] - Optional display name override (used for simulate)
|
|
439
|
-
*/
|
|
440
336
|
addMessage(role, content, name) {
|
|
441
337
|
const msg = { role, content };
|
|
442
338
|
if (name) msg.name = name;
|
|
@@ -457,11 +353,22 @@ const conversationState = {
|
|
|
457
353
|
}
|
|
458
354
|
},
|
|
459
355
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
356
|
+
replaceLatestUserMessage(content, name) {
|
|
357
|
+
let index = messages.length - 1;
|
|
358
|
+
while (index >= 0 && messages[index].role !== "user") {
|
|
359
|
+
index -= 1;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const msg = { role: "user", content };
|
|
363
|
+
if (name) msg.name = name;
|
|
364
|
+
|
|
365
|
+
if (index >= 0) {
|
|
366
|
+
messages = messages.slice(0, index);
|
|
367
|
+
}
|
|
368
|
+
messages.push(msg);
|
|
369
|
+
rebuildDisplayCache();
|
|
370
|
+
},
|
|
371
|
+
|
|
465
372
|
setAgentName(name) {
|
|
466
373
|
const next = name || DEFAULT_AGENT_NAME;
|
|
467
374
|
if (agentName === next) return;
|
|
@@ -469,36 +376,18 @@ const conversationState = {
|
|
|
469
376
|
transcriptDirty = true;
|
|
470
377
|
},
|
|
471
378
|
|
|
472
|
-
/**
|
|
473
|
-
* Return paginated, filtered, markdown-stripped page array.
|
|
474
|
-
*
|
|
475
|
-
* @returns {Array<{content: string, subPage: [number, number]|null}>}
|
|
476
|
-
*/
|
|
477
379
|
getPages() {
|
|
478
380
|
return paginate();
|
|
479
381
|
},
|
|
480
382
|
|
|
481
|
-
/**
|
|
482
|
-
* Return the number of pages.
|
|
483
|
-
*
|
|
484
|
-
* @returns {number}
|
|
485
|
-
*/
|
|
486
383
|
getPageCount() {
|
|
487
384
|
return displayEntries.length > 0 ? 1 : 0;
|
|
488
385
|
},
|
|
489
386
|
|
|
490
|
-
/**
|
|
491
|
-
* Return the full unfiltered transcript.
|
|
492
|
-
*
|
|
493
|
-
* @returns {Array<{role: string, content: string|Array}>}
|
|
494
|
-
*/
|
|
495
387
|
getRawMessages() {
|
|
496
388
|
return [...messages];
|
|
497
389
|
},
|
|
498
390
|
|
|
499
|
-
/**
|
|
500
|
-
* Reset all state.
|
|
501
|
-
*/
|
|
502
391
|
clear() {
|
|
503
392
|
messages = [];
|
|
504
393
|
agentName = DEFAULT_AGENT_NAME;
|
|
@@ -507,7 +396,6 @@ const conversationState = {
|
|
|
507
396
|
transcriptDirty = false;
|
|
508
397
|
},
|
|
509
398
|
|
|
510
|
-
// Exposed for testing
|
|
511
399
|
_markdownToPlainText: markdownToPlainText,
|
|
512
400
|
_extractText: extractText,
|
|
513
401
|
_isLikelySyntheticSessionStarterPrompt: isLikelySyntheticSessionStarterPrompt,
|
|
@@ -516,6 +404,7 @@ const conversationState = {
|
|
|
516
404
|
export const {
|
|
517
405
|
hydrate,
|
|
518
406
|
addMessage,
|
|
407
|
+
replaceLatestUserMessage,
|
|
519
408
|
setAgentName,
|
|
520
409
|
getPages,
|
|
521
410
|
getPageCount,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export function createBundleCache(opts) {
|
|
2
|
+
const maxEntries = Math.max(1, opts.maxEntries | 0);
|
|
3
|
+
const ttlMs = Number.isFinite(opts.ttlMs) && opts.ttlMs >= 0 ? opts.ttlMs : 5 * 60_000;
|
|
4
|
+
const now = typeof opts.now === "function" ? opts.now : () => Date.now();
|
|
5
|
+
|
|
6
|
+
const store = new Map();
|
|
7
|
+
|
|
8
|
+
function isExpired(entry) {
|
|
9
|
+
if (ttlMs === 0) return false;
|
|
10
|
+
return now() - entry.cachedMs > ttlMs;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function dropExpired() {
|
|
14
|
+
for (const [id, entry] of store) {
|
|
15
|
+
if (isExpired(entry)) store.delete(id);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function put(id, entry) {
|
|
20
|
+
dropExpired();
|
|
21
|
+
if (store.has(id)) store.delete(id);
|
|
22
|
+
store.set(id, entry);
|
|
23
|
+
while (store.size > maxEntries) {
|
|
24
|
+
const firstKey = store.keys().next().value;
|
|
25
|
+
store.delete(firstKey);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function get(id) {
|
|
30
|
+
const entry = store.get(id);
|
|
31
|
+
if (!entry) return null;
|
|
32
|
+
if (isExpired(entry)) {
|
|
33
|
+
store.delete(id);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return entry;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function del(id) {
|
|
40
|
+
store.delete(id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function size() {
|
|
44
|
+
return store.size;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sweep() {
|
|
48
|
+
dropExpired();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { put, get, delete: del, size, sweep };
|
|
52
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function sanitizeCategoryFilename(cat) {
|
|
2
|
+
return cat.replace(/\./g, "-") + ".jsonl";
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function bucketEventsToFiles(input) {
|
|
6
|
+
const { events, ringEvents, ringCapacity, appliedQuery } = input;
|
|
7
|
+
const byCategory = new Map();
|
|
8
|
+
for (const evt of events) {
|
|
9
|
+
if (!byCategory.has(evt.cat)) byCategory.set(evt.cat, []);
|
|
10
|
+
byCategory.get(evt.cat).push(evt);
|
|
11
|
+
}
|
|
12
|
+
const files = new Map();
|
|
13
|
+
const categories = [];
|
|
14
|
+
let overallFrom = null;
|
|
15
|
+
let overallTo = null;
|
|
16
|
+
let totalBytes = 0;
|
|
17
|
+
for (const [cat, list] of byCategory) {
|
|
18
|
+
list.sort((a, b) => a.ts - b.ts || (a.seq || 0) - (b.seq || 0));
|
|
19
|
+
const filename = sanitizeCategoryFilename(cat);
|
|
20
|
+
let bytes = 0;
|
|
21
|
+
let content = "";
|
|
22
|
+
for (const evt of list) {
|
|
23
|
+
const line = JSON.stringify(evt) + "\n";
|
|
24
|
+
content += line;
|
|
25
|
+
bytes += Buffer.byteLength(line, "utf8");
|
|
26
|
+
}
|
|
27
|
+
files.set(filename, content);
|
|
28
|
+
totalBytes += bytes;
|
|
29
|
+
const fromMs = list[0].ts;
|
|
30
|
+
const toMs = list[list.length - 1].ts;
|
|
31
|
+
if (overallFrom === null || fromMs < overallFrom) overallFrom = fromMs;
|
|
32
|
+
if (overallTo === null || toMs > overallTo) overallTo = toMs;
|
|
33
|
+
categories.push({ cat, count: list.length, bytes, fromMs, toMs, file: filename });
|
|
34
|
+
}
|
|
35
|
+
const summary = {
|
|
36
|
+
ringEvents, ringCapacity, totalBytes,
|
|
37
|
+
timeRange: overallFrom !== null && overallTo !== null
|
|
38
|
+
? { fromMs: overallFrom, toMs: overallTo, spanMs: overallTo - overallFrom } : null,
|
|
39
|
+
categories, appliedQuery,
|
|
40
|
+
};
|
|
41
|
+
return { files, summary };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function renderBundleReadme(summary) {
|
|
45
|
+
const lines = [];
|
|
46
|
+
lines.push("# Debug dump", "");
|
|
47
|
+
lines.push("See `.agents/skills/ocuclaw-debug/SKILL.md` → 'Analyzing bucketed dumps' for usage guidance.", "");
|
|
48
|
+
lines.push(`Ring: ${summary.ringEvents} / ${summary.ringCapacity}`);
|
|
49
|
+
lines.push(`Total bytes: ${summary.totalBytes}`);
|
|
50
|
+
if (summary.timeRange) lines.push(`Time range: ${summary.timeRange.fromMs} → ${summary.timeRange.toMs} (${summary.timeRange.spanMs} ms)`);
|
|
51
|
+
lines.push("", "## Buckets", "");
|
|
52
|
+
for (const c of summary.categories) {
|
|
53
|
+
const eventStr = c.count === 1 ? "1 event" : `${c.count} events`;
|
|
54
|
+
lines.push(`- \`${c.file}\` — ${eventStr}, ${c.bytes} bytes, ${c.fromMs} → ${c.toMs}`);
|
|
55
|
+
}
|
|
56
|
+
if (summary.categories.length === 0) lines.push("- (no events matched the query)");
|
|
57
|
+
lines.push("", "## Cross-category timeline", "");
|
|
58
|
+
lines.push("Per-category files are chronological WITHIN a category only. To reconstruct the global stream, merge-sort all `*.jsonl` by `(ts, seq)` — `seq` is the global monotonic tie-break that makes a same-`ts` merge deterministic.");
|
|
59
|
+
return lines.join("\n") + "\n";
|
|
60
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export function buildBundlePreview(files, opts) {
|
|
2
|
+
const maxEvents = (opts && typeof opts.maxEvents === "number" && opts.maxEvents > 0) ? opts.maxEvents : 15;
|
|
3
|
+
const maxCharsPerEvent = (opts && typeof opts.maxCharsPerEvent === "number" && opts.maxCharsPerEvent > 0) ? opts.maxCharsPerEvent : 80;
|
|
4
|
+
|
|
5
|
+
const SKIP = new Set(["README.md", "metadata.json", "correlation-liveui.jsonl"]);
|
|
6
|
+
|
|
7
|
+
const catParsed = [];
|
|
8
|
+
|
|
9
|
+
const catNames = [];
|
|
10
|
+
|
|
11
|
+
if (files && typeof files.forEach === "function") {
|
|
12
|
+
files.forEach((content, filename) => {
|
|
13
|
+
if (SKIP.has(filename)) return;
|
|
14
|
+
if (!filename.endsWith(".jsonl")) return;
|
|
15
|
+
|
|
16
|
+
const cat = filename.slice(0, -(".jsonl".length));
|
|
17
|
+
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
18
|
+
|
|
19
|
+
const parsed = [];
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
let obj;
|
|
22
|
+
try {
|
|
23
|
+
obj = JSON.parse(line);
|
|
24
|
+
} catch {
|
|
25
|
+
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
parsed.push(obj);
|
|
29
|
+
}
|
|
30
|
+
if (parsed.length > 0) {
|
|
31
|
+
catParsed.push(parsed);
|
|
32
|
+
catNames.push(cat);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const events = [];
|
|
38
|
+
|
|
39
|
+
let totalParsed = 0;
|
|
40
|
+
for (const parsed of catParsed) {
|
|
41
|
+
totalParsed += parsed.length;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (catParsed.length > 0) {
|
|
45
|
+
const indices = new Array(catParsed.length).fill(0);
|
|
46
|
+
let added = 0;
|
|
47
|
+
let anyProgress = true;
|
|
48
|
+
while (added < maxEvents && anyProgress) {
|
|
49
|
+
anyProgress = false;
|
|
50
|
+
for (let ci = 0; ci < catParsed.length && added < maxEvents; ci++) {
|
|
51
|
+
const idx = indices[ci];
|
|
52
|
+
if (idx >= catParsed[ci].length) continue;
|
|
53
|
+
indices[ci] = idx + 1;
|
|
54
|
+
anyProgress = true;
|
|
55
|
+
const parsed = catParsed[ci][idx];
|
|
56
|
+
const ts = (parsed && typeof parsed.ts === "number") ? parsed.ts : 0;
|
|
57
|
+
const cat = (parsed && typeof parsed.cat === "string" && parsed.cat) ? parsed.cat : catNames[ci];
|
|
58
|
+
const text = extractText(parsed, maxCharsPerEvent);
|
|
59
|
+
events.push({ ts, cat, text });
|
|
60
|
+
added++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const truncated = totalParsed > events.length;
|
|
66
|
+
|
|
67
|
+
return { events, truncated };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractText(parsed, maxChars) {
|
|
71
|
+
if (!parsed) return truncate("", maxChars);
|
|
72
|
+
|
|
73
|
+
const data = parsed.data;
|
|
74
|
+
const eventName = (typeof parsed.event === "string" && parsed.event) ? parsed.event : "";
|
|
75
|
+
|
|
76
|
+
let text = "";
|
|
77
|
+
|
|
78
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
79
|
+
|
|
80
|
+
const TEXT_LIKE_KEYS = ["message", "text", "label", "note", "description", "reason", "state", "error"];
|
|
81
|
+
for (const key of TEXT_LIKE_KEYS) {
|
|
82
|
+
if (typeof data[key] === "string" && data[key]) {
|
|
83
|
+
text = data[key];
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!text) {
|
|
88
|
+
|
|
89
|
+
for (const v of Object.values(data)) {
|
|
90
|
+
if (typeof v === "string" && v) {
|
|
91
|
+
text = v;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!text) {
|
|
97
|
+
|
|
98
|
+
const preview = {};
|
|
99
|
+
let count = 0;
|
|
100
|
+
for (const [k, v] of Object.entries(data)) {
|
|
101
|
+
if (typeof v === "number" || typeof v === "boolean") {
|
|
102
|
+
preview[k] = v;
|
|
103
|
+
if (++count >= 3) break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (count > 0) {
|
|
107
|
+
text = JSON.stringify(preview);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!text && eventName) {
|
|
113
|
+
text = eventName;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return truncate(text, maxChars);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function truncate(s, maxChars) {
|
|
120
|
+
if (typeof s !== "string") return "";
|
|
121
|
+
if (s.length <= maxChars) return s;
|
|
122
|
+
return s.slice(0, maxChars) + "…";
|
|
123
|
+
}
|