@useorgx/openclaw-plugin 0.7.11 → 0.7.16

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 (198) hide show
  1. package/README.md +94 -122
  2. package/dashboard/dist/assets/8pbG6uLK.js +1 -0
  3. package/dashboard/dist/assets/8pbG6uLK.js.br +0 -0
  4. package/dashboard/dist/assets/8pbG6uLK.js.gz +0 -0
  5. package/dashboard/dist/assets/B1LENRC8.js +212 -0
  6. package/dashboard/dist/assets/B1LENRC8.js.br +0 -0
  7. package/dashboard/dist/assets/B1LENRC8.js.gz +0 -0
  8. package/dashboard/dist/assets/{tcEHYcbW.js → BCudUvwg.js} +1 -1
  9. package/dashboard/dist/assets/BCudUvwg.js.br +0 -0
  10. package/dashboard/dist/assets/BCudUvwg.js.gz +0 -0
  11. package/dashboard/dist/assets/{Du1wfrXa.js → BM75sh1f.js} +2 -2
  12. package/dashboard/dist/assets/BM75sh1f.js.br +0 -0
  13. package/dashboard/dist/assets/BM75sh1f.js.gz +0 -0
  14. package/dashboard/dist/assets/BV0BcV1u.js +53 -0
  15. package/dashboard/dist/assets/BV0BcV1u.js.br +0 -0
  16. package/dashboard/dist/assets/BV0BcV1u.js.gz +0 -0
  17. package/dashboard/dist/assets/BYVYH9CH.js +1 -0
  18. package/dashboard/dist/assets/BYVYH9CH.js.br +0 -0
  19. package/dashboard/dist/assets/BYVYH9CH.js.gz +0 -0
  20. package/dashboard/dist/assets/BjK42gtU.js +1 -0
  21. package/dashboard/dist/assets/BjK42gtU.js.br +0 -0
  22. package/dashboard/dist/assets/BjK42gtU.js.gz +0 -0
  23. package/dashboard/dist/assets/BkMrrjAv.js +1 -0
  24. package/dashboard/dist/assets/BkMrrjAv.js.br +0 -0
  25. package/dashboard/dist/assets/BkMrrjAv.js.gz +0 -0
  26. package/dashboard/dist/assets/BpF7v1Dk.js +1 -0
  27. package/dashboard/dist/assets/BpF7v1Dk.js.br +0 -0
  28. package/dashboard/dist/assets/BpF7v1Dk.js.gz +0 -0
  29. package/dashboard/dist/assets/{AqVoI3SF.js → Bv_86bUY.js} +1 -1
  30. package/dashboard/dist/assets/Bv_86bUY.js.br +0 -0
  31. package/dashboard/dist/assets/Bv_86bUY.js.gz +0 -0
  32. package/dashboard/dist/assets/C-MOJWHs.js +1 -0
  33. package/dashboard/dist/assets/C-MOJWHs.js.br +0 -0
  34. package/dashboard/dist/assets/C-MOJWHs.js.gz +0 -0
  35. package/dashboard/dist/assets/C3PrI8L7.js +1 -0
  36. package/dashboard/dist/assets/C3PrI8L7.js.br +0 -0
  37. package/dashboard/dist/assets/C3PrI8L7.js.gz +0 -0
  38. package/dashboard/dist/assets/{CD-q5mdP.js → C6AqbA9J.js} +1 -1
  39. package/dashboard/dist/assets/C6AqbA9J.js.br +0 -0
  40. package/dashboard/dist/assets/C6AqbA9J.js.gz +0 -0
  41. package/dashboard/dist/assets/CSlBSRyv.js +1 -0
  42. package/dashboard/dist/assets/CSlBSRyv.js.br +0 -0
  43. package/dashboard/dist/assets/CSlBSRyv.js.gz +0 -0
  44. package/dashboard/dist/assets/{beHYBbh6.js → CUXb_4F3.js} +2 -2
  45. package/dashboard/dist/assets/CUXb_4F3.js.br +0 -0
  46. package/dashboard/dist/assets/CUXb_4F3.js.gz +0 -0
  47. package/dashboard/dist/assets/Cn8sRTkO.js +1 -0
  48. package/dashboard/dist/assets/Cn8sRTkO.js.br +0 -0
  49. package/dashboard/dist/assets/Cn8sRTkO.js.gz +0 -0
  50. package/dashboard/dist/assets/D5IgXoTj.js +1 -0
  51. package/dashboard/dist/assets/D5IgXoTj.js.br +0 -0
  52. package/dashboard/dist/assets/D5IgXoTj.js.gz +0 -0
  53. package/dashboard/dist/assets/DMKyYAtD.js +1 -0
  54. package/dashboard/dist/assets/DMKyYAtD.js.br +0 -0
  55. package/dashboard/dist/assets/DMKyYAtD.js.gz +0 -0
  56. package/dashboard/dist/assets/{DCP-C7fn.js → DXzpQUC0.js} +1 -1
  57. package/dashboard/dist/assets/DXzpQUC0.js.br +0 -0
  58. package/dashboard/dist/assets/DXzpQUC0.js.gz +0 -0
  59. package/dashboard/dist/assets/Dj2k1r16.js +8 -0
  60. package/dashboard/dist/assets/Dj2k1r16.js.br +0 -0
  61. package/dashboard/dist/assets/Dj2k1r16.js.gz +0 -0
  62. package/dashboard/dist/assets/JDPvhd68.js +1 -0
  63. package/dashboard/dist/assets/JDPvhd68.js.br +0 -0
  64. package/dashboard/dist/assets/JDPvhd68.js.gz +0 -0
  65. package/dashboard/dist/assets/R6N_VVqm.js +1 -0
  66. package/dashboard/dist/assets/R6N_VVqm.js.br +0 -0
  67. package/dashboard/dist/assets/R6N_VVqm.js.gz +0 -0
  68. package/dashboard/dist/assets/cEP7N1dn.js +1 -0
  69. package/dashboard/dist/assets/cEP7N1dn.js.br +0 -0
  70. package/dashboard/dist/assets/cEP7N1dn.js.gz +0 -0
  71. package/dashboard/dist/assets/cX2e-TLi.js +1 -0
  72. package/dashboard/dist/assets/cX2e-TLi.js.br +0 -0
  73. package/dashboard/dist/assets/cX2e-TLi.js.gz +0 -0
  74. package/dashboard/dist/assets/eeHXe_OQ.js +9 -0
  75. package/dashboard/dist/assets/eeHXe_OQ.js.br +0 -0
  76. package/dashboard/dist/assets/eeHXe_OQ.js.gz +0 -0
  77. package/dashboard/dist/assets/iLnvdWmW.css +1 -0
  78. package/dashboard/dist/assets/iLnvdWmW.css.br +0 -0
  79. package/dashboard/dist/assets/iLnvdWmW.css.gz +0 -0
  80. package/dashboard/dist/assets/konqMbVI.js +1 -0
  81. package/dashboard/dist/assets/konqMbVI.js.br +0 -0
  82. package/dashboard/dist/assets/konqMbVI.js.gz +0 -0
  83. package/dashboard/dist/assets/wc6cgXzV.js +1 -0
  84. package/dashboard/dist/assets/wc6cgXzV.js.br +0 -0
  85. package/dashboard/dist/assets/wc6cgXzV.js.gz +0 -0
  86. package/dashboard/dist/brand/control-tower.png +0 -0
  87. package/dashboard/dist/brand/design-codex.png +0 -0
  88. package/dashboard/dist/brand/engineering-autopilot.png +0 -0
  89. package/dashboard/dist/brand/launch-captain.png +0 -0
  90. package/dashboard/dist/brand/orgx-logo.png +0 -0
  91. package/dashboard/dist/brand/pipeline-intelligence.png +0 -0
  92. package/dashboard/dist/brand/product-orchestrator.png +0 -0
  93. package/dashboard/dist/brand/xandy-orchestrator.png +0 -0
  94. package/dashboard/dist/index.html +8 -6
  95. package/dashboard/dist/index.html.br +0 -0
  96. package/dashboard/dist/index.html.gz +0 -0
  97. package/dist/hash-utils.d.ts +1 -0
  98. package/dist/hash-utils.js +4 -0
  99. package/dist/http/helpers/auto-continue-engine.d.ts +15 -0
  100. package/dist/http/helpers/auto-continue-engine.js +289 -67
  101. package/dist/http/helpers/autopilot-slice-utils.js +112 -66
  102. package/dist/http/helpers/hash-utils.d.ts +1 -1
  103. package/dist/http/helpers/hash-utils.js +1 -1
  104. package/dist/http/helpers/mission-control.d.ts +3 -0
  105. package/dist/http/helpers/mission-control.js +43 -9
  106. package/dist/http/helpers/queue-constants.d.ts +37 -0
  107. package/dist/http/helpers/queue-constants.js +34 -0
  108. package/dist/http/helpers/slice-experience-v2.js +2 -5
  109. package/dist/http/helpers/slice-run-projections.js +2 -5
  110. package/dist/http/helpers/value-utils.d.ts +1 -1
  111. package/dist/http/helpers/value-utils.js +5 -2
  112. package/dist/http/helpers/workspace-scope.js +4 -3
  113. package/dist/http/index.js +104 -61
  114. package/dist/http/routes/chat.d.ts +1 -1
  115. package/dist/http/routes/chat.js +3 -23
  116. package/dist/http/routes/entities.js +60 -2
  117. package/dist/http/routes/entity-dynamic.js +49 -9
  118. package/dist/http/routes/live-snapshot.d.ts +10 -1
  119. package/dist/http/routes/live-snapshot.js +15 -26
  120. package/dist/http/routes/mission-control-actions.d.ts +6 -0
  121. package/dist/http/routes/mission-control-actions.js +35 -18
  122. package/dist/http/routes/mission-control-read.js +4 -107
  123. package/dist/lib/type-coercion.d.ts +10 -0
  124. package/dist/lib/type-coercion.js +82 -0
  125. package/dist/mcp-http-handler.js +14 -2
  126. package/dist/openclaw.plugin.json +1 -1
  127. package/dist/services/experiment-randomization.js +9 -2
  128. package/openclaw.plugin.json +1 -1
  129. package/package.json +3 -2
  130. package/dashboard/dist/assets/AqVoI3SF.js.br +0 -0
  131. package/dashboard/dist/assets/AqVoI3SF.js.gz +0 -0
  132. package/dashboard/dist/assets/BC4WvnHJ.js +0 -1
  133. package/dashboard/dist/assets/BC4WvnHJ.js.br +0 -0
  134. package/dashboard/dist/assets/BC4WvnHJ.js.gz +0 -0
  135. package/dashboard/dist/assets/BG5mwTkg.js +0 -1
  136. package/dashboard/dist/assets/BG5mwTkg.js.br +0 -0
  137. package/dashboard/dist/assets/BG5mwTkg.js.gz +0 -0
  138. package/dashboard/dist/assets/BJgZIVUQ.js +0 -53
  139. package/dashboard/dist/assets/BJgZIVUQ.js.br +0 -0
  140. package/dashboard/dist/assets/BJgZIVUQ.js.gz +0 -0
  141. package/dashboard/dist/assets/BNh-XYPV.js +0 -1
  142. package/dashboard/dist/assets/BNh-XYPV.js.br +0 -0
  143. package/dashboard/dist/assets/BNh-XYPV.js.gz +0 -0
  144. package/dashboard/dist/assets/BTAEErUY.js +0 -1
  145. package/dashboard/dist/assets/BTAEErUY.js.br +0 -0
  146. package/dashboard/dist/assets/BTAEErUY.js.gz +0 -0
  147. package/dashboard/dist/assets/BepW_590.js +0 -1
  148. package/dashboard/dist/assets/BepW_590.js.br +0 -0
  149. package/dashboard/dist/assets/BepW_590.js.gz +0 -0
  150. package/dashboard/dist/assets/BerAfzjq.js +0 -1
  151. package/dashboard/dist/assets/BerAfzjq.js.br +0 -0
  152. package/dashboard/dist/assets/BerAfzjq.js.gz +0 -0
  153. package/dashboard/dist/assets/Bp3N-QL5.js +0 -212
  154. package/dashboard/dist/assets/Bp3N-QL5.js.br +0 -0
  155. package/dashboard/dist/assets/Bp3N-QL5.js.gz +0 -0
  156. package/dashboard/dist/assets/C-KIc3Wc.js +0 -1
  157. package/dashboard/dist/assets/C-KIc3Wc.js.br +0 -0
  158. package/dashboard/dist/assets/C-KIc3Wc.js.gz +0 -0
  159. package/dashboard/dist/assets/C3dZRz9P.css +0 -1
  160. package/dashboard/dist/assets/C3dZRz9P.css.br +0 -0
  161. package/dashboard/dist/assets/C3dZRz9P.css.gz +0 -0
  162. package/dashboard/dist/assets/CD-q5mdP.js.br +0 -0
  163. package/dashboard/dist/assets/CD-q5mdP.js.gz +0 -0
  164. package/dashboard/dist/assets/CL_wXqR7.js +0 -1
  165. package/dashboard/dist/assets/CL_wXqR7.js.br +0 -0
  166. package/dashboard/dist/assets/CL_wXqR7.js.gz +0 -0
  167. package/dashboard/dist/assets/CdvjC9G9.js +0 -1
  168. package/dashboard/dist/assets/CdvjC9G9.js.br +0 -0
  169. package/dashboard/dist/assets/CdvjC9G9.js.gz +0 -0
  170. package/dashboard/dist/assets/Ck2agw-s.js +0 -1
  171. package/dashboard/dist/assets/Ck2agw-s.js.br +0 -0
  172. package/dashboard/dist/assets/Ck2agw-s.js.gz +0 -0
  173. package/dashboard/dist/assets/CxQ08qFN.js +0 -9
  174. package/dashboard/dist/assets/CxQ08qFN.js.br +0 -0
  175. package/dashboard/dist/assets/CxQ08qFN.js.gz +0 -0
  176. package/dashboard/dist/assets/D2CH1H6k.js +0 -1
  177. package/dashboard/dist/assets/D2CH1H6k.js.br +0 -0
  178. package/dashboard/dist/assets/D2CH1H6k.js.gz +0 -0
  179. package/dashboard/dist/assets/D9esz7jd.js +0 -1
  180. package/dashboard/dist/assets/D9esz7jd.js.br +0 -0
  181. package/dashboard/dist/assets/D9esz7jd.js.gz +0 -0
  182. package/dashboard/dist/assets/DCP-C7fn.js.br +0 -0
  183. package/dashboard/dist/assets/DCP-C7fn.js.gz +0 -0
  184. package/dashboard/dist/assets/DJASCd69.js +0 -1
  185. package/dashboard/dist/assets/DJASCd69.js.br +0 -0
  186. package/dashboard/dist/assets/DJASCd69.js.gz +0 -0
  187. package/dashboard/dist/assets/Dm9AybAp.js +0 -1
  188. package/dashboard/dist/assets/Dm9AybAp.js.br +0 -0
  189. package/dashboard/dist/assets/Dm9AybAp.js.gz +0 -0
  190. package/dashboard/dist/assets/Du1wfrXa.js.br +0 -0
  191. package/dashboard/dist/assets/Du1wfrXa.js.gz +0 -0
  192. package/dashboard/dist/assets/beHYBbh6.js.br +0 -0
  193. package/dashboard/dist/assets/beHYBbh6.js.gz +0 -0
  194. package/dashboard/dist/assets/cNrhgGc1.js +0 -8
  195. package/dashboard/dist/assets/cNrhgGc1.js.br +0 -0
  196. package/dashboard/dist/assets/cNrhgGc1.js.gz +0 -0
  197. package/dashboard/dist/assets/tcEHYcbW.js.br +0 -0
  198. package/dashboard/dist/assets/tcEHYcbW.js.gz +0 -0
