@yansigit/opencodex 2.31.2 → 2.32.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/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-BG43zwVe.js +102 -0
- package/gui/dist/assets/index-CiSI-jrP.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +6 -8
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code-project-context.ts +377 -0
- package/src/adapters/command-code.ts +7 -4
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/live-transport.ts +21 -0
- package/src/adapters/cursor/native-exec-bridge.ts +141 -0
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/google-http.ts +12 -2
- package/src/adapters/google-wire-compiler.ts +83 -2
- package/src/adapters/google.ts +62 -21
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-responses.ts +3 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +21 -15
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +24 -11
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +11 -4
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +2 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/catalog/aggregation.ts +13 -1
- package/src/codex/catalog/effort.ts +24 -9
- package/src/codex/catalog/model-metadata.ts +566 -0
- package/src/codex/catalog/parsing.ts +33 -0
- package/src/codex/catalog/provider-fetch.ts +123 -32
- package/src/codex/catalog/sync.ts +5 -4
- package/src/codex/desktop-app-restart.ts +342 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config.ts +77 -812
- package/src/generated/compatibility-version.json +162 -94
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/process-control.ts +2 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +3 -0
- package/src/providers/registry.ts +1 -1
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/custom-tool-compat.ts +132 -38
- package/src/responses/parser.ts +3 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/auth-cors.ts +9 -7
- package/src/server/index.ts +102 -21
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +1 -1
- package/src/server/management/config-routes.ts +4 -1
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -4
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +242 -101
- package/src/server/responses/fetch-helpers.ts +2 -97
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +156 -15
- package/src/service.ts +8 -4
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +12 -1
- package/src/types/tools.ts +87 -11
- package/src/types.ts +1 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/gemini-executor.ts +35 -13
- package/src/web-search/index.ts +85 -1
- package/gui/dist/assets/index-BAMgarF9.js +0 -102
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Everything mutable about history happens here, behind H: the SQLite rows, the
|
|
5
5
|
* backup manifest, and every rollout patch. Those three do not share a
|
|
6
6
|
* transaction — sync writes the manifest before its database transaction, and
|
|
7
|
-
* restore
|
|
8
|
-
*
|
|
7
|
+
* restore preflights every rollout, applies database CAS before rollout changes inside one
|
|
8
|
+
* SQLite transaction, then consumes the manifest after exact readback — so a busy timeout only
|
|
9
9
|
* ever serialized a third of a state transition. Holding H across the whole unit
|
|
10
10
|
* is what stops an opposite-direction process overtaking through the other two.
|
|
11
11
|
*
|
|
@@ -74,7 +74,8 @@ export type HistoryWorkerResult =
|
|
|
74
74
|
| { readonly type: "blocked"; readonly requestId: string; readonly jobId: string;
|
|
75
75
|
readonly reason: "busy" | "database" | "unsafe-path" | "desired_disabled" | "desired_enabled" }
|
|
76
76
|
| { readonly type: "error"; readonly requestId: string; readonly jobId: string;
|
|
77
|
-
readonly message: string; readonly reason?: CodexHistoryFailureReason
|
|
77
|
+
readonly message: string; readonly reason?: CodexHistoryFailureReason;
|
|
78
|
+
readonly rows?: number; readonly files?: number };
|
|
78
79
|
|
|
79
80
|
const OPERATIONS: ReadonlySet<string> = new Set<CodexHistoryWorkerOperation>([
|
|
80
81
|
"skip",
|
|
@@ -142,8 +143,9 @@ export function runHistoryUnitUnderLock(
|
|
|
142
143
|
const proof = snapshotCodexHistoryNoop(message.canonicalStateDbPath, message.canonicalBackupPath);
|
|
143
144
|
if (proof.kind === "verified-noop") return { verifiedNoop: proof } as const;
|
|
144
145
|
}
|
|
145
|
-
// apply-opencodex routes history to opencodex; migrate/restore
|
|
146
|
-
//
|
|
146
|
+
// apply-opencodex routes history to opencodex; migrate/restore recover only
|
|
147
|
+
// manifest-backed original metadata. The provider is derived from the operation,
|
|
148
|
+
// never from a caller; only recover-legacy-openai force-labels bare routed rows.
|
|
147
149
|
const provider = operation === "apply-opencodex" ? "opencodex" : "openai";
|
|
148
150
|
return writeHistoryProviderTransition(permit, target, provider);
|
|
149
151
|
},
|
|
@@ -179,6 +181,7 @@ export function runHistoryUnitUnderLock(
|
|
|
179
181
|
jobId,
|
|
180
182
|
message: "history_transition_failed",
|
|
181
183
|
...(result.failureReason ? { reason: result.failureReason } : {}),
|
|
184
|
+
...(result.rows > 0 || result.files > 0 ? { rows: result.rows, files: result.files } : {}),
|
|
182
185
|
};
|
|
183
186
|
}
|
|
184
187
|
return {
|
package/src/codex/inject.ts
CHANGED
|
@@ -1105,8 +1105,8 @@ export async function injectCodexConfig(
|
|
|
1105
1105
|
: legacyMode
|
|
1106
1106
|
? ` Codex resume history: ${history.rows} thread(s) made visible for opencodex; originals backed up for restore.\n`
|
|
1107
1107
|
: migratedRows > 0
|
|
1108
|
-
? ` Codex resume history: ${migratedRows}
|
|
1109
|
-
: ` Codex resume history:
|
|
1108
|
+
? ` Codex resume history: restored original provider metadata for ${migratedRows} manifest-backed thread(s) (one-time).\n`
|
|
1109
|
+
: ` Codex resume history: no backed-up metadata pending; untracked routed history left unchanged.\n`;
|
|
1110
1110
|
// A user-owned root openai_base_url means we did NOT install routing — say so honestly
|
|
1111
1111
|
// instead of claiming the proxy route is active (catalog/fast_mode were still written).
|
|
1112
1112
|
if (keptUserBaseUrl) {
|
|
@@ -1345,18 +1345,33 @@ export interface CodexNativeRestoreResult {
|
|
|
1345
1345
|
};
|
|
1346
1346
|
}
|
|
1347
1347
|
|
|
1348
|
-
function failedHistoryRestore(
|
|
1348
|
+
function failedHistoryRestore(
|
|
1349
|
+
reason?: CodexHistoryFailureReason,
|
|
1350
|
+
detail?: string,
|
|
1351
|
+
progress: { rows?: number; files?: number } = {},
|
|
1352
|
+
): CodexRestoreHistoryResult {
|
|
1353
|
+
const rows = progress.rows ?? 0;
|
|
1354
|
+
const files = progress.files ?? 0;
|
|
1355
|
+
const changed = rows > 0 || files > 0;
|
|
1349
1356
|
return {
|
|
1350
1357
|
state: "failed",
|
|
1351
|
-
changed
|
|
1358
|
+
changed,
|
|
1352
1359
|
...(reason ? { reason } : {}),
|
|
1353
|
-
rows
|
|
1354
|
-
files
|
|
1360
|
+
rows,
|
|
1361
|
+
files,
|
|
1355
1362
|
ejectedRows: 0,
|
|
1356
1363
|
message: reason === "permission"
|
|
1357
|
-
?
|
|
1364
|
+
? changed
|
|
1365
|
+
? "Codex resume history changed but did NOT converge because permission was denied while finalizing the backup manifest; the manifest was retained for review and safe retry."
|
|
1366
|
+
: "Codex resume history could NOT be restored because permission was denied."
|
|
1358
1367
|
: reason === "busy"
|
|
1359
|
-
?
|
|
1368
|
+
? changed
|
|
1369
|
+
? "Codex resume history changed but did NOT converge because backup-manifest finalization remained busy; the manifest was retained for review and safe retry."
|
|
1370
|
+
: detail ?? "Codex resume history could NOT be restored — the Codex app appears to be holding the history database."
|
|
1371
|
+
: reason === "integrity"
|
|
1372
|
+
? changed
|
|
1373
|
+
? "Codex resume history changed but did NOT converge because the backup or target changed; the manifest was retained for review and safe retry."
|
|
1374
|
+
: "Codex resume history could NOT be restored because the backup or restore target failed integrity checks; unverified provider metadata was left unchanged."
|
|
1360
1375
|
: detail
|
|
1361
1376
|
? `Codex resume history could NOT be restored: ${detail}`
|
|
1362
1377
|
: "Codex resume history could NOT be restored; the reason was not recorded. Run 'ocx doctor'.",
|
|
@@ -1378,9 +1393,18 @@ export function failedHistoryRestoreFromOutcome(
|
|
|
1378
1393
|
outcome: Extract<CodexHistoryJobOutcome, { kind: "blocked" | "failed" }>,
|
|
1379
1394
|
): CodexRestoreHistoryResult {
|
|
1380
1395
|
if (outcome.kind === "blocked" && outcome.reason === "busy") return failedHistoryRestore("busy");
|
|
1381
|
-
if (outcome.kind === "failed" && outcome.historyFailureReason === "busy")
|
|
1396
|
+
if (outcome.kind === "failed" && outcome.historyFailureReason === "busy") {
|
|
1397
|
+
return failedHistoryRestore(
|
|
1398
|
+
"busy",
|
|
1399
|
+
describeHistoryJobFailure(outcome, "restore"),
|
|
1400
|
+
{ rows: outcome.rows, files: outcome.files },
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1382
1403
|
if (outcome.kind === "failed" && outcome.historyFailureReason === "permission") {
|
|
1383
|
-
return failedHistoryRestore("permission");
|
|
1404
|
+
return failedHistoryRestore("permission", undefined, { rows: outcome.rows, files: outcome.files });
|
|
1405
|
+
}
|
|
1406
|
+
if (outcome.kind === "failed" && outcome.historyFailureReason === "integrity") {
|
|
1407
|
+
return failedHistoryRestore("integrity", undefined, { rows: outcome.rows, files: outcome.files });
|
|
1384
1408
|
}
|
|
1385
1409
|
return failedHistoryRestore(undefined, describeHistoryJobFailure(outcome, "restore"));
|
|
1386
1410
|
}
|
|
@@ -1620,10 +1644,10 @@ export async function restoreNativeCodexAsync(
|
|
|
1620
1644
|
}
|
|
1621
1645
|
const history: CodexRestoreHistoryResult = outcome.kind === "converged"
|
|
1622
1646
|
? {
|
|
1623
|
-
state: "ok", changed: outcome.rows > 0, rows: outcome.rows, files: outcome.files, ejectedRows: 0,
|
|
1647
|
+
state: "ok", changed: outcome.rows > 0 || outcome.files > 0, rows: outcome.rows, files: outcome.files, ejectedRows: 0,
|
|
1624
1648
|
message: outcome.rows > 0
|
|
1625
|
-
? `Resume history restored from opencodex backup (${outcome.rows} thread(s)).`
|
|
1626
|
-
: "
|
|
1649
|
+
? `Resume history metadata restored from opencodex backup (${outcome.rows} thread(s)); original providers preserved.`
|
|
1650
|
+
: "No backed-up resume-history metadata was pending; untracked routed history was left unchanged.",
|
|
1627
1651
|
}
|
|
1628
1652
|
: outcome.kind === "skipped"
|
|
1629
1653
|
? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "Codex resume history was skipped." }
|
|
@@ -1685,16 +1709,16 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
|
|
|
1685
1709
|
const history: CodexRestoreHistoryResult = options.skipHistory
|
|
1686
1710
|
? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "History restoration runs asynchronously." }
|
|
1687
1711
|
: rawHistory.failed
|
|
1688
|
-
? failedHistoryRestore(rawHistory.failureReason)
|
|
1712
|
+
? failedHistoryRestore(rawHistory.failureReason, undefined, rawHistory)
|
|
1689
1713
|
: {
|
|
1690
1714
|
state: "ok",
|
|
1691
|
-
changed: rawHistory.rows > 0 || (rawHistory.ejectedRows ?? 0) > 0,
|
|
1715
|
+
changed: rawHistory.rows > 0 || rawHistory.files > 0 || (rawHistory.ejectedRows ?? 0) > 0,
|
|
1692
1716
|
rows: rawHistory.rows,
|
|
1693
1717
|
files: rawHistory.files,
|
|
1694
1718
|
ejectedRows: rawHistory.ejectedRows ?? 0,
|
|
1695
1719
|
message: rawHistory.rows > 0
|
|
1696
|
-
? `Resume history restored from opencodex backup (${rawHistory.rows} thread(s)).`
|
|
1697
|
-
: "
|
|
1720
|
+
? `Resume history metadata restored from opencodex backup (${rawHistory.rows} thread(s)); original providers preserved.`
|
|
1721
|
+
: "No backed-up resume-history metadata was pending; untracked routed history was left unchanged.",
|
|
1698
1722
|
};
|
|
1699
1723
|
const message = catalog.removed > 0
|
|
1700
1724
|
? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
|
|
@@ -1724,10 +1748,14 @@ export function formatApplyHistoryFailure(outcome: CodexHistoryJobOutcome, legac
|
|
|
1724
1748
|
const busy =
|
|
1725
1749
|
(outcome.kind === "blocked" && outcome.reason === "busy") ||
|
|
1726
1750
|
(outcome.kind === "failed" && outcome.historyFailureReason === "busy");
|
|
1727
|
-
const
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1751
|
+
const partiallyChanged = outcome.kind === "failed"
|
|
1752
|
+
&& ((outcome.rows ?? 0) > 0 || (outcome.files ?? 0) > 0);
|
|
1753
|
+
const headline = partiallyChanged
|
|
1754
|
+
? "Codex resume history changed but did not converge"
|
|
1755
|
+
: legacyMode
|
|
1756
|
+
? "Codex resume history sync SKIPPED"
|
|
1757
|
+
: busy
|
|
1758
|
+
? "Codex resume history metadata restore deferred"
|
|
1731
1759
|
: "Codex resume history NOT changed";
|
|
1732
1760
|
return ` ⚠️ ${headline}: ${describeHistoryJobFailure(outcome, "apply", legacyMode)}\n`;
|
|
1733
1761
|
}
|
|
@@ -22,7 +22,7 @@ export function tomlStringPattern(key: string): RegExp {
|
|
|
22
22
|
// A basic string escapes backslashes, so a Windows path is stored doubled; reading
|
|
23
23
|
// the raw bytes back returned a path that matched nothing on disk and made the
|
|
24
24
|
// journal's recorded catalog path un-restorable (#1798).
|
|
25
|
-
return new RegExp(`^\\s*${keyToken}\\s*=\\s*("(?:\\\\.|[^"])*"|'[^']*')\\s*(?:#.*)?$`);
|
|
25
|
+
return new RegExp(`^\\s*${keyToken}\\s*=\\s*("(?:\\\\.|[^"\\\\])*"|'[^']*')\\s*(?:#.*)?$`);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function rootTomlString(content: string, key: string): string | null {
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* This module exists because the mutations are spread across three surfaces that
|
|
6
6
|
* do not share a transaction: SQLite rows, the backup manifest, and the rollout
|
|
7
7
|
* files. `syncCodexHistoryProvider` writes the manifest BEFORE its database
|
|
8
|
-
* transaction and patches rollouts inside it; restore
|
|
9
|
-
* database
|
|
10
|
-
* (`src/codex/history-provider.ts
|
|
8
|
+
* transaction and patches rollouts inside it; restore preflights every rollout,
|
|
9
|
+
* applies database CAS before file changes in one SQLite transaction, then consumes the
|
|
10
|
+
* manifest after exact readback (`src/codex/history-provider.ts`). A
|
|
11
|
+
* SQLite busy timeout
|
|
11
12
|
* serializes exactly one of those three, which is why an opposite-direction
|
|
12
13
|
* process could overtake through the other two.
|
|
13
14
|
*
|
|
@@ -3,6 +3,7 @@ import { clearAccountNeedsReauth } from "./account-runtime-state";
|
|
|
3
3
|
import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
4
4
|
import {
|
|
5
5
|
probeNativeProfileRecoveryState,
|
|
6
|
+
resolveNativeProfileContext,
|
|
6
7
|
type NativeProfileRecoveryState,
|
|
7
8
|
} from "./native-profile-store";
|
|
8
9
|
import {
|
|
@@ -43,6 +44,8 @@ export interface NativeMainStartupGateDeps {
|
|
|
43
44
|
probeRecoveryState?: typeof probeNativeProfileRecoveryState;
|
|
44
45
|
owner?: NativeMainOwnerOptions;
|
|
45
46
|
stageSweepIntervalMs?: number;
|
|
47
|
+
/** Test seam / activation-time revalidation for the ambient physical auth home. */
|
|
48
|
+
currentHomeId?: () => string | null;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
export interface NativeMainStartupLifecycle {
|
|
@@ -51,6 +54,11 @@ export interface NativeMainStartupLifecycle {
|
|
|
51
54
|
release(): Promise<void>;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
export interface PreparedNativeMainStartupLifecycle {
|
|
58
|
+
readonly homeId: string;
|
|
59
|
+
start(): NativeMainStartupLifecycle;
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
let epoch = 0;
|
|
55
63
|
let snapshot: NativeMainStartupGateSnapshot = { status: "ready", homeId: null };
|
|
56
64
|
let settled: Promise<NativeMainStartupGateSnapshot> = Promise.resolve(snapshot);
|
|
@@ -75,6 +83,7 @@ interface StartupEntry {
|
|
|
75
83
|
}
|
|
76
84
|
const startupEntries = new Map<string, StartupEntry>();
|
|
77
85
|
const serverLifecycles = new WeakMap<object, NativeMainStartupLifecycle>();
|
|
86
|
+
const serverLifecycleReleases = new WeakMap<object, Promise<void>>();
|
|
78
87
|
|
|
79
88
|
function ready(homeId: string | null): NativeMainStartupGateSnapshot {
|
|
80
89
|
return { status: "ready", homeId };
|
|
@@ -311,14 +320,48 @@ export function startNativeMainStartupLifecycle(
|
|
|
311
320
|
};
|
|
312
321
|
}
|
|
313
322
|
|
|
323
|
+
/** Resolve and pin the owned lifecycle target without acquiring ownership or creating artifacts. */
|
|
324
|
+
export function prepareNativeMainStartupLifecycle(
|
|
325
|
+
deps: NativeMainStartupGateDeps = {},
|
|
326
|
+
homes?: { codexHome: string; configDir: string },
|
|
327
|
+
): PreparedNativeMainStartupLifecycle | null {
|
|
328
|
+
let manager: NativeProfileManager;
|
|
329
|
+
try {
|
|
330
|
+
manager = deps.manager ?? new NativeProfileManager(homes);
|
|
331
|
+
if (homes) {
|
|
332
|
+
const expected = resolveNativeProfileContext(homes);
|
|
333
|
+
if (
|
|
334
|
+
manager.context.homeId !== expected.homeId
|
|
335
|
+
|| manager.context.instanceId !== expected.instanceId
|
|
336
|
+
) return null;
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
const currentHomeId = deps.currentHomeId ?? (() => {
|
|
342
|
+
try { return resolveNativeProfileContext().homeId; } catch { return null; }
|
|
343
|
+
});
|
|
344
|
+
const pinnedDeps = { ...deps, manager };
|
|
345
|
+
return {
|
|
346
|
+
homeId: manager.context.homeId,
|
|
347
|
+
start: () => {
|
|
348
|
+
if (currentHomeId() !== manager.context.homeId) {
|
|
349
|
+
throw new Error("The native-main startup home changed after ownership inspection.");
|
|
350
|
+
}
|
|
351
|
+
return startNativeMainStartupLifecycle(pinnedDeps);
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
314
356
|
/**
|
|
315
357
|
* How many times a service-ownership fence will re-ask before it stops asking (#2108).
|
|
316
358
|
*
|
|
317
359
|
* A host that is permanently unaskable must not re-probe on every request forever, and a
|
|
318
360
|
* host that recovers usually does so within the first few. The budget belongs to the
|
|
319
|
-
*
|
|
320
|
-
* out a fresh allowance, or a caller looping over fences could spin the probe
|
|
321
|
-
*
|
|
361
|
+
* live hook owner, not to an individual fence: raising a second fence deliberately does
|
|
362
|
+
* not hand out a fresh allowance, or a caller looping over fences could spin the probe
|
|
363
|
+
* forever. Spending or releasing that hook owner ends its budget generation; a later
|
|
364
|
+
* fence can install a new owner even if an older hookless fence is still draining.
|
|
322
365
|
*/
|
|
323
366
|
export const NATIVE_MAIN_OWNERSHIP_RETRY_LIMIT = 5;
|
|
324
367
|
|
|
@@ -327,6 +370,11 @@ const serviceOwnershipReprobes = new Map<NativeMainServiceOwnershipBlockReason,
|
|
|
327
370
|
|
|
328
371
|
interface ServiceOwnershipReprobe {
|
|
329
372
|
readonly probe: () => NativeCodexOwnership;
|
|
373
|
+
readonly expectedHomeId: () => string | null;
|
|
374
|
+
readonly activate: () => NativeMainStartupLifecycle;
|
|
375
|
+
readonly adopt: (lifecycle: NativeMainStartupLifecycle) => boolean;
|
|
376
|
+
readonly discard: (lifecycle: NativeMainStartupLifecycle) => void;
|
|
377
|
+
activating: boolean;
|
|
330
378
|
attempts: number;
|
|
331
379
|
/** The fence that installed this hook; only its own release may drop the entry. */
|
|
332
380
|
readonly owner: NativeMainStartupLifecycle;
|
|
@@ -334,6 +382,10 @@ interface ServiceOwnershipReprobe {
|
|
|
334
382
|
readonly spend: () => void;
|
|
335
383
|
}
|
|
336
384
|
|
|
385
|
+
function releaseUnadoptedLifecycle(lifecycle: NativeMainStartupLifecycle): Promise<void> {
|
|
386
|
+
try { return Promise.resolve(lifecycle.release()).catch(() => {}); } catch { return Promise.resolve(); }
|
|
387
|
+
}
|
|
388
|
+
|
|
337
389
|
/** Test-only: the retry budget is module state and would otherwise leak across tests. */
|
|
338
390
|
export function __resetNativeMainOwnershipRetries(): void {
|
|
339
391
|
for (const entry of serviceOwnershipReprobes.values()) entry.attempts = 0;
|
|
@@ -359,23 +411,53 @@ function reprobeServiceOwnership(reason: NativeMainServiceOwnershipBlockReason):
|
|
|
359
411
|
if (reason !== "ownership-unknown") return false;
|
|
360
412
|
const entry = serviceOwnershipReprobes.get(reason);
|
|
361
413
|
if (!entry) return false;
|
|
414
|
+
if (entry.activating) return false;
|
|
362
415
|
if (entry.attempts >= NATIVE_MAIN_OWNERSHIP_RETRY_LIMIT) return false;
|
|
363
416
|
entry.attempts += 1;
|
|
364
|
-
let
|
|
417
|
+
let activated: NativeMainStartupLifecycle | undefined;
|
|
418
|
+
let expectedHomeId: string | null = null;
|
|
419
|
+
entry.activating = true;
|
|
365
420
|
try {
|
|
366
|
-
answer = entry.probe();
|
|
421
|
+
const answer = entry.probe();
|
|
422
|
+
if (answer !== "owned") return false;
|
|
423
|
+
expectedHomeId = entry.expectedHomeId();
|
|
424
|
+
if (expectedHomeId === null) return false;
|
|
425
|
+
// Ownership becoming knowable is not itself startup completion. Install the
|
|
426
|
+
// normal owner/recovery lifecycle while this fence is still held, so native
|
|
427
|
+
// traffic cannot get ahead of owner registration, journal recovery, auth-temp
|
|
428
|
+
// scrubbing, or the initial stage sweep.
|
|
429
|
+
activated = entry.activate();
|
|
367
430
|
} catch {
|
|
368
|
-
//
|
|
431
|
+
// Neither a failed inspection nor a failed activation is evidence that
|
|
432
|
+
// native-main is safe to admit. Keep the fence and retry hook intact.
|
|
433
|
+
return false;
|
|
434
|
+
} finally {
|
|
435
|
+
entry.activating = false;
|
|
436
|
+
}
|
|
437
|
+
if (
|
|
438
|
+
!activated
|
|
439
|
+
|| activated.homeId === null
|
|
440
|
+
|| activated.homeId !== expectedHomeId
|
|
441
|
+
|| typeof activated.release !== "function"
|
|
442
|
+
) {
|
|
443
|
+
if (activated && typeof activated.release === "function") entry.discard(activated);
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
if (serviceOwnershipReprobes.get(reason) !== entry || !entry.adopt(activated)) {
|
|
447
|
+
// Shutdown or a re-entrant release can retire this fence while activation
|
|
448
|
+
// runs. A lifecycle that was never attached to the server must not retain
|
|
449
|
+
// another owner reference in the background.
|
|
450
|
+
entry.discard(activated);
|
|
369
451
|
return false;
|
|
370
452
|
}
|
|
371
|
-
if (answer !== "owned") return false;
|
|
372
453
|
// Release through the fence that installed this hook, and only that one.
|
|
373
454
|
//
|
|
374
455
|
// Several servers can hold a fence for the same reason while only one carries a hook, so
|
|
375
456
|
// clearing the shared refcount here would unblock fences this probe never spoke for.
|
|
376
457
|
// Decrementing here directly is just as wrong the other way: that fence's own release()
|
|
377
|
-
// would then pay a second time for one fence, leaving the count short.
|
|
378
|
-
// fence's idempotent
|
|
458
|
+
// would then pay a second time for one fence, leaving the count short. The
|
|
459
|
+
// fence's idempotent spend hook keeps exactly one payment per fence while the
|
|
460
|
+
// transitioned owned lifecycle remains attached until server shutdown.
|
|
379
461
|
entry.spend();
|
|
380
462
|
return true;
|
|
381
463
|
}
|
|
@@ -395,22 +477,44 @@ function serviceOwnershipSnapshot(
|
|
|
395
477
|
/** Close native-main admission without resolving or creating any CODEX_HOME artifacts. */
|
|
396
478
|
export function blockNativeMainStartupForUnownedServiceHome(
|
|
397
479
|
reason: NativeMainServiceOwnershipBlockReason,
|
|
398
|
-
options?: {
|
|
480
|
+
options?: {
|
|
481
|
+
reprobe: () => NativeCodexOwnership;
|
|
482
|
+
expectedHomeId: string | (() => string | null);
|
|
483
|
+
startOwnedLifecycle: () => NativeMainStartupLifecycle;
|
|
484
|
+
},
|
|
399
485
|
): NativeMainStartupLifecycle {
|
|
400
486
|
serviceOwnershipRefs.set(reason, (serviceOwnershipRefs.get(reason) ?? 0) + 1);
|
|
401
|
-
let
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
487
|
+
let fenceSpent = false;
|
|
488
|
+
let ownedLifecycle: NativeMainStartupLifecycle | undefined;
|
|
489
|
+
let releaseFlight: Promise<void> | undefined;
|
|
490
|
+
const orphanReleaseFlights = new Set<Promise<void>>();
|
|
491
|
+
let lifecycle!: NativeMainStartupLifecycle;
|
|
492
|
+
const blockedSettled = Promise.resolve(serviceOwnershipSnapshot(reason));
|
|
493
|
+
const spendFence = () => {
|
|
494
|
+
if (fenceSpent) return;
|
|
495
|
+
fenceSpent = true;
|
|
496
|
+
const remaining = Math.max(0, (serviceOwnershipRefs.get(reason) ?? 0) - 1);
|
|
497
|
+
if (remaining === 0) serviceOwnershipRefs.delete(reason);
|
|
498
|
+
else serviceOwnershipRefs.set(reason, remaining);
|
|
499
|
+
if (serviceOwnershipReprobes.get(reason)?.owner === lifecycle) {
|
|
500
|
+
serviceOwnershipReprobes.delete(reason);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
lifecycle = {
|
|
504
|
+
get homeId() { return ownedLifecycle?.homeId ?? null; },
|
|
505
|
+
get settled() { return ownedLifecycle?.settled ?? blockedSettled; },
|
|
506
|
+
release() {
|
|
507
|
+
return releaseFlight ??= (async () => {
|
|
508
|
+
spendFence();
|
|
509
|
+
// A synchronous activator can re-enter release before its returned
|
|
510
|
+
// lifecycle is adopted or discarded. Let that call stack finish so the
|
|
511
|
+
// cleanup set is complete before this shared release flight drains it.
|
|
512
|
+
await Promise.resolve();
|
|
513
|
+
await ownedLifecycle?.release();
|
|
514
|
+
if (orphanReleaseFlights.size > 0) {
|
|
515
|
+
await Promise.allSettled([...orphanReleaseFlights]);
|
|
516
|
+
}
|
|
517
|
+
})();
|
|
414
518
|
},
|
|
415
519
|
};
|
|
416
520
|
// Do NOT reset an existing budget: keying the reprobe by reason means a caller raising
|
|
@@ -418,12 +522,28 @@ export function blockNativeMainStartupForUnownedServiceHome(
|
|
|
418
522
|
// the probe forever. But once the holder is gone its entry is removed above, so a LATER
|
|
419
523
|
// fence installs its own hook — a server started after an earlier probe must not be left
|
|
420
524
|
// needing `ocx restart`, which is the very symptom this exists to remove.
|
|
421
|
-
if (options
|
|
525
|
+
if (options && reason === "ownership-unknown" && !serviceOwnershipReprobes.has(reason)) {
|
|
526
|
+
const expectedHomeId = options.expectedHomeId;
|
|
422
527
|
serviceOwnershipReprobes.set(reason, {
|
|
423
528
|
probe: options.reprobe,
|
|
529
|
+
expectedHomeId: typeof expectedHomeId === "function"
|
|
530
|
+
? expectedHomeId
|
|
531
|
+
: () => expectedHomeId,
|
|
532
|
+
activate: options.startOwnedLifecycle,
|
|
533
|
+
adopt: activated => {
|
|
534
|
+
if (releaseFlight !== undefined || fenceSpent || ownedLifecycle !== undefined) return false;
|
|
535
|
+
ownedLifecycle = activated;
|
|
536
|
+
return true;
|
|
537
|
+
},
|
|
538
|
+
discard: activated => {
|
|
539
|
+
const flight = releaseUnadoptedLifecycle(activated);
|
|
540
|
+
orphanReleaseFlights.add(flight);
|
|
541
|
+
void flight.finally(() => orphanReleaseFlights.delete(flight));
|
|
542
|
+
},
|
|
543
|
+
activating: false,
|
|
424
544
|
attempts: 0,
|
|
425
545
|
owner: lifecycle,
|
|
426
|
-
spend:
|
|
546
|
+
spend: spendFence,
|
|
427
547
|
});
|
|
428
548
|
}
|
|
429
549
|
return lifecycle;
|
|
@@ -434,10 +554,20 @@ export function bindNativeMainStartupLifecycle(server: object, lifecycle: Native
|
|
|
434
554
|
}
|
|
435
555
|
|
|
436
556
|
export async function releaseNativeMainStartupLifecycle(server: object): Promise<void> {
|
|
557
|
+
const existing = serverLifecycleReleases.get(server);
|
|
558
|
+
if (existing) return existing;
|
|
437
559
|
const lifecycle = serverLifecycles.get(server);
|
|
438
560
|
if (!lifecycle) return;
|
|
439
|
-
|
|
440
|
-
|
|
561
|
+
const flight = Promise.resolve().then(() => lifecycle.release());
|
|
562
|
+
serverLifecycleReleases.set(server, flight);
|
|
563
|
+
try {
|
|
564
|
+
await flight;
|
|
565
|
+
} finally {
|
|
566
|
+
if (serverLifecycleReleases.get(server) === flight) {
|
|
567
|
+
serverLifecycleReleases.delete(server);
|
|
568
|
+
serverLifecycles.delete(server);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
441
571
|
}
|
|
442
572
|
|
|
443
573
|
export function isNativeMainTrafficBlocked(): boolean {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import {
|
|
3
2
|
closeSync,
|
|
4
3
|
fstatSync,
|
|
@@ -17,6 +16,7 @@ import { Database } from "bun:sqlite";
|
|
|
17
16
|
|
|
18
17
|
import { getConfigDir } from "../config";
|
|
19
18
|
import { catalogHasRoutedEntries, parseCatalogJson } from "./catalog/parsing";
|
|
19
|
+
import { codexHistoryBackupId, validateCodexHistoryBackupManifest } from "./history-manifest";
|
|
20
20
|
import {
|
|
21
21
|
hasInjectedCodexRouting,
|
|
22
22
|
OCX_SECTION_MARKER,
|
|
@@ -559,18 +559,22 @@ function classifyHistoryDatabase(path: string): NativeRoutedResidueResult {
|
|
|
559
559
|
return indeterminate("history", resolved.path, "history row has no provider metadata");
|
|
560
560
|
}
|
|
561
561
|
}
|
|
562
|
+
// A bare opencodex row is not proof that OpenCodex owns a reversible transition: it may
|
|
563
|
+
// belong to any routed provider and has no native target without the backup manifest.
|
|
564
|
+
// Keep detecting interrupted metadata on native rows, but let the manifest classifier
|
|
565
|
+
// below be the authority for provenance-backed routed rows.
|
|
562
566
|
const rollouts = classifyReferencedRollouts(
|
|
563
567
|
"history",
|
|
564
|
-
rows
|
|
568
|
+
rows
|
|
569
|
+
.filter(row => row.model_provider !== "opencodex")
|
|
570
|
+
.map(row => ({ id: row.id, path: row.rollout_path })),
|
|
565
571
|
);
|
|
566
572
|
if (rollouts.kind !== "clean") return rollouts;
|
|
567
573
|
const after = statSync(resolved.path);
|
|
568
574
|
if (!sameStat(resolved.stat, after)) {
|
|
569
575
|
return indeterminate("history", resolved.path, "history database changed while it was being observed");
|
|
570
576
|
}
|
|
571
|
-
return
|
|
572
|
-
? { kind: "residue", surface: "history", path: resolved.path }
|
|
573
|
-
: { kind: "clean" };
|
|
577
|
+
return { kind: "clean" };
|
|
574
578
|
} catch (error) {
|
|
575
579
|
return indeterminate("history", resolved.path, `unreadable history database: ${errorReason(error)}`);
|
|
576
580
|
} finally {
|
|
@@ -579,11 +583,7 @@ function classifyHistoryDatabase(path: string): NativeRoutedResidueResult {
|
|
|
579
583
|
}
|
|
580
584
|
|
|
581
585
|
function historyBackupPath(stateDatabasePath: string): string {
|
|
582
|
-
|
|
583
|
-
? resolve(stateDatabasePath).toLowerCase()
|
|
584
|
-
: resolve(stateDatabasePath);
|
|
585
|
-
const id = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
586
|
-
return join(getConfigDir(), `codex-history-backup-${id}.json`);
|
|
586
|
+
return join(getConfigDir(), `codex-history-backup-${codexHistoryBackupId(stateDatabasePath)}.json`);
|
|
587
587
|
}
|
|
588
588
|
|
|
589
589
|
function classifyHistoryBackup(path: string, stateDatabasePath: string): NativeRoutedResidueResult {
|
|
@@ -596,32 +596,25 @@ function classifyHistoryBackup(path: string, stateDatabasePath: string): NativeR
|
|
|
596
596
|
} catch (error) {
|
|
597
597
|
return indeterminate("history-backup", read.path, `malformed history backup JSON: ${errorReason(error)}`);
|
|
598
598
|
}
|
|
599
|
-
|
|
600
|
-
|
|
599
|
+
const validated = validateCodexHistoryBackupManifest(parsed, stateDatabasePath);
|
|
600
|
+
if (!validated.ok && validated.reason === "foreign-database") {
|
|
601
|
+
return indeterminate("history-backup", read.path, "history backup names a different state database");
|
|
601
602
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
603
|
+
if (!validated.ok) {
|
|
604
|
+
return indeterminate(
|
|
605
|
+
"history-backup",
|
|
606
|
+
read.path,
|
|
607
|
+
validated.scope === "entry-shape"
|
|
608
|
+
? "history backup entry has an unknown shape"
|
|
609
|
+
: validated.scope === "entry-provenance"
|
|
610
|
+
? "history backup entry has invalid provenance metadata"
|
|
611
|
+
: "history backup has an unknown shape",
|
|
612
|
+
);
|
|
612
613
|
}
|
|
613
|
-
const entries = Object.
|
|
614
|
+
const entries = Object.entries(validated.manifest.entries);
|
|
614
615
|
const references: RolloutReference[] = [];
|
|
615
|
-
for (const entry of entries) {
|
|
616
|
-
|
|
617
|
-
return indeterminate("history-backup", read.path, "history backup entry has an unknown shape");
|
|
618
|
-
}
|
|
619
|
-
const candidate = entry as Record<string, unknown>;
|
|
620
|
-
if (typeof candidate.id !== "string" || !candidate.id
|
|
621
|
-
|| typeof candidate.rolloutPath !== "string" || !candidate.rolloutPath) {
|
|
622
|
-
return indeterminate("history-backup", read.path, "history backup entry has an unknown rollout reference");
|
|
623
|
-
}
|
|
624
|
-
references.push({ id: candidate.id, path: candidate.rolloutPath });
|
|
616
|
+
for (const entry of Object.values(validated.manifest.entries)) {
|
|
617
|
+
references.push({ id: entry.id, path: entry.rolloutPath });
|
|
625
618
|
}
|
|
626
619
|
const rollouts = classifyReferencedRollouts("history-backup", references);
|
|
627
620
|
if (rollouts.kind !== "clean") return rollouts;
|
package/src/combos/failover.ts
CHANGED
|
@@ -109,6 +109,28 @@ export function clearComboTargetCooldowns(comboId?: string): void {
|
|
|
109
109
|
|
|
110
110
|
export type ComboFailureDecision = "hop" | "stop";
|
|
111
111
|
|
|
112
|
+
function isModelLifecycleGone(
|
|
113
|
+
status: number,
|
|
114
|
+
message: string,
|
|
115
|
+
code?: string | null,
|
|
116
|
+
): boolean {
|
|
117
|
+
if (status !== 410) return false;
|
|
118
|
+
const normalizedCode = code?.trim().toLowerCase().replaceAll("-", "_");
|
|
119
|
+
if ([
|
|
120
|
+
"model_deprecated",
|
|
121
|
+
"model_end_of_life",
|
|
122
|
+
"model_eol",
|
|
123
|
+
"model_not_found",
|
|
124
|
+
"model_retired",
|
|
125
|
+
].includes(normalizedCode ?? "")) return true;
|
|
126
|
+
const text = message.toLowerCase();
|
|
127
|
+
return /\bmodel\b/.test(text) && (
|
|
128
|
+
/\bend[ -]of[ -]life\b/.test(text)
|
|
129
|
+
|| /\bno longer available\b/.test(text)
|
|
130
|
+
|| /\b(?:deprecated|retired|retirement|sunset|decommissioned)\b/.test(text)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
112
134
|
export function comboFailureDecision(
|
|
113
135
|
status: number,
|
|
114
136
|
message: string,
|
|
@@ -119,6 +141,11 @@ export function comboFailureDecision(
|
|
|
119
141
|
// Cyber policy is a hard non-retryable refusal — honor structured code even when
|
|
120
142
|
// classificationText was truncated before the JSON code field.
|
|
121
143
|
if (isCyberPolicyCode(options?.code)) return "stop";
|
|
144
|
+
// HTTP 410 is normally terminal. A model-specific lifecycle verdict is target-local,
|
|
145
|
+
// however: another provider/model in the declared combo can still serve the request.
|
|
146
|
+
// Require structured lifecycle code or explicit model+lifecycle prose so unrelated
|
|
147
|
+
// application-level 410 responses remain fail-closed.
|
|
148
|
+
if (isModelLifecycleGone(status, message, options?.code)) return "hop";
|
|
122
149
|
const error = classifyError(status, "upstream_error", message);
|
|
123
150
|
if (isCyberPolicyCode(error.code)) return "stop";
|
|
124
151
|
// A local input-admission refusal (#1524) says "this candidate cannot fit the request",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST } from "./openai-responses";
|
|
2
|
+
import type { CompatibilityManifestV1 } from "./manifest";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
COMPATIBILITY_DISPOSITIONS,
|
|
6
|
+
COMPATIBILITY_MANIFEST_SCHEMA_VERSION,
|
|
7
|
+
compatibilityManifestIssues,
|
|
8
|
+
defineCompatibilityManifest,
|
|
9
|
+
} from "./manifest";
|
|
10
|
+
export type {
|
|
11
|
+
CompatibilityClaimV1,
|
|
12
|
+
CompatibilityDisposition,
|
|
13
|
+
CompatibilityEvidenceKind,
|
|
14
|
+
CompatibilityEvidenceRefV1,
|
|
15
|
+
CompatibilityManifestV1,
|
|
16
|
+
CompatibilitySubjectV1,
|
|
17
|
+
} from "./manifest";
|
|
18
|
+
export { OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST } from "./openai-responses";
|
|
19
|
+
|
|
20
|
+
export const COMPATIBILITY_MANIFESTS: readonly CompatibilityManifestV1[] = Object.freeze([
|
|
21
|
+
OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST,
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export function getCompatibilityManifest(id: string): CompatibilityManifestV1 | undefined {
|
|
25
|
+
return COMPATIBILITY_MANIFESTS.find(manifest => manifest.id === id);
|
|
26
|
+
}
|