Skip to content

Commit f01c6b3

Browse files
authored
fix(session): type message list not found errors (anomalyco#27275)
1 parent fed043a commit f01c6b3

9 files changed

Lines changed: 95 additions & 23 deletions

File tree

packages/opencode/src/cli/cmd/stats.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Effect } from "effect"
22
import { effectCmd } from "../effect-cmd"
33
import { Session } from "@/session/session"
4+
import { NotFoundError } from "@/storage/storage"
45
import { Database } from "@/storage/db"
56
import { SessionTable } from "../../session/session.sql"
67
import { Project } from "@/project/project"
@@ -162,7 +163,9 @@ const aggregateSessionStats = Effect.fn("Cli.stats.aggregate")(function* (
162163
filteredSessions,
163164
(session) =>
164165
Effect.gen(function* () {
165-
const messages = yield* svc.messages({ sessionID: session.id })
166+
const messages = yield* svc.messages({ sessionID: session.id }).pipe(
167+
Effect.catchIf(NotFoundError.isInstance, () => Effect.succeed([])),
168+
)
166169

167170
const sessionCost = session.cost ?? 0
168171
const sessionTokens = session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }

packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
101101
}
102102
yield* SessionError.mapStorageNotFound(session.get(ctx.params.sessionID))
103103
if (ctx.query.limit === undefined || ctx.query.limit === 0) {
104-
return yield* session.messages({ sessionID: ctx.params.sessionID })
104+
return yield* SessionError.mapStorageNotFound(session.messages({ sessionID: ctx.params.sessionID }))
105105
}
106106

107107
const page = yield* SessionError.mapStorageNotFound(
@@ -250,7 +250,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
250250
payload: typeof SummarizePayload.Type
251251
}) {
252252
yield* revertSvc.cleanup(yield* SessionError.mapStorageNotFound(session.get(ctx.params.sessionID)))
253-
const messages = yield* session.messages({ sessionID: ctx.params.sessionID })
253+
const messages = yield* SessionError.mapStorageNotFound(session.messages({ sessionID: ctx.params.sessionID }))
254254
const defaultAgent = yield* agentSvc.defaultAgent()
255255
const currentAgent = messages.findLast((message) => message.info.role === "user")?.info.agent ?? defaultAgent
256256

packages/opencode/src/session/compaction.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,9 @@ export const layer: Layer.Layer<
565565
if (processor.message.error) return "stop"
566566
if (result === "continue") {
567567
const summary = summaryText(
568-
(yield* session.messages({ sessionID: input.sessionID })).find((item) => item.info.id === msg.id) ?? {
568+
(yield* session.messages({ sessionID: input.sessionID }).pipe(Effect.orDie)).find(
569+
(item) => item.info.id === msg.id,
570+
) ?? {
569571
info: msg,
570572
parts: [],
571573
},

packages/opencode/src/session/prompt.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,7 +1077,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
10771077
...(current.model.variant && current.model.variant !== "default" ? { variant: current.model.variant } : {}),
10781078
}
10791079
}
1080-
const match = yield* sessions.findMessage(sessionID, (m) => m.info.role === "user" && !!m.info.model)
1080+
const match = yield* sessions
1081+
.findMessage(sessionID, (m) => m.info.role === "user" && !!m.info.model)
1082+
.pipe(Effect.orDie)
10811083
if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model
10821084
return yield* provider.defaultModel()
10831085
})
@@ -1615,9 +1617,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
16151617
)
16161618

16171619
const lastAssistant = Effect.fnUntraced(function* (sessionID: SessionID) {
1618-
const match = yield* sessions.findMessage(sessionID, (m) => m.info.role !== "user")
1620+
const match = yield* sessions.findMessage(sessionID, (m) => m.info.role !== "user").pipe(Effect.orDie)
16191621
if (Option.isSome(match)) return match.value
1620-
const msgs = yield* sessions.messages({ sessionID, limit: 1 })
1622+
const msgs = yield* sessions.messages({ sessionID, limit: 1 }).pipe(Effect.orDie)
16211623
if (msgs.length > 0) return msgs[0]
16221624
throw new Error("Impossible")
16231625
})

