@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raysonmeng/agentbridge",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Bridge between Claude Code and Codex — bidirectional agent communication via MCP Channel + JSON-RPC",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.11",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentbridge",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Bridge Claude Code and Codex with a shared daemon, push channel delivery, and bidirectional reply tooling.",
5
5
  "author": {
6
6
  "name": "AgentBridge Contributors",
@@ -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-state.ts
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 = 92;
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\u5EFA\u8BAE\u5E76\u884C\u63D0\u901F\uFF09",
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.16", "0.0.0-source"),
14515
- commit: defineString("1dc5e4f", "source"),
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("1fc975838e46", "source")
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
- strategy: "conserve"
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 normalizeStrategy(value, fallback) {
15629
- return value === "conserve" || value === "maximize" ? value : fallback;
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
- strategy: normalizeStrategy(budget.strategy, fallback.strategy)
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
  });