bosun 0.40.21 → 0.41.1
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 +8 -0
- package/README.md +20 -0
- package/agent/agent-custom-tools.mjs +23 -5
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +131 -30
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/primary-agent.mjs +81 -7
- package/agent/retry-queue.mjs +164 -0
- package/bench/swebench/bosun-swebench.mjs +5 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +267 -8
- package/config/config-doctor.mjs +51 -2
- package/config/config.mjs +232 -5
- package/github/github-auth-manager.mjs +70 -19
- package/infra/library-manager.mjs +894 -60
- package/infra/monitor.mjs +701 -69
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +95 -28
- package/infra/test-runtime.mjs +267 -0
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +30 -8
- package/server/setup-web-server.mjs +29 -1
- package/server/ui-server.mjs +1571 -49
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-claims.mjs +6 -10
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/chat-view.js +18 -1
- package/ui/components/workspace-switcher.js +321 -9
- package/ui/demo-defaults.js +17830 -10433
- package/ui/demo.html +9 -1
- package/ui/modules/router.js +1 -1
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +376 -37
- package/ui/modules/voice-client.js +173 -33
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +571 -1
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/library.js +410 -55
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +1083 -507
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +38 -1
- package/ui/tabs/workflows.js +1275 -402
- package/voice/voice-agents-sdk.mjs +2 -2
- package/voice/voice-relay.mjs +28 -20
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/project-detection.mjs +559 -0
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-contract.mjs +433 -232
- package/workflow/workflow-engine.mjs +510 -47
- package/workflow/workflow-nodes/custom-loader.mjs +251 -0
- package/workflow/workflow-nodes.mjs +2024 -184
- package/workflow/workflow-templates.mjs +118 -24
- package/workflow-templates/agents.mjs +20 -20
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/code-quality.mjs +20 -14
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +27 -10
- package/workflow-templates/task-execution.mjs +752 -0
- package/workflow-templates/task-lifecycle.mjs +117 -14
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +153 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
package/agent/agent-pool.mjs
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
streamRetryDelay,
|
|
54
54
|
MAX_STREAM_RETRIES,
|
|
55
55
|
} from "../infra/stream-resilience.mjs";
|
|
56
|
+
import { ensureTestRuntimeSandbox } from "../infra/test-runtime.mjs";
|
|
56
57
|
import { compressAllItems, estimateSavings, estimateContextUsagePct, recordShreddingEvent } from "../workspace/context-cache.mjs";
|
|
57
58
|
import { resolveContextShreddingOptions } from "../config/context-shredding-config.mjs";
|
|
58
59
|
|
|
@@ -334,6 +335,17 @@ function envFlagEnabled(value) {
|
|
|
334
335
|
return ["1", "true", "yes", "on", "y"].includes(raw);
|
|
335
336
|
}
|
|
336
337
|
|
|
338
|
+
function applyNodeWarningSuppressionEnv(runtimeEnv) {
|
|
339
|
+
const nextEnv = { ...(runtimeEnv || {}) };
|
|
340
|
+
if (String(process.env.BOSUN_SUPPRESS_NODE_WARNINGS ?? "").trim() === "0") {
|
|
341
|
+
return nextEnv;
|
|
342
|
+
}
|
|
343
|
+
if (!nextEnv.NODE_NO_WARNINGS) {
|
|
344
|
+
nextEnv.NODE_NO_WARNINGS = "1";
|
|
345
|
+
}
|
|
346
|
+
return nextEnv;
|
|
347
|
+
}
|
|
348
|
+
|
|
337
349
|
const GITHUB_TOKEN_CACHE_TTL_MS = 60_000;
|
|
338
350
|
let cachedGithubSessionToken = null;
|
|
339
351
|
let cachedGithubSessionTokenAt = 0;
|
|
@@ -884,6 +896,7 @@ function applySdkFailureCooldown(name, error, nowMs = Date.now()) {
|
|
|
884
896
|
}
|
|
885
897
|
|
|
886
898
|
const MONITOR_MONITOR_TASK_KEY = "monitor-monitor";
|
|
899
|
+
const MONITOR_MONITOR_THREAD_REFRESH_TURNS_REMAINING = parseBoundedNumber(process.env.DEVMODE_MONITOR_MONITOR_THREAD_REFRESH_TURNS_REMAINING, 5, 1, 1000);
|
|
887
900
|
let monitorMonitorTimeoutBoundsWarningKey = "";
|
|
888
901
|
let monitorMonitorTimeoutAdjustmentKey = "";
|
|
889
902
|
|
|
@@ -1086,6 +1099,7 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1086
1099
|
onThreadReady = null,
|
|
1087
1100
|
taskKey: steerKey = null,
|
|
1088
1101
|
envOverrides = null,
|
|
1102
|
+
systemPrompt = "",
|
|
1089
1103
|
} = extra;
|
|
1090
1104
|
|
|
1091
1105
|
let reportedThreadId = null;
|
|
@@ -1129,7 +1143,8 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1129
1143
|
envOverrides && typeof envOverrides === "object"
|
|
1130
1144
|
? { ...process.env, ...envOverrides }
|
|
1131
1145
|
: process.env;
|
|
1132
|
-
const
|
|
1146
|
+
const codexSessionEnv = applyNodeWarningSuppressionEnv(codexRuntimeEnv);
|
|
1147
|
+
const codexOpts = buildCodexSdkOptions(codexSessionEnv);
|
|
1133
1148
|
const modelOverride = String(extra?.model || "").trim();
|
|
1134
1149
|
if (modelOverride) {
|
|
1135
1150
|
codexOpts.env = { ...(codexOpts.env || {}), CODEX_MODEL: modelOverride };
|
|
@@ -1197,7 +1212,10 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1197
1212
|
// ── 4. Stream the turn ───────────────────────────────────────────────────
|
|
1198
1213
|
try {
|
|
1199
1214
|
const streamSafety = resolveCodexStreamSafety(timeoutMs);
|
|
1200
|
-
const
|
|
1215
|
+
const anchoredPrompt = String(systemPrompt || "").trim()
|
|
1216
|
+
? `${String(systemPrompt).trim()}\n\n---\n\n${prompt}`
|
|
1217
|
+
: prompt;
|
|
1218
|
+
const safePrompt = sanitizeAndBoundPrompt(`${anchoredPrompt}${TOOL_OUTPUT_GUARDRAIL}`);
|
|
1201
1219
|
const turn = await thread.runStreamed(safePrompt, {
|
|
1202
1220
|
signal: controller.signal,
|
|
1203
1221
|
});
|
|
@@ -1432,11 +1450,12 @@ async function launchCopilotThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1432
1450
|
envOverrides && typeof envOverrides === "object"
|
|
1433
1451
|
? { ...process.env, ...envOverrides }
|
|
1434
1452
|
: process.env;
|
|
1453
|
+
const runtimeSessionEnv = applyNodeWarningSuppressionEnv(runtimeEnv);
|
|
1435
1454
|
const token =
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1455
|
+
runtimeSessionEnv.COPILOT_CLI_TOKEN ||
|
|
1456
|
+
runtimeSessionEnv.GITHUB_TOKEN ||
|
|
1457
|
+
runtimeSessionEnv.GH_TOKEN ||
|
|
1458
|
+
runtimeSessionEnv.GITHUB_PAT ||
|
|
1440
1459
|
undefined;
|
|
1441
1460
|
|
|
1442
1461
|
// ── 3. Create & start ephemeral client (LOCAL mode) ──────────────────────
|
|
@@ -1452,15 +1471,16 @@ async function launchCopilotThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1452
1471
|
|
|
1453
1472
|
let client;
|
|
1454
1473
|
let unsubscribe = null;
|
|
1474
|
+
let stopCopilotFirstEventWatch = null;
|
|
1455
1475
|
let finalResponse = "";
|
|
1456
1476
|
const allItems = [];
|
|
1457
1477
|
const autoApprovePermissions = shouldAutoApproveCopilotPermissions();
|
|
1458
1478
|
const clientEnv = autoApprovePermissions
|
|
1459
1479
|
? {
|
|
1460
|
-
...
|
|
1461
|
-
COPILOT_ALLOW_ALL:
|
|
1480
|
+
...runtimeSessionEnv,
|
|
1481
|
+
COPILOT_ALLOW_ALL: runtimeSessionEnv.COPILOT_ALLOW_ALL || "true",
|
|
1462
1482
|
}
|
|
1463
|
-
:
|
|
1483
|
+
: runtimeSessionEnv;
|
|
1464
1484
|
try {
|
|
1465
1485
|
await withSanitizedOpenAiEnv(async () => {
|
|
1466
1486
|
let clientOpts;
|
|
@@ -1480,7 +1500,7 @@ async function launchCopilotThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1480
1500
|
}
|
|
1481
1501
|
}
|
|
1482
1502
|
const cliLaunch = resolveCopilotCliLaunchConfig({
|
|
1483
|
-
env:
|
|
1503
|
+
env: runtimeSessionEnv,
|
|
1484
1504
|
repoRoot: REPO_ROOT,
|
|
1485
1505
|
cliArgs: buildPoolCopilotCliArgs(mcpConfigPath),
|
|
1486
1506
|
});
|
|
@@ -1686,7 +1706,50 @@ async function launchCopilotThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1686
1706
|
// Don't let this timer keep the process alive
|
|
1687
1707
|
if (ht && typeof ht.unref === "function") ht.unref();
|
|
1688
1708
|
});
|
|
1689
|
-
|
|
1709
|
+
// Some Copilot SDK builds can stall sendAndWait without yielding any
|
|
1710
|
+
// events. Apply an early watchdog so we can fail over before the full
|
|
1711
|
+
// task timeout elapses.
|
|
1712
|
+
let copilotFirstEventTimeoutMs = null;
|
|
1713
|
+
const firstEventWatch =
|
|
1714
|
+
typeof session.on === "function"
|
|
1715
|
+
? new Promise((_, reject) => {
|
|
1716
|
+
copilotFirstEventTimeoutMs = getFirstEventTimeoutMs(timeoutMs);
|
|
1717
|
+
if (!Number.isFinite(copilotFirstEventTimeoutMs) || copilotFirstEventTimeoutMs <= 0) {
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
let settled = false;
|
|
1721
|
+
let off = null;
|
|
1722
|
+
const timer = setTimeout(() => {
|
|
1723
|
+
settled = true;
|
|
1724
|
+
if (typeof off === "function") off();
|
|
1725
|
+
reject(new Error("timeout_no_events"));
|
|
1726
|
+
}, clampTimerDelayMs(copilotFirstEventTimeoutMs, "copilot-first-event-timeout"));
|
|
1727
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
1728
|
+
off = session.on((event) => {
|
|
1729
|
+
if (settled) return;
|
|
1730
|
+
if (!event || typeof event !== "object") return;
|
|
1731
|
+
const t = String(event.type || "");
|
|
1732
|
+
if (
|
|
1733
|
+
t === "assistant.message" ||
|
|
1734
|
+
t === "assistant.message_delta" ||
|
|
1735
|
+
t === "session.idle" ||
|
|
1736
|
+
t === "session.error"
|
|
1737
|
+
) {
|
|
1738
|
+
settled = true;
|
|
1739
|
+
clearTimeout(timer);
|
|
1740
|
+
if (typeof off === "function") off();
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
stopCopilotFirstEventWatch = () => {
|
|
1744
|
+
settled = true;
|
|
1745
|
+
clearTimeout(timer);
|
|
1746
|
+
if (typeof off === "function") off();
|
|
1747
|
+
};
|
|
1748
|
+
})
|
|
1749
|
+
: null;
|
|
1750
|
+
await Promise.race(
|
|
1751
|
+
[sendPromise, copilotHardTimeout, firstEventWatch].filter(Boolean),
|
|
1752
|
+
);
|
|
1690
1753
|
}
|
|
1691
1754
|
|
|
1692
1755
|
const output =
|
|
@@ -1708,6 +1771,7 @@ async function launchCopilotThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1708
1771
|
err?.name === "AbortError" ||
|
|
1709
1772
|
errMsg === "timeout" ||
|
|
1710
1773
|
errMsg === "hard_timeout" ||
|
|
1774
|
+
errMsg === "timeout_no_events" ||
|
|
1711
1775
|
errMsg === "timeout_waiting_for_idle" ||
|
|
1712
1776
|
isIdleWaitTimeout;
|
|
1713
1777
|
|
|
@@ -1729,11 +1793,15 @@ async function launchCopilotThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1729
1793
|
}
|
|
1730
1794
|
|
|
1731
1795
|
if (isTimeout) {
|
|
1796
|
+
const noEventsSuffix =
|
|
1797
|
+
errMsg === "timeout_no_events"
|
|
1798
|
+
? ` (no events received within ${getFirstEventTimeoutMs(timeoutMs)}ms)`
|
|
1799
|
+
: "";
|
|
1732
1800
|
return {
|
|
1733
1801
|
success: false,
|
|
1734
1802
|
output: "",
|
|
1735
1803
|
items: allItems,
|
|
1736
|
-
error: `${TAG} copilot timeout after ${timeoutMs}ms${isIdleWaitTimeout ? " waiting for session.idle" :
|
|
1804
|
+
error: `${TAG} copilot timeout after ${timeoutMs}ms${isIdleWaitTimeout ? " waiting for session.idle" : noEventsSuffix}`,
|
|
1737
1805
|
sdk: "copilot",
|
|
1738
1806
|
threadId: resumeThreadId,
|
|
1739
1807
|
};
|
|
@@ -1765,6 +1833,13 @@ async function launchCopilotThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1765
1833
|
threadId: resumeThreadId,
|
|
1766
1834
|
};
|
|
1767
1835
|
} finally {
|
|
1836
|
+
try {
|
|
1837
|
+
if (typeof stopCopilotFirstEventWatch === "function") {
|
|
1838
|
+
stopCopilotFirstEventWatch();
|
|
1839
|
+
}
|
|
1840
|
+
} catch {
|
|
1841
|
+
/* best effort */
|
|
1842
|
+
}
|
|
1768
1843
|
clearAbortScope();
|
|
1769
1844
|
if (steerKey) unregisterActiveSession(steerKey);
|
|
1770
1845
|
try {
|
|
@@ -1831,6 +1906,7 @@ async function launchClaudeThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1831
1906
|
model: requestedModel = null,
|
|
1832
1907
|
taskKey: steerKey = null,
|
|
1833
1908
|
envOverrides = null,
|
|
1909
|
+
systemPrompt = "",
|
|
1834
1910
|
} = extra;
|
|
1835
1911
|
|
|
1836
1912
|
// ── 1. Load the SDK ──────────────────────────────────────────────────────
|
|
@@ -1855,10 +1931,11 @@ async function launchClaudeThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1855
1931
|
envOverrides && typeof envOverrides === "object"
|
|
1856
1932
|
? { ...process.env, ...envOverrides }
|
|
1857
1933
|
: process.env;
|
|
1934
|
+
const runtimeSessionEnv = applyNodeWarningSuppressionEnv(runtimeEnv);
|
|
1858
1935
|
const apiKey =
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1936
|
+
runtimeSessionEnv.ANTHROPIC_API_KEY ||
|
|
1937
|
+
runtimeSessionEnv.CLAUDE_API_KEY ||
|
|
1938
|
+
runtimeSessionEnv.CLAUDE_KEY ||
|
|
1862
1939
|
undefined;
|
|
1863
1940
|
|
|
1864
1941
|
// ── 3. Build message queue ───────────────────────────────────────────────
|
|
@@ -1973,7 +2050,10 @@ async function launchClaudeThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1973
2050
|
`# ${extractTaskHeading(prompt)}\n\n${prompt}\n\n---\n` +
|
|
1974
2051
|
'Do NOT respond with "Ready" or ask what to do. EXECUTE this task.';
|
|
1975
2052
|
|
|
1976
|
-
|
|
2053
|
+
const anchoredPrompt = String(systemPrompt || "").trim()
|
|
2054
|
+
? `${String(systemPrompt).trim()}\n\n---\n\n${formattedPrompt}`
|
|
2055
|
+
: formattedPrompt;
|
|
2056
|
+
msgQueue.push(makeUserMessage(anchoredPrompt));
|
|
1977
2057
|
|
|
1978
2058
|
// Register active session for mid-execution steering (Claude uses message queue)
|
|
1979
2059
|
if (steerKey) {
|
|
@@ -1998,28 +2078,28 @@ async function launchClaudeThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1998
2078
|
settingSources: ["user", "project"],
|
|
1999
2079
|
permissionMode:
|
|
2000
2080
|
claudePermissionMode ||
|
|
2001
|
-
|
|
2081
|
+
runtimeSessionEnv.CLAUDE_PERMISSION_MODE ||
|
|
2002
2082
|
"bypassPermissions",
|
|
2003
2083
|
};
|
|
2004
2084
|
if (apiKey) options.apiKey = apiKey;
|
|
2005
2085
|
const explicitAllowedTools = normalizeList(claudeAllowedTools);
|
|
2006
2086
|
const allowedTools = explicitAllowedTools.length
|
|
2007
2087
|
? explicitAllowedTools
|
|
2008
|
-
: normalizeList(
|
|
2088
|
+
: normalizeList(runtimeSessionEnv.CLAUDE_ALLOWED_TOOLS);
|
|
2009
2089
|
if (allowedTools.length) {
|
|
2010
2090
|
options.allowedTools = allowedTools;
|
|
2011
2091
|
}
|
|
2012
2092
|
|
|
2013
2093
|
const model = String(
|
|
2014
2094
|
requestedModel ||
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2095
|
+
runtimeSessionEnv.CLAUDE_MODEL ||
|
|
2096
|
+
runtimeSessionEnv.CLAUDE_CODE_MODEL ||
|
|
2097
|
+
runtimeSessionEnv.ANTHROPIC_MODEL ||
|
|
2018
2098
|
"",
|
|
2019
2099
|
).trim();
|
|
2020
2100
|
if (model) options.model = model;
|
|
2021
2101
|
|
|
2022
|
-
const result = await withTemporaryEnv(
|
|
2102
|
+
const result = await withTemporaryEnv(runtimeSessionEnv, async () =>
|
|
2023
2103
|
queryFn({
|
|
2024
2104
|
prompt: msgQueue.iterator(),
|
|
2025
2105
|
options,
|
|
@@ -2521,7 +2601,10 @@ export async function execPooledPrompt(userMessage, options = {}) {
|
|
|
2521
2601
|
/** @type {Map<string, ThreadRecord>} In-memory registry keyed by taskKey */
|
|
2522
2602
|
const threadRegistry = new Map();
|
|
2523
2603
|
|
|
2524
|
-
const
|
|
2604
|
+
const testSandbox = ensureTestRuntimeSandbox();
|
|
2605
|
+
const THREAD_REGISTRY_FILE = testSandbox?.cacheDir
|
|
2606
|
+
? resolve(testSandbox.cacheDir, "thread-registry.json")
|
|
2607
|
+
: resolve(__dirname, "..", "logs", "thread-registry.json");
|
|
2525
2608
|
const THREAD_MAX_AGE_MS = 12 * 60 * 60 * 1000; // 12 hours
|
|
2526
2609
|
|
|
2527
2610
|
/** Maximum turns before a thread is considered exhausted and must be replaced */
|
|
@@ -2707,7 +2790,7 @@ async function loadThreadRegistry() {
|
|
|
2707
2790
|
async function saveThreadRegistry() {
|
|
2708
2791
|
try {
|
|
2709
2792
|
const { writeFile, mkdir } = await import("node:fs/promises");
|
|
2710
|
-
await mkdir(
|
|
2793
|
+
await mkdir(dirname(THREAD_REGISTRY_FILE), { recursive: true });
|
|
2711
2794
|
const obj = Object.fromEntries(threadRegistry);
|
|
2712
2795
|
await writeFile(THREAD_REGISTRY_FILE, JSON.stringify(obj, null, 2), "utf8");
|
|
2713
2796
|
} catch {
|
|
@@ -2799,7 +2882,8 @@ async function resumeCodexThread(threadId, prompt, cwd, timeoutMs, extra = {}) {
|
|
|
2799
2882
|
envOverrides && typeof envOverrides === "object"
|
|
2800
2883
|
? { ...process.env, ...envOverrides }
|
|
2801
2884
|
: process.env;
|
|
2802
|
-
const
|
|
2885
|
+
const codexSessionEnv = applyNodeWarningSuppressionEnv(codexRuntimeEnv);
|
|
2886
|
+
const codexOpts = buildCodexSdkOptions(codexSessionEnv);
|
|
2803
2887
|
const modelOverride = String(extra?.model || "").trim();
|
|
2804
2888
|
if (modelOverride) {
|
|
2805
2889
|
codexOpts.env = { ...(codexOpts.env || {}), CODEX_MODEL: modelOverride };
|
|
@@ -3013,6 +3097,7 @@ export async function launchOrResumeThread(
|
|
|
3013
3097
|
restBaseEnv,
|
|
3014
3098
|
resolvedGithubToken,
|
|
3015
3099
|
);
|
|
3100
|
+
restExtra.envOverrides = applyNodeWarningSuppressionEnv(restExtra.envOverrides);
|
|
3016
3101
|
// Pass taskKey through as steer key so SDK launchers can register active sessions
|
|
3017
3102
|
restExtra.taskKey = taskKey;
|
|
3018
3103
|
if (restExtra.sdk) {
|
|
@@ -3035,19 +3120,32 @@ export async function launchOrResumeThread(
|
|
|
3035
3120
|
// Check registry for existing thread
|
|
3036
3121
|
const existing = threadRegistry.get(taskKey);
|
|
3037
3122
|
if (existing && existing.alive && existing.threadId) {
|
|
3123
|
+
const turnsRemaining = MAX_THREAD_TURNS - existing.turnCount;
|
|
3124
|
+
const shouldForceRefreshMonitorMonitorThread =
|
|
3125
|
+
String(taskKey || "").trim() === MONITOR_MONITOR_TASK_KEY &&
|
|
3126
|
+
turnsRemaining <= MONITOR_MONITOR_THREAD_REFRESH_TURNS_REMAINING;
|
|
3127
|
+
if (shouldForceRefreshMonitorMonitorThread) {
|
|
3128
|
+
console.log(
|
|
3129
|
+
`${TAG} proactively refreshing monitor-monitor thread with ${turnsRemaining} turns remaining (threshold=${MONITOR_MONITOR_THREAD_REFRESH_TURNS_REMAINING})`,
|
|
3130
|
+
);
|
|
3131
|
+
existing.alive = false;
|
|
3132
|
+
threadRegistry.set(taskKey, existing);
|
|
3133
|
+
saveThreadRegistry().catch(() => {});
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3038
3136
|
// Approaching-exhaustion warning (non-blocking — still proceeds with resume)
|
|
3039
3137
|
if (
|
|
3040
3138
|
existing.turnCount >= THREAD_EXHAUSTION_WARNING_THRESHOLD &&
|
|
3041
|
-
existing.turnCount < MAX_THREAD_TURNS
|
|
3139
|
+
existing.turnCount < MAX_THREAD_TURNS &&
|
|
3140
|
+
existing.alive
|
|
3042
3141
|
) {
|
|
3043
|
-
const remaining = MAX_THREAD_TURNS - existing.turnCount;
|
|
3044
3142
|
console.warn(
|
|
3045
|
-
`${TAG} :alert: thread for task "${taskKey}" approaching exhaustion: ${existing.turnCount}/${MAX_THREAD_TURNS} turns (${
|
|
3143
|
+
`${TAG} :alert: thread for task "${taskKey}" approaching exhaustion: ${existing.turnCount}/${MAX_THREAD_TURNS} turns (${turnsRemaining} remaining)`,
|
|
3046
3144
|
);
|
|
3047
3145
|
}
|
|
3048
3146
|
|
|
3049
3147
|
// Check if thread has exceeded max turns — force fresh start
|
|
3050
|
-
if (existing.turnCount >= MAX_THREAD_TURNS) {
|
|
3148
|
+
if (existing.alive && existing.turnCount >= MAX_THREAD_TURNS) {
|
|
3051
3149
|
console.warn(
|
|
3052
3150
|
`${TAG} thread for task "${taskKey}" exceeded ${MAX_THREAD_TURNS} turns (has ${existing.turnCount}) — invalidating and starting fresh`,
|
|
3053
3151
|
);
|
|
@@ -3055,7 +3153,10 @@ export async function launchOrResumeThread(
|
|
|
3055
3153
|
threadRegistry.set(taskKey, existing);
|
|
3056
3154
|
saveThreadRegistry().catch(() => {});
|
|
3057
3155
|
// Fall through to fresh launch below
|
|
3058
|
-
} else if (
|
|
3156
|
+
} else if (
|
|
3157
|
+
existing.alive &&
|
|
3158
|
+
Date.now() - existing.createdAt > THREAD_MAX_ABSOLUTE_AGE_MS
|
|
3159
|
+
) {
|
|
3059
3160
|
console.warn(
|
|
3060
3161
|
`${TAG} thread for task "${taskKey}" exceeded absolute age limit — invalidating and starting fresh`,
|
|
3061
3162
|
);
|
|
@@ -69,25 +69,17 @@ const ALERT_COOLDOWN_RETENTION_MS = Math.max(
|
|
|
69
69
|
FAILED_SESSION_TRANSIENT_ALERT_MIN_COOLDOWN_MS * 3,
|
|
70
70
|
3 * 60 * 60 * 1000,
|
|
71
71
|
); // keep cooldown history bounded
|
|
72
|
-
const ALERT_COOLDOWN_REPLAY_MIN_BYTES = 256 * 1024;
|
|
73
|
-
const ALERT_COOLDOWN_REPLAY_DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
|
|
74
|
-
const ALERT_COOLDOWN_REPLAY_MAX_CAP_BYTES = 64 * 1024 * 1024;
|
|
75
|
-
|
|
76
72
|
function normalizeReplayMaxBytes(value) {
|
|
73
|
+
const fallbackBytes = 8 * 1024 * 1024;
|
|
74
|
+
const minBytes = 256 * 1024;
|
|
75
|
+
const maxBytes = 64 * 1024 * 1024;
|
|
77
76
|
const parsed = Number(value);
|
|
78
|
-
if (!Number.isFinite(parsed)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const rounded = Math.floor(parsed);
|
|
82
|
-
return Math.min(
|
|
83
|
-
ALERT_COOLDOWN_REPLAY_MAX_CAP_BYTES,
|
|
84
|
-
Math.max(ALERT_COOLDOWN_REPLAY_MIN_BYTES, rounded),
|
|
85
|
-
);
|
|
77
|
+
if (!Number.isFinite(parsed)) return fallbackBytes;
|
|
78
|
+
const rounded = Math.trunc(parsed);
|
|
79
|
+
return Math.min(maxBytes, Math.max(minBytes, rounded));
|
|
86
80
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
ALERT_COOLDOWN_REPLAY_MIN_BYTES,
|
|
90
|
-
normalizeReplayMaxBytes(process.env.AGENT_ALERT_COOLDOWN_REPLAY_MAX_BYTES),
|
|
81
|
+
const ALERT_COOLDOWN_REPLAY_MAX_BYTES = normalizeReplayMaxBytes(
|
|
82
|
+
process.env.AGENT_ALERT_COOLDOWN_REPLAY_MAX_BYTES,
|
|
91
83
|
);
|
|
92
84
|
|
|
93
85
|
function getAlertCooldownMs(alert) {
|
package/agent/primary-agent.mjs
CHANGED
|
@@ -67,7 +67,9 @@ import {
|
|
|
67
67
|
import { getModelsForExecutor, normalizeExecutorKey } from "../task/task-complexity.mjs";
|
|
68
68
|
|
|
69
69
|
/** Valid agent interaction modes */
|
|
70
|
-
const
|
|
70
|
+
const CORE_MODES = ["ask", "agent", "plan", "web", "instant"];
|
|
71
|
+
/** Custom modes loaded from library */
|
|
72
|
+
const _customModes = new Map();
|
|
71
73
|
|
|
72
74
|
const MODE_ALIASES = Object.freeze({
|
|
73
75
|
code: "agent",
|
|
@@ -116,7 +118,37 @@ function normalizeAgentMode(rawMode, fallback = "agent") {
|
|
|
116
118
|
const normalized = String(rawMode || "").trim().toLowerCase();
|
|
117
119
|
if (!normalized) return fallback;
|
|
118
120
|
const mapped = MODE_ALIASES[normalized] || normalized;
|
|
119
|
-
return
|
|
121
|
+
return getValidModes().includes(mapped) ? mapped : fallback;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get all valid modes including dynamically registered custom modes.
|
|
126
|
+
* @returns {string[]}
|
|
127
|
+
*/
|
|
128
|
+
function getValidModes() {
|
|
129
|
+
return [...CORE_MODES, ..._customModes.keys()];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get mode prefix for a given mode, including custom modes.
|
|
134
|
+
* @param {string} mode
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
function getModePrefix(mode) {
|
|
138
|
+
if (MODE_PREFIXES[mode] !== undefined) return MODE_PREFIXES[mode];
|
|
139
|
+
const custom = _customModes.get(mode);
|
|
140
|
+
return custom?.prefix || "";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get execution policy for a given mode, including custom modes.
|
|
145
|
+
* @param {string} mode
|
|
146
|
+
* @returns {object|null}
|
|
147
|
+
*/
|
|
148
|
+
function getModeExecPolicy(mode) {
|
|
149
|
+
if (MODE_EXEC_POLICIES[mode]) return MODE_EXEC_POLICIES[mode];
|
|
150
|
+
const custom = _customModes.get(mode);
|
|
151
|
+
return custom?.execPolicy || null;
|
|
120
152
|
}
|
|
121
153
|
|
|
122
154
|
function normalizeAttachments(input) {
|
|
@@ -908,7 +940,7 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
|
|
|
908
940
|
(options && options.sessionType ? String(options.sessionType) : "") ||
|
|
909
941
|
"primary";
|
|
910
942
|
const effectiveMode = normalizeAgentMode(options.mode || agentMode, agentMode);
|
|
911
|
-
const modePolicy =
|
|
943
|
+
const modePolicy = getModeExecPolicy(effectiveMode);
|
|
912
944
|
const timeoutMs = options.timeoutMs || modePolicy?.timeoutMs || PRIMARY_EXEC_TIMEOUT_MS;
|
|
913
945
|
const maxFailoverAttempts = Number.isInteger(options.maxFailoverAttempts)
|
|
914
946
|
? Math.max(0, Number(options.maxFailoverAttempts))
|
|
@@ -918,7 +950,7 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
|
|
|
918
950
|
const attachmentsAppended = options.attachmentsAppended === true;
|
|
919
951
|
|
|
920
952
|
// Apply mode prefix (options.mode overrides the global setting for this call)
|
|
921
|
-
const modePrefix =
|
|
953
|
+
const modePrefix = getModePrefix(effectiveMode);
|
|
922
954
|
const messageWithAttachments = attachments.length && !attachmentsAppended
|
|
923
955
|
? appendAttachmentsToPrompt(userMessage, attachments).message
|
|
924
956
|
: userMessage;
|
|
@@ -1241,8 +1273,8 @@ export function getAgentMode() {
|
|
|
1241
1273
|
*/
|
|
1242
1274
|
export function setAgentMode(mode) {
|
|
1243
1275
|
const normalized = normalizeAgentMode(mode, "");
|
|
1244
|
-
if (!
|
|
1245
|
-
return { ok: false, mode: agentMode, error: `Invalid mode "${mode}". Valid: ${
|
|
1276
|
+
if (!getValidModes().includes(normalized)) {
|
|
1277
|
+
return { ok: false, mode: agentMode, error: `Invalid mode "${mode}". Valid: ${getValidModes().join(", ")}` };
|
|
1246
1278
|
}
|
|
1247
1279
|
agentMode = normalized;
|
|
1248
1280
|
return { ok: true, mode: agentMode };
|
|
@@ -1254,10 +1286,52 @@ export function setAgentMode(mode) {
|
|
|
1254
1286
|
* @returns {string}
|
|
1255
1287
|
*/
|
|
1256
1288
|
export function applyModePrefix(userMessage) {
|
|
1257
|
-
const prefix =
|
|
1289
|
+
const prefix = getModePrefix(agentMode);
|
|
1258
1290
|
return prefix ? prefix + userMessage : userMessage;
|
|
1259
1291
|
}
|
|
1260
1292
|
|
|
1293
|
+
/**
|
|
1294
|
+
* Register a custom interaction mode at runtime.
|
|
1295
|
+
* Core modes cannot be overridden.
|
|
1296
|
+
* @param {string} id
|
|
1297
|
+
* @param {{ prefix?: string, execPolicy?: object|null, toolFilter?: object|null, description?: string }} config
|
|
1298
|
+
*/
|
|
1299
|
+
export function registerCustomMode(id, config) {
|
|
1300
|
+
if (!id || typeof id !== "string") return;
|
|
1301
|
+
const modeId = id.trim().toLowerCase();
|
|
1302
|
+
if (CORE_MODES.includes(modeId)) return;
|
|
1303
|
+
_customModes.set(modeId, {
|
|
1304
|
+
prefix: config.prefix || "",
|
|
1305
|
+
execPolicy: config.execPolicy || null,
|
|
1306
|
+
toolFilter: config.toolFilter || null,
|
|
1307
|
+
description: config.description || "",
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* List all available modes (core + custom) with metadata.
|
|
1313
|
+
* @returns {Array<{id: string, description: string, core: boolean}>}
|
|
1314
|
+
*/
|
|
1315
|
+
export function listAvailableModes() {
|
|
1316
|
+
const modes = CORE_MODES.map((m) => ({
|
|
1317
|
+
id: m,
|
|
1318
|
+
description: MODE_PREFIXES[m]?.slice(0, 80) || "Full agentic behavior",
|
|
1319
|
+
core: true,
|
|
1320
|
+
}));
|
|
1321
|
+
for (const [id, cfg] of _customModes) {
|
|
1322
|
+
modes.push({ id, description: cfg.description, core: false });
|
|
1323
|
+
}
|
|
1324
|
+
return modes;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Get all registered custom modes.
|
|
1329
|
+
* @returns {Array<{id: string, prefix: string, execPolicy: object|null, toolFilter: object|null, description: string}>}
|
|
1330
|
+
*/
|
|
1331
|
+
export function getCustomModes() {
|
|
1332
|
+
return [..._customModes.entries()].map(([id, cfg]) => ({ id, ...cfg }));
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1261
1335
|
/**
|
|
1262
1336
|
* Get the list of available agent adapters with capabilities.
|
|
1263
1337
|
* @returns {Array<{id:string, name:string, provider:string, available:boolean, busy:boolean, capabilities:object}>}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* retry-queue.mjs
|
|
3
|
+
*
|
|
4
|
+
* Pure reducer utilities for retry queue state.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const DEFAULT_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
function isoDayKey(ts) {
|
|
10
|
+
return new Date(ts).toISOString().slice(0, 10);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeItem(raw = {}, now = Date.now()) {
|
|
14
|
+
const taskId = String(raw.taskId || "").trim();
|
|
15
|
+
if (!taskId) return null;
|
|
16
|
+
const retryCount = Number.isFinite(Number(raw.retryCount))
|
|
17
|
+
? Math.max(0, Math.trunc(Number(raw.retryCount)))
|
|
18
|
+
: 0;
|
|
19
|
+
const nextAttemptAt = Number.isFinite(Number(raw.nextAttemptAt))
|
|
20
|
+
? Math.max(0, Math.trunc(Number(raw.nextAttemptAt)))
|
|
21
|
+
: now;
|
|
22
|
+
const updatedAt = Number.isFinite(Number(raw.updatedAt))
|
|
23
|
+
? Math.max(0, Math.trunc(Number(raw.updatedAt)))
|
|
24
|
+
: now;
|
|
25
|
+
const expiresAt = Number.isFinite(Number(raw.expiresAt))
|
|
26
|
+
? Math.max(0, Math.trunc(Number(raw.expiresAt)))
|
|
27
|
+
: nextAttemptAt + DEFAULT_RETENTION_MS;
|
|
28
|
+
return {
|
|
29
|
+
taskId,
|
|
30
|
+
taskTitle: String(raw.taskTitle || "").trim() || "",
|
|
31
|
+
lastError: String(raw.lastError || "").trim() || "",
|
|
32
|
+
retryCount,
|
|
33
|
+
maxRetries: Number.isFinite(Number(raw.maxRetries))
|
|
34
|
+
? Math.max(0, Math.trunc(Number(raw.maxRetries)))
|
|
35
|
+
: null,
|
|
36
|
+
nextAttemptAt,
|
|
37
|
+
status: String(raw.status || "pending"),
|
|
38
|
+
reason: String(raw.reason || "").trim() || "",
|
|
39
|
+
updatedAt,
|
|
40
|
+
expiresAt,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function materialize(itemsByTask) {
|
|
45
|
+
return Array.from(itemsByTask.values()).sort((a, b) => {
|
|
46
|
+
if (a.nextAttemptAt !== b.nextAttemptAt) return a.nextAttemptAt - b.nextAttemptAt;
|
|
47
|
+
return a.updatedAt - b.updatedAt;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ensureDay(state, now) {
|
|
52
|
+
const dayKey = isoDayKey(now);
|
|
53
|
+
if (state.stats.dayKey === dayKey) return state.stats;
|
|
54
|
+
return {
|
|
55
|
+
...state.stats,
|
|
56
|
+
dayKey,
|
|
57
|
+
totalRetriesToday: 0,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createRetryQueueState(now = Date.now()) {
|
|
62
|
+
return {
|
|
63
|
+
itemsByTask: new Map(),
|
|
64
|
+
stats: {
|
|
65
|
+
dayKey: isoDayKey(now),
|
|
66
|
+
totalRetriesToday: 0,
|
|
67
|
+
peakRetryDepth: 0,
|
|
68
|
+
exhaustedTaskIds: [],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function reduceRetryQueue(state, action = {}) {
|
|
74
|
+
const now = Number.isFinite(Number(action.now))
|
|
75
|
+
? Math.max(0, Math.trunc(Number(action.now)))
|
|
76
|
+
: Date.now();
|
|
77
|
+
const type = String(action.type || "").trim().toLowerCase();
|
|
78
|
+
const nextItems = new Map(state?.itemsByTask || []);
|
|
79
|
+
let stats = ensureDay(
|
|
80
|
+
state && state.stats ? state : createRetryQueueState(now),
|
|
81
|
+
now,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (type === "add" || type === "upsert") {
|
|
85
|
+
const item = normalizeItem(action.item, now);
|
|
86
|
+
if (!item) return { itemsByTask: nextItems, stats };
|
|
87
|
+
nextItems.set(item.taskId, item);
|
|
88
|
+
if (item.retryCount > stats.peakRetryDepth) {
|
|
89
|
+
stats = { ...stats, peakRetryDepth: item.retryCount };
|
|
90
|
+
}
|
|
91
|
+
return { itemsByTask: nextItems, stats };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (type === "remove") {
|
|
95
|
+
const taskId = String(action.taskId || "").trim();
|
|
96
|
+
if (taskId) nextItems.delete(taskId);
|
|
97
|
+
return { itemsByTask: nextItems, stats };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (type === "bump-count") {
|
|
101
|
+
const taskId = String(action.taskId || "").trim();
|
|
102
|
+
if (!taskId) return { itemsByTask: nextItems, stats };
|
|
103
|
+
const prev = nextItems.get(taskId) || normalizeItem({ taskId }, now);
|
|
104
|
+
if (!prev) return { itemsByTask: nextItems, stats };
|
|
105
|
+
const nextRetry = Number.isFinite(Number(action.retryCount))
|
|
106
|
+
? Math.max(0, Math.trunc(Number(action.retryCount)))
|
|
107
|
+
: prev.retryCount + 1;
|
|
108
|
+
const nextItem = normalizeItem({
|
|
109
|
+
...prev,
|
|
110
|
+
...action.item,
|
|
111
|
+
taskId,
|
|
112
|
+
retryCount: nextRetry,
|
|
113
|
+
updatedAt: now,
|
|
114
|
+
}, now);
|
|
115
|
+
nextItems.set(taskId, nextItem);
|
|
116
|
+
const peakRetryDepth = Math.max(stats.peakRetryDepth || 0, nextRetry);
|
|
117
|
+
stats = {
|
|
118
|
+
...stats,
|
|
119
|
+
totalRetriesToday: Math.max(0, (stats.totalRetriesToday || 0) + 1),
|
|
120
|
+
peakRetryDepth,
|
|
121
|
+
};
|
|
122
|
+
return { itemsByTask: nextItems, stats };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (type === "mark-exhausted") {
|
|
126
|
+
const taskId = String(action.taskId || "").trim();
|
|
127
|
+
if (!taskId) return { itemsByTask: nextItems, stats };
|
|
128
|
+
const exhausted = new Set(stats.exhaustedTaskIds || []);
|
|
129
|
+
exhausted.add(taskId);
|
|
130
|
+
nextItems.delete(taskId);
|
|
131
|
+
stats = { ...stats, exhaustedTaskIds: Array.from(exhausted) };
|
|
132
|
+
return { itemsByTask: nextItems, stats };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (type === "expire") {
|
|
136
|
+
for (const [taskId, item] of nextItems) {
|
|
137
|
+
if (!item) {
|
|
138
|
+
nextItems.delete(taskId);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (item.expiresAt <= now) {
|
|
142
|
+
nextItems.delete(taskId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { itemsByTask: nextItems, stats };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { itemsByTask: nextItems, stats };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function snapshotRetryQueue(state) {
|
|
152
|
+
const items = materialize(state?.itemsByTask || new Map());
|
|
153
|
+
return {
|
|
154
|
+
count: items.length,
|
|
155
|
+
items,
|
|
156
|
+
stats: {
|
|
157
|
+
totalRetriesToday: Number(state?.stats?.totalRetriesToday || 0),
|
|
158
|
+
peakRetryDepth: Number(state?.stats?.peakRetryDepth || 0),
|
|
159
|
+
exhaustedTaskIds: Array.isArray(state?.stats?.exhaustedTaskIds)
|
|
160
|
+
? [...new Set(state.stats.exhaustedTaskIds.map((id) => String(id || "").trim()).filter(Boolean))]
|
|
161
|
+
: [],
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|