@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
package/src/config.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
OPENAI_PROVIDER_TIER_VERSION,
|
|
61
61
|
pinnedWireAdapter,
|
|
62
62
|
REASONING_SUMMARY_DELIVERY_VALUES,
|
|
63
|
+
UPSTREAM_HTTP_VERSION_VALUES,
|
|
63
64
|
type OcxClaudeCodeConfig,
|
|
64
65
|
type OcxConfig,
|
|
65
66
|
type OcxApiKeyEntry,
|
|
@@ -734,6 +735,12 @@ const providerConfigSchema = z.object({
|
|
|
734
735
|
modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(),
|
|
735
736
|
preserveResponsesReasoningContent: z.boolean().optional(),
|
|
736
737
|
allowPrivateNetwork: z.boolean().optional(),
|
|
738
|
+
// The management API accepts `null` as "clear this", so a config written before the POST
|
|
739
|
+
// canonicalization below can hold one on disk. Rejecting it here would send the operator
|
|
740
|
+
// through invalid-config recovery for a value the API told them was fine.
|
|
741
|
+
upstreamHttpVersion: z.enum(UPSTREAM_HTTP_VERSION_VALUES)
|
|
742
|
+
.nullish()
|
|
743
|
+
.transform(value => value ?? undefined),
|
|
737
744
|
noStructuredOutputModels: z.array(z.string().min(1))
|
|
738
745
|
.transform(normalizeNonBlankStringArray)
|
|
739
746
|
.optional(),
|
|
@@ -906,6 +913,20 @@ export function apiKeyTransportConfigError(
|
|
|
906
913
|
return null;
|
|
907
914
|
}
|
|
908
915
|
|
|
916
|
+
/**
|
|
917
|
+
* Shared runtime boundary for the per-provider upstream HTTP-version pin (#1668). Used by
|
|
918
|
+
* the management write path (providerManagementConfigError / PATCH) so it can never disagree
|
|
919
|
+
* with the strict zod load schema: a value that survives POST/PATCH is always loadable, and
|
|
920
|
+
* a value the loader rejects is rejected at write time too.
|
|
921
|
+
*/
|
|
922
|
+
export function upstreamHttpVersionConfigError(value: unknown): string | null {
|
|
923
|
+
if (value === undefined || value === null) return null;
|
|
924
|
+
if (typeof value !== "string" || !(UPSTREAM_HTTP_VERSION_VALUES as readonly string[]).includes(value)) {
|
|
925
|
+
return 'upstreamHttpVersion must be one of "auto", "http1.1", "h1", "http2", "h2", or null to clear';
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
|
|
909
930
|
export function positiveIntegerRecordConfigError(value: unknown, field: string): string | null {
|
|
910
931
|
if (value === undefined) return null;
|
|
911
932
|
if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`;
|
|
@@ -1997,12 +2018,30 @@ function normalizePersistedClaudeCode(claudeCode: unknown): OcxConfig["claudeCod
|
|
|
1997
2018
|
if (Object.hasOwn(normalized, "subagentEffort") && !isClaudeSubagentEffort(normalized.subagentEffort)) {
|
|
1998
2019
|
delete normalized.subagentEffort;
|
|
1999
2020
|
}
|
|
2021
|
+
// A hand-authored config never passes through the management validator, so coerce here too.
|
|
2022
|
+
// A malformed classifierFallbacks (a bare string, or an array with non-string entries) would
|
|
2023
|
+
// otherwise reach the resolver unchecked.
|
|
2024
|
+
if (Object.hasOwn(normalized, "classifierModel")) {
|
|
2025
|
+
const value = typeof normalized.classifierModel === "string" ? normalized.classifierModel.trim() : "";
|
|
2026
|
+
if (value.length > 0) normalized.classifierModel = value;
|
|
2027
|
+
else delete normalized.classifierModel;
|
|
2028
|
+
}
|
|
2029
|
+
if (Object.hasOwn(normalized, "classifierFallbacks")) {
|
|
2030
|
+
const raw = normalized.classifierFallbacks;
|
|
2031
|
+
const kept = Array.isArray(raw)
|
|
2032
|
+
? raw.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0).map(entry => entry.trim())
|
|
2033
|
+
: [];
|
|
2034
|
+
if (kept.length > 0) normalized.classifierFallbacks = kept;
|
|
2035
|
+
else delete normalized.classifierFallbacks;
|
|
2036
|
+
}
|
|
2000
2037
|
return normalized as OcxConfig["claudeCode"];
|
|
2001
2038
|
}
|
|
2002
2039
|
|
|
2003
|
-
function normalizeClaudeSubagentEffort(config: OcxConfig,
|
|
2004
|
-
|
|
2005
|
-
|
|
2040
|
+
function normalizeClaudeSubagentEffort(config: OcxConfig, _rawParsed: unknown): OcxConfig {
|
|
2041
|
+
// Unconditional. This used to short-circuit when `subagentEffort` was absent or already valid,
|
|
2042
|
+
// which meant a config whose ONLY defect was elsewhere in `claudeCode` was never normalized.
|
|
2043
|
+
// The specialized subagentEffort WARNING is a separate concern and stays exactly as it is.
|
|
2044
|
+
if (!config.claudeCode) return config;
|
|
2006
2045
|
return { ...config, claudeCode: normalizePersistedClaudeCode(config.claudeCode) };
|
|
2007
2046
|
}
|
|
2008
2047
|
|
|
@@ -2172,6 +2211,26 @@ export function loadConfig(): OcxConfig {
|
|
|
2172
2211
|
warnDegradedAgentTaskRecovery(parsed);
|
|
2173
2212
|
return withRefreshedCostOverlays(normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed));
|
|
2174
2213
|
}
|
|
2214
|
+
// Still failing, but if every complaint is about one or more named entries
|
|
2215
|
+
// in an independent section, drop exactly those and keep the rest. Falling
|
|
2216
|
+
// back to defaults here would silently retire the operator's providers,
|
|
2217
|
+
// keys and prices over a mistake in one routing profile.
|
|
2218
|
+
const salvaged = salvageConfigCandidate(merged, retryResult.error);
|
|
2219
|
+
if (salvaged) {
|
|
2220
|
+
{
|
|
2221
|
+
warnDroppedConfigSections(configPath, salvaged.dropped, salvaged.issues);
|
|
2222
|
+
const config = normalizeApiKeyIds(salvaged.parsed);
|
|
2223
|
+
warnDegradedHostname(parsed, config);
|
|
2224
|
+
warnDegradedApiKeys(parsed, config);
|
|
2225
|
+
warnDegradedCodexAccountPriorities(parsed, config);
|
|
2226
|
+
warnDegradedClaudeSubagentEffort(parsed);
|
|
2227
|
+
warnDegradedNativeSubagentConfig(parsed, config);
|
|
2228
|
+
warnDegradedCodexAccountPicker(parsed);
|
|
2229
|
+
warnDegradedUpstreamHostCircuitThreshold(parsed);
|
|
2230
|
+
warnDegradedAgentTaskRecovery(parsed);
|
|
2231
|
+
return withRefreshedCostOverlays(normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed));
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2175
2234
|
// Merge couldn't fix it — truly broken config
|
|
2176
2235
|
warnAndBackupInvalidConfig(configPath, result.error);
|
|
2177
2236
|
return getDefaultConfig();
|
|
@@ -2450,11 +2509,31 @@ function configDiagnosticsFromRaw(raw: string): ConfigDiagnostics {
|
|
|
2450
2509
|
return validFileConfigDiagnostics(normalizeApiKeyIds(result.data as OcxConfig), parsed);
|
|
2451
2510
|
}
|
|
2452
2511
|
|
|
2453
|
-
const
|
|
2512
|
+
const merged = mergeConfigDefaults(parsed);
|
|
2513
|
+
const retryResult = configSchema.safeParse(merged);
|
|
2454
2514
|
if (retryResult.success) {
|
|
2455
2515
|
return validFileConfigDiagnostics(normalizeApiKeyIds(retryResult.data as OcxConfig), parsed);
|
|
2456
2516
|
}
|
|
2457
2517
|
|
|
2518
|
+
// #1785: one invalid routing profile must not make diagnostics report the built-in
|
|
2519
|
+
// defaults AS the config, because a later config write persists those defaults over the
|
|
2520
|
+
// operator's providers, keys and prices.
|
|
2521
|
+
//
|
|
2522
|
+
// The failure is still reported. `source` stays "fallback" and `error` keeps the real
|
|
2523
|
+
// schema message -- diagnostics is the surface that tells callers the file is invalid,
|
|
2524
|
+
// and every consumer that must refuse an invalid config (provider reload, catalog sync,
|
|
2525
|
+
// cost reconcile, codex admission) gates on exactly those two fields. Only `config`
|
|
2526
|
+
// changes: it carries the salvaged document instead of factory defaults, so a caller
|
|
2527
|
+
// that ignores the error and writes it back preserves what the operator configured.
|
|
2528
|
+
const salvaged = salvageConfigCandidate(merged, retryResult.error);
|
|
2529
|
+
if (salvaged) {
|
|
2530
|
+
return {
|
|
2531
|
+
config: normalizeApiKeyIds(salvaged.parsed),
|
|
2532
|
+
source: "fallback",
|
|
2533
|
+
error: schemaDiagnosticsError(result.error),
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2458
2537
|
return { config: getDefaultConfig(), source: "fallback", error: schemaDiagnosticsError(result.error) };
|
|
2459
2538
|
} catch {
|
|
2460
2539
|
return { config: getDefaultConfig(), source: "fallback", error: "invalid_json" };
|
|
@@ -3428,6 +3507,194 @@ function warnConfigRepaired(configPath: string, error: z.ZodError): void {
|
|
|
3428
3507
|
console.error(`opencodex config at ${configPath}: repaired missing field(s) [${fields}] with defaults. Your providers and accounts are preserved.`);
|
|
3429
3508
|
}
|
|
3430
3509
|
|
|
3510
|
+
/**
|
|
3511
|
+
* Sections whose entries are independent of one another, so one bad entry is
|
|
3512
|
+
* safe to drop without changing what the rest mean.
|
|
3513
|
+
*
|
|
3514
|
+
* Both are validated entry-by-entry in the `superRefine` above, which raises
|
|
3515
|
+
* every finding as a *document*-level issue. That is what made a single routing
|
|
3516
|
+
* candidate naming a disabled provider discard the operator's whole config —
|
|
3517
|
+
* all eleven providers, every API key, and the entire `modelCosts` table —
|
|
3518
|
+
* while the proxy carried on serving from built-in defaults and reporting
|
|
3519
|
+
* healthy.
|
|
3520
|
+
*/
|
|
3521
|
+
const SALVAGEABLE_CONFIG_SECTIONS = ["routingProfiles", "combos"] as const;
|
|
3522
|
+
|
|
3523
|
+
/**
|
|
3524
|
+
* Drop just the named entries a parse failure blamed, so the rest of the
|
|
3525
|
+
* document survives.
|
|
3526
|
+
*
|
|
3527
|
+
* Returns `null` when the failure was not confined to those sections — the
|
|
3528
|
+
* caller then keeps its existing behaviour rather than guessing.
|
|
3529
|
+
*
|
|
3530
|
+
* The whole entry goes, not the individual offending candidate. A routing
|
|
3531
|
+
* profile that quietly loses one candidate still routes, just not where the
|
|
3532
|
+
* operator said it should, and a policy that silently changed shape is a worse
|
|
3533
|
+
* outcome than one that is plainly absent. Absent is also the loud option: a
|
|
3534
|
+
* dry-run against it answers `unknown_profile`, which — paired with the warning
|
|
3535
|
+
* this emits — points at the real mistake.
|
|
3536
|
+
*/
|
|
3537
|
+
function dropInvalidConfigSections(
|
|
3538
|
+
parsed: unknown,
|
|
3539
|
+
error: z.ZodError,
|
|
3540
|
+
): { candidate: Record<string, unknown>; dropped: string[] } | null {
|
|
3541
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
3542
|
+
|
|
3543
|
+
const doomed = new Map<string, Set<string>>();
|
|
3544
|
+
for (const issue of error.issues) {
|
|
3545
|
+
if (isUnsalvageableIssue(issue)) return null;
|
|
3546
|
+
const [section, id] = issue.path;
|
|
3547
|
+
if (typeof section !== "string" || typeof id !== "string") return null;
|
|
3548
|
+
if (!(SALVAGEABLE_CONFIG_SECTIONS as readonly string[]).includes(section)) return null;
|
|
3549
|
+
// A complaint about the container itself ("combos must be an object") is
|
|
3550
|
+
// not about one entry, so there is nothing selective to drop.
|
|
3551
|
+
if (issue.path.length < 2) return null;
|
|
3552
|
+
let ids = doomed.get(section);
|
|
3553
|
+
if (!ids) doomed.set(section, ids = new Set());
|
|
3554
|
+
ids.add(id);
|
|
3555
|
+
}
|
|
3556
|
+
if (doomed.size === 0) return null;
|
|
3557
|
+
|
|
3558
|
+
const candidate: Record<string, unknown> = { ...(parsed as Record<string, unknown>) };
|
|
3559
|
+
const dropped: string[] = [];
|
|
3560
|
+
for (const [section, ids] of doomed) {
|
|
3561
|
+
const current = candidate[section];
|
|
3562
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return null;
|
|
3563
|
+
const kept: Record<string, unknown> = {};
|
|
3564
|
+
for (const [key, value] of Object.entries(current as Record<string, unknown>)) {
|
|
3565
|
+
if (ids.has(key)) dropped.push(`${section}.${key}`);
|
|
3566
|
+
else kept[key] = value;
|
|
3567
|
+
}
|
|
3568
|
+
candidate[section] = kept;
|
|
3569
|
+
}
|
|
3570
|
+
return dropped.length > 0 ? { candidate, dropped } : null;
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
/**
|
|
3574
|
+
* Salvage until the document parses, not just once.
|
|
3575
|
+
*
|
|
3576
|
+
* One pass is not enough because the sections depend on each other: routing
|
|
3577
|
+
* profiles are validated against the combo map, so dropping an invalid combo can
|
|
3578
|
+
* expose a profile that referenced it. A single-pass salvage sees that second
|
|
3579
|
+
* failure and gives up, discarding the whole config -- the exact outcome this
|
|
3580
|
+
* code exists to prevent.
|
|
3581
|
+
*
|
|
3582
|
+
* `rawDocument` is the operator's document before defaults were merged in. When
|
|
3583
|
+
* supplied, the same entries are deleted from it too, so a diagnostics caller can
|
|
3584
|
+
* still tell an absent optional setting from one we injected.
|
|
3585
|
+
*/
|
|
3586
|
+
|
|
3587
|
+
/**
|
|
3588
|
+
* Findings that must never be salvaged away.
|
|
3589
|
+
*
|
|
3590
|
+
* Salvage removes the entry a finding blamed, which is right for an ordinary
|
|
3591
|
+
* validation mistake and wrong for a namespace collision: the collision is a
|
|
3592
|
+
* *relationship* between a combo/profile and a Codex account selector, and it is
|
|
3593
|
+
* reported on the combo. Dropping that combo makes the document parse and quietly
|
|
3594
|
+
* admits the account selector the schema just refused, turning a hard admission
|
|
3595
|
+
* boundary into a config that loads. Refuse the whole document instead.
|
|
3596
|
+
*/
|
|
3597
|
+
const UNSALVAGEABLE_ISSUE_MESSAGES: readonly string[] = [
|
|
3598
|
+
CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR,
|
|
3599
|
+
];
|
|
3600
|
+
|
|
3601
|
+
function isUnsalvageableIssue(issue: z.ZodIssue): boolean {
|
|
3602
|
+
return UNSALVAGEABLE_ISSUE_MESSAGES.some(message => issue.message.includes(message));
|
|
3603
|
+
}
|
|
3604
|
+
function salvageConfigCandidate(
|
|
3605
|
+
merged: unknown,
|
|
3606
|
+
initialError: z.ZodError,
|
|
3607
|
+
rawDocument?: unknown,
|
|
3608
|
+
): {
|
|
3609
|
+
candidate: Record<string, unknown>;
|
|
3610
|
+
rawCandidate: unknown;
|
|
3611
|
+
parsed: OcxConfig;
|
|
3612
|
+
dropped: string[];
|
|
3613
|
+
issues: z.ZodIssue[];
|
|
3614
|
+
} | null {
|
|
3615
|
+
let candidate: unknown = merged;
|
|
3616
|
+
let rawCandidate: unknown = rawDocument;
|
|
3617
|
+
let error = initialError;
|
|
3618
|
+
const dropped: string[] = [];
|
|
3619
|
+
const issues: z.ZodIssue[] = [];
|
|
3620
|
+
// Bounded by construction: every pass must remove at least one entry, and there
|
|
3621
|
+
// are only so many entries to remove.
|
|
3622
|
+
const budget = countSalvageableEntries(merged) + 1;
|
|
3623
|
+
for (let pass = 0; pass < budget; pass++) {
|
|
3624
|
+
const step = dropInvalidConfigSections(candidate, error);
|
|
3625
|
+
if (!step || step.dropped.length === 0) return null;
|
|
3626
|
+
dropped.push(...step.dropped);
|
|
3627
|
+
issues.push(...error.issues);
|
|
3628
|
+
candidate = step.candidate;
|
|
3629
|
+
rawCandidate = deleteEntryPaths(rawCandidate, step.dropped);
|
|
3630
|
+
const result = configSchema.safeParse(candidate);
|
|
3631
|
+
if (result.success) {
|
|
3632
|
+
return { candidate: step.candidate, rawCandidate, parsed: result.data as OcxConfig, dropped, issues };
|
|
3633
|
+
}
|
|
3634
|
+
error = result.error;
|
|
3635
|
+
}
|
|
3636
|
+
return null;
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
function countSalvageableEntries(document: unknown): number {
|
|
3640
|
+
if (!document || typeof document !== "object" || Array.isArray(document)) return 0;
|
|
3641
|
+
let total = 0;
|
|
3642
|
+
for (const section of SALVAGEABLE_CONFIG_SECTIONS) {
|
|
3643
|
+
const value = (document as Record<string, unknown>)[section];
|
|
3644
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
3645
|
+
total += Object.keys(value as Record<string, unknown>).length;
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
return total;
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
/** Delete `section.id` entries from a copy of the raw document. */
|
|
3652
|
+
function deleteEntryPaths(document: unknown, entryPaths: readonly string[]): unknown {
|
|
3653
|
+
if (!document || typeof document !== "object" || Array.isArray(document)) return document;
|
|
3654
|
+
const next: Record<string, unknown> = { ...(document as Record<string, unknown>) };
|
|
3655
|
+
for (const entryPath of entryPaths) {
|
|
3656
|
+
const separator = entryPath.indexOf(".");
|
|
3657
|
+
if (separator <= 0) continue;
|
|
3658
|
+
const section = entryPath.slice(0, separator);
|
|
3659
|
+
const id = entryPath.slice(separator + 1);
|
|
3660
|
+
const container = next[section];
|
|
3661
|
+
if (!container || typeof container !== "object" || Array.isArray(container)) continue;
|
|
3662
|
+
const kept: Record<string, unknown> = { ...(container as Record<string, unknown>) };
|
|
3663
|
+
delete kept[id];
|
|
3664
|
+
next[section] = kept;
|
|
3665
|
+
}
|
|
3666
|
+
return next;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
/**
|
|
3670
|
+
* Entry ids are operator-chosen and can be token-shaped, so nothing dynamic reaches
|
|
3671
|
+
* the log unredacted. Static section names stay readable -- they are the part that
|
|
3672
|
+
* tells the operator where to look.
|
|
3673
|
+
*/
|
|
3674
|
+
function redactEntryPath(entryPath: string): string {
|
|
3675
|
+
const separator = entryPath.indexOf(".");
|
|
3676
|
+
if (separator <= 0) return redactSecretString(entryPath);
|
|
3677
|
+
return entryPath.slice(0, separator) + "." + redactSecretString(entryPath.slice(separator + 1));
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
function redactIssuePath(path: readonly PropertyKey[]): string {
|
|
3681
|
+
return path
|
|
3682
|
+
.map((segment, index) => (index === 0 && typeof segment === "string" ? segment : redactSecretString(String(segment))))
|
|
3683
|
+
.join(".");
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
function warnDroppedConfigSections(configPath: string, dropped: string[], issues: readonly z.ZodIssue[]): void {
|
|
3687
|
+
if (warnedConfigFallbacks.has(configPath)) return;
|
|
3688
|
+
warnedConfigFallbacks.add(configPath);
|
|
3689
|
+
const reasons = issues
|
|
3690
|
+
.map(issue => `${redactIssuePath(issue.path)}: ${redactSecretString(issue.message)}`)
|
|
3691
|
+
.join("; ");
|
|
3692
|
+
console.error(
|
|
3693
|
+
`opencodex config at ${configPath}: dropped [${dropped.map(redactEntryPath).join(", ")}] and loaded the rest — ${reasons}. `
|
|
3694
|
+
+ "Everything else in your config, including providers and modelCosts, is preserved.",
|
|
3695
|
+
);
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3431
3698
|
export function readPidFileValue(): number | null {
|
|
3432
3699
|
try {
|
|
3433
3700
|
return parsePidFile(readFileSync(getPidPath(), "utf-8"));
|