@raysonmeng/agentbridge 0.1.16 → 0.1.18
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +28 -1
- package/README.zh-CN.md +41 -7
- package/dist/cli.js +185 -46
- package/dist/daemon.js +1403 -179
- package/package.json +1 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/scripts/health-check.sh +83 -0
- package/plugins/agentbridge/server/bridge-server.js +211 -14
- package/plugins/agentbridge/server/daemon.js +1403 -179
package/package.json
CHANGED
|
@@ -19,6 +19,89 @@ if printf '%s' "$pair_id" | grep -Eq '^[A-Za-z0-9._-]+$'; then
|
|
|
19
19
|
pair_arg=" --pair ${pair_id}"
|
|
20
20
|
fi
|
|
21
21
|
|
|
22
|
+
# ── PR4 §6: resume-ack degraded escape hatch ─────────────────────────────────
|
|
23
|
+
# When the daemon's Claude-side ResumeAckTracker exhausts retries with no ack, it
|
|
24
|
+
# drops a sentinel in the state dir. Surface it here BEFORE the cooldown gate so
|
|
25
|
+
# a fresh session within the cooldown window still sees it (else it'd be eaten),
|
|
26
|
+
# then CONSUME (delete) the sentinel so the notice shows exactly once.
|
|
27
|
+
resolve_state_dir() {
|
|
28
|
+
if [ -n "${AGENTBRIDGE_STATE_DIR:-}" ]; then
|
|
29
|
+
printf '%s' "${AGENTBRIDGE_STATE_DIR}"
|
|
30
|
+
return
|
|
31
|
+
fi
|
|
32
|
+
case "$(uname -s 2>/dev/null)" in
|
|
33
|
+
Darwin)
|
|
34
|
+
printf '%s' "${HOME}/Library/Application Support/AgentBridge"
|
|
35
|
+
;;
|
|
36
|
+
*)
|
|
37
|
+
local xdg="${XDG_STATE_HOME:-}"
|
|
38
|
+
if [ -n "$xdg" ]; then
|
|
39
|
+
printf '%s' "${xdg}/agentbridge"
|
|
40
|
+
else
|
|
41
|
+
printf '%s' "${HOME}/.local/state/agentbridge"
|
|
42
|
+
fi
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
state_dir="$(resolve_state_dir)"
|
|
48
|
+
resume_sentinel="${state_dir}/resume-ack-degraded.json"
|
|
49
|
+
if [ -f "$resume_sentinel" ]; then
|
|
50
|
+
resume_id="$(grep -o '"resumeId"[[:space:]]*:[[:space:]]*"[^"]*"' "$resume_sentinel" 2>/dev/null | head -n1 | sed 's/.*"resumeId"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
|
|
51
|
+
# Staleness gate: the sentinel carries degradedAt (epoch MILLISECONDS, written by
|
|
52
|
+
# writeResumeAckDegradedSentinel). A degrade that happened long ago points at a
|
|
53
|
+
# checkpoint that is probably already handled, so surfacing "continue from
|
|
54
|
+
# checkpoint" would mislead. Drop (still consuming) a sentinel older than the TTL.
|
|
55
|
+
# Default 24h survives an overnight away-window (the core use case — wake up and
|
|
56
|
+
# the recovery notice is still there) yet suppresses multi-day-stale notices;
|
|
57
|
+
# configurable via AGENTBRIDGE_RESUME_SENTINEL_TTL_SEC. Fail-open: a missing,
|
|
58
|
+
# non-numeric, or implausibly-long (>16-digit) degradedAt / TTL is treated as
|
|
59
|
+
# FRESH so a possibly-real recovery is never silently suppressed. (A parseable
|
|
60
|
+
# but genuinely old timestamp is still aged normally → stale.)
|
|
61
|
+
degraded_at_ms="$(grep -o '"degradedAt"[[:space:]]*:[[:space:]]*[0-9][0-9]*' "$resume_sentinel" 2>/dev/null | head -n1 | grep -o '[0-9][0-9]*$')"
|
|
62
|
+
resume_ttl_sec="${AGENTBRIDGE_RESUME_SENTINEL_TTL_SEC:-86400}"
|
|
63
|
+
resume_stale=0
|
|
64
|
+
# Accept only a plausible 1–16 digit integer for BOTH operands: 16 digits keeps
|
|
65
|
+
# every value (epoch-ms ~13 digits, any sane TTL ≤ ~9.99e15) far under bash's
|
|
66
|
+
# signed 64-bit ceiling, so the subtraction below can never overflow; anything
|
|
67
|
+
# longer / non-numeric / missing is rejected here → FRESH (fail-open).
|
|
68
|
+
if printf '%s' "$degraded_at_ms" | grep -Eq '^[0-9]{1,16}$' && printf '%s' "$resume_ttl_sec" | grep -Eq '^[0-9]{1,16}$'; then
|
|
69
|
+
# Compare in SECONDS (degradedAt is epoch ms → integer-divide by 1000) and never
|
|
70
|
+
# multiply the user TTL by 1000, so the comparison can't overflow. `10#` forces
|
|
71
|
+
# base-10 so a leading-zero value (e.g. 0888…) parses decimally instead of as
|
|
72
|
+
# octal — an octal-invalid operand would otherwise make the $(( )) arithmetic
|
|
73
|
+
# fail, and in bash a failed arithmetic assignment terminates the enclosing
|
|
74
|
+
# if-block (regardless of set flags), skipping the consume (rm) below and leaking
|
|
75
|
+
# the sentinel. The ^[0-9]{1,16}$ guard above + `10#` together make this
|
|
76
|
+
# arithmetic TOTAL for every accepted input (no octal abort, no int64 overflow),
|
|
77
|
+
# so age_sec is always assigned and the unconditional rm below always runs
|
|
78
|
+
# (consume-once holds).
|
|
79
|
+
now_sec="$(date +%s 2>/dev/null)"
|
|
80
|
+
age_sec=$(( ${now_sec:-0} - 10#${degraded_at_ms} / 1000 ))
|
|
81
|
+
if [ "$age_sec" -gt "$resume_ttl_sec" ]; then
|
|
82
|
+
resume_stale=1
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
rm -f "$resume_sentinel" 2>/dev/null || true # consume once, whether surfaced or dropped-as-stale
|
|
86
|
+
if [ "$resume_stale" -eq 0 ]; then
|
|
87
|
+
# Defense-in-depth (resume_id is daemon-controlled, but harden anyway): only
|
|
88
|
+
# interpolate it into the JSON heredoc if it matches the known-safe id charset
|
|
89
|
+
# (system_budget_claude_recovered_<seq> — a plain monotonic sequence, no salt;
|
|
90
|
+
# the salt lives in BridgeMessage.id, not in the resumeId the sentinel stores).
|
|
91
|
+
# Anything else (a corrupted or
|
|
92
|
+
# tampered sentinel carrying " or \) collapses to "unknown" so the emitted hook
|
|
93
|
+
# JSON can never be broken by the value — mirrors the pair_id guard above.
|
|
94
|
+
if ! printf '%s' "$resume_id" | grep -Eq '^[A-Za-z0-9._-]+$'; then
|
|
95
|
+
resume_id="unknown"
|
|
96
|
+
fi
|
|
97
|
+
cat <<EOF
|
|
98
|
+
{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"AgentBridge: 上次额度刷新后的续接通知未被确认(resume_id=${resume_id:-unknown}),可能上一个会话已空闲或退出。请从 .agent/checkpoint.md 的「下一步」继续未完成的任务。"}}
|
|
99
|
+
EOF
|
|
100
|
+
exit 0
|
|
101
|
+
fi
|
|
102
|
+
# stale → sentinel consumed above; fall through to the normal health check below.
|
|
103
|
+
fi
|
|
104
|
+
|
|
22
105
|
if ! command -v curl >/dev/null 2>&1; then
|
|
23
106
|
exit 0
|
|
24
107
|
fi
|
|
@@ -13866,7 +13866,8 @@ class StateDirResolver {
|
|
|
13866
13866
|
// src/budget/types.ts
|
|
13867
13867
|
var STALE_MAX_AGE_SEC = 600;
|
|
13868
13868
|
|
|
13869
|
-
// src/budget/budget-
|
|
13869
|
+
// src/budget/budget-decision.ts
|
|
13870
|
+
var MAX_TIME_TO_RESET_HOURS = 7 * 24;
|
|
13870
13871
|
function isDecisionGrade(usage, now) {
|
|
13871
13872
|
if (!usage)
|
|
13872
13873
|
return false;
|
|
@@ -13877,7 +13878,6 @@ function isDecisionGrade(usage, now) {
|
|
|
13877
13878
|
return false;
|
|
13878
13879
|
return true;
|
|
13879
13880
|
}
|
|
13880
|
-
|
|
13881
13881
|
// src/budget/burn-view.ts
|
|
13882
13882
|
function agentWeeklyFiveHourWindowsLeft(usage, now) {
|
|
13883
13883
|
if (!usage || usage.stale || !usage.ok)
|
|
@@ -13895,7 +13895,7 @@ function agentWeeklyFiveHourWindowsLeft(usage, now) {
|
|
|
13895
13895
|
}
|
|
13896
13896
|
|
|
13897
13897
|
// src/budget/render.ts
|
|
13898
|
-
var DEFAULT_GUARD_HARD_PCT =
|
|
13898
|
+
var DEFAULT_GUARD_HARD_PCT = 99;
|
|
13899
13899
|
function resolveGuardHardHint(env = process.env) {
|
|
13900
13900
|
const raw = env.AGENTBRIDGE_GUARD_HARD_HINT;
|
|
13901
13901
|
if (raw === undefined || raw.trim() === "")
|
|
@@ -14007,16 +14007,59 @@ function formatFiveHourWindowsLeftLine(snapshot) {
|
|
|
14007
14007
|
const byAgent = values.map(([name, value]) => `${name} ~${value.toFixed(1)}`).join(" / ");
|
|
14008
14008
|
return `\u6309\u5F53\u524D\u8282\u594F\uFF0C\u5468\u989D\u5EA6\u8FD8\u591F ${byAgent} \u4E2A 5h \u7A97\u53E3`;
|
|
14009
14009
|
}
|
|
14010
|
+
var FIVE_HOUR_WINDOW_SEC = 5 * 3600;
|
|
14011
|
+
function clockWindowsLeft(usage, snapshotAt) {
|
|
14012
|
+
const weekly = usage?.weekly;
|
|
14013
|
+
if (!weekly || weekly.resetEpoch <= snapshotAt)
|
|
14014
|
+
return null;
|
|
14015
|
+
return (weekly.resetEpoch - snapshotAt) / FIVE_HOUR_WINDOW_SEC;
|
|
14016
|
+
}
|
|
14017
|
+
function formatClockWindowsLine(snapshot) {
|
|
14018
|
+
const values = [];
|
|
14019
|
+
const claude = clockWindowsLeft(snapshot.claude, snapshot.updatedAt);
|
|
14020
|
+
const codex = clockWindowsLeft(snapshot.codex, snapshot.updatedAt);
|
|
14021
|
+
if (claude !== null)
|
|
14022
|
+
values.push(["Claude", claude]);
|
|
14023
|
+
if (codex !== null)
|
|
14024
|
+
values.push(["Codex", codex]);
|
|
14025
|
+
if (values.length === 0)
|
|
14026
|
+
return null;
|
|
14027
|
+
const unique = [...new Set(values.map(([, value]) => value.toFixed(1)))];
|
|
14028
|
+
if (unique.length === 1)
|
|
14029
|
+
return `\u8DDD\u5468\u5237\u65B0\u8FD8\u80FD\u5BB9\u7EB3 ~${unique[0]} \u4E2A 5h \u7A97\u53E3\uFF08\u65F6\u949F\uFF09`;
|
|
14030
|
+
const byAgent = values.map(([name, value]) => `${name} ~${value.toFixed(1)}`).join(" / ");
|
|
14031
|
+
return `\u8DDD\u5468\u5237\u65B0\u8FD8\u80FD\u5BB9\u7EB3 ${byAgent} \u4E2A 5h \u7A97\u53E3\uFF08\u65F6\u949F\uFF09`;
|
|
14032
|
+
}
|
|
14033
|
+
function formatDynamicLineLine(snapshot) {
|
|
14034
|
+
const lines = snapshot.dynamicPauseLine;
|
|
14035
|
+
if (!lines)
|
|
14036
|
+
return null;
|
|
14037
|
+
const parts = [];
|
|
14038
|
+
const entries = [
|
|
14039
|
+
["Claude", lines.claude, snapshot.claude],
|
|
14040
|
+
["Codex", lines.codex, snapshot.codex]
|
|
14041
|
+
];
|
|
14042
|
+
for (const [name, line, usage] of entries) {
|
|
14043
|
+
if (line === null)
|
|
14044
|
+
continue;
|
|
14045
|
+
const headroom = usage ? `\uFF08util ${usage.gateUtil}%\uFF0C\u4F59\u91CF ${(line - usage.gateUtil).toFixed(1)}\uFF09` : "";
|
|
14046
|
+
parts.push(`${name} ${line.toFixed(1)}%${headroom}`);
|
|
14047
|
+
}
|
|
14048
|
+
if (parts.length === 0)
|
|
14049
|
+
return null;
|
|
14050
|
+
return `\u52A8\u6001\u6682\u505C\u7EBF\uFF1A${parts.join(" \xB7 ")}`;
|
|
14051
|
+
}
|
|
14010
14052
|
var PHASE_LABELS = {
|
|
14011
14053
|
normal: "normal\uFF08\u6B63\u5E38\uFF09",
|
|
14012
14054
|
balance: "balance\uFF08\u9700\u5747\u8861\uFF09",
|
|
14013
|
-
parallel: "parallel\uFF08\
|
|
14055
|
+
parallel: "parallel\uFF08\u5DF2\u9000\u5F79\uFF09",
|
|
14056
|
+
underutilized: "underutilized\uFF08\u989D\u5EA6\u6B20\u8F7D\uFF0C\u5EFA\u8BAE\u63D0\u901F\uFF09",
|
|
14014
14057
|
paused: "paused\uFF08\u9884\u7B97\u5E72\u9884\u4E2D\uFF09"
|
|
14015
14058
|
};
|
|
14016
14059
|
function renderBudgetSnapshot(snapshot, options = {}) {
|
|
14017
14060
|
const guardHardPct = options.guardHardPct ?? resolveGuardHardHint();
|
|
14018
14061
|
const lines = [];
|
|
14019
|
-
lines.push(`\u3010\u9884\u7B97\u5FEB\u7167 \xB7 \u8D26\u53F7\u7EA7\u3011\u9636\u6BB5\uFF1A${PHASE_LABELS[snapshot.phase]} \xB7 \u66F4\u65B0\u4E8E ${formatEpoch(snapshot.updatedAt)}`);
|
|
14062
|
+
lines.push(`\u3010\u9884\u7B97\u5FEB\u7167 \xB7 \u8D26\u53F7\u7EA7\u3011\u9636\u6BB5\uFF1A${PHASE_LABELS[snapshot.phase] ?? snapshot.phase} \xB7 \u66F4\u65B0\u4E8E ${formatEpoch(snapshot.updatedAt)}`);
|
|
14020
14063
|
lines.push(formatAgent("Claude", snapshot.claude, snapshot.updatedAt));
|
|
14021
14064
|
lines.push(formatAgent("Codex", snapshot.codex, snapshot.updatedAt));
|
|
14022
14065
|
if (snapshot.burnRate) {
|
|
@@ -14030,6 +14073,12 @@ function renderBudgetSnapshot(snapshot, options = {}) {
|
|
|
14030
14073
|
const fiveHourWindowsLeftLine = formatFiveHourWindowsLeftLine(snapshot);
|
|
14031
14074
|
if (fiveHourWindowsLeftLine)
|
|
14032
14075
|
lines.push(fiveHourWindowsLeftLine);
|
|
14076
|
+
const clockWindowsLine = formatClockWindowsLine(snapshot);
|
|
14077
|
+
if (clockWindowsLine)
|
|
14078
|
+
lines.push(clockWindowsLine);
|
|
14079
|
+
const dynamicLineLine = formatDynamicLineLine(snapshot);
|
|
14080
|
+
if (dynamicLineLine)
|
|
14081
|
+
lines.push(dynamicLineLine);
|
|
14033
14082
|
if (snapshot.claude && snapshot.codex) {
|
|
14034
14083
|
const abs = Math.abs(snapshot.driftPct);
|
|
14035
14084
|
if (abs > 0) {
|
|
@@ -14118,6 +14167,7 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
14118
14167
|
notificationIdPrefix;
|
|
14119
14168
|
instanceId;
|
|
14120
14169
|
replySender = null;
|
|
14170
|
+
resumeAckHandler = null;
|
|
14121
14171
|
logFile;
|
|
14122
14172
|
logger;
|
|
14123
14173
|
pendingMessages = [];
|
|
@@ -14168,6 +14218,9 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
14168
14218
|
setReplySender(sender) {
|
|
14169
14219
|
this.replySender = sender;
|
|
14170
14220
|
}
|
|
14221
|
+
setResumeAckHandler(handler) {
|
|
14222
|
+
this.resumeAckHandler = handler;
|
|
14223
|
+
}
|
|
14171
14224
|
getPendingMessageCount() {
|
|
14172
14225
|
return this.pendingMessages.length;
|
|
14173
14226
|
}
|
|
@@ -14195,7 +14248,8 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
14195
14248
|
user: "Codex",
|
|
14196
14249
|
user_id: "codex",
|
|
14197
14250
|
ts,
|
|
14198
|
-
source_type: "codex"
|
|
14251
|
+
source_type: "codex",
|
|
14252
|
+
...message.resumeId ? { resume_id: message.resumeId } : {}
|
|
14199
14253
|
}
|
|
14200
14254
|
}
|
|
14201
14255
|
});
|
|
@@ -14368,6 +14422,25 @@ chat_id: ${this.sessionId}`);
|
|
|
14368
14422
|
properties: {},
|
|
14369
14423
|
required: []
|
|
14370
14424
|
}
|
|
14425
|
+
},
|
|
14426
|
+
{
|
|
14427
|
+
name: "ack_resume",
|
|
14428
|
+
description: "ONLY for acknowledging a system_budget_resume directive (the budget window refreshed). NOT a general channel to Codex (use reply for that). This is an acknowledgement that you RECEIVED the resume directive \u2014 call it as soon as you see the notice, then continue the work; do NOT wait until the task is finished.",
|
|
14429
|
+
inputSchema: {
|
|
14430
|
+
type: "object",
|
|
14431
|
+
properties: {
|
|
14432
|
+
resume_id: {
|
|
14433
|
+
type: "string",
|
|
14434
|
+
description: "The resume_id from the system_budget_resume notice (meta.resume_id)."
|
|
14435
|
+
},
|
|
14436
|
+
status: {
|
|
14437
|
+
type: "string",
|
|
14438
|
+
enum: ["resumed", "declined", "already_running"],
|
|
14439
|
+
description: 'Acknowledgement outcome, recorded for observability only \u2014 all three values stop the resume re-push identically (the bridge takes no different downstream action for "declined"). "resumed" (default): you are resuming the task. "declined": you are not resuming. "already_running": you were already working and need no resume.'
|
|
14440
|
+
}
|
|
14441
|
+
},
|
|
14442
|
+
required: ["resume_id"]
|
|
14443
|
+
}
|
|
14371
14444
|
}
|
|
14372
14445
|
]
|
|
14373
14446
|
}));
|
|
@@ -14382,12 +14455,50 @@ chat_id: ${this.sessionId}`);
|
|
|
14382
14455
|
if (name === "get_budget") {
|
|
14383
14456
|
return this.handleGetBudget();
|
|
14384
14457
|
}
|
|
14458
|
+
if (name === "ack_resume") {
|
|
14459
|
+
return this.handleAckResume(args);
|
|
14460
|
+
}
|
|
14385
14461
|
return {
|
|
14386
14462
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
14387
14463
|
isError: true
|
|
14388
14464
|
};
|
|
14389
14465
|
});
|
|
14390
14466
|
}
|
|
14467
|
+
async handleAckResume(args) {
|
|
14468
|
+
const resumeIdRaw = args?.resume_id;
|
|
14469
|
+
if (typeof resumeIdRaw !== "string" || resumeIdRaw.length === 0) {
|
|
14470
|
+
return {
|
|
14471
|
+
content: [{ type: "text", text: "Error: missing required parameter 'resume_id'" }],
|
|
14472
|
+
isError: true
|
|
14473
|
+
};
|
|
14474
|
+
}
|
|
14475
|
+
if (resumeIdRaw.length > 128) {
|
|
14476
|
+
return {
|
|
14477
|
+
content: [{ type: "text", text: `Error: resume_id is too long (${resumeIdRaw.length} chars, max 128).` }],
|
|
14478
|
+
isError: true
|
|
14479
|
+
};
|
|
14480
|
+
}
|
|
14481
|
+
const statusRaw = args?.status;
|
|
14482
|
+
if (statusRaw !== undefined && statusRaw !== "resumed" && statusRaw !== "declined" && statusRaw !== "already_running") {
|
|
14483
|
+
return {
|
|
14484
|
+
content: [{ type: "text", text: `Error: invalid status value ${JSON.stringify(statusRaw)} \u2014 use "resumed", "declined" or "already_running".` }],
|
|
14485
|
+
isError: true
|
|
14486
|
+
};
|
|
14487
|
+
}
|
|
14488
|
+
const status = typeof statusRaw === "string" ? statusRaw : "resumed";
|
|
14489
|
+
if (!this.resumeAckHandler) {
|
|
14490
|
+
this.log("No resume ack handler registered");
|
|
14491
|
+
return {
|
|
14492
|
+
content: [{ type: "text", text: "Error: bridge not initialized, cannot acknowledge resume." }],
|
|
14493
|
+
isError: true
|
|
14494
|
+
};
|
|
14495
|
+
}
|
|
14496
|
+
this.log(`ack_resume received (resume_id=${resumeIdRaw}, status=${status}, instance=${this.instanceId})`);
|
|
14497
|
+
this.resumeAckHandler(resumeIdRaw, status);
|
|
14498
|
+
return {
|
|
14499
|
+
content: [{ type: "text", text: `Resume acknowledged (resume_id=${resumeIdRaw}, status=${status}).` }]
|
|
14500
|
+
};
|
|
14501
|
+
}
|
|
14391
14502
|
handleGetBudget() {
|
|
14392
14503
|
this.log(`get_budget called (instance=${this.instanceId}, hasSnapshot=${this.budgetSnapshot !== null})`);
|
|
14393
14504
|
const text = this.budgetSnapshot ? renderBudgetSnapshot(this.budgetSnapshot) : BUDGET_UNAVAILABLE_TEXT;
|
|
@@ -14511,11 +14622,11 @@ function defineNumber(value, fallback) {
|
|
|
14511
14622
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
14512
14623
|
}
|
|
14513
14624
|
var BUILD_INFO = Object.freeze({
|
|
14514
|
-
version: defineString("0.1.
|
|
14515
|
-
commit: defineString("
|
|
14625
|
+
version: defineString("0.1.18", "0.0.0-source"),
|
|
14626
|
+
commit: defineString("9db0aa3", "source"),
|
|
14516
14627
|
bundle: defineBundle("plugin"),
|
|
14517
14628
|
contractVersion: defineNumber(1, CONTRACT_VERSION),
|
|
14518
|
-
codeHash: defineString("
|
|
14629
|
+
codeHash: defineString("46a6407023f0", "source")
|
|
14519
14630
|
});
|
|
14520
14631
|
function sameRuntimeContract(a, b) {
|
|
14521
14632
|
if (!a || !b)
|
|
@@ -14781,6 +14892,9 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14781
14892
|
});
|
|
14782
14893
|
return pending;
|
|
14783
14894
|
}
|
|
14895
|
+
sendAckResume(resumeId, status) {
|
|
14896
|
+
this.send({ type: "ack_resume", resumeId, status });
|
|
14897
|
+
}
|
|
14784
14898
|
attachSocketHandlers(ws, socketId) {
|
|
14785
14899
|
ws.onmessage = (event) => {
|
|
14786
14900
|
const raw = typeof event.data === "string" ? event.data : event.data.toString();
|
|
@@ -15547,7 +15661,17 @@ var DEFAULT_BUDGET_CONFIG = {
|
|
|
15547
15661
|
balanced: { effort: "medium" },
|
|
15548
15662
|
eco: { effort: "low" }
|
|
15549
15663
|
},
|
|
15550
|
-
|
|
15664
|
+
maximize: {
|
|
15665
|
+
targetUtil: 98,
|
|
15666
|
+
reserveSlopePctPerHour: 0.4,
|
|
15667
|
+
reserveMaxPct: 7,
|
|
15668
|
+
finishingHorizonMinutes: 30,
|
|
15669
|
+
resumeHysteresisPct: 5
|
|
15670
|
+
},
|
|
15671
|
+
allocation: {
|
|
15672
|
+
minRunwayRatio: 50,
|
|
15673
|
+
minRunwayGapHours: 2
|
|
15674
|
+
}
|
|
15551
15675
|
};
|
|
15552
15676
|
var DEFAULT_CONFIG = {
|
|
15553
15677
|
version: "1.0",
|
|
@@ -15600,6 +15724,34 @@ function findShapeViolation(raw) {
|
|
|
15600
15724
|
}
|
|
15601
15725
|
}
|
|
15602
15726
|
}
|
|
15727
|
+
if ("maximize" in budget) {
|
|
15728
|
+
const maximize = budget.maximize;
|
|
15729
|
+
if (!isRecord(maximize)) {
|
|
15730
|
+
return "budget.maximize is present but not an object";
|
|
15731
|
+
}
|
|
15732
|
+
for (const key of [
|
|
15733
|
+
"targetUtil",
|
|
15734
|
+
"reserveSlopePctPerHour",
|
|
15735
|
+
"reserveMaxPct",
|
|
15736
|
+
"finishingHorizonMinutes",
|
|
15737
|
+
"resumeHysteresisPct"
|
|
15738
|
+
]) {
|
|
15739
|
+
if (key in maximize && !isCoercibleNumber(maximize[key])) {
|
|
15740
|
+
return `budget.maximize.${key} is present but not a number`;
|
|
15741
|
+
}
|
|
15742
|
+
}
|
|
15743
|
+
}
|
|
15744
|
+
if ("allocation" in budget) {
|
|
15745
|
+
const allocation = budget.allocation;
|
|
15746
|
+
if (!isRecord(allocation)) {
|
|
15747
|
+
return "budget.allocation is present but not an object";
|
|
15748
|
+
}
|
|
15749
|
+
for (const key of ["minRunwayRatio", "minRunwayGapHours"]) {
|
|
15750
|
+
if (key in allocation && !isCoercibleNumber(allocation[key])) {
|
|
15751
|
+
return `budget.allocation.${key} is present but not a number`;
|
|
15752
|
+
}
|
|
15753
|
+
}
|
|
15754
|
+
}
|
|
15603
15755
|
}
|
|
15604
15756
|
return null;
|
|
15605
15757
|
}
|
|
@@ -15607,7 +15759,7 @@ function hasCustomDecisionValues(config2) {
|
|
|
15607
15759
|
const d = DEFAULT_CONFIG;
|
|
15608
15760
|
const b = config2.budget;
|
|
15609
15761
|
const db = d.budget;
|
|
15610
|
-
return config2.idleShutdownSeconds !== d.idleShutdownSeconds || config2.turnCoordination.attentionWindowSeconds !== d.turnCoordination.attentionWindowSeconds || config2.codex.appPort !== d.codex.appPort || config2.codex.proxyPort !== d.codex.proxyPort || b.enabled !== db.enabled || b.pollSeconds !== db.pollSeconds || b.pauseAt !== db.pauseAt || b.resumeBelow !== db.resumeBelow || b.syncDriftPct !== db.syncDriftPct || b.parallel.minRemainingPct !== db.parallel.minRemainingPct || b.parallel.timeWindowSec !== db.parallel.timeWindowSec || b.codexTierControl !== db.codexTierControl;
|
|
15762
|
+
return config2.idleShutdownSeconds !== d.idleShutdownSeconds || config2.turnCoordination.attentionWindowSeconds !== d.turnCoordination.attentionWindowSeconds || config2.codex.appPort !== d.codex.appPort || config2.codex.proxyPort !== d.codex.proxyPort || b.enabled !== db.enabled || b.pollSeconds !== db.pollSeconds || b.pauseAt !== db.pauseAt || b.resumeBelow !== db.resumeBelow || b.syncDriftPct !== db.syncDriftPct || b.parallel.minRemainingPct !== db.parallel.minRemainingPct || b.parallel.timeWindowSec !== db.parallel.timeWindowSec || b.codexTierControl !== db.codexTierControl || b.maximize.targetUtil !== db.maximize.targetUtil || b.maximize.reserveSlopePctPerHour !== db.maximize.reserveSlopePctPerHour || b.maximize.reserveMaxPct !== db.maximize.reserveMaxPct || b.maximize.finishingHorizonMinutes !== db.maximize.finishingHorizonMinutes || b.maximize.resumeHysteresisPct !== db.maximize.resumeHysteresisPct || b.allocation.minRunwayRatio !== db.allocation.minRunwayRatio || b.allocation.minRunwayGapHours !== db.allocation.minRunwayGapHours;
|
|
15611
15763
|
}
|
|
15612
15764
|
function normalizeInteger(value, fallback) {
|
|
15613
15765
|
if (typeof value === "number" && Number.isFinite(value))
|
|
@@ -15625,8 +15777,41 @@ function normalizeBoundedInteger(value, fallback, min, max) {
|
|
|
15625
15777
|
return fallback;
|
|
15626
15778
|
return parsed;
|
|
15627
15779
|
}
|
|
15628
|
-
function
|
|
15629
|
-
|
|
15780
|
+
function normalizeBoundedNumber(value, fallback, min, max) {
|
|
15781
|
+
let parsed;
|
|
15782
|
+
if (typeof value === "number") {
|
|
15783
|
+
parsed = value;
|
|
15784
|
+
} else if (typeof value === "string" && value.trim() !== "") {
|
|
15785
|
+
parsed = Number(value);
|
|
15786
|
+
} else {
|
|
15787
|
+
return fallback;
|
|
15788
|
+
}
|
|
15789
|
+
if (!Number.isFinite(parsed))
|
|
15790
|
+
return fallback;
|
|
15791
|
+
if (parsed < min || parsed > max)
|
|
15792
|
+
return fallback;
|
|
15793
|
+
return parsed;
|
|
15794
|
+
}
|
|
15795
|
+
function normalizeMaximizeConfig(raw, pauseAt, fallback = DEFAULT_BUDGET_CONFIG.maximize) {
|
|
15796
|
+
const m = isRecord(raw) ? raw : {};
|
|
15797
|
+
const normalized = {
|
|
15798
|
+
targetUtil: normalizeBoundedInteger(m.targetUtil, fallback.targetUtil, 90, 99),
|
|
15799
|
+
reserveSlopePctPerHour: normalizeBoundedNumber(m.reserveSlopePctPerHour, fallback.reserveSlopePctPerHour, 0, 5),
|
|
15800
|
+
reserveMaxPct: normalizeBoundedInteger(m.reserveMaxPct, fallback.reserveMaxPct, 0, 30),
|
|
15801
|
+
finishingHorizonMinutes: normalizeBoundedInteger(m.finishingHorizonMinutes, fallback.finishingHorizonMinutes, 5, 180),
|
|
15802
|
+
resumeHysteresisPct: normalizeBoundedInteger(m.resumeHysteresisPct, fallback.resumeHysteresisPct, 1, 30)
|
|
15803
|
+
};
|
|
15804
|
+
if (normalized.targetUtil <= pauseAt) {
|
|
15805
|
+
return { ...DEFAULT_BUDGET_CONFIG.maximize };
|
|
15806
|
+
}
|
|
15807
|
+
return normalized;
|
|
15808
|
+
}
|
|
15809
|
+
function normalizeAllocationConfig(raw, fallback = DEFAULT_BUDGET_CONFIG.allocation) {
|
|
15810
|
+
const a = isRecord(raw) ? raw : {};
|
|
15811
|
+
return {
|
|
15812
|
+
minRunwayRatio: normalizeBoundedInteger(a.minRunwayRatio, fallback.minRunwayRatio, 10, 100),
|
|
15813
|
+
minRunwayGapHours: normalizeBoundedInteger(a.minRunwayGapHours, fallback.minRunwayGapHours, 1, 168)
|
|
15814
|
+
};
|
|
15630
15815
|
}
|
|
15631
15816
|
function normalizeBoolean(value, fallback) {
|
|
15632
15817
|
if (typeof value === "boolean")
|
|
@@ -15677,7 +15862,8 @@ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
|
|
|
15677
15862
|
},
|
|
15678
15863
|
codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
|
|
15679
15864
|
codexTiers,
|
|
15680
|
-
|
|
15865
|
+
maximize: normalizeMaximizeConfig(budget.maximize, pauseAt, fallback.maximize),
|
|
15866
|
+
allocation: normalizeAllocationConfig(budget.allocation, fallback.allocation)
|
|
15681
15867
|
};
|
|
15682
15868
|
}
|
|
15683
15869
|
function normalizeConfig(raw) {
|
|
@@ -16209,6 +16395,17 @@ claude.setReplySender(async (msg, requireReply, onBusy, idempotencyKey) => {
|
|
|
16209
16395
|
}
|
|
16210
16396
|
return daemonClient.sendReply(msg, requireReply, onBusy, idempotencyKey);
|
|
16211
16397
|
});
|
|
16398
|
+
claude.setResumeAckHandler((resumeId, status) => {
|
|
16399
|
+
if (daemonDisabled) {
|
|
16400
|
+
log(`Resume ack ${resumeId} (${status}) dropped \u2014 daemon disabled (${daemonDisabledReason ?? "killed"})`);
|
|
16401
|
+
return;
|
|
16402
|
+
}
|
|
16403
|
+
try {
|
|
16404
|
+
daemonClient.sendAckResume(resumeId, status);
|
|
16405
|
+
} catch (err) {
|
|
16406
|
+
log(`Resume ack ${resumeId} (${status}) send failed: ${err?.message ?? err}`);
|
|
16407
|
+
}
|
|
16408
|
+
});
|
|
16212
16409
|
daemonClient.on("turnStarted", ({ requestId, idempotencyKey, threadId, turnId }) => {
|
|
16213
16410
|
log(`Codex turn started for reply ${requestId} (turn=${turnId}, thread=${threadId}` + `${idempotencyKey ? `, idempotencyKey=${idempotencyKey}` : ""})`);
|
|
16214
16411
|
});
|