packages/opencode/src/session/revert.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const layer = Layer.effect(
4040

4141
const revert = Effect.fn("SessionRevert.revert")(function* (input: RevertInput) {
4242
yield* state.assertNotBusy(input.sessionID)
43-
const all = yield* sessions.messages({ sessionID: input.sessionID })
43+
const all = yield* sessions.messages({ sessionID: input.sessionID }).pipe(Effect.orDie)
4444
let lastUser: MessageV2.User | undefined
4545
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
4646

@@ -103,7 +103,7 @@ export const layer = Layer.effect(
103103
const cleanup = Effect.fn("SessionRevert.cleanup")(function* (session: Session.Info) {
104104
if (!session.revert) return
105105
const sessionID = session.id
106-
const msgs = yield* sessions.messages({ sessionID })
106+
const msgs = yield* sessions.messages({ sessionID }).pipe(Effect.orDie)
107107
const messageID = session.revert.messageID
108108
const remove = [] as MessageV2.WithParts[]
109109
let target: MessageV2.WithParts | undefined

packages/opencode/src/session/session.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ export interface Interface {
474474
readonly clearRevert: (sessionID: SessionID) => Effect.Effect<void>
475475
readonly setSummary: (input: { sessionID: SessionID; summary: Info["summary"] }) => Effect.Effect<void>
476476
readonly diff: (sessionID: SessionID) => Effect.Effect<Snapshot.FileDiff[]>
477-
readonly messages: (input: { sessionID: SessionID; limit?: number }) => Effect.Effect<MessageV2.WithParts[]>
477+
readonly messages: (input: { sessionID: SessionID; limit?: number }) => Effect.Effect<MessageV2.WithParts[], NotFound>
478478
readonly children: (parentID: SessionID) => Effect.Effect<Info[]>
479479
readonly remove: (sessionID: SessionID) => Effect.Effect<void, NotFound>
480480
readonly updateMessage: <T extends MessageV2.Info>(msg: T) => Effect.Effect<T>
@@ -497,7 +497,7 @@ export interface Interface {
497497
readonly findMessage: (
498498
sessionID: SessionID,
499499
predicate: (msg: MessageV2.WithParts) => boolean,
500-
) => Effect.Effect<Option.Option<MessageV2.WithParts>>
500+
) => Effect.Effect<Option.Option<MessageV2.WithParts>, NotFound>
501501
}
502502

503503
export class Service extends Context.Service<Service, Interface>()("@opencode/Session") {}
@@ -757,11 +757,25 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service |
757757
.pipe(Effect.orElseSucceed((): Snapshot.FileDiff[] => []))
758758
})
759759

760-
const messages = Effect.fn("Session.messages")(function* (input: { sessionID: SessionID; limit?: number }) {
760+
const messages: Interface["messages"] = Effect.fn("Session.messages")(function* (input) {
761761
if (input.limit) {
762-
return MessageV2.page({ sessionID: input.sessionID, limit: input.limit }).items
762+
return (yield* MessageV2.pageEffect({ sessionID: input.sessionID, limit: input.limit })).items
763763
}
764-
return Array.from(MessageV2.stream(input.sessionID)).reverse()
764+
765+
const size = 50
766+
const result = [] as MessageV2.WithParts[]
767+
let before: string | undefined
768+
while (true) {
769+
const page = yield* MessageV2.pageEffect({ sessionID: input.sessionID, limit: size, before })
770+
if (page.items.length === 0) break
771+
for (let i = page.items.length - 1; i >= 0; i--) {
772+
const item = page.items[i]
773+
if (item) result.push(item)
774+
}
775+
if (!page.more || !page.cursor) break
776+
before = page.cursor
777+
}
778+
return result.reverse()
765779
})
766780

767781
const removeMessage = Effect.fn("Session.removeMessage")(function* (input: {
@@ -799,12 +813,18 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service |
799813
})
800814

801815
/** Finds the first message matching the predicate, searching newest-first. */
802-
const findMessage = Effect.fn("Session.findMessage")(function* (
803-
sessionID: SessionID,
804-
predicate: (msg: MessageV2.WithParts) => boolean,
805-
) {
806-
for (const item of MessageV2.stream(sessionID)) {
807-
if (predicate(item)) return Option.some(item)
816+
const findMessage: Interface["findMessage"] = Effect.fn("Session.findMessage")(function* (sessionID, predicate) {
817+
const size = 50
818+
let before: string | undefined
819+
while (true) {
820+
const page = yield* MessageV2.pageEffect({ sessionID, limit: size, before })
821+
if (page.items.length === 0) break
822+
for (let i = page.items.length - 1; i >= 0; i--) {
823+
const item = page.items[i]
824+
if (item && predicate(item)) return Option.some(item)
825+
}
826+
if (!page.more || !page.cursor) break
827+
before = page.cursor
808828
}
809829
return Option.none<MessageV2.WithParts>()
810830
})

packages/opencode/src/session/summary.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export const layer = Layer.effect(
102102
sessionID: SessionID
103103
messageID: MessageID
104104
}) {
105-
const all = yield* sessions.messages({ sessionID: input.sessionID })
105+
const all = yield* sessions.messages({ sessionID: input.sessionID }).pipe(Effect.orDie)
106106
if (!all.length) return
107107

108108
const diffs = yield* computeDiff({ messages: all })

packages/opencode/test/server/httpapi-exercise/runner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ function withContext<A, E>(
168168
)
169169
return { info, part }
170170
}),
171-
messages: (sessionID) => run(modules.Session.Service.use((svc) => svc.messages({ sessionID }))),
171+
messages: (sessionID) =>
172+
run(modules.Session.Service.use((svc) => svc.messages({ sessionID }).pipe(Effect.orDie))),
172173
todos: (sessionID, todos) => run(modules.Todo.Service.use((svc) => svc.update({ sessionID, todos }))),
173174
worktree: (input) => run(modules.Worktree.Service.use((svc) => svc.create(input))),
174175
worktreeRemove: (directory) =>

packages/opencode/test/session/messages-pagination.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from "bun:test"
2-
import { Effect } from "effect"
2+
import { Effect, Option } from "effect"
33
import { Session as SessionNs } from "@/session/session"
44
import { MessageV2 } from "../../src/session/message-v2"
55
import { MessageID, PartID, type SessionID } from "../../src/session/schema"
@@ -582,6 +582,50 @@ describe("MessageV2.get", () => {
582582
)
583583
})
584584

585+
describe("Session.messages", () => {
586+
it.instance("returns all messages in chronological order across pages", () =>
587+
withSession(({ session, sessionID }) =>
588+
Effect.gen(function* () {
589+
const ids = yield* fill(sessionID, 55)
590+
const result = yield* session.messages({ sessionID })
591+
expect(result.map((item) => item.info.id)).toEqual(ids)
592+
}),
593+
),
594+
)
595+
596+
it.instance("fails with NotFoundError for non-existent session", () =>
597+
Effect.gen(function* () {
598+
const session = yield* SessionNs.Service
599+
const fake = "non-existent-session" as SessionID
600+
const error = yield* Effect.flip(session.messages({ sessionID: fake }))
601+
expect(error).toBeInstanceOf(NotFoundError)
602+
expect(error.message).toBe(`Session not found: ${fake}`)
603+
}),
604+
)
605+
})
606+
607+
describe("Session.findMessage", () => {
608+
it.instance("searches newest-first", () =>
609+
withSession(({ session, sessionID }) =>
610+
Effect.gen(function* () {
611+
const ids = yield* fill(sessionID, 3)
612+
const result = yield* session.findMessage(sessionID, () => true)
613+
expect(Option.isSome(result) ? result.value.info.id : undefined).toBe(ids.at(-1))
614+
}),
615+
),
616+
)
617+
618+
it.instance("fails with NotFoundError for non-existent session", () =>
619+
Effect.gen(function* () {
620+
const session = yield* SessionNs.Service
621+
const fake = "non-existent-session" as SessionID
622+
const error = yield* Effect.flip(session.findMessage(fake, () => true))
623+
expect(error).toBeInstanceOf(NotFoundError)
624+
expect(error.message).toBe(`Session not found: ${fake}`)
625+
}),
626+
)
627+
})
628+
585629
describe("MessageV2.filterCompacted", () => {
586630
it.instance("returns all messages when no compaction", () =>
587631
withSession(({ sessionID }) =>

0 commit comments

Comments
 (0)