@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.
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "name": "agentbridge",
14
14
  "description": "Bridge Claude Code and Codex through a shared daemon, push channel delivery, and reply/get_messages tools.",
15
- "version": "0.1.16",
15
+ "version": "0.1.18",
16
16
  "author": {
17
17
  "name": "AgentBridge Contributors",
18
18
  "email": "raysonmeng@qq.com"
package/README.md CHANGED
@@ -28,6 +28,16 @@ When Claude Code closes, the foreground MCP process exits while the background d
28
28
  - A generic orchestration framework for arbitrary agent backends
29
29
  - A hardened security boundary between tools you do not trust
30
30
 
31
+ ## Features
32
+
33
+ - **Bidirectional Claude ↔ Codex messaging** in one working session — Codex output is intercepted and pushed to Claude as channel notifications; Claude replies via the `reply` MCP tool, injected into the Codex thread as a `turn/start`.
34
+ - **Push delivery with fallback** — messages arrive as channel notifications; a failed push falls back to an in-memory queue drained by `get_messages`. Loop prevention via the per-message `source` field.
35
+ - **Turn coordination** — a busy-guard rejects replies during an active Codex turn; a per-turn inactivity watchdog stops a lost `turn/completed` from locking injection forever; noisy intermediate events are collapsed so only meaningful `agentMessage` payloads reach Claude.
36
+ - **Multiple pairs side by side** — one Claude+Codex pair per project directory, ports allocated per pair in +10 strides from 4500. Pair-aware `claude` / `codex` / `resume` / `kill` / `doctor` / `budget` via `--pair`.
37
+ - **Resilient lifecycle** — a persistent background daemon survives Claude Code restarts (auto-reconnect with backoff); orphan-process cleanup; `abg doctor` read-only diagnostics; `abg pairs prune` reclaims stranded state.
38
+ - **Thread auto-resume** — bare `abg codex` resumes the pair's last Codex thread; `abg resume` prints/performs the resume commands for both sides.
39
+ - **Budget coordination, slowdown-line & fully-automatic resume** — keep a long task moving across subscription-quota windows instead of dying at a limit. See [Budget Coordination & Auto-Resume](#budget-coordination--auto-resume).
40
+
31
41
  ## Architecture
32
42
 
33
43
  ```
@@ -161,12 +171,13 @@ After modifying AgentBridge source code, re-run `agentbridge dev` to sync change
161
171
  | `abg pairs` | List registered pairs; `abg pairs rm <name\|id>` removes one; `abg pairs prune` previews reclaimable orphan dirs + stranded registry entries (cwd-gone, dead, >1 day), `abg pairs prune --apply` deletes them |
162
172
  | `abg doctor [--json]` | Read-only diagnosis: env, daemon health/readiness, build drift, artifact alignment, TUI attachment, logs |
163
173
  | `abg budget [--json]` | Both agents' subscription quota snapshot (5h/weekly windows, drift, pause state) |
174
+ | `abg logs [--codex] [-f] [-n N]` | Tail this pair's daemon log (or the Codex wrapper log with `--codex`); `-f` follows, `-n N` sets the line count (default 100) |
164
175
  | `abg kill` | Gracefully stop this pair's daemon and managed Codex TUI, write killed sentinel; `abg kill --all` stops every pair |
165
176
  | `abg dev` | (Dev only) Register local marketplace + force-sync plugin to cache |
166
177
  | `abg --help` | Show help |
167
178
  | `abg --version` | Show version |
168
179
 
169
- The pair-aware commands (`claude`, `codex`, `resume`, `kill`, `doctor`, `budget`) accept `--pair <name>` to target a specific pair — one pair per project directory by default, with ports allocated per pair in +10 strides from 4500.
180
+ The pair-aware commands (`claude`, `codex`, `resume`, `kill`, `doctor`, `budget`, `logs`) accept `--pair <name>` to target a specific pair — one pair per project directory by default, with ports allocated per pair in +10 strides from 4500.
170
181
 
171
182
  ### Owned flags
172
183
 
@@ -304,6 +315,22 @@ The bridge can enter several dormant states when it cannot accept new MCP replie
304
315
  | `probe_in_progress` | A liveness probe is currently checking the incumbent — contention window. Transient (auto-recovers within `DISABLED_RECOVERY_INTERVAL_MS` × cap, ~30 s). | None needed; the recovery poller reconnects automatically when the slot clears. |
305
316
  | `auto_recovery_exhausted` | The auto-recovery poller for `probe_in_progress` ran its full retry budget (6 attempts, ~30 s) without succeeding. Terminal. | Retry manually with `agentbridge claude`. |
306
317
 
318
+ ## Budget Coordination & Auto-Resume
319
+
320
+ AgentBridge can keep a long task moving across subscription-quota windows instead of letting it die when one agent hits its limit. This whole capability is driven by the companion **agent-quota-guard** tool: the bridge reads its quota probe and its `pending` records — install the guard to enable it.
321
+
322
+ The daemon's budget coordinator polls **both** agents' account-level 5h/weekly quota (via the guard's probe) and coordinates the two sides; `abg budget [--json]` prints the live snapshot (both windows, drift, pause state). With the guard installed, two further capabilities activate:
323
+
324
+ - **Slowdown-line — no mid-task cut.** Near the quota hard-line the guard does NOT deny mid-tool-call. Instead it surfaces a reminder, lets the current turn finish, then stops cleanly at the turn boundary, writes a `.agent/checkpoint.md`, and drops a `pending` record the bridge detects.
325
+ - **Fully-automatic resume after the window refreshes.** When a paused side's quota window refreshes, the bridge resumes the task **in the original interactive TUI** — no headless background process, no manual step:
326
+ - **Codex** — a queued `turn/start` injection (`ResumeInjectionQueue`) opens a fresh turn that continues from the checkpoint. Fully automatic.
327
+ - **Claude** — a channel push carrying a stable `resume_id`; Claude echoes it back via the `ack_resume` MCP tool. Unacked pushes retry with a fresh delivery id (stable `resume_id`); after retries are exhausted a `SessionStart` degrade-sentinel surfaces a recovery hint the next session reads.
328
+ - **Idempotency** — per-pending claim/consumed tombstones (a sha256 of agent + session + cwd + content hash) ensure a given resume is injected at most once, even across daemon restarts; stale tombstones are TTL-pruned.
329
+
330
+ Coordination directives the bridge may emit while a task runs: **balance** (route more work to the side with more runway / remaining work-time), **underutilized** (the account will not use its weekly quota before reset — split more parallel subtasks / raise delegation density), and **pause / handoff / resume**.
331
+
332
+ > Slowdown-line + auto-resume are an opt-in, companion-guard feature. The Claude-side resume is best-effort (ack + retry + a SessionStart fallback): channel pushes to a fully idle session have known upstream variability, so the bridge only marks a side resumed once it sees a real `ack_resume`.
333
+
307
334
  ## Current Limitations
308
335
 
309
336
  - Only forwards `agentMessage` items, not intermediate `commandExecution`, `fileChange`, or similar events
package/README.zh-CN.md CHANGED
@@ -28,6 +28,16 @@ AgentBridge 采用两层进程结构:
28
28
  - 一个面向任意 Agent 后端的通用编排框架
29
29
  - 一个可以隔离不可信工具的强化安全边界
30
30
 
31
+ ## 功能
32
+
33
+ - **Claude ↔ Codex 双向消息**(同一工作会话):拦截 Codex 输出并以 channel 通知推给 Claude;Claude 用 `reply` MCP tool 回复,作为 `turn/start` 注入 Codex thread。
34
+ - **Push 投递 + 兜底**:消息以 channel 通知投递;推送失败回退到内存队列,由 `get_messages` 排空。靠每条消息的 `source` 字段防循环。
35
+ - **回合协调**:busy-guard 在 Codex 活跃 turn 期间拒绝回复;单 turn 非活动看门狗避免丢失 `turn/completed` 永久锁死注入;折叠噪声中间事件,只把有意义的 `agentMessage` 送达 Claude。
36
+ - **多对并行**:每个项目目录一对 Claude+Codex,端口按 +10 步长从 4500 分配;`claude` / `codex` / `resume` / `kill` / `doctor` / `budget` 支持 `--pair` 指定。
37
+ - **韧性生命周期**:常驻后台 daemon 跨 Claude Code 重启存活(指数退避自动重连);孤儿进程清理;`abg doctor` 只读诊断;`abg pairs prune` 回收滞留状态。
38
+ - **Thread 自动续接**:裸 `abg codex` 续接该对上次的 Codex thread;`abg resume` 打印/执行两侧的续接命令。
39
+ - **额度协调、减速线与全自动续接**:让长任务跨订阅额度窗口持续推进,而不是撞到上限就中断。见 [额度协调与自动续接](#额度协调与自动续接)。
40
+
31
41
  ## 架构
32
42
 
33
43
  ```
@@ -155,21 +165,29 @@ agentbridge codex
155
165
  | 命令 | 说明 |
156
166
  |------|------|
157
167
  | `abg init` | 安装插件、检查依赖(bun/claude/codex)、生成 `.agentbridge/config.json` |
158
- | `abg claude [args...]` | 启动 Claude Code 并启用 push channel。自动清除之前 `kill` 留下的 sentinel。额外参数透传给 `claude` |
159
- | `abg codex [args...]` | 启动连接到 AgentBridge daemon 的 Codex TUI。管理 TUI 进程生命周期(pid 跟踪、清理)。额外参数透传给 `codex` |
160
- | `abg kill` | 优雅停止 daemon 和托管的 Codex TUI,清理状态文件,写入 killed sentinel |
168
+ | `abg claude [args...]` | 启动 Claude Code 并启用 push channel。**默认带 `--dangerously-skip-permissions`**(关闭:`--safe` 或 `AGENTBRIDGE_SAFE=1`)。自动清除上次 `kill` 留下的 sentinel。额外参数透传给 `claude` |
169
+ | `abg codex [args...]` | 启动连接 AgentBridge daemon 的 Codex TUI。**裸 `abg codex` 自动续接该对上次的 thread;`abg codex --new` 开新 thread。TUI 默认带 `--yolo`**(关闭:`--safe` 或 `AGENTBRIDGE_SAFE=1`;`exec` 等非 TUI 子命令不受影响)。管理 TUI 进程生命周期(pid 跟踪、清理)。额外参数透传给 `codex` |
170
+ | `abg resume [claude\|codex]` | 不带目标:打印本目录上次 Claude 会话 + 本对当前 Codex thread 的续接命令。带目标:直接续接该侧 |
171
+ | `abg pairs` | 列出已注册的对;`abg pairs rm <name\|id>` 删除一个;`abg pairs prune` 预览可回收的孤儿目录 + 滞留 registry 条目(cwd 不存在/已死/>1 天),`abg pairs prune --apply` 执行删除 |
172
+ | `abg doctor [--json]` | 只读诊断:环境、daemon 健康/就绪、构建漂移、产物对齐、TUI 连接、日志 |
173
+ | `abg budget [--json]` | 两侧订阅额度快照(5h/周窗口、漂移、暂停态) |
174
+ | `abg logs [--codex] [-f] [-n N]` | tail 本对的 daemon 日志(或加 `--codex` tail Codex wrapper 日志);`-f` 跟随,`-n N` 指定行数(默认 100) |
175
+ | `abg kill` | 优雅停止本对 daemon 和托管的 Codex TUI,写入 killed sentinel;`abg kill --all` 停止所有对 |
161
176
  | `abg dev` | (开发用)注册本地 marketplace + 强制同步插件到缓存 |
162
177
  | `abg --help` | 显示帮助 |
163
178
  | `abg --version` | 显示版本 |
164
179
 
180
+ 成对命令(`claude`、`codex`、`resume`、`kill`、`doctor`、`budget`、`logs`)接受 `--pair <name>` 指定具体的对——默认每个项目目录一对,端口按 +10 步长从 4500 分配。
181
+
165
182
  ### Owned flags
166
183
 
167
184
  部分参数由 CLI 自动注入,不可手动指定:
168
185
 
169
186
  - `agentbridge claude` 拥有:`--channels`、`--dangerously-load-development-channels`
170
187
  - `agentbridge codex` 拥有:`--remote`、`--enable tui_app_server`
188
+ - 两个启动器都消费包装参数 `--safe`(永不透传):它关闭该次启动的最大权限默认值。当你自己显式传任何权限参数时(codex 的 `-a`/`--ask-for-approval`/`-s`/`--sandbox`;claude 的 `--permission-mode`/`--allow-dangerously-skip-permissions`),默认值也会自动抑制——在显式审批策略旁再注入 `--yolo` 会触发 codex CLI 硬冲突。
171
189
 
172
- 手动传入这些参数会报错,并提示使用原生命令。
190
+ 手动传入被拥有的参数会报错,并提示使用原生命令。
173
191
 
174
192
  > **关于 `agentbridge codex` 参数位置的说明:** 对于无子命令的 TUI 形式
175
193
  > (`agentbridge codex …`),bridge 注入的参数放在最前面。对于带有自己参数的
@@ -291,12 +309,28 @@ Bridge 在无法接受新 MCP 回复时会进入若干休眠状态。每种状
291
309
  | `probe_in_progress` | 当前正在对在位会话执行存活探测——争用窗口期。瞬态(在 `DISABLED_RECOVERY_INTERVAL_MS` × 重试上限内自动恢复,约 30 秒)。 | 无需操作;恢复轮询会在槽位释放后自动重连。 |
292
310
  | `auto_recovery_exhausted` | `probe_in_progress` 的自动恢复轮询用尽了完整的重试预算(6 次,约 30 秒)仍未成功。终态。 | 手动用 `agentbridge claude` 重试。 |
293
311
 
312
+ ## 额度协调与自动续接
313
+
314
+ AgentBridge 能让长任务跨订阅额度窗口持续推进,而不是某一侧撞到上限就中断。这套能力由配套的 **agent-quota-guard** 工具驱动:bridge 读它的额度探针和 `pending` 记录——装上 guard 才会启用。
315
+
316
+ daemon 里的额度协调器轮询**两侧**账号级 5h/周额度(经 guard 的探针)并协调两边;`abg budget [--json]` 打印实时快照(两个窗口、漂移、暂停态)。装上 guard 后再激活两项能力:
317
+
318
+ - **减速线——中途不腰斩。** 接近额度硬线时,guard **不**在工具调用中途 deny,而是给一条提醒、让当前 turn 跑完,在**回合边界干净停下**,写 `.agent/checkpoint.md`,并落一条 bridge 能检测的 `pending` 记录。
319
+ - **窗口刷新后全自动续接。** 当被暂停一侧的额度窗口刷新,bridge 在**原本的交互式 TUI** 里续接任务——不开后台无头进程、不需手动:
320
+ - **Codex**:排队的 `turn/start` 注入(`ResumeInjectionQueue`)开一个新 turn,从 checkpoint 接着干。全自动。
321
+ - **Claude**:channel push 一条带稳定 `resume_id` 的指令;Claude 经 `ack_resume` MCP tool 回执。未回执则用新 delivery id 重推(`resume_id` 不变);重试耗尽后落 `SessionStart` 降级 sentinel,下个会话读到恢复提示。
322
+ - **幂等**:每条 pending 一个 claim/consumed 墓碑(agent+session+cwd+内容哈希 的 sha256),保证同一续接最多注入一次,跨 daemon 重启亦然;陈旧墓碑按 TTL 清理。
323
+
324
+ 任务运行中 bridge 可能发的协调指令:**balance**(把更多活分给 runway 更长 / 剩余可工作时间更多的一侧)、**underutilized**(账号周额度在刷新前烧不满时——多拆并行子任务 / 提高委派密度)、**pause / handoff / resume**。
325
+
326
+ > 减速线 + 自动续接是可选的、依赖配套 guard 的能力。Claude 侧续接是 best-effort(ack + 重试 + SessionStart 兜底):对完全空闲会话的 channel push 存在已知的上游不确定性,故 bridge 只有看到真正的 `ack_resume` 才标记该侧已续接。
327
+
294
328
  ## 当前限制
295
329
 
296
330
  - 目前只转发 `agentMessage`,不转发 `commandExecution`、`fileChange` 等中间过程事件
297
- - 当前只支持单个 Codex thread,不支持多会话
298
- - 当前只支持单个 Claude 前台连接;新的 Claude 会话会替换旧连接
299
- - 固定端口意味着每台机器只能运行一个 AgentBridge 实例(多项目并行支持计划在 v1 之后)
331
+ - 每对只有单个 Codex thread,对内暂不支持多会话
332
+ - 每对只有单个 Claude 前台连接;新的 Claude 会话会替换旧连接
333
+ - 多对可在同机并行(每个项目目录一对、按对分配端口);Windows 暂非官方支持平台
300
334
 
301
335
  ### Codex 的 Git 操作限制
302
336
 
package/dist/cli.js CHANGED
@@ -176,7 +176,7 @@ function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env)
176
176
  var require_package = __commonJS((exports, module) => {
177
177
  module.exports = {
178
178
  name: "@raysonmeng/agentbridge",
179
- version: "0.1.16",
179
+ version: "0.1.18",
180
180
  description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
181
181
  type: "module",
182
182
  packageManager: "bun@1.3.11",
@@ -501,6 +501,34 @@ function findShapeViolation(raw) {
501
501
  }
502
502
  }
503
503
  }
504
+ if ("maximize" in budget) {
505
+ const maximize = budget.maximize;
506
+ if (!isRecord(maximize)) {
507
+ return "budget.maximize is present but not an object";
508
+ }
509
+ for (const key of [
510
+ "targetUtil",
511
+ "reserveSlopePctPerHour",
512
+ "reserveMaxPct",
513
+ "finishingHorizonMinutes",
514
+ "resumeHysteresisPct"
515
+ ]) {
516
+ if (key in maximize && !isCoercibleNumber(maximize[key])) {
517
+ return `budget.maximize.${key} is present but not a number`;
518
+ }
519
+ }
520
+ }
521
+ if ("allocation" in budget) {
522
+ const allocation = budget.allocation;
523
+ if (!isRecord(allocation)) {
524
+ return "budget.allocation is present but not an object";
525
+ }
526
+ for (const key of ["minRunwayRatio", "minRunwayGapHours"]) {
527
+ if (key in allocation && !isCoercibleNumber(allocation[key])) {
528
+ return `budget.allocation.${key} is present but not a number`;
529
+ }
530
+ }
531
+ }
504
532
  }
505
533
  return null;
506
534
  }
