fix(core): validate compaction keepID with SessionMessage.ID schema (#57945)

This commit is contained in:
Aiden Cline
2026-10-21 20:01:10 -05:00
committed by GitHub
Unverified
parent 7012c9dbf3
commit c87f6b710e
3 changed files with 44 additions and 14 deletions
+5 -7
View File
@@ -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
+2 -7
View File
@@ -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
@@ -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(),