fix: remove Codex API key auth path, add secret redaction and d7 auto-rotation

- Remove OPENAI_API_KEY from .env and Codex child process env to prevent
  API billing when subscription quota is exhausted
- Remove writeCodexApiKeyAuth entirely — Codex now uses OAuth only
- Add 9-pattern secret redaction in formatOutbound() to prevent key leaks
- Fix Codex usage bucket aggregation: use 'codex' bucket only instead of
  max across all buckets (bengalfox = Codex Spark, not needed)
- Add d7≥100% auto-rotation in updateCodexAccountUsage to skip exhausted
  accounts and prevent API billing fallback
- Add findNextCodexAvailable that checks both rate-limits and d7 usage
- Clean stale apikey auth.json files from all session directories
- Update tests: OAuth-only auth, d7 auto-skip (3 new tests), dashboard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eyejoker
2026-03-25 10:39:42 +09:00
parent 094278b08e
commit 00ffde7623
11 changed files with 818 additions and 192 deletions

View File

@@ -23,6 +23,8 @@ const MAX_RETRIES = 5;
const BASE_RETRY_MS = 5000;
const MAX_CONCURRENT_TASKS =
MAX_CONCURRENT_AGENTS > 1 ? MAX_CONCURRENT_AGENTS - 1 : 1;
const POST_CLOSE_SIGTERM_DELAY_MS = 60_000;
const POST_CLOSE_SIGKILL_DELAY_MS = 75_000;
interface GroupState {
active: boolean;
@@ -38,6 +40,8 @@ interface GroupState {
retryCount: number;
retryTimer: ReturnType<typeof setTimeout> | null;
retryScheduledAt: number | null;
postCloseTermTimer: ReturnType<typeof setTimeout> | null;
postCloseKillTimer: ReturnType<typeof setTimeout> | null;
startedAt: number | null;
}
@@ -78,6 +82,8 @@ export class GroupQueue {
retryCount: 0,
retryTimer: null,
retryScheduledAt: null,
postCloseTermTimer: null,
postCloseKillTimer: null,
startedAt: null,
};
this.groups.set(groupJid, state);
@@ -293,6 +299,91 @@ export class GroupQueue {
}
}
private clearPostCloseTimers(state: GroupState): void {
if (state.postCloseTermTimer) {
clearTimeout(state.postCloseTermTimer);
state.postCloseTermTimer = null;
}
if (state.postCloseKillTimer) {
clearTimeout(state.postCloseKillTimer);
state.postCloseKillTimer = null;
}
}
private schedulePostCloseTermination(
groupJid: string,
state: GroupState,
runId: string | null,
reason: string,
): void {
const proc = state.process;
if (!proc || !runId || state.isTaskProcess) {
return;
}
const processName = state.processName;
const isSameActiveProcess = () =>
state.process === proc &&
state.currentRunId === runId &&
this.isProcessAlive(proc);
this.clearPostCloseTimers(state);
state.postCloseTermTimer = setTimeout(() => {
state.postCloseTermTimer = null;
if (!isSameActiveProcess()) {
return;
}
logger.warn(
{
groupJid,
runId,
processName,
reason,
delayMs: POST_CLOSE_SIGTERM_DELAY_MS,
},
'Force-terminating lingering agent after stdin close request',
);
try {
proc.kill('SIGTERM');
} catch (err) {
logger.warn(
{ groupJid, runId, processName, err },
'Failed to SIGTERM lingering agent after stdin close request',
);
}
}, POST_CLOSE_SIGTERM_DELAY_MS);
state.postCloseKillTimer = setTimeout(() => {
state.postCloseKillTimer = null;
if (!isSameActiveProcess()) {
return;
}
logger.error(
{
groupJid,
runId,
processName,
reason,
delayMs: POST_CLOSE_SIGKILL_DELAY_MS,
},
'Force-killing stubborn agent after stdin close request',
);
try {
proc.kill('SIGKILL');
} catch (err) {
logger.warn(
{ groupJid, runId, processName, err },
'Failed to SIGKILL stubborn agent after stdin close request',
);
}
}, POST_CLOSE_SIGKILL_DELAY_MS);
}
/**
* Signal the active agent process to wind down by writing a close sentinel.
*/
@@ -327,6 +418,15 @@ export class GroupQueue {
'Failed to signal active agent to close stdin',
);
}
if (metadata?.reason === 'output-delivered-close') {
this.schedulePostCloseTermination(
groupJid,
state,
metadata.runId ?? state.currentRunId,
metadata.reason,
);
}
}
private async runForGroup(
@@ -371,6 +471,7 @@ export class GroupQueue {
);
this.scheduleRetry(groupJid, state, runId);
} finally {
this.clearPostCloseTimers(state);
const durationMs = state.startedAt ? Date.now() - state.startedAt : null;
logger.info(
{
@@ -416,6 +517,7 @@ export class GroupQueue {
} catch (err) {
logger.error({ groupJid, taskId: task.id, err }, 'Error running task');
} finally {
this.clearPostCloseTimers(state);
state.active = false;
state.isTaskProcess = false;
state.runningTaskId = null;
@@ -621,6 +723,7 @@ export class GroupQueue {
}> = [];
for (const [groupJid, state] of this.groups) {
this.clearPostCloseTimers(state);
if (state.retryTimer) {
clearTimeout(state.retryTimer);
state.retryTimer = null;