@useorgx/openclaw-plugin 0.4.9 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +35 -0
  2. package/dashboard/dist/assets/BJgZIVUQ.js +53 -0
  3. package/dashboard/dist/assets/BJgZIVUQ.js.br +0 -0
  4. package/dashboard/dist/assets/BJgZIVUQ.js.gz +0 -0
  5. package/dashboard/dist/assets/BXWDRGm-.js +1 -0
  6. package/dashboard/dist/assets/BXWDRGm-.js.br +0 -0
  7. package/dashboard/dist/assets/BXWDRGm-.js.gz +0 -0
  8. package/dashboard/dist/assets/BgOYB78t.js +4 -0
  9. package/dashboard/dist/assets/BgOYB78t.js.br +0 -0
  10. package/dashboard/dist/assets/BgOYB78t.js.gz +0 -0
  11. package/dashboard/dist/assets/C-KIc3Wc.js.br +0 -0
  12. package/dashboard/dist/assets/C-KIc3Wc.js.gz +0 -0
  13. package/dashboard/dist/assets/CE38zU4U.js +1 -0
  14. package/dashboard/dist/assets/CE38zU4U.js.br +0 -0
  15. package/dashboard/dist/assets/CE38zU4U.js.gz +0 -0
  16. package/dashboard/dist/assets/CFGKRAzG.js +1 -0
  17. package/dashboard/dist/assets/CFGKRAzG.js.br +0 -0
  18. package/dashboard/dist/assets/CFGKRAzG.js.gz +0 -0
  19. package/dashboard/dist/assets/CGGR2GZh.js +1 -0
  20. package/dashboard/dist/assets/CGGR2GZh.js.br +0 -0
  21. package/dashboard/dist/assets/CGGR2GZh.js.gz +0 -0
  22. package/dashboard/dist/assets/CL_wXqR7.js +1 -0
  23. package/dashboard/dist/assets/CL_wXqR7.js.br +0 -0
  24. package/dashboard/dist/assets/CL_wXqR7.js.gz +0 -0
  25. package/dashboard/dist/assets/CPFiTmlw.js +8 -0
  26. package/dashboard/dist/assets/CPFiTmlw.js.br +0 -0
  27. package/dashboard/dist/assets/CPFiTmlw.js.gz +0 -0
  28. package/dashboard/dist/assets/CZZTvkQZ.js +1 -0
  29. package/dashboard/dist/assets/CZZTvkQZ.js.br +0 -0
  30. package/dashboard/dist/assets/CZZTvkQZ.js.gz +0 -0
  31. package/dashboard/dist/assets/{CpJsfbXo.js → CxQ08qFN.js} +2 -2
  32. package/dashboard/dist/assets/CxQ08qFN.js.br +0 -0
  33. package/dashboard/dist/assets/CxQ08qFN.js.gz +0 -0
  34. package/dashboard/dist/assets/D-bf6hEI.js +213 -0
  35. package/dashboard/dist/assets/D-bf6hEI.js.br +0 -0
  36. package/dashboard/dist/assets/D-bf6hEI.js.gz +0 -0
  37. package/dashboard/dist/assets/DG6y9wJI.js +2 -0
  38. package/dashboard/dist/assets/DG6y9wJI.js.br +0 -0
  39. package/dashboard/dist/assets/DG6y9wJI.js.gz +0 -0
  40. package/dashboard/dist/assets/DNxKz-GV.js +1 -0
  41. package/dashboard/dist/assets/DNxKz-GV.js.br +0 -0
  42. package/dashboard/dist/assets/DNxKz-GV.js.gz +0 -0
  43. package/dashboard/dist/assets/DW_rKUic.js +11 -0
  44. package/dashboard/dist/assets/DW_rKUic.js.br +0 -0
  45. package/dashboard/dist/assets/DW_rKUic.js.gz +0 -0
  46. package/dashboard/dist/assets/DbNoijHm.js +1 -0
  47. package/dashboard/dist/assets/DbNoijHm.js.br +0 -0
  48. package/dashboard/dist/assets/DbNoijHm.js.gz +0 -0
  49. package/dashboard/dist/assets/DjcdE6jC.js +2 -0
  50. package/dashboard/dist/assets/DjcdE6jC.js.br +0 -0
  51. package/dashboard/dist/assets/DjcdE6jC.js.gz +0 -0
  52. package/dashboard/dist/assets/FZYuCDnt.js +1 -0
  53. package/dashboard/dist/assets/FZYuCDnt.js.br +0 -0
  54. package/dashboard/dist/assets/FZYuCDnt.js.gz +0 -0
  55. package/dashboard/dist/assets/PAUiij_z.js +1 -0
  56. package/dashboard/dist/assets/PAUiij_z.js.br +0 -0
  57. package/dashboard/dist/assets/PAUiij_z.js.gz +0 -0
  58. package/dashboard/dist/assets/cNrhgGc1.js +8 -0
  59. package/dashboard/dist/assets/cNrhgGc1.js.br +0 -0
  60. package/dashboard/dist/assets/cNrhgGc1.js.gz +0 -0
  61. package/dashboard/dist/assets/h5biQs2I.css +1 -0
  62. package/dashboard/dist/assets/h5biQs2I.css.br +0 -0
  63. package/dashboard/dist/assets/h5biQs2I.css.gz +0 -0
  64. package/dashboard/dist/assets/ic2FaMnh.js +1 -0
  65. package/dashboard/dist/assets/ic2FaMnh.js.br +0 -0
  66. package/dashboard/dist/assets/ic2FaMnh.js.gz +0 -0
  67. package/dashboard/dist/assets/nByHNHoW.js +1 -0
  68. package/dashboard/dist/assets/nByHNHoW.js.br +0 -0
  69. package/dashboard/dist/assets/nByHNHoW.js.gz +0 -0
  70. package/dashboard/dist/assets/qm8xLgv-.css +1 -0
  71. package/dashboard/dist/assets/qm8xLgv-.css.br +0 -0
  72. package/dashboard/dist/assets/qm8xLgv-.css.gz +0 -0
  73. package/dashboard/dist/assets/tS9mbYZi.js +1 -0
  74. package/dashboard/dist/assets/tS9mbYZi.js.br +0 -0
  75. package/dashboard/dist/assets/tS9mbYZi.js.gz +0 -0
  76. package/dashboard/dist/brand/anthropic-mark.svg.br +0 -0
  77. package/dashboard/dist/brand/anthropic-mark.svg.gz +0 -0
  78. package/dashboard/dist/brand/openai-mark.svg.br +0 -0
  79. package/dashboard/dist/brand/openai-mark.svg.gz +0 -0
  80. package/dashboard/dist/brand/openclaw-mark.svg.br +0 -0
  81. package/dashboard/dist/brand/openclaw-mark.svg.gz +0 -0
  82. package/dashboard/dist/brand/xandy-orchestrator.png +0 -0
  83. package/dashboard/dist/index.html +7 -5
  84. package/dashboard/dist/index.html.br +0 -0
  85. package/dashboard/dist/index.html.gz +0 -0
  86. package/dist/activity-actor-fields.js +26 -4
  87. package/dist/activity-store.js +34 -8
  88. package/dist/agent-context-store.js +79 -17
  89. package/dist/agent-run-store.js +44 -3
  90. package/dist/agent-suite.d.ts +9 -0
  91. package/dist/agent-suite.js +149 -9
  92. package/dist/artifacts/artifact-domain-schemas.d.ts +66 -0
  93. package/dist/artifacts/artifact-domain-schemas.js +357 -0
  94. package/dist/artifacts/register-artifact.d.ts +4 -3
  95. package/dist/artifacts/register-artifact.js +170 -57
  96. package/dist/chat-store.d.ts +157 -0
  97. package/dist/chat-store.js +586 -0
  98. package/dist/cli/orgx.js +11 -0
  99. package/dist/contracts/client.d.ts +43 -3
  100. package/dist/contracts/client.js +159 -30
  101. package/dist/contracts/retro-schema.d.ts +81 -0
  102. package/dist/contracts/retro-schema.js +80 -0
  103. package/dist/contracts/shared-types.d.ts +159 -0
  104. package/dist/contracts/shared-types.js +177 -1
  105. package/dist/contracts/skill-pack-schema.d.ts +192 -0
  106. package/dist/contracts/skill-pack-schema.js +180 -0
  107. package/dist/contracts/types.d.ts +227 -2
  108. package/dist/entities/auto-assignment.js +43 -17
  109. package/dist/event-sanitization.d.ts +11 -0
  110. package/dist/event-sanitization.js +113 -0
  111. package/dist/fs-utils.js +13 -1
  112. package/dist/gateway-watchdog.d.ts +5 -0
  113. package/dist/gateway-watchdog.js +50 -0
  114. package/dist/hooks/post-reporting-event.mjs +1 -5
  115. package/dist/http/helpers/activity-headline.js +13 -132
  116. package/dist/http/helpers/auto-continue-engine.d.ts +198 -10
  117. package/dist/http/helpers/auto-continue-engine.js +2531 -186
  118. package/dist/http/helpers/autopilot-operations.d.ts +19 -0
  119. package/dist/http/helpers/autopilot-operations.js +182 -31
  120. package/dist/http/helpers/autopilot-runtime.d.ts +1 -0
  121. package/dist/http/helpers/autopilot-runtime.js +308 -20
  122. package/dist/http/helpers/autopilot-slice-utils.d.ts +18 -0
  123. package/dist/http/helpers/autopilot-slice-utils.js +516 -93
  124. package/dist/http/helpers/decision-mapper.d.ts +40 -0
  125. package/dist/http/helpers/decision-mapper.js +223 -7
  126. package/dist/http/helpers/dispatch-lifecycle.d.ts +19 -2
  127. package/dist/http/helpers/dispatch-lifecycle.js +242 -37
  128. package/dist/http/helpers/kickoff-context.js +74 -0
  129. package/dist/http/helpers/llm-client.d.ts +47 -0
  130. package/dist/http/helpers/llm-client.js +256 -0
  131. package/dist/http/helpers/mission-control.d.ts +102 -3
  132. package/dist/http/helpers/mission-control.js +498 -9
  133. package/dist/http/helpers/sentinel-catalog.d.ts +23 -0
  134. package/dist/http/helpers/sentinel-catalog.js +193 -0
  135. package/dist/http/helpers/session-classification.d.ts +9 -0
  136. package/dist/http/helpers/session-classification.js +564 -0
  137. package/dist/http/helpers/slice-experience-v2.d.ts +137 -0
  138. package/dist/http/helpers/slice-experience-v2.js +677 -0
  139. package/dist/http/helpers/slice-run-projections.d.ts +72 -0
  140. package/dist/http/helpers/slice-run-projections.js +860 -0
  141. package/dist/http/helpers/triage-mapper.d.ts +43 -0
  142. package/dist/http/helpers/triage-mapper.js +549 -0
  143. package/dist/http/helpers/value-utils.js +7 -2
  144. package/dist/http/helpers/workspace-scope.d.ts +15 -0
  145. package/dist/http/helpers/workspace-scope.js +170 -0
  146. package/dist/http/index.js +1354 -97
  147. package/dist/http/routes/agent-suite.d.ts +9 -0
  148. package/dist/http/routes/agent-suite.js +207 -8
  149. package/dist/http/routes/agents-catalog.js +64 -19
  150. package/dist/http/routes/chat.d.ts +19 -0
  151. package/dist/http/routes/chat.js +522 -0
  152. package/dist/http/routes/decision-actions.d.ts +8 -1
  153. package/dist/http/routes/decision-actions.js +42 -5
  154. package/dist/http/routes/dispatch-gateway-envelope.d.ts +25 -0
  155. package/dist/http/routes/dispatch-gateway-envelope.js +26 -0
  156. package/dist/http/routes/entities.d.ts +16 -0
  157. package/dist/http/routes/entities.js +294 -6
  158. package/dist/http/routes/live-legacy.d.ts +5 -0
  159. package/dist/http/routes/live-legacy.js +23 -509
  160. package/dist/http/routes/live-misc.d.ts +12 -0
  161. package/dist/http/routes/live-misc.js +251 -31
  162. package/dist/http/routes/live-snapshot.d.ts +48 -2
  163. package/dist/http/routes/live-snapshot.js +638 -19
  164. package/dist/http/routes/live-terminal.d.ts +11 -0
  165. package/dist/http/routes/live-terminal.js +261 -0
  166. package/dist/http/routes/live-triage.d.ts +61 -0
  167. package/dist/http/routes/live-triage.js +248 -0
  168. package/dist/http/routes/mission-control-actions.d.ts +49 -1
  169. package/dist/http/routes/mission-control-actions.js +1334 -84
  170. package/dist/http/routes/mission-control-read.d.ts +48 -3
  171. package/dist/http/routes/mission-control-read.js +1593 -20
  172. package/dist/http/routes/realtime-orchestrator.d.ts +10 -0
  173. package/dist/http/routes/realtime-orchestrator.js +74 -0
  174. package/dist/http/routes/run-control.d.ts +5 -2
  175. package/dist/http/routes/run-control.js +10 -0
  176. package/dist/http/routes/sentinels-catalog.d.ts +7 -0
  177. package/dist/http/routes/sentinels-catalog.js +24 -0
  178. package/dist/http/routes/summary.js +10 -3
  179. package/dist/http/routes/usage.d.ts +24 -0
  180. package/dist/http/routes/usage.js +362 -0
  181. package/dist/http/routes/work-artifacts.js +28 -9
  182. package/dist/index.js +165 -27
  183. package/dist/local-openclaw.js +29 -6
  184. package/dist/mcp-client-setup.js +3 -3
  185. package/dist/mcp-http-handler.js +33 -59
  186. package/dist/next-up-queue-store.d.ts +16 -1
  187. package/dist/next-up-queue-store.js +89 -7
  188. package/dist/outbox.d.ts +5 -0
  189. package/dist/outbox.js +113 -9
  190. package/dist/paths.js +24 -5
  191. package/dist/reporting/rollups.d.ts +53 -0
  192. package/dist/reporting/rollups.js +148 -0
  193. package/dist/retro/domain-templates.d.ts +45 -0
  194. package/dist/retro/domain-templates.js +297 -0
  195. package/dist/retro/quality-rubric.d.ts +33 -0
  196. package/dist/retro/quality-rubric.js +213 -0
  197. package/dist/runtime-cleanup.d.ts +18 -0
  198. package/dist/runtime-cleanup.js +87 -0
  199. package/dist/services/background.d.ts +11 -0
  200. package/dist/services/background.js +22 -0
  201. package/dist/services/experiment-randomization.d.ts +21 -0
  202. package/dist/services/experiment-randomization.js +63 -0
  203. package/dist/skill-pack-state.d.ts +36 -5
  204. package/dist/skill-pack-state.js +273 -29
  205. package/dist/sync/local-agent-telemetry.d.ts +13 -0
  206. package/dist/sync/local-agent-telemetry.js +128 -0
  207. package/dist/sync/outbox-replay.js +131 -24
  208. package/dist/team-context-store.d.ts +23 -0
  209. package/dist/team-context-store.js +116 -0
  210. package/dist/telemetry/posthog.js +4 -2
  211. package/dist/tools/core-tools.d.ts +10 -14
  212. package/dist/tools/core-tools.js +1289 -24
  213. package/dist/types.d.ts +2 -0
  214. package/dist/types.js +2 -0
  215. package/dist/worker-supervisor.js +23 -0
  216. package/package.json +14 -4
  217. package/dashboard/dist/assets/B3ziCA02.js +0 -8
  218. package/dashboard/dist/assets/B5NEElEI.css +0 -1
  219. package/dashboard/dist/assets/BhapSNAs.js +0 -215
  220. package/dashboard/dist/assets/iFdvE7lx.js +0 -1
  221. package/dashboard/dist/assets/jRJsmpYM.js +0 -1
  222. package/dashboard/dist/assets/sAhvFnpk.js +0 -4
