@yansigit/opencodex 2.36.1-dev.20260829.23 → 2.36.1-dev.20260829.34

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.
Files changed (42) hide show
  1. package/gui/dist/assets/ApiKeys-BthcYAks.js +20 -0
  2. package/gui/dist/assets/Claude-D1uQ1rsr.js +2 -0
  3. package/gui/dist/assets/CodexSet-BUNKAStQ.js +12 -0
  4. package/gui/dist/assets/FileIntegrationPage-CyrJ_kz1.js +1 -0
  5. package/gui/dist/assets/Grok-CeSYaWba.js +1 -0
  6. package/gui/dist/assets/Integrations-BkE9NLJW.js +2 -0
  7. package/gui/dist/assets/IntegrationsOverview-D-nDl5t5.js +3 -0
  8. package/gui/dist/assets/Logs-uExIBMZr.js +1 -0
  9. package/gui/dist/assets/Models-CNpjs-ld.js +1 -0
  10. package/gui/dist/assets/NumberStepper-CKVXkKzE.js +1 -0
  11. package/gui/dist/assets/Providers-D72-5Egl.js +1 -0
  12. package/gui/dist/assets/RestoreDialog-2MVe4MQR.js +1 -0
  13. package/gui/dist/assets/Startup-ClZlpU_Z.js +1 -0
  14. package/gui/dist/assets/Storage-ChC2WGMp.js +1 -0
  15. package/gui/dist/assets/Subagents-BpkXAXLZ.js +1 -0
  16. package/gui/dist/assets/Usage-BkEOitI3.js +1 -0
  17. package/gui/dist/assets/catalog-CerhMn14.js +1 -0
  18. package/gui/dist/assets/collapse-store-DnqZvhx0.js +1 -0
  19. package/gui/dist/assets/data-surface-BzhqUIJ_.js +1 -0
  20. package/gui/dist/assets/data-surface-D8RXShV2.js +1 -0
  21. package/gui/dist/assets/index-DE4QGJSt.js +12 -0
  22. package/gui/dist/assets/index-pLho_LBF.css +1 -0
  23. package/gui/dist/assets/intl-formatters-CTcuZoAe.js +1 -0
  24. package/gui/dist/assets/model-display-CCqxubZd.js +1 -0
  25. package/gui/dist/assets/provider-payload-CI2ayvqv.js +1 -0
  26. package/gui/dist/assets/section-tabs-DEASco68.js +1 -0
  27. package/gui/dist/assets/shared-p0R307IX.js +69 -0
  28. package/gui/dist/index.html +3 -2
  29. package/package.json +1 -1
  30. package/src/adapters/command-code-project-context.ts +24 -19
  31. package/src/codex/catalog/sync.ts +7 -2
  32. package/src/generated/compatibility-version.json +11 -11
  33. package/src/oauth/google-antigravity.ts +18 -5
  34. package/src/storage/cleanup.ts +11 -0
  35. package/src/storage/policy-job.ts +29 -3
  36. package/src/storage/policy-worker.ts +8 -1
  37. package/src/storage/policy.ts +3 -0
  38. package/src/storage/restore-job.ts +5 -0
  39. package/src/storage/restore-worker.ts +7 -1
  40. package/src/storage/storage-mutation-coordinator.ts +6 -0
  41. package/gui/dist/assets/index-DLkXOXLC.css +0 -1
  42. package/gui/dist/assets/index-DMKDnQOg.js +0 -112
@@ -16,8 +16,9 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-DMKDnQOg.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-DLkXOXLC.css">
19
+ <script type="module" crossorigin src="/assets/index-DE4QGJSt.js"></script>
20
+ <link rel="modulepreload" crossorigin href="/assets/shared-p0R307IX.js">
21
+ <link rel="stylesheet" crossorigin href="/assets/index-pLho_LBF.css">
21
22
  </head>
22
23
  <body>
23
24
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yansigit/opencodex",
3
- "version": "2.36.1-dev.20260829.23",
3
+ "version": "2.36.1-dev.20260829.34",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -23,7 +23,12 @@ const MAX_SKILLS = 16;
23
23
  // directories (millions of entries) to a finite scan while preserving alphabetical
24
24
  // selection for any realistic case (≤ MAX_SKILL_DIRS_TO_SCAN valid dirs per root).
25
25
  const MAX_SKILL_DIRS_TO_SCAN = 256;
26
- const FILE_OP_TIMEOUT_MS = 2_000;
26
+ const COMMAND_CODE_FILE_OP_TIMEOUT_MS = 2_000;
27
+ let fileOpTimeoutForTests: number | undefined;
28
+
29
+ export function setCommandCodeFileOpTimeoutForTests(timeoutMs: number | undefined): void {
30
+ fileOpTimeoutForTests = timeoutMs;
31
+ }
27
32
  const PROJECT_CONTEXT_TTL_MS = 30_000;
