bosun 0.40.5 → 0.40.6
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 +13 -0
- package/agent/agent-pool.mjs +16 -7
- package/agent/agent-supervisor.mjs +81 -28
- package/agent/autofix.mjs +50 -36
- package/agent/fleet-coordinator.mjs +2 -1
- package/agent/hook-profiles.mjs +1 -1
- package/bosun.schema.json +51 -0
- package/config/context-shredding-config.mjs +143 -0
- package/infra/library-manager.mjs +717 -9
- package/infra/maintenance.mjs +9 -4
- package/infra/monitor.mjs +55 -8
- package/kanban/ve-kanban.mjs +12 -2
- package/kanban/vk-log-stream.mjs +4 -1
- package/package.json +2 -2
- package/server/setup-web-server.mjs +112 -10
- package/server/ui-server.mjs +146 -14
- package/setup.mjs +25 -16
- package/shell/codex-config.mjs +19 -8
- package/shell/copilot-shell.mjs +55 -7
- package/task/task-assessment.mjs +17 -9
- package/task/task-executor.mjs +37 -0
- package/telegram/telegram-bot.mjs +5 -4
- package/ui/demo-defaults.js +146 -24
- package/ui/demo.html +73 -2
- package/ui/modules/settings-schema.js +9 -0
- package/ui/setup.html +95 -0
- package/ui/tabs/library.js +31 -6
- package/ui/tabs/tasks.js +615 -85
- package/ui/tabs/telemetry.js +72 -22
- package/voice/voice-agents-sdk.mjs +5 -1
- package/workflow/workflow-engine.mjs +100 -16
- package/workflow/workflow-nodes.mjs +146 -9
- package/workflow-templates/github.mjs +11 -8
- package/workflow-templates/task-batch.mjs +1 -1
- package/workflow-templates/task-lifecycle.mjs +16 -3
- package/workspace/context-cache.mjs +399 -6
- package/workspace/workspace-manager.mjs +22 -16
- package/workspace/worktree-manager.mjs +7 -7
package/.env.example
CHANGED
|
@@ -196,6 +196,19 @@ VOICE_FALLBACK_MODE=browser
|
|
|
196
196
|
# Executor used by voice tool delegations for complex requests
|
|
197
197
|
VOICE_DELEGATE_EXECUTOR=codex-sdk
|
|
198
198
|
|
|
199
|
+
# ─── Context Shredding / Live Tool Compaction ───────────────────────────────
|
|
200
|
+
# Traditional shredding trims older context turns. Live tool compaction runs
|
|
201
|
+
# earlier: it summarizes large, noisy command outputs before they ever land in
|
|
202
|
+
# the active turn, while preserving a `bosun --tool-log <id>` retrieval path.
|
|
203
|
+
# CONTEXT_SHREDDING_ENABLED=true
|
|
204
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_ENABLED=false
|
|
205
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MODE=auto
|
|
206
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MIN_CHARS=4000
|
|
207
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_TARGET_CHARS=1800
|
|
208
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MIN_SAVINGS_PCT=15
|
|
209
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MIN_RUNTIME_MS=2000
|
|
210
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_BLOCK_STRUCTURED_OUTPUT=true
|
|
211
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_ALLOW_COMMANDS=grep,rg,find,findstr,select-string,ag,ack,sift,fd,where,which,ls,dir,tree,git,go,npm,pnpm,yarn,npx,bun,node,python,python3,pytest,pip,pip3,poetry,docker,kubectl,helm,terraform,ansible,ansible-playbook,journalctl,tail,get-content,cargo,gradle,maven,mvn,javac,tsc,jest,vitest,deno,make,cmake,bazel,buck,nx,turbo,rush,composer,bundle
|
|
199
212
|
# ─── Desktop Portal ────────────────────────────────────────────────────────
|
|
200
213
|
# Auto-start bosun daemon when the desktop portal launches (default: true)
|
|
201
214
|
# BOSUN_DESKTOP_AUTO_START_DAEMON=true
|
package/agent/agent-pool.mjs
CHANGED
|
@@ -44,6 +44,7 @@ import { fileURLToPath } from "node:url";
|
|
|
44
44
|
import { loadConfig } from "../config/config.mjs";
|
|
45
45
|
import { resolveRepoRoot, resolveAgentRepoRoot } from "../config/repo-root.mjs";
|
|
46
46
|
import { resolveCodexProfileRuntime } from "../shell/codex-model-profiles.mjs";
|
|
47
|
+
import { resolveCopilotCliLaunchConfig } from "../shell/copilot-shell.mjs";
|
|
47
48
|
import { getGitHubToken } from "../github/github-auth-manager.mjs";
|
|
48
49
|
import {
|
|
49
50
|
isTransientStreamError,
|
|
@@ -489,6 +490,10 @@ function shouldFallbackForSdkError(error) {
|
|
|
489
490
|
if (!error) return false;
|
|
490
491
|
const message = String(error).toLowerCase();
|
|
491
492
|
if (!message) return false;
|
|
493
|
+
if (message.includes("protocol version mismatch")) return true;
|
|
494
|
+
if (message.includes("sdk expects version") && message.includes("server reports version")) {
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
492
497
|
// SDK not installed / not found
|
|
493
498
|
if (message.includes("not available")) return true;
|
|
494
499
|
// Missing finish_reason (incomplete response)
|
|
@@ -808,6 +813,10 @@ function shouldApplySdkCooldown(error) {
|
|
|
808
813
|
if (!error) return false;
|
|
809
814
|
const message = String(error).toLowerCase();
|
|
810
815
|
if (!message) return false;
|
|
816
|
+
if (message.includes("protocol version mismatch")) return true;
|
|
817
|
+
if (message.includes("sdk expects version") && message.includes("server reports version")) {
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
811
820
|
if (message.includes("timeout")) return true;
|
|
812
821
|
if (message.includes("rate limit") || message.includes("429")) return true;
|
|
813
822
|
if (message.includes("service unavailable") || message.includes("503")) {
|
|
@@ -1448,22 +1457,22 @@ async function launchCopilotThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1448
1457
|
console.warn(`${TAG} copilot MCP config write failed (non-fatal): ${mcpErr.message}`);
|
|
1449
1458
|
}
|
|
1450
1459
|
}
|
|
1451
|
-
const
|
|
1460
|
+
const cliLaunch = resolveCopilotCliLaunchConfig({
|
|
1461
|
+
env: runtimeEnv,
|
|
1462
|
+
repoRoot: REPO_ROOT,
|
|
1463
|
+
cliArgs: buildPoolCopilotCliArgs(mcpConfigPath),
|
|
1464
|
+
});
|
|
1452
1465
|
clientOpts = {
|
|
1453
1466
|
cwd,
|
|
1454
1467
|
env: clientEnv,
|
|
1455
|
-
cliArgs,
|
|
1468
|
+
cliArgs: cliLaunch.cliArgs,
|
|
1456
1469
|
useStdio: true,
|
|
1457
1470
|
};
|
|
1458
1471
|
if (token) {
|
|
1459
1472
|
clientOpts.githubToken = token;
|
|
1460
1473
|
clientOpts.token = token;
|
|
1461
1474
|
}
|
|
1462
|
-
|
|
1463
|
-
process.env.COPILOT_CLI_PATH ||
|
|
1464
|
-
process.env.GITHUB_COPILOT_CLI_PATH ||
|
|
1465
|
-
undefined;
|
|
1466
|
-
if (cliPath) clientOpts.cliPath = cliPath;
|
|
1475
|
+
if (cliLaunch.cliPath) clientOpts.cliPath = cliLaunch.cliPath;
|
|
1467
1476
|
}
|
|
1468
1477
|
client = new CopilotClientClass(clientOpts);
|
|
1469
1478
|
await client.start();
|
|
@@ -901,6 +901,16 @@ export class AgentSupervisor {
|
|
|
901
901
|
const progress = signals.sessionProgress;
|
|
902
902
|
const analysis = signals.sessionAnalysis;
|
|
903
903
|
const anomalies = signals.activeAnomalies || [];
|
|
904
|
+
const includesAny = (text, needles) => needles.some((needle) => text.includes(needle));
|
|
905
|
+
const includesOrdered = (text, parts) => {
|
|
906
|
+
let cursor = 0;
|
|
907
|
+
for (const part of parts) {
|
|
908
|
+
const idx = text.indexOf(part, cursor);
|
|
909
|
+
if (idx === -1) return false;
|
|
910
|
+
cursor = idx + part.length;
|
|
911
|
+
}
|
|
912
|
+
return true;
|
|
913
|
+
};
|
|
904
914
|
|
|
905
915
|
// ── Agent completely dead ──
|
|
906
916
|
if (
|
|
@@ -914,7 +924,13 @@ export class AgentSupervisor {
|
|
|
914
924
|
// ── External errors (check first — these aren't the agent's fault) ──
|
|
915
925
|
if (context.error || signals.error) {
|
|
916
926
|
const errText = context.error || signals.error || "";
|
|
917
|
-
|
|
927
|
+
const errLower = String(errText).toLowerCase();
|
|
928
|
+
if (
|
|
929
|
+
errLower.includes("429") ||
|
|
930
|
+
includesOrdered(errLower, ["rate", "limit"]) ||
|
|
931
|
+
errLower.includes("too many requests") ||
|
|
932
|
+
errLower.includes("quota exceeded")
|
|
933
|
+
) {
|
|
918
934
|
// Check for flood
|
|
919
935
|
if (signals.errorPatterns?.length >= 3) {
|
|
920
936
|
const recent = signals.errorPatterns.slice(-5);
|
|
@@ -923,47 +939,83 @@ export class AgentSupervisor {
|
|
|
923
939
|
}
|
|
924
940
|
return SITUATION.RATE_LIMITED;
|
|
925
941
|
}
|
|
926
|
-
if (
|
|
942
|
+
if (
|
|
943
|
+
includesAny(errLower, ["econnrefused", "etimedout", "fetch failed"]) ||
|
|
944
|
+
includesAny(errLower, [" 500", "500 ", "500internal", "500internalservererror"]) ||
|
|
945
|
+
includesAny(errLower, [" 502", "502 ", "502badgateway", "502bad gateway"]) ||
|
|
946
|
+
errLower.includes(" 503")
|
|
947
|
+
) {
|
|
927
948
|
return SITUATION.API_ERROR;
|
|
928
949
|
}
|
|
929
|
-
if (
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
950
|
+
if (
|
|
951
|
+
(errLower.includes("context") && includesAny(errLower, ["too long", "exceeded", "overflow"])) ||
|
|
952
|
+
includesOrdered(errLower, ["max", "token"]) ||
|
|
953
|
+
includesAny(errLower, ["context_length_exceeded", "prompt_too_long", "this model's maximum context length", "turn_limit_reached", "string_above_max_length"]) ||
|
|
954
|
+
includesOrdered(errLower, ["prompt", "too", "long"]) ||
|
|
955
|
+
includesOrdered(errLower, ["token_budget", "exceeded"]) ||
|
|
956
|
+
includesOrdered(errLower, ["token", "budget"]) ||
|
|
957
|
+
includesOrdered(errLower, ["conversation", "too", "long"]) ||
|
|
958
|
+
includesOrdered(errLower, ["maximum", "number", "tokens"])
|
|
959
|
+
) {
|
|
935
960
|
return SITUATION.TOKEN_OVERFLOW;
|
|
936
961
|
}
|
|
937
|
-
if (
|
|
962
|
+
if (
|
|
963
|
+
includesOrdered(errLower, ["model", "not", "supported"]) ||
|
|
964
|
+
includesOrdered(errLower, ["model", "error"]) ||
|
|
965
|
+
includesOrdered(errLower, ["invalid", "model"]) ||
|
|
966
|
+
includesOrdered(errLower, ["model", "not", "found"]) ||
|
|
967
|
+
includesOrdered(errLower, ["model", "deprecated"])
|
|
968
|
+
) {
|
|
938
969
|
return SITUATION.MODEL_ERROR;
|
|
939
970
|
}
|
|
940
971
|
// ── Auth failures — MUST come BEFORE session_expired (which has 'unauthorized')
|
|
941
|
-
if (
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
972
|
+
if (
|
|
973
|
+
includesAny(errLower, ["invalid api key", "invalid-api-key", "invalid_api_key", "authentication_error", "permission_error", "401 unauthorized", "403 forbidden", "billing_hard_limit", "insufficient_quota"]) ||
|
|
974
|
+
includesOrdered(errLower, ["invalid", "credentials"]) ||
|
|
975
|
+
includesOrdered(errLower, ["access", "denied"]) ||
|
|
976
|
+
includesOrdered(errLower, ["not", "authorized"])
|
|
977
|
+
) {
|
|
945
978
|
return SITUATION.AUTH_FAILURE;
|
|
946
979
|
}
|
|
947
980
|
// ── Session expired (after auth check since 'unauthorized' overlaps)
|
|
948
|
-
if (
|
|
949
|
-
|
|
981
|
+
if (
|
|
982
|
+
includesOrdered(errLower, ["session", "expired"]) ||
|
|
983
|
+
includesOrdered(errLower, ["thread", "not", "found"]) ||
|
|
984
|
+
includesOrdered(errLower, ["invalid", "session"]) ||
|
|
985
|
+
includesOrdered(errLower, ["invalid", "token"])
|
|
986
|
+
) {
|
|
950
987
|
return SITUATION.SESSION_EXPIRED;
|
|
951
988
|
}
|
|
952
989
|
// ── Content policy (safety filter)
|
|
953
|
-
if (
|
|
954
|
-
|
|
990
|
+
if (
|
|
991
|
+
includesAny(errLower, ["content_policy", "content filter", "content-filter", "content_filter", "safety_system"]) ||
|
|
992
|
+
includesOrdered(errLower, ["safety", "filter"]) ||
|
|
993
|
+
includesOrdered(errLower, ["flagged", "content"]) ||
|
|
994
|
+
includesOrdered(errLower, ["output", "blocked"]) ||
|
|
995
|
+
includesOrdered(errLower, ["response", "blocked"])
|
|
996
|
+
) {
|
|
955
997
|
return SITUATION.CONTENT_POLICY;
|
|
956
998
|
}
|
|
957
999
|
// ── Codex sandbox/CLI errors
|
|
958
|
-
if (
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1000
|
+
if (
|
|
1001
|
+
includesOrdered(errLower, ["sandbox", "fail"]) ||
|
|
1002
|
+
includesOrdered(errLower, ["bwrap", "error"]) ||
|
|
1003
|
+
errLower.includes("bubblewrap") ||
|
|
1004
|
+
includesOrdered(errLower, ["eperm", "operation", "not", "permitted"]) ||
|
|
1005
|
+
errLower.includes("writable_roots") ||
|
|
1006
|
+
includesOrdered(errLower, ["namespace", "error"]) ||
|
|
1007
|
+
(errLower.includes("codex") && includesAny(errLower, ["segfault", "killed", "crash"]))
|
|
1008
|
+
) {
|
|
962
1009
|
return SITUATION.CODEX_SANDBOX;
|
|
963
1010
|
}
|
|
964
1011
|
// ── Invalid config (catch-all for config-related errors)
|
|
965
|
-
if (
|
|
966
|
-
|
|
1012
|
+
if (
|
|
1013
|
+
includesOrdered(errLower, ["config", "invalid"]) ||
|
|
1014
|
+
includesOrdered(errLower, ["config", "missing"]) ||
|
|
1015
|
+
errLower.includes("misconfigured") ||
|
|
1016
|
+
includesOrdered(errLower, ["openai_api_key", "not", "set"]) ||
|
|
1017
|
+
includesOrdered(errLower, ["anthropic_api_key", "not", "set"])
|
|
1018
|
+
) {
|
|
967
1019
|
return SITUATION.INVALID_CONFIG;
|
|
968
1020
|
}
|
|
969
1021
|
}
|
|
@@ -1009,11 +1061,11 @@ export class AgentSupervisor {
|
|
|
1009
1061
|
// ── Build/test/lint failures from context ──
|
|
1010
1062
|
if (context.error) {
|
|
1011
1063
|
const err = context.error.toLowerCase();
|
|
1012
|
-
if (
|
|
1013
|
-
if (
|
|
1014
|
-
if (
|
|
1015
|
-
if (
|
|
1016
|
-
if (
|
|
1064
|
+
if (includesOrdered(err, ["pre-push hook", "fail"])) return SITUATION.PRE_PUSH_FAILURE;
|
|
1065
|
+
if (includesOrdered(err, ["git push", "fail"]) || includesOrdered(err, ["rejected", "push"])) return SITUATION.PUSH_FAILURE;
|
|
1066
|
+
if (includesOrdered(err, ["go build", "fail"]) || err.includes("compilation error") || err.includes("cannot find")) return SITUATION.BUILD_FAILURE;
|
|
1067
|
+
if ((err.includes("fail") && err.includes("test")) || err.includes("fail ")) return SITUATION.TEST_FAILURE;
|
|
1068
|
+
if (err.includes("golangci-lint") || includesOrdered(err, ["lint", "error"])) return SITUATION.LINT_FAILURE;
|
|
1017
1069
|
if (/merge conflict|CONFLICT/i.test(err)) return SITUATION.GIT_CONFLICT;
|
|
1018
1070
|
}
|
|
1019
1071
|
|
|
@@ -1234,3 +1286,4 @@ export class AgentSupervisor {
|
|
|
1234
1286
|
export function createAgentSupervisor(opts) {
|
|
1235
1287
|
return new AgentSupervisor(opts);
|
|
1236
1288
|
}
|
|
1289
|
+
|
package/agent/autofix.mjs
CHANGED
|
@@ -93,7 +93,6 @@ export function extractErrors(logText) {
|
|
|
93
93
|
const atLineHeader = /^At\s+([A-Za-z]:\\[^\n:]+\.ps1):(\d+)\s+char:(\d+)/;
|
|
94
94
|
|
|
95
95
|
// Pattern C: TerminatingError(X): "message"
|
|
96
|
-
const terminatingPattern = /TerminatingError\(([^)]+)\):\s*"(.+?)"/;
|
|
97
96
|
|
|
98
97
|
for (let i = 0; i < lines.length; i++) {
|
|
99
98
|
const line = lines[i];
|
|
@@ -147,16 +146,24 @@ export function extractErrors(logText) {
|
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
// ── Check Pattern C (TerminatingError) ──────────────────────────
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
149
|
+
const marker = "TerminatingError(";
|
|
150
|
+
const markerPos = line.indexOf(marker);
|
|
151
|
+
if (markerPos !== -1) {
|
|
152
|
+
const typeStart = markerPos + marker.length;
|
|
153
|
+
const typeEnd = line.indexOf(")", typeStart);
|
|
154
|
+
const colonPos = typeEnd >= 0 ? line.indexOf(":", typeEnd + 1) : -1;
|
|
155
|
+
const errorType = typeEnd >= 0 ? line.slice(typeStart, typeEnd).trim() : "";
|
|
156
|
+
const detail = colonPos >= 0 ? line.slice(colonPos + 1).trim().replace(/^"|"$/g, "") : "";
|
|
157
|
+
if (errorType && detail) {
|
|
158
|
+
addError({
|
|
159
|
+
errorType: "TerminatingError",
|
|
160
|
+
file: "unknown",
|
|
161
|
+
line: 0,
|
|
162
|
+
column: null,
|
|
163
|
+
message: `${errorType}: ${detail}`,
|
|
164
|
+
signature: `TerminatingError:${errorType}`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
160
167
|
}
|
|
161
168
|
}
|
|
162
169
|
|
|
@@ -185,18 +192,22 @@ function parseLineBlock(lines, startIdx) {
|
|
|
185
192
|
}
|
|
186
193
|
|
|
187
194
|
// Capture code line: " NNN | code..."
|
|
188
|
-
if (i < lines.length
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
if (i < lines.length) {
|
|
196
|
+
const raw = String(lines[i] || "");
|
|
197
|
+
const sep = raw.indexOf("|");
|
|
198
|
+
if (sep !== -1 && /^\s*\d+\s*$/.test(raw.slice(0, sep))) {
|
|
199
|
+
codeLine = raw.slice(sep + 1).trim();
|
|
200
|
+
i++;
|
|
201
|
+
}
|
|
192
202
|
}
|
|
193
203
|
|
|
194
204
|
// Skip underline and intermediate "| ..." lines, capture last "| message" line
|
|
195
205
|
let lastPipeMessage = "";
|
|
196
206
|
while (i < lines.length) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
const raw = String(lines[i] || "");
|
|
208
|
+
const sep = raw.indexOf("|");
|
|
209
|
+
if (sep === -1 || raw.slice(0, sep).trim().length > 0) break;
|
|
210
|
+
const content = raw.slice(sep + 1).trim();
|
|
200
211
|
// Skip underline-only lines (~~~~) and empty lines
|
|
201
212
|
if (content && !/^~+$/.test(content)) {
|
|
202
213
|
lastPipeMessage = content;
|
|
@@ -225,29 +236,32 @@ function parsePlusBlock(lines, startIdx) {
|
|
|
225
236
|
let i = startIdx;
|
|
226
237
|
|
|
227
238
|
// First "+ " line is usually the code
|
|
228
|
-
if (i < lines.length
|
|
229
|
-
const
|
|
230
|
-
if (
|
|
231
|
-
const content =
|
|
239
|
+
if (i < lines.length) {
|
|
240
|
+
const raw = String(lines[i] || "").trimStart();
|
|
241
|
+
if (raw.startsWith("+")) {
|
|
242
|
+
const content = raw.slice(1).trim();
|
|
232
243
|
if (!/^~+$/.test(content)) codeLine = content;
|
|
244
|
+
i++;
|
|
233
245
|
}
|
|
234
|
-
i++;
|
|
235
246
|
}
|
|
236
247
|
|
|
237
248
|
// Subsequent "+ " lines — skip underlines, capture error type + message
|
|
238
|
-
while (i < lines.length
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
while (i < lines.length) {
|
|
250
|
+
const raw = String(lines[i] || "").trimStart();
|
|
251
|
+
if (!raw.startsWith("+")) break;
|
|
252
|
+
const content = raw.slice(1).trim();
|
|
253
|
+
if (/^~+$/.test(content)) {
|
|
254
|
+
i++;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
// Check for "ErrorType: message"
|
|
258
|
+
const colonPos = content.indexOf(":");
|
|
259
|
+
if (colonPos > 0 && colonPos < content.length - 1) {
|
|
260
|
+
const maybeType = content.slice(0, colonPos).trim();
|
|
261
|
+
const maybeMessage = content.slice(colonPos + 1).trim();
|
|
262
|
+
if (/^\w[\w.-]+$/.test(maybeType) && maybeMessage) {
|
|
263
|
+
errorType = maybeType;
|
|
264
|
+
message = maybeMessage;
|
|
251
265
|
}
|
|
252
266
|
}
|
|
253
267
|
i++;
|
|
@@ -160,7 +160,7 @@ export function normalizeGitUrl(url) {
|
|
|
160
160
|
s = s.replace(/\.git$/, "");
|
|
161
161
|
|
|
162
162
|
// Strip trailing slashes
|
|
163
|
-
s = s.
|
|
163
|
+
while (s.endsWith("/")) s = s.slice(0, -1);
|
|
164
164
|
|
|
165
165
|
return s.toLowerCase();
|
|
166
166
|
}
|
|
@@ -857,3 +857,4 @@ export function formatFleetSummary() {
|
|
|
857
857
|
|
|
858
858
|
return lines.join("\n");
|
|
859
859
|
}
|
|
860
|
+
|
package/agent/hook-profiles.mjs
CHANGED
|
@@ -187,7 +187,7 @@ function normalizeOverrideCommands(rawValue) {
|
|
|
187
187
|
return [];
|
|
188
188
|
}
|
|
189
189
|
const commands = raw
|
|
190
|
-
.split(/\
|
|
190
|
+
.split(/\r?\n/).flatMap((part) => part.split(";;"))
|
|
191
191
|
.map((part) => part.trim())
|
|
192
192
|
.filter(Boolean);
|
|
193
193
|
return commands;
|
package/bosun.schema.json
CHANGED
|
@@ -1199,6 +1199,56 @@
|
|
|
1199
1199
|
"default": 1,
|
|
1200
1200
|
"description": "Turns during which the full user prompt is preserved."
|
|
1201
1201
|
},
|
|
1202
|
+
"liveToolCompactionEnabled": {
|
|
1203
|
+
"type": "boolean",
|
|
1204
|
+
"default": false,
|
|
1205
|
+
"description": "Enable command-aware live compaction of large tool outputs before they are stored in the active turn."
|
|
1206
|
+
},
|
|
1207
|
+
"liveToolCompactionMode": {
|
|
1208
|
+
"type": "string",
|
|
1209
|
+
"enum": ["off", "auto", "aggressive"],
|
|
1210
|
+
"default": "auto",
|
|
1211
|
+
"description": "off disables live compaction, auto compacts only when pressure or signal justify it, and aggressive favors stronger reduction for noisy command families."
|
|
1212
|
+
},
|
|
1213
|
+
"liveToolCompactionMinChars": {
|
|
1214
|
+
"type": "integer",
|
|
1215
|
+
"minimum": 500,
|
|
1216
|
+
"maximum": 500000,
|
|
1217
|
+
"default": 4000,
|
|
1218
|
+
"description": "Minimum output size before live compaction is considered."
|
|
1219
|
+
},
|
|
1220
|
+
"liveToolCompactionTargetChars": {
|
|
1221
|
+
"type": "integer",
|
|
1222
|
+
"minimum": 200,
|
|
1223
|
+
"maximum": 50000,
|
|
1224
|
+
"default": 1800,
|
|
1225
|
+
"description": "Target upper bound for compacted live command output before retrieval hints and metadata."
|
|
1226
|
+
},
|
|
1227
|
+
"liveToolCompactionMinSavingsPct": {
|
|
1228
|
+
"type": "integer",
|
|
1229
|
+
"minimum": 0,
|
|
1230
|
+
"maximum": 95,
|
|
1231
|
+
"default": 15,
|
|
1232
|
+
"description": "Discard compacted output if it does not save at least this much space."
|
|
1233
|
+
},
|
|
1234
|
+
"liveToolCompactionMinRuntimeMs": {
|
|
1235
|
+
"type": "integer",
|
|
1236
|
+
"minimum": 0,
|
|
1237
|
+
"maximum": 3600000,
|
|
1238
|
+
"default": 2000,
|
|
1239
|
+
"description": "When command duration metadata is present, require at least this runtime before compacting in auto mode."
|
|
1240
|
+
},
|
|
1241
|
+
"liveToolCompactionBlockStructured": {
|
|
1242
|
+
"type": "boolean",
|
|
1243
|
+
"default": true,
|
|
1244
|
+
"description": "Avoid live compaction for likely JSON or other structured outputs where exact bytes matter more than size."
|
|
1245
|
+
},
|
|
1246
|
+
"liveToolCompactionAllowCommands": {
|
|
1247
|
+
"type": "array",
|
|
1248
|
+
"items": { "type": "string" },
|
|
1249
|
+
"default": ["grep", "rg", "find", "git", "go", "npm", "pnpm", "yarn", "node", "python", "python3", "pytest", "docker", "kubectl", "cargo", "gradle", "maven", "mvn", "javac", "tsc", "jest", "vitest", "deno"],
|
|
1250
|
+
"description": "Command or tool families eligible for live compaction."
|
|
1251
|
+
},
|
|
1202
1252
|
"perType": {
|
|
1203
1253
|
"type": "object",
|
|
1204
1254
|
"description": "Per-interaction-type config overrides. Keys: task, manual, primary, chat, voice, flow.",
|
|
@@ -1237,3 +1287,4 @@
|
|
|
1237
1287
|
}
|
|
1238
1288
|
}
|
|
1239
1289
|
}
|
|
1290
|
+
|
|
@@ -97,6 +97,14 @@ export const CONTENT_TYPES = Object.freeze([
|
|
|
97
97
|
* @property {boolean} compressToolOutputs - Enable tool output compression
|
|
98
98
|
* @property {boolean} compressAgentMessages - Enable agent message compression
|
|
99
99
|
* @property {boolean} compressUserMessages - Enable user message compression
|
|
100
|
+
* @property {boolean} liveToolCompactionEnabled - Enable command-aware live compaction before history shredding
|
|
101
|
+
* @property {string} liveToolCompactionMode - off|auto|aggressive smart mode for live compaction
|
|
102
|
+
* @property {number} liveToolCompactionMinChars - Minimum output chars before live compaction is considered
|
|
103
|
+
* @property {number} liveToolCompactionTargetChars - Target maximum chars for live-compacted outputs
|
|
104
|
+
* @property {number} liveToolCompactionMinSavingsPct - Minimum savings percentage required to keep a compacted result
|
|
105
|
+
* @property {number} liveToolCompactionMinRuntimeMs - Minimum runtime before live compaction is allowed when duration metadata exists
|
|
106
|
+
* @property {boolean} liveToolCompactionBlockStructured - Disable live compaction for likely structured outputs
|
|
107
|
+
* @property {string[]} liveToolCompactionAllowCommands - Command/tool allowlist for live compaction
|
|
100
108
|
* @property {Object} perType - Per interaction-type overrides (keyed by INTERACTION_TYPES)
|
|
101
109
|
* @property {Object} perAgent - Per agent-type overrides (keyed by AGENT_TYPES)
|
|
102
110
|
*/
|
|
@@ -138,6 +146,25 @@ export const DEFAULT_SHREDDING_CONFIG = Object.freeze({
|
|
|
138
146
|
msgMinCompressChars: 120,
|
|
139
147
|
userMsgFullTurns: 1, // only the current turn keeps the full user prompt
|
|
140
148
|
|
|
149
|
+
// ── Live command/tool compaction ─────────────────────────────
|
|
150
|
+
liveToolCompactionEnabled: false,
|
|
151
|
+
liveToolCompactionMode: "auto",
|
|
152
|
+
liveToolCompactionMinChars: 4000,
|
|
153
|
+
liveToolCompactionTargetChars: 1800,
|
|
154
|
+
liveToolCompactionMinSavingsPct: 15,
|
|
155
|
+
liveToolCompactionMinRuntimeMs: 2000,
|
|
156
|
+
liveToolCompactionBlockStructured: true,
|
|
157
|
+
liveToolCompactionAllowCommands: [
|
|
158
|
+
"grep", "rg", "find", "findstr", "select-string", "ag", "ack", "sift",
|
|
159
|
+
"fd", "where", "which", "ls", "dir", "tree", "git", "go", "npm",
|
|
160
|
+
"pnpm", "yarn", "npx", "bun", "node", "python", "python3", "pytest",
|
|
161
|
+
"pip", "pip3", "poetry", "docker", "kubectl", "helm", "terraform",
|
|
162
|
+
"ansible", "ansible-playbook", "journalctl", "tail", "get-content",
|
|
163
|
+
"cargo", "gradle", "maven", "mvn", "javac", "tsc", "jest", "vitest",
|
|
164
|
+
"deno", "make", "cmake", "bazel", "buck", "nx", "turbo", "rush",
|
|
165
|
+
"composer", "bundle",
|
|
166
|
+
],
|
|
167
|
+
|
|
141
168
|
// ── Per-type overrides (empty = use base config) ─────────────
|
|
142
169
|
/** @type {Record<string, Partial<ShreddingConfig>>} */
|
|
143
170
|
perType: {},
|
|
@@ -301,6 +328,38 @@ export function loadContextShreddingConfig() {
|
|
|
301
328
|
const userFull = parseEnvInt(env[`${ENV_PREFIX}USER_MSG_FULL_TURNS`], 0, 10);
|
|
302
329
|
if (userFull != null) cfg.userMsgFullTurns = userFull;
|
|
303
330
|
|
|
331
|
+
// ── Live command/tool compaction ─────────────────────────────
|
|
332
|
+
const liveToolCompactionEnabled = parseEnvBool(env[`${ENV_PREFIX}LIVE_TOOL_COMPACTION_ENABLED`]);
|
|
333
|
+
if (liveToolCompactionEnabled != null) cfg.liveToolCompactionEnabled = liveToolCompactionEnabled;
|
|
334
|
+
|
|
335
|
+
const liveToolCompactionMode = String(env[`${ENV_PREFIX}LIVE_TOOL_COMPACTION_MODE`] || "").trim().toLowerCase();
|
|
336
|
+
if (["off", "auto", "aggressive"].includes(liveToolCompactionMode)) {
|
|
337
|
+
cfg.liveToolCompactionMode = liveToolCompactionMode;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const liveToolCompactionMinChars = parseEnvInt(env[`${ENV_PREFIX}LIVE_TOOL_COMPACTION_MIN_CHARS`], 500, 500000);
|
|
341
|
+
if (liveToolCompactionMinChars != null) cfg.liveToolCompactionMinChars = liveToolCompactionMinChars;
|
|
342
|
+
|
|
343
|
+
const liveToolCompactionTargetChars = parseEnvInt(env[`${ENV_PREFIX}LIVE_TOOL_COMPACTION_TARGET_CHARS`], 200, 50000);
|
|
344
|
+
if (liveToolCompactionTargetChars != null) cfg.liveToolCompactionTargetChars = liveToolCompactionTargetChars;
|
|
345
|
+
|
|
346
|
+
const liveToolCompactionMinSavingsPct = parseEnvInt(env[`${ENV_PREFIX}LIVE_TOOL_COMPACTION_MIN_SAVINGS_PCT`], 0, 95);
|
|
347
|
+
if (liveToolCompactionMinSavingsPct != null) cfg.liveToolCompactionMinSavingsPct = liveToolCompactionMinSavingsPct;
|
|
348
|
+
|
|
349
|
+
const liveToolCompactionMinRuntimeMs = parseEnvInt(env[`${ENV_PREFIX}LIVE_TOOL_COMPACTION_MIN_RUNTIME_MS`], 0, 60 * 60 * 1000);
|
|
350
|
+
if (liveToolCompactionMinRuntimeMs != null) cfg.liveToolCompactionMinRuntimeMs = liveToolCompactionMinRuntimeMs;
|
|
351
|
+
|
|
352
|
+
const liveToolCompactionBlockStructured = parseEnvBool(env[`${ENV_PREFIX}LIVE_TOOL_COMPACTION_BLOCK_STRUCTURED_OUTPUT`]);
|
|
353
|
+
if (liveToolCompactionBlockStructured != null) cfg.liveToolCompactionBlockStructured = liveToolCompactionBlockStructured;
|
|
354
|
+
|
|
355
|
+
const liveToolCompactionAllowCommands = String(env[`${ENV_PREFIX}LIVE_TOOL_COMPACTION_ALLOW_COMMANDS`] || "").trim();
|
|
356
|
+
if (liveToolCompactionAllowCommands) {
|
|
357
|
+
cfg.liveToolCompactionAllowCommands = liveToolCompactionAllowCommands
|
|
358
|
+
.split(",")
|
|
359
|
+
.map((value) => value.trim().toLowerCase())
|
|
360
|
+
.filter(Boolean);
|
|
361
|
+
}
|
|
362
|
+
|
|
304
363
|
// ── Per-type JSON profiles ────────────────────────────────────
|
|
305
364
|
const profilesEnv = env[`${ENV_PREFIX}PROFILES`];
|
|
306
365
|
if (profilesEnv) {
|
|
@@ -450,6 +509,14 @@ function configToOptions(cfg) {
|
|
|
450
509
|
msgTier1MaxAge: cfg.msgTier1MaxAge,
|
|
451
510
|
msgMinCompressChars: cfg.msgMinCompressChars,
|
|
452
511
|
userMsgFullTurns: cfg.userMsgFullTurns,
|
|
512
|
+
liveToolCompactionEnabled: cfg.liveToolCompactionEnabled,
|
|
513
|
+
liveToolCompactionMode: cfg.liveToolCompactionMode,
|
|
514
|
+
liveToolCompactionMinChars: cfg.liveToolCompactionMinChars,
|
|
515
|
+
liveToolCompactionTargetChars: cfg.liveToolCompactionTargetChars,
|
|
516
|
+
liveToolCompactionMinSavingsPct: cfg.liveToolCompactionMinSavingsPct,
|
|
517
|
+
liveToolCompactionMinRuntimeMs: cfg.liveToolCompactionMinRuntimeMs,
|
|
518
|
+
liveToolCompactionBlockStructured: cfg.liveToolCompactionBlockStructured,
|
|
519
|
+
liveToolCompactionAllowCommands: [...(cfg.liveToolCompactionAllowCommands || [])],
|
|
453
520
|
};
|
|
454
521
|
}
|
|
455
522
|
|
|
@@ -677,6 +744,82 @@ export const CONTEXT_SHREDDING_ENV_DEFS = [
|
|
|
677
744
|
description: "Turns during which the full user prompt is preserved. After this, only a short summary is kept.",
|
|
678
745
|
advanced: true,
|
|
679
746
|
},
|
|
747
|
+
{
|
|
748
|
+
key: "CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_ENABLED",
|
|
749
|
+
label: "Live Tool Compaction",
|
|
750
|
+
type: "boolean",
|
|
751
|
+
default: false,
|
|
752
|
+
description: "Enable command-aware compaction of large tool outputs before they are stored in the active turn. Falls back to raw output on low confidence or unsafe shapes.",
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
key: "CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MODE",
|
|
756
|
+
label: "Live Compaction Mode",
|
|
757
|
+
type: "select",
|
|
758
|
+
default: "auto",
|
|
759
|
+
options: ["off", "auto", "aggressive"],
|
|
760
|
+
description: "off disables live compaction, auto compacts only when pressure or signal justify it, and aggressive favors stronger reduction for noisy command families.",
|
|
761
|
+
advanced: true,
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
key: "CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MIN_CHARS",
|
|
765
|
+
label: "Live Compaction Minimum Size",
|
|
766
|
+
type: "number",
|
|
767
|
+
default: 4000,
|
|
768
|
+
min: 500,
|
|
769
|
+
max: 500000,
|
|
770
|
+
unit: "chars",
|
|
771
|
+
description: "Minimum output size before live compaction is considered.",
|
|
772
|
+
advanced: true,
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
key: "CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_TARGET_CHARS",
|
|
776
|
+
label: "Live Compaction Target Size",
|
|
777
|
+
type: "number",
|
|
778
|
+
default: 1800,
|
|
779
|
+
min: 200,
|
|
780
|
+
max: 50000,
|
|
781
|
+
unit: "chars",
|
|
782
|
+
description: "Target upper bound for compacted live command output before retrieval hints and metadata.",
|
|
783
|
+
advanced: true,
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
key: "CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MIN_SAVINGS_PCT",
|
|
787
|
+
label: "Live Compaction Min Savings",
|
|
788
|
+
type: "number",
|
|
789
|
+
default: 15,
|
|
790
|
+
min: 0,
|
|
791
|
+
max: 95,
|
|
792
|
+
unit: "%",
|
|
793
|
+
description: "Discard compacted output if it does not save at least this much space.",
|
|
794
|
+
advanced: true,
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
key: "CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MIN_RUNTIME_MS",
|
|
798
|
+
label: "Live Compaction Min Runtime",
|
|
799
|
+
type: "number",
|
|
800
|
+
default: 2000,
|
|
801
|
+
min: 0,
|
|
802
|
+
max: 3600000,
|
|
803
|
+
unit: "ms",
|
|
804
|
+
description: "When command duration metadata is present, require at least this runtime before compacting in auto mode.",
|
|
805
|
+
advanced: true,
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
key: "CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_BLOCK_STRUCTURED_OUTPUT",
|
|
809
|
+
label: "Skip Structured Output",
|
|
810
|
+
type: "boolean",
|
|
811
|
+
default: true,
|
|
812
|
+
description: "Avoid live compaction for likely JSON or other structured outputs where exact bytes matter more than size.",
|
|
813
|
+
advanced: true,
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
key: "CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_ALLOW_COMMANDS",
|
|
817
|
+
label: "Live Compaction Allowlist",
|
|
818
|
+
type: "text",
|
|
819
|
+
default: "grep,rg,find,findstr,select-string,ag,ack,sift,fd,where,which,ls,dir,tree,git,go,npm,pnpm,yarn,npx,bun,node,python,python3,pytest,pip,pip3,poetry,docker,kubectl,helm,terraform,ansible,ansible-playbook,journalctl,tail,get-content,cargo,gradle,maven,mvn,javac,tsc,jest,vitest,deno,make,cmake,bazel,buck,nx,turbo,rush,composer,bundle",
|
|
820
|
+
description: "Comma-separated command or tool families eligible for live compaction. Commands outside the allowlist pass through untouched.",
|
|
821
|
+
advanced: true,
|
|
822
|
+
},
|
|
680
823
|
{
|
|
681
824
|
key: "CONTEXT_SHREDDING_PROFILES",
|
|
682
825
|
label: "Per-Type Profiles (JSON)",
|