@useorgx/openclaw-plugin 0.4.6 → 0.4.9
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 +310 -24
- package/dashboard/dist/assets/B5NEElEI.css +1 -0
- package/dashboard/dist/assets/BhapSNAs.js +215 -0
- package/dashboard/dist/assets/iFdvE7lx.js +1 -0
- package/dashboard/dist/assets/jRJsmpYM.js +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/activity-actor-fields.d.ts +3 -0
- package/dist/activity-actor-fields.js +128 -0
- package/dist/activity-store.js +12 -19
- package/dist/agent-context-store.js +5 -25
- package/dist/agent-run-store.js +5 -25
- package/dist/agent-suite.js +1 -8
- package/dist/artifacts/register-artifact.d.ts +47 -0
- package/dist/artifacts/register-artifact.js +271 -0
- package/dist/auth/flows.d.ts +47 -0
- package/dist/auth/flows.js +169 -0
- package/dist/auth-store.js +14 -39
- package/dist/byok-store.js +5 -19
- package/dist/cli/orgx.d.ts +66 -0
- package/dist/cli/orgx.js +91 -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 +1 -0
- package/dist/contracts/client.js +7 -5
- package/dist/contracts/shared-types.d.ts +147 -0
- package/dist/contracts/shared-types.js +3 -0
- package/dist/contracts/types.d.ts +1 -130
- package/dist/contracts/types.js +5 -0
- package/dist/entities/auto-assignment.d.ts +36 -0
- package/dist/entities/auto-assignment.js +115 -0
- package/dist/entity-comment-store.js +5 -25
- package/dist/hash-utils.d.ts +2 -0
- package/dist/hash-utils.js +12 -0
- package/dist/http/helpers/activity-headline.d.ts +10 -0
- package/dist/http/helpers/activity-headline.js +192 -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 +298 -0
- package/dist/http/helpers/auto-continue-engine.js +1218 -0
- package/dist/http/helpers/autopilot-operations.d.ts +157 -0
- package/dist/http/helpers/autopilot-operations.js +403 -0
- package/dist/http/helpers/autopilot-runtime.d.ts +42 -0
- package/dist/http/helpers/autopilot-runtime.js +319 -0
- package/dist/http/helpers/autopilot-slice-utils.d.ts +38 -0
- package/dist/http/helpers/autopilot-slice-utils.js +476 -0
- package/dist/http/helpers/decision-mapper.d.ts +12 -0
- package/dist/http/helpers/decision-mapper.js +44 -0
- package/dist/http/helpers/dispatch-lifecycle.d.ts +102 -0
- package/dist/http/helpers/dispatch-lifecycle.js +604 -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 +154 -0
- package/dist/http/helpers/mission-control.d.ts +94 -0
- package/dist/http/helpers/mission-control.js +894 -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/value-utils.d.ts +6 -0
- package/dist/http/helpers/value-utils.js +67 -0
- package/dist/http/index.d.ts +88 -0
- package/dist/http/index.js +2353 -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 +29 -0
- package/dist/http/routes/agent-suite.js +198 -0
- package/dist/http/routes/agents-catalog.d.ts +40 -0
- package/dist/http/routes/agents-catalog.js +83 -0
- package/dist/http/routes/billing.d.ts +23 -0
- package/dist/http/routes/billing.js +55 -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 +13 -0
- package/dist/http/routes/decision-actions.js +66 -0
- package/dist/http/routes/delegation.d.ts +19 -0
- package/dist/http/routes/delegation.js +32 -0
- package/dist/http/routes/entities.d.ts +47 -0
- package/dist/http/routes/entities.js +152 -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 +110 -0
- package/dist/http/routes/live-legacy.js +598 -0
- package/dist/http/routes/live-misc.d.ts +69 -0
- package/dist/http/routes/live-misc.js +206 -0
- package/dist/http/routes/live-snapshot.d.ts +90 -0
- package/dist/http/routes/live-snapshot.js +297 -0
- package/dist/http/routes/mission-control-actions.d.ts +83 -0
- package/dist/http/routes/mission-control-actions.js +541 -0
- package/dist/http/routes/mission-control-read.d.ts +28 -0
- package/dist/http/routes/mission-control-read.js +67 -0
- package/dist/http/routes/onboarding.d.ts +34 -0
- package/dist/http/routes/onboarding.js +101 -0
- package/dist/http/routes/run-control.d.ts +24 -0
- package/dist/http/routes/run-control.js +86 -0
- package/dist/http/routes/runtime-hooks.d.ts +69 -0
- package/dist/http/routes/runtime-hooks.js +437 -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 +42 -0
- package/dist/http/routes/work-artifacts.d.ts +9 -0
- package/dist/http/routes/work-artifacts.js +36 -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 -9664
- package/dist/index.js +122 -2121
- package/dist/json-utils.d.ts +1 -0
- package/dist/json-utils.js +8 -0
- package/dist/local-openclaw.js +8 -0
- package/dist/mcp-client-setup.js +75 -90
- package/dist/next-up-queue-store.js +4 -18
- package/dist/runtime-instance-store.js +8 -34
- package/dist/services/background.d.ts +23 -0
- package/dist/services/background.js +23 -0
- package/dist/services/instrumentation.d.ts +29 -0
- package/dist/services/instrumentation.js +136 -0
- 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/outbox-replay.d.ts +55 -0
- package/dist/sync/outbox-replay.js +514 -0
- package/dist/tools/core-tools.d.ts +76 -0
- package/dist/tools/core-tools.js +1005 -0
- package/dist/worker-supervisor.js +15 -0
- package/package.json +6 -1
- package/dashboard/dist/assets/0tOC3wSN.js +0 -214
- package/dashboard/dist/assets/Bm8QnMJ_.js +0 -1
- package/dashboard/dist/assets/CyxZio4Y.js +0 -1
- package/dashboard/dist/assets/DaAIOik3.css +0 -1
|
@@ -0,0 +1,2353 @@
|
|
|
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, 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 { appendActivityItems, listActivityPage, } from "../activity-store.js";
|
|
33
|
+
import { enrichActivityActorFields } from "../activity-actor-fields.js";
|
|
34
|
+
import { readByokKeys, writeByokKeys } from "../byok-store.js";
|
|
35
|
+
import { applyOrgxAgentSuitePlan, computeOrgxAgentSuitePlan, generateAgentSuiteOperationId, } from "../agent-suite.js";
|
|
36
|
+
import { listRuntimeInstances, resolveRuntimeHookToken, upsertRuntimeInstanceFromHook, } from "../runtime-instance-store.js";
|
|
37
|
+
import { parseJsonSafe } from "../json-utils.js";
|
|
38
|
+
import { readSkillPackState, refreshSkillPackState, updateSkillPackPolicy } from "../skill-pack-state.js";
|
|
39
|
+
import { posthogCapture } from "../telemetry/posthog.js";
|
|
40
|
+
import { createRouter } from "./router.js";
|
|
41
|
+
import { summarizeActivityHeadline } from "./helpers/activity-headline.js";
|
|
42
|
+
import { createAutoContinueEngine, } from "./helpers/auto-continue-engine.js";
|
|
43
|
+
import { createAutopilotOperations, } from "./helpers/autopilot-operations.js";
|
|
44
|
+
import { mapDecisionEntity } from "./helpers/decision-mapper.js";
|
|
45
|
+
import { idempotencyKey, stableHash } from "./helpers/hash-utils.js";
|
|
46
|
+
import { createCodexBinResolver, } from "./helpers/autopilot-slice-utils.js";
|
|
47
|
+
import { createLocalArtifactDetailFallbackBuilder } from "./helpers/artifact-fallback.js";
|
|
48
|
+
import { buildMissionControlGraph, dedupeStrings, isDoneStatus, isInProgressStatus, isTodoStatus, listEntitiesSafe, normalizeEntityMutationPayload, pickStringArray, resolveAutoAssignments, } from "./helpers/mission-control.js";
|
|
49
|
+
import { configureOpenClawProviderRouting, fetchBillingStatusSafe, isPidAlive, listOpenClawAgents, listOpenClawProviderModels, modelImpliesByok, normalizeOpenClawProvider, resolveAutoOpenClawProvider, resolveByokEnvOverrides, spawnOpenClawAgentTurn, stopDetachedProcess, } from "./helpers/openclaw-cli.js";
|
|
50
|
+
import { fetchKickoffContextSafe, renderKickoffMessage } from "./helpers/kickoff-context.js";
|
|
51
|
+
import { createDispatchLifecycle } from "./helpers/dispatch-lifecycle.js";
|
|
52
|
+
import { createRuntimeSseHub } from "./helpers/runtime-sse.js";
|
|
53
|
+
import { parseBooleanQuery, parsePositiveInt, pickHeaderString, pickNumber, pickString, } from "./helpers/value-utils.js";
|
|
54
|
+
import { registerAgentControlRoutes } from "./routes/agent-control.js";
|
|
55
|
+
import { registerAgentSuiteRoutes } from "./routes/agent-suite.js";
|
|
56
|
+
import { registerAgentsCatalogRoutes } from "./routes/agents-catalog.js";
|
|
57
|
+
import { registerBillingRoutes } from "./routes/billing.js";
|
|
58
|
+
import { registerDecisionActionsRoutes } from "./routes/decision-actions.js";
|
|
59
|
+
import { registerDelegationRoutes } from "./routes/delegation.js";
|
|
60
|
+
import { registerDebugRoutes } from "./routes/debug.js";
|
|
61
|
+
import { registerEntityDynamicRoutes } from "./routes/entity-dynamic.js";
|
|
62
|
+
import { registerEntitiesRoutes } from "./routes/entities.js";
|
|
63
|
+
import { registerHealthRoutes } from "./routes/health.js";
|
|
64
|
+
import { registerLiveLegacyRoutes } from "./routes/live-legacy.js";
|
|
65
|
+
import { registerLiveMiscRoutes } from "./routes/live-misc.js";
|
|
66
|
+
import { registerLiveSnapshotRoutes } from "./routes/live-snapshot.js";
|
|
67
|
+
import { registerMissionControlActionsRoutes } from "./routes/mission-control-actions.js";
|
|
68
|
+
import { registerMissionControlReadRoutes } from "./routes/mission-control-read.js";
|
|
69
|
+
import { registerOnboardingRoutes } from "./routes/onboarding.js";
|
|
70
|
+
import { registerRunControlRoutes } from "./routes/run-control.js";
|
|
71
|
+
import { registerRuntimeHookRoutes } from "./routes/runtime-hooks.js";
|
|
72
|
+
import { registerSettingsByokRoutes } from "./routes/settings-byok.js";
|
|
73
|
+
import { registerSummaryRoutes } from "./routes/summary.js";
|
|
74
|
+
import { registerWorkArtifactsRoutes } from "./routes/work-artifacts.js";
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// Helpers
|
|
77
|
+
// =============================================================================
|
|
78
|
+
async function resolveSkillPackOverrides(input) {
|
|
79
|
+
const state = readSkillPackState();
|
|
80
|
+
const force = Boolean(input.force);
|
|
81
|
+
if (!force && state.overrides)
|
|
82
|
+
return state.overrides;
|
|
83
|
+
const getSkillPack = input.client.getSkillPack;
|
|
84
|
+
if (typeof getSkillPack !== "function")
|
|
85
|
+
return state.overrides;
|
|
86
|
+
try {
|
|
87
|
+
const refreshed = await refreshSkillPackState({
|
|
88
|
+
getSkillPack: (args) => getSkillPack(args),
|
|
89
|
+
force,
|
|
90
|
+
});
|
|
91
|
+
return refreshed.state.overrides;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// If refresh fails (network, disk, etc.), fall back to cached overrides.
|
|
95
|
+
return state.overrides;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function safeErrorMessage(err) {
|
|
99
|
+
if (err instanceof Error)
|
|
100
|
+
return err.message;
|
|
101
|
+
if (typeof err === "string")
|
|
102
|
+
return err;
|
|
103
|
+
return "Unexpected error";
|
|
104
|
+
}
|
|
105
|
+
function titleCaseFromSlug(value) {
|
|
106
|
+
const parts = value
|
|
107
|
+
.split(/[^a-z0-9]+/i)
|
|
108
|
+
.map((part) => part.trim())
|
|
109
|
+
.filter(Boolean);
|
|
110
|
+
if (parts.length === 0)
|
|
111
|
+
return value;
|
|
112
|
+
return parts
|
|
113
|
+
.map((part) => `${part[0].toUpperCase()}${part.slice(1).toLowerCase()}`)
|
|
114
|
+
.join(" ");
|
|
115
|
+
}
|
|
116
|
+
function resolveOrgxAgentForDomain(domain) {
|
|
117
|
+
const normalized = domain.trim().toLowerCase();
|
|
118
|
+
if (!normalized)
|
|
119
|
+
return { id: "orgx", name: "OrgX" };
|
|
120
|
+
// Execution policies sometimes call this "orchestration" but the agent id is "orgx-orchestrator".
|
|
121
|
+
const slug = normalized === "orchestration" ? "orchestrator" : normalized;
|
|
122
|
+
// If the domain already looks like an OrgX agent id, keep it stable.
|
|
123
|
+
if (slug === "orgx")
|
|
124
|
+
return { id: "orgx", name: "OrgX" };
|
|
125
|
+
if (slug.startsWith("orgx-"))
|
|
126
|
+
return { id: slug, name: `OrgX ${titleCaseFromSlug(slug.slice(5))}` };
|
|
127
|
+
return { id: `orgx-${slug}`, name: `OrgX ${titleCaseFromSlug(slug)}` };
|
|
128
|
+
}
|
|
129
|
+
function isUnauthorizedOrgxError(err) {
|
|
130
|
+
const message = safeErrorMessage(err).toLowerCase();
|
|
131
|
+
return message.includes("401") || message.includes("unauthorized");
|
|
132
|
+
}
|
|
133
|
+
function readPositiveIntEnv(name, fallback, bounds) {
|
|
134
|
+
const raw = process.env[name];
|
|
135
|
+
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
136
|
+
return fallback;
|
|
137
|
+
const parsed = Number(raw);
|
|
138
|
+
if (!Number.isFinite(parsed))
|
|
139
|
+
return fallback;
|
|
140
|
+
const clamped = Math.floor(parsed);
|
|
141
|
+
if (typeof bounds?.min === "number" && clamped < bounds.min)
|
|
142
|
+
return fallback;
|
|
143
|
+
if (typeof bounds?.max === "number" && clamped > bounds.max)
|
|
144
|
+
return fallback;
|
|
145
|
+
return clamped;
|
|
146
|
+
}
|
|
147
|
+
async function withSoftTimeout(label, timeoutMs, work) {
|
|
148
|
+
let timer = null;
|
|
149
|
+
try {
|
|
150
|
+
return await Promise.race([
|
|
151
|
+
work,
|
|
152
|
+
new Promise((_, reject) => {
|
|
153
|
+
timer = setTimeout(() => {
|
|
154
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
155
|
+
}, timeoutMs);
|
|
156
|
+
}),
|
|
157
|
+
]);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
if (timer)
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
165
|
+
if (items.length === 0)
|
|
166
|
+
return [];
|
|
167
|
+
const limit = Math.max(1, Math.floor(concurrency));
|
|
168
|
+
const results = new Array(items.length);
|
|
169
|
+
let cursor = 0;
|
|
170
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
171
|
+
while (true) {
|
|
172
|
+
const index = cursor;
|
|
173
|
+
cursor += 1;
|
|
174
|
+
if (index >= items.length)
|
|
175
|
+
return;
|
|
176
|
+
results[index] = await mapper(items[index], index);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
await Promise.all(workers);
|
|
180
|
+
return results;
|
|
181
|
+
}
|
|
182
|
+
const ACTIVITY_WARM_THROTTLE_MS = 30_000;
|
|
183
|
+
const activityWarmByKey = new Map();
|
|
184
|
+
const SNAPSHOT_RESPONSE_CACHE_TTL_MS = 1_500;
|
|
185
|
+
const SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES = 16;
|
|
186
|
+
const SNAPSHOT_ACTIVITY_PERSIST_MIN_INTERVAL_MS = 15_000;
|
|
187
|
+
const SNAPSHOT_ACTIVITY_FINGERPRINT_DEPTH = 8;
|
|
188
|
+
const NEXT_UP_QUEUE_CACHE_TTL_MS = readPositiveIntEnv("ORGX_NEXT_UP_QUEUE_CACHE_TTL_MS", 4_000, { min: 250, max: 120_000 });
|
|
189
|
+
const NEXT_UP_QUEUE_STALE_TTL_MS = readPositiveIntEnv("ORGX_NEXT_UP_QUEUE_STALE_TTL_MS", 45_000, { min: 1_000, max: 600_000 });
|
|
190
|
+
const NEXT_UP_GRAPH_CONCURRENCY = readPositiveIntEnv("ORGX_NEXT_UP_GRAPH_CONCURRENCY", 20, { min: 1, max: 32 });
|
|
191
|
+
const NEXT_UP_LIVE_AGENTS_TIMEOUT_MS = readPositiveIntEnv("ORGX_NEXT_UP_LIVE_AGENTS_TIMEOUT_MS", 1_500, { min: 200, max: 20_000 });
|
|
192
|
+
const NEXT_UP_AGENT_CATALOG_TIMEOUT_MS = readPositiveIntEnv("ORGX_NEXT_UP_AGENT_CATALOG_TIMEOUT_MS", 900, { min: 100, max: 20_000 });
|
|
193
|
+
let lastSnapshotActivityPersistAt = 0;
|
|
194
|
+
let lastSnapshotActivityFingerprint = "";
|
|
195
|
+
const snapshotResponseCache = new Map();
|
|
196
|
+
const ACTIVITY_DECISION_EVENT_HINTS = new Set([
|
|
197
|
+
"decision_buffered",
|
|
198
|
+
"auto_continue_spawn_guard_blocked",
|
|
199
|
+
"autopilot_slice_mcp_handshake_failed",
|
|
200
|
+
"autopilot_slice_timeout",
|
|
201
|
+
"autopilot_slice_log_stall",
|
|
202
|
+
]);
|
|
203
|
+
const ACTIVITY_ARTIFACT_EVENT_HINTS = new Set([
|
|
204
|
+
"autopilot_slice_artifact_buffered",
|
|
205
|
+
]);
|
|
206
|
+
function normalizeActivityBucket(value) {
|
|
207
|
+
if (typeof value !== "string")
|
|
208
|
+
return null;
|
|
209
|
+
const normalized = value.trim().toLowerCase();
|
|
210
|
+
if (normalized === "artifact")
|
|
211
|
+
return "artifact";
|
|
212
|
+
if (normalized === "decision")
|
|
213
|
+
return "decision";
|
|
214
|
+
if (normalized === "message")
|
|
215
|
+
return "message";
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
function activityMetadataBoolean(metadata, keys) {
|
|
219
|
+
if (!metadata)
|
|
220
|
+
return null;
|
|
221
|
+
for (const key of keys) {
|
|
222
|
+
const value = metadata[key];
|
|
223
|
+
if (typeof value === "boolean")
|
|
224
|
+
return value;
|
|
225
|
+
if (typeof value === "string") {
|
|
226
|
+
const normalized = value.trim().toLowerCase();
|
|
227
|
+
if (normalized === "true")
|
|
228
|
+
return true;
|
|
229
|
+
if (normalized === "false")
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
function activityMetadataNumber(metadata, keys) {
|
|
236
|
+
if (!metadata)
|
|
237
|
+
return null;
|
|
238
|
+
for (const key of keys) {
|
|
239
|
+
const value = metadata[key];
|
|
240
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
241
|
+
return Math.max(0, value);
|
|
242
|
+
}
|
|
243
|
+
if (typeof value === "string") {
|
|
244
|
+
const parsed = Number(value);
|
|
245
|
+
if (Number.isFinite(parsed)) {
|
|
246
|
+
return Math.max(0, parsed);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
function activityMetadataEventName(metadata) {
|
|
253
|
+
if (!metadata)
|
|
254
|
+
return null;
|
|
255
|
+
const raw = metadata.event;
|
|
256
|
+
if (typeof raw !== "string")
|
|
257
|
+
return null;
|
|
258
|
+
const normalized = raw.trim().toLowerCase();
|
|
259
|
+
return normalized.length > 0 ? normalized : null;
|
|
260
|
+
}
|
|
261
|
+
function deriveStructuredActivityBucket(input) {
|
|
262
|
+
const metadata = input.metadata;
|
|
263
|
+
const explicit = normalizeActivityBucket(input.explicitBucket) ??
|
|
264
|
+
normalizeActivityBucket(metadata?.activity_bucket) ??
|
|
265
|
+
normalizeActivityBucket(metadata?.activityBucket) ??
|
|
266
|
+
normalizeActivityBucket(metadata?.bucket) ??
|
|
267
|
+
null;
|
|
268
|
+
if (explicit)
|
|
269
|
+
return explicit;
|
|
270
|
+
const event = activityMetadataEventName(metadata);
|
|
271
|
+
const decisionRequired = activityMetadataBoolean(metadata, ["decision_required", "decisionRequired"]) === true;
|
|
272
|
+
const artifacts = activityMetadataNumber(metadata, ["artifacts", "artifact_count", "artifactCount"]) ?? 0;
|
|
273
|
+
const decisions = activityMetadataNumber(metadata, ["decisions", "decision_count", "decisionCount"]) ?? 0;
|
|
274
|
+
const blockingDecisions = activityMetadataNumber(metadata, [
|
|
275
|
+
"blocking_decisions",
|
|
276
|
+
"blockingDecisions",
|
|
277
|
+
"blocking_decision_count",
|
|
278
|
+
"blockingDecisionCount",
|
|
279
|
+
]) ?? 0;
|
|
280
|
+
const nonBlockingDecisions = activityMetadataNumber(metadata, [
|
|
281
|
+
"non_blocking_decisions",
|
|
282
|
+
"nonBlockingDecisions",
|
|
283
|
+
"non_blocking_decision_count",
|
|
284
|
+
"nonBlockingDecisionCount",
|
|
285
|
+
]) ?? 0;
|
|
286
|
+
if (event === "autopilot_slice_result") {
|
|
287
|
+
if (decisionRequired || blockingDecisions > 0)
|
|
288
|
+
return "decision";
|
|
289
|
+
if (artifacts > 0)
|
|
290
|
+
return "artifact";
|
|
291
|
+
if (decisions > 0 || nonBlockingDecisions > 0)
|
|
292
|
+
return "decision";
|
|
293
|
+
return "message";
|
|
294
|
+
}
|
|
295
|
+
if (event && ACTIVITY_ARTIFACT_EVENT_HINTS.has(event))
|
|
296
|
+
return "artifact";
|
|
297
|
+
if (event && ACTIVITY_DECISION_EVENT_HINTS.has(event))
|
|
298
|
+
return "decision";
|
|
299
|
+
const hasArtifactReference = typeof metadata?.artifact_id === "string" ||
|
|
300
|
+
typeof metadata?.artifactId === "string" ||
|
|
301
|
+
typeof metadata?.work_artifact_id === "string";
|
|
302
|
+
if (hasArtifactReference || artifacts > 0)
|
|
303
|
+
return "artifact";
|
|
304
|
+
if (decisionRequired || blockingDecisions > 0 || decisions > 0 || nonBlockingDecisions > 0) {
|
|
305
|
+
return "decision";
|
|
306
|
+
}
|
|
307
|
+
return "message";
|
|
308
|
+
}
|
|
309
|
+
function snapshotActivityFingerprint(items) {
|
|
310
|
+
if (!Array.isArray(items) || items.length === 0)
|
|
311
|
+
return "0";
|
|
312
|
+
const sample = items
|
|
313
|
+
.slice(0, SNAPSHOT_ACTIVITY_FINGERPRINT_DEPTH)
|
|
314
|
+
.map((item) => `${item.id}|${item.timestamp}`)
|
|
315
|
+
.join(";");
|
|
316
|
+
return `${items.length}:${sample}`;
|
|
317
|
+
}
|
|
318
|
+
function readSnapshotResponseCache(key) {
|
|
319
|
+
const entry = snapshotResponseCache.get(key);
|
|
320
|
+
if (!entry)
|
|
321
|
+
return null;
|
|
322
|
+
if (entry.expiresAt <= Date.now()) {
|
|
323
|
+
snapshotResponseCache.delete(key);
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
return entry.payload;
|
|
327
|
+
}
|
|
328
|
+
function writeSnapshotResponseCache(key, payload) {
|
|
329
|
+
const now = Date.now();
|
|
330
|
+
snapshotResponseCache.set(key, {
|
|
331
|
+
expiresAt: now + SNAPSHOT_RESPONSE_CACHE_TTL_MS,
|
|
332
|
+
payload,
|
|
333
|
+
});
|
|
334
|
+
if (snapshotResponseCache.size <= SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES)
|
|
335
|
+
return;
|
|
336
|
+
for (const [cachedKey, entry] of snapshotResponseCache.entries()) {
|
|
337
|
+
if (entry.expiresAt <= now)
|
|
338
|
+
snapshotResponseCache.delete(cachedKey);
|
|
339
|
+
}
|
|
340
|
+
while (snapshotResponseCache.size > SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES) {
|
|
341
|
+
const oldestKey = snapshotResponseCache.keys().next().value;
|
|
342
|
+
if (!oldestKey)
|
|
343
|
+
break;
|
|
344
|
+
snapshotResponseCache.delete(oldestKey);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function clearSnapshotResponseCache() {
|
|
348
|
+
snapshotResponseCache.clear();
|
|
349
|
+
}
|
|
350
|
+
function isUserScopedApiKey(apiKey) {
|
|
351
|
+
return apiKey.trim().toLowerCase().startsWith("oxk_");
|
|
352
|
+
}
|
|
353
|
+
const buildLocalArtifactDetailFallback = createLocalArtifactDetailFallbackBuilder({
|
|
354
|
+
listActivityPage: ({ limit, cursor }) => listActivityPage({ limit, cursor }),
|
|
355
|
+
});
|
|
356
|
+
function maskSecret(value) {
|
|
357
|
+
if (!value)
|
|
358
|
+
return null;
|
|
359
|
+
const trimmed = value.trim();
|
|
360
|
+
if (!trimmed)
|
|
361
|
+
return null;
|
|
362
|
+
if (trimmed.length <= 8)
|
|
363
|
+
return `${trimmed[0]}…${trimmed.slice(-1)}`;
|
|
364
|
+
return `${trimmed.slice(0, 4)}…${trimmed.slice(-4)}`;
|
|
365
|
+
}
|
|
366
|
+
const { runtimeStreamSubscribers, writeRuntimeSseEvent, stopRuntimeStreamTimers, broadcastRuntimeSse, ensureRuntimeStreamTimers, } = createRuntimeSseHub({
|
|
367
|
+
listRuntimeInstances: ({ limit }) => listRuntimeInstances({ limit }),
|
|
368
|
+
});
|
|
369
|
+
function getScopedAgentIds(contexts) {
|
|
370
|
+
const scoped = new Set();
|
|
371
|
+
for (const [key, ctx] of Object.entries(contexts)) {
|
|
372
|
+
if (!ctx || typeof ctx !== "object")
|
|
373
|
+
continue;
|
|
374
|
+
const agentId = (ctx.agentId ?? key).trim();
|
|
375
|
+
if (!agentId)
|
|
376
|
+
continue;
|
|
377
|
+
const initiativeId = ctx.initiativeId?.trim() ?? "";
|
|
378
|
+
if (initiativeId) {
|
|
379
|
+
scoped.add(agentId);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return scoped;
|
|
383
|
+
}
|
|
384
|
+
function isUuidLike(value) {
|
|
385
|
+
const trimmed = (value ?? "").trim();
|
|
386
|
+
if (!trimmed)
|
|
387
|
+
return false;
|
|
388
|
+
// Accept any RFC 4122 UUID (v1-v5). We use this to distinguish real OrgX
|
|
389
|
+
// initiative ids from local placeholder group ids like "agent:main".
|
|
390
|
+
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);
|
|
391
|
+
}
|
|
392
|
+
function applyAgentContextsToSessionTree(input, contexts) {
|
|
393
|
+
if (!input || !Array.isArray(input.nodes))
|
|
394
|
+
return input;
|
|
395
|
+
const groupsById = new Map();
|
|
396
|
+
for (const group of input.groups ?? []) {
|
|
397
|
+
if (!group)
|
|
398
|
+
continue;
|
|
399
|
+
groupsById.set(group.id, {
|
|
400
|
+
id: group.id,
|
|
401
|
+
label: group.label,
|
|
402
|
+
status: group.status ?? null,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const nodes = input.nodes.map((node) => {
|
|
406
|
+
const existingInitiativeId = (node.initiativeId ?? "").trim();
|
|
407
|
+
if (isUuidLike(existingInitiativeId))
|
|
408
|
+
return node;
|
|
409
|
+
const runCtx = node.runId ? contexts.runs[node.runId] : null;
|
|
410
|
+
if (runCtx && runCtx.initiativeId && runCtx.initiativeId.trim().length > 0) {
|
|
411
|
+
const initiativeId = runCtx.initiativeId.trim();
|
|
412
|
+
const groupId = initiativeId;
|
|
413
|
+
const ctxTitle = (runCtx.initiativeTitle ?? "").trim();
|
|
414
|
+
const groupLabel = ctxTitle || node.groupLabel || initiativeId;
|
|
415
|
+
const existing = groupsById.get(groupId);
|
|
416
|
+
if (!existing) {
|
|
417
|
+
groupsById.set(groupId, {
|
|
418
|
+
id: groupId,
|
|
419
|
+
label: groupLabel,
|
|
420
|
+
status: node.status ?? null,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
else if (ctxTitle && (existing.label === groupId || existing.label.startsWith("Agent "))) {
|
|
424
|
+
groupsById.set(groupId, { ...existing, label: groupLabel });
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
...node,
|
|
428
|
+
initiativeId,
|
|
429
|
+
workstreamId: runCtx.workstreamId ?? node.workstreamId ?? null,
|
|
430
|
+
groupId,
|
|
431
|
+
groupLabel,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const agentId = node.agentId?.trim() ?? "";
|
|
435
|
+
if (!agentId)
|
|
436
|
+
return node;
|
|
437
|
+
const ctx = contexts.agents[agentId];
|
|
438
|
+
const initiativeId = ctx?.initiativeId?.trim() ?? "";
|
|
439
|
+
if (!initiativeId)
|
|
440
|
+
return node;
|
|
441
|
+
const groupId = initiativeId;
|
|
442
|
+
const ctxTitle = ctx.initiativeTitle?.trim() ?? "";
|
|
443
|
+
const groupLabel = ctxTitle || node.groupLabel || initiativeId;
|
|
444
|
+
const existing = groupsById.get(groupId);
|
|
445
|
+
if (!existing) {
|
|
446
|
+
groupsById.set(groupId, {
|
|
447
|
+
id: groupId,
|
|
448
|
+
label: groupLabel,
|
|
449
|
+
status: node.status ?? null,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
else if (ctxTitle && (existing.label === groupId || existing.label.startsWith("Agent "))) {
|
|
453
|
+
groupsById.set(groupId, { ...existing, label: groupLabel });
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
...node,
|
|
457
|
+
initiativeId,
|
|
458
|
+
workstreamId: ctx.workstreamId ?? node.workstreamId ?? null,
|
|
459
|
+
groupId,
|
|
460
|
+
groupLabel,
|
|
461
|
+
};
|
|
462
|
+
});
|
|
463
|
+
// Ensure every node's group exists.
|
|
464
|
+
for (const node of nodes) {
|
|
465
|
+
if (!groupsById.has(node.groupId)) {
|
|
466
|
+
groupsById.set(node.groupId, {
|
|
467
|
+
id: node.groupId,
|
|
468
|
+
label: node.groupLabel || node.groupId,
|
|
469
|
+
status: node.status ?? null,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
...input,
|
|
475
|
+
nodes,
|
|
476
|
+
groups: Array.from(groupsById.values()),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function applyAgentContextsToActivity(input, contexts) {
|
|
480
|
+
if (!Array.isArray(input))
|
|
481
|
+
return [];
|
|
482
|
+
return input.map((item) => {
|
|
483
|
+
let nextItem = item;
|
|
484
|
+
const existingInitiativeId = (item.initiativeId ?? "").trim();
|
|
485
|
+
if (!isUuidLike(existingInitiativeId)) {
|
|
486
|
+
const runCtx = item.runId ? contexts.runs[item.runId] : null;
|
|
487
|
+
if (runCtx && runCtx.initiativeId && runCtx.initiativeId.trim().length > 0) {
|
|
488
|
+
const initiativeId = runCtx.initiativeId.trim();
|
|
489
|
+
const metadata = item.metadata && typeof item.metadata === "object"
|
|
490
|
+
? { ...item.metadata }
|
|
491
|
+
: {};
|
|
492
|
+
metadata.orgx_context = {
|
|
493
|
+
initiativeId,
|
|
494
|
+
workstreamId: runCtx.workstreamId ?? null,
|
|
495
|
+
taskId: runCtx.taskId ?? null,
|
|
496
|
+
updatedAt: runCtx.updatedAt,
|
|
497
|
+
};
|
|
498
|
+
nextItem = {
|
|
499
|
+
...item,
|
|
500
|
+
initiativeId,
|
|
501
|
+
metadata,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
const agentId = item.agentId?.trim() ?? "";
|
|
506
|
+
if (agentId) {
|
|
507
|
+
const ctx = contexts.agents[agentId];
|
|
508
|
+
const initiativeId = ctx?.initiativeId?.trim() ?? "";
|
|
509
|
+
if (initiativeId) {
|
|
510
|
+
const metadata = item.metadata && typeof item.metadata === "object"
|
|
511
|
+
? { ...item.metadata }
|
|
512
|
+
: {};
|
|
513
|
+
metadata.orgx_context = {
|
|
514
|
+
initiativeId,
|
|
515
|
+
workstreamId: ctx.workstreamId ?? null,
|
|
516
|
+
taskId: ctx.taskId ?? null,
|
|
517
|
+
updatedAt: ctx.updatedAt,
|
|
518
|
+
};
|
|
519
|
+
nextItem = {
|
|
520
|
+
...item,
|
|
521
|
+
initiativeId,
|
|
522
|
+
metadata,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return enrichActivityActorFields(nextItem);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
function mergeSessionTrees(base, extra) {
|
|
532
|
+
const seenNodes = new Set();
|
|
533
|
+
const nodes = [];
|
|
534
|
+
for (const node of base.nodes ?? []) {
|
|
535
|
+
seenNodes.add(node.id);
|
|
536
|
+
nodes.push(node);
|
|
537
|
+
}
|
|
538
|
+
for (const node of extra.nodes ?? []) {
|
|
539
|
+
if (seenNodes.has(node.id))
|
|
540
|
+
continue;
|
|
541
|
+
seenNodes.add(node.id);
|
|
542
|
+
nodes.push(node);
|
|
543
|
+
}
|
|
544
|
+
const seenEdges = new Set();
|
|
545
|
+
const edges = [];
|
|
546
|
+
for (const edge of base.edges ?? []) {
|
|
547
|
+
const key = `${edge.parentId}→${edge.childId}`;
|
|
548
|
+
seenEdges.add(key);
|
|
549
|
+
edges.push(edge);
|
|
550
|
+
}
|
|
551
|
+
for (const edge of extra.edges ?? []) {
|
|
552
|
+
const key = `${edge.parentId}→${edge.childId}`;
|
|
553
|
+
if (seenEdges.has(key))
|
|
554
|
+
continue;
|
|
555
|
+
seenEdges.add(key);
|
|
556
|
+
edges.push(edge);
|
|
557
|
+
}
|
|
558
|
+
const groupsById = new Map();
|
|
559
|
+
for (const group of base.groups ?? []) {
|
|
560
|
+
groupsById.set(group.id, group);
|
|
561
|
+
}
|
|
562
|
+
for (const group of extra.groups ?? []) {
|
|
563
|
+
const existing = groupsById.get(group.id);
|
|
564
|
+
if (!existing) {
|
|
565
|
+
groupsById.set(group.id, group);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const nextLabel = existing.label === existing.id && group.label && group.label !== group.id
|
|
569
|
+
? group.label
|
|
570
|
+
: existing.label;
|
|
571
|
+
groupsById.set(group.id, { ...existing, label: nextLabel });
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
nodes,
|
|
575
|
+
edges,
|
|
576
|
+
groups: Array.from(groupsById.values()),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function mergeActivities(base, extra, limit) {
|
|
580
|
+
const merged = [...(base ?? []), ...(extra ?? [])].sort((a, b) => {
|
|
581
|
+
const timestampDelta = Date.parse(b.timestamp) - Date.parse(a.timestamp);
|
|
582
|
+
if (timestampDelta !== 0)
|
|
583
|
+
return timestampDelta;
|
|
584
|
+
return b.id.localeCompare(a.id);
|
|
585
|
+
});
|
|
586
|
+
const deduped = [];
|
|
587
|
+
const seen = new Set();
|
|
588
|
+
for (const item of merged) {
|
|
589
|
+
if (seen.has(item.id))
|
|
590
|
+
continue;
|
|
591
|
+
seen.add(item.id);
|
|
592
|
+
deduped.push(item);
|
|
593
|
+
if (deduped.length >= limit)
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
return deduped;
|
|
597
|
+
}
|
|
598
|
+
function normalizeRuntimeSourceForReporting(value) {
|
|
599
|
+
if (value === "codex")
|
|
600
|
+
return "codex";
|
|
601
|
+
if (value === "claude-code")
|
|
602
|
+
return "claude-code";
|
|
603
|
+
if (value === "api")
|
|
604
|
+
return "api";
|
|
605
|
+
return "openclaw";
|
|
606
|
+
}
|
|
607
|
+
function normalizeHookPhase(value) {
|
|
608
|
+
const normalized = (value ?? "").trim().toLowerCase();
|
|
609
|
+
if (normalized === "intent")
|
|
610
|
+
return "intent";
|
|
611
|
+
if (normalized === "execution")
|
|
612
|
+
return "execution";
|
|
613
|
+
if (normalized === "blocked")
|
|
614
|
+
return "blocked";
|
|
615
|
+
if (normalized === "review")
|
|
616
|
+
return "review";
|
|
617
|
+
if (normalized === "handoff")
|
|
618
|
+
return "handoff";
|
|
619
|
+
if (normalized === "completed")
|
|
620
|
+
return "completed";
|
|
621
|
+
return "execution";
|
|
622
|
+
}
|
|
623
|
+
function normalizeRuntimeSource(value) {
|
|
624
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
625
|
+
if (normalized === "openclaw")
|
|
626
|
+
return "openclaw";
|
|
627
|
+
if (normalized === "codex")
|
|
628
|
+
return "codex";
|
|
629
|
+
if (normalized === "claude-code")
|
|
630
|
+
return "claude-code";
|
|
631
|
+
if (normalized === "api")
|
|
632
|
+
return "api";
|
|
633
|
+
return "unknown";
|
|
634
|
+
}
|
|
635
|
+
function runtimeSourceDefaultAgentLabel(sourceClient) {
|
|
636
|
+
if (sourceClient === "codex")
|
|
637
|
+
return "Codex";
|
|
638
|
+
if (sourceClient === "claude-code")
|
|
639
|
+
return "Claude Code";
|
|
640
|
+
if (sourceClient === "openclaw")
|
|
641
|
+
return "OpenClaw";
|
|
642
|
+
if (sourceClient === "api")
|
|
643
|
+
return "OrgX API";
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
function runtimeSourceDefaultAgentId(sourceClient) {
|
|
647
|
+
if (sourceClient === "codex")
|
|
648
|
+
return "runtime:codex";
|
|
649
|
+
if (sourceClient === "claude-code")
|
|
650
|
+
return "runtime:claude-code";
|
|
651
|
+
if (sourceClient === "openclaw")
|
|
652
|
+
return "runtime:openclaw";
|
|
653
|
+
if (sourceClient === "api")
|
|
654
|
+
return "runtime:api";
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
function deriveRuntimeFallbackAgent(instance) {
|
|
658
|
+
const sourceClient = normalizeRuntimeSource(instance.sourceClient);
|
|
659
|
+
const agentId = (instance.agentId ?? "").trim() || runtimeSourceDefaultAgentId(sourceClient);
|
|
660
|
+
const agentName = (instance.agentName ?? "").trim() ||
|
|
661
|
+
(instance.displayName ?? "").trim() ||
|
|
662
|
+
runtimeSourceDefaultAgentLabel(sourceClient);
|
|
663
|
+
return {
|
|
664
|
+
agentId: agentId || null,
|
|
665
|
+
agentName: agentName || null,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function deriveRuntimeSessionStatus(instance) {
|
|
669
|
+
const state = (instance.state ?? "").trim().toLowerCase();
|
|
670
|
+
const phase = (instance.phase ?? "").trim().toLowerCase();
|
|
671
|
+
if (phase === "blocked" || state === "error")
|
|
672
|
+
return "blocked";
|
|
673
|
+
if (phase === "completed")
|
|
674
|
+
return "completed";
|
|
675
|
+
if (phase === "handoff")
|
|
676
|
+
return "handoff";
|
|
677
|
+
if (phase === "review")
|
|
678
|
+
return "review";
|
|
679
|
+
if (state === "stopped")
|
|
680
|
+
return "paused";
|
|
681
|
+
if (state === "stale")
|
|
682
|
+
return "queued";
|
|
683
|
+
return "running";
|
|
684
|
+
}
|
|
685
|
+
function runtimeMatchMaps(instances) {
|
|
686
|
+
const byRunId = new Map();
|
|
687
|
+
const byAgentInitiative = new Map();
|
|
688
|
+
for (const instance of instances) {
|
|
689
|
+
if (instance.runId && !byRunId.has(instance.runId)) {
|
|
690
|
+
byRunId.set(instance.runId, instance);
|
|
691
|
+
}
|
|
692
|
+
const agentId = instance.agentId?.trim() ?? "";
|
|
693
|
+
const initiativeId = instance.initiativeId?.trim() ?? "";
|
|
694
|
+
if (!agentId || !initiativeId)
|
|
695
|
+
continue;
|
|
696
|
+
const key = `${agentId}:${initiativeId}`;
|
|
697
|
+
if (!byAgentInitiative.has(key)) {
|
|
698
|
+
byAgentInitiative.set(key, instance);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return { byRunId, byAgentInitiative };
|
|
702
|
+
}
|
|
703
|
+
function enrichSessionsWithRuntime(input, instances) {
|
|
704
|
+
if (!Array.isArray(input.nodes) || input.nodes.length === 0)
|
|
705
|
+
return input;
|
|
706
|
+
if (instances.length === 0)
|
|
707
|
+
return input;
|
|
708
|
+
const { byRunId, byAgentInitiative } = runtimeMatchMaps(instances);
|
|
709
|
+
const nodes = input.nodes.map((node) => {
|
|
710
|
+
const byRun = node.runId ? byRunId.get(node.runId) ?? null : null;
|
|
711
|
+
const byAgent = !byRun && node.agentId && node.initiativeId
|
|
712
|
+
? byAgentInitiative.get(`${node.agentId}:${node.initiativeId}`) ?? null
|
|
713
|
+
: null;
|
|
714
|
+
const match = byRun ?? byAgent;
|
|
715
|
+
if (!match)
|
|
716
|
+
return node;
|
|
717
|
+
const runtimeStatus = deriveRuntimeSessionStatus(match);
|
|
718
|
+
const fallbackAgent = deriveRuntimeFallbackAgent(match);
|
|
719
|
+
const agentId = (node.agentId ?? "").trim() || fallbackAgent.agentId;
|
|
720
|
+
const agentName = (node.agentName ?? "").trim() || fallbackAgent.agentName;
|
|
721
|
+
const nodeStatus = (node.status ?? "").trim().toLowerCase();
|
|
722
|
+
const isLiveLikeNodeStatus = nodeStatus === "running" ||
|
|
723
|
+
nodeStatus === "active" ||
|
|
724
|
+
nodeStatus === "in_progress" ||
|
|
725
|
+
nodeStatus === "working" ||
|
|
726
|
+
nodeStatus === "planning" ||
|
|
727
|
+
nodeStatus === "dispatching";
|
|
728
|
+
const shouldDowngradeStatusFromRuntime = isLiveLikeNodeStatus && (runtimeStatus === "queued" || runtimeStatus === "paused");
|
|
729
|
+
const blockerReason = (node.blockerReason ?? "").trim() ||
|
|
730
|
+
(node.status?.toLowerCase() === "blocked" || match.phase?.toLowerCase() === "blocked"
|
|
731
|
+
? (match.lastMessage ?? "").trim()
|
|
732
|
+
: "");
|
|
733
|
+
return {
|
|
734
|
+
...node,
|
|
735
|
+
agentId: agentId || null,
|
|
736
|
+
agentName: agentName || null,
|
|
737
|
+
status: shouldDowngradeStatusFromRuntime ? runtimeStatus : node.status,
|
|
738
|
+
state: node.state ?? match.state ?? null,
|
|
739
|
+
lastEventSummary: shouldDowngradeStatusFromRuntime && runtimeStatus === "queued"
|
|
740
|
+
? node.lastEventSummary ?? "Recovered stale runtime; awaiting next dispatch."
|
|
741
|
+
: node.lastEventSummary,
|
|
742
|
+
blockerReason: blockerReason || node.blockerReason || null,
|
|
743
|
+
runtimeClient: normalizeRuntimeSource(match.sourceClient),
|
|
744
|
+
runtimeLabel: match.displayName,
|
|
745
|
+
runtimeProvider: match.providerLogo,
|
|
746
|
+
instanceId: match.id,
|
|
747
|
+
lastHeartbeatAt: match.lastHeartbeatAt ?? null,
|
|
748
|
+
};
|
|
749
|
+
});
|
|
750
|
+
return { ...input, nodes };
|
|
751
|
+
}
|
|
752
|
+
function injectRuntimeInstancesAsSessions(input, instances) {
|
|
753
|
+
if (!Array.isArray(input.nodes))
|
|
754
|
+
return input;
|
|
755
|
+
if (!Array.isArray(instances) || instances.length === 0)
|
|
756
|
+
return input;
|
|
757
|
+
const nodes = [...input.nodes];
|
|
758
|
+
const edges = Array.isArray(input.edges) ? input.edges : [];
|
|
759
|
+
const groups = [...(input.groups ?? [])];
|
|
760
|
+
const existingRunIds = new Set();
|
|
761
|
+
const existingNodeIds = new Set();
|
|
762
|
+
for (const node of nodes) {
|
|
763
|
+
existingNodeIds.add(node.id);
|
|
764
|
+
if (node.runId)
|
|
765
|
+
existingRunIds.add(node.runId);
|
|
766
|
+
}
|
|
767
|
+
const groupsById = new Map(groups.map((group) => [group.id, group]));
|
|
768
|
+
for (const instance of instances) {
|
|
769
|
+
if (!instance || typeof instance !== "object")
|
|
770
|
+
continue;
|
|
771
|
+
const runId = instance.runId?.trim() || instance.correlationId?.trim() || "";
|
|
772
|
+
if (!runId)
|
|
773
|
+
continue;
|
|
774
|
+
if (existingRunIds.has(runId))
|
|
775
|
+
continue;
|
|
776
|
+
// Only surface active runtime instances as synthetic sessions.
|
|
777
|
+
// Stale instances are reconciled onto existing sessions but shouldn't appear as fresh work.
|
|
778
|
+
if (instance.state !== "active")
|
|
779
|
+
continue;
|
|
780
|
+
const initiativeId = instance.initiativeId?.trim() || null;
|
|
781
|
+
const workstreamId = instance.workstreamId?.trim() || null;
|
|
782
|
+
const runtimeClient = normalizeRuntimeSource(instance.sourceClient);
|
|
783
|
+
const fallbackAgent = deriveRuntimeFallbackAgent(instance);
|
|
784
|
+
const groupId = initiativeId ?? fallbackAgent.agentId ?? `runtime:${runtimeClient}`;
|
|
785
|
+
const meta = instance.metadata && typeof instance.metadata === "object"
|
|
786
|
+
? instance.metadata
|
|
787
|
+
: {};
|
|
788
|
+
const titleHint = pickString(meta, ["workstream_title", "workstreamTitle"]) ??
|
|
789
|
+
(workstreamId ? `Workstream ${workstreamId.slice(0, 8)}` : null);
|
|
790
|
+
const initiativeHint = pickString(meta, ["initiative_title", "initiativeTitle"]) ??
|
|
791
|
+
(initiativeId ? `Initiative ${initiativeId.slice(0, 8)}` : null);
|
|
792
|
+
const groupLabel = (initiativeHint ?? fallbackAgent.agentName ?? groupId).trim();
|
|
793
|
+
if (!groupsById.has(groupId)) {
|
|
794
|
+
const group = { id: groupId, label: groupLabel, status: null };
|
|
795
|
+
groupsById.set(groupId, group);
|
|
796
|
+
groups.push(group);
|
|
797
|
+
}
|
|
798
|
+
const nodeId = `runtime:${instance.id}`;
|
|
799
|
+
if (existingNodeIds.has(nodeId))
|
|
800
|
+
continue;
|
|
801
|
+
existingNodeIds.add(nodeId);
|
|
802
|
+
existingRunIds.add(runId);
|
|
803
|
+
const status = deriveRuntimeSessionStatus(instance);
|
|
804
|
+
const blockerReason = status === "blocked" ? (instance.lastMessage ?? null) : null;
|
|
805
|
+
const blockers = status === "blocked" && typeof blockerReason === "string" && blockerReason.trim().length > 0
|
|
806
|
+
? [blockerReason.trim()]
|
|
807
|
+
: [];
|
|
808
|
+
const node = {
|
|
809
|
+
id: nodeId,
|
|
810
|
+
parentId: null,
|
|
811
|
+
runId,
|
|
812
|
+
title: titleHint ?? instance.lastMessage ?? `Runtime ${runId.slice(0, 8)}`,
|
|
813
|
+
agentId: fallbackAgent.agentId,
|
|
814
|
+
agentName: fallbackAgent.agentName,
|
|
815
|
+
status,
|
|
816
|
+
progress: instance.progressPct ?? null,
|
|
817
|
+
initiativeId,
|
|
818
|
+
workstreamId,
|
|
819
|
+
groupId,
|
|
820
|
+
groupLabel,
|
|
821
|
+
startedAt: instance.createdAt ?? instance.lastEventAt ?? null,
|
|
822
|
+
updatedAt: instance.updatedAt ?? null,
|
|
823
|
+
lastEventAt: instance.lastEventAt ?? null,
|
|
824
|
+
lastEventSummary: instance.lastMessage ?? null,
|
|
825
|
+
blockers,
|
|
826
|
+
blockerReason,
|
|
827
|
+
phase: instance.phase ?? null,
|
|
828
|
+
state: instance.state ?? null,
|
|
829
|
+
runtimeClient,
|
|
830
|
+
runtimeLabel: instance.displayName,
|
|
831
|
+
runtimeProvider: instance.providerLogo,
|
|
832
|
+
instanceId: instance.id,
|
|
833
|
+
lastHeartbeatAt: instance.lastHeartbeatAt ?? null,
|
|
834
|
+
};
|
|
835
|
+
nodes.push(node);
|
|
836
|
+
}
|
|
837
|
+
return { nodes, edges, groups };
|
|
838
|
+
}
|
|
839
|
+
function enrichActivityWithRuntime(input, instances) {
|
|
840
|
+
if (!Array.isArray(input) || input.length === 0)
|
|
841
|
+
return [];
|
|
842
|
+
if (instances.length === 0)
|
|
843
|
+
return input;
|
|
844
|
+
const { byRunId, byAgentInitiative } = runtimeMatchMaps(instances);
|
|
845
|
+
return input.map((item) => {
|
|
846
|
+
const byRun = item.runId ? byRunId.get(item.runId) ?? null : null;
|
|
847
|
+
const byAgent = !byRun && item.agentId && item.initiativeId
|
|
848
|
+
? byAgentInitiative.get(`${item.agentId}:${item.initiativeId}`) ?? null
|
|
849
|
+
: null;
|
|
850
|
+
const match = byRun ?? byAgent;
|
|
851
|
+
if (!match)
|
|
852
|
+
return item;
|
|
853
|
+
return {
|
|
854
|
+
...item,
|
|
855
|
+
runtimeClient: normalizeRuntimeSource(match.sourceClient),
|
|
856
|
+
runtimeLabel: match.displayName,
|
|
857
|
+
runtimeProvider: match.providerLogo,
|
|
858
|
+
instanceId: match.id,
|
|
859
|
+
lastHeartbeatAt: match.lastHeartbeatAt ?? null,
|
|
860
|
+
};
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
// =============================================================================
|
|
864
|
+
// Content-Type mapping
|
|
865
|
+
// =============================================================================
|
|
866
|
+
const MIME_TYPES = {
|
|
867
|
+
".html": "text/html; charset=utf-8",
|
|
868
|
+
".js": "application/javascript; charset=utf-8",
|
|
869
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
870
|
+
".css": "text/css; charset=utf-8",
|
|
871
|
+
".json": "application/json; charset=utf-8",
|
|
872
|
+
".png": "image/png",
|
|
873
|
+
".jpg": "image/jpeg",
|
|
874
|
+
".jpeg": "image/jpeg",
|
|
875
|
+
".gif": "image/gif",
|
|
876
|
+
".svg": "image/svg+xml",
|
|
877
|
+
".ico": "image/x-icon",
|
|
878
|
+
".woff": "font/woff",
|
|
879
|
+
".woff2": "font/woff2",
|
|
880
|
+
".ttf": "font/ttf",
|
|
881
|
+
".map": "application/json",
|
|
882
|
+
};
|
|
883
|
+
function contentType(filePath) {
|
|
884
|
+
return MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
885
|
+
}
|
|
886
|
+
// =============================================================================
|
|
887
|
+
// CORS + response hardening
|
|
888
|
+
// =============================================================================
|
|
889
|
+
const CORS_HEADERS = {
|
|
890
|
+
"Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
|
|
891
|
+
"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",
|
|
892
|
+
Vary: "Origin",
|
|
893
|
+
};
|
|
894
|
+
const CONTENT_SECURITY_POLICY = [
|
|
895
|
+
"default-src 'self'",
|
|
896
|
+
"base-uri 'self'",
|
|
897
|
+
"frame-ancestors 'none'",
|
|
898
|
+
"form-action 'self'",
|
|
899
|
+
"object-src 'none'",
|
|
900
|
+
"script-src 'self'",
|
|
901
|
+
"style-src 'self' 'unsafe-inline'",
|
|
902
|
+
"img-src 'self' data: blob:",
|
|
903
|
+
"font-src 'self' data:",
|
|
904
|
+
"media-src 'self'",
|
|
905
|
+
"connect-src 'self' https://*.useorgx.com https://*.openclaw.ai http://127.0.0.1:* http://localhost:*",
|
|
906
|
+
].join("; ");
|
|
907
|
+
const SECURITY_HEADERS = {
|
|
908
|
+
"X-Content-Type-Options": "nosniff",
|
|
909
|
+
"X-Frame-Options": "DENY",
|
|
910
|
+
"Referrer-Policy": "same-origin",
|
|
911
|
+
"X-Robots-Tag": "noindex, nofollow, noarchive, nosnippet, noimageindex",
|
|
912
|
+
"Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=(), midi=(), magnetometer=(), gyroscope=()",
|
|
913
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
914
|
+
"Cross-Origin-Resource-Policy": "same-origin",
|
|
915
|
+
"Origin-Agent-Cluster": "?1",
|
|
916
|
+
"X-Permitted-Cross-Domain-Policies": "none",
|
|
917
|
+
"Content-Security-Policy": CONTENT_SECURITY_POLICY,
|
|
918
|
+
};
|
|
919
|
+
function normalizeHost(value) {
|
|
920
|
+
return value.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
921
|
+
}
|
|
922
|
+
function isLoopbackHost(hostname) {
|
|
923
|
+
const host = normalizeHost(hostname);
|
|
924
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
925
|
+
}
|
|
926
|
+
function isTrustedOrigin(origin) {
|
|
927
|
+
try {
|
|
928
|
+
const parsed = new URL(origin);
|
|
929
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
return isLoopbackHost(parsed.hostname);
|
|
933
|
+
}
|
|
934
|
+
catch {
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function isTrustedRequestSource(headers) {
|
|
939
|
+
const fetchSite = pickHeaderString(headers, ["sec-fetch-site"]);
|
|
940
|
+
if (fetchSite) {
|
|
941
|
+
const normalizedFetchSite = fetchSite.trim().toLowerCase();
|
|
942
|
+
if (normalizedFetchSite !== "same-origin" &&
|
|
943
|
+
normalizedFetchSite !== "same-site" &&
|
|
944
|
+
normalizedFetchSite !== "none") {
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
const origin = pickHeaderString(headers, ["origin"]);
|
|
949
|
+
if (origin) {
|
|
950
|
+
return isTrustedOrigin(origin);
|
|
951
|
+
}
|
|
952
|
+
const referer = pickHeaderString(headers, ["referer"]);
|
|
953
|
+
if (referer) {
|
|
954
|
+
try {
|
|
955
|
+
return isTrustedOrigin(new URL(referer).origin);
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
const STREAM_IDLE_TIMEOUT_MS = 60_000;
|
|
964
|
+
// =============================================================================
|
|
965
|
+
// Resolve the dashboard/dist/ directory relative to this file
|
|
966
|
+
// =============================================================================
|
|
967
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
968
|
+
const __dirname = dirname(__filename);
|
|
969
|
+
const DIST_DIR = resolve(__dirname, "..", "..", "dashboard", "dist");
|
|
970
|
+
const RESOLVED_DIST_DIR = resolve(DIST_DIR);
|
|
971
|
+
const RESOLVED_DIST_ASSETS_DIR = resolve(DIST_DIR, "assets");
|
|
972
|
+
function resolveSafeDistPath(subPath) {
|
|
973
|
+
if (!subPath || subPath.includes("\0"))
|
|
974
|
+
return null;
|
|
975
|
+
const normalized = normalize(subPath).replace(/^([/\\])+/, "");
|
|
976
|
+
if (!normalized || normalized === ".")
|
|
977
|
+
return null;
|
|
978
|
+
const candidate = resolve(DIST_DIR, normalized);
|
|
979
|
+
const rel = relative(RESOLVED_DIST_DIR, candidate);
|
|
980
|
+
if (!rel || rel === "." || rel.startsWith("..") || rel.includes(`..${sep}`)) {
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
return candidate;
|
|
984
|
+
}
|
|
985
|
+
// =============================================================================
|
|
986
|
+
// Helpers
|
|
987
|
+
// =============================================================================
|
|
988
|
+
const IMMUTABLE_FILE_CACHE = new Map();
|
|
989
|
+
const IMMUTABLE_FILE_CACHE_MAX = 128;
|
|
990
|
+
const FILE_PREVIEW_MAX_BYTES = 1_000_000;
|
|
991
|
+
const FILE_PREVIEW_MAX_DIR_ENTRIES = 300;
|
|
992
|
+
function sendJson(res, status, data) {
|
|
993
|
+
const body = JSON.stringify(data);
|
|
994
|
+
res.writeHead(status, {
|
|
995
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
996
|
+
// Avoid browser/proxy caching for live dashboards.
|
|
997
|
+
"Cache-Control": "no-store",
|
|
998
|
+
...SECURITY_HEADERS,
|
|
999
|
+
...CORS_HEADERS,
|
|
1000
|
+
});
|
|
1001
|
+
res.end(body);
|
|
1002
|
+
}
|
|
1003
|
+
function sendHtml(res, status, html) {
|
|
1004
|
+
res.writeHead(status, {
|
|
1005
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1006
|
+
"Cache-Control": "no-store",
|
|
1007
|
+
...SECURITY_HEADERS,
|
|
1008
|
+
...CORS_HEADERS,
|
|
1009
|
+
});
|
|
1010
|
+
res.end(html);
|
|
1011
|
+
}
|
|
1012
|
+
function escapeHtml(value) {
|
|
1013
|
+
return value
|
|
1014
|
+
.replaceAll("&", "&")
|
|
1015
|
+
.replaceAll("<", "<")
|
|
1016
|
+
.replaceAll(">", ">")
|
|
1017
|
+
.replaceAll('"', """)
|
|
1018
|
+
.replaceAll("'", "'");
|
|
1019
|
+
}
|
|
1020
|
+
function resolveFilesystemOpenPath(rawPath) {
|
|
1021
|
+
let value = rawPath.trim();
|
|
1022
|
+
if (value.toLowerCase().startsWith("file://")) {
|
|
1023
|
+
value = value.replace(/^file:\/\//i, "");
|
|
1024
|
+
try {
|
|
1025
|
+
value = decodeURIComponent(value);
|
|
1026
|
+
}
|
|
1027
|
+
catch {
|
|
1028
|
+
// best effort
|
|
1029
|
+
}
|
|
1030
|
+
if (process.platform === "win32" && value.startsWith("/")) {
|
|
1031
|
+
value = value.slice(1);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (value.startsWith("~/")) {
|
|
1035
|
+
return resolve(homedir(), value.slice(2));
|
|
1036
|
+
}
|
|
1037
|
+
const looksWindowsAbsolute = /^[A-Za-z]:[\\/]/.test(value);
|
|
1038
|
+
if (value.startsWith("/") || looksWindowsAbsolute) {
|
|
1039
|
+
return resolve(value);
|
|
1040
|
+
}
|
|
1041
|
+
return resolve(process.cwd(), value);
|
|
1042
|
+
}
|
|
1043
|
+
function readFilePreview(pathname, totalBytes) {
|
|
1044
|
+
if (totalBytes <= 0) {
|
|
1045
|
+
return { previewBuffer: Buffer.alloc(0), truncated: false };
|
|
1046
|
+
}
|
|
1047
|
+
const previewBytes = Math.min(totalBytes, FILE_PREVIEW_MAX_BYTES);
|
|
1048
|
+
const previewBuffer = Buffer.alloc(previewBytes);
|
|
1049
|
+
const fd = openSync(pathname, "r");
|
|
1050
|
+
try {
|
|
1051
|
+
const bytesRead = readSync(fd, previewBuffer, 0, previewBytes, 0);
|
|
1052
|
+
if (bytesRead < previewBytes) {
|
|
1053
|
+
return {
|
|
1054
|
+
previewBuffer: previewBuffer.subarray(0, bytesRead),
|
|
1055
|
+
truncated: totalBytes > bytesRead,
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
return {
|
|
1059
|
+
previewBuffer,
|
|
1060
|
+
truncated: totalBytes > previewBytes,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
finally {
|
|
1064
|
+
closeSync(fd);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
function sendFile(res, filePath, cacheControl) {
|
|
1068
|
+
try {
|
|
1069
|
+
const shouldCacheImmutable = cacheControl.includes("immutable");
|
|
1070
|
+
if (shouldCacheImmutable) {
|
|
1071
|
+
const cached = IMMUTABLE_FILE_CACHE.get(filePath);
|
|
1072
|
+
if (cached) {
|
|
1073
|
+
res.writeHead(200, {
|
|
1074
|
+
"Content-Type": cached.contentType,
|
|
1075
|
+
"Cache-Control": cacheControl,
|
|
1076
|
+
...SECURITY_HEADERS,
|
|
1077
|
+
...CORS_HEADERS,
|
|
1078
|
+
});
|
|
1079
|
+
res.end(cached.content);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const content = readFileSync(filePath);
|
|
1084
|
+
const type = contentType(filePath);
|
|
1085
|
+
if (shouldCacheImmutable) {
|
|
1086
|
+
if (IMMUTABLE_FILE_CACHE.size >= IMMUTABLE_FILE_CACHE_MAX) {
|
|
1087
|
+
const firstKey = IMMUTABLE_FILE_CACHE.keys().next().value;
|
|
1088
|
+
if (firstKey)
|
|
1089
|
+
IMMUTABLE_FILE_CACHE.delete(firstKey);
|
|
1090
|
+
}
|
|
1091
|
+
IMMUTABLE_FILE_CACHE.set(filePath, { content, contentType: type });
|
|
1092
|
+
}
|
|
1093
|
+
res.writeHead(200, {
|
|
1094
|
+
"Content-Type": type,
|
|
1095
|
+
"Cache-Control": cacheControl,
|
|
1096
|
+
...SECURITY_HEADERS,
|
|
1097
|
+
...CORS_HEADERS,
|
|
1098
|
+
});
|
|
1099
|
+
res.end(content);
|
|
1100
|
+
}
|
|
1101
|
+
catch {
|
|
1102
|
+
send404(res);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function send404(res) {
|
|
1106
|
+
res.writeHead(404, {
|
|
1107
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1108
|
+
...SECURITY_HEADERS,
|
|
1109
|
+
...CORS_HEADERS,
|
|
1110
|
+
});
|
|
1111
|
+
res.end("Not Found");
|
|
1112
|
+
}
|
|
1113
|
+
function sendIndexHtml(res) {
|
|
1114
|
+
const indexPath = join(DIST_DIR, "index.html");
|
|
1115
|
+
if (existsSync(indexPath)) {
|
|
1116
|
+
sendFile(res, indexPath, "no-cache, no-store, must-revalidate");
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
res.writeHead(503, {
|
|
1120
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1121
|
+
...SECURITY_HEADERS,
|
|
1122
|
+
...CORS_HEADERS,
|
|
1123
|
+
});
|
|
1124
|
+
res.end("<html><body><h1>Dashboard not built</h1>" +
|
|
1125
|
+
"<p>Run <code>cd dashboard && npm run build</code> to build the SPA.</p>" +
|
|
1126
|
+
"</body></html>");
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
function parseJsonBody(body) {
|
|
1130
|
+
if (!body)
|
|
1131
|
+
return {};
|
|
1132
|
+
if (typeof body === "string") {
|
|
1133
|
+
try {
|
|
1134
|
+
const parsed = JSON.parse(body);
|
|
1135
|
+
return typeof parsed === "object" && parsed !== null
|
|
1136
|
+
? parsed
|
|
1137
|
+
: {};
|
|
1138
|
+
}
|
|
1139
|
+
catch {
|
|
1140
|
+
return {};
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (Buffer.isBuffer(body)) {
|
|
1144
|
+
try {
|
|
1145
|
+
const parsed = JSON.parse(body.toString("utf8"));
|
|
1146
|
+
return typeof parsed === "object" && parsed !== null
|
|
1147
|
+
? parsed
|
|
1148
|
+
: {};
|
|
1149
|
+
}
|
|
1150
|
+
catch {
|
|
1151
|
+
return {};
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (body instanceof Uint8Array) {
|
|
1155
|
+
try {
|
|
1156
|
+
const parsed = JSON.parse(Buffer.from(body).toString("utf8"));
|
|
1157
|
+
return typeof parsed === "object" && parsed !== null
|
|
1158
|
+
? parsed
|
|
1159
|
+
: {};
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
return {};
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
if (body instanceof ArrayBuffer) {
|
|
1166
|
+
try {
|
|
1167
|
+
const parsed = JSON.parse(Buffer.from(body).toString("utf8"));
|
|
1168
|
+
return typeof parsed === "object" && parsed !== null
|
|
1169
|
+
? parsed
|
|
1170
|
+
: {};
|
|
1171
|
+
}
|
|
1172
|
+
catch {
|
|
1173
|
+
return {};
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (typeof body === "object") {
|
|
1177
|
+
return body;
|
|
1178
|
+
}
|
|
1179
|
+
return {};
|
|
1180
|
+
}
|
|
1181
|
+
const MAX_JSON_BODY_BYTES = 1_000_000;
|
|
1182
|
+
const JSON_BODY_TIMEOUT_MS = 2_000;
|
|
1183
|
+
function chunkToBuffer(chunk) {
|
|
1184
|
+
if (!chunk)
|
|
1185
|
+
return Buffer.alloc(0);
|
|
1186
|
+
if (Buffer.isBuffer(chunk))
|
|
1187
|
+
return chunk;
|
|
1188
|
+
if (typeof chunk === "string")
|
|
1189
|
+
return Buffer.from(chunk, "utf8");
|
|
1190
|
+
if (chunk instanceof Uint8Array)
|
|
1191
|
+
return Buffer.from(chunk);
|
|
1192
|
+
try {
|
|
1193
|
+
return Buffer.from(JSON.stringify(chunk), "utf8");
|
|
1194
|
+
}
|
|
1195
|
+
catch {
|
|
1196
|
+
return Buffer.from(String(chunk), "utf8");
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
async function readRequestBodyBuffer(req) {
|
|
1200
|
+
const on = req.on ? req.on.bind(req) : null;
|
|
1201
|
+
if (!on)
|
|
1202
|
+
return null;
|
|
1203
|
+
return await new Promise((resolve) => {
|
|
1204
|
+
const chunks = [];
|
|
1205
|
+
let totalBytes = 0;
|
|
1206
|
+
let finished = false;
|
|
1207
|
+
const finish = (buffer) => {
|
|
1208
|
+
if (finished)
|
|
1209
|
+
return;
|
|
1210
|
+
finished = true;
|
|
1211
|
+
clearTimeout(timer);
|
|
1212
|
+
resolve(buffer);
|
|
1213
|
+
};
|
|
1214
|
+
const timer = setTimeout(() => finish(null), JSON_BODY_TIMEOUT_MS);
|
|
1215
|
+
on("data", (chunk) => {
|
|
1216
|
+
const buf = chunkToBuffer(chunk);
|
|
1217
|
+
if (buf.length === 0)
|
|
1218
|
+
return;
|
|
1219
|
+
totalBytes += buf.length;
|
|
1220
|
+
if (totalBytes > MAX_JSON_BODY_BYTES) {
|
|
1221
|
+
finish(null);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
chunks.push(buf);
|
|
1225
|
+
});
|
|
1226
|
+
const onDone = () => {
|
|
1227
|
+
if (chunks.length === 0) {
|
|
1228
|
+
finish(Buffer.alloc(0));
|
|
1229
|
+
}
|
|
1230
|
+
else {
|
|
1231
|
+
finish(Buffer.concat(chunks, totalBytes));
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
const once = (req.once ?? req.on)?.bind(req) ?? null;
|
|
1235
|
+
if (once) {
|
|
1236
|
+
once("end", onDone);
|
|
1237
|
+
once("error", () => finish(null));
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
on("end", onDone);
|
|
1241
|
+
on("error", () => finish(null));
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
async function parseJsonRequest(req) {
|
|
1246
|
+
const body = req.body;
|
|
1247
|
+
if (typeof body === "string" && body.length > 0) {
|
|
1248
|
+
return parseJsonBody(body);
|
|
1249
|
+
}
|
|
1250
|
+
if (Buffer.isBuffer(body) && body.length > 0) {
|
|
1251
|
+
return parseJsonBody(body);
|
|
1252
|
+
}
|
|
1253
|
+
if (body instanceof Uint8Array && body.byteLength > 0) {
|
|
1254
|
+
return parseJsonBody(body);
|
|
1255
|
+
}
|
|
1256
|
+
if (body instanceof ArrayBuffer && body.byteLength > 0) {
|
|
1257
|
+
return parseJsonBody(body);
|
|
1258
|
+
}
|
|
1259
|
+
if (body && typeof body === "object" && !Buffer.isBuffer(body)) {
|
|
1260
|
+
return parseJsonBody(body);
|
|
1261
|
+
}
|
|
1262
|
+
const streamed = await readRequestBodyBuffer(req);
|
|
1263
|
+
if (!streamed || streamed.length === 0) {
|
|
1264
|
+
return {};
|
|
1265
|
+
}
|
|
1266
|
+
return parseJsonBody(streamed);
|
|
1267
|
+
}
|
|
1268
|
+
// =============================================================================
|
|
1269
|
+
// Factory
|
|
1270
|
+
// =============================================================================
|
|
1271
|
+
export function createHttpHandler(config, client, getSnapshot, onboarding, diagnostics, adapters) {
|
|
1272
|
+
const dashboardEnabled = config.dashboardEnabled ??
|
|
1273
|
+
true;
|
|
1274
|
+
const outboxAdapter = adapters?.outbox ?? defaultOutboxAdapter;
|
|
1275
|
+
const openclawAdapter = adapters?.openclaw ?? {};
|
|
1276
|
+
const listAgents = openclawAdapter.listAgents ?? listOpenClawAgents;
|
|
1277
|
+
const spawnAgentTurn = openclawAdapter.spawnAgentTurn ?? spawnOpenClawAgentTurn;
|
|
1278
|
+
const stopProcess = openclawAdapter.stopDetachedProcess ?? stopDetachedProcess;
|
|
1279
|
+
const pidAlive = openclawAdapter.isPidAlive ?? isPidAlive;
|
|
1280
|
+
const telemetryDistinctId = (typeof config.installationId === "string" &&
|
|
1281
|
+
String(config.installationId).trim().length > 0
|
|
1282
|
+
? String(config.installationId).trim()
|
|
1283
|
+
: null) ?? "orgx-openclaw-plugin";
|
|
1284
|
+
const { emitActivitySafe, requestDecisionSafe, checkSpawnGuardSafe, extractSpawnGuardModelTier, buildPolicyEnforcedMessage, resolveDispatchExecutionPolicy, enforceSpawnGuardForDispatch, syncParentRollupsForTask, } = createDispatchLifecycle({
|
|
1285
|
+
client,
|
|
1286
|
+
pluginVersion: config.pluginVersion,
|
|
1287
|
+
randomUUID,
|
|
1288
|
+
safeErrorMessage,
|
|
1289
|
+
stableHash,
|
|
1290
|
+
idempotencyKey,
|
|
1291
|
+
pickString,
|
|
1292
|
+
deriveStructuredActivityBucket,
|
|
1293
|
+
});
|
|
1294
|
+
const { registerArtifactSafe, applyAgentStatusUpdatesSafe, resolveAgentDisplayName, dispatchFallbackWorkstreamTurn, } = createAutopilotOperations({
|
|
1295
|
+
client,
|
|
1296
|
+
randomUUID,
|
|
1297
|
+
safeErrorMessage,
|
|
1298
|
+
idempotencyKey,
|
|
1299
|
+
resolveDispatchExecutionPolicy,
|
|
1300
|
+
enforceSpawnGuardForDispatch,
|
|
1301
|
+
buildPolicyEnforcedMessage,
|
|
1302
|
+
syncParentRollupsForTask,
|
|
1303
|
+
emitActivitySafe,
|
|
1304
|
+
extractSpawnGuardModelTier,
|
|
1305
|
+
upsertAgentContext,
|
|
1306
|
+
upsertRunContext,
|
|
1307
|
+
spawnAgentTurn,
|
|
1308
|
+
upsertAgentRun,
|
|
1309
|
+
});
|
|
1310
|
+
const codexBinResolver = createCodexBinResolver();
|
|
1311
|
+
const resolveCodexBinInfo = () => codexBinResolver.resolveCodexBinInfo();
|
|
1312
|
+
const { autoContinueRuns, autoContinueSliceRuns, localInitiativeStatusOverrides, writeRuntimeEvent, autoContinueTickMs: AUTO_CONTINUE_TICK_MS, defaultAutoContinueTokenBudget, setLocalInitiativeStatusOverride, clearLocalInitiativeStatusOverride, applyLocalInitiativeOverrides, applyLocalInitiativeOverrideToGraph, updateInitiativeAutoContinueState, stopAutoContinueRun, tickAutoContinueRun, tickAllAutoContinue, isInitiativeActiveStatus, runningAutoContinueForWorkstream, startAutoContinueRun, } = createAutoContinueEngine({
|
|
1313
|
+
client,
|
|
1314
|
+
filename: __filename,
|
|
1315
|
+
safeErrorMessage,
|
|
1316
|
+
pidAlive,
|
|
1317
|
+
stopProcess,
|
|
1318
|
+
resolveOrgxAgentForDomain,
|
|
1319
|
+
checkSpawnGuardSafe,
|
|
1320
|
+
syncParentRollupsForTask,
|
|
1321
|
+
emitActivitySafe,
|
|
1322
|
+
requestDecisionSafe,
|
|
1323
|
+
registerArtifactSafe,
|
|
1324
|
+
applyAgentStatusUpdatesSafe,
|
|
1325
|
+
upsertRuntimeInstanceFromHook,
|
|
1326
|
+
broadcastRuntimeSse,
|
|
1327
|
+
clearSnapshotResponseCache,
|
|
1328
|
+
resolveByokEnvOverrides,
|
|
1329
|
+
randomUUID,
|
|
1330
|
+
});
|
|
1331
|
+
const nextUpQueueCache = new Map();
|
|
1332
|
+
const nextUpQueueInFlight = new Map();
|
|
1333
|
+
const nextUpQueueCacheKeyFor = (initiativeId) => initiativeId?.trim() || "__all__";
|
|
1334
|
+
const readNextUpQueueCache = (key, opts) => {
|
|
1335
|
+
const entry = nextUpQueueCache.get(key);
|
|
1336
|
+
if (!entry)
|
|
1337
|
+
return null;
|
|
1338
|
+
const now = Date.now();
|
|
1339
|
+
const allowStale = Boolean(opts?.allowStale);
|
|
1340
|
+
const stillFresh = entry.expiresAt > now;
|
|
1341
|
+
const stillStale = entry.staleUntil > now;
|
|
1342
|
+
if (!stillFresh && !stillStale) {
|
|
1343
|
+
nextUpQueueCache.delete(key);
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
if (!stillFresh && !allowStale)
|
|
1347
|
+
return null;
|
|
1348
|
+
return {
|
|
1349
|
+
items: entry.payload.items,
|
|
1350
|
+
degraded: [...entry.payload.degraded],
|
|
1351
|
+
};
|
|
1352
|
+
};
|
|
1353
|
+
const writeNextUpQueueCache = (key, payload) => {
|
|
1354
|
+
const now = Date.now();
|
|
1355
|
+
nextUpQueueCache.set(key, {
|
|
1356
|
+
expiresAt: now + NEXT_UP_QUEUE_CACHE_TTL_MS,
|
|
1357
|
+
staleUntil: now + NEXT_UP_QUEUE_STALE_TTL_MS,
|
|
1358
|
+
payload: {
|
|
1359
|
+
items: payload.items,
|
|
1360
|
+
degraded: [...payload.degraded],
|
|
1361
|
+
},
|
|
1362
|
+
});
|
|
1363
|
+
};
|
|
1364
|
+
async function buildNextUpQueueUncached(input) {
|
|
1365
|
+
const degraded = [];
|
|
1366
|
+
const requestedInitiativeId = input?.initiativeId?.trim() || null;
|
|
1367
|
+
const pinnedQueue = readNextUpQueuePins();
|
|
1368
|
+
const pinnedRankByKey = new Map();
|
|
1369
|
+
const pinnedByKey = new Map();
|
|
1370
|
+
for (let idx = 0; idx < pinnedQueue.pins.length; idx += 1) {
|
|
1371
|
+
const pin = pinnedQueue.pins[idx];
|
|
1372
|
+
const key = `${pin.initiativeId}:${pin.workstreamId}`;
|
|
1373
|
+
if (!pinnedRankByKey.has(key))
|
|
1374
|
+
pinnedRankByKey.set(key, idx);
|
|
1375
|
+
pinnedByKey.set(key, {
|
|
1376
|
+
preferredTaskId: pin.preferredTaskId ?? null,
|
|
1377
|
+
preferredMilestoneId: pin.preferredMilestoneId ?? null,
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
const initiativeTitleById = new Map();
|
|
1381
|
+
const initiativeStatusById = new Map();
|
|
1382
|
+
const initiativePriorityById = new Map();
|
|
1383
|
+
const snapshotInitiatives = formatInitiatives(getSnapshot());
|
|
1384
|
+
for (const initiative of snapshotInitiatives) {
|
|
1385
|
+
const id = initiative.id?.trim();
|
|
1386
|
+
if (!id)
|
|
1387
|
+
continue;
|
|
1388
|
+
initiativeTitleById.set(id, initiative.title);
|
|
1389
|
+
initiativeStatusById.set(id, initiative.status || "active");
|
|
1390
|
+
}
|
|
1391
|
+
const initiativeResult = await listEntitiesSafe(client, "initiative", { limit: 500 });
|
|
1392
|
+
if (initiativeResult.warning)
|
|
1393
|
+
degraded.push(initiativeResult.warning);
|
|
1394
|
+
const initiatives = initiativeResult.items;
|
|
1395
|
+
for (const entity of initiatives) {
|
|
1396
|
+
const record = entity;
|
|
1397
|
+
const id = pickString(record, ["id"]);
|
|
1398
|
+
if (!id)
|
|
1399
|
+
continue;
|
|
1400
|
+
const title = pickString(record, ["title", "name"]);
|
|
1401
|
+
const status = pickString(record, ["status"]);
|
|
1402
|
+
const priority = pickString(record, ["priority", "priority_label", "priorityLabel"]);
|
|
1403
|
+
if (title)
|
|
1404
|
+
initiativeTitleById.set(id, title);
|
|
1405
|
+
if (status)
|
|
1406
|
+
initiativeStatusById.set(id, status);
|
|
1407
|
+
if (priority)
|
|
1408
|
+
initiativePriorityById.set(id, priority);
|
|
1409
|
+
}
|
|
1410
|
+
for (const [initiativeId, override] of localInitiativeStatusOverrides.entries()) {
|
|
1411
|
+
initiativeStatusById.set(initiativeId, override.status);
|
|
1412
|
+
}
|
|
1413
|
+
const queueRank = (state) => {
|
|
1414
|
+
if (state === "running")
|
|
1415
|
+
return 0;
|
|
1416
|
+
if (state === "queued")
|
|
1417
|
+
return 1;
|
|
1418
|
+
if (state === "blocked")
|
|
1419
|
+
return 2;
|
|
1420
|
+
return 3;
|
|
1421
|
+
};
|
|
1422
|
+
const sortQueueItems = (a, b) => {
|
|
1423
|
+
const queueDelta = queueRank(a.queueState) - queueRank(b.queueState);
|
|
1424
|
+
if (queueDelta !== 0)
|
|
1425
|
+
return queueDelta;
|
|
1426
|
+
const aPinnedRank = pinnedRankByKey.get(`${a.initiativeId}:${a.workstreamId}`);
|
|
1427
|
+
const bPinnedRank = pinnedRankByKey.get(`${b.initiativeId}:${b.workstreamId}`);
|
|
1428
|
+
if (aPinnedRank !== undefined || bPinnedRank !== undefined) {
|
|
1429
|
+
const aRank = aPinnedRank ?? Number.POSITIVE_INFINITY;
|
|
1430
|
+
const bRank = bPinnedRank ?? Number.POSITIVE_INFINITY;
|
|
1431
|
+
if (aRank !== bRank)
|
|
1432
|
+
return aRank - bRank;
|
|
1433
|
+
}
|
|
1434
|
+
const priorityRank = (value) => {
|
|
1435
|
+
const normalized = (value ?? "").trim().toLowerCase();
|
|
1436
|
+
if (!normalized)
|
|
1437
|
+
return 4;
|
|
1438
|
+
if (normalized === "critical" || normalized === "p0" || normalized === "urgent")
|
|
1439
|
+
return 0;
|
|
1440
|
+
if (normalized === "high" || normalized === "p1")
|
|
1441
|
+
return 1;
|
|
1442
|
+
if (normalized === "medium" || normalized === "normal" || normalized === "p2")
|
|
1443
|
+
return 2;
|
|
1444
|
+
if (normalized === "low" || normalized === "p3")
|
|
1445
|
+
return 3;
|
|
1446
|
+
return 4;
|
|
1447
|
+
};
|
|
1448
|
+
const aInitiativePriority = priorityRank(initiativePriorityById.get(a.initiativeId));
|
|
1449
|
+
const bInitiativePriority = priorityRank(initiativePriorityById.get(b.initiativeId));
|
|
1450
|
+
if (aInitiativePriority !== bInitiativePriority) {
|
|
1451
|
+
return aInitiativePriority - bInitiativePriority;
|
|
1452
|
+
}
|
|
1453
|
+
const aPriority = typeof a.nextTaskPriority === "number" ? a.nextTaskPriority : 999;
|
|
1454
|
+
const bPriority = typeof b.nextTaskPriority === "number" ? b.nextTaskPriority : 999;
|
|
1455
|
+
if (aPriority !== bPriority)
|
|
1456
|
+
return aPriority - bPriority;
|
|
1457
|
+
const aDue = a.nextTaskDueAt ? Date.parse(a.nextTaskDueAt) : Number.POSITIVE_INFINITY;
|
|
1458
|
+
const bDue = b.nextTaskDueAt ? Date.parse(b.nextTaskDueAt) : Number.POSITIVE_INFINITY;
|
|
1459
|
+
if (aDue !== bDue)
|
|
1460
|
+
return aDue - bDue;
|
|
1461
|
+
const init = a.initiativeTitle.localeCompare(b.initiativeTitle);
|
|
1462
|
+
if (init !== 0)
|
|
1463
|
+
return init;
|
|
1464
|
+
return a.workstreamTitle.localeCompare(b.workstreamTitle);
|
|
1465
|
+
};
|
|
1466
|
+
const buildSessionFallbackQueue = async () => {
|
|
1467
|
+
let sessionTree = null;
|
|
1468
|
+
try {
|
|
1469
|
+
sessionTree = await client.getLiveSessions({
|
|
1470
|
+
initiative: requestedInitiativeId,
|
|
1471
|
+
limit: 500,
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
catch (err) {
|
|
1475
|
+
degraded.push(`live sessions fallback unavailable (${safeErrorMessage(err)})`);
|
|
1476
|
+
}
|
|
1477
|
+
const contextStore = readAgentContexts();
|
|
1478
|
+
const contextBundle = {
|
|
1479
|
+
agents: contextStore.agents,
|
|
1480
|
+
runs: contextStore.runs ?? {},
|
|
1481
|
+
};
|
|
1482
|
+
if (!sessionTree) {
|
|
1483
|
+
try {
|
|
1484
|
+
sessionTree = toLocalSessionTree(await loadLocalOpenClawSnapshot(400), 400);
|
|
1485
|
+
}
|
|
1486
|
+
catch (err) {
|
|
1487
|
+
degraded.push(`local sessions fallback unavailable (${safeErrorMessage(err)})`);
|
|
1488
|
+
return [];
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
sessionTree = applyAgentContextsToSessionTree(sessionTree, contextBundle);
|
|
1492
|
+
const grouped = new Map();
|
|
1493
|
+
const parseEpoch = (value) => {
|
|
1494
|
+
const parsed = value ? Date.parse(value) : Number.NaN;
|
|
1495
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1496
|
+
};
|
|
1497
|
+
for (const node of sessionTree.nodes ?? []) {
|
|
1498
|
+
const initiativeId = (node.initiativeId ?? "").trim();
|
|
1499
|
+
const workstreamId = (node.workstreamId ?? "").trim();
|
|
1500
|
+
if (!initiativeId || !workstreamId)
|
|
1501
|
+
continue;
|
|
1502
|
+
if (requestedInitiativeId && initiativeId !== requestedInitiativeId)
|
|
1503
|
+
continue;
|
|
1504
|
+
const initiativeStatus = initiativeStatusById.get(initiativeId) ?? "active";
|
|
1505
|
+
if (!isInitiativeActiveStatus(initiativeStatus))
|
|
1506
|
+
continue;
|
|
1507
|
+
const key = `${initiativeId}:${workstreamId}`;
|
|
1508
|
+
const epoch = parseEpoch(node.updatedAt ?? node.lastEventAt ?? node.startedAt);
|
|
1509
|
+
const existing = grouped.get(key);
|
|
1510
|
+
if (!existing) {
|
|
1511
|
+
grouped.set(key, {
|
|
1512
|
+
initiativeId,
|
|
1513
|
+
workstreamId,
|
|
1514
|
+
initiativeTitle: initiativeTitleById.get(initiativeId) ??
|
|
1515
|
+
node.groupLabel ??
|
|
1516
|
+
initiativeId,
|
|
1517
|
+
initiativeStatus,
|
|
1518
|
+
workstreamTitle: `Workstream ${workstreamId.slice(0, 8)}`,
|
|
1519
|
+
statuses: new Set([node.status]),
|
|
1520
|
+
blockers: Array.isArray(node.blockers) ? [...node.blockers] : [],
|
|
1521
|
+
latest: node,
|
|
1522
|
+
latestEpoch: epoch,
|
|
1523
|
+
});
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
existing.statuses.add(node.status);
|
|
1527
|
+
if (Array.isArray(node.blockers)) {
|
|
1528
|
+
for (const blocker of node.blockers) {
|
|
1529
|
+
if (typeof blocker !== "string" || blocker.trim().length === 0)
|
|
1530
|
+
continue;
|
|
1531
|
+
if (!existing.blockers.includes(blocker))
|
|
1532
|
+
existing.blockers.push(blocker);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (epoch >= existing.latestEpoch) {
|
|
1536
|
+
existing.latest = node;
|
|
1537
|
+
existing.latestEpoch = epoch;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
const fallbackItems = [];
|
|
1541
|
+
for (const entry of grouped.values()) {
|
|
1542
|
+
const statusValues = Array.from(entry.statuses).map((status) => status.toLowerCase());
|
|
1543
|
+
const hasBlocked = statusValues.some((status) => status === "blocked" || status === "failed") ||
|
|
1544
|
+
entry.blockers.length > 0;
|
|
1545
|
+
const hasRunning = statusValues.some((status) => isInProgressStatus(status));
|
|
1546
|
+
const hasQueued = statusValues.some((status) => status === "queued" || status === "pending");
|
|
1547
|
+
const queueState = hasRunning
|
|
1548
|
+
? "running"
|
|
1549
|
+
: hasBlocked
|
|
1550
|
+
? "blocked"
|
|
1551
|
+
: hasQueued
|
|
1552
|
+
? "queued"
|
|
1553
|
+
: "idle";
|
|
1554
|
+
const runnerAgentId = (entry.latest.agentId ?? "").trim() || "main";
|
|
1555
|
+
const runnerAgentName = (entry.latest.agentName ?? "").trim() ||
|
|
1556
|
+
initiativeTitleById.get(`agent:${runnerAgentId}`) ||
|
|
1557
|
+
runnerAgentId;
|
|
1558
|
+
const pinKey = `${entry.initiativeId}:${entry.workstreamId}`;
|
|
1559
|
+
fallbackItems.push({
|
|
1560
|
+
initiativeId: entry.initiativeId,
|
|
1561
|
+
initiativeTitle: entry.initiativeTitle,
|
|
1562
|
+
initiativeStatus: entry.initiativeStatus,
|
|
1563
|
+
workstreamId: entry.workstreamId,
|
|
1564
|
+
workstreamTitle: entry.workstreamTitle,
|
|
1565
|
+
workstreamStatus: hasBlocked ? "blocked" : hasRunning ? "active" : hasQueued ? "queued" : "idle",
|
|
1566
|
+
nextTaskId: entry.latest.id ?? null,
|
|
1567
|
+
nextTaskTitle: (entry.latest.lastEventSummary ?? "").trim() ||
|
|
1568
|
+
(entry.latest.title ?? "").trim() ||
|
|
1569
|
+
null,
|
|
1570
|
+
nextTaskPriority: null,
|
|
1571
|
+
nextTaskDueAt: null,
|
|
1572
|
+
runnerAgentId,
|
|
1573
|
+
runnerAgentName,
|
|
1574
|
+
runnerSource: "fallback",
|
|
1575
|
+
queueState,
|
|
1576
|
+
blockReason: hasBlocked
|
|
1577
|
+
? entry.blockers[0] ?? (statusValues.includes("failed") ? "Latest run failed" : "Workstream blocked")
|
|
1578
|
+
: null,
|
|
1579
|
+
isPinned: pinnedRankByKey.has(pinKey),
|
|
1580
|
+
pinnedRank: pinnedRankByKey.get(pinKey) ?? null,
|
|
1581
|
+
autoContinue: null,
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
fallbackItems.sort(sortQueueItems);
|
|
1585
|
+
return fallbackItems;
|
|
1586
|
+
};
|
|
1587
|
+
const scopedInitiatives = initiatives.filter((entity) => {
|
|
1588
|
+
const record = entity;
|
|
1589
|
+
const id = pickString(record, ["id"]);
|
|
1590
|
+
if (!id)
|
|
1591
|
+
return false;
|
|
1592
|
+
if (requestedInitiativeId && id !== requestedInitiativeId)
|
|
1593
|
+
return false;
|
|
1594
|
+
const status = pickString(record, ["status"]);
|
|
1595
|
+
return isInitiativeActiveStatus(status);
|
|
1596
|
+
});
|
|
1597
|
+
const agentCatalogById = new Map();
|
|
1598
|
+
try {
|
|
1599
|
+
const catalog = await withSoftTimeout("listAgents", NEXT_UP_AGENT_CATALOG_TIMEOUT_MS, listAgents());
|
|
1600
|
+
for (const entry of catalog) {
|
|
1601
|
+
if (!entry || typeof entry !== "object")
|
|
1602
|
+
continue;
|
|
1603
|
+
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
1604
|
+
if (!id)
|
|
1605
|
+
continue;
|
|
1606
|
+
const name = typeof entry.name === "string" && entry.name.trim().length > 0
|
|
1607
|
+
? entry.name.trim()
|
|
1608
|
+
: id;
|
|
1609
|
+
agentCatalogById.set(id, { id, name });
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
catch (err) {
|
|
1613
|
+
degraded.push(`agent catalog unavailable (${safeErrorMessage(err)})`);
|
|
1614
|
+
}
|
|
1615
|
+
const liveAgentsByInitiative = new Map();
|
|
1616
|
+
try {
|
|
1617
|
+
const data = await withSoftTimeout("live agents", NEXT_UP_LIVE_AGENTS_TIMEOUT_MS, client.getLiveAgents({
|
|
1618
|
+
initiative: requestedInitiativeId,
|
|
1619
|
+
includeIdle: true,
|
|
1620
|
+
}));
|
|
1621
|
+
for (const raw of Array.isArray(data.agents) ? data.agents : []) {
|
|
1622
|
+
if (!raw || typeof raw !== "object")
|
|
1623
|
+
continue;
|
|
1624
|
+
const row = raw;
|
|
1625
|
+
const initiativeId = pickString(row, ["initiativeId", "initiative_id"]);
|
|
1626
|
+
if (!initiativeId)
|
|
1627
|
+
continue;
|
|
1628
|
+
const id = pickString(row, ["id", "agentId", "agent_id"]) ??
|
|
1629
|
+
pickString(row, ["name", "agentName", "agent_name"]) ??
|
|
1630
|
+
"";
|
|
1631
|
+
const name = pickString(row, ["name", "agentName", "agent_name"]) ??
|
|
1632
|
+
id;
|
|
1633
|
+
if (!id || !name)
|
|
1634
|
+
continue;
|
|
1635
|
+
const list = liveAgentsByInitiative.get(initiativeId) ?? [];
|
|
1636
|
+
list.push({
|
|
1637
|
+
id,
|
|
1638
|
+
name,
|
|
1639
|
+
domain: pickString(row, ["domain", "role"]),
|
|
1640
|
+
});
|
|
1641
|
+
liveAgentsByInitiative.set(initiativeId, list);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
catch (err) {
|
|
1645
|
+
degraded.push(`live agents unavailable (${safeErrorMessage(err)})`);
|
|
1646
|
+
}
|
|
1647
|
+
const processInitiative = async (initiativeEntity) => {
|
|
1648
|
+
const initiativeRecord = initiativeEntity;
|
|
1649
|
+
const initiativeId = pickString(initiativeRecord, ["id"]);
|
|
1650
|
+
if (!initiativeId)
|
|
1651
|
+
return [];
|
|
1652
|
+
const initiativeTitle = pickString(initiativeRecord, ["title", "name"]) ?? initiativeId;
|
|
1653
|
+
const initiativeStatus = pickString(initiativeRecord, ["status"]) ?? "active";
|
|
1654
|
+
let graph;
|
|
1655
|
+
try {
|
|
1656
|
+
graph = applyLocalInitiativeOverrideToGraph(await buildMissionControlGraph(client, initiativeId, { initiativeEntity }));
|
|
1657
|
+
}
|
|
1658
|
+
catch (err) {
|
|
1659
|
+
degraded.push(`graph unavailable for ${initiativeId} (${safeErrorMessage(err)})`);
|
|
1660
|
+
return [];
|
|
1661
|
+
}
|
|
1662
|
+
const itemsForInitiative = [];
|
|
1663
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
1664
|
+
const workstreamNodes = graph.nodes.filter((node) => node.type === "workstream");
|
|
1665
|
+
const runningWorkstreams = new Set();
|
|
1666
|
+
const taskIsReady = (task) => task.dependencyIds.every((depId) => {
|
|
1667
|
+
const dependency = nodeById.get(depId);
|
|
1668
|
+
return dependency ? isDoneStatus(dependency.status) : true;
|
|
1669
|
+
});
|
|
1670
|
+
const taskHasBlockedParent = (task) => {
|
|
1671
|
+
const milestone = task.milestoneId ? nodeById.get(task.milestoneId) ?? null : null;
|
|
1672
|
+
const workstream = task.workstreamId ? nodeById.get(task.workstreamId) ?? null : null;
|
|
1673
|
+
return (milestone?.status?.toLowerCase() === "blocked" ||
|
|
1674
|
+
workstream?.status?.toLowerCase() === "blocked");
|
|
1675
|
+
};
|
|
1676
|
+
for (const workstream of workstreamNodes) {
|
|
1677
|
+
const todoTasks = graph.recentTodos
|
|
1678
|
+
.map((taskId) => nodeById.get(taskId))
|
|
1679
|
+
.filter((node) => node?.type === "task" &&
|
|
1680
|
+
node.workstreamId === workstream.id &&
|
|
1681
|
+
isTodoStatus(node.status));
|
|
1682
|
+
const pinKey = `${initiativeId}:${workstream.id}`;
|
|
1683
|
+
const pin = pinnedByKey.get(pinKey) ?? null;
|
|
1684
|
+
const preferredTask = pin?.preferredTaskId && nodeById.get(pin.preferredTaskId)
|
|
1685
|
+
? nodeById.get(pin.preferredTaskId) ?? null
|
|
1686
|
+
: null;
|
|
1687
|
+
const preferredMilestone = pin?.preferredMilestoneId && nodeById.get(pin.preferredMilestoneId)
|
|
1688
|
+
? nodeById.get(pin.preferredMilestoneId) ?? null
|
|
1689
|
+
: null;
|
|
1690
|
+
const preferredCandidates = [];
|
|
1691
|
+
if (preferredTask &&
|
|
1692
|
+
preferredTask.type === "task" &&
|
|
1693
|
+
preferredTask.workstreamId === workstream.id &&
|
|
1694
|
+
isTodoStatus(preferredTask.status)) {
|
|
1695
|
+
preferredCandidates.push(preferredTask);
|
|
1696
|
+
}
|
|
1697
|
+
if (preferredMilestone && preferredMilestone.type === "milestone") {
|
|
1698
|
+
for (const node of todoTasks) {
|
|
1699
|
+
if (node.milestoneId === preferredMilestone.id)
|
|
1700
|
+
preferredCandidates.push(node);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
const readyTask = todoTasks.find((task) => taskIsReady(task) && !taskHasBlockedParent(task));
|
|
1704
|
+
const preferredReadyTask = preferredCandidates.find((task) => taskIsReady(task) && !taskHasBlockedParent(task));
|
|
1705
|
+
const candidateTask = preferredReadyTask ?? readyTask ?? todoTasks[0] ?? null;
|
|
1706
|
+
const autoContinueRun = runningAutoContinueForWorkstream(initiativeId, workstream.id);
|
|
1707
|
+
let queueState = autoContinueRun
|
|
1708
|
+
? "running"
|
|
1709
|
+
: candidateTask
|
|
1710
|
+
? "queued"
|
|
1711
|
+
: "idle";
|
|
1712
|
+
let blockReason = null;
|
|
1713
|
+
if (!autoContinueRun && !readyTask && candidateTask) {
|
|
1714
|
+
queueState = "blocked";
|
|
1715
|
+
const blockedDeps = candidateTask.dependencyIds
|
|
1716
|
+
.map((depId) => nodeById.get(depId))
|
|
1717
|
+
.filter((dependency) => Boolean(dependency && !isDoneStatus(dependency.status)))
|
|
1718
|
+
.map((dependency) => dependency.title);
|
|
1719
|
+
if (blockedDeps.length > 0) {
|
|
1720
|
+
blockReason = `Waiting on ${blockedDeps.slice(0, 2).join(", ")}${blockedDeps.length > 2 ? "…" : ""}`;
|
|
1721
|
+
}
|
|
1722
|
+
else if (taskHasBlockedParent(candidateTask)) {
|
|
1723
|
+
blockReason = "Parent milestone or workstream is blocked";
|
|
1724
|
+
}
|
|
1725
|
+
else if (!taskIsReady(candidateTask)) {
|
|
1726
|
+
blockReason = "Task prerequisites are not complete";
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (!candidateTask && !autoContinueRun && !pin) {
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
runningWorkstreams.add(workstream.id);
|
|
1733
|
+
const assignedAgent = workstream.assignedAgents[0] ?? null;
|
|
1734
|
+
const inferredAgent = graph.initiative.assignedAgents[0] ??
|
|
1735
|
+
liveAgentsByInitiative.get(initiativeId)?.[0] ??
|
|
1736
|
+
(autoContinueRun?.agentId
|
|
1737
|
+
? {
|
|
1738
|
+
id: autoContinueRun.agentId,
|
|
1739
|
+
name: agentCatalogById.get(autoContinueRun.agentId)?.name ?? autoContinueRun.agentId,
|
|
1740
|
+
domain: null,
|
|
1741
|
+
}
|
|
1742
|
+
: null);
|
|
1743
|
+
const runnerSource = assignedAgent
|
|
1744
|
+
? "assigned"
|
|
1745
|
+
: inferredAgent
|
|
1746
|
+
? "inferred"
|
|
1747
|
+
: "fallback";
|
|
1748
|
+
const resolvedRunner = assignedAgent ?? inferredAgent;
|
|
1749
|
+
const runnerAgentId = resolvedRunner?.id ?? autoContinueRun?.agentId ?? "main";
|
|
1750
|
+
const runnerAgentName = resolvedRunner?.name ??
|
|
1751
|
+
agentCatalogById.get(runnerAgentId)?.name ??
|
|
1752
|
+
runnerAgentId;
|
|
1753
|
+
itemsForInitiative.push({
|
|
1754
|
+
initiativeId,
|
|
1755
|
+
initiativeTitle,
|
|
1756
|
+
initiativeStatus,
|
|
1757
|
+
workstreamId: workstream.id,
|
|
1758
|
+
workstreamTitle: workstream.title,
|
|
1759
|
+
workstreamStatus: workstream.status,
|
|
1760
|
+
nextTaskId: candidateTask?.id ??
|
|
1761
|
+
(autoContinueRun?.activeTaskId?.trim() || null),
|
|
1762
|
+
nextTaskTitle: candidateTask?.title ??
|
|
1763
|
+
(autoContinueRun?.activeTaskId
|
|
1764
|
+
? nodeById.get(autoContinueRun.activeTaskId)?.title ?? null
|
|
1765
|
+
: null),
|
|
1766
|
+
nextTaskPriority: candidateTask?.priorityNum ?? null,
|
|
1767
|
+
nextTaskDueAt: candidateTask?.dueDate ?? null,
|
|
1768
|
+
runnerAgentId,
|
|
1769
|
+
runnerAgentName,
|
|
1770
|
+
runnerSource,
|
|
1771
|
+
queueState,
|
|
1772
|
+
blockReason,
|
|
1773
|
+
isPinned: Boolean(pin),
|
|
1774
|
+
pinnedRank: pin ? (pinnedRankByKey.get(pinKey) ?? null) : null,
|
|
1775
|
+
autoContinue: autoContinueRun
|
|
1776
|
+
? {
|
|
1777
|
+
status: autoContinueRun.status,
|
|
1778
|
+
activeTaskId: autoContinueRun.activeTaskId,
|
|
1779
|
+
activeRunId: autoContinueRun.activeRunId,
|
|
1780
|
+
stopReason: autoContinueRun.stopReason,
|
|
1781
|
+
updatedAt: autoContinueRun.updatedAt,
|
|
1782
|
+
}
|
|
1783
|
+
: null,
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
const run = autoContinueRuns.get(initiativeId);
|
|
1787
|
+
if (run &&
|
|
1788
|
+
(run.status === "running" || run.status === "stopping") &&
|
|
1789
|
+
Array.isArray(run.allowedWorkstreamIds) &&
|
|
1790
|
+
run.allowedWorkstreamIds.length > 0) {
|
|
1791
|
+
for (const workstreamId of run.allowedWorkstreamIds) {
|
|
1792
|
+
if (runningWorkstreams.has(workstreamId))
|
|
1793
|
+
continue;
|
|
1794
|
+
const workstream = nodeById.get(workstreamId);
|
|
1795
|
+
if (!workstream || workstream.type !== "workstream")
|
|
1796
|
+
continue;
|
|
1797
|
+
itemsForInitiative.push({
|
|
1798
|
+
initiativeId,
|
|
1799
|
+
initiativeTitle,
|
|
1800
|
+
initiativeStatus,
|
|
1801
|
+
workstreamId: workstream.id,
|
|
1802
|
+
workstreamTitle: workstream.title,
|
|
1803
|
+
workstreamStatus: workstream.status,
|
|
1804
|
+
nextTaskId: run.activeTaskId,
|
|
1805
|
+
nextTaskTitle: run.activeTaskId
|
|
1806
|
+
? nodeById.get(run.activeTaskId)?.title ?? null
|
|
1807
|
+
: null,
|
|
1808
|
+
nextTaskPriority: null,
|
|
1809
|
+
nextTaskDueAt: null,
|
|
1810
|
+
runnerAgentId: run.agentId,
|
|
1811
|
+
runnerAgentName: agentCatalogById.get(run.agentId)?.name ?? run.agentId,
|
|
1812
|
+
runnerSource: "inferred",
|
|
1813
|
+
queueState: "running",
|
|
1814
|
+
blockReason: null,
|
|
1815
|
+
isPinned: Boolean(pinnedByKey.get(`${initiativeId}:${workstream.id}`)),
|
|
1816
|
+
pinnedRank: pinnedRankByKey.get(`${initiativeId}:${workstream.id}`) ?? null,
|
|
1817
|
+
autoContinue: {
|
|
1818
|
+
status: run.status,
|
|
1819
|
+
activeTaskId: run.activeTaskId,
|
|
1820
|
+
activeRunId: run.activeRunId,
|
|
1821
|
+
stopReason: run.stopReason,
|
|
1822
|
+
updatedAt: run.updatedAt,
|
|
1823
|
+
},
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
return itemsForInitiative;
|
|
1828
|
+
};
|
|
1829
|
+
const byInitiative = await mapWithConcurrency(scopedInitiatives, NEXT_UP_GRAPH_CONCURRENCY, processInitiative);
|
|
1830
|
+
const items = byInitiative.flat();
|
|
1831
|
+
if (items.length === 0) {
|
|
1832
|
+
const fallbackItems = await buildSessionFallbackQueue();
|
|
1833
|
+
if (fallbackItems.length > 0) {
|
|
1834
|
+
degraded.push("Using session-derived Next Up fallback.");
|
|
1835
|
+
items.push(...fallbackItems);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
items.sort(sortQueueItems);
|
|
1839
|
+
return { items, degraded };
|
|
1840
|
+
}
|
|
1841
|
+
async function buildNextUpQueue(input) {
|
|
1842
|
+
const key = nextUpQueueCacheKeyFor(input?.initiativeId?.trim() || null);
|
|
1843
|
+
const fresh = readNextUpQueueCache(key, { allowStale: false });
|
|
1844
|
+
if (fresh)
|
|
1845
|
+
return fresh;
|
|
1846
|
+
const inFlight = nextUpQueueInFlight.get(key) ?? null;
|
|
1847
|
+
if (inFlight) {
|
|
1848
|
+
const stale = readNextUpQueueCache(key, { allowStale: true });
|
|
1849
|
+
if (stale) {
|
|
1850
|
+
return {
|
|
1851
|
+
...stale,
|
|
1852
|
+
degraded: dedupeStrings([
|
|
1853
|
+
...stale.degraded,
|
|
1854
|
+
"Refreshing Next Up queue in background.",
|
|
1855
|
+
]),
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
return await inFlight;
|
|
1859
|
+
}
|
|
1860
|
+
const work = (async () => {
|
|
1861
|
+
const result = await buildNextUpQueueUncached(input);
|
|
1862
|
+
writeNextUpQueueCache(key, result);
|
|
1863
|
+
return result;
|
|
1864
|
+
})();
|
|
1865
|
+
nextUpQueueInFlight.set(key, work);
|
|
1866
|
+
try {
|
|
1867
|
+
const stale = readNextUpQueueCache(key, { allowStale: true });
|
|
1868
|
+
if (stale) {
|
|
1869
|
+
void work.catch(() => {
|
|
1870
|
+
// best effort refresh
|
|
1871
|
+
});
|
|
1872
|
+
return {
|
|
1873
|
+
...stale,
|
|
1874
|
+
degraded: dedupeStrings([
|
|
1875
|
+
...stale.degraded,
|
|
1876
|
+
"Using recent Next Up queue while refreshing.",
|
|
1877
|
+
]),
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
return await work;
|
|
1881
|
+
}
|
|
1882
|
+
finally {
|
|
1883
|
+
nextUpQueueInFlight.delete(key);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const autoContinueTimer = setInterval(() => {
|
|
1887
|
+
void tickAllAutoContinue();
|
|
1888
|
+
}, AUTO_CONTINUE_TICK_MS);
|
|
1889
|
+
autoContinueTimer.unref?.();
|
|
1890
|
+
const apiRouter = createRouter();
|
|
1891
|
+
registerOnboardingRoutes(apiRouter, {
|
|
1892
|
+
onboarding,
|
|
1893
|
+
parseJsonRequest,
|
|
1894
|
+
pickString: (input, keys) => pickString(input && typeof input === "object"
|
|
1895
|
+
? input
|
|
1896
|
+
: {}, keys),
|
|
1897
|
+
pickHeaderString: (headers, names) => pickHeaderString(headers && typeof headers === "object"
|
|
1898
|
+
? headers
|
|
1899
|
+
: {}, names),
|
|
1900
|
+
isUserScopedApiKey,
|
|
1901
|
+
sendJson,
|
|
1902
|
+
safeErrorMessage,
|
|
1903
|
+
getOnboardingState,
|
|
1904
|
+
});
|
|
1905
|
+
registerSummaryRoutes(apiRouter, {
|
|
1906
|
+
getSnapshot,
|
|
1907
|
+
getOrgSnapshot: () => client.getOrgSnapshot(),
|
|
1908
|
+
sendJson,
|
|
1909
|
+
writeHead: (response, status, headers) => response.writeHead(status, headers),
|
|
1910
|
+
end: (response) => response.end(),
|
|
1911
|
+
securityHeaders: SECURITY_HEADERS,
|
|
1912
|
+
corsHeaders: CORS_HEADERS,
|
|
1913
|
+
formatStatus,
|
|
1914
|
+
formatAgents,
|
|
1915
|
+
formatActivity,
|
|
1916
|
+
formatInitiatives,
|
|
1917
|
+
getOnboardingState: async () => getOnboardingState(await onboarding.getStatus()),
|
|
1918
|
+
});
|
|
1919
|
+
registerAgentSuiteRoutes(apiRouter, {
|
|
1920
|
+
pluginVersion: config.pluginVersion,
|
|
1921
|
+
telemetryDistinctId,
|
|
1922
|
+
parseJsonRequest,
|
|
1923
|
+
resolveSkillPackOverrides: ({ force }) => resolveSkillPackOverrides({ client, force }),
|
|
1924
|
+
readSkillPackState,
|
|
1925
|
+
computeOrgxAgentSuitePlan,
|
|
1926
|
+
applyOrgxAgentSuitePlan,
|
|
1927
|
+
generateAgentSuiteOperationId,
|
|
1928
|
+
updateSkillPackPolicy,
|
|
1929
|
+
posthogCapture,
|
|
1930
|
+
sendJson,
|
|
1931
|
+
safeErrorMessage,
|
|
1932
|
+
});
|
|
1933
|
+
registerDebugRoutes(apiRouter, {
|
|
1934
|
+
sendJson,
|
|
1935
|
+
safeErrorMessage,
|
|
1936
|
+
resolveCodexBinInfo,
|
|
1937
|
+
getCachedCodexProbeSummary: () => codexBinResolver.getCachedCodexProbeSummary(),
|
|
1938
|
+
});
|
|
1939
|
+
registerAgentsCatalogRoutes(apiRouter, {
|
|
1940
|
+
listAgents,
|
|
1941
|
+
loadLocalSnapshot: () => loadLocalOpenClawSnapshot(240).catch(() => null),
|
|
1942
|
+
readAgentContexts,
|
|
1943
|
+
readAgentRuns,
|
|
1944
|
+
sendJson,
|
|
1945
|
+
safeErrorMessage,
|
|
1946
|
+
});
|
|
1947
|
+
registerMissionControlReadRoutes(apiRouter, {
|
|
1948
|
+
autoContinueRuns,
|
|
1949
|
+
defaultAutoContinueTokenBudget,
|
|
1950
|
+
autoContinueTickMs: AUTO_CONTINUE_TICK_MS,
|
|
1951
|
+
buildMissionControlGraph: (initiativeId) => buildMissionControlGraph(client, initiativeId),
|
|
1952
|
+
applyLocalInitiativeOverrideToGraph: (graph) => applyLocalInitiativeOverrideToGraph(graph),
|
|
1953
|
+
buildNextUpQueue,
|
|
1954
|
+
sendJson,
|
|
1955
|
+
safeErrorMessage,
|
|
1956
|
+
});
|
|
1957
|
+
registerSettingsByokRoutes(apiRouter, {
|
|
1958
|
+
parseJsonRequest,
|
|
1959
|
+
readByokKeys,
|
|
1960
|
+
writeByokKeys,
|
|
1961
|
+
maskSecret,
|
|
1962
|
+
listAgents,
|
|
1963
|
+
listOpenClawProviderModels,
|
|
1964
|
+
sendJson,
|
|
1965
|
+
safeErrorMessage,
|
|
1966
|
+
});
|
|
1967
|
+
registerBillingRoutes(apiRouter, {
|
|
1968
|
+
client,
|
|
1969
|
+
parseJsonRequest,
|
|
1970
|
+
pickString,
|
|
1971
|
+
sendJson,
|
|
1972
|
+
safeErrorMessage,
|
|
1973
|
+
});
|
|
1974
|
+
registerDelegationRoutes(apiRouter, {
|
|
1975
|
+
client,
|
|
1976
|
+
parseJsonRequest,
|
|
1977
|
+
pickString,
|
|
1978
|
+
sendJson,
|
|
1979
|
+
safeErrorMessage,
|
|
1980
|
+
});
|
|
1981
|
+
registerEntitiesRoutes(apiRouter, {
|
|
1982
|
+
client,
|
|
1983
|
+
parseJsonRequest,
|
|
1984
|
+
pickString,
|
|
1985
|
+
normalizeEntityMutationPayload,
|
|
1986
|
+
resolveAutoAssignments: (input) => resolveAutoAssignments({
|
|
1987
|
+
client,
|
|
1988
|
+
...input,
|
|
1989
|
+
}),
|
|
1990
|
+
setLocalInitiativeStatusOverride,
|
|
1991
|
+
clearLocalInitiativeStatusOverride,
|
|
1992
|
+
isUnauthorizedOrgxError,
|
|
1993
|
+
applyLocalInitiativeOverrides,
|
|
1994
|
+
formatInitiatives,
|
|
1995
|
+
getSnapshot,
|
|
1996
|
+
sendJson,
|
|
1997
|
+
safeErrorMessage,
|
|
1998
|
+
});
|
|
1999
|
+
registerDecisionActionsRoutes(apiRouter, {
|
|
2000
|
+
parseJsonRequest,
|
|
2001
|
+
bulkDecideDecisions: (ids, action, note) => client.bulkDecideDecisions(ids, action, note),
|
|
2002
|
+
sendJson,
|
|
2003
|
+
safeErrorMessage,
|
|
2004
|
+
});
|
|
2005
|
+
registerRunControlRoutes(apiRouter, {
|
|
2006
|
+
parseJsonRequest,
|
|
2007
|
+
pickString,
|
|
2008
|
+
listRunCheckpoints: (runId) => client.listRunCheckpoints(runId),
|
|
2009
|
+
createRunCheckpoint: (runId, input) => client.createRunCheckpoint(runId, input),
|
|
2010
|
+
restoreRunCheckpoint: (runId, input) => client.restoreRunCheckpoint(runId, input),
|
|
2011
|
+
runAction: (runId, action, input) => client.runAction(runId, action, input),
|
|
2012
|
+
sendJson,
|
|
2013
|
+
safeErrorMessage,
|
|
2014
|
+
});
|
|
2015
|
+
registerWorkArtifactsRoutes(apiRouter, {
|
|
2016
|
+
rawRequest: (requestMethod, requestPath, body) => client.rawRequest(requestMethod, requestPath, body),
|
|
2017
|
+
buildLocalArtifactDetailFallback,
|
|
2018
|
+
sendJson,
|
|
2019
|
+
safeErrorMessage,
|
|
2020
|
+
});
|
|
2021
|
+
registerEntityDynamicRoutes(apiRouter, {
|
|
2022
|
+
parseJsonRequest,
|
|
2023
|
+
pickString,
|
|
2024
|
+
rawRequest: (requestMethod, requestPath, body) => client.rawRequest(requestMethod, requestPath, body),
|
|
2025
|
+
listEntityComments,
|
|
2026
|
+
mergeEntityComments: (remote, local) => mergeEntityComments(remote, local),
|
|
2027
|
+
appendEntityComment,
|
|
2028
|
+
updateEntity: (type, id, updates) => client.updateEntity(type, id, updates),
|
|
2029
|
+
setLocalInitiativeStatusOverride,
|
|
2030
|
+
clearLocalInitiativeStatusOverride,
|
|
2031
|
+
isUnauthorizedOrgxError,
|
|
2032
|
+
sendJson,
|
|
2033
|
+
safeErrorMessage,
|
|
2034
|
+
});
|
|
2035
|
+
registerMissionControlActionsRoutes(apiRouter, {
|
|
2036
|
+
parseJsonRequest,
|
|
2037
|
+
pickString,
|
|
2038
|
+
pickNumber,
|
|
2039
|
+
parseBooleanQuery,
|
|
2040
|
+
pickStringArray,
|
|
2041
|
+
dedupeStrings,
|
|
2042
|
+
resolveAgentDisplayName,
|
|
2043
|
+
buildNextUpQueue,
|
|
2044
|
+
startAutoContinueRun,
|
|
2045
|
+
autoContinueRuns,
|
|
2046
|
+
autoContinueSliceRuns,
|
|
2047
|
+
dispatchFallbackWorkstreamTurn,
|
|
2048
|
+
tickAutoContinueRun,
|
|
2049
|
+
stopAutoContinueRun,
|
|
2050
|
+
updateInitiativeAutoContinueState,
|
|
2051
|
+
tickAllAutoContinue,
|
|
2052
|
+
upsertNextUpQueuePin,
|
|
2053
|
+
removeNextUpQueuePin,
|
|
2054
|
+
setNextUpQueuePinOrder,
|
|
2055
|
+
resolveAutoAssignments,
|
|
2056
|
+
client,
|
|
2057
|
+
sendJson,
|
|
2058
|
+
safeErrorMessage,
|
|
2059
|
+
});
|
|
2060
|
+
registerAgentControlRoutes(apiRouter, {
|
|
2061
|
+
parseJsonRequest,
|
|
2062
|
+
pickString,
|
|
2063
|
+
parseBooleanQuery,
|
|
2064
|
+
randomUUID,
|
|
2065
|
+
normalizeOpenClawProvider,
|
|
2066
|
+
resolveAutoOpenClawProvider,
|
|
2067
|
+
modelImpliesByok,
|
|
2068
|
+
listAgents,
|
|
2069
|
+
fetchBillingStatusSafe,
|
|
2070
|
+
client,
|
|
2071
|
+
resolveDispatchExecutionPolicy,
|
|
2072
|
+
fetchKickoffContextSafe,
|
|
2073
|
+
renderKickoffMessage,
|
|
2074
|
+
posthogCapture: (input) => posthogCapture(input),
|
|
2075
|
+
telemetryDistinctId,
|
|
2076
|
+
pluginVersion: (config.pluginVersion ?? "").trim() || null,
|
|
2077
|
+
enforceSpawnGuardForDispatch,
|
|
2078
|
+
extractSpawnGuardModelTier,
|
|
2079
|
+
buildPolicyEnforcedMessage,
|
|
2080
|
+
syncParentRollupsForTask,
|
|
2081
|
+
emitActivitySafe,
|
|
2082
|
+
configureOpenClawProviderRouting,
|
|
2083
|
+
upsertAgentContext,
|
|
2084
|
+
upsertRunContext,
|
|
2085
|
+
spawnAgentTurn,
|
|
2086
|
+
upsertAgentRun,
|
|
2087
|
+
getAgentRun,
|
|
2088
|
+
stopProcess,
|
|
2089
|
+
markAgentRunStopped,
|
|
2090
|
+
writeRuntimeEvent,
|
|
2091
|
+
sendJson,
|
|
2092
|
+
safeErrorMessage,
|
|
2093
|
+
});
|
|
2094
|
+
registerLiveMiscRoutes(apiRouter, {
|
|
2095
|
+
parseJsonRequest,
|
|
2096
|
+
pickString,
|
|
2097
|
+
summarizeActivityHeadline,
|
|
2098
|
+
getLiveAgents: ({ initiative, includeIdle }) => client.getLiveAgents({ initiative, includeIdle }),
|
|
2099
|
+
getLiveInitiatives: ({ id, limit }) => client.getLiveInitiatives({ id, limit }),
|
|
2100
|
+
getLiveDecisions: ({ status, limit }) => client.getLiveDecisions({ status, limit }),
|
|
2101
|
+
getHandoffs: () => client.getHandoffs(),
|
|
2102
|
+
loadLocalOpenClawSnapshot,
|
|
2103
|
+
toLocalLiveAgents,
|
|
2104
|
+
toLocalLiveInitiatives,
|
|
2105
|
+
localInitiativeStatusOverrides,
|
|
2106
|
+
mapDecisionEntity,
|
|
2107
|
+
sendJson,
|
|
2108
|
+
safeErrorMessage,
|
|
2109
|
+
});
|
|
2110
|
+
registerLiveLegacyRoutes(apiRouter, {
|
|
2111
|
+
getLiveSessions: ({ initiative, limit }) => client.getLiveSessions({ initiative, limit }),
|
|
2112
|
+
getLiveActivity: ({ run, since, limit }) => client.getLiveActivity({ run, since, limit }),
|
|
2113
|
+
listRuntimeInstances,
|
|
2114
|
+
injectRuntimeInstancesAsSessions,
|
|
2115
|
+
enrichSessionsWithRuntime,
|
|
2116
|
+
loadLocalOpenClawSnapshot,
|
|
2117
|
+
toLocalSessionTree,
|
|
2118
|
+
readAgentContexts,
|
|
2119
|
+
applyAgentContextsToSessionTree,
|
|
2120
|
+
listActivityPage,
|
|
2121
|
+
applyAgentContextsToActivity,
|
|
2122
|
+
appendActivityItems,
|
|
2123
|
+
activityWarmByKey,
|
|
2124
|
+
activityWarmThrottleMs: ACTIVITY_WARM_THROTTLE_MS,
|
|
2125
|
+
outboxReadAllItems: () => outboxAdapter.readAllItems(),
|
|
2126
|
+
toLocalLiveActivity,
|
|
2127
|
+
loadLocalTurnDetail,
|
|
2128
|
+
sendJson,
|
|
2129
|
+
safeErrorMessage,
|
|
2130
|
+
sendHtml,
|
|
2131
|
+
resolveFilesystemOpenPath,
|
|
2132
|
+
escapeHtml,
|
|
2133
|
+
statSync,
|
|
2134
|
+
readdirSync,
|
|
2135
|
+
existsSync,
|
|
2136
|
+
resolvePath: resolve,
|
|
2137
|
+
readFilePreview,
|
|
2138
|
+
filePreviewMaxBytes: FILE_PREVIEW_MAX_BYTES,
|
|
2139
|
+
filePreviewMaxDirEntries: FILE_PREVIEW_MAX_DIR_ENTRIES,
|
|
2140
|
+
securityHeaders: SECURITY_HEADERS,
|
|
2141
|
+
corsHeaders: CORS_HEADERS,
|
|
2142
|
+
config: {
|
|
2143
|
+
baseUrl: config.baseUrl,
|
|
2144
|
+
apiKey: config.apiKey,
|
|
2145
|
+
userId: config.userId,
|
|
2146
|
+
},
|
|
2147
|
+
isUserScopedApiKey,
|
|
2148
|
+
streamIdleTimeoutMs: STREAM_IDLE_TIMEOUT_MS,
|
|
2149
|
+
});
|
|
2150
|
+
registerLiveSnapshotRoutes(apiRouter, {
|
|
2151
|
+
parsePositiveInt,
|
|
2152
|
+
readSnapshotResponseCache,
|
|
2153
|
+
writeSnapshotResponseCache,
|
|
2154
|
+
safeErrorMessage,
|
|
2155
|
+
readAgentContexts,
|
|
2156
|
+
getScopedAgentIds,
|
|
2157
|
+
readDiagnosticsOutboxStatus: async () => {
|
|
2158
|
+
if (!diagnostics?.getHealth)
|
|
2159
|
+
return null;
|
|
2160
|
+
const health = await diagnostics.getHealth({ probeRemote: false });
|
|
2161
|
+
if (!health || typeof health !== "object")
|
|
2162
|
+
return null;
|
|
2163
|
+
const maybeOutbox = health.outbox;
|
|
2164
|
+
if (!maybeOutbox || typeof maybeOutbox !== "object")
|
|
2165
|
+
return null;
|
|
2166
|
+
return maybeOutbox;
|
|
2167
|
+
},
|
|
2168
|
+
readOutboxSummary: () => outboxAdapter.readSummary(),
|
|
2169
|
+
readOutboxItems: () => outboxAdapter.readAllItems(),
|
|
2170
|
+
loadLocalOpenClawSnapshot,
|
|
2171
|
+
toLocalSessionTree,
|
|
2172
|
+
toLocalLiveActivity,
|
|
2173
|
+
toLocalLiveAgents,
|
|
2174
|
+
getLiveSessions: ({ initiative, limit }) => client.getLiveSessions({ initiative, limit }),
|
|
2175
|
+
getLiveActivity: ({ run, since, limit }) => client.getLiveActivity({ run, since, limit }),
|
|
2176
|
+
getHandoffs: () => client.getHandoffs(),
|
|
2177
|
+
getLiveDecisions: ({ status, limit }) => client.getLiveDecisions({ status, limit }),
|
|
2178
|
+
getLiveAgents: ({ initiative, includeIdle }) => client.getLiveAgents({ initiative, includeIdle }),
|
|
2179
|
+
mapDecisionEntity: (entry) => mapDecisionEntity(entry),
|
|
2180
|
+
applyAgentContextsToSessionTree,
|
|
2181
|
+
applyAgentContextsToActivity,
|
|
2182
|
+
mergeSessionTrees,
|
|
2183
|
+
mergeActivities,
|
|
2184
|
+
listRuntimeInstances,
|
|
2185
|
+
injectRuntimeInstancesAsSessions,
|
|
2186
|
+
enrichSessionsWithRuntime,
|
|
2187
|
+
enrichActivityWithRuntime,
|
|
2188
|
+
snapshotActivityFingerprint,
|
|
2189
|
+
appendActivityItems,
|
|
2190
|
+
snapshotActivityPersistMinIntervalMs: SNAPSHOT_ACTIVITY_PERSIST_MIN_INTERVAL_MS,
|
|
2191
|
+
readSnapshotPersistState: () => ({
|
|
2192
|
+
lastFingerprint: lastSnapshotActivityFingerprint,
|
|
2193
|
+
lastPersistAt: lastSnapshotActivityPersistAt,
|
|
2194
|
+
}),
|
|
2195
|
+
writeSnapshotPersistState: (state) => {
|
|
2196
|
+
lastSnapshotActivityFingerprint = state.lastFingerprint;
|
|
2197
|
+
lastSnapshotActivityPersistAt = state.lastPersistAt;
|
|
2198
|
+
},
|
|
2199
|
+
sendJson,
|
|
2200
|
+
});
|
|
2201
|
+
registerRuntimeHookRoutes(apiRouter, {
|
|
2202
|
+
parseJsonRequest,
|
|
2203
|
+
pickString,
|
|
2204
|
+
pickNumber,
|
|
2205
|
+
pickHeaderString,
|
|
2206
|
+
resolveRuntimeHookToken,
|
|
2207
|
+
maskSecret,
|
|
2208
|
+
parseJsonSafe,
|
|
2209
|
+
sendJson,
|
|
2210
|
+
safeErrorMessage,
|
|
2211
|
+
randomUUID,
|
|
2212
|
+
listRuntimeInstances,
|
|
2213
|
+
writeRuntimeSseEvent,
|
|
2214
|
+
runtimeStreamSubscribers,
|
|
2215
|
+
ensureRuntimeStreamTimers,
|
|
2216
|
+
stopRuntimeStreamTimers,
|
|
2217
|
+
upsertRuntimeInstanceFromHook,
|
|
2218
|
+
broadcastRuntimeSse,
|
|
2219
|
+
clearSnapshotResponseCache,
|
|
2220
|
+
normalizeHookPhase,
|
|
2221
|
+
normalizeRuntimeSourceForReporting: (value) => normalizeRuntimeSourceForReporting(value),
|
|
2222
|
+
emitActivity: (input) => client.emitActivity(input),
|
|
2223
|
+
securityHeaders: SECURITY_HEADERS,
|
|
2224
|
+
corsHeaders: CORS_HEADERS,
|
|
2225
|
+
});
|
|
2226
|
+
registerHealthRoutes(apiRouter, {
|
|
2227
|
+
diagnostics,
|
|
2228
|
+
readOutboxSummary: () => outboxAdapter.readSummary(),
|
|
2229
|
+
parseBooleanQuery,
|
|
2230
|
+
baseUrl: config.baseUrl,
|
|
2231
|
+
hasApiKey: Boolean(config.apiKey),
|
|
2232
|
+
sendJson,
|
|
2233
|
+
safeErrorMessage,
|
|
2234
|
+
});
|
|
2235
|
+
return async function handler(req, res) {
|
|
2236
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
2237
|
+
const rawUrl = req.url ?? "/";
|
|
2238
|
+
const [path, queryString] = rawUrl.split("?", 2);
|
|
2239
|
+
const url = path;
|
|
2240
|
+
const searchParams = new URLSearchParams(queryString ?? "");
|
|
2241
|
+
// Only handle /orgx paths — return false for everything else
|
|
2242
|
+
if (!url.startsWith("/orgx")) {
|
|
2243
|
+
return false;
|
|
2244
|
+
}
|
|
2245
|
+
// Handle CORS preflight
|
|
2246
|
+
if (method === "OPTIONS") {
|
|
2247
|
+
if (url.startsWith("/orgx/api/") && !isTrustedRequestSource(req.headers)) {
|
|
2248
|
+
sendJson(res, 403, {
|
|
2249
|
+
error: "Cross-origin browser requests are blocked for /orgx/api endpoints.",
|
|
2250
|
+
});
|
|
2251
|
+
return true;
|
|
2252
|
+
}
|
|
2253
|
+
res.writeHead(204, {
|
|
2254
|
+
...SECURITY_HEADERS,
|
|
2255
|
+
...CORS_HEADERS,
|
|
2256
|
+
});
|
|
2257
|
+
res.end();
|
|
2258
|
+
return true;
|
|
2259
|
+
}
|
|
2260
|
+
// ── API endpoints ──────────────────────────────────────────────────────
|
|
2261
|
+
if (url.startsWith("/orgx/api/")) {
|
|
2262
|
+
if (!isTrustedRequestSource(req.headers)) {
|
|
2263
|
+
sendJson(res, 403, {
|
|
2264
|
+
error: "Cross-origin browser requests are blocked for /orgx/api endpoints.",
|
|
2265
|
+
});
|
|
2266
|
+
return true;
|
|
2267
|
+
}
|
|
2268
|
+
const route = url.replace("/orgx/api/", "").replace(/\/+$/, "");
|
|
2269
|
+
const routed = apiRouter.match(method, route);
|
|
2270
|
+
if (routed) {
|
|
2271
|
+
await routed.handler({
|
|
2272
|
+
req,
|
|
2273
|
+
res,
|
|
2274
|
+
path: route,
|
|
2275
|
+
query: searchParams,
|
|
2276
|
+
body: undefined,
|
|
2277
|
+
state: {},
|
|
2278
|
+
});
|
|
2279
|
+
return true;
|
|
2280
|
+
}
|
|
2281
|
+
sendJson(res, 404, { error: "Unknown API endpoint" });
|
|
2282
|
+
return true;
|
|
2283
|
+
}
|
|
2284
|
+
// ── Dashboard SPA + static assets ──────────────────────────────────────
|
|
2285
|
+
if (!dashboardEnabled) {
|
|
2286
|
+
res.writeHead(404, {
|
|
2287
|
+
"Content-Type": "text/plain",
|
|
2288
|
+
...SECURITY_HEADERS,
|
|
2289
|
+
...CORS_HEADERS,
|
|
2290
|
+
});
|
|
2291
|
+
res.end("Dashboard is disabled");
|
|
2292
|
+
return true;
|
|
2293
|
+
}
|
|
2294
|
+
// Requests under /orgx/live
|
|
2295
|
+
if (url === "/orgx/live" || url.startsWith("/orgx/live/")) {
|
|
2296
|
+
const subPath = url.replace(/^\/orgx\/live\/?/, "");
|
|
2297
|
+
// Never expose source maps in shipped plugin dashboards.
|
|
2298
|
+
if (/\.map$/i.test(subPath)) {
|
|
2299
|
+
send404(res);
|
|
2300
|
+
return true;
|
|
2301
|
+
}
|
|
2302
|
+
// Static assets: /orgx/live/assets/* → dashboard/dist/assets/*
|
|
2303
|
+
// Hashed filenames get long-lived cache
|
|
2304
|
+
if (subPath.startsWith("assets/")) {
|
|
2305
|
+
const assetPath = resolveSafeDistPath(subPath);
|
|
2306
|
+
let isWithinAssetsDir = false;
|
|
2307
|
+
if (assetPath) {
|
|
2308
|
+
isWithinAssetsDir =
|
|
2309
|
+
assetPath === RESOLVED_DIST_ASSETS_DIR ||
|
|
2310
|
+
assetPath.startsWith(`${RESOLVED_DIST_ASSETS_DIR}${sep}`);
|
|
2311
|
+
}
|
|
2312
|
+
if (assetPath && isWithinAssetsDir && existsSync(assetPath)) {
|
|
2313
|
+
const assetExt = extname(assetPath).toLowerCase();
|
|
2314
|
+
// JS/CSS chunks can be invalidated by dashboard rebuilds while browsers retain
|
|
2315
|
+
// immutable cached entry chunks in local plugin environments.
|
|
2316
|
+
// Revalidate executable assets to avoid stale chunk graph 404s.
|
|
2317
|
+
const cacheControl = assetExt === ".js" || assetExt === ".css"
|
|
2318
|
+
? "no-cache"
|
|
2319
|
+
: "public, max-age=31536000, immutable";
|
|
2320
|
+
sendFile(res, assetPath, cacheControl);
|
|
2321
|
+
}
|
|
2322
|
+
else {
|
|
2323
|
+
send404(res);
|
|
2324
|
+
}
|
|
2325
|
+
return true;
|
|
2326
|
+
}
|
|
2327
|
+
// Check for an exact file match (e.g. favicon, manifest)
|
|
2328
|
+
if (subPath) {
|
|
2329
|
+
const filePath = resolveSafeDistPath(subPath);
|
|
2330
|
+
if (filePath && existsSync(filePath)) {
|
|
2331
|
+
sendFile(res, filePath, "no-cache");
|
|
2332
|
+
return true;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
// SPA fallback: serve index.html for all other routes under /orgx/live
|
|
2336
|
+
sendIndexHtml(res);
|
|
2337
|
+
return true;
|
|
2338
|
+
}
|
|
2339
|
+
// Catch-all for /orgx but not /orgx/live or /orgx/api
|
|
2340
|
+
if (url === "/orgx" || url === "/orgx/") {
|
|
2341
|
+
// Redirect to dashboard
|
|
2342
|
+
res.writeHead(302, {
|
|
2343
|
+
Location: "/orgx/live",
|
|
2344
|
+
...SECURITY_HEADERS,
|
|
2345
|
+
...CORS_HEADERS,
|
|
2346
|
+
});
|
|
2347
|
+
res.end();
|
|
2348
|
+
return true;
|
|
2349
|
+
}
|
|
2350
|
+
send404(res);
|
|
2351
|
+
return true;
|
|
2352
|
+
};
|
|
2353
|
+
}
|