@@ -508,7 +536,7 @@ function hasCustomDecisionValues(config) {
508
536
  const d = DEFAULT_CONFIG;
509
537
  const b = config.budget;
510
538
  const db = d.budget;
511
- return config.idleShutdownSeconds !== d.idleShutdownSeconds || config.turnCoordination.attentionWindowSeconds !== d.turnCoordination.attentionWindowSeconds || config.codex.appPort !== d.codex.appPort || config.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;
539
+ return config.idleShutdownSeconds !== d.idleShutdownSeconds || config.turnCoordination.attentionWindowSeconds !== d.turnCoordination.attentionWindowSeconds || config.codex.appPort !== d.codex.appPort || config.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;
512
540
  }
513
541
  function normalizeInteger(value, fallback) {
514
542
  if (typeof value === "number" && Number.isFinite(value))
@@ -526,8 +554,41 @@ function normalizeBoundedInteger(value, fallback, min, max) {
526
554
  return fallback;
527
555
  return parsed;
528
556
  }
529
- function normalizeStrategy(value, fallback) {
530
- return value === "conserve" || value === "maximize" ? value : fallback;
557
+ function normalizeBoundedNumber(value, fallback, min, max) {
558
+ let parsed;
559
+ if (typeof value === "number") {
560
+ parsed = value;
561
+ } else if (typeof value === "string" && value.trim() !== "") {
562
+ parsed = Number(value);
563
+ } else {
564
+ return fallback;
565
+ }
566
+ if (!Number.isFinite(parsed))
567
+ return fallback;
568
+ if (parsed < min || parsed > max)
569
+ return fallback;
570
+ return parsed;
571
+ }
572
+ function normalizeMaximizeConfig(raw, pauseAt, fallback = DEFAULT_BUDGET_CONFIG.maximize) {
573
+ const m = isRecord(raw) ? raw : {};
574
+ const normalized = {
575
+ targetUtil: normalizeBoundedInteger(m.targetUtil, fallback.targetUtil, 90, 99),
576
+ reserveSlopePctPerHour: normalizeBoundedNumber(m.reserveSlopePctPerHour, fallback.reserveSlopePctPerHour, 0, 5),
577
+ reserveMaxPct: normalizeBoundedInteger(m.reserveMaxPct, fallback.reserveMaxPct, 0, 30),
578
+ finishingHorizonMinutes: normalizeBoundedInteger(m.finishingHorizonMinutes, fallback.finishingHorizonMinutes, 5, 180),
579
+ resumeHysteresisPct: normalizeBoundedInteger(m.resumeHysteresisPct, fallback.resumeHysteresisPct, 1, 30)
580
+ };
581
+ if (normalized.targetUtil <= pauseAt) {
582
+ return { ...DEFAULT_BUDGET_CONFIG.maximize };
583
+ }
584
+ return normalized;
585
+ }
586
+ function normalizeAllocationConfig(raw, fallback = DEFAULT_BUDGET_CONFIG.allocation) {
587
+ const a = isRecord(raw) ? raw : {};
588
+ return {
589
+ minRunwayRatio: normalizeBoundedInteger(a.minRunwayRatio, fallback.minRunwayRatio, 10, 100),
590
+ minRunwayGapHours: normalizeBoundedInteger(a.minRunwayGapHours, fallback.minRunwayGapHours, 1, 168)
591
+ };
531
592
  }
532
593
  function normalizeBoolean(value, fallback) {
533
594
  if (typeof value === "boolean")
@@ -578,7 +639,8 @@ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
578
639
  },
579
640
  codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
580
641
  codexTiers,
581
- strategy: normalizeStrategy(budget.strategy, fallback.strategy)
642
+ maximize: normalizeMaximizeConfig(budget.maximize, pauseAt, fallback.maximize),
643
+ allocation: normalizeAllocationConfig(budget.allocation, fallback.allocation)
582
644
  };
583
645
  }
