Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
36e6070
add session resume supervisor for faster codex rotation
ndycode Mar 20, 2026
9823587
speed-up-supervisor-smoke-coverage
ndycode Mar 20, 2026
44e3393
supervisor-prewarm-next-account
ndycode Mar 20, 2026
b5a3d83
batch supervisor selection probes
ndycode Mar 20, 2026
8273a4c
refine supervisor prewarm and probe caching
ndycode Mar 20, 2026
a756824
fix supervisor review regressions
ndycode Mar 20, 2026
2ff7ede
Optimize session binding reuse
ndycode Mar 20, 2026
51d637e
Add supervisor lock regression
ndycode Mar 20, 2026
ed2bddb
Fix supervisor and storage review regressions
ndycode Mar 20, 2026
ecf17d6
Fix remaining supervisor review follow-ups
ndycode Mar 20, 2026
6579f08
Fix supervisor benchmark review comments
ndycode Mar 20, 2026
8a4046f
Merge origin/main into git-plan/05-fast-supervised-resume
ndycode Mar 20, 2026
f3ca99e
Fix post-merge storage cleanup
ndycode Mar 20, 2026
b09f489
fix: harden supervisor review follow-ups
ndycode Mar 20, 2026
9d84cf8
fix: address supervisor review follow-ups
ndycode Mar 20, 2026
77600ad
chore: trim review-only PR bloat
ndycode Mar 20, 2026
7f3c5d4
fix: address remaining review comments
ndycode Mar 20, 2026
c080cd0
fix: address remaining supervisor review comments
ndycode Mar 20, 2026
2c935d5
Fix supervisor command parsing follow-ups
ndycode Mar 20, 2026
1009819
Fix remaining supervisor review follow-ups
ndycode Mar 20, 2026
34db83e
Avoid double sync after supervisor forward
ndycode Mar 20, 2026
71f5b3f
Address supervisor review follow-ups
ndycode Mar 20, 2026
cde63b9
Fix supervisor review follow-ups
ndycode Mar 20, 2026
0c7846d
fix supervisor lock review followups
ndycode Mar 20, 2026
70341cc
fix remaining review followups
ndycode Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions lib/codex-manager/settings-hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ type ThemeConfigAction =

type BackendToggleSettingKey =
| "liveAccountSync"
| "codexCliSessionSupervisor"
| "sessionAffinity"
| "proactiveRefreshGuardian"
| "retryAllAccountsRateLimited"
Expand Down Expand Up @@ -272,6 +273,7 @@ type SettingsHubAction =
| { type: "back" };

