@useorgx/openclaw-plugin 0.7.11 → 0.7.15

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 (189) hide show
  1. package/README.md +94 -122
  2. package/dashboard/dist/assets/0RUEVzJa.js +1 -0
  3. package/dashboard/dist/assets/0RUEVzJa.js.br +0 -0
  4. package/dashboard/dist/assets/0RUEVzJa.js.gz +0 -0
  5. package/dashboard/dist/assets/3TtV4moZ.js +1 -0
  6. package/dashboard/dist/assets/3TtV4moZ.js.br +0 -0
  7. package/dashboard/dist/assets/3TtV4moZ.js.gz +0 -0
  8. package/dashboard/dist/assets/3VwNyxUf.js +1 -0
  9. package/dashboard/dist/assets/3VwNyxUf.js.br +0 -0
  10. package/dashboard/dist/assets/3VwNyxUf.js.gz +0 -0
  11. package/dashboard/dist/assets/{beHYBbh6.js → 7DhYqBrM.js} +2 -2
  12. package/dashboard/dist/assets/7DhYqBrM.js.br +0 -0
  13. package/dashboard/dist/assets/7DhYqBrM.js.gz +0 -0
  14. package/dashboard/dist/assets/{tcEHYcbW.js → BCudUvwg.js} +1 -1
  15. package/dashboard/dist/assets/BCudUvwg.js.br +0 -0
  16. package/dashboard/dist/assets/BCudUvwg.js.gz +0 -0
  17. package/dashboard/dist/assets/BV0BcV1u.js +53 -0
  18. package/dashboard/dist/assets/BV0BcV1u.js.br +0 -0
  19. package/dashboard/dist/assets/BV0BcV1u.js.gz +0 -0
  20. package/dashboard/dist/assets/BVvffj0x.js +1 -0
  21. package/dashboard/dist/assets/BVvffj0x.js.br +0 -0
  22. package/dashboard/dist/assets/BVvffj0x.js.gz +0 -0
  23. package/dashboard/dist/assets/BiOgVMED.js +1 -0
  24. package/dashboard/dist/assets/BiOgVMED.js.br +0 -0
  25. package/dashboard/dist/assets/BiOgVMED.js.gz +0 -0
  26. package/dashboard/dist/assets/BjK42gtU.js +1 -0
  27. package/dashboard/dist/assets/BjK42gtU.js.br +0 -0
  28. package/dashboard/dist/assets/BjK42gtU.js.gz +0 -0
  29. package/dashboard/dist/assets/C-MOJWHs.js +1 -0
  30. package/dashboard/dist/assets/C-MOJWHs.js.br +0 -0
  31. package/dashboard/dist/assets/C-MOJWHs.js.gz +0 -0
  32. package/dashboard/dist/assets/{CD-q5mdP.js → C91KLKit.js} +1 -1
  33. package/dashboard/dist/assets/C91KLKit.js.br +0 -0
  34. package/dashboard/dist/assets/C91KLKit.js.gz +0 -0
  35. package/dashboard/dist/assets/{AqVoI3SF.js → C9fvfXmS.js} +1 -1
  36. package/dashboard/dist/assets/C9fvfXmS.js.br +0 -0
  37. package/dashboard/dist/assets/C9fvfXmS.js.gz +0 -0
  38. package/dashboard/dist/assets/CFZ4Swr5.js +1 -0
  39. package/dashboard/dist/assets/CFZ4Swr5.js.br +0 -0
  40. package/dashboard/dist/assets/CFZ4Swr5.js.gz +0 -0
  41. package/dashboard/dist/assets/{DCP-C7fn.js → CGj8kRhg.js} +1 -1
  42. package/dashboard/dist/assets/CGj8kRhg.js.br +0 -0
  43. package/dashboard/dist/assets/CGj8kRhg.js.gz +0 -0
  44. package/dashboard/dist/assets/CJjEAGFN.js +1 -0
  45. package/dashboard/dist/assets/CJjEAGFN.js.br +0 -0
  46. package/dashboard/dist/assets/CJjEAGFN.js.gz +0 -0
  47. package/dashboard/dist/assets/CKrH5fYO.js +1 -0
  48. package/dashboard/dist/assets/CKrH5fYO.js.br +0 -0
  49. package/dashboard/dist/assets/CKrH5fYO.js.gz +0 -0
  50. package/dashboard/dist/assets/CMTTPXch.js +1 -0
  51. package/dashboard/dist/assets/CMTTPXch.js.br +0 -0
  52. package/dashboard/dist/assets/CMTTPXch.js.gz +0 -0
  53. package/dashboard/dist/assets/CSlBSRyv.js +1 -0
  54. package/dashboard/dist/assets/CSlBSRyv.js.br +0 -0
  55. package/dashboard/dist/assets/CSlBSRyv.js.gz +0 -0
  56. package/dashboard/dist/assets/CnPC783_.js +1 -0
  57. package/dashboard/dist/assets/CnPC783_.js.br +0 -0
  58. package/dashboard/dist/assets/CnPC783_.js.gz +0 -0
  59. package/dashboard/dist/assets/Ctw95IkC.js +1 -0
  60. package/dashboard/dist/assets/Ctw95IkC.js.br +0 -0
  61. package/dashboard/dist/assets/Ctw95IkC.js.gz +0 -0
  62. package/dashboard/dist/assets/DHz-aQPw.js +1 -0
  63. package/dashboard/dist/assets/DHz-aQPw.js.br +0 -0
  64. package/dashboard/dist/assets/DHz-aQPw.js.gz +0 -0
  65. package/dashboard/dist/assets/DNX2foSJ.css +1 -0
  66. package/dashboard/dist/assets/DNX2foSJ.css.br +0 -0
  67. package/dashboard/dist/assets/DNX2foSJ.css.gz +0 -0
  68. package/dashboard/dist/assets/Dj2k1r16.js +8 -0
  69. package/dashboard/dist/assets/Dj2k1r16.js.br +0 -0
  70. package/dashboard/dist/assets/Dj2k1r16.js.gz +0 -0
  71. package/dashboard/dist/assets/DxUw4FMR.js +212 -0
  72. package/dashboard/dist/assets/DxUw4FMR.js.br +0 -0
  73. package/dashboard/dist/assets/DxUw4FMR.js.gz +0 -0
  74. package/dashboard/dist/assets/T2NFtzAv.js +1 -0
  75. package/dashboard/dist/assets/T2NFtzAv.js.br +0 -0
  76. package/dashboard/dist/assets/T2NFtzAv.js.gz +0 -0
  77. package/dashboard/dist/assets/cX2e-TLi.js +1 -0
  78. package/dashboard/dist/assets/cX2e-TLi.js.br +0 -0
  79. package/dashboard/dist/assets/cX2e-TLi.js.gz +0 -0
  80. package/dashboard/dist/assets/eeHXe_OQ.js +9 -0
  81. package/dashboard/dist/assets/eeHXe_OQ.js.br +0 -0
  82. package/dashboard/dist/assets/eeHXe_OQ.js.gz +0 -0
  83. package/dashboard/dist/assets/{Du1wfrXa.js → gZr_xKlA.js} +2 -2
  84. package/dashboard/dist/assets/gZr_xKlA.js.br +0 -0
  85. package/dashboard/dist/assets/gZr_xKlA.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.js +124 -67
  100. package/dist/http/helpers/autopilot-slice-utils.js +112 -66
  101. package/dist/http/helpers/hash-utils.d.ts +1 -1
  102. package/dist/http/helpers/hash-utils.js +1 -1
  103. package/dist/http/helpers/mission-control.js +5 -2
  104. package/dist/http/helpers/queue-constants.d.ts +37 -0
  105. package/dist/http/helpers/queue-constants.js +34 -0
  106. package/dist/http/helpers/slice-experience-v2.js +2 -5
  107. package/dist/http/helpers/slice-run-projections.js +2 -5
  108. package/dist/http/helpers/workspace-scope.js +4 -3
  109. package/dist/http/index.js +101 -60
  110. package/dist/http/routes/chat.js +1 -21
  111. package/dist/http/routes/live-snapshot.js +4 -23
  112. package/dist/http/routes/mission-control-actions.js +2 -18
  113. package/dist/http/routes/mission-control-read.js +4 -107
  114. package/dist/lib/type-coercion.d.ts +10 -0
  115. package/dist/lib/type-coercion.js +82 -0
  116. package/dist/mcp-http-handler.js +14 -2
  117. package/dist/openclaw.plugin.json +1 -1
  118. package/dist/services/experiment-randomization.js +9 -2
  119. package/openclaw.plugin.json +1 -1
  120. package/package.json +3 -2
  121. package/dashboard/dist/assets/AqVoI3SF.js.br +0 -0
  122. package/dashboard/dist/assets/AqVoI3SF.js.gz +0 -0
  123. package/dashboard/dist/assets/BC4WvnHJ.js +0 -1
  124. package/dashboard/dist/assets/BC4WvnHJ.js.br +0 -0
  125. package/dashboard/dist/assets/BC4WvnHJ.js.gz +0 -0
  126. package/dashboard/dist/assets/BG5mwTkg.js +0 -1
  127. package/dashboard/dist/assets/BG5mwTkg.js.br +0 -0
  128. package/dashboard/dist/assets/BG5mwTkg.js.gz +0 -0
  129. package/dashboard/dist/assets/BJgZIVUQ.js +0 -53
  130. package/dashboard/dist/assets/BJgZIVUQ.js.br +0 -0
  131. package/dashboard/dist/assets/BJgZIVUQ.js.gz +0 -0
  132. package/dashboard/dist/assets/BNh-XYPV.js +0 -1
  133. package/dashboard/dist/assets/BNh-XYPV.js.br +0 -0
  134. package/dashboard/dist/assets/BNh-XYPV.js.gz +0 -0
  135. package/dashboard/dist/assets/BTAEErUY.js +0 -1
  136. package/dashboard/dist/assets/BTAEErUY.js.br +0 -0
  137. package/dashboard/dist/assets/BTAEErUY.js.gz +0 -0
  138. package/dashboard/dist/assets/BepW_590.js +0 -1
  139. package/dashboard/dist/assets/BepW_590.js.br +0 -0
  140. package/dashboard/dist/assets/BepW_590.js.gz +0 -0
  141. package/dashboard/dist/assets/BerAfzjq.js +0 -1
  142. package/dashboard/dist/assets/BerAfzjq.js.br +0 -0
  143. package/dashboard/dist/assets/BerAfzjq.js.gz +0 -0
  144. package/dashboard/dist/assets/Bp3N-QL5.js +0 -212
  145. package/dashboard/dist/assets/Bp3N-QL5.js.br +0 -0
  146. package/dashboard/dist/assets/Bp3N-QL5.js.gz +0 -0
  147. package/dashboard/dist/assets/C-KIc3Wc.js +0 -1
  148. package/dashboard/dist/assets/C-KIc3Wc.js.br +0 -0
  149. package/dashboard/dist/assets/C-KIc3Wc.js.gz +0 -0
  150. package/dashboard/dist/assets/C3dZRz9P.css +0 -1
  151. package/dashboard/dist/assets/C3dZRz9P.css.br +0 -0
  152. package/dashboard/dist/assets/C3dZRz9P.css.gz +0 -0
  153. package/dashboard/dist/assets/CD-q5mdP.js.br +0 -0
  154. package/dashboard/dist/assets/CD-q5mdP.js.gz +0 -0
  155. package/dashboard/dist/assets/CL_wXqR7.js +0 -1
  156. package/dashboard/dist/assets/CL_wXqR7.js.br +0 -0
  157. package/dashboard/dist/assets/CL_wXqR7.js.gz +0 -0
  158. package/dashboard/dist/assets/CdvjC9G9.js +0 -1
  159. package/dashboard/dist/assets/CdvjC9G9.js.br +0 -0
  160. package/dashboard/dist/assets/CdvjC9G9.js.gz +0 -0
  161. package/dashboard/dist/assets/Ck2agw-s.js +0 -1
  162. package/dashboard/dist/assets/Ck2agw-s.js.br +0 -0
  163. package/dashboard/dist/assets/Ck2agw-s.js.gz +0 -0
  164. package/dashboard/dist/assets/CxQ08qFN.js +0 -9
  165. package/dashboard/dist/assets/CxQ08qFN.js.br +0 -0
  166. package/dashboard/dist/assets/CxQ08qFN.js.gz +0 -0
  167. package/dashboard/dist/assets/D2CH1H6k.js +0 -1
  168. package/dashboard/dist/assets/D2CH1H6k.js.br +0 -0
  169. package/dashboard/dist/assets/D2CH1H6k.js.gz +0 -0
  170. package/dashboard/dist/assets/D9esz7jd.js +0 -1
  171. package/dashboard/dist/assets/D9esz7jd.js.br +0 -0
  172. package/dashboard/dist/assets/D9esz7jd.js.gz +0 -0
  173. package/dashboard/dist/assets/DCP-C7fn.js.br +0 -0
  174. package/dashboard/dist/assets/DCP-C7fn.js.gz +0 -0
  175. package/dashboard/dist/assets/DJASCd69.js +0 -1
  176. package/dashboard/dist/assets/DJASCd69.js.br +0 -0
  177. package/dashboard/dist/assets/DJASCd69.js.gz +0 -0
  178. package/dashboard/dist/assets/Dm9AybAp.js +0 -1
  179. package/dashboard/dist/assets/Dm9AybAp.js.br +0 -0
  180. package/dashboard/dist/assets/Dm9AybAp.js.gz +0 -0
  181. package/dashboard/dist/assets/Du1wfrXa.js.br +0 -0
  182. package/dashboard/dist/assets/Du1wfrXa.js.gz +0 -0
  183. package/dashboard/dist/assets/beHYBbh6.js.br +0 -0
  184. package/dashboard/dist/assets/beHYBbh6.js.gz +0 -0
  185. package/dashboard/dist/assets/cNrhgGc1.js +0 -8
  186. package/dashboard/dist/assets/cNrhgGc1.js.br +0 -0
  187. package/dashboard/dist/assets/cNrhgGc1.js.gz +0 -0
  188. package/dashboard/dist/assets/tcEHYcbW.js.br +0 -0
  189. 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;
