@raysonmeng/agentbridge 0.1.15 → 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.
@@ -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.15",
15
+ "version": "0.1.17",
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.15",
179
+ version: "0.1.17",
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",
@@ -854,7 +936,7 @@ Another AI agent (Codex, by OpenAI) is available in a parallel session on this m
854
936
  4. After task completion, **cross-review** each other's work.
855
937
 
856
938
  ### 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**.
939
+ - 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 side with more runway / remaining work-time), **underutilized** (the account won't use its weekly quota before reset \u2014 split more parallel subtasks / raise delegation density), **pause/handoff/resume**.
858
940
  - \`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
941
  - Side-aware pause semantics:
860
942
  - **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.
@@ -917,7 +999,7 @@ You MUST NOT run git **write** commands: \`commit\`, \`push\`, \`pull\`, \`fetch
917
999
  - 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
1000
  - 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
1001
  - **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.
1002
+ - Claude may route more or less work to you based on **remaining work-time (runway), not just raw usage %** \u2014 a side that looks heavily used but is close to a window reset still has plenty of runway. Expected load balancing, not preference. Claude may also ask for more parallel subtasks when the account will not use its weekly quota before reset (underutilization).
921
1003
  - 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.`;
922
1004
 
923
1005
  // src/cli/init.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.15", "0.0.0-source"),
1589
- commit: defineString("89d4c9a", "source"),
1673
+ version: defineString("0.1.17", "0.0.0-source"),
1674
+ commit: defineString("0d1e1bd", "source"),
1590
1675
  bundle: defineBundle("dist"),
1591
1676
  contractVersion: defineNumber(1, CONTRACT_VERSION),
1592
- codeHash: defineString("71ce81854ec9", "source")
1677
+ codeHash: defineString("c22387f3269f", "source")
1593
1678
  });
1594
1679
  });
1595
1680
 
