@useorgx/openclaw-plugin 0.4.9 → 0.7.2

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 (224) hide show
  1. package/README.md +77 -11
  2. package/dashboard/dist/assets/6mILZQ2a.js +1 -0
  3. package/dashboard/dist/assets/6mILZQ2a.js.br +0 -0
  4. package/dashboard/dist/assets/6mILZQ2a.js.gz +0 -0
  5. package/dashboard/dist/assets/8dksYiq4.js +2 -0
  6. package/dashboard/dist/assets/8dksYiq4.js.br +0 -0
  7. package/dashboard/dist/assets/8dksYiq4.js.gz +0 -0
  8. package/dashboard/dist/assets/B5zYRHc3.js +1 -0
  9. package/dashboard/dist/assets/B5zYRHc3.js.br +0 -0
  10. package/dashboard/dist/assets/B5zYRHc3.js.gz +0 -0
  11. package/dashboard/dist/assets/B6wPWJ35.js +1 -0
  12. package/dashboard/dist/assets/B6wPWJ35.js.br +0 -0
  13. package/dashboard/dist/assets/B6wPWJ35.js.gz +0 -0
  14. package/dashboard/dist/assets/BJgZIVUQ.js +53 -0
  15. package/dashboard/dist/assets/BJgZIVUQ.js.br +0 -0
  16. package/dashboard/dist/assets/BJgZIVUQ.js.gz +0 -0
  17. package/dashboard/dist/assets/BWEwjt1W.js +1 -0
  18. package/dashboard/dist/assets/BWEwjt1W.js.br +0 -0
  19. package/dashboard/dist/assets/BWEwjt1W.js.gz +0 -0
  20. package/dashboard/dist/assets/BgOYB78t.js +4 -0
  21. package/dashboard/dist/assets/BgOYB78t.js.br +0 -0
  22. package/dashboard/dist/assets/BgOYB78t.js.gz +0 -0
  23. package/dashboard/dist/assets/BzRbDCAD.css +1 -0
  24. package/dashboard/dist/assets/BzRbDCAD.css.br +0 -0
  25. package/dashboard/dist/assets/BzRbDCAD.css.gz +0 -0
  26. package/dashboard/dist/assets/C-KIc3Wc.js.br +0 -0
  27. package/dashboard/dist/assets/C-KIc3Wc.js.gz +0 -0
  28. package/dashboard/dist/assets/C8uM3AX8.js +1 -0
  29. package/dashboard/dist/assets/C8uM3AX8.js.br +0 -0
  30. package/dashboard/dist/assets/C8uM3AX8.js.gz +0 -0
  31. package/dashboard/dist/assets/C9jy61eu.js +212 -0
  32. package/dashboard/dist/assets/C9jy61eu.js.br +0 -0
  33. package/dashboard/dist/assets/C9jy61eu.js.gz +0 -0
  34. package/dashboard/dist/assets/CC63EwFD.js +1 -0
  35. package/dashboard/dist/assets/CC63EwFD.js.br +0 -0
  36. package/dashboard/dist/assets/CC63EwFD.js.gz +0 -0
  37. package/dashboard/dist/assets/CL_wXqR7.js +1 -0
  38. package/dashboard/dist/assets/CL_wXqR7.js.br +0 -0
  39. package/dashboard/dist/assets/CL_wXqR7.js.gz +0 -0
  40. package/dashboard/dist/assets/CZaT3ob_.js +1 -0
  41. package/dashboard/dist/assets/CZaT3ob_.js.br +0 -0
  42. package/dashboard/dist/assets/CZaT3ob_.js.gz +0 -0
  43. package/dashboard/dist/assets/CgaottFX.js +1 -0
  44. package/dashboard/dist/assets/CgaottFX.js.br +0 -0
  45. package/dashboard/dist/assets/CgaottFX.js.gz +0 -0
  46. package/dashboard/dist/assets/{CpJsfbXo.js → CxQ08qFN.js} +2 -2
  47. package/dashboard/dist/assets/CxQ08qFN.js.br +0 -0
  48. package/dashboard/dist/assets/CxQ08qFN.js.gz +0 -0
  49. package/dashboard/dist/assets/CzCxAZlW.js +1 -0
  50. package/dashboard/dist/assets/CzCxAZlW.js.br +0 -0
  51. package/dashboard/dist/assets/CzCxAZlW.js.gz +0 -0
  52. package/dashboard/dist/assets/D3iMTYEj.js +1 -0
  53. package/dashboard/dist/assets/D3iMTYEj.js.br +0 -0
  54. package/dashboard/dist/assets/D3iMTYEj.js.gz +0 -0
  55. package/dashboard/dist/assets/D8JNX8kq.js +2 -0
  56. package/dashboard/dist/assets/D8JNX8kq.js.br +0 -0
  57. package/dashboard/dist/assets/D8JNX8kq.js.gz +0 -0
  58. package/dashboard/dist/assets/DnA8dpj6.js +1 -0
  59. package/dashboard/dist/assets/DnA8dpj6.js.br +0 -0
  60. package/dashboard/dist/assets/DnA8dpj6.js.gz +0 -0
  61. package/dashboard/dist/assets/IUexzymk.js +1 -0
  62. package/dashboard/dist/assets/IUexzymk.js.br +0 -0
  63. package/dashboard/dist/assets/IUexzymk.js.gz +0 -0
  64. package/dashboard/dist/assets/cNrhgGc1.js +8 -0
  65. package/dashboard/dist/assets/cNrhgGc1.js.br +0 -0
  66. package/dashboard/dist/assets/cNrhgGc1.js.gz +0 -0
  67. package/dashboard/dist/assets/ic2FaMnh.js +1 -0
  68. package/dashboard/dist/assets/ic2FaMnh.js.br +0 -0
  69. package/dashboard/dist/assets/ic2FaMnh.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/rttbDbEx.js +1 -0
  74. package/dashboard/dist/assets/rttbDbEx.js.br +0 -0
  75. package/dashboard/dist/assets/rttbDbEx.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/practice-exercise-schema.d.ts +216 -0
  102. package/dist/contracts/practice-exercise-schema.js +314 -0
  103. package/dist/contracts/retro-schema.d.ts +81 -0
  104. package/dist/contracts/retro-schema.js +80 -0
  105. package/dist/contracts/shared-types.d.ts +159 -0
  106. package/dist/contracts/shared-types.js +199 -1
  107. package/dist/contracts/skill-pack-schema.d.ts +192 -0
  108. package/dist/contracts/skill-pack-schema.js +180 -0
  109. package/dist/contracts/types.d.ts +247 -2
  110. package/dist/entities/auto-assignment.js +43 -17
  111. package/dist/event-sanitization.d.ts +11 -0
  112. package/dist/event-sanitization.js +113 -0
  113. package/dist/gateway-watchdog.d.ts +5 -0
  114. package/dist/gateway-watchdog.js +50 -0
  115. package/dist/hooks/post-reporting-event.mjs +1 -5
  116. package/dist/http/helpers/activity-headline.js +13 -132
  117. package/dist/http/helpers/auto-continue-engine.d.ts +198 -10
  118. package/dist/http/helpers/auto-continue-engine.js +3145 -186
  119. package/dist/http/helpers/autopilot-operations.d.ts +19 -0
  120. package/dist/http/helpers/autopilot-operations.js +182 -31
  121. package/dist/http/helpers/autopilot-runtime.d.ts +1 -0
  122. package/dist/http/helpers/autopilot-runtime.js +328 -25
  123. package/dist/http/helpers/autopilot-slice-utils.d.ts +18 -0
  124. package/dist/http/helpers/autopilot-slice-utils.js +514 -93
  125. package/dist/http/helpers/decision-mapper.d.ts +40 -0
  126. package/dist/http/helpers/decision-mapper.js +223 -7
  127. package/dist/http/helpers/dispatch-lifecycle.d.ts +19 -2
  128. package/dist/http/helpers/dispatch-lifecycle.js +242 -37
  129. package/dist/http/helpers/kickoff-context.js +104 -0
  130. package/dist/http/helpers/llm-client.d.ts +47 -0
  131. package/dist/http/helpers/llm-client.js +256 -0
  132. package/dist/http/helpers/mission-control.d.ts +102 -3
  133. package/dist/http/helpers/mission-control.js +498 -9
  134. package/dist/http/helpers/sentinel-catalog.d.ts +23 -0
  135. package/dist/http/helpers/sentinel-catalog.js +193 -0
  136. package/dist/http/helpers/session-classification.d.ts +9 -0
  137. package/dist/http/helpers/session-classification.js +564 -0
  138. package/dist/http/helpers/slice-experience-v2.d.ts +137 -0
  139. package/dist/http/helpers/slice-experience-v2.js +677 -0
  140. package/dist/http/helpers/slice-run-projections.d.ts +72 -0
  141. package/dist/http/helpers/slice-run-projections.js +877 -0
  142. package/dist/http/helpers/triage-mapper.d.ts +43 -0
  143. package/dist/http/helpers/triage-mapper.js +549 -0
  144. package/dist/http/helpers/value-utils.js +7 -2
  145. package/dist/http/helpers/workspace-scope.d.ts +15 -0
  146. package/dist/http/helpers/workspace-scope.js +170 -0
  147. package/dist/http/index.js +1420 -105
  148. package/dist/http/routes/agent-suite.d.ts +9 -0
  149. package/dist/http/routes/agent-suite.js +294 -8
  150. package/dist/http/routes/agents-catalog.js +64 -19
  151. package/dist/http/routes/chat.d.ts +19 -0
  152. package/dist/http/routes/chat.js +522 -0
  153. package/dist/http/routes/decision-actions.d.ts +8 -1
  154. package/dist/http/routes/decision-actions.js +42 -5
  155. package/dist/http/routes/dispatch-gateway-envelope.d.ts +25 -0
  156. package/dist/http/routes/dispatch-gateway-envelope.js +26 -0
  157. package/dist/http/routes/entities.d.ts +16 -0
  158. package/dist/http/routes/entities.js +232 -6
  159. package/dist/http/routes/live-legacy.d.ts +5 -0
  160. package/dist/http/routes/live-legacy.js +23 -509
  161. package/dist/http/routes/live-misc.d.ts +12 -0
  162. package/dist/http/routes/live-misc.js +251 -31
  163. package/dist/http/routes/live-snapshot.d.ts +49 -2
  164. package/dist/http/routes/live-snapshot.js +653 -23
  165. package/dist/http/routes/live-terminal.d.ts +11 -0
  166. package/dist/http/routes/live-terminal.js +154 -0
  167. package/dist/http/routes/live-triage.d.ts +61 -0
  168. package/dist/http/routes/live-triage.js +192 -0
  169. package/dist/http/routes/mission-control-actions.d.ts +49 -1
  170. package/dist/http/routes/mission-control-actions.js +1246 -84
  171. package/dist/http/routes/mission-control-read.d.ts +48 -3
  172. package/dist/http/routes/mission-control-read.js +1658 -20
  173. package/dist/http/routes/realtime-orchestrator.d.ts +10 -0
  174. package/dist/http/routes/realtime-orchestrator.js +74 -0
  175. package/dist/http/routes/run-control.d.ts +5 -2
  176. package/dist/http/routes/run-control.js +10 -0
  177. package/dist/http/routes/sentinels-catalog.d.ts +7 -0
  178. package/dist/http/routes/sentinels-catalog.js +24 -0
  179. package/dist/http/routes/summary.js +10 -3
  180. package/dist/http/routes/usage.d.ts +24 -0
  181. package/dist/http/routes/usage.js +362 -0
  182. package/dist/http/routes/work-artifacts.js +28 -9
  183. package/dist/index.js +165 -27
  184. package/dist/local-openclaw.js +29 -6
  185. package/dist/mcp-client-setup.js +3 -3
  186. package/dist/mcp-http-handler.d.ts +3 -0
  187. package/dist/mcp-http-handler.js +34 -60
  188. package/dist/next-up-queue-store.d.ts +16 -1
  189. package/dist/next-up-queue-store.js +89 -7
  190. package/dist/outbox.d.ts +5 -0
  191. package/dist/outbox.js +113 -9
  192. package/dist/paths.js +36 -5
  193. package/dist/reporting/rollups.d.ts +41 -0
  194. package/dist/reporting/rollups.js +113 -0
  195. package/dist/retro/domain-templates.d.ts +45 -0
  196. package/dist/retro/domain-templates.js +297 -0
  197. package/dist/retro/quality-rubric.d.ts +33 -0
  198. package/dist/retro/quality-rubric.js +213 -0
  199. package/dist/runtime-cleanup.d.ts +18 -0
  200. package/dist/runtime-cleanup.js +87 -0
  201. package/dist/services/background.d.ts +11 -0
  202. package/dist/services/background.js +22 -0
  203. package/dist/services/experiment-randomization.d.ts +21 -0
  204. package/dist/services/experiment-randomization.js +63 -0
  205. package/dist/skill-pack-state.d.ts +36 -5
  206. package/dist/skill-pack-state.js +273 -29
  207. package/dist/sync/local-agent-telemetry.d.ts +13 -0
  208. package/dist/sync/local-agent-telemetry.js +128 -0
  209. package/dist/sync/outbox-replay.js +131 -24
  210. package/dist/team-context-store.d.ts +23 -0
  211. package/dist/team-context-store.js +116 -0
  212. package/dist/telemetry/posthog.js +4 -2
  213. package/dist/tools/core-tools.d.ts +10 -14
  214. package/dist/tools/core-tools.js +1289 -24
  215. package/dist/types.d.ts +2 -0
  216. package/dist/types.js +2 -0
  217. package/dist/worker-supervisor.js +23 -0
  218. package/package.json +20 -6
  219. package/dashboard/dist/assets/B3ziCA02.js +0 -8
  220. package/dashboard/dist/assets/B5NEElEI.css +0 -1
  221. package/dashboard/dist/assets/BhapSNAs.js +0 -215
  222. package/dashboard/dist/assets/iFdvE7lx.js +0 -1
  223. package/dashboard/dist/assets/jRJsmpYM.js +0 -1
  224. package/dashboard/dist/assets/sAhvFnpk.js +0 -4
@@ -22,20 +22,22 @@ 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
+ import { appendToOutbox } from "../outbox.js";
29
30
  import { readAgentContexts, upsertAgentContext, upsertRunContext } from "../agent-context-store.js";
30
31
  import { getAgentRun, markAgentRunStopped, readAgentRuns, upsertAgentRun, } from "../agent-run-store.js";
31
32
  import { appendEntityComment, listEntityComments, mergeEntityComments, } from "../entity-comment-store.js";
33
+ import { listChatThreads } from "../chat-store.js";
32
34
  import { appendActivityItems, listActivityPage, } from "../activity-store.js";
33
35
  import { enrichActivityActorFields } from "../activity-actor-fields.js";
