From c87f6b710ec0370ad2bb2193ce09a309c8d068f0 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:01:10 -0500 Subject: [PATCH] fix(core): validate compaction keepID with SessionMessage.ID schema (#57945) --- packages/core/src/session/history.ts | 12 +++--- packages/core/src/session/projector.ts | 9 +---- packages/core/test/session-compaction.test.ts | 37 +++++++++++++++++++ 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/packages/core/src/session/history.ts b/packages/core/src/session/history.ts index 74159a27b2b..6b96d00306a 100644 --- a/packages/core/src/session/history.ts +++ b/packages/core/src/session/history.ts @@ -6,7 +6,7 @@ import { SessionLegacy } from "@opencode/schema/session-legacy" import { Database } from "../database/database.js" import { MessageDecodeError } from "./error.js" import { SessionMessage } from "./message.js" -import { SessionSchema } from "./schema.js" +import type { SessionSchema } from "./schema.js" import { Instructions } from "../instructions/index.js" import { InstructionState } from "./instruction-state.js" import { SessionRecentText } from "./recent-text.js" @@ -93,8 +93,8 @@ export const decodeMessageRow = (row: typeof SessionMessageTable.$inferSelect) = Effect.mapError( () => new MessageDecodeError({ - sessionID: SessionSchema.ID.make(row.session_id), - messageID: SessionMessage.ID.make(row.id), + sessionID: row.session_id, + messageID: row.id, }), ), ) @@ -154,13 +154,11 @@ const withRecentText = Effect.fnUntraced(function* ( const compaction = first?.message if (compaction?.type !== "compaction" || compaction.status !== "completed") return entries const keepID = compaction.metadata?.keepID - if (typeof keepID !== "string") return entries + if (!Schema.is(SessionMessage.ID)(keepID)) return entries const start = yield* db .select({ seq: SessionMessageTable.seq }) .from(SessionMessageTable) - .where( - and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.id, SessionMessage.ID.make(keepID))), - ) + .where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.id, keepID))) .get() .pipe(Effect.orDie) if (!start) return entries diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index 86b6eed40d8..29d3c9b1477 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -224,16 +224,11 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* ( } // A summary references the first message it kept, which the fork copied under a new ID. const keepID = row.type === "compaction" ? row.data.metadata?.keepID : undefined - if (typeof keepID !== "string") return copy + if (!Schema.is(SessionMessage.ID)(keepID)) return copy const kept = yield* db .select({ seq: SessionMessageTable.seq }) .from(SessionMessageTable) - .where( - and( - eq(SessionMessageTable.session_id, event.data.parentID), - eq(SessionMessageTable.id, SessionMessage.ID.make(keepID)), - ), - ) + .where(and(eq(SessionMessageTable.session_id, event.data.parentID), eq(SessionMessageTable.id, keepID))) .get() .pipe(Effect.orDie) if (!kept) return copy diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index bd7bd3029f6..85eb563db4c 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -199,6 +199,43 @@ it.effect("forks rebuild the recent text of a summary they copied", () => }), ) +it.effect("ignores non-message keepID strings when loading history or forking", () => + Effect.gen(function* () { + const parentID = Session.ID.make("ses_invalid_keepid_parent") + const childID = Session.ID.make("ses_invalid_keepid_child") + yield* insertSession(parentID) + const compaction = SessionMessage.CompactionCompleted.make({ + id: SessionMessage.ID.create(), + type: "compaction", + status: "completed", + reason: "manual", + summary: "## Objective\n- plugin summary", + metadata: { keepID: "not-a-msg-id" }, + time: { created: DateTime.makeUnsafe(0) }, + }) + yield* storeMessages(parentID, [textUser("Earlier question"), compaction]) + + const store = yield* SessionStore.Service + const [parent] = yield* store.context(parentID) + expect(parent).toMatchObject({ + type: "compaction", + metadata: { keepID: "not-a-msg-id" }, + }) + + const bus = yield* Bus.Service + yield* bus.publish(SessionEvent.Forked, { + sessionID: childID, + parentID, + boundary: { type: "through", messageID: compaction.id }, + }) + const [child] = yield* store.context(childID) + expect(child).toMatchObject({ + type: "compaction", + metadata: { keepID: "not-a-msg-id" }, + }) + }), +) + const textUser = (text: string) => SessionMessage.User.make({ id: SessionMessage.ID.create(),