@bitkyc08/opencodex 2.42.0 → 2.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { effectiveProviderAlias } from "../../providers/default-aliases";
|
|
2
|
+
import { pendingModelSelectionProviders } from "../../providers/initial-model-selection";
|
|
1
3
|
import { execFileSync } from "node:child_process";
|
|
2
4
|
import { createHash } from "node:crypto";
|
|
3
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -73,7 +75,15 @@ import {
|
|
|
73
75
|
} from "../internal/catalog-writer";
|
|
74
76
|
import { codexRuntimeStatePath } from "../runtime";
|
|
75
77
|
import { accountBoundNativeDisplayName, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
|
|
76
|
-
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./native-models";
|
|
78
|
+
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS, NATIVE_RESERVE_MODEL } from "./native-models";
|
|
79
|
+
import { observedReserveCatalogSource } from "./metadata";
|
|
80
|
+
import {
|
|
81
|
+
createReserveCatalogProjection,
|
|
82
|
+
isReserveCatalogProjection,
|
|
83
|
+
RESERVE_LUNA_METADATA_SOURCE,
|
|
84
|
+
RESERVE_SOURCE_CATALOG_FIELD,
|
|
85
|
+
type ReserveCatalogProjection,
|
|
86
|
+
} from "./reserve";
|
|
77
87
|
|
|
78
88
|
export const MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5;
|
|
79
89
|
|
|
@@ -267,17 +277,30 @@ function isExactComboCatalogEntry(
|
|
|
267
277
|
* ids differ by a single dash (`command-code` vs `commandcode`), so relabel them to the
|
|
268
278
|
* lowercase-dash style the opencode presets use: `commandcode-auth/x` and `commandcode-api/x`.
|
|
269
279
|
* The model-id portion also carries a redundant `<vendor>-` prefix (`deepseek-deepseek-v4-flash`)
|
|
270
|
-
* that is dropped for display.
|
|
280
|
+
* that is dropped for display. Google Antigravity is relabeled to the compact `agy/` prefix for
|
|
281
|
+
* the same reason: `google-antigravity/` alone consumes most of the picker row. That prefix comes
|
|
282
|
+
* from the row's own `providerAlias`, decided once per gather flight; `null` means a cross-provider
|
|
283
|
+
* collision suppressed it and the canonical slug stands. This is the raw-slug path only -- a
|
|
284
|
+
* configured `modelAliases` entry is labeled by the effective-alias path in
|
|
285
|
+
* catalog/provider-fetch.ts (#2960) and keeps the canonical provider name. All other providers
|
|
286
|
+
* keep the raw slug exactly as before.
|
|
271
287
|
*/
|
|
272
|
-
function routedDisplayName(slug: string): string {
|
|
288
|
+
function routedDisplayName(slug: string, model?: CatalogModel, config?: Pick<OcxConfig, "providers">): string {
|
|
273
289
|
const slash = slug.indexOf("/");
|
|
274
290
|
if (slash <= 0) return slug;
|
|
275
291
|
const provider = slug.slice(0, slash);
|
|
276
|
-
let
|
|
292
|
+
let modelId = slug.slice(slash + 1);
|
|
293
|
+
if (provider === "google-antigravity") {
|
|
294
|
+
if (model?.providerAlias === null) return slug;
|
|
295
|
+
const alias = (typeof model?.providerAlias === "string" && model.providerAlias.trim().length > 0)
|
|
296
|
+
? model.providerAlias.trim()
|
|
297
|
+
: effectiveProviderAlias(provider, undefined, config);
|
|
298
|
+
return alias ? `${alias}/${modelId}` : slug;
|
|
299
|
+
}
|
|
277
300
|
if (provider === "command-code" || provider === "commandcode") {
|
|
278
|
-
const m =
|
|
279
|
-
if (m &&
|
|
280
|
-
return `${provider === "command-code" ? "commandcode-auth" : "commandcode-api"}/${
|
|
301
|
+
const m = modelId.match(/^([a-z0-9]+)-([a-z0-9]+(?:-[a-z0-9]+)+)$/i);
|
|
302
|
+
if (m && modelId.startsWith(`${m[1]}-${m[1]}-`)) modelId = modelId.slice(m[1]!.length + 1);
|
|
303
|
+
return `${provider === "command-code" ? "commandcode-auth" : "commandcode-api"}/${modelId}`;
|
|
281
304
|
}
|
|
282
305
|
return slug;
|
|
283
306
|
}
|
|
@@ -306,7 +329,7 @@ export function deriveEntry(
|
|
|
306
329
|
if (template || codexForwardNativeCapabilityAlias) {
|
|
307
330
|
const e = JSON.parse(JSON.stringify(codexForwardNativeCapabilityAlias ?? template)) as RawEntry;
|
|
308
331
|
e.slug = slug;
|
|
309
|
-
e.display_name = routedDisplayName(slug);
|
|
332
|
+
e.display_name = routedDisplayName(slug, model);
|
|
310
333
|
e.description = desc;
|
|
311
334
|
e.priority = priority;
|
|
312
335
|
e.visibility = "list";
|
|
@@ -375,7 +398,7 @@ export function deriveEntry(
|
|
|
375
398
|
// Cursor still omits hosted web-search metadata because runTurn bypasses that separate sidecar.
|
|
376
399
|
const isCursorFallback = isRouted && model?.provider === "cursor";
|
|
377
400
|
const entry: RawEntry = {
|
|
378
|
-
slug, display_name: routedDisplayName(slug), description: desc,
|
|
401
|
+
slug, display_name: routedDisplayName(slug, model), description: desc,
|
|
379
402
|
shell_type: "unified_exec", visibility: "list", supported_in_api: true,
|
|
380
403
|
priority, base_instructions: "You are a helpful coding assistant.",
|
|
381
404
|
...(isRouted
|
|
@@ -422,6 +445,8 @@ export interface ObservedCatalogEntryBuildInput {
|
|
|
422
445
|
readonly accountNativeSlugs?: readonly string[];
|
|
423
446
|
/** Per-selector account ids; unknown observations must not be copied to unrelated accounts. */
|
|
424
447
|
readonly accountNativeSlugsBySelector?: ReadonlyMap<string, readonly string[]>;
|
|
448
|
+
/** Codex-only manual selector metadata; deliberately independent of live permission. */
|
|
449
|
+
readonly reserve?: ReserveCatalogProjection;
|
|
425
450
|
}
|
|
426
451
|
|
|
427
452
|
/** Build entries with the process-observed Codex feature state. */
|
|
@@ -478,6 +503,7 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
478
503
|
openaiContextCap,
|
|
479
504
|
accountNativeSlugs,
|
|
480
505
|
accountNativeSlugsBySelector,
|
|
506
|
+
reserve,
|
|
481
507
|
}: ObservedCatalogEntryBuildInput): RawEntry[] {
|
|
482
508
|
// Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible
|
|
483
509
|
// models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog
|
|
@@ -574,15 +600,17 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
574
600
|
const selectorNativeSlugs = accountNativeSlugsBySelector?.get(selector)
|
|
575
601
|
?? accountNativeSlugs
|
|
576
602
|
?? gptSlugs;
|
|
577
|
-
const accountNativeEntries = selectorNativeSlugs.map(slug => (
|
|
603
|
+
const accountNativeEntries = selectorNativeSlugs.filter(slug => slug !== NATIVE_RESERVE_MODEL).map(slug => (
|
|
578
604
|
nativeEntriesBySlug.get(slug)
|
|
579
605
|
?? deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap)
|
|
580
606
|
));
|
|
607
|
+
if (reserve?.mainSelectors.includes(selector)) accountNativeEntries.push(reserve.source);
|
|
581
608
|
for (const [nativeIndex, native] of accountNativeEntries.entries()) {
|
|
582
609
|
const nativeSlug = String(native.slug);
|
|
583
610
|
if (disabledNativeAccountSlugs.has(nativeSlug)) continue;
|
|
584
611
|
const e = JSON.parse(JSON.stringify(native)) as RawEntry;
|
|
585
612
|
const catalogSlug = `${selector}/${nativeSlug}`;
|
|
613
|
+
if (nativeSlug === NATIVE_RESERVE_MODEL && disabledNativeAccountSlugs.has(catalogSlug)) continue;
|
|
586
614
|
e.slug = catalogSlug;
|
|
587
615
|
e.display_name = accountBoundNativeDisplayName(selector, native);
|
|
588
616
|
// Codex ignores this OpenCodex extension; preserve the native comp_hash unchanged.
|
|
@@ -656,6 +684,7 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
656
684
|
}
|
|
657
685
|
return applyMultiAgentMode(out, multiAgentMode, multiAgentV2Enabled, {
|
|
658
686
|
keepNativeChatGptOnV1,
|
|
687
|
+
preserveDefaultMultiAgentVersion: isReserveCatalogProjection,
|
|
659
688
|
});
|
|
660
689
|
}
|
|
661
690
|
|
|
@@ -761,6 +790,7 @@ export interface ObservedCatalogMergeInput {
|
|
|
761
790
|
readonly disabledModels: ReadonlySet<string>;
|
|
762
791
|
readonly selectedModelsByProvider: ReadonlyMap<string, ReadonlySet<string>>;
|
|
763
792
|
readonly gatheredProviderNames: ReadonlySet<string>;
|
|
793
|
+
readonly pendingProviderNames?: ReadonlySet<string>;
|
|
764
794
|
readonly degradedProviderNames: ReadonlySet<string>;
|
|
765
795
|
readonly legacyCustomModelSlugs: ReadonlySet<string>;
|
|
766
796
|
readonly multiAgentMode: MultiAgentMode;
|
|
@@ -792,6 +822,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
792
822
|
disabledModels,
|
|
793
823
|
selectedModelsByProvider,
|
|
794
824
|
gatheredProviderNames,
|
|
825
|
+
pendingProviderNames = new Set(),
|
|
795
826
|
degradedProviderNames,
|
|
796
827
|
legacyCustomModelSlugs,
|
|
797
828
|
multiAgentMode,
|
|
@@ -841,6 +872,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
841
872
|
if (disabledModelKeys.has(key)) return false;
|
|
842
873
|
const slash = slug.indexOf("/");
|
|
843
874
|
const provider = slug.slice(0, slash);
|
|
875
|
+
if (pendingProviderNames.has(provider)) return false;
|
|
844
876
|
const selected = selectedModelKeysByProvider.get(provider);
|
|
845
877
|
if (selected !== undefined && !selected.has(key)) return false;
|
|
846
878
|
return !gatheredProviderNames.has(provider) || degradedProviderNames.has(provider);
|
|
@@ -946,7 +978,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
946
978
|
const preserved = normalizeServiceTiers({ ...m, priority: nativePriority(slug, m.priority) });
|
|
947
979
|
// Older natives kept from disk still need the mock top tiers (max + ultra always
|
|
948
980
|
// for subagent max spawns; wire-clamped to the model's real top rung).
|
|
949
|
-
if (!isGpt56NativeSlug(slug)) ensureUltraReasoningLevel(preserved);
|
|
981
|
+
if (!isGpt56NativeSlug(slug) && slug !== NATIVE_RESERVE_MODEL) ensureUltraReasoningLevel(preserved);
|
|
950
982
|
return preserved;
|
|
951
983
|
})
|
|
952
984
|
: [];
|
|
@@ -981,6 +1013,9 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
981
1013
|
typeof entry.slug === "string" ? [[entry.slug, entry] as const] : []
|
|
982
1014
|
));
|
|
983
1015
|
const alignedAccountBoundEntries = detachedAccountBoundEntries.map(entry => {
|
|
1016
|
+
// The explicit Reserve source is already chosen (actual row or documented Luna adaptation).
|
|
1017
|
+
// A generic native merge must not replace its provenance or capability ladder.
|
|
1018
|
+
if (isReserveCatalogProjection(entry)) return entry;
|
|
984
1019
|
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
985
1020
|
const source = nativeSlug === undefined ? undefined : nativeSourceBySlug.get(nativeSlug);
|
|
986
1021
|
if (!source) return entry;
|
|
@@ -1036,6 +1071,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
1036
1071
|
if (freshExactComboEntries.has(entry)) return true;
|
|
1037
1072
|
const slash = slug.indexOf("/");
|
|
1038
1073
|
const provider = slug.slice(0, slash);
|
|
1074
|
+
if (pendingProviderNames.has(provider)) return false;
|
|
1039
1075
|
const selected = selectedModelKeysByProvider.get(provider);
|
|
1040
1076
|
return selected === undefined || selected.has(slugEquivalenceKey(slug));
|
|
1041
1077
|
});
|
|
@@ -1087,17 +1123,18 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
1087
1123
|
}));
|
|
1088
1124
|
for (const slug of policy.nativeBackfillSlugs) observedNativeSlugs.add(slug);
|
|
1089
1125
|
const mergedEntries = [...native, ...managedEntries].map(m => {
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1126
|
+
const reserveProjection = isReserveCatalogProjection(m);
|
|
1127
|
+
const normalized = reserveProjection ? m : normalizeServiceTiers(m);
|
|
1128
|
+
if (!reserveProjection && !isNativeAliasCatalogEntry(normalized)) applyNativeOpenAiContextOverride(normalized, openaiContextCap);
|
|
1092
1129
|
const exactCombo = isExactComboCatalogEntry(m, exactComboSlugs);
|
|
1093
|
-
const e = ensureStrictCatalogFields(normalized, {
|
|
1130
|
+
const e = reserveProjection ? normalized : ensureStrictCatalogFields(normalized, {
|
|
1094
1131
|
preserveExactInputModalities: exactCombo,
|
|
1095
1132
|
isRouted: finalRoutedEntrySet.has(m),
|
|
1096
1133
|
});
|
|
1097
1134
|
// Mock-max universality (260709): preserved routed entries from disk may predate
|
|
1098
1135
|
// the max rung — ensure it here so subagent max spawns validate on every
|
|
1099
1136
|
// reasoning-capable entry. max only: 5.6 exact ladders (luna: no ultra) stay intact.
|
|
1100
|
-
if (!exactCombo) {
|
|
1137
|
+
if (!exactCombo && !reserveProjection) {
|
|
1101
1138
|
const levels = Array.isArray(e.supported_reasoning_levels)
|
|
1102
1139
|
? e.supported_reasoning_levels as Array<{ effort?: string }>
|
|
1103
1140
|
: [];
|
|
@@ -1122,7 +1159,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
1122
1159
|
applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
|
|
1123
1160
|
multiAgentMode,
|
|
1124
1161
|
multiAgentV2Enabled,
|
|
1125
|
-
{ keepNativeChatGptOnV1 },
|
|
1162
|
+
{ keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection },
|
|
1126
1163
|
);
|
|
1127
1164
|
for (const entry of versionedEntries) {
|
|
1128
1165
|
const kind = entry.opencodex_catalog_kind;
|
|
@@ -1610,6 +1647,36 @@ function writeRetainedCatalogSync({
|
|
|
1610
1647
|
trustedAccountBoundNativeCatalogSlug(entry) !== undefined),
|
|
1611
1648
|
];
|
|
1612
1649
|
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
1650
|
+
const reserveMainSelectors = accountSelectors.filter(selector =>
|
|
1651
|
+
isMainCodexAccountTarget(accountTargets.get(selector) ?? ""));
|
|
1652
|
+
// The active file can own a bare source even when the bundled catalog is the build base.
|
|
1653
|
+
// A previously clamped qualified projection must not shorten a retained genuine ladder.
|
|
1654
|
+
const reserveObservations = [
|
|
1655
|
+
...(onDiskCatalog?.models ?? []),
|
|
1656
|
+
...(read.modelsCache?.models ?? []),
|
|
1657
|
+
...(catalog.models ?? []),
|
|
1658
|
+
];
|
|
1659
|
+
const retainedReserve = onDiskCatalog?.[RESERVE_SOURCE_CATALOG_FIELD];
|
|
1660
|
+
const retainedReserveSource = retainedReserve && typeof retainedReserve === "object" && !Array.isArray(retainedReserve)
|
|
1661
|
+
? observedReserveCatalogSource([retainedReserve as RawEntry], [])
|
|
1662
|
+
: null;
|
|
1663
|
+
const observedReserveSource = observedReserveCatalogSource(
|
|
1664
|
+
// Cache invalidation carries historical bare observations alongside emitted models.
|
|
1665
|
+
// Only unmarked observations are fresh enough to supersede the retained source.
|
|
1666
|
+
reserveObservations.filter(entry => entry.slug === NATIVE_RESERVE_MODEL
|
|
1667
|
+
&& entry.opencodex_account_observed_native === undefined), reserveMainSelectors,
|
|
1668
|
+
) ?? retainedReserveSource ?? observedReserveCatalogSource(reserveObservations, reserveMainSelectors);
|
|
1669
|
+
// This root is read only by OCX. Upstream ModelsResponse ignores unknown root fields.
|
|
1670
|
+
// Retain before final runtime clamping: an omitted row must not turn into Luna next sync.
|
|
1671
|
+
if (observedReserveSource) catalog[RESERVE_SOURCE_CATALOG_FIELD] = structuredClone(observedReserveSource);
|
|
1672
|
+
else delete catalog[RESERVE_SOURCE_CATALOG_FIELD];
|
|
1673
|
+
const lunaSource = upstreamNativeEntry(RESERVE_LUNA_METADATA_SOURCE);
|
|
1674
|
+
const reserve = createReserveCatalogProjection(
|
|
1675
|
+
config,
|
|
1676
|
+
reserveMainSelectors,
|
|
1677
|
+
observedReserveSource,
|
|
1678
|
+
lunaSource ? finishUpstreamNativeEntry(lunaSource, 9, openaiContextCap) : null,
|
|
1679
|
+
);
|
|
1613
1680
|
const accountNativeSlugsBySelector = accountSelectors.length > 0
|
|
1614
1681
|
? new Map([...accountBoundNativeOpenAiSlugsBySelector(config, observedAccountNativeEntries)].map(([selector, slugs]) => {
|
|
1615
1682
|
const target = accountTargets.get(selector);
|
|
@@ -1691,6 +1758,7 @@ function writeRetainedCatalogSync({
|
|
|
1691
1758
|
openaiContextCap,
|
|
1692
1759
|
accountNativeSlugs,
|
|
1693
1760
|
accountNativeSlugsBySelector,
|
|
1761
|
+
reserve,
|
|
1694
1762
|
}).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined)
|
|
1695
1763
|
: [];
|
|
1696
1764
|
catalog.models = mergeCatalogEntriesFromObservedState({
|
|
@@ -1704,6 +1772,7 @@ function writeRetainedCatalogSync({
|
|
|
1704
1772
|
disabledModels: new Set(config.disabledModels ?? []),
|
|
1705
1773
|
selectedModelsByProvider,
|
|
1706
1774
|
gatheredProviderNames,
|
|
1775
|
+
pendingProviderNames: pendingModelSelectionProviders(config),
|
|
1707
1776
|
degradedProviderNames,
|
|
1708
1777
|
legacyCustomModelSlugs: legacyCustomModelCatalogSlugs(config),
|
|
1709
1778
|
multiAgentMode,
|
|
@@ -1806,6 +1875,10 @@ export async function syncCatalogModels(
|
|
|
1806
1875
|
config: OcxConfig,
|
|
1807
1876
|
options?: CodexCatalogSyncOptions,
|
|
1808
1877
|
): Promise<RetainedCatalogSyncResult> {
|
|
1878
|
+
if (pendingModelSelectionProviders(config).size) {
|
|
1879
|
+
const { resolvePendingInitialModelSelection } = await import("../../providers/initial-model-selection-runtime");
|
|
1880
|
+
await resolvePendingInitialModelSelection(config);
|
|
1881
|
+
}
|
|
1809
1882
|
const owningCodexHome = getCodexHome();
|
|
1810
1883
|
const preflightRead = readRetainedCatalogSync(config);
|
|
1811
1884
|
if (preflightRead === null) {
|
|
@@ -429,6 +429,7 @@ export interface CatalogTrustedOpenAiApiPolicySnapshot {
|
|
|
429
429
|
readonly models?: readonly string[];
|
|
430
430
|
readonly modelContextWindows?: Readonly<Record<string, number>>;
|
|
431
431
|
readonly modelMaxInputTokens?: Readonly<Record<string, number>>;
|
|
432
|
+
readonly modelMaxOutputTokens?: Readonly<Record<string, number>>;
|
|
432
433
|
readonly virtualModels?: Readonly<Record<string, Readonly<{ wireModelId: string; reasoningMode: "pro" }>>>;
|
|
433
434
|
readonly modelInputModalities?: Readonly<Record<string, readonly string[]>>;
|
|
434
435
|
readonly modelReasoningEfforts?: Readonly<Record<string, readonly string[]>>;
|
package/src/codex/convergence.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import { getConfigDir, saveConfigPreservingClaudeCode, websocketsEnabled, withExpectedConfigGenerationSync } from "../config";
|
|
4
4
|
import { reconcileSuccessfulModelDiscoveries } from "../providers/new-model-policy";
|
|
5
|
+
import { pendingModelSelectionProviders } from "../providers/initial-model-selection";
|
|
5
6
|
import { COMBO_NAMESPACE } from "../combos";
|
|
6
7
|
import { getAuthStorePath } from "../oauth/store";
|
|
7
8
|
import type { OcxConfig } from "../types";
|
|
@@ -351,6 +352,7 @@ function prepareCatalog(
|
|
|
351
352
|
disabledModels: new Set(config.disabledModels ?? []),
|
|
352
353
|
selectedModelsByProvider,
|
|
353
354
|
gatheredProviderNames,
|
|
355
|
+
pendingProviderNames: pendingModelSelectionProviders(config),
|
|
354
356
|
degradedProviderNames,
|
|
355
357
|
legacyCustomModelSlugs: legacyCustomModelCatalogSlugs(config),
|
|
356
358
|
multiAgentMode,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Native request metadata shared by the HTTP adapter and WS preparation. */
|
|
2
|
+
export const CODEX_RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite";
|
|
3
|
+
export const CODEX_RESPONSES_LITE_METADATA_KEY = "ws_request_header_x_openai_internal_codex_responses_lite";
|
|
4
|
+
export const CODEX_ROUTING_HINT_HEADER = "x-codex-routing-hint";
|
|
5
|
+
|
|
6
|
+
const MAX_ROUTING_MODEL_BYTES = 256;
|
|
7
|
+
const MAX_ROUTING_TIER_BYTES = 64;
|
|
8
|
+
|
|
9
|
+
function isRoutingHintComponent(value: unknown, maxBytes: number): value is string {
|
|
10
|
+
// Printable non-whitespace ASCII makes code-unit length equal to byte length.
|
|
11
|
+
return typeof value === "string" && value.length > 0 && value.length <= maxBytes
|
|
12
|
+
&& /^[\x21-\x7e]+$/.test(value) && !/[;=]/.test(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The final wire body is authoritative; an invalid component never revives a stale hint. */
|
|
16
|
+
export function applyCodexRoutingHint(headers: Headers, body: unknown): void {
|
|
17
|
+
headers.delete(CODEX_ROUTING_HINT_HEADER);
|
|
18
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return;
|
|
19
|
+
const record = body as Record<string, unknown>;
|
|
20
|
+
if (!isRoutingHintComponent(record.model, MAX_ROUTING_MODEL_BYTES)) return;
|
|
21
|
+
const tier = record.service_tier;
|
|
22
|
+
if (tier !== undefined && !isRoutingHintComponent(tier, MAX_ROUTING_TIER_BYTES)) return;
|
|
23
|
+
headers.set(CODEX_ROUTING_HINT_HEADER,
|
|
24
|
+
`model=${record.model}${tier === undefined ? "" : `;tier=${tier}`}`);
|
|
25
|
+
}
|
package/src/codex/inject.ts
CHANGED
|
@@ -77,6 +77,9 @@ import {
|
|
|
77
77
|
type ManagedSubagentDefaults,
|
|
78
78
|
} from "./subagent-defaults";
|
|
79
79
|
import type { OcxConfig } from "../types";
|
|
80
|
+
import { isLoopbackHostname, shouldInjectApiAuthHeader } from "./loopback-target";
|
|
81
|
+
|
|
82
|
+
export { isLoopbackHostname, shouldInjectApiAuthHeader } from "./loopback-target";
|
|
80
83
|
|
|
81
84
|
// Ownership predicates live in `./injected-marker` so `journal.ts` can reach them
|
|
82
85
|
// without importing this module back. Re-exported for existing external callers.
|
|
@@ -233,23 +236,6 @@ function configuredManagedSubagentDefaults(
|
|
|
233
236
|
* whatever `[table]` happened to be open last (e.g. `[plugins."chrome@openai-bundled"]`), so Codex
|
|
234
237
|
* never saw a global model_provider and silently fell back to the `openai` (ChatGPT) provider.
|
|
235
238
|
*/
|
|
236
|
-
/**
|
|
237
|
-
* True only for hostnames that bind loopback ONLY. Wildcard binds ("0.0.0.0", "::") are NOT
|
|
238
|
-
* loopback: they expose the proxy on every interface and therefore require the admission token.
|
|
239
|
-
* Do not use `providerBaseHost` for this decision — it folds wildcards to 127.0.0.1 because it
|
|
240
|
-
* answers "what address do I dial", which is a different question from "is this exposed".
|
|
241
|
-
*/
|
|
242
|
-
export function isLoopbackHostname(hostname: string | undefined): boolean {
|
|
243
|
-
const normalized = (hostname ?? "127.0.0.1").trim().toLowerCase();
|
|
244
|
-
return (
|
|
245
|
-
normalized === "" ||
|
|
246
|
-
normalized === "localhost" ||
|
|
247
|
-
normalized === "127.0.0.1" ||
|
|
248
|
-
normalized === "::1" ||
|
|
249
|
-
normalized === "[::1]"
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
239
|
export function providerBaseHost(hostname: string | undefined): string {
|
|
254
240
|
const trimmed = (hostname ?? "127.0.0.1").trim();
|
|
255
241
|
const lower = trimmed.toLowerCase();
|
|
@@ -267,17 +253,6 @@ export function providerBaseHost(hostname: string | undefined): string {
|
|
|
267
253
|
return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
|
|
268
254
|
}
|
|
269
255
|
|
|
270
|
-
export function shouldInjectApiAuthHeader(
|
|
271
|
-
config: Pick<OcxConfig, "hostname" | "unauthenticatedLoopbackListener"> | undefined,
|
|
272
|
-
): boolean {
|
|
273
|
-
// The unauthenticated loopback listener is a loopback bind, so it admits without a
|
|
274
|
-
// credential (#1102). Emitting the env header anyway would be worse than useless: the
|
|
275
|
-
// directly-spawned app-server this exists for has no OPENCODEX_API_AUTH_TOKEN in its
|
|
276
|
-
// environment, and Codex would send an empty header value.
|
|
277
|
-
if (config?.unauthenticatedLoopbackListener?.enabled) return false;
|
|
278
|
-
return !isLoopbackHostname(config?.hostname);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
256
|
export function buildProviderTableBlock(
|
|
282
257
|
port: number,
|
|
283
258
|
supportsWebsockets?: boolean,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveCodexHomeDir } from "./home";
|
|
4
|
+
import { parseTomlDocument } from "./project-config-warnings";
|
|
5
|
+
import { redactUserPath } from "../lib/redact";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Keys that are valid in model catalog data but not at the top level of the
|
|
9
|
+
* user config. Codex rejects them when strict config parsing is enabled.
|
|
10
|
+
*/
|
|
11
|
+
const LEGACY_KEYS = ["persistent_instructions"] as const;
|
|
12
|
+
|
|
13
|
+
export type LegacyCodexConfigKey = (typeof LEGACY_KEYS)[number];
|
|
14
|
+
|
|
15
|
+
export interface LegacyCodexConfigKeyDiagnostic {
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly code: LegacyCodexConfigKey;
|
|
18
|
+
readonly detail: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type LegacyCodexConfigKeyDiagnosticsResult =
|
|
22
|
+
| { readonly status: "available"; readonly path: string; readonly diagnostics: readonly LegacyCodexConfigKeyDiagnostic[] }
|
|
23
|
+
| { readonly status: "unavailable"; readonly path: string; readonly reason: "read_failed" | "not_a_file" };
|
|
24
|
+
|
|
25
|
+
function resolveCodexConfigPath(codexConfigPath?: string): string {
|
|
26
|
+
if (codexConfigPath) return codexConfigPath;
|
|
27
|
+
return join(resolveCodexHomeDir(), "config.toml");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function collectLegacyCodexConfigKeyDiagnostics(
|
|
31
|
+
options: { codexConfigPath?: string } = {},
|
|
32
|
+
): LegacyCodexConfigKeyDiagnosticsResult {
|
|
33
|
+
const path = resolveCodexConfigPath(options.codexConfigPath);
|
|
34
|
+
if (!existsSync(path)) return { status: "available", path, diagnostics: [] };
|
|
35
|
+
let content: string;
|
|
36
|
+
try {
|
|
37
|
+
if (!statSync(path).isFile()) return { status: "unavailable", path, reason: "not_a_file" };
|
|
38
|
+
content = readFileSync(path, "utf-8");
|
|
39
|
+
} catch { // no-excuse-ok: catch -- optional doctor diagnostics must not fail on an unreadable file.
|
|
40
|
+
return { status: "unavailable", path, reason: "read_failed" };
|
|
41
|
+
}
|
|
42
|
+
const { root } = parseTomlDocument(content);
|
|
43
|
+
const found: LegacyCodexConfigKeyDiagnostic[] = [];
|
|
44
|
+
for (const key of LEGACY_KEYS) {
|
|
45
|
+
if (key in root) {
|
|
46
|
+
found.push({
|
|
47
|
+
path,
|
|
48
|
+
code: key,
|
|
49
|
+
detail: `top-level '${key}' is not a valid Codex config key. `
|
|
50
|
+
+ "codex --strict-config rejects the whole file. Remove the key and put durable guidance in AGENTS.md.",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { status: "available", path, diagnostics: found };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatLegacyCodexConfigKeyDiagnosticsForDoctor(
|
|
58
|
+
result: LegacyCodexConfigKeyDiagnosticsResult,
|
|
59
|
+
): string[] {
|
|
60
|
+
const displayPath = redactUserPath(result.path);
|
|
61
|
+
if (result.status === "unavailable") {
|
|
62
|
+
return [` -- Codex config at ${displayPath} could not be read (${result.reason}); legacy-key check skipped`];
|
|
63
|
+
}
|
|
64
|
+
if (result.diagnostics.length === 0) {
|
|
65
|
+
return [" ok no unsupported legacy top-level keys in the Codex config"];
|
|
66
|
+
}
|
|
67
|
+
return result.diagnostics.map(diagnostic => `[WARN] ${redactUserPath(diagnostic.path)}: ${diagnostic.detail}`);
|
|
68
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
interface CurrentLogColumn {
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
notnull: number;
|
|
7
|
+
defaultValue: string | null;
|
|
8
|
+
pk: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Pinned to Codex logs migration 0002. Keep this schema private: inspection reports
|
|
12
|
+
// compatibility, not column names, so sensitive payload-bearing fields never leak through
|
|
13
|
+
// the management API. Any additive/rebuilt future schema is monitor-only until reviewed.
|
|
14
|
+
const CURRENT_LOG_SCHEMA: readonly CurrentLogColumn[] = [
|
|
15
|
+
{ name: "id", type: "INTEGER", notnull: 0, defaultValue: null, pk: 1 },
|
|
16
|
+
{ name: "ts", type: "INTEGER", notnull: 1, defaultValue: null, pk: 0 },
|
|
17
|
+
{ name: "ts_nanos", type: "INTEGER", notnull: 1, defaultValue: null, pk: 0 },
|
|
18
|
+
{ name: "level", type: "TEXT", notnull: 1, defaultValue: null, pk: 0 },
|
|
19
|
+
{ name: "target", type: "TEXT", notnull: 1, defaultValue: null, pk: 0 },
|
|
20
|
+
{ name: "feedback_log_body", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 },
|
|
21
|
+
{ name: "module_path", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 },
|
|
22
|
+
{ name: "file", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 },
|
|
23
|
+
{ name: "line", type: "INTEGER", notnull: 0, defaultValue: null, pk: 0 },
|
|
24
|
+
{ name: "thread_id", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 },
|
|
25
|
+
{ name: "process_uuid", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 },
|
|
26
|
+
{ name: "estimated_bytes", type: "INTEGER", notnull: 1, defaultValue: "0", pk: 0 },
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
const CURRENT_LOG_TABLE_SQL = `CREATE TABLE logs (
|
|
30
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
31
|
+
ts INTEGER NOT NULL,
|
|
32
|
+
ts_nanos INTEGER NOT NULL,
|
|
33
|
+
level TEXT NOT NULL,
|
|
34
|
+
target TEXT NOT NULL,
|
|
35
|
+
feedback_log_body TEXT,
|
|
36
|
+
module_path TEXT,
|
|
37
|
+
file TEXT,
|
|
38
|
+
line INTEGER,
|
|
39
|
+
thread_id TEXT,
|
|
40
|
+
process_uuid TEXT,
|
|
41
|
+
estimated_bytes INTEGER NOT NULL DEFAULT 0
|
|
42
|
+
)`;
|
|
43
|
+
|
|
44
|
+
const CURRENT_LOG_INDEX_SQL = {
|
|
45
|
+
idx_logs_ts: "CREATE INDEX idx_logs_ts ON logs(ts DESC, ts_nanos DESC, id DESC)",
|
|
46
|
+
idx_logs_thread_id: "CREATE INDEX idx_logs_thread_id ON logs(thread_id)",
|
|
47
|
+
idx_logs_thread_id_ts: "CREATE INDEX idx_logs_thread_id_ts ON logs(thread_id, ts DESC, ts_nanos DESC, id DESC)",
|
|
48
|
+
idx_logs_process_uuid_threadless_ts: `CREATE INDEX idx_logs_process_uuid_threadless_ts
|
|
49
|
+
ON logs(process_uuid, ts DESC, ts_nanos DESC, id DESC)
|
|
50
|
+
WHERE thread_id IS NULL`,
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
export interface ColumnRow {
|
|
55
|
+
cid: number;
|
|
56
|
+
name: string;
|
|
57
|
+
type: string;
|
|
58
|
+
notnull: number;
|
|
59
|
+
dflt_value: string | null;
|
|
60
|
+
pk: number;
|
|
61
|
+
}
|
|
62
|
+
interface SchemaObjectRow { name: string; type: string; sql: string | null }
|
|
63
|
+
|
|
64
|
+
function normalizeDeclaredType(type: string): string {
|
|
65
|
+
return String(type ?? "").trim().toUpperCase();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeDefault(value: string | null): string | null {
|
|
69
|
+
return value === null ? null : String(value).trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeSchemaSql(sql: string | null | undefined): string {
|
|
73
|
+
return (sql ?? "").trim().replace(/;\s*$/, "").replace(/\s+/g, " ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sameColumns(columns: ColumnRow[]): boolean {
|
|
77
|
+
if (columns.length !== CURRENT_LOG_SCHEMA.length) return false;
|
|
78
|
+
return columns.every((column, index) => {
|
|
79
|
+
const expected = CURRENT_LOG_SCHEMA[index];
|
|
80
|
+
return column.cid === index
|
|
81
|
+
&& column.name === expected.name
|
|
82
|
+
&& normalizeDeclaredType(column.type) === expected.type
|
|
83
|
+
&& Number(column.notnull) === expected.notnull
|
|
84
|
+
&& normalizeDefault(column.dflt_value) === expected.defaultValue
|
|
85
|
+
&& Number(column.pk) === expected.pk;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The authoritative compatibility predicate: exact table SQL, exact column
|
|
91
|
+
* metadata, and every canonical index.
|
|
92
|
+
*
|
|
93
|
+
* Exported because the mutation paths must apply the SAME test inside their
|
|
94
|
+
* write transaction. They used to check column NAMES only, which is strictly
|
|
95
|
+
* weaker than what the inspector reports, so a schema change landing between
|
|
96
|
+
* the outer inspection and the locked write let Protect install a row-dropping
|
|
97
|
+
* trigger and let Reclaim vacuum pages on a database the inspector classifies
|
|
98
|
+
* as monitor-only. The lock serializes OpenCodex against itself; it does not
|
|
99
|
+
* stop Codex or another SQLite writer, so that TOCTOU window is real.
|
|
100
|
+
*/
|
|
101
|
+
export function hasCurrentLogsSchema(db: Database): boolean {
|
|
102
|
+
const columns = db.query<ColumnRow, []>("PRAGMA table_info(logs)").all();
|
|
103
|
+
return hasCurrentLogsTable(db, columns);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function hasCurrentLogsTable(db: Database, columns: ColumnRow[]): boolean {
|
|
107
|
+
const table = db.query<SchemaObjectRow, []>(
|
|
108
|
+
"SELECT name, type, sql FROM sqlite_schema WHERE name = 'logs' LIMIT 1",
|
|
109
|
+
).get();
|
|
110
|
+
if (table?.type !== "table"
|
|
111
|
+
|| !sameColumns(columns)
|
|
112
|
+
|| normalizeSchemaSql(table.sql) !== normalizeSchemaSql(CURRENT_LOG_TABLE_SQL)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const indexes = db.query<SchemaObjectRow, []>(`
|
|
117
|
+
SELECT name, type, sql FROM sqlite_schema
|
|
118
|
+
WHERE name IN (
|
|
119
|
+
'idx_logs_ts',
|
|
120
|
+
'idx_logs_thread_id',
|
|
121
|
+
'idx_logs_thread_id_ts',
|
|
122
|
+
'idx_logs_process_uuid_threadless_ts'
|
|
123
|
+
)
|
|
124
|
+
`).all();
|
|
125
|
+
const byName = new Map(indexes.map(row => [row.name, row]));
|
|
126
|
+
for (const [name, expectedSql] of Object.entries(CURRENT_LOG_INDEX_SQL)) {
|
|
127
|
+
const row = byName.get(name);
|
|
128
|
+
if (row?.type !== "index" || normalizeSchemaSql(row.sql) !== normalizeSchemaSql(expectedSql)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Extra indexes and triggers do not redefine the table contract. In particular,
|
|
134
|
+
// Protect intentionally installs OpenCodex-owned triggers and unrelated user triggers
|
|
135
|
+
// are supported, so compatibility is based on the canonical table plus required indexes.
|
|
136
|
+
return true;
|
|
137
|
+
}
|