ocuclaw 1.2.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +18 -5
  2. package/dist/config/runtime-config.js +81 -3
  3. package/dist/domain/activity-status-adapter.js +138 -605
  4. package/dist/domain/activity-status-arbiter.js +109 -0
  5. package/dist/domain/activity-status-labels.js +906 -0
  6. package/dist/domain/code-span-regions.js +103 -0
  7. package/dist/domain/conversation-state.js +14 -1
  8. package/dist/domain/debug-store.js +38 -182
  9. package/dist/domain/glasses-ui-content-summary.js +62 -0
  10. package/dist/domain/glasses-ui-system-prompt.js +28 -0
  11. package/dist/domain/message-emoji-allowlist.js +16 -0
  12. package/dist/domain/message-emoji-filter.js +33 -55
  13. package/dist/domain/neural-emoji-reactor-system-prompt.js +43 -0
  14. package/dist/domain/neural-emoji-reactor-tag-config.js +56 -0
  15. package/dist/domain/neural-pace-modulator-system-prompt.js +32 -0
  16. package/dist/domain/neural-pace-modulator-tag-config.js +51 -0
  17. package/dist/domain/tagged-span-parser.js +121 -0
  18. package/dist/domain/tagged-span-strip.js +38 -0
  19. package/dist/even-ai/even-ai-endpoint.js +91 -0
  20. package/dist/even-ai/even-ai-run-waiter.js +14 -0
  21. package/dist/even-ai/even-ai-settings-store.js +14 -0
  22. package/dist/gateway/gateway-bridge.js +14 -2
  23. package/dist/gateway/gateway-timing-ledger.js +457 -0
  24. package/dist/gateway/openclaw-client.js +462 -38
  25. package/dist/index.js +28 -1
  26. package/dist/runtime/downstream-handler.js +754 -83
  27. package/dist/runtime/downstream-server.js +700 -534
  28. package/dist/runtime/ocuclaw-settings-store.js +74 -31
  29. package/dist/runtime/plugin-update-service.js +216 -0
  30. package/dist/runtime/protocol-adapter.js +9 -0
  31. package/dist/runtime/provider-usage-select.js +168 -0
  32. package/dist/runtime/relay-client-nudge-controller.js +553 -0
  33. package/dist/runtime/relay-core.js +1209 -204
  34. package/dist/runtime/relay-health-monitor.js +172 -0
  35. package/dist/runtime/relay-operation-registry.js +263 -0
  36. package/dist/runtime/relay-service.js +201 -1
  37. package/dist/runtime/relay-worker-approval-replay-cache.js +68 -0
  38. package/dist/runtime/relay-worker-entry.js +32 -0
  39. package/dist/runtime/relay-worker-health.js +272 -0
  40. package/dist/runtime/relay-worker-protocol.js +285 -0
  41. package/dist/runtime/relay-worker-queue.js +202 -0
  42. package/dist/runtime/relay-worker-supervisor.js +1081 -0
  43. package/dist/runtime/relay-worker-transport.js +1051 -0
  44. package/dist/runtime/session-context-service.js +189 -0
  45. package/dist/runtime/session-service.js +615 -24
  46. package/dist/runtime/upstream-runtime.js +1167 -60
  47. package/dist/tools/device-info-tool.js +242 -0
  48. package/dist/tools/glasses-ui-cron.js +427 -0
  49. package/dist/tools/glasses-ui-descriptors.js +261 -0
  50. package/dist/tools/glasses-ui-limits.js +21 -0
  51. package/dist/tools/glasses-ui-paint-floor.js +99 -0
  52. package/dist/tools/glasses-ui-recipes.js +746 -0
  53. package/dist/tools/glasses-ui-surfaces.js +278 -0
  54. package/dist/tools/glasses-ui-template.js +182 -0
  55. package/dist/tools/glasses-ui-tool.js +1147 -0
  56. package/dist/tools/session-title-tool.js +209 -0
  57. package/dist/version.js +2 -0
  58. package/openclaw.plugin.json +163 -15
  59. package/package.json +12 -4
@@ -1,12 +1,21 @@
1
- import path from "node:path";
1
+ import { classifyRank } from "./activity-status-arbiter.js";
2
+ import {
3
+ DEFAULT_MAX_LABEL_CHARS,
4
+ SHORT_LABEL_MAX_CHARS,
5
+ isObject,
6
+ asString,
7
+ normalizeLowerToken,
8
+ pickString,
9
+ pickStringEntry,
10
+ collapseWhitespace,
11
+ sanitizeText,
12
+ intentFromToolName,
13
+ mapToolLabel,
14
+ } from "./activity-status-labels.js";
2
15
 
3
16
  const GLOBAL_RUN_KEY = "__global__";
