@useorgx/openclaw-plugin 0.2.1 → 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 +9 -1
  17. package/dist/api.d.ts.map +1 -1
  18. package/dist/api.js +76 -14
  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 +28 -3
  29. package/dist/http-handler.d.ts.map +1 -1
  30. package/dist/http-handler.js +1686 -44
  31. package/dist/http-handler.js.map +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1294 -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 +90 -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-C_w24A8p.css +0 -1
  50. package/dashboard/dist/assets/index-DfkN5JSS.js +0 -48
@@ -17,7 +17,261 @@
17
17
  import { readFileSync, existsSync } from "node:fs";
18
18
  import { join, extname } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
+ import { homedir } from "node:os";
21
+ import { createHash } from "node:crypto";
20
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
+ }
21
275
  // =============================================================================
22
276
  // Content-Type mapping
23
277
  // =============================================================================
@@ -46,9 +300,10 @@ function contentType(filePath) {
46
300
  // =============================================================================
47
301
  const CORS_HEADERS = {
48
302
  "Access-Control-Allow-Origin": "*",
49
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
303
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
50
304
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
51
305
  };
306
+ const STREAM_IDLE_TIMEOUT_MS = 60_000;
52
307
  // =============================================================================
53
308
  // Resolve the dashboard/dist/ directory relative to this file
54
309
  // =============================================================================
@@ -141,6 +396,23 @@ function pickString(record, keys) {
141
396
  }
142
397
  return null;
143
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
+ }
144
416
  function pickNumber(record, keys) {
145
417
  for (const key of keys) {
146
418
  const value = record[key];
@@ -207,10 +479,727 @@ function mapDecisionEntity(entity) {
207
479
  metadata: record,
208
480
  };
209
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
+ }
210
1199
  // =============================================================================
211
1200
  // Factory
212
1201
  // =============================================================================
213
- export function createHttpHandler(config, client, getSnapshot) {
1202
+ export function createHttpHandler(config, client, getSnapshot, onboarding) {
214
1203
  const dashboardEnabled = config.dashboardEnabled ??
215
1204
  true;
216
1205
  return async function handler(req, res) {
@@ -237,7 +1226,115 @@ export function createHttpHandler(config, client, getSnapshot) {
237
1226
  const runCheckpointsMatch = route.match(/^runs\/([^/]+)\/checkpoints$/);
238
1227
  const runCheckpointRestoreMatch = route.match(/^runs\/([^/]+)\/checkpoints\/([^/]+)\/restore$/);
239
1228
  const isDelegationPreflight = route === "delegation/preflight";
1229
+ const isMissionControlAutoAssignmentRoute = route === "mission-control/assignments/auto";
240
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
+ }
241
1338
  if (method === "POST" &&
242
1339
  (route === "live/decisions/approve" || decisionApproveMatch)) {
243
1340
  try {
@@ -277,7 +1374,7 @@ export function createHttpHandler(config, client, getSnapshot) {
277
1374
  }
278
1375
  catch (err) {
279
1376
  sendJson(res, 500, {
280
- error: err instanceof Error ? err.message : String(err),
1377
+ error: safeErrorMessage(err),
281
1378
  });
282
1379
  }
283
1380
  return true;
@@ -303,7 +1400,40 @@ export function createHttpHandler(config, client, getSnapshot) {
303
1400
  }
304
1401
  catch (err) {
305
1402
  sendJson(res, 500, {
306
- error: err instanceof Error ? err.message : String(err),
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),
307
1437
  });
308
1438
  }
309
1439
  return true;
@@ -325,7 +1455,7 @@ export function createHttpHandler(config, client, getSnapshot) {
325
1455
  }
326
1456
  catch (err) {
327
1457
  sendJson(res, 500, {
328
- error: err instanceof Error ? err.message : String(err),
1458
+ error: safeErrorMessage(err),
329
1459
  });
330
1460
  }
331
1461
  return true;
@@ -344,7 +1474,7 @@ export function createHttpHandler(config, client, getSnapshot) {
344
1474
  }
345
1475
  catch (err) {
346
1476
  sendJson(res, 500, {
347
- error: err instanceof Error ? err.message : String(err),
1477
+ error: safeErrorMessage(err),
348
1478
  });
349
1479
  }
350
1480
  return true;
@@ -364,7 +1494,52 @@ export function createHttpHandler(config, client, getSnapshot) {
364
1494
  }
365
1495
  catch (err) {
366
1496
  sendJson(res, 500, {
367
- error: err instanceof Error ? err.message : String(err),
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),
368
1543
  });
369
1544
  }
370
1545
  return true;
@@ -374,7 +1549,14 @@ export function createHttpHandler(config, client, getSnapshot) {
374
1549
  !(runCheckpointRestoreMatch && method === "POST") &&
375
1550
  !(runActionMatch && method === "POST") &&
376
1551
  !(isDelegationPreflight && method === "POST") &&
377
- !(isEntitiesRoute && 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")) {
378
1560
  res.writeHead(405, {
379
1561
  "Content-Type": "text/plain",
380
1562
  ...CORS_HEADERS,
@@ -407,8 +1589,28 @@ export function createHttpHandler(config, client, getSnapshot) {
407
1589
  sendJson(res, 200, formatInitiatives(getSnapshot()));
408
1590
  return true;
409
1591
  case "onboarding":
410
- 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
+ }
411
1612
  return true;
1613
+ }
412
1614
  case "entities": {
413
1615
  if (method === "POST") {
414
1616
  try {
@@ -421,14 +1623,61 @@ export function createHttpHandler(config, client, getSnapshot) {
421
1623
  });
422
1624
  return true;
423
1625
  }
424
- const data = { ...payload, title };
1626
+ const data = normalizeEntityMutationPayload({ ...payload, title });
425
1627
  delete data.type;
426
- const entity = await client.createEntity(type, data);
427
- sendJson(res, 201, { ok: true, entity });
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 });
428
1653
  }
429
1654
  catch (err) {
430
1655
  sendJson(res, 500, {
431
- error: err instanceof Error ? err.message : String(err),
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),
432
1681
  });
433
1682
  }
434
1683
  return true;
@@ -442,22 +1691,200 @@ export function createHttpHandler(config, client, getSnapshot) {
442
1691
  return true;
443
1692
  }
444
1693
  const status = searchParams.get("status") ?? undefined;
1694
+ const initiativeId = searchParams.get("initiative_id") ?? undefined;
445
1695
  const limit = searchParams.get("limit")
446
1696
  ? Number(searchParams.get("limit"))
447
1697
  : undefined;
448
1698
  const data = await client.listEntities(type, {
449
1699
  status,
1700
+ initiative_id: initiativeId,
450
1701
  limit: Number.isFinite(limit) ? limit : undefined,
451
1702
  });
452
1703
  sendJson(res, 200, data);
453
1704
  }
454
1705
  catch (err) {
455
1706
  sendJson(res, 500, {
456
- error: err instanceof Error ? err.message : String(err),
1707
+ error: safeErrorMessage(err),
457
1708
  });
458
1709
  }
459
1710
  return true;
460
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
+ });
1885
+ return true;
1886
+ }
1887
+ // Legacy endpoints retained for backwards compatibility.
461
1888
  case "live/sessions": {
462
1889
  try {
463
1890
  const initiative = searchParams.get("initiative");
@@ -471,9 +1898,31 @@ export function createHttpHandler(config, client, getSnapshot) {
471
1898
  sendJson(res, 200, data);
472
1899
  }
473
1900
  catch (err) {
474
- sendJson(res, 500, {
475
- error: err instanceof Error ? err.message : String(err),
476
- });
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
+ }
477
1926
  }
478
1927
  return true;
479
1928
  }