28
33
  const MAX_PROJECT_CONTEXT_CACHE_ENTRIES = 128;
29
34
 
@@ -112,7 +117,7 @@ function truncateUtf8(text: string, capBytes: number): string {
112
117
  return buf.subarray(0, end).toString("utf8") + TRUNCATION_MARKER;
113
118
  }
114
119
 
115
- async function readUtf8File(path: string, capBytes: number): Promise<string | null> {
120
+ async function readUtf8File(path: string, capBytes: number, timeoutMs = COMMAND_CODE_FILE_OP_TIMEOUT_MS): Promise<string | null> {
116
121
  type FileHandle = Awaited<ReturnType<typeof open>>;
117
122
  let fileHandle: FileHandle | undefined;
118
123
  const closedHandles = new WeakSet<object>();
@@ -141,7 +146,7 @@ async function readUtf8File(path: string, capBytes: number): Promise<string | nu
141
146
  })();
142
147
 
143
148
  try {
144
- return await withTimeout(read, FILE_OP_TIMEOUT_MS);
149
+ return await withTimeout(read, timeoutMs);
145
150
  } catch {
146
151
  if (fileHandle) void closeBestEffort(fileHandle);
147
152
  void opened.then(handle => closeBestEffort(handle), () => undefined);
@@ -178,21 +183,21 @@ function parseSkillFrontmatter(text: string): { name: string | null; body: strin
178
183
  return { name, body };
179
184
  }
180
185
 
181
- async function readMemory(cwd: string, cwdCanonical: string): Promise<string> {
186
+ async function readMemory(cwd: string, cwdCanonical: string, timeoutMs: number): Promise<string> {
182
187
  const path = join(cwd, "AGENTS.md");
183
188
  const canonical = confinedCanonicalPath(path, cwdCanonical);
184
189
  if (!canonical) return "";
185
- const text = await readUtf8File(canonical, MEMORY_CAP_BYTES);
190
+ const text = await readUtf8File(canonical, MEMORY_CAP_BYTES, timeoutMs);
186
191
  if (text === null) return "";
187
192
  return truncateUtf8(text, MEMORY_CAP_BYTES);
188
193
  }
189
194
 
190
- async function readTaste(cwd: string, cwdCanonical: string): Promise<string | null> {
195
+ async function readTaste(cwd: string, cwdCanonical: string, timeoutMs: number): Promise<string | null> {
191
196
  const path = join(cwd, ".commandcode", "taste", "taste.md");
192
197
  const canonical = confinedCanonicalPath(path, cwdCanonical);
193
198
  if (!canonical) return null;
194
199
  if (!existsSync(canonical)) return null;
195
- const text = await readUtf8File(canonical, TASTE_CAP_BYTES);
200
+ const text = await readUtf8File(canonical, TASTE_CAP_BYTES, timeoutMs);
196
201
  if (text === null) return null;
197
202
  return truncateUtf8(text, TASTE_CAP_BYTES);
198
203
  }
@@ -202,7 +207,7 @@ interface SkillEntry {
202
207
  body: string;
203
208
  }
204
209
 
205
- async function listSkillDirs(skillRoot: string, cwdCanonical: string, scanBudget: number): Promise<string[]> {
210
+ async function listSkillDirs(skillRoot: string, cwdCanonical: string, scanBudget: number, timeoutMs: number): Promise<string[]> {
206
211
  if (scanBudget <= 0) return [];
207
212
  const skillRootCanonical = confinedCanonicalPath(skillRoot, cwdCanonical);
208
213
  if (!skillRootCanonical) return [];
@@ -235,7 +240,7 @@ async function listSkillDirs(skillRoot: string, cwdCanonical: string, scanBudget
235
240
  names.sort();
236
241
  return names;
237
242
  })(),
238
- FILE_OP_TIMEOUT_MS,
243
+ timeoutMs,
239
244
  );
240
245
  } catch {
241
246
  if (dir) {
@@ -249,11 +254,11 @@ async function listSkillDirs(skillRoot: string, cwdCanonical: string, scanBudget
249
254
  }
250
255
  }
251
256
 
252
- async function readSkill(skillRoot: string, dirName: string, cwdCanonical: string): Promise<SkillEntry | null> {
257
+ async function readSkill(skillRoot: string, dirName: string, cwdCanonical: string, timeoutMs: number): Promise<SkillEntry | null> {
253
258
  const path = join(skillRoot, dirName, "SKILL.md");
254
259
  const canonical = confinedCanonicalPath(path, cwdCanonical);
255
260
  if (!canonical) return null;
256
- const text = await readUtf8File(canonical, SKILLS_XML_CAP_BYTES);
261
+ const text = await readUtf8File(canonical, SKILLS_XML_CAP_BYTES, timeoutMs);
257
262
  if (text === null) return null;
258
263
  const { name, body } = parseSkillFrontmatter(text);
259
264
  return { name: name ?? dirName, body };
@@ -322,16 +327,16 @@ function truncateUtf8BodyForXml(body: string, capBytes: number): string | null {
322
327
  return best;
323
328
  }
324
329
 
325
- async function readSkills(cwd: string, cwdCanonical: string): Promise<string | null> {
330
+ async function readSkills(cwd: string, cwdCanonical: string, timeoutMs: number): Promise<string | null> {
326
331
  const seen = new Set<string>();
327
332
  const collected: SkillEntry[] = [];
328
333
 
329
334
  for (const rootRel of SKILL_ROOTS) {
330
335
  const skillRoot = join(cwd, ...rootRel.split("/"));
331
- const dirs = await listSkillDirs(skillRoot, cwdCanonical, MAX_SKILL_DIRS_TO_SCAN);
336
+ const dirs = await listSkillDirs(skillRoot, cwdCanonical, MAX_SKILL_DIRS_TO_SCAN, timeoutMs);
332
337
  for (const dirName of dirs) {
333
338
  if (collected.length >= MAX_SKILLS) break;
334
- const skill = await readSkill(skillRoot, dirName, cwdCanonical);
339
+ const skill = await readSkill(skillRoot, dirName, cwdCanonical, timeoutMs);
335
340
  if (!skill) continue;
336
341
  if (seen.has(skill.name)) continue;
337
342
  seen.add(skill.name);
@@ -343,14 +348,14 @@ async function readSkills(cwd: string, cwdCanonical: string): Promise<string | n
343
348
  return buildSkillsXml(collected);
344
349
  }
345
350
 
346
- async function collectProjectContext(cwd: string): Promise<CommandCodeProjectContext> {
351
+ async function collectProjectContext(cwd: string, timeoutMs: number): Promise<CommandCodeProjectContext> {
347
352
  const cwdCanonical = canonicalPath(cwd);
348
353
  if (!cwdCanonical) return { ...EMPTY_COMMAND_CODE_PROJECT_CONTEXT };
349
354
 
350
355
  const [memory, taste, skills] = await Promise.all([
351
- readMemory(cwd, cwdCanonical),
352
- readTaste(cwd, cwdCanonical),
353
- readSkills(cwd, cwdCanonical),
356
+ readMemory(cwd, cwdCanonical, timeoutMs),
357
+ readTaste(cwd, cwdCanonical, timeoutMs),
358
+ readSkills(cwd, cwdCanonical, timeoutMs),
354
359
  ]);
355
360
 
356
361
  return { memory, taste, skills };
@@ -365,7 +370,7 @@ export async function loadCommandCodeProjectContext(cwd: string | undefined): Pr
365
370
  return cached.value;
366
371
  }
367
372
 
368
- const value = await collectProjectContext(cwd);
373
+ const value = await collectProjectContext(cwd, fileOpTimeoutForTests ?? COMMAND_CODE_FILE_OP_TIMEOUT_MS);
369
374
  const now = Date.now();
370
375
  if (hadCachedEntry) {
371
376
  pruneExpiredProjectContextCache(now);
@@ -1374,6 +1374,12 @@ function currentCatalogFileContent(path: string): Buffer | null {
1374
1374
  }
1375
1375
  }
1376
1376
 
1377
+ /** Internal byte guard shared by sync and its focused no-op-write test. */
1378
+ export function catalogContentChangedForTests(path: string, content: string): boolean {
1379
+ const current = currentCatalogFileContent(path);
1380
+ return current === null || !current.equals(Buffer.from(content, "utf8"));
1381
+ }
1382
+
1377
1383
  function pristineCatalogBytes(read: RetainedCatalogSyncRead): string | null {
1378
1384
  if (read.onDiskCatalog && !catalogHasRoutedEntries(read.onDiskCatalog)) {
1379
1385
  try {
@@ -1735,8 +1741,7 @@ function writeRetainedCatalogSync({
1735
1741
  // nothing about the catalog changed. Skipping the no-op write keeps both the mtime
1736
1742
  // and `catalogWritten` honest; `added` still reports the routed rows the catalog
1737
1743
  // carries, because they are on disk either way.
1738
- const onDiskBytes = currentCatalogFileContent(catalogPath);
1739
- if (onDiskBytes !== null && onDiskBytes.equals(Buffer.from(content, "utf8"))) {
1744
+ if (!catalogContentChangedForTests(catalogPath, content)) {
1740
1745
  return { added, path: catalogPath, catalogWritten: false, comboOmissions };
1741
1746
  }
1742
1747
 
@@ -10,7 +10,7 @@
10
10
  },
11
11
  {
12
12
  "path": "package.json",
13
- "sha256": "816a5398120e0e0088f66606ab33121f3b7566fabf43cf5371872a8c9241ba06"
13
+ "sha256": "3d5021c8d2ed4fd75d71843af16e27288d71580386a8fb5b6f9d2c2ea2851fb6"
14
14
  },
15
15
  {
16
16
  "path": "scripts/model-metadata.source.json",
@@ -54,7 +54,7 @@
54
54
  },
55
55
  {
56
56
  "path": "src/adapters/command-code-project-context.ts",
57
- "sha256": "72bcc19ad2ce2a3f743391d6388c67912e3cf3bfc244dd6b410638c2c34241a5"
57
+ "sha256": "44c4f2ce44231fb19d7068363b6a13f38c8d7fb7ffd7e049f3c47da967abb7ab"
58
58
  },
59
59
  {
60
60
  "path": "src/adapters/command-code.ts",
@@ -814,7 +814,7 @@
814
814
  },
815
815
  {
816
816
  "path": "src/codex/catalog/sync.ts",
817
- "sha256": "ca087dfda6c09914a97aff53a3427e8b56d7e27f9db1e942a435e1fdbaaa4174"
817
+ "sha256": "f03b4c28e4be29f9df3784d1cb58bc83ca8a58e646d91894b9a90fa339508be4"
818
818
  },
819
819
  {
820
820
  "path": "src/codex/codex-write-lock.ts",
@@ -2166,7 +2166,7 @@
2166
2166
  },
2167
2167
  {
2168
2168
  "path": "src/oauth/google-antigravity.ts",
2169
- "sha256": "babefb5a2b6648bf97b47e5cb1ceb52c6c03443625f02195175d6c14b8d83bc0"
2169
+ "sha256": "a5546286e647862c4b6be097495e10ec75ba0026f4c20ab8cb277c1d2697893d"
2170
2170
  },
2171
2171
  {
2172
2172
  "path": "src/oauth/health.ts",
@@ -3074,11 +3074,11 @@
3074
3074
  },
3075
3075
  {
3076
3076
  "path": "src/storage/cleanup.ts",
3077
- "sha256": "57a8a06f9f2a898fa129766124cbbc51596f5c6a650c3ea22b9ba02e139cc20a"
3077
+ "sha256": "2a90e83983c7da9bdb2ac0134870dd7ba85d4469237fae798c60b34985c8e554"
3078
3078
  },
3079
3079
  {
3080
3080
  "path": "src/storage/policy-job.ts",
3081
- "sha256": "25b014d6b188505ab713640bb9ba223807d33c4258d15450a7b3397adec1f4bc"
3081
+ "sha256": "4fa9f2297feb1a9404af6173adbf875c19f5e79f4b1aca0056744e83d503ad8a"
3082
3082
  },
3083
3083
  {
3084
3084
  "path": "src/storage/policy-scheduler.ts",
@@ -3086,19 +3086,19 @@
3086
3086
  },
3087
3087
  {
3088
3088
  "path": "src/storage/policy-worker.ts",
3089
- "sha256": "e4637c162efd2f91cf291d9eecc575f71ca8322afa1868146e4a6f387bbaaee7"
3089
+ "sha256": "c33d74fe319fc9103bd1937a81c8f0b23f319d7f634bea87a691da845ebec282"
3090
3090
  },
3091
3091
  {
3092
3092
  "path": "src/storage/policy.ts",
3093
- "sha256": "64df434d0bc5fb2c9f9ef3cada20c34a439f1bd658a23c251b3b17cdadf4412d"
3093
+ "sha256": "93050c0be848f0df9c7114d903272738fb817b62fb0cdd9d7a59e55b9b267601"
3094
3094
  },
3095
3095
  {
3096
3096
  "path": "src/storage/restore-job.ts",
3097
- "sha256": "b3bd80c88d96cd79a57fb964f58b3134a00d556e56bac01f5d4718515306048e"
3097
+ "sha256": "216a1a1c3610f5d3fbf50952855b792583f851bc989294f986208c93fd9eefaa"
3098
3098
  },
3099
3099
  {
3100
3100
  "path": "src/storage/restore-worker.ts",
3101
- "sha256": "a74ecc8a3ef5d1991da15bcb24a60d43c877ee1bd698e65f0ba86d97750bf96d"
3101
+ "sha256": "4ff17868a3e04fd28db86269c17d2e5600cf6e0222374fc3032854cb0990c766"
3102
3102
  },
3103
3103
  {
3104
3104
  "path": "src/storage/scanner.ts",
@@ -3106,7 +3106,7 @@
3106
3106
  },
3107
3107
  {
3108
3108
  "path": "src/storage/storage-mutation-coordinator.ts",
3109
- "sha256": "b669af88a7c66d1d07b6c4b6cf6111409a07790a74349a22022ffac602832694"
3109
+ "sha256": "65d0560e25f73d6b4bbfb64168cfd37bd796d74781368819207339be254df51b"
3110
3110
  },
3111
3111
  {
3112
3112
  "path": "src/storage/worker-lifecycle.ts",
@@ -37,7 +37,13 @@ const CALLBACK_PATH = "/callback";
37
37
  const REFRESH_SKEW_MS = 5 * 60 * 1000;
38
38
  const REQUEST_TIMEOUT_MS = 30_000;
39
39
  const ONBOARD_ATTEMPTS = 5;
40
- const ONBOARD_POLL_MS = 2_000;
40
+ const ANTIGRAVITY_ONBOARD_POLL_MS = 2_000;
41
+ const defaultSleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
42
+ let sleepForTests: ((ms: number) => Promise<void>) | undefined;
43
+
44
+ export function setAntigravityOnboardSleepForTests(sleep: ((ms: number) => Promise<void>) | undefined): void {
45
+ sleepForTests = sleep;
46
+ }
41
47
 
42
48
  function requestSignal(signal: AbortSignal | undefined): AbortSignal {
43
49
  const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
@@ -136,7 +142,11 @@ async function loadCodeAssistProject(accessToken: string, signal?: AbortSignal):
136
142
  return extractProjectId((await response.json().catch(() => undefined)) as Record<string, unknown> | undefined);
137
143
  }
138
144
 
139
- async function onboardProject(accessToken: string, signal?: AbortSignal): Promise<string | undefined> {
145
+ async function onboardProject(
146
+ accessToken: string,
147
+ signal?: AbortSignal,
148
+ ): Promise<string | undefined> {
149
+ const sleep = sleepForTests ?? defaultSleep;
140
150
  for (let attempt = 0; attempt < ONBOARD_ATTEMPTS; attempt++) {
141
151
  if (signal?.aborted) throw signal.reason ?? new Error("Antigravity onboarding aborted");
142
152
  const response = await fetch(`${DAILY_API}/${API_VERSION}:onboardUser`, {
@@ -153,7 +163,7 @@ async function onboardProject(accessToken: string, signal?: AbortSignal): Promis
153
163
  if (!response.ok) {
154
164
  // Transient (429/5xx): keep polling within the attempt budget. Hard 4xx: give up now.
155
165
  if (response.status === 429 || response.status >= 500) {
156
- await new Promise(resolve => setTimeout(resolve, ONBOARD_POLL_MS));
166
+ await sleep(ANTIGRAVITY_ONBOARD_POLL_MS);
157
167
  continue;
158
168
  }
159
169
  return undefined;
@@ -162,13 +172,16 @@ async function onboardProject(accessToken: string, signal?: AbortSignal): Promis
162
172
  if (data.done === true) {
163
173
  return extractProjectId(data.response as Record<string, unknown> | undefined);
164
174
  }
165
- await new Promise(resolve => setTimeout(resolve, ONBOARD_POLL_MS));
175
+ await sleep(ANTIGRAVITY_ONBOARD_POLL_MS);
166
176
  }
167
177
  return undefined;
168
178
  }
169
179
 
170
180
  /** Discover the CCA project for an access token (loadCodeAssist → onboardUser fallback). */
171
- export async function discoverAntigravityProject(accessToken: string, signal?: AbortSignal): Promise<string | undefined> {
181
+ export async function discoverAntigravityProject(
182
+ accessToken: string,
183
+ signal?: AbortSignal,
184
+ ): Promise<string | undefined> {
172
185
  return (await loadCodeAssistProject(accessToken, signal)) ?? (await onboardProject(accessToken, signal));
173
186
  }
174
187
 
@@ -2578,6 +2578,10 @@ export interface RestoreTestHooks {
2578
2578
  * reconcile, so cleanup can race an in-flight restore.
2579
2579
  */
2580
2580
  holdAfterFileMovesMs?: number;
2581
+ /** Internal test seam: pause after moves until the owner releases it. */
2582
+ waitAfterFileMoves?: SharedArrayBuffer;
2583
+ /** Internal worker-only notification after rollout moves complete. */
2584
+ notifyAfterFileMoves?: boolean;
2581
2585
  }
2582
2586
 
2583
2587
  /**
@@ -2924,6 +2928,13 @@ export function restoreTrashEntry(
2924
2928
  while (Date.now() < deadline) { /* test-only spin wait */ }
2925
2929
  }
2926
2930
  }
2931
+ if (hooks?.waitAfterFileMoves) {
2932
+ if (hooks.notifyAfterFileMoves) {
2933
+ Atomics.store(new Int32Array(hooks.waitAfterFileMoves), 1, 1);
2934
+ Atomics.notify(new Int32Array(hooks.waitAfterFileMoves), 1);
2935
+ }
2936
+ Atomics.wait(new Int32Array(hooks.waitAfterFileMoves), 0, 0);
2937
+ }
2927
2938
 
2928
2939
  if (hooks?.failAfterFileMoves) {
2929
2940
  return abortAfterMoves("fs_failed");
@@ -62,6 +62,14 @@ export interface PolicyJobTestHooks {
62
62
  * policy load, so concurrent PUTs can race completion metadata writes.
63
63
  */
64
64
  blockMs?: number;
65
+ /** Notify race tests once the shared mutation lease is acquired. */
66
+ onAcquired?: () => void;
67
+ /** Notify after the worker has loaded its policy snapshot. */
68
+ onPolicyLoaded?: () => void;
69
+ /** Test-only worker gate after the policy snapshot, before metadata save. */
70
+ waitAfterPolicyLoadedBuffer?: SharedArrayBuffer;
71
+ /** Keep the policy job at the lease boundary until a competing request is issued. */
72
+ waitForRelease?: Promise<void>;
65
73
  /**
66
74
  * When true, run on the main thread via `queueMicrotask` + optional sleep.
67
75
  * Used only for unit tests that cannot spawn workers; responsiveness tests
@@ -292,13 +300,14 @@ function applyMutationBusy(): void {
292
300
  };
293
301
  }
294
302
 
295
- function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Promise<PolicyRunResult> {
303
+ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number; notifyLoaded?: () => void; waitAfterPolicyLoadedBuffer?: SharedArrayBuffer }): Promise<PolicyRunResult> {
296
304
  const reservation = tryReserveStorageWorker();
297
305
  if (!reservation) return Promise.reject(new StorageWorkerAdmissionBusyError());
298
306
  return withStorageWorkerSpawnGate(() => new Promise<PolicyRunResult>((resolve, reject) => {
299
307
  const requestId = crypto.randomUUID();
300
308
  let settled = false;
301
309
  let worker: Worker;
310
+ const loadedRelease = opts.waitAfterPolicyLoadedBuffer;
302
311
  try {
303
312
  worker = new Worker(new URL("./policy-worker.ts", import.meta.url).href);
304
313
  reservation.bind(worker);
@@ -335,6 +344,10 @@ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Prom
335
344
  if (!data || typeof data !== "object") return;
336
345
  const msg = data as Record<string, unknown>;
337
346
  if (msg.requestId !== requestId) return;
347
+ if (msg.type === "loaded") {
348
+ opts.notifyLoaded?.();
349
+ return;
350
+ }
338
351
  if (msg.type === "done" && msg.result && typeof msg.result === "object") {
339
352
  finish(() => resolve(msg.result as PolicyRunResult));
340
353
  return;
@@ -357,6 +370,9 @@ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Prom
357
370
  ...(opts.codexHome ? { codexHome: opts.codexHome } : {}),
358
371
  ...(opts.busyTimeoutMs !== undefined ? { busyTimeoutMs: opts.busyTimeoutMs } : {}),
359
372
  ...(opts.blockMs !== undefined ? { blockMs: opts.blockMs } : {}),
373
+ ...(opts.notifyLoaded ? { notifyLoaded: true } : {}),
374
+ ...(loadedRelease ? { waitForLoadedRelease: true } : {}),
375
+ ...(loadedRelease ? { loadedRelease } : {}),
360
376
  env: {
361
377
  ...(process.env.CODEX_HOME ? { CODEX_HOME: process.env.CODEX_HOME } : {}),
362
378
  ...(process.env.OPENCODEX_HOME ? { OPENCODEX_HOME: process.env.OPENCODEX_HOME } : {}),
@@ -378,8 +394,12 @@ async function executeJob(opts: RequestPolicyRunOptions): Promise<void> {
378
394
  }
379
395
  return;
380
396
  }
381
- heldMutationLease = gate.lease;
397
+ const lease = gate.lease;
398
+ heldMutationLease = lease;
382
399
  try {
400
+ testHooks?.onAcquired?.();
401
+ await testHooks?.waitForRelease;
402
+ if (generation !== runGeneration) return;
383
403
  const blockMs = testHooks?.blockMs;
384
404
  let result: PolicyRunResult;
385
405
 
@@ -392,12 +412,15 @@ async function executeJob(opts: RequestPolicyRunOptions): Promise<void> {
392
412
  codexHome,
393
413
  ...(opts.busyTimeoutMs !== undefined ? { busyTimeoutMs: opts.busyTimeoutMs } : {}),
394
414
  ...(typeof blockMs === "number" && blockMs > 0 ? { holdAfterLoadMs: blockMs } : {}),
415
+ ...(testHooks?.onPolicyLoaded ? { onPolicyLoaded: testHooks.onPolicyLoaded } : {}),
395
416
  });
396
417
  } else {
397
418
  result = await runInWorker({
398
419
  ...opts,
399
420
  codexHome,
400
421
  ...(typeof blockMs === "number" && blockMs > 0 ? { blockMs } : {}),
422
+ ...(testHooks?.onPolicyLoaded ? { notifyLoaded: testHooks.onPolicyLoaded } : {}),
423
+ ...(testHooks?.waitAfterPolicyLoadedBuffer ? { waitAfterPolicyLoadedBuffer: testHooks.waitAfterPolicyLoadedBuffer } : {}),
401
424
  });
402
425
  }
403
426
 
@@ -408,7 +431,10 @@ async function executeJob(opts: RequestPolicyRunOptions): Promise<void> {
408
431
  if (err instanceof StorageWorkerAdmissionBusyError) applyMutationBusy();
409
432
  else applyFailed(err instanceof Error ? err.message : "worker_failed");
410
433
  } finally {
411
- releaseHeldMutationSlot();
434
+ // A reset may let a newer run acquire the global mirror before this stale
435
+ // continuation resumes. Release only the lease captured by this run.
436
+ lease.release();
437
+ if (heldMutationLease === lease) heldMutationLease = undefined;
412
438
  }
413
439
  }
414
440
 
@@ -13,6 +13,9 @@ interface RunMessage {
13
13
  busyTimeoutMs?: number;
14
14
  /** Test-only: block after loading the start-of-job policy (see holdAfterLoadMs). */
15
15
  blockMs?: number;
16
+ notifyLoaded?: boolean;
17
+ waitForLoadedRelease?: boolean;
18
+ loadedRelease?: SharedArrayBuffer;
16
19
  /** Env snapshot — Workers may not see parent mutations on all platforms. */
17
20
  env?: { CODEX_HOME?: string; OPENCODEX_HOME?: string };
18
21
  }
@@ -29,7 +32,7 @@ declare const self: Worker;
29
32
 
30
33
  self.onmessage = (event: MessageEvent<unknown>) => {
31
34
  if (!isRunMessage(event.data)) return;
32
- const { requestId, reason, force, codexHome, busyTimeoutMs, blockMs, env } = event.data;
35
+ const { requestId, reason, force, codexHome, busyTimeoutMs, blockMs, notifyLoaded, waitForLoadedRelease, loadedRelease, env } = event.data;
33
36
  try {
34
37
  if (env?.CODEX_HOME) process.env.CODEX_HOME = env.CODEX_HOME;
35
38
  if (env?.OPENCODEX_HOME) process.env.OPENCODEX_HOME = env.OPENCODEX_HOME;
@@ -38,6 +41,10 @@ self.onmessage = (event: MessageEvent<unknown>) => {
38
41
  force: force === true,
39
42
  ...(codexHome ? { codexHome } : {}),
40
43
  ...(busyTimeoutMs !== undefined ? { busyTimeoutMs } : {}),
44
+ ...(notifyLoaded ? { onPolicyLoaded: () => {
45
+ self.postMessage({ type: "loaded", requestId });
46
+ if (waitForLoadedRelease && loadedRelease) Atomics.wait(new Int32Array(loadedRelease), 0, 0);
47
+ } } : {}),
41
48
  ...(typeof blockMs === "number" && Number.isFinite(blockMs) && blockMs > 0
42
49
  ? { holdAfterLoadMs: Math.floor(blockMs) }
43
50
  : {}),
@@ -72,6 +72,8 @@ export interface PolicyRunDeps {
72
72
  * land before completion merges run metadata.
73
73
  */
74
74
  holdAfterLoadMs?: number;
75
+ /** Test-only synchronization seam; omitted in production. */
76
+ onPolicyLoaded?: () => void;
75
77
  }
76
78
 
77
79
  /** Canonical defaults — enabled is always false. */
@@ -436,6 +438,7 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
436
438
  const execute = deps.execute ?? executeArchivedCleanup;
437
439
 
438
440
  const policy = normalizeStorageCleanupPolicy(load());
441
+ deps.onPolicyLoaded?.();
439
442
 
440
443
  if (typeof deps.holdAfterLoadMs === "number" && Number.isFinite(deps.holdAfterLoadMs) && deps.holdAfterLoadMs > 0) {
441
444
  Bun.sleepSync(Math.floor(deps.holdAfterLoadMs));
@@ -35,6 +35,8 @@ export interface RestoreJobTestHooks extends StorageMutationCoordinatorTestHooks
35
35
  enableTestStream?: boolean;
36
36
  /** Forwarded to restoreTrashEntry _test hooks. */
37
37
  restoreTest?: RestoreTestHooks;
38
+ /** Notify responsiveness tests once the worker is about to block. */
39
+ workerReady?: SharedArrayBuffer;
38
40
  }
39
41
 
40
42
  const WORKER_TIMEOUT_MS = 10 * 60 * 1000;
@@ -159,6 +161,7 @@ function runInWorker(opts: {
159
161
  busyTimeoutMs?: number;
160
162
  blockMs?: number;
161
163
  restoreTest?: RestoreTestHooks;
164
+ workerReady?: SharedArrayBuffer;
162
165
  }): Promise<RestoreResult> {
163
166
  const reservation = tryReserveStorageWorker();
164
167
  if (!reservation) return Promise.reject(new StorageWorkerAdmissionBusyError());
@@ -222,6 +225,7 @@ function runInWorker(opts: {
222
225
  ...(opts.busyTimeoutMs !== undefined ? { busyTimeoutMs: opts.busyTimeoutMs } : {}),
223
226
  ...(opts.blockMs !== undefined ? { blockMs: opts.blockMs } : {}),
224
227
  ...(opts.restoreTest ? { restoreTest: opts.restoreTest } : {}),
228
+ ...(opts.workerReady ? { workerReady: opts.workerReady } : {}),
225
229
  env: {
226
230
  ...(process.env.CODEX_HOME ? { CODEX_HOME: process.env.CODEX_HOME } : {}),
227
231
  ...(process.env.OPENCODEX_HOME ? { OPENCODEX_HOME: process.env.OPENCODEX_HOME } : {}),
@@ -258,6 +262,7 @@ async function executeRestore(opts: {
258
262
  ...(opts.busyTimeoutMs !== undefined ? { busyTimeoutMs: opts.busyTimeoutMs } : {}),
259
263
  ...(typeof blockMs === "number" && blockMs > 0 ? { blockMs } : {}),
260
264
  ...(restoreTest ? { restoreTest } : {}),
265
+ ...(testHooks?.workerReady ? { workerReady: testHooks.workerReady } : {}),
261
266
  });
262
267
  } catch (err) {
263
268
  if (err instanceof StorageWorkerAdmissionBusyError) return busyRestoreResult();
@@ -13,6 +13,7 @@ interface RunMessage {
13
13
  /** Test-only: block before restore so responsiveness tests can probe /healthz. */
14
14
  blockMs?: number;
15
15
  restoreTest?: RestoreTestHooks;
16
+ workerReady?: SharedArrayBuffer;
16
17
  /** Env snapshot — Workers may not see parent mutations on all platforms. */
17
18
  env?: { CODEX_HOME?: string; OPENCODEX_HOME?: string };
18
19
  }
@@ -27,11 +28,16 @@ declare const self: Worker;
27
28
 
28
29
  self.onmessage = async (event: MessageEvent<unknown>) => {
29
30
  if (!isRunMessage(event.data)) return;
30
- const { requestId, trashId, codexHome, busyTimeoutMs, blockMs, restoreTest, env } = event.data;
31
+ const { requestId, trashId, codexHome, busyTimeoutMs, blockMs, restoreTest, workerReady, env } = event.data;
31
32
  try {
32
33
  if (env?.CODEX_HOME) process.env.CODEX_HOME = env.CODEX_HOME;
33
34
  if (env?.OPENCODEX_HOME) process.env.OPENCODEX_HOME = env.OPENCODEX_HOME;
34
35
  if (typeof blockMs === "number" && Number.isFinite(blockMs) && blockMs > 0) {
36
+ if (workerReady) {
37
+ const ready = new Int32Array(workerReady);
38
+ Atomics.store(ready, 0, 1);
39
+ Atomics.notify(ready, 0);
40
+ }
35
41
  await Bun.sleep(Math.floor(blockMs));
36
42
  }
37
43
  const result = restoreTrashEntry(trashId, {
@@ -16,6 +16,10 @@ export type StorageMutationBusyError = "storage_mutation_busy";
16
16
  export interface StorageMutationCoordinatorTestHooks {
17
17
  /** Block after acquiring the slot, before mutation work (race tests). */
18
18
  blockMs?: number;
19
+ /** Notify race tests as soon as the per-home lease is owned. */
20
+ onAcquired?: () => void;
21
+ /** Keep the lease held until a race test has issued its competing operation. */
22
+ waitForRelease?: Promise<void>;
19
23
  }
20
24
 
21
25
  interface ActiveSlot {
@@ -79,6 +83,7 @@ export function tryBeginStorageMutation(
79
83
  },
80
84
  };
81
85
  slots.set(key, { kind, startedAt: Date.now(), lease: ownerLease });
86
+ testHooks?.onAcquired?.();
82
87
  return { acquired: true, lease: ownerLease };
83
88
  }
84
89
 
@@ -94,6 +99,7 @@ export function endStorageMutation(codexHome?: string): void {
94
99
  }
95
100
 
96
101
  async function applyCoordinatorBlock(): Promise<void> {
102
+ await testHooks?.waitForRelease;
97
103
  const blockMs = testHooks?.blockMs;
98
104
  if (typeof blockMs === "number" && Number.isFinite(blockMs) && blockMs > 0) {
99
105
  await Bun.sleep(Math.floor(blockMs));