@useorgx/openclaw-plugin 0.7.0 → 0.7.3

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 (124) hide show
  1. package/README.md +42 -11
  2. package/dashboard/dist/assets/6mILZQ2a.js +1 -0
  3. package/dashboard/dist/assets/6mILZQ2a.js.br +0 -0
  4. package/dashboard/dist/assets/6mILZQ2a.js.gz +0 -0
  5. package/dashboard/dist/assets/{DG6y9wJI.js → 8dksYiq4.js} +1 -1
  6. package/dashboard/dist/assets/8dksYiq4.js.br +0 -0
  7. package/dashboard/dist/assets/8dksYiq4.js.gz +0 -0
  8. package/dashboard/dist/assets/{PAUiij_z.js → B5zYRHc3.js} +1 -1
  9. package/dashboard/dist/assets/B5zYRHc3.js.br +0 -0
  10. package/dashboard/dist/assets/B5zYRHc3.js.gz +0 -0
  11. package/dashboard/dist/assets/{CFGKRAzG.js → B6wPWJ35.js} +1 -1
  12. package/dashboard/dist/assets/B6wPWJ35.js.br +0 -0
  13. package/dashboard/dist/assets/B6wPWJ35.js.gz +0 -0
  14. package/dashboard/dist/assets/BWEwjt1W.js +1 -0
  15. package/dashboard/dist/assets/BWEwjt1W.js.br +0 -0
  16. package/dashboard/dist/assets/BWEwjt1W.js.gz +0 -0
  17. package/dashboard/dist/assets/BzRbDCAD.css +1 -0
  18. package/dashboard/dist/assets/BzRbDCAD.css.br +0 -0
  19. package/dashboard/dist/assets/BzRbDCAD.css.gz +0 -0
  20. package/dashboard/dist/assets/{DNxKz-GV.js → C8uM3AX8.js} +1 -1
  21. package/dashboard/dist/assets/C8uM3AX8.js.br +0 -0
  22. package/dashboard/dist/assets/C8uM3AX8.js.gz +0 -0
  23. package/dashboard/dist/assets/C9jy61eu.js +212 -0
  24. package/dashboard/dist/assets/C9jy61eu.js.br +0 -0
  25. package/dashboard/dist/assets/C9jy61eu.js.gz +0 -0
  26. package/dashboard/dist/assets/{CE38zU4U.js → CC63EwFD.js} +1 -1
  27. package/dashboard/dist/assets/CC63EwFD.js.br +0 -0
  28. package/dashboard/dist/assets/CC63EwFD.js.gz +0 -0
  29. package/dashboard/dist/assets/{nByHNHoW.js → CZaT3ob_.js} +1 -1
  30. package/dashboard/dist/assets/CZaT3ob_.js.br +0 -0
  31. package/dashboard/dist/assets/CZaT3ob_.js.gz +0 -0
  32. package/dashboard/dist/assets/{tS9mbYZi.js → CgaottFX.js} +1 -1
  33. package/dashboard/dist/assets/CgaottFX.js.br +0 -0
  34. package/dashboard/dist/assets/CgaottFX.js.gz +0 -0
  35. package/dashboard/dist/assets/CzCxAZlW.js +1 -0
  36. package/dashboard/dist/assets/CzCxAZlW.js.br +0 -0
  37. package/dashboard/dist/assets/CzCxAZlW.js.gz +0 -0
  38. package/dashboard/dist/assets/D3iMTYEj.js +1 -0
  39. package/dashboard/dist/assets/D3iMTYEj.js.br +0 -0
  40. package/dashboard/dist/assets/D3iMTYEj.js.gz +0 -0
  41. package/dashboard/dist/assets/{DjcdE6jC.js → D8JNX8kq.js} +1 -1
  42. package/dashboard/dist/assets/D8JNX8kq.js.br +0 -0
  43. package/dashboard/dist/assets/D8JNX8kq.js.gz +0 -0
  44. package/dashboard/dist/assets/DnA8dpj6.js +1 -0
  45. package/dashboard/dist/assets/DnA8dpj6.js.br +0 -0
  46. package/dashboard/dist/assets/DnA8dpj6.js.gz +0 -0
  47. package/dashboard/dist/assets/{DbNoijHm.js → IUexzymk.js} +1 -1
  48. package/dashboard/dist/assets/IUexzymk.js.br +0 -0
  49. package/dashboard/dist/assets/IUexzymk.js.gz +0 -0
  50. package/dashboard/dist/assets/{CZZTvkQZ.js → rttbDbEx.js} +1 -1
  51. package/dashboard/dist/assets/rttbDbEx.js.br +0 -0
  52. package/dashboard/dist/assets/rttbDbEx.js.gz +0 -0
  53. package/dashboard/dist/index.html +2 -2
  54. package/dashboard/dist/index.html.br +0 -0
  55. package/dashboard/dist/index.html.gz +0 -0
  56. package/dist/contracts/practice-exercise-schema.d.ts +216 -0
  57. package/dist/contracts/practice-exercise-schema.js +314 -0
  58. package/dist/contracts/shared-types.d.ts +2 -2
  59. package/dist/contracts/shared-types.js +22 -0
  60. package/dist/contracts/types.d.ts +20 -0
  61. package/dist/fs-utils.js +1 -13
  62. package/dist/http/helpers/auto-continue-engine.js +638 -24
  63. package/dist/http/helpers/autopilot-runtime.js +22 -7
  64. package/dist/http/helpers/autopilot-slice-utils.js +0 -2
  65. package/dist/http/helpers/kickoff-context.js +30 -0
  66. package/dist/http/helpers/slice-run-projections.js +19 -2
  67. package/dist/http/index.js +151 -93
  68. package/dist/http/routes/agent-suite.js +87 -0
  69. package/dist/http/routes/entities.js +1 -63
  70. package/dist/http/routes/live-snapshot.d.ts +1 -0
  71. package/dist/http/routes/live-snapshot.js +15 -4
  72. package/dist/http/routes/live-terminal.js +1 -108
  73. package/dist/http/routes/live-triage.js +1 -57
  74. package/dist/http/routes/mission-control-actions.js +0 -88
  75. package/dist/http/routes/mission-control-read.js +73 -8
  76. package/dist/mcp-http-handler.d.ts +3 -0
  77. package/dist/mcp-http-handler.js +2 -2
  78. package/dist/openclaw.plugin.json +1 -1
  79. package/dist/paths.js +14 -2
  80. package/dist/reporting/rollups.d.ts +0 -12
  81. package/dist/reporting/rollups.js +0 -35
  82. package/openclaw.plugin.json +1 -1
  83. package/package.json +7 -3
  84. package/dashboard/dist/assets/BXWDRGm-.js +0 -1
  85. package/dashboard/dist/assets/BXWDRGm-.js.br +0 -0
  86. package/dashboard/dist/assets/BXWDRGm-.js.gz +0 -0
  87. package/dashboard/dist/assets/CE38zU4U.js.br +0 -0
  88. package/dashboard/dist/assets/CE38zU4U.js.gz +0 -0
  89. package/dashboard/dist/assets/CFGKRAzG.js.br +0 -0
  90. package/dashboard/dist/assets/CFGKRAzG.js.gz +0 -0
  91. package/dashboard/dist/assets/CGGR2GZh.js +0 -1
  92. package/dashboard/dist/assets/CGGR2GZh.js.br +0 -0
  93. package/dashboard/dist/assets/CGGR2GZh.js.gz +0 -0
  94. package/dashboard/dist/assets/CPFiTmlw.js +0 -8
  95. package/dashboard/dist/assets/CPFiTmlw.js.br +0 -0
  96. package/dashboard/dist/assets/CPFiTmlw.js.gz +0 -0
  97. package/dashboard/dist/assets/CZZTvkQZ.js.br +0 -0
  98. package/dashboard/dist/assets/CZZTvkQZ.js.gz +0 -0
  99. package/dashboard/dist/assets/D-bf6hEI.js +0 -213
  100. package/dashboard/dist/assets/D-bf6hEI.js.br +0 -0
  101. package/dashboard/dist/assets/D-bf6hEI.js.gz +0 -0
  102. package/dashboard/dist/assets/DG6y9wJI.js.br +0 -0
  103. package/dashboard/dist/assets/DG6y9wJI.js.gz +0 -0
  104. package/dashboard/dist/assets/DNxKz-GV.js.br +0 -0
  105. package/dashboard/dist/assets/DNxKz-GV.js.gz +0 -0
  106. package/dashboard/dist/assets/DW_rKUic.js +0 -11
  107. package/dashboard/dist/assets/DW_rKUic.js.br +0 -0
  108. package/dashboard/dist/assets/DW_rKUic.js.gz +0 -0
  109. package/dashboard/dist/assets/DbNoijHm.js.br +0 -0
  110. package/dashboard/dist/assets/DbNoijHm.js.gz +0 -0
  111. package/dashboard/dist/assets/DjcdE6jC.js.br +0 -0
  112. package/dashboard/dist/assets/DjcdE6jC.js.gz +0 -0
  113. package/dashboard/dist/assets/FZYuCDnt.js +0 -1
  114. package/dashboard/dist/assets/FZYuCDnt.js.br +0 -0
  115. package/dashboard/dist/assets/FZYuCDnt.js.gz +0 -0
  116. package/dashboard/dist/assets/PAUiij_z.js.br +0 -0
  117. package/dashboard/dist/assets/PAUiij_z.js.gz +0 -0
  118. package/dashboard/dist/assets/h5biQs2I.css +0 -1
  119. package/dashboard/dist/assets/h5biQs2I.css.br +0 -0
  120. package/dashboard/dist/assets/h5biQs2I.css.gz +0 -0
  121. package/dashboard/dist/assets/nByHNHoW.js.br +0 -0
  122. package/dashboard/dist/assets/nByHNHoW.js.gz +0 -0
  123. package/dashboard/dist/assets/tS9mbYZi.js.br +0 -0
  124. package/dashboard/dist/assets/tS9mbYZi.js.gz +0 -0
