@useorgx/openclaw-plugin 0.4.9 → 0.7.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/README.md +35 -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/BXWDRGm-.js +1 -0
- package/dashboard/dist/assets/BXWDRGm-.js.br +0 -0
- package/dashboard/dist/assets/BXWDRGm-.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/C-KIc3Wc.js.br +0 -0
- package/dashboard/dist/assets/C-KIc3Wc.js.gz +0 -0
- package/dashboard/dist/assets/CE38zU4U.js +1 -0
- package/dashboard/dist/assets/CE38zU4U.js.br +0 -0
- package/dashboard/dist/assets/CE38zU4U.js.gz +0 -0
- package/dashboard/dist/assets/CFGKRAzG.js +1 -0
- package/dashboard/dist/assets/CFGKRAzG.js.br +0 -0
- package/dashboard/dist/assets/CFGKRAzG.js.gz +0 -0
- package/dashboard/dist/assets/CGGR2GZh.js +1 -0
- package/dashboard/dist/assets/CGGR2GZh.js.br +0 -0
- package/dashboard/dist/assets/CGGR2GZh.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/CPFiTmlw.js +8 -0
- package/dashboard/dist/assets/CPFiTmlw.js.br +0 -0
- package/dashboard/dist/assets/CPFiTmlw.js.gz +0 -0
- package/dashboard/dist/assets/CZZTvkQZ.js +1 -0
- package/dashboard/dist/assets/CZZTvkQZ.js.br +0 -0
- package/dashboard/dist/assets/CZZTvkQZ.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/D-bf6hEI.js +213 -0
- package/dashboard/dist/assets/D-bf6hEI.js.br +0 -0
- package/dashboard/dist/assets/D-bf6hEI.js.gz +0 -0
- package/dashboard/dist/assets/DG6y9wJI.js +2 -0
- package/dashboard/dist/assets/DG6y9wJI.js.br +0 -0
- package/dashboard/dist/assets/DG6y9wJI.js.gz +0 -0
- package/dashboard/dist/assets/DNxKz-GV.js +1 -0
- package/dashboard/dist/assets/DNxKz-GV.js.br +0 -0
- package/dashboard/dist/assets/DNxKz-GV.js.gz +0 -0
- package/dashboard/dist/assets/DW_rKUic.js +11 -0
- package/dashboard/dist/assets/DW_rKUic.js.br +0 -0
- package/dashboard/dist/assets/DW_rKUic.js.gz +0 -0
- package/dashboard/dist/assets/DbNoijHm.js +1 -0
- package/dashboard/dist/assets/DbNoijHm.js.br +0 -0
- package/dashboard/dist/assets/DbNoijHm.js.gz +0 -0
- package/dashboard/dist/assets/DjcdE6jC.js +2 -0
- package/dashboard/dist/assets/DjcdE6jC.js.br +0 -0
- package/dashboard/dist/assets/DjcdE6jC.js.gz +0 -0
- package/dashboard/dist/assets/FZYuCDnt.js +1 -0
- package/dashboard/dist/assets/FZYuCDnt.js.br +0 -0
- package/dashboard/dist/assets/FZYuCDnt.js.gz +0 -0
- package/dashboard/dist/assets/PAUiij_z.js +1 -0
- package/dashboard/dist/assets/PAUiij_z.js.br +0 -0
- package/dashboard/dist/assets/PAUiij_z.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/h5biQs2I.css +1 -0
- package/dashboard/dist/assets/h5biQs2I.css.br +0 -0
- package/dashboard/dist/assets/h5biQs2I.css.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/nByHNHoW.js +1 -0
- package/dashboard/dist/assets/nByHNHoW.js.br +0 -0
- package/dashboard/dist/assets/nByHNHoW.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/tS9mbYZi.js +1 -0
- package/dashboard/dist/assets/tS9mbYZi.js.br +0 -0
- package/dashboard/dist/assets/tS9mbYZi.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/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 +177 -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 +227 -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/fs-utils.js +13 -1
- 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 +2531 -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 +308 -20
- package/dist/http/helpers/autopilot-slice-utils.d.ts +18 -0
- package/dist/http/helpers/autopilot-slice-utils.js +516 -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 +74 -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 +860 -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 +1354 -97
- package/dist/http/routes/agent-suite.d.ts +9 -0
- package/dist/http/routes/agent-suite.js +207 -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 +294 -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 +48 -2
- package/dist/http/routes/live-snapshot.js +638 -19
- package/dist/http/routes/live-terminal.d.ts +11 -0
- package/dist/http/routes/live-terminal.js +261 -0
- package/dist/http/routes/live-triage.d.ts +61 -0
- package/dist/http/routes/live-triage.js +248 -0
- package/dist/http/routes/mission-control-actions.d.ts +49 -1
- package/dist/http/routes/mission-control-actions.js +1334 -84
- package/dist/http/routes/mission-control-read.d.ts +48 -3
- package/dist/http/routes/mission-control-read.js +1593 -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.js +33 -59
- 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 +24 -5
- package/dist/reporting/rollups.d.ts +53 -0
- package/dist/reporting/rollups.js +148 -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 +14 -4
- 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,14 +641,109 @@ 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");
|
|
663
|
+
router.add("POST", "mission-control/next-up/launch", async ({ req, query, res }) => {
|
|
664
|
+
try {
|
|
665
|
+
const payload = await deps.parseJsonRequest(req);
|
|
666
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
667
|
+
query.get("initiativeId") ??
|
|
668
|
+
query.get("initiative_id") ??
|
|
669
|
+
"")
|
|
670
|
+
.trim();
|
|
671
|
+
if (!initiativeId) {
|
|
672
|
+
sendRouteError(res, 400, "mission-control.next-up.launch.validation", "initiativeId is required");
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const requestedWorkstreamIds = deps.dedupeStrings(deps.pickStringArray(payload, ["workstreamIds", "workstream_ids"]));
|
|
676
|
+
const requestedScopeRaw = deps.pickString(payload, ["scope", "sliceScope", "slice_scope"]) ??
|
|
677
|
+
query.get("scope") ??
|
|
678
|
+
null;
|
|
679
|
+
const scope = normalizeScope(requestedScopeRaw) ?? "task";
|
|
680
|
+
const ignoreSpawnGuardRateLimitRaw = payload.ignoreSpawnGuardRateLimit ??
|
|
681
|
+
payload.ignore_spawn_guard_rate_limit ??
|
|
682
|
+
null;
|
|
683
|
+
const ignoreSpawnGuardRateLimit = typeof ignoreSpawnGuardRateLimitRaw === "boolean"
|
|
684
|
+
? ignoreSpawnGuardRateLimitRaw
|
|
685
|
+
: deps.parseBooleanQuery(typeof ignoreSpawnGuardRateLimitRaw === "string"
|
|
686
|
+
? ignoreSpawnGuardRateLimitRaw
|
|
687
|
+
: null);
|
|
688
|
+
// Build the queue to discover workstreams to dispatch
|
|
689
|
+
let queue;
|
|
690
|
+
try {
|
|
691
|
+
queue = await deps.buildNextUpQueue({ initiativeId });
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
sendRouteError(res, 503, "mission-control.next-up.launch.queue", "Unable to load queue to determine dispatchable workstreams.");
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
// Filter to requested workstreams if specified, otherwise take all
|
|
698
|
+
const candidateItems = requestedWorkstreamIds.length > 0
|
|
699
|
+
? queue.items.filter((item) => requestedWorkstreamIds.includes(item.workstreamId))
|
|
700
|
+
: queue.items.filter((item) => item.queueState === "queued" || item.queueState === "idle");
|
|
701
|
+
if (candidateItems.length === 0) {
|
|
702
|
+
deps.sendJson(res, 200, {
|
|
703
|
+
ok: true,
|
|
704
|
+
dispatched: 0,
|
|
705
|
+
initiativeId,
|
|
706
|
+
message: "No dispatchable workstreams found in the queue.",
|
|
707
|
+
});
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
// Dispatch each candidate as a one-shot (stopAfterSlice: true)
|
|
711
|
+
let dispatched = 0;
|
|
712
|
+
const errors = [];
|
|
713
|
+
for (const item of candidateItems) {
|
|
714
|
+
try {
|
|
715
|
+
const agentId = item.runnerAgentId || "main";
|
|
716
|
+
const agentName = await deps.resolveAgentDisplayName(agentId, item.runnerAgentName ?? null);
|
|
717
|
+
const run = await deps.startAutoContinueRun({
|
|
718
|
+
initiativeId,
|
|
719
|
+
agentId,
|
|
720
|
+
agentName,
|
|
721
|
+
allowedWorkstreamIds: [item.workstreamId],
|
|
722
|
+
stopAfterSlice: true,
|
|
723
|
+
ignoreSpawnGuardRateLimit: ignoreSpawnGuardRateLimit === true,
|
|
724
|
+
scope,
|
|
725
|
+
});
|
|
726
|
+
// Fire-and-forget tick to start the actual dispatch
|
|
727
|
+
void deps.tickAutoContinueRun(run).catch(() => null);
|
|
728
|
+
dispatched += 1;
|
|
729
|
+
}
|
|
730
|
+
catch (err) {
|
|
731
|
+
errors.push(`${item.workstreamId}: ${deps.safeErrorMessage(err)}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
735
|
+
deps.sendJson(res, 200, {
|
|
736
|
+
ok: true,
|
|
737
|
+
dispatched,
|
|
738
|
+
initiativeId,
|
|
739
|
+
requested: candidateItems.length,
|
|
740
|
+
...(errors.length > 0 ? { errors } : {}),
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
sendRouteException(res, "mission-control.next-up.launch.handler", err);
|
|
745
|
+
}
|
|
746
|
+
}, "Mission-control next-up launch (dispatch without auto-continue loop)");
|
|
261
747
|
router.add("POST", "mission-control/next-up/pin", async ({ req, query, res }) => {
|
|
262
748
|
try {
|
|
263
749
|
const payload = await deps.parseJsonRequest(req);
|
|
@@ -286,10 +772,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
286
772
|
]) ?? "")
|
|
287
773
|
.trim() || null;
|
|
288
774
|
if (!initiativeId || !workstreamId) {
|
|
289
|
-
|
|
290
|
-
ok: false,
|
|
291
|
-
error: "initiativeId and workstreamId are required",
|
|
292
|
-
});
|
|
775
|
+
sendRouteError(res, 400, "mission-control.next-up.pin.validation", "initiativeId and workstreamId are required");
|
|
293
776
|
return;
|
|
294
777
|
}
|
|
295
778
|
const next = deps.upsertNextUpQueuePin({
|
|
@@ -298,10 +781,11 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
298
781
|
preferredTaskId,
|
|
299
782
|
preferredMilestoneId,
|
|
300
783
|
});
|
|
784
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
301
785
|
deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
|
|
302
786
|
}
|
|
303
787
|
catch (err) {
|
|
304
|
-
|
|
788
|
+
sendRouteException(res, "mission-control.next-up.pin.handler", err);
|
|
305
789
|
}
|
|
306
790
|
}, "Mission-control next-up pin");
|
|
307
791
|
router.add("POST", "mission-control/next-up/unpin", async ({ req, query, res }) => {
|
|
@@ -318,50 +802,773 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
318
802
|
"")
|
|
319
803
|
.trim();
|
|
320
804
|
if (!initiativeId || !workstreamId) {
|
|
321
|
-
|
|
322
|
-
ok: false,
|
|
323
|
-
error: "initiativeId and workstreamId are required",
|
|
324
|
-
});
|
|
805
|
+
sendRouteError(res, 400, "mission-control.next-up.unpin.validation", "initiativeId and workstreamId are required");
|
|
325
806
|
return;
|
|
326
807
|
}
|
|
327
808
|
const next = deps.removeNextUpQueuePin({ initiativeId, workstreamId });
|
|
809
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
328
810
|
deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
|
|
329
811
|
}
|
|
330
812
|
catch (err) {
|
|
331
|
-
|
|
813
|
+
sendRouteException(res, "mission-control.next-up.unpin.handler", err);
|
|
332
814
|
}
|
|
333
815
|
}, "Mission-control next-up unpin");
|
|
334
816
|
router.add("POST", "mission-control/next-up/reorder", async ({ req, res }) => {
|
|
335
817
|
try {
|
|
336
818
|
const payload = await deps.parseJsonRequest(req);
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
819
|
+
const order = dedupeQueueOrder(parseQueueOrder(payload?.order, deps));
|
|
820
|
+
const next = deps.setNextUpQueuePinOrder({ order });
|
|
821
|
+
deps.clearNextUpQueueCache(null);
|
|
822
|
+
deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
|
|
823
|
+
}
|
|
824
|
+
catch (err) {
|
|
825
|
+
sendRouteException(res, "mission-control.next-up.reorder.handler", err);
|
|
826
|
+
}
|
|
827
|
+
}, "Mission-control next-up reorder");
|
|
828
|
+
router.add("POST", "mission-control/slices/reorder", async ({ req, query, res }) => {
|
|
829
|
+
try {
|
|
830
|
+
const payload = await deps.parseJsonRequest(req);
|
|
831
|
+
const level = normalizeSliceLevel(deps.pickString(payload, ["level"]) ?? query.get("level"));
|
|
832
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
833
|
+
query.get("initiativeId") ??
|
|
834
|
+
query.get("initiative_id") ??
|
|
835
|
+
"").trim() || null;
|
|
836
|
+
const scope = resolveWorkspaceScope(payload, query);
|
|
837
|
+
if (scope.error) {
|
|
838
|
+
sendRouteError(res, 400, "mission-control.slices.reorder.validation", scope.error);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
const workspaceId = scope.workspaceId;
|
|
842
|
+
const order = parseSliceOrderForMutation(payload?.order);
|
|
843
|
+
const canonicalOrder = order.map((sliceId) => ({ sliceId }));
|
|
844
|
+
if (canonicalOrder.length === 0) {
|
|
845
|
+
sendRouteError(res, 400, "mission-control.slices.reorder.validation", "order must contain at least one slice id");
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const rawRequest = deps.rawRequest ??
|
|
849
|
+
(typeof deps.client?.rawRequest === "function"
|
|
850
|
+
? deps.client.rawRequest.bind(deps.client)
|
|
851
|
+
: null);
|
|
852
|
+
if (!rawRequest) {
|
|
853
|
+
sendRouteError(res, 503, "mission-control.slices.reorder.unavailable", "Canonical mission-control slices API is unavailable");
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const response = await rawRequest("POST", "/api/client/mission-control/slices/reorder", {
|
|
857
|
+
...(workspaceId
|
|
858
|
+
? {
|
|
859
|
+
workspace_id: workspaceId,
|
|
860
|
+
command_center_id: workspaceId,
|
|
861
|
+
}
|
|
862
|
+
: {}),
|
|
863
|
+
level,
|
|
864
|
+
...(initiativeId ? { initiative_id: initiativeId } : {}),
|
|
865
|
+
order: canonicalOrder,
|
|
866
|
+
});
|
|
867
|
+
deps.sendJson(res, 200, {
|
|
868
|
+
...(response && typeof response === "object" ? response : { ok: true }),
|
|
869
|
+
source: "canonical",
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
catch (err) {
|
|
873
|
+
sendRouteError(res, 503, "mission-control.slices.reorder.canonical", "Canonical mission-control slices API unavailable for reorder", {
|
|
874
|
+
degraded: [`canonical unavailable (${deps.safeErrorMessage(err)})`],
|
|
875
|
+
canonical_only: true,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}, "Mission-control slices reorder (canonical)");
|
|
879
|
+
router.add("POST", "mission-control/slices/order-mode", async ({ req, query, res }) => {
|
|
880
|
+
try {
|
|
881
|
+
const payload = await deps.parseJsonRequest(req);
|
|
882
|
+
const level = normalizeSliceLevel(deps.pickString(payload, ["level"]) ?? query.get("level"));
|
|
883
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
884
|
+
query.get("initiativeId") ??
|
|
885
|
+
query.get("initiative_id") ??
|
|
886
|
+
"").trim() || null;
|
|
887
|
+
const scope = resolveWorkspaceScope(payload, query);
|
|
888
|
+
if (scope.error) {
|
|
889
|
+
sendRouteError(res, 400, "mission-control.slices.order-mode.validation", scope.error);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const workspaceId = scope.workspaceId;
|
|
893
|
+
const orderMode = normalizeSliceOrderMode(deps.pickString(payload, ["orderMode", "order_mode"]) ??
|
|
894
|
+
query.get("orderMode") ??
|
|
895
|
+
query.get("order_mode"));
|
|
896
|
+
if (!orderMode) {
|
|
897
|
+
sendRouteError(res, 400, "mission-control.slices.order-mode.validation", "order_mode must be either 'manual' or 'algorithmic'");
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const rawRequest = deps.rawRequest ??
|
|
901
|
+
(typeof deps.client?.rawRequest === "function"
|
|
902
|
+
? deps.client.rawRequest.bind(deps.client)
|
|
903
|
+
: null);
|
|
904
|
+
if (!rawRequest) {
|
|
905
|
+
sendRouteError(res, 503, "mission-control.slices.order-mode.unavailable", "Canonical mission-control slices API is unavailable");
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const response = await rawRequest("POST", "/api/client/mission-control/slices/order-mode", {
|
|
909
|
+
...(workspaceId
|
|
910
|
+
? {
|
|
911
|
+
workspace_id: workspaceId,
|
|
912
|
+
command_center_id: workspaceId,
|
|
913
|
+
}
|
|
914
|
+
: {}),
|
|
915
|
+
level,
|
|
916
|
+
...(initiativeId ? { initiative_id: initiativeId } : {}),
|
|
917
|
+
order_mode: orderMode,
|
|
918
|
+
});
|
|
919
|
+
deps.sendJson(res, 200, {
|
|
920
|
+
...(response && typeof response === "object" ? response : { ok: true }),
|
|
921
|
+
source: "canonical",
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
catch (err) {
|
|
925
|
+
sendRouteError(res, 503, "mission-control.slices.order-mode.canonical", "Canonical mission-control slices API unavailable for mode changes", {
|
|
926
|
+
degraded: [`canonical unavailable (${deps.safeErrorMessage(err)})`],
|
|
927
|
+
canonical_only: true,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}, "Mission-control slices order mode (canonical)");
|
|
931
|
+
router.add("POST", "mission-control/next-up/move", async ({ req, query, res }) => {
|
|
932
|
+
try {
|
|
933
|
+
const payload = await deps.parseJsonRequest(req);
|
|
934
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
935
|
+
query.get("initiativeId") ??
|
|
936
|
+
query.get("initiative_id") ??
|
|
937
|
+
"")
|
|
938
|
+
.trim();
|
|
939
|
+
const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
|
|
940
|
+
query.get("workstreamId") ??
|
|
941
|
+
query.get("workstream_id") ??
|
|
942
|
+
"")
|
|
943
|
+
.trim();
|
|
944
|
+
const placement = normalizePlacement(deps.pickString(payload, ["placement", "queuePlacement", "queue_placement"]) ??
|
|
945
|
+
query.get("placement") ??
|
|
946
|
+
query.get("queuePlacement") ??
|
|
947
|
+
query.get("queue_placement"), "bottom");
|
|
948
|
+
if (!initiativeId || !workstreamId) {
|
|
949
|
+
sendRouteError(res, 400, "mission-control.next-up.move.validation", "initiativeId and workstreamId are required");
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const queue = await deps.buildNextUpQueue({ initiativeId });
|
|
953
|
+
const order = dedupeQueueOrder(queue.items.map((item) => ({
|
|
954
|
+
initiativeId: item.initiativeId,
|
|
955
|
+
workstreamId: item.workstreamId,
|
|
956
|
+
})));
|
|
957
|
+
const key = `${initiativeId}:${workstreamId}`;
|
|
958
|
+
const current = order.filter((entry) => `${entry.initiativeId}:${entry.workstreamId}` !== key);
|
|
959
|
+
const nextOrder = placement === "top"
|
|
960
|
+
? [{ initiativeId, workstreamId }, ...current]
|
|
961
|
+
: [...current, { initiativeId, workstreamId }];
|
|
962
|
+
const next = deps.setNextUpQueuePinOrder({ order: nextOrder });
|
|
963
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
964
|
+
deps.sendJson(res, 200, {
|
|
965
|
+
ok: true,
|
|
966
|
+
placement,
|
|
967
|
+
orderApplied: nextOrder.length,
|
|
968
|
+
pins: next.pins,
|
|
969
|
+
updatedAt: next.updatedAt,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
catch (err) {
|
|
973
|
+
sendRouteException(res, "mission-control.next-up.move.handler", err);
|
|
974
|
+
}
|
|
975
|
+
}, "Mission-control next-up move");
|
|
976
|
+
router.add("POST", "mission-control/next-up/triage/stop", async ({ req, query, res }) => {
|
|
977
|
+
try {
|
|
978
|
+
const payload = await deps.parseJsonRequest(req);
|
|
979
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
980
|
+
query.get("initiativeId") ??
|
|
981
|
+
query.get("initiative_id") ??
|
|
982
|
+
"")
|
|
983
|
+
.trim();
|
|
984
|
+
const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
|
|
985
|
+
query.get("workstreamId") ??
|
|
986
|
+
query.get("workstream_id") ??
|
|
987
|
+
"")
|
|
988
|
+
.trim();
|
|
989
|
+
const placement = normalizePlacement(deps.pickString(payload, ["placement", "queuePlacement", "queue_placement"]) ??
|
|
990
|
+
query.get("placement") ??
|
|
991
|
+
query.get("queuePlacement") ??
|
|
992
|
+
query.get("queue_placement"), "bottom");
|
|
993
|
+
const resetToTodoRaw = payload.resetToTodo ??
|
|
994
|
+
payload.reset_to_todo ??
|
|
995
|
+
query.get("resetToTodo") ??
|
|
996
|
+
query.get("reset_to_todo") ??
|
|
997
|
+
null;
|
|
998
|
+
const resetToTodo = typeof resetToTodoRaw === "boolean"
|
|
999
|
+
? resetToTodoRaw
|
|
1000
|
+
: deps.parseBooleanQuery(typeof resetToTodoRaw === "string" ? resetToTodoRaw : null) ?? false;
|
|
1001
|
+
if (!initiativeId || !workstreamId) {
|
|
1002
|
+
sendRouteError(res, 400, "mission-control.next-up.triage.stop.validation", "initiativeId and workstreamId are required");
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const run = deps.autoContinueRuns.get(initiativeId) ?? null;
|
|
1006
|
+
let stoppedAutoContinue = false;
|
|
1007
|
+
if (run) {
|
|
1008
|
+
const now = new Date().toISOString();
|
|
1009
|
+
const activeRunIds = Array.isArray(run.activeSliceRunIds)
|
|
1010
|
+
? run.activeSliceRunIds.filter((id) => typeof id === "string" && id.trim().length > 0)
|
|
1011
|
+
: typeof run.activeRunId === "string" && run.activeRunId.trim().length > 0
|
|
1012
|
+
? [run.activeRunId]
|
|
1013
|
+
: [];
|
|
1014
|
+
run.stopRequested = true;
|
|
1015
|
+
run.status = activeRunIds.length > 0 ? "stopping" : "stopped";
|
|
1016
|
+
run.updatedAt = now;
|
|
1017
|
+
if (activeRunIds.length === 0) {
|
|
1018
|
+
await deps.stopAutoContinueRun({ run, reason: "stopped" });
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
try {
|
|
1022
|
+
await deps.updateInitiativeAutoContinueState({ initiativeId, run });
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
// best effort
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
stoppedAutoContinue = true;
|
|
1029
|
+
}
|
|
1030
|
+
let resetTaskCount = 0;
|
|
1031
|
+
if (resetToTodo) {
|
|
1032
|
+
const taskResult = await deps.client.listEntities("task", {
|
|
1033
|
+
initiative_id: initiativeId,
|
|
1034
|
+
workstream_id: workstreamId,
|
|
1035
|
+
limit: 100,
|
|
1036
|
+
});
|
|
1037
|
+
const tasks = Array.isArray(taskResult?.data) ? taskResult.data : [];
|
|
1038
|
+
const statesToReset = new Set(["running", "blocked"]);
|
|
1039
|
+
for (const task of tasks) {
|
|
1040
|
+
if (!task || typeof task !== "object")
|
|
1041
|
+
continue;
|
|
1042
|
+
const record = task;
|
|
1043
|
+
const taskId = deps.pickString(record, ["id"]);
|
|
1044
|
+
if (!taskId)
|
|
1045
|
+
continue;
|
|
1046
|
+
if (!shouldResetTaskStatus(record.status, statesToReset))
|
|
1047
|
+
continue;
|
|
1048
|
+
await deps.client.updateEntity("task", taskId, { status: "todo" });
|
|
1049
|
+
resetTaskCount += 1;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const queue = await deps.buildNextUpQueue({ initiativeId });
|
|
1053
|
+
const order = dedupeQueueOrder(queue.items.map((item) => ({
|
|
1054
|
+
initiativeId: item.initiativeId,
|
|
1055
|
+
workstreamId: item.workstreamId,
|
|
1056
|
+
})));
|
|
1057
|
+
const targetKey = `${initiativeId}:${workstreamId}`;
|
|
1058
|
+
const nextOrder = buildPlacedOrder({
|
|
1059
|
+
order,
|
|
1060
|
+
targets: new Set([targetKey]),
|
|
1061
|
+
placement,
|
|
1062
|
+
});
|
|
1063
|
+
const next = deps.setNextUpQueuePinOrder({
|
|
1064
|
+
order: nextOrder.length > 0
|
|
1065
|
+
? nextOrder
|
|
1066
|
+
: [{ initiativeId, workstreamId }],
|
|
1067
|
+
});
|
|
1068
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
1069
|
+
deps.sendJson(res, 200, {
|
|
1070
|
+
ok: true,
|
|
1071
|
+
placement,
|
|
1072
|
+
stoppedAutoContinue,
|
|
1073
|
+
resetToTodo,
|
|
1074
|
+
resetTaskCount,
|
|
1075
|
+
run,
|
|
1076
|
+
pins: next.pins,
|
|
1077
|
+
updatedAt: next.updatedAt,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
catch (err) {
|
|
1081
|
+
sendRouteException(res, "mission-control.next-up.triage.stop.handler", err);
|
|
1082
|
+
}
|
|
1083
|
+
}, "Mission-control next-up triage stop");
|
|
1084
|
+
router.add("POST", "mission-control/next-up/remove", async ({ req, query, res }) => {
|
|
1085
|
+
try {
|
|
1086
|
+
const payload = await deps.parseJsonRequest(req);
|
|
1087
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
1088
|
+
query.get("initiativeId") ??
|
|
1089
|
+
query.get("initiative_id") ??
|
|
1090
|
+
"")
|
|
1091
|
+
.trim();
|
|
1092
|
+
const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
|
|
1093
|
+
query.get("workstreamId") ??
|
|
1094
|
+
query.get("workstream_id") ??
|
|
1095
|
+
"")
|
|
1096
|
+
.trim();
|
|
1097
|
+
if (!initiativeId || !workstreamId) {
|
|
1098
|
+
sendRouteError(res, 400, "mission-control.next-up.remove.validation", "initiativeId and workstreamId are required");
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
deps.removeNextUpQueuePin({ initiativeId, workstreamId });
|
|
1102
|
+
const next = deps.suppressNextUpQueueItem({ initiativeId, workstreamId });
|
|
1103
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
1104
|
+
deps.sendJson(res, 200, {
|
|
1105
|
+
ok: true,
|
|
1106
|
+
removed: { initiativeId, workstreamId },
|
|
1107
|
+
suppressions: next.suppressions,
|
|
1108
|
+
updatedAt: next.updatedAt,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
catch (err) {
|
|
1112
|
+
sendRouteException(res, "mission-control.next-up.remove.handler", err);
|
|
1113
|
+
}
|
|
1114
|
+
}, "Mission-control next-up remove");
|
|
1115
|
+
router.add("POST", "mission-control/next-up/bulk", async ({ req, query, res }) => {
|
|
1116
|
+
try {
|
|
1117
|
+
const payload = await deps.parseJsonRequest(req);
|
|
1118
|
+
const actionRaw = deps.pickString(payload, ["action"]) ??
|
|
1119
|
+
query.get("action") ??
|
|
1120
|
+
"";
|
|
1121
|
+
const action = actionRaw.trim().toLowerCase();
|
|
1122
|
+
const initiativeScopeRaw = deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
1123
|
+
query.get("initiativeId") ??
|
|
1124
|
+
query.get("initiative_id") ??
|
|
1125
|
+
"";
|
|
1126
|
+
const initiativeScope = initiativeScopeRaw.trim() || null;
|
|
1127
|
+
const items = dedupeQueueOrder(parseQueueOrder(payload.items, deps));
|
|
1128
|
+
if (!["move_top", "move_bottom", "remove"].includes(action)) {
|
|
1129
|
+
sendRouteError(res, 400, "mission-control.next-up.bulk.validation", "action must be one of: move_top, move_bottom, remove");
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (items.length === 0) {
|
|
1133
|
+
sendRouteError(res, 400, "mission-control.next-up.bulk.validation", "items must include at least one initiativeId/workstreamId pair");
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
const queue = await deps.buildNextUpQueue({ initiativeId: initiativeScope });
|
|
1137
|
+
const baseOrder = dedupeQueueOrder(queue.items.map((item) => ({
|
|
1138
|
+
initiativeId: item.initiativeId,
|
|
1139
|
+
workstreamId: item.workstreamId,
|
|
1140
|
+
})));
|
|
1141
|
+
const knownKeys = new Set(baseOrder.map((entry) => `${entry.initiativeId}:${entry.workstreamId}`));
|
|
1142
|
+
const results = items.map((entry) => {
|
|
1143
|
+
const key = `${entry.initiativeId}:${entry.workstreamId}`;
|
|
1144
|
+
if (knownKeys.has(key)) {
|
|
1145
|
+
return { ...entry, ok: true };
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
...entry,
|
|
1149
|
+
ok: false,
|
|
1150
|
+
error: "Queue item is not currently available in this scope",
|
|
1151
|
+
};
|
|
1152
|
+
});
|
|
1153
|
+
const targetKeys = new Set(results
|
|
1154
|
+
.filter((entry) => entry.ok)
|
|
1155
|
+
.map((entry) => `${entry.initiativeId}:${entry.workstreamId}`));
|
|
1156
|
+
let nextOrder = baseOrder;
|
|
1157
|
+
if (targetKeys.size > 0) {
|
|
1158
|
+
if (action === "remove") {
|
|
1159
|
+
for (const entry of results) {
|
|
1160
|
+
if (!entry.ok)
|
|
1161
|
+
continue;
|
|
1162
|
+
deps.removeNextUpQueuePin({
|
|
1163
|
+
initiativeId: entry.initiativeId,
|
|
1164
|
+
workstreamId: entry.workstreamId,
|
|
1165
|
+
});
|
|
1166
|
+
deps.suppressNextUpQueueItem({
|
|
1167
|
+
initiativeId: entry.initiativeId,
|
|
1168
|
+
workstreamId: entry.workstreamId,
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
nextOrder = baseOrder.filter((entry) => {
|
|
1172
|
+
const key = `${entry.initiativeId}:${entry.workstreamId}`;
|
|
1173
|
+
return !targetKeys.has(key);
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
else {
|
|
1177
|
+
nextOrder = buildPlacedOrder({
|
|
1178
|
+
order: baseOrder,
|
|
1179
|
+
targets: targetKeys,
|
|
1180
|
+
placement: action === "move_top" ? "top" : "bottom",
|
|
1181
|
+
});
|
|
1182
|
+
deps.setNextUpQueuePinOrder({ order: nextOrder });
|
|
1183
|
+
}
|
|
1184
|
+
deps.clearNextUpQueueCache(initiativeScope);
|
|
1185
|
+
}
|
|
1186
|
+
const updated = results.filter((entry) => entry.ok).length;
|
|
1187
|
+
const failed = results.length - updated;
|
|
1188
|
+
deps.sendJson(res, 200, {
|
|
1189
|
+
ok: true,
|
|
1190
|
+
action,
|
|
1191
|
+
requested: results.length,
|
|
1192
|
+
updated,
|
|
1193
|
+
failed,
|
|
1194
|
+
results: results.map((entry) => ({
|
|
1195
|
+
initiativeId: entry.initiativeId,
|
|
1196
|
+
workstreamId: entry.workstreamId,
|
|
1197
|
+
ok: entry.ok,
|
|
1198
|
+
error: entry.ok ? null : entry.error,
|
|
1199
|
+
})),
|
|
1200
|
+
orderSize: nextOrder.length,
|
|
1201
|
+
updatedAt: new Date().toISOString(),
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
catch (err) {
|
|
1205
|
+
sendRouteException(res, "mission-control.next-up.bulk.handler", err);
|
|
1206
|
+
}
|
|
1207
|
+
}, "Mission-control next-up bulk");
|
|
1208
|
+
router.add("POST", "mission-control/next-up/clear", async ({ req, query, res }) => {
|
|
1209
|
+
try {
|
|
1210
|
+
const payload = await deps.parseJsonRequest(req);
|
|
1211
|
+
const initiativeIdRaw = deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
1212
|
+
query.get("initiativeId") ??
|
|
1213
|
+
query.get("initiative_id") ??
|
|
1214
|
+
"";
|
|
1215
|
+
const initiativeId = initiativeIdRaw.trim() || null;
|
|
1216
|
+
const workstreamIdRaw = deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
|
|
1217
|
+
query.get("workstreamId") ??
|
|
1218
|
+
query.get("workstream_id") ??
|
|
1219
|
+
"";
|
|
1220
|
+
const workstreamId = workstreamIdRaw.trim() || null;
|
|
1221
|
+
const placement = normalizePlacement(deps.pickString(payload, ["placement", "queuePlacement", "queue_placement"]) ??
|
|
1222
|
+
query.get("placement") ??
|
|
1223
|
+
query.get("queuePlacement") ??
|
|
1224
|
+
query.get("queue_placement"), "bottom");
|
|
1225
|
+
const requestedStates = deps.dedupeStrings([
|
|
1226
|
+
...deps.pickStringArray(payload, ["states", "queueStates", "queue_states"]),
|
|
1227
|
+
...(query.get("states") ?? query.get("queueStates") ?? query.get("queue_states") ?? "")
|
|
1228
|
+
.split(",")
|
|
1229
|
+
.map((entry) => entry.trim())
|
|
1230
|
+
.filter(Boolean),
|
|
1231
|
+
])
|
|
1232
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
1233
|
+
.filter((entry) => entry === "running" || entry === "blocked");
|
|
1234
|
+
const states = new Set(requestedStates.length > 0 ? requestedStates : ["running", "blocked"]);
|
|
1235
|
+
const queue = await deps.buildNextUpQueue({ initiativeId });
|
|
1236
|
+
const scopedItems = queue.items.filter((item) => {
|
|
1237
|
+
if (initiativeId && item.initiativeId !== initiativeId)
|
|
1238
|
+
return false;
|
|
1239
|
+
if (workstreamId && item.workstreamId !== workstreamId)
|
|
1240
|
+
return false;
|
|
1241
|
+
if (states.has(item.queueState))
|
|
1242
|
+
return true;
|
|
1243
|
+
if (states.has("running") &&
|
|
1244
|
+
item.autoContinue?.status === "running" &&
|
|
1245
|
+
!item.autoContinue?.stopReason) {
|
|
1246
|
+
return true;
|
|
1247
|
+
}
|
|
1248
|
+
return false;
|
|
1249
|
+
});
|
|
1250
|
+
const updatedTaskIds = new Set();
|
|
1251
|
+
let failedUpdates = 0;
|
|
1252
|
+
for (const item of scopedItems) {
|
|
1253
|
+
let taskRows = [];
|
|
1254
|
+
try {
|
|
1255
|
+
const response = await deps.client.listEntities("task", {
|
|
1256
|
+
initiative_id: item.initiativeId,
|
|
1257
|
+
workstream_id: item.workstreamId,
|
|
1258
|
+
limit: 100,
|
|
1259
|
+
});
|
|
1260
|
+
taskRows = Array.isArray(response?.data) ? response.data : [];
|
|
1261
|
+
}
|
|
1262
|
+
catch {
|
|
1263
|
+
// best effort: keep progressing through queue
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
for (const row of taskRows) {
|
|
1267
|
+
if (!row || typeof row !== "object")
|
|
1268
|
+
continue;
|
|
1269
|
+
const record = row;
|
|
1270
|
+
const taskId = deps.pickString(record, ["id"]);
|
|
1271
|
+
if (!taskId || updatedTaskIds.has(taskId))
|
|
1272
|
+
continue;
|
|
1273
|
+
if (!shouldResetTaskStatus(record.status, states))
|
|
1274
|
+
continue;
|
|
1275
|
+
try {
|
|
1276
|
+
await deps.client.updateEntity("task", taskId, { status: "todo" });
|
|
1277
|
+
updatedTaskIds.add(taskId);
|
|
1278
|
+
}
|
|
1279
|
+
catch {
|
|
1280
|
+
failedUpdates += 1;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
const baseOrder = dedupeQueueOrder(queue.items.map((item) => ({
|
|
1285
|
+
initiativeId: item.initiativeId,
|
|
1286
|
+
workstreamId: item.workstreamId,
|
|
1287
|
+
})));
|
|
1288
|
+
const targetKeys = new Set(scopedItems.map((item) => `${item.initiativeId}:${item.workstreamId}`));
|
|
1289
|
+
const nextOrder = buildPlacedOrder({
|
|
1290
|
+
order: baseOrder,
|
|
1291
|
+
targets: targetKeys,
|
|
1292
|
+
placement,
|
|
1293
|
+
});
|
|
1294
|
+
const next = deps.setNextUpQueuePinOrder({ order: nextOrder });
|
|
1295
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
1296
|
+
deps.sendJson(res, 200, {
|
|
1297
|
+
ok: true,
|
|
1298
|
+
placement,
|
|
1299
|
+
states: Array.from(states),
|
|
1300
|
+
queueItemsCleared: scopedItems.length,
|
|
1301
|
+
tasksReset: updatedTaskIds.size,
|
|
1302
|
+
taskResetFailures: failedUpdates,
|
|
1303
|
+
pins: next.pins,
|
|
1304
|
+
updatedAt: next.updatedAt,
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
catch (err) {
|
|
1308
|
+
sendRouteException(res, "mission-control.next-up.clear.handler", err);
|
|
1309
|
+
}
|
|
1310
|
+
}, "Mission-control next-up clear");
|
|
1311
|
+
router.add("POST", "mission-control/graph/cycles/auto-fix", async ({ req, query, res }) => {
|
|
1312
|
+
try {
|
|
1313
|
+
const payload = await deps.parseJsonRequest(req);
|
|
1314
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
1315
|
+
query.get("initiativeId") ??
|
|
1316
|
+
query.get("initiative_id") ??
|
|
1317
|
+
"")
|
|
1318
|
+
.trim();
|
|
1319
|
+
const dryRunRaw = payload.dryRun ??
|
|
1320
|
+
payload.dry_run ??
|
|
1321
|
+
query.get("dryRun") ??
|
|
1322
|
+
query.get("dry_run") ??
|
|
1323
|
+
null;
|
|
1324
|
+
const dryRun = typeof dryRunRaw === "boolean"
|
|
1325
|
+
? dryRunRaw
|
|
1326
|
+
: deps.parseBooleanQuery(typeof dryRunRaw === "string" ? dryRunRaw : null) ?? false;
|
|
1327
|
+
if (!initiativeId) {
|
|
1328
|
+
sendRouteError(res, 400, "mission-control.graph.cycles.auto-fix.validation", "initiativeId is required");
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
const graph = deps.applyLocalInitiativeOverrideToGraph(await deps.buildMissionControlGraph(initiativeId));
|
|
1332
|
+
const diagnosticsRemovedEdges = parseCycleDiagnosticsRemovedEdges(graph);
|
|
1333
|
+
const graphNodes = parseCycleGraphNodes(graph);
|
|
1334
|
+
const nodeById = new Map(graphNodes.map((node) => [node.id, node]));
|
|
1335
|
+
const workingDependencies = new Map(graphNodes.map((node) => [node.id, new Set(node.dependencyIds)]));
|
|
1336
|
+
const removedEdgeKeys = new Set();
|
|
1337
|
+
const maxPasses = 12;
|
|
1338
|
+
for (let pass = 0; pass < maxPasses; pass += 1) {
|
|
1339
|
+
const edges = [];
|
|
1340
|
+
for (const node of graphNodes) {
|
|
1341
|
+
const depsSet = workingDependencies.get(node.id) ?? new Set();
|
|
1342
|
+
for (const depId of depsSet.values()) {
|
|
1343
|
+
if (!nodeById.has(depId) || depId === node.id)
|
|
1344
|
+
continue;
|
|
1345
|
+
edges.push({ from: depId, to: node.id });
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const cycleEdgeKeys = detectCycleEdgeKeys(edges);
|
|
1349
|
+
if (cycleEdgeKeys.size === 0)
|
|
1350
|
+
break;
|
|
1351
|
+
let removedInPass = 0;
|
|
1352
|
+
for (const edgeKey of cycleEdgeKeys.values()) {
|
|
1353
|
+
const [from, to] = edgeKey.split("->", 2);
|
|
1354
|
+
if (!from || !to)
|
|
1355
|
+
continue;
|
|
1356
|
+
const nodeDeps = workingDependencies.get(to);
|
|
1357
|
+
if (!nodeDeps || !nodeDeps.has(from))
|
|
1358
|
+
continue;
|
|
1359
|
+
nodeDeps.delete(from);
|
|
1360
|
+
removedEdgeKeys.add(edgeKey);
|
|
1361
|
+
removedInPass += 1;
|
|
1362
|
+
}
|
|
1363
|
+
if (removedInPass === 0)
|
|
1364
|
+
break;
|
|
1365
|
+
}
|
|
1366
|
+
let removedEdges = Array.from(removedEdgeKeys.values())
|
|
1367
|
+
.map((edgeKey) => {
|
|
1368
|
+
const [from, to] = edgeKey.split("->", 2);
|
|
1369
|
+
if (!from || !to)
|
|
1370
|
+
return null;
|
|
1371
|
+
return { from, to };
|
|
1372
|
+
})
|
|
1373
|
+
.filter((entry) => Boolean(entry));
|
|
1374
|
+
if (removedEdges.length === 0 && diagnosticsRemovedEdges.length > 0) {
|
|
1375
|
+
removedEdges = diagnosticsRemovedEdges;
|
|
1376
|
+
}
|
|
1377
|
+
const affectedNodes = new Map();
|
|
1378
|
+
for (const edge of removedEdges) {
|
|
1379
|
+
const node = nodeById.get(edge.to);
|
|
1380
|
+
if (!node)
|
|
343
1381
|
continue;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
1382
|
+
const existing = affectedNodes.get(node.id) ?? {
|
|
1383
|
+
id: node.id,
|
|
1384
|
+
type: node.type,
|
|
1385
|
+
title: node.title,
|
|
1386
|
+
workstreamId: node.workstreamId,
|
|
1387
|
+
removedDependencyIds: [],
|
|
1388
|
+
dependencyIds: [],
|
|
1389
|
+
};
|
|
1390
|
+
if (!existing.removedDependencyIds.includes(edge.from)) {
|
|
1391
|
+
existing.removedDependencyIds.push(edge.from);
|
|
1392
|
+
}
|
|
1393
|
+
existing.dependencyIds = Array.from((workingDependencies.get(node.id) ?? new Set()).values());
|
|
1394
|
+
affectedNodes.set(node.id, existing);
|
|
1395
|
+
}
|
|
1396
|
+
const affected = Array.from(affectedNodes.values()).sort((left, right) => left.title.localeCompare(right.title));
|
|
1397
|
+
if (dryRun) {
|
|
1398
|
+
deps.sendJson(res, 200, {
|
|
1399
|
+
ok: true,
|
|
1400
|
+
dryRun: true,
|
|
1401
|
+
initiativeId,
|
|
1402
|
+
cycleEdgesDetected: removedEdges.length,
|
|
1403
|
+
nodesToUpdate: affected.length,
|
|
1404
|
+
removedEdges,
|
|
1405
|
+
affected,
|
|
1406
|
+
});
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const updateResults = [];
|
|
1410
|
+
for (const node of affected) {
|
|
1411
|
+
if (node.type !== "initiative" &&
|
|
1412
|
+
node.type !== "workstream" &&
|
|
1413
|
+
node.type !== "milestone" &&
|
|
1414
|
+
node.type !== "task") {
|
|
1415
|
+
updateResults.push({
|
|
1416
|
+
id: node.id,
|
|
1417
|
+
type: node.type,
|
|
1418
|
+
ok: false,
|
|
1419
|
+
error: "Unsupported entity type for dependency update",
|
|
1420
|
+
dependencyIds: node.dependencyIds,
|
|
1421
|
+
removedDependencyIds: node.removedDependencyIds,
|
|
1422
|
+
});
|
|
348
1423
|
continue;
|
|
349
1424
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
1425
|
+
try {
|
|
1426
|
+
await deps.client.updateEntity(node.type, node.id, {
|
|
1427
|
+
depends_on: node.dependencyIds,
|
|
1428
|
+
dependency_ids: node.dependencyIds,
|
|
1429
|
+
dependencyIds: node.dependencyIds,
|
|
1430
|
+
});
|
|
1431
|
+
updateResults.push({
|
|
1432
|
+
id: node.id,
|
|
1433
|
+
type: node.type,
|
|
1434
|
+
ok: true,
|
|
1435
|
+
dependencyIds: node.dependencyIds,
|
|
1436
|
+
removedDependencyIds: node.removedDependencyIds,
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
catch (err) {
|
|
1440
|
+
updateResults.push({
|
|
1441
|
+
id: node.id,
|
|
1442
|
+
type: node.type,
|
|
1443
|
+
ok: false,
|
|
1444
|
+
error: deps.safeErrorMessage(err),
|
|
1445
|
+
dependencyIds: node.dependencyIds,
|
|
1446
|
+
removedDependencyIds: node.removedDependencyIds,
|
|
1447
|
+
});
|
|
356
1448
|
}
|
|
357
1449
|
}
|
|
358
|
-
const
|
|
359
|
-
|
|
1450
|
+
const scheduled = [];
|
|
1451
|
+
const failedSchedules = [];
|
|
1452
|
+
const workstreamIds = Array.from(new Set(affected
|
|
1453
|
+
.map((node) => {
|
|
1454
|
+
if (node.type === "workstream")
|
|
1455
|
+
return node.id;
|
|
1456
|
+
return node.workstreamId;
|
|
1457
|
+
})
|
|
1458
|
+
.filter((workstreamId) => typeof workstreamId === "string" &&
|
|
1459
|
+
workstreamId.trim().length > 0)));
|
|
1460
|
+
for (const workstreamId of workstreamIds) {
|
|
1461
|
+
try {
|
|
1462
|
+
const scheduledFix = await deps.scheduleAutoFixForWorkstream({
|
|
1463
|
+
initiativeId,
|
|
1464
|
+
workstreamId,
|
|
1465
|
+
runId: null,
|
|
1466
|
+
event: "dependency_cycle_auto_fix",
|
|
1467
|
+
requestedByAgentId: "orgx-orchestrator",
|
|
1468
|
+
requestedByAgentName: "OrgX Orchestrator",
|
|
1469
|
+
graceMs: 250,
|
|
1470
|
+
});
|
|
1471
|
+
scheduled.push({
|
|
1472
|
+
workstreamId,
|
|
1473
|
+
requestId: scheduledFix.requestId,
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
catch (err) {
|
|
1477
|
+
failedSchedules.push({
|
|
1478
|
+
workstreamId,
|
|
1479
|
+
error: deps.safeErrorMessage(err),
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (removedEdges.length > 0 || affected.length > 0) {
|
|
1484
|
+
deps.clearNextUpQueueCache(initiativeId);
|
|
1485
|
+
}
|
|
1486
|
+
const updated = updateResults.filter((result) => result.ok).length;
|
|
1487
|
+
const failed = updateResults.length - updated;
|
|
1488
|
+
deps.sendJson(res, 200, {
|
|
1489
|
+
ok: true,
|
|
1490
|
+
initiativeId,
|
|
1491
|
+
cycleEdgesDetected: removedEdges.length,
|
|
1492
|
+
nodesUpdated: updated,
|
|
1493
|
+
nodesFailed: failed,
|
|
1494
|
+
removedEdges,
|
|
1495
|
+
updates: updateResults,
|
|
1496
|
+
scheduledAutofixes: scheduled,
|
|
1497
|
+
autofixScheduleFailures: failedSchedules,
|
|
1498
|
+
});
|
|
360
1499
|
}
|
|
361
1500
|
catch (err) {
|
|
362
|
-
|
|
1501
|
+
sendRouteException(res, "mission-control.graph.cycles.auto-fix.handler", err);
|
|
363
1502
|
}
|
|
364
|
-
}, "Mission-control
|
|
1503
|
+
}, "Mission-control dependency cycle auto-fix");
|
|
1504
|
+
router.add("POST", "mission-control/activity/auto-fix", async ({ req, query, res }) => {
|
|
1505
|
+
try {
|
|
1506
|
+
const payload = await deps.parseJsonRequest(req);
|
|
1507
|
+
const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
|
|
1508
|
+
query.get("initiativeId") ??
|
|
1509
|
+
query.get("initiative_id") ??
|
|
1510
|
+
"")
|
|
1511
|
+
.trim();
|
|
1512
|
+
const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
|
|
1513
|
+
query.get("workstreamId") ??
|
|
1514
|
+
query.get("workstream_id") ??
|
|
1515
|
+
"")
|
|
1516
|
+
.trim();
|
|
1517
|
+
if (!initiativeId || !workstreamId) {
|
|
1518
|
+
sendRouteError(res, 400, "mission-control.activity.auto-fix.validation", "initiativeId and workstreamId are required");
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
const runId = (deps.pickString(payload, ["runId", "run_id", "sessionId", "session_id"]) ??
|
|
1522
|
+
query.get("runId") ??
|
|
1523
|
+
query.get("run_id") ??
|
|
1524
|
+
query.get("sessionId") ??
|
|
1525
|
+
query.get("session_id") ??
|
|
1526
|
+
"")
|
|
1527
|
+
.trim() || null;
|
|
1528
|
+
const event = (deps.pickString(payload, ["event", "eventName", "event_name"]) ??
|
|
1529
|
+
query.get("event") ??
|
|
1530
|
+
query.get("eventName") ??
|
|
1531
|
+
query.get("event_name") ??
|
|
1532
|
+
"")
|
|
1533
|
+
.trim() || null;
|
|
1534
|
+
const requestedByAgentId = (deps.pickString(payload, ["requestedByAgentId", "requested_by_agent_id"]) ??
|
|
1535
|
+
query.get("requestedByAgentId") ??
|
|
1536
|
+
query.get("requested_by_agent_id") ??
|
|
1537
|
+
"")
|
|
1538
|
+
.trim() || null;
|
|
1539
|
+
const requestedByAgentName = (deps.pickString(payload, ["requestedByAgentName", "requested_by_agent_name"]) ??
|
|
1540
|
+
query.get("requestedByAgentName") ??
|
|
1541
|
+
query.get("requested_by_agent_name") ??
|
|
1542
|
+
"")
|
|
1543
|
+
.trim() || null;
|
|
1544
|
+
const graceMsFromQueryRaw = query.get("graceMs") ??
|
|
1545
|
+
query.get("grace_ms") ??
|
|
1546
|
+
query.get("delayMs") ??
|
|
1547
|
+
query.get("delay_ms") ??
|
|
1548
|
+
null;
|
|
1549
|
+
const graceMsFromQuery = typeof graceMsFromQueryRaw === "string" && graceMsFromQueryRaw.trim().length > 0
|
|
1550
|
+
? Number(graceMsFromQueryRaw)
|
|
1551
|
+
: null;
|
|
1552
|
+
const graceMs = deps.pickNumber(payload, ["graceMs", "grace_ms", "delayMs", "delay_ms"]) ??
|
|
1553
|
+
(Number.isFinite(graceMsFromQuery) ? graceMsFromQuery : null);
|
|
1554
|
+
const schedule = await deps.scheduleAutoFixForWorkstream({
|
|
1555
|
+
initiativeId,
|
|
1556
|
+
workstreamId,
|
|
1557
|
+
runId,
|
|
1558
|
+
event,
|
|
1559
|
+
requestedByAgentId,
|
|
1560
|
+
requestedByAgentName,
|
|
1561
|
+
graceMs,
|
|
1562
|
+
});
|
|
1563
|
+
deps.sendJson(res, 202, {
|
|
1564
|
+
ok: true,
|
|
1565
|
+
scheduled: schedule,
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
catch (err) {
|
|
1569
|
+
sendRouteException(res, "mission-control.activity.auto-fix.handler", err);
|
|
1570
|
+
}
|
|
1571
|
+
}, "Mission-control activity auto-fix");
|
|
365
1572
|
router.add("POST", "mission-control/auto-continue/start", async ({ req, query, res }) => {
|
|
366
1573
|
try {
|
|
367
1574
|
const payload = await deps.parseJsonRequest(req);
|
|
@@ -371,7 +1578,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
371
1578
|
"")
|
|
372
1579
|
.trim();
|
|
373
1580
|
if (!initiativeId) {
|
|
374
|
-
|
|
1581
|
+
sendRouteError(res, 400, "mission-control.auto-continue.start.validation", "initiativeId is required");
|
|
375
1582
|
return;
|
|
376
1583
|
}
|
|
377
1584
|
const agentIdRaw = (deps.pickString(payload, ["agentId", "agent_id"]) ??
|
|
@@ -381,10 +1588,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
381
1588
|
.trim();
|
|
382
1589
|
const agentId = agentIdRaw || "main";
|
|
383
1590
|
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
|
|
384
|
-
|
|
385
|
-
ok: false,
|
|
386
|
-
error: "agentId must be a simple identifier (letters, numbers, _ or -).",
|
|
387
|
-
});
|
|
1591
|
+
sendRouteError(res, 400, "mission-control.auto-continue.start.validation", "agentId must be a simple identifier (letters, numbers, _ or -).");
|
|
388
1592
|
return;
|
|
389
1593
|
}
|
|
390
1594
|
const tokenBudget = deps.pickNumber(payload, [
|
|
@@ -412,6 +1616,16 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
412
1616
|
: deps.parseBooleanQuery(typeof includeVerificationRaw === "string"
|
|
413
1617
|
? includeVerificationRaw
|
|
414
1618
|
: null);
|
|
1619
|
+
const ignoreSpawnGuardRateLimitRaw = payload.ignoreSpawnGuardRateLimit ??
|
|
1620
|
+
payload.ignore_spawn_guard_rate_limit ??
|
|
1621
|
+
query.get("ignoreSpawnGuardRateLimit") ??
|
|
1622
|
+
query.get("ignore_spawn_guard_rate_limit") ??
|
|
1623
|
+
null;
|
|
1624
|
+
const ignoreSpawnGuardRateLimit = typeof ignoreSpawnGuardRateLimitRaw === "boolean"
|
|
1625
|
+
? ignoreSpawnGuardRateLimitRaw
|
|
1626
|
+
: deps.parseBooleanQuery(typeof ignoreSpawnGuardRateLimitRaw === "string"
|
|
1627
|
+
? ignoreSpawnGuardRateLimitRaw
|
|
1628
|
+
: null);
|
|
415
1629
|
const workstreamFilter = deps.dedupeStrings([
|
|
416
1630
|
...deps.pickStringArray(payload, [
|
|
417
1631
|
"workstreamIds",
|
|
@@ -429,6 +1643,32 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
429
1643
|
.filter(Boolean),
|
|
430
1644
|
]);
|
|
431
1645
|
const allowedWorkstreamIds = workstreamFilter.length > 0 ? workstreamFilter : null;
|
|
1646
|
+
const maxParallelRaw = deps.pickNumber(payload, [
|
|
1647
|
+
"maxParallelSlices",
|
|
1648
|
+
"max_parallel_slices",
|
|
1649
|
+
"maxParallel",
|
|
1650
|
+
"max_parallel",
|
|
1651
|
+
]) ??
|
|
1652
|
+
query.get("maxParallelSlices") ??
|
|
1653
|
+
query.get("max_parallel_slices") ??
|
|
1654
|
+
query.get("maxParallel") ??
|
|
1655
|
+
query.get("max_parallel") ??
|
|
1656
|
+
null;
|
|
1657
|
+
const parallelModeRaw = (deps.pickString(payload, ["parallelMode", "parallel_mode"]) ??
|
|
1658
|
+
query.get("parallelMode") ??
|
|
1659
|
+
query.get("parallel_mode") ??
|
|
1660
|
+
"iwmt")
|
|
1661
|
+
.trim()
|
|
1662
|
+
.toLowerCase();
|
|
1663
|
+
const parallelMode = parallelModeRaw === "iwmt" ? "iwmt" : "iwmt";
|
|
1664
|
+
const startScopeRaw = deps.pickString(payload, ["scope", "sliceScope", "slice_scope"]) ??
|
|
1665
|
+
query.get("scope") ??
|
|
1666
|
+
query.get("sliceScope") ??
|
|
1667
|
+
query.get("slice_scope") ??
|
|
1668
|
+
null;
|
|
1669
|
+
const startScope = startScopeRaw === "milestone" || startScopeRaw === "workstream"
|
|
1670
|
+
? startScopeRaw
|
|
1671
|
+
: "task";
|
|
432
1672
|
const run = await deps.startAutoContinueRun({
|
|
433
1673
|
initiativeId,
|
|
434
1674
|
agentId,
|
|
@@ -436,11 +1676,22 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
436
1676
|
tokenBudget,
|
|
437
1677
|
includeVerification,
|
|
438
1678
|
allowedWorkstreamIds,
|
|
1679
|
+
maxParallelSlices: maxParallelRaw,
|
|
1680
|
+
parallelMode,
|
|
1681
|
+
ignoreSpawnGuardRateLimit: ignoreSpawnGuardRateLimit === true,
|
|
1682
|
+
scope: startScope,
|
|
439
1683
|
});
|
|
440
|
-
|
|
1684
|
+
const dispatchEnvelope = buildDispatchGatewayEnvelope({
|
|
1685
|
+
dispatchMode: "server",
|
|
1686
|
+
route: "mission-control.auto-continue.start",
|
|
1687
|
+
source: "auto_continue_start",
|
|
1688
|
+
initiativeId,
|
|
1689
|
+
workstreamIds: allowedWorkstreamIds,
|
|
1690
|
+
});
|
|
1691
|
+
deps.sendJson(res, 200, { ok: true, ...dispatchEnvelope, run });
|
|
441
1692
|
}
|
|
442
1693
|
catch (err) {
|
|
443
|
-
|
|
1694
|
+
sendRouteException(res, "mission-control.auto-continue.start.handler", err);
|
|
444
1695
|
}
|
|
445
1696
|
}, "Mission-control auto-continue start");
|
|
446
1697
|
router.add("POST", "mission-control/auto-continue/stop", async ({ req, query, res }) => {
|
|
@@ -452,19 +1703,24 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
452
1703
|
"")
|
|
453
1704
|
.trim();
|
|
454
1705
|
if (!initiativeId) {
|
|
455
|
-
|
|
1706
|
+
sendRouteError(res, 400, "mission-control.auto-continue.stop.validation", "initiativeId is required");
|
|
456
1707
|
return;
|
|
457
1708
|
}
|
|
458
1709
|
const run = deps.autoContinueRuns.get(initiativeId) ?? null;
|
|
459
1710
|
if (!run) {
|
|
460
|
-
|
|
1711
|
+
sendRouteError(res, 404, "mission-control.auto-continue.stop.lookup", "No auto-continue run found");
|
|
461
1712
|
return;
|
|
462
1713
|
}
|
|
463
1714
|
const now = new Date().toISOString();
|
|
1715
|
+
const activeRunIds = Array.isArray(run.activeSliceRunIds)
|
|
1716
|
+
? run.activeSliceRunIds.filter((id) => typeof id === "string" && id.trim().length > 0)
|
|
1717
|
+
: typeof run.activeRunId === "string" && run.activeRunId.trim().length > 0
|
|
1718
|
+
? [run.activeRunId]
|
|
1719
|
+
: [];
|
|
464
1720
|
run.stopRequested = true;
|
|
465
|
-
run.status =
|
|
1721
|
+
run.status = activeRunIds.length > 0 ? "stopping" : "stopped";
|
|
466
1722
|
run.updatedAt = now;
|
|
467
|
-
if (
|
|
1723
|
+
if (activeRunIds.length === 0) {
|
|
468
1724
|
await deps.stopAutoContinueRun({ run, reason: "stopped" });
|
|
469
1725
|
}
|
|
470
1726
|
else {
|
|
@@ -478,7 +1734,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
478
1734
|
deps.sendJson(res, 200, { ok: true, run });
|
|
479
1735
|
}
|
|
480
1736
|
catch (err) {
|
|
481
|
-
|
|
1737
|
+
sendRouteException(res, "mission-control.auto-continue.stop.handler", err);
|
|
482
1738
|
}
|
|
483
1739
|
}, "Mission-control auto-continue stop");
|
|
484
1740
|
router.add("POST", "mission-control/auto-continue/tick", async ({ req, query, res }) => {
|
|
@@ -492,7 +1748,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
492
1748
|
if (initiativeId) {
|
|
493
1749
|
const run = deps.autoContinueRuns.get(initiativeId) ?? null;
|
|
494
1750
|
if (!run) {
|
|
495
|
-
|
|
1751
|
+
sendRouteError(res, 404, "mission-control.auto-continue.tick.lookup", "No auto-continue run found");
|
|
496
1752
|
return;
|
|
497
1753
|
}
|
|
498
1754
|
await deps.tickAutoContinueRun(run);
|
|
@@ -503,7 +1759,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
503
1759
|
deps.sendJson(res, 200, { ok: true });
|
|
504
1760
|
}
|
|
505
1761
|
catch (err) {
|
|
506
|
-
|
|
1762
|
+
sendRouteException(res, "mission-control.auto-continue.tick.handler", err);
|
|
507
1763
|
}
|
|
508
1764
|
}, "Mission-control auto-continue tick");
|
|
509
1765
|
router.add("POST", "mission-control/assignments/auto", async ({ req, res }) => {
|
|
@@ -515,10 +1771,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
515
1771
|
const title = deps.pickString(payload, ["title", "name"]) ?? "Untitled";
|
|
516
1772
|
const summary = deps.pickString(payload, ["summary", "description", "context"]) ?? null;
|
|
517
1773
|
if (!entityId || !entityType) {
|
|
518
|
-
|
|
519
|
-
ok: false,
|
|
520
|
-
error: "entity_id and entity_type are required.",
|
|
521
|
-
});
|
|
1774
|
+
sendRouteError(res, 400, "mission-control.assignments.auto.validation", "entity_id and entity_type are required.");
|
|
522
1775
|
return;
|
|
523
1776
|
}
|
|
524
1777
|
const assignment = await deps.resolveAutoAssignments({
|
|
@@ -532,10 +1785,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
|
|
|
532
1785
|
deps.sendJson(res, 200, assignment);
|
|
533
1786
|
}
|
|
534
1787
|
catch (err) {
|
|
535
|
-
|
|
536
|
-
ok: false,
|
|
537
|
-
error: deps.safeErrorMessage(err),
|
|
538
|
-
});
|
|
1788
|
+
sendRouteException(res, "mission-control.assignments.auto.handler", err);
|
|
539
1789
|
}
|
|
540
1790
|
}, "Mission-control auto assignment");
|
|
541
1791
|
}
|