@@ -491,9 +1940,102 @@ export function createHttpHandler(config, client, getSnapshot) {
491
1940
  });
492
1941
  sendJson(res, 200, data);
493
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
+ }
494
2036
  catch (err) {
495
2037
  sendJson(res, 500, {
496
- error: err instanceof Error ? err.message : String(err),
2038
+ error: safeErrorMessage(err),
497
2039
  });
498
2040
  }
499
2041
  return true;
@@ -510,9 +2052,31 @@ export function createHttpHandler(config, client, getSnapshot) {
510
2052
  sendJson(res, 200, data);
511
2053
  }
512
2054
  catch (err) {
513
- sendJson(res, 500, {
514
- error: err instanceof Error ? err.message : String(err),
515
- });
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
+ }
516
2080
  }
517
2081
  return true;
518
2082
  }
@@ -529,9 +2093,28 @@ export function createHttpHandler(config, client, getSnapshot) {
529
2093
  sendJson(res, 200, data);
530
2094
  }
531
2095
  catch (err) {
532
- sendJson(res, 500, {
533
- error: err instanceof Error ? err.message : String(err),
534
- });
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
+ }
535
2118
  }
536
2119
  return true;
537
2120
  }
@@ -553,9 +2136,10 @@ export function createHttpHandler(config, client, getSnapshot) {
553
2136
  total: data.total,
554
2137
  });