@@ -46,10 +46,11 @@ import { summarizeActivityHeadline } from "./helpers/activity-headline.js";
46
46
  import { createAutoContinueEngine, } from "./helpers/auto-continue-engine.js";
47
47
  import { createAutopilotOperations, } from "./helpers/autopilot-operations.js";
48
48
  import { mapDecisionEntity } from "./helpers/decision-mapper.js";
49
- import { idempotencyKey, stableHash } from "./helpers/hash-utils.js";
49
+ import { deterministicActivityId, idempotencyKey, stableHash } from "./helpers/hash-utils.js";
50
50
  import { createCodexBinResolver, } from "./helpers/autopilot-slice-utils.js";
51
51
  import { createLocalArtifactDetailFallbackBuilder } from "./helpers/artifact-fallback.js";
52
52
  import { buildMissionControlGraph, deriveExecutionPolicy, dedupeStrings, isDispatchableWorkstreamStatus, isDoneStatus, isInProgressStatus, isTodoStatus, listEntitiesSafe, normalizeEntityMutationPayload, pickStringArray, resolveAutoAssignments, selectSliceTasksByScope, } from "./helpers/mission-control.js";
53
+ import { QueueState, LaneState, RunStatus, RunnerSource, } from "./helpers/queue-constants.js";
53
54
  import { configureOpenClawProviderRouting, fetchBillingStatusSafe, isPidAlive, listOpenClawAgents, listOpenClawProviderModels, modelImpliesByok, normalizeOpenClawProvider, resolveAutoOpenClawProvider, resolveByokEnvOverrides, spawnOpenClawAgentTurn, stopDetachedProcess, } from "./helpers/openclaw-cli.js";
