@bitkyc08/opencodex 2.57.0 → 2.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +2 -2
- package/src/adapters/base.ts +11 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +27 -6
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +141 -0
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +97 -6
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +45 -13
- package/src/adapters/google.ts +23 -4
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +40 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/physical-send.ts +50 -0
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +166 -25
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +46 -5
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +71 -4
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +132 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +72 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +58 -20
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +37 -5
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +6 -9
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +40 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/atomic-write.ts +83 -8
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +23 -0
- package/src/config/schema/leaf-validators.ts +65 -17
- package/src/generated/compatibility-version.json +337 -201
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +146 -28
- package/src/providers/registry/model-seeds.ts +136 -29
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- 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/server/auth-cors.ts +27 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +57 -10
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +72 -3
- 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 +21 -1
- package/src/server/responses/passthrough-dispatch.ts +146 -49
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +70 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +23 -2
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +29 -7
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +104 -3
- package/src/types/provider.ts +32 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-Cz7CLdif.js +0 -128
|
@@ -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 {
|
|
@@ -101,6 +142,14 @@ export interface CodexNativeRestoreResult {
|
|
|
101
142
|
* make a safety decision depend on prose.
|
|
102
143
|
*/
|
|
103
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;
|
|
104
153
|
artifacts: {
|
|
105
154
|
config: CodexRestoreConfigResult;
|
|
106
155
|
catalog: CodexRestoreCatalogResult;
|
|
@@ -243,10 +292,42 @@ function historyPreflightRefusalEnvelope(historyError: string): CodexNativeResto
|
|
|
243
292
|
return result;
|
|
244
293
|
}
|
|
245
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
|
+
|
|
246
327
|
/** The config/profile half of a native restore, reported as one artifact. */
|
|
247
|
-
function restoreCodexConfigInline(kind = "sync"): CodexRestoreConfigResult {
|
|
328
|
+
function restoreCodexConfigInline(kind = "sync", options: RestoreConfigOptions = {}): CodexRestoreConfigResult {
|
|
248
329
|
const preImages = captureCodexPreImages();
|
|
249
|
-
const result = restoreCodexConfigInlineImpl(kind);
|
|
330
|
+
const result = restoreCodexConfigInlineImpl(kind, options);
|
|
250
331
|
if (result.state === "failed") {
|
|
251
332
|
const compensated = restoreCodexPreImages(preImages);
|
|
252
333
|
if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
|
|
@@ -254,11 +335,25 @@ function restoreCodexConfigInline(kind = "sync"): CodexRestoreConfigResult {
|
|
|
254
335
|
return result;
|
|
255
336
|
}
|
|
256
337
|
|
|
257
|
-
function restoreCodexConfigInlineImpl(kind: string): CodexRestoreConfigResult {
|
|
338
|
+
function restoreCodexConfigInlineImpl(kind: string, options: RestoreConfigOptions): CodexRestoreConfigResult {
|
|
258
339
|
try {
|
|
259
340
|
beforeRestoreConfigForTests?.(kind);
|
|
260
|
-
const
|
|
261
|
-
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();
|
|
262
357
|
const journal = restoreJournalState();
|
|
263
358
|
if (journal.unverified) {
|
|
264
359
|
return {
|
|
@@ -267,13 +362,51 @@ function restoreCodexConfigInlineImpl(kind: string): CodexRestoreConfigResult {
|
|
|
267
362
|
};
|
|
268
363
|
}
|
|
269
364
|
const restored = journal.configRestored
|
|
270
|
-
? { success: true, message: "Codex config restored from opencodex journal." }
|
|
271
|
-
: 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;
|
|
272
375
|
if (restored.success) {
|
|
273
376
|
// A successful journal/fallback write can race native history migration too.
|
|
274
377
|
// Refuse here while preimage compensation and the remove transaction can roll back.
|
|
275
|
-
|
|
276
|
-
|
|
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
|
+
};
|
|
277
410
|
}
|
|
278
411
|
return restored.success
|
|
279
412
|
? {
|
|
@@ -336,7 +469,7 @@ function restoreCodexCatalogArtifact(
|
|
|
336
469
|
* that lost race into the discriminated `desired_enabled` skip.
|
|
337
470
|
*/
|
|
338
471
|
export async function restoreNativeCodexAsync(
|
|
339
|
-
options: { revalidateDesiredState?: boolean } = {},
|
|
472
|
+
options: { revalidateDesiredState?: boolean; removeProviderTable?: boolean } = {},
|
|
340
473
|
): Promise<CodexNativeRestoreResult> {
|
|
341
474
|
try {
|
|
342
475
|
return await restoreNativeCodexAsyncImpl(options);
|
|
@@ -347,7 +480,7 @@ export async function restoreNativeCodexAsync(
|
|
|
347
480
|
}
|
|
348
481
|
|
|
349
482
|
async function restoreNativeCodexAsyncImpl(
|
|
350
|
-
options: { revalidateDesiredState?: boolean },
|
|
483
|
+
options: { revalidateDesiredState?: boolean; removeProviderTable?: boolean },
|
|
351
484
|
): Promise<CodexNativeRestoreResult> {
|
|
352
485
|
const activeProvider = currentExternalCodexModelProvider();
|
|
353
486
|
if (activeProvider) {
|
|
@@ -368,8 +501,12 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
368
501
|
if (shouldSyncCodexOnStart(loadConfig())) return desiredEnabledRestoreSkip();
|
|
369
502
|
}
|
|
370
503
|
|
|
371
|
-
const
|
|
372
|
-
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";
|
|
373
510
|
|
|
374
511
|
const eligibility = codexWriteCoordinationEligibility({
|
|
375
512
|
coordinatorPath: () =>
|
|
@@ -420,7 +557,7 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
420
557
|
const preImages = captureCodexPreImages();
|
|
421
558
|
let restored: CodexRestoreConfigResult;
|
|
422
559
|
try {
|
|
423
|
-
restored = restoreCodexConfigInline(eligibility.kind);
|
|
560
|
+
restored = restoreCodexConfigInline(eligibility.kind, options);
|
|
424
561
|
// Throw inside N so the published remove transition rolls back too.
|
|
425
562
|
if (restored.state === "failed") throw new CodexRestoreRefusal(restored);
|
|
426
563
|
} catch (error) {
|
|
@@ -463,20 +600,34 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
463
600
|
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
464
601
|
return desiredEnabledRestoreSkip();
|
|
465
602
|
}
|
|
466
|
-
config = restoreCodexConfigInline(eligibility.kind);
|
|
603
|
+
config = restoreCodexConfigInline(eligibility.kind, options);
|
|
467
604
|
}
|
|
468
605
|
|
|
469
606
|
if (config.state === "failed") return failedConfigRestoreEnvelope(config);
|
|
470
607
|
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
+
});
|
|
476
621
|
if (transitionReceipt) {
|
|
477
622
|
resolveCodexHistoryTransition(transitionReceipt, outcome);
|
|
478
623
|
}
|
|
479
|
-
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"
|
|
480
631
|
? {
|
|
481
632
|
state: "ok", changed: outcome.rows > 0 || outcome.files > 0, rows: outcome.rows, files: outcome.files, ejectedRows: 0,
|
|
482
633
|
message: outcome.rows > 0
|
|
@@ -500,14 +651,24 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
500
651
|
: config.message;
|
|
501
652
|
const success = catalog.state !== "failed"
|
|
502
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}` : "";
|
|
503
661
|
return {
|
|
504
662
|
success,
|
|
505
|
-
message: `${base}${
|
|
663
|
+
message: `${base}${historyNote}`,
|
|
664
|
+
...(config.retained ? { retainedCodexProviderTable: config.retained } : {}),
|
|
506
665
|
artifacts: { config, catalog, history },
|
|
507
666
|
};
|
|
508
667
|
}
|
|
509
668
|
|
|
510
|
-
export function restoreNativeCodex(
|
|
669
|
+
export function restoreNativeCodex(
|
|
670
|
+
options: { skipHistory?: boolean; revalidateDesiredState?: boolean; removeProviderTable?: boolean } = {},
|
|
671
|
+
): CodexNativeRestoreResult {
|
|
511
672
|
const activeProvider = currentExternalCodexModelProvider();
|
|
512
673
|
if (activeProvider) {
|
|
513
674
|
removeJournal();
|
|
@@ -516,14 +677,18 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
|
|
|
516
677
|
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
517
678
|
return desiredEnabledRestoreSkip();
|
|
518
679
|
}
|
|
519
|
-
const
|
|
520
|
-
if (
|
|
680
|
+
const disposition = resolveRestoreHistoryDisposition(options.removeProviderTable);
|
|
681
|
+
if (disposition.kind === "refuse") return historyPreflightRefusalEnvelope(disposition.reason);
|
|
682
|
+
const historyStandsDown = disposition.kind === "stand-down";
|
|
521
683
|
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
522
684
|
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
523
685
|
// catalog we actually wrote (#1798).
|
|
524
686
|
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
525
|
-
const config = restoreCodexConfigInline();
|
|
687
|
+
const config = restoreCodexConfigInline("sync", options);
|
|
526
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";
|
|
527
692
|
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
528
693
|
// Design B (loopback) steady state: threads are already tagged openai, so prove the
|
|
529
694
|
// no-op with a readonly probe instead of write-opening a DB the Codex app may hold
|
|
@@ -537,12 +702,18 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
|
|
|
537
702
|
}
|
|
538
703
|
// `skipHistory` is how the async wrapper takes this work for itself: the
|
|
539
704
|
// native files come down here, and history runs in the Worker under H.
|
|
540
|
-
const rawHistory = options.skipHistory
|
|
705
|
+
const rawHistory = options.skipHistory || historyStoodDown
|
|
541
706
|
? { rows: 0, files: 0 }
|
|
542
707
|
: syncCodexHistoryProvider("openai", undefined, undefined, {
|
|
543
708
|
skipWhenProvablyNoop,
|
|
544
709
|
});
|
|
545
|
-
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
|
|
546
717
|
? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "History restoration runs asynchronously." }
|
|
547
718
|
: rawHistory.failed
|
|
548
719
|
? failedHistoryRestore(rawHistory.failureReason, undefined, rawHistory)
|
|
@@ -561,7 +732,8 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
|
|
|
561
732
|
: config.message;
|
|
562
733
|
return {
|
|
563
734
|
success: catalog.state !== "failed" && history.state !== "failed",
|
|
564
|
-
message,
|
|
735
|
+
message: historyStoodDown ? `${message} ${history.message}` : message,
|
|
736
|
+
...(config.retained ? { retainedCodexProviderTable: config.retained } : {}),
|
|
565
737
|
artifacts: { config, catalog, history },
|
|
566
738
|
};
|
|
567
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 {
|
|
@@ -373,7 +369,7 @@ async function injectCodexConfigImpl(
|
|
|
373
369
|
content =
|
|
374
370
|
content.trimEnd() +
|
|
375
371
|
"\n" +
|
|
376
|
-
buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}));
|
|
372
|
+
buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName);
|
|
377
373
|
// 3) Keep existing `openai`-tagged threads reaching the proxy (see above). Ownership rules
|
|
378
374
|
// are the Design B ones: a user's own root line is never replaced.
|
|
379
375
|
if (keepRootOverrideAlongsideTable) {
|
|
@@ -434,6 +430,7 @@ async function injectCodexConfigImpl(
|
|
|
434
430
|
catalogPath,
|
|
435
431
|
websocketsEnabled(config ?? {}),
|
|
436
432
|
config?.fastMode,
|
|
433
|
+
config?.codexProviderDisplayName,
|
|
437
434
|
);
|
|
438
435
|
content = applyEol(content, eol);
|
|
439
436
|
|
|
@@ -496,7 +493,7 @@ async function injectCodexConfigImpl(
|
|
|
496
493
|
*/
|
|
497
494
|
if (hadOcxProviderTableOnDisk && !providerTableMode) {
|
|
498
495
|
content = applyEol(
|
|
499
|
-
content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {})),
|
|
496
|
+
content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName),
|
|
500
497
|
eol,
|
|
501
498
|
);
|
|
502
499
|
}
|
package/src/codex/lineage.ts
CHANGED
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
*
|
|
15
15
|
* What it is for, and what it is not for:
|
|
16
16
|
*
|
|
17
|
-
* - FIRST PLACEMENT.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* - FIRST PLACEMENT. Largely subsumed by cohort keying (#4780): a member of a tree that any
|
|
18
|
+
* other member has already bound resolves to that same binding, so there is nothing to place.
|
|
19
|
+
* `pickLineageServingAccount` in ./routing remains for the one case cohort keying cannot
|
|
20
|
+
* unify -- a session-less chain whose parent is not recorded in this scope -- and is gated on
|
|
21
|
+
* the parent's key actually differing from this request's.
|
|
20
22
|
* - COST ATTRIBUTION. {@link codexThreadLineageLookup} and {@link codexLineageRootForRequest}
|
|
21
23
|
* answer which root workflow a conversation belongs to, so a grandchild's spend aggregates
|
|
22
24
|
* onto the root. No budget is implemented here.
|
|
@@ -155,30 +157,83 @@ export function codexConversationKeyFor(familyId: string, threadId: string): str
|
|
|
155
157
|
.digest("base64url")}`;
|
|
156
158
|
}
|
|
157
159
|
|
|
160
|
+
/**
|
|
161
|
+
* The cohort anchor's key: the same string on both sides of the derivation.
|
|
162
|
+
*
|
|
163
|
+
* A cohort is identified by one value the whole tree shares, so the key is that value keyed
|
|
164
|
+
* against itself rather than against a member. Using {@link codexConversationKeyFor} keeps one
|
|
165
|
+
* derivation in the module, which is what guarantees a lineage record's `conversationKey` stays
|
|
166
|
+
* byte-identical to the key the thread actually binds under.
|
|
167
|
+
*/
|
|
168
|
+
function cohortKeyFromAnchor(sessionId: string | undefined, fallbackAnchor: string): string {
|
|
169
|
+
const anchor = sessionId ?? fallbackAnchor;
|
|
170
|
+
return codexConversationKeyFor(anchor, anchor);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The cohort this request belongs to, or undefined when it names no cohort at all.
|
|
175
|
+
*
|
|
176
|
+
* The session IS the cohort, which is exactly how upstream keys the prompt cache:
|
|
177
|
+
* `prompt_cache_key()` returns `responses_metadata.session_id` (or `{source}:{parent_thread_id}`
|
|
178
|
+
* for an internal session), and `AgentControl.session_id` is the root thread's id, shared with
|
|
179
|
+
* every sub-agent spawned from that root. Two requests carrying the same `prompt_cache_key` must
|
|
180
|
+
* not be served by different accounts, and keying on the session is what makes that structural
|
|
181
|
+
* rather than a hint (#4780).
|
|
182
|
+
*
|
|
183
|
+
* Without a session there is nothing in the headers that names the tree, so the cohort is
|
|
184
|
+
* whatever the parent is already bound to. That lookup is what keeps a chain of parent-only
|
|
185
|
+
* turns converging on one key: anchoring each depth on its own parent would split the cohort
|
|
186
|
+
* again at every hop. When the parent has not been seen in this scope the parent id anchors it,
|
|
187
|
+
* which is the same key that parent derives for itself.
|
|
188
|
+
*/
|
|
189
|
+
function cohortConversationKey(
|
|
190
|
+
headers: Headers,
|
|
191
|
+
sessionId: string | undefined,
|
|
192
|
+
parentThreadId: string | undefined,
|
|
193
|
+
now: number,
|
|
194
|
+
): string | undefined {
|
|
195
|
+
if (sessionId !== undefined) return cohortKeyFromAnchor(sessionId, sessionId);
|
|
196
|
+
if (parentThreadId === undefined) return undefined;
|
|
197
|
+
const recorded = liveLineageRecord(
|
|
198
|
+
lineageByScope.get(codexLineageScopeKey(headers)),
|
|
199
|
+
parentThreadId,
|
|
200
|
+
now,
|
|
201
|
+
);
|
|
202
|
+
return recorded?.conversationKey ?? cohortKeyFromAnchor(undefined, parentThreadId);
|
|
203
|
+
}
|
|
204
|
+
|
|
158
205
|
/**
|
|
159
206
|
* Resolve a request's conversation identity, or undefined when it carries no bindable thread
|
|
160
207
|
* identity at all.
|
|
161
208
|
*
|
|
162
|
-
* The set of requests that produce NO key is deliberately unchanged
|
|
163
|
-
* bare `thread-id` with neither a session nor a parent stays unbound, exactly as the
|
|
164
|
-
* fallback required both halves of its pair. Only the VALUE moves
|
|
165
|
-
*
|
|
209
|
+
* The set of requests that produce NO key is deliberately unchanged, through both #4546 and
|
|
210
|
+
* #4780: a bare `thread-id` with neither a session nor a parent stays unbound, exactly as the
|
|
211
|
+
* Desktop fallback required both halves of its pair. Only the VALUE moves.
|
|
212
|
+
*
|
|
213
|
+
* Every member of one tree resolves to the SAME key, because the cohort is the binding unit
|
|
214
|
+
* (#4780):
|
|
215
|
+
*
|
|
216
|
+
* - root (`session-id` + `thread-id`) -> the session's cohort key;
|
|
217
|
+
* - child and grandchild (parent + own `thread-id`) -> the same cohort key, since they carry the
|
|
218
|
+
* same session;
|
|
219
|
+
* - parent-only (no `thread-id`) -> the same cohort key, derived from the session or, without
|
|
220
|
+
* one, read from the parent's record.
|
|
166
221
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
222
|
+
* THIS IS NOT A REVERT OF #4546 wp8, and reading it as one would flip it straight back. wp8
|
|
223
|
+
* fixed a real incoherence: a child keyed under the RAW parent id, which is a different identity
|
|
224
|
+
* from the root's own `app:HMAC(session, thread)` binding, so siblings shared an entry unrelated
|
|
225
|
+
* to the root's and a grandchild keying on its own parent landed on a key nobody had ever bound.
|
|
226
|
+
* A cohort key cannot produce that, because the root's own binding IS the cohort key: there is
|
|
227
|
+
* one identity for the tree rather than two competing ones. What wp8 additionally gave each
|
|
228
|
+
* thread -- a binding of its own -- is what #4780 deliberately gives up, and the reason is that
|
|
229
|
+
* upstream never agreed to it: `prompt_cache_key` is keyed on the session the whole tree shares,
|
|
230
|
+
* so a proxy that splits the tree makes every split member assert a warm prefix that is cold on
|
|
231
|
+
* its account.
|
|
173
232
|
*
|
|
174
|
-
*
|
|
175
|
-
* conversation, so it
|
|
176
|
-
*
|
|
177
|
-
* and the
|
|
178
|
-
* exactly why deriving the key looks correct until a caller whose session differs from its thread
|
|
179
|
-
* starts a COLD conversation on every parent-only turn and overwrites the parent's record on the
|
|
180
|
-
* way through. So the recorded key wins, the session-derived key is the fallback that reproduces
|
|
181
|
-
* it when the parent has not been seen in this scope, and the raw parent id is never the answer.
|
|
233
|
+
* The parent-only case keeps the trap it always had. Such a turn belongs to the parent's
|
|
234
|
+
* conversation, so it must land on the binding the parent is already using. With a session in
|
|
235
|
+
* hand that is immediate, since both derive the same cohort key. Without one, the recorded key
|
|
236
|
+
* wins and the parent id anchors the fallback; the raw parent id is never the answer.
|
|
182
237
|
*/
|
|
183
238
|
export function codexConversationIdentity(
|
|
184
239
|
headers: Headers,
|
|
@@ -190,24 +245,20 @@ export function codexConversationIdentity(
|
|
|
190
245
|
|
|
191
246
|
if (threadId === undefined) {
|
|
192
247
|
if (parentThreadId === undefined) return undefined;
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
parentThreadId,
|
|
196
|
-
now,
|
|
197
|
-
);
|
|
248
|
+
const parentOnlyKey = cohortConversationKey(headers, sessionId, parentThreadId, now);
|
|
249
|
+
if (parentOnlyKey === undefined) return undefined;
|
|
198
250
|
return {
|
|
199
|
-
conversationKey:
|
|
200
|
-
?? codexConversationKeyFor(sessionId ?? parentThreadId, parentThreadId),
|
|
251
|
+
conversationKey: parentOnlyKey,
|
|
201
252
|
recordThreadId: parentThreadId,
|
|
202
253
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
203
254
|
legacyConversationKey: parentThreadId,
|
|
204
255
|
declaresParent: false,
|
|
205
256
|
};
|
|
206
257
|
}
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
258
|
+
const conversationKey = cohortConversationKey(headers, sessionId, parentThreadId, now);
|
|
259
|
+
if (conversationKey === undefined) return undefined;
|
|
209
260
|
return {
|
|
210
|
-
conversationKey
|
|
261
|
+
conversationKey,
|
|
211
262
|
recordThreadId: threadId,
|
|
212
263
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
213
264
|
...(parentThreadId !== undefined ? { parentThreadId } : {}),
|
|
@@ -313,7 +364,7 @@ function lineageFor(
|
|
|
313
364
|
const parentConversationKey = parentThreadId === undefined
|
|
314
365
|
? undefined
|
|
315
366
|
: parentRecord?.conversationKey
|
|
316
|
-
??
|
|
367
|
+
?? cohortKeyFromAnchor(identity.sessionId, parentThreadId);
|
|
317
368
|
const rootSessionKey = parentConversationKey === undefined
|
|
318
369
|
? identity.conversationKey
|
|
319
370
|
: parentRecord?.rootSessionKey ?? parentConversationKey;
|
|
@@ -5,6 +5,37 @@ import { NATIVE_RESERVE_MODEL } from "./catalog/native-models";
|
|
|
5
5
|
export const CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE =
|
|
6
6
|
"Luna Reserve compatibility is only available as a conversation model, not a vision helper. Choose another vision model.";
|
|
7
7
|
|
|
8
|
+
export const CODEX_RESERVE_OPT_IN_REQUIRED_MESSAGE =
|
|
9
|
+
"Luna Reserve (gpt-reserve) is not forwarded without the local Desktop authless opt-in."
|
|
10
|
+
+ " OpenCodex holds no Reserve entitlement to send for this request, so the upstream would answer with a"
|
|
11
|
+
+ " usage-limit error that names neither the cause nor the fix."
|
|
12
|
+
+ " Enable the opt-in with 'ocx system settings --desktop-authless on' (or set codexDesktopAuthless to true"
|
|
13
|
+
+ " in config.json), then retry. Choose another model to keep working without it.";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Strict complement of {@link isCodexReserveRequestEligible} for the FLAG reason ONLY (#4940).
|
|
17
|
+
*
|
|
18
|
+
* Read the two together. This answers a narrower question: Reserve was asked for, every ingress
|
|
19
|
+
* condition eligibility requires already holds, and the single missing piece is the operator
|
|
20
|
+
* opt-in. Flipping `codexDesktopAuthless` to true therefore always turns a true here into a true
|
|
21
|
+
* from {@link isCodexReserveRequestEligible}, which is what makes it honest for the refusal to
|
|
22
|
+
* name that one setting. The other two ineligibility reasons are deliberately not covered: a
|
|
23
|
+
* client role and a non-loopback admission source are different situations, and the correct
|
|
24
|
+
* answer for both is still to forward exactly as before.
|
|
25
|
+
*
|
|
26
|
+
* Callers classify the concrete destination as canonical forward before using this predicate, the
|
|
27
|
+
* same obligation {@link isCodexReserveHelperUnsupported} carries. An operator who has aliased or
|
|
28
|
+
* routed `gpt-reserve` onto some other provider owns a path that works, and it must keep working.
|
|
29
|
+
*/
|
|
30
|
+
export function isCodexReserveOptInMissing(
|
|
31
|
+
config: Pick<OcxConfig, "codexDesktopAuthless" | "runtimeRole">,
|
|
32
|
+
modelId: string,
|
|
33
|
+
admission: Pick<DataPlaneAdmission, "source"> | undefined,
|
|
34
|
+
): boolean {
|
|
35
|
+
return modelId === NATIVE_RESERVE_MODEL && config.codexDesktopAuthless !== true
|
|
36
|
+
&& config.runtimeRole !== "client" && admission?.source === "loopback";
|
|
37
|
+
}
|
|
38
|
+
|
|
8
39
|
/** Callers classify the concrete destination as canonical forward before using this predicate. */
|
|
9
40
|
export function isCodexReserveHelperUnsupported(
|
|
10
41
|
config: Pick<OcxConfig, "codexDesktopAuthless" | "runtimeRole">,
|
|
@@ -97,3 +128,12 @@ export function isEffectiveCodexDesktopAuthless(
|
|
|
97
128
|
&& config.runtimeRole !== "client"
|
|
98
129
|
&& !shouldInjectApiAuthHeader(config);
|
|
99
130
|
}
|
|
131
|
+
|
|
132
|
+
/** Keep reporting aligned with the admission-token gate used by standalone injection. */
|
|
133
|
+
export function isEffectiveCodexClientCompaction(
|
|
134
|
+
config: Pick<OcxConfig, "runtimeRole" | "hostname" | "unauthenticatedLoopbackListener" | "codexClientCompaction"> | undefined,
|
|
135
|
+
): boolean {
|
|
136
|
+
return config?.codexClientCompaction === true
|
|
137
|
+
&& config.runtimeRole !== "client"
|
|
138
|
+
&& !shouldInjectApiAuthHeader(config);
|
|
139
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
2
|
import { getMainPolicyQuota } from "./quota";
|
|
3
|
+
import { MAIN_ACCOUNT_HARD_LOCK_PERCENT } from "./quota-types";
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
+
export { MAIN_ACCOUNT_HARD_LOCK_PERCENT };
|
|
5
6
|
|
|
6
7
|
export interface MainAccountHardLockStatus {
|
|
7
8
|
enabled: boolean;
|