@useorgx/openclaw-plugin 0.4.9 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -11
- package/dashboard/dist/assets/6mILZQ2a.js +1 -0
- package/dashboard/dist/assets/6mILZQ2a.js.br +0 -0
- package/dashboard/dist/assets/6mILZQ2a.js.gz +0 -0
- package/dashboard/dist/assets/8dksYiq4.js +2 -0
- package/dashboard/dist/assets/8dksYiq4.js.br +0 -0
- package/dashboard/dist/assets/8dksYiq4.js.gz +0 -0
- package/dashboard/dist/assets/B5zYRHc3.js +1 -0
- package/dashboard/dist/assets/B5zYRHc3.js.br +0 -0
- package/dashboard/dist/assets/B5zYRHc3.js.gz +0 -0
- package/dashboard/dist/assets/B6wPWJ35.js +1 -0
- package/dashboard/dist/assets/B6wPWJ35.js.br +0 -0
- package/dashboard/dist/assets/B6wPWJ35.js.gz +0 -0
- package/dashboard/dist/assets/BJgZIVUQ.js +53 -0
- package/dashboard/dist/assets/BJgZIVUQ.js.br +0 -0
- package/dashboard/dist/assets/BJgZIVUQ.js.gz +0 -0
- package/dashboard/dist/assets/BWEwjt1W.js +1 -0
- package/dashboard/dist/assets/BWEwjt1W.js.br +0 -0
- package/dashboard/dist/assets/BWEwjt1W.js.gz +0 -0
- package/dashboard/dist/assets/BgOYB78t.js +4 -0
- package/dashboard/dist/assets/BgOYB78t.js.br +0 -0
- package/dashboard/dist/assets/BgOYB78t.js.gz +0 -0
- package/dashboard/dist/assets/BzRbDCAD.css +1 -0
- package/dashboard/dist/assets/BzRbDCAD.css.br +0 -0
- package/dashboard/dist/assets/BzRbDCAD.css.gz +0 -0
- package/dashboard/dist/assets/C-KIc3Wc.js.br +0 -0
- package/dashboard/dist/assets/C-KIc3Wc.js.gz +0 -0
- package/dashboard/dist/assets/C8uM3AX8.js +1 -0
- package/dashboard/dist/assets/C8uM3AX8.js.br +0 -0
- package/dashboard/dist/assets/C8uM3AX8.js.gz +0 -0
- package/dashboard/dist/assets/C9jy61eu.js +212 -0
- package/dashboard/dist/assets/C9jy61eu.js.br +0 -0
- package/dashboard/dist/assets/C9jy61eu.js.gz +0 -0
- package/dashboard/dist/assets/CC63EwFD.js +1 -0
- package/dashboard/dist/assets/CC63EwFD.js.br +0 -0
- package/dashboard/dist/assets/CC63EwFD.js.gz +0 -0
- package/dashboard/dist/assets/CL_wXqR7.js +1 -0
- package/dashboard/dist/assets/CL_wXqR7.js.br +0 -0
- package/dashboard/dist/assets/CL_wXqR7.js.gz +0 -0
- package/dashboard/dist/assets/CZaT3ob_.js +1 -0
- package/dashboard/dist/assets/CZaT3ob_.js.br +0 -0
- package/dashboard/dist/assets/CZaT3ob_.js.gz +0 -0
- package/dashboard/dist/assets/CgaottFX.js +1 -0
- package/dashboard/dist/assets/CgaottFX.js.br +0 -0
- package/dashboard/dist/assets/CgaottFX.js.gz +0 -0
- package/dashboard/dist/assets/{CpJsfbXo.js → CxQ08qFN.js} +2 -2
- package/dashboard/dist/assets/CxQ08qFN.js.br +0 -0
- package/dashboard/dist/assets/CxQ08qFN.js.gz +0 -0
- package/dashboard/dist/assets/CzCxAZlW.js +1 -0
- package/dashboard/dist/assets/CzCxAZlW.js.br +0 -0
- package/dashboard/dist/assets/CzCxAZlW.js.gz +0 -0
- package/dashboard/dist/assets/D3iMTYEj.js +1 -0
- package/dashboard/dist/assets/D3iMTYEj.js.br +0 -0
- package/dashboard/dist/assets/D3iMTYEj.js.gz +0 -0
- package/dashboard/dist/assets/D8JNX8kq.js +2 -0
- package/dashboard/dist/assets/D8JNX8kq.js.br +0 -0
- package/dashboard/dist/assets/D8JNX8kq.js.gz +0 -0
- package/dashboard/dist/assets/DnA8dpj6.js +1 -0
- package/dashboard/dist/assets/DnA8dpj6.js.br +0 -0
- package/dashboard/dist/assets/DnA8dpj6.js.gz +0 -0
- package/dashboard/dist/assets/IUexzymk.js +1 -0
- package/dashboard/dist/assets/IUexzymk.js.br +0 -0
- package/dashboard/dist/assets/IUexzymk.js.gz +0 -0
- package/dashboard/dist/assets/cNrhgGc1.js +8 -0
- package/dashboard/dist/assets/cNrhgGc1.js.br +0 -0
- package/dashboard/dist/assets/cNrhgGc1.js.gz +0 -0
- package/dashboard/dist/assets/ic2FaMnh.js +1 -0
- package/dashboard/dist/assets/ic2FaMnh.js.br +0 -0
- package/dashboard/dist/assets/ic2FaMnh.js.gz +0 -0
- package/dashboard/dist/assets/qm8xLgv-.css +1 -0
- package/dashboard/dist/assets/qm8xLgv-.css.br +0 -0
- package/dashboard/dist/assets/qm8xLgv-.css.gz +0 -0
- package/dashboard/dist/assets/rttbDbEx.js +1 -0
- package/dashboard/dist/assets/rttbDbEx.js.br +0 -0
- package/dashboard/dist/assets/rttbDbEx.js.gz +0 -0
- package/dashboard/dist/brand/anthropic-mark.svg.br +0 -0
- package/dashboard/dist/brand/anthropic-mark.svg.gz +0 -0
- package/dashboard/dist/brand/openai-mark.svg.br +0 -0
- package/dashboard/dist/brand/openai-mark.svg.gz +0 -0
- package/dashboard/dist/brand/openclaw-mark.svg.br +0 -0
- package/dashboard/dist/brand/openclaw-mark.svg.gz +0 -0
- package/dashboard/dist/brand/xandy-orchestrator.png +0 -0
- package/dashboard/dist/index.html +7 -5
- package/dashboard/dist/index.html.br +0 -0
- package/dashboard/dist/index.html.gz +0 -0
- package/dist/activity-actor-fields.js +26 -4
- package/dist/activity-store.js +34 -8
- package/dist/agent-context-store.js +79 -17
- package/dist/agent-run-store.js +44 -3
- package/dist/agent-suite.d.ts +9 -0
- package/dist/agent-suite.js +149 -9
- package/dist/artifacts/artifact-domain-schemas.d.ts +66 -0
- package/dist/artifacts/artifact-domain-schemas.js +357 -0
- package/dist/artifacts/register-artifact.d.ts +4 -3
- package/dist/artifacts/register-artifact.js +170 -57
- package/dist/chat-store.d.ts +157 -0
- package/dist/chat-store.js +586 -0
- package/dist/cli/orgx.js +11 -0
- package/dist/contracts/client.d.ts +43 -3
- package/dist/contracts/client.js +159 -30
- package/dist/contracts/practice-exercise-schema.d.ts +216 -0
- package/dist/contracts/practice-exercise-schema.js +314 -0
- package/dist/contracts/retro-schema.d.ts +81 -0
- package/dist/contracts/retro-schema.js +80 -0
- package/dist/contracts/shared-types.d.ts +159 -0
- package/dist/contracts/shared-types.js +199 -1
- package/dist/contracts/skill-pack-schema.d.ts +192 -0
- package/dist/contracts/skill-pack-schema.js +180 -0
- package/dist/contracts/types.d.ts +247 -2
- package/dist/entities/auto-assignment.js +43 -17
- package/dist/event-sanitization.d.ts +11 -0
- package/dist/event-sanitization.js +113 -0
- package/dist/gateway-watchdog.d.ts +5 -0
- package/dist/gateway-watchdog.js +50 -0
- package/dist/hooks/post-reporting-event.mjs +1 -5
- package/dist/http/helpers/activity-headline.js +13 -132
- package/dist/http/helpers/auto-continue-engine.d.ts +198 -10
- package/dist/http/helpers/auto-continue-engine.js +3145 -186
- package/dist/http/helpers/autopilot-operations.d.ts +19 -0
- package/dist/http/helpers/autopilot-operations.js +182 -31
- package/dist/http/helpers/autopilot-runtime.d.ts +1 -0
- package/dist/http/helpers/autopilot-runtime.js +328 -25
- package/dist/http/helpers/autopilot-slice-utils.d.ts +18 -0
- package/dist/http/helpers/autopilot-slice-utils.js +514 -93
- package/dist/http/helpers/decision-mapper.d.ts +40 -0
- package/dist/http/helpers/decision-mapper.js +223 -7
- package/dist/http/helpers/dispatch-lifecycle.d.ts +19 -2
- package/dist/http/helpers/dispatch-lifecycle.js +242 -37
- package/dist/http/helpers/kickoff-context.js +104 -0
- package/dist/http/helpers/llm-client.d.ts +47 -0
- package/dist/http/helpers/llm-client.js +256 -0
- package/dist/http/helpers/mission-control.d.ts +102 -3
- package/dist/http/helpers/mission-control.js +498 -9
- package/dist/http/helpers/sentinel-catalog.d.ts +23 -0
- package/dist/http/helpers/sentinel-catalog.js +193 -0
- package/dist/http/helpers/session-classification.d.ts +9 -0
- package/dist/http/helpers/session-classification.js +564 -0
- package/dist/http/helpers/slice-experience-v2.d.ts +137 -0
- package/dist/http/helpers/slice-experience-v2.js +677 -0
- package/dist/http/helpers/slice-run-projections.d.ts +72 -0
- package/dist/http/helpers/slice-run-projections.js +877 -0
- package/dist/http/helpers/triage-mapper.d.ts +43 -0
- package/dist/http/helpers/triage-mapper.js +549 -0
- package/dist/http/helpers/value-utils.js +7 -2
- package/dist/http/helpers/workspace-scope.d.ts +15 -0
- package/dist/http/helpers/workspace-scope.js +170 -0
- package/dist/http/index.js +1420 -105
- package/dist/http/routes/agent-suite.d.ts +9 -0
- package/dist/http/routes/agent-suite.js +294 -8
- package/dist/http/routes/agents-catalog.js +64 -19
- package/dist/http/routes/chat.d.ts +19 -0
- package/dist/http/routes/chat.js +522 -0
- package/dist/http/routes/decision-actions.d.ts +8 -1
- package/dist/http/routes/decision-actions.js +42 -5
- package/dist/http/routes/dispatch-gateway-envelope.d.ts +25 -0
- package/dist/http/routes/dispatch-gateway-envelope.js +26 -0
- package/dist/http/routes/entities.d.ts +16 -0
- package/dist/http/routes/entities.js +232 -6
- package/dist/http/routes/live-legacy.d.ts +5 -0
- package/dist/http/routes/live-legacy.js +23 -509
- package/dist/http/routes/live-misc.d.ts +12 -0
- package/dist/http/routes/live-misc.js +251 -31
- package/dist/http/routes/live-snapshot.d.ts +49 -2
- package/dist/http/routes/live-snapshot.js +653 -23
- package/dist/http/routes/live-terminal.d.ts +11 -0
- package/dist/http/routes/live-terminal.js +154 -0
- package/dist/http/routes/live-triage.d.ts +61 -0
- package/dist/http/routes/live-triage.js +192 -0
- package/dist/http/routes/mission-control-actions.d.ts +49 -1
- package/dist/http/routes/mission-control-actions.js +1246 -84
- package/dist/http/routes/mission-control-read.d.ts +48 -3
- package/dist/http/routes/mission-control-read.js +1658 -20
- package/dist/http/routes/realtime-orchestrator.d.ts +10 -0
- package/dist/http/routes/realtime-orchestrator.js +74 -0
- package/dist/http/routes/run-control.d.ts +5 -2
- package/dist/http/routes/run-control.js +10 -0
- package/dist/http/routes/sentinels-catalog.d.ts +7 -0
- package/dist/http/routes/sentinels-catalog.js +24 -0
- package/dist/http/routes/summary.js +10 -3
- package/dist/http/routes/usage.d.ts +24 -0
- package/dist/http/routes/usage.js +362 -0
- package/dist/http/routes/work-artifacts.js +28 -9
- package/dist/index.js +165 -27
- package/dist/local-openclaw.js +29 -6
- package/dist/mcp-client-setup.js +3 -3
- package/dist/mcp-http-handler.d.ts +3 -0
- package/dist/mcp-http-handler.js +34 -60
- package/dist/next-up-queue-store.d.ts +16 -1
- package/dist/next-up-queue-store.js +89 -7
- package/dist/outbox.d.ts +5 -0
- package/dist/outbox.js +113 -9
- package/dist/paths.js +36 -5
- package/dist/reporting/rollups.d.ts +41 -0
- package/dist/reporting/rollups.js +113 -0
- package/dist/retro/domain-templates.d.ts +45 -0
- package/dist/retro/domain-templates.js +297 -0
- package/dist/retro/quality-rubric.d.ts +33 -0
- package/dist/retro/quality-rubric.js +213 -0
- package/dist/runtime-cleanup.d.ts +18 -0
- package/dist/runtime-cleanup.js +87 -0
- package/dist/services/background.d.ts +11 -0
- package/dist/services/background.js +22 -0
- package/dist/services/experiment-randomization.d.ts +21 -0
- package/dist/services/experiment-randomization.js +63 -0
- package/dist/skill-pack-state.d.ts +36 -5
- package/dist/skill-pack-state.js +273 -29
- package/dist/sync/local-agent-telemetry.d.ts +13 -0
- package/dist/sync/local-agent-telemetry.js +128 -0
- package/dist/sync/outbox-replay.js +131 -24
- package/dist/team-context-store.d.ts +23 -0
- package/dist/team-context-store.js +116 -0
- package/dist/telemetry/posthog.js +4 -2
- package/dist/tools/core-tools.d.ts +10 -14
- package/dist/tools/core-tools.js +1289 -24
- package/dist/types.d.ts +2 -0
- package/dist/types.js +2 -0
- package/dist/worker-supervisor.js +23 -0
- package/package.json +20 -6
- package/dashboard/dist/assets/B3ziCA02.js +0 -8
- package/dashboard/dist/assets/B5NEElEI.css +0 -1
- package/dashboard/dist/assets/BhapSNAs.js +0 -215
- package/dashboard/dist/assets/iFdvE7lx.js +0 -1
- package/dashboard/dist/assets/jRJsmpYM.js +0 -1
- package/dashboard/dist/assets/sAhvFnpk.js +0 -4
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { resolveWorkspaceScope as resolveCanonicalWorkspaceScope } from "../helpers/workspace-scope.js";
|
|
3
|
+
import { buildDispatchGatewayEnvelope } from "./dispatch-gateway-envelope.js";
|
|
1
4
|
const PLAY_QUEUE_LOOKUP_TIMEOUT_MS = (() => {
|
|
2
5
|
const raw = process.env.ORGX_PLAY_QUEUE_LOOKUP_TIMEOUT_MS;
|
|
3
6
|
const parsed = Number(raw);
|
|
@@ -5,6 +8,17 @@ const PLAY_QUEUE_LOOKUP_TIMEOUT_MS = (() => {
|
|
|
5
8
|
return 350;
|
|
6
9
|
return Math.max(200, Math.floor(parsed));
|
|
7
10
|
})();
|
|
11
|
+
const IN_PROGRESS_TASK_STATUSES = new Set([
|
|
12
|
+
"in_progress",
|
|
13
|
+
"inprogress",
|
|
14
|
+
"active",
|
|
15
|
+
"running",
|
|
16
|
+
"working",
|
|
17
|
+
"planning",
|
|
18
|
+
"dispatching",
|
|
19
|
+
"pending",
|
|
20
|
+
]);
|
|
21
|
+
const BLOCKED_TASK_STATUSES = new Set(["blocked", "stalled", "failed", "error"]);
|
|
8
22
|
async function withSoftTimeout(work, timeoutMs) {
|
|
9
23
|
let timer = null;
|
|
10
24
|
try {
|
|
@@ -22,7 +36,275 @@ async function withSoftTimeout(work, timeoutMs) {
|
|
|
22
36
|
clearTimeout(timer);
|
|
23
37
|
}
|
|
24
38
|
}
|
|
39
|
+
function normalizeStatusValue(value) {
|
|
40
|
+
if (typeof value !== "string")
|
|
41
|
+
return "";
|
|
42
|
+
return value.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
43
|
+
}
|
|
44
|
+
function normalizePlacement(value, fallback = "bottom") {
|
|
45
|
+
if (typeof value !== "string")
|
|
46
|
+
return fallback;
|
|
47
|
+
const normalized = value.trim().toLowerCase();
|
|
48
|
+
if (normalized === "top")
|
|
49
|
+
return "top";
|
|
50
|
+
if (normalized === "bottom")
|
|
51
|
+
return "bottom";
|
|
52
|
+
return fallback;
|
|
53
|
+
}
|
|
54
|
+
function normalizeScope(value) {
|
|
55
|
+
if (value === "task" || value === "milestone" || value === "workstream") {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
const normalized = value.trim().toLowerCase();
|
|
60
|
+
if (normalized === "task" || normalized === "milestone" || normalized === "workstream") {
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function normalizeParallelMode(value) {
|
|
67
|
+
if (typeof value === "string" && value.trim().toLowerCase() === "iwmt") {
|
|
68
|
+
return "iwmt";
|
|
69
|
+
}
|
|
70
|
+
return "iwmt";
|
|
71
|
+
}
|
|
72
|
+
function normalizeMaxParallelSlices(value, fallback) {
|
|
73
|
+
const normalizeValue = (input) => Math.max(1, Math.min(5, Math.floor(input)));
|
|
74
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
75
|
+
return normalizeValue(value);
|
|
76
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
77
|
+
const parsed = Number(value);
|
|
78
|
+
if (Number.isFinite(parsed))
|
|
79
|
+
return normalizeValue(parsed);
|
|
80
|
+
}
|
|
81
|
+
return normalizeValue(fallback);
|
|
82
|
+
}
|
|
83
|
+
function parseQueueOrder(input, deps) {
|
|
84
|
+
const rawOrder = Array.isArray(input) ? input : [];
|
|
85
|
+
const order = [];
|
|
86
|
+
for (const entry of rawOrder) {
|
|
87
|
+
if (!entry)
|
|
88
|
+
continue;
|
|
89
|
+
if (typeof entry === "string") {
|
|
90
|
+
const [initiativeId, workstreamId] = entry.split(":", 2).map((s) => s.trim());
|
|
91
|
+
if (initiativeId && workstreamId)
|
|
92
|
+
order.push({ initiativeId, workstreamId });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (typeof entry !== "object")
|
|
96
|
+
continue;
|
|
97
|
+
const record = entry;
|
|
98
|
+
const initiativeId = (deps.pickString(record, ["initiativeId", "initiative_id"]) ?? "").trim();
|
|
99
|
+
const workstreamId = (deps.pickString(record, ["workstreamId", "workstream_id"]) ?? "").trim();
|
|
100
|
+
if (initiativeId && workstreamId)
|
|
101
|
+
order.push({ initiativeId, workstreamId });
|
|
102
|
+
}
|
|
103
|
+
return order;
|
|
104
|
+
}
|
|
105
|
+
function dedupeQueueOrder(order) {
|
|
106
|
+
const next = [];
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
for (const entry of order) {
|
|
109
|
+
const initiativeId = (entry.initiativeId ?? "").trim();
|
|
110
|
+
const workstreamId = (entry.workstreamId ?? "").trim();
|
|
111
|
+
if (!initiativeId || !workstreamId)
|
|
112
|
+
continue;
|
|
113
|
+
const key = `${initiativeId}:${workstreamId}`;
|
|
114
|
+
if (seen.has(key))
|
|
115
|
+
continue;
|
|
116
|
+
seen.add(key);
|
|
117
|
+
next.push({ initiativeId, workstreamId });
|
|
118
|
+
}
|
|
119
|
+
return next;
|
|
120
|
+
}
|
|
121
|
+
function normalizeSliceLevel(value) {
|
|
122
|
+
if (typeof value !== "string")
|
|
123
|
+
return "workstream";
|
|
124
|
+
const normalized = value.trim().toLowerCase();
|
|
125
|
+
if (normalized === "initiative")
|
|
126
|
+
return "initiative";
|
|
127
|
+
if (normalized === "milestone")
|
|
128
|
+
return "milestone";
|
|
129
|
+
if (normalized === "task")
|
|
130
|
+
return "task";
|
|
131
|
+
return "workstream";
|
|
132
|
+
}
|
|
133
|
+
function normalizeSliceOrderMode(value) {
|
|
134
|
+
if (typeof value !== "string")
|
|
135
|
+
return null;
|
|
136
|
+
const normalized = value.trim().toLowerCase();
|
|
137
|
+
if (normalized === "manual" || normalized === "algorithmic")
|
|
138
|
+
return normalized;
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
function parseSliceOrderForMutation(input) {
|
|
142
|
+
const values = Array.isArray(input) ? input : [];
|
|
143
|
+
const output = [];
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
for (const entry of values) {
|
|
146
|
+
let raw = "";
|
|
147
|
+
if (typeof entry === "string")
|
|
148
|
+
raw = entry;
|
|
149
|
+
else if (entry && typeof entry === "object") {
|
|
150
|
+
const record = entry;
|
|
151
|
+
if (typeof record.sliceId === "string")
|
|
152
|
+
raw = record.sliceId;
|
|
153
|
+
else if (typeof record.id === "string")
|
|
154
|
+
raw = record.id;
|
|
155
|
+
}
|
|
156
|
+
const normalized = raw.trim();
|
|
157
|
+
if (!normalized || seen.has(normalized))
|
|
158
|
+
continue;
|
|
159
|
+
seen.add(normalized);
|
|
160
|
+
output.push(normalized);
|
|
161
|
+
}
|
|
162
|
+
return output;
|
|
163
|
+
}
|
|
164
|
+
function buildPlacedOrder(input) {
|
|
165
|
+
if (input.targets.size === 0)
|
|
166
|
+
return input.order;
|
|
167
|
+
const selected = [];
|
|
168
|
+
const remaining = [];
|
|
169
|
+
for (const entry of input.order) {
|
|
170
|
+
const key = `${entry.initiativeId}:${entry.workstreamId}`;
|
|
171
|
+
if (input.targets.has(key))
|
|
172
|
+
selected.push(entry);
|
|
173
|
+
else
|
|
174
|
+
remaining.push(entry);
|
|
175
|
+
}
|
|
176
|
+
if (selected.length === 0)
|
|
177
|
+
return input.order;
|
|
178
|
+
return input.placement === "top"
|
|
179
|
+
? [...selected, ...remaining]
|
|
180
|
+
: [...remaining, ...selected];
|
|
181
|
+
}
|
|
182
|
+
function shouldResetTaskStatus(status, states) {
|
|
183
|
+
const normalized = normalizeStatusValue(status);
|
|
184
|
+
if (!normalized)
|
|
185
|
+
return false;
|
|
186
|
+
if (normalized === "todo" || normalized === "done" || normalized === "completed") {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
if (states.has("running") && IN_PROGRESS_TASK_STATUSES.has(normalized)) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (states.has("blocked") && BLOCKED_TASK_STATUSES.has(normalized)) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
function asRecord(value) {
|
|
198
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
199
|
+
return null;
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
function asString(value) {
|
|
203
|
+
if (typeof value !== "string")
|
|
204
|
+
return null;
|
|
205
|
+
const trimmed = value.trim();
|
|
206
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
207
|
+
}
|
|
208
|
+
function asStringArray(value) {
|
|
209
|
+
if (!Array.isArray(value))
|
|
210
|
+
return [];
|
|
211
|
+
return value
|
|
212
|
+
.map((entry) => asString(entry))
|
|
213
|
+
.filter((entry) => Boolean(entry));
|
|
214
|
+
}
|
|
215
|
+
function parseCycleGraphNodes(graph) {
|
|
216
|
+
const root = asRecord(graph);
|
|
217
|
+
const rawNodes = Array.isArray(root?.nodes) ? root.nodes : [];
|
|
218
|
+
const nodes = [];
|
|
219
|
+
for (const entry of rawNodes) {
|
|
220
|
+
const record = asRecord(entry);
|
|
221
|
+
if (!record)
|
|
222
|
+
continue;
|
|
223
|
+
const id = asString(record.id);
|
|
224
|
+
const type = asString(record.type);
|
|
225
|
+
if (!id || !type)
|
|
226
|
+
continue;
|
|
227
|
+
if (type !== "initiative" &&
|
|
228
|
+
type !== "workstream" &&
|
|
229
|
+
type !== "milestone" &&
|
|
230
|
+
type !== "task") {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
nodes.push({
|
|
234
|
+
id,
|
|
235
|
+
type,
|
|
236
|
+
title: asString(record.title) ?? id,
|
|
237
|
+
workstreamId: asString(record.workstreamId),
|
|
238
|
+
dependencyIds: Array.from(new Set(asStringArray(record.dependencyIds).filter((depId) => depId !== id))),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return nodes;
|
|
242
|
+
}
|
|
243
|
+
function parseCycleDiagnosticsRemovedEdges(graph) {
|
|
244
|
+
const root = asRecord(graph);
|
|
245
|
+
const diagnostics = asRecord(root?.cycleDiagnostics);
|
|
246
|
+
const rawRemoved = Array.isArray(diagnostics?.removedEdges)
|
|
247
|
+
? diagnostics?.removedEdges
|
|
248
|
+
: [];
|
|
249
|
+
const removedEdges = [];
|
|
250
|
+
for (const entry of rawRemoved) {
|
|
251
|
+
const record = asRecord(entry);
|
|
252
|
+
if (!record)
|
|
253
|
+
continue;
|
|
254
|
+
const from = asString(record.from);
|
|
255
|
+
const to = asString(record.to);
|
|
256
|
+
if (!from || !to)
|
|
257
|
+
continue;
|
|
258
|
+
removedEdges.push({ from, to });
|
|
259
|
+
}
|
|
260
|
+
return removedEdges;
|
|
261
|
+
}
|
|
262
|
+
function detectCycleEdgeKeys(edges) {
|
|
263
|
+
const adjacency = new Map();
|
|
264
|
+
for (const edge of edges) {
|
|
265
|
+
const list = adjacency.get(edge.from) ?? [];
|
|
266
|
+
list.push(edge.to);
|
|
267
|
+
adjacency.set(edge.from, list);
|
|
268
|
+
}
|
|
269
|
+
const visiting = new Set();
|
|
270
|
+
const visited = new Set();
|
|
271
|
+
const cycleEdgeKeys = new Set();
|
|
272
|
+
const dfs = (nodeId) => {
|
|
273
|
+
if (visited.has(nodeId))
|
|
274
|
+
return;
|
|
275
|
+
visiting.add(nodeId);
|
|
276
|
+
const children = adjacency.get(nodeId) ?? [];
|
|
277
|
+
for (const childId of children) {
|
|
278
|
+
if (visiting.has(childId)) {
|
|
279
|
+
cycleEdgeKeys.add(`${nodeId}->${childId}`);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
dfs(childId);
|
|
283
|
+
}
|
|
284
|
+
visiting.delete(nodeId);
|
|
285
|
+
visited.add(nodeId);
|
|
286
|
+
};
|
|
287
|
+
for (const nodeId of adjacency.keys()) {
|
|
288
|
+
if (!visited.has(nodeId))
|
|
289
|
+
dfs(nodeId);
|
|
290
|
+
}
|
|
291
|
+
return cycleEdgeKeys;
|
|
292
|
+
}
|
|
25
293
|
export function registerMissionControlActionsRoutes(router, deps) {
|
|
294
|
+
const sendRouteError = (res, status, location, error, extra = {}) => {
|
|
295
|
+
deps.sendJson(res, status, {
|
|
296
|
+
ok: false,
|
|
297
|
+
error,
|
|
298
|
+
error_location: location,
|
|
299
|
+
...extra,
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
const sendRouteException = (res, location, err, extra = {}) => {
|
|
303
|
+
sendRouteError(res, 500, location, deps.safeErrorMessage(err), extra);
|
|
304
|
+
};
|
|
305
|
+
const resolveWorkspaceScope = (payload, query) => resolveCanonicalWorkspaceScope(query, payload, {
|
|
306
|
+
allowProjectScope: false,
|
|
307
|
+
});
|
|
26
308
|
router.add("POST", "mission-control/next-up/play", async ({ req, query, res }) => {
|
|
27
309
|
try {
|
|
28
310
|
const payload = await deps.parseJsonRequest(req);
|
|
@@ -37,10 +319,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
37
319
|
"")
|
|
38
320
|
.trim();
|
|
39
321
|
if (!initiativeId || !workstreamId) {
|
|
40
|
-
|
|
41
|
-
ok: false,
|
|
42
|
-
error: "initiativeId and workstreamId are required",
|
|
43
|
-
});
|
|
322
|
+
sendRouteError(res, 400, "mission-control.next-up.play.validation", "initiativeId and workstreamId are required");
|
|
44
323
|
return;
|
|
45
324
|
}
|
|
46
325
|
let agentIdRaw = (deps.pickString(payload, ["agentId", "agent_id"]) ??
|
|
@@ -75,10 +354,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
75
354
|
}
|
|
76
355
|
const agentId = agentIdRaw || "main";
|
|
77
356
|
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
|
|
78
|
-
|
|
79
|
-
ok: false,
|
|
80
|
-
error: "agentId must be a simple identifier (letters, numbers, _ or -).",
|
|
81
|
-
});
|
|
357
|
+
sendRouteError(res, 400, "mission-control.next-up.play.validation", "agentId must be a simple identifier (letters, numbers, _ or -).");
|
|
82
358
|
return;
|
|
83
359
|
}
|
|
84
360
|
const requestedAgentName = await deps.resolveAgentDisplayName(agentId, matchedQueueItem?.runnerAgentId === agentId
|
|
@@ -109,11 +385,48 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
109
385
|
: deps.parseBooleanQuery(typeof includeVerificationRaw === "string"
|
|
110
386
|
? includeVerificationRaw
|
|
111
387
|
: null);
|
|
388
|
+
const ignoreSpawnGuardRateLimitRaw = payload.ignoreSpawnGuardRateLimit ??
|
|
389
|
+
payload.ignore_spawn_guard_rate_limit ??
|
|
390
|
+
query.get("ignoreSpawnGuardRateLimit") ??
|
|
391
|
+
query.get("ignore_spawn_guard_rate_limit") ??
|
|
392
|
+
null;
|
|
393
|
+
const ignoreSpawnGuardRateLimit = typeof ignoreSpawnGuardRateLimitRaw === "boolean"
|
|
394
|
+
? ignoreSpawnGuardRateLimitRaw
|
|
395
|
+
: deps.parseBooleanQuery(typeof ignoreSpawnGuardRateLimitRaw === "string"
|
|
396
|
+
? ignoreSpawnGuardRateLimitRaw
|
|
397
|
+
: null);
|
|
398
|
+
const requestedScopeRaw = deps.pickString(payload, ["scope", "sliceScope", "slice_scope"]) ??
|
|
399
|
+
query.get("scope") ??
|
|
400
|
+
query.get("sliceScope") ??
|
|
401
|
+
query.get("slice_scope") ??
|
|
402
|
+
null;
|
|
403
|
+
const queueScope = normalizeScope(matchedQueueItem?.sliceScope ?? null);
|
|
404
|
+
const scope = normalizeScope(requestedScopeRaw) ?? queueScope ?? "task";
|
|
405
|
+
const requestedParallelModeRaw = deps.pickString(payload, ["parallelMode", "parallel_mode"]) ??
|
|
406
|
+
query.get("parallelMode") ??
|
|
407
|
+
query.get("parallel_mode") ??
|
|
408
|
+
null;
|
|
409
|
+
const requestedMaxParallelSlicesRaw = deps.pickNumber(payload, ["maxParallelSlices", "max_parallel_slices"]) ??
|
|
410
|
+
query.get("maxParallelSlices") ??
|
|
411
|
+
query.get("max_parallel_slices") ??
|
|
412
|
+
null;
|
|
413
|
+
const queuePreferredParallel = typeof matchedQueueItem?.executionPolicy?.maxParallelAgents === "number"
|
|
414
|
+
? matchedQueueItem.executionPolicy.maxParallelAgents
|
|
415
|
+
: null;
|
|
416
|
+
const maxParallelSlices = normalizeMaxParallelSlices(requestedMaxParallelSlicesRaw, queuePreferredParallel ?? 1);
|
|
417
|
+
const parallelMode = normalizeParallelMode(requestedParallelModeRaw);
|
|
112
418
|
const existingRun = deps.autoContinueRuns.get(initiativeId) ?? null;
|
|
419
|
+
const existingActiveRunIds = Array.isArray(existingRun?.activeSliceRunIds)
|
|
420
|
+
? (existingRun?.activeSliceRunIds)
|
|
421
|
+
.filter((id) => typeof id === "string" && id.trim().length > 0)
|
|
422
|
+
.map((id) => id.trim())
|
|
423
|
+
: typeof existingRun?.activeRunId === "string" && existingRun.activeRunId.trim().length > 0
|
|
424
|
+
? [existingRun.activeRunId.trim()]
|
|
425
|
+
: [];
|
|
113
426
|
if (existingRun &&
|
|
114
427
|
(existingRun.status === "running" || existingRun.status === "stopping") &&
|
|
115
|
-
|
|
116
|
-
const activeSlice = deps.autoContinueSliceRuns.get(
|
|
428
|
+
existingActiveRunIds.length > 0) {
|
|
429
|
+
const activeSlice = deps.autoContinueSliceRuns.get(existingActiveRunIds[0]) ?? null;
|
|
117
430
|
const activeWorkstreamId = activeSlice?.workstreamId ?? null;
|
|
118
431
|
const activeWorkstreamTitle = activeSlice?.workstreamTitle ?? null;
|
|
119
432
|
deps.sendJson(res, 409, {
|
|
@@ -123,8 +436,10 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
123
436
|
? `Auto-continue is already running for ${activeWorkstreamTitle ?? activeWorkstreamId}. Stop it before launching another Play run.`
|
|
124
437
|
: "Auto-continue is already running for this initiative. Stop it before launching another Play run.",
|
|
125
438
|
run: existingRun,
|
|
439
|
+
activeRunIds: existingActiveRunIds,
|
|
126
440
|
activeWorkstreamId,
|
|
127
441
|
activeWorkstreamTitle,
|
|
442
|
+
error_location: "mission-control.next-up.play.concurrent_run",
|
|
128
443
|
});
|
|
129
444
|
return;
|
|
130
445
|
}
|
|
@@ -135,7 +450,24 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
135
450
|
tokenBudget,
|
|
136
451
|
includeVerification,
|
|
137
452
|
allowedWorkstreamIds: [workstreamId],
|
|
453
|
+
maxParallelSlices,
|
|
454
|
+
parallelMode,
|
|
138
455
|
stopAfterSlice: true,
|
|
456
|
+
ignoreSpawnGuardRateLimit: ignoreSpawnGuardRateLimit === true,
|
|
457
|
+
scope,
|
|
458
|
+
});
|
|
459
|
+
const dispatchId = randomUUID();
|
|
460
|
+
const playDispatchEnvelope = (dispatchMode) => buildDispatchGatewayEnvelope({
|
|
461
|
+
dispatchId,
|
|
462
|
+
dispatchMode,
|
|
463
|
+
route: "mission-control.next-up.play",
|
|
464
|
+
source: "manual_play",
|
|
465
|
+
initiativeId,
|
|
466
|
+
workstreamId,
|
|
467
|
+
workstreamIds: [workstreamId],
|
|
468
|
+
taskIds: Array.isArray(matchedQueueItem?.sliceTaskIds)
|
|
469
|
+
? matchedQueueItem.sliceTaskIds
|
|
470
|
+
: [],
|
|
139
471
|
});
|
|
140
472
|
let fallbackDispatch = null;
|
|
141
473
|
const maybeDispatchFallback = async () => {
|
|
@@ -172,26 +504,50 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
172
504
|
new Promise((resolve) => setTimeout(() => resolve(false), 1100)),
|
|
173
505
|
]);
|
|
174
506
|
if (!tickCompleted) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
507
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
508
|
+
const settledImmediately = Boolean(run.activeRunId) ||
|
|
509
|
+
Boolean(run.lastRunId) ||
|
|
510
|
+
Boolean(run.stopReason) ||
|
|
511
|
+
run.status !== "running";
|
|
512
|
+
if (settledImmediately) {
|
|
513
|
+
await tickPromise.catch(() => null);
|
|
514
|
+
fallbackDispatch = await maybeDispatchFallback();
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
void tickPromise
|
|
518
|
+
.then(async () => {
|
|
519
|
+
await maybeDispatchFallback().catch(() => null);
|
|
520
|
+
})
|
|
521
|
+
.catch(() => {
|
|
522
|
+
// best effort
|
|
523
|
+
});
|
|
524
|
+
deps.sendJson(res, 202, {
|
|
525
|
+
ok: true,
|
|
526
|
+
run,
|
|
527
|
+
initiativeId,
|
|
528
|
+
workstreamId,
|
|
529
|
+
agentId,
|
|
530
|
+
...playDispatchEnvelope("pending"),
|
|
531
|
+
sessionId: null,
|
|
532
|
+
slice: {
|
|
533
|
+
scope,
|
|
534
|
+
taskIds: matchedQueueItem?.sliceTaskIds ?? [],
|
|
535
|
+
taskCount: typeof matchedQueueItem?.sliceTaskCount === "number"
|
|
536
|
+
? matchedQueueItem.sliceTaskCount
|
|
537
|
+
: Array.isArray(matchedQueueItem?.sliceTaskIds)
|
|
538
|
+
? matchedQueueItem.sliceTaskIds.length
|
|
539
|
+
: 0,
|
|
540
|
+
primaryTaskId: matchedQueueItem?.nextTaskId ?? null,
|
|
541
|
+
},
|
|
542
|
+
executionPolicy: matchedQueueItem?.executionPolicy ?? null,
|
|
543
|
+
});
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
await tickPromise;
|
|
549
|
+
fallbackDispatch = await maybeDispatchFallback();
|
|
192
550
|
}
|
|
193
|
-
await tickPromise;
|
|
194
|
-
fallbackDispatch = await maybeDispatchFallback();
|
|
195
551
|
}
|
|
196
552
|
const fallbackStarted = Boolean(fallbackDispatch?.sessionId);
|
|
197
553
|
const dispatchMode = run.activeRunId
|
|
@@ -215,8 +571,42 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
215
571
|
initiativeId,
|
|
216
572
|
workstreamId,
|
|
217
573
|
agentId,
|
|
218
|
-
|
|
574
|
+
...playDispatchEnvelope(finalizedDispatchMode),
|
|
219
575
|
sessionId: run.lastRunId,
|
|
576
|
+
slice: {
|
|
577
|
+
scope,
|
|
578
|
+
taskIds: matchedQueueItem?.sliceTaskIds ?? [],
|
|
579
|
+
taskCount: typeof matchedQueueItem?.sliceTaskCount === "number"
|
|
580
|
+
? matchedQueueItem.sliceTaskCount
|
|
581
|
+
: Array.isArray(matchedQueueItem?.sliceTaskIds)
|
|
582
|
+
? matchedQueueItem.sliceTaskIds.length
|
|
583
|
+
: 0,
|
|
584
|
+
primaryTaskId: matchedQueueItem?.nextTaskId ?? null,
|
|
585
|
+
},
|
|
586
|
+
executionPolicy: matchedQueueItem?.executionPolicy ?? null,
|
|
587
|
+
});
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (dispatchMode === "none" && run.status === "running" && !run.stopReason) {
|
|
591
|
+
deps.sendJson(res, 202, {
|
|
592
|
+
ok: true,
|
|
593
|
+
run,
|
|
594
|
+
initiativeId,
|
|
595
|
+
workstreamId,
|
|
596
|
+
agentId,
|
|
597
|
+
...playDispatchEnvelope("pending"),
|
|
598
|
+
sessionId: null,
|
|
599
|
+
slice: {
|
|
600
|
+
scope,
|
|
601
|
+
taskIds: matchedQueueItem?.sliceTaskIds ?? [],
|
|
602
|
+
taskCount: typeof matchedQueueItem?.sliceTaskCount === "number"
|
|
603
|
+
? matchedQueueItem.sliceTaskCount
|
|
604
|
+
: Array.isArray(matchedQueueItem?.sliceTaskIds)
|
|
605
|
+
? matchedQueueItem.sliceTaskIds.length
|
|
606
|
+
: 0,
|
|
607
|
+
primaryTaskId: matchedQueueItem?.nextTaskId ?? null,
|
|
608
|
+
},
|
|
609
|
+
executionPolicy: matchedQueueItem?.executionPolicy ?? null,
|
|
220
610
|
});
|
|
221
611
|
return;
|
|
222
612
|
}
|
|
@@ -241,6 +631,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
241
631
|
workstreamId,
|
|
242
632
|
agentId,
|
|
243
633
|
fallbackDispatch,
|
|
634
|
+
error_location: "mission-control.next-up.play.dispatch",
|
|
244
635
|
});
|
|
245
636
|
return;
|
|
246
637
|
}
|
|
@@ -250,12 +641,23 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
250
641
|
initiativeId,
|
|
251
642
|
workstreamId,
|
|
252
643
|
agentId,
|
|
253
|
-
dispatchMode,
|
|
644
|
+
...playDispatchEnvelope(dispatchMode),
|
|
254
645
|
sessionId: run.activeRunId ?? fallbackDispatch?.sessionId ?? null,
|
|
646
|
+
slice: {
|
|
647
|
+
scope,
|
|
648
|
+
taskIds: matchedQueueItem?.sliceTaskIds ?? [],
|
|
649
|
+
taskCount: typeof matchedQueueItem?.sliceTaskCount === "number"
|
|
650
|
+
? matchedQueueItem.sliceTaskCount
|
|
651
|
+
: Array.isArray(matchedQueueItem?.sliceTaskIds)
|
|
652
|
+
? matchedQueueItem.sliceTaskIds.length
|
|
653
|
+
: 0,
|
|
654
|
+
primaryTaskId: matchedQueueItem?.nextTaskId ?? null,
|
|
655
|
+
},
|
|
656
|
+
executionPolicy: matchedQueueItem?.executionPolicy ?? null,
|
|
255
657
|
});
|
|
256
658
|
}
|
|
257
659
|
catch (err) {
|
|
258
|
-
|
|
660
|
+
sendRouteException(res, "mission-control.next-up.play.handler", err);
|
|
259
661
|
}
|
|
260
662
|
}, "Mission-control next-up play");
|
|
261
663
|
router.add("POST", "mission-control/next-up/pin", async ({ req, query, res }) => {
|
|
@@ -286,10 +688,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
286
688
|
]) ?? "")
|
|
287
689
|
.trim() || null;
|
|
288
690
|
if (!initiativeId || !workstreamId) {
|
|
289
|
-
|
|
290
|
-
ok: false,
|
|
291
|
-
error: "initiativeId and workstreamId are required",
|
|
292
|
-
});
|
|
691
|
+
sendRouteError(res, 400, "mission-control.next-up.pin.validation", "initiativeId and workstreamId are required");
|
|
293
692
|
return;
|
|
294
693
|
}
|
|
295
694
|
const next = deps.upsertNextUpQueuePin({
|
|
@@ -298,10 +697,11 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
298
697
|
preferredTaskId,
|
|
299
698
|
preferredMilestoneId,
|
|
300
699
|
});
|
|
700
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
301
701
|
deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
|
|
302
702
|
}
|
|
303
703
|
catch (err) {
|
|
304
|
-
|
|
704
|
+
sendRouteException(res, "mission-control.next-up.pin.handler", err);
|
|
305
705
|
}
|
|
306
706
|
}, "Mission-control next-up pin");
|
|
307
707
|
router.add("POST", "mission-control/next-up/unpin", async ({ req, query, res }) => {
|
|
@@ -318,50 +718,769 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
318
718
|
"")
|
|
319
719
|
.trim();
|
|
320
720
|
if (!initiativeId || !workstreamId) {
|
|
321
|
-
|
|
322
|
-
ok: false,
|
|
323
|
-
error: "initiativeId and workstreamId are required",
|
|
324
|
-
});
|
|
721
|
+
sendRouteError(res, 400, "mission-control.next-up.unpin.validation", "initiativeId and workstreamId are required");
|
|
325
722
|
return;
|
|
326
723
|
}
|
|
327
724
|
const next = deps.removeNextUpQueuePin({ initiativeId, workstreamId });
|
|
725
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
328
726
|
deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
|
|
329
727
|
}
|
|
330
728
|
catch (err) {
|
|
331
|
-
|
|
729
|
+
sendRouteException(res, "mission-control.next-up.unpin.handler", err);
|
|
332
730
|
}
|
|
333
731
|
}, "Mission-control next-up unpin");
|
|
334
732
|
router.add("POST", "mission-control/next-up/reorder", async ({ req, res }) => {
|
|
335
733
|
try {
|
|
336
734
|
const payload = await deps.parseJsonRequest(req);
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
735
|
+
const order = dedupeQueueOrder(parseQueueOrder(payload?.order, deps));
|
|
736
|
+
const next = deps.setNextUpQueuePinOrder({ order });
|
|
737
|
+
deps.clearNextUpQueueCache(null);
|
|
738
|
+
deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
sendRouteException(res, "mission-control.next-up.reorder.handler", err);
|
|
742
|
+
}
|
|
743
|
+
}, "Mission-control next-up reorder");
|
|
744
|
+
router.add("POST", "mission-control/slices/reorder", async ({ req, query, res }) => {
|
|
745
|
+
try {
|
|
746
|
+
const payload = await deps.parseJsonRequest(req);
|
|
747
|
+
const level = normalizeSliceLevel(deps.pickString(payload, ["level"]) ?? query.get("level"));
|
|
748
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
749
|
+
query.get("initiativeId") ??
|
|
750
|
+
query.get("initiative_id") ??
|
|
751
|
+
"").trim() || null;
|
|
752
|
+
const scope = resolveWorkspaceScope(payload, query);
|
|
753
|
+
if (scope.error) {
|
|
754
|
+
sendRouteError(res, 400, "mission-control.slices.reorder.validation", scope.error);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const workspaceId = scope.workspaceId;
|
|
758
|
+
const order = parseSliceOrderForMutation(payload?.order);
|
|
759
|
+
const canonicalOrder = order.map((sliceId) => ({ sliceId }));
|
|
760
|
+
const rawRequest = deps.rawRequest ??
|
|
761
|
+
(typeof deps.client?.rawRequest === "function"
|
|
762
|
+
? deps.client.rawRequest.bind(deps.client)
|
|
763
|
+
: null);
|
|
764
|
+
if (!rawRequest) {
|
|
765
|
+
sendRouteError(res, 503, "mission-control.slices.reorder.unavailable", "Canonical mission-control slices API is unavailable");
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const response = await rawRequest("POST", "/api/client/mission-control/slices/reorder", {
|
|
769
|
+
...(workspaceId
|
|
770
|
+
? {
|
|
771
|
+
workspace_id: workspaceId,
|
|
772
|
+
command_center_id: workspaceId,
|
|
773
|
+
}
|
|
774
|
+
: {}),
|
|
775
|
+
level,
|
|
776
|
+
...(initiativeId ? { initiative_id: initiativeId } : {}),
|
|
777
|
+
order: canonicalOrder,
|
|
778
|
+
});
|
|
779
|
+
deps.sendJson(res, 200, {
|
|
780
|
+
...(response && typeof response === "object" ? response : { ok: true }),
|
|
781
|
+
source: "canonical",
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
catch (err) {
|
|
785
|
+
sendRouteError(res, 503, "mission-control.slices.reorder.canonical", "Canonical mission-control slices API unavailable for reorder", {
|
|
786
|
+
degraded: [`canonical unavailable (${deps.safeErrorMessage(err)})`],
|
|
787
|
+
canonical_only: true,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}, "Mission-control slices reorder (canonical)");
|
|
791
|
+
router.add("POST", "mission-control/slices/order-mode", async ({ req, query, res }) => {
|
|
792
|
+
try {
|
|
793
|
+
const payload = await deps.parseJsonRequest(req);
|
|
794
|
+
const level = normalizeSliceLevel(deps.pickString(payload, ["level"]) ?? query.get("level"));
|
|
795
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
796
|
+
query.get("initiativeId") ??
|
|
797
|
+
query.get("initiative_id") ??
|
|
798
|
+
"").trim() || null;
|
|
799
|
+
const scope = resolveWorkspaceScope(payload, query);
|
|
800
|
+
if (scope.error) {
|
|
801
|
+
sendRouteError(res, 400, "mission-control.slices.order-mode.validation", scope.error);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const workspaceId = scope.workspaceId;
|
|
805
|
+
const orderMode = normalizeSliceOrderMode(deps.pickString(payload, ["orderMode", "order_mode"]) ??
|
|
806
|
+
query.get("orderMode") ??
|
|
807
|
+
query.get("order_mode"));
|
|
808
|
+
if (!orderMode) {
|
|
809
|
+
sendRouteError(res, 400, "mission-control.slices.order-mode.validation", "order_mode must be either 'manual' or 'algorithmic'");
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const rawRequest = deps.rawRequest ??
|
|
813
|
+
(typeof deps.client?.rawRequest === "function"
|
|
814
|
+
? deps.client.rawRequest.bind(deps.client)
|
|
815
|
+
: null);
|
|
816
|
+
if (!rawRequest) {
|
|
817
|
+
sendRouteError(res, 503, "mission-control.slices.order-mode.unavailable", "Canonical mission-control slices API is unavailable");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const response = await rawRequest("POST", "/api/client/mission-control/slices/order-mode", {
|
|
821
|
+
...(workspaceId
|
|
822
|
+
? {
|
|
823
|
+
workspace_id: workspaceId,
|
|
824
|
+
command_center_id: workspaceId,
|
|
825
|
+
}
|
|
826
|
+
: {}),
|
|
827
|
+
level,
|
|
828
|
+
...(initiativeId ? { initiative_id: initiativeId } : {}),
|
|
829
|
+
order_mode: orderMode,
|
|
830
|
+
});
|
|
831
|
+
deps.sendJson(res, 200, {
|
|
832
|
+
...(response && typeof response === "object" ? response : { ok: true }),
|
|
833
|
+
source: "canonical",
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
catch (err) {
|
|
837
|
+
sendRouteError(res, 503, "mission-control.slices.order-mode.canonical", "Canonical mission-control slices API unavailable for mode changes", {
|
|
838
|
+
degraded: [`canonical unavailable (${deps.safeErrorMessage(err)})`],
|
|
839
|
+
canonical_only: true,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}, "Mission-control slices order mode (canonical)");
|
|
843
|
+
router.add("POST", "mission-control/next-up/move", async ({ req, query, res }) => {
|
|
844
|
+
try {
|
|
845
|
+
const payload = await deps.parseJsonRequest(req);
|
|
846
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
847
|
+
query.get("initiativeId") ??
|
|
848
|
+
query.get("initiative_id") ??
|
|
849
|
+
"")
|
|
850
|
+
.trim();
|
|
851
|
+
const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
|
|
852
|
+
query.get("workstreamId") ??
|
|
853
|
+
query.get("workstream_id") ??
|
|
854
|
+
"")
|
|
855
|
+
.trim();
|
|
856
|
+
const placement = normalizePlacement(deps.pickString(payload, ["placement", "queuePlacement", "queue_placement"]) ??
|
|
857
|
+
query.get("placement") ??
|
|
858
|
+
query.get("queuePlacement") ??
|
|
859
|
+
query.get("queue_placement"), "bottom");
|
|
860
|
+
if (!initiativeId || !workstreamId) {
|
|
861
|
+
sendRouteError(res, 400, "mission-control.next-up.move.validation", "initiativeId and workstreamId are required");
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const queue = await deps.buildNextUpQueue({ initiativeId });
|
|
865
|
+
const order = dedupeQueueOrder(queue.items.map((item) => ({
|
|
866
|
+
initiativeId: item.initiativeId,
|
|
867
|
+
workstreamId: item.workstreamId,
|
|
868
|
+
})));
|
|
869
|
+
const key = `${initiativeId}:${workstreamId}`;
|
|
870
|
+
const current = order.filter((entry) => `${entry.initiativeId}:${entry.workstreamId}` !== key);
|
|
871
|
+
const nextOrder = placement === "top"
|
|
872
|
+
? [{ initiativeId, workstreamId }, ...current]
|
|
873
|
+
: [...current, { initiativeId, workstreamId }];
|
|
874
|
+
const next = deps.setNextUpQueuePinOrder({ order: nextOrder });
|
|
875
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
876
|
+
deps.sendJson(res, 200, {
|
|
877
|
+
ok: true,
|
|
878
|
+
placement,
|
|
879
|
+
orderApplied: nextOrder.length,
|
|
880
|
+
pins: next.pins,
|
|
881
|
+
updatedAt: next.updatedAt,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
catch (err) {
|
|
885
|
+
sendRouteException(res, "mission-control.next-up.move.handler", err);
|
|
886
|
+
}
|
|
887
|
+
}, "Mission-control next-up move");
|
|
888
|
+
router.add("POST", "mission-control/next-up/triage/stop", async ({ req, query, res }) => {
|
|
889
|
+
try {
|
|
890
|
+
const payload = await deps.parseJsonRequest(req);
|
|
891
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
892
|
+
query.get("initiativeId") ??
|
|
893
|
+
query.get("initiative_id") ??
|
|
894
|
+
"")
|
|
895
|
+
.trim();
|
|
896
|
+
const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
|
|
897
|
+
query.get("workstreamId") ??
|
|
898
|
+
query.get("workstream_id") ??
|
|
899
|
+
"")
|
|
900
|
+
.trim();
|
|
901
|
+
const placement = normalizePlacement(deps.pickString(payload, ["placement", "queuePlacement", "queue_placement"]) ??
|
|
902
|
+
query.get("placement") ??
|
|
903
|
+
query.get("queuePlacement") ??
|
|
904
|
+
query.get("queue_placement"), "bottom");
|
|
905
|
+
const resetToTodoRaw = payload.resetToTodo ??
|
|
906
|
+
payload.reset_to_todo ??
|
|
907
|
+
query.get("resetToTodo") ??
|
|
908
|
+
query.get("reset_to_todo") ??
|
|
909
|
+
null;
|
|
910
|
+
const resetToTodo = typeof resetToTodoRaw === "boolean"
|
|
911
|
+
? resetToTodoRaw
|
|
912
|
+
: deps.parseBooleanQuery(typeof resetToTodoRaw === "string" ? resetToTodoRaw : null) ?? false;
|
|
913
|
+
if (!initiativeId || !workstreamId) {
|
|
914
|
+
sendRouteError(res, 400, "mission-control.next-up.triage.stop.validation", "initiativeId and workstreamId are required");
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const run = deps.autoContinueRuns.get(initiativeId) ?? null;
|
|
918
|
+
let stoppedAutoContinue = false;
|
|
919
|
+
if (run) {
|
|
920
|
+
const now = new Date().toISOString();
|
|
921
|
+
const activeRunIds = Array.isArray(run.activeSliceRunIds)
|
|
922
|
+
? run.activeSliceRunIds.filter((id) => typeof id === "string" && id.trim().length > 0)
|
|
923
|
+
: typeof run.activeRunId === "string" && run.activeRunId.trim().length > 0
|
|
924
|
+
? [run.activeRunId]
|
|
925
|
+
: [];
|
|
926
|
+
run.stopRequested = true;
|
|
927
|
+
run.status = activeRunIds.length > 0 ? "stopping" : "stopped";
|
|
928
|
+
run.updatedAt = now;
|
|
929
|
+
if (activeRunIds.length === 0) {
|
|
930
|
+
await deps.stopAutoContinueRun({ run, reason: "stopped" });
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
try {
|
|
934
|
+
await deps.updateInitiativeAutoContinueState({ initiativeId, run });
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
// best effort
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
stoppedAutoContinue = true;
|
|
941
|
+
}
|
|
942
|
+
let resetTaskCount = 0;
|
|
943
|
+
if (resetToTodo) {
|
|
944
|
+
const taskResult = await deps.client.listEntities("task", {
|
|
945
|
+
initiative_id: initiativeId,
|
|
946
|
+
workstream_id: workstreamId,
|
|
947
|
+
limit: 100,
|
|
948
|
+
});
|
|
949
|
+
const tasks = Array.isArray(taskResult?.data) ? taskResult.data : [];
|
|
950
|
+
const statesToReset = new Set(["running", "blocked"]);
|
|
951
|
+
for (const task of tasks) {
|
|
952
|
+
if (!task || typeof task !== "object")
|
|
953
|
+
continue;
|
|
954
|
+
const record = task;
|
|
955
|
+
const taskId = deps.pickString(record, ["id"]);
|
|
956
|
+
if (!taskId)
|
|
957
|
+
continue;
|
|
958
|
+
if (!shouldResetTaskStatus(record.status, statesToReset))
|
|
959
|
+
continue;
|
|
960
|
+
await deps.client.updateEntity("task", taskId, { status: "todo" });
|
|
961
|
+
resetTaskCount += 1;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const queue = await deps.buildNextUpQueue({ initiativeId });
|
|
965
|
+
const order = dedupeQueueOrder(queue.items.map((item) => ({
|
|
966
|
+
initiativeId: item.initiativeId,
|
|
967
|
+
workstreamId: item.workstreamId,
|
|
968
|
+
})));
|
|
969
|
+
const targetKey = `${initiativeId}:${workstreamId}`;
|
|
970
|
+
const nextOrder = buildPlacedOrder({
|
|
971
|
+
order,
|
|
972
|
+
targets: new Set([targetKey]),
|
|
973
|
+
placement,
|
|
974
|
+
});
|
|
975
|
+
const next = deps.setNextUpQueuePinOrder({
|
|
976
|
+
order: nextOrder.length > 0
|
|
977
|
+
? nextOrder
|
|
978
|
+
: [{ initiativeId, workstreamId }],
|
|
979
|
+
});
|
|
980
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
981
|
+
deps.sendJson(res, 200, {
|
|
982
|
+
ok: true,
|
|
983
|
+
placement,
|
|
984
|
+
stoppedAutoContinue,
|
|
985
|
+
resetToTodo,
|
|
986
|
+
resetTaskCount,
|
|
987
|
+
run,
|
|
988
|
+
pins: next.pins,
|
|
989
|
+
updatedAt: next.updatedAt,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
catch (err) {
|
|
993
|
+
sendRouteException(res, "mission-control.next-up.triage.stop.handler", err);
|
|
994
|
+
}
|
|
995
|
+
}, "Mission-control next-up triage stop");
|
|
996
|
+
router.add("POST", "mission-control/next-up/remove", async ({ req, query, res }) => {
|
|
997
|
+
try {
|
|
998
|
+
const payload = await deps.parseJsonRequest(req);
|
|
999
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
1000
|
+
query.get("initiativeId") ??
|
|
1001
|
+
query.get("initiative_id") ??
|
|
1002
|
+
"")
|
|
1003
|
+
.trim();
|
|
1004
|
+
const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
|
|
1005
|
+
query.get("workstreamId") ??
|
|
1006
|
+
query.get("workstream_id") ??
|
|
1007
|
+
"")
|
|
1008
|
+
.trim();
|
|
1009
|
+
if (!initiativeId || !workstreamId) {
|
|
1010
|
+
sendRouteError(res, 400, "mission-control.next-up.remove.validation", "initiativeId and workstreamId are required");
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
deps.removeNextUpQueuePin({ initiativeId, workstreamId });
|
|
1014
|
+
const next = deps.suppressNextUpQueueItem({ initiativeId, workstreamId });
|
|
1015
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
1016
|
+
deps.sendJson(res, 200, {
|
|
1017
|
+
ok: true,
|
|
1018
|
+
removed: { initiativeId, workstreamId },
|
|
1019
|
+
suppressions: next.suppressions,
|
|
1020
|
+
updatedAt: next.updatedAt,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
catch (err) {
|
|
1024
|
+
sendRouteException(res, "mission-control.next-up.remove.handler", err);
|
|
1025
|
+
}
|
|
1026
|
+
}, "Mission-control next-up remove");
|
|
1027
|
+
router.add("POST", "mission-control/next-up/bulk", async ({ req, query, res }) => {
|
|
1028
|
+
try {
|
|
1029
|
+
const payload = await deps.parseJsonRequest(req);
|
|
1030
|
+
const actionRaw = deps.pickString(payload, ["action"]) ??
|
|
1031
|
+
query.get("action") ??
|
|
1032
|
+
"";
|
|
1033
|
+
const action = actionRaw.trim().toLowerCase();
|
|
1034
|
+
const initiativeScopeRaw = deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
1035
|
+
query.get("initiativeId") ??
|
|
1036
|
+
query.get("initiative_id") ??
|
|
1037
|
+
"";
|
|
1038
|
+
const initiativeScope = initiativeScopeRaw.trim() || null;
|
|
1039
|
+
const items = dedupeQueueOrder(parseQueueOrder(payload.items, deps));
|
|
1040
|
+
if (!["move_top", "move_bottom", "remove"].includes(action)) {
|
|
1041
|
+
sendRouteError(res, 400, "mission-control.next-up.bulk.validation", "action must be one of: move_top, move_bottom, remove");
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (items.length === 0) {
|
|
1045
|
+
sendRouteError(res, 400, "mission-control.next-up.bulk.validation", "items must include at least one initiativeId/workstreamId pair");
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const queue = await deps.buildNextUpQueue({ initiativeId: initiativeScope });
|
|
1049
|
+
const baseOrder = dedupeQueueOrder(queue.items.map((item) => ({
|
|
1050
|
+
initiativeId: item.initiativeId,
|
|
1051
|
+
workstreamId: item.workstreamId,
|
|
1052
|
+
})));
|
|
1053
|
+
const knownKeys = new Set(baseOrder.map((entry) => `${entry.initiativeId}:${entry.workstreamId}`));
|
|
1054
|
+
const results = items.map((entry) => {
|
|
1055
|
+
const key = `${entry.initiativeId}:${entry.workstreamId}`;
|
|
1056
|
+
if (knownKeys.has(key)) {
|
|
1057
|
+
return { ...entry, ok: true };
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
...entry,
|
|
1061
|
+
ok: false,
|
|
1062
|
+
error: "Queue item is not currently available in this scope",
|
|
1063
|
+
};
|
|
1064
|
+
});
|
|
1065
|
+
const targetKeys = new Set(results
|
|
1066
|
+
.filter((entry) => entry.ok)
|
|
1067
|
+
.map((entry) => `${entry.initiativeId}:${entry.workstreamId}`));
|
|
1068
|
+
let nextOrder = baseOrder;
|
|
1069
|
+
if (targetKeys.size > 0) {
|
|
1070
|
+
if (action === "remove") {
|
|
1071
|
+
for (const entry of results) {
|
|
1072
|
+
if (!entry.ok)
|
|
1073
|
+
continue;
|
|
1074
|
+
deps.removeNextUpQueuePin({
|
|
1075
|
+
initiativeId: entry.initiativeId,
|
|
1076
|
+
workstreamId: entry.workstreamId,
|
|
1077
|
+
});
|
|
1078
|
+
deps.suppressNextUpQueueItem({
|
|
1079
|
+
initiativeId: entry.initiativeId,
|
|
1080
|
+
workstreamId: entry.workstreamId,
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
nextOrder = baseOrder.filter((entry) => {
|
|
1084
|
+
const key = `${entry.initiativeId}:${entry.workstreamId}`;
|
|
1085
|
+
return !targetKeys.has(key);
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
nextOrder = buildPlacedOrder({
|
|
1090
|
+
order: baseOrder,
|
|
1091
|
+
targets: targetKeys,
|
|
1092
|
+
placement: action === "move_top" ? "top" : "bottom",
|
|
1093
|
+
});
|
|
1094
|
+
deps.setNextUpQueuePinOrder({ order: nextOrder });
|
|
1095
|
+
}
|
|
1096
|
+
deps.clearNextUpQueueCache(initiativeScope);
|
|
1097
|
+
}
|
|
1098
|
+
const updated = results.filter((entry) => entry.ok).length;
|
|
1099
|
+
const failed = results.length - updated;
|
|
1100
|
+
deps.sendJson(res, 200, {
|
|
1101
|
+
ok: true,
|
|
1102
|
+
action,
|
|
1103
|
+
requested: results.length,
|
|
1104
|
+
updated,
|
|
1105
|
+
failed,
|
|
1106
|
+
results: results.map((entry) => ({
|
|
1107
|
+
initiativeId: entry.initiativeId,
|
|
1108
|
+
workstreamId: entry.workstreamId,
|
|
1109
|
+
ok: entry.ok,
|
|
1110
|
+
error: entry.ok ? null : entry.error,
|
|
1111
|
+
})),
|
|
1112
|
+
orderSize: nextOrder.length,
|
|
1113
|
+
updatedAt: new Date().toISOString(),
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
catch (err) {
|
|
1117
|
+
sendRouteException(res, "mission-control.next-up.bulk.handler", err);
|
|
1118
|
+
}
|
|
1119
|
+
}, "Mission-control next-up bulk");
|
|
1120
|
+
router.add("POST", "mission-control/next-up/clear", async ({ req, query, res }) => {
|
|
1121
|
+
try {
|
|
1122
|
+
const payload = await deps.parseJsonRequest(req);
|
|
1123
|
+
const initiativeIdRaw = deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
1124
|
+
query.get("initiativeId") ??
|
|
1125
|
+
query.get("initiative_id") ??
|
|
1126
|
+
"";
|
|
1127
|
+
const initiativeId = initiativeIdRaw.trim() || null;
|
|
1128
|
+
const workstreamIdRaw = deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
|
|
1129
|
+
query.get("workstreamId") ??
|
|
1130
|
+
query.get("workstream_id") ??
|
|
1131
|
+
"";
|
|
1132
|
+
const workstreamId = workstreamIdRaw.trim() || null;
|
|
1133
|
+
const placement = normalizePlacement(deps.pickString(payload, ["placement", "queuePlacement", "queue_placement"]) ??
|
|
1134
|
+
query.get("placement") ??
|
|
1135
|
+
query.get("queuePlacement") ??
|
|
1136
|
+
query.get("queue_placement"), "bottom");
|
|
1137
|
+
const requestedStates = deps.dedupeStrings([
|
|
1138
|
+
...deps.pickStringArray(payload, ["states", "queueStates", "queue_states"]),
|
|
1139
|
+
...(query.get("states") ?? query.get("queueStates") ?? query.get("queue_states") ?? "")
|
|
1140
|
+
.split(",")
|
|
1141
|
+
.map((entry) => entry.trim())
|
|
1142
|
+
.filter(Boolean),
|
|
1143
|
+
])
|
|
1144
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
1145
|
+
.filter((entry) => entry === "running" || entry === "blocked");
|
|
1146
|
+
const states = new Set(requestedStates.length > 0 ? requestedStates : ["running", "blocked"]);
|
|
1147
|
+
const queue = await deps.buildNextUpQueue({ initiativeId });
|
|
1148
|
+
const scopedItems = queue.items.filter((item) => {
|
|
1149
|
+
if (initiativeId && item.initiativeId !== initiativeId)
|
|
1150
|
+
return false;
|
|
1151
|
+
if (workstreamId && item.workstreamId !== workstreamId)
|
|
1152
|
+
return false;
|
|
1153
|
+
if (states.has(item.queueState))
|
|
1154
|
+
return true;
|
|
1155
|
+
if (states.has("running") &&
|
|
1156
|
+
item.autoContinue?.status === "running" &&
|
|
1157
|
+
!item.autoContinue?.stopReason) {
|
|
1158
|
+
return true;
|
|
1159
|
+
}
|
|
1160
|
+
return false;
|
|
1161
|
+
});
|
|
1162
|
+
const updatedTaskIds = new Set();
|
|
1163
|
+
let failedUpdates = 0;
|
|
1164
|
+
for (const item of scopedItems) {
|
|
1165
|
+
let taskRows = [];
|
|
1166
|
+
try {
|
|
1167
|
+
const response = await deps.client.listEntities("task", {
|
|
1168
|
+
initiative_id: item.initiativeId,
|
|
1169
|
+
workstream_id: item.workstreamId,
|
|
1170
|
+
limit: 100,
|
|
1171
|
+
});
|
|
1172
|
+
taskRows = Array.isArray(response?.data) ? response.data : [];
|
|
1173
|
+
}
|
|
1174
|
+
catch {
|
|
1175
|
+
// best effort: keep progressing through queue
|
|
343
1176
|
continue;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (
|
|
347
|
-
|
|
1177
|
+
}
|
|
1178
|
+
for (const row of taskRows) {
|
|
1179
|
+
if (!row || typeof row !== "object")
|
|
1180
|
+
continue;
|
|
1181
|
+
const record = row;
|
|
1182
|
+
const taskId = deps.pickString(record, ["id"]);
|
|
1183
|
+
if (!taskId || updatedTaskIds.has(taskId))
|
|
1184
|
+
continue;
|
|
1185
|
+
if (!shouldResetTaskStatus(record.status, states))
|
|
1186
|
+
continue;
|
|
1187
|
+
try {
|
|
1188
|
+
await deps.client.updateEntity("task", taskId, { status: "todo" });
|
|
1189
|
+
updatedTaskIds.add(taskId);
|
|
1190
|
+
}
|
|
1191
|
+
catch {
|
|
1192
|
+
failedUpdates += 1;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const baseOrder = dedupeQueueOrder(queue.items.map((item) => ({
|
|
1197
|
+
initiativeId: item.initiativeId,
|
|
1198
|
+
workstreamId: item.workstreamId,
|
|
1199
|
+
})));
|
|
1200
|
+
const targetKeys = new Set(scopedItems.map((item) => `${item.initiativeId}:${item.workstreamId}`));
|
|
1201
|
+
const nextOrder = buildPlacedOrder({
|
|
1202
|
+
order: baseOrder,
|
|
1203
|
+
targets: targetKeys,
|
|
1204
|
+
placement,
|
|
1205
|
+
});
|
|
1206
|
+
const next = deps.setNextUpQueuePinOrder({ order: nextOrder });
|
|
1207
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
1208
|
+
deps.sendJson(res, 200, {
|
|
1209
|
+
ok: true,
|
|
1210
|
+
placement,
|
|
1211
|
+
states: Array.from(states),
|
|
1212
|
+
queueItemsCleared: scopedItems.length,
|
|
1213
|
+
tasksReset: updatedTaskIds.size,
|
|
1214
|
+
taskResetFailures: failedUpdates,
|
|
1215
|
+
pins: next.pins,
|
|
1216
|
+
updatedAt: next.updatedAt,
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
catch (err) {
|
|
1220
|
+
sendRouteException(res, "mission-control.next-up.clear.handler", err);
|
|
1221
|
+
}
|
|
1222
|
+
}, "Mission-control next-up clear");
|
|
1223
|
+
router.add("POST", "mission-control/graph/cycles/auto-fix", async ({ req, query, res }) => {
|
|
1224
|
+
try {
|
|
1225
|
+
const payload = await deps.parseJsonRequest(req);
|
|
1226
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
1227
|
+
query.get("initiativeId") ??
|
|
1228
|
+
query.get("initiative_id") ??
|
|
1229
|
+
"")
|
|
1230
|
+
.trim();
|
|
1231
|
+
const dryRunRaw = payload.dryRun ??
|
|
1232
|
+
payload.dry_run ??
|
|
1233
|
+
query.get("dryRun") ??
|
|
1234
|
+
query.get("dry_run") ??
|
|
1235
|
+
null;
|
|
1236
|
+
const dryRun = typeof dryRunRaw === "boolean"
|
|
1237
|
+
? dryRunRaw
|
|
1238
|
+
: deps.parseBooleanQuery(typeof dryRunRaw === "string" ? dryRunRaw : null) ?? false;
|
|
1239
|
+
if (!initiativeId) {
|
|
1240
|
+
sendRouteError(res, 400, "mission-control.graph.cycles.auto-fix.validation", "initiativeId is required");
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
const graph = deps.applyLocalInitiativeOverrideToGraph(await deps.buildMissionControlGraph(initiativeId));
|
|
1244
|
+
const diagnosticsRemovedEdges = parseCycleDiagnosticsRemovedEdges(graph);
|
|
1245
|
+
const graphNodes = parseCycleGraphNodes(graph);
|
|
1246
|
+
const nodeById = new Map(graphNodes.map((node) => [node.id, node]));
|
|
1247
|
+
const workingDependencies = new Map(graphNodes.map((node) => [node.id, new Set(node.dependencyIds)]));
|
|
1248
|
+
const removedEdgeKeys = new Set();
|
|
1249
|
+
const maxPasses = 12;
|
|
1250
|
+
for (let pass = 0; pass < maxPasses; pass += 1) {
|
|
1251
|
+
const edges = [];
|
|
1252
|
+
for (const node of graphNodes) {
|
|
1253
|
+
const depsSet = workingDependencies.get(node.id) ?? new Set();
|
|
1254
|
+
for (const depId of depsSet.values()) {
|
|
1255
|
+
if (!nodeById.has(depId) || depId === node.id)
|
|
1256
|
+
continue;
|
|
1257
|
+
edges.push({ from: depId, to: node.id });
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
const cycleEdgeKeys = detectCycleEdgeKeys(edges);
|
|
1261
|
+
if (cycleEdgeKeys.size === 0)
|
|
1262
|
+
break;
|
|
1263
|
+
let removedInPass = 0;
|
|
1264
|
+
for (const edgeKey of cycleEdgeKeys.values()) {
|
|
1265
|
+
const [from, to] = edgeKey.split("->", 2);
|
|
1266
|
+
if (!from || !to)
|
|
1267
|
+
continue;
|
|
1268
|
+
const nodeDeps = workingDependencies.get(to);
|
|
1269
|
+
if (!nodeDeps || !nodeDeps.has(from))
|
|
1270
|
+
continue;
|
|
1271
|
+
nodeDeps.delete(from);
|
|
1272
|
+
removedEdgeKeys.add(edgeKey);
|
|
1273
|
+
removedInPass += 1;
|
|
1274
|
+
}
|
|
1275
|
+
if (removedInPass === 0)
|
|
1276
|
+
break;
|
|
1277
|
+
}
|
|
1278
|
+
let removedEdges = Array.from(removedEdgeKeys.values())
|
|
1279
|
+
.map((edgeKey) => {
|
|
1280
|
+
const [from, to] = edgeKey.split("->", 2);
|
|
1281
|
+
if (!from || !to)
|
|
1282
|
+
return null;
|
|
1283
|
+
return { from, to };
|
|
1284
|
+
})
|
|
1285
|
+
.filter((entry) => Boolean(entry));
|
|
1286
|
+
if (removedEdges.length === 0 && diagnosticsRemovedEdges.length > 0) {
|
|
1287
|
+
removedEdges = diagnosticsRemovedEdges;
|
|
1288
|
+
}
|
|
1289
|
+
const affectedNodes = new Map();
|
|
1290
|
+
for (const edge of removedEdges) {
|
|
1291
|
+
const node = nodeById.get(edge.to);
|
|
1292
|
+
if (!node)
|
|
348
1293
|
continue;
|
|
1294
|
+
const existing = affectedNodes.get(node.id) ?? {
|
|
1295
|
+
id: node.id,
|
|
1296
|
+
type: node.type,
|
|
1297
|
+
title: node.title,
|
|
1298
|
+
workstreamId: node.workstreamId,
|
|
1299
|
+
removedDependencyIds: [],
|
|
1300
|
+
dependencyIds: [],
|
|
1301
|
+
};
|
|
1302
|
+
if (!existing.removedDependencyIds.includes(edge.from)) {
|
|
1303
|
+
existing.removedDependencyIds.push(edge.from);
|
|
349
1304
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
1305
|
+
existing.dependencyIds = Array.from((workingDependencies.get(node.id) ?? new Set()).values());
|
|
1306
|
+
affectedNodes.set(node.id, existing);
|
|
1307
|
+
}
|
|
1308
|
+
const affected = Array.from(affectedNodes.values()).sort((left, right) => left.title.localeCompare(right.title));
|
|
1309
|
+
if (dryRun) {
|
|
1310
|
+
deps.sendJson(res, 200, {
|
|
1311
|
+
ok: true,
|
|
1312
|
+
dryRun: true,
|
|
1313
|
+
initiativeId,
|
|
1314
|
+
cycleEdgesDetected: removedEdges.length,
|
|
1315
|
+
nodesToUpdate: affected.length,
|
|
1316
|
+
removedEdges,
|
|
1317
|
+
affected,
|
|
1318
|
+
});
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const updateResults = [];
|
|
1322
|
+
for (const node of affected) {
|
|
1323
|
+
if (node.type !== "initiative" &&
|
|
1324
|
+
node.type !== "workstream" &&
|
|
1325
|
+
node.type !== "milestone" &&
|
|
1326
|
+
node.type !== "task") {
|
|
1327
|
+
updateResults.push({
|
|
1328
|
+
id: node.id,
|
|
1329
|
+
type: node.type,
|
|
1330
|
+
ok: false,
|
|
1331
|
+
error: "Unsupported entity type for dependency update",
|
|
1332
|
+
dependencyIds: node.dependencyIds,
|
|
1333
|
+
removedDependencyIds: node.removedDependencyIds,
|
|
1334
|
+
});
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
try {
|
|
1338
|
+
await deps.client.updateEntity(node.type, node.id, {
|
|
1339
|
+
depends_on: node.dependencyIds,
|
|
1340
|
+
dependency_ids: node.dependencyIds,
|
|
1341
|
+
dependencyIds: node.dependencyIds,
|
|
1342
|
+
});
|
|
1343
|
+
updateResults.push({
|
|
1344
|
+
id: node.id,
|
|
1345
|
+
type: node.type,
|
|
1346
|
+
ok: true,
|
|
1347
|
+
dependencyIds: node.dependencyIds,
|
|
1348
|
+
removedDependencyIds: node.removedDependencyIds,
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
catch (err) {
|
|
1352
|
+
updateResults.push({
|
|
1353
|
+
id: node.id,
|
|
1354
|
+
type: node.type,
|
|
1355
|
+
ok: false,
|
|
1356
|
+
error: deps.safeErrorMessage(err),
|
|
1357
|
+
dependencyIds: node.dependencyIds,
|
|
1358
|
+
removedDependencyIds: node.removedDependencyIds,
|
|
1359
|
+
});
|
|
356
1360
|
}
|
|
357
1361
|
}
|
|
358
|
-
const
|
|
359
|
-
|
|
1362
|
+
const scheduled = [];
|
|
1363
|
+
const failedSchedules = [];
|
|
1364
|
+
const workstreamIds = Array.from(new Set(affected
|
|
1365
|
+
.map((node) => {
|
|
1366
|
+
if (node.type === "workstream")
|
|
1367
|
+
return node.id;
|
|
1368
|
+
return node.workstreamId;
|
|
1369
|
+
})
|
|
1370
|
+
.filter((workstreamId) => typeof workstreamId === "string" &&
|
|
1371
|
+
workstreamId.trim().length > 0)));
|
|
1372
|
+
for (const workstreamId of workstreamIds) {
|
|
1373
|
+
try {
|
|
1374
|
+
const scheduledFix = await deps.scheduleAutoFixForWorkstream({
|
|
1375
|
+
initiativeId,
|
|
1376
|
+
workstreamId,
|
|
1377
|
+
runId: null,
|
|
1378
|
+
event: "dependency_cycle_auto_fix",
|
|
1379
|
+
requestedByAgentId: "orgx-orchestrator",
|
|
1380
|
+
requestedByAgentName: "OrgX Orchestrator",
|
|
1381
|
+
graceMs: 250,
|
|
1382
|
+
});
|
|
1383
|
+
scheduled.push({
|
|
1384
|
+
workstreamId,
|
|
1385
|
+
requestId: scheduledFix.requestId,
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
catch (err) {
|
|
1389
|
+
failedSchedules.push({
|
|
1390
|
+
workstreamId,
|
|
1391
|
+
error: deps.safeErrorMessage(err),
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (removedEdges.length > 0 || affected.length > 0) {
|
|
1396
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
1397
|
+
}
|
|
1398
|
+
const updated = updateResults.filter((result) => result.ok).length;
|
|
1399
|
+
const failed = updateResults.length - updated;
|
|
1400
|
+
deps.sendJson(res, 200, {
|
|
1401
|
+
ok: true,
|
|
1402
|
+
initiativeId,
|
|
1403
|
+
cycleEdgesDetected: removedEdges.length,
|
|
1404
|
+
nodesUpdated: updated,
|
|
1405
|
+
nodesFailed: failed,
|
|
1406
|
+
removedEdges,
|
|
1407
|
+
updates: updateResults,
|
|
1408
|
+
scheduledAutofixes: scheduled,
|
|
1409
|
+
autofixScheduleFailures: failedSchedules,
|
|
1410
|
+
});
|
|
360
1411
|
}
|
|
361
1412
|
catch (err) {
|
|
362
|
-
|
|
1413
|
+
sendRouteException(res, "mission-control.graph.cycles.auto-fix.handler", err);
|
|
363
1414
|
}
|
|
364
|
-
}, "Mission-control
|
|
1415
|
+
}, "Mission-control dependency cycle auto-fix");
|
|
1416
|
+
router.add("POST", "mission-control/activity/auto-fix", async ({ req, query, res }) => {
|
|
1417
|
+
try {
|
|
1418
|
+
const payload = await deps.parseJsonRequest(req);
|
|
1419
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
1420
|
+
query.get("initiativeId") ??
|
|
1421
|
+
query.get("initiative_id") ??
|
|
1422
|
+
"")
|
|
1423
|
+
.trim();
|
|
1424
|
+
const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
|
|
1425
|
+
query.get("workstreamId") ??
|
|
1426
|
+
query.get("workstream_id") ??
|
|
1427
|
+
"")
|
|
1428
|
+
.trim();
|
|
1429
|
+
if (!initiativeId || !workstreamId) {
|
|
1430
|
+
sendRouteError(res, 400, "mission-control.activity.auto-fix.validation", "initiativeId and workstreamId are required");
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
const runId = (deps.pickString(payload, ["runId", "run_id", "sessionId", "session_id"]) ??
|
|
1434
|
+
query.get("runId") ??
|
|
1435
|
+
query.get("run_id") ??
|
|
1436
|
+
query.get("sessionId") ??
|
|
1437
|
+
query.get("session_id") ??
|
|
1438
|
+
"")
|
|
1439
|
+
.trim() || null;
|
|
1440
|
+
const event = (deps.pickString(payload, ["event", "eventName", "event_name"]) ??
|
|
1441
|
+
query.get("event") ??
|
|
1442
|
+
query.get("eventName") ??
|
|
1443
|
+
query.get("event_name") ??
|
|
1444
|
+
"")
|
|
1445
|
+
.trim() || null;
|
|
1446
|
+
const requestedByAgentId = (deps.pickString(payload, ["requestedByAgentId", "requested_by_agent_id"]) ??
|
|
1447
|
+
query.get("requestedByAgentId") ??
|
|
1448
|
+
query.get("requested_by_agent_id") ??
|
|
1449
|
+
"")
|
|
1450
|
+
.trim() || null;
|
|
1451
|
+
const requestedByAgentName = (deps.pickString(payload, ["requestedByAgentName", "requested_by_agent_name"]) ??
|
|
1452
|
+
query.get("requestedByAgentName") ??
|
|
1453
|
+
query.get("requested_by_agent_name") ??
|
|
1454
|
+
"")
|
|
1455
|
+
.trim() || null;
|
|
1456
|
+
const graceMsFromQueryRaw = query.get("graceMs") ??
|
|
1457
|
+
query.get("grace_ms") ??
|
|
1458
|
+
query.get("delayMs") ??
|
|
1459
|
+
query.get("delay_ms") ??
|
|
1460
|
+
null;
|
|
1461
|
+
const graceMsFromQuery = typeof graceMsFromQueryRaw === "string" && graceMsFromQueryRaw.trim().length > 0
|
|
1462
|
+
? Number(graceMsFromQueryRaw)
|
|
1463
|
+
: null;
|
|
1464
|
+
const graceMs = deps.pickNumber(payload, ["graceMs", "grace_ms", "delayMs", "delay_ms"]) ??
|
|
1465
|
+
(Number.isFinite(graceMsFromQuery) ? graceMsFromQuery : null);
|
|
1466
|
+
const schedule = await deps.scheduleAutoFixForWorkstream({
|
|
1467
|
+
initiativeId,
|
|
1468
|
+
workstreamId,
|
|
1469
|
+
runId,
|
|
1470
|
+
event,
|
|
1471
|
+
requestedByAgentId,
|
|
1472
|
+
requestedByAgentName,
|
|
1473
|
+
graceMs,
|
|
1474
|
+
});
|
|
1475
|
+
deps.sendJson(res, 202, {
|
|
1476
|
+
ok: true,
|
|
1477
|
+
scheduled: schedule,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
catch (err) {
|
|
1481
|
+
sendRouteException(res, "mission-control.activity.auto-fix.handler", err);
|
|
1482
|
+
}
|
|
1483
|
+
}, "Mission-control activity auto-fix");
|
|
365
1484
|
router.add("POST", "mission-control/auto-continue/start", async ({ req, query, res }) => {
|
|
366
1485
|
try {
|
|
367
1486
|
const payload = await deps.parseJsonRequest(req);
|
|
@@ -371,7 +1490,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
371
1490
|
"")
|
|
372
1491
|
.trim();
|
|
373
1492
|
if (!initiativeId) {
|
|
374
|
-
|
|
1493
|
+
sendRouteError(res, 400, "mission-control.auto-continue.start.validation", "initiativeId is required");
|
|
375
1494
|
return;
|
|
376
1495
|
}
|
|
377
1496
|
const agentIdRaw = (deps.pickString(payload, ["agentId", "agent_id"]) ??
|
|
@@ -381,10 +1500,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
381
1500
|
.trim();
|
|
382
1501
|
const agentId = agentIdRaw || "main";
|
|
383
1502
|
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
|
|
384
|
-
|
|
385
|
-
ok: false,
|
|
386
|
-
error: "agentId must be a simple identifier (letters, numbers, _ or -).",
|
|
387
|
-
});
|
|
1503
|
+
sendRouteError(res, 400, "mission-control.auto-continue.start.validation", "agentId must be a simple identifier (letters, numbers, _ or -).");
|
|
388
1504
|
return;
|
|
389
1505
|
}
|
|
390
1506
|
const tokenBudget = deps.pickNumber(payload, [
|
|
@@ -412,6 +1528,16 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
412
1528
|
: deps.parseBooleanQuery(typeof includeVerificationRaw === "string"
|
|
413
1529
|
? includeVerificationRaw
|
|
414
1530
|
: null);
|
|
1531
|
+
const ignoreSpawnGuardRateLimitRaw = payload.ignoreSpawnGuardRateLimit ??
|
|
1532
|
+
payload.ignore_spawn_guard_rate_limit ??
|
|
1533
|
+
query.get("ignoreSpawnGuardRateLimit") ??
|
|
1534
|
+
query.get("ignore_spawn_guard_rate_limit") ??
|
|
1535
|
+
null;
|
|
1536
|
+
const ignoreSpawnGuardRateLimit = typeof ignoreSpawnGuardRateLimitRaw === "boolean"
|
|
1537
|
+
? ignoreSpawnGuardRateLimitRaw
|
|
1538
|
+
: deps.parseBooleanQuery(typeof ignoreSpawnGuardRateLimitRaw === "string"
|
|
1539
|
+
? ignoreSpawnGuardRateLimitRaw
|
|
1540
|
+
: null);
|
|
415
1541
|
const workstreamFilter = deps.dedupeStrings([
|
|
416
1542
|
...deps.pickStringArray(payload, [
|
|
417
1543
|
"workstreamIds",
|
|
@@ -429,6 +1555,32 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
429
1555
|
.filter(Boolean),
|
|
430
1556
|
]);
|
|
431
1557
|
const allowedWorkstreamIds = workstreamFilter.length > 0 ? workstreamFilter : null;
|
|
1558
|
+
const maxParallelRaw = deps.pickNumber(payload, [
|
|
1559
|
+
"maxParallelSlices",
|
|
1560
|
+
"max_parallel_slices",
|
|
1561
|
+
"maxParallel",
|
|
1562
|
+
"max_parallel",
|
|
1563
|
+
]) ??
|
|
1564
|
+
query.get("maxParallelSlices") ??
|
|
1565
|
+
query.get("max_parallel_slices") ??
|
|
1566
|
+
query.get("maxParallel") ??
|
|
1567
|
+
query.get("max_parallel") ??
|
|
1568
|
+
null;
|
|
1569
|
+
const parallelModeRaw = (deps.pickString(payload, ["parallelMode", "parallel_mode"]) ??
|
|
1570
|
+
query.get("parallelMode") ??
|
|
1571
|
+
query.get("parallel_mode") ??
|
|
1572
|
+
"iwmt")
|
|
1573
|
+
.trim()
|
|
1574
|
+
.toLowerCase();
|
|
1575
|
+
const parallelMode = parallelModeRaw === "iwmt" ? "iwmt" : "iwmt";
|
|
1576
|
+
const startScopeRaw = deps.pickString(payload, ["scope", "sliceScope", "slice_scope"]) ??
|
|
1577
|
+
query.get("scope") ??
|
|
1578
|
+
query.get("sliceScope") ??
|
|
1579
|
+
query.get("slice_scope") ??
|
|
1580
|
+
null;
|
|
1581
|
+
const startScope = startScopeRaw === "milestone" || startScopeRaw === "workstream"
|
|
1582
|
+
? startScopeRaw
|
|
1583
|
+
: "task";
|
|
432
1584
|
const run = await deps.startAutoContinueRun({
|
|
433
1585
|
initiativeId,
|
|
434
1586
|
agentId,
|
|
@@ -436,11 +1588,22 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
436
1588
|
tokenBudget,
|
|
437
1589
|
includeVerification,
|
|
438
1590
|
allowedWorkstreamIds,
|
|
1591
|
+
maxParallelSlices: maxParallelRaw,
|
|
1592
|
+
parallelMode,
|
|
1593
|
+
ignoreSpawnGuardRateLimit: ignoreSpawnGuardRateLimit === true,
|
|
1594
|
+
scope: startScope,
|
|
439
1595
|
});
|
|
440
|
-
|
|
1596
|
+
const dispatchEnvelope = buildDispatchGatewayEnvelope({
|
|
1597
|
+
dispatchMode: "server",
|
|
1598
|
+
route: "mission-control.auto-continue.start",
|
|
1599
|
+
source: "auto_continue_start",
|
|
1600
|
+
initiativeId,
|
|
1601
|
+
workstreamIds: allowedWorkstreamIds,
|
|
1602
|
+
});
|
|
1603
|
+
deps.sendJson(res, 200, { ok: true, ...dispatchEnvelope, run });
|
|
441
1604
|
}
|
|
442
1605
|
catch (err) {
|
|
443
|
-
|
|
1606
|
+
sendRouteException(res, "mission-control.auto-continue.start.handler", err);
|
|
444
1607
|
}
|
|
445
1608
|
}, "Mission-control auto-continue start");
|
|
446
1609
|
router.add("POST", "mission-control/auto-continue/stop", async ({ req, query, res }) => {
|
|
@@ -452,19 +1615,24 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
452
1615
|
"")
|
|
453
1616
|
.trim();
|
|
454
1617
|
if (!initiativeId) {
|
|
455
|
-
|
|
1618
|
+
sendRouteError(res, 400, "mission-control.auto-continue.stop.validation", "initiativeId is required");
|
|
456
1619
|
return;
|
|
457
1620
|
}
|
|
458
1621
|
const run = deps.autoContinueRuns.get(initiativeId) ?? null;
|
|
459
1622
|
if (!run) {
|
|
460
|
-
|
|
1623
|
+
sendRouteError(res, 404, "mission-control.auto-continue.stop.lookup", "No auto-continue run found");
|
|
461
1624
|
return;
|
|
462
1625
|
}
|
|
463
1626
|
const now = new Date().toISOString();
|
|
1627
|
+
const activeRunIds = Array.isArray(run.activeSliceRunIds)
|
|
1628
|
+
? run.activeSliceRunIds.filter((id) => typeof id === "string" && id.trim().length > 0)
|
|
1629
|
+
: typeof run.activeRunId === "string" && run.activeRunId.trim().length > 0
|
|
1630
|
+
? [run.activeRunId]
|
|
1631
|
+
: [];
|
|
464
1632
|
run.stopRequested = true;
|
|
465
|
-
run.status =
|
|
1633
|
+
run.status = activeRunIds.length > 0 ? "stopping" : "stopped";
|
|
466
1634
|
run.updatedAt = now;
|
|
467
|
-
if (
|
|
1635
|
+
if (activeRunIds.length === 0) {
|
|
468
1636
|
await deps.stopAutoContinueRun({ run, reason: "stopped" });
|
|
469
1637
|
}
|
|
470
1638
|
else {
|
|
@@ -478,7 +1646,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
478
1646
|
deps.sendJson(res, 200, { ok: true, run });
|
|
479
1647
|
}
|
|
480
1648
|
catch (err) {
|
|
481
|
-
|
|
1649
|
+
sendRouteException(res, "mission-control.auto-continue.stop.handler", err);
|
|
482
1650
|
}
|
|
483
1651
|
}, "Mission-control auto-continue stop");
|
|
484
1652
|
router.add("POST", "mission-control/auto-continue/tick", async ({ req, query, res }) => {
|
|
@@ -492,7 +1660,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
492
1660
|
if (initiativeId) {
|
|
493
1661
|
const run = deps.autoContinueRuns.get(initiativeId) ?? null;
|
|
494
1662
|
if (!run) {
|
|
495
|
-
|
|
1663
|
+
sendRouteError(res, 404, "mission-control.auto-continue.tick.lookup", "No auto-continue run found");
|
|
496
1664
|
return;
|
|
497
1665
|
}
|
|
498
1666
|
await deps.tickAutoContinueRun(run);
|
|
@@ -503,7 +1671,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
503
1671
|
deps.sendJson(res, 200, { ok: true });
|
|
504
1672
|
}
|
|
505
1673
|
catch (err) {
|
|
506
|
-
|
|
1674
|
+
sendRouteException(res, "mission-control.auto-continue.tick.handler", err);
|
|
507
1675
|
}
|
|
508
1676
|
}, "Mission-control auto-continue tick");
|
|
509
1677
|
router.add("POST", "mission-control/assignments/auto", async ({ req, res }) => {
|
|
@@ -515,10 +1683,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
515
1683
|
const title = deps.pickString(payload, ["title", "name"]) ?? "Untitled";
|
|
516
1684
|
const summary = deps.pickString(payload, ["summary", "description", "context"]) ?? null;
|
|
517
1685
|
if (!entityId || !entityType) {
|
|
518
|
-
|
|
519
|
-
ok: false,
|
|
520
|
-
error: "entity_id and entity_type are required.",
|
|
521
|
-
});
|
|
1686
|
+
sendRouteError(res, 400, "mission-control.assignments.auto.validation", "entity_id and entity_type are required.");
|
|
522
1687
|
return;
|
|
523
1688
|
}
|
|
524
1689
|
const assignment = await deps.resolveAutoAssignments({
|
|
@@ -532,10 +1697,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
532
1697
|
deps.sendJson(res, 200, assignment);
|
|
533
1698
|
}
|
|
534
1699
|
catch (err) {
|
|
535
|
-
|
|
536
|
-
ok: false,
|
|
537
|
-
error: deps.safeErrorMessage(err),
|
|
538
|
-
});
|
|
1700
|
+
sendRouteException(res, "mission-control.assignments.auto.handler", err);
|
|
539
1701
|
}
|
|
540
1702
|
}, "Mission-control auto assignment");
|
|
541
1703
|
}
|