@useorgx/openclaw-plugin 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/MissionControlView-DVNfDWKZ.js +1 -0
- package/dashboard/dist/assets/{SessionInspector-Dq0Z5WMo.js → SessionInspector-BaqnAys4.js} +1 -1
- package/dashboard/dist/assets/index-B4Yix84X.js +212 -0
- package/dashboard/dist/assets/index-BWSvw1HR.css +1 -0
- package/dashboard/dist/assets/motion-x9c01cgK.js +9 -0
- package/dashboard/dist/index.html +3 -3
- package/dist/contracts/types.d.ts +33 -0
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/http-handler.d.ts.map +1 -1
- package/dist/http-handler.js +1307 -110
- package/dist/http-handler.js.map +1 -1
- package/dist/runtime-instance-store.d.ts +63 -0
- package/dist/runtime-instance-store.d.ts.map +1 -0
- package/dist/runtime-instance-store.js +363 -0
- package/dist/runtime-instance-store.js.map +1 -0
- package/package.json +1 -1
- package/dashboard/dist/assets/MissionControlView-CthHdl6R.js +0 -1
- package/dashboard/dist/assets/index-CoLgC4zE.js +0 -11
- package/dashboard/dist/assets/index-jfEYE0kO.css +0 -1
- package/dashboard/dist/assets/motion-CVDprFZg.js +0 -9
package/dist/http-handler.js
CHANGED
|
@@ -23,11 +23,13 @@ import { spawn } from "node:child_process";
|
|
|
23
23
|
import { createHash, randomUUID } from "node:crypto";
|
|
24
24
|
import { formatStatus, formatAgents, formatActivity, formatInitiatives, getOnboardingState, } from "./dashboard-api.js";
|
|
25
25
|
import { loadLocalOpenClawSnapshot, loadLocalTurnDetail, toLocalLiveActivity, toLocalLiveAgents, toLocalLiveInitiatives, toLocalSessionTree, } from "./local-openclaw.js";
|
|
26
|
+
import { appendToOutbox } from "./outbox.js";
|
|
26
27
|
import { defaultOutboxAdapter } from "./adapters/outbox.js";
|
|
27
28
|
import { readAgentContexts, upsertAgentContext } from "./agent-context-store.js";
|
|
28
29
|
import { getAgentRun, markAgentRunStopped, readAgentRuns, upsertAgentRun, } from "./agent-run-store.js";
|
|
29
30
|
import { readByokKeys, writeByokKeys } from "./byok-store.js";
|
|
30
31
|
import { computeMilestoneRollup, computeWorkstreamRollup, } from "./reporting/rollups.js";
|
|
32
|
+
import { listRuntimeInstances, resolveRuntimeHookToken, upsertRuntimeInstanceFromHook, } from "./runtime-instance-store.js";
|
|
31
33
|
// =============================================================================
|
|
32
34
|
// Helpers
|
|
33
35
|
// =============================================================================
|
|
@@ -38,6 +40,10 @@ function safeErrorMessage(err) {
|
|
|
38
40
|
return err;
|
|
39
41
|
return "Unexpected error";
|
|
40
42
|
}
|
|
43
|
+
function isUnauthorizedOrgxError(err) {
|
|
44
|
+
const message = safeErrorMessage(err).toLowerCase();
|
|
45
|
+
return message.includes("401") || message.includes("unauthorized");
|
|
46
|
+
}
|
|
41
47
|
function isUserScopedApiKey(apiKey) {
|
|
42
48
|
return apiKey.trim().toLowerCase().startsWith("oxk_");
|
|
43
49
|
}
|
|
@@ -477,6 +483,110 @@ function mergeActivities(base, extra, limit) {
|
|
|
477
483
|
}
|
|
478
484
|
return deduped;
|
|
479
485
|
}
|
|
486
|
+
function normalizeRuntimeSourceForReporting(value) {
|
|
487
|
+
if (value === "codex")
|
|
488
|
+
return "codex";
|
|
489
|
+
if (value === "claude-code")
|
|
490
|
+
return "claude-code";
|
|
491
|
+
if (value === "api")
|
|
492
|
+
return "api";
|
|
493
|
+
return "openclaw";
|
|
494
|
+
}
|
|
495
|
+
function normalizeHookPhase(value) {
|
|
496
|
+
const normalized = (value ?? "").trim().toLowerCase();
|
|
497
|
+
if (normalized === "intent")
|
|
498
|
+
return "intent";
|
|
499
|
+
if (normalized === "execution")
|
|
500
|
+
return "execution";
|
|
501
|
+
if (normalized === "blocked")
|
|
502
|
+
return "blocked";
|
|
503
|
+
if (normalized === "review")
|
|
504
|
+
return "review";
|
|
505
|
+
if (normalized === "handoff")
|
|
506
|
+
return "handoff";
|
|
507
|
+
if (normalized === "completed")
|
|
508
|
+
return "completed";
|
|
509
|
+
return "execution";
|
|
510
|
+
}
|
|
511
|
+
function normalizeRuntimeSource(value) {
|
|
512
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
513
|
+
if (normalized === "openclaw")
|
|
514
|
+
return "openclaw";
|
|
515
|
+
if (normalized === "codex")
|
|
516
|
+
return "codex";
|
|
517
|
+
if (normalized === "claude-code")
|
|
518
|
+
return "claude-code";
|
|
519
|
+
if (normalized === "api")
|
|
520
|
+
return "api";
|
|
521
|
+
return "unknown";
|
|
522
|
+
}
|
|
523
|
+
function runtimeMatchMaps(instances) {
|
|
524
|
+
const byRunId = new Map();
|
|
525
|
+
const byAgentInitiative = new Map();
|
|
526
|
+
for (const instance of instances) {
|
|
527
|
+
if (instance.runId && !byRunId.has(instance.runId)) {
|
|
528
|
+
byRunId.set(instance.runId, instance);
|
|
529
|
+
}
|
|
530
|
+
const agentId = instance.agentId?.trim() ?? "";
|
|
531
|
+
const initiativeId = instance.initiativeId?.trim() ?? "";
|
|
532
|
+
if (!agentId || !initiativeId)
|
|
533
|
+
continue;
|
|
534
|
+
const key = `${agentId}:${initiativeId}`;
|
|
535
|
+
if (!byAgentInitiative.has(key)) {
|
|
536
|
+
byAgentInitiative.set(key, instance);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return { byRunId, byAgentInitiative };
|
|
540
|
+
}
|
|
541
|
+
function enrichSessionsWithRuntime(input, instances) {
|
|
542
|
+
if (!Array.isArray(input.nodes) || input.nodes.length === 0)
|
|
543
|
+
return input;
|
|
544
|
+
if (instances.length === 0)
|
|
545
|
+
return input;
|
|
546
|
+
const { byRunId, byAgentInitiative } = runtimeMatchMaps(instances);
|
|
547
|
+
const nodes = input.nodes.map((node) => {
|
|
548
|
+
const byRun = node.runId ? byRunId.get(node.runId) ?? null : null;
|
|
549
|
+
const byAgent = !byRun && node.agentId && node.initiativeId
|
|
550
|
+
? byAgentInitiative.get(`${node.agentId}:${node.initiativeId}`) ?? null
|
|
551
|
+
: null;
|
|
552
|
+
const match = byRun ?? byAgent;
|
|
553
|
+
if (!match)
|
|
554
|
+
return node;
|
|
555
|
+
return {
|
|
556
|
+
...node,
|
|
557
|
+
runtimeClient: normalizeRuntimeSource(match.sourceClient),
|
|
558
|
+
runtimeLabel: match.displayName,
|
|
559
|
+
runtimeProvider: match.providerLogo,
|
|
560
|
+
instanceId: match.id,
|
|
561
|
+
lastHeartbeatAt: match.lastHeartbeatAt ?? null,
|
|
562
|
+
};
|
|
563
|
+
});
|
|
564
|
+
return { ...input, nodes };
|
|
565
|
+
}
|
|
566
|
+
function enrichActivityWithRuntime(input, instances) {
|
|
567
|
+
if (!Array.isArray(input) || input.length === 0)
|
|
568
|
+
return [];
|
|
569
|
+
if (instances.length === 0)
|
|
570
|
+
return input;
|
|
571
|
+
const { byRunId, byAgentInitiative } = runtimeMatchMaps(instances);
|
|
572
|
+
return input.map((item) => {
|
|
573
|
+
const byRun = item.runId ? byRunId.get(item.runId) ?? null : null;
|
|
574
|
+
const byAgent = !byRun && item.agentId && item.initiativeId
|
|
575
|
+
? byAgentInitiative.get(`${item.agentId}:${item.initiativeId}`) ?? null
|
|
576
|
+
: null;
|
|
577
|
+
const match = byRun ?? byAgent;
|
|
578
|
+
if (!match)
|
|
579
|
+
return item;
|
|
580
|
+
return {
|
|
581
|
+
...item,
|
|
582
|
+
runtimeClient: normalizeRuntimeSource(match.sourceClient),
|
|
583
|
+
runtimeLabel: match.displayName,
|
|
584
|
+
runtimeProvider: match.providerLogo,
|
|
585
|
+
instanceId: match.id,
|
|
586
|
+
lastHeartbeatAt: match.lastHeartbeatAt ?? null,
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
}
|
|
480
590
|
const ACTIVITY_HEADLINE_TIMEOUT_MS = 4_000;
|
|
481
591
|
const ACTIVITY_HEADLINE_CACHE_TTL_MS = 12 * 60 * 60_000;
|
|
482
592
|
const ACTIVITY_HEADLINE_CACHE_MAX = 1_000;
|
|
@@ -695,7 +805,7 @@ function contentType(filePath) {
|
|
|
695
805
|
// =============================================================================
|
|
696
806
|
const CORS_HEADERS = {
|
|
697
807
|
"Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
|
|
698
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-OrgX-Api-Key, X-API-Key, X-OrgX-User-Id",
|
|
808
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-OrgX-Api-Key, X-API-Key, X-OrgX-User-Id, X-OrgX-Hook-Token, X-Hook-Token",
|
|
699
809
|
Vary: "Origin",
|
|
700
810
|
};
|
|
701
811
|
const SECURITY_HEADERS = {
|
|
@@ -1481,6 +1591,17 @@ function isInProgressStatus(status) {
|
|
|
1481
1591
|
normalized === "running" ||
|
|
1482
1592
|
normalized === "queued");
|
|
1483
1593
|
}
|
|
1594
|
+
function isDispatchableWorkstreamStatus(status) {
|
|
1595
|
+
const normalized = status.toLowerCase();
|
|
1596
|
+
if (!normalized)
|
|
1597
|
+
return true;
|
|
1598
|
+
return !(normalized === "blocked" ||
|
|
1599
|
+
normalized === "done" ||
|
|
1600
|
+
normalized === "completed" ||
|
|
1601
|
+
normalized === "cancelled" ||
|
|
1602
|
+
normalized === "archived" ||
|
|
1603
|
+
normalized === "deleted");
|
|
1604
|
+
}
|
|
1484
1605
|
function isDoneStatus(status) {
|
|
1485
1606
|
const normalized = status.toLowerCase();
|
|
1486
1607
|
return (normalized === "done" ||
|
|
@@ -1933,7 +2054,56 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
1933
2054
|
});
|
|
1934
2055
|
}
|
|
1935
2056
|
catch {
|
|
1936
|
-
//
|
|
2057
|
+
// Fall back to local outbox so activity is still visible in Mission Control/Activity.
|
|
2058
|
+
try {
|
|
2059
|
+
const timestamp = new Date().toISOString();
|
|
2060
|
+
const runId = input.runId?.trim() ||
|
|
2061
|
+
input.correlationId?.trim() ||
|
|
2062
|
+
null;
|
|
2063
|
+
const activityItem = {
|
|
2064
|
+
id: randomUUID(),
|
|
2065
|
+
type: input.phase === "completed"
|
|
2066
|
+
? "run_completed"
|
|
2067
|
+
: input.phase === "blocked"
|
|
2068
|
+
? "run_failed"
|
|
2069
|
+
: "run_started",
|
|
2070
|
+
title: message,
|
|
2071
|
+
description: input.nextStep ?? null,
|
|
2072
|
+
agentId: (typeof input.metadata?.agent_id === "string"
|
|
2073
|
+
? input.metadata.agent_id
|
|
2074
|
+
: null) ?? null,
|
|
2075
|
+
agentName: (typeof input.metadata?.agent_name === "string"
|
|
2076
|
+
? input.metadata.agent_name
|
|
2077
|
+
: null) ?? null,
|
|
2078
|
+
runId,
|
|
2079
|
+
initiativeId,
|
|
2080
|
+
timestamp,
|
|
2081
|
+
phase: input.phase,
|
|
2082
|
+
summary: message,
|
|
2083
|
+
metadata: {
|
|
2084
|
+
...(input.metadata ?? {}),
|
|
2085
|
+
source: "openclaw_local_fallback",
|
|
2086
|
+
},
|
|
2087
|
+
};
|
|
2088
|
+
await appendToOutbox(initiativeId, {
|
|
2089
|
+
id: randomUUID(),
|
|
2090
|
+
type: "progress",
|
|
2091
|
+
timestamp,
|
|
2092
|
+
payload: {
|
|
2093
|
+
phase: input.phase,
|
|
2094
|
+
message,
|
|
2095
|
+
level: input.level ?? "info",
|
|
2096
|
+
runId,
|
|
2097
|
+
initiativeId,
|
|
2098
|
+
nextStep: input.nextStep ?? null,
|
|
2099
|
+
metadata: input.metadata ?? null,
|
|
2100
|
+
},
|
|
2101
|
+
activityItem,
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
catch {
|
|
2105
|
+
// best effort
|
|
2106
|
+
}
|
|
1937
2107
|
}
|
|
1938
2108
|
}
|
|
1939
2109
|
async function syncParentRollupsForTask(input) {
|
|
@@ -2008,8 +2178,71 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2008
2178
|
}
|
|
2009
2179
|
}
|
|
2010
2180
|
const autoContinueRuns = new Map();
|
|
2181
|
+
const localInitiativeStatusOverrides = new Map();
|
|
2011
2182
|
let autoContinueTickInFlight = false;
|
|
2012
2183
|
const AUTO_CONTINUE_TICK_MS = 2_500;
|
|
2184
|
+
const setLocalInitiativeStatusOverride = (initiativeId, status) => {
|
|
2185
|
+
const normalizedId = initiativeId.trim();
|
|
2186
|
+
if (!normalizedId)
|
|
2187
|
+
return;
|
|
2188
|
+
localInitiativeStatusOverrides.set(normalizedId, {
|
|
2189
|
+
status,
|
|
2190
|
+
updatedAt: new Date().toISOString(),
|
|
2191
|
+
});
|
|
2192
|
+
};
|
|
2193
|
+
const clearLocalInitiativeStatusOverride = (initiativeId) => {
|
|
2194
|
+
const normalizedId = initiativeId.trim();
|
|
2195
|
+
if (!normalizedId)
|
|
2196
|
+
return;
|
|
2197
|
+
localInitiativeStatusOverrides.delete(normalizedId);
|
|
2198
|
+
};
|
|
2199
|
+
const applyLocalInitiativeOverrides = (rows) => {
|
|
2200
|
+
const seenIds = new Set();
|
|
2201
|
+
const next = rows.map((row) => {
|
|
2202
|
+
const id = pickString(row, ["id"]);
|
|
2203
|
+
if (!id)
|
|
2204
|
+
return row;
|
|
2205
|
+
seenIds.add(id);
|
|
2206
|
+
const override = localInitiativeStatusOverrides.get(id);
|
|
2207
|
+
if (!override)
|
|
2208
|
+
return row;
|
|
2209
|
+
return {
|
|
2210
|
+
...row,
|
|
2211
|
+
status: override.status,
|
|
2212
|
+
updated_at: pickString(row, ["updated_at", "updatedAt"]) ?? override.updatedAt,
|
|
2213
|
+
};
|
|
2214
|
+
});
|
|
2215
|
+
for (const [id, override] of localInitiativeStatusOverrides.entries()) {
|
|
2216
|
+
if (seenIds.has(id))
|
|
2217
|
+
continue;
|
|
2218
|
+
next.push({
|
|
2219
|
+
id,
|
|
2220
|
+
title: `Initiative ${id.slice(0, 8)}`,
|
|
2221
|
+
name: `Initiative ${id.slice(0, 8)}`,
|
|
2222
|
+
summary: null,
|
|
2223
|
+
status: override.status,
|
|
2224
|
+
progress_pct: null,
|
|
2225
|
+
created_at: override.updatedAt,
|
|
2226
|
+
updated_at: override.updatedAt,
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
return next;
|
|
2230
|
+
};
|
|
2231
|
+
const applyLocalInitiativeOverrideToGraph = (graph) => {
|
|
2232
|
+
const override = localInitiativeStatusOverrides.get(graph.initiative.id) ?? null;
|
|
2233
|
+
if (!override)
|
|
2234
|
+
return graph;
|
|
2235
|
+
return {
|
|
2236
|
+
...graph,
|
|
2237
|
+
initiative: {
|
|
2238
|
+
...graph.initiative,
|
|
2239
|
+
status: override.status,
|
|
2240
|
+
},
|
|
2241
|
+
nodes: graph.nodes.map((node) => node.type === "initiative" && node.id === graph.initiative.id
|
|
2242
|
+
? { ...node, status: override.status }
|
|
2243
|
+
: node),
|
|
2244
|
+
};
|
|
2245
|
+
};
|
|
2013
2246
|
function normalizeTokenBudget(value, fallback) {
|
|
2014
2247
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2015
2248
|
return Math.max(1_000, Math.round(value));
|
|
@@ -2209,6 +2442,59 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2209
2442
|
// best effort
|
|
2210
2443
|
}
|
|
2211
2444
|
}
|
|
2445
|
+
async function dispatchFallbackWorkstreamTurn(input) {
|
|
2446
|
+
const now = new Date().toISOString();
|
|
2447
|
+
const sessionId = randomUUID();
|
|
2448
|
+
const message = [
|
|
2449
|
+
`Initiative: ${input.initiativeTitle}`,
|
|
2450
|
+
`Workstream: ${input.workstreamTitle}`,
|
|
2451
|
+
"",
|
|
2452
|
+
"Continue this workstream from the latest context.",
|
|
2453
|
+
"Identify and execute the next concrete task, then provide a concise progress summary.",
|
|
2454
|
+
].join("\n");
|
|
2455
|
+
await emitActivitySafe({
|
|
2456
|
+
initiativeId: input.initiativeId,
|
|
2457
|
+
correlationId: sessionId,
|
|
2458
|
+
phase: "execution",
|
|
2459
|
+
level: "info",
|
|
2460
|
+
message: `Next Up dispatched ${input.workstreamTitle}.`,
|
|
2461
|
+
metadata: {
|
|
2462
|
+
event: "next_up_manual_dispatch_started",
|
|
2463
|
+
agent_id: input.agentId,
|
|
2464
|
+
session_id: sessionId,
|
|
2465
|
+
workstream_id: input.workstreamId,
|
|
2466
|
+
workstream_title: input.workstreamTitle,
|
|
2467
|
+
fallback: true,
|
|
2468
|
+
},
|
|
2469
|
+
});
|
|
2470
|
+
upsertAgentContext({
|
|
2471
|
+
agentId: input.agentId,
|
|
2472
|
+
initiativeId: input.initiativeId,
|
|
2473
|
+
initiativeTitle: input.initiativeTitle,
|
|
2474
|
+
workstreamId: input.workstreamId,
|
|
2475
|
+
taskId: null,
|
|
2476
|
+
});
|
|
2477
|
+
const spawned = spawnAgentTurn({
|
|
2478
|
+
agentId: input.agentId,
|
|
2479
|
+
sessionId,
|
|
2480
|
+
message,
|
|
2481
|
+
});
|
|
2482
|
+
upsertAgentRun({
|
|
2483
|
+
runId: sessionId,
|
|
2484
|
+
agentId: input.agentId,
|
|
2485
|
+
pid: spawned.pid,
|
|
2486
|
+
message,
|
|
2487
|
+
provider: null,
|
|
2488
|
+
model: null,
|
|
2489
|
+
initiativeId: input.initiativeId,
|
|
2490
|
+
initiativeTitle: input.initiativeTitle,
|
|
2491
|
+
workstreamId: input.workstreamId,
|
|
2492
|
+
taskId: null,
|
|
2493
|
+
startedAt: now,
|
|
2494
|
+
status: "running",
|
|
2495
|
+
});
|
|
2496
|
+
return { sessionId, pid: spawned.pid };
|
|
2497
|
+
}
|
|
2212
2498
|
async function tickAutoContinueRun(run) {
|
|
2213
2499
|
if (run.status !== "running" && run.status !== "stopping")
|
|
2214
2500
|
return;
|
|
@@ -2317,7 +2603,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2317
2603
|
// 3) Pick next-up task and dispatch.
|
|
2318
2604
|
let graph;
|
|
2319
2605
|
try {
|
|
2320
|
-
graph = await buildMissionControlGraph(client, run.initiativeId);
|
|
2606
|
+
graph = applyLocalInitiativeOverrideToGraph(await buildMissionControlGraph(client, run.initiativeId));
|
|
2321
2607
|
}
|
|
2322
2608
|
catch (err) {
|
|
2323
2609
|
await stopAutoContinueRun({
|
|
@@ -2364,7 +2650,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2364
2650
|
}
|
|
2365
2651
|
if (node.workstreamId) {
|
|
2366
2652
|
const ws = nodeById.get(node.workstreamId);
|
|
2367
|
-
if (ws && !
|
|
2653
|
+
if (ws && !isDispatchableWorkstreamStatus(ws.status)) {
|
|
2368
2654
|
continue;
|
|
2369
2655
|
}
|
|
2370
2656
|
}
|
|
@@ -2407,6 +2693,21 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2407
2693
|
]
|
|
2408
2694
|
.filter((line) => typeof line === "string")
|
|
2409
2695
|
.join("\n");
|
|
2696
|
+
if (nextTaskNode.workstreamId) {
|
|
2697
|
+
const workstreamNode = nodeById.get(nextTaskNode.workstreamId);
|
|
2698
|
+
if (workstreamNode &&
|
|
2699
|
+
!isInProgressStatus(workstreamNode.status) &&
|
|
2700
|
+
isDispatchableWorkstreamStatus(workstreamNode.status)) {
|
|
2701
|
+
try {
|
|
2702
|
+
await client.updateEntity("workstream", workstreamNode.id, {
|
|
2703
|
+
status: "active",
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
catch {
|
|
2707
|
+
// best effort
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2410
2711
|
try {
|
|
2411
2712
|
await client.updateEntity("task", nextTaskNode.id, {
|
|
2412
2713
|
status: "in_progress",
|
|
@@ -2514,6 +2815,528 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2514
2815
|
autoContinueTickInFlight = false;
|
|
2515
2816
|
}
|
|
2516
2817
|
}
|
|
2818
|
+
function isInitiativeActiveStatus(status) {
|
|
2819
|
+
const normalized = (status ?? "").trim().toLowerCase();
|
|
2820
|
+
if (!normalized)
|
|
2821
|
+
return false;
|
|
2822
|
+
return !(normalized === "completed" ||
|
|
2823
|
+
normalized === "done" ||
|
|
2824
|
+
normalized === "archived" ||
|
|
2825
|
+
normalized === "deleted" ||
|
|
2826
|
+
normalized === "cancelled");
|
|
2827
|
+
}
|
|
2828
|
+
function runningAutoContinueForWorkstream(initiativeId, workstreamId) {
|
|
2829
|
+
const run = autoContinueRuns.get(initiativeId) ?? null;
|
|
2830
|
+
if (!run)
|
|
2831
|
+
return null;
|
|
2832
|
+
if (run.status !== "running" && run.status !== "stopping")
|
|
2833
|
+
return null;
|
|
2834
|
+
if (!Array.isArray(run.allowedWorkstreamIds) || run.allowedWorkstreamIds.length === 0) {
|
|
2835
|
+
return run;
|
|
2836
|
+
}
|
|
2837
|
+
return run.allowedWorkstreamIds.includes(workstreamId) ? run : null;
|
|
2838
|
+
}
|
|
2839
|
+
async function resolveAutoContinueUpgradeGate(agentId) {
|
|
2840
|
+
let requiresPremiumAutoContinue = false;
|
|
2841
|
+
try {
|
|
2842
|
+
const agents = await listAgents();
|
|
2843
|
+
const agentEntry = agents.find((entry) => String(entry.id ?? "").trim() === agentId) ??
|
|
2844
|
+
null;
|
|
2845
|
+
const agentModel = agentEntry && typeof agentEntry.model === "string"
|
|
2846
|
+
? agentEntry.model
|
|
2847
|
+
: null;
|
|
2848
|
+
requiresPremiumAutoContinue = modelImpliesByok(agentModel);
|
|
2849
|
+
}
|
|
2850
|
+
catch {
|
|
2851
|
+
// ignore
|
|
2852
|
+
}
|
|
2853
|
+
if (!requiresPremiumAutoContinue)
|
|
2854
|
+
return null;
|
|
2855
|
+
const billingStatus = await fetchBillingStatusSafe(client);
|
|
2856
|
+
if (!billingStatus || billingStatus.plan !== "free")
|
|
2857
|
+
return null;
|
|
2858
|
+
const pricingUrl = `${client.getBaseUrl().replace(/\/+$/, "")}/pricing`;
|
|
2859
|
+
return {
|
|
2860
|
+
code: "upgrade_required",
|
|
2861
|
+
error: "Auto-continue for BYOK agents requires a paid OrgX plan. Upgrade, then retry.",
|
|
2862
|
+
currentPlan: billingStatus.plan,
|
|
2863
|
+
requiredPlan: "starter",
|
|
2864
|
+
actions: {
|
|
2865
|
+
checkout: "/orgx/api/billing/checkout",
|
|
2866
|
+
portal: "/orgx/api/billing/portal",
|
|
2867
|
+
pricing: pricingUrl,
|
|
2868
|
+
},
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
async function startAutoContinueRun(input) {
|
|
2872
|
+
const now = new Date().toISOString();
|
|
2873
|
+
const existing = autoContinueRuns.get(input.initiativeId) ?? null;
|
|
2874
|
+
const run = existing ??
|
|
2875
|
+
{
|
|
2876
|
+
initiativeId: input.initiativeId,
|
|
2877
|
+
agentId: input.agentId,
|
|
2878
|
+
includeVerification: false,
|
|
2879
|
+
allowedWorkstreamIds: null,
|
|
2880
|
+
tokenBudget: defaultAutoContinueTokenBudget(),
|
|
2881
|
+
tokensUsed: 0,
|
|
2882
|
+
status: "running",
|
|
2883
|
+
stopReason: null,
|
|
2884
|
+
stopRequested: false,
|
|
2885
|
+
startedAt: now,
|
|
2886
|
+
stoppedAt: null,
|
|
2887
|
+
updatedAt: now,
|
|
2888
|
+
lastError: null,
|
|
2889
|
+
lastTaskId: null,
|
|
2890
|
+
lastRunId: null,
|
|
2891
|
+
activeTaskId: null,
|
|
2892
|
+
activeRunId: null,
|
|
2893
|
+
activeTaskTokenEstimate: null,
|
|
2894
|
+
};
|
|
2895
|
+
run.agentId = input.agentId;
|
|
2896
|
+
run.includeVerification = input.includeVerification;
|
|
2897
|
+
run.allowedWorkstreamIds = input.allowedWorkstreamIds;
|
|
2898
|
+
run.tokenBudget = normalizeTokenBudget(input.tokenBudget, run.tokenBudget || defaultAutoContinueTokenBudget());
|
|
2899
|
+
run.status = "running";
|
|
2900
|
+
run.stopReason = null;
|
|
2901
|
+
run.stopRequested = false;
|
|
2902
|
+
run.startedAt = now;
|
|
2903
|
+
run.stoppedAt = null;
|
|
2904
|
+
run.updatedAt = now;
|
|
2905
|
+
run.lastError = null;
|
|
2906
|
+
autoContinueRuns.set(input.initiativeId, run);
|
|
2907
|
+
try {
|
|
2908
|
+
await client.updateEntity("initiative", input.initiativeId, { status: "active" });
|
|
2909
|
+
}
|
|
2910
|
+
catch {
|
|
2911
|
+
// best effort
|
|
2912
|
+
}
|
|
2913
|
+
try {
|
|
2914
|
+
await updateInitiativeAutoContinueState({
|
|
2915
|
+
initiativeId: input.initiativeId,
|
|
2916
|
+
run,
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
catch {
|
|
2920
|
+
// best effort
|
|
2921
|
+
}
|
|
2922
|
+
return run;
|
|
2923
|
+
}
|
|
2924
|
+
async function buildNextUpQueue(input) {
|
|
2925
|
+
const degraded = [];
|
|
2926
|
+
const requestedInitiativeId = input?.initiativeId?.trim() || null;
|
|
2927
|
+
const initiativeTitleById = new Map();
|
|
2928
|
+
const initiativeStatusById = new Map();
|
|
2929
|
+
const initiativePriorityById = new Map();
|
|
2930
|
+
const snapshotInitiatives = formatInitiatives(getSnapshot());
|
|
2931
|
+
for (const initiative of snapshotInitiatives) {
|
|
2932
|
+
const id = initiative.id?.trim();
|
|
2933
|
+
if (!id)
|
|
2934
|
+
continue;
|
|
2935
|
+
initiativeTitleById.set(id, initiative.title);
|
|
2936
|
+
initiativeStatusById.set(id, initiative.status || "active");
|
|
2937
|
+
}
|
|
2938
|
+
const initiativeResult = await listEntitiesSafe(client, "initiative", { limit: 500 });
|
|
2939
|
+
if (initiativeResult.warning)
|
|
2940
|
+
degraded.push(initiativeResult.warning);
|
|
2941
|
+
const initiatives = initiativeResult.items;
|
|
2942
|
+
for (const entity of initiatives) {
|
|
2943
|
+
const record = entity;
|
|
2944
|
+
const id = pickString(record, ["id"]);
|
|
2945
|
+
if (!id)
|
|
2946
|
+
continue;
|
|
2947
|
+
const title = pickString(record, ["title", "name"]);
|
|
2948
|
+
const status = pickString(record, ["status"]);
|
|
2949
|
+
const priority = pickString(record, ["priority", "priority_label", "priorityLabel"]);
|
|
2950
|
+
if (title)
|
|
2951
|
+
initiativeTitleById.set(id, title);
|
|
2952
|
+
if (status)
|
|
2953
|
+
initiativeStatusById.set(id, status);
|
|
2954
|
+
if (priority)
|
|
2955
|
+
initiativePriorityById.set(id, priority);
|
|
2956
|
+
}
|
|
2957
|
+
for (const [initiativeId, override] of localInitiativeStatusOverrides.entries()) {
|
|
2958
|
+
initiativeStatusById.set(initiativeId, override.status);
|
|
2959
|
+
}
|
|
2960
|
+
const queueRank = (state) => {
|
|
2961
|
+
if (state === "running")
|
|
2962
|
+
return 0;
|
|
2963
|
+
if (state === "queued")
|
|
2964
|
+
return 1;
|
|
2965
|
+
if (state === "blocked")
|
|
2966
|
+
return 2;
|
|
2967
|
+
return 3;
|
|
2968
|
+
};
|
|
2969
|
+
const sortQueueItems = (a, b) => {
|
|
2970
|
+
const queueDelta = queueRank(a.queueState) - queueRank(b.queueState);
|
|
2971
|
+
if (queueDelta !== 0)
|
|
2972
|
+
return queueDelta;
|
|
2973
|
+
const priorityRank = (value) => {
|
|
2974
|
+
const normalized = (value ?? "").trim().toLowerCase();
|
|
2975
|
+
if (!normalized)
|
|
2976
|
+
return 4;
|
|
2977
|
+
if (normalized === "critical" || normalized === "p0" || normalized === "urgent")
|
|
2978
|
+
return 0;
|
|
2979
|
+
if (normalized === "high" || normalized === "p1")
|
|
2980
|
+
return 1;
|
|
2981
|
+
if (normalized === "medium" || normalized === "normal" || normalized === "p2")
|
|
2982
|
+
return 2;
|
|
2983
|
+
if (normalized === "low" || normalized === "p3")
|
|
2984
|
+
return 3;
|
|
2985
|
+
return 4;
|
|
2986
|
+
};
|
|
2987
|
+
const aInitiativePriority = priorityRank(initiativePriorityById.get(a.initiativeId));
|
|
2988
|
+
const bInitiativePriority = priorityRank(initiativePriorityById.get(b.initiativeId));
|
|
2989
|
+
if (aInitiativePriority !== bInitiativePriority) {
|
|
2990
|
+
return aInitiativePriority - bInitiativePriority;
|
|
2991
|
+
}
|
|
2992
|
+
const aPriority = typeof a.nextTaskPriority === "number" ? a.nextTaskPriority : 999;
|
|
2993
|
+
const bPriority = typeof b.nextTaskPriority === "number" ? b.nextTaskPriority : 999;
|
|
2994
|
+
if (aPriority !== bPriority)
|
|
2995
|
+
return aPriority - bPriority;
|
|
2996
|
+
const aDue = a.nextTaskDueAt ? Date.parse(a.nextTaskDueAt) : Number.POSITIVE_INFINITY;
|
|
2997
|
+
const bDue = b.nextTaskDueAt ? Date.parse(b.nextTaskDueAt) : Number.POSITIVE_INFINITY;
|
|
2998
|
+
if (aDue !== bDue)
|
|
2999
|
+
return aDue - bDue;
|
|
3000
|
+
const init = a.initiativeTitle.localeCompare(b.initiativeTitle);
|
|
3001
|
+
if (init !== 0)
|
|
3002
|
+
return init;
|
|
3003
|
+
return a.workstreamTitle.localeCompare(b.workstreamTitle);
|
|
3004
|
+
};
|
|
3005
|
+
const buildSessionFallbackQueue = async () => {
|
|
3006
|
+
let sessionTree = null;
|
|
3007
|
+
try {
|
|
3008
|
+
sessionTree = await client.getLiveSessions({
|
|
3009
|
+
initiative: requestedInitiativeId,
|
|
3010
|
+
limit: 500,
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
catch (err) {
|
|
3014
|
+
degraded.push(`live sessions fallback unavailable (${safeErrorMessage(err)})`);
|
|
3015
|
+
}
|
|
3016
|
+
if (!sessionTree) {
|
|
3017
|
+
try {
|
|
3018
|
+
const localTree = toLocalSessionTree(await loadLocalOpenClawSnapshot(400), 400);
|
|
3019
|
+
sessionTree = applyAgentContextsToSessionTree(localTree, readAgentContexts().agents);
|
|
3020
|
+
}
|
|
3021
|
+
catch (err) {
|
|
3022
|
+
degraded.push(`local sessions fallback unavailable (${safeErrorMessage(err)})`);
|
|
3023
|
+
return [];
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
sessionTree = applyAgentContextsToSessionTree(sessionTree, readAgentContexts().agents);
|
|
3027
|
+
const grouped = new Map();
|
|
3028
|
+
const parseEpoch = (value) => {
|
|
3029
|
+
const parsed = value ? Date.parse(value) : Number.NaN;
|
|
3030
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
3031
|
+
};
|
|
3032
|
+
for (const node of sessionTree.nodes ?? []) {
|
|
3033
|
+
const initiativeId = (node.initiativeId ?? "").trim();
|
|
3034
|
+
const workstreamId = (node.workstreamId ?? "").trim();
|
|
3035
|
+
if (!initiativeId || !workstreamId)
|
|
3036
|
+
continue;
|
|
3037
|
+
if (requestedInitiativeId && initiativeId !== requestedInitiativeId)
|
|
3038
|
+
continue;
|
|
3039
|
+
const initiativeStatus = initiativeStatusById.get(initiativeId) ?? "active";
|
|
3040
|
+
if (!isInitiativeActiveStatus(initiativeStatus))
|
|
3041
|
+
continue;
|
|
3042
|
+
const key = `${initiativeId}:${workstreamId}`;
|
|
3043
|
+
const epoch = parseEpoch(node.updatedAt ?? node.lastEventAt ?? node.startedAt);
|
|
3044
|
+
const existing = grouped.get(key);
|
|
3045
|
+
if (!existing) {
|
|
3046
|
+
grouped.set(key, {
|
|
3047
|
+
initiativeId,
|
|
3048
|
+
workstreamId,
|
|
3049
|
+
initiativeTitle: initiativeTitleById.get(initiativeId) ??
|
|
3050
|
+
node.groupLabel ??
|
|
3051
|
+
initiativeId,
|
|
3052
|
+
initiativeStatus,
|
|
3053
|
+
workstreamTitle: `Workstream ${workstreamId.slice(0, 8)}`,
|
|
3054
|
+
statuses: new Set([node.status]),
|
|
3055
|
+
blockers: Array.isArray(node.blockers) ? [...node.blockers] : [],
|
|
3056
|
+
latest: node,
|
|
3057
|
+
latestEpoch: epoch,
|
|
3058
|
+
});
|
|
3059
|
+
continue;
|
|
3060
|
+
}
|
|
3061
|
+
existing.statuses.add(node.status);
|
|
3062
|
+
if (Array.isArray(node.blockers)) {
|
|
3063
|
+
for (const blocker of node.blockers) {
|
|
3064
|
+
if (typeof blocker !== "string" || blocker.trim().length === 0)
|
|
3065
|
+
continue;
|
|
3066
|
+
if (!existing.blockers.includes(blocker))
|
|
3067
|
+
existing.blockers.push(blocker);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
if (epoch >= existing.latestEpoch) {
|
|
3071
|
+
existing.latest = node;
|
|
3072
|
+
existing.latestEpoch = epoch;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
const fallbackItems = [];
|
|
3076
|
+
for (const entry of grouped.values()) {
|
|
3077
|
+
const statusValues = Array.from(entry.statuses).map((status) => status.toLowerCase());
|
|
3078
|
+
const hasBlocked = statusValues.some((status) => status === "blocked" || status === "failed") ||
|
|
3079
|
+
entry.blockers.length > 0;
|
|
3080
|
+
const hasRunning = statusValues.some((status) => isInProgressStatus(status));
|
|
3081
|
+
const hasQueued = statusValues.some((status) => status === "queued" || status === "pending");
|
|
3082
|
+
const queueState = hasRunning
|
|
3083
|
+
? "running"
|
|
3084
|
+
: hasBlocked
|
|
3085
|
+
? "blocked"
|
|
3086
|
+
: hasQueued
|
|
3087
|
+
? "queued"
|
|
3088
|
+
: "idle";
|
|
3089
|
+
const runnerAgentId = (entry.latest.agentId ?? "").trim() || "main";
|
|
3090
|
+
const runnerAgentName = (entry.latest.agentName ?? "").trim() ||
|
|
3091
|
+
initiativeTitleById.get(`agent:${runnerAgentId}`) ||
|
|
3092
|
+
runnerAgentId;
|
|
3093
|
+
fallbackItems.push({
|
|
3094
|
+
initiativeId: entry.initiativeId,
|
|
3095
|
+
initiativeTitle: entry.initiativeTitle,
|
|
3096
|
+
initiativeStatus: entry.initiativeStatus,
|
|
3097
|
+
workstreamId: entry.workstreamId,
|
|
3098
|
+
workstreamTitle: entry.workstreamTitle,
|
|
3099
|
+
workstreamStatus: hasBlocked ? "blocked" : hasRunning ? "active" : hasQueued ? "queued" : "idle",
|
|
3100
|
+
nextTaskId: entry.latest.id ?? null,
|
|
3101
|
+
nextTaskTitle: (entry.latest.lastEventSummary ?? "").trim() ||
|
|
3102
|
+
(entry.latest.title ?? "").trim() ||
|
|
3103
|
+
null,
|
|
3104
|
+
nextTaskPriority: null,
|
|
3105
|
+
nextTaskDueAt: null,
|
|
3106
|
+
runnerAgentId,
|
|
3107
|
+
runnerAgentName,
|
|
3108
|
+
runnerSource: "fallback",
|
|
3109
|
+
queueState,
|
|
3110
|
+
blockReason: hasBlocked
|
|
3111
|
+
? entry.blockers[0] ?? (statusValues.includes("failed") ? "Latest run failed" : "Workstream blocked")
|
|
3112
|
+
: null,
|
|
3113
|
+
autoContinue: null,
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
fallbackItems.sort(sortQueueItems);
|
|
3117
|
+
return fallbackItems;
|
|
3118
|
+
};
|
|
3119
|
+
const scopedInitiatives = initiatives.filter((entity) => {
|
|
3120
|
+
const record = entity;
|
|
3121
|
+
const id = pickString(record, ["id"]);
|
|
3122
|
+
if (!id)
|
|
3123
|
+
return false;
|
|
3124
|
+
if (requestedInitiativeId && id !== requestedInitiativeId)
|
|
3125
|
+
return false;
|
|
3126
|
+
const status = pickString(record, ["status"]);
|
|
3127
|
+
return isInitiativeActiveStatus(status);
|
|
3128
|
+
});
|
|
3129
|
+
const agentCatalogById = new Map();
|
|
3130
|
+
try {
|
|
3131
|
+
const catalog = await listAgents();
|
|
3132
|
+
for (const entry of catalog) {
|
|
3133
|
+
if (!entry || typeof entry !== "object")
|
|
3134
|
+
continue;
|
|
3135
|
+
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
3136
|
+
if (!id)
|
|
3137
|
+
continue;
|
|
3138
|
+
const name = typeof entry.name === "string" && entry.name.trim().length > 0
|
|
3139
|
+
? entry.name.trim()
|
|
3140
|
+
: id;
|
|
3141
|
+
agentCatalogById.set(id, { id, name });
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
catch (err) {
|
|
3145
|
+
degraded.push(`agent catalog unavailable (${safeErrorMessage(err)})`);
|
|
3146
|
+
}
|
|
3147
|
+
const liveAgentsByInitiative = new Map();
|
|
3148
|
+
try {
|
|
3149
|
+
const data = await client.getLiveAgents({
|
|
3150
|
+
initiative: requestedInitiativeId,
|
|
3151
|
+
includeIdle: true,
|
|
3152
|
+
});
|
|
3153
|
+
for (const raw of Array.isArray(data.agents) ? data.agents : []) {
|
|
3154
|
+
if (!raw || typeof raw !== "object")
|
|
3155
|
+
continue;
|
|
3156
|
+
const row = raw;
|
|
3157
|
+
const initiativeId = pickString(row, ["initiativeId", "initiative_id"]);
|
|
3158
|
+
if (!initiativeId)
|
|
3159
|
+
continue;
|
|
3160
|
+
const id = pickString(row, ["id", "agentId", "agent_id"]) ??
|
|
3161
|
+
pickString(row, ["name", "agentName", "agent_name"]) ??
|
|
3162
|
+
"";
|
|
3163
|
+
const name = pickString(row, ["name", "agentName", "agent_name"]) ??
|
|
3164
|
+
id;
|
|
3165
|
+
if (!id || !name)
|
|
3166
|
+
continue;
|
|
3167
|
+
const list = liveAgentsByInitiative.get(initiativeId) ?? [];
|
|
3168
|
+
list.push({
|
|
3169
|
+
id,
|
|
3170
|
+
name,
|
|
3171
|
+
domain: pickString(row, ["domain", "role"]),
|
|
3172
|
+
});
|
|
3173
|
+
liveAgentsByInitiative.set(initiativeId, list);
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
catch (err) {
|
|
3177
|
+
degraded.push(`live agents unavailable (${safeErrorMessage(err)})`);
|
|
3178
|
+
}
|
|
3179
|
+
const items = [];
|
|
3180
|
+
for (const initiativeEntity of scopedInitiatives) {
|
|
3181
|
+
const initiativeRecord = initiativeEntity;
|
|
3182
|
+
const initiativeId = pickString(initiativeRecord, ["id"]);
|
|
3183
|
+
if (!initiativeId)
|
|
3184
|
+
continue;
|
|
3185
|
+
const initiativeTitle = pickString(initiativeRecord, ["title", "name"]) ?? initiativeId;
|
|
3186
|
+
const initiativeStatus = pickString(initiativeRecord, ["status"]) ?? "active";
|
|
3187
|
+
let graph;
|
|
3188
|
+
try {
|
|
3189
|
+
graph = applyLocalInitiativeOverrideToGraph(await buildMissionControlGraph(client, initiativeId));
|
|
3190
|
+
}
|
|
3191
|
+
catch (err) {
|
|
3192
|
+
degraded.push(`graph unavailable for ${initiativeId} (${safeErrorMessage(err)})`);
|
|
3193
|
+
continue;
|
|
3194
|
+
}
|
|
3195
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
3196
|
+
const workstreamNodes = graph.nodes.filter((node) => node.type === "workstream");
|
|
3197
|
+
const runningWorkstreams = new Set();
|
|
3198
|
+
const taskIsReady = (task) => task.dependencyIds.every((depId) => {
|
|
3199
|
+
const dependency = nodeById.get(depId);
|
|
3200
|
+
return dependency ? isDoneStatus(dependency.status) : true;
|
|
3201
|
+
});
|
|
3202
|
+
const taskHasBlockedParent = (task) => {
|
|
3203
|
+
const milestone = task.milestoneId ? nodeById.get(task.milestoneId) ?? null : null;
|
|
3204
|
+
const workstream = task.workstreamId ? nodeById.get(task.workstreamId) ?? null : null;
|
|
3205
|
+
return (milestone?.status?.toLowerCase() === "blocked" ||
|
|
3206
|
+
workstream?.status?.toLowerCase() === "blocked");
|
|
3207
|
+
};
|
|
3208
|
+
for (const workstream of workstreamNodes) {
|
|
3209
|
+
const todoTasks = graph.recentTodos
|
|
3210
|
+
.map((taskId) => nodeById.get(taskId))
|
|
3211
|
+
.filter((node) => node?.type === "task" &&
|
|
3212
|
+
node.workstreamId === workstream.id &&
|
|
3213
|
+
isTodoStatus(node.status));
|
|
3214
|
+
const readyTask = todoTasks.find((task) => taskIsReady(task) && !taskHasBlockedParent(task));
|
|
3215
|
+
const candidateTask = readyTask ?? todoTasks[0] ?? null;
|
|
3216
|
+
const autoContinueRun = runningAutoContinueForWorkstream(initiativeId, workstream.id);
|
|
3217
|
+
let queueState = autoContinueRun ? "running" : "queued";
|
|
3218
|
+
let blockReason = null;
|
|
3219
|
+
if (!autoContinueRun && !readyTask && candidateTask) {
|
|
3220
|
+
queueState = "blocked";
|
|
3221
|
+
const blockedDeps = candidateTask.dependencyIds
|
|
3222
|
+
.map((depId) => nodeById.get(depId))
|
|
3223
|
+
.filter((dependency) => Boolean(dependency && !isDoneStatus(dependency.status)))
|
|
3224
|
+
.map((dependency) => dependency.title);
|
|
3225
|
+
if (blockedDeps.length > 0) {
|
|
3226
|
+
blockReason = `Waiting on ${blockedDeps.slice(0, 2).join(", ")}${blockedDeps.length > 2 ? "…" : ""}`;
|
|
3227
|
+
}
|
|
3228
|
+
else if (taskHasBlockedParent(candidateTask)) {
|
|
3229
|
+
blockReason = "Parent milestone or workstream is blocked";
|
|
3230
|
+
}
|
|
3231
|
+
else if (!taskIsReady(candidateTask)) {
|
|
3232
|
+
blockReason = "Task prerequisites are not complete";
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
if (!candidateTask && !autoContinueRun) {
|
|
3236
|
+
continue;
|
|
3237
|
+
}
|
|
3238
|
+
runningWorkstreams.add(workstream.id);
|
|
3239
|
+
const assignedAgent = workstream.assignedAgents[0] ?? null;
|
|
3240
|
+
const inferredAgent = graph.initiative.assignedAgents[0] ??
|
|
3241
|
+
liveAgentsByInitiative.get(initiativeId)?.[0] ??
|
|
3242
|
+
(autoContinueRun?.agentId
|
|
3243
|
+
? {
|
|
3244
|
+
id: autoContinueRun.agentId,
|
|
3245
|
+
name: agentCatalogById.get(autoContinueRun.agentId)?.name ?? autoContinueRun.agentId,
|
|
3246
|
+
domain: null,
|
|
3247
|
+
}
|
|
3248
|
+
: null);
|
|
3249
|
+
const runnerSource = assignedAgent
|
|
3250
|
+
? "assigned"
|
|
3251
|
+
: inferredAgent
|
|
3252
|
+
? "inferred"
|
|
3253
|
+
: "fallback";
|
|
3254
|
+
const resolvedRunner = assignedAgent ?? inferredAgent;
|
|
3255
|
+
const runnerAgentId = resolvedRunner?.id ?? autoContinueRun?.agentId ?? "main";
|
|
3256
|
+
const runnerAgentName = resolvedRunner?.name ??
|
|
3257
|
+
agentCatalogById.get(runnerAgentId)?.name ??
|
|
3258
|
+
runnerAgentId;
|
|
3259
|
+
items.push({
|
|
3260
|
+
initiativeId,
|
|
3261
|
+
initiativeTitle,
|
|
3262
|
+
initiativeStatus,
|
|
3263
|
+
workstreamId: workstream.id,
|
|
3264
|
+
workstreamTitle: workstream.title,
|
|
3265
|
+
workstreamStatus: workstream.status,
|
|
3266
|
+
nextTaskId: candidateTask?.id ??
|
|
3267
|
+
(autoContinueRun?.activeTaskId?.trim() || null),
|
|
3268
|
+
nextTaskTitle: candidateTask?.title ??
|
|
3269
|
+
(autoContinueRun?.activeTaskId
|
|
3270
|
+
? nodeById.get(autoContinueRun.activeTaskId)?.title ?? null
|
|
3271
|
+
: null),
|
|
3272
|
+
nextTaskPriority: candidateTask?.priorityNum ?? null,
|
|
3273
|
+
nextTaskDueAt: candidateTask?.dueDate ?? null,
|
|
3274
|
+
runnerAgentId,
|
|
3275
|
+
runnerAgentName,
|
|
3276
|
+
runnerSource,
|
|
3277
|
+
queueState,
|
|
3278
|
+
blockReason,
|
|
3279
|
+
autoContinue: autoContinueRun
|
|
3280
|
+
? {
|
|
3281
|
+
status: autoContinueRun.status,
|
|
3282
|
+
activeTaskId: autoContinueRun.activeTaskId,
|
|
3283
|
+
activeRunId: autoContinueRun.activeRunId,
|
|
3284
|
+
stopReason: autoContinueRun.stopReason,
|
|
3285
|
+
updatedAt: autoContinueRun.updatedAt,
|
|
3286
|
+
}
|
|
3287
|
+
: null,
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
const run = autoContinueRuns.get(initiativeId);
|
|
3291
|
+
if (run &&
|
|
3292
|
+
(run.status === "running" || run.status === "stopping") &&
|
|
3293
|
+
Array.isArray(run.allowedWorkstreamIds) &&
|
|
3294
|
+
run.allowedWorkstreamIds.length > 0) {
|
|
3295
|
+
for (const workstreamId of run.allowedWorkstreamIds) {
|
|
3296
|
+
if (runningWorkstreams.has(workstreamId))
|
|
3297
|
+
continue;
|
|
3298
|
+
const workstream = nodeById.get(workstreamId);
|
|
3299
|
+
if (!workstream || workstream.type !== "workstream")
|
|
3300
|
+
continue;
|
|
3301
|
+
items.push({
|
|
3302
|
+
initiativeId,
|
|
3303
|
+
initiativeTitle,
|
|
3304
|
+
initiativeStatus,
|
|
3305
|
+
workstreamId: workstream.id,
|
|
3306
|
+
workstreamTitle: workstream.title,
|
|
3307
|
+
workstreamStatus: workstream.status,
|
|
3308
|
+
nextTaskId: run.activeTaskId,
|
|
3309
|
+
nextTaskTitle: run.activeTaskId
|
|
3310
|
+
? nodeById.get(run.activeTaskId)?.title ?? null
|
|
3311
|
+
: null,
|
|
3312
|
+
nextTaskPriority: null,
|
|
3313
|
+
nextTaskDueAt: null,
|
|
3314
|
+
runnerAgentId: run.agentId,
|
|
3315
|
+
runnerAgentName: agentCatalogById.get(run.agentId)?.name ?? run.agentId,
|
|
3316
|
+
runnerSource: "inferred",
|
|
3317
|
+
queueState: "running",
|
|
3318
|
+
blockReason: null,
|
|
3319
|
+
autoContinue: {
|
|
3320
|
+
status: run.status,
|
|
3321
|
+
activeTaskId: run.activeTaskId,
|
|
3322
|
+
activeRunId: run.activeRunId,
|
|
3323
|
+
stopReason: run.stopReason,
|
|
3324
|
+
updatedAt: run.updatedAt,
|
|
3325
|
+
},
|
|
3326
|
+
});
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
if (items.length === 0) {
|
|
3331
|
+
const fallbackItems = await buildSessionFallbackQueue();
|
|
3332
|
+
if (fallbackItems.length > 0) {
|
|
3333
|
+
degraded.push("Using session-derived Next Up fallback.");
|
|
3334
|
+
items.push(...fallbackItems);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
items.sort(sortQueueItems);
|
|
3338
|
+
return { items, degraded };
|
|
3339
|
+
}
|
|
2517
3340
|
const autoContinueTimer = setInterval(() => {
|
|
2518
3341
|
void tickAllAutoContinue();
|
|
2519
3342
|
}, AUTO_CONTINUE_TICK_MS);
|
|
@@ -2558,6 +3381,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2558
3381
|
const runCheckpointRestoreMatch = route.match(/^runs\/([^/]+)\/checkpoints\/([^/]+)\/restore$/);
|
|
2559
3382
|
const isDelegationPreflight = route === "delegation/preflight";
|
|
2560
3383
|
const isMissionControlAutoAssignmentRoute = route === "mission-control/assignments/auto";
|
|
3384
|
+
const isMissionControlNextUpPlayRoute = route === "mission-control/next-up/play";
|
|
2561
3385
|
const isMissionControlAutoContinueStartRoute = route === "mission-control/auto-continue/start";
|
|
2562
3386
|
const isMissionControlAutoContinueStopRoute = route === "mission-control/auto-continue/stop";
|
|
2563
3387
|
const isEntitiesRoute = route === "entities";
|
|
@@ -3071,6 +3895,135 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
3071
3895
|
}
|
|
3072
3896
|
return true;
|
|
3073
3897
|
}
|
|
3898
|
+
if (method === "POST" && isMissionControlNextUpPlayRoute) {
|
|
3899
|
+
try {
|
|
3900
|
+
const payload = await parseJsonRequest(req);
|
|
3901
|
+
const initiativeId = (pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
3902
|
+
searchParams.get("initiativeId") ??
|
|
3903
|
+
searchParams.get("initiative_id") ??
|
|
3904
|
+
"")
|
|
3905
|
+
.trim();
|
|
3906
|
+
const workstreamId = (pickString(payload, ["workstreamId", "workstream_id"]) ??
|
|
3907
|
+
searchParams.get("workstreamId") ??
|
|
3908
|
+
searchParams.get("workstream_id") ??
|
|
3909
|
+
"")
|
|
3910
|
+
.trim();
|
|
3911
|
+
if (!initiativeId || !workstreamId) {
|
|
3912
|
+
sendJson(res, 400, {
|
|
3913
|
+
ok: false,
|
|
3914
|
+
error: "initiativeId and workstreamId are required",
|
|
3915
|
+
});
|
|
3916
|
+
return true;
|
|
3917
|
+
}
|
|
3918
|
+
let agentIdRaw = (pickString(payload, ["agentId", "agent_id"]) ??
|
|
3919
|
+
searchParams.get("agentId") ??
|
|
3920
|
+
searchParams.get("agent_id") ??
|
|
3921
|
+
"")
|
|
3922
|
+
.trim();
|
|
3923
|
+
const queue = await buildNextUpQueue({ initiativeId });
|
|
3924
|
+
const matchedQueueItem = queue.items.find((item) => item.workstreamId === workstreamId) ?? null;
|
|
3925
|
+
if (!agentIdRaw && matchedQueueItem?.runnerAgentId) {
|
|
3926
|
+
agentIdRaw = matchedQueueItem.runnerAgentId;
|
|
3927
|
+
}
|
|
3928
|
+
const agentId = agentIdRaw || "main";
|
|
3929
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
|
|
3930
|
+
sendJson(res, 400, {
|
|
3931
|
+
ok: false,
|
|
3932
|
+
error: "agentId must be a simple identifier (letters, numbers, _ or -).",
|
|
3933
|
+
});
|
|
3934
|
+
return true;
|
|
3935
|
+
}
|
|
3936
|
+
const upgradeGate = await resolveAutoContinueUpgradeGate(agentId);
|
|
3937
|
+
if (upgradeGate) {
|
|
3938
|
+
sendJson(res, 402, {
|
|
3939
|
+
ok: false,
|
|
3940
|
+
...upgradeGate,
|
|
3941
|
+
});
|
|
3942
|
+
return true;
|
|
3943
|
+
}
|
|
3944
|
+
const tokenBudget = pickNumber(payload, [
|
|
3945
|
+
"tokenBudget",
|
|
3946
|
+
"token_budget",
|
|
3947
|
+
"tokenBudgetTokens",
|
|
3948
|
+
"token_budget_tokens",
|
|
3949
|
+
"maxTokens",
|
|
3950
|
+
"max_tokens",
|
|
3951
|
+
]) ??
|
|
3952
|
+
searchParams.get("tokenBudget") ??
|
|
3953
|
+
searchParams.get("token_budget") ??
|
|
3954
|
+
searchParams.get("tokenBudgetTokens") ??
|
|
3955
|
+
searchParams.get("token_budget_tokens") ??
|
|
3956
|
+
searchParams.get("maxTokens") ??
|
|
3957
|
+
searchParams.get("max_tokens") ??
|
|
3958
|
+
null;
|
|
3959
|
+
const includeVerificationRaw = payload.includeVerification ??
|
|
3960
|
+
payload.include_verification ??
|
|
3961
|
+
searchParams.get("includeVerification") ??
|
|
3962
|
+
searchParams.get("include_verification") ??
|
|
3963
|
+
null;
|
|
3964
|
+
const includeVerification = typeof includeVerificationRaw === "boolean"
|
|
3965
|
+
? includeVerificationRaw
|
|
3966
|
+
: parseBooleanQuery(typeof includeVerificationRaw === "string"
|
|
3967
|
+
? includeVerificationRaw
|
|
3968
|
+
: null);
|
|
3969
|
+
const run = await startAutoContinueRun({
|
|
3970
|
+
initiativeId,
|
|
3971
|
+
agentId,
|
|
3972
|
+
tokenBudget,
|
|
3973
|
+
includeVerification,
|
|
3974
|
+
allowedWorkstreamIds: [workstreamId],
|
|
3975
|
+
});
|
|
3976
|
+
// Play should feel immediate. Run one dispatch tick synchronously so the
|
|
3977
|
+
// user gets an actual launch (or a concrete error) in this response.
|
|
3978
|
+
await tickAutoContinueRun(run);
|
|
3979
|
+
let fallbackDispatch = null;
|
|
3980
|
+
if (!run.activeRunId &&
|
|
3981
|
+
matchedQueueItem &&
|
|
3982
|
+
matchedQueueItem.runnerSource === "fallback") {
|
|
3983
|
+
fallbackDispatch = await dispatchFallbackWorkstreamTurn({
|
|
3984
|
+
initiativeId,
|
|
3985
|
+
initiativeTitle: matchedQueueItem.initiativeTitle,
|
|
3986
|
+
workstreamId,
|
|
3987
|
+
workstreamTitle: matchedQueueItem.workstreamTitle,
|
|
3988
|
+
agentId,
|
|
3989
|
+
});
|
|
3990
|
+
}
|
|
3991
|
+
const dispatchMode = run.activeRunId
|
|
3992
|
+
? "task"
|
|
3993
|
+
: fallbackDispatch
|
|
3994
|
+
? "fallback"
|
|
3995
|
+
: "none";
|
|
3996
|
+
if (dispatchMode === "none") {
|
|
3997
|
+
const reason = run.stopReason === "blocked"
|
|
3998
|
+
? "No dispatchable task is ready for this workstream yet."
|
|
3999
|
+
: run.stopReason === "completed"
|
|
4000
|
+
? "No queued task is available for this workstream."
|
|
4001
|
+
: "Unable to dispatch this workstream right now.";
|
|
4002
|
+
sendJson(res, 409, {
|
|
4003
|
+
ok: false,
|
|
4004
|
+
error: reason,
|
|
4005
|
+
run,
|
|
4006
|
+
initiativeId,
|
|
4007
|
+
workstreamId,
|
|
4008
|
+
agentId,
|
|
4009
|
+
});
|
|
4010
|
+
return true;
|
|
4011
|
+
}
|
|
4012
|
+
sendJson(res, 200, {
|
|
4013
|
+
ok: true,
|
|
4014
|
+
run,
|
|
4015
|
+
initiativeId,
|
|
4016
|
+
workstreamId,
|
|
4017
|
+
agentId,
|
|
4018
|
+
dispatchMode,
|
|
4019
|
+
sessionId: run.activeRunId ?? fallbackDispatch?.sessionId ?? null,
|
|
4020
|
+
});
|
|
4021
|
+
}
|
|
4022
|
+
catch (err) {
|
|
4023
|
+
sendJson(res, 500, { ok: false, error: safeErrorMessage(err) });
|
|
4024
|
+
}
|
|
4025
|
+
return true;
|
|
4026
|
+
}
|
|
3074
4027
|
if (method === "POST" && isMissionControlAutoContinueStartRoute) {
|
|
3075
4028
|
try {
|
|
3076
4029
|
const payload = await parseJsonRequest(req);
|
|
@@ -3096,37 +4049,13 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
3096
4049
|
});
|
|
3097
4050
|
return true;
|
|
3098
4051
|
}
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
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
|
-
}
|
|
4052
|
+
const upgradeGate = await resolveAutoContinueUpgradeGate(agentId);
|
|
4053
|
+
if (upgradeGate) {
|
|
4054
|
+
sendJson(res, 402, {
|
|
4055
|
+
ok: false,
|
|
4056
|
+
...upgradeGate,
|
|
4057
|
+
});
|
|
4058
|
+
return true;
|
|
3130
4059
|
}
|
|
3131
4060
|
const tokenBudget = pickNumber(payload, [
|
|
3132
4061
|
"tokenBudget",
|
|
@@ -3170,53 +4099,13 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
3170
4099
|
.filter(Boolean),
|
|
3171
4100
|
]);
|
|
3172
4101
|
const allowedWorkstreamIds = workstreamFilter.length > 0 ? workstreamFilter : null;
|
|
3173
|
-
const
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
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
|
-
}
|
|
4102
|
+
const run = await startAutoContinueRun({
|
|
4103
|
+
initiativeId,
|
|
4104
|
+
agentId,
|
|
4105
|
+
tokenBudget,
|
|
4106
|
+
includeVerification,
|
|
4107
|
+
allowedWorkstreamIds,
|
|
4108
|
+
});
|
|
3220
4109
|
sendJson(res, 200, { ok: true, run });
|
|
3221
4110
|
}
|
|
3222
4111
|
catch (err) {
|
|
@@ -3435,11 +4324,38 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
3435
4324
|
const entityAction = decodeURIComponent(entityActionMatch[3]);
|
|
3436
4325
|
const payload = await parseJsonRequest(req);
|
|
3437
4326
|
if (entityAction === "delete") {
|
|
3438
|
-
// Delete via status update
|
|
3439
|
-
const
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
4327
|
+
// Delete via status update. Initiatives use `archived` in OrgX.
|
|
4328
|
+
const deleteStatus = entityType.trim().toLowerCase() === "initiative"
|
|
4329
|
+
? "archived"
|
|
4330
|
+
: "deleted";
|
|
4331
|
+
try {
|
|
4332
|
+
const entity = await client.updateEntity(entityType, entityId, {
|
|
4333
|
+
status: deleteStatus,
|
|
4334
|
+
});
|
|
4335
|
+
if (entityType.trim().toLowerCase() === "initiative") {
|
|
4336
|
+
clearLocalInitiativeStatusOverride(entityId);
|
|
4337
|
+
}
|
|
4338
|
+
sendJson(res, 200, { ok: true, entity, deletedAsStatus: deleteStatus });
|
|
4339
|
+
}
|
|
4340
|
+
catch (err) {
|
|
4341
|
+
if (entityType.trim().toLowerCase() === "initiative" &&
|
|
4342
|
+
isUnauthorizedOrgxError(err)) {
|
|
4343
|
+
setLocalInitiativeStatusOverride(entityId, deleteStatus);
|
|
4344
|
+
sendJson(res, 200, {
|
|
4345
|
+
ok: true,
|
|
4346
|
+
localFallback: true,
|
|
4347
|
+
warning: safeErrorMessage(err),
|
|
4348
|
+
entity: {
|
|
4349
|
+
id: entityId,
|
|
4350
|
+
type: entityType,
|
|
4351
|
+
status: deleteStatus,
|
|
4352
|
+
},
|
|
4353
|
+
deletedAsStatus: deleteStatus,
|
|
4354
|
+
});
|
|
4355
|
+
return true;
|
|
4356
|
+
}
|
|
4357
|
+
throw err;
|
|
4358
|
+
}
|
|
3443
4359
|
}
|
|
3444
4360
|
else {
|
|
3445
4361
|
// Map action to status update
|
|
@@ -3458,11 +4374,34 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
3458
4374
|
});
|
|
3459
4375
|
return true;
|
|
3460
4376
|
}
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
4377
|
+
try {
|
|
4378
|
+
const entity = await client.updateEntity(entityType, entityId, {
|
|
4379
|
+
status: newStatus,
|
|
4380
|
+
...(payload.force ? { force: true } : {}),
|
|
4381
|
+
});
|
|
4382
|
+
if (entityType.trim().toLowerCase() === "initiative") {
|
|
4383
|
+
clearLocalInitiativeStatusOverride(entityId);
|
|
4384
|
+
}
|
|
4385
|
+
sendJson(res, 200, { ok: true, entity });
|
|
4386
|
+
}
|
|
4387
|
+
catch (err) {
|
|
4388
|
+
if (entityType.trim().toLowerCase() === "initiative" &&
|
|
4389
|
+
isUnauthorizedOrgxError(err)) {
|
|
4390
|
+
setLocalInitiativeStatusOverride(entityId, newStatus);
|
|
4391
|
+
sendJson(res, 200, {
|
|
4392
|
+
ok: true,
|
|
4393
|
+
localFallback: true,
|
|
4394
|
+
warning: safeErrorMessage(err),
|
|
4395
|
+
entity: {
|
|
4396
|
+
id: entityId,
|
|
4397
|
+
type: entityType,
|
|
4398
|
+
status: newStatus,
|
|
4399
|
+
},
|
|
4400
|
+
});
|
|
4401
|
+
return true;
|
|
4402
|
+
}
|
|
4403
|
+
throw err;
|
|
4404
|
+
}
|
|
3466
4405
|
}
|
|
3467
4406
|
}
|
|
3468
4407
|
catch (err) {
|
|
@@ -3479,6 +4418,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
3479
4418
|
!(runActionMatch && method === "POST") &&
|
|
3480
4419
|
!(isDelegationPreflight && method === "POST") &&
|
|
3481
4420
|
!(isMissionControlAutoAssignmentRoute && method === "POST") &&
|
|
4421
|
+
!(isMissionControlNextUpPlayRoute && method === "POST") &&
|
|
3482
4422
|
!(isEntitiesRoute && method === "POST") &&
|
|
3483
4423
|
!(isEntitiesRoute && method === "PATCH") &&
|
|
3484
4424
|
!(entityActionMatch && method === "POST") &&
|
|
@@ -3660,6 +4600,108 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
3660
4600
|
case "onboarding":
|
|
3661
4601
|
sendJson(res, 200, getOnboardingState(await onboarding.getStatus()));
|
|
3662
4602
|
return true;
|
|
4603
|
+
case "hooks/runtime": {
|
|
4604
|
+
if (method !== "POST") {
|
|
4605
|
+
sendJson(res, 405, { ok: false, error: "Use POST /orgx/api/hooks/runtime" });
|
|
4606
|
+
return true;
|
|
4607
|
+
}
|
|
4608
|
+
const expectedHookToken = resolveRuntimeHookToken();
|
|
4609
|
+
const providedHookToken = pickHeaderString(req.headers, ["x-orgx-hook-token", "x-hook-token"]) ??
|
|
4610
|
+
searchParams.get("hook_token") ??
|
|
4611
|
+
searchParams.get("token");
|
|
4612
|
+
if (!providedHookToken || providedHookToken.trim() !== expectedHookToken) {
|
|
4613
|
+
sendJson(res, 401, {
|
|
4614
|
+
ok: false,
|
|
4615
|
+
error: "Invalid hook token",
|
|
4616
|
+
});
|
|
4617
|
+
return true;
|
|
4618
|
+
}
|
|
4619
|
+
try {
|
|
4620
|
+
const payloadRecord = await parseJsonRequest(req);
|
|
4621
|
+
const payload = {
|
|
4622
|
+
source_client: pickString(payloadRecord, ["source_client", "sourceClient"]) ??
|
|
4623
|
+
"unknown",
|
|
4624
|
+
event: pickString(payloadRecord, ["event", "hook_event"]) ?? "heartbeat",
|
|
4625
|
+
run_id: pickString(payloadRecord, ["run_id", "runId", "session_id", "sessionId"]),
|
|
4626
|
+
correlation_id: pickString(payloadRecord, ["correlation_id", "correlationId"]),
|
|
4627
|
+
initiative_id: pickString(payloadRecord, ["initiative_id", "initiativeId"]),
|
|
4628
|
+
workstream_id: pickString(payloadRecord, ["workstream_id", "workstreamId"]),
|
|
4629
|
+
task_id: pickString(payloadRecord, ["task_id", "taskId"]),
|
|
4630
|
+
agent_id: pickString(payloadRecord, ["agent_id", "agentId"]),
|
|
4631
|
+
agent_name: pickString(payloadRecord, ["agent_name", "agentName"]),
|
|
4632
|
+
phase: pickString(payloadRecord, ["phase"]),
|
|
4633
|
+
progress_pct: pickNumber(payloadRecord, ["progress_pct", "progressPct"]) ??
|
|
4634
|
+
null,
|
|
4635
|
+
message: pickString(payloadRecord, ["message", "summary"]),
|
|
4636
|
+
metadata: payloadRecord.metadata && typeof payloadRecord.metadata === "object"
|
|
4637
|
+
? payloadRecord.metadata
|
|
4638
|
+
: null,
|
|
4639
|
+
timestamp: pickString(payloadRecord, ["timestamp", "time", "ts"]),
|
|
4640
|
+
};
|
|
4641
|
+
const instance = upsertRuntimeInstanceFromHook(payload);
|
|
4642
|
+
const fallbackPhaseByEvent = {
|
|
4643
|
+
session_start: "intent",
|
|
4644
|
+
heartbeat: "execution",
|
|
4645
|
+
progress: "execution",
|
|
4646
|
+
task_update: "execution",
|
|
4647
|
+
session_stop: "completed",
|
|
4648
|
+
error: "blocked",
|
|
4649
|
+
};
|
|
4650
|
+
const phase = normalizeHookPhase(payload.phase ??
|
|
4651
|
+
fallbackPhaseByEvent[instance.event] ??
|
|
4652
|
+
"execution");
|
|
4653
|
+
const level = instance.event === "error" ? "error" : phase === "blocked" ? "warn" : "info";
|
|
4654
|
+
const message = payload.message ??
|
|
4655
|
+
`${instance.displayName} ${instance.event.replace(/_/g, " ")}`;
|
|
4656
|
+
let forwarded = false;
|
|
4657
|
+
let forwardError = null;
|
|
4658
|
+
if (instance.initiativeId) {
|
|
4659
|
+
try {
|
|
4660
|
+
await client.emitActivity({
|
|
4661
|
+
initiative_id: instance.initiativeId,
|
|
4662
|
+
run_id: instance.runId ?? undefined,
|
|
4663
|
+
correlation_id: instance.runId
|
|
4664
|
+
? undefined
|
|
4665
|
+
: (instance.correlationId ?? undefined),
|
|
4666
|
+
source_client: normalizeRuntimeSourceForReporting(instance.sourceClient),
|
|
4667
|
+
message,
|
|
4668
|
+
phase,
|
|
4669
|
+
progress_pct: instance.progressPct ?? undefined,
|
|
4670
|
+
level,
|
|
4671
|
+
metadata: {
|
|
4672
|
+
source: "runtime_hook_relay",
|
|
4673
|
+
hook_event: instance.event,
|
|
4674
|
+
instance_id: instance.id,
|
|
4675
|
+
runtime_client: instance.sourceClient,
|
|
4676
|
+
task_id: instance.taskId,
|
|
4677
|
+
workstream_id: instance.workstreamId,
|
|
4678
|
+
...(instance.metadata ?? {}),
|
|
4679
|
+
},
|
|
4680
|
+
});
|
|
4681
|
+
forwarded = true;
|
|
4682
|
+
}
|
|
4683
|
+
catch (err) {
|
|
4684
|
+
forwardError = safeErrorMessage(err);
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
sendJson(res, 200, {
|
|
4688
|
+
ok: true,
|
|
4689
|
+
instance_id: instance.id,
|
|
4690
|
+
state: instance.state,
|
|
4691
|
+
last_seen_at: instance.lastHeartbeatAt ?? instance.lastEventAt,
|
|
4692
|
+
run_id: instance.runId ?? null,
|
|
4693
|
+
forwarded,
|
|
4694
|
+
forward_error: forwardError,
|
|
4695
|
+
});
|
|
4696
|
+
}
|
|
4697
|
+
catch (err) {
|
|
4698
|
+
sendJson(res, 500, {
|
|
4699
|
+
ok: false,
|
|
4700
|
+
error: safeErrorMessage(err),
|
|
4701
|
+
});
|
|
4702
|
+
}
|
|
4703
|
+
return true;
|
|
4704
|
+
}
|
|
3663
4705
|
case "mission-control/auto-continue/status": {
|
|
3664
4706
|
const initiativeId = searchParams.get("initiative_id") ??
|
|
3665
4707
|
searchParams.get("initiativeId") ??
|
|
@@ -3893,7 +4935,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
3893
4935
|
return true;
|
|
3894
4936
|
}
|
|
3895
4937
|
try {
|
|
3896
|
-
const graph = await buildMissionControlGraph(client, initiativeId.trim());
|
|
4938
|
+
const graph = applyLocalInitiativeOverrideToGraph(await buildMissionControlGraph(client, initiativeId.trim()));
|
|
3897
4939
|
sendJson(res, 200, graph);
|
|
3898
4940
|
}
|
|
3899
4941
|
catch (err) {
|
|
@@ -3903,6 +4945,29 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
3903
4945
|
}
|
|
3904
4946
|
return true;
|
|
3905
4947
|
}
|
|
4948
|
+
case "mission-control/next-up": {
|
|
4949
|
+
const initiativeIdRaw = searchParams.get("initiative_id") ??
|
|
4950
|
+
searchParams.get("initiativeId") ??
|
|
4951
|
+
"";
|
|
4952
|
+
const initiativeId = initiativeIdRaw.trim() || null;
|
|
4953
|
+
try {
|
|
4954
|
+
const queue = await buildNextUpQueue({ initiativeId });
|
|
4955
|
+
sendJson(res, 200, {
|
|
4956
|
+
ok: true,
|
|
4957
|
+
generatedAt: new Date().toISOString(),
|
|
4958
|
+
total: queue.items.length,
|
|
4959
|
+
items: queue.items,
|
|
4960
|
+
degraded: queue.degraded,
|
|
4961
|
+
});
|
|
4962
|
+
}
|
|
4963
|
+
catch (err) {
|
|
4964
|
+
sendJson(res, 500, {
|
|
4965
|
+
ok: false,
|
|
4966
|
+
error: safeErrorMessage(err),
|
|
4967
|
+
});
|
|
4968
|
+
}
|
|
4969
|
+
return true;
|
|
4970
|
+
}
|
|
3906
4971
|
case "entities": {
|
|
3907
4972
|
if (method === "POST") {
|
|
3908
4973
|
try {
|
|
@@ -3951,10 +5016,15 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
3951
5016
|
return true;
|
|
3952
5017
|
}
|
|
3953
5018
|
if (method === "PATCH") {
|
|
5019
|
+
let payload = {};
|
|
5020
|
+
let type = null;
|
|
5021
|
+
let id = null;
|
|
5022
|
+
let requestedStatus = null;
|
|
3954
5023
|
try {
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
5024
|
+
payload = await parseJsonRequest(req);
|
|
5025
|
+
type = pickString(payload, ["type"]);
|
|
5026
|
+
id = pickString(payload, ["id"]);
|
|
5027
|
+
requestedStatus = pickString(payload, ["status"]);
|
|
3958
5028
|
if (!type || !id) {
|
|
3959
5029
|
sendJson(res, 400, {
|
|
3960
5030
|
error: "Both 'type' and 'id' are required for PATCH.",
|
|
@@ -3964,37 +5034,91 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
3964
5034
|
const updates = { ...payload };
|
|
3965
5035
|
delete updates.type;
|
|
3966
5036
|
delete updates.id;
|
|
3967
|
-
const
|
|
5037
|
+
const normalizedType = type.trim().toLowerCase();
|
|
5038
|
+
const normalizedUpdates = normalizeEntityMutationPayload(updates);
|
|
5039
|
+
const entity = await client.updateEntity(type, id, normalizedUpdates);
|
|
5040
|
+
if (normalizedType === "initiative") {
|
|
5041
|
+
clearLocalInitiativeStatusOverride(id);
|
|
5042
|
+
}
|
|
3968
5043
|
sendJson(res, 200, { ok: true, entity });
|
|
3969
5044
|
}
|
|
3970
5045
|
catch (err) {
|
|
5046
|
+
if (type?.trim().toLowerCase() === "initiative" &&
|
|
5047
|
+
id &&
|
|
5048
|
+
requestedStatus &&
|
|
5049
|
+
isUnauthorizedOrgxError(err)) {
|
|
5050
|
+
setLocalInitiativeStatusOverride(id, requestedStatus);
|
|
5051
|
+
sendJson(res, 200, {
|
|
5052
|
+
ok: true,
|
|
5053
|
+
localFallback: true,
|
|
5054
|
+
warning: safeErrorMessage(err),
|
|
5055
|
+
entity: {
|
|
5056
|
+
id,
|
|
5057
|
+
type,
|
|
5058
|
+
status: requestedStatus,
|
|
5059
|
+
},
|
|
5060
|
+
});
|
|
5061
|
+
return true;
|
|
5062
|
+
}
|
|
3971
5063
|
sendJson(res, 500, {
|
|
3972
5064
|
error: safeErrorMessage(err),
|
|
3973
5065
|
});
|
|
3974
5066
|
}
|
|
3975
5067
|
return true;
|
|
3976
5068
|
}
|
|
5069
|
+
const type = searchParams.get("type");
|
|
5070
|
+
if (!type) {
|
|
5071
|
+
sendJson(res, 400, {
|
|
5072
|
+
error: "Query parameter 'type' is required for GET /entities.",
|
|
5073
|
+
});
|
|
5074
|
+
return true;
|
|
5075
|
+
}
|
|
5076
|
+
const status = searchParams.get("status") ?? undefined;
|
|
5077
|
+
const initiativeId = searchParams.get("initiative_id") ?? undefined;
|
|
5078
|
+
const limit = searchParams.get("limit")
|
|
5079
|
+
? Number(searchParams.get("limit"))
|
|
5080
|
+
: undefined;
|
|
3977
5081
|
try {
|
|
3978
|
-
const type = searchParams.get("type");
|
|
3979
|
-
if (!type) {
|
|
3980
|
-
sendJson(res, 400, {
|
|
3981
|
-
error: "Query parameter 'type' is required for GET /entities.",
|
|
3982
|
-
});
|
|
3983
|
-
return true;
|
|
3984
|
-
}
|
|
3985
|
-
const status = searchParams.get("status") ?? undefined;
|
|
3986
|
-
const initiativeId = searchParams.get("initiative_id") ?? undefined;
|
|
3987
|
-
const limit = searchParams.get("limit")
|
|
3988
|
-
? Number(searchParams.get("limit"))
|
|
3989
|
-
: undefined;
|
|
3990
5082
|
const data = await client.listEntities(type, {
|
|
3991
5083
|
status,
|
|
3992
5084
|
initiative_id: initiativeId,
|
|
3993
5085
|
limit: Number.isFinite(limit) ? limit : undefined,
|
|
3994
5086
|
});
|
|
5087
|
+
if (type.trim().toLowerCase() === "initiative") {
|
|
5088
|
+
const payload = data;
|
|
5089
|
+
const rows = Array.isArray(payload.data)
|
|
5090
|
+
? payload.data.filter((row) => Boolean(row && typeof row === "object"))
|
|
5091
|
+
: [];
|
|
5092
|
+
sendJson(res, 200, {
|
|
5093
|
+
...payload,
|
|
5094
|
+
data: applyLocalInitiativeOverrides(rows),
|
|
5095
|
+
});
|
|
5096
|
+
return true;
|
|
5097
|
+
}
|
|
3995
5098
|
sendJson(res, 200, data);
|
|
3996
5099
|
}
|
|
3997
5100
|
catch (err) {
|
|
5101
|
+
if (type.trim().toLowerCase() === "initiative" &&
|
|
5102
|
+
isUnauthorizedOrgxError(err)) {
|
|
5103
|
+
const snapshotInitiatives = formatInitiatives(getSnapshot())
|
|
5104
|
+
.map((item) => ({
|
|
5105
|
+
id: item.id,
|
|
5106
|
+
title: item.title,
|
|
5107
|
+
name: item.title,
|
|
5108
|
+
summary: null,
|
|
5109
|
+
status: item.status,
|
|
5110
|
+
progress_pct: item.progress ?? null,
|
|
5111
|
+
created_at: null,
|
|
5112
|
+
updated_at: null,
|
|
5113
|
+
}))
|
|
5114
|
+
.filter((item) => initiativeId ? item.id === initiativeId : true);
|
|
5115
|
+
sendJson(res, 200, {
|
|
5116
|
+
data: applyLocalInitiativeOverrides(snapshotInitiatives),
|
|
5117
|
+
localFallback: true,
|
|
5118
|
+
warning: safeErrorMessage(err),
|
|
5119
|
+
});
|
|
5120
|
+
return true;
|
|
5121
|
+
}
|
|
3998
5122
|
sendJson(res, 500, {
|
|
3999
5123
|
error: safeErrorMessage(err),
|
|
4000
5124
|
});
|
|
@@ -4252,12 +5376,22 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
4252
5376
|
catch (err) {
|
|
4253
5377
|
degraded.push(`outbox unavailable (${safeErrorMessage(err)})`);
|
|
4254
5378
|
}
|
|
5379
|
+
let runtimeInstances = listRuntimeInstances({ limit: 320 });
|
|
5380
|
+
if (initiative && initiative.trim().length > 0) {
|
|
5381
|
+
runtimeInstances = runtimeInstances.filter((instance) => instance.initiativeId === initiative);
|
|
5382
|
+
}
|
|
5383
|
+
if (run && run.trim().length > 0) {
|
|
5384
|
+
runtimeInstances = runtimeInstances.filter((instance) => instance.runId === run || instance.correlationId === run);
|
|
5385
|
+
}
|
|
5386
|
+
sessions = enrichSessionsWithRuntime(sessions, runtimeInstances);
|
|
5387
|
+
activity = enrichActivityWithRuntime(activity, runtimeInstances);
|
|
4255
5388
|
sendJson(res, 200, {
|
|
4256
5389
|
sessions,
|
|
4257
5390
|
activity,
|
|
4258
5391
|
handoffs,
|
|
4259
5392
|
decisions,
|
|
4260
5393
|
agents,
|
|
5394
|
+
runtimeInstances,
|
|
4261
5395
|
outbox: outboxStatus,
|
|
4262
5396
|
generatedAt: new Date().toISOString(),
|
|
4263
5397
|
degraded: degraded.length > 0 ? degraded : undefined,
|
|
@@ -4472,7 +5606,30 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
4472
5606
|
id,
|
|
4473
5607
|
limit: Number.isFinite(limit) ? limit : undefined,
|
|
4474
5608
|
});
|
|
4475
|
-
|
|
5609
|
+
const payload = data;
|
|
5610
|
+
const initiatives = Array.isArray(payload.initiatives)
|
|
5611
|
+
? payload.initiatives.map((entry) => {
|
|
5612
|
+
if (!entry || typeof entry !== "object")
|
|
5613
|
+
return entry;
|
|
5614
|
+
const row = entry;
|
|
5615
|
+
const initiativeId = pickString(row, ["id"]);
|
|
5616
|
+
if (!initiativeId)
|
|
5617
|
+
return entry;
|
|
5618
|
+
const override = localInitiativeStatusOverrides.get(initiativeId) ?? null;
|
|
5619
|
+
if (!override)
|
|
5620
|
+
return entry;
|
|
5621
|
+
return {
|
|
5622
|
+
...row,
|
|
5623
|
+
status: override.status,
|
|
5624
|
+
updatedAt: pickString(row, ["updatedAt", "updated_at"]) ??
|
|
5625
|
+
override.updatedAt,
|
|
5626
|
+
};
|
|
5627
|
+
})
|
|
5628
|
+
: payload.initiatives;
|
|
5629
|
+
sendJson(res, 200, {
|
|
5630
|
+
...payload,
|
|
5631
|
+
initiatives,
|
|
5632
|
+
});
|
|
4476
5633
|
}
|
|
4477
5634
|
catch (err) {
|
|
4478
5635
|
try {
|
|
@@ -4486,9 +5643,49 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
4486
5643
|
if (id && id.trim().length > 0) {
|
|
4487
5644
|
initiatives = initiatives.filter((item) => item.id === id);
|
|
4488
5645
|
}
|
|
5646
|
+
initiatives = initiatives.map((item) => {
|
|
5647
|
+
const override = localInitiativeStatusOverrides.get(item.id) ?? null;
|
|
5648
|
+
if (!override)
|
|
5649
|
+
return item;
|
|
5650
|
+
return {
|
|
5651
|
+
...item,
|
|
5652
|
+
status: override.status,
|
|
5653
|
+
updatedAt: item.updatedAt ?? override.updatedAt,
|
|
5654
|
+
};
|
|
5655
|
+
});
|
|
5656
|
+
const requestedId = id?.trim() ?? "";
|
|
5657
|
+
if (requestedId.length > 0) {
|
|
5658
|
+
const override = localInitiativeStatusOverrides.get(requestedId) ?? null;
|
|
5659
|
+
if (override && !initiatives.some((item) => item.id === requestedId)) {
|
|
5660
|
+
initiatives.push({
|
|
5661
|
+
id: requestedId,
|
|
5662
|
+
title: `Initiative ${requestedId.slice(0, 8)}`,
|
|
5663
|
+
status: override.status,
|
|
5664
|
+
updatedAt: override.updatedAt,
|
|
5665
|
+
sessionCount: 0,
|
|
5666
|
+
activeAgents: 0,
|
|
5667
|
+
});
|
|
5668
|
+
}
|
|
5669
|
+
}
|
|
5670
|
+
else {
|
|
5671
|
+
for (const [initiativeId, override] of localInitiativeStatusOverrides.entries()) {
|
|
5672
|
+
if (initiatives.some((item) => item.id === initiativeId))
|
|
5673
|
+
continue;
|
|
5674
|
+
initiatives.push({
|
|
5675
|
+
id: initiativeId,
|
|
5676
|
+
title: `Initiative ${initiativeId.slice(0, 8)}`,
|
|
5677
|
+
status: override.status,
|
|
5678
|
+
updatedAt: override.updatedAt,
|
|
5679
|
+
sessionCount: 0,
|
|
5680
|
+
activeAgents: 0,
|
|
5681
|
+
});
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
4489
5684
|
sendJson(res, 200, {
|
|
4490
5685
|
initiatives: initiatives.slice(0, limit),
|
|
4491
5686
|
total: initiatives.length,
|
|
5687
|
+
localFallback: true,
|
|
5688
|
+
warning: safeErrorMessage(err),
|
|
4492
5689
|
});
|
|
4493
5690
|
}
|
|
4494
5691
|
catch (localErr) {
|