diff --git a/packages/opencode/src/kilocode/session/fork.ts b/packages/opencode/src/kilocode/session/fork.ts index 9c64afd28c5..abf289879d1 100644 --- a/packages/opencode/src/kilocode/session/fork.ts +++ b/packages/opencode/src/kilocode/session/fork.ts @@ -1,4 +1,4 @@ -import { Effect } from "effect" +import { Effect, Schema } from "effect" import type { Session } from "@/session/session" import { MessageV2 } from "@/session/message-v2" import { MessageID, PartID, SessionID } from "@/session/schema" @@ -49,14 +49,7 @@ function mapRecord(value: Record | undefined, map: Map) { return [...map].reduce( - (text, [source, target]) => - source === target - ? text - : text - .replaceAll(source, target) - .replaceAll(`task id="${source}"`, `task id="${target}"`) - .replaceAll(`task_id="${source}"`, `task_id="${target}"`) - .replaceAll(`task_id: ${source}`, `task_id: ${target}`), + (text, [source, target]) => (source === target ? text : text.replaceAll(source, target)), value, ) } @@ -154,6 +147,7 @@ export function remapChildren(input: { for (const ref of refs) { if (map.has(ref.child)) continue + if (!Schema.is(SessionID)(ref.child)) continue const source = yield* input.ops.get(SessionID.make(ref.child)).pipe(Effect.orElseSucceed(() => undefined)) if (!source) continue const target = yield* copy({ source, parentID: input.sessionID, ops: input.ops }) diff --git a/packages/opencode/test/kilocode/session-fork-remap.test.ts b/packages/opencode/test/kilocode/session-fork-remap.test.ts index 6e3ebe6a577..9cd1d9d6cfb 100644 --- a/packages/opencode/test/kilocode/session-fork-remap.test.ts +++ b/packages/opencode/test/kilocode/session-fork-remap.test.ts @@ -388,6 +388,43 @@ describe("Session.fork task children", () => { { timeout: 30000 }, ) + test( + "ignores malformed task references while forking", + async () => { + await using tmp = await tmpdir({ git: true }) + await instance({ + directory: tmp.path, + fn: async () => { + const parent = await sessions.create({ title: "parent" }) + const user = await userMsg(parent.id) + const assistant = await asstMsg(parent.id, user) + await sessions.updatePart({ + id: PartID.ascending(), + messageID: assistant, + sessionID: parent.id, + type: "tool", + callID: "call_malformed", + tool: "task", + state: { + status: "error", + input: { task_id: "not-a-session-id" }, + error: "Cannot resume the malformed task reference", + time: { start: Date.now(), end: Date.now() }, + }, + } as MessageV2.ToolPart) + + const forked = await Session.fork({ sessionID: parent.id }) + const msgs = await sessions.messages({ sessionID: forked.id }) + const tool = msgs.flatMap((msg) => msg.parts).find((part) => part.type === "tool") as MessageV2.ToolPart + expect(tool.state.status).toBe("error") + if (tool.state.status !== "error") throw new Error("expected malformed task error") + expect(tool.state.input.task_id).toBe("not-a-session-id") + }, + }) + }, + { timeout: 30000 }, + ) + test( "preserves workspace sync event sequencing in the atomic copy", async () => {