@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.
- 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 +9 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +76 -14
- 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 +28 -3
- package/dist/http-handler.d.ts.map +1 -1
- package/dist/http-handler.js +1686 -44
- package/dist/http-handler.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1294 -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 +90 -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-C_w24A8p.css +0 -1
- package/dashboard/dist/assets/index-DfkN5JSS.js +0 -48
package/dist/http-handler.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
!(
|
|
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(
|
|
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
|
-
|
|
427
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
475
|
-
|
|
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:
|
|
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
|
-
|
|
514
|
-
|
|
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
|
-
|
|
533
|
-
|
|
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
|
|
557
|
-
sendJson(res,
|
|
558
|
-
|
|
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
|
|
569
|
-
sendJson(res,
|
|
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
|
-
|
|
2227
|
+
closeStream();
|
|
614
2228
|
return true;
|
|
615
2229
|
}
|
|
616
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
631
|
-
|
|
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:
|
|
2291
|
+
error: safeErrorMessage(err),
|
|
650
2292
|
});
|
|
651
2293
|
}
|
|
652
2294
|
return true;
|