4
- const DEFAULT_MAX_LABEL_CHARS = 120;
5
- const TOOL_PREVIEW_CHARS = 30;
6
-
7
- const REDACT_QUERY_KEYS = "(token|access_token|api_key|key|password|secret)";
8
17
  const THINKING_SUMMARY_KEYS = ["summary", "thinkingSummary", "reasoningSummary", "intentLabel"];
9
- const THINKING_DETAIL_KEYS = ["thinking", "reasoning", "thinkingText", "text", "analysis"];
18
+ const THINKING_DETAIL_KEYS = ["thinking", "reasoning", "thinkingText", "analysis"];
10
19
  const GENERIC_THINKING_LABEL = "Thinking...";
11
20
  const ACTIVITY_INTENTS = new Set([
12
21
  "thinking",
@@ -28,48 +37,11 @@ const ACTIVITY_INTENTS = new Set([
28
37
  "message.send",
29
38
  "session.manage",
30
39
  "canvas.edit",
40
+ "session.title.update",
41
+ "device.check",
31
42
  "generic",
32
43
  ]);
33
44
 
34
- function isObject(value) {
35
- return value && typeof value === "object" && !Array.isArray(value);
36
- }
37
-
38
- function asString(value) {
39
- return typeof value === "string" ? value : null;
40
- }
41
-
42
- function isNullishToken(value) {
43
- if (typeof value !== "string") return false;
44
- const normalized = value.trim().toLowerCase();
45
- return (
46
- normalized === "null" ||
47
- normalized === "undefined" ||
48
- normalized === "(null)" ||
49
- normalized === "(undefined)" ||
50
- normalized === "none"
51
- );
52
- }
53
-
54
- function normalizeLowerToken(value) {
55
- const text = asString(value);
56
- return text ? text.trim().toLowerCase() : "";
57
- }
58
-
59
- function pickString(obj, keys) {
60
- const entry = pickStringEntry(obj, keys);
61
- return entry ? entry.value : null;
62
- }
63
-
64
- function pickStringEntry(obj, keys) {
65
- if (!isObject(obj)) return null;
66
- for (const key of keys) {
67
- const value = asString(obj[key]);
68
- if (value && value.trim()) return { key, value };
69
- }
70
- return null;
71
- }
72
-
73
45
  function normalizeThinkingText(raw) {
74
46
  const text = asString(raw);
75
47
  if (!text) return null;
@@ -206,13 +178,6 @@ function isThinkingActivity(activity, category) {
206
178
  return !(activity && activity.tool);
207
179
  }
208
180
 
209
- function shortText(text, maxChars) {
210
- if (!text) return "";
211
- if (text.length <= maxChars) return text;
212
- if (maxChars <= 3) return ".".repeat(Math.max(maxChars, 0));
213
- return `${text.slice(0, maxChars - 3)}...`;
214
- }
215
-
216
181
  function lowercaseLeadingWord(rawText) {
217
182
  if (typeof rawText !== "string" || rawText.length === 0) return rawText;
218
183
  const match = rawText.match(/[A-Za-z][A-Za-z0-9_-]*/);
@@ -223,218 +188,6 @@ function lowercaseLeadingWord(rawText) {
223
188
  return `${rawText.slice(0, start)}${token.toLowerCase()}${rawText.slice(end)}`;
224
189
  }
225
190
 
226
- function collapseWhitespace(text) {
227
- return text.replace(/\s+/g, " ").trim();
228
- }
229
-
230
- function redactSecrets(rawText) {
231
- if (!rawText) return "";
232
- let text = String(rawText);
233
-
234
- text = text.replace(
235
- new RegExp(`([?&]${REDACT_QUERY_KEYS}=)[^&#\\s]+`, "gi"),
236
- "$1[redacted]",
237
- );
238
- text = text.replace(
239
- /((?:api[_-]?key|token|password|secret)\s*[=:]\s*)([^,\s"'`]+)/gi,
240
- "$1[redacted]",
241
- );
242
- text = text.replace(/(authorization\s*:\s*bearer\s+)[^\s"'`]+/gi, "$1[redacted]");
243
- text = text.replace(/\bBearer\s+[A-Za-z0-9._-]{8,}\b/g, "Bearer [redacted]");
244
- text = text.replace(/\b(sk-[A-Za-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g, "[redacted]");
245
-
246
- return text;
247
- }
248
-
249
- function sanitizeText(rawText, maxChars) {
250
- const redacted = redactSecrets(rawText);
251
- const collapsed = collapseWhitespace(redacted);
252
- return shortText(collapsed, maxChars);
253
- }
254
-
255
- function hostFromUrl(urlString) {
256
- if (!urlString) return null;
257
- try {
258
- const parsed = new URL(urlString);
259
- return parsed.host || null;
260
- } catch {
261
- return null;
262
- }
263
- }
264
-
265
- function extractFirstUrl(text) {
266
- if (!text) return null;
267
- const match = text.match(/https?:\/\/[^\s"'`]+/i);
268
- return match ? match[0] : null;
269
- }
270
-
271
- function extractBrowserQueryFromCommand(command) {
272
- const match = command.match(/[?&]q=([^&"'`\s]+)/i);
273
- if (!match) return null;
274
- try {
275
- return decodeURIComponent(match[1].replace(/\+/g, " "));
276
- } catch {
277
- return match[1].replace(/\+/g, " ");
278
- }
279
- }
280
-
281
- function stripQuotes(value) {
282
- if (!value) return value;
283
- return String(value).replace(/^['"]+|['"]+$/g, "");
284
- }
285
-
286
- function escapeRegex(value) {
287
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
288
- }
289
-
290
- function filenameFromPath(pathValue) {
291
- if (!pathValue || typeof pathValue !== "string") return null;
292
- const cleaned = stripQuotes(pathValue.trim());
293
- if (!cleaned) return null;
294
- const normalized = cleaned.replace(/[;,)]+$/g, "");
295
- if (!normalized) return null;
296
- if (isNullishToken(normalized)) return null;
297
- // Ignore shell variable/subshell paths like "$f", "${file}", or "$(mktemp)".
298
- if (/\$[({]?[A-Za-z_][A-Za-z0-9_]*[)}]?/.test(normalized) || /\$\(.+\)/.test(normalized)) {
299
- return null;
300
- }
301
- if (/^(?:\/dev\/(?:null|stdout|stderr)|nul)$/i.test(normalized)) return null;
302
- return path.basename(normalized);
303
- }
304
-
305
- function pickMktempTemplatePath(rawArgs) {
306
- if (!rawArgs || typeof rawArgs !== "string") return null;
307
- const tokens = rawArgs
308
- .match(/"[^"]*"|'[^']*'|[^\s]+/g)
309
- ?.map((token) => stripQuotes(token).trim())
310
- ?.filter(Boolean);
311
- if (!tokens || tokens.length === 0) return null;
312
-
313
- for (let index = tokens.length - 1; index >= 0; index -= 1) {
314
- const token = tokens[index];
315
- if (!token || token === "mktemp" || token.startsWith("-")) continue;
316
- if (isNullishToken(token)) continue;
317
- return token;
318
- }
319
- return null;
320
- }
321
-
322
- function extractMktempBindings(command) {
323
- if (!command || typeof command !== "string") return [];
324
- const out = [];
325
- const regex = /([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\$\(\s*mktemp\b([^)]*)\)/g;
326
- let match;
327
- while ((match = regex.exec(command)) !== null) {
328
- const varName = match[1];
329
- const templatePath = pickMktempTemplatePath(match[2]);
330
- const fileName = filenameFromPath(templatePath);
331
- if (!fileName) continue;
332
- out.push({ varName, fileName });
333
- }
334
- return out;
335
- }
336
-
337
- function commandRefsVarWithRedirect(command, varName, operator) {
338
- if (!command || !varName) return false;
339
- const varRef = `\\$\\{?${escapeRegex(varName)}\\}?`;
340
- const op = escapeRegex(operator);
341
- const regex = new RegExp(`(?:^|\\s)${op}\\s*(?:["']?${varRef}["']?)`);
342
- return regex.test(command);
343
- }
344
-
345
- function commandReadsVarWithCat(command, varName) {
346
- if (!command || !varName) return false;
347
- const varRef = `\\$\\{?${escapeRegex(varName)}\\}?`;
348
- const regex = new RegExp(`(?:^|\\s)cat\\s+(?:["']?${varRef}["']?)`);
349
- return regex.test(command);
350
- }
351
-
352
- function categoryFromToolName(lowName) {
353
- if (lowName.startsWith("browser") || lowName === "web" || lowName === "web.search") return "browser";
354
- if (lowName === "read" || lowName === "write" || lowName === "edit" || lowName.startsWith("fs.")) return "filesystem";
355
- if (lowName === "search" || lowName.startsWith("vector") || lowName === "grep" || lowName === "find") return "search";
356
- if (lowName === "exec" || lowName === "bash" || lowName.startsWith("shell")) return "terminal";
357
- return "generic";
358
- }
359
-
360
- function intentFromToolName(lowName, args) {
361
- const query = pickString(args, ["query", "q", "term", "search"]);
362
-
363
- switch (lowName) {
364
- case "read":
365
- case "fs.read":
366
- return "fs.read";
367
- case "write":
368
- case "apply_patch":
369
- case "fs.write":
370
- return "fs.write";
371
- case "edit":
372
- case "fs.edit":
373
- return "fs.edit";
374
- case "search":
375
- case "grep":
376
- case "find":
377
- return "search.files";
378
- case "browser.search":
379
- case "web.search":
380
- return "search.web";
381
- case "browser.click":
382
- case "browser.navigate":
383
- return "browser.navigate";
384
- case "browser.fill":
385
- return "browser.fill";
386
- case "browser":
387
- case "web":
388
- return query ? "search.web" : "browser.browse";
389
- case "exec":
390
- case "bash":
391
- return "terminal.exec";
392
- case "git":
393
- return "terminal.git";
394
- case "llm_task":
395
- return "agent.subtask";
396
- case "agent_send":
397
- return "agent.coordinate";
398
- case "message":
399
- return "message.send";
400
- case "sessions_list":
401
- case "sessions_read":
402
- case "session_status":
403
- return "session.manage";
404
- case "canvas":
405
- return "canvas.edit";
406
- case "fetch":
407
- return "network.fetch";
408
- default:
409
- break;
410
- }
411
-
412
- if (lowName.startsWith("browser")) {
413
- if (lowName.includes("fill")) return "browser.fill";
414
- if (lowName.includes("click") || lowName.includes("navigate")) return "browser.navigate";
415
- if (lowName.includes("search")) return "search.web";
416
- return "browser.browse";
417
- }
418
- if (lowName.startsWith("web")) {
419
- return lowName.includes("search") ? "search.web" : "browser.browse";
420
- }
421
- if (lowName.includes("search")) {
422
- return lowName.includes("web") || lowName.includes("browser")
423
- ? "search.web"
424
- : "search.files";
425
- }
426
- if (lowName.startsWith("fs.")) {
427
- if (lowName.includes("read")) return "fs.read";
428
- if (lowName.includes("edit")) return "fs.edit";
429
- return "fs.write";
430
- }
431
- if (lowName.startsWith("session")) return "session.manage";
432
- if (lowName.startsWith("http")) return "network.fetch";
433
- if (lowName.startsWith("git")) return "terminal.git";
434
- if (lowName.startsWith("shell")) return "terminal.exec";
435
- return "generic";
436
- }
437
-
438
191
  function isExplanatoryThinkingLabel(label) {
439
192
  const normalizedLabel = normalizeThinkingText(label);
440
193
  if (!normalizedLabel) return false;
@@ -444,345 +197,6 @@ function isExplanatoryThinkingLabel(label) {
444
197
  return normalizedToken !== "thinking" && normalizedToken !== "thinking...";
445
198
  }
446
199
 
447
- function labelFromExecCommand(command) {
448
- const raw = command ? String(command).trim() : "";
449
- if (!raw) {
450
- return {
451
- label: "Running a command...",
452
- category: "terminal",
453
- intent: "terminal.exec",
454
- };
455
- }
456
-
457
- if (raw.includes("agent-browser")) {
458
- const query = extractBrowserQueryFromCommand(raw);
459
- if (query) {
460
- return {
461
- label: `Searching "${shortText(sanitizeText(query, TOOL_PREVIEW_CHARS), TOOL_PREVIEW_CHARS)}"...`,
462
- category: "browser",
463
- intent: "search.web",
464
- };
465
- }
466
- const browserUrl = extractFirstUrl(raw);
467
- if (browserUrl) {
468
- const host = hostFromUrl(browserUrl);
469
- return {
470
- label: host ? `Browsing ${host}...` : "Using browser...",
471
- category: "browser",
472
- intent: "browser.browse",
473
- };
474
- }
475
- return {
476
- label: "Using browser...",
477
- category: "browser",
478
- intent: "browser.browse",
479
- };
480
- }
481
-
482
- if (/(^|\s)(curl|wget)\b/i.test(raw) || /https?:\/\//i.test(raw)) {
483
- const url = extractFirstUrl(raw);
484
- const host = hostFromUrl(url);
485
- if (host) {
486
- return {
487
- label: `Fetching from ${host}...`,
488
- category: "network",
489
- intent: "network.fetch",
490
- };
491
- }
492
- return {
493
- label: "Fetching data...",
494
- category: "network",
495
- intent: "network.fetch",
496
- };
497
- }
498
-
499
- const appendMatch = raw.match(/(?:^|\s)>>\s*([^\s]+)/);
500
- if (appendMatch) {
501
- const fileName = filenameFromPath(appendMatch[1]);
502
- if (fileName) {
503
- return {
504
- label: `Appending to ${fileName}...`,
505
- category: "filesystem",
506
- intent: "fs.write",
507
- };
508
- }
509
- }
510
-
511
- const writeMatch = raw.match(/(?:^|\s)>\s*([^\s]+)/);
512
- if (writeMatch) {
513
- const fileName = filenameFromPath(writeMatch[1]);
514
- if (fileName) {
515
- return {
516
- label: `Writing ${fileName}...`,
517
- category: "filesystem",
518
- intent: "fs.write",
519
- };
520
- }
521
- }
522
-
523
- const mktempBindings = extractMktempBindings(raw);
524
- for (const binding of mktempBindings) {
525
- if (commandRefsVarWithRedirect(raw, binding.varName, ">>")) {
526
- return {
527
- label: `Appending to ${binding.fileName}...`,
528
- category: "filesystem",
529
- intent: "fs.write",
530
- };
531
- }
532
- }
533
- for (const binding of mktempBindings) {
534
- if (commandRefsVarWithRedirect(raw, binding.varName, ">")) {
535
- return {
536
- label: `Writing ${binding.fileName}...`,
537
- category: "filesystem",
538
- intent: "fs.write",
539
- };
540
- }
541
- }
542
- for (const binding of mktempBindings) {
543
- if (commandReadsVarWithCat(raw, binding.varName)) {
544
- return {
545
- label: `Reading ${binding.fileName}...`,
546
- category: "filesystem",
547
- intent: "fs.read",
548
- };
549
- }
550
- }
551
-
552
- const catMatch = raw.match(/^cat\s+([^\s>]+)/);
553
- if (catMatch) {
554
- const fileName = filenameFromPath(catMatch[1]);
555
- if (fileName) {
556
- return {
557
- label: `Reading ${fileName}...`,
558
- category: "filesystem",
559
- intent: "fs.read",
560
- };
561
- }
562
- }
563
-
564
- if (/^(grep|rg|find)\b/.test(raw)) {
565
- return {
566
- label: "Searching files...",
567
- category: "search",
568
- intent: "search.files",
569
- };
570
- }
571
- if (/\bgit\s+/.test(raw)) {
572
- return {
573
- label: "Running git...",
574
- category: "terminal",
575
- intent: "terminal.git",
576
- };
577
- }
578
-
579
- return {
580
- label: `Running: ${shortText(sanitizeText(raw, TOOL_PREVIEW_CHARS), TOOL_PREVIEW_CHARS)}`,
581
- category: "terminal",
582
- intent: "terminal.exec",
583
- };
584
- }
585
-
586
- function mapToolLabel(toolName, activityPath, args, options) {
587
- const maxLabelChars = options.maxLabelChars;
588
- const lowName = String(toolName || "").toLowerCase();
589
- const rawPathValue = asString(activityPath) || pickString(args, [
590
- "path",
591
- "filePath",
592
- "file_path",
593
- "filepath",
594
- "file",
595
- "target",
596
- "outputPath",
597
- "output_path",
598
- "output",
599
- "destination",
600
- "dest",
601
- ]);
602
- const pathValue = rawPathValue && !isNullishToken(rawPathValue) ? rawPathValue : null;
603
- const fileName = filenameFromPath(pathValue);
604
- const query = pickString(args, ["query", "q", "term", "search"]);
605
- const url = pickString(args, ["url", "href", "uri"]);
606
- const command = pickString(args, ["command", "cmd", "shell"]);
607
-
608
- switch (lowName) {
609
- case "write":
610
- case "apply_patch":
611
- case "fs.write":
612
- return {
613
- label: `Writing ${fileName || "file"}...`,
614
- detail: pathValue || command || null,
615
- category: "filesystem",
616
- intent: "fs.write",
617
- };
618
- case "read":
619
- case "fs.read":
620
- return {
621
- label: `Reading ${fileName || "file"}...`,
622
- detail: pathValue || command || null,
623
- category: "filesystem",
624
- intent: "fs.read",
625
- };
626
- case "edit":
627
- case "fs.edit":
628
- return {
629
- label: `Editing ${fileName || "file"}...`,
630
- detail: pathValue || command || null,
631
- category: "filesystem",
632
- intent: "fs.edit",
633
- };
634
- case "search":
635
- if (query) {
636
- const shortQuery = shortText(sanitizeText(query, TOOL_PREVIEW_CHARS), TOOL_PREVIEW_CHARS);
637
- return {
638
- label: `Searching for "${shortQuery}"...`,
639
- detail: query,
640
- category: "search",
641
- intent: "search.files",
642
- };
643
- }
644
- return {
645
- label: "Searching files...",
646
- detail: pathValue || null,
647
- category: "search",
648
- intent: "search.files",
649
- };
650
- case "bash":
651
- case "exec": {
652
- const fromCommand = labelFromExecCommand(command);
653
- return {
654
- label: fromCommand.label,
655
- detail: command || pathValue || null,
656
- category: fromCommand.category,
657
- intent: fromCommand.intent,
658
- };
659
- }
660
- case "browser.search":
661
- case "web.search":
662
- if (query) {
663
- const shortQuery = shortText(sanitizeText(query, TOOL_PREVIEW_CHARS), TOOL_PREVIEW_CHARS);
664
- return {
665
- label: `Searching the web for "${shortQuery}"...`,
666
- detail: query,
667
- category: "browser",
668
- intent: "search.web",
669
- };
670
- }
671
- if (url) {
672
- return {
673
- label: "Searching the web...",
674
- detail: url,
675
- category: "browser",
676
- intent: "search.web",
677
- };
678
- }
679
- return {
680
- label: "Searching the web...",
681
- detail: null,
682
- category: "browser",
683
- intent: "search.web",
684
- };
685
- case "browser":
686
- case "web":
687
- if (query) {
688
- const shortQuery = shortText(sanitizeText(query, TOOL_PREVIEW_CHARS), TOOL_PREVIEW_CHARS);
689
- return {
690
- label: `Searching the web for "${shortQuery}"...`,
691
- detail: query,
692
- category: "browser",
693
- intent: "search.web",
694
- };
695
- }
696
- if (url) {
697
- return {
698
- label: "Browsing the web...",
699
- detail: url,
700
- category: "browser",
701
- intent: "browser.browse",
702
- };
703
- }
704
- return {
705
- label: "Browsing the web...",
706
- detail: null,
707
- category: "browser",
708
- intent: "browser.browse",
709
- };
710
- case "browser.click":
711
- return {
712
- label: "Navigating a webpage...",
713
- detail: url || null,
714
- category: "browser",
715
- intent: "browser.navigate",
716
- };
717
- case "browser.fill":
718
- return {
719
- label: "Filling out a form...",
720
- detail: url || null,
721
- category: "browser",
722
- intent: "browser.fill",
723
- };
724
- case "browser.navigate":
725
- return {
726
- label: "Opening a webpage...",
727
- detail: url || null,
728
- category: "browser",
729
- intent: "browser.navigate",
730
- };
731
- case "llm_task":
732
- return {
733
- label: "Running a sub-task...",
734
- detail: null,
735
- category: "generic",
736
- intent: "agent.subtask",
737
- };
738
- case "agent_send":
739
- return {
740
- label: "Coordinating with another agent...",
741
- detail: null,
742
- category: "generic",
743
- intent: "agent.coordinate",
744
- };
745
- case "message":
746
- return {
747
- label: "Sending a message...",
748
- detail: null,
749
- category: "generic",
750
- intent: "message.send",
751
- };
752
- case "sessions_list":
753
- case "sessions_read":
754
- case "session_status":
755
- return {
756
- label: "Checking sessions...",
757
- detail: null,
758
- category: "generic",
759
- intent: "session.manage",
760
- };
761
- case "canvas":
762
- return {
763
- label: "Working on canvas...",
764
- detail: null,
765
- category: "generic",
766
- intent: "canvas.edit",
767
- };
768
- default:
769
- if (fileName) {
770
- return {
771
- label: `${toolName} ${fileName}...`,
772
- detail: pathValue || null,
773
- category: categoryFromToolName(lowName),
774
- intent: intentFromToolName(lowName, args),
775
- };
776
- }
777
- return {
778
- label: `Using ${toolName}...`,
779
- detail: query || url || command || null,
780
- category: categoryFromToolName(lowName),
781
- intent: intentFromToolName(lowName, args),
782
- };
783
- }
784
- }
785
-
786
200
  function normalizePhase(phase, state) {
787
201
  const p = typeof phase === "string" ? phase.toLowerCase() : "";
788
202
  if (p === "start" || p === "update" || p === "end") return p;
@@ -818,6 +232,13 @@ function sanitizeIdPart(value) {
818
232
  .replace(/^-|-$/g, "");
819
233
  }
820
234
 
235
+ function clampFreshnessWindow(value) {
236
+ const n = Number.isFinite(value) ? Math.floor(value) : 5000;
237
+ if (n < 3000) return 3000;
238
+ if (n > 8000) return 8000;
239
+ return n;
240
+ }
241
+
821
242
  function createActivityStatusAdapter(opts) {
822
243
  const options = opts || {};
823
244
  const enabled = options.enabled !== false;
@@ -826,6 +247,9 @@ function createActivityStatusAdapter(opts) {
826
247
  Number.isFinite(options.maxLabelChars) && options.maxLabelChars > 0
827
248
  ? Math.floor(options.maxLabelChars)
828
249
  : DEFAULT_MAX_LABEL_CHARS;
250
+ const freshnessWindowMs = clampFreshnessWindow(options.freshnessWindowMs);
251
+ const now =
252
+ typeof options.now === "function" ? options.now : () => Date.now();
829
253
 
830
254
  /** @type {Map<string, {seq: number, toolStartCount: number, currentActivityId: string|null, toolContextByActivityId: Map<string, {label: string, detail: string|null, category: string|null, intent: string|null}>}>} */
831
255
  const runStates = new Map();
@@ -838,6 +262,14 @@ function createActivityStatusAdapter(opts) {
838
262
  toolStartCount: 0,
839
263
  currentActivityId: null,
840
264
  toolContextByActivityId: new Map(),
265
+ capabilities: {
266
+ sawSummary: false,
267
+ sawToolCall: false,
268
+ sawThinkingBlock: false,
269
+ hasToolCallId: false,
270
+ hasSignature: false,
271
+ hasTurnId: false,
272
+ },
841
273
  };
842
274
  runStates.set(runKey, state);
843
275
  }
@@ -855,6 +287,10 @@ function createActivityStatusAdapter(opts) {
855
287
  const runKey = normalizeRunKey(activity.runId);
856
288
  const runState = getRunState(runKey);
857
289
  const phase = normalizePhase(activity.phase, activity.state);
290
+ const rawPhase = asString(activity.phase) && activity.phase.trim()
291
+ ? activity.phase.trim()
292
+ : null;
293
+ const preserveErrorPhase = rawPhase && rawPhase.toLowerCase() === "error";
858
294
 
859
295
  runState.seq += 1;
860
296
  const seq = Number.isFinite(activity.seq) ? Math.floor(activity.seq) : runState.seq;
@@ -887,6 +323,7 @@ function createActivityStatusAdapter(opts) {
887
323
  }
888
324
 
889
325
  let label = asString(activity.label);
326
+ let shortLabel = null;
890
327
  let detail = asString(activity.detail);
891
328
  let category = asString(activity.category);
892
329
  let thinkingSummarySource = null;
@@ -923,6 +360,7 @@ function createActivityStatusAdapter(opts) {
923
360
  const mappedTool = activity.tool
924
361
  ? mapToolLabel(activity.tool, activity.path, args, {
925
362
  maxLabelChars,
363
+ stabilityKey: activityId,
926
364
  })
927
365
  : null;
928
366
  const isThinking = isThinkingActivity(activity, category);
@@ -943,21 +381,30 @@ function createActivityStatusAdapter(opts) {
943
381
  detail = resolvedThinking.detail;
944
382
  }
945
383
 
384
+ // Agent-authored summaries get a clamp-only shortLabel when the
385
+ // 64-char header budget needs it; generic "Thinking..." never does.
386
+ if (label && isExplanatoryThinkingLabel(label) && label.length > SHORT_LABEL_MAX_CHARS) {
387
+ shortLabel = label;
388
+ }
389
+
946
390
  if (!includeThinking) {
947
391
  suppressThinkingContent = true;
948
392
  label = null;
949
393
  detail = null;
394
+ shortLabel = null;
950
395
  thinkingSummarySource = null;
951
396
  }
952
397
  } else if (!label && activity.tool) {
953
398
  if (previousToolContext && !hasCurrentToolContext) {
954
399
  label = previousToolContext.label;
400
+ shortLabel = previousToolContext.shortLabel || null;
955
401
  if (!detail) detail = previousToolContext.detail;
956
402
  if (!category) category = previousToolContext.category;
957
403
  }
958
404
  if (!label) {
959
405
  const mapped = mappedTool;
960
406
  label = mapped.label;
407
+ shortLabel = mapped.shortLabel || null;
961
408
  if (!detail) detail = mapped.detail;
962
409
  if (!category) category = mapped.category;
963
410
  }
@@ -979,6 +426,7 @@ function createActivityStatusAdapter(opts) {
979
426
  if (activity.tool && activityId && label) {
980
427
  runState.toolContextByActivityId.set(activityId, {
981
428
  label,
429
+ shortLabel: shortLabel || null,
982
430
  detail: detail || null,
983
431
  category: category || null,
984
432
  intent: intent || null,
@@ -989,12 +437,13 @@ function createActivityStatusAdapter(opts) {
989
437
  ...activity,
990
438
  activityId,
991
439
  seq,
992
- phase,
440
+ phase: preserveErrorPhase ? "error" : phase,
993
441
  };
994
442
 
995
443
  if (suppressThinkingContent) {
996
444
  delete result.label;
997
445
  delete result.detail;
446
+ delete result.shortLabel;
998
447
  }
999
448
 
1000
449
  if (runKey !== GLOBAL_RUN_KEY && !result.runId) {
@@ -1004,6 +453,9 @@ function createActivityStatusAdapter(opts) {
1004
453
  if (typeof activity.isError === "boolean") {
1005
454
  result.isError = activity.isError;
1006
455
  }
456
+ if (typeof activity.code === "string" && activity.code.trim()) {
457
+ result.code = activity.code.trim();
458
+ }
1007
459
  if (Number.isFinite(activity.exitCode)) {
1008
460
  result.exitCode = Math.trunc(activity.exitCode);
1009
461
  }
@@ -1011,8 +463,89 @@ function createActivityStatusAdapter(opts) {
1011
463
  result.durationMs = Math.trunc(activity.durationMs);
1012
464
  }
1013
465
 
466
+ if (isObject(activity.rateLimitInfo)) {
467
+ result.rateLimitInfo = activity.rateLimitInfo;
468
+ }
469
+ if (activity.failoverPending === true) {
470
+ result.failoverPending = true;
471
+ }
472
+
473
+ const candidateRank = classifyRank({
474
+ isError: activity.isError === true,
475
+ phaseIsError: preserveErrorPhase === true,
476
+ hasRateLimitInfo: isObject(activity.rateLimitInfo),
477
+ failoverPending: activity.failoverPending === true,
478
+ hasTool: !!activity.tool,
479
+ isThinking,
480
+ includeThinking,
481
+ thinkingSummarySource,
482
+ label,
483
+ });
484
+ result.candidateRank = candidateRank;
485
+ result.sourceType = candidateRank;
486
+
487
+ const caps = runState.capabilities;
488
+ if (candidateRank === "generated_summary") caps.sawSummary = true;
489
+ if (activity.tool) caps.sawToolCall = true;
490
+ if (isThinking) caps.sawThinkingBlock = true;
491
+ if (typeof activity.toolCallId === "string" && activity.toolCallId.trim()) {
492
+ caps.hasToolCallId = true;
493
+ }
494
+ if (
495
+ typeof activity.thinkingSignatureId === "string" &&
496
+ activity.thinkingSignatureId.trim()
497
+ ) {
498
+ caps.hasSignature = true;
499
+ }
500
+ if (typeof activity.turnId === "string" && activity.turnId.trim()) {
501
+ caps.hasTurnId = true;
502
+ }
503
+ result.capabilityFlags = { ...caps };
504
+
505
+ result.candidateAtMs = now();
506
+ result.freshnessWindowMs = freshnessWindowMs;
507
+
508
+ // Association ids (optional, normalized): trim, or drop when empty. The
509
+ // {...activity} spread already copies them, but normalize at the contract
510
+ // boundary so untrusted callers can't leak whitespace/empty ids. See
511
+ // transport spec docs/superpowers/specs/2026-06-01-...-redesign-design.md §4/§11.
512
+ if (typeof activity.toolCallId === "string" && activity.toolCallId.trim()) {
513
+ result.toolCallId = activity.toolCallId.trim();
514
+ } else {
515
+ delete result.toolCallId;
516
+ }
517
+ if (typeof activity.turnId === "string" && activity.turnId.trim()) {
518
+ result.turnId = activity.turnId.trim();
519
+ } else {
520
+ delete result.turnId;
521
+ }
522
+ if (typeof activity.thinkingSignatureId === "string" && activity.thinkingSignatureId.trim()) {
523
+ result.thinkingSignatureId = activity.thinkingSignatureId.trim();
524
+ } else {
525
+ delete result.thinkingSignatureId;
526
+ }
527
+
528
+ delete result.shortLabel;
1014
529
  if (enabled) {
1015
- if (label) result.label = sanitizeText(lowercaseLeadingWord(label), maxLabelChars);
530
+ if (label) {
531
+ const preserveErrorLabelCase =
532
+ activity.isError === true || preserveErrorPhase;
533
+ result.label = sanitizeText(
534
+ preserveErrorLabelCase ? label : lowercaseLeadingWord(label),
535
+ maxLabelChars,
536
+ );
537
+ }
538
+ if (label && shortLabel) {
539
+ const preserveShortLabelCase =
540
+ activity.isError === true || preserveErrorPhase;
541
+ const sanitizedShort = sanitizeText(
542
+ preserveShortLabelCase ? shortLabel : lowercaseLeadingWord(shortLabel),
543
+ SHORT_LABEL_MAX_CHARS,
544
+ );
545
+ if (sanitizedShort && sanitizedShort !== result.label) {
546
+ result.shortLabel = sanitizedShort;
547
+ }
548
+ }
1016
549
  if (detail) result.detail = sanitizeText(detail, Math.max(maxLabelChars, 200));
1017
550
  if (category) result.category = sanitizeIdPart(category.toLowerCase()) || "generic";
1018
551
  if (thinkingSummarySource) result.thinkingSummarySource = thinkingSummarySource;