mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-31 02:25:18 +00:00
fix(app): reconcile v2 session projections
This commit is contained in:
parent
c849b1f0f7
commit
6a5b8af665
5 changed files with 67 additions and 3 deletions
|
|
@ -45,6 +45,10 @@ describe("timeline fixture validation", () => {
|
|||
expect(first.payload.id).toMatch(/^evt_timeline_\d{4}$/)
|
||||
expect(Number(second.payload.id.slice(-4))).toBe(Number(first.payload.id.slice(-4)) + 1)
|
||||
})
|
||||
|
||||
test("uses the projected tool ID as its call ID", () => {
|
||||
expect(toolPart("call_1", "read", "running", {})).toMatchObject({ id: "call_1", callID: "call_1" })
|
||||
})
|
||||
})
|
||||
|
||||
if (false) {
|
||||
|
|
|
|||
|
|
@ -265,6 +265,34 @@ describe("server session", () => {
|
|||
|
||||
expect(requests).toEqual([{ sessionID: "root", limit: 20, order: "desc" }])
|
||||
expect(store.data.session_message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
|
||||
expect(store.history.more("root")).toBe(false)
|
||||
})
|
||||
|
||||
test("replaces stale current projections on complete refreshes", async () => {
|
||||
const first = { id: "msg_1", type: "user", text: "first", time: { created: 1 } } as const
|
||||
const second = { id: "msg_2", type: "user", text: "second", time: { created: 2 } } as const
|
||||
const pages = [
|
||||
{ data: [first], cursor: { previous: null, next: null } },
|
||||
{ data: [second], cursor: { previous: null, next: null } },
|
||||
{ data: [], cursor: { previous: null, next: null } },
|
||||
]
|
||||
const messageApi = {
|
||||
list: async () => pages.shift()!,
|
||||
} as unknown as MessageApi
|
||||
const sessionApi = { get: async () => session("root") } as unknown as SessionApi
|
||||
const store = createServerSession({} as OpencodeClient, sessionApi, messageApi)
|
||||
store.remember(session("root"))
|
||||
|
||||
await store.sync("root")
|
||||
expect(store.data.session_message.root.map((message) => message.id)).toEqual([first.id])
|
||||
|
||||
await store.sync("root", { force: true })
|
||||
expect(store.data.session_message.root.map((message) => message.id)).toEqual([second.id])
|
||||
expect(store.data.message.root.map((message) => message.id)).toEqual([second.id])
|
||||
|
||||
await store.sync("root", { force: true })
|
||||
expect(store.data.session_message.root).toEqual([])
|
||||
expect(store.data.message.root).toEqual([])
|
||||
})
|
||||
|
||||
test("extends a current page to include the user for split assistant turns", async () => {
|
||||
|
|
|
|||
|
|
@ -563,7 +563,7 @@ export function createServerSession(
|
|||
sourceMode: before ? ("older" as const) : ("latest" as const),
|
||||
projectSource: true,
|
||||
cursor: response.cursor.next ?? undefined,
|
||||
complete: response.data.length === 0,
|
||||
complete: !response.cursor.next,
|
||||
}
|
||||
}
|
||||
const response = await (options?.retry ?? retry)(() => {
|
||||
|
|
@ -683,7 +683,14 @@ export function createServerSession(
|
|||
? (() => {
|
||||
const incoming = new Map(page.source.map((message) => [message.id, message]))
|
||||
const existing = data.session_message[sessionID] ?? []
|
||||
const current = existing.filter((message) => !incoming.has(message.id))
|
||||
const boundary = Math.min(...page.source.map((message) => message.time.created))
|
||||
const current = existing.filter(
|
||||
(message) =>
|
||||
!incoming.has(message.id) &&
|
||||
(page.sourceMode === "older" ||
|
||||
load?.touchedSource.has(message.id) ||
|
||||
(!page.complete && message.time.created < boundary)),
|
||||
)
|
||||
const live = new Map(existing.map((message) => [message.id, message]))
|
||||
return (page.sourceMode === "older" ? [...page.source, ...current] : [...current, ...page.source]).map(
|
||||
(message) => (load?.touchedSource.has(message.id) ? (live.get(message.id) ?? message) : message),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,31 @@ import type { SessionMessageInfo } from "@opencode-ai/client/promise"
|
|||
import { normalizeSessionMessages } from "./session-message"
|
||||
|
||||
describe("normalizeSessionMessages", () => {
|
||||
test("keeps attachments without inventing an empty text part", () => {
|
||||
const source = [
|
||||
{
|
||||
id: "msg_1",
|
||||
type: "user",
|
||||
text: "",
|
||||
files: [
|
||||
{
|
||||
data: "aGVsbG8=",
|
||||
mime: "text/plain",
|
||||
name: "note.txt",
|
||||
source: { type: "inline" },
|
||||
},
|
||||
],
|
||||
agents: [{ name: "review" }],
|
||||
time: { created: 1 },
|
||||
},
|
||||
] satisfies SessionMessageInfo[]
|
||||
|
||||
const result = normalizeSessionMessages("ses_1", source)
|
||||
|
||||
expect(result.messages).toHaveLength(1)
|
||||
expect(result.parts.get("msg_1")?.map((part) => part.type)).toEqual(["file", "agent"])
|
||||
})
|
||||
|
||||
test("projects current turns into stable legacy rendering records", () => {
|
||||
const source = [
|
||||
{ id: "msg_1", type: "agent-switched", agent: "build", time: { created: 1 } },
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ function userMessage(
|
|||
|
||||
function userParts(sessionID: string, message: SessionMessageUser): Part[] {
|
||||
return [
|
||||
textPart(sessionID, message.id, 0, message.text),
|
||||
...(message.text ? [textPart(sessionID, message.id, 0, message.text)] : []),
|
||||
...(message.files ?? []).map(
|
||||
(file, index): FilePart => ({
|
||||
id: `${message.id}:file:${index}`,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue