diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 25c809467..b77d32457 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Batched same-turn file-session appends behind the session writer's microtask boundary. Render and tool paths can now record several transcript entries without paying one synchronous file write per entry, while explicit `flush()` and `close()` still make queued lines durable in order. + ## [17.2.1] - 2026-07-30 ### Added diff --git a/packages/coding-agent/src/session/session-storage.ts b/packages/coding-agent/src/session/session-storage.ts index a2db80cf2..97b8a7337 100644 --- a/packages/coding-agent/src/session/session-storage.ts +++ b/packages/coding-agent/src/session/session-storage.ts @@ -90,6 +90,8 @@ class FileSessionStorageWriter implements SessionStorageWriter { #closed = false; #error: Error | undefined; #onError: ((err: Error) => void) | undefined; + #pending = ""; + #flushScheduled = false; constructor(fpath: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }) { this.#onError = options?.onError; @@ -112,25 +114,41 @@ class FileSessionStorageWriter implements SessionStorageWriter { return error; } - async append(line: string): Promise { - if (this.#closed) throw new Error("Writer closed"); - if (this.#error) throw this.#error; - try { - const buf = Buffer.from(line, "utf-8"); - let offset = 0; - while (offset < buf.length) { - const written = fs.writeSync(this.#fd, buf, offset, buf.length - offset); - if (written === 0) { - throw new Error("Short write"); - } - offset += written; + #writeNow(line: string): void { + const buf = Buffer.from(line, "utf-8"); + let offset = 0; + while (offset < buf.length) { + const written = fs.writeSync(this.#fd, buf, offset, buf.length - offset); + if (written === 0) { + throw new Error("Short write"); } - } catch (err) { - throw this.#recordError(err); + offset += written; } } + #flushPendingNow(): void { + this.#flushScheduled = false; + if (this.#pending.length === 0) return; + const pending = this.#pending; + this.#pending = ""; + try { + this.#writeNow(pending); + } catch (err) { + this.#recordError(err); + } + } + + async append(line: string): Promise { + if (this.#closed) throw new Error("Writer closed"); + if (this.#error) throw this.#error; + this.#pending += line; + if (this.#flushScheduled) return; + this.#flushScheduled = true; + queueMicrotask(() => this.#flushPendingNow()); + } + async flush(): Promise { + this.#flushPendingNow(); if (this.#error) throw this.#error; } @@ -140,6 +158,7 @@ class FileSessionStorageWriter implements SessionStorageWriter { async close(): Promise { if (this.#closed) return; + this.#flushPendingNow(); this.#closed = true; // Unregister from finalization - we're closing properly writerRegistry.unregister(this); diff --git a/packages/coding-agent/test/session-storage.test.ts b/packages/coding-agent/test/session-storage.test.ts index 0cb8221f2..0cd35200a 100644 --- a/packages/coding-agent/test/session-storage.test.ts +++ b/packages/coding-agent/test/session-storage.test.ts @@ -87,6 +87,41 @@ class ControlledTitleUpdateBackend implements SessionStorageBackend { this.#firstUpdate.reject(error); } } +describe("FileSessionStorage writer", () => { + let tempDir: string; + let storage: FileSessionStorage; + + beforeEach(async () => { + tempDir = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-session-writer-")); + storage = new FileSessionStorage(); + }); + + afterEach(async () => { + await fsp.rm(tempDir, { recursive: true, force: true }); + }); + + it("preserves append order through flush and close", async () => { + const sessionPath = path.join(tempDir, "batched.jsonl"); + const writer = storage.openWriter(sessionPath, { flags: "w" }); + await writer.append("one\n"); + await writer.append("two\n"); + + await writer.flush(); + expect(fs.readFileSync(sessionPath, "utf8")).toBe("one\ntwo\n"); + await writer.close(); + }); + + it("flushes queued appends before closing", async () => { + const sessionPath = path.join(tempDir, "closed.jsonl"); + const writer = storage.openWriter(sessionPath, { flags: "w" }); + await writer.append("one\n"); + await writer.append("two\n"); + await writer.close(); + + expect(fs.readFileSync(sessionPath, "utf8")).toBe("one\ntwo\n"); + }); +}); + describe("FileSessionStorage.deleteSessionWithArtifacts", () => { let tempDir: string; let storage: FileSessionStorage;