bosun 0.41.7 → 0.41.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/agent/agent-event-bus.mjs +31 -2
- package/agent/agent-pool.mjs +251 -11
- package/agent/agent-prompts.mjs +5 -1
- package/agent/agent-supervisor.mjs +22 -0
- package/agent/primary-agent.mjs +115 -5
- package/cli.mjs +3 -2
- package/config/config.mjs +4 -1
- package/desktop/main.mjs +350 -25
- package/desktop/preload.cjs +8 -0
- package/desktop/preload.mjs +19 -0
- package/entrypoint.mjs +332 -0
- package/infra/health-status.mjs +72 -0
- package/infra/library-manager.mjs +58 -1
- package/infra/maintenance.mjs +1 -2
- package/infra/monitor.mjs +25 -7
- package/infra/session-tracker.mjs +30 -3
- package/package.json +10 -4
- package/server/bosun-mcp-server.mjs +1004 -0
- package/server/setup-web-server.mjs +287 -258
- package/server/ui-server.mjs +218 -23
- package/shell/claude-shell.mjs +14 -1
- package/shell/codex-model-profiles.mjs +166 -29
- package/shell/codex-shell.mjs +56 -18
- package/shell/opencode-providers.mjs +20 -8
- package/task/task-executor.mjs +28 -0
- package/task/task-store.mjs +13 -4
- package/tools/list-todos.mjs +7 -1
- package/ui/app.js +3 -2
- package/ui/components/agent-selector.js +127 -0
- package/ui/components/session-list.js +2 -0
- package/ui/demo-defaults.js +6 -6
- package/ui/modules/router.js +2 -0
- package/ui/modules/state.js +13 -5
- package/ui/tabs/chat.js +3 -0
- package/ui/tabs/library.js +284 -52
- package/ui/tabs/tasks.js +5 -13
- package/workflow/workflow-engine.mjs +16 -4
- package/workflow/workflow-nodes/definitions.mjs +37 -0
- package/workflow/workflow-nodes.mjs +489 -153
- package/workflow/workflow-templates.mjs +0 -5
- package/workflow-templates/github.mjs +106 -16
- package/workspace/worktree-manager.mjs +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ _The name "Bosun" comes from "boatswain", the ship's officer responsible for coo
|
|
|
12
12
|
_That maps directly to the Bosun project: it does not replace the captain or crew, it orchestrates the work. Our Bosun plans tasks, routes them to the right executors, enforces operational checks, and keeps humans in control while the system keeps delivery moving. Autonomous engineering with you in control of the operation._
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
|
-
<a href="https://bosun.engineer">Website</a> · <a href="https://bosun.engineer/docs/">Docs</a> · <a href="https://github.com/virtengine/bosun?tab=readme-ov-file#bosun">GitHub</a> · <a href="https://www.npmjs.com/package/bosun">npm</a> · <a href="https://github.com/virtengine/bosun/issues">Issues</a>
|
|
15
|
+
<a href="https://bosun.engineer">Website</a> · <a href="https://bosun.engineer/docs/">Docs</a> · <a href="https://github.com/virtengine/bosun?tab=readme-ov-file#bosun">GitHub</a> · <a href="https://www.npmjs.com/package/bosun">npm</a> · <a href="https://hub.docker.com/r/virtengine/bosun">Docker Hub</a> · <a href="https://github.com/virtengine/bosun/issues">Issues</a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
@@ -41,6 +41,28 @@ First run launches setup automatically. You can also run setup directly:
|
|
|
41
41
|
bosun --setup
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
### Docker quick start
|
|
45
|
+
|
|
46
|
+
Run Bosun with no local Node.js install — pull from Docker Hub:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
docker run -d --name bosun -p 3080:3080 \
|
|
50
|
+
-v bosun-data:/data \
|
|
51
|
+
-e BOSUN_API_KEY=your-secret-key \
|
|
52
|
+
virtengine/bosun:latest
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or build from the cloned repo:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/virtengine/bosun.git && cd bosun
|
|
59
|
+
docker compose up -d
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Open `https://localhost:3080` to start the setup wizard.
|
|
63
|
+
|
|
64
|
+
> **All installation options** — npm, source, Docker Hub, docker-compose, Electron desktop — are documented in [`INSTALL.md`](INSTALL.md) and on [bosun.engineer/docs/installation](https://bosun.engineer/docs/installation.html).
|
|
65
|
+
|
|
44
66
|
Requires:
|
|
45
67
|
|
|
46
68
|
- Node.js 18+
|
|
@@ -117,6 +117,7 @@ export class AgentEventBus {
|
|
|
117
117
|
this._sendTelegram = options.sendTelegram || null;
|
|
118
118
|
this._getTask = options.getTask || null;
|
|
119
119
|
this._setTaskStatus = options.setTaskStatus || null;
|
|
120
|
+
this._updateTask = options.updateTask || null;
|
|
120
121
|
this._supervisor = options.supervisor || null;
|
|
121
122
|
|
|
122
123
|
this._maxEventLogSize = options.maxEventLogSize || DEFAULTS.maxEventLogSize;
|
|
@@ -234,7 +235,7 @@ export class AgentEventBus {
|
|
|
234
235
|
|
|
235
236
|
// ── Dedup
|
|
236
237
|
const key = `${type}:${taskId}`;
|
|
237
|
-
const last = this._recentEmits.get(key);
|
|
238
|
+
const last = this._recentEmits.get(key);
|
|
238
239
|
if (typeof last === "number" && ts - last < this._dedupeWindowMs) return;
|
|
239
240
|
this._recentEmits.set(key, ts);
|
|
240
241
|
if (this._recentEmits.size > 200) {
|
|
@@ -856,6 +857,34 @@ export class AgentEventBus {
|
|
|
856
857
|
this._setTaskStatus(taskId, "blocked", "agent-event-bus");
|
|
857
858
|
} catch { /* best-effort */ }
|
|
858
859
|
}
|
|
860
|
+
|
|
861
|
+
// Persist blocked metadata so auto-recovery can unblock after cooldown.
|
|
862
|
+
if (this._updateTask) {
|
|
863
|
+
try {
|
|
864
|
+
const autoRecoverDelayMs = 30 * 60 * 1000; // 30 min cooldown
|
|
865
|
+
const retryAt = new Date(now + autoRecoverDelayMs).toISOString();
|
|
866
|
+
const blockedReason = String(recovery?.reason || "too many consecutive errors").trim();
|
|
867
|
+
const existingTask = typeof this._getTask === "function" ? this._getTask(taskId) : null;
|
|
868
|
+
const existingMeta = existingTask?.meta && typeof existingTask.meta === "object" ? existingTask.meta : {};
|
|
869
|
+
this._updateTask(taskId, {
|
|
870
|
+
blockedReason,
|
|
871
|
+
cooldownUntil: retryAt,
|
|
872
|
+
meta: {
|
|
873
|
+
...existingMeta,
|
|
874
|
+
autoRecovery: {
|
|
875
|
+
active: true,
|
|
876
|
+
reason: "consecutive_errors",
|
|
877
|
+
errorCount: recovery?.errorCount || 0,
|
|
878
|
+
pattern: classification?.pattern || null,
|
|
879
|
+
retryAt,
|
|
880
|
+
recoveryDelayMs: autoRecoverDelayMs,
|
|
881
|
+
recordedAt: new Date(now).toISOString(),
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
} catch { /* best-effort */ }
|
|
886
|
+
}
|
|
887
|
+
|
|
859
888
|
if (this._sendTelegram) {
|
|
860
889
|
const title = taskTitle || taskId;
|
|
861
890
|
this._sendTelegram(
|
|
@@ -1062,4 +1091,4 @@ export class AgentEventBus {
|
|
|
1062
1091
|
export function createAgentEventBus(options) {
|
|
1063
1092
|
return new AgentEventBus(options);
|
|
1064
1093
|
}
|
|
1065
|
-
|
|
1094
|
+
|
package/agent/agent-pool.mjs
CHANGED
|
@@ -43,6 +43,7 @@ import { resolve, dirname } from "node:path";
|
|
|
43
43
|
import { existsSync, readFileSync } from "node:fs";
|
|
44
44
|
import { homedir } from "node:os";
|
|
45
45
|
import { fileURLToPath } from "node:url";
|
|
46
|
+
import { createRequire } from "node:module";
|
|
46
47
|
import { loadConfig } from "../config/config.mjs";
|
|
47
48
|
import { resolveRepoRoot, resolveAgentRepoRoot } from "../config/repo-root.mjs";
|
|
48
49
|
import { resolveCodexProfileRuntime } from "../shell/codex-model-profiles.mjs";
|
|
@@ -97,6 +98,23 @@ const HARD_TIMEOUT_BUFFER_MS = 5 * 60_000; // 5 minutes
|
|
|
97
98
|
|
|
98
99
|
/** Tag for console logging */
|
|
99
100
|
const TAG = "[agent-pool]";
|
|
101
|
+
const require = createRequire(import.meta.url);
|
|
102
|
+
const MODULE_PRESENCE_CACHE = new Map();
|
|
103
|
+
|
|
104
|
+
function hasOptionalModule(specifier) {
|
|
105
|
+
if (MODULE_PRESENCE_CACHE.has(specifier)) {
|
|
106
|
+
return MODULE_PRESENCE_CACHE.get(specifier);
|
|
107
|
+
}
|
|
108
|
+
let ok = false;
|
|
109
|
+
try {
|
|
110
|
+
require.resolve(specifier);
|
|
111
|
+
ok = true;
|
|
112
|
+
} catch {
|
|
113
|
+
ok = false;
|
|
114
|
+
}
|
|
115
|
+
MODULE_PRESENCE_CACHE.set(specifier, ok);
|
|
116
|
+
return ok;
|
|
117
|
+
}
|
|
100
118
|
const MAX_PROMPT_BYTES = 180_000;
|
|
101
119
|
const MAX_SET_TIMEOUT_MS = 2_147_483_647; // Node.js setTimeout 32-bit signed max
|
|
102
120
|
let timeoutClampWarningKey = "";
|
|
@@ -424,13 +442,27 @@ function injectGitHubSessionEnv(baseEnv, token) {
|
|
|
424
442
|
|
|
425
443
|
/**
|
|
426
444
|
* Extract a human-readable task heading from the prompt built by _buildTaskPrompt.
|
|
427
|
-
* The first line is "#
|
|
445
|
+
* The first line is "# Task: Task Title" (legacy: "# TASKID — Task Title");
|
|
446
|
+
* we return the title portion only.
|
|
428
447
|
* Falls back to the raw first line if no em-dash separator is found.
|
|
429
448
|
* @param {string} prompt
|
|
430
449
|
* @returns {string}
|
|
431
450
|
*/
|
|
432
451
|
function extractTaskHeading(prompt) {
|
|
433
452
|
const firstLine = String(prompt || "").split(/\r?\n/)[0].replace(/^#+\s*/, "").trim();
|
|
453
|
+
if (!firstLine) return "Execute Task";
|
|
454
|
+
const lowerLine = firstLine.toLowerCase();
|
|
455
|
+
if (lowerLine.startsWith("task")) {
|
|
456
|
+
let index = 4;
|
|
457
|
+
while (index < firstLine.length && /\s/.test(firstLine[index])) index += 1;
|
|
458
|
+
const separator = firstLine[index];
|
|
459
|
+
if (separator === ":" || separator === "-" || separator === "\u2014") {
|
|
460
|
+
index += 1;
|
|
461
|
+
while (index < firstLine.length && /\s/.test(firstLine[index])) index += 1;
|
|
462
|
+
const title = firstLine.slice(index).trim();
|
|
463
|
+
if (title) return title;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
434
466
|
const dashIdx = firstLine.indexOf(" \u2014 ");
|
|
435
467
|
const title = dashIdx !== -1 ? firstLine.slice(dashIdx + 3).trim() : firstLine;
|
|
436
468
|
return title || "Execute Task";
|
|
@@ -580,15 +612,20 @@ function hasSdkPrerequisites(name, runtimeEnv = process.env) {
|
|
|
580
612
|
}
|
|
581
613
|
|
|
582
614
|
if (name === "codex") {
|
|
583
|
-
|
|
584
|
-
|
|
615
|
+
if (!hasOptionalModule("@openai/codex-sdk")) {
|
|
616
|
+
return { ok: false, reason: "@openai/codex-sdk not installed" };
|
|
617
|
+
}
|
|
618
|
+
// Codex auth can come from env vars, config env_key mappings, or persisted
|
|
619
|
+
// CLI login state (for example ~/.codex/auth.json). Because login-based
|
|
620
|
+
// auth is valid and hard to validate exhaustively, avoid false negatives.
|
|
585
621
|
const hasKey =
|
|
586
622
|
runtimeEnv.OPENAI_API_KEY ||
|
|
587
623
|
runtimeEnv.AZURE_OPENAI_API_KEY ||
|
|
588
624
|
runtimeEnv.CODEX_MODEL_PROFILE_XL_API_KEY ||
|
|
589
625
|
runtimeEnv.CODEX_MODEL_PROFILE_M_API_KEY;
|
|
590
626
|
if (hasKey) return { ok: true, reason: null };
|
|
591
|
-
|
|
627
|
+
|
|
628
|
+
// Check ~/.codex/config.toml — Codex CLI SDK reads auth env_key refs from there.
|
|
592
629
|
try {
|
|
593
630
|
const configToml = resolve(homedir(), ".codex", "config.toml");
|
|
594
631
|
if (existsSync(configToml)) {
|
|
@@ -601,14 +638,33 @@ function hasSdkPrerequisites(name, runtimeEnv = process.env) {
|
|
|
601
638
|
} catch {
|
|
602
639
|
// best effort — fall through to failure
|
|
603
640
|
}
|
|
604
|
-
|
|
641
|
+
|
|
642
|
+
// Login-based auth sessions are sufficient even without env keys.
|
|
643
|
+
try {
|
|
644
|
+
const authJson = resolve(homedir(), ".codex", "auth.json");
|
|
645
|
+
if (existsSync(authJson)) {
|
|
646
|
+
const authText = readFileSync(authJson, "utf8").trim();
|
|
647
|
+
if (authText) return { ok: true, reason: null };
|
|
648
|
+
}
|
|
649
|
+
} catch {
|
|
650
|
+
// best effort — fall through to permissive behavior
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Do not hard-block Codex when auth source cannot be determined.
|
|
654
|
+
return { ok: true, reason: null };
|
|
605
655
|
}
|
|
606
656
|
if (name === "copilot") {
|
|
657
|
+
if (!hasOptionalModule("@github/copilot-sdk")) {
|
|
658
|
+
return { ok: false, reason: "@github/copilot-sdk not installed" };
|
|
659
|
+
}
|
|
607
660
|
// Copilot auth can come from multiple sources (OAuth manager, gh auth,
|
|
608
661
|
// VS Code Copilot login, env tokens). Don't block execution here.
|
|
609
662
|
return { ok: true, reason: null };
|
|
610
663
|
}
|
|
611
664
|
if (name === "claude") {
|
|
665
|
+
if (!hasOptionalModule("@anthropic-ai/claude-agent-sdk")) {
|
|
666
|
+
return { ok: false, reason: "@anthropic-ai/claude-agent-sdk not installed" };
|
|
667
|
+
}
|
|
612
668
|
const hasKey = runtimeEnv.ANTHROPIC_API_KEY;
|
|
613
669
|
if (!hasKey) {
|
|
614
670
|
return { ok: false, reason: "no ANTHROPIC_API_KEY" };
|
|
@@ -1065,6 +1121,19 @@ export function setPoolSdk(name) {
|
|
|
1065
1121
|
export function resetPoolSdkCache() {
|
|
1066
1122
|
resolvedSdkName = null;
|
|
1067
1123
|
resolutionLogged = false;
|
|
1124
|
+
sdkFailureCooldownUntil.clear();
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
export function setSdkFailureCooldownForTest(name, cooldownRemainingMs, nowMs = Date.now()) {
|
|
1128
|
+
if (!SDK_ADAPTERS[name]) {
|
|
1129
|
+
throw new Error(`Unknown SDK: ${name}`);
|
|
1130
|
+
}
|
|
1131
|
+
const remainingMs = Number(cooldownRemainingMs);
|
|
1132
|
+
if (!Number.isFinite(remainingMs) || remainingMs <= 0) {
|
|
1133
|
+
sdkFailureCooldownUntil.delete(name);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
sdkFailureCooldownUntil.set(name, nowMs + Math.trunc(remainingMs));
|
|
1068
1137
|
}
|
|
1069
1138
|
|
|
1070
1139
|
/**
|
|
@@ -1145,6 +1214,11 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1145
1214
|
: process.env;
|
|
1146
1215
|
const codexSessionEnv = applyNodeWarningSuppressionEnv(codexRuntimeEnv);
|
|
1147
1216
|
const codexOpts = buildCodexSdkOptions(codexSessionEnv);
|
|
1217
|
+
const explicitEnvModel = String(envOverrides?.CODEX_MODEL || "").trim();
|
|
1218
|
+
if (explicitEnvModel) {
|
|
1219
|
+
codexOpts.env = { ...(codexOpts.env || {}), CODEX_MODEL: explicitEnvModel };
|
|
1220
|
+
codexOpts.config = { ...(codexOpts.config || {}), model: explicitEnvModel };
|
|
1221
|
+
}
|
|
1148
1222
|
const modelOverride = String(extra?.model || "").trim();
|
|
1149
1223
|
if (modelOverride) {
|
|
1150
1224
|
codexOpts.env = { ...(codexOpts.env || {}), CODEX_MODEL: modelOverride };
|
|
@@ -2384,6 +2458,7 @@ export async function launchEphemeralThread(
|
|
|
2384
2458
|
];
|
|
2385
2459
|
|
|
2386
2460
|
let lastAttemptResult = null;
|
|
2461
|
+
let primaryFailureResult = null;
|
|
2387
2462
|
const triedSdkNames = [];
|
|
2388
2463
|
const missingPrereqSdks = [];
|
|
2389
2464
|
const cooledDownSdks = [];
|
|
@@ -2443,6 +2518,10 @@ export async function launchEphemeralThread(
|
|
|
2443
2518
|
return result;
|
|
2444
2519
|
}
|
|
2445
2520
|
|
|
2521
|
+
if (name === primaryName) {
|
|
2522
|
+
primaryFailureResult = result;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2446
2525
|
applySdkFailureCooldown(name, result.error);
|
|
2447
2526
|
|
|
2448
2527
|
if (name === primaryName) {
|
|
@@ -2456,15 +2535,170 @@ export async function launchEphemeralThread(
|
|
|
2456
2535
|
}
|
|
2457
2536
|
}
|
|
2458
2537
|
|
|
2538
|
+
const eligibleSdks = Array.from(
|
|
2539
|
+
new Set(attemptOrder.filter((name) => SDK_ADAPTERS[name] && !isDisabled(name))),
|
|
2540
|
+
);
|
|
2541
|
+
const runnableSdks = eligibleSdks.filter((name) => {
|
|
2542
|
+
const prereq = hasSdkPrerequisites(name, sessionEnv);
|
|
2543
|
+
if (!prereq.ok) {
|
|
2544
|
+
if (!missingPrereqSdks.some((entry) => entry.name === name)) {
|
|
2545
|
+
missingPrereqSdks.push({ name, reason: prereq.reason });
|
|
2546
|
+
}
|
|
2547
|
+
return false;
|
|
2548
|
+
}
|
|
2549
|
+
return true;
|
|
2550
|
+
});
|
|
2551
|
+
const cooledDownSet = new Set(cooledDownSdks.map((entry) => entry.name));
|
|
2552
|
+
const missingPrereqSet = new Set(missingPrereqSdks.map((entry) => entry.name));
|
|
2553
|
+
const allEligibleUnavailable =
|
|
2554
|
+
eligibleSdks.length > 0 &&
|
|
2555
|
+
eligibleSdks.every(
|
|
2556
|
+
(name) => cooledDownSet.has(name) || missingPrereqSet.has(name),
|
|
2557
|
+
);
|
|
2558
|
+
|
|
2559
|
+
if (
|
|
2560
|
+
!ignoreSdkCooldown &&
|
|
2561
|
+
!lastAttemptResult &&
|
|
2562
|
+
triedSdkNames.length === 0 &&
|
|
2563
|
+
runnableSdks.length > 0 &&
|
|
2564
|
+
runnableSdks.every((name) => cooledDownSet.has(name))
|
|
2565
|
+
) {
|
|
2566
|
+
const forcedRetryOrder = runnableSdks.includes(primaryName)
|
|
2567
|
+
? [primaryName, ...runnableSdks.filter((name) => name !== primaryName)]
|
|
2568
|
+
: runnableSdks;
|
|
2569
|
+
|
|
2570
|
+
for (const name of forcedRetryOrder) {
|
|
2571
|
+
const cooldownRemainingMs = getSdkCooldownRemainingMs(name);
|
|
2572
|
+
if (cooldownRemainingMs <= 0) continue;
|
|
2573
|
+
|
|
2574
|
+
const prereq = hasSdkPrerequisites(name, sessionEnv);
|
|
2575
|
+
if (!prereq.ok) {
|
|
2576
|
+
if (!missingPrereqSdks.some((entry) => entry.name === name)) {
|
|
2577
|
+
missingPrereqSdks.push({ name, reason: prereq.reason });
|
|
2578
|
+
}
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
const remainingSec = Math.max(1, Math.ceil(cooldownRemainingMs / 1000));
|
|
2583
|
+
console.warn(
|
|
2584
|
+
`${TAG} all eligible SDKs are cooling down; force-retrying "${name}" (${remainingSec}s remaining)`,
|
|
2585
|
+
);
|
|
2586
|
+
|
|
2587
|
+
triedSdkNames.push(name);
|
|
2588
|
+
const launcher = await SDK_ADAPTERS[name].load();
|
|
2589
|
+
const result = await launcher(prompt, cwd, timeoutMs, launchExtra);
|
|
2590
|
+
lastAttemptResult = result;
|
|
2591
|
+
|
|
2592
|
+
if (result.success) {
|
|
2593
|
+
return result;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
if (!shouldFallbackForSdkError(result.error)) {
|
|
2597
|
+
return result;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
applySdkFailureCooldown(name, result.error);
|
|
2601
|
+
break;
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
// If every eligible SDK is unavailable (cooldown and/or missing credentials),
|
|
2606
|
+
// force one cooled-down attempt so work is not blocked for the full cooldown window.
|
|
2607
|
+
const shouldForceCooldownBypassAttempt =
|
|
2608
|
+
!ignoreSdkCooldown &&
|
|
2609
|
+
!lastAttemptResult &&
|
|
2610
|
+
triedSdkNames.length === 0 &&
|
|
2611
|
+
cooledDownSet.has(primaryName) &&
|
|
2612
|
+
allEligibleUnavailable;
|
|
2613
|
+
const forcedSdkName = shouldForceCooldownBypassAttempt
|
|
2614
|
+
? cooledDownSet.has(primaryName)
|
|
2615
|
+
? primaryName
|
|
2616
|
+
: eligibleSdks.find((name) => cooledDownSet.has(name)) || null
|
|
2617
|
+
: null;
|
|
2618
|
+
|
|
2619
|
+
if (forcedSdkName) {
|
|
2620
|
+
const prereq = hasSdkPrerequisites(forcedSdkName, sessionEnv);
|
|
2621
|
+
if (!prereq.ok) {
|
|
2622
|
+
// Cooldown means we recently attempted this SDK. Keep one forced retry path
|
|
2623
|
+
// to avoid hard-blocking work on strict prerequisite heuristics.
|
|
2624
|
+
if (
|
|
2625
|
+
!missingPrereqSdks.some(
|
|
2626
|
+
(entry) => entry.name === forcedSdkName && entry.reason === prereq.reason,
|
|
2627
|
+
)
|
|
2628
|
+
) {
|
|
2629
|
+
missingPrereqSdks.push({ name: forcedSdkName, reason: prereq.reason });
|
|
2630
|
+
}
|
|
2631
|
+
console.warn(
|
|
2632
|
+
`${TAG} all eligible SDKs unavailable; bypassing SDK "${forcedSdkName}" prerequisite gate for forced retry (${prereq.reason})`,
|
|
2633
|
+
);
|
|
2634
|
+
} else {
|
|
2635
|
+
console.warn(
|
|
2636
|
+
`${TAG} no runnable fallback SDK is available (cooldown/prerequisite gate); forcing SDK "${forcedSdkName}" retry`,
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
triedSdkNames.push(forcedSdkName);
|
|
2640
|
+
const launcher = await SDK_ADAPTERS[forcedSdkName].load();
|
|
2641
|
+
const forcedResult = await launcher(prompt, cwd, timeoutMs, launchExtra);
|
|
2642
|
+
if (forcedResult.success) {
|
|
2643
|
+
return forcedResult;
|
|
2644
|
+
}
|
|
2645
|
+
lastAttemptResult = forcedResult;
|
|
2646
|
+
if (!shouldFallbackForSdkError(forcedResult.error)) {
|
|
2647
|
+
return forcedResult;
|
|
2648
|
+
}
|
|
2649
|
+
applySdkFailureCooldown(forcedSdkName, forcedResult.error);
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
// Recovery path: when all SDKs were skipped due cooldown/prereq gates, force
|
|
2653
|
+
// one direct attempt against cooled-down SDKs to surface a concrete error.
|
|
2654
|
+
if (!lastAttemptResult && triedSdkNames.length === 0 && cooledDownSdks.length > 0) {
|
|
2655
|
+
const cooledDownRescueOrder = cooledDownSdks
|
|
2656
|
+
.map((entry) => entry.name)
|
|
2657
|
+
.filter((name, idx, arr) => SDK_ADAPTERS[name] && arr.indexOf(name) === idx)
|
|
2658
|
+
.sort((a, b) => {
|
|
2659
|
+
if (a === primaryName) return -1;
|
|
2660
|
+
if (b === primaryName) return 1;
|
|
2661
|
+
return 0;
|
|
2662
|
+
});
|
|
2663
|
+
|
|
2664
|
+
for (const name of cooledDownRescueOrder) {
|
|
2665
|
+
const remainingMs = getSdkCooldownRemainingMs(name);
|
|
2666
|
+
const remainingSec = Math.max(1, Math.ceil(remainingMs / 1000));
|
|
2667
|
+
console.warn(
|
|
2668
|
+
`${TAG} no runnable SDK remained after gating; rescue-attempting cooled-down SDK "${name}" (${remainingSec}s remaining)`,
|
|
2669
|
+
);
|
|
2670
|
+
|
|
2671
|
+
triedSdkNames.push(name);
|
|
2672
|
+
const launcher = await SDK_ADAPTERS[name].load();
|
|
2673
|
+
const result = await launcher(prompt, cwd, timeoutMs, launchExtra);
|
|
2674
|
+
lastAttemptResult = result;
|
|
2675
|
+
|
|
2676
|
+
if (result.success) {
|
|
2677
|
+
return result;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
if (!shouldFallbackForSdkError(result.error)) {
|
|
2681
|
+
return result;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
if (name === primaryName) {
|
|
2685
|
+
primaryFailureResult = result;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
applySdkFailureCooldown(name, result.error);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2459
2691
|
// ── All SDKs exhausted ───────────────────────────────────────────────────
|
|
2460
2692
|
if (lastAttemptResult) {
|
|
2693
|
+
if (
|
|
2694
|
+
primaryFailureResult &&
|
|
2695
|
+
lastAttemptResult.sdk !== primaryFailureResult.sdk
|
|
2696
|
+
) {
|
|
2697
|
+
return primaryFailureResult;
|
|
2698
|
+
}
|
|
2461
2699
|
return lastAttemptResult;
|
|
2462
2700
|
}
|
|
2463
2701
|
|
|
2464
|
-
const eligibleSdks = Array.from(
|
|
2465
|
-
new Set(attemptOrder.filter((name) => SDK_ADAPTERS[name] && !isDisabled(name))),
|
|
2466
|
-
);
|
|
2467
|
-
|
|
2468
2702
|
let errorMsg = `${TAG} no SDK available.`;
|
|
2469
2703
|
if (triedSdkNames.length > 0) {
|
|
2470
2704
|
errorMsg += ` Tried: ${triedSdkNames.join(", ")}.`;
|
|
@@ -2888,6 +3122,11 @@ async function resumeCodexThread(threadId, prompt, cwd, timeoutMs, extra = {}) {
|
|
|
2888
3122
|
: process.env;
|
|
2889
3123
|
const codexSessionEnv = applyNodeWarningSuppressionEnv(codexRuntimeEnv);
|
|
2890
3124
|
const codexOpts = buildCodexSdkOptions(codexSessionEnv);
|
|
3125
|
+
const explicitEnvModel = String(envOverrides?.CODEX_MODEL || "").trim();
|
|
3126
|
+
if (explicitEnvModel) {
|
|
3127
|
+
codexOpts.env = { ...(codexOpts.env || {}), CODEX_MODEL: explicitEnvModel };
|
|
3128
|
+
codexOpts.config = { ...(codexOpts.config || {}), model: explicitEnvModel };
|
|
3129
|
+
}
|
|
2891
3130
|
const modelOverride = String(extra?.model || "").trim();
|
|
2892
3131
|
if (modelOverride) {
|
|
2893
3132
|
codexOpts.env = { ...(codexOpts.env || {}), CODEX_MODEL: modelOverride };
|
|
@@ -3081,6 +3320,7 @@ async function resumeGenericThread(
|
|
|
3081
3320
|
* @param {string} [extra.sessionType] Interaction type used for context-shredding policy lookup.
|
|
3082
3321
|
* @param {boolean} [extra.forceContextShredding] Force compression regardless of session type defaults.
|
|
3083
3322
|
* @param {boolean} [extra.skipContextShredding] Skip compression regardless of policy.
|
|
3323
|
+
* @param {boolean} [extra.pinSdk] Keep SDK pinned when provided (disables fallback).
|
|
3084
3324
|
* @param {Function} [extra.onEvent] Event callback.
|
|
3085
3325
|
* @param {AbortController} [extra.abortController]
|
|
3086
3326
|
* @returns {Promise<{ success: boolean, output: string, items: Array, error: string|null, sdk: string, threadId: string|null, resumed: boolean }>}
|
|
@@ -3105,8 +3345,8 @@ export async function launchOrResumeThread(
|
|
|
3105
3345
|
restExtra.envOverrides = applyNodeWarningSuppressionEnv(restExtra.envOverrides);
|
|
3106
3346
|
// Pass taskKey through as steer key so SDK launchers can register active sessions
|
|
3107
3347
|
restExtra.taskKey = taskKey;
|
|
3108
|
-
if (restExtra.sdk) {
|
|
3109
|
-
// Task-bound runs with an explicit SDK
|
|
3348
|
+
if (restExtra.sdk && restExtra.pinSdk === true) {
|
|
3349
|
+
// Task-bound runs with an explicit SDK can optionally stay pinned to that SDK.
|
|
3110
3350
|
restExtra.disableFallback = true;
|
|
3111
3351
|
}
|
|
3112
3352
|
timeoutMs = clampMonitorMonitorTimeout(timeoutMs, taskKey);
|
package/agent/agent-prompts.mjs
CHANGED
|
@@ -8,7 +8,11 @@ export { AGENT_PROMPT_DEFINITIONS, PROMPT_WORKSPACE_DIR };
|
|
|
8
8
|
|
|
9
9
|
function normalizeTemplateValue(value) {
|
|
10
10
|
if (value == null) return "";
|
|
11
|
-
if (typeof value === "string")
|
|
11
|
+
if (typeof value === "string") {
|
|
12
|
+
const text = value.trim();
|
|
13
|
+
if (/^\{\{\s*[\w.-]+\s*\}\}$/.test(text)) return "";
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
12
16
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
13
17
|
return String(value);
|
|
14
18
|
}
|
|
@@ -384,6 +384,7 @@ export class AgentSupervisor {
|
|
|
384
384
|
this._sendTelegram = opts.sendTelegram || null;
|
|
385
385
|
this._getTask = opts.getTask || null;
|
|
386
386
|
this._setTaskStatus = opts.setTaskStatus || null;
|
|
387
|
+
this._updateTask = opts.updateTask || null;
|
|
387
388
|
this._reviewAgent = opts.reviewAgent || null;
|
|
388
389
|
|
|
389
390
|
// ── Dispatch functions ──
|
|
@@ -525,6 +526,27 @@ export class AgentSupervisor {
|
|
|
525
526
|
this._setTaskStatus(taskId, "blocked", "supervisor");
|
|
526
527
|
} catch { /* best-effort */ }
|
|
527
528
|
}
|
|
529
|
+
// Persist recovery metadata so auto-recovery can unblock later
|
|
530
|
+
if (this._updateTask) {
|
|
531
|
+
try {
|
|
532
|
+
const existing = this._resolveTask(taskId);
|
|
533
|
+
const existingMeta = existing?.meta || {};
|
|
534
|
+
const cooldownMs = 30 * 60_000; // 30 minutes
|
|
535
|
+
this._updateTask(taskId, {
|
|
536
|
+
blockedReason: `Supervisor: ${reason} (situation: ${situation})`,
|
|
537
|
+
cooldownUntil: Date.now() + cooldownMs,
|
|
538
|
+
meta: {
|
|
539
|
+
...existingMeta,
|
|
540
|
+
autoRecovery: {
|
|
541
|
+
active: true,
|
|
542
|
+
reason: "supervisor_block",
|
|
543
|
+
situation,
|
|
544
|
+
retryAt: Date.now() + cooldownMs,
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
} catch { /* best-effort */ }
|
|
549
|
+
}
|
|
528
550
|
const task = this._resolveTask(taskId);
|
|
529
551
|
const title = task?.title || taskId;
|
|
530
552
|
const state = this._ensureTaskState(taskId);
|