@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.
- package/README.md +16 -1
- package/dashboard/dist/assets/index-BrAP-X_H.css +1 -0
- package/dashboard/dist/assets/index-cOk6qwh-.js +56 -0
- package/dashboard/dist/assets/orgx-logo-QSE5QWy4.png +0 -0
- package/dashboard/dist/brand/anthropic-mark.svg +10 -0
- package/dashboard/dist/brand/control-tower.png +0 -0
- package/dashboard/dist/brand/design-codex.png +0 -0
- package/dashboard/dist/brand/engineering-autopilot.png +0 -0
- package/dashboard/dist/brand/launch-captain.png +0 -0
- package/dashboard/dist/brand/openai-mark.svg +10 -0
- package/dashboard/dist/brand/openclaw-mark.svg +11 -0
- package/dashboard/dist/brand/orgx-logo.png +0 -0
- package/dashboard/dist/brand/pipeline-intelligence.png +0 -0
- package/dashboard/dist/brand/product-orchestrator.png +0 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/api.d.ts +51 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +105 -15
- package/dist/api.js.map +1 -1
- package/dist/auth-store.d.ts +20 -0
- package/dist/auth-store.d.ts.map +1 -0
- package/dist/auth-store.js +128 -0
- package/dist/auth-store.js.map +1 -0
- package/dist/dashboard-api.d.ts +2 -7
- package/dist/dashboard-api.d.ts.map +1 -1
- package/dist/dashboard-api.js +2 -4
- package/dist/dashboard-api.js.map +1 -1
- package/dist/http-handler.d.ts +32 -3
- package/dist/http-handler.d.ts.map +1 -1
- package/dist/http-handler.js +1849 -35
- package/dist/http-handler.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1453 -44
- package/dist/index.js.map +1 -1
- package/dist/local-openclaw.d.ts +87 -0
- package/dist/local-openclaw.d.ts.map +1 -0
- package/dist/local-openclaw.js +774 -0
- package/dist/local-openclaw.js.map +1 -0
- package/dist/openclaw.plugin.json +76 -0
- package/dist/outbox.d.ts +20 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +86 -0
- package/dist/outbox.js.map +1 -0
- package/dist/types.d.ts +165 -0
- package/dist/types.d.ts.map +1 -1
- package/openclaw.plugin.json +1 -0
- package/package.json +4 -2
- package/skills/orgx/SKILL.md +180 -0
- package/dashboard/dist/assets/index-B_ag4FNd.css +0 -1
- package/dashboard/dist/assets/index-CNJpL8Wo.js +0 -40
package/dist/http-handler.js
CHANGED
|
@@ -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:
|
|
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(
|
|
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
|
-
|
|
325
|
-
|
|
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:
|
|
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
|
-
|
|
364
|
-
|
|
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
|
-
|
|
383
|
-
|
|
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
|
|
407
|
-
sendJson(res,
|
|
408
|
-
|
|
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
|
|
419
|
-
sendJson(res,
|
|
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
|
-
|
|
2227
|
+
closeStream();
|
|
464
2228
|
return true;
|
|
465
2229
|
}
|
|
466
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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 ──────────────────────────────────────
|