@@ -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,
@@ -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" ||
@@ -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,
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { resolveWorkspaceScope as resolveCanonicalWorkspaceScope } from "../helpers/workspace-scope.js";
3
+ import { asRecord, asString, asStringArray } from "../../lib/type-coercion.js";
3
4
  import { buildDispatchGatewayEnvelope } from "./dispatch-gateway-envelope.js";
4
5
  const PLAY_QUEUE_LOOKUP_TIMEOUT_MS = (() => {
5
6
  const raw = process.env.ORGX_PLAY_QUEUE_LOOKUP_TIMEOUT_MS;
@@ -194,24 +195,7 @@ function shouldResetTaskStatus(status, states) {
194
195
  }
195
196
  return false;
196
197
  }
197
- function asRecord(value) {
198
- if (!value || typeof value !== "object" || Array.isArray(value))
199
- return null;
200
- return value;
201
- }
202
- function asString(value) {
203
- if (typeof value !== "string")
204
- return null;
205
- const trimmed = value.trim();
206
- return trimmed.length > 0 ? trimmed : null;
207
- }
208
- function asStringArray(value) {
209
- if (!Array.isArray(value))
210
- return [];
211
- return value
212
- .map((entry) => asString(entry))
213
- .filter((entry) => Boolean(entry));
214
- }
198
+ // asRecord, asString, asStringArray imported from ../../lib/type-coercion.js
215
199
  function parseCycleGraphNodes(graph) {
216
200
  const root = asRecord(graph);
217
201
  const rawNodes = Array.isArray(root?.nodes) ? root.nodes : [];
@@ -1,34 +1,7 @@
1
1
  import { listBuiltInSentinels } from "../helpers/sentinel-catalog.js";
2
2
  import { resolveWorkspaceScope, workspaceScopeFromHeaders, } from "../helpers/workspace-scope.js";
3
- function asRecord(value) {
4
- if (!value || typeof value !== "object" || Array.isArray(value))
5
- return null;
6
- return value;
7
- }
8
- function asString(value) {
9
- if (typeof value !== "string")
10
- return null;
11
- const trimmed = value.trim();
12
- return trimmed.length > 0 ? trimmed : null;
13
- }
14
- function asArray(value) {
15
- if (Array.isArray(value))
16
- return value;
17
- if (typeof value !== "string")
18
- return [];
19
- const trimmed = value.trim();
20
- try {
21
- const parsed = JSON.parse(trimmed);
22
- if (Array.isArray(parsed))
23
- return parsed;
24
- if (parsed && typeof parsed === "object")
25
- return [parsed];
26
- return [];
27
- }
28
- catch {
29
- return [];
30
- }
31
- }
3
+ import { asRecord, asString, asNumber, asArray, asStringArray, } from "../../lib/type-coercion.js";
4
+ // asRecord, asString, asArray, asNumber, asStringArray imported from ../../lib/type-coercion.js
32
5
  function normalizeRunnerValue(value) {
33
6
  const raw = asString(value);
34
7
  if (!raw)
@@ -105,29 +78,7 @@ function mergeRunnerAgents(...groups) {
105
78
  }
106
79
  return output;
107
80
  }
108
- function asNumber(value) {
109
- if (typeof value === "number" && Number.isFinite(value))
110
- return value;
111
- if (typeof value === "string" && value.trim().length > 0) {
112
- const parsed = Number(value);
113
- if (Number.isFinite(parsed))
114
- return parsed;
115
- }
116
- return null;
117
- }
118
- function asStringArray(value) {
119
- const entries = asArray(value);
120
- if (entries.length === 0)
121
- return [];
122
- const values = [];
123
- for (const entry of entries) {
124
- const normalized = asString(entry);
125
- if (!normalized)
126
- continue;
127
- values.push(normalized);
128
- }
129
- return dedupeStrings(values);
130
- }
81
+ // asNumber, asStringArray imported from ../../lib/type-coercion.js
131
82
  function isCanonicalAllScopeMismatch(canonicalRecord, useAllScope) {
132
83
  if (!useAllScope)
133
84
  return false;
@@ -846,64 +797,10 @@ export function registerMissionControlReadRoutes(router, deps) {
846
797
  sendRouteError(res, 400, "mission-control.read.auto-continue.status.validation", workspaceScope.error);
847
798
  return;
848
799
  }
849
- const scopedProjectId = workspaceScope.workspaceId;
850
800
  const initiativeId = query.get("initiative_id") ?? query.get("initiativeId") ?? "";
851
801
  const id = initiativeId.trim();
852
- let scopedInitiatives = null;
853
- if (scopedProjectId) {
854
- try {
855
- const ids = await deps.listInitiativeIdsForProject({ projectId: scopedProjectId });
856
- scopedInitiatives = new Set(ids);
857
- }
858
- catch {
859
- // best effort: if scope lookup is unavailable, fall back to unscoped run resolution.
860
- scopedInitiatives = null;
861
- }
862
- }
863
- const statusRank = (value) => {
864
- const normalized = (value ?? "").trim().toLowerCase();
865
- if (normalized === "running")
866
- return 0;
867
- if (normalized === "stopping")
868
- return 1;
869
- if (normalized === "blocked")
870
- return 2;
871
- if (normalized === "stopped")
872
- return 3;
873
- return 4;
874
- };
875
- const updatedEpoch = (value) => {
876
- const parsed = Date.parse(value ?? "");
877
- return Number.isFinite(parsed) ? parsed : 0;
878
- };
879
802
  if (!id) {
880
- const scopedRuns = Array.from(deps.autoContinueRuns.values()).filter((run) => {
881
- const runInitiativeId = (run.initiativeId ?? "").trim();
882
- if (!runInitiativeId)
883
- return false;
884
- if (scopedInitiatives && !scopedInitiatives.has(runInitiativeId))
885
- return false;
886
- return true;
887
- });
888
- scopedRuns.sort((left, right) => {
889
- const statusDelta = statusRank(left.status) - statusRank(right.status);
890
- if (statusDelta !== 0)
891
- return statusDelta;
892
- return updatedEpoch(right.updatedAt) - updatedEpoch(left.updatedAt);
893
- });
894
- const run = scopedRuns[0] ?? null;
895
- deps.sendJson(res, 200, {
896
- ok: true,
897
- initiativeId: run?.initiativeId ?? null,
898
- run,
899
- defaults: {
900
- tokenBudget: deps.defaultAutoContinueTokenBudget(),
901
- maxParallelSlices: typeof deps.defaultAutoContinueMaxParallelSlices === "function"
902
- ? deps.defaultAutoContinueMaxParallelSlices()
903
- : 1,
904
- tickMs: deps.autoContinueTickMs,
905
- },
906
- });
803
+ sendRouteError(res, 400, "mission-control.read.auto-continue.status.validation", "initiativeId is required");
907
804
  return;
908
805
  }
909
806
  const run = deps.autoContinueRuns.get(id) ?? null;
@@ -0,0 +1,10 @@
1
+ /** Safely cast unknown to a Record, or null if not a plain object. */
2
+ export declare function asRecord(value: unknown): Record<string, unknown> | null;
3
+ /** Safely cast unknown to a trimmed non-empty string, or null. */
4
+ export declare function asString(value: unknown): string | null;
5
+ /** Safely cast unknown to a finite number, or null. */
6
+ export declare function asNumber(value: unknown): number | null;
7
+ /** Safely cast unknown to an array. Attempts JSON parse for string values. */
8
+ export declare function asArray(value: unknown): unknown[];
9
+ /** Extract a deduplicated string array from unknown. */
10
+ export declare function asStringArray(value: unknown): string[];
@@ -0,0 +1,82 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared type-coercion helpers for safe parsing of unknown API data.
3
+ // Previously duplicated across 20+ files.
4
+ // ---------------------------------------------------------------------------
5
+ /** Safely cast unknown to a Record, or null if not a plain object. */
6
+ export function asRecord(value) {
7
+ if (!value || typeof value !== "object" || Array.isArray(value))
8
+ return null;
9
+ const proto = Object.getPrototypeOf(value);
10
+ if (proto !== Object.prototype && proto !== null)
11
+ return null;
12
+ return value;
13
+ }
14
+ /** Safely cast unknown to a trimmed non-empty string, or null. */
15
+ export function asString(value) {
16
+ if (typeof value !== "string")
17
+ return null;
18
+ const trimmed = value.trim();
19
+ return trimmed.length > 0 ? trimmed : null;
20
+ }
21
+ /** Safely cast unknown to a finite number, or null. */
22
+ export function asNumber(value) {
23
+ if (typeof value === "number" && Number.isFinite(value))
24
+ return value;
25
+ if (typeof value === "string" && value.trim().length > 0) {
26
+ const parsed = Number(value);
27
+ if (Number.isFinite(parsed))
28
+ return parsed;
29
+ }
30
+ return null;
31
+ }
32
+ /** Safely cast unknown to an array. Attempts JSON parse for string values. */
33
+ export function asArray(value) {
34
+ if (Array.isArray(value))
35
+ return value;
36
+ if (asRecord(value))
37
+ return [value];
38
+ if (typeof value !== "string")
39
+ return [];
40
+ const trimmed = value.trim();
41
+ try {
42
+ const parsed = JSON.parse(trimmed);
43
+ if (Array.isArray(parsed))
44
+ return parsed;
45
+ if (asRecord(parsed))
46
+ return [parsed];
47
+ return [];
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
53
+ /** Extract a deduplicated string array from unknown. */
54
+ export function asStringArray(value) {
55
+ const entries = asArray(value);
56
+ if (entries.length === 0) {
57
+ const scalar = asString(value);
58
+ if (!scalar)
59
+ return [];
60
+ const fallbackEntries = scalar.includes(",") ? scalar.split(",") : [scalar];
61
+ const seenFallback = new Set();
62
+ const fallbackValues = [];
63
+ for (const entry of fallbackEntries) {
64
+ const normalized = asString(entry);
65
+ if (!normalized || seenFallback.has(normalized))
66
+ continue;
67
+ seenFallback.add(normalized);
68
+ fallbackValues.push(normalized);
69
+ }
70
+ return fallbackValues;
71
+ }
72
+ const seen = new Set();
73
+ const values = [];
74
+ for (const entry of entries) {
75
+ const normalized = asString(entry);
76
+ if (!normalized || seen.has(normalized))
77
+ continue;
78
+ seen.add(normalized);
79
+ values.push(normalized);
80
+ }
81
+ return values;
82
+ }