54
55
  import { fetchKickoffContextSafe, renderKickoffMessage } from "./helpers/kickoff-context.js";
55
56
  import { createDispatchLifecycle } from "./helpers/dispatch-lifecycle.js";
@@ -177,6 +178,9 @@ function safeErrorMessage(err) {
177
178
  if (normalized.includes("failed to fetch") || normalized.includes("network")) {
178
179
  return "network request failed";
179
180
  }
181
+ if (normalized.includes("relation does not exist")) {
182
+ return sanitized;
183
+ }
180
184
  if (normalized.includes("internal_error") || normalized.includes("internal server error")) {
181
185
  return "temporary server issue";
182
186
  }
@@ -380,6 +384,16 @@ function deriveStructuredActivityBucket(input) {
380
384
  "nonBlockingDecisionCount",
381
385
  ]) ?? 0;
382
386
  if (event === "autopilot_slice_result") {
387
+ const parsedStatus = typeof metadata?.parsed_status === "string"
388
+ ? metadata.parsed_status.trim().toLowerCase()
389
+ : typeof metadata?.parsedStatus === "string"
390
+ ? metadata.parsedStatus.trim().toLowerCase()
391
+ : "";
392
+ if (parsedStatus === "completed" && decisionRequired === false) {
393
+ if (artifacts > 0)
394
+ return "artifact";
395
+ return "message";
396
+ }
383
397
  // Any blocked slice result needs decision-first surfacing in the Activity UX.
384
398
  if (input.phase === "blocked")
385
399
  return "decision";
@@ -712,26 +726,10 @@ function activityMetadataStr(metadata, keys) {
712
726
  }
713
727
  return null;
714
728
  }
