@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.
- package/gui/dist/assets/ApiKeys-BthcYAks.js +20 -0
- package/gui/dist/assets/Claude-D1uQ1rsr.js +2 -0
- package/gui/dist/assets/CodexSet-BUNKAStQ.js +12 -0
- package/gui/dist/assets/FileIntegrationPage-CyrJ_kz1.js +1 -0
- package/gui/dist/assets/Grok-CeSYaWba.js +1 -0
- package/gui/dist/assets/Integrations-BkE9NLJW.js +2 -0
- package/gui/dist/assets/IntegrationsOverview-D-nDl5t5.js +3 -0
- package/gui/dist/assets/Logs-uExIBMZr.js +1 -0
- package/gui/dist/assets/Models-CNpjs-ld.js +1 -0
- package/gui/dist/assets/NumberStepper-CKVXkKzE.js +1 -0
- package/gui/dist/assets/Providers-D72-5Egl.js +1 -0
- package/gui/dist/assets/RestoreDialog-2MVe4MQR.js +1 -0
- package/gui/dist/assets/Startup-ClZlpU_Z.js +1 -0
- package/gui/dist/assets/Storage-ChC2WGMp.js +1 -0
- package/gui/dist/assets/Subagents-BpkXAXLZ.js +1 -0
- package/gui/dist/assets/Usage-BkEOitI3.js +1 -0
- package/gui/dist/assets/catalog-CerhMn14.js +1 -0
- package/gui/dist/assets/collapse-store-DnqZvhx0.js +1 -0
- package/gui/dist/assets/data-surface-BzhqUIJ_.js +1 -0
- package/gui/dist/assets/data-surface-D8RXShV2.js +1 -0
- package/gui/dist/assets/index-DE4QGJSt.js +12 -0
- package/gui/dist/assets/index-pLho_LBF.css +1 -0
- package/gui/dist/assets/intl-formatters-CTcuZoAe.js +1 -0
- package/gui/dist/assets/model-display-CCqxubZd.js +1 -0
- package/gui/dist/assets/provider-payload-CI2ayvqv.js +1 -0
- package/gui/dist/assets/section-tabs-DEASco68.js +1 -0
- package/gui/dist/assets/shared-p0R307IX.js +69 -0
- package/gui/dist/index.html +3 -2
- package/package.json +1 -1
- package/src/adapters/command-code-project-context.ts +24 -19
- package/src/codex/catalog/sync.ts +7 -2
- package/src/generated/compatibility-version.json +11 -11
- package/src/oauth/google-antigravity.ts +18 -5
- package/src/storage/cleanup.ts +11 -0
- package/src/storage/policy-job.ts +29 -3
- package/src/storage/policy-worker.ts +8 -1
- package/src/storage/policy.ts +3 -0
- package/src/storage/restore-job.ts +5 -0
- package/src/storage/restore-worker.ts +7 -1
- package/src/storage/storage-mutation-coordinator.ts +6 -0
- package/gui/dist/assets/index-DLkXOXLC.css +0 -1
- package/gui/dist/assets/index-DMKDnQOg.js +0 -112
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="
|
|
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.
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
3077
|
+
"sha256": "2a90e83983c7da9bdb2ac0134870dd7ba85d4469237fae798c60b34985c8e554"
|
|
3078
3078
|
},
|
|
3079
3079
|
{
|
|
3080
3080
|
"path": "src/storage/policy-job.ts",
|
|
3081
|
-
"sha256": "
|
|
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": "
|
|
3089
|
+
"sha256": "c33d74fe319fc9103bd1937a81c8f0b23f319d7f634bea87a691da845ebec282"
|
|
3090
3090
|
},
|
|
3091
3091
|
{
|
|
3092
3092
|
"path": "src/storage/policy.ts",
|
|
3093
|
-
"sha256": "
|
|
3093
|
+
"sha256": "93050c0be848f0df9c7114d903272738fb817b62fb0cdd9d7a59e55b9b267601"
|
|
3094
3094
|
},
|
|
3095
3095
|
{
|
|
3096
3096
|
"path": "src/storage/restore-job.ts",
|
|
3097
|
-
"sha256": "
|
|
3097
|
+
"sha256": "216a1a1c3610f5d3fbf50952855b792583f851bc989294f986208c93fd9eefaa"
|
|
3098
3098
|
},
|
|
3099
3099
|
{
|
|
3100
3100
|
"path": "src/storage/restore-worker.ts",
|
|
3101
|
-
"sha256": "
|
|
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": "
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
package/src/storage/cleanup.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
: {}),
|
package/src/storage/policy.ts
CHANGED
|
@@ -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));
|