@useorgx/openclaw-plugin 0.4.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dashboard/dist/assets/BJgZIVUQ.js +53 -0
- package/dashboard/dist/assets/BJgZIVUQ.js.br +0 -0
- package/dashboard/dist/assets/BJgZIVUQ.js.gz +0 -0
- package/dashboard/dist/assets/BXWDRGm-.js +1 -0
- package/dashboard/dist/assets/BXWDRGm-.js.br +0 -0
- package/dashboard/dist/assets/BXWDRGm-.js.gz +0 -0
- package/dashboard/dist/assets/BgOYB78t.js +4 -0
- package/dashboard/dist/assets/BgOYB78t.js.br +0 -0
- package/dashboard/dist/assets/BgOYB78t.js.gz +0 -0
- package/dashboard/dist/assets/C-KIc3Wc.js.br +0 -0
- package/dashboard/dist/assets/C-KIc3Wc.js.gz +0 -0
- package/dashboard/dist/assets/CE38zU4U.js +1 -0
- package/dashboard/dist/assets/CE38zU4U.js.br +0 -0
- package/dashboard/dist/assets/CE38zU4U.js.gz +0 -0
- package/dashboard/dist/assets/CFGKRAzG.js +1 -0
- package/dashboard/dist/assets/CFGKRAzG.js.br +0 -0
- package/dashboard/dist/assets/CFGKRAzG.js.gz +0 -0
- package/dashboard/dist/assets/CGGR2GZh.js +1 -0
- package/dashboard/dist/assets/CGGR2GZh.js.br +0 -0
- package/dashboard/dist/assets/CGGR2GZh.js.gz +0 -0
- package/dashboard/dist/assets/CL_wXqR7.js +1 -0
- package/dashboard/dist/assets/CL_wXqR7.js.br +0 -0
- package/dashboard/dist/assets/CL_wXqR7.js.gz +0 -0
- package/dashboard/dist/assets/CPFiTmlw.js +8 -0
- package/dashboard/dist/assets/CPFiTmlw.js.br +0 -0
- package/dashboard/dist/assets/CPFiTmlw.js.gz +0 -0
- package/dashboard/dist/assets/CZZTvkQZ.js +1 -0
- package/dashboard/dist/assets/CZZTvkQZ.js.br +0 -0
- package/dashboard/dist/assets/CZZTvkQZ.js.gz +0 -0
- package/dashboard/dist/assets/{CpJsfbXo.js → CxQ08qFN.js} +2 -2
- package/dashboard/dist/assets/CxQ08qFN.js.br +0 -0
- package/dashboard/dist/assets/CxQ08qFN.js.gz +0 -0
- package/dashboard/dist/assets/D-bf6hEI.js +213 -0
- package/dashboard/dist/assets/D-bf6hEI.js.br +0 -0
- package/dashboard/dist/assets/D-bf6hEI.js.gz +0 -0
- package/dashboard/dist/assets/DG6y9wJI.js +2 -0
- package/dashboard/dist/assets/DG6y9wJI.js.br +0 -0
- package/dashboard/dist/assets/DG6y9wJI.js.gz +0 -0
- package/dashboard/dist/assets/DNxKz-GV.js +1 -0
- package/dashboard/dist/assets/DNxKz-GV.js.br +0 -0
- package/dashboard/dist/assets/DNxKz-GV.js.gz +0 -0
- package/dashboard/dist/assets/DW_rKUic.js +11 -0
- package/dashboard/dist/assets/DW_rKUic.js.br +0 -0
- package/dashboard/dist/assets/DW_rKUic.js.gz +0 -0
- package/dashboard/dist/assets/DbNoijHm.js +1 -0
- package/dashboard/dist/assets/DbNoijHm.js.br +0 -0
- package/dashboard/dist/assets/DbNoijHm.js.gz +0 -0
- package/dashboard/dist/assets/DjcdE6jC.js +2 -0
- package/dashboard/dist/assets/DjcdE6jC.js.br +0 -0
- package/dashboard/dist/assets/DjcdE6jC.js.gz +0 -0
- package/dashboard/dist/assets/FZYuCDnt.js +1 -0
- package/dashboard/dist/assets/FZYuCDnt.js.br +0 -0
- package/dashboard/dist/assets/FZYuCDnt.js.gz +0 -0
- package/dashboard/dist/assets/PAUiij_z.js +1 -0
- package/dashboard/dist/assets/PAUiij_z.js.br +0 -0
- package/dashboard/dist/assets/PAUiij_z.js.gz +0 -0
- package/dashboard/dist/assets/cNrhgGc1.js +8 -0
- package/dashboard/dist/assets/cNrhgGc1.js.br +0 -0
- package/dashboard/dist/assets/cNrhgGc1.js.gz +0 -0
- package/dashboard/dist/assets/h5biQs2I.css +1 -0
- package/dashboard/dist/assets/h5biQs2I.css.br +0 -0
- package/dashboard/dist/assets/h5biQs2I.css.gz +0 -0
- package/dashboard/dist/assets/ic2FaMnh.js +1 -0
- package/dashboard/dist/assets/ic2FaMnh.js.br +0 -0
- package/dashboard/dist/assets/ic2FaMnh.js.gz +0 -0
- package/dashboard/dist/assets/nByHNHoW.js +1 -0
- package/dashboard/dist/assets/nByHNHoW.js.br +0 -0
- package/dashboard/dist/assets/nByHNHoW.js.gz +0 -0
- package/dashboard/dist/assets/qm8xLgv-.css +1 -0
- package/dashboard/dist/assets/qm8xLgv-.css.br +0 -0
- package/dashboard/dist/assets/qm8xLgv-.css.gz +0 -0
- package/dashboard/dist/assets/tS9mbYZi.js +1 -0
- package/dashboard/dist/assets/tS9mbYZi.js.br +0 -0
- package/dashboard/dist/assets/tS9mbYZi.js.gz +0 -0
- package/dashboard/dist/brand/anthropic-mark.svg.br +0 -0
- package/dashboard/dist/brand/anthropic-mark.svg.gz +0 -0
- package/dashboard/dist/brand/openai-mark.svg.br +0 -0
- package/dashboard/dist/brand/openai-mark.svg.gz +0 -0
- package/dashboard/dist/brand/openclaw-mark.svg.br +0 -0
- package/dashboard/dist/brand/openclaw-mark.svg.gz +0 -0
- package/dashboard/dist/brand/xandy-orchestrator.png +0 -0
- package/dashboard/dist/index.html +7 -5
- package/dashboard/dist/index.html.br +0 -0
- package/dashboard/dist/index.html.gz +0 -0
- package/dist/activity-actor-fields.js +26 -4
- package/dist/activity-store.js +38 -26
- package/dist/agent-context-store.js +84 -42
- package/dist/agent-run-store.js +49 -28
- package/dist/agent-suite.d.ts +9 -0
- package/dist/agent-suite.js +150 -17
- package/dist/artifacts/artifact-domain-schemas.d.ts +66 -0
- package/dist/artifacts/artifact-domain-schemas.js +357 -0
- package/dist/artifacts/register-artifact.d.ts +4 -3
- package/dist/artifacts/register-artifact.js +170 -57
- package/dist/auth/flows.d.ts +47 -0
- package/dist/auth/flows.js +169 -0
- package/dist/auth-store.js +6 -26
- package/dist/byok-store.js +5 -19
- package/dist/chat-store.d.ts +157 -0
- package/dist/chat-store.js +586 -0
- package/dist/cli/orgx.d.ts +66 -0
- package/dist/cli/orgx.js +102 -0
- package/dist/config/refresh.d.ts +32 -0
- package/dist/config/refresh.js +55 -0
- package/dist/config/resolution.d.ts +37 -0
- package/dist/config/resolution.js +178 -0
- package/dist/contracts/client.d.ts +43 -3
- package/dist/contracts/client.js +159 -30
- package/dist/contracts/retro-schema.d.ts +81 -0
- package/dist/contracts/retro-schema.js +80 -0
- package/dist/contracts/shared-types.d.ts +306 -0
- package/dist/contracts/shared-types.js +179 -0
- package/dist/contracts/skill-pack-schema.d.ts +192 -0
- package/dist/contracts/skill-pack-schema.js +180 -0
- package/dist/contracts/types.d.ts +224 -132
- package/dist/contracts/types.js +5 -0
- package/dist/entities/auto-assignment.d.ts +36 -0
- package/dist/entities/auto-assignment.js +141 -0
- package/dist/entity-comment-store.js +5 -25
- package/dist/event-sanitization.d.ts +11 -0
- package/dist/event-sanitization.js +113 -0
- package/dist/fs-utils.js +13 -1
- package/dist/gateway-watchdog.d.ts +5 -0
- package/dist/gateway-watchdog.js +50 -0
- package/dist/hash-utils.d.ts +2 -0
- package/dist/hash-utils.js +12 -0
- package/dist/hooks/post-reporting-event.mjs +1 -5
- package/dist/http/helpers/activity-headline.d.ts +10 -0
- package/dist/http/helpers/activity-headline.js +73 -0
- package/dist/http/helpers/artifact-fallback.d.ts +13 -0
- package/dist/http/helpers/artifact-fallback.js +148 -0
- package/dist/http/helpers/auto-continue-engine.d.ts +486 -0
- package/dist/http/helpers/auto-continue-engine.js +3563 -0
- package/dist/http/helpers/autopilot-operations.d.ts +176 -0
- package/dist/http/helpers/autopilot-operations.js +554 -0
- package/dist/http/helpers/autopilot-runtime.d.ts +43 -0
- package/dist/http/helpers/autopilot-runtime.js +607 -0
- package/dist/http/helpers/autopilot-slice-utils.d.ts +56 -0
- package/dist/http/helpers/autopilot-slice-utils.js +899 -0
- package/dist/http/helpers/decision-mapper.d.ts +52 -0
- package/dist/http/helpers/decision-mapper.js +260 -0
- package/dist/http/helpers/dispatch-lifecycle.d.ts +119 -0
- package/dist/http/helpers/dispatch-lifecycle.js +809 -0
- package/dist/http/helpers/hash-utils.d.ts +1 -0
- package/dist/http/helpers/hash-utils.js +1 -0
- package/dist/http/helpers/kickoff-context.d.ts +12 -0
- package/dist/http/helpers/kickoff-context.js +228 -0
- package/dist/http/helpers/llm-client.d.ts +47 -0
- package/dist/http/helpers/llm-client.js +256 -0
- package/dist/http/helpers/mission-control.d.ts +193 -0
- package/dist/http/helpers/mission-control.js +1383 -0
- package/dist/http/helpers/openclaw-cli.d.ts +37 -0
- package/dist/http/helpers/openclaw-cli.js +283 -0
- package/dist/http/helpers/runtime-sse.d.ts +20 -0
- package/dist/http/helpers/runtime-sse.js +110 -0
- package/dist/http/helpers/sentinel-catalog.d.ts +23 -0
- package/dist/http/helpers/sentinel-catalog.js +193 -0
- package/dist/http/helpers/session-classification.d.ts +9 -0
- package/dist/http/helpers/session-classification.js +564 -0
- package/dist/http/helpers/slice-experience-v2.d.ts +137 -0
- package/dist/http/helpers/slice-experience-v2.js +677 -0
- package/dist/http/helpers/slice-run-projections.d.ts +72 -0
- package/dist/http/helpers/slice-run-projections.js +860 -0
- package/dist/http/helpers/triage-mapper.d.ts +43 -0
- package/dist/http/helpers/triage-mapper.js +549 -0
- package/dist/http/helpers/value-utils.d.ts +6 -0
- package/dist/http/helpers/value-utils.js +72 -0
- package/dist/http/helpers/workspace-scope.d.ts +15 -0
- package/dist/http/helpers/workspace-scope.js +170 -0
- package/dist/http/index.d.ts +88 -0
- package/dist/http/index.js +3610 -0
- package/dist/http/router.d.ts +23 -0
- package/dist/http/router.js +23 -0
- package/dist/http/routes/agent-control.d.ts +79 -0
- package/dist/http/routes/agent-control.js +684 -0
- package/dist/http/routes/agent-suite.d.ts +38 -0
- package/dist/http/routes/agent-suite.js +397 -0
- package/dist/http/routes/agents-catalog.d.ts +40 -0
- package/dist/http/routes/agents-catalog.js +128 -0
- package/dist/http/routes/billing.d.ts +23 -0
- package/dist/http/routes/billing.js +55 -0
- package/dist/http/routes/chat.d.ts +19 -0
- package/dist/http/routes/chat.js +522 -0
- package/dist/http/routes/debug.d.ts +14 -0
- package/dist/http/routes/debug.js +21 -0
- package/dist/http/routes/decision-actions.d.ts +20 -0
- package/dist/http/routes/decision-actions.js +103 -0
- package/dist/http/routes/delegation.d.ts +19 -0
- package/dist/http/routes/delegation.js +32 -0
- package/dist/http/routes/dispatch-gateway-envelope.d.ts +25 -0
- package/dist/http/routes/dispatch-gateway-envelope.js +26 -0
- package/dist/http/routes/entities.d.ts +63 -0
- package/dist/http/routes/entities.js +440 -0
- package/dist/http/routes/entity-dynamic.d.ts +25 -0
- package/dist/http/routes/entity-dynamic.js +191 -0
- package/dist/http/routes/health.d.ts +22 -0
- package/dist/http/routes/health.js +49 -0
- package/dist/http/routes/live-legacy.d.ts +115 -0
- package/dist/http/routes/live-legacy.js +112 -0
- package/dist/http/routes/live-misc.d.ts +81 -0
- package/dist/http/routes/live-misc.js +426 -0
- package/dist/http/routes/live-snapshot.d.ts +136 -0
- package/dist/http/routes/live-snapshot.js +916 -0
- package/dist/http/routes/live-terminal.d.ts +11 -0
- package/dist/http/routes/live-terminal.js +261 -0
- package/dist/http/routes/live-triage.d.ts +61 -0
- package/dist/http/routes/live-triage.js +248 -0
- package/dist/http/routes/mission-control-actions.d.ts +131 -0
- package/dist/http/routes/mission-control-actions.js +1791 -0
- package/dist/http/routes/mission-control-read.d.ts +73 -0
- package/dist/http/routes/mission-control-read.js +1640 -0
- package/dist/http/routes/onboarding.d.ts +34 -0
- package/dist/http/routes/onboarding.js +101 -0
- package/dist/http/routes/realtime-orchestrator.d.ts +10 -0
- package/dist/http/routes/realtime-orchestrator.js +74 -0
- package/dist/http/routes/run-control.d.ts +27 -0
- package/dist/http/routes/run-control.js +96 -0
- package/dist/http/routes/runtime-hooks.d.ts +69 -0
- package/dist/http/routes/runtime-hooks.js +437 -0
- package/dist/http/routes/sentinels-catalog.d.ts +7 -0
- package/dist/http/routes/sentinels-catalog.js +24 -0
- package/dist/http/routes/settings-byok.d.ts +23 -0
- package/dist/http/routes/settings-byok.js +163 -0
- package/dist/http/routes/summary.d.ts +18 -0
- package/dist/http/routes/summary.js +49 -0
- package/dist/http/routes/usage.d.ts +24 -0
- package/dist/http/routes/usage.js +362 -0
- package/dist/http/routes/work-artifacts.d.ts +9 -0
- package/dist/http/routes/work-artifacts.js +55 -0
- package/dist/http/shared-state.d.ts +16 -0
- package/dist/http/shared-state.js +1 -0
- package/dist/http-handler.d.ts +1 -88
- package/dist/http-handler.js +1 -10605
- package/dist/index.js +287 -2284
- package/dist/json-utils.d.ts +1 -0
- package/dist/json-utils.js +8 -0
- package/dist/local-openclaw.js +29 -6
- package/dist/mcp-client-setup.js +3 -3
- package/dist/mcp-http-handler.js +33 -59
- package/dist/next-up-queue-store.d.ts +16 -1
- package/dist/next-up-queue-store.js +93 -25
- package/dist/outbox.d.ts +5 -0
- package/dist/outbox.js +113 -9
- package/dist/paths.js +24 -5
- package/dist/reporting/rollups.d.ts +53 -0
- package/dist/reporting/rollups.js +148 -0
- package/dist/retro/domain-templates.d.ts +45 -0
- package/dist/retro/domain-templates.js +297 -0
- package/dist/retro/quality-rubric.d.ts +33 -0
- package/dist/retro/quality-rubric.js +213 -0
- package/dist/runtime-cleanup.d.ts +18 -0
- package/dist/runtime-cleanup.js +87 -0
- package/dist/runtime-instance-store.js +5 -31
- package/dist/services/background.d.ts +34 -0
- package/dist/services/background.js +45 -0
- package/dist/services/experiment-randomization.d.ts +21 -0
- package/dist/services/experiment-randomization.js +63 -0
- package/dist/services/instrumentation.d.ts +29 -0
- package/dist/services/instrumentation.js +136 -0
- package/dist/skill-pack-state.d.ts +36 -5
- package/dist/skill-pack-state.js +273 -29
- package/dist/snapshot-store.js +5 -25
- package/dist/stores/json-store.d.ts +11 -0
- package/dist/stores/json-store.js +42 -0
- package/dist/sync/local-agent-telemetry.d.ts +13 -0
- package/dist/sync/local-agent-telemetry.js +128 -0
- package/dist/sync/outbox-replay.d.ts +55 -0
- package/dist/sync/outbox-replay.js +621 -0
- package/dist/team-context-store.d.ts +23 -0
- package/dist/team-context-store.js +116 -0
- package/dist/telemetry/posthog.js +4 -2
- package/dist/tools/core-tools.d.ts +72 -0
- package/dist/tools/core-tools.js +2270 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +2 -0
- package/dist/worker-supervisor.js +23 -0
- package/package.json +14 -4
- package/dashboard/dist/assets/B3ziCA02.js +0 -8
- package/dashboard/dist/assets/BNeJ0kpF.js +0 -1
- package/dashboard/dist/assets/BzkiMPmM.js +0 -215
- package/dashboard/dist/assets/CUV9IHHi.js +0 -1
- package/dashboard/dist/assets/Ie7d9Iq2.css +0 -1
- package/dashboard/dist/assets/sAhvFnpk.js +0 -4
|
@@ -0,0 +1,3610 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Handler — Serves the React dashboard SPA and API proxy endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Registered at the `/orgx` prefix. Handles:
|
|
5
|
+
* /orgx/live → dashboard SPA (index.html)
|
|
6
|
+
* /orgx/live/assets/* → static assets (JS, CSS, images)
|
|
7
|
+
* /orgx/api/status → org status summary
|
|
8
|
+
* /orgx/api/agents → agent states
|
|
9
|
+
* /orgx/api/activity → activity feed
|
|
10
|
+
* /orgx/api/initiatives → initiative data
|
|
11
|
+
* /orgx/api/health → plugin diagnostics + outbox/sync status
|
|
12
|
+
* /orgx/api/onboarding → onboarding / config state
|
|
13
|
+
* /orgx/api/agent-suite/status → suite provisioning plan (OpenClaw-local)
|
|
14
|
+
* /orgx/api/agent-suite/install → install/update suite (OpenClaw-local)
|
|
15
|
+
* /orgx/api/delegation/preflight → delegation preflight
|
|
16
|
+
* /orgx/api/runs/:id/checkpoints → list/create checkpoints
|
|
17
|
+
* /orgx/api/runs/:id/checkpoints/:checkpointId/restore → restore checkpoint
|
|
18
|
+
* /orgx/api/runs/:id/actions/:action → run control action
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync, existsSync, readdirSync, statSync, openSync, readSync, closeSync, } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { join, dirname, extname, normalize, resolve, relative, sep } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { randomUUID } from "node:crypto";
|
|
25
|
+
import { readNextUpQueuePins, removeNextUpQueuePin, setNextUpQueuePinOrder, suppressNextUpQueueItem, upsertNextUpQueuePin, } from "../next-up-queue-store.js";
|
|
26
|
+
import { formatStatus, formatAgents, formatActivity, formatInitiatives, getOnboardingState, } from "../dashboard-api.js";
|
|
27
|
+
import { loadLocalOpenClawSnapshot, loadLocalTurnDetail, toLocalLiveActivity, toLocalLiveAgents, toLocalLiveInitiatives, toLocalSessionTree, } from "../local-openclaw.js";
|
|
28
|
+
import { defaultOutboxAdapter } from "../adapters/outbox.js";
|
|
29
|
+
import { readAgentContexts, upsertAgentContext, upsertRunContext } from "../agent-context-store.js";
|
|
30
|
+
import { getAgentRun, markAgentRunStopped, readAgentRuns, upsertAgentRun, } from "../agent-run-store.js";
|
|
31
|
+
import { appendEntityComment, listEntityComments, mergeEntityComments, } from "../entity-comment-store.js";
|
|
32
|
+
import { listChatThreads } from "../chat-store.js";
|
|
33
|
+
import { appendActivityItems, listActivityPage, } from "../activity-store.js";
|
|
34
|
+
import { enrichActivityActorFields } from "../activity-actor-fields.js";
|
|
35
|
+
import { readByokKeys, writeByokKeys } from "../byok-store.js";
|
|
36
|
+
import { applyOrgxAgentSuitePlan, computeOrgxAgentSuitePlan, generateAgentSuiteOperationId, } from "../agent-suite.js";
|
|
37
|
+
import { listRuntimeInstances, resolveRuntimeHookToken, upsertRuntimeInstanceFromHook, } from "../runtime-instance-store.js";
|
|
38
|
+
import { parseJsonSafe } from "../json-utils.js";
|
|
39
|
+
import { readSkillPackState, refreshSkillPackState, rollbackSkillPackPolicy, updateSkillPackPolicy, } from "../skill-pack-state.js";
|
|
40
|
+
import { posthogCapture } from "../telemetry/posthog.js";
|
|
41
|
+
import { createRouter } from "./router.js";
|
|
42
|
+
import { summarizeActivityHeadline } from "./helpers/activity-headline.js";
|
|
43
|
+
import { createAutoContinueEngine, } from "./helpers/auto-continue-engine.js";
|
|
44
|
+
import { createAutopilotOperations, } from "./helpers/autopilot-operations.js";
|
|
45
|
+
import { mapDecisionEntity } from "./helpers/decision-mapper.js";
|
|
46
|
+
import { idempotencyKey, stableHash } from "./helpers/hash-utils.js";
|
|
47
|
+
import { createCodexBinResolver, } from "./helpers/autopilot-slice-utils.js";
|
|
48
|
+
import { createLocalArtifactDetailFallbackBuilder } from "./helpers/artifact-fallback.js";
|
|
49
|
+
import { buildMissionControlGraph, deriveExecutionPolicy, dedupeStrings, isDispatchableWorkstreamStatus, isDoneStatus, isInProgressStatus, isTodoStatus, listEntitiesSafe, normalizeEntityMutationPayload, pickStringArray, resolveAutoAssignments, selectSliceTasksByScope, } from "./helpers/mission-control.js";
|
|
50
|
+
import { configureOpenClawProviderRouting, fetchBillingStatusSafe, isPidAlive, listOpenClawAgents, listOpenClawProviderModels, modelImpliesByok, normalizeOpenClawProvider, resolveAutoOpenClawProvider, resolveByokEnvOverrides, spawnOpenClawAgentTurn, stopDetachedProcess, } from "./helpers/openclaw-cli.js";
|
|
51
|
+
import { fetchKickoffContextSafe, renderKickoffMessage } from "./helpers/kickoff-context.js";
|
|
52
|
+
import { createDispatchLifecycle } from "./helpers/dispatch-lifecycle.js";
|
|
53
|
+
import { createRuntimeSseHub } from "./helpers/runtime-sse.js";
|
|
54
|
+
import { parseBooleanQuery, parsePositiveInt, pickHeaderString, pickNumber, pickString, } from "./helpers/value-utils.js";
|
|
55
|
+
import { registerAgentControlRoutes } from "./routes/agent-control.js";
|
|
56
|
+
import { registerAgentSuiteRoutes } from "./routes/agent-suite.js";
|
|
57
|
+
import { registerAgentsCatalogRoutes } from "./routes/agents-catalog.js";
|
|
58
|
+
import { registerBillingRoutes } from "./routes/billing.js";
|
|
59
|
+
import { registerDecisionActionsRoutes } from "./routes/decision-actions.js";
|
|
60
|
+
import { registerDelegationRoutes } from "./routes/delegation.js";
|
|
61
|
+
import { registerDebugRoutes } from "./routes/debug.js";
|
|
62
|
+
import { registerEntityDynamicRoutes } from "./routes/entity-dynamic.js";
|
|
63
|
+
import { registerEntitiesRoutes } from "./routes/entities.js";
|
|
64
|
+
import { registerHealthRoutes } from "./routes/health.js";
|
|
65
|
+
import { registerChatRoutes } from "./routes/chat.js";
|
|
66
|
+
import { registerLiveLegacyRoutes } from "./routes/live-legacy.js";
|
|
67
|
+
import { registerLiveMiscRoutes } from "./routes/live-misc.js";
|
|
68
|
+
import { registerLiveTerminalRoutes } from "./routes/live-terminal.js";
|
|
69
|
+
import { registerLiveSnapshotRoutes } from "./routes/live-snapshot.js";
|
|
70
|
+
import { registerMissionControlActionsRoutes } from "./routes/mission-control-actions.js";
|
|
71
|
+
import { registerMissionControlReadRoutes } from "./routes/mission-control-read.js";
|
|
72
|
+
import { registerOnboardingRoutes } from "./routes/onboarding.js";
|
|
73
|
+
import { registerRunControlRoutes } from "./routes/run-control.js";
|
|
74
|
+
import { registerRuntimeHookRoutes } from "./routes/runtime-hooks.js";
|
|
75
|
+
import { registerSentinelsCatalogRoutes } from "./routes/sentinels-catalog.js";
|
|
76
|
+
import { registerSettingsByokRoutes } from "./routes/settings-byok.js";
|
|
77
|
+
import { registerSummaryRoutes } from "./routes/summary.js";
|
|
78
|
+
import { registerUsageRoutes } from "./routes/usage.js";
|
|
79
|
+
import { registerWorkArtifactsRoutes } from "./routes/work-artifacts.js";
|
|
80
|
+
import { registerLiveTriageRoutes } from "./routes/live-triage.js";
|
|
81
|
+
import { registerRealtimeOrchestratorRoutes } from "./routes/realtime-orchestrator.js";
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// Helpers
|
|
84
|
+
// =============================================================================
|
|
85
|
+
async function resolveSkillPackOverrides(input) {
|
|
86
|
+
const state = readSkillPackState();
|
|
87
|
+
const force = Boolean(input.force);
|
|
88
|
+
if (!force && state.overrides)
|
|
89
|
+
return state.overrides;
|
|
90
|
+
const getSkillPack = input.client.getSkillPack;
|
|
91
|
+
if (typeof getSkillPack !== "function")
|
|
92
|
+
return state.overrides;
|
|
93
|
+
try {
|
|
94
|
+
const refreshed = await refreshSkillPackState({
|
|
95
|
+
getSkillPack: (args) => getSkillPack(args),
|
|
96
|
+
force,
|
|
97
|
+
});
|
|
98
|
+
return refreshed.state.overrides;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// If refresh fails (network, disk, etc.), fall back to cached overrides.
|
|
102
|
+
return state.overrides;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function safeErrorMessage(err) {
|
|
106
|
+
const raw = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
107
|
+
const normalized = raw.trim().toLowerCase();
|
|
108
|
+
if (normalized.length > 0) {
|
|
109
|
+
if (normalized.includes("signal is aborted") ||
|
|
110
|
+
normalized.includes("aborterror") ||
|
|
111
|
+
normalized.includes("request cancelled") ||
|
|
112
|
+
normalized.includes("request canceled")) {
|
|
113
|
+
return "request timed out before upstream completed";
|
|
114
|
+
}
|
|
115
|
+
if (normalized.includes("timed out") || normalized.includes("timeout")) {
|
|
116
|
+
return "request timed out before upstream completed";
|
|
117
|
+
}
|
|
118
|
+
if (normalized.includes("failed to fetch") || normalized.includes("network")) {
|
|
119
|
+
return "network request failed";
|
|
120
|
+
}
|
|
121
|
+
return raw;
|
|
122
|
+
}
|
|
123
|
+
return "Unexpected error";
|
|
124
|
+
}
|
|
125
|
+
function titleCaseFromSlug(value) {
|
|
126
|
+
const parts = value
|
|
127
|
+
.split(/[^a-z0-9]+/i)
|
|
128
|
+
.map((part) => part.trim())
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
if (parts.length === 0)
|
|
131
|
+
return value;
|
|
132
|
+
return parts
|
|
133
|
+
.map((part) => `${part[0].toUpperCase()}${part.slice(1).toLowerCase()}`)
|
|
134
|
+
.join(" ");
|
|
135
|
+
}
|
|
136
|
+
function resolveOrgxAgentForDomain(domain) {
|
|
137
|
+
const normalized = domain.trim().toLowerCase();
|
|
138
|
+
if (!normalized)
|
|
139
|
+
return { id: "orgx", name: "OrgX" };
|
|
140
|
+
// Execution policies sometimes call this "orchestration" but the agent id is "orgx-orchestrator".
|
|
141
|
+
const slug = normalized === "orchestration" ? "orchestrator" : normalized;
|
|
142
|
+
// If the domain already looks like an OrgX agent id, keep it stable.
|
|
143
|
+
if (slug === "orgx")
|
|
144
|
+
return { id: "orgx", name: "OrgX" };
|
|
145
|
+
if (slug.startsWith("orgx-"))
|
|
146
|
+
return { id: slug, name: `OrgX ${titleCaseFromSlug(slug.slice(5))}` };
|
|
147
|
+
return { id: `orgx-${slug}`, name: `OrgX ${titleCaseFromSlug(slug)}` };
|
|
148
|
+
}
|
|
149
|
+
function isUnauthorizedOrgxError(err) {
|
|
150
|
+
const message = safeErrorMessage(err).toLowerCase();
|
|
151
|
+
return message.includes("401") || message.includes("unauthorized");
|
|
152
|
+
}
|
|
153
|
+
function readPositiveIntEnv(name, fallback, bounds) {
|
|
154
|
+
const raw = process.env[name];
|
|
155
|
+
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
156
|
+
return fallback;
|
|
157
|
+
const parsed = Number(raw);
|
|
158
|
+
if (!Number.isFinite(parsed))
|
|
159
|
+
return fallback;
|
|
160
|
+
const clamped = Math.floor(parsed);
|
|
161
|
+
if (typeof bounds?.min === "number" && clamped < bounds.min)
|
|
162
|
+
return fallback;
|
|
163
|
+
if (typeof bounds?.max === "number" && clamped > bounds.max)
|
|
164
|
+
return fallback;
|
|
165
|
+
return clamped;
|
|
166
|
+
}
|
|
167
|
+
async function withSoftTimeout(label, timeoutMs, work) {
|
|
168
|
+
let timer = null;
|
|
169
|
+
try {
|
|
170
|
+
return await Promise.race([
|
|
171
|
+
work,
|
|
172
|
+
new Promise((_, reject) => {
|
|
173
|
+
timer = setTimeout(() => {
|
|
174
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
175
|
+
}, timeoutMs);
|
|
176
|
+
}),
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
if (timer)
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
185
|
+
if (items.length === 0)
|
|
186
|
+
return [];
|
|
187
|
+
const limit = Math.max(1, Math.floor(concurrency));
|
|
188
|
+
const results = new Array(items.length);
|
|
189
|
+
let cursor = 0;
|
|
190
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
191
|
+
while (true) {
|
|
192
|
+
const index = cursor;
|
|
193
|
+
cursor += 1;
|
|
194
|
+
if (index >= items.length)
|
|
195
|
+
return;
|
|
196
|
+
results[index] = await mapper(items[index], index);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
await Promise.all(workers);
|
|
200
|
+
return results;
|
|
201
|
+
}
|
|
202
|
+
const ACTIVITY_WARM_THROTTLE_MS = 30_000;
|
|
203
|
+
const activityWarmByKey = new Map();
|
|
204
|
+
const SNAPSHOT_RESPONSE_CACHE_TTL_MS = 1_500;
|
|
205
|
+
const SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES = 16;
|
|
206
|
+
const SNAPSHOT_ACTIVITY_PERSIST_MIN_INTERVAL_MS = 15_000;
|
|
207
|
+
const SNAPSHOT_ACTIVITY_FINGERPRINT_DEPTH = 8;
|
|
208
|
+
const NEXT_UP_QUEUE_CACHE_TTL_MS = readPositiveIntEnv("ORGX_NEXT_UP_QUEUE_CACHE_TTL_MS", 30_000, { min: 250, max: 120_000 });
|
|
209
|
+
const NEXT_UP_QUEUE_STALE_TTL_MS = readPositiveIntEnv("ORGX_NEXT_UP_QUEUE_STALE_TTL_MS", 45_000, { min: 1_000, max: 600_000 });
|
|
210
|
+
const NEXT_UP_GRAPH_CONCURRENCY = readPositiveIntEnv("ORGX_NEXT_UP_GRAPH_CONCURRENCY", 20, { min: 1, max: 32 });
|
|
211
|
+
const NEXT_UP_LIVE_AGENTS_TIMEOUT_MS = readPositiveIntEnv("ORGX_NEXT_UP_LIVE_AGENTS_TIMEOUT_MS", 1_500, { min: 200, max: 20_000 });
|
|
212
|
+
const NEXT_UP_AGENT_CATALOG_TIMEOUT_MS = readPositiveIntEnv("ORGX_NEXT_UP_AGENT_CATALOG_TIMEOUT_MS", 900, { min: 100, max: 20_000 });
|
|
213
|
+
const NEXT_UP_LIVE_SESSIONS_TIMEOUT_MS = readPositiveIntEnv("ORGX_NEXT_UP_LIVE_SESSIONS_TIMEOUT_MS", 2_500, { min: 250, max: 30_000 });
|
|
214
|
+
const PROJECT_SCOPE_LOOKUP_TIMEOUT_MS = readPositiveIntEnv("ORGX_PROJECT_SCOPE_LOOKUP_TIMEOUT_MS", 2_500, { min: 250, max: 20_000 });
|
|
215
|
+
const PROJECT_SCOPE_MAX_INITIATIVE_PAGES = readPositiveIntEnv("ORGX_PROJECT_SCOPE_MAX_INITIATIVE_PAGES", 12, { min: 1, max: 100 });
|
|
216
|
+
const LIVE_WORKSPACE_INITIATIVE_STATUSES = [
|
|
217
|
+
"active",
|
|
218
|
+
"planning",
|
|
219
|
+
"paused",
|
|
220
|
+
"draft",
|
|
221
|
+
"in_progress",
|
|
222
|
+
];
|
|
223
|
+
let lastSnapshotActivityPersistAt = 0;
|
|
224
|
+
let lastSnapshotActivityFingerprint = "";
|
|
225
|
+
const snapshotResponseCache = new Map();
|
|
226
|
+
const ACTIVITY_DECISION_EVENT_HINTS = new Set([
|
|
227
|
+
"decision_buffered",
|
|
228
|
+
"auto_continue_spawn_guard_blocked",
|
|
229
|
+
"autopilot_slice_mcp_handshake_failed",
|
|
230
|
+
"autopilot_slice_timeout",
|
|
231
|
+
"autopilot_slice_log_stall",
|
|
232
|
+
]);
|
|
233
|
+
const ACTIVITY_ARTIFACT_EVENT_HINTS = new Set([
|
|
234
|
+
"autopilot_slice_artifact_buffered",
|
|
235
|
+
]);
|
|
236
|
+
function normalizeActivityBucket(value) {
|
|
237
|
+
if (typeof value !== "string")
|
|
238
|
+
return null;
|
|
239
|
+
const normalized = value.trim().toLowerCase();
|
|
240
|
+
if (normalized === "artifact")
|
|
241
|
+
return "artifact";
|
|
242
|
+
if (normalized === "decision")
|
|
243
|
+
return "decision";
|
|
244
|
+
if (normalized === "message")
|
|
245
|
+
return "message";
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
function activityMetadataBoolean(metadata, keys) {
|
|
249
|
+
if (!metadata)
|
|
250
|
+
return null;
|
|
251
|
+
for (const key of keys) {
|
|
252
|
+
const value = metadata[key];
|
|
253
|
+
if (typeof value === "boolean")
|
|
254
|
+
return value;
|
|
255
|
+
if (typeof value === "string") {
|
|
256
|
+
const normalized = value.trim().toLowerCase();
|
|
257
|
+
if (normalized === "true")
|
|
258
|
+
return true;
|
|
259
|
+
if (normalized === "false")
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
function activityMetadataNumber(metadata, keys) {
|
|
266
|
+
if (!metadata)
|
|
267
|
+
return null;
|
|
268
|
+
for (const key of keys) {
|
|
269
|
+
const value = metadata[key];
|
|
270
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
271
|
+
return Math.max(0, value);
|
|
272
|
+
}
|
|
273
|
+
if (typeof value === "string") {
|
|
274
|
+
const parsed = Number(value);
|
|
275
|
+
if (Number.isFinite(parsed)) {
|
|
276
|
+
return Math.max(0, parsed);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
function activityMetadataEventName(metadata) {
|
|
283
|
+
if (!metadata)
|
|
284
|
+
return null;
|
|
285
|
+
const raw = metadata.event;
|
|
286
|
+
if (typeof raw !== "string")
|
|
287
|
+
return null;
|
|
288
|
+
const normalized = raw.trim().toLowerCase();
|
|
289
|
+
return normalized.length > 0 ? normalized : null;
|
|
290
|
+
}
|
|
291
|
+
function deriveStructuredActivityBucket(input) {
|
|
292
|
+
const metadata = input.metadata;
|
|
293
|
+
const explicit = normalizeActivityBucket(input.explicitBucket) ??
|
|
294
|
+
normalizeActivityBucket(metadata?.activity_bucket) ??
|
|
295
|
+
normalizeActivityBucket(metadata?.activityBucket) ??
|
|
296
|
+
normalizeActivityBucket(metadata?.bucket) ??
|
|
297
|
+
null;
|
|
298
|
+
if (explicit)
|
|
299
|
+
return explicit;
|
|
300
|
+
const event = activityMetadataEventName(metadata);
|
|
301
|
+
const decisionRequired = activityMetadataBoolean(metadata, ["decision_required", "decisionRequired"]) === true;
|
|
302
|
+
const artifacts = activityMetadataNumber(metadata, ["artifacts", "artifact_count", "artifactCount"]) ?? 0;
|
|
303
|
+
const decisions = activityMetadataNumber(metadata, ["decisions", "decision_count", "decisionCount"]) ?? 0;
|
|
304
|
+
const blockingDecisions = activityMetadataNumber(metadata, [
|
|
305
|
+
"blocking_decisions",
|
|
306
|
+
"blockingDecisions",
|
|
307
|
+
"blocking_decision_count",
|
|
308
|
+
"blockingDecisionCount",
|
|
309
|
+
]) ?? 0;
|
|
310
|
+
const nonBlockingDecisions = activityMetadataNumber(metadata, [
|
|
311
|
+
"non_blocking_decisions",
|
|
312
|
+
"nonBlockingDecisions",
|
|
313
|
+
"non_blocking_decision_count",
|
|
314
|
+
"nonBlockingDecisionCount",
|
|
315
|
+
]) ?? 0;
|
|
316
|
+
if (event === "autopilot_slice_result") {
|
|
317
|
+
// Any blocked slice result needs decision-first surfacing in the Activity UX.
|
|
318
|
+
if (input.phase === "blocked")
|
|
319
|
+
return "decision";
|
|
320
|
+
if (decisionRequired || blockingDecisions > 0)
|
|
321
|
+
return "decision";
|
|
322
|
+
if (artifacts > 0)
|
|
323
|
+
return "artifact";
|
|
324
|
+
if (decisions > 0 || nonBlockingDecisions > 0)
|
|
325
|
+
return "decision";
|
|
326
|
+
return "message";
|
|
327
|
+
}
|
|
328
|
+
if (event === "auto_continue_stopped") {
|
|
329
|
+
const stopReason = typeof metadata?.stop_reason === "string"
|
|
330
|
+
? metadata.stop_reason.trim().toLowerCase()
|
|
331
|
+
: "";
|
|
332
|
+
if (stopReason === "blocked" || stopReason === "error")
|
|
333
|
+
return "decision";
|
|
334
|
+
}
|
|
335
|
+
if (event && ACTIVITY_ARTIFACT_EVENT_HINTS.has(event))
|
|
336
|
+
return "artifact";
|
|
337
|
+
if (event && ACTIVITY_DECISION_EVENT_HINTS.has(event))
|
|
338
|
+
return "decision";
|
|
339
|
+
const hasArtifactReference = typeof metadata?.artifact_id === "string" ||
|
|
340
|
+
typeof metadata?.artifactId === "string" ||
|
|
341
|
+
typeof metadata?.work_artifact_id === "string";
|
|
342
|
+
if (hasArtifactReference || artifacts > 0)
|
|
343
|
+
return "artifact";
|
|
344
|
+
if (decisionRequired || blockingDecisions > 0 || decisions > 0 || nonBlockingDecisions > 0) {
|
|
345
|
+
return "decision";
|
|
346
|
+
}
|
|
347
|
+
return "message";
|
|
348
|
+
}
|
|
349
|
+
function snapshotActivityFingerprint(items) {
|
|
350
|
+
if (!Array.isArray(items) || items.length === 0)
|
|
351
|
+
return "0";
|
|
352
|
+
const sample = items
|
|
353
|
+
.slice(0, SNAPSHOT_ACTIVITY_FINGERPRINT_DEPTH)
|
|
354
|
+
.map((item) => `${item.id}|${item.timestamp}`)
|
|
355
|
+
.join(";");
|
|
356
|
+
return `${items.length}:${sample}`;
|
|
357
|
+
}
|
|
358
|
+
function readSnapshotResponseCache(key) {
|
|
359
|
+
const entry = snapshotResponseCache.get(key);
|
|
360
|
+
if (!entry)
|
|
361
|
+
return null;
|
|
362
|
+
if (entry.expiresAt <= Date.now()) {
|
|
363
|
+
snapshotResponseCache.delete(key);
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
return entry.payload;
|
|
367
|
+
}
|
|
368
|
+
function writeSnapshotResponseCache(key, payload) {
|
|
369
|
+
const now = Date.now();
|
|
370
|
+
snapshotResponseCache.set(key, {
|
|
371
|
+
expiresAt: now + SNAPSHOT_RESPONSE_CACHE_TTL_MS,
|
|
372
|
+
payload,
|
|
373
|
+
});
|
|
374
|
+
if (snapshotResponseCache.size <= SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES)
|
|
375
|
+
return;
|
|
376
|
+
for (const [cachedKey, entry] of snapshotResponseCache.entries()) {
|
|
377
|
+
if (entry.expiresAt <= now)
|
|
378
|
+
snapshotResponseCache.delete(cachedKey);
|
|
379
|
+
}
|
|
380
|
+
while (snapshotResponseCache.size > SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES) {
|
|
381
|
+
const oldestKey = snapshotResponseCache.keys().next().value;
|
|
382
|
+
if (!oldestKey)
|
|
383
|
+
break;
|
|
384
|
+
snapshotResponseCache.delete(oldestKey);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function clearSnapshotResponseCache() {
|
|
388
|
+
snapshotResponseCache.clear();
|
|
389
|
+
}
|
|
390
|
+
function isUserScopedApiKey(apiKey) {
|
|
391
|
+
return apiKey.trim().toLowerCase().startsWith("oxk_");
|
|
392
|
+
}
|
|
393
|
+
const buildLocalArtifactDetailFallback = createLocalArtifactDetailFallbackBuilder({
|
|
394
|
+
listActivityPage: ({ limit, cursor }) => listActivityPage({ limit, cursor }),
|
|
395
|
+
});
|
|
396
|
+
function maskSecret(value) {
|
|
397
|
+
if (!value)
|
|
398
|
+
return null;
|
|
399
|
+
const trimmed = value.trim();
|
|
400
|
+
if (!trimmed)
|
|
401
|
+
return null;
|
|
402
|
+
if (trimmed.length <= 8)
|
|
403
|
+
return `${trimmed[0]}…${trimmed.slice(-1)}`;
|
|
404
|
+
return `${trimmed.slice(0, 4)}…${trimmed.slice(-4)}`;
|
|
405
|
+
}
|
|
406
|
+
const { runtimeStreamSubscribers, writeRuntimeSseEvent, stopRuntimeStreamTimers, broadcastRuntimeSse, ensureRuntimeStreamTimers, } = createRuntimeSseHub({
|
|
407
|
+
listRuntimeInstances: ({ limit }) => listRuntimeInstances({ limit }),
|
|
408
|
+
});
|
|
409
|
+
function getScopedAgentIds(contexts) {
|
|
410
|
+
const scoped = new Set();
|
|
411
|
+
for (const [key, ctx] of Object.entries(contexts)) {
|
|
412
|
+
if (!ctx || typeof ctx !== "object")
|
|
413
|
+
continue;
|
|
414
|
+
const agentId = (ctx.agentId ?? key).trim();
|
|
415
|
+
if (!agentId)
|
|
416
|
+
continue;
|
|
417
|
+
const initiativeId = ctx.initiativeId?.trim() ?? "";
|
|
418
|
+
if (initiativeId) {
|
|
419
|
+
scoped.add(agentId);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return scoped;
|
|
423
|
+
}
|
|
424
|
+
function isUuidLike(value) {
|
|
425
|
+
const trimmed = (value ?? "").trim();
|
|
426
|
+
if (!trimmed)
|
|
427
|
+
return false;
|
|
428
|
+
// Accept any RFC 4122 UUID (v1-v5). We use this to distinguish real OrgX
|
|
429
|
+
// initiative ids from local placeholder group ids like "agent:main".
|
|
430
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed);
|
|
431
|
+
}
|
|
432
|
+
function applyAgentContextsToSessionTree(input, contexts) {
|
|
433
|
+
if (!input || !Array.isArray(input.nodes))
|
|
434
|
+
return input;
|
|
435
|
+
const groupsById = new Map();
|
|
436
|
+
for (const group of input.groups ?? []) {
|
|
437
|
+
if (!group)
|
|
438
|
+
continue;
|
|
439
|
+
groupsById.set(group.id, {
|
|
440
|
+
id: group.id,
|
|
441
|
+
label: group.label,
|
|
442
|
+
status: group.status ?? null,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const nodes = input.nodes.map((node) => {
|
|
446
|
+
const existingInitiativeId = (node.initiativeId ?? "").trim();
|
|
447
|
+
if (isUuidLike(existingInitiativeId))
|
|
448
|
+
return node;
|
|
449
|
+
const runCtx = node.runId ? contexts.runs[node.runId] : null;
|
|
450
|
+
if (runCtx && runCtx.initiativeId && runCtx.initiativeId.trim().length > 0) {
|
|
451
|
+
const initiativeId = runCtx.initiativeId.trim();
|
|
452
|
+
const groupId = initiativeId;
|
|
453
|
+
const ctxTitle = (runCtx.initiativeTitle ?? "").trim();
|
|
454
|
+
const groupLabel = ctxTitle || node.groupLabel || initiativeId;
|
|
455
|
+
const existing = groupsById.get(groupId);
|
|
456
|
+
if (!existing) {
|
|
457
|
+
groupsById.set(groupId, {
|
|
458
|
+
id: groupId,
|
|
459
|
+
label: groupLabel,
|
|
460
|
+
status: node.status ?? null,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
else if (ctxTitle && (existing.label === groupId || existing.label.startsWith("Agent "))) {
|
|
464
|
+
groupsById.set(groupId, { ...existing, label: groupLabel });
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
...node,
|
|
468
|
+
initiativeId,
|
|
469
|
+
workstreamId: runCtx.workstreamId ?? node.workstreamId ?? null,
|
|
470
|
+
groupId,
|
|
471
|
+
groupLabel,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const agentId = node.agentId?.trim() ?? "";
|
|
475
|
+
if (!agentId)
|
|
476
|
+
return node;
|
|
477
|
+
const ctx = contexts.agents[agentId];
|
|
478
|
+
const initiativeId = ctx?.initiativeId?.trim() ?? "";
|
|
479
|
+
if (!initiativeId)
|
|
480
|
+
return node;
|
|
481
|
+
const groupId = initiativeId;
|
|
482
|
+
const ctxTitle = ctx.initiativeTitle?.trim() ?? "";
|
|
483
|
+
const groupLabel = ctxTitle || node.groupLabel || initiativeId;
|
|
484
|
+
const existing = groupsById.get(groupId);
|
|
485
|
+
if (!existing) {
|
|
486
|
+
groupsById.set(groupId, {
|
|
487
|
+
id: groupId,
|
|
488
|
+
label: groupLabel,
|
|
489
|
+
status: node.status ?? null,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
else if (ctxTitle && (existing.label === groupId || existing.label.startsWith("Agent "))) {
|
|
493
|
+
groupsById.set(groupId, { ...existing, label: groupLabel });
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
...node,
|
|
497
|
+
initiativeId,
|
|
498
|
+
workstreamId: ctx.workstreamId ?? node.workstreamId ?? null,
|
|
499
|
+
groupId,
|
|
500
|
+
groupLabel,
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
// Ensure every node's group exists.
|
|
504
|
+
for (const node of nodes) {
|
|
505
|
+
if (!groupsById.has(node.groupId)) {
|
|
506
|
+
groupsById.set(node.groupId, {
|
|
507
|
+
id: node.groupId,
|
|
508
|
+
label: node.groupLabel || node.groupId,
|
|
509
|
+
status: node.status ?? null,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
...input,
|
|
515
|
+
nodes,
|
|
516
|
+
groups: Array.from(groupsById.values()),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function applyAgentContextsToActivity(input, contexts) {
|
|
520
|
+
if (!Array.isArray(input))
|
|
521
|
+
return [];
|
|
522
|
+
return input.map((item) => {
|
|
523
|
+
let nextItem = item;
|
|
524
|
+
const existingInitiativeId = (item.initiativeId ?? "").trim();
|
|
525
|
+
if (!isUuidLike(existingInitiativeId)) {
|
|
526
|
+
const runCtx = item.runId ? contexts.runs[item.runId] : null;
|
|
527
|
+
if (runCtx && runCtx.initiativeId && runCtx.initiativeId.trim().length > 0) {
|
|
528
|
+
const initiativeId = runCtx.initiativeId.trim();
|
|
529
|
+
const metadata = item.metadata && typeof item.metadata === "object"
|
|
530
|
+
? { ...item.metadata }
|
|
531
|
+
: {};
|
|
532
|
+
metadata.orgx_context = {
|
|
533
|
+
initiativeId,
|
|
534
|
+
workstreamId: runCtx.workstreamId ?? null,
|
|
535
|
+
taskId: runCtx.taskId ?? null,
|
|
536
|
+
updatedAt: runCtx.updatedAt,
|
|
537
|
+
};
|
|
538
|
+
nextItem = {
|
|
539
|
+
...item,
|
|
540
|
+
initiativeId,
|
|
541
|
+
metadata,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
const agentId = item.agentId?.trim() ?? "";
|
|
546
|
+
if (agentId) {
|
|
547
|
+
const ctx = contexts.agents[agentId];
|
|
548
|
+
const initiativeId = ctx?.initiativeId?.trim() ?? "";
|
|
549
|
+
if (initiativeId) {
|
|
550
|
+
const metadata = item.metadata && typeof item.metadata === "object"
|
|
551
|
+
? { ...item.metadata }
|
|
552
|
+
: {};
|
|
553
|
+
metadata.orgx_context = {
|
|
554
|
+
initiativeId,
|
|
555
|
+
workstreamId: ctx.workstreamId ?? null,
|
|
556
|
+
taskId: ctx.taskId ?? null,
|
|
557
|
+
updatedAt: ctx.updatedAt,
|
|
558
|
+
};
|
|
559
|
+
nextItem = {
|
|
560
|
+
...item,
|
|
561
|
+
initiativeId,
|
|
562
|
+
metadata,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return enrichActivityActorFields(nextItem);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
function mergeSessionTrees(base, extra) {
|
|
572
|
+
const seenNodes = new Set();
|
|
573
|
+
const nodes = [];
|
|
574
|
+
for (const node of base.nodes ?? []) {
|
|
575
|
+
seenNodes.add(node.id);
|
|
576
|
+
nodes.push(node);
|
|
577
|
+
}
|
|
578
|
+
for (const node of extra.nodes ?? []) {
|
|
579
|
+
if (seenNodes.has(node.id))
|
|
580
|
+
continue;
|
|
581
|
+
seenNodes.add(node.id);
|
|
582
|
+
nodes.push(node);
|
|
583
|
+
}
|
|
584
|
+
const seenEdges = new Set();
|
|
585
|
+
const edges = [];
|
|
586
|
+
for (const edge of base.edges ?? []) {
|
|
587
|
+
const key = `${edge.parentId}→${edge.childId}`;
|
|
588
|
+
seenEdges.add(key);
|
|
589
|
+
edges.push(edge);
|
|
590
|
+
}
|
|
591
|
+
for (const edge of extra.edges ?? []) {
|
|
592
|
+
const key = `${edge.parentId}→${edge.childId}`;
|
|
593
|
+
if (seenEdges.has(key))
|
|
594
|
+
continue;
|
|
595
|
+
seenEdges.add(key);
|
|
596
|
+
edges.push(edge);
|
|
597
|
+
}
|
|
598
|
+
const groupsById = new Map();
|
|
599
|
+
for (const group of base.groups ?? []) {
|
|
600
|
+
groupsById.set(group.id, group);
|
|
601
|
+
}
|
|
602
|
+
for (const group of extra.groups ?? []) {
|
|
603
|
+
const existing = groupsById.get(group.id);
|
|
604
|
+
if (!existing) {
|
|
605
|
+
groupsById.set(group.id, group);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const nextLabel = existing.label === existing.id && group.label && group.label !== group.id
|
|
609
|
+
? group.label
|
|
610
|
+
: existing.label;
|
|
611
|
+
groupsById.set(group.id, { ...existing, label: nextLabel });
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
nodes,
|
|
615
|
+
edges,
|
|
616
|
+
groups: Array.from(groupsById.values()),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function mergeActivities(base, extra, limit) {
|
|
620
|
+
const asMetadataRecord = (value) => {
|
|
621
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
622
|
+
return null;
|
|
623
|
+
return value;
|
|
624
|
+
};
|
|
625
|
+
const metadataString = (metadata, keys) => {
|
|
626
|
+
if (!metadata)
|
|
627
|
+
return null;
|
|
628
|
+
for (const key of keys) {
|
|
629
|
+
const value = metadata[key];
|
|
630
|
+
if (typeof value !== "string")
|
|
631
|
+
continue;
|
|
632
|
+
const normalized = value.trim();
|
|
633
|
+
if (normalized.length > 0)
|
|
634
|
+
return normalized;
|
|
635
|
+
}
|
|
636
|
+
return null;
|
|
637
|
+
};
|
|
638
|
+
const semanticEvents = new Set([
|
|
639
|
+
"autopilot_slice_result",
|
|
640
|
+
"auto_continue_started",
|
|
641
|
+
"auto_continue_stopped",
|
|
642
|
+
"next_up_manual_dispatch_started",
|
|
643
|
+
"autopilot_slice_mcp_handshake_failed",
|
|
644
|
+
"autopilot_slice_timeout",
|
|
645
|
+
"autopilot_slice_log_stall",
|
|
646
|
+
"auto_continue_spawn_guard_blocked",
|
|
647
|
+
"auto_continue_spawn_guard_rate_limited",
|
|
648
|
+
"autopilot_autofix_scheduled",
|
|
649
|
+
"autopilot_autofix_executed",
|
|
650
|
+
"autopilot_autofix_skipped",
|
|
651
|
+
]);
|
|
652
|
+
const semanticActivityKey = (item) => {
|
|
653
|
+
const metadata = asMetadataRecord(item.metadata);
|
|
654
|
+
const eventRaw = metadata?.event;
|
|
655
|
+
const event = typeof eventRaw === "string" ? eventRaw.trim().toLowerCase() : "";
|
|
656
|
+
if (!event || !semanticEvents.has(event))
|
|
657
|
+
return null;
|
|
658
|
+
const runLike = (typeof item.runId === "string" && item.runId.trim().length > 0
|
|
659
|
+
? item.runId.trim()
|
|
660
|
+
: null) ??
|
|
661
|
+
metadataString(metadata, [
|
|
662
|
+
"run_id",
|
|
663
|
+
"runId",
|
|
664
|
+
"slice_run_id",
|
|
665
|
+
"sliceRunId",
|
|
666
|
+
"active_run_id",
|
|
667
|
+
"activeRunId",
|
|
668
|
+
"last_run_id",
|
|
669
|
+
"lastRunId",
|
|
670
|
+
]);
|
|
671
|
+
const correlationId = metadataString(metadata, ["correlation_id", "correlationId"]);
|
|
672
|
+
const initiativeId = (typeof item.initiativeId === "string" && item.initiativeId.trim().length > 0
|
|
673
|
+
? item.initiativeId.trim()
|
|
674
|
+
: null) ??
|
|
675
|
+
metadataString(metadata, ["initiative_id", "initiativeId"]);
|
|
676
|
+
const workstreamId = metadataString(metadata, ["workstream_id", "workstreamId"]);
|
|
677
|
+
const taskId = metadataString(metadata, ["task_id", "taskId"]);
|
|
678
|
+
const stopReason = metadataString(metadata, ["stop_reason", "stopReason"]);
|
|
679
|
+
const parsedStatus = metadataString(metadata, ["parsed_status", "parsedStatus"]);
|
|
680
|
+
const title = (item.title ?? "").trim().toLowerCase();
|
|
681
|
+
if (!runLike && !correlationId && !workstreamId && !taskId)
|
|
682
|
+
return null;
|
|
683
|
+
return [
|
|
684
|
+
event,
|
|
685
|
+
initiativeId ?? "",
|
|
686
|
+
workstreamId ?? "",
|
|
687
|
+
taskId ?? "",
|
|
688
|
+
runLike ?? "",
|
|
689
|
+
correlationId ?? "",
|
|
690
|
+
stopReason ?? "",
|
|
691
|
+
parsedStatus ?? "",
|
|
692
|
+
title,
|
|
693
|
+
].join("|");
|
|
694
|
+
};
|
|
695
|
+
const merged = [...(base ?? []), ...(extra ?? [])].sort((a, b) => {
|
|
696
|
+
const timestampDelta = Date.parse(b.timestamp) - Date.parse(a.timestamp);
|
|
697
|
+
if (timestampDelta !== 0)
|
|
698
|
+
return timestampDelta;
|
|
699
|
+
return b.id.localeCompare(a.id);
|
|
700
|
+
});
|
|
701
|
+
const deduped = [];
|
|
702
|
+
const seenIds = new Set();
|
|
703
|
+
const seenSemantic = new Set();
|
|
704
|
+
for (const item of merged) {
|
|
705
|
+
if (seenIds.has(item.id))
|
|
706
|
+
continue;
|
|
707
|
+
seenIds.add(item.id);
|
|
708
|
+
const semanticKey = semanticActivityKey(item);
|
|
709
|
+
if (semanticKey && seenSemantic.has(semanticKey))
|
|
710
|
+
continue;
|
|
711
|
+
if (semanticKey)
|
|
712
|
+
seenSemantic.add(semanticKey);
|
|
713
|
+
deduped.push(item);
|
|
714
|
+
if (deduped.length >= limit)
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
return deduped;
|
|
718
|
+
}
|
|
719
|
+
function normalizeRuntimeSourceForReporting(value) {
|
|
720
|
+
if (value === "codex")
|
|
721
|
+
return "codex";
|
|
722
|
+
if (value === "claude-code")
|
|
723
|
+
return "claude-code";
|
|
724
|
+
if (value === "api")
|
|
725
|
+
return "api";
|
|
726
|
+
return "openclaw";
|
|
727
|
+
}
|
|
728
|
+
function normalizeHookPhase(value) {
|
|
729
|
+
const normalized = (value ?? "").trim().toLowerCase();
|
|
730
|
+
if (normalized === "intent")
|
|
731
|
+
return "intent";
|
|
732
|
+
if (normalized === "execution")
|
|
733
|
+
return "execution";
|
|
734
|
+
if (normalized === "blocked")
|
|
735
|
+
return "blocked";
|
|
736
|
+
if (normalized === "review")
|
|
737
|
+
return "review";
|
|
738
|
+
if (normalized === "handoff")
|
|
739
|
+
return "handoff";
|
|
740
|
+
if (normalized === "completed")
|
|
741
|
+
return "completed";
|
|
742
|
+
return "execution";
|
|
743
|
+
}
|
|
744
|
+
function normalizeRuntimeSource(value) {
|
|
745
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
746
|
+
if (normalized === "openclaw")
|
|
747
|
+
return "openclaw";
|
|
748
|
+
if (normalized === "codex")
|
|
749
|
+
return "codex";
|
|
750
|
+
if (normalized === "claude-code")
|
|
751
|
+
return "claude-code";
|
|
752
|
+
if (normalized === "api")
|
|
753
|
+
return "api";
|
|
754
|
+
return "unknown";
|
|
755
|
+
}
|
|
756
|
+
function runtimeSourceDefaultAgentLabel(sourceClient) {
|
|
757
|
+
if (sourceClient === "codex")
|
|
758
|
+
return "Codex";
|
|
759
|
+
if (sourceClient === "claude-code")
|
|
760
|
+
return "Claude Code";
|
|
761
|
+
if (sourceClient === "openclaw")
|
|
762
|
+
return "OpenClaw";
|
|
763
|
+
if (sourceClient === "api")
|
|
764
|
+
return "OrgX API";
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
function runtimeSourceDefaultAgentId(sourceClient) {
|
|
768
|
+
if (sourceClient === "codex")
|
|
769
|
+
return "runtime:codex";
|
|
770
|
+
if (sourceClient === "claude-code")
|
|
771
|
+
return "runtime:claude-code";
|
|
772
|
+
if (sourceClient === "openclaw")
|
|
773
|
+
return "runtime:openclaw";
|
|
774
|
+
if (sourceClient === "api")
|
|
775
|
+
return "runtime:api";
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
function deriveRuntimeFallbackAgent(instance) {
|
|
779
|
+
const sourceClient = normalizeRuntimeSource(instance.sourceClient);
|
|
780
|
+
const agentId = (instance.agentId ?? "").trim() || runtimeSourceDefaultAgentId(sourceClient);
|
|
781
|
+
const agentName = (instance.agentName ?? "").trim() ||
|
|
782
|
+
(instance.displayName ?? "").trim() ||
|
|
783
|
+
runtimeSourceDefaultAgentLabel(sourceClient);
|
|
784
|
+
return {
|
|
785
|
+
agentId: agentId || null,
|
|
786
|
+
agentName: agentName || null,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function deriveRuntimeSessionStatus(instance) {
|
|
790
|
+
const state = (instance.state ?? "").trim().toLowerCase();
|
|
791
|
+
const phase = (instance.phase ?? "").trim().toLowerCase();
|
|
792
|
+
if (phase === "blocked" || state === "error")
|
|
793
|
+
return "blocked";
|
|
794
|
+
if (phase === "completed")
|
|
795
|
+
return "completed";
|
|
796
|
+
if (phase === "handoff")
|
|
797
|
+
return "handoff";
|
|
798
|
+
if (phase === "review")
|
|
799
|
+
return "review";
|
|
800
|
+
if (state === "stopped")
|
|
801
|
+
return "paused";
|
|
802
|
+
if (state === "stale")
|
|
803
|
+
return "queued";
|
|
804
|
+
return "running";
|
|
805
|
+
}
|
|
806
|
+
function runtimeMatchMaps(instances) {
|
|
807
|
+
const byRunId = new Map();
|
|
808
|
+
const byAgentInitiative = new Map();
|
|
809
|
+
for (const instance of instances) {
|
|
810
|
+
if (instance.runId && !byRunId.has(instance.runId)) {
|
|
811
|
+
byRunId.set(instance.runId, instance);
|
|
812
|
+
}
|
|
813
|
+
const agentId = instance.agentId?.trim() ?? "";
|
|
814
|
+
const initiativeId = instance.initiativeId?.trim() ?? "";
|
|
815
|
+
if (!agentId || !initiativeId)
|
|
816
|
+
continue;
|
|
817
|
+
const key = `${agentId}:${initiativeId}`;
|
|
818
|
+
if (!byAgentInitiative.has(key)) {
|
|
819
|
+
byAgentInitiative.set(key, instance);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return { byRunId, byAgentInitiative };
|
|
823
|
+
}
|
|
824
|
+
function enrichSessionsWithRuntime(input, instances) {
|
|
825
|
+
if (!Array.isArray(input.nodes) || input.nodes.length === 0)
|
|
826
|
+
return input;
|
|
827
|
+
if (instances.length === 0)
|
|
828
|
+
return input;
|
|
829
|
+
const { byRunId, byAgentInitiative } = runtimeMatchMaps(instances);
|
|
830
|
+
const nodes = input.nodes.map((node) => {
|
|
831
|
+
const byRun = node.runId ? byRunId.get(node.runId) ?? null : null;
|
|
832
|
+
const byAgent = !byRun && node.agentId && node.initiativeId
|
|
833
|
+
? byAgentInitiative.get(`${node.agentId}:${node.initiativeId}`) ?? null
|
|
834
|
+
: null;
|
|
835
|
+
const match = byRun ?? byAgent;
|
|
836
|
+
if (!match)
|
|
837
|
+
return node;
|
|
838
|
+
const runtimeStatus = deriveRuntimeSessionStatus(match);
|
|
839
|
+
const fallbackAgent = deriveRuntimeFallbackAgent(match);
|
|
840
|
+
const agentId = (node.agentId ?? "").trim() || fallbackAgent.agentId;
|
|
841
|
+
const agentName = (node.agentName ?? "").trim() || fallbackAgent.agentName;
|
|
842
|
+
const nodeStatus = (node.status ?? "").trim().toLowerCase();
|
|
843
|
+
const isTerminalNodeStatus = nodeStatus === "completed" ||
|
|
844
|
+
nodeStatus === "cancelled" ||
|
|
845
|
+
nodeStatus === "archived";
|
|
846
|
+
const isLiveLikeNodeStatus = nodeStatus === "running" ||
|
|
847
|
+
nodeStatus === "active" ||
|
|
848
|
+
nodeStatus === "in_progress" ||
|
|
849
|
+
nodeStatus === "working" ||
|
|
850
|
+
nodeStatus === "planning" ||
|
|
851
|
+
nodeStatus === "dispatching";
|
|
852
|
+
const shouldDowngradeStatusFromRuntime = isLiveLikeNodeStatus && (runtimeStatus === "queued" || runtimeStatus === "paused");
|
|
853
|
+
const shouldPromoteStatusFromRuntime = runtimeStatus === "completed" ||
|
|
854
|
+
runtimeStatus === "blocked" ||
|
|
855
|
+
runtimeStatus === "review" ||
|
|
856
|
+
runtimeStatus === "handoff" ||
|
|
857
|
+
(runtimeStatus === "running" &&
|
|
858
|
+
(nodeStatus === "blocked" ||
|
|
859
|
+
nodeStatus === "failed" ||
|
|
860
|
+
nodeStatus === "queued" ||
|
|
861
|
+
nodeStatus === "paused"));
|
|
862
|
+
const nextStatus = shouldDowngradeStatusFromRuntime ||
|
|
863
|
+
(!isTerminalNodeStatus && shouldPromoteStatusFromRuntime)
|
|
864
|
+
? runtimeStatus
|
|
865
|
+
: node.status;
|
|
866
|
+
const runtimeExplicitlyBlocked = runtimeStatus === "blocked" || match.phase?.toLowerCase() === "blocked";
|
|
867
|
+
const runtimeExplicitlyUnblocked = !runtimeExplicitlyBlocked && typeof match.phase === "string" && match.phase.trim().length > 0;
|
|
868
|
+
const runtimeBlockedReason = (match.lastMessage ?? "").trim();
|
|
869
|
+
const nextBlockerReason = runtimeExplicitlyBlocked
|
|
870
|
+
? runtimeBlockedReason || (node.blockerReason ?? "").trim() || null
|
|
871
|
+
: runtimeExplicitlyUnblocked
|
|
872
|
+
? null
|
|
873
|
+
: node.blockerReason ?? null;
|
|
874
|
+
const nextBlockers = runtimeExplicitlyBlocked
|
|
875
|
+
? runtimeBlockedReason
|
|
876
|
+
? [runtimeBlockedReason]
|
|
877
|
+
: Array.isArray(node.blockers)
|
|
878
|
+
? node.blockers
|
|
879
|
+
: []
|
|
880
|
+
: runtimeExplicitlyUnblocked
|
|
881
|
+
? []
|
|
882
|
+
: Array.isArray(node.blockers)
|
|
883
|
+
? node.blockers
|
|
884
|
+
: [];
|
|
885
|
+
return {
|
|
886
|
+
...node,
|
|
887
|
+
agentId: agentId || null,
|
|
888
|
+
agentName: agentName || null,
|
|
889
|
+
status: nextStatus,
|
|
890
|
+
state: node.state ?? match.state ?? null,
|
|
891
|
+
lastEventSummary: shouldDowngradeStatusFromRuntime && runtimeStatus === "queued"
|
|
892
|
+
? node.lastEventSummary ?? "Recovered stale runtime; awaiting next dispatch."
|
|
893
|
+
: node.lastEventSummary,
|
|
894
|
+
blockers: nextBlockers,
|
|
895
|
+
blockerReason: nextBlockerReason,
|
|
896
|
+
runtimeClient: normalizeRuntimeSource(match.sourceClient),
|
|
897
|
+
runtimeLabel: match.displayName,
|
|
898
|
+
runtimeProvider: match.providerLogo,
|
|
899
|
+
instanceId: match.id,
|
|
900
|
+
lastHeartbeatAt: match.lastHeartbeatAt ?? null,
|
|
901
|
+
};
|
|
902
|
+
});
|
|
903
|
+
return { ...input, nodes };
|
|
904
|
+
}
|
|
905
|
+
function metadataHasStructuredScope(meta) {
|
|
906
|
+
const scalarScope = pickString(meta, [
|
|
907
|
+
"initiative_id",
|
|
908
|
+
"initiativeId",
|
|
909
|
+
"workstream_id",
|
|
910
|
+
"workstreamId",
|
|
911
|
+
"workstream_title",
|
|
912
|
+
"workstreamTitle",
|
|
913
|
+
"task_id",
|
|
914
|
+
"taskId",
|
|
915
|
+
"task_title",
|
|
916
|
+
"taskTitle",
|
|
917
|
+
"slice_run_id",
|
|
918
|
+
"sliceRunId",
|
|
919
|
+
"iwmt_id",
|
|
920
|
+
"iwmtId",
|
|
921
|
+
"milestone_id",
|
|
922
|
+
"milestoneId",
|
|
923
|
+
"milestone_title",
|
|
924
|
+
"milestoneTitle",
|
|
925
|
+
]) ?? null;
|
|
926
|
+
if (scalarScope)
|
|
927
|
+
return true;
|
|
928
|
+
const listScopeKeys = [
|
|
929
|
+
"initiative_ids",
|
|
930
|
+
"initiativeIds",
|
|
931
|
+
"workstream_ids",
|
|
932
|
+
"workstreamIds",
|
|
933
|
+
"task_ids",
|
|
934
|
+
"taskIds",
|
|
935
|
+
"milestone_ids",
|
|
936
|
+
"milestoneIds",
|
|
937
|
+
"iwmt_ids",
|
|
938
|
+
"iwmtIds",
|
|
939
|
+
];
|
|
940
|
+
for (const key of listScopeKeys) {
|
|
941
|
+
const value = meta[key];
|
|
942
|
+
if (!Array.isArray(value))
|
|
943
|
+
continue;
|
|
944
|
+
if (value.some((entry) => typeof entry === "string" && entry.trim().length > 0)) {
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
function shouldInjectRuntimeInstanceAsSession(instance, runId, meta) {
|
|
951
|
+
if (instance.state !== "active")
|
|
952
|
+
return false;
|
|
953
|
+
// Synthetic hook correlation ids are telemetry-only and should never render as user-facing sessions.
|
|
954
|
+
if (runId.toLowerCase().startsWith("hook-"))
|
|
955
|
+
return false;
|
|
956
|
+
const workstreamId = instance.workstreamId?.trim() ?? "";
|
|
957
|
+
const taskId = instance.taskId?.trim() ?? "";
|
|
958
|
+
if (workstreamId.length > 0 || taskId.length > 0)
|
|
959
|
+
return true;
|
|
960
|
+
// Keep only runtime records that include structured execution scope.
|
|
961
|
+
return metadataHasStructuredScope(meta);
|
|
962
|
+
}
|
|
963
|
+
function injectRuntimeInstancesAsSessions(input, instances) {
|
|
964
|
+
if (!Array.isArray(input.nodes))
|
|
965
|
+
return input;
|
|
966
|
+
if (!Array.isArray(instances) || instances.length === 0)
|
|
967
|
+
return input;
|
|
968
|
+
const nodes = [...input.nodes];
|
|
969
|
+
const edges = Array.isArray(input.edges) ? input.edges : [];
|
|
970
|
+
const groups = [...(input.groups ?? [])];
|
|
971
|
+
const existingRunIds = new Set();
|
|
972
|
+
const existingNodeIds = new Set();
|
|
973
|
+
for (const node of nodes) {
|
|
974
|
+
existingNodeIds.add(node.id);
|
|
975
|
+
if (node.runId)
|
|
976
|
+
existingRunIds.add(node.runId);
|
|
977
|
+
}
|
|
978
|
+
const groupsById = new Map(groups.map((group) => [group.id, group]));
|
|
979
|
+
for (const instance of instances) {
|
|
980
|
+
if (!instance || typeof instance !== "object")
|
|
981
|
+
continue;
|
|
982
|
+
const runId = instance.runId?.trim() || instance.correlationId?.trim() || "";
|
|
983
|
+
if (!runId)
|
|
984
|
+
continue;
|
|
985
|
+
if (existingRunIds.has(runId))
|
|
986
|
+
continue;
|
|
987
|
+
const initiativeId = instance.initiativeId?.trim() || null;
|
|
988
|
+
const workstreamId = instance.workstreamId?.trim() || null;
|
|
989
|
+
const runtimeClient = normalizeRuntimeSource(instance.sourceClient);
|
|
990
|
+
const fallbackAgent = deriveRuntimeFallbackAgent(instance);
|
|
991
|
+
const groupId = initiativeId ?? fallbackAgent.agentId ?? `runtime:${runtimeClient}`;
|
|
992
|
+
const meta = instance.metadata && typeof instance.metadata === "object"
|
|
993
|
+
? instance.metadata
|
|
994
|
+
: {};
|
|
995
|
+
if (!shouldInjectRuntimeInstanceAsSession(instance, runId, meta))
|
|
996
|
+
continue;
|
|
997
|
+
const titleHint = pickString(meta, ["workstream_title", "workstreamTitle"]) ??
|
|
998
|
+
(workstreamId ? `Workstream ${workstreamId.slice(0, 8)}` : null);
|
|
999
|
+
const initiativeHint = pickString(meta, ["initiative_title", "initiativeTitle"]) ??
|
|
1000
|
+
(initiativeId ? `Initiative ${initiativeId.slice(0, 8)}` : null);
|
|
1001
|
+
const groupLabel = (initiativeHint ?? fallbackAgent.agentName ?? groupId).trim();
|
|
1002
|
+
if (!groupsById.has(groupId)) {
|
|
1003
|
+
const group = { id: groupId, label: groupLabel, status: null };
|
|
1004
|
+
groupsById.set(groupId, group);
|
|
1005
|
+
groups.push(group);
|
|
1006
|
+
}
|
|
1007
|
+
const nodeId = `runtime:${instance.id}`;
|
|
1008
|
+
if (existingNodeIds.has(nodeId))
|
|
1009
|
+
continue;
|
|
1010
|
+
existingNodeIds.add(nodeId);
|
|
1011
|
+
existingRunIds.add(runId);
|
|
1012
|
+
const status = deriveRuntimeSessionStatus(instance);
|
|
1013
|
+
const blockerReason = status === "blocked" ? (instance.lastMessage ?? null) : null;
|
|
1014
|
+
const blockers = status === "blocked" && typeof blockerReason === "string" && blockerReason.trim().length > 0
|
|
1015
|
+
? [blockerReason.trim()]
|
|
1016
|
+
: [];
|
|
1017
|
+
const node = {
|
|
1018
|
+
id: nodeId,
|
|
1019
|
+
parentId: null,
|
|
1020
|
+
runId,
|
|
1021
|
+
title: titleHint ?? instance.lastMessage ?? `Runtime ${runId.slice(0, 8)}`,
|
|
1022
|
+
agentId: fallbackAgent.agentId,
|
|
1023
|
+
agentName: fallbackAgent.agentName,
|
|
1024
|
+
status,
|
|
1025
|
+
progress: instance.progressPct ?? null,
|
|
1026
|
+
initiativeId,
|
|
1027
|
+
workstreamId,
|
|
1028
|
+
groupId,
|
|
1029
|
+
groupLabel,
|
|
1030
|
+
startedAt: instance.createdAt ?? instance.lastEventAt ?? null,
|
|
1031
|
+
updatedAt: instance.updatedAt ?? null,
|
|
1032
|
+
lastEventAt: instance.lastEventAt ?? null,
|
|
1033
|
+
lastEventSummary: instance.lastMessage ?? null,
|
|
1034
|
+
blockers,
|
|
1035
|
+
blockerReason,
|
|
1036
|
+
phase: instance.phase ?? null,
|
|
1037
|
+
state: instance.state ?? null,
|
|
1038
|
+
runtimeClient,
|
|
1039
|
+
runtimeLabel: instance.displayName,
|
|
1040
|
+
runtimeProvider: instance.providerLogo,
|
|
1041
|
+
instanceId: instance.id,
|
|
1042
|
+
lastHeartbeatAt: instance.lastHeartbeatAt ?? null,
|
|
1043
|
+
};
|
|
1044
|
+
nodes.push(node);
|
|
1045
|
+
}
|
|
1046
|
+
return { nodes, edges, groups };
|
|
1047
|
+
}
|
|
1048
|
+
function enrichActivityWithRuntime(input, instances) {
|
|
1049
|
+
if (!Array.isArray(input) || input.length === 0)
|
|
1050
|
+
return [];
|
|
1051
|
+
if (instances.length === 0)
|
|
1052
|
+
return input;
|
|
1053
|
+
const { byRunId, byAgentInitiative } = runtimeMatchMaps(instances);
|
|
1054
|
+
return input.map((item) => {
|
|
1055
|
+
const byRun = item.runId ? byRunId.get(item.runId) ?? null : null;
|
|
1056
|
+
const byAgent = !byRun && item.agentId && item.initiativeId
|
|
1057
|
+
? byAgentInitiative.get(`${item.agentId}:${item.initiativeId}`) ?? null
|
|
1058
|
+
: null;
|
|
1059
|
+
const match = byRun ?? byAgent;
|
|
1060
|
+
if (!match)
|
|
1061
|
+
return item;
|
|
1062
|
+
return {
|
|
1063
|
+
...item,
|
|
1064
|
+
runtimeClient: normalizeRuntimeSource(match.sourceClient),
|
|
1065
|
+
runtimeLabel: match.displayName,
|
|
1066
|
+
runtimeProvider: match.providerLogo,
|
|
1067
|
+
instanceId: match.id,
|
|
1068
|
+
lastHeartbeatAt: match.lastHeartbeatAt ?? null,
|
|
1069
|
+
};
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
// =============================================================================
|
|
1073
|
+
// Content-Type mapping
|
|
1074
|
+
// =============================================================================
|
|
1075
|
+
const MIME_TYPES = {
|
|
1076
|
+
".html": "text/html; charset=utf-8",
|
|
1077
|
+
".js": "application/javascript; charset=utf-8",
|
|
1078
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
1079
|
+
".css": "text/css; charset=utf-8",
|
|
1080
|
+
".json": "application/json; charset=utf-8",
|
|
1081
|
+
".png": "image/png",
|
|
1082
|
+
".jpg": "image/jpeg",
|
|
1083
|
+
".jpeg": "image/jpeg",
|
|
1084
|
+
".gif": "image/gif",
|
|
1085
|
+
".svg": "image/svg+xml",
|
|
1086
|
+
".ico": "image/x-icon",
|
|
1087
|
+
".woff": "font/woff",
|
|
1088
|
+
".woff2": "font/woff2",
|
|
1089
|
+
".ttf": "font/ttf",
|
|
1090
|
+
".map": "application/json",
|
|
1091
|
+
};
|
|
1092
|
+
function contentType(filePath) {
|
|
1093
|
+
return MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
1094
|
+
}
|
|
1095
|
+
// =============================================================================
|
|
1096
|
+
// CORS + response hardening
|
|
1097
|
+
// =============================================================================
|
|
1098
|
+
const CORS_HEADERS = {
|
|
1099
|
+
"Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
|
|
1100
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-OrgX-Api-Key, X-API-Key, X-OrgX-User-Id, X-OrgX-Hook-Token, X-Hook-Token",
|
|
1101
|
+
Vary: "Origin",
|
|
1102
|
+
};
|
|
1103
|
+
const CONTENT_SECURITY_POLICY = [
|
|
1104
|
+
"default-src 'self'",
|
|
1105
|
+
"base-uri 'self'",
|
|
1106
|
+
"frame-ancestors 'none'",
|
|
1107
|
+
"form-action 'self'",
|
|
1108
|
+
"object-src 'none'",
|
|
1109
|
+
"script-src 'self'",
|
|
1110
|
+
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
|
1111
|
+
"img-src 'self' data: blob:",
|
|
1112
|
+
"font-src 'self' data: https://fonts.gstatic.com",
|
|
1113
|
+
"media-src 'self'",
|
|
1114
|
+
"connect-src 'self' https://*.useorgx.com https://*.openclaw.ai https://api.openai.com https://*.openai.com http://127.0.0.1:* http://localhost:*",
|
|
1115
|
+
].join("; ");
|
|
1116
|
+
const SECURITY_HEADERS = {
|
|
1117
|
+
"X-Content-Type-Options": "nosniff",
|
|
1118
|
+
"X-Frame-Options": "DENY",
|
|
1119
|
+
"Referrer-Policy": "same-origin",
|
|
1120
|
+
"X-Robots-Tag": "noindex, nofollow, noarchive, nosnippet, noimageindex",
|
|
1121
|
+
"Permissions-Policy": "camera=(), microphone=(self), geolocation=(), payment=(), usb=(), midi=(), magnetometer=(), gyroscope=()",
|
|
1122
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
1123
|
+
"Cross-Origin-Resource-Policy": "same-origin",
|
|
1124
|
+
"Origin-Agent-Cluster": "?1",
|
|
1125
|
+
"X-Permitted-Cross-Domain-Policies": "none",
|
|
1126
|
+
"Content-Security-Policy": CONTENT_SECURITY_POLICY,
|
|
1127
|
+
};
|
|
1128
|
+
function normalizeHost(value) {
|
|
1129
|
+
return value.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
1130
|
+
}
|
|
1131
|
+
function isLoopbackHost(hostname) {
|
|
1132
|
+
const host = normalizeHost(hostname);
|
|
1133
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
1134
|
+
}
|
|
1135
|
+
function isTrustedOrigin(origin) {
|
|
1136
|
+
try {
|
|
1137
|
+
const parsed = new URL(origin);
|
|
1138
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
return isLoopbackHost(parsed.hostname);
|
|
1142
|
+
}
|
|
1143
|
+
catch {
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
function isTrustedRequestSource(headers) {
|
|
1148
|
+
const fetchSite = pickHeaderString(headers, ["sec-fetch-site"]);
|
|
1149
|
+
if (fetchSite) {
|
|
1150
|
+
const normalizedFetchSite = fetchSite.trim().toLowerCase();
|
|
1151
|
+
if (normalizedFetchSite !== "same-origin" &&
|
|
1152
|
+
normalizedFetchSite !== "same-site" &&
|
|
1153
|
+
normalizedFetchSite !== "none") {
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
const origin = pickHeaderString(headers, ["origin"]);
|
|
1158
|
+
if (origin) {
|
|
1159
|
+
return isTrustedOrigin(origin);
|
|
1160
|
+
}
|
|
1161
|
+
const referer = pickHeaderString(headers, ["referer"]);
|
|
1162
|
+
if (referer) {
|
|
1163
|
+
try {
|
|
1164
|
+
return isTrustedOrigin(new URL(referer).origin);
|
|
1165
|
+
}
|
|
1166
|
+
catch {
|
|
1167
|
+
return false;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return true;
|
|
1171
|
+
}
|
|
1172
|
+
const STREAM_IDLE_TIMEOUT_MS = 60_000;
|
|
1173
|
+
// =============================================================================
|
|
1174
|
+
// Resolve the dashboard/dist/ directory relative to this file
|
|
1175
|
+
// =============================================================================
|
|
1176
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
1177
|
+
const __dirname = dirname(__filename);
|
|
1178
|
+
const DIST_DIR = resolve(__dirname, "..", "..", "dashboard", "dist");
|
|
1179
|
+
const RESOLVED_DIST_DIR = resolve(DIST_DIR);
|
|
1180
|
+
const RESOLVED_DIST_ASSETS_DIR = resolve(DIST_DIR, "assets");
|
|
1181
|
+
function resolveSafeDistPath(subPath) {
|
|
1182
|
+
if (!subPath || subPath.includes("\0"))
|
|
1183
|
+
return null;
|
|
1184
|
+
const normalized = normalize(subPath).replace(/^([/\\])+/, "");
|
|
1185
|
+
if (!normalized || normalized === ".")
|
|
1186
|
+
return null;
|
|
1187
|
+
const candidate = resolve(DIST_DIR, normalized);
|
|
1188
|
+
const rel = relative(RESOLVED_DIST_DIR, candidate);
|
|
1189
|
+
if (!rel || rel === "." || rel.startsWith("..") || rel.includes(`..${sep}`)) {
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
return candidate;
|
|
1193
|
+
}
|
|
1194
|
+
const PRECOMPRESSED_FILE_EXTENSIONS = new Set([
|
|
1195
|
+
".css",
|
|
1196
|
+
".html",
|
|
1197
|
+
".js",
|
|
1198
|
+
".json",
|
|
1199
|
+
".map",
|
|
1200
|
+
".svg",
|
|
1201
|
+
".txt",
|
|
1202
|
+
".xml",
|
|
1203
|
+
]);
|
|
1204
|
+
const IMMUTABLE_FILE_CACHE = new Map();
|
|
1205
|
+
const IMMUTABLE_FILE_CACHE_MAX = 128;
|
|
1206
|
+
const FILE_PREVIEW_MAX_BYTES = 1_000_000;
|
|
1207
|
+
const FILE_PREVIEW_MAX_DIR_ENTRIES = 300;
|
|
1208
|
+
function sendJson(res, status, data) {
|
|
1209
|
+
const body = JSON.stringify(data);
|
|
1210
|
+
res.writeHead(status, {
|
|
1211
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1212
|
+
// Avoid browser/proxy caching for live dashboards.
|
|
1213
|
+
"Cache-Control": "no-store",
|
|
1214
|
+
...SECURITY_HEADERS,
|
|
1215
|
+
...CORS_HEADERS,
|
|
1216
|
+
});
|
|
1217
|
+
res.end(body);
|
|
1218
|
+
}
|
|
1219
|
+
function sendHtml(res, status, html) {
|
|
1220
|
+
res.writeHead(status, {
|
|
1221
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1222
|
+
"Cache-Control": "no-store",
|
|
1223
|
+
...SECURITY_HEADERS,
|
|
1224
|
+
...CORS_HEADERS,
|
|
1225
|
+
});
|
|
1226
|
+
res.end(html);
|
|
1227
|
+
}
|
|
1228
|
+
function escapeHtml(value) {
|
|
1229
|
+
return value
|
|
1230
|
+
.replaceAll("&", "&")
|
|
1231
|
+
.replaceAll("<", "<")
|
|
1232
|
+
.replaceAll(">", ">")
|
|
1233
|
+
.replaceAll('"', """)
|
|
1234
|
+
.replaceAll("'", "'");
|
|
1235
|
+
}
|
|
1236
|
+
function resolveFilesystemOpenPath(rawPath) {
|
|
1237
|
+
let value = rawPath.trim();
|
|
1238
|
+
if (value.toLowerCase().startsWith("file://")) {
|
|
1239
|
+
value = value.replace(/^file:\/\//i, "");
|
|
1240
|
+
try {
|
|
1241
|
+
value = decodeURIComponent(value);
|
|
1242
|
+
}
|
|
1243
|
+
catch {
|
|
1244
|
+
// best effort
|
|
1245
|
+
}
|
|
1246
|
+
if (process.platform === "win32" && value.startsWith("/")) {
|
|
1247
|
+
value = value.slice(1);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
if (value.startsWith("~/")) {
|
|
1251
|
+
return resolve(homedir(), value.slice(2));
|
|
1252
|
+
}
|
|
1253
|
+
const looksWindowsAbsolute = /^[A-Za-z]:[\\/]/.test(value);
|
|
1254
|
+
if (value.startsWith("/") || looksWindowsAbsolute) {
|
|
1255
|
+
return resolve(value);
|
|
1256
|
+
}
|
|
1257
|
+
return resolve(process.cwd(), value);
|
|
1258
|
+
}
|
|
1259
|
+
function readFilePreview(pathname, totalBytes) {
|
|
1260
|
+
if (totalBytes <= 0) {
|
|
1261
|
+
return { previewBuffer: Buffer.alloc(0), truncated: false };
|
|
1262
|
+
}
|
|
1263
|
+
const previewBytes = Math.min(totalBytes, FILE_PREVIEW_MAX_BYTES);
|
|
1264
|
+
const previewBuffer = Buffer.alloc(previewBytes);
|
|
1265
|
+
const fd = openSync(pathname, "r");
|
|
1266
|
+
try {
|
|
1267
|
+
const bytesRead = readSync(fd, previewBuffer, 0, previewBytes, 0);
|
|
1268
|
+
if (bytesRead < previewBytes) {
|
|
1269
|
+
return {
|
|
1270
|
+
previewBuffer: previewBuffer.subarray(0, bytesRead),
|
|
1271
|
+
truncated: totalBytes > bytesRead,
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
return {
|
|
1275
|
+
previewBuffer,
|
|
1276
|
+
truncated: totalBytes > previewBytes,
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
finally {
|
|
1280
|
+
closeSync(fd);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function parseAcceptedEncodings(rawHeader) {
|
|
1284
|
+
const parsed = new Map();
|
|
1285
|
+
if (!rawHeader || rawHeader.trim().length === 0)
|
|
1286
|
+
return parsed;
|
|
1287
|
+
const parts = rawHeader.split(",");
|
|
1288
|
+
for (const part of parts) {
|
|
1289
|
+
const [nameRaw, ...params] = part.split(";");
|
|
1290
|
+
const name = nameRaw?.trim().toLowerCase();
|
|
1291
|
+
if (!name)
|
|
1292
|
+
continue;
|
|
1293
|
+
let q = 1;
|
|
1294
|
+
for (const param of params) {
|
|
1295
|
+
const [keyRaw, valueRaw] = param.split("=");
|
|
1296
|
+
const key = keyRaw?.trim().toLowerCase();
|
|
1297
|
+
if (key !== "q")
|
|
1298
|
+
continue;
|
|
1299
|
+
const candidate = Number.parseFloat((valueRaw ?? "").trim());
|
|
1300
|
+
if (Number.isFinite(candidate)) {
|
|
1301
|
+
q = candidate;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (q <= 0)
|
|
1305
|
+
continue;
|
|
1306
|
+
const existing = parsed.get(name);
|
|
1307
|
+
if (existing == null || q > existing) {
|
|
1308
|
+
parsed.set(name, q);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
return parsed;
|
|
1312
|
+
}
|
|
1313
|
+
function resolveEncodingQuality(accepted, encoding) {
|
|
1314
|
+
if (accepted.has(encoding))
|
|
1315
|
+
return accepted.get(encoding) ?? 0;
|
|
1316
|
+
if (accepted.has("*"))
|
|
1317
|
+
return accepted.get("*") ?? 0;
|
|
1318
|
+
return 0;
|
|
1319
|
+
}
|
|
1320
|
+
function resolvePrecompressedVariant(req, filePath) {
|
|
1321
|
+
const ext = extname(filePath).toLowerCase();
|
|
1322
|
+
if (!PRECOMPRESSED_FILE_EXTENSIONS.has(ext))
|
|
1323
|
+
return null;
|
|
1324
|
+
const accepted = parseAcceptedEncodings(pickHeaderString(req.headers, ["accept-encoding"]));
|
|
1325
|
+
if (accepted.size === 0)
|
|
1326
|
+
return null;
|
|
1327
|
+
const candidates = [
|
|
1328
|
+
{
|
|
1329
|
+
encoding: "br",
|
|
1330
|
+
path: `${filePath}.br`,
|
|
1331
|
+
quality: resolveEncodingQuality(accepted, "br"),
|
|
1332
|
+
priority: 2,
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
encoding: "gzip",
|
|
1336
|
+
path: `${filePath}.gz`,
|
|
1337
|
+
quality: resolveEncodingQuality(accepted, "gzip"),
|
|
1338
|
+
priority: 1,
|
|
1339
|
+
},
|
|
1340
|
+
];
|
|
1341
|
+
candidates.sort((left, right) => {
|
|
1342
|
+
if (right.quality !== left.quality)
|
|
1343
|
+
return right.quality - left.quality;
|
|
1344
|
+
return right.priority - left.priority;
|
|
1345
|
+
});
|
|
1346
|
+
for (const candidate of candidates) {
|
|
1347
|
+
if (candidate.quality <= 0)
|
|
1348
|
+
continue;
|
|
1349
|
+
if (existsSync(candidate.path)) {
|
|
1350
|
+
return { path: candidate.path, encoding: candidate.encoding };
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
function sendFile(req, res, filePath, cacheControl) {
|
|
1356
|
+
try {
|
|
1357
|
+
const shouldCacheImmutable = cacheControl.includes("immutable");
|
|
1358
|
+
const shouldVaryByEncoding = PRECOMPRESSED_FILE_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
1359
|
+
const precompressed = resolvePrecompressedVariant(req, filePath);
|
|
1360
|
+
const responsePath = precompressed?.path ?? filePath;
|
|
1361
|
+
const responseEncoding = precompressed?.encoding ?? null;
|
|
1362
|
+
const cacheKey = `${responsePath}|${cacheControl}`;
|
|
1363
|
+
if (shouldCacheImmutable) {
|
|
1364
|
+
const cached = IMMUTABLE_FILE_CACHE.get(cacheKey);
|
|
1365
|
+
if (cached) {
|
|
1366
|
+
const headers = {
|
|
1367
|
+
"Content-Type": cached.contentType,
|
|
1368
|
+
"Cache-Control": cacheControl,
|
|
1369
|
+
...SECURITY_HEADERS,
|
|
1370
|
+
...CORS_HEADERS,
|
|
1371
|
+
};
|
|
1372
|
+
if (cached.contentEncoding === "br")
|
|
1373
|
+
headers["Content-Encoding"] = "br";
|
|
1374
|
+
if (cached.contentEncoding === "gzip")
|
|
1375
|
+
headers["Content-Encoding"] = "gzip";
|
|
1376
|
+
if (cached.varyAcceptEncoding)
|
|
1377
|
+
headers["Vary"] = "Accept-Encoding";
|
|
1378
|
+
res.writeHead(200, {
|
|
1379
|
+
...headers,
|
|
1380
|
+
});
|
|
1381
|
+
res.end(cached.content);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
const content = readFileSync(responsePath);
|
|
1386
|
+
const type = contentType(filePath);
|
|
1387
|
+
if (shouldCacheImmutable) {
|
|
1388
|
+
if (IMMUTABLE_FILE_CACHE.size >= IMMUTABLE_FILE_CACHE_MAX) {
|
|
1389
|
+
const firstKey = IMMUTABLE_FILE_CACHE.keys().next().value;
|
|
1390
|
+
if (firstKey)
|
|
1391
|
+
IMMUTABLE_FILE_CACHE.delete(firstKey);
|
|
1392
|
+
}
|
|
1393
|
+
IMMUTABLE_FILE_CACHE.set(cacheKey, {
|
|
1394
|
+
content,
|
|
1395
|
+
contentType: type,
|
|
1396
|
+
contentEncoding: responseEncoding,
|
|
1397
|
+
varyAcceptEncoding: shouldVaryByEncoding,
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
const headers = {
|
|
1401
|
+
"Content-Type": type,
|
|
1402
|
+
"Cache-Control": cacheControl,
|
|
1403
|
+
...SECURITY_HEADERS,
|
|
1404
|
+
...CORS_HEADERS,
|
|
1405
|
+
};
|
|
1406
|
+
if (responseEncoding === "br")
|
|
1407
|
+
headers["Content-Encoding"] = "br";
|
|
1408
|
+
if (responseEncoding === "gzip")
|
|
1409
|
+
headers["Content-Encoding"] = "gzip";
|
|
1410
|
+
if (shouldVaryByEncoding)
|
|
1411
|
+
headers["Vary"] = "Accept-Encoding";
|
|
1412
|
+
res.writeHead(200, headers);
|
|
1413
|
+
res.end(content);
|
|
1414
|
+
}
|
|
1415
|
+
catch {
|
|
1416
|
+
send404(res);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
function send404(res) {
|
|
1420
|
+
res.writeHead(404, {
|
|
1421
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1422
|
+
...SECURITY_HEADERS,
|
|
1423
|
+
...CORS_HEADERS,
|
|
1424
|
+
});
|
|
1425
|
+
res.end("Not Found");
|
|
1426
|
+
}
|
|
1427
|
+
function sendStaleChunkRecovery(res) {
|
|
1428
|
+
const body = [
|
|
1429
|
+
"// Recover from stale chunk references after dashboard/plugin upgrades.",
|
|
1430
|
+
"window.location.replace('/orgx/live' + window.location.search);",
|
|
1431
|
+
"export {};"
|
|
1432
|
+
].join("\n");
|
|
1433
|
+
res.writeHead(200, {
|
|
1434
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
1435
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
1436
|
+
...SECURITY_HEADERS,
|
|
1437
|
+
...CORS_HEADERS,
|
|
1438
|
+
});
|
|
1439
|
+
res.end(body);
|
|
1440
|
+
}
|
|
1441
|
+
function sendIndexHtml(req, res) {
|
|
1442
|
+
const indexPath = join(DIST_DIR, "index.html");
|
|
1443
|
+
if (existsSync(indexPath)) {
|
|
1444
|
+
sendFile(req, res, indexPath, "no-cache, no-store, must-revalidate");
|
|
1445
|
+
}
|
|
1446
|
+
else {
|
|
1447
|
+
res.writeHead(503, {
|
|
1448
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1449
|
+
...SECURITY_HEADERS,
|
|
1450
|
+
...CORS_HEADERS,
|
|
1451
|
+
});
|
|
1452
|
+
res.end("<html><body><h1>Dashboard not built</h1>" +
|
|
1453
|
+
"<p>Run <code>cd dashboard && npm run build</code> to build the SPA.</p>" +
|
|
1454
|
+
"</body></html>");
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
function parseJsonBody(body) {
|
|
1458
|
+
if (!body)
|
|
1459
|
+
return {};
|
|
1460
|
+
if (typeof body === "string") {
|
|
1461
|
+
try {
|
|
1462
|
+
const parsed = JSON.parse(body);
|
|
1463
|
+
return typeof parsed === "object" && parsed !== null
|
|
1464
|
+
? parsed
|
|
1465
|
+
: {};
|
|
1466
|
+
}
|
|
1467
|
+
catch {
|
|
1468
|
+
return {};
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (Buffer.isBuffer(body)) {
|
|
1472
|
+
try {
|
|
1473
|
+
const parsed = JSON.parse(body.toString("utf8"));
|
|
1474
|
+
return typeof parsed === "object" && parsed !== null
|
|
1475
|
+
? parsed
|
|
1476
|
+
: {};
|
|
1477
|
+
}
|
|
1478
|
+
catch {
|
|
1479
|
+
return {};
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (body instanceof Uint8Array) {
|
|
1483
|
+
try {
|
|
1484
|
+
const parsed = JSON.parse(Buffer.from(body).toString("utf8"));
|
|
1485
|
+
return typeof parsed === "object" && parsed !== null
|
|
1486
|
+
? parsed
|
|
1487
|
+
: {};
|
|
1488
|
+
}
|
|
1489
|
+
catch {
|
|
1490
|
+
return {};
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (body instanceof ArrayBuffer) {
|
|
1494
|
+
try {
|
|
1495
|
+
const parsed = JSON.parse(Buffer.from(body).toString("utf8"));
|
|
1496
|
+
return typeof parsed === "object" && parsed !== null
|
|
1497
|
+
? parsed
|
|
1498
|
+
: {};
|
|
1499
|
+
}
|
|
1500
|
+
catch {
|
|
1501
|
+
return {};
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (typeof body === "object") {
|
|
1505
|
+
return body;
|
|
1506
|
+
}
|
|
1507
|
+
return {};
|
|
1508
|
+
}
|
|
1509
|
+
const MAX_JSON_BODY_BYTES = 1_000_000;
|
|
1510
|
+
const JSON_BODY_TIMEOUT_MS = 2_000;
|
|
1511
|
+
function chunkToBuffer(chunk) {
|
|
1512
|
+
if (!chunk)
|
|
1513
|
+
return Buffer.alloc(0);
|
|
1514
|
+
if (Buffer.isBuffer(chunk))
|
|
1515
|
+
return chunk;
|
|
1516
|
+
if (typeof chunk === "string")
|
|
1517
|
+
return Buffer.from(chunk, "utf8");
|
|
1518
|
+
if (chunk instanceof Uint8Array)
|
|
1519
|
+
return Buffer.from(chunk);
|
|
1520
|
+
try {
|
|
1521
|
+
return Buffer.from(JSON.stringify(chunk), "utf8");
|
|
1522
|
+
}
|
|
1523
|
+
catch {
|
|
1524
|
+
return Buffer.from(String(chunk), "utf8");
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
async function readRequestBodyBuffer(req) {
|
|
1528
|
+
const on = req.on ? req.on.bind(req) : null;
|
|
1529
|
+
if (!on)
|
|
1530
|
+
return null;
|
|
1531
|
+
return await new Promise((resolve) => {
|
|
1532
|
+
const chunks = [];
|
|
1533
|
+
let totalBytes = 0;
|
|
1534
|
+
let finished = false;
|
|
1535
|
+
const finish = (buffer) => {
|
|
1536
|
+
if (finished)
|
|
1537
|
+
return;
|
|
1538
|
+
finished = true;
|
|
1539
|
+
clearTimeout(timer);
|
|
1540
|
+
resolve(buffer);
|
|
1541
|
+
};
|
|
1542
|
+
const timer = setTimeout(() => finish(null), JSON_BODY_TIMEOUT_MS);
|
|
1543
|
+
on("data", (chunk) => {
|
|
1544
|
+
const buf = chunkToBuffer(chunk);
|
|
1545
|
+
if (buf.length === 0)
|
|
1546
|
+
return;
|
|
1547
|
+
totalBytes += buf.length;
|
|
1548
|
+
if (totalBytes > MAX_JSON_BODY_BYTES) {
|
|
1549
|
+
finish(null);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
chunks.push(buf);
|
|
1553
|
+
});
|
|
1554
|
+
const onDone = () => {
|
|
1555
|
+
if (chunks.length === 0) {
|
|
1556
|
+
finish(Buffer.alloc(0));
|
|
1557
|
+
}
|
|
1558
|
+
else {
|
|
1559
|
+
finish(Buffer.concat(chunks, totalBytes));
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
const once = (req.once ?? req.on)?.bind(req) ?? null;
|
|
1563
|
+
if (once) {
|
|
1564
|
+
once("end", onDone);
|
|
1565
|
+
once("error", () => finish(null));
|
|
1566
|
+
}
|
|
1567
|
+
else {
|
|
1568
|
+
on("end", onDone);
|
|
1569
|
+
on("error", () => finish(null));
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
async function parseJsonRequest(req) {
|
|
1574
|
+
const body = req.body;
|
|
1575
|
+
if (typeof body === "string" && body.length > 0) {
|
|
1576
|
+
return parseJsonBody(body);
|
|
1577
|
+
}
|
|
1578
|
+
if (Buffer.isBuffer(body) && body.length > 0) {
|
|
1579
|
+
return parseJsonBody(body);
|
|
1580
|
+
}
|
|
1581
|
+
if (body instanceof Uint8Array && body.byteLength > 0) {
|
|
1582
|
+
return parseJsonBody(body);
|
|
1583
|
+
}
|
|
1584
|
+
if (body instanceof ArrayBuffer && body.byteLength > 0) {
|
|
1585
|
+
return parseJsonBody(body);
|
|
1586
|
+
}
|
|
1587
|
+
if (body && typeof body === "object" && !Buffer.isBuffer(body)) {
|
|
1588
|
+
return parseJsonBody(body);
|
|
1589
|
+
}
|
|
1590
|
+
const streamed = await readRequestBodyBuffer(req);
|
|
1591
|
+
if (!streamed || streamed.length === 0) {
|
|
1592
|
+
return {};
|
|
1593
|
+
}
|
|
1594
|
+
return parseJsonBody(streamed);
|
|
1595
|
+
}
|
|
1596
|
+
// =============================================================================
|
|
1597
|
+
// Factory
|
|
1598
|
+
// =============================================================================
|
|
1599
|
+
export function createHttpHandler(config, client, getSnapshot, onboarding, diagnostics, adapters) {
|
|
1600
|
+
const dashboardEnabled = config.dashboardEnabled ??
|
|
1601
|
+
true;
|
|
1602
|
+
const outboxAdapter = adapters?.outbox ?? defaultOutboxAdapter;
|
|
1603
|
+
const openclawAdapter = adapters?.openclaw ?? {};
|
|
1604
|
+
const listAgents = openclawAdapter.listAgents ?? listOpenClawAgents;
|
|
1605
|
+
const spawnAgentTurn = openclawAdapter.spawnAgentTurn ?? spawnOpenClawAgentTurn;
|
|
1606
|
+
const stopProcess = openclawAdapter.stopDetachedProcess ?? stopDetachedProcess;
|
|
1607
|
+
const pidAlive = openclawAdapter.isPidAlive ?? isPidAlive;
|
|
1608
|
+
const telemetryDistinctId = (typeof config.installationId === "string" &&
|
|
1609
|
+
String(config.installationId).trim().length > 0
|
|
1610
|
+
? String(config.installationId).trim()
|
|
1611
|
+
: null) ?? "orgx-openclaw-plugin";
|
|
1612
|
+
const { emitActivitySafe, requestDecisionSafe, checkSpawnGuardSafe, extractSpawnGuardModelTier, buildPolicyEnforcedMessage, resolveDispatchExecutionPolicy, enforceSpawnGuardForDispatch, syncParentRollupsForTask, } = createDispatchLifecycle({
|
|
1613
|
+
client,
|
|
1614
|
+
pluginVersion: config.pluginVersion,
|
|
1615
|
+
randomUUID,
|
|
1616
|
+
safeErrorMessage,
|
|
1617
|
+
stableHash,
|
|
1618
|
+
idempotencyKey,
|
|
1619
|
+
pickString,
|
|
1620
|
+
deriveStructuredActivityBucket,
|
|
1621
|
+
});
|
|
1622
|
+
const { registerArtifactSafe, applyAgentStatusUpdatesSafe, resolveAgentDisplayName, dispatchFallbackWorkstreamTurn, } = createAutopilotOperations({
|
|
1623
|
+
client,
|
|
1624
|
+
randomUUID,
|
|
1625
|
+
safeErrorMessage,
|
|
1626
|
+
idempotencyKey,
|
|
1627
|
+
resolveDispatchExecutionPolicy,
|
|
1628
|
+
enforceSpawnGuardForDispatch,
|
|
1629
|
+
buildPolicyEnforcedMessage,
|
|
1630
|
+
syncParentRollupsForTask,
|
|
1631
|
+
emitActivitySafe,
|
|
1632
|
+
extractSpawnGuardModelTier,
|
|
1633
|
+
upsertAgentContext,
|
|
1634
|
+
upsertRunContext,
|
|
1635
|
+
spawnAgentTurn,
|
|
1636
|
+
upsertAgentRun,
|
|
1637
|
+
});
|
|
1638
|
+
const normalizeRunnerAgentToken = (value) => {
|
|
1639
|
+
if (typeof value !== "string")
|
|
1640
|
+
return null;
|
|
1641
|
+
const trimmed = value.trim();
|
|
1642
|
+
if (!trimmed)
|
|
1643
|
+
return null;
|
|
1644
|
+
const normalized = trimmed.toLowerCase();
|
|
1645
|
+
if (normalized === "main" ||
|
|
1646
|
+
normalized === "undefined" ||
|
|
1647
|
+
normalized === "null" ||
|
|
1648
|
+
normalized === "n/a" ||
|
|
1649
|
+
normalized === "na") {
|
|
1650
|
+
return null;
|
|
1651
|
+
}
|
|
1652
|
+
return trimmed;
|
|
1653
|
+
};
|
|
1654
|
+
const pushRunnerAgent = (target, seen, input) => {
|
|
1655
|
+
const agentId = normalizeRunnerAgentToken(input.id ?? null);
|
|
1656
|
+
const agentName = normalizeRunnerAgentToken(input.name ?? null);
|
|
1657
|
+
if (!agentId && !agentName)
|
|
1658
|
+
return;
|
|
1659
|
+
const resolvedId = agentId ?? agentName;
|
|
1660
|
+
const dedupeKey = resolvedId.toLowerCase();
|
|
1661
|
+
if (seen.has(dedupeKey))
|
|
1662
|
+
return;
|
|
1663
|
+
seen.add(dedupeKey);
|
|
1664
|
+
target.push({
|
|
1665
|
+
id: resolvedId,
|
|
1666
|
+
name: agentName ?? resolvedId,
|
|
1667
|
+
});
|
|
1668
|
+
};
|
|
1669
|
+
const dedupeWithPrimary = (primary, extras) => {
|
|
1670
|
+
const merged = [];
|
|
1671
|
+
const seen = new Set();
|
|
1672
|
+
for (const candidate of [...primary, ...extras]) {
|
|
1673
|
+
const id = normalizeRunnerAgentToken(candidate.id);
|
|
1674
|
+
const name = normalizeRunnerAgentToken(candidate.name);
|
|
1675
|
+
if (!id && !name)
|
|
1676
|
+
continue;
|
|
1677
|
+
const resolvedId = id ?? name;
|
|
1678
|
+
const key = resolvedId.toLowerCase();
|
|
1679
|
+
if (seen.has(key))
|
|
1680
|
+
continue;
|
|
1681
|
+
seen.add(key);
|
|
1682
|
+
merged.push({
|
|
1683
|
+
id: resolvedId,
|
|
1684
|
+
name: name ?? resolvedId,
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
return merged;
|
|
1688
|
+
};
|
|
1689
|
+
const codexBinResolver = createCodexBinResolver();
|
|
1690
|
+
const resolveCodexBinInfo = () => codexBinResolver.resolveCodexBinInfo();
|
|
1691
|
+
const { autoContinueRuns, autoContinueSliceRuns, localInitiativeStatusOverrides, writeRuntimeEvent, autoContinueTickMs: AUTO_CONTINUE_TICK_MS, defaultAutoContinueTokenBudget, defaultAutoContinueMaxParallelSlices, setLocalInitiativeStatusOverride, clearLocalInitiativeStatusOverride, applyLocalInitiativeOverrides, applyLocalInitiativeOverrideToGraph, updateInitiativeAutoContinueState, stopAutoContinueRun, tickAutoContinueRun, tickAllAutoContinue, isInitiativeActiveStatus, runningAutoContinueForWorkstream, getAutoContinueLaneForWorkstream, scheduleAutoFixForWorkstream, startAutoContinueRun, } = createAutoContinueEngine({
|
|
1692
|
+
client,
|
|
1693
|
+
filename: __filename,
|
|
1694
|
+
safeErrorMessage,
|
|
1695
|
+
pidAlive,
|
|
1696
|
+
stopProcess,
|
|
1697
|
+
resolveOrgxAgentForDomain,
|
|
1698
|
+
checkSpawnGuardSafe,
|
|
1699
|
+
syncParentRollupsForTask,
|
|
1700
|
+
emitActivitySafe,
|
|
1701
|
+
requestDecisionSafe,
|
|
1702
|
+
registerArtifactSafe,
|
|
1703
|
+
applyAgentStatusUpdatesSafe,
|
|
1704
|
+
upsertRuntimeInstanceFromHook,
|
|
1705
|
+
broadcastRuntimeSse,
|
|
1706
|
+
clearSnapshotResponseCache,
|
|
1707
|
+
resolveByokEnvOverrides,
|
|
1708
|
+
randomUUID,
|
|
1709
|
+
fetchKickoffContextSafe,
|
|
1710
|
+
renderKickoffMessage,
|
|
1711
|
+
});
|
|
1712
|
+
const nextUpQueueCache = new Map();
|
|
1713
|
+
const nextUpQueueInFlight = new Map();
|
|
1714
|
+
const PROJECT_INITIATIVE_IDS_CACHE_TTL_MS = 20_000;
|
|
1715
|
+
const projectInitiativeIdsCache = new Map();
|
|
1716
|
+
const commandCenterScopeCache = new Map();
|
|
1717
|
+
const nextUpQueueCacheKeyFor = (initiativeId, projectId) => {
|
|
1718
|
+
const normalizedInitiative = initiativeId?.trim() || "__all__";
|
|
1719
|
+
const normalizedProject = projectId?.trim() || "__all__";
|
|
1720
|
+
return `${normalizedProject}::${normalizedInitiative}`;
|
|
1721
|
+
};
|
|
1722
|
+
async function listInitiativeIdsForProject(input) {
|
|
1723
|
+
const projectId = input.projectId.trim();
|
|
1724
|
+
if (!projectId)
|
|
1725
|
+
return [];
|
|
1726
|
+
const cached = projectInitiativeIdsCache.get(projectId);
|
|
1727
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
1728
|
+
return [...cached.ids];
|
|
1729
|
+
}
|
|
1730
|
+
const mapInitiativeIds = (rows, opts) => {
|
|
1731
|
+
const projectScopeId = opts?.projectId?.trim() ?? "";
|
|
1732
|
+
const commandCenterId = opts?.commandCenterId?.trim() ?? "";
|
|
1733
|
+
return rows
|
|
1734
|
+
.map((entry) => {
|
|
1735
|
+
const record = entry;
|
|
1736
|
+
if (projectScopeId) {
|
|
1737
|
+
const rowProjectId = pickString(record, ["project_id", "projectId"]) ?? "";
|
|
1738
|
+
if (rowProjectId !== projectScopeId)
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1741
|
+
if (commandCenterId) {
|
|
1742
|
+
const rowCommandCenterId = pickString(record, [
|
|
1743
|
+
"workspace_id",
|
|
1744
|
+
"workspaceId",
|
|
1745
|
+
"command_center_id",
|
|
1746
|
+
"commandCenterId",
|
|
1747
|
+
]) ?? "";
|
|
1748
|
+
if (rowCommandCenterId !== commandCenterId)
|
|
1749
|
+
return null;
|
|
1750
|
+
}
|
|
1751
|
+
return pickString(record, ["id"]);
|
|
1752
|
+
})
|
|
1753
|
+
.filter((id) => Boolean(id && id.trim().length > 0));
|
|
1754
|
+
};
|
|
1755
|
+
const cacheAndReturn = (ids) => {
|
|
1756
|
+
const normalized = dedupeStrings(ids
|
|
1757
|
+
.map((id) => id.trim())
|
|
1758
|
+
.filter((id) => id.length > 0));
|
|
1759
|
+
projectInitiativeIdsCache.set(projectId, {
|
|
1760
|
+
expiresAt: Date.now() + PROJECT_INITIATIVE_IDS_CACHE_TTL_MS,
|
|
1761
|
+
ids: normalized,
|
|
1762
|
+
});
|
|
1763
|
+
return normalized;
|
|
1764
|
+
};
|
|
1765
|
+
const isKnownCommandCenterScope = async () => {
|
|
1766
|
+
const cachedScope = commandCenterScopeCache.get(projectId);
|
|
1767
|
+
if (cachedScope && cachedScope.expiresAt > Date.now()) {
|
|
1768
|
+
return cachedScope.exists;
|
|
1769
|
+
}
|
|
1770
|
+
const cacheScope = (exists) => {
|
|
1771
|
+
commandCenterScopeCache.set(projectId, {
|
|
1772
|
+
expiresAt: Date.now() + PROJECT_INITIATIVE_IDS_CACHE_TTL_MS,
|
|
1773
|
+
exists,
|
|
1774
|
+
});
|
|
1775
|
+
return exists;
|
|
1776
|
+
};
|
|
1777
|
+
const hasId = (rows) => rows.some((entry) => {
|
|
1778
|
+
const record = entry;
|
|
1779
|
+
const id = pickString(record, ["id"]) ?? "";
|
|
1780
|
+
return id === projectId;
|
|
1781
|
+
});
|
|
1782
|
+
try {
|
|
1783
|
+
const byId = await withSoftTimeout("command center scope lookup", PROJECT_SCOPE_LOOKUP_TIMEOUT_MS, client.listEntities("command_center", {
|
|
1784
|
+
id: projectId,
|
|
1785
|
+
limit: 1,
|
|
1786
|
+
}));
|
|
1787
|
+
const byIdRows = Array.isArray(byId.data) ? byId.data : [];
|
|
1788
|
+
if (hasId(byIdRows))
|
|
1789
|
+
return cacheScope(true);
|
|
1790
|
+
}
|
|
1791
|
+
catch {
|
|
1792
|
+
// continue to all-command-center fallback
|
|
1793
|
+
}
|
|
1794
|
+
try {
|
|
1795
|
+
const all = await withSoftTimeout("command center catalog lookup", PROJECT_SCOPE_LOOKUP_TIMEOUT_MS, client.listEntities("command_center", {
|
|
1796
|
+
limit: 100,
|
|
1797
|
+
}));
|
|
1798
|
+
const allRows = Array.isArray(all.data) ? all.data : [];
|
|
1799
|
+
return cacheScope(hasId(allRows));
|
|
1800
|
+
}
|
|
1801
|
+
catch {
|
|
1802
|
+
return cacheScope(false);
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
const listInitiativesWithFilters = async (filters) => {
|
|
1806
|
+
const rows = [];
|
|
1807
|
+
const pageSize = 100;
|
|
1808
|
+
const seenIds = new Set();
|
|
1809
|
+
let offset = 0;
|
|
1810
|
+
let page = 0;
|
|
1811
|
+
while (page < PROJECT_SCOPE_MAX_INITIATIVE_PAGES) {
|
|
1812
|
+
const result = await withSoftTimeout("initiative scope lookup", PROJECT_SCOPE_LOOKUP_TIMEOUT_MS, client.listEntities("initiative", {
|
|
1813
|
+
...filters,
|
|
1814
|
+
limit: pageSize,
|
|
1815
|
+
offset,
|
|
1816
|
+
}));
|
|
1817
|
+
const pageRows = Array.isArray(result.data) ? result.data : [];
|
|
1818
|
+
let addedCount = 0;
|
|
1819
|
+
for (const entry of pageRows) {
|
|
1820
|
+
const record = entry;
|
|
1821
|
+
const id = pickString(record, ["id"]);
|
|
1822
|
+
if (id && seenIds.has(id))
|
|
1823
|
+
continue;
|
|
1824
|
+
if (id)
|
|
1825
|
+
seenIds.add(id);
|
|
1826
|
+
rows.push(entry);
|
|
1827
|
+
addedCount += 1;
|
|
1828
|
+
}
|
|
1829
|
+
const hasMoreFlag = Boolean(result.pagination?.has_more);
|
|
1830
|
+
const likelyMore = hasMoreFlag || pageRows.length >= pageSize;
|
|
1831
|
+
if (!likelyMore)
|
|
1832
|
+
break;
|
|
1833
|
+
if (addedCount === 0)
|
|
1834
|
+
break;
|
|
1835
|
+
offset += pageRows.length;
|
|
1836
|
+
page += 1;
|
|
1837
|
+
}
|
|
1838
|
+
return rows;
|
|
1839
|
+
};
|
|
1840
|
+
const listLiveInitiativesWithFilters = async (filters) => {
|
|
1841
|
+
// Fast path: request once without status fan-out. Upstream often returns
|
|
1842
|
+
// all relevant rows and this avoids 5x paginated round-trips.
|
|
1843
|
+
const broadRows = await listInitiativesWithFilters(filters);
|
|
1844
|
+
if (broadRows.length > 0)
|
|
1845
|
+
return broadRows;
|
|
1846
|
+
// Backward-compat fallback for upstreams that require explicit status.
|
|
1847
|
+
const rows = [];
|
|
1848
|
+
for (const status of LIVE_WORKSPACE_INITIATIVE_STATUSES) {
|
|
1849
|
+
const statusRows = await listInitiativesWithFilters({
|
|
1850
|
+
...filters,
|
|
1851
|
+
status,
|
|
1852
|
+
});
|
|
1853
|
+
rows.push(...statusRows);
|
|
1854
|
+
}
|
|
1855
|
+
return rows;
|
|
1856
|
+
};
|
|
1857
|
+
try {
|
|
1858
|
+
// Workspace selection in the plugin uses command-center IDs.
|
|
1859
|
+
// Resolve that scope first so broad project queries never leak cross-workspace items.
|
|
1860
|
+
const byCommandCenterIds = mapInitiativeIds(await listLiveInitiativesWithFilters({
|
|
1861
|
+
workspace_id: projectId,
|
|
1862
|
+
command_center_id: projectId,
|
|
1863
|
+
}), { commandCenterId: projectId });
|
|
1864
|
+
if (byCommandCenterIds.length > 0)
|
|
1865
|
+
return cacheAndReturn(byCommandCenterIds);
|
|
1866
|
+
}
|
|
1867
|
+
catch {
|
|
1868
|
+
// continue to project-id fallback
|
|
1869
|
+
}
|
|
1870
|
+
try {
|
|
1871
|
+
// Do not hard-return empty for known command-center scopes here.
|
|
1872
|
+
// Some tenants only populate project_id links, so we continue through
|
|
1873
|
+
// project-id fallbacks before concluding the scope is empty.
|
|
1874
|
+
await isKnownCommandCenterScope();
|
|
1875
|
+
}
|
|
1876
|
+
catch {
|
|
1877
|
+
// continue to project-id fallback
|
|
1878
|
+
}
|
|
1879
|
+
try {
|
|
1880
|
+
const byWorkspaceFallback = mapInitiativeIds(await listLiveInitiativesWithFilters({
|
|
1881
|
+
workspace_id: projectId,
|
|
1882
|
+
command_center_id: projectId,
|
|
1883
|
+
}), { projectId });
|
|
1884
|
+
if (byWorkspaceFallback.length > 0)
|
|
1885
|
+
return cacheAndReturn(byWorkspaceFallback);
|
|
1886
|
+
}
|
|
1887
|
+
catch {
|
|
1888
|
+
// continue to empty fallback
|
|
1889
|
+
}
|
|
1890
|
+
try {
|
|
1891
|
+
return cacheAndReturn([]);
|
|
1892
|
+
}
|
|
1893
|
+
catch {
|
|
1894
|
+
return [];
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
const readNextUpQueueCache = (key, opts) => {
|
|
1898
|
+
const entry = nextUpQueueCache.get(key);
|
|
1899
|
+
if (!entry)
|
|
1900
|
+
return null;
|
|
1901
|
+
const now = Date.now();
|
|
1902
|
+
const allowStale = Boolean(opts?.allowStale);
|
|
1903
|
+
const stillFresh = entry.expiresAt > now;
|
|
1904
|
+
const stillStale = entry.staleUntil > now;
|
|
1905
|
+
if (!stillFresh && !stillStale) {
|
|
1906
|
+
nextUpQueueCache.delete(key);
|
|
1907
|
+
return null;
|
|
1908
|
+
}
|
|
1909
|
+
if (!stillFresh && !allowStale)
|
|
1910
|
+
return null;
|
|
1911
|
+
return {
|
|
1912
|
+
items: entry.payload.items,
|
|
1913
|
+
degraded: [...entry.payload.degraded],
|
|
1914
|
+
};
|
|
1915
|
+
};
|
|
1916
|
+
const writeNextUpQueueCache = (key, payload) => {
|
|
1917
|
+
const now = Date.now();
|
|
1918
|
+
nextUpQueueCache.set(key, {
|
|
1919
|
+
expiresAt: now + NEXT_UP_QUEUE_CACHE_TTL_MS,
|
|
1920
|
+
staleUntil: now + NEXT_UP_QUEUE_STALE_TTL_MS,
|
|
1921
|
+
payload: {
|
|
1922
|
+
items: payload.items,
|
|
1923
|
+
degraded: [...payload.degraded],
|
|
1924
|
+
},
|
|
1925
|
+
});
|
|
1926
|
+
};
|
|
1927
|
+
const clearNextUpQueueCache = (initiativeId) => {
|
|
1928
|
+
const normalized = initiativeId?.trim() || null;
|
|
1929
|
+
if (!normalized) {
|
|
1930
|
+
nextUpQueueCache.clear();
|
|
1931
|
+
nextUpQueueInFlight.clear();
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
for (const key of Array.from(nextUpQueueCache.keys())) {
|
|
1935
|
+
if (key.endsWith(`::${normalized}`) || key.endsWith("::__all__")) {
|
|
1936
|
+
nextUpQueueCache.delete(key);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
for (const key of Array.from(nextUpQueueInFlight.keys())) {
|
|
1940
|
+
if (key.endsWith(`::${normalized}`) || key.endsWith("::__all__")) {
|
|
1941
|
+
nextUpQueueInFlight.delete(key);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
async function buildNextUpQueueUncached(input) {
|
|
1946
|
+
const degraded = [];
|
|
1947
|
+
const requestedInitiativeId = input?.initiativeId?.trim() || null;
|
|
1948
|
+
const requestedProjectId = input?.projectId?.trim() || null;
|
|
1949
|
+
let allowedInitiativeIds = null;
|
|
1950
|
+
if (requestedProjectId && requestedProjectId.length > 0) {
|
|
1951
|
+
const scopedIds = await listInitiativeIdsForProject({
|
|
1952
|
+
projectId: requestedProjectId,
|
|
1953
|
+
});
|
|
1954
|
+
if (scopedIds.length > 0) {
|
|
1955
|
+
allowedInitiativeIds = new Set(scopedIds);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
const pinnedQueue = readNextUpQueuePins();
|
|
1959
|
+
const pinnedRankByKey = new Map();
|
|
1960
|
+
const pinnedByKey = new Map();
|
|
1961
|
+
for (let idx = 0; idx < pinnedQueue.pins.length; idx += 1) {
|
|
1962
|
+
const pin = pinnedQueue.pins[idx];
|
|
1963
|
+
const key = `${pin.initiativeId}:${pin.workstreamId}`;
|
|
1964
|
+
if (!pinnedRankByKey.has(key))
|
|
1965
|
+
pinnedRankByKey.set(key, idx);
|
|
1966
|
+
pinnedByKey.set(key, {
|
|
1967
|
+
preferredTaskId: pin.preferredTaskId ?? null,
|
|
1968
|
+
preferredMilestoneId: pin.preferredMilestoneId ?? null,
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
const suppressedKeySet = new Set();
|
|
1972
|
+
for (const suppression of pinnedQueue.suppressions ?? []) {
|
|
1973
|
+
const initiativeId = suppression.initiativeId?.trim();
|
|
1974
|
+
const workstreamId = suppression.workstreamId?.trim();
|
|
1975
|
+
if (!initiativeId || !workstreamId)
|
|
1976
|
+
continue;
|
|
1977
|
+
if (requestedInitiativeId && initiativeId !== requestedInitiativeId)
|
|
1978
|
+
continue;
|
|
1979
|
+
suppressedKeySet.add(`${initiativeId}:${workstreamId}`);
|
|
1980
|
+
}
|
|
1981
|
+
const isSuppressed = (initiativeId, workstreamId) => suppressedKeySet.has(`${initiativeId}:${workstreamId}`);
|
|
1982
|
+
const initiativeTitleById = new Map();
|
|
1983
|
+
const initiativeStatusById = new Map();
|
|
1984
|
+
const initiativePriorityById = new Map();
|
|
1985
|
+
const snapshotInitiatives = formatInitiatives(getSnapshot());
|
|
1986
|
+
for (const initiative of snapshotInitiatives) {
|
|
1987
|
+
const id = initiative.id?.trim();
|
|
1988
|
+
if (!id)
|
|
1989
|
+
continue;
|
|
1990
|
+
initiativeTitleById.set(id, initiative.title);
|
|
1991
|
+
initiativeStatusById.set(id, initiative.status || "active");
|
|
1992
|
+
}
|
|
1993
|
+
const initiativeResult = await withSoftTimeout("initiative list", PROJECT_SCOPE_LOOKUP_TIMEOUT_MS, listEntitiesSafe(client, "initiative", { limit: 500 })).catch((err) => ({
|
|
1994
|
+
items: [],
|
|
1995
|
+
warning: `initiative unavailable (${safeErrorMessage(err)})`,
|
|
1996
|
+
}));
|
|
1997
|
+
if (initiativeResult.warning)
|
|
1998
|
+
degraded.push(initiativeResult.warning);
|
|
1999
|
+
const initiatives = initiativeResult.items;
|
|
2000
|
+
for (const entity of initiatives) {
|
|
2001
|
+
const record = entity;
|
|
2002
|
+
const id = pickString(record, ["id"]);
|
|
2003
|
+
if (!id)
|
|
2004
|
+
continue;
|
|
2005
|
+
const title = pickString(record, ["title", "name"]);
|
|
2006
|
+
const status = pickString(record, ["status"]);
|
|
2007
|
+
const priority = pickString(record, ["priority", "priority_label", "priorityLabel"]);
|
|
2008
|
+
if (title)
|
|
2009
|
+
initiativeTitleById.set(id, title);
|
|
2010
|
+
if (status)
|
|
2011
|
+
initiativeStatusById.set(id, status);
|
|
2012
|
+
if (priority)
|
|
2013
|
+
initiativePriorityById.set(id, priority);
|
|
2014
|
+
}
|
|
2015
|
+
const initiativeMatchesRequestedProject = (record) => {
|
|
2016
|
+
if (!requestedProjectId)
|
|
2017
|
+
return true;
|
|
2018
|
+
const scopedValue = pickString(record, [
|
|
2019
|
+
"workspace_id",
|
|
2020
|
+
"workspaceId",
|
|
2021
|
+
"command_center_id",
|
|
2022
|
+
"commandCenterId",
|
|
2023
|
+
"project_id",
|
|
2024
|
+
"projectId",
|
|
2025
|
+
]) ?? null;
|
|
2026
|
+
if (!scopedValue)
|
|
2027
|
+
return false;
|
|
2028
|
+
return scopedValue === requestedProjectId;
|
|
2029
|
+
};
|
|
2030
|
+
if (requestedProjectId && !allowedInitiativeIds) {
|
|
2031
|
+
const metadataScopedIds = initiatives
|
|
2032
|
+
.map((entity) => {
|
|
2033
|
+
const record = entity;
|
|
2034
|
+
const id = pickString(record, ["id"]);
|
|
2035
|
+
if (!id)
|
|
2036
|
+
return null;
|
|
2037
|
+
return initiativeMatchesRequestedProject(record) ? id : null;
|
|
2038
|
+
})
|
|
2039
|
+
.filter((value) => Boolean(value));
|
|
2040
|
+
if (metadataScopedIds.length > 0) {
|
|
2041
|
+
allowedInitiativeIds = new Set(metadataScopedIds);
|
|
2042
|
+
degraded.push("workspace initiative scope lookup returned no rows; using metadata scoped initiatives.");
|
|
2043
|
+
}
|
|
2044
|
+
else {
|
|
2045
|
+
degraded.push("workspace initiative scope lookup returned no rows; local queue may be incomplete.");
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
for (const [initiativeId, override] of localInitiativeStatusOverrides.entries()) {
|
|
2049
|
+
initiativeStatusById.set(initiativeId, override.status);
|
|
2050
|
+
}
|
|
2051
|
+
const queueRank = (state) => {
|
|
2052
|
+
if (state === "running")
|
|
2053
|
+
return 0;
|
|
2054
|
+
if (state === "queued")
|
|
2055
|
+
return 1;
|
|
2056
|
+
if (state === "blocked")
|
|
2057
|
+
return 2;
|
|
2058
|
+
return 3;
|
|
2059
|
+
};
|
|
2060
|
+
const sortQueueItems = (a, b) => {
|
|
2061
|
+
const aPinnedRank = pinnedRankByKey.get(`${a.initiativeId}:${a.workstreamId}`);
|
|
2062
|
+
const bPinnedRank = pinnedRankByKey.get(`${b.initiativeId}:${b.workstreamId}`);
|
|
2063
|
+
if (aPinnedRank !== undefined || bPinnedRank !== undefined) {
|
|
2064
|
+
const aRank = aPinnedRank ?? Number.POSITIVE_INFINITY;
|
|
2065
|
+
const bRank = bPinnedRank ?? Number.POSITIVE_INFINITY;
|
|
2066
|
+
if (aRank !== bRank)
|
|
2067
|
+
return aRank - bRank;
|
|
2068
|
+
}
|
|
2069
|
+
const queueDelta = queueRank(a.queueState) - queueRank(b.queueState);
|
|
2070
|
+
if (queueDelta !== 0)
|
|
2071
|
+
return queueDelta;
|
|
2072
|
+
const priorityRank = (value) => {
|
|
2073
|
+
const normalized = (value ?? "").trim().toLowerCase();
|
|
2074
|
+
if (!normalized)
|
|
2075
|
+
return 4;
|
|
2076
|
+
if (normalized === "critical" || normalized === "p0" || normalized === "urgent")
|
|
2077
|
+
return 0;
|
|
2078
|
+
if (normalized === "high" || normalized === "p1")
|
|
2079
|
+
return 1;
|
|
2080
|
+
if (normalized === "medium" || normalized === "normal" || normalized === "p2")
|
|
2081
|
+
return 2;
|
|
2082
|
+
if (normalized === "low" || normalized === "p3")
|
|
2083
|
+
return 3;
|
|
2084
|
+
return 4;
|
|
2085
|
+
};
|
|
2086
|
+
const aInitiativePriority = priorityRank(initiativePriorityById.get(a.initiativeId));
|
|
2087
|
+
const bInitiativePriority = priorityRank(initiativePriorityById.get(b.initiativeId));
|
|
2088
|
+
if (aInitiativePriority !== bInitiativePriority) {
|
|
2089
|
+
return aInitiativePriority - bInitiativePriority;
|
|
2090
|
+
}
|
|
2091
|
+
const aPriority = typeof a.nextTaskPriority === "number" ? a.nextTaskPriority : 999;
|
|
2092
|
+
const bPriority = typeof b.nextTaskPriority === "number" ? b.nextTaskPriority : 999;
|
|
2093
|
+
if (aPriority !== bPriority)
|
|
2094
|
+
return aPriority - bPriority;
|
|
2095
|
+
const aDue = a.nextTaskDueAt ? Date.parse(a.nextTaskDueAt) : Number.POSITIVE_INFINITY;
|
|
2096
|
+
const bDue = b.nextTaskDueAt ? Date.parse(b.nextTaskDueAt) : Number.POSITIVE_INFINITY;
|
|
2097
|
+
if (aDue !== bDue)
|
|
2098
|
+
return aDue - bDue;
|
|
2099
|
+
const init = a.initiativeTitle.localeCompare(b.initiativeTitle);
|
|
2100
|
+
if (init !== 0)
|
|
2101
|
+
return init;
|
|
2102
|
+
return a.workstreamTitle.localeCompare(b.workstreamTitle);
|
|
2103
|
+
};
|
|
2104
|
+
const buildSessionFallbackQueue = async () => {
|
|
2105
|
+
let sessionTree = null;
|
|
2106
|
+
try {
|
|
2107
|
+
sessionTree = await withSoftTimeout("live sessions", NEXT_UP_LIVE_SESSIONS_TIMEOUT_MS, client.getLiveSessions({
|
|
2108
|
+
initiative: requestedInitiativeId,
|
|
2109
|
+
projectId: requestedProjectId,
|
|
2110
|
+
limit: 500,
|
|
2111
|
+
}));
|
|
2112
|
+
}
|
|
2113
|
+
catch (err) {
|
|
2114
|
+
degraded.push(`live sessions fallback unavailable (${safeErrorMessage(err)})`);
|
|
2115
|
+
}
|
|
2116
|
+
const contextStore = readAgentContexts();
|
|
2117
|
+
const contextBundle = {
|
|
2118
|
+
agents: contextStore.agents,
|
|
2119
|
+
runs: contextStore.runs ?? {},
|
|
2120
|
+
};
|
|
2121
|
+
if (!sessionTree) {
|
|
2122
|
+
try {
|
|
2123
|
+
sessionTree = toLocalSessionTree(await loadLocalOpenClawSnapshot(400), 400);
|
|
2124
|
+
}
|
|
2125
|
+
catch (err) {
|
|
2126
|
+
degraded.push(`local sessions fallback unavailable (${safeErrorMessage(err)})`);
|
|
2127
|
+
return [];
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
sessionTree = applyAgentContextsToSessionTree(sessionTree, contextBundle);
|
|
2131
|
+
const grouped = new Map();
|
|
2132
|
+
const parseEpoch = (value) => {
|
|
2133
|
+
const parsed = value ? Date.parse(value) : Number.NaN;
|
|
2134
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
2135
|
+
};
|
|
2136
|
+
for (const node of sessionTree.nodes ?? []) {
|
|
2137
|
+
const initiativeId = (node.initiativeId ?? "").trim();
|
|
2138
|
+
const workstreamId = (node.workstreamId ?? "").trim();
|
|
2139
|
+
if (!initiativeId || !workstreamId)
|
|
2140
|
+
continue;
|
|
2141
|
+
if (requestedInitiativeId && initiativeId !== requestedInitiativeId)
|
|
2142
|
+
continue;
|
|
2143
|
+
if (allowedInitiativeIds && !allowedInitiativeIds.has(initiativeId))
|
|
2144
|
+
continue;
|
|
2145
|
+
const initiativeStatus = initiativeStatusById.get(initiativeId) ?? "active";
|
|
2146
|
+
if (!isInitiativeActiveStatus(initiativeStatus))
|
|
2147
|
+
continue;
|
|
2148
|
+
const key = `${initiativeId}:${workstreamId}`;
|
|
2149
|
+
const epoch = parseEpoch(node.updatedAt ?? node.lastEventAt ?? node.startedAt);
|
|
2150
|
+
const existing = grouped.get(key);
|
|
2151
|
+
if (!existing) {
|
|
2152
|
+
const runnerAgents = [];
|
|
2153
|
+
const runnerAgentSeen = new Set();
|
|
2154
|
+
pushRunnerAgent(runnerAgents, runnerAgentSeen, {
|
|
2155
|
+
id: node.agentId,
|
|
2156
|
+
name: node.agentName,
|
|
2157
|
+
});
|
|
2158
|
+
grouped.set(key, {
|
|
2159
|
+
initiativeId,
|
|
2160
|
+
workstreamId,
|
|
2161
|
+
initiativeTitle: initiativeTitleById.get(initiativeId) ??
|
|
2162
|
+
node.groupLabel ??
|
|
2163
|
+
initiativeId,
|
|
2164
|
+
initiativeStatus,
|
|
2165
|
+
workstreamTitle: `Workstream ${workstreamId.slice(0, 8)}`,
|
|
2166
|
+
statuses: new Set([node.status]),
|
|
2167
|
+
blockers: Array.isArray(node.blockers) ? [...node.blockers] : [],
|
|
2168
|
+
runnerAgents,
|
|
2169
|
+
runnerAgentSeen,
|
|
2170
|
+
latest: node,
|
|
2171
|
+
latestEpoch: epoch,
|
|
2172
|
+
});
|
|
2173
|
+
continue;
|
|
2174
|
+
}
|
|
2175
|
+
existing.statuses.add(node.status);
|
|
2176
|
+
if (Array.isArray(node.blockers)) {
|
|
2177
|
+
for (const blocker of node.blockers) {
|
|
2178
|
+
if (typeof blocker !== "string" || blocker.trim().length === 0)
|
|
2179
|
+
continue;
|
|
2180
|
+
if (!existing.blockers.includes(blocker))
|
|
2181
|
+
existing.blockers.push(blocker);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
pushRunnerAgent(existing.runnerAgents, existing.runnerAgentSeen, {
|
|
2185
|
+
id: node.agentId,
|
|
2186
|
+
name: node.agentName,
|
|
2187
|
+
});
|
|
2188
|
+
if (epoch >= existing.latestEpoch) {
|
|
2189
|
+
existing.latest = node;
|
|
2190
|
+
existing.latestEpoch = epoch;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
const fallbackItems = [];
|
|
2194
|
+
for (const entry of grouped.values()) {
|
|
2195
|
+
const statusValues = Array.from(entry.statuses).map((status) => status.toLowerCase());
|
|
2196
|
+
const hasBlocked = statusValues.some((status) => status === "blocked" || status === "failed") ||
|
|
2197
|
+
entry.blockers.length > 0;
|
|
2198
|
+
const hasRunning = statusValues.some((status) => isInProgressStatus(status));
|
|
2199
|
+
const hasQueued = statusValues.some((status) => status === "queued" || status === "pending");
|
|
2200
|
+
const queueState = hasRunning
|
|
2201
|
+
? "running"
|
|
2202
|
+
: hasBlocked
|
|
2203
|
+
? "blocked"
|
|
2204
|
+
: hasQueued
|
|
2205
|
+
? "queued"
|
|
2206
|
+
: "idle";
|
|
2207
|
+
const latestRunner = [];
|
|
2208
|
+
const latestRunnerSeen = new Set();
|
|
2209
|
+
pushRunnerAgent(latestRunner, latestRunnerSeen, {
|
|
2210
|
+
id: entry.latest.agentId,
|
|
2211
|
+
name: entry.latest.agentName,
|
|
2212
|
+
});
|
|
2213
|
+
const runnerAgents = latestRunner.length > 0
|
|
2214
|
+
? dedupeWithPrimary(latestRunner, entry.runnerAgents)
|
|
2215
|
+
: [...entry.runnerAgents];
|
|
2216
|
+
const primaryRunner = runnerAgents[0] ?? null;
|
|
2217
|
+
const runnerAgentId = primaryRunner?.id ?? "unassigned";
|
|
2218
|
+
const runnerAgentName = primaryRunner?.name ?? "Unassigned";
|
|
2219
|
+
const pinKey = `${entry.initiativeId}:${entry.workstreamId}`;
|
|
2220
|
+
if (isSuppressed(entry.initiativeId, entry.workstreamId) && queueState !== "running") {
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
fallbackItems.push({
|
|
2224
|
+
initiativeId: entry.initiativeId,
|
|
2225
|
+
initiativeTitle: entry.initiativeTitle,
|
|
2226
|
+
initiativeStatus: entry.initiativeStatus,
|
|
2227
|
+
workstreamId: entry.workstreamId,
|
|
2228
|
+
workstreamTitle: entry.workstreamTitle,
|
|
2229
|
+
workstreamStatus: hasBlocked ? "blocked" : hasRunning ? "active" : hasQueued ? "queued" : "idle",
|
|
2230
|
+
nextTaskId: entry.latest.id ?? null,
|
|
2231
|
+
nextTaskTitle: (entry.latest.lastEventSummary ?? "").trim() ||
|
|
2232
|
+
(entry.latest.title ?? "").trim() ||
|
|
2233
|
+
null,
|
|
2234
|
+
nextTaskPriority: null,
|
|
2235
|
+
nextTaskDueAt: null,
|
|
2236
|
+
runnerAgentId,
|
|
2237
|
+
runnerAgentName,
|
|
2238
|
+
runnerAgents,
|
|
2239
|
+
runnerSource: "fallback",
|
|
2240
|
+
queueState,
|
|
2241
|
+
blockReason: hasBlocked
|
|
2242
|
+
? entry.blockers[0] ??
|
|
2243
|
+
(statusValues.includes("failed") ? "Latest run failed" : "Workstream blocked")
|
|
2244
|
+
: null,
|
|
2245
|
+
isPinned: pinnedRankByKey.has(pinKey),
|
|
2246
|
+
pinnedRank: pinnedRankByKey.get(pinKey) ?? null,
|
|
2247
|
+
autoContinue: null,
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
fallbackItems.sort(sortQueueItems);
|
|
2251
|
+
return fallbackItems;
|
|
2252
|
+
};
|
|
2253
|
+
const scopedInitiatives = initiatives.filter((entity) => {
|
|
2254
|
+
const record = entity;
|
|
2255
|
+
const id = pickString(record, ["id"]);
|
|
2256
|
+
if (!id)
|
|
2257
|
+
return false;
|
|
2258
|
+
if (requestedInitiativeId && id !== requestedInitiativeId)
|
|
2259
|
+
return false;
|
|
2260
|
+
if (!initiativeMatchesRequestedProject(record))
|
|
2261
|
+
return false;
|
|
2262
|
+
if (allowedInitiativeIds && !allowedInitiativeIds.has(id))
|
|
2263
|
+
return false;
|
|
2264
|
+
const status = pickString(record, ["status"]);
|
|
2265
|
+
return isInitiativeActiveStatus(status);
|
|
2266
|
+
});
|
|
2267
|
+
const agentCatalogById = new Map();
|
|
2268
|
+
try {
|
|
2269
|
+
const catalog = await withSoftTimeout("listAgents", NEXT_UP_AGENT_CATALOG_TIMEOUT_MS, listAgents());
|
|
2270
|
+
for (const entry of catalog) {
|
|
2271
|
+
if (!entry || typeof entry !== "object")
|
|
2272
|
+
continue;
|
|
2273
|
+
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
2274
|
+
if (!id)
|
|
2275
|
+
continue;
|
|
2276
|
+
const name = typeof entry.name === "string" && entry.name.trim().length > 0
|
|
2277
|
+
? entry.name.trim()
|
|
2278
|
+
: id;
|
|
2279
|
+
agentCatalogById.set(id, { id, name });
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
catch (err) {
|
|
2283
|
+
degraded.push(`agent catalog unavailable (${safeErrorMessage(err)})`);
|
|
2284
|
+
}
|
|
2285
|
+
const liveAgentsByInitiative = new Map();
|
|
2286
|
+
try {
|
|
2287
|
+
const data = await withSoftTimeout("live agents", NEXT_UP_LIVE_AGENTS_TIMEOUT_MS, client.getLiveAgents({
|
|
2288
|
+
initiative: requestedInitiativeId,
|
|
2289
|
+
projectId: requestedProjectId,
|
|
2290
|
+
includeIdle: true,
|
|
2291
|
+
}));
|
|
2292
|
+
for (const raw of Array.isArray(data.agents) ? data.agents : []) {
|
|
2293
|
+
if (!raw || typeof raw !== "object")
|
|
2294
|
+
continue;
|
|
2295
|
+
const row = raw;
|
|
2296
|
+
const initiativeId = pickString(row, ["initiativeId", "initiative_id"]);
|
|
2297
|
+
if (!initiativeId)
|
|
2298
|
+
continue;
|
|
2299
|
+
const id = pickString(row, ["id", "agentId", "agent_id"]) ??
|
|
2300
|
+
pickString(row, ["name", "agentName", "agent_name"]) ??
|
|
2301
|
+
"";
|
|
2302
|
+
const name = pickString(row, ["name", "agentName", "agent_name"]) ??
|
|
2303
|
+
id;
|
|
2304
|
+
if (!id || !name)
|
|
2305
|
+
continue;
|
|
2306
|
+
const list = liveAgentsByInitiative.get(initiativeId) ?? [];
|
|
2307
|
+
list.push({
|
|
2308
|
+
id,
|
|
2309
|
+
name,
|
|
2310
|
+
domain: pickString(row, ["domain", "role"]),
|
|
2311
|
+
});
|
|
2312
|
+
liveAgentsByInitiative.set(initiativeId, list);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
catch (err) {
|
|
2316
|
+
degraded.push(`live agents unavailable (${safeErrorMessage(err)})`);
|
|
2317
|
+
}
|
|
2318
|
+
const processInitiative = async (initiativeEntity) => {
|
|
2319
|
+
const initiativeRecord = initiativeEntity;
|
|
2320
|
+
const initiativeId = pickString(initiativeRecord, ["id"]);
|
|
2321
|
+
if (!initiativeId)
|
|
2322
|
+
return [];
|
|
2323
|
+
const initiativeTitle = pickString(initiativeRecord, ["title", "name"]) ?? initiativeId;
|
|
2324
|
+
const initiativeStatus = pickString(initiativeRecord, ["status"]) ?? "active";
|
|
2325
|
+
let graph;
|
|
2326
|
+
try {
|
|
2327
|
+
graph = applyLocalInitiativeOverrideToGraph(await buildMissionControlGraph(client, initiativeId, { initiativeEntity }));
|
|
2328
|
+
}
|
|
2329
|
+
catch (err) {
|
|
2330
|
+
degraded.push(`graph unavailable for ${initiativeId} (${safeErrorMessage(err)})`);
|
|
2331
|
+
return [];
|
|
2332
|
+
}
|
|
2333
|
+
const itemsForInitiative = [];
|
|
2334
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
2335
|
+
const workstreamNodes = graph.nodes.filter((node) => node.type === "workstream");
|
|
2336
|
+
const runningWorkstreams = new Set();
|
|
2337
|
+
const taskIsReady = (task) => task.dependencyIds.every((depId) => {
|
|
2338
|
+
const dependency = nodeById.get(depId);
|
|
2339
|
+
return dependency ? isDoneStatus(dependency.status) : true;
|
|
2340
|
+
});
|
|
2341
|
+
const taskHasBlockedParent = (task) => {
|
|
2342
|
+
const milestone = task.milestoneId ? nodeById.get(task.milestoneId) ?? null : null;
|
|
2343
|
+
const workstream = task.workstreamId ? nodeById.get(task.workstreamId) ?? null : null;
|
|
2344
|
+
return (milestone?.status?.toLowerCase() === "blocked" ||
|
|
2345
|
+
workstream?.status?.toLowerCase() === "blocked");
|
|
2346
|
+
};
|
|
2347
|
+
const normalizeSliceScope = (value) => {
|
|
2348
|
+
if (value === "task" || value === "milestone" || value === "workstream") {
|
|
2349
|
+
return value;
|
|
2350
|
+
}
|
|
2351
|
+
return null;
|
|
2352
|
+
};
|
|
2353
|
+
const resolveExecutionPolicyFromActiveRuns = (activeRunIds, workstreamId) => {
|
|
2354
|
+
for (const runId of activeRunIds) {
|
|
2355
|
+
const slice = autoContinueSliceRuns.get(runId);
|
|
2356
|
+
if (!slice)
|
|
2357
|
+
continue;
|
|
2358
|
+
if (typeof slice.workstreamId === "string" && slice.workstreamId.trim()) {
|
|
2359
|
+
if (slice.workstreamId.trim() !== workstreamId)
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
const domain = (slice.domain ?? "").trim();
|
|
2363
|
+
const requiredSkills = Array.isArray(slice.requiredSkills)
|
|
2364
|
+
? slice.requiredSkills.filter((skill) => typeof skill === "string" && skill.trim().length > 0)
|
|
2365
|
+
: [];
|
|
2366
|
+
if (!domain || requiredSkills.length === 0)
|
|
2367
|
+
continue;
|
|
2368
|
+
const executionPolicy = {
|
|
2369
|
+
domain,
|
|
2370
|
+
requiredSkills,
|
|
2371
|
+
};
|
|
2372
|
+
if (typeof slice.behaviorConfigId === "string" && slice.behaviorConfigId.trim()) {
|
|
2373
|
+
executionPolicy.profile = slice.behaviorConfigId.trim();
|
|
2374
|
+
}
|
|
2375
|
+
const scope = normalizeSliceScope(slice.scope ?? null);
|
|
2376
|
+
if (scope) {
|
|
2377
|
+
executionPolicy.sliceScopePreference = scope;
|
|
2378
|
+
}
|
|
2379
|
+
return executionPolicy;
|
|
2380
|
+
}
|
|
2381
|
+
return null;
|
|
2382
|
+
};
|
|
2383
|
+
for (const workstream of workstreamNodes) {
|
|
2384
|
+
const workstreamKey = `${initiativeId}:${workstream.id}`;
|
|
2385
|
+
const todoTasks = graph.recentTodos
|
|
2386
|
+
.map((taskId) => nodeById.get(taskId))
|
|
2387
|
+
.filter((node) => node?.type === "task" &&
|
|
2388
|
+
node.workstreamId === workstream.id &&
|
|
2389
|
+
isTodoStatus(node.status));
|
|
2390
|
+
const pinKey = workstreamKey;
|
|
2391
|
+
const pin = pinnedByKey.get(pinKey) ?? null;
|
|
2392
|
+
const preferredTask = pin?.preferredTaskId && nodeById.get(pin.preferredTaskId)
|
|
2393
|
+
? nodeById.get(pin.preferredTaskId) ?? null
|
|
2394
|
+
: null;
|
|
2395
|
+
const preferredMilestone = pin?.preferredMilestoneId && nodeById.get(pin.preferredMilestoneId)
|
|
2396
|
+
? nodeById.get(pin.preferredMilestoneId) ?? null
|
|
2397
|
+
: null;
|
|
2398
|
+
const preferredCandidates = [];
|
|
2399
|
+
if (preferredTask &&
|
|
2400
|
+
preferredTask.type === "task" &&
|
|
2401
|
+
preferredTask.workstreamId === workstream.id &&
|
|
2402
|
+
isTodoStatus(preferredTask.status)) {
|
|
2403
|
+
preferredCandidates.push(preferredTask);
|
|
2404
|
+
}
|
|
2405
|
+
if (preferredMilestone && preferredMilestone.type === "milestone") {
|
|
2406
|
+
for (const node of todoTasks) {
|
|
2407
|
+
if (node.milestoneId === preferredMilestone.id)
|
|
2408
|
+
preferredCandidates.push(node);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
const readyTask = todoTasks.find((task) => taskIsReady(task) && !taskHasBlockedParent(task));
|
|
2412
|
+
const preferredReadyTask = preferredCandidates.find((task) => taskIsReady(task) && !taskHasBlockedParent(task));
|
|
2413
|
+
const candidateTask = preferredReadyTask ?? readyTask ?? todoTasks[0] ?? null;
|
|
2414
|
+
const autoContinueRun = runningAutoContinueForWorkstream(initiativeId, workstream.id);
|
|
2415
|
+
const autoContinueLane = getAutoContinueLaneForWorkstream(initiativeId, workstream.id);
|
|
2416
|
+
const laneState = autoContinueLane?.state ?? null;
|
|
2417
|
+
const scopedAllowedWorkstreams = Array.isArray(autoContinueRun?.allowedWorkstreamIds)
|
|
2418
|
+
? (autoContinueRun.allowedWorkstreamIds
|
|
2419
|
+
.filter((id) => typeof id === "string" && id.trim().length > 0)
|
|
2420
|
+
.map((id) => id.trim()))
|
|
2421
|
+
: [];
|
|
2422
|
+
const runScopedToCurrentWorkstream = scopedAllowedWorkstreams.length === 1 &&
|
|
2423
|
+
scopedAllowedWorkstreams[0] === workstream.id &&
|
|
2424
|
+
autoContinueRun?.status === "running";
|
|
2425
|
+
const activeRunIds = Array.isArray(autoContinueRun?.activeSliceRunIds)
|
|
2426
|
+
? autoContinueRun.activeSliceRunIds
|
|
2427
|
+
.filter((id) => typeof id === "string" && id.trim().length > 0)
|
|
2428
|
+
.map((id) => id.trim())
|
|
2429
|
+
: [];
|
|
2430
|
+
const activeTaskId = (autoContinueLane?.activeTaskIds?.[0]?.trim() ||
|
|
2431
|
+
autoContinueRun?.activeTaskId?.trim() ||
|
|
2432
|
+
null) ??
|
|
2433
|
+
null;
|
|
2434
|
+
const activeTaskNode = activeTaskId ? nodeById.get(activeTaskId) ?? null : null;
|
|
2435
|
+
const policyTask = candidateTask ??
|
|
2436
|
+
activeTaskNode ??
|
|
2437
|
+
todoTasks.find((task) => task.workstreamId === workstream.id) ??
|
|
2438
|
+
null;
|
|
2439
|
+
const derivedExecutionPolicy = policyTask
|
|
2440
|
+
? deriveExecutionPolicy(policyTask, workstream)
|
|
2441
|
+
: null;
|
|
2442
|
+
const activeExecutionPolicy = resolveExecutionPolicyFromActiveRuns(activeRunIds, workstream.id);
|
|
2443
|
+
const executionPolicy = derivedExecutionPolicy ?? activeExecutionPolicy;
|
|
2444
|
+
const runScope = normalizeSliceScope(autoContinueRun?.scope ?? null);
|
|
2445
|
+
const preferredPolicyScope = normalizeSliceScope(executionPolicy?.sliceScopePreference ?? null);
|
|
2446
|
+
const defaultScope = runScope ??
|
|
2447
|
+
(preferredPolicyScope && preferredPolicyScope !== "task"
|
|
2448
|
+
? preferredPolicyScope
|
|
2449
|
+
: pin?.preferredMilestoneId
|
|
2450
|
+
? "milestone"
|
|
2451
|
+
: "task");
|
|
2452
|
+
const scopeSelection = selectSliceTasksByScope({
|
|
2453
|
+
scope: defaultScope,
|
|
2454
|
+
workstreamId: workstream.id,
|
|
2455
|
+
milestoneId: pin?.preferredMilestoneId ?? null,
|
|
2456
|
+
recentTodos: graph.recentTodos,
|
|
2457
|
+
nodeById,
|
|
2458
|
+
includeVerification: autoContinueRun?.includeVerification ?? false,
|
|
2459
|
+
});
|
|
2460
|
+
const cappedSliceTasks = typeof executionPolicy?.maxSliceTasks === "number" &&
|
|
2461
|
+
executionPolicy.maxSliceTasks > 0
|
|
2462
|
+
? scopeSelection.tasks.slice(0, executionPolicy.maxSliceTasks)
|
|
2463
|
+
: scopeSelection.tasks;
|
|
2464
|
+
const sliceTaskIds = cappedSliceTasks.length > 0
|
|
2465
|
+
? cappedSliceTasks.map((task) => task.id)
|
|
2466
|
+
: candidateTask?.id
|
|
2467
|
+
? [candidateTask.id]
|
|
2468
|
+
: activeTaskId
|
|
2469
|
+
? [activeTaskId]
|
|
2470
|
+
: [];
|
|
2471
|
+
const sliceMilestoneId = defaultScope === "milestone"
|
|
2472
|
+
? scopeSelection.milestoneIds[0] ?? pin?.preferredMilestoneId ?? null
|
|
2473
|
+
: null;
|
|
2474
|
+
let queueState = laneState === "running"
|
|
2475
|
+
? "running"
|
|
2476
|
+
: runScopedToCurrentWorkstream
|
|
2477
|
+
? "running"
|
|
2478
|
+
: candidateTask
|
|
2479
|
+
? "queued"
|
|
2480
|
+
: "idle";
|
|
2481
|
+
let blockReason = null;
|
|
2482
|
+
if (laneState === "blocked") {
|
|
2483
|
+
queueState = "blocked";
|
|
2484
|
+
blockReason = autoContinueLane?.blockedReason ?? "Blocked";
|
|
2485
|
+
}
|
|
2486
|
+
else if (laneState === "waiting_dependency") {
|
|
2487
|
+
queueState = "blocked";
|
|
2488
|
+
if (Array.isArray(autoContinueLane?.waitingOnWorkstreamIds) &&
|
|
2489
|
+
autoContinueLane.waitingOnWorkstreamIds.length > 0) {
|
|
2490
|
+
const waitingTitles = autoContinueLane.waitingOnWorkstreamIds
|
|
2491
|
+
.map((id) => {
|
|
2492
|
+
const node = nodeById.get(id);
|
|
2493
|
+
return node?.type === "workstream" ? node.title : id;
|
|
2494
|
+
})
|
|
2495
|
+
.filter(Boolean);
|
|
2496
|
+
blockReason =
|
|
2497
|
+
waitingTitles.length > 0
|
|
2498
|
+
? `Waiting on ${waitingTitles.slice(0, 2).join(", ")}${waitingTitles.length > 2 ? "…" : ""}`
|
|
2499
|
+
: "Waiting on dependency workstreams";
|
|
2500
|
+
}
|
|
2501
|
+
else {
|
|
2502
|
+
blockReason = "Waiting on dependency workstreams";
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
else if (laneState === "rate_limited") {
|
|
2506
|
+
queueState = "blocked";
|
|
2507
|
+
blockReason = autoContinueLane?.blockedReason ?? "Rate-limited";
|
|
2508
|
+
}
|
|
2509
|
+
if (!autoContinueRun && !readyTask && candidateTask) {
|
|
2510
|
+
queueState = "blocked";
|
|
2511
|
+
const blockedDeps = candidateTask.dependencyIds
|
|
2512
|
+
.map((depId) => nodeById.get(depId))
|
|
2513
|
+
.filter((dependency) => Boolean(dependency && !isDoneStatus(dependency.status)))
|
|
2514
|
+
.map((dependency) => dependency.title);
|
|
2515
|
+
if (blockedDeps.length > 0) {
|
|
2516
|
+
blockReason = `Waiting on ${blockedDeps.slice(0, 2).join(", ")}${blockedDeps.length > 2 ? "…" : ""}`;
|
|
2517
|
+
}
|
|
2518
|
+
else if (taskHasBlockedParent(candidateTask)) {
|
|
2519
|
+
blockReason = "Parent milestone or workstream is blocked";
|
|
2520
|
+
}
|
|
2521
|
+
else if (!taskIsReady(candidateTask)) {
|
|
2522
|
+
blockReason = "Task prerequisites are not complete";
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
if (!candidateTask && !autoContinueRun && !pin) {
|
|
2526
|
+
continue;
|
|
2527
|
+
}
|
|
2528
|
+
if (isSuppressed(initiativeId, workstream.id) && queueState !== "running") {
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
runningWorkstreams.add(workstream.id);
|
|
2532
|
+
const assignedRunnerAgents = [];
|
|
2533
|
+
const assignedRunnerSeen = new Set();
|
|
2534
|
+
for (const agent of workstream.assignedAgents) {
|
|
2535
|
+
pushRunnerAgent(assignedRunnerAgents, assignedRunnerSeen, {
|
|
2536
|
+
id: agent.id,
|
|
2537
|
+
name: agent.name,
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
const inferredRunnerAgents = [];
|
|
2541
|
+
const inferredRunnerSeen = new Set();
|
|
2542
|
+
for (const agent of graph.initiative.assignedAgents) {
|
|
2543
|
+
pushRunnerAgent(inferredRunnerAgents, inferredRunnerSeen, {
|
|
2544
|
+
id: agent.id,
|
|
2545
|
+
name: agent.name,
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
for (const agent of liveAgentsByInitiative.get(initiativeId) ?? []) {
|
|
2549
|
+
pushRunnerAgent(inferredRunnerAgents, inferredRunnerSeen, {
|
|
2550
|
+
id: agent.id,
|
|
2551
|
+
name: agent.name,
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
if (autoContinueRun?.agentId) {
|
|
2555
|
+
pushRunnerAgent(inferredRunnerAgents, inferredRunnerSeen, {
|
|
2556
|
+
id: autoContinueRun.agentId,
|
|
2557
|
+
name: agentCatalogById.get(autoContinueRun.agentId)?.name ??
|
|
2558
|
+
autoContinueRun.agentId,
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
const runnerAgents = assignedRunnerAgents.length > 0 ? assignedRunnerAgents : inferredRunnerAgents;
|
|
2562
|
+
const runnerSource = assignedRunnerAgents.length > 0
|
|
2563
|
+
? "assigned"
|
|
2564
|
+
: runnerAgents.length > 0
|
|
2565
|
+
? "inferred"
|
|
2566
|
+
: "fallback";
|
|
2567
|
+
const primaryRunner = runnerAgents[0] ?? null;
|
|
2568
|
+
const runnerAgentId = primaryRunner?.id ?? "unassigned";
|
|
2569
|
+
const runnerAgentName = primaryRunner?.name ?? "Unassigned";
|
|
2570
|
+
itemsForInitiative.push({
|
|
2571
|
+
initiativeId,
|
|
2572
|
+
initiativeTitle,
|
|
2573
|
+
initiativeStatus,
|
|
2574
|
+
workstreamId: workstream.id,
|
|
2575
|
+
workstreamTitle: workstream.title,
|
|
2576
|
+
workstreamStatus: workstream.status,
|
|
2577
|
+
nextTaskId: candidateTask?.id ??
|
|
2578
|
+
activeTaskId,
|
|
2579
|
+
nextTaskTitle: candidateTask?.title ??
|
|
2580
|
+
((activeTaskId)
|
|
2581
|
+
? nodeById.get(activeTaskId)?.title ?? null
|
|
2582
|
+
: null),
|
|
2583
|
+
nextTaskPriority: candidateTask?.priorityNum ?? null,
|
|
2584
|
+
nextTaskDueAt: candidateTask?.dueDate ?? null,
|
|
2585
|
+
runnerAgentId,
|
|
2586
|
+
runnerAgentName,
|
|
2587
|
+
runnerAgents,
|
|
2588
|
+
runnerSource,
|
|
2589
|
+
queueState,
|
|
2590
|
+
blockReason,
|
|
2591
|
+
isPinned: Boolean(pin),
|
|
2592
|
+
pinnedRank: pin ? (pinnedRankByKey.get(pinKey) ?? null) : null,
|
|
2593
|
+
sliceScope: defaultScope,
|
|
2594
|
+
sliceTaskIds,
|
|
2595
|
+
sliceTaskCount: sliceTaskIds.length,
|
|
2596
|
+
sliceMilestoneId,
|
|
2597
|
+
executionPolicy,
|
|
2598
|
+
autoContinue: autoContinueRun
|
|
2599
|
+
? {
|
|
2600
|
+
status: autoContinueRun.status,
|
|
2601
|
+
activeTaskId: autoContinueRun.activeTaskId,
|
|
2602
|
+
activeRunId: autoContinueRun.activeRunId,
|
|
2603
|
+
activeTaskIds: Array.isArray(autoContinueRun.activeTaskIds)
|
|
2604
|
+
? autoContinueRun.activeTaskIds
|
|
2605
|
+
: [],
|
|
2606
|
+
activeRunIds: Array.isArray(autoContinueRun.activeSliceRunIds)
|
|
2607
|
+
? autoContinueRun.activeSliceRunIds
|
|
2608
|
+
: [],
|
|
2609
|
+
laneState,
|
|
2610
|
+
laneBlockedReason: autoContinueLane?.blockedReason ?? null,
|
|
2611
|
+
laneWaitingOnWorkstreamIds: Array.isArray(autoContinueLane?.waitingOnWorkstreamIds)
|
|
2612
|
+
? autoContinueLane.waitingOnWorkstreamIds
|
|
2613
|
+
: [],
|
|
2614
|
+
laneRetryAt: autoContinueLane?.retryAt ?? null,
|
|
2615
|
+
maxParallelSlices: typeof autoContinueRun.maxParallelSlices === "number"
|
|
2616
|
+
? autoContinueRun.maxParallelSlices
|
|
2617
|
+
: 1,
|
|
2618
|
+
parallelMode: (typeof autoContinueRun.parallelMode === "string" &&
|
|
2619
|
+
autoContinueRun.parallelMode.toLowerCase() === "iwmt"
|
|
2620
|
+
? "iwmt"
|
|
2621
|
+
: "iwmt"),
|
|
2622
|
+
stopReason: autoContinueRun.stopReason,
|
|
2623
|
+
updatedAt: autoContinueRun.updatedAt,
|
|
2624
|
+
}
|
|
2625
|
+
: null,
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
const run = autoContinueRuns.get(initiativeId);
|
|
2629
|
+
if (run &&
|
|
2630
|
+
(run.status === "running" || run.status === "stopping") &&
|
|
2631
|
+
Array.isArray(run.allowedWorkstreamIds) &&
|
|
2632
|
+
run.allowedWorkstreamIds.length > 0) {
|
|
2633
|
+
for (const workstreamId of run.allowedWorkstreamIds) {
|
|
2634
|
+
if (runningWorkstreams.has(workstreamId))
|
|
2635
|
+
continue;
|
|
2636
|
+
const workstream = nodeById.get(workstreamId);
|
|
2637
|
+
if (!workstream || workstream.type !== "workstream")
|
|
2638
|
+
continue;
|
|
2639
|
+
const lane = getAutoContinueLaneForWorkstream(initiativeId, workstream.id);
|
|
2640
|
+
if (!lane &&
|
|
2641
|
+
!(typeof run.activeRunId === "string" && run.activeRunId.trim().length > 0)) {
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2644
|
+
const laneState = lane?.state ?? null;
|
|
2645
|
+
const activeRunIds = Array.isArray(run.activeSliceRunIds)
|
|
2646
|
+
? run.activeSliceRunIds
|
|
2647
|
+
.filter((id) => typeof id === "string" && id.trim().length > 0)
|
|
2648
|
+
.map((id) => id.trim())
|
|
2649
|
+
: [];
|
|
2650
|
+
const activeTaskId = lane?.activeTaskIds?.[0] ?? run.activeTaskId;
|
|
2651
|
+
const activeTaskNode = activeTaskId ? nodeById.get(activeTaskId) ?? null : null;
|
|
2652
|
+
const executionPolicy = (activeTaskNode ? deriveExecutionPolicy(activeTaskNode, workstream) : null) ??
|
|
2653
|
+
resolveExecutionPolicyFromActiveRuns(activeRunIds, workstream.id);
|
|
2654
|
+
const sliceScope = normalizeSliceScope(run.scope ?? null) ?? "task";
|
|
2655
|
+
const sliceTaskIds = lane?.activeTaskIds?.length
|
|
2656
|
+
? lane.activeTaskIds
|
|
2657
|
+
: activeTaskId
|
|
2658
|
+
? [activeTaskId]
|
|
2659
|
+
: [];
|
|
2660
|
+
const queueState = laneState === "running"
|
|
2661
|
+
? "running"
|
|
2662
|
+
: laneState === "blocked" ||
|
|
2663
|
+
laneState === "waiting_dependency" ||
|
|
2664
|
+
laneState === "rate_limited"
|
|
2665
|
+
? "blocked"
|
|
2666
|
+
: "queued";
|
|
2667
|
+
if (isSuppressed(initiativeId, workstream.id) && queueState !== "running") {
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
const runRunnerAgents = [];
|
|
2671
|
+
const runRunnerSeen = new Set();
|
|
2672
|
+
pushRunnerAgent(runRunnerAgents, runRunnerSeen, {
|
|
2673
|
+
id: run.agentId,
|
|
2674
|
+
name: agentCatalogById.get(run.agentId)?.name ?? run.agentId,
|
|
2675
|
+
});
|
|
2676
|
+
const runPrimaryRunner = runRunnerAgents[0] ?? null;
|
|
2677
|
+
itemsForInitiative.push({
|
|
2678
|
+
initiativeId,
|
|
2679
|
+
initiativeTitle,
|
|
2680
|
+
initiativeStatus,
|
|
2681
|
+
workstreamId: workstream.id,
|
|
2682
|
+
workstreamTitle: workstream.title,
|
|
2683
|
+
workstreamStatus: workstream.status,
|
|
2684
|
+
nextTaskId: activeTaskId ?? null,
|
|
2685
|
+
nextTaskTitle: activeTaskId
|
|
2686
|
+
? nodeById.get(activeTaskId)?.title ?? null
|
|
2687
|
+
: null,
|
|
2688
|
+
nextTaskPriority: null,
|
|
2689
|
+
nextTaskDueAt: null,
|
|
2690
|
+
runnerAgentId: runPrimaryRunner?.id ?? "unassigned",
|
|
2691
|
+
runnerAgentName: runPrimaryRunner?.name ?? "Unassigned",
|
|
2692
|
+
runnerAgents: runRunnerAgents,
|
|
2693
|
+
runnerSource: runPrimaryRunner ? "inferred" : "fallback",
|
|
2694
|
+
queueState,
|
|
2695
|
+
blockReason: queueState === "blocked"
|
|
2696
|
+
? lane?.blockedReason ?? "Blocked"
|
|
2697
|
+
: null,
|
|
2698
|
+
isPinned: Boolean(pinnedByKey.get(`${initiativeId}:${workstream.id}`)),
|
|
2699
|
+
pinnedRank: pinnedRankByKey.get(`${initiativeId}:${workstream.id}`) ?? null,
|
|
2700
|
+
sliceScope,
|
|
2701
|
+
sliceTaskIds,
|
|
2702
|
+
sliceTaskCount: sliceTaskIds.length,
|
|
2703
|
+
sliceMilestoneId: sliceScope === "milestone"
|
|
2704
|
+
? activeTaskNode?.milestoneId ?? null
|
|
2705
|
+
: null,
|
|
2706
|
+
executionPolicy,
|
|
2707
|
+
autoContinue: {
|
|
2708
|
+
status: run.status,
|
|
2709
|
+
activeTaskId: run.activeTaskId,
|
|
2710
|
+
activeRunId: run.activeRunId,
|
|
2711
|
+
activeTaskIds: Array.isArray(run.activeTaskIds)
|
|
2712
|
+
? run.activeTaskIds
|
|
2713
|
+
: [],
|
|
2714
|
+
activeRunIds: Array.isArray(run.activeSliceRunIds)
|
|
2715
|
+
? run.activeSliceRunIds
|
|
2716
|
+
: [],
|
|
2717
|
+
laneState,
|
|
2718
|
+
laneBlockedReason: lane?.blockedReason ?? null,
|
|
2719
|
+
laneWaitingOnWorkstreamIds: Array.isArray(lane?.waitingOnWorkstreamIds)
|
|
2720
|
+
? lane.waitingOnWorkstreamIds
|
|
2721
|
+
: [],
|
|
2722
|
+
laneRetryAt: lane?.retryAt ?? null,
|
|
2723
|
+
maxParallelSlices: typeof run.maxParallelSlices === "number"
|
|
2724
|
+
? run.maxParallelSlices
|
|
2725
|
+
: 1,
|
|
2726
|
+
parallelMode: typeof run.parallelMode === "string" &&
|
|
2727
|
+
run.parallelMode.toLowerCase() === "iwmt"
|
|
2728
|
+
? "iwmt"
|
|
2729
|
+
: "iwmt",
|
|
2730
|
+
stopReason: run.stopReason,
|
|
2731
|
+
updatedAt: run.updatedAt,
|
|
2732
|
+
},
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
return itemsForInitiative;
|
|
2737
|
+
};
|
|
2738
|
+
const byInitiative = await mapWithConcurrency(scopedInitiatives, NEXT_UP_GRAPH_CONCURRENCY, processInitiative);
|
|
2739
|
+
const items = byInitiative.flat();
|
|
2740
|
+
if (items.length === 0) {
|
|
2741
|
+
const fallbackItems = await buildSessionFallbackQueue();
|
|
2742
|
+
if (fallbackItems.length > 0) {
|
|
2743
|
+
degraded.push("Using session-derived Next Up fallback.");
|
|
2744
|
+
items.push(...fallbackItems);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
items.sort(sortQueueItems);
|
|
2748
|
+
return { items, degraded };
|
|
2749
|
+
}
|
|
2750
|
+
async function buildNextUpQueue(input) {
|
|
2751
|
+
const key = nextUpQueueCacheKeyFor(input?.initiativeId?.trim() || null, input?.projectId?.trim() || null);
|
|
2752
|
+
const fresh = readNextUpQueueCache(key, { allowStale: false });
|
|
2753
|
+
if (fresh)
|
|
2754
|
+
return fresh;
|
|
2755
|
+
const inFlight = nextUpQueueInFlight.get(key) ?? null;
|
|
2756
|
+
if (inFlight) {
|
|
2757
|
+
const stale = readNextUpQueueCache(key, { allowStale: true });
|
|
2758
|
+
if (stale) {
|
|
2759
|
+
return {
|
|
2760
|
+
...stale,
|
|
2761
|
+
degraded: dedupeStrings([
|
|
2762
|
+
...stale.degraded,
|
|
2763
|
+
"Refreshing Next Up queue in background.",
|
|
2764
|
+
]),
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
return await inFlight;
|
|
2768
|
+
}
|
|
2769
|
+
const work = (async () => {
|
|
2770
|
+
const result = await buildNextUpQueueUncached(input);
|
|
2771
|
+
writeNextUpQueueCache(key, result);
|
|
2772
|
+
return result;
|
|
2773
|
+
})();
|
|
2774
|
+
nextUpQueueInFlight.set(key, work);
|
|
2775
|
+
try {
|
|
2776
|
+
const stale = readNextUpQueueCache(key, { allowStale: true });
|
|
2777
|
+
if (stale) {
|
|
2778
|
+
void work.catch(() => {
|
|
2779
|
+
// best effort refresh
|
|
2780
|
+
});
|
|
2781
|
+
return {
|
|
2782
|
+
...stale,
|
|
2783
|
+
degraded: dedupeStrings([
|
|
2784
|
+
...stale.degraded,
|
|
2785
|
+
"Using recent Next Up queue while refreshing.",
|
|
2786
|
+
]),
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
return await work;
|
|
2790
|
+
}
|
|
2791
|
+
finally {
|
|
2792
|
+
nextUpQueueInFlight.delete(key);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
// Prime queue cache shortly after boot so first dashboard paint is not cold.
|
|
2796
|
+
const prewarmNextUpQueue = () => {
|
|
2797
|
+
void buildNextUpQueue({ initiativeId: null, projectId: null }).catch(() => {
|
|
2798
|
+
// best effort prewarm only
|
|
2799
|
+
});
|
|
2800
|
+
};
|
|
2801
|
+
const nextUpPrewarmTimer = setTimeout(prewarmNextUpQueue, 75);
|
|
2802
|
+
nextUpPrewarmTimer.unref?.();
|
|
2803
|
+
const autoContinueTimer = setInterval(() => {
|
|
2804
|
+
void tickAllAutoContinue();
|
|
2805
|
+
}, AUTO_CONTINUE_TICK_MS);
|
|
2806
|
+
autoContinueTimer.unref?.();
|
|
2807
|
+
const apiRouter = createRouter();
|
|
2808
|
+
registerOnboardingRoutes(apiRouter, {
|
|
2809
|
+
onboarding,
|
|
2810
|
+
parseJsonRequest,
|
|
2811
|
+
pickString: (input, keys) => pickString(input && typeof input === "object"
|
|
2812
|
+
? input
|
|
2813
|
+
: {}, keys),
|
|
2814
|
+
pickHeaderString: (headers, names) => pickHeaderString(headers && typeof headers === "object"
|
|
2815
|
+
? headers
|
|
2816
|
+
: {}, names),
|
|
2817
|
+
isUserScopedApiKey,
|
|
2818
|
+
sendJson,
|
|
2819
|
+
safeErrorMessage,
|
|
2820
|
+
getOnboardingState,
|
|
2821
|
+
});
|
|
2822
|
+
registerSummaryRoutes(apiRouter, {
|
|
2823
|
+
getSnapshot,
|
|
2824
|
+
getOrgSnapshot: () => client.getOrgSnapshot(),
|
|
2825
|
+
sendJson,
|
|
2826
|
+
writeHead: (response, status, headers) => response.writeHead(status, headers),
|
|
2827
|
+
end: (response) => response.end(),
|
|
2828
|
+
securityHeaders: SECURITY_HEADERS,
|
|
2829
|
+
corsHeaders: CORS_HEADERS,
|
|
2830
|
+
formatStatus,
|
|
2831
|
+
formatAgents,
|
|
2832
|
+
formatActivity,
|
|
2833
|
+
formatInitiatives,
|
|
2834
|
+
getOnboardingState: async () => getOnboardingState(await onboarding.getStatus()),
|
|
2835
|
+
});
|
|
2836
|
+
registerUsageRoutes(apiRouter, {
|
|
2837
|
+
client,
|
|
2838
|
+
listActivityPage: ({ limit, runId, since, until, cursor }) => listActivityPage({ limit, runId, since, until, cursor }),
|
|
2839
|
+
sendJson,
|
|
2840
|
+
safeErrorMessage,
|
|
2841
|
+
});
|
|
2842
|
+
registerAgentSuiteRoutes(apiRouter, {
|
|
2843
|
+
pluginVersion: config.pluginVersion,
|
|
2844
|
+
telemetryDistinctId,
|
|
2845
|
+
parseJsonRequest,
|
|
2846
|
+
resolveSkillPackOverrides: ({ force }) => resolveSkillPackOverrides({ client, force }),
|
|
2847
|
+
readSkillPackState,
|
|
2848
|
+
computeOrgxAgentSuitePlan,
|
|
2849
|
+
applyOrgxAgentSuitePlan,
|
|
2850
|
+
generateAgentSuiteOperationId,
|
|
2851
|
+
updateSkillPackPolicy,
|
|
2852
|
+
rollbackSkillPackPolicy,
|
|
2853
|
+
fetchAgentRuntimeSettings: ({ workspaceId, projectId } = {}) => client.getClientAgentRuntimeSettings({
|
|
2854
|
+
workspaceId: workspaceId ?? projectId ?? null,
|
|
2855
|
+
}),
|
|
2856
|
+
updateAgentRuntimeSettings: (payload) => client.updateClientAgentRuntimeSettings(payload),
|
|
2857
|
+
posthogCapture,
|
|
2858
|
+
sendJson,
|
|
2859
|
+
safeErrorMessage,
|
|
2860
|
+
});
|
|
2861
|
+
registerDebugRoutes(apiRouter, {
|
|
2862
|
+
sendJson,
|
|
2863
|
+
safeErrorMessage,
|
|
2864
|
+
resolveCodexBinInfo,
|
|
2865
|
+
getCachedCodexProbeSummary: () => codexBinResolver.getCachedCodexProbeSummary(),
|
|
2866
|
+
});
|
|
2867
|
+
registerAgentsCatalogRoutes(apiRouter, {
|
|
2868
|
+
listAgents,
|
|
2869
|
+
loadLocalSnapshot: () => loadLocalOpenClawSnapshot(240).catch(() => null),
|
|
2870
|
+
readAgentContexts,
|
|
2871
|
+
readAgentRuns,
|
|
2872
|
+
sendJson,
|
|
2873
|
+
safeErrorMessage,
|
|
2874
|
+
});
|
|
2875
|
+
registerSentinelsCatalogRoutes(apiRouter, {
|
|
2876
|
+
sendJson,
|
|
2877
|
+
safeErrorMessage,
|
|
2878
|
+
});
|
|
2879
|
+
registerMissionControlReadRoutes(apiRouter, {
|
|
2880
|
+
autoContinueRuns,
|
|
2881
|
+
defaultAutoContinueTokenBudget,
|
|
2882
|
+
defaultAutoContinueMaxParallelSlices,
|
|
2883
|
+
autoContinueTickMs: AUTO_CONTINUE_TICK_MS,
|
|
2884
|
+
buildMissionControlGraph: (initiativeId) => buildMissionControlGraph(client, initiativeId),
|
|
2885
|
+
applyLocalInitiativeOverrideToGraph: (graph) => applyLocalInitiativeOverrideToGraph(graph),
|
|
2886
|
+
listInitiativeIdsForProject: ({ projectId }) => listInitiativeIdsForProject({ projectId }),
|
|
2887
|
+
buildNextUpQueue,
|
|
2888
|
+
rawRequest: (requestMethod, requestPath, body) => client.rawRequest(requestMethod, requestPath, body),
|
|
2889
|
+
sendJson,
|
|
2890
|
+
safeErrorMessage,
|
|
2891
|
+
});
|
|
2892
|
+
registerSettingsByokRoutes(apiRouter, {
|
|
2893
|
+
parseJsonRequest,
|
|
2894
|
+
readByokKeys,
|
|
2895
|
+
writeByokKeys,
|
|
2896
|
+
maskSecret,
|
|
2897
|
+
listAgents,
|
|
2898
|
+
listOpenClawProviderModels,
|
|
2899
|
+
sendJson,
|
|
2900
|
+
safeErrorMessage,
|
|
2901
|
+
});
|
|
2902
|
+
registerBillingRoutes(apiRouter, {
|
|
2903
|
+
client,
|
|
2904
|
+
parseJsonRequest,
|
|
2905
|
+
pickString,
|
|
2906
|
+
sendJson,
|
|
2907
|
+
safeErrorMessage,
|
|
2908
|
+
});
|
|
2909
|
+
registerDelegationRoutes(apiRouter, {
|
|
2910
|
+
client,
|
|
2911
|
+
parseJsonRequest,
|
|
2912
|
+
pickString,
|
|
2913
|
+
sendJson,
|
|
2914
|
+
safeErrorMessage,
|
|
2915
|
+
});
|
|
2916
|
+
registerEntitiesRoutes(apiRouter, {
|
|
2917
|
+
client,
|
|
2918
|
+
parseJsonRequest,
|
|
2919
|
+
pickString,
|
|
2920
|
+
normalizeEntityMutationPayload,
|
|
2921
|
+
resolveAutoAssignments: (input) => resolveAutoAssignments({
|
|
2922
|
+
client,
|
|
2923
|
+
...input,
|
|
2924
|
+
}),
|
|
2925
|
+
setLocalInitiativeStatusOverride,
|
|
2926
|
+
clearLocalInitiativeStatusOverride,
|
|
2927
|
+
isUnauthorizedOrgxError,
|
|
2928
|
+
applyLocalInitiativeOverrides,
|
|
2929
|
+
formatInitiatives,
|
|
2930
|
+
getSnapshot,
|
|
2931
|
+
scheduleWorkstreamReassignment: async (input) => {
|
|
2932
|
+
const initiativeId = input.initiativeId.trim();
|
|
2933
|
+
const workstreamId = input.workstreamId.trim();
|
|
2934
|
+
if (!initiativeId || !workstreamId)
|
|
2935
|
+
return null;
|
|
2936
|
+
const normalizedStatus = (input.status ?? "").trim().toLowerCase();
|
|
2937
|
+
const shouldRedispatch = normalizedStatus === "active" ||
|
|
2938
|
+
normalizedStatus === "ready" ||
|
|
2939
|
+
normalizedStatus === "queued" ||
|
|
2940
|
+
normalizedStatus === "running" ||
|
|
2941
|
+
normalizedStatus === "in_progress" ||
|
|
2942
|
+
normalizedStatus === "pending";
|
|
2943
|
+
if (!shouldRedispatch)
|
|
2944
|
+
return null;
|
|
2945
|
+
if (!isDispatchableWorkstreamStatus(normalizedStatus))
|
|
2946
|
+
return null;
|
|
2947
|
+
const liveRun = runningAutoContinueForWorkstream(initiativeId, workstreamId);
|
|
2948
|
+
return await scheduleAutoFixForWorkstream({
|
|
2949
|
+
initiativeId,
|
|
2950
|
+
workstreamId,
|
|
2951
|
+
runId: liveRun?.activeRunId ?? null,
|
|
2952
|
+
event: input.event,
|
|
2953
|
+
requestedByAgentId: "system",
|
|
2954
|
+
requestedByAgentName: "System",
|
|
2955
|
+
graceMs: 5_000,
|
|
2956
|
+
});
|
|
2957
|
+
},
|
|
2958
|
+
sendJson,
|
|
2959
|
+
safeErrorMessage,
|
|
2960
|
+
});
|
|
2961
|
+
const readCachedDecisionRows = () => {
|
|
2962
|
+
const snapshots = [
|
|
2963
|
+
readSnapshotResponseCache("live-snapshot"),
|
|
2964
|
+
readSnapshotResponseCache("dashboard-bundle"),
|
|
2965
|
+
readSnapshotResponseCache("live-snapshot-v2"),
|
|
2966
|
+
];
|
|
2967
|
+
const rows = [];
|
|
2968
|
+
for (const snapshot of snapshots) {
|
|
2969
|
+
if (!snapshot || typeof snapshot !== "object")
|
|
2970
|
+
continue;
|
|
2971
|
+
const decisionsRaw = snapshot.decisions;
|
|
2972
|
+
if (!Array.isArray(decisionsRaw))
|
|
2973
|
+
continue;
|
|
2974
|
+
for (const entry of decisionsRaw) {
|
|
2975
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
2976
|
+
continue;
|
|
2977
|
+
rows.push(entry);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
return rows;
|
|
2981
|
+
};
|
|
2982
|
+
const emitDecisionResolvedActivity = async (input) => {
|
|
2983
|
+
const ids = Array.from(new Set(input.ids
|
|
2984
|
+
.filter((id) => typeof id === "string")
|
|
2985
|
+
.map((id) => id.trim())
|
|
2986
|
+
.filter(Boolean)));
|
|
2987
|
+
if (ids.length === 0)
|
|
2988
|
+
return;
|
|
2989
|
+
const decisionById = new Map();
|
|
2990
|
+
for (const row of readCachedDecisionRows()) {
|
|
2991
|
+
const rowId = pickString(row, ["id"])?.trim() ?? "";
|
|
2992
|
+
if (!rowId || decisionById.has(rowId))
|
|
2993
|
+
continue;
|
|
2994
|
+
decisionById.set(rowId, row);
|
|
2995
|
+
}
|
|
2996
|
+
for (const decisionId of ids) {
|
|
2997
|
+
const row = decisionById.get(decisionId) ?? null;
|
|
2998
|
+
const decisionTitle = pickString(row ?? {}, ["title", "summary"]) ??
|
|
2999
|
+
`Decision ${decisionId.slice(0, 8)}`;
|
|
3000
|
+
const scopedInitiativeId = input.initiativeId ??
|
|
3001
|
+
pickString(row ?? {}, ["initiative_id", "initiativeId"]) ??
|
|
3002
|
+
null;
|
|
3003
|
+
const scopedRunId = input.sliceRunId ??
|
|
3004
|
+
pickString(row ?? {}, [
|
|
3005
|
+
"run_id",
|
|
3006
|
+
"runId",
|
|
3007
|
+
"source_run_id",
|
|
3008
|
+
"sourceRunId",
|
|
3009
|
+
"correlation_id",
|
|
3010
|
+
"correlationId",
|
|
3011
|
+
]) ??
|
|
3012
|
+
null;
|
|
3013
|
+
await emitActivitySafe({
|
|
3014
|
+
initiativeId: scopedInitiativeId,
|
|
3015
|
+
runId: scopedRunId ?? undefined,
|
|
3016
|
+
correlationId: scopedRunId ?? undefined,
|
|
3017
|
+
phase: "review",
|
|
3018
|
+
level: "info",
|
|
3019
|
+
message: `Decision ${input.action === "approve" ? "approved" : "rejected"}: ${decisionTitle}`,
|
|
3020
|
+
progressPct: 100,
|
|
3021
|
+
nextStep: input.action === "approve"
|
|
3022
|
+
? "Execution can continue with the approved direction."
|
|
3023
|
+
: "Review the rejected decision and provide revised guidance to continue safely.",
|
|
3024
|
+
metadata: {
|
|
3025
|
+
event: "decision_resolved",
|
|
3026
|
+
action: input.action,
|
|
3027
|
+
resolver: "human",
|
|
3028
|
+
decision_id: decisionId,
|
|
3029
|
+
decision_ids: ids,
|
|
3030
|
+
decision_title: decisionTitle,
|
|
3031
|
+
initiative_id: scopedInitiativeId,
|
|
3032
|
+
workstream_id: pickString(row ?? {}, ["workstream_id", "workstreamId"]) ?? null,
|
|
3033
|
+
source_run_id: scopedRunId,
|
|
3034
|
+
option_id: input.optionId ?? null,
|
|
3035
|
+
note: input.note ?? null,
|
|
3036
|
+
slice_run_id: input.sliceRunId ?? null,
|
|
3037
|
+
},
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
3041
|
+
registerDecisionActionsRoutes(apiRouter, {
|
|
3042
|
+
parseJsonRequest,
|
|
3043
|
+
bulkDecideDecisions: (ids, action, input) => client.bulkDecideDecisions(ids, action, input),
|
|
3044
|
+
emitDecisionResolvedActivity: async (ids, action, input) => {
|
|
3045
|
+
await emitDecisionResolvedActivity({
|
|
3046
|
+
ids,
|
|
3047
|
+
action,
|
|
3048
|
+
note: input?.note ?? null,
|
|
3049
|
+
optionId: input?.optionId ?? null,
|
|
3050
|
+
});
|
|
3051
|
+
},
|
|
3052
|
+
sendJson,
|
|
3053
|
+
safeErrorMessage,
|
|
3054
|
+
});
|
|
3055
|
+
registerRunControlRoutes(apiRouter, {
|
|
3056
|
+
parseJsonRequest,
|
|
3057
|
+
pickString,
|
|
3058
|
+
listRunCheckpoints: (runId) => client.listRunCheckpoints(runId),
|
|
3059
|
+
createRunCheckpoint: (runId, input) => client.createRunCheckpoint(runId, input),
|
|
3060
|
+
restoreRunCheckpoint: (runId, input) => client.restoreRunCheckpoint(runId, input),
|
|
3061
|
+
runAction: (runId, action, input) => client.runAction(runId, action, input),
|
|
3062
|
+
markRunCompleted: async (runId, input) => {
|
|
3063
|
+
const normalizedRunId = runId.trim();
|
|
3064
|
+
if (!normalizedRunId) {
|
|
3065
|
+
throw new Error("runId is required");
|
|
3066
|
+
}
|
|
3067
|
+
const nowIso = new Date().toISOString();
|
|
3068
|
+
const reason = input.reason?.trim() || null;
|
|
3069
|
+
const message = reason
|
|
3070
|
+
? `Marked completed from dashboard (${reason}).`
|
|
3071
|
+
: "Marked completed from dashboard.";
|
|
3072
|
+
const existingRun = getAgentRun(normalizedRunId);
|
|
3073
|
+
// ── Try OrgX-side completion first (for remote sessions) ────────────
|
|
3074
|
+
let remoteOk = false;
|
|
3075
|
+
try {
|
|
3076
|
+
await client.updateEntity("run", normalizedRunId, {
|
|
3077
|
+
status: "completed",
|
|
3078
|
+
phase: "completed",
|
|
3079
|
+
});
|
|
3080
|
+
remoteOk = true;
|
|
3081
|
+
}
|
|
3082
|
+
catch {
|
|
3083
|
+
// OrgX may not support updating runs directly — fall through to local
|
|
3084
|
+
}
|
|
3085
|
+
// ── Local operations (defensive — partial failures don't block) ─────
|
|
3086
|
+
let runtimeRecord = null;
|
|
3087
|
+
try {
|
|
3088
|
+
runtimeRecord = upsertRuntimeInstanceFromHook({
|
|
3089
|
+
source_client: "api",
|
|
3090
|
+
event: "session_stop",
|
|
3091
|
+
run_id: normalizedRunId,
|
|
3092
|
+
correlation_id: normalizedRunId,
|
|
3093
|
+
initiative_id: existingRun?.initiativeId ?? null,
|
|
3094
|
+
workstream_id: existingRun?.workstreamId ?? null,
|
|
3095
|
+
task_id: existingRun?.taskId ?? null,
|
|
3096
|
+
agent_id: existingRun?.agentId ?? null,
|
|
3097
|
+
phase: "completed",
|
|
3098
|
+
message,
|
|
3099
|
+
timestamp: nowIso,
|
|
3100
|
+
metadata: {
|
|
3101
|
+
source: "dashboard_manual_complete",
|
|
3102
|
+
reason,
|
|
3103
|
+
},
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
catch (err) {
|
|
3107
|
+
console.error(`[markRunCompleted] upsertRuntime failed for ${normalizedRunId}:`, err);
|
|
3108
|
+
}
|
|
3109
|
+
try {
|
|
3110
|
+
markAgentRunStopped(normalizedRunId);
|
|
3111
|
+
}
|
|
3112
|
+
catch (err) {
|
|
3113
|
+
console.error(`[markRunCompleted] markAgentRunStopped failed for ${normalizedRunId}:`, err);
|
|
3114
|
+
}
|
|
3115
|
+
if (runtimeRecord) {
|
|
3116
|
+
broadcastRuntimeSse("runtime.updated", runtimeRecord);
|
|
3117
|
+
}
|
|
3118
|
+
clearSnapshotResponseCache();
|
|
3119
|
+
try {
|
|
3120
|
+
appendActivityItems([
|
|
3121
|
+
{
|
|
3122
|
+
id: randomUUID(),
|
|
3123
|
+
type: "run_completed",
|
|
3124
|
+
title: message,
|
|
3125
|
+
description: reason,
|
|
3126
|
+
agentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
|
|
3127
|
+
agentName: runtimeRecord?.agentName ?? null,
|
|
3128
|
+
requesterAgentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
|
|
3129
|
+
requesterAgentName: runtimeRecord?.agentName ?? null,
|
|
3130
|
+
executorAgentId: runtimeRecord?.agentId ?? existingRun?.agentId ?? null,
|
|
3131
|
+
executorAgentName: runtimeRecord?.agentName ?? null,
|
|
3132
|
+
runId: normalizedRunId,
|
|
3133
|
+
initiativeId: runtimeRecord?.initiativeId ?? existingRun?.initiativeId ?? null,
|
|
3134
|
+
timestamp: nowIso,
|
|
3135
|
+
phase: "completed",
|
|
3136
|
+
state: "done",
|
|
3137
|
+
summary: message,
|
|
3138
|
+
metadata: {
|
|
3139
|
+
source: "dashboard_manual_complete",
|
|
3140
|
+
reason,
|
|
3141
|
+
remoteOk,
|
|
3142
|
+
event: "dashboard_run_mark_completed",
|
|
3143
|
+
},
|
|
3144
|
+
},
|
|
3145
|
+
]);
|
|
3146
|
+
}
|
|
3147
|
+
catch (err) {
|
|
3148
|
+
console.error(`[markRunCompleted] appendActivity failed for ${normalizedRunId}:`, err);
|
|
3149
|
+
}
|
|
3150
|
+
return {
|
|
3151
|
+
ok: true,
|
|
3152
|
+
data: {
|
|
3153
|
+
runId: normalizedRunId,
|
|
3154
|
+
action: "complete",
|
|
3155
|
+
status: "completed",
|
|
3156
|
+
remoteOk,
|
|
3157
|
+
},
|
|
3158
|
+
};
|
|
3159
|
+
},
|
|
3160
|
+
sendJson,
|
|
3161
|
+
safeErrorMessage,
|
|
3162
|
+
});
|
|
3163
|
+
registerWorkArtifactsRoutes(apiRouter, {
|
|
3164
|
+
rawRequest: (requestMethod, requestPath, body) => client.rawRequest(requestMethod, requestPath, body),
|
|
3165
|
+
buildLocalArtifactDetailFallback,
|
|
3166
|
+
sendJson,
|
|
3167
|
+
safeErrorMessage,
|
|
3168
|
+
});
|
|
3169
|
+
registerEntityDynamicRoutes(apiRouter, {
|
|
3170
|
+
parseJsonRequest,
|
|
3171
|
+
pickString,
|
|
3172
|
+
rawRequest: (requestMethod, requestPath, body) => client.rawRequest(requestMethod, requestPath, body),
|
|
3173
|
+
listEntityComments,
|
|
3174
|
+
mergeEntityComments: (remote, local) => mergeEntityComments(remote, local),
|
|
3175
|
+
appendEntityComment,
|
|
3176
|
+
updateEntity: (type, id, updates) => client.updateEntity(type, id, updates),
|
|
3177
|
+
setLocalInitiativeStatusOverride,
|
|
3178
|
+
clearLocalInitiativeStatusOverride,
|
|
3179
|
+
isUnauthorizedOrgxError,
|
|
3180
|
+
sendJson,
|
|
3181
|
+
safeErrorMessage,
|
|
3182
|
+
});
|
|
3183
|
+
registerChatRoutes(apiRouter, {
|
|
3184
|
+
parseJsonRequest,
|
|
3185
|
+
pickString,
|
|
3186
|
+
parsePositiveInt,
|
|
3187
|
+
emitActivitySafe,
|
|
3188
|
+
sendJson,
|
|
3189
|
+
safeErrorMessage,
|
|
3190
|
+
});
|
|
3191
|
+
registerRealtimeOrchestratorRoutes(apiRouter, {
|
|
3192
|
+
parseJsonRequest,
|
|
3193
|
+
rawRequest: (requestMethod, requestPath, body) => client.rawRequest(requestMethod, requestPath, body),
|
|
3194
|
+
sendJson,
|
|
3195
|
+
safeErrorMessage,
|
|
3196
|
+
});
|
|
3197
|
+
registerMissionControlActionsRoutes(apiRouter, {
|
|
3198
|
+
parseJsonRequest,
|
|
3199
|
+
pickString,
|
|
3200
|
+
pickNumber,
|
|
3201
|
+
parseBooleanQuery,
|
|
3202
|
+
pickStringArray,
|
|
3203
|
+
dedupeStrings,
|
|
3204
|
+
resolveAgentDisplayName,
|
|
3205
|
+
buildNextUpQueue,
|
|
3206
|
+
startAutoContinueRun,
|
|
3207
|
+
autoContinueRuns,
|
|
3208
|
+
autoContinueSliceRuns,
|
|
3209
|
+
dispatchFallbackWorkstreamTurn,
|
|
3210
|
+
tickAutoContinueRun,
|
|
3211
|
+
stopAutoContinueRun,
|
|
3212
|
+
updateInitiativeAutoContinueState,
|
|
3213
|
+
tickAllAutoContinue,
|
|
3214
|
+
scheduleAutoFixForWorkstream,
|
|
3215
|
+
upsertNextUpQueuePin,
|
|
3216
|
+
removeNextUpQueuePin,
|
|
3217
|
+
suppressNextUpQueueItem,
|
|
3218
|
+
setNextUpQueuePinOrder,
|
|
3219
|
+
clearNextUpQueueCache,
|
|
3220
|
+
resolveAutoAssignments,
|
|
3221
|
+
buildMissionControlGraph: (initiativeId) => buildMissionControlGraph(client, initiativeId),
|
|
3222
|
+
applyLocalInitiativeOverrideToGraph: (graph) => applyLocalInitiativeOverrideToGraph(graph),
|
|
3223
|
+
client,
|
|
3224
|
+
rawRequest: (requestMethod, requestPath, body) => client.rawRequest(requestMethod, requestPath, body),
|
|
3225
|
+
sendJson,
|
|
3226
|
+
safeErrorMessage,
|
|
3227
|
+
});
|
|
3228
|
+
registerAgentControlRoutes(apiRouter, {
|
|
3229
|
+
parseJsonRequest,
|
|
3230
|
+
pickString,
|
|
3231
|
+
parseBooleanQuery,
|
|
3232
|
+
randomUUID,
|
|
3233
|
+
normalizeOpenClawProvider,
|
|
3234
|
+
resolveAutoOpenClawProvider,
|
|
3235
|
+
modelImpliesByok,
|
|
3236
|
+
listAgents,
|
|
3237
|
+
fetchBillingStatusSafe,
|
|
3238
|
+
client,
|
|
3239
|
+
resolveDispatchExecutionPolicy,
|
|
3240
|
+
fetchKickoffContextSafe,
|
|
3241
|
+
renderKickoffMessage,
|
|
3242
|
+
posthogCapture: (input) => posthogCapture(input),
|
|
3243
|
+
telemetryDistinctId,
|
|
3244
|
+
pluginVersion: (config.pluginVersion ?? "").trim() || null,
|
|
3245
|
+
enforceSpawnGuardForDispatch,
|
|
3246
|
+
extractSpawnGuardModelTier,
|
|
3247
|
+
buildPolicyEnforcedMessage,
|
|
3248
|
+
syncParentRollupsForTask,
|
|
3249
|
+
emitActivitySafe,
|
|
3250
|
+
configureOpenClawProviderRouting,
|
|
3251
|
+
upsertAgentContext,
|
|
3252
|
+
upsertRunContext,
|
|
3253
|
+
spawnAgentTurn,
|
|
3254
|
+
upsertAgentRun,
|
|
3255
|
+
getAgentRun,
|
|
3256
|
+
stopProcess,
|
|
3257
|
+
markAgentRunStopped,
|
|
3258
|
+
writeRuntimeEvent,
|
|
3259
|
+
sendJson,
|
|
3260
|
+
safeErrorMessage,
|
|
3261
|
+
});
|
|
3262
|
+
registerLiveMiscRoutes(apiRouter, {
|
|
3263
|
+
parseJsonRequest,
|
|
3264
|
+
pickString,
|
|
3265
|
+
summarizeActivityHeadline,
|
|
3266
|
+
getLiveAgents: ({ initiative, projectId, includeIdle }) => client.getLiveAgents({ initiative, projectId, includeIdle }),
|
|
3267
|
+
getLiveInitiatives: ({ id, projectId, limit, offset }) => client.getLiveInitiatives({ id, projectId, limit, offset }),
|
|
3268
|
+
getLiveDecisions: ({ status, projectId, limit }) => client.getLiveDecisions({ status, projectId, limit }),
|
|
3269
|
+
getHandoffs: () => client.getHandoffs(),
|
|
3270
|
+
listInitiativeIdsForProject: ({ projectId }) => listInitiativeIdsForProject({ projectId }),
|
|
3271
|
+
loadLocalOpenClawSnapshot,
|
|
3272
|
+
toLocalLiveAgents,
|
|
3273
|
+
toLocalLiveInitiatives,
|
|
3274
|
+
localInitiativeStatusOverrides,
|
|
3275
|
+
mapDecisionEntity,
|
|
3276
|
+
sendJson,
|
|
3277
|
+
safeErrorMessage,
|
|
3278
|
+
});
|
|
3279
|
+
registerLiveTerminalRoutes(apiRouter, {
|
|
3280
|
+
parseJsonRequest,
|
|
3281
|
+
sendJson,
|
|
3282
|
+
safeErrorMessage,
|
|
3283
|
+
});
|
|
3284
|
+
registerLiveLegacyRoutes(apiRouter, {
|
|
3285
|
+
getLiveSessions: ({ initiative, projectId, limit }) => client.getLiveSessions({ initiative, projectId, limit }),
|
|
3286
|
+
getLiveActivity: ({ run, since, projectId, limit }) => client.getLiveActivity({ run, since, projectId, limit }),
|
|
3287
|
+
listInitiativeIdsForProject: ({ projectId }) => listInitiativeIdsForProject({ projectId }),
|
|
3288
|
+
listRuntimeInstances,
|
|
3289
|
+
injectRuntimeInstancesAsSessions,
|
|
3290
|
+
enrichSessionsWithRuntime,
|
|
3291
|
+
loadLocalOpenClawSnapshot,
|
|
3292
|
+
toLocalSessionTree,
|
|
3293
|
+
readAgentContexts,
|
|
3294
|
+
applyAgentContextsToSessionTree,
|
|
3295
|
+
listActivityPage,
|
|
3296
|
+
applyAgentContextsToActivity,
|
|
3297
|
+
appendActivityItems,
|
|
3298
|
+
activityWarmByKey,
|
|
3299
|
+
activityWarmThrottleMs: ACTIVITY_WARM_THROTTLE_MS,
|
|
3300
|
+
outboxReadAllItems: () => outboxAdapter.readAllItems(),
|
|
3301
|
+
toLocalLiveActivity,
|
|
3302
|
+
loadLocalTurnDetail,
|
|
3303
|
+
sendJson,
|
|
3304
|
+
safeErrorMessage,
|
|
3305
|
+
sendHtml,
|
|
3306
|
+
resolveFilesystemOpenPath,
|
|
3307
|
+
escapeHtml,
|
|
3308
|
+
statSync,
|
|
3309
|
+
readdirSync,
|
|
3310
|
+
existsSync,
|
|
3311
|
+
resolvePath: resolve,
|
|
3312
|
+
readFilePreview,
|
|
3313
|
+
filePreviewMaxBytes: FILE_PREVIEW_MAX_BYTES,
|
|
3314
|
+
filePreviewMaxDirEntries: FILE_PREVIEW_MAX_DIR_ENTRIES,
|
|
3315
|
+
securityHeaders: SECURITY_HEADERS,
|
|
3316
|
+
corsHeaders: CORS_HEADERS,
|
|
3317
|
+
config: {
|
|
3318
|
+
baseUrl: config.baseUrl,
|
|
3319
|
+
apiKey: config.apiKey,
|
|
3320
|
+
userId: config.userId,
|
|
3321
|
+
},
|
|
3322
|
+
isUserScopedApiKey,
|
|
3323
|
+
streamIdleTimeoutMs: STREAM_IDLE_TIMEOUT_MS,
|
|
3324
|
+
});
|
|
3325
|
+
registerLiveSnapshotRoutes(apiRouter, {
|
|
3326
|
+
parsePositiveInt,
|
|
3327
|
+
readSnapshotResponseCache,
|
|
3328
|
+
writeSnapshotResponseCache,
|
|
3329
|
+
safeErrorMessage,
|
|
3330
|
+
readAgentContexts,
|
|
3331
|
+
getScopedAgentIds,
|
|
3332
|
+
readDiagnosticsOutboxStatus: async () => {
|
|
3333
|
+
if (!diagnostics?.getHealth)
|
|
3334
|
+
return null;
|
|
3335
|
+
const health = await diagnostics.getHealth({ probeRemote: false });
|
|
3336
|
+
if (!health || typeof health !== "object")
|
|
3337
|
+
return null;
|
|
3338
|
+
const maybeOutbox = health.outbox;
|
|
3339
|
+
if (!maybeOutbox || typeof maybeOutbox !== "object")
|
|
3340
|
+
return null;
|
|
3341
|
+
return maybeOutbox;
|
|
3342
|
+
},
|
|
3343
|
+
readOutboxSummary: () => outboxAdapter.readSummary(),
|
|
3344
|
+
readOutboxItems: () => outboxAdapter.readAllItems(),
|
|
3345
|
+
loadLocalOpenClawSnapshot,
|
|
3346
|
+
toLocalSessionTree,
|
|
3347
|
+
toLocalLiveActivity,
|
|
3348
|
+
toLocalLiveAgents,
|
|
3349
|
+
getLiveSessions: ({ initiative, projectId, limit }) => client.getLiveSessions({ initiative, projectId, limit }),
|
|
3350
|
+
getLiveActivity: ({ run, since, projectId, limit }) => client.getLiveActivity({ run, since, projectId, limit }),
|
|
3351
|
+
getHandoffs: () => client.getHandoffs(),
|
|
3352
|
+
getLiveDecisions: ({ status, projectId, limit }) => client.getLiveDecisions({ status, projectId, limit }),
|
|
3353
|
+
getLiveAgents: ({ initiative, projectId, includeIdle }) => client.getLiveAgents({ initiative, projectId, includeIdle }),
|
|
3354
|
+
listInitiativeIdsForProject: ({ projectId }) => listInitiativeIdsForProject({ projectId }),
|
|
3355
|
+
mapDecisionEntity: (entry) => mapDecisionEntity(entry),
|
|
3356
|
+
applyAgentContextsToSessionTree,
|
|
3357
|
+
applyAgentContextsToActivity,
|
|
3358
|
+
mergeSessionTrees,
|
|
3359
|
+
mergeActivities,
|
|
3360
|
+
listRuntimeInstances,
|
|
3361
|
+
injectRuntimeInstancesAsSessions,
|
|
3362
|
+
enrichSessionsWithRuntime,
|
|
3363
|
+
enrichActivityWithRuntime,
|
|
3364
|
+
snapshotActivityFingerprint,
|
|
3365
|
+
appendActivityItems,
|
|
3366
|
+
snapshotActivityPersistMinIntervalMs: SNAPSHOT_ACTIVITY_PERSIST_MIN_INTERVAL_MS,
|
|
3367
|
+
readSnapshotPersistState: () => ({
|
|
3368
|
+
lastFingerprint: lastSnapshotActivityFingerprint,
|
|
3369
|
+
lastPersistAt: lastSnapshotActivityPersistAt,
|
|
3370
|
+
}),
|
|
3371
|
+
writeSnapshotPersistState: (state) => {
|
|
3372
|
+
lastSnapshotActivityFingerprint = state.lastFingerprint;
|
|
3373
|
+
lastSnapshotActivityPersistAt = state.lastPersistAt;
|
|
3374
|
+
},
|
|
3375
|
+
parseJsonRequest,
|
|
3376
|
+
buildNextUpQueue: ({ initiativeId, projectId }) => buildNextUpQueue({ initiativeId, projectId }),
|
|
3377
|
+
bulkDecideDecisions: (ids, action, input) => client.bulkDecideDecisions(ids, action, input),
|
|
3378
|
+
emitDecisionResolvedActivity: async (input) => {
|
|
3379
|
+
await emitDecisionResolvedActivity(input);
|
|
3380
|
+
},
|
|
3381
|
+
runAction: (runId, action, input) => client.runAction(runId, action, input),
|
|
3382
|
+
listChatThreads: ({ commandCenterId, initiativeId, limit, offset }) => listChatThreads({ commandCenterId, initiativeId, limit, offset }),
|
|
3383
|
+
sendJson,
|
|
3384
|
+
});
|
|
3385
|
+
registerRuntimeHookRoutes(apiRouter, {
|
|
3386
|
+
parseJsonRequest,
|
|
3387
|
+
pickString,
|
|
3388
|
+
pickNumber,
|
|
3389
|
+
pickHeaderString,
|
|
3390
|
+
resolveRuntimeHookToken,
|
|
3391
|
+
maskSecret,
|
|
3392
|
+
parseJsonSafe,
|
|
3393
|
+
sendJson,
|
|
3394
|
+
safeErrorMessage,
|
|
3395
|
+
randomUUID,
|
|
3396
|
+
listRuntimeInstances,
|
|
3397
|
+
writeRuntimeSseEvent,
|
|
3398
|
+
runtimeStreamSubscribers,
|
|
3399
|
+
ensureRuntimeStreamTimers,
|
|
3400
|
+
stopRuntimeStreamTimers,
|
|
3401
|
+
upsertRuntimeInstanceFromHook,
|
|
3402
|
+
broadcastRuntimeSse,
|
|
3403
|
+
clearSnapshotResponseCache,
|
|
3404
|
+
normalizeHookPhase,
|
|
3405
|
+
normalizeRuntimeSourceForReporting: (value) => normalizeRuntimeSourceForReporting(value),
|
|
3406
|
+
emitActivity: (input) => client.emitActivity(input),
|
|
3407
|
+
securityHeaders: SECURITY_HEADERS,
|
|
3408
|
+
corsHeaders: CORS_HEADERS,
|
|
3409
|
+
});
|
|
3410
|
+
registerHealthRoutes(apiRouter, {
|
|
3411
|
+
diagnostics,
|
|
3412
|
+
readOutboxSummary: () => outboxAdapter.readSummary(),
|
|
3413
|
+
parseBooleanQuery,
|
|
3414
|
+
baseUrl: config.baseUrl,
|
|
3415
|
+
hasApiKey: Boolean(config.apiKey),
|
|
3416
|
+
sendJson,
|
|
3417
|
+
safeErrorMessage,
|
|
3418
|
+
});
|
|
3419
|
+
registerLiveTerminalRoutes(apiRouter, {
|
|
3420
|
+
parseJsonRequest,
|
|
3421
|
+
sendJson,
|
|
3422
|
+
safeErrorMessage,
|
|
3423
|
+
});
|
|
3424
|
+
registerLiveTriageRoutes(apiRouter, {
|
|
3425
|
+
parseJsonRequest,
|
|
3426
|
+
sendJson,
|
|
3427
|
+
getDecisions: (workspaceId) => {
|
|
3428
|
+
// Return cached decisions from latest snapshot, or empty array
|
|
3429
|
+
const normalizedWorkspaceId = (workspaceId ?? "").trim();
|
|
3430
|
+
const scopedKeys = normalizedWorkspaceId
|
|
3431
|
+
? [
|
|
3432
|
+
`live-snapshot:${normalizedWorkspaceId}`,
|
|
3433
|
+
`live-snapshot-v2:${normalizedWorkspaceId}`,
|
|
3434
|
+
`dashboard-bundle:${normalizedWorkspaceId}`,
|
|
3435
|
+
]
|
|
3436
|
+
: [];
|
|
3437
|
+
const fallbackKeys = ["live-snapshot", "dashboard-bundle", "live-snapshot-v2"];
|
|
3438
|
+
const keys = [...scopedKeys, ...fallbackKeys];
|
|
3439
|
+
try {
|
|
3440
|
+
for (const key of keys) {
|
|
3441
|
+
const cached = readSnapshotResponseCache(key);
|
|
3442
|
+
if (!cached || typeof cached !== "object" || !("decisions" in cached))
|
|
3443
|
+
continue;
|
|
3444
|
+
const decisions = cached.decisions;
|
|
3445
|
+
if (Array.isArray(decisions))
|
|
3446
|
+
return decisions;
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
catch {
|
|
3450
|
+
// best effort
|
|
3451
|
+
}
|
|
3452
|
+
return [];
|
|
3453
|
+
},
|
|
3454
|
+
getBlockerEvents: () => {
|
|
3455
|
+
// Extract blocker events from recent activity
|
|
3456
|
+
// In future, this will read from a dedicated blocker store
|
|
3457
|
+
return [];
|
|
3458
|
+
},
|
|
3459
|
+
resolveDecisionAction: async (decisionId, action, note, optionId) => {
|
|
3460
|
+
try {
|
|
3461
|
+
await client.bulkDecideDecisions([decisionId], action, { note: note ?? undefined, optionId: optionId ?? undefined });
|
|
3462
|
+
return { ok: true };
|
|
3463
|
+
}
|
|
3464
|
+
catch (err) {
|
|
3465
|
+
return {
|
|
3466
|
+
ok: false,
|
|
3467
|
+
error: err instanceof Error ? err.message : "Decision action failed",
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
},
|
|
3471
|
+
emitDecisionResolvedActivity: async (input) => {
|
|
3472
|
+
await emitDecisionResolvedActivity(input);
|
|
3473
|
+
},
|
|
3474
|
+
});
|
|
3475
|
+
return async function handler(req, res) {
|
|
3476
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
3477
|
+
const rawUrl = req.url ?? "/";
|
|
3478
|
+
const [path, queryString] = rawUrl.split("?", 2);
|
|
3479
|
+
const url = path;
|
|
3480
|
+
const searchParams = new URLSearchParams(queryString ?? "");
|
|
3481
|
+
// Legacy deep-link compatibility:
|
|
3482
|
+
// Older launch paths still point at /workspace-hub. Route those into
|
|
3483
|
+
// the current dashboard entrypoint while preserving query params.
|
|
3484
|
+
if (url === "/workspace-hub" || url === "/workspace-hub/") {
|
|
3485
|
+
const suffix = queryString && queryString.trim().length > 0 ? `?${queryString}` : "";
|
|
3486
|
+
res.writeHead(302, {
|
|
3487
|
+
Location: `/orgx/live${suffix}`,
|
|
3488
|
+
...SECURITY_HEADERS,
|
|
3489
|
+
...CORS_HEADERS,
|
|
3490
|
+
});
|
|
3491
|
+
res.end();
|
|
3492
|
+
return true;
|
|
3493
|
+
}
|
|
3494
|
+
// Only handle /orgx paths — return false for everything else
|
|
3495
|
+
if (!url.startsWith("/orgx")) {
|
|
3496
|
+
return false;
|
|
3497
|
+
}
|
|
3498
|
+
// Handle CORS preflight
|
|
3499
|
+
if (method === "OPTIONS") {
|
|
3500
|
+
if (url.startsWith("/orgx/api/") && !isTrustedRequestSource(req.headers)) {
|
|
3501
|
+
sendJson(res, 403, {
|
|
3502
|
+
error: "Cross-origin browser requests are blocked for /orgx/api endpoints.",
|
|
3503
|
+
});
|
|
3504
|
+
return true;
|
|
3505
|
+
}
|
|
3506
|
+
res.writeHead(204, {
|
|
3507
|
+
...SECURITY_HEADERS,
|
|
3508
|
+
...CORS_HEADERS,
|
|
3509
|
+
});
|
|
3510
|
+
res.end();
|
|
3511
|
+
return true;
|
|
3512
|
+
}
|
|
3513
|
+
// ── API endpoints ──────────────────────────────────────────────────────
|
|
3514
|
+
if (url.startsWith("/orgx/api/")) {
|
|
3515
|
+
if (!isTrustedRequestSource(req.headers)) {
|
|
3516
|
+
sendJson(res, 403, {
|
|
3517
|
+
error: "Cross-origin browser requests are blocked for /orgx/api endpoints.",
|
|
3518
|
+
});
|
|
3519
|
+
return true;
|
|
3520
|
+
}
|
|
3521
|
+
const route = url.replace("/orgx/api/", "").replace(/\/+$/, "");
|
|
3522
|
+
const routed = apiRouter.match(method, route);
|
|
3523
|
+
if (routed) {
|
|
3524
|
+
await routed.handler({
|
|
3525
|
+
req,
|
|
3526
|
+
res,
|
|
3527
|
+
path: route,
|
|
3528
|
+
query: searchParams,
|
|
3529
|
+
body: undefined,
|
|
3530
|
+
state: {},
|
|
3531
|
+
});
|
|
3532
|
+
return true;
|
|
3533
|
+
}
|
|
3534
|
+
sendJson(res, 404, { error: "Unknown API endpoint" });
|
|
3535
|
+
return true;
|
|
3536
|
+
}
|
|
3537
|
+
// ── Dashboard SPA + static assets ──────────────────────────────────────
|
|
3538
|
+
if (!dashboardEnabled) {
|
|
3539
|
+
res.writeHead(404, {
|
|
3540
|
+
"Content-Type": "text/plain",
|
|
3541
|
+
...SECURITY_HEADERS,
|
|
3542
|
+
...CORS_HEADERS,
|
|
3543
|
+
});
|
|
3544
|
+
res.end("Dashboard is disabled");
|
|
3545
|
+
return true;
|
|
3546
|
+
}
|
|
3547
|
+
// Requests under /orgx/live
|
|
3548
|
+
if (url === "/orgx/live" || url.startsWith("/orgx/live/")) {
|
|
3549
|
+
const subPath = url.replace(/^\/orgx\/live\/?/, "");
|
|
3550
|
+
// Never expose source maps in shipped plugin dashboards.
|
|
3551
|
+
if (/\.map$/i.test(subPath)) {
|
|
3552
|
+
send404(res);
|
|
3553
|
+
return true;
|
|
3554
|
+
}
|
|
3555
|
+
// Static assets: /orgx/live/assets/* → dashboard/dist/assets/*
|
|
3556
|
+
// Hashed filenames get long-lived cache
|
|
3557
|
+
if (subPath.startsWith("assets/")) {
|
|
3558
|
+
const assetPath = resolveSafeDistPath(subPath);
|
|
3559
|
+
let isWithinAssetsDir = false;
|
|
3560
|
+
if (assetPath) {
|
|
3561
|
+
isWithinAssetsDir =
|
|
3562
|
+
assetPath === RESOLVED_DIST_ASSETS_DIR ||
|
|
3563
|
+
assetPath.startsWith(`${RESOLVED_DIST_ASSETS_DIR}${sep}`);
|
|
3564
|
+
}
|
|
3565
|
+
if (assetPath && isWithinAssetsDir && existsSync(assetPath)) {
|
|
3566
|
+
const assetExt = extname(assetPath).toLowerCase();
|
|
3567
|
+
// JS/CSS chunks can be invalidated by dashboard rebuilds while browsers retain
|
|
3568
|
+
// immutable cached entry chunks in local plugin environments.
|
|
3569
|
+
// Revalidate executable assets to avoid stale chunk graph 404s.
|
|
3570
|
+
const cacheControl = assetExt === ".js" || assetExt === ".css"
|
|
3571
|
+
? "no-cache"
|
|
3572
|
+
: "public, max-age=31536000, immutable";
|
|
3573
|
+
sendFile(req, res, assetPath, cacheControl);
|
|
3574
|
+
}
|
|
3575
|
+
else {
|
|
3576
|
+
if (/^assets\/[A-Za-z0-9_-]+\.js$/i.test(subPath)) {
|
|
3577
|
+
sendStaleChunkRecovery(res);
|
|
3578
|
+
return true;
|
|
3579
|
+
}
|
|
3580
|
+
send404(res);
|
|
3581
|
+
}
|
|
3582
|
+
return true;
|
|
3583
|
+
}
|
|
3584
|
+
// Check for an exact file match (e.g. favicon, manifest)
|
|
3585
|
+
if (subPath) {
|
|
3586
|
+
const filePath = resolveSafeDistPath(subPath);
|
|
3587
|
+
if (filePath && existsSync(filePath)) {
|
|
3588
|
+
sendFile(req, res, filePath, "no-cache");
|
|
3589
|
+
return true;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
// SPA fallback: serve index.html for all other routes under /orgx/live
|
|
3593
|
+
sendIndexHtml(req, res);
|
|
3594
|
+
return true;
|
|
3595
|
+
}
|
|
3596
|
+
// Catch-all for /orgx but not /orgx/live or /orgx/api
|
|
3597
|
+
if (url === "/orgx" || url === "/orgx/") {
|
|
3598
|
+
// Redirect to dashboard
|
|
3599
|
+
res.writeHead(302, {
|
|
3600
|
+
Location: "/orgx/live",
|
|
3601
|
+
...SECURITY_HEADERS,
|
|
3602
|
+
...CORS_HEADERS,
|
|
3603
|
+
});
|
|
3604
|
+
res.end();
|
|
3605
|
+
return true;
|
|
3606
|
+
}
|
|
3607
|
+
send404(res);
|
|
3608
|
+
return true;
|
|
3609
|
+
};
|
|
3610
|
+
}
|