@bitkyc08/opencodex 2.52.0 → 2.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-BBOZWGB6.css +1 -0
- package/gui/dist/assets/index-BlO4Yl6q.js +128 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/Cargo.lock +130 -0
- package/native/remote-workspace-helper/Cargo.toml +24 -0
- package/native/remote-workspace-helper/src/main.rs +49 -0
- package/native/remote-workspace-helper/src/protocol.rs +246 -0
- package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
- package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
- package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
- package/package.json +6 -1
- package/src/adapters/anthropic-image-normalize.ts +30 -2
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +8 -2
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +93 -0
- package/src/adapters/cursor.ts +104 -73
- package/src/adapters/devin/cloud-direct/chat.ts +312 -23
- package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
- package/src/adapters/devin/live-models.ts +70 -3
- package/src/adapters/devin.ts +281 -21
- package/src/adapters/google-wire-compiler.ts +14 -6
- package/src/adapters/google.ts +22 -8
- package/src/adapters/kiro/adapter.ts +316 -0
- package/src/adapters/kiro/conversation.ts +136 -0
- package/src/adapters/kiro/payload.ts +432 -0
- package/src/adapters/kiro/reasoning.ts +56 -0
- package/src/adapters/kiro/stream.ts +1153 -0
- package/src/adapters/kiro/usage.ts +223 -0
- package/src/adapters/kiro/wire.ts +76 -0
- package/src/adapters/kiro.ts +8 -2319
- package/src/adapters/mimo-free.ts +1 -1
- package/src/adapters/openai-chat-images.ts +101 -0
- package/src/adapters/openai-chat.ts +201 -181
- package/src/adapters/openai-responses.ts +92 -224
- package/src/adapters/registry.ts +0 -7
- package/src/adapters/run-turn-queue.ts +13 -6
- package/src/bridge.ts +14 -15
- package/src/chat/inbound.ts +29 -4
- package/src/chat/outbound.ts +145 -107
- package/src/claude/desktop-profile.ts +4 -6
- package/src/cli/account-api.ts +14 -0
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account-history.ts +60 -0
- package/src/cli/account-main.ts +80 -0
- package/src/cli/account.ts +11 -3
- package/src/cli/capabilities.ts +113 -0
- package/src/cli/catalog.ts +109 -0
- package/src/cli/dispatch.ts +9 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/observe.ts +28 -1
- package/src/cli/opencode.ts +42 -8
- package/src/cli/provider-runtime.ts +11 -1
- package/src/cli/provider.ts +22 -2
- package/src/cli/registry.ts +21 -0
- package/src/cli/remote-workspace.ts +154 -0
- package/src/cli/status.ts +39 -7
- package/src/cli/usage-report.ts +14 -2
- package/src/client/hub-client.ts +34 -0
- package/src/client/hub-state.ts +9 -1
- package/src/codex/account-store.ts +78 -0
- package/src/codex/auth-api.ts +81 -54
- package/src/codex/auth-context.ts +45 -16
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/metadata.ts +3 -6
- package/src/codex/catalog/native-models.ts +4 -4
- package/src/codex/catalog/parsing.ts +2 -20
- package/src/codex/catalog/provider-fetch.ts +10 -1
- package/src/codex/catalog/remote.ts +233 -0
- package/src/codex/catalog/sync.ts +403 -35
- package/src/codex/convergence.ts +1 -1
- package/src/codex/history-manifest.ts +36 -0
- package/src/codex/history-provider.ts +32 -5
- package/src/codex/inject.ts +9 -0
- package/src/codex/main-account.ts +113 -0
- package/src/codex/main-device-reauth-api.ts +89 -0
- package/src/codex/main-device-reauth.ts +217 -0
- package/src/codex/native-residue.ts +9 -2
- package/src/codex/quota-auto-refresh.ts +3 -2
- package/src/codex/quota-capacity.ts +98 -0
- package/src/codex/quota-history.ts +160 -0
- package/src/codex/quota-types.ts +8 -0
- package/src/codex/quota.ts +118 -91
- package/src/codex/refresh.ts +2 -1
- package/src/codex/routing.ts +90 -17
- package/src/codex/sync.ts +33 -4
- package/src/combos/request.ts +19 -1
- package/src/config/multi-agent-surface.ts +61 -0
- package/src/config/provider-validation.ts +176 -0
- package/src/config.ts +213 -11
- package/src/generated/compatibility-version.json +436 -168
- package/src/images/loop.ts +119 -36
- package/src/lib/admission.ts +12 -6
- package/src/lib/redact.ts +7 -0
- package/src/lib/translator-budget.ts +4 -3
- package/src/lib/windows-atomic-replace.ts +1 -0
- package/src/lib/windows-elevation.ts +1 -1
- package/src/oauth/chatgpt-device.ts +62 -5
- package/src/oauth/devin/cli-import.ts +130 -0
- package/src/oauth/devin.ts +63 -8
- package/src/oauth/index.ts +29 -14
- package/src/oauth/kiro.ts +18 -6
- package/src/oauth/login-cli.ts +9 -1
- package/src/oauth/meta-muse-device.ts +464 -0
- package/src/oauth/meta-muse.ts +123 -32
- package/src/oauth/pool-kernel.ts +9 -0
- package/src/oauth/pool-settings-capability.ts +2 -2
- package/src/oauth/store.ts +57 -0
- package/src/oauth/types.ts +31 -0
- package/src/providers/derive.ts +13 -3
- package/src/providers/devin-cli-authmode-migration.ts +57 -35
- package/src/providers/devin-provider-merge-migration.ts +240 -0
- package/src/providers/muse-key-quota.ts +117 -0
- package/src/providers/muse-subscription-usage.ts +14 -2
- package/src/providers/openai-sidecar.ts +25 -3
- package/src/providers/opencode-zen-rate-limit.ts +58 -0
- package/src/providers/provider-id-rewrite.ts +20 -5
- package/src/providers/quota-types.ts +12 -0
- package/src/providers/quota.ts +143 -102
- package/src/providers/reasoning-metadata.ts +543 -0
- package/src/providers/registry.ts +80 -49
- package/src/reasoning-effort.ts +26 -2
- package/src/remote/hub-usage.ts +32 -0
- package/src/remote-control/index.ts +192 -41
- package/src/remote-control/workspace-activation.ts +9 -0
- package/src/remote-control/workspace-agent-connection.ts +366 -0
- package/src/remote-control/workspace-claude-runtime.ts +243 -0
- package/src/remote-control/workspace-codex-runtime.ts +531 -0
- package/src/remote-control/workspace-codex-sandbox.ts +115 -0
- package/src/remote-control/workspace-command-runner.ts +748 -0
- package/src/remote-control/workspace-coordinator.ts +231 -0
- package/src/remote-control/workspace-device.ts +585 -0
- package/src/remote-control/workspace-executable.ts +43 -0
- package/src/remote-control/workspace-executor.ts +397 -0
- package/src/remote-control/workspace-hub.ts +519 -0
- package/src/remote-control/workspace-pi-runtime.ts +382 -0
- package/src/remote-control/workspace-process.ts +129 -0
- package/src/remote-control/workspace-rpc.ts +304 -0
- package/src/remote-control/workspace-runtime.ts +60 -0
- package/src/remote-control/workspace-secret-store.ts +39 -0
- package/src/remote-control/workspace-sessions.ts +799 -0
- package/src/remote-control/workspace-tool-bridge.ts +192 -0
- package/src/responses/code-mode-helper-compat.ts +22 -3
- package/src/responses/hosted-tool-policy.ts +0 -1
- package/src/responses/muse-tool-name-alias.ts +379 -0
- package/src/responses/plaintext-v2-agent-messages.ts +902 -0
- package/src/router.ts +7 -0
- package/src/routing/compatibility/behavior.ts +0 -1
- package/src/server/audio-client.ts +64 -0
- package/src/server/audio-dictation.ts +91 -0
- package/src/server/audio-live.ts +185 -0
- package/src/server/audio-transcriptions.ts +183 -0
- package/src/server/audio-upstream.ts +153 -0
- package/src/server/auth-cors.ts +61 -2
- package/src/server/chat-completions.ts +1 -1
- package/src/server/chat-native-sse.ts +92 -48
- package/src/server/chat-native.ts +37 -15
- package/src/server/hub-usage.ts +57 -0
- package/src/server/images.ts +4 -0
- package/src/server/index.ts +722 -57
- package/src/server/lifecycle.ts +5 -6
- package/src/server/live-call-bindings.ts +60 -0
- package/src/server/live.ts +12 -1
- package/src/server/management/agent-settings-routes.ts +25 -4
- package/src/server/management/api-access.ts +37 -0
- package/src/server/management/api-key-usage.ts +7 -2
- package/src/server/management/config-routes.ts +1 -18
- package/src/server/management/context.ts +15 -0
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/oauth-account-routes.ts +39 -12
- package/src/server/management/provider-routes.ts +125 -2
- package/src/server/management/remote-workspace-routes.ts +140 -0
- package/src/server/management/route-registry.ts +15 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -15
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management-api.ts +23 -0
- package/src/server/ports.ts +17 -0
- package/src/server/relay-eager.ts +4 -1
- package/src/server/relay.ts +70 -10
- package/src/server/request-decompress.ts +6 -3
- package/src/server/responses/agent-task-recovery.ts +25 -32
- package/src/server/responses/codex-auth-error.ts +11 -0
- package/src/server/responses/codex-ws-exchange.ts +52 -3
- package/src/server/responses/codex-ws-wire.ts +55 -0
- package/src/server/responses/compact.ts +9 -1
- package/src/server/responses/core.ts +337 -73
- package/src/server/responses/encrypted-payload.ts +45 -2
- package/src/server/responses/ws-upstream.ts +4 -1
- package/src/server/responses-self-named-namespace-scrub.ts +1 -3
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/search.ts +3 -0
- package/src/server/sse-payload-rewrite.ts +136 -51
- package/src/server/ws-bridge.ts +35 -1
- package/src/service/cli.ts +372 -0
- package/src/service/diagnostics.ts +340 -0
- package/src/service/guards.ts +303 -0
- package/src/service/health.ts +222 -0
- package/src/service/launchd.ts +853 -0
- package/src/service/orchestration.ts +617 -0
- package/src/service/repair.ts +334 -0
- package/src/service/state.ts +363 -0
- package/src/service/systemd.ts +229 -0
- package/src/service/windows-ops.ts +690 -0
- package/src/service/windows-scheduler.ts +769 -0
- package/src/service/windows-taskxml.ts +613 -0
- package/src/service.ts +22 -5550
- package/src/storage/cleanup/db.ts +258 -0
- package/src/storage/cleanup/execute.ts +358 -0
- package/src/storage/cleanup/paths.ts +189 -0
- package/src/storage/cleanup/pending.ts +140 -0
- package/src/storage/cleanup/preview.ts +292 -0
- package/src/storage/cleanup/reconcile.ts +347 -0
- package/src/storage/cleanup/restore.ts +932 -0
- package/src/storage/cleanup/satellite.ts +474 -0
- package/src/storage/cleanup/staging.ts +129 -0
- package/src/storage/cleanup/types.ts +98 -0
- package/src/storage/cleanup.ts +49 -3127
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +13 -12
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +17 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +127 -0
- package/src/usage/log.ts +58 -1
- package/src/vision/eligibility.ts +13 -2
- package/src/web-search/loop.ts +56 -3
- package/gui/dist/assets/index-CWXut3rG.js +0 -115
- package/gui/dist/assets/index-EdoPnm9_.css +0 -1
- package/src/adapters/devin-cli/acp.ts +0 -204
- package/src/adapters/devin-cli/adapter.ts +0 -345
- package/src/adapters/devin-cli/binary.ts +0 -69
- package/src/adapters/devin-cli/models.ts +0 -57
- package/src/oauth/devin-cli.ts +0 -149
- package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
|
@@ -8,6 +8,7 @@ import { atomicWriteFile, getConfigDir } from "../config";
|
|
|
8
8
|
import {
|
|
9
9
|
CODEX_HISTORY_RESUMABLE_SOURCES,
|
|
10
10
|
codexHistoryBackupId,
|
|
11
|
+
legacyCodexHistoryBackupId,
|
|
11
12
|
sameCodexHistoryPath,
|
|
12
13
|
validateCodexHistoryBackupManifest,
|
|
13
14
|
type CodexHistoryBackupEntry,
|
|
@@ -32,6 +33,32 @@ export function historyBackupPathFor(stateDbPath: string): string {
|
|
|
32
33
|
return join(getConfigDir(), `codex-history-backup-${codexHistoryBackupId(stateDbPath)}.json`);
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Manifest name a database path spelled with the Win32 extended-length prefix
|
|
38
|
+
* (`\\?\C:\...`) received before that prefix was normalized out of the identity
|
|
39
|
+
* (#4442). Identical to historyBackupPathFor for every other spelling.
|
|
40
|
+
*/
|
|
41
|
+
export function legacyHistoryBackupPathFor(stateDbPath: string): string {
|
|
42
|
+
return join(getConfigDir(), `codex-history-backup-${legacyCodexHistoryBackupId(stateDbPath)}.json`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The manifest that actually shadows one state database. Canonical name first; when
|
|
47
|
+
* only a pre-#4442 extended-length name exists, that manifest still shadows the
|
|
48
|
+
* database rather than reading as absent. When BOTH names exist the canonical
|
|
49
|
+
* manifest wins and the legacy file is left untouched — a conflict is resolved by
|
|
50
|
+
* keeping both, never by silently replacing one.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveExistingHistoryBackupPath(
|
|
53
|
+
stateDbPath: string,
|
|
54
|
+
exists: (path: string) => boolean = existsSync,
|
|
55
|
+
): string {
|
|
56
|
+
const canonical = historyBackupPathFor(stateDbPath);
|
|
57
|
+
if (exists(canonical)) return canonical;
|
|
58
|
+
const legacy = legacyHistoryBackupPathFor(stateDbPath);
|
|
59
|
+
return legacy !== canonical && exists(legacy) ? legacy : canonical;
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
/**
|
|
36
63
|
* Open the live `state_5.sqlite` the way the Codex app expects a *secondary* writer to behave:
|
|
37
64
|
* wait on the WAL/file lock instead of failing instantly, so we never race the app's own
|
|
@@ -318,7 +345,7 @@ export function preflightCodexHistoryInjection(
|
|
|
318
345
|
// A partially restored row may already be native while its manifest still
|
|
319
346
|
// owns work. Match the restore worker's target set before removing routing.
|
|
320
347
|
const restoreEntries = providerTableMode ? []
|
|
321
|
-
: Object.values(readBackup(
|
|
348
|
+
: Object.values(readBackup(resolveExistingHistoryBackupPath(resolvedPath), resolvedPath).manifest.entries);
|
|
322
349
|
if (!existsSync(resolvedPath)) {
|
|
323
350
|
return restoreEntries.length > 0 ? "history_state_database_missing" : null;
|
|
324
351
|
}
|
|
@@ -1388,7 +1415,7 @@ function openaiRestoreIsNoop(stateDbPath: string, backupPath: string): boolean {
|
|
|
1388
1415
|
export function syncCodexHistoryProvider(
|
|
1389
1416
|
provider: CodexHistoryProvider,
|
|
1390
1417
|
stateDbPath = resolveCodexStateDbPath(),
|
|
1391
|
-
backupPath =
|
|
1418
|
+
backupPath = resolveExistingHistoryBackupPath(stateDbPath),
|
|
1392
1419
|
opts: { skipWhenProvablyNoop?: boolean } = {},
|
|
1393
1420
|
): CodexHistorySyncResult {
|
|
1394
1421
|
// Opt-in steady-state gate (Design B loopback callers only): default semantics of
|
|
@@ -1714,7 +1741,7 @@ export function restoreLegacyOpenaiHistory(stateDbPath = resolveCodexStateDbPath
|
|
|
1714
1741
|
*/
|
|
1715
1742
|
export function migrateHistoryToOpenai(
|
|
1716
1743
|
stateDbPath = resolveCodexStateDbPath(),
|
|
1717
|
-
backupPath =
|
|
1744
|
+
backupPath = resolveExistingHistoryBackupPath(stateDbPath),
|
|
1718
1745
|
opts: { attempts?: number; delayMs?: number; sleepFn?: (ms: number) => void } = {},
|
|
1719
1746
|
): CodexHistorySyncResult {
|
|
1720
1747
|
// Steady-state gate: this migration is Design-B-specific (inject + guardian callers),
|
|
@@ -1747,7 +1774,7 @@ export function snapshotCodexHistoryNoop(
|
|
|
1747
1774
|
const stateDbPresent = existsSync(stateDbPath);
|
|
1748
1775
|
const backupPresent = existsSync(backupPath);
|
|
1749
1776
|
const base = { canonicalStateDbPath, stateDbPresent, canonicalBackupPath, backupPresent };
|
|
1750
|
-
if (!sameCodexHistoryPath(backupPath,
|
|
1777
|
+
if (!sameCodexHistoryPath(backupPath, resolveExistingHistoryBackupPath(stateDbPath))) {
|
|
1751
1778
|
return { kind: "unknown", pendingRows: null, backupEntries: null, ...base, reason: "backup-path" };
|
|
1752
1779
|
}
|
|
1753
1780
|
const backup = inspectBackupForNoop(backupPath, stateDbPath);
|
|
@@ -1831,7 +1858,7 @@ export interface PendingHistoryCount {
|
|
|
1831
1858
|
*/
|
|
1832
1859
|
export function countPendingOpencodexHistory(
|
|
1833
1860
|
stateDbPath = resolveCodexStateDbPath(),
|
|
1834
|
-
backupPath =
|
|
1861
|
+
backupPath = resolveExistingHistoryBackupPath(stateDbPath),
|
|
1835
1862
|
opts: { validateRestoreTargets?: boolean } = {},
|
|
1836
1863
|
): PendingHistoryCount {
|
|
1837
1864
|
const backup = readBackupStrict(backupPath, stateDbPath);
|
package/src/codex/inject.ts
CHANGED
|
@@ -895,6 +895,8 @@ export function chooseCatalogPathForInjection(
|
|
|
895
895
|
export interface CodexInjectResult {
|
|
896
896
|
success: boolean;
|
|
897
897
|
message: string;
|
|
898
|
+
/** Structured read-only history preflight refusal; never parsed from display text. */
|
|
899
|
+
historyPreflightFailureReason?: string;
|
|
898
900
|
status?: "skipped";
|
|
899
901
|
/** `hub-gated` is the hub-role gate (#4236), distinct from the user's own OFF switch. */
|
|
900
902
|
skippedReason?: "desired_disabled" | "desired_enabled" | "hub-gated";
|
|
@@ -1192,6 +1194,7 @@ async function injectCodexConfigImpl(
|
|
|
1192
1194
|
if (historyPreflightError) {
|
|
1193
1195
|
return {
|
|
1194
1196
|
success: false,
|
|
1197
|
+
historyPreflightFailureReason: historyPreflightError,
|
|
1195
1198
|
message: `Codex config injection refused: ${historyPreflightError}. `
|
|
1196
1199
|
+ "Existing provider definitions and conversation files were preserved. "
|
|
1197
1200
|
+ "Paginated history requires native-writer coordination; do not run legacy recovery or retry this transition blindly.",
|
|
@@ -1949,6 +1952,12 @@ function restoreCodexConfigInlineImpl(kind: string): CodexRestoreConfigResult {
|
|
|
1949
1952
|
const restored = journal.configRestored
|
|
1950
1953
|
? { success: true, message: "Codex config restored from opencodex journal." }
|
|
1951
1954
|
: removeCodexConfig({ preserveProfile: journal.profileRestored || journal.profileChanged });
|
|
1955
|
+
if (restored.success) {
|
|
1956
|
+
// A successful journal/fallback write can race native history migration too.
|
|
1957
|
+
// Refuse here while preimage compensation and the remove transaction can roll back.
|
|
1958
|
+
const finalHistoryError = preflightCodexHistoryInjection(false, false);
|
|
1959
|
+
if (finalHistoryError) return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${finalHistoryError}.` };
|
|
1960
|
+
}
|
|
1952
1961
|
return restored.success
|
|
1953
1962
|
? {
|
|
1954
1963
|
state: "ok",
|
|
@@ -21,6 +21,7 @@ import { clearAccountNeedsReauth } from "./account-runtime-state";
|
|
|
21
21
|
import { advanceCodexCredentialMutationEpoch } from "./credential-mutation-epoch";
|
|
22
22
|
import { withNativeMainExclusiveClaim } from "./native-main-claim";
|
|
23
23
|
import { resolveNativeProfileContext } from "./native-profile-store";
|
|
24
|
+
import { isNativeMainTrafficBlocked } from "./native-profile-startup";
|
|
24
25
|
|
|
25
26
|
export { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
26
27
|
|
|
@@ -218,6 +219,118 @@ export function setMainAuthJsonBeforeRenameHookForTests(hook: (() => void) | nul
|
|
|
218
219
|
beforeMainAuthJsonRenameForTests = hook;
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
/** Complete token set a native device reauth commits into the main slot (#3898). */
|
|
223
|
+
export interface NativeMainReauthTokens {
|
|
224
|
+
accessToken: string;
|
|
225
|
+
refreshToken: string;
|
|
226
|
+
idToken: string;
|
|
227
|
+
chatgptAccountId: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export class NativeMainReauthUnavailableError extends Error {
|
|
231
|
+
constructor(message = "Native main credential cannot be reauthenticated in this state") {
|
|
232
|
+
super(message);
|
|
233
|
+
this.name = "NativeMainReauthUnavailableError";
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export class NativeMainReauthIdentityMismatchError extends Error {
|
|
238
|
+
constructor() {
|
|
239
|
+
super("Device login completed for a different ChatGPT account than the native main identity");
|
|
240
|
+
this.name = "NativeMainReauthIdentityMismatchError";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The reauth twin of persistRefreshedMainAuthJson (#3898). That function
|
|
246
|
+
* spreads expected.tokens and never writes id_token, which would keep the
|
|
247
|
+
* OLD identity token beside the new grant; this sibling sets all four
|
|
248
|
+
* credential fields together and overwrites any prior id_token. Everything
|
|
249
|
+
* else — allowed root metadata, the pre-rename snapshot guards, the
|
|
250
|
+
* mutation epoch — follows the refresh path exactly.
|
|
251
|
+
*/
|
|
252
|
+
function persistNativeMainReauthTokens(
|
|
253
|
+
expected: MainAuthJsonCredential,
|
|
254
|
+
tokens: NativeMainReauthTokens,
|
|
255
|
+
): void {
|
|
256
|
+
assertNotRealCodexHomeUnderTest(resolveCodexHomeDir());
|
|
257
|
+
const nextTokens = {
|
|
258
|
+
...expected.tokens,
|
|
259
|
+
access_token: tokens.accessToken,
|
|
260
|
+
refresh_token: tokens.refreshToken,
|
|
261
|
+
id_token: tokens.idToken,
|
|
262
|
+
account_id: tokens.chatgptAccountId,
|
|
263
|
+
};
|
|
264
|
+
atomicWriteFile(
|
|
265
|
+
expected.path,
|
|
266
|
+
JSON.stringify({ ...expected.root, tokens: nextTokens }, null, 2) + "\n",
|
|
267
|
+
undefined,
|
|
268
|
+
{
|
|
269
|
+
beforeRename: () => {
|
|
270
|
+
assertMainAuthJsonSnapshotUnchanged(expected);
|
|
271
|
+
const hook = beforeMainAuthJsonRenameForTests;
|
|
272
|
+
beforeMainAuthJsonRenameForTests = null;
|
|
273
|
+
hook?.();
|
|
274
|
+
},
|
|
275
|
+
validateBeforeRename: () => assertMainAuthJsonSnapshotUnchanged(expected),
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
advanceCodexCredentialMutationEpoch();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Prepare a same-identity reauth of the native __main__ slot (#3898).
|
|
283
|
+
*
|
|
284
|
+
* The existing credential snapshot is captured NOW and held only inside the
|
|
285
|
+
* closure — callers (the device-reauth service) never see the expected
|
|
286
|
+
* account id, so a flow cannot be steered toward a different identity. No
|
|
287
|
+
* claim is held while the human completes the device page. The returned
|
|
288
|
+
* commit, called once the device grant exists:
|
|
289
|
+
*
|
|
290
|
+
* 1. requires the SAME chatgpt account identity as the snapshot;
|
|
291
|
+
* 2. acquires the owner-independent exclusive claim (native-main-claim) —
|
|
292
|
+
* deliberately NOT assertNativeMainOwner, which a headless hub cannot
|
|
293
|
+
* satisfy;
|
|
294
|
+
* 3. re-verifies the snapshot (path + hash + dev/ino) inside the claim;
|
|
295
|
+
* 4. writes access/refresh/id token + account_id atomically and clears the
|
|
296
|
+
* main account's reauth quarantine for the new credential generation.
|
|
297
|
+
*/
|
|
298
|
+
export function beginNativeMainReauth(): {
|
|
299
|
+
commit: (tokens: NativeMainReauthTokens) => Promise<{ chatgptAccountId: string }>;
|
|
300
|
+
} {
|
|
301
|
+
const expected = readMainAuthJsonCredential();
|
|
302
|
+
if (!expected || !expected.chatgptAccountId) {
|
|
303
|
+
throw new NativeMainReauthUnavailableError(
|
|
304
|
+
"No native main credential exists to reauthenticate; enrollment is the native profile workflow",
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
async commit(tokens: NativeMainReauthTokens): Promise<{ chatgptAccountId: string }> {
|
|
309
|
+
if (!tokens.accessToken || !tokens.refreshToken || !tokens.idToken) {
|
|
310
|
+
throw new NativeMainReauthUnavailableError("Device grant did not produce a complete token set");
|
|
311
|
+
}
|
|
312
|
+
if (tokens.chatgptAccountId !== expected.chatgptAccountId) {
|
|
313
|
+
throw new NativeMainReauthIdentityMismatchError();
|
|
314
|
+
}
|
|
315
|
+
return withNativeMainExclusiveClaim(resolveNativeProfileContext(), async () => {
|
|
316
|
+
// Recovery/admission recheck (080): a recovery-blocked or not-ready
|
|
317
|
+
// home fails native_main_unavailable rather than rewriting auth.json
|
|
318
|
+
// underneath the gate. The claim waits bounded like the refresh path
|
|
319
|
+
// (30s) so a busy claim is not an instant refusal.
|
|
320
|
+
if (isNativeMainTrafficBlocked()) {
|
|
321
|
+
throw new NativeMainReauthUnavailableError(
|
|
322
|
+
"Native main traffic is blocked by startup or recovery state",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
assertMainAuthJsonSnapshotUnchanged(expected);
|
|
326
|
+
persistNativeMainReauthTokens(expected, tokens);
|
|
327
|
+
clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
328
|
+
return { chatgptAccountId: tokens.chatgptAccountId };
|
|
329
|
+
}, { waitMs: 30_000 });
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
221
334
|
async function resolveMainAccountToken(
|
|
222
335
|
dependencies: NativeMainRefreshDependencies = {},
|
|
223
336
|
rejectedAccessToken?: string,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { jsonResponse } from "../server/auth-cors";
|
|
3
|
+
import {
|
|
4
|
+
cancelMainDeviceReauth,
|
|
5
|
+
getMainDeviceReauthStatus,
|
|
6
|
+
MainDeviceReauthFlowBusyError,
|
|
7
|
+
startMainDeviceReauth,
|
|
8
|
+
} from "./main-device-reauth";
|
|
9
|
+
import { NativeMainReauthUnavailableError } from "./main-account";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Dedicated native-main device reauth route (#3898).
|
|
13
|
+
*
|
|
14
|
+
* `/api/codex-auth/login` stays pool-only and keeps rejecting __main__;
|
|
15
|
+
* this namespace is the only device-reauth surface for the native main slot.
|
|
16
|
+
* DTOs carry flowId/status/verificationUrl/deviceCode and safe failure codes
|
|
17
|
+
* — never tokens, emails, or raw account ids. The route is registered in
|
|
18
|
+
* management-api ahead of the generic /api/codex-auth/* dispatch, so the
|
|
19
|
+
* existing management origin/auth/session controls wrap it unchanged.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const ROUTE = "/api/codex-auth/main/reauth-device";
|
|
23
|
+
|
|
24
|
+
function errorResponse(
|
|
25
|
+
req: Request,
|
|
26
|
+
config: OcxConfig,
|
|
27
|
+
message: string,
|
|
28
|
+
code: string,
|
|
29
|
+
status: number,
|
|
30
|
+
): Response {
|
|
31
|
+
return jsonResponse({ error: message, code }, status, req, config);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function flowIdFromQuery(url: URL): string | null {
|
|
35
|
+
for (const key of url.searchParams.keys()) {
|
|
36
|
+
if (key !== "flowId") return null;
|
|
37
|
+
}
|
|
38
|
+
const flowId = url.searchParams.get("flowId");
|
|
39
|
+
return flowId && flowId.trim() ? flowId : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function handleMainDeviceReauthAPI(
|
|
43
|
+
req: Request,
|
|
44
|
+
url: URL,
|
|
45
|
+
config: OcxConfig,
|
|
46
|
+
): Promise<Response | null> {
|
|
47
|
+
if (url.pathname !== ROUTE) return null;
|
|
48
|
+
|
|
49
|
+
if (req.method === "POST") {
|
|
50
|
+
// Strict body: no request keys exist for start; anything supplied is an error.
|
|
51
|
+
const text = await req.text();
|
|
52
|
+
if (text.trim()) {
|
|
53
|
+
return errorResponse(req, config, "The reauth-device start takes no request body", "invalid_request", 400);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return jsonResponse(startMainDeviceReauth(), 200, req, config);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof MainDeviceReauthFlowBusyError) {
|
|
59
|
+
return errorResponse(req, config, error.message, error.code, 409);
|
|
60
|
+
}
|
|
61
|
+
if (error instanceof NativeMainReauthUnavailableError) {
|
|
62
|
+
return errorResponse(req, config, error.message, "native_main_unavailable", 503);
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (req.method === "GET") {
|
|
69
|
+
const flowId = flowIdFromQuery(url);
|
|
70
|
+
if (!flowId) {
|
|
71
|
+
return errorResponse(req, config, "An exact flowId query is required", "invalid_request", 400);
|
|
72
|
+
}
|
|
73
|
+
const status = getMainDeviceReauthStatus(flowId);
|
|
74
|
+
if (!status) return errorResponse(req, config, "Unknown or expired reauth flow", "unknown_flow", 404);
|
|
75
|
+
return jsonResponse(status, 200, req, config);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (req.method === "DELETE") {
|
|
79
|
+
const flowId = flowIdFromQuery(url);
|
|
80
|
+
if (!flowId) {
|
|
81
|
+
return errorResponse(req, config, "An exact flowId query is required", "invalid_request", 400);
|
|
82
|
+
}
|
|
83
|
+
const status = cancelMainDeviceReauth(flowId);
|
|
84
|
+
if (!status) return errorResponse(req, config, "Unknown or expired reauth flow", "unknown_flow", 404);
|
|
85
|
+
return jsonResponse(status, 200, req, config);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return errorResponse(req, config, "Method not allowed", "method_not_allowed", 405);
|
|
89
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { loginChatGPTNativeDevice, type NativeDeviceLogin } from "../oauth/chatgpt-device";
|
|
3
|
+
import type { OAuthController } from "../oauth/types";
|
|
4
|
+
import {
|
|
5
|
+
beginNativeMainReauth,
|
|
6
|
+
MainAuthJsonChangedDuringRefreshError,
|
|
7
|
+
NativeMainReauthIdentityMismatchError,
|
|
8
|
+
NativeMainReauthUnavailableError,
|
|
9
|
+
type NativeMainReauthTokens,
|
|
10
|
+
} from "./main-account";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Process-owned native-main device reauth flow (#3898).
|
|
14
|
+
*
|
|
15
|
+
* Exactly one active flow per process, started from the management API or the
|
|
16
|
+
* CLI. The human-facing DTO carries only flowId, status, the verification
|
|
17
|
+
* URL and the device code: tokens, emails, and raw account ids never leave
|
|
18
|
+
* the device/grant layer, and the opaque device_auth_id never leaves
|
|
19
|
+
* chatgpt-device.ts at all. The grant runs on this flow's own
|
|
20
|
+
* AbortController — deliberately NOT through startLoginFlow("chatgpt"),
|
|
21
|
+
* which would overwrite the pool scratch slot and collide with pool logins.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type MainDeviceReauthStatus =
|
|
25
|
+
| { flowId: string; status: "pending"; verificationUrl: string; deviceCode: string }
|
|
26
|
+
| { flowId: string; status: "committing" }
|
|
27
|
+
| { flowId: string; status: "succeeded"; credentialUpdated: true }
|
|
28
|
+
| { flowId: string; status: "cancelled" }
|
|
29
|
+
| {
|
|
30
|
+
flowId: string;
|
|
31
|
+
status: "failed";
|
|
32
|
+
credentialUpdated?: true;
|
|
33
|
+
code:
|
|
34
|
+
| "identity_mismatch"
|
|
35
|
+
| "credential_changed"
|
|
36
|
+
| "native_main_unavailable"
|
|
37
|
+
| "device_authorization_failed"
|
|
38
|
+
| "publication_failed"
|
|
39
|
+
| "reconciliation_failed";
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export class MainDeviceReauthFlowBusyError extends Error {
|
|
43
|
+
readonly code = "flow_in_progress";
|
|
44
|
+
constructor() {
|
|
45
|
+
super("A native main device reauth is already in progress");
|
|
46
|
+
this.name = "MainDeviceReauthFlowBusyError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ActiveFlow {
|
|
51
|
+
flowId: string;
|
|
52
|
+
controller: AbortController;
|
|
53
|
+
status: MainDeviceReauthStatus;
|
|
54
|
+
/** Set once auth.json has been replaced; cancellation can no longer win. */
|
|
55
|
+
published: boolean;
|
|
56
|
+
/** Snapshot-holding commit prepared at start; closure-private identity. */
|
|
57
|
+
prepared?: { commit: (tokens: NativeMainReauthTokens) => Promise<{ chatgptAccountId: string }> };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Bounded terminal retention so status/cancel stay answerable after completion. */
|
|
61
|
+
const TERMINAL_RETENTION_MS = 300_000;
|
|
62
|
+
|
|
63
|
+
let activeFlow: ActiveFlow | null = null;
|
|
64
|
+
const terminalFlows = new Map<string, { status: MainDeviceReauthStatus; expiresAt: number }>();
|
|
65
|
+
|
|
66
|
+
export interface MainDeviceReauthDeps {
|
|
67
|
+
login?: (ctrl: OAuthController) => Promise<NativeDeviceLogin>;
|
|
68
|
+
beginCommit?: () => { commit: (tokens: NativeMainReauthTokens) => Promise<{ chatgptAccountId: string }> };
|
|
69
|
+
flowId?: () => string;
|
|
70
|
+
now?: () => number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isTerminal(status: MainDeviceReauthStatus): boolean {
|
|
74
|
+
return status.status === "succeeded" || status.status === "cancelled" || status.status === "failed";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sweepTerminal(now: number): void {
|
|
78
|
+
for (const [flowId, row] of terminalFlows) {
|
|
79
|
+
if (row.expiresAt <= now) terminalFlows.delete(flowId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function finish(flow: ActiveFlow, status: MainDeviceReauthStatus, now: number): void {
|
|
84
|
+
// Publication beats a racing cancellation: once auth.json was replaced the
|
|
85
|
+
// honest terminal is succeeded, never cancelled (080). Every other terminal
|
|
86
|
+
// is first-write-wins so a superseded or cancelled completion cannot
|
|
87
|
+
// publish a later result.
|
|
88
|
+
if (isTerminal(flow.status)) {
|
|
89
|
+
if (!(flow.published && status.status === "succeeded")) return;
|
|
90
|
+
}
|
|
91
|
+
if (status.status === "succeeded") flow.published = true;
|
|
92
|
+
flow.status = status;
|
|
93
|
+
terminalFlows.set(flow.flowId, { status, expiresAt: now + TERMINAL_RETENTION_MS });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mapFailure(flowId: string, error: unknown): MainDeviceReauthStatus {
|
|
97
|
+
if (error instanceof NativeMainReauthIdentityMismatchError) {
|
|
98
|
+
return { flowId, status: "failed", code: "identity_mismatch" };
|
|
99
|
+
}
|
|
100
|
+
if (error instanceof MainAuthJsonChangedDuringRefreshError) {
|
|
101
|
+
return { flowId, status: "failed", code: "credential_changed" };
|
|
102
|
+
}
|
|
103
|
+
if (error instanceof NativeMainReauthUnavailableError) {
|
|
104
|
+
return { flowId, status: "failed", code: "native_main_unavailable" };
|
|
105
|
+
}
|
|
106
|
+
const code = (error as { code?: unknown } | null)?.code;
|
|
107
|
+
if (code === "NATIVE_MAIN_CLAIM_UNAVAILABLE" || code === "NATIVE_MAIN_OWNER_UNAVAILABLE") {
|
|
108
|
+
return { flowId, status: "failed", code: "native_main_unavailable" };
|
|
109
|
+
}
|
|
110
|
+
const name = (error as { name?: unknown } | null)?.name;
|
|
111
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
112
|
+
return { flowId, status: "failed", code: "device_authorization_failed" };
|
|
113
|
+
}
|
|
114
|
+
if (error instanceof Error && /device authorization/.test(error.message)) {
|
|
115
|
+
return { flowId, status: "failed", code: "device_authorization_failed" };
|
|
116
|
+
}
|
|
117
|
+
return { flowId, status: "failed", code: "publication_failed" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Start the one active flow. Returns the pending status; the URL/code arrive
|
|
122
|
+
* with the usercode response and are visible through the status endpoint.
|
|
123
|
+
*/
|
|
124
|
+
export function startMainDeviceReauth(deps: MainDeviceReauthDeps = {}): MainDeviceReauthStatus {
|
|
125
|
+
const now = (deps.now ?? Date.now)();
|
|
126
|
+
sweepTerminal(now);
|
|
127
|
+
if (activeFlow && !isTerminal(activeFlow.status)) throw new MainDeviceReauthFlowBusyError();
|
|
128
|
+
const flowId = (deps.flowId ?? randomUUID)();
|
|
129
|
+
const flow: ActiveFlow = {
|
|
130
|
+
flowId,
|
|
131
|
+
controller: new AbortController(),
|
|
132
|
+
status: { flowId, status: "pending", verificationUrl: "", deviceCode: "" },
|
|
133
|
+
published: false,
|
|
134
|
+
};
|
|
135
|
+
// Claim the singleflight slot BEFORE preparing: two overlapping starts must
|
|
136
|
+
// not both snapshot and then have one throw flow_in_progress after the
|
|
137
|
+
// other already began polling.
|
|
138
|
+
activeFlow = flow;
|
|
139
|
+
try {
|
|
140
|
+
// Prepare NOW: the existing credential snapshot is captured at start (080),
|
|
141
|
+
// so a hub with no reauthenticatable main credential fails fast with
|
|
142
|
+
// native_main_unavailable instead of after the human completes the page.
|
|
143
|
+
flow.prepared = (deps.beginCommit ?? beginNativeMainReauth)();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
activeFlow = null;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
const login = deps.login ?? loginChatGPTNativeDevice;
|
|
149
|
+
const clock = deps.now ?? Date.now;
|
|
150
|
+
void (async () => {
|
|
151
|
+
try {
|
|
152
|
+
const grant = await login({
|
|
153
|
+
signal: flow.controller.signal,
|
|
154
|
+
onAuth: info => {
|
|
155
|
+
// A superseded or cancelled flow may not publish its URL/code.
|
|
156
|
+
if (activeFlow !== flow || isTerminal(flow.status)) return;
|
|
157
|
+
flow.status = {
|
|
158
|
+
flowId,
|
|
159
|
+
status: "pending",
|
|
160
|
+
verificationUrl: info.url,
|
|
161
|
+
deviceCode: info.deviceCode ?? "",
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
if (flow.controller.signal.aborted) return;
|
|
166
|
+
if (!isTerminal(flow.status)) flow.status = { flowId, status: "committing" };
|
|
167
|
+
// Recheck immediately before the write: a cancel that landed while the
|
|
168
|
+
// grant was resolving must not reach auth.json. Once the commit DOES
|
|
169
|
+
// publish, the terminal is succeeded even if a cancel raced it (080).
|
|
170
|
+
if (activeFlow !== flow || flow.controller.signal.aborted || isTerminal(flow.status)) return;
|
|
171
|
+
await flow.prepared!.commit({
|
|
172
|
+
accessToken: grant.credential.access,
|
|
173
|
+
refreshToken: grant.credential.refresh,
|
|
174
|
+
idToken: grant.idToken,
|
|
175
|
+
chatgptAccountId: grant.credential.accountId!,
|
|
176
|
+
});
|
|
177
|
+
flow.published = true;
|
|
178
|
+
finish(flow, { flowId, status: "succeeded", credentialUpdated: true }, clock());
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (flow.controller.signal.aborted && !flow.published) return;
|
|
181
|
+
finish(flow, mapFailure(flowId, error), clock());
|
|
182
|
+
}
|
|
183
|
+
})();
|
|
184
|
+
return flow.status;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function getMainDeviceReauthStatus(flowId: string, deps: MainDeviceReauthDeps = {}): MainDeviceReauthStatus | null {
|
|
188
|
+
const now = (deps.now ?? Date.now)();
|
|
189
|
+
sweepTerminal(now);
|
|
190
|
+
if (activeFlow?.flowId === flowId) return activeFlow.status;
|
|
191
|
+
return terminalFlows.get(flowId)?.status ?? null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Cancel the flow. Cancellation after publication returns the published
|
|
196
|
+
* terminal (succeeded), never cancelled; a pending/committing flow aborts its
|
|
197
|
+
* grant and settles cancelled.
|
|
198
|
+
*/
|
|
199
|
+
export function cancelMainDeviceReauth(flowId: string, deps: MainDeviceReauthDeps = {}): MainDeviceReauthStatus | null {
|
|
200
|
+
const now = (deps.now ?? Date.now)();
|
|
201
|
+
sweepTerminal(now);
|
|
202
|
+
if (activeFlow?.flowId === flowId && !isTerminal(activeFlow.status)) {
|
|
203
|
+
activeFlow.controller.abort();
|
|
204
|
+
finish(activeFlow, { flowId, status: "cancelled" }, now);
|
|
205
|
+
return activeFlow.status;
|
|
206
|
+
}
|
|
207
|
+
return terminalFlows.get(flowId)?.status ?? null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Test hook: drop all in-memory flow state. Production never calls this. */
|
|
211
|
+
export function resetMainDeviceReauthForTests(): void {
|
|
212
|
+
// Abort first: a reset that only clears the maps leaves a pending grant
|
|
213
|
+
// polling against real timers for up to the 15-minute device TTL.
|
|
214
|
+
activeFlow?.controller.abort();
|
|
215
|
+
activeFlow = null;
|
|
216
|
+
terminalFlows.clear();
|
|
217
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
closeSync,
|
|
3
|
+
existsSync,
|
|
3
4
|
fstatSync,
|
|
4
5
|
lstatSync,
|
|
5
6
|
openSync,
|
|
@@ -16,7 +17,7 @@ import { Database } from "bun:sqlite";
|
|
|
16
17
|
|
|
17
18
|
import { getConfigDir } from "../config";
|
|
18
19
|
import { catalogHasRoutedEntries, parseCatalogJson } from "./catalog/parsing";
|
|
19
|
-
import { codexHistoryBackupId, validateCodexHistoryBackupManifest } from "./history-manifest";
|
|
20
|
+
import { codexHistoryBackupId, legacyCodexHistoryBackupId, validateCodexHistoryBackupManifest } from "./history-manifest";
|
|
20
21
|
import {
|
|
21
22
|
hasInjectedCodexRouting,
|
|
22
23
|
OCX_SECTION_MARKER,
|
|
@@ -590,7 +591,13 @@ function classifyHistoryDatabase(path: string): NativeRoutedResidueResult {
|
|
|
590
591
|
}
|
|
591
592
|
|
|
592
593
|
function historyBackupPath(stateDatabasePath: string): string {
|
|
593
|
-
|
|
594
|
+
const canonical = join(getConfigDir(), `codex-history-backup-${codexHistoryBackupId(stateDatabasePath)}.json`);
|
|
595
|
+
if (existsSync(canonical)) return canonical;
|
|
596
|
+
// A database path spelled with the Win32 extended-length prefix hashed to a different
|
|
597
|
+
// manifest name before #4442; that manifest still shadows the database (and a canonical
|
|
598
|
+
// file always wins over it, with the legacy one left in place).
|
|
599
|
+
const legacy = join(getConfigDir(), `codex-history-backup-${legacyCodexHistoryBackupId(stateDatabasePath)}.json`);
|
|
600
|
+
return legacy !== canonical && existsSync(legacy) ? legacy : canonical;
|
|
594
601
|
}
|
|
595
602
|
|
|
596
603
|
function classifyHistoryBackup(path: string, stateDatabasePath: string): NativeRoutedResidueResult {
|
|
@@ -8,7 +8,7 @@ import { isSelectableCodexPoolAccount } from "./account-id";
|
|
|
8
8
|
import { reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
9
9
|
import { isCodexAccountPaused } from "./account-pause";
|
|
10
10
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
11
|
-
import { getValidCodexToken, isCodexAccountGenerationLive, readCodexAccountRecord } from "./account-store";
|
|
11
|
+
import { capturePoolQuotaWriter, getValidCodexToken, isCodexAccountGenerationLive, readCodexAccountRecord } from "./account-store";
|
|
12
12
|
import { codexAccountLogLabel } from "./account-label";
|
|
13
13
|
import { getMainAccountToken, getValidMainAccountToken, MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
14
14
|
import { isMainAccountHardLocked } from "./main-account-hard-lock";
|
|
@@ -170,10 +170,11 @@ async function warmAccount(config: OcxConfig, accountId: string): Promise<void |
|
|
|
170
170
|
if (!record?.credential || record.deletedAt != null || record.codexValidationPending
|
|
171
171
|
|| record.generation !== token.generation) return false;
|
|
172
172
|
if (isCodexAccountPaused(config, accountId) || isAccountNeedsReauth(accountId)) return false;
|
|
173
|
+
const poolWriter = capturePoolQuotaWriter(accountId, token);
|
|
173
174
|
try {
|
|
174
175
|
await warmCodexAccount({ ...token, onCompleted: headers => {
|
|
175
176
|
if (isCodexAccountGenerationLive(accountId, token.generation)) {
|
|
176
|
-
applyAccountQuotaFromUpstreamHeaders(accountId, headers, writerGeneration);
|
|
177
|
+
applyAccountQuotaFromUpstreamHeaders(accountId, headers, writerGeneration, undefined, { poolWriter });
|
|
177
178
|
}
|
|
178
179
|
} });
|
|
179
180
|
} catch (error) {
|