34
36
  import { readByokKeys, writeByokKeys } from "../byok-store.js";
35
37
  import { applyOrgxAgentSuitePlan, computeOrgxAgentSuitePlan, generateAgentSuiteOperationId, } from "../agent-suite.js";
36
38
  import { listRuntimeInstances, resolveRuntimeHookToken, upsertRuntimeInstanceFromHook, } from "../runtime-instance-store.js";
37
39
  import { parseJsonSafe } from "../json-utils.js";
38
- import { readSkillPackState, refreshSkillPackState, updateSkillPackPolicy } from "../skill-pack-state.js";
40
+ import { readSkillPackState, refreshSkillPackState, rollbackSkillPackPolicy, updateSkillPackPolicy, } from "../skill-pack-state.js";
39
41
  import { posthogCapture } from "../telemetry/posthog.js";
40
42
  import { createRouter } from "./router.js";
41
43
  import { summarizeActivityHeadline } from "./helpers/activity-headline.js";
@@ -45,7 +47,7 @@ import { mapDecisionEntity } from "./helpers/decision-mapper.js";
45
47
  import { idempotencyKey, stableHash } from "./helpers/hash-utils.js";
46
48
  import { createCodexBinResolver, } from "./helpers/autopilot-slice-utils.js";
47
49
  import { createLocalArtifactDetailFallbackBuilder } from "./helpers/artifact-fallback.js";
48
- import { buildMissionControlGraph, dedupeStrings, isDoneStatus, isInProgressStatus, isTodoStatus, listEntitiesSafe, normalizeEntityMutationPayload, pickStringArray, resolveAutoAssignments, } from "./helpers/mission-control.js";
50
+ import { buildMissionControlGraph, deriveExecutionPolicy, dedupeStrings, isDispatchableWorkstreamStatus, isDoneStatus, isInProgressStatus, isTodoStatus, listEntitiesSafe, normalizeEntityMutationPayload, pickStringArray, resolveAutoAssignments, selectSliceTasksByScope, } from "./helpers/mission-control.js";
49
51
  import { configureOpenClawProviderRouting, fetchBillingStatusSafe, isPidAlive, listOpenClawAgents, listOpenClawProviderModels, modelImpliesByok, normalizeOpenClawProvider, resolveAutoOpenClawProvider, resolveByokEnvOverrides, spawnOpenClawAgentTurn, stopDetachedProcess, } from "./helpers/openclaw-cli.js";
50
52
  import { fetchKickoffContextSafe, renderKickoffMessage } from "./helpers/kickoff-context.js";
51
53
  import { createDispatchLifecycle } from "./helpers/dispatch-lifecycle.js";
@@ -61,17 +63,23 @@ import { registerDebugRoutes } from "./routes/debug.js";
61
63
  import { registerEntityDynamicRoutes } from "./routes/entity-dynamic.js";
62
64
  import { registerEntitiesRoutes } from "./routes/entities.js";
63
65
  import { registerHealthRoutes } from "./routes/health.js";
66
+ import { registerChatRoutes } from "./routes/chat.js";
64
67
  import { registerLiveLegacyRoutes } from "./routes/live-legacy.js";
65
68
  import { registerLiveMiscRoutes } from "./routes/live-misc.js";
69
+ import { registerLiveTerminalRoutes } from "./routes/live-terminal.js";
66
70
  import { registerLiveSnapshotRoutes } from "./routes/live-snapshot.js";
67
71
  import { registerMissionControlActionsRoutes } from "./routes/mission-control-actions.js";
68
72
  import { registerMissionControlReadRoutes } from "./routes/mission-control-read.js";
69
73
  import { registerOnboardingRoutes } from "./routes/onboarding.js";
70
74
  import { registerRunControlRoutes } from "./routes/run-control.js";
71
75
  import { registerRuntimeHookRoutes } from "./routes/runtime-hooks.js";
76
+ import { registerSentinelsCatalogRoutes } from "./routes/sentinels-catalog.js";
72
77
  import { registerSettingsByokRoutes } from "./routes/settings-byok.js";
73
78
  import { registerSummaryRoutes } from "./routes/summary.js";
79
+ import { registerUsageRoutes } from "./routes/usage.js";
74
80
  import { registerWorkArtifactsRoutes } from "./routes/work-artifacts.js";
81
+ import { registerLiveTriageRoutes } from "./routes/live-triage.js";
82
+ import { registerRealtimeOrchestratorRoutes } from "./routes/realtime-orchestrator.js";
75
83
  // =============================================================================
76
84
  // Helpers
77
85
  // =============================================================================
@@ -96,10 +104,23 @@ async function resolveSkillPackOverrides(input) {
96
104
  }
97
105
  }
98
106
  function safeErrorMessage(err) {
99
- if (err instanceof Error)
100
- return err.message;
101
- if (typeof err === "string")
102
- return err;
107
+ const raw = err instanceof Error ? err.message : typeof err === "string" ? err : "";
108
+ const normalized = raw.trim().toLowerCase();
109
+ if (normalized.length > 0) {
110
+ if (normalized.includes("signal is aborted") ||
111
+ normalized.includes("aborterror") ||
112
+ normalized.includes("request cancelled") ||
113
+ normalized.includes("request canceled")) {
114
+ return "request timed out before upstream completed";
115
+ }
116
+ if (normalized.includes("timed out") || normalized.includes("timeout")) {
117
+ return "request timed out before upstream completed";
118
+ }
119
+ if (normalized.includes("failed to fetch") || normalized.includes("network")) {
120
+ return "network request failed";
121
+ }
122
+ return raw;
123
+ }
103
124
  return "Unexpected error";
104
125
  }
105
126
  function titleCaseFromSlug(value) {
@@ -181,17 +202,28 @@ async function mapWithConcurrency(items, concurrency, mapper) {
181
202
  }
182
203
  const ACTIVITY_WARM_THROTTLE_MS = 30_000;
183
204
  const activityWarmByKey = new Map();
184
- const SNAPSHOT_RESPONSE_CACHE_TTL_MS = 1_500;
205
+ const SNAPSHOT_RESPONSE_CACHE_TTL_MS = 800;
185
206
  const SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES = 16;
186
207
  const SNAPSHOT_ACTIVITY_PERSIST_MIN_INTERVAL_MS = 15_000;
187
208
  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 });
209
+ const NEXT_UP_QUEUE_CACHE_TTL_MS = readPositiveIntEnv("ORGX_NEXT_UP_QUEUE_CACHE_TTL_MS", 30_000, { min: 250, max: 120_000 });
189
210
  const NEXT_UP_QUEUE_STALE_TTL_MS = readPositiveIntEnv("ORGX_NEXT_UP_QUEUE_STALE_TTL_MS", 45_000, { min: 1_000, max: 600_000 });
190
211
  const NEXT_UP_GRAPH_CONCURRENCY = readPositiveIntEnv("ORGX_NEXT_UP_GRAPH_CONCURRENCY", 20, { min: 1, max: 32 });
191
212
  const NEXT_UP_LIVE_AGENTS_TIMEOUT_MS = readPositiveIntEnv("ORGX_NEXT_UP_LIVE_AGENTS_TIMEOUT_MS", 1_500, { min: 200, max: 20_000 });
192
213
  const NEXT_UP_AGENT_CATALOG_TIMEOUT_MS = readPositiveIntEnv("ORGX_NEXT_UP_AGENT_CATALOG_TIMEOUT_MS", 900, { min: 100, max: 20_000 });
214
+ const NEXT_UP_LIVE_SESSIONS_TIMEOUT_MS = readPositiveIntEnv("ORGX_NEXT_UP_LIVE_SESSIONS_TIMEOUT_MS", 2_500, { min: 250, max: 30_000 });
215
+ const PROJECT_SCOPE_LOOKUP_TIMEOUT_MS = readPositiveIntEnv("ORGX_PROJECT_SCOPE_LOOKUP_TIMEOUT_MS", 2_500, { min: 250, max: 20_000 });
216
+ const PROJECT_SCOPE_MAX_INITIATIVE_PAGES = readPositiveIntEnv("ORGX_PROJECT_SCOPE_MAX_INITIATIVE_PAGES", 12, { min: 1, max: 100 });
217
+ const LIVE_WORKSPACE_INITIATIVE_STATUSES = [
218
+ "active",
219
+ "planning",
220
+ "paused",
221
+ "draft",
222
+ "in_progress",
223
+ ];
193
224
  let lastSnapshotActivityPersistAt = 0;
194
225
  let lastSnapshotActivityFingerprint = "";
226
+ let snapshotCacheGeneration = 0;
195
227
  const snapshotResponseCache = new Map();
