@useorgx/openclaw-plugin 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +16 -1
  2. package/dashboard/dist/assets/index-BrAP-X_H.css +1 -0
  3. package/dashboard/dist/assets/index-cOk6qwh-.js +56 -0
  4. package/dashboard/dist/assets/orgx-logo-QSE5QWy4.png +0 -0
  5. package/dashboard/dist/brand/anthropic-mark.svg +10 -0
  6. package/dashboard/dist/brand/control-tower.png +0 -0
  7. package/dashboard/dist/brand/design-codex.png +0 -0
  8. package/dashboard/dist/brand/engineering-autopilot.png +0 -0
  9. package/dashboard/dist/brand/launch-captain.png +0 -0
  10. package/dashboard/dist/brand/openai-mark.svg +10 -0
  11. package/dashboard/dist/brand/openclaw-mark.svg +11 -0
  12. package/dashboard/dist/brand/orgx-logo.png +0 -0
  13. package/dashboard/dist/brand/pipeline-intelligence.png +0 -0
  14. package/dashboard/dist/brand/product-orchestrator.png +0 -0
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/api.d.ts +51 -1
  17. package/dist/api.d.ts.map +1 -1
  18. package/dist/api.js +105 -15
  19. package/dist/api.js.map +1 -1
  20. package/dist/auth-store.d.ts +20 -0
  21. package/dist/auth-store.d.ts.map +1 -0
  22. package/dist/auth-store.js +128 -0
  23. package/dist/auth-store.js.map +1 -0
  24. package/dist/dashboard-api.d.ts +2 -7
  25. package/dist/dashboard-api.d.ts.map +1 -1
  26. package/dist/dashboard-api.js +2 -4
  27. package/dist/dashboard-api.js.map +1 -1
  28. package/dist/http-handler.d.ts +32 -3
  29. package/dist/http-handler.d.ts.map +1 -1
  30. package/dist/http-handler.js +1849 -35
  31. package/dist/http-handler.js.map +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1453 -44
  34. package/dist/index.js.map +1 -1
  35. package/dist/local-openclaw.d.ts +87 -0
  36. package/dist/local-openclaw.d.ts.map +1 -0
  37. package/dist/local-openclaw.js +774 -0
  38. package/dist/local-openclaw.js.map +1 -0
  39. package/dist/openclaw.plugin.json +76 -0
  40. package/dist/outbox.d.ts +20 -0
  41. package/dist/outbox.d.ts.map +1 -0
  42. package/dist/outbox.js +86 -0
  43. package/dist/outbox.js.map +1 -0
  44. package/dist/types.d.ts +165 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/openclaw.plugin.json +1 -0
  47. package/package.json +4 -2
  48. package/skills/orgx/SKILL.md +180 -0
  49. package/dashboard/dist/assets/index-B_ag4FNd.css +0 -1
  50. package/dashboard/dist/assets/index-CNJpL8Wo.js +0 -40
@@ -9,11 +9,269 @@
9
9
  * /orgx/api/activity → activity feed
10
10
  * /orgx/api/initiatives → initiative data
11
11
  * /orgx/api/onboarding → onboarding / config state
12
+ * /orgx/api/delegation/preflight → delegation preflight
13
+ * /orgx/api/runs/:id/checkpoints → list/create checkpoints
14
+ * /orgx/api/runs/:id/checkpoints/:checkpointId/restore → restore checkpoint
15
+ * /orgx/api/runs/:id/actions/:action → run control action
12
16
  */
13
17
  import { readFileSync, existsSync } from "node:fs";
14
18
  import { join, extname } from "node:path";
15
19
  import { fileURLToPath } from "node:url";
20
+ import { homedir } from "node:os";
21
+ import { createHash } from "node:crypto";
16
22
  import { formatStatus, formatAgents, formatActivity, formatInitiatives, getOnboardingState, } from "./dashboard-api.js";
