config: add auto-compact settings for Claude and Codex agents

Claude: CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=50 (~500K on 1M context)
Codex: model_auto_compact_token_limit=258000 (matching CLI default)
This commit is contained in:
Eyejoker
2026-03-25 03:56:03 +09:00
parent 744e2ce6e2
commit 906a3dfadb
16 changed files with 484 additions and 379 deletions

View File

@@ -14,8 +14,17 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import {
classifyAgentError,
classifyCodexAuthError,
} from './agent-error-detection.js';
import { DATA_DIR } from './config.js';
import { logger } from './logger.js';
import {
computeCooldownUntil,
findNextAvailable,
parseRetryAfterFromError,
} from './token-rotation-base.js';
const STATE_FILE = path.join(DATA_DIR, 'codex-rotation-state.json');
@@ -195,35 +204,6 @@ function loadCodexState(): void {
}
}
const BUFFER_MS = 3 * 60_000; // 3 min buffer after reset time
const DEFAULT_COOLDOWN_MS = 3_600_000; // 1 hour fallback
/**
* Parse "try again at Mar 26th, 2026 9:00 AM" from error message.
* Returns timestamp in ms, or null if not found.
*/
function parseRetryAfterFromError(error?: string): number | null {
if (!error) return null;
const match = error.match(
/try again at\s+(\w+ \d{1,2}(?:st|nd|rd|th)?,?\s+\d{4}\s+\d{1,2}:\d{2}\s*(?:AM|PM)?)/i,
);
if (!match) return null;
try {
// Remove ordinal suffixes (1st, 2nd, 3rd, 4th)
const cleaned = match[1].replace(/(\d+)(?:st|nd|rd|th)/i, '$1');
const ts = new Date(cleaned).getTime();
if (Number.isNaN(ts)) return null;
return ts;
} catch {
return null;
}
}
function computeCooldownUntil(error?: string): number {
const retryAt = parseRetryAfterFromError(error);
if (retryAt) return retryAt + BUFFER_MS;
return Date.now() + DEFAULT_COOLDOWN_MS;
}
/** Get the auth.json path for the current active account. */
export function getActiveCodexAuthPath(): string | null {
@@ -236,43 +216,16 @@ export function detectCodexRotationTrigger(
): CodexRotationTriggerResult {
if (!error) return { shouldRotate: false, reason: '' };
const lower = error.toLowerCase();
if (
lower.includes('429') ||
lower.includes('rate limit') ||
lower.includes('usage limit') ||
lower.includes('hit your limit') ||
lower.includes('too many requests') ||
lower.includes('rate_limit')
) {
return { shouldRotate: true, reason: '429' };
// Common patterns (429, 503, network) — delegated to SSOT
const common = classifyAgentError(error);
if (common.category !== 'none') {
return { shouldRotate: true, reason: common.reason };
}
if (lower.includes('503') || lower.includes('overloaded')) {
return { shouldRotate: true, reason: 'overloaded' };
}
if (
lower.includes('econnrefused') ||
lower.includes('econnreset') ||
lower.includes('etimedout') ||
lower.includes('enotfound') ||
lower.includes('fetch failed') ||
lower.includes('network error')
) {
return { shouldRotate: true, reason: 'network-error' };
}
if (
lower.includes('401') ||
lower.includes('authentication_error') ||
lower.includes('failed to authenticate') ||
lower.includes('oauth token has expired') ||
lower.includes('refresh your existing token') ||
lower.includes('unauthorized')
) {
return { shouldRotate: true, reason: 'auth-expired' };
// Codex-specific loose auth check
const auth = classifyCodexAuthError(error);
if (auth.category !== 'none') {
return { shouldRotate: true, reason: auth.reason };
}
return { shouldRotate: false, reason: '' };
@@ -282,36 +235,35 @@ export function detectCodexRotationTrigger(
* Try to rotate to the next available Codex account.
* Returns true if a fresh account was found.
*/
export function rotateCodexToken(errorMessage?: string): boolean {
export function rotateCodexToken(
errorMessage?: string,
opts?: { ignoreRateLimits?: boolean },
): boolean {
if (accounts.length <= 1) return false;
const now = Date.now();
const acct = accounts[currentIndex];
acct.rateLimitedUntil = computeCooldownUntil(errorMessage);
acct.lastUsagePct = 100;
// Extract reset time string from error for display
const resetMatch = errorMessage?.match(
/try again at\s+(\w+ \d{1,2}(?:st|nd|rd|th)?,?\s+\d{4}\s+\d{1,2}:\d{2}\s*(?:AM|PM)?)/i,
);
if (resetMatch) acct.resetAt = resetMatch[1];
const retryAt = parseRetryAfterFromError(errorMessage);
if (retryAt) {
acct.resetAt = new Date(retryAt).toISOString();
}
for (let i = 1; i < accounts.length; i++) {
const idx = (currentIndex + i) % accounts.length;
const acct = accounts[idx];
if (!acct.rateLimitedUntil || acct.rateLimitedUntil <= now) {
acct.rateLimitedUntil = null;
currentIndex = idx;
logger.info(
{
accountIndex: currentIndex,
totalAccounts: accounts.length,
accountId: acct.accountId,
},
`Codex rotated to account #${currentIndex + 1}/${accounts.length}`,
);
saveCodexState();
return true;
}
const nextIdx = findNextAvailable(accounts, currentIndex, opts);
if (nextIdx !== null) {
accounts[nextIdx].rateLimitedUntil = null;
currentIndex = nextIdx;
logger.info(
{
accountIndex: currentIndex,
totalAccounts: accounts.length,
accountId: accounts[nextIdx].accountId,
},
`Codex rotated to account #${currentIndex + 1}/${accounts.length}`,
);
saveCodexState();
return true;
}
logger.warn('All Codex accounts are rate-limited');
@@ -325,15 +277,10 @@ export function rotateCodexToken(errorMessage?: string): boolean {
*/
export function advanceCodexAccount(): void {
if (accounts.length <= 1) return;
const now = Date.now();
for (let i = 1; i < accounts.length; i++) {
const idx = (currentIndex + i) % accounts.length;
const acct = accounts[idx];
if (!acct.rateLimitedUntil || acct.rateLimitedUntil <= now) {
currentIndex = idx;
saveCodexState();
return;
}
const nextIdx = findNextAvailable(accounts, currentIndex);
if (nextIdx !== null) {
currentIndex = nextIdx;
saveCodexState();
}
// All others rate-limited, stay on current
}