@yansigit/opencodex 2.33.0 → 2.35.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/README.md +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +141 -23
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +17 -1
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +116 -31
- package/src/adapters/cursor/native-exec.ts +38 -10
- package/src/adapters/cursor/protobuf-events.ts +28 -2
- package/src/adapters/cursor/protobuf-request.ts +93 -41
- package/src/adapters/cursor/request-builder.ts +39 -10
- package/src/adapters/cursor/tool-definitions.ts +27 -3
- package/src/adapters/cursor/tool-result-normalize.ts +51 -6
- package/src/adapters/cursor/types.ts +23 -4
- package/src/adapters/cursor.ts +170 -29
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +219 -36
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +335 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +29 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +6 -1
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +279 -159
- package/src/generated/model-metadata.ts +3 -0
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +116 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +18 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +65 -1
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +29 -1
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +157 -46
- package/src/providers/registry.ts +184 -71
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +43 -2
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +24 -0
- package/src/server/chat-completions.ts +26 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +180 -14
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/oauth-account-routes.ts +13 -3
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +33 -0
- package/src/server/request-log.ts +48 -21
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +575 -140
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +79 -1
- package/src/types/request.ts +18 -10
- package/src/types/tools.ts +30 -11
- package/src/types.ts +1 -0
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +126 -24
- package/src/usage/log.ts +18 -8
- package/src/usage/summary.ts +34 -12
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-DKLr4LTE.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
|
@@ -193,6 +193,12 @@ export function deriveComboCatalogModel(
|
|
|
193
193
|
? { supportsServiceTier: false }
|
|
194
194
|
: {}),
|
|
195
195
|
...(members.some(member => member.supportsReasoningSummaries === false) ? { supportsReasoningSummaries: false } : {}),
|
|
196
|
+
// A combo is only as capable as its least capable member. One member that cannot honour
|
|
197
|
+
// text.verbosity is enough to make the control a no-op for the whole combo, so the
|
|
198
|
+
// conservative false propagates — the same rule supportsReasoningSummaries uses above.
|
|
199
|
+
// Without this, routing a combo through an xAI or Kiro member re-advertised a control the
|
|
200
|
+
// upstream accepts and ignores.
|
|
201
|
+
...(members.some(member => member.supportsVerbosity === false) ? { supportsVerbosity: false } : {}),
|
|
196
202
|
...(members.every(member => member.codexToolMode === "shell")
|
|
197
203
|
? { codexToolMode: "shell" as const }
|
|
198
204
|
: {}),
|
|
@@ -5,7 +5,11 @@ import { isModelCacheGenerationCurrent } from "../model-cache";
|
|
|
5
5
|
import type { GenerationContext } from "../../lib/state-store-sweeper";
|
|
6
6
|
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
7
7
|
import { assertNotRealHomeUnderTest } from "../../lib/test-home-guard";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
CURSOR_KNOWN_UNCALLABLE_MODEL_IDS,
|
|
10
|
+
CURSOR_STATIC_MODELS,
|
|
11
|
+
inferCursorContextWindow,
|
|
12
|
+
} from "../../adapters/cursor/discovery";
|
|
9
13
|
import { cursorModelEffortLadder } from "../../adapters/cursor/effort-map";
|
|
10
14
|
import { generatedModelMetadata, type CatalogModel } from "./parsing";
|
|
11
15
|
|
|
@@ -494,6 +498,14 @@ export function registryLayerForModel(provider: string, modelId: string): ModelM
|
|
|
494
498
|
: {}),
|
|
495
499
|
};
|
|
496
500
|
}
|
|
501
|
+
// Quarantined models stay out of the routed catalog, but retain metadata for
|
|
502
|
+
// existing configurations and diagnostics.
|
|
503
|
+
if (CURSOR_KNOWN_UNCALLABLE_MODEL_IDS.has(modelId)) {
|
|
504
|
+
return {
|
|
505
|
+
contextWindow: inferCursorContextWindow(modelId),
|
|
506
|
+
reasoningEfforts: cursorModelEffortLadder(modelId) ?? [],
|
|
507
|
+
};
|
|
508
|
+
}
|
|
497
509
|
}
|
|
498
510
|
return undefined;
|
|
499
511
|
}
|
|
@@ -3,6 +3,9 @@ export const NATIVE_DAYBREAK_BLUE_MODEL = "gpt-daybreak-blue-latest";
|
|
|
3
3
|
|
|
4
4
|
/** Native ChatGPT/Codex ids whose availability is proven per authenticated account. */
|
|
5
5
|
export const ACCOUNT_GATED_NATIVE_OPENAI_MODELS: ReadonlySet<string> = new Set([
|
|
6
|
+
"gpt-5.6-sol",
|
|
7
|
+
"gpt-5.6-terra",
|
|
8
|
+
"gpt-5.6-luna",
|
|
6
9
|
NATIVE_DAYBREAK_BLUE_MODEL,
|
|
7
10
|
]);
|
|
8
11
|
|
|
@@ -58,8 +61,8 @@ export function nativeOpenAiCapabilitySourceSlug(slug: string): string {
|
|
|
58
61
|
* discover it on a clean install.
|
|
59
62
|
*
|
|
60
63
|
* Availability is not static: catalog sync and Pool routing require the account's authenticated
|
|
61
|
-
* `/models` roster to contain
|
|
62
|
-
* request. `disabledModels` remains the independent user visibility control.
|
|
64
|
+
* `/models` roster to contain account-gated slugs. An unconfirmed or unentitled account never
|
|
65
|
+
* receives the request. `disabledModels` remains the independent user visibility control.
|
|
63
66
|
*
|
|
64
67
|
* Devlog: 260816_codexrs_multiagent_v2_and_history_perf/011 §4-bis.
|
|
65
68
|
*/
|
|
@@ -238,6 +238,22 @@ export function readCodexCatalogPathForHome(codexHome: string): string {
|
|
|
238
238
|
return join(codexHome, "opencodex-catalog.json");
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Read the configured auto-review model from the root of Codex's config.toml (issue #1225).
|
|
243
|
+
* Stamped onto catalog entries as `auto_review_model_override` during sync so the auto-review
|
|
244
|
+
* subagent uses the operator's chosen model across catalog regenerations.
|
|
245
|
+
*/
|
|
246
|
+
export function readConfiguredAutoReviewModel(): string | null {
|
|
247
|
+
try {
|
|
248
|
+
const configPath = activeCodexConfigPath();
|
|
249
|
+
if (existsSync(configPath)) {
|
|
250
|
+
const toml = readFileSync(configPath, "utf-8");
|
|
251
|
+
return readRootTomlString(toml, "auto_review_model");
|
|
252
|
+
}
|
|
253
|
+
} catch { /* ignore */ }
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
241
257
|
export function parseCatalogJson(raw: string): RawCatalog | null {
|
|
242
258
|
try {
|
|
243
259
|
const cat = JSON.parse(raw);
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
serviceTierSupportFromPolicy,
|
|
39
39
|
} from "../../providers/service-tier";
|
|
40
40
|
import type { FastPolicyAuthority } from "../../providers/fastwire";
|
|
41
|
-
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
|
|
41
|
+
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../../providers/registry";
|
|
42
42
|
import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models";
|
|
43
43
|
import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
|
|
44
44
|
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
@@ -578,6 +578,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
|
|
|
578
578
|
re: prov.modelReasoningEfforts ?? null,
|
|
579
579
|
defRe: prov.modelDefaultReasoningEfforts ?? null,
|
|
580
580
|
rsSum: prov.modelSupportsReasoningSummaries ?? null,
|
|
581
|
+
verbosity: prov.modelSupportsVerbosity ?? null,
|
|
581
582
|
rsDel: prov.modelReasoningSummaryDelivery ?? null,
|
|
582
583
|
serviceTier: prov.modelSupportsServiceTier ?? null,
|
|
583
584
|
noVis: [...(prov.noVisionModels ?? [])].sort(),
|
|
@@ -646,8 +647,23 @@ function configuredReasoningSummarySupport(prov: OcxProviderConfig | undefined,
|
|
|
646
647
|
return modelRecordValue(prov.modelReasoningSummaryDelivery, id) !== undefined ? true : undefined;
|
|
647
648
|
}
|
|
648
649
|
|
|
649
|
-
|
|
650
|
+
function configuredVerbositySupport(name: string, prov: OcxProviderConfig | undefined, id: string): boolean | undefined {
|
|
651
|
+
const explicit = prov ? modelRecordValue(prov.modelSupportsVerbosity, id) : undefined;
|
|
652
|
+
if (explicit !== undefined) return explicit;
|
|
653
|
+
if (!prov) return undefined;
|
|
650
654
|
void name;
|
|
655
|
+
// Provider-wide fallback for ids the per-model map does not enumerate — a live-discovered
|
|
656
|
+
// model would otherwise re-advertise a control the upstream accepts and ignores.
|
|
657
|
+
//
|
|
658
|
+
// Read from the PROVIDER CONFIG, never from PROVIDER_REGISTRY. A gather flight captures its
|
|
659
|
+
// registry authority up front and forbids any later registry read, so consulting the registry
|
|
660
|
+
// here made a custom-destination flight fall back to "configured" instead of serving its own
|
|
661
|
+
// discovery result (tests/codex-gather-authority.test.ts). `applyVerbosityDefaults` in
|
|
662
|
+
// providers/derive.ts materializes the registry default into the config at seed/enrich time.
|
|
663
|
+
return prov.supportsVerbosity;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, model: CatalogModel, providerCap?: number): CatalogModel {
|
|
651
667
|
const configuredCap = configuredContextWindow(prov, model.id);
|
|
652
668
|
const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
|
|
653
669
|
const configuredAutoCompact = configuredAutoCompactTokenLimit(prov, model.id);
|
|
@@ -663,6 +679,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
|
|
|
663
679
|
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
|
|
664
680
|
const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
|
|
665
681
|
const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
|
|
682
|
+
const supportsVerbosity = configuredVerbositySupport(name, prov, model.id);
|
|
666
683
|
const fastPolicy = fastPolicyForModel(prov, model.id, name);
|
|
667
684
|
const supportsServiceTier = serviceTierSupportFromPolicy(fastPolicy);
|
|
668
685
|
const {
|
|
@@ -691,11 +708,11 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
|
|
|
691
708
|
: {}),
|
|
692
709
|
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
693
710
|
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
|
|
711
|
+
...(typeof supportsVerbosity === "boolean" ? { supportsVerbosity } : {}),
|
|
694
712
|
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
|
|
695
713
|
...(supportsServiceTier === true && fastPolicy.fastTierDescription !== undefined
|
|
696
714
|
? { fastTierDescription: fastPolicy.fastTierDescription }
|
|
697
715
|
: {}),
|
|
698
|
-
...(prov.adapter === "kiro" ? { supportsVerbosity: false } : {}),
|
|
699
716
|
// Default-on for openai-chat providers (explicit false opts out); other adapters
|
|
700
717
|
// advertise only on explicit opt-in.
|
|
701
718
|
...(prov.parallelToolCalls === true || (prov.adapter === "openai-chat" && prov.parallelToolCalls !== false)
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from "../model-entitlements";
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readCodexCatalogPathForHome, readNativeBaseline } from "./parsing";
|
|
44
|
+
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readCodexCatalogPathForHome, readConfiguredAutoReviewModel, readNativeBaseline } from "./parsing";
|
|
45
45
|
import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
|
|
46
46
|
import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata";
|
|
47
47
|
import {
|
|
@@ -1403,6 +1403,130 @@ function catalogModelsForMergeWithNativeRecovery(
|
|
|
1403
1403
|
]);
|
|
1404
1404
|
}
|
|
1405
1405
|
|
|
1406
|
+
const AUTO_REVIEW_MODEL_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\s]/;
|
|
1407
|
+
|
|
1408
|
+
export function isValidAutoReviewModel(value: unknown): value is string {
|
|
1409
|
+
if (typeof value !== "string") return false;
|
|
1410
|
+
const trimmed = value.trim();
|
|
1411
|
+
return Boolean(trimmed)
|
|
1412
|
+
&& trimmed.length <= 1024
|
|
1413
|
+
&& !AUTO_REVIEW_MODEL_CONTROL_CHARS.test(trimmed);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
export type AutoReviewModelOverrideResult = "absent" | "applied" | "invalid" | "unresolved";
|
|
1417
|
+
|
|
1418
|
+
function isRoutedCatalogEntry(entry: RawEntry): boolean {
|
|
1419
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1420
|
+
return slug.includes("/")
|
|
1421
|
+
|| (typeof entry.description === "string" && entry.description.startsWith("Routed via opencodex → "));
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function clearAutoReviewModelOverride(
|
|
1425
|
+
models: readonly RawEntry[],
|
|
1426
|
+
sourceModels: readonly RawEntry[] = [],
|
|
1427
|
+
): void {
|
|
1428
|
+
const observedModels = [...models, ...sourceModels];
|
|
1429
|
+
const configuredValues = new Set(observedModels.flatMap(entry => {
|
|
1430
|
+
const value = entry?.auto_review_model_override;
|
|
1431
|
+
return typeof value === "string" && value.trim() ? [value] : [];
|
|
1432
|
+
}));
|
|
1433
|
+
const globalStamp = configuredValues.size === 1
|
|
1434
|
+
&& observedModels.some(entry => {
|
|
1435
|
+
const value = entry.auto_review_model_override;
|
|
1436
|
+
return isRoutedCatalogEntry(entry)
|
|
1437
|
+
&& typeof value === "string"
|
|
1438
|
+
&& value.trim().length > 0
|
|
1439
|
+
&& configuredValues.has(value);
|
|
1440
|
+
})
|
|
1441
|
+
&& observedModels.every(entry => {
|
|
1442
|
+
const value = entry?.auto_review_model_override;
|
|
1443
|
+
return value === null
|
|
1444
|
+
|| value === undefined
|
|
1445
|
+
|| (typeof value === "string" && configuredValues.has(value));
|
|
1446
|
+
});
|
|
1447
|
+
for (const entry of models) {
|
|
1448
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1449
|
+
const current = entry.auto_review_model_override;
|
|
1450
|
+
if (isRoutedCatalogEntry(entry)
|
|
1451
|
+
|| (globalStamp && typeof current === "string" && configuredValues.has(current))) {
|
|
1452
|
+
entry.auto_review_model_override = null;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function warnAutoReviewModelDiagnostic(
|
|
1458
|
+
reason: "invalid" | "unresolved",
|
|
1459
|
+
configured: string,
|
|
1460
|
+
): void {
|
|
1461
|
+
const safeConfigured = JSON.stringify(redactSecretString(configured));
|
|
1462
|
+
const detail = reason === "unresolved"
|
|
1463
|
+
? "the selector was not found in the final catalog"
|
|
1464
|
+
: "the selector format is invalid";
|
|
1465
|
+
console.warn(
|
|
1466
|
+
`[opencodex] auto_review_model ${detail} (${safeConfigured}); preserving normal upstream auto-review behavior.`,
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function preserveNativeAutoReviewModelOverrides(
|
|
1471
|
+
models: readonly RawEntry[],
|
|
1472
|
+
sourceModels: readonly RawEntry[],
|
|
1473
|
+
): void {
|
|
1474
|
+
const existing = new Map<string, string | null>();
|
|
1475
|
+
for (const entry of sourceModels) {
|
|
1476
|
+
const slug = typeof entry.slug === "string" ? entry.slug : undefined;
|
|
1477
|
+
const value = entry.auto_review_model_override;
|
|
1478
|
+
if (!slug || isRoutedCatalogEntry(entry)) continue;
|
|
1479
|
+
if (typeof value === "string" || value === null) existing.set(slug, value);
|
|
1480
|
+
}
|
|
1481
|
+
for (const entry of models) {
|
|
1482
|
+
const slug = typeof entry.slug === "string" ? entry.slug : undefined;
|
|
1483
|
+
if (!slug || isRoutedCatalogEntry(entry) || !existing.has(slug)) continue;
|
|
1484
|
+
entry.auto_review_model_override = existing.get(slug) ?? null;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
export function applyAutoReviewModelOverride(
|
|
1489
|
+
models: RawEntry[] | undefined,
|
|
1490
|
+
autoReviewModel: string | null | undefined,
|
|
1491
|
+
sourceModels: readonly RawEntry[] = [],
|
|
1492
|
+
): AutoReviewModelOverrideResult {
|
|
1493
|
+
if (!models || !Array.isArray(models)) return "absent";
|
|
1494
|
+
if (autoReviewModel === null || autoReviewModel === undefined) {
|
|
1495
|
+
clearAutoReviewModelOverride(models, sourceModels);
|
|
1496
|
+
return "absent";
|
|
1497
|
+
}
|
|
1498
|
+
const trimmed = autoReviewModel.trim();
|
|
1499
|
+
if (!trimmed) {
|
|
1500
|
+
clearAutoReviewModelOverride(models, sourceModels);
|
|
1501
|
+
return "absent";
|
|
1502
|
+
}
|
|
1503
|
+
if (!isValidAutoReviewModel(trimmed)) {
|
|
1504
|
+
clearAutoReviewModelOverride(models, sourceModels);
|
|
1505
|
+
warnAutoReviewModelDiagnostic("invalid", trimmed);
|
|
1506
|
+
return "invalid";
|
|
1507
|
+
}
|
|
1508
|
+
if (!configuredCatalogEntry(models, trimmed)) {
|
|
1509
|
+
clearAutoReviewModelOverride(models, sourceModels);
|
|
1510
|
+
warnAutoReviewModelDiagnostic("unresolved", trimmed);
|
|
1511
|
+
return "unresolved";
|
|
1512
|
+
}
|
|
1513
|
+
for (const entry of models) {
|
|
1514
|
+
if (entry && typeof entry === "object") {
|
|
1515
|
+
entry.auto_review_model_override = trimmed;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return "applied";
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/** Apply the root Codex auto-review selector after the final catalog merge. */
|
|
1522
|
+
export function finalizeAutoReviewModelOverride(
|
|
1523
|
+
models: RawEntry[] | undefined,
|
|
1524
|
+
sourceModels: readonly RawEntry[] = [],
|
|
1525
|
+
): AutoReviewModelOverrideResult {
|
|
1526
|
+
if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(models, sourceModels);
|
|
1527
|
+
return applyAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), sourceModels);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1406
1530
|
function writeRetainedCatalogSync({
|
|
1407
1531
|
config,
|
|
1408
1532
|
goModels,
|
|
@@ -1596,6 +1720,7 @@ function writeRetainedCatalogSync({
|
|
|
1596
1720
|
},
|
|
1597
1721
|
});
|
|
1598
1722
|
clampCatalogModelsToCodexSupport(catalog.models);
|
|
1723
|
+
finalizeAutoReviewModelOverride(catalog.models, catalogModelsForMerge);
|
|
1599
1724
|
|
|
1600
1725
|
const added = goEntries.length + accountBoundEntries.length;
|
|
1601
1726
|
const content = `${JSON.stringify(catalog, null, 2)}\n`;
|
|
@@ -1833,10 +1958,10 @@ export function invalidateCodexModelsCacheWithPermit(
|
|
|
1833
1958
|
// keeps the cache consistent with the catalog it just wrote.
|
|
1834
1959
|
if (!shouldSyncCodexOnStart(loadConfig()) && options?.allowWhenDesiredDisabled !== true) return false;
|
|
1835
1960
|
const catalogPath = readCodexCatalogPathForHome(owningCodexHome);
|
|
1836
|
-
const cachePath = join(owningCodexHome, "models_cache.json");
|
|
1837
1961
|
if (!existsSync(catalogPath)) return false;
|
|
1838
1962
|
const catalog = JSON.parse(readFileSync(catalogPath, "utf8"));
|
|
1839
1963
|
const models = catalog.models ?? catalog;
|
|
1964
|
+
const cachePath = join(owningCodexHome, "models_cache.json");
|
|
1840
1965
|
const currentCache = readCatalog(cachePath);
|
|
1841
1966
|
const existingSlugs = new Set(models.flatMap((entry: RawEntry) =>
|
|
1842
1967
|
typeof entry.slug === "string" ? [entry.slug] : []));
|
package/src/codex/catalog.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, c
|
|
|
8
8
|
export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithMetadata, resolveComboCatalogMember, configuredComboTargetModelsByProvider } from "./catalog/provider-fetch";
|
|
9
9
|
export { deriveComboCatalogModel, exactComboCatalogSlugs, getLastComboCatalogOmissions, resetOpenAiApiCatalogWarningStateForTests, uniqueCatalogModelsForPublicList, uniqueCatalogModelsForRawPublicList, buildComboCatalogOmission, comboCatalogOmissionReason, summarizeComboCatalogOmissions } from "./catalog/aggregation";
|
|
10
10
|
export type { ComboCatalogOmission, ComboCatalogOmissionReason } from "./catalog/aggregation";
|
|
11
|
-
export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, effectiveSubagentRoster, buildCatalogEntries, mergeCatalogEntriesFromObservedState, resetCatalogRuntimeStateForTests, orderForSubagents, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache } from "./catalog/sync";
|
|
11
|
+
export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, effectiveSubagentRoster, buildCatalogEntries, mergeCatalogEntriesFromObservedState, resetCatalogRuntimeStateForTests, orderForSubagents, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache, finalizeAutoReviewModelOverride } from "./catalog/sync";
|
|
12
12
|
export type { ObservedCatalogMergeInput } from "./catalog/sync";
|
|
13
13
|
export type { SpawnAgentSurface, SubagentRosterExclusionReason, EffectiveSubagentModel, SubagentRosterExclusion, EffectiveSubagentRoster } from "./catalog/sync";
|
|
14
14
|
export { accountBoundNativeDisplayName, accountBoundNativeModelSlugs, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./catalog/account-models";
|
|
@@ -93,6 +93,8 @@ export interface CodexWriteLockOptions {
|
|
|
93
93
|
admitted: CodexWriteWitness;
|
|
94
94
|
/** Authoritative synchronous re-read while N and C are both held. */
|
|
95
95
|
readAdmissionUnderLock(): CodexWriteWitness;
|
|
96
|
+
/** Positively authorized migration of an already-routed pre-substrate home. */
|
|
97
|
+
adoption?: { readonly direction: "apply" | "remove" };
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
/**
|
|
@@ -304,7 +306,7 @@ export async function withCodexWriteLock<T>(
|
|
|
304
306
|
|
|
305
307
|
let transaction: ReturnType<typeof openCodexCoordinatorTransaction> | undefined;
|
|
306
308
|
try {
|
|
307
|
-
transaction = openCodexCoordinatorTransaction(databasePath);
|
|
309
|
+
transaction = openCodexCoordinatorTransaction(databasePath, options.adoption);
|
|
308
310
|
} catch (error) {
|
|
309
311
|
// Only contention retries. A malformed database, an unsafe path, or an
|
|
310
312
|
// identity failure will fail identically forever; telling a caller to retry
|
|
@@ -34,7 +34,7 @@ export interface CodexIntegrationRecord {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export interface CodexHistoryState {
|
|
37
|
-
status: "converged" | "pending" | "running" | "blocked" | "unknown" | "not-evaluated";
|
|
37
|
+
status: "adoption-pending" | "converged" | "pending" | "running" | "blocked" | "unknown" | "not-evaluated";
|
|
38
38
|
/**
|
|
39
39
|
* Why it is not converged, when it is not. These are terminal observations
|
|
40
40
|
* for one attempt, not reasons to collapse the durable retry schedule.
|
package/src/codex/convergence.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
|
|
3
|
-
import { getConfigDir, websocketsEnabled, withExpectedConfigGenerationSync } from "../config";
|
|
3
|
+
import { getConfigDir, saveConfigPreservingClaudeCode, websocketsEnabled, withExpectedConfigGenerationSync } from "../config";
|
|
4
|
+
import { reconcileSuccessfulModelDiscoveries } from "../providers/new-model-policy";
|
|
4
5
|
import { COMBO_NAMESPACE } from "../combos";
|
|
5
6
|
import { getAuthStorePath } from "../oauth/store";
|
|
6
7
|
import type { OcxConfig } from "../types";
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
import {
|
|
42
43
|
buildCatalogEntriesFromObservedState,
|
|
43
44
|
CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
45
|
+
finalizeAutoReviewModelOverride,
|
|
44
46
|
mergeCatalogEntriesFromObservedState,
|
|
45
47
|
mergeCatalogModelsWithNativeRecovery,
|
|
46
48
|
orderForSubagents,
|
|
@@ -135,6 +137,7 @@ interface CandidateState {
|
|
|
135
137
|
readonly changed: boolean;
|
|
136
138
|
readonly notices: readonly CatalogNotice[];
|
|
137
139
|
readonly modelEntitlements: CodexModelEntitlementSnapshot;
|
|
140
|
+
readonly discoveryConfig?: OcxConfig;
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
const candidateStates = new WeakMap<object, CandidateState>();
|
|
@@ -369,6 +372,7 @@ function prepareCatalog(
|
|
|
369
372
|
? supportedCodexReasoningEffortsFromObservedCatalog(source.runtimeSupport.catalog)
|
|
370
373
|
: null,
|
|
371
374
|
);
|
|
375
|
+
finalizeAutoReviewModelOverride(mergedModels, catalogModels);
|
|
372
376
|
catalog.models = mergedModels;
|
|
373
377
|
return catalog;
|
|
374
378
|
}
|
|
@@ -436,8 +440,17 @@ export async function gatherCodexCatalogCandidate(
|
|
|
436
440
|
? active
|
|
437
441
|
: !hasRoutedEntries(source.catalog) ? source.catalog : null)
|
|
438
442
|
: null);
|
|
443
|
+
const discoveryConfig = structuredClone(snapshot.config) as OcxConfig;
|
|
444
|
+
const discoveryChanged = reconcileSuccessfulModelDiscoveries({
|
|
445
|
+
config: discoveryConfig,
|
|
446
|
+
models: routedModels,
|
|
447
|
+
authoritativeProviders: providerModelOutcomes
|
|
448
|
+
.filter(outcome => outcome.state === "authoritative")
|
|
449
|
+
.map(outcome => outcome.provider),
|
|
450
|
+
now: new Date().toISOString(),
|
|
451
|
+
});
|
|
439
452
|
const preparedCatalog = prepareCatalog(
|
|
440
|
-
|
|
453
|
+
discoveryConfig,
|
|
441
454
|
source,
|
|
442
455
|
active,
|
|
443
456
|
routedModels,
|
|
@@ -502,6 +515,7 @@ export async function gatherCodexCatalogCandidate(
|
|
|
502
515
|
|| Buffer.from(cacheBytes ?? []).toString("utf8") !== preparedCacheBytes,
|
|
503
516
|
notices: Object.freeze([...notices]),
|
|
504
517
|
modelEntitlements,
|
|
518
|
+
...(discoveryChanged ? { discoveryConfig } : {}),
|
|
505
519
|
});
|
|
506
520
|
return { kind: "candidate", candidate };
|
|
507
521
|
} catch (error) {
|
|
@@ -649,6 +663,12 @@ export async function convergeCodexCatalog(
|
|
|
649
663
|
const state = candidateStates.get(gathered.candidate as object)!;
|
|
650
664
|
lifecycle.onCommitBegin?.();
|
|
651
665
|
const committed = await commitCodexCatalogCandidate(gathered.candidate, request.deadlineMs);
|
|
666
|
+
if (committed.kind === "committed" && state.discoveryConfig) {
|
|
667
|
+
const mutable = snapshot.config as OcxConfig;
|
|
668
|
+
mutable.modelDiscovery = state.discoveryConfig.modelDiscovery;
|
|
669
|
+
mutable.disabledModels = state.discoveryConfig.disabledModels;
|
|
670
|
+
saveConfigPreservingClaudeCode(mutable);
|
|
671
|
+
}
|
|
652
672
|
return {
|
|
653
673
|
changed: committed.kind === "committed" ? committed.changed : false,
|
|
654
674
|
catalogRefresh: projectCommit(committed, state.notices),
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Design record: devlog/_fin/260803_codex_desktop_toggle/030_desired_state.md.
|
|
22
22
|
*/
|
|
23
|
-
import { loadConfig, mutatePersistedConfig } from "../config";
|
|
23
|
+
import { deleteConfigTopLevelKey, loadConfig, mutatePersistedConfig } from "../config";
|
|
24
24
|
import type { OcxClientIntegrationsConfig, OcxConfig } from "../types";
|
|
25
25
|
import { runStartupReadinessSync, type ReadinessGate, type SyncOutcomeLike } from "../server/readiness";
|
|
26
26
|
|
|
@@ -114,7 +114,7 @@ export function setIntegrationEnabled(
|
|
|
114
114
|
}
|
|
115
115
|
// Drop the key entirely once nothing is left in it, so enabling twice does
|
|
116
116
|
// not leave `"clientIntegrations": {}` behind in the user's file.
|
|
117
|
-
if (Object.keys(integrations).length === 0)
|
|
117
|
+
if (Object.keys(integrations).length === 0) deleteConfigTopLevelKey(config, "clientIntegrations");
|
|
118
118
|
else config.clientIntegrations = integrations;
|
|
119
119
|
return { changed: true, value: enabled };
|
|
120
120
|
});
|
|
@@ -43,6 +43,7 @@ export interface DesktopAppRestartIo {
|
|
|
43
43
|
export type DesktopAppRestartReason =
|
|
44
44
|
| "windows_only"
|
|
45
45
|
| "package_discovery_failed"
|
|
46
|
+
| "process_probe_failed"
|
|
46
47
|
| "no_targets"
|
|
47
48
|
| "self_ancestry"
|
|
48
49
|
| "targets_survived";
|
|
@@ -119,7 +120,7 @@ interface DesktopProcess {
|
|
|
119
120
|
function listPackageProcesses(
|
|
120
121
|
exec: NonNullable<DesktopAppRestartIo["execFile"]>,
|
|
121
122
|
installLocation: string,
|
|
122
|
-
): DesktopProcess[] {
|
|
123
|
+
): DesktopProcess[] | null {
|
|
123
124
|
const literal = installLocation.replace(/'/g, "''");
|
|
124
125
|
const script = [
|
|
125
126
|
"$ErrorActionPreference='SilentlyContinue'",
|
|
@@ -136,7 +137,10 @@ function listPackageProcesses(
|
|
|
136
137
|
" }",
|
|
137
138
|
" }",
|
|
138
139
|
" }",
|
|
139
|
-
|
|
140
|
+
// Statements must be newline-separated. Joining with a space concatenates
|
|
141
|
+
// `$ErrorActionPreference='SilentlyContinue' $root = '...'` into one malformed statement,
|
|
142
|
+
// which PowerShell rejects — so the probe threw and every caller read "not running" (#2557).
|
|
143
|
+
].join("\n");
|
|
140
144
|
let stdout: string;
|
|
141
145
|
try {
|
|
142
146
|
stdout = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
@@ -144,7 +148,10 @@ function listPackageProcesses(
|
|
|
144
148
|
windowsHide: true,
|
|
145
149
|
});
|
|
146
150
|
} catch {
|
|
147
|
-
|
|
151
|
+
// A probe that could not run is NOT proof the app is absent. Returning [] here made a
|
|
152
|
+
// failed enumeration indistinguishable from "no targets", so the CLI reported the app as
|
|
153
|
+
// not running and skipped a restart the user had explicitly asked for.
|
|
154
|
+
return null;
|
|
148
155
|
}
|
|
149
156
|
const processes: DesktopProcess[] = [];
|
|
150
157
|
for (const line of stdout.split(/\r?\n/)) {
|
|
@@ -170,8 +177,11 @@ function stillSameProcess(
|
|
|
170
177
|
installLocation: string,
|
|
171
178
|
target: DesktopProcess,
|
|
172
179
|
): boolean {
|
|
173
|
-
const
|
|
174
|
-
|
|
180
|
+
const processes = listPackageProcesses(exec, installLocation);
|
|
181
|
+
// Fail CLOSED on a failed re-probe: this guards a kill, and "we could not look" must not be
|
|
182
|
+
// read as "the pid was recycled and is now someone else's process".
|
|
183
|
+
if (processes === null) return false;
|
|
184
|
+
const current = processes.find(p => p.pid === target.pid);
|
|
175
185
|
return current !== undefined && current.createdAt === target.createdAt;
|
|
176
186
|
}
|
|
177
187
|
|
|
@@ -266,6 +276,9 @@ export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopApp
|
|
|
266
276
|
if (!pkg) return skipped("package_discovery_failed");
|
|
267
277
|
|
|
268
278
|
const processes = listPackageProcesses(exec, pkg.installLocation);
|
|
279
|
+
// A probe that could not run is not evidence of absence. Reporting it as `no_targets` told
|
|
280
|
+
// the user the app was not running and silently skipped the restart they asked for (#2557).
|
|
281
|
+
if (processes === null) return skipped("process_probe_failed");
|
|
269
282
|
const roots = rootProcesses(processes);
|
|
270
283
|
if (roots.length === 0) return skipped("no_targets");
|
|
271
284
|
|
|
@@ -11,7 +11,12 @@ import { atomicWriteFile } from "../config";
|
|
|
11
11
|
import type { CodexWriteLockResult } from "./codex-write-lock";
|
|
12
12
|
import { inspectCodexCoordinatorPath } from "./coordinator-doctor";
|
|
13
13
|
import { JOURNAL_PATH } from "./journal";
|
|
14
|
+
import { updateIntegrationRecord } from "./integration-record";
|
|
14
15
|
import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "./paths";
|
|
16
|
+
import type {
|
|
17
|
+
CodexArtifactId,
|
|
18
|
+
CodexProvenanceEntry,
|
|
19
|
+
} from "./convergence-types";
|
|
15
20
|
import {
|
|
16
21
|
codexWriteCoordination,
|
|
17
22
|
type CodexWriteCandidate,
|
|
@@ -41,6 +46,7 @@ export const DEFAULT_INJECT_LOCK_TIMEOUT_MS = 5_000;
|
|
|
41
46
|
*/
|
|
42
47
|
export type CodexWriteCoordinationEligibility =
|
|
43
48
|
| { kind: "coordinated" }
|
|
49
|
+
| { kind: "adopt" }
|
|
44
50
|
| { kind: "legacy-uncoordinated"; reason: string }
|
|
45
51
|
| { kind: "refused"; reason: string };
|
|
46
52
|
|
|
@@ -97,6 +103,7 @@ export function codexWriteCoordinationEligibility(deps: {
|
|
|
97
103
|
|
|
98
104
|
const residue = deps.residue();
|
|
99
105
|
if (residue.kind === "clean") return { kind: "coordinated" };
|
|
106
|
+
if (residue.kind === "residue" && !coordinatorIsStableZeroByte) return { kind: "adopt" };
|
|
100
107
|
/*
|
|
101
108
|
* Everything else keeps the path it has always had.
|
|
102
109
|
*
|
|
@@ -176,6 +183,82 @@ export function captureCodexPreImages(): CodexPreImages {
|
|
|
176
183
|
};
|
|
177
184
|
}
|
|
178
185
|
|
|
186
|
+
/**
|
|
187
|
+
* How many transactions of evidence the ledger keeps.
|
|
188
|
+
*
|
|
189
|
+
* Each transaction appends three entries, and a `present` baseline embeds the artifact's exact
|
|
190
|
+
* bytes as base64 — a 25 KB `config.toml` is ~34 KB per entry, so roughly 100 KB per transaction.
|
|
191
|
+
* A machine that syncs on every start would grow this file without limit, and it is re-read and
|
|
192
|
+
* re-serialized on every append, so the cost is quadratic rather than merely large.
|
|
193
|
+
*
|
|
194
|
+
* A ledger is evidence, not an archive. The most recent transactions are the ones anyone
|
|
195
|
+
* diagnoses against, so the window keeps those and drops the oldest.
|
|
196
|
+
*/
|
|
197
|
+
export const CODEX_PROVENANCE_MAX_TRANSACTIONS = 16;
|
|
198
|
+
|
|
199
|
+
function provenanceBaseline(bytes: string | null): CodexProvenanceEntry["baseline"] {
|
|
200
|
+
if (bytes === null) return { kind: "absent" };
|
|
201
|
+
return {
|
|
202
|
+
kind: "present",
|
|
203
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
204
|
+
bytesBase64: Buffer.from(bytes).toString("base64"),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function provenancePostImage(path: string): string | null {
|
|
209
|
+
try {
|
|
210
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Keep the newest `CODEX_PROVENANCE_MAX_TRANSACTIONS` transactions, whole.
|
|
218
|
+
*
|
|
219
|
+
* Trimming by ENTRY count would cut a transaction in half and leave evidence that says a
|
|
220
|
+
* transaction touched two artifacts when it touched three — worse than dropping it outright,
|
|
221
|
+
* because a partial record still reads as complete. Order is preserved; only whole leading
|
|
222
|
+
* transactions are removed.
|
|
223
|
+
*/
|
|
224
|
+
export function boundProvenanceEntries(
|
|
225
|
+
entries: readonly CodexProvenanceEntry[],
|
|
226
|
+
maxTransactions = CODEX_PROVENANCE_MAX_TRANSACTIONS,
|
|
227
|
+
): readonly CodexProvenanceEntry[] {
|
|
228
|
+
const order: string[] = [];
|
|
229
|
+
for (const entry of entries) if (!order.includes(entry.txId)) order.push(entry.txId);
|
|
230
|
+
if (order.length <= maxTransactions) return entries;
|
|
231
|
+
const keep = new Set(order.slice(order.length - maxTransactions));
|
|
232
|
+
return entries.filter(entry => keep.has(entry.txId));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Append evidence for an already-committed native transaction, best-effort. */
|
|
236
|
+
export function recordCodexNativeTransactionProvenance(
|
|
237
|
+
preImages: CodexPreImages,
|
|
238
|
+
txId: string,
|
|
239
|
+
) {
|
|
240
|
+
const at = new Date().toISOString();
|
|
241
|
+
const surfaces: readonly [CodexArtifactId, string, string | null][] = [
|
|
242
|
+
[{ kind: "config" }, CODEX_CONFIG_PATH, preImages.config],
|
|
243
|
+
[{ kind: "generated-profile" }, CODEX_PROFILE_PATH, preImages.profile],
|
|
244
|
+
[{ kind: "injection-journal" }, JOURNAL_PATH, preImages.journal],
|
|
245
|
+
];
|
|
246
|
+
const entries: readonly CodexProvenanceEntry[] = surfaces.map(([artifact, path, baseline]) => ({
|
|
247
|
+
artifact,
|
|
248
|
+
baseline: provenanceBaseline(baseline),
|
|
249
|
+
postImage: provenancePostImage(path),
|
|
250
|
+
txId,
|
|
251
|
+
at,
|
|
252
|
+
}));
|
|
253
|
+
return updateIntegrationRecord(record => ({
|
|
254
|
+
...record,
|
|
255
|
+
provenance: {
|
|
256
|
+
...record.provenance,
|
|
257
|
+
entries: boundProvenanceEntries([...(record.provenance?.entries ?? []), ...entries]),
|
|
258
|
+
},
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
|
|
179
262
|
/**
|
|
180
263
|
* Put back exactly what was there, and report honestly when that fails.
|
|
181
264
|
*
|