@bitkyc08/opencodex 2.22.0 → 2.23.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/gui/dist/assets/{index-ClEcVlFO.js → index-rFWrIE11.js} +19 -19
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic.ts +39 -7
- package/src/adapters/cursor/tool-definitions.ts +48 -0
- package/src/adapters/google.ts +18 -12
- package/src/adapters/openai-chat.ts +106 -13
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +3 -0
- package/src/bridge.ts +16 -5
- package/src/chat/inbound.ts +5 -11
- package/src/claude/context-windows.ts +5 -1
- package/src/claude/desktop-3p.ts +11 -6
- package/src/claude/inbound.ts +39 -1
- package/src/claude/model-info.ts +28 -8
- package/src/cli/account-api.ts +5 -1
- package/src/cli/claude-desktop.ts +3 -0
- package/src/cli/config-command.ts +37 -14
- package/src/codex/app-server-restart-service.ts +1 -1
- package/src/codex/auth-api.ts +5 -0
- package/src/codex/auth-context.ts +43 -2
- package/src/codex/catalog/metadata.ts +55 -8
- package/src/codex/catalog/native-models.ts +32 -2
- package/src/codex/catalog/parsing.ts +21 -7
- package/src/codex/catalog/provider-fetch.ts +35 -7
- package/src/codex/catalog/sync.ts +40 -18
- package/src/codex/catalog-refresh-status.ts +21 -3
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +23 -2
- package/src/codex/desired-state.ts +1 -1
- package/src/codex/inject.ts +38 -7
- package/src/codex/injected-marker.ts +28 -0
- package/src/codex/journal.ts +40 -1
- package/src/codex/management-convergence.ts +55 -2
- package/src/codex/quota-rejection.ts +61 -1
- package/src/codex/quota.ts +60 -6
- package/src/codex/routing.ts +30 -3
- package/src/combos/failover.ts +20 -0
- package/src/config.ts +271 -4
- package/src/generated/compatibility-version.json +86 -74
- package/src/grok/sync.ts +3 -1
- package/src/lab/artifacts/sanitize.ts +1 -1
- package/src/lab/live/manifest.ts +1 -1
- package/src/lib/codex-restart-contract.ts +1 -1
- package/src/lib/config-ownership.ts +1 -0
- package/src/lib/errors.ts +9 -0
- package/src/lib/lab-activation.ts +1 -1
- package/src/lib/optional-shutdown-hooks.ts +1 -1
- package/src/providers/quota.ts +10 -4
- package/src/providers/registry.ts +2 -2
- package/src/responses/parser.ts +42 -7
- package/src/responses/provider-opaque-metadata.ts +1 -1
- package/src/responses/thought-signature-replay.ts +261 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/provider-slot.ts +1 -1
- package/src/routing/evaluator.ts +12 -2
- package/src/routing/health.ts +16 -5
- package/src/routing/history/schema.ts +1 -1
- package/src/routing/trace.ts +1 -1
- package/src/server/auth-cors.ts +56 -21
- package/src/server/chat-completions.ts +6 -2
- package/src/server/index.ts +5 -3
- package/src/server/management/agent-settings-routes.ts +26 -4
- package/src/server/management/context.ts +1 -1
- package/src/server/management/model-rows.ts +5 -0
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/provider-routes.ts +19 -0
- package/src/server/management/shared.ts +3 -3
- package/src/server/management-api.ts +13 -6
- package/src/server/passive-route-linker.ts +1 -1
- package/src/server/relay.ts +16 -0
- package/src/server/responses/compact.ts +10 -3
- package/src/server/responses/core.ts +160 -33
- package/src/server/responses/fetch-helpers.ts +34 -2
- package/src/server/responses/input-admission.ts +17 -9
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/system-env.ts +4 -2
- package/src/service.ts +14 -7
- package/src/types.ts +34 -0
|
@@ -266,6 +266,21 @@ export function isNativeOpenAiEntry(entry: RawEntry): boolean {
|
|
|
266
266
|
return typeof entry.slug === "string" && !entry.slug.includes("/");
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Auto-compaction threshold for a native row.
|
|
271
|
+
*
|
|
272
|
+
* The usual rule is 90% of the window, but a model whose measured input ceiling sits below
|
|
273
|
+
* that (GPT-5.6: 922,000 against a 1,050,000 window, where 90% would be 945,000) has to
|
|
274
|
+
* clamp to the ceiling instead — otherwise the client keeps filling until upstream answers
|
|
275
|
+
* `context_length_exceeded` and compaction never gets a chance to run.
|
|
276
|
+
*/
|
|
277
|
+
function nativeAutoCompactLimit(contextWindow: number, maxInputTokens: number | undefined, contextCap?: number): number {
|
|
278
|
+
const ninety = Math.floor(contextWindow * 0.9);
|
|
279
|
+
if (typeof maxInputTokens !== "number" || maxInputTokens <= 0) return ninety;
|
|
280
|
+
const cappedMaxInput = applyProviderContextCap(maxInputTokens, contextCap) ?? maxInputTokens;
|
|
281
|
+
return Math.min(ninety, cappedMaxInput, contextWindow);
|
|
282
|
+
}
|
|
283
|
+
|
|
269
284
|
export function applyNativeOpenAiContextOverride(entry: RawEntry, contextCap?: number): void {
|
|
270
285
|
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry)
|
|
271
286
|
?? (isNativeOpenAiEntry(entry) ? entry.slug as string : undefined);
|
|
@@ -275,7 +290,7 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, contextCap?: n
|
|
|
275
290
|
if (typeof override.contextWindow === "number") {
|
|
276
291
|
const contextWindow = applyProviderContextCap(override.contextWindow, contextCap) ?? override.contextWindow;
|
|
277
292
|
entry.context_window = contextWindow;
|
|
278
|
-
entry.auto_compact_token_limit =
|
|
293
|
+
entry.auto_compact_token_limit = nativeAutoCompactLimit(contextWindow, override.maxInputTokens, contextCap);
|
|
279
294
|
}
|
|
280
295
|
if (typeof override.maxContextWindow === "number") {
|
|
281
296
|
entry.max_context_window = applyProviderContextCap(override.maxContextWindow, contextCap) ?? override.maxContextWindow;
|
|
@@ -288,7 +303,7 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, contextCap?: n
|
|
|
288
303
|
const cappedContext = applyProviderContextCap(currentContext, contextCap);
|
|
289
304
|
if (cappedContext !== currentContext && typeof cappedContext === "number") {
|
|
290
305
|
entry.context_window = cappedContext;
|
|
291
|
-
entry.auto_compact_token_limit =
|
|
306
|
+
entry.auto_compact_token_limit = nativeAutoCompactLimit(cappedContext, override?.maxInputTokens, contextCap);
|
|
292
307
|
}
|
|
293
308
|
const currentMax = typeof entry.max_context_window === "number" ? entry.max_context_window : undefined;
|
|
294
309
|
const cappedMax = applyProviderContextCap(currentMax, contextCap);
|
|
@@ -462,16 +477,15 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry, parallelToolCalls =
|
|
|
462
477
|
// tool_search round-trip (upstream codex-rs code_mode suite; live canary 2026-08-13: routed
|
|
463
478
|
// kimi/k3 called tools.mcp__node_repl__js → isError:false). Stamping false here instead forces
|
|
464
479
|
// every MCP declaration into exec.description — a measured 2.7x turn-1 payload regression
|
|
465
|
-
// (96,699 → 258,929 chars; devlog/_plan/260813_tool_catalog_deferral/010). So
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
// the web-search sidecar and has no proven deferred path.
|
|
480
|
+
// (96,699 → 258,929 chars; devlog/_plan/260813_tool_catalog_deferral/010). So every routed
|
|
481
|
+
// code-mode row advertises deferred discovery. Cursor still omits hosted web-search metadata below,
|
|
482
|
+
// but disabling this separate exposure bit can inflate `exec` past Cursor's 120 KB wire cap (#1830).
|
|
469
483
|
if (isCursorEntry) {
|
|
470
484
|
delete entry.web_search_tool_type;
|
|
471
485
|
} else {
|
|
472
486
|
entry.web_search_tool_type = "text_and_image";
|
|
473
487
|
}
|
|
474
|
-
entry.supports_search_tool =
|
|
488
|
+
entry.supports_search_tool = true;
|
|
475
489
|
// Cursor's transport already serializes overlapping tool calls into atomic Responses tool events.
|
|
476
490
|
// Advertising parallel calls lets Codex send the same native capability bit it sends for OpenAI.
|
|
477
491
|
// Opt-in providers (OcxProviderConfig.parallelToolCalls, e.g. xAI) advertise it too: the
|
|
@@ -74,7 +74,7 @@ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } fr
|
|
|
74
74
|
|
|
75
75
|
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
|
|
76
76
|
import type { CatalogModel } from "./parsing";
|
|
77
|
-
import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
|
|
77
|
+
import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
|
|
78
78
|
import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
|
|
79
79
|
import type { ComboCatalogOmission } from "./aggregation";
|
|
80
80
|
import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
|
|
@@ -703,6 +703,8 @@ const COMBO_MEMBER_CONTEXT_FALLBACK = 128_000;
|
|
|
703
703
|
|
|
704
704
|
interface ComboCatalogMemberFallback {
|
|
705
705
|
readonly contextWindow?: number;
|
|
706
|
+
/** Input ceiling when it is lower than the window (native GPT-5.6: 922k under 1.05M). */
|
|
707
|
+
readonly maxInputTokens?: number;
|
|
706
708
|
readonly inputModalities?: readonly string[];
|
|
707
709
|
readonly reasoningEfforts?: readonly string[];
|
|
708
710
|
}
|
|
@@ -744,7 +746,11 @@ export function resolveComboCatalogMember(
|
|
|
744
746
|
if (!addMaxInput && !addModalities && !addReasoning) return member;
|
|
745
747
|
return {
|
|
746
748
|
...member,
|
|
747
|
-
|
|
749
|
+
// Never claim a larger input budget than the window, and prefer the model's own
|
|
750
|
+
// measured ceiling when the fallback carries one.
|
|
751
|
+
...(addMaxInput
|
|
752
|
+
? { maxInputTokens: Math.min(fallback.maxInputTokens ?? contextWindow!, contextWindow!) }
|
|
753
|
+
: {}),
|
|
748
754
|
...(addModalities ? { inputModalities: [...fallback.inputModalities!] } : {}),
|
|
749
755
|
...(addReasoning ? { reasoningEfforts: [...fallback.reasoningEfforts!] } : {}),
|
|
750
756
|
};
|
|
@@ -765,7 +771,7 @@ export function resolveComboCatalogMember(
|
|
|
765
771
|
}
|
|
766
772
|
const maxInput = typeof existing.maxInputTokens === "number" && existing.maxInputTokens > 0
|
|
767
773
|
? Math.min(existing.maxInputTokens, capped)
|
|
768
|
-
: capped;
|
|
774
|
+
: Math.min(fallback?.maxInputTokens ?? capped, capped);
|
|
769
775
|
return withFallbackMetadata({
|
|
770
776
|
...existing,
|
|
771
777
|
contextWindow: capped,
|
|
@@ -790,6 +796,10 @@ export function resolveComboCatalogMember(
|
|
|
790
796
|
: (typeof base.maxInputTokens === "number" && base.maxInputTokens > 0
|
|
791
797
|
? base.maxInputTokens
|
|
792
798
|
: undefined);
|
|
799
|
+
// Kept OUT of knownMaxInput on purpose: that value doubles as a context-window fallback
|
|
800
|
+
// below, and a native alias whose input ceiling (922k) is lower than its window (1.05M)
|
|
801
|
+
// would otherwise shrink the advertised window to the input limit.
|
|
802
|
+
const fallbackMaxInput = existing || prov ? fallback?.maxInputTokens : undefined;
|
|
793
803
|
// Real discovery/config values win. A native alias is the next fallback tier.
|
|
794
804
|
// The generic 128k/text synthesis from #1305 remains the final fallback.
|
|
795
805
|
const fallbackContext = existing || prov ? fallback?.contextWindow : undefined;
|
|
@@ -814,8 +824,11 @@ export function resolveComboCatalogMember(
|
|
|
814
824
|
?? (prov ? configuredReasoningEfforts(prov, target.model) : undefined)
|
|
815
825
|
?? base.reasoningEfforts
|
|
816
826
|
?? (fallback?.reasoningEfforts ? [...fallback.reasoningEfforts] : undefined);
|
|
817
|
-
|
|
818
|
-
|
|
827
|
+
// The model's own measured input ceiling still applies when discovery gave us nothing:
|
|
828
|
+
// GPT-5.6 advertises a 1.05M window but refuses input past 922k.
|
|
829
|
+
const effectiveMaxInput = knownMaxInput ?? fallbackMaxInput;
|
|
830
|
+
const maxInputTokens = effectiveMaxInput !== undefined
|
|
831
|
+
? Math.min(effectiveMaxInput, contextWindow)
|
|
819
832
|
: contextWindow;
|
|
820
833
|
|
|
821
834
|
return {
|
|
@@ -1712,7 +1725,9 @@ async function gatherRoutedModelsUncached(
|
|
|
1712
1725
|
id: slug,
|
|
1713
1726
|
owned_by: "openai",
|
|
1714
1727
|
contextWindow,
|
|
1715
|
-
|
|
1728
|
+
// Input limit, not the total window: GPT-5.6 advertises 1,050,000 but refuses past
|
|
1729
|
+
// 922,000 (measured). Falls back to the window for slugs with no separate ceiling.
|
|
1730
|
+
maxInputTokens: Math.min(nativeOpenAiMaxInputTokens(slug, openaiContextCap) ?? contextWindow, contextWindow),
|
|
1716
1731
|
inputModalities: nativeInputModalities(slug),
|
|
1717
1732
|
reasoningEfforts: nativeReasoningEfforts(slug),
|
|
1718
1733
|
...(nativeParallelToolCalls(slug) ? { parallelToolCalls: true } : {}),
|
|
@@ -1730,11 +1745,15 @@ async function gatherRoutedModelsUncached(
|
|
|
1730
1745
|
const combo = getCombo(config, id);
|
|
1731
1746
|
if (!combo) continue;
|
|
1732
1747
|
const nativeContextWindow = combo.nativeAlias && combo.alias
|
|
1733
|
-
? nativeOpenAiContextWindow(combo.alias)
|
|
1748
|
+
? nativeOpenAiContextWindow(combo.alias, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID))
|
|
1749
|
+
: undefined;
|
|
1750
|
+
const nativeAliasMaxInput = combo.nativeAlias && combo.alias
|
|
1751
|
+
? nativeOpenAiMaxInputTokens(combo.alias, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID))
|
|
1734
1752
|
: undefined;
|
|
1735
1753
|
const nativeAliasFallback = combo.nativeAlias && combo.alias && nativeContextWindow !== undefined
|
|
1736
1754
|
? {
|
|
1737
1755
|
contextWindow: nativeContextWindow,
|
|
1756
|
+
...(nativeAliasMaxInput !== undefined ? { maxInputTokens: Math.min(nativeAliasMaxInput, nativeContextWindow) } : {}),
|
|
1738
1757
|
inputModalities: nativeInputModalities(combo.alias),
|
|
1739
1758
|
reasoningEfforts: nativeReasoningEfforts(combo.alias),
|
|
1740
1759
|
}
|
|
@@ -1788,6 +1807,14 @@ async function gatherRoutedModelsUncached(
|
|
|
1788
1807
|
? Math.min(cm.contextWindow, nativeAliasContextWindow)
|
|
1789
1808
|
: cm.contextWindow
|
|
1790
1809
|
: nativeAliasContextWindow;
|
|
1810
|
+
// Input ceiling for a native capability alias, clamped to whatever window we settled on
|
|
1811
|
+
// above. A custom row that lowered the window must not keep the full native input budget.
|
|
1812
|
+
const nativeAliasMaxInputTokens = codexForwardNativeCapabilityAlias
|
|
1813
|
+
? nativeOpenAiMaxInputTokens(cm.modelId, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID))
|
|
1814
|
+
: undefined;
|
|
1815
|
+
const customMaxInputTokens = nativeAliasMaxInputTokens !== undefined && customContextWindow !== undefined
|
|
1816
|
+
? Math.min(nativeAliasMaxInputTokens, customContextWindow)
|
|
1817
|
+
: nativeAliasMaxInputTokens;
|
|
1791
1818
|
const nativeAliasDefaultEffort = codexForwardNativeCapabilityAlias
|
|
1792
1819
|
? nativeDefaultReasoningEffort(cm.modelId)
|
|
1793
1820
|
: undefined;
|
|
@@ -1804,6 +1831,7 @@ async function gatherRoutedModelsUncached(
|
|
|
1804
1831
|
? { displayName: cm.displayName }
|
|
1805
1832
|
: codexForwardNativeCapabilityAlias ? { displayName: "Daybreak Blue" } : {}),
|
|
1806
1833
|
...(customContextWindow !== undefined ? { contextWindow: customContextWindow } : {}),
|
|
1834
|
+
...(customMaxInputTokens !== undefined ? { maxInputTokens: customMaxInputTokens } : {}),
|
|
1807
1835
|
...(cm.inputModalities
|
|
1808
1836
|
? { inputModalities: cm.inputModalities }
|
|
1809
1837
|
: codexForwardNativeCapabilityAlias ? { inputModalities: nativeInputModalities(cm.modelId) } : {}),
|
|
@@ -90,21 +90,36 @@ export type SubagentRosterExclusionReason =
|
|
|
90
90
|
/**
|
|
91
91
|
* Whether a catalog entry may be offered as a V2 subagent model.
|
|
92
92
|
*
|
|
93
|
-
* Upstream
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
93
|
+
* Upstream changed this rule in codex-rs `6d4d9442c` ("Support leaf models in
|
|
94
|
+
* multi-agent v2"). `model_supports_multi_agent_backend`
|
|
95
|
+
* (core/src/tools/handlers/multi_agents_common.rs:36-42) now admits EVERY model
|
|
96
|
+
* except one explicitly marked `disabled`; the older `== Some(V2)` equality that
|
|
97
|
+
* `92938d880` introduced is gone.
|
|
97
98
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
99
|
+
* The field no longer answers "may I be a delegation target". It answers "does the
|
|
100
|
+
* CHILD get collaboration tools": `collab_tools_enabled`
|
|
101
|
+
* (core/src/tools/spec_plan.rs:599-610) grants a child recursive tools only when its
|
|
102
|
+
* own catalog value is exactly `Some(V2)`. The three-way distinction survives, but it
|
|
103
|
+
* now means eligible-recursive / eligible-LEAF / excluded:
|
|
104
|
+
*
|
|
105
|
+
* - `"v2"` -> eligible, and the child may itself delegate.
|
|
106
|
+
* - `"v1"` -> eligible LEAF worker. This is upstream's pin for `gpt-5.6-luna`
|
|
107
|
+
* (models-manager/models.json); excluding it here is exactly what
|
|
108
|
+
* kept Luna out of opencodex's roster.
|
|
109
|
+
* - absent/null -> eligible LEAF worker (routed or unpinned-native model).
|
|
110
|
+
* - `"disabled"` -> the sole capability-based exclusion.
|
|
111
|
+
*
|
|
112
|
+
* This is the roster filter only. Catalog STAMPING is a separate concern owned by
|
|
113
|
+
* `applyMultiAgentMode`, including the `keepNativeChatGptOnV1` policy (#1728) that
|
|
114
|
+
* keeps ChatGPT-native rows on `v1` so a native parent can still spawn a routed child
|
|
115
|
+
* despite backend-encrypted NEW_TASK bodies (#92). Recognizing those `v1` rows as
|
|
116
|
+
* eligible leaves here is what makes that policy usable, not a contradiction of it.
|
|
117
|
+
*
|
|
118
|
+
* Devlog: 260816_codexrs_multiagent_v2_and_history_perf/011 (C1), superseding the
|
|
119
|
+
* option-B decision in 260730_codex_rs_upstream_v2_live_handoff/060.
|
|
104
120
|
*/
|
|
105
121
|
export function isEligibleV2SubagentEntry(entry: RawEntry): boolean {
|
|
106
|
-
|
|
107
|
-
return pinned === "v2" || pinned === null || pinned === undefined;
|
|
122
|
+
return entry.multi_agent_version !== "disabled";
|
|
108
123
|
}
|
|
109
124
|
|
|
110
125
|
export interface EffectiveSubagentModel {
|
|
@@ -343,10 +358,9 @@ export function deriveEntry(
|
|
|
343
358
|
});
|
|
344
359
|
}
|
|
345
360
|
// Fallback when no template is available (best-effort; strict parser may need more).
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
// 260813_tool_catalog_deferral/010+020); search=false costs a measured 2.7x turn-1 payload.
|
|
361
|
+
// All routed fallbacks enable deferred code-mode tool exposure; otherwise the nested catalog
|
|
362
|
+
// expands into `exec.description` and can exceed Cursor's 120 KB serialized tool limit (#1830).
|
|
363
|
+
// Cursor still omits hosted web-search metadata because runTurn bypasses that separate sidecar.
|
|
350
364
|
const isCursorFallback = isRouted && model?.provider === "cursor";
|
|
351
365
|
const entry: RawEntry = {
|
|
352
366
|
slug, display_name: routedDisplayName(slug), description: desc,
|
|
@@ -354,7 +368,7 @@ export function deriveEntry(
|
|
|
354
368
|
priority, base_instructions: "You are a helpful coding assistant.",
|
|
355
369
|
...(isRouted
|
|
356
370
|
? isCursorFallback
|
|
357
|
-
? { supports_search_tool:
|
|
371
|
+
? { supports_search_tool: true }
|
|
358
372
|
: { web_search_tool_type: "text_and_image", supports_search_tool: true }
|
|
359
373
|
: {}),
|
|
360
374
|
};
|
|
@@ -1672,8 +1686,16 @@ export async function syncCatalogModels(config: OcxConfig): Promise<RetainedCata
|
|
|
1672
1686
|
export function restoreCodexCatalogWithPermit(
|
|
1673
1687
|
permit: CatalogWritePermit,
|
|
1674
1688
|
owningCodexHome: string,
|
|
1689
|
+
/**
|
|
1690
|
+
* The catalog this injection actually wrote, when it is known (#1798).
|
|
1691
|
+
*
|
|
1692
|
+
* Re-resolving from the CURRENT config is wrong after a Codex app rewrite that dropped
|
|
1693
|
+
* `model_catalog_json`: that sends restore to the default catalog while the routed file we
|
|
1694
|
+
* really wrote is left untouched. The recorded path is the file whose routing is ours.
|
|
1695
|
+
*/
|
|
1696
|
+
injectedCatalogPath?: string | null,
|
|
1675
1697
|
): { removed: number; kept: number; path: string } {
|
|
1676
|
-
const catalogPath = readCodexCatalogPath();
|
|
1698
|
+
const catalogPath = injectedCatalogPath ?? readCodexCatalogPath();
|
|
1677
1699
|
const catalog = readCatalog(catalogPath);
|
|
1678
1700
|
if (!catalog || !Array.isArray(catalog.models)) return { removed: 0, kept: 0, path: catalogPath };
|
|
1679
1701
|
const disabledModels = currentDisabledModelsForRestore();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CatalogDisposition, CatalogNotice } from "./convergence-types";
|
|
1
|
+
import type { CatalogDisposition, CatalogFailureCause, CatalogNotice } from "./convergence-types";
|
|
2
2
|
|
|
3
3
|
const INVALID_CATALOG_DISPOSITION_FIELD = Symbol("invalid-catalog-disposition-field");
|
|
4
4
|
|
|
@@ -69,11 +69,15 @@ export function normalizeCatalogDisposition(value: unknown): CatalogDisposition
|
|
|
69
69
|
const phase = ownDataProperty(value, "phase");
|
|
70
70
|
const retryable = ownDataProperty(value, "retryable");
|
|
71
71
|
const partialWrite = ownDataProperty(value, "partialWrite");
|
|
72
|
-
if ((reason !== "provider-auth" && reason !== "provider-network" && reason !== "disk"
|
|
72
|
+
if ((reason !== "provider-auth" && reason !== "provider-network" && reason !== "disk"
|
|
73
|
+
&& reason !== "request-invalid" && reason !== "admission" && reason !== "internal")
|
|
73
74
|
|| (phase !== "gather" && phase !== "commit")
|
|
74
75
|
|| typeof retryable !== "boolean"
|
|
75
76
|
|| typeof partialWrite !== "boolean") return null;
|
|
76
|
-
|
|
77
|
+
// The cause is rebuilt from closed vocabularies, never copied through: this is the
|
|
78
|
+
// boundary that keeps a message, path or account id from riding out on a failure.
|
|
79
|
+
const cause = normalizeCatalogFailureCause(ownDataProperty(value, "cause"));
|
|
80
|
+
return { status, reason, phase, retryable, partialWrite, ...(cause ? { cause } : {}) };
|
|
77
81
|
}
|
|
78
82
|
return null;
|
|
79
83
|
} catch {
|
|
@@ -81,6 +85,20 @@ export function normalizeCatalogDisposition(value: unknown): CatalogDisposition
|
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
const FAILURE_CAUSE_KINDS: ReadonlySet<string> = new Set(["invalid-request", "lock-busy", "io", "unknown"]);
|
|
89
|
+
const FAILURE_CAUSE_CODES: ReadonlySet<string> = new Set([
|
|
90
|
+
"ENOSPC", "EACCES", "EPERM", "EROFS", "ENOENT", "SQLITE_BUSY",
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
function normalizeCatalogFailureCause(value: unknown): CatalogFailureCause | undefined {
|
|
94
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
95
|
+
const kind = ownDataProperty(value, "kind");
|
|
96
|
+
if (typeof kind !== "string" || !FAILURE_CAUSE_KINDS.has(kind)) return undefined;
|
|
97
|
+
const code = ownDataProperty(value, "code");
|
|
98
|
+
const safeCode = typeof code === "string" && FAILURE_CAUSE_CODES.has(code) ? code : undefined;
|
|
99
|
+
return { kind, ...(safeCode ? { code: safeCode } : {}) } as CatalogFailureCause;
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
/** Whether a persisted mutation still needs a successful catalog commit. */
|
|
85
103
|
export function catalogRefreshIsPending(disposition: CatalogDisposition): boolean {
|
|
86
104
|
return disposition.status !== "committed";
|
package/src/codex/catalog.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Public surface preserved exactly; importers keep using "src/codex/catalog".
|
|
3
3
|
export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, readCatalog, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing";
|
|
4
4
|
export type { CatalogModel, MultiAgentMode } from "./catalog/parsing";
|
|
5
|
-
export { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, isNativeOpenAiCapabilityAliasModel, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi } from "./catalog/metadata";
|
|
5
|
+
export { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_GPT56_MAX_INPUT_TOKENS, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, isNativeOpenAiCapabilityAliasModel, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi } from "./catalog/metadata";
|
|
6
6
|
export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled";
|
|
7
7
|
export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort";
|
|
8
8
|
export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithMetadata, resolveComboCatalogMember, configuredComboTargetModelsByProvider } from "./catalog/provider-fetch";
|
|
@@ -161,8 +161,29 @@ export type CatalogDisposition =
|
|
|
161
161
|
| { status: "skipped";
|
|
162
162
|
reason: "not-requested" | "catalog-unavailable" | "busy" | "stale" | "refused";
|
|
163
163
|
retryable: boolean }
|
|
164
|
-
| { status: "failed";
|
|
165
|
-
|
|
164
|
+
| { status: "failed";
|
|
165
|
+
/**
|
|
166
|
+
* `disk` used to absorb every unclassified failure, so a malformed request and a
|
|
167
|
+
* genuine ENOSPC were indistinguishable and both reported non-retryable (#1784).
|
|
168
|
+
*/
|
|
169
|
+
reason: "provider-auth" | "provider-network" | "disk" | "request-invalid" | "admission" | "internal";
|
|
170
|
+
phase: "gather" | "commit"; retryable: boolean; partialWrite: boolean;
|
|
171
|
+
/** Allowlisted cause summary. Closed vocabularies only -- never message text. */
|
|
172
|
+
cause?: CatalogFailureCause };
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Why a catalog operation failed, in terms safe to return from the management plane.
|
|
176
|
+
*
|
|
177
|
+
* Both fields are closed sets on purpose. An `Error.constructor.name` is dependency- or
|
|
178
|
+
* input-influenced (any thrown custom class names itself) and an `Error.message` routinely
|
|
179
|
+
* carries paths, home directories and account identifiers, none of which may cross this
|
|
180
|
+
* boundary.
|
|
181
|
+
*/
|
|
182
|
+
export type CatalogFailureCause = {
|
|
183
|
+
kind: "invalid-request" | "lock-busy" | "io" | "unknown";
|
|
184
|
+
/** Recognized errno/code token, when the underlying error carried one. */
|
|
185
|
+
code?: "ENOSPC" | "EACCES" | "EPERM" | "EROFS" | "ENOENT" | "SQLITE_BUSY";
|
|
186
|
+
};
|
|
166
187
|
|
|
167
188
|
/**
|
|
168
189
|
* The ONLY way Codex-owned bytes are written. Startup, ensure, /api/sync, the
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* depending on the temp root. Convergence takes the write lock; this module only
|
|
19
19
|
* records intent through the config coordinator.
|
|
20
20
|
*
|
|
21
|
-
* Design record: devlog/
|
|
21
|
+
* Design record: devlog/_fin/260803_codex_desktop_toggle/030_desired_state.md.
|
|
22
22
|
*/
|
|
23
23
|
import { loadConfig, mutatePersistedConfig } from "../config";
|
|
24
24
|
import type { OcxClientIntegrationsConfig, OcxConfig } from "../types";
|
package/src/codex/inject.ts
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
} from "./user-identity";
|
|
31
31
|
import {
|
|
32
32
|
markJournalInjectedState,
|
|
33
|
+
journaledInjectedOpenaiBaseUrl,
|
|
34
|
+
journaledInjectedCatalogPath,
|
|
33
35
|
removeJournal,
|
|
34
36
|
restoreJournalState,
|
|
35
37
|
writeJournal,
|
|
@@ -52,6 +54,7 @@ import {
|
|
|
52
54
|
providerTableStart,
|
|
53
55
|
providerTableString,
|
|
54
56
|
rootTomlString,
|
|
57
|
+
stripJournaledOpenaiBaseUrl,
|
|
55
58
|
tomlStringPattern,
|
|
56
59
|
} from "./injected-marker";
|
|
57
60
|
import {
|
|
@@ -1138,12 +1141,18 @@ interface StripOpencodexConfigResult {
|
|
|
1138
1141
|
*/
|
|
1139
1142
|
function stripOpencodexConfigResult(
|
|
1140
1143
|
content: string,
|
|
1144
|
+
journaledBaseUrl: string | null = null,
|
|
1141
1145
|
): StripOpencodexConfigResult {
|
|
1142
1146
|
let out = content;
|
|
1143
1147
|
const hadRootOcxProvider =
|
|
1144
1148
|
readRootTomlString(out, "model_provider") === "opencodex";
|
|
1145
|
-
|
|
1149
|
+
// #1798: marker adjacency is FORMATTING evidence, and a Codex app rewrite keeps values
|
|
1150
|
+
// while dropping comments. Fall back to VALUE evidence -- the exact URL we recorded
|
|
1151
|
+
// writing -- so an app-rewritten config is still recognized as ours.
|
|
1152
|
+
const hadInjectedBaseUrl = hasInjectedOpenaiBaseUrl(out)
|
|
1153
|
+
|| (journaledBaseUrl !== null && rootTomlString(out, "openai_base_url") === journaledBaseUrl);
|
|
1146
1154
|
out = stripInjectedOpenaiBaseUrl(out); // before removeOcxSection — it keys on the marker line too
|
|
1155
|
+
out = stripJournaledOpenaiBaseUrl(out, journaledBaseUrl);
|
|
1147
1156
|
if (out.includes("[model_providers.opencodex]")) {
|
|
1148
1157
|
out = removeOcxSection(out);
|
|
1149
1158
|
}
|
|
@@ -1195,8 +1204,12 @@ export function removeCodexConfig(
|
|
|
1195
1204
|
// The unchanged fast path compares in LF space so an untouched file is never rewritten.
|
|
1196
1205
|
const eol = dominantEol(rawContent);
|
|
1197
1206
|
const content = applyEol(rawContent, "\n");
|
|
1198
|
-
|
|
1199
|
-
|
|
1207
|
+
// Read the recorded injection once: the strip below consumes it, and so does the
|
|
1208
|
+
// ownership verdict, which must agree with what was actually removed.
|
|
1209
|
+
const journaledBaseUrl = journaledInjectedOpenaiBaseUrl();
|
|
1210
|
+
const had = hasOpencodexRouting(content)
|
|
1211
|
+
|| (journaledBaseUrl !== null && rootTomlString(content, "openai_base_url") === journaledBaseUrl);
|
|
1212
|
+
const stripped = stripOpencodexConfigResult(content, journaledBaseUrl);
|
|
1200
1213
|
if (had || stripped.content !== content) {
|
|
1201
1214
|
atomicWriteFile(CODEX_CONFIG_PATH, applyEol(stripped.content, eol));
|
|
1202
1215
|
}
|
|
@@ -1371,13 +1384,23 @@ function restoreCodexConfigInline(): CodexRestoreConfigResult {
|
|
|
1371
1384
|
}
|
|
1372
1385
|
|
|
1373
1386
|
/** The catalog half, always inside its own K acquisition. */
|
|
1374
|
-
|
|
1387
|
+
/**
|
|
1388
|
+
* The catalog half, always inside its own K acquisition.
|
|
1389
|
+
*
|
|
1390
|
+
* `journaledCatalogPath` must be captured by the CALLER, before the config half runs: a
|
|
1391
|
+
* successful journal restore deletes the journal, and a config restore can remove
|
|
1392
|
+
* `model_catalog_json`. Reading it here would be too late in both cases (#1798).
|
|
1393
|
+
*/
|
|
1394
|
+
function restoreCodexCatalogArtifact(
|
|
1395
|
+
revalidateDesiredState: boolean,
|
|
1396
|
+
journaledCatalogPath: string | null,
|
|
1397
|
+
): CodexRestoreCatalogResult {
|
|
1375
1398
|
const owningCodexHome = getCodexHome();
|
|
1376
1399
|
try {
|
|
1377
1400
|
const restored = withCatalogWriteSerialization(owningCodexHome, permit =>
|
|
1378
1401
|
revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())
|
|
1379
1402
|
? null
|
|
1380
|
-
: restoreCodexCatalogWithPermit(permit, owningCodexHome));
|
|
1403
|
+
: restoreCodexCatalogWithPermit(permit, owningCodexHome, journaledCatalogPath));
|
|
1381
1404
|
return restored.kind === "completed" && restored.value !== null
|
|
1382
1405
|
? { state: "ok", changed: restored.value.removed > 0, ...restored.value, message: "Codex catalog restored." }
|
|
1383
1406
|
: restored.kind === "completed"
|
|
@@ -1435,6 +1458,10 @@ export async function restoreNativeCodexAsync(
|
|
|
1435
1458
|
integrationRecord: () => readIntegrationRecord(),
|
|
1436
1459
|
});
|
|
1437
1460
|
|
|
1461
|
+
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
1462
|
+
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
1463
|
+
// catalog we actually wrote (#1798).
|
|
1464
|
+
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
1438
1465
|
let config: CodexRestoreConfigResult;
|
|
1439
1466
|
let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
|
|
1440
1467
|
|
|
@@ -1511,7 +1538,7 @@ export async function restoreNativeCodexAsync(
|
|
|
1511
1538
|
config = restoreCodexConfigInline();
|
|
1512
1539
|
}
|
|
1513
1540
|
|
|
1514
|
-
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true);
|
|
1541
|
+
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
1515
1542
|
const outcome = await runCodexHistoryJob({
|
|
1516
1543
|
...resolveCodexHistoryJobTarget(),
|
|
1517
1544
|
...(options.revalidateDesiredState ? { expectedDesiredEnabled: false } : {}),
|
|
@@ -1561,8 +1588,12 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
|
|
|
1561
1588
|
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
1562
1589
|
return desiredEnabledRestoreSkip();
|
|
1563
1590
|
}
|
|
1591
|
+
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
1592
|
+
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
1593
|
+
// catalog we actually wrote (#1798).
|
|
1594
|
+
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
1564
1595
|
const config = restoreCodexConfigInline();
|
|
1565
|
-
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true);
|
|
1596
|
+
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
1566
1597
|
// Design B (loopback) steady state: threads are already tagged openai, so prove the
|
|
1567
1598
|
// no-op with a readonly probe instead of write-opening a DB the Codex app may hold
|
|
1568
1599
|
// (Windows: WAL writer lock -> seconds of stalling + a false warning on every stop).
|
|
@@ -50,6 +50,34 @@ export function providerTableString(content: string, provider: string, key: stri
|
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Drop a root `openai_base_url` whose VALUE is the one a recorded injection wrote.
|
|
55
|
+
*
|
|
56
|
+
* #1798: the marker-adjacency rule below is formatting evidence, and the Codex app
|
|
57
|
+
* reserializes the file -- values kept, comments dropped. This rule is value evidence
|
|
58
|
+
* instead, so it still recognizes our URL after that rewrite. It is deliberately an
|
|
59
|
+
* EXACT value match against what we recorded writing: a user gateway we never wrote
|
|
60
|
+
* cannot match, so restore can never delete a URL that was not ours.
|
|
61
|
+
*/
|
|
62
|
+
export function stripJournaledOpenaiBaseUrl(content: string, injectedUrl: string | null): string {
|
|
63
|
+
if (!injectedUrl) return content;
|
|
64
|
+
const lines = content.split(String.fromCharCode(10));
|
|
65
|
+
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
66
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
67
|
+
const drop = new Set<number>();
|
|
68
|
+
for (let i = 0; i < rootEnd; i++) {
|
|
69
|
+
const line = lines[i]!;
|
|
70
|
+
if (!isRootOpenaiBaseUrlLine(line)) continue;
|
|
71
|
+
if (rootTomlString(line, "openai_base_url") !== injectedUrl) continue;
|
|
72
|
+
drop.add(i);
|
|
73
|
+
// Take an ownership marker directly above it too, so repeated cycles cannot
|
|
74
|
+
// accumulate orphaned comments.
|
|
75
|
+
if (i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER)) drop.add(i - 1);
|
|
76
|
+
}
|
|
77
|
+
if (drop.size === 0) return content;
|
|
78
|
+
return lines.filter((_, i) => !drop.has(i)).join(String.fromCharCode(10));
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
export function hasInjectedOpenaiBaseUrl(content: string): boolean {
|
|
54
82
|
const lines = content.split("\n");
|
|
55
83
|
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
package/src/codex/journal.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { atomicWriteFile } from "../config";
|
|
5
|
-
import { hasInjectedCodexRouting } from "./injected-marker";
|
|
5
|
+
import { hasInjectedCodexRouting, rootTomlString } from "./injected-marker";
|
|
6
6
|
import { CODEX_HOME, CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "./paths";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -22,6 +22,24 @@ interface Journal {
|
|
|
22
22
|
originalProfile: string | null;
|
|
23
23
|
injectedConfigHash?: string;
|
|
24
24
|
injectedProfileHash?: string | null;
|
|
25
|
+
/**
|
|
26
|
+
* The exact root `openai_base_url` this injection wrote, when it wrote one.
|
|
27
|
+
*
|
|
28
|
+
* #1798: ownership used to be inferred from a marker COMMENT on the preceding line,
|
|
29
|
+
* which a reserializing Codex app deletes while keeping the value. Recording the value
|
|
30
|
+
* we actually wrote makes ownership provable from evidence rather than from formatting,
|
|
31
|
+
* and it is what lets restore tell OUR loopback URL apart from a gateway the user set.
|
|
32
|
+
*/
|
|
33
|
+
injectedOpenaiBaseUrl?: string | null;
|
|
34
|
+
/**
|
|
35
|
+
* The catalog path this injection actually wrote to.
|
|
36
|
+
*
|
|
37
|
+
* #1798: restore re-resolves the catalog from the CURRENT config, so a Codex app rewrite
|
|
38
|
+
* that dropped `model_catalog_json` sends restore to the default catalog while the
|
|
39
|
+
* proxy-written one is left routed. The injected path is the only durable record of which
|
|
40
|
+
* file we actually touched.
|
|
41
|
+
*/
|
|
42
|
+
injectedCatalogPath?: string | null;
|
|
25
43
|
pid: number;
|
|
26
44
|
timestamp: string;
|
|
27
45
|
}
|
|
@@ -96,9 +114,30 @@ export function markJournalInjectedState(config: string, profile: string | null)
|
|
|
96
114
|
if (journal.injectedConfigHash) return;
|
|
97
115
|
journal.injectedConfigHash = sha256(config) ?? undefined;
|
|
98
116
|
journal.injectedProfileHash = sha256(profile);
|
|
117
|
+
// Read from the bytes we are about to install, not from the file: another writer may
|
|
118
|
+
// already have rewritten it, and then the recorded value would describe their config.
|
|
119
|
+
journal.injectedOpenaiBaseUrl = rootTomlString(config, "openai_base_url");
|
|
120
|
+
journal.injectedCatalogPath = rootTomlString(config, "model_catalog_json");
|
|
99
121
|
atomicWriteFile(JOURNAL_PATH, JSON.stringify(journal));
|
|
100
122
|
}
|
|
101
123
|
|
|
124
|
+
/**
|
|
125
|
+
* The root `openai_base_url` the last injection wrote, or null when it wrote none.
|
|
126
|
+
*
|
|
127
|
+
* #1798: the fallback strip recognizes an injected URL by the marker COMMENT above it,
|
|
128
|
+
* and a Codex app rewrite keeps values while dropping comments. This is the evidence that
|
|
129
|
+
* survives such a rewrite, so restore can still prove the URL is ours -- and, just as
|
|
130
|
+
* importantly, prove that a DIFFERENT URL is not.
|
|
131
|
+
*/
|
|
132
|
+
export function journaledInjectedOpenaiBaseUrl(): string | null {
|
|
133
|
+
return readJournal()?.injectedOpenaiBaseUrl ?? null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The catalog path the last injection wrote to, or null when none was recorded. */
|
|
137
|
+
export function journaledInjectedCatalogPath(): string | null {
|
|
138
|
+
return readJournal()?.injectedCatalogPath ?? null;
|
|
139
|
+
}
|
|
140
|
+
|
|
102
141
|
export function removeJournal(): void {
|
|
103
142
|
try { unlinkSync(JOURNAL_PATH); } catch { /* ignore */ }
|
|
104
143
|
}
|