@@ -22,20 +22,21 @@ import { homedir } from "node:os";
22
22
  import { join, dirname, extname, normalize, resolve, relative, sep } from "node:path";
23
23
  import { fileURLToPath } from "node:url";
24
24
  import { randomUUID } from "node:crypto";
25
- import { readNextUpQueuePins, removeNextUpQueuePin, setNextUpQueuePinOrder, upsertNextUpQueuePin, } from "../next-up-queue-store.js";
25
+ import { readNextUpQueuePins, removeNextUpQueuePin, setNextUpQueuePinOrder, suppressNextUpQueueItem, upsertNextUpQueuePin, } from "../next-up-queue-store.js";
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
29
  import { readAgentContexts, upsertAgentContext, upsertRunContext } from "../agent-context-store.js";
30
30
  import { getAgentRun, markAgentRunStopped, readAgentRuns, upsertAgentRun, } from "../agent-run-store.js";
31
31
  import { appendEntityComment, listEntityComments, mergeEntityComments, } from "../entity-comment-store.js";
32
+ import { listChatThreads } from "../chat-store.js";
32
33
  import { appendActivityItems, listActivityPage, } from "../activity-store.js";
33
34
  import { enrichActivityActorFields } from "../activity-actor-fields.js";
34
35
  import { readByokKeys, writeByokKeys } from "../byok-store.js";
35
36
  import { applyOrgxAgentSuitePlan, computeOrgxAgentSuitePlan, generateAgentSuiteOperationId, } from "../agent-suite.js";
36
37
  import { listRuntimeInstances, resolveRuntimeHookToken, upsertRuntimeInstanceFromHook, } from "../runtime-instance-store.js";
37
38
  import { parseJsonSafe } from "../json-utils.js";
38
- import { readSkillPackState, refreshSkillPackState, updateSkillPackPolicy } from "../skill-pack-state.js";
39
+ import { readSkillPackState, refreshSkillPackState, rollbackSkillPackPolicy, updateSkillPackPolicy, } from "../skill-pack-state.js";
39
40
  import { posthogCapture } from "../telemetry/posthog.js";
40
41
  import { createRouter } from "./router.js";
41
42
  import { summarizeActivityHeadline } from "./helpers/activity-headline.js";
@@ -45,7 +46,7 @@ import { mapDecisionEntity } from "./helpers/decision-mapper.js";
45
46
  import { idempotencyKey, stableHash } from "./helpers/hash-utils.js";
46
47
  import { createCodexBinResolver, } from "./helpers/autopilot-slice-utils.js";
47
48
  import { createLocalArtifactDetailFallbackBuilder } from "./helpers/artifact-fallback.js";
48
- import { buildMissionControlGraph, dedupeStrings, isDoneStatus, isInProgressStatus, isTodoStatus, listEntitiesSafe, normalizeEntityMutationPayload, pickStringArray, resolveAutoAssignments, } from "./helpers/mission-control.js";
49
+ import { buildMissionControlGraph, deriveExecutionPolicy, dedupeStrings, isDispatchableWorkstreamStatus, isDoneStatus, isInProgressStatus, isTodoStatus, listEntitiesSafe, normalizeEntityMutationPayload, pickStringArray, resolveAutoAssignments, selectSliceTasksByScope, } from "./helpers/mission-control.js";
49
50
  import { configureOpenClawProviderRouting, fetchBillingStatusSafe, isPidAlive, listOpenClawAgents, listOpenClawProviderModels, modelImpliesByok, normalizeOpenClawProvider, resolveAutoOpenClawProvider, resolveByokEnvOverrides, spawnOpenClawAgentTurn, stopDetachedProcess, } from "./helpers/openclaw-cli.js";
50
51
  import { fetchKickoffContextSafe, renderKickoffMessage } from "./helpers/kickoff-context.js";
51
52
  import { createDispatchLifecycle } from "./helpers/dispatch-lifecycle.js";
@@ -61,17 +62,23 @@ import { registerDebugRoutes } from "./routes/debug.js";
61
62
  import { registerEntityDynamicRoutes } from "./routes/entity-dynamic.js";
62
63
  import { registerEntitiesRoutes } from "./routes/entities.js";
63
64
  import { registerHealthRoutes } from "./routes/health.js";
65
+ import { registerChatRoutes } from "./routes/chat.js";
64
66
  import { registerLiveLegacyRoutes } from "./routes/live-legacy.js";
65
67
  import { registerLiveMiscRoutes } from "./routes/live-misc.js";
68
+ import { registerLiveTerminalRoutes } from "./routes/live-terminal.js";
66
69
  import { registerLiveSnapshotRoutes } from "./routes/live-snapshot.js";
67
70
  import { registerMissionControlActionsRoutes } from "./routes/mission-control-actions.js";
68
71
  import { registerMissionControlReadRoutes } from "./routes/mission-control-read.js";
69
72
  import { registerOnboardingRoutes } from "./routes/onboarding.js";
70
73
  import { registerRunControlRoutes } from "./routes/run-control.js";
71
74
  import { registerRuntimeHookRoutes } from "./routes/runtime-hooks.js";
75
+ import { registerSentinelsCatalogRoutes } from "./routes/sentinels-catalog.js";
72
76
  import { registerSettingsByokRoutes } from "./routes/settings-byok.js";
73
77
  import { registerSummaryRoutes } from "./routes/summary.js";
78
+ import { registerUsageRoutes } from "./routes/usage.js";
74
79
  import { registerWorkArtifactsRoutes } from "./routes/work-artifacts.js";
80
+ import { registerLiveTriageRoutes } from "./routes/live-triage.js";
81
+ import { registerRealtimeOrchestratorRoutes } from "./routes/realtime-orchestrator.js";
75
82
  // =============================================================================
76
83
  // Helpers
77
84
  // =============================================================================
@@ -96,10 +103,23 @@ async function resolveSkillPackOverrides(input) {
96
103
  }
97
104
  }
98
105
  function safeErrorMessage(err) {
99
- if (err instanceof Error)
100
- return err.message;
101
- if (typeof err === "string")
102
- return err;
106
+ const raw = err instanceof Error ? err.message : typeof err === "string" ? err : "";
107
+ const normalized = raw.trim().toLowerCase();
108
+ if (normalized.length > 0) {
109
+ if (normalized.includes("signal is aborted") ||
110
+ normalized.includes("aborterror") ||
111
+ normalized.includes("request cancelled") ||
112
+ normalized.includes("request canceled")) {
113
+ return "request timed out before upstream completed";
114
+ }
115
+ if (normalized.includes("timed out") || normalized.includes("timeout")) {
116
+ return "request timed out before upstream completed";
117
+ }
118
+ if (normalized.includes("failed to fetch") || normalized.includes("network")) {
119
+ return "network request failed";
120
+ }
121
+ return raw;
122
+ }
103
123
  return "Unexpected error";
104
124
  }
105
125
  function titleCaseFromSlug(value) {
@@ -185,11 +205,21 @@ const SNAPSHOT_RESPONSE_CACHE_TTL_MS = 1_500;
185
205
  const SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES = 16;
186
206
  const SNAPSHOT_ACTIVITY_PERSIST_MIN_INTERVAL_MS = 15_000;
187
207
  const SNAPSHOT_ACTIVITY_FINGERPRINT_DEPTH = 8;
188
- const NEXT_UP_QUEUE_CACHE_TTL_MS = readPositiveIntEnv("ORGX_NEXT_UP_QUEUE_CACHE_TTL_MS", 4_000, { min: 250, max: 120_000 });
208
+ const NEXT_UP_QUEUE_CACHE_TTL_MS = readPositiveIntEnv("ORGX_NEXT_UP_QUEUE_CACHE_TTL_MS", 30_000, { min: 250, max: 120_000 });
189
209
  const NEXT_UP_QUEUE_STALE_TTL_MS = readPositiveIntEnv("ORGX_NEXT_UP_QUEUE_STALE_TTL_MS", 45_000, { min: 1_000, max: 600_000 });
190
210
  const NEXT_UP_GRAPH_CONCURRENCY = readPositiveIntEnv("ORGX_NEXT_UP_GRAPH_CONCURRENCY", 20, { min: 1, max: 32 });
191
211
  const NEXT_UP_LIVE_AGENTS_TIMEOUT_MS = readPositiveIntEnv("ORGX_NEXT_UP_LIVE_AGENTS_TIMEOUT_MS", 1_500, { min: 200, max: 20_000 });
192
212
  const NEXT_UP_AGENT_CATALOG_TIMEOUT_MS = readPositiveIntEnv("ORGX_NEXT_UP_AGENT_CATALOG_TIMEOUT_MS", 900, { min: 100, max: 20_000 });
213
+ const NEXT_UP_LIVE_SESSIONS_TIMEOUT_MS = readPositiveIntEnv("ORGX_NEXT_UP_LIVE_SESSIONS_TIMEOUT_MS", 2_500, { min: 250, max: 30_000 });
214
+ const PROJECT_SCOPE_LOOKUP_TIMEOUT_MS = readPositiveIntEnv("ORGX_PROJECT_SCOPE_LOOKUP_TIMEOUT_MS", 2_500, { min: 250, max: 20_000 });
215
+ const PROJECT_SCOPE_MAX_INITIATIVE_PAGES = readPositiveIntEnv("ORGX_PROJECT_SCOPE_MAX_INITIATIVE_PAGES", 12, { min: 1, max: 100 });
216
+ const LIVE_WORKSPACE_INITIATIVE_STATUSES = [
217
+ "active",
218
+ "planning",
219
+ "paused",
220
+ "draft",
221
+ "in_progress",
222
+ ];
193
223
  let lastSnapshotActivityPersistAt = 0;
194
224
  let lastSnapshotActivityFingerprint = "";
195
225
  const snapshotResponseCache = new Map();
@@ -284,6 +314,9 @@ function deriveStructuredActivityBucket(input) {
284
314
  "nonBlockingDecisionCount",
285
315
  ]) ?? 0;
286
316
  if (event === "autopilot_slice_result") {
317
+ // Any blocked slice result needs decision-first surfacing in the Activity UX.
318
+ if (input.phase === "blocked")
319
+ return "decision";
287
320
  if (decisionRequired || blockingDecisions > 0)
288
321
  return "decision";
289
322
  if (artifacts > 0)
@@ -292,6 +325,13 @@ function deriveStructuredActivityBucket(input) {
292
325
  return "decision";
293
326
  return "message";
294
327
  }
328
+ if (event === "auto_continue_stopped") {
329
+ const stopReason = typeof metadata?.stop_reason === "string"
330
+ ? metadata.stop_reason.trim().toLowerCase()
331
+ : "";
332
+ if (stopReason === "blocked" || stopReason === "error")
333
+ return "decision";
334
+ }
295
335
  if (event && ACTIVITY_ARTIFACT_EVENT_HINTS.has(event))
296
336
  return "artifact";
297
337
  if (event && ACTIVITY_DECISION_EVENT_HINTS.has(event))
@@ -577,6 +617,81 @@ function mergeSessionTrees(base, extra) {
577
617
  };
578
618
  }
