@bitkyc08/opencodex 2.55.0 → 2.57.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 +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import { loadConfig } from "../../config";
|
|
2
|
+
import { shouldSyncCodexOnStart } from "../desired-state";
|
|
3
|
+
import { withCatalogWriteSerialization } from "../catalog-write-serialization";
|
|
4
|
+
import { restoreCodexCatalogWithPermit } from "../catalog/sync";
|
|
5
|
+
import { withCodexWriteLock, CodexWriteLockSkipped } from "../codex-write-lock";
|
|
6
|
+
import { inspectNativeCodexOwnership } from "../../integrations/native/ownership-preflight";
|
|
7
|
+
import { resolveCodexHistoryTransition } from "../history-transition";
|
|
8
|
+
import {
|
|
9
|
+
captureCodexPreImages,
|
|
10
|
+
codexWriteCoordinationEligibility,
|
|
11
|
+
CodexPartialWriteError,
|
|
12
|
+
CodexWriteConflictError,
|
|
13
|
+
DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
14
|
+
recordCodexNativeTransactionProvenance,
|
|
15
|
+
restoreCodexPreImages,
|
|
16
|
+
} from "../inject-coordination";
|
|
17
|
+
import { readIntegrationRecord } from "../integration-record";
|
|
18
|
+
import { classifyNativeRoutedResidue } from "../native-residue";
|
|
19
|
+
import {
|
|
20
|
+
resolveCodexCoordinatorDatabasePath,
|
|
21
|
+
resolveEffectiveUserIdentity,
|
|
22
|
+
} from "../user-identity";
|
|
23
|
+
import {
|
|
24
|
+
journaledInjectedCatalogPath,
|
|
25
|
+
removeJournal,
|
|
26
|
+
restoreJournalState,
|
|
27
|
+
} from "../journal";
|
|
28
|
+
import {
|
|
29
|
+
preflightCodexHistoryInjection,
|
|
30
|
+
syncCodexHistoryProvider,
|
|
31
|
+
type CodexHistoryFailureReason,
|
|
32
|
+
} from "../history-provider";
|
|
33
|
+
import {
|
|
34
|
+
describeHistoryJobFailure,
|
|
35
|
+
deriveCodexHistoryOperation,
|
|
36
|
+
resolveCodexHistoryJobTarget,
|
|
37
|
+
runCodexHistoryJob,
|
|
38
|
+
type CodexHistoryJobOutcome,
|
|
39
|
+
} from "../history-job";
|
|
40
|
+
import {
|
|
41
|
+
DEFAULT_CATALOG_PATH,
|
|
42
|
+
getCodexHome,
|
|
43
|
+
tomlString,
|
|
44
|
+
} from "../paths";
|
|
45
|
+
import { shouldInjectApiAuthHeader } from "../loopback-target";
|
|
46
|
+
import { currentExternalCodexModelProvider } from "./config-toml";
|
|
47
|
+
import { removeCodexConfig } from "./remove";
|
|
48
|
+
|
|
49
|
+
class CodexRestoreRefusal extends Error {
|
|
50
|
+
constructor(readonly config: CodexRestoreConfigResult) {
|
|
51
|
+
super(config.message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let beforeRestoreConfigForTests: ((kind: string) => void) | undefined;
|
|
56
|
+
export function setBeforeRestoreConfigForTests(hook: typeof beforeRestoreConfigForTests): void {
|
|
57
|
+
beforeRestoreConfigForTests = hook;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type CodexRestoreArtifactState = "ok" | "skipped" | "failed";
|
|
61
|
+
|
|
62
|
+
export interface CodexRestoreConfigResult {
|
|
63
|
+
state: CodexRestoreArtifactState;
|
|
64
|
+
changed: boolean;
|
|
65
|
+
action: "journal-restored" | "owned-fields-stripped" | "external-provider-preserved" | "failed";
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CodexRestoreCatalogResult {
|
|
70
|
+
state: CodexRestoreArtifactState;
|
|
71
|
+
changed: boolean;
|
|
72
|
+
removed: number;
|
|
73
|
+
kept: number;
|
|
74
|
+
path: string | null;
|
|
75
|
+
message: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CodexRestoreHistoryResult {
|
|
79
|
+
state: CodexRestoreArtifactState;
|
|
80
|
+
changed: boolean;
|
|
81
|
+
reason?: CodexHistoryFailureReason;
|
|
82
|
+
rows: number;
|
|
83
|
+
files: number;
|
|
84
|
+
ejectedRows: number;
|
|
85
|
+
message: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface CodexNativeRestoreResult {
|
|
89
|
+
success: boolean;
|
|
90
|
+
message: string;
|
|
91
|
+
externalProvider?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Set when the restore refused at the Codex history preflight (#4718).
|
|
94
|
+
*
|
|
95
|
+
* The preflight runs before the config half, so a refusal leaves config, catalog,
|
|
96
|
+
* history and provenance exactly as they were. That is a different outcome from a
|
|
97
|
+
* restore that ran and failed, and callers that decide whether an obligation was
|
|
98
|
+
* discharged need to tell them apart. Reading the artifact states alone cannot: a
|
|
99
|
+
* refusal reports every artifact as `skipped`, which is also what an ownership refusal
|
|
100
|
+
* and a desired-state skip report. Matching the human-readable message instead would
|
|
101
|
+
* make a safety decision depend on prose.
|
|
102
|
+
*/
|
|
103
|
+
historyPreflightRefusal?: string;
|
|
104
|
+
artifacts: {
|
|
105
|
+
config: CodexRestoreConfigResult;
|
|
106
|
+
catalog: CodexRestoreCatalogResult;
|
|
107
|
+
history: CodexRestoreHistoryResult;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function failedHistoryRestore(
|
|
112
|
+
reason?: CodexHistoryFailureReason,
|
|
113
|
+
detail?: string,
|
|
114
|
+
progress: { rows?: number; files?: number } = {},
|
|
115
|
+
): CodexRestoreHistoryResult {
|
|
116
|
+
const rows = progress.rows ?? 0;
|
|
117
|
+
const files = progress.files ?? 0;
|
|
118
|
+
const changed = rows > 0 || files > 0;
|
|
119
|
+
return {
|
|
120
|
+
state: "failed",
|
|
121
|
+
changed,
|
|
122
|
+
...(reason ? { reason } : {}),
|
|
123
|
+
rows,
|
|
124
|
+
files,
|
|
125
|
+
ejectedRows: 0,
|
|
126
|
+
message: reason === "permission"
|
|
127
|
+
? changed
|
|
128
|
+
? "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."
|
|
129
|
+
: "Codex resume history could NOT be restored because permission was denied."
|
|
130
|
+
: reason === "busy"
|
|
131
|
+
? changed
|
|
132
|
+
? "Codex resume history changed but did NOT converge because backup-manifest finalization remained busy; the manifest was retained for review and safe retry."
|
|
133
|
+
: detail ?? "Codex resume history could NOT be restored — the Codex app appears to be holding the history database."
|
|
134
|
+
: reason === "integrity"
|
|
135
|
+
? changed
|
|
136
|
+
? "Codex resume history changed but did NOT converge because the backup or target changed; the manifest was retained for review and safe retry."
|
|
137
|
+
: "Codex resume history could NOT be restored because the backup or restore target failed integrity checks; unverified provider metadata was left unchanged."
|
|
138
|
+
: detail
|
|
139
|
+
? `Codex resume history could NOT be restored: ${detail}`
|
|
140
|
+
: "Codex resume history could NOT be restored; the reason was not recorded. Run 'ocx doctor'.",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Restore failure wording for a Worker outcome.
|
|
146
|
+
*
|
|
147
|
+
* Only a genuine busy result blames the Codex app. An unsafe-path refusal, an
|
|
148
|
+
* unavailable coordinator database, a permission denial, or a dead/timed-out
|
|
149
|
+
* worker is a different problem; the old collapse made every one of those read
|
|
150
|
+
* as "the Codex app is holding the database" (issue #1191). `busy` and
|
|
151
|
+
* `permission` keep the restore-specific sentence built by
|
|
152
|
+
* `failedHistoryRestore`; every other reason reuses the single formatter so
|
|
153
|
+
* the two modules cannot drift apart.
|
|
154
|
+
*/
|
|
155
|
+
export function failedHistoryRestoreFromOutcome(
|
|
156
|
+
outcome: Extract<CodexHistoryJobOutcome, { kind: "blocked" | "failed" }>,
|
|
157
|
+
): CodexRestoreHistoryResult {
|
|
158
|
+
if (outcome.kind === "blocked" && outcome.reason === "busy") return failedHistoryRestore("busy");
|
|
159
|
+
if (outcome.kind === "failed" && outcome.historyFailureReason === "busy") {
|
|
160
|
+
return failedHistoryRestore(
|
|
161
|
+
"busy",
|
|
162
|
+
describeHistoryJobFailure(outcome, "restore"),
|
|
163
|
+
{ rows: outcome.rows, files: outcome.files },
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (outcome.kind === "failed" && outcome.historyFailureReason === "permission") {
|
|
167
|
+
return failedHistoryRestore("permission", undefined, { rows: outcome.rows, files: outcome.files });
|
|
168
|
+
}
|
|
169
|
+
if (outcome.kind === "failed" && outcome.historyFailureReason === "integrity") {
|
|
170
|
+
return failedHistoryRestore("integrity", undefined, { rows: outcome.rows, files: outcome.files });
|
|
171
|
+
}
|
|
172
|
+
return failedHistoryRestore(undefined, describeHistoryJobFailure(outcome, "restore"));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function externalProviderRestoreResult(activeProvider: string): CodexNativeRestoreResult {
|
|
176
|
+
const message = `External Codex provider ${tomlString(activeProvider)} preserved; no native restore was needed.`;
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
message,
|
|
180
|
+
externalProvider: activeProvider,
|
|
181
|
+
artifacts: {
|
|
182
|
+
config: { state: "skipped", changed: false, action: "external-provider-preserved", message },
|
|
183
|
+
catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
|
|
184
|
+
history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A foreign service claim is an authority boundary, including explicit CLI restore. */
|
|
190
|
+
function foreignOwnershipRestoreRefusal(message: string): CodexNativeRestoreResult {
|
|
191
|
+
return {
|
|
192
|
+
success: false,
|
|
193
|
+
message: `Codex native restore refused: ${message}`,
|
|
194
|
+
artifacts: {
|
|
195
|
+
config: { state: "skipped", changed: false, action: "failed", message },
|
|
196
|
+
catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
|
|
197
|
+
history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function desiredEnabledRestoreSkip(): CodexNativeRestoreResult {
|
|
203
|
+
const message = "Codex integration was re-enabled; native restore was skipped.";
|
|
204
|
+
return skippedRestoreEnvelope(true, message);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A schema-complete all-skipped envelope for outcomes decided before any
|
|
209
|
+
* restore machinery runs. Every `restore --json` path must stay shape-stable
|
|
210
|
+
* with `CodexNativeRestoreResult`; consumers never special-case early exits.
|
|
211
|
+
*/
|
|
212
|
+
export function skippedRestoreEnvelope(success: boolean, message: string): CodexNativeRestoreResult {
|
|
213
|
+
return {
|
|
214
|
+
success,
|
|
215
|
+
message,
|
|
216
|
+
artifacts: {
|
|
217
|
+
config: { state: "skipped", changed: false, action: "owned-fields-stripped", message },
|
|
218
|
+
catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
|
|
219
|
+
history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Config was attempted and failed; downstream artifacts were never attempted. */
|
|
225
|
+
function failedConfigRestoreEnvelope(config: CodexRestoreConfigResult): CodexNativeRestoreResult {
|
|
226
|
+
const result = skippedRestoreEnvelope(false, config.message);
|
|
227
|
+
result.artifacts.config = config;
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The history preflight refused, so nothing was attempted at all (#4718).
|
|
233
|
+
*
|
|
234
|
+
* The message is unchanged from what this path has always printed; the structured reason
|
|
235
|
+
* is added beside it so a caller can act on the refusal without reading the prose.
|
|
236
|
+
*/
|
|
237
|
+
function historyPreflightRefusalEnvelope(historyError: string): CodexNativeRestoreResult {
|
|
238
|
+
const result = skippedRestoreEnvelope(
|
|
239
|
+
false,
|
|
240
|
+
`Native restore refused: ${historyError}. Config, catalog, history and provenance were preserved.`,
|
|
241
|
+
);
|
|
242
|
+
result.historyPreflightRefusal = historyError;
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The config/profile half of a native restore, reported as one artifact. */
|
|
247
|
+
function restoreCodexConfigInline(kind = "sync"): CodexRestoreConfigResult {
|
|
248
|
+
const preImages = captureCodexPreImages();
|
|
249
|
+
const result = restoreCodexConfigInlineImpl(kind);
|
|
250
|
+
if (result.state === "failed") {
|
|
251
|
+
const compensated = restoreCodexPreImages(preImages);
|
|
252
|
+
if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function restoreCodexConfigInlineImpl(kind: string): CodexRestoreConfigResult {
|
|
258
|
+
try {
|
|
259
|
+
beforeRestoreConfigForTests?.(kind);
|
|
260
|
+
const historyError = preflightCodexHistoryInjection(false, false);
|
|
261
|
+
if (historyError) return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${historyError}.` };
|
|
262
|
+
const journal = restoreJournalState();
|
|
263
|
+
if (journal.unverified) {
|
|
264
|
+
return {
|
|
265
|
+
state: "failed", changed: false, action: "failed",
|
|
266
|
+
message: "Codex journal recovery was not verified; current configuration files and the journal were preserved.",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const restored = journal.configRestored
|
|
270
|
+
? { success: true, message: "Codex config restored from opencodex journal." }
|
|
271
|
+
: removeCodexConfig({ preserveProfile: journal.profileRestored || journal.profileChanged });
|
|
272
|
+
if (restored.success) {
|
|
273
|
+
// A successful journal/fallback write can race native history migration too.
|
|
274
|
+
// Refuse here while preimage compensation and the remove transaction can roll back.
|
|
275
|
+
const finalHistoryError = preflightCodexHistoryInjection(false, false);
|
|
276
|
+
if (finalHistoryError) return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${finalHistoryError}.` };
|
|
277
|
+
}
|
|
278
|
+
return restored.success
|
|
279
|
+
? {
|
|
280
|
+
state: "ok",
|
|
281
|
+
changed: journal.configRestored || journal.profileRestored || journal.profileChanged || restored.message.startsWith("Removed"),
|
|
282
|
+
action: journal.configRestored ? "journal-restored" : "owned-fields-stripped",
|
|
283
|
+
message: restored.message,
|
|
284
|
+
}
|
|
285
|
+
: { state: "failed", changed: false, action: "failed", message: restored.message };
|
|
286
|
+
} catch (error) {
|
|
287
|
+
return { state: "failed", changed: false, action: "failed", message: error instanceof Error ? error.message : String(error) };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The catalog half, always inside its own K acquisition. */
|
|
292
|
+
/**
|
|
293
|
+
* The catalog half, always inside its own K acquisition.
|
|
294
|
+
*
|
|
295
|
+
* `journaledCatalogPath` must be captured by the CALLER, before the config half runs: a
|
|
296
|
+
* successful journal restore deletes the journal, and a config restore can remove
|
|
297
|
+
* `model_catalog_json`. Reading it here would be too late in both cases (#1798).
|
|
298
|
+
*/
|
|
299
|
+
function restoreCodexCatalogArtifact(
|
|
300
|
+
revalidateDesiredState: boolean,
|
|
301
|
+
journaledCatalogPath: string | null,
|
|
302
|
+
): CodexRestoreCatalogResult {
|
|
303
|
+
const owningCodexHome = getCodexHome();
|
|
304
|
+
try {
|
|
305
|
+
const restored = withCatalogWriteSerialization(owningCodexHome, permit =>
|
|
306
|
+
revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())
|
|
307
|
+
? null
|
|
308
|
+
: restoreCodexCatalogWithPermit(permit, owningCodexHome, journaledCatalogPath));
|
|
309
|
+
return restored.kind === "completed" && restored.value !== null
|
|
310
|
+
? { state: "ok", changed: restored.value.removed > 0, ...restored.value, message: "Codex catalog restored." }
|
|
311
|
+
: restored.kind === "completed"
|
|
312
|
+
? {
|
|
313
|
+
state: "skipped", changed: false, removed: 0, kept: 0, path: null,
|
|
314
|
+
message: "Codex integration was re-enabled; native catalog restoration was skipped.",
|
|
315
|
+
}
|
|
316
|
+
: {
|
|
317
|
+
state: "failed", changed: false, removed: 0, kept: 0, path: DEFAULT_CATALOG_PATH,
|
|
318
|
+
message: `Codex catalog could not be restored: ${restored.reason}.`,
|
|
319
|
+
};
|
|
320
|
+
} catch (error) {
|
|
321
|
+
return {
|
|
322
|
+
state: "failed", changed: false, removed: 0, kept: 0, path: DEFAULT_CATALOG_PATH,
|
|
323
|
+
message: error instanceof Error ? error.message : String(error),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Restore native Codex, running history in a Worker under H.
|
|
330
|
+
*
|
|
331
|
+
* On a coordinated home the config/profile restore happens INSIDE the Codex
|
|
332
|
+
* write lock, publishing a `remove` transition — the same serialization inject
|
|
333
|
+
* uses. Without it, an older restore could overwrite a config a concurrent
|
|
334
|
+
* enable had just written under the lock, and then honestly report success
|
|
335
|
+
* while desired intent said ON. The desired-state re-read under the lock turns
|
|
336
|
+
* that lost race into the discriminated `desired_enabled` skip.
|
|
337
|
+
*/
|
|
338
|
+
export async function restoreNativeCodexAsync(
|
|
339
|
+
options: { revalidateDesiredState?: boolean } = {},
|
|
340
|
+
): Promise<CodexNativeRestoreResult> {
|
|
341
|
+
try {
|
|
342
|
+
return await restoreNativeCodexAsyncImpl(options);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (!(error instanceof CodexRestoreRefusal)) throw error;
|
|
345
|
+
return failedConfigRestoreEnvelope(error.config);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function restoreNativeCodexAsyncImpl(
|
|
350
|
+
options: { revalidateDesiredState?: boolean },
|
|
351
|
+
): Promise<CodexNativeRestoreResult> {
|
|
352
|
+
const activeProvider = currentExternalCodexModelProvider();
|
|
353
|
+
if (activeProvider) {
|
|
354
|
+
// External-provider courtesy: only the stale journal is removed. The
|
|
355
|
+
// history worker must not launch — it would turn a read-mostly courtesy
|
|
356
|
+
// result into a history mutation on a home we do not own.
|
|
357
|
+
removeJournal();
|
|
358
|
+
return externalProviderRestoreResult(activeProvider);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// `restore` normally honours a human request even when an unrelated
|
|
362
|
+
// service-manager probe is unavailable. A recorded FOREIGN home is not an
|
|
363
|
+
// unrelated probe: it is positive evidence another installation owns these
|
|
364
|
+
// native artifacts, so do not create profile/claim locks before refusing.
|
|
365
|
+
if (options.revalidateDesiredState) {
|
|
366
|
+
const ownership = inspectNativeCodexOwnership();
|
|
367
|
+
if (ownership.ownership === "foreign") return foreignOwnershipRestoreRefusal(ownership.reason);
|
|
368
|
+
if (shouldSyncCodexOnStart(loadConfig())) return desiredEnabledRestoreSkip();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const historyError = preflightCodexHistoryInjection(false, false);
|
|
372
|
+
if (historyError) return historyPreflightRefusalEnvelope(historyError);
|
|
373
|
+
|
|
374
|
+
const eligibility = codexWriteCoordinationEligibility({
|
|
375
|
+
coordinatorPath: () =>
|
|
376
|
+
resolveCodexCoordinatorDatabasePath(resolveEffectiveUserIdentity(), getCodexHome()),
|
|
377
|
+
residue: () => classifyNativeRoutedResidue(),
|
|
378
|
+
integrationRecord: () => readIntegrationRecord(),
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
382
|
+
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
383
|
+
// catalog we actually wrote (#1798).
|
|
384
|
+
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
385
|
+
let config: CodexRestoreConfigResult;
|
|
386
|
+
let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
|
|
387
|
+
|
|
388
|
+
if (eligibility.kind === "coordinated" || eligibility.kind === "adopt") {
|
|
389
|
+
// The restore has no candidate bytes to witness; freshness comes from the
|
|
390
|
+
// filesystem reads and the desired-state re-read performed under the lock.
|
|
391
|
+
const witness = { authoritySnapshotId: "codex-native-restore" };
|
|
392
|
+
const coordinated = await withCodexWriteLock(
|
|
393
|
+
{
|
|
394
|
+
timeoutMs: DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
395
|
+
...(eligibility.kind === "adopt" ? { adoption: { direction: "remove" as const } } : {}),
|
|
396
|
+
admitted: witness,
|
|
397
|
+
readAdmissionUnderLock: () => witness,
|
|
398
|
+
},
|
|
399
|
+
(ctx) => {
|
|
400
|
+
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
401
|
+
throw new CodexWriteLockSkipped("desired_enabled");
|
|
402
|
+
}
|
|
403
|
+
const published = ctx.coordinator.beginTransition(
|
|
404
|
+
{
|
|
405
|
+
nativeGeneration: ctx.expectation.nativeBefore,
|
|
406
|
+
currentTxId: ctx.currentTxId,
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
txId: ctx.expectation.txId,
|
|
410
|
+
direction: "remove",
|
|
411
|
+
authoritySnapshotId: ctx.admission.authoritySnapshotId,
|
|
412
|
+
nextRetryAt: new Date().toISOString(),
|
|
413
|
+
},
|
|
414
|
+
);
|
|
415
|
+
if (published.kind !== "updated") {
|
|
416
|
+
throw new CodexWriteConflictError(
|
|
417
|
+
`The Codex transition could not be published: ${published.kind}.`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const preImages = captureCodexPreImages();
|
|
421
|
+
let restored: CodexRestoreConfigResult;
|
|
422
|
+
try {
|
|
423
|
+
restored = restoreCodexConfigInline(eligibility.kind);
|
|
424
|
+
// Throw inside N so the published remove transition rolls back too.
|
|
425
|
+
if (restored.state === "failed") throw new CodexRestoreRefusal(restored);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
const compensated = restoreCodexPreImages(preImages);
|
|
428
|
+
if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
config: restored,
|
|
433
|
+
preImages,
|
|
434
|
+
receipt: {
|
|
435
|
+
nativeGeneration: ctx.expectation.nativeAfter,
|
|
436
|
+
currentTxId: ctx.expectation.txId,
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
},
|
|
440
|
+
);
|
|
441
|
+
if (coordinated.status === "skipped") return desiredEnabledRestoreSkip();
|
|
442
|
+
if (coordinated.status !== "acquired") {
|
|
443
|
+
config = {
|
|
444
|
+
state: "failed",
|
|
445
|
+
changed: false,
|
|
446
|
+
action: "failed",
|
|
447
|
+
message: coordinated.status === "busy"
|
|
448
|
+
? `Another process is writing Codex configuration right now (waited ${coordinated.waitedMs}ms). Retry shortly.`
|
|
449
|
+
: `Codex configuration was not restored: ${coordinated.message}`,
|
|
450
|
+
};
|
|
451
|
+
} else {
|
|
452
|
+
recordCodexNativeTransactionProvenance(
|
|
453
|
+
coordinated.value.preImages,
|
|
454
|
+
coordinated.value.receipt.currentTxId,
|
|
455
|
+
);
|
|
456
|
+
config = coordinated.value.config;
|
|
457
|
+
transitionReceipt = coordinated.value.receipt;
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
// Legacy-uncoordinated (or unresolvable) homes keep the unserialized path
|
|
461
|
+
// they have always had; restore is the escape hatch and must not strand
|
|
462
|
+
// them. The plain re-read still honors an intervening re-enable.
|
|
463
|
+
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
464
|
+
return desiredEnabledRestoreSkip();
|
|
465
|
+
}
|
|
466
|
+
config = restoreCodexConfigInline(eligibility.kind);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (config.state === "failed") return failedConfigRestoreEnvelope(config);
|
|
470
|
+
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
471
|
+
const outcome = await runCodexHistoryJob({
|
|
472
|
+
...resolveCodexHistoryJobTarget(),
|
|
473
|
+
...(options.revalidateDesiredState ? { expectedDesiredEnabled: false } : {}),
|
|
474
|
+
operation: deriveCodexHistoryOperation({ direction: "restore", resumeHistory: true, legacyMode: false }),
|
|
475
|
+
});
|
|
476
|
+
if (transitionReceipt) {
|
|
477
|
+
resolveCodexHistoryTransition(transitionReceipt, outcome);
|
|
478
|
+
}
|
|
479
|
+
const history: CodexRestoreHistoryResult = outcome.kind === "converged"
|
|
480
|
+
? {
|
|
481
|
+
state: "ok", changed: outcome.rows > 0 || outcome.files > 0, rows: outcome.rows, files: outcome.files, ejectedRows: 0,
|
|
482
|
+
message: outcome.rows > 0
|
|
483
|
+
? `Resume history metadata restored from opencodex backup (${outcome.rows} thread(s)); original providers preserved.`
|
|
484
|
+
: "No backed-up resume-history metadata was pending; untracked routed history was left unchanged.",
|
|
485
|
+
}
|
|
486
|
+
: outcome.kind === "skipped"
|
|
487
|
+
? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "Codex resume history was skipped." }
|
|
488
|
+
: outcome.kind === "blocked" && (outcome.reason === "desired_disabled" || outcome.reason === "desired_enabled")
|
|
489
|
+
? {
|
|
490
|
+
state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0,
|
|
491
|
+
message: outcome.reason === "desired_disabled"
|
|
492
|
+
? "Codex integration was disabled; history restoration was skipped."
|
|
493
|
+
: "Codex integration was enabled; history restoration was skipped.",
|
|
494
|
+
}
|
|
495
|
+
: outcome.kind === "blocked" || outcome.kind === "failed"
|
|
496
|
+
? failedHistoryRestoreFromOutcome(outcome)
|
|
497
|
+
: failedHistoryRestore();
|
|
498
|
+
const base = catalog.removed > 0
|
|
499
|
+
? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
|
|
500
|
+
: config.message;
|
|
501
|
+
const success = catalog.state !== "failed"
|
|
502
|
+
&& history.state !== "failed";
|
|
503
|
+
return {
|
|
504
|
+
success,
|
|
505
|
+
message: `${base}${history.state === "failed" ? ` ⚠️ ${history.message}` : ""}`,
|
|
506
|
+
artifacts: { config, catalog, history },
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateDesiredState?: boolean } = {}): CodexNativeRestoreResult {
|
|
511
|
+
const activeProvider = currentExternalCodexModelProvider();
|
|
512
|
+
if (activeProvider) {
|
|
513
|
+
removeJournal();
|
|
514
|
+
return externalProviderRestoreResult(activeProvider);
|
|
515
|
+
}
|
|
516
|
+
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
517
|
+
return desiredEnabledRestoreSkip();
|
|
518
|
+
}
|
|
519
|
+
const historyError = preflightCodexHistoryInjection(false, false);
|
|
520
|
+
if (historyError) return historyPreflightRefusalEnvelope(historyError);
|
|
521
|
+
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
522
|
+
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
523
|
+
// catalog we actually wrote (#1798).
|
|
524
|
+
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
525
|
+
const config = restoreCodexConfigInline();
|
|
526
|
+
if (config.state === "failed") return failedConfigRestoreEnvelope(config);
|
|
527
|
+
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
528
|
+
// Design B (loopback) steady state: threads are already tagged openai, so prove the
|
|
529
|
+
// no-op with a readonly probe instead of write-opening a DB the Codex app may hold
|
|
530
|
+
// (Windows: WAL writer lock -> seconds of stalling + a false warning on every stop).
|
|
531
|
+
// Legacy (non-loopback) installs keep the unconditional write-open restore.
|
|
532
|
+
let skipWhenProvablyNoop = false;
|
|
533
|
+
try {
|
|
534
|
+
skipWhenProvablyNoop = !shouldInjectApiAuthHeader(loadConfig());
|
|
535
|
+
} catch {
|
|
536
|
+
/* unreadable config: keep the conservative write-open restore */
|
|
537
|
+
}
|
|
538
|
+
// `skipHistory` is how the async wrapper takes this work for itself: the
|
|
539
|
+
// native files come down here, and history runs in the Worker under H.
|
|
540
|
+
const rawHistory = options.skipHistory
|
|
541
|
+
? { rows: 0, files: 0 }
|
|
542
|
+
: syncCodexHistoryProvider("openai", undefined, undefined, {
|
|
543
|
+
skipWhenProvablyNoop,
|
|
544
|
+
});
|
|
545
|
+
const history: CodexRestoreHistoryResult = options.skipHistory
|
|
546
|
+
? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "History restoration runs asynchronously." }
|
|
547
|
+
: rawHistory.failed
|
|
548
|
+
? failedHistoryRestore(rawHistory.failureReason, undefined, rawHistory)
|
|
549
|
+
: {
|
|
550
|
+
state: "ok",
|
|
551
|
+
changed: rawHistory.rows > 0 || rawHistory.files > 0 || (rawHistory.ejectedRows ?? 0) > 0,
|
|
552
|
+
rows: rawHistory.rows,
|
|
553
|
+
files: rawHistory.files,
|
|
554
|
+
ejectedRows: rawHistory.ejectedRows ?? 0,
|
|
555
|
+
message: rawHistory.rows > 0
|
|
556
|
+
? `Resume history metadata restored from opencodex backup (${rawHistory.rows} thread(s)); original providers preserved.`
|
|
557
|
+
: "No backed-up resume-history metadata was pending; untracked routed history was left unchanged.",
|
|
558
|
+
};
|
|
559
|
+
const message = catalog.removed > 0
|
|
560
|
+
? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
|
|
561
|
+
: config.message;
|
|
562
|
+
return {
|
|
563
|
+
success: catalog.state !== "failed" && history.state !== "failed",
|
|
564
|
+
message,
|
|
565
|
+
artifacts: { config, catalog, history },
|
|
566
|
+
};
|
|
567
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
hasInjectedCodexRouting,
|
|
4
|
+
hasInjectedOpenaiBaseUrl,
|
|
5
|
+
providerTableStart,
|
|
6
|
+
providerTableString,
|
|
7
|
+
rootTomlString,
|
|
8
|
+
} from "../injected-marker";
|
|
9
|
+
import { CODEX_CONFIG_PATH } from "../paths";
|
|
10
|
+
|
|
11
|
+
export type CodexRoutingKind =
|
|
12
|
+
"native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown";
|
|
13
|
+
|
|
14
|
+
type RoutingEndpointKind = "local" | "remote" | "unknown";
|
|
15
|
+
|
|
16
|
+
function ipv4Octets(hostname: string): number[] | null {
|
|
17
|
+
const dotted = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
18
|
+
if (dotted) {
|
|
19
|
+
const octets = dotted.slice(1).map(Number);
|
|
20
|
+
return octets.some((octet) => octet > 255) ? null : octets;
|
|
21
|
+
}
|
|
22
|
+
const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(hostname);
|
|
23
|
+
if (!mapped) return null;
|
|
24
|
+
const high = Number.parseInt(mapped[1], 16);
|
|
25
|
+
const low = Number.parseInt(mapped[2], 16);
|
|
26
|
+
return [high >>> 8, high & 0xff, low >>> 8, low & 0xff];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function classifyRoutingEndpoint(value: string): RoutingEndpointKind {
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(value);
|
|
32
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return "unknown";
|
|
33
|
+
const hostname = url.hostname
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/^\[|\]$/g, "")
|
|
36
|
+
.replace(/\.$/, "");
|
|
37
|
+
if (!hostname) return "unknown";
|
|
38
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost"))
|
|
39
|
+
return "local";
|
|
40
|
+
if (hostname === "::" || hostname === "::1" || hostname === "0.0.0.0")
|
|
41
|
+
return "local";
|
|
42
|
+
const octets = ipv4Octets(hostname);
|
|
43
|
+
if (octets) {
|
|
44
|
+
if (octets.every((octet) => octet === 0)) return "local";
|
|
45
|
+
if (octets[0] === 127) return "local";
|
|
46
|
+
return "remote";
|
|
47
|
+
}
|
|
48
|
+
if (/^::ffff:/i.test(hostname)) return "unknown";
|
|
49
|
+
return "remote";
|
|
50
|
+
} catch {
|
|
51
|
+
return "unknown";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Classify actual routing dependency separately from opencodex ownership. */
|
|
56
|
+
export function classifyCodexRouting(content: string): CodexRoutingKind {
|
|
57
|
+
const rootBaseUrl = rootTomlString(content, "openai_base_url");
|
|
58
|
+
if (rootBaseUrl) {
|
|
59
|
+
const endpoint = classifyRoutingEndpoint(rootBaseUrl);
|
|
60
|
+
if (endpoint === "unknown") return "unknown";
|
|
61
|
+
if (hasInjectedOpenaiBaseUrl(content)) return "opencodex-local";
|
|
62
|
+
return endpoint === "local" ? "custom-local" : "custom-remote";
|
|
63
|
+
}
|
|
64
|
+
const rootProvider = rootTomlString(content, "model_provider");
|
|
65
|
+
if (rootProvider) {
|
|
66
|
+
const providerTableExists =
|
|
67
|
+
providerTableStart(content.split("\n"), rootProvider) !== -1;
|
|
68
|
+
const providerBaseUrl = providerTableString(
|
|
69
|
+
content,
|
|
70
|
+
rootProvider,
|
|
71
|
+
"base_url",
|
|
72
|
+
);
|
|
73
|
+
if (providerBaseUrl) {
|
|
74
|
+
const endpoint = classifyRoutingEndpoint(providerBaseUrl);
|
|
75
|
+
if (endpoint === "unknown") return "unknown";
|
|
76
|
+
if (rootProvider === "opencodex") return "opencodex-local";
|
|
77
|
+
return endpoint === "local" ? "custom-local" : "custom-remote";
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
rootProvider === "opencodex" ||
|
|
81
|
+
providerTableExists ||
|
|
82
|
+
rootProvider !== "openai"
|
|
83
|
+
)
|
|
84
|
+
return "unknown";
|
|
85
|
+
}
|
|
86
|
+
return "native";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Read-only probe used by status, doctor, and the dashboard. */
|
|
90
|
+
export function isCodexRoutingInjected(): boolean {
|
|
91
|
+
const path = CODEX_CONFIG_PATH;
|
|
92
|
+
if (!existsSync(path)) return false;
|
|
93
|
+
try {
|
|
94
|
+
return hasInjectedCodexRouting(readFileSync(path, "utf8"));
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getCodexRoutingKind(): CodexRoutingKind {
|
|
101
|
+
const path = CODEX_CONFIG_PATH;
|
|
102
|
+
if (!existsSync(path)) return "native";
|
|
103
|
+
try {
|
|
104
|
+
return classifyCodexRouting(readFileSync(path, "utf8"));
|
|
105
|
+
} catch {
|
|
106
|
+
return "unknown";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|