bosun 0.41.8 → 0.41.10
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/README.md +23 -1
- package/agent/agent-event-bus.mjs +31 -2
- package/agent/agent-pool.mjs +275 -40
- package/agent/agent-prompts.mjs +9 -1
- package/agent/agent-supervisor.mjs +22 -0
- package/agent/autofix.mjs +1 -1
- package/agent/primary-agent.mjs +115 -5
- package/cli.mjs +3 -2
- package/config/config.mjs +47 -33
- package/config/context-shredding-config.mjs +1 -1
- package/config/repo-root.mjs +41 -33
- package/desktop/main.mjs +350 -25
- package/desktop/preload.cjs +8 -0
- package/desktop/preload.mjs +19 -0
- package/entrypoint.mjs +332 -0
- package/git/sdk-conflict-resolver.mjs +1 -1
- package/infra/health-status.mjs +72 -0
- package/infra/library-manager.mjs +58 -1
- package/infra/maintenance.mjs +1 -2
- package/infra/monitor.mjs +26 -8
- package/infra/session-tracker.mjs +30 -3
- package/package.json +12 -4
- package/server/bosun-mcp-server.mjs +1004 -0
- package/server/setup-web-server.mjs +288 -259
- package/server/ui-server.mjs +1323 -26
- package/shell/claude-shell.mjs +14 -1
- package/shell/codex-config.mjs +1 -1
- package/shell/codex-model-profiles.mjs +170 -30
- package/shell/codex-shell.mjs +63 -18
- package/shell/opencode-providers.mjs +20 -8
- package/task/task-executor.mjs +28 -0
- package/task/task-store.mjs +13 -4
- package/telegram/telegram-sentinel.mjs +54 -3
- 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 +15 -10
- package/ui/demo-defaults.js +334 -336
- 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/ui/tabs/workflows.js +766 -3
- package/workflow/workflow-engine.mjs +246 -5
- package/workflow/workflow-nodes/definitions.mjs +37 -0
- package/workflow/workflow-nodes.mjs +1014 -184
- package/workflow/workflow-templates.mjs +0 -5
- package/workflow-templates/_helpers.mjs +253 -0
- package/workflow-templates/agents.mjs +199 -226
- package/workflow-templates/github.mjs +106 -16
- package/workflow-templates/sub-workflows.mjs +233 -0
- package/workflow-templates/task-execution.mjs +125 -471
- package/workflow-templates/task-lifecycle.mjs +11 -48
- package/workspace/command-diagnostics.mjs +460 -0
- package/workspace/context-cache.mjs +396 -28
- package/workspace/worktree-manager.mjs +1 -1
package/.env.example
CHANGED
|
@@ -201,7 +201,7 @@ VOICE_DELEGATE_EXECUTOR=codex-sdk
|
|
|
201
201
|
# earlier: it summarizes large, noisy command outputs before they ever land in
|
|
202
202
|
# the active turn, while preserving a `bosun --tool-log <id>` retrieval path.
|
|
203
203
|
# CONTEXT_SHREDDING_ENABLED=true
|
|
204
|
-
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_ENABLED=
|
|
204
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_ENABLED=true
|
|
205
205
|
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MODE=auto
|
|
206
206
|
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MIN_CHARS=4000
|
|
207
207
|
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_TARGET_CHARS=1800
|
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";
|
|
@@ -54,8 +55,7 @@ import {
|
|
|
54
55
|
MAX_STREAM_RETRIES,
|
|
55
56
|
} from "../infra/stream-resilience.mjs";
|
|
56
57
|
import { ensureTestRuntimeSandbox } from "../infra/test-runtime.mjs";
|
|
57
|
-
import {
|
|
58
|
-
import { resolveContextShreddingOptions } from "../config/context-shredding-config.mjs";
|
|
58
|
+
import { maybeCompressSessionItems } from "../workspace/context-cache.mjs";
|
|
59
59
|
|
|
60
60
|
// Lazy-load MCP registry to avoid circular dependencies.
|
|
61
61
|
// Cached at module scope per AGENTS.md hard rules.
|
|
@@ -97,6 +97,31 @@ const HARD_TIMEOUT_BUFFER_MS = 5 * 60_000; // 5 minutes
|
|
|
97
97
|
|
|
98
98
|
/** Tag for console logging */
|
|
99
99
|
const TAG = "[agent-pool]";
|
|
100
|
+
const require = createRequire(import.meta.url);
|
|
101
|
+
const MODULE_PRESENCE_CACHE = new Map();
|
|
102
|
+
|
|
103
|
+
function hasOptionalModule(specifier) {
|
|
104
|
+
if (MODULE_PRESENCE_CACHE.has(specifier)) {
|
|
105
|
+
return MODULE_PRESENCE_CACHE.get(specifier);
|
|
106
|
+
}
|
|
107
|
+
let ok = false;
|
|
108
|
+
try {
|
|
109
|
+
require.resolve(specifier);
|
|
110
|
+
ok = true;
|
|
111
|
+
} catch {
|
|
112
|
+
// ESM-only packages have no CJS "require" export so require.resolve
|
|
113
|
+
// throws even when the package is installed. Fall back to checking
|
|
114
|
+
// whether the package directory exists on disk.
|
|
115
|
+
try {
|
|
116
|
+
const pkgDir = resolve(__dirname, "..", "node_modules", ...specifier.split("/"));
|
|
117
|
+
ok = existsSync(resolve(pkgDir, "package.json"));
|
|
118
|
+
} catch {
|
|
119
|
+
ok = false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
MODULE_PRESENCE_CACHE.set(specifier, ok);
|
|
123
|
+
return ok;
|
|
124
|
+
}
|
|
100
125
|
const MAX_PROMPT_BYTES = 180_000;
|
|
101
126
|
const MAX_SET_TIMEOUT_MS = 2_147_483_647; // Node.js setTimeout 32-bit signed max
|
|
102
127
|
let timeoutClampWarningKey = "";
|
|
@@ -226,29 +251,12 @@ async function maybeCompressResultItems(
|
|
|
226
251
|
|
|
227
252
|
const resolvedSessionType = normalizeSessionType(sessionType, "task");
|
|
228
253
|
const agentType = normalizeSdkForShredding(sdk);
|
|
229
|
-
|
|
230
|
-
resolvedSessionType,
|
|
254
|
+
return maybeCompressSessionItems(items, {
|
|
255
|
+
sessionType: resolvedSessionType,
|
|
231
256
|
agentType,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const usagePct = estimateContextUsagePct(items);
|
|
236
|
-
const threshold = Number.isFinite(shreddingOpts?.contextUsageThreshold)
|
|
237
|
-
? Number(shreddingOpts.contextUsageThreshold)
|
|
238
|
-
: 0.5;
|
|
239
|
-
if (usagePct < threshold) return items;
|
|
240
|
-
|
|
241
|
-
shreddingOpts.contextUsagePct = usagePct;
|
|
242
|
-
const compressedItems = await compressAllItems(items, shreddingOpts);
|
|
243
|
-
try {
|
|
244
|
-
const savings = estimateSavings(items, compressedItems);
|
|
245
|
-
if (savings.savedChars > 0) {
|
|
246
|
-
recordShreddingEvent({ ...savings, agentType: agentType || sdk });
|
|
247
|
-
}
|
|
248
|
-
} catch {
|
|
249
|
-
/* non-fatal */
|
|
250
|
-
}
|
|
251
|
-
return compressedItems;
|
|
257
|
+
force: forceCompression,
|
|
258
|
+
skip: skipCompression,
|
|
259
|
+
});
|
|
252
260
|
}
|
|
253
261
|
|
|
254
262
|
function resolveCodexStreamSafety(totalTimeoutMs) {
|
|
@@ -424,13 +432,27 @@ function injectGitHubSessionEnv(baseEnv, token) {
|
|
|
424
432
|
|
|
425
433
|
/**
|
|
426
434
|
* Extract a human-readable task heading from the prompt built by _buildTaskPrompt.
|
|
427
|
-
* The first line is "#
|
|
435
|
+
* The first line is "# Task: Task Title" (legacy: "# TASKID — Task Title");
|
|
436
|
+
* we return the title portion only.
|
|
428
437
|
* Falls back to the raw first line if no em-dash separator is found.
|
|
429
438
|
* @param {string} prompt
|
|
430
439
|
* @returns {string}
|
|
431
440
|
*/
|
|
432
441
|
function extractTaskHeading(prompt) {
|
|
433
442
|
const firstLine = String(prompt || "").split(/\r?\n/)[0].replace(/^#+\s*/, "").trim();
|
|
443
|
+
if (!firstLine) return "Execute Task";
|
|
444
|
+
const lowerLine = firstLine.toLowerCase();
|
|
445
|
+
if (lowerLine.startsWith("task")) {
|
|
446
|
+
let index = 4;
|
|
447
|
+
while (index < firstLine.length && /\s/.test(firstLine[index])) index += 1;
|
|
448
|
+
const separator = firstLine[index];
|
|
449
|
+
if (separator === ":" || separator === "-" || separator === "\u2014") {
|
|
450
|
+
index += 1;
|
|
451
|
+
while (index < firstLine.length && /\s/.test(firstLine[index])) index += 1;
|
|
452
|
+
const title = firstLine.slice(index).trim();
|
|
453
|
+
if (title) return title;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
434
456
|
const dashIdx = firstLine.indexOf(" \u2014 ");
|
|
435
457
|
const title = dashIdx !== -1 ? firstLine.slice(dashIdx + 3).trim() : firstLine;
|
|
436
458
|
return title || "Execute Task";
|
|
@@ -580,15 +602,20 @@ function hasSdkPrerequisites(name, runtimeEnv = process.env) {
|
|
|
580
602
|
}
|
|
581
603
|
|
|
582
604
|
if (name === "codex") {
|
|
583
|
-
|
|
584
|
-
|
|
605
|
+
if (!hasOptionalModule("@openai/codex-sdk")) {
|
|
606
|
+
return { ok: false, reason: "@openai/codex-sdk not installed" };
|
|
607
|
+
}
|
|
608
|
+
// Codex auth can come from env vars, config env_key mappings, or persisted
|
|
609
|
+
// CLI login state (for example ~/.codex/auth.json). Because login-based
|
|
610
|
+
// auth is valid and hard to validate exhaustively, avoid false negatives.
|
|
585
611
|
const hasKey =
|
|
586
612
|
runtimeEnv.OPENAI_API_KEY ||
|
|
587
613
|
runtimeEnv.AZURE_OPENAI_API_KEY ||
|
|
588
614
|
runtimeEnv.CODEX_MODEL_PROFILE_XL_API_KEY ||
|
|
589
615
|
runtimeEnv.CODEX_MODEL_PROFILE_M_API_KEY;
|
|
590
616
|
if (hasKey) return { ok: true, reason: null };
|
|
591
|
-
|
|
617
|
+
|
|
618
|
+
// Check ~/.codex/config.toml — Codex CLI SDK reads auth env_key refs from there.
|
|
592
619
|
try {
|
|
593
620
|
const configToml = resolve(homedir(), ".codex", "config.toml");
|
|
594
621
|
if (existsSync(configToml)) {
|
|
@@ -601,14 +628,33 @@ function hasSdkPrerequisites(name, runtimeEnv = process.env) {
|
|
|
601
628
|
} catch {
|
|
602
629
|
// best effort — fall through to failure
|
|
603
630
|
}
|
|
604
|
-
|
|
631
|
+
|
|
632
|
+
// Login-based auth sessions are sufficient even without env keys.
|
|
633
|
+
try {
|
|
634
|
+
const authJson = resolve(homedir(), ".codex", "auth.json");
|
|
635
|
+
if (existsSync(authJson)) {
|
|
636
|
+
const authText = readFileSync(authJson, "utf8").trim();
|
|
637
|
+
if (authText) return { ok: true, reason: null };
|
|
638
|
+
}
|
|
639
|
+
} catch {
|
|
640
|
+
// best effort — fall through to permissive behavior
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Do not hard-block Codex when auth source cannot be determined.
|
|
644
|
+
return { ok: true, reason: null };
|
|
605
645
|
}
|
|
606
646
|
if (name === "copilot") {
|
|
647
|
+
if (!hasOptionalModule("@github/copilot-sdk")) {
|
|
648
|
+
return { ok: false, reason: "@github/copilot-sdk not installed" };
|
|
649
|
+
}
|
|
607
650
|
// Copilot auth can come from multiple sources (OAuth manager, gh auth,
|
|
608
651
|
// VS Code Copilot login, env tokens). Don't block execution here.
|
|
609
652
|
return { ok: true, reason: null };
|
|
610
653
|
}
|
|
611
654
|
if (name === "claude") {
|
|
655
|
+
if (!hasOptionalModule("@anthropic-ai/claude-agent-sdk")) {
|
|
656
|
+
return { ok: false, reason: "@anthropic-ai/claude-agent-sdk not installed" };
|
|
657
|
+
}
|
|
612
658
|
const hasKey = runtimeEnv.ANTHROPIC_API_KEY;
|
|
613
659
|
if (!hasKey) {
|
|
614
660
|
return { ok: false, reason: "no ANTHROPIC_API_KEY" };
|
|
@@ -706,13 +752,14 @@ async function withTemporaryEnv(overrides, fn) {
|
|
|
706
752
|
* Otherwise strips OPENAI_BASE_URL so the SDK uses its default auth.
|
|
707
753
|
*/
|
|
708
754
|
function buildCodexSdkOptions(envInput = process.env) {
|
|
709
|
-
const
|
|
755
|
+
const resolved = resolveCodexProfileRuntime(envInput);
|
|
756
|
+
const { env: resolvedEnv, configProvider } = resolved;
|
|
710
757
|
const baseUrl = resolvedEnv.OPENAI_BASE_URL || "";
|
|
711
758
|
const isAzure = (() => {
|
|
712
759
|
try {
|
|
713
760
|
const parsed = new URL(baseUrl);
|
|
714
761
|
const host = String(parsed.hostname || "").toLowerCase();
|
|
715
|
-
return host === "openai.azure.com" || host.endsWith(".openai.azure.com");
|
|
762
|
+
return host === "openai.azure.com" || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
|
|
716
763
|
} catch {
|
|
717
764
|
return false;
|
|
718
765
|
}
|
|
@@ -727,16 +774,20 @@ function buildCodexSdkOptions(envInput = process.env) {
|
|
|
727
774
|
if (env.OPENAI_API_KEY && !env.AZURE_OPENAI_API_KEY) {
|
|
728
775
|
env.AZURE_OPENAI_API_KEY = env.OPENAI_API_KEY;
|
|
729
776
|
}
|
|
777
|
+
// Use the config.toml provider section name and env_key when available,
|
|
778
|
+
// so the SDK config override is consistent with the user's config.toml.
|
|
779
|
+
const providerSectionName = configProvider?.name || "azure";
|
|
780
|
+
const providerEnvKey = configProvider?.envKey || "AZURE_OPENAI_API_KEY";
|
|
730
781
|
const azureModel = env.CODEX_MODEL || undefined;
|
|
731
782
|
return {
|
|
732
783
|
env,
|
|
733
784
|
config: {
|
|
734
|
-
model_provider:
|
|
785
|
+
model_provider: providerSectionName,
|
|
735
786
|
model_providers: {
|
|
736
|
-
|
|
787
|
+
[providerSectionName]: {
|
|
737
788
|
name: "Azure OpenAI",
|
|
738
789
|
base_url: baseUrl,
|
|
739
|
-
env_key:
|
|
790
|
+
env_key: providerEnvKey,
|
|
740
791
|
wire_api: "responses",
|
|
741
792
|
},
|
|
742
793
|
},
|
|
@@ -1065,6 +1116,19 @@ export function setPoolSdk(name) {
|
|
|
1065
1116
|
export function resetPoolSdkCache() {
|
|
1066
1117
|
resolvedSdkName = null;
|
|
1067
1118
|
resolutionLogged = false;
|
|
1119
|
+
sdkFailureCooldownUntil.clear();
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
export function setSdkFailureCooldownForTest(name, cooldownRemainingMs, nowMs = Date.now()) {
|
|
1123
|
+
if (!SDK_ADAPTERS[name]) {
|
|
1124
|
+
throw new Error(`Unknown SDK: ${name}`);
|
|
1125
|
+
}
|
|
1126
|
+
const remainingMs = Number(cooldownRemainingMs);
|
|
1127
|
+
if (!Number.isFinite(remainingMs) || remainingMs <= 0) {
|
|
1128
|
+
sdkFailureCooldownUntil.delete(name);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
sdkFailureCooldownUntil.set(name, nowMs + Math.trunc(remainingMs));
|
|
1068
1132
|
}
|
|
1069
1133
|
|
|
1070
1134
|
/**
|
|
@@ -1145,6 +1209,11 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1145
1209
|
: process.env;
|
|
1146
1210
|
const codexSessionEnv = applyNodeWarningSuppressionEnv(codexRuntimeEnv);
|
|
1147
1211
|
const codexOpts = buildCodexSdkOptions(codexSessionEnv);
|
|
1212
|
+
const explicitEnvModel = String(envOverrides?.CODEX_MODEL || "").trim();
|
|
1213
|
+
if (explicitEnvModel) {
|
|
1214
|
+
codexOpts.env = { ...(codexOpts.env || {}), CODEX_MODEL: explicitEnvModel };
|
|
1215
|
+
codexOpts.config = { ...(codexOpts.config || {}), model: explicitEnvModel };
|
|
1216
|
+
}
|
|
1148
1217
|
const modelOverride = String(extra?.model || "").trim();
|
|
1149
1218
|
if (modelOverride) {
|
|
1150
1219
|
codexOpts.env = { ...(codexOpts.env || {}), CODEX_MODEL: modelOverride };
|
|
@@ -2384,6 +2453,7 @@ export async function launchEphemeralThread(
|
|
|
2384
2453
|
];
|
|
2385
2454
|
|
|
2386
2455
|
let lastAttemptResult = null;
|
|
2456
|
+
let primaryFailureResult = null;
|
|
2387
2457
|
const triedSdkNames = [];
|
|
2388
2458
|
const missingPrereqSdks = [];
|
|
2389
2459
|
const cooledDownSdks = [];
|
|
@@ -2443,6 +2513,10 @@ export async function launchEphemeralThread(
|
|
|
2443
2513
|
return result;
|
|
2444
2514
|
}
|
|
2445
2515
|
|
|
2516
|
+
if (name === primaryName) {
|
|
2517
|
+
primaryFailureResult = result;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2446
2520
|
applySdkFailureCooldown(name, result.error);
|
|
2447
2521
|
|
|
2448
2522
|
if (name === primaryName) {
|
|
@@ -2456,15 +2530,170 @@ export async function launchEphemeralThread(
|
|
|
2456
2530
|
}
|
|
2457
2531
|
}
|
|
2458
2532
|
|
|
2533
|
+
const eligibleSdks = Array.from(
|
|
2534
|
+
new Set(attemptOrder.filter((name) => SDK_ADAPTERS[name] && !isDisabled(name))),
|
|
2535
|
+
);
|
|
2536
|
+
const runnableSdks = eligibleSdks.filter((name) => {
|
|
2537
|
+
const prereq = hasSdkPrerequisites(name, sessionEnv);
|
|
2538
|
+
if (!prereq.ok) {
|
|
2539
|
+
if (!missingPrereqSdks.some((entry) => entry.name === name)) {
|
|
2540
|
+
missingPrereqSdks.push({ name, reason: prereq.reason });
|
|
2541
|
+
}
|
|
2542
|
+
return false;
|
|
2543
|
+
}
|
|
2544
|
+
return true;
|
|
2545
|
+
});
|
|
2546
|
+
const cooledDownSet = new Set(cooledDownSdks.map((entry) => entry.name));
|
|
2547
|
+
const missingPrereqSet = new Set(missingPrereqSdks.map((entry) => entry.name));
|
|
2548
|
+
const allEligibleUnavailable =
|
|
2549
|
+
eligibleSdks.length > 0 &&
|
|
2550
|
+
eligibleSdks.every(
|
|
2551
|
+
(name) => cooledDownSet.has(name) || missingPrereqSet.has(name),
|
|
2552
|
+
);
|
|
2553
|
+
|
|
2554
|
+
if (
|
|
2555
|
+
!ignoreSdkCooldown &&
|
|
2556
|
+
!lastAttemptResult &&
|
|
2557
|
+
triedSdkNames.length === 0 &&
|
|
2558
|
+
runnableSdks.length > 0 &&
|
|
2559
|
+
runnableSdks.every((name) => cooledDownSet.has(name))
|
|
2560
|
+
) {
|
|
2561
|
+
const forcedRetryOrder = runnableSdks.includes(primaryName)
|
|
2562
|
+
? [primaryName, ...runnableSdks.filter((name) => name !== primaryName)]
|
|
2563
|
+
: runnableSdks;
|
|
2564
|
+
|
|
2565
|
+
for (const name of forcedRetryOrder) {
|
|
2566
|
+
const cooldownRemainingMs = getSdkCooldownRemainingMs(name);
|
|
2567
|
+
if (cooldownRemainingMs <= 0) continue;
|
|
2568
|
+
|
|
2569
|
+
const prereq = hasSdkPrerequisites(name, sessionEnv);
|
|
2570
|
+
if (!prereq.ok) {
|
|
2571
|
+
if (!missingPrereqSdks.some((entry) => entry.name === name)) {
|
|
2572
|
+
missingPrereqSdks.push({ name, reason: prereq.reason });
|
|
2573
|
+
}
|
|
2574
|
+
continue;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
const remainingSec = Math.max(1, Math.ceil(cooldownRemainingMs / 1000));
|
|
2578
|
+
console.warn(
|
|
2579
|
+
`${TAG} all eligible SDKs are cooling down; force-retrying "${name}" (${remainingSec}s remaining)`,
|
|
2580
|
+
);
|
|
2581
|
+
|
|
2582
|
+
triedSdkNames.push(name);
|
|
2583
|
+
const launcher = await SDK_ADAPTERS[name].load();
|
|
2584
|
+
const result = await launcher(prompt, cwd, timeoutMs, launchExtra);
|
|
2585
|
+
lastAttemptResult = result;
|
|
2586
|
+
|
|
2587
|
+
if (result.success) {
|
|
2588
|
+
return result;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
if (!shouldFallbackForSdkError(result.error)) {
|
|
2592
|
+
return result;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
applySdkFailureCooldown(name, result.error);
|
|
2596
|
+
break;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// If every eligible SDK is unavailable (cooldown and/or missing credentials),
|
|
2601
|
+
// force one cooled-down attempt so work is not blocked for the full cooldown window.
|
|
2602
|
+
const shouldForceCooldownBypassAttempt =
|
|
2603
|
+
!ignoreSdkCooldown &&
|
|
2604
|
+
!lastAttemptResult &&
|
|
2605
|
+
triedSdkNames.length === 0 &&
|
|
2606
|
+
cooledDownSet.has(primaryName) &&
|
|
2607
|
+
allEligibleUnavailable;
|
|
2608
|
+
const forcedSdkName = shouldForceCooldownBypassAttempt
|
|
2609
|
+
? cooledDownSet.has(primaryName)
|
|
2610
|
+
? primaryName
|
|
2611
|
+
: eligibleSdks.find((name) => cooledDownSet.has(name)) || null
|
|
2612
|
+
: null;
|
|
2613
|
+
|
|
2614
|
+
if (forcedSdkName) {
|
|
2615
|
+
const prereq = hasSdkPrerequisites(forcedSdkName, sessionEnv);
|
|
2616
|
+
if (!prereq.ok) {
|
|
2617
|
+
// Cooldown means we recently attempted this SDK. Keep one forced retry path
|
|
2618
|
+
// to avoid hard-blocking work on strict prerequisite heuristics.
|
|
2619
|
+
if (
|
|
2620
|
+
!missingPrereqSdks.some(
|
|
2621
|
+
(entry) => entry.name === forcedSdkName && entry.reason === prereq.reason,
|
|
2622
|
+
)
|
|
2623
|
+
) {
|
|
2624
|
+
missingPrereqSdks.push({ name: forcedSdkName, reason: prereq.reason });
|
|
2625
|
+
}
|
|
2626
|
+
console.warn(
|
|
2627
|
+
`${TAG} all eligible SDKs unavailable; bypassing SDK "${forcedSdkName}" prerequisite gate for forced retry (${prereq.reason})`,
|
|
2628
|
+
);
|
|
2629
|
+
} else {
|
|
2630
|
+
console.warn(
|
|
2631
|
+
`${TAG} no runnable fallback SDK is available (cooldown/prerequisite gate); forcing SDK "${forcedSdkName}" retry`,
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
triedSdkNames.push(forcedSdkName);
|
|
2635
|
+
const launcher = await SDK_ADAPTERS[forcedSdkName].load();
|
|
2636
|
+
const forcedResult = await launcher(prompt, cwd, timeoutMs, launchExtra);
|
|
2637
|
+
if (forcedResult.success) {
|
|
2638
|
+
return forcedResult;
|
|
2639
|
+
}
|
|
2640
|
+
lastAttemptResult = forcedResult;
|
|
2641
|
+
if (!shouldFallbackForSdkError(forcedResult.error)) {
|
|
2642
|
+
return forcedResult;
|
|
2643
|
+
}
|
|
2644
|
+
applySdkFailureCooldown(forcedSdkName, forcedResult.error);
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// Recovery path: when all SDKs were skipped due cooldown/prereq gates, force
|
|
2648
|
+
// one direct attempt against cooled-down SDKs to surface a concrete error.
|
|
2649
|
+
if (!lastAttemptResult && triedSdkNames.length === 0 && cooledDownSdks.length > 0) {
|
|
2650
|
+
const cooledDownRescueOrder = cooledDownSdks
|
|
2651
|
+
.map((entry) => entry.name)
|
|
2652
|
+
.filter((name, idx, arr) => SDK_ADAPTERS[name] && arr.indexOf(name) === idx)
|
|
2653
|
+
.sort((a, b) => {
|
|
2654
|
+
if (a === primaryName) return -1;
|
|
2655
|
+
if (b === primaryName) return 1;
|
|
2656
|
+
return 0;
|
|
2657
|
+
});
|
|
2658
|
+
|
|
2659
|
+
for (const name of cooledDownRescueOrder) {
|
|
2660
|
+
const remainingMs = getSdkCooldownRemainingMs(name);
|
|
2661
|
+
const remainingSec = Math.max(1, Math.ceil(remainingMs / 1000));
|
|
2662
|
+
console.warn(
|
|
2663
|
+
`${TAG} no runnable SDK remained after gating; rescue-attempting cooled-down SDK "${name}" (${remainingSec}s remaining)`,
|
|
2664
|
+
);
|
|
2665
|
+
|
|
2666
|
+
triedSdkNames.push(name);
|
|
2667
|
+
const launcher = await SDK_ADAPTERS[name].load();
|
|
2668
|
+
const result = await launcher(prompt, cwd, timeoutMs, launchExtra);
|
|
2669
|
+
lastAttemptResult = result;
|
|
2670
|
+
|
|
2671
|
+
if (result.success) {
|
|
2672
|
+
return result;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
if (!shouldFallbackForSdkError(result.error)) {
|
|
2676
|
+
return result;
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
if (name === primaryName) {
|
|
2680
|
+
primaryFailureResult = result;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
applySdkFailureCooldown(name, result.error);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2459
2686
|
// ── All SDKs exhausted ───────────────────────────────────────────────────
|
|
2460
2687
|
if (lastAttemptResult) {
|
|
2688
|
+
if (
|
|
2689
|
+
primaryFailureResult &&
|
|
2690
|
+
lastAttemptResult.sdk !== primaryFailureResult.sdk
|
|
2691
|
+
) {
|
|
2692
|
+
return primaryFailureResult;
|
|
2693
|
+
}
|
|
2461
2694
|
return lastAttemptResult;
|
|
2462
2695
|
}
|
|
2463
2696
|
|
|
2464
|
-
const eligibleSdks = Array.from(
|
|
2465
|
-
new Set(attemptOrder.filter((name) => SDK_ADAPTERS[name] && !isDisabled(name))),
|
|
2466
|
-
);
|
|
2467
|
-
|
|
2468
2697
|
let errorMsg = `${TAG} no SDK available.`;
|
|
2469
2698
|
if (triedSdkNames.length > 0) {
|
|
2470
2699
|
errorMsg += ` Tried: ${triedSdkNames.join(", ")}.`;
|
|
@@ -2888,6 +3117,11 @@ async function resumeCodexThread(threadId, prompt, cwd, timeoutMs, extra = {}) {
|
|
|
2888
3117
|
: process.env;
|
|
2889
3118
|
const codexSessionEnv = applyNodeWarningSuppressionEnv(codexRuntimeEnv);
|
|
2890
3119
|
const codexOpts = buildCodexSdkOptions(codexSessionEnv);
|
|
3120
|
+
const explicitEnvModel = String(envOverrides?.CODEX_MODEL || "").trim();
|
|
3121
|
+
if (explicitEnvModel) {
|
|
3122
|
+
codexOpts.env = { ...(codexOpts.env || {}), CODEX_MODEL: explicitEnvModel };
|
|
3123
|
+
codexOpts.config = { ...(codexOpts.config || {}), model: explicitEnvModel };
|
|
3124
|
+
}
|
|
2891
3125
|
const modelOverride = String(extra?.model || "").trim();
|
|
2892
3126
|
if (modelOverride) {
|
|
2893
3127
|
codexOpts.env = { ...(codexOpts.env || {}), CODEX_MODEL: modelOverride };
|
|
@@ -3081,6 +3315,7 @@ async function resumeGenericThread(
|
|
|
3081
3315
|
* @param {string} [extra.sessionType] Interaction type used for context-shredding policy lookup.
|
|
3082
3316
|
* @param {boolean} [extra.forceContextShredding] Force compression regardless of session type defaults.
|
|
3083
3317
|
* @param {boolean} [extra.skipContextShredding] Skip compression regardless of policy.
|
|
3318
|
+
* @param {boolean} [extra.pinSdk] Keep SDK pinned when provided (disables fallback).
|
|
3084
3319
|
* @param {Function} [extra.onEvent] Event callback.
|
|
3085
3320
|
* @param {AbortController} [extra.abortController]
|
|
3086
3321
|
* @returns {Promise<{ success: boolean, output: string, items: Array, error: string|null, sdk: string, threadId: string|null, resumed: boolean }>}
|
|
@@ -3105,8 +3340,8 @@ export async function launchOrResumeThread(
|
|
|
3105
3340
|
restExtra.envOverrides = applyNodeWarningSuppressionEnv(restExtra.envOverrides);
|
|
3106
3341
|
// Pass taskKey through as steer key so SDK launchers can register active sessions
|
|
3107
3342
|
restExtra.taskKey = taskKey;
|
|
3108
|
-
if (restExtra.sdk) {
|
|
3109
|
-
// Task-bound runs with an explicit SDK
|
|
3343
|
+
if (restExtra.sdk && restExtra.pinSdk === true) {
|
|
3344
|
+
// Task-bound runs with an explicit SDK can optionally stay pinned to that SDK.
|
|
3110
3345
|
restExtra.disableFallback = true;
|
|
3111
3346
|
}
|
|
3112
3347
|
timeoutMs = clampMonitorMonitorTimeout(timeoutMs, taskKey);
|
package/agent/agent-prompts.mjs
CHANGED
|
@@ -8,7 +8,15 @@ 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 (!text) return "";
|
|
14
|
+
const sanitized = text
|
|
15
|
+
.replace(/\{\{\s*[\w.-]+\s*\}\}/g, " ")
|
|
16
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
17
|
+
.trim();
|
|
18
|
+
return sanitized;
|
|
19
|
+
}
|
|
12
20
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
13
21
|
return String(value);
|
|
14
22
|
}
|
|
@@ -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);
|
package/agent/autofix.mjs
CHANGED
|
@@ -545,7 +545,7 @@ export function runCodexExec(
|
|
|
545
545
|
try {
|
|
546
546
|
const parsed = new URL(baseUrl);
|
|
547
547
|
const host = String(parsed.hostname || "").toLowerCase();
|
|
548
|
-
return host === "openai.azure.com" || host.endsWith(".openai.azure.com");
|
|
548
|
+
return host === "openai.azure.com" || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
|
|
549
549
|
} catch {
|
|
550
550
|
return false;
|
|
551
551
|
}
|