@bitkyc08/opencodex 2.81.0 → 2.83.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-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.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/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- 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/account-auth.ts +20 -12
- 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 +26 -3
- 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/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- 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/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- 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/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -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/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -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/main-account.ts +6 -7
- 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 +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- 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/cursor-detect.ts +1 -1
- 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/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- 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 +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.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 +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- 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/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.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/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- 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/compact.ts +3 -2
- 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-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -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 +10 -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/usage/expected-prices.ts +1 -1
- 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
|
+
}
|
|
@@ -88,14 +88,13 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
|
|
|
88
88
|
const now = Date.now();
|
|
89
89
|
const quota = getMainPolicyQuota();
|
|
90
90
|
const observedAt = quota?.credits?.observedAt;
|
|
91
|
-
//
|
|
92
|
-
//
|
|
91
|
+
// Positive evidence renews before expiry; unavailable evidence keeps discovery
|
|
92
|
+
// eligible under the same pacing. Admission still requires fresh spendable funds.
|
|
93
93
|
return codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
|
|
94
94
|
&& !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
|
|
95
95
|
&& codexUsageLimitResetAt(quota, undefined, now) !== undefined
|
|
96
|
-
&&
|
|
97
|
-
|
|
98
|
-
&& hasSpendableCodexCredits(quota, observedAt);
|
|
96
|
+
&& (!hasSpendableCodexCredits(quota, now)
|
|
97
|
+
|| (observedAt !== undefined && now - observedAt >= MAIN_CREDIT_RENEWAL_AGE_MS));
|
|
99
98
|
};
|
|
100
99
|
const status = getMainAccountHardLockStatus(config);
|
|
101
100
|
if (status.state !== "blocked" && !creditRecoveryNeeded()) { mainHardLockRecoveryAttempt = undefined; return; }
|
|
@@ -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") {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { bindPoolCreditPolicy, poolCreditHoldResetAt, poolContextCreditHoldResetAt } from "./pool-credit-policy";
|
|
2
2
|
import { hasSpendableCodexCredits } from "./quota-types";
|
|
3
|
+
import { nextQuotaQueryDelay } from "./quota-query-backoff";
|
|
3
4
|
import { noteMainAccountActivity } from "./main-account-external-usage";
|
|
4
5
|
import { codexAccountPriorityFailbackEnabled } from "./account-priority";
|
|
5
6
|
import type { PoolQuotaWriter } from "./quota-types";
|
|
@@ -81,11 +82,13 @@ import {
|
|
|
81
82
|
import {
|
|
82
83
|
captureMainAccountIdentityGeneration,
|
|
83
84
|
captureMainQuotaWriter,
|
|
85
|
+
captureMainQuotaDispatch,
|
|
84
86
|
getObservedMainQuotaIdentityKey,
|
|
85
87
|
isMainQuotaWriterLive,
|
|
86
88
|
matchesMainQuotaCredential,
|
|
87
89
|
observeMainQuotaCredential,
|
|
88
90
|
type MainQuotaWriter,
|
|
91
|
+
type MainQuotaDispatch,
|
|
89
92
|
} from "./main-account-cache";
|
|
90
93
|
import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupported, isCodexReserveRequestEligible } from "./loopback-target";
|
|
91
94
|
import type { DataPlaneAdmission } from "../server/auth-cors";
|
|
@@ -275,7 +278,11 @@ export function previewCodexPoolLineage(
|
|
|
275
278
|
}
|
|
276
279
|
|
|
277
280
|
export type CodexAuthContext =
|
|
278
|
-
| {
|
|
281
|
+
| {
|
|
282
|
+
kind: "main"; accountId: null; reserveAuthorization?: MainReserveAuthorization;
|
|
283
|
+
/** Captured for the observed main credential actually selected for upstream. */
|
|
284
|
+
mainQuotaDispatch?: MainQuotaDispatch;
|
|
285
|
+
}
|
|
279
286
|
| {
|
|
280
287
|
kind: "pool";
|
|
281
288
|
accountId: string;
|
|
@@ -547,8 +554,11 @@ function assertPoolAccountCredits(accountId: string, resetAt: number | undefined
|
|
|
547
554
|
export class CodexMainAccountCreditsOffError extends CodexAccountCooldownError {
|
|
548
555
|
readonly resetAt?: number;
|
|
549
556
|
|
|
550
|
-
constructor(resetAt?: number) {
|
|
551
|
-
|
|
557
|
+
constructor(resetAt?: number, creditRecovery = false) {
|
|
558
|
+
// Keep the included reset as policy evidence, but let consented callers retry
|
|
559
|
+
// within the base metadata recovery interval even when backoff grows longer.
|
|
560
|
+
super(MAIN_CODEX_ACCOUNT_ID, creditRecovery
|
|
561
|
+
? Math.min(resetAt ?? Infinity, Date.now() + nextQuotaQueryDelay()) : resetAt ?? 0);
|
|
552
562
|
this.name = "CodexMainAccountCreditsOffError";
|
|
553
563
|
this.resetAt = resetAt;
|
|
554
564
|
this.message = "Codex main account reached its usage limit, and spending ChatGPT credits is off or no fresh spendable balance is available."
|
|
@@ -772,7 +782,8 @@ function assertMainAccountPolicy(
|
|
|
772
782
|
const status = getMainAccountHardLockStatus(config);
|
|
773
783
|
if (status.state === "blocked") throw new CodexMainAccountHardLockError(status.resetAt, status.thresholds);
|
|
774
784
|
const creditsResetAt = mainCreditsHoldResetAt(config);
|
|
775
|
-
if (creditsResetAt !== undefined) throw new CodexMainAccountCreditsOffError(creditsResetAt
|
|
785
|
+
if (creditsResetAt !== undefined) throw new CodexMainAccountCreditsOffError(creditsResetAt,
|
|
786
|
+
codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID));
|
|
776
787
|
}
|
|
777
788
|
// Only an admitted request is opencodex's own use of the main account. Counting a refused one
|
|
778
789
|
// would hide outside usage from the warning exactly while the lock is holding.
|
|
@@ -1635,6 +1646,14 @@ export class CodexMainSubstitutionUnavailableError extends Error {
|
|
|
1635
1646
|
}
|
|
1636
1647
|
}
|
|
1637
1648
|
|
|
1649
|
+
/** Dispatch identity comes only from an owned observation of the selected credential. */
|
|
1650
|
+
function selectedMainQuotaDispatch(selected: Headers): MainQuotaDispatch | undefined {
|
|
1651
|
+
const bearer = selected.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
1652
|
+
if (!bearer) return undefined;
|
|
1653
|
+
const accountId = selected.get("chatgpt-account-id") ?? extractAccountId(undefined, bearer);
|
|
1654
|
+
return captureMainQuotaDispatch(bearer, accountId, captureConfigGeneration());
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1638
1657
|
/**
|
|
1639
1658
|
* Build the upstream auth headers for one Codex turn.
|
|
1640
1659
|
*
|
|
@@ -1654,6 +1673,7 @@ export function materializeCodexUpstreamAuth(
|
|
|
1654
1673
|
ctx: CodexAuthContext,
|
|
1655
1674
|
options: CodexAuthMaterializationOptions = {},
|
|
1656
1675
|
): Headers {
|
|
1676
|
+
if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
|
|
1657
1677
|
const selected = new Headers();
|
|
1658
1678
|
for (const name of FORWARD_HEADERS) {
|
|
1659
1679
|
const value = headers.get(name);
|
|
@@ -1695,10 +1715,12 @@ export function materializeCodexUpstreamAuth(
|
|
|
1695
1715
|
observeSelectedMainCredential(stored, writer);
|
|
1696
1716
|
assertMainAccountPolicy(options.config);
|
|
1697
1717
|
assertMaterializedReserve(selected, ctx, options);
|
|
1718
|
+
ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
|
|
1698
1719
|
return selected;
|
|
1699
1720
|
}
|
|
1700
1721
|
if (callerMatchesObservedMain(selected)) assertMainAccountPolicy(options.config);
|
|
1701
1722
|
assertMaterializedReserve(selected, ctx, options);
|
|
1723
|
+
if (ctx.kind === "main") ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
|
|
1702
1724
|
return selected;
|
|
1703
1725
|
}
|
|
1704
1726
|
|
|
@@ -1749,6 +1771,7 @@ export async function materializeCodexUpstreamAuthAsync(
|
|
|
1749
1771
|
ctx: CodexAuthContext,
|
|
1750
1772
|
options: CodexAuthMaterializationOptions = {},
|
|
1751
1773
|
): Promise<Headers> {
|
|
1774
|
+
if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
|
|
1752
1775
|
if (requiresReserveAuthorization(options.config, options.modelId, options.admission)) {
|
|
1753
1776
|
return materializeReserveUpstreamAuth(headers, ctx, options);
|
|
1754
1777
|
}
|
|
@@ -1776,6 +1799,7 @@ export async function materializeCodexUpstreamAuthAsync(
|
|
|
1776
1799
|
assertMainAccountPolicy(options.config);
|
|
1777
1800
|
// An opt-in enabled during token refresh must not turn a proof-less context into Reserve.
|
|
1778
1801
|
assertMaterializedReserve(selected, ctx, options);
|
|
1802
|
+
ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
|
|
1779
1803
|
return selected;
|
|
1780
1804
|
}
|
|
1781
1805
|
|
|
@@ -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;
|