579
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
+ }
636
+ 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
+ };
580
695
  const merged = [...(base ?? []), ...(extra ?? [])].sort((a, b) => {
581
696
  const timestampDelta = Date.parse(b.timestamp) - Date.parse(a.timestamp);
582
697
  if (timestampDelta !== 0)
@@ -584,11 +699,17 @@ function mergeActivities(base, extra, limit) {
584
699
  return b.id.localeCompare(a.id);
585
700
  });
586
701
  const deduped = [];
587
- const seen = new Set();
702
+ const seenIds = new Set();
703
+ const seenSemantic = new Set();
588
704
  for (const item of merged) {
589
- if (seen.has(item.id))
705
+ if (seenIds.has(item.id))
706
+ continue;
707
+ seenIds.add(item.id);
708
+ const semanticKey = semanticActivityKey(item);
709
+ if (semanticKey && seenSemantic.has(semanticKey))
590
710
  continue;
591
- seen.add(item.id);
711
+ if (semanticKey)
712
+ seenSemantic.add(semanticKey);
592
713
  deduped.push(item);
593
714
  if (deduped.length >= limit)
594
715
  break;
@@ -719,6 +840,9 @@ function enrichSessionsWithRuntime(input, instances) {
719
840
  const agentId = (node.agentId ?? "").trim() || fallbackAgent.agentId;
720
841
  const agentName = (node.agentName ?? "").trim() || fallbackAgent.agentName;
721
842
  const nodeStatus = (node.status ?? "").trim().toLowerCase();
843
+ const isTerminalNodeStatus = nodeStatus === "completed" ||
844
+ nodeStatus === "cancelled" ||
845
+ nodeStatus === "archived";
722
846
  const isLiveLikeNodeStatus = nodeStatus === "running" ||
723
847
  nodeStatus === "active" ||
724
848
  nodeStatus === "in_progress" ||
@@ -726,20 +850,49 @@ function enrichSessionsWithRuntime(input, instances) {
726
850
  nodeStatus === "planning" ||
727
851
  nodeStatus === "dispatching";
728
852
  const shouldDowngradeStatusFromRuntime = isLiveLikeNodeStatus && (runtimeStatus === "queued" || runtimeStatus === "paused");
729
- const blockerReason = (node.blockerReason ?? "").trim() ||
730
- (node.status?.toLowerCase() === "blocked" || match.phase?.toLowerCase() === "blocked"
731
- ? (match.lastMessage ?? "").trim()
732
- : "");
853
+ const shouldPromoteStatusFromRuntime = runtimeStatus === "completed" ||
854
+ runtimeStatus === "blocked" ||
855
+ runtimeStatus === "review" ||
856
+ runtimeStatus === "handoff" ||
857
+ (runtimeStatus === "running" &&
858
+ (nodeStatus === "blocked" ||
859
+ nodeStatus === "failed" ||
860
+ nodeStatus === "queued" ||
861
+ nodeStatus === "paused"));
862
+ const nextStatus = shouldDowngradeStatusFromRuntime ||
863
+ (!isTerminalNodeStatus && shouldPromoteStatusFromRuntime)
864
+ ? runtimeStatus
865
+ : node.status;
866
+ const runtimeExplicitlyBlocked = runtimeStatus === "blocked" || match.phase?.toLowerCase() === "blocked";
867
+ const runtimeExplicitlyUnblocked = !runtimeExplicitlyBlocked && typeof match.phase === "string" && match.phase.trim().length > 0;
868
+ const runtimeBlockedReason = (match.lastMessage ?? "").trim();
869
+ const nextBlockerReason = runtimeExplicitlyBlocked
870
+ ? runtimeBlockedReason || (node.blockerReason ?? "").trim() || null
871
+ : runtimeExplicitlyUnblocked
872
+ ? null
873
+ : node.blockerReason ?? null;
874
+ const nextBlockers = runtimeExplicitlyBlocked
875
+ ? runtimeBlockedReason
876
+ ? [runtimeBlockedReason]
877
+ : Array.isArray(node.blockers)
878
+ ? node.blockers
879
+ : []
880
+ : runtimeExplicitlyUnblocked
881
+ ? []
882
+ : Array.isArray(node.blockers)
883
+ ? node.blockers
884
+ : [];
733
885
  return {
734
886
  ...node,
735
887
  agentId: agentId || null,
736
888
  agentName: agentName || null,
737
- status: shouldDowngradeStatusFromRuntime ? runtimeStatus : node.status,
889
+ status: nextStatus,
738
890
  state: node.state ?? match.state ?? null,
739
891
  lastEventSummary: shouldDowngradeStatusFromRuntime && runtimeStatus === "queued"
740
892
  ? node.lastEventSummary ?? "Recovered stale runtime; awaiting next dispatch."
741
893
  : node.lastEventSummary,
742
- blockerReason: blockerReason || node.blockerReason || null,
894
+ blockers: nextBlockers,
895
+ blockerReason: nextBlockerReason,
743
896
  runtimeClient: normalizeRuntimeSource(match.sourceClient),
744
897
  runtimeLabel: match.displayName,
745
898
  runtimeProvider: match.providerLogo,
@@ -749,6 +902,64 @@ function enrichSessionsWithRuntime(input, instances) {
749
902
  });
750
903
  return { ...input, nodes };
751
904
  }
905
+ function metadataHasStructuredScope(meta) {
906
+ const scalarScope = pickString(meta, [
907
+ "initiative_id",
908
+ "initiativeId",
909
+ "workstream_id",
910
+ "workstreamId",
911
+ "workstream_title",
912
+ "workstreamTitle",
913
+ "task_id",
914
+ "taskId",
915
+ "task_title",
916
+ "taskTitle",
917
+ "slice_run_id",
918
+ "sliceRunId",
919
+ "iwmt_id",
920
+ "iwmtId",
921
+ "milestone_id",
922
+ "milestoneId",
923
+ "milestone_title",
924
+ "milestoneTitle",
925
+ ]) ?? null;
926
+ if (scalarScope)
927
+ return true;
928
+ const listScopeKeys = [
929
+ "initiative_ids",
930
+ "initiativeIds",
931
+ "workstream_ids",
932
+ "workstreamIds",
933
+ "task_ids",
934
+ "taskIds",
935
+ "milestone_ids",
936
+ "milestoneIds",
937
+ "iwmt_ids",
938
+ "iwmtIds",
939
+ ];
940
+ for (const key of listScopeKeys) {
941
+ const value = meta[key];
942
+ if (!Array.isArray(value))
943
+ continue;
944
+ if (value.some((entry) => typeof entry === "string" && entry.trim().length > 0)) {
945
+ return true;
946
+ }
947
+ }
948
+ return false;
949
+ }
950
+ function shouldInjectRuntimeInstanceAsSession(instance, runId, meta) {
951
+ if (instance.state !== "active")
952
+ return false;
953
+ // Synthetic hook correlation ids are telemetry-only and should never render as user-facing sessions.
954
+ if (runId.toLowerCase().startsWith("hook-"))
955
+ return false;
956
+ const workstreamId = instance.workstreamId?.trim() ?? "";
957
+ const taskId = instance.taskId?.trim() ?? "";
958
+ if (workstreamId.length > 0 || taskId.length > 0)
959
+ return true;
960
+ // Keep only runtime records that include structured execution scope.
961
+ return metadataHasStructuredScope(meta);
962
+ }
752
963
  function injectRuntimeInstancesAsSessions(input, instances) {
753
964
  if (!Array.isArray(input.nodes))
754
965
  return input;
@@ -773,10 +984,6 @@ function injectRuntimeInstancesAsSessions(input, instances) {
773
984
  continue;
774
985
  if (existingRunIds.has(runId))
775
986
  continue;
776
- // Only surface active runtime instances as synthetic sessions.
777
- // Stale instances are reconciled onto existing sessions but shouldn't appear as fresh work.
778
- if (instance.state !== "active")
779
- continue;
780
987
  const initiativeId = instance.initiativeId?.trim() || null;
781
988
  const workstreamId = instance.workstreamId?.trim() || null;
782
989
  const runtimeClient = normalizeRuntimeSource(instance.sourceClient);
@@ -785,6 +992,8 @@ function injectRuntimeInstancesAsSessions(input, instances) {
785
992
  const meta = instance.metadata && typeof instance.metadata === "object"
786
993
  ? instance.metadata
787
994
  : {};
995
+ if (!shouldInjectRuntimeInstanceAsSession(instance, runId, meta))
996
+ continue;
788
997
  const titleHint = pickString(meta, ["workstream_title", "workstreamTitle"]) ??
789
998
  (workstreamId ? `Workstream ${workstreamId.slice(0, 8)}` : null);
790
999
  const initiativeHint = pickString(meta, ["initiative_title", "initiativeTitle"]) ??
@@ -898,18 +1107,18 @@ const CONTENT_SECURITY_POLICY = [
898
1107
  "form-action 'self'",
899
1108
  "object-src 'none'",
900
1109
  "script-src 'self'",
901
- "style-src 'self' 'unsafe-inline'",
1110
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
902
1111
  "img-src 'self' data: blob:",
903
- "font-src 'self' data:",
1112
+ "font-src 'self' data: https://fonts.gstatic.com",
904
1113
  "media-src 'self'",
905
- "connect-src 'self' https://*.useorgx.com https://*.openclaw.ai http://127.0.0.1:* http://localhost:*",
1114
+ "connect-src 'self' https://*.useorgx.com https://*.openclaw.ai https://api.openai.com https://*.openai.com http://127.0.0.1:* http://localhost:*",
906
1115
  ].join("; ");
907
1116
  const SECURITY_HEADERS = {
908
1117
  "X-Content-Type-Options": "nosniff",
909
1118
  "X-Frame-Options": "DENY",
910
1119
  "Referrer-Policy": "same-origin",
911
1120
  "X-Robots-Tag": "noindex, nofollow, noarchive, nosnippet, noimageindex",
912
- "Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=(), midi=(), magnetometer=(), gyroscope=()",
1121
+ "Permissions-Policy": "camera=(), microphone=(self), geolocation=(), payment=(), usb=(), midi=(), magnetometer=(), gyroscope=()",
913
1122
  "Cross-Origin-Opener-Policy": "same-origin",
914
1123
  "Cross-Origin-Resource-Policy": "same-origin",
915
1124
  "Origin-Agent-Cluster": "?1",
@@ -982,9 +1191,16 @@ function resolveSafeDistPath(subPath) {
982
1191
  }
983
1192
  return candidate;
984
1193
  }
985
- // =============================================================================
986
- // Helpers
987
- // =============================================================================
1194
+ const PRECOMPRESSED_FILE_EXTENSIONS = new Set([
1195
+ ".css",
1196
+ ".html",
1197
+ ".js",
1198
+ ".json",
1199
+ ".map",
1200
+ ".svg",
1201
+ ".txt",
1202
+ ".xml",
1203
+ ]);
988
1204
  const IMMUTABLE_FILE_CACHE = new Map();
989
1205
  const IMMUTABLE_FILE_CACHE_MAX = 128;
990
1206
  const FILE_PREVIEW_MAX_BYTES = 1_000_000;
@@ -1064,23 +1280,109 @@ function readFilePreview(pathname, totalBytes) {
1064
1280
  closeSync(fd);
1065
1281
  }
1066
1282
  }
1067
- function sendFile(res, filePath, cacheControl) {
1283
+ function parseAcceptedEncodings(rawHeader) {
1284
+ const parsed = new Map();
1285
+ if (!rawHeader || rawHeader.trim().length === 0)
1286
+ return parsed;
1287
+ const parts = rawHeader.split(",");
1288
+ for (const part of parts) {
1289
+ const [nameRaw, ...params] = part.split(";");
1290
+ const name = nameRaw?.trim().toLowerCase();
1291
+ if (!name)
1292
+ continue;
1293
+ let q = 1;
1294
+ for (const param of params) {
1295
+ const [keyRaw, valueRaw] = param.split("=");
1296
+ const key = keyRaw?.trim().toLowerCase();
1297
+ if (key !== "q")
1298
+ continue;
1299
+ const candidate = Number.parseFloat((valueRaw ?? "").trim());
1300
+ if (Number.isFinite(candidate)) {
1301
+ q = candidate;
1302
+ }
1303
+ }
1304
+ if (q <= 0)
1305
+ continue;
1306
+ const existing = parsed.get(name);
1307
+ if (existing == null || q > existing) {
1308
+ parsed.set(name, q);
1309
+ }
1310
+ }
1311
+ return parsed;
1312
+ }
1313
+ function resolveEncodingQuality(accepted, encoding) {
1314
+ if (accepted.has(encoding))
1315
+ return accepted.get(encoding) ?? 0;
1316
+ if (accepted.has("*"))
1317
+ return accepted.get("*") ?? 0;
1318
+ return 0;
1319
+ }
1320
+ function resolvePrecompressedVariant(req, filePath) {
1321
+ const ext = extname(filePath).toLowerCase();
1322
+ if (!PRECOMPRESSED_FILE_EXTENSIONS.has(ext))
1323
+ return null;
1324
+ const accepted = parseAcceptedEncodings(pickHeaderString(req.headers, ["accept-encoding"]));
1325
+ if (accepted.size === 0)
1326
+ return null;
1327
+ const candidates = [
1328
+ {
1329
+ encoding: "br",
1330
+ path: `${filePath}.br`,
1331
+ quality: resolveEncodingQuality(accepted, "br"),
1332
+ priority: 2,
1333
+ },
1334
+ {
1335
+ encoding: "gzip",
1336
+ path: `${filePath}.gz`,
1337
+ quality: resolveEncodingQuality(accepted, "gzip"),
1338
+ priority: 1,
1339
+ },
1340
+ ];
1341
+ candidates.sort((left, right) => {
1342
+ if (right.quality !== left.quality)
1343
+ return right.quality - left.quality;
1344
+ return right.priority - left.priority;
1345
+ });
1346
+ for (const candidate of candidates) {
1347
+ if (candidate.quality <= 0)
1348
+ continue;
1349
+ if (existsSync(candidate.path)) {
1350
+ return { path: candidate.path, encoding: candidate.encoding };
1351
+ }
1352
+ }
1353
+ return null;
1354
+ }
1355
+ function sendFile(req, res, filePath, cacheControl) {
1068
1356
  try {
1069
1357
  const shouldCacheImmutable = cacheControl.includes("immutable");
1358
+ const shouldVaryByEncoding = PRECOMPRESSED_FILE_EXTENSIONS.has(extname(filePath).toLowerCase());
1359
+ const precompressed = resolvePrecompressedVariant(req, filePath);
1360
+ const responsePath = precompressed?.path ?? filePath;
1361
+ const responseEncoding = precompressed?.encoding ?? null;
1362
+ const cacheKey = `${responsePath}|${cacheControl}`;
1070
1363
  if (shouldCacheImmutable) {
1071
- const cached = IMMUTABLE_FILE_CACHE.get(filePath);
1364
+ const cached = IMMUTABLE_FILE_CACHE.get(cacheKey);
1072
1365
  if (cached) {
1073
- res.writeHead(200, {
1366
+ const headers = {
1074
1367
  "Content-Type": cached.contentType,
1075
1368
  "Cache-Control": cacheControl,
1076
1369
  ...SECURITY_HEADERS,
1077
1370
  ...CORS_HEADERS,
1371
+ };
1372
+ if (cached.contentEncoding === "br")
1373
+ headers["Content-Encoding"] = "br";
1374
+ if (cached.contentEncoding === "gzip")
1375
+ headers["Content-Encoding"] = "gzip";
1376
+ if (cached.varyAcceptEncoding)
1377
+ headers["Vary"] = "Accept-Encoding";
1378
+ res.writeHead(200, {
1379
+ ...headers,
1078
1380
  });
1079
1381
  res.end(cached.content);
1080
1382
  return;
1081
1383
  }
1082
1384
  }
1083
- const content = readFileSync(filePath);
1385
+ const content = readFileSync(responsePath);
1084
1386
  const type = contentType(filePath);
1085
1387
  if (shouldCacheImmutable) {
1086
1388
  if (IMMUTABLE_FILE_CACHE.size >= IMMUTABLE_FILE_CACHE_MAX) {
@@ -1088,14 +1390,26 @@ function sendFile(res, filePath, cacheControl) {
1088
1390
  if (firstKey)
1089
1391
  IMMUTABLE_FILE_CACHE.delete(firstKey);
1090
1392
  }
1091
- IMMUTABLE_FILE_CACHE.set(filePath, { content, contentType: type });
1393
+ IMMUTABLE_FILE_CACHE.set(cacheKey, {
1394
+ content,
1395
+ contentType: type,
1396
+ contentEncoding: responseEncoding,
1397
+ varyAcceptEncoding: shouldVaryByEncoding,
1398
+ });
1092
1399
  }
1093
- res.writeHead(200, {
1400
+ const headers = {
1094
1401
  "Content-Type": type,
1095
1402
  "Cache-Control": cacheControl,
1096
1403
  ...SECURITY_HEADERS,
1097
1404
  ...CORS_HEADERS,
1098
- });
1405
+ };
1406
+ if (responseEncoding === "br")
1407
+ headers["Content-Encoding"] = "br";
1408
+ if (responseEncoding === "gzip")
1409
+ headers["Content-Encoding"] = "gzip";
1410
+ if (shouldVaryByEncoding)
1411
+ headers["Vary"] = "Accept-Encoding";
1412
+ res.writeHead(200, headers);
1099
1413
  res.end(content);
1100
1414
  }
1101
1415
  catch {
@@ -1110,10 +1424,24 @@ function send404(res) {
1110
1424
  });
1111
1425
  res.end("Not Found");
1112
1426
  }
1113
- function sendIndexHtml(res) {
1427
+ function sendStaleChunkRecovery(res) {
1428
+ const body = [
1429
+ "// Recover from stale chunk references after dashboard/plugin upgrades.",
1430
+ "window.location.replace('/orgx/live' + window.location.search);",
1431
+ "export {};"
1432
+ ].join("\n");
1433
+ res.writeHead(200, {
1434
+ "Content-Type": "application/javascript; charset=utf-8",
1435
+ "Cache-Control": "no-cache, no-store, must-revalidate",
1436
+ ...SECURITY_HEADERS,
1437
+ ...CORS_HEADERS,
1438
+ });
1439
+ res.end(body);
1440
+ }
1441
+ function sendIndexHtml(req, res) {
1114
1442
  const indexPath = join(DIST_DIR, "index.html");
1115
1443
  if (existsSync(indexPath)) {
1116
- sendFile(res, indexPath, "no-cache, no-store, must-revalidate");
1444
+ sendFile(req, res, indexPath, "no-cache, no-store, must-revalidate");
1117
1445
  }
1118
1446
  else {
1119
1447
  res.writeHead(503, {
@@ -1307,9 +1635,60 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1307
1635
  spawnAgentTurn,
1308
1636
  upsertAgentRun,
1309
1637
  });
1638
+ const normalizeRunnerAgentToken = (value) => {
1639
+ if (typeof value !== "string")
1640
+ return null;
1641
+ const trimmed = value.trim();
1642
+ if (!trimmed)
1643
+ return null;
1644
+ const normalized = trimmed.toLowerCase();
1645
+ if (normalized === "main" ||
1646
+ normalized === "undefined" ||
1647
+ normalized === "null" ||
1648
+ normalized === "n/a" ||
1649
+ normalized === "na") {
1650
+ return null;
1651
+ }
1652
+ return trimmed;
1653
+ };
1654
+ const pushRunnerAgent = (target, seen, input) => {
1655
+ const agentId = normalizeRunnerAgentToken(input.id ?? null);
1656
+ const agentName = normalizeRunnerAgentToken(input.name ?? null);
1657
+ if (!agentId && !agentName)
1658
+ return;
1659
+ const resolvedId = agentId ?? agentName;
1660
+ const dedupeKey = resolvedId.toLowerCase();
1661
+ if (seen.has(dedupeKey))
1662
+ return;
1663
+ seen.add(dedupeKey);
1664
+ target.push({
1665
+ id: resolvedId,
1666
+ name: agentName ?? resolvedId,
1667
+ });
1668
+ };
1669
+ const dedupeWithPrimary = (primary, extras) => {
1670
+ const merged = [];
1671
+ const seen = new Set();
1672
+ for (const candidate of [...primary, ...extras]) {
1673
+ const id = normalizeRunnerAgentToken(candidate.id);
1674
+ const name = normalizeRunnerAgentToken(candidate.name);
1675
+ if (!id && !name)
1676
+ continue;
1677
+ const resolvedId = id ?? name;
1678
+ const key = resolvedId.toLowerCase();
1679
+ if (seen.has(key))
1680
+ continue;
1681
+ seen.add(key);
1682
+ merged.push({
1683
+ id: resolvedId,
1684
+ name: name ?? resolvedId,
1685
+ });
1686
+ }
1687
+ return merged;
1688
+ };
1310
1689
  const codexBinResolver = createCodexBinResolver();
1311
1690
  const resolveCodexBinInfo = () => codexBinResolver.resolveCodexBinInfo();
1312
- const { autoContinueRuns, autoContinueSliceRuns, localInitiativeStatusOverrides, writeRuntimeEvent, autoContinueTickMs: AUTO_CONTINUE_TICK_MS, defaultAutoContinueTokenBudget, setLocalInitiativeStatusOverride, clearLocalInitiativeStatusOverride, applyLocalInitiativeOverrides, applyLocalInitiativeOverrideToGraph, updateInitiativeAutoContinueState, stopAutoContinueRun, tickAutoContinueRun, tickAllAutoContinue, isInitiativeActiveStatus, runningAutoContinueForWorkstream, startAutoContinueRun, } = createAutoContinueEngine({
1691
+ 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({
1313
1692
  client,
1314
1693
  filename: __filename,
1315
1694
  safeErrorMessage,
@@ -1327,10 +1706,194 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1327
1706
  clearSnapshotResponseCache,
1328
1707
  resolveByokEnvOverrides,
1329
1708
  randomUUID,
1709
+ fetchKickoffContextSafe,
1710
+ renderKickoffMessage,
1330
1711
  });
1331
1712
  const nextUpQueueCache = new Map();
1332
1713
  const nextUpQueueInFlight = new Map();
1333
- const nextUpQueueCacheKeyFor = (initiativeId) => initiativeId?.trim() || "__all__";
1714
+ const PROJECT_INITIATIVE_IDS_CACHE_TTL_MS = 20_000;
1715
+ const projectInitiativeIdsCache = new Map();
1716
+ const commandCenterScopeCache = new Map();
1717
+ const nextUpQueueCacheKeyFor = (initiativeId, projectId) => {
1718
+ const normalizedInitiative = initiativeId?.trim() || "__all__";
1719
+ const normalizedProject = projectId?.trim() || "__all__";
1720
+ return `${normalizedProject}::${normalizedInitiative}`;
1721
+ };
1722
+ async function listInitiativeIdsForProject(input) {
1723
+ const projectId = input.projectId.trim();
1724
+ if (!projectId)
1725
+ return [];
1726
+ const cached = projectInitiativeIdsCache.get(projectId);
1727
+ if (cached && cached.expiresAt > Date.now()) {
1728
+ return [...cached.ids];
1729
+ }
1730
+ const mapInitiativeIds = (rows, opts) => {
1731
+ const projectScopeId = opts?.projectId?.trim() ?? "";
1732
+ const commandCenterId = opts?.commandCenterId?.trim() ?? "";
1733
+ return rows
1734
+ .map((entry) => {
1735
+ const record = entry;
1736
+ if (projectScopeId) {
1737
+ const rowProjectId = pickString(record, ["project_id", "projectId"]) ?? "";
1738
+ if (rowProjectId !== projectScopeId)
1739
+ return null;
1740
+ }
1741
+ if (commandCenterId) {
1742
+ const rowCommandCenterId = pickString(record, [
1743
+ "workspace_id",
1744
+ "workspaceId",
1745
+ "command_center_id",
1746
+ "commandCenterId",
1747
+ ]) ?? "";
1748
+ if (rowCommandCenterId !== commandCenterId)
1749
+ return null;
1750
+ }
1751
+ return pickString(record, ["id"]);
1752
+ })
1753
+ .filter((id) => Boolean(id && id.trim().length > 0));
1754
+ };
1755
+ const cacheAndReturn = (ids) => {
1756
+ const normalized = dedupeStrings(ids
1757
+ .map((id) => id.trim())
1758
+ .filter((id) => id.length > 0));
1759
+ projectInitiativeIdsCache.set(projectId, {
1760
+ expiresAt: Date.now() + PROJECT_INITIATIVE_IDS_CACHE_TTL_MS,
1761
+ ids: normalized,
1762
+ });
1763
+ return normalized;
1764
+ };
1765
+ const isKnownCommandCenterScope = async () => {
1766
+ const cachedScope = commandCenterScopeCache.get(projectId);
1767
+ if (cachedScope && cachedScope.expiresAt > Date.now()) {
1768
+ return cachedScope.exists;
1769
+ }
1770
+ const cacheScope = (exists) => {
1771
+ commandCenterScopeCache.set(projectId, {
1772
+ expiresAt: Date.now() + PROJECT_INITIATIVE_IDS_CACHE_TTL_MS,
1773
+ exists,
1774
+ });
1775
+ return exists;
1776
+ };
1777
+ const hasId = (rows) => rows.some((entry) => {
1778
+ const record = entry;
1779
+ const id = pickString(record, ["id"]) ?? "";
1780
+ return id === projectId;
1781
+ });
1782
+ try {
1783
+ const byId = await withSoftTimeout("command center scope lookup", PROJECT_SCOPE_LOOKUP_TIMEOUT_MS, client.listEntities("command_center", {
1784
+ id: projectId,
1785
+ limit: 1,
1786
+ }));
1787
+ const byIdRows = Array.isArray(byId.data) ? byId.data : [];
1788
+ if (hasId(byIdRows))
1789
+ return cacheScope(true);
1790
+ }
1791
+ catch {
1792
+ // continue to all-command-center fallback
1793
+ }
1794
+ try {
1795
+ const all = await withSoftTimeout("command center catalog lookup", PROJECT_SCOPE_LOOKUP_TIMEOUT_MS, client.listEntities("command_center", {
1796
+ limit: 100,
1797
+ }));
1798
+ const allRows = Array.isArray(all.data) ? all.data : [];
1799
+ return cacheScope(hasId(allRows));
1800
+ }
1801
+ catch {
1802
+ return cacheScope(false);
1803
+ }
1804
+ };
1805
+ const listInitiativesWithFilters = async (filters) => {
1806
+ const rows = [];
1807
+ const pageSize = 100;
1808
+ const seenIds = new Set();
1809
+ let offset = 0;
1810
+ let page = 0;
1811
+ while (page < PROJECT_SCOPE_MAX_INITIATIVE_PAGES) {
1812
+ const result = await withSoftTimeout("initiative scope lookup", PROJECT_SCOPE_LOOKUP_TIMEOUT_MS, client.listEntities("initiative", {
1813
+ ...filters,
1814
+ limit: pageSize,
1815
+ offset,
1816
+ }));
1817
+ const pageRows = Array.isArray(result.data) ? result.data : [];
1818
+ let addedCount = 0;
1819
+ for (const entry of pageRows) {
1820
+ const record = entry;
1821
+ const id = pickString(record, ["id"]);
1822
+ if (id && seenIds.has(id))
1823
+ continue;
1824
+ if (id)
1825
+ seenIds.add(id);
1826
+ rows.push(entry);
1827
+ addedCount += 1;
1828
+ }
1829
+ const hasMoreFlag = Boolean(result.pagination?.has_more);
1830
+ const likelyMore = hasMoreFlag || pageRows.length >= pageSize;
1831
+ if (!likelyMore)
1832
+ break;
1833
+ if (addedCount === 0)
1834
+ break;
1835
+ offset += pageRows.length;
1836
+ page += 1;
1837
+ }
1838
+ return rows;
1839
+ };
1840
+ const listLiveInitiativesWithFilters = async (filters) => {
1841
+ // Fast path: request once without status fan-out. Upstream often returns
1842
+ // all relevant rows and this avoids 5x paginated round-trips.
1843
+ const broadRows = await listInitiativesWithFilters(filters);
1844
+ if (broadRows.length > 0)
1845
+ return broadRows;
1846
+ // Backward-compat fallback for upstreams that require explicit status.
1847
+ const rows = [];
1848
+ for (const status of LIVE_WORKSPACE_INITIATIVE_STATUSES) {
1849
+ const statusRows = await listInitiativesWithFilters({
1850
+ ...filters,
1851
+ status,
1852
+ });
1853
+ rows.push(...statusRows);
1854
+ }
1855
+ return rows;
1856
+ };
1857
+ try {
1858
+ // Workspace selection in the plugin uses command-center IDs.
1859
+ // Resolve that scope first so broad project queries never leak cross-workspace items.
1860
+ const byCommandCenterIds = mapInitiativeIds(await listLiveInitiativesWithFilters({
1861
+ workspace_id: projectId,
1862
+ command_center_id: projectId,
1863
+ }), { commandCenterId: projectId });
1864
+ if (byCommandCenterIds.length > 0)
1865
+ return cacheAndReturn(byCommandCenterIds);
1866
+ }
1867
+ catch {
1868
+ // continue to project-id fallback
1869
+ }
1870
+ try {
1871
+ // Do not hard-return empty for known command-center scopes here.
1872
+ // Some tenants only populate project_id links, so we continue through
1873
+ // project-id fallbacks before concluding the scope is empty.
1874
+ await isKnownCommandCenterScope();
1875
+ }
1876
+ catch {
1877
+ // continue to project-id fallback
1878
+ }
1879
+ try {
1880
+ const byWorkspaceFallback = mapInitiativeIds(await listLiveInitiativesWithFilters({
1881
+ workspace_id: projectId,
1882
+ command_center_id: projectId,
1883
+ }), { projectId });
1884
+ if (byWorkspaceFallback.length > 0)
1885
+ return cacheAndReturn(byWorkspaceFallback);
1886
+ }
1887
+ catch {
1888
+ // continue to empty fallback
1889
+ }
1890
+ try {
1891
+ return cacheAndReturn([]);
1892
+ }
1893
+ catch {
1894
+ return [];
1895
+ }
1896
+ }
1334
1897
  const readNextUpQueueCache = (key, opts) => {
1335
1898
  const entry = nextUpQueueCache.get(key);
1336
1899
  if (!entry)
@@ -1361,9 +1924,37 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1361
1924
  },
1362
1925
  });
1363
1926
  };
1927
+ const clearNextUpQueueCache = (initiativeId) => {
1928
+ const normalized = initiativeId?.trim() || null;
1929
+ if (!normalized) {
1930
+ nextUpQueueCache.clear();
1931
+ nextUpQueueInFlight.clear();
1932
+ return;
1933
+ }
1934
+ for (const key of Array.from(nextUpQueueCache.keys())) {
1935
+ if (key.endsWith(`::${normalized}`) || key.endsWith("::__all__")) {
1936
+ nextUpQueueCache.delete(key);
1937
+ }
1938
+ }
1939
+ for (const key of Array.from(nextUpQueueInFlight.keys())) {
1940
+ if (key.endsWith(`::${normalized}`) || key.endsWith("::__all__")) {
1941
+ nextUpQueueInFlight.delete(key);
1942
+ }
1943
+ }
1944
+ };
1364
1945
  async function buildNextUpQueueUncached(input) {
1365
1946
  const degraded = [];
1366
1947
  const requestedInitiativeId = input?.initiativeId?.trim() || null;
1948
+ const requestedProjectId = input?.projectId?.trim() || null;
1949
+ let allowedInitiativeIds = null;
1950
+ if (requestedProjectId && requestedProjectId.length > 0) {
1951
+ const scopedIds = await listInitiativeIdsForProject({
1952
+ projectId: requestedProjectId,
1953
+ });
1954
+ if (scopedIds.length > 0) {
1955
+ allowedInitiativeIds = new Set(scopedIds);
1956
+ }
1957
+ }
1367
1958
  const pinnedQueue = readNextUpQueuePins();
1368
1959
  const pinnedRankByKey = new Map();
1369
1960
  const pinnedByKey = new Map();
@@ -1377,6 +1968,17 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1377
1968
  preferredMilestoneId: pin.preferredMilestoneId ?? null,
1378
1969
  });
1379
1970
  }
1971
+ const suppressedKeySet = new Set();
1972
+ for (const suppression of pinnedQueue.suppressions ?? []) {
1973
+ const initiativeId = suppression.initiativeId?.trim();
1974
+ const workstreamId = suppression.workstreamId?.trim();
1975
+ if (!initiativeId || !workstreamId)
1976
+ continue;
1977
+ if (requestedInitiativeId && initiativeId !== requestedInitiativeId)
1978
+ continue;
1979
+ suppressedKeySet.add(`${initiativeId}:${workstreamId}`);
1980
+ }
1981
+ const isSuppressed = (initiativeId, workstreamId) => suppressedKeySet.has(`${initiativeId}:${workstreamId}`);
1380
1982
  const initiativeTitleById = new Map();
1381
1983
  const initiativeStatusById = new Map();
1382
1984
  const initiativePriorityById = new Map();
@@ -1388,7 +1990,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1388
1990
  initiativeTitleById.set(id, initiative.title);
1389
1991
  initiativeStatusById.set(id, initiative.status || "active");
1390
1992
  }
1391
- const initiativeResult = await listEntitiesSafe(client, "initiative", { limit: 500 });
1993
+ const initiativeResult = await withSoftTimeout("initiative list", PROJECT_SCOPE_LOOKUP_TIMEOUT_MS, listEntitiesSafe(client, "initiative", { limit: 500 })).catch((err) => ({
1994
+ items: [],
1995
+ warning: `initiative unavailable (${safeErrorMessage(err)})`,
1996
+ }));
1392
1997
  if (initiativeResult.warning)
1393
1998
  degraded.push(initiativeResult.warning);
1394
1999
  const initiatives = initiativeResult.items;
@@ -1407,6 +2012,39 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1407
2012
  if (priority)
1408
2013
  initiativePriorityById.set(id, priority);
1409
2014
  }
2015
+ const initiativeMatchesRequestedProject = (record) => {
2016
+ if (!requestedProjectId)
2017
+ return true;
2018
+ const scopedValue = pickString(record, [
2019
+ "workspace_id",
2020
+ "workspaceId",
2021
+ "command_center_id",
2022
+ "commandCenterId",
2023
+ "project_id",
2024
+ "projectId",
2025
+ ]) ?? null;
2026
+ if (!scopedValue)
2027
+ return false;
2028
+ return scopedValue === requestedProjectId;
2029
+ };
2030
+ if (requestedProjectId && !allowedInitiativeIds) {
2031
+ const metadataScopedIds = initiatives
2032
+ .map((entity) => {
2033
+ const record = entity;
2034
+ const id = pickString(record, ["id"]);
2035
+ if (!id)
2036
+ return null;
2037
+ return initiativeMatchesRequestedProject(record) ? id : null;
2038
+ })
2039
+ .filter((value) => Boolean(value));
2040
+ if (metadataScopedIds.length > 0) {
2041
+ allowedInitiativeIds = new Set(metadataScopedIds);
2042
+ degraded.push("workspace initiative scope lookup returned no rows; using metadata scoped initiatives.");
2043
+ }
2044
+ else {
2045
+ degraded.push("workspace initiative scope lookup returned no rows; local queue may be incomplete.");
2046
+ }
2047
+ }
1410
2048
  for (const [initiativeId, override] of localInitiativeStatusOverrides.entries()) {
1411
2049
  initiativeStatusById.set(initiativeId, override.status);
1412
2050
  }
@@ -1420,9 +2058,6 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1420
2058
  return 3;
1421
2059
  };
1422
2060
  const sortQueueItems = (a, b) => {
1423
- const queueDelta = queueRank(a.queueState) - queueRank(b.queueState);
1424
- if (queueDelta !== 0)
1425
- return queueDelta;
1426
2061
  const aPinnedRank = pinnedRankByKey.get(`${a.initiativeId}:${a.workstreamId}`);
1427
2062
  const bPinnedRank = pinnedRankByKey.get(`${b.initiativeId}:${b.workstreamId}`);
1428
2063
  if (aPinnedRank !== undefined || bPinnedRank !== undefined) {
@@ -1431,6 +2066,9 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1431
2066
  if (aRank !== bRank)
1432
2067
  return aRank - bRank;
1433
2068
  }
2069
+ const queueDelta = queueRank(a.queueState) - queueRank(b.queueState);
2070
+ if (queueDelta !== 0)
2071
+ return queueDelta;
1434
2072
  const priorityRank = (value) => {
1435
2073
  const normalized = (value ?? "").trim().toLowerCase();
1436
2074
  if (!normalized)
@@ -1466,10 +2104,11 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1466
2104
  const buildSessionFallbackQueue = async () => {
1467
2105
  let sessionTree = null;
1468
2106
  try {
1469
- sessionTree = await client.getLiveSessions({
2107
+ sessionTree = await withSoftTimeout("live sessions", NEXT_UP_LIVE_SESSIONS_TIMEOUT_MS, client.getLiveSessions({
1470
2108
  initiative: requestedInitiativeId,
2109
+ projectId: requestedProjectId,
1471
2110
  limit: 500,
1472
- });
2111
+ }));
1473
2112
  }
1474
2113
  catch (err) {
1475
2114
  degraded.push(`live sessions fallback unavailable (${safeErrorMessage(err)})`);
@@ -1501,6 +2140,8 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1501
2140
  continue;
1502
2141
  if (requestedInitiativeId && initiativeId !== requestedInitiativeId)
1503
2142
  continue;
2143
+ if (allowedInitiativeIds && !allowedInitiativeIds.has(initiativeId))
2144
+ continue;
1504
2145
  const initiativeStatus = initiativeStatusById.get(initiativeId) ?? "active";
1505
2146
  if (!isInitiativeActiveStatus(initiativeStatus))
1506
2147
  continue;
@@ -1508,6 +2149,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1508
2149
  const epoch = parseEpoch(node.updatedAt ?? node.lastEventAt ?? node.startedAt);
1509
2150
  const existing = grouped.get(key);
1510
2151
  if (!existing) {
2152
+ const runnerAgents = [];
2153
+ const runnerAgentSeen = new Set();
2154
+ pushRunnerAgent(runnerAgents, runnerAgentSeen, {
2155
+ id: node.agentId,
2156
+ name: node.agentName,
2157
+ });
1511
2158
  grouped.set(key, {
1512
2159
  initiativeId,
1513
2160
  workstreamId,
@@ -1518,6 +2165,8 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1518
2165
  workstreamTitle: `Workstream ${workstreamId.slice(0, 8)}`,
1519
2166
  statuses: new Set([node.status]),
1520
2167
  blockers: Array.isArray(node.blockers) ? [...node.blockers] : [],
2168
+ runnerAgents,
2169
+ runnerAgentSeen,
1521
2170
  latest: node,
1522
2171
  latestEpoch: epoch,
1523
2172
  });
@@ -1532,6 +2181,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1532
2181
  existing.blockers.push(blocker);
1533
2182
  }
1534
2183
  }
2184
+ pushRunnerAgent(existing.runnerAgents, existing.runnerAgentSeen, {
2185
+ id: node.agentId,
2186
+ name: node.agentName,
2187
+ });
1535
2188
  if (epoch >= existing.latestEpoch) {
1536
2189
  existing.latest = node;
1537
2190
  existing.latestEpoch = epoch;
@@ -1551,11 +2204,22 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1551
2204
  : hasQueued
1552
2205
  ? "queued"
1553
2206
  : "idle";
1554
- const runnerAgentId = (entry.latest.agentId ?? "").trim() || "main";
1555
- const runnerAgentName = (entry.latest.agentName ?? "").trim() ||
1556
- initiativeTitleById.get(`agent:${runnerAgentId}`) ||
1557
- runnerAgentId;
2207
+ const latestRunner = [];
2208
+ const latestRunnerSeen = new Set();
2209
+ pushRunnerAgent(latestRunner, latestRunnerSeen, {
2210
+ id: entry.latest.agentId,
2211
+ name: entry.latest.agentName,
2212
+ });
2213
+ const runnerAgents = latestRunner.length > 0
2214
+ ? dedupeWithPrimary(latestRunner, entry.runnerAgents)
2215
+ : [...entry.runnerAgents];
2216
+ const primaryRunner = runnerAgents[0] ?? null;
2217
+ const runnerAgentId = primaryRunner?.id ?? "unassigned";
2218
+ const runnerAgentName = primaryRunner?.name ?? "Unassigned";
1558
2219
  const pinKey = `${entry.initiativeId}:${entry.workstreamId}`;
2220
+ if (isSuppressed(entry.initiativeId, entry.workstreamId) && queueState !== "running") {
2221
+ continue;
2222
+ }
1559
2223
  fallbackItems.push({
1560
2224
  initiativeId: entry.initiativeId,
1561
2225
  initiativeTitle: entry.initiativeTitle,
@@ -1571,10 +2235,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1571
2235
  nextTaskDueAt: null,
1572
2236
  runnerAgentId,
1573
2237
  runnerAgentName,
2238
+ runnerAgents,
1574
2239
  runnerSource: "fallback",
1575
2240
  queueState,
1576
2241
  blockReason: hasBlocked
1577
- ? entry.blockers[0] ?? (statusValues.includes("failed") ? "Latest run failed" : "Workstream blocked")
2242
+ ? entry.blockers[0] ??
2243
+ (statusValues.includes("failed") ? "Latest run failed" : "Workstream blocked")
1578
2244
  : null,
1579
2245
  isPinned: pinnedRankByKey.has(pinKey),
1580
2246
  pinnedRank: pinnedRankByKey.get(pinKey) ?? null,
@@ -1591,6 +2257,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1591
2257
  return false;
1592
2258
  if (requestedInitiativeId && id !== requestedInitiativeId)
1593
2259
  return false;
2260
+ if (!initiativeMatchesRequestedProject(record))
2261
+ return false;
2262
+ if (allowedInitiativeIds && !allowedInitiativeIds.has(id))
2263
+ return false;
1594
2264
  const status = pickString(record, ["status"]);
1595
2265
  return isInitiativeActiveStatus(status);
1596
2266
  });
@@ -1616,6 +2286,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1616
2286
  try {
1617
2287
  const data = await withSoftTimeout("live agents", NEXT_UP_LIVE_AGENTS_TIMEOUT_MS, client.getLiveAgents({
1618
2288
  initiative: requestedInitiativeId,
2289
+ projectId: requestedProjectId,
1619
2290
  includeIdle: true,
1620
2291
  }));
1621
2292
  for (const raw of Array.isArray(data.agents) ? data.agents : []) {
@@ -1673,13 +2344,50 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1673
2344
  return (milestone?.status?.toLowerCase() === "blocked" ||
1674
2345
  workstream?.status?.toLowerCase() === "blocked");
1675
2346
  };
2347
+ const normalizeSliceScope = (value) => {
2348
+ if (value === "task" || value === "milestone" || value === "workstream") {
2349
+ return value;
2350
+ }
2351
+ return null;
2352
+ };
2353
+ const resolveExecutionPolicyFromActiveRuns = (activeRunIds, workstreamId) => {
2354
+ for (const runId of activeRunIds) {
2355
+ const slice = autoContinueSliceRuns.get(runId);
2356
+ if (!slice)
2357
+ continue;
2358
+ if (typeof slice.workstreamId === "string" && slice.workstreamId.trim()) {
2359
+ if (slice.workstreamId.trim() !== workstreamId)
2360
+ continue;
2361
+ }
2362
+ const domain = (slice.domain ?? "").trim();
2363
+ const requiredSkills = Array.isArray(slice.requiredSkills)
2364
+ ? slice.requiredSkills.filter((skill) => typeof skill === "string" && skill.trim().length > 0)
2365
+ : [];
2366
+ if (!domain || requiredSkills.length === 0)
2367
+ continue;
2368
+ const executionPolicy = {
2369
+ domain,
2370
+ requiredSkills,
2371
+ };
2372
+ if (typeof slice.behaviorConfigId === "string" && slice.behaviorConfigId.trim()) {
2373
+ executionPolicy.profile = slice.behaviorConfigId.trim();
2374
+ }
2375
+ const scope = normalizeSliceScope(slice.scope ?? null);
2376
+ if (scope) {
2377
+ executionPolicy.sliceScopePreference = scope;
2378
+ }
2379
+ return executionPolicy;
2380
+ }
2381
+ return null;
2382
+ };
1676
2383
  for (const workstream of workstreamNodes) {
2384
+ const workstreamKey = `${initiativeId}:${workstream.id}`;
1677
2385
  const todoTasks = graph.recentTodos
1678
2386
  .map((taskId) => nodeById.get(taskId))
1679
2387
  .filter((node) => node?.type === "task" &&
1680
2388
  node.workstreamId === workstream.id &&
1681
2389
  isTodoStatus(node.status));
1682
- const pinKey = `${initiativeId}:${workstream.id}`;
2390
+ const pinKey = workstreamKey;
1683
2391
  const pin = pinnedByKey.get(pinKey) ?? null;
1684
2392
  const preferredTask = pin?.preferredTaskId && nodeById.get(pin.preferredTaskId)
1685
2393
  ? nodeById.get(pin.preferredTaskId) ?? null
@@ -1704,12 +2412,100 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1704
2412
  const preferredReadyTask = preferredCandidates.find((task) => taskIsReady(task) && !taskHasBlockedParent(task));
1705
2413
  const candidateTask = preferredReadyTask ?? readyTask ?? todoTasks[0] ?? null;
1706
2414
  const autoContinueRun = runningAutoContinueForWorkstream(initiativeId, workstream.id);
1707
- let queueState = autoContinueRun
2415
+ const autoContinueLane = getAutoContinueLaneForWorkstream(initiativeId, workstream.id);
2416
+ const laneState = autoContinueLane?.state ?? null;
2417
+ const scopedAllowedWorkstreams = Array.isArray(autoContinueRun?.allowedWorkstreamIds)
2418
+ ? (autoContinueRun.allowedWorkstreamIds
2419
+ .filter((id) => typeof id === "string" && id.trim().length > 0)
2420
+ .map((id) => id.trim()))
2421
+ : [];
2422
+ const runScopedToCurrentWorkstream = scopedAllowedWorkstreams.length === 1 &&
2423
+ scopedAllowedWorkstreams[0] === workstream.id &&
2424
+ autoContinueRun?.status === "running";
2425
+ const activeRunIds = Array.isArray(autoContinueRun?.activeSliceRunIds)
2426
+ ? autoContinueRun.activeSliceRunIds
2427
+ .filter((id) => typeof id === "string" && id.trim().length > 0)
2428
+ .map((id) => id.trim())
2429
+ : [];
2430
+ const activeTaskId = (autoContinueLane?.activeTaskIds?.[0]?.trim() ||
2431
+ autoContinueRun?.activeTaskId?.trim() ||
2432
+ null) ??
2433
+ null;
2434
+ const activeTaskNode = activeTaskId ? nodeById.get(activeTaskId) ?? null : null;
2435
+ const policyTask = candidateTask ??
2436
+ activeTaskNode ??
2437
+ todoTasks.find((task) => task.workstreamId === workstream.id) ??
2438
+ null;
2439
+ const derivedExecutionPolicy = policyTask
2440
+ ? deriveExecutionPolicy(policyTask, workstream)
2441
+ : null;
2442
+ const activeExecutionPolicy = resolveExecutionPolicyFromActiveRuns(activeRunIds, workstream.id);
2443
+ const executionPolicy = derivedExecutionPolicy ?? activeExecutionPolicy;
2444
+ const runScope = normalizeSliceScope(autoContinueRun?.scope ?? null);
2445
+ const preferredPolicyScope = normalizeSliceScope(executionPolicy?.sliceScopePreference ?? null);
2446
+ const defaultScope = runScope ??
2447
+ (preferredPolicyScope && preferredPolicyScope !== "task"
2448
+ ? preferredPolicyScope
2449
+ : pin?.preferredMilestoneId
2450
+ ? "milestone"
2451
+ : "task");
2452
+ const scopeSelection = selectSliceTasksByScope({
2453
+ scope: defaultScope,
2454
+ workstreamId: workstream.id,
2455
+ milestoneId: pin?.preferredMilestoneId ?? null,
2456
+ recentTodos: graph.recentTodos,
2457
+ nodeById,
2458
+ includeVerification: autoContinueRun?.includeVerification ?? false,
2459
+ });
2460
+ const cappedSliceTasks = typeof executionPolicy?.maxSliceTasks === "number" &&
2461
+ executionPolicy.maxSliceTasks > 0
2462
+ ? scopeSelection.tasks.slice(0, executionPolicy.maxSliceTasks)
2463
+ : scopeSelection.tasks;
2464
+ const sliceTaskIds = cappedSliceTasks.length > 0
2465
+ ? cappedSliceTasks.map((task) => task.id)
2466
+ : candidateTask?.id
2467
+ ? [candidateTask.id]
2468
+ : activeTaskId
2469
+ ? [activeTaskId]
2470
+ : [];
2471
+ const sliceMilestoneId = defaultScope === "milestone"
2472
+ ? scopeSelection.milestoneIds[0] ?? pin?.preferredMilestoneId ?? null
2473
+ : null;
2474
+ let queueState = laneState === "running"
1708
2475
  ? "running"
1709
- : candidateTask
1710
- ? "queued"
1711
- : "idle";
2476
+ : runScopedToCurrentWorkstream
2477
+ ? "running"
2478
+ : candidateTask
2479
+ ? "queued"
2480
+ : "idle";
1712
2481
  let blockReason = null;
2482
+ if (laneState === "blocked") {
2483
+ queueState = "blocked";
2484
+ blockReason = autoContinueLane?.blockedReason ?? "Blocked";
2485
+ }
2486
+ else if (laneState === "waiting_dependency") {
2487
+ queueState = "blocked";
2488
+ if (Array.isArray(autoContinueLane?.waitingOnWorkstreamIds) &&
2489
+ autoContinueLane.waitingOnWorkstreamIds.length > 0) {
2490
+ const waitingTitles = autoContinueLane.waitingOnWorkstreamIds
2491
+ .map((id) => {
2492
+ const node = nodeById.get(id);
2493
+ return node?.type === "workstream" ? node.title : id;
2494
+ })
2495
+ .filter(Boolean);
2496
+ blockReason =
2497
+ waitingTitles.length > 0
2498
+ ? `Waiting on ${waitingTitles.slice(0, 2).join(", ")}${waitingTitles.length > 2 ? "…" : ""}`
2499
+ : "Waiting on dependency workstreams";
2500
+ }
2501
+ else {
2502
+ blockReason = "Waiting on dependency workstreams";
2503
+ }
2504
+ }
2505
+ else if (laneState === "rate_limited") {
2506
+ queueState = "blocked";
2507
+ blockReason = autoContinueLane?.blockedReason ?? "Rate-limited";
2508
+ }
1713
2509
  if (!autoContinueRun && !readyTask && candidateTask) {
1714
2510
  queueState = "blocked";
1715
2511
  const blockedDeps = candidateTask.dependencyIds
@@ -1729,27 +2525,48 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1729
2525
  if (!candidateTask && !autoContinueRun && !pin) {
1730
2526
  continue;
1731
2527
  }
2528
+ if (isSuppressed(initiativeId, workstream.id) && queueState !== "running") {
2529
+ continue;
2530
+ }
1732
2531
  runningWorkstreams.add(workstream.id);
1733
- const assignedAgent = workstream.assignedAgents[0] ?? null;
1734
- const inferredAgent = graph.initiative.assignedAgents[0] ??
1735
- liveAgentsByInitiative.get(initiativeId)?.[0] ??
1736
- (autoContinueRun?.agentId
1737
- ? {
1738
- id: autoContinueRun.agentId,
1739
- name: agentCatalogById.get(autoContinueRun.agentId)?.name ?? autoContinueRun.agentId,
1740
- domain: null,
1741
- }
1742
- : null);
1743
- const runnerSource = assignedAgent
2532
+ const assignedRunnerAgents = [];
2533
+ const assignedRunnerSeen = new Set();
2534
+ for (const agent of workstream.assignedAgents) {
2535
+ pushRunnerAgent(assignedRunnerAgents, assignedRunnerSeen, {
2536
+ id: agent.id,
2537
+ name: agent.name,
2538
+ });
2539
+ }
2540
+ const inferredRunnerAgents = [];
2541
+ const inferredRunnerSeen = new Set();
2542
+ for (const agent of graph.initiative.assignedAgents) {
2543
+ pushRunnerAgent(inferredRunnerAgents, inferredRunnerSeen, {
2544
+ id: agent.id,
2545
+ name: agent.name,
2546
+ });
2547
+ }
2548
+ for (const agent of liveAgentsByInitiative.get(initiativeId) ?? []) {
2549
+ pushRunnerAgent(inferredRunnerAgents, inferredRunnerSeen, {
2550
+ id: agent.id,
2551
+ name: agent.name,
2552
+ });
2553
+ }
2554
+ if (autoContinueRun?.agentId) {
2555
+ pushRunnerAgent(inferredRunnerAgents, inferredRunnerSeen, {
2556
+ id: autoContinueRun.agentId,
2557
+ name: agentCatalogById.get(autoContinueRun.agentId)?.name ??
2558
+ autoContinueRun.agentId,
2559
+ });
2560
+ }
2561
+ const runnerAgents = assignedRunnerAgents.length > 0 ? assignedRunnerAgents : inferredRunnerAgents;
2562
+ const runnerSource = assignedRunnerAgents.length > 0
1744
2563
  ? "assigned"
1745
- : inferredAgent
2564
+ : runnerAgents.length > 0
1746
2565
  ? "inferred"
1747
2566
  : "fallback";
1748
- const resolvedRunner = assignedAgent ?? inferredAgent;
1749
- const runnerAgentId = resolvedRunner?.id ?? autoContinueRun?.agentId ?? "main";
1750
- const runnerAgentName = resolvedRunner?.name ??
1751
- agentCatalogById.get(runnerAgentId)?.name ??
1752
- runnerAgentId;
2567
+ const primaryRunner = runnerAgents[0] ?? null;
2568
+ const runnerAgentId = primaryRunner?.id ?? "unassigned";
2569
+ const runnerAgentName = primaryRunner?.name ?? "Unassigned";
1753
2570
  itemsForInitiative.push({
1754
2571
  initiativeId,
1755
2572
  initiativeTitle,
@@ -1758,25 +2575,50 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1758
2575
  workstreamTitle: workstream.title,
1759
2576
  workstreamStatus: workstream.status,
1760
2577
  nextTaskId: candidateTask?.id ??
1761
- (autoContinueRun?.activeTaskId?.trim() || null),
2578
+ activeTaskId,
1762
2579
  nextTaskTitle: candidateTask?.title ??
1763
- (autoContinueRun?.activeTaskId
1764
- ? nodeById.get(autoContinueRun.activeTaskId)?.title ?? null
2580
+ ((activeTaskId)
2581
+ ? nodeById.get(activeTaskId)?.title ?? null
1765
2582
  : null),
1766
2583
  nextTaskPriority: candidateTask?.priorityNum ?? null,
1767
2584
  nextTaskDueAt: candidateTask?.dueDate ?? null,
1768
2585
  runnerAgentId,
1769
2586
  runnerAgentName,
2587
+ runnerAgents,
1770
2588
  runnerSource,
1771
2589
  queueState,
1772
2590
  blockReason,
1773
2591
  isPinned: Boolean(pin),
1774
2592
  pinnedRank: pin ? (pinnedRankByKey.get(pinKey) ?? null) : null,
2593
+ sliceScope: defaultScope,
2594
+ sliceTaskIds,
2595
+ sliceTaskCount: sliceTaskIds.length,
2596
+ sliceMilestoneId,
2597
+ executionPolicy,
1775
2598
  autoContinue: autoContinueRun
1776
2599
  ? {
1777
2600
  status: autoContinueRun.status,
1778
2601
  activeTaskId: autoContinueRun.activeTaskId,
1779
2602
  activeRunId: autoContinueRun.activeRunId,
2603
+ activeTaskIds: Array.isArray(autoContinueRun.activeTaskIds)
2604
+ ? autoContinueRun.activeTaskIds
2605
+ : [],
2606
+ activeRunIds: Array.isArray(autoContinueRun.activeSliceRunIds)
2607
+ ? autoContinueRun.activeSliceRunIds
2608
+ : [],
2609
+ laneState,
2610
+ laneBlockedReason: autoContinueLane?.blockedReason ?? null,
2611
+ laneWaitingOnWorkstreamIds: Array.isArray(autoContinueLane?.waitingOnWorkstreamIds)
2612
+ ? autoContinueLane.waitingOnWorkstreamIds
2613
+ : [],
2614
+ laneRetryAt: autoContinueLane?.retryAt ?? null,
2615
+ maxParallelSlices: typeof autoContinueRun.maxParallelSlices === "number"
2616
+ ? autoContinueRun.maxParallelSlices
2617
+ : 1,
2618
+ parallelMode: (typeof autoContinueRun.parallelMode === "string" &&
2619
+ autoContinueRun.parallelMode.toLowerCase() === "iwmt"
2620
+ ? "iwmt"
2621
+ : "iwmt"),
1780
2622
  stopReason: autoContinueRun.stopReason,
1781
2623
  updatedAt: autoContinueRun.updatedAt,
1782
2624
  }
@@ -1794,6 +2636,44 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1794
2636
  const workstream = nodeById.get(workstreamId);
1795
2637
  if (!workstream || workstream.type !== "workstream")
1796
2638
  continue;
2639
+ const lane = getAutoContinueLaneForWorkstream(initiativeId, workstream.id);
2640
+ if (!lane &&
2641
+ !(typeof run.activeRunId === "string" && run.activeRunId.trim().length > 0)) {
2642
+ continue;
2643
+ }
2644
+ const laneState = lane?.state ?? null;
2645
+ const activeRunIds = Array.isArray(run.activeSliceRunIds)
2646
+ ? run.activeSliceRunIds
2647
+ .filter((id) => typeof id === "string" && id.trim().length > 0)
2648
+ .map((id) => id.trim())
2649
+ : [];
2650
+ const activeTaskId = lane?.activeTaskIds?.[0] ?? run.activeTaskId;
2651
+ const activeTaskNode = activeTaskId ? nodeById.get(activeTaskId) ?? null : null;
2652
+ const executionPolicy = (activeTaskNode ? deriveExecutionPolicy(activeTaskNode, workstream) : null) ??
2653
+ resolveExecutionPolicyFromActiveRuns(activeRunIds, workstream.id);
2654
+ const sliceScope = normalizeSliceScope(run.scope ?? null) ?? "task";
2655
+ const sliceTaskIds = lane?.activeTaskIds?.length
2656
+ ? lane.activeTaskIds
2657
+ : activeTaskId
2658
+ ? [activeTaskId]
2659
+ : [];
2660
+ const queueState = laneState === "running"
2661
+ ? "running"
2662
+ : laneState === "blocked" ||
2663
+ laneState === "waiting_dependency" ||
2664
+ laneState === "rate_limited"
2665
+ ? "blocked"
2666
+ : "queued";
2667
+ if (isSuppressed(initiativeId, workstream.id) && queueState !== "running") {
2668
+ continue;
2669
+ }
2670
+ const runRunnerAgents = [];
2671
+ const runRunnerSeen = new Set();
2672
+ pushRunnerAgent(runRunnerAgents, runRunnerSeen, {
2673
+ id: run.agentId,
2674
+ name: agentCatalogById.get(run.agentId)?.name ?? run.agentId,
2675
+ });
2676
+ const runPrimaryRunner = runRunnerAgents[0] ?? null;
1797
2677
  itemsForInitiative.push({
1798
2678
  initiativeId,
1799
2679
  initiativeTitle,
@@ -1801,23 +2681,52 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1801
2681
  workstreamId: workstream.id,
1802
2682
  workstreamTitle: workstream.title,
1803
2683
  workstreamStatus: workstream.status,
1804
- nextTaskId: run.activeTaskId,
1805
- nextTaskTitle: run.activeTaskId
1806
- ? nodeById.get(run.activeTaskId)?.title ?? null
2684
+ nextTaskId: activeTaskId ?? null,
2685
+ nextTaskTitle: activeTaskId
2686
+ ? nodeById.get(activeTaskId)?.title ?? null
1807
2687
  : null,
1808
2688
  nextTaskPriority: null,
1809
2689
  nextTaskDueAt: null,
1810
- runnerAgentId: run.agentId,
1811
- runnerAgentName: agentCatalogById.get(run.agentId)?.name ?? run.agentId,
1812
- runnerSource: "inferred",
1813
- queueState: "running",
1814
- blockReason: null,
2690
+ runnerAgentId: runPrimaryRunner?.id ?? "unassigned",
2691
+ runnerAgentName: runPrimaryRunner?.name ?? "Unassigned",
2692
+ runnerAgents: runRunnerAgents,
2693
+ runnerSource: runPrimaryRunner ? "inferred" : "fallback",
2694
+ queueState,
2695
+ blockReason: queueState === "blocked"
2696
+ ? lane?.blockedReason ?? "Blocked"
2697
+ : null,
1815
2698
  isPinned: Boolean(pinnedByKey.get(`${initiativeId}:${workstream.id}`)),
1816
2699
  pinnedRank: pinnedRankByKey.get(`${initiativeId}:${workstream.id}`) ?? null,
2700
+ sliceScope,
2701
+ sliceTaskIds,
2702
+ sliceTaskCount: sliceTaskIds.length,
2703
+ sliceMilestoneId: sliceScope === "milestone"
2704
+ ? activeTaskNode?.milestoneId ?? null
2705
+ : null,
2706
+ executionPolicy,
1817
2707
  autoContinue: {
1818
2708
  status: run.status,
1819
2709
  activeTaskId: run.activeTaskId,
1820
2710
  activeRunId: run.activeRunId,
2711
+ activeTaskIds: Array.isArray(run.activeTaskIds)
2712
+ ? run.activeTaskIds
2713
+ : [],
2714
+ activeRunIds: Array.isArray(run.activeSliceRunIds)
2715
+ ? run.activeSliceRunIds
2716
+ : [],
2717
+ laneState,
2718
+ laneBlockedReason: lane?.blockedReason ?? null,
2719
+ laneWaitingOnWorkstreamIds: Array.isArray(lane?.waitingOnWorkstreamIds)
2720
+ ? lane.waitingOnWorkstreamIds
2721
+ : [],
2722
+ laneRetryAt: lane?.retryAt ?? null,
2723
+ maxParallelSlices: typeof run.maxParallelSlices === "number"
2724
+ ? run.maxParallelSlices
2725
+ : 1,
2726
+ parallelMode: typeof run.parallelMode === "string" &&
2727
+ run.parallelMode.toLowerCase() === "iwmt"
2728
+ ? "iwmt"
2729
+ : "iwmt",
1821
2730
  stopReason: run.stopReason,
1822
2731
  updatedAt: run.updatedAt,
1823
2732
  },
@@ -1839,7 +2748,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1839
2748
  return { items, degraded };
1840
2749
  }
1841
2750
  async function buildNextUpQueue(input) {
1842
- const key = nextUpQueueCacheKeyFor(input?.initiativeId?.trim() || null);
2751
+ const key = nextUpQueueCacheKeyFor(input?.initiativeId?.trim() || null, input?.projectId?.trim() || null);
1843
2752
  const fresh = readNextUpQueueCache(key, { allowStale: false });
1844
2753
  if (fresh)
1845
2754
  return fresh;
@@ -1883,6 +2792,14 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1883
2792
  nextUpQueueInFlight.delete(key);
1884
2793
  }
1885
2794
  }
2795
+ // Prime queue cache shortly after boot so first dashboard paint is not cold.
2796
+ const prewarmNextUpQueue = () => {
2797
+ void buildNextUpQueue({ initiativeId: null, projectId: null }).catch(() => {
2798
+ // best effort prewarm only
2799
+ });
2800
+ };
2801
+ const nextUpPrewarmTimer = setTimeout(prewarmNextUpQueue, 75);
2802
+ nextUpPrewarmTimer.unref?.();
1886
2803
  const autoContinueTimer = setInterval(() => {
1887
2804
  void tickAllAutoContinue();
1888
2805
  }, AUTO_CONTINUE_TICK_MS);
@@ -1916,6 +2833,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1916
2833
  formatInitiatives,
1917
2834
  getOnboardingState: async () => getOnboardingState(await onboarding.getStatus()),
1918
2835
  });
2836
+ registerUsageRoutes(apiRouter, {
2837
+ client,
2838
+ listActivityPage: ({ limit, runId, since, until, cursor }) => listActivityPage({ limit, runId, since, until, cursor }),
2839
+ sendJson,
2840
+ safeErrorMessage,
2841
+ });
1919
2842
  registerAgentSuiteRoutes(apiRouter, {
1920
2843
  pluginVersion: config.pluginVersion,
1921
2844
  telemetryDistinctId,
@@ -1926,6 +2849,11 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1926
2849
  applyOrgxAgentSuitePlan,
1927
2850
  generateAgentSuiteOperationId,
1928
2851
  updateSkillPackPolicy,
2852
+ rollbackSkillPackPolicy,
2853
+ fetchAgentRuntimeSettings: ({ workspaceId, projectId } = {}) => client.getClientAgentRuntimeSettings({
2854
+ workspaceId: workspaceId ?? projectId ?? null,
2855
+ }),
2856
+ updateAgentRuntimeSettings: (payload) => client.updateClientAgentRuntimeSettings(payload),
1929
2857
  posthogCapture,
1930
2858
  sendJson,
1931
2859
  safeErrorMessage,
@@ -1944,13 +2872,20 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1944
2872
  sendJson,
1945
2873
  safeErrorMessage,
1946
2874
  });
2875
+ registerSentinelsCatalogRoutes(apiRouter, {
2876
+ sendJson,
2877
+ safeErrorMessage,
2878
+ });
1947
2879
  registerMissionControlReadRoutes(apiRouter, {
1948
2880
  autoContinueRuns,
1949
2881
  defaultAutoContinueTokenBudget,
2882
+ defaultAutoContinueMaxParallelSlices,
1950
2883
  autoContinueTickMs: AUTO_CONTINUE_TICK_MS,
1951
2884
  buildMissionControlGraph: (initiativeId) => buildMissionControlGraph(client, initiativeId),
1952
2885
  applyLocalInitiativeOverrideToGraph: (graph) => applyLocalInitiativeOverrideToGraph(graph),
2886
+ listInitiativeIdsForProject: ({ projectId }) => listInitiativeIdsForProject({ projectId }),
1953
2887
  buildNextUpQueue,
2888
+ rawRequest: (requestMethod, requestPath, body) => client.rawRequest(requestMethod, requestPath, body),
1954
2889
  sendJson,
1955
2890
  safeErrorMessage,
1956
2891
  });
@@ -1993,12 +2928,127 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1993
2928
  applyLocalInitiativeOverrides,
1994
2929
  formatInitiatives,
1995
2930
  getSnapshot,
2931
+ scheduleWorkstreamReassignment: async (input) => {
2932
+ const initiativeId = input.initiativeId.trim();
2933
+ const workstreamId = input.workstreamId.trim();
2934
+ if (!initiativeId || !workstreamId)
2935
+ return null;
2936
+ const normalizedStatus = (input.status ?? "").trim().toLowerCase();
2937
+ const shouldRedispatch = normalizedStatus === "active" ||
2938
+ normalizedStatus === "ready" ||
2939
+ normalizedStatus === "queued" ||
2940
+ normalizedStatus === "running" ||
2941
+ normalizedStatus === "in_progress" ||
2942
+ normalizedStatus === "pending";
2943
+ if (!shouldRedispatch)
2944
+ return null;
2945
+ if (!isDispatchableWorkstreamStatus(normalizedStatus))
2946
+ return null;
2947
+ const liveRun = runningAutoContinueForWorkstream(initiativeId, workstreamId);
2948
+ return await scheduleAutoFixForWorkstream({
2949
+ initiativeId,
2950
+ workstreamId,
2951
+ runId: liveRun?.activeRunId ?? null,
2952
+ event: input.event,
2953
+ requestedByAgentId: "system",
2954
+ requestedByAgentName: "System",
2955
+ graceMs: 5_000,
2956
+ });
2957
+ },
1996
2958
  sendJson,
1997
2959
  safeErrorMessage,
1998
2960
  });
2961
+ const readCachedDecisionRows = () => {
2962
+ const snapshots = [
2963
+ readSnapshotResponseCache("live-snapshot"),
2964
+ readSnapshotResponseCache("dashboard-bundle"),
2965
+ readSnapshotResponseCache("live-snapshot-v2"),
2966
+ ];
2967
+ const rows = [];
2968
+ for (const snapshot of snapshots) {
2969
+ if (!snapshot || typeof snapshot !== "object")
2970
+ continue;
2971
+ const decisionsRaw = snapshot.decisions;
2972
+ if (!Array.isArray(decisionsRaw))
2973
+ continue;
2974
+ for (const entry of decisionsRaw) {
2975
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
2976
+ continue;
2977
+ rows.push(entry);
2978
+ }
2979
+ }
2980
+ return rows;
2981
+ };
2982
+ const emitDecisionResolvedActivity = async (input) => {
2983
+ const ids = Array.from(new Set(input.ids
2984
+ .filter((id) => typeof id === "string")
2985
+ .map((id) => id.trim())
2986
+ .filter(Boolean)));
2987
+ if (ids.length === 0)
2988
+ return;
2989
+ const decisionById = new Map();
2990
+ for (const row of readCachedDecisionRows()) {
2991
+ const rowId = pickString(row, ["id"])?.trim() ?? "";
2992
+ if (!rowId || decisionById.has(rowId))
2993
+ continue;
2994
+ decisionById.set(rowId, row);
2995
+ }
2996
+ for (const decisionId of ids) {
2997
+ const row = decisionById.get(decisionId) ?? null;
2998
+ const decisionTitle = pickString(row ?? {}, ["title", "summary"]) ??
2999
+ `Decision ${decisionId.slice(0, 8)}`;
3000
+ const scopedInitiativeId = input.initiativeId ??
3001
+ pickString(row ?? {}, ["initiative_id", "initiativeId"]) ??
3002
+ null;
3003
+ const scopedRunId = input.sliceRunId ??
3004
+ pickString(row ?? {}, [
3005
+ "run_id",
3006
+ "runId",
3007
+ "source_run_id",
3008
+ "sourceRunId",
3009
+ "correlation_id",
3010
+ "correlationId",
3011
+ ]) ??
3012
+ null;
3013
+ await emitActivitySafe({
3014
+ initiativeId: scopedInitiativeId,
3015
+ runId: scopedRunId ?? undefined,
3016
+ correlationId: scopedRunId ?? undefined,
3017
+ phase: "review",
3018
+ level: "info",
3019
+ message: `Decision ${input.action === "approve" ? "approved" : "rejected"}: ${decisionTitle}`,
3020
+ progressPct: 100,
3021
+ nextStep: input.action === "approve"
3022
+ ? "Execution can continue with the approved direction."
3023
+ : "Review the rejected decision and provide revised guidance to continue safely.",
3024
+ metadata: {
3025
+ event: "decision_resolved",
3026
+ action: input.action,
3027
+ resolver: "human",
3028
+ decision_id: decisionId,
3029
+ decision_ids: ids,
3030
+ decision_title: decisionTitle,
3031
+ initiative_id: scopedInitiativeId,
3032
+ workstream_id: pickString(row ?? {}, ["workstream_id", "workstreamId"]) ?? null,
3033
+ source_run_id: scopedRunId,
3034
+ option_id: input.optionId ?? null,
3035
+ note: input.note ?? null,
3036
+ slice_run_id: input.sliceRunId ?? null,
3037
+ },
3038
+ });
3039
+ }
3040
+ };
1999
3041
  registerDecisionActionsRoutes(apiRouter, {
2000
3042
  parseJsonRequest,
2001
- bulkDecideDecisions: (ids, action, note) => client.bulkDecideDecisions(ids, action, note),
3043
+ bulkDecideDecisions: (ids, action, input) => client.bulkDecideDecisions(ids, action, input),
3044
+ emitDecisionResolvedActivity: async (ids, action, input) => {
3045
+ await emitDecisionResolvedActivity({
3046
+ ids,
3047
+ action,
3048
+ note: input?.note ?? null,
3049
+ optionId: input?.optionId ?? null,
3050
+ });
3051
+ },
2002
3052
  sendJson,
2003
3053
  safeErrorMessage,
2004
3054
  });
@@ -2009,6 +3059,104 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2009
3059
  createRunCheckpoint: (runId, input) => client.createRunCheckpoint(runId, input),
2010
3060
  restoreRunCheckpoint: (runId, input) => client.restoreRunCheckpoint(runId, input),
2011
3061
  runAction: (runId, action, input) => client.runAction(runId, action, input),
3062
+ markRunCompleted: async (runId, input) => {
3063
+ const normalizedRunId = runId.trim();
3064
+ if (!normalizedRunId) {
3065
+ throw new Error("runId is required");
3066
+ }
3067
+ const nowIso = new Date().toISOString();
3068
+ const reason = input.reason?.trim() || null;
3069
+ const message = reason
3070
+ ? `Marked completed from dashboard (${reason}).`
3071
+ : "Marked completed from dashboard.";
3072
+ const existingRun = getAgentRun(normalizedRunId);
3073
+ // ── Try OrgX-side completion first (for remote sessions) ────────────
3074
+ let remoteOk = false;
3075
+ try {
3076
+ await client.updateEntity("run", normalizedRunId, {
3077
+ status: "completed",
3078
+ phase: "completed",
3079
+ });
3080
+ remoteOk = true;
3081
+ }
3082
+ catch {
3083
+ // OrgX may not support updating runs directly — fall through to local
3084
+ }
3085
+ // ── Local operations (defensive — partial failures don't block) ─────
3086
+ let runtimeRecord = null;
3087
+ try {
3088
+ runtimeRecord = upsertRuntimeInstanceFromHook({
3089
+ source_client: "api",
3090
+ event: "session_stop",
3091
+ run_id: normalizedRunId,
3092
+ correlation_id: normalizedRunId,
3093
+ initiative_id: existingRun?.initiativeId ?? null,
3094
+ workstream_id: existingRun?.workstreamId ?? null,
3095
+ task_id: existingRun?.taskId ?? null,
3096
+ agent_id: existingRun?.agentId ?? null,
3097
+ phase: "completed",
3098
+ message,
3099
+ timestamp: nowIso,
3100
+ metadata: {
3101
+ source: "dashboard_manual_complete",
3102
+ reason,
3103
+ },
3104
+ });
3105
+ }
3106
+ catch (err) {
3107
+ console.error(`[markRunCompleted] upsertRuntime failed for ${normalizedRunId}:`, err);
3108
+ }
3109
+ try {
3110
+ markAgentRunStopped(normalizedRunId);
3111
+ }
3112
+ catch (err) {
3113
+ console.error(`[markRunCompleted] markAgentRunStopped failed for ${normalizedRunId}:`, err);
3114
+ }
3115
+ if (runtimeRecord) {
3116
+ broadcastRuntimeSse("runtime.updated", runtimeRecord);
3117
+ }
3118
+ clearSnapshotResponseCache();
3119
+ try {
3120
+ appendActivityItems([
3121
+ {
3122
+ id: randomUUID(),
3123
+ type: "run_completed",
3124
+ title: message,
3125
+ description: reason,
3126
+ agentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
3127
+ agentName: runtimeRecord?.agentName ?? null,
3128
+ requesterAgentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
3129
+ requesterAgentName: runtimeRecord?.agentName ?? null,
3130
+ executorAgentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
3131
+ executorAgentName: runtimeRecord?.agentName ?? null,
3132
+ runId: normalizedRunId,
3133
+ initiativeId: runtimeRecord?.initiativeId ?? existingRun?.initiativeId ?? null,
3134
+ timestamp: nowIso,
3135
+ phase: "completed",
3136
+ state: "done",
3137
+ summary: message,
3138
+ metadata: {
3139
+ source: "dashboard_manual_complete",
3140
+ reason,
3141
+ remoteOk,
3142
+ event: "dashboard_run_mark_completed",
3143
+ },
3144
+ },
3145
+ ]);
3146
+ }
3147
+ catch (err) {
3148
+ console.error(`[markRunCompleted] appendActivity failed for ${normalizedRunId}:`, err);
3149
+ }
3150
+ return {
3151
+ ok: true,
3152
+ data: {
3153
+ runId: normalizedRunId,
3154
+ action: "complete",
3155
+ status: "completed",
3156
+ remoteOk,
3157
+ },
3158
+ };
3159
+ },
2012
3160
  sendJson,
2013
3161
  safeErrorMessage,
2014
3162
  });
@@ -2032,6 +3180,20 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2032
3180
  sendJson,
2033
3181
  safeErrorMessage,
2034
3182
  });
3183
+ registerChatRoutes(apiRouter, {
3184
+ parseJsonRequest,
3185
+ pickString,
3186
+ parsePositiveInt,
3187
+ emitActivitySafe,
3188
+ sendJson,
3189
+ safeErrorMessage,
3190
+ });
3191
+ registerRealtimeOrchestratorRoutes(apiRouter, {
3192
+ parseJsonRequest,
3193
+ rawRequest: (requestMethod, requestPath, body) => client.rawRequest(requestMethod, requestPath, body),
3194
+ sendJson,
3195
+ safeErrorMessage,
3196
+ });
2035
3197
  registerMissionControlActionsRoutes(apiRouter, {
2036
3198
  parseJsonRequest,
2037
3199
  pickString,
@@ -2049,11 +3211,17 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2049
3211
  stopAutoContinueRun,
2050
3212
  updateInitiativeAutoContinueState,
2051
3213
  tickAllAutoContinue,
3214
+ scheduleAutoFixForWorkstream,
2052
3215
  upsertNextUpQueuePin,
2053
3216
  removeNextUpQueuePin,
3217
+ suppressNextUpQueueItem,
2054
3218
  setNextUpQueuePinOrder,
3219
+ clearNextUpQueueCache,
2055
3220
  resolveAutoAssignments,
3221
+ buildMissionControlGraph: (initiativeId) => buildMissionControlGraph(client, initiativeId),
3222
+ applyLocalInitiativeOverrideToGraph: (graph) => applyLocalInitiativeOverrideToGraph(graph),
2056
3223
  client,
3224
+ rawRequest: (requestMethod, requestPath, body) => client.rawRequest(requestMethod, requestPath, body),
2057
3225
  sendJson,
2058
3226
  safeErrorMessage,
2059
3227
  });
@@ -2095,10 +3263,11 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2095
3263
  parseJsonRequest,
2096
3264
  pickString,
2097
3265
  summarizeActivityHeadline,
2098
- getLiveAgents: ({ initiative, includeIdle }) => client.getLiveAgents({ initiative, includeIdle }),
2099
- getLiveInitiatives: ({ id, limit }) => client.getLiveInitiatives({ id, limit }),
2100
- getLiveDecisions: ({ status, limit }) => client.getLiveDecisions({ status, limit }),
3266
+ getLiveAgents: ({ initiative, projectId, includeIdle }) => client.getLiveAgents({ initiative, projectId, includeIdle }),
3267
+ getLiveInitiatives: ({ id, projectId, limit, offset }) => client.getLiveInitiatives({ id, projectId, limit, offset }),
3268
+ getLiveDecisions: ({ status, projectId, limit }) => client.getLiveDecisions({ status, projectId, limit }),
2101
3269
  getHandoffs: () => client.getHandoffs(),
3270
+ listInitiativeIdsForProject: ({ projectId }) => listInitiativeIdsForProject({ projectId }),
2102
3271
  loadLocalOpenClawSnapshot,
2103
3272
  toLocalLiveAgents,
2104
3273
  toLocalLiveInitiatives,
@@ -2107,9 +3276,15 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2107
3276
  sendJson,
2108
3277
  safeErrorMessage,
2109
3278
  });
3279
+ registerLiveTerminalRoutes(apiRouter, {
3280
+ parseJsonRequest,
3281
+ sendJson,
3282
+ safeErrorMessage,
3283
+ });
2110
3284
  registerLiveLegacyRoutes(apiRouter, {
2111
- getLiveSessions: ({ initiative, limit }) => client.getLiveSessions({ initiative, limit }),
2112
- getLiveActivity: ({ run, since, limit }) => client.getLiveActivity({ run, since, limit }),
3285
+ getLiveSessions: ({ initiative, projectId, limit }) => client.getLiveSessions({ initiative, projectId, limit }),
3286
+ getLiveActivity: ({ run, since, projectId, limit }) => client.getLiveActivity({ run, since, projectId, limit }),
3287
+ listInitiativeIdsForProject: ({ projectId }) => listInitiativeIdsForProject({ projectId }),
2113
3288
  listRuntimeInstances,
2114
3289
  injectRuntimeInstancesAsSessions,
2115
3290
  enrichSessionsWithRuntime,
@@ -2171,11 +3346,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2171
3346
  toLocalSessionTree,
2172
3347
  toLocalLiveActivity,
2173
3348
  toLocalLiveAgents,
2174
- getLiveSessions: ({ initiative, limit }) => client.getLiveSessions({ initiative, limit }),
2175
- getLiveActivity: ({ run, since, limit }) => client.getLiveActivity({ run, since, limit }),
3349
+ getLiveSessions: ({ initiative, projectId, limit }) => client.getLiveSessions({ initiative, projectId, limit }),
3350
+ getLiveActivity: ({ run, since, projectId, limit }) => client.getLiveActivity({ run, since, projectId, limit }),
2176
3351
  getHandoffs: () => client.getHandoffs(),
2177
- getLiveDecisions: ({ status, limit }) => client.getLiveDecisions({ status, limit }),
2178
- getLiveAgents: ({ initiative, includeIdle }) => client.getLiveAgents({ initiative, includeIdle }),
3352
+ getLiveDecisions: ({ status, projectId, limit }) => client.getLiveDecisions({ status, projectId, limit }),
3353
+ getLiveAgents: ({ initiative, projectId, includeIdle }) => client.getLiveAgents({ initiative, projectId, includeIdle }),
3354
+ listInitiativeIdsForProject: ({ projectId }) => listInitiativeIdsForProject({ projectId }),
2179
3355
  mapDecisionEntity: (entry) => mapDecisionEntity(entry),
2180
3356
  applyAgentContextsToSessionTree,
2181
3357
  applyAgentContextsToActivity,
@@ -2196,6 +3372,14 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2196
3372
  lastSnapshotActivityFingerprint = state.lastFingerprint;
2197
3373
  lastSnapshotActivityPersistAt = state.lastPersistAt;
2198
3374
  },
3375
+ parseJsonRequest,
3376
+ buildNextUpQueue: ({ initiativeId, projectId }) => buildNextUpQueue({ initiativeId, projectId }),
3377
+ bulkDecideDecisions: (ids, action, input) => client.bulkDecideDecisions(ids, action, input),
3378
+ emitDecisionResolvedActivity: async (input) => {
3379
+ await emitDecisionResolvedActivity(input);
3380
+ },
3381
+ runAction: (runId, action, input) => client.runAction(runId, action, input),
3382
+ listChatThreads: ({ commandCenterId, initiativeId, limit, offset }) => listChatThreads({ commandCenterId, initiativeId, limit, offset }),
2199
3383
  sendJson,
2200
3384
  });
2201
3385
  registerRuntimeHookRoutes(apiRouter, {
@@ -2232,12 +3416,81 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2232
3416
  sendJson,
2233
3417
  safeErrorMessage,
2234
3418
  });
3419
+ registerLiveTerminalRoutes(apiRouter, {
3420
+ parseJsonRequest,
3421
+ sendJson,
3422
+ safeErrorMessage,
3423
+ });
3424
+ registerLiveTriageRoutes(apiRouter, {
3425
+ parseJsonRequest,
3426
+ sendJson,
3427
+ getDecisions: (workspaceId) => {
3428
+ // Return cached decisions from latest snapshot, or empty array
3429
+ const normalizedWorkspaceId = (workspaceId ?? "").trim();
3430
+ const scopedKeys = normalizedWorkspaceId
3431
+ ? [
3432
+ `live-snapshot:${normalizedWorkspaceId}`,
3433
+ `live-snapshot-v2:${normalizedWorkspaceId}`,
3434
+ `dashboard-bundle:${normalizedWorkspaceId}`,
3435
+ ]
3436
+ : [];
3437
+ const fallbackKeys = ["live-snapshot", "dashboard-bundle", "live-snapshot-v2"];
3438
+ const keys = [...scopedKeys, ...fallbackKeys];
3439
+ try {
3440
+ for (const key of keys) {
3441
+ const cached = readSnapshotResponseCache(key);
3442
+ if (!cached || typeof cached !== "object" || !("decisions" in cached))
3443
+ continue;
3444
+ const decisions = cached.decisions;
3445
+ if (Array.isArray(decisions))
3446
+ return decisions;
3447
+ }
3448
+ }
3449
+ catch {
3450
+ // best effort
3451
+ }
3452
+ return [];
3453
+ },
3454
+ getBlockerEvents: () => {
3455
+ // Extract blocker events from recent activity
3456
+ // In future, this will read from a dedicated blocker store
3457
+ return [];
3458
+ },
3459
+ resolveDecisionAction: async (decisionId, action, note, optionId) => {
3460
+ try {
3461
+ await client.bulkDecideDecisions([decisionId], action, { note: note ?? undefined, optionId: optionId ?? undefined });
3462
+ return { ok: true };
3463
+ }
3464
+ catch (err) {
3465
+ return {
3466
+ ok: false,
3467
+ error: err instanceof Error ? err.message : "Decision action failed",
3468
+ };
3469
+ }
3470
+ },
3471
+ emitDecisionResolvedActivity: async (input) => {
3472
+ await emitDecisionResolvedActivity(input);
3473
+ },
3474
+ });
2235
3475
  return async function handler(req, res) {
2236
3476
  const method = (req.method ?? "GET").toUpperCase();
2237
3477
  const rawUrl = req.url ?? "/";
2238
3478
  const [path, queryString] = rawUrl.split("?", 2);
2239
3479
  const url = path;
2240
3480
  const searchParams = new URLSearchParams(queryString ?? "");
3481
+ // Legacy deep-link compatibility:
3482
+ // Older launch paths still point at /workspace-hub. Route those into
3483
+ // the current dashboard entrypoint while preserving query params.
3484
+ if (url === "/workspace-hub" || url === "/workspace-hub/") {
3485
+ const suffix = queryString && queryString.trim().length > 0 ? `?${queryString}` : "";
3486
+ res.writeHead(302, {
3487
+ Location: `/orgx/live${suffix}`,
3488
+ ...SECURITY_HEADERS,
3489
+ ...CORS_HEADERS,
3490
+ });
3491
+ res.end();
3492
+ return true;
3493
+ }
2241
3494
  // Only handle /orgx paths — return false for everything else
2242
3495
  if (!url.startsWith("/orgx")) {
2243
3496
  return false;
@@ -2317,9 +3570,13 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2317
3570
  const cacheControl = assetExt === ".js" || assetExt === ".css"
2318
3571
  ? "no-cache"
2319
3572
  : "public, max-age=31536000, immutable";
2320
- sendFile(res, assetPath, cacheControl);
3573
+ sendFile(req, res, assetPath, cacheControl);
2321
3574
  }
2322
3575
  else {
3576
+ if (/^assets\/[A-Za-z0-9_-]+\.js$/i.test(subPath)) {
3577
+ sendStaleChunkRecovery(res);
3578
+ return true;
3579
+ }
2323
3580
  send404(res);
2324
3581
  }
2325
3582
  return true;
@@ -2328,12 +3585,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2328
3585
  if (subPath) {
2329
3586
  const filePath = resolveSafeDistPath(subPath);
2330
3587
  if (filePath && existsSync(filePath)) {
2331
- sendFile(res, filePath, "no-cache");
3588
+ sendFile(req, res, filePath, "no-cache");
2332
3589
  return true;
2333
3590
  }
2334
3591
  }
2335
3592
  // SPA fallback: serve index.html for all other routes under /orgx/live
2336
- sendIndexHtml(res);
3593
+ sendIndexHtml(req, res);
2337
3594
  return true;
2338
3595
  }
2339
3596
  // Catch-all for /orgx but not /orgx/live or /orgx/api