@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,12 +1,742 @@
|
|
|
1
|
+
import { listBuiltInSentinels } from "../helpers/sentinel-catalog.js";
|
|
2
|
+
import { resolveWorkspaceScope, workspaceScopeFromHeaders, } from "../helpers/workspace-scope.js";
|
|
3
|
+
function asRecord(value) {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5
|
+
return null;
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function asString(value) {
|
|
9
|
+
if (typeof value !== "string")
|
|
10
|
+
return null;
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
13
|
+
}
|
|
14
|
+
function normalizeRunnerValue(value) {
|
|
15
|
+
const raw = asString(value);
|
|
16
|
+
if (!raw)
|
|
17
|
+
return null;
|
|
18
|
+
const normalized = raw.trim().toLowerCase();
|
|
19
|
+
if (!normalized || normalized === "undefined" || normalized === "null")
|
|
20
|
+
return null;
|
|
21
|
+
if (normalized === "main" || normalized === "unassigned")
|
|
22
|
+
return null;
|
|
23
|
+
if (normalized === "n/a" || normalized === "na" || normalized === "none")
|
|
24
|
+
return null;
|
|
25
|
+
if (normalized === "-" || normalized === "default")
|
|
26
|
+
return null;
|
|
27
|
+
return raw.trim();
|
|
28
|
+
}
|
|
29
|
+
function normalizeRunnerSource(value) {
|
|
30
|
+
const raw = asString(value);
|
|
31
|
+
if (!raw)
|
|
32
|
+
return null;
|
|
33
|
+
const normalized = raw.trim().toLowerCase();
|
|
34
|
+
if (normalized === "assigned")
|
|
35
|
+
return "assigned";
|
|
36
|
+
if (normalized === "inferred")
|
|
37
|
+
return "inferred";
|
|
38
|
+
if (normalized === "fallback")
|
|
39
|
+
return "fallback";
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function normalizeRunnerAgents(value) {
|
|
43
|
+
if (!Array.isArray(value))
|
|
44
|
+
return [];
|
|
45
|
+
const output = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
for (const entry of value) {
|
|
48
|
+
const record = asRecord(entry);
|
|
49
|
+
if (!record)
|
|
50
|
+
continue;
|
|
51
|
+
const id = normalizeRunnerValue(record.id);
|
|
52
|
+
const name = normalizeRunnerValue(record.name);
|
|
53
|
+
if (!id && !name)
|
|
54
|
+
continue;
|
|
55
|
+
const resolvedId = id ?? name;
|
|
56
|
+
const key = resolvedId.toLowerCase();
|
|
57
|
+
if (seen.has(key))
|
|
58
|
+
continue;
|
|
59
|
+
seen.add(key);
|
|
60
|
+
output.push({
|
|
61
|
+
id: resolvedId,
|
|
62
|
+
name: name ?? resolvedId,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
function mergeRunnerAgents(...groups) {
|
|
68
|
+
const output = [];
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
for (const group of groups) {
|
|
71
|
+
for (const agent of group) {
|
|
72
|
+
const id = normalizeRunnerValue(agent.id);
|
|
73
|
+
const name = normalizeRunnerValue(agent.name);
|
|
74
|
+
if (!id && !name)
|
|
75
|
+
continue;
|
|
76
|
+
const resolvedId = id ?? name;
|
|
77
|
+
const key = resolvedId.toLowerCase();
|
|
78
|
+
if (seen.has(key))
|
|
79
|
+
continue;
|
|
80
|
+
seen.add(key);
|
|
81
|
+
output.push({
|
|
82
|
+
id: resolvedId,
|
|
83
|
+
name: name ?? resolvedId,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return output;
|
|
88
|
+
}
|
|
89
|
+
function asNumber(value) {
|
|
90
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
91
|
+
return value;
|
|
92
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
93
|
+
const parsed = Number(value);
|
|
94
|
+
if (Number.isFinite(parsed))
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function asStringArray(value) {
|
|
100
|
+
if (!Array.isArray(value))
|
|
101
|
+
return [];
|
|
102
|
+
const values = [];
|
|
103
|
+
for (const entry of value) {
|
|
104
|
+
const normalized = asString(entry);
|
|
105
|
+
if (!normalized)
|
|
106
|
+
continue;
|
|
107
|
+
values.push(normalized);
|
|
108
|
+
}
|
|
109
|
+
return dedupeStrings(values);
|
|
110
|
+
}
|
|
111
|
+
function isCanonicalAllScopeMismatch(canonicalRecord, useAllScope) {
|
|
112
|
+
if (!useAllScope)
|
|
113
|
+
return false;
|
|
114
|
+
const workspaceRaw = asString(canonicalRecord.workspaceId) ??
|
|
115
|
+
asString(canonicalRecord.workspace_id);
|
|
116
|
+
if (!workspaceRaw)
|
|
117
|
+
return false;
|
|
118
|
+
const normalized = workspaceRaw.trim().toLowerCase();
|
|
119
|
+
if (!normalized)
|
|
120
|
+
return false;
|
|
121
|
+
return normalized !== "all" && normalized !== "__all__" && normalized !== "*";
|
|
122
|
+
}
|
|
123
|
+
function dedupeStrings(values) {
|
|
124
|
+
const output = [];
|
|
125
|
+
const seen = new Set();
|
|
126
|
+
for (const value of values) {
|
|
127
|
+
const normalized = value.trim();
|
|
128
|
+
if (!normalized)
|
|
129
|
+
continue;
|
|
130
|
+
const key = normalized.toLowerCase();
|
|
131
|
+
if (seen.has(key))
|
|
132
|
+
continue;
|
|
133
|
+
seen.add(key);
|
|
134
|
+
output.push(normalized);
|
|
135
|
+
}
|
|
136
|
+
return output;
|
|
137
|
+
}
|
|
138
|
+
function parseBoolean(value) {
|
|
139
|
+
if (!value)
|
|
140
|
+
return false;
|
|
141
|
+
const normalized = value.trim().toLowerCase();
|
|
142
|
+
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
143
|
+
}
|
|
144
|
+
function parsePositiveInt(value, fallback, max = 300) {
|
|
145
|
+
if (!value || value.trim().length === 0)
|
|
146
|
+
return fallback;
|
|
147
|
+
const parsed = Number.parseInt(value, 10);
|
|
148
|
+
if (!Number.isFinite(parsed))
|
|
149
|
+
return fallback;
|
|
150
|
+
return Math.max(0, Math.min(max, parsed));
|
|
151
|
+
}
|
|
152
|
+
function normalizeSliceSearchTerm(value) {
|
|
153
|
+
return (value ?? "").trim().toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
function extractSliceSearchText(item) {
|
|
156
|
+
const record = asRecord(item);
|
|
157
|
+
if (!record)
|
|
158
|
+
return "";
|
|
159
|
+
const candidates = [
|
|
160
|
+
asString(record.sliceId),
|
|
161
|
+
asString(record.id),
|
|
162
|
+
asString(record.title),
|
|
163
|
+
asString(record.initiativeTitle),
|
|
164
|
+
asString(record.workstreamTitle),
|
|
165
|
+
asString(record.milestoneTitle),
|
|
166
|
+
asString(record.taskTitle),
|
|
167
|
+
asString(record.initiativeId),
|
|
168
|
+
asString(record.workstreamId),
|
|
169
|
+
asString(record.milestoneId),
|
|
170
|
+
asString(record.taskId),
|
|
171
|
+
asString(record.scope),
|
|
172
|
+
asString(record.level),
|
|
173
|
+
].filter((entry) => Boolean(entry));
|
|
174
|
+
return candidates.join(" ").toLowerCase();
|
|
175
|
+
}
|
|
176
|
+
function applySliceSearchAndPagination(input) {
|
|
177
|
+
const filtered = input.searchTerm.length === 0
|
|
178
|
+
? input.items
|
|
179
|
+
: input.items.filter((item) => extractSliceSearchText(item).includes(input.searchTerm));
|
|
180
|
+
const offset = Math.max(0, input.offset);
|
|
181
|
+
const paged = filtered.slice(offset, offset + input.limit);
|
|
182
|
+
const nextOffset = offset + input.limit;
|
|
183
|
+
const hasMore = nextOffset < filtered.length;
|
|
184
|
+
return {
|
|
185
|
+
filtered,
|
|
186
|
+
paged,
|
|
187
|
+
pagination: {
|
|
188
|
+
offset,
|
|
189
|
+
limit: input.limit,
|
|
190
|
+
total: filtered.length,
|
|
191
|
+
nextCursor: hasMore ? String(nextOffset) : null,
|
|
192
|
+
hasMore,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function parsePaginationEnvelope(value, fallback) {
|
|
197
|
+
const record = asRecord(value);
|
|
198
|
+
const offset = Math.max(0, Math.min(100_000, Math.floor(asNumber(record?.offset) ?? fallback.offset)));
|
|
199
|
+
const limit = Math.max(1, Math.min(300, Math.floor(asNumber(record?.limit) ?? fallback.limit)));
|
|
200
|
+
const total = Math.max(0, Math.floor(asNumber(record?.total) ?? fallback.total));
|
|
201
|
+
const nextCursor = asString(record?.nextCursor);
|
|
202
|
+
const hasMore = typeof record?.hasMore === "boolean"
|
|
203
|
+
? record.hasMore
|
|
204
|
+
: offset + limit < total;
|
|
205
|
+
return { offset, limit, total, nextCursor, hasMore };
|
|
206
|
+
}
|
|
207
|
+
const WARMUP_MIN_INTERVAL_MS = 12_000;
|
|
208
|
+
const NEXT_UP_DEFAULT_PAGE_SIZE = 24;
|
|
209
|
+
const SLICES_DEFAULT_PAGE_SIZE = 24;
|
|
210
|
+
const CANONICAL_NEXT_UP_TIMEOUT_MS = 20_000;
|
|
211
|
+
const CANONICAL_SLICES_TIMEOUT_MS = 20_000;
|
|
212
|
+
const CANONICAL_READ_CACHE_TTL_MS = 30_000;
|
|
213
|
+
const CANONICAL_READ_STALE_TTL_MS = 180_000;
|
|
214
|
+
const CANONICAL_AUTH_BYPASS_MS = 8_000;
|
|
215
|
+
const warmupByKey = new Map();
|
|
216
|
+
const canonicalReadCache = new Map();
|
|
217
|
+
const canonicalBypassState = new Map();
|
|
218
|
+
function shouldRunWarmup(key) {
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
const previous = warmupByKey.get(key);
|
|
221
|
+
if (typeof previous === "number" && now - previous < WARMUP_MIN_INTERVAL_MS) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
warmupByKey.set(key, now);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
function canonicalReadCacheKey(input) {
|
|
228
|
+
return [
|
|
229
|
+
input.route,
|
|
230
|
+
input.workspaceId ?? "__all__",
|
|
231
|
+
input.scopeMode ?? "implicit",
|
|
232
|
+
input.initiativeId ?? "__any__",
|
|
233
|
+
input.includeCompleted ? "include_completed" : "exclude_completed",
|
|
234
|
+
String(input.offset),
|
|
235
|
+
String(input.limit),
|
|
236
|
+
input.scope ?? "__none__",
|
|
237
|
+
input.order ?? "__none__",
|
|
238
|
+
input.mixPolicy ?? "__none__",
|
|
239
|
+
input.search ?? "__none__",
|
|
240
|
+
].join("|");
|
|
241
|
+
}
|
|
242
|
+
function cloneCanonicalReadPayload(payload) {
|
|
243
|
+
const clone = { ...payload };
|
|
244
|
+
if (Array.isArray(payload.items))
|
|
245
|
+
clone.items = [...payload.items];
|
|
246
|
+
if (Array.isArray(payload.degraded))
|
|
247
|
+
clone.degraded = [...payload.degraded];
|
|
248
|
+
const pagination = asRecord(payload.pagination);
|
|
249
|
+
if (pagination)
|
|
250
|
+
clone.pagination = { ...pagination };
|
|
251
|
+
return clone;
|
|
252
|
+
}
|
|
253
|
+
function readCanonicalReadCache(key, opts) {
|
|
254
|
+
const cached = canonicalReadCache.get(key);
|
|
255
|
+
if (!cached)
|
|
256
|
+
return null;
|
|
257
|
+
const now = Date.now();
|
|
258
|
+
const allowStale = Boolean(opts?.allowStale);
|
|
259
|
+
const stillFresh = cached.expiresAt > now;
|
|
260
|
+
const stillStale = cached.staleUntil > now;
|
|
261
|
+
if (!stillFresh && !stillStale) {
|
|
262
|
+
canonicalReadCache.delete(key);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
if (!stillFresh && !allowStale)
|
|
266
|
+
return null;
|
|
267
|
+
return cloneCanonicalReadPayload(cached.payload);
|
|
268
|
+
}
|
|
269
|
+
function writeCanonicalReadCache(key, payload) {
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
canonicalReadCache.set(key, {
|
|
272
|
+
expiresAt: now + CANONICAL_READ_CACHE_TTL_MS,
|
|
273
|
+
staleUntil: now + CANONICAL_READ_STALE_TTL_MS,
|
|
274
|
+
payload: cloneCanonicalReadPayload(payload),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function readCanonicalBypass(route) {
|
|
278
|
+
const record = canonicalBypassState.get(route);
|
|
279
|
+
if (!record)
|
|
280
|
+
return null;
|
|
281
|
+
if (record.until <= Date.now()) {
|
|
282
|
+
canonicalBypassState.delete(route);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return { reason: record.reason };
|
|
286
|
+
}
|
|
287
|
+
function setCanonicalBypass(route, reason, durationMs) {
|
|
288
|
+
canonicalBypassState.set(route, {
|
|
289
|
+
until: Date.now() + Math.max(1_000, durationMs),
|
|
290
|
+
reason,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function isCanonicalAuthFailure(error) {
|
|
294
|
+
const message = String(error instanceof Error ? error.message : error ?? "").toLowerCase();
|
|
295
|
+
return (message.includes("401") ||
|
|
296
|
+
message.includes("403") ||
|
|
297
|
+
message.includes("unauthorized") ||
|
|
298
|
+
message.includes("forbidden") ||
|
|
299
|
+
message.includes("authentication required"));
|
|
300
|
+
}
|
|
301
|
+
function isCanonicalAllScopeMismatchError(error) {
|
|
302
|
+
const message = String(error instanceof Error ? error.message : error ?? "").toLowerCase();
|
|
303
|
+
return message.includes("all-workspaces scope mismatch");
|
|
304
|
+
}
|
|
305
|
+
async function withSoftTimeout(request, timeoutMs, label) {
|
|
306
|
+
let timer = null;
|
|
307
|
+
const timeout = new Promise((_, reject) => {
|
|
308
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
309
|
+
});
|
|
310
|
+
try {
|
|
311
|
+
return await Promise.race([request, timeout]);
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
if (timer)
|
|
315
|
+
clearTimeout(timer);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function shouldRetryLegacyCanonicalPath(error) {
|
|
319
|
+
const message = String(error instanceof Error ? error.message : error ?? "").toLowerCase();
|
|
320
|
+
return (message.includes("404") ||
|
|
321
|
+
message.includes("not found") ||
|
|
322
|
+
message.includes("unknown api endpoint") ||
|
|
323
|
+
message.includes("401") ||
|
|
324
|
+
message.includes("403") ||
|
|
325
|
+
message.includes("unauthorized") ||
|
|
326
|
+
message.includes("forbidden"));
|
|
327
|
+
}
|
|
328
|
+
async function requestCanonicalWithLegacyFallback(deps, input) {
|
|
329
|
+
try {
|
|
330
|
+
return await withSoftTimeout(deps.rawRequest("GET", input.modernPath), input.timeoutMs, input.label);
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
if (!shouldRetryLegacyCanonicalPath(error))
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
return await withSoftTimeout(deps.rawRequest("GET", input.legacyPath), Math.min(input.timeoutMs, 1_500), `${input.label} (legacy fallback)`);
|
|
337
|
+
}
|
|
338
|
+
function normalizeQueueState(value) {
|
|
339
|
+
const normalized = normalizeStatus(asString(value));
|
|
340
|
+
if (normalized === "running" || normalized === "in_progress" || normalized === "active") {
|
|
341
|
+
return "running";
|
|
342
|
+
}
|
|
343
|
+
if (normalized === "queued" || normalized === "pending" || normalized === "todo" || normalized === "ready") {
|
|
344
|
+
return "queued";
|
|
345
|
+
}
|
|
346
|
+
if (normalized === "blocked" || normalized === "waiting")
|
|
347
|
+
return "blocked";
|
|
348
|
+
if (normalized === "completed" || normalized === "done")
|
|
349
|
+
return "completed";
|
|
350
|
+
return "idle";
|
|
351
|
+
}
|
|
352
|
+
function normalizeStatus(value) {
|
|
353
|
+
return (value ?? "").trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
354
|
+
}
|
|
355
|
+
function isDoneStatus(value) {
|
|
356
|
+
const normalized = normalizeStatus(value);
|
|
357
|
+
return (normalized === "done" ||
|
|
358
|
+
normalized === "completed" ||
|
|
359
|
+
normalized === "resolved" ||
|
|
360
|
+
normalized === "cancelled" ||
|
|
361
|
+
normalized === "canceled" ||
|
|
362
|
+
normalized === "archived" ||
|
|
363
|
+
normalized === "closed");
|
|
364
|
+
}
|
|
365
|
+
function queueStateRank(state) {
|
|
366
|
+
if (state === "running")
|
|
367
|
+
return 0;
|
|
368
|
+
if (state === "queued")
|
|
369
|
+
return 1;
|
|
370
|
+
if (state === "blocked")
|
|
371
|
+
return 2;
|
|
372
|
+
if (state === "idle")
|
|
373
|
+
return 3;
|
|
374
|
+
return 4;
|
|
375
|
+
}
|
|
376
|
+
function combinedQueueState(states) {
|
|
377
|
+
if (states.some((state) => state === "running"))
|
|
378
|
+
return "running";
|
|
379
|
+
if (states.some((state) => state === "blocked"))
|
|
380
|
+
return "blocked";
|
|
381
|
+
if (states.some((state) => state === "queued"))
|
|
382
|
+
return "queued";
|
|
383
|
+
if (states.some((state) => state === "idle"))
|
|
384
|
+
return "idle";
|
|
385
|
+
return "completed";
|
|
386
|
+
}
|
|
387
|
+
function dueEpoch(value) {
|
|
388
|
+
if (!value)
|
|
389
|
+
return Number.MAX_SAFE_INTEGER;
|
|
390
|
+
const parsed = Date.parse(value);
|
|
391
|
+
return Number.isFinite(parsed) ? parsed : Number.MAX_SAFE_INTEGER;
|
|
392
|
+
}
|
|
393
|
+
function updatedEpoch(value) {
|
|
394
|
+
if (!value)
|
|
395
|
+
return 0;
|
|
396
|
+
const parsed = Date.parse(value);
|
|
397
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
398
|
+
}
|
|
399
|
+
function parseSliceScope(value) {
|
|
400
|
+
const normalized = (value ?? "").trim().toLowerCase();
|
|
401
|
+
if (normalized === "initiative")
|
|
402
|
+
return "initiative";
|
|
403
|
+
if (normalized === "milestone")
|
|
404
|
+
return "milestone";
|
|
405
|
+
if (normalized === "task")
|
|
406
|
+
return "task";
|
|
407
|
+
return "workstream";
|
|
408
|
+
}
|
|
409
|
+
function parseSliceOrder(value) {
|
|
410
|
+
const normalized = (value ?? "").trim().toLowerCase();
|
|
411
|
+
if (normalized === "priority")
|
|
412
|
+
return "priority";
|
|
413
|
+
if (normalized === "due")
|
|
414
|
+
return "due";
|
|
415
|
+
if (normalized === "updated")
|
|
416
|
+
return "updated";
|
|
417
|
+
return "iwmt";
|
|
418
|
+
}
|
|
419
|
+
function sortSlices(items, order) {
|
|
420
|
+
return [...items].sort((left, right) => {
|
|
421
|
+
if (order === "priority") {
|
|
422
|
+
const leftPriority = left.nextTaskPriority ?? Number.MAX_SAFE_INTEGER;
|
|
423
|
+
const rightPriority = right.nextTaskPriority ?? Number.MAX_SAFE_INTEGER;
|
|
424
|
+
if (leftPriority !== rightPriority)
|
|
425
|
+
return leftPriority - rightPriority;
|
|
426
|
+
}
|
|
427
|
+
else if (order === "due") {
|
|
428
|
+
const leftDue = dueEpoch(left.nextTaskDueAt);
|
|
429
|
+
const rightDue = dueEpoch(right.nextTaskDueAt);
|
|
430
|
+
if (leftDue !== rightDue)
|
|
431
|
+
return leftDue - rightDue;
|
|
432
|
+
}
|
|
433
|
+
else if (order === "updated") {
|
|
434
|
+
const leftUpdated = updatedEpoch(left.updatedAt);
|
|
435
|
+
const rightUpdated = updatedEpoch(right.updatedAt);
|
|
436
|
+
if (leftUpdated !== rightUpdated)
|
|
437
|
+
return rightUpdated - leftUpdated;
|
|
438
|
+
}
|
|
439
|
+
const iwmtDelta = left.iwmtRank - right.iwmtRank;
|
|
440
|
+
if (iwmtDelta !== 0)
|
|
441
|
+
return iwmtDelta;
|
|
442
|
+
const queueDelta = queueStateRank(left.queueState) - queueStateRank(right.queueState);
|
|
443
|
+
if (queueDelta !== 0)
|
|
444
|
+
return queueDelta;
|
|
445
|
+
const initiativeDelta = left.initiativeTitle.localeCompare(right.initiativeTitle);
|
|
446
|
+
if (initiativeDelta !== 0)
|
|
447
|
+
return initiativeDelta;
|
|
448
|
+
const workstreamDelta = (left.workstreamTitle ?? "").localeCompare(right.workstreamTitle ?? "");
|
|
449
|
+
if (workstreamDelta !== 0)
|
|
450
|
+
return workstreamDelta;
|
|
451
|
+
return left.id.localeCompare(right.id);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
function normalizeQueueItems(input) {
|
|
455
|
+
const output = [];
|
|
456
|
+
for (const entry of input) {
|
|
457
|
+
const record = asRecord(entry);
|
|
458
|
+
if (!record)
|
|
459
|
+
continue;
|
|
460
|
+
const initiativeId = asString(record.initiativeId) ?? asString(record.initiative_id);
|
|
461
|
+
const workstreamId = asString(record.workstreamId) ?? asString(record.workstream_id);
|
|
462
|
+
if (!initiativeId || !workstreamId)
|
|
463
|
+
continue;
|
|
464
|
+
const nextTaskId = asString(record.nextTaskId) ?? asString(record.next_task_id);
|
|
465
|
+
const sliceTaskIds = dedupeStrings([
|
|
466
|
+
...asStringArray(record.sliceTaskIds),
|
|
467
|
+
...asStringArray(record.slice_task_ids),
|
|
468
|
+
...(nextTaskId ? [nextTaskId] : []),
|
|
469
|
+
]);
|
|
470
|
+
const runnerAgentsRaw = mergeRunnerAgents(normalizeRunnerAgents(record.runnerAgents), normalizeRunnerAgents(record.runner_agents));
|
|
471
|
+
const runnerAgentIdRaw = normalizeRunnerValue(record.runnerAgentId) ?? normalizeRunnerValue(record.runner_agent_id);
|
|
472
|
+
const runnerAgentNameRaw = normalizeRunnerValue(record.runnerAgentName) ??
|
|
473
|
+
normalizeRunnerValue(record.runner_agent_name) ??
|
|
474
|
+
normalizeRunnerValue(record.agentName) ??
|
|
475
|
+
normalizeRunnerValue(record.runner);
|
|
476
|
+
const runnerAgents = mergeRunnerAgents(runnerAgentsRaw, runnerAgentIdRaw || runnerAgentNameRaw
|
|
477
|
+
? [
|
|
478
|
+
{
|
|
479
|
+
id: runnerAgentIdRaw ?? runnerAgentNameRaw ?? "Unassigned",
|
|
480
|
+
name: runnerAgentNameRaw ?? runnerAgentIdRaw ?? "Unassigned",
|
|
481
|
+
},
|
|
482
|
+
]
|
|
483
|
+
: []);
|
|
484
|
+
const runnerPrimary = runnerAgents[0] ?? null;
|
|
485
|
+
const runnerAgentId = runnerPrimary?.id ?? null;
|
|
486
|
+
const runnerAgentName = runnerPrimary?.name ?? "Unassigned";
|
|
487
|
+
const runnerSourceHint = normalizeRunnerSource(record.runnerSource) ?? normalizeRunnerSource(record.runner_source);
|
|
488
|
+
const runnerSource = runnerAgentId
|
|
489
|
+
? runnerSourceHint ?? "inferred"
|
|
490
|
+
: "fallback";
|
|
491
|
+
const queueState = normalizeQueueState(record.queueState ?? record.queue_state);
|
|
492
|
+
const rawSliceScope = asString(record.sliceScope) ?? asString(record.slice_scope);
|
|
493
|
+
const sliceScope = rawSliceScope === "task" || rawSliceScope === "milestone" || rawSliceScope === "workstream"
|
|
494
|
+
? rawSliceScope
|
|
495
|
+
: null;
|
|
496
|
+
const sliceTaskCountRaw = asNumber(record.sliceTaskCount ?? record.slice_task_count);
|
|
497
|
+
const blockReason = asString(record.blockReason) ??
|
|
498
|
+
asString(record.block_reason) ??
|
|
499
|
+
(queueState === "blocked"
|
|
500
|
+
? `Waiting on dependency ${asString(record.nextTaskTitle) ?? asString(record.next_task_title) ?? asString(record.nextTaskId) ?? asString(record.next_task_id) ?? "task"}`
|
|
501
|
+
: null);
|
|
502
|
+
output.push({
|
|
503
|
+
initiativeId,
|
|
504
|
+
initiativeTitle: asString(record.initiativeTitle) ?? asString(record.initiative_title) ?? initiativeId,
|
|
505
|
+
initiativeStatus: asString(record.initiativeStatus) ?? asString(record.initiative_status) ?? "active",
|
|
506
|
+
initiativePriority: asString(record.initiativePriority) ?? asString(record.initiative_priority),
|
|
507
|
+
initiativePriorityNum: asNumber(record.initiativePriorityNum ?? record.initiative_priority_num),
|
|
508
|
+
workstreamId,
|
|
509
|
+
workstreamTitle: asString(record.workstreamTitle) ?? asString(record.workstream_title) ?? workstreamId,
|
|
510
|
+
workstreamStatus: asString(record.workstreamStatus) ?? asString(record.workstream_status) ?? "active",
|
|
511
|
+
nextTaskId,
|
|
512
|
+
nextTaskTitle: asString(record.nextTaskTitle) ?? asString(record.next_task_title),
|
|
513
|
+
nextTaskPriority: asNumber(record.nextTaskPriority ?? record.next_task_priority),
|
|
514
|
+
nextTaskDueAt: asString(record.nextTaskDueAt) ?? asString(record.next_task_due_at),
|
|
515
|
+
nextTaskMilestoneId: asString(record.nextTaskMilestoneId) ?? asString(record.next_task_milestone_id),
|
|
516
|
+
runnerAgentId,
|
|
517
|
+
runnerAgentName,
|
|
518
|
+
runnerAgents,
|
|
519
|
+
runnerSource,
|
|
520
|
+
queueState,
|
|
521
|
+
blockReason,
|
|
522
|
+
sliceScope,
|
|
523
|
+
sliceTaskIds,
|
|
524
|
+
sliceTaskCount: typeof sliceTaskCountRaw === "number"
|
|
525
|
+
? Math.max(0, Math.floor(sliceTaskCountRaw))
|
|
526
|
+
: sliceTaskIds.length,
|
|
527
|
+
sliceMilestoneId: asString(record.sliceMilestoneId) ?? asString(record.slice_milestone_id),
|
|
528
|
+
isPinned: Boolean(record.isPinned ?? record.is_pinned),
|
|
529
|
+
pinnedRank: asNumber(record.pinnedRank ?? record.pinned_rank),
|
|
530
|
+
compositeScore: asNumber(record.compositeScore ?? record.composite_score) ?? undefined,
|
|
531
|
+
scoringTier: asString(record.scoringTier ?? record.scoring_tier) === "urgent" ||
|
|
532
|
+
asString(record.scoringTier ?? record.scoring_tier) === "ready" ||
|
|
533
|
+
asString(record.scoringTier ?? record.scoring_tier) === "waiting" ||
|
|
534
|
+
asString(record.scoringTier ?? record.scoring_tier) === "deferred"
|
|
535
|
+
? asString(record.scoringTier ?? record.scoring_tier)
|
|
536
|
+
: undefined,
|
|
537
|
+
updatedAt: asString(record.updatedAt) ?? asString(record.updated_at) ?? null,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
return output.sort((left, right) => {
|
|
541
|
+
const pinnedLeft = left.isPinned ? 0 : 1;
|
|
542
|
+
const pinnedRight = right.isPinned ? 0 : 1;
|
|
543
|
+
if (pinnedLeft !== pinnedRight)
|
|
544
|
+
return pinnedLeft - pinnedRight;
|
|
545
|
+
if (pinnedLeft === 0) {
|
|
546
|
+
const rankDelta = (left.pinnedRank ?? Number.MAX_SAFE_INTEGER) -
|
|
547
|
+
(right.pinnedRank ?? Number.MAX_SAFE_INTEGER);
|
|
548
|
+
if (rankDelta !== 0)
|
|
549
|
+
return rankDelta;
|
|
550
|
+
}
|
|
551
|
+
const queueDelta = queueStateRank(left.queueState) - queueStateRank(right.queueState);
|
|
552
|
+
if (queueDelta !== 0)
|
|
553
|
+
return queueDelta;
|
|
554
|
+
const priorityDelta = (left.nextTaskPriority ?? Number.MAX_SAFE_INTEGER) -
|
|
555
|
+
(right.nextTaskPriority ?? Number.MAX_SAFE_INTEGER);
|
|
556
|
+
if (priorityDelta !== 0)
|
|
557
|
+
return priorityDelta;
|
|
558
|
+
const initiativePriorityDelta = (left.initiativePriorityNum ?? Number.MAX_SAFE_INTEGER) -
|
|
559
|
+
(right.initiativePriorityNum ?? Number.MAX_SAFE_INTEGER);
|
|
560
|
+
if (initiativePriorityDelta !== 0)
|
|
561
|
+
return initiativePriorityDelta;
|
|
562
|
+
const dueDelta = dueEpoch(left.nextTaskDueAt) - dueEpoch(right.nextTaskDueAt);
|
|
563
|
+
if (dueDelta !== 0)
|
|
564
|
+
return dueDelta;
|
|
565
|
+
const titleDelta = left.initiativeTitle.localeCompare(right.initiativeTitle);
|
|
566
|
+
if (titleDelta !== 0)
|
|
567
|
+
return titleDelta;
|
|
568
|
+
return left.workstreamTitle.localeCompare(right.workstreamTitle);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
function mapCanonicalSlicesToQueueItems(input) {
|
|
572
|
+
const queueLike = [];
|
|
573
|
+
for (const entry of input) {
|
|
574
|
+
const record = asRecord(entry);
|
|
575
|
+
if (!record)
|
|
576
|
+
continue;
|
|
577
|
+
const initiativeId = asString(record.initiativeId) ?? asString(record.initiative_id);
|
|
578
|
+
const workstreamId = asString(record.workstreamId) ?? asString(record.workstream_id);
|
|
579
|
+
if (!initiativeId || !workstreamId)
|
|
580
|
+
continue;
|
|
581
|
+
const dispatch = asRecord(record.dispatch) ?? {};
|
|
582
|
+
const lineage = asRecord(record.lineage) ?? {};
|
|
583
|
+
const taskId = asString(record.taskId) ?? asString(record.task_id);
|
|
584
|
+
const sliceTaskIds = dedupeStrings([
|
|
585
|
+
...asStringArray(record.sliceTaskIds),
|
|
586
|
+
...asStringArray(record.slice_task_ids),
|
|
587
|
+
...asStringArray(lineage.taskIds),
|
|
588
|
+
...asStringArray(lineage.task_ids),
|
|
589
|
+
...(taskId ? [taskId] : []),
|
|
590
|
+
]);
|
|
591
|
+
const rawStatus = asString(record.status) ?? "active";
|
|
592
|
+
const normalizedStatus = normalizeStatus(rawStatus);
|
|
593
|
+
const runnable = Boolean(dispatch.runnable);
|
|
594
|
+
let queueState;
|
|
595
|
+
if (isDoneStatus(rawStatus)) {
|
|
596
|
+
queueState = "completed";
|
|
597
|
+
}
|
|
598
|
+
else if (normalizedStatus === "running" || normalizedStatus === "in_progress") {
|
|
599
|
+
queueState = "running";
|
|
600
|
+
}
|
|
601
|
+
else if (normalizedStatus === "blocked" ||
|
|
602
|
+
normalizedStatus === "waiting_dependency" ||
|
|
603
|
+
normalizedStatus === "paused" ||
|
|
604
|
+
!runnable) {
|
|
605
|
+
queueState = "blocked";
|
|
606
|
+
}
|
|
607
|
+
else if (normalizedStatus === "idle" ||
|
|
608
|
+
normalizedStatus === "not_started" ||
|
|
609
|
+
normalizedStatus === "draft") {
|
|
610
|
+
queueState = "idle";
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
queueState = "queued";
|
|
614
|
+
}
|
|
615
|
+
const runnerAgentIdRaw = normalizeRunnerValue(record.runnerAgentId) ?? normalizeRunnerValue(record.runner_agent_id);
|
|
616
|
+
const runnerAgentNameRaw = normalizeRunnerValue(record.runnerAgentName) ?? normalizeRunnerValue(record.runner_agent_name);
|
|
617
|
+
const runnerAgents = mergeRunnerAgents(normalizeRunnerAgents(record.runnerAgents), normalizeRunnerAgents(record.runner_agents), runnerAgentIdRaw || runnerAgentNameRaw
|
|
618
|
+
? [
|
|
619
|
+
{
|
|
620
|
+
id: runnerAgentIdRaw ?? runnerAgentNameRaw ?? "Unassigned",
|
|
621
|
+
name: runnerAgentNameRaw ?? runnerAgentIdRaw ?? "Unassigned",
|
|
622
|
+
},
|
|
623
|
+
]
|
|
624
|
+
: []);
|
|
625
|
+
const runnerSourceHint = normalizeRunnerSource(record.runnerSource) ?? normalizeRunnerSource(record.runner_source);
|
|
626
|
+
const runnerSource = runnerAgents.length > 0 ? runnerSourceHint ?? "inferred" : "fallback";
|
|
627
|
+
const suggestedScope = asString(dispatch.suggestedScope) ??
|
|
628
|
+
asString(dispatch.suggested_scope) ??
|
|
629
|
+
asString(record.level);
|
|
630
|
+
const sliceScope = suggestedScope === "task" ||
|
|
631
|
+
suggestedScope === "milestone" ||
|
|
632
|
+
suggestedScope === "workstream"
|
|
633
|
+
? suggestedScope
|
|
634
|
+
: null;
|
|
635
|
+
const order = asRecord(record.order) ?? {};
|
|
636
|
+
const manualRank = asNumber(order.manualRank ?? order.manual_rank);
|
|
637
|
+
const iwmt = asRecord(record.iwmt);
|
|
638
|
+
const objective = asRecord(record.objective);
|
|
639
|
+
queueLike.push({
|
|
640
|
+
initiativeId,
|
|
641
|
+
initiativeTitle: asString(record.initiativeTitle) ??
|
|
642
|
+
asString(record.initiative_title) ??
|
|
643
|
+
initiativeId,
|
|
644
|
+
initiativeStatus: asString(record.initiativeStatus) ??
|
|
645
|
+
asString(record.initiative_status) ??
|
|
646
|
+
"active",
|
|
647
|
+
initiativePriority: asString(record.initiativePriority) ?? asString(record.initiative_priority),
|
|
648
|
+
initiativePriorityNum: asNumber(record.initiativePriorityNum ?? record.initiative_priority_num),
|
|
649
|
+
workstreamId,
|
|
650
|
+
workstreamTitle: asString(record.workstreamTitle) ??
|
|
651
|
+
asString(record.workstream_title) ??
|
|
652
|
+
asString(record.title) ??
|
|
653
|
+
workstreamId,
|
|
654
|
+
workstreamStatus: asString(record.workstreamStatus) ??
|
|
655
|
+
asString(record.workstream_status) ??
|
|
656
|
+
rawStatus,
|
|
657
|
+
nextTaskId: taskId ?? sliceTaskIds[0] ?? null,
|
|
658
|
+
nextTaskTitle: asString(record.nextTaskTitle) ?? asString(record.next_task_title),
|
|
659
|
+
nextTaskPriority: asNumber(record.priorityNum ?? record.nextTaskPriority),
|
|
660
|
+
nextTaskDueAt: asString(record.dueAt) ?? asString(record.nextTaskDueAt),
|
|
661
|
+
nextTaskMilestoneId: asString(record.milestoneId) ?? asString(record.milestone_id),
|
|
662
|
+
runnerAgentId: runnerAgentIdRaw,
|
|
663
|
+
runnerAgentName: runnerAgentNameRaw,
|
|
664
|
+
runnerAgents,
|
|
665
|
+
runnerSource,
|
|
666
|
+
queueState,
|
|
667
|
+
blockReason: asString(dispatch.blockReason) ??
|
|
668
|
+
asString(dispatch.block_reason) ??
|
|
669
|
+
null,
|
|
670
|
+
sliceScope,
|
|
671
|
+
sliceTaskIds,
|
|
672
|
+
sliceTaskCount: sliceTaskIds.length,
|
|
673
|
+
sliceMilestoneId: asString(record.milestoneId) ?? asString(record.milestone_id),
|
|
674
|
+
isPinned: typeof manualRank === "number",
|
|
675
|
+
pinnedRank: manualRank,
|
|
676
|
+
compositeScore: asNumber(iwmt?.mixScore ?? objective?.objectiveScore),
|
|
677
|
+
updatedAt: asString(record.updatedAt) ?? asString(record.updated_at) ?? null,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
return normalizeQueueItems(queueLike);
|
|
681
|
+
}
|
|
682
|
+
async function loadInitiativeGraphIndex(deps, initiativeId) {
|
|
683
|
+
const graphRaw = deps.applyLocalInitiativeOverrideToGraph(await deps.buildMissionControlGraph(initiativeId));
|
|
684
|
+
const graph = asRecord(graphRaw);
|
|
685
|
+
const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
|
|
686
|
+
const tasksById = new Map();
|
|
687
|
+
const milestoneTitleById = new Map();
|
|
688
|
+
for (const nodeEntry of nodes) {
|
|
689
|
+
const node = asRecord(nodeEntry);
|
|
690
|
+
if (!node)
|
|
691
|
+
continue;
|
|
692
|
+
const id = asString(node.id);
|
|
693
|
+
const type = asString(node.type);
|
|
694
|
+
if (!id || !type)
|
|
695
|
+
continue;
|
|
696
|
+
if (type === "milestone") {
|
|
697
|
+
milestoneTitleById.set(id, asString(node.title) ?? id);
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
if (type !== "task")
|
|
701
|
+
continue;
|
|
702
|
+
tasksById.set(id, {
|
|
703
|
+
id,
|
|
704
|
+
title: asString(node.title) ?? id,
|
|
705
|
+
status: asString(node.status),
|
|
706
|
+
milestoneId: asString(node.milestoneId),
|
|
707
|
+
workstreamId: asString(node.workstreamId),
|
|
708
|
+
priorityNum: asNumber(node.priorityNum),
|
|
709
|
+
dueDate: asString(node.dueDate),
|
|
710
|
+
updatedAt: asString(node.updatedAt),
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
tasksById,
|
|
715
|
+
milestoneTitleById,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
1
718
|
export function registerMissionControlReadRoutes(router, deps) {
|
|
719
|
+
// Handler registrations are process-local. Reset route caches so each newly
|
|
720
|
+
// constructed handler starts from a clean canonical cache/bypass state.
|
|
721
|
+
warmupByKey.clear();
|
|
722
|
+
canonicalReadCache.clear();
|
|
723
|
+
canonicalBypassState.clear();
|
|
724
|
+
const sendRouteError = (res, status, location, error, extra = {}) => {
|
|
725
|
+
deps.sendJson(res, status, {
|
|
726
|
+
ok: false,
|
|
727
|
+
error,
|
|
728
|
+
error_location: location,
|
|
729
|
+
...extra,
|
|
730
|
+
});
|
|
731
|
+
};
|
|
732
|
+
const sendRouteException = (res, location, err) => {
|
|
733
|
+
sendRouteError(res, 500, location, deps.safeErrorMessage(err));
|
|
734
|
+
};
|
|
2
735
|
async function renderAutoContinueStatus(query, res) {
|
|
3
736
|
const initiativeId = query.get("initiative_id") ?? query.get("initiativeId") ?? "";
|
|
4
737
|
const id = initiativeId.trim();
|
|
5
738
|
if (!id) {
|
|
6
|
-
|
|
7
|
-
ok: false,
|
|
8
|
-
error: "Query parameter 'initiative_id' is required.",
|
|
9
|
-
});
|
|
739
|
+
sendRouteError(res, 400, "mission-control.read.auto-continue.status.validation", "Query parameter 'initiative_id' is required.");
|
|
10
740
|
return;
|
|
11
741
|
}
|
|
12
742
|
const run = deps.autoContinueRuns.get(id) ?? null;
|
|
@@ -16,6 +746,9 @@ export function registerMissionControlReadRoutes(router, deps) {
|
|
|
16
746
|
run,
|
|
17
747
|
defaults: {
|
|
18
748
|
tokenBudget: deps.defaultAutoContinueTokenBudget(),
|
|
749
|
+
maxParallelSlices: typeof deps.defaultAutoContinueMaxParallelSlices === "function"
|
|
750
|
+
? deps.defaultAutoContinueMaxParallelSlices()
|
|
751
|
+
: 1,
|
|
19
752
|
tickMs: deps.autoContinueTickMs,
|
|
20
753
|
},
|
|
21
754
|
});
|
|
@@ -23,9 +756,7 @@ export function registerMissionControlReadRoutes(router, deps) {
|
|
|
23
756
|
async function renderMissionControlGraph(query, res) {
|
|
24
757
|
const initiativeId = query.get("initiative_id") ?? query.get("initiativeId");
|
|
25
758
|
if (!initiativeId || initiativeId.trim().length === 0) {
|
|
26
|
-
|
|
27
|
-
error: "Query parameter 'initiative_id' is required.",
|
|
28
|
-
});
|
|
759
|
+
sendRouteError(res, 400, "mission-control.read.graph.validation", "Query parameter 'initiative_id' is required.");
|
|
29
760
|
return;
|
|
30
761
|
}
|
|
31
762
|
try {
|
|
@@ -33,35 +764,877 @@ export function registerMissionControlReadRoutes(router, deps) {
|
|
|
33
764
|
deps.sendJson(res, 200, graph);
|
|
34
765
|
}
|
|
35
766
|
catch (err) {
|
|
36
|
-
|
|
37
|
-
error: deps.safeErrorMessage(err),
|
|
38
|
-
});
|
|
767
|
+
sendRouteException(res, "mission-control.read.graph.handler", err);
|
|
39
768
|
}
|
|
40
769
|
}
|
|
41
|
-
async function renderNextUpQueue(query, res) {
|
|
770
|
+
async function renderNextUpQueue(query, res, headerScope) {
|
|
42
771
|
const initiativeIdRaw = query.get("initiative_id") ?? query.get("initiativeId") ?? "";
|
|
43
772
|
const initiativeId = initiativeIdRaw.trim() || null;
|
|
773
|
+
const scope = resolveWorkspaceScope(query, headerScope, {
|
|
774
|
+
allowProjectScope: false,
|
|
775
|
+
});
|
|
776
|
+
if (scope.error) {
|
|
777
|
+
sendRouteError(res, 400, "mission-control.read.next-up.validation", scope.error);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const projectId = scope.workspaceId;
|
|
781
|
+
const useAllScope = scope.isAll === true;
|
|
782
|
+
const includeCompleted = parseBoolean(query.get("include_completed"));
|
|
783
|
+
const offset = parsePositiveInt(query.get("cursor") ?? query.get("offset"), 0, 100_000);
|
|
784
|
+
const pageSize = parsePositiveInt(query.get("page_size") ?? query.get("pageSize") ?? query.get("limit"), NEXT_UP_DEFAULT_PAGE_SIZE, 300);
|
|
785
|
+
const requestedSliceLevelContext = query.get("slice_level_context") ?? query.get("sliceLevelContext");
|
|
786
|
+
const requestedMixPolicy = query.get("mix_policy") ?? query.get("mixPolicy");
|
|
787
|
+
const requestedOrderMode = query.get("order_mode") ?? query.get("orderMode");
|
|
788
|
+
const includeLineage = parseBoolean(query.get("include_lineage") ?? query.get("includeLineage"));
|
|
789
|
+
// Noise reduction params — suppress blocked/idle queue items by severity.
|
|
790
|
+
// noise_threshold: 'low' (show all) | 'medium' (default, hide low-severity blocked) | 'high' (only critical/high blocked)
|
|
791
|
+
const noiseThresholdRaw = query.get("noise_threshold") ?? query.get("noiseThreshold");
|
|
792
|
+
const noiseThreshold = noiseThresholdRaw === "low" || noiseThresholdRaw === "high"
|
|
793
|
+
? noiseThresholdRaw
|
|
794
|
+
: "medium";
|
|
795
|
+
// dedup_window: time window in ms for grouping duplicate blocked items (default: 60000)
|
|
796
|
+
const dedupWindowRaw = query.get("dedup_window") ?? query.get("dedupWindow");
|
|
797
|
+
const dedupWindowMs = dedupWindowRaw != null
|
|
798
|
+
? Math.max(0, parseInt(dedupWindowRaw, 10) || 60000)
|
|
799
|
+
: 60000;
|
|
800
|
+
// TODO: wire noiseThreshold + dedupWindowMs into triage query once server-side filtering lands
|
|
801
|
+
void noiseThreshold;
|
|
802
|
+
void dedupWindowMs;
|
|
803
|
+
const nextUpCanonicalCacheKey = canonicalReadCacheKey({
|
|
804
|
+
route: "next-up",
|
|
805
|
+
workspaceId: projectId,
|
|
806
|
+
scopeMode: useAllScope ? "all" : projectId ? "scoped" : "implicit",
|
|
807
|
+
initiativeId,
|
|
808
|
+
includeCompleted,
|
|
809
|
+
offset,
|
|
810
|
+
limit: pageSize,
|
|
811
|
+
scope: requestedSliceLevelContext,
|
|
812
|
+
order: requestedOrderMode,
|
|
813
|
+
mixPolicy: requestedMixPolicy,
|
|
814
|
+
search: includeLineage ? "lineage:1" : null,
|
|
815
|
+
});
|
|
816
|
+
const cachedCanonicalNextUp = readCanonicalReadCache(nextUpCanonicalCacheKey, {
|
|
817
|
+
allowStale: false,
|
|
818
|
+
});
|
|
819
|
+
if (cachedCanonicalNextUp) {
|
|
820
|
+
deps.sendJson(res, 200, cachedCanonicalNextUp);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const canonicalBypass = readCanonicalBypass("next-up");
|
|
824
|
+
const staleCanonicalForBypass = canonicalBypass
|
|
825
|
+
? readCanonicalReadCache(nextUpCanonicalCacheKey, { allowStale: true })
|
|
826
|
+
: null;
|
|
827
|
+
const bypassAllScopeUnsupported = Boolean(useAllScope &&
|
|
828
|
+
canonicalBypass &&
|
|
829
|
+
canonicalBypass.reason.toLowerCase().includes("all-workspaces"));
|
|
830
|
+
const honorCanonicalBypass = Boolean(canonicalBypass && (staleCanonicalForBypass || bypassAllScopeUnsupported));
|
|
831
|
+
let canonicalFallbackReason = honorCanonicalBypass
|
|
832
|
+
? `canonical next-up bypassed (${canonicalBypass?.reason ?? "unavailable"})`
|
|
833
|
+
: null;
|
|
834
|
+
if (honorCanonicalBypass && staleCanonicalForBypass) {
|
|
835
|
+
const staleDegraded = dedupeStrings([
|
|
836
|
+
...asStringArray(staleCanonicalForBypass.degraded),
|
|
837
|
+
"Using cached canonical queue while sync recovers.",
|
|
838
|
+
]);
|
|
839
|
+
deps.sendJson(res, 200, {
|
|
840
|
+
...staleCanonicalForBypass,
|
|
841
|
+
degraded: staleDegraded,
|
|
842
|
+
source: "canonical_cache_stale",
|
|
843
|
+
});
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (deps.rawRequest && !honorCanonicalBypass) {
|
|
847
|
+
try {
|
|
848
|
+
const params = new URLSearchParams();
|
|
849
|
+
if (initiativeId)
|
|
850
|
+
params.set("initiative_id", initiativeId);
|
|
851
|
+
if (projectId) {
|
|
852
|
+
params.set("workspace_id", projectId);
|
|
853
|
+
params.set("command_center_id", projectId);
|
|
854
|
+
}
|
|
855
|
+
else if (useAllScope) {
|
|
856
|
+
params.set("workspace_id", "all");
|
|
857
|
+
params.set("command_center_id", "all");
|
|
858
|
+
}
|
|
859
|
+
params.set("offset", String(offset));
|
|
860
|
+
params.set("limit", String(pageSize));
|
|
861
|
+
params.set("include_completed", includeCompleted ? "1" : "0");
|
|
862
|
+
if (requestedSliceLevelContext) {
|
|
863
|
+
params.set("slice_level_context", requestedSliceLevelContext);
|
|
864
|
+
}
|
|
865
|
+
if (requestedMixPolicy)
|
|
866
|
+
params.set("mix_policy", requestedMixPolicy);
|
|
867
|
+
if (requestedOrderMode)
|
|
868
|
+
params.set("order_mode", requestedOrderMode);
|
|
869
|
+
if (includeLineage)
|
|
870
|
+
params.set("include_lineage", "1");
|
|
871
|
+
const canonical = await requestCanonicalWithLegacyFallback(deps, {
|
|
872
|
+
timeoutMs: CANONICAL_NEXT_UP_TIMEOUT_MS,
|
|
873
|
+
label: "canonical next-up",
|
|
874
|
+
modernPath: `/api/client/mission-control/next-up?${params.toString()}`,
|
|
875
|
+
legacyPath: `/api/mission-control/next-up?${params.toString()}`,
|
|
876
|
+
});
|
|
877
|
+
const canonicalRecord = asRecord(canonical);
|
|
878
|
+
if (!canonicalRecord || !Array.isArray(canonicalRecord.items)) {
|
|
879
|
+
throw new Error("invalid canonical next-up payload");
|
|
880
|
+
}
|
|
881
|
+
if (isCanonicalAllScopeMismatch(canonicalRecord, useAllScope)) {
|
|
882
|
+
throw new Error("canonical next-up all-workspaces scope mismatch");
|
|
883
|
+
}
|
|
884
|
+
const canonicalItems = normalizeQueueItems(canonicalRecord.items).filter((item) => includeCompleted ? true : item.queueState !== "completed");
|
|
885
|
+
const canonicalTotal = Math.max(canonicalItems.length, Math.floor(asNumber(canonicalRecord.total) ?? canonicalItems.length)) ?? canonicalItems.length;
|
|
886
|
+
const canonicalPagination = parsePaginationEnvelope(canonicalRecord.pagination, {
|
|
887
|
+
offset,
|
|
888
|
+
limit: pageSize,
|
|
889
|
+
total: canonicalTotal,
|
|
890
|
+
});
|
|
891
|
+
const shouldRepaginateCanonically = canonicalItems.length > pageSize ||
|
|
892
|
+
canonicalPagination.offset !== offset ||
|
|
893
|
+
canonicalPagination.limit !== pageSize;
|
|
894
|
+
const paged = shouldRepaginateCanonically
|
|
895
|
+
? applySliceSearchAndPagination({
|
|
896
|
+
items: canonicalItems,
|
|
897
|
+
searchTerm: "",
|
|
898
|
+
offset,
|
|
899
|
+
limit: pageSize,
|
|
900
|
+
})
|
|
901
|
+
: null;
|
|
902
|
+
const degraded = dedupeStrings(asStringArray(canonicalRecord.degraded));
|
|
903
|
+
const responsePayload = {
|
|
904
|
+
ok: true,
|
|
905
|
+
generatedAt: asString(canonicalRecord.generatedAt) ?? new Date().toISOString(),
|
|
906
|
+
total: paged ? paged.filtered.length : canonicalPagination.total,
|
|
907
|
+
items: paged ? paged.paged : canonicalItems,
|
|
908
|
+
pagination: paged ? paged.pagination : canonicalPagination,
|
|
909
|
+
source: "canonical",
|
|
910
|
+
degraded,
|
|
911
|
+
};
|
|
912
|
+
writeCanonicalReadCache(nextUpCanonicalCacheKey, responsePayload);
|
|
913
|
+
deps.sendJson(res, 200, responsePayload);
|
|
914
|
+
const paginationForWarmup = paged ? paged.pagination : canonicalPagination;
|
|
915
|
+
if (paginationForWarmup.hasMore && paginationForWarmup.nextCursor && shouldRunWarmup(`next-up:${projectId ?? "__all__"}:${initiativeId ?? "__all__"}:${paginationForWarmup.nextCursor}:${pageSize}`)) {
|
|
916
|
+
const nextOffset = parsePositiveInt(paginationForWarmup.nextCursor, offset + pageSize, 100_000);
|
|
917
|
+
const warmParams = new URLSearchParams(params);
|
|
918
|
+
warmParams.set("offset", String(nextOffset));
|
|
919
|
+
warmParams.set("limit", String(pageSize));
|
|
920
|
+
void requestCanonicalWithLegacyFallback(deps, {
|
|
921
|
+
timeoutMs: CANONICAL_NEXT_UP_TIMEOUT_MS,
|
|
922
|
+
label: "canonical next-up warmup",
|
|
923
|
+
modernPath: `/api/client/mission-control/next-up?${warmParams.toString()}`,
|
|
924
|
+
legacyPath: `/api/mission-control/next-up?${warmParams.toString()}`,
|
|
925
|
+
}).catch(() => undefined);
|
|
926
|
+
}
|
|
927
|
+
if (offset === 0 &&
|
|
928
|
+
shouldRunWarmup(`next-up->slices:${projectId ?? "__all__"}:${initiativeId ?? "__all__"}:${pageSize}`)) {
|
|
929
|
+
const warmSlicesParams = new URLSearchParams();
|
|
930
|
+
if (initiativeId)
|
|
931
|
+
warmSlicesParams.set("initiative_id", initiativeId);
|
|
932
|
+
if (projectId) {
|
|
933
|
+
warmSlicesParams.set("workspace_id", projectId);
|
|
934
|
+
warmSlicesParams.set("command_center_id", projectId);
|
|
935
|
+
}
|
|
936
|
+
else if (useAllScope) {
|
|
937
|
+
warmSlicesParams.set("workspace_id", "all");
|
|
938
|
+
warmSlicesParams.set("command_center_id", "all");
|
|
939
|
+
}
|
|
940
|
+
warmSlicesParams.set("level", "initiative");
|
|
941
|
+
warmSlicesParams.set("include_completed", includeCompleted ? "1" : "0");
|
|
942
|
+
warmSlicesParams.set("offset", "0");
|
|
943
|
+
warmSlicesParams.set("limit", String(Math.max(SLICES_DEFAULT_PAGE_SIZE, Math.min(pageSize, 300))));
|
|
944
|
+
void requestCanonicalWithLegacyFallback(deps, {
|
|
945
|
+
timeoutMs: CANONICAL_SLICES_TIMEOUT_MS,
|
|
946
|
+
label: "canonical slices warmup",
|
|
947
|
+
modernPath: `/api/client/mission-control/slices?${warmSlicesParams.toString()}`,
|
|
948
|
+
legacyPath: `/api/mission-control/slices?${warmSlicesParams.toString()}`,
|
|
949
|
+
}).catch(() => undefined);
|
|
950
|
+
}
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
catch (err) {
|
|
954
|
+
if (isCanonicalAuthFailure(err)) {
|
|
955
|
+
setCanonicalBypass("next-up", "authentication unavailable", CANONICAL_AUTH_BYPASS_MS);
|
|
956
|
+
}
|
|
957
|
+
else if (isCanonicalAllScopeMismatchError(err)) {
|
|
958
|
+
setCanonicalBypass("next-up", "all-workspaces unsupported", Math.max(CANONICAL_AUTH_BYPASS_MS, 60_000));
|
|
959
|
+
}
|
|
960
|
+
const staleCanonical = readCanonicalReadCache(nextUpCanonicalCacheKey, {
|
|
961
|
+
allowStale: true,
|
|
962
|
+
});
|
|
963
|
+
if (staleCanonical) {
|
|
964
|
+
const staleDegraded = dedupeStrings([
|
|
965
|
+
...asStringArray(staleCanonical.degraded),
|
|
966
|
+
"Using cached canonical queue while sync recovers.",
|
|
967
|
+
]);
|
|
968
|
+
deps.sendJson(res, 200, {
|
|
969
|
+
...staleCanonical,
|
|
970
|
+
degraded: staleDegraded,
|
|
971
|
+
source: "canonical_cache_stale",
|
|
972
|
+
});
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
canonicalFallbackReason = `canonical next-up unavailable (${deps.safeErrorMessage(err)})`;
|
|
976
|
+
if (projectId || useAllScope) {
|
|
977
|
+
try {
|
|
978
|
+
const bridgeParams = new URLSearchParams();
|
|
979
|
+
if (initiativeId)
|
|
980
|
+
bridgeParams.set("initiative_id", initiativeId);
|
|
981
|
+
if (projectId) {
|
|
982
|
+
bridgeParams.set("workspace_id", projectId);
|
|
983
|
+
bridgeParams.set("command_center_id", projectId);
|
|
984
|
+
}
|
|
985
|
+
else if (useAllScope) {
|
|
986
|
+
bridgeParams.set("workspace_id", "all");
|
|
987
|
+
bridgeParams.set("command_center_id", "all");
|
|
988
|
+
}
|
|
989
|
+
bridgeParams.set("level", "workstream");
|
|
990
|
+
bridgeParams.set("offset", String(Math.max(0, offset)));
|
|
991
|
+
bridgeParams.set("limit", String(Math.min(300, Math.max(pageSize, offset + pageSize))));
|
|
992
|
+
bridgeParams.set("include_completed", includeCompleted ? "1" : "0");
|
|
993
|
+
bridgeParams.set("mix_policy", requestedMixPolicy ?? "iwmt_v1");
|
|
994
|
+
if (requestedOrderMode) {
|
|
995
|
+
bridgeParams.set("order_mode", requestedOrderMode);
|
|
996
|
+
}
|
|
997
|
+
const canonicalSlices = await requestCanonicalWithLegacyFallback(deps, {
|
|
998
|
+
timeoutMs: CANONICAL_SLICES_TIMEOUT_MS,
|
|
999
|
+
label: "canonical slices bridge",
|
|
1000
|
+
modernPath: `/api/client/mission-control/slices?${bridgeParams.toString()}`,
|
|
1001
|
+
legacyPath: `/api/mission-control/slices?${bridgeParams.toString()}`,
|
|
1002
|
+
});
|
|
1003
|
+
const canonicalSlicesRecord = asRecord(canonicalSlices);
|
|
1004
|
+
if (!canonicalSlicesRecord || !Array.isArray(canonicalSlicesRecord.items)) {
|
|
1005
|
+
throw new Error("invalid canonical slices payload");
|
|
1006
|
+
}
|
|
1007
|
+
if (isCanonicalAllScopeMismatch(canonicalSlicesRecord, useAllScope)) {
|
|
1008
|
+
throw new Error("canonical slices all-workspaces scope mismatch");
|
|
1009
|
+
}
|
|
1010
|
+
const bridgedItems = mapCanonicalSlicesToQueueItems(canonicalSlicesRecord.items).filter((item) => includeCompleted ? true : item.queueState !== "completed");
|
|
1011
|
+
if (bridgedItems.length > 0) {
|
|
1012
|
+
const paged = applySliceSearchAndPagination({
|
|
1013
|
+
items: bridgedItems,
|
|
1014
|
+
searchTerm: "",
|
|
1015
|
+
offset,
|
|
1016
|
+
limit: pageSize,
|
|
1017
|
+
});
|
|
1018
|
+
const degraded = dedupeStrings([
|
|
1019
|
+
...(Array.isArray(canonicalSlicesRecord.degraded)
|
|
1020
|
+
? canonicalSlicesRecord.degraded
|
|
1021
|
+
: []),
|
|
1022
|
+
...(canonicalFallbackReason ? [canonicalFallbackReason] : []),
|
|
1023
|
+
"Next Up derived from canonical slices.",
|
|
1024
|
+
]);
|
|
1025
|
+
const responsePayload = {
|
|
1026
|
+
ok: true,
|
|
1027
|
+
generatedAt: asString(canonicalSlicesRecord.generatedAt) ??
|
|
1028
|
+
new Date().toISOString(),
|
|
1029
|
+
total: paged.filtered.length,
|
|
1030
|
+
items: paged.paged,
|
|
1031
|
+
pagination: paged.pagination,
|
|
1032
|
+
source: "canonical_slices_bridge",
|
|
1033
|
+
degraded,
|
|
1034
|
+
};
|
|
1035
|
+
writeCanonicalReadCache(nextUpCanonicalCacheKey, responsePayload);
|
|
1036
|
+
deps.sendJson(res, 200, responsePayload);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
catch (bridgeErr) {
|
|
1041
|
+
canonicalFallbackReason = dedupeStrings([
|
|
1042
|
+
canonicalFallbackReason ?? "",
|
|
1043
|
+
`canonical slices bridge unavailable (${deps.safeErrorMessage(bridgeErr)})`,
|
|
1044
|
+
]).join(" | ");
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
// Continue to local fallback.
|
|
1048
|
+
try {
|
|
1049
|
+
const queue = await deps.buildNextUpQueue({
|
|
1050
|
+
initiativeId,
|
|
1051
|
+
projectId,
|
|
1052
|
+
});
|
|
1053
|
+
const items = normalizeQueueItems(queue.items ?? []).filter((item) => includeCompleted ? true : item.queueState !== "completed");
|
|
1054
|
+
const paged = applySliceSearchAndPagination({
|
|
1055
|
+
items,
|
|
1056
|
+
searchTerm: "",
|
|
1057
|
+
offset,
|
|
1058
|
+
limit: pageSize,
|
|
1059
|
+
});
|
|
1060
|
+
const degraded = dedupeStrings([
|
|
1061
|
+
...(Array.isArray(queue.degraded) ? queue.degraded : []),
|
|
1062
|
+
...(canonicalFallbackReason ? [canonicalFallbackReason] : []),
|
|
1063
|
+
]);
|
|
1064
|
+
deps.sendJson(res, 200, {
|
|
1065
|
+
ok: true,
|
|
1066
|
+
generatedAt: new Date().toISOString(),
|
|
1067
|
+
total: paged.filtered.length,
|
|
1068
|
+
items: paged.paged,
|
|
1069
|
+
pagination: paged.pagination,
|
|
1070
|
+
source: "local_fallback",
|
|
1071
|
+
degraded,
|
|
1072
|
+
});
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
catch (fallbackErr) {
|
|
1076
|
+
sendRouteException(res, "mission-control.read.next-up.handler", fallbackErr);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
44
1081
|
try {
|
|
45
|
-
const queue = await deps.buildNextUpQueue({
|
|
1082
|
+
const queue = await deps.buildNextUpQueue({
|
|
1083
|
+
initiativeId,
|
|
1084
|
+
projectId,
|
|
1085
|
+
});
|
|
1086
|
+
const items = normalizeQueueItems(queue.items ?? []).filter((item) => includeCompleted ? true : item.queueState !== "completed");
|
|
1087
|
+
const paged = applySliceSearchAndPagination({
|
|
1088
|
+
items,
|
|
1089
|
+
searchTerm: "",
|
|
1090
|
+
offset,
|
|
1091
|
+
limit: pageSize,
|
|
1092
|
+
});
|
|
1093
|
+
const degraded = dedupeStrings([
|
|
1094
|
+
...(Array.isArray(queue.degraded) ? queue.degraded : []),
|
|
1095
|
+
...(canonicalFallbackReason ? [canonicalFallbackReason] : []),
|
|
1096
|
+
]);
|
|
46
1097
|
deps.sendJson(res, 200, {
|
|
47
1098
|
ok: true,
|
|
48
1099
|
generatedAt: new Date().toISOString(),
|
|
49
|
-
total:
|
|
50
|
-
items:
|
|
51
|
-
|
|
1100
|
+
total: paged.filtered.length,
|
|
1101
|
+
items: paged.paged,
|
|
1102
|
+
pagination: paged.pagination,
|
|
1103
|
+
source: "local",
|
|
1104
|
+
degraded: dedupeStrings(degraded),
|
|
52
1105
|
});
|
|
53
1106
|
}
|
|
54
1107
|
catch (err) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
1108
|
+
sendRouteException(res, "mission-control.read.next-up.handler", err);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
async function renderSliceProjection(query, res, headerScope) {
|
|
1112
|
+
const initiativeIdRaw = query.get("initiative_id") ?? query.get("initiativeId") ?? "";
|
|
1113
|
+
const initiativeId = initiativeIdRaw.trim() || null;
|
|
1114
|
+
const workspaceScope = resolveWorkspaceScope(query, headerScope, {
|
|
1115
|
+
allowProjectScope: false,
|
|
1116
|
+
});
|
|
1117
|
+
if (workspaceScope.error) {
|
|
1118
|
+
sendRouteError(res, 400, "mission-control.read.slices.validation", workspaceScope.error);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
const projectId = workspaceScope.workspaceId;
|
|
1122
|
+
const useAllScope = workspaceScope.isAll === true;
|
|
1123
|
+
const includeCompleted = parseBoolean(query.get("include_completed"));
|
|
1124
|
+
const sliceScope = parseSliceScope(query.get("scope") ?? query.get("level"));
|
|
1125
|
+
const order = parseSliceOrder(query.get("order"));
|
|
1126
|
+
const searchTerm = normalizeSliceSearchTerm(query.get("q") ?? query.get("search"));
|
|
1127
|
+
const offset = parsePositiveInt(query.get("cursor") ?? query.get("offset"), 0, 100_000);
|
|
1128
|
+
const pageSize = parsePositiveInt(query.get("page_size") ?? query.get("pageSize") ?? query.get("limit"), SLICES_DEFAULT_PAGE_SIZE, 300);
|
|
1129
|
+
const requestedMixPolicy = query.get("mix_policy") ?? query.get("mixPolicy") ?? "iwmt_v1";
|
|
1130
|
+
const requestedOrderMode = query.get("order_mode") ?? query.get("orderMode");
|
|
1131
|
+
const slicesCanonicalCacheKey = canonicalReadCacheKey({
|
|
1132
|
+
route: "slices",
|
|
1133
|
+
workspaceId: projectId,
|
|
1134
|
+
scopeMode: useAllScope ? "all" : projectId ? "scoped" : "implicit",
|
|
1135
|
+
initiativeId,
|
|
1136
|
+
includeCompleted,
|
|
1137
|
+
offset,
|
|
1138
|
+
limit: pageSize,
|
|
1139
|
+
scope: sliceScope,
|
|
1140
|
+
order: requestedOrderMode ?? order,
|
|
1141
|
+
mixPolicy: requestedMixPolicy,
|
|
1142
|
+
search: searchTerm || null,
|
|
1143
|
+
});
|
|
1144
|
+
const cachedCanonicalSlices = readCanonicalReadCache(slicesCanonicalCacheKey, {
|
|
1145
|
+
allowStale: false,
|
|
1146
|
+
});
|
|
1147
|
+
if (cachedCanonicalSlices) {
|
|
1148
|
+
deps.sendJson(res, 200, cachedCanonicalSlices);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const canonicalBypass = readCanonicalBypass("slices");
|
|
1152
|
+
const staleCanonicalForBypass = canonicalBypass
|
|
1153
|
+
? readCanonicalReadCache(slicesCanonicalCacheKey, { allowStale: true })
|
|
1154
|
+
: null;
|
|
1155
|
+
const bypassAllScopeUnsupported = Boolean(useAllScope &&
|
|
1156
|
+
canonicalBypass &&
|
|
1157
|
+
canonicalBypass.reason.toLowerCase().includes("all-workspaces"));
|
|
1158
|
+
const honorCanonicalBypass = Boolean(canonicalBypass && (staleCanonicalForBypass || bypassAllScopeUnsupported));
|
|
1159
|
+
let canonicalFallbackReason = honorCanonicalBypass
|
|
1160
|
+
? `canonical slices bypassed (${canonicalBypass?.reason ?? "unavailable"})`
|
|
1161
|
+
: null;
|
|
1162
|
+
if (honorCanonicalBypass && staleCanonicalForBypass) {
|
|
1163
|
+
const staleDegraded = dedupeStrings([
|
|
1164
|
+
...asStringArray(staleCanonicalForBypass.degraded),
|
|
1165
|
+
"Using cached canonical slices while sync recovers.",
|
|
1166
|
+
]);
|
|
1167
|
+
deps.sendJson(res, 200, {
|
|
1168
|
+
...staleCanonicalForBypass,
|
|
1169
|
+
degraded: staleDegraded,
|
|
1170
|
+
source: "canonical_cache_stale",
|
|
1171
|
+
});
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if (deps.rawRequest && !honorCanonicalBypass) {
|
|
1175
|
+
try {
|
|
1176
|
+
const params = new URLSearchParams();
|
|
1177
|
+
if (initiativeId)
|
|
1178
|
+
params.set("initiative_id", initiativeId);
|
|
1179
|
+
if (projectId) {
|
|
1180
|
+
params.set("workspace_id", projectId);
|
|
1181
|
+
params.set("command_center_id", projectId);
|
|
1182
|
+
}
|
|
1183
|
+
else if (useAllScope) {
|
|
1184
|
+
params.set("workspace_id", "all");
|
|
1185
|
+
params.set("command_center_id", "all");
|
|
1186
|
+
}
|
|
1187
|
+
params.set("level", sliceScope);
|
|
1188
|
+
params.set("include_completed", includeCompleted ? "1" : "0");
|
|
1189
|
+
params.set("mix_policy", requestedMixPolicy);
|
|
1190
|
+
if (requestedOrderMode)
|
|
1191
|
+
params.set("order_mode", requestedOrderMode);
|
|
1192
|
+
const canonicalSupportsDirectPaging = searchTerm.length === 0;
|
|
1193
|
+
if (canonicalSupportsDirectPaging) {
|
|
1194
|
+
params.set("offset", String(offset));
|
|
1195
|
+
params.set("limit", String(pageSize));
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
params.set("offset", "0");
|
|
1199
|
+
params.set("limit", String(Math.min(300, Math.max(pageSize + offset, pageSize))));
|
|
1200
|
+
}
|
|
1201
|
+
const canonical = await requestCanonicalWithLegacyFallback(deps, {
|
|
1202
|
+
timeoutMs: CANONICAL_SLICES_TIMEOUT_MS,
|
|
1203
|
+
label: "canonical slices",
|
|
1204
|
+
modernPath: `/api/client/mission-control/slices?${params.toString()}`,
|
|
1205
|
+
legacyPath: `/api/mission-control/slices?${params.toString()}`,
|
|
1206
|
+
});
|
|
1207
|
+
const canonicalRecord = asRecord(canonical);
|
|
1208
|
+
if (!canonicalRecord || !Array.isArray(canonicalRecord.items)) {
|
|
1209
|
+
throw new Error("invalid canonical slices payload");
|
|
1210
|
+
}
|
|
1211
|
+
if (isCanonicalAllScopeMismatch(canonicalRecord, useAllScope)) {
|
|
1212
|
+
throw new Error("canonical slices all-workspaces scope mismatch");
|
|
1213
|
+
}
|
|
1214
|
+
const canonicalItems = canonicalRecord.items;
|
|
1215
|
+
const canonicalTotal = Math.max(canonicalItems.length, Math.floor(asNumber(canonicalRecord.total) ?? canonicalItems.length)) ?? canonicalItems.length;
|
|
1216
|
+
const canonicalPagination = parsePaginationEnvelope(canonicalRecord.pagination, {
|
|
1217
|
+
offset,
|
|
1218
|
+
limit: pageSize,
|
|
1219
|
+
total: canonicalTotal,
|
|
1220
|
+
});
|
|
1221
|
+
const shouldRepaginateCanonically = canonicalItems.length > pageSize ||
|
|
1222
|
+
canonicalPagination.offset !== offset ||
|
|
1223
|
+
canonicalPagination.limit !== pageSize;
|
|
1224
|
+
const canonicalPaged = shouldRepaginateCanonically
|
|
1225
|
+
? applySliceSearchAndPagination({
|
|
1226
|
+
items: canonicalItems,
|
|
1227
|
+
searchTerm: "",
|
|
1228
|
+
offset,
|
|
1229
|
+
limit: pageSize,
|
|
1230
|
+
})
|
|
1231
|
+
: null;
|
|
1232
|
+
if (searchTerm.length === 0) {
|
|
1233
|
+
const responsePayload = {
|
|
1234
|
+
...canonicalRecord,
|
|
1235
|
+
level: asString(canonicalRecord.level) ?? sliceScope,
|
|
1236
|
+
scope: asString(canonicalRecord.level) ?? sliceScope,
|
|
1237
|
+
order: asString(canonicalRecord.orderMode) ??
|
|
1238
|
+
asString(canonicalRecord.order) ??
|
|
1239
|
+
order,
|
|
1240
|
+
total: canonicalPaged ? canonicalPaged.filtered.length : canonicalPagination.total,
|
|
1241
|
+
items: canonicalPaged ? canonicalPaged.paged : canonicalItems,
|
|
1242
|
+
pagination: canonicalPaged ? canonicalPaged.pagination : canonicalPagination,
|
|
1243
|
+
source: "canonical",
|
|
1244
|
+
};
|
|
1245
|
+
writeCanonicalReadCache(slicesCanonicalCacheKey, responsePayload);
|
|
1246
|
+
deps.sendJson(res, 200, responsePayload);
|
|
1247
|
+
if (offset === 0 &&
|
|
1248
|
+
shouldRunWarmup(`slices->next-up:${projectId ?? "__all__"}:${initiativeId ?? "__all__"}:${pageSize}`)) {
|
|
1249
|
+
const warmNextUpParams = new URLSearchParams();
|
|
1250
|
+
if (initiativeId)
|
|
1251
|
+
warmNextUpParams.set("initiative_id", initiativeId);
|
|
1252
|
+
if (projectId) {
|
|
1253
|
+
warmNextUpParams.set("workspace_id", projectId);
|
|
1254
|
+
warmNextUpParams.set("command_center_id", projectId);
|
|
1255
|
+
}
|
|
1256
|
+
else if (useAllScope) {
|
|
1257
|
+
warmNextUpParams.set("workspace_id", "all");
|
|
1258
|
+
warmNextUpParams.set("command_center_id", "all");
|
|
1259
|
+
}
|
|
1260
|
+
warmNextUpParams.set("offset", "0");
|
|
1261
|
+
warmNextUpParams.set("limit", String(Math.max(NEXT_UP_DEFAULT_PAGE_SIZE, Math.min(pageSize, 300))));
|
|
1262
|
+
warmNextUpParams.set("include_completed", includeCompleted ? "1" : "0");
|
|
1263
|
+
void requestCanonicalWithLegacyFallback(deps, {
|
|
1264
|
+
timeoutMs: CANONICAL_NEXT_UP_TIMEOUT_MS,
|
|
1265
|
+
label: "canonical next-up warmup",
|
|
1266
|
+
modernPath: `/api/client/mission-control/next-up?${warmNextUpParams.toString()}`,
|
|
1267
|
+
legacyPath: `/api/mission-control/next-up?${warmNextUpParams.toString()}`,
|
|
1268
|
+
}).catch(() => undefined);
|
|
1269
|
+
}
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const paged = applySliceSearchAndPagination({
|
|
1273
|
+
items: canonicalItems,
|
|
1274
|
+
searchTerm,
|
|
1275
|
+
offset,
|
|
1276
|
+
limit: pageSize,
|
|
1277
|
+
});
|
|
1278
|
+
const responsePayload = {
|
|
1279
|
+
...canonicalRecord,
|
|
1280
|
+
level: asString(canonicalRecord.level) ?? sliceScope,
|
|
1281
|
+
scope: asString(canonicalRecord.level) ?? sliceScope,
|
|
1282
|
+
order: asString(canonicalRecord.orderMode) ??
|
|
1283
|
+
asString(canonicalRecord.order) ??
|
|
1284
|
+
order,
|
|
1285
|
+
total: paged.filtered.length,
|
|
1286
|
+
items: paged.paged,
|
|
1287
|
+
pagination: paged.pagination,
|
|
1288
|
+
source: "canonical",
|
|
1289
|
+
};
|
|
1290
|
+
writeCanonicalReadCache(slicesCanonicalCacheKey, responsePayload);
|
|
1291
|
+
deps.sendJson(res, 200, responsePayload);
|
|
1292
|
+
if (offset === 0 &&
|
|
1293
|
+
shouldRunWarmup(`slices->next-up:${projectId ?? "__all__"}:${initiativeId ?? "__all__"}:${pageSize}:search`)) {
|
|
1294
|
+
const warmNextUpParams = new URLSearchParams();
|
|
1295
|
+
if (initiativeId)
|
|
1296
|
+
warmNextUpParams.set("initiative_id", initiativeId);
|
|
1297
|
+
if (projectId) {
|
|
1298
|
+
warmNextUpParams.set("workspace_id", projectId);
|
|
1299
|
+
warmNextUpParams.set("command_center_id", projectId);
|
|
1300
|
+
}
|
|
1301
|
+
else if (useAllScope) {
|
|
1302
|
+
warmNextUpParams.set("workspace_id", "all");
|
|
1303
|
+
warmNextUpParams.set("command_center_id", "all");
|
|
1304
|
+
}
|
|
1305
|
+
warmNextUpParams.set("offset", "0");
|
|
1306
|
+
warmNextUpParams.set("limit", String(Math.max(NEXT_UP_DEFAULT_PAGE_SIZE, Math.min(pageSize, 300))));
|
|
1307
|
+
warmNextUpParams.set("include_completed", includeCompleted ? "1" : "0");
|
|
1308
|
+
void requestCanonicalWithLegacyFallback(deps, {
|
|
1309
|
+
timeoutMs: CANONICAL_NEXT_UP_TIMEOUT_MS,
|
|
1310
|
+
label: "canonical next-up warmup",
|
|
1311
|
+
modernPath: `/api/client/mission-control/next-up?${warmNextUpParams.toString()}`,
|
|
1312
|
+
legacyPath: `/api/mission-control/next-up?${warmNextUpParams.toString()}`,
|
|
1313
|
+
}).catch(() => undefined);
|
|
1314
|
+
}
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
catch (err) {
|
|
1318
|
+
if (isCanonicalAuthFailure(err)) {
|
|
1319
|
+
setCanonicalBypass("slices", "authentication unavailable", CANONICAL_AUTH_BYPASS_MS);
|
|
1320
|
+
}
|
|
1321
|
+
else if (isCanonicalAllScopeMismatchError(err)) {
|
|
1322
|
+
setCanonicalBypass("slices", "all-workspaces unsupported", Math.max(CANONICAL_AUTH_BYPASS_MS, 60_000));
|
|
1323
|
+
}
|
|
1324
|
+
const staleCanonical = readCanonicalReadCache(slicesCanonicalCacheKey, {
|
|
1325
|
+
allowStale: true,
|
|
1326
|
+
});
|
|
1327
|
+
if (staleCanonical) {
|
|
1328
|
+
const staleDegraded = dedupeStrings([
|
|
1329
|
+
...asStringArray(staleCanonical.degraded),
|
|
1330
|
+
"Using cached canonical slices while sync recovers.",
|
|
1331
|
+
]);
|
|
1332
|
+
deps.sendJson(res, 200, {
|
|
1333
|
+
...staleCanonical,
|
|
1334
|
+
degraded: staleDegraded,
|
|
1335
|
+
source: "canonical_cache_stale",
|
|
1336
|
+
});
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
canonicalFallbackReason = `canonical slices unavailable (${deps.safeErrorMessage(err)})`;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
const queue = await deps.buildNextUpQueue({
|
|
1344
|
+
initiativeId,
|
|
1345
|
+
projectId,
|
|
58
1346
|
});
|
|
1347
|
+
const queueItems = normalizeQueueItems(queue.items ?? []).filter((item) => includeCompleted ? true : item.queueState !== "completed");
|
|
1348
|
+
const graphIndexByInitiative = new Map();
|
|
1349
|
+
const degraded = dedupeStrings([
|
|
1350
|
+
...(Array.isArray(queue.degraded) ? queue.degraded : []),
|
|
1351
|
+
...(canonicalFallbackReason ? [canonicalFallbackReason] : []),
|
|
1352
|
+
]);
|
|
1353
|
+
if (sliceScope === "milestone" || sliceScope === "task") {
|
|
1354
|
+
const uniqueInitiatives = dedupeStrings(queueItems.map((item) => item.initiativeId));
|
|
1355
|
+
for (const id of uniqueInitiatives) {
|
|
1356
|
+
try {
|
|
1357
|
+
graphIndexByInitiative.set(id, await loadInitiativeGraphIndex(deps, id));
|
|
1358
|
+
}
|
|
1359
|
+
catch (err) {
|
|
1360
|
+
degraded.push(`graph unavailable for ${id} (${deps.safeErrorMessage(err)})`);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
const slices = [];
|
|
1365
|
+
if (sliceScope === "initiative") {
|
|
1366
|
+
const grouped = new Map();
|
|
1367
|
+
queueItems.forEach((item, index) => {
|
|
1368
|
+
const bucket = grouped.get(item.initiativeId);
|
|
1369
|
+
if (!bucket) {
|
|
1370
|
+
grouped.set(item.initiativeId, {
|
|
1371
|
+
base: item,
|
|
1372
|
+
states: [item.queueState],
|
|
1373
|
+
taskIds: new Set(item.sliceTaskIds ?? []),
|
|
1374
|
+
workstreamIds: new Set([item.workstreamId]),
|
|
1375
|
+
runnerAgents: item.runnerAgents ?? [],
|
|
1376
|
+
iwmtRank: index,
|
|
1377
|
+
});
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
bucket.states.push(item.queueState);
|
|
1381
|
+
for (const taskId of item.sliceTaskIds ?? [])
|
|
1382
|
+
bucket.taskIds.add(taskId);
|
|
1383
|
+
bucket.workstreamIds.add(item.workstreamId);
|
|
1384
|
+
bucket.runnerAgents = mergeRunnerAgents(bucket.runnerAgents, item.runnerAgents ?? []);
|
|
1385
|
+
if (index < bucket.iwmtRank) {
|
|
1386
|
+
bucket.base = item;
|
|
1387
|
+
bucket.iwmtRank = index;
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
for (const [initiativeKey, bucket] of grouped.entries()) {
|
|
1391
|
+
const runnerAgents = mergeRunnerAgents(bucket.runnerAgents, bucket.base.runnerAgents ?? []);
|
|
1392
|
+
const runnerPrimary = runnerAgents[0] ?? null;
|
|
1393
|
+
slices.push({
|
|
1394
|
+
id: initiativeKey,
|
|
1395
|
+
scope: sliceScope,
|
|
1396
|
+
initiativeId: bucket.base.initiativeId,
|
|
1397
|
+
initiativeTitle: bucket.base.initiativeTitle,
|
|
1398
|
+
workstreamId: null,
|
|
1399
|
+
workstreamTitle: null,
|
|
1400
|
+
milestoneId: null,
|
|
1401
|
+
milestoneTitle: null,
|
|
1402
|
+
taskId: null,
|
|
1403
|
+
taskTitle: null,
|
|
1404
|
+
queueState: combinedQueueState(bucket.states),
|
|
1405
|
+
sourceWorkstreamIds: Array.from(bucket.workstreamIds.values()),
|
|
1406
|
+
runnerAgentId: runnerPrimary?.id ?? null,
|
|
1407
|
+
runnerAgentName: runnerPrimary?.name ?? "Unassigned",
|
|
1408
|
+
runnerAgents,
|
|
1409
|
+
runnerSource: bucket.base.runnerSource ??
|
|
1410
|
+
(runnerPrimary ? "inferred" : "fallback"),
|
|
1411
|
+
nextTaskId: bucket.base.nextTaskId,
|
|
1412
|
+
nextTaskTitle: bucket.base.nextTaskTitle,
|
|
1413
|
+
nextTaskPriority: bucket.base.nextTaskPriority,
|
|
1414
|
+
nextTaskDueAt: bucket.base.nextTaskDueAt,
|
|
1415
|
+
updatedAt: bucket.base.updatedAt ?? null,
|
|
1416
|
+
sliceTaskIds: Array.from(bucket.taskIds.values()),
|
|
1417
|
+
sliceTaskCount: bucket.taskIds.size,
|
|
1418
|
+
compositeScore: bucket.base.compositeScore,
|
|
1419
|
+
scoringTier: bucket.base.scoringTier,
|
|
1420
|
+
iwmtRank: bucket.iwmtRank,
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
else if (sliceScope === "workstream") {
|
|
1425
|
+
queueItems.forEach((item, index) => {
|
|
1426
|
+
const runnerAgents = mergeRunnerAgents(item.runnerAgents ?? []);
|
|
1427
|
+
const runnerPrimary = runnerAgents[0] ?? null;
|
|
1428
|
+
slices.push({
|
|
1429
|
+
id: `${item.initiativeId}:${item.workstreamId}`,
|
|
1430
|
+
scope: sliceScope,
|
|
1431
|
+
initiativeId: item.initiativeId,
|
|
1432
|
+
initiativeTitle: item.initiativeTitle,
|
|
1433
|
+
workstreamId: item.workstreamId,
|
|
1434
|
+
workstreamTitle: item.workstreamTitle,
|
|
1435
|
+
milestoneId: item.sliceMilestoneId ?? item.nextTaskMilestoneId ?? null,
|
|
1436
|
+
milestoneTitle: null,
|
|
1437
|
+
taskId: null,
|
|
1438
|
+
taskTitle: null,
|
|
1439
|
+
queueState: item.queueState,
|
|
1440
|
+
sourceWorkstreamIds: [item.workstreamId],
|
|
1441
|
+
runnerAgentId: runnerPrimary?.id ?? null,
|
|
1442
|
+
runnerAgentName: runnerPrimary?.name ?? "Unassigned",
|
|
1443
|
+
runnerAgents,
|
|
1444
|
+
runnerSource: item.runnerSource ?? (runnerPrimary ? "inferred" : "fallback"),
|
|
1445
|
+
nextTaskId: item.nextTaskId,
|
|
1446
|
+
nextTaskTitle: item.nextTaskTitle,
|
|
1447
|
+
nextTaskPriority: item.nextTaskPriority,
|
|
1448
|
+
nextTaskDueAt: item.nextTaskDueAt,
|
|
1449
|
+
updatedAt: item.updatedAt ?? null,
|
|
1450
|
+
sliceTaskIds: dedupeStrings(item.sliceTaskIds ?? []),
|
|
1451
|
+
sliceTaskCount: typeof item.sliceTaskCount === "number"
|
|
1452
|
+
? Math.max(0, Math.floor(item.sliceTaskCount))
|
|
1453
|
+
: (item.sliceTaskIds ?? []).length,
|
|
1454
|
+
compositeScore: item.compositeScore,
|
|
1455
|
+
scoringTier: item.scoringTier,
|
|
1456
|
+
iwmtRank: index,
|
|
1457
|
+
});
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
else if (sliceScope === "milestone") {
|
|
1461
|
+
const grouped = new Map();
|
|
1462
|
+
queueItems.forEach((item, index) => {
|
|
1463
|
+
const graphIndex = graphIndexByInitiative.get(item.initiativeId) ?? null;
|
|
1464
|
+
const selectedTaskIds = dedupeStrings([
|
|
1465
|
+
...(item.sliceTaskIds ?? []),
|
|
1466
|
+
...(item.nextTaskId ? [item.nextTaskId] : []),
|
|
1467
|
+
]);
|
|
1468
|
+
if (selectedTaskIds.length === 0)
|
|
1469
|
+
return;
|
|
1470
|
+
const taskBuckets = new Map();
|
|
1471
|
+
for (const taskId of selectedTaskIds) {
|
|
1472
|
+
const task = graphIndex?.tasksById.get(taskId) ?? null;
|
|
1473
|
+
if (!includeCompleted && isDoneStatus(task?.status ?? null))
|
|
1474
|
+
continue;
|
|
1475
|
+
const milestoneId = task?.milestoneId ??
|
|
1476
|
+
item.sliceMilestoneId ??
|
|
1477
|
+
item.nextTaskMilestoneId ??
|
|
1478
|
+
null;
|
|
1479
|
+
const bucketKey = milestoneId ?? "__none__";
|
|
1480
|
+
const bucket = taskBuckets.get(bucketKey) ?? {
|
|
1481
|
+
milestoneId,
|
|
1482
|
+
taskIds: [],
|
|
1483
|
+
};
|
|
1484
|
+
bucket.taskIds.push(taskId);
|
|
1485
|
+
taskBuckets.set(bucketKey, bucket);
|
|
1486
|
+
}
|
|
1487
|
+
for (const [bucketKey, bucket] of taskBuckets.entries()) {
|
|
1488
|
+
const scopedKey = `${item.initiativeId}:${item.workstreamId}:${bucketKey}`;
|
|
1489
|
+
const existing = grouped.get(scopedKey);
|
|
1490
|
+
if (!existing) {
|
|
1491
|
+
grouped.set(scopedKey, {
|
|
1492
|
+
base: item,
|
|
1493
|
+
milestoneId: bucket.milestoneId,
|
|
1494
|
+
milestoneTitle: (bucket.milestoneId
|
|
1495
|
+
? graphIndex?.milestoneTitleById.get(bucket.milestoneId)
|
|
1496
|
+
: null) ?? null,
|
|
1497
|
+
taskIds: new Set(bucket.taskIds),
|
|
1498
|
+
iwmtRank: index,
|
|
1499
|
+
});
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
for (const taskId of bucket.taskIds)
|
|
1503
|
+
existing.taskIds.add(taskId);
|
|
1504
|
+
if (index < existing.iwmtRank) {
|
|
1505
|
+
existing.base = item;
|
|
1506
|
+
existing.iwmtRank = index;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
for (const [id, bucket] of grouped.entries()) {
|
|
1511
|
+
const runnerAgents = mergeRunnerAgents(bucket.base.runnerAgents ?? []);
|
|
1512
|
+
const runnerPrimary = runnerAgents[0] ?? null;
|
|
1513
|
+
slices.push({
|
|
1514
|
+
id,
|
|
1515
|
+
scope: sliceScope,
|
|
1516
|
+
initiativeId: bucket.base.initiativeId,
|
|
1517
|
+
initiativeTitle: bucket.base.initiativeTitle,
|
|
1518
|
+
workstreamId: bucket.base.workstreamId,
|
|
1519
|
+
workstreamTitle: bucket.base.workstreamTitle,
|
|
1520
|
+
milestoneId: bucket.milestoneId,
|
|
1521
|
+
milestoneTitle: bucket.milestoneTitle,
|
|
1522
|
+
taskId: null,
|
|
1523
|
+
taskTitle: null,
|
|
1524
|
+
queueState: bucket.base.queueState,
|
|
1525
|
+
sourceWorkstreamIds: [bucket.base.workstreamId],
|
|
1526
|
+
runnerAgentId: runnerPrimary?.id ?? null,
|
|
1527
|
+
runnerAgentName: runnerPrimary?.name ?? "Unassigned",
|
|
1528
|
+
runnerAgents,
|
|
1529
|
+
runnerSource: bucket.base.runnerSource ?? (runnerPrimary ? "inferred" : "fallback"),
|
|
1530
|
+
nextTaskId: bucket.base.nextTaskId,
|
|
1531
|
+
nextTaskTitle: bucket.base.nextTaskTitle,
|
|
1532
|
+
nextTaskPriority: bucket.base.nextTaskPriority,
|
|
1533
|
+
nextTaskDueAt: bucket.base.nextTaskDueAt,
|
|
1534
|
+
updatedAt: bucket.base.updatedAt ?? null,
|
|
1535
|
+
sliceTaskIds: Array.from(bucket.taskIds.values()),
|
|
1536
|
+
sliceTaskCount: bucket.taskIds.size,
|
|
1537
|
+
compositeScore: bucket.base.compositeScore,
|
|
1538
|
+
scoringTier: bucket.base.scoringTier,
|
|
1539
|
+
iwmtRank: bucket.iwmtRank,
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
else {
|
|
1544
|
+
queueItems.forEach((item, index) => {
|
|
1545
|
+
const graphIndex = graphIndexByInitiative.get(item.initiativeId) ?? null;
|
|
1546
|
+
const selectedTaskIds = dedupeStrings([
|
|
1547
|
+
...(item.sliceTaskIds ?? []),
|
|
1548
|
+
...(item.nextTaskId ? [item.nextTaskId] : []),
|
|
1549
|
+
]);
|
|
1550
|
+
for (const taskId of selectedTaskIds) {
|
|
1551
|
+
const task = graphIndex?.tasksById.get(taskId) ?? null;
|
|
1552
|
+
if (!includeCompleted && isDoneStatus(task?.status ?? null))
|
|
1553
|
+
continue;
|
|
1554
|
+
const taskTitle = task?.title ??
|
|
1555
|
+
(taskId === item.nextTaskId ? item.nextTaskTitle : null) ??
|
|
1556
|
+
taskId;
|
|
1557
|
+
slices.push({
|
|
1558
|
+
id: `${item.initiativeId}:${item.workstreamId}:${taskId}`,
|
|
1559
|
+
scope: sliceScope,
|
|
1560
|
+
initiativeId: item.initiativeId,
|
|
1561
|
+
initiativeTitle: item.initiativeTitle,
|
|
1562
|
+
workstreamId: item.workstreamId,
|
|
1563
|
+
workstreamTitle: item.workstreamTitle,
|
|
1564
|
+
milestoneId: task?.milestoneId ??
|
|
1565
|
+
item.sliceMilestoneId ??
|
|
1566
|
+
item.nextTaskMilestoneId ??
|
|
1567
|
+
null,
|
|
1568
|
+
milestoneTitle: task?.milestoneId
|
|
1569
|
+
? graphIndex?.milestoneTitleById.get(task.milestoneId) ?? null
|
|
1570
|
+
: null,
|
|
1571
|
+
taskId,
|
|
1572
|
+
taskTitle,
|
|
1573
|
+
queueState: isDoneStatus(task?.status ?? null) ? "completed" : item.queueState,
|
|
1574
|
+
sourceWorkstreamIds: [item.workstreamId],
|
|
1575
|
+
runnerAgentId: (item.runnerAgents ?? [])[0]?.id ?? item.runnerAgentId ?? null,
|
|
1576
|
+
runnerAgentName: (item.runnerAgents ?? [])[0]?.name ?? item.runnerAgentName ?? "Unassigned",
|
|
1577
|
+
runnerAgents: mergeRunnerAgents(item.runnerAgents ?? []),
|
|
1578
|
+
runnerSource: item.runnerSource ??
|
|
1579
|
+
((item.runnerAgents ?? [])[0] ? "inferred" : "fallback"),
|
|
1580
|
+
nextTaskId: item.nextTaskId,
|
|
1581
|
+
nextTaskTitle: item.nextTaskTitle,
|
|
1582
|
+
nextTaskPriority: task?.priorityNum ?? item.nextTaskPriority,
|
|
1583
|
+
nextTaskDueAt: task?.dueDate ?? item.nextTaskDueAt,
|
|
1584
|
+
updatedAt: task?.updatedAt ?? item.updatedAt ?? null,
|
|
1585
|
+
sliceTaskIds: [taskId],
|
|
1586
|
+
sliceTaskCount: 1,
|
|
1587
|
+
compositeScore: item.compositeScore,
|
|
1588
|
+
scoringTier: item.scoringTier,
|
|
1589
|
+
iwmtRank: index,
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
const sorted = sortSlices(slices, order);
|
|
1595
|
+
const paged = applySliceSearchAndPagination({
|
|
1596
|
+
items: sorted,
|
|
1597
|
+
searchTerm,
|
|
1598
|
+
offset,
|
|
1599
|
+
limit: pageSize,
|
|
1600
|
+
});
|
|
1601
|
+
deps.sendJson(res, 200, {
|
|
1602
|
+
ok: true,
|
|
1603
|
+
generatedAt: new Date().toISOString(),
|
|
1604
|
+
level: sliceScope,
|
|
1605
|
+
scope: sliceScope,
|
|
1606
|
+
order,
|
|
1607
|
+
includeCompleted,
|
|
1608
|
+
total: paged.filtered.length,
|
|
1609
|
+
items: paged.paged,
|
|
1610
|
+
pagination: paged.pagination,
|
|
1611
|
+
source: canonicalFallbackReason ? "local_fallback" : "local",
|
|
1612
|
+
degraded: degraded.length > 0 ? dedupeStrings(degraded) : undefined,
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
catch (err) {
|
|
1616
|
+
sendRouteException(res, "mission-control.read.slices.handler", err);
|
|
59
1617
|
}
|
|
60
1618
|
}
|
|
1619
|
+
async function renderSentinelCatalog(query, res) {
|
|
1620
|
+
const domain = query.get("domain");
|
|
1621
|
+
const signal = query.get("signal");
|
|
1622
|
+
const items = listBuiltInSentinels({ domain, signal });
|
|
1623
|
+
deps.sendJson(res, 200, {
|
|
1624
|
+
ok: true,
|
|
1625
|
+
generatedAt: new Date().toISOString(),
|
|
1626
|
+
total: items.length,
|
|
1627
|
+
items,
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
61
1630
|
router.add("GET", "mission-control/auto-continue/status", async ({ query, res }) => renderAutoContinueStatus(query, res), "Get auto-continue status for an initiative");
|
|
62
1631
|
router.add("HEAD", "mission-control/auto-continue/status", async ({ query, res }) => renderAutoContinueStatus(query, res), "Get auto-continue status for an initiative (HEAD)");
|
|
63
1632
|
router.add("GET", "mission-control/graph", async ({ query, res }) => renderMissionControlGraph(query, res), "Get mission-control dependency graph");
|
|
64
1633
|
router.add("HEAD", "mission-control/graph", async ({ query, res }) => renderMissionControlGraph(query, res), "Get mission-control dependency graph (HEAD)");
|
|
65
|
-
router.add("GET", "mission-control/next-up", async ({ query, res }) => renderNextUpQueue(query, res), "Get next-up queue");
|
|
66
|
-
router.add("HEAD", "mission-control/next-up", async ({ query, res }) => renderNextUpQueue(query, res), "Get next-up queue (HEAD)");
|
|
1634
|
+
router.add("GET", "mission-control/next-up", async ({ query, res, req }) => renderNextUpQueue(query, res, workspaceScopeFromHeaders(req?.headers)), "Get next-up queue");
|
|
1635
|
+
router.add("HEAD", "mission-control/next-up", async ({ query, res, req }) => renderNextUpQueue(query, res, workspaceScopeFromHeaders(req?.headers)), "Get next-up queue (HEAD)");
|
|
1636
|
+
router.add("GET", "mission-control/slices", async ({ query, res, req }) => renderSliceProjection(query, res, workspaceScopeFromHeaders(req?.headers)), "Get mission-control slices at initiative/workstream/milestone/task scope");
|
|
1637
|
+
router.add("HEAD", "mission-control/slices", async ({ query, res, req }) => renderSliceProjection(query, res, workspaceScopeFromHeaders(req?.headers)), "Get mission-control slices at initiative/workstream/milestone/task scope (HEAD)");
|
|
1638
|
+
router.add("GET", "mission-control/sentinels", async ({ query, res }) => renderSentinelCatalog(query, res), "Get built-in sentinel catalog");
|
|
1639
|
+
router.add("HEAD", "mission-control/sentinels", async ({ query, res }) => renderSentinelCatalog(query, res), "Get built-in sentinel catalog (HEAD)");
|
|
67
1640
|
}
|