From 60e6ba55d77ba796ab4321fe01bdfa11e83c47b6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 21:15:47 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Batched=20GC=20operations?= =?UTF-8?q?=20for=20ArtifactStore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optimized sequential `fs.stat` and `fs.rm` calls in `ArtifactStore.gc` into batched concurrent operations using `Promise.all` (chunk size 10), preserving array order for correctness. --- .jules/bolt.md | 3 ++ src/core/sub-agent/artifacts/store.ts | 42 +++++++++++++++++----- tests/integration/prompt_templates.test.ts | 4 ++- 3 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 00000000..283330f8 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2025-02-12 - Batched I/O for GC performance +**Learning:** Concurrent array mutation inside Promise.all chunking produces non-deterministic order, causing subtle bugs in artifact GC which relies on predictable sorting. +**Action:** Always map Promise.all results and iterate sequentially to apply mutations or maintain order. diff --git a/src/core/sub-agent/artifacts/store.ts b/src/core/sub-agent/artifacts/store.ts index 8a7aff5c..d041ced2 100644 --- a/src/core/sub-agent/artifacts/store.ts +++ b/src/core/sub-agent/artifacts/store.ts @@ -93,13 +93,35 @@ export class ArtifactStore { const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []); const files: Array<{ name: string; path: string; mtimeMs: number; size: number }> = []; - for (const entry of entries) { - if (!entry.isFile()) continue; - const filePath = path.join(root, entry.name); - if (!isWithinDir(root, filePath)) continue; - const stat = await fs.stat(filePath).catch(() => null); - if (!stat) continue; - files.push({ name: entry.name, path: filePath, mtimeMs: stat.mtimeMs, size: stat.size }); + // Batch fs.stat calls for performance + const validEntries = entries.filter((e) => { + if (!e.isFile()) return false; + const filePath = path.join(root, e.name); + return isWithinDir(root, filePath); + }); + + for (let i = 0; i < validEntries.length; i += 10) { + const chunk = validEntries.slice(i, i + 10); + const results = await Promise.all( + chunk.map(async (entry) => { + const filePath = path.join(root, entry.name); + const stat = await fs.stat(filePath).catch(() => null); + return stat + ? { + name: entry.name, + path: filePath, + mtimeMs: stat.mtimeMs, + size: stat.size, + } + : null; + }), + ); + + for (const result of results) { + if (result) { + files.push(result); + } + } } const maxAgeMs = options?.maxAgeMs ?? LIMITS.artifactTtlMs; @@ -118,8 +140,10 @@ export class ArtifactStore { removedBytes += file.size; }; - for (const file of expired) { - await removeFile(file); + // Batch expired file deletion for performance + for (let i = 0; i < expired.length; i += 10) { + const chunk = expired.slice(i, i + 10); + await Promise.all(chunk.map(removeFile)); } // Recompute remaining after TTL removal (newest first). diff --git a/tests/integration/prompt_templates.test.ts b/tests/integration/prompt_templates.test.ts index 21905259..f1360d78 100644 --- a/tests/integration/prompt_templates.test.ts +++ b/tests/integration/prompt_templates.test.ts @@ -36,7 +36,9 @@ describe('Prompt templates', () => { expect(planSystem).toContain('You are SalmonLoop.'); expect(patchSystem).toContain('You are PATCH, a phase-native diff compiler.'); - expect(autopilotSystem).toContain('You are a senior software engineer running in "autopilot" mode.'); + expect(autopilotSystem).toContain( + 'You are a senior software engineer running in "autopilot" mode.', + ); expect(answerSystem).toContain('You are a coding assistant in "answer" mode.'); expect(researchSystem).toContain('You are a research assistant.'); });