bosun 0.41.2 → 0.41.4
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/.env.example +1 -1
- package/agent/agent-pool.mjs +9 -2
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +119 -6
- package/agent/autofix-git.mjs +33 -0
- package/agent/autofix-prompts.mjs +151 -0
- package/agent/autofix.mjs +11 -175
- package/agent/bosun-skills.mjs +3 -2
- package/bosun.config.example.json +17 -0
- package/bosun.schema.json +87 -188
- package/cli.mjs +34 -1
- package/config/config-doctor.mjs +5 -250
- package/config/config-file-names.mjs +5 -0
- package/config/config.mjs +89 -493
- package/config/executor-config.mjs +493 -0
- package/config/repo-root.mjs +1 -2
- package/config/workspace-health.mjs +242 -0
- package/git/git-safety.mjs +15 -0
- package/github/github-oauth-portal.mjs +46 -0
- package/infra/library-manager-utils.mjs +22 -0
- package/infra/library-manager-well-known-sources.mjs +578 -0
- package/infra/library-manager.mjs +512 -1030
- package/infra/monitor.mjs +35 -9
- package/infra/session-tracker.mjs +10 -7
- package/kanban/kanban-adapter.mjs +17 -1
- package/lib/codebase-audit-manifests.mjs +117 -0
- package/lib/codebase-audit.mjs +18 -115
- package/package.json +18 -3
- package/server/setup-web-server.mjs +58 -5
- package/server/ui-server.mjs +1394 -79
- package/shell/codex-config-file.mjs +178 -0
- package/shell/codex-config.mjs +538 -575
- package/task/task-cli.mjs +54 -3
- package/task/task-executor.mjs +143 -13
- package/task/task-store.mjs +409 -1
- package/telegram/telegram-bot.mjs +127 -0
- package/tools/apply-pr-suggestions.mjs +401 -0
- package/tools/syntax-check.mjs +28 -9
- package/ui/app.js +3 -14
- package/ui/components/kanban-board.js +227 -4
- package/ui/components/session-list.js +85 -5
- package/ui/demo-defaults.js +338 -84
- package/ui/demo.html +155 -0
- package/ui/modules/session-api.js +96 -0
- package/ui/modules/settings-schema.js +1 -2
- package/ui/modules/state.js +43 -3
- package/ui/setup.html +4 -5
- package/ui/styles/components.css +58 -4
- package/ui/tabs/agents.js +12 -15
- package/ui/tabs/control.js +1 -0
- package/ui/tabs/library.js +484 -22
- package/ui/tabs/manual-flows.js +105 -29
- package/ui/tabs/tasks.js +848 -141
- package/ui/tabs/telemetry.js +129 -11
- package/ui/tabs/workflow-canvas-utils.mjs +130 -0
- package/ui/tabs/workflows.js +293 -23
- package/voice/voice-tool-definitions.mjs +757 -0
- package/voice/voice-tools.mjs +34 -778
- package/workflow/manual-flow-audit.mjs +165 -0
- package/workflow/manual-flows.mjs +164 -259
- package/workflow/workflow-engine.mjs +147 -58
- package/workflow/workflow-nodes/definitions.mjs +1207 -0
- package/workflow/workflow-nodes/transforms.mjs +612 -0
- package/workflow/workflow-nodes.mjs +358 -63
- package/workflow/workflow-templates.mjs +313 -191
- package/workflow-templates/_helpers.mjs +154 -0
- package/workflow-templates/agents.mjs +61 -4
- package/workflow-templates/code-quality.mjs +7 -7
- package/workflow-templates/github.mjs +20 -10
- package/workflow-templates/task-batch.mjs +44 -11
- package/workflow-templates/task-lifecycle.mjs +31 -6
- package/workspace/worktree-manager.mjs +277 -3
package/server/ui-server.mjs
CHANGED
|
@@ -208,6 +208,7 @@ const TASK_STORE_SPRINT_EXPORTS = Object.freeze({
|
|
|
208
208
|
const TASK_STORE_DAG_EXPORTS = Object.freeze({
|
|
209
209
|
sprint: ["getSprintDag", "getTaskDagForSprint", "buildSprintDag", "buildTaskDag"],
|
|
210
210
|
global: ["getGlobalDagOfDags", "getDagOfDags", "buildGlobalDagOfDags"],
|
|
211
|
+
organize: ["organizeTaskDag"],
|
|
211
212
|
});
|
|
212
213
|
const TASK_STORE_GET_TASK_EXPORTS = ["getTaskById", "getTask"];
|
|
213
214
|
const TASK_STORE_COMMENT_EXPORTS = ["getTaskComments", "listTaskComments"];
|
|
@@ -308,6 +309,16 @@ function addInternalTaskComment(taskId, comment = {}) {
|
|
|
308
309
|
}
|
|
309
310
|
}
|
|
310
311
|
|
|
312
|
+
function unblockInternalTask(taskId, options = {}) {
|
|
313
|
+
const fn = getTaskStoreApiSync()?.unblockTask;
|
|
314
|
+
if (typeof fn !== "function") return null;
|
|
315
|
+
try {
|
|
316
|
+
return fn(taskId, options);
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
311
322
|
const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
312
323
|
const repoRoot = resolveRepoRoot();
|
|
313
324
|
const uiRootPreferred = resolve(__dirname, "..", "ui");
|
|
@@ -1104,6 +1115,73 @@ let _wfInitPromise = null;
|
|
|
1104
1115
|
let _wfInitDone = false;
|
|
1105
1116
|
let _wfLoadedBase = null;
|
|
1106
1117
|
let _wfTaskTraceHookRegistered = false;
|
|
1118
|
+
let _workflowTelegramDigestPromise = null;
|
|
1119
|
+
const workflowTelegramDedup = new Map();
|
|
1120
|
+
|
|
1121
|
+
async function getWorkflowTelegramDigest() {
|
|
1122
|
+
if (_workflowTelegramDigestPromise) {
|
|
1123
|
+
return _workflowTelegramDigestPromise;
|
|
1124
|
+
}
|
|
1125
|
+
_workflowTelegramDigestPromise = (async () => {
|
|
1126
|
+
try {
|
|
1127
|
+
const mod = await import("../telegram/telegram-bot.mjs");
|
|
1128
|
+
if (typeof mod.restoreLiveDigest === "function") {
|
|
1129
|
+
await mod.restoreLiveDigest().catch(() => {});
|
|
1130
|
+
}
|
|
1131
|
+
return mod;
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
console.warn("[workflows/telegram] live digest unavailable:", err?.message || err);
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
})();
|
|
1137
|
+
return _workflowTelegramDigestPromise;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async function sendWorkflowTelegramMessage(chatId, text, options = {}) {
|
|
1141
|
+
const telegramToken = process.env.TELEGRAM_BOT_TOKEN;
|
|
1142
|
+
const defaultChatId = String(process.env.TELEGRAM_CHAT_ID || "").trim();
|
|
1143
|
+
const target = String(chatId || defaultChatId || "").trim();
|
|
1144
|
+
if (!telegramToken || !target) return;
|
|
1145
|
+
|
|
1146
|
+
const message = String(text || "");
|
|
1147
|
+
const dedupKey = `${target}:${message.trim()}`;
|
|
1148
|
+
const now = Date.now();
|
|
1149
|
+
const lastSentAt = workflowTelegramDedup.get(dedupKey) || 0;
|
|
1150
|
+
if (dedupKey && now - lastSentAt < 5 * 60 * 1000) {
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
workflowTelegramDedup.set(dedupKey, now);
|
|
1154
|
+
|
|
1155
|
+
const parseMode = String(options?.parseMode || "").trim();
|
|
1156
|
+
if (!parseMode && defaultChatId && target === defaultChatId) {
|
|
1157
|
+
const digest = await getWorkflowTelegramDigest();
|
|
1158
|
+
if (typeof digest?.notify === "function") {
|
|
1159
|
+
await digest.notify(message, 4, {
|
|
1160
|
+
silent: Boolean(options?.silent),
|
|
1161
|
+
category: "workflow",
|
|
1162
|
+
});
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
try {
|
|
1168
|
+
await fetch(
|
|
1169
|
+
`https://api.telegram.org/bot${telegramToken}/sendMessage`,
|
|
1170
|
+
{
|
|
1171
|
+
method: "POST",
|
|
1172
|
+
headers: { "Content-Type": "application/json" },
|
|
1173
|
+
body: JSON.stringify({
|
|
1174
|
+
chat_id: target,
|
|
1175
|
+
text: message,
|
|
1176
|
+
parse_mode: parseMode || "HTML",
|
|
1177
|
+
disable_notification: Boolean(options?.silent),
|
|
1178
|
+
}),
|
|
1179
|
+
},
|
|
1180
|
+
);
|
|
1181
|
+
} catch (e) {
|
|
1182
|
+
console.warn("[workflows/telegram] sendMessage failed:", e.message);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1107
1185
|
|
|
1108
1186
|
/**
|
|
1109
1187
|
* Test-only: inject a mock workflow engine module and pre-seed the per-workspace
|
|
@@ -1251,6 +1329,9 @@ async function getWorkflowEngineModule() {
|
|
|
1251
1329
|
_wfEngine = await import(new URL("../workflow/workflow-engine.mjs", base).href);
|
|
1252
1330
|
_wfNodes = await import(new URL("../workflow/workflow-nodes.mjs", base).href);
|
|
1253
1331
|
_wfTemplates = await import(new URL("../workflow/workflow-templates.mjs", base).href);
|
|
1332
|
+
if (typeof _wfNodes?.ensureWorkflowNodeTypesLoaded === "function") {
|
|
1333
|
+
await _wfNodes.ensureWorkflowNodeTypesLoaded({ repoRoot });
|
|
1334
|
+
}
|
|
1254
1335
|
if (_wfLoadedBase !== base) {
|
|
1255
1336
|
console.log(`[workflows] Loaded workflow modules from: ${base}`);
|
|
1256
1337
|
_wfLoadedBase = base;
|
|
@@ -1276,26 +1357,11 @@ async function getWorkflowEngineModule() {
|
|
|
1276
1357
|
const telegramService = telegramToken
|
|
1277
1358
|
? {
|
|
1278
1359
|
async sendMessage(chatId, text, options = {}) {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
`https://api.telegram.org/bot${telegramToken}/sendMessage`,
|
|
1285
|
-
{
|
|
1286
|
-
method: "POST",
|
|
1287
|
-
headers: { "Content-Type": "application/json" },
|
|
1288
|
-
body: JSON.stringify({
|
|
1289
|
-
chat_id: target,
|
|
1290
|
-
text: String(text || ""),
|
|
1291
|
-
parse_mode: parseMode,
|
|
1292
|
-
disable_notification: Boolean(options?.silent),
|
|
1293
|
-
}),
|
|
1294
|
-
}
|
|
1295
|
-
);
|
|
1296
|
-
} catch (e) {
|
|
1297
|
-
console.warn("[workflows/telegram] sendMessage failed:", e.message);
|
|
1298
|
-
}
|
|
1360
|
+
await sendWorkflowTelegramMessage(
|
|
1361
|
+
chatId || telegramChatId,
|
|
1362
|
+
text,
|
|
1363
|
+
options,
|
|
1364
|
+
);
|
|
1299
1365
|
},
|
|
1300
1366
|
}
|
|
1301
1367
|
: null;
|
|
@@ -1541,15 +1607,67 @@ function handleTaskWorkflowTraceEvent(event = {}) {
|
|
|
1541
1607
|
|
|
1542
1608
|
function mergeTaskWorkflowRuns(baseRuns = [], extraRuns = [], limit = 60) {
|
|
1543
1609
|
const merged = [];
|
|
1544
|
-
const
|
|
1610
|
+
const indexByKey = new Map();
|
|
1611
|
+
const resolveLinkedSessionId = (entry) => {
|
|
1612
|
+
const candidates = [
|
|
1613
|
+
entry?.sessionId,
|
|
1614
|
+
entry?.threadId,
|
|
1615
|
+
entry?.agentSessionId,
|
|
1616
|
+
entry?.meta?.sessionId,
|
|
1617
|
+
entry?.meta?.threadId,
|
|
1618
|
+
entry?.data?.sessionId,
|
|
1619
|
+
entry?.data?.threadId,
|
|
1620
|
+
];
|
|
1621
|
+
for (const value of candidates) {
|
|
1622
|
+
const normalized = String(value || "").trim();
|
|
1623
|
+
if (normalized) return normalized;
|
|
1624
|
+
}
|
|
1625
|
+
return null;
|
|
1626
|
+
};
|
|
1627
|
+
const resolveSessionId = (entry) => {
|
|
1628
|
+
const directSessionId = resolveLinkedSessionId(entry);
|
|
1629
|
+
if (directSessionId) return directSessionId;
|
|
1630
|
+
const primarySessionId = String(entry?.primarySessionId || "").trim();
|
|
1631
|
+
return primarySessionId || null;
|
|
1632
|
+
};
|
|
1633
|
+
const mergeEntries = (current, incoming) => {
|
|
1634
|
+
const currentMeta = current?.meta && typeof current.meta === "object" ? current.meta : {};
|
|
1635
|
+
const incomingMeta = incoming?.meta && typeof incoming.meta === "object" ? incoming.meta : {};
|
|
1636
|
+
const mergedMeta = { ...currentMeta, ...incomingMeta };
|
|
1637
|
+
const currentMetaSessionId = String(currentMeta.sessionId || "").trim();
|
|
1638
|
+
const currentMetaThreadId = String(currentMeta.threadId || "").trim();
|
|
1639
|
+
if (currentMetaSessionId) mergedMeta.sessionId = currentMetaSessionId;
|
|
1640
|
+
if (currentMetaThreadId) mergedMeta.threadId = currentMetaThreadId;
|
|
1641
|
+
return {
|
|
1642
|
+
...current,
|
|
1643
|
+
...incoming,
|
|
1644
|
+
runId: incoming.runId || current.runId || null,
|
|
1645
|
+
workflowId: incoming.workflowId || current.workflowId || null,
|
|
1646
|
+
workflowName: incoming.workflowName || current.workflowName || null,
|
|
1647
|
+
status: incoming.status || current.status || null,
|
|
1648
|
+
outcome: incoming.outcome || current.outcome || null,
|
|
1649
|
+
summary: incoming.summary || current.summary || null,
|
|
1650
|
+
startedAt: incoming.startedAt || current.startedAt || null,
|
|
1651
|
+
endedAt: incoming.endedAt || current.endedAt || null,
|
|
1652
|
+
duration: incoming.duration ?? current.duration ?? null,
|
|
1653
|
+
url: incoming.url || current.url || null,
|
|
1654
|
+
nodeId: incoming.nodeId || current.nodeId || null,
|
|
1655
|
+
source: incoming.source || current.source || "workflow",
|
|
1656
|
+
sessionId: resolveLinkedSessionId(incoming) || resolveLinkedSessionId(current) || null,
|
|
1657
|
+
primarySessionId:
|
|
1658
|
+
String(incoming.primarySessionId || "").trim()
|
|
1659
|
+
|| String(current.primarySessionId || "").trim()
|
|
1660
|
+
|| resolveLinkedSessionId(incoming)
|
|
1661
|
+
|| resolveLinkedSessionId(current),
|
|
1662
|
+
meta: mergedMeta,
|
|
1663
|
+
};
|
|
1664
|
+
};
|
|
1545
1665
|
const push = (entry) => {
|
|
1546
1666
|
if (!entry || typeof entry !== "object") return;
|
|
1547
1667
|
const runId = String(entry.runId || "").trim();
|
|
1548
1668
|
const workflowId = String(entry.workflowId || "").trim();
|
|
1549
1669
|
const dedupKey = runId ? `run:${runId}` : `wf:${workflowId}:${entry.startedAt || entry.endedAt || ""}`;
|
|
1550
|
-
|
|
1551
|
-
seen.add(dedupKey);
|
|
1552
|
-
merged.push({
|
|
1670
|
+
const normalized = {
|
|
1553
1671
|
runId: runId || null,
|
|
1554
1672
|
workflowId: workflowId || null,
|
|
1555
1673
|
workflowName: entry.workflowName != null ? String(entry.workflowName) : null,
|
|
@@ -1559,8 +1677,21 @@ function mergeTaskWorkflowRuns(baseRuns = [], extraRuns = [], limit = 60) {
|
|
|
1559
1677
|
startedAt: entry.startedAt || null,
|
|
1560
1678
|
endedAt: entry.endedAt || null,
|
|
1561
1679
|
duration: Number.isFinite(Number(entry.duration)) ? Number(entry.duration) : null,
|
|
1680
|
+
url: entry.url != null ? String(entry.url) : null,
|
|
1681
|
+
nodeId: entry.nodeId != null ? String(entry.nodeId) : null,
|
|
1562
1682
|
source: entry.source ? String(entry.source) : "workflow",
|
|
1563
|
-
|
|
1683
|
+
sessionId: resolveLinkedSessionId(entry),
|
|
1684
|
+
primarySessionId:
|
|
1685
|
+
String(entry.primarySessionId || "").trim() || resolveSessionId(entry),
|
|
1686
|
+
meta: entry.meta && typeof entry.meta === "object" ? { ...entry.meta } : {},
|
|
1687
|
+
};
|
|
1688
|
+
const existingIndex = indexByKey.get(dedupKey);
|
|
1689
|
+
if (existingIndex == null) {
|
|
1690
|
+
indexByKey.set(dedupKey, merged.length);
|
|
1691
|
+
merged.push(normalized);
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
merged[existingIndex] = mergeEntries(merged[existingIndex], normalized);
|
|
1564
1695
|
};
|
|
1565
1696
|
|
|
1566
1697
|
for (const run of Array.isArray(baseRuns) ? baseRuns : []) push(run);
|
|
@@ -1599,6 +1730,33 @@ async function collectWorkflowRunsForTask(taskId, reqUrl, limit = 40) {
|
|
|
1599
1730
|
matches = traceEvents.some((event) => String(event?.taskId || "").trim() === normalizedTaskId);
|
|
1600
1731
|
}
|
|
1601
1732
|
if (!matches) continue;
|
|
1733
|
+
const primarySessionId = (() => {
|
|
1734
|
+
for (const value of [
|
|
1735
|
+
data.sessionId,
|
|
1736
|
+
data.threadId,
|
|
1737
|
+
data?.task?.sessionId,
|
|
1738
|
+
data?.task?.threadId,
|
|
1739
|
+
]) {
|
|
1740
|
+
const normalized = String(value || "").trim();
|
|
1741
|
+
if (normalized) return normalized;
|
|
1742
|
+
}
|
|
1743
|
+
const traceEvents = typeof engine.getTaskTraceEvents === "function"
|
|
1744
|
+
? engine.getTaskTraceEvents(summary.runId) || []
|
|
1745
|
+
: [];
|
|
1746
|
+
for (let index = traceEvents.length - 1; index >= 0; index -= 1) {
|
|
1747
|
+
const event = traceEvents[index];
|
|
1748
|
+
for (const value of [
|
|
1749
|
+
event?.sessionId,
|
|
1750
|
+
event?.threadId,
|
|
1751
|
+
event?.meta?.sessionId,
|
|
1752
|
+
event?.meta?.threadId,
|
|
1753
|
+
]) {
|
|
1754
|
+
const normalized = String(value || "").trim();
|
|
1755
|
+
if (normalized) return normalized;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
return null;
|
|
1759
|
+
})();
|
|
1602
1760
|
out.push({
|
|
1603
1761
|
runId: detail.runId,
|
|
1604
1762
|
workflowId: detail.workflowId,
|
|
@@ -1611,6 +1769,8 @@ async function collectWorkflowRunsForTask(taskId, reqUrl, limit = 40) {
|
|
|
1611
1769
|
startedAt: detail.startedAt || null,
|
|
1612
1770
|
endedAt: detail.endedAt || null,
|
|
1613
1771
|
duration: detail.duration || null,
|
|
1772
|
+
sessionId: null,
|
|
1773
|
+
primarySessionId,
|
|
1614
1774
|
source: "workflow",
|
|
1615
1775
|
});
|
|
1616
1776
|
if (out.length >= limit) break;
|
|
@@ -1620,6 +1780,230 @@ async function collectWorkflowRunsForTask(taskId, reqUrl, limit = 40) {
|
|
|
1620
1780
|
return [];
|
|
1621
1781
|
}
|
|
1622
1782
|
}
|
|
1783
|
+
|
|
1784
|
+
function sanitizeTaskDiagnosticText(value, maxLength = 240) {
|
|
1785
|
+
const normalized = String(value || "")
|
|
1786
|
+
.replace(/\s+/g, " ")
|
|
1787
|
+
.trim();
|
|
1788
|
+
if (!normalized) return "";
|
|
1789
|
+
if (!Number.isFinite(maxLength) || maxLength <= 0 || normalized.length <= maxLength) {
|
|
1790
|
+
return normalized;
|
|
1791
|
+
}
|
|
1792
|
+
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function collectTaskTimelineDiagnostics(task, limit = 8) {
|
|
1796
|
+
const timeline = Array.isArray(task?.timeline) ? task.timeline : [];
|
|
1797
|
+
const relevant = [];
|
|
1798
|
+
for (const entry of timeline) {
|
|
1799
|
+
const message = sanitizeTaskDiagnosticText(
|
|
1800
|
+
entry?.message || entry?.reason || entry?.error || "",
|
|
1801
|
+
280,
|
|
1802
|
+
);
|
|
1803
|
+
const status = String(entry?.status || "").trim().toLowerCase();
|
|
1804
|
+
if (
|
|
1805
|
+
!message &&
|
|
1806
|
+
status !== "blocked"
|
|
1807
|
+
) {
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
const isRelevant =
|
|
1811
|
+
status === "blocked" ||
|
|
1812
|
+
/worktree failed/i.test(message) ||
|
|
1813
|
+
/pre-pr validation failed/i.test(message) ||
|
|
1814
|
+
/claim was stolen/i.test(message) ||
|
|
1815
|
+
/blocked/i.test(message);
|
|
1816
|
+
if (!isRelevant) continue;
|
|
1817
|
+
relevant.push({
|
|
1818
|
+
source: String(entry?.source || entry?.type || "timeline").trim() || "timeline",
|
|
1819
|
+
message: message || `Task entered ${status || "blocked"} state`,
|
|
1820
|
+
timestamp: entry?.timestamp || entry?.createdAt || entry?.updatedAt || null,
|
|
1821
|
+
status: status || null,
|
|
1822
|
+
kind: "timeline",
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
return relevant.slice(-Math.max(1, limit));
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function collectTaskLogDiagnostics(task, workspaceDir = "", limit = 8) {
|
|
1829
|
+
const taskId = String(task?.id || task?.taskId || "").trim();
|
|
1830
|
+
if (!taskId) {
|
|
1831
|
+
return {
|
|
1832
|
+
counts: {
|
|
1833
|
+
prePrValidationFailed: 0,
|
|
1834
|
+
worktreeFailed: 0,
|
|
1835
|
+
blockedTransitions: 0,
|
|
1836
|
+
createPrFailed: 0,
|
|
1837
|
+
},
|
|
1838
|
+
entries: [],
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
const taskBranch = String(task?.branch || task?.branchName || "").trim();
|
|
1843
|
+
const needles = [taskId, taskBranch].filter((value) => value && value.length >= 8);
|
|
1844
|
+
const logPaths = [];
|
|
1845
|
+
const pushLogPath = (candidate) => {
|
|
1846
|
+
if (!candidate || !existsSync(candidate) || logPaths.includes(candidate)) return;
|
|
1847
|
+
logPaths.push(candidate);
|
|
1848
|
+
};
|
|
1849
|
+
if (workspaceDir) {
|
|
1850
|
+
pushLogPath(resolve(workspaceDir, ".bosun", "logs", "monitor-error.log"));
|
|
1851
|
+
pushLogPath(resolve(workspaceDir, ".bosun", "logs", "monitor.log"));
|
|
1852
|
+
}
|
|
1853
|
+
pushLogPath(resolve(repoRoot, ".bosun", "logs", "monitor-error.log"));
|
|
1854
|
+
pushLogPath(resolve(repoRoot, ".bosun", "logs", "monitor.log"));
|
|
1855
|
+
|
|
1856
|
+
const counts = {
|
|
1857
|
+
prePrValidationFailed: 0,
|
|
1858
|
+
worktreeFailed: 0,
|
|
1859
|
+
blockedTransitions: 0,
|
|
1860
|
+
createPrFailed: 0,
|
|
1861
|
+
};
|
|
1862
|
+
const entries = [];
|
|
1863
|
+
|
|
1864
|
+
for (const logPath of logPaths) {
|
|
1865
|
+
let raw = "";
|
|
1866
|
+
try {
|
|
1867
|
+
raw = readFileSync(logPath, "utf8");
|
|
1868
|
+
} catch {
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
const logName = /monitor-error\.log$/i.test(logPath) ? "monitor-error.log" : "monitor.log";
|
|
1872
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1873
|
+
if (!line) continue;
|
|
1874
|
+
if (!needles.some((needle) => line.includes(needle))) continue;
|
|
1875
|
+
const text = sanitizeTaskDiagnosticText(line, 320);
|
|
1876
|
+
let matched = false;
|
|
1877
|
+
if (/pre-PR validation failed/i.test(text)) {
|
|
1878
|
+
counts.prePrValidationFailed += 1;
|
|
1879
|
+
matched = true;
|
|
1880
|
+
}
|
|
1881
|
+
if (/Worktree failed for/i.test(text) || /Worktree acquisition failed/i.test(text)) {
|
|
1882
|
+
counts.worktreeFailed += 1;
|
|
1883
|
+
matched = true;
|
|
1884
|
+
}
|
|
1885
|
+
if (/-> blocked/i.test(text) || /status: .*blocked/i.test(text)) {
|
|
1886
|
+
counts.blockedTransitions += 1;
|
|
1887
|
+
matched = true;
|
|
1888
|
+
}
|
|
1889
|
+
if (/create-pr FAILED/i.test(text)) {
|
|
1890
|
+
counts.createPrFailed += 1;
|
|
1891
|
+
matched = true;
|
|
1892
|
+
}
|
|
1893
|
+
if (!matched) continue;
|
|
1894
|
+
entries.push({
|
|
1895
|
+
source: logName,
|
|
1896
|
+
message: text,
|
|
1897
|
+
kind: "log",
|
|
1898
|
+
});
|
|
1899
|
+
if (entries.length > limit) entries.shift();
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
return { counts, entries };
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function buildTaskBlockedContext(task, options = {}) {
|
|
1907
|
+
const currentTask = task && typeof task === "object" ? task : {};
|
|
1908
|
+
const canStart = options.canStart && typeof options.canStart === "object"
|
|
1909
|
+
? options.canStart
|
|
1910
|
+
: null;
|
|
1911
|
+
const normalizedStatus = normalizeTaskStatusKey(currentTask?.status);
|
|
1912
|
+
const explicitReason = sanitizeTaskDiagnosticText(
|
|
1913
|
+
currentTask?.blockedReason
|
|
1914
|
+
|| currentTask?.meta?.worktreeFailure?.blockedReason
|
|
1915
|
+
|| currentTask?.meta?.autoRecovery?.error
|
|
1916
|
+
|| currentTask?.meta?.worktreeFailure?.error
|
|
1917
|
+
|| "",
|
|
1918
|
+
280,
|
|
1919
|
+
);
|
|
1920
|
+
const workflowRuns = Array.isArray(options.workflowRuns)
|
|
1921
|
+
? options.workflowRuns
|
|
1922
|
+
: Array.isArray(currentTask?.workflowRuns)
|
|
1923
|
+
? currentTask.workflowRuns
|
|
1924
|
+
: [];
|
|
1925
|
+
const timelineEvidence = collectTaskTimelineDiagnostics(currentTask, 6);
|
|
1926
|
+
const logDiagnostics = collectTaskLogDiagnostics(
|
|
1927
|
+
currentTask,
|
|
1928
|
+
normalizeCandidatePath(options.workspaceDir),
|
|
1929
|
+
6,
|
|
1930
|
+
);
|
|
1931
|
+
const hasPlannerCorruption = /planner payload corrupted/i.test(explicitReason);
|
|
1932
|
+
const hasWorktreeFailure =
|
|
1933
|
+
Boolean(currentTask?.meta?.worktreeFailure) ||
|
|
1934
|
+
logDiagnostics.counts.worktreeFailed > 0 ||
|
|
1935
|
+
timelineEvidence.some((entry) => /worktree failed/i.test(String(entry?.message || "")));
|
|
1936
|
+
const isDependencyBlocked = canStart?.canStart === false && String(canStart?.reason || "") === "dependency_blocked";
|
|
1937
|
+
|
|
1938
|
+
let category = "";
|
|
1939
|
+
let headline = "";
|
|
1940
|
+
let summary = "";
|
|
1941
|
+
let recommendation = "";
|
|
1942
|
+
|
|
1943
|
+
if (hasPlannerCorruption) {
|
|
1944
|
+
category = "planner_payload_corruption";
|
|
1945
|
+
headline = "Planner payload corruption quarantined this task.";
|
|
1946
|
+
summary = explicitReason;
|
|
1947
|
+
recommendation = "Do not requeue this task as-is. Recreate it from the fixed planner path or repair its payload first.";
|
|
1948
|
+
} else if (hasWorktreeFailure) {
|
|
1949
|
+
category = "worktree_failure";
|
|
1950
|
+
headline = "Task Lifecycle blocked this task after worktree acquisition failed.";
|
|
1951
|
+
summary = explicitReason
|
|
1952
|
+
|| "Bosun could not acquire or refresh a clean managed worktree for this task.";
|
|
1953
|
+
recommendation = "If the worktree guard fix is now deployed, move the task back to todo to retry it on a fresh lifecycle run.";
|
|
1954
|
+
} else if (isDependencyBlocked) {
|
|
1955
|
+
category = "dependency_blocked";
|
|
1956
|
+
headline = "This task cannot start because one or more dependencies are not done yet.";
|
|
1957
|
+
summary = "Bosun will not dispatch this task until every blocking dependency below is resolved.";
|
|
1958
|
+
recommendation = "Complete or unblock the listed dependencies, then dispatch this task again.";
|
|
1959
|
+
} else if (normalizedStatus === "blocked") {
|
|
1960
|
+
category = "blocked";
|
|
1961
|
+
headline = "This task is blocked.";
|
|
1962
|
+
summary = explicitReason || "Bosun marked this task as blocked, but the original blocked reason was not persisted.";
|
|
1963
|
+
recommendation = "Review the recent workflow evidence below. After the underlying issue is fixed, move the task back to todo to clear the block and retry it.";
|
|
1964
|
+
} else if (canStart?.canStart === false) {
|
|
1965
|
+
category = "start_guard_blocked";
|
|
1966
|
+
headline = "This task is currently not startable.";
|
|
1967
|
+
summary = sanitizeTaskDiagnosticText(canStart?.reason || "Bosun start guards rejected dispatch for this task.");
|
|
1968
|
+
recommendation = "Resolve the blocking condition below before dispatching the task.";
|
|
1969
|
+
} else {
|
|
1970
|
+
return null;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
return {
|
|
1974
|
+
status: normalizedStatus,
|
|
1975
|
+
category,
|
|
1976
|
+
headline,
|
|
1977
|
+
summary,
|
|
1978
|
+
recommendation,
|
|
1979
|
+
reason: explicitReason || sanitizeTaskDiagnosticText(canStart?.reason || ""),
|
|
1980
|
+
workflowRunCount: workflowRuns.length,
|
|
1981
|
+
prePrValidationFailureCount: logDiagnostics.counts.prePrValidationFailed,
|
|
1982
|
+
worktreeFailureCount: logDiagnostics.counts.worktreeFailed,
|
|
1983
|
+
blockedTransitionCount: logDiagnostics.counts.blockedTransitions,
|
|
1984
|
+
createPrFailureCount: logDiagnostics.counts.createPrFailed,
|
|
1985
|
+
blockedBy: Array.isArray(canStart?.blockedBy) ? canStart.blockedBy : [],
|
|
1986
|
+
blockingTaskIds: Array.isArray(canStart?.blockingTaskIds) ? canStart.blockingTaskIds : [],
|
|
1987
|
+
timelineEvidence,
|
|
1988
|
+
logEvidence: logDiagnostics.entries,
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function buildTaskMetaPatch(previousMeta, metadataPatchMeta, options = {}) {
|
|
1993
|
+
const clearBlockedState = options.clearBlockedState === true;
|
|
1994
|
+
const nextMeta = previousMeta && typeof previousMeta === "object"
|
|
1995
|
+
? { ...previousMeta }
|
|
1996
|
+
: {};
|
|
1997
|
+
if (clearBlockedState) {
|
|
1998
|
+
delete nextMeta.autoRecovery;
|
|
1999
|
+
delete nextMeta.blockedReason;
|
|
2000
|
+
}
|
|
2001
|
+
if (metadataPatchMeta && typeof metadataPatchMeta === "object") {
|
|
2002
|
+
Object.assign(nextMeta, metadataPatchMeta);
|
|
2003
|
+
}
|
|
2004
|
+
return nextMeta;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
1623
2007
|
function maybeBootstrapWorkspaceWorkflowTemplates(engine, workspaceKey, workspaceLabel) {
|
|
1624
2008
|
if (!engine || !_wfTemplates) return;
|
|
1625
2009
|
if (_wfRecommendedInstalledByWorkspace.has(workspaceKey)) return;
|
|
@@ -2450,31 +2834,159 @@ function normalizeTriggerTemplateId(template = {}) {
|
|
|
2450
2834
|
.toLowerCase();
|
|
2451
2835
|
}
|
|
2452
2836
|
|
|
2837
|
+
function sanitizeTriggerTemplateInput(template = {}) {
|
|
2838
|
+
if (!template || typeof template !== "object" || Array.isArray(template)) {
|
|
2839
|
+
return {};
|
|
2840
|
+
}
|
|
2841
|
+
const sanitized = {};
|
|
2842
|
+
for (const key of [
|
|
2843
|
+
"id",
|
|
2844
|
+
"name",
|
|
2845
|
+
"description",
|
|
2846
|
+
"enabled",
|
|
2847
|
+
"action",
|
|
2848
|
+
"minIntervalMinutes",
|
|
2849
|
+
"trigger",
|
|
2850
|
+
"config",
|
|
2851
|
+
]) {
|
|
2852
|
+
if (Object.prototype.hasOwnProperty.call(template, key)) {
|
|
2853
|
+
sanitized[key] = template[key];
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return sanitized;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2453
2859
|
function normalizeTriggerTemplate(template = {}) {
|
|
2454
|
-
const
|
|
2860
|
+
const source = sanitizeTriggerTemplateInput(template);
|
|
2861
|
+
const id = normalizeTriggerTemplateId(source);
|
|
2455
2862
|
if (!id) return null;
|
|
2456
2863
|
return {
|
|
2457
|
-
...template,
|
|
2458
2864
|
id,
|
|
2459
|
-
name: String(
|
|
2460
|
-
|
|
2461
|
-
|
|
2865
|
+
name: String(source?.name || id).trim() || id,
|
|
2866
|
+
description: String(source?.description || "").trim(),
|
|
2867
|
+
enabled: source?.enabled === true,
|
|
2868
|
+
action: String(source?.action || "create-task").trim(),
|
|
2462
2869
|
minIntervalMinutes:
|
|
2463
|
-
Number.isFinite(Number(
|
|
2464
|
-
Number(
|
|
2465
|
-
? Number(
|
|
2870
|
+
Number.isFinite(Number(source?.minIntervalMinutes)) &&
|
|
2871
|
+
Number(source?.minIntervalMinutes) > 0
|
|
2872
|
+
? Number(source.minIntervalMinutes)
|
|
2466
2873
|
: undefined,
|
|
2467
2874
|
trigger:
|
|
2468
|
-
|
|
2469
|
-
?
|
|
2875
|
+
source?.trigger && typeof source.trigger === "object"
|
|
2876
|
+
? source.trigger
|
|
2470
2877
|
: { anyOf: [] },
|
|
2471
2878
|
config:
|
|
2472
|
-
|
|
2473
|
-
?
|
|
2879
|
+
source?.config && typeof source.config === "object"
|
|
2880
|
+
? source.config
|
|
2474
2881
|
: {},
|
|
2475
2882
|
};
|
|
2476
2883
|
}
|
|
2477
2884
|
|
|
2885
|
+
function buildTaskStateExportPayload(tasks = [], backend = "unknown") {
|
|
2886
|
+
return {
|
|
2887
|
+
schemaVersion: 1,
|
|
2888
|
+
kind: "bosun-task-state-export",
|
|
2889
|
+
exportedAt: new Date().toISOString(),
|
|
2890
|
+
backend,
|
|
2891
|
+
tasks: Array.isArray(tasks) ? tasks : [],
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
function extractImportedTaskList(body = null) {
|
|
2896
|
+
if (Array.isArray(body)) return body;
|
|
2897
|
+
if (body && typeof body === "object") {
|
|
2898
|
+
if (Array.isArray(body.tasks)) return body.tasks;
|
|
2899
|
+
if (Array.isArray(body.backlog)) return body.backlog;
|
|
2900
|
+
if (body.data && typeof body.data === "object" && Array.isArray(body.data.tasks)) {
|
|
2901
|
+
return body.data.tasks;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
return null;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
async function importInternalTaskStateSnapshot(body = {}) {
|
|
2908
|
+
const taskStore = await ensureTaskStoreApi();
|
|
2909
|
+
const addTaskFn = typeof taskStore?.addTask === "function" ? taskStore.addTask : null;
|
|
2910
|
+
const updateTaskFn = typeof taskStore?.updateTask === "function" ? taskStore.updateTask : null;
|
|
2911
|
+
if (!addTaskFn || !updateTaskFn) {
|
|
2912
|
+
throw new Error("Internal task store import is unavailable");
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
const tasks = extractImportedTaskList(body);
|
|
2916
|
+
if (!Array.isArray(tasks)) {
|
|
2917
|
+
throw new Error("JSON must contain an array of tasks (top-level or under 'tasks' key)");
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
const mode = String(body?.mode || "merge").trim().toLowerCase();
|
|
2921
|
+
if (!["merge", "upsert"].includes(mode)) {
|
|
2922
|
+
throw new Error("Only merge/upsert import mode is supported");
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
const existingById = new Map(
|
|
2926
|
+
getAllInternalTasks()
|
|
2927
|
+
.filter((task) => task && task.id)
|
|
2928
|
+
.map((task) => [String(task.id), task]),
|
|
2929
|
+
);
|
|
2930
|
+
const summary = {
|
|
2931
|
+
total: tasks.length,
|
|
2932
|
+
created: 0,
|
|
2933
|
+
updated: 0,
|
|
2934
|
+
failed: 0,
|
|
2935
|
+
};
|
|
2936
|
+
const results = [];
|
|
2937
|
+
|
|
2938
|
+
for (const entry of tasks) {
|
|
2939
|
+
const task = entry && typeof entry === "object" && !Array.isArray(entry) ? entry : null;
|
|
2940
|
+
const taskId = String(task?.id || "").trim();
|
|
2941
|
+
if (!task || !taskId) {
|
|
2942
|
+
summary.failed += 1;
|
|
2943
|
+
results.push({ id: taskId || null, status: "failed", error: "task.id is required" });
|
|
2944
|
+
continue;
|
|
2945
|
+
}
|
|
2946
|
+
if (!String(task.title || "").trim()) {
|
|
2947
|
+
summary.failed += 1;
|
|
2948
|
+
results.push({ id: taskId, status: "failed", error: "task.title is required" });
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
try {
|
|
2953
|
+
if (existingById.has(taskId)) {
|
|
2954
|
+
updateTaskFn(taskId, task);
|
|
2955
|
+
summary.updated += 1;
|
|
2956
|
+
results.push({ id: taskId, status: "updated" });
|
|
2957
|
+
} else {
|
|
2958
|
+
addTaskFn(task);
|
|
2959
|
+
existingById.set(taskId, task);
|
|
2960
|
+
summary.created += 1;
|
|
2961
|
+
results.push({ id: taskId, status: "created" });
|
|
2962
|
+
}
|
|
2963
|
+
} catch (err) {
|
|
2964
|
+
summary.failed += 1;
|
|
2965
|
+
results.push({
|
|
2966
|
+
id: taskId,
|
|
2967
|
+
status: "failed",
|
|
2968
|
+
error: err?.message || "import failed",
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
if (summary.created > 0 || summary.updated > 0) {
|
|
2974
|
+
const waitForWritesFn = typeof taskStore?.waitForStoreWrites === "function"
|
|
2975
|
+
? taskStore.waitForStoreWrites
|
|
2976
|
+
: null;
|
|
2977
|
+
if (waitForWritesFn) {
|
|
2978
|
+
await waitForWritesFn();
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
return {
|
|
2983
|
+
backend: "internal",
|
|
2984
|
+
mode,
|
|
2985
|
+
summary,
|
|
2986
|
+
results,
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2478
2990
|
function readConfigDocument() {
|
|
2479
2991
|
const configPath = resolveConfigPath();
|
|
2480
2992
|
let configData = { $schema: "./bosun.schema.json" };
|
|
@@ -2925,6 +3437,7 @@ async function applySharedStateToTasks(tasks) {
|
|
|
2925
3437
|
function mapTaskStatusToBoardColumn(status) {
|
|
2926
3438
|
const normalized = String(status || "").trim().toLowerCase();
|
|
2927
3439
|
if (normalized === "draft") return "draft";
|
|
3440
|
+
if (["blocked", "error", "failed"].includes(normalized)) return "blocked";
|
|
2928
3441
|
if (["inprogress", "in-progress", "working", "active", "assigned", "running"].includes(normalized)) return "inProgress";
|
|
2929
3442
|
if (["inreview", "in-review", "review", "pr-open", "pr-review"].includes(normalized)) return "inReview";
|
|
2930
3443
|
if (["done", "completed", "closed", "merged", "cancelled"].includes(normalized)) return "done";
|
|
@@ -3017,6 +3530,87 @@ function resolveActiveWorkspaceExecutionContext() {
|
|
|
3017
3530
|
};
|
|
3018
3531
|
}
|
|
3019
3532
|
|
|
3533
|
+
function resolveDefaultRepositoryForWorkspaceContext(workspaceContext = {}) {
|
|
3534
|
+
const configDir = resolveUiConfigDir();
|
|
3535
|
+
if (!configDir) return "";
|
|
3536
|
+
const listed = listManagedWorkspaces(configDir, { repoRoot });
|
|
3537
|
+
const workspaceId = String(workspaceContext?.workspaceId || "").trim().toLowerCase();
|
|
3538
|
+
const workspace =
|
|
3539
|
+
(workspaceId
|
|
3540
|
+
? listed.find((entry) => String(entry?.id || "").trim().toLowerCase() === workspaceId)
|
|
3541
|
+
: null) ||
|
|
3542
|
+
getActiveManagedWorkspace(configDir) ||
|
|
3543
|
+
listed[0] ||
|
|
3544
|
+
null;
|
|
3545
|
+
if (!workspace) return "";
|
|
3546
|
+
return String(
|
|
3547
|
+
workspace?.activeRepo ||
|
|
3548
|
+
workspace?.repos?.find((repo) => repo?.primary)?.name ||
|
|
3549
|
+
workspace?.repos?.[0]?.name ||
|
|
3550
|
+
"",
|
|
3551
|
+
).trim();
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
async function resolveDefaultKanbanProjectId(adapter, requestedProjectId = "") {
|
|
3555
|
+
const explicitProjectId = String(requestedProjectId || "").trim();
|
|
3556
|
+
if (explicitProjectId) return explicitProjectId;
|
|
3557
|
+
if (!adapter || typeof adapter.listProjects !== "function") return "";
|
|
3558
|
+
try {
|
|
3559
|
+
const projects = await adapter.listProjects();
|
|
3560
|
+
return String(projects?.[0]?.id || projects?.[0]?.project_id || "").trim();
|
|
3561
|
+
} catch {
|
|
3562
|
+
return "";
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
function createManualFlowTaskManager(workspaceContext = {}, opts = {}) {
|
|
3567
|
+
return {
|
|
3568
|
+
async createTask(spec = {}) {
|
|
3569
|
+
const title = String(spec?.title || "").trim();
|
|
3570
|
+
if (!title) throw new Error("title is required");
|
|
3571
|
+
|
|
3572
|
+
const adapter = getKanbanAdapter();
|
|
3573
|
+
const projectId = await resolveDefaultKanbanProjectId(
|
|
3574
|
+
adapter,
|
|
3575
|
+
opts?.projectId || spec?.projectId || spec?.project || "",
|
|
3576
|
+
);
|
|
3577
|
+
const labels = normalizeTagsInput(spec?.labels || spec?.tags);
|
|
3578
|
+
const workspace = String(
|
|
3579
|
+
spec?.workspace || opts?.workspaceId || workspaceContext?.workspaceId || "",
|
|
3580
|
+
).trim();
|
|
3581
|
+
const repository = String(
|
|
3582
|
+
spec?.repository ||
|
|
3583
|
+
spec?.meta?.repository ||
|
|
3584
|
+
opts?.repository ||
|
|
3585
|
+
resolveDefaultRepositoryForWorkspaceContext(workspaceContext),
|
|
3586
|
+
).trim();
|
|
3587
|
+
const repositories = Array.isArray(spec?.repositories)
|
|
3588
|
+
? spec.repositories.filter((value) => typeof value === "string" && value.trim())
|
|
3589
|
+
: [];
|
|
3590
|
+
const taskPayload = {
|
|
3591
|
+
title,
|
|
3592
|
+
description: String(spec?.description || ""),
|
|
3593
|
+
status: String(spec?.status || "todo").trim() || "todo",
|
|
3594
|
+
priority: spec?.priority || undefined,
|
|
3595
|
+
...(workspace ? { workspace } : {}),
|
|
3596
|
+
...(repository ? { repository } : {}),
|
|
3597
|
+
...(repositories.length ? { repositories } : {}),
|
|
3598
|
+
...(labels.length ? { labels, tags: labels } : {}),
|
|
3599
|
+
meta: {
|
|
3600
|
+
...(workspace ? { workspace } : {}),
|
|
3601
|
+
...(repository ? { repository } : {}),
|
|
3602
|
+
...(repositories.length ? { repositories } : {}),
|
|
3603
|
+
...(labels.length ? { tags: labels } : {}),
|
|
3604
|
+
manualFlowTemplateId: String(opts?.templateId || "").trim() || undefined,
|
|
3605
|
+
...(spec?.meta && typeof spec.meta === "object" ? spec.meta : {}),
|
|
3606
|
+
},
|
|
3607
|
+
};
|
|
3608
|
+
const createdRaw = await adapter.createTask(projectId, taskPayload);
|
|
3609
|
+
return withTaskMetadataTopLevel(createdRaw);
|
|
3610
|
+
},
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3020
3614
|
function resolveWorkspaceContextById(workspaceId = "") {
|
|
3021
3615
|
const requestedId = String(workspaceId || "").trim().toLowerCase();
|
|
3022
3616
|
if (!requestedId) return resolveActiveWorkspaceExecutionContext();
|
|
@@ -6371,6 +6965,53 @@ function normalizeJsonResponsePayload(payload) {
|
|
|
6371
6965
|
return scrubStackTraces(payload);
|
|
6372
6966
|
}
|
|
6373
6967
|
|
|
6968
|
+
function makeJsonSafe(value, options = {}) {
|
|
6969
|
+
const depth = Number.isFinite(options.depth) ? options.depth : 0;
|
|
6970
|
+
const maxDepth = Number.isFinite(options.maxDepth) ? options.maxDepth : 5;
|
|
6971
|
+
const seen = options.seen instanceof WeakSet ? options.seen : new WeakSet();
|
|
6972
|
+
|
|
6973
|
+
if (value == null) return value;
|
|
6974
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
6975
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
|
|
6976
|
+
if (typeof value === "bigint") return String(value);
|
|
6977
|
+
if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") {
|
|
6978
|
+
return undefined;
|
|
6979
|
+
}
|
|
6980
|
+
if (value instanceof Date) {
|
|
6981
|
+
return Number.isFinite(value.getTime()) ? value.toISOString() : String(value);
|
|
6982
|
+
}
|
|
6983
|
+
if (value instanceof Error) {
|
|
6984
|
+
const errorValue = {
|
|
6985
|
+
name: String(value.name || "Error"),
|
|
6986
|
+
message: String(value.message || ""),
|
|
6987
|
+
};
|
|
6988
|
+
if (value.code != null) errorValue.code = String(value.code);
|
|
6989
|
+
return errorValue;
|
|
6990
|
+
}
|
|
6991
|
+
if (depth >= maxDepth) return "[Truncated]";
|
|
6992
|
+
if (typeof value !== "object") return String(value);
|
|
6993
|
+
if (seen.has(value)) return "[Circular]";
|
|
6994
|
+
|
|
6995
|
+
seen.add(value);
|
|
6996
|
+
try {
|
|
6997
|
+
if (Array.isArray(value)) {
|
|
6998
|
+
return value.map((entry) => {
|
|
6999
|
+
const normalized = makeJsonSafe(entry, { depth: depth + 1, maxDepth, seen });
|
|
7000
|
+
return normalized === undefined ? null : normalized;
|
|
7001
|
+
});
|
|
7002
|
+
}
|
|
7003
|
+
|
|
7004
|
+
const out = {};
|
|
7005
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
7006
|
+
const normalized = makeJsonSafe(entry, { depth: depth + 1, maxDepth, seen });
|
|
7007
|
+
if (normalized !== undefined) out[key] = normalized;
|
|
7008
|
+
}
|
|
7009
|
+
return out;
|
|
7010
|
+
} finally {
|
|
7011
|
+
seen.delete(value);
|
|
7012
|
+
}
|
|
7013
|
+
}
|
|
7014
|
+
|
|
6374
7015
|
function extractSafeErrorMessage(payload) {
|
|
6375
7016
|
if (payload == null) return "Internal server error";
|
|
6376
7017
|
if (payload instanceof Error) {
|
|
@@ -6391,13 +7032,58 @@ function extractSafeErrorMessage(payload) {
|
|
|
6391
7032
|
return "Internal server error";
|
|
6392
7033
|
}
|
|
6393
7034
|
|
|
7035
|
+
function createRequestDiagnosticId() {
|
|
7036
|
+
return `req_${randomBytes(6).toString("hex")}`;
|
|
7037
|
+
}
|
|
7038
|
+
|
|
7039
|
+
function ensureResponseDiagnosticId(res) {
|
|
7040
|
+
if (!res || typeof res !== "object") return createRequestDiagnosticId();
|
|
7041
|
+
if (!res.__bosunDiagnosticId) {
|
|
7042
|
+
res.__bosunDiagnosticId = createRequestDiagnosticId();
|
|
7043
|
+
}
|
|
7044
|
+
return res.__bosunDiagnosticId;
|
|
7045
|
+
}
|
|
7046
|
+
|
|
7047
|
+
function describePayloadForErrorLog(payload, depth = 0) {
|
|
7048
|
+
if (payload instanceof Error) {
|
|
7049
|
+
const described = {
|
|
7050
|
+
name: String(payload.name || "Error"),
|
|
7051
|
+
message: String(payload.message || ""),
|
|
7052
|
+
};
|
|
7053
|
+
if (payload.stack) described.stack = String(payload.stack);
|
|
7054
|
+
if (payload.code != null) described.code = String(payload.code);
|
|
7055
|
+
if (depth < 3 && payload.cause) {
|
|
7056
|
+
described.cause = describePayloadForErrorLog(payload.cause, depth + 1);
|
|
7057
|
+
}
|
|
7058
|
+
return described;
|
|
7059
|
+
}
|
|
7060
|
+
return makeJsonSafe(payload, { maxDepth: 6 });
|
|
7061
|
+
}
|
|
7062
|
+
|
|
7063
|
+
function logJsonFailure(res, statusCode, payload, diagnosticId) {
|
|
7064
|
+
const requestContext = res?.__bosunRequestContext || {};
|
|
7065
|
+
console.error("[ui-server] request failed", {
|
|
7066
|
+
diagnosticId,
|
|
7067
|
+
statusCode,
|
|
7068
|
+
method: requestContext.method || null,
|
|
7069
|
+
path: requestContext.path || null,
|
|
7070
|
+
query: requestContext.query || "",
|
|
7071
|
+
payload: describePayloadForErrorLog(payload),
|
|
7072
|
+
});
|
|
7073
|
+
}
|
|
7074
|
+
|
|
6394
7075
|
function jsonResponse(res, statusCode, payload) {
|
|
7076
|
+
const diagnosticId = statusCode >= 500 ? ensureResponseDiagnosticId(res) : null;
|
|
7077
|
+
if (statusCode >= 500) {
|
|
7078
|
+
logJsonFailure(res, statusCode, payload, diagnosticId);
|
|
7079
|
+
}
|
|
6395
7080
|
const normalizedPayload = normalizeJsonResponsePayload(payload);
|
|
6396
7081
|
const safePayload =
|
|
6397
7082
|
statusCode >= 500
|
|
6398
7083
|
? {
|
|
6399
7084
|
ok: false,
|
|
6400
7085
|
error: extractSafeErrorMessage(normalizedPayload),
|
|
7086
|
+
diagnosticId,
|
|
6401
7087
|
}
|
|
6402
7088
|
: normalizedPayload;
|
|
6403
7089
|
const body = JSON.stringify(safePayload, null, 2);
|
|
@@ -6505,7 +7191,7 @@ function normalizeCanStartResult(result, { override = false } = {}) {
|
|
|
6505
7191
|
missingDependencyTaskIds,
|
|
6506
7192
|
blockingSprintIds,
|
|
6507
7193
|
blockingEpicIds,
|
|
6508
|
-
raw,
|
|
7194
|
+
raw: makeJsonSafe(raw),
|
|
6509
7195
|
};
|
|
6510
7196
|
}
|
|
6511
7197
|
|
|
@@ -6658,6 +7344,15 @@ async function getGlobalDagData() {
|
|
|
6658
7344
|
};
|
|
6659
7345
|
}
|
|
6660
7346
|
|
|
7347
|
+
async function organizeDagData(options = {}) {
|
|
7348
|
+
const organizeResult = await callTaskStoreFunction(TASK_STORE_DAG_EXPORTS.organize, [options]);
|
|
7349
|
+
if (!organizeResult.found) return null;
|
|
7350
|
+
return {
|
|
7351
|
+
source: `task-store.${organizeResult.found}`,
|
|
7352
|
+
data: organizeResult.value,
|
|
7353
|
+
};
|
|
7354
|
+
}
|
|
7355
|
+
|
|
6661
7356
|
function normalizeTaskComments(comments = []) {
|
|
6662
7357
|
if (!Array.isArray(comments)) return [];
|
|
6663
7358
|
return comments
|
|
@@ -7240,6 +7935,129 @@ function withTaskRuntimeSnapshot(task) {
|
|
|
7240
7935
|
};
|
|
7241
7936
|
}
|
|
7242
7937
|
|
|
7938
|
+
function normalizeTaskDiagnosticText(value) {
|
|
7939
|
+
const text = String(value || "").trim();
|
|
7940
|
+
return text ? text.replace(/\s+/g, " ") : "";
|
|
7941
|
+
}
|
|
7942
|
+
|
|
7943
|
+
function buildTaskStableCause(task, supervisorDiagnostics = null) {
|
|
7944
|
+
const lastError = normalizeTaskDiagnosticText(task?.lastError || "");
|
|
7945
|
+
const blockedReason = normalizeTaskDiagnosticText(task?.blockedReason || "");
|
|
7946
|
+
const errorPattern = String(task?.errorPattern || "").trim().toLowerCase();
|
|
7947
|
+
const apiErrorRecovery = supervisorDiagnostics?.apiErrorRecovery || null;
|
|
7948
|
+
const apiSignature = normalizeTaskDiagnosticText(apiErrorRecovery?.signature || "");
|
|
7949
|
+
const lastErrorLower = lastError.toLowerCase();
|
|
7950
|
+
const blockedReasonLower = blockedReason.toLowerCase();
|
|
7951
|
+
|
|
7952
|
+
if (lastErrorLower.includes("codex resume timeout")) {
|
|
7953
|
+
return {
|
|
7954
|
+
code: "codex_resume_timeout",
|
|
7955
|
+
title: "Codex resume timed out",
|
|
7956
|
+
severity: "warning",
|
|
7957
|
+
summary: "Bosun timed out while resuming a cached Codex thread and will start fresh on the next attempt.",
|
|
7958
|
+
};
|
|
7959
|
+
}
|
|
7960
|
+
if (
|
|
7961
|
+
lastErrorLower.includes("invalid_encrypted_content") ||
|
|
7962
|
+
lastErrorLower.includes("state db missing rollout path") ||
|
|
7963
|
+
lastErrorLower.includes("could not be verified") ||
|
|
7964
|
+
lastErrorLower.includes("tool_call_id")
|
|
7965
|
+
) {
|
|
7966
|
+
return {
|
|
7967
|
+
code: "codex_resume_corrupted_state",
|
|
7968
|
+
title: "Codex resume state is corrupted",
|
|
7969
|
+
severity: "error",
|
|
7970
|
+
summary: "Bosun detected poisoned Codex thread metadata and will discard the cached resume state.",
|
|
7971
|
+
};
|
|
7972
|
+
}
|
|
7973
|
+
if (errorPattern === "rate_limit") {
|
|
7974
|
+
return {
|
|
7975
|
+
code: "agent_rate_limit",
|
|
7976
|
+
title: "Agent is rate limited",
|
|
7977
|
+
severity: "warning",
|
|
7978
|
+
summary: "The assigned agent hit a rate limit and Bosun is waiting before retrying.",
|
|
7979
|
+
};
|
|
7980
|
+
}
|
|
7981
|
+
if (errorPattern === "token_overflow") {
|
|
7982
|
+
return {
|
|
7983
|
+
code: "token_overflow",
|
|
7984
|
+
title: "Context window exhausted",
|
|
7985
|
+
severity: "error",
|
|
7986
|
+
summary: "The current task exceeded the model context budget and needs a smaller prompt or a fresh session.",
|
|
7987
|
+
};
|
|
7988
|
+
}
|
|
7989
|
+
if (errorPattern === "api_error" || apiErrorRecovery) {
|
|
7990
|
+
return {
|
|
7991
|
+
code: Number(apiErrorRecovery?.cooldownUntil || 0) > Date.now()
|
|
7992
|
+
? "api_error_cooldown"
|
|
7993
|
+
: "api_error_recovery",
|
|
7994
|
+
title: "Transient API failure",
|
|
7995
|
+
severity: "warning",
|
|
7996
|
+
summary: "Bosun detected a backend API failure and is applying the task-level recovery ladder before escalating.",
|
|
7997
|
+
};
|
|
7998
|
+
}
|
|
7999
|
+
if (blockedReason && blockedReasonLower.includes("dependency")) {
|
|
8000
|
+
return {
|
|
8001
|
+
code: "dependency_blocked",
|
|
8002
|
+
title: "Dependency is still blocked",
|
|
8003
|
+
severity: "warning",
|
|
8004
|
+
summary: "Bosun is holding this task until one or more dependencies finish.",
|
|
8005
|
+
};
|
|
8006
|
+
}
|
|
8007
|
+
if (blockedReason) {
|
|
8008
|
+
return {
|
|
8009
|
+
code: "task_blocked",
|
|
8010
|
+
title: "Task is blocked",
|
|
8011
|
+
severity: "warning",
|
|
8012
|
+
summary: "Bosun recorded a blocking condition for this task and will not dispatch it until the condition clears.",
|
|
8013
|
+
};
|
|
8014
|
+
}
|
|
8015
|
+
if (lastError || apiSignature) {
|
|
8016
|
+
return {
|
|
8017
|
+
code: "agent_runtime_error",
|
|
8018
|
+
title: "Agent runtime error",
|
|
8019
|
+
severity: "error",
|
|
8020
|
+
summary: "Bosun recorded an agent-side runtime failure for this task.",
|
|
8021
|
+
};
|
|
8022
|
+
}
|
|
8023
|
+
return null;
|
|
8024
|
+
}
|
|
8025
|
+
|
|
8026
|
+
function buildTaskDiagnostics(task, supervisorDiagnostics = null) {
|
|
8027
|
+
if (!task || typeof task !== "object") return null;
|
|
8028
|
+
const apiErrorRecovery = supervisorDiagnostics?.apiErrorRecovery
|
|
8029
|
+
? makeJsonSafe(supervisorDiagnostics.apiErrorRecovery, { maxDepth: 4 })
|
|
8030
|
+
: null;
|
|
8031
|
+
const diagnostics = {
|
|
8032
|
+
stableCause: buildTaskStableCause(task, supervisorDiagnostics),
|
|
8033
|
+
lastError: normalizeTaskDiagnosticText(task?.lastError || "") || null,
|
|
8034
|
+
errorPattern: normalizeTaskDiagnosticText(task?.errorPattern || "") || null,
|
|
8035
|
+
blockedReason: normalizeTaskDiagnosticText(task?.blockedReason || "") || null,
|
|
8036
|
+
cooldownUntil: task?.cooldownUntil || apiErrorRecovery?.cooldownUntil || null,
|
|
8037
|
+
supervisor: supervisorDiagnostics
|
|
8038
|
+
? {
|
|
8039
|
+
interventionCount: Number(supervisorDiagnostics.interventionCount || 0),
|
|
8040
|
+
lastIntervention: supervisorDiagnostics.lastIntervention || null,
|
|
8041
|
+
lastDecision: supervisorDiagnostics.lastDecision
|
|
8042
|
+
? makeJsonSafe(supervisorDiagnostics.lastDecision, { maxDepth: 3 })
|
|
8043
|
+
: null,
|
|
8044
|
+
apiErrorRecovery,
|
|
8045
|
+
}
|
|
8046
|
+
: null,
|
|
8047
|
+
};
|
|
8048
|
+
if (
|
|
8049
|
+
!diagnostics.stableCause &&
|
|
8050
|
+
!diagnostics.lastError &&
|
|
8051
|
+
!diagnostics.errorPattern &&
|
|
8052
|
+
!diagnostics.blockedReason &&
|
|
8053
|
+
!diagnostics.cooldownUntil &&
|
|
8054
|
+
!diagnostics.supervisor
|
|
8055
|
+
) {
|
|
8056
|
+
return null;
|
|
8057
|
+
}
|
|
8058
|
+
return diagnostics;
|
|
8059
|
+
}
|
|
8060
|
+
|
|
7243
8061
|
async function maybeStartTaskFromLifecycleAction({
|
|
7244
8062
|
taskId,
|
|
7245
8063
|
updatedTask,
|
|
@@ -9056,12 +9874,12 @@ function runGit(args, timeoutMs = 10000) {
|
|
|
9056
9874
|
return String(res.stdout || "").trim();
|
|
9057
9875
|
}
|
|
9058
9876
|
|
|
9059
|
-
async function readJsonBody(req) {
|
|
9877
|
+
async function readJsonBody(req, maxBytes = 1_000_000) {
|
|
9060
9878
|
return new Promise((resolveBody, rejectBody) => {
|
|
9061
9879
|
let data = "";
|
|
9062
9880
|
req.on("data", (chunk) => {
|
|
9063
9881
|
data += chunk;
|
|
9064
|
-
if (data.length >
|
|
9882
|
+
if (data.length > maxBytes) {
|
|
9065
9883
|
rejectBody(new Error("payload too large"));
|
|
9066
9884
|
req.destroy();
|
|
9067
9885
|
}
|
|
@@ -9279,6 +10097,14 @@ function buildTaskMetadataPatch(input = {}) {
|
|
|
9279
10097
|
}
|
|
9280
10098
|
}
|
|
9281
10099
|
|
|
10100
|
+
if (hasOwn(input, "blockedReason")) {
|
|
10101
|
+
const blockedReason = normalizeOptionalStringInput(input?.blockedReason);
|
|
10102
|
+
if (blockedReason) {
|
|
10103
|
+
topLevel.blockedReason = blockedReason;
|
|
10104
|
+
meta.blockedReason = blockedReason;
|
|
10105
|
+
}
|
|
10106
|
+
}
|
|
10107
|
+
|
|
9282
10108
|
return { topLevel, meta };
|
|
9283
10109
|
}
|
|
9284
10110
|
|
|
@@ -9464,13 +10290,105 @@ async function readJsonlTail(filePath, maxLines = 2000) {
|
|
|
9464
10290
|
.filter(Boolean);
|
|
9465
10291
|
}
|
|
9466
10292
|
|
|
10293
|
+
function getEntryTimestamp(entry) {
|
|
10294
|
+
const numericCandidates = [
|
|
10295
|
+
entry?.endedAt,
|
|
10296
|
+
entry?.startedAt,
|
|
10297
|
+
];
|
|
10298
|
+
for (const candidate of numericCandidates) {
|
|
10299
|
+
const parsed = Number(candidate);
|
|
10300
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
10301
|
+
}
|
|
10302
|
+
|
|
10303
|
+
const isoCandidates = [
|
|
10304
|
+
entry?.timestamp,
|
|
10305
|
+
entry?.recordedAt,
|
|
10306
|
+
entry?.updatedAt,
|
|
10307
|
+
entry?.createdAt,
|
|
10308
|
+
];
|
|
10309
|
+
for (const candidate of isoCandidates) {
|
|
10310
|
+
const parsed = Date.parse(candidate || "");
|
|
10311
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
10312
|
+
}
|
|
10313
|
+
return Number.NaN;
|
|
10314
|
+
}
|
|
10315
|
+
|
|
10316
|
+
function getEntryDayKey(entry, fallbackTs = Number.NaN) {
|
|
10317
|
+
const isoCandidates = [
|
|
10318
|
+
entry?.timestamp,
|
|
10319
|
+
entry?.recordedAt,
|
|
10320
|
+
entry?.updatedAt,
|
|
10321
|
+
entry?.createdAt,
|
|
10322
|
+
];
|
|
10323
|
+
for (const candidate of isoCandidates) {
|
|
10324
|
+
const value = String(candidate || "").trim();
|
|
10325
|
+
if (value.length >= 10) return value.slice(0, 10);
|
|
10326
|
+
}
|
|
10327
|
+
const ts = Number.isFinite(fallbackTs) ? fallbackTs : getEntryTimestamp(entry);
|
|
10328
|
+
if (!Number.isFinite(ts)) return "";
|
|
10329
|
+
return new Date(ts).toISOString().slice(0, 10);
|
|
10330
|
+
}
|
|
10331
|
+
|
|
9467
10332
|
function withinDays(entry, days) {
|
|
9468
10333
|
if (!days) return true;
|
|
9469
|
-
const ts =
|
|
10334
|
+
const ts = getEntryTimestamp(entry);
|
|
9470
10335
|
if (!Number.isFinite(ts)) return true;
|
|
9471
10336
|
return ts >= Date.now() - days * 24 * 60 * 60 * 1000;
|
|
9472
10337
|
}
|
|
9473
10338
|
|
|
10339
|
+
async function readCompletedSessionEntries(maxLines = 100_000) {
|
|
10340
|
+
const sessionLogPath = resolve(repoRoot, ".cache", "session-accumulator.jsonl");
|
|
10341
|
+
const entries = await readJsonlTail(sessionLogPath, maxLines);
|
|
10342
|
+
return {
|
|
10343
|
+
sessionLogPath,
|
|
10344
|
+
entries: entries.filter((entry) => String(entry?.type || "completed_session") === "completed_session"),
|
|
10345
|
+
};
|
|
10346
|
+
}
|
|
10347
|
+
|
|
10348
|
+
function roundMetric(value, precision = 6) {
|
|
10349
|
+
const numeric = Number(value);
|
|
10350
|
+
if (!Number.isFinite(numeric)) return 0;
|
|
10351
|
+
return Number(numeric.toFixed(precision));
|
|
10352
|
+
}
|
|
10353
|
+
|
|
10354
|
+
const SHREDDING_ESTIMATED_CHARS_PER_TOKEN = 4;
|
|
10355
|
+
|
|
10356
|
+
function estimateTokensFromChars(chars) {
|
|
10357
|
+
const numeric = Number(chars);
|
|
10358
|
+
if (!Number.isFinite(numeric) || numeric <= 0) return 0;
|
|
10359
|
+
return Math.max(0, Math.round(numeric / SHREDDING_ESTIMATED_CHARS_PER_TOKEN));
|
|
10360
|
+
}
|
|
10361
|
+
|
|
10362
|
+
function summarizeObservedSessionCostModel(entries = []) {
|
|
10363
|
+
let totalCostUsd = 0;
|
|
10364
|
+
let totalTokens = 0;
|
|
10365
|
+
let totalInputTokens = 0;
|
|
10366
|
+
let pricedSessions = 0;
|
|
10367
|
+
for (const entry of entries) {
|
|
10368
|
+
const costUsd = numberOrZero(entry?.costUsd);
|
|
10369
|
+
const tokenCount = numberOrZero(entry?.tokenCount);
|
|
10370
|
+
const inputTokens = numberOrZero(entry?.inputTokens);
|
|
10371
|
+
if (costUsd <= 0 || tokenCount <= 0) continue;
|
|
10372
|
+
totalCostUsd += costUsd;
|
|
10373
|
+
totalTokens += tokenCount;
|
|
10374
|
+
totalInputTokens += inputTokens;
|
|
10375
|
+
pricedSessions += 1;
|
|
10376
|
+
}
|
|
10377
|
+
const blendedCostPerToken = totalCostUsd > 0 && totalTokens > 0
|
|
10378
|
+
? totalCostUsd / totalTokens
|
|
10379
|
+
: null;
|
|
10380
|
+
return {
|
|
10381
|
+
pricedSessions,
|
|
10382
|
+
totalCostUsd: roundMetric(totalCostUsd),
|
|
10383
|
+
totalTokens,
|
|
10384
|
+
totalInputTokens,
|
|
10385
|
+
blendedCostPerToken,
|
|
10386
|
+
blendedCostPerMillionTokensUsd: blendedCostPerToken != null
|
|
10387
|
+
? roundMetric(blendedCostPerToken * 1_000_000, 4)
|
|
10388
|
+
: null,
|
|
10389
|
+
};
|
|
10390
|
+
}
|
|
10391
|
+
|
|
9474
10392
|
function summarizeTelemetry(metrics, days) {
|
|
9475
10393
|
const filtered = metrics.filter((m) => withinDays(m, days));
|
|
9476
10394
|
if (filtered.length === 0) return null;
|
|
@@ -9562,7 +10480,10 @@ function isEffectiveShreddingEvent(event) {
|
|
|
9562
10480
|
async function buildUsageAnalytics(days) {
|
|
9563
10481
|
const logDir = resolveAgentWorkLogDir();
|
|
9564
10482
|
const streamPath = resolve(logDir, "agent-work-stream.jsonl");
|
|
9565
|
-
const events = await
|
|
10483
|
+
const [{ entries: completedSessions }, events] = await Promise.all([
|
|
10484
|
+
readCompletedSessionEntries(100_000),
|
|
10485
|
+
readJsonlTail(streamPath, 100_000),
|
|
10486
|
+
]);
|
|
9566
10487
|
|
|
9567
10488
|
const cutoff = days ? Date.now() - days * 24 * 60 * 60 * 1000 : 0;
|
|
9568
10489
|
|
|
@@ -9588,22 +10509,50 @@ async function buildUsageAnalytics(days) {
|
|
|
9588
10509
|
|
|
9589
10510
|
const allDates = new Set();
|
|
9590
10511
|
|
|
10512
|
+
const sessionWindow = completedSessions.filter((session) => {
|
|
10513
|
+
const ts = getEntryTimestamp(session);
|
|
10514
|
+
return !cutoff || (Number.isFinite(ts) && ts >= cutoff);
|
|
10515
|
+
});
|
|
10516
|
+
|
|
10517
|
+
if (sessionWindow.length > 0) {
|
|
10518
|
+
for (const session of sessionWindow) {
|
|
10519
|
+
const ts = getEntryTimestamp(session);
|
|
10520
|
+
if (!Number.isFinite(ts)) continue;
|
|
10521
|
+
if (ts < oldestTs) oldestTs = ts;
|
|
10522
|
+
if (ts > newestTs) newestTs = ts;
|
|
10523
|
+
const day = getEntryDayKey(session, ts);
|
|
10524
|
+
if (day) allDates.add(day);
|
|
10525
|
+
|
|
10526
|
+
agentRuns += 1;
|
|
10527
|
+
const exec = String(session.executor || session.model || "unknown").trim() || "unknown";
|
|
10528
|
+
agents.set(exec, (agents.get(exec) || 0) + 1);
|
|
10529
|
+
if (day) {
|
|
10530
|
+
(dailyAgents[day] = dailyAgents[day] || {})[exec] =
|
|
10531
|
+
(dailyAgents[day][exec] || 0) + 1;
|
|
10532
|
+
}
|
|
10533
|
+
}
|
|
10534
|
+
}
|
|
10535
|
+
|
|
10536
|
+
let streamSessionStarts = 0;
|
|
9591
10537
|
for (const e of events) {
|
|
9592
|
-
const ts =
|
|
10538
|
+
const ts = getEntryTimestamp(e);
|
|
9593
10539
|
if (!Number.isFinite(ts)) continue;
|
|
9594
10540
|
if (cutoff && ts < cutoff) continue;
|
|
9595
10541
|
if (ts < oldestTs) oldestTs = ts;
|
|
9596
10542
|
if (ts > newestTs) newestTs = ts;
|
|
9597
|
-
const day = (e
|
|
10543
|
+
const day = getEntryDayKey(e, ts);
|
|
9598
10544
|
if (day) allDates.add(day);
|
|
9599
10545
|
|
|
9600
10546
|
if (e.event_type === "session_start") {
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
(
|
|
9606
|
-
|
|
10547
|
+
streamSessionStarts += 1;
|
|
10548
|
+
if (sessionWindow.length === 0) {
|
|
10549
|
+
agentRuns++;
|
|
10550
|
+
const exec = e.executor || "unknown";
|
|
10551
|
+
agents.set(exec, (agents.get(exec) || 0) + 1);
|
|
10552
|
+
if (day) {
|
|
10553
|
+
(dailyAgents[day] = dailyAgents[day] || {})[exec] =
|
|
10554
|
+
(dailyAgents[day][exec] || 0) + 1;
|
|
10555
|
+
}
|
|
9607
10556
|
}
|
|
9608
10557
|
} else if (e.event_type === "skill_invoke") {
|
|
9609
10558
|
skillInvocations++;
|
|
@@ -9669,6 +10618,11 @@ async function buildUsageAnalytics(days) {
|
|
|
9669
10618
|
topSkills,
|
|
9670
10619
|
topMcpTools,
|
|
9671
10620
|
trend,
|
|
10621
|
+
diagnostics: {
|
|
10622
|
+
agentRunSource: sessionWindow.length > 0 ? "completed_sessions" : "session_start_events",
|
|
10623
|
+
completedSessions: sessionWindow.length,
|
|
10624
|
+
sessionStarts: streamSessionStarts,
|
|
10625
|
+
},
|
|
9672
10626
|
};
|
|
9673
10627
|
}
|
|
9674
10628
|
|
|
@@ -10391,6 +11345,44 @@ async function handleApi(req, res, url) {
|
|
|
10391
11345
|
return;
|
|
10392
11346
|
}
|
|
10393
11347
|
|
|
11348
|
+
if (path === "/api/tasks/export") {
|
|
11349
|
+
try {
|
|
11350
|
+
const adapter = getKanbanAdapter();
|
|
11351
|
+
const tasks = await listAllTasksForApi(adapter);
|
|
11352
|
+
jsonResponse(res, 200, {
|
|
11353
|
+
ok: true,
|
|
11354
|
+
data: buildTaskStateExportPayload(tasks, getKanbanBackendName()),
|
|
11355
|
+
});
|
|
11356
|
+
} catch (err) {
|
|
11357
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
11358
|
+
}
|
|
11359
|
+
return;
|
|
11360
|
+
}
|
|
11361
|
+
|
|
11362
|
+
if (path === "/api/tasks/import" && req.method === "POST") {
|
|
11363
|
+
try {
|
|
11364
|
+
const backend = getKanbanBackendName();
|
|
11365
|
+
if (backend !== "internal") {
|
|
11366
|
+
jsonResponse(res, 400, {
|
|
11367
|
+
ok: false,
|
|
11368
|
+
error: "Task state import is only supported for the internal backend.",
|
|
11369
|
+
});
|
|
11370
|
+
return;
|
|
11371
|
+
}
|
|
11372
|
+
const body = await readJsonBody(req, 10_000_000);
|
|
11373
|
+
const imported = await importInternalTaskStateSnapshot(body || {});
|
|
11374
|
+
jsonResponse(res, 200, { ok: true, data: imported });
|
|
11375
|
+
broadcastUiEvent(["tasks", "overview"], "invalidate", {
|
|
11376
|
+
reason: "task-state-imported",
|
|
11377
|
+
created: imported.summary?.created || 0,
|
|
11378
|
+
updated: imported.summary?.updated || 0,
|
|
11379
|
+
});
|
|
11380
|
+
} catch (err) {
|
|
11381
|
+
jsonResponse(res, 400, { ok: false, error: err.message });
|
|
11382
|
+
}
|
|
11383
|
+
return;
|
|
11384
|
+
}
|
|
11385
|
+
|
|
10394
11386
|
if (path === "/api/tasks") {
|
|
10395
11387
|
const status = url.searchParams.get("status") || "";
|
|
10396
11388
|
const projectId = url.searchParams.get("project") || "";
|
|
@@ -10477,6 +11469,7 @@ async function handleApi(req, res, url) {
|
|
|
10477
11469
|
const statusCounts = {
|
|
10478
11470
|
draft: 0,
|
|
10479
11471
|
backlog: 0,
|
|
11472
|
+
blocked: 0,
|
|
10480
11473
|
inProgress: 0,
|
|
10481
11474
|
inReview: 0,
|
|
10482
11475
|
done: 0,
|
|
@@ -10510,6 +11503,11 @@ async function handleApi(req, res, url) {
|
|
|
10510
11503
|
try {
|
|
10511
11504
|
const taskId =
|
|
10512
11505
|
url.searchParams.get("taskId") || url.searchParams.get("id") || "";
|
|
11506
|
+
const includeDagParam = String(url.searchParams.get("includeDag") || "").trim().toLowerCase();
|
|
11507
|
+
const includeWorkflowRunsParam = String(url.searchParams.get("includeWorkflowRuns") || "").trim().toLowerCase();
|
|
11508
|
+
const includeDag = !["0", "false", "no"].includes(includeDagParam);
|
|
11509
|
+
const includeWorkflowRuns = !["0", "false", "no"].includes(includeWorkflowRunsParam);
|
|
11510
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
10513
11511
|
if (!taskId) {
|
|
10514
11512
|
jsonResponse(res, 400, { ok: false, error: "taskId required" });
|
|
10515
11513
|
return;
|
|
@@ -10519,25 +11517,55 @@ async function handleApi(req, res, url) {
|
|
|
10519
11517
|
const enriched = await applySharedStateToTasks(task ? [task] : []);
|
|
10520
11518
|
let detailTask = enriched[0] || null;
|
|
10521
11519
|
if (detailTask) {
|
|
10522
|
-
const workflowRuns =
|
|
10523
|
-
|
|
11520
|
+
const workflowRuns = includeWorkflowRuns
|
|
11521
|
+
? await collectWorkflowRunsForTask(detailTask.id, url, 40)
|
|
11522
|
+
: [];
|
|
11523
|
+
const mergedWorkflowRuns = includeWorkflowRuns
|
|
11524
|
+
? mergeTaskWorkflowRuns(detailTask.workflowRuns, workflowRuns, 80)
|
|
11525
|
+
: Array.isArray(detailTask.workflowRuns)
|
|
11526
|
+
? detailTask.workflowRuns
|
|
11527
|
+
: [];
|
|
10524
11528
|
detailTask.workflowRuns = mergedWorkflowRuns;
|
|
11529
|
+
const canStart = await evaluateTaskCanStart({
|
|
11530
|
+
taskId: detailTask.id,
|
|
11531
|
+
task: detailTask,
|
|
11532
|
+
reqUrl: url,
|
|
11533
|
+
adapter,
|
|
11534
|
+
});
|
|
11535
|
+
const supervisor = typeof uiDeps.getAgentSupervisor === "function"
|
|
11536
|
+
? uiDeps.getAgentSupervisor()
|
|
11537
|
+
: null;
|
|
11538
|
+
const supervisorDiagnostics = typeof supervisor?.getTaskDiagnostics === "function"
|
|
11539
|
+
? supervisor.getTaskDiagnostics(detailTask.id)
|
|
11540
|
+
: null;
|
|
10525
11541
|
|
|
10526
11542
|
const sprintId = resolveTaskSprintId(detailTask);
|
|
10527
|
-
const sprintDag = sprintId ? await getSprintDagData(sprintId) : null;
|
|
10528
|
-
const globalDag = await getGlobalDagData();
|
|
11543
|
+
const sprintDag = includeDag && sprintId ? await getSprintDagData(sprintId) : null;
|
|
11544
|
+
const globalDag = includeDag ? await getGlobalDagData() : null;
|
|
11545
|
+
const blockedContext = buildTaskBlockedContext(detailTask, {
|
|
11546
|
+
canStart,
|
|
11547
|
+
workflowRuns: mergedWorkflowRuns,
|
|
11548
|
+
workspaceDir: workspaceContext?.workspaceDir || repoRoot,
|
|
11549
|
+
});
|
|
11550
|
+
const diagnostics = buildTaskDiagnostics(detailTask, supervisorDiagnostics);
|
|
10529
11551
|
|
|
10530
11552
|
detailTask.meta = {
|
|
10531
11553
|
...(detailTask.meta || {}),
|
|
10532
11554
|
workflowRuns: mergedWorkflowRuns,
|
|
10533
11555
|
historyCount: Array.isArray(detailTask.statusHistory) ? detailTask.statusHistory.length : 0,
|
|
10534
11556
|
timelineCount: Array.isArray(detailTask.timeline) ? detailTask.timeline.length : 0,
|
|
11557
|
+
canStart,
|
|
11558
|
+
blockedContext,
|
|
11559
|
+
...(diagnostics ? { diagnostics } : {}),
|
|
10535
11560
|
...(sprintId ? { sprintId } : {}),
|
|
10536
11561
|
...(sprintDag ? { sprintDag: sprintDag.data } : {}),
|
|
10537
11562
|
...(globalDag ? { dagOfDags: globalDag.data } : {}),
|
|
10538
11563
|
};
|
|
10539
11564
|
if (sprintDag) detailTask.sprintDag = sprintDag.data;
|
|
10540
11565
|
if (globalDag) detailTask.dagOfDags = globalDag.data;
|
|
11566
|
+
detailTask.canStart = canStart;
|
|
11567
|
+
detailTask.blockedContext = blockedContext;
|
|
11568
|
+
if (diagnostics) detailTask.diagnostics = diagnostics;
|
|
10541
11569
|
detailTask = withTaskRuntimeSnapshot(detailTask);
|
|
10542
11570
|
}
|
|
10543
11571
|
jsonResponse(res, 200, { ok: true, data: detailTask });
|
|
@@ -11488,6 +12516,41 @@ async function handleApi(req, res, url) {
|
|
|
11488
12516
|
}
|
|
11489
12517
|
return;
|
|
11490
12518
|
}
|
|
12519
|
+
|
|
12520
|
+
if (path === "/api/tasks/dag/organize" && req.method === "POST") {
|
|
12521
|
+
try {
|
|
12522
|
+
const body = await readJsonBody(req);
|
|
12523
|
+
const sprintId = String(body?.sprintId || body?.sprint || "").trim();
|
|
12524
|
+
const organizeOptions = {
|
|
12525
|
+
...(sprintId ? { sprintId } : {}),
|
|
12526
|
+
...(body?.applyDependencySuggestions != null
|
|
12527
|
+
? { applyDependencySuggestions: Boolean(body.applyDependencySuggestions) }
|
|
12528
|
+
: {}),
|
|
12529
|
+
...(body?.syncEpicDependencies != null
|
|
12530
|
+
? { syncEpicDependencies: Boolean(body.syncEpicDependencies) }
|
|
12531
|
+
: {}),
|
|
12532
|
+
};
|
|
12533
|
+
const organized = await organizeDagData(organizeOptions);
|
|
12534
|
+
if (!organized) {
|
|
12535
|
+
jsonResponse(res, 501, { ok: false, error: "DAG organize API is unavailable." });
|
|
12536
|
+
return;
|
|
12537
|
+
}
|
|
12538
|
+
jsonResponse(res, 200, {
|
|
12539
|
+
ok: true,
|
|
12540
|
+
sprintId: sprintId || null,
|
|
12541
|
+
source: organized.source,
|
|
12542
|
+
data: organized.data,
|
|
12543
|
+
suggestions: Array.isArray(organized.data?.suggestions) ? organized.data.suggestions : [],
|
|
12544
|
+
});
|
|
12545
|
+
broadcastUiEvent(["tasks", "overview"], "invalidate", {
|
|
12546
|
+
reason: "dag-organized",
|
|
12547
|
+
sprintId: sprintId || null,
|
|
12548
|
+
});
|
|
12549
|
+
} catch (err) {
|
|
12550
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
12551
|
+
}
|
|
12552
|
+
return;
|
|
12553
|
+
}
|
|
11491
12554
|
if (path === "/api/tasks/attachments/upload" && req.method === "POST") {
|
|
11492
12555
|
try {
|
|
11493
12556
|
const { fields, files } = await readMultipartForm(req);
|
|
@@ -11825,11 +12888,20 @@ async function handleApi(req, res, url) {
|
|
|
11825
12888
|
const tagsProvided = hasOwn(body, "tags");
|
|
11826
12889
|
const tags = tagsProvided ? normalizeTagsInput(body?.tags) : undefined;
|
|
11827
12890
|
const draftProvided = hasOwn(body, "draft");
|
|
12891
|
+
const blockedReasonProvided = hasOwn(body, "blockedReason");
|
|
12892
|
+
const blockedReason = blockedReasonProvided
|
|
12893
|
+
? String(body?.blockedReason || "").trim() || null
|
|
12894
|
+
: undefined;
|
|
11828
12895
|
const baseBranchProvided = hasOwn(body, "baseBranch") || hasOwn(body, "base_branch");
|
|
11829
12896
|
const baseBranch = baseBranchProvided
|
|
11830
12897
|
? normalizeBranchInput(body?.baseBranch ?? body?.base_branch)
|
|
11831
12898
|
: undefined;
|
|
11832
12899
|
const metadataPatch = buildTaskMetadataPatch(body || {});
|
|
12900
|
+
const requestedStatus = normalizeTaskStatusKey(body?.status);
|
|
12901
|
+
const clearsBlockedState = requestedStatus === "todo";
|
|
12902
|
+
const nextMeta = (Object.keys(metadataPatch.meta).length > 0 || clearsBlockedState)
|
|
12903
|
+
? buildTaskMetaPatch(previousTask?.meta, metadataPatch.meta, { clearBlockedState: clearsBlockedState })
|
|
12904
|
+
: null;
|
|
11833
12905
|
const patch = {
|
|
11834
12906
|
status: body?.status,
|
|
11835
12907
|
title: body?.title,
|
|
@@ -11840,16 +12912,13 @@ async function handleApi(req, res, url) {
|
|
|
11840
12912
|
repositories: Array.isArray(body?.repositories) ? body.repositories : undefined,
|
|
11841
12913
|
...(tagsProvided ? { tags } : {}),
|
|
11842
12914
|
...(draftProvided ? { draft: Boolean(body?.draft) } : {}),
|
|
12915
|
+
...(clearsBlockedState
|
|
12916
|
+
? { cooldownUntil: null, blockedReason: null }
|
|
12917
|
+
: (blockedReasonProvided ? { blockedReason } : {})),
|
|
12918
|
+
...(clearsBlockedState ? { replaceMeta: true } : {}),
|
|
11843
12919
|
...(baseBranchProvided ? { baseBranch } : {}),
|
|
11844
12920
|
...metadataPatch.topLevel,
|
|
11845
|
-
...(
|
|
11846
|
-
? {
|
|
11847
|
-
meta: {
|
|
11848
|
-
...(previousTask?.meta && typeof previousTask.meta === "object" ? previousTask.meta : {}),
|
|
11849
|
-
...metadataPatch.meta,
|
|
11850
|
-
},
|
|
11851
|
-
}
|
|
11852
|
-
: {}),
|
|
12921
|
+
...(nextMeta ? { meta: nextMeta } : {}),
|
|
11853
12922
|
};
|
|
11854
12923
|
if (!hasTaskPatchValues(patch) && !baseBranchProvided && !draftProvided && !tagsProvided) {
|
|
11855
12924
|
jsonResponse(res, 400, {
|
|
@@ -11960,11 +13029,20 @@ async function handleApi(req, res, url) {
|
|
|
11960
13029
|
const tagsProvided = hasOwn(body, "tags");
|
|
11961
13030
|
const tags = tagsProvided ? normalizeTagsInput(body?.tags) : undefined;
|
|
11962
13031
|
const draftProvided = hasOwn(body, "draft");
|
|
13032
|
+
const blockedReasonProvided = hasOwn(body, "blockedReason");
|
|
13033
|
+
const blockedReason = blockedReasonProvided
|
|
13034
|
+
? String(body?.blockedReason || "").trim() || null
|
|
13035
|
+
: undefined;
|
|
11963
13036
|
const baseBranchProvided = hasOwn(body, "baseBranch") || hasOwn(body, "base_branch");
|
|
11964
13037
|
const baseBranch = baseBranchProvided
|
|
11965
13038
|
? normalizeBranchInput(body?.baseBranch ?? body?.base_branch)
|
|
11966
13039
|
: undefined;
|
|
11967
13040
|
const metadataPatch = buildTaskMetadataPatch(body || {});
|
|
13041
|
+
const requestedStatus = normalizeTaskStatusKey(body?.status);
|
|
13042
|
+
const clearsBlockedState = requestedStatus === "todo";
|
|
13043
|
+
const nextMeta = (Object.keys(metadataPatch.meta).length > 0 || clearsBlockedState)
|
|
13044
|
+
? buildTaskMetaPatch(previousTask?.meta, metadataPatch.meta, { clearBlockedState: clearsBlockedState })
|
|
13045
|
+
: null;
|
|
11968
13046
|
const patch = {
|
|
11969
13047
|
title: body?.title,
|
|
11970
13048
|
description: body?.description,
|
|
@@ -11975,16 +13053,13 @@ async function handleApi(req, res, url) {
|
|
|
11975
13053
|
repositories: Array.isArray(body?.repositories) ? body.repositories : undefined,
|
|
11976
13054
|
...(tagsProvided ? { tags } : {}),
|
|
11977
13055
|
...(draftProvided ? { draft: Boolean(body?.draft) } : {}),
|
|
13056
|
+
...(clearsBlockedState
|
|
13057
|
+
? { cooldownUntil: null, blockedReason: null }
|
|
13058
|
+
: (blockedReasonProvided ? { blockedReason } : {})),
|
|
13059
|
+
...(clearsBlockedState ? { replaceMeta: true } : {}),
|
|
11978
13060
|
...(baseBranchProvided ? { baseBranch } : {}),
|
|
11979
13061
|
...metadataPatch.topLevel,
|
|
11980
|
-
...(
|
|
11981
|
-
? {
|
|
11982
|
-
meta: {
|
|
11983
|
-
...(previousTask?.meta && typeof previousTask.meta === "object" ? previousTask.meta : {}),
|
|
11984
|
-
...metadataPatch.meta,
|
|
11985
|
-
},
|
|
11986
|
-
}
|
|
11987
|
-
: {}),
|
|
13062
|
+
...(nextMeta ? { meta: nextMeta } : {}),
|
|
11988
13063
|
};
|
|
11989
13064
|
if (!hasTaskPatchValues(patch) && !baseBranchProvided && !draftProvided && !tagsProvided) {
|
|
11990
13065
|
jsonResponse(res, 400, {
|
|
@@ -12157,6 +13232,10 @@ async function handleApi(req, res, url) {
|
|
|
12157
13232
|
const adapter = getKanbanAdapter();
|
|
12158
13233
|
const tags = normalizeTagsInput(body?.tags);
|
|
12159
13234
|
const wantsDraft = Boolean(body?.draft) || body?.status === "draft";
|
|
13235
|
+
const blockedReasonProvided = hasOwn(body, "blockedReason");
|
|
13236
|
+
const blockedReason = blockedReasonProvided
|
|
13237
|
+
? String(body?.blockedReason || "").trim() || null
|
|
13238
|
+
: undefined;
|
|
12160
13239
|
const baseBranch = normalizeBranchInput(body?.baseBranch ?? body?.base_branch);
|
|
12161
13240
|
const activeWorkspace = getActiveManagedWorkspace(resolveUiConfigDir());
|
|
12162
13241
|
const defaultRepository =
|
|
@@ -12175,6 +13254,7 @@ async function handleApi(req, res, url) {
|
|
|
12175
13254
|
description: body?.description || "",
|
|
12176
13255
|
status: body?.status || (wantsDraft ? "draft" : "todo"),
|
|
12177
13256
|
priority: body?.priority || undefined,
|
|
13257
|
+
...(blockedReasonProvided ? { blockedReason } : {}),
|
|
12178
13258
|
...(workspace ? { workspace } : {}),
|
|
12179
13259
|
...(repository ? { repository } : {}),
|
|
12180
13260
|
...(repositories.length ? { repositories } : {}),
|
|
@@ -13704,7 +14784,10 @@ async function handleApi(req, res, url) {
|
|
|
13704
14784
|
resolveAgentWorkLogDir(),
|
|
13705
14785
|
"shredding-stats.jsonl",
|
|
13706
14786
|
);
|
|
13707
|
-
const raw = await
|
|
14787
|
+
const [{ entries: completedSessions }, raw] = await Promise.all([
|
|
14788
|
+
readCompletedSessionEntries(100_000),
|
|
14789
|
+
readJsonlTail(shreddingPath, 10_000),
|
|
14790
|
+
]);
|
|
13708
14791
|
const inWindow = raw.filter((e) => withinDays(e, days));
|
|
13709
14792
|
let excludedSynthetic = 0;
|
|
13710
14793
|
let excludedNoop = 0;
|
|
@@ -13729,7 +14812,14 @@ async function handleApi(req, res, url) {
|
|
|
13729
14812
|
let totalOriginalChars = 0;
|
|
13730
14813
|
let totalCompressedChars = 0;
|
|
13731
14814
|
let totalSavedChars = 0;
|
|
14815
|
+
let totalOriginalTokensEstimated = 0;
|
|
14816
|
+
let totalCompressedTokensEstimated = 0;
|
|
14817
|
+
let totalSavedTokensEstimated = 0;
|
|
13732
14818
|
const dailySaved = {};
|
|
14819
|
+
const dailyOriginal = {};
|
|
14820
|
+
const dailyCompressed = {};
|
|
14821
|
+
const dailySavedTokensEstimated = {};
|
|
14822
|
+
const dailyCostSavedUsd = {};
|
|
13733
14823
|
const dailyCounts = {};
|
|
13734
14824
|
const agentCounts = {};
|
|
13735
14825
|
const stageCounts = {};
|
|
@@ -13740,17 +14830,37 @@ async function handleApi(req, res, url) {
|
|
|
13740
14830
|
let liveOriginalChars = 0;
|
|
13741
14831
|
let liveCompressedChars = 0;
|
|
13742
14832
|
let liveSavedChars = 0;
|
|
14833
|
+
let liveSavedTokensEstimated = 0;
|
|
14834
|
+
const sessionCostModel = summarizeObservedSessionCostModel(
|
|
14835
|
+
completedSessions.filter((entry) => withinDays(entry, days)),
|
|
14836
|
+
);
|
|
14837
|
+
const blendedCostPerToken = sessionCostModel.blendedCostPerToken;
|
|
13743
14838
|
|
|
13744
14839
|
for (const e of events) {
|
|
13745
14840
|
const originalChars = numberOrZero(e.originalChars);
|
|
13746
14841
|
const compressedChars = numberOrZero(e.compressedChars);
|
|
13747
14842
|
const savedChars = numberOrZero(e.savedChars);
|
|
14843
|
+
const originalTokensEstimated = estimateTokensFromChars(originalChars);
|
|
14844
|
+
const compressedTokensEstimated = estimateTokensFromChars(compressedChars);
|
|
14845
|
+
const savedTokensEstimated = estimateTokensFromChars(savedChars);
|
|
14846
|
+
const estimatedCostSavedUsd = blendedCostPerToken != null
|
|
14847
|
+
? roundMetric(savedTokensEstimated * blendedCostPerToken)
|
|
14848
|
+
: null;
|
|
13748
14849
|
totalOriginalChars += originalChars;
|
|
13749
14850
|
totalCompressedChars += compressedChars;
|
|
13750
14851
|
totalSavedChars += savedChars;
|
|
13751
|
-
|
|
14852
|
+
totalOriginalTokensEstimated += originalTokensEstimated;
|
|
14853
|
+
totalCompressedTokensEstimated += compressedTokensEstimated;
|
|
14854
|
+
totalSavedTokensEstimated += savedTokensEstimated;
|
|
14855
|
+
const day = getEntryDayKey(e);
|
|
13752
14856
|
if (day) {
|
|
14857
|
+
dailyOriginal[day] = (dailyOriginal[day] || 0) + originalChars;
|
|
14858
|
+
dailyCompressed[day] = (dailyCompressed[day] || 0) + compressedChars;
|
|
13753
14859
|
dailySaved[day] = (dailySaved[day] || 0) + savedChars;
|
|
14860
|
+
dailySavedTokensEstimated[day] = (dailySavedTokensEstimated[day] || 0) + savedTokensEstimated;
|
|
14861
|
+
if (estimatedCostSavedUsd != null) {
|
|
14862
|
+
dailyCostSavedUsd[day] = roundMetric((dailyCostSavedUsd[day] || 0) + estimatedCostSavedUsd);
|
|
14863
|
+
}
|
|
13754
14864
|
dailyCounts[day] = (dailyCounts[day] || 0) + 1;
|
|
13755
14865
|
}
|
|
13756
14866
|
const agent = normalizeShreddingAgentType(e.agentType);
|
|
@@ -13764,6 +14874,7 @@ async function handleApi(req, res, url) {
|
|
|
13764
14874
|
liveOriginalChars += originalChars;
|
|
13765
14875
|
liveCompressedChars += compressedChars;
|
|
13766
14876
|
liveSavedChars += savedChars;
|
|
14877
|
+
liveSavedTokensEstimated += savedTokensEstimated;
|
|
13767
14878
|
const compactionFamily = String(e.compactionFamily || "unknown").trim().toLowerCase() || "unknown";
|
|
13768
14879
|
const commandFamily = String(e.commandFamily || "unknown").trim().toLowerCase() || "unknown";
|
|
13769
14880
|
compactionFamilyCounts[compactionFamily] = (compactionFamilyCounts[compactionFamily] || 0) + 1;
|
|
@@ -13798,12 +14909,27 @@ async function handleApi(req, res, url) {
|
|
|
13798
14909
|
savedPct: numberOrZero(e.savedPct),
|
|
13799
14910
|
originalChars: numberOrZero(e.originalChars),
|
|
13800
14911
|
compressedChars: numberOrZero(e.compressedChars),
|
|
14912
|
+
estimatedSavedTokens: estimateTokensFromChars(numberOrZero(e.savedChars)),
|
|
14913
|
+
estimatedCostSavedUsd: blendedCostPerToken != null
|
|
14914
|
+
? roundMetric(estimateTokensFromChars(numberOrZero(e.savedChars)) * blendedCostPerToken)
|
|
14915
|
+
: null,
|
|
13801
14916
|
agentType: normalizeShreddingAgentType(e.agentType),
|
|
13802
14917
|
attemptId: e.attemptId || null,
|
|
13803
14918
|
stage: String(e.stage || "session_total").trim().toLowerCase() || "session_total",
|
|
13804
14919
|
compactionFamily: String(e.compactionFamily || "").trim().toLowerCase() || null,
|
|
13805
14920
|
commandFamily: String(e.commandFamily || "").trim().toLowerCase() || null,
|
|
13806
14921
|
}));
|
|
14922
|
+
const dailyReductionPct = {};
|
|
14923
|
+
for (const day of Object.keys(dailyOriginal)) {
|
|
14924
|
+
const originalChars = numberOrZero(dailyOriginal[day]);
|
|
14925
|
+
const savedChars = numberOrZero(dailySaved[day]);
|
|
14926
|
+
dailyReductionPct[day] = originalChars > 0
|
|
14927
|
+
? Math.round((savedChars / originalChars) * 100)
|
|
14928
|
+
: 0;
|
|
14929
|
+
}
|
|
14930
|
+
const totalEstimatedCostSavedUsd = blendedCostPerToken != null
|
|
14931
|
+
? roundMetric(totalSavedTokensEstimated * blendedCostPerToken)
|
|
14932
|
+
: null;
|
|
13807
14933
|
|
|
13808
14934
|
jsonResponse(res, 200, {
|
|
13809
14935
|
ok: true,
|
|
@@ -13814,17 +14940,35 @@ async function handleApi(req, res, url) {
|
|
|
13814
14940
|
totalSavedChars,
|
|
13815
14941
|
avgSavedPct,
|
|
13816
14942
|
sortedDates,
|
|
14943
|
+
dailyOriginal,
|
|
14944
|
+
dailyCompressed,
|
|
13817
14945
|
dailySaved,
|
|
14946
|
+
dailySavedTokensEstimated,
|
|
14947
|
+
dailyCostSavedUsd,
|
|
14948
|
+
dailyReductionPct,
|
|
13818
14949
|
dailyCounts,
|
|
13819
14950
|
topAgents,
|
|
13820
14951
|
stageCounts,
|
|
13821
14952
|
topCompactionFamilies,
|
|
13822
14953
|
topCommandFamilies,
|
|
14954
|
+
totals: {
|
|
14955
|
+
originalTokensEstimated: totalOriginalTokensEstimated,
|
|
14956
|
+
compressedTokensEstimated: totalCompressedTokensEstimated,
|
|
14957
|
+
savedTokensEstimated: totalSavedTokensEstimated,
|
|
14958
|
+
estimatedCostSavedUsd: totalEstimatedCostSavedUsd,
|
|
14959
|
+
},
|
|
14960
|
+
estimation: {
|
|
14961
|
+
charsPerToken: SHREDDING_ESTIMATED_CHARS_PER_TOKEN,
|
|
14962
|
+
costModel: blendedCostPerToken != null ? "observed_blended_session_cost" : "unavailable",
|
|
14963
|
+
blendedCostPerMillionTokensUsd: sessionCostModel.blendedCostPerMillionTokensUsd,
|
|
14964
|
+
pricedSessions: sessionCostModel.pricedSessions,
|
|
14965
|
+
},
|
|
13823
14966
|
liveCompaction: {
|
|
13824
14967
|
totalEvents: liveTotalEvents,
|
|
13825
14968
|
totalOriginalChars: liveOriginalChars,
|
|
13826
14969
|
totalCompressedChars: liveCompressedChars,
|
|
13827
14970
|
totalSavedChars: liveSavedChars,
|
|
14971
|
+
savedTokensEstimated: liveSavedTokensEstimated,
|
|
13828
14972
|
avgSavedPct: liveAvgSavedPct,
|
|
13829
14973
|
},
|
|
13830
14974
|
recentEvents,
|
|
@@ -13835,6 +14979,7 @@ async function handleApi(req, res, url) {
|
|
|
13835
14979
|
unknownAttribution,
|
|
13836
14980
|
includeSynthetic,
|
|
13837
14981
|
includeNoop,
|
|
14982
|
+
pricedSessions: sessionCostModel.pricedSessions,
|
|
13838
14983
|
},
|
|
13839
14984
|
},
|
|
13840
14985
|
});
|
|
@@ -14575,6 +15720,9 @@ async function handleApi(req, res, url) {
|
|
|
14575
15720
|
templateName: template.name,
|
|
14576
15721
|
workflowId,
|
|
14577
15722
|
variables: { ...template.variables, ...userVars },
|
|
15723
|
+
workspaceId,
|
|
15724
|
+
repository: executeInput.repository || executeInput._targetRepo || null,
|
|
15725
|
+
targetRepo: executeInput._targetRepo || executeInput.repository || null,
|
|
14578
15726
|
dispatchedAt,
|
|
14579
15727
|
});
|
|
14580
15728
|
return;
|
|
@@ -14587,6 +15735,9 @@ async function handleApi(req, res, url) {
|
|
|
14587
15735
|
templateId,
|
|
14588
15736
|
templateName: template.name,
|
|
14589
15737
|
workflowId,
|
|
15738
|
+
workspaceId,
|
|
15739
|
+
repository: executeInput.repository || executeInput._targetRepo || null,
|
|
15740
|
+
targetRepo: executeInput._targetRepo || executeInput.repository || null,
|
|
14590
15741
|
result,
|
|
14591
15742
|
});
|
|
14592
15743
|
} catch (err) {
|
|
@@ -14646,6 +15797,31 @@ async function handleApi(req, res, url) {
|
|
|
14646
15797
|
return;
|
|
14647
15798
|
}
|
|
14648
15799
|
|
|
15800
|
+
if (path === "/api/workflows/reflow-template-layouts" && req.method === "POST") {
|
|
15801
|
+
try {
|
|
15802
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
15803
|
+
if (!wfCtx.ok) {
|
|
15804
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
15805
|
+
return;
|
|
15806
|
+
}
|
|
15807
|
+
if (typeof _wfTemplates?.relayoutInstalledTemplateWorkflows !== "function") {
|
|
15808
|
+
jsonResponse(res, 503, { ok: false, error: "Template relayout service unavailable" });
|
|
15809
|
+
return;
|
|
15810
|
+
}
|
|
15811
|
+
const body = await readJsonBody(req).catch(() => ({}));
|
|
15812
|
+
const result = _wfTemplates.relayoutInstalledTemplateWorkflows(wfCtx.engine, {
|
|
15813
|
+
workflowIds: body?.workflowIds || body?.workflowId,
|
|
15814
|
+
});
|
|
15815
|
+
const workflows = result.updatedWorkflowIds
|
|
15816
|
+
.map((workflowId) => wfCtx.engine.get(workflowId))
|
|
15817
|
+
.filter(Boolean);
|
|
15818
|
+
jsonResponse(res, 200, { ok: true, result, workflows });
|
|
15819
|
+
} catch (err) {
|
|
15820
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
15821
|
+
}
|
|
15822
|
+
return;
|
|
15823
|
+
}
|
|
15824
|
+
|
|
14649
15825
|
if (path === "/api/workflows/template-updates") {
|
|
14650
15826
|
try {
|
|
14651
15827
|
const wfCtx = await getWorkflowRequestContext(url);
|
|
@@ -14953,6 +16129,23 @@ async function handleApi(req, res, url) {
|
|
|
14953
16129
|
return;
|
|
14954
16130
|
}
|
|
14955
16131
|
|
|
16132
|
+
if (action === "reflow-layout" && req.method === "POST") {
|
|
16133
|
+
if (typeof _wfTemplates?.relayoutInstalledTemplateWorkflows !== "function") {
|
|
16134
|
+
jsonResponse(res, 503, { ok: false, error: "Template relayout service unavailable" });
|
|
16135
|
+
return;
|
|
16136
|
+
}
|
|
16137
|
+
const result = _wfTemplates.relayoutInstalledTemplateWorkflows(engine, {
|
|
16138
|
+
workflowId,
|
|
16139
|
+
});
|
|
16140
|
+
const workflow = engine.get(workflowId);
|
|
16141
|
+
if (!workflow) {
|
|
16142
|
+
jsonResponse(res, 404, { ok: false, error: "Workflow not found after relayout" });
|
|
16143
|
+
return;
|
|
16144
|
+
}
|
|
16145
|
+
jsonResponse(res, 200, { ok: true, workflow, result });
|
|
16146
|
+
return;
|
|
16147
|
+
}
|
|
16148
|
+
|
|
14956
16149
|
if (action === "runs") {
|
|
14957
16150
|
const rawOffset = Number(url.searchParams.get("offset"));
|
|
14958
16151
|
const rawLimit = Number(url.searchParams.get("limit"));
|
|
@@ -15074,7 +16267,7 @@ async function handleApi(req, res, url) {
|
|
|
15074
16267
|
if (path === "/api/manual-flows/execute" && req.method === "POST") {
|
|
15075
16268
|
try {
|
|
15076
16269
|
const body = await readJsonBody(req);
|
|
15077
|
-
const { templateId, formValues } = body || {};
|
|
16270
|
+
const { templateId, formValues, executionContext } = body || {};
|
|
15078
16271
|
if (!templateId) {
|
|
15079
16272
|
jsonResponse(res, 400, { ok: false, error: "templateId is required" });
|
|
15080
16273
|
return;
|
|
@@ -15082,7 +16275,40 @@ async function handleApi(req, res, url) {
|
|
|
15082
16275
|
const mf = await import("../workflow/manual-flows.mjs");
|
|
15083
16276
|
const ctx = resolveActiveWorkspaceExecutionContext();
|
|
15084
16277
|
const wfCtx = await getWorkflowRequestContext(url);
|
|
15085
|
-
const
|
|
16278
|
+
const repository = String(
|
|
16279
|
+
executionContext?.repository ||
|
|
16280
|
+
executionContext?.targetRepo ||
|
|
16281
|
+
formValues?._targetRepo ||
|
|
16282
|
+
resolveDefaultRepositoryForWorkspaceContext(ctx),
|
|
16283
|
+
).trim();
|
|
16284
|
+
const workspaceId = String(
|
|
16285
|
+
executionContext?.workspaceId ||
|
|
16286
|
+
executionContext?.workspace ||
|
|
16287
|
+
ctx.workspaceId ||
|
|
16288
|
+
"",
|
|
16289
|
+
).trim();
|
|
16290
|
+
const projectId = String(
|
|
16291
|
+
executionContext?.projectId ||
|
|
16292
|
+
executionContext?.project ||
|
|
16293
|
+
body?.project ||
|
|
16294
|
+
"",
|
|
16295
|
+
).trim();
|
|
16296
|
+
const flowContext = {
|
|
16297
|
+
...(wfCtx?.ok ? { engine: wfCtx.engine } : {}),
|
|
16298
|
+
taskManager: createManualFlowTaskManager(ctx, {
|
|
16299
|
+
repository,
|
|
16300
|
+
workspaceId,
|
|
16301
|
+
projectId,
|
|
16302
|
+
templateId,
|
|
16303
|
+
}),
|
|
16304
|
+
runMetadata: {
|
|
16305
|
+
repository,
|
|
16306
|
+
workspaceId,
|
|
16307
|
+
workspaceDir: ctx.workspaceDir,
|
|
16308
|
+
projectId,
|
|
16309
|
+
triggerSource: "manual-ui",
|
|
16310
|
+
},
|
|
16311
|
+
};
|
|
15086
16312
|
const run = await mf.executeFlow(templateId, formValues || {}, ctx.workspaceDir, flowContext);
|
|
15087
16313
|
jsonResponse(res, 200, { ok: true, run });
|
|
15088
16314
|
} catch (err) {
|
|
@@ -15606,12 +16832,26 @@ async function handleApi(req, res, url) {
|
|
|
15606
16832
|
jsonResponse(res, 404, { ok: false, error: "Task not found." });
|
|
15607
16833
|
return;
|
|
15608
16834
|
}
|
|
15609
|
-
|
|
15610
|
-
|
|
15611
|
-
|
|
15612
|
-
|
|
16835
|
+
let nextTask = unblockInternalTask(taskId, {
|
|
16836
|
+
status: "todo",
|
|
16837
|
+
source: "manual-retry",
|
|
16838
|
+
});
|
|
16839
|
+
if (!nextTask) {
|
|
16840
|
+
if (typeof adapter.updateTask === "function") {
|
|
16841
|
+
await adapter.updateTask(taskId, {
|
|
16842
|
+
status: "todo",
|
|
16843
|
+
cooldownUntil: null,
|
|
16844
|
+
blockedReason: null,
|
|
16845
|
+
meta: task?.meta && typeof task.meta === "object"
|
|
16846
|
+
? Object.fromEntries(Object.entries(task.meta).filter(([key]) => key !== "autoRecovery"))
|
|
16847
|
+
: task?.meta,
|
|
16848
|
+
});
|
|
16849
|
+
} else if (typeof adapter.updateTaskStatus === "function") {
|
|
16850
|
+
await adapter.updateTaskStatus(taskId, "todo");
|
|
16851
|
+
}
|
|
16852
|
+
nextTask = await adapter.getTask(taskId);
|
|
15613
16853
|
}
|
|
15614
|
-
executor.executeTask(task).catch((error) => {
|
|
16854
|
+
executor.executeTask(nextTask || { ...task, status: "todo" }).catch((error) => {
|
|
15615
16855
|
console.warn(
|
|
15616
16856
|
`[telegram-ui] failed to retry task ${taskId}: ${error.message}`,
|
|
15617
16857
|
);
|
|
@@ -15636,6 +16876,58 @@ async function handleApi(req, res, url) {
|
|
|
15636
16876
|
return;
|
|
15637
16877
|
}
|
|
15638
16878
|
|
|
16879
|
+
if (path === "/api/tasks/unblock") {
|
|
16880
|
+
if (req.method !== "POST") {
|
|
16881
|
+
res.setHeader("Allow", "POST");
|
|
16882
|
+
jsonResponse(res, 405, { ok: false, error: "Method Not Allowed" });
|
|
16883
|
+
return;
|
|
16884
|
+
}
|
|
16885
|
+
try {
|
|
16886
|
+
const body = await readJsonBody(req);
|
|
16887
|
+
const taskId = body?.taskId || body?.id;
|
|
16888
|
+
const targetStatus = String(body?.status || "todo").trim().toLowerCase() || "todo";
|
|
16889
|
+
if (!taskId) {
|
|
16890
|
+
jsonResponse(res, 400, { ok: false, error: "taskId is required" });
|
|
16891
|
+
return;
|
|
16892
|
+
}
|
|
16893
|
+
const adapter = getKanbanAdapter();
|
|
16894
|
+
const task = await adapter.getTask(taskId);
|
|
16895
|
+
if (!task) {
|
|
16896
|
+
jsonResponse(res, 404, { ok: false, error: "Task not found." });
|
|
16897
|
+
return;
|
|
16898
|
+
}
|
|
16899
|
+
let updatedTask = unblockInternalTask(taskId, {
|
|
16900
|
+
status: targetStatus,
|
|
16901
|
+
source: "api.tasks.unblock",
|
|
16902
|
+
});
|
|
16903
|
+
if (!updatedTask) {
|
|
16904
|
+
const nextMeta = task?.meta && typeof task.meta === "object"
|
|
16905
|
+
? Object.fromEntries(Object.entries(task.meta).filter(([key]) => key !== "autoRecovery"))
|
|
16906
|
+
: task?.meta;
|
|
16907
|
+
if (typeof adapter.updateTask === "function") {
|
|
16908
|
+
await adapter.updateTask(taskId, {
|
|
16909
|
+
status: targetStatus,
|
|
16910
|
+
cooldownUntil: null,
|
|
16911
|
+
blockedReason: null,
|
|
16912
|
+
meta: nextMeta,
|
|
16913
|
+
});
|
|
16914
|
+
} else if (typeof adapter.updateTaskStatus === "function") {
|
|
16915
|
+
await adapter.updateTaskStatus(taskId, targetStatus);
|
|
16916
|
+
}
|
|
16917
|
+
updatedTask = await adapter.getTask(taskId);
|
|
16918
|
+
}
|
|
16919
|
+
jsonResponse(res, 200, { ok: true, taskId, data: updatedTask || null });
|
|
16920
|
+
broadcastUiEvent(
|
|
16921
|
+
["tasks", "overview", "executor", "agents"],
|
|
16922
|
+
"invalidate",
|
|
16923
|
+
{ reason: "task-unblocked", taskId },
|
|
16924
|
+
);
|
|
16925
|
+
} catch (err) {
|
|
16926
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
16927
|
+
}
|
|
16928
|
+
return;
|
|
16929
|
+
}
|
|
16930
|
+
|
|
15639
16931
|
// ── GET /api/retry-queue ───────────────────────────────────────────
|
|
15640
16932
|
if (path === "/api/retry-queue" && req.method === "GET") {
|
|
15641
16933
|
try {
|
|
@@ -18054,8 +19346,16 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
18054
19346
|
req.url || "/",
|
|
18055
19347
|
`http://${req.headers.host || "localhost"}`,
|
|
18056
19348
|
);
|
|
19349
|
+
res.__bosunRequestContext = {
|
|
19350
|
+
diagnosticId: ensureResponseDiagnosticId(res),
|
|
19351
|
+
method: String(req?.method || "GET").toUpperCase(),
|
|
19352
|
+
path: url.pathname,
|
|
19353
|
+
query: url.search || "",
|
|
19354
|
+
};
|
|
18057
19355
|
const webhookPath = getGitHubWebhookPath();
|
|
18058
19356
|
|
|
19357
|
+
try {
|
|
19358
|
+
|
|
18059
19359
|
// Token exchange: ?token=<hex> → set session cookie and redirect to clean URL
|
|
18060
19360
|
const qToken = url.searchParams.get("token");
|
|
18061
19361
|
if (qToken && sessionToken) {
|
|
@@ -18197,6 +19497,21 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
18197
19497
|
}
|
|
18198
19498
|
}
|
|
18199
19499
|
await handleStatic(req, res, url);
|
|
19500
|
+
} catch (err) {
|
|
19501
|
+
if (res.headersSent) {
|
|
19502
|
+
console.error("[ui-server] unhandled request failure after headers sent", {
|
|
19503
|
+
diagnosticId: ensureResponseDiagnosticId(res),
|
|
19504
|
+
payload: describePayloadForErrorLog(err),
|
|
19505
|
+
});
|
|
19506
|
+
try {
|
|
19507
|
+
res.destroy?.(err);
|
|
19508
|
+
} catch {
|
|
19509
|
+
/* best effort */
|
|
19510
|
+
}
|
|
19511
|
+
return;
|
|
19512
|
+
}
|
|
19513
|
+
jsonResponse(res, 500, err);
|
|
19514
|
+
}
|
|
18200
19515
|
};
|
|
18201
19516
|
|
|
18202
19517
|
try {
|