@useorgx/openclaw-plugin 0.3.1 → 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.
Files changed (62) hide show
  1. package/dashboard/dist/assets/MissionControlView-DVNfDWKZ.js +1 -0
  2. package/dashboard/dist/assets/SessionInspector-BaqnAys4.js +1 -0
  3. package/dashboard/dist/assets/index-B4Yix84X.js +212 -0
  4. package/dashboard/dist/assets/index-BWSvw1HR.css +1 -0
  5. package/dashboard/dist/assets/motion-x9c01cgK.js +9 -0
  6. package/dashboard/dist/assets/orgx-logo-Fm0FhtnV.png +0 -0
  7. package/dashboard/dist/assets/react-vendor-C2t2w4r2.js +32 -0
  8. package/dashboard/dist/assets/tanstack-C-KIc3Wc.js +1 -0
  9. package/dashboard/dist/assets/vendor-C-AHK0Ly.js +9 -0
  10. package/dashboard/dist/index.html +6 -2
  11. package/dist/agent-context-store.d.ts.map +1 -1
  12. package/dist/agent-context-store.js +21 -20
  13. package/dist/agent-context-store.js.map +1 -1
  14. package/dist/agent-run-store.d.ts.map +1 -1
  15. package/dist/agent-run-store.js +21 -20
  16. package/dist/agent-run-store.js.map +1 -1
  17. package/dist/auth-store.d.ts.map +1 -1
  18. package/dist/auth-store.js +39 -44
  19. package/dist/auth-store.js.map +1 -1
  20. package/dist/byok-store.d.ts.map +1 -1
  21. package/dist/byok-store.js +24 -20
  22. package/dist/byok-store.js.map +1 -1
  23. package/dist/contracts/types.d.ts +33 -0
  24. package/dist/contracts/types.d.ts.map +1 -1
  25. package/dist/fs-utils.d.ts +7 -0
  26. package/dist/fs-utils.d.ts.map +1 -0
  27. package/dist/fs-utils.js +63 -0
  28. package/dist/fs-utils.js.map +1 -0
  29. package/dist/http-handler.d.ts +17 -0
  30. package/dist/http-handler.d.ts.map +1 -1
  31. package/dist/http-handler.js +1586 -119
  32. package/dist/http-handler.js.map +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/mcp-apps/orgx-live.html +690 -0
  37. package/dist/outbox.d.ts.map +1 -1
  38. package/dist/outbox.js +74 -29
  39. package/dist/outbox.js.map +1 -1
  40. package/dist/paths.d.ts +23 -0
  41. package/dist/paths.d.ts.map +1 -0
  42. package/dist/paths.js +50 -0
  43. package/dist/paths.js.map +1 -0
  44. package/dist/reporting/outbox-replay.d.ts +2 -0
  45. package/dist/reporting/outbox-replay.d.ts.map +1 -0
  46. package/dist/reporting/outbox-replay.js +17 -0
  47. package/dist/reporting/outbox-replay.js.map +1 -0
  48. package/dist/reporting/rollups.d.ts +21 -0
  49. package/dist/reporting/rollups.d.ts.map +1 -0
  50. package/dist/reporting/rollups.js +85 -0
  51. package/dist/reporting/rollups.js.map +1 -0
  52. package/dist/runtime-instance-store.d.ts +63 -0
  53. package/dist/runtime-instance-store.d.ts.map +1 -0
  54. package/dist/runtime-instance-store.js +363 -0
  55. package/dist/runtime-instance-store.js.map +1 -0
  56. package/dist/snapshot-store.d.ts.map +1 -1
  57. package/dist/snapshot-store.js +24 -20
  58. package/dist/snapshot-store.js.map +1 -1
  59. package/package.json +2 -2
  60. package/dashboard/dist/assets/index-BjqNjHpY.css +0 -1
  61. package/dashboard/dist/assets/index-DCLkU4AM.js +0 -57
  62. package/dashboard/dist/assets/orgx-logo-QSE5QWy4.png +0 -0
@@ -23,10 +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";
31
+ import { computeMilestoneRollup, computeWorkstreamRollup, } from "./reporting/rollups.js";
32
+ import { listRuntimeInstances, resolveRuntimeHookToken, upsertRuntimeInstanceFromHook, } from "./runtime-instance-store.js";
30
33
  // =============================================================================
31
34
  // Helpers
32
35
  // =============================================================================
@@ -37,6 +40,10 @@ function safeErrorMessage(err) {
37
40
  return err;
38
41
  return "Unexpected error";
39
42
  }
43
+ function isUnauthorizedOrgxError(err) {
44
+ const message = safeErrorMessage(err).toLowerCase();
45
+ return message.includes("401") || message.includes("unauthorized");
46
+ }
40
47
  function isUserScopedApiKey(apiKey) {
41
48
  return apiKey.trim().toLowerCase().startsWith("oxk_");
42
49
  }
