@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raysonmeng/agentbridge",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
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.17",
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,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\u5EFA\u8BAE\u5E76\u884C\u63D0\u901F\uFF09",
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.16", "0.0.0-source"),
14515
- commit: defineString("1dc5e4f", "source"),
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("1fc975838e46", "source")
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
- strategy: "conserve"
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 normalizeStrategy(value, fallback) {
15629
- return value === "conserve" || value === "maximize" ? value : fallback;
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
- strategy: normalizeStrategy(budget.strategy, fallback.strategy)
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
  });