@bitkyc08/opencodex 2.8.2-preview.20260731 → 2.9.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 +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-u5eFOv2y.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +86 -5
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +89 -14
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/effort.ts +16 -6
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +8 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro.ts +10 -2
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +239 -97
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +75 -32
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +37 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +173 -53
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +51 -12
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +23 -3
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +45 -18
- package/src/types.ts +34 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
- package/gui/dist/assets/index-GC0Vlu1Z.js +0 -67
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
import type { CleanupMode, CleanupResult } from "./cleanup";
|
|
9
9
|
import { resolveCodexHomeDir } from "../codex/home";
|
|
10
10
|
import {
|
|
11
|
-
endStorageMutation,
|
|
12
11
|
tryBeginStorageMutation,
|
|
13
12
|
} from "./storage-mutation-coordinator";
|
|
13
|
+
import type { AdmissionLease } from "../lib/admission";
|
|
14
14
|
import {
|
|
15
15
|
isPolicyDue,
|
|
16
16
|
readStorageCleanupPolicyFromConfig,
|
|
@@ -19,9 +19,12 @@ import {
|
|
|
19
19
|
type PolicySkipReason,
|
|
20
20
|
} from "./policy";
|
|
21
21
|
import {
|
|
22
|
+
cancelQueuedStorageWorkerSpawns,
|
|
22
23
|
drainStorageWorkers,
|
|
23
|
-
|
|
24
|
+
StorageWorkerAdmissionBusyError,
|
|
24
25
|
terminateStorageWorker,
|
|
26
|
+
tryReserveStorageWorker,
|
|
27
|
+
withStorageWorkerSpawnGate,
|
|
25
28
|
} from "./worker-lifecycle";
|
|
26
29
|
|
|
27
30
|
export type PolicyJobStatus = "idle" | "running";
|
|
@@ -82,12 +85,11 @@ let cancelActiveRun: (() => void) | null = null;
|
|
|
82
85
|
/** Bumped on abort/reset so a late worker completion cannot clobber newer job state. */
|
|
83
86
|
let runGeneration = 0;
|
|
84
87
|
/** CODEX_HOME whose mutation slot this job holds (parent thread only). */
|
|
85
|
-
let
|
|
88
|
+
let heldMutationLease: AdmissionLease | undefined;
|
|
86
89
|
|
|
87
90
|
function releaseHeldMutationSlot(): void {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
heldMutationHome = undefined;
|
|
91
|
+
heldMutationLease?.release();
|
|
92
|
+
heldMutationLease = undefined;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
export function setStorageCleanupPolicyJobLiveApply(
|
|
@@ -129,8 +131,16 @@ function disownActiveRun(): void {
|
|
|
129
131
|
cancel?.();
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Fire-and-forget reset. Prefer {@link resetStorageCleanupPolicyJobForTestsAsync}
|
|
136
|
+
* from test beforeEach/afterEach — sync terminate races Windows
|
|
137
|
+
* `bun test --isolate` reclaim when the next case spawns immediately.
|
|
138
|
+
*/
|
|
132
139
|
export function resetStorageCleanupPolicyJobForTests(): void {
|
|
133
140
|
disownActiveRun();
|
|
141
|
+
// Invalidate spawnGate callbacks that have not created a Worker yet — otherwise
|
|
142
|
+
// they can still spawn after this sync path releases the mutation slot.
|
|
143
|
+
cancelQueuedStorageWorkerSpawns();
|
|
134
144
|
if (activeWorker) {
|
|
135
145
|
void terminateStorageWorker(activeWorker);
|
|
136
146
|
activeWorker = null;
|
|
@@ -148,19 +158,39 @@ export function resetStorageCleanupPolicyJobForTests(): void {
|
|
|
148
158
|
* worker still exiting at that moment trips a Bun-internal assertion on Windows
|
|
149
159
|
* and takes the whole run down, so a suite that spawns workers must be able to
|
|
150
160
|
* wait for them rather than fire-and-forget.
|
|
161
|
+
*
|
|
162
|
+
* Do not route through the sync reset's `void terminate(...)`: that used to
|
|
163
|
+
* deregister the worker before the thread exited, so a follow-up drain saw an
|
|
164
|
+
* empty registry and returned while Windows was still reclaiming the thread.
|
|
151
165
|
*/
|
|
152
166
|
export async function resetStorageCleanupPolicyJobForTestsAsync(): Promise<void> {
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
disownActiveRun();
|
|
168
|
+
cancelQueuedStorageWorkerSpawns();
|
|
169
|
+
const worker = activeWorker;
|
|
170
|
+
activeWorker = null;
|
|
171
|
+
inflight = null;
|
|
172
|
+
testHooks = null;
|
|
173
|
+
state = { status: "idle" };
|
|
174
|
+
// Join the worker before releasing the mutation slot so a concurrent run
|
|
175
|
+
// cannot acquire CODEX_HOME while the aborted thread is still mutating.
|
|
176
|
+
try {
|
|
177
|
+
if (worker) await terminateStorageWorker(worker);
|
|
178
|
+
await drainStorageWorkers();
|
|
179
|
+
} finally {
|
|
180
|
+
releaseHeldMutationSlot();
|
|
181
|
+
}
|
|
155
182
|
}
|
|
156
183
|
|
|
157
184
|
/** Terminate an in-flight worker during process shutdown. */
|
|
158
185
|
export function abortStorageCleanupPolicyJob(): void {
|
|
159
186
|
disownActiveRun();
|
|
187
|
+
cancelQueuedStorageWorkerSpawns();
|
|
160
188
|
if (activeWorker) {
|
|
161
189
|
void terminateStorageWorker(activeWorker);
|
|
162
190
|
activeWorker = null;
|
|
163
191
|
}
|
|
192
|
+
// Sync path cannot join; prefer abortStorageCleanupPolicyJobAsync when the
|
|
193
|
+
// caller can await so the mutation slot stays held until the thread exits.
|
|
164
194
|
releaseHeldMutationSlot();
|
|
165
195
|
if (state.status === "running") {
|
|
166
196
|
state = {
|
|
@@ -177,6 +207,37 @@ export function abortStorageCleanupPolicyJob(): void {
|
|
|
177
207
|
inflight = null;
|
|
178
208
|
}
|
|
179
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Shutdown path that joins worker threads. Prefer this over the sync abort when
|
|
212
|
+
* the caller can await (server drain, test teardown) so Windows isolate reclaim
|
|
213
|
+
* does not race a still-exiting storage worker.
|
|
214
|
+
*/
|
|
215
|
+
export async function abortStorageCleanupPolicyJobAsync(): Promise<void> {
|
|
216
|
+
disownActiveRun();
|
|
217
|
+
cancelQueuedStorageWorkerSpawns();
|
|
218
|
+
const worker = activeWorker;
|
|
219
|
+
activeWorker = null;
|
|
220
|
+
if (state.status === "running") {
|
|
221
|
+
state = {
|
|
222
|
+
...state,
|
|
223
|
+
status: "idle",
|
|
224
|
+
finishedAt: Date.now(),
|
|
225
|
+
lastError: "aborted",
|
|
226
|
+
lastOutcome: {
|
|
227
|
+
ok: false,
|
|
228
|
+
error: "evaluation_failed",
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
inflight = null;
|
|
233
|
+
try {
|
|
234
|
+
if (worker) await terminateStorageWorker(worker);
|
|
235
|
+
await drainStorageWorkers();
|
|
236
|
+
} finally {
|
|
237
|
+
releaseHeldMutationSlot();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
180
241
|
function outcomeFromResult(result: PolicyRunResult): PolicyJobOutcome {
|
|
181
242
|
return {
|
|
182
243
|
ok: result.ok,
|
|
@@ -232,22 +293,22 @@ function applyMutationBusy(): void {
|
|
|
232
293
|
}
|
|
233
294
|
|
|
234
295
|
function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Promise<PolicyRunResult> {
|
|
235
|
-
|
|
296
|
+
const reservation = tryReserveStorageWorker();
|
|
297
|
+
if (!reservation) return Promise.reject(new StorageWorkerAdmissionBusyError());
|
|
298
|
+
return withStorageWorkerSpawnGate(() => new Promise<PolicyRunResult>((resolve, reject) => {
|
|
236
299
|
const requestId = crypto.randomUUID();
|
|
237
300
|
let settled = false;
|
|
238
|
-
|
|
239
|
-
|
|
301
|
+
let worker: Worker;
|
|
302
|
+
try {
|
|
303
|
+
worker = new Worker(new URL("./policy-worker.ts", import.meta.url).href);
|
|
304
|
+
reservation.bind(worker);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
reservation.release();
|
|
307
|
+
reject(error);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
240
310
|
activeWorker = worker;
|
|
241
311
|
|
|
242
|
-
const timer = setTimeout(() => {
|
|
243
|
-
if (settled) return;
|
|
244
|
-
settled = true;
|
|
245
|
-
cancelActiveRun = null;
|
|
246
|
-
void terminateStorageWorker(worker);
|
|
247
|
-
if (activeWorker === worker) activeWorker = null;
|
|
248
|
-
reject(new Error("storage_cleanup_worker_timeout"));
|
|
249
|
-
}, WORKER_TIMEOUT_MS);
|
|
250
|
-
|
|
251
312
|
const finish = (fn: () => void) => {
|
|
252
313
|
if (settled) return;
|
|
253
314
|
settled = true;
|
|
@@ -256,10 +317,15 @@ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Prom
|
|
|
256
317
|
if (activeWorker === worker) activeWorker = null;
|
|
257
318
|
// Settle the caller only after the thread is actually gone, so a suite
|
|
258
319
|
// that awaits its request cannot reach the next test file with a worker
|
|
259
|
-
// still exiting behind it.
|
|
320
|
+
// still exiting behind it. Also keeps executeJob's finally from releasing
|
|
321
|
+
// the mutation slot while the thread may still mutate CODEX_HOME.
|
|
260
322
|
void terminateStorageWorker(worker).then(fn, fn);
|
|
261
323
|
};
|
|
262
324
|
|
|
325
|
+
const timer = setTimeout(() => {
|
|
326
|
+
finish(() => reject(new Error("storage_cleanup_worker_timeout")));
|
|
327
|
+
}, WORKER_TIMEOUT_MS);
|
|
328
|
+
|
|
263
329
|
cancelActiveRun = () => {
|
|
264
330
|
finish(() => reject(new Error("aborted")));
|
|
265
331
|
};
|
|
@@ -296,6 +362,9 @@ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Prom
|
|
|
296
362
|
...(process.env.OPENCODEX_HOME ? { OPENCODEX_HOME: process.env.OPENCODEX_HOME } : {}),
|
|
297
363
|
},
|
|
298
364
|
});
|
|
365
|
+
})).catch(error => {
|
|
366
|
+
reservation.release();
|
|
367
|
+
throw error;
|
|
299
368
|
});
|
|
300
369
|
}
|
|
301
370
|
|
|
@@ -309,7 +378,7 @@ async function executeJob(opts: RequestPolicyRunOptions): Promise<void> {
|
|
|
309
378
|
}
|
|
310
379
|
return;
|
|
311
380
|
}
|
|
312
|
-
|
|
381
|
+
heldMutationLease = gate.lease;
|
|
313
382
|
try {
|
|
314
383
|
const blockMs = testHooks?.blockMs;
|
|
315
384
|
let result: PolicyRunResult;
|
|
@@ -336,7 +405,8 @@ async function executeJob(opts: RequestPolicyRunOptions): Promise<void> {
|
|
|
336
405
|
applyFinished(result);
|
|
337
406
|
} catch (err) {
|
|
338
407
|
if (generation !== runGeneration) return;
|
|
339
|
-
|
|
408
|
+
if (err instanceof StorageWorkerAdmissionBusyError) applyMutationBusy();
|
|
409
|
+
else applyFailed(err instanceof Error ? err.message : "worker_failed");
|
|
340
410
|
} finally {
|
|
341
411
|
releaseHeldMutationSlot();
|
|
342
412
|
}
|
|
@@ -49,5 +49,11 @@ self.onmessage = (event: MessageEvent<unknown>) => {
|
|
|
49
49
|
requestId,
|
|
50
50
|
message: err instanceof Error ? err.message : "worker_failed",
|
|
51
51
|
});
|
|
52
|
+
} finally {
|
|
53
|
+
// Close from inside the worker so the thread begins exiting before the
|
|
54
|
+
// parent’s terminate() races isolate realm reclaim on Windows.
|
|
55
|
+
try {
|
|
56
|
+
(self as unknown as { close?: () => void }).close?.();
|
|
57
|
+
} catch { /* already closing */ }
|
|
52
58
|
}
|
|
53
59
|
};
|
|
@@ -17,9 +17,12 @@ import {
|
|
|
17
17
|
type StorageMutationCoordinatorTestHooks,
|
|
18
18
|
} from "./storage-mutation-coordinator";
|
|
19
19
|
import {
|
|
20
|
+
cancelQueuedStorageWorkerSpawns,
|
|
20
21
|
drainStorageWorkers,
|
|
21
|
-
|
|
22
|
+
StorageWorkerAdmissionBusyError,
|
|
22
23
|
terminateStorageWorker,
|
|
24
|
+
tryReserveStorageWorker,
|
|
25
|
+
withStorageWorkerSpawnGate,
|
|
23
26
|
} from "./worker-lifecycle";
|
|
24
27
|
|
|
25
28
|
export interface RestoreJobTestHooks extends StorageMutationCoordinatorTestHooks {
|
|
@@ -72,7 +75,12 @@ export function setRestoreTrashJobTestHooks(hooks: RestoreJobTestHooks | null):
|
|
|
72
75
|
setStorageMutationCoordinatorTestHooks(hooks);
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Fire-and-forget reset. Prefer {@link resetRestoreTrashJobForTestsAsync} from
|
|
80
|
+
* test beforeEach/afterEach under `bun test --isolate` on Windows.
|
|
81
|
+
*/
|
|
75
82
|
export function resetRestoreTrashJobForTests(): void {
|
|
83
|
+
cancelQueuedStorageWorkerSpawns();
|
|
76
84
|
if (activeWorker) {
|
|
77
85
|
void terminateStorageWorker(activeWorker);
|
|
78
86
|
activeWorker = null;
|
|
@@ -83,14 +91,31 @@ export function resetRestoreTrashJobForTests(): void {
|
|
|
83
91
|
resetStorageMutationCoordinatorForTests();
|
|
84
92
|
}
|
|
85
93
|
|
|
86
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Await-able reset for test teardown; see policy-job's equivalent for why.
|
|
96
|
+
* Awaits the active worker directly so drain cannot race a fire-and-forget
|
|
97
|
+
* deregister from the sync reset.
|
|
98
|
+
*/
|
|
87
99
|
export async function resetRestoreTrashJobForTestsAsync(): Promise<void> {
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
cancelQueuedStorageWorkerSpawns();
|
|
101
|
+
const worker = activeWorker;
|
|
102
|
+
activeWorker = null;
|
|
103
|
+
cancelActiveRun?.();
|
|
104
|
+
cancelActiveRun = null;
|
|
105
|
+
testHooks = null;
|
|
106
|
+
// Join the worker before clearing the mutation coordinator so a concurrent
|
|
107
|
+
// run cannot acquire CODEX_HOME while the aborted thread is still mutating.
|
|
108
|
+
try {
|
|
109
|
+
if (worker) await terminateStorageWorker(worker);
|
|
110
|
+
await drainStorageWorkers();
|
|
111
|
+
} finally {
|
|
112
|
+
resetStorageMutationCoordinatorForTests();
|
|
113
|
+
}
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
/** Terminate an in-flight worker during process shutdown. */
|
|
93
117
|
export function abortRestoreTrashJob(): void {
|
|
118
|
+
cancelQueuedStorageWorkerSpawns();
|
|
94
119
|
if (activeWorker) {
|
|
95
120
|
void terminateStorageWorker(activeWorker);
|
|
96
121
|
activeWorker = null;
|
|
@@ -99,6 +124,17 @@ export function abortRestoreTrashJob(): void {
|
|
|
99
124
|
cancelActiveRun = null;
|
|
100
125
|
}
|
|
101
126
|
|
|
127
|
+
/** Await-able shutdown sibling; joins the restore worker thread before returning. */
|
|
128
|
+
export async function abortRestoreTrashJobAsync(): Promise<void> {
|
|
129
|
+
cancelQueuedStorageWorkerSpawns();
|
|
130
|
+
const worker = activeWorker;
|
|
131
|
+
activeWorker = null;
|
|
132
|
+
cancelActiveRun?.();
|
|
133
|
+
cancelActiveRun = null;
|
|
134
|
+
if (worker) await terminateStorageWorker(worker);
|
|
135
|
+
await drainStorageWorkers();
|
|
136
|
+
}
|
|
137
|
+
|
|
102
138
|
/** Test-only SSE/text stream served from the proxy while a worker is blocked. */
|
|
103
139
|
export function getRestoreTrashTestStreamResponse(): Response | null {
|
|
104
140
|
if (!testHooks?.enableTestStream) return null;
|
|
@@ -124,31 +160,37 @@ function runInWorker(opts: {
|
|
|
124
160
|
blockMs?: number;
|
|
125
161
|
restoreTest?: RestoreTestHooks;
|
|
126
162
|
}): Promise<RestoreResult> {
|
|
127
|
-
|
|
163
|
+
const reservation = tryReserveStorageWorker();
|
|
164
|
+
if (!reservation) return Promise.reject(new StorageWorkerAdmissionBusyError());
|
|
165
|
+
return withStorageWorkerSpawnGate(() => new Promise<RestoreResult>((resolve, reject) => {
|
|
128
166
|
const requestId = crypto.randomUUID();
|
|
129
167
|
let settled = false;
|
|
130
|
-
|
|
131
|
-
|
|
168
|
+
let worker: Worker;
|
|
169
|
+
try {
|
|
170
|
+
worker = new Worker(new URL("./restore-worker.ts", import.meta.url).href);
|
|
171
|
+
reservation.bind(worker);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
reservation.release();
|
|
174
|
+
reject(error);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
132
177
|
activeWorker = worker;
|
|
133
178
|
|
|
134
|
-
const timer = setTimeout(() => {
|
|
135
|
-
if (settled) return;
|
|
136
|
-
settled = true;
|
|
137
|
-
cancelActiveRun = null;
|
|
138
|
-
void terminateStorageWorker(worker);
|
|
139
|
-
if (activeWorker === worker) activeWorker = null;
|
|
140
|
-
reject(new Error("restore_worker_timeout"));
|
|
141
|
-
}, WORKER_TIMEOUT_MS);
|
|
142
|
-
|
|
143
179
|
const finish = (fn: () => void) => {
|
|
144
180
|
if (settled) return;
|
|
145
181
|
settled = true;
|
|
146
182
|
cancelActiveRun = null;
|
|
147
183
|
clearTimeout(timer);
|
|
148
184
|
if (activeWorker === worker) activeWorker = null;
|
|
185
|
+
// Join before settle so mutation coordination inside the restore worker
|
|
186
|
+
// cannot overlap a follow-on run after a watchdog timeout.
|
|
149
187
|
void terminateStorageWorker(worker).then(fn, fn);
|
|
150
188
|
};
|
|
151
189
|
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
finish(() => reject(new Error("restore_worker_timeout")));
|
|
192
|
+
}, WORKER_TIMEOUT_MS);
|
|
193
|
+
|
|
152
194
|
cancelActiveRun = () => {
|
|
153
195
|
finish(() => reject(new Error("aborted")));
|
|
154
196
|
};
|
|
@@ -185,6 +227,9 @@ function runInWorker(opts: {
|
|
|
185
227
|
...(process.env.OPENCODEX_HOME ? { OPENCODEX_HOME: process.env.OPENCODEX_HOME } : {}),
|
|
186
228
|
},
|
|
187
229
|
});
|
|
230
|
+
})).catch(error => {
|
|
231
|
+
reservation.release();
|
|
232
|
+
throw error;
|
|
188
233
|
});
|
|
189
234
|
}
|
|
190
235
|
|
|
@@ -215,6 +260,7 @@ async function executeRestore(opts: {
|
|
|
215
260
|
...(restoreTest ? { restoreTest } : {}),
|
|
216
261
|
});
|
|
217
262
|
} catch (err) {
|
|
263
|
+
if (err instanceof StorageWorkerAdmissionBusyError) return busyRestoreResult();
|
|
218
264
|
return restoreResultFromWorkerRejection(err, opts.trashId);
|
|
219
265
|
}
|
|
220
266
|
}
|
|
@@ -46,6 +46,12 @@ self.onmessage = async (event: MessageEvent<unknown>) => {
|
|
|
46
46
|
requestId,
|
|
47
47
|
message: err instanceof Error ? err.message : "worker_failed",
|
|
48
48
|
});
|
|
49
|
+
} finally {
|
|
50
|
+
// Close from inside the worker so the thread begins exiting before the
|
|
51
|
+
// parent’s terminate() races isolate realm reclaim on Windows.
|
|
52
|
+
try {
|
|
53
|
+
(self as unknown as { close?: () => void }).close?.();
|
|
54
|
+
} catch { /* already closing */ }
|
|
49
55
|
}
|
|
50
56
|
};
|
|
51
57
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
9
|
import { resolveCodexHomeDir } from "../codex/home";
|
|
10
|
+
import { createAdmissionGate, type AdmissionLease, type AdmissionMetrics } from "../lib/admission";
|
|
10
11
|
|
|
11
12
|
export type StorageMutationKind = "cleanup" | "restore" | "policy";
|
|
12
13
|
|
|
@@ -20,9 +21,13 @@ export interface StorageMutationCoordinatorTestHooks {
|
|
|
20
21
|
interface ActiveSlot {
|
|
21
22
|
kind: StorageMutationKind;
|
|
22
23
|
startedAt: number;
|
|
24
|
+
lease: AdmissionLease;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
export const MAX_ACTIVE_STORAGE_HOME_SLOTS = 32;
|
|
25
28
|
const slots = new Map<string, ActiveSlot>();
|
|
29
|
+
const slotGate = createAdmissionGate("storage_home_slots", MAX_ACTIVE_STORAGE_HOME_SLOTS);
|
|
30
|
+
let releaseMisses = 0;
|
|
26
31
|
let testHooks: StorageMutationCoordinatorTestHooks | null = null;
|
|
27
32
|
|
|
28
33
|
function slotKey(codexHome?: string): string {
|
|
@@ -37,9 +42,15 @@ export function setStorageMutationCoordinatorTestHooks(
|
|
|
37
42
|
|
|
38
43
|
export function resetStorageMutationCoordinatorForTests(): void {
|
|
39
44
|
testHooks = null;
|
|
45
|
+
for (const slot of slots.values()) slot.lease.release();
|
|
40
46
|
slots.clear();
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
export function storageMutationAdmissionMetrics(): AdmissionMetrics {
|
|
50
|
+
const metrics = slotGate.metrics();
|
|
51
|
+
return { ...metrics, releaseMisses: metrics.releaseMisses + releaseMisses };
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
export function getActiveStorageMutation(
|
|
44
55
|
codexHome?: string,
|
|
45
56
|
): { kind: StorageMutationKind; startedAt: number } | null {
|
|
@@ -50,17 +61,36 @@ export function getActiveStorageMutation(
|
|
|
50
61
|
export function tryBeginStorageMutation(
|
|
51
62
|
kind: StorageMutationKind,
|
|
52
63
|
codexHome?: string,
|
|
53
|
-
): { acquired: true } | { acquired: false; error: StorageMutationBusyError } {
|
|
64
|
+
): { acquired: true; lease: AdmissionLease } | { acquired: false; error: StorageMutationBusyError } {
|
|
54
65
|
const key = slotKey(codexHome);
|
|
55
66
|
if (slots.has(key)) {
|
|
56
67
|
return { acquired: false, error: "storage_mutation_busy" };
|
|
57
68
|
}
|
|
58
|
-
|
|
59
|
-
return { acquired:
|
|
69
|
+
const lease = slotGate.tryAcquire();
|
|
70
|
+
if (!lease) return { acquired: false, error: "storage_mutation_busy" };
|
|
71
|
+
let active = true;
|
|
72
|
+
const ownerLease: AdmissionLease = {
|
|
73
|
+
release() {
|
|
74
|
+
if (!active) return;
|
|
75
|
+
active = false;
|
|
76
|
+
const owner = slots.get(key);
|
|
77
|
+
if (owner?.lease === ownerLease) slots.delete(key);
|
|
78
|
+
lease.release();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
slots.set(key, { kind, startedAt: Date.now(), lease: ownerLease });
|
|
82
|
+
return { acquired: true, lease: ownerLease };
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
export function endStorageMutation(codexHome?: string): void {
|
|
63
|
-
|
|
86
|
+
const key = slotKey(codexHome);
|
|
87
|
+
const slot = slots.get(key);
|
|
88
|
+
if (!slot) {
|
|
89
|
+
releaseMisses += 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
slots.delete(key);
|
|
93
|
+
slot.lease.release();
|
|
64
94
|
}
|
|
65
95
|
|
|
66
96
|
async function applyCoordinatorBlock(): Promise<void> {
|
|
@@ -87,7 +117,7 @@ export async function runPolicyStorageMutation<T>(
|
|
|
87
117
|
await applyCoordinatorBlock();
|
|
88
118
|
return await work();
|
|
89
119
|
} finally {
|
|
90
|
-
|
|
120
|
+
gate.lease.release();
|
|
91
121
|
}
|
|
92
122
|
}
|
|
93
123
|
|
|
@@ -104,6 +134,6 @@ export async function withStorageMutationSlot<T>(
|
|
|
104
134
|
await applyCoordinatorBlock();
|
|
105
135
|
return await work();
|
|
106
136
|
} finally {
|
|
107
|
-
|
|
137
|
+
gate.lease.release();
|
|
108
138
|
}
|
|
109
139
|
}
|