@bitkyc08/opencodex 2.81.0 → 2.82.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 +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-BqHF5Trd.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-D3FBCNiP.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DTBC8n7-.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +43 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +303 -171
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +6 -2
- package/src/oauth/store.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +1 -0
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +31 -2
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +34 -13
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +19 -2
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +25 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +5 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config-write-lock.ts — one advisory lock for every opencodex-originated
|
|
3
|
+
* `config.toml` write.
|
|
4
|
+
*
|
|
5
|
+
* Why a second lock next to the existing two: the injector's SQLite lock N
|
|
6
|
+
* serializes injection against injection, and the prompt-layers lock serializes
|
|
7
|
+
* prompt edits against prompt edits. Neither covers the writers BETWEEN them —
|
|
8
|
+
* the feature scalars in `features.ts`, the `ocx restore`/`removeCodexConfig`
|
|
9
|
+
* transforms, and the journal replay each did their own unlocked read → edit →
|
|
10
|
+
* `atomicWriteFile`. Each write is individually atomic, but a foreign rewrite
|
|
11
|
+
* landing between any writer's read and its rename was silently discarded:
|
|
12
|
+
* a features edit could drop an injection's freshly-written routing keys, a
|
|
13
|
+
* remove could drop a prompt projection committed a millisecond earlier.
|
|
14
|
+
*
|
|
15
|
+
* This lock closes that class at its source rather than per call site: every
|
|
16
|
+
* opencodex writer reads `config.toml` and renames over it while holding
|
|
17
|
+
* `<config>.ocx-write.lock`, so no cooperating writer can be computed against
|
|
18
|
+
* stale bytes and none can land mid-section.
|
|
19
|
+
*
|
|
20
|
+
* What it deliberately does NOT cover:
|
|
21
|
+
* - Codex itself knows nothing about the file; upstream's own rewrites cannot
|
|
22
|
+
* be serialized from here. That residual is why the injector's witness and
|
|
23
|
+
* the drift healer still exist.
|
|
24
|
+
* - Lock ordering: prompt-layers commit() takes the prompt store lock first,
|
|
25
|
+
* then this one; the injector takes this one first, then N, then C — and the
|
|
26
|
+
* coordinated restore path follows that same file-first order
|
|
27
|
+
* (`restoreNativeCodexAsyncImpl` acquires this lock before its `withCodexWriteLock`
|
|
28
|
+
* journal replay), so no cross-lock inversion remains. Synchronous
|
|
29
|
+
* acquisition fails fast; asynchronous acquisition has a bounded wait.
|
|
30
|
+
* - The injector's held section contains awaits (`withCodexWriteLock` is
|
|
31
|
+
* async), so the lock must NOT be implicitly reentrant — a same-process
|
|
32
|
+
* writer that slipped inside on a process-global check would interleave
|
|
33
|
+
* mid-section. Reentrancy is explicit instead: a caller that already holds
|
|
34
|
+
* the file passes its handle (`heldConfigWriteLock`) to the writer it
|
|
35
|
+
* calls.
|
|
36
|
+
* - Acquisition is a single attempt for synchronous writers: the section under
|
|
37
|
+
* this lock is a handful of file operations, so a busy result means the
|
|
38
|
+
* caller reports "locked" and the operator retries — the same contract the
|
|
39
|
+
* prompt store lock already exposes. Async callers (the injector) use the
|
|
40
|
+
* bounded wait in `acquireConfigWriteLock`.
|
|
41
|
+
*/
|
|
42
|
+
import { atomicWriteFileStreamed, type AtomicWriteHooks } from "../config/atomic-write";
|
|
43
|
+
import { lstatSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
44
|
+
import { randomUUID } from "node:crypto";
|
|
45
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
46
|
+
import { release, stillHeld, tryAcquire, type AcquireResult, type LockHandle } from "./prompt-lock";
|
|
47
|
+
|
|
48
|
+
interface Destination { canonical: string; inode: string | null }
|
|
49
|
+
const destinations = new WeakMap<LockHandle, Destination>();
|
|
50
|
+
interface ConfigWriteRecovery {
|
|
51
|
+
canonical: string;
|
|
52
|
+
preimage: Buffer | null;
|
|
53
|
+
identity: string | null;
|
|
54
|
+
refreshBeforeSpawn: boolean;
|
|
55
|
+
}
|
|
56
|
+
const recoveries = new WeakMap<LockHandle, ConfigWriteRecovery>();
|
|
57
|
+
export class ConfigWriteParentMissing extends Error {
|
|
58
|
+
constructor(readonly path: string) { super(`Codex configuration parent is missing or unresolved at ${path}`); }
|
|
59
|
+
}
|
|
60
|
+
type ConfigWriteAcquireResult = AcquireResult | { ok: false; error: "missing-parent"; detail: string };
|
|
61
|
+
function destination(path: string): Destination {
|
|
62
|
+
const absolute = resolve(path);
|
|
63
|
+
let canonical: string;
|
|
64
|
+
try { canonical = realpathSync.native(absolute); }
|
|
65
|
+
catch (error) {
|
|
66
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
67
|
+
try { canonical = join(realpathSync.native(dirname(absolute)), basename(absolute)); }
|
|
68
|
+
catch (parentError) {
|
|
69
|
+
if ((parentError as NodeJS.ErrnoException).code === "ENOENT") throw new ConfigWriteParentMissing(dirname(absolute));
|
|
70
|
+
throw parentError;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const stat = statSync(canonical, { bigint: true });
|
|
75
|
+
return { canonical, inode: `${stat.dev}:${stat.ino}` };
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
78
|
+
return { canonical, inode: null };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Symlink aliases share the lock beside their canonical write destination. */
|
|
82
|
+
export function configWriteLockPath(configPath: string): string {
|
|
83
|
+
return `${destination(configPath).canonical}.ocx-write.lock`;
|
|
84
|
+
}
|
|
85
|
+
export const CONFIG_WRITE_LOCKED_MESSAGE =
|
|
86
|
+
"another opencodex process is writing Codex configuration — retry shortly";
|
|
87
|
+
export type ConfigWriteLockOutcome<T> =
|
|
88
|
+
| { ok: true; value: T }
|
|
89
|
+
| Exclude<ConfigWriteAcquireResult, { ok: true }>;
|
|
90
|
+
|
|
91
|
+
function acquire(configPath: string): ConfigWriteAcquireResult {
|
|
92
|
+
let target: Destination;
|
|
93
|
+
try { target = destination(configPath); }
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof ConfigWriteParentMissing) return { ok: false, error: "missing-parent", detail: error.path };
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
const acquired = tryAcquire(`${target.canonical}.ocx-write.lock`);
|
|
99
|
+
if (acquired.ok) destinations.set(acquired.handle, target);
|
|
100
|
+
return acquired;
|
|
101
|
+
}
|
|
102
|
+
export function withConfigWriteLock<T>(configPath: string, run: (handle: LockHandle) => T): ConfigWriteLockOutcome<T> {
|
|
103
|
+
const acquired = acquire(configPath);
|
|
104
|
+
if (!acquired.ok) return acquired;
|
|
105
|
+
try { return { ok: true, value: run(acquired.handle) }; }
|
|
106
|
+
finally { release(acquired.handle); }
|
|
107
|
+
}
|
|
108
|
+
export function configWriteLockHeld(handle: LockHandle): boolean { return stillHeld(handle); }
|
|
109
|
+
export function releaseConfigWriteLock(handle: LockHandle): void { release(handle); }
|
|
110
|
+
export type { LockHandle };
|
|
111
|
+
|
|
112
|
+
export function assertConfigWriteDestination(configPath: string, held: LockHandle): string {
|
|
113
|
+
const current = destination(configPath);
|
|
114
|
+
const expected = destinations.get(held);
|
|
115
|
+
if (!stillHeld(held) || held.path !== `${current.canonical}.ocx-write.lock`
|
|
116
|
+
|| (expected && (current.canonical !== expected.canonical || current.inode !== expected.inode))) {
|
|
117
|
+
throw new Error("Codex configuration destination changed; files and recovery evidence were preserved.");
|
|
118
|
+
}
|
|
119
|
+
if (!expected) destinations.set(held, current);
|
|
120
|
+
return current.canonical;
|
|
121
|
+
}
|
|
122
|
+
/** A writer advances the witness only through its confirmed publication hook. */
|
|
123
|
+
export function publishConfigWrite<T>(configPath: string, held: LockHandle, run: (destination: string, hooks: AtomicWriteHooks) => T): T {
|
|
124
|
+
const canonical = assertConfigWriteDestination(configPath, held);
|
|
125
|
+
return run(canonical, {
|
|
126
|
+
validateBeforeRename: () => { assertConfigWriteDestination(configPath, held); },
|
|
127
|
+
afterRename: () => {
|
|
128
|
+
const recovery = recoveries.get(held);
|
|
129
|
+
if (recovery?.canonical === canonical) recovery.identity = canonicalFileIdentity(canonical);
|
|
130
|
+
const after = destination(configPath);
|
|
131
|
+
if (!stillHeld(held) || after.canonical !== canonical) throw new Error("Codex configuration destination changed; recovery evidence was preserved.");
|
|
132
|
+
destinations.set(held, after);
|
|
133
|
+
const watched = targets.get(held);
|
|
134
|
+
if (watched) for (const [path, expected] of watched) if (expected.canonical === canonical) watched.set(path, after);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** Native children use the acquisition witness, never a fresh home resolution. */
|
|
139
|
+
export class ConfigWriteDestinationChanged extends Error {
|
|
140
|
+
readonly retryable = false;
|
|
141
|
+
constructor(message = "Codex configuration destination changed; files and recovery evidence were preserved.") { super(message); }
|
|
142
|
+
}
|
|
143
|
+
/** Refuse before transition staging: native Codex always writes home/config.toml. */
|
|
144
|
+
export function assertNativeConfigWriteDestination(configPath: string, held: LockHandle): string {
|
|
145
|
+
let canonical: string;
|
|
146
|
+
try { canonical = assertConfigWriteDestination(configPath, held); }
|
|
147
|
+
catch { throw new ConfigWriteDestinationChanged(); }
|
|
148
|
+
const home = realpathSync.native(dirname(canonical));
|
|
149
|
+
if (basename(canonical) !== "config.toml" || canonical !== join(home, "config.toml")) {
|
|
150
|
+
throw new ConfigWriteDestinationChanged("Native feature changes are unavailable when config.toml is a symlink to a differently named file.");
|
|
151
|
+
}
|
|
152
|
+
return canonical;
|
|
153
|
+
}
|
|
154
|
+
function canonicalFileIdentity(canonical: string): string | null {
|
|
155
|
+
const current = destination(canonical);
|
|
156
|
+
if (current.canonical !== canonical) throw new Error("canonical configuration target was redirected");
|
|
157
|
+
let stat;
|
|
158
|
+
try { stat = lstatSync(canonical, { bigint: true }); }
|
|
159
|
+
catch (error) {
|
|
160
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT" && current.inode === null) return null;
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
if (!stat.isFile() || `${stat.dev}:${stat.ino}` !== current.inode) throw new Error("canonical configuration target is not the observed regular file");
|
|
164
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
165
|
+
}
|
|
166
|
+
/** Capture before the first write; nested native children share the same recovery scope. */
|
|
167
|
+
export function withConfigWriteRecovery<T>(
|
|
168
|
+
configPath: string, held: LockHandle, run: () => T,
|
|
169
|
+
options: { refreshBeforeSpawn?: boolean; preimage?: Buffer } = {},
|
|
170
|
+
): T {
|
|
171
|
+
if (recoveries.has(held)) return run();
|
|
172
|
+
let canonical: string;
|
|
173
|
+
try { canonical = assertConfigWriteDestination(configPath, held); }
|
|
174
|
+
catch { throw new ConfigWriteDestinationChanged(); }
|
|
175
|
+
const identity = canonicalFileIdentity(canonical);
|
|
176
|
+
const preimage = identity === null ? null : readFileSync(canonical);
|
|
177
|
+
if (canonicalFileIdentity(canonical) !== identity) throw new ConfigWriteDestinationChanged();
|
|
178
|
+
const recovery: ConfigWriteRecovery = {
|
|
179
|
+
canonical, identity, preimage: options.preimage ?? preimage,
|
|
180
|
+
refreshBeforeSpawn: options.refreshBeforeSpawn === true && options.preimage === undefined,
|
|
181
|
+
};
|
|
182
|
+
recoveries.set(held, recovery);
|
|
183
|
+
try {
|
|
184
|
+
const value = run();
|
|
185
|
+
assertConfigWriteDestination(configPath, held);
|
|
186
|
+
return value;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
let changed = error instanceof ConfigWriteDestinationChanged;
|
|
189
|
+
try { assertConfigWriteDestination(configPath, held); } catch { changed = true; }
|
|
190
|
+
try {
|
|
191
|
+
const validateRecovery = (targetPath = canonical) => {
|
|
192
|
+
if (targetPath !== canonical || !stillHeld(held) || held.path !== `${canonical}.ocx-write.lock`
|
|
193
|
+
|| canonicalFileIdentity(canonical) !== recovery.identity) {
|
|
194
|
+
throw new Error("canonical configuration target changed before recovery");
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
validateRecovery();
|
|
198
|
+
// A pre-write refusal leaves the original entry intact, including its inode.
|
|
199
|
+
const unchanged = recovery.identity === identity && (recovery.preimage === preimage
|
|
200
|
+
|| (recovery.preimage !== null && preimage !== null && recovery.preimage.equals(preimage)));
|
|
201
|
+
if (!unchanged) {
|
|
202
|
+
if (recovery.preimage === null) { validateRecovery(); if (recovery.identity !== null) unlinkSync(canonical); }
|
|
203
|
+
else atomicWriteFileStreamed(canonical, fd => writeFileSync(fd, recovery.preimage!), { validateBeforeRename: validateRecovery });
|
|
204
|
+
}
|
|
205
|
+
destinations.set(held, destination(canonical));
|
|
206
|
+
} catch (recoveryError) {
|
|
207
|
+
let evidence = "recovery evidence could not be persisted";
|
|
208
|
+
try {
|
|
209
|
+
const recoveryPath = `${canonical}.ocx-native-preimage.${randomUUID()}`;
|
|
210
|
+
atomicWriteFileStreamed(recoveryPath, fd => writeFileSync(fd, recovery.preimage ?? Buffer.from('{"preimage":"absent"}\n')));
|
|
211
|
+
evidence = `recovery evidence retained at ${recoveryPath}`;
|
|
212
|
+
} catch { /* Preserve the refusal and diagnostic even when evidence storage fails. */ }
|
|
213
|
+
throw new ConfigWriteDestinationChanged(`Codex configuration destination changed; canonical preimage recovery refused: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}; ${evidence}.`);
|
|
214
|
+
}
|
|
215
|
+
if (changed) throw new ConfigWriteDestinationChanged(`${error instanceof Error ? error.message : String(error)}; canonical preimage restored and recovery evidence preserved.`);
|
|
216
|
+
throw error;
|
|
217
|
+
} finally { recoveries.delete(held); }
|
|
218
|
+
}
|
|
219
|
+
export function runConfigWriteChild(
|
|
220
|
+
configPath: string, held: LockHandle,
|
|
221
|
+
run: (env: NodeJS.ProcessEnv, validateBeforeSpawn: () => void) => void,
|
|
222
|
+
transitionPreimage?: Buffer,
|
|
223
|
+
): void {
|
|
224
|
+
withConfigWriteRecovery(configPath, held, () => {
|
|
225
|
+
const canonical = assertNativeConfigWriteDestination(configPath, held);
|
|
226
|
+
const recovery = recoveries.get(held)!;
|
|
227
|
+
let readyToSpawn = false;
|
|
228
|
+
const validateBeforeSpawn = () => {
|
|
229
|
+
readyToSpawn = false;
|
|
230
|
+
assertNativeConfigWriteDestination(configPath, held);
|
|
231
|
+
const identity = canonicalFileIdentity(canonical);
|
|
232
|
+
const bytes = identity === null ? null : readFileSync(canonical);
|
|
233
|
+
if (canonicalFileIdentity(canonical) !== identity) throw new ConfigWriteDestinationChanged();
|
|
234
|
+
if (recovery.refreshBeforeSpawn) { recovery.preimage = bytes; recovery.identity = identity; }
|
|
235
|
+
readyToSpawn = true;
|
|
236
|
+
};
|
|
237
|
+
publishConfigWrite(configPath, held, (_canonical, hooks) => {
|
|
238
|
+
const env = { ...process.env };
|
|
239
|
+
// Windows env names are case-insensitive; wrappers may honor Orca's override.
|
|
240
|
+
for (const key of Object.keys(env)) {
|
|
241
|
+
if (["CODEX_HOME", "ORCA_CODEX_HOME"].includes(key.toUpperCase())) delete env[key];
|
|
242
|
+
}
|
|
243
|
+
env.CODEX_HOME = realpathSync.native(dirname(canonical));
|
|
244
|
+
validateBeforeSpawn();
|
|
245
|
+
try { run(env, validateBeforeSpawn); }
|
|
246
|
+
// Even a failed child can replace config.toml. Observe only after it entered.
|
|
247
|
+
finally {
|
|
248
|
+
try { if (readyToSpawn) hooks.afterRename?.(canonical); }
|
|
249
|
+
catch { throw new ConfigWriteDestinationChanged(); }
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}, { refreshBeforeSpawn: true, preimage: transitionPreimage });
|
|
253
|
+
}
|
|
254
|
+
export function withConfigWriteLockHeld<T>(
|
|
255
|
+
configPath: string, held: LockHandle | undefined, run: (handle: LockHandle) => T,
|
|
256
|
+
): ConfigWriteLockOutcome<T> {
|
|
257
|
+
if (held !== undefined) {
|
|
258
|
+
try { assertConfigWriteDestination(configPath, held); }
|
|
259
|
+
catch { return { ok: false, error: "unsafe", detail: held.path }; }
|
|
260
|
+
return { ok: true, value: run(held) };
|
|
261
|
+
}
|
|
262
|
+
return withConfigWriteLock(configPath, run);
|
|
263
|
+
}
|
|
264
|
+
const RETRY_MIN_MS = 25;
|
|
265
|
+
const RETRY_MAX_MS = 75;
|
|
266
|
+
export const CONFIG_WRITE_LOCK_WAIT_MS = 2_000;
|
|
267
|
+
export async function acquireConfigWriteLock(
|
|
268
|
+
configPath: string,
|
|
269
|
+
options: { timeoutMs?: number; nowMs?: () => number; sleep?: (ms: number) => Promise<void> } = {},
|
|
270
|
+
): Promise<ConfigWriteAcquireResult> {
|
|
271
|
+
const timeoutMs = options.timeoutMs ?? CONFIG_WRITE_LOCK_WAIT_MS;
|
|
272
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 10_000) throw new RangeError("Invalid config lock timeout");
|
|
273
|
+
const now = options.nowMs ?? (() => performance.now());
|
|
274
|
+
const sleep = options.sleep ?? (ms => new Promise(done => setTimeout(done, ms)));
|
|
275
|
+
const deadline = now() + timeoutMs;
|
|
276
|
+
for (;;) {
|
|
277
|
+
const acquired = acquire(configPath);
|
|
278
|
+
if (acquired.ok || acquired.error !== "locked") return acquired;
|
|
279
|
+
const remaining = deadline - now();
|
|
280
|
+
if (timeoutMs === 0 || remaining <= 0) return { ok: false, error: "locked" };
|
|
281
|
+
await sleep(Math.max(1, Math.min(remaining,
|
|
282
|
+
RETRY_MIN_MS + Math.floor(Math.random() * (RETRY_MAX_MS - RETRY_MIN_MS + 1)))));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const targets = new WeakMap<LockHandle, Map<string, Destination>>();
|
|
287
|
+
export function watchConfigWriteTargets(held: LockHandle, paths: readonly string[]): void {
|
|
288
|
+
let watched = targets.get(held);
|
|
289
|
+
if (!watched) { watched = new Map(); targets.set(held, watched); }
|
|
290
|
+
for (const path of paths) if (!watched.has(path)) watched.set(path, destination(path));
|
|
291
|
+
}
|
|
292
|
+
export function publishConfigWriteTarget<T>(
|
|
293
|
+
configPath: string, held: LockHandle, path: string, run: (destination: string, hooks: AtomicWriteHooks) => T,
|
|
294
|
+
): T {
|
|
295
|
+
assertConfigWriteDestination(configPath, held);
|
|
296
|
+
watchConfigWriteTargets(held, [path]);
|
|
297
|
+
const watched = targets.get(held)!, expected = watched.get(path)!, current = destination(path);
|
|
298
|
+
if (expected.canonical !== current.canonical || expected.inode !== current.inode) {
|
|
299
|
+
throw new Error("Codex artifact destination changed; files and recovery evidence were preserved.");
|
|
300
|
+
}
|
|
301
|
+
if (path === configPath) {
|
|
302
|
+
const value = publishConfigWrite(configPath, held, run);
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
return run(current.canonical, {
|
|
306
|
+
validateBeforeRename: () => {
|
|
307
|
+
assertConfigWriteDestination(configPath, held);
|
|
308
|
+
const now = destination(path);
|
|
309
|
+
if (expected.canonical !== now.canonical || expected.inode !== now.inode) throw new Error("Codex artifact destination changed; files and recovery evidence were preserved.");
|
|
310
|
+
},
|
|
311
|
+
afterRename: () => {
|
|
312
|
+
assertConfigWriteDestination(configPath, held);
|
|
313
|
+
const after = destination(path);
|
|
314
|
+
if (after.canonical !== current.canonical) throw new Error("Codex artifact destination changed; recovery evidence was preserved.");
|
|
315
|
+
watched.set(path, after);
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function configWriteLockFailureMessage(failure: Exclude<ConfigWriteAcquireResult, { ok: true }>): string {
|
|
321
|
+
if (failure.error === "missing-parent") return `Codex configuration parent is missing or unresolved at ${failure.detail}; no files were changed.`;
|
|
322
|
+
return failure.error === "unsafe"
|
|
323
|
+
? `Unsafe Codex configuration lock at ${failure.detail}; deliberate removal is required. Files and recovery evidence were preserved.`
|
|
324
|
+
: CONFIG_WRITE_LOCKED_MESSAGE;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export class ConfigWriteLockRefusal extends Error {
|
|
328
|
+
readonly retryable: boolean;
|
|
329
|
+
constructor(failure: Exclude<ConfigWriteAcquireResult, { ok: true }>) {
|
|
330
|
+
super(configWriteLockFailureMessage(failure));
|
|
331
|
+
this.retryable = failure.error === "locked";
|
|
332
|
+
}
|
|
333
|
+
}
|
package/src/codex/convergence.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { projectAntigravitySelectedModels } from "../providers/antigravity-effor
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { sameCatalogHealPath } from "./catalog/heal-observation";
|
|
4
4
|
|
|
5
|
-
import { getConfigDir, saveConfigPreservingClaudeCode, websocketsEnabled, withExpectedConfigGenerationSync } from "../config";
|
|
5
|
+
import { getConfigDir, saveConfigPreservingClaudeCode, ultraFastTierEnabled, websocketsEnabled, withExpectedConfigGenerationSync } from "../config";
|
|
6
6
|
import { reconcileSuccessfulModelDiscoveries } from "../providers/new-model-policy";
|
|
7
7
|
import { pendingModelSelectionProviders } from "../providers/initial-model-selection";
|
|
8
8
|
import { COMBO_NAMESPACE } from "../combos";
|
|
@@ -370,6 +370,7 @@ function prepareCatalog(
|
|
|
370
370
|
)),
|
|
371
371
|
);
|
|
372
372
|
const mergedModels = mergeCatalogEntriesFromObservedState({
|
|
373
|
+
ultraFastTier: ultraFastTierEnabled(config),
|
|
373
374
|
modelPickerOrder,
|
|
374
375
|
accountSelectors,
|
|
375
376
|
catalogModels,
|