type ExperimentalSettingsAction =
| { type: "toggle-session-supervisor" }
| { type: "sync" }
| { type: "backup" }
| { type: "toggle-refresh-guardian" }
Expand All @@ -287,6 +289,11 @@ const BACKEND_TOGGLE_OPTIONS: BackendToggleSettingOption[] = [
label: "Enable Live Sync",
description: "Keep accounts synced when files change in another window.",
},
{
key: "codexCliSessionSupervisor",
label: "Enable Session Resume Supervisor",
description: "Wrap interactive Codex sessions so they can relaunch with resume after rotation.",
},
{
key: "sessionAffinity",
label: "Enable Session Affinity",
Expand Down Expand Up @@ -2530,6 +2537,11 @@ async function promptExperimentalSettings(
while (true) {
const action = await select<ExperimentalSettingsAction>(
[
{
label: `${formatDashboardSettingState(draft.codexCliSessionSupervisor ?? BACKEND_DEFAULTS.codexCliSessionSupervisor ?? false)} ${UI_COPY.settings.experimentalSessionSupervisor}`,
value: { type: "toggle-session-supervisor" },
color: "yellow",
},
Comment on lines +2579 to +2583
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

renumber the experimental hotkeys with this new first item.

adding the supervisor row at lib/codex-manager/settings-hub.ts:2578-2582 shifts the numeric shortcuts, but lib/codex-manager/settings-hub.ts:305-313 still dispatches 1/2/3 as sync/backup/guard. with the updated help in lib/ui/copy.ts:97-101, pressing 1 now runs sync instead of toggling the supervisor, and test/settings-hub-utils.test.ts:754-791 still locks in the stale mapping.

possible fix
 function mapExperimentalMenuHotkey(
 	raw: string,
 ): ExperimentalSettingsAction | undefined {
-	if (raw === "1") return { type: "sync" };
-	if (raw === "2") return { type: "backup" };
-	if (raw === "3") return { type: "toggle-refresh-guardian" };
+	if (raw === "1") return { type: "toggle-session-supervisor" };
+	if (raw === "2") return { type: "sync" };
+	if (raw === "3") return { type: "backup" };
+	if (raw === "4") return { type: "toggle-refresh-guardian" };
 	if (raw === "[" || raw === "-") return { type: "decrease-refresh-interval" };
 	if (raw === "]" || raw === "+") return { type: "increase-refresh-interval" };

As per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
label: `${formatDashboardSettingState(draft.codexCliSessionSupervisor ?? BACKEND_DEFAULTS.codexCliSessionSupervisor ?? false)} ${UI_COPY.settings.experimentalSessionSupervisor}`,
value: { type: "toggle-session-supervisor" },
color: "yellow",
},
function mapExperimentalMenuHotkey(
raw: string,
): ExperimentalSettingsAction | undefined {
if (raw === "1") return { type: "toggle-session-supervisor" };
if (raw === "2") return { type: "sync" };
if (raw === "3") return { type: "backup" };
if (raw === "4") return { type: "toggle-refresh-guardian" };
if (raw === "[" || raw === "-") return { type: "decrease-refresh-interval" };
if (raw === "]" || raw === "+") return { type: "increase-refresh-interval" };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/codex-manager/settings-hub.ts` around lines 2578 - 2582, Adding the new
supervisor row shifts numeric hotkeys; update the numeric-to-action mapping so
the new first item (value: { type: "toggle-session-supervisor" }, label built
with formatDashboardSettingState and
UI_COPY.settings.experimentalSessionSupervisor) is bound to key "1" and all
subsequent shortcuts increment by one. Locate the hotkey dispatch table in
settings-hub (the block that currently dispatches 1/2/3 around the previous
305-313 area) and renumber the entries to account for the inserted supervisor
row, update the help copy in lib/ui/copy.ts to reflect the new numbering, and
update the failing test test/settings-hub-utils.test.ts to assert the revised
key-to-action mapping (ensure the supervisor toggle is tested for key "1" and
other actions shifted accordingly).

{
label: UI_COPY.settings.experimentalSync,
value: { type: "sync" },
Expand Down Expand Up @@ -2586,6 +2598,17 @@ async function promptExperimentalSettings(
);
if (!action || action.type === "back") return null;
if (action.type === "save") return draft;
if (action.type === "toggle-session-supervisor") {
draft = {
...draft,
codexCliSessionSupervisor: !(
draft.codexCliSessionSupervisor ??
BACKEND_DEFAULTS.codexCliSessionSupervisor ??
false
),
};
continue;
}
if (action.type === "toggle-refresh-guardian") {
draft = {
...draft,
Expand Down
24 changes: 22 additions & 2 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
liveAccountSync: true,
liveAccountSyncDebounceMs: 250,
liveAccountSyncPollMs: 2_000,
codexCliSessionSupervisor: false,
sessionAffinity: true,
sessionAffinityTtlMs: 20 * 60_000,
sessionAffinityMaxEntries: 512,
Expand All @@ -155,7 +156,7 @@ export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
serverErrorCooldownMs: 4_000,
storageBackupEnabled: true,
preemptiveQuotaEnabled: true,
preemptiveQuotaRemainingPercent5h: 5,
preemptiveQuotaRemainingPercent5h: 10,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
preemptiveQuotaRemainingPercent7d: 5,
preemptiveQuotaMaxDeferralMs: 2 * 60 * 60_000,
};
Expand Down Expand Up @@ -857,6 +858,25 @@ export function getLiveAccountSyncPollMs(pluginConfig: PluginConfig): number {
);
}

/**
* Determines whether the CLI session supervisor wrapper is enabled.
*
* This accessor is synchronous, side-effect free, and safe for concurrent reads.
* It performs no filesystem I/O and does not expose token material.
*
* @param pluginConfig - The plugin configuration object used as the non-environment fallback
* @returns `true` when the session supervisor should wrap interactive Codex sessions
*/
export function getCodexCliSessionSupervisor(
pluginConfig: PluginConfig,
): boolean {
return resolveBooleanSetting(
"CODEX_AUTH_CLI_SESSION_SUPERVISOR",
pluginConfig.codexCliSessionSupervisor,
false,
);
}

/**
* Indicates whether session affinity is enabled.
*
Expand Down Expand Up @@ -1057,7 +1077,7 @@ export function getPreemptiveQuotaRemainingPercent5h(pluginConfig: PluginConfig)
return resolveNumberSetting(
"CODEX_AUTH_PREEMPTIVE_QUOTA_5H_REMAINING_PCT",
pluginConfig.preemptiveQuotaRemainingPercent5h,
5,
10,
{ min: 0, max: 100 },
);
}
Expand Down
24 changes: 24 additions & 0 deletions lib/quota-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,13 @@ export interface ProbeCodexQuotaOptions {
model?: string;
fallbackModels?: readonly string[];
timeoutMs?: number;
signal?: AbortSignal;
}

function createAbortError(message: string): Error {
const error = new Error(message);
error.name = "AbortError";
return error;
}

/**
Expand All @@ -331,6 +338,9 @@ export async function fetchCodexQuotaSnapshot(
let lastError: Error | null = null;

for (const model of models) {
if (options.signal?.aborted) {
throw createAbortError("Quota probe aborted");
}
try {
const instructions = await getCodexInstructions(model);
const probeBody: RequestBody = {
Expand All @@ -356,6 +366,12 @@ export async function fetchCodexQuotaSnapshot(
headers.set("content-type", "application/json");

const controller = new AbortController();
const onAbort = () => controller.abort(options.signal?.reason);
if (options.signal?.aborted) {
controller.abort(options.signal.reason);
} else {
options.signal?.addEventListener("abort", onAbort, { once: true });
}
const timeout = setTimeout(() => controller.abort(), timeoutMs);
let response: Response;
try {
Expand All @@ -367,6 +383,7 @@ export async function fetchCodexQuotaSnapshot(
});
} finally {
clearTimeout(timeout);
options.signal?.removeEventListener("abort", onAbort);
}

const snapshotBase = parseQuotaSnapshotBase(response.headers, response.status);
Expand Down Expand Up @@ -406,9 +423,16 @@ export async function fetchCodexQuotaSnapshot(
}
lastError = new Error("Codex response did not include quota headers");
} catch (error) {
if (options.signal?.aborted) {
throw error instanceof Error ? error : createAbortError("Quota probe aborted");
}
lastError = error instanceof Error ? error : new Error(String(error));
}
}

if (options.signal?.aborted) {
throw createAbortError("Quota probe aborted");
}

throw lastError ?? new Error("Failed to fetch quotas");
}
1 change: 1 addition & 0 deletions lib/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const PluginConfigSchema = z.object({
liveAccountSync: z.boolean().optional(),
liveAccountSyncDebounceMs: z.number().min(50).optional(),
liveAccountSyncPollMs: z.number().min(500).optional(),
codexCliSessionSupervisor: z.boolean().optional(),
sessionAffinity: z.boolean().optional(),
sessionAffinityTtlMs: z.number().min(1_000).optional(),
sessionAffinityMaxEntries: z.number().min(8).optional(),
Expand Down
30 changes: 23 additions & 7 deletions lib/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ let storageMutex: Promise<void> = Promise.resolve();
const transactionSnapshotContext = new AsyncLocalStorage<{
snapshot: AccountStorageV3 | null;
active: boolean;
storagePath: string;
}>();

function withStorageLock<T>(fn: () => Promise<T>): Promise<T> {
Expand Down Expand Up @@ -1912,18 +1913,24 @@ export async function withAccountStorageTransaction<T>(
) => Promise<T>,
): Promise<T> {
return withStorageLock(async () => {
const storagePath = getStoragePath();
const state = {
snapshot: await loadAccountsInternal(saveAccountsUnlocked),
active: true,
storagePath,
};
const current = state.snapshot;
const persist = async (storage: AccountStorageV3): Promise<void> => {
await saveAccountsUnlocked(storage);
state.snapshot = storage;
};
return transactionSnapshotContext.run(state, () =>
handler(current, persist),
);
return transactionSnapshotContext.run(state, async () => {
try {
return await handler(current, persist);
} finally {
state.active = false;
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
}

Expand All @@ -1937,9 +1944,11 @@ export async function withAccountAndFlaggedStorageTransaction<T>(
) => Promise<T>,
): Promise<T> {
return withStorageLock(async () => {
const storagePath = getStoragePath();
const state = {
snapshot: await loadAccountsInternal(saveAccountsUnlocked),
active: true,
storagePath,
};
const current = state.snapshot;
const persist = async (
Expand Down Expand Up @@ -1973,9 +1982,13 @@ export async function withAccountAndFlaggedStorageTransaction<T>(
throw error;
}
};
return transactionSnapshotContext.run(state, () =>
handler(current, persist),
);
return transactionSnapshotContext.run(state, async () => {
try {
return await handler(current, persist);
} finally {
state.active = false;
}
});
});
}

Expand Down Expand Up @@ -2308,13 +2321,16 @@ export async function exportAccounts(
beforeCommit?: (resolvedPath: string) => Promise<void> | void,
): Promise<void> {
const resolvedPath = resolvePath(filePath);
const activeStoragePath = getStoragePath();

if (!force && existsSync(resolvedPath)) {
throw new Error(`File already exists: ${resolvedPath}`);
}

const transactionState = transactionSnapshotContext.getStore();
const storage = transactionState?.active
const storage =
transactionState?.active &&
transactionState.storagePath === activeStoragePath
? transactionState.snapshot
: await withAccountStorageTransaction((current) =>
Promise.resolve(current),
Expand Down
1 change: 1 addition & 0 deletions lib/ui/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const UI_COPY = {
experimentalHelpMenu: "Enter Select | Q Back",
experimentalHelpPreview: "A Apply | Q Back",
experimentalHelpStatus: "Enter Select | Q Back",
experimentalSessionSupervisor: "Enable Session Resume Supervisor",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
experimentalSync: "Sync Accounts to oc-chatgpt-multi-auth",
experimentalApplySync: "Apply Sync",
experimentalBackup: "Save Pool Backup",
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,14 @@
"test:model-matrix": "node scripts/test-model-matrix.js",
"test:model-matrix:smoke": "node scripts/test-model-matrix.js --smoke",
"test:model-matrix:report": "node scripts/test-model-matrix.js --smoke --report-json=.tmp/model-matrix-report.json",
"test:session-supervisor:smoke": "vitest run test/codex-supervisor.test.ts test/codex-bin-wrapper.test.ts test/plugin-config.test.ts test/quota-probe.test.ts test/settings-hub-utils.test.ts",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
"clean:repo": "node scripts/repo-hygiene.js clean --mode aggressive",
"clean:repo:check": "node scripts/repo-hygiene.js check",
"bench:edit-formats": "node scripts/benchmark-edit-formats.mjs --preset=codex-core",
"bench:edit-formats:smoke": "node scripts/benchmark-edit-formats.mjs --smoke --preset=codex-core",
"bench:edit-formats:render": "node scripts/benchmark-render-dashboard.mjs",
"bench:session-supervisor": "node scripts/benchmark-session-supervisor.mjs",
"bench:session-supervisor:smoke": "node scripts/benchmark-session-supervisor.mjs --smoke",
"bench:runtime-path": "npm run build && node scripts/benchmark-runtime-path.mjs",
"bench:runtime-path:quick": "node scripts/benchmark-runtime-path.mjs",
"test:coverage": "vitest run --coverage",
Expand Down
Loading