@bitkyc08/opencodex 2.10.2 → 2.11.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 +31 -0
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/index-Bk-PN-70.css +1 -0
- package/gui/dist/assets/index-BynIEIV-.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -2
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/live-transport.ts +11 -0
- package/src/adapters/cursor/native-exec-fs.ts +9 -6
- package/src/adapters/cursor/native-exec.ts +4 -2
- package/src/adapters/cursor/protobuf-events.ts +176 -4
- package/src/adapters/cursor/request-builder.ts +15 -4
- package/src/adapters/cursor/tool-definitions.ts +118 -2
- package/src/adapters/google.ts +15 -5
- package/src/adapters/openai-chat.ts +24 -2
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +9 -5
- package/src/chat/outbound.ts +4 -3
- package/src/claude/desktop-3p.ts +222 -2
- package/src/claude/outbound.ts +15 -6
- package/src/cli/account-api.ts +4 -0
- package/src/cli/account-extended.ts +112 -0
- package/src/cli/account.ts +23 -6
- package/src/cli/claude-desktop.ts +26 -3
- package/src/cli/config-command.ts +9 -0
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +277 -55
- package/src/cli/models.ts +5 -1
- package/src/cli/provider.ts +8 -2
- package/src/cli/ready.ts +301 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/clients/config-export.ts +12 -19
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/auth-api.ts +83 -0
- package/src/codex/auth-context.ts +5 -2
- package/src/codex/catalog/provider-fetch.ts +11 -0
- package/src/codex/catalog/sync.ts +23 -1
- package/src/codex/codex-write-lock.ts +16 -4
- package/src/codex/desired-state.ts +37 -4
- package/src/codex/history-job.ts +15 -5
- package/src/codex/history-provider.ts +31 -14
- package/src/codex/history-worker.ts +28 -4
- package/src/codex/inject-coordination.ts +13 -1
- package/src/codex/inject.ts +360 -66
- package/src/codex/internal/history-writer.ts +1 -1
- package/src/codex/native-main-lock-file.ts +5 -1
- package/src/codex/native-main-owner.ts +17 -3
- package/src/codex/native-profile-manager.ts +19 -0
- package/src/codex/native-profile-startup.ts +8 -0
- package/src/codex/native-residue.ts +140 -27
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/refresh.ts +7 -0
- package/src/codex/routing.ts +177 -36
- package/src/codex/subagent-model-fallback.ts +34 -4
- package/src/codex/sync.ts +61 -0
- package/src/codex/upstream-host-health.ts +329 -31
- package/src/combos/request.ts +2 -0
- package/src/config.ts +221 -2
- package/src/images/loop.ts +1 -1
- package/src/integrations/native/ownership-preflight.ts +39 -2
- package/src/lib/bun-stream-caps.ts +3 -3
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/windows-secret-acl.ts +141 -39
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +18 -2
- package/src/oauth/key-providers.ts +12 -0
- package/src/providers/derive.ts +54 -2
- package/src/providers/free-directory.ts +6 -5
- package/src/providers/model-discovery.ts +9 -3
- package/src/providers/quota.ts +592 -0
- package/src/providers/registry.ts +316 -13
- package/src/responses/parser.ts +26 -10
- package/src/responses/reasoning-replay-cache.ts +1 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +2 -1
- package/src/server/auth-cors.ts +44 -13
- package/src/server/chat-completions.ts +0 -4
- package/src/server/claude-messages.ts +73 -15
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/index.ts +328 -111
- package/src/server/lifecycle.ts +36 -0
- package/src/server/management/agent-settings-routes.ts +147 -56
- package/src/server/management/config-routes.ts +7 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/native-integration-routes.ts +199 -20
- package/src/server/management/provider-routes.ts +41 -0
- package/src/server/management/routing-profile-routes.ts +234 -5
- package/src/server/management/system-restart.ts +12 -10
- package/src/server/management/system-routes.ts +20 -0
- package/src/server/management-auth.ts +51 -3
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +129 -4
- package/src/server/readiness.ts +99 -0
- package/src/server/relay.ts +113 -97
- package/src/server/request-log.ts +10 -4
- package/src/server/responses/compact.ts +107 -12
- package/src/server/responses/core.ts +220 -39
- package/src/server/responses-item-id-repair.ts +22 -3
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +25 -14
- package/src/server/ws-bridge.ts +27 -22
- package/src/service-manager-probe.ts +520 -10
- package/src/service.ts +134 -2
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +74 -9
- package/src/types.ts +68 -2
- package/src/update/index.ts +12 -0
- package/src/update/job.ts +392 -18
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/usage/log.ts +1 -1
- package/src/vision/index.ts +77 -2
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/parse.ts +4 -1
- package/gui/dist/assets/index-BKVqyYqT.js +0 -70
- package/gui/dist/assets/index-Ca_3269W.css +0 -1
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { loadConfig, mutatePersistedConfig } from "../config";
|
|
24
24
|
import type { OcxClientIntegrationsConfig, OcxConfig } from "../types";
|
|
25
|
+
import { runStartupReadinessSync, type ReadinessGate, type SyncOutcomeLike } from "../server/readiness";
|
|
25
26
|
|
|
26
27
|
/** Clients whose durable intent this module owns. */
|
|
27
28
|
export type DurableIntentClientId = keyof OcxClientIntegrationsConfig;
|
|
@@ -36,8 +37,11 @@ export type DurableIntentClientId = keyof OcxClientIntegrationsConfig;
|
|
|
36
37
|
export interface CodexStartupSyncOutcome {
|
|
37
38
|
catalogWritten: boolean;
|
|
38
39
|
cacheSynced: boolean;
|
|
40
|
+
/** Readiness-observable fields carried by the raw startup sync. */
|
|
41
|
+
ok?: boolean;
|
|
42
|
+
warning?: string;
|
|
39
43
|
}
|
|
40
|
-
export type CodexStartupSync = (port: number) => Promise<
|
|
44
|
+
export type CodexStartupSync = (port: number) => Promise<SyncOutcomeLike | undefined>;
|
|
41
45
|
|
|
42
46
|
export type CodexDesiredStateResult =
|
|
43
47
|
| { readonly ok: true; readonly status: "committed" | "unchanged"; readonly enabled: boolean }
|
|
@@ -66,6 +70,11 @@ export function codexIntegrationEnabled(config: Pick<OcxConfig, "clientIntegrati
|
|
|
66
70
|
return integrationEnabled(config, "codex");
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
/** Whether a Codex sync is permitted for this admitted config snapshot. */
|
|
74
|
+
export function shouldSyncCodexOnStart(config: Pick<OcxConfig, "clientIntegrations">): boolean {
|
|
75
|
+
return codexIntegrationEnabled(config);
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
/**
|
|
70
79
|
* Grok's toggle SHIPPED without this, which is the bug: it strips the fence in
|
|
71
80
|
* `~/.grok/config.toml` and records nothing, so the next `ocx start` calls
|
|
@@ -138,6 +147,20 @@ export function setGrokIntegrationEnabled(enabled: boolean): CodexDesiredStateRe
|
|
|
138
147
|
return setIntegrationEnabled("grok", enabled);
|
|
139
148
|
}
|
|
140
149
|
|
|
150
|
+
/** Whether Claude Desktop's managed gateway profile is wanted. */
|
|
151
|
+
export function claudeDesktopIntegrationEnabled(config: Pick<OcxConfig, "clientIntegrations">): boolean {
|
|
152
|
+
return integrationEnabled(config, "claude-desktop");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The same question when no admitted config snapshot is in hand. */
|
|
156
|
+
export function claudeDesktopIntegrationEnabledNow(): boolean {
|
|
157
|
+
return claudeDesktopIntegrationEnabled(loadConfig());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function setClaudeDesktopIntegrationEnabled(enabled: boolean): CodexDesiredStateResult {
|
|
161
|
+
return setIntegrationEnabled("claude-desktop", enabled);
|
|
162
|
+
}
|
|
163
|
+
|
|
141
164
|
/**
|
|
142
165
|
* The startup gate, as a function rather than an `if` buried in `handleStart`.
|
|
143
166
|
*
|
|
@@ -161,13 +184,23 @@ export async function syncCodexOnStartIfEnabled(
|
|
|
161
184
|
port: number,
|
|
162
185
|
config: Pick<OcxConfig, "clientIntegrations">,
|
|
163
186
|
sync: CodexStartupSync = defaultStartupSync,
|
|
187
|
+
readinessGate?: ReadinessGate,
|
|
164
188
|
): Promise<{ ran: boolean; catalogWritten: boolean; cacheSynced: boolean }> {
|
|
165
|
-
if (!
|
|
189
|
+
if (!shouldSyncCodexOnStart(config)) {
|
|
190
|
+
// The user explicitly turned Codex off: there is nothing to sync, so the
|
|
191
|
+
// proxy is ready as soon as it is up. The gate is driven here so /readyz
|
|
192
|
+
// does not stay pending forever for a deployment that deliberately disabled
|
|
193
|
+
// the native Codex integration.
|
|
194
|
+
readinessGate?.markReady();
|
|
166
195
|
return { ran: false, catalogWritten: false, cacheSynced: false };
|
|
167
196
|
}
|
|
168
197
|
// The `.catch` is deliberate and stays: a failure to APPLY must not stop the
|
|
169
|
-
// proxy from coming up. A failed sync simply reports no writes.
|
|
170
|
-
|
|
198
|
+
// proxy from coming up. A failed sync simply reports no writes. The readiness
|
|
199
|
+
// gate observes the real outcome so /readyz reflects the sync state exactly as
|
|
200
|
+
// the PR contract defines (ready only on ok=true with no warning).
|
|
201
|
+
const outcome = readinessGate
|
|
202
|
+
? await runStartupReadinessSync(readinessGate, async () => (await sync(port)) ?? null)
|
|
203
|
+
: await sync(port).catch(() => undefined);
|
|
171
204
|
return {
|
|
172
205
|
ran: true,
|
|
173
206
|
catalogWritten: outcome?.catalogWritten === true,
|
package/src/codex/history-job.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
HistoryWorkerResult,
|
|
25
25
|
} from "./history-worker";
|
|
26
26
|
import { historyBackupPathFor } from "./history-provider";
|
|
27
|
+
import type { CodexHistoryFailureReason } from "./history-provider";
|
|
27
28
|
import { getCodexHome } from "./paths";
|
|
28
29
|
|
|
29
30
|
/** Where Codex keeps its resume history, and the manifest that shadows it. */
|
|
@@ -63,14 +64,15 @@ export interface CodexHistoryJobRequest {
|
|
|
63
64
|
readonly canonicalStateDbPath: string;
|
|
64
65
|
readonly canonicalBackupPath: string;
|
|
65
66
|
readonly operation: CodexHistoryWorkerOperation;
|
|
67
|
+
readonly expectedDesiredEnabled?: boolean;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
export type CodexHistoryJobOutcome =
|
|
69
71
|
| { readonly kind: "converged"; readonly rows: number; readonly files: number }
|
|
70
72
|
| { readonly kind: "skipped" }
|
|
71
|
-
| { readonly kind: "blocked"; readonly reason: "busy" | "database" | "unsafe-path" }
|
|
73
|
+
| { readonly kind: "blocked"; readonly reason: "busy" | "database" | "unsafe-path" | "desired_disabled" | "desired_enabled" }
|
|
72
74
|
| { readonly kind: "failed"; readonly reason: "worker-error" | "worker-died" | "timeout";
|
|
73
|
-
readonly message: string };
|
|
75
|
+
readonly message: string; readonly historyFailureReason?: CodexHistoryFailureReason };
|
|
74
76
|
|
|
75
77
|
/**
|
|
76
78
|
* Derive the durable history operation from admitted intent.
|
|
@@ -113,9 +115,11 @@ function isPlausibleWorkerResult(
|
|
|
113
115
|
&& typeof message.rows === "number"
|
|
114
116
|
&& typeof message.files === "number";
|
|
115
117
|
case "blocked":
|
|
116
|
-
return message.reason === "busy" || message.reason === "database" || message.reason === "unsafe-path"
|
|
118
|
+
return message.reason === "busy" || message.reason === "database" || message.reason === "unsafe-path"
|
|
119
|
+
|| message.reason === "desired_disabled" || message.reason === "desired_enabled";
|
|
117
120
|
case "error":
|
|
118
|
-
return typeof message.message === "string"
|
|
121
|
+
return typeof message.message === "string"
|
|
122
|
+
&& (message.reason === undefined || message.reason === "busy" || message.reason === "permission");
|
|
119
123
|
default:
|
|
120
124
|
return false;
|
|
121
125
|
}
|
|
@@ -134,7 +138,12 @@ export function deriveCodexHistoryOperation(intent: {
|
|
|
134
138
|
function classifyWorkerResult(result: HistoryWorkerResult): CodexHistoryJobOutcome {
|
|
135
139
|
if (result.type === "blocked") return { kind: "blocked", reason: result.reason };
|
|
136
140
|
if (result.type === "error") {
|
|
137
|
-
return {
|
|
141
|
+
return {
|
|
142
|
+
kind: "failed",
|
|
143
|
+
reason: "worker-error",
|
|
144
|
+
message: result.message,
|
|
145
|
+
...(result.reason ? { historyFailureReason: result.reason } : {}),
|
|
146
|
+
};
|
|
138
147
|
}
|
|
139
148
|
return result.outcome === "skipped"
|
|
140
149
|
? { kind: "skipped" }
|
|
@@ -248,6 +257,7 @@ export async function runCodexHistoryJob(
|
|
|
248
257
|
canonicalCodexHome: request.canonicalCodexHome,
|
|
249
258
|
canonicalStateDbPath: request.canonicalStateDbPath,
|
|
250
259
|
canonicalBackupPath: request.canonicalBackupPath,
|
|
260
|
+
...(request.expectedDesiredEnabled === undefined ? {} : { expectedDesiredEnabled: request.expectedDesiredEnabled }),
|
|
251
261
|
env: {
|
|
252
262
|
...(process.env.CODEX_HOME ? { CODEX_HOME: process.env.CODEX_HOME } : {}),
|
|
253
263
|
...(process.env.OPENCODEX_HOME ? { OPENCODEX_HOME: process.env.OPENCODEX_HOME } : {}),
|
|
@@ -166,12 +166,16 @@ function patchFirstLineProviderInPlace(path: string, expectedId: string, provide
|
|
|
166
166
|
|
|
167
167
|
export type CodexHistoryProvider = "openai" | "opencodex";
|
|
168
168
|
|
|
169
|
+
export type CodexHistoryFailureReason = "busy" | "permission";
|
|
170
|
+
|
|
169
171
|
export interface CodexHistorySyncResult {
|
|
170
172
|
rows: number;
|
|
171
173
|
files: number;
|
|
172
174
|
ejectedRows?: number;
|
|
173
175
|
/** Set when a lock/busy error survived retries and the sync was SKIPPED, not empty. */
|
|
174
176
|
failed?: true;
|
|
177
|
+
/** Why the retry budget was exhausted when `failed` is set. */
|
|
178
|
+
failureReason?: CodexHistoryFailureReason;
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
interface ThreadRow {
|
|
@@ -515,19 +519,24 @@ function ejectRemainingOpencodexHistory(db: Database): { rows: number; files: nu
|
|
|
515
519
|
return { rows: rows.length, files };
|
|
516
520
|
}
|
|
517
521
|
|
|
518
|
-
export function
|
|
522
|
+
export function classifyRecoverableHistoryError(error: unknown): CodexHistoryFailureReason | null {
|
|
519
523
|
const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
520
524
|
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
521
|
-
|
|
525
|
+
if (code === "SQLITE_BUSY"
|
|
522
526
|
|| code === "SQLITE_LOCKED"
|
|
523
527
|
|| code === "EBUSY"
|
|
524
|
-
|| code === "EPERM"
|
|
525
|
-
|| code === "EACCES"
|
|
526
528
|
|| message.includes("database is locked")
|
|
527
529
|
|| message.includes("database is busy")
|
|
528
|
-
|| message.includes("resource busy")
|
|
530
|
+
|| message.includes("resource busy")) return "busy";
|
|
531
|
+
if (code === "EPERM"
|
|
532
|
+
|| code === "EACCES"
|
|
529
533
|
|| message.includes("operation not permitted")
|
|
530
|
-
|| message.includes("permission denied");
|
|
534
|
+
|| message.includes("permission denied")) return "permission";
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function isRecoverableHistoryError(error: unknown): boolean {
|
|
539
|
+
return classifyRecoverableHistoryError(error) !== null;
|
|
531
540
|
}
|
|
532
541
|
|
|
533
542
|
const HISTORY_RETRY_DELAY_MS = 500;
|
|
@@ -540,21 +549,29 @@ const HISTORY_RETRY_ATTEMPTS = 2;
|
|
|
540
549
|
* error — callers surface that as `failed: true` instead of a silent no-op. Hard errors
|
|
541
550
|
* (corruption, programming bugs) still throw.
|
|
542
551
|
*/
|
|
543
|
-
|
|
552
|
+
function withHistoryRetryResult<T>(fn: () => T, io: { sleepFn?: (ms: number) => void; attempts?: number; delayMs?: number } = {}):
|
|
553
|
+
| { ok: true; value: T }
|
|
554
|
+
| { ok: false; reason: CodexHistoryFailureReason } {
|
|
544
555
|
const sleepFn = io.sleepFn ?? Bun.sleepSync;
|
|
545
556
|
const attempts = Math.max(1, io.attempts ?? HISTORY_RETRY_ATTEMPTS);
|
|
546
557
|
const delayMs = io.delayMs ?? HISTORY_RETRY_DELAY_MS;
|
|
547
558
|
for (let attempt = 0; ; attempt++) {
|
|
548
559
|
try {
|
|
549
|
-
return fn();
|
|
560
|
+
return { ok: true, value: fn() };
|
|
550
561
|
} catch (error) {
|
|
551
|
-
|
|
552
|
-
if (
|
|
562
|
+
const reason = classifyRecoverableHistoryError(error);
|
|
563
|
+
if (!reason) throw error;
|
|
564
|
+
if (attempt >= attempts - 1) return { ok: false, reason };
|
|
553
565
|
try { sleepFn(delayMs); } catch { /* sleep is best-effort */ }
|
|
554
566
|
}
|
|
555
567
|
}
|
|
556
568
|
}
|
|
557
569
|
|
|
570
|
+
export function withHistoryRetry<T>(fn: () => T, io: { sleepFn?: (ms: number) => void; attempts?: number; delayMs?: number } = {}): T | null {
|
|
571
|
+
const result = withHistoryRetryResult(fn, io);
|
|
572
|
+
return result.ok ? result.value : null;
|
|
573
|
+
}
|
|
574
|
+
|
|
558
575
|
/**
|
|
559
576
|
* True when a READONLY probe proves the openai-direction restore would be a no-op:
|
|
560
577
|
* zero threads still tagged opencodex AND an empty backup manifest. Used to skip the
|
|
@@ -581,8 +598,8 @@ export function syncCodexHistoryProvider(
|
|
|
581
598
|
&& openaiRestoreIsNoop(stateDbPath, backupPath)) {
|
|
582
599
|
return { rows: 0, files: 0 };
|
|
583
600
|
}
|
|
584
|
-
|
|
585
|
-
|
|
601
|
+
const retried = withHistoryRetryResult(() => syncCodexHistoryProviderUnsafe(provider, stateDbPath, backupPath));
|
|
602
|
+
return retried.ok ? retried.value : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
|
|
586
603
|
}
|
|
587
604
|
|
|
588
605
|
function syncCodexHistoryProviderUnsafe(provider: CodexHistoryProvider, stateDbPath: string, backupPath: string): CodexHistorySyncResult {
|
|
@@ -734,8 +751,8 @@ export function migrateHistoryToOpenai(
|
|
|
734
751
|
// nothing. A missing DB with a leftover backup manifest does NOT satisfy the gate
|
|
735
752
|
// (backupEntries > 0), so the guardian's fresh-reinstall re-count protection holds.
|
|
736
753
|
if (openaiRestoreIsNoop(stateDbPath, backupPath)) return { rows: 0, files: 0 };
|
|
737
|
-
|
|
738
|
-
|
|
754
|
+
const retried = withHistoryRetryResult(() => syncCodexHistoryProviderUnsafe("openai", stateDbPath, backupPath), opts);
|
|
755
|
+
return retried.ok ? retried.value : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
|
|
739
756
|
}
|
|
740
757
|
|
|
741
758
|
export interface PendingHistoryCount {
|
|
@@ -24,11 +24,14 @@
|
|
|
24
24
|
* Design record: devlog/_fin/260804_codex_write_substrate/020_history_isolation.md.
|
|
25
25
|
*/
|
|
26
26
|
import { withHistoryWriteSerialization } from "./history-lock";
|
|
27
|
+
import { loadConfig } from "../config";
|
|
28
|
+
import { shouldSyncCodexOnStart } from "./desired-state";
|
|
27
29
|
import {
|
|
28
30
|
writeHistoryProviderTransition,
|
|
29
31
|
writeLegacyOpenaiHistoryRecovery,
|
|
30
32
|
type HistoryWriteTarget,
|
|
31
33
|
} from "./internal/history-writer";
|
|
34
|
+
import type { CodexHistoryFailureReason } from "./history-provider";
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* The durable operation, mirrored into the request for diagnostics only.
|
|
@@ -53,6 +56,8 @@ export interface HistoryWorkerRunMessage {
|
|
|
53
56
|
readonly canonicalCodexHome: string;
|
|
54
57
|
readonly canonicalStateDbPath: string;
|
|
55
58
|
readonly canonicalBackupPath: string;
|
|
59
|
+
/** When set, prove this transition's desired direction while H is held. */
|
|
60
|
+
readonly expectedDesiredEnabled?: boolean;
|
|
56
61
|
/** Env snapshot: a Worker may not observe parent mutations on every platform. */
|
|
57
62
|
readonly env?: { readonly CODEX_HOME?: string; readonly OPENCODEX_HOME?: string };
|
|
58
63
|
}
|
|
@@ -62,9 +67,9 @@ export type HistoryWorkerResult =
|
|
|
62
67
|
readonly outcome: "converged" | "skipped";
|
|
63
68
|
readonly rows: number; readonly files: number }
|
|
64
69
|
| { readonly type: "blocked"; readonly requestId: string; readonly jobId: string;
|
|
65
|
-
readonly reason: "busy" | "database" | "unsafe-path" }
|
|
70
|
+
readonly reason: "busy" | "database" | "unsafe-path" | "desired_disabled" | "desired_enabled" }
|
|
66
71
|
| { readonly type: "error"; readonly requestId: string; readonly jobId: string;
|
|
67
|
-
readonly message: string };
|
|
72
|
+
readonly message: string; readonly reason?: CodexHistoryFailureReason };
|
|
68
73
|
|
|
69
74
|
const OPERATIONS: ReadonlySet<string> = new Set<CodexHistoryWorkerOperation>([
|
|
70
75
|
"skip",
|
|
@@ -93,7 +98,8 @@ export function isHistoryWorkerRunMessage(data: unknown): data is HistoryWorkerR
|
|
|
93
98
|
&& OPERATIONS.has(message.operation)
|
|
94
99
|
&& nonEmpty(message.canonicalCodexHome)
|
|
95
100
|
&& nonEmpty(message.canonicalStateDbPath)
|
|
96
|
-
&& nonEmpty(message.canonicalBackupPath)
|
|
101
|
+
&& nonEmpty(message.canonicalBackupPath)
|
|
102
|
+
&& (message.expectedDesiredEnabled === undefined || typeof message.expectedDesiredEnabled === "boolean");
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
/**
|
|
@@ -120,6 +126,10 @@ export function runHistoryUnitUnderLock(
|
|
|
120
126
|
message.canonicalCodexHome,
|
|
121
127
|
message.canonicalStateDbPath,
|
|
122
128
|
permit => {
|
|
129
|
+
if (message.expectedDesiredEnabled !== undefined
|
|
130
|
+
&& shouldSyncCodexOnStart(loadConfig()) !== message.expectedDesiredEnabled) {
|
|
131
|
+
return { desiredStateChanged: true as const };
|
|
132
|
+
}
|
|
123
133
|
if (operation === "recover-legacy-openai") {
|
|
124
134
|
return writeLegacyOpenaiHistoryRecovery(permit, target);
|
|
125
135
|
}
|
|
@@ -134,8 +144,22 @@ export function runHistoryUnitUnderLock(
|
|
|
134
144
|
return { type: "blocked", requestId, jobId, reason: acquired.reason };
|
|
135
145
|
}
|
|
136
146
|
const result = acquired.value;
|
|
147
|
+
if ("desiredStateChanged" in result) {
|
|
148
|
+
return {
|
|
149
|
+
type: "blocked",
|
|
150
|
+
requestId,
|
|
151
|
+
jobId,
|
|
152
|
+
reason: message.expectedDesiredEnabled ? "desired_disabled" : "desired_enabled",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
137
155
|
if (result.failed === true) {
|
|
138
|
-
return {
|
|
156
|
+
return {
|
|
157
|
+
type: "error",
|
|
158
|
+
requestId,
|
|
159
|
+
jobId,
|
|
160
|
+
message: "history_transition_failed",
|
|
161
|
+
...(result.failureReason ? { reason: result.failureReason } : {}),
|
|
162
|
+
};
|
|
139
163
|
}
|
|
140
164
|
return {
|
|
141
165
|
type: "done",
|
|
@@ -229,7 +229,19 @@ export function recomputeInjectWitness(options: {
|
|
|
229
229
|
/** Project a non-acquired lock result into the injection result shape. */
|
|
230
230
|
export function codexInjectLockOutcome(
|
|
231
231
|
result: Exclude<CodexWriteLockResult<unknown>, { status: "acquired" }>,
|
|
232
|
-
): { success: false; message: string; retryable: boolean } {
|
|
232
|
+
): { success: false; message: string; retryable: boolean } | {
|
|
233
|
+
success: true; status: "skipped"; skippedReason: "desired_disabled" | "desired_enabled"; message: string;
|
|
234
|
+
} {
|
|
235
|
+
if (result.status === "skipped") {
|
|
236
|
+
return {
|
|
237
|
+
success: true,
|
|
238
|
+
status: "skipped",
|
|
239
|
+
skippedReason: result.reason,
|
|
240
|
+
message: result.reason === "desired_disabled"
|
|
241
|
+
? "Codex integration is OFF; no Codex config, catalog, cache, or history was changed."
|
|
242
|
+
: "Codex integration was re-enabled; native restore was skipped.",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
233
245
|
if (result.status === "busy") {
|
|
234
246
|
return {
|
|
235
247
|
success: false,
|