fix: reject raw message transcript events

This commit is contained in:
Josh Lehman
2026-06-04 14:46:35 -07:00
parent b787f97349
commit da45b43789
2 changed files with 42 additions and 17 deletions

View File

@@ -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([

View File

@@ -243,6 +243,7 @@ export async function appendTranscriptEvent(
scope: SessionTranscriptAccessScope,
event: TranscriptEvent,
): Promise<void> {
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<TMessage>(
scope: SessionTranscriptWriteScope,