715
- const SEMANTIC_ACTIVITY_EVENTS = new Set([
716
- "autopilot_slice_result",
717
- "auto_continue_started",
718
- "auto_continue_stopped",
719
- "next_up_manual_dispatch_started",
720
- "autopilot_slice_mcp_handshake_failed",
721
- "autopilot_slice_timeout",
722
- "autopilot_slice_log_stall",
723
- "auto_continue_spawn_guard_blocked",
724
- "auto_continue_spawn_guard_rate_limited",
725
- "autopilot_autofix_scheduled",
726
- "autopilot_autofix_executed",
727
- "autopilot_autofix_skipped",
728
- ]);
729
729
  function semanticActivityKey(item) {
730
730
  const metadata = asActivityMetadataRecord(item.metadata);
731
731
  const eventRaw = metadata?.event;
732
732
  const event = typeof eventRaw === "string" ? eventRaw.trim().toLowerCase() : "";
733
- if (!event || !SEMANTIC_ACTIVITY_EVENTS.has(event))
734
- return null;
735
733
  const runLike = (typeof item.runId === "string" && item.runId.trim().length > 0
736
734
  ? item.runId.trim()
737
735
  : null) ??
@@ -755,6 +753,8 @@ function semanticActivityKey(item) {
755
753
  const stopReason = activityMetadataStr(metadata, ["stop_reason", "stopReason"]);
756
754
  const parsedStatus = activityMetadataStr(metadata, ["parsed_status", "parsedStatus"]);
757
755
  const title = (item.title ?? "").trim().toLowerCase();
756
+ if (!event && !runLike && !correlationId)
757
+ return null;
758
758
  if (!runLike && !correlationId && !workstreamId && !taskId)
759
759
  return null;
760
760
  return [
@@ -769,6 +769,27 @@ function semanticActivityKey(item) {
769
769
  title,
770
770
  ].join("|");
771
771
  }
772
+ function activityRichness(item) {
773
+ let score = 0;
774
+ const meta = item.metadata;
775
+ if (meta)
776
+ score += Object.keys(meta).length;
777
+ if (item.summary)
778
+ score += 2;
779
+ if (item.description)
780
+ score += 2;
781
+ if (item.agentName)
782
+ score += 1;
783
+ if (item.runtimeLabel)
784
+ score += 1;
785
+ if (item.runtimeProvider)
786
+ score += 1;
787
+ if (item.phase)
788
+ score += 1;
789
+ if (item.state)
790
+ score += 1;
791
+ return score;
792
+ }
772
793
  function mergeActivities(base, extra, limit) {
773
794
  const merged = [...(base ?? []), ...(extra ?? [])].sort((a, b) => {
774
795
  const timestampDelta = Date.parse(b.timestamp) - Date.parse(a.timestamp);
@@ -779,15 +800,28 @@ function mergeActivities(base, extra, limit) {
779
800
  const deduped = [];
780
801
  const seenIds = new Set();
781
802
  const seenSemantic = new Set();
803
+ const semanticIdx = new Map();
782
804
  for (const item of merged) {
783
- if (seenIds.has(item.id))
805
+ if (seenIds.has(item.id)) {
806
+ const existingIdx = deduped.findIndex(d => d.id === item.id);
807
+ if (existingIdx >= 0 && activityRichness(item) > activityRichness(deduped[existingIdx])) {
808
+ deduped[existingIdx] = item;
809
+ }
784
810
  continue;
811
+ }
785
812
  seenIds.add(item.id);
786
813
  const sk = semanticActivityKey(item);
787
- if (sk && seenSemantic.has(sk))
814
+ if (sk && seenSemantic.has(sk)) {
815
+ const existingIdx = semanticIdx.get(sk);
816
+ if (activityRichness(item) > activityRichness(deduped[existingIdx])) {
817
+ deduped[existingIdx] = item;
818
+ }
788
819
  continue;
789
- if (sk)
820
+ }
821
+ if (sk) {
790
822
  seenSemantic.add(sk);
823
+ semanticIdx.set(sk, deduped.length);
824
+ }
791
825
  deduped.push(item);
792
826
  if (deduped.length >= limit)
793
827
  break;
@@ -1812,7 +1846,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1812
1846
  };
1813
1847
  const codexBinResolver = createCodexBinResolver();
1814
1848
  const resolveCodexBinInfo = () => codexBinResolver.resolveCodexBinInfo();
1815
- const { autoContinueRuns, autoContinueSliceRuns, localInitiativeStatusOverrides, writeRuntimeEvent, autoContinueTickMs: AUTO_CONTINUE_TICK_MS, defaultAutoContinueTokenBudget, defaultAutoContinueMaxParallelSlices, setLocalInitiativeStatusOverride, clearLocalInitiativeStatusOverride, applyLocalInitiativeOverrides, applyLocalInitiativeOverrideToGraph, updateInitiativeAutoContinueState, stopAutoContinueRun, tickAutoContinueRun, tickAllAutoContinue, isInitiativeActiveStatus, runningAutoContinueForWorkstream, getAutoContinueLaneForWorkstream, scheduleAutoFixForWorkstream, startAutoContinueRun, } = createAutoContinueEngine({
1849
+ const { autoContinueRuns, autoContinueSliceRuns, localInitiativeStatusOverrides, writeRuntimeEvent, autoContinueTickMs: AUTO_CONTINUE_TICK_MS, defaultAutoContinueTokenBudget, defaultAutoContinueMaxParallelSlices, setLocalInitiativeStatusOverride, clearLocalInitiativeStatusOverride, applyLocalInitiativeOverrides, applyLocalInitiativeOverrideToGraph, updateInitiativeAutoContinueState, stopAutoContinueRun, tickAutoContinueRun, tickAllAutoContinue, isInitiativeActiveStatus, runningAutoContinueForWorkstream, getAutoContinueLaneForWorkstream, scheduleAutoFixForWorkstream, startAutoContinueRun, skipCurrentWorkstream, getCanonicalAutopilotState, } = createAutoContinueEngine({
1816
1850
  client,
1817
1851
  filename: __filename,
1818
1852
  safeErrorMessage,
@@ -2173,11 +2207,11 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2173
2207
  initiativeStatusById.set(initiativeId, override.status);
2174
2208
  }
2175
2209
  const queueRank = (state) => {
2176
- if (state === "running")
2210
+ if (state === QueueState.RUNNING)
2177
2211
  return 0;
2178
- if (state === "queued")
2212
+ if (state === QueueState.QUEUED)
2179
2213
  return 1;
2180
- if (state === "blocked")
2214
+ if (state === QueueState.BLOCKED)
2181
2215
  return 2;
2182
2216
  return 3;
2183
2217
  };
@@ -2322,12 +2356,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2322
2356
  const hasRunning = statusValues.some((status) => isInProgressStatus(status));
2323
2357
  const hasQueued = statusValues.some((status) => status === "queued" || status === "pending");
2324
2358
  const queueState = hasRunning
2325
- ? "running"
2359
+ ? QueueState.RUNNING
2326
2360
  : hasBlocked
2327
- ? "blocked"
2361
+ ? QueueState.BLOCKED
2328
2362
  : hasQueued
2329
- ? "queued"
2330
- : "idle";
2363
+ ? QueueState.QUEUED
2364
+ : QueueState.IDLE;
2331
2365
  const latestRunner = [];
2332
2366
  const latestRunnerSeen = new Set();
2333
2367
  pushRunnerAgent(latestRunner, latestRunnerSeen, {
@@ -2341,7 +2375,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2341
2375
  const runnerAgentId = primaryRunner?.id ?? "unassigned";
2342
2376
  const runnerAgentName = primaryRunner?.name ?? "Unassigned";
2343
2377
  const pinKey = `${entry.initiativeId}:${entry.workstreamId}`;
2344
- if (isSuppressed(entry.initiativeId, entry.workstreamId) && queueState !== "running") {
2378
+ if (isSuppressed(entry.initiativeId, entry.workstreamId) && queueState !== QueueState.RUNNING) {
2345
2379
  continue;
2346
2380
  }
2347
2381
  fallbackItems.push({
@@ -2360,7 +2394,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2360
2394
  runnerAgentId,
2361
2395
  runnerAgentName,
2362
2396
  runnerAgents,
2363
- runnerSource: "fallback",
2397
+ runnerSource: RunnerSource.FALLBACK,
2364
2398
  queueState,
2365
2399
  blockReason: hasBlocked
2366
2400
  ? entry.blockers[0] ??
@@ -2545,7 +2579,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2545
2579
  : [];
2546
2580
  const runScopedToCurrentWorkstream = scopedAllowedWorkstreams.length === 1 &&
2547
2581
  scopedAllowedWorkstreams[0] === workstream.id &&
2548
- autoContinueRun?.status === "running";
2582
+ autoContinueRun?.status === RunStatus.RUNNING;
2549
2583
  const activeRunIds = Array.isArray(autoContinueRun?.activeSliceRunIds)
2550
2584
  ? autoContinueRun.activeSliceRunIds
2551
2585
  .filter((id) => typeof id === "string" && id.trim().length > 0)
@@ -2595,20 +2629,26 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2595
2629
  const sliceMilestoneId = defaultScope === "milestone"
2596
2630
  ? scopeSelection.milestoneIds[0] ?? pin?.preferredMilestoneId ?? null
2597
2631
  : null;
2598
- let queueState = laneState === "running"
2599
- ? "running"
2600
- : runScopedToCurrentWorkstream
2601
- ? "running"
2602
- : candidateTask
2603
- ? "queued"
2604
- : "idle";
2632
+ // Only mark as "running" when the lane is actively executing a slice.
2633
+ // runScopedToCurrentWorkstream means the initiative run targets this
2634
+ // workstream, but the lane may be idle between slices — don't conflate
2635
+ // "scoped to a running initiative" with "actively executing".
2636
+ const laneIsActivelyRunning = laneState === LaneState.RUNNING ||
2637
+ (runScopedToCurrentWorkstream && activeRunIds.length > 0);
2638
+ let queueState = laneIsActivelyRunning
2639
+ ? QueueState.RUNNING
2640
+ : candidateTask
2641
+ ? QueueState.QUEUED
2642
+ : runScopedToCurrentWorkstream
2643
+ ? QueueState.QUEUED
2644
+ : QueueState.IDLE;
2605
2645
  let blockReason = null;
2606
- if (laneState === "blocked") {
2607
- queueState = "blocked";
2646
+ if (laneState === LaneState.BLOCKED) {
2647
+ queueState = QueueState.BLOCKED;
2608
2648
  blockReason = autoContinueLane?.blockedReason ?? "Blocked";
2609
2649
  }
2610
- else if (laneState === "waiting_dependency") {
2611
- queueState = "blocked";
2650
+ else if (laneState === LaneState.WAITING_DEPENDENCY) {
2651
+ queueState = QueueState.BLOCKED;
2612
2652
  if (Array.isArray(autoContinueLane?.waitingOnWorkstreamIds) &&
2613
2653
  autoContinueLane.waitingOnWorkstreamIds.length > 0) {
2614
2654
  const waitingTitles = autoContinueLane.waitingOnWorkstreamIds
@@ -2626,12 +2666,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2626
2666
  blockReason = "Waiting on dependency workstreams";
2627
2667
  }
2628
2668
  }
2629
- else if (laneState === "rate_limited") {
2630
- queueState = "blocked";
2669
+ else if (laneState === LaneState.RATE_LIMITED) {
2670
+ queueState = QueueState.BLOCKED;
2631
2671
  blockReason = autoContinueLane?.blockedReason ?? "Rate-limited";
2632
2672
  }
2633
2673
  if (!autoContinueRun && !readyTask && candidateTask) {
2634
- queueState = "blocked";
2674
+ queueState = QueueState.BLOCKED;
2635
2675
  const blockedDeps = candidateTask.dependencyIds
2636
2676
  .map((depId) => nodeById.get(depId))
2637
2677
  .filter((dependency) => Boolean(dependency && !isDoneStatus(dependency.status)))
@@ -2649,7 +2689,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2649
2689
  if (!candidateTask && !autoContinueRun && !pin) {
2650
2690
  continue;
2651
2691
  }
2652
- if (isSuppressed(initiativeId, workstream.id) && queueState !== "running") {
2692
+ if (isSuppressed(initiativeId, workstream.id) && queueState !== QueueState.RUNNING) {
2653
2693
  continue;
2654
2694
  }
2655
2695
  runningWorkstreams.add(workstream.id);
@@ -2684,10 +2724,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2684
2724
  }
2685
2725
  const runnerAgents = assignedRunnerAgents.length > 0 ? assignedRunnerAgents : inferredRunnerAgents;
2686
2726
  const runnerSource = assignedRunnerAgents.length > 0
2687
- ? "assigned"
2727
+ ? RunnerSource.ASSIGNED
2688
2728
  : runnerAgents.length > 0
2689
- ? "inferred"
2690
- : "fallback";
2729
+ ? RunnerSource.INFERRED
2730
+ : RunnerSource.FALLBACK;
2691
2731
  const primaryRunner = runnerAgents[0] ?? null;
2692
2732
  const runnerAgentId = primaryRunner?.id ?? "unassigned";
2693
2733
  const runnerAgentName = primaryRunner?.name ?? "Unassigned";
@@ -2781,14 +2821,14 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2781
2821
  : activeTaskId
2782
2822
  ? [activeTaskId]
2783
2823
  : [];
2784
- const queueState = laneState === "running"
2785
- ? "running"
2786
- : laneState === "blocked" ||
2787
- laneState === "waiting_dependency" ||
2788
- laneState === "rate_limited"
2789
- ? "blocked"
2790
- : "queued";
2791
- if (isSuppressed(initiativeId, workstream.id) && queueState !== "running") {
2824
+ const queueState = laneState === LaneState.RUNNING
2825
+ ? QueueState.RUNNING
2826
+ : laneState === LaneState.BLOCKED ||
2827
+ laneState === LaneState.WAITING_DEPENDENCY ||
2828
+ laneState === LaneState.RATE_LIMITED
2829
+ ? QueueState.BLOCKED
2830
+ : QueueState.QUEUED;
2831
+ if (isSuppressed(initiativeId, workstream.id) && queueState !== QueueState.RUNNING) {
2792
2832
  continue;
2793
2833
  }
2794
2834
  const runRunnerAgents = [];
@@ -2814,9 +2854,9 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2814
2854
  runnerAgentId: runPrimaryRunner?.id ?? "unassigned",
2815
2855
  runnerAgentName: runPrimaryRunner?.name ?? "Unassigned",
2816
2856
  runnerAgents: runRunnerAgents,
2817
- runnerSource: runPrimaryRunner ? "inferred" : "fallback",
2857
+ runnerSource: runPrimaryRunner ? RunnerSource.INFERRED : RunnerSource.FALLBACK,
2818
2858
  queueState,
2819
- blockReason: queueState === "blocked"
2859
+ blockReason: queueState === QueueState.BLOCKED
2820
2860
  ? lane?.blockedReason ?? "Blocked"
2821
2861
  : null,
2822
2862
  isPinned: Boolean(pinnedByKey.get(`${initiativeId}:${workstream.id}`)),
@@ -3240,10 +3280,11 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3240
3280
  broadcastRuntimeSse("runtime.updated", runtimeRecord);
3241
3281
  }
3242
3282
  clearSnapshotResponseCache();
3283
+ const activityId = deterministicActivityId("run_completed", normalizedRunId, nowIso, runtimeRecord?.agentId ?? existingRun?.agentId ?? null, "dashboard_run_mark_completed");
3243
3284
  try {
3244
3285
  appendActivityItems([
3245
3286
  {
3246
- id: randomUUID(),
3287
+ id: activityId,
3247
3288
  type: "run_completed",
3248
3289
  title: message,
3249
3290
  description: reason,
@@ -3275,7 +3316,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3275
3316
  try {
3276
3317
  const outboxSessionId = runtimeRecord?.initiativeId ?? existingRun?.initiativeId ?? normalizedRunId;
3277
3318
  const outboxActivityItem = {
3278
- id: randomUUID(),
3319
+ id: activityId,
3279
3320
  type: "run_completed",
3280
3321
  title: message,
3281
3322
  description: reason,
@@ -3376,6 +3417,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3376
3417
  dispatchFallbackWorkstreamTurn,
3377
3418
  tickAutoContinueRun,
3378
3419
  stopAutoContinueRun,
3420
+ skipCurrentWorkstream,
3379
3421
  updateInitiativeAutoContinueState,
3380
3422
  tickAllAutoContinue,
3381
3423
  scheduleAutoFixForWorkstream,
@@ -3551,6 +3593,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3551
3593
  },
3552
3594
  runAction: (runId, action, input) => client.runAction(runId, action, input),
3553
3595
  listChatThreads: ({ commandCenterId, initiativeId, limit, offset }) => listChatThreads({ commandCenterId, initiativeId, limit, offset }),
3596
+ getCanonicalAutopilotState,
3554
3597
  sendJson,
3555
3598
  });
3556
3599
  registerRuntimeHookRoutes(apiRouter, {
@@ -3,7 +3,7 @@ type JsonRecord = Record<string, unknown>;
3
3
  type RegisterChatRoutesDeps<TReq, TRes> = {
4
4
  parseJsonRequest: (req: TReq) => Promise<JsonRecord>;
5
5
  pickString: (input: Record<string, unknown>, keys: string[]) => string | null;
6
- parsePositiveInt: (raw: string | null, fallback: number) => number;
6
+ parsePositiveInt: (raw: string | null, fallback: number, max?: number) => number;
7
7
  emitActivitySafe?: (input: {
8
8
  initiativeId: string | null;
9
9
  sourceClient?: string;
@@ -1,25 +1,5 @@
1
1
  import { getChatThread, linkChatThreadScope, listChatThreads, recordChatLaunch, sendChatMessage, updateChatLaunchStatus, } from "../../chat-store.js";
2
- function asRecord(value) {
3
- if (!value || typeof value !== "object" || Array.isArray(value))
4
- return null;
5
- return value;
6
- }
7
- function asStringArray(value) {
8
- if (!Array.isArray(value))
9
- return [];
10
- const out = [];
11
- const seen = new Set();
12
- for (const entry of value) {
13
- if (typeof entry !== "string")
14
- continue;
15
- const trimmed = entry.trim();
16
- if (!trimmed || seen.has(trimmed))
17
- continue;
18
- seen.add(trimmed);
19
- out.push(trimmed);
20
- }
21
- return out;
22
- }
2
+ import { asRecord, asStringArray } from "../../lib/type-coercion.js";
23
3
  function normalizeStatus(value) {
24
4
  const normalized = (value ?? "").trim().toLowerCase();
25
5
  if (normalized === "requested" ||
@@ -181,8 +161,8 @@ export function registerChatRoutes(router, deps) {
181
161
  const initiativeId = query.get("initiative_id") ?? query.get("initiative");
182
162
  const searchQuery = query.get("query") ?? query.get("q");
183
163
  const status = query.get("status");
184
- const limit = deps.parsePositiveInt(query.get("limit"), 60);
185
- const offset = deps.parsePositiveInt(query.get("offset"), 0);
164
+ const limit = deps.parsePositiveInt(query.get("limit"), 60, 200);
165
+ const offset = deps.parsePositiveInt(query.get("offset"), 0, 10000);
186
166
  sendThreadList(deps, res, {
187
167
  commandCenterId: commandCenterId?.trim() ?? null,
188
168
  initiativeId: initiativeId?.trim() ?? null,
@@ -1,4 +1,5 @@
1
1
  import { resolveWorkspaceScope, workspaceScopeFromHeaders, } from "../helpers/workspace-scope.js";
2
+ import { deriveInitiativeLifecycleStatus } from "../helpers/mission-control.js";
2
3
  const WORKSTREAM_REASSIGNMENT_FIELDS = [
3
4
  "domain",
4
5
  "role",
@@ -38,6 +39,59 @@ function toObjectArray(input) {
38
39
  return [];
39
40
  return input.filter((item) => Boolean(item) && typeof item === "object");
40
41
  }
42
+ async function reconcileInitiativeStatusesFromWorkstreams(deps, rows, input) {
43
+ if (rows.length === 0)
44
+ return rows;
45
+ const initiativeIds = new Set(rows
46
+ .map((row) => deps.pickString(row, ["id"]))
47
+ .filter((value) => Boolean(value)));
48
+ if (initiativeIds.size === 0)
49
+ return rows;
50
+ const scopedInitiativeId = input.initiativeId?.trim() ?? "";
51
+ const scopedWorkspaceId = input.workspaceId?.trim() ?? "";
52
+ if (!scopedInitiativeId && !scopedWorkspaceId) {
53
+ // Avoid unscoped global workstream scans.
54
+ return rows;
55
+ }
56
+ const filters = { limit: 2000 };
57
+ if (scopedInitiativeId) {
58
+ filters.initiative_id = scopedInitiativeId;
59
+ }
60
+ if (scopedWorkspaceId) {
61
+ filters.workspace_id = scopedWorkspaceId;
62
+ filters.command_center_id = scopedWorkspaceId;
63
+ }
64
+ const response = await deps.client.listEntities("workstream", filters);
65
+ const workstreamRows = toObjectArray(response && typeof response === "object"
66
+ ? response.data
67
+ : []);
68
+ const childStatusesByInitiative = new Map();
69
+ for (const workstreamRow of workstreamRows) {
70
+ const initiativeId = deps.pickString(workstreamRow, ["initiative_id", "initiativeId"]);
71
+ if (!initiativeId || !initiativeIds.has(initiativeId))
72
+ continue;
73
+ const status = deps.pickString(workstreamRow, ["status"]) ?? "todo";
74
+ const statuses = childStatusesByInitiative.get(initiativeId) ?? [];
75
+ statuses.push(status);
76
+ childStatusesByInitiative.set(initiativeId, statuses);
77
+ }
78
+ return rows.map((row) => {
79
+ const initiativeId = deps.pickString(row, ["id"]);
80
+ const status = deps.pickString(row, ["status"]);
81
+ if (!initiativeId || !status)
82
+ return row;
83
+ const childStatuses = childStatusesByInitiative.get(initiativeId) ?? [];
84
+ if (childStatuses.length === 0)
85
+ return row;
86
+ const normalized = deriveInitiativeLifecycleStatus(status, childStatuses);
87
+ if (normalized === status)
88
+ return row;
89
+ return {
90
+ ...row,
91
+ status: normalized,
92
+ };
93
+ });
94
+ }
41
95
  export function registerEntitiesRoutes(router, deps) {
42
96
  router.add("POST", "entities", async ({ req, res }) => {
43
97
  try {
@@ -305,13 +359,17 @@ export function registerEntitiesRoutes(router, deps) {
305
359
  return rowScope.trim() === workspaceScopeId;
306
360
  })
307
361
  : searchedRows;
362
+ const reconciledRows = await reconcileInitiativeStatusesFromWorkstreams(deps, rows, {
363
+ initiativeId: id,
364
+ workspaceId: workspaceScopeId,
365
+ }).catch(() => rows);
308
366
  deps.sendJson(res, 200, {
309
367
  ...payload,
310
- data: deps.applyLocalInitiativeOverrides(rows),
368
+ data: deps.applyLocalInitiativeOverrides(reconciledRows),
311
369
  pagination: payload.pagination && typeof payload.pagination === "object"
312
370
  ? {
313
371
  ...payload.pagination,
314
- total: rows.length,
372
+ total: reconciledRows.length,
315
373
  }
316
374
  : payload.pagination,
317
375
  });
@@ -1,3 +1,51 @@
1
+ function resolveEntityActionStatus(entityType, entityAction) {
2
+ const normalizedType = entityType.trim().toLowerCase();
3
+ const normalizedAction = entityAction.trim().toLowerCase();
4
+ const defaultStatusMap = {
5
+ start: "in_progress",
6
+ complete: "done",
7
+ block: "blocked",
8
+ unblock: "in_progress",
9
+ pause: "paused",
10
+ resume: "active",
11
+ };
12
+ const statusMapByEntityType = {
13
+ initiative: {
14
+ start: "active",
15
+ complete: "completed",
16
+ block: "blocked",
17
+ unblock: "active",
18
+ pause: "paused",
19
+ resume: "active",
20
+ },
21
+ workstream: {
22
+ start: "active",
23
+ complete: "completed",
24
+ block: "blocked",
25
+ unblock: "active",
26
+ pause: "paused",
27
+ resume: "active",
28
+ },
29
+ milestone: {
30
+ start: "in_progress",
31
+ complete: "completed",
32
+ block: "at_risk",
33
+ unblock: "in_progress",
34
+ pause: "planned",
35
+ resume: "in_progress",
36
+ },
37
+ task: {
38
+ start: "in_progress",
39
+ complete: "done",
40
+ block: "blocked",
41
+ unblock: "in_progress",
42
+ pause: "todo",
43
+ resume: "in_progress",
44
+ },
45
+ };
46
+ const map = statusMapByEntityType[normalizedType] ?? defaultStatusMap;
47
+ return map[normalizedAction] ?? null;
48
+ }
1
49
  export function registerEntityDynamicRoutes(router, deps) {
2
50
  router.add("*", "entities/*", async ({ req, res, path }) => {
3
51
  const method = (req.method ?? "GET").toUpperCase();
@@ -137,15 +185,7 @@ export function registerEntityDynamicRoutes(router, deps) {
137
185
  }
138
186
  return;
139
187
  }
140
- const statusMap = {
141
- start: "in_progress",
142
- complete: "done",
143
- block: "blocked",
144
- unblock: "in_progress",
145
- pause: "paused",
146
- resume: "active",
147
- };
148
- const newStatus = statusMap[entityAction];
188
+ const newStatus = resolveEntityActionStatus(entityType, entityAction);
149
189
  if (!newStatus) {
150
190
  deps.sendJson(res, 400, {
151
191
  error: `Unknown entity action: ${entityAction}`,
@@ -14,7 +14,7 @@ type SnapshotPersistState = {
14
14
  lastPersistAt: number;
15
15
  };
16
16
  type LiveSnapshotRoutesDeps<TReq, TRes> = {
17
- parsePositiveInt: (raw: string | null, fallback: number) => number;
17
+ parsePositiveInt: (raw: string | null, fallback: number, max?: number) => number;
18
18
  readSnapshotResponseCache: (key: string) => Record<string, unknown> | null;
19
19
  writeSnapshotResponseCache: (key: string, payload: Record<string, unknown>) => void;
20
20
  safeErrorMessage: (err: unknown) => string;
@@ -131,6 +131,15 @@ type LiveSnapshotRoutesDeps<TReq, TRes> = {
131
131
  total: number;
132
132
  updatedAt: string;
133
133
  };
134
+ getCanonicalAutopilotState?: (initiativeId: string) => {
135
+ state: "idle" | "running" | "blocked" | "stopping";
136
+ reason: string | null;
137
+ activeRunId: string | null;
138
+ activeWorkstreamId: string | null;
139
+ activeWorkstreamTitle: string | null;
140
+ queueHeadTitle: string | null;
141
+ lastTransitionAt: string;
142
+ } | null;
134
143
  sendJson: (res: TRes, status: number, payload: unknown) => void;
135
144
  };
136
145
  export declare function registerLiveSnapshotRoutes<TReq, TRes>(router: Router<Record<string, never>, TReq, TRes>, deps: LiveSnapshotRoutesDeps<TReq, TRes>): void;
@@ -231,9 +231,9 @@ export function registerLiveSnapshotRoutes(router, deps) {
231
231
  };
232
232
  const headerScopeFromRequest = (req) => workspaceScopeFromHeaders(req?.headers);
233
233
  function parseSnapshotQuery(query, headerScope) {
234
- const sessionsLimit = deps.parsePositiveInt(query.get("sessionsLimit") ?? query.get("sessions_limit"), 320);
235
- const activityLimit = deps.parsePositiveInt(query.get("activityLimit") ?? query.get("activity_limit"), 600);
236
- const decisionsLimit = deps.parsePositiveInt(query.get("decisionsLimit") ?? query.get("decisions_limit"), 120);
234
+ const sessionsLimit = deps.parsePositiveInt(query.get("sessionsLimit") ?? query.get("sessions_limit"), 320, 1000);
235
+ const activityLimit = deps.parsePositiveInt(query.get("activityLimit") ?? query.get("activity_limit"), 600, 2000);
236
+ const decisionsLimit = deps.parsePositiveInt(query.get("decisionsLimit") ?? query.get("decisions_limit"), 120, 500);
237
237
  const initiative = query.get("initiative");
238
238
  const scope = resolveWorkspaceScope(query, headerScope, {
239
239
  allowProjectScope: false,
@@ -450,29 +450,7 @@ export function registerLiveSnapshotRoutes(router, deps) {
450
450
  try {
451
451
  const buffered = await deps.readOutboxItems();
452
452
  if (buffered.length > 0) {
453
- const merged = [...activity, ...buffered]
454
- .sort((a, b) => {
455
- const d = Date.parse(b.timestamp) - Date.parse(a.timestamp);
456
- if (d !== 0)
457
- return d;
458
- return b.id.localeCompare(a.id);
459
- })
460
- .slice(0, activityLimit);
461
- const deduped = [];
462
- const seenIds = new Set();
463
- const seenSemantic = new Set();
464
- for (const item of merged) {
465
- if (seenIds.has(item.id))
466
- continue;
467
- seenIds.add(item.id);
468
- const sk = deps.semanticActivityKey(item);
469
- if (sk && seenSemantic.has(sk))
470
- continue;
471
- if (sk)
472
- seenSemantic.add(sk);
473
- deduped.push(item);
474
- }
475
- activity = deduped;
453
+ activity = deps.mergeActivities(activity, buffered, activityLimit);
476
454
  }
477
455
  }
478
456
  catch (err) {
@@ -500,6 +478,9 @@ export function registerLiveSnapshotRoutes(router, deps) {
500
478
  agents: agentContexts,
501
479
  runs: runContexts,
502
480
  });
481
+ // Final dedup pass after enrichment — catches items that became
482
+ // semantically identical after runtime/agent context decoration.
483
+ activity = deps.mergeActivities(activity, [], activityLimit);
503
484
  activity = activity.filter((item) => !shouldHideActivityItem(item));
504
485
  sessions = normalizeReportingBlockedSessions({
505
486
  sessions,
@@ -542,6 +523,14 @@ export function registerLiveSnapshotRoutes(router, deps) {
542
523
  projectId,
543
524
  degraded: degraded.length > 0 ? degraded : undefined,
544
525
  };
526
+ if (typeof deps.getCanonicalAutopilotState === "function" && initiative) {
527
+ try {
528
+ payload.autopilot = deps.getCanonicalAutopilotState(initiative) ?? null;
529
+ }
530
+ catch {
531
+ // best effort
532
+ }
533
+ }
545
534
  if (typeof deps.listChatThreads === "function") {
546
535
  try {
547
536
  const listed = deps.listChatThreads({
@@ -66,6 +66,12 @@ type RegisterMissionControlActionsRoutesDeps<TReq, TRes> = {
66
66
  }>;
67
67
  tickAutoContinueRun: (run: any) => Promise<void>;
68
68
  stopAutoContinueRun: (input: any) => Promise<void>;
69
+ skipCurrentWorkstream: (initiativeId: string, workstreamId: string, reason?: string) => Promise<{
70
+ ok: boolean;
71
+ skippedWorkstreamId: string;
72
+ nextWorkstreamId?: string;
73
+ nextWorkstreamTitle?: string;
74
+ }>;
69
75
  updateInitiativeAutoContinueState: (input: any) => Promise<void>;
70
76
  tickAllAutoContinue: () => Promise<void>;
71
77
  scheduleAutoFixForWorkstream: (input: {