196
228
  const ACTIVITY_DECISION_EVENT_HINTS = new Set([
197
229
  "decision_buffered",
@@ -284,6 +316,9 @@ function deriveStructuredActivityBucket(input) {
284
316
  "nonBlockingDecisionCount",
285
317
  ]) ?? 0;
286
318
  if (event === "autopilot_slice_result") {
319
+ // Any blocked slice result needs decision-first surfacing in the Activity UX.
320
+ if (input.phase === "blocked")
321
+ return "decision";
287
322
  if (decisionRequired || blockingDecisions > 0)
288
323
  return "decision";
289
324
  if (artifacts > 0)
@@ -292,6 +327,13 @@ function deriveStructuredActivityBucket(input) {
292
327
  return "decision";
293
328
  return "message";
294
329
  }
330
+ if (event === "auto_continue_stopped") {
331
+ const stopReason = typeof metadata?.stop_reason === "string"
332
+ ? metadata.stop_reason.trim().toLowerCase()
333
+ : "";
334
+ if (stopReason === "blocked" || stopReason === "error")
335
+ return "decision";
336
+ }
295
337
  if (event && ACTIVITY_ARTIFACT_EVENT_HINTS.has(event))
296
338
  return "artifact";
297
339
  if (event && ACTIVITY_DECISION_EVENT_HINTS.has(event))
@@ -319,7 +361,7 @@ function readSnapshotResponseCache(key) {
319
361
  const entry = snapshotResponseCache.get(key);
320
362
  if (!entry)
321
363
  return null;
322
- if (entry.expiresAt <= Date.now()) {
364
+ if (entry.generation !== snapshotCacheGeneration || entry.expiresAt <= Date.now()) {
323
365
  snapshotResponseCache.delete(key);
324
366
  return null;
325
367
  }
@@ -329,6 +371,7 @@ function writeSnapshotResponseCache(key, payload) {
329
371
  const now = Date.now();
330
372
  snapshotResponseCache.set(key, {
331
373
  expiresAt: now + SNAPSHOT_RESPONSE_CACHE_TTL_MS,
374
+ generation: snapshotCacheGeneration,
332
375
  payload,
333
376
  });
334
377
  if (snapshotResponseCache.size <= SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES)
@@ -345,6 +388,7 @@ function writeSnapshotResponseCache(key, payload) {
345
388
  }
346
389
  }
347
390
  function clearSnapshotResponseCache() {
391
+ snapshotCacheGeneration += 1;
348
392
  snapshotResponseCache.clear();
349
393
  }
350
394
  function isUserScopedApiKey(apiKey) {
@@ -528,19 +572,29 @@ function applyAgentContextsToActivity(input, contexts) {
528
572
  return enrichActivityActorFields(nextItem);
529
573
  });
530
574
  }
575
+ function sessionNodeEpoch(node) {
576
+ const raw = node.updatedAt ?? node.lastEventAt ?? node.startedAt;
577
+ if (!raw)
578
+ return 0;
579
+ const epoch = Date.parse(raw);
580
+ return Number.isFinite(epoch) ? epoch : 0;
581
+ }
531
582
  function mergeSessionTrees(base, extra) {
532
- const seenNodes = new Set();
533
- const nodes = [];
534
- for (const node of base.nodes ?? []) {
535
- seenNodes.add(node.id);
536
- nodes.push(node);
537
- }
583
+ const nodeById = new Map();
584
+ for (const node of base.nodes ?? [])
585
+ nodeById.set(node.id, node);
538
586
  for (const node of extra.nodes ?? []) {
539
- if (seenNodes.has(node.id))
587
+ const existing = nodeById.get(node.id);
588
+ if (!existing) {
589
+ nodeById.set(node.id, node);
540
590
  continue;
541
- seenNodes.add(node.id);
542
- nodes.push(node);
591
+ }
592
+ const existingEpoch = sessionNodeEpoch(existing);
593
+ const extraEpoch = sessionNodeEpoch(node);
594
+ if (extraEpoch > existingEpoch)
595
+ nodeById.set(node.id, node);
543
596
  }
597
+ const nodes = Array.from(nodeById.values());
544
598
  const seenEdges = new Set();
545
599
  const edges = [];
546
600
  for (const edge of base.edges ?? []) {
@@ -576,6 +630,81 @@ function mergeSessionTrees(base, extra) {
576
630
  groups: Array.from(groupsById.values()),
577
631
  };
578
632
  }
633
+ function asActivityMetadataRecord(value) {
634
+ if (!value || typeof value !== "object" || Array.isArray(value))
635
+ return null;
636
+ return value;
637
+ }
638
+ function activityMetadataStr(metadata, keys) {
639
+ if (!metadata)
640
+ return null;
641
+ for (const key of keys) {
642
+ const value = metadata[key];
643
+ if (typeof value !== "string")
644
+ continue;
645
+ const normalized = value.trim();
646
+ if (normalized.length > 0)
647
+ return normalized;
648
+ }
649
+ return null;
650
+ }
651
+ const SEMANTIC_ACTIVITY_EVENTS = new Set([
652
+ "autopilot_slice_result",
653
+ "auto_continue_started",
654
+ "auto_continue_stopped",
655
+ "next_up_manual_dispatch_started",
656
+ "autopilot_slice_mcp_handshake_failed",
657
+ "autopilot_slice_timeout",
658
+ "autopilot_slice_log_stall",
659
+ "auto_continue_spawn_guard_blocked",
660
+ "auto_continue_spawn_guard_rate_limited",
661
+ "autopilot_autofix_scheduled",
662
+ "autopilot_autofix_executed",
663
+ "autopilot_autofix_skipped",
664
+ ]);
665
+ function semanticActivityKey(item) {
666
+ const metadata = asActivityMetadataRecord(item.metadata);
667
+ const eventRaw = metadata?.event;
668
+ const event = typeof eventRaw === "string" ? eventRaw.trim().toLowerCase() : "";
669
+ if (!event || !SEMANTIC_ACTIVITY_EVENTS.has(event))
670
+ return null;
671
+ const runLike = (typeof item.runId === "string" && item.runId.trim().length > 0
672
+ ? item.runId.trim()
673
+ : null) ??
674
+ activityMetadataStr(metadata, [
675
+ "run_id",
676
+ "runId",
677
+ "slice_run_id",
678
+ "sliceRunId",
679
+ "active_run_id",
680
+ "activeRunId",
681
+ "last_run_id",
682
+ "lastRunId",
683
+ ]);
684
+ const correlationId = activityMetadataStr(metadata, ["correlation_id", "correlationId"]);
685
+ const initiativeId = (typeof item.initiativeId === "string" && item.initiativeId.trim().length > 0
686
+ ? item.initiativeId.trim()
687
+ : null) ??
688
+ activityMetadataStr(metadata, ["initiative_id", "initiativeId"]);
689
+ const workstreamId = activityMetadataStr(metadata, ["workstream_id", "workstreamId"]);
690
+ const taskId = activityMetadataStr(metadata, ["task_id", "taskId"]);
691
+ const stopReason = activityMetadataStr(metadata, ["stop_reason", "stopReason"]);
692
+ const parsedStatus = activityMetadataStr(metadata, ["parsed_status", "parsedStatus"]);
693
+ const title = (item.title ?? "").trim().toLowerCase();
694
+ if (!runLike && !correlationId && !workstreamId && !taskId)
695
+ return null;
696
+ return [
697
+ event,
698
+ initiativeId ?? "",
699
+ workstreamId ?? "",
700
+ taskId ?? "",
701
+ runLike ?? "",
702
+ correlationId ?? "",
703
+ stopReason ?? "",
704
+ parsedStatus ?? "",
705
+ title,
706
+ ].join("|");
707
+ }
579
708
  function mergeActivities(base, extra, limit) {
580
709
  const merged = [...(base ?? []), ...(extra ?? [])].sort((a, b) => {
581
710
  const timestampDelta = Date.parse(b.timestamp) - Date.parse(a.timestamp);
@@ -584,11 +713,17 @@ function mergeActivities(base, extra, limit) {
584
713
  return b.id.localeCompare(a.id);
585
714
  });
586
715
  const deduped = [];
587
- const seen = new Set();
716
+ const seenIds = new Set();
717
+ const seenSemantic = new Set();
588
718
  for (const item of merged) {
589
- if (seen.has(item.id))
719
+ if (seenIds.has(item.id))
720
+ continue;
721
+ seenIds.add(item.id);
722
+ const sk = semanticActivityKey(item);
723
+ if (sk && seenSemantic.has(sk))
590
724
  continue;
591
- seen.add(item.id);
725
+ if (sk)
726
+ seenSemantic.add(sk);
592
727
  deduped.push(item);
593
728
  if (deduped.length >= limit)
594
729
  break;
@@ -719,6 +854,9 @@ function enrichSessionsWithRuntime(input, instances) {
719
854
  const agentId = (node.agentId ?? "").trim() || fallbackAgent.agentId;
720
855
  const agentName = (node.agentName ?? "").trim() || fallbackAgent.agentName;
721
856
  const nodeStatus = (node.status ?? "").trim().toLowerCase();
857
+ const isTerminalNodeStatus = nodeStatus === "completed" ||
858
+ nodeStatus === "cancelled" ||
859
+ nodeStatus === "archived";
722
860
  const isLiveLikeNodeStatus = nodeStatus === "running" ||
723
861
  nodeStatus === "active" ||
724
862
  nodeStatus === "in_progress" ||
@@ -726,20 +864,49 @@ function enrichSessionsWithRuntime(input, instances) {
726
864
  nodeStatus === "planning" ||
727
865
  nodeStatus === "dispatching";
728
866
  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
- : "");
867
+ const shouldPromoteStatusFromRuntime = runtimeStatus === "completed" ||
868
+ runtimeStatus === "blocked" ||
869
+ runtimeStatus === "review" ||
870
+ runtimeStatus === "handoff" ||
871
+ (runtimeStatus === "running" &&
872
+ (nodeStatus === "blocked" ||
873
+ nodeStatus === "failed" ||
874
+ nodeStatus === "queued" ||
875
+ nodeStatus === "paused"));
876
+ const nextStatus = shouldDowngradeStatusFromRuntime ||
877
+ (!isTerminalNodeStatus && shouldPromoteStatusFromRuntime)
878
+ ? runtimeStatus
879
+ : node.status;
880
+ const runtimeExplicitlyBlocked = runtimeStatus === "blocked" || match.phase?.toLowerCase() === "blocked";
881
+ const runtimeExplicitlyUnblocked = !runtimeExplicitlyBlocked && typeof match.phase === "string" && match.phase.trim().length > 0;
882
+ const runtimeBlockedReason = (match.lastMessage ?? "").trim();
883
+ const nextBlockerReason = runtimeExplicitlyBlocked
884
+ ? runtimeBlockedReason || (node.blockerReason ?? "").trim() || null
885
+ : runtimeExplicitlyUnblocked
886
+ ? null
887
+ : node.blockerReason ?? null;
888
+ const nextBlockers = runtimeExplicitlyBlocked
889
+ ? runtimeBlockedReason
890
+ ? [runtimeBlockedReason]
891
+ : Array.isArray(node.blockers)
892
+ ? node.blockers
893
+ : []
894
+ : runtimeExplicitlyUnblocked
895
+ ? []
896
+ : Array.isArray(node.blockers)
897
+ ? node.blockers
898
+ : [];
733
899
  return {
734
900
  ...node,
735
901
  agentId: agentId || null,
736
902
  agentName: agentName || null,
737
- status: shouldDowngradeStatusFromRuntime ? runtimeStatus : node.status,
903
+ status: nextStatus,
738
904
  state: node.state ?? match.state ?? null,
739
905
  lastEventSummary: shouldDowngradeStatusFromRuntime && runtimeStatus === "queued"
740
906
  ? node.lastEventSummary ?? "Recovered stale runtime; awaiting next dispatch."
741
907
  : node.lastEventSummary,
742
- blockerReason: blockerReason || node.blockerReason || null,
908
+ blockers: nextBlockers,
909
+ blockerReason: nextBlockerReason,
743
910
  runtimeClient: normalizeRuntimeSource(match.sourceClient),
744
911
  runtimeLabel: match.displayName,
745
912
  runtimeProvider: match.providerLogo,
@@ -749,6 +916,64 @@ function enrichSessionsWithRuntime(input, instances) {
749
916
  });
750
917
  return { ...input, nodes };
751
918
  }
919
+ function metadataHasStructuredScope(meta) {
920
+ const scalarScope = pickString(meta, [
921
+ "initiative_id",
922
+ "initiativeId",
923
+ "workstream_id",
924
+ "workstreamId",
925
+ "workstream_title",
926
+ "workstreamTitle",
927
+ "task_id",
928
+ "taskId",
929
+ "task_title",
930
+ "taskTitle",
931
+ "slice_run_id",
932
+ "sliceRunId",
933
+ "iwmt_id",
934
+ "iwmtId",
935
+ "milestone_id",
936
+ "milestoneId",
937
+ "milestone_title",
938
+ "milestoneTitle",
939
+ ]) ?? null;
940
+ if (scalarScope)
941
+ return true;
942
+ const listScopeKeys = [
943
+ "initiative_ids",
944
+ "initiativeIds",
945
+ "workstream_ids",
946
+ "workstreamIds",
947
+ "task_ids",
948
+ "taskIds",
949
+ "milestone_ids",
950
+ "milestoneIds",
951
+ "iwmt_ids",
952
+ "iwmtIds",
953
+ ];
954
+ for (const key of listScopeKeys) {
955
+ const value = meta[key];
956
+ if (!Array.isArray(value))
957
+ continue;
958
+ if (value.some((entry) => typeof entry === "string" && entry.trim().length > 0)) {
959
+ return true;
960
+ }
961
+ }
962
+ return false;
963
+ }
964
+ function shouldInjectRuntimeInstanceAsSession(instance, runId, meta) {
965
+ if (instance.state !== "active")
966
+ return false;
967
+ // Synthetic hook correlation ids are telemetry-only and should never render as user-facing sessions.
968
+ if (runId.toLowerCase().startsWith("hook-"))
969
+ return false;
970
+ const workstreamId = instance.workstreamId?.trim() ?? "";
971
+ const taskId = instance.taskId?.trim() ?? "";
972
+ if (workstreamId.length > 0 || taskId.length > 0)
973
+ return true;
974
+ // Keep only runtime records that include structured execution scope.
975
+ return metadataHasStructuredScope(meta);
976
+ }
752
977
  function injectRuntimeInstancesAsSessions(input, instances) {
753
978
  if (!Array.isArray(input.nodes))
754
979
  return input;
@@ -773,10 +998,6 @@ function injectRuntimeInstancesAsSessions(input, instances) {
773
998
  continue;
774
999
  if (existingRunIds.has(runId))
775
1000
  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
1001
  const initiativeId = instance.initiativeId?.trim() || null;
781
1002
  const workstreamId = instance.workstreamId?.trim() || null;
782
1003
  const runtimeClient = normalizeRuntimeSource(instance.sourceClient);
@@ -785,6 +1006,8 @@ function injectRuntimeInstancesAsSessions(input, instances) {
785
1006
  const meta = instance.metadata && typeof instance.metadata === "object"
786
1007
  ? instance.metadata
787
1008
  : {};
1009
+ if (!shouldInjectRuntimeInstanceAsSession(instance, runId, meta))
1010
+ continue;
788
1011
  const titleHint = pickString(meta, ["workstream_title", "workstreamTitle"]) ??
789
1012
  (workstreamId ? `Workstream ${workstreamId.slice(0, 8)}` : null);
790
1013
  const initiativeHint = pickString(meta, ["initiative_title", "initiativeTitle"]) ??
@@ -898,18 +1121,18 @@ const CONTENT_SECURITY_POLICY = [
898
1121
  "form-action 'self'",
899
1122
  "object-src 'none'",
900
1123
  "script-src 'self'",
901
- "style-src 'self' 'unsafe-inline'",
1124
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
902
1125
  "img-src 'self' data: blob:",
903
- "font-src 'self' data:",
1126
+ "font-src 'self' data: https://fonts.gstatic.com",
904
1127
  "media-src 'self'",
905
- "connect-src 'self' https://*.useorgx.com https://*.openclaw.ai http://127.0.0.1:* http://localhost:*",
1128
+ "connect-src 'self' https://*.useorgx.com https://*.openclaw.ai https://api.openai.com https://*.openai.com http://127.0.0.1:* http://localhost:*",
906
1129
  ].join("; ");
907
1130
  const SECURITY_HEADERS = {
908
1131
  "X-Content-Type-Options": "nosniff",
909
1132
  "X-Frame-Options": "DENY",
910
1133
  "Referrer-Policy": "same-origin",
911
1134
  "X-Robots-Tag": "noindex, nofollow, noarchive, nosnippet, noimageindex",
912
- "Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=(), midi=(), magnetometer=(), gyroscope=()",
1135
+ "Permissions-Policy": "camera=(), microphone=(self), geolocation=(), payment=(), usb=(), midi=(), magnetometer=(), gyroscope=()",
913
1136
  "Cross-Origin-Opener-Policy": "same-origin",
914
1137
  "Cross-Origin-Resource-Policy": "same-origin",
915
1138
  "Origin-Agent-Cluster": "?1",
@@ -982,9 +1205,16 @@ function resolveSafeDistPath(subPath) {
982
1205
  }
983
1206
  return candidate;
984
1207
  }
985
- // =============================================================================
986
- // Helpers
987
- // =============================================================================
1208
+ const PRECOMPRESSED_FILE_EXTENSIONS = new Set([
1209
+ ".css",
1210
+ ".html",
1211
+ ".js",
1212
+ ".json",
1213
+ ".map",
1214
+ ".svg",
1215
+ ".txt",
1216
+ ".xml",
1217
+ ]);
988
1218
  const IMMUTABLE_FILE_CACHE = new Map();
989
1219
  const IMMUTABLE_FILE_CACHE_MAX = 128;
990
1220
  const FILE_PREVIEW_MAX_BYTES = 1_000_000;
@@ -1064,23 +1294,109 @@ function readFilePreview(pathname, totalBytes) {
1064
1294
  closeSync(fd);
1065
1295
  }
1066
1296
  }
1067
- function sendFile(res, filePath, cacheControl) {
1297
+ function parseAcceptedEncodings(rawHeader) {
1298
+ const parsed = new Map();
1299
+ if (!rawHeader || rawHeader.trim().length === 0)
1300
+ return parsed;
1301
+ const parts = rawHeader.split(",");
1302
+ for (const part of parts) {
1303
+ const [nameRaw, ...params] = part.split(";");
1304
+ const name = nameRaw?.trim().toLowerCase();
1305
+ if (!name)
1306
+ continue;
1307
+ let q = 1;
1308
+ for (const param of params) {
1309
+ const [keyRaw, valueRaw] = param.split("=");
1310
+ const key = keyRaw?.trim().toLowerCase();
1311
+ if (key !== "q")
1312
+ continue;
1313
+ const candidate = Number.parseFloat((valueRaw ?? "").trim());
1314
+ if (Number.isFinite(candidate)) {
1315
+ q = candidate;
1316
+ }
1317
+ }
1318
+ if (q <= 0)
1319
+ continue;
1320
+ const existing = parsed.get(name);
1321
+ if (existing == null || q > existing) {
1322
+ parsed.set(name, q);
1323
+ }
1324
+ }
1325
+ return parsed;
1326
+ }
1327
+ function resolveEncodingQuality(accepted, encoding) {
1328
+ if (accepted.has(encoding))
1329
+ return accepted.get(encoding) ?? 0;
1330
+ if (accepted.has("*"))
1331
+ return accepted.get("*") ?? 0;
1332
+ return 0;
1333
+ }
1334
+ function resolvePrecompressedVariant(req, filePath) {
1335
+ const ext = extname(filePath).toLowerCase();
1336
+ if (!PRECOMPRESSED_FILE_EXTENSIONS.has(ext))
1337
+ return null;
1338
+ const accepted = parseAcceptedEncodings(pickHeaderString(req.headers, ["accept-encoding"]));
1339
+ if (accepted.size === 0)
1340
+ return null;
1341
+ const candidates = [
1342
+ {
1343
+ encoding: "br",
1344
+ path: `${filePath}.br`,
1345
+ quality: resolveEncodingQuality(accepted, "br"),
1346
+ priority: 2,
1347
+ },
1348
+ {
1349
+ encoding: "gzip",
1350
+ path: `${filePath}.gz`,
1351
+ quality: resolveEncodingQuality(accepted, "gzip"),
1352
+ priority: 1,
1353
+ },
1354
+ ];
1355
+ candidates.sort((left, right) => {
1356
+ if (right.quality !== left.quality)
1357
+ return right.quality - left.quality;
1358
+ return right.priority - left.priority;
1359
+ });
1360
+ for (const candidate of candidates) {
1361
+ if (candidate.quality <= 0)
1362
+ continue;
1363
+ if (existsSync(candidate.path)) {
1364
+ return { path: candidate.path, encoding: candidate.encoding };
1365
+ }
1366
+ }
1367
+ return null;
1368
+ }
1369
+ function sendFile(req, res, filePath, cacheControl) {
1068
1370
  try {
1069
1371
  const shouldCacheImmutable = cacheControl.includes("immutable");
1372
+ const shouldVaryByEncoding = PRECOMPRESSED_FILE_EXTENSIONS.has(extname(filePath).toLowerCase());
1373
+ const precompressed = resolvePrecompressedVariant(req, filePath);
1374
+ const responsePath = precompressed?.path ?? filePath;
1375
+ const responseEncoding = precompressed?.encoding ?? null;
1376
+ const cacheKey = `${responsePath}|${cacheControl}`;
1070
1377
  if (shouldCacheImmutable) {
1071
- const cached = IMMUTABLE_FILE_CACHE.get(filePath);
1378
+ const cached = IMMUTABLE_FILE_CACHE.get(cacheKey);
1072
1379
  if (cached) {
1073
- res.writeHead(200, {
1380
+ const headers = {
1074
1381
  "Content-Type": cached.contentType,
1075
1382
  "Cache-Control": cacheControl,
1076
1383
  ...SECURITY_HEADERS,
1077
1384
  ...CORS_HEADERS,
1385
+ };
1386
+ if (cached.contentEncoding === "br")
1387
+ headers["Content-Encoding"] = "br";
1388
+ if (cached.contentEncoding === "gzip")
1389
+ headers["Content-Encoding"] = "gzip";
1390
+ if (cached.varyAcceptEncoding)
1391
+ headers["Vary"] = "Accept-Encoding";
1392
+ res.writeHead(200, {
1393
+ ...headers,
1078
1394
  });
1079
1395
  res.end(cached.content);
1080
1396
  return;
1081
1397
  }
1082
1398
  }
1083
- const content = readFileSync(filePath);
1399
+ const content = readFileSync(responsePath);
1084
1400
  const type = contentType(filePath);
1085
1401
  if (shouldCacheImmutable) {
1086
1402
  if (IMMUTABLE_FILE_CACHE.size >= IMMUTABLE_FILE_CACHE_MAX) {
@@ -1088,14 +1404,26 @@ function sendFile(res, filePath, cacheControl) {
1088
1404
  if (firstKey)
1089
1405
  IMMUTABLE_FILE_CACHE.delete(firstKey);
1090
1406
  }
1091
- IMMUTABLE_FILE_CACHE.set(filePath, { content, contentType: type });
1407
+ IMMUTABLE_FILE_CACHE.set(cacheKey, {
1408
+ content,
1409
+ contentType: type,
1410
+ contentEncoding: responseEncoding,
1411
+ varyAcceptEncoding: shouldVaryByEncoding,
1412
+ });
1092
1413
  }
1093
- res.writeHead(200, {
1414
+ const headers = {
1094
1415
  "Content-Type": type,
1095
1416
  "Cache-Control": cacheControl,
1096
1417
  ...SECURITY_HEADERS,
1097
1418
  ...CORS_HEADERS,
1098
- });
1419
+ };
1420
+ if (responseEncoding === "br")
1421
+ headers["Content-Encoding"] = "br";
1422
+ if (responseEncoding === "gzip")
1423
+ headers["Content-Encoding"] = "gzip";
1424
+ if (shouldVaryByEncoding)
1425
+ headers["Vary"] = "Accept-Encoding";
1426
+ res.writeHead(200, headers);
1099
1427
  res.end(content);
1100
1428
  }
1101
1429
  catch {
@@ -1110,10 +1438,24 @@ function send404(res) {
1110
1438
  });
1111
1439
  res.end("Not Found");
1112
1440
  }
1113
- function sendIndexHtml(res) {
1441
+ function sendStaleChunkRecovery(res) {
1442
+ const body = [
1443
+ "// Recover from stale chunk references after dashboard/plugin upgrades.",
1444
+ "window.location.replace('/orgx/live' + window.location.search);",
1445
+ "export {};"
1446
+ ].join("\n");
1447
+ res.writeHead(200, {
1448
+ "Content-Type": "application/javascript; charset=utf-8",
1449
+ "Cache-Control": "no-cache, no-store, must-revalidate",
1450
+ ...SECURITY_HEADERS,
1451
+ ...CORS_HEADERS,
1452
+ });
1453
+ res.end(body);
1454
+ }
1455
+ function sendIndexHtml(req, res) {
1114
1456
  const indexPath = join(DIST_DIR, "index.html");
1115
1457
  if (existsSync(indexPath)) {
1116
- sendFile(res, indexPath, "no-cache, no-store, must-revalidate");
1458
+ sendFile(req, res, indexPath, "no-cache, no-store, must-revalidate");
1117
1459
  }
1118
1460
  else {
1119
1461
  res.writeHead(503, {
@@ -1307,9 +1649,60 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1307
1649
  spawnAgentTurn,
1308
1650
  upsertAgentRun,
1309
1651
  });
1652
+ const normalizeRunnerAgentToken = (value) => {
1653
+ if (typeof value !== "string")
1654
+ return null;
1655
+ const trimmed = value.trim();
1656
+ if (!trimmed)
1657
+ return null;
1658
+ const normalized = trimmed.toLowerCase();
1659
+ if (normalized === "main" ||
1660
+ normalized === "undefined" ||
1661
+ normalized === "null" ||
1662
+ normalized === "n/a" ||
1663
+ normalized === "na") {
1664
+ return null;
1665
+ }
1666
+ return trimmed;
1667
+ };
1668
+ const pushRunnerAgent = (target, seen, input) => {
1669
+ const agentId = normalizeRunnerAgentToken(input.id ?? null);
1670
+ const agentName = normalizeRunnerAgentToken(input.name ?? null);
1671
+ if (!agentId && !agentName)
1672
+ return;
1673
+ const resolvedId = agentId ?? agentName;
1674
+ const dedupeKey = resolvedId.toLowerCase();
1675
+ if (seen.has(dedupeKey))
1676
+ return;
1677
+ seen.add(dedupeKey);
1678
+ target.push({
1679
+ id: resolvedId,
1680
+ name: agentName ?? resolvedId,
1681
+ });
1682
+ };
1683
+ const dedupeWithPrimary = (primary, extras) => {
1684
+ const merged = [];
1685
+ const seen = new Set();
1686
+ for (const candidate of [...primary, ...extras]) {
1687
+ const id = normalizeRunnerAgentToken(candidate.id);
1688
+ const name = normalizeRunnerAgentToken(candidate.name);
1689
+ if (!id && !name)
1690
+ continue;
1691
+ const resolvedId = id ?? name;
1692
+ const key = resolvedId.toLowerCase();
1693
+ if (seen.has(key))
1694
+ continue;
1695
+ seen.add(key);
1696
+ merged.push({
1697
+ id: resolvedId,
1698
+ name: name ?? resolvedId,
1699
+ });
1700
+ }
1701
+ return merged;
1702
+ };
1310
1703
  const codexBinResolver = createCodexBinResolver();
1311
1704
  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({
1705
+ 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
1706
  client,
1314
1707
  filename: __filename,
1315
1708
  safeErrorMessage,
@@ -1327,10 +1720,194 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1327
1720
  clearSnapshotResponseCache,
1328
1721
  resolveByokEnvOverrides,
1329
1722
  randomUUID,
1723
+ fetchKickoffContextSafe,
1724
+ renderKickoffMessage,
1330
1725
  });
1331
1726
  const nextUpQueueCache = new Map();
1332
1727
  const nextUpQueueInFlight = new Map();
1333
- const nextUpQueueCacheKeyFor = (initiativeId) => initiativeId?.trim() || "__all__";
1728
+ const PROJECT_INITIATIVE_IDS_CACHE_TTL_MS = 20_000;
1729
+ const projectInitiativeIdsCache = new Map();
1730
+ const commandCenterScopeCache = new Map();
1731
+ const nextUpQueueCacheKeyFor = (initiativeId, projectId) => {
1732
+ const normalizedInitiative = initiativeId?.trim() || "__all__";
1733
+ const normalizedProject = projectId?.trim() || "__all__";
1734
+ return `${normalizedProject}::${normalizedInitiative}`;
1735
+ };
1736
+ async function listInitiativeIdsForProject(input) {
1737
+ const projectId = input.projectId.trim();
1738
+ if (!projectId)
1739
+ return [];
1740
+ const cached = projectInitiativeIdsCache.get(projectId);
1741
+ if (cached && cached.expiresAt > Date.now()) {
1742
+ return [...cached.ids];
1743
+ }
1744
+ const mapInitiativeIds = (rows, opts) => {
1745
+ const projectScopeId = opts?.projectId?.trim() ?? "";
1746
+ const commandCenterId = opts?.commandCenterId?.trim() ?? "";
1747
+ return rows
1748
+ .map((entry) => {
1749
+ const record = entry;
1750
+ if (projectScopeId) {
1751
+ const rowProjectId = pickString(record, ["project_id", "projectId"]) ?? "";
1752
+ if (rowProjectId !== projectScopeId)
1753
+ return null;
1754
+ }
1755
+ if (commandCenterId) {
1756
+ const rowCommandCenterId = pickString(record, [
1757
+ "workspace_id",
1758
+ "workspaceId",
1759
+ "command_center_id",
1760
+ "commandCenterId",
1761
+ ]) ?? "";
1762
+ if (rowCommandCenterId !== commandCenterId)
1763
+ return null;
1764
+ }
1765
+ return pickString(record, ["id"]);
1766
+ })
1767
+ .filter((id) => Boolean(id && id.trim().length > 0));
1768
+ };
1769
+ const cacheAndReturn = (ids) => {
1770
+ const normalized = dedupeStrings(ids
1771
+ .map((id) => id.trim())
1772
+ .filter((id) => id.length > 0));
1773
+ projectInitiativeIdsCache.set(projectId, {
1774
+ expiresAt: Date.now() + PROJECT_INITIATIVE_IDS_CACHE_TTL_MS,
1775
+ ids: normalized,
1776
+ });
1777
+ return normalized;
1778
+ };
1779
+ const isKnownCommandCenterScope = async () => {
1780
+ const cachedScope = commandCenterScopeCache.get(projectId);
1781
+ if (cachedScope && cachedScope.expiresAt > Date.now()) {
1782
+ return cachedScope.exists;
1783
+ }
1784
+ const cacheScope = (exists) => {
1785
+ commandCenterScopeCache.set(projectId, {
1786
+ expiresAt: Date.now() + PROJECT_INITIATIVE_IDS_CACHE_TTL_MS,
1787
+ exists,
1788
+ });
1789
+ return exists;
1790
+ };
1791
+ const hasId = (rows) => rows.some((entry) => {
1792
+ const record = entry;
1793
+ const id = pickString(record, ["id"]) ?? "";
1794
+ return id === projectId;
1795
+ });
1796
+ try {
1797
+ const byId = await withSoftTimeout("command center scope lookup", PROJECT_SCOPE_LOOKUP_TIMEOUT_MS, client.listEntities("command_center", {
1798
+ id: projectId,
1799
+ limit: 1,
1800
+ }));
1801
+ const byIdRows = Array.isArray(byId.data) ? byId.data : [];
1802
+ if (hasId(byIdRows))
1803
+ return cacheScope(true);
1804
+ }
1805
+ catch {
1806
+ // continue to all-command-center fallback
1807
+ }
1808
+ try {
1809
+ const all = await withSoftTimeout("command center catalog lookup", PROJECT_SCOPE_LOOKUP_TIMEOUT_MS, client.listEntities("command_center", {
1810
+ limit: 100,
1811
+ }));
1812
+ const allRows = Array.isArray(all.data) ? all.data : [];
1813
+ return cacheScope(hasId(allRows));
1814
+ }
1815
+ catch {
1816
+ return cacheScope(false);
1817
+ }
1818
+ };
1819
+ const listInitiativesWithFilters = async (filters) => {
1820
+ const rows = [];
1821
+ const pageSize = 100;
1822
+ const seenIds = new Set();
1823
+ let offset = 0;
1824
+ let page = 0;
1825
+ while (page < PROJECT_SCOPE_MAX_INITIATIVE_PAGES) {
1826
+ const result = await withSoftTimeout("initiative scope lookup", PROJECT_SCOPE_LOOKUP_TIMEOUT_MS, client.listEntities("initiative", {
1827
+ ...filters,
1828
+ limit: pageSize,
1829
+ offset,
1830
+ }));
1831
+ const pageRows = Array.isArray(result.data) ? result.data : [];
1832
+ let addedCount = 0;
1833
+ for (const entry of pageRows) {
1834
+ const record = entry;
1835
+ const id = pickString(record, ["id"]);
1836
+ if (id && seenIds.has(id))
1837
+ continue;
1838
+ if (id)
1839
+ seenIds.add(id);
1840
+ rows.push(entry);
1841
+ addedCount += 1;
1842
+ }
1843
+ const hasMoreFlag = Boolean(result.pagination?.has_more);
1844
+ const likelyMore = hasMoreFlag || pageRows.length >= pageSize;
1845
+ if (!likelyMore)
1846
+ break;
1847
+ if (addedCount === 0)
1848
+ break;
1849
+ offset += pageRows.length;
1850
+ page += 1;
1851
+ }
1852
+ return rows;
1853
+ };
1854
+ const listLiveInitiativesWithFilters = async (filters) => {
1855
+ // Fast path: request once without status fan-out. Upstream often returns
1856
+ // all relevant rows and this avoids 5x paginated round-trips.
1857
+ const broadRows = await listInitiativesWithFilters(filters);
1858
+ if (broadRows.length > 0)
1859
+ return broadRows;
1860
+ // Backward-compat fallback for upstreams that require explicit status.
1861
+ const rows = [];
1862
+ for (const status of LIVE_WORKSPACE_INITIATIVE_STATUSES) {
1863
+ const statusRows = await listInitiativesWithFilters({
1864
+ ...filters,
1865
+ status,
1866
+ });
1867
+ rows.push(...statusRows);
1868
+ }
1869
+ return rows;
1870
+ };
1871
+ try {
1872
+ // Workspace selection in the plugin uses command-center IDs.
1873
+ // Resolve that scope first so broad project queries never leak cross-workspace items.
1874
+ const byCommandCenterIds = mapInitiativeIds(await listLiveInitiativesWithFilters({
1875
+ workspace_id: projectId,
1876
+ command_center_id: projectId,
1877
+ }), { commandCenterId: projectId });
1878
+ if (byCommandCenterIds.length > 0)
1879
+ return cacheAndReturn(byCommandCenterIds);
1880
+ }
1881
+ catch {
1882
+ // continue to project-id fallback
1883
+ }
1884
+ try {
1885
+ // Do not hard-return empty for known command-center scopes here.
1886
+ // Some tenants only populate project_id links, so we continue through
1887
+ // project-id fallbacks before concluding the scope is empty.
1888
+ await isKnownCommandCenterScope();
1889
+ }
1890
+ catch {
1891
+ // continue to project-id fallback
1892
+ }
1893
+ try {
1894
+ const byWorkspaceFallback = mapInitiativeIds(await listLiveInitiativesWithFilters({
1895
+ workspace_id: projectId,
1896
+ command_center_id: projectId,
1897
+ }), { projectId });
1898
+ if (byWorkspaceFallback.length > 0)
1899
+ return cacheAndReturn(byWorkspaceFallback);
1900
+ }
1901
+ catch {
1902
+ // continue to empty fallback
1903
+ }
1904
+ try {
1905
+ return cacheAndReturn([]);
1906
+ }
1907
+ catch {
1908
+ return [];
1909
+ }
1910
+ }
1334
1911
  const readNextUpQueueCache = (key, opts) => {
1335
1912
  const entry = nextUpQueueCache.get(key);
1336
1913
  if (!entry)
@@ -1361,9 +1938,37 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1361
1938
  },
1362
1939
  });
1363
1940
  };
1941
+ const clearNextUpQueueCache = (initiativeId) => {
1942
+ const normalized = initiativeId?.trim() || null;
1943
+ if (!normalized) {
1944
+ nextUpQueueCache.clear();
1945
+ nextUpQueueInFlight.clear();
1946
+ return;
1947
+ }
1948
+ for (const key of Array.from(nextUpQueueCache.keys())) {
1949
+ if (key.endsWith(`::${normalized}`) || key.endsWith("::__all__")) {
1950
+ nextUpQueueCache.delete(key);
1951
+ }
1952
+ }
1953
+ for (const key of Array.from(nextUpQueueInFlight.keys())) {
1954
+ if (key.endsWith(`::${normalized}`) || key.endsWith("::__all__")) {
1955
+ nextUpQueueInFlight.delete(key);
1956
+ }
1957
+ }
1958
+ };
1364
1959
  async function buildNextUpQueueUncached(input) {
1365
1960
  const degraded = [];
1366
1961
  const requestedInitiativeId = input?.initiativeId?.trim() || null;
1962
+ const requestedProjectId = input?.projectId?.trim() || null;
1963
+ let allowedInitiativeIds = null;
1964
+ if (requestedProjectId && requestedProjectId.length > 0) {
1965
+ const scopedIds = await listInitiativeIdsForProject({
1966
+ projectId: requestedProjectId,
1967
+ });
1968
+ if (scopedIds.length > 0) {
1969
+ allowedInitiativeIds = new Set(scopedIds);
1970
+ }
1971
+ }
1367
1972
  const pinnedQueue = readNextUpQueuePins();
1368
1973
  const pinnedRankByKey = new Map();
1369
1974
  const pinnedByKey = new Map();
@@ -1377,6 +1982,17 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1377
1982
  preferredMilestoneId: pin.preferredMilestoneId ?? null,
1378
1983
  });
1379
1984
  }
1985
+ const suppressedKeySet = new Set();
1986
+ for (const suppression of pinnedQueue.suppressions ?? []) {
1987
+ const initiativeId = suppression.initiativeId?.trim();
1988
+ const workstreamId = suppression.workstreamId?.trim();
1989
+ if (!initiativeId || !workstreamId)
1990
+ continue;
1991
+ if (requestedInitiativeId && initiativeId !== requestedInitiativeId)
1992
+ continue;
1993
+ suppressedKeySet.add(`${initiativeId}:${workstreamId}`);
1994
+ }
1995
+ const isSuppressed = (initiativeId, workstreamId) => suppressedKeySet.has(`${initiativeId}:${workstreamId}`);
1380
1996
  const initiativeTitleById = new Map();
1381
1997
  const initiativeStatusById = new Map();
1382
1998
  const initiativePriorityById = new Map();
@@ -1388,7 +2004,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1388
2004
  initiativeTitleById.set(id, initiative.title);
1389
2005
  initiativeStatusById.set(id, initiative.status || "active");
1390
2006
  }
1391
- const initiativeResult = await listEntitiesSafe(client, "initiative", { limit: 500 });
2007
+ const initiativeResult = await withSoftTimeout("initiative list", PROJECT_SCOPE_LOOKUP_TIMEOUT_MS, listEntitiesSafe(client, "initiative", { limit: 500 })).catch((err) => ({
2008
+ items: [],
2009
+ warning: `initiative unavailable (${safeErrorMessage(err)})`,
2010
+ }));
1392
2011
  if (initiativeResult.warning)
1393
2012
  degraded.push(initiativeResult.warning);
1394
2013
  const initiatives = initiativeResult.items;
@@ -1407,6 +2026,39 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1407
2026
  if (priority)
1408
2027
  initiativePriorityById.set(id, priority);
1409
2028
  }
2029
+ const initiativeMatchesRequestedProject = (record) => {
2030
+ if (!requestedProjectId)
2031
+ return true;
2032
+ const scopedValue = pickString(record, [
2033
+ "workspace_id",
2034
+ "workspaceId",
2035
+ "command_center_id",
2036
+ "commandCenterId",
2037
+ "project_id",
2038
+ "projectId",
2039
+ ]) ?? null;
2040
+ if (!scopedValue)
2041
+ return false;
2042
+ return scopedValue === requestedProjectId;
2043
+ };
2044
+ if (requestedProjectId && !allowedInitiativeIds) {
2045
+ const metadataScopedIds = initiatives
2046
+ .map((entity) => {
2047
+ const record = entity;
2048
+ const id = pickString(record, ["id"]);
2049
+ if (!id)
2050
+ return null;
2051
+ return initiativeMatchesRequestedProject(record) ? id : null;
2052
+ })
2053
+ .filter((value) => Boolean(value));
2054
+ if (metadataScopedIds.length > 0) {
2055
+ allowedInitiativeIds = new Set(metadataScopedIds);
2056
+ degraded.push("workspace initiative scope lookup returned no rows; using metadata scoped initiatives.");
2057
+ }
2058
+ else {
2059
+ degraded.push("workspace initiative scope lookup returned no rows; local queue may be incomplete.");
2060
+ }
2061
+ }
1410
2062
  for (const [initiativeId, override] of localInitiativeStatusOverrides.entries()) {
1411
2063
  initiativeStatusById.set(initiativeId, override.status);
1412
2064
  }
@@ -1466,10 +2118,11 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1466
2118
  const buildSessionFallbackQueue = async () => {
1467
2119
  let sessionTree = null;
1468
2120
  try {
1469
- sessionTree = await client.getLiveSessions({
2121
+ sessionTree = await withSoftTimeout("live sessions", NEXT_UP_LIVE_SESSIONS_TIMEOUT_MS, client.getLiveSessions({
1470
2122
  initiative: requestedInitiativeId,
2123
+ projectId: requestedProjectId,
1471
2124
  limit: 500,
1472
- });
2125
+ }));
1473
2126
  }
1474
2127
  catch (err) {
1475
2128
  degraded.push(`live sessions fallback unavailable (${safeErrorMessage(err)})`);
@@ -1501,6 +2154,8 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1501
2154
  continue;
1502
2155
  if (requestedInitiativeId && initiativeId !== requestedInitiativeId)
1503
2156
  continue;
2157
+ if (allowedInitiativeIds && !allowedInitiativeIds.has(initiativeId))
2158
+ continue;
1504
2159
  const initiativeStatus = initiativeStatusById.get(initiativeId) ?? "active";
1505
2160
  if (!isInitiativeActiveStatus(initiativeStatus))
1506
2161
  continue;
@@ -1508,6 +2163,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1508
2163
  const epoch = parseEpoch(node.updatedAt ?? node.lastEventAt ?? node.startedAt);
1509
2164
  const existing = grouped.get(key);
1510
2165
  if (!existing) {
2166
+ const runnerAgents = [];
2167
+ const runnerAgentSeen = new Set();
2168
+ pushRunnerAgent(runnerAgents, runnerAgentSeen, {
2169
+ id: node.agentId,
2170
+ name: node.agentName,
2171
+ });
1511
2172
  grouped.set(key, {
1512
2173
  initiativeId,
1513
2174
  workstreamId,
@@ -1518,6 +2179,8 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1518
2179
  workstreamTitle: `Workstream ${workstreamId.slice(0, 8)}`,
1519
2180
  statuses: new Set([node.status]),
1520
2181
  blockers: Array.isArray(node.blockers) ? [...node.blockers] : [],
2182
+ runnerAgents,
2183
+ runnerAgentSeen,
1521
2184
  latest: node,
1522
2185
  latestEpoch: epoch,
1523
2186
  });
@@ -1532,6 +2195,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1532
2195
  existing.blockers.push(blocker);
1533
2196
  }
1534
2197
  }
2198
+ pushRunnerAgent(existing.runnerAgents, existing.runnerAgentSeen, {
2199
+ id: node.agentId,
2200
+ name: node.agentName,
2201
+ });
1535
2202
  if (epoch >= existing.latestEpoch) {
1536
2203
  existing.latest = node;
1537
2204
  existing.latestEpoch = epoch;
@@ -1551,11 +2218,22 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1551
2218
  : hasQueued
1552
2219
  ? "queued"
1553
2220
  : "idle";
1554
- const runnerAgentId = (entry.latest.agentId ?? "").trim() || "main";
1555
- const runnerAgentName = (entry.latest.agentName ?? "").trim() ||
1556
- initiativeTitleById.get(`agent:${runnerAgentId}`) ||
1557
- runnerAgentId;
2221
+ const latestRunner = [];
2222
+ const latestRunnerSeen = new Set();
2223
+ pushRunnerAgent(latestRunner, latestRunnerSeen, {
2224
+ id: entry.latest.agentId,
2225
+ name: entry.latest.agentName,
2226
+ });
2227
+ const runnerAgents = latestRunner.length > 0
2228
+ ? dedupeWithPrimary(latestRunner, entry.runnerAgents)
2229
+ : [...entry.runnerAgents];
2230
+ const primaryRunner = runnerAgents[0] ?? null;
2231
+ const runnerAgentId = primaryRunner?.id ?? "unassigned";
2232
+ const runnerAgentName = primaryRunner?.name ?? "Unassigned";
1558
2233
  const pinKey = `${entry.initiativeId}:${entry.workstreamId}`;
2234
+ if (isSuppressed(entry.initiativeId, entry.workstreamId) && queueState !== "running") {
2235
+ continue;
2236
+ }
1559
2237
  fallbackItems.push({
1560
2238
  initiativeId: entry.initiativeId,
1561
2239
  initiativeTitle: entry.initiativeTitle,
@@ -1571,10 +2249,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1571
2249
  nextTaskDueAt: null,
1572
2250
  runnerAgentId,
1573
2251
  runnerAgentName,
2252
+ runnerAgents,
1574
2253
  runnerSource: "fallback",
1575
2254
  queueState,
1576
2255
  blockReason: hasBlocked
1577
- ? entry.blockers[0] ?? (statusValues.includes("failed") ? "Latest run failed" : "Workstream blocked")
2256
+ ? entry.blockers[0] ??
2257
+ (statusValues.includes("failed") ? "Latest run failed" : "Workstream blocked")
1578
2258
  : null,
1579
2259
  isPinned: pinnedRankByKey.has(pinKey),
1580
2260
  pinnedRank: pinnedRankByKey.get(pinKey) ?? null,
@@ -1591,6 +2271,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1591
2271
  return false;
1592
2272
  if (requestedInitiativeId && id !== requestedInitiativeId)
1593
2273
  return false;
2274
+ if (!initiativeMatchesRequestedProject(record))
2275
+ return false;
2276
+ if (allowedInitiativeIds && !allowedInitiativeIds.has(id))
2277
+ return false;
1594
2278
  const status = pickString(record, ["status"]);
1595
2279
  return isInitiativeActiveStatus(status);
1596
2280
  });
@@ -1616,6 +2300,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1616
2300
  try {
1617
2301
  const data = await withSoftTimeout("live agents", NEXT_UP_LIVE_AGENTS_TIMEOUT_MS, client.getLiveAgents({
1618
2302
  initiative: requestedInitiativeId,
2303
+ projectId: requestedProjectId,
1619
2304
  includeIdle: true,
1620
2305
  }));
1621
2306
  for (const raw of Array.isArray(data.agents) ? data.agents : []) {
@@ -1673,13 +2358,50 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1673
2358
  return (milestone?.status?.toLowerCase() === "blocked" ||
1674
2359
  workstream?.status?.toLowerCase() === "blocked");
1675
2360
  };
2361
+ const normalizeSliceScope = (value) => {
2362
+ if (value === "task" || value === "milestone" || value === "workstream") {
2363
+ return value;
2364
+ }
2365
+ return null;
2366
+ };
2367
+ const resolveExecutionPolicyFromActiveRuns = (activeRunIds, workstreamId) => {
2368
+ for (const runId of activeRunIds) {
2369
+ const slice = autoContinueSliceRuns.get(runId);
2370
+ if (!slice)
2371
+ continue;
2372
+ if (typeof slice.workstreamId === "string" && slice.workstreamId.trim()) {
2373
+ if (slice.workstreamId.trim() !== workstreamId)
2374
+ continue;
2375
+ }
2376
+ const domain = (slice.domain ?? "").trim();
2377
+ const requiredSkills = Array.isArray(slice.requiredSkills)
2378
+ ? slice.requiredSkills.filter((skill) => typeof skill === "string" && skill.trim().length > 0)
2379
+ : [];
2380
+ if (!domain || requiredSkills.length === 0)
2381
+ continue;
2382
+ const executionPolicy = {
2383
+ domain,
2384
+ requiredSkills,
2385
+ };
2386
+ if (typeof slice.behaviorConfigId === "string" && slice.behaviorConfigId.trim()) {
2387
+ executionPolicy.profile = slice.behaviorConfigId.trim();
2388
+ }
2389
+ const scope = normalizeSliceScope(slice.scope ?? null);
2390
+ if (scope) {
2391
+ executionPolicy.sliceScopePreference = scope;
2392
+ }
2393
+ return executionPolicy;
2394
+ }
2395
+ return null;
2396
+ };
1676
2397
  for (const workstream of workstreamNodes) {
2398
+ const workstreamKey = `${initiativeId}:${workstream.id}`;
1677
2399
  const todoTasks = graph.recentTodos
1678
2400
  .map((taskId) => nodeById.get(taskId))
1679
2401
  .filter((node) => node?.type === "task" &&
1680
2402
  node.workstreamId === workstream.id &&
1681
2403
  isTodoStatus(node.status));
1682
- const pinKey = `${initiativeId}:${workstream.id}`;
2404
+ const pinKey = workstreamKey;
1683
2405
  const pin = pinnedByKey.get(pinKey) ?? null;
1684
2406
  const preferredTask = pin?.preferredTaskId && nodeById.get(pin.preferredTaskId)
1685
2407
  ? nodeById.get(pin.preferredTaskId) ?? null
@@ -1704,12 +2426,100 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1704
2426
  const preferredReadyTask = preferredCandidates.find((task) => taskIsReady(task) && !taskHasBlockedParent(task));
1705
2427
  const candidateTask = preferredReadyTask ?? readyTask ?? todoTasks[0] ?? null;
1706
2428
  const autoContinueRun = runningAutoContinueForWorkstream(initiativeId, workstream.id);
1707
- let queueState = autoContinueRun
2429
+ const autoContinueLane = getAutoContinueLaneForWorkstream(initiativeId, workstream.id);
2430
+ const laneState = autoContinueLane?.state ?? null;
2431
+ const scopedAllowedWorkstreams = Array.isArray(autoContinueRun?.allowedWorkstreamIds)
2432
+ ? (autoContinueRun.allowedWorkstreamIds
2433
+ .filter((id) => typeof id === "string" && id.trim().length > 0)
2434
+ .map((id) => id.trim()))
2435
+ : [];
2436
+ const runScopedToCurrentWorkstream = scopedAllowedWorkstreams.length === 1 &&
2437
+ scopedAllowedWorkstreams[0] === workstream.id &&
2438
+ autoContinueRun?.status === "running";
2439
+ const activeRunIds = Array.isArray(autoContinueRun?.activeSliceRunIds)
2440
+ ? autoContinueRun.activeSliceRunIds
2441
+ .filter((id) => typeof id === "string" && id.trim().length > 0)
2442
+ .map((id) => id.trim())
2443
+ : [];
2444
+ const activeTaskId = (autoContinueLane?.activeTaskIds?.[0]?.trim() ||
2445
+ autoContinueRun?.activeTaskId?.trim() ||
2446
+ null) ??
2447
+ null;
2448
+ const activeTaskNode = activeTaskId ? nodeById.get(activeTaskId) ?? null : null;
2449
+ const policyTask = candidateTask ??
2450
+ activeTaskNode ??
2451
+ todoTasks.find((task) => task.workstreamId === workstream.id) ??
2452
+ null;
2453
+ const derivedExecutionPolicy = policyTask
2454
+ ? deriveExecutionPolicy(policyTask, workstream)
2455
+ : null;
2456
+ const activeExecutionPolicy = resolveExecutionPolicyFromActiveRuns(activeRunIds, workstream.id);
2457
+ const executionPolicy = derivedExecutionPolicy ?? activeExecutionPolicy;
2458
+ const runScope = normalizeSliceScope(autoContinueRun?.scope ?? null);
2459
+ const preferredPolicyScope = normalizeSliceScope(executionPolicy?.sliceScopePreference ?? null);
2460
+ const defaultScope = runScope ??
2461
+ (preferredPolicyScope && preferredPolicyScope !== "task"
2462
+ ? preferredPolicyScope
2463
+ : pin?.preferredMilestoneId
2464
+ ? "milestone"
2465
+ : "task");
2466
+ const scopeSelection = selectSliceTasksByScope({
2467
+ scope: defaultScope,
2468
+ workstreamId: workstream.id,
2469
+ milestoneId: pin?.preferredMilestoneId ?? null,
2470
+ recentTodos: graph.recentTodos,
2471
+ nodeById,
2472
+ includeVerification: autoContinueRun?.includeVerification ?? false,
2473
+ });
2474
+ const cappedSliceTasks = typeof executionPolicy?.maxSliceTasks === "number" &&
2475
+ executionPolicy.maxSliceTasks > 0
2476
+ ? scopeSelection.tasks.slice(0, executionPolicy.maxSliceTasks)
2477
+ : scopeSelection.tasks;
2478
+ const sliceTaskIds = cappedSliceTasks.length > 0
2479
+ ? cappedSliceTasks.map((task) => task.id)
2480
+ : candidateTask?.id
2481
+ ? [candidateTask.id]
2482
+ : activeTaskId
2483
+ ? [activeTaskId]
2484
+ : [];
2485
+ const sliceMilestoneId = defaultScope === "milestone"
2486
+ ? scopeSelection.milestoneIds[0] ?? pin?.preferredMilestoneId ?? null
2487
+ : null;
2488
+ let queueState = laneState === "running"
1708
2489
  ? "running"
1709
- : candidateTask
1710
- ? "queued"
1711
- : "idle";
2490
+ : runScopedToCurrentWorkstream
2491
+ ? "running"
2492
+ : candidateTask
2493
+ ? "queued"
2494
+ : "idle";
1712
2495
  let blockReason = null;
2496
+ if (laneState === "blocked") {
2497
+ queueState = "blocked";
2498
+ blockReason = autoContinueLane?.blockedReason ?? "Blocked";
2499
+ }
2500
+ else if (laneState === "waiting_dependency") {
2501
+ queueState = "blocked";
2502
+ if (Array.isArray(autoContinueLane?.waitingOnWorkstreamIds) &&
2503
+ autoContinueLane.waitingOnWorkstreamIds.length > 0) {
2504
+ const waitingTitles = autoContinueLane.waitingOnWorkstreamIds
2505
+ .map((id) => {
2506
+ const node = nodeById.get(id);
2507
+ return node?.type === "workstream" ? node.title : id;
2508
+ })
2509
+ .filter(Boolean);
2510
+ blockReason =
2511
+ waitingTitles.length > 0
2512
+ ? `Waiting on ${waitingTitles.slice(0, 2).join(", ")}${waitingTitles.length > 2 ? "…" : ""}`
2513
+ : "Waiting on dependency workstreams";
2514
+ }
2515
+ else {
2516
+ blockReason = "Waiting on dependency workstreams";
2517
+ }
2518
+ }
2519
+ else if (laneState === "rate_limited") {
2520
+ queueState = "blocked";
2521
+ blockReason = autoContinueLane?.blockedReason ?? "Rate-limited";
2522
+ }
1713
2523
  if (!autoContinueRun && !readyTask && candidateTask) {
1714
2524
  queueState = "blocked";
1715
2525
  const blockedDeps = candidateTask.dependencyIds
@@ -1729,27 +2539,48 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1729
2539
  if (!candidateTask && !autoContinueRun && !pin) {
1730
2540
  continue;
1731
2541
  }
2542
+ if (isSuppressed(initiativeId, workstream.id) && queueState !== "running") {
2543
+ continue;
2544
+ }
1732
2545
  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
2546
+ const assignedRunnerAgents = [];
2547
+ const assignedRunnerSeen = new Set();
2548
+ for (const agent of workstream.assignedAgents) {
2549
+ pushRunnerAgent(assignedRunnerAgents, assignedRunnerSeen, {
2550
+ id: agent.id,
2551
+ name: agent.name,
2552
+ });
2553
+ }
2554
+ const inferredRunnerAgents = [];
2555
+ const inferredRunnerSeen = new Set();
2556
+ for (const agent of graph.initiative.assignedAgents) {
2557
+ pushRunnerAgent(inferredRunnerAgents, inferredRunnerSeen, {
2558
+ id: agent.id,
2559
+ name: agent.name,
2560
+ });
2561
+ }
2562
+ for (const agent of liveAgentsByInitiative.get(initiativeId) ?? []) {
2563
+ pushRunnerAgent(inferredRunnerAgents, inferredRunnerSeen, {
2564
+ id: agent.id,
2565
+ name: agent.name,
2566
+ });
2567
+ }
2568
+ if (autoContinueRun?.agentId) {
2569
+ pushRunnerAgent(inferredRunnerAgents, inferredRunnerSeen, {
2570
+ id: autoContinueRun.agentId,
2571
+ name: agentCatalogById.get(autoContinueRun.agentId)?.name ??
2572
+ autoContinueRun.agentId,
2573
+ });
2574
+ }
2575
+ const runnerAgents = assignedRunnerAgents.length > 0 ? assignedRunnerAgents : inferredRunnerAgents;
2576
+ const runnerSource = assignedRunnerAgents.length > 0
1744
2577
  ? "assigned"
1745
- : inferredAgent
2578
+ : runnerAgents.length > 0
1746
2579
  ? "inferred"
1747
2580
  : "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;
2581
+ const primaryRunner = runnerAgents[0] ?? null;
2582
+ const runnerAgentId = primaryRunner?.id ?? "unassigned";
2583
+ const runnerAgentName = primaryRunner?.name ?? "Unassigned";
1753
2584
  itemsForInitiative.push({
1754
2585
  initiativeId,
1755
2586
  initiativeTitle,
@@ -1758,25 +2589,50 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1758
2589
  workstreamTitle: workstream.title,
1759
2590
  workstreamStatus: workstream.status,
1760
2591
  nextTaskId: candidateTask?.id ??
1761
- (autoContinueRun?.activeTaskId?.trim() || null),
2592
+ activeTaskId,
1762
2593
  nextTaskTitle: candidateTask?.title ??
1763
- (autoContinueRun?.activeTaskId
1764
- ? nodeById.get(autoContinueRun.activeTaskId)?.title ?? null
2594
+ ((activeTaskId)
2595
+ ? nodeById.get(activeTaskId)?.title ?? null
1765
2596
  : null),
1766
2597
  nextTaskPriority: candidateTask?.priorityNum ?? null,
1767
2598
  nextTaskDueAt: candidateTask?.dueDate ?? null,
1768
2599
  runnerAgentId,
1769
2600
  runnerAgentName,
2601
+ runnerAgents,
1770
2602
  runnerSource,
1771
2603
  queueState,
1772
2604
  blockReason,
1773
2605
  isPinned: Boolean(pin),
1774
2606
  pinnedRank: pin ? (pinnedRankByKey.get(pinKey) ?? null) : null,
2607
+ sliceScope: defaultScope,
2608
+ sliceTaskIds,
2609
+ sliceTaskCount: sliceTaskIds.length,
2610
+ sliceMilestoneId,
2611
+ executionPolicy,
1775
2612
  autoContinue: autoContinueRun
1776
2613
  ? {
1777
2614
  status: autoContinueRun.status,
1778
2615
  activeTaskId: autoContinueRun.activeTaskId,
1779
2616
  activeRunId: autoContinueRun.activeRunId,
2617
+ activeTaskIds: Array.isArray(autoContinueRun.activeTaskIds)
2618
+ ? autoContinueRun.activeTaskIds
2619
+ : [],
2620
+ activeRunIds: Array.isArray(autoContinueRun.activeSliceRunIds)
2621
+ ? autoContinueRun.activeSliceRunIds
2622
+ : [],
2623
+ laneState,
2624
+ laneBlockedReason: autoContinueLane?.blockedReason ?? null,
2625
+ laneWaitingOnWorkstreamIds: Array.isArray(autoContinueLane?.waitingOnWorkstreamIds)
2626
+ ? autoContinueLane.waitingOnWorkstreamIds
2627
+ : [],
2628
+ laneRetryAt: autoContinueLane?.retryAt ?? null,
2629
+ maxParallelSlices: typeof autoContinueRun.maxParallelSlices === "number"
2630
+ ? autoContinueRun.maxParallelSlices
2631
+ : 1,
2632
+ parallelMode: (typeof autoContinueRun.parallelMode === "string" &&
2633
+ autoContinueRun.parallelMode.toLowerCase() === "iwmt"
2634
+ ? "iwmt"
2635
+ : "iwmt"),
1780
2636
  stopReason: autoContinueRun.stopReason,
1781
2637
  updatedAt: autoContinueRun.updatedAt,
1782
2638
  }
@@ -1794,6 +2650,44 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1794
2650
  const workstream = nodeById.get(workstreamId);
1795
2651
  if (!workstream || workstream.type !== "workstream")
1796
2652
  continue;
2653
+ const lane = getAutoContinueLaneForWorkstream(initiativeId, workstream.id);
2654
+ if (!lane &&
2655
+ !(typeof run.activeRunId === "string" && run.activeRunId.trim().length > 0)) {
2656
+ continue;
2657
+ }
2658
+ const laneState = lane?.state ?? null;
2659
+ const activeRunIds = Array.isArray(run.activeSliceRunIds)
2660
+ ? run.activeSliceRunIds
2661
+ .filter((id) => typeof id === "string" && id.trim().length > 0)
2662
+ .map((id) => id.trim())
2663
+ : [];
2664
+ const activeTaskId = lane?.activeTaskIds?.[0] ?? run.activeTaskId;
2665
+ const activeTaskNode = activeTaskId ? nodeById.get(activeTaskId) ?? null : null;
2666
+ const executionPolicy = (activeTaskNode ? deriveExecutionPolicy(activeTaskNode, workstream) : null) ??
2667
+ resolveExecutionPolicyFromActiveRuns(activeRunIds, workstream.id);
2668
+ const sliceScope = normalizeSliceScope(run.scope ?? null) ?? "task";
2669
+ const sliceTaskIds = lane?.activeTaskIds?.length
2670
+ ? lane.activeTaskIds
2671
+ : activeTaskId
2672
+ ? [activeTaskId]
2673
+ : [];
2674
+ const queueState = laneState === "running"
2675
+ ? "running"
2676
+ : laneState === "blocked" ||
2677
+ laneState === "waiting_dependency" ||
2678
+ laneState === "rate_limited"
2679
+ ? "blocked"
2680
+ : "queued";
2681
+ if (isSuppressed(initiativeId, workstream.id) && queueState !== "running") {
2682
+ continue;
2683
+ }
2684
+ const runRunnerAgents = [];
2685
+ const runRunnerSeen = new Set();
2686
+ pushRunnerAgent(runRunnerAgents, runRunnerSeen, {
2687
+ id: run.agentId,
2688
+ name: agentCatalogById.get(run.agentId)?.name ?? run.agentId,
2689
+ });
2690
+ const runPrimaryRunner = runRunnerAgents[0] ?? null;
1797
2691
  itemsForInitiative.push({
1798
2692
  initiativeId,
1799
2693
  initiativeTitle,
@@ -1801,23 +2695,52 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1801
2695
  workstreamId: workstream.id,
1802
2696
  workstreamTitle: workstream.title,
1803
2697
  workstreamStatus: workstream.status,
1804
- nextTaskId: run.activeTaskId,
1805
- nextTaskTitle: run.activeTaskId
1806
- ? nodeById.get(run.activeTaskId)?.title ?? null
2698
+ nextTaskId: activeTaskId ?? null,
2699
+ nextTaskTitle: activeTaskId
2700
+ ? nodeById.get(activeTaskId)?.title ?? null
1807
2701
  : null,
1808
2702
  nextTaskPriority: null,
1809
2703
  nextTaskDueAt: null,
1810
- runnerAgentId: run.agentId,
1811
- runnerAgentName: agentCatalogById.get(run.agentId)?.name ?? run.agentId,
1812
- runnerSource: "inferred",
1813
- queueState: "running",
1814
- blockReason: null,
2704
+ runnerAgentId: runPrimaryRunner?.id ?? "unassigned",
2705
+ runnerAgentName: runPrimaryRunner?.name ?? "Unassigned",
2706
+ runnerAgents: runRunnerAgents,
2707
+ runnerSource: runPrimaryRunner ? "inferred" : "fallback",
2708
+ queueState,
2709
+ blockReason: queueState === "blocked"
2710
+ ? lane?.blockedReason ?? "Blocked"
2711
+ : null,
1815
2712
  isPinned: Boolean(pinnedByKey.get(`${initiativeId}:${workstream.id}`)),
1816
2713
  pinnedRank: pinnedRankByKey.get(`${initiativeId}:${workstream.id}`) ?? null,
2714
+ sliceScope,
2715
+ sliceTaskIds,
2716
+ sliceTaskCount: sliceTaskIds.length,
2717
+ sliceMilestoneId: sliceScope === "milestone"
2718
+ ? activeTaskNode?.milestoneId ?? null
2719
+ : null,
2720
+ executionPolicy,
1817
2721
  autoContinue: {
1818
2722
  status: run.status,
1819
2723
  activeTaskId: run.activeTaskId,
1820
2724
  activeRunId: run.activeRunId,
2725
+ activeTaskIds: Array.isArray(run.activeTaskIds)
2726
+ ? run.activeTaskIds
2727
+ : [],
2728
+ activeRunIds: Array.isArray(run.activeSliceRunIds)
2729
+ ? run.activeSliceRunIds
2730
+ : [],
2731
+ laneState,
2732
+ laneBlockedReason: lane?.blockedReason ?? null,
2733
+ laneWaitingOnWorkstreamIds: Array.isArray(lane?.waitingOnWorkstreamIds)
2734
+ ? lane.waitingOnWorkstreamIds
2735
+ : [],
2736
+ laneRetryAt: lane?.retryAt ?? null,
2737
+ maxParallelSlices: typeof run.maxParallelSlices === "number"
2738
+ ? run.maxParallelSlices
2739
+ : 1,
2740
+ parallelMode: typeof run.parallelMode === "string" &&
2741
+ run.parallelMode.toLowerCase() === "iwmt"
2742
+ ? "iwmt"
2743
+ : "iwmt",
1821
2744
  stopReason: run.stopReason,
1822
2745
  updatedAt: run.updatedAt,
1823
2746
  },
@@ -1839,7 +2762,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1839
2762
  return { items, degraded };
1840
2763
  }
1841
2764
  async function buildNextUpQueue(input) {
1842
- const key = nextUpQueueCacheKeyFor(input?.initiativeId?.trim() || null);
2765
+ const key = nextUpQueueCacheKeyFor(input?.initiativeId?.trim() || null, input?.projectId?.trim() || null);
1843
2766
  const fresh = readNextUpQueueCache(key, { allowStale: false });
1844
2767
  if (fresh)
1845
2768
  return fresh;
@@ -1883,6 +2806,14 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1883
2806
  nextUpQueueInFlight.delete(key);
1884
2807
  }
1885
2808
  }
2809
+ // Prime queue cache shortly after boot so first dashboard paint is not cold.
2810
+ const prewarmNextUpQueue = () => {
2811
+ void buildNextUpQueue({ initiativeId: null, projectId: null }).catch(() => {
2812
+ // best effort prewarm only
2813
+ });
2814
+ };
2815
+ const nextUpPrewarmTimer = setTimeout(prewarmNextUpQueue, 75);
2816
+ nextUpPrewarmTimer.unref?.();
1886
2817
  const autoContinueTimer = setInterval(() => {
1887
2818
  void tickAllAutoContinue();
1888
2819
  }, AUTO_CONTINUE_TICK_MS);
@@ -1916,6 +2847,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1916
2847
  formatInitiatives,
1917
2848
  getOnboardingState: async () => getOnboardingState(await onboarding.getStatus()),
1918
2849
  });
2850
+ registerUsageRoutes(apiRouter, {
2851
+ client,
2852
+ listActivityPage: ({ limit, runId, since, until, cursor }) => listActivityPage({ limit, runId, since, until, cursor }),
2853
+ sendJson,
2854
+ safeErrorMessage,
2855
+ });
1919
2856
  registerAgentSuiteRoutes(apiRouter, {
1920
2857
  pluginVersion: config.pluginVersion,
1921
2858
  telemetryDistinctId,
@@ -1926,6 +2863,11 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1926
2863
  applyOrgxAgentSuitePlan,
1927
2864
  generateAgentSuiteOperationId,
1928
2865
  updateSkillPackPolicy,
2866
+ rollbackSkillPackPolicy,
2867
+ fetchAgentRuntimeSettings: ({ workspaceId, projectId } = {}) => client.getClientAgentRuntimeSettings({
2868
+ workspaceId: workspaceId ?? projectId ?? null,
2869
+ }),
2870
+ updateAgentRuntimeSettings: (payload) => client.updateClientAgentRuntimeSettings(payload),
1929
2871
  posthogCapture,
1930
2872
  sendJson,
1931
2873
  safeErrorMessage,
@@ -1944,13 +2886,20 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1944
2886
  sendJson,
1945
2887
  safeErrorMessage,
1946
2888
  });
2889
+ registerSentinelsCatalogRoutes(apiRouter, {
2890
+ sendJson,
2891
+ safeErrorMessage,
2892
+ });
1947
2893
  registerMissionControlReadRoutes(apiRouter, {
1948
2894
  autoContinueRuns,
1949
2895
  defaultAutoContinueTokenBudget,
2896
+ defaultAutoContinueMaxParallelSlices,
1950
2897
  autoContinueTickMs: AUTO_CONTINUE_TICK_MS,
1951
2898
  buildMissionControlGraph: (initiativeId) => buildMissionControlGraph(client, initiativeId),
1952
2899
  applyLocalInitiativeOverrideToGraph: (graph) => applyLocalInitiativeOverrideToGraph(graph),
2900
+ listInitiativeIdsForProject: ({ projectId }) => listInitiativeIdsForProject({ projectId }),
1953
2901
  buildNextUpQueue,
2902
+ rawRequest: (requestMethod, requestPath, body) => client.rawRequest(requestMethod, requestPath, body),
1954
2903
  sendJson,
1955
2904
  safeErrorMessage,
1956
2905
  });
@@ -1993,12 +2942,127 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1993
2942
  applyLocalInitiativeOverrides,
1994
2943
  formatInitiatives,
1995
2944
  getSnapshot,
2945
+ scheduleWorkstreamReassignment: async (input) => {
2946
+ const initiativeId = input.initiativeId.trim();
2947
+ const workstreamId = input.workstreamId.trim();
2948
+ if (!initiativeId || !workstreamId)
2949
+ return null;
2950
+ const normalizedStatus = (input.status ?? "").trim().toLowerCase();
2951
+ const shouldRedispatch = normalizedStatus === "active" ||
2952
+ normalizedStatus === "ready" ||
2953
+ normalizedStatus === "queued" ||
2954
+ normalizedStatus === "running" ||
2955
+ normalizedStatus === "in_progress" ||
2956
+ normalizedStatus === "pending";
2957
+ if (!shouldRedispatch)
2958
+ return null;
2959
+ if (!isDispatchableWorkstreamStatus(normalizedStatus))
2960
+ return null;
2961
+ const liveRun = runningAutoContinueForWorkstream(initiativeId, workstreamId);
2962
+ return await scheduleAutoFixForWorkstream({
2963
+ initiativeId,
2964
+ workstreamId,
2965
+ runId: liveRun?.activeRunId ?? null,
2966
+ event: input.event,
2967
+ requestedByAgentId: "system",
2968
+ requestedByAgentName: "System",
2969
+ graceMs: 5_000,
2970
+ });
2971
+ },
1996
2972
  sendJson,
1997
2973
  safeErrorMessage,
1998
2974
  });
2975
+ const readCachedDecisionRows = () => {
2976
+ const snapshots = [
2977
+ readSnapshotResponseCache("live-snapshot"),
2978
+ readSnapshotResponseCache("dashboard-bundle"),
2979
+ readSnapshotResponseCache("live-snapshot-v2"),
2980
+ ];
2981
+ const rows = [];
2982
+ for (const snapshot of snapshots) {
2983
+ if (!snapshot || typeof snapshot !== "object")
2984
+ continue;
2985
+ const decisionsRaw = snapshot.decisions;
2986
+ if (!Array.isArray(decisionsRaw))
2987
+ continue;
2988
+ for (const entry of decisionsRaw) {
2989
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
2990
+ continue;
2991
+ rows.push(entry);
2992
+ }
2993
+ }
2994
+ return rows;
2995
+ };
2996
+ const emitDecisionResolvedActivity = async (input) => {
2997
+ const ids = Array.from(new Set(input.ids
2998
+ .filter((id) => typeof id === "string")
2999
+ .map((id) => id.trim())
3000
+ .filter(Boolean)));
3001
+ if (ids.length === 0)
3002
+ return;
3003
+ const decisionById = new Map();
3004
+ for (const row of readCachedDecisionRows()) {
3005
+ const rowId = pickString(row, ["id"])?.trim() ?? "";
3006
+ if (!rowId || decisionById.has(rowId))
3007
+ continue;
3008
+ decisionById.set(rowId, row);
3009
+ }
3010
+ for (const decisionId of ids) {
3011
+ const row = decisionById.get(decisionId) ?? null;
3012
+ const decisionTitle = pickString(row ?? {}, ["title", "summary"]) ??
3013
+ `Decision ${decisionId.slice(0, 8)}`;
3014
+ const scopedInitiativeId = input.initiativeId ??
3015
+ pickString(row ?? {}, ["initiative_id", "initiativeId"]) ??
3016
+ null;
3017
+ const scopedRunId = input.sliceRunId ??
3018
+ pickString(row ?? {}, [
3019
+ "run_id",
3020
+ "runId",
3021
+ "source_run_id",
3022
+ "sourceRunId",
3023
+ "correlation_id",
3024
+ "correlationId",
3025
+ ]) ??
3026
+ null;
3027
+ await emitActivitySafe({
3028
+ initiativeId: scopedInitiativeId,
3029
+ runId: scopedRunId ?? undefined,
3030
+ correlationId: scopedRunId ?? undefined,
3031
+ phase: "review",
3032
+ level: "info",
3033
+ message: `Decision ${input.action === "approve" ? "approved" : "rejected"}: ${decisionTitle}`,
3034
+ progressPct: 100,
3035
+ nextStep: input.action === "approve"
3036
+ ? "Execution can continue with the approved direction."
3037
+ : "Review the rejected decision and provide revised guidance to continue safely.",
3038
+ metadata: {
3039
+ event: "decision_resolved",
3040
+ action: input.action,
3041
+ resolver: "human",
3042
+ decision_id: decisionId,
3043
+ decision_ids: ids,
3044
+ decision_title: decisionTitle,
3045
+ initiative_id: scopedInitiativeId,
3046
+ workstream_id: pickString(row ?? {}, ["workstream_id", "workstreamId"]) ?? null,
3047
+ source_run_id: scopedRunId,
3048
+ option_id: input.optionId ?? null,
3049
+ note: input.note ?? null,
3050
+ slice_run_id: input.sliceRunId ?? null,
3051
+ },
3052
+ });
3053
+ }
3054
+ };
1999
3055
  registerDecisionActionsRoutes(apiRouter, {
2000
3056
  parseJsonRequest,
2001
- bulkDecideDecisions: (ids, action, note) => client.bulkDecideDecisions(ids, action, note),
3057
+ bulkDecideDecisions: (ids, action, input) => client.bulkDecideDecisions(ids, action, input),
3058
+ emitDecisionResolvedActivity: async (ids, action, input) => {
3059
+ await emitDecisionResolvedActivity({
3060
+ ids,
3061
+ action,
3062
+ note: input?.note ?? null,
3063
+ optionId: input?.optionId ?? null,
3064
+ });
3065
+ },
2002
3066
  sendJson,
2003
3067
  safeErrorMessage,
2004
3068
  });
@@ -2009,6 +3073,147 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2009
3073
  createRunCheckpoint: (runId, input) => client.createRunCheckpoint(runId, input),
2010
3074
  restoreRunCheckpoint: (runId, input) => client.restoreRunCheckpoint(runId, input),
2011
3075
  runAction: (runId, action, input) => client.runAction(runId, action, input),
3076
+ markRunCompleted: async (runId, input) => {
3077
+ const normalizedRunId = runId.trim();
3078
+ if (!normalizedRunId) {
3079
+ throw new Error("runId is required");
3080
+ }
3081
+ const nowIso = new Date().toISOString();
3082
+ const reason = input.reason?.trim() || null;
3083
+ const message = reason
3084
+ ? `Marked completed from dashboard (${reason}).`
3085
+ : "Marked completed from dashboard.";
3086
+ const existingRun = getAgentRun(normalizedRunId);
3087
+ // ── Try OrgX-side completion first (for remote sessions) ────────────
3088
+ let remoteOk = false;
3089
+ try {
3090
+ await client.updateEntity("run", normalizedRunId, {
3091
+ status: "completed",
3092
+ phase: "completed",
3093
+ });
3094
+ remoteOk = true;
3095
+ }
3096
+ catch {
3097
+ // OrgX may not support updating runs directly — fall through to local
3098
+ }
3099
+ // ── Local operations (defensive — partial failures don't block) ─────
3100
+ let runtimeRecord = null;
3101
+ try {
3102
+ runtimeRecord = upsertRuntimeInstanceFromHook({
3103
+ source_client: "api",
3104
+ event: "session_stop",
3105
+ run_id: normalizedRunId,
3106
+ correlation_id: normalizedRunId,
3107
+ initiative_id: existingRun?.initiativeId ?? null,
3108
+ workstream_id: existingRun?.workstreamId ?? null,
3109
+ task_id: existingRun?.taskId ?? null,
3110
+ agent_id: existingRun?.agentId ?? null,
3111
+ phase: "completed",
3112
+ message,
3113
+ timestamp: nowIso,
3114
+ metadata: {
3115
+ source: "dashboard_manual_complete",
3116
+ reason,
3117
+ },
3118
+ });
3119
+ }
3120
+ catch (err) {
3121
+ console.error(`[markRunCompleted] upsertRuntime failed for ${normalizedRunId}:`, err);
3122
+ }
3123
+ try {
3124
+ markAgentRunStopped(normalizedRunId);
3125
+ }
3126
+ catch (err) {
3127
+ console.error(`[markRunCompleted] markAgentRunStopped failed for ${normalizedRunId}:`, err);
3128
+ }
3129
+ if (runtimeRecord) {
3130
+ broadcastRuntimeSse("runtime.updated", runtimeRecord);
3131
+ }
3132
+ clearSnapshotResponseCache();
3133
+ try {
3134
+ appendActivityItems([
3135
+ {
3136
+ id: randomUUID(),
3137
+ type: "run_completed",
3138
+ title: message,
3139
+ description: reason,
3140
+ agentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
3141
+ agentName: runtimeRecord?.agentName ?? null,
3142
+ requesterAgentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
3143
+ requesterAgentName: runtimeRecord?.agentName ?? null,
3144
+ executorAgentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
3145
+ executorAgentName: runtimeRecord?.agentName ?? null,
3146
+ runId: normalizedRunId,
3147
+ initiativeId: runtimeRecord?.initiativeId ?? existingRun?.initiativeId ?? null,
3148
+ timestamp: nowIso,
3149
+ phase: "completed",
3150
+ state: "done",
3151
+ summary: message,
3152
+ metadata: {
3153
+ source: "dashboard_manual_complete",
3154
+ reason,
3155
+ remoteOk,
3156
+ event: "dashboard_run_mark_completed",
3157
+ },
3158
+ },
3159
+ ]);
3160
+ }
3161
+ catch (err) {
3162
+ console.error(`[markRunCompleted] appendActivity failed for ${normalizedRunId}:`, err);
3163
+ }
3164
+ // ── Write to outbox so snapshot merge picks up the completion ───────
3165
+ try {
3166
+ const outboxSessionId = runtimeRecord?.initiativeId ?? existingRun?.initiativeId ?? normalizedRunId;
3167
+ const outboxActivityItem = {
3168
+ id: randomUUID(),
3169
+ type: "run_completed",
3170
+ title: message,
3171
+ description: reason,
3172
+ agentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
3173
+ agentName: runtimeRecord?.agentName ?? null,
3174
+ requesterAgentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
3175
+ requesterAgentName: runtimeRecord?.agentName ?? null,
3176
+ executorAgentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
3177
+ executorAgentName: runtimeRecord?.agentName ?? null,
3178
+ runId: normalizedRunId,
3179
+ initiativeId: runtimeRecord?.initiativeId ?? existingRun?.initiativeId ?? null,
3180
+ timestamp: nowIso,
3181
+ phase: "completed",
3182
+ state: "done",
3183
+ summary: message,
3184
+ metadata: {
3185
+ source: "dashboard_manual_complete",
3186
+ reason,
3187
+ remoteOk,
3188
+ event: "dashboard_run_mark_completed",
3189
+ },
3190
+ };
3191
+ await appendToOutbox(outboxSessionId, {
3192
+ id: outboxActivityItem.id,
3193
+ type: "outcome",
3194
+ timestamp: nowIso,
3195
+ payload: {
3196
+ runId: normalizedRunId,
3197
+ status: "completed",
3198
+ reason,
3199
+ remoteOk,
3200
+ },
3201
+ activityItem: outboxActivityItem,
3202
+ });
3203
+ }
3204
+ catch (err) {
3205
+ console.error(`[markRunCompleted] outbox write failed for ${normalizedRunId}:`, err);
3206
+ }
3207
+ return {
3208
+ ok: true,
3209
+ data: {
3210
+ runId: normalizedRunId,
3211
+ action: "complete",
3212
+ status: "completed",
3213
+ remoteOk,
3214
+ },
3215
+ };
3216
+ },
2012
3217
  sendJson,
2013
3218
  safeErrorMessage,
2014
3219
  });
@@ -2032,6 +3237,20 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2032
3237
  sendJson,
2033
3238
  safeErrorMessage,
2034
3239
  });
3240
+ registerChatRoutes(apiRouter, {
3241
+ parseJsonRequest,
3242
+ pickString,
3243
+ parsePositiveInt,
3244
+ emitActivitySafe,
3245
+ sendJson,
3246
+ safeErrorMessage,
3247
+ });
3248
+ registerRealtimeOrchestratorRoutes(apiRouter, {
3249
+ parseJsonRequest,
3250
+ rawRequest: (requestMethod, requestPath, body) => client.rawRequest(requestMethod, requestPath, body),
3251
+ sendJson,
3252
+ safeErrorMessage,
3253
+ });
2035
3254
  registerMissionControlActionsRoutes(apiRouter, {
2036
3255
  parseJsonRequest,
2037
3256
  pickString,
@@ -2049,11 +3268,17 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2049
3268
  stopAutoContinueRun,
2050
3269
  updateInitiativeAutoContinueState,
2051
3270
  tickAllAutoContinue,
3271
+ scheduleAutoFixForWorkstream,
2052
3272
  upsertNextUpQueuePin,
2053
3273
  removeNextUpQueuePin,
3274
+ suppressNextUpQueueItem,
2054
3275
  setNextUpQueuePinOrder,
3276
+ clearNextUpQueueCache,
2055
3277
  resolveAutoAssignments,
3278
+ buildMissionControlGraph: (initiativeId) => buildMissionControlGraph(client, initiativeId),
3279
+ applyLocalInitiativeOverrideToGraph: (graph) => applyLocalInitiativeOverrideToGraph(graph),
2056
3280
  client,
3281
+ rawRequest: (requestMethod, requestPath, body) => client.rawRequest(requestMethod, requestPath, body),
2057
3282
  sendJson,
2058
3283
  safeErrorMessage,
2059
3284
  });
@@ -2095,10 +3320,11 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2095
3320
  parseJsonRequest,
2096
3321
  pickString,
2097
3322
  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 }),
3323
+ getLiveAgents: ({ initiative, projectId, includeIdle }) => client.getLiveAgents({ initiative, projectId, includeIdle }),
3324
+ getLiveInitiatives: ({ id, projectId, limit, offset }) => client.getLiveInitiatives({ id, projectId, limit, offset }),
3325
+ getLiveDecisions: ({ status, projectId, limit }) => client.getLiveDecisions({ status, projectId, limit }),
2101
3326
  getHandoffs: () => client.getHandoffs(),
3327
+ listInitiativeIdsForProject: ({ projectId }) => listInitiativeIdsForProject({ projectId }),
2102
3328
  loadLocalOpenClawSnapshot,
2103
3329
  toLocalLiveAgents,
2104
3330
  toLocalLiveInitiatives,
@@ -2107,9 +3333,15 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2107
3333
  sendJson,
2108
3334
  safeErrorMessage,
2109
3335
  });
3336
+ registerLiveTerminalRoutes(apiRouter, {
3337
+ parseJsonRequest,
3338
+ sendJson,
3339
+ safeErrorMessage,
3340
+ });
2110
3341
  registerLiveLegacyRoutes(apiRouter, {
2111
- getLiveSessions: ({ initiative, limit }) => client.getLiveSessions({ initiative, limit }),
2112
- getLiveActivity: ({ run, since, limit }) => client.getLiveActivity({ run, since, limit }),
3342
+ getLiveSessions: ({ initiative, projectId, limit }) => client.getLiveSessions({ initiative, projectId, limit }),
3343
+ getLiveActivity: ({ run, since, projectId, limit }) => client.getLiveActivity({ run, since, projectId, limit }),
3344
+ listInitiativeIdsForProject: ({ projectId }) => listInitiativeIdsForProject({ projectId }),
2113
3345
  listRuntimeInstances,
2114
3346
  injectRuntimeInstancesAsSessions,
2115
3347
  enrichSessionsWithRuntime,
@@ -2171,16 +3403,18 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2171
3403
  toLocalSessionTree,
2172
3404
  toLocalLiveActivity,
2173
3405
  toLocalLiveAgents,
2174
- getLiveSessions: ({ initiative, limit }) => client.getLiveSessions({ initiative, limit }),
2175
- getLiveActivity: ({ run, since, limit }) => client.getLiveActivity({ run, since, limit }),
3406
+ getLiveSessions: ({ initiative, projectId, limit }) => client.getLiveSessions({ initiative, projectId, limit }),
3407
+ getLiveActivity: ({ run, since, projectId, limit }) => client.getLiveActivity({ run, since, projectId, limit }),
2176
3408
  getHandoffs: () => client.getHandoffs(),
2177
- getLiveDecisions: ({ status, limit }) => client.getLiveDecisions({ status, limit }),
2178
- getLiveAgents: ({ initiative, includeIdle }) => client.getLiveAgents({ initiative, includeIdle }),
3409
+ getLiveDecisions: ({ status, projectId, limit }) => client.getLiveDecisions({ status, projectId, limit }),
3410
+ getLiveAgents: ({ initiative, projectId, includeIdle }) => client.getLiveAgents({ initiative, projectId, includeIdle }),
3411
+ listInitiativeIdsForProject: ({ projectId }) => listInitiativeIdsForProject({ projectId }),
2179
3412
  mapDecisionEntity: (entry) => mapDecisionEntity(entry),
2180
3413
  applyAgentContextsToSessionTree,
2181
3414
  applyAgentContextsToActivity,
2182
3415
  mergeSessionTrees,
2183
3416
  mergeActivities,
3417
+ semanticActivityKey,
2184
3418
  listRuntimeInstances,
2185
3419
  injectRuntimeInstancesAsSessions,
2186
3420
  enrichSessionsWithRuntime,
@@ -2196,6 +3430,14 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2196
3430
  lastSnapshotActivityFingerprint = state.lastFingerprint;
2197
3431
  lastSnapshotActivityPersistAt = state.lastPersistAt;
2198
3432
  },
3433
+ parseJsonRequest,
3434
+ buildNextUpQueue: ({ initiativeId, projectId }) => buildNextUpQueue({ initiativeId, projectId }),
3435
+ bulkDecideDecisions: (ids, action, input) => client.bulkDecideDecisions(ids, action, input),
3436
+ emitDecisionResolvedActivity: async (input) => {
3437
+ await emitDecisionResolvedActivity(input);
3438
+ },
3439
+ runAction: (runId, action, input) => client.runAction(runId, action, input),
3440
+ listChatThreads: ({ commandCenterId, initiativeId, limit, offset }) => listChatThreads({ commandCenterId, initiativeId, limit, offset }),
2199
3441
  sendJson,
2200
3442
  });
2201
3443
  registerRuntimeHookRoutes(apiRouter, {
@@ -2232,12 +3474,81 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2232
3474
  sendJson,
2233
3475
  safeErrorMessage,
2234
3476
  });
3477
+ registerLiveTerminalRoutes(apiRouter, {
3478
+ parseJsonRequest,
3479
+ sendJson,
3480
+ safeErrorMessage,
3481
+ });
3482
+ registerLiveTriageRoutes(apiRouter, {
3483
+ parseJsonRequest,
3484
+ sendJson,
3485
+ getDecisions: (workspaceId) => {
3486
+ // Return cached decisions from latest snapshot, or empty array
3487
+ const normalizedWorkspaceId = (workspaceId ?? "").trim();
3488
+ const scopedKeys = normalizedWorkspaceId
3489
+ ? [
3490
+ `live-snapshot:${normalizedWorkspaceId}`,
3491
+ `live-snapshot-v2:${normalizedWorkspaceId}`,
3492
+ `dashboard-bundle:${normalizedWorkspaceId}`,
3493
+ ]
3494
+ : [];
3495
+ const fallbackKeys = ["live-snapshot", "dashboard-bundle", "live-snapshot-v2"];
3496
+ const keys = [...scopedKeys, ...fallbackKeys];
3497
+ try {
3498
+ for (const key of keys) {
3499
+ const cached = readSnapshotResponseCache(key);
3500
+ if (!cached || typeof cached !== "object" || !("decisions" in cached))
3501
+ continue;
3502
+ const decisions = cached.decisions;
3503
+ if (Array.isArray(decisions))
3504
+ return decisions;
3505
+ }
3506
+ }
3507
+ catch {
3508
+ // best effort
3509
+ }
3510
+ return [];
3511
+ },
3512
+ getBlockerEvents: () => {
3513
+ // Extract blocker events from recent activity
3514
+ // In future, this will read from a dedicated blocker store
3515
+ return [];
3516
+ },
3517
+ resolveDecisionAction: async (decisionId, action, note, optionId) => {
3518
+ try {
3519
+ await client.bulkDecideDecisions([decisionId], action, { note: note ?? undefined, optionId: optionId ?? undefined });
3520
+ return { ok: true };
3521
+ }
3522
+ catch (err) {
3523
+ return {
3524
+ ok: false,
3525
+ error: err instanceof Error ? err.message : "Decision action failed",
3526
+ };
3527
+ }
3528
+ },
3529
+ emitDecisionResolvedActivity: async (input) => {
3530
+ await emitDecisionResolvedActivity(input);
3531
+ },
3532
+ });
2235
3533
  return async function handler(req, res) {
2236
3534
  const method = (req.method ?? "GET").toUpperCase();
2237
3535
  const rawUrl = req.url ?? "/";
2238
3536
  const [path, queryString] = rawUrl.split("?", 2);
2239
3537
  const url = path;
2240
3538
  const searchParams = new URLSearchParams(queryString ?? "");
3539
+ // Legacy deep-link compatibility:
3540
+ // Older launch paths still point at /workspace-hub. Route those into
3541
+ // the current dashboard entrypoint while preserving query params.
3542
+ if (url === "/workspace-hub" || url === "/workspace-hub/") {
3543
+ const suffix = queryString && queryString.trim().length > 0 ? `?${queryString}` : "";
3544
+ res.writeHead(302, {
3545
+ Location: `/orgx/live${suffix}`,
3546
+ ...SECURITY_HEADERS,
3547
+ ...CORS_HEADERS,
3548
+ });
3549
+ res.end();
3550
+ return true;
3551
+ }
2241
3552
  // Only handle /orgx paths — return false for everything else
2242
3553
  if (!url.startsWith("/orgx")) {
2243
3554
  return false;
@@ -2317,9 +3628,13 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2317
3628
  const cacheControl = assetExt === ".js" || assetExt === ".css"
2318
3629
  ? "no-cache"
2319
3630
  : "public, max-age=31536000, immutable";
2320
- sendFile(res, assetPath, cacheControl);
3631
+ sendFile(req, res, assetPath, cacheControl);
2321
3632
  }
2322
3633
  else {
3634
+ if (/^assets\/[A-Za-z0-9_-]+\.js$/i.test(subPath)) {
3635
+ sendStaleChunkRecovery(res);
3636
+ return true;
3637
+ }
2323
3638
  send404(res);
2324
3639
  }
2325
3640
  return true;
@@ -2328,12 +3643,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2328
3643
  if (subPath) {
2329
3644
  const filePath = resolveSafeDistPath(subPath);
2330
3645
  if (filePath && existsSync(filePath)) {
2331
- sendFile(res, filePath, "no-cache");
3646
+ sendFile(req, res, filePath, "no-cache");
2332
3647
  return true;
2333
3648
  }
2334
3649
  }
2335
3650
  // SPA fallback: serve index.html for all other routes under /orgx/live
2336
- sendIndexHtml(res);
3651
+ sendIndexHtml(req, res);
2337
3652
  return true;
2338
3653
  }
2339
3654
  // Catch-all for /orgx but not /orgx/live or /orgx/api