@bitkyc08/opencodex 2.81.0 → 2.82.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/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-BqHF5Trd.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-D3FBCNiP.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DTBC8n7-.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +43 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +303 -171
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +6 -2
- package/src/oauth/store.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +1 -0
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +31 -2
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +34 -13
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +19 -2
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +25 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +5 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import { getConfigPath, initializePersistedConfigIfMissing, mutatePersistedConfig } from "../../config";
|
|
3
|
+
import { mergeConfigDefaults, readConfigFileSnapshot, validateConfigCandidate } from "../../config/diagnostics";
|
|
4
|
+
import { adoptPersistedCodexAccountSelection, claudeCodeBaselineArmed } from "../../config/live-reconcile";
|
|
5
|
+
import { deleteConfigTopLevelKey, fileBackedConfigPath, recordFileBackedConfig } from "../../config/rebase-provenance";
|
|
6
|
+
import { MAIN_CODEX_ACCOUNT_ID, hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
|
|
7
|
+
import { isCodexAccountPaused } from "../account-pause";
|
|
8
|
+
import { readCodexAccountRecord } from "../account-store";
|
|
9
|
+
import { codexAccountPinDrainReason, resetCodexRoutingForManualSelection } from "../routing";
|
|
10
|
+
import { jsonResponse } from "./http";
|
|
11
|
+
|
|
12
|
+
class SelectionRejected extends Error {
|
|
13
|
+
constructor(message: string, readonly status: number) { super(message); }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function validateSelection(config: OcxConfig, accountId: string | null): void {
|
|
17
|
+
if (accountId === null) return;
|
|
18
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID && hasLegacyMainCodexPoolAccount(config.codexAccounts)) {
|
|
19
|
+
throw new SelectionRejected("Remove the legacy __main__ pool row before selecting the Desktop account", 409);
|
|
20
|
+
}
|
|
21
|
+
if (isCodexAccountPaused(config, accountId)) throw new SelectionRejected("Account is paused", 409);
|
|
22
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) return;
|
|
23
|
+
if (!isValidCodexAccountId(accountId)) throw new SelectionRejected("Invalid account id format", 400);
|
|
24
|
+
if (!(config.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === accountId)) {
|
|
25
|
+
throw new SelectionRejected("Account not found", 400);
|
|
26
|
+
}
|
|
27
|
+
if (readCodexAccountRecord(accountId)?.codexValidationPending) {
|
|
28
|
+
throw new SelectionRejected("Account validation is pending. Refresh quota after recovery to validate it.", 409);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function applySelection(config: OcxConfig, accountId: string | null): boolean {
|
|
33
|
+
const changed = config.activeCodexAccountId !== (accountId ?? undefined)
|
|
34
|
+
|| config.activeCodexAccountPinned !== (accountId ?? undefined);
|
|
35
|
+
if (accountId === null) {
|
|
36
|
+
deleteConfigTopLevelKey(config, "activeCodexAccountId");
|
|
37
|
+
deleteConfigTopLevelKey(config, "activeCodexAccountPinned");
|
|
38
|
+
} else {
|
|
39
|
+
config.activeCodexAccountId = accountId;
|
|
40
|
+
config.activeCodexAccountPinned = accountId;
|
|
41
|
+
}
|
|
42
|
+
return changed;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A manual selection is an explicit command, even when equal to the live merge baseline. */
|
|
46
|
+
export function persistCodexAccountSelection(config: OcxConfig, accountId: string | null): Response {
|
|
47
|
+
const unknown = () => jsonResponse({
|
|
48
|
+
error: "Account selection could not be confirmed; reload settings before retrying",
|
|
49
|
+
code: "account_selection_unavailable",
|
|
50
|
+
}, 409);
|
|
51
|
+
const configPath = getConfigPath();
|
|
52
|
+
const sourcePath = fileBackedConfigPath(config);
|
|
53
|
+
if (sourcePath !== undefined && sourcePath !== configPath) return unknown();
|
|
54
|
+
let committed: OcxConfig;
|
|
55
|
+
let warning = false;
|
|
56
|
+
try {
|
|
57
|
+
// Do not activate a newly disk-only account that the running proxy has not adopted.
|
|
58
|
+
validateSelection(config, accountId);
|
|
59
|
+
const before = readConfigFileSnapshot();
|
|
60
|
+
if (before.diagnostics.source === "default" && !fileBackedConfigPath(config) && !claudeCodeBaselineArmed(config)) {
|
|
61
|
+
// Preserve transient/initial callers without recreating a deleted live configuration.
|
|
62
|
+
// The create-only owner cannot replace a competing writer's newly published config.
|
|
63
|
+
const normalized = validateConfigCandidate(mergeConfigDefaults(config));
|
|
64
|
+
if (!normalized.ok) return unknown();
|
|
65
|
+
const initial = normalized.config;
|
|
66
|
+
applySelection(initial, accountId);
|
|
67
|
+
initializePersistedConfigIfMissing(initial);
|
|
68
|
+
}
|
|
69
|
+
const result = mutatePersistedConfig(persisted => {
|
|
70
|
+
// This callback is re-run under the mutation lock immediately before publication.
|
|
71
|
+
// A removed or paused account must not be resurrected by a stale UI snapshot.
|
|
72
|
+
validateSelection(config, accountId);
|
|
73
|
+
validateSelection(persisted, accountId);
|
|
74
|
+
return { changed: applySelection(persisted, accountId), value: persisted };
|
|
75
|
+
});
|
|
76
|
+
if (result.status === "unavailable") return unknown();
|
|
77
|
+
committed = result.value;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof SelectionRejected) return jsonResponse({ error: error.message }, error.status);
|
|
80
|
+
// Atomic publication may succeed before generation/bookkeeping fails. Confirm the exact
|
|
81
|
+
// durable selection instead of rolling live state back behind an already published write.
|
|
82
|
+
const snapshot = readConfigFileSnapshot();
|
|
83
|
+
if (snapshot.raw === undefined) return unknown();
|
|
84
|
+
try {
|
|
85
|
+
const strict = validateConfigCandidate(JSON.parse(snapshot.raw.replace(/^\uFEFF/, "")));
|
|
86
|
+
if (!strict.ok) return unknown();
|
|
87
|
+
validateSelection(config, accountId);
|
|
88
|
+
validateSelection(strict.config, accountId);
|
|
89
|
+
if (strict.config.activeCodexAccountId !== (accountId ?? undefined)
|
|
90
|
+
|| strict.config.activeCodexAccountPinned !== (accountId ?? undefined)) return unknown();
|
|
91
|
+
committed = strict.config;
|
|
92
|
+
warning = true;
|
|
93
|
+
} catch { return unknown(); }
|
|
94
|
+
}
|
|
95
|
+
adoptPersistedCodexAccountSelection(config, committed);
|
|
96
|
+
recordFileBackedConfig(config, configPath);
|
|
97
|
+
// No failed save may clear affinity, spend the manual preference, or move the RR cursor.
|
|
98
|
+
resetCodexRoutingForManualSelection(accountId ?? MAIN_CODEX_ACCOUNT_ID);
|
|
99
|
+
const pinDrainReason = accountId === null ? undefined : codexAccountPinDrainReason(config, accountId);
|
|
100
|
+
return jsonResponse({
|
|
101
|
+
ok: true,
|
|
102
|
+
activeCodexAccountId: config.activeCodexAccountId ?? null,
|
|
103
|
+
appliesImmediately: true,
|
|
104
|
+
...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
|
|
105
|
+
...(warning ? { warning: "config_bookkeeping_failed" } : {}),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { persistCodexAccountSelection } from "./account-selection";
|
|
1
2
|
import { CODEX_ACCOUNT_LOG_LABEL_RE, codexAccountLogLabel } from "../account-label";
|
|
2
3
|
import { poolQuotaHistoryIdentity, readCodexAccountRecord } from "../account-store";
|
|
3
4
|
import { estimateCodexQuotaCapacity, insufficientCodexCapacity } from "../quota-capacity";
|
|
@@ -7,13 +8,13 @@ import { getAccountQuotaHistory, listAccountQuotas } from "../quota";
|
|
|
7
8
|
import { deleteCodexAccount } from "../account-lifecycle";
|
|
8
9
|
import { isCodexAccountPaused, setCodexAccountPaused } from "../account-pause";
|
|
9
10
|
import { setAllCodexAccountsCreditsAfterLimit, setCodexAccountCreditsAfterLimit } from "../account-credit-use";
|
|
10
|
-
import { clearCodexAccountPin, isCodexAccountPriorityKey, pinnedCodexAccountId,
|
|
11
|
-
import {
|
|
11
|
+
import { clearCodexAccountPin, isCodexAccountPriorityKey, pinnedCodexAccountId, setCodexAccountPriority } from "../account-priority";
|
|
12
|
+
import { codexQuotaScopeForModel, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, isEffectiveCodexAccountPinned } from "../routing";
|
|
12
13
|
import { DEFAULT_ACCOUNT_PRIORITY, MAX_ACCOUNT_PRIORITY, MIN_ACCOUNT_PRIORITY, normalizeAccountPoolStickyLimit, normalizeCodexAccountPoolStrategy, parseAccountPoolStickyLimit, parseCodexAccountPoolStrategy, parseAccountPriority } from "../pool-rotation";
|
|
13
14
|
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
14
15
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
15
16
|
import type { OcxConfig } from "../../types";
|
|
16
|
-
import { CODEX_ACCOUNT_ID_RE,
|
|
17
|
+
import { CODEX_ACCOUNT_ID_RE, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
|
|
17
18
|
import { isCodexResetCreditOperationId } from "../reset-credit-recovery";
|
|
18
19
|
import { listCodexAuthAccounts, selectFallbackAfterPause, pauseExhaustedCodexAccounts } from "./account-list";
|
|
19
20
|
import { jsonResponse, manualImportDisabledResponse } from "./http";
|
|
@@ -247,53 +248,13 @@ export async function handleCodexAuthAPI(
|
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
if (url.pathname === "/api/codex-auth/active" && req.method === "PUT") {
|
|
250
|
-
let body:
|
|
251
|
-
try { body =
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
return jsonResponse({ error: "Remove the legacy __main__ pool row before selecting the Desktop account" }, 409);
|
|
256
|
-
}
|
|
257
|
-
if (body.accountId != null && isCodexAccountPaused(runtimeConfig, targetAccountId)) {
|
|
258
|
-
return jsonResponse({ error: "Account is paused" }, 409);
|
|
259
|
-
}
|
|
260
|
-
if (body.accountId != null && body.accountId !== MAIN_CODEX_ACCOUNT_ID) {
|
|
261
|
-
if (!isValidCodexAccountId(body.accountId)) return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
262
|
-
const exists = (runtimeConfig.codexAccounts ?? [])
|
|
263
|
-
.some(account => isSelectableCodexPoolAccount(account) && account.id === body.accountId);
|
|
264
|
-
if (!exists) return jsonResponse({ error: "Account not found" }, 400);
|
|
265
|
-
if (readCodexAccountRecord(body.accountId)?.codexValidationPending) {
|
|
266
|
-
return jsonResponse({ error: "Account validation is pending. Refresh quota after recovery to validate it." }, 409);
|
|
267
|
-
}
|
|
251
|
+
let body: unknown;
|
|
252
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
253
|
+
if (!body || typeof body !== "object" || Array.isArray(body)
|
|
254
|
+
|| !("accountId" in body) || (body.accountId !== null && typeof body.accountId !== "string")) {
|
|
255
|
+
return jsonResponse({ error: "accountId must be an account id or null" }, 400);
|
|
268
256
|
}
|
|
269
|
-
|
|
270
|
-
// "Use this account now" outranks selection order until the account is spent:
|
|
271
|
-
// persisted here rather than in resetCodexRoutingForManualSelection, which is
|
|
272
|
-
// runtime state only. A null id clears the selection instead of making one, so it
|
|
273
|
-
// must release the pin rather than record one: pinning the `targetAccountId`
|
|
274
|
-
// fallback would leave a pin that no effective active account matches, which
|
|
275
|
-
// `isEffectiveCodexAccountPinned` reports as unpinned while the tier filter still
|
|
276
|
-
// honours it as a ceiling — invisibly capping the pool at the main account's tier.
|
|
277
|
-
if (body.accountId == null) clearCodexAccountPin(runtimeConfig);
|
|
278
|
-
else setCodexAccountPin(runtimeConfig, targetAccountId);
|
|
279
|
-
resetCodexRoutingForManualSelection(targetAccountId);
|
|
280
|
-
saveRuntimeConfig(config, runtimeConfig);
|
|
281
|
-
// A pin this route accepts can still be dropped by the very next resolve, and saying
|
|
282
|
-
// nothing about that is what made the setting look ignored (#4521). The checks above
|
|
283
|
-
// refuse an account that cannot be selected at all; this reports the one remaining
|
|
284
|
-
// outcome they do not cover, from the same predicate routing releases on, so the two
|
|
285
|
-
// cannot drift. Absent means the pin survives — additive for existing clients.
|
|
286
|
-
// `appliesImmediately` is unchanged: it answers whether thread affinity was cleared,
|
|
287
|
-
// not whether the pin is durable.
|
|
288
|
-
const pinDrainReason = body.accountId == null
|
|
289
|
-
? undefined
|
|
290
|
-
: codexAccountPinDrainReason(runtimeConfig, targetAccountId);
|
|
291
|
-
return jsonResponse({
|
|
292
|
-
ok: true,
|
|
293
|
-
activeCodexAccountId: body.accountId,
|
|
294
|
-
appliesImmediately: true,
|
|
295
|
-
...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
|
|
296
|
-
});
|
|
257
|
+
return persistCodexAccountSelection(getRuntimeConfig(config), body.accountId);
|
|
297
258
|
}
|
|
298
259
|
|
|
299
260
|
if (url.pathname === "/api/codex-auth/active" && req.method === "GET") {
|
|
@@ -81,11 +81,13 @@ import {
|
|
|
81
81
|
import {
|
|
82
82
|
captureMainAccountIdentityGeneration,
|
|
83
83
|
captureMainQuotaWriter,
|
|
84
|
+
captureMainQuotaDispatch,
|
|
84
85
|
getObservedMainQuotaIdentityKey,
|
|
85
86
|
isMainQuotaWriterLive,
|
|
86
87
|
matchesMainQuotaCredential,
|
|
87
88
|
observeMainQuotaCredential,
|
|
88
89
|
type MainQuotaWriter,
|
|
90
|
+
type MainQuotaDispatch,
|
|
89
91
|
} from "./main-account-cache";
|
|
90
92
|
import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupported, isCodexReserveRequestEligible } from "./loopback-target";
|
|
91
93
|
import type { DataPlaneAdmission } from "../server/auth-cors";
|
|
@@ -275,7 +277,11 @@ export function previewCodexPoolLineage(
|
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
export type CodexAuthContext =
|
|
278
|
-
| {
|
|
280
|
+
| {
|
|
281
|
+
kind: "main"; accountId: null; reserveAuthorization?: MainReserveAuthorization;
|
|
282
|
+
/** Captured for the observed main credential actually selected for upstream. */
|
|
283
|
+
mainQuotaDispatch?: MainQuotaDispatch;
|
|
284
|
+
}
|
|
279
285
|
| {
|
|
280
286
|
kind: "pool";
|
|
281
287
|
accountId: string;
|
|
@@ -1635,6 +1641,14 @@ export class CodexMainSubstitutionUnavailableError extends Error {
|
|
|
1635
1641
|
}
|
|
1636
1642
|
}
|
|
1637
1643
|
|
|
1644
|
+
/** Dispatch identity comes only from an owned observation of the selected credential. */
|
|
1645
|
+
function selectedMainQuotaDispatch(selected: Headers): MainQuotaDispatch | undefined {
|
|
1646
|
+
const bearer = selected.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
1647
|
+
if (!bearer) return undefined;
|
|
1648
|
+
const accountId = selected.get("chatgpt-account-id") ?? extractAccountId(undefined, bearer);
|
|
1649
|
+
return captureMainQuotaDispatch(bearer, accountId, captureConfigGeneration());
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1638
1652
|
/**
|
|
1639
1653
|
* Build the upstream auth headers for one Codex turn.
|
|
1640
1654
|
*
|
|
@@ -1654,6 +1668,7 @@ export function materializeCodexUpstreamAuth(
|
|
|
1654
1668
|
ctx: CodexAuthContext,
|
|
1655
1669
|
options: CodexAuthMaterializationOptions = {},
|
|
1656
1670
|
): Headers {
|
|
1671
|
+
if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
|
|
1657
1672
|
const selected = new Headers();
|
|
1658
1673
|
for (const name of FORWARD_HEADERS) {
|
|
1659
1674
|
const value = headers.get(name);
|
|
@@ -1695,10 +1710,12 @@ export function materializeCodexUpstreamAuth(
|
|
|
1695
1710
|
observeSelectedMainCredential(stored, writer);
|
|
1696
1711
|
assertMainAccountPolicy(options.config);
|
|
1697
1712
|
assertMaterializedReserve(selected, ctx, options);
|
|
1713
|
+
ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
|
|
1698
1714
|
return selected;
|
|
1699
1715
|
}
|
|
1700
1716
|
if (callerMatchesObservedMain(selected)) assertMainAccountPolicy(options.config);
|
|
1701
1717
|
assertMaterializedReserve(selected, ctx, options);
|
|
1718
|
+
if (ctx.kind === "main") ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
|
|
1702
1719
|
return selected;
|
|
1703
1720
|
}
|
|
1704
1721
|
|
|
@@ -1749,6 +1766,7 @@ export async function materializeCodexUpstreamAuthAsync(
|
|
|
1749
1766
|
ctx: CodexAuthContext,
|
|
1750
1767
|
options: CodexAuthMaterializationOptions = {},
|
|
1751
1768
|
): Promise<Headers> {
|
|
1769
|
+
if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
|
|
1752
1770
|
if (requiresReserveAuthorization(options.config, options.modelId, options.admission)) {
|
|
1753
1771
|
return materializeReserveUpstreamAuth(headers, ctx, options);
|
|
1754
1772
|
}
|
|
@@ -1776,6 +1794,7 @@ export async function materializeCodexUpstreamAuthAsync(
|
|
|
1776
1794
|
assertMainAccountPolicy(options.config);
|
|
1777
1795
|
// An opt-in enabled during token refresh must not turn a proof-less context into Reserve.
|
|
1778
1796
|
assertMaterializedReserve(selected, ctx, options);
|
|
1797
|
+
ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
|
|
1779
1798
|
return selected;
|
|
1780
1799
|
}
|
|
1781
1800
|
|
|
@@ -60,6 +60,7 @@ export interface StartupHealth {
|
|
|
60
60
|
platform: NodeJS.Platform;
|
|
61
61
|
diagnosticStale: boolean;
|
|
62
62
|
recommendedCommand: string | null;
|
|
63
|
+
recommendedAction?: string | null;
|
|
63
64
|
commands: {
|
|
64
65
|
installService: string;
|
|
65
66
|
repairService: string;
|
|
@@ -91,7 +92,8 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
|
|
|
91
92
|
// An arbitrary localhost gateway has an independent lifecycle that OCX cannot repair.
|
|
92
93
|
const ownsLocalRouting = inputs.routingKind === "opencodex-local";
|
|
93
94
|
const desktopEffective = (inputs.platform === "darwin" || inputs.platform === "linux") && !inputs.diagnosticStale
|
|
94
|
-
&& inputs.desktop
|
|
95
|
+
&& inputs.desktop !== undefined && (inputs.desktop.owned || inputs.desktop.supervisor !== undefined)
|
|
96
|
+
&& inputs.desktop.loginEnabled
|
|
95
97
|
&& inputs.desktop.running && inputs.desktop.viable;
|
|
96
98
|
const protection: StartupProtection = ownsLocalRouting && inputs.serviceViable
|
|
97
99
|
? "service"
|
|
@@ -105,7 +107,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
|
|
|
105
107
|
: rebootSafe
|
|
106
108
|
? "protected"
|
|
107
109
|
: "at-risk";
|
|
108
|
-
const recommendedCommand = status !== "at-risk" || (ownsLocalRouting && inputs.desktop
|
|
110
|
+
const recommendedCommand = status !== "at-risk" || (ownsLocalRouting && inputs.desktop !== undefined)
|
|
109
111
|
? null
|
|
110
112
|
: inputs.routingKind === "custom-local" || inputs.routingKind === "unknown"
|
|
111
113
|
? COMMANDS.restoreNative
|
|
@@ -117,6 +119,11 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
|
|
|
117
119
|
// gets the registering command.
|
|
118
120
|
? (inputs.serviceInstalled && !inputs.serviceConflict ? COMMANDS.repairService : COMMANDS.installService)
|
|
119
121
|
: COMMANDS.restoreNative;
|
|
122
|
+
const recommendedAction = status === "at-risk" && ownsLocalRouting && inputs.desktop !== undefined
|
|
123
|
+
? inputs.diagnosticStale || inputs.desktop.loginEnabled
|
|
124
|
+
? "Reopen OpenCodex and check Start at Login."
|
|
125
|
+
: "Turn on Start at Login in the OpenCodex menu so the desktop app starts this proxy after a restart."
|
|
126
|
+
: null;
|
|
120
127
|
return {
|
|
121
128
|
...inputs,
|
|
122
129
|
diagnosticStale: inputs.diagnosticStale ?? false,
|
|
@@ -127,6 +134,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
|
|
|
127
134
|
protection,
|
|
128
135
|
shimCoverage,
|
|
129
136
|
recommendedCommand,
|
|
137
|
+
recommendedAction,
|
|
130
138
|
commands: { ...COMMANDS },
|
|
131
139
|
};
|
|
132
140
|
}
|
|
@@ -211,7 +219,8 @@ function classifyStartupHealthSummary(health: StartupHealth): string {
|
|
|
211
219
|
const command = health.recommendedCommand ?? health.commands.restoreNative;
|
|
212
220
|
if (health.routingKind === "unknown") return `AT RISK after restart (Codex routing could not be verified; run '${command}')`;
|
|
213
221
|
if (health.routingKind === "custom-local") return `AT RISK after restart (custom local gateway lifecycle is not managed by opencodex; run '${command}')`;
|
|
214
|
-
if (health.desktop?.
|
|
222
|
+
if (health.desktop?.supervisor && !health.desktop.loginEnabled && !health.diagnosticStale) return "AT RISK after restart (OpenCodex Desktop runs this proxy, but its Start at Login could not be verified; turn it on in the OpenCodex menu)";
|
|
223
|
+
if (health.desktop) return "AT RISK after restart (desktop startup could not be verified; reopen OpenCodex and check Start at Login)";
|
|
215
224
|
if (health.shimCoverage === "cli-only") return `AT RISK for Codex Desktop after restart (launcher shim covers CLI scripts only; run '${command}')`;
|
|
216
225
|
if (health.serviceConflict) return `AT RISK after restart (background service managers conflict; run '${command}')`;
|
|
217
226
|
if (health.serviceStale) return `AT RISK after restart (background service files are stale; run '${command}')`;
|
|
@@ -48,6 +48,7 @@ import { NATIVE_RESERVE_MODEL } from "./native-models";
|
|
|
48
48
|
import { isReserveCatalogProjection, type ReserveCatalogProjection } from "./reserve";
|
|
49
49
|
import { deriveEntry, finishUpstreamNativeEntry, isExactComboCatalogEntry } from "./derive-entry";
|
|
50
50
|
import { PICKER_ORDER_PRIORITY_BASE, SPAWN_PRIORITY_FIELD } from "./subagent-roster";
|
|
51
|
+
import { preserveOperatorUltraFastTiers } from "./operator-tiers";
|
|
51
52
|
|
|
52
53
|
export interface ObservedCatalogEntryBuildInput {
|
|
53
54
|
readonly template: RawEntry | null;
|
|
@@ -470,6 +471,8 @@ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly s
|
|
|
470
471
|
}
|
|
471
472
|
|
|
472
473
|
export interface ObservedCatalogMergeInput {
|
|
474
|
+
/** Preserve operator-supplied Ultra Fast only from the exact persisted routed row. */
|
|
475
|
+
readonly ultraFastTier?: boolean;
|
|
473
476
|
readonly catalogModels: readonly RawEntry[];
|
|
474
477
|
readonly baselineCatalogModels: readonly RawEntry[];
|
|
475
478
|
readonly routedEntries: readonly RawEntry[];
|
|
@@ -510,6 +513,7 @@ export interface ObservedCatalogMergeInput {
|
|
|
510
513
|
* accidentally fall back to process-ambient catalog discovery or merge-policy warnings.
|
|
511
514
|
*/
|
|
512
515
|
export function mergeCatalogEntriesFromObservedState({
|
|
516
|
+
ultraFastTier = false,
|
|
513
517
|
catalogModels,
|
|
514
518
|
baselineCatalogModels,
|
|
515
519
|
routedEntries,
|
|
@@ -849,6 +853,20 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
849
853
|
}
|
|
850
854
|
return false;
|
|
851
855
|
});
|
|
856
|
+
// First occurrence wins, matching enforceCatalogSlugUniqueness: a later duplicate row of the
|
|
857
|
+
// same slug is what the writer drops, so it must not decide which declaration survives.
|
|
858
|
+
const operatorRows = new Map<string, RawEntry | undefined>();
|
|
859
|
+
for (const entry of detachedCatalogModels) {
|
|
860
|
+
if (typeof entry.slug !== "string" || operatorRows.has(entry.slug)) continue;
|
|
861
|
+
operatorRows.set(entry.slug, trustedAccountBoundNativeCatalogSlug(entry) === undefined
|
|
862
|
+
&& !isNativeAliasCatalogEntry(entry) && entry.owned_by !== COMBO_NAMESPACE ? entry : undefined);
|
|
863
|
+
}
|
|
864
|
+
for (const entry of finalRoutedEntries) {
|
|
865
|
+
if (typeof entry.slug !== "string" || !entry.slug.includes("/")
|
|
866
|
+
|| isNativeAliasCatalogEntry(entry) || isExactComboCatalogEntry(entry, exactComboSlugs)
|
|
867
|
+
|| trustedAccountBoundNativeCatalogSlug(entry) !== undefined) continue;
|
|
868
|
+
preserveOperatorUltraFastTiers(entry, operatorRows.get(entry.slug), ultraFastTier);
|
|
869
|
+
}
|
|
852
870
|
const finalRoutedEntrySet = new Set(finalRoutedEntries);
|
|
853
871
|
const degradedPreservedCount = preservedRoutedEntries.filter(entry => {
|
|
854
872
|
if (!finalRoutedEntrySet.has(entry)) return false;
|