@raysonmeng/agentbridge 0.1.16 → 0.1.17
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 +143 -32
- 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 +185 -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,36 @@ 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
|
+
function formatDynamicLineLine(snapshot) {
|
|
14011
|
+
const lines = snapshot.dynamicPauseLine;
|
|
14012
|
+
if (!lines)
|
|
14013
|
+
return null;
|
|
14014
|
+
const parts = [];
|
|
14015
|
+
const entries = [
|
|
14016
|
+
["Claude", lines.claude, snapshot.claude],
|
|
14017
|
+
["Codex", lines.codex, snapshot.codex]
|
|
14018
|
+
];
|
|
14019
|
+
for (const [name, line, usage] of entries) {
|
|
14020
|
+
if (line === null)
|
|
14021
|
+
continue;
|
|
14022
|
+
const headroom = usage ? `\uFF08util ${usage.gateUtil}%\uFF0C\u4F59\u91CF ${(line - usage.gateUtil).toFixed(1)}\uFF09` : "";
|
|
14023
|
+
parts.push(`${name} ${line.toFixed(1)}%${headroom}`);
|
|
14024
|
+
}
|
|
14025
|
+
if (parts.length === 0)
|
|
14026
|
+
return null;
|
|
14027
|
+
return `\u52A8\u6001\u6682\u505C\u7EBF\uFF1A${parts.join(" \xB7 ")}`;
|
|
14028
|
+
}
|
|
14010
14029
|
var PHASE_LABELS = {
|
|
14011
14030
|
normal: "normal\uFF08\u6B63\u5E38\uFF09",
|
|
14012
14031
|
balance: "balance\uFF08\u9700\u5747\u8861\uFF09",
|
|
14013
|
-
parallel: "parallel\uFF08\
|
|
14032
|
+
parallel: "parallel\uFF08\u5DF2\u9000\u5F79\uFF09",
|
|
14033
|
+
underutilized: "underutilized\uFF08\u989D\u5EA6\u6B20\u8F7D\uFF0C\u5EFA\u8BAE\u63D0\u901F\uFF09",
|
|
14014
14034
|
paused: "paused\uFF08\u9884\u7B97\u5E72\u9884\u4E2D\uFF09"
|
|
14015
14035
|
};
|
|
14016
14036
|
function renderBudgetSnapshot(snapshot, options = {}) {
|
|
14017
14037
|
const guardHardPct = options.guardHardPct ?? resolveGuardHardHint();
|
|
14018
14038
|
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)}`);
|
|
14039
|
+
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
14040
|
lines.push(formatAgent("Claude", snapshot.claude, snapshot.updatedAt));
|
|
14021
14041
|
lines.push(formatAgent("Codex", snapshot.codex, snapshot.updatedAt));
|
|
14022
14042
|
if (snapshot.burnRate) {
|
|
@@ -14030,6 +14050,9 @@ function renderBudgetSnapshot(snapshot, options = {}) {
|
|
|
14030
14050
|
const fiveHourWindowsLeftLine = formatFiveHourWindowsLeftLine(snapshot);
|
|
14031
14051
|
if (fiveHourWindowsLeftLine)
|
|
14032
14052
|
lines.push(fiveHourWindowsLeftLine);
|
|
14053
|
+
const dynamicLineLine = formatDynamicLineLine(snapshot);
|
|
14054
|
+
if (dynamicLineLine)
|
|
14055
|
+
lines.push(dynamicLineLine);
|
|
14033
14056
|
if (snapshot.claude && snapshot.codex) {
|
|
14034
14057
|
const abs = Math.abs(snapshot.driftPct);
|
|
14035
14058
|
if (abs > 0) {
|
|
@@ -14118,6 +14141,7 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
14118
14141
|
notificationIdPrefix;
|
|
14119
14142
|
instanceId;
|
|
14120
14143
|
replySender = null;
|
|
14144
|
+
resumeAckHandler = null;
|
|
14121
14145
|
logFile;
|
|
14122
14146
|
logger;
|
|
14123
14147
|
pendingMessages = [];
|
|
@@ -14168,6 +14192,9 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
14168
14192
|
setReplySender(sender) {
|
|
14169
14193
|
this.replySender = sender;
|
|
14170
14194
|
}
|
|
14195
|
+
setResumeAckHandler(handler) {
|
|
14196
|
+
this.resumeAckHandler = handler;
|
|
14197
|
+
}
|
|
14171
14198
|
getPendingMessageCount() {
|
|
14172
14199
|
return this.pendingMessages.length;
|
|
14173
14200
|
}
|
|
@@ -14195,7 +14222,8 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
14195
14222
|
user: "Codex",
|
|
14196
14223
|
user_id: "codex",
|
|
14197
14224
|
ts,
|
|
14198
|
-
source_type: "codex"
|
|
14225
|
+
source_type: "codex",
|
|
14226
|
+
...message.resumeId ? { resume_id: message.resumeId } : {}
|
|
14199
14227
|
}
|
|
14200
14228
|
}
|
|
14201
14229
|
});
|
|
@@ -14368,6 +14396,25 @@ chat_id: ${this.sessionId}`);
|
|
|
14368
14396
|
properties: {},
|
|
14369
14397
|
required: []
|
|
14370
14398
|
}
|
|
14399
|
+
},
|
|
14400
|
+
{
|
|
14401
|
+
name: "ack_resume",
|
|
14402
|
+
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.",
|
|
14403
|
+
inputSchema: {
|
|
14404
|
+
type: "object",
|
|
14405
|
+
properties: {
|
|
14406
|
+
resume_id: {
|
|
14407
|
+
type: "string",
|
|
14408
|
+
description: "The resume_id from the system_budget_resume notice (meta.resume_id)."
|
|
14409
|
+
},
|
|
14410
|
+
status: {
|
|
14411
|
+
type: "string",
|
|
14412
|
+
enum: ["resumed", "declined", "already_running"],
|
|
14413
|
+
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.'
|
|
14414
|
+
}
|
|
14415
|
+
},
|
|
14416
|
+
required: ["resume_id"]
|
|
14417
|
+
}
|
|
14371
14418
|
}
|
|
14372
14419
|
]
|
|
14373
14420
|
}));
|
|
@@ -14382,12 +14429,50 @@ chat_id: ${this.sessionId}`);
|
|
|
14382
14429
|
if (name === "get_budget") {
|
|
14383
14430
|
return this.handleGetBudget();
|
|
14384
14431
|
}
|
|
14432
|
+
if (name === "ack_resume") {
|
|
14433
|
+
return this.handleAckResume(args);
|
|
14434
|
+
}
|
|
14385
14435
|
return {
|
|
14386
14436
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
14387
14437
|
isError: true
|
|
14388
14438
|
};
|
|
14389
14439
|
});
|
|
14390
14440
|
}
|
|
14441
|
+
async handleAckResume(args) {
|
|
14442
|
+
const resumeIdRaw = args?.resume_id;
|
|
14443
|
+
if (typeof resumeIdRaw !== "string" || resumeIdRaw.length === 0) {
|
|
14444
|
+
return {
|
|
14445
|
+
content: [{ type: "text", text: "Error: missing required parameter 'resume_id'" }],
|
|
14446
|
+
isError: true
|
|
14447
|
+
};
|
|
14448
|
+
}
|
|
14449
|
+
if (resumeIdRaw.length > 128) {
|
|
14450
|
+
return {
|
|
14451
|
+
content: [{ type: "text", text: `Error: resume_id is too long (${resumeIdRaw.length} chars, max 128).` }],
|
|
14452
|
+
isError: true
|
|
14453
|
+
};
|
|
14454
|
+
}
|
|
14455
|
+
const statusRaw = args?.status;
|
|
14456
|
+
if (statusRaw !== undefined && statusRaw !== "resumed" && statusRaw !== "declined" && statusRaw !== "already_running") {
|
|
14457
|
+
return {
|
|
14458
|
+
content: [{ type: "text", text: `Error: invalid status value ${JSON.stringify(statusRaw)} \u2014 use "resumed", "declined" or "already_running".` }],
|
|
14459
|
+
isError: true
|
|
14460
|
+
};
|
|
14461
|
+
}
|
|
14462
|
+
const status = typeof statusRaw === "string" ? statusRaw : "resumed";
|
|
14463
|
+
if (!this.resumeAckHandler) {
|
|
14464
|
+
this.log("No resume ack handler registered");
|
|
14465
|
+
return {
|
|
14466
|
+
content: [{ type: "text", text: "Error: bridge not initialized, cannot acknowledge resume." }],
|
|
14467
|
+
isError: true
|
|
14468
|
+
};
|
|
14469
|
+
}
|
|
14470
|
+
this.log(`ack_resume received (resume_id=${resumeIdRaw}, status=${status}, instance=${this.instanceId})`);
|
|
14471
|
+
this.resumeAckHandler(resumeIdRaw, status);
|
|
14472
|
+
return {
|
|
14473
|
+
content: [{ type: "text", text: `Resume acknowledged (resume_id=${resumeIdRaw}, status=${status}).` }]
|
|
14474
|
+
};
|
|
14475
|
+
}
|
|
14391
14476
|
handleGetBudget() {
|
|
14392
14477
|
this.log(`get_budget called (instance=${this.instanceId}, hasSnapshot=${this.budgetSnapshot !== null})`);
|
|
14393
14478
|
const text = this.budgetSnapshot ? renderBudgetSnapshot(this.budgetSnapshot) : BUDGET_UNAVAILABLE_TEXT;
|
|
@@ -14511,11 +14596,11 @@ function defineNumber(value, fallback) {
|
|
|
14511
14596
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
14512
14597
|
}
|
|
14513
14598
|
var BUILD_INFO = Object.freeze({
|
|
14514
|
-
version: defineString("0.1.
|
|
14515
|
-
commit: defineString("
|
|
14599
|
+
version: defineString("0.1.17", "0.0.0-source"),
|
|
14600
|
+
commit: defineString("0d1e1bd", "source"),
|
|
14516
14601
|
bundle: defineBundle("plugin"),
|
|
14517
14602
|
contractVersion: defineNumber(1, CONTRACT_VERSION),
|
|
14518
|
-
codeHash: defineString("
|
|
14603
|
+
codeHash: defineString("c22387f3269f", "source")
|
|
14519
14604
|
});
|
|
14520
14605
|
function sameRuntimeContract(a, b) {
|
|
14521
14606
|
if (!a || !b)
|
|
@@ -14781,6 +14866,9 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14781
14866
|
});
|
|
14782
14867
|
return pending;
|
|
14783
14868
|
}
|
|
14869
|
+
sendAckResume(resumeId, status) {
|
|
14870
|
+
this.send({ type: "ack_resume", resumeId, status });
|
|
14871
|
+
}
|
|
14784
14872
|
attachSocketHandlers(ws, socketId) {
|
|
14785
14873
|
ws.onmessage = (event) => {
|
|
14786
14874
|
const raw = typeof event.data === "string" ? event.data : event.data.toString();
|
|
@@ -15547,7 +15635,17 @@ var DEFAULT_BUDGET_CONFIG = {
|
|
|
15547
15635
|
balanced: { effort: "medium" },
|
|
15548
15636
|
eco: { effort: "low" }
|
|
15549
15637
|
},
|
|
15550
|
-
|
|
15638
|
+
maximize: {
|
|
15639
|
+
targetUtil: 98,
|
|
15640
|
+
reserveSlopePctPerHour: 0.4,
|
|
15641
|
+
reserveMaxPct: 7,
|
|
15642
|
+
finishingHorizonMinutes: 30,
|
|
15643
|
+
resumeHysteresisPct: 5
|
|
15644
|
+
},
|
|
15645
|
+
allocation: {
|
|
15646
|
+
minRunwayRatio: 50,
|
|
15647
|
+
minRunwayGapHours: 2
|
|
15648
|
+
}
|
|
15551
15649
|
};
|
|
15552
15650
|
var DEFAULT_CONFIG = {
|
|
15553
15651
|
version: "1.0",
|
|
@@ -15600,6 +15698,34 @@ function findShapeViolation(raw) {
|
|
|
15600
15698
|
}
|
|
15601
15699
|
}
|
|
15602
15700
|
}
|
|
15701
|
+
if ("maximize" in budget) {
|
|
15702
|
+
const maximize = budget.maximize;
|
|
15703
|
+
if (!isRecord(maximize)) {
|
|
15704
|
+
return "budget.maximize is present but not an object";
|
|
15705
|
+
}
|
|
15706
|
+
for (const key of [
|
|
15707
|
+
"targetUtil",
|
|
15708
|
+
"reserveSlopePctPerHour",
|
|
15709
|
+
"reserveMaxPct",
|
|
15710
|
+
"finishingHorizonMinutes",
|
|
15711
|
+
"resumeHysteresisPct"
|
|
15712
|
+
]) {
|
|
15713
|
+
if (key in maximize && !isCoercibleNumber(maximize[key])) {
|
|
15714
|
+
return `budget.maximize.${key} is present but not a number`;
|
|
15715
|
+
}
|
|
15716
|
+
}
|
|
15717
|
+
}
|
|
15718
|
+
if ("allocation" in budget) {
|
|
15719
|
+
const allocation = budget.allocation;
|
|
15720
|
+
if (!isRecord(allocation)) {
|
|
15721
|
+
return "budget.allocation is present but not an object";
|
|
15722
|
+
}
|
|
15723
|
+
for (const key of ["minRunwayRatio", "minRunwayGapHours"]) {
|
|
15724
|
+
if (key in allocation && !isCoercibleNumber(allocation[key])) {
|
|
15725
|
+
return `budget.allocation.${key} is present but not a number`;
|
|
15726
|
+
}
|
|
15727
|
+
}
|
|
15728
|
+
}
|
|
15603
15729
|
}
|
|
15604
15730
|
return null;
|
|
15605
15731
|
}
|
|
@@ -15607,7 +15733,7 @@ function hasCustomDecisionValues(config2) {
|
|
|
15607
15733
|
const d = DEFAULT_CONFIG;
|
|
15608
15734
|
const b = config2.budget;
|
|
15609
15735
|
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;
|
|
15736
|
+
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
15737
|
}
|
|
15612
15738
|
function normalizeInteger(value, fallback) {
|
|
15613
15739
|
if (typeof value === "number" && Number.isFinite(value))
|
|
@@ -15625,8 +15751,41 @@ function normalizeBoundedInteger(value, fallback, min, max) {
|
|
|
15625
15751
|
return fallback;
|
|
15626
15752
|
return parsed;
|
|
15627
15753
|
}
|
|
15628
|
-
function
|
|
15629
|
-
|
|
15754
|
+
function normalizeBoundedNumber(value, fallback, min, max) {
|
|
15755
|
+
let parsed;
|
|
15756
|
+
if (typeof value === "number") {
|
|
15757
|
+
parsed = value;
|
|
15758
|
+
} else if (typeof value === "string" && value.trim() !== "") {
|
|
15759
|
+
parsed = Number(value);
|
|
15760
|
+
} else {
|
|
15761
|
+
return fallback;
|
|
15762
|
+
}
|
|
15763
|
+
if (!Number.isFinite(parsed))
|
|
15764
|
+
return fallback;
|
|
15765
|
+
if (parsed < min || parsed > max)
|
|
15766
|
+
return fallback;
|
|
15767
|
+
return parsed;
|
|
15768
|
+
}
|
|
15769
|
+
function normalizeMaximizeConfig(raw, pauseAt, fallback = DEFAULT_BUDGET_CONFIG.maximize) {
|
|
15770
|
+
const m = isRecord(raw) ? raw : {};
|
|
15771
|
+
const normalized = {
|
|
15772
|
+
targetUtil: normalizeBoundedInteger(m.targetUtil, fallback.targetUtil, 90, 99),
|
|
15773
|
+
reserveSlopePctPerHour: normalizeBoundedNumber(m.reserveSlopePctPerHour, fallback.reserveSlopePctPerHour, 0, 5),
|
|
15774
|
+
reserveMaxPct: normalizeBoundedInteger(m.reserveMaxPct, fallback.reserveMaxPct, 0, 30),
|
|
15775
|
+
finishingHorizonMinutes: normalizeBoundedInteger(m.finishingHorizonMinutes, fallback.finishingHorizonMinutes, 5, 180),
|
|
15776
|
+
resumeHysteresisPct: normalizeBoundedInteger(m.resumeHysteresisPct, fallback.resumeHysteresisPct, 1, 30)
|
|
15777
|
+
};
|
|
15778
|
+
if (normalized.targetUtil <= pauseAt) {
|
|
15779
|
+
return { ...DEFAULT_BUDGET_CONFIG.maximize };
|
|
15780
|
+
}
|
|
15781
|
+
return normalized;
|
|
15782
|
+
}
|
|
15783
|
+
function normalizeAllocationConfig(raw, fallback = DEFAULT_BUDGET_CONFIG.allocation) {
|
|
15784
|
+
const a = isRecord(raw) ? raw : {};
|
|
15785
|
+
return {
|
|
15786
|
+
minRunwayRatio: normalizeBoundedInteger(a.minRunwayRatio, fallback.minRunwayRatio, 10, 100),
|
|
15787
|
+
minRunwayGapHours: normalizeBoundedInteger(a.minRunwayGapHours, fallback.minRunwayGapHours, 1, 168)
|
|
15788
|
+
};
|
|
15630
15789
|
}
|
|
15631
15790
|
function normalizeBoolean(value, fallback) {
|
|
15632
15791
|
if (typeof value === "boolean")
|
|
@@ -15677,7 +15836,8 @@ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
|
|
|
15677
15836
|
},
|
|
15678
15837
|
codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
|
|
15679
15838
|
codexTiers,
|
|
15680
|
-
|
|
15839
|
+
maximize: normalizeMaximizeConfig(budget.maximize, pauseAt, fallback.maximize),
|
|
15840
|
+
allocation: normalizeAllocationConfig(budget.allocation, fallback.allocation)
|
|
15681
15841
|
};
|
|
15682
15842
|
}
|
|
15683
15843
|
function normalizeConfig(raw) {
|
|
@@ -16209,6 +16369,17 @@ claude.setReplySender(async (msg, requireReply, onBusy, idempotencyKey) => {
|
|
|
16209
16369
|
}
|
|
16210
16370
|
return daemonClient.sendReply(msg, requireReply, onBusy, idempotencyKey);
|
|
16211
16371
|
});
|
|
16372
|
+
claude.setResumeAckHandler((resumeId, status) => {
|
|
16373
|
+
if (daemonDisabled) {
|
|
16374
|
+
log(`Resume ack ${resumeId} (${status}) dropped \u2014 daemon disabled (${daemonDisabledReason ?? "killed"})`);
|
|
16375
|
+
return;
|
|
16376
|
+
}
|
|
16377
|
+
try {
|
|
16378
|
+
daemonClient.sendAckResume(resumeId, status);
|
|
16379
|
+
} catch (err) {
|
|
16380
|
+
log(`Resume ack ${resumeId} (${status}) send failed: ${err?.message ?? err}`);
|
|
16381
|
+
}
|
|
16382
|
+
});
|
|
16212
16383
|
daemonClient.on("turnStarted", ({ requestId, idempotencyKey, threadId, turnId }) => {
|
|
16213
16384
|
log(`Codex turn started for reply ${requestId} (turn=${turnId}, thread=${threadId}` + `${idempotencyKey ? `, idempotencyKey=${idempotencyKey}` : ""})`);
|
|
16214
16385
|
});
|