@raysonmeng/agentbridge 0.1.18 → 0.1.19
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/dist/cli.js +24 -12
- package/dist/daemon.js +439 -28
- package/package.json +1 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +30 -14
- package/plugins/agentbridge/server/daemon.js +439 -28
package/dist/daemon.js
CHANGED
|
@@ -29,11 +29,11 @@ function defineNumber(value, fallback) {
|
|
|
29
29
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
30
30
|
}
|
|
31
31
|
var BUILD_INFO = Object.freeze({
|
|
32
|
-
version: defineString("0.1.
|
|
33
|
-
commit: defineString("
|
|
32
|
+
version: defineString("0.1.19", "0.0.0-source"),
|
|
33
|
+
commit: defineString("dee06ce", "source"),
|
|
34
34
|
bundle: defineBundle("dist"),
|
|
35
35
|
contractVersion: defineNumber(1, CONTRACT_VERSION),
|
|
36
|
-
codeHash: defineString("
|
|
36
|
+
codeHash: defineString("9ddee00c61f9", "source")
|
|
37
37
|
});
|
|
38
38
|
function daemonStatusBuildInfo() {
|
|
39
39
|
return { ...BUILD_INFO };
|
|
@@ -290,6 +290,9 @@ class StateDirResolver {
|
|
|
290
290
|
get killedFile() {
|
|
291
291
|
return join(this.stateDir, "killed");
|
|
292
292
|
}
|
|
293
|
+
get admissionQuotaFile() {
|
|
294
|
+
return join(this.stateDir, "admission-quota.json");
|
|
295
|
+
}
|
|
293
296
|
get updateCheckFile() {
|
|
294
297
|
return join(this.stateDir, "update-check.json");
|
|
295
298
|
}
|
|
@@ -1014,6 +1017,9 @@ class CodexAdapter extends EventEmitter {
|
|
|
1014
1017
|
get activeThreadId() {
|
|
1015
1018
|
return this.threadId;
|
|
1016
1019
|
}
|
|
1020
|
+
canInject() {
|
|
1021
|
+
return !!this.threadId && this.appServerWs?.readyState === WebSocket.OPEN && !this.turnInProgress;
|
|
1022
|
+
}
|
|
1017
1023
|
get capturedAppServerInfo() {
|
|
1018
1024
|
return this.appServerInfo;
|
|
1019
1025
|
}
|
|
@@ -3386,6 +3392,86 @@ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
|
3386
3392
|
}
|
|
3387
3393
|
}
|
|
3388
3394
|
|
|
3395
|
+
// src/budget/admission-quota.ts
|
|
3396
|
+
function nodeFs() {
|
|
3397
|
+
return __require("fs");
|
|
3398
|
+
}
|
|
3399
|
+
function freshState(fiveHourResetEpoch) {
|
|
3400
|
+
return { version: 1, fiveHourResetEpoch, wrapUpUsed: 0, checkpointBatonUsed: false };
|
|
3401
|
+
}
|
|
3402
|
+
function parseAdmissionQuota(value) {
|
|
3403
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
3404
|
+
return null;
|
|
3405
|
+
const record = value;
|
|
3406
|
+
if (record.version !== 1)
|
|
3407
|
+
return null;
|
|
3408
|
+
const epoch = record.fiveHourResetEpoch;
|
|
3409
|
+
const used = record.wrapUpUsed;
|
|
3410
|
+
if (typeof epoch !== "number" || !Number.isFinite(epoch))
|
|
3411
|
+
return null;
|
|
3412
|
+
if (typeof used !== "number" || !Number.isFinite(used) || used < 0)
|
|
3413
|
+
return null;
|
|
3414
|
+
return {
|
|
3415
|
+
version: 1,
|
|
3416
|
+
fiveHourResetEpoch: epoch,
|
|
3417
|
+
wrapUpUsed: Math.floor(used),
|
|
3418
|
+
checkpointBatonUsed: record.checkpointBatonUsed === true
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
function currentWindowState(path, fiveHourResetEpoch, log = () => {}) {
|
|
3422
|
+
let raw;
|
|
3423
|
+
try {
|
|
3424
|
+
raw = nodeFs().readFileSync(path, "utf-8");
|
|
3425
|
+
} catch {
|
|
3426
|
+
return freshState(fiveHourResetEpoch);
|
|
3427
|
+
}
|
|
3428
|
+
const text = String(raw).trim();
|
|
3429
|
+
if (text === "")
|
|
3430
|
+
return freshState(fiveHourResetEpoch);
|
|
3431
|
+
let parsed;
|
|
3432
|
+
try {
|
|
3433
|
+
parsed = JSON.parse(text);
|
|
3434
|
+
} catch {
|
|
3435
|
+
log(`admission-quota: skip malformed JSON ${path}`);
|
|
3436
|
+
return freshState(fiveHourResetEpoch);
|
|
3437
|
+
}
|
|
3438
|
+
const state = parseAdmissionQuota(parsed);
|
|
3439
|
+
if (!state || state.fiveHourResetEpoch !== fiveHourResetEpoch) {
|
|
3440
|
+
return freshState(fiveHourResetEpoch);
|
|
3441
|
+
}
|
|
3442
|
+
return state;
|
|
3443
|
+
}
|
|
3444
|
+
function persist(path, state, log) {
|
|
3445
|
+
try {
|
|
3446
|
+
atomicWriteJson(path, state);
|
|
3447
|
+
return true;
|
|
3448
|
+
} catch (error) {
|
|
3449
|
+
log(`admission-quota: write failed ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3450
|
+
return false;
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
function consumeWrapUp(path, fiveHourResetEpoch, limit, log = () => {}) {
|
|
3454
|
+
if (!Number.isFinite(fiveHourResetEpoch))
|
|
3455
|
+
return { allowed: false, used: 0, remaining: 0 };
|
|
3456
|
+
const state = currentWindowState(path, fiveHourResetEpoch, log);
|
|
3457
|
+
if (state.wrapUpUsed >= limit) {
|
|
3458
|
+
return { allowed: false, used: state.wrapUpUsed, remaining: Math.max(0, limit - state.wrapUpUsed) };
|
|
3459
|
+
}
|
|
3460
|
+
const next = { ...state, wrapUpUsed: state.wrapUpUsed + 1 };
|
|
3461
|
+
if (!persist(path, next, log)) {
|
|
3462
|
+
return { allowed: false, used: state.wrapUpUsed, remaining: Math.max(0, limit - state.wrapUpUsed) };
|
|
3463
|
+
}
|
|
3464
|
+
return { allowed: true, used: next.wrapUpUsed, remaining: Math.max(0, limit - next.wrapUpUsed) };
|
|
3465
|
+
}
|
|
3466
|
+
function consumeCheckpointBaton(path, fiveHourResetEpoch, log = () => {}) {
|
|
3467
|
+
if (!Number.isFinite(fiveHourResetEpoch))
|
|
3468
|
+
return false;
|
|
3469
|
+
const state = currentWindowState(path, fiveHourResetEpoch, log);
|
|
3470
|
+
if (state.checkpointBatonUsed)
|
|
3471
|
+
return false;
|
|
3472
|
+
return persist(path, { ...state, checkpointBatonUsed: true }, log);
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3389
3475
|
// src/config-service.ts
|
|
3390
3476
|
import { readFileSync as readFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync4 } from "fs";
|
|
3391
3477
|
import { join as join4 } from "path";
|
|
@@ -3410,7 +3496,9 @@ var DEFAULT_BUDGET_CONFIG = {
|
|
|
3410
3496
|
reserveSlopePctPerHour: 0.4,
|
|
3411
3497
|
reserveMaxPct: 7,
|
|
3412
3498
|
finishingHorizonMinutes: 30,
|
|
3413
|
-
resumeHysteresisPct: 5
|
|
3499
|
+
resumeHysteresisPct: 5,
|
|
3500
|
+
admissionAt: 85,
|
|
3501
|
+
wrapUpQuota: 2
|
|
3414
3502
|
},
|
|
3415
3503
|
allocation: {
|
|
3416
3504
|
minRunwayRatio: 50,
|
|
@@ -3478,7 +3566,9 @@ function findShapeViolation(raw) {
|
|
|
3478
3566
|
"reserveSlopePctPerHour",
|
|
3479
3567
|
"reserveMaxPct",
|
|
3480
3568
|
"finishingHorizonMinutes",
|
|
3481
|
-
"resumeHysteresisPct"
|
|
3569
|
+
"resumeHysteresisPct",
|
|
3570
|
+
"admissionAt",
|
|
3571
|
+
"wrapUpQuota"
|
|
3482
3572
|
]) {
|
|
3483
3573
|
if (key in maximize && !isCoercibleNumber(maximize[key])) {
|
|
3484
3574
|
return `budget.maximize.${key} is present but not a number`;
|
|
@@ -3503,7 +3593,7 @@ function hasCustomDecisionValues(config) {
|
|
|
3503
3593
|
const d = DEFAULT_CONFIG;
|
|
3504
3594
|
const b = config.budget;
|
|
3505
3595
|
const db = d.budget;
|
|
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;
|
|
3596
|
+
return config.idleShutdownSeconds !== d.idleShutdownSeconds || config.turnCoordination.attentionWindowSeconds !== d.turnCoordination.attentionWindowSeconds || config.codex.appPort !== d.codex.appPort || config.codex.proxyPort !== d.codex.proxyPort || b.enabled !== db.enabled || b.pollSeconds !== db.pollSeconds || b.pauseAt !== db.pauseAt || b.resumeBelow !== db.resumeBelow || b.syncDriftPct !== db.syncDriftPct || b.parallel.minRemainingPct !== db.parallel.minRemainingPct || b.parallel.timeWindowSec !== db.parallel.timeWindowSec || b.codexTierControl !== db.codexTierControl || b.maximize.targetUtil !== db.maximize.targetUtil || b.maximize.reserveSlopePctPerHour !== db.maximize.reserveSlopePctPerHour || b.maximize.reserveMaxPct !== db.maximize.reserveMaxPct || b.maximize.finishingHorizonMinutes !== db.maximize.finishingHorizonMinutes || b.maximize.resumeHysteresisPct !== db.maximize.resumeHysteresisPct || b.maximize.admissionAt !== db.maximize.admissionAt || b.maximize.wrapUpQuota !== db.maximize.wrapUpQuota || b.allocation.minRunwayRatio !== db.allocation.minRunwayRatio || b.allocation.minRunwayGapHours !== db.allocation.minRunwayGapHours;
|
|
3507
3597
|
}
|
|
3508
3598
|
function normalizeInteger(value, fallback) {
|
|
3509
3599
|
if (typeof value === "number" && Number.isFinite(value))
|
|
@@ -3543,9 +3633,11 @@ function normalizeMaximizeConfig(raw, pauseAt, fallback = DEFAULT_BUDGET_CONFIG.
|
|
|
3543
3633
|
reserveSlopePctPerHour: normalizeBoundedNumber(m.reserveSlopePctPerHour, fallback.reserveSlopePctPerHour, 0, 5),
|
|
3544
3634
|
reserveMaxPct: normalizeBoundedInteger(m.reserveMaxPct, fallback.reserveMaxPct, 0, 30),
|
|
3545
3635
|
finishingHorizonMinutes: normalizeBoundedInteger(m.finishingHorizonMinutes, fallback.finishingHorizonMinutes, 5, 180),
|
|
3546
|
-
resumeHysteresisPct: normalizeBoundedInteger(m.resumeHysteresisPct, fallback.resumeHysteresisPct, 1, 30)
|
|
3636
|
+
resumeHysteresisPct: normalizeBoundedInteger(m.resumeHysteresisPct, fallback.resumeHysteresisPct, 1, 30),
|
|
3637
|
+
admissionAt: normalizeBoundedInteger(m.admissionAt, fallback.admissionAt, 50, 99),
|
|
3638
|
+
wrapUpQuota: Math.floor(normalizeBoundedInteger(m.wrapUpQuota, fallback.wrapUpQuota, 0, 10))
|
|
3547
3639
|
};
|
|
3548
|
-
if (normalized.targetUtil <= pauseAt) {
|
|
3640
|
+
if (normalized.targetUtil <= pauseAt || normalized.admissionAt >= normalized.targetUtil) {
|
|
3549
3641
|
return { ...DEFAULT_BUDGET_CONFIG.maximize };
|
|
3550
3642
|
}
|
|
3551
3643
|
return normalized;
|
|
@@ -3628,7 +3720,9 @@ function applyBudgetEnvOverrides(budget, env = process.env) {
|
|
|
3628
3720
|
reserveSlopePctPerHour: env.AGENTBRIDGE_BUDGET_RESERVE_SLOPE_PCT_PER_HOUR ?? budget.maximize.reserveSlopePctPerHour,
|
|
3629
3721
|
reserveMaxPct: env.AGENTBRIDGE_BUDGET_RESERVE_MAX_PCT ?? budget.maximize.reserveMaxPct,
|
|
3630
3722
|
finishingHorizonMinutes: env.AGENTBRIDGE_BUDGET_FINISHING_HORIZON_MINUTES ?? budget.maximize.finishingHorizonMinutes,
|
|
3631
|
-
resumeHysteresisPct: env.AGENTBRIDGE_BUDGET_RESUME_HYSTERESIS_PCT ?? budget.maximize.resumeHysteresisPct
|
|
3723
|
+
resumeHysteresisPct: env.AGENTBRIDGE_BUDGET_RESUME_HYSTERESIS_PCT ?? budget.maximize.resumeHysteresisPct,
|
|
3724
|
+
admissionAt: env.AGENTBRIDGE_BUDGET_ADMISSION_AT ?? budget.maximize.admissionAt,
|
|
3725
|
+
wrapUpQuota: env.AGENTBRIDGE_BUDGET_WRAP_UP_QUOTA ?? budget.maximize.wrapUpQuota
|
|
3632
3726
|
},
|
|
3633
3727
|
allocation: {
|
|
3634
3728
|
minRunwayRatio: env.AGENTBRIDGE_BUDGET_MIN_RUNWAY_RATIO ?? budget.allocation.minRunwayRatio,
|
|
@@ -3967,6 +4061,79 @@ function resumeBlockingEpochFor(usage, cfg, now) {
|
|
|
3967
4061
|
return 0;
|
|
3968
4062
|
return Math.min(...blockingResets);
|
|
3969
4063
|
}
|
|
4064
|
+
var ADMISSION_WEEKLY_RUNWAY_ENTER_MULT = 2;
|
|
4065
|
+
var ADMISSION_WEEKLY_RUNWAY_EXIT_MULT = 3;
|
|
4066
|
+
function weeklyRunwayFloorSec(cfg, mult) {
|
|
4067
|
+
return cfg.maximize.finishingHorizonMinutes * 60 * mult;
|
|
4068
|
+
}
|
|
4069
|
+
function weeklyRunwayShort(usage, cfg, now, floorSec) {
|
|
4070
|
+
const weekly = usage.weekly;
|
|
4071
|
+
if (!weekly || weekly.resetEpoch <= now)
|
|
4072
|
+
return false;
|
|
4073
|
+
if (dynamicWindowVerdict(weekly, cfg, now).kind !== "will-fill")
|
|
4074
|
+
return false;
|
|
4075
|
+
const runway = weekly.runwaySeconds;
|
|
4076
|
+
if (typeof runway !== "number" || !Number.isFinite(runway))
|
|
4077
|
+
return false;
|
|
4078
|
+
return runway < floorSec;
|
|
4079
|
+
}
|
|
4080
|
+
function hardCapWindow(usage, cfg, now) {
|
|
4081
|
+
for (const { key, window } of freshWindows(usage, now)) {
|
|
4082
|
+
const rate = confidentRate(window);
|
|
4083
|
+
if (rate === null)
|
|
4084
|
+
continue;
|
|
4085
|
+
if (dynamicPauseAt(window, rate, cfg, now) === "admission-closed")
|
|
4086
|
+
return key;
|
|
4087
|
+
}
|
|
4088
|
+
return null;
|
|
4089
|
+
}
|
|
4090
|
+
var NO_ADMIT_CLOSE = { admitClose: false, window: null, reason: "" };
|
|
4091
|
+
function agentShouldAdmitClose(agent, usage, cfg, now) {
|
|
4092
|
+
if (!usage)
|
|
4093
|
+
return NO_ADMIT_CLOSE;
|
|
4094
|
+
if (!isDecisionGrade(usage, now))
|
|
4095
|
+
return NO_ADMIT_CLOSE;
|
|
4096
|
+
const fiveHour = usage.fiveHour;
|
|
4097
|
+
if (fiveHour && fiveHour.resetEpoch > now && fiveHour.util >= cfg.maximize.admissionAt) {
|
|
4098
|
+
return {
|
|
4099
|
+
admitClose: true,
|
|
4100
|
+
window: "fiveHour",
|
|
4101
|
+
reason: `${AGENT_LABEL[agent]} 5h\u7A97\u53E3 util ${pct(fiveHour.util)} \u2265 admissionAt ${pct(cfg.maximize.admissionAt)}\uFF08\u6536\u5C3E\u4FDD\u62A4\uFF1A\u62D2\u65B0\u4EFB\u52A1\u3001\u653E\u6536\u5C3E\uFF09`
|
|
4102
|
+
};
|
|
4103
|
+
}
|
|
4104
|
+
const hard = hardCapWindow(usage, cfg, now);
|
|
4105
|
+
if (hard !== null) {
|
|
4106
|
+
return {
|
|
4107
|
+
admitClose: true,
|
|
4108
|
+
window: hard,
|
|
4109
|
+
reason: `${AGENT_LABEL[agent]} ${WINDOW_LABEL[hard]}\u7A97\u53E3\u89E6\u53D1\u6536\u5C3E\u4FDD\u62A4\u786C\u7EBF\uFF08util \u5DF2\u8FBE targetUtil \u6216\u4E34\u8FD1\u91CD\u7F6E\u6536\u5C3E\u5E26\uFF09`
|
|
4110
|
+
};
|
|
4111
|
+
}
|
|
4112
|
+
if (weeklyRunwayShort(usage, cfg, now, weeklyRunwayFloorSec(cfg, ADMISSION_WEEKLY_RUNWAY_ENTER_MULT))) {
|
|
4113
|
+
return {
|
|
4114
|
+
admitClose: true,
|
|
4115
|
+
window: "weekly",
|
|
4116
|
+
reason: `${AGENT_LABEL[agent]} \u5468\u7A97\u53E3 runway \u4F4E\u4E8E ${ADMISSION_WEEKLY_RUNWAY_ENTER_MULT}\xD7\u6536\u5C3E\u89C6\u91CE\uFF08\u9632\u65B0\u957F\u4EFB\u52A1\u649E\u7A7F\u5468\u989D\u5EA6\uFF09`
|
|
4117
|
+
};
|
|
4118
|
+
}
|
|
4119
|
+
return NO_ADMIT_CLOSE;
|
|
4120
|
+
}
|
|
4121
|
+
function agentCanAdmitOpen(usage, cfg, now) {
|
|
4122
|
+
if (!isDecisionGrade(usage, now))
|
|
4123
|
+
return false;
|
|
4124
|
+
if (usage.rateLimitedUntil > now)
|
|
4125
|
+
return false;
|
|
4126
|
+
const fiveHour = usage.fiveHour;
|
|
4127
|
+
if (fiveHour && fiveHour.resetEpoch > now) {
|
|
4128
|
+
if (fiveHour.util >= cfg.maximize.admissionAt - cfg.maximize.resumeHysteresisPct)
|
|
4129
|
+
return false;
|
|
4130
|
+
}
|
|
4131
|
+
if (hardCapWindow(usage, cfg, now) !== null)
|
|
4132
|
+
return false;
|
|
4133
|
+
if (weeklyRunwayShort(usage, cfg, now, weeklyRunwayFloorSec(cfg, ADMISSION_WEEKLY_RUNWAY_EXIT_MULT)))
|
|
4134
|
+
return false;
|
|
4135
|
+
return true;
|
|
4136
|
+
}
|
|
3970
4137
|
|
|
3971
4138
|
// src/budget/budget-state.ts
|
|
3972
4139
|
var NO_RUNWAY = { claude: null, codex: null };
|
|
@@ -4112,6 +4279,18 @@ function renderBudgetInterventionDirective(claude, codex, side, reason, resumeEp
|
|
|
4112
4279
|
].join(`
|
|
4113
4280
|
`);
|
|
4114
4281
|
}
|
|
4282
|
+
function renderBudgetAdmissionDirective(claude, codex, side, reason, resetEpoch, cfg) {
|
|
4283
|
+
const resetText = `\u5BF9\u5E94\u7A97\u53E3\u7EA6 ${formatEpoch(resetEpoch)} \u5237\u65B0\uFF08\u4EE5\u5B9E\u6D4B\u4E3A\u51C6\uFF1B\u63D0\u524D\u5237\u65B0\u4F1A\u66F4\u65E9\u89E3\u9664\uFF09`;
|
|
4284
|
+
const head = side === "both" ? "\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011\u53CC\u65B9\u8FDB\u5165\u6536\u5C3E\u4FDD\u62A4\uFF08admission-closed\uFF09\u3002" : "\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011Codex \u4FA7\u8FDB\u5165\u6536\u5C3E\u4FDD\u62A4\uFF08admission-closed\uFF09\u3002";
|
|
4285
|
+
return [
|
|
4286
|
+
head,
|
|
4287
|
+
`\u89E6\u53D1\u539F\u56E0\uFF1A${reason}\u3002`,
|
|
4288
|
+
`${usageSummary("claude", claude)}\uFF1B${usageSummary("codex", codex)}\u3002`,
|
|
4289
|
+
`\u95F8\u95E8\u5DF2\u6536\u7D27\uFF1A\u65B0\u7684 Codex \u4EFB\u52A1\u4F1A\u88AB\u62D2\uFF08budget_admission\uFF09\uFF0C\u4F46\u4ECD\u53EF\u7528 reply \u5E26 wrap_up=true \u628A\u5F53\u524D\u534F\u4F5C\u6536\u5C3E\u5230 checkpoint` + `\uFF08\u6BCF\u7A97\u53E3\u81F3\u591A ${cfg.maximize.wrapUpQuota} \u4E2A\uFF09\uFF0Csteer \u4FEE\u6B63\u4E0D\u53D7\u9650\uFF1B${resetText}\u3002`,
|
|
4290
|
+
"\u5EFA\u8BAE\uFF1A\u4E0D\u8981\u518D\u5411 Codex \u6D3E\u65B0\u4EFB\u52A1\uFF1B\u628A\u5F53\u524D Codex \u534F\u4F5C\u6536\u5C3E\u3001\u5199 checkpoint\uFF0C\u53EF\u72EC\u7ACB\u63A8\u8FDB\u7684\u90E8\u5206 Claude \u53EF solo \u7EE7\u7EED\u3002"
|
|
4291
|
+
].join(`
|
|
4292
|
+
`);
|
|
4293
|
+
}
|
|
4115
4294
|
function balanceDirective(claude, codex, drift, basis, runway) {
|
|
4116
4295
|
const heavier = drift.heavier ? AGENT_LABEL2[drift.heavier] : "\u672A\u77E5";
|
|
4117
4296
|
const lighter = drift.lighter ? AGENT_LABEL2[drift.lighter] : "\u672A\u77E5";
|
|
@@ -4154,7 +4333,7 @@ function computeBudgetState(claude, codex, cfg, now, runway = NO_RUNWAY) {
|
|
|
4154
4333
|
const paused = triggers.length > 0;
|
|
4155
4334
|
const { drift, basis } = allocationDrift(claude, codex, runway, cfg);
|
|
4156
4335
|
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);
|
|
4336
|
+
const adviceEligible = !paused && claude !== null && codex !== null && claude.rateLimitedUntil <= now && codex.rateLimitedUntil <= now && isDecisionGrade(claude, now) && isDecisionGrade(codex, now) && !agentShouldAdmitClose("claude", claude, cfg, now).admitClose && !agentShouldAdmitClose("codex", codex, cfg, now).admitClose;
|
|
4158
4337
|
const balanceActive = adviceEligible && drift.heavier !== null && drift.lighter !== null;
|
|
4159
4338
|
const underutilization = adviceEligible && !balanceActive ? underutilizationState(claude, codex, cfg, now) : { recommended: false, reason: null };
|
|
4160
4339
|
const resetEpochs = {
|
|
@@ -4445,6 +4624,58 @@ function computeResumeCandidate(sides, state, cfg, signals) {
|
|
|
4445
4624
|
candidate.detail = detail;
|
|
4446
4625
|
return candidate;
|
|
4447
4626
|
}
|
|
4627
|
+
var INITIAL_ADMISSION_STATE = { side: null, fingerprint: null, reason: null };
|
|
4628
|
+
function nextAdmissionSide(prevSide, state, cfg) {
|
|
4629
|
+
const active = new Set(sideToAgents(prevSide));
|
|
4630
|
+
for (const agent of ["claude", "codex"]) {
|
|
4631
|
+
const usage = state.perAgent[agent];
|
|
4632
|
+
if (agentShouldAdmitClose(agent, usage, cfg, state.now).admitClose) {
|
|
4633
|
+
active.add(agent);
|
|
4634
|
+
} else if (active.has(agent) && agentCanAdmitOpen(usage, cfg, state.now)) {
|
|
4635
|
+
active.delete(agent);
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
return agentsToSide(active);
|
|
4639
|
+
}
|
|
4640
|
+
function admissionReason(side, state, cfg) {
|
|
4641
|
+
return sideToAgents(side).map((agent) => {
|
|
4642
|
+
const usage = state.perAgent[agent];
|
|
4643
|
+
if (!usage)
|
|
4644
|
+
return `${AGENT_LABEL3[agent]} \u63A2\u6D4B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u4FDD\u6301\u4E0A\u4E00\u8F6E\u6536\u5C3E\u4FDD\u62A4`;
|
|
4645
|
+
if (usage.rateLimitedUntil > state.now) {
|
|
4646
|
+
return `${AGENT_LABEL3[agent]} \u63A2\u9488\u88AB\u9650\u6D41\u81F3 ${formatEpoch2(usage.rateLimitedUntil)}\uFF0C\u4FDD\u6301\u6536\u5C3E\u4FDD\u62A4`;
|
|
4647
|
+
}
|
|
4648
|
+
const decision = agentShouldAdmitClose(agent, usage, cfg, state.now);
|
|
4649
|
+
if (decision.admitClose)
|
|
4650
|
+
return decision.reason;
|
|
4651
|
+
return `${AGENT_LABEL3[agent]} \u6536\u5C3E\u4FDD\u62A4\u51FA\u95F8\u6EDE\u56DE\u5E26\uFF0C\u5C1A\u672A\u6EE1\u8DB3\u5F00\u95F8\u6761\u4EF6`;
|
|
4652
|
+
}).join("\uFF1B");
|
|
4653
|
+
}
|
|
4654
|
+
function admissionFingerprint(state, side) {
|
|
4655
|
+
let reset = 0;
|
|
4656
|
+
for (const agent of sideToAgents(side)) {
|
|
4657
|
+
reset = Math.max(reset, state.pause.resetEpochs[agent] ?? 0);
|
|
4658
|
+
}
|
|
4659
|
+
return ["admission", side, Math.round(reset / RESET_FINGERPRINT_BUCKET_SEC)].join("|");
|
|
4660
|
+
}
|
|
4661
|
+
function classifyAdmission(prev, state, cfg) {
|
|
4662
|
+
const previousSide = prev.side;
|
|
4663
|
+
const currentSide = nextAdmissionSide(previousSide, state, cfg);
|
|
4664
|
+
if (currentSide) {
|
|
4665
|
+
const reason = admissionReason(currentSide, state, cfg);
|
|
4666
|
+
const uncertain = previousSide === currentSide && activeSideProbeUncertain(currentSide, state) && prev.fingerprint;
|
|
4667
|
+
const fingerprint = uncertain ? prev.fingerprint : admissionFingerprint(state, currentSide);
|
|
4668
|
+
const emit = !previousSide || previousSide !== currentSide || fingerprint !== prev.fingerprint;
|
|
4669
|
+
return {
|
|
4670
|
+
next: { side: currentSide, fingerprint, reason },
|
|
4671
|
+
effect: { kind: uncertain ? "hold-uncertain" : "enter", side: currentSide, reason, emit }
|
|
4672
|
+
};
|
|
4673
|
+
}
|
|
4674
|
+
if (previousSide) {
|
|
4675
|
+
return { next: INITIAL_ADMISSION_STATE, effect: { kind: "exit", previousSide } };
|
|
4676
|
+
}
|
|
4677
|
+
return { next: INITIAL_ADMISSION_STATE, effect: { kind: "none" } };
|
|
4678
|
+
}
|
|
4448
4679
|
|
|
4449
4680
|
// src/budget/burn-view.ts
|
|
4450
4681
|
function windowBurnRate(window) {
|
|
@@ -4586,9 +4817,13 @@ class BudgetCoordinator {
|
|
|
4586
4817
|
onResume;
|
|
4587
4818
|
resumeSignals;
|
|
4588
4819
|
adviceCooldown;
|
|
4820
|
+
isCodexTurnActive;
|
|
4589
4821
|
timer = null;
|
|
4590
4822
|
running = false;
|
|
4591
4823
|
fpState = INITIAL_FINGERPRINT_STATE;
|
|
4824
|
+
admissionState = INITIAL_ADMISSION_STATE;
|
|
4825
|
+
pendingAdmissionDirective = null;
|
|
4826
|
+
lastEmittedAdmissionFingerprint = null;
|
|
4592
4827
|
resumeCandidate = {};
|
|
4593
4828
|
latestSnapshot = null;
|
|
4594
4829
|
pendingOverrideTier = null;
|
|
@@ -4612,6 +4847,7 @@ class BudgetCoordinator {
|
|
|
4612
4847
|
cooldownSec: resolveAdviceCooldownSec(),
|
|
4613
4848
|
log: this.log
|
|
4614
4849
|
});
|
|
4850
|
+
this.isCodexTurnActive = options.isCodexTurnActive ?? (() => false);
|
|
4615
4851
|
}
|
|
4616
4852
|
async start() {
|
|
4617
4853
|
if (this.running || !this.config.enabled)
|
|
@@ -4634,6 +4870,13 @@ class BudgetCoordinator {
|
|
|
4634
4870
|
isGateClosed() {
|
|
4635
4871
|
return this.fpState.side === "codex" || this.fpState.side === "both";
|
|
4636
4872
|
}
|
|
4873
|
+
gateState() {
|
|
4874
|
+
if (this.fpState.side === "codex" || this.fpState.side === "both")
|
|
4875
|
+
return "closed";
|
|
4876
|
+
if (this.admissionState.side === "codex" || this.admissionState.side === "both")
|
|
4877
|
+
return "admission-closed";
|
|
4878
|
+
return "open";
|
|
4879
|
+
}
|
|
4637
4880
|
getSnapshot() {
|
|
4638
4881
|
return this.latestSnapshot;
|
|
4639
4882
|
}
|
|
@@ -4722,8 +4965,14 @@ class BudgetCoordinator {
|
|
|
4722
4965
|
applyState(state) {
|
|
4723
4966
|
const { next, effect } = classifyPoll(this.fpState, state, this.config);
|
|
4724
4967
|
this.fpState = next;
|
|
4968
|
+
this.admissionState = classifyAdmission(this.admissionState, state, this.config).next;
|
|
4969
|
+
this.applyAdmissionDirective(state);
|
|
4725
4970
|
this.resumeCandidate = this.resumeSignals ? computeResumeCandidate(resumeCandidateSides(effect), state, this.config, this.resumeSignals()) : {};
|
|
4726
4971
|
for (const side of effect.recoveredSides) {
|
|
4972
|
+
if (side === "codex" && (this.admissionState.side === "codex" || this.admissionState.side === "both")) {
|
|
4973
|
+
this.log(`Budget recovery for Codex held: pause cleared but still admission-closed`);
|
|
4974
|
+
continue;
|
|
4975
|
+
}
|
|
4727
4976
|
const { id, directive } = this.emitRecovery(side, state);
|
|
4728
4977
|
this.onResume(side, directive, id);
|
|
4729
4978
|
}
|
|
@@ -4742,6 +4991,10 @@ class BudgetCoordinator {
|
|
|
4742
4991
|
return;
|
|
4743
4992
|
}
|
|
4744
4993
|
case "advise": {
|
|
4994
|
+
if (this.gateState() !== "open") {
|
|
4995
|
+
this.fpState = { ...this.fpState, fingerprint: null };
|
|
4996
|
+
return;
|
|
4997
|
+
}
|
|
4745
4998
|
if (effect.phase === "underutilized") {
|
|
4746
4999
|
if (!this.adviceCooldown.tryAcquire("underutilization", state.now))
|
|
4747
5000
|
return;
|
|
@@ -4755,6 +5008,51 @@ class BudgetCoordinator {
|
|
|
4755
5008
|
return;
|
|
4756
5009
|
}
|
|
4757
5010
|
}
|
|
5011
|
+
applyAdmissionDirective(state) {
|
|
5012
|
+
const side = this.admissionState.side;
|
|
5013
|
+
if (side !== "codex" && side !== "both") {
|
|
5014
|
+
this.pendingAdmissionDirective = null;
|
|
5015
|
+
this.lastEmittedAdmissionFingerprint = null;
|
|
5016
|
+
return;
|
|
5017
|
+
}
|
|
5018
|
+
const fingerprint = this.admissionState.fingerprint;
|
|
5019
|
+
if (fingerprint === null || fingerprint === this.lastEmittedAdmissionFingerprint) {
|
|
5020
|
+
this.pendingAdmissionDirective = null;
|
|
5021
|
+
return;
|
|
5022
|
+
}
|
|
5023
|
+
if (this.isPaused()) {
|
|
5024
|
+
this.pendingAdmissionDirective = null;
|
|
5025
|
+
return;
|
|
5026
|
+
}
|
|
5027
|
+
const content = renderBudgetAdmissionDirective(state.perAgent.claude, state.perAgent.codex, side, this.admissionState.reason ?? "\u989D\u5EA6\u7A97\u53E3\u6536\u5C3E\u4FDD\u62A4", this.admissionResetEpoch(state), this.config);
|
|
5028
|
+
if (this.isCodexTurnActive()) {
|
|
5029
|
+
this.pendingAdmissionDirective = { content, fingerprint };
|
|
5030
|
+
return;
|
|
5031
|
+
}
|
|
5032
|
+
this.emitAdmission(content, fingerprint);
|
|
5033
|
+
}
|
|
5034
|
+
emitAdmission(content, fingerprint) {
|
|
5035
|
+
this.emitDirective("system_budget_admission", content);
|
|
5036
|
+
this.lastEmittedAdmissionFingerprint = fingerprint;
|
|
5037
|
+
this.pendingAdmissionDirective = null;
|
|
5038
|
+
}
|
|
5039
|
+
admissionResetEpoch(state) {
|
|
5040
|
+
const usage = state.perAgent.codex;
|
|
5041
|
+
const now = state.now;
|
|
5042
|
+
const fiveHour = usage?.fiveHour?.resetEpoch ?? 0;
|
|
5043
|
+
const weekly = usage?.weekly?.resetEpoch ?? 0;
|
|
5044
|
+
const fresh = fiveHour > now ? fiveHour : weekly > now ? weekly : 0;
|
|
5045
|
+
return fresh > 0 ? fresh : null;
|
|
5046
|
+
}
|
|
5047
|
+
onCodexTurnIdle() {
|
|
5048
|
+
const pending = this.pendingAdmissionDirective;
|
|
5049
|
+
if (!pending)
|
|
5050
|
+
return;
|
|
5051
|
+
this.pendingAdmissionDirective = null;
|
|
5052
|
+
if (this.isPaused() || this.gateState() !== "admission-closed")
|
|
5053
|
+
return;
|
|
5054
|
+
this.emitAdmission(pending.content, pending.fingerprint);
|
|
5055
|
+
}
|
|
4758
5056
|
tierControlEnabled() {
|
|
4759
5057
|
if (!this.config.codexTierControl)
|
|
4760
5058
|
return false;
|
|
@@ -4836,6 +5134,7 @@ class BudgetCoordinator {
|
|
|
4836
5134
|
driftPct: state.drift.pct,
|
|
4837
5135
|
paused,
|
|
4838
5136
|
gateClosed: this.isGateClosed(),
|
|
5137
|
+
gateState: this.gateState(),
|
|
4839
5138
|
pauseSide: this.fpState.side,
|
|
4840
5139
|
pauseReason: paused ? this.fpState.reason ?? state.pause.reason : null,
|
|
4841
5140
|
resumeAfterEpoch: paused ? this.fpState.resumeEpoch ?? state.pause.resumeAfterEpoch : null,
|
|
@@ -5258,14 +5557,14 @@ function createQuotaSource(options) {
|
|
|
5258
5557
|
// src/budget/pending-reader.ts
|
|
5259
5558
|
import { createHash } from "crypto";
|
|
5260
5559
|
import { join as join7 } from "path";
|
|
5261
|
-
function
|
|
5560
|
+
function nodeFs2() {
|
|
5262
5561
|
return __require("fs");
|
|
5263
5562
|
}
|
|
5264
5563
|
function cwdMatches(entryCwd, optsCwd) {
|
|
5265
5564
|
if (entryCwd === optsCwd)
|
|
5266
5565
|
return true;
|
|
5267
5566
|
try {
|
|
5268
|
-
const fs2 =
|
|
5567
|
+
const fs2 = nodeFs2();
|
|
5269
5568
|
return fs2.realpathSync(entryCwd) === fs2.realpathSync(optsCwd);
|
|
5270
5569
|
} catch {
|
|
5271
5570
|
return false;
|
|
@@ -5303,7 +5602,7 @@ function resolveStateDir2(homeDir) {
|
|
|
5303
5602
|
function readPendingFile(path, log) {
|
|
5304
5603
|
let raw;
|
|
5305
5604
|
try {
|
|
5306
|
-
raw =
|
|
5605
|
+
raw = nodeFs2().readFileSync(path, "utf-8");
|
|
5307
5606
|
} catch (error) {
|
|
5308
5607
|
log(`pending reader: skip unreadable ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5309
5608
|
return null;
|
|
@@ -5327,7 +5626,7 @@ function listScopeFiles(stateDir, agent, log) {
|
|
|
5327
5626
|
const pendingDir = join7(stateDir, "pending");
|
|
5328
5627
|
let names;
|
|
5329
5628
|
try {
|
|
5330
|
-
names =
|
|
5629
|
+
names = nodeFs2().readdirSync(pendingDir);
|
|
5331
5630
|
} catch {
|
|
5332
5631
|
return [];
|
|
5333
5632
|
}
|
|
@@ -6451,7 +6750,10 @@ function ensureBudgetCoordinatorStarted() {
|
|
|
6451
6750
|
onPauseChange: (paused) => {
|
|
6452
6751
|
log(`Budget intervention ${paused ? "ACTIVE" : "CLEARED"} ` + `(gate ${budgetCoordinator?.isGateClosed() ? "CLOSED" : "OPEN"})`);
|
|
6453
6752
|
},
|
|
6454
|
-
onSnapshot: () =>
|
|
6753
|
+
onSnapshot: () => {
|
|
6754
|
+
broadcastStatus();
|
|
6755
|
+
maybeFireCheckpointBaton("snapshot");
|
|
6756
|
+
},
|
|
6455
6757
|
log,
|
|
6456
6758
|
onResume: (side, _directive, resumeId) => {
|
|
6457
6759
|
if (side === "claude") {
|
|
@@ -6462,7 +6764,8 @@ function ensureBudgetCoordinatorStarted() {
|
|
|
6462
6764
|
enqueueCodex: enqueueCodexBudgetResume
|
|
6463
6765
|
});
|
|
6464
6766
|
},
|
|
6465
|
-
resumeSignals: readResumeSignals
|
|
6767
|
+
resumeSignals: readResumeSignals,
|
|
6768
|
+
isCodexTurnActive: () => codex.turnInProgress
|
|
6466
6769
|
});
|
|
6467
6770
|
}
|
|
6468
6771
|
budgetCoordinator.start();
|
|
@@ -6478,6 +6781,79 @@ function budgetPauseGateError() {
|
|
|
6478
6781
|
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
6782
|
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`;
|
|
6480
6783
|
}
|
|
6784
|
+
function budgetAdmissionGateError(windowResetEpoch, wrapUpLeft, quotaExhausted) {
|
|
6785
|
+
const resetAt = windowResetEpoch > 0 ? new Date(windowResetEpoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z") : "\u672A\u77E5";
|
|
6786
|
+
const quota = BUDGET_CONFIG.maximize.wrapUpQuota;
|
|
6787
|
+
if (quotaExhausted) {
|
|
6788
|
+
return `\u989D\u5EA6\u7A97\u53E3\u6536\u5C3E\u4FDD\u62A4\u4E2D\uFF08admission-closed\uFF09\uFF1A\u672C\u7A97\u53E3 wrap-up \u914D\u989D\uFF08\u6BCF\u7A97\u53E3 ${quota} \u4E2A\uFF09\u5DF2\u7528\u5C3D\uFF0C\u5DF2\u62D2\u7EDD\u8F6C\u53D1\u3002` + `\u8BF7\u52FF\u518D\u6D3E\u65B0\u4EFB\u52A1\uFF1B\u5199 checkpoint\uFF0C\u7B49\u989D\u5EA6\u7A97\u53E3\u5237\u65B0\uFF08\u7EA6 ${resetAt}\uFF09\u540E\u518D\u7EE7\u7EED\u3002`;
|
|
6789
|
+
}
|
|
6790
|
+
return `\u989D\u5EA6\u7A97\u53E3\u6536\u5C3E\u4FDD\u62A4\u4E2D\uFF08admission-closed\uFF09\uFF1A\u4EC5\u63A5\u6536\u6536\u5C3E\u7C7B\u6CE8\u5165\uFF0C\u5DF2\u62D2\u7EDD\u8BE5\u65B0\u4EFB\u52A1\u3002` + `\u5982\u9700\u628A\u5F53\u524D\u534F\u4F5C\u6536\u5C3E\u5230 checkpoint\uFF0C\u53EF\u7528 reply \u5E26 wrap_up=true \u91CD\u53D1\uFF08\u672C\u7A97\u53E3\u8FD8\u5269 ${wrapUpLeft} \u4E2A\u6536\u5C3E\u914D\u989D\uFF09\uFF1Bsteer \u4FEE\u6B63\u4E0D\u53D7\u9650\u3002` + `\u65B0\u4EFB\u52A1\u8BF7\u7B49\u989D\u5EA6\u7A97\u53E3\u5237\u65B0\uFF08\u7EA6 ${resetAt}\uFF09\u540E\u518D\u6D3E\u3002`;
|
|
6791
|
+
}
|
|
6792
|
+
function evaluateInjectionBudgetGate(message, willInject, isSteer) {
|
|
6793
|
+
const gateState = budgetCoordinator?.gateState() ?? "open";
|
|
6794
|
+
if (gateState === "closed") {
|
|
6795
|
+
log(`Injection rejected by budget pause gate`);
|
|
6796
|
+
const resumeAfterEpoch3 = budgetCoordinator?.getSnapshot()?.resumeAfterEpoch ?? null;
|
|
6797
|
+
const retryAfterMs = retryAfterMsForResume(resumeAfterEpoch3, Date.now());
|
|
6798
|
+
return {
|
|
6799
|
+
allow: false,
|
|
6800
|
+
code: "budget_paused",
|
|
6801
|
+
error: budgetPauseGateError(),
|
|
6802
|
+
...retryAfterMs !== undefined ? { retryAfterMs } : {}
|
|
6803
|
+
};
|
|
6804
|
+
}
|
|
6805
|
+
if (gateState === "admission-closed" && !isSteer) {
|
|
6806
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
6807
|
+
const admSnap = budgetCoordinator?.getSnapshot()?.codex;
|
|
6808
|
+
const admFiveHour = admSnap?.fiveHour?.resetEpoch ?? 0;
|
|
6809
|
+
const admWeekly = admSnap?.weekly?.resetEpoch ?? 0;
|
|
6810
|
+
const admissionWindowReset = admFiveHour > nowSec ? admFiveHour : admWeekly > nowSec ? admWeekly : 0;
|
|
6811
|
+
if (admissionWindowReset <= 0) {
|
|
6812
|
+
log(`Injection rejected by admission gate: no fresh quota window (probe stale / snapshot lost)`);
|
|
6813
|
+
return { allow: false, code: "budget_admission", error: budgetAdmissionGateError(0, 0, true) };
|
|
6814
|
+
}
|
|
6815
|
+
if (message.wrapUp === true && willInject) {
|
|
6816
|
+
const peek = currentWindowState(stateDir.admissionQuotaFile, admissionWindowReset, log);
|
|
6817
|
+
if (peek.wrapUpUsed >= BUDGET_CONFIG.maximize.wrapUpQuota) {
|
|
6818
|
+
log(`Injection rejected by admission gate: wrap-up quota exhausted`);
|
|
6819
|
+
return { allow: false, code: "budget_admission", error: budgetAdmissionGateError(admissionWindowReset, 0, true) };
|
|
6820
|
+
}
|
|
6821
|
+
log(`Admission-closed: wrap-up permitted (${peek.wrapUpUsed}/${BUDGET_CONFIG.maximize.wrapUpQuota} used; slot committed on inject)`);
|
|
6822
|
+
return { allow: true, pendingWrapUpReset: admissionWindowReset };
|
|
6823
|
+
}
|
|
6824
|
+
if (message.wrapUp === true && !willInject) {
|
|
6825
|
+
return { allow: true, pendingWrapUpReset: null };
|
|
6826
|
+
}
|
|
6827
|
+
const left = Math.max(0, BUDGET_CONFIG.maximize.wrapUpQuota - currentWindowState(stateDir.admissionQuotaFile, admissionWindowReset, log).wrapUpUsed);
|
|
6828
|
+
log(`Injection rejected by admission gate: new task (set wrap_up to finish the current work)`);
|
|
6829
|
+
return { allow: false, code: "budget_admission", error: budgetAdmissionGateError(admissionWindowReset, left, false) };
|
|
6830
|
+
}
|
|
6831
|
+
return { allow: true, pendingWrapUpReset: null };
|
|
6832
|
+
}
|
|
6833
|
+
var CHECKPOINT_BATON_PROMPT = "\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u7CFB\u7EDF\u53D1\u8D77\u3011\u8D26\u53F7\u7EA7\u989D\u5EA6\u5373\u5C06\u8017\u5C3D\uFF0C\u95F8\u95E8\u5DF2\u5173\u95ED\u3002\u8FD9\u662F\u672C\u989D\u5EA6\u7A97\u53E3\u552F\u4E00\u4E00\u6B21\u7CFB\u7EDF\u63D0\u9192\uFF1A" + "\u8BF7\u7ACB\u5373\u628A\u5F53\u524D\u8FDB\u5EA6\u5199\u5165 checkpoint\uFF08.agent/checkpoint.md\uFF1A\u4EFB\u52A1 / \u5DF2\u5B8C\u6210 / \u8FDB\u884C\u4E2D\u65AD\u70B9 / \u4E0B\u4E00\u6B65 / \u5173\u952E\u51B3\u7B56\u4E0E\u7EA6\u675F\uFF09\uFF0C" + "\u7136\u540E\u505C\u624B\u7B49\u5F85\u989D\u5EA6\u7A97\u53E3\u5237\u65B0\uFF1B\u5237\u65B0\u524D\u4E0D\u8981\u518D\u5F00\u65B0\u4EFB\u52A1\u3002\u6B64\u4E3A\u7CFB\u7EDF\u63D0\u9192\uFF0C\u65E0\u9700\u56DE\u590D Claude\u3002";
|
|
6834
|
+
function maybeFireCheckpointBaton(trigger) {
|
|
6835
|
+
if (!budgetCoordinator)
|
|
6836
|
+
return;
|
|
6837
|
+
if (budgetCoordinator.gateState() !== "closed")
|
|
6838
|
+
return;
|
|
6839
|
+
if (!codex.canInject())
|
|
6840
|
+
return;
|
|
6841
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
6842
|
+
const snap = budgetCoordinator.getSnapshot()?.codex;
|
|
6843
|
+
const fiveHour = snap?.fiveHour?.resetEpoch ?? 0;
|
|
6844
|
+
const weekly = snap?.weekly?.resetEpoch ?? 0;
|
|
6845
|
+
const windowReset = fiveHour > nowSec ? fiveHour : weekly > nowSec ? weekly : 0;
|
|
6846
|
+
if (windowReset <= 0)
|
|
6847
|
+
return;
|
|
6848
|
+
if (!consumeCheckpointBaton(stateDir.admissionQuotaFile, windowReset, log))
|
|
6849
|
+
return;
|
|
6850
|
+
const injectionId = codex.injectMessage(CHECKPOINT_BATON_PROMPT);
|
|
6851
|
+
if (injectionId === null) {
|
|
6852
|
+
log(`Checkpoint baton (${trigger}): inject failed after consume \u2014 baton lost this window (reset ${windowReset})`);
|
|
6853
|
+
return;
|
|
6854
|
+
}
|
|
6855
|
+
log(`Checkpoint baton fired (${trigger}, window reset ${windowReset})`);
|
|
6856
|
+
}
|
|
6481
6857
|
var tuiConnectionState = new TuiConnectionState({
|
|
6482
6858
|
disconnectGraceMs: TUI_DISCONNECT_GRACE_MS,
|
|
6483
6859
|
log,
|
|
@@ -6499,6 +6875,10 @@ function tryWriteStatusFile(reason) {
|
|
|
6499
6875
|
codex.on("turnPhaseChanged", ({ phase, previous }) => {
|
|
6500
6876
|
log(`Codex turn phase: ${previous} \u2192 ${phase}`);
|
|
6501
6877
|
tryWriteStatusFile(`turnPhase:${phase}`);
|
|
6878
|
+
if (phase === "idle" || phase === "aborted") {
|
|
6879
|
+
budgetCoordinator?.onCodexTurnIdle();
|
|
6880
|
+
maybeFireCheckpointBaton("turnIdle");
|
|
6881
|
+
}
|
|
6502
6882
|
broadcastStatus();
|
|
6503
6883
|
});
|
|
6504
6884
|
codex.on("steerFailed", ({ requestId, reason }) => {
|
|
@@ -6903,18 +7283,21 @@ async function handleClaudeToCodex(ws, message) {
|
|
|
6903
7283
|
});
|
|
6904
7284
|
return;
|
|
6905
7285
|
}
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
const
|
|
6910
|
-
const
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
7286
|
+
let pendingWrapUpReset = null;
|
|
7287
|
+
{
|
|
7288
|
+
const isSteer = codex.turnInProgress && message.onBusy === "steer";
|
|
7289
|
+
const willInject = !codex.turnInProgress || message.onBusy === "interrupt";
|
|
7290
|
+
const gate = evaluateInjectionBudgetGate(message, willInject, isSteer);
|
|
7291
|
+
if (!gate.allow) {
|
|
7292
|
+
sendClaudeToCodexResult(ws, message.requestId, {
|
|
7293
|
+
success: false,
|
|
7294
|
+
code: gate.code,
|
|
7295
|
+
error: gate.error,
|
|
7296
|
+
...gate.retryAfterMs !== undefined ? { retryAfterMs: gate.retryAfterMs } : {}
|
|
7297
|
+
});
|
|
7298
|
+
return;
|
|
7299
|
+
}
|
|
7300
|
+
pendingWrapUpReset = gate.pendingWrapUpReset;
|
|
6918
7301
|
}
|
|
6919
7302
|
const requireReply = !!message.requireReply;
|
|
6920
7303
|
let contentToSend = message.message.content;
|
|
@@ -7003,8 +7386,36 @@ async function handleClaudeToCodex(ws, message) {
|
|
|
7003
7386
|
if (interruptThreadId && codex.activeThreadId !== interruptThreadId) {
|
|
7004
7387
|
releaseInterruptKey();
|
|
7005
7388
|
}
|
|
7389
|
+
{
|
|
7390
|
+
const gate = evaluateInjectionBudgetGate(message, true, false);
|
|
7391
|
+
if (!gate.allow) {
|
|
7392
|
+
releaseInterruptKey();
|
|
7393
|
+
log(`Interrupt-path injection rejected by budget gate after await (${gate.code})`);
|
|
7394
|
+
sendClaudeToCodexResult(ws, message.requestId, {
|
|
7395
|
+
success: false,
|
|
7396
|
+
code: gate.code,
|
|
7397
|
+
error: gate.error,
|
|
7398
|
+
...gate.retryAfterMs !== undefined ? { retryAfterMs: gate.retryAfterMs } : {}
|
|
7399
|
+
});
|
|
7400
|
+
return;
|
|
7401
|
+
}
|
|
7402
|
+
pendingWrapUpReset = gate.pendingWrapUpReset;
|
|
7403
|
+
}
|
|
7006
7404
|
}
|
|
7007
7405
|
const injectThreadId = codex.activeThreadId;
|
|
7406
|
+
if (pendingWrapUpReset !== null) {
|
|
7407
|
+
const committed = consumeWrapUp(stateDir.admissionQuotaFile, pendingWrapUpReset, BUDGET_CONFIG.maximize.wrapUpQuota, log);
|
|
7408
|
+
if (!committed.allowed) {
|
|
7409
|
+
log(`Injection rejected by admission gate: wrap-up slot not durably recorded (write failure or raced to cap)`);
|
|
7410
|
+
sendClaudeToCodexResult(ws, message.requestId, {
|
|
7411
|
+
success: false,
|
|
7412
|
+
code: "budget_admission",
|
|
7413
|
+
error: budgetAdmissionGateError(pendingWrapUpReset, 0, true)
|
|
7414
|
+
});
|
|
7415
|
+
return;
|
|
7416
|
+
}
|
|
7417
|
+
log(`Admission wrap-up slot committed (${committed.used}/${BUDGET_CONFIG.maximize.wrapUpQuota})`);
|
|
7418
|
+
}
|
|
7008
7419
|
const injectionId = codex.injectMessage(contentToSend, tierOverrides);
|
|
7009
7420
|
if (injectionId === null) {
|
|
7010
7421
|
if (idempotencyKey && injectThreadId) {
|