@bitkyc08/opencodex 2.63.0 → 2.64.0-preview.20260923
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/AGENTS_INSTALL.md +4 -3
- package/README.md +46 -33
- package/assets/download-linux.svg +10 -0
- package/assets/download-macos.svg +10 -0
- package/assets/download-windows.svg +10 -0
- package/bin/ocx.mjs +12 -4
- package/gui/dist/assets/App-D5eN1TID.js +50 -0
- package/gui/dist/assets/App-I5AnaSLh.css +1 -0
- package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-Bh_ErQDh.js} +1 -1
- package/gui/dist/assets/index-BEq4OCOz.js +86 -0
- package/gui/dist/assets/index-DiBRuK-d.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/src/protocol.rs +5 -0
- package/package.json +4 -1
- package/src/adapters/anthropic.ts +66 -7
- package/src/adapters/codebuddy/adapter.ts +112 -16
- package/src/adapters/codebuddy/mcp-server.ts +180 -0
- package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
- package/src/adapters/codebuddy/tool-bridge.ts +597 -0
- package/src/adapters/coding-agent/protocol.ts +123 -10
- package/src/adapters/coding-agent/turn.ts +324 -2
- package/src/adapters/command-code-restored-schema.ts +112 -0
- package/src/adapters/command-code-tool-text.ts +589 -0
- package/src/adapters/command-code.ts +80 -6
- package/src/adapters/cursor/catalog.ts +12 -0
- package/src/adapters/cursor/current-request.ts +46 -0
- package/src/adapters/cursor/discovery.ts +1 -1
- package/src/adapters/cursor/effort-map.ts +4 -0
- package/src/adapters/cursor/native-exec.ts +15 -0
- package/src/adapters/cursor/protobuf-events.ts +40 -12
- package/src/adapters/cursor/protobuf-request.ts +83 -18
- package/src/adapters/cursor/request-builder.ts +5 -4
- package/src/adapters/cursor/tool-guidance.ts +1 -1
- package/src/adapters/devin/live-models.ts +7 -0
- package/src/adapters/inline-think-tags.ts +251 -0
- package/src/adapters/kiro/adapter.ts +1 -0
- package/src/adapters/kiro/stream.ts +5 -3
- package/src/adapters/kiro/usage.ts +4 -3
- package/src/adapters/mimo-free.ts +37 -18
- package/src/adapters/openai-chat/messages.ts +5 -5
- package/src/adapters/openai-chat/tool-schema.ts +124 -2
- package/src/adapters/openai-chat.ts +13 -3
- package/src/adapters/openai-responses/passthrough.ts +15 -2
- package/src/adapters/openai-responses/reasoning.ts +14 -1
- package/src/adapters/openai-responses/request-strips.ts +29 -3
- package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
- package/src/adapters/qoder/adapter.ts +15 -12
- package/src/adapters/qoder/scaffold-guard.ts +45 -13
- package/src/bridge/internal.ts +20 -0
- package/src/bridge/sse.ts +14 -5
- package/src/claude/agents-inject.ts +2 -1
- package/src/claude/desktop-applied-marker.ts +44 -0
- package/src/claude/desktop-profile.ts +22 -12
- package/src/claude/inbound.ts +8 -3
- package/src/cli/access.ts +22 -5
- package/src/cli/account-api.ts +6 -0
- package/src/cli/account-auth.ts +5 -2
- package/src/cli/account.ts +5 -6
- package/src/cli/aside-profiles.ts +61 -1
- package/src/cli/claude-desktop.ts +3 -2
- package/src/cli/claude.ts +3 -1
- package/src/cli/codex-cli-update.ts +2 -2
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +12 -4
- package/src/cli/index.ts +98 -10
- package/src/cli/registry.ts +1 -1
- package/src/cli/resolve.ts +113 -1
- package/src/cli/root.ts +5 -0
- package/src/cli/stop-approval.ts +186 -0
- package/src/cli/stop-report.ts +1 -1
- package/src/cli/system-restart-client.ts +19 -4
- package/src/client/hub-client.ts +25 -18
- package/src/clients/config-export.ts +12 -4
- package/src/codex/account-auto-switch.ts +59 -0
- package/src/codex/account-lifecycle.ts +7 -0
- package/src/codex/account-priority.ts +10 -0
- package/src/codex/account-usability.ts +9 -0
- package/src/codex/auth-api/account-list.ts +5 -0
- package/src/codex/auth-api/login-flow.ts +15 -5
- package/src/codex/auth-api/login-state.ts +5 -16
- package/src/codex/auth-api/pool-mode-gate.ts +30 -7
- package/src/codex/auth-api/routes.ts +39 -3
- package/src/codex/auth-context.ts +145 -17
- package/src/codex/catalog/build-entries.ts +2 -1
- package/src/codex/catalog/effort.ts +8 -2
- package/src/codex/catalog/metadata.ts +33 -7
- package/src/codex/catalog/native-models.ts +102 -5
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +19 -6
- package/src/codex/cli-installation-targets.ts +44 -40
- package/src/codex/codex-write-lock.ts +32 -2
- package/src/codex/inject/multi-agent-v2.ts +90 -0
- package/src/codex/inject/plan.ts +365 -0
- package/src/codex/inject.ts +237 -362
- package/src/codex/log-guard/maintenance.ts +30 -13
- package/src/codex/project-config-warnings.ts +47 -7
- package/src/codex/prompt-layers/encoding.ts +41 -0
- package/src/codex/prompt-layers/toml-read.ts +1 -1
- package/src/codex/prompt-layers.ts +17 -13
- package/src/codex/prompt-text-probe.ts +20 -10
- package/src/codex/quota-observation-freshness.ts +34 -0
- package/src/codex/quota-rejection.ts +8 -4
- package/src/codex/quota.ts +4 -2
- package/src/codex/routing/selection.ts +32 -9
- package/src/codex/routing.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +4 -3
- package/src/codex/sync.ts +11 -0
- package/src/codex/windows-installation-files.ts +33 -11
- package/src/codex/write-coordination.ts +4 -0
- package/src/combos/request.ts +6 -2
- package/src/companion/settings.ts +6 -1
- package/src/config/atomic-write.ts +12 -1
- package/src/config/derived-registries.ts +29 -0
- package/src/config/diagnostics.ts +17 -0
- package/src/config/live-reconcile.ts +11 -2
- package/src/config/load-degrade.ts +9 -2
- package/src/config/persist-unlocked.ts +4 -3
- package/src/config/persisted-mutation.ts +94 -0
- package/src/config/provider-validation.ts +1 -1
- package/src/config/proxy-env.ts +62 -12
- package/src/config/rebase-provenance.ts +80 -1
- package/src/config/schema/config-schema.ts +5 -0
- package/src/config/schema/leaf-validators.ts +50 -1
- package/src/config/subagent-models.ts +41 -15
- package/src/config.ts +18 -95
- package/src/generated/compatibility-version.json +330 -218
- package/src/generated/model-metadata.ts +8 -5
- package/src/github/star-state.ts +46 -6
- package/src/grok/inject.ts +74 -40
- package/src/grok/status.ts +11 -4
- package/src/integrations/cursor-effort-table.ts +50 -13
- package/src/integrations/raycast-detect.ts +19 -4
- package/src/integrations/serialize.ts +11 -1
- package/src/lib/bounded-body.ts +30 -0
- package/src/lib/crash-guard.ts +52 -4
- package/src/lib/local-aside-sync-contract.ts +41 -0
- package/src/lib/package-tree-integrity.ts +181 -9
- package/src/lib/proxy-env.ts +18 -1
- package/src/lib/request-failure-attribution.ts +1 -0
- package/src/lib/request-failure-model.ts +3 -0
- package/src/lib/service-secrets.ts +99 -2
- package/src/lib/socks5-fetch.ts +43 -14
- package/src/lib/token-estimate.ts +17 -2
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/generic-account-failover.ts +29 -1
- package/src/oauth/index.ts +24 -16
- package/src/oauth/login-flow-state.ts +5 -5
- package/src/oauth/meta-muse-device.ts +49 -7
- package/src/oauth/store.ts +74 -9
- package/src/providers/alibaba-region-backup.ts +16 -1
- package/src/providers/anthropic-fast.ts +89 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
- package/src/providers/anthropic-reset-grants.ts +329 -0
- package/src/providers/claude-cli-identity.ts +12 -0
- package/src/providers/command-code-efforts.ts +184 -102
- package/src/providers/derive.ts +11 -3
- package/src/providers/fastwire.ts +16 -3
- package/src/providers/label.ts +8 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota/vendor-probes-oauth.ts +2 -1
- package/src/providers/reasoning-metadata.ts +38 -15
- package/src/providers/registry/entries-core.ts +95 -7
- package/src/providers/registry/entries-extended.ts +21 -10
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +31 -3
- package/src/providers/registry/types.ts +3 -1
- package/src/providers/resolved-model-policy-merge.ts +38 -8
- package/src/providers/resolved-model-policy.ts +4 -2
- package/src/providers/service-tier.ts +3 -1
- package/src/reasoning-effort.ts +6 -5
- package/src/responses/code-mode-helper-compat.ts +14 -4
- package/src/responses/code-mode-shell-input.ts +54 -0
- package/src/responses/custom-tool-compat.ts +173 -5
- package/src/responses/parser.ts +38 -2
- package/src/responses/reasoning-replay-cache.ts +26 -0
- package/src/router.ts +11 -1
- package/src/routing/history/indexer.ts +7 -2
- package/src/routing/history/schema.ts +3 -1
- package/src/server/adapter-resolve.ts +2 -2
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native.ts +17 -0
- package/src/server/claude-messages.ts +4 -3
- package/src/server/direct-local-http.ts +45 -19
- package/src/server/gui-session.ts +6 -15
- package/src/server/index/package-tree-guard.ts +53 -0
- package/src/server/index/serve-options.ts +17 -3
- package/src/server/index/startup-warnings.ts +17 -0
- package/src/server/index.ts +15 -17
- package/src/server/lifecycle.ts +8 -0
- package/src/server/live.ts +54 -4
- package/src/server/management/agent-settings-routes.ts +44 -8
- package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
- package/src/server/management/codex-prompt-routes.ts +5 -1
- package/src/server/management/config-routes.ts +14 -4
- package/src/server/management/oauth-account-routes.ts +17 -1
- package/src/server/management/provider-overwrite-carry.ts +164 -0
- package/src/server/management/provider-routes.ts +36 -6
- package/src/server/management/remote-workspace-routes.ts +2 -2
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/shadow-call-validation.ts +39 -0
- package/src/server/management/system-restart.ts +72 -6
- package/src/server/management-api.ts +16 -2
- package/src/server/management-auth.ts +52 -8
- package/src/server/proxy-liveness.ts +110 -4
- package/src/server/request-log.ts +26 -9
- package/src/server/request-metrics.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +6 -3
- package/src/server/responses/adapter-dispatch.ts +65 -1
- package/src/server/responses/compact.ts +31 -1
- package/src/server/responses/compaction-routing.ts +28 -3
- package/src/server/responses/core-codex-account.ts +85 -26
- package/src/server/responses/core-combo-failure.ts +16 -15
- package/src/server/responses/core-normalize.ts +5 -1
- package/src/server/responses/core-opaque-recovery.ts +50 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +7 -0
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/encrypted-payload.ts +23 -9
- package/src/server/responses/passthrough-delivery.ts +55 -35
- package/src/server/responses/passthrough-dispatch.ts +12 -13
- package/src/server/responses/policy-fallback.ts +12 -1
- package/src/server/responses/request-prepare.ts +46 -7
- package/src/server/responses/request-spend.ts +31 -15
- package/src/server/responses/run-turn-execution.ts +6 -5
- package/src/server/responses/shadow-target-availability.ts +61 -0
- package/src/server/responses-custom-tool-repair.ts +2 -0
- package/src/service/claim.ts +185 -0
- package/src/service/cli.ts +10 -1
- package/src/service/guarded-manager-target.ts +151 -0
- package/src/service/guards.ts +47 -48
- package/src/service/managing-cli.ts +170 -0
- package/src/service/orchestration.ts +3 -1
- package/src/service/state.ts +4 -0
- package/src/service/systemd.ts +16 -1
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -0
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +10 -0
- package/src/types/tools.ts +9 -1
- package/src/types/wire.ts +68 -4
- package/src/types.ts +1 -0
- package/src/update/job.ts +10 -16
- package/src/update/npm-invocation.mjs +17 -16
- package/src/update/transactional-install.d.mts +22 -1
- package/src/update/transactional-install.mjs +176 -19
- package/src/update/update-failure-guidance.d.mts +8 -0
- package/src/update/update-failure-guidance.mjs +47 -0
- package/src/usage/expected-prices.ts +54 -12
- package/src/usage/log.ts +117 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/timeline.ts +34 -6
- package/src/usage/user-cost-overlay-reconciler.ts +3 -3
- package/src/vision/reasoning.ts +2 -4
- package/src/web-search/xai-executor.ts +14 -4
- package/gui/dist/assets/App-BqrsSrIR.js +0 -50
- package/gui/dist/assets/index-C6SJrh0N.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
package/src/codex/routing.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { getEffectiveCodexAutoSwitchThreshold } from "./account-auto-switch";
|
|
2
|
+
import { codexQuotaHasFreshUsage } from "./quota-observation-freshness";
|
|
1
3
|
import { saveConfigPreservingClaudeCode } from "../config";
|
|
2
4
|
import { isCodexAccountGenerationLive, registerCodexRefreshGenerationHandoff } from "./account-store";
|
|
3
5
|
import { handOffThreadAffinityGeneration } from "./routing/thread-affinity";
|
|
4
6
|
import { codexAccountLogLabel } from "./account-label";
|
|
5
7
|
import { isCodexAccountPaused } from "./account-pause";
|
|
6
|
-
import { clearCodexAccountPin, pinnedCodexAccountId } from "./account-priority";
|
|
8
|
+
import { clearCodexAccountPin, pinnedCodexAccountId, codexAccountPriorityFailbackEnabled, CODEX_PRIORITY_FAILBACK_REFRESH_MS } from "./account-priority";
|
|
7
9
|
import { isCodexAccountUsable, type CodexAccountUsabilityOptions } from "./account-usability";
|
|
8
10
|
import { markAccountNeedsReauth } from "./account-runtime-state";
|
|
9
11
|
import { codexAccountPinDrainReason } from "./routing/pin-drain";
|
|
@@ -109,6 +111,7 @@ import {
|
|
|
109
111
|
pickUnboundStrategyAccount,
|
|
110
112
|
preferModelEntitledAccount,
|
|
111
113
|
sharedStateSelectionOptions,
|
|
114
|
+
sharesActiveSelection,
|
|
112
115
|
strategySelectionOptionsForModelDetour,
|
|
113
116
|
shouldFailover,
|
|
114
117
|
peekAlternateCodexAccount,
|
|
@@ -492,6 +495,38 @@ export function resolveCodexAccountForThread(
|
|
|
492
495
|
return resolution.status === "selected" ? resolution.accountId : null;
|
|
493
496
|
}
|
|
494
497
|
|
|
498
|
+
/** The opt-in may preempt priority, never eligibility, a quota refusal or a manual pin. */
|
|
499
|
+
function pickAffinityPriorityFailback(
|
|
500
|
+
config: OcxConfig,
|
|
501
|
+
accountId: string,
|
|
502
|
+
now: number,
|
|
503
|
+
quotaScope?: CodexQuotaScope,
|
|
504
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
505
|
+
): string | null {
|
|
506
|
+
if (!codexAccountPriorityFailbackEnabled(config, accountId)
|
|
507
|
+
|| accountPoolStrategyForScope(config, quotaScope) !== "quota") return null;
|
|
508
|
+
if (!pickPriorityPreemption(config, accountId, now, quotaScope, selectionOptions)) return null;
|
|
509
|
+
// The selected tier can contain a stale cooler account beside a fresh one.
|
|
510
|
+
// Check every member before the lowest-usage picker sees it.
|
|
511
|
+
const candidates = getEligiblePoolAccounts(config, undefined, now, quotaScope, selectionOptions).filter(id => {
|
|
512
|
+
if (!hasCodexQuotaHeadroom(config, id, selectionOptions, now)
|
|
513
|
+
|| hasUnrecoveredCodexQuotaRefusal(id, quotaScope)
|
|
514
|
+
|| shouldFailover(config, id, now)) return false;
|
|
515
|
+
const quota = getAccountQuota(id);
|
|
516
|
+
const plan = getPoolAccountPlanForSelection(config, id, selectionOptions);
|
|
517
|
+
// Retained bars alone are not a reason to discard a healthy conversation's cache.
|
|
518
|
+
if (!quota || !codexQuotaHasFreshUsage(quota, plan, now, CODEX_PRIORITY_FAILBACK_REFRESH_MS)
|
|
519
|
+
|| !Number.isFinite(quota.updatedAt)
|
|
520
|
+
|| now - quota.updatedAt >= CODEX_PRIORITY_FAILBACK_REFRESH_MS
|
|
521
|
+
|| (quota.shortObservedAt !== undefined
|
|
522
|
+
&& now - quota.shortObservedAt >= CODEX_PRIORITY_FAILBACK_REFRESH_MS)) return false;
|
|
523
|
+
const usage = computeCodexUsageScore(quota, plan, now);
|
|
524
|
+
const threshold = getEffectiveCodexAutoSwitchThreshold(config, id);
|
|
525
|
+
return !isUnknownUsage(usage) && usage < 100 && (threshold <= 0 || usage < threshold);
|
|
526
|
+
});
|
|
527
|
+
return pickLowestUsageAmong(config, candidates, selectionOptions, now);
|
|
528
|
+
}
|
|
529
|
+
|
|
495
530
|
function previewReusableAffinityAccount(
|
|
496
531
|
entry: ThreadAffinityEntry | undefined,
|
|
497
532
|
config: OcxConfig,
|
|
@@ -528,10 +563,12 @@ function previewReusableAffinityAccount(
|
|
|
528
563
|
if (accountPoolStrategyForScope(config, quotaScope) === "reset-first") {
|
|
529
564
|
return resetFirstAffinityReplacement(entry, config, now, quotaScope, selectionOptions) ?? entry.accountId;
|
|
530
565
|
}
|
|
566
|
+
const recovered = pickAffinityPriorityFailback(config, entry.accountId, now, quotaScope, selectionOptions);
|
|
567
|
+
if (recovered) return recovered;
|
|
531
568
|
// Quota strategy only: non-quota strategies keep affinity for ongoing threads
|
|
532
569
|
// (new-session-only rotation — docs / affinity policy A).
|
|
533
570
|
if (accountPoolStrategyForScope(config, quotaScope) === "quota") {
|
|
534
|
-
const threshold = config.
|
|
571
|
+
const threshold = getEffectiveCodexAutoSwitchThreshold(config, entry.accountId);
|
|
535
572
|
if (threshold > 0) {
|
|
536
573
|
const usage = computeCodexUsageScore(
|
|
537
574
|
getAccountQuota(entry.accountId),
|
|
@@ -564,7 +601,7 @@ function resetFirstAffinityReplacement(
|
|
|
564
601
|
quotaScope?: CodexQuotaScope,
|
|
565
602
|
selectionOptions?: CodexAccountUsabilityOptions,
|
|
566
603
|
): string | null {
|
|
567
|
-
const threshold = config.
|
|
604
|
+
const threshold = getEffectiveCodexAutoSwitchThreshold(config, entry.accountId);
|
|
568
605
|
if (threshold <= 0) return null;
|
|
569
606
|
const usage = computeCodexUsageScore(getAccountQuota(entry.accountId), getPoolAccountPlanForSelection(config, entry.accountId, selectionOptions), now);
|
|
570
607
|
if (!mayRebindAffinityForQuota(config, entry.accountId, usage, threshold, selectionOptions)) return null;
|
|
@@ -652,7 +689,9 @@ function reevaluateAffinityQuota(
|
|
|
652
689
|
return replacement;
|
|
653
690
|
}
|
|
654
691
|
if (strategy !== "quota") return null;
|
|
655
|
-
const
|
|
692
|
+
const recovered = pickAffinityPriorityFailback(config, entry.accountId, now, quotaScope, selectionOptions);
|
|
693
|
+
if (recovered) { entry.lastReevalAt = now; return recovered; }
|
|
694
|
+
const threshold = getEffectiveCodexAutoSwitchThreshold(config, entry.accountId);
|
|
656
695
|
const usage = threshold > 0
|
|
657
696
|
? computeCodexUsageScore(
|
|
658
697
|
getAccountQuota(entry.accountId),
|
|
@@ -773,7 +812,7 @@ export function previewCodexAccountForRequest(
|
|
|
773
812
|
}
|
|
774
813
|
active = pickPriorityPreemption(config, active, now, quotaScope, selectionOptions) ?? active;
|
|
775
814
|
|
|
776
|
-
const threshold = config
|
|
815
|
+
const threshold = getEffectiveCodexAutoSwitchThreshold(config, active);
|
|
777
816
|
if (threshold > 0) {
|
|
778
817
|
const usage = computeCodexUsageScore(
|
|
779
818
|
getAccountQuota(active),
|
|
@@ -932,7 +971,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
932
971
|
if (entry.transientHoldSince !== undefined) delete entry.transientHoldSince;
|
|
933
972
|
if (entry.transientDetourAccountId !== undefined) delete entry.transientDetourAccountId;
|
|
934
973
|
// Periodic quota re-eval: a long-lived bound thread must still switch when
|
|
935
|
-
// it crosses
|
|
974
|
+
// it crosses its effective threshold, but only onto an account that has genuine
|
|
936
975
|
// quota headroom AND is strictly cooler — moving to a destination still over
|
|
937
976
|
// the threshold just trades the warmed prompt-cache prefix for an equally hot
|
|
938
977
|
// account, which is the #4546 ping-pong.
|
|
@@ -944,7 +983,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
944
983
|
// rotation is new-session-only (affinity policy A).
|
|
945
984
|
const cooler = reevaluateAffinityQuota(entry, config, now, quotaScope, selectionOptions);
|
|
946
985
|
if (cooler) {
|
|
947
|
-
if (!isIndependentCodexQuotaScope(quotaScope)) {
|
|
986
|
+
if (!isIndependentCodexQuotaScope(quotaScope) && sharesActiveSelection(cooler, selectionOptions)) {
|
|
948
987
|
promoteActiveCodexAccount(config, cooler);
|
|
949
988
|
}
|
|
950
989
|
bindThreadAffinity(threadId, cooler, now, quotaScope); // rebinds + resets clocks
|
|
@@ -990,7 +1029,9 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
990
1029
|
&& !shouldFailover(config, expiredDetour, now)
|
|
991
1030
|
&& !isCodexAccountSoftAvoided(expiredDetour, now)
|
|
992
1031
|
) {
|
|
993
|
-
if (!isIndependentCodexQuotaScope(quotaScope)
|
|
1032
|
+
if (!isIndependentCodexQuotaScope(quotaScope) && sharesActiveSelection(expiredDetour, selectionOptions)) {
|
|
1033
|
+
promoteActiveCodexAccount(config, expiredDetour);
|
|
1034
|
+
}
|
|
994
1035
|
bindThreadAffinity(threadId, expiredDetour, now, quotaScope);
|
|
995
1036
|
return {
|
|
996
1037
|
status: "selected",
|
|
@@ -1084,7 +1125,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1084
1125
|
// process-local cursor to whoever is actually serving and releases the pin; the
|
|
1085
1126
|
// operator's persisted activeCodexAccountId is left untouched either way, which is
|
|
1086
1127
|
// the thing the preference exists to protect.
|
|
1087
|
-
promoteActiveCodexAccount(config, strategyPick);
|
|
1128
|
+
if (sharesActiveSelection(strategyPick, selectionOptions)) promoteActiveCodexAccount(config, strategyPick);
|
|
1088
1129
|
}
|
|
1089
1130
|
return { status: "selected", accountId: strategyPick, affinity: affinityAfterRelease(threadId, releaseReason) };
|
|
1090
1131
|
}
|
|
@@ -1102,7 +1143,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1102
1143
|
return { status: "none", affinity: affinityOnNoAccount(threadId, releaseReason) };
|
|
1103
1144
|
}
|
|
1104
1145
|
if (!isIndependentCodexQuotaScope(quotaScope) && !modelScopedSelection) {
|
|
1105
|
-
setActiveCodexAccount(config, selected);
|
|
1146
|
+
if (sharesActiveSelection(selected, selectionOptions)) setActiveCodexAccount(config, selected);
|
|
1106
1147
|
}
|
|
1107
1148
|
active = selected;
|
|
1108
1149
|
}
|
|
@@ -1123,7 +1164,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1123
1164
|
&& preserveSharedSelectionForModelDetour
|
|
1124
1165
|
&& activeHealthyForSharedSelection;
|
|
1125
1166
|
if (!isIndependentCodexQuotaScope(quotaScope) && !modelOnlyMove) {
|
|
1126
|
-
setActiveCodexAccount(config, fallback);
|
|
1167
|
+
if (sharesActiveSelection(fallback, selectionOptions)) setActiveCodexAccount(config, fallback);
|
|
1127
1168
|
}
|
|
1128
1169
|
active = fallback;
|
|
1129
1170
|
} else if (
|
|
@@ -1157,6 +1198,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1157
1198
|
if (
|
|
1158
1199
|
!preserveSharedSelectionForModelDetour
|
|
1159
1200
|
&& !isIndependentCodexQuotaScope(quotaScope)
|
|
1201
|
+
&& sharesActiveSelection(preempted, selectionOptions)
|
|
1160
1202
|
) {
|
|
1161
1203
|
// Preemption is an automatic pick competing with the operator, so it yields.
|
|
1162
1204
|
if (!manualPreferenceBlocks(POOL_KEY_CODEX, preempted)) {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type CodexAccountUsabilityOptions,
|
|
28
28
|
} from "./account-usability";
|
|
29
29
|
import { isCodexAccountPaused } from "./account-pause";
|
|
30
|
+
import { getEffectiveCodexAutoSwitchThreshold } from "./account-auto-switch";
|
|
30
31
|
import { slugEquals } from "../providers/slug-codec";
|
|
31
32
|
import { isThreadSpawnRequest } from "../server/effort-policy";
|
|
32
33
|
import { PROVIDER_REGISTRY } from "../providers/registry";
|
|
@@ -157,8 +158,8 @@ export function buildSubagentModelChain(
|
|
|
157
158
|
return normalizedChain(primary, config, extraFallback);
|
|
158
159
|
}
|
|
159
160
|
|
|
160
|
-
function quotaThreshold(config: OcxConfig): number {
|
|
161
|
-
const threshold = config
|
|
161
|
+
function quotaThreshold(config: OcxConfig, accountId: string): number {
|
|
162
|
+
const threshold = getEffectiveCodexAutoSwitchThreshold(config, accountId);
|
|
162
163
|
return threshold > 0 ? threshold : Number.POSITIVE_INFINITY;
|
|
163
164
|
}
|
|
164
165
|
|
|
@@ -233,7 +234,7 @@ export function isNativeModelQuotaExhausted(
|
|
|
233
234
|
// rather than letting the scorer read wall time - the two would silently diverge.
|
|
234
235
|
const usage = computeCodexUsageScore(quota, getPoolAccountPlan(config, resolvedAccountId), now);
|
|
235
236
|
if (usage >= CODEX_UNKNOWN_USAGE_SCORE) return false;
|
|
236
|
-
return usage >= quotaThreshold(config);
|
|
237
|
+
return usage >= quotaThreshold(config, resolvedAccountId);
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
export function isModelHealthBlocked(
|
package/src/codex/sync.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { admitCodexWrite, type CodexAdmission } from "./admission";
|
|
15
15
|
import type { CodexCatalogSyncOptions } from "./catalog/sync";
|
|
16
16
|
import { resetCodexAppServerCatalogStateCache } from "./app-server-processes";
|
|
17
|
+
import { providerUsesReasoningMetadata, refreshReasoningMetadata } from "../providers/reasoning-metadata";
|
|
17
18
|
|
|
18
19
|
export interface CodexSyncResult {
|
|
19
20
|
/**
|
|
@@ -67,13 +68,21 @@ interface CodexSyncDeps {
|
|
|
67
68
|
admitCodexWrite?: () => CodexSyncAdmission;
|
|
68
69
|
currentExternalCodexModelProvider?: typeof currentExternalCodexModelProvider;
|
|
69
70
|
collectCodexHomeDiagnostic?: typeof collectOrcaCodexHomeDiagnostic;
|
|
71
|
+
refreshReasoningMetadata?: typeof refreshReasoningMetadata;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
const defaultDeps: CodexSyncDeps = {
|
|
73
75
|
refreshCodexModelCatalog,
|
|
74
76
|
injectCodexConfig,
|
|
77
|
+
refreshReasoningMetadata,
|
|
75
78
|
};
|
|
76
79
|
|
|
80
|
+
async function refreshReasoningMetadataForSync(config: OcxConfig, deps: CodexSyncDeps): Promise<void> {
|
|
81
|
+
if (Object.values(config.providers).some(providerUsesReasoningMetadata)) {
|
|
82
|
+
await deps.refreshReasoningMetadata?.({ waitMs: 2_000 });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
function reportCodexHomeTarget(
|
|
78
87
|
log: Pick<Console, "log" | "error"> | null,
|
|
79
88
|
collectDiagnostic: typeof collectOrcaCodexHomeDiagnostic,
|
|
@@ -234,6 +243,7 @@ export async function syncModelsToCodex(
|
|
|
234
243
|
}
|
|
235
244
|
|
|
236
245
|
applyProxyEnv(config); // `ocx ensure`/`ocx sync` fetch provider models outside the server process
|
|
246
|
+
await refreshReasoningMetadataForSync(config, deps);
|
|
237
247
|
let added = 0;
|
|
238
248
|
let catalogPath: string | null = null;
|
|
239
249
|
let catalogPathForInjection: string | null | undefined;
|
|
@@ -339,6 +349,7 @@ async function refreshCatalogForSync(
|
|
|
339
349
|
let refreshOutcome: "committed" | "refused" | undefined;
|
|
340
350
|
let comboOmissions: ComboCatalogOmission[] = [];
|
|
341
351
|
try {
|
|
352
|
+
await refreshReasoningMetadataForSync(config, deps);
|
|
342
353
|
const cat = await deps.refreshCodexModelCatalog(config, undefined, catalogOptions);
|
|
343
354
|
refreshOutcome = cat.refreshOutcome;
|
|
344
355
|
added = cat.added;
|
|
@@ -18,6 +18,10 @@ export interface WindowsInstallationFileRequest {
|
|
|
18
18
|
readonly path: string;
|
|
19
19
|
readonly maxBytes: number;
|
|
20
20
|
readonly hashOnly?: boolean;
|
|
21
|
+
/** Validate and hold the path without reading its contents. */
|
|
22
|
+
readonly metadataOnly?: boolean;
|
|
23
|
+
/** Read at most maxBytes from the start instead of refusing an oversized file. */
|
|
24
|
+
readonly prefixOnly?: boolean;
|
|
21
25
|
}
|
|
22
26
|
export interface WindowsInstallationFileIdentity {
|
|
23
27
|
readonly volumeSerial: string;
|
|
@@ -31,10 +35,20 @@ export type WindowsInstallationFilesResult =
|
|
|
31
35
|
path: string; identity: WindowsInstallationFileIdentity; bytes: Uint8Array; digest: string;
|
|
32
36
|
}[] }
|
|
33
37
|
| { kind: "refused"; reason: "unsupported-platform" | "invalid-request" | "native-api-unavailable"
|
|
34
|
-
| "volume-unavailable" | "open-refused" | "reparse-point" | "not-regular-file"
|
|
38
|
+
| "volume-unavailable" | "not-found" | "open-refused" | "reparse-point" | "not-regular-file"
|
|
35
39
|
| "size-limit" | "read-failed" | "identity-changed" | "inspection-failed" };
|
|
36
40
|
|
|
37
41
|
type Refusal = Extract<WindowsInstallationFilesResult, { kind: "refused" }>["reason"];
|
|
42
|
+
/** Only these two NTSTATUS values prove that a candidate or its ancestor is absent. */
|
|
43
|
+
export function ntCreateFileRefusal(status: number): Refusal {
|
|
44
|
+
switch (status >>> 0) {
|
|
45
|
+
case 0xc0000034: // STATUS_OBJECT_NAME_NOT_FOUND
|
|
46
|
+
case 0xc000003a: // STATUS_OBJECT_PATH_NOT_FOUND
|
|
47
|
+
return "not-found";
|
|
48
|
+
default:
|
|
49
|
+
return "open-refused";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
38
52
|
class InspectionRefusal extends Error {
|
|
39
53
|
constructor(readonly reason: Refusal) { super(reason); }
|
|
40
54
|
}
|
|
@@ -76,6 +90,8 @@ export async function inspectWindowsInstallationFiles(
|
|
|
76
90
|
const parsedPath = request && components(request.path);
|
|
77
91
|
if (!parsedPath || !Number.isSafeInteger(request.maxBytes) || request.maxBytes < 0
|
|
78
92
|
|| (request.hashOnly !== undefined && typeof request.hashOnly !== "boolean")
|
|
93
|
+
|| (request.metadataOnly !== undefined && typeof request.metadataOnly !== "boolean")
|
|
94
|
+
|| (request.prefixOnly !== undefined && typeof request.prefixOnly !== "boolean")
|
|
79
95
|
|| request.maxBytes > (request.hashOnly ? 256 * MIB : MIB)) return null;
|
|
80
96
|
ceiling += request.maxBytes;
|
|
81
97
|
return parsedPath;
|
|
@@ -163,7 +179,7 @@ export async function inspectWindowsInstallationFiles(
|
|
|
163
179
|
// Share READ only: while held, writes/reparse edits and delete/rename opens are refused.
|
|
164
180
|
const result = nt!.symbols.NtCreateFile!(ffi.ptr(output), 0x100081, ffi.ptr(attributes),
|
|
165
181
|
ffi.ptr(status), null, 0, 1, 1, 0x200020 | (directory ? 1 : 0), null, 0);
|
|
166
|
-
if (result < 0) throw new InspectionRefusal(
|
|
182
|
+
if (result < 0) throw new InspectionRefusal(ntCreateFileRefusal(result));
|
|
167
183
|
const handle = keep(output.readBigUInt64LE(0));
|
|
168
184
|
inspect(handle, directory);
|
|
169
185
|
return handle;
|
|
@@ -184,29 +200,35 @@ export async function inspectWindowsInstallationFiles(
|
|
|
184
200
|
}
|
|
185
201
|
const handle = relativeOpen(parent, names[names.length - 1]!, false);
|
|
186
202
|
const identity = inspect(handle, false);
|
|
187
|
-
if (
|
|
203
|
+
if (!request.metadataOnly && !(request.prefixOnly && !request.hashOnly)
|
|
204
|
+
&& identity.size > request.maxBytes) throw new InspectionRefusal("size-limit");
|
|
188
205
|
return { request, handle, identity };
|
|
189
206
|
});
|
|
190
207
|
openedForTests?.();
|
|
191
208
|
const observed = files.map(({ request, handle, identity }) => {
|
|
192
209
|
const hash = createHash("sha256");
|
|
193
|
-
|
|
194
|
-
const
|
|
210
|
+
if (request.metadataOnly) return { path: request.path, identity, bytes: new Uint8Array(), digest: "" };
|
|
211
|
+
const truncated = Boolean(request.prefixOnly) && !request.hashOnly && identity.size > request.maxBytes;
|
|
212
|
+
const readLimit = truncated ? request.maxBytes : identity.size;
|
|
213
|
+
const bytes = request.hashOnly ? new Uint8Array() : new Uint8Array(readLimit);
|
|
214
|
+
const chunk = Buffer.alloc(Math.min(MIB, Math.max(1, readLimit)));
|
|
195
215
|
const read = Buffer.alloc(4);
|
|
196
216
|
let offset = 0;
|
|
197
|
-
while (offset <
|
|
198
|
-
const length = Math.min(chunk.length,
|
|
217
|
+
while (offset < readLimit) {
|
|
218
|
+
const length = Math.min(chunk.length, readLimit - offset);
|
|
199
219
|
if (!k.ReadFile!(handle, ffi.ptr(chunk), length, ffi.ptr(read), null)) throw new InspectionRefusal("read-failed");
|
|
200
220
|
const count = read.readUInt32LE(0);
|
|
201
221
|
if (!count || count > length) throw new InspectionRefusal("read-failed");
|
|
202
|
-
hash.update(chunk.subarray(0, count));
|
|
203
222
|
if (!request.hashOnly) bytes.set(chunk.subarray(0, count), offset);
|
|
223
|
+
if (!truncated) hash.update(chunk.subarray(0, count));
|
|
204
224
|
offset += count;
|
|
205
225
|
}
|
|
206
|
-
if (!
|
|
207
|
-
|
|
226
|
+
if (!truncated) {
|
|
227
|
+
if (!k.ReadFile!(handle, ffi.ptr(chunk), 1, ffi.ptr(read), null) || read.readUInt32LE(0) !== 0) {
|
|
228
|
+
throw new InspectionRefusal("identity-changed");
|
|
229
|
+
}
|
|
208
230
|
}
|
|
209
|
-
return { path: request.path, identity, bytes, digest: hash.digest("hex") };
|
|
231
|
+
return { path: request.path, identity, bytes, digest: truncated ? "" : hash.digest("hex") };
|
|
210
232
|
});
|
|
211
233
|
for (const file of files) {
|
|
212
234
|
if (JSON.stringify(inspect(file.handle, false)) !== JSON.stringify(file.identity)) {
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
* enumeration of those was already incomplete once. Hashing the computed
|
|
21
21
|
* candidate bytes closes the class instead of the instance — an input that
|
|
22
22
|
* changes the output changes the id whether or not anyone remembered to list it.
|
|
23
|
+
* The pre-lock comparison id admits the original candidate. If a native feature
|
|
24
|
+
* transition changes that candidate under the lock, the coordinator publishes
|
|
25
|
+
* a new id derived from the bytes it commits; admission remains the stale-input
|
|
26
|
+
* guard and is not reused as a committed-byte witness.
|
|
23
27
|
*/
|
|
24
28
|
import { createHash } from "node:crypto";
|
|
25
29
|
|
package/src/combos/request.ts
CHANGED
|
@@ -110,9 +110,13 @@ export function concreteComboRequestBody(
|
|
|
110
110
|
return clone;
|
|
111
111
|
}
|
|
112
112
|
if (reasoning === undefined) {
|
|
113
|
-
clone.reasoning = { effort: resolvedEffort };
|
|
113
|
+
clone.reasoning = { effort: resolvedEffort, summary: "auto" };
|
|
114
114
|
} else {
|
|
115
|
-
clone.reasoning = {
|
|
115
|
+
clone.reasoning = {
|
|
116
|
+
...(reasoning as Record<string, unknown>),
|
|
117
|
+
effort: resolvedEffort,
|
|
118
|
+
...((reasoning as Record<string, unknown>).summary === undefined ? { summary: "auto" } : {}),
|
|
119
|
+
};
|
|
116
120
|
}
|
|
117
121
|
return clone;
|
|
118
122
|
}
|
|
@@ -4,6 +4,7 @@ import { getConfigDir } from "../config/paths";
|
|
|
4
4
|
import {
|
|
5
5
|
TIMELINE_HOURS,
|
|
6
6
|
isTimelineModelId,
|
|
7
|
+
normalizeTimelineModelId,
|
|
7
8
|
type TimelineAggregation,
|
|
8
9
|
type TimelineGrouping,
|
|
9
10
|
type TimelineMetric,
|
|
@@ -104,7 +105,11 @@ export function applyCompanionSettingsPatch(
|
|
|
104
105
|
const error = validateValue(key as keyof CompanionSettings, values[key]);
|
|
105
106
|
if (error) return invalid(error);
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
+
const next = { ...current, ...values } as CompanionSettings;
|
|
109
|
+
// Every companion re-filters timeline rows against these ids, so a selection saved while the chart
|
|
110
|
+
// still split pool accounts has to name the merged row the timeline now returns.
|
|
111
|
+
if (next.models !== null) next.models = [...new Set(next.models.map(normalizeTimelineModelId))];
|
|
112
|
+
return next;
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
export function loadCompanionSettings(): { settings: CompanionSettings; updatedAt: number | null; corrupt?: true } {
|
|
@@ -277,8 +277,9 @@ function atomicWriteFileToTarget(
|
|
|
277
277
|
target: string,
|
|
278
278
|
io?: AtomicWriteIO,
|
|
279
279
|
hooks: AtomicWriteHooks = {},
|
|
280
|
+
recordOwnership = true,
|
|
280
281
|
): void {
|
|
281
|
-
recordOwnedConfigPath(getConfigDir(), path);
|
|
282
|
+
if (recordOwnership) recordOwnedConfigPath(getConfigDir(), path);
|
|
282
283
|
assertResolvedTargetAllowed(path, target);
|
|
283
284
|
const tmp = `${target}.ocx.${process.pid}.${nextAtomicTempSequence()}.tmp`;
|
|
284
285
|
let hardened = false;
|
|
@@ -399,6 +400,16 @@ export function atomicWriteFileNoFollow(
|
|
|
399
400
|
atomicWriteFileToTarget(path, content, join(resolveWriteTarget(dirname(path)), basename(path)), io, hooks);
|
|
400
401
|
}
|
|
401
402
|
|
|
403
|
+
/**
|
|
404
|
+
* The no-follow replacement above, for rewriting a file whose uninstall ownership must stay as
|
|
405
|
+
* it was: it does not record the path in the owner manifest. Used to rewrite the OAuth downgrade
|
|
406
|
+
* backup, which a pre-registration install may have left deliberately unclaimed; claiming it
|
|
407
|
+
* here would let a later uninstall delete recovery data it never owned.
|
|
408
|
+
*/
|
|
409
|
+
export function atomicWriteFileNoFollowUnclaimed(path: string, content: string): void {
|
|
410
|
+
atomicWriteFileToTarget(path, content, join(resolveWriteTarget(dirname(path)), basename(path)), undefined, {}, false);
|
|
411
|
+
}
|
|
412
|
+
|
|
402
413
|
export interface AtomicWriteAsyncIO {
|
|
403
414
|
write: (path: string, content: string) => void | Promise<void>;
|
|
404
415
|
harden: (path: string) => void | Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { setConfiguredNativeOpenAiModels } from "../codex/catalog/native-models";
|
|
2
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers-destination";
|
|
3
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
4
|
+
import { refreshUserCostOverlays } from "../usage/user-cost-overlays";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bare `gpt-*` ids listed under `providers.openai.models` that become configured natives.
|
|
8
|
+
*
|
|
9
|
+
* Only the canonical Codex forward provider qualifies (an omitted authMode is the registry's
|
|
10
|
+
* forward default): `providers.openai.models` is otherwise ignored for that provider, and on any
|
|
11
|
+
* other shape the list means routed models, which must not turn into native rows.
|
|
12
|
+
*/
|
|
13
|
+
export function configuredNativeOpenAiModelIds(config: Pick<OcxConfig, "providers">): string[] {
|
|
14
|
+
const provider = config.providers?.[OPENAI_CODEX_PROVIDER_ID] as OcxProviderConfig | undefined;
|
|
15
|
+
if (!provider || provider.disabled === true || !Array.isArray(provider.models)) return [];
|
|
16
|
+
const canonical = provider.authMode === undefined ? { ...provider, authMode: "forward" as const } : provider;
|
|
17
|
+
if (!isCanonicalOpenAiForwardProvider(canonical)) return [];
|
|
18
|
+
return provider.models.flatMap(id => typeof id === "string" ? [id.trim()] : []);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Rebuild every process-local registry derived from a committed config: user price overlays and
|
|
23
|
+
* configured native GPT models. Called wherever a loaded, persisted or reconciled config becomes
|
|
24
|
+
* current, so the two never disagree about which config is live.
|
|
25
|
+
*/
|
|
26
|
+
export function refreshConfigDerivedRegistries(config: OcxConfig): void {
|
|
27
|
+
refreshUserCostOverlays(config);
|
|
28
|
+
setConfiguredNativeOpenAiModels(configuredNativeOpenAiModelIds(config));
|
|
29
|
+
}
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
clientConnectionSchema,
|
|
53
53
|
CODEX_ACCOUNT_PIN_PATTERN,
|
|
54
54
|
codexAccountPrioritiesSchema,
|
|
55
|
+
codexAccountAutoSwitchThresholdsSchema,
|
|
55
56
|
codexPoolSchema,
|
|
56
57
|
codexQuotaAutoRefreshSchema,
|
|
57
58
|
credentialGroupsSchema,
|
|
@@ -347,6 +348,13 @@ function codexAccountPrioritiesError(value: unknown): string | null {
|
|
|
347
348
|
return schemaDiagnosticsError(parsed.error).replace("schema_invalid: ", "schema_invalid: codexAccountPriorities.");
|
|
348
349
|
}
|
|
349
350
|
}
|
|
351
|
+
if (raw.codexAccountAutoSwitchThresholds !== undefined) {
|
|
352
|
+
const parsed = codexAccountAutoSwitchThresholdsSchema.safeParse(raw.codexAccountAutoSwitchThresholds);
|
|
353
|
+
if (!parsed.success) {
|
|
354
|
+
return schemaDiagnosticsError(parsed.error)
|
|
355
|
+
.replace("schema_invalid: ", "schema_invalid: codexAccountAutoSwitchThresholds.");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
350
358
|
// Tested as a string rather than coerced: `String(123)` matches the id pattern, so a
|
|
351
359
|
// coercing guard waves a non-string pin through to the schema, where `.catch(undefined)`
|
|
352
360
|
// drops it and reports the write as a success — the exact silent-degrade this guards.
|
|
@@ -357,6 +365,14 @@ function codexAccountPrioritiesError(value: unknown): string | null {
|
|
|
357
365
|
return null;
|
|
358
366
|
}
|
|
359
367
|
|
|
368
|
+
function codexAccountPriorityFailbackError(value: unknown): string | null {
|
|
369
|
+
const raw = rawConfigRecord(value);
|
|
370
|
+
if (!raw || !Object.hasOwn(raw, "codexAccountPriorityFailback")) return null;
|
|
371
|
+
const enabled = raw.codexAccountPriorityFailback;
|
|
372
|
+
if (enabled === undefined || typeof enabled === "boolean") return null;
|
|
373
|
+
return "schema_invalid: codexAccountPriorityFailback: must be a boolean or omitted";
|
|
374
|
+
}
|
|
375
|
+
|
|
360
376
|
/**
|
|
361
377
|
* Same reasoning as {@link codexAccountPrioritiesError}, plus one of its own. The read
|
|
362
378
|
* path drops an invalid grouping, so a degraded write would erase a declaration the
|
|
@@ -595,6 +611,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
595
611
|
?? codexPoolError(value)
|
|
596
612
|
?? googleAntigravityStaticCatalogVersionError(value)
|
|
597
613
|
?? codexAccountPrioritiesError(value)
|
|
614
|
+
?? codexAccountPriorityFailbackError(value)
|
|
598
615
|
?? poolCredentialGroupsError(value)
|
|
599
616
|
?? codexQuotaAutoRefreshError(value)
|
|
600
617
|
?? codexAccountPickerEnabledError(value)
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
2
2
|
import { configReasoningPinsConfigError } from "./provider-validation";
|
|
3
3
|
import { adoptCustomModelCatalogMigration, projectCustomModelCatalogMigration } from "../codex/custom-model-catalog-migration";
|
|
4
|
-
import { refreshPreservedProviderOwner
|
|
4
|
+
import { refreshPreservedProviderOwner } from "../usage/user-cost-overlays";
|
|
5
|
+
import { refreshConfigDerivedRegistries } from "./derived-registries";
|
|
5
6
|
import {
|
|
7
|
+
applyConfigObjectChildDeletions,
|
|
8
|
+
clearPendingConfigObjectChildDeletions,
|
|
9
|
+
prepareConfigObjectChildDeletionRebase,
|
|
6
10
|
clearPendingConfigTopLevelDeletions,
|
|
7
11
|
configHasRebaseProvenance,
|
|
8
12
|
configRebaseDeletionKeys,
|
|
@@ -357,12 +361,14 @@ export function reconcileLiveConfigFromDisk(config: OcxConfig, persistedBaseline
|
|
|
357
361
|
...(persisted.hostname !== undefined ? { hostname: persisted.hostname } : {}),
|
|
358
362
|
});
|
|
359
363
|
|
|
364
|
+
const childDeletions = prepareConfigObjectChildDeletionRebase(config);
|
|
360
365
|
reconcileConfigRecord(
|
|
361
366
|
config as unknown as Record<string, unknown>,
|
|
362
367
|
persistedBaseline as unknown as Record<string, unknown>,
|
|
363
368
|
persisted as unknown as Record<string, unknown>,
|
|
364
369
|
new Set(["hostname", "port", ...(claudeGuardArmed ? ["claudeCode"] : [])]),
|
|
365
370
|
);
|
|
371
|
+
applyConfigObjectChildDeletions(config, childDeletions);
|
|
366
372
|
|
|
367
373
|
if (claudeGuardArmed && !pendingLiveClaudeMutation) {
|
|
368
374
|
if (persisted.claudeCode === undefined) delete config.claudeCode;
|
|
@@ -372,7 +378,7 @@ export function reconcileLiveConfigFromDisk(config: OcxConfig, persistedBaseline
|
|
|
372
378
|
// The reconciliation may have adopted a providers.<name>.modelCosts edit made
|
|
373
379
|
// by a cooperating process while the OAuth login was pending; keep the overlay
|
|
374
380
|
// registry (and the usage-cache overlay version) in sync with the live config.
|
|
375
|
-
|
|
381
|
+
refreshConfigDerivedRegistries(config);
|
|
376
382
|
}
|
|
377
383
|
|
|
378
384
|
/**
|
|
@@ -420,6 +426,7 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
420
426
|
const pinError = configReasoningPinsConfigError(config);
|
|
421
427
|
if (pinError) throw new Error(pinError);
|
|
422
428
|
withConfigMutationLockSync(() => {
|
|
429
|
+
const childDeletions = prepareConfigObjectChildDeletionRebase(config);
|
|
423
430
|
const bindingBaseline = persistedLiveServerBinding.get(config);
|
|
424
431
|
// One authoritative pre-write read feeds both the live-config reconciliation and
|
|
425
432
|
// custom-model deletion migration. A second read could observe different bytes.
|
|
@@ -477,6 +484,7 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
477
484
|
}
|
|
478
485
|
}
|
|
479
486
|
}
|
|
487
|
+
applyConfigObjectChildDeletions(config, childDeletions);
|
|
480
488
|
if (claudeCodeBaseline.has(config)) {
|
|
481
489
|
if (onDisk !== undefined) {
|
|
482
490
|
const baseline = claudeCodeBaseline.get(config);
|
|
@@ -516,6 +524,7 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
516
524
|
else config.configRebaseProvenance = structuredClone(projectedConfig.configRebaseProvenance);
|
|
517
525
|
liveConfigBaseline.set(config, structuredClone(projectedConfig));
|
|
518
526
|
}
|
|
527
|
+
clearPendingConfigObjectChildDeletions(config);
|
|
519
528
|
clearPendingConfigTopLevelDeletions(config);
|
|
520
529
|
});
|
|
521
530
|
}
|
|
@@ -17,7 +17,7 @@ import { MODEL_ALIAS_PATTERN } from "../providers/default-aliases";
|
|
|
17
17
|
import { MODEL_DISCOVERY_MAX_MODELS } from "../providers/model-discovery-limits";
|
|
18
18
|
import { getProviderRegistryEntry, providerMatchesRegistryTransport, registryModelServiceTierCapabilityApplies } from "../providers/registry";
|
|
19
19
|
import { isCodexReasoningEffort } from "../reasoning-effort";
|
|
20
|
-
import {
|
|
20
|
+
import { refreshConfigDerivedRegistries } from "./derived-registries";
|
|
21
21
|
import { type OcxClaudeCodeConfig, type OcxConfig } from "../types";
|
|
22
22
|
import {
|
|
23
23
|
agentTaskRecoverySchema,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
isUsableApiKeySecret,
|
|
27
27
|
managementIngressSchema,
|
|
28
28
|
codexPoolSchema,
|
|
29
|
+
codexAccountAutoSwitchThresholdsSchema,
|
|
29
30
|
providerModelCostsConfigError,
|
|
30
31
|
credentialGroupsSchema,
|
|
31
32
|
hubConfigSchema,
|
|
@@ -389,6 +390,12 @@ export function degradedCodexAccountPriorityWarnings(rawParsed: unknown, validat
|
|
|
389
390
|
if (raw !== undefined && validated.codexAccountPriorities === undefined) {
|
|
390
391
|
warnings.push("codexAccountPriorities is invalid (expected account ids mapped to integers between -100 and 100) — account selection order is disabled");
|
|
391
392
|
}
|
|
393
|
+
const rawThresholds = record?.codexAccountAutoSwitchThresholds;
|
|
394
|
+
if (rawThresholds !== undefined && !codexAccountAutoSwitchThresholdsSchema.safeParse(rawThresholds).success) {
|
|
395
|
+
warnings.push(validated.codexAccountAutoSwitchThresholds === undefined
|
|
396
|
+
? "codexAccountAutoSwitchThresholds is invalid (expected account ids mapped to integers between 0 and 100) — per-account usage thresholds are disabled"
|
|
397
|
+
: "codexAccountAutoSwitchThresholds contains invalid entries (expected account ids mapped to integers between 0 and 100) — invalid entries were ignored");
|
|
398
|
+
}
|
|
392
399
|
return warnings;
|
|
393
400
|
}
|
|
394
401
|
|
|
@@ -934,6 +941,6 @@ export function sanitizeModelDisplayNamesForLoad(raw: unknown): void {
|
|
|
934
941
|
|
|
935
942
|
/** Refresh the user cost-overlay registry from `config` and return it unchanged. */
|
|
936
943
|
export function withRefreshedCostOverlays(config: OcxConfig): OcxConfig {
|
|
937
|
-
|
|
944
|
+
refreshConfigDerivedRegistries(config);
|
|
938
945
|
return config;
|
|
939
946
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { configReasoningPinsConfigError } from "./provider-validation";
|
|
3
3
|
import type { OcxConfig } from "../types";
|
|
4
|
-
import {
|
|
4
|
+
import { withPreservedDiskOnlyProviders } from "../usage/user-cost-overlays";
|
|
5
|
+
import { refreshConfigDerivedRegistries } from "./derived-registries";
|
|
5
6
|
import { atomicWriteFile, isMissingPathError } from "./atomic-write";
|
|
6
7
|
import { getConfigPath } from "./paths";
|
|
7
8
|
import { configRebaseDeletionKeys, projectConfigRebaseProvenance } from "./rebase-provenance";
|
|
@@ -81,12 +82,12 @@ export function persistConfigUnlocked(config: OcxConfig): boolean {
|
|
|
81
82
|
// the same bytes (e.g. before a proxy notification), and Logs/Usage must
|
|
82
83
|
// adopt the overlay without waiting for a changed save or restart.
|
|
83
84
|
if (unchanged) {
|
|
84
|
-
|
|
85
|
+
refreshConfigDerivedRegistries(persisted);
|
|
85
86
|
return false;
|
|
86
87
|
}
|
|
87
88
|
atomicWriteFile(configPath, bytes);
|
|
88
89
|
// For changed saves, refresh only AFTER the write succeeded so a failed
|
|
89
90
|
// write cannot leave estimates reflecting configuration never persisted.
|
|
90
|
-
|
|
91
|
+
refreshConfigDerivedRegistries(persisted);
|
|
91
92
|
return true;
|
|
92
93
|
}
|