Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
30 changes: 27 additions & 3 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 Down Expand Up @@ -300,9 +302,10 @@ function getExperimentalSelectOptions(
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" };
const lower = raw.toLowerCase();
Expand All @@ -323,6 +326,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 @@ -2568,6 +2576,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
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 @@ -2619,6 +2632,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
27 changes: 25 additions & 2 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { readFileSync, existsSync, promises as fs } from "node:fs";
import { dirname, join } from "node:path";
import type { PluginConfig } from "./types.js";
import { logWarn } from "./logger.js";
import { DEFAULT_PREEMPTIVE_QUOTA_REMAINING_PERCENT_5H } from "./preemptive-quota-scheduler.js";
import { PluginConfigSchema, getValidationErrors } from "./schemas.js";
import { getCodexHomeDir, getCodexMultiAuthDir, getLegacyCodexDir } from "./runtime-paths.js";
import {
Expand Down Expand Up @@ -145,6 +146,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 +157,8 @@ export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
serverErrorCooldownMs: 4_000,
storageBackupEnabled: true,
preemptiveQuotaEnabled: true,
preemptiveQuotaRemainingPercent5h: 5,
preemptiveQuotaRemainingPercent5h:
DEFAULT_PREEMPTIVE_QUOTA_REMAINING_PERCENT_5H,
preemptiveQuotaRemainingPercent7d: 5,
preemptiveQuotaMaxDeferralMs: 2 * 60 * 60_000,
};
Expand Down Expand Up @@ -857,6 +860,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 +1079,8 @@ export function getPreemptiveQuotaRemainingPercent5h(pluginConfig: PluginConfig)
return resolveNumberSetting(
"CODEX_AUTH_PREEMPTIVE_QUOTA_5H_REMAINING_PCT",
pluginConfig.preemptiveQuotaRemainingPercent5h,
5,
// Keep this fallback aligned with PreemptiveQuotaScheduler's shared 5h default.
DEFAULT_PREEMPTIVE_QUOTA_REMAINING_PERCENT_5H,
{ min: 0, max: 100 },
);
}
Expand Down
10 changes: 7 additions & 3 deletions lib/preemptive-quota-scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ export interface QuotaSchedulerOptions {
maxDeferralMs?: number;
}

const DEFAULT_REMAINING_PERCENT_THRESHOLD = 5;
export const DEFAULT_PREEMPTIVE_QUOTA_REMAINING_PERCENT_5H = 5;
// Keep the 7d default separate so it can diverge from
// DEFAULT_PREEMPTIVE_QUOTA_REMAINING_PERCENT_5H without changing callers.
// It currently matches the 5h default.
const DEFAULT_SECONDARY_REMAINING_PERCENT_THRESHOLD = 5;
const DEFAULT_MAX_DEFERRAL_MS = 2 * 60 * 60_000;

/**
Expand Down Expand Up @@ -148,8 +152,8 @@ export class PreemptiveQuotaScheduler {

constructor(options: QuotaSchedulerOptions = {}) {
this.enabled = true;
this.primaryRemainingPercentThreshold = DEFAULT_REMAINING_PERCENT_THRESHOLD;
this.secondaryRemainingPercentThreshold = DEFAULT_REMAINING_PERCENT_THRESHOLD;
this.primaryRemainingPercentThreshold = DEFAULT_PREEMPTIVE_QUOTA_REMAINING_PERCENT_5H;
this.secondaryRemainingPercentThreshold = DEFAULT_SECONDARY_REMAINING_PERCENT_THRESHOLD;
this.maxDeferralMs = DEFAULT_MAX_DEFERRAL_MS;
this.configure(options);
}
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
Loading