@@ -50,10 +50,18 @@ export function createAutopilotRuntime(deps) {
50
50
  }
51
51
  if (!inTargetSection)
52
52
  continue;
53
- const urlMatch = line.match(/^url\s*=\s*"([^"]+)"\s*$/i);
53
+ const urlMatch = line.match(/^url\s*=\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*(?:#.*)?$/i);
54
54
  if (!urlMatch)
55
55
  continue;
56
- const rawUrl = (urlMatch[1] ?? "").trim();
56
+ const token = (urlMatch[1] ?? "").trim();
57
+ if (token.length < 2)
58
+ continue;
59
+ const quote = token[0];
60
+ const unescaped = token
61
+ .slice(1, -1)
62
+ .replace(quote === '"' ? /\\"/g : /\\'/g, quote)
63
+ .trim();
64
+ const rawUrl = unescaped;
57
65
  if (rawUrl.length > 0)
58
66
  return rawUrl;
59
67
  }
@@ -147,6 +155,13 @@ export function createAutopilotRuntime(deps) {
147
155
  // best effort
148
156
  }
149
157
  }
158
+ function createSafeAppendStream(pathname) {
159
+ const stream = createWriteStream(pathname, { flags: "a" });
160
+ stream.on("error", () => {
161
+ // Best-effort logging only.
162
+ });
163
+ return stream;
164
+ }
150
165
  function hasSliceOutput(pathname) {
151
166
  try {
152
167
  if (!existsSync(pathname))
@@ -181,8 +196,8 @@ export function createAutopilotRuntime(deps) {
181
196
  const workerKind = (process.env.ORGX_AUTOPILOT_WORKER_KIND ?? "").trim().toLowerCase();
182
197
  if (workerKind === "mock") {
183
198
  const scriptPath = resolve(dirname(deps.filename), "..", "..", "scripts", "mock-autopilot-slice-worker.mjs");
184
- const logStream = createWriteStream(input.logPath, { flags: "a" });
185
- const outStream = createWriteStream(input.outputPath, { flags: "a" });
199
+ const logStream = createSafeAppendStream(input.logPath);
200
+ const outStream = createSafeAppendStream(input.outputPath);
186
201
  logStream.write(`\n==== ${new Date().toISOString()} :: mock slice ${input.runId} ====\n`);
187
202
  const child = spawn("node", [scriptPath], {
188
203
  cwd: input.cwd,
@@ -329,8 +344,8 @@ export function createAutopilotRuntime(deps) {
329
344
  }
330
345
  if (schemaArg)
331
346
  claudeExtraArgs.push("--json-schema", schemaArg);
332
- const logStream = createWriteStream(input.logPath, { flags: "a" });
333
- const outStream = createWriteStream(input.outputPath, { flags: "a" });
347
+ const logStream = createSafeAppendStream(input.logPath);
348
+ const outStream = createSafeAppendStream(input.outputPath);
334
349
  logStream.write(`\n==== ${new Date().toISOString()} :: claude slice ${input.runId} ====\n`);
335
350
  logStream.write(`claude_bin: ${claudeBin}\n`);
336
351
  if (claudeExtraArgs.length > 0) {
@@ -462,7 +477,7 @@ export function createAutopilotRuntime(deps) {
462
477
  ? normalizedArgs
463
478
  : ["exec", ...normalizedArgs];
464
479
  const extraArgs = [];
465
- const logStream = createWriteStream(input.logPath, { flags: "a" });
480
+ const logStream = createSafeAppendStream(input.logPath);
466
481
  logStream.write(`\n==== ${new Date().toISOString()} :: slice ${input.runId} ====\n`);
467
482
  logStream.write(`codex_bin: ${codexBin}${codexInfo.versionString ? ` (${codexInfo.versionString})` : ""}\n`);
468
483
  const childEnv = {
@@ -316,7 +316,6 @@ export function parseSliceResult(raw) {
316
316
  const parsedFinalOutput = parseEmbeddedText(finalOutput);
317
317
  if (parsedFinalOutput)
318
318
  return parsedFinalOutput;
319
- return null;
320
319
  }
321
320
  if (typeof finalOutput === "string") {
322
321
  const parsedFinalOutput = parseSliceJsonText(finalOutput);
@@ -330,7 +329,6 @@ export function parseSliceResult(raw) {
330
329
  const parsedStructured = parseEmbeddedText(structured);
331
330
  if (parsedStructured)
332
331
  return parsedStructured;
333
- return null;
334
332
  }
335
333
  if (typeof structured === "string") {
336
334
  const parsedStructured = parseSliceJsonText(structured);
@@ -80,6 +80,36 @@ export function renderKickoffMessage(input) {
80
80
  typeof runtimeSettings.decision_auto_resolve_guarded_enabled === "boolean"
81
81
  ? `- Guarded auto-resolve: ${runtimeSettings.decision_auto_resolve_guarded_enabled ? "enabled" : "disabled"}`
82
82
  : null,
83
+ runtimeSettings &&
84
+ typeof runtimeSettings.question_auto_answer_enabled === "boolean"
85
+ ? `- Question auto-answer: ${runtimeSettings.question_auto_answer_enabled ? "enabled" : "disabled"}`
86
+ : null,
87
+ runtimeSettings &&
88
+ typeof runtimeSettings.question_auto_answer_timeout_sec === "number"
89
+ ? `- Question auto-answer timeout: ${Math.max(10, Math.min(3600, Math.floor(runtimeSettings.question_auto_answer_timeout_sec)))}s`
90
+ : runtimeSettings &&
91
+ typeof runtimeSettings.question_auto_answer_delay_seconds === "number"
92
+ ? `- Question auto-answer delay: ${Math.max(1, Math.min(900, Math.floor(runtimeSettings.question_auto_answer_delay_seconds)))}s`
93
+ : null,
94
+ runtimeSettings &&
95
+ typeof runtimeSettings.question_auto_answer_policy === "string"
96
+ ? `- Question auto-answer policy: ${runtimeSettings.question_auto_answer_policy}`
97
+ : null,
98
+ runtimeSettings &&
99
+ typeof runtimeSettings.question_blocking_behavior === "string"
100
+ ? `- Blocking question behavior: ${runtimeSettings.question_blocking_behavior}`
101
+ : null,
102
+ runtimeSettings &&
103
+ typeof runtimeSettings.question_auto_answer_action === "string"
104
+ ? `- Question auto-answer action: ${runtimeSettings.question_auto_answer_action.trim().toLowerCase() === "reject"
105
+ ? "reject"
106
+ : "approve"}`
107
+ : null,
108
+ runtimeSettings &&
109
+ runtimeSettings.workspace_question_defaults &&
110
+ typeof runtimeSettings.workspace_question_defaults === "object"
111
+ ? "- Workspace question defaults available"
112
+ : null,
83
113
  ].filter((line) => Boolean(line));
84
114
  const contextHash = kickoff.context_hash?.trim() || null;
85
115
  const schemaVersion = (kickoff.schema_version ?? "").trim();
@@ -137,6 +137,14 @@ function resolveSliceRunId(item, metadata) {
137
137
  return item.runId.trim();
138
138
  return null;
139
139
  }
140
+ function resolveDecisionSliceRunId(decision, metadata) {
141
+ return (metadataString(metadata, ["slice_run_id", "sliceRunId", "run_id", "runId", "correlation_id", "correlationId"]) ??
142
+ normalizeText(decision.runId) ??
143
+ normalizeText(decision.run_id) ??
144
+ normalizeText(decision.correlationId) ??
145
+ normalizeText(decision.correlation_id) ??
146
+ null);
147
+ }
140
148
  function resolveCorrelationId(item, metadata) {
141
149
  return (metadataString(metadata, ["correlation_id", "correlationId"]) ??
142
150
  metadataString(metadata, ["run_id", "runId"]) ??
@@ -267,6 +275,8 @@ function createProjection(sliceRunId) {
267
275
  _statusUpdatedEpoch: 0,
268
276
  _updatedEpoch: 0,
269
277
  _artifactIds: new Set(),
278
+ _hasExplicitCompletion: false,
279
+ _peakReportedArtifacts: 0,
270
280
  };
271
281
  }
272
282
  function upsertProjection(map, sliceRunId) {
@@ -532,6 +542,7 @@ export function buildSliceRunProjections(input) {
532
542
  if (event === "autopilot_slice_finished" || event === "autopilot_slice_result") {
533
543
  const parsedStatus = (metadataString(metadata, ["parsed_status", "status"]) ?? "").toLowerCase();
534
544
  const reportedArtifacts = Math.max(0, Math.floor(metadataNumber(metadata, ["artifacts"]) ?? 0));
545
+ projection._peakReportedArtifacts = Math.max(projection._peakReportedArtifacts, reportedArtifacts);
535
546
  projection.decisionCount = Math.max(projection.decisionCount, Math.max(0, Math.floor(metadataNumber(metadata, ["decisions"]) ?? 0)));
536
547
  projection.blockingDecisionCount = Math.max(projection.blockingDecisionCount, Math.max(0, Math.floor(metadataNumber(metadata, ["blocking_decisions"]) ?? 0)));
537
548
  if (parsedStatus === "error") {
@@ -621,6 +632,7 @@ export function buildSliceRunProjections(input) {
621
632
  continue;
622
633
  }
623
634
  if (item.type === "run_completed") {
635
+ projection._hasExplicitCompletion = true;
624
636
  setStatus({
625
637
  projection,
626
638
  status: "completed",
@@ -698,7 +710,9 @@ export function buildSliceRunProjections(input) {
698
710
  for (const decision of input.decisions) {
699
711
  const decisionRecord = asRecord(decision);
700
712
  const metadata = asRecord(decisionRecord?.metadata);
701
- const sliceRunId = metadataString(metadata, ["slice_run_id", "sliceRunId", "run_id", "runId", "correlation_id", "correlationId"]);
713
+ const sliceRunId = decisionRecord
714
+ ? resolveDecisionSliceRunId(decisionRecord, metadata)
715
+ : null;
702
716
  if (!sliceRunId)
703
717
  continue;
704
718
  const projection = projections.get(sliceRunId);
@@ -767,7 +781,10 @@ export function buildSliceRunProjections(input) {
767
781
  const nowEpoch = Date.now();
768
782
  const output = [];
769
783
  for (const projection of projections.values()) {
770
- if (projection.status === "completed" && !projection.hasArtifact) {
784
+ if (projection.status === "completed" &&
785
+ !projection.hasArtifact &&
786
+ !projection._hasExplicitCompletion &&
787
+ projection._peakReportedArtifacts === 0) {
771
788
  setStatus({
772
789
  projection,
773
790
  status: "needs_review",
@@ -26,6 +26,7 @@ import { readNextUpQueuePins, removeNextUpQueuePin, setNextUpQueuePinOrder, supp
26
26
  import { formatStatus, formatAgents, formatActivity, formatInitiatives, getOnboardingState, } from "../dashboard-api.js";
27
27
  import { loadLocalOpenClawSnapshot, loadLocalTurnDetail, toLocalLiveActivity, toLocalLiveAgents, toLocalLiveInitiatives, toLocalSessionTree, } from "../local-openclaw.js";
28
28
  import { defaultOutboxAdapter } from "../adapters/outbox.js";
29
+ import { appendToOutbox } from "../outbox.js";
29
30
  import { readAgentContexts, upsertAgentContext, upsertRunContext } from "../agent-context-store.js";
30
31
  import { getAgentRun, markAgentRunStopped, readAgentRuns, upsertAgentRun, } from "../agent-run-store.js";
31
32
  import { appendEntityComment, listEntityComments, mergeEntityComments, } from "../entity-comment-store.js";
@@ -201,7 +202,7 @@ async function mapWithConcurrency(items, concurrency, mapper) {
201
202
  }
202
203
  const ACTIVITY_WARM_THROTTLE_MS = 30_000;
203
204
  const activityWarmByKey = new Map();
204
- const SNAPSHOT_RESPONSE_CACHE_TTL_MS = 1_500;
205
+ const SNAPSHOT_RESPONSE_CACHE_TTL_MS = 800;
205
206
  const SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES = 16;
206
207
  const SNAPSHOT_ACTIVITY_PERSIST_MIN_INTERVAL_MS = 15_000;
207
208
  const SNAPSHOT_ACTIVITY_FINGERPRINT_DEPTH = 8;
@@ -222,6 +223,7 @@ const LIVE_WORKSPACE_INITIATIVE_STATUSES = [
222
223
  ];
223
224
  let lastSnapshotActivityPersistAt = 0;
224
225
  let lastSnapshotActivityFingerprint = "";
226
+ let snapshotCacheGeneration = 0;
225
227
  const snapshotResponseCache = new Map();
226
228
  const ACTIVITY_DECISION_EVENT_HINTS = new Set([
227
229
  "decision_buffered",
@@ -359,7 +361,7 @@ function readSnapshotResponseCache(key) {
359
361
  const entry = snapshotResponseCache.get(key);
360
362
  if (!entry)
361
363
  return null;
362
- if (entry.expiresAt <= Date.now()) {
364
+ if (entry.generation !== snapshotCacheGeneration || entry.expiresAt <= Date.now()) {
363
365
  snapshotResponseCache.delete(key);
364
366
  return null;
365
367
  }
@@ -369,6 +371,7 @@ function writeSnapshotResponseCache(key, payload) {
369
371
  const now = Date.now();
370
372
  snapshotResponseCache.set(key, {
371
373
  expiresAt: now + SNAPSHOT_RESPONSE_CACHE_TTL_MS,
374
+ generation: snapshotCacheGeneration,
372
375
  payload,
373
376
  });
374
377
  if (snapshotResponseCache.size <= SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES)
@@ -385,6 +388,7 @@ function writeSnapshotResponseCache(key, payload) {
385
388
  }
386
389
  }
387
390
  function clearSnapshotResponseCache() {
391
+ snapshotCacheGeneration += 1;
388
392
  snapshotResponseCache.clear();
389
393
  }
390
394
  function isUserScopedApiKey(apiKey) {
@@ -568,19 +572,29 @@ function applyAgentContextsToActivity(input, contexts) {
568
572
  return enrichActivityActorFields(nextItem);
569
573
  });
570
574
  }
575
+ function sessionNodeEpoch(node) {
576
+ const raw = node.updatedAt ?? node.lastEventAt ?? node.startedAt;
577
+ if (!raw)
578
+ return 0;
579
+ const epoch = Date.parse(raw);
580
+ return Number.isFinite(epoch) ? epoch : 0;
581
+ }
571
582
  function mergeSessionTrees(base, extra) {
572
- const seenNodes = new Set();
573
- const nodes = [];
574
- for (const node of base.nodes ?? []) {
575
- seenNodes.add(node.id);
576
- nodes.push(node);
577
- }
583
+ const nodeById = new Map();
584
+ for (const node of base.nodes ?? [])
585
+ nodeById.set(node.id, node);
578
586
  for (const node of extra.nodes ?? []) {
579
- if (seenNodes.has(node.id))
587
+ const existing = nodeById.get(node.id);
588
+ if (!existing) {
589
+ nodeById.set(node.id, node);
580
590
  continue;
581
- seenNodes.add(node.id);
582
- nodes.push(node);
591
+ }
592
+ const existingEpoch = sessionNodeEpoch(existing);
593
+ const extraEpoch = sessionNodeEpoch(node);
594
+ if (extraEpoch > existingEpoch)
595
+ nodeById.set(node.id, node);
583
596
  }
597
+ const nodes = Array.from(nodeById.values());
584
598
  const seenEdges = new Set();
585
599
  const edges = [];
586
600
  for (const edge of base.edges ?? []) {
@@ -616,82 +630,82 @@ function mergeSessionTrees(base, extra) {
616
630
  groups: Array.from(groupsById.values()),
617
631
  };
618
632
  }
619
- function mergeActivities(base, extra, limit) {
620
- const asMetadataRecord = (value) => {
621
- if (!value || typeof value !== "object" || Array.isArray(value))
622
- return null;
623
- return value;
624
- };
625
- const metadataString = (metadata, keys) => {
626
- if (!metadata)
627
- return null;
628
- for (const key of keys) {
629
- const value = metadata[key];
630
- if (typeof value !== "string")
631
- continue;
632
- const normalized = value.trim();
633
- if (normalized.length > 0)
634
- return normalized;
635
- }
633
+ function asActivityMetadataRecord(value) {
634
+ if (!value || typeof value !== "object" || Array.isArray(value))
636
635
  return null;
637
- };
638
- const semanticEvents = new Set([
639
- "autopilot_slice_result",
640
- "auto_continue_started",
641
- "auto_continue_stopped",
642
- "next_up_manual_dispatch_started",
643
- "autopilot_slice_mcp_handshake_failed",
644
- "autopilot_slice_timeout",
645
- "autopilot_slice_log_stall",
646
- "auto_continue_spawn_guard_blocked",
647
- "auto_continue_spawn_guard_rate_limited",
648
- "autopilot_autofix_scheduled",
649
- "autopilot_autofix_executed",
650
- "autopilot_autofix_skipped",
651
- ]);
652
- const semanticActivityKey = (item) => {
653
- const metadata = asMetadataRecord(item.metadata);
654
- const eventRaw = metadata?.event;
655
- const event = typeof eventRaw === "string" ? eventRaw.trim().toLowerCase() : "";
656
- if (!event || !semanticEvents.has(event))
657
- return null;
658
- const runLike = (typeof item.runId === "string" && item.runId.trim().length > 0
659
- ? item.runId.trim()
660
- : null) ??
661
- metadataString(metadata, [
662
- "run_id",
663
- "runId",
664
- "slice_run_id",
665
- "sliceRunId",
666
- "active_run_id",
667
- "activeRunId",
668
- "last_run_id",
669
- "lastRunId",
670
- ]);
671
- const correlationId = metadataString(metadata, ["correlation_id", "correlationId"]);
672
- const initiativeId = (typeof item.initiativeId === "string" && item.initiativeId.trim().length > 0
673
- ? item.initiativeId.trim()
674
- : null) ??
675
- metadataString(metadata, ["initiative_id", "initiativeId"]);
676
- const workstreamId = metadataString(metadata, ["workstream_id", "workstreamId"]);
677
- const taskId = metadataString(metadata, ["task_id", "taskId"]);
678
- const stopReason = metadataString(metadata, ["stop_reason", "stopReason"]);
679
- const parsedStatus = metadataString(metadata, ["parsed_status", "parsedStatus"]);
680
- const title = (item.title ?? "").trim().toLowerCase();
681
- if (!runLike && !correlationId && !workstreamId && !taskId)
682
- return null;
683
- return [
684
- event,
685
- initiativeId ?? "",
686
- workstreamId ?? "",
687
- taskId ?? "",
688
- runLike ?? "",
689
- correlationId ?? "",
690
- stopReason ?? "",
691
- parsedStatus ?? "",
692
- title,
693
- ].join("|");
694
- };
636
+ return value;
637
+ }
638
+ function activityMetadataStr(metadata, keys) {
639
+ if (!metadata)
640
+ return null;
641
+ for (const key of keys) {
642
+ const value = metadata[key];
643
+ if (typeof value !== "string")
644
+ continue;
645
+ const normalized = value.trim();
646
+ if (normalized.length > 0)
647
+ return normalized;
648
+ }
649
+ return null;
650
+ }
651
+ const SEMANTIC_ACTIVITY_EVENTS = new Set([
652
+ "autopilot_slice_result",
653
+ "auto_continue_started",
654
+ "auto_continue_stopped",
655
+ "next_up_manual_dispatch_started",
656
+ "autopilot_slice_mcp_handshake_failed",
657
+ "autopilot_slice_timeout",
658
+ "autopilot_slice_log_stall",
659
+ "auto_continue_spawn_guard_blocked",
660
+ "auto_continue_spawn_guard_rate_limited",
661
+ "autopilot_autofix_scheduled",
662
+ "autopilot_autofix_executed",
663
+ "autopilot_autofix_skipped",
664
+ ]);
665
+ function semanticActivityKey(item) {
666
+ const metadata = asActivityMetadataRecord(item.metadata);
667
+ const eventRaw = metadata?.event;
668
+ const event = typeof eventRaw === "string" ? eventRaw.trim().toLowerCase() : "";
669
+ if (!event || !SEMANTIC_ACTIVITY_EVENTS.has(event))
670
+ return null;
671
+ const runLike = (typeof item.runId === "string" && item.runId.trim().length > 0
672
+ ? item.runId.trim()
673
+ : null) ??
674
+ activityMetadataStr(metadata, [
675
+ "run_id",
676
+ "runId",
677
+ "slice_run_id",
678
+ "sliceRunId",
679
+ "active_run_id",
680
+ "activeRunId",
681
+ "last_run_id",
682
+ "lastRunId",
683
+ ]);
684
+ const correlationId = activityMetadataStr(metadata, ["correlation_id", "correlationId"]);
685
+ const initiativeId = (typeof item.initiativeId === "string" && item.initiativeId.trim().length > 0
686
+ ? item.initiativeId.trim()
687
+ : null) ??
688
+ activityMetadataStr(metadata, ["initiative_id", "initiativeId"]);
689
+ const workstreamId = activityMetadataStr(metadata, ["workstream_id", "workstreamId"]);
690
+ const taskId = activityMetadataStr(metadata, ["task_id", "taskId"]);
691
+ const stopReason = activityMetadataStr(metadata, ["stop_reason", "stopReason"]);
692
+ const parsedStatus = activityMetadataStr(metadata, ["parsed_status", "parsedStatus"]);
693
+ const title = (item.title ?? "").trim().toLowerCase();
694
+ if (!runLike && !correlationId && !workstreamId && !taskId)
695
+ return null;
696
+ return [
697
+ event,
698
+ initiativeId ?? "",
699
+ workstreamId ?? "",
700
+ taskId ?? "",
701
+ runLike ?? "",
702
+ correlationId ?? "",
703
+ stopReason ?? "",
704
+ parsedStatus ?? "",
705
+ title,
706
+ ].join("|");
707
+ }
708
+ function mergeActivities(base, extra, limit) {
695
709
  const merged = [...(base ?? []), ...(extra ?? [])].sort((a, b) => {
696
710
  const timestampDelta = Date.parse(b.timestamp) - Date.parse(a.timestamp);
697
711
  if (timestampDelta !== 0)
@@ -705,11 +719,11 @@ function mergeActivities(base, extra, limit) {
705
719
  if (seenIds.has(item.id))
706
720
  continue;
707
721
  seenIds.add(item.id);
708
- const semanticKey = semanticActivityKey(item);
709
- if (semanticKey && seenSemantic.has(semanticKey))
722
+ const sk = semanticActivityKey(item);
723
+ if (sk && seenSemantic.has(sk))
710
724
  continue;
711
- if (semanticKey)
712
- seenSemantic.add(semanticKey);
725
+ if (sk)
726
+ seenSemantic.add(sk);
713
727
  deduped.push(item);
714
728
  if (deduped.length >= limit)
715
729
  break;
@@ -2058,6 +2072,9 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2058
2072
  return 3;
2059
2073
  };
2060
2074
  const sortQueueItems = (a, b) => {
2075
+ const queueDelta = queueRank(a.queueState) - queueRank(b.queueState);
2076
+ if (queueDelta !== 0)
2077
+ return queueDelta;
2061
2078
  const aPinnedRank = pinnedRankByKey.get(`${a.initiativeId}:${a.workstreamId}`);
2062
2079
  const bPinnedRank = pinnedRankByKey.get(`${b.initiativeId}:${b.workstreamId}`);
2063
2080
  if (aPinnedRank !== undefined || bPinnedRank !== undefined) {
@@ -2066,9 +2083,6 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2066
2083
  if (aRank !== bRank)
2067
2084
  return aRank - bRank;
2068
2085
  }
2069
- const queueDelta = queueRank(a.queueState) - queueRank(b.queueState);
2070
- if (queueDelta !== 0)
2071
- return queueDelta;
2072
2086
  const priorityRank = (value) => {
2073
2087
  const normalized = (value ?? "").trim().toLowerCase();
2074
2088
  if (!normalized)
@@ -3147,6 +3161,49 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3147
3161
  catch (err) {
3148
3162
  console.error(`[markRunCompleted] appendActivity failed for ${normalizedRunId}:`, err);
3149
3163
  }
3164
+ // ── Write to outbox so snapshot merge picks up the completion ───────
3165
+ try {
3166
+ const outboxSessionId = runtimeRecord?.initiativeId ?? existingRun?.initiativeId ?? normalizedRunId;
3167
+ const outboxActivityItem = {
3168
+ id: randomUUID(),
3169
+ type: "run_completed",
3170
+ title: message,
3171
+ description: reason,
3172
+ agentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
3173
+ agentName: runtimeRecord?.agentName ?? null,
3174
+ requesterAgentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
3175
+ requesterAgentName: runtimeRecord?.agentName ?? null,
3176
+ executorAgentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
3177
+ executorAgentName: runtimeRecord?.agentName ?? null,
3178
+ runId: normalizedRunId,
3179
+ initiativeId: runtimeRecord?.initiativeId ?? existingRun?.initiativeId ?? null,
3180
+ timestamp: nowIso,
3181
+ phase: "completed",
3182
+ state: "done",
3183
+ summary: message,
3184
+ metadata: {
3185
+ source: "dashboard_manual_complete",
3186
+ reason,
3187
+ remoteOk,
3188
+ event: "dashboard_run_mark_completed",
3189
+ },
3190
+ };
3191
+ await appendToOutbox(outboxSessionId, {
3192
+ id: outboxActivityItem.id,
3193
+ type: "outcome",
3194
+ timestamp: nowIso,
3195
+ payload: {
3196
+ runId: normalizedRunId,
3197
+ status: "completed",
3198
+ reason,
3199
+ remoteOk,
3200
+ },
3201
+ activityItem: outboxActivityItem,
3202
+ });
3203
+ }
3204
+ catch (err) {
3205
+ console.error(`[markRunCompleted] outbox write failed for ${normalizedRunId}:`, err);
3206
+ }
3150
3207
  return {
3151
3208
  ok: true,
3152
3209
  data: {
@@ -3357,6 +3414,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3357
3414
  applyAgentContextsToActivity,
3358
3415
  mergeSessionTrees,
3359
3416
  mergeActivities,
3417
+ semanticActivityKey,
3360
3418
  listRuntimeInstances,
3361
3419
  injectRuntimeInstancesAsSessions,
3362
3420
  enrichSessionsWithRuntime,
@@ -40,6 +40,65 @@ function readOptionalBoolean(payload, ...keys) {
40
40
  }
41
41
  return undefined;
42
42
  }
43
+ function readOptionalInteger(payload, options, ...keys) {
44
+ for (const key of keys) {
45
+ const raw = payload[key];
46
+ let parsed = Number.NaN;
47
+ if (typeof raw === "number") {
48
+ parsed = raw;
49
+ }
50
+ else if (typeof raw === "string") {
51
+ const trimmed = raw.trim();
52
+ if (/^[+-]?\d+$/.test(trimmed)) {
53
+ parsed = Number.parseInt(trimmed, 10);
54
+ }
55
+ }
56
+ if (!Number.isFinite(parsed))
57
+ continue;
58
+ const normalized = Math.max(options.min, Math.min(options.max, Math.floor(parsed)));
59
+ return normalized;
60
+ }
61
+ return undefined;
62
+ }
63
+ function readOptionalAction(payload, ...keys) {
64
+ for (const key of keys) {
65
+ const raw = payload[key];
66
+ if (typeof raw !== "string")
67
+ continue;
68
+ const normalized = raw.trim().toLowerCase();
69
+ if (normalized === "approve" || normalized === "reject") {
70
+ return normalized;
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+ function readOptionalPolicy(payload, ...keys) {
76
+ for (const key of keys) {
77
+ const raw = payload[key];
78
+ if (typeof raw !== "string")
79
+ continue;
80
+ const normalized = raw.trim().toLowerCase();
81
+ if (normalized === "contextual" ||
82
+ normalized === "approve_non_blocking" ||
83
+ normalized === "defer_non_blocking") {
84
+ return normalized;
85
+ }
86
+ }
87
+ return undefined;
88
+ }
89
+ function readOptionalBlockingBehavior(payload, ...keys) {
90
+ for (const key of keys) {
91
+ const raw = payload[key];
92
+ if (typeof raw !== "string")
93
+ continue;
94
+ const normalized = raw.trim().toLowerCase();
95
+ if (normalized === "require_human" ||
96
+ normalized === "guarded_auto_resolve_then_human") {
97
+ return normalized;
98
+ }
99
+ }
100
+ return undefined;
101
+ }
43
102
  function normalizeRuntimeSettingsPatch(payload) {
44
103
  const runtime = toRecord(payload.runtime_settings ?? payload.runtimeSettings);
45
104
  const patch = {};
@@ -61,6 +120,34 @@ function normalizeRuntimeSettingsPatch(payload) {
61
120
  patch.decision_auto_resolve_guarded_enabled =
62
121
  decisionAutoResolveGuardedEnabled;
63
122
  }
123
+ const questionAutoAnswerEnabled = readOptionalBoolean(runtime, "question_auto_answer_enabled", "questionAutoAnswerEnabled");
124
+ if (typeof questionAutoAnswerEnabled === "boolean") {
125
+ patch.question_auto_answer_enabled = questionAutoAnswerEnabled;
126
+ }
127
+ const questionAutoAnswerDelaySeconds = readOptionalInteger(runtime, { min: 1, max: 900 }, "question_auto_answer_delay_seconds", "questionAutoAnswerDelaySeconds");
128
+ if (typeof questionAutoAnswerDelaySeconds === "number") {
129
+ patch.question_auto_answer_delay_seconds = questionAutoAnswerDelaySeconds;
130
+ }
131
+ const questionAutoAnswerAction = readOptionalAction(runtime, "question_auto_answer_action", "questionAutoAnswerAction");
132
+ if (questionAutoAnswerAction) {
133
+ patch.question_auto_answer_action = questionAutoAnswerAction;
134
+ }
135
+ const questionAutoAnswerTimeoutSec = readOptionalInteger(runtime, { min: 10, max: 3600 }, "question_auto_answer_timeout_sec", "questionAutoAnswerTimeoutSec");
136
+ if (typeof questionAutoAnswerTimeoutSec === "number") {
137
+ patch.question_auto_answer_timeout_sec = questionAutoAnswerTimeoutSec;
138
+ }
139
+ const questionAutoAnswerPolicy = readOptionalPolicy(runtime, "question_auto_answer_policy", "questionAutoAnswerPolicy");
140
+ if (questionAutoAnswerPolicy) {
141
+ patch.question_auto_answer_policy = questionAutoAnswerPolicy;
142
+ }
143
+ const questionBlockingBehavior = readOptionalBlockingBehavior(runtime, "question_blocking_behavior", "questionBlockingBehavior");
144
+ if (questionBlockingBehavior) {
145
+ patch.question_blocking_behavior = questionBlockingBehavior;
146
+ }
147
+ const questionPolicyVersion = readOptionalInteger(runtime, { min: 1, max: 10 }, "question_policy_version", "questionPolicyVersion");
148
+ if (typeof questionPolicyVersion === "number") {
149
+ patch.question_policy_version = questionPolicyVersion;
150
+ }
64
151
  const customRunInstructions = toOptionalString(runtime.custom_run_instructions ?? runtime.customRunInstructions);
65
152
  if (typeof customRunInstructions === "string") {
66
153
  patch.custom_run_instructions = customRunInstructions.slice(0, 4000);