From da45b43789d5b258241e149aa87e797deff4811d Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Thu, 4 Jun 2026 14:46:35 -0700 Subject: [PATCH] fix: reject raw message transcript events --- src/config/sessions/session-accessor.test.ts | 45 ++++++++++++-------- src/config/sessions/session-accessor.ts | 14 ++++++ 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/config/sessions/session-accessor.test.ts b/src/config/sessions/session-accessor.test.ts index 9a1d674d66bf..71ce2a8091b7 100644 --- a/src/config/sessions/session-accessor.test.ts +++ b/src/config/sessions/session-accessor.test.ts @@ -367,10 +367,8 @@ describe("session accessor file-backed seam", () => { storePath, }; const event = { - id: "msg-1", - message: { role: "user", content: "hello" }, - parentId: null, - type: "message", + payload: { value: "hello" }, + type: "metadata", }; await appendTranscriptEvent(scope, { type: "session", sessionId: "session-1" }); @@ -383,16 +381,33 @@ describe("session accessor file-backed seam", () => { expect(fs.statSync(transcriptPath).mode & 0o777).toBe(0o600); }); + it("rejects raw message transcript events", async () => { + const scope = { + sessionFile: transcriptPath, + sessionId: "session-1", + sessionKey: "agent:main:main", + storePath, + }; + + await expect( + appendTranscriptEvent(scope, { + id: "msg-1", + message: { role: "user", content: "hello" }, + parentId: null, + type: "message", + }), + ).rejects.toThrow(/appendTranscriptMessage/); + expect(fs.existsSync(transcriptPath)).toBe(false); + }); + it("loads transcript events without a session key when the read target is explicit", async () => { const scope = { sessionFile: transcriptPath, sessionId: "session-1", }; const event = { - id: "msg-1", - message: { role: "user", content: "hello" }, - parentId: null, - type: "message", + payload: { value: "hello" }, + type: "metadata", }; await appendTranscriptEvent( @@ -409,10 +424,8 @@ describe("session accessor file-backed seam", () => { it("loads transcript events from a generated read target without a session key", async () => { const event = { - id: "msg-1", - message: { role: "user", content: "hello" }, - parentId: null, - type: "message", + payload: { value: "hello" }, + type: "metadata", }; fs.writeFileSync(path.join(tempDir, "session-1.jsonl"), `${JSON.stringify(event)}\n`, "utf-8"); @@ -502,10 +515,8 @@ describe("session accessor file-backed seam", () => { storePath, }; const event = { - id: "msg-1", - message: { role: "user", content: "hello" }, - parentId: null, - type: "message", + payload: { value: "hello" }, + type: "metadata", }; await upsertSessionEntry(scope, { @@ -541,7 +552,7 @@ describe("session accessor file-backed seam", () => { sessionKey: "AGENT:MAIN:MAIN", storePath, }, - { id: "msg-1", type: "message" }, + { id: "event-1", type: "metadata" }, ); expect(listSessionEntries({ storePath }).map((entry) => entry.sessionKey)).toEqual([ diff --git a/src/config/sessions/session-accessor.ts b/src/config/sessions/session-accessor.ts index d4a8649426b9..91fab72e9d89 100644 --- a/src/config/sessions/session-accessor.ts +++ b/src/config/sessions/session-accessor.ts @@ -243,6 +243,7 @@ export async function appendTranscriptEvent( scope: SessionTranscriptAccessScope, event: TranscriptEvent, ): Promise { + assertNonMessageTranscriptEvent(event); const transcript = await resolveTranscriptAccess(scope); await appendSessionTranscriptEvent({ event, @@ -250,6 +251,19 @@ export async function appendTranscriptEvent( }); } +function assertNonMessageTranscriptEvent(event: TranscriptEvent): void { + if (!event || typeof event !== "object" || Array.isArray(event)) { + return; + } + // Message records require parent-link, idempotency, and redaction handling + // from appendTranscriptMessage; raw event writes would bypass those invariants. + if ((event as { type?: unknown }).type === "message") { + throw new Error( + "appendTranscriptEvent cannot write message transcript records; use appendTranscriptMessage instead.", + ); + } +} + /** Appends one transcript message through the storage-neutral writer seam. */ export async function appendTranscriptMessage( scope: SessionTranscriptWriteScope,