23
+ import { loadLocalOpenClawSnapshot, loadLocalTurnDetail, toLocalLiveActivity, toLocalLiveAgents, toLocalLiveInitiatives, toLocalSessionTree, } from "./local-openclaw.js";
24
+ import { readAllOutboxItems } from "./outbox.js";
25
+ // =============================================================================
26
+ // Helpers
27
+ // =============================================================================
28
+ function safeErrorMessage(err) {
29
+ if (err instanceof Error)
30
+ return err.message;
31
+ if (typeof err === "string")
32
+ return err;
33
+ return "Unexpected error";
34
+ }
35
+ const ACTIVITY_HEADLINE_TIMEOUT_MS = 4_000;
36
+ const ACTIVITY_HEADLINE_CACHE_TTL_MS = 12 * 60 * 60_000;
37
+ const ACTIVITY_HEADLINE_CACHE_MAX = 1_000;
38
+ const ACTIVITY_HEADLINE_MAX_INPUT_CHARS = 8_000;
39
+ const DEFAULT_ACTIVITY_HEADLINE_MODEL = "openai/gpt-4.1-nano";
40
+ const activityHeadlineCache = new Map();
41
+ let resolvedActivitySummaryApiKey;
42
+ function normalizeSpaces(value) {
43
+ return value.replace(/\s+/g, " ").trim();
44
+ }
45
+ function stripMarkdownLite(value) {
46
+ return value
47
+ .replace(/```[\s\S]*?```/g, " ")
48
+ .replace(/`([^`]+)`/g, "$1")
49
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
50
+ .replace(/__([^_]+)__/g, "$1")
51
+ .replace(/\*([^*\n]+)\*/g, "$1")
52
+ .replace(/_([^_\n]+)_/g, "$1")
53
+ .replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, "$1")
54
+ .replace(/^\s{0,3}#{1,6}\s+/gm, "")
55
+ .replace(/^\s*[-*]\s+/gm, "")
56
+ .replace(/^\s*\d+\.\s+/gm, "")
57
+ .replace(/\r\n/g, "\n")
58
+ .trim();
59
+ }
60
+ function cleanActivityHeadline(value) {
61
+ const lines = stripMarkdownLite(value)
62
+ .split("\n")
63
+ .map((line) => normalizeSpaces(line))
64
+ .filter((line) => line.length > 0 && !/^\|?[:\-| ]+\|?$/.test(line));
65
+ const headline = lines[0] ?? "";
66
+ if (!headline)
67
+ return "";
68
+ if (headline.length <= 108)
69
+ return headline;
70
+ return `${headline.slice(0, 107).trimEnd()}…`;
71
+ }
72
+ function heuristicActivityHeadline(text, title) {
73
+ const cleanedText = cleanActivityHeadline(text);
74
+ if (cleanedText.length > 0)
75
+ return cleanedText;
76
+ const cleanedTitle = cleanActivityHeadline(title ?? "");
77
+ if (cleanedTitle.length > 0)
78
+ return cleanedTitle;
79
+ return "Activity update";
80
+ }
81
+ function readDotEnvValue(pattern) {
82
+ try {
83
+ const envPath = join(homedir(), "Code", "orgx", "orgx", ".env.local");
84
+ const envContent = readFileSync(envPath, "utf-8");
85
+ const match = envContent.match(pattern);
86
+ return match?.[1]?.trim() ?? "";
87
+ }
88
+ catch {
89
+ return "";
90
+ }
91
+ }
92
+ function findOpenRouterApiKeyInConfig(input, trail = []) {
93
+ if (!input || typeof input !== "object")
94
+ return null;
95
+ if (Array.isArray(input)) {
96
+ for (const value of input) {
97
+ const nested = findOpenRouterApiKeyInConfig(value, trail);
98
+ if (nested)
99
+ return nested;
100
+ }
101
+ return null;
102
+ }
103
+ const record = input;
104
+ for (const [key, value] of Object.entries(record)) {
105
+ const nextTrail = [...trail, key.toLowerCase()];
106
+ if (typeof value === "string" &&
107
+ key.toLowerCase() === "apikey" &&
108
+ nextTrail.join(".").includes("openrouter") &&
109
+ value.trim().length > 0) {
110
+ return value.trim();
111
+ }
112
+ const nested = findOpenRouterApiKeyInConfig(value, nextTrail);
113
+ if (nested)
114
+ return nested;
115
+ }
116
+ return null;
117
+ }
118
+ function readOpenRouterApiKeyFromConfig() {
119
+ try {
120
+ const raw = readFileSync(join(homedir(), ".openclaw", "openclaw.json"), "utf8");
121
+ const parsed = JSON.parse(raw);
122
+ return findOpenRouterApiKeyInConfig(parsed) ?? "";
123
+ }
124
+ catch {
125
+ return "";
126
+ }
127
+ }
128
+ function resolveActivitySummaryApiKey() {
129
+ if (resolvedActivitySummaryApiKey !== undefined) {
130
+ return resolvedActivitySummaryApiKey;
131
+ }
132
+ const candidates = [
133
+ process.env.ORGX_ACTIVITY_SUMMARY_API_KEY ?? "",
134
+ process.env.OPENROUTER_API_KEY ?? "",
135
+ readDotEnvValue(/^ORGX_ACTIVITY_SUMMARY_API_KEY=["']?([^"'\n]+)["']?$/m),
136
+ readDotEnvValue(/^OPENROUTER_API_KEY=["']?([^"'\n]+)["']?$/m),
137
+ readOpenRouterApiKeyFromConfig(),
138
+ ];
139
+ const key = candidates.find((candidate) => candidate.trim().length > 0)?.trim() ?? "";
140
+ resolvedActivitySummaryApiKey = key || null;
141
+ return resolvedActivitySummaryApiKey;
142
+ }
143
+ function trimActivityHeadlineCache() {
144
+ while (activityHeadlineCache.size > ACTIVITY_HEADLINE_CACHE_MAX) {
145
+ const firstKey = activityHeadlineCache.keys().next().value;
146
+ if (!firstKey)
147
+ break;
148
+ activityHeadlineCache.delete(firstKey);
149
+ }
150
+ }
151
+ function extractCompletionText(payload) {
152
+ const choices = payload.choices;
153
+ if (!Array.isArray(choices) || choices.length === 0)
154
+ return null;
155
+ const first = choices[0];
156
+ if (!first || typeof first !== "object")
157
+ return null;
158
+ const firstRecord = first;
159
+ const message = firstRecord.message;
160
+ if (message && typeof message === "object") {
161
+ const content = message.content;
162
+ if (typeof content === "string") {
163
+ return content;
164
+ }
165
+ if (Array.isArray(content)) {
166
+ const textParts = content
167
+ .map((part) => {
168
+ if (typeof part === "string")
169
+ return part;
170
+ if (!part || typeof part !== "object")
171
+ return "";
172
+ const record = part;
173
+ return typeof record.text === "string" ? record.text : "";
174
+ })
175
+ .filter((part) => part.length > 0);
176
+ if (textParts.length > 0) {
177
+ return textParts.join(" ");
178
+ }
179
+ }
180
+ }
181
+ return pickString(firstRecord, ["text", "content"]);
182
+ }
183
+ async function summarizeActivityHeadline(input) {
184
+ const normalizedText = normalizeSpaces(input.text).slice(0, ACTIVITY_HEADLINE_MAX_INPUT_CHARS);
185
+ const normalizedTitle = normalizeSpaces(input.title ?? "");
186
+ const normalizedType = normalizeSpaces(input.type ?? "");
187
+ const heuristic = heuristicActivityHeadline(normalizedText, normalizedTitle);
188
+ const cacheKey = createHash("sha256")
189
+ .update(`${normalizedType}\n${normalizedTitle}\n${normalizedText}`)
190
+ .digest("hex");
191
+ const cached = activityHeadlineCache.get(cacheKey);
192
+ if (cached && cached.expiresAt > Date.now()) {
193
+ return { headline: cached.headline, source: cached.source, model: null };
194
+ }
195
+ const apiKey = resolveActivitySummaryApiKey();
196
+ if (!apiKey) {
197
+ activityHeadlineCache.set(cacheKey, {
198
+ headline: heuristic,
199
+ source: "heuristic",
200
+ expiresAt: Date.now() + ACTIVITY_HEADLINE_CACHE_TTL_MS,
201
+ });
202
+ trimActivityHeadlineCache();
203
+ return { headline: heuristic, source: "heuristic", model: null };
204
+ }
205
+ const controller = new AbortController();
206
+ const timeout = setTimeout(() => controller.abort(), ACTIVITY_HEADLINE_TIMEOUT_MS);
207
+ const model = process.env.ORGX_ACTIVITY_SUMMARY_MODEL?.trim() || DEFAULT_ACTIVITY_HEADLINE_MODEL;
208
+ const prompt = [
209
+ "Create one short activity title for a dashboard header.",
210
+ "Rules:",
211
+ "- Max 96 characters.",
212
+ "- Keep key numbers/status markers (for example: 15 tasks, 0 blocked).",
213
+ "- No markdown, no quotes, no trailing period unless needed.",
214
+ "- Prefer plain language over jargon.",
215
+ "",
216
+ `Type: ${normalizedType || "activity"}`,
217
+ normalizedTitle ? `Current title: ${normalizedTitle}` : "",
218
+ "Full detail:",
219
+ normalizedText,
220
+ ]
221
+ .filter(Boolean)
222
+ .join("\n");
223
+ try {
224
+ const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
225
+ method: "POST",
226
+ headers: {
227
+ "Content-Type": "application/json",
228
+ Authorization: `Bearer ${apiKey}`,
229
+ },
230
+ body: JSON.stringify({
231
+ model,
232
+ temperature: 0.1,
233
+ max_tokens: 48,
234
+ messages: [
235
+ {
236
+ role: "system",
237
+ content: "You write concise activity headers for operational dashboards. Return only the header text.",
238
+ },
239
+ {
240
+ role: "user",
241
+ content: prompt,
242
+ },
243
+ ],
244
+ }),
245
+ signal: controller.signal,
246
+ });
247
+ if (!response.ok) {
248
+ throw new Error(`headline model request failed (${response.status})`);
249
+ }
250
+ const payload = (await response.json());
251
+ const generated = cleanActivityHeadline(extractCompletionText(payload) ?? "");
252
+ const headline = generated || heuristic;
253
+ const source = generated ? "llm" : "heuristic";
254
+ activityHeadlineCache.set(cacheKey, {
255
+ headline,
256
+ source,
257
+ expiresAt: Date.now() + ACTIVITY_HEADLINE_CACHE_TTL_MS,
258
+ });
259
+ trimActivityHeadlineCache();
260
+ return { headline, source, model };
261
+ }
262
+ catch {
263
+ activityHeadlineCache.set(cacheKey, {
264
+ headline: heuristic,
265
+ source: "heuristic",
266
+ expiresAt: Date.now() + ACTIVITY_HEADLINE_CACHE_TTL_MS,
267
+ });
268
+ trimActivityHeadlineCache();
269
+ return { headline: heuristic, source: "heuristic", model: null };
270
+ }
271
+ finally {
272
+ clearTimeout(timeout);
273
+ }
274
+ }
17
275
  // =============================================================================
18
276
  // Content-Type mapping
19
277
  // =============================================================================
@@ -42,9 +300,10 @@ function contentType(filePath) {
42
300
  // =============================================================================
43
301
  const CORS_HEADERS = {
44
302
  "Access-Control-Allow-Origin": "*",
45
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
303
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
46
304
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
47
305
  };
306
+ const STREAM_IDLE_TIMEOUT_MS = 60_000;
48
307
  // =============================================================================
49
308
  // Resolve the dashboard/dist/ directory relative to this file
50
309
  // =============================================================================
@@ -137,6 +396,23 @@ function pickString(record, keys) {
137
396
  }
138
397
  return null;
139
398
  }
399
+ function pickHeaderString(headers, keys) {
400
+ for (const key of keys) {
401
+ const candidates = [key, key.toLowerCase(), key.toUpperCase()];
402
+ for (const candidate of candidates) {
403
+ const raw = headers[candidate];
404
+ if (typeof raw === "string" && raw.trim().length > 0) {
405
+ return raw.trim();
406
+ }
407
+ if (Array.isArray(raw)) {
408
+ const first = raw.find((value) => typeof value === "string" && value.trim().length > 0);
409
+ if (first)
410
+ return first.trim();
411
+ }
412
+ }
413
+ }
414
+ return null;
415
+ }
140
416
  function pickNumber(record, keys) {
141
417
  for (const key of keys) {
142
418
  const value = record[key];
@@ -203,10 +479,727 @@ function mapDecisionEntity(entity) {
203
479
  metadata: record,
204
480
  };
205
481
  }
482
+ function parsePositiveInt(raw, fallback) {
483
+ if (!raw)
484
+ return fallback;
485
+ const parsed = Number(raw);
486
+ if (!Number.isFinite(parsed))
487
+ return fallback;
488
+ return Math.max(1, Math.floor(parsed));
489
+ }
490
+ const DEFAULT_DURATION_HOURS = {
491
+ initiative: 40,
492
+ workstream: 16,
493
+ milestone: 6,
494
+ task: 2,
495
+ };
496
+ const DEFAULT_BUDGET_USD = {
497
+ initiative: 1500,
498
+ workstream: 300,
499
+ milestone: 120,
500
+ task: 40,
501
+ };
502
+ const PRIORITY_LABEL_TO_NUM = {
503
+ urgent: 10,
504
+ high: 25,
505
+ medium: 50,
506
+ low: 75,
507
+ };
508
+ function clampPriority(value) {
509
+ if (!Number.isFinite(value))
510
+ return 60;
511
+ return Math.max(1, Math.min(100, Math.round(value)));
512
+ }
513
+ function mapPriorityNumToLabel(priorityNum) {
514
+ if (priorityNum <= 12)
515
+ return "urgent";
516
+ if (priorityNum <= 30)
517
+ return "high";
518
+ if (priorityNum <= 60)
519
+ return "medium";
520
+ return "low";
521
+ }
522
+ function getRecordMetadata(record) {
523
+ const metadata = record.metadata;
524
+ if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
525
+ return metadata;
526
+ }
527
+ return {};
528
+ }
529
+ function extractBudgetUsdFromText(...texts) {
530
+ for (const text of texts) {
531
+ if (typeof text !== "string" || text.trim().length === 0)
532
+ continue;
533
+ const moneyMatch = /(?:expected\s+budget|budget)[^0-9$]{0,24}\$?\s*([0-9][0-9,]*(?:\.[0-9]{1,2})?)/i.exec(text);
534
+ if (!moneyMatch)
535
+ continue;
536
+ const numeric = Number(moneyMatch[1].replace(/,/g, ""));
537
+ if (Number.isFinite(numeric) && numeric >= 0)
538
+ return numeric;
539
+ }
540
+ return null;
541
+ }
542
+ function extractDurationHoursFromText(...texts) {
543
+ for (const text of texts) {
544
+ if (typeof text !== "string" || text.trim().length === 0)
545
+ continue;
546
+ const durationMatch = /(?:expected\s+duration|duration)[^0-9]{0,24}([0-9]+(?:\.[0-9]+)?)\s*h/i.exec(text);
547
+ if (!durationMatch)
548
+ continue;
549
+ const numeric = Number(durationMatch[1]);
550
+ if (Number.isFinite(numeric) && numeric >= 0)
551
+ return numeric;
552
+ }
553
+ return null;
554
+ }
555
+ function pickStringArray(record, keys) {
556
+ for (const key of keys) {
557
+ const value = record[key];
558
+ if (Array.isArray(value)) {
559
+ const items = value
560
+ .filter((entry) => typeof entry === "string")
561
+ .map((entry) => entry.trim())
562
+ .filter(Boolean);
563
+ if (items.length > 0)
564
+ return items;
565
+ }
566
+ if (typeof value === "string") {
567
+ const items = value
568
+ .split(",")
569
+ .map((entry) => entry.trim())
570
+ .filter(Boolean);
571
+ if (items.length > 0)
572
+ return items;
573
+ }
574
+ }
575
+ return [];
576
+ }
577
+ function dedupeStrings(items) {
578
+ const seen = new Set();
579
+ const out = [];
580
+ for (const item of items) {
581
+ if (!item || seen.has(item))
582
+ continue;
583
+ seen.add(item);
584
+ out.push(item);
585
+ }
586
+ return out;
587
+ }
588
+ function normalizePriorityForEntity(record) {
589
+ const explicitPriorityNum = pickNumber(record, [
590
+ "priority_num",
591
+ "priorityNum",
592
+ "priority_number",
593
+ ]);
594
+ const priorityLabelRaw = pickString(record, ["priority", "priority_label"]);
595
+ if (explicitPriorityNum !== null) {
596
+ const clamped = clampPriority(explicitPriorityNum);
597
+ return {
598
+ priorityNum: clamped,
599
+ priorityLabel: priorityLabelRaw ?? mapPriorityNumToLabel(clamped),
600
+ };
601
+ }
602
+ if (priorityLabelRaw) {
603
+ const mapped = PRIORITY_LABEL_TO_NUM[priorityLabelRaw.toLowerCase()] ?? 60;
604
+ return {
605
+ priorityNum: mapped,
606
+ priorityLabel: priorityLabelRaw.toLowerCase(),
607
+ };
608
+ }
609
+ return {
610
+ priorityNum: 60,
611
+ priorityLabel: null,
612
+ };
613
+ }
614
+ function normalizeDependencies(record) {
615
+ const metadata = getRecordMetadata(record);
616
+ const direct = pickStringArray(record, [
617
+ "depends_on",
618
+ "dependsOn",
619
+ "dependency_ids",
620
+ "dependencyIds",
621
+ "dependencies",
622
+ ]);
623
+ const nested = pickStringArray(metadata, [
624
+ "depends_on",
625
+ "dependsOn",
626
+ "dependency_ids",
627
+ "dependencyIds",
628
+ "dependencies",
629
+ ]);
630
+ return dedupeStrings([...direct, ...nested]);
631
+ }
632
+ function normalizeAssignedAgents(record) {
633
+ const metadata = getRecordMetadata(record);
634
+ const ids = dedupeStrings([
635
+ ...pickStringArray(record, ["assigned_agent_ids", "assignedAgentIds"]),
636
+ ...pickStringArray(metadata, ["assigned_agent_ids", "assignedAgentIds"]),
637
+ ]);
638
+ const names = dedupeStrings([
639
+ ...pickStringArray(record, ["assigned_agent_names", "assignedAgentNames"]),
640
+ ...pickStringArray(metadata, ["assigned_agent_names", "assignedAgentNames"]),
641
+ ]);
642
+ const objectCandidates = [
643
+ record.assigned_agents,
644
+ record.assignedAgents,
645
+ metadata.assigned_agents,
646
+ metadata.assignedAgents,
647
+ ];
648
+ const fromObjects = [];
649
+ for (const candidate of objectCandidates) {
650
+ if (!Array.isArray(candidate))
651
+ continue;
652
+ for (const entry of candidate) {
653
+ if (!entry || typeof entry !== "object")
654
+ continue;
655
+ const item = entry;
656
+ const id = pickString(item, ["id", "agent_id", "agentId"]) ?? "";
657
+ const name = pickString(item, ["name", "agent_name", "agentName"]) ?? id;
658
+ if (!name)
659
+ continue;
660
+ fromObjects.push({
661
+ id: id || `name:${name}`,
662
+ name,
663
+ domain: pickString(item, ["domain", "role"]),
664
+ });
665
+ }
666
+ }
667
+ const merged = [...fromObjects];
668
+ if (merged.length === 0 && (names.length > 0 || ids.length > 0)) {
669
+ const maxLen = Math.max(names.length, ids.length);
670
+ for (let i = 0; i < maxLen; i += 1) {
671
+ const id = ids[i] ?? `name:${names[i] ?? `agent-${i + 1}`}`;
672
+ const name = names[i] ?? ids[i] ?? `Agent ${i + 1}`;
673
+ merged.push({ id, name, domain: null });
674
+ }
675
+ }
676
+ const seen = new Set();
677
+ const deduped = [];
678
+ for (const item of merged) {
679
+ const key = `${item.id}:${item.name}`.toLowerCase();
680
+ if (seen.has(key))
681
+ continue;
682
+ seen.add(key);
683
+ deduped.push(item);
684
+ }
685
+ return deduped;
686
+ }
687
+ function toMissionControlNode(type, entity, fallbackInitiativeId) {
688
+ const record = entity;
689
+ const metadata = getRecordMetadata(record);
690
+ const initiativeId = pickString(record, ["initiative_id", "initiativeId"]) ??
691
+ pickString(metadata, ["initiative_id", "initiativeId"]) ??
692
+ (type === "initiative" ? String(record.id ?? fallbackInitiativeId) : fallbackInitiativeId);
693
+ const workstreamId = type === "workstream"
694
+ ? String(record.id ?? "")
695
+ : pickString(record, ["workstream_id", "workstreamId"]) ??
696
+ pickString(metadata, ["workstream_id", "workstreamId"]);
697
+ const milestoneId = type === "milestone"
698
+ ? String(record.id ?? "")
699
+ : pickString(record, ["milestone_id", "milestoneId"]) ??
700
+ pickString(metadata, ["milestone_id", "milestoneId"]);
701
+ const parentIdRaw = pickString(record, ["parentId", "parent_id"]) ??
702
+ pickString(metadata, ["parentId", "parent_id"]);
703
+ const parentId = parentIdRaw ??
704
+ (type === "initiative"
705
+ ? null
706
+ : type === "workstream"
707
+ ? initiativeId
708
+ : type === "milestone"
709
+ ? workstreamId ?? initiativeId
710
+ : milestoneId ?? workstreamId ?? initiativeId);
711
+ const status = pickString(record, ["status"]) ??
712
+ (type === "task" ? "todo" : "planned");
713
+ const dueDate = toIsoString(pickString(record, ["due_date", "dueDate", "target_date", "targetDate"]));
714
+ const etaEndAt = toIsoString(pickString(record, ["eta_end_at", "etaEndAt"]));
715
+ const expectedDuration = pickNumber(record, [
716
+ "expected_duration_hours",
717
+ "expectedDurationHours",
718
+ "duration_hours",
719
+ "durationHours",
720
+ ]) ??
721
+ pickNumber(metadata, [
722
+ "expected_duration_hours",
723
+ "expectedDurationHours",
724
+ "duration_hours",
725
+ "durationHours",
726
+ ]) ??
727
+ extractDurationHoursFromText(pickString(record, ["description", "summary", "context"]), pickString(metadata, ["description", "summary", "context"])) ??
728
+ DEFAULT_DURATION_HOURS[type];
729
+ const expectedBudget = pickNumber(record, [
730
+ "expected_budget_usd",
731
+ "expectedBudgetUsd",
732
+ "budget_usd",
733
+ "budgetUsd",
734
+ ]) ??
735
+ pickNumber(metadata, [
736
+ "expected_budget_usd",
737
+ "expectedBudgetUsd",
738
+ "budget_usd",
739
+ "budgetUsd",
740
+ ]) ??
741
+ extractBudgetUsdFromText(pickString(record, ["description", "summary", "context"]), pickString(metadata, ["description", "summary", "context"])) ??
742
+ DEFAULT_BUDGET_USD[type];
743
+ const priority = normalizePriorityForEntity(record);
744
+ return {
745
+ id: String(record.id ?? ""),
746
+ type,
747
+ title: pickString(record, ["title", "name"]) ??
748
+ `${type[0].toUpperCase()}${type.slice(1)} ${String(record.id ?? "")}`,
749
+ status,
750
+ parentId: parentId ?? null,
751
+ initiativeId: initiativeId ?? null,
752
+ workstreamId: workstreamId ?? null,
753
+ milestoneId: milestoneId ?? null,
754
+ priorityNum: priority.priorityNum,
755
+ priorityLabel: priority.priorityLabel,
756
+ dependencyIds: normalizeDependencies(record),
757
+ dueDate,
758
+ etaEndAt,
759
+ expectedDurationHours: expectedDuration > 0 ? expectedDuration : DEFAULT_DURATION_HOURS[type],
760
+ expectedBudgetUsd: expectedBudget >= 0 ? expectedBudget : DEFAULT_BUDGET_USD[type],
761
+ assignedAgents: normalizeAssignedAgents(record),
762
+ updatedAt: toIsoString(pickString(record, [
763
+ "updated_at",
764
+ "updatedAt",
765
+ "created_at",
766
+ "createdAt",
767
+ ])),
768
+ };
769
+ }
770
+ function isTodoStatus(status) {
771
+ const normalized = status.toLowerCase();
772
+ return (normalized === "todo" ||
773
+ normalized === "not_started" ||
774
+ normalized === "planned" ||
775
+ normalized === "backlog" ||
776
+ normalized === "pending");
777
+ }
778
+ function isInProgressStatus(status) {
779
+ const normalized = status.toLowerCase();
780
+ return (normalized === "in_progress" ||
781
+ normalized === "active" ||
782
+ normalized === "running" ||
783
+ normalized === "queued");
784
+ }
785
+ function isDoneStatus(status) {
786
+ const normalized = status.toLowerCase();
787
+ return (normalized === "done" ||
788
+ normalized === "completed" ||
789
+ normalized === "cancelled" ||
790
+ normalized === "archived" ||
791
+ normalized === "deleted");
792
+ }
793
+ function detectCycleEdgeKeys(edges) {
794
+ const adjacency = new Map();
795
+ for (const edge of edges) {
796
+ const list = adjacency.get(edge.from) ?? [];
797
+ list.push(edge.to);
798
+ adjacency.set(edge.from, list);
799
+ }
800
+ const visiting = new Set();
801
+ const visited = new Set();
802
+ const cycleEdgeKeys = new Set();
803
+ function dfs(nodeId) {
804
+ if (visited.has(nodeId))
805
+ return;
806
+ visiting.add(nodeId);
807
+ const next = adjacency.get(nodeId) ?? [];
808
+ for (const childId of next) {
809
+ if (visiting.has(childId)) {
810
+ cycleEdgeKeys.add(`${nodeId}->${childId}`);
811
+ continue;
812
+ }
813
+ dfs(childId);
814
+ }
815
+ visiting.delete(nodeId);
816
+ visited.add(nodeId);
817
+ }
818
+ for (const key of adjacency.keys()) {
819
+ if (!visited.has(key))
820
+ dfs(key);
821
+ }
822
+ return cycleEdgeKeys;
823
+ }
824
+ async function listEntitiesSafe(client, type, filters) {
825
+ try {
826
+ const response = await client.listEntities(type, filters);
827
+ const items = Array.isArray(response.data) ? response.data : [];
828
+ return { items, warning: null };
829
+ }
830
+ catch (err) {
831
+ return {
832
+ items: [],
833
+ warning: `${type} unavailable (${safeErrorMessage(err)})`,
834
+ };
835
+ }
836
+ }
837
+ async function buildMissionControlGraph(client, initiativeId) {
838
+ const degraded = [];
839
+ const [initiativeResult, workstreamResult, milestoneResult, taskResult] = await Promise.all([
840
+ listEntitiesSafe(client, "initiative", { limit: 300 }),
841
+ listEntitiesSafe(client, "workstream", {
842
+ initiative_id: initiativeId,
843
+ limit: 500,
844
+ }),
845
+ listEntitiesSafe(client, "milestone", {
846
+ initiative_id: initiativeId,
847
+ limit: 700,
848
+ }),
849
+ listEntitiesSafe(client, "task", {
850
+ initiative_id: initiativeId,
851
+ limit: 1200,
852
+ }),
853
+ ]);
854
+ for (const warning of [
855
+ initiativeResult.warning,
856
+ workstreamResult.warning,
857
+ milestoneResult.warning,
858
+ taskResult.warning,
859
+ ]) {
860
+ if (warning)
861
+ degraded.push(warning);
862
+ }
863
+ const initiativeEntity = initiativeResult.items.find((item) => String(item.id ?? "") === initiativeId);
864
+ const initiativeNode = initiativeEntity
865
+ ? toMissionControlNode("initiative", initiativeEntity, initiativeId)
866
+ : {
867
+ id: initiativeId,
868
+ type: "initiative",
869
+ title: `Initiative ${initiativeId.slice(0, 8)}`,
870
+ status: "active",
871
+ parentId: null,
872
+ initiativeId,
873
+ workstreamId: null,
874
+ milestoneId: null,
875
+ priorityNum: 60,
876
+ priorityLabel: null,
877
+ dependencyIds: [],
878
+ dueDate: null,
879
+ etaEndAt: null,
880
+ expectedDurationHours: DEFAULT_DURATION_HOURS.initiative,
881
+ expectedBudgetUsd: DEFAULT_BUDGET_USD.initiative,
882
+ assignedAgents: [],
883
+ updatedAt: null,
884
+ };
885
+ const workstreamNodes = workstreamResult.items.map((item) => toMissionControlNode("workstream", item, initiativeId));
886
+ const milestoneNodes = milestoneResult.items.map((item) => toMissionControlNode("milestone", item, initiativeId));
887
+ const taskNodes = taskResult.items.map((item) => toMissionControlNode("task", item, initiativeId));
888
+ const nodes = [
889
+ initiativeNode,
890
+ ...workstreamNodes,
891
+ ...milestoneNodes,
892
+ ...taskNodes,
893
+ ];
894
+ const nodeMap = new Map(nodes.map((node) => [node.id, node]));
895
+ for (const node of nodes) {
896
+ const validDependencies = dedupeStrings(node.dependencyIds.filter((depId) => depId !== node.id && nodeMap.has(depId)));
897
+ node.dependencyIds = validDependencies;
898
+ }
899
+ let edges = [];
900
+ for (const node of nodes) {
901
+ if (node.type === "initiative")
902
+ continue;
903
+ for (const depId of node.dependencyIds) {
904
+ edges.push({
905
+ from: depId,
906
+ to: node.id,
907
+ kind: "depends_on",
908
+ });
909
+ }
910
+ }
911
+ edges = edges.filter((edge, index, arr) => arr.findIndex((candidate) => candidate.from === edge.from &&
912
+ candidate.to === edge.to &&
913
+ candidate.kind === edge.kind) === index);
914
+ const cyclicEdgeKeys = detectCycleEdgeKeys(edges);
915
+ if (cyclicEdgeKeys.size > 0) {
916
+ degraded.push(`Detected ${cyclicEdgeKeys.size} cyclic dependency edge(s); excluded from ETA graph.`);
917
+ edges = edges.filter((edge) => !cyclicEdgeKeys.has(`${edge.from}->${edge.to}`));
918
+ for (const node of nodes) {
919
+ node.dependencyIds = node.dependencyIds.filter((depId) => !cyclicEdgeKeys.has(`${depId}->${node.id}`));
920
+ }
921
+ }
922
+ const etaMemo = new Map();
923
+ const etaVisiting = new Set();
924
+ const computeEtaEpoch = (nodeId) => {
925
+ const node = nodeMap.get(nodeId);
926
+ if (!node)
927
+ return Date.now();
928
+ const cached = etaMemo.get(nodeId);
929
+ if (cached !== undefined)
930
+ return cached;
931
+ const parsedEtaOverride = node.etaEndAt ? Date.parse(node.etaEndAt) : Number.NaN;
932
+ if (Number.isFinite(parsedEtaOverride)) {
933
+ etaMemo.set(nodeId, parsedEtaOverride);
934
+ return parsedEtaOverride;
935
+ }
936
+ const parsedDueDate = node.dueDate ? Date.parse(node.dueDate) : Number.NaN;
937
+ if (Number.isFinite(parsedDueDate)) {
938
+ etaMemo.set(nodeId, parsedDueDate);
939
+ return parsedDueDate;
940
+ }
941
+ if (etaVisiting.has(nodeId)) {
942
+ degraded.push(`ETA cycle fallback on node ${nodeId}.`);
943
+ const fallback = Date.now();
944
+ etaMemo.set(nodeId, fallback);
945
+ return fallback;
946
+ }
947
+ etaVisiting.add(nodeId);
948
+ let dependencyMax = 0;
949
+ for (const depId of node.dependencyIds) {
950
+ dependencyMax = Math.max(dependencyMax, computeEtaEpoch(depId));
951
+ }
952
+ etaVisiting.delete(nodeId);
953
+ const durationMs = (node.expectedDurationHours > 0
954
+ ? node.expectedDurationHours
955
+ : DEFAULT_DURATION_HOURS[node.type]) * 60 * 60 * 1000;
956
+ const eta = Math.max(Date.now(), dependencyMax) + durationMs;
957
+ etaMemo.set(nodeId, eta);
958
+ return eta;
959
+ };
960
+ for (const node of nodes) {
961
+ const eta = computeEtaEpoch(node.id);
962
+ if (Number.isFinite(eta)) {
963
+ node.etaEndAt = new Date(eta).toISOString();
964
+ }
965
+ }
966
+ const taskNodesOnly = nodes.filter((node) => node.type === "task");
967
+ const hasActiveTasks = taskNodesOnly.some((node) => isInProgressStatus(node.status));
968
+ const hasTodoTasks = taskNodesOnly.some((node) => isTodoStatus(node.status));
969
+ if (initiativeNode.status.toLowerCase() === "active" &&
970
+ !hasActiveTasks &&
971
+ hasTodoTasks) {
972
+ initiativeNode.status = "paused";
973
+ }
974
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
975
+ const taskIsReady = (task) => task.dependencyIds.every((depId) => {
976
+ const dependency = nodeById.get(depId);
977
+ return dependency ? isDoneStatus(dependency.status) : true;
978
+ });
979
+ const taskHasBlockedParent = (task) => {
980
+ const milestone = task.milestoneId ? nodeById.get(task.milestoneId) ?? null : null;
981
+ const workstream = task.workstreamId ? nodeById.get(task.workstreamId) ?? null : null;
982
+ return (milestone?.status?.toLowerCase() === "blocked" ||
983
+ workstream?.status?.toLowerCase() === "blocked");
984
+ };
985
+ const recentTodos = nodes
986
+ .filter((node) => node.type === "task" && isTodoStatus(node.status))
987
+ .sort((a, b) => {
988
+ const aReady = taskIsReady(a);
989
+ const bReady = taskIsReady(b);
990
+ if (aReady !== bReady)
991
+ return aReady ? -1 : 1;
992
+ const aBlocked = taskHasBlockedParent(a);
993
+ const bBlocked = taskHasBlockedParent(b);
994
+ if (aBlocked !== bBlocked)
995
+ return aBlocked ? 1 : -1;
996
+ const priorityDelta = a.priorityNum - b.priorityNum;
997
+ if (priorityDelta !== 0)
998
+ return priorityDelta;
999
+ const aDue = a.dueDate ? Date.parse(a.dueDate) : Number.POSITIVE_INFINITY;
1000
+ const bDue = b.dueDate ? Date.parse(b.dueDate) : Number.POSITIVE_INFINITY;
1001
+ if (aDue !== bDue)
1002
+ return aDue - bDue;
1003
+ const aEta = a.etaEndAt ? Date.parse(a.etaEndAt) : Number.POSITIVE_INFINITY;
1004
+ const bEta = b.etaEndAt ? Date.parse(b.etaEndAt) : Number.POSITIVE_INFINITY;
1005
+ if (aEta !== bEta)
1006
+ return aEta - bEta;
1007
+ const aEpoch = a.updatedAt ? Date.parse(a.updatedAt) : 0;
1008
+ const bEpoch = b.updatedAt ? Date.parse(b.updatedAt) : 0;
1009
+ return aEpoch - bEpoch;
1010
+ })
1011
+ .map((node) => node.id);
1012
+ return {
1013
+ initiative: {
1014
+ id: initiativeNode.id,
1015
+ title: initiativeNode.title,
1016
+ status: initiativeNode.status,
1017
+ summary: initiativeEntity
1018
+ ? pickString(initiativeEntity, [
1019
+ "summary",
1020
+ "description",
1021
+ "context",
1022
+ ])
1023
+ : null,
1024
+ assignedAgents: initiativeNode.assignedAgents,
1025
+ },
1026
+ nodes,
1027
+ edges,
1028
+ recentTodos,
1029
+ degraded,
1030
+ };
1031
+ }
1032
+ function normalizeEntityMutationPayload(payload) {
1033
+ const next = { ...payload };
1034
+ const priorityNumRaw = pickNumber(next, ["priority_num", "priorityNum"]);
1035
+ const priorityLabelRaw = pickString(next, ["priority", "priority_label"]);
1036
+ if (priorityNumRaw !== null) {
1037
+ const clamped = clampPriority(priorityNumRaw);
1038
+ next.priority_num = clamped;
1039
+ if (!priorityLabelRaw) {
1040
+ next.priority = mapPriorityNumToLabel(clamped);
1041
+ }
1042
+ }
1043
+ else if (priorityLabelRaw) {
1044
+ next.priority_num = PRIORITY_LABEL_TO_NUM[priorityLabelRaw.toLowerCase()] ?? 60;
1045
+ next.priority = priorityLabelRaw.toLowerCase();
1046
+ }
1047
+ const dependsOnArray = pickStringArray(next, ["depends_on", "dependsOn", "dependencies"]);
1048
+ if (dependsOnArray.length > 0) {
1049
+ next.depends_on = dedupeStrings(dependsOnArray);
1050
+ }
1051
+ else if ("depends_on" in next) {
1052
+ next.depends_on = [];
1053
+ }
1054
+ const expectedDuration = pickNumber(next, [
1055
+ "expected_duration_hours",
1056
+ "expectedDurationHours",
1057
+ ]);
1058
+ if (expectedDuration !== null) {
1059
+ next.expected_duration_hours = Math.max(0, expectedDuration);
1060
+ }
1061
+ const expectedBudget = pickNumber(next, [
1062
+ "expected_budget_usd",
1063
+ "expectedBudgetUsd",
1064
+ "budget_usd",
1065
+ "budgetUsd",
1066
+ ]);
1067
+ if (expectedBudget !== null) {
1068
+ next.expected_budget_usd = Math.max(0, expectedBudget);
1069
+ }
1070
+ const etaEndAt = pickString(next, ["eta_end_at", "etaEndAt"]);
1071
+ if (etaEndAt !== null) {
1072
+ next.eta_end_at = toIsoString(etaEndAt) ?? null;
1073
+ }
1074
+ const assignedIds = pickStringArray(next, [
1075
+ "assigned_agent_ids",
1076
+ "assignedAgentIds",
1077
+ ]);
1078
+ const assignedNames = pickStringArray(next, [
1079
+ "assigned_agent_names",
1080
+ "assignedAgentNames",
1081
+ ]);
1082
+ if (assignedIds.length > 0) {
1083
+ next.assigned_agent_ids = dedupeStrings(assignedIds);
1084
+ }
1085
+ if (assignedNames.length > 0) {
1086
+ next.assigned_agent_names = dedupeStrings(assignedNames);
1087
+ }
1088
+ return next;
1089
+ }
1090
+ async function resolveAutoAssignments(input) {
1091
+ const warnings = [];
1092
+ const assignedById = new Map();
1093
+ const addAgent = (agent) => {
1094
+ const key = agent.id || `name:${agent.name}`;
1095
+ if (!assignedById.has(key))
1096
+ assignedById.set(key, agent);
1097
+ };
1098
+ let liveAgents = [];
1099
+ try {
1100
+ const data = await input.client.getLiveAgents({
1101
+ initiative: input.initiativeId,
1102
+ includeIdle: true,
1103
+ });
1104
+ liveAgents = (Array.isArray(data.agents) ? data.agents : [])
1105
+ .map((raw) => {
1106
+ if (!raw || typeof raw !== "object")
1107
+ return null;
1108
+ const record = raw;
1109
+ const id = pickString(record, ["id", "agentId"]) ?? "";
1110
+ const name = pickString(record, ["name", "agentName"]) ?? (id ? `Agent ${id}` : "");
1111
+ if (!name)
1112
+ return null;
1113
+ return {
1114
+ id: id || `name:${name}`,
1115
+ name,
1116
+ domain: pickString(record, ["domain", "role"]),
1117
+ status: pickString(record, ["status"]),
1118
+ };
1119
+ })
1120
+ .filter((item) => item !== null);
1121
+ }
1122
+ catch (err) {
1123
+ warnings.push(`live agent lookup failed (${safeErrorMessage(err)})`);
1124
+ }
1125
+ const orchestrator = liveAgents.find((agent) => /holt|orchestrator/i.test(agent.name) ||
1126
+ /orchestrator/i.test(agent.domain ?? ""));
1127
+ if (orchestrator)
1128
+ addAgent(orchestrator);
1129
+ let assignmentSource = "fallback";
1130
+ try {
1131
+ const preflight = await input.client.delegationPreflight({
1132
+ intent: `${input.title}${input.summary ? `: ${input.summary}` : ""}`,
1133
+ });
1134
+ const recommendations = preflight.data?.recommended_split ?? [];
1135
+ const recommendedDomains = dedupeStrings(recommendations
1136
+ .map((entry) => String(entry.owner_domain ?? "").trim().toLowerCase())
1137
+ .filter(Boolean));
1138
+ for (const domain of recommendedDomains) {
1139
+ const matched = liveAgents.find((agent) => (agent.domain ?? "").toLowerCase().includes(domain));
1140
+ if (matched)
1141
+ addAgent(matched);
1142
+ }
1143
+ if (recommendedDomains.length > 0) {
1144
+ assignmentSource = "orchestrator";
1145
+ }
1146
+ }
1147
+ catch (err) {
1148
+ warnings.push(`delegation preflight failed (${safeErrorMessage(err)})`);
1149
+ }
1150
+ if (assignedById.size === 0) {
1151
+ const text = `${input.title} ${input.summary ?? ""}`.toLowerCase();
1152
+ const fallbackDomains = [];
1153
+ if (/market|campaign|thread|article|tweet|copy/.test(text)) {
1154
+ fallbackDomains.push("marketing");
1155
+ }
1156
+ else if (/design|ux|ui|a11y|accessibility/.test(text)) {
1157
+ fallbackDomains.push("design");
1158
+ }
1159
+ else if (/ops|incident|runbook|reliability/.test(text)) {
1160
+ fallbackDomains.push("operations");
1161
+ }
1162
+ else if (/sales|deal|pipeline|mrr/.test(text)) {
1163
+ fallbackDomains.push("sales");
1164
+ }
1165
+ else {
1166
+ fallbackDomains.push("engineering", "product");
1167
+ }
1168
+ for (const domain of fallbackDomains) {
1169
+ const matched = liveAgents.find((agent) => (agent.domain ?? "").toLowerCase().includes(domain));
1170
+ if (matched)
1171
+ addAgent(matched);
1172
+ }
1173
+ }
1174
+ if (assignedById.size === 0 && liveAgents.length > 0) {
1175
+ addAgent(liveAgents[0]);
1176
+ warnings.push("using first available live agent as fallback");
1177
+ }
1178
+ const assignedAgents = Array.from(assignedById.values());
1179
+ const updatePayload = normalizeEntityMutationPayload({
1180
+ assigned_agent_ids: assignedAgents.map((agent) => agent.id),
1181
+ assigned_agent_names: assignedAgents.map((agent) => agent.name),
1182
+ assignment_source: assignmentSource,
1183
+ });
1184
+ let updatedEntity;
1185
+ try {
1186
+ updatedEntity = await input.client.updateEntity(input.entityType, input.entityId, updatePayload);
1187
+ }
1188
+ catch (err) {
1189
+ warnings.push(`assignment patch failed (${safeErrorMessage(err)})`);
1190
+ }
1191
+ return {
1192
+ ok: true,
1193
+ assignment_source: assignmentSource,
1194
+ assigned_agents: assignedAgents,
1195
+ warnings,
1196
+ ...(updatedEntity ? { updated_entity: updatedEntity } : {}),
1197
+ };
1198
+ }
206
1199
  // =============================================================================
207
1200
  // Factory
208
1201
  // =============================================================================
209
- export function createHttpHandler(config, client, getSnapshot) {
1202
+ export function createHttpHandler(config, client, getSnapshot, onboarding) {
210
1203
  const dashboardEnabled = config.dashboardEnabled ??
211
1204
  true;
212
1205
  return async function handler(req, res) {
@@ -229,6 +1222,119 @@ export function createHttpHandler(config, client, getSnapshot) {
229
1222
  if (url.startsWith("/orgx/api/")) {
230
1223
  const route = url.replace("/orgx/api/", "").replace(/\/+$/, "");
231
1224
  const decisionApproveMatch = route.match(/^live\/decisions\/([^/]+)\/approve$/);
1225
+ const runActionMatch = route.match(/^runs\/([^/]+)\/actions\/([^/]+)$/);
1226
+ const runCheckpointsMatch = route.match(/^runs\/([^/]+)\/checkpoints$/);
1227
+ const runCheckpointRestoreMatch = route.match(/^runs\/([^/]+)\/checkpoints\/([^/]+)\/restore$/);
1228
+ const isDelegationPreflight = route === "delegation/preflight";
1229
+ const isMissionControlAutoAssignmentRoute = route === "mission-control/assignments/auto";
1230
+ const isEntitiesRoute = route === "entities";
1231
+ const entityActionMatch = route.match(/^entities\/([^/]+)\/([^/]+)\/([^/]+)$/);
1232
+ const isOnboardingStartRoute = route === "onboarding/start";
1233
+ const isOnboardingStatusRoute = route === "onboarding/status";
1234
+ const isOnboardingManualKeyRoute = route === "onboarding/manual-key";
1235
+ const isOnboardingDisconnectRoute = route === "onboarding/disconnect";
1236
+ const isLiveActivityHeadlineRoute = route === "live/activity/headline";
1237
+ if (method === "POST" && isOnboardingStartRoute) {
1238
+ try {
1239
+ const payload = parseJsonBody(req.body);
1240
+ const started = await onboarding.startPairing({
1241
+ openclawVersion: pickString(payload, ["openclawVersion", "openclaw_version"]) ??
1242
+ undefined,
1243
+ platform: pickString(payload, ["platform"]) ?? undefined,
1244
+ deviceName: pickString(payload, ["deviceName", "device_name"]) ?? undefined,
1245
+ });
1246
+ sendJson(res, 200, {
1247
+ ok: true,
1248
+ data: {
1249
+ pairingId: started.pairingId,
1250
+ connectUrl: started.connectUrl,
1251
+ expiresAt: started.expiresAt,
1252
+ pollIntervalMs: started.pollIntervalMs,
1253
+ state: getOnboardingState(started.state),
1254
+ },
1255
+ });
1256
+ }
1257
+ catch (err) {
1258
+ sendJson(res, 400, {
1259
+ ok: false,
1260
+ error: safeErrorMessage(err),
1261
+ });
1262
+ }
1263
+ return true;
1264
+ }
1265
+ if (method === "GET" && isOnboardingStatusRoute) {
1266
+ try {
1267
+ const state = await onboarding.getStatus();
1268
+ sendJson(res, 200, {
1269
+ ok: true,
1270
+ data: getOnboardingState(state),
1271
+ });
1272
+ }
1273
+ catch (err) {
1274
+ sendJson(res, 500, {
1275
+ ok: false,
1276
+ error: safeErrorMessage(err),
1277
+ });
1278
+ }
1279
+ return true;
1280
+ }
1281
+ if (method === "POST" && isOnboardingManualKeyRoute) {
1282
+ try {
1283
+ const payload = parseJsonBody(req.body);
1284
+ const authHeader = pickHeaderString(req.headers, ["authorization"]);
1285
+ const bearerApiKey = authHeader && authHeader.toLowerCase().startsWith("bearer ")
1286
+ ? authHeader.slice("bearer ".length).trim()
1287
+ : null;
1288
+ const headerApiKey = pickHeaderString(req.headers, [
1289
+ "x-orgx-api-key",
1290
+ "x-api-key",
1291
+ ]);
1292
+ const apiKey = pickString(payload, ["apiKey", "api_key"]) ??
1293
+ headerApiKey ??
1294
+ bearerApiKey;
1295
+ if (!apiKey) {
1296
+ sendJson(res, 400, {
1297
+ ok: false,
1298
+ error: "apiKey is required",
1299
+ });
1300
+ return true;
1301
+ }
1302
+ const userId = pickString(payload, ["userId", "user_id"]) ??
1303
+ pickHeaderString(req.headers, ["x-orgx-user-id", "x-user-id"]) ??
1304
+ undefined;
1305
+ const state = await onboarding.submitManualKey({
1306
+ apiKey,
1307
+ userId,
1308
+ });
1309
+ sendJson(res, 200, {
1310
+ ok: true,
1311
+ data: getOnboardingState(state),
1312
+ });
1313
+ }
1314
+ catch (err) {
1315
+ sendJson(res, 400, {
1316
+ ok: false,
1317
+ error: safeErrorMessage(err),
1318
+ });
1319
+ }
1320
+ return true;
1321
+ }
1322
+ if (method === "POST" && isOnboardingDisconnectRoute) {
1323
+ try {
1324
+ const state = await onboarding.disconnect();
1325
+ sendJson(res, 200, {
1326
+ ok: true,
1327
+ data: getOnboardingState(state),
1328
+ });
1329
+ }
1330
+ catch (err) {
1331
+ sendJson(res, 500, {
1332
+ ok: false,
1333
+ error: safeErrorMessage(err),
1334
+ });
1335
+ }
1336
+ return true;
1337
+ }
232
1338
  if (method === "POST" &&
233
1339
  (route === "live/decisions/approve" || decisionApproveMatch)) {
234
1340
  try {
@@ -268,12 +1374,189 @@ export function createHttpHandler(config, client, getSnapshot) {
268
1374
  }
269
1375
  catch (err) {
270
1376
  sendJson(res, 500, {
271
- error: err instanceof Error ? err.message : String(err),
1377
+ error: safeErrorMessage(err),
1378
+ });
1379
+ }
1380
+ return true;
1381
+ }
1382
+ if (method === "POST" && isDelegationPreflight) {
1383
+ try {
1384
+ const payload = parseJsonBody(req.body);
1385
+ const intent = pickString(payload, ["intent"]);
1386
+ if (!intent) {
1387
+ sendJson(res, 400, { error: "intent is required" });
1388
+ return true;
1389
+ }
1390
+ const toStringArray = (value) => Array.isArray(value)
1391
+ ? value.filter((entry) => typeof entry === "string")
1392
+ : undefined;
1393
+ const data = await client.delegationPreflight({
1394
+ intent,
1395
+ acceptanceCriteria: toStringArray(payload.acceptanceCriteria),
1396
+ constraints: toStringArray(payload.constraints),
1397
+ domains: toStringArray(payload.domains),
1398
+ });
1399
+ sendJson(res, 200, data);
1400
+ }
1401
+ catch (err) {
1402
+ sendJson(res, 500, {
1403
+ error: safeErrorMessage(err),
1404
+ });
1405
+ }
1406
+ return true;
1407
+ }
1408
+ if (method === "POST" && isMissionControlAutoAssignmentRoute) {
1409
+ try {
1410
+ const payload = parseJsonBody(req.body);
1411
+ const entityId = pickString(payload, ["entity_id", "entityId"]);
1412
+ const entityType = pickString(payload, ["entity_type", "entityType"]);
1413
+ const initiativeId = pickString(payload, ["initiative_id", "initiativeId"]) ?? null;
1414
+ const title = pickString(payload, ["title", "name"]) ?? "Untitled";
1415
+ const summary = pickString(payload, ["summary", "description", "context"]) ?? null;
1416
+ if (!entityId || !entityType) {
1417
+ sendJson(res, 400, {
1418
+ ok: false,
1419
+ error: "entity_id and entity_type are required.",
1420
+ });
1421
+ return true;
1422
+ }
1423
+ const assignment = await resolveAutoAssignments({
1424
+ client,
1425
+ entityId,
1426
+ entityType,
1427
+ initiativeId,
1428
+ title,
1429
+ summary,
1430
+ });
1431
+ sendJson(res, 200, assignment);
1432
+ }
1433
+ catch (err) {
1434
+ sendJson(res, 500, {
1435
+ ok: false,
1436
+ error: safeErrorMessage(err),
1437
+ });
1438
+ }
1439
+ return true;
1440
+ }
1441
+ if (runCheckpointsMatch && method === "POST") {
1442
+ try {
1443
+ const runId = decodeURIComponent(runCheckpointsMatch[1]);
1444
+ const payload = parseJsonBody(req.body);
1445
+ const reason = pickString(payload, ["reason"]) ?? undefined;
1446
+ const rawPayload = payload.payload;
1447
+ const checkpointPayload = rawPayload && typeof rawPayload === "object" && !Array.isArray(rawPayload)
1448
+ ? rawPayload
1449
+ : undefined;
1450
+ const data = await client.createRunCheckpoint(runId, {
1451
+ reason,
1452
+ payload: checkpointPayload,
1453
+ });
1454
+ sendJson(res, 200, data);
1455
+ }
1456
+ catch (err) {
1457
+ sendJson(res, 500, {
1458
+ error: safeErrorMessage(err),
1459
+ });
1460
+ }
1461
+ return true;
1462
+ }
1463
+ if (runCheckpointRestoreMatch && method === "POST") {
1464
+ try {
1465
+ const runId = decodeURIComponent(runCheckpointRestoreMatch[1]);
1466
+ const checkpointId = decodeURIComponent(runCheckpointRestoreMatch[2]);
1467
+ const payload = parseJsonBody(req.body);
1468
+ const reason = pickString(payload, ["reason"]) ?? undefined;
1469
+ const data = await client.restoreRunCheckpoint(runId, {
1470
+ checkpointId,
1471
+ reason,
1472
+ });
1473
+ sendJson(res, 200, data);
1474
+ }
1475
+ catch (err) {
1476
+ sendJson(res, 500, {
1477
+ error: safeErrorMessage(err),
1478
+ });
1479
+ }
1480
+ return true;
1481
+ }
1482
+ if (runActionMatch && method === "POST") {
1483
+ try {
1484
+ const runId = decodeURIComponent(runActionMatch[1]);
1485
+ const action = decodeURIComponent(runActionMatch[2]);
1486
+ const payload = parseJsonBody(req.body);
1487
+ const checkpointId = pickString(payload, ["checkpointId", "checkpoint_id"]);
1488
+ const reason = pickString(payload, ["reason"]);
1489
+ const data = await client.runAction(runId, action, {
1490
+ checkpointId: checkpointId ?? undefined,
1491
+ reason: reason ?? undefined,
1492
+ });
1493
+ sendJson(res, 200, data);
1494
+ }
1495
+ catch (err) {
1496
+ sendJson(res, 500, {
1497
+ error: safeErrorMessage(err),
1498
+ });
1499
+ }
1500
+ return true;
1501
+ }
1502
+ // Entity action / delete route: POST /orgx/api/entities/{type}/{id}/{action}
1503
+ if (entityActionMatch && method === "POST") {
1504
+ try {
1505
+ const entityType = decodeURIComponent(entityActionMatch[1]);
1506
+ const entityId = decodeURIComponent(entityActionMatch[2]);
1507
+ const entityAction = decodeURIComponent(entityActionMatch[3]);
1508
+ const payload = parseJsonBody(req.body);
1509
+ if (entityAction === "delete") {
1510
+ // Delete via status update
1511
+ const entity = await client.updateEntity(entityType, entityId, {
1512
+ status: "deleted",
1513
+ });
1514
+ sendJson(res, 200, { ok: true, entity });
1515
+ }
1516
+ else {
1517
+ // Map action to status update
1518
+ const statusMap = {
1519
+ start: "in_progress",
1520
+ complete: "done",
1521
+ block: "blocked",
1522
+ unblock: "in_progress",
1523
+ pause: "paused",
1524
+ resume: "active",
1525
+ };
1526
+ const newStatus = statusMap[entityAction];
1527
+ if (!newStatus) {
1528
+ sendJson(res, 400, {
1529
+ error: `Unknown entity action: ${entityAction}`,
1530
+ });
1531
+ return true;
1532
+ }
1533
+ const entity = await client.updateEntity(entityType, entityId, {
1534
+ status: newStatus,
1535
+ ...(payload.force ? { force: true } : {}),
1536
+ });
1537
+ sendJson(res, 200, { ok: true, entity });
1538
+ }
1539
+ }
1540
+ catch (err) {
1541
+ sendJson(res, 500, {
1542
+ error: safeErrorMessage(err),
272
1543
  });
273
1544
  }
274
1545
  return true;
275
1546
  }
276
- if (method !== "GET") {
1547
+ if (method !== "GET" &&
1548
+ !(runCheckpointsMatch && method === "POST") &&
1549
+ !(runCheckpointRestoreMatch && method === "POST") &&
1550
+ !(runActionMatch && method === "POST") &&
1551
+ !(isDelegationPreflight && method === "POST") &&
1552
+ !(isMissionControlAutoAssignmentRoute && method === "POST") &&
1553
+ !(isEntitiesRoute && method === "POST") &&
1554
+ !(isEntitiesRoute && method === "PATCH") &&
1555
+ !(entityActionMatch && method === "POST") &&
1556
+ !(isOnboardingStartRoute && method === "POST") &&
1557
+ !(isOnboardingManualKeyRoute && method === "POST") &&
1558
+ !(isOnboardingDisconnectRoute && method === "POST") &&
1559
+ !(isLiveActivityHeadlineRoute && method === "POST")) {
277
1560
  res.writeHead(405, {
278
1561
  "Content-Type": "text/plain",
279
1562
  ...CORS_HEADERS,
@@ -306,8 +1589,302 @@ export function createHttpHandler(config, client, getSnapshot) {
306
1589
  sendJson(res, 200, formatInitiatives(getSnapshot()));
307
1590
  return true;
308
1591
  case "onboarding":
309
- sendJson(res, 200, getOnboardingState(config, dashboardEnabled));
1592
+ sendJson(res, 200, getOnboardingState(await onboarding.getStatus()));
1593
+ return true;
1594
+ case "mission-control/graph": {
1595
+ const initiativeId = searchParams.get("initiative_id") ??
1596
+ searchParams.get("initiativeId");
1597
+ if (!initiativeId || initiativeId.trim().length === 0) {
1598
+ sendJson(res, 400, {
1599
+ error: "Query parameter 'initiative_id' is required.",
1600
+ });
1601
+ return true;
1602
+ }
1603
+ try {
1604
+ const graph = await buildMissionControlGraph(client, initiativeId.trim());
1605
+ sendJson(res, 200, graph);
1606
+ }
1607
+ catch (err) {
1608
+ sendJson(res, 500, {
1609
+ error: safeErrorMessage(err),
1610
+ });
1611
+ }
1612
+ return true;
1613
+ }
1614
+ case "entities": {
1615
+ if (method === "POST") {
1616
+ try {
1617
+ const payload = parseJsonBody(req.body);
1618
+ const type = pickString(payload, ["type"]);
1619
+ const title = pickString(payload, ["title", "name"]);
1620
+ if (!type || !title) {
1621
+ sendJson(res, 400, {
1622
+ error: "Both 'type' and 'title' are required.",
1623
+ });
1624
+ return true;
1625
+ }
1626
+ const data = normalizeEntityMutationPayload({ ...payload, title });
1627
+ delete data.type;
1628
+ let entity = await client.createEntity(type, data);
1629
+ let autoAssignment = null;
1630
+ if (type === "initiative" || type === "workstream") {
1631
+ const entityRecord = entity;
1632
+ autoAssignment = await resolveAutoAssignments({
1633
+ client,
1634
+ entityId: String(entityRecord.id ?? ""),
1635
+ entityType: type,
1636
+ initiativeId: type === "initiative"
1637
+ ? String(entityRecord.id ?? "")
1638
+ : pickString(data, ["initiative_id", "initiativeId"]),
1639
+ title: pickString(entityRecord, ["title", "name"]) ??
1640
+ title ??
1641
+ "Untitled",
1642
+ summary: pickString(entityRecord, [
1643
+ "summary",
1644
+ "description",
1645
+ "context",
1646
+ ]) ?? null,
1647
+ });
1648
+ if (autoAssignment.updated_entity) {
1649
+ entity = autoAssignment.updated_entity;
1650
+ }
1651
+ }
1652
+ sendJson(res, 201, { ok: true, entity, auto_assignment: autoAssignment });
1653
+ }
1654
+ catch (err) {
1655
+ sendJson(res, 500, {
1656
+ error: safeErrorMessage(err),
1657
+ });
1658
+ }
1659
+ return true;
1660
+ }
1661
+ if (method === "PATCH") {
1662
+ try {
1663
+ const payload = parseJsonBody(req.body);
1664
+ const type = pickString(payload, ["type"]);
1665
+ const id = pickString(payload, ["id"]);
1666
+ if (!type || !id) {
1667
+ sendJson(res, 400, {
1668
+ error: "Both 'type' and 'id' are required for PATCH.",
1669
+ });
1670
+ return true;
1671
+ }
1672
+ const updates = { ...payload };
1673
+ delete updates.type;
1674
+ delete updates.id;
1675
+ const entity = await client.updateEntity(type, id, normalizeEntityMutationPayload(updates));
1676
+ sendJson(res, 200, { ok: true, entity });
1677
+ }
1678
+ catch (err) {
1679
+ sendJson(res, 500, {
1680
+ error: safeErrorMessage(err),
1681
+ });
1682
+ }
1683
+ return true;
1684
+ }
1685
+ try {
1686
+ const type = searchParams.get("type");
1687
+ if (!type) {
1688
+ sendJson(res, 400, {
1689
+ error: "Query parameter 'type' is required for GET /entities.",
1690
+ });
1691
+ return true;
1692
+ }
1693
+ const status = searchParams.get("status") ?? undefined;
1694
+ const initiativeId = searchParams.get("initiative_id") ?? undefined;
1695
+ const limit = searchParams.get("limit")
1696
+ ? Number(searchParams.get("limit"))
1697
+ : undefined;
1698
+ const data = await client.listEntities(type, {
1699
+ status,
1700
+ initiative_id: initiativeId,
1701
+ limit: Number.isFinite(limit) ? limit : undefined,
1702
+ });
1703
+ sendJson(res, 200, data);
1704
+ }
1705
+ catch (err) {
1706
+ sendJson(res, 500, {
1707
+ error: safeErrorMessage(err),
1708
+ });
1709
+ }
1710
+ return true;
1711
+ }
1712
+ case "live/snapshot": {
1713
+ const sessionsLimit = parsePositiveInt(searchParams.get("sessionsLimit") ?? searchParams.get("sessions_limit"), 320);
1714
+ const activityLimit = parsePositiveInt(searchParams.get("activityLimit") ?? searchParams.get("activity_limit"), 600);
1715
+ const decisionsLimit = parsePositiveInt(searchParams.get("decisionsLimit") ?? searchParams.get("decisions_limit"), 120);
1716
+ const initiative = searchParams.get("initiative");
1717
+ const run = searchParams.get("run");
1718
+ const since = searchParams.get("since");
1719
+ const decisionStatus = searchParams.get("status") ?? "pending";
1720
+ const includeIdleRaw = searchParams.get("include_idle");
1721
+ const includeIdle = includeIdleRaw === null ? undefined : includeIdleRaw !== "false";
1722
+ let localSnapshot = null;
1723
+ const ensureLocalSnapshot = async (minimumLimit) => {
1724
+ if (!localSnapshot || localSnapshot.sessions.length < minimumLimit) {
1725
+ localSnapshot = await loadLocalOpenClawSnapshot(minimumLimit);
1726
+ }
1727
+ return localSnapshot;
1728
+ };
1729
+ const settled = await Promise.allSettled([
1730
+ client.getLiveSessions({
1731
+ initiative,
1732
+ limit: sessionsLimit,
1733
+ }),
1734
+ client.getLiveActivity({
1735
+ run,
1736
+ since,
1737
+ limit: activityLimit,
1738
+ }),
1739
+ client.getHandoffs(),
1740
+ client.getLiveDecisions({
1741
+ status: decisionStatus,
1742
+ limit: decisionsLimit,
1743
+ }),
1744
+ client.getLiveAgents({
1745
+ initiative,
1746
+ includeIdle,
1747
+ }),
1748
+ ]);
1749
+ const degraded = [];
1750
+ // sessions
1751
+ let sessions = {
1752
+ nodes: [],
1753
+ edges: [],
1754
+ groups: [],
1755
+ };
1756
+ const sessionsResult = settled[0];
1757
+ if (sessionsResult.status === "fulfilled") {
1758
+ sessions = sessionsResult.value;
1759
+ }
1760
+ else {
1761
+ degraded.push(`sessions unavailable (${safeErrorMessage(sessionsResult.reason)})`);
1762
+ try {
1763
+ let local = toLocalSessionTree(await ensureLocalSnapshot(Math.max(sessionsLimit, 200)), sessionsLimit);
1764
+ if (initiative && initiative.trim().length > 0) {
1765
+ const filteredNodes = local.nodes.filter((node) => node.initiativeId === initiative || node.groupId === initiative);
1766
+ const filteredIds = new Set(filteredNodes.map((node) => node.id));
1767
+ const filteredGroupIds = new Set(filteredNodes.map((node) => node.groupId));
1768
+ local = {
1769
+ nodes: filteredNodes,
1770
+ edges: local.edges.filter((edge) => filteredIds.has(edge.parentId) && filteredIds.has(edge.childId)),
1771
+ groups: local.groups.filter((group) => filteredGroupIds.has(group.id)),
1772
+ };
1773
+ }
1774
+ sessions = local;
1775
+ }
1776
+ catch (localErr) {
1777
+ degraded.push(`sessions local fallback failed (${safeErrorMessage(localErr)})`);
1778
+ }
1779
+ }
1780
+ // activity
1781
+ let activity = [];
1782
+ const activityResult = settled[1];
1783
+ if (activityResult.status === "fulfilled") {
1784
+ activity = Array.isArray(activityResult.value.activities)
1785
+ ? activityResult.value.activities
1786
+ : [];
1787
+ }
1788
+ else {
1789
+ degraded.push(`activity unavailable (${safeErrorMessage(activityResult.reason)})`);
1790
+ try {
1791
+ const local = await toLocalLiveActivity(await ensureLocalSnapshot(Math.max(activityLimit, 240)), Math.max(activityLimit, 240));
1792
+ let filtered = local.activities;
1793
+ if (run && run.trim().length > 0) {
1794
+ filtered = filtered.filter((item) => item.runId === run);
1795
+ }
1796
+ if (since && since.trim().length > 0) {
1797
+ const sinceEpoch = Date.parse(since);
1798
+ if (Number.isFinite(sinceEpoch)) {
1799
+ filtered = filtered.filter((item) => Date.parse(item.timestamp) >= sinceEpoch);
1800
+ }
1801
+ }
1802
+ activity = filtered.slice(0, activityLimit);
1803
+ }
1804
+ catch (localErr) {
1805
+ degraded.push(`activity local fallback failed (${safeErrorMessage(localErr)})`);
1806
+ }
1807
+ }
1808
+ // handoffs
1809
+ let handoffs = [];
1810
+ const handoffsResult = settled[2];
1811
+ if (handoffsResult.status === "fulfilled") {
1812
+ handoffs = Array.isArray(handoffsResult.value.handoffs)
1813
+ ? handoffsResult.value.handoffs
1814
+ : [];
1815
+ }
1816
+ else {
1817
+ degraded.push(`handoffs unavailable (${safeErrorMessage(handoffsResult.reason)})`);
1818
+ }
1819
+ // decisions
1820
+ let decisions = [];
1821
+ const decisionsResult = settled[3];
1822
+ if (decisionsResult.status === "fulfilled") {
1823
+ decisions = decisionsResult.value.decisions
1824
+ .map(mapDecisionEntity)
1825
+ .sort((a, b) => b.waitingMinutes - a.waitingMinutes);
1826
+ }
1827
+ else {
1828
+ degraded.push(`decisions unavailable (${safeErrorMessage(decisionsResult.reason)})`);
1829
+ }
1830
+ // agents
1831
+ let agents = [];
1832
+ const agentsResult = settled[4];
1833
+ if (agentsResult.status === "fulfilled") {
1834
+ agents = Array.isArray(agentsResult.value.agents)
1835
+ ? agentsResult.value.agents
1836
+ : [];
1837
+ }
1838
+ else {
1839
+ degraded.push(`agents unavailable (${safeErrorMessage(agentsResult.reason)})`);
1840
+ try {
1841
+ const local = toLocalLiveAgents(await ensureLocalSnapshot(Math.max(sessionsLimit, 240)));
1842
+ let localAgents = local.agents;
1843
+ if (initiative && initiative.trim().length > 0) {
1844
+ localAgents = localAgents.filter((agent) => agent.initiativeId === initiative);
1845
+ }
1846
+ if (includeIdle === false) {
1847
+ localAgents = localAgents.filter((agent) => agent.status !== "idle");
1848
+ }
1849
+ agents = localAgents;
1850
+ }
1851
+ catch (localErr) {
1852
+ degraded.push(`agents local fallback failed (${safeErrorMessage(localErr)})`);
1853
+ }
1854
+ }
1855
+ // include locally buffered events so offline-generated actions are visible
1856
+ try {
1857
+ const buffered = await readAllOutboxItems();
1858
+ if (buffered.length > 0) {
1859
+ const merged = [...activity, ...buffered]
1860
+ .sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp))
1861
+ .slice(0, activityLimit);
1862
+ const deduped = [];
1863
+ const seen = new Set();
1864
+ for (const item of merged) {
1865
+ if (seen.has(item.id))
1866
+ continue;
1867
+ seen.add(item.id);
1868
+ deduped.push(item);
1869
+ }
1870
+ activity = deduped;
1871
+ }
1872
+ }
1873
+ catch (err) {
1874
+ degraded.push(`outbox unavailable (${safeErrorMessage(err)})`);
1875
+ }
1876
+ sendJson(res, 200, {
1877
+ sessions,
1878
+ activity,
1879
+ handoffs,
1880
+ decisions,
1881
+ agents,
1882
+ generatedAt: new Date().toISOString(),
1883
+ degraded: degraded.length > 0 ? degraded : undefined,
1884
+ });
310
1885
  return true;
1886
+ }
1887
+ // Legacy endpoints retained for backwards compatibility.
311
1888
  case "live/sessions": {
312
1889
  try {
313
1890
  const initiative = searchParams.get("initiative");
@@ -321,9 +1898,31 @@ export function createHttpHandler(config, client, getSnapshot) {
321
1898
  sendJson(res, 200, data);
322
1899
  }
323
1900
  catch (err) {
324
- sendJson(res, 500, {
325
- error: err instanceof Error ? err.message : String(err),
326
- });
1901
+ try {
1902
+ const initiative = searchParams.get("initiative");
1903
+ const limitRaw = searchParams.get("limit")
1904
+ ? Number(searchParams.get("limit"))
1905
+ : undefined;
1906
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Number(limitRaw)) : 100;
1907
+ let local = toLocalSessionTree(await loadLocalOpenClawSnapshot(Math.max(limit, 200)), limit);
1908
+ if (initiative && initiative.trim().length > 0) {
1909
+ const filteredNodes = local.nodes.filter((node) => node.initiativeId === initiative || node.groupId === initiative);
1910
+ const filteredIds = new Set(filteredNodes.map((node) => node.id));
1911
+ const filteredGroupIds = new Set(filteredNodes.map((node) => node.groupId));
1912
+ local = {
1913
+ nodes: filteredNodes,
1914
+ edges: local.edges.filter((edge) => filteredIds.has(edge.parentId) && filteredIds.has(edge.childId)),
1915
+ groups: local.groups.filter((group) => filteredGroupIds.has(group.id)),
1916
+ };
1917
+ }
1918
+ sendJson(res, 200, local);
1919
+ }
1920
+ catch (localErr) {
1921
+ sendJson(res, 500, {
1922
+ error: safeErrorMessage(err),
1923
+ localFallbackError: safeErrorMessage(localErr),
1924
+ });
1925
+ }
327
1926
  }
328
1927
  return true;
329
1928
  }
@@ -341,9 +1940,102 @@ export function createHttpHandler(config, client, getSnapshot) {
341
1940
  });
342
1941
  sendJson(res, 200, data);
343
1942
  }
1943
+ catch (err) {
1944
+ try {
1945
+ const run = searchParams.get("run");
1946
+ const limitRaw = searchParams.get("limit")
1947
+ ? Number(searchParams.get("limit"))
1948
+ : undefined;
1949
+ const since = searchParams.get("since");
1950
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Number(limitRaw)) : 240;
1951
+ const localSnapshot = await loadLocalOpenClawSnapshot(Math.max(limit, 240));
1952
+ let local = await toLocalLiveActivity(localSnapshot, Math.max(limit, 240));
1953
+ if (run && run.trim().length > 0) {
1954
+ local = {
1955
+ activities: local.activities.filter((item) => item.runId === run),
1956
+ total: local.activities.filter((item) => item.runId === run).length,
1957
+ };
1958
+ }
1959
+ if (since && since.trim().length > 0) {
1960
+ const sinceEpoch = Date.parse(since);
1961
+ if (Number.isFinite(sinceEpoch)) {
1962
+ const filtered = local.activities.filter((item) => Date.parse(item.timestamp) >= sinceEpoch);
1963
+ local = {
1964
+ activities: filtered,
1965
+ total: filtered.length,
1966
+ };
1967
+ }
1968
+ }
1969
+ sendJson(res, 200, {
1970
+ activities: local.activities.slice(0, limit),
1971
+ total: local.total,
1972
+ });
1973
+ }
1974
+ catch (localErr) {
1975
+ sendJson(res, 500, {
1976
+ error: safeErrorMessage(err),
1977
+ localFallbackError: safeErrorMessage(localErr),
1978
+ });
1979
+ }
1980
+ }
1981
+ return true;
1982
+ }
1983
+ case "live/activity/detail": {
1984
+ const turnId = searchParams.get("turnId") ?? searchParams.get("turn_id");
1985
+ const sessionKey = searchParams.get("sessionKey") ?? searchParams.get("session_key");
1986
+ const run = searchParams.get("run");
1987
+ if (!turnId || turnId.trim().length === 0) {
1988
+ sendJson(res, 400, { error: "turnId is required" });
1989
+ return true;
1990
+ }
1991
+ try {
1992
+ const detail = await loadLocalTurnDetail({
1993
+ turnId,
1994
+ sessionKey,
1995
+ runId: run,
1996
+ });
1997
+ if (!detail) {
1998
+ sendJson(res, 404, {
1999
+ error: "Turn detail unavailable",
2000
+ turnId,
2001
+ });
2002
+ return true;
2003
+ }
2004
+ sendJson(res, 200, { detail });
2005
+ }
2006
+ catch (err) {
2007
+ sendJson(res, 500, { error: safeErrorMessage(err), turnId });
2008
+ }
2009
+ return true;
2010
+ }
2011
+ case "live/activity/headline": {
2012
+ if (method !== "POST") {
2013
+ sendJson(res, 405, { error: "Use POST /orgx/api/live/activity/headline" });
2014
+ return true;
2015
+ }
2016
+ try {
2017
+ const payload = parseJsonBody(req.body);
2018
+ const text = pickString(payload, ["text", "summary", "detail", "content"]);
2019
+ if (!text) {
2020
+ sendJson(res, 400, { error: "text is required" });
2021
+ return true;
2022
+ }
2023
+ const title = pickString(payload, ["title", "name"]);
2024
+ const type = pickString(payload, ["type", "kind"]);
2025
+ const result = await summarizeActivityHeadline({
2026
+ text,
2027
+ title,
2028
+ type,
2029
+ });
2030
+ sendJson(res, 200, {
2031
+ headline: result.headline,
2032
+ source: result.source,
2033
+ model: result.model,
2034
+ });
2035
+ }
344
2036
  catch (err) {
345
2037
  sendJson(res, 500, {
346
- error: err instanceof Error ? err.message : String(err),
2038
+ error: safeErrorMessage(err),
347
2039
  });
348
2040
  }
349
2041
  return true;
@@ -360,9 +2052,31 @@ export function createHttpHandler(config, client, getSnapshot) {
360
2052
  sendJson(res, 200, data);
361
2053
  }
362
2054
  catch (err) {
363
- sendJson(res, 500, {
364
- error: err instanceof Error ? err.message : String(err),
365
- });
2055
+ try {
2056
+ const initiative = searchParams.get("initiative");
2057
+ const includeIdleRaw = searchParams.get("include_idle");
2058
+ const includeIdle = includeIdleRaw === null ? undefined : includeIdleRaw !== "false";
2059
+ const localSnapshot = await loadLocalOpenClawSnapshot(240);
2060
+ const local = toLocalLiveAgents(localSnapshot);
2061
+ let agents = local.agents;
2062
+ if (initiative && initiative.trim().length > 0) {
2063
+ agents = agents.filter((agent) => agent.initiativeId === initiative);
2064
+ }
2065
+ if (includeIdle === false) {
2066
+ agents = agents.filter((agent) => agent.status !== "idle");
2067
+ }
2068
+ const summary = agents.reduce((acc, agent) => {
2069
+ acc[agent.status] = (acc[agent.status] ?? 0) + 1;
2070
+ return acc;
2071
+ }, {});
2072
+ sendJson(res, 200, { agents, summary });
2073
+ }
2074
+ catch (localErr) {
2075
+ sendJson(res, 500, {
2076
+ error: safeErrorMessage(err),
2077
+ localFallbackError: safeErrorMessage(localErr),
2078
+ });
2079
+ }
366
2080
  }
367
2081
  return true;
368
2082
  }
@@ -379,9 +2093,28 @@ export function createHttpHandler(config, client, getSnapshot) {
379
2093
  sendJson(res, 200, data);
380
2094
  }
381
2095
  catch (err) {
382
- sendJson(res, 500, {
383
- error: err instanceof Error ? err.message : String(err),
384
- });
2096
+ try {
2097
+ const id = searchParams.get("id");
2098
+ const limitRaw = searchParams.get("limit")
2099
+ ? Number(searchParams.get("limit"))
2100
+ : undefined;
2101
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Number(limitRaw)) : 100;
2102
+ const local = toLocalLiveInitiatives(await loadLocalOpenClawSnapshot(240));
2103
+ let initiatives = local.initiatives;
2104
+ if (id && id.trim().length > 0) {
2105
+ initiatives = initiatives.filter((item) => item.id === id);
2106
+ }
2107
+ sendJson(res, 200, {
2108
+ initiatives: initiatives.slice(0, limit),
2109
+ total: initiatives.length,
2110
+ });
2111
+ }
2112
+ catch (localErr) {
2113
+ sendJson(res, 500, {
2114
+ error: safeErrorMessage(err),
2115
+ localFallbackError: safeErrorMessage(localErr),
2116
+ });
2117
+ }
385
2118
  }
386
2119
  return true;
387
2120
  }
@@ -403,9 +2136,10 @@ export function createHttpHandler(config, client, getSnapshot) {
403
2136
  total: data.total,
404
2137
  });
405
2138
  }
406
- catch (err) {
407
- sendJson(res, 500, {
408
- error: err instanceof Error ? err.message : String(err),
2139
+ catch {
2140
+ sendJson(res, 200, {
2141
+ decisions: [],
2142
+ total: 0,
409
2143
  });
410
2144
  }
411
2145
  return true;
@@ -415,10 +2149,8 @@ export function createHttpHandler(config, client, getSnapshot) {
415
2149
  const data = await client.getHandoffs();
416
2150
  sendJson(res, 200, data);
417
2151
  }
418
- catch (err) {
419
- sendJson(res, 500, {
420
- error: err instanceof Error ? err.message : String(err),
421
- });
2152
+ catch {
2153
+ sendJson(res, 200, { handoffs: [] });
422
2154
  }
423
2155
  return true;
424
2156
  }
@@ -429,6 +2161,36 @@ export function createHttpHandler(config, client, getSnapshot) {
429
2161
  return true;
430
2162
  }
431
2163
  const target = `${config.baseUrl.replace(/\/+$/, "")}/api/client/live/stream${queryString ? `?${queryString}` : ""}`;
2164
+ const streamAbortController = new AbortController();
2165
+ let reader = null;
2166
+ let closed = false;
2167
+ let streamOpened = false;
2168
+ let idleTimer = null;
2169
+ const clearIdleTimer = () => {
2170
+ if (idleTimer) {
2171
+ clearTimeout(idleTimer);
2172
+ idleTimer = null;
2173
+ }
2174
+ };
2175
+ const closeStream = () => {
2176
+ if (closed)
2177
+ return;
2178
+ closed = true;
2179
+ clearIdleTimer();
2180
+ streamAbortController.abort();
2181
+ if (reader) {
2182
+ void reader.cancel().catch(() => undefined);
2183
+ }
2184
+ if (streamOpened && !res.writableEnded) {
2185
+ res.end();
2186
+ }
2187
+ };
2188
+ const resetIdleTimer = () => {
2189
+ clearIdleTimer();
2190
+ idleTimer = setTimeout(() => {
2191
+ closeStream();
2192
+ }, STREAM_IDLE_TIMEOUT_MS);
2193
+ };
432
2194
  try {
433
2195
  const upstream = await fetch(target, {
434
2196
  method: "GET",
@@ -439,6 +2201,7 @@ export function createHttpHandler(config, client, getSnapshot) {
439
2201
  ? { "X-Orgx-User-Id": config.userId }
440
2202
  : {}),
441
2203
  },
2204
+ signal: streamAbortController.signal,
442
2205
  });
443
2206
  const contentType = upstream.headers.get("content-type")?.toLowerCase() ?? "";
444
2207
  if (!upstream.ok || !contentType.includes("text/event-stream")) {
@@ -459,33 +2222,84 @@ export function createHttpHandler(config, client, getSnapshot) {
459
2222
  Connection: "keep-alive",
460
2223
  ...CORS_HEADERS,
461
2224
  });
2225
+ streamOpened = true;
462
2226
  if (!upstream.body) {
463
- res.end();
2227
+ closeStream();
464
2228
  return true;
465
2229
  }
466
- const reader = upstream.body.getReader();
2230
+ req.on?.("close", closeStream);
2231
+ req.on?.("aborted", closeStream);
2232
+ res.on?.("close", closeStream);
2233
+ res.on?.("finish", closeStream);
2234
+ reader = upstream.body.getReader();
2235
+ const streamReader = reader;
2236
+ resetIdleTimer();
2237
+ const waitForDrain = async () => {
2238
+ if (typeof res.once === "function") {
2239
+ await new Promise((resolve) => {
2240
+ res.once?.("drain", () => resolve());
2241
+ });
2242
+ }
2243
+ };
467
2244
  const pump = async () => {
468
- while (true) {
469
- const { done, value } = await reader.read();
470
- if (done)
471
- break;
472
- if (value)
473
- write(Buffer.from(value));
2245
+ try {
2246
+ while (!closed) {
2247
+ const { done, value } = await streamReader.read();
2248
+ if (done)
2249
+ break;
2250
+ if (!value || value.byteLength === 0)
2251
+ continue;
2252
+ resetIdleTimer();
2253
+ const accepted = write(Buffer.from(value));
2254
+ if (accepted === false) {
2255
+ await waitForDrain();
2256
+ }
2257
+ }
2258
+ }
2259
+ catch {
2260
+ // Swallow pump errors; client disconnects are expected.
2261
+ }
2262
+ finally {
2263
+ closeStream();
474
2264
  }
475
- res.end();
476
2265
  };
477
2266
  void pump();
478
2267
  }
479
2268
  catch (err) {
480
- sendJson(res, 500, {
481
- error: err instanceof Error ? err.message : String(err),
482
- });
2269
+ closeStream();
2270
+ if (!streamOpened && !res.writableEnded) {
2271
+ sendJson(res, 500, {
2272
+ error: safeErrorMessage(err),
2273
+ });
2274
+ }
483
2275
  }
484
2276
  return true;
485
2277
  }
486
- default:
2278
+ case "delegation/preflight": {
2279
+ sendJson(res, 405, { error: "Use POST /orgx/api/delegation/preflight" });
2280
+ return true;
2281
+ }
2282
+ default: {
2283
+ if (runCheckpointsMatch) {
2284
+ try {
2285
+ const runId = decodeURIComponent(runCheckpointsMatch[1]);
2286
+ const data = await client.listRunCheckpoints(runId);
2287
+ sendJson(res, 200, data);
2288
+ }
2289
+ catch (err) {
2290
+ sendJson(res, 500, {
2291
+ error: safeErrorMessage(err),
2292
+ });
2293
+ }
2294
+ return true;
2295
+ }
2296
+ if (runActionMatch || runCheckpointRestoreMatch) {
2297
+ sendJson(res, 405, { error: "Use POST for this endpoint" });
2298
+ return true;
2299
+ }
487
2300
  sendJson(res, 404, { error: "Unknown API endpoint" });
488
2301
  return true;
2302
+ }
489
2303
  }
490
2304
  }
491
2305
  // ── Dashboard SPA + static assets ──────────────────────────────────────