@@ -5198,7 +5283,7 @@ var init_pairs = __esm(() => {
5198
5283
  // src/budget/types.ts
5199
5284
  var STALE_MAX_AGE_SEC = 600;
5200
5285
 
5201
- // src/budget/budget-state.ts
5286
+ // src/budget/budget-decision.ts
5202
5287
  function isDecisionGrade(usage, now) {
5203
5288
  if (!usage)
5204
5289
  return false;
@@ -5209,7 +5294,16 @@ function isDecisionGrade(usage, now) {
5209
5294
  return false;
5210
5295
  return true;
5211
5296
  }
5212
- var init_budget_state = () => {};
5297
+ var MAX_TIME_TO_RESET_HOURS;
5298
+ var init_budget_decision = __esm(() => {
5299
+ MAX_TIME_TO_RESET_HOURS = 7 * 24;
5300
+ });
5301
+
5302
+ // src/budget/budget-state.ts
5303
+ var init_budget_state = __esm(() => {
5304
+ init_budget_decision();
5305
+ init_budget_decision();
5306
+ });
5213
5307
 
5214
5308
  // src/budget/burn-view.ts
5215
5309
  function agentWeeklyFiveHourWindowsLeft(usage, now) {
@@ -5337,10 +5431,29 @@ function formatFiveHourWindowsLeftLine(snapshot) {
5337
5431
  const byAgent = values.map(([name, value]) => `${name} ~${value.toFixed(1)}`).join(" / ");
5338
5432
  return `\u6309\u5F53\u524D\u8282\u594F\uFF0C\u5468\u989D\u5EA6\u8FD8\u591F ${byAgent} \u4E2A 5h \u7A97\u53E3`;
5339
5433
  }
5434
+ function formatDynamicLineLine(snapshot) {
5435
+ const lines = snapshot.dynamicPauseLine;
5436
+ if (!lines)
5437
+ return null;
5438
+ const parts = [];
5439
+ const entries = [
5440
+ ["Claude", lines.claude, snapshot.claude],
5441
+ ["Codex", lines.codex, snapshot.codex]
5442
+ ];
5443
+ for (const [name, line, usage] of entries) {
5444
+ if (line === null)
5445
+ continue;
5446
+ const headroom = usage ? `\uFF08util ${usage.gateUtil}%\uFF0C\u4F59\u91CF ${(line - usage.gateUtil).toFixed(1)}\uFF09` : "";
5447
+ parts.push(`${name} ${line.toFixed(1)}%${headroom}`);
5448
+ }
5449
+ if (parts.length === 0)
5450
+ return null;
5451
+ return `\u52A8\u6001\u6682\u505C\u7EBF\uFF1A${parts.join(" \xB7 ")}`;
5452
+ }
5340
5453
  function renderBudgetSnapshot(snapshot, options = {}) {
5341
5454
  const guardHardPct = options.guardHardPct ?? resolveGuardHardHint();
5342
5455
  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)}`);
5456
+ 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
5457
  lines.push(formatAgent("Claude", snapshot.claude, snapshot.updatedAt));
5345
5458
  lines.push(formatAgent("Codex", snapshot.codex, snapshot.updatedAt));
5346
5459
  if (snapshot.burnRate) {
@@ -5354,6 +5467,9 @@ function renderBudgetSnapshot(snapshot, options = {}) {
5354
5467
  const fiveHourWindowsLeftLine = formatFiveHourWindowsLeftLine(snapshot);
5355
5468
  if (fiveHourWindowsLeftLine)
5356
5469
  lines.push(fiveHourWindowsLeftLine);
5470
+ const dynamicLineLine = formatDynamicLineLine(snapshot);
5471
+ if (dynamicLineLine)
5472
+ lines.push(dynamicLineLine);
5357
5473
  if (snapshot.claude && snapshot.codex) {
5358
5474
  const abs = Math.abs(snapshot.driftPct);
5359
5475
  if (abs > 0) {
@@ -5390,7 +5506,7 @@ function renderBudgetSnapshot(snapshot, options = {}) {
5390
5506
  return lines.join(`
5391
5507
  `);
5392
5508
  }
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";
5509
+ var DEFAULT_GUARD_HARD_PCT = 99, 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";
5394
5510
  var init_render = __esm(() => {
5395
5511
  init_burn_view();
5396
5512
  WINDOW_LABELS = {
@@ -5400,7 +5516,8 @@ var init_render = __esm(() => {
5400
5516
  PHASE_LABELS = {
5401
5517
  normal: "normal\uFF08\u6B63\u5E38\uFF09",
5402
5518
  balance: "balance\uFF08\u9700\u5747\u8861\uFF09",
5403
- parallel: "parallel\uFF08\u5EFA\u8BAE\u5E76\u884C\u63D0\u901F\uFF09",
5519
+ parallel: "parallel\uFF08\u5DF2\u9000\u5F79\uFF09",
5520
+ underutilized: "underutilized\uFF08\u989D\u5EA6\u6B20\u8F7D\uFF0C\u5EFA\u8BAE\u63D0\u901F\uFF09",
5404
5521
  paused: "paused\uFF08\u9884\u7B97\u5E72\u9884\u4E2D\uFF09"
5405
5522
  };
5406
5523
  });
@@ -5918,32 +6035,25 @@ function configParseabilityCheck(cwd, cli) {
5918
6035
  detail: desc.customValues ? `parsed at ${desc.path} \u2014 custom values in effect` : `parsed at ${desc.path} \u2014 all values match defaults`
5919
6036
  };
5920
6037
  }
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
- }
6038
+ function evaluateBudgetStrategyGuard(guardHardPct, targetUtilPct = V3_DEFAULT_TARGET_UTIL) {
5929
6039
  if (guardHardPct >= targetUtilPct) {
5930
6040
  return {
5931
- name: "budget strategy",
6041
+ name: "budget target",
5932
6042
  status: "ok",
5933
- detail: `strategy=maximize \u2014 outer guard hard line ${guardHardPct}% covers targetUtil ${targetUtilPct}%`
6043
+ detail: `dynamic line \u2192 targetUtil ${targetUtilPct}%; outer guard hard line ${guardHardPct}% covers it`
5934
6044
  };
5935
6045
  }
5936
6046
  return {
5937
- name: "budget strategy",
6047
+ name: "budget target",
5938
6048
  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"
6049
+ detail: `outer quota-guard hard line (${guardHardPct}%) is below targetUtil (${targetUtilPct}%) \u2014 ` + `the ${guardHardPct}\u2192${targetUtilPct}% band is unreachable for Claude`,
6050
+ 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
6051
  };
5942
6052
  }
5943
6053
  function budgetStrategyGuardCheck(cwd) {
5944
6054
  const config = new ConfigService(cwd).loadOrDefault();
5945
6055
  const budget = applyBudgetEnvOverrides(config.budget);
5946
- return evaluateBudgetStrategyGuard(budget.strategy, resolveGuardHardHint());
6056
+ return evaluateBudgetStrategyGuard(resolveGuardHardHint(), budget.maximize.targetUtil);
5947
6057
  }
5948
6058
  function logCheck(name, path, cli) {
5949
6059
  if (!existsSync15(path)) {
@@ -5994,7 +6104,7 @@ function printDoctorReport(report) {
5994
6104
  console.log(line);
5995
6105
  }
5996
6106
  }
5997
- var LARGE_LOG_WARN_BYTES, V3_DEFAULT_TARGET_UTIL = 97;
6107
+ var LARGE_LOG_WARN_BYTES, V3_DEFAULT_TARGET_UTIL;
5998
6108
  var init_doctor = __esm(() => {
5999
6109
  init_plugin_cache();
6000
6110
  init_build_info();
@@ -6007,6 +6117,7 @@ var init_doctor = __esm(() => {
6007
6117
  init_resume_pollution();
6008
6118
  init_process_lifecycle();
6009
6119
  LARGE_LOG_WARN_BYTES = 100 * 1024 * 1024;
6120
+ V3_DEFAULT_TARGET_UTIL = DEFAULT_BUDGET_CONFIG.maximize.targetUtil;
6010
6121
  });
6011
6122
 
6012
6123
  // src/cli/budget.ts