@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +28 -1
- package/README.zh-CN.md +41 -7
- package/dist/cli.js +143 -32
- package/dist/daemon.js +1421 -197
- package/package.json +1 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/scripts/health-check.sh +83 -0
- package/plugins/agentbridge/server/bridge-server.js +185 -14
- package/plugins/agentbridge/server/daemon.js +1421 -197
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
+
var __require = import.meta.require;
|
|
3
4
|
|
|
4
5
|
// src/daemon.ts
|
|
5
|
-
import { rmSync as rmSync2 } from "fs";
|
|
6
|
+
import { existsSync as existsSync8, realpathSync as realpathSync2, rmSync as rmSync2 } from "fs";
|
|
7
|
+
import { homedir as homedir5 } from "os";
|
|
8
|
+
import { join as join11 } from "path";
|
|
6
9
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
7
10
|
|
|
8
11
|
// src/contract-version.ts
|
|
@@ -26,11 +29,11 @@ function defineNumber(value, fallback) {
|
|
|
26
29
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
27
30
|
}
|
|
28
31
|
var BUILD_INFO = Object.freeze({
|
|
29
|
-
version: defineString("0.1.
|
|
30
|
-
commit: defineString("
|
|
32
|
+
version: defineString("0.1.17", "0.0.0-source"),
|
|
33
|
+
commit: defineString("0d1e1bd", "source"),
|
|
31
34
|
bundle: defineBundle("plugin"),
|
|
32
35
|
contractVersion: defineNumber(1, CONTRACT_VERSION),
|
|
33
|
-
codeHash: defineString("
|
|
36
|
+
codeHash: defineString("c22387f3269f", "source")
|
|
34
37
|
});
|
|
35
38
|
function daemonStatusBuildInfo() {
|
|
36
39
|
return { ...BUILD_INFO };
|
|
@@ -3402,7 +3405,17 @@ var DEFAULT_BUDGET_CONFIG = {
|
|
|
3402
3405
|
balanced: { effort: "medium" },
|
|
3403
3406
|
eco: { effort: "low" }
|
|
3404
3407
|
},
|
|
3405
|
-
|
|
3408
|
+
maximize: {
|
|
3409
|
+
targetUtil: 98,
|
|
3410
|
+
reserveSlopePctPerHour: 0.4,
|
|
3411
|
+
reserveMaxPct: 7,
|
|
3412
|
+
finishingHorizonMinutes: 30,
|
|
3413
|
+
resumeHysteresisPct: 5
|
|
3414
|
+
},
|
|
3415
|
+
allocation: {
|
|
3416
|
+
minRunwayRatio: 50,
|
|
3417
|
+
minRunwayGapHours: 2
|
|
3418
|
+
}
|
|
3406
3419
|
};
|
|
3407
3420
|
var DEFAULT_CONFIG = {
|
|
3408
3421
|
version: "1.0",
|
|
@@ -3455,6 +3468,34 @@ function findShapeViolation(raw) {
|
|
|
3455
3468
|
}
|
|
3456
3469
|
}
|
|
3457
3470
|
}
|
|
3471
|
+
if ("maximize" in budget) {
|
|
3472
|
+
const maximize = budget.maximize;
|
|
3473
|
+
if (!isRecord(maximize)) {
|
|
3474
|
+
return "budget.maximize is present but not an object";
|
|
3475
|
+
}
|
|
3476
|
+
for (const key of [
|
|
3477
|
+
"targetUtil",
|
|
3478
|
+
"reserveSlopePctPerHour",
|
|
3479
|
+
"reserveMaxPct",
|
|
3480
|
+
"finishingHorizonMinutes",
|
|
3481
|
+
"resumeHysteresisPct"
|
|
3482
|
+
]) {
|
|
3483
|
+
if (key in maximize && !isCoercibleNumber(maximize[key])) {
|
|
3484
|
+
return `budget.maximize.${key} is present but not a number`;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
if ("allocation" in budget) {
|
|
3489
|
+
const allocation = budget.allocation;
|
|
3490
|
+
if (!isRecord(allocation)) {
|
|
3491
|
+
return "budget.allocation is present but not an object";
|
|
3492
|
+
}
|
|
3493
|
+
for (const key of ["minRunwayRatio", "minRunwayGapHours"]) {
|
|
3494
|
+
if (key in allocation && !isCoercibleNumber(allocation[key])) {
|
|
3495
|
+
return `budget.allocation.${key} is present but not a number`;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3458
3499
|
}
|
|
3459
3500
|
return null;
|
|
3460
3501
|
}
|
|
@@ -3462,7 +3503,7 @@ function hasCustomDecisionValues(config) {
|
|
|
3462
3503
|
const d = DEFAULT_CONFIG;
|
|
3463
3504
|
const b = config.budget;
|
|
3464
3505
|
const db = d.budget;
|
|
3465
|
-
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;
|
|
3506
|
+
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;
|
|
3466
3507
|
}
|
|
3467
3508
|
function normalizeInteger(value, fallback) {
|
|
3468
3509
|
if (typeof value === "number" && Number.isFinite(value))
|
|
@@ -3480,8 +3521,41 @@ function normalizeBoundedInteger(value, fallback, min, max) {
|
|
|
3480
3521
|
return fallback;
|
|
3481
3522
|
return parsed;
|
|
3482
3523
|
}
|
|
3483
|
-
function
|
|
3484
|
-
|
|
3524
|
+
function normalizeBoundedNumber(value, fallback, min, max) {
|
|
3525
|
+
let parsed;
|
|
3526
|
+
if (typeof value === "number") {
|
|
3527
|
+
parsed = value;
|
|
3528
|
+
} else if (typeof value === "string" && value.trim() !== "") {
|
|
3529
|
+
parsed = Number(value);
|
|
3530
|
+
} else {
|
|
3531
|
+
return fallback;
|
|
3532
|
+
}
|
|
3533
|
+
if (!Number.isFinite(parsed))
|
|
3534
|
+
return fallback;
|
|
3535
|
+
if (parsed < min || parsed > max)
|
|
3536
|
+
return fallback;
|
|
3537
|
+
return parsed;
|
|
3538
|
+
}
|
|
3539
|
+
function normalizeMaximizeConfig(raw, pauseAt, fallback = DEFAULT_BUDGET_CONFIG.maximize) {
|
|
3540
|
+
const m = isRecord(raw) ? raw : {};
|
|
3541
|
+
const normalized = {
|
|
3542
|
+
targetUtil: normalizeBoundedInteger(m.targetUtil, fallback.targetUtil, 90, 99),
|
|
3543
|
+
reserveSlopePctPerHour: normalizeBoundedNumber(m.reserveSlopePctPerHour, fallback.reserveSlopePctPerHour, 0, 5),
|
|
3544
|
+
reserveMaxPct: normalizeBoundedInteger(m.reserveMaxPct, fallback.reserveMaxPct, 0, 30),
|
|
3545
|
+
finishingHorizonMinutes: normalizeBoundedInteger(m.finishingHorizonMinutes, fallback.finishingHorizonMinutes, 5, 180),
|
|
3546
|
+
resumeHysteresisPct: normalizeBoundedInteger(m.resumeHysteresisPct, fallback.resumeHysteresisPct, 1, 30)
|
|
3547
|
+
};
|
|
3548
|
+
if (normalized.targetUtil <= pauseAt) {
|
|
3549
|
+
return { ...DEFAULT_BUDGET_CONFIG.maximize };
|
|
3550
|
+
}
|
|
3551
|
+
return normalized;
|
|
3552
|
+
}
|
|
3553
|
+
function normalizeAllocationConfig(raw, fallback = DEFAULT_BUDGET_CONFIG.allocation) {
|
|
3554
|
+
const a = isRecord(raw) ? raw : {};
|
|
3555
|
+
return {
|
|
3556
|
+
minRunwayRatio: normalizeBoundedInteger(a.minRunwayRatio, fallback.minRunwayRatio, 10, 100),
|
|
3557
|
+
minRunwayGapHours: normalizeBoundedInteger(a.minRunwayGapHours, fallback.minRunwayGapHours, 1, 168)
|
|
3558
|
+
};
|
|
3485
3559
|
}
|
|
3486
3560
|
function normalizeBoolean(value, fallback) {
|
|
3487
3561
|
if (typeof value === "boolean")
|
|
@@ -3532,7 +3606,8 @@ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
|
|
|
3532
3606
|
},
|
|
3533
3607
|
codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
|
|
3534
3608
|
codexTiers,
|
|
3535
|
-
|
|
3609
|
+
maximize: normalizeMaximizeConfig(budget.maximize, pauseAt, fallback.maximize),
|
|
3610
|
+
allocation: normalizeAllocationConfig(budget.allocation, fallback.allocation)
|
|
3536
3611
|
};
|
|
3537
3612
|
}
|
|
3538
3613
|
function applyBudgetEnvOverrides(budget, env = process.env) {
|
|
@@ -3548,7 +3623,17 @@ function applyBudgetEnvOverrides(budget, env = process.env) {
|
|
|
3548
3623
|
},
|
|
3549
3624
|
codexTierControl: env.AGENTBRIDGE_BUDGET_CODEX_TIER_CONTROL ?? budget.codexTierControl,
|
|
3550
3625
|
codexTiers: budget.codexTiers,
|
|
3551
|
-
|
|
3626
|
+
maximize: {
|
|
3627
|
+
targetUtil: env.AGENTBRIDGE_BUDGET_TARGET_UTIL ?? budget.maximize.targetUtil,
|
|
3628
|
+
reserveSlopePctPerHour: env.AGENTBRIDGE_BUDGET_RESERVE_SLOPE_PCT_PER_HOUR ?? budget.maximize.reserveSlopePctPerHour,
|
|
3629
|
+
reserveMaxPct: env.AGENTBRIDGE_BUDGET_RESERVE_MAX_PCT ?? budget.maximize.reserveMaxPct,
|
|
3630
|
+
finishingHorizonMinutes: env.AGENTBRIDGE_BUDGET_FINISHING_HORIZON_MINUTES ?? budget.maximize.finishingHorizonMinutes,
|
|
3631
|
+
resumeHysteresisPct: env.AGENTBRIDGE_BUDGET_RESUME_HYSTERESIS_PCT ?? budget.maximize.resumeHysteresisPct
|
|
3632
|
+
},
|
|
3633
|
+
allocation: {
|
|
3634
|
+
minRunwayRatio: env.AGENTBRIDGE_BUDGET_MIN_RUNWAY_RATIO ?? budget.allocation.minRunwayRatio,
|
|
3635
|
+
minRunwayGapHours: env.AGENTBRIDGE_BUDGET_MIN_RUNWAY_GAP_HOURS ?? budget.allocation.minRunwayGapHours
|
|
3636
|
+
}
|
|
3552
3637
|
};
|
|
3553
3638
|
return normalizeBudgetConfig(overlay, budget);
|
|
3554
3639
|
}
|
|
@@ -3661,6 +3746,9 @@ class ConfigService {
|
|
|
3661
3746
|
}
|
|
3662
3747
|
}
|
|
3663
3748
|
|
|
3749
|
+
// src/budget/budget-coordinator.ts
|
|
3750
|
+
import { homedir as homedir2 } from "os";
|
|
3751
|
+
|
|
3664
3752
|
// src/budget/budget-gate.ts
|
|
3665
3753
|
function matchingGateReset(usage) {
|
|
3666
3754
|
if (!usage)
|
|
@@ -3672,28 +3760,225 @@ function matchingGateReset(usage) {
|
|
|
3672
3760
|
return 0;
|
|
3673
3761
|
return Math.min(...candidates.map((window) => window.resetEpoch));
|
|
3674
3762
|
}
|
|
3675
|
-
function
|
|
3763
|
+
function retryAfterMsForResume(resumeAfterEpoch, nowMs) {
|
|
3764
|
+
if (resumeAfterEpoch === null)
|
|
3765
|
+
return;
|
|
3766
|
+
const remainingMs = resumeAfterEpoch * 1000 - nowMs;
|
|
3767
|
+
return remainingMs > 0 ? remainingMs : undefined;
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
// src/budget/types.ts
|
|
3771
|
+
var STALE_MAX_AGE_SEC = 600;
|
|
3772
|
+
|
|
3773
|
+
// src/budget/budget-decision.ts
|
|
3774
|
+
var AGENT_LABEL = {
|
|
3775
|
+
claude: "Claude",
|
|
3776
|
+
codex: "Codex"
|
|
3777
|
+
};
|
|
3778
|
+
var WINDOW_LABEL = {
|
|
3779
|
+
fiveHour: "5h",
|
|
3780
|
+
weekly: "\u5468"
|
|
3781
|
+
};
|
|
3782
|
+
var WINDOW_KEYS = ["fiveHour", "weekly"];
|
|
3783
|
+
var MAX_TIME_TO_RESET_HOURS = 7 * 24;
|
|
3784
|
+
var FINISHING_MARGIN_MIN_PCT = 1;
|
|
3785
|
+
var FINISHING_MARGIN_MAX_PCT = 10;
|
|
3786
|
+
var DYNAMIC_LINE_CEILING_PCT = 99;
|
|
3787
|
+
function pct(value) {
|
|
3788
|
+
return `${Math.round(value * 10) / 10}%`;
|
|
3789
|
+
}
|
|
3790
|
+
function clamp(value, min, max) {
|
|
3791
|
+
return Math.min(Math.max(value, min), max);
|
|
3792
|
+
}
|
|
3793
|
+
function clampedTimeToResetHours(window, now) {
|
|
3794
|
+
return Math.min((window.resetEpoch - now) / 3600, MAX_TIME_TO_RESET_HOURS);
|
|
3795
|
+
}
|
|
3796
|
+
function isDecisionGrade(usage, now) {
|
|
3797
|
+
if (!usage)
|
|
3798
|
+
return false;
|
|
3799
|
+
const freshWindow = usage.fiveHour !== null && usage.fiveHour.resetEpoch > now || usage.weekly !== null && usage.weekly.resetEpoch > now;
|
|
3800
|
+
if (!freshWindow)
|
|
3801
|
+
return false;
|
|
3802
|
+
if (usage.fetchedAt > 0 && now - usage.fetchedAt > STALE_MAX_AGE_SEC)
|
|
3803
|
+
return false;
|
|
3804
|
+
return true;
|
|
3805
|
+
}
|
|
3806
|
+
function dynamicPauseAt(window, burnRatePctPerHour, cfg, now) {
|
|
3807
|
+
const m = cfg.maximize;
|
|
3808
|
+
const rawTimeToResetHours = (window.resetEpoch - now) / 3600;
|
|
3809
|
+
if (rawTimeToResetHours <= 0)
|
|
3810
|
+
return 100;
|
|
3811
|
+
const tH = clampedTimeToResetHours(window, now);
|
|
3812
|
+
const finishingMarginPct = clamp(burnRatePctPerHour * (m.finishingHorizonMinutes / 60), FINISHING_MARGIN_MIN_PCT, FINISHING_MARGIN_MAX_PCT);
|
|
3813
|
+
const projectedAtReset = window.util + burnRatePctPerHour * tH;
|
|
3814
|
+
if (projectedAtReset <= m.targetUtil) {
|
|
3815
|
+
if (window.util >= m.targetUtil)
|
|
3816
|
+
return "admission-closed";
|
|
3817
|
+
if (tH < m.finishingHorizonMinutes / 60 && window.util >= m.targetUtil - finishingMarginPct) {
|
|
3818
|
+
return "admission-closed";
|
|
3819
|
+
}
|
|
3820
|
+
return 100;
|
|
3821
|
+
}
|
|
3822
|
+
const reservePct = Math.min(m.reserveMaxPct, m.reserveSlopePctPerHour * tH);
|
|
3823
|
+
const line = m.targetUtil - finishingMarginPct - reservePct;
|
|
3824
|
+
return clamp(line, cfg.pauseAt, Math.max(cfg.pauseAt, DYNAMIC_LINE_CEILING_PCT));
|
|
3825
|
+
}
|
|
3826
|
+
function dynamicWindowVerdict(window, cfg, now) {
|
|
3827
|
+
const rate = confidentRate(window);
|
|
3828
|
+
if (rate === null)
|
|
3829
|
+
return { kind: "degraded" };
|
|
3830
|
+
if (window.resetEpoch <= now)
|
|
3831
|
+
return { kind: "degraded" };
|
|
3832
|
+
const line = dynamicPauseAt(window, rate, cfg, now);
|
|
3833
|
+
if (line === "admission-closed")
|
|
3834
|
+
return { kind: "admission-closed" };
|
|
3835
|
+
const projectedAtReset = window.util + rate * clampedTimeToResetHours(window, now);
|
|
3836
|
+
if (projectedAtReset <= cfg.maximize.targetUtil) {
|
|
3837
|
+
return { kind: "will-not-fill", projectedAtReset };
|
|
3838
|
+
}
|
|
3839
|
+
return { kind: "will-fill", line };
|
|
3840
|
+
}
|
|
3841
|
+
function confidentRate(window) {
|
|
3842
|
+
if (window.burnConfident !== true)
|
|
3843
|
+
return null;
|
|
3844
|
+
if (typeof window.burnRate !== "number" || !Number.isFinite(window.burnRate) || window.burnRate < 0) {
|
|
3845
|
+
return null;
|
|
3846
|
+
}
|
|
3847
|
+
return window.burnRate;
|
|
3848
|
+
}
|
|
3849
|
+
function maximizeWindowEntry(window, cfg, now) {
|
|
3850
|
+
const rate = confidentRate(window);
|
|
3851
|
+
if (rate === null) {
|
|
3852
|
+
return { blocks: window.util >= cfg.pauseAt, line: null, admission: false };
|
|
3853
|
+
}
|
|
3854
|
+
const line = dynamicPauseAt(window, rate, cfg, now);
|
|
3855
|
+
if (line === "admission-closed") {
|
|
3856
|
+
return { blocks: window.util >= cfg.pauseAt, line: null, admission: true };
|
|
3857
|
+
}
|
|
3858
|
+
return { blocks: window.util >= line, line, admission: false };
|
|
3859
|
+
}
|
|
3860
|
+
function maximizeWindowBlocksResume(window, cfg, now) {
|
|
3861
|
+
const rate = confidentRate(window);
|
|
3862
|
+
if (rate === null) {
|
|
3863
|
+
return window.util >= cfg.resumeBelow;
|
|
3864
|
+
}
|
|
3865
|
+
const line = dynamicPauseAt(window, rate, cfg, now);
|
|
3866
|
+
const hyst = cfg.maximize.resumeHysteresisPct;
|
|
3867
|
+
if (line === "admission-closed") {
|
|
3868
|
+
return window.util >= cfg.pauseAt - hyst;
|
|
3869
|
+
}
|
|
3870
|
+
if (line === 100)
|
|
3871
|
+
return false;
|
|
3872
|
+
return window.util >= line - hyst;
|
|
3873
|
+
}
|
|
3874
|
+
function freshWindows(usage, now) {
|
|
3875
|
+
const out = [];
|
|
3876
|
+
for (const key of WINDOW_KEYS) {
|
|
3877
|
+
const window = usage[key];
|
|
3878
|
+
if (window && window.resetEpoch > now)
|
|
3879
|
+
out.push({ key, window });
|
|
3880
|
+
}
|
|
3881
|
+
return out;
|
|
3882
|
+
}
|
|
3883
|
+
var NO_PAUSE = { pause: false, window: null, line: null, reason: "" };
|
|
3884
|
+
function fallbackPauseReason(agent, usage, cfg) {
|
|
3885
|
+
return `${AGENT_LABEL[agent]} gateUtil ${pct(usage.gateUtil)} \u2265 pauseAt ${pct(cfg.pauseAt)}\uFF08\u515C\u5E95\u5224\u636E\uFF09`;
|
|
3886
|
+
}
|
|
3887
|
+
function agentShouldPause(agent, usage, cfg, now) {
|
|
3888
|
+
if (!usage)
|
|
3889
|
+
return NO_PAUSE;
|
|
3890
|
+
if (!isDecisionGrade(usage, now))
|
|
3891
|
+
return NO_PAUSE;
|
|
3892
|
+
const windows = freshWindows(usage, now);
|
|
3893
|
+
if (windows.length === 0) {
|
|
3894
|
+
if (usage.gateUtil >= cfg.pauseAt) {
|
|
3895
|
+
return { pause: true, window: null, line: null, reason: fallbackPauseReason(agent, usage, cfg) };
|
|
3896
|
+
}
|
|
3897
|
+
return NO_PAUSE;
|
|
3898
|
+
}
|
|
3899
|
+
for (const { key, window } of windows) {
|
|
3900
|
+
const verdict = maximizeWindowEntry(window, cfg, now);
|
|
3901
|
+
if (verdict.blocks) {
|
|
3902
|
+
return {
|
|
3903
|
+
pause: true,
|
|
3904
|
+
window: key,
|
|
3905
|
+
line: verdict.line,
|
|
3906
|
+
reason: buildMaximizeReason(agent, key, window, verdict, cfg)
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
return NO_PAUSE;
|
|
3911
|
+
}
|
|
3912
|
+
function buildMaximizeReason(agent, key, window, verdict, cfg) {
|
|
3913
|
+
const head = `${AGENT_LABEL[agent]} ${WINDOW_LABEL[key]}\u7A97\u53E3 util ${pct(window.util)}`;
|
|
3914
|
+
if (verdict.line !== null) {
|
|
3915
|
+
const rate = window.burnRate;
|
|
3916
|
+
const rateText = typeof rate === "number" ? `\uFF0C\u71C3\u5C3D\u7387\u2248${pct(rate)}/h` : "";
|
|
3917
|
+
return `${head} \u2265 \u52A8\u6001\u6682\u505C\u7EBF ${pct(verdict.line)}${rateText}`;
|
|
3918
|
+
}
|
|
3919
|
+
if (verdict.admission) {
|
|
3920
|
+
return `${head} \u89E6\u53D1\u6536\u5C3E\u4FDD\u62A4\u786C\u7EBF\uFF08\u2265 pauseAt ${pct(cfg.pauseAt)}\uFF09`;
|
|
3921
|
+
}
|
|
3922
|
+
return `${head} \u2265 pauseAt ${pct(cfg.pauseAt)}\uFF08\u71C3\u5C3D\u7387\u91C7\u6837\u4E2D\uFF0C\u9000\u515C\u5E95\u5224\u636E\uFF09`;
|
|
3923
|
+
}
|
|
3924
|
+
function agentCanResume(usage, cfg, now) {
|
|
3925
|
+
if (!isDecisionGrade(usage, now))
|
|
3926
|
+
return false;
|
|
3927
|
+
if (usage.rateLimitedUntil > now)
|
|
3928
|
+
return false;
|
|
3929
|
+
const windows = freshWindows(usage, now);
|
|
3930
|
+
for (const { window } of windows) {
|
|
3931
|
+
if (maximizeWindowBlocksResume(window, cfg, now))
|
|
3932
|
+
return false;
|
|
3933
|
+
}
|
|
3934
|
+
return true;
|
|
3935
|
+
}
|
|
3936
|
+
function effectiveDynamicLine(usage, cfg, now) {
|
|
3937
|
+
if (!usage || !isDecisionGrade(usage, now))
|
|
3938
|
+
return null;
|
|
3939
|
+
let bestLine = null;
|
|
3940
|
+
let bestHeadroom = Number.POSITIVE_INFINITY;
|
|
3941
|
+
for (const { window } of freshWindows(usage, now)) {
|
|
3942
|
+
const rate = confidentRate(window);
|
|
3943
|
+
if (rate === null)
|
|
3944
|
+
continue;
|
|
3945
|
+
const line = dynamicPauseAt(window, rate, cfg, now);
|
|
3946
|
+
if (line === "admission-closed" || line >= 100)
|
|
3947
|
+
continue;
|
|
3948
|
+
const headroom = line - window.util;
|
|
3949
|
+
if (headroom < bestHeadroom) {
|
|
3950
|
+
bestHeadroom = headroom;
|
|
3951
|
+
bestLine = line;
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
return bestLine;
|
|
3955
|
+
}
|
|
3956
|
+
function resumeBlockingEpochFor(usage, cfg, now) {
|
|
3676
3957
|
if (!usage)
|
|
3677
3958
|
return 0;
|
|
3678
3959
|
if (usage.rateLimitedUntil > now)
|
|
3679
3960
|
return usage.rateLimitedUntil;
|
|
3680
|
-
if (usage
|
|
3681
|
-
|
|
3682
|
-
|
|
3961
|
+
if (!isDecisionGrade(usage, now)) {
|
|
3962
|
+
const reset = matchingGateReset(usage);
|
|
3963
|
+
return reset > now ? reset : 0;
|
|
3964
|
+
}
|
|
3965
|
+
const blockingResets = freshWindows(usage, now).filter(({ window }) => maximizeWindowBlocksResume(window, cfg, now)).map(({ window }) => window.resetEpoch).filter((epoch) => epoch > 0);
|
|
3966
|
+
if (blockingResets.length === 0)
|
|
3967
|
+
return 0;
|
|
3968
|
+
return Math.min(...blockingResets);
|
|
3683
3969
|
}
|
|
3684
3970
|
|
|
3685
|
-
// src/budget/types.ts
|
|
3686
|
-
var STALE_MAX_AGE_SEC = 600;
|
|
3687
|
-
|
|
3688
3971
|
// src/budget/budget-state.ts
|
|
3689
|
-
var
|
|
3972
|
+
var NO_RUNWAY = { claude: null, codex: null };
|
|
3973
|
+
var UNDERUTILIZATION_MIN_WASTE_PCT = 10;
|
|
3974
|
+
var AGENT_LABEL2 = {
|
|
3690
3975
|
claude: "Claude",
|
|
3691
3976
|
codex: "Codex"
|
|
3692
3977
|
};
|
|
3693
3978
|
var CODEX_BALANCED_WARN_UTIL = 60;
|
|
3694
3979
|
var CODEX_ECO_WARN_UTIL = 80;
|
|
3695
3980
|
var CLAUDE_ADVICE_WARN_UTIL = 80;
|
|
3696
|
-
function
|
|
3981
|
+
function pct2(value) {
|
|
3697
3982
|
return `${Math.round(value * 10) / 10}%`;
|
|
3698
3983
|
}
|
|
3699
3984
|
function formatEpoch(epoch) {
|
|
@@ -3703,40 +3988,23 @@ function formatEpoch(epoch) {
|
|
|
3703
3988
|
}
|
|
3704
3989
|
function usageSummary(name, usage) {
|
|
3705
3990
|
if (!usage)
|
|
3706
|
-
return `${
|
|
3707
|
-
return `${
|
|
3991
|
+
return `${AGENT_LABEL2[name]} \u672A\u77E5`;
|
|
3992
|
+
return `${AGENT_LABEL2[name]} gate=${pct2(usage.gateUtil)} warn=${pct2(usage.warnUtil)} 5h\u91CD\u7F6E=${formatEpoch(usage.fiveHour?.resetEpoch ?? 0)}`;
|
|
3708
3993
|
}
|
|
3709
3994
|
function resumeAfterEpoch(claude, codex, cfg, now) {
|
|
3710
3995
|
const epochs = [
|
|
3711
|
-
|
|
3712
|
-
|
|
3996
|
+
resumeBlockingEpochFor(claude, cfg, now),
|
|
3997
|
+
resumeBlockingEpochFor(codex, cfg, now)
|
|
3713
3998
|
].filter((epoch) => epoch > 0);
|
|
3714
3999
|
if (epochs.length === 0)
|
|
3715
4000
|
return null;
|
|
3716
4001
|
return Math.max(...epochs);
|
|
3717
4002
|
}
|
|
3718
|
-
function isDecisionGrade(usage, now) {
|
|
3719
|
-
if (!usage)
|
|
3720
|
-
return false;
|
|
3721
|
-
const freshWindow = usage.fiveHour !== null && usage.fiveHour.resetEpoch > now || usage.weekly !== null && usage.weekly.resetEpoch > now;
|
|
3722
|
-
if (!freshWindow)
|
|
3723
|
-
return false;
|
|
3724
|
-
if (usage.fetchedAt > 0 && now - usage.fetchedAt > STALE_MAX_AGE_SEC)
|
|
3725
|
-
return false;
|
|
3726
|
-
return true;
|
|
3727
|
-
}
|
|
3728
4003
|
function pauseTrigger(agent, usage, cfg, now) {
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
if (!isDecisionGrade(usage, now))
|
|
4004
|
+
const decision = agentShouldPause(agent, usage, cfg, now);
|
|
4005
|
+
if (!decision.pause)
|
|
3732
4006
|
return null;
|
|
3733
|
-
|
|
3734
|
-
return {
|
|
3735
|
-
agent,
|
|
3736
|
-
reason: `${AGENT_LABEL[agent]} gateUtil ${pct(usage.gateUtil)} \u2265 pauseAt ${pct(cfg.pauseAt)}`
|
|
3737
|
-
};
|
|
3738
|
-
}
|
|
3739
|
-
return null;
|
|
4007
|
+
return { agent, reason: decision.reason };
|
|
3740
4008
|
}
|
|
3741
4009
|
function driftFor(claude, codex, cfg) {
|
|
3742
4010
|
if (!claude || !codex)
|
|
@@ -3751,33 +4019,75 @@ function driftFor(claude, codex, cfg) {
|
|
|
3751
4019
|
lighter: drift > 0 ? "codex" : "claude"
|
|
3752
4020
|
};
|
|
3753
4021
|
}
|
|
3754
|
-
function
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
4022
|
+
function runwayBalance(claudeRunway, codexRunway, cfg) {
|
|
4023
|
+
const ch = claudeRunway.seconds / 3600;
|
|
4024
|
+
const xh = codexRunway.seconds / 3600;
|
|
4025
|
+
const lo = Math.min(ch, xh);
|
|
4026
|
+
const hi = Math.max(ch, xh);
|
|
4027
|
+
const ratioPct = hi <= 0 ? 100 : Math.round(100 * lo / hi);
|
|
4028
|
+
const gapHours = Math.abs(ch - xh);
|
|
4029
|
+
if (ratioPct < cfg.allocation.minRunwayRatio && gapHours >= cfg.allocation.minRunwayGapHours) {
|
|
4030
|
+
const shorter = ch < xh ? "claude" : "codex";
|
|
4031
|
+
return { heavier: shorter, lighter: shorter === "claude" ? "codex" : "claude" };
|
|
3759
4032
|
}
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
const
|
|
4033
|
+
return null;
|
|
4034
|
+
}
|
|
4035
|
+
function allocationDrift(claude, codex, runway, cfg) {
|
|
4036
|
+
const warnDrift = driftFor(claude, codex, cfg);
|
|
4037
|
+
if (!claude || !codex || !runway.claude || !runway.codex) {
|
|
4038
|
+
return { drift: warnDrift, basis: "warn" };
|
|
4039
|
+
}
|
|
4040
|
+
const balance = runwayBalance(runway.claude, runway.codex, cfg);
|
|
3768
4041
|
return {
|
|
3769
|
-
|
|
3770
|
-
|
|
4042
|
+
drift: {
|
|
4043
|
+
pct: warnDrift.pct,
|
|
4044
|
+
heavier: balance?.heavier ?? null,
|
|
4045
|
+
lighter: balance?.lighter ?? null
|
|
4046
|
+
},
|
|
4047
|
+
basis: "runway"
|
|
3771
4048
|
};
|
|
3772
4049
|
}
|
|
4050
|
+
function runwayHoursText(runway) {
|
|
4051
|
+
if (!runway)
|
|
4052
|
+
return "\u672A\u77E5";
|
|
4053
|
+
return `~${(runway.seconds / 3600).toFixed(1)}h`;
|
|
4054
|
+
}
|
|
4055
|
+
function underutilizationState(claude, codex, cfg, now) {
|
|
4056
|
+
let top = null;
|
|
4057
|
+
for (const [agent, usage] of [["claude", claude], ["codex", codex]]) {
|
|
4058
|
+
const weekly = usage?.weekly;
|
|
4059
|
+
if (!weekly)
|
|
4060
|
+
continue;
|
|
4061
|
+
const verdict = dynamicWindowVerdict(weekly, cfg, now);
|
|
4062
|
+
if (verdict.kind !== "will-not-fill")
|
|
4063
|
+
continue;
|
|
4064
|
+
const waste = Math.round((cfg.maximize.targetUtil - verdict.projectedAtReset) * 10) / 10;
|
|
4065
|
+
if (waste < UNDERUTILIZATION_MIN_WASTE_PCT)
|
|
4066
|
+
continue;
|
|
4067
|
+
if (top === null || waste > top.waste) {
|
|
4068
|
+
top = { agent, projected: verdict.projectedAtReset, waste, resetEpoch: weekly.resetEpoch };
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
if (top === null)
|
|
4072
|
+
return { recommended: false, reason: null };
|
|
4073
|
+
const hoursToReset = Math.max(0, (top.resetEpoch - now) / 3600);
|
|
4074
|
+
const reason = [
|
|
4075
|
+
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011\u989D\u5EA6\u5C06\u6B20\u8F7D\uFF0C\u5EFA\u8BAE\u63D0\u9AD8\u5E76\u884C/\u59D4\u6D3E\u5BC6\u5EA6\u3002",
|
|
4076
|
+
`${AGENT_LABEL2[top.agent]} \u6309\u5F53\u524D\u71C3\u5C3D\u7387\u5468\u7A97\u53E3\u5237\u65B0\u65F6\u53EA\u4F1A\u7528\u5230 ~${pct2(top.projected)}\uFF0C` + `\u8DDD\u5237\u65B0\u8FD8\u6709 ~${hoursToReset.toFixed(1)}h \u2014\u2014 \u5EFA\u8BAE\u62C6\u66F4\u591A\u5E76\u884C\u5B50\u4EFB\u52A1/\u63D0\u9AD8\u59D4\u6D3E\u5BC6\u5EA6\uFF0C` + `\u5426\u5219\u7EA6 ${pct2(top.waste)} \u5468\u989D\u5EA6\u5C06\u4F5C\u5E9F\u3002`
|
|
4077
|
+
].join(`
|
|
4078
|
+
`);
|
|
4079
|
+
return { recommended: true, reason };
|
|
4080
|
+
}
|
|
3773
4081
|
function renderBudgetInterventionDirective(claude, codex, side, reason, resumeEpoch, cfg) {
|
|
3774
4082
|
const resumeText = `\u9884\u8BA1\u6062\u590D\u65F6\u95F4\uFF08\u4EE5\u5B9E\u6D4B\u4E3A\u51C6\uFF1B\u63D0\u524D\u5237\u65B0\u4F1A\u66F4\u65E9\u89E3\u9664\uFF09\uFF1A${formatEpoch(resumeEpoch)}\u3002`;
|
|
4083
|
+
const resumeCondSingle = `\u5404\u7A97\u53E3 util \u56DE\u843D\u81F3\u52A8\u6001\u6682\u505C\u7EBF \u2212 ${pct2(cfg.maximize.resumeHysteresisPct)} \u4EE5\u4E0B\u6216\u5BF9\u5E94\u7A97\u53E3\u5237\u65B0`;
|
|
4084
|
+
const resumeCondBoth = `\u5404\u7A97\u53E3 util \u90FD\u56DE\u843D\u81F3\u52A8\u6001\u6682\u505C\u7EBF \u2212 ${pct2(cfg.maximize.resumeHysteresisPct)} \u4EE5\u4E0B\u6216\u5BF9\u5E94\u7A97\u53E3\u5237\u65B0`;
|
|
3775
4085
|
if (side === "claude") {
|
|
3776
4086
|
return [
|
|
3777
4087
|
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011Claude \u4FA7\u989D\u5EA6\u7D27\u5F20\uFF0C\u8FDB\u5165\u63A5\u529B\u6A21\u5F0F\u3002",
|
|
3778
4088
|
`\u89E6\u53D1\u65B9\uFF1AClaude\uFF1B\u539F\u56E0\uFF1A${reason}\u3002`,
|
|
3779
4089
|
`${usageSummary("claude", claude)}\uFF1B${usageSummary("codex", codex)}\u3002`,
|
|
3780
|
-
`\u6062\u590D\u53C2\u8003\uFF1AClaude
|
|
4090
|
+
`\u6062\u590D\u53C2\u8003\uFF1AClaude ${resumeCondSingle} \u4E14\u6CA1\u6709\u6709\u6548 rate_limit\uFF1B${resumeText}`,
|
|
3781
4091
|
"\u8BF7\u7ACB\u5373\u4EA4\u63A5\uFF1A\u628A\u5269\u4F59\u4EFB\u52A1\u6E05\u5355\u3001\u5173\u952E\u4E0A\u4E0B\u6587\u3001\u4EA7\u51FA\u4F4D\u7F6E\u3001\u9A8C\u6536\u6807\u51C6\u6253\u5305\u6210\u4E00\u6761 reply \u53D1\u7ED9 Codex\u3002",
|
|
3782
4092
|
"\u4EA4\u63A5\u540E Claude \u505C\u624B\uFF1B\u8981\u6C42 Codex \u5728\u5355 turn \u5185\u5C3D\u91CF\u5B8C\u6210\uFF0C\u5C3E\u5DF4\u5199 checkpoint\uFF0C\u6682\u505C\u671F\u4E0D\u8981\u671F\u5F85 Claude \u56DE\u590D\u3002"
|
|
3783
4093
|
].join(`
|
|
@@ -3788,7 +4098,7 @@ function renderBudgetInterventionDirective(claude, codex, side, reason, resumeEp
|
|
|
3788
4098
|
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011Codex \u4FA7\u989D\u5EA6\u7D27\u5F20\uFF0C\u6682\u505C\u59D4\u6D3E\u3002",
|
|
3789
4099
|
`\u89E6\u53D1\u65B9\uFF1ACodex\uFF1B\u539F\u56E0\uFF1A${reason}\u3002`,
|
|
3790
4100
|
`${usageSummary("claude", claude)}\uFF1B${usageSummary("codex", codex)}\u3002`,
|
|
3791
|
-
`\u6062\u590D\u53C2\u8003\uFF1ACodex
|
|
4101
|
+
`\u6062\u590D\u53C2\u8003\uFF1ACodex ${resumeCondSingle} \u4E14\u6CA1\u6709\u6709\u6548 rate_limit\uFF1B${resumeText}`,
|
|
3792
4102
|
"\u8BF7 Claude \u5199 checkpoint\uFF0C\u5E76\u53EF solo \u63A8\u8FDB\u4E0D\u4F9D\u8D56 Codex \u7684\u72EC\u7ACB\u90E8\u5206\uFF1B\u4E0D\u8981\u7EE7\u7EED\u5411 Codex \u59D4\u6D3E\uFF0C\u6807\u6CE8\u6E05\u695A\u5206\u5DE5\u65AD\u70B9\u3002"
|
|
3793
4103
|
].join(`
|
|
3794
4104
|
`);
|
|
@@ -3797,30 +4107,26 @@ function renderBudgetInterventionDirective(claude, codex, side, reason, resumeEp
|
|
|
3797
4107
|
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011\u8FDB\u5165\u8054\u5408\u6682\u505C\u3002",
|
|
3798
4108
|
`\u89E6\u53D1\u65B9\uFF1A\u53CC\u65B9\uFF1B\u539F\u56E0\uFF1A${reason}\u3002`,
|
|
3799
4109
|
`${usageSummary("claude", claude)}\uFF1B${usageSummary("codex", codex)}\u3002`,
|
|
3800
|
-
`\u6062\u590D\u6761\u4EF6\uFF1AClaude \u4E0E Codex \u7684
|
|
4110
|
+
`\u6062\u590D\u6761\u4EF6\uFF1AClaude \u4E0E Codex \u7684 ${resumeCondBoth}\uFF0C\u4E14\u6CA1\u6709\u6709\u6548 rate_limit\uFF1B${resumeText}`,
|
|
3801
4111
|
"\u8BF7\u6536\u5C3E\u5F53\u524D\u6B65\u3001\u5199 checkpoint\u3001\u505C\u6B62\u7EE7\u7EED\u59D4\u6D3E\uFF1Bpause \u671F\u95F4\u4E0D\u8981\u91CD\u8BD5\u5411 Codex \u53D1\u9001 reply\u3002"
|
|
3802
4112
|
].join(`
|
|
3803
4113
|
`);
|
|
3804
4114
|
}
|
|
3805
|
-
function balanceDirective(claude, codex, drift,
|
|
3806
|
-
const heavier = drift.heavier ?
|
|
3807
|
-
const lighter = drift.lighter ?
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
lines.push(`${parallel.reason}\uFF1B\u53EF\u8BA9 ${lighter} \u627F\u62C5\u66F4\u591A\u5E76\u884C\u5B50\u4EFB\u52A1\uFF0C\u517C\u987E\u5747\u8861\u4E0E\u63D0\u901F\u3002`);
|
|
3815
|
-
}
|
|
3816
|
-
return lines.join(`
|
|
4115
|
+
function balanceDirective(claude, codex, drift, basis, runway) {
|
|
4116
|
+
const heavier = drift.heavier ? AGENT_LABEL2[drift.heavier] : "\u672A\u77E5";
|
|
4117
|
+
const lighter = drift.lighter ? AGENT_LABEL2[drift.lighter] : "\u672A\u77E5";
|
|
4118
|
+
if (basis === "runway") {
|
|
4119
|
+
return [
|
|
4120
|
+
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011\u6309\u5269\u4F59\u53EF\u5DE5\u4F5C\u65F6\u95F4\u9700\u8981\u5747\u8861\u3002",
|
|
4121
|
+
`${usageSummary("claude", claude)}\uFF1B${usageSummary("codex", codex)}\u3002`,
|
|
4122
|
+
`Claude \u6309\u5F53\u524D\u71C3\u5C3D\u7387\u7EA6\u53EF\u518D\u5DE5\u4F5C ${runwayHoursText(runway.claude)}\u3001` + `Codex ${runwayHoursText(runway.codex)}\uFF08\u7A97\u53E3\u4E3A\u7EA6\u675F\uFF09\uFF1B` + `runway \u8F83\u77ED\u7684\u4E00\u4FA7\u662F ${heavier}\uFF0C\u8BF7\u628A\u540E\u7EED\u53EF\u62C6\u5206\u4EFB\u52A1\u4F18\u5148\u6D3E\u7ED9 ${lighter}\u3002`
|
|
4123
|
+
].join(`
|
|
3817
4124
|
`);
|
|
3818
|
-
}
|
|
3819
|
-
function parallelDirective(claude, codex, parallel) {
|
|
4125
|
+
}
|
|
3820
4126
|
return [
|
|
3821
|
-
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011\
|
|
4127
|
+
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011\u68C0\u6D4B\u5230\u53CC\u65B9\u7528\u91CF\u6BD4\u4F8B\u6F02\u79FB\u3002",
|
|
3822
4128
|
`${usageSummary("claude", claude)}\uFF1B${usageSummary("codex", codex)}\u3002`,
|
|
3823
|
-
`${
|
|
4129
|
+
`${heavier} \u6BD4 ${lighter} \u9AD8 ${pct2(Math.abs(drift.pct))}\uFF0C\u8BF7\u4F18\u5148\u628A\u540E\u7EED\u53EF\u62C6\u5206\u4EFB\u52A1\u5206\u7ED9 ${lighter}\uFF0C\u76F4\u5230 warnUtil \u63A5\u8FD1\u3002`
|
|
3824
4130
|
].join(`
|
|
3825
4131
|
`);
|
|
3826
4132
|
}
|
|
@@ -3838,16 +4144,19 @@ function claudeAdviceFor(claude, now) {
|
|
|
3838
4144
|
return null;
|
|
3839
4145
|
if (claude.warnUtil < CLAUDE_ADVICE_WARN_UTIL)
|
|
3840
4146
|
return null;
|
|
3841
|
-
return `Claude warnUtil ${
|
|
4147
|
+
return `Claude warnUtil ${pct2(claude.warnUtil)} \u5DF2\u504F\u9AD8\uFF1B\u540E\u7EED\u53EF\u62C6\u5206 subagent \u5EFA\u8BAE\u964D\u6863\u5230 haiku/sonnet\uFF0C\u5E76\u4FDD\u7559\u9AD8\u96BE\u5EA6\u4E3B\u7EBF\u7ED9\u5F53\u524D\u4F1A\u8BDD\u3002`;
|
|
3842
4148
|
}
|
|
3843
|
-
function computeBudgetState(claude, codex, cfg, now) {
|
|
4149
|
+
function computeBudgetState(claude, codex, cfg, now, runway = NO_RUNWAY) {
|
|
3844
4150
|
const triggers = [
|
|
3845
4151
|
pauseTrigger("claude", claude, cfg, now),
|
|
3846
4152
|
pauseTrigger("codex", codex, cfg, now)
|
|
3847
4153
|
].filter((trigger) => trigger !== null);
|
|
3848
4154
|
const paused = triggers.length > 0;
|
|
3849
|
-
const drift =
|
|
3850
|
-
const parallel =
|
|
4155
|
+
const { drift, basis } = allocationDrift(claude, codex, runway, cfg);
|
|
4156
|
+
const parallel = { recommended: false, reason: null };
|
|
4157
|
+
const adviceEligible = !paused && claude !== null && codex !== null && claude.rateLimitedUntil <= now && codex.rateLimitedUntil <= now && isDecisionGrade(claude, now) && isDecisionGrade(codex, now);
|
|
4158
|
+
const balanceActive = adviceEligible && drift.heavier !== null && drift.lighter !== null;
|
|
4159
|
+
const underutilization = adviceEligible && !balanceActive ? underutilizationState(claude, codex, cfg, now) : { recommended: false, reason: null };
|
|
3851
4160
|
const resetEpochs = {
|
|
3852
4161
|
claude: matchingGateReset(claude),
|
|
3853
4162
|
codex: matchingGateReset(codex)
|
|
@@ -3856,18 +4165,18 @@ function computeBudgetState(claude, codex, cfg, now) {
|
|
|
3856
4165
|
let phase = "normal";
|
|
3857
4166
|
if (paused)
|
|
3858
4167
|
phase = "paused";
|
|
3859
|
-
else if (
|
|
4168
|
+
else if (balanceActive)
|
|
3860
4169
|
phase = "balance";
|
|
3861
|
-
else if (
|
|
3862
|
-
phase = "
|
|
4170
|
+
else if (underutilization.recommended)
|
|
4171
|
+
phase = "underutilized";
|
|
3863
4172
|
const pauseSide = !paused ? null : triggers.length > 1 ? "both" : triggers[0].agent;
|
|
3864
4173
|
let directiveToClaude = null;
|
|
3865
4174
|
if (phase === "paused") {
|
|
3866
4175
|
directiveToClaude = renderBudgetInterventionDirective(claude, codex, pauseSide ?? "both", triggers.map((trigger) => trigger.reason).join("\uFF1B"), filteredResumeAfterEpoch, cfg);
|
|
3867
4176
|
} else if (phase === "balance" && claude && codex) {
|
|
3868
|
-
directiveToClaude = balanceDirective(claude, codex, drift,
|
|
3869
|
-
} else if (phase === "
|
|
3870
|
-
directiveToClaude =
|
|
4177
|
+
directiveToClaude = balanceDirective(claude, codex, drift, basis, runway);
|
|
4178
|
+
} else if (phase === "underutilized") {
|
|
4179
|
+
directiveToClaude = underutilization.reason;
|
|
3871
4180
|
}
|
|
3872
4181
|
return {
|
|
3873
4182
|
phase,
|
|
@@ -3883,18 +4192,84 @@ function computeBudgetState(claude, codex, cfg, now) {
|
|
|
3883
4192
|
resetEpochs
|
|
3884
4193
|
},
|
|
3885
4194
|
parallel,
|
|
4195
|
+
underutilization,
|
|
3886
4196
|
effort: { claudeAdvice: claudeAdviceFor(claude, now), codexTier: codexTierFor(codex, now) },
|
|
3887
4197
|
directiveToClaude
|
|
3888
4198
|
};
|
|
3889
4199
|
}
|
|
3890
4200
|
|
|
4201
|
+
// src/budget/advice-cooldown.ts
|
|
4202
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
4203
|
+
import { join as join5 } from "path";
|
|
4204
|
+
var DEFAULT_ADVICE_COOLDOWN_SEC = 1800;
|
|
4205
|
+
var COOLDOWN_FILENAME = "advice-cooldown.json";
|
|
4206
|
+
function resolveAdviceCooldownSec(env = process.env) {
|
|
4207
|
+
const raw = env.AGENTBRIDGE_BUDGET_ADVICE_COOLDOWN_SEC;
|
|
4208
|
+
if (raw === undefined || raw.trim() === "")
|
|
4209
|
+
return DEFAULT_ADVICE_COOLDOWN_SEC;
|
|
4210
|
+
const parsed = Number(raw);
|
|
4211
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 86400)
|
|
4212
|
+
return DEFAULT_ADVICE_COOLDOWN_SEC;
|
|
4213
|
+
return parsed;
|
|
4214
|
+
}
|
|
4215
|
+
function resolveStateDir(homeDir) {
|
|
4216
|
+
const override = process.env.BUDGET_STATE_DIR;
|
|
4217
|
+
if (override && override.trim() !== "")
|
|
4218
|
+
return override.trim();
|
|
4219
|
+
return join5(homeDir, ".budget-guard");
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
class AdviceCooldown {
|
|
4223
|
+
path;
|
|
4224
|
+
cooldownSec;
|
|
4225
|
+
log;
|
|
4226
|
+
constructor(options) {
|
|
4227
|
+
this.path = join5(resolveStateDir(options.homeDir), COOLDOWN_FILENAME);
|
|
4228
|
+
this.cooldownSec = options.cooldownSec ?? DEFAULT_ADVICE_COOLDOWN_SEC;
|
|
4229
|
+
this.log = options.log ?? (() => {});
|
|
4230
|
+
}
|
|
4231
|
+
tryAcquire(direction, now) {
|
|
4232
|
+
const file = this.read();
|
|
4233
|
+
const last = file[direction]?.lastEmittedEpoch;
|
|
4234
|
+
if (this.cooldownSec > 0 && typeof last === "number" && Number.isFinite(last) && now - last < this.cooldownSec && last <= now) {
|
|
4235
|
+
return false;
|
|
4236
|
+
}
|
|
4237
|
+
this.write({ ...file, [direction]: { lastEmittedEpoch: now } });
|
|
4238
|
+
return true;
|
|
4239
|
+
}
|
|
4240
|
+
read() {
|
|
4241
|
+
let raw;
|
|
4242
|
+
try {
|
|
4243
|
+
raw = readFileSync5(this.path, "utf-8");
|
|
4244
|
+
} catch {
|
|
4245
|
+
return {};
|
|
4246
|
+
}
|
|
4247
|
+
try {
|
|
4248
|
+
const parsed = JSON.parse(raw);
|
|
4249
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
4250
|
+
return {};
|
|
4251
|
+
return parsed;
|
|
4252
|
+
} catch {
|
|
4253
|
+
this.log(`advice-cooldown: ignoring malformed ${this.path}`);
|
|
4254
|
+
return {};
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
write(file) {
|
|
4258
|
+
try {
|
|
4259
|
+
atomicWriteJson(this.path, file);
|
|
4260
|
+
} catch (error) {
|
|
4261
|
+
this.log(`advice-cooldown: write failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
|
|
3891
4266
|
// src/budget/budget-fingerprint.ts
|
|
3892
4267
|
var RESET_FINGERPRINT_BUCKET_SEC = 600;
|
|
3893
|
-
var
|
|
4268
|
+
var AGENT_LABEL3 = {
|
|
3894
4269
|
claude: "Claude",
|
|
3895
4270
|
codex: "Codex"
|
|
3896
4271
|
};
|
|
3897
|
-
function
|
|
4272
|
+
function pct3(value) {
|
|
3898
4273
|
return `${Math.round(value * 10) / 10}%`;
|
|
3899
4274
|
}
|
|
3900
4275
|
function formatEpoch2(epoch) {
|
|
@@ -3926,46 +4301,38 @@ function agentsToSide(agents) {
|
|
|
3926
4301
|
return "codex";
|
|
3927
4302
|
return null;
|
|
3928
4303
|
}
|
|
3929
|
-
function shouldEnter(usage, cfg, now) {
|
|
3930
|
-
if (!isDecisionGrade(usage, now))
|
|
3931
|
-
return false;
|
|
3932
|
-
return usage.gateUtil >= cfg.pauseAt;
|
|
3933
|
-
}
|
|
3934
|
-
function canAgentResume(usage, cfg, now) {
|
|
3935
|
-
if (!isDecisionGrade(usage, now))
|
|
3936
|
-
return false;
|
|
3937
|
-
if (usage.rateLimitedUntil > now)
|
|
3938
|
-
return false;
|
|
3939
|
-
return usage.gateUtil < cfg.resumeBelow;
|
|
3940
|
-
}
|
|
3941
4304
|
function nextActiveSide(prevSide, state, cfg) {
|
|
3942
4305
|
const active = new Set(sideToAgents(prevSide));
|
|
3943
4306
|
for (const agent of ["claude", "codex"]) {
|
|
3944
4307
|
const usage = state.perAgent[agent];
|
|
3945
|
-
if (
|
|
4308
|
+
if (agentShouldPause(agent, usage, cfg, state.now).pause) {
|
|
3946
4309
|
active.add(agent);
|
|
3947
|
-
} else if (active.has(agent) &&
|
|
4310
|
+
} else if (active.has(agent) && agentCanResume(usage, cfg, state.now)) {
|
|
3948
4311
|
active.delete(agent);
|
|
3949
4312
|
}
|
|
3950
4313
|
}
|
|
3951
4314
|
return agentsToSide(active);
|
|
3952
4315
|
}
|
|
4316
|
+
function removedAgents(prevSide, currentSide) {
|
|
4317
|
+
const current = new Set(sideToAgents(currentSide));
|
|
4318
|
+
return sideToAgents(prevSide).filter((agent) => !current.has(agent));
|
|
4319
|
+
}
|
|
3953
4320
|
function activeSideReason(agent, usage, cfg, now) {
|
|
3954
4321
|
if (!usage)
|
|
3955
|
-
return `${
|
|
4322
|
+
return `${AGENT_LABEL3[agent]} \u63A2\u6D4B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u4FDD\u6301\u4E0A\u4E00\u8F6E\u9884\u7B97\u5E72\u9884`;
|
|
3956
4323
|
if (usage.rateLimitedUntil > now) {
|
|
3957
|
-
return `${
|
|
4324
|
+
return `${AGENT_LABEL3[agent]} \u63A2\u9488\u88AB\u9650\u6D41\u81F3 ${formatEpoch2(usage.rateLimitedUntil)}`;
|
|
3958
4325
|
}
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
return `${
|
|
4326
|
+
const decision = agentShouldPause(agent, usage, cfg, now);
|
|
4327
|
+
if (decision.pause)
|
|
4328
|
+
return decision.reason;
|
|
4329
|
+
return `${AGENT_LABEL3[agent]} gateUtil ${pct3(usage.gateUtil)} \u5C1A\u672A\u6EE1\u8DB3\u51FA\u95F8\u6761\u4EF6`;
|
|
3963
4330
|
}
|
|
3964
4331
|
function interventionReason(side, state, cfg) {
|
|
3965
4332
|
return sideToAgents(side).map((agent) => activeSideReason(agent, state.perAgent[agent], cfg, state.now)).join("\uFF1B");
|
|
3966
4333
|
}
|
|
3967
4334
|
function resumeAfterEpoch2(side, state, cfg) {
|
|
3968
|
-
const epochs = sideToAgents(side).map((agent) =>
|
|
4335
|
+
const epochs = sideToAgents(side).map((agent) => resumeBlockingEpochFor(state.perAgent[agent], cfg, state.now)).filter((epoch) => epoch > 0);
|
|
3969
4336
|
if (epochs.length === 0)
|
|
3970
4337
|
return null;
|
|
3971
4338
|
return Math.max(...epochs);
|
|
@@ -3999,6 +4366,7 @@ function directiveFingerprint(state, activeSide) {
|
|
|
3999
4366
|
function classifyPoll(prev, state, cfg) {
|
|
4000
4367
|
const previousSide = prev.side;
|
|
4001
4368
|
const currentSide = nextActiveSide(previousSide, state, cfg);
|
|
4369
|
+
const recoveredSides = removedAgents(previousSide, currentSide);
|
|
4002
4370
|
if (currentSide) {
|
|
4003
4371
|
const reason = interventionReason(currentSide, state, cfg);
|
|
4004
4372
|
const nextResumeRaw = resumeAfterEpoch2(currentSide, state, cfg);
|
|
@@ -4015,33 +4383,67 @@ function classifyPoll(prev, state, cfg) {
|
|
|
4015
4383
|
reason,
|
|
4016
4384
|
resumeEpoch,
|
|
4017
4385
|
emit,
|
|
4018
|
-
pauseChanged
|
|
4386
|
+
pauseChanged,
|
|
4387
|
+
recoveredSides
|
|
4019
4388
|
}
|
|
4020
4389
|
};
|
|
4021
4390
|
}
|
|
4022
4391
|
if (previousSide) {
|
|
4023
4392
|
return {
|
|
4024
4393
|
next: { side: null, fingerprint: null, resumeEpoch: null, reason: null },
|
|
4025
|
-
effect: { kind: "exit", previousSide }
|
|
4394
|
+
effect: { kind: "exit", previousSide, recoveredSides }
|
|
4026
4395
|
};
|
|
4027
4396
|
}
|
|
4028
4397
|
if (!isDecisionGrade(state.perAgent.claude, state.now) || !isDecisionGrade(state.perAgent.codex, state.now)) {
|
|
4029
|
-
return { next: prev, effect: { kind: "none" } };
|
|
4398
|
+
return { next: prev, effect: { kind: "none", recoveredSides: [] } };
|
|
4030
4399
|
}
|
|
4031
4400
|
if (!state.directiveToClaude) {
|
|
4032
4401
|
return {
|
|
4033
4402
|
next: { side: null, fingerprint: null, resumeEpoch: null, reason: null },
|
|
4034
|
-
effect: { kind: "none" }
|
|
4403
|
+
effect: { kind: "none", recoveredSides: [] }
|
|
4035
4404
|
};
|
|
4036
4405
|
}
|
|
4037
4406
|
const fingerprint = directiveFingerprint(state);
|
|
4038
4407
|
if (fingerprint !== prev.fingerprint) {
|
|
4039
4408
|
return {
|
|
4040
4409
|
next: { side: null, fingerprint, resumeEpoch: null, reason: null },
|
|
4041
|
-
effect: { kind: "advise", phase: state.phase }
|
|
4410
|
+
effect: { kind: "advise", phase: state.phase, recoveredSides: [] }
|
|
4411
|
+
};
|
|
4412
|
+
}
|
|
4413
|
+
return { next: prev, effect: { kind: "none", recoveredSides: [] } };
|
|
4414
|
+
}
|
|
4415
|
+
function resumeCandidateSides(effect) {
|
|
4416
|
+
if (effect.recoveredSides.length > 0) {
|
|
4417
|
+
return effect.recoveredSides;
|
|
4418
|
+
}
|
|
4419
|
+
switch (effect.kind) {
|
|
4420
|
+
case "exit":
|
|
4421
|
+
return sideToAgents(effect.previousSide);
|
|
4422
|
+
case "enter":
|
|
4423
|
+
case "hold-uncertain":
|
|
4424
|
+
return sideToAgents(effect.side);
|
|
4425
|
+
case "advise":
|
|
4426
|
+
case "none":
|
|
4427
|
+
return [];
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
function computeResumeCandidate(sides, state, cfg, signals) {
|
|
4431
|
+
const candidate = {};
|
|
4432
|
+
const detail = {};
|
|
4433
|
+
for (const agent of sides) {
|
|
4434
|
+
const windowRefreshed = agentCanResume(state.perAgent[agent], cfg, state.now);
|
|
4435
|
+
const ready = windowRefreshed && signals.pendingExists[agent] && signals.tuiReady[agent] && signals.checkpointExists;
|
|
4436
|
+
candidate[agent] = ready;
|
|
4437
|
+
const pending = signals.pending?.[agent];
|
|
4438
|
+
detail[agent] = {
|
|
4439
|
+
ready,
|
|
4440
|
+
...pending ? { pending } : {},
|
|
4441
|
+
...signals.checkpointPath ? { checkpointPath: signals.checkpointPath } : {}
|
|
4042
4442
|
};
|
|
4043
4443
|
}
|
|
4044
|
-
|
|
4444
|
+
if (sides.length > 0)
|
|
4445
|
+
candidate.detail = detail;
|
|
4446
|
+
return candidate;
|
|
4045
4447
|
}
|
|
4046
4448
|
|
|
4047
4449
|
// src/budget/burn-view.ts
|
|
@@ -4110,17 +4512,17 @@ var REAL_BUDGET_POLL_SCHEDULER = {
|
|
|
4110
4512
|
clearTimeout(timer);
|
|
4111
4513
|
}
|
|
4112
4514
|
};
|
|
4113
|
-
var
|
|
4515
|
+
var AGENT_LABEL4 = {
|
|
4114
4516
|
claude: "Claude",
|
|
4115
4517
|
codex: "Codex"
|
|
4116
4518
|
};
|
|
4117
|
-
function
|
|
4519
|
+
function pct4(value) {
|
|
4118
4520
|
return `${Math.round(value * 10) / 10}%`;
|
|
4119
4521
|
}
|
|
4120
4522
|
function usageLine(agent, usage) {
|
|
4121
4523
|
if (!usage)
|
|
4122
|
-
return `${
|
|
4123
|
-
return `${
|
|
4524
|
+
return `${AGENT_LABEL4[agent]} \u672A\u77E5`;
|
|
4525
|
+
return `${AGENT_LABEL4[agent]} gate=${pct4(usage.gateUtil)} warn=${pct4(usage.warnUtil)}`;
|
|
4124
4526
|
}
|
|
4125
4527
|
function maxPollDelayMs(config) {
|
|
4126
4528
|
return Math.max(0, config.pollSeconds * 1000);
|
|
@@ -4181,9 +4583,13 @@ class BudgetCoordinator {
|
|
|
4181
4583
|
now;
|
|
4182
4584
|
scheduler;
|
|
4183
4585
|
log;
|
|
4586
|
+
onResume;
|
|
4587
|
+
resumeSignals;
|
|
4588
|
+
adviceCooldown;
|
|
4184
4589
|
timer = null;
|
|
4185
4590
|
running = false;
|
|
4186
4591
|
fpState = INITIAL_FINGERPRINT_STATE;
|
|
4592
|
+
resumeCandidate = {};
|
|
4187
4593
|
latestSnapshot = null;
|
|
4188
4594
|
pendingOverrideTier = null;
|
|
4189
4595
|
pendingOverrides = null;
|
|
@@ -4199,6 +4605,13 @@ class BudgetCoordinator {
|
|
|
4199
4605
|
this.now = options.now ?? (() => Math.floor(Date.now() / 1000));
|
|
4200
4606
|
this.scheduler = options.scheduler ?? REAL_BUDGET_POLL_SCHEDULER;
|
|
4201
4607
|
this.log = options.log ?? (() => {});
|
|
4608
|
+
this.onResume = options.onResume ?? (() => {});
|
|
4609
|
+
this.resumeSignals = options.resumeSignals ?? null;
|
|
4610
|
+
this.adviceCooldown = options.adviceCooldown ?? new AdviceCooldown({
|
|
4611
|
+
homeDir: homedir2(),
|
|
4612
|
+
cooldownSec: resolveAdviceCooldownSec(),
|
|
4613
|
+
log: this.log
|
|
4614
|
+
});
|
|
4202
4615
|
}
|
|
4203
4616
|
async start() {
|
|
4204
4617
|
if (this.running || !this.config.enabled)
|
|
@@ -4224,6 +4637,19 @@ class BudgetCoordinator {
|
|
|
4224
4637
|
getSnapshot() {
|
|
4225
4638
|
return this.latestSnapshot;
|
|
4226
4639
|
}
|
|
4640
|
+
getResumeCandidate() {
|
|
4641
|
+
const { detail, ...rest } = this.resumeCandidate;
|
|
4642
|
+
return detail ? {
|
|
4643
|
+
...rest,
|
|
4644
|
+
detail: Object.fromEntries(Object.entries(detail).map(([side, value]) => [
|
|
4645
|
+
side,
|
|
4646
|
+
{
|
|
4647
|
+
...value,
|
|
4648
|
+
...value.pending ? { pending: { ...value.pending } } : {}
|
|
4649
|
+
}
|
|
4650
|
+
]))
|
|
4651
|
+
} : { ...rest };
|
|
4652
|
+
}
|
|
4227
4653
|
getCodexTurnOverrides() {
|
|
4228
4654
|
if (!this.tierControlEnabled())
|
|
4229
4655
|
return null;
|
|
@@ -4279,10 +4705,15 @@ class BudgetCoordinator {
|
|
|
4279
4705
|
if (!this.running) {
|
|
4280
4706
|
return;
|
|
4281
4707
|
}
|
|
4282
|
-
const
|
|
4708
|
+
const now = this.now();
|
|
4709
|
+
const runway = {
|
|
4710
|
+
claude: agentRunway(usage.claude, now),
|
|
4711
|
+
codex: agentRunway(usage.codex, now)
|
|
4712
|
+
};
|
|
4713
|
+
const state = computeBudgetState(usage.claude, usage.codex, this.config, now, runway);
|
|
4283
4714
|
this.updatePendingOverrides(state.effort.codexTier);
|
|
4284
4715
|
this.applyState(state);
|
|
4285
|
-
this.setSnapshot(this.toSnapshot(state));
|
|
4716
|
+
this.setSnapshot(this.toSnapshot(state, runway));
|
|
4286
4717
|
}
|
|
4287
4718
|
setSnapshot(snapshot) {
|
|
4288
4719
|
this.latestSnapshot = snapshot;
|
|
@@ -4291,6 +4722,11 @@ class BudgetCoordinator {
|
|
|
4291
4722
|
applyState(state) {
|
|
4292
4723
|
const { next, effect } = classifyPoll(this.fpState, state, this.config);
|
|
4293
4724
|
this.fpState = next;
|
|
4725
|
+
this.resumeCandidate = this.resumeSignals ? computeResumeCandidate(resumeCandidateSides(effect), state, this.config, this.resumeSignals()) : {};
|
|
4726
|
+
for (const side of effect.recoveredSides) {
|
|
4727
|
+
const { id, directive } = this.emitRecovery(side, state);
|
|
4728
|
+
this.onResume(side, directive, id);
|
|
4729
|
+
}
|
|
4294
4730
|
switch (effect.kind) {
|
|
4295
4731
|
case "enter":
|
|
4296
4732
|
case "hold-uncertain": {
|
|
@@ -4303,12 +4739,16 @@ class BudgetCoordinator {
|
|
|
4303
4739
|
}
|
|
4304
4740
|
case "exit": {
|
|
4305
4741
|
this.onPauseChange(false);
|
|
4306
|
-
this.emitDirective(this.recoveryPrefix(effect.previousSide), this.recoveryDirective(state, effect.previousSide));
|
|
4307
4742
|
return;
|
|
4308
4743
|
}
|
|
4309
4744
|
case "advise": {
|
|
4310
|
-
|
|
4311
|
-
|
|
4745
|
+
if (effect.phase === "underutilized") {
|
|
4746
|
+
if (!this.adviceCooldown.tryAcquire("underutilization", state.now))
|
|
4747
|
+
return;
|
|
4748
|
+
this.emitDirective("system_budget_underutilized", state.directiveToClaude);
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4751
|
+
this.emitDirective("system_budget_balance", state.directiveToClaude);
|
|
4312
4752
|
return;
|
|
4313
4753
|
}
|
|
4314
4754
|
case "none":
|
|
@@ -4349,45 +4789,44 @@ class BudgetCoordinator {
|
|
|
4349
4789
|
this.pendingOverrides = { ...overrides };
|
|
4350
4790
|
}
|
|
4351
4791
|
emitDirective(prefix, content) {
|
|
4352
|
-
|
|
4792
|
+
const id = `${prefix}_${this.sequence++}`;
|
|
4793
|
+
this.emit(id, content);
|
|
4794
|
+
return id;
|
|
4353
4795
|
}
|
|
4354
4796
|
interventionPrefix(side) {
|
|
4355
4797
|
return side === "claude" ? "system_budget_handoff" : "system_budget_pause";
|
|
4356
4798
|
}
|
|
4357
|
-
recoveryPrefix(
|
|
4358
|
-
return
|
|
4799
|
+
recoveryPrefix(side) {
|
|
4800
|
+
return side === "claude" ? "system_budget_claude_recovered" : "system_budget_resume";
|
|
4801
|
+
}
|
|
4802
|
+
emitRecovery(side, state) {
|
|
4803
|
+
const directive = this.recoveryDirective(state, side);
|
|
4804
|
+
const id = this.emitDirective(this.recoveryPrefix(side), directive);
|
|
4805
|
+
return { id, directive };
|
|
4359
4806
|
}
|
|
4360
4807
|
interventionDirective(state, side, reason, resumeEpoch) {
|
|
4361
4808
|
return renderBudgetInterventionDirective(state.perAgent.claude, state.perAgent.codex, side, reason || "\u9884\u7B97\u63A5\u8FD1\u8017\u5C3D", resumeEpoch, this.config);
|
|
4362
4809
|
}
|
|
4363
|
-
recoveryDirective(state,
|
|
4364
|
-
|
|
4810
|
+
recoveryDirective(state, side) {
|
|
4811
|
+
const recoveredText = `\u5404\u7A97\u53E3 util \u5DF2\u56DE\u843D\u81F3\u52A8\u6001\u6682\u505C\u7EBF \u2212 ${pct4(this.config.maximize.resumeHysteresisPct)} \u4EE5\u4E0B\u6216\u5BF9\u5E94\u7A97\u53E3\u5DF2\u5237\u65B0`;
|
|
4812
|
+
if (side === "claude") {
|
|
4365
4813
|
return [
|
|
4366
4814
|
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011Claude \u4FA7\u9884\u7B97\u5DF2\u6062\u590D\u3002",
|
|
4367
4815
|
`${usageLine("claude", state.perAgent.claude)}\uFF1B${usageLine("codex", state.perAgent.codex)}\u3002`,
|
|
4368
|
-
`Claude
|
|
4816
|
+
`Claude ${recoveredText}\uFF0C\u4E14\u6CA1\u6709\u6709\u6548 rate_limit\u3002`,
|
|
4369
4817
|
"Claude \u53EF\u6062\u590D orchestrator \u89D2\u8272\uFF1B\u540E\u7EED\u5206\u914D\u524D\u8BF7\u91CD\u65B0\u67E5\u8BE2\u5B9E\u65F6\u989D\u5EA6\uFF0C\u4E0D\u8981\u4F9D\u8D56\u65E7\u6570\u5B57\u3002"
|
|
4370
4818
|
].join(`
|
|
4371
|
-
`);
|
|
4372
|
-
}
|
|
4373
|
-
if (previousSide === "codex") {
|
|
4374
|
-
return [
|
|
4375
|
-
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011Codex \u4FA7\u9884\u7B97\u95F8\u95E8\u89E3\u9664\u3002",
|
|
4376
|
-
`${usageLine("claude", state.perAgent.claude)}\uFF1B${usageLine("codex", state.perAgent.codex)}\u3002`,
|
|
4377
|
-
`\u95F8\u95E8\u5DF2\u653E\u5F00\uFF1ACodex gateUtil \u4F4E\u4E8E ${pct3(this.config.resumeBelow)}\uFF0C\u4E14\u6CA1\u6709\u6709\u6548 rate_limit\u3002`,
|
|
4378
|
-
"\u5EFA\u8BAE Claude \u7528 reply \u5E26\u4E0A\u5F53\u524D\u76EE\u6807\u3001checkpoint \u548C\u4E0B\u4E00\u6B65\uFF0C\u5524\u9192 Codex \u63A5\u7EED\u6267\u884C\u3002"
|
|
4379
|
-
].join(`
|
|
4380
4819
|
`);
|
|
4381
4820
|
}
|
|
4382
4821
|
return [
|
|
4383
|
-
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\
|
|
4822
|
+
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011Codex \u4FA7\u9884\u7B97\u95F8\u95E8\u89E3\u9664\u3002",
|
|
4384
4823
|
`${usageLine("claude", state.perAgent.claude)}\uFF1B${usageLine("codex", state.perAgent.codex)}\u3002`,
|
|
4385
|
-
`\u95F8\u95E8\u5DF2\u653E\u5F00\
|
|
4824
|
+
`\u95F8\u95E8\u5DF2\u653E\u5F00\uFF1ACodex ${recoveredText}\uFF0C\u4E14\u6CA1\u6709\u6709\u6548 rate_limit\u3002`,
|
|
4386
4825
|
"\u5EFA\u8BAE Claude \u7528 reply \u5E26\u4E0A\u5F53\u524D\u76EE\u6807\u3001checkpoint \u548C\u4E0B\u4E00\u6B65\uFF0C\u5524\u9192 Codex \u63A5\u7EED\u6267\u884C\u3002"
|
|
4387
4826
|
].join(`
|
|
4388
4827
|
`);
|
|
4389
4828
|
}
|
|
4390
|
-
toSnapshot(state) {
|
|
4829
|
+
toSnapshot(state, runway) {
|
|
4391
4830
|
const paused = this.isPaused();
|
|
4392
4831
|
return {
|
|
4393
4832
|
phase: paused ? "paused" : state.phase,
|
|
@@ -4403,18 +4842,23 @@ class BudgetCoordinator {
|
|
|
4403
4842
|
parallelRecommended: paused ? false : state.parallel.recommended,
|
|
4404
4843
|
codexTier: state.effort.codexTier,
|
|
4405
4844
|
claudeAdvice: state.effort.claudeAdvice,
|
|
4406
|
-
...this.burnRateSnapshotFields(state)
|
|
4845
|
+
...this.burnRateSnapshotFields(state, runway),
|
|
4846
|
+
...this.dynamicLineSnapshotFields(state)
|
|
4847
|
+
};
|
|
4848
|
+
}
|
|
4849
|
+
dynamicLineSnapshotFields(state) {
|
|
4850
|
+
return {
|
|
4851
|
+
dynamicPauseLine: {
|
|
4852
|
+
claude: effectiveDynamicLine(state.perAgent.claude, this.config, state.now),
|
|
4853
|
+
codex: effectiveDynamicLine(state.perAgent.codex, this.config, state.now)
|
|
4854
|
+
}
|
|
4407
4855
|
};
|
|
4408
4856
|
}
|
|
4409
|
-
burnRateSnapshotFields(state) {
|
|
4857
|
+
burnRateSnapshotFields(state, runway) {
|
|
4410
4858
|
const rates = {
|
|
4411
4859
|
claude: agentBurnRates(state.perAgent.claude),
|
|
4412
4860
|
codex: agentBurnRates(state.perAgent.codex)
|
|
4413
4861
|
};
|
|
4414
|
-
const runway = {
|
|
4415
|
-
claude: agentRunway(state.perAgent.claude, state.now),
|
|
4416
|
-
codex: agentRunway(state.perAgent.codex, state.now)
|
|
4417
|
-
};
|
|
4418
4862
|
if (!hasAnyBurnSignal(rates, runway))
|
|
4419
4863
|
return {};
|
|
4420
4864
|
return { burnRate: rates, runway };
|
|
@@ -4424,8 +4868,8 @@ class BudgetCoordinator {
|
|
|
4424
4868
|
// src/budget/quota-source.ts
|
|
4425
4869
|
import { execFile } from "child_process";
|
|
4426
4870
|
import { existsSync as existsSync5 } from "fs";
|
|
4427
|
-
import { homedir as
|
|
4428
|
-
import { basename, join as
|
|
4871
|
+
import { homedir as homedir3 } from "os";
|
|
4872
|
+
import { basename, join as join6 } from "path";
|
|
4429
4873
|
function parseBurnFields(record) {
|
|
4430
4874
|
const group = {};
|
|
4431
4875
|
let any = false;
|
|
@@ -4515,7 +4959,7 @@ function asFiniteNumber(value) {
|
|
|
4515
4959
|
function numberOr(value, fallback) {
|
|
4516
4960
|
return asFiniteNumber(value) ?? fallback;
|
|
4517
4961
|
}
|
|
4518
|
-
function
|
|
4962
|
+
function clamp2(value, min, max) {
|
|
4519
4963
|
return Math.min(max, Math.max(min, value));
|
|
4520
4964
|
}
|
|
4521
4965
|
function asRecord(value) {
|
|
@@ -4536,7 +4980,7 @@ function normalizeBucket(value, fetchedAt) {
|
|
|
4536
4980
|
}
|
|
4537
4981
|
return {
|
|
4538
4982
|
id,
|
|
4539
|
-
util:
|
|
4983
|
+
util: clamp2(util, 0, 100),
|
|
4540
4984
|
resetEpoch: Math.max(0, resetEpoch),
|
|
4541
4985
|
resetAfterSeconds: resetAfter === null ? null : Math.max(0, resetAfter),
|
|
4542
4986
|
burn: parseBurnFields(bucket)
|
|
@@ -4550,7 +4994,7 @@ function normalizeTopLevelBucket(record, util, fetchedAt) {
|
|
|
4550
4994
|
}
|
|
4551
4995
|
return {
|
|
4552
4996
|
id: "top_level",
|
|
4553
|
-
util:
|
|
4997
|
+
util: clamp2(util, 0, 100),
|
|
4554
4998
|
resetEpoch: Math.max(0, resetEpoch),
|
|
4555
4999
|
resetAfterSeconds: resetAfter === null ? null : Math.max(0, resetAfter),
|
|
4556
5000
|
burn: parseBurnFields(record)
|
|
@@ -4617,8 +5061,8 @@ function identifyWindows(buckets) {
|
|
|
4617
5061
|
function normalizeTolerantProbeRecord(record) {
|
|
4618
5062
|
const fetchedAt = numberOr(record.fetched_at ?? record.fetchedAt ?? record.now_epoch ?? record.nowEpoch, 0);
|
|
4619
5063
|
const hasFiniteUtil = asFiniteNumber(record.util ?? record.hard_util ?? record.hardUtil) !== null || asFiniteNumber(record.warn_util ?? record.warnUtil) !== null;
|
|
4620
|
-
const gateUtil =
|
|
4621
|
-
const warnUtil =
|
|
5064
|
+
const gateUtil = clamp2(numberOr(record.util ?? record.hard_util ?? record.hardUtil, 0), 0, 100);
|
|
5065
|
+
const warnUtil = clamp2(numberOr(record.warn_util ?? record.warnUtil, gateUtil), 0, 100);
|
|
4622
5066
|
const rawBuckets = Array.isArray(record.buckets) ? record.buckets : [];
|
|
4623
5067
|
const buckets = rawBuckets.map((bucket) => normalizeBucket(bucket, fetchedAt)).filter((bucket) => bucket !== null);
|
|
4624
5068
|
let parsedVia = "id-match";
|
|
@@ -4645,7 +5089,7 @@ function normalizeTolerantProbeRecord(record) {
|
|
|
4645
5089
|
warnUtil,
|
|
4646
5090
|
fiveHour,
|
|
4647
5091
|
weekly,
|
|
4648
|
-
remaining:
|
|
5092
|
+
remaining: clamp2(100 - gateUtil, 0, 100),
|
|
4649
5093
|
rateLimitedUntil,
|
|
4650
5094
|
fetchedAt,
|
|
4651
5095
|
parsedVia
|
|
@@ -4693,7 +5137,11 @@ function isDegradedUsage(usage, now = Math.floor(Date.now() / 1000)) {
|
|
|
4693
5137
|
if (usage.stale || !usage.ok)
|
|
4694
5138
|
return true;
|
|
4695
5139
|
const hasFreshWindow = usage.fiveHour !== null && usage.fiveHour.resetEpoch > now || usage.weekly !== null && usage.weekly.resetEpoch > now;
|
|
4696
|
-
|
|
5140
|
+
if (!hasFreshWindow)
|
|
5141
|
+
return true;
|
|
5142
|
+
if (usage.fetchedAt > 0 && now - usage.fetchedAt > STALE_MAX_AGE_SEC)
|
|
5143
|
+
return true;
|
|
5144
|
+
return false;
|
|
4697
5145
|
}
|
|
4698
5146
|
|
|
4699
5147
|
class QuotaSource {
|
|
@@ -4708,7 +5156,7 @@ class QuotaSource {
|
|
|
4708
5156
|
unknownSchemaVersionsLogged = new Set;
|
|
4709
5157
|
constructor(options = {}) {
|
|
4710
5158
|
this.env = options.env ?? process.env;
|
|
4711
|
-
this.homeDir = options.homeDir ??
|
|
5159
|
+
this.homeDir = options.homeDir ?? homedir3();
|
|
4712
5160
|
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
4713
5161
|
this.runner = options.runner ?? defaultRunner;
|
|
4714
5162
|
this.log = options.log ?? (() => {});
|
|
@@ -4740,13 +5188,13 @@ class QuotaSource {
|
|
|
4740
5188
|
add(command, commandKind(command));
|
|
4741
5189
|
return candidates;
|
|
4742
5190
|
}
|
|
4743
|
-
const binDir =
|
|
4744
|
-
const
|
|
4745
|
-
if (existsSync5(installedBudgetProbe))
|
|
4746
|
-
add(installedBudgetProbe, "budget-probe");
|
|
4747
|
-
const installedProbeMjs = join5(binDir, "probe.mjs");
|
|
5191
|
+
const binDir = join6(this.homeDir, ".budget-guard/bin");
|
|
5192
|
+
const installedProbeMjs = join6(binDir, "probe.mjs");
|
|
4748
5193
|
if (existsSync5(installedProbeMjs))
|
|
4749
5194
|
add(installedProbeMjs, "probe-mjs");
|
|
5195
|
+
const installedBudgetProbe = join6(binDir, "budget-probe");
|
|
5196
|
+
if (existsSync5(installedBudgetProbe))
|
|
5197
|
+
add(installedBudgetProbe, "budget-probe");
|
|
4750
5198
|
return candidates;
|
|
4751
5199
|
}
|
|
4752
5200
|
async fetchAgent(candidates, agent) {
|
|
@@ -4807,30 +5255,624 @@ function createQuotaSource(options) {
|
|
|
4807
5255
|
return new QuotaSource(options);
|
|
4808
5256
|
}
|
|
4809
5257
|
|
|
4810
|
-
// src/
|
|
4811
|
-
import {
|
|
4812
|
-
|
|
4813
|
-
function
|
|
4814
|
-
|
|
5258
|
+
// src/budget/pending-reader.ts
|
|
5259
|
+
import { createHash } from "crypto";
|
|
5260
|
+
import { join as join7 } from "path";
|
|
5261
|
+
function nodeFs() {
|
|
5262
|
+
return __require("fs");
|
|
5263
|
+
}
|
|
5264
|
+
function cwdMatches(entryCwd, optsCwd) {
|
|
5265
|
+
if (entryCwd === optsCwd)
|
|
5266
|
+
return true;
|
|
4815
5267
|
try {
|
|
4816
|
-
|
|
5268
|
+
const fs2 = nodeFs();
|
|
5269
|
+
return fs2.realpathSync(entryCwd) === fs2.realpathSync(optsCwd);
|
|
4817
5270
|
} catch {
|
|
4818
5271
|
return false;
|
|
4819
5272
|
}
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
if (
|
|
4824
|
-
return
|
|
4825
|
-
const
|
|
4826
|
-
if (
|
|
4827
|
-
return
|
|
4828
|
-
|
|
5273
|
+
}
|
|
5274
|
+
function parsePendingPayload(value) {
|
|
5275
|
+
const record = asRecord(value);
|
|
5276
|
+
if (!record)
|
|
5277
|
+
return null;
|
|
5278
|
+
const sessionId = record.session_id;
|
|
5279
|
+
if (typeof sessionId !== "string" || sessionId === "")
|
|
5280
|
+
return null;
|
|
5281
|
+
const util = asFiniteNumber(record.util);
|
|
5282
|
+
if (util === null)
|
|
5283
|
+
return null;
|
|
5284
|
+
const warnUtil = numberOr(record.warn_util, util);
|
|
5285
|
+
const resetEpoch = numberOr(record.reset_epoch ?? record.reset, 0);
|
|
5286
|
+
const at = numberOr(record.at, 0);
|
|
5287
|
+
const cwd = typeof record.cwd === "string" ? record.cwd : "";
|
|
5288
|
+
const status = typeof record.status === "string" ? record.status : "";
|
|
5289
|
+
const agent = record.agent === "claude" || record.agent === "codex" ? record.agent : null;
|
|
5290
|
+
if (agent === null)
|
|
5291
|
+
return null;
|
|
5292
|
+
return { status, agent, sessionId, cwd, resetEpoch, util, warnUtil, at, sourcePath: "", contentHash: "" };
|
|
5293
|
+
}
|
|
5294
|
+
function sha256(value) {
|
|
5295
|
+
return createHash("sha256").update(value).digest("hex");
|
|
5296
|
+
}
|
|
5297
|
+
function resolveStateDir2(homeDir) {
|
|
5298
|
+
const override = process.env.BUDGET_STATE_DIR;
|
|
5299
|
+
if (override && override.trim() !== "")
|
|
5300
|
+
return override.trim();
|
|
5301
|
+
return join7(homeDir, ".budget-guard");
|
|
5302
|
+
}
|
|
5303
|
+
function readPendingFile(path, log) {
|
|
5304
|
+
let raw;
|
|
5305
|
+
try {
|
|
5306
|
+
raw = nodeFs().readFileSync(path, "utf-8");
|
|
5307
|
+
} catch (error) {
|
|
5308
|
+
log(`pending reader: skip unreadable ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5309
|
+
return null;
|
|
5310
|
+
}
|
|
5311
|
+
const text = String(raw).trim();
|
|
5312
|
+
if (text === "")
|
|
5313
|
+
return null;
|
|
5314
|
+
let parsed;
|
|
5315
|
+
try {
|
|
5316
|
+
parsed = JSON.parse(text);
|
|
5317
|
+
} catch {
|
|
5318
|
+
log(`pending reader: skip malformed JSON ${path}`);
|
|
5319
|
+
return null;
|
|
5320
|
+
}
|
|
5321
|
+
const entry = parsePendingPayload(parsed);
|
|
5322
|
+
if (!entry)
|
|
5323
|
+
return null;
|
|
5324
|
+
return { ...entry, sourcePath: path, contentHash: sha256(text) };
|
|
5325
|
+
}
|
|
5326
|
+
function listScopeFiles(stateDir, agent, log) {
|
|
5327
|
+
const pendingDir = join7(stateDir, "pending");
|
|
5328
|
+
let names;
|
|
5329
|
+
try {
|
|
5330
|
+
names = nodeFs().readdirSync(pendingDir);
|
|
5331
|
+
} catch {
|
|
5332
|
+
return [];
|
|
5333
|
+
}
|
|
5334
|
+
const prefix = `${agent}_`;
|
|
5335
|
+
return names.filter((name) => name.startsWith(prefix) && name.endsWith(".json")).map((name) => join7(pendingDir, name));
|
|
5336
|
+
}
|
|
5337
|
+
function readGuardPending(opts) {
|
|
5338
|
+
const log = opts.log ?? (() => {});
|
|
5339
|
+
const stateDir = resolveStateDir2(opts.homeDir);
|
|
5340
|
+
const paths = [
|
|
5341
|
+
...listScopeFiles(stateDir, opts.agent, log),
|
|
5342
|
+
join7(stateDir, `pending_${opts.agent}.json`)
|
|
5343
|
+
];
|
|
5344
|
+
const bySession = new Map;
|
|
5345
|
+
for (const path of paths) {
|
|
5346
|
+
const entry = readPendingFile(path, log);
|
|
5347
|
+
if (!entry)
|
|
5348
|
+
continue;
|
|
5349
|
+
if (entry.agent !== opts.agent)
|
|
5350
|
+
continue;
|
|
5351
|
+
if (entry.status !== "paused")
|
|
5352
|
+
continue;
|
|
5353
|
+
if (opts.cwd !== undefined && !cwdMatches(entry.cwd, opts.cwd))
|
|
5354
|
+
continue;
|
|
5355
|
+
if (!bySession.has(entry.sessionId)) {
|
|
5356
|
+
bySession.set(entry.sessionId, entry);
|
|
5357
|
+
}
|
|
5358
|
+
}
|
|
5359
|
+
return [...bySession.values()];
|
|
5360
|
+
}
|
|
5361
|
+
|
|
5362
|
+
// src/budget/resume-injection-queue.ts
|
|
5363
|
+
import { createHash as createHash2 } from "crypto";
|
|
5364
|
+
import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync5, openSync as openSync3, readdirSync, readFileSync as readFileSync6, realpathSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
5365
|
+
import { join as join8 } from "path";
|
|
5366
|
+
|
|
5367
|
+
// src/budget/resume-prompt.ts
|
|
5368
|
+
var RESUME_PROMPT = "\u989D\u5EA6\u7A97\u53E3\u5DF2\u5237\u65B0\uFF0C\u7EE7\u7EED\u4E0A\u6B21\u672A\u5B8C\u6210\u7684\u4EFB\u52A1\uFF1A\u4ECE .agent/checkpoint.md \u7684\u300C\u4E0B\u4E00\u6B65\u300D\u63A5\u7740\u505A\uFF1B\u5B8C\u6210\u540E\u505C\u4E0B\u5E76\u6807 DONE\u3002";
|
|
5369
|
+
function claudeResumePrompt(resumeId) {
|
|
5370
|
+
return "\u989D\u5EA6\u7A97\u53E3\u5DF2\u5237\u65B0\u3002" + `\u8BF7\u5148\u8C03\u7528 ack_resume(resume_id="${resumeId}", status="resumed") \u786E\u8BA4\u5DF2\u6536\u5230\u672C\u901A\u77E5\uFF08ACK = \u5DF2\u63A5\u6536\uFF0C\u4E0D\u662F\u5B8C\u6210\uFF0C\u8BF7\u7ACB\u5373\u8C03\u7528\uFF0C\u4E0D\u8981\u7B49\u4EFB\u52A1\u505A\u5B8C\uFF09\uFF0C` + "\u518D\u4ECE .agent/checkpoint.md \u7684\u300C\u4E0B\u4E00\u6B65\u300D\u63A5\u7740\u505A\uFF1B\u5B8C\u6210\u540E\u505C\u4E0B\u5E76\u6807 DONE\u3002";
|
|
5371
|
+
}
|
|
5372
|
+
|
|
5373
|
+
// src/budget/resume-injection-queue.ts
|
|
5374
|
+
var DEFAULT_RETRY_MS = 5000;
|
|
5375
|
+
var DEFAULT_CONFIRM_TIMEOUT_MS = 60000;
|
|
5376
|
+
var DEFAULT_MAX_ATTEMPTS = 5;
|
|
5377
|
+
var DEFAULT_STALE_CLAIM_TTL_SEC = 300;
|
|
5378
|
+
var DEFAULT_CONSUMED_TTL_SEC = 7 * 24 * 3600;
|
|
5379
|
+
function finitePositive(value, fallback) {
|
|
5380
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
5381
|
+
}
|
|
5382
|
+
|
|
5383
|
+
class ResumeInjectionQueue {
|
|
5384
|
+
inject;
|
|
5385
|
+
scheduler;
|
|
5386
|
+
retryMs;
|
|
5387
|
+
confirmTimeoutMs;
|
|
5388
|
+
maxAttempts;
|
|
5389
|
+
log;
|
|
5390
|
+
onInjectionAccepted;
|
|
5391
|
+
onInjectionSuperseded;
|
|
5392
|
+
onConfirmed;
|
|
5393
|
+
onAbandoned;
|
|
5394
|
+
entries = new Map;
|
|
5395
|
+
resetSweepDepth = 0;
|
|
5396
|
+
constructor(options) {
|
|
5397
|
+
this.inject = options.inject;
|
|
5398
|
+
this.scheduler = options.scheduler ?? globalThis;
|
|
5399
|
+
this.retryMs = finitePositive(options.retryMs, DEFAULT_RETRY_MS);
|
|
5400
|
+
this.confirmTimeoutMs = finitePositive(options.confirmTimeoutMs, DEFAULT_CONFIRM_TIMEOUT_MS);
|
|
5401
|
+
this.maxAttempts = finitePositive(options.maxAttempts, DEFAULT_MAX_ATTEMPTS);
|
|
5402
|
+
this.log = options.log ?? (() => {});
|
|
5403
|
+
this.onInjectionAccepted = options.onInjectionAccepted ?? (() => {});
|
|
5404
|
+
this.onInjectionSuperseded = options.onInjectionSuperseded ?? (() => {});
|
|
5405
|
+
this.onConfirmed = options.onConfirmed ?? (() => {});
|
|
5406
|
+
this.onAbandoned = options.onAbandoned ?? (() => {});
|
|
5407
|
+
}
|
|
5408
|
+
get size() {
|
|
5409
|
+
return this.entries.size;
|
|
5410
|
+
}
|
|
5411
|
+
get(resumeId) {
|
|
5412
|
+
const entry = this.entries.get(resumeId);
|
|
5413
|
+
if (!entry)
|
|
5414
|
+
return;
|
|
5415
|
+
const { retryTimer: _retryTimer, confirmTimer: _confirmTimer, claim: _claim, ...publicEntry } = entry;
|
|
5416
|
+
return { ...publicEntry };
|
|
5417
|
+
}
|
|
5418
|
+
enqueue(input) {
|
|
5419
|
+
if (this.entries.has(input.resumeId)) {
|
|
5420
|
+
this.log(`resume injection deduped: ${input.resumeId}`);
|
|
5421
|
+
try {
|
|
5422
|
+
input.claim?.release();
|
|
5423
|
+
} catch (error) {
|
|
5424
|
+
this.log(`resume claim release failed (${input.resumeId} dedup): ${error instanceof Error ? error.message : String(error)}`);
|
|
5425
|
+
}
|
|
5426
|
+
return;
|
|
5427
|
+
}
|
|
5428
|
+
if (input.claim) {
|
|
5429
|
+
const identity = input.claim.identity;
|
|
5430
|
+
for (const existing of this.entries.values()) {
|
|
5431
|
+
if (existing.claim && existing.claim.identity === identity) {
|
|
5432
|
+
this.log(`resume injection identity-deduped: ${input.resumeId} ~ existing ${existing.resumeId} (identity ${identity})`);
|
|
5433
|
+
existing.claim = input.claim;
|
|
5434
|
+
return;
|
|
5435
|
+
}
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
this.entries.set(input.resumeId, {
|
|
5439
|
+
resumeId: input.resumeId,
|
|
5440
|
+
prompt: input.prompt ?? RESUME_PROMPT,
|
|
5441
|
+
state: "pending",
|
|
5442
|
+
attempts: 0,
|
|
5443
|
+
...input.claim ? { claim: input.claim } : {}
|
|
5444
|
+
});
|
|
5445
|
+
this.tryInjectNext();
|
|
5446
|
+
}
|
|
5447
|
+
onTurnDrained() {
|
|
5448
|
+
this.tryInjectNext();
|
|
5449
|
+
}
|
|
5450
|
+
stop() {
|
|
5451
|
+
for (const entry of this.entries.values()) {
|
|
5452
|
+
const requestId = entry.injectionId;
|
|
5453
|
+
this.clearRetryTimer(entry);
|
|
5454
|
+
this.clearConfirmTimer(entry);
|
|
5455
|
+
if (requestId !== undefined) {
|
|
5456
|
+
this.onInjectionSuperseded({ resumeId: entry.resumeId, requestId, reason: "stop" });
|
|
5457
|
+
}
|
|
5458
|
+
try {
|
|
5459
|
+
entry.claim?.release();
|
|
5460
|
+
} catch (error) {
|
|
5461
|
+
this.log(`resume claim release failed (${entry.resumeId}): ${error instanceof Error ? error.message : String(error)}`);
|
|
5462
|
+
}
|
|
5463
|
+
}
|
|
5464
|
+
this.entries.clear();
|
|
5465
|
+
}
|
|
5466
|
+
onTurnTrackingReset() {
|
|
5467
|
+
this.resetSweepDepth++;
|
|
5468
|
+
try {
|
|
5469
|
+
for (const entry of [...this.entries.values()]) {
|
|
5470
|
+
if (entry.state === "awaiting_confirm") {
|
|
5471
|
+
this.supersedeAwaiting(entry, "turn_tracking_reset");
|
|
5472
|
+
this.countRealAttemptOrAbandon(entry, "turn tracking reset before turn/start confirmation");
|
|
5473
|
+
} else if (entry.state === "pending") {
|
|
5474
|
+
this.clearRetryTimer(entry);
|
|
5475
|
+
this.scheduleRetry(entry);
|
|
5476
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
} finally {
|
|
5479
|
+
this.resetSweepDepth--;
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
onBridgeTurnStarted(event) {
|
|
5483
|
+
const entry = this.entries.get(event.resumeId);
|
|
5484
|
+
if (!entry || entry.state !== "awaiting_confirm" || entry.injectionId !== event.requestId)
|
|
5485
|
+
return;
|
|
5486
|
+
this.clearConfirmTimer(entry);
|
|
5487
|
+
try {
|
|
5488
|
+
entry.claim?.consume();
|
|
5489
|
+
} catch (error) {
|
|
5490
|
+
this.log(`resume claim consume failed (${event.resumeId}): ${error instanceof Error ? error.message : String(error)}`);
|
|
5491
|
+
}
|
|
5492
|
+
this.entries.delete(event.resumeId);
|
|
5493
|
+
this.onConfirmed({ resumeId: event.resumeId, requestId: event.requestId, turnId: event.turnId });
|
|
5494
|
+
}
|
|
5495
|
+
onBridgeTurnRejected(event) {
|
|
5496
|
+
const entry = this.entries.get(event.resumeId);
|
|
5497
|
+
if (!entry || entry.state !== "awaiting_confirm" || entry.injectionId !== event.requestId)
|
|
5498
|
+
return;
|
|
5499
|
+
this.supersedeAwaiting(entry, "bridge_rejected");
|
|
5500
|
+
this.countRealAttemptOrAbandon(entry, event.error);
|
|
5501
|
+
}
|
|
5502
|
+
tryInjectNext() {
|
|
5503
|
+
if (this.resetSweepDepth > 0)
|
|
5504
|
+
return;
|
|
5505
|
+
for (const entry of this.entries.values()) {
|
|
5506
|
+
if (entry.state === "awaiting_confirm")
|
|
5507
|
+
return;
|
|
5508
|
+
}
|
|
5509
|
+
for (const entry of this.entries.values()) {
|
|
5510
|
+
if (entry.state !== "pending")
|
|
5511
|
+
continue;
|
|
5512
|
+
this.clearRetryTimer(entry);
|
|
5513
|
+
let requestId;
|
|
5514
|
+
try {
|
|
5515
|
+
requestId = this.inject(entry.prompt);
|
|
5516
|
+
} catch (error) {
|
|
5517
|
+
this.countRealAttemptOrAbandon(entry, error instanceof Error ? error.message : String(error));
|
|
5518
|
+
return;
|
|
5519
|
+
}
|
|
5520
|
+
if (requestId === null) {
|
|
5521
|
+
this.scheduleRetry(entry);
|
|
5522
|
+
return;
|
|
5523
|
+
}
|
|
5524
|
+
entry.state = "awaiting_confirm";
|
|
5525
|
+
entry.injectionId = requestId;
|
|
5526
|
+
this.onInjectionAccepted({ resumeId: entry.resumeId, requestId });
|
|
5527
|
+
this.scheduleConfirmTimeout(entry);
|
|
5528
|
+
return;
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
countRealAttemptOrAbandon(entry, reason) {
|
|
5532
|
+
entry.attempts += 1;
|
|
5533
|
+
if (entry.attempts >= this.maxAttempts) {
|
|
5534
|
+
this.abandon(entry, reason);
|
|
5535
|
+
return;
|
|
5536
|
+
}
|
|
5537
|
+
this.scheduleRetry(entry);
|
|
5538
|
+
}
|
|
5539
|
+
abandon(entry, reason) {
|
|
5540
|
+
this.clearRetryTimer(entry);
|
|
5541
|
+
this.clearConfirmTimer(entry);
|
|
5542
|
+
this.entries.delete(entry.resumeId);
|
|
5543
|
+
try {
|
|
5544
|
+
entry.claim?.release();
|
|
5545
|
+
} catch (error) {
|
|
5546
|
+
this.log(`resume claim release failed (${entry.resumeId}): ${error instanceof Error ? error.message : String(error)}`);
|
|
5547
|
+
}
|
|
5548
|
+
this.onAbandoned({ resumeId: entry.resumeId, reason });
|
|
5549
|
+
this.tryInjectNext();
|
|
5550
|
+
}
|
|
5551
|
+
supersedeAwaiting(entry, reason) {
|
|
5552
|
+
this.clearConfirmTimer(entry);
|
|
5553
|
+
const requestId = entry.injectionId;
|
|
5554
|
+
delete entry.injectionId;
|
|
5555
|
+
entry.state = "pending";
|
|
5556
|
+
if (requestId !== undefined) {
|
|
5557
|
+
this.onInjectionSuperseded({ resumeId: entry.resumeId, requestId, reason });
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
scheduleRetry(entry) {
|
|
5561
|
+
if (!this.entries.has(entry.resumeId))
|
|
5562
|
+
return;
|
|
5563
|
+
this.clearRetryTimer(entry);
|
|
5564
|
+
entry.retryTimer = this.scheduler.setTimeout(() => {
|
|
5565
|
+
delete entry.retryTimer;
|
|
5566
|
+
this.tryInjectNext();
|
|
5567
|
+
}, this.retryMs);
|
|
5568
|
+
entry.retryTimer?.unref?.();
|
|
5569
|
+
}
|
|
5570
|
+
scheduleConfirmTimeout(entry) {
|
|
5571
|
+
this.clearConfirmTimer(entry);
|
|
5572
|
+
entry.confirmTimer = this.scheduler.setTimeout(() => {
|
|
5573
|
+
delete entry.confirmTimer;
|
|
5574
|
+
if (entry.state !== "awaiting_confirm")
|
|
5575
|
+
return;
|
|
5576
|
+
this.supersedeAwaiting(entry, "confirm_timeout");
|
|
5577
|
+
this.countRealAttemptOrAbandon(entry, "turn/start confirmation timed out");
|
|
5578
|
+
}, this.confirmTimeoutMs);
|
|
5579
|
+
entry.confirmTimer?.unref?.();
|
|
5580
|
+
}
|
|
5581
|
+
clearRetryTimer(entry) {
|
|
5582
|
+
if (entry.retryTimer === undefined)
|
|
5583
|
+
return;
|
|
5584
|
+
this.scheduler.clearTimeout(entry.retryTimer);
|
|
5585
|
+
delete entry.retryTimer;
|
|
5586
|
+
}
|
|
5587
|
+
clearConfirmTimer(entry) {
|
|
5588
|
+
if (entry.confirmTimer === undefined)
|
|
5589
|
+
return;
|
|
5590
|
+
this.scheduler.clearTimeout(entry.confirmTimer);
|
|
5591
|
+
delete entry.confirmTimer;
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
function realpathOrRaw(path) {
|
|
5595
|
+
try {
|
|
5596
|
+
return realpathSync(path);
|
|
5597
|
+
} catch {
|
|
5598
|
+
return path;
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
function sha2562(value) {
|
|
5602
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
5603
|
+
}
|
|
5604
|
+
function writeJsonWx(path, value) {
|
|
5605
|
+
let fd;
|
|
5606
|
+
try {
|
|
5607
|
+
fd = openSync3(path, "wx", 384);
|
|
5608
|
+
} catch (error) {
|
|
5609
|
+
if (error?.code === "EEXIST")
|
|
5610
|
+
return false;
|
|
5611
|
+
throw error;
|
|
5612
|
+
}
|
|
5613
|
+
try {
|
|
5614
|
+
writeFileSync3(fd, JSON.stringify(value, null, 2));
|
|
5615
|
+
} finally {
|
|
5616
|
+
closeSync3(fd);
|
|
5617
|
+
}
|
|
5618
|
+
return true;
|
|
5619
|
+
}
|
|
5620
|
+
function unlinkIfExists(path) {
|
|
5621
|
+
try {
|
|
5622
|
+
unlinkSync4(path);
|
|
5623
|
+
} catch (error) {
|
|
5624
|
+
if (error?.code === "ENOENT")
|
|
5625
|
+
return;
|
|
5626
|
+
throw error;
|
|
5627
|
+
}
|
|
5628
|
+
}
|
|
5629
|
+
function readClaimedAt(path) {
|
|
5630
|
+
try {
|
|
5631
|
+
const parsed = JSON.parse(readFileSync6(path, "utf-8"));
|
|
5632
|
+
const claimedAt = parsed?.claimed_at;
|
|
5633
|
+
return typeof claimedAt === "number" && Number.isFinite(claimedAt) ? claimedAt : null;
|
|
5634
|
+
} catch {
|
|
5635
|
+
return null;
|
|
5636
|
+
}
|
|
5637
|
+
}
|
|
5638
|
+
function pruneStaleResumeArtifacts(dir, tsField, ttlSec, nowSec, log) {
|
|
5639
|
+
let names;
|
|
5640
|
+
try {
|
|
5641
|
+
names = readdirSync(dir);
|
|
5642
|
+
} catch {
|
|
5643
|
+
return;
|
|
5644
|
+
}
|
|
5645
|
+
for (const name of names) {
|
|
5646
|
+
if (!name.endsWith(".json"))
|
|
5647
|
+
continue;
|
|
5648
|
+
const p = join8(dir, name);
|
|
5649
|
+
try {
|
|
5650
|
+
const parsed = JSON.parse(readFileSync6(p, "utf-8"));
|
|
5651
|
+
const ts = parsed?.[tsField];
|
|
5652
|
+
if (typeof ts === "number" && Number.isFinite(ts) && nowSec - ts > ttlSec) {
|
|
5653
|
+
unlinkIfExists(p);
|
|
5654
|
+
}
|
|
5655
|
+
} catch (error) {
|
|
5656
|
+
log?.(`resume artifact prune skipped ${p}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
function tryClaimPendingResume(opts) {
|
|
5661
|
+
const now = opts.now ?? (() => Math.floor(Date.now() / 1000));
|
|
5662
|
+
const claimTtlSec = finitePositive(opts.claimTtlSec, DEFAULT_STALE_CLAIM_TTL_SEC);
|
|
5663
|
+
const consumedTtlSec = finitePositive(opts.consumedTtlSec, DEFAULT_CONSUMED_TTL_SEC);
|
|
5664
|
+
const cwd = realpathOrRaw(opts.pending.cwd);
|
|
5665
|
+
const sourcePath = opts.pending.sourcePath ?? "";
|
|
5666
|
+
const contentHash = opts.pending.contentHash ?? "";
|
|
5667
|
+
const identity = sha2562([
|
|
5668
|
+
opts.agent,
|
|
5669
|
+
opts.pending.sessionId,
|
|
5670
|
+
cwd,
|
|
5671
|
+
contentHash
|
|
5672
|
+
].join("\x00"));
|
|
5673
|
+
const claimsDir = join8(opts.stateDir, "claims");
|
|
5674
|
+
const consumedDir = join8(opts.stateDir, "consumed");
|
|
5675
|
+
const claimPath = join8(claimsDir, `${identity}.json`);
|
|
5676
|
+
const consumedPath = join8(consumedDir, `${identity}.json`);
|
|
5677
|
+
mkdirSync5(claimsDir, { recursive: true });
|
|
5678
|
+
mkdirSync5(consumedDir, { recursive: true });
|
|
5679
|
+
const nowSec = now();
|
|
5680
|
+
pruneStaleResumeArtifacts(consumedDir, "consumed_at", consumedTtlSec, nowSec, opts.log);
|
|
5681
|
+
pruneStaleResumeArtifacts(claimsDir, "claimed_at", claimTtlSec, nowSec, opts.log);
|
|
5682
|
+
if (existsSync6(consumedPath))
|
|
5683
|
+
return { ok: false, reason: "consumed" };
|
|
5684
|
+
if (existsSync6(claimPath)) {
|
|
5685
|
+
const claimedAt = readClaimedAt(claimPath);
|
|
5686
|
+
if (claimedAt !== null && nowSec - claimedAt > claimTtlSec) {
|
|
5687
|
+
try {
|
|
5688
|
+
unlinkIfExists(claimPath);
|
|
5689
|
+
} catch (error) {
|
|
5690
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5691
|
+
opts.log?.(`stale resume claim cleanup failed: ${message}`);
|
|
5692
|
+
return { ok: false, reason: "error", error: message };
|
|
5693
|
+
}
|
|
5694
|
+
} else {
|
|
5695
|
+
return { ok: false, reason: "claimed" };
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
const payload = {
|
|
5699
|
+
identity,
|
|
5700
|
+
agent: opts.agent,
|
|
5701
|
+
session_id: opts.pending.sessionId,
|
|
5702
|
+
cwd,
|
|
5703
|
+
pending_path: sourcePath,
|
|
5704
|
+
pending_hash: contentHash,
|
|
5705
|
+
checkpoint_path: opts.checkpointPath,
|
|
5706
|
+
claimed_at: nowSec
|
|
5707
|
+
};
|
|
5708
|
+
try {
|
|
5709
|
+
if (!writeJsonWx(claimPath, payload))
|
|
5710
|
+
return { ok: false, reason: "claimed" };
|
|
5711
|
+
} catch (error) {
|
|
5712
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5713
|
+
opts.log?.(`resume claim failed: ${message}`);
|
|
5714
|
+
return { ok: false, reason: "error", error: message };
|
|
5715
|
+
}
|
|
5716
|
+
return {
|
|
5717
|
+
ok: true,
|
|
5718
|
+
claim: {
|
|
5719
|
+
identity,
|
|
5720
|
+
claimPath,
|
|
5721
|
+
consumedPath,
|
|
5722
|
+
consume: () => {
|
|
5723
|
+
mkdirSync5(consumedDir, { recursive: true });
|
|
5724
|
+
writeFileSync3(consumedPath, JSON.stringify({ ...payload, consumed_at: now() }, null, 2));
|
|
5725
|
+
unlinkIfExists(claimPath);
|
|
5726
|
+
},
|
|
5727
|
+
release: () => {
|
|
5728
|
+
unlinkIfExists(claimPath);
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
};
|
|
5732
|
+
}
|
|
5733
|
+
|
|
5734
|
+
// src/budget/resume-ack-tracker.ts
|
|
5735
|
+
function finitePositive2(value, fallback) {
|
|
5736
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
5737
|
+
}
|
|
5738
|
+
|
|
5739
|
+
class ResumeAckTracker {
|
|
5740
|
+
push;
|
|
5741
|
+
scheduler;
|
|
5742
|
+
timeoutMs;
|
|
5743
|
+
retries;
|
|
5744
|
+
onDegraded;
|
|
5745
|
+
entries = new Map;
|
|
5746
|
+
deliverySeq = 0;
|
|
5747
|
+
constructor(options) {
|
|
5748
|
+
this.push = options.push;
|
|
5749
|
+
this.scheduler = options.scheduler;
|
|
5750
|
+
this.timeoutMs = finitePositive2(options.timeoutMs, 60000);
|
|
5751
|
+
this.retries = finitePositive2(options.retries, 3);
|
|
5752
|
+
this.onDegraded = options.onDegraded ?? (() => {});
|
|
5753
|
+
}
|
|
5754
|
+
get size() {
|
|
5755
|
+
return this.entries.size;
|
|
5756
|
+
}
|
|
5757
|
+
get(resumeId) {
|
|
5758
|
+
const entry = this.entries.get(resumeId);
|
|
5759
|
+
if (!entry)
|
|
5760
|
+
return;
|
|
5761
|
+
const { timer: _timer, ...publicEntry } = entry;
|
|
5762
|
+
return { ...publicEntry };
|
|
5763
|
+
}
|
|
5764
|
+
start(resumeId) {
|
|
5765
|
+
if (this.entries.has(resumeId))
|
|
5766
|
+
return;
|
|
5767
|
+
const entry = { resumeId, attempts: 0, state: "awaiting_ack" };
|
|
5768
|
+
this.entries.set(resumeId, entry);
|
|
5769
|
+
this.pushAttempt(entry);
|
|
5770
|
+
this.armTimer(entry);
|
|
5771
|
+
}
|
|
5772
|
+
ack(resumeId) {
|
|
5773
|
+
const entry = this.entries.get(resumeId);
|
|
5774
|
+
if (!entry)
|
|
5775
|
+
return;
|
|
5776
|
+
this.clearTimer(entry);
|
|
5777
|
+
entry.state = "resumed";
|
|
5778
|
+
this.entries.delete(resumeId);
|
|
5779
|
+
}
|
|
5780
|
+
stop() {
|
|
5781
|
+
for (const entry of this.entries.values()) {
|
|
5782
|
+
this.clearTimer(entry);
|
|
5783
|
+
}
|
|
5784
|
+
this.entries.clear();
|
|
5785
|
+
}
|
|
5786
|
+
pushAttempt(entry) {
|
|
5787
|
+
const deliveryId = `${entry.resumeId}_retry${entry.attempts}_${++this.deliverySeq}`;
|
|
5788
|
+
this.push({ resumeId: entry.resumeId, deliveryId, attempt: entry.attempts });
|
|
5789
|
+
}
|
|
5790
|
+
armTimer(entry) {
|
|
5791
|
+
this.clearTimer(entry);
|
|
5792
|
+
entry.timer = this.scheduler.setTimeout(() => {
|
|
5793
|
+
delete entry.timer;
|
|
5794
|
+
this.onTimeout(entry);
|
|
5795
|
+
}, this.timeoutMs);
|
|
5796
|
+
entry.timer?.unref?.();
|
|
5797
|
+
}
|
|
5798
|
+
onTimeout(entry) {
|
|
5799
|
+
if (entry.state !== "awaiting_ack" || !this.entries.has(entry.resumeId))
|
|
5800
|
+
return;
|
|
5801
|
+
entry.attempts += 1;
|
|
5802
|
+
if (entry.attempts >= this.retries) {
|
|
5803
|
+
entry.state = "degraded";
|
|
5804
|
+
this.entries.delete(entry.resumeId);
|
|
5805
|
+
this.onDegraded(entry.resumeId);
|
|
5806
|
+
return;
|
|
5807
|
+
}
|
|
5808
|
+
this.pushAttempt(entry);
|
|
5809
|
+
this.armTimer(entry);
|
|
5810
|
+
}
|
|
5811
|
+
clearTimer(entry) {
|
|
5812
|
+
if (entry.timer === undefined)
|
|
5813
|
+
return;
|
|
5814
|
+
this.scheduler.clearTimeout(entry.timer);
|
|
5815
|
+
delete entry.timer;
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
|
|
5819
|
+
// src/budget/route-resume.ts
|
|
5820
|
+
function routeResume(side, resumeId, deps) {
|
|
5821
|
+
if (side === "codex") {
|
|
5822
|
+
deps.enqueueCodex(resumeId);
|
|
5823
|
+
return;
|
|
5824
|
+
}
|
|
5825
|
+
deps.claudeTracker.start(resumeId);
|
|
5826
|
+
}
|
|
5827
|
+
|
|
5828
|
+
// src/budget/resume-ack-sentinel.ts
|
|
5829
|
+
import { renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5830
|
+
import { join as join9 } from "path";
|
|
5831
|
+
var RESUME_ACK_DEGRADED_SENTINEL = "resume-ack-degraded.json";
|
|
5832
|
+
function resumeAckSentinelPath(stateDir) {
|
|
5833
|
+
return join9(stateDir, RESUME_ACK_DEGRADED_SENTINEL);
|
|
5834
|
+
}
|
|
5835
|
+
function writeResumeAckDegradedSentinel(opts) {
|
|
5836
|
+
const now = opts.now ?? (() => Date.now());
|
|
5837
|
+
const payload = {
|
|
5838
|
+
resumeId: opts.resumeId,
|
|
5839
|
+
degradedAt: now()
|
|
5840
|
+
};
|
|
5841
|
+
const target = resumeAckSentinelPath(opts.stateDir);
|
|
5842
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
5843
|
+
try {
|
|
5844
|
+
writeFileSync4(tmp, JSON.stringify(payload, null, 2), { mode: 384 });
|
|
5845
|
+
renameSync3(tmp, target);
|
|
5846
|
+
opts.log?.(`Resume-ack degraded sentinel written: ${opts.resumeId}`);
|
|
5847
|
+
} catch (err) {
|
|
5848
|
+
opts.log?.(`Resume-ack degraded sentinel write failed (${opts.resumeId}): ${err?.message ?? err}`);
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5851
|
+
|
|
5852
|
+
// src/daemon-identity-ownership.ts
|
|
5853
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
5854
|
+
var defaultRead2 = (path) => readFileSync7(path, "utf-8");
|
|
5855
|
+
function pidFileOwnedByUs(pidFilePath, ourPid, read = defaultRead2) {
|
|
5856
|
+
let raw;
|
|
5857
|
+
try {
|
|
5858
|
+
raw = read(pidFilePath);
|
|
5859
|
+
} catch {
|
|
5860
|
+
return false;
|
|
5861
|
+
}
|
|
5862
|
+
const trimmed = raw.trim();
|
|
5863
|
+
if (trimmed.length === 0)
|
|
5864
|
+
return false;
|
|
5865
|
+
if (!/^[+-]?\d+$/.test(trimmed))
|
|
5866
|
+
return false;
|
|
5867
|
+
const pid = Number.parseInt(trimmed, 10);
|
|
5868
|
+
if (!Number.isFinite(pid))
|
|
5869
|
+
return false;
|
|
5870
|
+
return pid === ourPid;
|
|
4829
5871
|
}
|
|
4830
5872
|
|
|
4831
|
-
// src/idempotency-tracker.ts
|
|
4832
|
-
var DEFAULT_TOMBSTONE_TTL_MS = 20 * 60 * 1000;
|
|
4833
|
-
|
|
5873
|
+
// src/idempotency-tracker.ts
|
|
5874
|
+
var DEFAULT_TOMBSTONE_TTL_MS = 20 * 60 * 1000;
|
|
5875
|
+
|
|
4834
5876
|
class IdempotencyTracker {
|
|
4835
5877
|
entries = new Map;
|
|
4836
5878
|
ttlMs;
|
|
@@ -4977,12 +6019,12 @@ class ReplyRequiredTracker {
|
|
|
4977
6019
|
|
|
4978
6020
|
// src/thread-state.ts
|
|
4979
6021
|
import {
|
|
4980
|
-
existsSync as
|
|
4981
|
-
readdirSync,
|
|
4982
|
-
readFileSync as
|
|
6022
|
+
existsSync as existsSync7,
|
|
6023
|
+
readdirSync as readdirSync2,
|
|
6024
|
+
readFileSync as readFileSync8
|
|
4983
6025
|
} from "fs";
|
|
4984
|
-
import { homedir as
|
|
4985
|
-
import { basename as basename2, join as
|
|
6026
|
+
import { homedir as homedir4 } from "os";
|
|
6027
|
+
import { basename as basename2, join as join10 } from "path";
|
|
4986
6028
|
function nowIso() {
|
|
4987
6029
|
return new Date().toISOString();
|
|
4988
6030
|
}
|
|
@@ -4991,11 +6033,11 @@ function threadTag(identity) {
|
|
|
4991
6033
|
return `abg:${name}:${identity.cwd}`;
|
|
4992
6034
|
}
|
|
4993
6035
|
function codexHome(env = process.env) {
|
|
4994
|
-
return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME :
|
|
6036
|
+
return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : join10(homedir4(), ".codex");
|
|
4995
6037
|
}
|
|
4996
6038
|
function readRawCurrentThread(stateDir) {
|
|
4997
6039
|
try {
|
|
4998
|
-
const parsed = JSON.parse(
|
|
6040
|
+
const parsed = JSON.parse(readFileSync8(stateDir.currentThreadFile, "utf-8"));
|
|
4999
6041
|
if (parsed?.version === 1 && typeof parsed.threadId === "string" && parsed.threadId.length > 0 && (parsed.status === "pending" || parsed.status === "current") && typeof parsed.cwd === "string") {
|
|
5000
6042
|
return parsed;
|
|
5001
6043
|
}
|
|
@@ -5003,8 +6045,8 @@ function readRawCurrentThread(stateDir) {
|
|
|
5003
6045
|
return null;
|
|
5004
6046
|
}
|
|
5005
6047
|
function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
|
|
5006
|
-
const sessionsDir =
|
|
5007
|
-
if (!threadId || !
|
|
6048
|
+
const sessionsDir = join10(codexHome(env), "sessions");
|
|
6049
|
+
if (!threadId || !existsSync7(sessionsDir))
|
|
5008
6050
|
return null;
|
|
5009
6051
|
const exactName = `rollout-${threadId}.jsonl`;
|
|
5010
6052
|
const stack = [sessionsDir];
|
|
@@ -5013,13 +6055,13 @@ function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
|
|
|
5013
6055
|
const dir = stack.pop();
|
|
5014
6056
|
let entries;
|
|
5015
6057
|
try {
|
|
5016
|
-
entries =
|
|
6058
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
5017
6059
|
} catch {
|
|
5018
6060
|
continue;
|
|
5019
6061
|
}
|
|
5020
6062
|
for (const entry of entries) {
|
|
5021
6063
|
visited++;
|
|
5022
|
-
const path =
|
|
6064
|
+
const path = join10(dir, entry.name);
|
|
5023
6065
|
if (entry.isDirectory()) {
|
|
5024
6066
|
stack.push(path);
|
|
5025
6067
|
continue;
|
|
@@ -5208,6 +6250,11 @@ var BOOTSTRAP_TIMEOUT_MS = parsePositiveIntEnv("AGENTBRIDGE_BOOTSTRAP_TIMEOUT_MS
|
|
|
5208
6250
|
var CODEX_BOOT_RETRIES = parsePositiveIntEnv("AGENTBRIDGE_CODEX_BOOT_RETRIES", 2);
|
|
5209
6251
|
var ALLOW_IDENTITYLESS_CLIENT = process.env.AGENTBRIDGE_COMPAT_IDENTITYLESS === "1";
|
|
5210
6252
|
var BUDGET_CONFIG = applyBudgetEnvOverrides(config.budget);
|
|
6253
|
+
var RESUME_INJECT_RETRY_MS = parsePositiveIntEnv("AGENTBRIDGE_RESUME_INJECT_RETRY_MS", 5000, log);
|
|
6254
|
+
var RESUME_CONFIRM_TIMEOUT_MS = parsePositiveIntEnv("AGENTBRIDGE_RESUME_CONFIRM_TIMEOUT_MS", 60000, log);
|
|
6255
|
+
var RESUME_INJECT_MAX_ATTEMPTS = parsePositiveIntEnv("AGENTBRIDGE_RESUME_INJECT_MAX_ATTEMPTS", 5, log);
|
|
6256
|
+
var RESUME_ACK_TIMEOUT_MS = parsePositiveIntEnv("AGENTBRIDGE_RESUME_ACK_TIMEOUT_MS", 60000, log);
|
|
6257
|
+
var RESUME_ACK_RETRIES = parsePositiveIntEnv("AGENTBRIDGE_RESUME_ACK_RETRIES", 3, log);
|
|
5211
6258
|
var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
|
|
5212
6259
|
var DAEMON_NONCE = randomUUID4();
|
|
5213
6260
|
var DAEMON_STARTED_AT = Date.now();
|
|
@@ -5225,6 +6272,52 @@ var inAttentionWindow = false;
|
|
|
5225
6272
|
var replyTracker = new ReplyRequiredTracker;
|
|
5226
6273
|
var idempotencyTracker = new IdempotencyTracker;
|
|
5227
6274
|
var pendingTurnStarts = new Map;
|
|
6275
|
+
var pendingResumeTurnStarts = new Map;
|
|
6276
|
+
var resumeInjectionQueue = new ResumeInjectionQueue({
|
|
6277
|
+
inject: (prompt) => codex.injectMessage(prompt),
|
|
6278
|
+
retryMs: RESUME_INJECT_RETRY_MS,
|
|
6279
|
+
confirmTimeoutMs: RESUME_CONFIRM_TIMEOUT_MS,
|
|
6280
|
+
maxAttempts: RESUME_INJECT_MAX_ATTEMPTS,
|
|
6281
|
+
log,
|
|
6282
|
+
onInjectionAccepted: ({ resumeId, requestId }) => {
|
|
6283
|
+
pendingResumeTurnStarts.set(requestId, { resumeId });
|
|
6284
|
+
log(`Budget resume injection accepted: ${resumeId} \u2192 request ${requestId}`);
|
|
6285
|
+
},
|
|
6286
|
+
onInjectionSuperseded: ({ resumeId, requestId, reason }) => {
|
|
6287
|
+
pendingResumeTurnStarts.delete(requestId);
|
|
6288
|
+
log(`Budget resume injection superseded: ${resumeId} request ${requestId} (${reason})`);
|
|
6289
|
+
},
|
|
6290
|
+
onConfirmed: ({ resumeId, requestId, turnId }) => {
|
|
6291
|
+
log(`Budget resume injection confirmed: ${resumeId} request ${requestId} \u2192 turn ${turnId}`);
|
|
6292
|
+
},
|
|
6293
|
+
onAbandoned: ({ resumeId, reason }) => {
|
|
6294
|
+
log(`Budget resume injection abandoned: ${resumeId}: ${reason}`);
|
|
6295
|
+
}
|
|
6296
|
+
});
|
|
6297
|
+
var claudeResumeTracker = new ResumeAckTracker({
|
|
6298
|
+
push: ({ resumeId, deliveryId, attempt }) => {
|
|
6299
|
+
const message = {
|
|
6300
|
+
id: `system_budget_resume_${SYSTEM_MSG_SALT}_${deliveryId}`,
|
|
6301
|
+
source: "codex",
|
|
6302
|
+
content: claudeResumePrompt(resumeId),
|
|
6303
|
+
timestamp: Date.now(),
|
|
6304
|
+
resumeId
|
|
6305
|
+
};
|
|
6306
|
+
log(`Budget resume push to Claude: ${resumeId} (attempt ${attempt}, delivery ${deliveryId})`);
|
|
6307
|
+
emitToClaude(message);
|
|
6308
|
+
},
|
|
6309
|
+
scheduler: globalThis,
|
|
6310
|
+
timeoutMs: RESUME_ACK_TIMEOUT_MS,
|
|
6311
|
+
retries: RESUME_ACK_RETRIES,
|
|
6312
|
+
onDegraded: (resumeId) => {
|
|
6313
|
+
log(`Budget resume ${resumeId} degraded: no ack from Claude after ${RESUME_ACK_RETRIES} attempts`);
|
|
6314
|
+
try {
|
|
6315
|
+
writeResumeAckDegradedSentinel({ stateDir: stateDir.dir, resumeId, log });
|
|
6316
|
+
} catch (err) {
|
|
6317
|
+
log(`Resume degraded sentinel write failed (${resumeId}): ${err?.message ?? err}`);
|
|
6318
|
+
}
|
|
6319
|
+
}
|
|
6320
|
+
});
|
|
5228
6321
|
var pendingSteerDispatches = new Map;
|
|
5229
6322
|
var BUSY_RETRY_ADVISORY_MS = 15000;
|
|
5230
6323
|
var shuttingDown = false;
|
|
@@ -5250,11 +6343,105 @@ function createPendingBackpressureBuffer() {
|
|
|
5250
6343
|
});
|
|
5251
6344
|
}
|
|
5252
6345
|
var budgetCoordinator = null;
|
|
6346
|
+
function pairCwd() {
|
|
6347
|
+
const raw = process.cwd();
|
|
6348
|
+
try {
|
|
6349
|
+
return realpathSync2(raw);
|
|
6350
|
+
} catch {
|
|
6351
|
+
return raw;
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
function budgetGuardStateDir() {
|
|
6355
|
+
const override = process.env.BUDGET_STATE_DIR;
|
|
6356
|
+
if (override && override.trim() !== "")
|
|
6357
|
+
return override.trim();
|
|
6358
|
+
return join11(homedir5(), ".budget-guard");
|
|
6359
|
+
}
|
|
6360
|
+
function resumeClaimTtlSec() {
|
|
6361
|
+
const totalMs = RESUME_CONFIRM_TIMEOUT_MS * RESUME_INJECT_MAX_ATTEMPTS + RESUME_INJECT_RETRY_MS * Math.max(0, RESUME_INJECT_MAX_ATTEMPTS - 1);
|
|
6362
|
+
return Math.max(1, Math.ceil(totalMs / 1000));
|
|
6363
|
+
}
|
|
6364
|
+
function readResumeSignals() {
|
|
6365
|
+
let tuiReadyCodex = false;
|
|
6366
|
+
let tuiReadyClaude = false;
|
|
6367
|
+
try {
|
|
6368
|
+
tuiReadyCodex = tuiConnectionState.canReply();
|
|
6369
|
+
} catch (error) {
|
|
6370
|
+
log(`resume signal: codex tuiReady failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
6371
|
+
}
|
|
6372
|
+
try {
|
|
6373
|
+
tuiReadyClaude = attachedClaude !== null;
|
|
6374
|
+
} catch (error) {
|
|
6375
|
+
log(`resume signal: claude tuiReady failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
6376
|
+
}
|
|
6377
|
+
let pendingCodex = false;
|
|
6378
|
+
let pendingClaude = false;
|
|
6379
|
+
let pendingCodexEntry;
|
|
6380
|
+
let pendingClaudeEntry;
|
|
6381
|
+
try {
|
|
6382
|
+
const home = homedir5();
|
|
6383
|
+
const cwd = pairCwd();
|
|
6384
|
+
pendingCodexEntry = readGuardPending({ homeDir: home, agent: "codex", cwd, log })[0];
|
|
6385
|
+
pendingClaudeEntry = readGuardPending({ homeDir: home, agent: "claude", cwd, log })[0];
|
|
6386
|
+
pendingCodex = pendingCodexEntry !== undefined;
|
|
6387
|
+
pendingClaude = pendingClaudeEntry !== undefined;
|
|
6388
|
+
} catch (error) {
|
|
6389
|
+
log(`resume signal: pending read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
6390
|
+
}
|
|
6391
|
+
let checkpointExists = false;
|
|
6392
|
+
let checkpointPath;
|
|
6393
|
+
try {
|
|
6394
|
+
checkpointPath = join11(pairCwd(), ".agent", "checkpoint.md");
|
|
6395
|
+
checkpointExists = existsSync8(checkpointPath);
|
|
6396
|
+
} catch (error) {
|
|
6397
|
+
log(`resume signal: checkpoint stat failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
6398
|
+
checkpointPath = undefined;
|
|
6399
|
+
}
|
|
6400
|
+
return {
|
|
6401
|
+
tuiReady: { codex: tuiReadyCodex, claude: tuiReadyClaude },
|
|
6402
|
+
pendingExists: { codex: pendingCodex, claude: pendingClaude },
|
|
6403
|
+
pending: {
|
|
6404
|
+
...pendingCodexEntry ? { codex: pendingCodexEntry } : {},
|
|
6405
|
+
...pendingClaudeEntry ? { claude: pendingClaudeEntry } : {}
|
|
6406
|
+
},
|
|
6407
|
+
checkpointExists,
|
|
6408
|
+
...checkpointPath ? { checkpointPath } : {}
|
|
6409
|
+
};
|
|
6410
|
+
}
|
|
6411
|
+
function enqueueCodexBudgetResume(resumeId) {
|
|
6412
|
+
const candidate = budgetCoordinator?.getResumeCandidate();
|
|
6413
|
+
const detail = candidate?.detail?.codex;
|
|
6414
|
+
if (candidate?.codex !== true || detail?.ready !== true) {
|
|
6415
|
+
log(`Budget resume ${resumeId} ignored: Codex resume candidate is not ready`);
|
|
6416
|
+
return;
|
|
6417
|
+
}
|
|
6418
|
+
if (!detail.pending) {
|
|
6419
|
+
log(`Budget resume ${resumeId} ignored: missing Codex guard pending entry`);
|
|
6420
|
+
return;
|
|
6421
|
+
}
|
|
6422
|
+
if (!detail.checkpointPath) {
|
|
6423
|
+
log(`Budget resume ${resumeId} ignored: missing checkpoint path`);
|
|
6424
|
+
return;
|
|
6425
|
+
}
|
|
6426
|
+
const claim = tryClaimPendingResume({
|
|
6427
|
+
stateDir: budgetGuardStateDir(),
|
|
6428
|
+
agent: "codex",
|
|
6429
|
+
pending: detail.pending,
|
|
6430
|
+
checkpointPath: detail.checkpointPath,
|
|
6431
|
+
claimTtlSec: resumeClaimTtlSec(),
|
|
6432
|
+
log
|
|
6433
|
+
});
|
|
6434
|
+
if (!claim.ok) {
|
|
6435
|
+
log(`Budget resume ${resumeId} not enqueued: pending claim ${claim.reason}${claim.error ? ` (${claim.error})` : ""}`);
|
|
6436
|
+
return;
|
|
6437
|
+
}
|
|
6438
|
+
resumeInjectionQueue.enqueue({ resumeId, prompt: RESUME_PROMPT, claim: claim.claim });
|
|
6439
|
+
}
|
|
5253
6440
|
function ensureBudgetCoordinatorStarted() {
|
|
5254
6441
|
if (!BUDGET_CONFIG.enabled)
|
|
5255
6442
|
return;
|
|
5256
6443
|
if (!budgetCoordinator) {
|
|
5257
|
-
log(`Budget coordinator config: pollSeconds=${BUDGET_CONFIG.pollSeconds} pauseAt=${BUDGET_CONFIG.pauseAt} ` + `resumeBelow=${BUDGET_CONFIG.resumeBelow} syncDriftPct=${BUDGET_CONFIG.syncDriftPct} ` + `parallel=${BUDGET_CONFIG.parallel.minRemainingPct}%/${BUDGET_CONFIG.parallel.timeWindowSec}s ` + `codexTierControl=${BUDGET_CONFIG.codexTierControl} ` + `codexTiersFull=${BUDGET_CONFIG.codexTiers.full ? "configured" : "missing"} ` + `
|
|
6444
|
+
log(`Budget coordinator config: pollSeconds=${BUDGET_CONFIG.pollSeconds} pauseAt=${BUDGET_CONFIG.pauseAt} ` + `resumeBelow=${BUDGET_CONFIG.resumeBelow} syncDriftPct=${BUDGET_CONFIG.syncDriftPct} ` + `parallel=${BUDGET_CONFIG.parallel.minRemainingPct}%/${BUDGET_CONFIG.parallel.timeWindowSec}s ` + `codexTierControl=${BUDGET_CONFIG.codexTierControl} ` + `codexTiersFull=${BUDGET_CONFIG.codexTiers.full ? "configured" : "missing"} ` + `targetUtil=${BUDGET_CONFIG.maximize.targetUtil} fallback=${BUDGET_CONFIG.pauseAt}/${BUDGET_CONFIG.resumeBelow}`);
|
|
5258
6445
|
budgetCoordinator = new BudgetCoordinator({
|
|
5259
6446
|
source: createQuotaSource({ log }),
|
|
5260
6447
|
config: BUDGET_CONFIG,
|
|
@@ -5265,7 +6452,17 @@ function ensureBudgetCoordinatorStarted() {
|
|
|
5265
6452
|
log(`Budget intervention ${paused ? "ACTIVE" : "CLEARED"} ` + `(gate ${budgetCoordinator?.isGateClosed() ? "CLOSED" : "OPEN"})`);
|
|
5266
6453
|
},
|
|
5267
6454
|
onSnapshot: () => broadcastStatus(),
|
|
5268
|
-
log
|
|
6455
|
+
log,
|
|
6456
|
+
onResume: (side, _directive, resumeId) => {
|
|
6457
|
+
if (side === "claude") {
|
|
6458
|
+
log(`Budget resume ${resumeId} for Claude side \u2192 arming ack tracker`);
|
|
6459
|
+
}
|
|
6460
|
+
routeResume(side, resumeId, {
|
|
6461
|
+
claudeTracker: claudeResumeTracker,
|
|
6462
|
+
enqueueCodex: enqueueCodexBudgetResume
|
|
6463
|
+
});
|
|
6464
|
+
},
|
|
6465
|
+
resumeSignals: readResumeSignals
|
|
5269
6466
|
});
|
|
5270
6467
|
}
|
|
5271
6468
|
budgetCoordinator.start();
|
|
@@ -5278,7 +6475,8 @@ function budgetPauseGateError() {
|
|
|
5278
6475
|
const reason = snapshot?.pauseReason ?? "Codex \u4FA7\u989D\u5EA6\u63A5\u8FD1\u8017\u5C3D";
|
|
5279
6476
|
const resumeAt = snapshot?.resumeAfterEpoch ? new Date(snapshot.resumeAfterEpoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z") : null;
|
|
5280
6477
|
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";
|
|
5281
|
-
|
|
6478
|
+
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`;
|
|
6479
|
+
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`;
|
|
5282
6480
|
}
|
|
5283
6481
|
var tuiConnectionState = new TuiConnectionState({
|
|
5284
6482
|
disconnectGraceMs: TUI_DISCONNECT_GRACE_MS,
|
|
@@ -5324,6 +6522,12 @@ codex.on("steerAccepted", ({ requestId }) => {
|
|
|
5324
6522
|
}
|
|
5325
6523
|
});
|
|
5326
6524
|
codex.on("bridgeTurnStarted", ({ requestId, turnId }) => {
|
|
6525
|
+
const pendingResume = pendingResumeTurnStarts.get(requestId);
|
|
6526
|
+
if (pendingResume) {
|
|
6527
|
+
pendingResumeTurnStarts.delete(requestId);
|
|
6528
|
+
resumeInjectionQueue.onBridgeTurnStarted({ resumeId: pendingResume.resumeId, requestId, turnId });
|
|
6529
|
+
return;
|
|
6530
|
+
}
|
|
5327
6531
|
const pending = pendingTurnStarts.get(requestId);
|
|
5328
6532
|
if (!pending) {
|
|
5329
6533
|
log(`bridgeTurnStarted for unknown injection ${requestId} (turn ${turnId}) \u2014 correlation dropped`);
|
|
@@ -5345,6 +6549,12 @@ codex.on("bridgeTurnStarted", ({ requestId, turnId }) => {
|
|
|
5345
6549
|
}
|
|
5346
6550
|
});
|
|
5347
6551
|
codex.on("bridgeTurnRejected", ({ requestId, error }) => {
|
|
6552
|
+
const pendingResume = pendingResumeTurnStarts.get(requestId);
|
|
6553
|
+
if (pendingResume) {
|
|
6554
|
+
pendingResumeTurnStarts.delete(requestId);
|
|
6555
|
+
resumeInjectionQueue.onBridgeTurnRejected({ resumeId: pendingResume.resumeId, requestId, error });
|
|
6556
|
+
return;
|
|
6557
|
+
}
|
|
5348
6558
|
const pending = pendingTurnStarts.get(requestId);
|
|
5349
6559
|
if (!pending)
|
|
5350
6560
|
return;
|
|
@@ -5362,11 +6572,16 @@ codex.on("turnTrackingReset", (reason) => {
|
|
|
5362
6572
|
if (pendingTurnStarts.size > 0) {
|
|
5363
6573
|
log(`Cleared ${pendingTurnStarts.size} pending turn-start correlation(s) on turn tracking reset (${reason})`);
|
|
5364
6574
|
}
|
|
6575
|
+
if (pendingResumeTurnStarts.size > 0) {
|
|
6576
|
+
log(`Cleared ${pendingResumeTurnStarts.size} pending resume turn-start correlation(s) on turn tracking reset (${reason})`);
|
|
6577
|
+
}
|
|
5365
6578
|
if (pendingSteerDispatches.size > 0) {
|
|
5366
6579
|
log(`Cleared ${pendingSteerDispatches.size} pending steer dispatch(es) on turn tracking reset (${reason})`);
|
|
5367
6580
|
}
|
|
5368
6581
|
pendingTurnStarts.clear();
|
|
6582
|
+
pendingResumeTurnStarts.clear();
|
|
5369
6583
|
pendingSteerDispatches.clear();
|
|
6584
|
+
resumeInjectionQueue.onTurnTrackingReset();
|
|
5370
6585
|
});
|
|
5371
6586
|
codex.on("turnStarted", () => {
|
|
5372
6587
|
log("Codex turn started");
|
|
@@ -5411,6 +6626,7 @@ codex.on("turnCompleted", () => {
|
|
|
5411
6626
|
}
|
|
5412
6627
|
emitToClaude(systemMessage("system_turn_completed", "\u2705 Codex finished the current turn. You can reply now if needed."));
|
|
5413
6628
|
startAttentionWindow();
|
|
6629
|
+
resumeInjectionQueue.onTurnDrained();
|
|
5414
6630
|
});
|
|
5415
6631
|
codex.on("turnAborted", (reason) => {
|
|
5416
6632
|
log(`Codex turn aborted (${reason}) \u2014 clearing reply-required state`);
|
|
@@ -5470,7 +6686,9 @@ codex.on("exit", (code) => {
|
|
|
5470
6686
|
replyTracker.reset();
|
|
5471
6687
|
idempotencyTracker.terminateAll("aborted");
|
|
5472
6688
|
pendingTurnStarts.clear();
|
|
6689
|
+
pendingResumeTurnStarts.clear();
|
|
5473
6690
|
pendingSteerDispatches.clear();
|
|
6691
|
+
resumeInjectionQueue.onTurnTrackingReset();
|
|
5474
6692
|
statusBuffer.flush("codex exited");
|
|
5475
6693
|
tuiConnectionState.handleCodexExit();
|
|
5476
6694
|
clearPendingClaudeDisconnect("Codex process exited");
|
|
@@ -5580,6 +6798,10 @@ function handleControlMessage(ws, raw) {
|
|
|
5580
6798
|
case "status":
|
|
5581
6799
|
sendStatus(ws);
|
|
5582
6800
|
return;
|
|
6801
|
+
case "ack_resume":
|
|
6802
|
+
log(`Received ack_resume from Claude #${ws.data.clientId}: ${message.resumeId} (${message.status})`);
|
|
6803
|
+
claudeResumeTracker.ack(message.resumeId);
|
|
6804
|
+
return;
|
|
5583
6805
|
case "probe_incumbent":
|
|
5584
6806
|
handleProbeIncumbent(ws).catch((err) => {
|
|
5585
6807
|
log(`handleProbeIncumbent threw for #${ws.data.clientId}: ${err?.message ?? err}`);
|
|
@@ -5685,7 +6907,7 @@ async function handleClaudeToCodex(ws, message) {
|
|
|
5685
6907
|
const reason = budgetPauseGateError();
|
|
5686
6908
|
log(`Injection rejected by budget pause gate`);
|
|
5687
6909
|
const resumeAfterEpoch3 = budgetCoordinator?.getSnapshot()?.resumeAfterEpoch ?? null;
|
|
5688
|
-
const retryAfterMs = resumeAfterEpoch3
|
|
6910
|
+
const retryAfterMs = retryAfterMsForResume(resumeAfterEpoch3, Date.now());
|
|
5689
6911
|
sendClaudeToCodexResult(ws, message.requestId, {
|
|
5690
6912
|
success: false,
|
|
5691
6913
|
code: "budget_paused",
|
|
@@ -6233,6 +7455,8 @@ function shutdown(reason, exitCode = 0) {
|
|
|
6233
7455
|
shuttingDown = true;
|
|
6234
7456
|
log(`Shutting down daemon (${reason})...`);
|
|
6235
7457
|
clearBootDeadline();
|
|
7458
|
+
resumeInjectionQueue.stop();
|
|
7459
|
+
claudeResumeTracker.stop();
|
|
6236
7460
|
stopBudgetCoordinator();
|
|
6237
7461
|
idempotencyTracker.dispose();
|
|
6238
7462
|
tuiConnectionState.dispose(`daemon shutdown (${reason})`);
|