584
646
  function applyBudgetEnvOverrides(budget, env = process.env) {
@@ -594,7 +656,17 @@ function applyBudgetEnvOverrides(budget, env = process.env) {
594
656
  },
595
657
  codexTierControl: env.AGENTBRIDGE_BUDGET_CODEX_TIER_CONTROL ?? budget.codexTierControl,
596
658
  codexTiers: budget.codexTiers,
597
- strategy: env.AGENTBRIDGE_BUDGET_STRATEGY ?? budget.strategy
659
+ maximize: {
660
+ targetUtil: env.AGENTBRIDGE_BUDGET_TARGET_UTIL ?? budget.maximize.targetUtil,
661
+ reserveSlopePctPerHour: env.AGENTBRIDGE_BUDGET_RESERVE_SLOPE_PCT_PER_HOUR ?? budget.maximize.reserveSlopePctPerHour,
662
+ reserveMaxPct: env.AGENTBRIDGE_BUDGET_RESERVE_MAX_PCT ?? budget.maximize.reserveMaxPct,
663
+ finishingHorizonMinutes: env.AGENTBRIDGE_BUDGET_FINISHING_HORIZON_MINUTES ?? budget.maximize.finishingHorizonMinutes,
664
+ resumeHysteresisPct: env.AGENTBRIDGE_BUDGET_RESUME_HYSTERESIS_PCT ?? budget.maximize.resumeHysteresisPct
665
+ },
666
+ allocation: {
667
+ minRunwayRatio: env.AGENTBRIDGE_BUDGET_MIN_RUNWAY_RATIO ?? budget.allocation.minRunwayRatio,
668
+ minRunwayGapHours: env.AGENTBRIDGE_BUDGET_MIN_RUNWAY_GAP_HOURS ?? budget.allocation.minRunwayGapHours
669
+ }
598
670
  };