@@ -476,6 +483,110 @@ function mergeActivities(base, extra, limit) {
476
483
  }
477
484
  return deduped;
478
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
+ }
479
590
  const ACTIVITY_HEADLINE_TIMEOUT_MS = 4_000;
480
591
  const ACTIVITY_HEADLINE_CACHE_TTL_MS = 12 * 60 * 60_000;
481
592
  const ACTIVITY_HEADLINE_CACHE_MAX = 1_000;
@@ -694,7 +805,7 @@ function contentType(filePath) {
694
805
  // =============================================================================
695
806
  const CORS_HEADERS = {
696
807
  "Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
697
- "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",
698
809
  Vary: "Origin",
699
810
  };
700
811
  const SECURITY_HEADERS = {
@@ -772,10 +883,14 @@ function resolveSafeDistPath(subPath) {
772
883
  // =============================================================================
773
884
  // Helpers
774
885
  // =============================================================================
886
+ const IMMUTABLE_FILE_CACHE = new Map();
887
+ const IMMUTABLE_FILE_CACHE_MAX = 128;
775
888
  function sendJson(res, status, data) {
776
889
  const body = JSON.stringify(data);
777
890
  res.writeHead(status, {
778
891
  "Content-Type": "application/json; charset=utf-8",
892
+ // Avoid browser/proxy caching for live dashboards.
893
+ "Cache-Control": "no-store",
779
894
  ...SECURITY_HEADERS,
780
895
  ...CORS_HEADERS,
781
896
  });
@@ -783,9 +898,32 @@ function sendJson(res, status, data) {
783
898
  }
784
899
  function sendFile(res, filePath, cacheControl) {
785
900
  try {
901
+ const shouldCacheImmutable = cacheControl.includes("immutable");
902
+ if (shouldCacheImmutable) {
903
+ const cached = IMMUTABLE_FILE_CACHE.get(filePath);
904
+ if (cached) {
905
+ res.writeHead(200, {
906
+ "Content-Type": cached.contentType,
907
+ "Cache-Control": cacheControl,
908
+ ...SECURITY_HEADERS,
909
+ ...CORS_HEADERS,
910
+ });
911
+ res.end(cached.content);
912
+ return;
913
+ }
914
+ }
786
915
  const content = readFileSync(filePath);
916
+ const type = contentType(filePath);
917
+ if (shouldCacheImmutable) {
918
+ if (IMMUTABLE_FILE_CACHE.size >= IMMUTABLE_FILE_CACHE_MAX) {
919
+ const firstKey = IMMUTABLE_FILE_CACHE.keys().next().value;
920
+ if (firstKey)
921
+ IMMUTABLE_FILE_CACHE.delete(firstKey);
922
+ }
923
+ IMMUTABLE_FILE_CACHE.set(filePath, { content, contentType: type });
924
+ }
787
925
  res.writeHead(200, {
788
- "Content-Type": contentType(filePath),
926
+ "Content-Type": type,
789
927
  "Cache-Control": cacheControl,
790
928
  ...SECURITY_HEADERS,
791
929
  ...CORS_HEADERS,
@@ -1068,6 +1206,15 @@ function parseBooleanQuery(raw) {
1068
1206
  normalized === "yes" ||
1069
1207
  normalized === "on");
1070
1208
  }
1209
+ function stableHash(value) {
1210
+ return createHash("sha256").update(value).digest("hex");
1211
+ }
1212
+ function idempotencyKey(parts) {
1213
+ const raw = parts.filter((part) => typeof part === "string" && part.length > 0).join(":");
1214
+ const cleaned = raw.replace(/[^a-zA-Z0-9:_-]/g, "-").slice(0, 84);
1215
+ const suffix = stableHash(raw).slice(0, 20);
1216
+ return `${cleaned}:${suffix}`.slice(0, 120);
1217
+ }
1071
1218
  const DEFAULT_DURATION_HOURS = {
1072
1219
  initiative: 40,
1073
1220
  workstream: 16,
@@ -1444,6 +1591,17 @@ function isInProgressStatus(status) {
1444
1591
  normalized === "running" ||
1445
1592
  normalized === "queued");
1446
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
+ }
1447
1605
  function isDoneStatus(status) {
1448
1606
  const normalized = status.toLowerCase();
1449
1607
  return (normalized === "done" ||
@@ -1865,9 +2023,226 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
1865
2023
  const dashboardEnabled = config.dashboardEnabled ??
1866
2024
  true;
1867
2025
  const outboxAdapter = adapters?.outbox ?? defaultOutboxAdapter;
2026
+ const openclawAdapter = adapters?.openclaw ?? {};
2027
+ const listAgents = openclawAdapter.listAgents ?? listOpenClawAgents;
2028
+ const spawnAgentTurn = openclawAdapter.spawnAgentTurn ?? spawnOpenClawAgentTurn;
2029
+ const stopProcess = openclawAdapter.stopDetachedProcess ?? stopDetachedProcess;
2030
+ const pidAlive = openclawAdapter.isPidAlive ?? isPidAlive;
2031
+ async function emitActivitySafe(input) {
2032
+ const initiativeId = input.initiativeId?.trim() ?? "";
2033
+ if (!initiativeId)
2034
+ return;
2035
+ const message = input.message.trim();
2036
+ if (!message)
2037
+ return;
2038
+ try {
2039
+ await client.emitActivity({
2040
+ initiative_id: initiativeId,
2041
+ run_id: input.runId ?? undefined,
2042
+ correlation_id: input.runId
2043
+ ? undefined
2044
+ : (input.correlationId?.trim() || `openclaw-${Date.now()}`),
2045
+ source_client: "openclaw",
2046
+ message,
2047
+ phase: input.phase,
2048
+ progress_pct: typeof input.progressPct === "number" && Number.isFinite(input.progressPct)
2049
+ ? Math.max(0, Math.min(100, Math.round(input.progressPct)))
2050
+ : undefined,
2051
+ level: input.level,
2052
+ next_step: input.nextStep,
2053
+ metadata: input.metadata,
2054
+ });
2055
+ }
2056
+ catch {
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
+ }
2107
+ }
2108
+ }
2109
+ async function syncParentRollupsForTask(input) {
2110
+ const initiativeId = input.initiativeId?.trim() ?? "";
2111
+ const taskId = input.taskId?.trim() ?? "";
2112
+ if (!initiativeId || !taskId)
2113
+ return;
2114
+ let tasks = [];
2115
+ try {
2116
+ const response = await client.listEntities("task", {
2117
+ initiative_id: initiativeId,
2118
+ limit: 4000,
2119
+ });
2120
+ tasks = Array.isArray(response?.data)
2121
+ ? response.data
2122
+ : [];
2123
+ }
2124
+ catch {
2125
+ return;
2126
+ }
2127
+ const task = tasks.find((row) => String(row.id ?? "").trim() === taskId) ?? null;
2128
+ const resolvedMilestoneId = (input.milestoneId?.trim() || "") ||
2129
+ (task ? pickString(task, ["milestone_id", "milestoneId"]) ?? "" : "");
2130
+ const resolvedWorkstreamId = (input.workstreamId?.trim() || "") ||
2131
+ (task ? pickString(task, ["workstream_id", "workstreamId"]) ?? "" : "");
2132
+ if (resolvedMilestoneId) {
2133
+ const milestoneTaskStatuses = tasks
2134
+ .filter((row) => pickString(row, ["milestone_id", "milestoneId"]) === resolvedMilestoneId)
2135
+ .map((row) => pickString(row, ["status"]) ?? "todo");
2136
+ const rollup = computeMilestoneRollup(milestoneTaskStatuses);
2137
+ try {
2138
+ await client.applyChangeset({
2139
+ initiative_id: initiativeId,
2140
+ correlation_id: input.correlationId?.trim() || undefined,
2141
+ source_client: "openclaw",
2142
+ idempotency_key: idempotencyKey([
2143
+ "openclaw",
2144
+ "rollup",
2145
+ "milestone",
2146
+ resolvedMilestoneId,
2147
+ rollup.status,
2148
+ String(rollup.progressPct),
2149
+ String(rollup.done),
2150
+ String(rollup.total),
2151
+ ]),
2152
+ operations: [
2153
+ {
2154
+ op: "milestone.update",
2155
+ milestone_id: resolvedMilestoneId,
2156
+ status: rollup.status,
2157
+ },
2158
+ ],
2159
+ });
2160
+ }
2161
+ catch {
2162
+ // best effort
2163
+ }
2164
+ }
2165
+ if (resolvedWorkstreamId) {
2166
+ const workstreamTaskStatuses = tasks
2167
+ .filter((row) => pickString(row, ["workstream_id", "workstreamId"]) === resolvedWorkstreamId)
2168
+ .map((row) => pickString(row, ["status"]) ?? "todo");
2169
+ const rollup = computeWorkstreamRollup(workstreamTaskStatuses);
2170
+ try {
2171
+ await client.updateEntity("workstream", resolvedWorkstreamId, {
2172
+ status: rollup.status,
2173
+ });
2174
+ }
2175
+ catch {
2176
+ // best effort
2177
+ }
2178
+ }
2179
+ }
1868
2180
  const autoContinueRuns = new Map();
2181
+ const localInitiativeStatusOverrides = new Map();
1869
2182
  let autoContinueTickInFlight = false;
1870
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
+ };
1871
2246
  function normalizeTokenBudget(value, fallback) {
1872
2247
  if (typeof value === "number" && Number.isFinite(value)) {
1873
2248
  return Math.max(1_000, Math.round(value));
@@ -2067,6 +2442,59 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2067
2442
  // best effort
2068
2443
  }
2069
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
+ }
2070
2498
  async function tickAutoContinueRun(run) {
2071
2499
  if (run.status !== "running" && run.status !== "stopping")
2072
2500
  return;
@@ -2075,7 +2503,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2075
2503
  if (run.activeRunId) {
2076
2504
  const record = getAgentRun(run.activeRunId);
2077
2505
  const pid = record?.pid ?? null;
2078
- if (pid && isPidAlive(pid)) {
2506
+ if (pid && pidAlive(pid)) {
2079
2507
  return;
2080
2508
  }
2081
2509
  // Run finished (or pid missing). Mark stopped and auto-complete the task.
@@ -2104,6 +2532,34 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2104
2532
  run.lastError = safeErrorMessage(err);
2105
2533
  }
2106
2534
  }
2535
+ if (record.taskId) {
2536
+ await syncParentRollupsForTask({
2537
+ initiativeId: run.initiativeId,
2538
+ taskId: record.taskId,
2539
+ workstreamId: record.workstreamId,
2540
+ correlationId: record.runId,
2541
+ });
2542
+ }
2543
+ await emitActivitySafe({
2544
+ initiativeId: run.initiativeId,
2545
+ correlationId: record.runId,
2546
+ phase: summary.hadError ? "blocked" : "completed",
2547
+ level: summary.hadError ? "warn" : "info",
2548
+ message: record.taskId
2549
+ ? `Auto-continue ${summary.hadError ? "blocked" : "completed"} task ${record.taskId}.`
2550
+ : `Auto-continue run finished (${summary.hadError ? "blocked" : "completed"}).`,
2551
+ metadata: {
2552
+ event: "auto_continue_task_finished",
2553
+ agent_id: record.agentId,
2554
+ session_id: record.runId,
2555
+ task_id: record.taskId,
2556
+ workstream_id: record.workstreamId,
2557
+ tokens: summary.tokens,
2558
+ cost_usd: summary.costUsd,
2559
+ had_error: summary.hadError,
2560
+ error_message: summary.errorMessage,
2561
+ },
2562
+ });
2107
2563
  run.lastRunId = record.runId;
2108
2564
  run.lastTaskId = record.taskId ?? run.lastTaskId;
2109
2565
  run.activeRunId = null;
@@ -2147,7 +2603,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2147
2603
  // 3) Pick next-up task and dispatch.
2148
2604
  let graph;
2149
2605
  try {
2150
- graph = await buildMissionControlGraph(client, run.initiativeId);
2606
+ graph = applyLocalInitiativeOverrideToGraph(await buildMissionControlGraph(client, run.initiativeId));
2151
2607
  }
2152
2608
  catch (err) {
2153
2609
  await stopAutoContinueRun({
@@ -2194,7 +2650,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2194
2650
  }
2195
2651
  if (node.workstreamId) {
2196
2652
  const ws = nodeById.get(node.workstreamId);
2197
- if (ws && !isInProgressStatus(ws.status)) {
2653
+ if (ws && !isDispatchableWorkstreamStatus(ws.status)) {
2198
2654
  continue;
2199
2655
  }
2200
2656
  }
@@ -2237,6 +2693,21 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2237
2693
  ]
2238
2694
  .filter((line) => typeof line === "string")
2239
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
+ }
2240
2711
  try {
2241
2712
  await client.updateEntity("task", nextTaskNode.id, {
2242
2713
  status: "in_progress",
@@ -2250,6 +2721,31 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2250
2721
  });
2251
2722
  return;
2252
2723
  }
2724
+ await syncParentRollupsForTask({
2725
+ initiativeId: run.initiativeId,
2726
+ taskId: nextTaskNode.id,
2727
+ workstreamId: nextTaskNode.workstreamId,
2728
+ milestoneId: nextTaskNode.milestoneId,
2729
+ correlationId: sessionId,
2730
+ });
2731
+ await emitActivitySafe({
2732
+ initiativeId: run.initiativeId,
2733
+ correlationId: sessionId,
2734
+ phase: "execution",
2735
+ level: "info",
2736
+ message: `Auto-continue started task ${nextTaskNode.id}.`,
2737
+ metadata: {
2738
+ event: "auto_continue_task_started",
2739
+ agent_id: agentId,
2740
+ session_id: sessionId,
2741
+ task_id: nextTaskNode.id,
2742
+ task_title: nextTaskNode.title,
2743
+ workstream_id: nextTaskNode.workstreamId,
2744
+ workstream_title: workstreamTitle,
2745
+ milestone_id: nextTaskNode.milestoneId,
2746
+ milestone_title: milestoneTitle,
2747
+ },
2748
+ });
2253
2749
  upsertAgentContext({
2254
2750
  agentId,
2255
2751
  initiativeId: run.initiativeId,
@@ -2257,7 +2753,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2257
2753
  workstreamId: nextTaskNode.workstreamId,
2258
2754
  taskId: nextTaskNode.id,
2259
2755
  });
2260
- const spawned = spawnOpenClawAgentTurn({
2756
+ const spawned = spawnAgentTurn({
2261
2757
  agentId,
2262
2758
  sessionId,
2263
2759
  message,
@@ -2319,6 +2815,528 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2319
2815
  autoContinueTickInFlight = false;
2320
2816
  }
2321
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
+ }
2322
3340
  const autoContinueTimer = setInterval(() => {
2323
3341
  void tickAllAutoContinue();
2324
3342
  }, AUTO_CONTINUE_TICK_MS);
@@ -2363,6 +3381,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2363
3381
  const runCheckpointRestoreMatch = route.match(/^runs\/([^/]+)\/checkpoints\/([^/]+)\/restore$/);
2364
3382
  const isDelegationPreflight = route === "delegation/preflight";
2365
3383
  const isMissionControlAutoAssignmentRoute = route === "mission-control/assignments/auto";
3384
+ const isMissionControlNextUpPlayRoute = route === "mission-control/next-up/play";
2366
3385
  const isMissionControlAutoContinueStartRoute = route === "mission-control/auto-continue/start";
2367
3386
  const isMissionControlAutoContinueStopRoute = route === "mission-control/auto-continue/stop";
2368
3387
  const isEntitiesRoute = route === "entities";
@@ -2553,7 +3572,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2553
3572
  let requiresPremiumLaunch = Boolean(provider) || modelImpliesByok(requestedModel);
2554
3573
  if (!requiresPremiumLaunch) {
2555
3574
  try {
2556
- const agents = await listOpenClawAgents();
3575
+ const agents = await listAgents();
2557
3576
  const agentEntry = agents.find((entry) => String(entry.id ?? "").trim() === agentId) ??
2558
3577
  null;
2559
3578
  const agentModel = agentEntry && typeof agentEntry.model === "string"
@@ -2610,6 +3629,46 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2610
3629
  });
2611
3630
  return true;
2612
3631
  }
3632
+ if (initiativeId) {
3633
+ try {
3634
+ await client.updateEntity("initiative", initiativeId, { status: "active" });
3635
+ }
3636
+ catch {
3637
+ // best effort
3638
+ }
3639
+ }
3640
+ if (taskId) {
3641
+ try {
3642
+ await client.updateEntity("task", taskId, { status: "in_progress" });
3643
+ }
3644
+ catch {
3645
+ // best effort
3646
+ }
3647
+ await syncParentRollupsForTask({
3648
+ initiativeId,
3649
+ taskId,
3650
+ workstreamId,
3651
+ correlationId: sessionId,
3652
+ });
3653
+ }
3654
+ await emitActivitySafe({
3655
+ initiativeId,
3656
+ correlationId: sessionId,
3657
+ phase: "execution",
3658
+ message: taskId
3659
+ ? `Launched agent ${agentId} for task ${taskId}.`
3660
+ : `Launched agent ${agentId}.`,
3661
+ level: "info",
3662
+ metadata: {
3663
+ event: "agent_launch",
3664
+ agent_id: agentId,
3665
+ session_id: sessionId,
3666
+ workstream_id: workstreamId,
3667
+ task_id: taskId,
3668
+ provider,
3669
+ model: requestedModel,
3670
+ },
3671
+ });
2613
3672
  let routedProvider = null;
2614
3673
  let routedModel = null;
2615
3674
  if (provider) {
@@ -2628,7 +3687,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2628
3687
  workstreamId,
2629
3688
  taskId,
2630
3689
  });
2631
- const spawned = spawnOpenClawAgentTurn({
3690
+ const spawned = spawnAgentTurn({
2632
3691
  agentId,
2633
3692
  sessionId,
2634
3693
  message,
@@ -2692,7 +3751,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2692
3751
  sendJson(res, 409, { ok: false, error: "Run has no tracked pid" });
2693
3752
  return true;
2694
3753
  }
2695
- const result = await stopDetachedProcess(record.pid);
3754
+ const result = await stopProcess(record.pid);
2696
3755
  const updated = markAgentRunStopped(runId);
2697
3756
  sendJson(res, 200, {
2698
3757
  ok: true,
@@ -2752,7 +3811,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2752
3811
  modelImpliesByok(record.model ?? null);
2753
3812
  if (!requiresPremiumRestart) {
2754
3813
  try {
2755
- const agents = await listOpenClawAgents();
3814
+ const agents = await listAgents();
2756
3815
  const agentEntry = agents.find((entry) => String(entry.id ?? "").trim() === record.agentId) ?? null;
2757
3816
  const agentModel = agentEntry && typeof agentEntry.model === "string"
2758
3817
  ? agentEntry.model
@@ -2802,7 +3861,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2802
3861
  workstreamId: record.workstreamId,
2803
3862
  taskId: record.taskId,
2804
3863
  });
2805
- const spawned = spawnOpenClawAgentTurn({
3864
+ const spawned = spawnAgentTurn({
2806
3865
  agentId: record.agentId,
2807
3866
  sessionId,
2808
3867
  message,
@@ -2836,6 +3895,135 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2836
3895
  }
2837
3896
  return true;
2838
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
+ }
2839
4027
  if (method === "POST" && isMissionControlAutoContinueStartRoute) {
2840
4028
  try {
2841
4029
  const payload = await parseJsonRequest(req);
@@ -2861,37 +4049,13 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2861
4049
  });
2862
4050
  return true;
2863
4051
  }
2864
- let requiresPremiumAutoContinue = false;
2865
- try {
2866
- const agents = await listOpenClawAgents();
2867
- const agentEntry = agents.find((entry) => String(entry.id ?? "").trim() === agentId) ??
2868
- null;
2869
- const agentModel = agentEntry && typeof agentEntry.model === "string"
2870
- ? agentEntry.model
2871
- : null;
2872
- requiresPremiumAutoContinue = modelImpliesByok(agentModel);
2873
- }
2874
- catch {
2875
- // ignore
2876
- }
2877
- if (requiresPremiumAutoContinue) {
2878
- const billingStatus = await fetchBillingStatusSafe(client);
2879
- if (billingStatus && billingStatus.plan === "free") {
2880
- const pricingUrl = `${client.getBaseUrl().replace(/\/+$/, "")}/pricing`;
2881
- sendJson(res, 402, {
2882
- ok: false,
2883
- code: "upgrade_required",
2884
- error: "Auto-continue for BYOK agents requires a paid OrgX plan. Upgrade, then retry.",
2885
- currentPlan: billingStatus.plan,
2886
- requiredPlan: "starter",
2887
- actions: {
2888
- checkout: "/orgx/api/billing/checkout",
2889
- portal: "/orgx/api/billing/portal",
2890
- pricing: pricingUrl,
2891
- },
2892
- });
2893
- return true;
2894
- }
4052
+ const upgradeGate = await resolveAutoContinueUpgradeGate(agentId);
4053
+ if (upgradeGate) {
4054
+ sendJson(res, 402, {
4055
+ ok: false,
4056
+ ...upgradeGate,
4057
+ });
4058
+ return true;
2895
4059
  }
2896
4060
  const tokenBudget = pickNumber(payload, [
2897
4061
  "tokenBudget",
@@ -2935,53 +4099,13 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2935
4099
  .filter(Boolean),
2936
4100
  ]);
2937
4101
  const allowedWorkstreamIds = workstreamFilter.length > 0 ? workstreamFilter : null;
2938
- const now = new Date().toISOString();
2939
- const existing = autoContinueRuns.get(initiativeId) ?? null;
2940
- const run = existing ??
2941
- {
2942
- initiativeId,
2943
- agentId,
2944
- includeVerification: false,
2945
- allowedWorkstreamIds: null,
2946
- tokenBudget: defaultAutoContinueTokenBudget(),
2947
- tokensUsed: 0,
2948
- status: "running",
2949
- stopReason: null,
2950
- stopRequested: false,
2951
- startedAt: now,
2952
- stoppedAt: null,
2953
- updatedAt: now,
2954
- lastError: null,
2955
- lastTaskId: null,
2956
- lastRunId: null,
2957
- activeTaskId: null,
2958
- activeRunId: null,
2959
- activeTaskTokenEstimate: null,
2960
- };
2961
- run.agentId = agentId;
2962
- run.includeVerification = includeVerification;
2963
- run.allowedWorkstreamIds = allowedWorkstreamIds;
2964
- run.tokenBudget = normalizeTokenBudget(tokenBudget, run.tokenBudget || defaultAutoContinueTokenBudget());
2965
- run.status = "running";
2966
- run.stopReason = null;
2967
- run.stopRequested = false;
2968
- run.startedAt = now;
2969
- run.stoppedAt = null;
2970
- run.updatedAt = now;
2971
- run.lastError = null;
2972
- autoContinueRuns.set(initiativeId, run);
2973
- try {
2974
- await client.updateEntity("initiative", initiativeId, { status: "active" });
2975
- }
2976
- catch {
2977
- // best effort
2978
- }
2979
- try {
2980
- await updateInitiativeAutoContinueState({ initiativeId, run });
2981
- }
2982
- catch {
2983
- // best effort
2984
- }
4102
+ const run = await startAutoContinueRun({
4103
+ initiativeId,
4104
+ agentId,
4105
+ tokenBudget,
4106
+ includeVerification,
4107
+ allowedWorkstreamIds,
4108
+ });
2985
4109
  sendJson(res, 200, { ok: true, run });
2986
4110
  }
2987
4111
  catch (err) {
@@ -3200,11 +4324,38 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3200
4324
  const entityAction = decodeURIComponent(entityActionMatch[3]);
3201
4325
  const payload = await parseJsonRequest(req);
3202
4326
  if (entityAction === "delete") {
3203
- // Delete via status update
3204
- const entity = await client.updateEntity(entityType, entityId, {
3205
- status: "deleted",
3206
- });
3207
- sendJson(res, 200, { ok: true, entity });
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
+ }
3208
4359
  }
3209
4360
  else {
3210
4361
  // Map action to status update
@@ -3223,11 +4374,34 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3223
4374
  });
3224
4375
  return true;
3225
4376
  }
3226
- const entity = await client.updateEntity(entityType, entityId, {
3227
- status: newStatus,
3228
- ...(payload.force ? { force: true } : {}),
3229
- });
3230
- sendJson(res, 200, { ok: true, entity });
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
+ }
3231
4405
  }
3232
4406
  }
3233
4407
  catch (err) {
@@ -3244,6 +4418,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3244
4418
  !(runActionMatch && method === "POST") &&
3245
4419
  !(isDelegationPreflight && method === "POST") &&
3246
4420
  !(isMissionControlAutoAssignmentRoute && method === "POST") &&
4421
+ !(isMissionControlNextUpPlayRoute && method === "POST") &&
3247
4422
  !(isEntitiesRoute && method === "POST") &&
3248
4423
  !(isEntitiesRoute && method === "PATCH") &&
3249
4424
  !(entityActionMatch && method === "POST") &&
@@ -3338,7 +4513,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3338
4513
  case "agents/catalog": {
3339
4514
  try {
3340
4515
  const [openclawAgents, localSnapshot] = await Promise.all([
3341
- listOpenClawAgents(),
4516
+ listAgents(),
3342
4517
  loadLocalOpenClawSnapshot(240).catch(() => null),
3343
4518
  ]);
3344
4519
  const localById = new Map();
@@ -3425,6 +4600,108 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3425
4600
  case "onboarding":
3426
4601
  sendJson(res, 200, getOnboardingState(await onboarding.getStatus()));
3427
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
+ }
3428
4705
  case "mission-control/auto-continue/status": {
3429
4706
  const initiativeId = searchParams.get("initiative_id") ??
3430
4707
  searchParams.get("initiativeId") ??
@@ -3612,7 +4889,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3612
4889
  agentId = agentId.trim();
3613
4890
  if (!agentId) {
3614
4891
  try {
3615
- const agents = await listOpenClawAgents();
4892
+ const agents = await listAgents();
3616
4893
  const defaultAgent = agents.find((entry) => Boolean(entry.isDefault)) ?? agents[0] ?? null;
3617
4894
  const candidate = defaultAgent && typeof defaultAgent.id === "string" ? defaultAgent.id.trim() : "";
3618
4895
  if (candidate)
@@ -3658,7 +4935,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3658
4935
  return true;
3659
4936
  }
3660
4937
  try {
3661
- const graph = await buildMissionControlGraph(client, initiativeId.trim());
4938
+ const graph = applyLocalInitiativeOverrideToGraph(await buildMissionControlGraph(client, initiativeId.trim()));
3662
4939
  sendJson(res, 200, graph);
3663
4940
  }
3664
4941
  catch (err) {
@@ -3668,6 +4945,29 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3668
4945
  }
3669
4946
  return true;
3670
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
+ }
3671
4971
  case "entities": {
3672
4972
  if (method === "POST") {
3673
4973
  try {
@@ -3716,10 +5016,15 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3716
5016
  return true;
3717
5017
  }
3718
5018
  if (method === "PATCH") {
5019
+ let payload = {};
5020
+ let type = null;
5021
+ let id = null;
5022
+ let requestedStatus = null;
3719
5023
  try {
3720
- const payload = await parseJsonRequest(req);
3721
- const type = pickString(payload, ["type"]);
3722
- const id = pickString(payload, ["id"]);
5024
+ payload = await parseJsonRequest(req);
5025
+ type = pickString(payload, ["type"]);
5026
+ id = pickString(payload, ["id"]);
5027
+ requestedStatus = pickString(payload, ["status"]);
3723
5028
  if (!type || !id) {
3724
5029
  sendJson(res, 400, {
3725
5030
  error: "Both 'type' and 'id' are required for PATCH.",
@@ -3729,37 +5034,91 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3729
5034
  const updates = { ...payload };
3730
5035
  delete updates.type;
3731
5036
  delete updates.id;
3732
- const entity = await client.updateEntity(type, id, normalizeEntityMutationPayload(updates));
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
+ }
3733
5043
  sendJson(res, 200, { ok: true, entity });
3734
5044
  }
3735
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
+ }
3736
5063
  sendJson(res, 500, {
3737
5064
  error: safeErrorMessage(err),
3738
5065
  });
3739
5066
  }
3740
5067
  return true;
3741
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;
3742
5081
  try {
3743
- const type = searchParams.get("type");
3744
- if (!type) {
3745
- sendJson(res, 400, {
3746
- error: "Query parameter 'type' is required for GET /entities.",
3747
- });
3748
- return true;
3749
- }
3750
- const status = searchParams.get("status") ?? undefined;
3751
- const initiativeId = searchParams.get("initiative_id") ?? undefined;
3752
- const limit = searchParams.get("limit")
3753
- ? Number(searchParams.get("limit"))
3754
- : undefined;
3755
5082
  const data = await client.listEntities(type, {
3756
5083
  status,
3757
5084
  initiative_id: initiativeId,
3758
5085
  limit: Number.isFinite(limit) ? limit : undefined,
3759
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
+ }
3760
5098
  sendJson(res, 200, data);
3761
5099
  }
3762
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
+ }
3763
5122
  sendJson(res, 500, {
3764
5123
  error: safeErrorMessage(err),
3765
5124
  });
@@ -4017,12 +5376,22 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4017
5376
  catch (err) {
4018
5377
  degraded.push(`outbox unavailable (${safeErrorMessage(err)})`);
4019
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);
4020
5388
  sendJson(res, 200, {
4021
5389
  sessions,
4022
5390
  activity,
4023
5391
  handoffs,
4024
5392
  decisions,
4025
5393
  agents,
5394
+ runtimeInstances,
4026
5395
  outbox: outboxStatus,
4027
5396
  generatedAt: new Date().toISOString(),
4028
5397
  degraded: degraded.length > 0 ? degraded : undefined,
@@ -4237,7 +5606,30 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4237
5606
  id,
4238
5607
  limit: Number.isFinite(limit) ? limit : undefined,
4239
5608
  });
4240
- sendJson(res, 200, data);
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
+ });
4241
5633
  }
4242
5634
  catch (err) {
4243
5635
  try {
@@ -4251,9 +5643,49 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4251
5643
  if (id && id.trim().length > 0) {
4252
5644
  initiatives = initiatives.filter((item) => item.id === id);
4253
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
+ }
4254
5684
  sendJson(res, 200, {
4255
5685
  initiatives: initiatives.slice(0, limit),
4256
5686
  total: initiatives.length,
5687
+ localFallback: true,
5688
+ warning: safeErrorMessage(err),
4257
5689
  });
4258
5690
  }
4259
5691
  catch (localErr) {
@@ -4313,17 +5745,26 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4313
5745
  let closed = false;
4314
5746
  let streamOpened = false;
4315
5747
  let idleTimer = null;
5748
+ let heartbeatTimer = null;
5749
+ let heartbeatBackpressure = false;
4316
5750
  const clearIdleTimer = () => {
4317
5751
  if (idleTimer) {
4318
5752
  clearTimeout(idleTimer);
4319
5753
  idleTimer = null;
4320
5754
  }
4321
5755
  };
5756
+ const clearHeartbeatTimer = () => {
5757
+ if (heartbeatTimer) {
5758
+ clearInterval(heartbeatTimer);
5759
+ heartbeatTimer = null;
5760
+ }
5761
+ };
4322
5762
  const closeStream = () => {
4323
5763
  if (closed)
4324
5764
  return;
4325
5765
  closed = true;
4326
5766
  clearIdleTimer();
5767
+ clearHeartbeatTimer();
4327
5768
  streamAbortController.abort();
4328
5769
  if (reader) {
4329
5770
  void reader.cancel().catch(() => undefined);
@@ -4373,6 +5814,32 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4373
5814
  ...CORS_HEADERS,
4374
5815
  });
4375
5816
  streamOpened = true;
5817
+ // Heartbeat comments keep intermediary proxies from timing out idle SSE.
5818
+ // They also prevent the dashboard from flickering into reconnect mode
5819
+ // during long quiet periods.
5820
+ heartbeatTimer = setInterval(() => {
5821
+ if (closed || heartbeatBackpressure)
5822
+ return;
5823
+ try {
5824
+ // Keepalive comment line (single newline to avoid terminating an upstream event mid-chunk).
5825
+ const accepted = write(Buffer.from(`: ping ${Date.now()}\n`, "utf8"));
5826
+ resetIdleTimer();
5827
+ if (accepted === false) {
5828
+ heartbeatBackpressure = true;
5829
+ if (typeof res.once === "function") {
5830
+ res.once("drain", () => {
5831
+ heartbeatBackpressure = false;
5832
+ if (!closed)
5833
+ resetIdleTimer();
5834
+ });
5835
+ }
5836
+ }
5837
+ }
5838
+ catch {
5839
+ closeStream();
5840
+ }
5841
+ }, 20_000);
5842
+ heartbeatTimer.unref?.();
4376
5843
  if (!upstream.body) {
4377
5844
  closeStream();
4378
5845
  return true;