Skip to content

Commit fc423a0

Browse files
committed
fix(session): restore message pagination hydration
1 parent 67a7f5e commit fc423a0

3 files changed

Lines changed: 72 additions & 41 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
2+
import { Database } from "@opencode-ai/core/database/database"
23
import { Agent } from "@/agent/agent"
34
import { SessionV1 } from "@opencode-ai/core/v1/session"
45
import { EventV2Bridge } from "@/event-v2-bridge"
@@ -48,6 +49,7 @@ const tryParseJson = (text: string) =>
4849
export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", (handlers) =>
4950
Effect.gen(function* () {
5051
const session = yield* Session.Service
52+
const database = yield* Database.Service
5153
const shareSvc = yield* SessionShare.Service
5254
const promptSvc = yield* SessionPrompt.Service
5355
const revertSvc = yield* SessionRevert.Service
@@ -128,7 +130,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
128130
limit: ctx.query.limit,
129131
before: ctx.query.before,
130132
after: ctx.query.after,
131-
}),
133+
}).pipe(Effect.provideService(Database.Service, database)),
132134
)
133135
if (!page.cursor) return page.items
134136

packages/opencode/src/session/message-v2.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ function hydrate(rows: (typeof MessageTable.$inferSelect)[]) {
105105
const partByMessage = new Map<string, Part[]>()
106106
return Effect.gen(function* () {
107107
if (ids.length > 0) {
108+
const { db } = yield* Database.Service
108109
const partRows = yield* db
109110
.select()
110111
.from(PartTable)
@@ -442,22 +443,25 @@ export const page = Effect.fn("MessageV2.page")(function* (input: {
442443
: after
443444
? and(eq(MessageTable.session_id, input.sessionID), newer(after))
444445
: eq(MessageTable.session_id, input.sessionID)
445-
const rows = yield* Database.use((db) =>
446-
db
447-
.select()
448-
.from(MessageTable)
449-
.where(where)
450-
.orderBy(
451-
after ? asc(MessageTable.time_created) : desc(MessageTable.time_created),
452-
after ? asc(MessageTable.id) : desc(MessageTable.id),
453-
)
454-
.limit(input.limit + 1)
455-
.all(),
456-
)
457-
if (rows.length === 0) {
458-
const row = yield* Database.use((db) =>
459-
db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.id, input.sessionID)).get(),
446+
const { db } = yield* Database.Service
447+
const rows = yield* db
448+
.select()
449+
.from(MessageTable)
450+
.where(where)
451+
.orderBy(
452+
after ? asc(MessageTable.time_created) : desc(MessageTable.time_created),
453+
after ? asc(MessageTable.id) : desc(MessageTable.id),
460454
)
455+
.limit(input.limit + 1)
456+
.all()
457+
.pipe(Effect.orDie)
458+
if (rows.length === 0) {
459+
const row = yield* db
460+
.select({ id: SessionTable.id })
461+
.from(SessionTable)
462+
.where(eq(SessionTable.id, input.sessionID))
463+
.get()
464+
.pipe(Effect.orDie)
461465
if (!row) return yield* new NotFoundError({ message: `Session not found: ${input.sessionID}` })
462466
return {
463467
items: [] as WithParts[],

packages/tui/src/context/sync.tsx

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -604,31 +604,56 @@ export const {
604604
},
605605
async sync(sessionID: string) {
606606
if (fullSyncedSessions.has(sessionID)) return
607-
const [session, messages, todo, diff] = await Promise.all([
608-
sdk.client.session.get({ sessionID }, { throwOnError: true }),
609-
sdk.client.session.messages({ sessionID, limit: INITIAL_PAGE_SIZE }),
610-
sdk.client.session.todo({ sessionID }),
611-
sdk.client.session.diff({ sessionID }),
612-
])
613-
const olderCursor = (messages.response?.headers.get("x-next-cursor") as string | null | undefined) ?? null
614-
setStore(
615-
produce((draft) => {
616-
const match = search(draft.session, sessionID, (s) => s.id)
617-
if (match.found) draft.session[match.index] = session.data!
618-
if (!match.found) draft.session.splice(match.index, 0, session.data!)
619-
draft.todo[sessionID] = todo.data ?? []
620-
const infos: (typeof draft.message)[string] = []
621-
for (const message of messages.data ?? []) {
622-
infos.push(message.info)
623-
draft.part[message.info.id] = message.parts
624-
}
625-
draft.message[sessionID] = infos
626-
draft.session_diff[sessionID] = diff.data ?? []
627-
draft.messageOlderCursor[sessionID] = olderCursor
628-
draft.messageNewerCursor[sessionID] = null
629-
}),
630-
)
631-
if (!olderCursor) fullSyncedSessions.add(sessionID)
607+
const hydration = { messages: new Set<string>(), parts: new Set<string>() }
608+
hydratingSessions.set(sessionID, hydration)
609+
try {
610+
const [session, messages, todo, diff] = await Promise.all([
611+
sdk.client.session.get({ sessionID }, { throwOnError: true }),
612+
sdk.client.session.messages({ sessionID, limit: INITIAL_PAGE_SIZE }),
613+
sdk.client.session.todo({ sessionID }),
614+
sdk.client.session.diff({ sessionID }),
615+
])
616+
const olderCursor = (messages.response?.headers.get("x-next-cursor") as string | null | undefined) ?? null
617+
setStore(
618+
produce((draft) => {
619+
const match = search(draft.session, sessionID, (s) => s.id)
620+
if (match.found) draft.session[match.index] = session.data!
621+
if (!match.found) draft.session.splice(match.index, 0, session.data!)
622+
draft.todo[sessionID] = todo.data ?? []
623+
if (messages.data) {
624+
const existing = draft.message[sessionID] ?? []
625+
const infos: (typeof draft.message)[string] = []
626+
const hydratedIDs = new Set<string>()
627+
for (const message of messages.data) {
628+
hydratedIDs.add(message.info.id)
629+
const current = existing.find((item) => item.id === message.info.id)
630+
if (current) {
631+
infos.push(current)
632+
continue
633+
}
634+
if (hydration.messages.has(message.info.id)) continue
635+
infos.push(message.info)
636+
draft.part[message.info.id] = message.parts
637+
}
638+
for (const message of existing) {
639+
if (hydratedIDs.has(message.id)) continue
640+
infos.push(message)
641+
}
642+
while (infos.length > INITIAL_PAGE_SIZE) {
643+
const evicted = infos.shift()
644+
if (evicted) delete draft.part[evicted.id]
645+
}
646+
draft.message[sessionID] = infos
647+
draft.messageOlderCursor[sessionID] = olderCursor
648+
draft.messageNewerCursor[sessionID] = null
649+
}
650+
draft.session_diff[sessionID] = diff.data ?? []
651+
}),
652+
)
653+
if (!olderCursor) fullSyncedSessions.add(sessionID)
654+
} finally {
655+
hydratingSessions.delete(sessionID)
656+
}
632657
},
633658
async loadOlderMessages(sessionID: string) {
634659
const cursor = store.messageOlderCursor[sessionID]

0 commit comments

Comments
 (0)