599
671
  return normalizeBudgetConfig(overlay, budget);
600
672
  }
@@ -725,7 +797,17 @@ var init_config_service = __esm(() => {
725
797
  balanced: { effort: "medium" },
726
798
  eco: { effort: "low" }
727
799
  },
728
- strategy: "conserve"
800
+ maximize: {
801
+ targetUtil: 98,
802
+ reserveSlopePctPerHour: 0.4,
803
+ reserveMaxPct: 7,
804
+ finishingHorizonMinutes: 30,
805
+ resumeHysteresisPct: 5
806
+ },
807
+ allocation: {
808
+ minRunwayRatio: 50,
809
+ minRunwayGapHours: 2
810
+ }
729
811
  };
730
812
  DEFAULT_CONFIG = {
731
813
  version: "1.0",
@@ -822,7 +904,16 @@ ${endMarker}`;
822
904
  var MARKER_START = (id) => `<!-- ${id}:start -->`, MARKER_END = (id) => `<!-- ${id}:end -->`;
823
905
 
824
906
  // src/collaboration-content.ts
825
- var MARKER_ID = "AgentBridge", CLAUDE_MD_SECTION = `## AgentBridge \u2014 Multi-Agent Collaboration
907
+ var MARKER_ID = "AgentBridge", BUDGET_PACING = `### Budget pacing \u2014 drive the WEEKLY quota to ~100% over the week, evenly, without reaching a 5h cap (active when agent-quota-guard is installed)
908
+ - **Core principle: token is the means, value is the end.** Raising intensity means producing more real parallel value (deeper reviews, more independent exploration / verification / genuine subtasks) \u2014 never manufacturing low-value work to consume quota. The budget to MAXIMIZE is the **weekly** quota (refreshed once a week): drive each side's weekly toward ~100% by its weekly reset, and consume it **evenly** across the week \u2014 front-loading then starving, or under-consuming throughout, both leave weekly quota unredeemed (forfeited). The **5h window is NOT a quota bucket to fill \u2014 it is a RATE CAP**: stay under it within any 5h period; reaching it = a forced pause until the 5h resets = wasted time, not progress.
909
+ - **Re-query your budget before EVERY allocation decision** \u2014 Claude: \`get_budget\` \u2192 **rendered text** covering both sides; Codex: \`check_budget\` with \`agent:"claude"|"codex"\` \u2192 **normalized JSON**, per side. (Two different shapes \u2014 read the right one below.) Never reuse remembered numbers: a weekly window can refresh EARLY (resetting both 5h and weekly), fully restoring a side you believed was exhausted.
910
+ - **Even-pacing test (per side \u2014 Claude runs it)** \u2014 compare two quantities: *budget-windows* = how many 5h windows the weekly quota still covers at the current burn rate; *clock-windows* = how many 5h windows physically fit before the weekly reset = (weekly reset \u2212 now) \xF7 5h. **Claude** (\`get_budget\` text) carries BOTH, pre-computed for BOTH sides: the lines "\u6309\u5F53\u524D\u8282\u594F\uFF0C\u5468\u989D\u5EA6\u8FD8\u591F \u2026 \u4E2A 5h \u7A97\u53E3" (budget-windows) and "\u8DDD\u5468\u5237\u65B0\u8FD8\u80FD\u5BB9\u7EB3 \u2026 \u4E2A 5h \u7A97\u53E3\uFF08\u65F6\u949F\uFF09" (clock-windows). **Codex** (\`check_budget\` JSON) today carries only per-bucket \`util\` / \`reset_epoch\` / \`reset_after_seconds\` \u2014 no burn rate, no \`five_hour_windows_left\` \u2014 so Codex CANNOT compute budget-windows itself; it reads its weekly \`util\` and clock-windows only. To locate Codex's weekly bucket: of the \`buckets[]\` entries whose \`id\` contains \`seven_day\` or \`secondary_window\` (there can be several \u2014 e.g. a model-specific \`additional_rate_limits[\u2026]\` one at 0%), take the HIGHEST-util one (the binding account-level window, matching how the bridge parses it); its clock-windows = \`reset_after_seconds\` \xF7 5h (never the top-level \`reset_epoch\`, which tracks the current limiter, not necessarily the weekly window). For the budget-windows half and the raise/hold/reduce verdict, Codex relies on Claude's \`get_budget\` (the burn projection lives there, for both sides) and reports its own weekly \`util\` + reset timing so Claude can run the test. (If a future \`check_budget\` exposes \`five_hour_windows_left\` on the weekly bucket, Codex reads it directly.) **The verdict (Claude computes it, per side):** budget > clock \u2192 **under-consuming** (weekly will be left unused) \u2192 **raise intensity**; budget < clock \u2192 **over-consuming** (won't last to the weekly reset) \u2192 **reduce intensity**; within ~1 window, or no confident rate \u2192 **hold**. **Codex, absent a fresh Claude verdict, holds at its current intensity (it never escalates unilaterally) and stays clear of the 5h cap \u2014 surfacing its weekly \`util\` + reset timing so Claude can issue the verdict.**
911
+ - **Raise intensity \u2014 use the levers your role has.** Orchestrator (Claude): pick larger, more-decomposable tasks; run more parallel subagents at once (3\u20135+ vs 1); raise delegation density; open more concurrent streams (review + explore + verify in parallel). Executor (Codex): go deeper in-turn, take larger chunks, run more verification/repro. Both: deepen quality (multi-angle review, broader test/repro) \u2014 never manufacture make-work. **Reduce intensity:** fewer/serial subagents (Claude), short bounded chunks, defer optional deep work. Stay below the **\u52A8\u6001\u6682\u505C\u7EBF** (shown in \`get_budget\`; its \`\u4F59\u91CF\` = headroom from your current util to that soft line, measured on the resettable hard-winner window \u2014 the 5h OR the weekly window, whichever currently limits you) \u2014 that soft ceiling, not the raw 5h cap, is the "do not cross, avoid a forced pause" line. **If that line is absent, or you only have JSON (Codex),** fall back to the 5h bucket's raw util vs 100% (Codex: of the \`buckets[]\` entries whose \`id\` contains \`five_hour\` or \`primary_window\`, take the HIGHEST-util one) and keep clear of the 5h cap.
912
+ - **Distinguish 5h from weekly:** a 5h window resetting does NOT consume or waste weekly budget \u2014 it only refreshes your rate headroom, so you can keep going when weekly is under-consumed. A near 5h reset is therefore not urgency but the release of a rate limit. The real "unused = forfeited" is the **weekly budget as its WEEKLY reset nears**: if weekly is still under-consumed then, raise intensity (within the 5h cap) to use it. If even pacing needs a rate beyond one 5h window's capacity, you are rate-limited \u2192 keep each 5h window as full as possible (under the cap).
913
+ - **Two-subscription imbalance \u2014 the quotas are INDEPENDENT and differ in BOTH amount AND reset timing** (each side's weekly and 5h windows reset on different clocks). **The cross-side split is the orchestrator's (Claude) decision:** route more work to the side that is MORE under-consuming on the even-pacing test (the larger budget-windows \u2212 clock-windows gap); when EITHER side lacks a confident rate (so the gap can't be compared), fall back to the more budget-rich side (larger absolute weekly headroom). On any tie (equal gap, or equal headroom), prefer the side whose **weekly resets SOONER** (its leftover is forfeited earlier). **As the executor (Codex) you do NOT decide the global split** \u2014 execute what you're assigned, and when your own budget is rich report it (with evidence) so Claude routes more to you. The tighter / over-consuming side carries less.
914
+ - **Side-aware pause (the hard floor the code enforces \u2014 obey, do not reinvent), with each side's own action:** **Codex exhausted** (\`system_budget_pause\`) \u2192 Codex's turns stop (gate closed); **Claude** must not retry replies and continues solo on independent work, checkpointing the split point \u2014 but the SAME \`system_budget_pause\` is ALSO emitted when both sides are exhausted, so do not infer "solo" from the directive name alone: read its content (it names the paused side[s]) or re-check \`get_budget\`, and continue solo ONLY while Claude's own side is healthy; if Claude is also at its line, handle it as **Both** below. **Claude exhausted** (\`system_budget_handoff\`) \u2192 **Claude** sends ONE handoff (remaining tasks / context / artifact locations / acceptance criteria) then stops; **Codex** receives the baton and carries the work forward as far as its remaining quota allows that turn. **Both** \u2192 joint pause; checkpoint and wait for \`resume\` (Claude's own quota-guard also hard-stops Claude independently). A transient probe **429 is NOT exhaustion** \u2192 fall back to cached util and keep working.`, CLAUDE_MD_SECTION, AGENTS_MD_SECTION;
915
+ var init_collaboration_content = __esm(() => {
916
+ CLAUDE_MD_SECTION = `## AgentBridge \u2014 Multi-Agent Collaboration
826
917
 
827
918
  You are working in a **multi-agent environment** powered by AgentBridge.
828
919
  Another AI agent (Codex, by OpenAI) is available in a parallel session on this machine.
@@ -853,14 +944,8 @@ Another AI agent (Codex, by OpenAI) is available in a parallel session on this m
853
944
  3. Ask for Codex's agreement or counter-proposal before proceeding.
854
945
  4. After task completion, **cross-review** each other's work.
855
946
 
856
- ### Budget awareness (active when agent-quota-guard is installed)
857
- - Goal: **keep the task moving while fully using the subscription quota**. The bridge polls both agents' account-level 5h/weekly windows and may send \`system_budget_*\` notices: **balance** (route more work to the lighter side), **parallel** (quota surplus near the 5h reset \u2014 split more parallel subtasks), **pause/handoff/resume**.
858
- - \`get_budget\` shows BOTH sides' quota \u2014 re-check it **before every task-allocation decision**. NEVER rely on quota numbers remembered from earlier in the conversation: the weekly window can refresh EARLY (resetting both 5h and weekly), so a side you remember as nearly exhausted may be fully restored.
859
- - Side-aware pause semantics:
860
- - **Codex exhausted** (\`system_budget_pause\`): the reply gate closes. Do not retry replies; continue solo on independent work, note the split point in a checkpoint.
861
- - **You (Claude) exhausted** (\`system_budget_handoff\`): the gate stays OPEN \u2014 immediately send ONE handoff reply to Codex packaging the remaining task list, context, artifact locations and acceptance criteria, then stop working (your own quota-guard will hard-stop you at 92%). Codex relays the baton.
862
- - **Both exhausted**: joint pause; checkpoint and wait for the resume notice.
863
- - Save quota with model tiers: route mechanical subagent work to **haiku**, routine work to **sonnet**, reserve **opus** for architecture decisions; when your side is the heavier consumer, delegate more to Codex.`, AGENTS_MD_SECTION = `## AgentBridge \u2014 Multi-Agent Collaboration
947
+ ${BUDGET_PACING}`;
948
+ AGENTS_MD_SECTION = `## AgentBridge \u2014 Multi-Agent Collaboration
864
949
 
865
950
  You are working in a **multi-agent environment** powered by AgentBridge.
866
951
  Another AI agent (Claude, by Anthropic) is available in a parallel session on this machine.
@@ -913,12 +998,8 @@ You MUST NOT run git **write** commands: \`commit\`, \`push\`, \`pull\`, \`fetch
913
998
  - Do not blindly follow Claude \u2014 challenge with evidence when you disagree.
914
999
  - Use explicit collaboration phrases: "My independent view is:", "I agree on:", "I disagree on:", "Current consensus:".
915
1000
 
916
- ### Budget awareness (active when agent-quota-guard is installed)
917
- - Goal: **keep the task moving while fully using the subscription quota**. You can check BOTH sides' quota yourself via your quota-guard MCP tool \`check_budget\` with \`agent: "claude"\` or \`"codex"\` \u2014 re-check **before negotiating task splits**, and NEVER rely on remembered numbers: the weekly window can refresh early (resetting both 5h and weekly windows).
918
- - During a **budget pause** (your side exhausted) you simply stop receiving new turns \u2014 that IS the pause. Your own quota-guard hooks still apply; work resumes when Claude's next message arrives.
919
- - **Handoff (Claude's side exhausted)**: you may receive a baton message packaging the remaining work. Push as far as possible within that single turn; write leftovers to a checkpoint file; do NOT expect Claude to respond until its quota refreshes.
920
- - Claude may route more or less work to you based on quota drift \u2014 expected load balancing, not preference.
921
- - When the user enabled tier control, the bridge may adjust your model/reasoning-effort via turn parameters under budget pressure; if asked to economize, prefer lower effort and concise outputs.`;
1001
+ ${BUDGET_PACING}`;
1002
+ });
922
1003
 
923
1004
  // src/cli/init.ts
924
1005
  var exports_init = {};
@@ -1118,6 +1199,7 @@ var init_init = __esm(() => {
1118
1199
  init_pkg_root();
1119
1200
  init_plugin_cache();
1120
1201
  init_version_utils();
1202
+ init_collaboration_content();
1121
1203
  });
1122
1204
 
1123
1205
  // src/cli/dev.ts
@@ -1470,6 +1552,9 @@ var init_daemon_client = __esm(() => {
1470
1552
  });
1471
1553
  return pending;
1472
1554
  }
1555
+ sendAckResume(resumeId, status) {
1556
+ this.send({ type: "ack_resume", resumeId, status });
1557
+ }
1473
1558
  attachSocketHandlers(ws, socketId) {
1474
1559
  ws.onmessage = (event) => {
1475
1560
  const raw = typeof event.data === "string" ? event.data : event.data.toString();
@@ -1585,11 +1670,11 @@ function formatBuildInfo(build) {
1585
1670
  var CODE_HASH_SENTINEL = "source", BUILD_INFO;
1586
1671
  var init_build_info = __esm(() => {
1587
1672
  BUILD_INFO = Object.freeze({
1588
- version: defineString("0.1.16", "0.0.0-source"),
1589
- commit: defineString("1dc5e4f", "source"),
1673
+ version: defineString("0.1.18", "0.0.0-source"),
1674
+ commit: defineString("9db0aa3", "source"),
1590
1675
  bundle: defineBundle("dist"),
1591
1676
  contractVersion: defineNumber(1, CONTRACT_VERSION),
1592
- codeHash: defineString("1fc975838e46", "source")
1677
+ codeHash: defineString("46a6407023f0", "source")
1593
1678
  });
1594
1679
  });
1595
1680
 
@@ -3499,7 +3584,9 @@ function isFreshAgentsMdContract(content) {
3499
3584
  return false;
3500
3585
  return content.includes("transparent proxy") && content.includes("Do not") && content.includes("sendToClaude") && content.includes("Git operations") && content.includes("Implementer, Executor, Verifier");
3501
3586
  }
3502
- var init_agents_contract = () => {};
3587
+ var init_agents_contract = __esm(() => {
3588
+ init_collaboration_content();
3589
+ });
3503
3590
 
3504
3591
  // src/wrapper-exit-observability.ts
3505
3592
  import { readFileSync as readFileSync8, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
@@ -5198,7 +5285,7 @@ var init_pairs = __esm(() => {
5198
5285
  // src/budget/types.ts
5199
5286
  var STALE_MAX_AGE_SEC = 600;
5200
5287
 
5201
- // src/budget/budget-state.ts
5288
+ // src/budget/budget-decision.ts
5202
5289
  function isDecisionGrade(usage, now) {
5203
5290
  if (!usage)
5204
5291
  return false;
@@ -5209,7 +5296,16 @@ function isDecisionGrade(usage, now) {
5209
5296
  return false;
5210
5297
  return true;
5211
5298
  }
5212
- var init_budget_state = () => {};
5299
+ var MAX_TIME_TO_RESET_HOURS;
5300
+ var init_budget_decision = __esm(() => {
5301
+ MAX_TIME_TO_RESET_HOURS = 7 * 24;
5302
+ });
5303
+
5304
+ // src/budget/budget-state.ts
5305
+ var init_budget_state = __esm(() => {
5306
+ init_budget_decision();
5307
+ init_budget_decision();
5308
+ });
5213
5309
 
5214
5310
  // src/budget/burn-view.ts
5215
5311
  function agentWeeklyFiveHourWindowsLeft(usage, now) {
@@ -5337,10 +5433,51 @@ function formatFiveHourWindowsLeftLine(snapshot) {
5337
5433
  const byAgent = values.map(([name, value]) => `${name} ~${value.toFixed(1)}`).join(" / ");
5338
5434
  return `\u6309\u5F53\u524D\u8282\u594F\uFF0C\u5468\u989D\u5EA6\u8FD8\u591F ${byAgent} \u4E2A 5h \u7A97\u53E3`;
5339
5435
  }
5436
+ function clockWindowsLeft(usage, snapshotAt) {
5437
+ const weekly = usage?.weekly;
5438
+ if (!weekly || weekly.resetEpoch <= snapshotAt)
5439
+ return null;
5440
+ return (weekly.resetEpoch - snapshotAt) / FIVE_HOUR_WINDOW_SEC;
5441
+ }
5442
+ function formatClockWindowsLine(snapshot) {
5443
+ const values = [];
5444
+ const claude = clockWindowsLeft(snapshot.claude, snapshot.updatedAt);
5445
+ const codex = clockWindowsLeft(snapshot.codex, snapshot.updatedAt);
5446
+ if (claude !== null)
5447
+ values.push(["Claude", claude]);
5448
+ if (codex !== null)
5449
+ values.push(["Codex", codex]);
5450
+ if (values.length === 0)
5451
+ return null;
5452
+ const unique = [...new Set(values.map(([, value]) => value.toFixed(1)))];
5453
+ if (unique.length === 1)
5454
+ return `\u8DDD\u5468\u5237\u65B0\u8FD8\u80FD\u5BB9\u7EB3 ~${unique[0]} \u4E2A 5h \u7A97\u53E3\uFF08\u65F6\u949F\uFF09`;
5455
+ const byAgent = values.map(([name, value]) => `${name} ~${value.toFixed(1)}`).join(" / ");
5456
+ return `\u8DDD\u5468\u5237\u65B0\u8FD8\u80FD\u5BB9\u7EB3 ${byAgent} \u4E2A 5h \u7A97\u53E3\uFF08\u65F6\u949F\uFF09`;
5457
+ }
5458
+ function formatDynamicLineLine(snapshot) {
5459
+ const lines = snapshot.dynamicPauseLine;
5460
+ if (!lines)
5461
+ return null;
5462
+ const parts = [];
5463
+ const entries = [
5464
+ ["Claude", lines.claude, snapshot.claude],
5465
+ ["Codex", lines.codex, snapshot.codex]
5466
+ ];
5467
+ for (const [name, line, usage] of entries) {
5468
+ if (line === null)
5469
+ continue;
5470
+ const headroom = usage ? `\uFF08util ${usage.gateUtil}%\uFF0C\u4F59\u91CF ${(line - usage.gateUtil).toFixed(1)}\uFF09` : "";
5471
+ parts.push(`${name} ${line.toFixed(1)}%${headroom}`);
5472
+ }
5473
+ if (parts.length === 0)
5474
+ return null;
5475
+ return `\u52A8\u6001\u6682\u505C\u7EBF\uFF1A${parts.join(" \xB7 ")}`;
5476
+ }
5340
5477
  function renderBudgetSnapshot(snapshot, options = {}) {
5341
5478
  const guardHardPct = options.guardHardPct ?? resolveGuardHardHint();
5342
5479
  const lines = [];
5343
- 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)}`);
5480
+ 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)}`);
5344
5481
  lines.push(formatAgent("Claude", snapshot.claude, snapshot.updatedAt));
5345
5482
  lines.push(formatAgent("Codex", snapshot.codex, snapshot.updatedAt));
5346
5483
  if (snapshot.burnRate) {
@@ -5354,6 +5491,12 @@ function renderBudgetSnapshot(snapshot, options = {}) {
5354
5491
  const fiveHourWindowsLeftLine = formatFiveHourWindowsLeftLine(snapshot);
5355
5492
  if (fiveHourWindowsLeftLine)
5356
5493
  lines.push(fiveHourWindowsLeftLine);
5494
+ const clockWindowsLine = formatClockWindowsLine(snapshot);
5495
+ if (clockWindowsLine)
5496
+ lines.push(clockWindowsLine);
5497
+ const dynamicLineLine = formatDynamicLineLine(snapshot);
5498
+ if (dynamicLineLine)
5499
+ lines.push(dynamicLineLine);
5357
5500
  if (snapshot.claude && snapshot.codex) {
5358
5501
  const abs = Math.abs(snapshot.driftPct);
5359
5502
  if (abs > 0) {
@@ -5390,17 +5533,19 @@ function renderBudgetSnapshot(snapshot, options = {}) {
5390
5533
  return lines.join(`
