@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.
- package/dashboard/dist/assets/MissionControlView-DVNfDWKZ.js +1 -0
- package/dashboard/dist/assets/SessionInspector-BaqnAys4.js +1 -0
- 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/assets/orgx-logo-Fm0FhtnV.png +0 -0
- package/dashboard/dist/assets/react-vendor-C2t2w4r2.js +32 -0
- package/dashboard/dist/assets/tanstack-C-KIc3Wc.js +1 -0
- package/dashboard/dist/assets/vendor-C-AHK0Ly.js +9 -0
- package/dashboard/dist/index.html +6 -2
- package/dist/agent-context-store.d.ts.map +1 -1
- package/dist/agent-context-store.js +21 -20
- package/dist/agent-context-store.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +21 -20
- package/dist/agent-run-store.js.map +1 -1
- package/dist/auth-store.d.ts.map +1 -1
- package/dist/auth-store.js +39 -44
- package/dist/auth-store.js.map +1 -1
- package/dist/byok-store.d.ts.map +1 -1
- package/dist/byok-store.js +24 -20
- package/dist/byok-store.js.map +1 -1
- package/dist/contracts/types.d.ts +33 -0
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/fs-utils.d.ts +7 -0
- package/dist/fs-utils.d.ts.map +1 -0
- package/dist/fs-utils.js +63 -0
- package/dist/fs-utils.js.map +1 -0
- package/dist/http-handler.d.ts +17 -0
- package/dist/http-handler.d.ts.map +1 -1
- package/dist/http-handler.js +1586 -119
- package/dist/http-handler.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-apps/orgx-live.html +690 -0
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +74 -29
- package/dist/outbox.js.map +1 -1
- package/dist/paths.d.ts +23 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +50 -0
- package/dist/paths.js.map +1 -0
- package/dist/reporting/outbox-replay.d.ts +2 -0
- package/dist/reporting/outbox-replay.d.ts.map +1 -0
- package/dist/reporting/outbox-replay.js +17 -0
- package/dist/reporting/outbox-replay.js.map +1 -0
- package/dist/reporting/rollups.d.ts +21 -0
- package/dist/reporting/rollups.d.ts.map +1 -0
- package/dist/reporting/rollups.js +85 -0
- package/dist/reporting/rollups.js.map +1 -0
- package/dist/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/dist/snapshot-store.d.ts.map +1 -1
- package/dist/snapshot-store.js +24 -20
- package/dist/snapshot-store.js.map +1 -1
- package/package.json +2 -2
- package/dashboard/dist/assets/index-BjqNjHpY.css +0 -1
- package/dashboard/dist/assets/index-DCLkU4AM.js +0 -57
- package/dashboard/dist/assets/orgx-logo-QSE5QWy4.png +0 -0
package/dist/http-handler.js
CHANGED
|
@@ -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":
|
|
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 &&
|
|
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 && !
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
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
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
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
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
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
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
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
|
|
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
|
-
|
|
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;
|