@raysonmeng/agentbridge 0.1.18 → 0.1.20
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 +26 -12
- package/dist/daemon.js +442 -29
- package/package.json +1 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +32 -14
- package/plugins/agentbridge/server/daemon.js +442 -29
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.20", "0.0.0-source"),
|
|
33
|
+
commit: defineString("d6034c6", "source"),
|
|
34
34
|
bundle: defineBundle("dist"),
|
|
35
35
|
contractVersion: defineNumber(1, CONTRACT_VERSION),
|
|
36
|
-
codeHash: defineString("
|
|
36
|
+
codeHash: defineString("d2425ba159fe", "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,16 @@ class BudgetCoordinator {
|
|
|
4722
4965
|
applyState(state) {
|
|
4723
4966
|
const { next, effect } = classifyPoll(this.fpState, state, this.config);
|
|
4724
4967
|
this.fpState = next;
|
|
4725
|
-
this.
|
|
4968
|
+
this.admissionState = classifyAdmission(this.admissionState, state, this.config).next;
|
|
4969
|
+
this.applyAdmissionDirective(state);
|
|
4970
|
+
const codexAdmissionHeld = this.admissionState.side === "codex" || this.admissionState.side === "both";
|
|
4971
|
+
const candidateSides = resumeCandidateSides(effect).filter((side) => !(side === "codex" && codexAdmissionHeld));
|
|
4972
|
+
this.resumeCandidate = this.resumeSignals ? computeResumeCandidate(candidateSides, state, this.config, this.resumeSignals()) : {};
|
|
4726
4973
|
for (const side of effect.recoveredSides) {
|
|
4974
|
+
if (side === "codex" && codexAdmissionHeld) {
|
|
4975
|
+
this.log(`Budget recovery for Codex held: pause cleared but still admission-closed`);
|
|
4976
|
+
continue;
|
|
4977
|
+
}
|
|
4727
4978
|
const { id, directive } = this.emitRecovery(side, state);
|
|
4728
4979
|
this.onResume(side, directive, id);
|
|
4729
4980
|
}
|
|
@@ -4742,6 +4993,10 @@ class BudgetCoordinator {
|
|
|
4742
4993
|
return;
|
|
4743
4994
|
}
|
|
4744
4995
|
case "advise": {
|
|
4996
|
+
if (this.gateState() !== "open") {
|
|
4997
|
+
this.fpState = { ...this.fpState, fingerprint: null };
|
|
4998
|
+
return;
|
|
4999
|
+
}
|
|
4745
5000
|
if (effect.phase === "underutilized") {
|
|
4746
5001
|
if (!this.adviceCooldown.tryAcquire("underutilization", state.now))
|
|
4747
5002
|
return;
|
|
@@ -4755,6 +5010,51 @@ class BudgetCoordinator {
|
|
|
4755
5010
|
return;
|
|
4756
5011
|
}
|
|
4757
5012
|
}
|
|
5013
|
+
applyAdmissionDirective(state) {
|
|
5014
|
+
const side = this.admissionState.side;
|
|
5015
|
+
if (side !== "codex" && side !== "both") {
|
|
5016
|
+
this.pendingAdmissionDirective = null;
|
|
5017
|
+
this.lastEmittedAdmissionFingerprint = null;
|
|
5018
|
+
return;
|
|
5019
|
+
}
|
|
5020
|
+
const fingerprint = this.admissionState.fingerprint;
|
|
5021
|
+
if (fingerprint === null || fingerprint === this.lastEmittedAdmissionFingerprint) {
|
|
5022
|
+
this.pendingAdmissionDirective = null;
|
|
5023
|
+
return;
|
|
5024
|
+
}
|
|
5025
|
+
if (this.isPaused()) {
|
|
5026
|
+
this.pendingAdmissionDirective = null;
|
|
5027
|
+
return;
|
|
5028
|
+
}
|
|
5029
|
+
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);
|
|
5030
|
+
if (this.isCodexTurnActive()) {
|
|
5031
|
+
this.pendingAdmissionDirective = { content, fingerprint };
|
|
5032
|
+
return;
|
|
5033
|
+
}
|
|
5034
|
+
this.emitAdmission(content, fingerprint);
|
|
5035
|
+
}
|
|
5036
|
+
emitAdmission(content, fingerprint) {
|
|
5037
|
+
this.emitDirective("system_budget_admission", content);
|
|
5038
|
+
this.lastEmittedAdmissionFingerprint = fingerprint;
|
|
5039
|
+
this.pendingAdmissionDirective = null;
|
|
5040
|
+
}
|
|
5041
|
+
admissionResetEpoch(state) {
|
|
5042
|
+
const usage = state.perAgent.codex;
|
|
5043
|
+
const now = state.now;
|
|
5044
|
+
const fiveHour = usage?.fiveHour?.resetEpoch ?? 0;
|
|
5045
|
+
const weekly = usage?.weekly?.resetEpoch ?? 0;
|
|
5046
|
+
const fresh = fiveHour > now ? fiveHour : weekly > now ? weekly : 0;
|
|
5047
|
+
return fresh > 0 ? fresh : null;
|
|
5048
|
+
}
|
|
5049
|
+
onCodexTurnIdle() {
|
|
5050
|
+
const pending = this.pendingAdmissionDirective;
|
|
5051
|
+
if (!pending)
|
|
5052
|
+
return;
|
|
5053
|
+
this.pendingAdmissionDirective = null;
|
|
5054
|
+
if (this.isPaused() || this.gateState() !== "admission-closed")
|
|
5055
|
+
return;
|
|
5056
|
+
this.emitAdmission(pending.content, pending.fingerprint);
|
|
5057
|
+
}
|
|
4758
5058
|
tierControlEnabled() {
|
|
4759
5059
|
if (!this.config.codexTierControl)
|
|
4760
5060
|
return false;
|
|
@@ -4836,6 +5136,7 @@ class BudgetCoordinator {
|
|
|
4836
5136
|
driftPct: state.drift.pct,
|
|
4837
5137
|
paused,
|
|
4838
5138
|
gateClosed: this.isGateClosed(),
|
|
5139
|
+
gateState: this.gateState(),
|
|
4839
5140
|
pauseSide: this.fpState.side,
|
|
4840
5141
|
pauseReason: paused ? this.fpState.reason ?? state.pause.reason : null,
|
|
4841
5142
|
resumeAfterEpoch: paused ? this.fpState.resumeEpoch ?? state.pause.resumeAfterEpoch : null,
|
|
@@ -5258,14 +5559,14 @@ function createQuotaSource(options) {
|
|
|
5258
5559
|
// src/budget/pending-reader.ts
|
|
5259
5560
|
import { createHash } from "crypto";
|
|
5260
5561
|
import { join as join7 } from "path";
|
|
5261
|
-
function
|
|
5562
|
+
function nodeFs2() {
|
|
5262
5563
|
return __require("fs");
|
|
5263
5564
|
}
|
|
5264
5565
|
function cwdMatches(entryCwd, optsCwd) {
|
|
5265
5566
|
if (entryCwd === optsCwd)
|
|
5266
5567
|
return true;
|
|
5267
5568
|
try {
|
|
5268
|
-
const fs2 =
|
|
5569
|
+
const fs2 = nodeFs2();
|
|
5269
5570
|
return fs2.realpathSync(entryCwd) === fs2.realpathSync(optsCwd);
|
|
5270
5571
|
} catch {
|
|
5271
5572
|
return false;
|
|
@@ -5303,7 +5604,7 @@ function resolveStateDir2(homeDir) {
|
|
|
5303
5604
|
function readPendingFile(path, log) {
|
|
5304
5605
|
let raw;
|
|
5305
5606
|
try {
|
|
5306
|
-
raw =
|
|
5607
|
+
raw = nodeFs2().readFileSync(path, "utf-8");
|
|
5307
5608
|
} catch (error) {
|
|
5308
5609
|
log(`pending reader: skip unreadable ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5309
5610
|
return null;
|
|
@@ -5327,7 +5628,7 @@ function listScopeFiles(stateDir, agent, log) {
|
|
|
5327
5628
|
const pendingDir = join7(stateDir, "pending");
|
|
5328
5629
|
let names;
|
|
5329
5630
|
try {
|
|
5330
|
-
names =
|
|
5631
|
+
names = nodeFs2().readdirSync(pendingDir);
|
|
5331
5632
|
} catch {
|
|
5332
5633
|
return [];
|
|
5333
5634
|
}
|
|
@@ -6451,7 +6752,10 @@ function ensureBudgetCoordinatorStarted() {
|
|
|
6451
6752
|
onPauseChange: (paused) => {
|
|
6452
6753
|
log(`Budget intervention ${paused ? "ACTIVE" : "CLEARED"} ` + `(gate ${budgetCoordinator?.isGateClosed() ? "CLOSED" : "OPEN"})`);
|
|
6453
6754
|
},
|
|
6454
|
-
onSnapshot: () =>
|
|
6755
|
+
onSnapshot: () => {
|
|
6756
|
+
broadcastStatus();
|
|
6757
|
+
maybeFireCheckpointBaton("snapshot");
|
|
6758
|
+
},
|
|
6455
6759
|
log,
|
|
6456
6760
|
onResume: (side, _directive, resumeId) => {
|
|
6457
6761
|
if (side === "claude") {
|
|
@@ -6462,7 +6766,8 @@ function ensureBudgetCoordinatorStarted() {
|
|
|
6462
6766
|
enqueueCodex: enqueueCodexBudgetResume
|
|
6463
6767
|
});
|
|
6464
6768
|
},
|
|
6465
|
-
resumeSignals: readResumeSignals
|
|
6769
|
+
resumeSignals: readResumeSignals,
|
|
6770
|
+
isCodexTurnActive: () => codex.turnInProgress
|
|
6466
6771
|
});
|
|
6467
6772
|
}
|
|
6468
6773
|
budgetCoordinator.start();
|
|
@@ -6478,6 +6783,79 @@ function budgetPauseGateError() {
|
|
|
6478
6783
|
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
6784
|
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
6785
|
}
|
|
6786
|
+
function budgetAdmissionGateError(windowResetEpoch, wrapUpLeft, quotaExhausted) {
|
|
6787
|
+
const resetAt = windowResetEpoch > 0 ? new Date(windowResetEpoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z") : "\u672A\u77E5";
|
|
6788
|
+
const quota = BUDGET_CONFIG.maximize.wrapUpQuota;
|
|
6789
|
+
if (quotaExhausted) {
|
|
6790
|
+
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`;
|
|
6791
|
+
}
|
|
6792
|
+
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`;
|
|
6793
|
+
}
|
|
6794
|
+
function evaluateInjectionBudgetGate(message, willInject, isSteer) {
|
|
6795
|
+
const gateState = budgetCoordinator?.gateState() ?? "open";
|
|
6796
|
+
if (gateState === "closed") {
|
|
6797
|
+
log(`Injection rejected by budget pause gate`);
|
|
6798
|
+
const resumeAfterEpoch3 = budgetCoordinator?.getSnapshot()?.resumeAfterEpoch ?? null;
|
|
6799
|
+
const retryAfterMs = retryAfterMsForResume(resumeAfterEpoch3, Date.now());
|
|
6800
|
+
return {
|
|
6801
|
+
allow: false,
|
|
6802
|
+
code: "budget_paused",
|
|
6803
|
+
error: budgetPauseGateError(),
|
|
6804
|
+
...retryAfterMs !== undefined ? { retryAfterMs } : {}
|
|
6805
|
+
};
|
|
6806
|
+
}
|
|
6807
|
+
if (gateState === "admission-closed" && !isSteer) {
|
|
6808
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
6809
|
+
const admSnap = budgetCoordinator?.getSnapshot()?.codex;
|
|
6810
|
+
const admFiveHour = admSnap?.fiveHour?.resetEpoch ?? 0;
|
|
6811
|
+
const admWeekly = admSnap?.weekly?.resetEpoch ?? 0;
|
|
6812
|
+
const admissionWindowReset = admFiveHour > nowSec ? admFiveHour : admWeekly > nowSec ? admWeekly : 0;
|
|
6813
|
+
if (admissionWindowReset <= 0) {
|
|
6814
|
+
log(`Injection rejected by admission gate: no fresh quota window (probe stale / snapshot lost)`);
|
|
6815
|
+
return { allow: false, code: "budget_admission", error: budgetAdmissionGateError(0, 0, true) };
|
|
6816
|
+
}
|
|
6817
|
+
if (message.wrapUp === true && willInject) {
|
|
6818
|
+
const peek = currentWindowState(stateDir.admissionQuotaFile, admissionWindowReset, log);
|
|
6819
|
+
if (peek.wrapUpUsed >= BUDGET_CONFIG.maximize.wrapUpQuota) {
|
|
6820
|
+
log(`Injection rejected by admission gate: wrap-up quota exhausted`);
|
|
6821
|
+
return { allow: false, code: "budget_admission", error: budgetAdmissionGateError(admissionWindowReset, 0, true) };
|
|
6822
|
+
}
|
|
6823
|
+
log(`Admission-closed: wrap-up permitted (${peek.wrapUpUsed}/${BUDGET_CONFIG.maximize.wrapUpQuota} used; slot committed on inject)`);
|
|
6824
|
+
return { allow: true, pendingWrapUpReset: admissionWindowReset };
|
|
6825
|
+
}
|
|
6826
|
+
if (message.wrapUp === true && !willInject) {
|
|
6827
|
+
return { allow: true, pendingWrapUpReset: null };
|
|
6828
|
+
}
|
|
6829
|
+
const left = Math.max(0, BUDGET_CONFIG.maximize.wrapUpQuota - currentWindowState(stateDir.admissionQuotaFile, admissionWindowReset, log).wrapUpUsed);
|
|
6830
|
+
log(`Injection rejected by admission gate: new task (set wrap_up to finish the current work)`);
|
|
6831
|
+
return { allow: false, code: "budget_admission", error: budgetAdmissionGateError(admissionWindowReset, left, false) };
|
|
6832
|
+
}
|
|
6833
|
+
return { allow: true, pendingWrapUpReset: null };
|
|
6834
|
+
}
|
|
6835
|
+
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";
|
|
6836
|
+
function maybeFireCheckpointBaton(trigger) {
|
|
6837
|
+
if (!budgetCoordinator)
|
|
6838
|
+
return;
|
|
6839
|
+
if (budgetCoordinator.gateState() !== "closed")
|
|
6840
|
+
return;
|
|
6841
|
+
if (!codex.canInject())
|
|
6842
|
+
return;
|
|
6843
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
6844
|
+
const snap = budgetCoordinator.getSnapshot()?.codex;
|
|
6845
|
+
const fiveHour = snap?.fiveHour?.resetEpoch ?? 0;
|
|
6846
|
+
const weekly = snap?.weekly?.resetEpoch ?? 0;
|
|
6847
|
+
const windowReset = fiveHour > nowSec ? fiveHour : weekly > nowSec ? weekly : 0;
|
|
6848
|
+
if (windowReset <= 0)
|
|
6849
|
+
return;
|
|
6850
|
+
if (!consumeCheckpointBaton(stateDir.admissionQuotaFile, windowReset, log))
|
|
6851
|
+
return;
|
|
6852
|
+
const injectionId = codex.injectMessage(CHECKPOINT_BATON_PROMPT);
|
|
6853
|
+
if (injectionId === null) {
|
|
6854
|
+
log(`Checkpoint baton (${trigger}): inject failed after consume \u2014 baton lost this window (reset ${windowReset})`);
|
|
6855
|
+
return;
|
|
6856
|
+
}
|
|
6857
|
+
log(`Checkpoint baton fired (${trigger}, window reset ${windowReset})`);
|
|
6858
|
+
}
|
|
6481
6859
|
var tuiConnectionState = new TuiConnectionState({
|
|
6482
6860
|
disconnectGraceMs: TUI_DISCONNECT_GRACE_MS,
|
|
6483
6861
|
log,
|
|
@@ -6499,6 +6877,10 @@ function tryWriteStatusFile(reason) {
|
|
|
6499
6877
|
codex.on("turnPhaseChanged", ({ phase, previous }) => {
|
|
6500
6878
|
log(`Codex turn phase: ${previous} \u2192 ${phase}`);
|
|
6501
6879
|
tryWriteStatusFile(`turnPhase:${phase}`);
|
|
6880
|
+
if (phase === "idle" || phase === "aborted") {
|
|
6881
|
+
budgetCoordinator?.onCodexTurnIdle();
|
|
6882
|
+
maybeFireCheckpointBaton("turnIdle");
|
|
6883
|
+
}
|
|
6502
6884
|
broadcastStatus();
|
|
6503
6885
|
});
|
|
6504
6886
|
codex.on("steerFailed", ({ requestId, reason }) => {
|
|
@@ -6903,18 +7285,21 @@ async function handleClaudeToCodex(ws, message) {
|
|
|
6903
7285
|
});
|
|
6904
7286
|
return;
|
|
6905
7287
|
}
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
const
|
|
6910
|
-
const
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
7288
|
+
let pendingWrapUpReset = null;
|
|
7289
|
+
{
|
|
7290
|
+
const isSteer = codex.turnInProgress && message.onBusy === "steer";
|
|
7291
|
+
const willInject = !codex.turnInProgress || message.onBusy === "interrupt";
|
|
7292
|
+
const gate = evaluateInjectionBudgetGate(message, willInject, isSteer);
|
|
7293
|
+
if (!gate.allow) {
|
|
7294
|
+
sendClaudeToCodexResult(ws, message.requestId, {
|
|
7295
|
+
success: false,
|
|
7296
|
+
code: gate.code,
|
|
7297
|
+
error: gate.error,
|
|
7298
|
+
...gate.retryAfterMs !== undefined ? { retryAfterMs: gate.retryAfterMs } : {}
|
|
7299
|
+
});
|
|
7300
|
+
return;
|
|
7301
|
+
}
|
|
7302
|
+
pendingWrapUpReset = gate.pendingWrapUpReset;
|
|
6918
7303
|
}
|
|
6919
7304
|
const requireReply = !!message.requireReply;
|
|
6920
7305
|
let contentToSend = message.message.content;
|
|
@@ -7003,8 +7388,36 @@ async function handleClaudeToCodex(ws, message) {
|
|
|
7003
7388
|
if (interruptThreadId && codex.activeThreadId !== interruptThreadId) {
|
|
7004
7389
|
releaseInterruptKey();
|
|
7005
7390
|
}
|
|
7391
|
+
{
|
|
7392
|
+
const gate = evaluateInjectionBudgetGate(message, true, false);
|
|
7393
|
+
if (!gate.allow) {
|
|
7394
|
+
releaseInterruptKey();
|
|
7395
|
+
log(`Interrupt-path injection rejected by budget gate after await (${gate.code})`);
|
|
7396
|
+
sendClaudeToCodexResult(ws, message.requestId, {
|
|
7397
|
+
success: false,
|
|
7398
|
+
code: gate.code,
|
|
7399
|
+
error: gate.error,
|
|
7400
|
+
...gate.retryAfterMs !== undefined ? { retryAfterMs: gate.retryAfterMs } : {}
|
|
7401
|
+
});
|
|
7402
|
+
return;
|
|
7403
|
+
}
|
|
7404
|
+
pendingWrapUpReset = gate.pendingWrapUpReset;
|
|
7405
|
+
}
|
|
7006
7406
|
}
|
|
7007
7407
|
const injectThreadId = codex.activeThreadId;
|
|
7408
|
+
if (pendingWrapUpReset !== null) {
|
|
7409
|
+
const committed = consumeWrapUp(stateDir.admissionQuotaFile, pendingWrapUpReset, BUDGET_CONFIG.maximize.wrapUpQuota, log);
|
|
7410
|
+
if (!committed.allowed) {
|
|
7411
|
+
log(`Injection rejected by admission gate: wrap-up slot not durably recorded (write failure or raced to cap)`);
|
|
7412
|
+
sendClaudeToCodexResult(ws, message.requestId, {
|
|
7413
|
+
success: false,
|
|
7414
|
+
code: "budget_admission",
|
|
7415
|
+
error: budgetAdmissionGateError(pendingWrapUpReset, 0, true)
|
|
7416
|
+
});
|
|
7417
|
+
return;
|
|
7418
|
+
}
|
|
7419
|
+
log(`Admission wrap-up slot committed (${committed.used}/${BUDGET_CONFIG.maximize.wrapUpQuota})`);
|
|
7420
|
+
}
|
|
7008
7421
|
const injectionId = codex.injectMessage(contentToSend, tierOverrides);
|
|
7009
7422
|
if (injectionId === null) {
|
|
7010
7423
|
if (idempotencyKey && injectThreadId) {
|