@bitkyc08/opencodex 2.63.0-preview.20260923 → 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-B03pW-Uf.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-EJxiUFMq.js +0 -50
- package/gui/dist/assets/index-BmJwNBHL.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-IftE60UK.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
|
@@ -100,6 +100,7 @@ export interface CodexLogGuardMaintenanceDeps {
|
|
|
100
100
|
) => CodexLogGuardLockOutcome<T>;
|
|
101
101
|
quickCheck?: (db: Database) => string[];
|
|
102
102
|
openDatabase?: (databasePath: string, flags: number) => Database;
|
|
103
|
+
statDatabasePath?: (databasePath: string) => DatabasePathStat;
|
|
103
104
|
batchPages?: number;
|
|
104
105
|
maxPagesPerRun?: number;
|
|
105
106
|
}
|
|
@@ -112,15 +113,30 @@ interface CheckpointRow {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
interface DatabaseFileIdentity {
|
|
115
|
-
dev:
|
|
116
|
-
ino:
|
|
116
|
+
dev: bigint;
|
|
117
|
+
ino: bigint;
|
|
117
118
|
realPath: string;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
|
|
121
|
+
interface DatabasePathStat {
|
|
122
|
+
dev?: bigint | null;
|
|
123
|
+
ino?: bigint | null;
|
|
124
|
+
isFile(): boolean;
|
|
125
|
+
isSymbolicLink(): boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function databasePathIdentity(
|
|
129
|
+
databasePath: string,
|
|
130
|
+
deps: CodexLogGuardMaintenanceDeps,
|
|
131
|
+
): DatabaseFileIdentity | null {
|
|
121
132
|
try {
|
|
122
|
-
const stat =
|
|
123
|
-
|
|
133
|
+
const stat = deps.statDatabasePath
|
|
134
|
+
? deps.statDatabasePath(databasePath)
|
|
135
|
+
: lstatSync(databasePath, { bigint: true });
|
|
136
|
+
// Windows file IDs are 64-bit; a number can round adjacent IDs equal.
|
|
137
|
+
// Missing or zero means the filesystem did not supply an identity, so fail closed.
|
|
138
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
139
|
+
|| typeof stat.dev !== "bigint" || typeof stat.ino !== "bigint" || stat.ino === 0n) return null;
|
|
124
140
|
const realPath = realpathSync.native(databasePath);
|
|
125
141
|
if (!sameLogGuardPathIdentity(realPath, databasePath)) return null;
|
|
126
142
|
return { dev: stat.dev, ino: stat.ino, realPath };
|
|
@@ -129,15 +145,16 @@ function databasePathIdentity(databasePath: string): DatabaseFileIdentity | null
|
|
|
129
145
|
}
|
|
130
146
|
}
|
|
131
147
|
|
|
132
|
-
function databasePathIsSafe(databasePath: string): boolean {
|
|
133
|
-
return databasePathIdentity(databasePath) !== null;
|
|
148
|
+
function databasePathIsSafe(databasePath: string, deps: CodexLogGuardMaintenanceDeps): boolean {
|
|
149
|
+
return databasePathIdentity(databasePath, deps) !== null;
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
function databasePathStillMatches(
|
|
137
153
|
databasePath: string,
|
|
138
154
|
before: DatabaseFileIdentity,
|
|
155
|
+
deps: CodexLogGuardMaintenanceDeps,
|
|
139
156
|
): boolean {
|
|
140
|
-
const after = databasePathIdentity(databasePath);
|
|
157
|
+
const after = databasePathIdentity(databasePath, deps);
|
|
141
158
|
return after !== null
|
|
142
159
|
&& after.dev === before.dev
|
|
143
160
|
&& after.ino === before.ino
|
|
@@ -228,15 +245,15 @@ function runCompaction(
|
|
|
228
245
|
let probeOpen = false;
|
|
229
246
|
let reportBusyPartial: (() => CodexLogGuardCompactionResult) | undefined;
|
|
230
247
|
try {
|
|
231
|
-
const beforeOpenIdentity = databasePathIdentity(databasePath);
|
|
248
|
+
const beforeOpenIdentity = databasePathIdentity(databasePath, deps);
|
|
232
249
|
if (!beforeOpenIdentity) return { ok: false, error: "unsafe_path" };
|
|
233
250
|
const openDatabase = deps.openDatabase
|
|
234
251
|
?? ((path: string, flags: number) => new Database(path, flags));
|
|
235
252
|
db = openDatabase(databasePath, sqliteConstants.SQLITE_OPEN_READWRITE);
|
|
236
253
|
// The path is user-writable foreign state. Re-check its regular-file,
|
|
237
|
-
// canonical-path and st_dev/st_ino identity immediately after
|
|
238
|
-
// it, before issuing any pragma or write-capable statement.
|
|
239
|
-
if (!databasePathStillMatches(databasePath, beforeOpenIdentity)) {
|
|
254
|
+
// canonical-path and full-width st_dev/st_ino identity immediately after
|
|
255
|
+
// SQLite opens it, before issuing any pragma or write-capable statement.
|
|
256
|
+
if (!databasePathStillMatches(databasePath, beforeOpenIdentity, deps)) {
|
|
240
257
|
return { ok: false, error: "unsafe_path" };
|
|
241
258
|
}
|
|
242
259
|
db.exec("PRAGMA busy_timeout = 0");
|
|
@@ -373,7 +390,7 @@ export function compactCodexLogs(
|
|
|
373
390
|
if (inspection.capabilities.reclaim.state !== "supported") {
|
|
374
391
|
return { ok: false, error: "unsupported_schema" };
|
|
375
392
|
}
|
|
376
|
-
if (!databasePathIsSafe(databasePath)) return { ok: false, error: "unsafe_path" };
|
|
393
|
+
if (!databasePathIsSafe(databasePath, deps)) return { ok: false, error: "unsafe_path" };
|
|
377
394
|
|
|
378
395
|
const checkProcesses = deps.processCheck ?? listRunningCodexProcesses;
|
|
379
396
|
const firstRefusal = processRefusal(checkProcesses());
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
constants,
|
|
4
|
+
existsSync,
|
|
5
|
+
fstatSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
} from "node:fs";
|
|
2
12
|
import path, { dirname, join, resolve } from "node:path";
|
|
3
13
|
import { expandUserPath } from "../config";
|
|
4
14
|
import { defaultCodexHome } from "./home";
|
|
@@ -8,6 +18,34 @@ import { truncateRetainedUtf8 } from "../lib/admission";
|
|
|
8
18
|
const OCX_SECTION_MARKER = "# Auto-injected by opencodex";
|
|
9
19
|
const DIAGNOSTICS_CACHE_TTL_MS = 30_000;
|
|
10
20
|
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
21
|
+
const MAX_PROJECT_CONFIG_BYTES = 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
export function readBoundedProjectConfig(filePath: string): string | null {
|
|
24
|
+
let fd: number | undefined;
|
|
25
|
+
try {
|
|
26
|
+
// A candidate can become a FIFO after discovery; opening must not wait for a writer.
|
|
27
|
+
fd = openSync(filePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
28
|
+
const stat = fstatSync(fd);
|
|
29
|
+
if (!stat.isFile() || stat.size > MAX_PROJECT_CONFIG_BYTES) return null;
|
|
30
|
+
|
|
31
|
+
const buffer = Buffer.allocUnsafe(stat.size + 1);
|
|
32
|
+
let bytesRead = 0;
|
|
33
|
+
while (bytesRead < buffer.length) {
|
|
34
|
+
const count = readSync(fd, buffer, bytesRead, buffer.length - bytesRead, null);
|
|
35
|
+
if (count === 0) break;
|
|
36
|
+
bytesRead += count;
|
|
37
|
+
}
|
|
38
|
+
// Reject a file that changed while it was read, including a same-size rewrite.
|
|
39
|
+
const after = fstatSync(fd);
|
|
40
|
+
if (bytesRead !== stat.size || after.size !== stat.size
|
|
41
|
+
|| after.mtimeMs !== stat.mtimeMs || after.ctimeMs !== stat.ctimeMs) return null;
|
|
42
|
+
return buffer.toString("utf-8", 0, bytesRead);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
} finally {
|
|
46
|
+
if (fd !== undefined) closeSync(fd);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
11
49
|
|
|
12
50
|
function resolveCodexConfigPath(): string {
|
|
13
51
|
const raw = process.env.CODEX_HOME?.trim();
|
|
@@ -359,6 +397,12 @@ export function discoverProjectCodexConfigPaths(options: {
|
|
|
359
397
|
const globalConfigIdentity = normalizeExistingPath(codexConfigPath);
|
|
360
398
|
const addIfExists = (projectRoot: string) => {
|
|
361
399
|
const candidate = join(resolve(projectRoot), ".codex", "config.toml");
|
|
400
|
+
try {
|
|
401
|
+
const stat = lstatSync(candidate);
|
|
402
|
+
if (!stat.isFile() || stat.size > MAX_PROJECT_CONFIG_BYTES) return;
|
|
403
|
+
} catch {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
362
406
|
const candidateIdentity = normalizeExistingPath(candidate);
|
|
363
407
|
if (candidateIdentity && candidateIdentity !== globalConfigIdentity) found.add(candidate);
|
|
364
408
|
};
|
|
@@ -397,12 +441,8 @@ export function collectProjectCodexConfigWarnings(options: {
|
|
|
397
441
|
|
|
398
442
|
const warnings: ProjectCodexConfigWarning[] = [];
|
|
399
443
|
for (const path of discoverProjectCodexConfigPaths({ cwd: options.cwd, codexConfigPath })) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
warnings.push(...analyzeProjectCodexConfig(content, path));
|
|
403
|
-
} catch {
|
|
404
|
-
/* skip unreadable project config */
|
|
405
|
-
}
|
|
444
|
+
const content = readBoundedProjectConfig(path);
|
|
445
|
+
if (content !== null) warnings.push(...analyzeProjectCodexConfig(content, path));
|
|
406
446
|
}
|
|
407
447
|
return warnings;
|
|
408
448
|
}
|
|
@@ -78,3 +78,44 @@ export function decodeBasicString(literal: string): string | null {
|
|
|
78
78
|
}
|
|
79
79
|
return out;
|
|
80
80
|
}
|
|
81
|
+
|
|
82
|
+
/** Decode externally authored TOML basic strings on the read-only fallback path. */
|
|
83
|
+
export function decodeTomlBasicString(literal: string): string | null {
|
|
84
|
+
if (literal.length < 2 || !literal.startsWith('"') || !literal.endsWith('"')) return null;
|
|
85
|
+
const inner = literal.slice(1, -1);
|
|
86
|
+
let out = "";
|
|
87
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
88
|
+
const ch = inner[i]!;
|
|
89
|
+
if (ch !== "\\") {
|
|
90
|
+
const code = inner.codePointAt(i)!;
|
|
91
|
+
if (ch === '"' || (code < 0x20 && code !== 0x09) || code === 0x7f
|
|
92
|
+
|| (code >= 0xd800 && code <= 0xdfff)) return null;
|
|
93
|
+
out += String.fromCodePoint(code);
|
|
94
|
+
if (code > 0xffff) i += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const escape = inner[++i];
|
|
98
|
+
switch (escape) {
|
|
99
|
+
case "b": out += "\b"; break;
|
|
100
|
+
case "t": out += "\t"; break;
|
|
101
|
+
case "n": out += "\n"; break;
|
|
102
|
+
case "f": out += "\f"; break;
|
|
103
|
+
case "r": out += "\r"; break;
|
|
104
|
+
case '"': out += '"'; break;
|
|
105
|
+
case "\\": out += "\\"; break;
|
|
106
|
+
case "u":
|
|
107
|
+
case "U": {
|
|
108
|
+
const digits = escape === "u" ? 4 : 8;
|
|
109
|
+
const hex = inner.slice(i + 1, i + 1 + digits);
|
|
110
|
+
if (hex.length !== digits || !/^[0-9a-fA-F]+$/.test(hex)) return null;
|
|
111
|
+
const code = Number.parseInt(hex, 16);
|
|
112
|
+
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) return null;
|
|
113
|
+
out += String.fromCodePoint(code);
|
|
114
|
+
i += digits;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
default: return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
@@ -38,7 +38,7 @@ export function rootArrayEntries(configBytes: string | null, key: string): strin
|
|
|
38
38
|
const PARSE_FAILED = Symbol("toml-parse-failed");
|
|
39
39
|
|
|
40
40
|
/** A root-scope value, `undefined` when the key is absent, `PARSE_FAILED` when the file will not parse. */
|
|
41
|
-
function rootValue(configBytes: string | null, key: string): unknown {
|
|
41
|
+
export function rootValue(configBytes: string | null, key: string): unknown {
|
|
42
42
|
if (configBytes === null) return undefined;
|
|
43
43
|
let parsed: unknown;
|
|
44
44
|
try {
|
|
@@ -149,8 +149,8 @@ export type { Ownership } from "./prompt-layers/toml-read";
|
|
|
149
149
|
|
|
150
150
|
import { activeConfigPath, activeStorePath, activeBaseVariantDir, journalPathFor, lockPathFor, type Paths } from "./prompt-layers/paths";
|
|
151
151
|
import { readFileOrNull, computeRevision, updateFingerprintField } from "./prompt-layers/revision";
|
|
152
|
-
import { normalizeBody, findInvalidCharacter, decodeBasicString } from "./prompt-layers/encoding";
|
|
153
|
-
import { rootArrayEntries, hasRootKey, rootLines, tableLines, boolInLines, inspectOwnership } from "./prompt-layers/toml-read";
|
|
152
|
+
import { normalizeBody, findInvalidCharacter, decodeBasicString, decodeTomlBasicString } from "./prompt-layers/encoding";
|
|
153
|
+
import { rootArrayEntries, hasRootKey, rootLines, rootValue, tableLines, boolInLines, inspectOwnership } from "./prompt-layers/toml-read";
|
|
154
154
|
import { setRootBool, setRootString, setTableBool, setProjection, removeUnownedProjection } from "./prompt-layers/toml-edit";
|
|
155
155
|
|
|
156
156
|
/**
|
|
@@ -367,22 +367,23 @@ function readToggle(configBytes: string | null, id: ToggleId): ToggleState {
|
|
|
367
367
|
|
|
368
368
|
function readModelInstructionsFile(configBytes: string | null): string | null {
|
|
369
369
|
if (configBytes === null) return null;
|
|
370
|
+
const parsed = rootValue(configBytes, "model_instructions_file");
|
|
371
|
+
if (typeof parsed === "string") return parsed;
|
|
372
|
+
if (parsed === undefined) return null;
|
|
370
373
|
for (const line of rootLines(configBytes)) {
|
|
371
374
|
// Capture the whole literal INCLUDING its quotes and decode it, rather than
|
|
372
375
|
// returning the raw inner text. `setRootString` writes this key through
|
|
373
376
|
// `encodeBasicString`, which escapes backslashes, so on Windows the stored
|
|
374
|
-
// literal is "C:\\Users\\..." while the path is "C:\Users\...".
|
|
375
|
-
// inner text verbatim returned the doubled form: the round trip did not
|
|
376
|
-
// survive, `baseSelection` compared a doubled path against the real variant
|
|
377
|
-
// path and reported `external` for a variant this code had just selected.
|
|
377
|
+
// literal is "C:\\Users\\..." while the path is "C:\Users\...".
|
|
378
378
|
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (m) return decodeBasicString(m[1]!);
|
|
379
|
+
// Bun may reject an unrelated safe-for-Codex integer. Decode the standard
|
|
380
|
+
// TOML escapes here, and never mistake an undecodable literal for a path.
|
|
381
|
+
const m = /^\s*model_instructions_file\s*=\s*("(?:[^"\\]|\\.)*")\s*(?:#.*)?$/.exec(line);
|
|
382
|
+
if (m) return decodeTomlBasicString(m[1]!) ?? "<unreadable model_instructions_file>";
|
|
384
383
|
}
|
|
385
|
-
|
|
384
|
+
// A present non-string value or unrecognised spelling fails closed. Only
|
|
385
|
+
// `undefined` above proves that the setting is absent.
|
|
386
|
+
return "<unreadable model_instructions_file>";
|
|
386
387
|
}
|
|
387
388
|
|
|
388
389
|
/** Variant ids are ours to generate, so they stay in one narrow shape. */
|
|
@@ -875,7 +876,10 @@ export function writeBaseVariant(
|
|
|
875
876
|
? input.id
|
|
876
877
|
: input.id ?? newBaseVariantId(existing);
|
|
877
878
|
if (!BASE_VARIANT_ID.test(targetId)) return { ok: false, error: "unknown_layer", detail: targetId };
|
|
878
|
-
|
|
879
|
+
// A caller-supplied id is an edit (or delete), never an alternate create path.
|
|
880
|
+
// Requiring it to exist keeps the generated-id path as the sole place where a
|
|
881
|
+
// new variant can enter, and therefore makes the cap impossible to bypass.
|
|
882
|
+
if (input.id !== null && !existing.some(v => v.id === targetId)) {
|
|
879
883
|
return { ok: false, error: "unknown_layer", detail: targetId };
|
|
880
884
|
}
|
|
881
885
|
if (!deleting && input.id === null && existing.length >= MAX_BASE_VARIANTS) {
|
|
@@ -49,6 +49,9 @@ const LAYER_SECTION_TAGS: Record<string, string> = {
|
|
|
49
49
|
skills: "skills_instructions",
|
|
50
50
|
apps: "apps_instructions",
|
|
51
51
|
plugins: "plugins_instructions",
|
|
52
|
+
// Context-dependent: it is absent when the active collaboration mode adds no
|
|
53
|
+
// instructions, but Codex wraps it in this tag when it does render.
|
|
54
|
+
collaboration: "collaboration_mode",
|
|
52
55
|
environment: "environment_context",
|
|
53
56
|
permissions: "permissions instructions",
|
|
54
57
|
// Synthetic: the project doc carries no tag of its own (see extractSections).
|
|
@@ -71,7 +74,6 @@ const UNMAPPED_LAYER_IDS = [
|
|
|
71
74
|
// truth is that this extractor has no verified tag for them.
|
|
72
75
|
"personality",
|
|
73
76
|
"realtime",
|
|
74
|
-
"collaboration",
|
|
75
77
|
// The Rust source names a <git_attribution> marker pair, but a world-state section is
|
|
76
78
|
// DIFF-rendered: it emits nothing on a turn where its state has not changed. Live
|
|
77
79
|
// `codex debug prompt-input` (codex-cli 0.145.0, 32978 bytes) showed no such block and
|
|
@@ -802,6 +804,22 @@ function extractSections(raw: string): Map<string, string> {
|
|
|
802
804
|
/** Test seam: the extraction is the part worth pinning, not the spawn. */
|
|
803
805
|
export const extractSectionsForTests = extractSections;
|
|
804
806
|
|
|
807
|
+
function mapSectionsToLayers(sections: Map<string, string>): Record<string, LayerText> {
|
|
808
|
+
const layers: Record<string, LayerText> = {};
|
|
809
|
+
for (const [layerId, tag] of Object.entries(LAYER_SECTION_TAGS)) {
|
|
810
|
+
const text = sections.get(tag) ?? null;
|
|
811
|
+
layers[layerId] = text === null
|
|
812
|
+
// Registered but not rendered on this turn, which is an ordinary state for
|
|
813
|
+
// a diff-rendered section rather than an error.
|
|
814
|
+
? { text: null, reason: "not-rendered", bytes: 0 }
|
|
815
|
+
: { text, reason: "ok", bytes: Buffer.byteLength(text, "utf8") };
|
|
816
|
+
}
|
|
817
|
+
return layers;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** Test seam: pin section-to-layer projection independently of the subprocess. */
|
|
821
|
+
export const mapSectionsToLayersForTests = mapSectionsToLayers;
|
|
822
|
+
|
|
805
823
|
/**
|
|
806
824
|
* Probe once and map every known layer to its rendered text.
|
|
807
825
|
*
|
|
@@ -915,15 +933,7 @@ export async function probePromptText(
|
|
|
915
933
|
detail: "prompt output could not be parsed",
|
|
916
934
|
};
|
|
917
935
|
}
|
|
918
|
-
const layers
|
|
919
|
-
for (const [layerId, tag] of Object.entries(LAYER_SECTION_TAGS)) {
|
|
920
|
-
const text = sections.get(tag) ?? null;
|
|
921
|
-
layers[layerId] = text === null
|
|
922
|
-
// Registered but not rendered on this turn, which is an ordinary state for
|
|
923
|
-
// a diff-rendered section rather than an error.
|
|
924
|
-
? { text: null, reason: "not-rendered", bytes: 0 }
|
|
925
|
-
: { text, reason: "ok", bytes: Buffer.byteLength(text, "utf8") };
|
|
926
|
-
}
|
|
936
|
+
const layers = mapSectionsToLayers(sections);
|
|
927
937
|
|
|
928
938
|
// A file that exists and is empty is not the same as a layer that chose to send
|
|
929
939
|
// nothing. Reporting "sent nothing" for an empty AGENTS.md tells the user their
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isThirtyDayOnlyCodexPlan } from "./plan";
|
|
2
|
+
import type { StoredAccountQuota } from "./quota-types";
|
|
3
|
+
|
|
4
|
+
type Window = "weeklyPercent" | "monthlyPercent" | "shortPercent";
|
|
5
|
+
// Failback needs observations, not a recent cache write. Keep this request-policy evidence
|
|
6
|
+
// process-local: hydrated display bars do not prove a fresh observation in this process.
|
|
7
|
+
const observed = new WeakMap<StoredAccountQuota, Partial<Record<Window, number>>>();
|
|
8
|
+
const windows: Window[] = ["weeklyPercent", "monthlyPercent", "shortPercent"];
|
|
9
|
+
const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
|
|
10
|
+
|
|
11
|
+
export function stampCodexQuotaUsageObservation(
|
|
12
|
+
next: StoredAccountQuota,
|
|
13
|
+
incoming: Pick<StoredAccountQuota, Window>,
|
|
14
|
+
existing?: StoredAccountQuota,
|
|
15
|
+
): StoredAccountQuota {
|
|
16
|
+
const previous = existing && observed.get(existing);
|
|
17
|
+
const timestamps: Partial<Record<Window, number>> = {};
|
|
18
|
+
for (const window of windows) {
|
|
19
|
+
if (!finite(next[window])) continue;
|
|
20
|
+
if (finite(incoming[window])) timestamps[window] = next.updatedAt;
|
|
21
|
+
else if (previous?.[window] !== undefined) timestamps[window] = previous[window];
|
|
22
|
+
}
|
|
23
|
+
observed.set(next, timestamps);
|
|
24
|
+
return next;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function codexQuotaHasFreshUsage(quota: StoredAccountQuota, plan: unknown, now: number, maxAgeMs: number): boolean {
|
|
28
|
+
const long: Window[] = isThirtyDayOnlyCodexPlan(plan) ? ["monthlyPercent"] : ["weeklyPercent", "monthlyPercent"];
|
|
29
|
+
const relevant = long.filter(window => finite(quota[window]));
|
|
30
|
+
if (relevant.length === 0) return false;
|
|
31
|
+
if (finite(quota.shortPercent)) relevant.push("shortPercent");
|
|
32
|
+
const timestamps = observed.get(quota);
|
|
33
|
+
return relevant.every(window => finite(timestamps?.[window]) && now - timestamps![window]! < maxAgeMs);
|
|
34
|
+
}
|
|
@@ -349,9 +349,10 @@ export async function codexScopedExhaustionCode(
|
|
|
349
349
|
* Status alone and message text are intentionally insufficient. The broad
|
|
350
350
|
* alternate-account retry remains eligible for 429/402 to preserve #584.
|
|
351
351
|
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
352
|
+
* Organization- or project-scoped exhaustion ({@link SCOPED_EXHAUSTION_CODE_VALUES}) remains
|
|
353
|
+
* alternate-retry eligible here because the response does not identify the refusing scope. The
|
|
354
|
+
* account-rotation path may suppress the send later when the resolved alternate carries binding
|
|
355
|
+
* evidence that it shares an organization-level counter.
|
|
355
356
|
*/
|
|
356
357
|
export async function classifyCodexPreStreamRejection(
|
|
357
358
|
response: Response,
|
|
@@ -377,7 +378,10 @@ export async function classifyCodexPreStreamRejection(
|
|
|
377
378
|
});
|
|
378
379
|
}
|
|
379
380
|
if (scoped) {
|
|
380
|
-
return rejection(status, "scoped-quota-exhaustion", {
|
|
381
|
+
return rejection(status, "scoped-quota-exhaustion", {
|
|
382
|
+
alternateRetryEligible: true,
|
|
383
|
+
scopedExhaustionCode: scoped,
|
|
384
|
+
});
|
|
381
385
|
}
|
|
382
386
|
return rejection(
|
|
383
387
|
status,
|
package/src/codex/quota.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { atomicWriteFile, getConfigDir } from "../config";
|
|
4
4
|
import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
|
|
5
5
|
import { isThirtyDayOnlyCodexPlan } from "./plan";
|
|
6
|
+
import { stampCodexQuotaUsageObservation } from "./quota-observation-freshness";
|
|
6
7
|
import { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
7
8
|
import { getObservedMainQuotaIdentityKey, isMainQuotaWriterLive, type MainQuotaWriter } from "./main-account-cache";
|
|
8
9
|
|
|
@@ -333,7 +334,7 @@ function mergeAccountQuota(
|
|
|
333
334
|
assignCarriedShort(next, existing, updatedAt, policyEvidence);
|
|
334
335
|
if (existing?.customWindows !== undefined) next.customWindows = existing.customWindows;
|
|
335
336
|
next.resetCredits = quota.resetCredits;
|
|
336
|
-
return next;
|
|
337
|
+
return stampCodexQuotaUsageObservation(next, quota, existing);
|
|
337
338
|
}
|
|
338
339
|
|
|
339
340
|
if (snapshotHasWeekly(quota)) {
|
|
@@ -393,7 +394,7 @@ function mergeAccountQuota(
|
|
|
393
394
|
if (quota.resetCredits !== undefined) next.resetCredits = quota.resetCredits;
|
|
394
395
|
else if (existing?.resetCredits !== undefined) next.resetCredits = existing.resetCredits;
|
|
395
396
|
|
|
396
|
-
return next;
|
|
397
|
+
return stampCodexQuotaUsageObservation(next, quota, existing);
|
|
397
398
|
}
|
|
398
399
|
|
|
399
400
|
/**
|
|
@@ -612,6 +613,7 @@ export function updateAccountQuota(
|
|
|
612
613
|
}
|
|
613
614
|
if (resetCredits !== undefined) quota.resetCredits = resetCredits;
|
|
614
615
|
|
|
616
|
+
stampCodexQuotaUsageObservation(quota, { weeklyPercent: nextWeekly, monthlyPercent: nextMonthly }, existing);
|
|
615
617
|
accountQuota.set(accountId, quota);
|
|
616
618
|
// This legacy writer has no physical credential provenance.
|
|
617
619
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) mainPolicyQuota = null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getEffectiveCodexAutoSwitchThreshold } from "../account-auto-switch";
|
|
1
2
|
import { isCodexAccountPaused } from "../account-pause";
|
|
2
3
|
import { codexAccountPriorityLookup, pinnedCodexAccountId } from "../account-priority";
|
|
3
4
|
import { isSelectableCodexPoolAccount } from "../account-id";
|
|
@@ -250,7 +251,7 @@ export function hasCodexQuotaHeadroom(
|
|
|
250
251
|
selectionOptions?: CodexAccountUsabilityOptions,
|
|
251
252
|
now: number = Date.now(),
|
|
252
253
|
): boolean {
|
|
253
|
-
const threshold = config
|
|
254
|
+
const threshold = getEffectiveCodexAutoSwitchThreshold(config, accountId);
|
|
254
255
|
if (threshold <= 0) return true;
|
|
255
256
|
const usage = computeCodexUsageScore(
|
|
256
257
|
getAccountQuota(accountId),
|
|
@@ -440,7 +441,7 @@ export function pickUnboundStrategyAccount(
|
|
|
440
441
|
}
|
|
441
442
|
picked = pickRoundRobinAccount(poolKey, eligible, limit);
|
|
442
443
|
if (!picked) return null;
|
|
443
|
-
if (commitSharedActive) {
|
|
444
|
+
if (commitSharedActive && sharesActiveSelection(picked, selectionOptions)) {
|
|
444
445
|
if (!isIndependentCodexQuotaScope(quotaScope)
|
|
445
446
|
&& !manualPreferenceBlocks(codexPoolKeyForScope(quotaScope), picked)) {
|
|
446
447
|
rememberActiveCodexAccount(config, picked);
|
|
@@ -456,7 +457,7 @@ export function pickUnboundStrategyAccount(
|
|
|
456
457
|
? pickResetFirstCodexAccount(config, listEligibleCodexAccountIds(config, now, quotaScope, selectionOptions), now, selectionOptions)
|
|
457
458
|
: pickFillFirstCodexAccount(config, now, quotaScope, selectionOptions);
|
|
458
459
|
if (!picked) return null;
|
|
459
|
-
if (commitSharedActive) {
|
|
460
|
+
if (commitSharedActive && sharesActiveSelection(picked, selectionOptions)) {
|
|
460
461
|
if (!isIndependentCodexQuotaScope(quotaScope)
|
|
461
462
|
&& !manualPreferenceBlocks(codexPoolKeyForScope(quotaScope), picked)) {
|
|
462
463
|
rememberActiveCodexAccount(config, picked);
|
|
@@ -475,13 +476,21 @@ export function getPoolAccountPlan(config: OcxConfig, accountId: string): string
|
|
|
475
476
|
.find(account => isSelectableCodexPoolAccount(account) && account.id === accountId)?.plan;
|
|
476
477
|
}
|
|
477
478
|
|
|
478
|
-
/**
|
|
479
|
+
/**
|
|
480
|
+
* Selection-only main routing must not lazily read the fenced native credential for its plan, and
|
|
481
|
+
* neither may a request whose main candidacy comes from its own bearer (#5019): that request is
|
|
482
|
+
* forbidden to read the physical main credential, so main is ranked without a plan.
|
|
483
|
+
*/
|
|
479
484
|
export function getPoolAccountPlanForSelection(
|
|
480
485
|
config: OcxConfig,
|
|
481
486
|
accountId: string,
|
|
482
487
|
selectionOptions?: CodexAccountUsabilityOptions,
|
|
483
488
|
): string | undefined {
|
|
484
|
-
if (
|
|
489
|
+
if (
|
|
490
|
+
accountId === MAIN_CODEX_ACCOUNT_ID
|
|
491
|
+
&& (selectionOptions?.nativeMainSelectionOnly === true
|
|
492
|
+
|| selectionOptions?.requestOwnedMainCredential === true)
|
|
493
|
+
) {
|
|
485
494
|
return undefined;
|
|
486
495
|
}
|
|
487
496
|
return getPoolAccountPlan(config, accountId);
|
|
@@ -505,6 +514,18 @@ export function sharedStateSelectionOptions(
|
|
|
505
514
|
};
|
|
506
515
|
}
|
|
507
516
|
|
|
517
|
+
/**
|
|
518
|
+
* A main that is live only through this request's own credential serves this request alone.
|
|
519
|
+
* Recording it as the shared active account would route later requests through a credential
|
|
520
|
+
* they do not carry (see CodexAccountUsabilityOptions.requestOwnedMainCredential).
|
|
521
|
+
*/
|
|
522
|
+
export function sharesActiveSelection(
|
|
523
|
+
accountId: string,
|
|
524
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
525
|
+
): boolean {
|
|
526
|
+
return !(accountId === MAIN_CODEX_ACCOUNT_ID && selectionOptions?.requestOwnedMainCredential === true);
|
|
527
|
+
}
|
|
528
|
+
|
|
508
529
|
export function pickLowerUsageAccount(
|
|
509
530
|
config: OcxConfig,
|
|
510
531
|
active: string,
|
|
@@ -681,7 +702,7 @@ export function preferModelEntitledAccount(
|
|
|
681
702
|
*
|
|
682
703
|
* Downward moves are deliberately left to {@link applyQuotaAutoSwitch}: this only
|
|
683
704
|
* fires when the tier filter has already excluded `active`, and only toward a
|
|
684
|
-
* tier that strictly outranks it.
|
|
705
|
+
* tier that strictly outranks it. Bound threads reach it only through explicit priority failback.
|
|
685
706
|
*/
|
|
686
707
|
export function pickPriorityPreemption(
|
|
687
708
|
config: OcxConfig,
|
|
@@ -721,7 +742,7 @@ export function applyQuotaAutoSwitch(
|
|
|
721
742
|
selectionOptions?: CodexAccountUsabilityOptions,
|
|
722
743
|
commitSharedSelection = true,
|
|
723
744
|
): string {
|
|
724
|
-
const threshold = config
|
|
745
|
+
const threshold = getEffectiveCodexAutoSwitchThreshold(config, active);
|
|
725
746
|
if (threshold <= 0) return active;
|
|
726
747
|
const quota = getAccountQuota(active);
|
|
727
748
|
const activeUsage = computeCodexUsageScore(
|
|
@@ -735,7 +756,8 @@ export function applyQuotaAutoSwitch(
|
|
|
735
756
|
if (activeUsage < threshold) return active;
|
|
736
757
|
const best = pickLowerUsageAccount(config, active, activeUsage, now, quotaScope, selectionOptions);
|
|
737
758
|
if (best !== active) {
|
|
738
|
-
if (commitSharedSelection && !isIndependentCodexQuotaScope(quotaScope)
|
|
759
|
+
if (commitSharedSelection && !isIndependentCodexQuotaScope(quotaScope)
|
|
760
|
+
&& sharesActiveSelection(best, selectionOptions)) {
|
|
739
761
|
setActiveCodexAccount(config, best);
|
|
740
762
|
}
|
|
741
763
|
return best;
|
|
@@ -807,7 +829,8 @@ export function applyFailureFailover(
|
|
|
807
829
|
// the moment of the failure; the streak outlives the soft avoid, so a later
|
|
808
830
|
// scoped resolve reaches here with the streak still tripped and would otherwise
|
|
809
831
|
// move the shared cursor after all.
|
|
810
|
-
if (commitSharedSelection && !isIndependentCodexQuotaScope(quotaScope)
|
|
832
|
+
if (commitSharedSelection && !isIndependentCodexQuotaScope(quotaScope)
|
|
833
|
+
&& sharesActiveSelection(best, selectionOptions)) {
|
|
811
834
|
promoteActiveCodexAccount(config, best);
|
|
812
835
|
}
|
|
813
836
|
return best;
|