@bitkyc08/opencodex 2.56.0 → 2.58.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-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
- package/src/adapters/command-code.ts +12 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +19 -1
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +55 -5
- package/src/adapters/google-http.ts +38 -13
- package/src/adapters/google.ts +7 -7
- 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/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +25 -7
- package/src/adapters/openai-chat.ts +8 -8
- package/src/adapters/openai-responses/passthrough.ts +62 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/physical-send.ts +50 -0
- package/src/bridge/errors.ts +26 -2
- package/src/bridge/response-json.ts +8 -2
- package/src/bridge/sse.ts +20 -2
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +32 -4
- 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/config-command.ts +35 -18
- package/src/cli/dispatch.ts +17 -4
- package/src/cli/index.ts +92 -7
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +74 -5
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-store.ts +113 -26
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/login-flow.ts +14 -2
- package/src/codex/auth-api/reset-credit-service.ts +11 -2
- package/src/codex/auth-context.ts +199 -15
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/model-visibility.ts +1 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/retained-sync.ts +9 -1
- package/src/codex/catalog/routed-gather.ts +38 -1
- 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/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +33 -4
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +231 -32
- package/src/codex/inject.ts +12 -16
- package/src/codex/loopback-target.ts +9 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/pool-refresh-backoff.ts +12 -3
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +10 -0
- package/src/codex/routing/selection.ts +79 -2
- package/src/codex/routing/thread-affinity.ts +50 -2
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +29 -49
- 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/atomic-write.ts +83 -8
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +272 -180
- package/src/images/loop.ts +1 -1
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +147 -21
- package/src/lib/spend-reservation-ledger.ts +18 -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 +77 -10
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/derive.ts +6 -0
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +35 -7
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +59 -28
- package/src/providers/registry/model-seeds.ts +71 -17
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/routing/identity-domains.ts +21 -14
- package/src/routing/probe-lease.ts +103 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/live-sideband.ts +37 -1
- package/src/server/index/websocket-handler.ts +54 -3
- package/src/server/index.ts +5 -5
- 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/config-routes.ts +27 -5
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log.ts +127 -3
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +74 -0
- package/src/server/responses/adapter-continuation.ts +33 -7
- package/src/server/responses/adapter-delivery.ts +5 -11
- package/src/server/responses/adapter-dispatch.ts +84 -13
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- 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/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +54 -13
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-codex-account.ts +51 -3
- package/src/server/responses/core-combo.ts +129 -23
- package/src/server/responses/core-errors.ts +18 -0
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/core-replay.ts +105 -32
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/fetch-helpers.ts +4 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +30 -6
- package/src/server/responses/passthrough-dispatch.ts +61 -11
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/request-prepare.ts +173 -22
- package/src/server/responses/request-send-budget.ts +97 -2
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +62 -3
- package/src/server/responses/run-turn-execution.ts +59 -31
- package/src/server/responses/sidecar-execution.ts +7 -13
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses/ws-upstream.ts +21 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +13 -1
- 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 +8 -1
- package/src/types/provider.ts +13 -0
- 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 +1 -1
- 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
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import {
|
|
29
29
|
preflightCodexHistoryInjection,
|
|
30
30
|
syncCodexHistoryProvider,
|
|
31
|
+
HISTORY_RELABEL_STANDS_DOWN,
|
|
31
32
|
type CodexHistoryFailureReason,
|
|
32
33
|
} from "../history-provider";
|
|
33
34
|
import {
|
|
@@ -44,7 +45,11 @@ import {
|
|
|
44
45
|
} from "../paths";
|
|
45
46
|
import { shouldInjectApiAuthHeader } from "../loopback-target";
|
|
46
47
|
import { currentExternalCodexModelProvider } from "./config-toml";
|
|
47
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
readOcxProviderTableBlock,
|
|
50
|
+
removeCodexConfig,
|
|
51
|
+
retainOcxProviderTableOnDisk,
|
|
52
|
+
} from "./remove";
|
|
48
53
|
|
|
49
54
|
class CodexRestoreRefusal extends Error {
|
|
50
55
|
constructor(readonly config: CodexRestoreConfigResult) {
|
|
@@ -57,13 +62,49 @@ export function setBeforeRestoreConfigForTests(hook: typeof beforeRestoreConfigF
|
|
|
57
62
|
beforeRestoreConfigForTests = hook;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
/**
|
|
66
|
+
* `partial` means the artifact was restored as far as it safely could be and named what
|
|
67
|
+
* it left behind. It is not a failure — the caller's obligation was discharged — but it
|
|
68
|
+
* is not a plain `ok` either, because something on disk still needs a decision (#4812).
|
|
69
|
+
*/
|
|
70
|
+
export type CodexRestoreArtifactState = "ok" | "partial" | "skipped" | "failed";
|
|
71
|
+
|
|
72
|
+
/** What a degraded restore kept, why, and how to finish the job. */
|
|
73
|
+
export interface RetainedCodexProviderTable {
|
|
74
|
+
reason: typeof HISTORY_RELABEL_STANDS_DOWN;
|
|
75
|
+
/** The exact `config.toml` lines left on disk. */
|
|
76
|
+
lines: string[];
|
|
77
|
+
followUp: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const RETAINED_PROVIDER_TABLE_FOLLOW_UP =
|
|
81
|
+
"Run 'ocx restore --remove-codex-provider-table' to remove it; conversations already tagged "
|
|
82
|
+
+ "opencodex will stop opening if you do.";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The one sentence every teardown surface prints about retained residue.
|
|
86
|
+
*
|
|
87
|
+
* Shared rather than rewritten per caller: `restore`, `stop`, `uninstall`, the service
|
|
88
|
+
* subcommands and the stop API all report this same outcome, and a user who runs two of
|
|
89
|
+
* them should not have to work out whether two different descriptions mean the same state.
|
|
90
|
+
*/
|
|
91
|
+
export function describeRetainedCodexProviderTable(retained: RetainedCodexProviderTable): string {
|
|
92
|
+
return "Kept [model_providers.opencodex] in $CODEX_HOME/config.toml because Codex owns this home's"
|
|
93
|
+
+ ` paginated history (${retained.reason}): conversations already tagged opencodex resolve only`
|
|
94
|
+
+ ` through that table. Plain \`codex\` is native again. ${retained.followUp}`;
|
|
95
|
+
}
|
|
61
96
|
|
|
62
97
|
export interface CodexRestoreConfigResult {
|
|
63
98
|
state: CodexRestoreArtifactState;
|
|
64
99
|
changed: boolean;
|
|
65
|
-
action:
|
|
100
|
+
action:
|
|
101
|
+
| "journal-restored"
|
|
102
|
+
| "owned-fields-stripped"
|
|
103
|
+
| "routing-restored-provider-retained"
|
|
104
|
+
| "external-provider-preserved"
|
|
105
|
+
| "failed";
|
|
66
106
|
message: string;
|
|
107
|
+
retained?: RetainedCodexProviderTable;
|
|
67
108
|
}
|
|
68
109
|
|
|
69
110
|
export interface CodexRestoreCatalogResult {
|
|
@@ -89,6 +130,26 @@ export interface CodexNativeRestoreResult {
|
|
|
89
130
|
success: boolean;
|
|
90
131
|
message: string;
|
|
91
132
|
externalProvider?: string;
|
|
133
|
+
/**
|
|
134
|
+
* Set when the restore refused at the Codex history preflight (#4718).
|
|
135
|
+
*
|
|
136
|
+
* The preflight runs before the config half, so a refusal leaves config, catalog,
|
|
137
|
+
* history and provenance exactly as they were. That is a different outcome from a
|
|
138
|
+
* restore that ran and failed, and callers that decide whether an obligation was
|
|
139
|
+
* discharged need to tell them apart. Reading the artifact states alone cannot: a
|
|
140
|
+
* refusal reports every artifact as `skipped`, which is also what an ownership refusal
|
|
141
|
+
* and a desired-state skip report. Matching the human-readable message instead would
|
|
142
|
+
* make a safety decision depend on prose.
|
|
143
|
+
*/
|
|
144
|
+
historyPreflightRefusal?: string;
|
|
145
|
+
/**
|
|
146
|
+
* Set when routing came out but `[model_providers.opencodex]` stayed (#4812).
|
|
147
|
+
*
|
|
148
|
+
* Distinct from `historyPreflightRefusal`, which means nothing was attempted at all.
|
|
149
|
+
* This one means the config obligation WAS discharged, so a stop receipt must be
|
|
150
|
+
* released rather than preserved.
|
|
151
|
+
*/
|
|
152
|
+
retainedCodexProviderTable?: RetainedCodexProviderTable;
|
|
92
153
|
artifacts: {
|
|
93
154
|
config: CodexRestoreConfigResult;
|
|
94
155
|
catalog: CodexRestoreCatalogResult;
|
|
@@ -216,10 +277,57 @@ function failedConfigRestoreEnvelope(config: CodexRestoreConfigResult): CodexNat
|
|
|
216
277
|
return result;
|
|
217
278
|
}
|
|
218
279
|
|
|
280
|
+
/**
|
|
281
|
+
* The history preflight refused, so nothing was attempted at all (#4718).
|
|
282
|
+
*
|
|
283
|
+
* The message is unchanged from what this path has always printed; the structured reason
|
|
284
|
+
* is added beside it so a caller can act on the refusal without reading the prose.
|
|
285
|
+
*/
|
|
286
|
+
function historyPreflightRefusalEnvelope(historyError: string): CodexNativeRestoreResult {
|
|
287
|
+
const result = skippedRestoreEnvelope(
|
|
288
|
+
false,
|
|
289
|
+
`Native restore refused: ${historyError}. Config, catalog, history and provenance were preserved.`,
|
|
290
|
+
);
|
|
291
|
+
result.historyPreflightRefusal = historyError;
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* How a restore may proceed given what the history preflight says.
|
|
297
|
+
*
|
|
298
|
+
* The preflight answers one question — may conversation history be rewritten — and this
|
|
299
|
+
* translates it into the separate question the restore actually needs answered: may
|
|
300
|
+
* OpenCodex routing come out of `config.toml`, and what has to stay if it does.
|
|
301
|
+
*/
|
|
302
|
+
export type RestoreHistoryDisposition =
|
|
303
|
+
| { kind: "proceed" }
|
|
304
|
+
| { kind: "stand-down"; retainProviderTable: boolean }
|
|
305
|
+
| { kind: "refuse"; reason: string };
|
|
306
|
+
|
|
307
|
+
export function resolveRestoreHistoryDisposition(
|
|
308
|
+
removeProviderTable: boolean | undefined,
|
|
309
|
+
reason: string | null = preflightCodexHistoryInjection(false, false),
|
|
310
|
+
): RestoreHistoryDisposition {
|
|
311
|
+
if (!reason) return { kind: "proceed" };
|
|
312
|
+
// Every other reason still means the history state itself is wrong — a missing store, an
|
|
313
|
+
// unreadable rollout, an integrity failure. Those keep the hard refusal and the
|
|
314
|
+
// compensating rollback they have always had.
|
|
315
|
+
if (reason !== HISTORY_RELABEL_STANDS_DOWN) return { kind: "refuse", reason };
|
|
316
|
+
// The rows stay tagged `opencodex` either way, because the native writer owns them.
|
|
317
|
+
// Retaining the table is what keeps those conversations openable; the explicit flag is
|
|
318
|
+
// the user accepting that they will not be.
|
|
319
|
+
return { kind: "stand-down", retainProviderTable: removeProviderTable !== true };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface RestoreConfigOptions {
|
|
323
|
+
/** Strip `[model_providers.opencodex]` too, accepting that tagged threads stop opening. */
|
|
324
|
+
removeProviderTable?: boolean;
|
|
325
|
+
}
|
|
326
|
+
|
|
219
327
|
/** The config/profile half of a native restore, reported as one artifact. */
|
|
220
|
-
function restoreCodexConfigInline(kind = "sync"): CodexRestoreConfigResult {
|
|
328
|
+
function restoreCodexConfigInline(kind = "sync", options: RestoreConfigOptions = {}): CodexRestoreConfigResult {
|
|
221
329
|
const preImages = captureCodexPreImages();
|
|
222
|
-
const result = restoreCodexConfigInlineImpl(kind);
|
|
330
|
+
const result = restoreCodexConfigInlineImpl(kind, options);
|
|
223
331
|
if (result.state === "failed") {
|
|
224
332
|
const compensated = restoreCodexPreImages(preImages);
|
|
225
333
|
if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
|
|
@@ -227,11 +335,25 @@ function restoreCodexConfigInline(kind = "sync"): CodexRestoreConfigResult {
|
|
|
227
335
|
return result;
|
|
228
336
|
}
|
|
229
337
|
|
|
230
|
-
function restoreCodexConfigInlineImpl(kind: string): CodexRestoreConfigResult {
|
|
338
|
+
function restoreCodexConfigInlineImpl(kind: string, options: RestoreConfigOptions): CodexRestoreConfigResult {
|
|
231
339
|
try {
|
|
232
340
|
beforeRestoreConfigForTests?.(kind);
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
341
|
+
const disposition = resolveRestoreHistoryDisposition(options.removeProviderTable);
|
|
342
|
+
if (disposition.kind === "refuse") {
|
|
343
|
+
return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${disposition.reason}.` };
|
|
344
|
+
}
|
|
345
|
+
// Captured unconditionally, not only when the stand-down is already known.
|
|
346
|
+
//
|
|
347
|
+
// Two different paths need bytes that only exist before the write. The journal restore
|
|
348
|
+
// replays the pre-injection config, which never contained our table, and then deletes
|
|
349
|
+
// the journal. And Codex can paginate DURING the write: the post-write re-check below
|
|
350
|
+
// then sees a stand-down that the pre-write check did not, at which point the table has
|
|
351
|
+
// already been stripped and there is nothing left to read. Both are cheap to prevent
|
|
352
|
+
// and impossible to repair afterwards, so the read happens once, here.
|
|
353
|
+
//
|
|
354
|
+
// The one caller that must not capture is the explicit removal flag: it is the user
|
|
355
|
+
// accepting that tagged conversations stop opening.
|
|
356
|
+
const capturedBlock = options.removeProviderTable === true ? null : readOcxProviderTableBlock();
|
|
235
357
|
const journal = restoreJournalState();
|
|
236
358
|
if (journal.unverified) {
|
|
237
359
|
return {
|
|
@@ -240,13 +362,51 @@ function restoreCodexConfigInlineImpl(kind: string): CodexRestoreConfigResult {
|
|
|
240
362
|
};
|
|
241
363
|
}
|
|
242
364
|
const restored = journal.configRestored
|
|
243
|
-
? { success: true, message: "Codex config restored from opencodex journal." }
|
|
244
|
-
: removeCodexConfig({
|
|
365
|
+
? { success: true, message: "Codex config restored from opencodex journal.", retainedProviderTable: undefined as string[] | undefined }
|
|
366
|
+
: removeCodexConfig({
|
|
367
|
+
preserveProfile: journal.profileRestored || journal.profileChanged,
|
|
368
|
+
// The history question was resolved above; hand the answer down rather than making
|
|
369
|
+
// the transform re-derive it, which refused the explicit-removal path outright.
|
|
370
|
+
historyDisposition: disposition.kind === "stand-down"
|
|
371
|
+
? disposition.retainProviderTable ? "stand-down-retain" : "stand-down-remove"
|
|
372
|
+
: "refuse-on-any",
|
|
373
|
+
});
|
|
374
|
+
let retainedLines = restored.retainedProviderTable ?? null;
|
|
245
375
|
if (restored.success) {
|
|
246
376
|
// A successful journal/fallback write can race native history migration too.
|
|
247
377
|
// Refuse here while preimage compensation and the remove transaction can roll back.
|
|
248
|
-
|
|
249
|
-
|
|
378
|
+
// A stand-down observed now is the same stand-down that was already accounted for —
|
|
379
|
+
// it must not undo a routing removal that has already reached disk.
|
|
380
|
+
const settled = resolveRestoreHistoryDisposition(options.removeProviderTable);
|
|
381
|
+
if (settled.kind === "refuse") {
|
|
382
|
+
return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${settled.reason}.` };
|
|
383
|
+
}
|
|
384
|
+
// One re-attach covers three cases that all need the same bytes on disk: the journal
|
|
385
|
+
// path, which wrote a config without our table; the migration race, where the strip ran
|
|
386
|
+
// before anyone knew a table was needed; and the ordinary planned retention, where
|
|
387
|
+
// `removeCodexConfig` already put it back and this is a no-op. Re-attaching is
|
|
388
|
+
// idempotent — it checks for the table before appending — so the three do not have to
|
|
389
|
+
// be told apart here.
|
|
390
|
+
if (settled.kind === "stand-down" && settled.retainProviderTable && capturedBlock !== null) {
|
|
391
|
+
retainedLines = retainOcxProviderTableOnDisk(capturedBlock) ?? retainedLines;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (restored.success && retainedLines !== null) {
|
|
395
|
+
return {
|
|
396
|
+
state: "partial",
|
|
397
|
+
changed: true,
|
|
398
|
+
action: "routing-restored-provider-retained",
|
|
399
|
+
message: journal.configRestored
|
|
400
|
+
? "Codex config restored from opencodex journal. Kept [model_providers.opencodex] so conversations already"
|
|
401
|
+
+ " tagged opencodex still open; remove it with 'ocx restore --remove-codex-provider-table'"
|
|
402
|
+
+ " (those conversations stop opening)."
|
|
403
|
+
: restored.message,
|
|
404
|
+
retained: {
|
|
405
|
+
reason: HISTORY_RELABEL_STANDS_DOWN,
|
|
406
|
+
lines: retainedLines,
|
|
407
|
+
followUp: RETAINED_PROVIDER_TABLE_FOLLOW_UP,
|
|
408
|
+
},
|
|
409
|
+
};
|
|
250
410
|
}
|
|
251
411
|
return restored.success
|
|
252
412
|
? {
|
|
@@ -309,7 +469,7 @@ function restoreCodexCatalogArtifact(
|
|
|
309
469
|
* that lost race into the discriminated `desired_enabled` skip.
|
|
310
470
|
*/
|
|
311
471
|
export async function restoreNativeCodexAsync(
|
|
312
|
-
options: { revalidateDesiredState?: boolean } = {},
|
|
472
|
+
options: { revalidateDesiredState?: boolean; removeProviderTable?: boolean } = {},
|
|
313
473
|
): Promise<CodexNativeRestoreResult> {
|
|
314
474
|
try {
|
|
315
475
|
return await restoreNativeCodexAsyncImpl(options);
|
|
@@ -320,7 +480,7 @@ export async function restoreNativeCodexAsync(
|
|
|
320
480
|
}
|
|
321
481
|
|
|
322
482
|
async function restoreNativeCodexAsyncImpl(
|
|
323
|
-
options: { revalidateDesiredState?: boolean },
|
|
483
|
+
options: { revalidateDesiredState?: boolean; removeProviderTable?: boolean },
|
|
324
484
|
): Promise<CodexNativeRestoreResult> {
|
|
325
485
|
const activeProvider = currentExternalCodexModelProvider();
|
|
326
486
|
if (activeProvider) {
|
|
@@ -341,8 +501,12 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
341
501
|
if (shouldSyncCodexOnStart(loadConfig())) return desiredEnabledRestoreSkip();
|
|
342
502
|
}
|
|
343
503
|
|
|
344
|
-
const
|
|
345
|
-
if (
|
|
504
|
+
const disposition = resolveRestoreHistoryDisposition(options.removeProviderTable);
|
|
505
|
+
if (disposition.kind === "refuse") return historyPreflightRefusalEnvelope(disposition.reason);
|
|
506
|
+
// A stand-down spawns no history Worker. The preflight the Worker would run first has
|
|
507
|
+
// already answered, and the rows stay tagged `opencodex` on purpose — which is exactly
|
|
508
|
+
// why the provider table has to survive the config half.
|
|
509
|
+
const historyStandsDown = disposition.kind === "stand-down";
|
|
346
510
|
|
|
347
511
|
const eligibility = codexWriteCoordinationEligibility({
|
|
348
512
|
coordinatorPath: () =>
|
|
@@ -393,7 +557,7 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
393
557
|
const preImages = captureCodexPreImages();
|
|
394
558
|
let restored: CodexRestoreConfigResult;
|
|
395
559
|
try {
|
|
396
|
-
restored = restoreCodexConfigInline(eligibility.kind);
|
|
560
|
+
restored = restoreCodexConfigInline(eligibility.kind, options);
|
|
397
561
|
// Throw inside N so the published remove transition rolls back too.
|
|
398
562
|
if (restored.state === "failed") throw new CodexRestoreRefusal(restored);
|
|
399
563
|
} catch (error) {
|
|
@@ -436,20 +600,34 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
436
600
|
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
437
601
|
return desiredEnabledRestoreSkip();
|
|
438
602
|
}
|
|
439
|
-
config = restoreCodexConfigInline(eligibility.kind);
|
|
603
|
+
config = restoreCodexConfigInline(eligibility.kind, options);
|
|
440
604
|
}
|
|
441
605
|
|
|
442
606
|
if (config.state === "failed") return failedConfigRestoreEnvelope(config);
|
|
443
607
|
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
608
|
+
// Re-asked after the config half, because the store can paginate mid-transaction. Deciding
|
|
609
|
+
// the history job from the pre-write answer alone would spawn a Worker whose own preflight
|
|
610
|
+
// is now guaranteed to refuse, and report that refusal as a restore failure on a home that
|
|
611
|
+
// was in fact restored.
|
|
612
|
+
const historyStoodDown = historyStandsDown
|
|
613
|
+
|| resolveRestoreHistoryDisposition(options.removeProviderTable).kind === "stand-down";
|
|
614
|
+
const outcome: CodexHistoryJobOutcome = historyStoodDown
|
|
615
|
+
? { kind: "skipped" }
|
|
616
|
+
: await runCodexHistoryJob({
|
|
617
|
+
...resolveCodexHistoryJobTarget(),
|
|
618
|
+
...(options.revalidateDesiredState ? { expectedDesiredEnabled: false } : {}),
|
|
619
|
+
operation: deriveCodexHistoryOperation({ direction: "restore", resumeHistory: true, legacyMode: false }),
|
|
620
|
+
});
|
|
449
621
|
if (transitionReceipt) {
|
|
450
622
|
resolveCodexHistoryTransition(transitionReceipt, outcome);
|
|
451
623
|
}
|
|
452
|
-
const history: CodexRestoreHistoryResult =
|
|
624
|
+
const history: CodexRestoreHistoryResult = historyStoodDown
|
|
625
|
+
? {
|
|
626
|
+
state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0,
|
|
627
|
+
message: `Codex resume history was left to Codex's native writer (${HISTORY_RELABEL_STANDS_DOWN});`
|
|
628
|
+
+ " existing threads keep the provider they are tagged with and no rollout byte was read or written.",
|
|
629
|
+
}
|
|
630
|
+
: outcome.kind === "converged"
|
|
453
631
|
? {
|
|
454
632
|
state: "ok", changed: outcome.rows > 0 || outcome.files > 0, rows: outcome.rows, files: outcome.files, ejectedRows: 0,
|
|
455
633
|
message: outcome.rows > 0
|
|
@@ -473,14 +651,24 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
473
651
|
: config.message;
|
|
474
652
|
const success = catalog.state !== "failed"
|
|
475
653
|
&& history.state !== "failed";
|
|
654
|
+
// A stood-down relabel is not a failure, but it is something the operator has to be told:
|
|
655
|
+
// their existing conversations keep the provider they are tagged with, and nothing will
|
|
656
|
+
// ever change that from this side. Printing only the config half would be the same
|
|
657
|
+
// partial-success-reported-as-success problem this change exists to end.
|
|
658
|
+
const historyNote = history.state === "failed"
|
|
659
|
+
? ` ⚠️ ${history.message}`
|
|
660
|
+
: historyStoodDown ? ` ${history.message}` : "";
|
|
476
661
|
return {
|
|
477
662
|
success,
|
|
478
|
-
message: `${base}${
|
|
663
|
+
message: `${base}${historyNote}`,
|
|
664
|
+
...(config.retained ? { retainedCodexProviderTable: config.retained } : {}),
|
|
479
665
|
artifacts: { config, catalog, history },
|
|
480
666
|
};
|
|
481
667
|
}
|
|
482
668
|
|
|
483
|
-
export function restoreNativeCodex(
|
|
669
|
+
export function restoreNativeCodex(
|
|
670
|
+
options: { skipHistory?: boolean; revalidateDesiredState?: boolean; removeProviderTable?: boolean } = {},
|
|
671
|
+
): CodexNativeRestoreResult {
|
|
484
672
|
const activeProvider = currentExternalCodexModelProvider();
|
|
485
673
|
if (activeProvider) {
|
|
486
674
|
removeJournal();
|
|
@@ -489,14 +677,18 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
|
|
|
489
677
|
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
490
678
|
return desiredEnabledRestoreSkip();
|
|
491
679
|
}
|
|
492
|
-
const
|
|
493
|
-
if (
|
|
680
|
+
const disposition = resolveRestoreHistoryDisposition(options.removeProviderTable);
|
|
681
|
+
if (disposition.kind === "refuse") return historyPreflightRefusalEnvelope(disposition.reason);
|
|
682
|
+
const historyStandsDown = disposition.kind === "stand-down";
|
|
494
683
|
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
495
684
|
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
496
685
|
// catalog we actually wrote (#1798).
|
|
497
686
|
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
498
|
-
const config = restoreCodexConfigInline();
|
|
687
|
+
const config = restoreCodexConfigInline("sync", options);
|
|
499
688
|
if (config.state === "failed") return failedConfigRestoreEnvelope(config);
|
|
689
|
+
// Same mid-transaction pagination re-check as the async path.
|
|
690
|
+
const historyStoodDown = historyStandsDown
|
|
691
|
+
|| resolveRestoreHistoryDisposition(options.removeProviderTable).kind === "stand-down";
|
|
500
692
|
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
501
693
|
// Design B (loopback) steady state: threads are already tagged openai, so prove the
|
|
502
694
|
// no-op with a readonly probe instead of write-opening a DB the Codex app may hold
|
|
@@ -510,12 +702,18 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
|
|
|
510
702
|
}
|
|
511
703
|
// `skipHistory` is how the async wrapper takes this work for itself: the
|
|
512
704
|
// native files come down here, and history runs in the Worker under H.
|
|
513
|
-
const rawHistory = options.skipHistory
|
|
705
|
+
const rawHistory = options.skipHistory || historyStoodDown
|
|
514
706
|
? { rows: 0, files: 0 }
|
|
515
707
|
: syncCodexHistoryProvider("openai", undefined, undefined, {
|
|
516
708
|
skipWhenProvablyNoop,
|
|
517
709
|
});
|
|
518
|
-
const history: CodexRestoreHistoryResult =
|
|
710
|
+
const history: CodexRestoreHistoryResult = historyStoodDown
|
|
711
|
+
? {
|
|
712
|
+
state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0,
|
|
713
|
+
message: `Codex resume history was left to Codex's native writer (${HISTORY_RELABEL_STANDS_DOWN});`
|
|
714
|
+
+ " existing threads keep the provider they are tagged with and no rollout byte was read or written.",
|
|
715
|
+
}
|
|
716
|
+
: options.skipHistory
|
|
519
717
|
? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "History restoration runs asynchronously." }
|
|
520
718
|
: rawHistory.failed
|
|
521
719
|
? failedHistoryRestore(rawHistory.failureReason, undefined, rawHistory)
|
|
@@ -534,7 +732,8 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
|
|
|
534
732
|
: config.message;
|
|
535
733
|
return {
|
|
536
734
|
success: catalog.state !== "failed" && history.state !== "failed",
|
|
537
|
-
message,
|
|
735
|
+
message: historyStoodDown ? `${message} ${history.message}` : message,
|
|
736
|
+
...(config.retained ? { retainedCodexProviderTable: config.retained } : {}),
|
|
538
737
|
artifacts: { config, catalog, history },
|
|
539
738
|
};
|
|
540
739
|
}
|
package/src/codex/inject.ts
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
removeJournal,
|
|
41
41
|
writeJournal,
|
|
42
42
|
} from "./journal";
|
|
43
|
-
import { preflightCodexHistoryInjection } from "./history-provider";
|
|
43
|
+
import { HISTORY_RELABEL_STANDS_DOWN, preflightCodexHistoryInjection } from "./history-provider";
|
|
44
44
|
import {
|
|
45
45
|
describeHistoryJobFailure,
|
|
46
46
|
deriveCodexHistoryOperation,
|
|
@@ -158,17 +158,13 @@ export interface CodexInjectResult {
|
|
|
158
158
|
*/
|
|
159
159
|
historyPreflightFailureReason?: string;
|
|
160
160
|
status?: "skipped";
|
|
161
|
+
/** Busy write lock, emitted by `codexInjectLockOutcome` and undeclared here until #4809. */
|
|
162
|
+
retryable?: boolean;
|
|
161
163
|
/** `hub-gated` is the hub-role gate (#4236), distinct from the user's own OFF switch. */
|
|
162
164
|
skippedReason?: "desired_disabled" | "desired_enabled" | "hub-gated";
|
|
163
165
|
nativeSubagentDefaultsWarning?: string;
|
|
164
166
|
}
|
|
165
167
|
|
|
166
|
-
/**
|
|
167
|
-
* The one history preflight reason that is permanent rather than operational: Codex owns
|
|
168
|
-
* paginated rollout ordinals, so no retry makes the legacy relabel protocol available again.
|
|
169
|
-
*/
|
|
170
|
-
const HISTORY_RELABEL_STANDS_DOWN = "history_paginated_requires_native_writer";
|
|
171
|
-
|
|
172
168
|
class CodexHistoryPreflightRefusal extends Error {}
|
|
173
169
|
let historyArtifactStageForTests: ((stage: string) => void) | undefined;
|
|
174
170
|
export function setHistoryArtifactStageForTests(hook: typeof historyArtifactStageForTests): void {
|
|
@@ -465,9 +461,9 @@ async function injectCodexConfigImpl(
|
|
|
465
461
|
*/
|
|
466
462
|
/*
|
|
467
463
|
* Re-observed inside the artifact transaction. A store that migrates to paginated history
|
|
468
|
-
* mid-write
|
|
469
|
-
*
|
|
470
|
-
*
|
|
464
|
+
* mid-write can retire the relabel unit while its already-admitted candidate leaves
|
|
465
|
+
* existing provider references resolvable. Existing provider definitions are retained
|
|
466
|
+
* before the witness; no post-commit compensation may overwrite a newer native write.
|
|
471
467
|
*/
|
|
472
468
|
const observeHistoryRefusalOrThrow = (known: string | null): string | null => {
|
|
473
469
|
if (known) return known;
|
|
@@ -489,12 +485,12 @@ async function injectCodexConfigImpl(
|
|
|
489
485
|
|
|
490
486
|
/*
|
|
491
487
|
* Rows this home may have tagged `opencodex` resolve only through a provider table. Design B
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
488
|
+
* selects built-in `openai` for new work, but background relabel and native publication are
|
|
489
|
+
* not atomic. Codex can paginate after the final check or when the worker starts. Retain
|
|
490
|
+
* an existing definition BEFORE the witness regardless of preflight, so worker failure
|
|
491
|
+
* cannot orphan old references. Explicit restoration keeps its removal and history guards.
|
|
496
492
|
*/
|
|
497
|
-
if (
|
|
493
|
+
if (hadOcxProviderTableOnDisk && !providerTableMode) {
|
|
498
494
|
content = applyEol(
|
|
499
495
|
content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {})),
|
|
500
496
|
eol,
|
|
@@ -787,6 +783,7 @@ async function injectCodexConfigImpl(
|
|
|
787
783
|
// handed down fixed; the Worker never takes a direction from its caller.
|
|
788
784
|
// A stood-down relabel unit spawns no Worker: the preflight it would run first has
|
|
789
785
|
// already refused, and the config half is committed either way.
|
|
786
|
+
historyArtifactStageForTests?.("before-history-worker");
|
|
790
787
|
const historyOutcome: CodexHistoryJobOutcome = historyRelabelRefusal
|
|
791
788
|
? { kind: "skipped" }
|
|
792
789
|
: await runCodexHistoryJob({
|
|
@@ -984,4 +981,3 @@ export {
|
|
|
984
981
|
setBeforeRestoreConfigForTests,
|
|
985
982
|
skippedRestoreEnvelope,
|
|
986
983
|
} from "./inject/restore";
|
|
987
|
-
|
|
@@ -97,3 +97,12 @@ export function isEffectiveCodexDesktopAuthless(
|
|
|
97
97
|
&& config.runtimeRole !== "client"
|
|
98
98
|
&& !shouldInjectApiAuthHeader(config);
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
/** Keep reporting aligned with the admission-token gate used by standalone injection. */
|
|
102
|
+
export function isEffectiveCodexClientCompaction(
|
|
103
|
+
config: Pick<OcxConfig, "runtimeRole" | "hostname" | "unauthenticatedLoopbackListener" | "codexClientCompaction"> | undefined,
|
|
104
|
+
): boolean {
|
|
105
|
+
return config?.codexClientCompaction === true
|
|
106
|
+
&& config.runtimeRole !== "client"
|
|
107
|
+
&& !shouldInjectApiAuthHeader(config);
|
|
108
|
+
}
|