fix(primer): repair Claude binary path and re-anchor Codex window after reset

Two evidence-based fixes for the usage-window primer that aligns the 5h
reset to fixed KST slots (08/13/18/23):

1. Claude primer always failed with binary_missing: it only looked under
   runners/agent-runner/node_modules, but the SDK platform binary is hoisted
   to the repo-root node_modules. Resolve both locations.

2. When a Codex slot lands while the 5h window is still active/exhausted, the
   warm-up call fails with a rate-limit and the window only re-anchors at its
   natural reset — leaving Codex un-primed until the next 5h slot. Parse the
   account's known reset time and schedule a one-shot retry right after it so
   the next window anchors close to the slot.

Also corrected a stale comment claiming a 04:00–08:00 gap is enforced by
message-runtime-gating (it is not).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-09 13:31:05 +09:00
parent 65510e0fc3
commit 2313d6cf3e
2 changed files with 124 additions and 16 deletions

View File

@@ -12,8 +12,13 @@ const refreshActiveCodexUsage = vi.fn(async () => {
});
const runCodexWarmupCycle = vi.fn(async () => {
callOrder.push('warmup');
return { status: 'warmed', accountIndex: 0 };
return { status: 'warmed', accountIndex: 0 } as {
status: string;
accountIndex?: number;
reason?: string;
};
});
const getAllCodexAccounts = vi.fn<() => Array<{ resetAt?: string }>>(() => []);
vi.mock('./config.js', () => ({
CODEX_WARMUP_CONFIG: {
@@ -53,6 +58,10 @@ vi.mock('./codex-warmup.js', () => ({
runCodexWarmupCycle,
}));
vi.mock('./codex-token-rotation.js', () => ({
getAllCodexAccounts,
}));
describe('usage-primer', () => {
it('refreshes Codex usage before primer selection and fires regardless of usage level', async () => {
callOrder.length = 0;
@@ -74,4 +83,31 @@ describe('usage-primer', () => {
{ ignoreZeroUsageWindow: true },
);
});
it('reschedules a retry at the window reset when the slot is rate-limited', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-06-09T00:00:00.000Z'));
runCodexWarmupCycle.mockClear();
// First slot: the 5h window is still active/exhausted → command fails.
runCodexWarmupCycle.mockResolvedValueOnce({
status: 'failed',
accountIndex: 0,
reason: 'exit_1',
});
// The account's window resets 60s from now.
getAllCodexAccounts.mockReturnValue([
{ resetAt: '2026-06-09T00:01:00.000Z' },
]);
const { runCodexPrimerCycle } = await import('./usage-primer.js');
await runCodexPrimerCycle();
expect(runCodexWarmupCycle).toHaveBeenCalledTimes(1);
// Retry is scheduled at reset (+60s) plus a 30s buffer = +90s.
await vi.advanceTimersByTimeAsync(91_000);
expect(runCodexWarmupCycle).toHaveBeenCalledTimes(2);
getAllCodexAccounts.mockReturnValue([]);
vi.useRealTimers();
});
});