@raysonmeng/agentbridge 0.1.20 → 0.1.22

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.20",
15
+ "version": "0.1.22",
16
16
  "author": {
17
17
  "name": "AgentBridge Contributors",
18
18
  "email": "raysonmeng@qq.com"
package/dist/cli.js CHANGED
@@ -179,7 +179,7 @@ function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env)
179
179
  var require_package = __commonJS((exports, module) => {
180
180
  module.exports = {
181
181
  name: "@raysonmeng/agentbridge",
182
- version: "0.1.20",
182
+ version: "0.1.22",
183
183
  description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
184
184
  type: "module",
185
185
  packageManager: "bun@1.3.11",
@@ -487,7 +487,7 @@ function findShapeViolation(raw) {
487
487
  if (!isRecord(budget)) {
488
488
  return "budget is present but not an object";
489
489
  }
490
- const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct"];
490
+ const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct", "budgetFreshTtlSec", "idleAdviceActivityWindowSec"];
491
491
  for (const key of numericKeys) {
492
492
  if (key in budget && !isCoercibleNumber(budget[key])) {
493
493
  return `budget.${key} is present but not a number`;
@@ -541,7 +541,7 @@ function hasCustomDecisionValues(config) {
541
541
  const d = DEFAULT_CONFIG;
542
542
  const b = config.budget;
543
543
  const db = d.budget;
544
- 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.maximize.admissionAt !== db.maximize.admissionAt || b.maximize.wrapUpQuota !== db.maximize.wrapUpQuota || b.allocation.minRunwayRatio !== db.allocation.minRunwayRatio || b.allocation.minRunwayGapHours !== db.allocation.minRunwayGapHours;
544
+ 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.budgetFreshTtlSec !== db.budgetFreshTtlSec || b.idleAdviceActivityWindowSec !== db.idleAdviceActivityWindowSec || 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.maximize.admissionAt !== db.maximize.admissionAt || b.maximize.wrapUpQuota !== db.maximize.wrapUpQuota || b.allocation.minRunwayRatio !== db.allocation.minRunwayRatio || b.allocation.minRunwayGapHours !== db.allocation.minRunwayGapHours;
545
545
  }
546
546
  function normalizeInteger(value, fallback) {
547
547
  if (typeof value === "number" && Number.isFinite(value))
@@ -637,6 +637,8 @@ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
637
637
  return {
638
638
  enabled: normalizeBoolean(budget.enabled, fallback.enabled),
639
639
  pollSeconds: normalizeBoundedInteger(budget.pollSeconds, fallback.pollSeconds, 5, 3600),
640
+ budgetFreshTtlSec: normalizeBoundedInteger(budget.budgetFreshTtlSec, fallback.budgetFreshTtlSec, 1, 300),
641
+ idleAdviceActivityWindowSec: normalizeBoundedInteger(budget.idleAdviceActivityWindowSec, fallback.idleAdviceActivityWindowSec, 0, 86400),
640
642
  pauseAt,
641
643
  resumeBelow,
642
644
  syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, fallback.syncDriftPct, 1, 100),
@@ -654,6 +656,8 @@ function applyBudgetEnvOverrides(budget, env = process.env) {
654
656
  const overlay = {
655
657
  enabled: env.AGENTBRIDGE_BUDGET_ENABLED ?? budget.enabled,
656
658
  pollSeconds: env.AGENTBRIDGE_BUDGET_POLL_SECONDS ?? budget.pollSeconds,
659
+ budgetFreshTtlSec: env.AGENTBRIDGE_BUDGET_FRESH_TTL_SEC ?? budget.budgetFreshTtlSec,
660
+ idleAdviceActivityWindowSec: env.AGENTBRIDGE_BUDGET_IDLE_ADVICE_ACTIVITY_WINDOW_SEC ?? budget.idleAdviceActivityWindowSec,
657
661
  pauseAt: env.AGENTBRIDGE_BUDGET_PAUSE_AT ?? budget.pauseAt,
658
662
  resumeBelow: env.AGENTBRIDGE_BUDGET_RESUME_BELOW ?? budget.resumeBelow,
659
663
  syncDriftPct: env.AGENTBRIDGE_BUDGET_SYNC_DRIFT_PCT ?? budget.syncDriftPct,
@@ -793,6 +797,8 @@ var init_config_service = __esm(() => {
793
797
  DEFAULT_BUDGET_CONFIG = {
794
798
  enabled: true,
795
799
  pollSeconds: 300,
800
+ budgetFreshTtlSec: 25,
801
+ idleAdviceActivityWindowSec: 600,
796
802
  pauseAt: 90,
797
803
  resumeBelow: 30,
798
804
  syncDriftPct: 10,
@@ -1501,9 +1507,26 @@ var init_daemon_client = __esm(() => {
1501
1507
  send: () => this.send({ type: "probe_incumbent" })
1502
1508
  });
1503
1509
  }
1510
+ async requestBudgetRefresh(timeoutMs = 3000) {
1511
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1512
+ return null;
1513
+ }
1514
+ const requestId = `budget_${Date.now()}_${this.nextRequestId++}`;
1515
+ return this.awaitTypedResponse({
1516
+ key: "budget_refresh",
1517
+ successEvent: "budgetRefresh",
1518
+ match: (payload) => payload.requestId === requestId,
1519
+ successValue: (payload) => payload.snapshot,
1520
+ failValue: null,
1521
+ timeoutMs,
1522
+ send: () => this.send({ type: "request_budget_refresh", requestId })
1523
+ });
1524
+ }
1504
1525
  awaitTypedResponse(opts) {
1505
- const { key, successEvent, successValue, failValue, timeoutMs, send } = opts;
1526
+ const { key, successEvent, successValue, failValue, timeoutMs, send, match } = opts;
1506
1527
  const onSuccess = (payload) => {
1528
+ if (match && !match(payload))
1529
+ return;
1507
1530
  this.pendingEventWaiters.settle(key, successValue(payload));
1508
1531
  };
1509
1532
  const onRejected = () => {
@@ -1604,6 +1627,9 @@ var init_daemon_client = __esm(() => {
1604
1627
  case "incumbent_status":
1605
1628
  this.emit("incumbentStatus", { connected: message.connected, alive: message.alive });
1606
1629
  return;
1630
+ case "budget_refresh":
1631
+ this.emit("budgetRefresh", { requestId: message.requestId, snapshot: message.snapshot });
1632
+ return;
1607
1633
  }
1608
1634
  };
1609
1635
  ws.onclose = (event) => {
@@ -1682,11 +1708,11 @@ function formatBuildInfo(build) {
1682
1708
  var CODE_HASH_SENTINEL = "source", BUILD_INFO;
1683
1709
  var init_build_info = __esm(() => {
1684
1710
  BUILD_INFO = Object.freeze({
1685
- version: defineString("0.1.20", "0.0.0-source"),
1686
- commit: defineString("d6034c6", "source"),
1711
+ version: defineString("0.1.22", "0.0.0-source"),
1712
+ commit: defineString("f5e401b", "source"),
1687
1713
  bundle: defineBundle("dist"),
1688
1714
  contractVersion: defineNumber(1, CONTRACT_VERSION),
1689
- codeHash: defineString("d2425ba159fe", "source")
1715
+ codeHash: defineString("9688463029af", "source")
1690
1716
  });
1691
1717
  });
1692
1718
 
@@ -5294,6 +5320,45 @@ var init_pairs = __esm(() => {
5294
5320
  init_thread_state();
5295
5321
  init_kill();
5296
5322
  });
5323
+ // src/budget/format-time.ts
5324
+ function parts(epochSeconds, options) {
5325
+ const fmt = new Intl.DateTimeFormat("en-CA", {
5326
+ timeZone: BEIJING_TZ,
5327
+ hour12: false,
5328
+ ...options
5329
+ });
5330
+ const out = {};
5331
+ for (const part of fmt.formatToParts(new Date(epochSeconds * 1000))) {
5332
+ out[part.type] = part.value;
5333
+ }
5334
+ return out;
5335
+ }
5336
+ function formatBeijing(epochSeconds) {
5337
+ if (!epochSeconds || epochSeconds <= 0)
5338
+ return "\u672A\u77E5";
5339
+ const d = new Date(epochSeconds * 1000);
5340
+ if (Number.isNaN(d.getTime()))
5341
+ return "\u672A\u77E5";
5342
+ const p = parts(epochSeconds, {
5343
+ year: "numeric",
5344
+ month: "2-digit",
5345
+ day: "2-digit",
5346
+ hour: "2-digit",
5347
+ minute: "2-digit"
5348
+ });
5349
+ return `${p.year}-${p.month}-${p.day} ${p.hour}:${p.minute}`;
5350
+ }
5351
+ function formatBeijingClock(epochSeconds) {
5352
+ if (!epochSeconds || epochSeconds <= 0)
5353
+ return "\u672A\u77E5";
5354
+ const d = new Date(epochSeconds * 1000);
5355
+ if (Number.isNaN(d.getTime()))
5356
+ return "\u672A\u77E5";
5357
+ const p = parts(epochSeconds, { hour: "2-digit", minute: "2-digit" });
5358
+ return `${p.hour}:${p.minute}`;
5359
+ }
5360
+ var BEIJING_TZ = "Asia/Shanghai";
5361
+
5297
5362
  // src/budget/types.ts
5298
5363
  var STALE_MAX_AGE_SEC = 600;
5299
5364
 
@@ -5349,9 +5414,7 @@ function resolveGuardHardHint(env = process.env) {
5349
5414
  return parsed;
5350
5415
  }
5351
5416
  function formatEpoch(epochSeconds) {
5352
- if (!epochSeconds || epochSeconds <= 0)
5353
- return "\u672A\u77E5";
5354
- return new Date(epochSeconds * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
5417
+ return formatBeijing(epochSeconds);
5355
5418
  }
5356
5419
  function formatWindow(window, label) {
5357
5420
  if (!window)
@@ -5361,25 +5424,25 @@ function formatWindow(window, label) {
5361
5424
  function formatAgent(name, usage, snapshotAt) {
5362
5425
  if (!usage)
5363
5426
  return `${name}\uFF1A\u672A\u77E5\uFF08\u63A2\u6D4B\u4E0D\u53EF\u7528\uFF09`;
5364
- const parts = [
5427
+ const parts2 = [
5365
5428
  formatWindow(usage.fiveHour, "5h"),
5366
5429
  formatWindow(usage.weekly, "\u5468"),
5367
5430
  `\u95E8\u63A7 ${usage.gateUtil}%`,
5368
5431
  `\u9884\u8B66 ${usage.warnUtil}%`
5369
5432
  ];
5370
5433
  if (usage.rateLimitedUntil > 0) {
5371
- parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
5434
+ parts2.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
5372
5435
  }
5373
5436
  if (usage.parsedVia === "positional") {
5374
- parts.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
5437
+ parts2.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
5375
5438
  }
5376
5439
  const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
5377
5440
  if (ageSec > 300) {
5378
- parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
5441
+ parts2.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
5379
5442
  } else if (usage.stale) {
5380
- parts.push("\uFF08\u7F13\u5B58\u6570\u636E\uFF09");
5443
+ parts2.push("\uFF08\u7F13\u5B58\u6570\u636E\uFF09");
5381
5444
  }
5382
- return `${name}\uFF1A${parts.join(" \xB7 ")}`;
5445
+ return `${name}\uFF1A${parts2.join(" \xB7 ")}`;
5383
5446
  }
5384
5447
  function formatDuration(seconds) {
5385
5448
  const totalMinutes = Math.max(0, Math.round(seconds / 60));
@@ -5390,10 +5453,7 @@ function formatDuration(seconds) {
5390
5453
  return `${hours}\u5C0F\u65F6${minutes}\u5206\u949F`;
5391
5454
  }
5392
5455
  function formatClockTime(epochSeconds) {
5393
- const date = new Date(epochSeconds * 1000);
5394
- const hh = String(date.getHours()).padStart(2, "0");
5395
- const mm = String(date.getMinutes()).padStart(2, "0");
5396
- return `${hh}:${mm}`;
5456
+ return formatBeijingClock(epochSeconds);
5397
5457
  }
5398
5458
  function formatWindowRate(label, rate) {
5399
5459
  if (!rate)
@@ -5414,20 +5474,20 @@ function formatRunwaySegment(runway, basisWindow, snapshotAt) {
5414
5474
  return `\u7EA6\u53EF\u518D\u5DE5\u4F5C ${formatDuration(runway.seconds)}\uFF08${clockNote}${WINDOW_LABELS[runway.basis]}\u4E3A\u7EA6\u675F\uFF09`;
5415
5475
  }
5416
5476
  function formatBurnRateLine(name, usage, rates, runway, snapshotAt, guardHardPct) {
5417
- const parts = [
5477
+ const parts2 = [
5418
5478
  formatWindowRate("5h", rates.fiveHour),
5419
5479
  formatWindowRate("\u5468", rates.weekly)
5420
5480
  ].filter((part) => part !== null);
5421
- if (parts.length === 0 && !runway)
5481
+ if (parts2.length === 0 && !runway)
5422
5482
  return null;
5423
5483
  if (runway) {
5424
5484
  const basisWindow = usage ? usage[runway.basis] : null;
5425
- parts.push(formatRunwaySegment(runway, basisWindow, snapshotAt));
5485
+ parts2.push(formatRunwaySegment(runway, basisWindow, snapshotAt));
5426
5486
  }
5427
5487
  if (guardHardPct !== null) {
5428
- parts.push(`\u5916\u5C42 guard \u786C\u7EBF ${guardHardPct}%\uFF08v3 \u4E0D\u53EF\u8D8A\u8FC7\uFF1Brunway \u4E3A\u4E2D\u6027\u53E3\u5F84\uFF0CClaude \u4F1A\u5148\u5728\u786C\u7EBF\u88AB\u5916\u5C42\u505C\u4F4F\uFF09`);
5488
+ parts2.push(`\u5916\u5C42 guard \u786C\u7EBF ${guardHardPct}%\uFF08v3 \u4E0D\u53EF\u8D8A\u8FC7\uFF1Brunway \u4E3A\u4E2D\u6027\u53E3\u5F84\uFF0CClaude \u4F1A\u5148\u5728\u786C\u7EBF\u88AB\u5916\u5C42\u505C\u4F4F\uFF09`);
5429
5489
  }
5430
- return `${name} \u71C3\u5C3D\u7387\uFF1A${parts.join(" \xB7 ")}`;
5490
+ return `${name} \u71C3\u5C3D\u7387\uFF1A${parts2.join(" \xB7 ")}`;
5431
5491
  }
5432
5492
  function formatFiveHourWindowsLeftLine(snapshot) {
5433
5493
  const values = [];
@@ -5471,7 +5531,7 @@ function formatDynamicLineLine(snapshot) {
5471
5531
  const lines = snapshot.dynamicPauseLine;
5472
5532
  if (!lines)
5473
5533
  return null;
5474
- const parts = [];
5534
+ const parts2 = [];
5475
5535
  const entries = [
5476
5536
  ["Claude", lines.claude, snapshot.claude],
5477
5537
  ["Codex", lines.codex, snapshot.codex]
@@ -5480,16 +5540,16 @@ function formatDynamicLineLine(snapshot) {
5480
5540
  if (line === null)
5481
5541
  continue;
5482
5542
  const headroom = usage ? `\uFF08util ${usage.gateUtil}%\uFF0C\u4F59\u91CF ${(line - usage.gateUtil).toFixed(1)}\uFF09` : "";
5483
- parts.push(`${name} ${line.toFixed(1)}%${headroom}`);
5543
+ parts2.push(`${name} ${line.toFixed(1)}%${headroom}`);
5484
5544
  }
5485
- if (parts.length === 0)
5545
+ if (parts2.length === 0)
5486
5546
  return null;
5487
- return `\u52A8\u6001\u6682\u505C\u7EBF\uFF1A${parts.join(" \xB7 ")}`;
5547
+ return `\u52A8\u6001\u6682\u505C\u7EBF\uFF1A${parts2.join(" \xB7 ")}`;
5488
5548
  }
5489
5549
  function renderBudgetSnapshot(snapshot, options = {}) {
5490
5550
  const guardHardPct = options.guardHardPct ?? resolveGuardHardHint();
5491
5551
  const lines = [];
5492
- 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)}`);
5552
+ 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)}\uFF08\u65F6\u95F4\u5747\u4E3A\u5317\u4EAC\u65F6\u95F4 UTC+8\uFF09`);
5493
5553
  lines.push(formatAgent("Claude", snapshot.claude, snapshot.updatedAt));
5494
5554
  lines.push(formatAgent("Codex", snapshot.codex, snapshot.updatedAt));
5495
5555
  if (snapshot.burnRate) {
@@ -5733,8 +5793,8 @@ function extractUserText(entry) {
5733
5793
  const content = entry.payload.content;
5734
5794
  if (!Array.isArray(content))
5735
5795
  return null;
5736
- const parts = content.map((item) => typeof item?.text === "string" ? item.text : typeof item?.input_text?.text === "string" ? item.input_text.text : null).filter((part) => !!part);
5737
- return parts.length > 0 ? parts.join(`
5796
+ const parts2 = content.map((item) => typeof item?.text === "string" ? item.text : typeof item?.input_text?.text === "string" ? item.input_text.text : null).filter((part) => !!part);
5797
+ return parts2.length > 0 ? parts2.join(`
5738
5798
  `) : null;
5739
5799
  }
5740
5800
  return null;
package/dist/daemon.js CHANGED
@@ -29,11 +29,11 @@ function defineNumber(value, fallback) {
29
29
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
30
30
  }
31
31
  var BUILD_INFO = Object.freeze({
32
- version: defineString("0.1.20", "0.0.0-source"),
33
- commit: defineString("d6034c6", "source"),
32
+ version: defineString("0.1.22", "0.0.0-source"),
33
+ commit: defineString("f5e401b", "source"),
34
34
  bundle: defineBundle("dist"),
35
35
  contractVersion: defineNumber(1, CONTRACT_VERSION),
36
- codeHash: defineString("d2425ba159fe", "source")
36
+ codeHash: defineString("9688463029af", "source")
37
37
  });
38
38
  function daemonStatusBuildInfo() {
39
39
  return { ...BUILD_INFO };
@@ -3478,6 +3478,8 @@ import { join as join4 } from "path";
3478
3478
  var DEFAULT_BUDGET_CONFIG = {
3479
3479
  enabled: true,
3480
3480
  pollSeconds: 300,
3481
+ budgetFreshTtlSec: 25,
3482
+ idleAdviceActivityWindowSec: 600,
3481
3483
  pauseAt: 90,
3482
3484
  resumeBelow: 30,
3483
3485
  syncDriftPct: 10,
@@ -3539,7 +3541,7 @@ function findShapeViolation(raw) {
3539
3541
  if (!isRecord(budget)) {
3540
3542
  return "budget is present but not an object";
3541
3543
  }
3542
- const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct"];
3544
+ const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct", "budgetFreshTtlSec", "idleAdviceActivityWindowSec"];
3543
3545
  for (const key of numericKeys) {
3544
3546
  if (key in budget && !isCoercibleNumber(budget[key])) {
3545
3547
  return `budget.${key} is present but not a number`;
@@ -3593,7 +3595,7 @@ function hasCustomDecisionValues(config) {
3593
3595
  const d = DEFAULT_CONFIG;
3594
3596
  const b = config.budget;
3595
3597
  const db = d.budget;
3596
- 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.maximize.admissionAt !== db.maximize.admissionAt || b.maximize.wrapUpQuota !== db.maximize.wrapUpQuota || b.allocation.minRunwayRatio !== db.allocation.minRunwayRatio || b.allocation.minRunwayGapHours !== db.allocation.minRunwayGapHours;
3598
+ 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.budgetFreshTtlSec !== db.budgetFreshTtlSec || b.idleAdviceActivityWindowSec !== db.idleAdviceActivityWindowSec || 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.maximize.admissionAt !== db.maximize.admissionAt || b.maximize.wrapUpQuota !== db.maximize.wrapUpQuota || b.allocation.minRunwayRatio !== db.allocation.minRunwayRatio || b.allocation.minRunwayGapHours !== db.allocation.minRunwayGapHours;
3597
3599
  }
3598
3600
  function normalizeInteger(value, fallback) {
3599
3601
  if (typeof value === "number" && Number.isFinite(value))
@@ -3689,6 +3691,8 @@ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
3689
3691
  return {
3690
3692
  enabled: normalizeBoolean(budget.enabled, fallback.enabled),
3691
3693
  pollSeconds: normalizeBoundedInteger(budget.pollSeconds, fallback.pollSeconds, 5, 3600),
3694
+ budgetFreshTtlSec: normalizeBoundedInteger(budget.budgetFreshTtlSec, fallback.budgetFreshTtlSec, 1, 300),
3695
+ idleAdviceActivityWindowSec: normalizeBoundedInteger(budget.idleAdviceActivityWindowSec, fallback.idleAdviceActivityWindowSec, 0, 86400),
3692
3696
  pauseAt,
3693
3697
  resumeBelow,
3694
3698
  syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, fallback.syncDriftPct, 1, 100),
@@ -3706,6 +3710,8 @@ function applyBudgetEnvOverrides(budget, env = process.env) {
3706
3710
  const overlay = {
3707
3711
  enabled: env.AGENTBRIDGE_BUDGET_ENABLED ?? budget.enabled,
3708
3712
  pollSeconds: env.AGENTBRIDGE_BUDGET_POLL_SECONDS ?? budget.pollSeconds,
3713
+ budgetFreshTtlSec: env.AGENTBRIDGE_BUDGET_FRESH_TTL_SEC ?? budget.budgetFreshTtlSec,
3714
+ idleAdviceActivityWindowSec: env.AGENTBRIDGE_BUDGET_IDLE_ADVICE_ACTIVITY_WINDOW_SEC ?? budget.idleAdviceActivityWindowSec,
3709
3715
  pauseAt: env.AGENTBRIDGE_BUDGET_PAUSE_AT ?? budget.pauseAt,
3710
3716
  resumeBelow: env.AGENTBRIDGE_BUDGET_RESUME_BELOW ?? budget.resumeBelow,
3711
3717
  syncDriftPct: env.AGENTBRIDGE_BUDGET_SYNC_DRIFT_PCT ?? budget.syncDriftPct,
@@ -3861,6 +3867,36 @@ function retryAfterMsForResume(resumeAfterEpoch, nowMs) {
3861
3867
  return remainingMs > 0 ? remainingMs : undefined;
3862
3868
  }
3863
3869
 
3870
+ // src/budget/format-time.ts
3871
+ var BEIJING_TZ = "Asia/Shanghai";
3872
+ function parts(epochSeconds, options) {
3873
+ const fmt = new Intl.DateTimeFormat("en-CA", {
3874
+ timeZone: BEIJING_TZ,
3875
+ hour12: false,
3876
+ ...options
3877
+ });
3878
+ const out = {};
3879
+ for (const part of fmt.formatToParts(new Date(epochSeconds * 1000))) {
3880
+ out[part.type] = part.value;
3881
+ }
3882
+ return out;
3883
+ }
3884
+ function formatBeijing(epochSeconds) {
3885
+ if (!epochSeconds || epochSeconds <= 0)
3886
+ return "\u672A\u77E5";
3887
+ const d = new Date(epochSeconds * 1000);
3888
+ if (Number.isNaN(d.getTime()))
3889
+ return "\u672A\u77E5";
3890
+ const p = parts(epochSeconds, {
3891
+ year: "numeric",
3892
+ month: "2-digit",
3893
+ day: "2-digit",
3894
+ hour: "2-digit",
3895
+ minute: "2-digit"
3896
+ });
3897
+ return `${p.year}-${p.month}-${p.day} ${p.hour}:${p.minute}`;
3898
+ }
3899
+
3864
3900
  // src/budget/types.ts
3865
3901
  var STALE_MAX_AGE_SEC = 600;
3866
3902
 
@@ -4149,14 +4185,12 @@ function pct2(value) {
4149
4185
  return `${Math.round(value * 10) / 10}%`;
4150
4186
  }
4151
4187
  function formatEpoch(epoch) {
4152
- if (!epoch || epoch <= 0)
4153
- return "\u672A\u77E5";
4154
- return new Date(epoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
4188
+ return formatBeijing(epoch);
4155
4189
  }
4156
4190
  function usageSummary(name, usage) {
4157
4191
  if (!usage)
4158
4192
  return `${AGENT_LABEL2[name]} \u672A\u77E5`;
4159
- return `${AGENT_LABEL2[name]} gate=${pct2(usage.gateUtil)} warn=${pct2(usage.warnUtil)} 5h\u91CD\u7F6E=${formatEpoch(usage.fiveHour?.resetEpoch ?? 0)}`;
4193
+ return `${AGENT_LABEL2[name]} gate=${pct2(usage.gateUtil)} warn=${pct2(usage.warnUtil)} 5h\u91CD\u7F6E=${formatEpoch(usage.fiveHour?.resetEpoch ?? 0)}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09`;
4160
4194
  }
4161
4195
  function resumeAfterEpoch(claude, codex, cfg, now) {
4162
4196
  const epochs = [
@@ -4452,7 +4486,7 @@ function pct3(value) {
4452
4486
  return `${Math.round(value * 10) / 10}%`;
4453
4487
  }
4454
4488
  function formatEpoch2(epoch) {
4455
- return new Date(epoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
4489
+ return formatBeijing(epoch);
4456
4490
  }
4457
4491
  var INITIAL_FINGERPRINT_STATE = {
4458
4492
  side: null,
@@ -4500,7 +4534,7 @@ function activeSideReason(agent, usage, cfg, now) {
4500
4534
  if (!usage)
4501
4535
  return `${AGENT_LABEL3[agent]} \u63A2\u6D4B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u4FDD\u6301\u4E0A\u4E00\u8F6E\u9884\u7B97\u5E72\u9884`;
4502
4536
  if (usage.rateLimitedUntil > now) {
4503
- return `${AGENT_LABEL3[agent]} \u63A2\u9488\u88AB\u9650\u6D41\u81F3 ${formatEpoch2(usage.rateLimitedUntil)}`;
4537
+ return `${AGENT_LABEL3[agent]} \u63A2\u9488\u88AB\u9650\u6D41\u81F3 ${formatEpoch2(usage.rateLimitedUntil)}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09`;
4504
4538
  }
4505
4539
  const decision = agentShouldPause(agent, usage, cfg, now);
4506
4540
  if (decision.pause)
@@ -4643,7 +4677,7 @@ function admissionReason(side, state, cfg) {
4643
4677
  if (!usage)
4644
4678
  return `${AGENT_LABEL3[agent]} \u63A2\u6D4B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u4FDD\u6301\u4E0A\u4E00\u8F6E\u6536\u5C3E\u4FDD\u62A4`;
4645
4679
  if (usage.rateLimitedUntil > state.now) {
4646
- return `${AGENT_LABEL3[agent]} \u63A2\u9488\u88AB\u9650\u6D41\u81F3 ${formatEpoch2(usage.rateLimitedUntil)}\uFF0C\u4FDD\u6301\u6536\u5C3E\u4FDD\u62A4`;
4680
+ return `${AGENT_LABEL3[agent]} \u63A2\u9488\u88AB\u9650\u6D41\u81F3 ${formatEpoch2(usage.rateLimitedUntil)}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09\uFF0C\u4FDD\u6301\u6536\u5C3E\u4FDD\u62A4`;
4647
4681
  }
4648
4682
  const decision = agentShouldAdmitClose(agent, usage, cfg, state.now);
4649
4683
  if (decision.admitClose)
@@ -4818,6 +4852,7 @@ class BudgetCoordinator {
4818
4852
  resumeSignals;
4819
4853
  adviceCooldown;
4820
4854
  isCodexTurnActive;
4855
+ hasRecentActivity;
4821
4856
  timer = null;
4822
4857
  running = false;
4823
4858
  fpState = INITIAL_FINGERPRINT_STATE;
@@ -4848,6 +4883,7 @@ class BudgetCoordinator {
4848
4883
  log: this.log
4849
4884
  });
4850
4885
  this.isCodexTurnActive = options.isCodexTurnActive ?? (() => false);
4886
+ this.hasRecentActivity = options.hasRecentActivity ?? (() => true);
4851
4887
  }
4852
4888
  async start() {
4853
4889
  if (this.running || !this.config.enabled)
@@ -4880,6 +4916,24 @@ class BudgetCoordinator {
4880
4916
  getSnapshot() {
4881
4917
  return this.latestSnapshot;
4882
4918
  }
4919
+ async refreshSnapshotReadonly() {
4920
+ let usage;
4921
+ try {
4922
+ usage = await this.source.fetchBoth();
4923
+ } catch (error) {
4924
+ this.log(`budget readonly refresh failed: ${error instanceof Error ? error.message : String(error)}`);
4925
+ return null;
4926
+ }
4927
+ if (!usage)
4928
+ return null;
4929
+ const now = this.now();
4930
+ const runway = {
4931
+ claude: agentRunway(usage.claude, now),
4932
+ codex: agentRunway(usage.codex, now)
4933
+ };
4934
+ const state = computeBudgetState(usage.claude, usage.codex, this.config, now, runway);
4935
+ return this.toSnapshot(state, runway);
4936
+ }
4883
4937
  getResumeCandidate() {
4884
4938
  const { detail, ...rest } = this.resumeCandidate;
4885
4939
  return detail ? {
@@ -4997,6 +5051,12 @@ class BudgetCoordinator {
4997
5051
  this.fpState = { ...this.fpState, fingerprint: null };
4998
5052
  return;
4999
5053
  }
5054
+ const activityWindowSec = this.config.idleAdviceActivityWindowSec;
5055
+ if (activityWindowSec > 0 && !this.hasRecentActivity(activityWindowSec)) {
5056
+ this.log(`budget advise suppressed: no agent activity in last ${activityWindowSec}s`);
5057
+ this.fpState = { ...this.fpState, fingerprint: null };
5058
+ return;
5059
+ }
5000
5060
  if (effect.phase === "underutilized") {
5001
5061
  if (!this.adviceCooldown.tryAcquire("underutilization", state.now))
5002
5062
  return;
@@ -6767,7 +6827,8 @@ function ensureBudgetCoordinatorStarted() {
6767
6827
  });
6768
6828
  },
6769
6829
  resumeSignals: readResumeSignals,
6770
- isCodexTurnActive: () => codex.turnInProgress
6830
+ isCodexTurnActive: () => codex.turnInProgress,
6831
+ hasRecentActivity: (windowSec) => codex.turnInProgress || Date.now() - lastActivityEpochMs <= windowSec * 1000
6771
6832
  });
6772
6833
  }
6773
6834
  budgetCoordinator.start();
@@ -6778,13 +6839,13 @@ function stopBudgetCoordinator() {
6778
6839
  function budgetPauseGateError() {
6779
6840
  const snapshot = budgetCoordinator?.getSnapshot() ?? null;
6780
6841
  const reason = snapshot?.pauseReason ?? "Codex \u4FA7\u989D\u5EA6\u63A5\u8FD1\u8017\u5C3D";
6781
- const resumeAt = snapshot?.resumeAfterEpoch ? new Date(snapshot.resumeAfterEpoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z") : null;
6842
+ const resumeAt = snapshot?.resumeAfterEpoch ? `${formatBeijing(snapshot.resumeAfterEpoch)}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09` : null;
6782
6843
  const sideHint = snapshot?.pauseSide === "both" ? "\u53CC\u4FA7\u989D\u5EA6\u5747\u5DF2\u8017\u5C3D\uFF0C\u8BF7\u5199 checkpoint \u7B49\u5F85\u5237\u65B0" : "\u4F60\u53EF\u7EE7\u7EED solo \u63A8\u8FDB\u53EF\u72EC\u7ACB\u90E8\u5206\uFF0C\u5E76\u5199 checkpoint \u6807\u6CE8\u5206\u5DE5\u65AD\u70B9";
6783
6844
  const reopenText = `Codex \u4FA7\u5404\u7A97\u53E3 util \u56DE\u843D\u81F3\u52A8\u6001\u6682\u505C\u7EBF \u2212 ${BUDGET_CONFIG.maximize.resumeHysteresisPct}% \u4EE5\u4E0B\u6216\u5BF9\u5E94\u7A97\u53E3\u5237\u65B0\u540E\u95F8\u95E8\u81EA\u52A8\u653E\u5F00`;
6784
6845
  return `\u9884\u7B97\u6682\u505C\uFF08\u95F8\u95E8\u5173\u95ED\uFF09\uFF0C\u5DF2\u62D2\u7EDD\u8F6C\u53D1\uFF1A${reason}\u3002` + reopenText + (resumeAt ? `\uFF08\u9884\u8BA1\u6062\u590D ${resumeAt}\uFF0C\u4EE5\u5B9E\u6D4B\u4E3A\u51C6\uFF1B\u63D0\u524D\u5237\u65B0\u4F1A\u66F4\u65E9\u89E3\u9664\uFF09` : "") + `\u3002\u6536\u5230 RESUME \u901A\u77E5\u524D\u8BF7\u52FF\u91CD\u8BD5\u5411 Codex \u53D1\u9001 reply\uFF1B${sideHint}\u3002`;
6785
6846
  }
6786
6847
  function budgetAdmissionGateError(windowResetEpoch, wrapUpLeft, quotaExhausted) {
6787
- const resetAt = windowResetEpoch > 0 ? new Date(windowResetEpoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z") : "\u672A\u77E5";
6848
+ const resetAt = windowResetEpoch > 0 ? `${formatBeijing(windowResetEpoch)}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09` : "\u672A\u77E5";
6788
6849
  const quota = BUDGET_CONFIG.maximize.wrapUpQuota;
6789
6850
  if (quotaExhausted) {
6790
6851
  return `\u989D\u5EA6\u7A97\u53E3\u6536\u5C3E\u4FDD\u62A4\u4E2D\uFF08admission-closed\uFF09\uFF1A\u672C\u7A97\u53E3 wrap-up \u914D\u989D\uFF08\u6BCF\u7A97\u53E3 ${quota} \u4E2A\uFF09\u5DF2\u7528\u5C3D\uFF0C\u5DF2\u62D2\u7EDD\u8F6C\u53D1\u3002` + `\u8BF7\u52FF\u518D\u6D3E\u65B0\u4EFB\u52A1\uFF1B\u5199 checkpoint\uFF0C\u7B49\u989D\u5EA6\u7A97\u53E3\u5237\u65B0\uFF08\u7EA6 ${resetAt}\uFF09\u540E\u518D\u7EE7\u7EED\u3002`;
@@ -6896,6 +6957,7 @@ codex.on("steerFailed", ({ requestId, reason }) => {
6896
6957
  });
6897
6958
  codex.on("steerAccepted", ({ requestId }) => {
6898
6959
  log("Steer accepted by app-server");
6960
+ recordAgentActivity();
6899
6961
  const dispatch = pendingSteerDispatches.get(requestId);
6900
6962
  pendingSteerDispatches.delete(requestId);
6901
6963
  if (dispatch?.requireReply) {
@@ -6965,13 +7027,19 @@ codex.on("turnTrackingReset", (reason) => {
6965
7027
  pendingSteerDispatches.clear();
6966
7028
  resumeInjectionQueue.onTurnTrackingReset();
6967
7029
  });
7030
+ var lastActivityEpochMs = 0;
7031
+ function recordAgentActivity() {
7032
+ lastActivityEpochMs = Date.now();
7033
+ }
6968
7034
  codex.on("turnStarted", () => {
6969
7035
  log("Codex turn started");
7036
+ recordAgentActivity();
6970
7037
  emitToClaude(systemMessage("system_turn_started", "\u23F3 Codex is working on the current task. Wait for completion before sending a reply."));
6971
7038
  });
6972
7039
  codex.on("agentMessage", (msg) => {
6973
7040
  if (msg.source !== "codex")
6974
7041
  return;
7042
+ recordAgentActivity();
6975
7043
  const route = routeCodexMessage(msg.content, {
6976
7044
  mode: FILTER_MODE,
6977
7045
  replyArmed: replyTracker.isArmed,
@@ -7189,6 +7257,11 @@ function handleControlMessage(ws, raw) {
7189
7257
  log(`handleProbeIncumbent threw for #${ws.data.clientId}: ${err?.message ?? err}`);
7190
7258
  });
7191
7259
  return;
7260
+ case "request_budget_refresh":
7261
+ handleRequestBudgetRefresh(ws, message.requestId).catch((err) => {
7262
+ log(`handleRequestBudgetRefresh threw for #${ws.data.clientId}: ${err?.message ?? err}`);
7263
+ });
7264
+ return;
7192
7265
  case "claude_to_codex": {
7193
7266
  handleClaudeToCodex(ws, message).catch((err) => {
7194
7267
  log(`handleClaudeToCodex threw for request ${message.requestId}: ${err?.message ?? err}`);
@@ -7547,6 +7620,11 @@ async function handleProbeIncumbent(ws) {
7547
7620
  alive: stillConnected && alive
7548
7621
  });
7549
7622
  }
7623
+ async function handleRequestBudgetRefresh(ws, requestId) {
7624
+ const snapshot = budgetCoordinator ? await budgetCoordinator.refreshSnapshotReadonly() : null;
7625
+ log(`request_budget_refresh from #${ws.data.clientId}: ${snapshot ? "fresh" : "unavailable"}`);
7626
+ sendProtocolMessage(ws, { type: "budget_refresh", requestId, snapshot });
7627
+ }
7550
7628
  async function probeLiveness2(ws, timeoutMs) {
7551
7629
  return probeLiveness({
7552
7630
  get readyState() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raysonmeng/agentbridge",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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.20",
3
+ "version": "0.1.22",
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",
@@ -13866,6 +13866,45 @@ class StateDirResolver {
13866
13866
  }
13867
13867
  }
13868
13868
 
13869
+ // src/budget/format-time.ts
13870
+ var BEIJING_TZ = "Asia/Shanghai";
13871
+ function parts(epochSeconds, options) {
13872
+ const fmt = new Intl.DateTimeFormat("en-CA", {
13873
+ timeZone: BEIJING_TZ,
13874
+ hour12: false,
13875
+ ...options
13876
+ });
13877
+ const out = {};
13878
+ for (const part of fmt.formatToParts(new Date(epochSeconds * 1000))) {
13879
+ out[part.type] = part.value;
13880
+ }
13881
+ return out;
13882
+ }
13883
+ function formatBeijing(epochSeconds) {
13884
+ if (!epochSeconds || epochSeconds <= 0)
13885
+ return "\u672A\u77E5";
13886
+ const d = new Date(epochSeconds * 1000);
13887
+ if (Number.isNaN(d.getTime()))
13888
+ return "\u672A\u77E5";
13889
+ const p = parts(epochSeconds, {
13890
+ year: "numeric",
13891
+ month: "2-digit",
13892
+ day: "2-digit",
13893
+ hour: "2-digit",
13894
+ minute: "2-digit"
13895
+ });
13896
+ return `${p.year}-${p.month}-${p.day} ${p.hour}:${p.minute}`;
13897
+ }
13898
+ function formatBeijingClock(epochSeconds) {
13899
+ if (!epochSeconds || epochSeconds <= 0)
13900
+ return "\u672A\u77E5";
13901
+ const d = new Date(epochSeconds * 1000);
13902
+ if (Number.isNaN(d.getTime()))
13903
+ return "\u672A\u77E5";
13904
+ const p = parts(epochSeconds, { hour: "2-digit", minute: "2-digit" });
13905
+ return `${p.hour}:${p.minute}`;
13906
+ }
13907
+
13869
13908
  // src/budget/types.ts
13870
13909
  var STALE_MAX_AGE_SEC = 600;
13871
13910
 
@@ -13909,9 +13948,7 @@ function resolveGuardHardHint(env = process.env) {
13909
13948
  return parsed;
13910
13949
  }
13911
13950
  function formatEpoch(epochSeconds) {
13912
- if (!epochSeconds || epochSeconds <= 0)
13913
- return "\u672A\u77E5";
13914
- return new Date(epochSeconds * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
13951
+ return formatBeijing(epochSeconds);
13915
13952
  }
13916
13953
  function formatWindow(window, label) {
13917
13954
  if (!window)
@@ -13921,25 +13958,25 @@ function formatWindow(window, label) {
13921
13958
  function formatAgent(name, usage, snapshotAt) {
13922
13959
  if (!usage)
13923
13960
  return `${name}\uFF1A\u672A\u77E5\uFF08\u63A2\u6D4B\u4E0D\u53EF\u7528\uFF09`;
13924
- const parts = [
13961
+ const parts2 = [
13925
13962
  formatWindow(usage.fiveHour, "5h"),
13926
13963
  formatWindow(usage.weekly, "\u5468"),
13927
13964
  `\u95E8\u63A7 ${usage.gateUtil}%`,
13928
13965
  `\u9884\u8B66 ${usage.warnUtil}%`
13929
13966
  ];
13930
13967
  if (usage.rateLimitedUntil > 0) {
13931
- parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
13968
+ parts2.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
13932
13969
  }
13933
13970
  if (usage.parsedVia === "positional") {
13934
- parts.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
13971
+ parts2.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
13935
13972
  }
13936
13973
  const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
13937
13974
  if (ageSec > 300) {
13938
- parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
13975
+ parts2.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
13939
13976
  } else if (usage.stale) {
13940
- parts.push("\uFF08\u7F13\u5B58\u6570\u636E\uFF09");
13977
+ parts2.push("\uFF08\u7F13\u5B58\u6570\u636E\uFF09");
13941
13978
  }
13942
- return `${name}\uFF1A${parts.join(" \xB7 ")}`;
13979
+ return `${name}\uFF1A${parts2.join(" \xB7 ")}`;
13943
13980
  }
13944
13981
  var WINDOW_LABELS = {
13945
13982
  fiveHour: "5h \u7A97\u53E3",
@@ -13955,10 +13992,7 @@ function formatDuration(seconds) {
13955
13992
  return `${hours}\u5C0F\u65F6${minutes}\u5206\u949F`;
13956
13993
  }
13957
13994
  function formatClockTime(epochSeconds) {
13958
- const date4 = new Date(epochSeconds * 1000);
13959
- const hh = String(date4.getHours()).padStart(2, "0");
13960
- const mm = String(date4.getMinutes()).padStart(2, "0");
13961
- return `${hh}:${mm}`;
13995
+ return formatBeijingClock(epochSeconds);
13962
13996
  }
13963
13997
  function formatWindowRate(label, rate) {
13964
13998
  if (!rate)
@@ -13979,20 +14013,20 @@ function formatRunwaySegment(runway, basisWindow, snapshotAt) {
13979
14013
  return `\u7EA6\u53EF\u518D\u5DE5\u4F5C ${formatDuration(runway.seconds)}\uFF08${clockNote}${WINDOW_LABELS[runway.basis]}\u4E3A\u7EA6\u675F\uFF09`;
13980
14014
  }
13981
14015
  function formatBurnRateLine(name, usage, rates, runway, snapshotAt, guardHardPct) {
13982
- const parts = [
14016
+ const parts2 = [
13983
14017
  formatWindowRate("5h", rates.fiveHour),
13984
14018
  formatWindowRate("\u5468", rates.weekly)
13985
14019
  ].filter((part) => part !== null);
13986
- if (parts.length === 0 && !runway)
14020
+ if (parts2.length === 0 && !runway)
13987
14021
  return null;
13988
14022
  if (runway) {
13989
14023
  const basisWindow = usage ? usage[runway.basis] : null;
13990
- parts.push(formatRunwaySegment(runway, basisWindow, snapshotAt));
14024
+ parts2.push(formatRunwaySegment(runway, basisWindow, snapshotAt));
13991
14025
  }
13992
14026
  if (guardHardPct !== null) {
13993
- parts.push(`\u5916\u5C42 guard \u786C\u7EBF ${guardHardPct}%\uFF08v3 \u4E0D\u53EF\u8D8A\u8FC7\uFF1Brunway \u4E3A\u4E2D\u6027\u53E3\u5F84\uFF0CClaude \u4F1A\u5148\u5728\u786C\u7EBF\u88AB\u5916\u5C42\u505C\u4F4F\uFF09`);
14027
+ parts2.push(`\u5916\u5C42 guard \u786C\u7EBF ${guardHardPct}%\uFF08v3 \u4E0D\u53EF\u8D8A\u8FC7\uFF1Brunway \u4E3A\u4E2D\u6027\u53E3\u5F84\uFF0CClaude \u4F1A\u5148\u5728\u786C\u7EBF\u88AB\u5916\u5C42\u505C\u4F4F\uFF09`);
13994
14028
  }
13995
- return `${name} \u71C3\u5C3D\u7387\uFF1A${parts.join(" \xB7 ")}`;
14029
+ return `${name} \u71C3\u5C3D\u7387\uFF1A${parts2.join(" \xB7 ")}`;
13996
14030
  }
13997
14031
  function formatFiveHourWindowsLeftLine(snapshot) {
13998
14032
  const values = [];
@@ -14037,7 +14071,7 @@ function formatDynamicLineLine(snapshot) {
14037
14071
  const lines = snapshot.dynamicPauseLine;
14038
14072
  if (!lines)
14039
14073
  return null;
14040
- const parts = [];
14074
+ const parts2 = [];
14041
14075
  const entries = [
14042
14076
  ["Claude", lines.claude, snapshot.claude],
14043
14077
  ["Codex", lines.codex, snapshot.codex]
@@ -14046,11 +14080,11 @@ function formatDynamicLineLine(snapshot) {
14046
14080
  if (line === null)
14047
14081
  continue;
14048
14082
  const headroom = usage ? `\uFF08util ${usage.gateUtil}%\uFF0C\u4F59\u91CF ${(line - usage.gateUtil).toFixed(1)}\uFF09` : "";
14049
- parts.push(`${name} ${line.toFixed(1)}%${headroom}`);
14083
+ parts2.push(`${name} ${line.toFixed(1)}%${headroom}`);
14050
14084
  }
14051
- if (parts.length === 0)
14085
+ if (parts2.length === 0)
14052
14086
  return null;
14053
- return `\u52A8\u6001\u6682\u505C\u7EBF\uFF1A${parts.join(" \xB7 ")}`;
14087
+ return `\u52A8\u6001\u6682\u505C\u7EBF\uFF1A${parts2.join(" \xB7 ")}`;
14054
14088
  }
14055
14089
  var PHASE_LABELS = {
14056
14090
  normal: "normal\uFF08\u6B63\u5E38\uFF09",
@@ -14062,7 +14096,7 @@ var PHASE_LABELS = {
14062
14096
  function renderBudgetSnapshot(snapshot, options = {}) {
14063
14097
  const guardHardPct = options.guardHardPct ?? resolveGuardHardHint();
14064
14098
  const lines = [];
14065
- 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)}`);
14099
+ 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)}\uFF08\u65F6\u95F4\u5747\u4E3A\u5317\u4EAC\u65F6\u95F4 UTC+8\uFF09`);
14066
14100
  lines.push(formatAgent("Claude", snapshot.claude, snapshot.updatedAt));
14067
14101
  lines.push(formatAgent("Codex", snapshot.codex, snapshot.updatedAt));
14068
14102
  if (snapshot.burnRate) {
@@ -14127,6 +14161,7 @@ var DEFAULT_MAX_BUFFERED_MESSAGES = 100;
14127
14161
  var DEFAULT_MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
14128
14162
  var DEFAULT_DEDUPE_CAPACITY = 2048;
14129
14163
  var DEFAULT_DEDUPE_TTL_MS = 20 * 60 * 1000;
14164
+ var DEFAULT_BUDGET_FRESH_TTL_MS = 25 * 1000;
14130
14165
  var CLAUDE_INSTRUCTIONS = [
14131
14166
  "Codex is an AI coding agent (OpenAI) running in a separate session on the same machine.",
14132
14167
  "",
@@ -14190,6 +14225,10 @@ class ClaudeAdapter extends EventEmitter {
14190
14225
  monotonicNow;
14191
14226
  deliveredMessageIds = new Map;
14192
14227
  budgetSnapshot = null;
14228
+ budgetFreshTtlMs;
14229
+ wallNow;
14230
+ requestFreshSnapshot = null;
14231
+ pendingBudgetRefresh = null;
14193
14232
  constructor(logFile = new StateDirResolver().logFile, options = {}) {
14194
14233
  super();
14195
14234
  this.logFile = logFile;
@@ -14206,6 +14245,8 @@ class ClaudeAdapter extends EventEmitter {
14206
14245
  this.dedupeCapacity = positiveIntegerOr(options.dedupeCapacity, DEFAULT_DEDUPE_CAPACITY);
14207
14246
  this.dedupeTtlMs = positiveIntegerOr(options.dedupeTtlMs, DEFAULT_DEDUPE_TTL_MS);
14208
14247
  this.monotonicNow = options.now ?? (() => performance.now());
14248
+ this.budgetFreshTtlMs = positiveIntegerOr(options.budgetFreshTtlMs, parsePositiveIntegerEnv("AGENTBRIDGE_BUDGET_FRESH_TTL_SEC", DEFAULT_BUDGET_FRESH_TTL_MS / 1000) * 1000);
14249
+ this.wallNow = options.wallNow ?? (() => Date.now());
14209
14250
  this.server = new Server({ name: "agentbridge", version: "0.1.0" }, {
14210
14251
  capabilities: {
14211
14252
  experimental: { "claude/channel": {} },
@@ -14233,6 +14274,9 @@ class ClaudeAdapter extends EventEmitter {
14233
14274
  setBudgetSnapshot(snapshot) {
14234
14275
  this.budgetSnapshot = snapshot;
14235
14276
  }
14277
+ setRequestFreshSnapshot(fetcher) {
14278
+ this.requestFreshSnapshot = fetcher;
14279
+ }
14236
14280
  async pushNotification(message) {
14237
14281
  this.log(`pushNotification (instance=${this.instanceId}, msgId=${message.id}, len=${message.content.length})`);
14238
14282
  if (!this.rememberDelivery(message))
@@ -14356,21 +14400,21 @@ Codex: ${msg.content}`;
14356
14400
  `);
14357
14401
  const noticeText = notices.map((notice) => `WARNING: ${notice}`).join(`
14358
14402
  `);
14359
- const parts = [];
14403
+ const parts2 = [];
14360
14404
  if (count > 0) {
14361
- parts.push(`[${count} new message${count > 1 ? "s" : ""} from Codex]
14405
+ parts2.push(`[${count} new message${count > 1 ? "s" : ""} from Codex]
14362
14406
  chat_id: ${this.sessionId}`);
14363
14407
  }
14364
14408
  if (noticeText)
14365
- parts.push(noticeText);
14409
+ parts2.push(noticeText);
14366
14410
  if (formatted)
14367
- parts.push(formatted);
14411
+ parts2.push(formatted);
14368
14412
  this.log(`get_messages returning ${count} message(s) ` + `(instance=${this.instanceId}, dropped=${dropped}, oversized=${oversized}, oversizedBytes=${oversizedBytes})`);
14369
14413
  return {
14370
14414
  content: [
14371
14415
  {
14372
14416
  type: "text",
14373
- text: parts.join(`
14417
+ text: parts2.join(`
14374
14418
 
14375
14419
  `)
14376
14420
  }
@@ -14509,13 +14553,42 @@ chat_id: ${this.sessionId}`);
14509
14553
  content: [{ type: "text", text: `Resume acknowledged (resume_id=${resumeIdRaw}, status=${status}).` }]
14510
14554
  };
14511
14555
  }
14512
- handleGetBudget() {
14513
- this.log(`get_budget called (instance=${this.instanceId}, hasSnapshot=${this.budgetSnapshot !== null})`);
14514
- const text = this.budgetSnapshot ? renderBudgetSnapshot(this.budgetSnapshot) : BUDGET_UNAVAILABLE_TEXT;
14556
+ async handleGetBudget() {
14557
+ let snapshot = this.budgetSnapshot;
14558
+ const fresh = snapshot !== null && this.isBudgetSnapshotFresh(snapshot);
14559
+ this.log(`get_budget called (instance=${this.instanceId}, hasSnapshot=${snapshot !== null}, fresh=${fresh})`);
14560
+ if (!fresh && this.requestFreshSnapshot) {
14561
+ const refreshed = await this.refreshBudgetSnapshot();
14562
+ snapshot = refreshed ?? this.budgetSnapshot;
14563
+ }
14564
+ const text = snapshot ? renderBudgetSnapshot(snapshot) : BUDGET_UNAVAILABLE_TEXT;
14515
14565
  return {
14516
14566
  content: [{ type: "text", text }]
14517
14567
  };
14518
14568
  }
14569
+ isBudgetSnapshotFresh(snapshot) {
14570
+ if (!snapshot.updatedAt || snapshot.updatedAt <= 0)
14571
+ return false;
14572
+ const ageMs = this.wallNow() - snapshot.updatedAt * 1000;
14573
+ return ageMs < this.budgetFreshTtlMs;
14574
+ }
14575
+ refreshBudgetSnapshot() {
14576
+ if (!this.requestFreshSnapshot)
14577
+ return Promise.resolve(null);
14578
+ if (!this.pendingBudgetRefresh) {
14579
+ this.pendingBudgetRefresh = this.requestFreshSnapshot().then((snapshot) => {
14580
+ if (snapshot)
14581
+ this.budgetSnapshot = snapshot;
14582
+ return snapshot;
14583
+ }).catch((error2) => {
14584
+ this.log(`get_budget refresh failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
14585
+ return null;
14586
+ }).finally(() => {
14587
+ this.pendingBudgetRefresh = null;
14588
+ });
14589
+ }
14590
+ return this.pendingBudgetRefresh;
14591
+ }
14519
14592
  async handleReply(args) {
14520
14593
  const text = args?.text;
14521
14594
  if (!text) {
@@ -14633,11 +14706,11 @@ function defineNumber(value, fallback) {
14633
14706
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
14634
14707
  }
14635
14708
  var BUILD_INFO = Object.freeze({
14636
- version: defineString("0.1.20", "0.0.0-source"),
14637
- commit: defineString("d6034c6", "source"),
14709
+ version: defineString("0.1.22", "0.0.0-source"),
14710
+ commit: defineString("f5e401b", "source"),
14638
14711
  bundle: defineBundle("plugin"),
14639
14712
  contractVersion: defineNumber(1, CONTRACT_VERSION),
14640
- codeHash: defineString("d2425ba159fe", "source")
14713
+ codeHash: defineString("9688463029af", "source")
14641
14714
  });
14642
14715
  function sameRuntimeContract(a, b) {
14643
14716
  if (!a || !b)
@@ -14841,9 +14914,26 @@ class DaemonClient extends EventEmitter2 {
14841
14914
  send: () => this.send({ type: "probe_incumbent" })
14842
14915
  });
14843
14916
  }
14917
+ async requestBudgetRefresh(timeoutMs = 3000) {
14918
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
14919
+ return null;
14920
+ }
14921
+ const requestId = `budget_${Date.now()}_${this.nextRequestId++}`;
14922
+ return this.awaitTypedResponse({
14923
+ key: "budget_refresh",
14924
+ successEvent: "budgetRefresh",
14925
+ match: (payload) => payload.requestId === requestId,
14926
+ successValue: (payload) => payload.snapshot,
14927
+ failValue: null,
14928
+ timeoutMs,
14929
+ send: () => this.send({ type: "request_budget_refresh", requestId })
14930
+ });
14931
+ }
14844
14932
  awaitTypedResponse(opts) {
14845
- const { key, successEvent, successValue, failValue, timeoutMs, send } = opts;
14933
+ const { key, successEvent, successValue, failValue, timeoutMs, send, match } = opts;
14846
14934
  const onSuccess = (payload) => {
14935
+ if (match && !match(payload))
14936
+ return;
14847
14937
  this.pendingEventWaiters.settle(key, successValue(payload));
14848
14938
  };
14849
14939
  const onRejected = () => {
@@ -14944,6 +15034,9 @@ class DaemonClient extends EventEmitter2 {
14944
15034
  case "incumbent_status":
14945
15035
  this.emit("incumbentStatus", { connected: message.connected, alive: message.alive });
14946
15036
  return;
15037
+ case "budget_refresh":
15038
+ this.emit("budgetRefresh", { requestId: message.requestId, snapshot: message.snapshot });
15039
+ return;
14947
15040
  }
14948
15041
  };
14949
15042
  ws.onclose = (event) => {
@@ -15660,6 +15753,8 @@ import { join as join2 } from "path";
15660
15753
  var DEFAULT_BUDGET_CONFIG = {
15661
15754
  enabled: true,
15662
15755
  pollSeconds: 300,
15756
+ budgetFreshTtlSec: 25,
15757
+ idleAdviceActivityWindowSec: 600,
15663
15758
  pauseAt: 90,
15664
15759
  resumeBelow: 30,
15665
15760
  syncDriftPct: 10,
@@ -15721,7 +15816,7 @@ function findShapeViolation(raw) {
15721
15816
  if (!isRecord(budget)) {
15722
15817
  return "budget is present but not an object";
15723
15818
  }
15724
- const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct"];
15819
+ const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct", "budgetFreshTtlSec", "idleAdviceActivityWindowSec"];
15725
15820
  for (const key of numericKeys) {
15726
15821
  if (key in budget && !isCoercibleNumber(budget[key])) {
15727
15822
  return `budget.${key} is present but not a number`;
@@ -15775,7 +15870,7 @@ function hasCustomDecisionValues(config2) {
15775
15870
  const d = DEFAULT_CONFIG;
15776
15871
  const b = config2.budget;
15777
15872
  const db = d.budget;
15778
- 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.maximize.admissionAt !== db.maximize.admissionAt || b.maximize.wrapUpQuota !== db.maximize.wrapUpQuota || b.allocation.minRunwayRatio !== db.allocation.minRunwayRatio || b.allocation.minRunwayGapHours !== db.allocation.minRunwayGapHours;
15873
+ 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.budgetFreshTtlSec !== db.budgetFreshTtlSec || b.idleAdviceActivityWindowSec !== db.idleAdviceActivityWindowSec || 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.maximize.admissionAt !== db.maximize.admissionAt || b.maximize.wrapUpQuota !== db.maximize.wrapUpQuota || b.allocation.minRunwayRatio !== db.allocation.minRunwayRatio || b.allocation.minRunwayGapHours !== db.allocation.minRunwayGapHours;
15779
15874
  }
15780
15875
  function normalizeInteger(value, fallback) {
15781
15876
  if (typeof value === "number" && Number.isFinite(value))
@@ -15871,6 +15966,8 @@ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
15871
15966
  return {
15872
15967
  enabled: normalizeBoolean(budget.enabled, fallback.enabled),
15873
15968
  pollSeconds: normalizeBoundedInteger(budget.pollSeconds, fallback.pollSeconds, 5, 3600),
15969
+ budgetFreshTtlSec: normalizeBoundedInteger(budget.budgetFreshTtlSec, fallback.budgetFreshTtlSec, 1, 300),
15970
+ idleAdviceActivityWindowSec: normalizeBoundedInteger(budget.idleAdviceActivityWindowSec, fallback.idleAdviceActivityWindowSec, 0, 86400),
15874
15971
  pauseAt,
15875
15972
  resumeBelow,
15876
15973
  syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, fallback.syncDriftPct, 1, 100),
@@ -15884,6 +15981,37 @@ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
15884
15981
  allocation: normalizeAllocationConfig(budget.allocation, fallback.allocation)
15885
15982
  };
15886
15983
  }
15984
+ function applyBudgetEnvOverrides(budget, env = process.env) {
15985
+ const overlay = {
15986
+ enabled: env.AGENTBRIDGE_BUDGET_ENABLED ?? budget.enabled,
15987
+ pollSeconds: env.AGENTBRIDGE_BUDGET_POLL_SECONDS ?? budget.pollSeconds,
15988
+ budgetFreshTtlSec: env.AGENTBRIDGE_BUDGET_FRESH_TTL_SEC ?? budget.budgetFreshTtlSec,
15989
+ idleAdviceActivityWindowSec: env.AGENTBRIDGE_BUDGET_IDLE_ADVICE_ACTIVITY_WINDOW_SEC ?? budget.idleAdviceActivityWindowSec,
15990
+ pauseAt: env.AGENTBRIDGE_BUDGET_PAUSE_AT ?? budget.pauseAt,
15991
+ resumeBelow: env.AGENTBRIDGE_BUDGET_RESUME_BELOW ?? budget.resumeBelow,
15992
+ syncDriftPct: env.AGENTBRIDGE_BUDGET_SYNC_DRIFT_PCT ?? budget.syncDriftPct,
15993
+ parallel: {
15994
+ minRemainingPct: env.AGENTBRIDGE_BUDGET_PARALLEL_MIN_REMAINING_PCT ?? budget.parallel.minRemainingPct,
15995
+ timeWindowSec: env.AGENTBRIDGE_BUDGET_PARALLEL_TIME_WINDOW_SEC ?? budget.parallel.timeWindowSec
15996
+ },
15997
+ codexTierControl: env.AGENTBRIDGE_BUDGET_CODEX_TIER_CONTROL ?? budget.codexTierControl,
15998
+ codexTiers: budget.codexTiers,
15999
+ maximize: {
16000
+ targetUtil: env.AGENTBRIDGE_BUDGET_TARGET_UTIL ?? budget.maximize.targetUtil,
16001
+ reserveSlopePctPerHour: env.AGENTBRIDGE_BUDGET_RESERVE_SLOPE_PCT_PER_HOUR ?? budget.maximize.reserveSlopePctPerHour,
16002
+ reserveMaxPct: env.AGENTBRIDGE_BUDGET_RESERVE_MAX_PCT ?? budget.maximize.reserveMaxPct,
16003
+ finishingHorizonMinutes: env.AGENTBRIDGE_BUDGET_FINISHING_HORIZON_MINUTES ?? budget.maximize.finishingHorizonMinutes,
16004
+ resumeHysteresisPct: env.AGENTBRIDGE_BUDGET_RESUME_HYSTERESIS_PCT ?? budget.maximize.resumeHysteresisPct,
16005
+ admissionAt: env.AGENTBRIDGE_BUDGET_ADMISSION_AT ?? budget.maximize.admissionAt,
16006
+ wrapUpQuota: env.AGENTBRIDGE_BUDGET_WRAP_UP_QUOTA ?? budget.maximize.wrapUpQuota
16007
+ },
16008
+ allocation: {
16009
+ minRunwayRatio: env.AGENTBRIDGE_BUDGET_MIN_RUNWAY_RATIO ?? budget.allocation.minRunwayRatio,
16010
+ minRunwayGapHours: env.AGENTBRIDGE_BUDGET_MIN_RUNWAY_GAP_HOURS ?? budget.allocation.minRunwayGapHours
16011
+ }
16012
+ };
16013
+ return normalizeBudgetConfig(overlay, budget);
16014
+ }
15887
16015
  function normalizeConfig(raw) {
15888
16016
  if (!isRecord(raw))
15889
16017
  return null;
@@ -16363,8 +16491,12 @@ var config2 = configService.loadOrDefault(processLogger.log);
16363
16491
  var CONTROL_PORT = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
16364
16492
  var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
16365
16493
  var CONTROL_WS_URL = daemonLifecycle.controlWsUrl;
16366
- var claude = new ClaudeAdapter(stateDir.logFile);
16494
+ var effectiveBudget = applyBudgetEnvOverrides(config2.budget);
16495
+ var claude = new ClaudeAdapter(stateDir.logFile, {
16496
+ budgetFreshTtlMs: effectiveBudget.budgetFreshTtlSec * 1000
16497
+ });
16367
16498
  var daemonClient = new DaemonClient(CONTROL_WS_URL, { identity: currentClientIdentity });
16499
+ claude.setRequestFreshSnapshot(() => daemonClient.requestBudgetRefresh());
16368
16500
  var shuttingDown = false;
16369
16501
  var daemonDisabled = false;
16370
16502
  var daemonDisabledReason = null;
@@ -29,11 +29,11 @@ function defineNumber(value, fallback) {
29
29
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
30
30
  }
31
31
  var BUILD_INFO = Object.freeze({
32
- version: defineString("0.1.20", "0.0.0-source"),
33
- commit: defineString("d6034c6", "source"),
32
+ version: defineString("0.1.22", "0.0.0-source"),
33
+ commit: defineString("f5e401b", "source"),
34
34
  bundle: defineBundle("plugin"),
35
35
  contractVersion: defineNumber(1, CONTRACT_VERSION),
36
- codeHash: defineString("d2425ba159fe", "source")
36
+ codeHash: defineString("9688463029af", "source")
37
37
  });
38
38
  function daemonStatusBuildInfo() {
39
39
  return { ...BUILD_INFO };
@@ -3478,6 +3478,8 @@ import { join as join4 } from "path";
3478
3478
  var DEFAULT_BUDGET_CONFIG = {
3479
3479
  enabled: true,
3480
3480
  pollSeconds: 300,
3481
+ budgetFreshTtlSec: 25,
3482
+ idleAdviceActivityWindowSec: 600,
3481
3483
  pauseAt: 90,
3482
3484
  resumeBelow: 30,
3483
3485
  syncDriftPct: 10,
@@ -3539,7 +3541,7 @@ function findShapeViolation(raw) {
3539
3541
  if (!isRecord(budget)) {
3540
3542
  return "budget is present but not an object";
3541
3543
  }
3542
- const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct"];
3544
+ const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct", "budgetFreshTtlSec", "idleAdviceActivityWindowSec"];
3543
3545
  for (const key of numericKeys) {
3544
3546
  if (key in budget && !isCoercibleNumber(budget[key])) {
3545
3547
  return `budget.${key} is present but not a number`;
@@ -3593,7 +3595,7 @@ function hasCustomDecisionValues(config) {
3593
3595
  const d = DEFAULT_CONFIG;
3594
3596
  const b = config.budget;
3595
3597
  const db = d.budget;
3596
- 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.maximize.admissionAt !== db.maximize.admissionAt || b.maximize.wrapUpQuota !== db.maximize.wrapUpQuota || b.allocation.minRunwayRatio !== db.allocation.minRunwayRatio || b.allocation.minRunwayGapHours !== db.allocation.minRunwayGapHours;
3598
+ 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.budgetFreshTtlSec !== db.budgetFreshTtlSec || b.idleAdviceActivityWindowSec !== db.idleAdviceActivityWindowSec || 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.maximize.admissionAt !== db.maximize.admissionAt || b.maximize.wrapUpQuota !== db.maximize.wrapUpQuota || b.allocation.minRunwayRatio !== db.allocation.minRunwayRatio || b.allocation.minRunwayGapHours !== db.allocation.minRunwayGapHours;
3597
3599
  }
3598
3600
  function normalizeInteger(value, fallback) {
3599
3601
  if (typeof value === "number" && Number.isFinite(value))
@@ -3689,6 +3691,8 @@ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
3689
3691
  return {
3690
3692
  enabled: normalizeBoolean(budget.enabled, fallback.enabled),
3691
3693
  pollSeconds: normalizeBoundedInteger(budget.pollSeconds, fallback.pollSeconds, 5, 3600),
3694
+ budgetFreshTtlSec: normalizeBoundedInteger(budget.budgetFreshTtlSec, fallback.budgetFreshTtlSec, 1, 300),
3695
+ idleAdviceActivityWindowSec: normalizeBoundedInteger(budget.idleAdviceActivityWindowSec, fallback.idleAdviceActivityWindowSec, 0, 86400),
3692
3696
  pauseAt,
3693
3697
  resumeBelow,
3694
3698
  syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, fallback.syncDriftPct, 1, 100),
@@ -3706,6 +3710,8 @@ function applyBudgetEnvOverrides(budget, env = process.env) {
3706
3710
  const overlay = {
3707
3711
  enabled: env.AGENTBRIDGE_BUDGET_ENABLED ?? budget.enabled,
3708
3712
  pollSeconds: env.AGENTBRIDGE_BUDGET_POLL_SECONDS ?? budget.pollSeconds,
3713
+ budgetFreshTtlSec: env.AGENTBRIDGE_BUDGET_FRESH_TTL_SEC ?? budget.budgetFreshTtlSec,
3714
+ idleAdviceActivityWindowSec: env.AGENTBRIDGE_BUDGET_IDLE_ADVICE_ACTIVITY_WINDOW_SEC ?? budget.idleAdviceActivityWindowSec,
3709
3715
  pauseAt: env.AGENTBRIDGE_BUDGET_PAUSE_AT ?? budget.pauseAt,
3710
3716
  resumeBelow: env.AGENTBRIDGE_BUDGET_RESUME_BELOW ?? budget.resumeBelow,
3711
3717
  syncDriftPct: env.AGENTBRIDGE_BUDGET_SYNC_DRIFT_PCT ?? budget.syncDriftPct,
@@ -3861,6 +3867,36 @@ function retryAfterMsForResume(resumeAfterEpoch, nowMs) {
3861
3867
  return remainingMs > 0 ? remainingMs : undefined;
3862
3868
  }
3863
3869
 
3870
+ // src/budget/format-time.ts
3871
+ var BEIJING_TZ = "Asia/Shanghai";
3872
+ function parts(epochSeconds, options) {
3873
+ const fmt = new Intl.DateTimeFormat("en-CA", {
3874
+ timeZone: BEIJING_TZ,
3875
+ hour12: false,
3876
+ ...options
3877
+ });
3878
+ const out = {};
3879
+ for (const part of fmt.formatToParts(new Date(epochSeconds * 1000))) {
3880
+ out[part.type] = part.value;
3881
+ }
3882
+ return out;
3883
+ }
3884
+ function formatBeijing(epochSeconds) {
3885
+ if (!epochSeconds || epochSeconds <= 0)
3886
+ return "\u672A\u77E5";
3887
+ const d = new Date(epochSeconds * 1000);
3888
+ if (Number.isNaN(d.getTime()))
3889
+ return "\u672A\u77E5";
3890
+ const p = parts(epochSeconds, {
3891
+ year: "numeric",
3892
+ month: "2-digit",
3893
+ day: "2-digit",
3894
+ hour: "2-digit",
3895
+ minute: "2-digit"
3896
+ });
3897
+ return `${p.year}-${p.month}-${p.day} ${p.hour}:${p.minute}`;
3898
+ }
3899
+
3864
3900
  // src/budget/types.ts
3865
3901
  var STALE_MAX_AGE_SEC = 600;
3866
3902
 
@@ -4149,14 +4185,12 @@ function pct2(value) {
4149
4185
  return `${Math.round(value * 10) / 10}%`;
4150
4186
  }
4151
4187
  function formatEpoch(epoch) {
4152
- if (!epoch || epoch <= 0)
4153
- return "\u672A\u77E5";
4154
- return new Date(epoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
4188
+ return formatBeijing(epoch);
4155
4189
  }
4156
4190
  function usageSummary(name, usage) {
4157
4191
  if (!usage)
4158
4192
  return `${AGENT_LABEL2[name]} \u672A\u77E5`;
4159
- return `${AGENT_LABEL2[name]} gate=${pct2(usage.gateUtil)} warn=${pct2(usage.warnUtil)} 5h\u91CD\u7F6E=${formatEpoch(usage.fiveHour?.resetEpoch ?? 0)}`;
4193
+ return `${AGENT_LABEL2[name]} gate=${pct2(usage.gateUtil)} warn=${pct2(usage.warnUtil)} 5h\u91CD\u7F6E=${formatEpoch(usage.fiveHour?.resetEpoch ?? 0)}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09`;
4160
4194
  }
4161
4195
  function resumeAfterEpoch(claude, codex, cfg, now) {
4162
4196
  const epochs = [
@@ -4452,7 +4486,7 @@ function pct3(value) {
4452
4486
  return `${Math.round(value * 10) / 10}%`;
4453
4487
  }
4454
4488
  function formatEpoch2(epoch) {
4455
- return new Date(epoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
4489
+ return formatBeijing(epoch);
4456
4490
  }
4457
4491
  var INITIAL_FINGERPRINT_STATE = {
4458
4492
  side: null,
@@ -4500,7 +4534,7 @@ function activeSideReason(agent, usage, cfg, now) {
4500
4534
  if (!usage)
4501
4535
  return `${AGENT_LABEL3[agent]} \u63A2\u6D4B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u4FDD\u6301\u4E0A\u4E00\u8F6E\u9884\u7B97\u5E72\u9884`;
4502
4536
  if (usage.rateLimitedUntil > now) {
4503
- return `${AGENT_LABEL3[agent]} \u63A2\u9488\u88AB\u9650\u6D41\u81F3 ${formatEpoch2(usage.rateLimitedUntil)}`;
4537
+ return `${AGENT_LABEL3[agent]} \u63A2\u9488\u88AB\u9650\u6D41\u81F3 ${formatEpoch2(usage.rateLimitedUntil)}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09`;
4504
4538
  }
4505
4539
  const decision = agentShouldPause(agent, usage, cfg, now);
4506
4540
  if (decision.pause)
@@ -4643,7 +4677,7 @@ function admissionReason(side, state, cfg) {
4643
4677
  if (!usage)
4644
4678
  return `${AGENT_LABEL3[agent]} \u63A2\u6D4B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u4FDD\u6301\u4E0A\u4E00\u8F6E\u6536\u5C3E\u4FDD\u62A4`;
4645
4679
  if (usage.rateLimitedUntil > state.now) {
4646
- return `${AGENT_LABEL3[agent]} \u63A2\u9488\u88AB\u9650\u6D41\u81F3 ${formatEpoch2(usage.rateLimitedUntil)}\uFF0C\u4FDD\u6301\u6536\u5C3E\u4FDD\u62A4`;
4680
+ return `${AGENT_LABEL3[agent]} \u63A2\u9488\u88AB\u9650\u6D41\u81F3 ${formatEpoch2(usage.rateLimitedUntil)}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09\uFF0C\u4FDD\u6301\u6536\u5C3E\u4FDD\u62A4`;
4647
4681
  }
4648
4682
  const decision = agentShouldAdmitClose(agent, usage, cfg, state.now);
4649
4683
  if (decision.admitClose)
@@ -4818,6 +4852,7 @@ class BudgetCoordinator {
4818
4852
  resumeSignals;
4819
4853
  adviceCooldown;
4820
4854
  isCodexTurnActive;
4855
+ hasRecentActivity;
4821
4856
  timer = null;
4822
4857
  running = false;
4823
4858
  fpState = INITIAL_FINGERPRINT_STATE;
@@ -4848,6 +4883,7 @@ class BudgetCoordinator {
4848
4883
  log: this.log
4849
4884
  });
4850
4885
  this.isCodexTurnActive = options.isCodexTurnActive ?? (() => false);
4886
+ this.hasRecentActivity = options.hasRecentActivity ?? (() => true);
4851
4887
  }
4852
4888
  async start() {
4853
4889
  if (this.running || !this.config.enabled)
@@ -4880,6 +4916,24 @@ class BudgetCoordinator {
4880
4916
  getSnapshot() {
4881
4917
  return this.latestSnapshot;
4882
4918
  }
4919
+ async refreshSnapshotReadonly() {
4920
+ let usage;
4921
+ try {
4922
+ usage = await this.source.fetchBoth();
4923
+ } catch (error) {
4924
+ this.log(`budget readonly refresh failed: ${error instanceof Error ? error.message : String(error)}`);
4925
+ return null;
4926
+ }
4927
+ if (!usage)
4928
+ return null;
4929
+ const now = this.now();
4930
+ const runway = {
4931
+ claude: agentRunway(usage.claude, now),
4932
+ codex: agentRunway(usage.codex, now)
4933
+ };
4934
+ const state = computeBudgetState(usage.claude, usage.codex, this.config, now, runway);
4935
+ return this.toSnapshot(state, runway);
4936
+ }
4883
4937
  getResumeCandidate() {
4884
4938
  const { detail, ...rest } = this.resumeCandidate;
4885
4939
  return detail ? {
@@ -4997,6 +5051,12 @@ class BudgetCoordinator {
4997
5051
  this.fpState = { ...this.fpState, fingerprint: null };
4998
5052
  return;
4999
5053
  }
5054
+ const activityWindowSec = this.config.idleAdviceActivityWindowSec;
5055
+ if (activityWindowSec > 0 && !this.hasRecentActivity(activityWindowSec)) {
5056
+ this.log(`budget advise suppressed: no agent activity in last ${activityWindowSec}s`);
5057
+ this.fpState = { ...this.fpState, fingerprint: null };
5058
+ return;
5059
+ }
5000
5060
  if (effect.phase === "underutilized") {
5001
5061
  if (!this.adviceCooldown.tryAcquire("underutilization", state.now))
5002
5062
  return;
@@ -6767,7 +6827,8 @@ function ensureBudgetCoordinatorStarted() {
6767
6827
  });
6768
6828
  },
6769
6829
  resumeSignals: readResumeSignals,
6770
- isCodexTurnActive: () => codex.turnInProgress
6830
+ isCodexTurnActive: () => codex.turnInProgress,
6831
+ hasRecentActivity: (windowSec) => codex.turnInProgress || Date.now() - lastActivityEpochMs <= windowSec * 1000
6771
6832
  });
6772
6833
  }
6773
6834
  budgetCoordinator.start();
@@ -6778,13 +6839,13 @@ function stopBudgetCoordinator() {
6778
6839
  function budgetPauseGateError() {
6779
6840
  const snapshot = budgetCoordinator?.getSnapshot() ?? null;
6780
6841
  const reason = snapshot?.pauseReason ?? "Codex \u4FA7\u989D\u5EA6\u63A5\u8FD1\u8017\u5C3D";
6781
- const resumeAt = snapshot?.resumeAfterEpoch ? new Date(snapshot.resumeAfterEpoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z") : null;
6842
+ const resumeAt = snapshot?.resumeAfterEpoch ? `${formatBeijing(snapshot.resumeAfterEpoch)}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09` : null;
6782
6843
  const sideHint = snapshot?.pauseSide === "both" ? "\u53CC\u4FA7\u989D\u5EA6\u5747\u5DF2\u8017\u5C3D\uFF0C\u8BF7\u5199 checkpoint \u7B49\u5F85\u5237\u65B0" : "\u4F60\u53EF\u7EE7\u7EED solo \u63A8\u8FDB\u53EF\u72EC\u7ACB\u90E8\u5206\uFF0C\u5E76\u5199 checkpoint \u6807\u6CE8\u5206\u5DE5\u65AD\u70B9";
6783
6844
  const reopenText = `Codex \u4FA7\u5404\u7A97\u53E3 util \u56DE\u843D\u81F3\u52A8\u6001\u6682\u505C\u7EBF \u2212 ${BUDGET_CONFIG.maximize.resumeHysteresisPct}% \u4EE5\u4E0B\u6216\u5BF9\u5E94\u7A97\u53E3\u5237\u65B0\u540E\u95F8\u95E8\u81EA\u52A8\u653E\u5F00`;
6784
6845
  return `\u9884\u7B97\u6682\u505C\uFF08\u95F8\u95E8\u5173\u95ED\uFF09\uFF0C\u5DF2\u62D2\u7EDD\u8F6C\u53D1\uFF1A${reason}\u3002` + reopenText + (resumeAt ? `\uFF08\u9884\u8BA1\u6062\u590D ${resumeAt}\uFF0C\u4EE5\u5B9E\u6D4B\u4E3A\u51C6\uFF1B\u63D0\u524D\u5237\u65B0\u4F1A\u66F4\u65E9\u89E3\u9664\uFF09` : "") + `\u3002\u6536\u5230 RESUME \u901A\u77E5\u524D\u8BF7\u52FF\u91CD\u8BD5\u5411 Codex \u53D1\u9001 reply\uFF1B${sideHint}\u3002`;
6785
6846
  }
6786
6847
  function budgetAdmissionGateError(windowResetEpoch, wrapUpLeft, quotaExhausted) {
6787
- const resetAt = windowResetEpoch > 0 ? new Date(windowResetEpoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z") : "\u672A\u77E5";
6848
+ const resetAt = windowResetEpoch > 0 ? `${formatBeijing(windowResetEpoch)}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09` : "\u672A\u77E5";
6788
6849
  const quota = BUDGET_CONFIG.maximize.wrapUpQuota;
6789
6850
  if (quotaExhausted) {
6790
6851
  return `\u989D\u5EA6\u7A97\u53E3\u6536\u5C3E\u4FDD\u62A4\u4E2D\uFF08admission-closed\uFF09\uFF1A\u672C\u7A97\u53E3 wrap-up \u914D\u989D\uFF08\u6BCF\u7A97\u53E3 ${quota} \u4E2A\uFF09\u5DF2\u7528\u5C3D\uFF0C\u5DF2\u62D2\u7EDD\u8F6C\u53D1\u3002` + `\u8BF7\u52FF\u518D\u6D3E\u65B0\u4EFB\u52A1\uFF1B\u5199 checkpoint\uFF0C\u7B49\u989D\u5EA6\u7A97\u53E3\u5237\u65B0\uFF08\u7EA6 ${resetAt}\uFF09\u540E\u518D\u7EE7\u7EED\u3002`;
@@ -6896,6 +6957,7 @@ codex.on("steerFailed", ({ requestId, reason }) => {
6896
6957
  });
6897
6958
  codex.on("steerAccepted", ({ requestId }) => {
6898
6959
  log("Steer accepted by app-server");
6960
+ recordAgentActivity();
6899
6961
  const dispatch = pendingSteerDispatches.get(requestId);
6900
6962
  pendingSteerDispatches.delete(requestId);
6901
6963
  if (dispatch?.requireReply) {
@@ -6965,13 +7027,19 @@ codex.on("turnTrackingReset", (reason) => {
6965
7027
  pendingSteerDispatches.clear();
6966
7028
  resumeInjectionQueue.onTurnTrackingReset();
6967
7029
  });
7030
+ var lastActivityEpochMs = 0;
7031
+ function recordAgentActivity() {
7032
+ lastActivityEpochMs = Date.now();
7033
+ }
6968
7034
  codex.on("turnStarted", () => {
6969
7035
  log("Codex turn started");
7036
+ recordAgentActivity();
6970
7037
  emitToClaude(systemMessage("system_turn_started", "\u23F3 Codex is working on the current task. Wait for completion before sending a reply."));
6971
7038
  });
6972
7039
  codex.on("agentMessage", (msg) => {
6973
7040
  if (msg.source !== "codex")
6974
7041
  return;
7042
+ recordAgentActivity();
6975
7043
  const route = routeCodexMessage(msg.content, {
6976
7044
  mode: FILTER_MODE,
6977
7045
  replyArmed: replyTracker.isArmed,
@@ -7189,6 +7257,11 @@ function handleControlMessage(ws, raw) {
7189
7257
  log(`handleProbeIncumbent threw for #${ws.data.clientId}: ${err?.message ?? err}`);
7190
7258
  });
7191
7259
  return;
7260
+ case "request_budget_refresh":
7261
+ handleRequestBudgetRefresh(ws, message.requestId).catch((err) => {
7262
+ log(`handleRequestBudgetRefresh threw for #${ws.data.clientId}: ${err?.message ?? err}`);
7263
+ });
7264
+ return;
7192
7265
  case "claude_to_codex": {
7193
7266
  handleClaudeToCodex(ws, message).catch((err) => {
7194
7267
  log(`handleClaudeToCodex threw for request ${message.requestId}: ${err?.message ?? err}`);
@@ -7547,6 +7620,11 @@ async function handleProbeIncumbent(ws) {
7547
7620
  alive: stillConnected && alive
7548
7621
  });
7549
7622
  }
7623
+ async function handleRequestBudgetRefresh(ws, requestId) {
7624
+ const snapshot = budgetCoordinator ? await budgetCoordinator.refreshSnapshotReadonly() : null;
7625
+ log(`request_budget_refresh from #${ws.data.clientId}: ${snapshot ? "fresh" : "unavailable"}`);
7626
+ sendProtocolMessage(ws, { type: "budget_refresh", requestId, snapshot });
7627
+ }
7550
7628
  async function probeLiveness2(ws, timeoutMs) {
7551
7629
  return probeLiveness({
7552
7630
  get readyState() {