5391
5534
  `);
5392
5535
  }
5393
- var DEFAULT_GUARD_HARD_PCT = 92, WINDOW_LABELS, RESET_TRUNCATION_EPSILON_SEC = 60, PHASE_LABELS, BUDGET_UNAVAILABLE_TEXT = "\u9884\u7B97\u611F\u77E5\u4E0D\u53EF\u7528\uFF1A\u672A\u68C0\u6D4B\u5230 agent-quota-guard \u63A2\u9488\uFF08~/.budget-guard/bin/budget-probe\uFF09\u6216 budget \u529F\u80FD\u5DF2\u7981\u7528\u3002\u534F\u4F5C\u4E0D\u53D7\u5F71\u54CD\u3002";
5536
+ var DEFAULT_GUARD_HARD_PCT = 99, WINDOW_LABELS, RESET_TRUNCATION_EPSILON_SEC = 60, FIVE_HOUR_WINDOW_SEC, PHASE_LABELS, BUDGET_UNAVAILABLE_TEXT = "\u9884\u7B97\u611F\u77E5\u4E0D\u53EF\u7528\uFF1A\u672A\u68C0\u6D4B\u5230 agent-quota-guard \u63A2\u9488\uFF08~/.budget-guard/bin/budget-probe\uFF09\u6216 budget \u529F\u80FD\u5DF2\u7981\u7528\u3002\u534F\u4F5C\u4E0D\u53D7\u5F71\u54CD\u3002";
5394
5537
  var init_render = __esm(() => {
5395
5538
  init_burn_view();
5396
5539
  WINDOW_LABELS = {
5397
5540
  fiveHour: "5h \u7A97\u53E3",
5398
5541
  weekly: "\u5468\u7A97\u53E3"
5399
5542
  };
5543
+ FIVE_HOUR_WINDOW_SEC = 5 * 3600;
5400
5544
  PHASE_LABELS = {
5401
5545
  normal: "normal\uFF08\u6B63\u5E38\uFF09",
5402
5546
  balance: "balance\uFF08\u9700\u5747\u8861\uFF09",
5403
- parallel: "parallel\uFF08\u5EFA\u8BAE\u5E76\u884C\u63D0\u901F\uFF09",
5547
+ parallel: "parallel\uFF08\u5DF2\u9000\u5F79\uFF09",
5548
+ underutilized: "underutilized\uFF08\u989D\u5EA6\u6B20\u8F7D\uFF0C\u5EFA\u8BAE\u63D0\u901F\uFF09",
5404
5549
  paused: "paused\uFF08\u9884\u7B97\u5E72\u9884\u4E2D\uFF09"
5405
5550
  };
5406
5551
  });
@@ -5918,32 +6063,25 @@ function configParseabilityCheck(cwd, cli) {
5918
6063
  detail: desc.customValues ? `parsed at ${desc.path} \u2014 custom values in effect` : `parsed at ${desc.path} \u2014 all values match defaults`
5919
6064
  };
5920
6065
  }
5921
- function evaluateBudgetStrategyGuard(strategy, guardHardPct, targetUtilPct = V3_DEFAULT_TARGET_UTIL) {
5922
- if (strategy !== "maximize") {
5923
- return {
5924
- name: "budget strategy",
5925
- status: "ok",
5926
- detail: "strategy=conserve \u2014 v2-equivalent budget behavior (v3 maximize is opt-in)"
5927
- };
5928
- }
6066
+ function evaluateBudgetStrategyGuard(guardHardPct, targetUtilPct = V3_DEFAULT_TARGET_UTIL) {
5929
6067
  if (guardHardPct >= targetUtilPct) {
5930
6068
  return {
5931
- name: "budget strategy",
6069
+ name: "budget target",
5932
6070
  status: "ok",
5933
- detail: `strategy=maximize \u2014 outer guard hard line ${guardHardPct}% covers targetUtil ${targetUtilPct}%`
6071
+ detail: `dynamic line \u2192 targetUtil ${targetUtilPct}%; outer guard hard line ${guardHardPct}% covers it`
5934
6072
  };
5935
6073
  }
5936
6074
  return {
5937
- name: "budget strategy",
6075
+ name: "budget target",
5938
6076
  status: "warn",
5939
- detail: `strategy=maximize but the outer quota-guard hard line (${guardHardPct}%) is below ` + `targetUtil (${targetUtilPct}%) \u2014 the ${guardHardPct}\u2192${targetUtilPct} band is unreachable for Claude`,
5940
- hint: "v3 \u4E0D\u53EF\u8D8A\u8FC7\u5916\u5C42 quota-guard \u786C\u7EBF\uFF1AClaude \u4FA7\u8FBE\u5230 guard \u786C\u7EBF\u65F6\u8FDB\u7A0B\u4F1A\u88AB\u5916\u5C42\u5F3A\u505C\uFF0C" + `maximize \u7684 ${guardHardPct}%\u2192${targetUtilPct}% \u533A\u95F4\u5B9E\u9645\u70E7\u4E0D\u5230\u3002\u60F3\u771F\u6B63\u7528\u5230 targetUtil\uFF0C` + "\u9700\u81EA\u884C\u8C03\u9AD8 quota-guard \u7684 BUDGET_HARD\uFF08\u672C\u4ED3\u5E93\u4E0D\u4EE3\u6539\u5916\u5C42\u914D\u7F6E\uFF09\uFF1B\u5C55\u793A\u4FA7\u5DF2\u6309 guard \u7EBF\u6536\u53E3\u3002"
6077
+ detail: `outer quota-guard hard line (${guardHardPct}%) is below targetUtil (${targetUtilPct}%) \u2014 ` + `the ${guardHardPct}\u2192${targetUtilPct}% band is unreachable for Claude`,
6078
+ hint: "\u5916\u5C42 quota-guard \u786C\u7EBF\u5148\u4E8E bridge \u52A8\u6001\u7EBF\u505C Claude \u8FDB\u7A0B\uFF08REAL-3\uFF0C\u4E0D\u53EF\u8D8A\u8FC7\uFF09\uFF1A" + `${guardHardPct}%\u2192${targetUtilPct}% \u533A\u95F4\u70E7\u4E0D\u5230\u3002\u60F3\u7528\u6EE1\u5230 targetUtil\uFF0C\u8C03\u9AD8 quota-guard \u7684 ` + "BUDGET_HARD\uFF08\u672C\u4ED3\u5E93\u4E0D\u4EE3\u6539\u5916\u5C42\u914D\u7F6E\uFF09\uFF1B\u5C55\u793A\u4FA7\u5DF2\u6309 guard \u7EBF\u6536\u53E3\u3002"
5941
6079
  };
5942
6080
  }
5943
6081
  function budgetStrategyGuardCheck(cwd) {
5944
6082
  const config = new ConfigService(cwd).loadOrDefault();
5945
6083
  const budget = applyBudgetEnvOverrides(config.budget);
5946
- return evaluateBudgetStrategyGuard(budget.strategy, resolveGuardHardHint());
6084
+ return evaluateBudgetStrategyGuard(resolveGuardHardHint(), budget.maximize.targetUtil);
5947
6085
  }
5948
6086
  function logCheck(name, path, cli) {
5949
6087
  if (!existsSync15(path)) {
@@ -5994,7 +6132,7 @@ function printDoctorReport(report) {
5994
6132
  console.log(line);
5995
6133
  }
5996
6134
  }
5997
- var LARGE_LOG_WARN_BYTES, V3_DEFAULT_TARGET_UTIL = 97;
6135
+ var LARGE_LOG_WARN_BYTES, V3_DEFAULT_TARGET_UTIL;
5998
6136
  var init_doctor = __esm(() => {
5999
6137
  init_plugin_cache();
6000
6138
  init_build_info();
@@ -6007,6 +6145,7 @@ var init_doctor = __esm(() => {
6007
6145
  init_resume_pollution();
6008
6146
  init_process_lifecycle();
6009
6147
  LARGE_LOG_WARN_BYTES = 100 * 1024 * 1024;
6148
+ V3_DEFAULT_TARGET_UTIL = DEFAULT_BUDGET_CONFIG.maximize.targetUtil;
6010
6149
  });
6011
6150
 
6012
6151
  // src/cli/budget.ts