@bitkyc08/opencodex 2.33.0 → 2.34.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-C4TMRloX.js +112 -0
- package/gui/dist/assets/index-DQ-Ie18T.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 +40 -3
- 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 +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +29 -6
- package/src/adapters/cursor/tool-definitions.ts +11 -2
- package/src/adapters/cursor/tool-result-normalize.ts +32 -1
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +189 -37
- package/src/adapters/google-antigravity-replay.ts +102 -23
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +115 -16
- 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 +334 -24
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -24
- package/src/chat/inbound.ts +14 -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 +32 -4
- package/src/cli/account-extended.ts +1 -1
- 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/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/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 +46 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- 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 +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +104 -25
- 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/rebase-provenance.ts +68 -0
- package/src/config.ts +107 -9
- package/src/generated/compatibility-version.json +191 -139
- 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/generic-account-failover.ts +231 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/types.ts +15 -0
- 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 +28 -0
- 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 +65 -7
- package/src/providers/registry.ts +160 -68
- 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 +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +24 -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/index.ts +77 -11
- 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/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +6 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +21 -0
- package/src/server/request-log.ts +41 -15
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +494 -114
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- 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/types/config.ts +53 -0
- package/src/types/provider.ts +64 -0
- package/src/types/request.ts +11 -3
- package/src/types/tools.ts +7 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +3 -0
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-23-Lf7jR.js +0 -102
- package/gui/dist/assets/index-DxJMDyOr.css +0 -1
|
@@ -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
|
*
|
package/src/codex/inject.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
CodexWriteConflictError,
|
|
20
20
|
DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
21
21
|
recomputeInjectWitness,
|
|
22
|
+
recordCodexNativeTransactionProvenance,
|
|
22
23
|
restoreCodexPreImages,
|
|
23
24
|
} from "./inject-coordination";
|
|
24
25
|
import { readIntegrationRecord } from "./integration-record";
|
|
@@ -953,6 +954,7 @@ export async function injectCodexConfig(
|
|
|
953
954
|
const coordinated = await withCodexWriteLock(
|
|
954
955
|
{
|
|
955
956
|
timeoutMs: options.lockTimeoutMs ?? DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
957
|
+
...(eligibility.kind === "adopt" ? { adoption: { direction: "apply" as const } } : {}),
|
|
956
958
|
admitted: { authoritySnapshotId: witness.comparisonId },
|
|
957
959
|
readAdmissionUnderLock: () => ({
|
|
958
960
|
authoritySnapshotId: recomputeInjectWitness({
|
|
@@ -1019,6 +1021,7 @@ export async function injectCodexConfig(
|
|
|
1019
1021
|
}
|
|
1020
1022
|
return {
|
|
1021
1023
|
kind: "applied" as const,
|
|
1024
|
+
preImages,
|
|
1022
1025
|
/*
|
|
1023
1026
|
* The receipt the terminal update matches on. The transition commits
|
|
1024
1027
|
* when the callback returns, so this pair is what the post-job
|
|
@@ -1036,6 +1039,10 @@ export async function injectCodexConfig(
|
|
|
1036
1039
|
if (coordinated.status !== "acquired") {
|
|
1037
1040
|
return codexInjectLockOutcome(coordinated);
|
|
1038
1041
|
}
|
|
1042
|
+
recordCodexNativeTransactionProvenance(
|
|
1043
|
+
coordinated.value.preImages,
|
|
1044
|
+
coordinated.value.receipt.currentTxId,
|
|
1045
|
+
);
|
|
1039
1046
|
transitionReceipt = coordinated.value.receipt;
|
|
1040
1047
|
}
|
|
1041
1048
|
// Legacy mode still forward-tags history so re-tagged threads stay listable. Design B needs
|
|
@@ -1547,13 +1554,14 @@ export async function restoreNativeCodexAsync(
|
|
|
1547
1554
|
let config: CodexRestoreConfigResult;
|
|
1548
1555
|
let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
|
|
1549
1556
|
|
|
1550
|
-
if (eligibility.kind === "coordinated") {
|
|
1557
|
+
if (eligibility.kind === "coordinated" || eligibility.kind === "adopt") {
|
|
1551
1558
|
// The restore has no candidate bytes to witness; freshness comes from the
|
|
1552
1559
|
// filesystem reads and the desired-state re-read performed under the lock.
|
|
1553
1560
|
const witness = { authoritySnapshotId: "codex-native-restore" };
|
|
1554
1561
|
const coordinated = await withCodexWriteLock(
|
|
1555
1562
|
{
|
|
1556
1563
|
timeoutMs: DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
1564
|
+
...(eligibility.kind === "adopt" ? { adoption: { direction: "remove" as const } } : {}),
|
|
1557
1565
|
admitted: witness,
|
|
1558
1566
|
readAdmissionUnderLock: () => witness,
|
|
1559
1567
|
},
|
|
@@ -1589,6 +1597,7 @@ export async function restoreNativeCodexAsync(
|
|
|
1589
1597
|
}
|
|
1590
1598
|
return {
|
|
1591
1599
|
config: restored,
|
|
1600
|
+
preImages,
|
|
1592
1601
|
receipt: {
|
|
1593
1602
|
nativeGeneration: ctx.expectation.nativeAfter,
|
|
1594
1603
|
currentTxId: ctx.expectation.txId,
|
|
@@ -1607,6 +1616,10 @@ export async function restoreNativeCodexAsync(
|
|
|
1607
1616
|
: `Codex configuration was not restored: ${coordinated.message}`,
|
|
1608
1617
|
};
|
|
1609
1618
|
} else {
|
|
1619
|
+
recordCodexNativeTransactionProvenance(
|
|
1620
|
+
coordinated.value.preImages,
|
|
1621
|
+
coordinated.value.receipt.currentTxId,
|
|
1622
|
+
);
|
|
1610
1623
|
config = coordinated.value.config;
|
|
1611
1624
|
transitionReceipt = coordinated.value.receipt;
|
|
1612
1625
|
}
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
|
|
12
12
|
const IMMUTABLE_READONLY_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLITE_OPEN_URI;
|
|
13
13
|
const KNOWN_LOG_LEVELS = new Set(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]);
|
|
14
|
+
// The issue reporter measured a ~1 GB database taking 17.3s for GROUP BY level
|
|
15
|
+
// alone; skipping all row aggregates above 64 MiB reduced /api/storage to 628ms.
|
|
16
|
+
const MAX_SYNCHRONOUS_METRICS_DATABASE_BYTES = 64 * 1024 * 1024;
|
|
14
17
|
|
|
15
18
|
interface CurrentLogColumn {
|
|
16
19
|
name: string;
|
|
@@ -115,6 +118,10 @@ export interface CodexLogGuardInspection {
|
|
|
115
118
|
reclaim: CodexLogGuardCapability;
|
|
116
119
|
};
|
|
117
120
|
metrics: CodexLogGuardMetrics | null;
|
|
121
|
+
metricsSkipped: null | {
|
|
122
|
+
reason: "database_too_large";
|
|
123
|
+
thresholdBytes: number;
|
|
124
|
+
};
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
interface ColumnRow {
|
|
@@ -167,9 +174,8 @@ function fileSize(path: string): number {
|
|
|
167
174
|
* repeated stalls without ever serving stale numbers: any write changes the WAL
|
|
168
175
|
* and invalidates the entry.
|
|
169
176
|
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* tracked separately.
|
|
177
|
+
* Memoization bounds repeat cost. The database-size gate below separately bounds
|
|
178
|
+
* cold request-thread work by omitting these aggregates for large databases.
|
|
173
179
|
*/
|
|
174
180
|
type InspectionCacheEntry = {
|
|
175
181
|
key: string;
|
|
@@ -233,6 +239,7 @@ function unavailableInspection(): CodexLogGuardInspection {
|
|
|
233
239
|
reclaim: unavailable,
|
|
234
240
|
},
|
|
235
241
|
metrics: null,
|
|
242
|
+
metricsSkipped: null,
|
|
236
243
|
};
|
|
237
244
|
}
|
|
238
245
|
|
|
@@ -425,6 +432,7 @@ function inspectCodexLogsUncached(deps: CodexSqliteHomeDeps = {}): CodexLogGuard
|
|
|
425
432
|
...common,
|
|
426
433
|
schema,
|
|
427
434
|
metrics: null,
|
|
435
|
+
metricsSkipped: null,
|
|
428
436
|
capabilities: {
|
|
429
437
|
inspection: { state: "supported" },
|
|
430
438
|
protection: mutation,
|
|
@@ -440,6 +448,7 @@ function inspectCodexLogsUncached(deps: CodexSqliteHomeDeps = {}): CodexLogGuard
|
|
|
440
448
|
...common,
|
|
441
449
|
schema,
|
|
442
450
|
metrics: null,
|
|
451
|
+
metricsSkipped: null,
|
|
443
452
|
capabilities: {
|
|
444
453
|
inspection: { state: "supported" },
|
|
445
454
|
protection: mutation,
|
|
@@ -458,6 +467,7 @@ function inspectCodexLogsUncached(deps: CodexSqliteHomeDeps = {}): CodexLogGuard
|
|
|
458
467
|
...common,
|
|
459
468
|
schema,
|
|
460
469
|
metrics: null,
|
|
470
|
+
metricsSkipped: null,
|
|
461
471
|
capabilities: {
|
|
462
472
|
inspection: { state: "supported" },
|
|
463
473
|
protection: mutation,
|
|
@@ -476,10 +486,17 @@ function inspectCodexLogsUncached(deps: CodexSqliteHomeDeps = {}): CodexLogGuard
|
|
|
476
486
|
? { state: "compatible" }
|
|
477
487
|
: { state: "unsupported", reason: "unknown_schema" };
|
|
478
488
|
const mutation = capabilityFor(schema);
|
|
489
|
+
const metricsSkipped = files.databaseBytes > MAX_SYNCHRONOUS_METRICS_DATABASE_BYTES
|
|
490
|
+
? {
|
|
491
|
+
reason: "database_too_large" as const,
|
|
492
|
+
thresholdBytes: MAX_SYNCHRONOUS_METRICS_DATABASE_BYTES,
|
|
493
|
+
}
|
|
494
|
+
: null;
|
|
479
495
|
return {
|
|
480
496
|
...common,
|
|
481
497
|
schema,
|
|
482
|
-
metrics: readMetrics(db, columns),
|
|
498
|
+
metrics: metricsSkipped === null ? readMetrics(db, columns) : null,
|
|
499
|
+
metricsSkipped,
|
|
483
500
|
capabilities: {
|
|
484
501
|
inspection: { state: "supported" },
|
|
485
502
|
protection: mutation,
|
|
@@ -496,6 +513,7 @@ function inspectCodexLogsUncached(deps: CodexSqliteHomeDeps = {}): CodexLogGuard
|
|
|
496
513
|
...common,
|
|
497
514
|
schema,
|
|
498
515
|
metrics: null,
|
|
516
|
+
metricsSkipped: null,
|
|
499
517
|
capabilities: {
|
|
500
518
|
inspection: { state: "supported" },
|
|
501
519
|
protection: mutation,
|
|
@@ -40,6 +40,10 @@ export interface CodexModelEntitlementResolveOptions {
|
|
|
40
40
|
readonly now?: number;
|
|
41
41
|
/** Test-only credential seam; production callers enumerate local main + Pool credentials. */
|
|
42
42
|
readonly credentials?: readonly CodexModelEntitlementCredentialSnapshot[];
|
|
43
|
+
/** Test-only seam for proving lifecycle exclusions happen before credential reads. */
|
|
44
|
+
readonly credentialSnapshot?: typeof accountCredentialSnapshot;
|
|
45
|
+
/** Accounts whose credentials must not be read while another lifecycle owns them. */
|
|
46
|
+
readonly excludeAccountIds?: ReadonlySet<string>;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
const accountModelsCache = new Map<string, CachedAccountModels>();
|
|
@@ -252,9 +256,12 @@ export async function resolveCodexModelEntitlements(
|
|
|
252
256
|
): Promise<CodexModelEntitlementSnapshot> {
|
|
253
257
|
const now = options.now ?? Date.now();
|
|
254
258
|
const fetcher = options.fetcher ?? fetch;
|
|
259
|
+
const allowedAccountIds = candidateAccountIds(config)
|
|
260
|
+
.filter(accountId => !options.excludeAccountIds?.has(accountId));
|
|
261
|
+
const credentialSnapshot = options.credentialSnapshot ?? accountCredentialSnapshot;
|
|
255
262
|
const credentials = options.credentials
|
|
256
|
-
? [...options.credentials]
|
|
257
|
-
: (await Promise.all(
|
|
263
|
+
? [...options.credentials].filter(credential => !options.excludeAccountIds?.has(credential.accountId))
|
|
264
|
+
: (await Promise.all(allowedAccountIds.map(credentialSnapshot)))
|
|
258
265
|
.filter((value): value is CodexModelEntitlementCredentialSnapshot => value !== null);
|
|
259
266
|
const results = await Promise.all(credentials.map(async credential => ({
|
|
260
267
|
credential,
|