@useorgx/openclaw-plugin 0.3.0 → 0.3.2
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 +48 -1
- package/dashboard/dist/assets/MissionControlView-CthHdl6R.js +1 -0
- package/dashboard/dist/assets/SessionInspector-Dq0Z5WMo.js +1 -0
- package/dashboard/dist/assets/index-CoLgC4zE.js +11 -0
- package/dashboard/dist/assets/index-jfEYE0kO.css +1 -0
- package/dashboard/dist/assets/motion-CVDprFZg.js +9 -0
- package/dashboard/dist/assets/orgx-logo-Fm0FhtnV.png +0 -0
- package/dashboard/dist/assets/react-vendor-C2t2w4r2.js +32 -0
- package/dashboard/dist/assets/tanstack-C-KIc3Wc.js +1 -0
- package/dashboard/dist/assets/vendor-C-AHK0Ly.js +9 -0
- package/dashboard/dist/index.html +6 -2
- package/dist/adapters/outbox.d.ts +8 -0
- package/dist/adapters/outbox.d.ts.map +1 -0
- package/dist/adapters/outbox.js +6 -0
- package/dist/adapters/outbox.js.map +1 -0
- package/dist/agent-context-store.d.ts +24 -0
- package/dist/agent-context-store.d.ts.map +1 -0
- package/dist/agent-context-store.js +111 -0
- package/dist/agent-context-store.js.map +1 -0
- package/dist/agent-run-store.d.ts +31 -0
- package/dist/agent-run-store.d.ts.map +1 -0
- package/dist/agent-run-store.js +159 -0
- package/dist/agent-run-store.js.map +1 -0
- package/dist/api.d.ts +4 -139
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -347
- package/dist/api.js.map +1 -1
- package/dist/auth-store.d.ts.map +1 -1
- package/dist/auth-store.js +56 -35
- package/dist/auth-store.js.map +1 -1
- package/dist/byok-store.d.ts +11 -0
- package/dist/byok-store.d.ts.map +1 -0
- package/dist/byok-store.js +98 -0
- package/dist/byok-store.js.map +1 -0
- package/dist/contracts/client.d.ts +154 -0
- package/dist/contracts/client.d.ts.map +1 -0
- package/dist/contracts/client.js +422 -0
- package/dist/contracts/client.js.map +1 -0
- package/dist/contracts/types.d.ts +430 -0
- package/dist/contracts/types.d.ts.map +1 -0
- package/dist/contracts/types.js +8 -0
- package/dist/contracts/types.js.map +1 -0
- package/dist/fs-utils.d.ts +7 -0
- package/dist/fs-utils.d.ts.map +1 -0
- package/dist/fs-utils.js +63 -0
- package/dist/fs-utils.js.map +1 -0
- package/dist/http-handler.d.ts +27 -1
- package/dist/http-handler.d.ts.map +1 -1
- package/dist/http-handler.js +2520 -92
- package/dist/http-handler.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +352 -27
- package/dist/index.js.map +1 -1
- package/dist/local-openclaw.d.ts.map +1 -1
- package/dist/local-openclaw.js +57 -15
- package/dist/local-openclaw.js.map +1 -1
- package/dist/mcp-apps/orgx-live.html +690 -0
- package/dist/openclaw.plugin.json +3 -3
- package/dist/outbox.d.ts +7 -0
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +150 -17
- package/dist/outbox.js.map +1 -1
- package/dist/paths.d.ts +23 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +50 -0
- package/dist/paths.js.map +1 -0
- package/dist/reporting/outbox-replay.d.ts +2 -0
- package/dist/reporting/outbox-replay.d.ts.map +1 -0
- package/dist/reporting/outbox-replay.js +17 -0
- package/dist/reporting/outbox-replay.js.map +1 -0
- package/dist/reporting/rollups.d.ts +21 -0
- package/dist/reporting/rollups.d.ts.map +1 -0
- package/dist/reporting/rollups.js +85 -0
- package/dist/reporting/rollups.js.map +1 -0
- package/dist/snapshot-store.d.ts +10 -0
- package/dist/snapshot-store.d.ts.map +1 -0
- package/dist/snapshot-store.js +68 -0
- package/dist/snapshot-store.js.map +1 -0
- package/dist/types.d.ts +5 -410
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -4
- package/dist/types.js.map +1 -1
- package/openclaw.plugin.json +3 -3
- package/package.json +13 -3
- package/dashboard/dist/assets/index-BrAP-X_H.css +0 -1
- package/dashboard/dist/assets/index-cOk6qwh-.js +0 -56
- package/dashboard/dist/assets/orgx-logo-QSE5QWy4.png +0 -0
package/dist/http-handler.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* /orgx/api/agents → agent states
|
|
9
9
|
* /orgx/api/activity → activity feed
|
|
10
10
|
* /orgx/api/initiatives → initiative data
|
|
11
|
+
* /orgx/api/health → plugin diagnostics + outbox/sync status
|
|
11
12
|
* /orgx/api/onboarding → onboarding / config state
|
|
12
13
|
* /orgx/api/delegation/preflight → delegation preflight
|
|
13
14
|
* /orgx/api/runs/:id/checkpoints → list/create checkpoints
|
|
@@ -15,13 +16,18 @@
|
|
|
15
16
|
* /orgx/api/runs/:id/actions/:action → run control action
|
|
16
17
|
*/
|
|
17
18
|
import { readFileSync, existsSync } from "node:fs";
|
|
18
|
-
import { join, extname } from "node:path";
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
20
19
|
import { homedir } from "node:os";
|
|
21
|
-
import {
|
|
20
|
+
import { join, extname, normalize, resolve, relative, sep } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
22
24
|
import { formatStatus, formatAgents, formatActivity, formatInitiatives, getOnboardingState, } from "./dashboard-api.js";
|
|
23
25
|
import { loadLocalOpenClawSnapshot, loadLocalTurnDetail, toLocalLiveActivity, toLocalLiveAgents, toLocalLiveInitiatives, toLocalSessionTree, } from "./local-openclaw.js";
|
|
24
|
-
import {
|
|
26
|
+
import { defaultOutboxAdapter } from "./adapters/outbox.js";
|
|
27
|
+
import { readAgentContexts, upsertAgentContext } from "./agent-context-store.js";
|
|
28
|
+
import { getAgentRun, markAgentRunStopped, readAgentRuns, upsertAgentRun, } from "./agent-run-store.js";
|
|
29
|
+
import { readByokKeys, writeByokKeys } from "./byok-store.js";
|
|
30
|
+
import { computeMilestoneRollup, computeWorkstreamRollup, } from "./reporting/rollups.js";
|
|
25
31
|
// =============================================================================
|
|
26
32
|
// Helpers
|
|
27
33
|
// =============================================================================
|
|
@@ -32,6 +38,445 @@ function safeErrorMessage(err) {
|
|
|
32
38
|
return err;
|
|
33
39
|
return "Unexpected error";
|
|
34
40
|
}
|
|
41
|
+
function isUserScopedApiKey(apiKey) {
|
|
42
|
+
return apiKey.trim().toLowerCase().startsWith("oxk_");
|
|
43
|
+
}
|
|
44
|
+
function parseJsonSafe(value) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(value);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function maskSecret(value) {
|
|
53
|
+
if (!value)
|
|
54
|
+
return null;
|
|
55
|
+
const trimmed = value.trim();
|
|
56
|
+
if (!trimmed)
|
|
57
|
+
return null;
|
|
58
|
+
if (trimmed.length <= 8)
|
|
59
|
+
return `${trimmed[0]}…${trimmed.slice(-1)}`;
|
|
60
|
+
return `${trimmed.slice(0, 4)}…${trimmed.slice(-4)}`;
|
|
61
|
+
}
|
|
62
|
+
function modelImpliesByok(model) {
|
|
63
|
+
const lower = (model ?? "").trim().toLowerCase();
|
|
64
|
+
if (!lower)
|
|
65
|
+
return false;
|
|
66
|
+
return (lower.includes("openrouter") ||
|
|
67
|
+
lower.includes("anthropic") ||
|
|
68
|
+
lower.includes("openai"));
|
|
69
|
+
}
|
|
70
|
+
async function fetchBillingStatusSafe(client) {
|
|
71
|
+
try {
|
|
72
|
+
return await client.getBillingStatus();
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function resolveByokEnvOverrides() {
|
|
79
|
+
const stored = readByokKeys();
|
|
80
|
+
const env = {};
|
|
81
|
+
const openai = stored?.openaiApiKey?.trim() ?? "";
|
|
82
|
+
const anthropic = stored?.anthropicApiKey?.trim() ?? "";
|
|
83
|
+
const openrouter = stored?.openrouterApiKey?.trim() ?? "";
|
|
84
|
+
if (openai)
|
|
85
|
+
env.OPENAI_API_KEY = openai;
|
|
86
|
+
if (anthropic)
|
|
87
|
+
env.ANTHROPIC_API_KEY = anthropic;
|
|
88
|
+
if (openrouter)
|
|
89
|
+
env.OPENROUTER_API_KEY = openrouter;
|
|
90
|
+
return env;
|
|
91
|
+
}
|
|
92
|
+
async function runCommandCollect(input) {
|
|
93
|
+
const timeoutMs = input.timeoutMs ?? 10_000;
|
|
94
|
+
return await new Promise((resolve, reject) => {
|
|
95
|
+
const child = spawn(input.command, input.args, {
|
|
96
|
+
env: input.env ? { ...process.env, ...input.env } : process.env,
|
|
97
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
98
|
+
});
|
|
99
|
+
let stdout = "";
|
|
100
|
+
let stderr = "";
|
|
101
|
+
const timer = timeoutMs
|
|
102
|
+
? setTimeout(() => {
|
|
103
|
+
try {
|
|
104
|
+
child.kill("SIGKILL");
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// best effort
|
|
108
|
+
}
|
|
109
|
+
reject(new Error(`Command timed out after ${timeoutMs}ms`));
|
|
110
|
+
}, timeoutMs)
|
|
111
|
+
: null;
|
|
112
|
+
child.stdout?.on("data", (chunk) => {
|
|
113
|
+
stdout += chunk.toString("utf8");
|
|
114
|
+
});
|
|
115
|
+
child.stderr?.on("data", (chunk) => {
|
|
116
|
+
stderr += chunk.toString("utf8");
|
|
117
|
+
});
|
|
118
|
+
child.on("error", (err) => {
|
|
119
|
+
if (timer)
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
reject(err);
|
|
122
|
+
});
|
|
123
|
+
child.on("close", (code) => {
|
|
124
|
+
if (timer)
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
resolve({ stdout, stderr, exitCode: typeof code === "number" ? code : null });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async function listOpenClawAgents() {
|
|
131
|
+
const result = await runCommandCollect({
|
|
132
|
+
command: "openclaw",
|
|
133
|
+
args: ["agents", "list", "--json"],
|
|
134
|
+
timeoutMs: 5_000,
|
|
135
|
+
env: resolveByokEnvOverrides(),
|
|
136
|
+
});
|
|
137
|
+
if (result.exitCode !== 0) {
|
|
138
|
+
throw new Error(result.stderr.trim() || "openclaw agents list failed");
|
|
139
|
+
}
|
|
140
|
+
const parsed = parseJsonSafe(result.stdout);
|
|
141
|
+
if (!Array.isArray(parsed)) {
|
|
142
|
+
throw new Error("openclaw agents list returned invalid JSON");
|
|
143
|
+
}
|
|
144
|
+
return parsed.filter((entry) => Boolean(entry && typeof entry === "object"));
|
|
145
|
+
}
|
|
146
|
+
function spawnOpenClawAgentTurn(input) {
|
|
147
|
+
const args = [
|
|
148
|
+
"agent",
|
|
149
|
+
"--agent",
|
|
150
|
+
input.agentId,
|
|
151
|
+
"--session-id",
|
|
152
|
+
input.sessionId,
|
|
153
|
+
"--message",
|
|
154
|
+
input.message,
|
|
155
|
+
];
|
|
156
|
+
if (input.thinking) {
|
|
157
|
+
args.push("--thinking", input.thinking);
|
|
158
|
+
}
|
|
159
|
+
const child = spawn("openclaw", args, {
|
|
160
|
+
env: { ...process.env, ...resolveByokEnvOverrides() },
|
|
161
|
+
stdio: "ignore",
|
|
162
|
+
detached: true,
|
|
163
|
+
});
|
|
164
|
+
child.unref();
|
|
165
|
+
return { pid: child.pid ?? null };
|
|
166
|
+
}
|
|
167
|
+
function normalizeOpenClawProvider(value) {
|
|
168
|
+
const raw = (value ?? "").trim().toLowerCase();
|
|
169
|
+
if (!raw)
|
|
170
|
+
return null;
|
|
171
|
+
if (raw === "auto")
|
|
172
|
+
return null;
|
|
173
|
+
if (raw === "claude")
|
|
174
|
+
return "anthropic";
|
|
175
|
+
if (raw === "anthropic")
|
|
176
|
+
return "anthropic";
|
|
177
|
+
if (raw === "openrouter" || raw === "open-router")
|
|
178
|
+
return "openrouter";
|
|
179
|
+
if (raw === "openai")
|
|
180
|
+
return "openai";
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
async function setOpenClawAgentModel(input) {
|
|
184
|
+
const agentId = input.agentId.trim();
|
|
185
|
+
const model = input.model.trim();
|
|
186
|
+
if (!agentId || !model) {
|
|
187
|
+
throw new Error("agentId and model are required");
|
|
188
|
+
}
|
|
189
|
+
const result = await runCommandCollect({
|
|
190
|
+
command: "openclaw",
|
|
191
|
+
args: ["models", "--agent", agentId, "set", model],
|
|
192
|
+
timeoutMs: 10_000,
|
|
193
|
+
env: resolveByokEnvOverrides(),
|
|
194
|
+
});
|
|
195
|
+
if (result.exitCode !== 0) {
|
|
196
|
+
throw new Error(result.stderr.trim() || `openclaw models set failed for ${agentId}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function listOpenClawProviderModels(input) {
|
|
200
|
+
const result = await runCommandCollect({
|
|
201
|
+
command: "openclaw",
|
|
202
|
+
args: [
|
|
203
|
+
"models",
|
|
204
|
+
"--agent",
|
|
205
|
+
input.agentId,
|
|
206
|
+
"list",
|
|
207
|
+
"--provider",
|
|
208
|
+
input.provider,
|
|
209
|
+
"--json",
|
|
210
|
+
],
|
|
211
|
+
timeoutMs: 10_000,
|
|
212
|
+
env: resolveByokEnvOverrides(),
|
|
213
|
+
});
|
|
214
|
+
if (result.exitCode !== 0) {
|
|
215
|
+
throw new Error(result.stderr.trim() || "openclaw models list failed");
|
|
216
|
+
}
|
|
217
|
+
const parsed = parseJsonSafe(result.stdout);
|
|
218
|
+
if (!parsed || typeof parsed !== "object") {
|
|
219
|
+
const trimmed = result.stdout.trim();
|
|
220
|
+
if (!trimmed || /no models found/i.test(trimmed)) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
throw new Error("openclaw models list returned invalid JSON");
|
|
224
|
+
}
|
|
225
|
+
const modelsRaw = "models" in parsed && Array.isArray(parsed.models)
|
|
226
|
+
? parsed.models
|
|
227
|
+
: [];
|
|
228
|
+
return modelsRaw
|
|
229
|
+
.map((entry) => {
|
|
230
|
+
if (!entry || typeof entry !== "object")
|
|
231
|
+
return null;
|
|
232
|
+
const row = entry;
|
|
233
|
+
const key = typeof row.key === "string" ? row.key.trim() : "";
|
|
234
|
+
const tags = Array.isArray(row.tags)
|
|
235
|
+
? row.tags.filter((t) => typeof t === "string")
|
|
236
|
+
: [];
|
|
237
|
+
if (!key)
|
|
238
|
+
return null;
|
|
239
|
+
return { key, tags };
|
|
240
|
+
})
|
|
241
|
+
.filter((entry) => Boolean(entry));
|
|
242
|
+
}
|
|
243
|
+
function pickPreferredModel(models) {
|
|
244
|
+
if (models.length === 0)
|
|
245
|
+
return null;
|
|
246
|
+
const preferred = models.find((m) => m.tags.some((t) => t === "default"));
|
|
247
|
+
return preferred?.key ?? models[0]?.key ?? null;
|
|
248
|
+
}
|
|
249
|
+
async function configureOpenClawProviderRouting(input) {
|
|
250
|
+
const requestedModel = (input.requestedModel ?? "").trim() || null;
|
|
251
|
+
// Fast path: use known aliases where possible.
|
|
252
|
+
const aliasByProvider = {
|
|
253
|
+
anthropic: "opus",
|
|
254
|
+
openrouter: "sonnet",
|
|
255
|
+
openai: null,
|
|
256
|
+
};
|
|
257
|
+
const candidate = requestedModel ?? aliasByProvider[input.provider];
|
|
258
|
+
if (candidate) {
|
|
259
|
+
try {
|
|
260
|
+
await setOpenClawAgentModel({ agentId: input.agentId, model: candidate });
|
|
261
|
+
return { provider: input.provider, model: candidate };
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Fall through to discovery-based selection.
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const models = await listOpenClawProviderModels({
|
|
268
|
+
agentId: input.agentId,
|
|
269
|
+
provider: input.provider,
|
|
270
|
+
});
|
|
271
|
+
const selected = pickPreferredModel(models);
|
|
272
|
+
if (!selected) {
|
|
273
|
+
throw new Error(`No ${input.provider} models configured for agent ${input.agentId}. Add a model in OpenClaw and retry.`);
|
|
274
|
+
}
|
|
275
|
+
await setOpenClawAgentModel({ agentId: input.agentId, model: selected });
|
|
276
|
+
return { provider: input.provider, model: selected };
|
|
277
|
+
}
|
|
278
|
+
function isPidAlive(pid) {
|
|
279
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
280
|
+
return false;
|
|
281
|
+
try {
|
|
282
|
+
process.kill(pid, 0);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async function stopDetachedProcess(pid) {
|
|
290
|
+
const alive = isPidAlive(pid);
|
|
291
|
+
if (!alive) {
|
|
292
|
+
return { stopped: true, wasRunning: false };
|
|
293
|
+
}
|
|
294
|
+
const tryKill = (signal) => {
|
|
295
|
+
try {
|
|
296
|
+
// Detached child becomes its own process group (pgid = pid) on Unix.
|
|
297
|
+
process.kill(-pid, signal);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Fall back to direct pid kill.
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
process.kill(pid, signal);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// ignore
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
tryKill("SIGTERM");
|
|
311
|
+
await new Promise((resolve) => setTimeout(resolve, 450));
|
|
312
|
+
if (isPidAlive(pid)) {
|
|
313
|
+
tryKill("SIGKILL");
|
|
314
|
+
}
|
|
315
|
+
return { stopped: !isPidAlive(pid), wasRunning: true };
|
|
316
|
+
}
|
|
317
|
+
function getScopedAgentIds(contexts) {
|
|
318
|
+
const scoped = new Set();
|
|
319
|
+
for (const [key, ctx] of Object.entries(contexts)) {
|
|
320
|
+
if (!ctx || typeof ctx !== "object")
|
|
321
|
+
continue;
|
|
322
|
+
const agentId = (ctx.agentId ?? key).trim();
|
|
323
|
+
if (!agentId)
|
|
324
|
+
continue;
|
|
325
|
+
const initiativeId = ctx.initiativeId?.trim() ?? "";
|
|
326
|
+
if (initiativeId) {
|
|
327
|
+
scoped.add(agentId);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return scoped;
|
|
331
|
+
}
|
|
332
|
+
function applyAgentContextsToSessionTree(input, contexts) {
|
|
333
|
+
if (!input || !Array.isArray(input.nodes))
|
|
334
|
+
return input;
|
|
335
|
+
const groupsById = new Map();
|
|
336
|
+
for (const group of input.groups ?? []) {
|
|
337
|
+
if (!group)
|
|
338
|
+
continue;
|
|
339
|
+
groupsById.set(group.id, {
|
|
340
|
+
id: group.id,
|
|
341
|
+
label: group.label,
|
|
342
|
+
status: group.status ?? null,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const nodes = input.nodes.map((node) => {
|
|
346
|
+
const agentId = node.agentId?.trim() ?? "";
|
|
347
|
+
if (!agentId)
|
|
348
|
+
return node;
|
|
349
|
+
const ctx = contexts[agentId];
|
|
350
|
+
const initiativeId = ctx?.initiativeId?.trim() ?? "";
|
|
351
|
+
if (!initiativeId)
|
|
352
|
+
return node;
|
|
353
|
+
const groupId = initiativeId;
|
|
354
|
+
const ctxTitle = ctx.initiativeTitle?.trim() ?? "";
|
|
355
|
+
const groupLabel = ctxTitle || node.groupLabel || initiativeId;
|
|
356
|
+
const existing = groupsById.get(groupId);
|
|
357
|
+
if (!existing) {
|
|
358
|
+
groupsById.set(groupId, {
|
|
359
|
+
id: groupId,
|
|
360
|
+
label: groupLabel,
|
|
361
|
+
status: node.status ?? null,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
else if (ctxTitle && (existing.label === groupId || existing.label.startsWith("Agent "))) {
|
|
365
|
+
groupsById.set(groupId, { ...existing, label: groupLabel });
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
...node,
|
|
369
|
+
initiativeId,
|
|
370
|
+
workstreamId: ctx.workstreamId ?? node.workstreamId ?? null,
|
|
371
|
+
groupId,
|
|
372
|
+
groupLabel,
|
|
373
|
+
};
|
|
374
|
+
});
|
|
375
|
+
// Ensure every node's group exists.
|
|
376
|
+
for (const node of nodes) {
|
|
377
|
+
if (!groupsById.has(node.groupId)) {
|
|
378
|
+
groupsById.set(node.groupId, {
|
|
379
|
+
id: node.groupId,
|
|
380
|
+
label: node.groupLabel || node.groupId,
|
|
381
|
+
status: node.status ?? null,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
...input,
|
|
387
|
+
nodes,
|
|
388
|
+
groups: Array.from(groupsById.values()),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function applyAgentContextsToActivity(input, contexts) {
|
|
392
|
+
if (!Array.isArray(input))
|
|
393
|
+
return [];
|
|
394
|
+
return input.map((item) => {
|
|
395
|
+
const agentId = item.agentId?.trim() ?? "";
|
|
396
|
+
if (!agentId)
|
|
397
|
+
return item;
|
|
398
|
+
const ctx = contexts[agentId];
|
|
399
|
+
const initiativeId = ctx?.initiativeId?.trim() ?? "";
|
|
400
|
+
if (!initiativeId)
|
|
401
|
+
return item;
|
|
402
|
+
const metadata = item.metadata && typeof item.metadata === "object"
|
|
403
|
+
? { ...item.metadata }
|
|
404
|
+
: {};
|
|
405
|
+
metadata.orgx_context = {
|
|
406
|
+
initiativeId,
|
|
407
|
+
workstreamId: ctx.workstreamId ?? null,
|
|
408
|
+
taskId: ctx.taskId ?? null,
|
|
409
|
+
updatedAt: ctx.updatedAt,
|
|
410
|
+
};
|
|
411
|
+
return {
|
|
412
|
+
...item,
|
|
413
|
+
initiativeId,
|
|
414
|
+
metadata,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
function mergeSessionTrees(base, extra) {
|
|
419
|
+
const seenNodes = new Set();
|
|
420
|
+
const nodes = [];
|
|
421
|
+
for (const node of base.nodes ?? []) {
|
|
422
|
+
seenNodes.add(node.id);
|
|
423
|
+
nodes.push(node);
|
|
424
|
+
}
|
|
425
|
+
for (const node of extra.nodes ?? []) {
|
|
426
|
+
if (seenNodes.has(node.id))
|
|
427
|
+
continue;
|
|
428
|
+
seenNodes.add(node.id);
|
|
429
|
+
nodes.push(node);
|
|
430
|
+
}
|
|
431
|
+
const seenEdges = new Set();
|
|
432
|
+
const edges = [];
|
|
433
|
+
for (const edge of base.edges ?? []) {
|
|
434
|
+
const key = `${edge.parentId}→${edge.childId}`;
|
|
435
|
+
seenEdges.add(key);
|
|
436
|
+
edges.push(edge);
|
|
437
|
+
}
|
|
438
|
+
for (const edge of extra.edges ?? []) {
|
|
439
|
+
const key = `${edge.parentId}→${edge.childId}`;
|
|
440
|
+
if (seenEdges.has(key))
|
|
441
|
+
continue;
|
|
442
|
+
seenEdges.add(key);
|
|
443
|
+
edges.push(edge);
|
|
444
|
+
}
|
|
445
|
+
const groupsById = new Map();
|
|
446
|
+
for (const group of base.groups ?? []) {
|
|
447
|
+
groupsById.set(group.id, group);
|
|
448
|
+
}
|
|
449
|
+
for (const group of extra.groups ?? []) {
|
|
450
|
+
const existing = groupsById.get(group.id);
|
|
451
|
+
if (!existing) {
|
|
452
|
+
groupsById.set(group.id, group);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const nextLabel = existing.label === existing.id && group.label && group.label !== group.id
|
|
456
|
+
? group.label
|
|
457
|
+
: existing.label;
|
|
458
|
+
groupsById.set(group.id, { ...existing, label: nextLabel });
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
nodes,
|
|
462
|
+
edges,
|
|
463
|
+
groups: Array.from(groupsById.values()),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function mergeActivities(base, extra, limit) {
|
|
467
|
+
const merged = [...(base ?? []), ...(extra ?? [])].sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
|
|
468
|
+
const deduped = [];
|
|
469
|
+
const seen = new Set();
|
|
470
|
+
for (const item of merged) {
|
|
471
|
+
if (seen.has(item.id))
|
|
472
|
+
continue;
|
|
473
|
+
seen.add(item.id);
|
|
474
|
+
deduped.push(item);
|
|
475
|
+
if (deduped.length >= limit)
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
return deduped;
|
|
479
|
+
}
|
|
35
480
|
const ACTIVITY_HEADLINE_TIMEOUT_MS = 4_000;
|
|
36
481
|
const ACTIVITY_HEADLINE_CACHE_TTL_MS = 12 * 60 * 60_000;
|
|
37
482
|
const ACTIVITY_HEADLINE_CACHE_MAX = 1_000;
|
|
@@ -78,53 +523,6 @@ function heuristicActivityHeadline(text, title) {
|
|
|
78
523
|
return cleanedTitle;
|
|
79
524
|
return "Activity update";
|
|
80
525
|
}
|
|
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
526
|
function resolveActivitySummaryApiKey() {
|
|
129
527
|
if (resolvedActivitySummaryApiKey !== undefined) {
|
|
130
528
|
return resolvedActivitySummaryApiKey;
|
|
@@ -132,9 +530,6 @@ function resolveActivitySummaryApiKey() {
|
|
|
132
530
|
const candidates = [
|
|
133
531
|
process.env.ORGX_ACTIVITY_SUMMARY_API_KEY ?? "",
|
|
134
532
|
process.env.OPENROUTER_API_KEY ?? "",
|
|
135
|
-
readDotEnvValue(/^ORGX_ACTIVITY_SUMMARY_API_KEY=["']?([^"'\n]+)["']?$/m),
|
|
136
|
-
readDotEnvValue(/^OPENROUTER_API_KEY=["']?([^"'\n]+)["']?$/m),
|
|
137
|
-
readOpenRouterApiKeyFromConfig(),
|
|
138
533
|
];
|
|
139
534
|
const key = candidates.find((candidate) => candidate.trim().length > 0)?.trim() ?? "";
|
|
140
535
|
resolvedActivitySummaryApiKey = key || null;
|
|
@@ -296,13 +691,63 @@ function contentType(filePath) {
|
|
|
296
691
|
return MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
297
692
|
}
|
|
298
693
|
// =============================================================================
|
|
299
|
-
// CORS
|
|
694
|
+
// CORS + response hardening
|
|
300
695
|
// =============================================================================
|
|
301
696
|
const CORS_HEADERS = {
|
|
302
|
-
"Access-Control-Allow-Origin": "*",
|
|
303
697
|
"Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
|
|
304
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
698
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-OrgX-Api-Key, X-API-Key, X-OrgX-User-Id",
|
|
699
|
+
Vary: "Origin",
|
|
700
|
+
};
|
|
701
|
+
const SECURITY_HEADERS = {
|
|
702
|
+
"X-Content-Type-Options": "nosniff",
|
|
703
|
+
"X-Frame-Options": "DENY",
|
|
704
|
+
"Referrer-Policy": "same-origin",
|
|
705
|
+
"Cross-Origin-Resource-Policy": "same-origin",
|
|
305
706
|
};
|
|
707
|
+
function normalizeHost(value) {
|
|
708
|
+
return value.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
709
|
+
}
|
|
710
|
+
function isLoopbackHost(hostname) {
|
|
711
|
+
const host = normalizeHost(hostname);
|
|
712
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
713
|
+
}
|
|
714
|
+
function isTrustedOrigin(origin) {
|
|
715
|
+
try {
|
|
716
|
+
const parsed = new URL(origin);
|
|
717
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
return isLoopbackHost(parsed.hostname);
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function isTrustedRequestSource(headers) {
|
|
727
|
+
const fetchSite = pickHeaderString(headers, ["sec-fetch-site"]);
|
|
728
|
+
if (fetchSite) {
|
|
729
|
+
const normalizedFetchSite = fetchSite.trim().toLowerCase();
|
|
730
|
+
if (normalizedFetchSite !== "same-origin" &&
|
|
731
|
+
normalizedFetchSite !== "same-site" &&
|
|
732
|
+
normalizedFetchSite !== "none") {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const origin = pickHeaderString(headers, ["origin"]);
|
|
737
|
+
if (origin) {
|
|
738
|
+
return isTrustedOrigin(origin);
|
|
739
|
+
}
|
|
740
|
+
const referer = pickHeaderString(headers, ["referer"]);
|
|
741
|
+
if (referer) {
|
|
742
|
+
try {
|
|
743
|
+
return isTrustedOrigin(new URL(referer).origin);
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
306
751
|
const STREAM_IDLE_TIMEOUT_MS = 60_000;
|
|
307
752
|
// =============================================================================
|
|
308
753
|
// Resolve the dashboard/dist/ directory relative to this file
|
|
@@ -310,23 +755,67 @@ const STREAM_IDLE_TIMEOUT_MS = 60_000;
|
|
|
310
755
|
const __filename = fileURLToPath(import.meta.url);
|
|
311
756
|
// src/http-handler.ts → up to plugin root → dashboard/dist
|
|
312
757
|
const DIST_DIR = join(__filename, "..", "..", "dashboard", "dist");
|
|
758
|
+
const RESOLVED_DIST_DIR = resolve(DIST_DIR);
|
|
759
|
+
const RESOLVED_DIST_ASSETS_DIR = resolve(DIST_DIR, "assets");
|
|
760
|
+
function resolveSafeDistPath(subPath) {
|
|
761
|
+
if (!subPath || subPath.includes("\0"))
|
|
762
|
+
return null;
|
|
763
|
+
const normalized = normalize(subPath).replace(/^([/\\])+/, "");
|
|
764
|
+
if (!normalized || normalized === ".")
|
|
765
|
+
return null;
|
|
766
|
+
const candidate = resolve(DIST_DIR, normalized);
|
|
767
|
+
const rel = relative(RESOLVED_DIST_DIR, candidate);
|
|
768
|
+
if (!rel || rel === "." || rel.startsWith("..") || rel.includes(`..${sep}`)) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
return candidate;
|
|
772
|
+
}
|
|
313
773
|
// =============================================================================
|
|
314
774
|
// Helpers
|
|
315
775
|
// =============================================================================
|
|
776
|
+
const IMMUTABLE_FILE_CACHE = new Map();
|
|
777
|
+
const IMMUTABLE_FILE_CACHE_MAX = 128;
|
|
316
778
|
function sendJson(res, status, data) {
|
|
317
779
|
const body = JSON.stringify(data);
|
|
318
780
|
res.writeHead(status, {
|
|
319
781
|
"Content-Type": "application/json; charset=utf-8",
|
|
782
|
+
// Avoid browser/proxy caching for live dashboards.
|
|
783
|
+
"Cache-Control": "no-store",
|
|
784
|
+
...SECURITY_HEADERS,
|
|
320
785
|
...CORS_HEADERS,
|
|
321
786
|
});
|
|
322
787
|
res.end(body);
|
|
323
788
|
}
|
|
324
789
|
function sendFile(res, filePath, cacheControl) {
|
|
325
790
|
try {
|
|
791
|
+
const shouldCacheImmutable = cacheControl.includes("immutable");
|
|
792
|
+
if (shouldCacheImmutable) {
|
|
793
|
+
const cached = IMMUTABLE_FILE_CACHE.get(filePath);
|
|
794
|
+
if (cached) {
|
|
795
|
+
res.writeHead(200, {
|
|
796
|
+
"Content-Type": cached.contentType,
|
|
797
|
+
"Cache-Control": cacheControl,
|
|
798
|
+
...SECURITY_HEADERS,
|
|
799
|
+
...CORS_HEADERS,
|
|
800
|
+
});
|
|
801
|
+
res.end(cached.content);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
326
805
|
const content = readFileSync(filePath);
|
|
806
|
+
const type = contentType(filePath);
|
|
807
|
+
if (shouldCacheImmutable) {
|
|
808
|
+
if (IMMUTABLE_FILE_CACHE.size >= IMMUTABLE_FILE_CACHE_MAX) {
|
|
809
|
+
const firstKey = IMMUTABLE_FILE_CACHE.keys().next().value;
|
|
810
|
+
if (firstKey)
|
|
811
|
+
IMMUTABLE_FILE_CACHE.delete(firstKey);
|
|
812
|
+
}
|
|
813
|
+
IMMUTABLE_FILE_CACHE.set(filePath, { content, contentType: type });
|
|
814
|
+
}
|
|
327
815
|
res.writeHead(200, {
|
|
328
|
-
"Content-Type":
|
|
816
|
+
"Content-Type": type,
|
|
329
817
|
"Cache-Control": cacheControl,
|
|
818
|
+
...SECURITY_HEADERS,
|
|
330
819
|
...CORS_HEADERS,
|
|
331
820
|
});
|
|
332
821
|
res.end(content);
|
|
@@ -338,6 +827,7 @@ function sendFile(res, filePath, cacheControl) {
|
|
|
338
827
|
function send404(res) {
|
|
339
828
|
res.writeHead(404, {
|
|
340
829
|
"Content-Type": "text/plain; charset=utf-8",
|
|
830
|
+
...SECURITY_HEADERS,
|
|
341
831
|
...CORS_HEADERS,
|
|
342
832
|
});
|
|
343
833
|
res.end("Not Found");
|
|
@@ -350,6 +840,7 @@ function sendIndexHtml(res) {
|
|
|
350
840
|
else {
|
|
351
841
|
res.writeHead(503, {
|
|
352
842
|
"Content-Type": "text/html; charset=utf-8",
|
|
843
|
+
...SECURITY_HEADERS,
|
|
353
844
|
...CORS_HEADERS,
|
|
354
845
|
});
|
|
355
846
|
res.end("<html><body><h1>Dashboard not built</h1>" +
|
|
@@ -382,11 +873,120 @@ function parseJsonBody(body) {
|
|
|
382
873
|
return {};
|
|
383
874
|
}
|
|
384
875
|
}
|
|
876
|
+
if (body instanceof Uint8Array) {
|
|
877
|
+
try {
|
|
878
|
+
const parsed = JSON.parse(Buffer.from(body).toString("utf8"));
|
|
879
|
+
return typeof parsed === "object" && parsed !== null
|
|
880
|
+
? parsed
|
|
881
|
+
: {};
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
return {};
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (body instanceof ArrayBuffer) {
|
|
888
|
+
try {
|
|
889
|
+
const parsed = JSON.parse(Buffer.from(body).toString("utf8"));
|
|
890
|
+
return typeof parsed === "object" && parsed !== null
|
|
891
|
+
? parsed
|
|
892
|
+
: {};
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
return {};
|
|
896
|
+
}
|
|
897
|
+
}
|
|
385
898
|
if (typeof body === "object") {
|
|
386
899
|
return body;
|
|
387
900
|
}
|
|
388
901
|
return {};
|
|
389
902
|
}
|
|
903
|
+
const MAX_JSON_BODY_BYTES = 1_000_000;
|
|
904
|
+
const JSON_BODY_TIMEOUT_MS = 2_000;
|
|
905
|
+
function chunkToBuffer(chunk) {
|
|
906
|
+
if (!chunk)
|
|
907
|
+
return Buffer.alloc(0);
|
|
908
|
+
if (Buffer.isBuffer(chunk))
|
|
909
|
+
return chunk;
|
|
910
|
+
if (typeof chunk === "string")
|
|
911
|
+
return Buffer.from(chunk, "utf8");
|
|
912
|
+
if (chunk instanceof Uint8Array)
|
|
913
|
+
return Buffer.from(chunk);
|
|
914
|
+
try {
|
|
915
|
+
return Buffer.from(JSON.stringify(chunk), "utf8");
|
|
916
|
+
}
|
|
917
|
+
catch {
|
|
918
|
+
return Buffer.from(String(chunk), "utf8");
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async function readRequestBodyBuffer(req) {
|
|
922
|
+
const on = req.on ? req.on.bind(req) : null;
|
|
923
|
+
if (!on)
|
|
924
|
+
return null;
|
|
925
|
+
return await new Promise((resolve) => {
|
|
926
|
+
const chunks = [];
|
|
927
|
+
let totalBytes = 0;
|
|
928
|
+
let finished = false;
|
|
929
|
+
const finish = (buffer) => {
|
|
930
|
+
if (finished)
|
|
931
|
+
return;
|
|
932
|
+
finished = true;
|
|
933
|
+
clearTimeout(timer);
|
|
934
|
+
resolve(buffer);
|
|
935
|
+
};
|
|
936
|
+
const timer = setTimeout(() => finish(null), JSON_BODY_TIMEOUT_MS);
|
|
937
|
+
on("data", (chunk) => {
|
|
938
|
+
const buf = chunkToBuffer(chunk);
|
|
939
|
+
if (buf.length === 0)
|
|
940
|
+
return;
|
|
941
|
+
totalBytes += buf.length;
|
|
942
|
+
if (totalBytes > MAX_JSON_BODY_BYTES) {
|
|
943
|
+
finish(null);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
chunks.push(buf);
|
|
947
|
+
});
|
|
948
|
+
const onDone = () => {
|
|
949
|
+
if (chunks.length === 0) {
|
|
950
|
+
finish(Buffer.alloc(0));
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
finish(Buffer.concat(chunks, totalBytes));
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
const once = (req.once ?? req.on)?.bind(req) ?? null;
|
|
957
|
+
if (once) {
|
|
958
|
+
once("end", onDone);
|
|
959
|
+
once("error", () => finish(null));
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
on("end", onDone);
|
|
963
|
+
on("error", () => finish(null));
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
async function parseJsonRequest(req) {
|
|
968
|
+
const body = req.body;
|
|
969
|
+
if (typeof body === "string" && body.length > 0) {
|
|
970
|
+
return parseJsonBody(body);
|
|
971
|
+
}
|
|
972
|
+
if (Buffer.isBuffer(body) && body.length > 0) {
|
|
973
|
+
return parseJsonBody(body);
|
|
974
|
+
}
|
|
975
|
+
if (body instanceof Uint8Array && body.byteLength > 0) {
|
|
976
|
+
return parseJsonBody(body);
|
|
977
|
+
}
|
|
978
|
+
if (body instanceof ArrayBuffer && body.byteLength > 0) {
|
|
979
|
+
return parseJsonBody(body);
|
|
980
|
+
}
|
|
981
|
+
if (body && typeof body === "object" && !Buffer.isBuffer(body)) {
|
|
982
|
+
return parseJsonBody(body);
|
|
983
|
+
}
|
|
984
|
+
const streamed = await readRequestBodyBuffer(req);
|
|
985
|
+
if (!streamed || streamed.length === 0) {
|
|
986
|
+
return {};
|
|
987
|
+
}
|
|
988
|
+
return parseJsonBody(streamed);
|
|
989
|
+
}
|
|
390
990
|
function pickString(record, keys) {
|
|
391
991
|
for (const key of keys) {
|
|
392
992
|
const value = record[key];
|
|
@@ -487,17 +1087,110 @@ function parsePositiveInt(raw, fallback) {
|
|
|
487
1087
|
return fallback;
|
|
488
1088
|
return Math.max(1, Math.floor(parsed));
|
|
489
1089
|
}
|
|
1090
|
+
function parseBooleanQuery(raw) {
|
|
1091
|
+
if (!raw)
|
|
1092
|
+
return false;
|
|
1093
|
+
const normalized = raw.trim().toLowerCase();
|
|
1094
|
+
return (normalized === "1" ||
|
|
1095
|
+
normalized === "true" ||
|
|
1096
|
+
normalized === "yes" ||
|
|
1097
|
+
normalized === "on");
|
|
1098
|
+
}
|
|
1099
|
+
function stableHash(value) {
|
|
1100
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1101
|
+
}
|
|
1102
|
+
function idempotencyKey(parts) {
|
|
1103
|
+
const raw = parts.filter((part) => typeof part === "string" && part.length > 0).join(":");
|
|
1104
|
+
const cleaned = raw.replace(/[^a-zA-Z0-9:_-]/g, "-").slice(0, 84);
|
|
1105
|
+
const suffix = stableHash(raw).slice(0, 20);
|
|
1106
|
+
return `${cleaned}:${suffix}`.slice(0, 120);
|
|
1107
|
+
}
|
|
490
1108
|
const DEFAULT_DURATION_HOURS = {
|
|
491
1109
|
initiative: 40,
|
|
492
1110
|
workstream: 16,
|
|
493
1111
|
milestone: 6,
|
|
494
1112
|
task: 2,
|
|
495
1113
|
};
|
|
1114
|
+
function readBudgetEnvNumber(name, fallback, bounds = {}) {
|
|
1115
|
+
const raw = process.env[name];
|
|
1116
|
+
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
1117
|
+
return fallback;
|
|
1118
|
+
const parsed = Number(raw);
|
|
1119
|
+
if (!Number.isFinite(parsed))
|
|
1120
|
+
return fallback;
|
|
1121
|
+
if (typeof bounds.min === "number" && parsed < bounds.min)
|
|
1122
|
+
return fallback;
|
|
1123
|
+
if (typeof bounds.max === "number" && parsed > bounds.max)
|
|
1124
|
+
return fallback;
|
|
1125
|
+
return parsed;
|
|
1126
|
+
}
|
|
1127
|
+
const DEFAULT_TOKEN_MODEL_PRICING_USD_PER_1M = {
|
|
1128
|
+
// GPT-5.3 Codex API pricing is not published yet; use GPT-5.2 Codex pricing as proxy.
|
|
1129
|
+
gpt53CodexProxy: {
|
|
1130
|
+
input: readBudgetEnvNumber("ORGX_BUDGET_GPT53_CODEX_INPUT_PER_1M", 1.75, { min: 0 }),
|
|
1131
|
+
cachedInput: readBudgetEnvNumber("ORGX_BUDGET_GPT53_CODEX_CACHED_INPUT_PER_1M", 0.175, {
|
|
1132
|
+
min: 0,
|
|
1133
|
+
}),
|
|
1134
|
+
output: readBudgetEnvNumber("ORGX_BUDGET_GPT53_CODEX_OUTPUT_PER_1M", 14, { min: 0 }),
|
|
1135
|
+
},
|
|
1136
|
+
opus46: {
|
|
1137
|
+
input: readBudgetEnvNumber("ORGX_BUDGET_OPUS46_INPUT_PER_1M", 5, { min: 0 }),
|
|
1138
|
+
// Anthropic does not publish a fixed cached-input rate on the model page.
|
|
1139
|
+
cachedInput: readBudgetEnvNumber("ORGX_BUDGET_OPUS46_CACHED_INPUT_PER_1M", 5, { min: 0 }),
|
|
1140
|
+
output: readBudgetEnvNumber("ORGX_BUDGET_OPUS46_OUTPUT_PER_1M", 25, { min: 0 }),
|
|
1141
|
+
},
|
|
1142
|
+
};
|
|
1143
|
+
const DEFAULT_TOKEN_BUDGET_ASSUMPTIONS = {
|
|
1144
|
+
tokensPerHour: readBudgetEnvNumber("ORGX_BUDGET_TOKENS_PER_HOUR", 1_200_000, { min: 1 }),
|
|
1145
|
+
inputShare: readBudgetEnvNumber("ORGX_BUDGET_INPUT_TOKEN_SHARE", 0.86, { min: 0, max: 1 }),
|
|
1146
|
+
cachedInputShare: readBudgetEnvNumber("ORGX_BUDGET_CACHED_INPUT_SHARE", 0.15, {
|
|
1147
|
+
min: 0,
|
|
1148
|
+
max: 1,
|
|
1149
|
+
}),
|
|
1150
|
+
contingencyMultiplier: readBudgetEnvNumber("ORGX_BUDGET_CONTINGENCY_MULTIPLIER", 1.3, {
|
|
1151
|
+
min: 0.1,
|
|
1152
|
+
}),
|
|
1153
|
+
roundingStepUsd: readBudgetEnvNumber("ORGX_BUDGET_ROUNDING_STEP_USD", 5, { min: 0.01 }),
|
|
1154
|
+
};
|
|
1155
|
+
const DEFAULT_TOKEN_MODEL_MIX = {
|
|
1156
|
+
gpt53CodexProxy: 0.7,
|
|
1157
|
+
opus46: 0.3,
|
|
1158
|
+
};
|
|
1159
|
+
function modelCostPerMillionTokensUsd(pricing) {
|
|
1160
|
+
const inputShare = DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.inputShare;
|
|
1161
|
+
const outputShare = Math.max(0, 1 - inputShare);
|
|
1162
|
+
const cachedShare = DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.cachedInputShare;
|
|
1163
|
+
const uncachedShare = Math.max(0, 1 - cachedShare);
|
|
1164
|
+
const effectiveInputRate = pricing.input * uncachedShare + pricing.cachedInput * cachedShare;
|
|
1165
|
+
return inputShare * effectiveInputRate + outputShare * pricing.output;
|
|
1166
|
+
}
|
|
1167
|
+
function estimateBudgetUsdFromDurationHours(durationHours) {
|
|
1168
|
+
if (!Number.isFinite(durationHours) || durationHours <= 0)
|
|
1169
|
+
return 0;
|
|
1170
|
+
const blendedPerMillionUsd = DEFAULT_TOKEN_MODEL_MIX.gpt53CodexProxy *
|
|
1171
|
+
modelCostPerMillionTokensUsd(DEFAULT_TOKEN_MODEL_PRICING_USD_PER_1M.gpt53CodexProxy) +
|
|
1172
|
+
DEFAULT_TOKEN_MODEL_MIX.opus46 *
|
|
1173
|
+
modelCostPerMillionTokensUsd(DEFAULT_TOKEN_MODEL_PRICING_USD_PER_1M.opus46);
|
|
1174
|
+
const tokenMillions = (durationHours * DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.tokensPerHour) / 1_000_000;
|
|
1175
|
+
const rawBudgetUsd = tokenMillions *
|
|
1176
|
+
blendedPerMillionUsd *
|
|
1177
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.contingencyMultiplier;
|
|
1178
|
+
const roundedBudgetUsd = Math.round(rawBudgetUsd / DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.roundingStepUsd) *
|
|
1179
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.roundingStepUsd;
|
|
1180
|
+
return Math.max(0, roundedBudgetUsd);
|
|
1181
|
+
}
|
|
1182
|
+
function isLegacyHourlyBudget(budgetUsd, durationHours) {
|
|
1183
|
+
if (!Number.isFinite(budgetUsd) || !Number.isFinite(durationHours) || durationHours <= 0) {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
const legacyHourlyBudget = durationHours * 40;
|
|
1187
|
+
return Math.abs(budgetUsd - legacyHourlyBudget) <= 0.5;
|
|
1188
|
+
}
|
|
496
1189
|
const DEFAULT_BUDGET_USD = {
|
|
497
|
-
initiative:
|
|
498
|
-
workstream:
|
|
499
|
-
milestone:
|
|
500
|
-
task:
|
|
1190
|
+
initiative: estimateBudgetUsdFromDurationHours(DEFAULT_DURATION_HOURS.initiative),
|
|
1191
|
+
workstream: estimateBudgetUsdFromDurationHours(DEFAULT_DURATION_HOURS.workstream),
|
|
1192
|
+
milestone: estimateBudgetUsdFromDurationHours(DEFAULT_DURATION_HOURS.milestone),
|
|
1193
|
+
task: estimateBudgetUsdFromDurationHours(DEFAULT_DURATION_HOURS.task),
|
|
501
1194
|
};
|
|
502
1195
|
const PRIORITY_LABEL_TO_NUM = {
|
|
503
1196
|
urgent: 10,
|
|
@@ -726,7 +1419,7 @@ function toMissionControlNode(type, entity, fallbackInitiativeId) {
|
|
|
726
1419
|
]) ??
|
|
727
1420
|
extractDurationHoursFromText(pickString(record, ["description", "summary", "context"]), pickString(metadata, ["description", "summary", "context"])) ??
|
|
728
1421
|
DEFAULT_DURATION_HOURS[type];
|
|
729
|
-
const
|
|
1422
|
+
const explicitBudget = pickNumber(record, [
|
|
730
1423
|
"expected_budget_usd",
|
|
731
1424
|
"expectedBudgetUsd",
|
|
732
1425
|
"budget_usd",
|
|
@@ -737,9 +1430,15 @@ function toMissionControlNode(type, entity, fallbackInitiativeId) {
|
|
|
737
1430
|
"expectedBudgetUsd",
|
|
738
1431
|
"budget_usd",
|
|
739
1432
|
"budgetUsd",
|
|
740
|
-
])
|
|
741
|
-
|
|
742
|
-
|
|
1433
|
+
]);
|
|
1434
|
+
const extractedBudget = extractBudgetUsdFromText(pickString(record, ["description", "summary", "context"]), pickString(metadata, ["description", "summary", "context"])) ?? null;
|
|
1435
|
+
const tokenModeledBudget = estimateBudgetUsdFromDurationHours(expectedDuration > 0 ? expectedDuration : DEFAULT_DURATION_HOURS[type]) || DEFAULT_BUDGET_USD[type];
|
|
1436
|
+
const expectedBudget = explicitBudget ??
|
|
1437
|
+
(typeof extractedBudget === "number"
|
|
1438
|
+
? isLegacyHourlyBudget(extractedBudget, expectedDuration)
|
|
1439
|
+
? tokenModeledBudget
|
|
1440
|
+
: extractedBudget
|
|
1441
|
+
: DEFAULT_BUDGET_USD[type]);
|
|
743
1442
|
const priority = normalizePriorityForEntity(record);
|
|
744
1443
|
return {
|
|
745
1444
|
id: String(record.id ?? ""),
|
|
@@ -1199,11 +1898,628 @@ async function resolveAutoAssignments(input) {
|
|
|
1199
1898
|
// =============================================================================
|
|
1200
1899
|
// Factory
|
|
1201
1900
|
// =============================================================================
|
|
1202
|
-
export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
1901
|
+
export function createHttpHandler(config, client, getSnapshot, onboarding, diagnostics, adapters) {
|
|
1203
1902
|
const dashboardEnabled = config.dashboardEnabled ??
|
|
1204
1903
|
true;
|
|
1205
|
-
|
|
1206
|
-
|
|
1904
|
+
const outboxAdapter = adapters?.outbox ?? defaultOutboxAdapter;
|
|
1905
|
+
const openclawAdapter = adapters?.openclaw ?? {};
|
|
1906
|
+
const listAgents = openclawAdapter.listAgents ?? listOpenClawAgents;
|
|
1907
|
+
const spawnAgentTurn = openclawAdapter.spawnAgentTurn ?? spawnOpenClawAgentTurn;
|
|
1908
|
+
const stopProcess = openclawAdapter.stopDetachedProcess ?? stopDetachedProcess;
|
|
1909
|
+
const pidAlive = openclawAdapter.isPidAlive ?? isPidAlive;
|
|
1910
|
+
async function emitActivitySafe(input) {
|
|
1911
|
+
const initiativeId = input.initiativeId?.trim() ?? "";
|
|
1912
|
+
if (!initiativeId)
|
|
1913
|
+
return;
|
|
1914
|
+
const message = input.message.trim();
|
|
1915
|
+
if (!message)
|
|
1916
|
+
return;
|
|
1917
|
+
try {
|
|
1918
|
+
await client.emitActivity({
|
|
1919
|
+
initiative_id: initiativeId,
|
|
1920
|
+
run_id: input.runId ?? undefined,
|
|
1921
|
+
correlation_id: input.runId
|
|
1922
|
+
? undefined
|
|
1923
|
+
: (input.correlationId?.trim() || `openclaw-${Date.now()}`),
|
|
1924
|
+
source_client: "openclaw",
|
|
1925
|
+
message,
|
|
1926
|
+
phase: input.phase,
|
|
1927
|
+
progress_pct: typeof input.progressPct === "number" && Number.isFinite(input.progressPct)
|
|
1928
|
+
? Math.max(0, Math.min(100, Math.round(input.progressPct)))
|
|
1929
|
+
: undefined,
|
|
1930
|
+
level: input.level,
|
|
1931
|
+
next_step: input.nextStep,
|
|
1932
|
+
metadata: input.metadata,
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
catch {
|
|
1936
|
+
// best effort
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
async function syncParentRollupsForTask(input) {
|
|
1940
|
+
const initiativeId = input.initiativeId?.trim() ?? "";
|
|
1941
|
+
const taskId = input.taskId?.trim() ?? "";
|
|
1942
|
+
if (!initiativeId || !taskId)
|
|
1943
|
+
return;
|
|
1944
|
+
let tasks = [];
|
|
1945
|
+
try {
|
|
1946
|
+
const response = await client.listEntities("task", {
|
|
1947
|
+
initiative_id: initiativeId,
|
|
1948
|
+
limit: 4000,
|
|
1949
|
+
});
|
|
1950
|
+
tasks = Array.isArray(response?.data)
|
|
1951
|
+
? response.data
|
|
1952
|
+
: [];
|
|
1953
|
+
}
|
|
1954
|
+
catch {
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
const task = tasks.find((row) => String(row.id ?? "").trim() === taskId) ?? null;
|
|
1958
|
+
const resolvedMilestoneId = (input.milestoneId?.trim() || "") ||
|
|
1959
|
+
(task ? pickString(task, ["milestone_id", "milestoneId"]) ?? "" : "");
|
|
1960
|
+
const resolvedWorkstreamId = (input.workstreamId?.trim() || "") ||
|
|
1961
|
+
(task ? pickString(task, ["workstream_id", "workstreamId"]) ?? "" : "");
|
|
1962
|
+
if (resolvedMilestoneId) {
|
|
1963
|
+
const milestoneTaskStatuses = tasks
|
|
1964
|
+
.filter((row) => pickString(row, ["milestone_id", "milestoneId"]) === resolvedMilestoneId)
|
|
1965
|
+
.map((row) => pickString(row, ["status"]) ?? "todo");
|
|
1966
|
+
const rollup = computeMilestoneRollup(milestoneTaskStatuses);
|
|
1967
|
+
try {
|
|
1968
|
+
await client.applyChangeset({
|
|
1969
|
+
initiative_id: initiativeId,
|
|
1970
|
+
correlation_id: input.correlationId?.trim() || undefined,
|
|
1971
|
+
source_client: "openclaw",
|
|
1972
|
+
idempotency_key: idempotencyKey([
|
|
1973
|
+
"openclaw",
|
|
1974
|
+
"rollup",
|
|
1975
|
+
"milestone",
|
|
1976
|
+
resolvedMilestoneId,
|
|
1977
|
+
rollup.status,
|
|
1978
|
+
String(rollup.progressPct),
|
|
1979
|
+
String(rollup.done),
|
|
1980
|
+
String(rollup.total),
|
|
1981
|
+
]),
|
|
1982
|
+
operations: [
|
|
1983
|
+
{
|
|
1984
|
+
op: "milestone.update",
|
|
1985
|
+
milestone_id: resolvedMilestoneId,
|
|
1986
|
+
status: rollup.status,
|
|
1987
|
+
},
|
|
1988
|
+
],
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
catch {
|
|
1992
|
+
// best effort
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
if (resolvedWorkstreamId) {
|
|
1996
|
+
const workstreamTaskStatuses = tasks
|
|
1997
|
+
.filter((row) => pickString(row, ["workstream_id", "workstreamId"]) === resolvedWorkstreamId)
|
|
1998
|
+
.map((row) => pickString(row, ["status"]) ?? "todo");
|
|
1999
|
+
const rollup = computeWorkstreamRollup(workstreamTaskStatuses);
|
|
2000
|
+
try {
|
|
2001
|
+
await client.updateEntity("workstream", resolvedWorkstreamId, {
|
|
2002
|
+
status: rollup.status,
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
catch {
|
|
2006
|
+
// best effort
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
const autoContinueRuns = new Map();
|
|
2011
|
+
let autoContinueTickInFlight = false;
|
|
2012
|
+
const AUTO_CONTINUE_TICK_MS = 2_500;
|
|
2013
|
+
function normalizeTokenBudget(value, fallback) {
|
|
2014
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2015
|
+
return Math.max(1_000, Math.round(value));
|
|
2016
|
+
}
|
|
2017
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
2018
|
+
const parsed = Number(value);
|
|
2019
|
+
if (Number.isFinite(parsed)) {
|
|
2020
|
+
return Math.max(1_000, Math.round(parsed));
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return Math.max(1_000, Math.round(fallback));
|
|
2024
|
+
}
|
|
2025
|
+
function defaultAutoContinueTokenBudget() {
|
|
2026
|
+
const hours = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_BUDGET_HOURS", 4, {
|
|
2027
|
+
min: 0.05,
|
|
2028
|
+
max: 24,
|
|
2029
|
+
});
|
|
2030
|
+
const fallback = DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.tokensPerHour *
|
|
2031
|
+
hours *
|
|
2032
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.contingencyMultiplier;
|
|
2033
|
+
return normalizeTokenBudget(process.env.ORGX_AUTO_CONTINUE_TOKEN_BUDGET, fallback);
|
|
2034
|
+
}
|
|
2035
|
+
function estimateTokensForDurationHours(durationHours) {
|
|
2036
|
+
if (!Number.isFinite(durationHours) || durationHours <= 0)
|
|
2037
|
+
return 0;
|
|
2038
|
+
const raw = durationHours *
|
|
2039
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.tokensPerHour *
|
|
2040
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.contingencyMultiplier;
|
|
2041
|
+
return Math.max(0, Math.round(raw));
|
|
2042
|
+
}
|
|
2043
|
+
function isSafePathSegment(value) {
|
|
2044
|
+
const normalized = value.trim();
|
|
2045
|
+
if (!normalized || normalized === "." || normalized === "..")
|
|
2046
|
+
return false;
|
|
2047
|
+
if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("\0")) {
|
|
2048
|
+
return false;
|
|
2049
|
+
}
|
|
2050
|
+
if (normalized.includes(".."))
|
|
2051
|
+
return false;
|
|
2052
|
+
return true;
|
|
2053
|
+
}
|
|
2054
|
+
function toFiniteNumber(value) {
|
|
2055
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
2056
|
+
return value;
|
|
2057
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
2058
|
+
const parsed = Number(value);
|
|
2059
|
+
if (Number.isFinite(parsed))
|
|
2060
|
+
return parsed;
|
|
2061
|
+
}
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2064
|
+
function readOpenClawSessionSummary(input) {
|
|
2065
|
+
const agentId = input.agentId.trim();
|
|
2066
|
+
const sessionId = input.sessionId.trim();
|
|
2067
|
+
if (!agentId || !sessionId) {
|
|
2068
|
+
return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
|
|
2069
|
+
}
|
|
2070
|
+
if (!isSafePathSegment(agentId) || !isSafePathSegment(sessionId)) {
|
|
2071
|
+
return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
|
|
2072
|
+
}
|
|
2073
|
+
const jsonlPath = join(homedir(), ".openclaw", "agents", agentId, "sessions", `${sessionId}.jsonl`);
|
|
2074
|
+
try {
|
|
2075
|
+
if (!existsSync(jsonlPath)) {
|
|
2076
|
+
return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
|
|
2077
|
+
}
|
|
2078
|
+
const raw = readFileSync(jsonlPath, "utf8");
|
|
2079
|
+
const lines = raw.split("\n");
|
|
2080
|
+
let tokens = 0;
|
|
2081
|
+
let costUsd = 0;
|
|
2082
|
+
let hadError = false;
|
|
2083
|
+
let errorMessage = null;
|
|
2084
|
+
for (const line of lines) {
|
|
2085
|
+
const trimmed = line.trim();
|
|
2086
|
+
if (!trimmed)
|
|
2087
|
+
continue;
|
|
2088
|
+
try {
|
|
2089
|
+
const evt = JSON.parse(trimmed);
|
|
2090
|
+
if (evt.type !== "message")
|
|
2091
|
+
continue;
|
|
2092
|
+
const msg = evt.message;
|
|
2093
|
+
if (!msg || typeof msg !== "object")
|
|
2094
|
+
continue;
|
|
2095
|
+
const usage = msg.usage;
|
|
2096
|
+
if (usage && typeof usage === "object") {
|
|
2097
|
+
const totalTokens = toFiniteNumber(usage.totalTokens) ??
|
|
2098
|
+
toFiniteNumber(usage.total_tokens) ??
|
|
2099
|
+
null;
|
|
2100
|
+
const inputTokens = toFiniteNumber(usage.input) ?? 0;
|
|
2101
|
+
const outputTokens = toFiniteNumber(usage.output) ?? 0;
|
|
2102
|
+
const cacheReadTokens = toFiniteNumber(usage.cacheRead) ?? 0;
|
|
2103
|
+
const cacheWriteTokens = toFiniteNumber(usage.cacheWrite) ?? 0;
|
|
2104
|
+
tokens += Math.max(0, Math.round(totalTokens ??
|
|
2105
|
+
inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens));
|
|
2106
|
+
const cost = usage.cost;
|
|
2107
|
+
const costTotal = cost ? toFiniteNumber(cost.total) : null;
|
|
2108
|
+
if (costTotal !== null) {
|
|
2109
|
+
costUsd += Math.max(0, costTotal);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
const stopReason = typeof msg.stopReason === "string" ? msg.stopReason : "";
|
|
2113
|
+
const msgError = typeof msg.errorMessage === "string" && msg.errorMessage.trim().length > 0
|
|
2114
|
+
? msg.errorMessage.trim()
|
|
2115
|
+
: null;
|
|
2116
|
+
if (stopReason === "error" || msgError) {
|
|
2117
|
+
hadError = true;
|
|
2118
|
+
errorMessage = msgError ?? errorMessage;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
catch {
|
|
2122
|
+
// Ignore malformed lines.
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
return {
|
|
2126
|
+
tokens,
|
|
2127
|
+
costUsd: Math.round(costUsd * 10_000) / 10_000,
|
|
2128
|
+
hadError,
|
|
2129
|
+
errorMessage,
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
catch {
|
|
2133
|
+
return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
async function fetchInitiativeEntity(initiativeId) {
|
|
2137
|
+
try {
|
|
2138
|
+
const list = await client.listEntities("initiative", { limit: 200 });
|
|
2139
|
+
const match = list.data.find((candidate) => String(candidate?.id ?? "") === initiativeId);
|
|
2140
|
+
return match ?? null;
|
|
2141
|
+
}
|
|
2142
|
+
catch {
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
async function updateInitiativeMetadata(initiativeId, patch) {
|
|
2147
|
+
const existing = await fetchInitiativeEntity(initiativeId);
|
|
2148
|
+
const existingMeta = existing && typeof existing === "object"
|
|
2149
|
+
? getRecordMetadata(existing)
|
|
2150
|
+
: {};
|
|
2151
|
+
const nextMeta = { ...existingMeta, ...patch };
|
|
2152
|
+
await client.updateEntity("initiative", initiativeId, { metadata: nextMeta });
|
|
2153
|
+
}
|
|
2154
|
+
async function updateInitiativeAutoContinueState(input) {
|
|
2155
|
+
const now = new Date().toISOString();
|
|
2156
|
+
const patch = {
|
|
2157
|
+
auto_continue_enabled: true,
|
|
2158
|
+
auto_continue_status: input.run.status,
|
|
2159
|
+
auto_continue_stop_reason: input.run.stopReason,
|
|
2160
|
+
auto_continue_started_at: input.run.startedAt,
|
|
2161
|
+
auto_continue_stopped_at: input.run.stoppedAt,
|
|
2162
|
+
auto_continue_updated_at: now,
|
|
2163
|
+
auto_continue_token_budget: input.run.tokenBudget,
|
|
2164
|
+
auto_continue_tokens_used: input.run.tokensUsed,
|
|
2165
|
+
auto_continue_active_task_id: input.run.activeTaskId,
|
|
2166
|
+
auto_continue_active_run_id: input.run.activeRunId,
|
|
2167
|
+
auto_continue_active_task_token_estimate: input.run.activeTaskTokenEstimate,
|
|
2168
|
+
auto_continue_last_task_id: input.run.lastTaskId,
|
|
2169
|
+
auto_continue_last_run_id: input.run.lastRunId,
|
|
2170
|
+
auto_continue_include_verification: input.run.includeVerification,
|
|
2171
|
+
auto_continue_workstream_filter: input.run.allowedWorkstreamIds,
|
|
2172
|
+
...(input.run.lastError ? { auto_continue_last_error: input.run.lastError } : {}),
|
|
2173
|
+
};
|
|
2174
|
+
await updateInitiativeMetadata(input.initiativeId, patch);
|
|
2175
|
+
}
|
|
2176
|
+
async function stopAutoContinueRun(input) {
|
|
2177
|
+
const now = new Date().toISOString();
|
|
2178
|
+
input.run.status = "stopped";
|
|
2179
|
+
input.run.stopReason = input.reason;
|
|
2180
|
+
input.run.stoppedAt = now;
|
|
2181
|
+
input.run.updatedAt = now;
|
|
2182
|
+
input.run.stopRequested = false;
|
|
2183
|
+
input.run.activeRunId = null;
|
|
2184
|
+
input.run.activeTaskId = null;
|
|
2185
|
+
if (input.error)
|
|
2186
|
+
input.run.lastError = input.error;
|
|
2187
|
+
try {
|
|
2188
|
+
if (input.reason === "completed") {
|
|
2189
|
+
await client.updateEntity("initiative", input.run.initiativeId, {
|
|
2190
|
+
status: "completed",
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
else {
|
|
2194
|
+
await client.updateEntity("initiative", input.run.initiativeId, {
|
|
2195
|
+
status: "paused",
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
catch {
|
|
2200
|
+
// best effort; UI still derives paused state locally
|
|
2201
|
+
}
|
|
2202
|
+
try {
|
|
2203
|
+
await updateInitiativeAutoContinueState({
|
|
2204
|
+
initiativeId: input.run.initiativeId,
|
|
2205
|
+
run: input.run,
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
catch {
|
|
2209
|
+
// best effort
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
async function tickAutoContinueRun(run) {
|
|
2213
|
+
if (run.status !== "running" && run.status !== "stopping")
|
|
2214
|
+
return;
|
|
2215
|
+
const now = new Date().toISOString();
|
|
2216
|
+
// 1) If we have an active run, wait for it to finish.
|
|
2217
|
+
if (run.activeRunId) {
|
|
2218
|
+
const record = getAgentRun(run.activeRunId);
|
|
2219
|
+
const pid = record?.pid ?? null;
|
|
2220
|
+
if (pid && pidAlive(pid)) {
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
// Run finished (or pid missing). Mark stopped and auto-complete the task.
|
|
2224
|
+
if (record) {
|
|
2225
|
+
try {
|
|
2226
|
+
markAgentRunStopped(record.runId);
|
|
2227
|
+
}
|
|
2228
|
+
catch {
|
|
2229
|
+
// ignore
|
|
2230
|
+
}
|
|
2231
|
+
const summary = readOpenClawSessionSummary({
|
|
2232
|
+
agentId: record.agentId,
|
|
2233
|
+
sessionId: record.runId,
|
|
2234
|
+
});
|
|
2235
|
+
const modeledTokens = run.activeTaskTokenEstimate ?? 0;
|
|
2236
|
+
const consumedTokens = summary.tokens > 0 ? summary.tokens : modeledTokens;
|
|
2237
|
+
run.tokensUsed += Math.max(0, consumedTokens);
|
|
2238
|
+
run.activeTaskTokenEstimate = null;
|
|
2239
|
+
if (record.taskId) {
|
|
2240
|
+
try {
|
|
2241
|
+
await client.updateEntity("task", record.taskId, {
|
|
2242
|
+
status: summary.hadError ? "blocked" : "done",
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
catch (err) {
|
|
2246
|
+
run.lastError = safeErrorMessage(err);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
if (record.taskId) {
|
|
2250
|
+
await syncParentRollupsForTask({
|
|
2251
|
+
initiativeId: run.initiativeId,
|
|
2252
|
+
taskId: record.taskId,
|
|
2253
|
+
workstreamId: record.workstreamId,
|
|
2254
|
+
correlationId: record.runId,
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
await emitActivitySafe({
|
|
2258
|
+
initiativeId: run.initiativeId,
|
|
2259
|
+
correlationId: record.runId,
|
|
2260
|
+
phase: summary.hadError ? "blocked" : "completed",
|
|
2261
|
+
level: summary.hadError ? "warn" : "info",
|
|
2262
|
+
message: record.taskId
|
|
2263
|
+
? `Auto-continue ${summary.hadError ? "blocked" : "completed"} task ${record.taskId}.`
|
|
2264
|
+
: `Auto-continue run finished (${summary.hadError ? "blocked" : "completed"}).`,
|
|
2265
|
+
metadata: {
|
|
2266
|
+
event: "auto_continue_task_finished",
|
|
2267
|
+
agent_id: record.agentId,
|
|
2268
|
+
session_id: record.runId,
|
|
2269
|
+
task_id: record.taskId,
|
|
2270
|
+
workstream_id: record.workstreamId,
|
|
2271
|
+
tokens: summary.tokens,
|
|
2272
|
+
cost_usd: summary.costUsd,
|
|
2273
|
+
had_error: summary.hadError,
|
|
2274
|
+
error_message: summary.errorMessage,
|
|
2275
|
+
},
|
|
2276
|
+
});
|
|
2277
|
+
run.lastRunId = record.runId;
|
|
2278
|
+
run.lastTaskId = record.taskId ?? run.lastTaskId;
|
|
2279
|
+
run.activeRunId = null;
|
|
2280
|
+
run.activeTaskId = null;
|
|
2281
|
+
run.updatedAt = now;
|
|
2282
|
+
if (summary.hadError && summary.errorMessage) {
|
|
2283
|
+
run.lastError = summary.errorMessage;
|
|
2284
|
+
}
|
|
2285
|
+
try {
|
|
2286
|
+
await updateInitiativeAutoContinueState({
|
|
2287
|
+
initiativeId: run.initiativeId,
|
|
2288
|
+
run,
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
catch {
|
|
2292
|
+
// best effort
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
else {
|
|
2296
|
+
// No record; clear active pointers so we can continue.
|
|
2297
|
+
run.activeRunId = null;
|
|
2298
|
+
run.activeTaskId = null;
|
|
2299
|
+
}
|
|
2300
|
+
// If a stop was requested, finalize after the active run completes.
|
|
2301
|
+
if (run.stopRequested) {
|
|
2302
|
+
await stopAutoContinueRun({ run, reason: "stopped" });
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
if (run.stopRequested) {
|
|
2307
|
+
run.status = "stopping";
|
|
2308
|
+
run.updatedAt = now;
|
|
2309
|
+
await stopAutoContinueRun({ run, reason: "stopped" });
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
// 2) Enforce token guardrail before starting a new task.
|
|
2313
|
+
if (run.tokensUsed >= run.tokenBudget) {
|
|
2314
|
+
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
// 3) Pick next-up task and dispatch.
|
|
2318
|
+
let graph;
|
|
2319
|
+
try {
|
|
2320
|
+
graph = await buildMissionControlGraph(client, run.initiativeId);
|
|
2321
|
+
}
|
|
2322
|
+
catch (err) {
|
|
2323
|
+
await stopAutoContinueRun({
|
|
2324
|
+
run,
|
|
2325
|
+
reason: "error",
|
|
2326
|
+
error: safeErrorMessage(err),
|
|
2327
|
+
});
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
const nodes = graph.nodes;
|
|
2331
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
2332
|
+
const taskNodes = nodes.filter((node) => node.type === "task");
|
|
2333
|
+
const todoTasks = taskNodes.filter((node) => isTodoStatus(node.status));
|
|
2334
|
+
if (todoTasks.length === 0) {
|
|
2335
|
+
await stopAutoContinueRun({ run, reason: "completed" });
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
const taskIsReady = (task) => task.dependencyIds.every((depId) => {
|
|
2339
|
+
const dependency = nodeById.get(depId);
|
|
2340
|
+
return dependency ? isDoneStatus(dependency.status) : true;
|
|
2341
|
+
});
|
|
2342
|
+
const taskHasBlockedParent = (task) => {
|
|
2343
|
+
const milestone = task.milestoneId ? nodeById.get(task.milestoneId) ?? null : null;
|
|
2344
|
+
const workstream = task.workstreamId ? nodeById.get(task.workstreamId) ?? null : null;
|
|
2345
|
+
return (milestone?.status?.toLowerCase() === "blocked" ||
|
|
2346
|
+
workstream?.status?.toLowerCase() === "blocked");
|
|
2347
|
+
};
|
|
2348
|
+
let nextTaskNode = null;
|
|
2349
|
+
for (const taskId of graph.recentTodos) {
|
|
2350
|
+
const node = nodeById.get(taskId);
|
|
2351
|
+
if (!node || node.type !== "task")
|
|
2352
|
+
continue;
|
|
2353
|
+
if (!isTodoStatus(node.status))
|
|
2354
|
+
continue;
|
|
2355
|
+
if (!run.includeVerification &&
|
|
2356
|
+
typeof node.title === "string" &&
|
|
2357
|
+
/^verification\s+scenario/i.test(node.title)) {
|
|
2358
|
+
continue;
|
|
2359
|
+
}
|
|
2360
|
+
if (run.allowedWorkstreamIds &&
|
|
2361
|
+
node.workstreamId &&
|
|
2362
|
+
!run.allowedWorkstreamIds.includes(node.workstreamId)) {
|
|
2363
|
+
continue;
|
|
2364
|
+
}
|
|
2365
|
+
if (node.workstreamId) {
|
|
2366
|
+
const ws = nodeById.get(node.workstreamId);
|
|
2367
|
+
if (ws && !isInProgressStatus(ws.status)) {
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (!taskIsReady(node))
|
|
2372
|
+
continue;
|
|
2373
|
+
if (taskHasBlockedParent(node))
|
|
2374
|
+
continue;
|
|
2375
|
+
nextTaskNode = node;
|
|
2376
|
+
break;
|
|
2377
|
+
}
|
|
2378
|
+
if (!nextTaskNode) {
|
|
2379
|
+
await stopAutoContinueRun({ run, reason: "blocked" });
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
const nextTaskTokenEstimate = estimateTokensForDurationHours(typeof nextTaskNode.expectedDurationHours === "number"
|
|
2383
|
+
? nextTaskNode.expectedDurationHours
|
|
2384
|
+
: 0);
|
|
2385
|
+
if (nextTaskTokenEstimate > 0 &&
|
|
2386
|
+
run.tokensUsed + nextTaskTokenEstimate > run.tokenBudget) {
|
|
2387
|
+
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
const agentId = run.agentId || "main";
|
|
2391
|
+
const sessionId = randomUUID();
|
|
2392
|
+
const initiativeNode = nodes.find((node) => node.type === "initiative") ?? null;
|
|
2393
|
+
const workstreamTitle = nextTaskNode.workstreamId
|
|
2394
|
+
? nodeById.get(nextTaskNode.workstreamId)?.title ?? null
|
|
2395
|
+
: null;
|
|
2396
|
+
const milestoneTitle = nextTaskNode.milestoneId
|
|
2397
|
+
? nodeById.get(nextTaskNode.milestoneId)?.title ?? null
|
|
2398
|
+
: null;
|
|
2399
|
+
const message = [
|
|
2400
|
+
initiativeNode ? `Initiative: ${initiativeNode.title}` : null,
|
|
2401
|
+
workstreamTitle ? `Workstream: ${workstreamTitle}` : null,
|
|
2402
|
+
milestoneTitle ? `Milestone: ${milestoneTitle}` : null,
|
|
2403
|
+
"",
|
|
2404
|
+
`Task: ${nextTaskNode.title}`,
|
|
2405
|
+
"",
|
|
2406
|
+
"Execute this task. When finished, provide a concise completion summary and any relevant commands/notes.",
|
|
2407
|
+
]
|
|
2408
|
+
.filter((line) => typeof line === "string")
|
|
2409
|
+
.join("\n");
|
|
2410
|
+
try {
|
|
2411
|
+
await client.updateEntity("task", nextTaskNode.id, {
|
|
2412
|
+
status: "in_progress",
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
catch (err) {
|
|
2416
|
+
await stopAutoContinueRun({
|
|
2417
|
+
run,
|
|
2418
|
+
reason: "error",
|
|
2419
|
+
error: safeErrorMessage(err),
|
|
2420
|
+
});
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
await syncParentRollupsForTask({
|
|
2424
|
+
initiativeId: run.initiativeId,
|
|
2425
|
+
taskId: nextTaskNode.id,
|
|
2426
|
+
workstreamId: nextTaskNode.workstreamId,
|
|
2427
|
+
milestoneId: nextTaskNode.milestoneId,
|
|
2428
|
+
correlationId: sessionId,
|
|
2429
|
+
});
|
|
2430
|
+
await emitActivitySafe({
|
|
2431
|
+
initiativeId: run.initiativeId,
|
|
2432
|
+
correlationId: sessionId,
|
|
2433
|
+
phase: "execution",
|
|
2434
|
+
level: "info",
|
|
2435
|
+
message: `Auto-continue started task ${nextTaskNode.id}.`,
|
|
2436
|
+
metadata: {
|
|
2437
|
+
event: "auto_continue_task_started",
|
|
2438
|
+
agent_id: agentId,
|
|
2439
|
+
session_id: sessionId,
|
|
2440
|
+
task_id: nextTaskNode.id,
|
|
2441
|
+
task_title: nextTaskNode.title,
|
|
2442
|
+
workstream_id: nextTaskNode.workstreamId,
|
|
2443
|
+
workstream_title: workstreamTitle,
|
|
2444
|
+
milestone_id: nextTaskNode.milestoneId,
|
|
2445
|
+
milestone_title: milestoneTitle,
|
|
2446
|
+
},
|
|
2447
|
+
});
|
|
2448
|
+
upsertAgentContext({
|
|
2449
|
+
agentId,
|
|
2450
|
+
initiativeId: run.initiativeId,
|
|
2451
|
+
initiativeTitle: initiativeNode?.title ?? null,
|
|
2452
|
+
workstreamId: nextTaskNode.workstreamId,
|
|
2453
|
+
taskId: nextTaskNode.id,
|
|
2454
|
+
});
|
|
2455
|
+
const spawned = spawnAgentTurn({
|
|
2456
|
+
agentId,
|
|
2457
|
+
sessionId,
|
|
2458
|
+
message,
|
|
2459
|
+
});
|
|
2460
|
+
upsertAgentRun({
|
|
2461
|
+
runId: sessionId,
|
|
2462
|
+
agentId,
|
|
2463
|
+
pid: spawned.pid,
|
|
2464
|
+
message,
|
|
2465
|
+
provider: null,
|
|
2466
|
+
model: null,
|
|
2467
|
+
initiativeId: run.initiativeId,
|
|
2468
|
+
initiativeTitle: initiativeNode?.title ?? null,
|
|
2469
|
+
workstreamId: nextTaskNode.workstreamId,
|
|
2470
|
+
taskId: nextTaskNode.id,
|
|
2471
|
+
startedAt: now,
|
|
2472
|
+
status: "running",
|
|
2473
|
+
});
|
|
2474
|
+
run.lastTaskId = nextTaskNode.id;
|
|
2475
|
+
run.lastRunId = sessionId;
|
|
2476
|
+
run.activeTaskId = nextTaskNode.id;
|
|
2477
|
+
run.activeRunId = sessionId;
|
|
2478
|
+
run.activeTaskTokenEstimate = nextTaskTokenEstimate > 0 ? nextTaskTokenEstimate : null;
|
|
2479
|
+
run.updatedAt = now;
|
|
2480
|
+
try {
|
|
2481
|
+
await client.updateEntity("initiative", run.initiativeId, { status: "active" });
|
|
2482
|
+
}
|
|
2483
|
+
catch {
|
|
2484
|
+
// best effort
|
|
2485
|
+
}
|
|
2486
|
+
try {
|
|
2487
|
+
await updateInitiativeAutoContinueState({
|
|
2488
|
+
initiativeId: run.initiativeId,
|
|
2489
|
+
run,
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
catch {
|
|
2493
|
+
// best effort
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
async function tickAllAutoContinue() {
|
|
2497
|
+
if (autoContinueTickInFlight)
|
|
2498
|
+
return;
|
|
2499
|
+
autoContinueTickInFlight = true;
|
|
2500
|
+
try {
|
|
2501
|
+
for (const run of autoContinueRuns.values()) {
|
|
2502
|
+
try {
|
|
2503
|
+
await tickAutoContinueRun(run);
|
|
2504
|
+
}
|
|
2505
|
+
catch (err) {
|
|
2506
|
+
// Never let one loop crash the whole handler.
|
|
2507
|
+
run.lastError = safeErrorMessage(err);
|
|
2508
|
+
run.updatedAt = new Date().toISOString();
|
|
2509
|
+
await stopAutoContinueRun({ run, reason: "error", error: run.lastError });
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
finally {
|
|
2514
|
+
autoContinueTickInFlight = false;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
const autoContinueTimer = setInterval(() => {
|
|
2518
|
+
void tickAllAutoContinue();
|
|
2519
|
+
}, AUTO_CONTINUE_TICK_MS);
|
|
2520
|
+
autoContinueTimer.unref?.();
|
|
2521
|
+
return async function handler(req, res) {
|
|
2522
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
1207
2523
|
const rawUrl = req.url ?? "/";
|
|
1208
2524
|
const [path, queryString] = rawUrl.split("?", 2);
|
|
1209
2525
|
const url = path;
|
|
@@ -1214,12 +2530,27 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1214
2530
|
}
|
|
1215
2531
|
// Handle CORS preflight
|
|
1216
2532
|
if (method === "OPTIONS") {
|
|
1217
|
-
|
|
2533
|
+
if (url.startsWith("/orgx/api/") && !isTrustedRequestSource(req.headers)) {
|
|
2534
|
+
sendJson(res, 403, {
|
|
2535
|
+
error: "Cross-origin browser requests are blocked for /orgx/api endpoints.",
|
|
2536
|
+
});
|
|
2537
|
+
return true;
|
|
2538
|
+
}
|
|
2539
|
+
res.writeHead(204, {
|
|
2540
|
+
...SECURITY_HEADERS,
|
|
2541
|
+
...CORS_HEADERS,
|
|
2542
|
+
});
|
|
1218
2543
|
res.end();
|
|
1219
2544
|
return true;
|
|
1220
2545
|
}
|
|
1221
2546
|
// ── API endpoints ──────────────────────────────────────────────────────
|
|
1222
2547
|
if (url.startsWith("/orgx/api/")) {
|
|
2548
|
+
if (!isTrustedRequestSource(req.headers)) {
|
|
2549
|
+
sendJson(res, 403, {
|
|
2550
|
+
error: "Cross-origin browser requests are blocked for /orgx/api endpoints.",
|
|
2551
|
+
});
|
|
2552
|
+
return true;
|
|
2553
|
+
}
|
|
1223
2554
|
const route = url.replace("/orgx/api/", "").replace(/\/+$/, "");
|
|
1224
2555
|
const decisionApproveMatch = route.match(/^live\/decisions\/([^/]+)\/approve$/);
|
|
1225
2556
|
const runActionMatch = route.match(/^runs\/([^/]+)\/actions\/([^/]+)$/);
|
|
@@ -1227,6 +2558,8 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1227
2558
|
const runCheckpointRestoreMatch = route.match(/^runs\/([^/]+)\/checkpoints\/([^/]+)\/restore$/);
|
|
1228
2559
|
const isDelegationPreflight = route === "delegation/preflight";
|
|
1229
2560
|
const isMissionControlAutoAssignmentRoute = route === "mission-control/assignments/auto";
|
|
2561
|
+
const isMissionControlAutoContinueStartRoute = route === "mission-control/auto-continue/start";
|
|
2562
|
+
const isMissionControlAutoContinueStopRoute = route === "mission-control/auto-continue/stop";
|
|
1230
2563
|
const isEntitiesRoute = route === "entities";
|
|
1231
2564
|
const entityActionMatch = route.match(/^entities\/([^/]+)\/([^/]+)\/([^/]+)$/);
|
|
1232
2565
|
const isOnboardingStartRoute = route === "onboarding/start";
|
|
@@ -1234,9 +2567,13 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1234
2567
|
const isOnboardingManualKeyRoute = route === "onboarding/manual-key";
|
|
1235
2568
|
const isOnboardingDisconnectRoute = route === "onboarding/disconnect";
|
|
1236
2569
|
const isLiveActivityHeadlineRoute = route === "live/activity/headline";
|
|
2570
|
+
const isAgentLaunchRoute = route === "agents/launch";
|
|
2571
|
+
const isAgentStopRoute = route === "agents/stop";
|
|
2572
|
+
const isAgentRestartRoute = route === "agents/restart";
|
|
2573
|
+
const isByokSettingsRoute = route === "settings/byok";
|
|
1237
2574
|
if (method === "POST" && isOnboardingStartRoute) {
|
|
1238
2575
|
try {
|
|
1239
|
-
const payload =
|
|
2576
|
+
const payload = await parseJsonRequest(req);
|
|
1240
2577
|
const started = await onboarding.startPairing({
|
|
1241
2578
|
openclawVersion: pickString(payload, ["openclawVersion", "openclaw_version"]) ??
|
|
1242
2579
|
undefined,
|
|
@@ -1280,7 +2617,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1280
2617
|
}
|
|
1281
2618
|
if (method === "POST" && isOnboardingManualKeyRoute) {
|
|
1282
2619
|
try {
|
|
1283
|
-
const payload =
|
|
2620
|
+
const payload = await parseJsonRequest(req);
|
|
1284
2621
|
const authHeader = pickHeaderString(req.headers, ["authorization"]);
|
|
1285
2622
|
const bearerApiKey = authHeader && authHeader.toLowerCase().startsWith("bearer ")
|
|
1286
2623
|
? authHeader.slice("bearer ".length).trim()
|
|
@@ -1299,9 +2636,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1299
2636
|
});
|
|
1300
2637
|
return true;
|
|
1301
2638
|
}
|
|
1302
|
-
const
|
|
2639
|
+
const requestedUserId = pickString(payload, ["userId", "user_id"]) ??
|
|
1303
2640
|
pickHeaderString(req.headers, ["x-orgx-user-id", "x-user-id"]) ??
|
|
1304
2641
|
undefined;
|
|
2642
|
+
const userId = isUserScopedApiKey(apiKey) ? undefined : requestedUserId;
|
|
1305
2643
|
const state = await onboarding.submitManualKey({
|
|
1306
2644
|
apiKey,
|
|
1307
2645
|
userId,
|
|
@@ -1335,10 +2673,600 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1335
2673
|
}
|
|
1336
2674
|
return true;
|
|
1337
2675
|
}
|
|
2676
|
+
if (method === "POST" && isAgentLaunchRoute) {
|
|
2677
|
+
try {
|
|
2678
|
+
const payload = await parseJsonRequest(req);
|
|
2679
|
+
const agentId = (pickString(payload, ["agentId", "agent_id", "id"]) ??
|
|
2680
|
+
searchParams.get("agentId") ??
|
|
2681
|
+
searchParams.get("agent_id") ??
|
|
2682
|
+
searchParams.get("id") ??
|
|
2683
|
+
"")
|
|
2684
|
+
.trim();
|
|
2685
|
+
if (!agentId) {
|
|
2686
|
+
sendJson(res, 400, { ok: false, error: "agentId is required" });
|
|
2687
|
+
return true;
|
|
2688
|
+
}
|
|
2689
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
|
|
2690
|
+
sendJson(res, 400, {
|
|
2691
|
+
ok: false,
|
|
2692
|
+
error: "agentId must be a simple identifier (letters, numbers, _ or -).",
|
|
2693
|
+
});
|
|
2694
|
+
return true;
|
|
2695
|
+
}
|
|
2696
|
+
const sessionId = (pickString(payload, ["sessionId", "session_id"]) ??
|
|
2697
|
+
searchParams.get("sessionId") ??
|
|
2698
|
+
searchParams.get("session_id") ??
|
|
2699
|
+
"")
|
|
2700
|
+
.trim() ||
|
|
2701
|
+
randomUUID();
|
|
2702
|
+
const initiativeId = pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
2703
|
+
searchParams.get("initiativeId") ??
|
|
2704
|
+
searchParams.get("initiative_id") ??
|
|
2705
|
+
null;
|
|
2706
|
+
const initiativeTitle = pickString(payload, [
|
|
2707
|
+
"initiativeTitle",
|
|
2708
|
+
"initiative_title",
|
|
2709
|
+
"initiativeName",
|
|
2710
|
+
"initiative_name",
|
|
2711
|
+
]) ??
|
|
2712
|
+
searchParams.get("initiativeTitle") ??
|
|
2713
|
+
searchParams.get("initiative_title") ??
|
|
2714
|
+
searchParams.get("initiativeName") ??
|
|
2715
|
+
searchParams.get("initiative_name") ??
|
|
2716
|
+
null;
|
|
2717
|
+
const workstreamId = pickString(payload, ["workstreamId", "workstream_id"]) ??
|
|
2718
|
+
searchParams.get("workstreamId") ??
|
|
2719
|
+
searchParams.get("workstream_id") ??
|
|
2720
|
+
null;
|
|
2721
|
+
const taskId = pickString(payload, ["taskId", "task_id"]) ??
|
|
2722
|
+
searchParams.get("taskId") ??
|
|
2723
|
+
searchParams.get("task_id") ??
|
|
2724
|
+
null;
|
|
2725
|
+
const thinking = (pickString(payload, ["thinking"]) ??
|
|
2726
|
+
searchParams.get("thinking") ??
|
|
2727
|
+
"")
|
|
2728
|
+
.trim() || null;
|
|
2729
|
+
const provider = normalizeOpenClawProvider(pickString(payload, ["provider", "modelProvider", "model_provider"]) ??
|
|
2730
|
+
searchParams.get("provider") ??
|
|
2731
|
+
searchParams.get("modelProvider") ??
|
|
2732
|
+
searchParams.get("model_provider") ??
|
|
2733
|
+
null);
|
|
2734
|
+
const requestedModel = (pickString(payload, ["model", "modelId", "model_id"]) ??
|
|
2735
|
+
searchParams.get("model") ??
|
|
2736
|
+
searchParams.get("modelId") ??
|
|
2737
|
+
searchParams.get("model_id") ??
|
|
2738
|
+
"")
|
|
2739
|
+
.trim() || null;
|
|
2740
|
+
const dryRunRaw = payload.dryRun ??
|
|
2741
|
+
payload.dry_run ??
|
|
2742
|
+
searchParams.get("dryRun") ??
|
|
2743
|
+
searchParams.get("dry_run") ??
|
|
2744
|
+
null;
|
|
2745
|
+
const dryRun = typeof dryRunRaw === "boolean"
|
|
2746
|
+
? dryRunRaw
|
|
2747
|
+
: parseBooleanQuery(typeof dryRunRaw === "string" ? dryRunRaw : null);
|
|
2748
|
+
let requiresPremiumLaunch = Boolean(provider) || modelImpliesByok(requestedModel);
|
|
2749
|
+
if (!requiresPremiumLaunch) {
|
|
2750
|
+
try {
|
|
2751
|
+
const agents = await listAgents();
|
|
2752
|
+
const agentEntry = agents.find((entry) => String(entry.id ?? "").trim() === agentId) ??
|
|
2753
|
+
null;
|
|
2754
|
+
const agentModel = agentEntry && typeof agentEntry.model === "string"
|
|
2755
|
+
? agentEntry.model
|
|
2756
|
+
: null;
|
|
2757
|
+
requiresPremiumLaunch = modelImpliesByok(agentModel);
|
|
2758
|
+
}
|
|
2759
|
+
catch {
|
|
2760
|
+
// ignore
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
if (requiresPremiumLaunch) {
|
|
2764
|
+
const billingStatus = await fetchBillingStatusSafe(client);
|
|
2765
|
+
if (billingStatus && billingStatus.plan === "free") {
|
|
2766
|
+
const pricingUrl = `${client.getBaseUrl().replace(/\/+$/, "")}/pricing`;
|
|
2767
|
+
sendJson(res, 402, {
|
|
2768
|
+
ok: false,
|
|
2769
|
+
code: "upgrade_required",
|
|
2770
|
+
error: "BYOK agent launch requires a paid OrgX plan. Upgrade, then retry.",
|
|
2771
|
+
currentPlan: billingStatus.plan,
|
|
2772
|
+
requiredPlan: "starter",
|
|
2773
|
+
actions: {
|
|
2774
|
+
checkout: "/orgx/api/billing/checkout",
|
|
2775
|
+
portal: "/orgx/api/billing/portal",
|
|
2776
|
+
pricing: pricingUrl,
|
|
2777
|
+
},
|
|
2778
|
+
});
|
|
2779
|
+
return true;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
const messageInput = (pickString(payload, ["message", "prompt", "text"]) ??
|
|
2783
|
+
searchParams.get("message") ??
|
|
2784
|
+
searchParams.get("prompt") ??
|
|
2785
|
+
searchParams.get("text") ??
|
|
2786
|
+
"")
|
|
2787
|
+
.trim();
|
|
2788
|
+
const message = messageInput ||
|
|
2789
|
+
(initiativeTitle
|
|
2790
|
+
? `Kick off: ${initiativeTitle}`
|
|
2791
|
+
: initiativeId
|
|
2792
|
+
? `Kick off initiative ${initiativeId}`
|
|
2793
|
+
: `Kick off agent ${agentId}`);
|
|
2794
|
+
if (dryRun) {
|
|
2795
|
+
sendJson(res, 200, {
|
|
2796
|
+
ok: true,
|
|
2797
|
+
dryRun: true,
|
|
2798
|
+
agentId,
|
|
2799
|
+
initiativeId,
|
|
2800
|
+
workstreamId,
|
|
2801
|
+
taskId,
|
|
2802
|
+
requiresPremiumLaunch,
|
|
2803
|
+
startedAt: new Date().toISOString(),
|
|
2804
|
+
message,
|
|
2805
|
+
});
|
|
2806
|
+
return true;
|
|
2807
|
+
}
|
|
2808
|
+
if (initiativeId) {
|
|
2809
|
+
try {
|
|
2810
|
+
await client.updateEntity("initiative", initiativeId, { status: "active" });
|
|
2811
|
+
}
|
|
2812
|
+
catch {
|
|
2813
|
+
// best effort
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
if (taskId) {
|
|
2817
|
+
try {
|
|
2818
|
+
await client.updateEntity("task", taskId, { status: "in_progress" });
|
|
2819
|
+
}
|
|
2820
|
+
catch {
|
|
2821
|
+
// best effort
|
|
2822
|
+
}
|
|
2823
|
+
await syncParentRollupsForTask({
|
|
2824
|
+
initiativeId,
|
|
2825
|
+
taskId,
|
|
2826
|
+
workstreamId,
|
|
2827
|
+
correlationId: sessionId,
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
await emitActivitySafe({
|
|
2831
|
+
initiativeId,
|
|
2832
|
+
correlationId: sessionId,
|
|
2833
|
+
phase: "execution",
|
|
2834
|
+
message: taskId
|
|
2835
|
+
? `Launched agent ${agentId} for task ${taskId}.`
|
|
2836
|
+
: `Launched agent ${agentId}.`,
|
|
2837
|
+
level: "info",
|
|
2838
|
+
metadata: {
|
|
2839
|
+
event: "agent_launch",
|
|
2840
|
+
agent_id: agentId,
|
|
2841
|
+
session_id: sessionId,
|
|
2842
|
+
workstream_id: workstreamId,
|
|
2843
|
+
task_id: taskId,
|
|
2844
|
+
provider,
|
|
2845
|
+
model: requestedModel,
|
|
2846
|
+
},
|
|
2847
|
+
});
|
|
2848
|
+
let routedProvider = null;
|
|
2849
|
+
let routedModel = null;
|
|
2850
|
+
if (provider) {
|
|
2851
|
+
const routed = await configureOpenClawProviderRouting({
|
|
2852
|
+
agentId,
|
|
2853
|
+
provider,
|
|
2854
|
+
requestedModel,
|
|
2855
|
+
});
|
|
2856
|
+
routedProvider = routed.provider;
|
|
2857
|
+
routedModel = routed.model;
|
|
2858
|
+
}
|
|
2859
|
+
upsertAgentContext({
|
|
2860
|
+
agentId,
|
|
2861
|
+
initiativeId,
|
|
2862
|
+
initiativeTitle,
|
|
2863
|
+
workstreamId,
|
|
2864
|
+
taskId,
|
|
2865
|
+
});
|
|
2866
|
+
const spawned = spawnAgentTurn({
|
|
2867
|
+
agentId,
|
|
2868
|
+
sessionId,
|
|
2869
|
+
message,
|
|
2870
|
+
thinking,
|
|
2871
|
+
});
|
|
2872
|
+
upsertAgentRun({
|
|
2873
|
+
runId: sessionId,
|
|
2874
|
+
agentId,
|
|
2875
|
+
pid: spawned.pid,
|
|
2876
|
+
message,
|
|
2877
|
+
provider: routedProvider,
|
|
2878
|
+
model: routedModel,
|
|
2879
|
+
initiativeId,
|
|
2880
|
+
initiativeTitle,
|
|
2881
|
+
workstreamId,
|
|
2882
|
+
taskId,
|
|
2883
|
+
startedAt: new Date().toISOString(),
|
|
2884
|
+
status: "running",
|
|
2885
|
+
});
|
|
2886
|
+
sendJson(res, 202, {
|
|
2887
|
+
ok: true,
|
|
2888
|
+
agentId,
|
|
2889
|
+
sessionId,
|
|
2890
|
+
pid: spawned.pid,
|
|
2891
|
+
provider: routedProvider,
|
|
2892
|
+
model: routedModel,
|
|
2893
|
+
initiativeId,
|
|
2894
|
+
workstreamId,
|
|
2895
|
+
taskId,
|
|
2896
|
+
startedAt: new Date().toISOString(),
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2899
|
+
catch (err) {
|
|
2900
|
+
sendJson(res, 500, {
|
|
2901
|
+
ok: false,
|
|
2902
|
+
error: safeErrorMessage(err),
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
return true;
|
|
2906
|
+
}
|
|
2907
|
+
if (method === "POST" && isAgentStopRoute) {
|
|
2908
|
+
try {
|
|
2909
|
+
const payload = await parseJsonRequest(req);
|
|
2910
|
+
const runId = (pickString(payload, ["runId", "run_id", "sessionId", "session_id"]) ??
|
|
2911
|
+
searchParams.get("runId") ??
|
|
2912
|
+
searchParams.get("run_id") ??
|
|
2913
|
+
searchParams.get("sessionId") ??
|
|
2914
|
+
searchParams.get("session_id") ??
|
|
2915
|
+
"")
|
|
2916
|
+
.trim();
|
|
2917
|
+
if (!runId) {
|
|
2918
|
+
sendJson(res, 400, { ok: false, error: "runId is required" });
|
|
2919
|
+
return true;
|
|
2920
|
+
}
|
|
2921
|
+
const record = getAgentRun(runId);
|
|
2922
|
+
if (!record) {
|
|
2923
|
+
sendJson(res, 404, { ok: false, error: "Run not found" });
|
|
2924
|
+
return true;
|
|
2925
|
+
}
|
|
2926
|
+
if (!record.pid) {
|
|
2927
|
+
sendJson(res, 409, { ok: false, error: "Run has no tracked pid" });
|
|
2928
|
+
return true;
|
|
2929
|
+
}
|
|
2930
|
+
const result = await stopProcess(record.pid);
|
|
2931
|
+
const updated = markAgentRunStopped(runId);
|
|
2932
|
+
sendJson(res, 200, {
|
|
2933
|
+
ok: true,
|
|
2934
|
+
runId,
|
|
2935
|
+
agentId: record.agentId,
|
|
2936
|
+
pid: record.pid,
|
|
2937
|
+
stopped: result.stopped,
|
|
2938
|
+
wasRunning: result.wasRunning,
|
|
2939
|
+
record: updated,
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
catch (err) {
|
|
2943
|
+
sendJson(res, 500, { ok: false, error: safeErrorMessage(err) });
|
|
2944
|
+
}
|
|
2945
|
+
return true;
|
|
2946
|
+
}
|
|
2947
|
+
if (method === "POST" && isAgentRestartRoute) {
|
|
2948
|
+
try {
|
|
2949
|
+
const payload = await parseJsonRequest(req);
|
|
2950
|
+
const previousRunId = (pickString(payload, ["runId", "run_id", "sessionId", "session_id"]) ??
|
|
2951
|
+
searchParams.get("runId") ??
|
|
2952
|
+
searchParams.get("run_id") ??
|
|
2953
|
+
searchParams.get("sessionId") ??
|
|
2954
|
+
searchParams.get("session_id") ??
|
|
2955
|
+
"")
|
|
2956
|
+
.trim();
|
|
2957
|
+
if (!previousRunId) {
|
|
2958
|
+
sendJson(res, 400, { ok: false, error: "runId is required" });
|
|
2959
|
+
return true;
|
|
2960
|
+
}
|
|
2961
|
+
const record = getAgentRun(previousRunId);
|
|
2962
|
+
if (!record) {
|
|
2963
|
+
sendJson(res, 404, { ok: false, error: "Run not found" });
|
|
2964
|
+
return true;
|
|
2965
|
+
}
|
|
2966
|
+
const messageOverride = (pickString(payload, ["message", "prompt", "text"]) ??
|
|
2967
|
+
searchParams.get("message") ??
|
|
2968
|
+
searchParams.get("prompt") ??
|
|
2969
|
+
searchParams.get("text") ??
|
|
2970
|
+
"")
|
|
2971
|
+
.trim() || null;
|
|
2972
|
+
const providerOverride = normalizeOpenClawProvider(pickString(payload, ["provider", "modelProvider", "model_provider"]) ??
|
|
2973
|
+
searchParams.get("provider") ??
|
|
2974
|
+
searchParams.get("modelProvider") ??
|
|
2975
|
+
searchParams.get("model_provider") ??
|
|
2976
|
+
record.provider ??
|
|
2977
|
+
null);
|
|
2978
|
+
const requestedModel = (pickString(payload, ["model", "modelId", "model_id"]) ??
|
|
2979
|
+
searchParams.get("model") ??
|
|
2980
|
+
searchParams.get("modelId") ??
|
|
2981
|
+
searchParams.get("model_id") ??
|
|
2982
|
+
record.model ??
|
|
2983
|
+
"")
|
|
2984
|
+
.trim() || null;
|
|
2985
|
+
let requiresPremiumRestart = Boolean(providerOverride) ||
|
|
2986
|
+
modelImpliesByok(requestedModel) ||
|
|
2987
|
+
modelImpliesByok(record.model ?? null);
|
|
2988
|
+
if (!requiresPremiumRestart) {
|
|
2989
|
+
try {
|
|
2990
|
+
const agents = await listAgents();
|
|
2991
|
+
const agentEntry = agents.find((entry) => String(entry.id ?? "").trim() === record.agentId) ?? null;
|
|
2992
|
+
const agentModel = agentEntry && typeof agentEntry.model === "string"
|
|
2993
|
+
? agentEntry.model
|
|
2994
|
+
: null;
|
|
2995
|
+
requiresPremiumRestart = modelImpliesByok(agentModel);
|
|
2996
|
+
}
|
|
2997
|
+
catch {
|
|
2998
|
+
// ignore
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
if (requiresPremiumRestart) {
|
|
3002
|
+
const billingStatus = await fetchBillingStatusSafe(client);
|
|
3003
|
+
if (billingStatus && billingStatus.plan === "free") {
|
|
3004
|
+
const pricingUrl = `${client.getBaseUrl().replace(/\/+$/, "")}/pricing`;
|
|
3005
|
+
sendJson(res, 402, {
|
|
3006
|
+
ok: false,
|
|
3007
|
+
code: "upgrade_required",
|
|
3008
|
+
error: "BYOK agent launch requires a paid OrgX plan. Upgrade, then retry.",
|
|
3009
|
+
currentPlan: billingStatus.plan,
|
|
3010
|
+
requiredPlan: "starter",
|
|
3011
|
+
actions: {
|
|
3012
|
+
checkout: "/orgx/api/billing/checkout",
|
|
3013
|
+
portal: "/orgx/api/billing/portal",
|
|
3014
|
+
pricing: pricingUrl,
|
|
3015
|
+
},
|
|
3016
|
+
});
|
|
3017
|
+
return true;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
const sessionId = randomUUID();
|
|
3021
|
+
const message = messageOverride ?? record.message ?? `Restart agent ${record.agentId}`;
|
|
3022
|
+
let routedProvider = providerOverride ?? null;
|
|
3023
|
+
let routedModel = requestedModel ?? null;
|
|
3024
|
+
if (providerOverride) {
|
|
3025
|
+
const routed = await configureOpenClawProviderRouting({
|
|
3026
|
+
agentId: record.agentId,
|
|
3027
|
+
provider: providerOverride,
|
|
3028
|
+
requestedModel,
|
|
3029
|
+
});
|
|
3030
|
+
routedProvider = routed.provider;
|
|
3031
|
+
routedModel = routed.model;
|
|
3032
|
+
}
|
|
3033
|
+
upsertAgentContext({
|
|
3034
|
+
agentId: record.agentId,
|
|
3035
|
+
initiativeId: record.initiativeId,
|
|
3036
|
+
initiativeTitle: record.initiativeTitle,
|
|
3037
|
+
workstreamId: record.workstreamId,
|
|
3038
|
+
taskId: record.taskId,
|
|
3039
|
+
});
|
|
3040
|
+
const spawned = spawnAgentTurn({
|
|
3041
|
+
agentId: record.agentId,
|
|
3042
|
+
sessionId,
|
|
3043
|
+
message,
|
|
3044
|
+
});
|
|
3045
|
+
upsertAgentRun({
|
|
3046
|
+
runId: sessionId,
|
|
3047
|
+
agentId: record.agentId,
|
|
3048
|
+
pid: spawned.pid,
|
|
3049
|
+
message,
|
|
3050
|
+
provider: routedProvider,
|
|
3051
|
+
model: routedModel,
|
|
3052
|
+
initiativeId: record.initiativeId,
|
|
3053
|
+
initiativeTitle: record.initiativeTitle,
|
|
3054
|
+
workstreamId: record.workstreamId,
|
|
3055
|
+
taskId: record.taskId,
|
|
3056
|
+
startedAt: new Date().toISOString(),
|
|
3057
|
+
status: "running",
|
|
3058
|
+
});
|
|
3059
|
+
sendJson(res, 202, {
|
|
3060
|
+
ok: true,
|
|
3061
|
+
previousRunId,
|
|
3062
|
+
sessionId,
|
|
3063
|
+
agentId: record.agentId,
|
|
3064
|
+
pid: spawned.pid,
|
|
3065
|
+
provider: routedProvider,
|
|
3066
|
+
model: routedModel,
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
catch (err) {
|
|
3070
|
+
sendJson(res, 500, { ok: false, error: safeErrorMessage(err) });
|
|
3071
|
+
}
|
|
3072
|
+
return true;
|
|
3073
|
+
}
|
|
3074
|
+
if (method === "POST" && isMissionControlAutoContinueStartRoute) {
|
|
3075
|
+
try {
|
|
3076
|
+
const payload = await parseJsonRequest(req);
|
|
3077
|
+
const initiativeId = (pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
3078
|
+
searchParams.get("initiativeId") ??
|
|
3079
|
+
searchParams.get("initiative_id") ??
|
|
3080
|
+
"")
|
|
3081
|
+
.trim();
|
|
3082
|
+
if (!initiativeId) {
|
|
3083
|
+
sendJson(res, 400, { ok: false, error: "initiativeId is required" });
|
|
3084
|
+
return true;
|
|
3085
|
+
}
|
|
3086
|
+
const agentIdRaw = (pickString(payload, ["agentId", "agent_id"]) ??
|
|
3087
|
+
searchParams.get("agentId") ??
|
|
3088
|
+
searchParams.get("agent_id") ??
|
|
3089
|
+
"main")
|
|
3090
|
+
.trim();
|
|
3091
|
+
const agentId = agentIdRaw || "main";
|
|
3092
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
|
|
3093
|
+
sendJson(res, 400, {
|
|
3094
|
+
ok: false,
|
|
3095
|
+
error: "agentId must be a simple identifier (letters, numbers, _ or -).",
|
|
3096
|
+
});
|
|
3097
|
+
return true;
|
|
3098
|
+
}
|
|
3099
|
+
let requiresPremiumAutoContinue = false;
|
|
3100
|
+
try {
|
|
3101
|
+
const agents = await listAgents();
|
|
3102
|
+
const agentEntry = agents.find((entry) => String(entry.id ?? "").trim() === agentId) ??
|
|
3103
|
+
null;
|
|
3104
|
+
const agentModel = agentEntry && typeof agentEntry.model === "string"
|
|
3105
|
+
? agentEntry.model
|
|
3106
|
+
: null;
|
|
3107
|
+
requiresPremiumAutoContinue = modelImpliesByok(agentModel);
|
|
3108
|
+
}
|
|
3109
|
+
catch {
|
|
3110
|
+
// ignore
|
|
3111
|
+
}
|
|
3112
|
+
if (requiresPremiumAutoContinue) {
|
|
3113
|
+
const billingStatus = await fetchBillingStatusSafe(client);
|
|
3114
|
+
if (billingStatus && billingStatus.plan === "free") {
|
|
3115
|
+
const pricingUrl = `${client.getBaseUrl().replace(/\/+$/, "")}/pricing`;
|
|
3116
|
+
sendJson(res, 402, {
|
|
3117
|
+
ok: false,
|
|
3118
|
+
code: "upgrade_required",
|
|
3119
|
+
error: "Auto-continue for BYOK agents requires a paid OrgX plan. Upgrade, then retry.",
|
|
3120
|
+
currentPlan: billingStatus.plan,
|
|
3121
|
+
requiredPlan: "starter",
|
|
3122
|
+
actions: {
|
|
3123
|
+
checkout: "/orgx/api/billing/checkout",
|
|
3124
|
+
portal: "/orgx/api/billing/portal",
|
|
3125
|
+
pricing: pricingUrl,
|
|
3126
|
+
},
|
|
3127
|
+
});
|
|
3128
|
+
return true;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
const tokenBudget = pickNumber(payload, [
|
|
3132
|
+
"tokenBudget",
|
|
3133
|
+
"token_budget",
|
|
3134
|
+
"tokenBudgetTokens",
|
|
3135
|
+
"token_budget_tokens",
|
|
3136
|
+
"maxTokens",
|
|
3137
|
+
"max_tokens",
|
|
3138
|
+
]) ??
|
|
3139
|
+
searchParams.get("tokenBudget") ??
|
|
3140
|
+
searchParams.get("token_budget") ??
|
|
3141
|
+
searchParams.get("tokenBudgetTokens") ??
|
|
3142
|
+
searchParams.get("token_budget_tokens") ??
|
|
3143
|
+
searchParams.get("maxTokens") ??
|
|
3144
|
+
searchParams.get("max_tokens") ??
|
|
3145
|
+
null;
|
|
3146
|
+
const includeVerificationRaw = payload.includeVerification ??
|
|
3147
|
+
payload.include_verification ??
|
|
3148
|
+
searchParams.get("includeVerification") ??
|
|
3149
|
+
searchParams.get("include_verification") ??
|
|
3150
|
+
null;
|
|
3151
|
+
const includeVerification = typeof includeVerificationRaw === "boolean"
|
|
3152
|
+
? includeVerificationRaw
|
|
3153
|
+
: parseBooleanQuery(typeof includeVerificationRaw === "string"
|
|
3154
|
+
? includeVerificationRaw
|
|
3155
|
+
: null);
|
|
3156
|
+
const workstreamFilter = dedupeStrings([
|
|
3157
|
+
...pickStringArray(payload, [
|
|
3158
|
+
"workstreamIds",
|
|
3159
|
+
"workstream_ids",
|
|
3160
|
+
"workstreamId",
|
|
3161
|
+
"workstream_id",
|
|
3162
|
+
]),
|
|
3163
|
+
...(searchParams.get("workstreamIds") ??
|
|
3164
|
+
searchParams.get("workstream_ids") ??
|
|
3165
|
+
searchParams.get("workstreamId") ??
|
|
3166
|
+
searchParams.get("workstream_id") ??
|
|
3167
|
+
"")
|
|
3168
|
+
.split(",")
|
|
3169
|
+
.map((entry) => entry.trim())
|
|
3170
|
+
.filter(Boolean),
|
|
3171
|
+
]);
|
|
3172
|
+
const allowedWorkstreamIds = workstreamFilter.length > 0 ? workstreamFilter : null;
|
|
3173
|
+
const now = new Date().toISOString();
|
|
3174
|
+
const existing = autoContinueRuns.get(initiativeId) ?? null;
|
|
3175
|
+
const run = existing ??
|
|
3176
|
+
{
|
|
3177
|
+
initiativeId,
|
|
3178
|
+
agentId,
|
|
3179
|
+
includeVerification: false,
|
|
3180
|
+
allowedWorkstreamIds: null,
|
|
3181
|
+
tokenBudget: defaultAutoContinueTokenBudget(),
|
|
3182
|
+
tokensUsed: 0,
|
|
3183
|
+
status: "running",
|
|
3184
|
+
stopReason: null,
|
|
3185
|
+
stopRequested: false,
|
|
3186
|
+
startedAt: now,
|
|
3187
|
+
stoppedAt: null,
|
|
3188
|
+
updatedAt: now,
|
|
3189
|
+
lastError: null,
|
|
3190
|
+
lastTaskId: null,
|
|
3191
|
+
lastRunId: null,
|
|
3192
|
+
activeTaskId: null,
|
|
3193
|
+
activeRunId: null,
|
|
3194
|
+
activeTaskTokenEstimate: null,
|
|
3195
|
+
};
|
|
3196
|
+
run.agentId = agentId;
|
|
3197
|
+
run.includeVerification = includeVerification;
|
|
3198
|
+
run.allowedWorkstreamIds = allowedWorkstreamIds;
|
|
3199
|
+
run.tokenBudget = normalizeTokenBudget(tokenBudget, run.tokenBudget || defaultAutoContinueTokenBudget());
|
|
3200
|
+
run.status = "running";
|
|
3201
|
+
run.stopReason = null;
|
|
3202
|
+
run.stopRequested = false;
|
|
3203
|
+
run.startedAt = now;
|
|
3204
|
+
run.stoppedAt = null;
|
|
3205
|
+
run.updatedAt = now;
|
|
3206
|
+
run.lastError = null;
|
|
3207
|
+
autoContinueRuns.set(initiativeId, run);
|
|
3208
|
+
try {
|
|
3209
|
+
await client.updateEntity("initiative", initiativeId, { status: "active" });
|
|
3210
|
+
}
|
|
3211
|
+
catch {
|
|
3212
|
+
// best effort
|
|
3213
|
+
}
|
|
3214
|
+
try {
|
|
3215
|
+
await updateInitiativeAutoContinueState({ initiativeId, run });
|
|
3216
|
+
}
|
|
3217
|
+
catch {
|
|
3218
|
+
// best effort
|
|
3219
|
+
}
|
|
3220
|
+
sendJson(res, 200, { ok: true, run });
|
|
3221
|
+
}
|
|
3222
|
+
catch (err) {
|
|
3223
|
+
sendJson(res, 500, { ok: false, error: safeErrorMessage(err) });
|
|
3224
|
+
}
|
|
3225
|
+
return true;
|
|
3226
|
+
}
|
|
3227
|
+
if (method === "POST" && isMissionControlAutoContinueStopRoute) {
|
|
3228
|
+
try {
|
|
3229
|
+
const payload = await parseJsonRequest(req);
|
|
3230
|
+
const initiativeId = (pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
3231
|
+
searchParams.get("initiativeId") ??
|
|
3232
|
+
searchParams.get("initiative_id") ??
|
|
3233
|
+
"")
|
|
3234
|
+
.trim();
|
|
3235
|
+
if (!initiativeId) {
|
|
3236
|
+
sendJson(res, 400, { ok: false, error: "initiativeId is required" });
|
|
3237
|
+
return true;
|
|
3238
|
+
}
|
|
3239
|
+
const run = autoContinueRuns.get(initiativeId) ?? null;
|
|
3240
|
+
if (!run) {
|
|
3241
|
+
sendJson(res, 404, { ok: false, error: "No auto-continue run found" });
|
|
3242
|
+
return true;
|
|
3243
|
+
}
|
|
3244
|
+
const now = new Date().toISOString();
|
|
3245
|
+
run.stopRequested = true;
|
|
3246
|
+
run.status = run.activeRunId ? "stopping" : "stopped";
|
|
3247
|
+
run.updatedAt = now;
|
|
3248
|
+
if (!run.activeRunId) {
|
|
3249
|
+
await stopAutoContinueRun({ run, reason: "stopped" });
|
|
3250
|
+
}
|
|
3251
|
+
else {
|
|
3252
|
+
try {
|
|
3253
|
+
await updateInitiativeAutoContinueState({ initiativeId, run });
|
|
3254
|
+
}
|
|
3255
|
+
catch {
|
|
3256
|
+
// best effort
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
sendJson(res, 200, { ok: true, run });
|
|
3260
|
+
}
|
|
3261
|
+
catch (err) {
|
|
3262
|
+
sendJson(res, 500, { ok: false, error: safeErrorMessage(err) });
|
|
3263
|
+
}
|
|
3264
|
+
return true;
|
|
3265
|
+
}
|
|
1338
3266
|
if (method === "POST" &&
|
|
1339
3267
|
(route === "live/decisions/approve" || decisionApproveMatch)) {
|
|
1340
3268
|
try {
|
|
1341
|
-
const payload =
|
|
3269
|
+
const payload = await parseJsonRequest(req);
|
|
1342
3270
|
const action = payload.action === "reject" ? "reject" : "approve";
|
|
1343
3271
|
const note = typeof payload.note === "string" && payload.note.trim().length > 0
|
|
1344
3272
|
? payload.note.trim()
|
|
@@ -1381,7 +3309,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1381
3309
|
}
|
|
1382
3310
|
if (method === "POST" && isDelegationPreflight) {
|
|
1383
3311
|
try {
|
|
1384
|
-
const payload =
|
|
3312
|
+
const payload = await parseJsonRequest(req);
|
|
1385
3313
|
const intent = pickString(payload, ["intent"]);
|
|
1386
3314
|
if (!intent) {
|
|
1387
3315
|
sendJson(res, 400, { error: "intent is required" });
|
|
@@ -1407,7 +3335,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1407
3335
|
}
|
|
1408
3336
|
if (method === "POST" && isMissionControlAutoAssignmentRoute) {
|
|
1409
3337
|
try {
|
|
1410
|
-
const payload =
|
|
3338
|
+
const payload = await parseJsonRequest(req);
|
|
1411
3339
|
const entityId = pickString(payload, ["entity_id", "entityId"]);
|
|
1412
3340
|
const entityType = pickString(payload, ["entity_type", "entityType"]);
|
|
1413
3341
|
const initiativeId = pickString(payload, ["initiative_id", "initiativeId"]) ?? null;
|
|
@@ -1441,7 +3369,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1441
3369
|
if (runCheckpointsMatch && method === "POST") {
|
|
1442
3370
|
try {
|
|
1443
3371
|
const runId = decodeURIComponent(runCheckpointsMatch[1]);
|
|
1444
|
-
const payload =
|
|
3372
|
+
const payload = await parseJsonRequest(req);
|
|
1445
3373
|
const reason = pickString(payload, ["reason"]) ?? undefined;
|
|
1446
3374
|
const rawPayload = payload.payload;
|
|
1447
3375
|
const checkpointPayload = rawPayload && typeof rawPayload === "object" && !Array.isArray(rawPayload)
|
|
@@ -1464,7 +3392,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1464
3392
|
try {
|
|
1465
3393
|
const runId = decodeURIComponent(runCheckpointRestoreMatch[1]);
|
|
1466
3394
|
const checkpointId = decodeURIComponent(runCheckpointRestoreMatch[2]);
|
|
1467
|
-
const payload =
|
|
3395
|
+
const payload = await parseJsonRequest(req);
|
|
1468
3396
|
const reason = pickString(payload, ["reason"]) ?? undefined;
|
|
1469
3397
|
const data = await client.restoreRunCheckpoint(runId, {
|
|
1470
3398
|
checkpointId,
|
|
@@ -1483,7 +3411,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1483
3411
|
try {
|
|
1484
3412
|
const runId = decodeURIComponent(runActionMatch[1]);
|
|
1485
3413
|
const action = decodeURIComponent(runActionMatch[2]);
|
|
1486
|
-
const payload =
|
|
3414
|
+
const payload = await parseJsonRequest(req);
|
|
1487
3415
|
const checkpointId = pickString(payload, ["checkpointId", "checkpoint_id"]);
|
|
1488
3416
|
const reason = pickString(payload, ["reason"]);
|
|
1489
3417
|
const data = await client.runAction(runId, action, {
|
|
@@ -1505,7 +3433,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1505
3433
|
const entityType = decodeURIComponent(entityActionMatch[1]);
|
|
1506
3434
|
const entityId = decodeURIComponent(entityActionMatch[2]);
|
|
1507
3435
|
const entityAction = decodeURIComponent(entityActionMatch[3]);
|
|
1508
|
-
const payload =
|
|
3436
|
+
const payload = await parseJsonRequest(req);
|
|
1509
3437
|
if (entityAction === "delete") {
|
|
1510
3438
|
// Delete via status update
|
|
1511
3439
|
const entity = await client.updateEntity(entityType, entityId, {
|
|
@@ -1545,6 +3473,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1545
3473
|
return true;
|
|
1546
3474
|
}
|
|
1547
3475
|
if (method !== "GET" &&
|
|
3476
|
+
method !== "HEAD" &&
|
|
1548
3477
|
!(runCheckpointsMatch && method === "POST") &&
|
|
1549
3478
|
!(runCheckpointRestoreMatch && method === "POST") &&
|
|
1550
3479
|
!(runActionMatch && method === "POST") &&
|
|
@@ -1556,9 +3485,11 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1556
3485
|
!(isOnboardingStartRoute && method === "POST") &&
|
|
1557
3486
|
!(isOnboardingManualKeyRoute && method === "POST") &&
|
|
1558
3487
|
!(isOnboardingDisconnectRoute && method === "POST") &&
|
|
3488
|
+
!(isByokSettingsRoute && method === "POST") &&
|
|
1559
3489
|
!(isLiveActivityHeadlineRoute && method === "POST")) {
|
|
1560
3490
|
res.writeHead(405, {
|
|
1561
3491
|
"Content-Type": "text/plain",
|
|
3492
|
+
...SECURITY_HEADERS,
|
|
1562
3493
|
...CORS_HEADERS,
|
|
1563
3494
|
});
|
|
1564
3495
|
res.end("Method Not Allowed");
|
|
@@ -1576,12 +3507,150 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1576
3507
|
// use null snapshot
|
|
1577
3508
|
}
|
|
1578
3509
|
}
|
|
3510
|
+
if (method === "HEAD") {
|
|
3511
|
+
// The dashboard uses a HEAD probe to determine connection state.
|
|
3512
|
+
// Mirror the GET semantics (connected vs not) via status code,
|
|
3513
|
+
// but omit a response body.
|
|
3514
|
+
res.writeHead(snapshot ? 200 : 503, {
|
|
3515
|
+
...SECURITY_HEADERS,
|
|
3516
|
+
...CORS_HEADERS,
|
|
3517
|
+
});
|
|
3518
|
+
res.end();
|
|
3519
|
+
return true;
|
|
3520
|
+
}
|
|
1579
3521
|
sendJson(res, 200, formatStatus(snapshot));
|
|
1580
3522
|
return true;
|
|
1581
3523
|
}
|
|
3524
|
+
case "health": {
|
|
3525
|
+
const probeRemote = parseBooleanQuery(searchParams.get("probe") ?? searchParams.get("probe_remote"));
|
|
3526
|
+
try {
|
|
3527
|
+
if (diagnostics?.getHealth) {
|
|
3528
|
+
const health = await diagnostics.getHealth({ probeRemote });
|
|
3529
|
+
sendJson(res, 200, health);
|
|
3530
|
+
return true;
|
|
3531
|
+
}
|
|
3532
|
+
const outbox = await outboxAdapter.readSummary();
|
|
3533
|
+
sendJson(res, 200, {
|
|
3534
|
+
ok: true,
|
|
3535
|
+
status: "ok",
|
|
3536
|
+
generatedAt: new Date().toISOString(),
|
|
3537
|
+
checks: [],
|
|
3538
|
+
plugin: {
|
|
3539
|
+
baseUrl: config.baseUrl,
|
|
3540
|
+
},
|
|
3541
|
+
auth: {
|
|
3542
|
+
hasApiKey: Boolean(config.apiKey),
|
|
3543
|
+
},
|
|
3544
|
+
outbox: {
|
|
3545
|
+
pendingTotal: outbox.pendingTotal,
|
|
3546
|
+
pendingByQueue: outbox.pendingByQueue,
|
|
3547
|
+
oldestEventAt: outbox.oldestEventAt,
|
|
3548
|
+
newestEventAt: outbox.newestEventAt,
|
|
3549
|
+
replayStatus: "idle",
|
|
3550
|
+
lastReplayAttemptAt: null,
|
|
3551
|
+
lastReplaySuccessAt: null,
|
|
3552
|
+
lastReplayFailureAt: null,
|
|
3553
|
+
lastReplayError: null,
|
|
3554
|
+
},
|
|
3555
|
+
remote: {
|
|
3556
|
+
enabled: false,
|
|
3557
|
+
reachable: null,
|
|
3558
|
+
latencyMs: null,
|
|
3559
|
+
error: null,
|
|
3560
|
+
},
|
|
3561
|
+
});
|
|
3562
|
+
}
|
|
3563
|
+
catch (err) {
|
|
3564
|
+
sendJson(res, 500, {
|
|
3565
|
+
error: safeErrorMessage(err),
|
|
3566
|
+
});
|
|
3567
|
+
}
|
|
3568
|
+
return true;
|
|
3569
|
+
}
|
|
1582
3570
|
case "agents":
|
|
1583
3571
|
sendJson(res, 200, formatAgents(getSnapshot()));
|
|
1584
3572
|
return true;
|
|
3573
|
+
case "agents/catalog": {
|
|
3574
|
+
try {
|
|
3575
|
+
const [openclawAgents, localSnapshot] = await Promise.all([
|
|
3576
|
+
listAgents(),
|
|
3577
|
+
loadLocalOpenClawSnapshot(240).catch(() => null),
|
|
3578
|
+
]);
|
|
3579
|
+
const localById = new Map();
|
|
3580
|
+
if (localSnapshot) {
|
|
3581
|
+
for (const agent of localSnapshot.agents) {
|
|
3582
|
+
localById.set(agent.id, {
|
|
3583
|
+
status: agent.status,
|
|
3584
|
+
currentTask: agent.currentTask,
|
|
3585
|
+
runId: agent.runId,
|
|
3586
|
+
startedAt: agent.startedAt,
|
|
3587
|
+
blockers: agent.blockers,
|
|
3588
|
+
});
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
const contexts = readAgentContexts().agents;
|
|
3592
|
+
const runs = readAgentRuns().runs;
|
|
3593
|
+
const latestRunByAgent = new Map();
|
|
3594
|
+
for (const run of Object.values(runs)) {
|
|
3595
|
+
if (!run || typeof run !== "object")
|
|
3596
|
+
continue;
|
|
3597
|
+
const agentId = typeof run.agentId === "string" ? run.agentId.trim() : "";
|
|
3598
|
+
if (!agentId)
|
|
3599
|
+
continue;
|
|
3600
|
+
const existing = latestRunByAgent.get(agentId);
|
|
3601
|
+
const nextTs = Date.parse(run.startedAt ?? "");
|
|
3602
|
+
const existingTs = existing ? Date.parse(existing.startedAt ?? "") : 0;
|
|
3603
|
+
// Prefer latest running record; fall back to latest overall if none running.
|
|
3604
|
+
if (!existing) {
|
|
3605
|
+
latestRunByAgent.set(agentId, run);
|
|
3606
|
+
continue;
|
|
3607
|
+
}
|
|
3608
|
+
const existingRunning = existing.status === "running";
|
|
3609
|
+
const nextRunning = run.status === "running";
|
|
3610
|
+
if (nextRunning && !existingRunning) {
|
|
3611
|
+
latestRunByAgent.set(agentId, run);
|
|
3612
|
+
continue;
|
|
3613
|
+
}
|
|
3614
|
+
if (nextRunning === existingRunning && nextTs > existingTs) {
|
|
3615
|
+
latestRunByAgent.set(agentId, run);
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
const agents = openclawAgents.map((entry) => {
|
|
3619
|
+
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
3620
|
+
const name = typeof entry.name === "string" && entry.name.trim().length > 0
|
|
3621
|
+
? entry.name.trim()
|
|
3622
|
+
: id || "unknown";
|
|
3623
|
+
const local = id ? localById.get(id) ?? null : null;
|
|
3624
|
+
const context = id ? contexts[id] ?? null : null;
|
|
3625
|
+
const runFromSession = id && local?.runId ? runs[local.runId] ?? null : null;
|
|
3626
|
+
const run = runFromSession ?? (id ? latestRunByAgent.get(id) ?? null : null);
|
|
3627
|
+
return {
|
|
3628
|
+
id,
|
|
3629
|
+
name,
|
|
3630
|
+
workspace: typeof entry.workspace === "string" ? entry.workspace : null,
|
|
3631
|
+
model: typeof entry.model === "string" ? entry.model : null,
|
|
3632
|
+
isDefault: Boolean(entry.isDefault),
|
|
3633
|
+
status: local?.status ?? null,
|
|
3634
|
+
currentTask: local?.currentTask ?? null,
|
|
3635
|
+
runId: local?.runId ?? null,
|
|
3636
|
+
startedAt: local?.startedAt ?? null,
|
|
3637
|
+
blockers: local?.blockers ?? [],
|
|
3638
|
+
context,
|
|
3639
|
+
run,
|
|
3640
|
+
};
|
|
3641
|
+
});
|
|
3642
|
+
sendJson(res, 200, {
|
|
3643
|
+
generatedAt: new Date().toISOString(),
|
|
3644
|
+
agents,
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
catch (err) {
|
|
3648
|
+
sendJson(res, 500, {
|
|
3649
|
+
error: safeErrorMessage(err),
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
return true;
|
|
3653
|
+
}
|
|
1585
3654
|
case "activity":
|
|
1586
3655
|
sendJson(res, 200, formatActivity(getSnapshot()));
|
|
1587
3656
|
return true;
|
|
@@ -1591,6 +3660,229 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1591
3660
|
case "onboarding":
|
|
1592
3661
|
sendJson(res, 200, getOnboardingState(await onboarding.getStatus()));
|
|
1593
3662
|
return true;
|
|
3663
|
+
case "mission-control/auto-continue/status": {
|
|
3664
|
+
const initiativeId = searchParams.get("initiative_id") ??
|
|
3665
|
+
searchParams.get("initiativeId") ??
|
|
3666
|
+
"";
|
|
3667
|
+
const id = initiativeId.trim();
|
|
3668
|
+
if (!id) {
|
|
3669
|
+
sendJson(res, 400, {
|
|
3670
|
+
ok: false,
|
|
3671
|
+
error: "Query parameter 'initiative_id' is required.",
|
|
3672
|
+
});
|
|
3673
|
+
return true;
|
|
3674
|
+
}
|
|
3675
|
+
const run = autoContinueRuns.get(id) ?? null;
|
|
3676
|
+
sendJson(res, 200, {
|
|
3677
|
+
ok: true,
|
|
3678
|
+
initiativeId: id,
|
|
3679
|
+
run,
|
|
3680
|
+
defaults: {
|
|
3681
|
+
tokenBudget: defaultAutoContinueTokenBudget(),
|
|
3682
|
+
tickMs: AUTO_CONTINUE_TICK_MS,
|
|
3683
|
+
},
|
|
3684
|
+
});
|
|
3685
|
+
return true;
|
|
3686
|
+
}
|
|
3687
|
+
case "billing/status": {
|
|
3688
|
+
if (method !== "GET") {
|
|
3689
|
+
sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
3690
|
+
return true;
|
|
3691
|
+
}
|
|
3692
|
+
try {
|
|
3693
|
+
const status = await client.getBillingStatus();
|
|
3694
|
+
sendJson(res, 200, { ok: true, data: status });
|
|
3695
|
+
}
|
|
3696
|
+
catch (err) {
|
|
3697
|
+
sendJson(res, 200, { ok: false, error: safeErrorMessage(err) });
|
|
3698
|
+
}
|
|
3699
|
+
return true;
|
|
3700
|
+
}
|
|
3701
|
+
case "billing/checkout": {
|
|
3702
|
+
if (method !== "POST") {
|
|
3703
|
+
sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
3704
|
+
return true;
|
|
3705
|
+
}
|
|
3706
|
+
const basePricingUrl = `${client.getBaseUrl().replace(/\/+$/, "")}/pricing`;
|
|
3707
|
+
try {
|
|
3708
|
+
const payload = await parseJsonRequest(req);
|
|
3709
|
+
const planIdRaw = (pickString(payload, ["planId", "plan_id", "plan"]) ?? "starter").trim().toLowerCase();
|
|
3710
|
+
const billingCycleRaw = (pickString(payload, ["billingCycle", "billing_cycle"]) ?? "monthly").trim().toLowerCase();
|
|
3711
|
+
const planId = planIdRaw === "team" || planIdRaw === "enterprise" ? planIdRaw : "starter";
|
|
3712
|
+
const billingCycle = billingCycleRaw === "annual" ? "annual" : "monthly";
|
|
3713
|
+
const result = await client.createBillingCheckout({
|
|
3714
|
+
planId,
|
|
3715
|
+
billingCycle,
|
|
3716
|
+
});
|
|
3717
|
+
const url = result?.url ?? result?.checkout_url ?? null;
|
|
3718
|
+
sendJson(res, 200, { ok: true, data: { url: url ?? basePricingUrl } });
|
|
3719
|
+
}
|
|
3720
|
+
catch (err) {
|
|
3721
|
+
// If the remote billing endpoints are not deployed yet, degrade gracefully.
|
|
3722
|
+
sendJson(res, 200, { ok: true, data: { url: basePricingUrl } });
|
|
3723
|
+
}
|
|
3724
|
+
return true;
|
|
3725
|
+
}
|
|
3726
|
+
case "billing/portal": {
|
|
3727
|
+
if (method !== "POST") {
|
|
3728
|
+
sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
3729
|
+
return true;
|
|
3730
|
+
}
|
|
3731
|
+
const basePricingUrl = `${client.getBaseUrl().replace(/\/+$/, "")}/pricing`;
|
|
3732
|
+
try {
|
|
3733
|
+
const result = await client.createBillingPortal();
|
|
3734
|
+
const url = result?.url ?? null;
|
|
3735
|
+
sendJson(res, 200, { ok: true, data: { url: url ?? basePricingUrl } });
|
|
3736
|
+
}
|
|
3737
|
+
catch (err) {
|
|
3738
|
+
sendJson(res, 200, { ok: true, data: { url: basePricingUrl } });
|
|
3739
|
+
}
|
|
3740
|
+
return true;
|
|
3741
|
+
}
|
|
3742
|
+
case "settings/byok": {
|
|
3743
|
+
const stored = readByokKeys();
|
|
3744
|
+
const effectiveOpenai = stored?.openaiApiKey ?? process.env.OPENAI_API_KEY ?? null;
|
|
3745
|
+
const effectiveAnthropic = stored?.anthropicApiKey ?? process.env.ANTHROPIC_API_KEY ?? null;
|
|
3746
|
+
const effectiveOpenrouter = stored?.openrouterApiKey ?? process.env.OPENROUTER_API_KEY ?? null;
|
|
3747
|
+
const toProvider = (input) => {
|
|
3748
|
+
const hasStored = typeof input.storedValue === "string" && input.storedValue.trim().length > 0;
|
|
3749
|
+
const hasEnv = typeof input.envValue === "string" && input.envValue.trim().length > 0;
|
|
3750
|
+
const source = hasStored ? "stored" : hasEnv ? "env" : "none";
|
|
3751
|
+
return {
|
|
3752
|
+
configured: Boolean(input.effective && input.effective.trim().length > 0),
|
|
3753
|
+
source,
|
|
3754
|
+
masked: maskSecret(input.effective),
|
|
3755
|
+
};
|
|
3756
|
+
};
|
|
3757
|
+
if (method === "POST") {
|
|
3758
|
+
try {
|
|
3759
|
+
const payload = await parseJsonRequest(req);
|
|
3760
|
+
const updates = {};
|
|
3761
|
+
const setIfPresent = (key, aliases) => {
|
|
3762
|
+
for (const alias of aliases) {
|
|
3763
|
+
if (!Object.prototype.hasOwnProperty.call(payload, alias))
|
|
3764
|
+
continue;
|
|
3765
|
+
const raw = payload[alias];
|
|
3766
|
+
if (raw === null) {
|
|
3767
|
+
updates[key] = null;
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
if (typeof raw === "string") {
|
|
3771
|
+
updates[key] = raw;
|
|
3772
|
+
return;
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
};
|
|
3776
|
+
setIfPresent("openaiApiKey", ["openaiApiKey", "openai_api_key", "openaiKey", "openai_key"]);
|
|
3777
|
+
setIfPresent("anthropicApiKey", [
|
|
3778
|
+
"anthropicApiKey",
|
|
3779
|
+
"anthropic_api_key",
|
|
3780
|
+
"anthropicKey",
|
|
3781
|
+
"anthropic_key",
|
|
3782
|
+
]);
|
|
3783
|
+
setIfPresent("openrouterApiKey", [
|
|
3784
|
+
"openrouterApiKey",
|
|
3785
|
+
"openrouter_api_key",
|
|
3786
|
+
"openrouterKey",
|
|
3787
|
+
"openrouter_key",
|
|
3788
|
+
]);
|
|
3789
|
+
const saved = writeByokKeys(updates);
|
|
3790
|
+
const nextEffectiveOpenai = saved.openaiApiKey ?? process.env.OPENAI_API_KEY ?? null;
|
|
3791
|
+
const nextEffectiveAnthropic = saved.anthropicApiKey ?? process.env.ANTHROPIC_API_KEY ?? null;
|
|
3792
|
+
const nextEffectiveOpenrouter = saved.openrouterApiKey ?? process.env.OPENROUTER_API_KEY ?? null;
|
|
3793
|
+
sendJson(res, 200, {
|
|
3794
|
+
ok: true,
|
|
3795
|
+
updatedAt: saved.updatedAt,
|
|
3796
|
+
providers: {
|
|
3797
|
+
openai: toProvider({
|
|
3798
|
+
storedValue: saved.openaiApiKey,
|
|
3799
|
+
envValue: process.env.OPENAI_API_KEY,
|
|
3800
|
+
effective: nextEffectiveOpenai,
|
|
3801
|
+
}),
|
|
3802
|
+
anthropic: toProvider({
|
|
3803
|
+
storedValue: saved.anthropicApiKey,
|
|
3804
|
+
envValue: process.env.ANTHROPIC_API_KEY,
|
|
3805
|
+
effective: nextEffectiveAnthropic,
|
|
3806
|
+
}),
|
|
3807
|
+
openrouter: toProvider({
|
|
3808
|
+
storedValue: saved.openrouterApiKey,
|
|
3809
|
+
envValue: process.env.OPENROUTER_API_KEY,
|
|
3810
|
+
effective: nextEffectiveOpenrouter,
|
|
3811
|
+
}),
|
|
3812
|
+
},
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3815
|
+
catch (err) {
|
|
3816
|
+
sendJson(res, 500, { ok: false, error: safeErrorMessage(err) });
|
|
3817
|
+
}
|
|
3818
|
+
return true;
|
|
3819
|
+
}
|
|
3820
|
+
sendJson(res, 200, {
|
|
3821
|
+
ok: true,
|
|
3822
|
+
updatedAt: stored?.updatedAt ?? null,
|
|
3823
|
+
providers: {
|
|
3824
|
+
openai: toProvider({
|
|
3825
|
+
storedValue: stored?.openaiApiKey,
|
|
3826
|
+
envValue: process.env.OPENAI_API_KEY,
|
|
3827
|
+
effective: effectiveOpenai,
|
|
3828
|
+
}),
|
|
3829
|
+
anthropic: toProvider({
|
|
3830
|
+
storedValue: stored?.anthropicApiKey,
|
|
3831
|
+
envValue: process.env.ANTHROPIC_API_KEY,
|
|
3832
|
+
effective: effectiveAnthropic,
|
|
3833
|
+
}),
|
|
3834
|
+
openrouter: toProvider({
|
|
3835
|
+
storedValue: stored?.openrouterApiKey,
|
|
3836
|
+
envValue: process.env.OPENROUTER_API_KEY,
|
|
3837
|
+
effective: effectiveOpenrouter,
|
|
3838
|
+
}),
|
|
3839
|
+
},
|
|
3840
|
+
});
|
|
3841
|
+
return true;
|
|
3842
|
+
}
|
|
3843
|
+
case "settings/byok/health": {
|
|
3844
|
+
let agentId = searchParams.get("agentId") ??
|
|
3845
|
+
searchParams.get("agent_id") ??
|
|
3846
|
+
"";
|
|
3847
|
+
agentId = agentId.trim();
|
|
3848
|
+
if (!agentId) {
|
|
3849
|
+
try {
|
|
3850
|
+
const agents = await listAgents();
|
|
3851
|
+
const defaultAgent = agents.find((entry) => Boolean(entry.isDefault)) ?? agents[0] ?? null;
|
|
3852
|
+
const candidate = defaultAgent && typeof defaultAgent.id === "string" ? defaultAgent.id.trim() : "";
|
|
3853
|
+
if (candidate)
|
|
3854
|
+
agentId = candidate;
|
|
3855
|
+
}
|
|
3856
|
+
catch {
|
|
3857
|
+
// ignore
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
if (!agentId)
|
|
3861
|
+
agentId = "main";
|
|
3862
|
+
const providers = {};
|
|
3863
|
+
for (const provider of ["openai", "anthropic", "openrouter"]) {
|
|
3864
|
+
try {
|
|
3865
|
+
const models = await listOpenClawProviderModels({ agentId, provider });
|
|
3866
|
+
providers[provider] = {
|
|
3867
|
+
ok: true,
|
|
3868
|
+
modelCount: models.length,
|
|
3869
|
+
sample: models.slice(0, 4).map((model) => model.key),
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3872
|
+
catch (err) {
|
|
3873
|
+
providers[provider] = {
|
|
3874
|
+
ok: false,
|
|
3875
|
+
error: safeErrorMessage(err),
|
|
3876
|
+
};
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
sendJson(res, 200, {
|
|
3880
|
+
ok: true,
|
|
3881
|
+
agentId,
|
|
3882
|
+
providers,
|
|
3883
|
+
});
|
|
3884
|
+
return true;
|
|
3885
|
+
}
|
|
1594
3886
|
case "mission-control/graph": {
|
|
1595
3887
|
const initiativeId = searchParams.get("initiative_id") ??
|
|
1596
3888
|
searchParams.get("initiativeId");
|
|
@@ -1614,7 +3906,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1614
3906
|
case "entities": {
|
|
1615
3907
|
if (method === "POST") {
|
|
1616
3908
|
try {
|
|
1617
|
-
const payload =
|
|
3909
|
+
const payload = await parseJsonRequest(req);
|
|
1618
3910
|
const type = pickString(payload, ["type"]);
|
|
1619
3911
|
const title = pickString(payload, ["title", "name"]);
|
|
1620
3912
|
if (!type || !title) {
|
|
@@ -1660,7 +3952,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1660
3952
|
}
|
|
1661
3953
|
if (method === "PATCH") {
|
|
1662
3954
|
try {
|
|
1663
|
-
const payload =
|
|
3955
|
+
const payload = await parseJsonRequest(req);
|
|
1664
3956
|
const type = pickString(payload, ["type"]);
|
|
1665
3957
|
const id = pickString(payload, ["id"]);
|
|
1666
3958
|
if (!type || !id) {
|
|
@@ -1709,6 +4001,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1709
4001
|
}
|
|
1710
4002
|
return true;
|
|
1711
4003
|
}
|
|
4004
|
+
case "dashboard-bundle":
|
|
1712
4005
|
case "live/snapshot": {
|
|
1713
4006
|
const sessionsLimit = parsePositiveInt(searchParams.get("sessionsLimit") ?? searchParams.get("sessions_limit"), 320);
|
|
1714
4007
|
const activityLimit = parsePositiveInt(searchParams.get("activityLimit") ?? searchParams.get("activity_limit"), 600);
|
|
@@ -1719,6 +4012,49 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1719
4012
|
const decisionStatus = searchParams.get("status") ?? "pending";
|
|
1720
4013
|
const includeIdleRaw = searchParams.get("include_idle");
|
|
1721
4014
|
const includeIdle = includeIdleRaw === null ? undefined : includeIdleRaw !== "false";
|
|
4015
|
+
const degraded = [];
|
|
4016
|
+
const agentContexts = readAgentContexts().agents;
|
|
4017
|
+
const scopedAgentIds = getScopedAgentIds(agentContexts);
|
|
4018
|
+
let outboxStatus = null;
|
|
4019
|
+
try {
|
|
4020
|
+
if (diagnostics?.getHealth) {
|
|
4021
|
+
const health = await diagnostics.getHealth({ probeRemote: false });
|
|
4022
|
+
if (health && typeof health === "object") {
|
|
4023
|
+
const maybeOutbox = health.outbox;
|
|
4024
|
+
if (maybeOutbox && typeof maybeOutbox === "object") {
|
|
4025
|
+
outboxStatus = maybeOutbox;
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
if (!outboxStatus) {
|
|
4030
|
+
const outbox = await outboxAdapter.readSummary();
|
|
4031
|
+
outboxStatus = {
|
|
4032
|
+
pendingTotal: outbox.pendingTotal,
|
|
4033
|
+
pendingByQueue: outbox.pendingByQueue,
|
|
4034
|
+
oldestEventAt: outbox.oldestEventAt,
|
|
4035
|
+
newestEventAt: outbox.newestEventAt,
|
|
4036
|
+
replayStatus: "idle",
|
|
4037
|
+
lastReplayAttemptAt: null,
|
|
4038
|
+
lastReplaySuccessAt: null,
|
|
4039
|
+
lastReplayFailureAt: null,
|
|
4040
|
+
lastReplayError: null,
|
|
4041
|
+
};
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
catch (err) {
|
|
4045
|
+
degraded.push(`outbox status unavailable (${safeErrorMessage(err)})`);
|
|
4046
|
+
outboxStatus = {
|
|
4047
|
+
pendingTotal: 0,
|
|
4048
|
+
pendingByQueue: {},
|
|
4049
|
+
oldestEventAt: null,
|
|
4050
|
+
newestEventAt: null,
|
|
4051
|
+
replayStatus: "idle",
|
|
4052
|
+
lastReplayAttemptAt: null,
|
|
4053
|
+
lastReplaySuccessAt: null,
|
|
4054
|
+
lastReplayFailureAt: null,
|
|
4055
|
+
lastReplayError: null,
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
1722
4058
|
let localSnapshot = null;
|
|
1723
4059
|
const ensureLocalSnapshot = async (minimumLimit) => {
|
|
1724
4060
|
if (!localSnapshot || localSnapshot.sessions.length < minimumLimit) {
|
|
@@ -1746,7 +4082,6 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1746
4082
|
includeIdle,
|
|
1747
4083
|
}),
|
|
1748
4084
|
]);
|
|
1749
|
-
const degraded = [];
|
|
1750
4085
|
// sessions
|
|
1751
4086
|
let sessions = {
|
|
1752
4087
|
nodes: [],
|
|
@@ -1761,6 +4096,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1761
4096
|
degraded.push(`sessions unavailable (${safeErrorMessage(sessionsResult.reason)})`);
|
|
1762
4097
|
try {
|
|
1763
4098
|
let local = toLocalSessionTree(await ensureLocalSnapshot(Math.max(sessionsLimit, 200)), sessionsLimit);
|
|
4099
|
+
local = applyAgentContextsToSessionTree(local, agentContexts);
|
|
1764
4100
|
if (initiative && initiative.trim().length > 0) {
|
|
1765
4101
|
const filteredNodes = local.nodes.filter((node) => node.initiativeId === initiative || node.groupId === initiative);
|
|
1766
4102
|
const filteredIds = new Set(filteredNodes.map((node) => node.id));
|
|
@@ -1799,6 +4135,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1799
4135
|
filtered = filtered.filter((item) => Date.parse(item.timestamp) >= sinceEpoch);
|
|
1800
4136
|
}
|
|
1801
4137
|
}
|
|
4138
|
+
filtered = applyAgentContextsToActivity(filtered, agentContexts);
|
|
1802
4139
|
activity = filtered.slice(0, activityLimit);
|
|
1803
4140
|
}
|
|
1804
4141
|
catch (localErr) {
|
|
@@ -1852,9 +4189,51 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1852
4189
|
degraded.push(`agents local fallback failed (${safeErrorMessage(localErr)})`);
|
|
1853
4190
|
}
|
|
1854
4191
|
}
|
|
4192
|
+
// Merge locally-launched OpenClaw agent sessions/activity into the snapshot so
|
|
4193
|
+
// the UI reflects one-click launches even when the cloud reporting plane is reachable.
|
|
4194
|
+
if (scopedAgentIds.size > 0) {
|
|
4195
|
+
try {
|
|
4196
|
+
const minimum = Math.max(Math.max(sessionsLimit, activityLimit), 240);
|
|
4197
|
+
const snapshot = await ensureLocalSnapshot(minimum);
|
|
4198
|
+
const scopedSnapshot = {
|
|
4199
|
+
...snapshot,
|
|
4200
|
+
sessions: snapshot.sessions.filter((session) => Boolean(session.agentId && scopedAgentIds.has(session.agentId))),
|
|
4201
|
+
agents: snapshot.agents.filter((agent) => scopedAgentIds.has(agent.id)),
|
|
4202
|
+
};
|
|
4203
|
+
// Sessions
|
|
4204
|
+
let localSessions = applyAgentContextsToSessionTree(toLocalSessionTree(scopedSnapshot, sessionsLimit), agentContexts);
|
|
4205
|
+
if (initiative && initiative.trim().length > 0) {
|
|
4206
|
+
const filteredNodes = localSessions.nodes.filter((node) => node.initiativeId === initiative || node.groupId === initiative);
|
|
4207
|
+
const filteredIds = new Set(filteredNodes.map((node) => node.id));
|
|
4208
|
+
const filteredGroupIds = new Set(filteredNodes.map((node) => node.groupId));
|
|
4209
|
+
localSessions = {
|
|
4210
|
+
nodes: filteredNodes,
|
|
4211
|
+
edges: localSessions.edges.filter((edge) => filteredIds.has(edge.parentId) && filteredIds.has(edge.childId)),
|
|
4212
|
+
groups: localSessions.groups.filter((group) => filteredGroupIds.has(group.id)),
|
|
4213
|
+
};
|
|
4214
|
+
}
|
|
4215
|
+
sessions = mergeSessionTrees(sessions, localSessions);
|
|
4216
|
+
// Activity
|
|
4217
|
+
const localActivity = await toLocalLiveActivity(scopedSnapshot, Math.max(activityLimit, 240));
|
|
4218
|
+
let localItems = applyAgentContextsToActivity(localActivity.activities, agentContexts);
|
|
4219
|
+
if (run && run.trim().length > 0) {
|
|
4220
|
+
localItems = localItems.filter((item) => item.runId === run);
|
|
4221
|
+
}
|
|
4222
|
+
if (since && since.trim().length > 0) {
|
|
4223
|
+
const sinceEpoch = Date.parse(since);
|
|
4224
|
+
if (Number.isFinite(sinceEpoch)) {
|
|
4225
|
+
localItems = localItems.filter((item) => Date.parse(item.timestamp) >= sinceEpoch);
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
activity = mergeActivities(activity, localItems, activityLimit);
|
|
4229
|
+
}
|
|
4230
|
+
catch (err) {
|
|
4231
|
+
degraded.push(`local agent merge failed (${safeErrorMessage(err)})`);
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
1855
4234
|
// include locally buffered events so offline-generated actions are visible
|
|
1856
4235
|
try {
|
|
1857
|
-
const buffered = await
|
|
4236
|
+
const buffered = await outboxAdapter.readAllItems();
|
|
1858
4237
|
if (buffered.length > 0) {
|
|
1859
4238
|
const merged = [...activity, ...buffered]
|
|
1860
4239
|
.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp))
|
|
@@ -1879,6 +4258,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1879
4258
|
handoffs,
|
|
1880
4259
|
decisions,
|
|
1881
4260
|
agents,
|
|
4261
|
+
outbox: outboxStatus,
|
|
1882
4262
|
generatedAt: new Date().toISOString(),
|
|
1883
4263
|
degraded: degraded.length > 0 ? degraded : undefined,
|
|
1884
4264
|
});
|
|
@@ -1905,6 +4285,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1905
4285
|
: undefined;
|
|
1906
4286
|
const limit = Number.isFinite(limitRaw) ? Math.max(1, Number(limitRaw)) : 100;
|
|
1907
4287
|
let local = toLocalSessionTree(await loadLocalOpenClawSnapshot(Math.max(limit, 200)), limit);
|
|
4288
|
+
local = applyAgentContextsToSessionTree(local, readAgentContexts().agents);
|
|
1908
4289
|
if (initiative && initiative.trim().length > 0) {
|
|
1909
4290
|
const filteredNodes = local.nodes.filter((node) => node.initiativeId === initiative || node.groupId === initiative);
|
|
1910
4291
|
const filteredIds = new Set(filteredNodes.map((node) => node.id));
|
|
@@ -1966,8 +4347,9 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
1966
4347
|
};
|
|
1967
4348
|
}
|
|
1968
4349
|
}
|
|
4350
|
+
const activitiesWithContexts = applyAgentContextsToActivity(local.activities, readAgentContexts().agents);
|
|
1969
4351
|
sendJson(res, 200, {
|
|
1970
|
-
activities:
|
|
4352
|
+
activities: activitiesWithContexts.slice(0, limit),
|
|
1971
4353
|
total: local.total,
|
|
1972
4354
|
});
|
|
1973
4355
|
}
|
|
@@ -2014,7 +4396,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
2014
4396
|
return true;
|
|
2015
4397
|
}
|
|
2016
4398
|
try {
|
|
2017
|
-
const payload =
|
|
4399
|
+
const payload = await parseJsonRequest(req);
|
|
2018
4400
|
const text = pickString(payload, ["text", "summary", "detail", "content"]);
|
|
2019
4401
|
if (!text) {
|
|
2020
4402
|
sendJson(res, 400, { error: "text is required" });
|
|
@@ -2166,17 +4548,26 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
2166
4548
|
let closed = false;
|
|
2167
4549
|
let streamOpened = false;
|
|
2168
4550
|
let idleTimer = null;
|
|
4551
|
+
let heartbeatTimer = null;
|
|
4552
|
+
let heartbeatBackpressure = false;
|
|
2169
4553
|
const clearIdleTimer = () => {
|
|
2170
4554
|
if (idleTimer) {
|
|
2171
4555
|
clearTimeout(idleTimer);
|
|
2172
4556
|
idleTimer = null;
|
|
2173
4557
|
}
|
|
2174
4558
|
};
|
|
4559
|
+
const clearHeartbeatTimer = () => {
|
|
4560
|
+
if (heartbeatTimer) {
|
|
4561
|
+
clearInterval(heartbeatTimer);
|
|
4562
|
+
heartbeatTimer = null;
|
|
4563
|
+
}
|
|
4564
|
+
};
|
|
2175
4565
|
const closeStream = () => {
|
|
2176
4566
|
if (closed)
|
|
2177
4567
|
return;
|
|
2178
4568
|
closed = true;
|
|
2179
4569
|
clearIdleTimer();
|
|
4570
|
+
clearHeartbeatTimer();
|
|
2180
4571
|
streamAbortController.abort();
|
|
2181
4572
|
if (reader) {
|
|
2182
4573
|
void reader.cancel().catch(() => undefined);
|
|
@@ -2192,12 +4583,14 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
2192
4583
|
}, STREAM_IDLE_TIMEOUT_MS);
|
|
2193
4584
|
};
|
|
2194
4585
|
try {
|
|
4586
|
+
const includeUserHeader = Boolean(config.userId && config.userId.trim().length > 0) &&
|
|
4587
|
+
!isUserScopedApiKey(config.apiKey);
|
|
2195
4588
|
const upstream = await fetch(target, {
|
|
2196
4589
|
method: "GET",
|
|
2197
4590
|
headers: {
|
|
2198
4591
|
Authorization: `Bearer ${config.apiKey}`,
|
|
2199
4592
|
Accept: "text/event-stream",
|
|
2200
|
-
...(
|
|
4593
|
+
...(includeUserHeader
|
|
2201
4594
|
? { "X-Orgx-User-Id": config.userId }
|
|
2202
4595
|
: {}),
|
|
2203
4596
|
},
|
|
@@ -2220,9 +4613,36 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
2220
4613
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
2221
4614
|
"Cache-Control": "no-cache, no-transform",
|
|
2222
4615
|
Connection: "keep-alive",
|
|
4616
|
+
...SECURITY_HEADERS,
|
|
2223
4617
|
...CORS_HEADERS,
|
|
2224
4618
|
});
|
|
2225
4619
|
streamOpened = true;
|
|
4620
|
+
// Heartbeat comments keep intermediary proxies from timing out idle SSE.
|
|
4621
|
+
// They also prevent the dashboard from flickering into reconnect mode
|
|
4622
|
+
// during long quiet periods.
|
|
4623
|
+
heartbeatTimer = setInterval(() => {
|
|
4624
|
+
if (closed || heartbeatBackpressure)
|
|
4625
|
+
return;
|
|
4626
|
+
try {
|
|
4627
|
+
// Keepalive comment line (single newline to avoid terminating an upstream event mid-chunk).
|
|
4628
|
+
const accepted = write(Buffer.from(`: ping ${Date.now()}\n`, "utf8"));
|
|
4629
|
+
resetIdleTimer();
|
|
4630
|
+
if (accepted === false) {
|
|
4631
|
+
heartbeatBackpressure = true;
|
|
4632
|
+
if (typeof res.once === "function") {
|
|
4633
|
+
res.once("drain", () => {
|
|
4634
|
+
heartbeatBackpressure = false;
|
|
4635
|
+
if (!closed)
|
|
4636
|
+
resetIdleTimer();
|
|
4637
|
+
});
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
catch {
|
|
4642
|
+
closeStream();
|
|
4643
|
+
}
|
|
4644
|
+
}, 20_000);
|
|
4645
|
+
heartbeatTimer.unref?.();
|
|
2226
4646
|
if (!upstream.body) {
|
|
2227
4647
|
closeStream();
|
|
2228
4648
|
return true;
|
|
@@ -2306,6 +4726,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
2306
4726
|
if (!dashboardEnabled) {
|
|
2307
4727
|
res.writeHead(404, {
|
|
2308
4728
|
"Content-Type": "text/plain",
|
|
4729
|
+
...SECURITY_HEADERS,
|
|
2309
4730
|
...CORS_HEADERS,
|
|
2310
4731
|
});
|
|
2311
4732
|
res.end("Dashboard is disabled");
|
|
@@ -2317,8 +4738,14 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
2317
4738
|
// Static assets: /orgx/live/assets/* → dashboard/dist/assets/*
|
|
2318
4739
|
// Hashed filenames get long-lived cache
|
|
2319
4740
|
if (subPath.startsWith("assets/")) {
|
|
2320
|
-
const assetPath =
|
|
2321
|
-
|
|
4741
|
+
const assetPath = resolveSafeDistPath(subPath);
|
|
4742
|
+
let isWithinAssetsDir = false;
|
|
4743
|
+
if (assetPath) {
|
|
4744
|
+
isWithinAssetsDir =
|
|
4745
|
+
assetPath === RESOLVED_DIST_ASSETS_DIR ||
|
|
4746
|
+
assetPath.startsWith(`${RESOLVED_DIST_ASSETS_DIR}${sep}`);
|
|
4747
|
+
}
|
|
4748
|
+
if (assetPath && isWithinAssetsDir && existsSync(assetPath)) {
|
|
2322
4749
|
sendFile(res, assetPath, "public, max-age=31536000, immutable");
|
|
2323
4750
|
}
|
|
2324
4751
|
else {
|
|
@@ -2327,9 +4754,9 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
2327
4754
|
return true;
|
|
2328
4755
|
}
|
|
2329
4756
|
// Check for an exact file match (e.g. favicon, manifest)
|
|
2330
|
-
if (subPath
|
|
2331
|
-
const filePath =
|
|
2332
|
-
if (existsSync(filePath)) {
|
|
4757
|
+
if (subPath) {
|
|
4758
|
+
const filePath = resolveSafeDistPath(subPath);
|
|
4759
|
+
if (filePath && existsSync(filePath)) {
|
|
2333
4760
|
sendFile(res, filePath, "no-cache");
|
|
2334
4761
|
return true;
|
|
2335
4762
|
}
|
|
@@ -2343,6 +4770,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding) {
|
|
|
2343
4770
|
// Redirect to dashboard
|
|
2344
4771
|
res.writeHead(302, {
|
|
2345
4772
|
Location: "/orgx/live",
|
|
4773
|
+
...SECURITY_HEADERS,
|
|
2346
4774
|
...CORS_HEADERS,
|
|
2347
4775
|
});
|
|
2348
4776
|
res.end();
|