555
2138
  }
556
- catch (err) {
557
- sendJson(res, 500, {
558
- error: err instanceof Error ? err.message : String(err),
2139
+ catch {
2140
+ sendJson(res, 200, {
2141
+ decisions: [],
2142
+ total: 0,
559
2143
  });
560
2144
  }
561
2145
  return true;
@@ -565,10 +2149,8 @@ export function createHttpHandler(config, client, getSnapshot) {
565
2149
  const data = await client.getHandoffs();
566
2150
  sendJson(res, 200, data);
567
2151
  }
568
- catch (err) {
569
- sendJson(res, 500, {
570
- error: err instanceof Error ? err.message : String(err),
571
- });
2152
+ catch {
2153
+ sendJson(res, 200, { handoffs: [] });
572
2154
  }
573
2155
  return true;
574
2156
  }
@@ -579,6 +2161,36 @@ export function createHttpHandler(config, client, getSnapshot) {
579
2161
  return true;
580
2162
  }
581
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
+ };
582
2194
  try {
583
2195
  const upstream = await fetch(target, {
584
2196
  method: "GET",
@@ -589,6 +2201,7 @@ export function createHttpHandler(config, client, getSnapshot) {
589
2201
  ? { "X-Orgx-User-Id": config.userId }
590
2202
  : {}),
591
2203
  },
2204
+ signal: streamAbortController.signal,
592
2205
  });
593
2206
  const contentType = upstream.headers.get("content-type")?.toLowerCase() ?? "";
594
2207
  if (!upstream.ok || !contentType.includes("text/event-stream")) {
@@ -609,27 +2222,56 @@ export function createHttpHandler(config, client, getSnapshot) {
609
2222
  Connection: "keep-alive",
610
2223
  ...CORS_HEADERS,
611
2224
  });
2225
+ streamOpened = true;
612
2226
  if (!upstream.body) {
613
- res.end();
2227
+ closeStream();
614
2228
  return true;
615
2229
  }
616
- 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
+ };
617
2244
  const pump = async () => {
618
- while (true) {
619
- const { done, value } = await reader.read();
620
- if (done)
621
- break;
622
- if (value)
623
- 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();
624
2264
  }
625
- res.end();
626
2265
  };
627
2266
  void pump();
628
2267
  }
629
2268
  catch (err) {
630
- sendJson(res, 500, {
631
- error: err instanceof Error ? err.message : String(err),
632
- });
2269
+ closeStream();
2270
+ if (!streamOpened && !res.writableEnded) {
2271
+ sendJson(res, 500, {
2272
+ error: safeErrorMessage(err),
2273
+ });
2274
+ }
633
2275
  }
634
2276
  return true;
635
2277
  }
@@ -646,7 +2288,7 @@ export function createHttpHandler(config, client, getSnapshot) {
646
2288
  }
647
2289
  catch (err) {
648
2290
  sendJson(res, 500, {
649
- error: err instanceof Error ? err.message : String(err),
2291
+ error: safeErrorMessage(err),
650
2292
  });
651
2293
  }
652
2294
  return true;