From ee6d1112b0d89a842002ef65564e4776d646f2cd Mon Sep 17 00:00:00 2001 From: Lakshmi Tanmay Date: Mon, 2 Nov 2026 15:13:22 +0530 Subject: [PATCH 1/3] fix(opencode): forks from compacted threads no longer fail --- .../Adapters/OpenCode2AdapterV2.test.ts | 42 +++++++++++++++---- .../src/server/v2/adapter.ts | 13 +++--- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts index b4c319e8e9a96..f07a671e4a98f 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts @@ -3293,7 +3293,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { const kept = "msg_t3_turn_run-attempt:kept:1"; const dropped = "msg_t3_turn_run-attempt:dropped:1"; const { runtime, thread } = yield* resumed([ - out("message.list", ""), + out("message.list", { sessionID: SESSION, limit: "100", type: "user" }), reply("message.list", { data: [ { id: dropped, time: { created: 2 }, text: "second", type: "user" }, @@ -3301,6 +3301,8 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { ], cursor: {}, }), + out("message.list", { sessionID: SESSION, limit: "100", type: "synthetic" }), + reply("message.list", { data: [], cursor: {} }), out("session.revert.stage", { sessionID: SESSION, messageID: dropped, files: false }), replyData("session.revert.stage", { messageID: dropped, files: [] }), out("session.revert.commit", { sessionID: SESSION }), @@ -3362,7 +3364,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { const dropped = "msg_t3_turn_run-attempt:dropped:1"; const offers: Array = []; const { runtime, thread } = yield* resumed([ - out("message.list", ""), + out("message.list", { sessionID: SESSION, limit: "100", type: "user" }), reply("message.list", { data: [ { id: dropped, time: { created: 2 }, text: "second", type: "user" }, @@ -3370,6 +3372,8 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { ], cursor: {}, }), + out("message.list", { sessionID: SESSION, limit: "100", type: "synthetic" }), + reply("message.list", { data: [], cursor: {} }), out("session.revert.stage", { sessionID: SESSION, messageID: dropped, files: false }), replyData("session.revert.stage", { messageID: dropped, files: [] }), out("session.revert.commit", { sessionID: SESSION }), @@ -3592,11 +3596,13 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { ...(running ? [] : [ - out("message.list", ""), + out("message.list", { sessionID: SESSION, limit: "100", type: "user" }), reply("message.list", { data: [{ id: prompt, time: { created: 1 }, text: "hi", type: "user" }], cursor: {}, }), + out("message.list", { sessionID: SESSION, limit: "100", type: "synthetic" }), + reply("message.list", { data: [], cursor: {} }), out("session.revert.stage", { sessionID: SESSION, messageID: prompt, @@ -3752,7 +3758,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { promptAccepted, event("session.execution.started", { sessionID: SESSION }), event("session.execution.succeeded", { sessionID: SESSION }), - out("message.list", ""), + out("message.list", { sessionID: SESSION, limit: "100", type: "user" }), reply("message.list", { data: [ { id: dropped, time: { created: 2 }, text: "second", type: "user" }, @@ -3760,6 +3766,8 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { ], cursor: {}, }), + out("message.list", { sessionID: SESSION, limit: "100", type: "synthetic" }), + reply("message.list", { data: [], cursor: {} }), out("session.revert.stage", { sessionID: SESSION, messageID: dropped, files: false }), replyData("session.revert.stage", { messageID: dropped, files: [] }), out("session.revert.commit", { sessionID: SESSION }), @@ -3812,11 +3820,13 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { Effect.gen(function* () { const prompted = `msg_t3_turn_${SESSION}:attempt:earlier`; const { runtime, thread } = yield* resumed([ - out("message.list", ""), + out("message.list", { sessionID: SESSION, limit: "100", type: "user" }), reply("message.list", { data: [{ id: prompted, time: { created: 1 }, text: "earlier", type: "user" }], cursor: {}, }), + out("message.list", { sessionID: SESSION, limit: "100", type: "synthetic" }), + reply("message.list", { data: [], cursor: {} }), out("session.revert.stage", { sessionID: SESSION, messageID: prompted, files: false }), replyData("session.revert.stage", { messageID: prompted, files: [] }), out("session.revert.commit", { sessionID: SESSION }), @@ -3878,7 +3888,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { out("session.get", { sessionID: SESSION }), replyData("session.get", sessionInfo()), ...noOpenRequests, - out("message.list", ""), + out("message.list", { sessionID: SESSION, limit: "100", type: "user" }), reply("message.list", { data: [ { id: second, time: { created: 2 }, text: "second", type: "user" }, @@ -3886,6 +3896,8 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { ], cursor: {}, }), + out("message.list", { sessionID: SESSION, limit: "100", type: "synthetic" }), + reply("message.list", { data: [], cursor: {} }), out("session.revert.stage", { sessionID: SESSION, messageID: second, @@ -4075,8 +4087,22 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { Effect.gen(function* () { const FORK = "ses_f1484db83ffeLGtrRCFimo1H0e"; const { runtime, thread, first, second } = yield* continued([ - out("message.list", ""), - reply("message.list", continuedHistory), + out("message.list", { sessionID: SESSION, limit: "100", type: "user" }), + reply("message.list", { data: [continuedHistory.data[1]], cursor: {} }), + out("message.list", { sessionID: SESSION, limit: "100", type: "synthetic" }), + reply("message.list", { + data: [ + { id: "msg_later_report", time: { created: 3 }, text: "later", type: "synthetic" }, + ], + cursor: { next: "synthetic-page-2" }, + }), + out("message.list", { + sessionID: SESSION, + limit: "100", + type: "synthetic", + cursor: "synthetic-page-2", + }), + reply("message.list", { data: [continuedHistory.data[0]], cursor: {} }), out("session.fork", { sessionID: SESSION, before: "msg_report" }), replyData("session.fork", sessionInfo({ id: FORK })), out("session.update", { sessionID: FORK, permissions: "" }), diff --git a/packages/provider-opencode/src/server/v2/adapter.ts b/packages/provider-opencode/src/server/v2/adapter.ts index f49de61b24690..60e035745529f 100644 --- a/packages/provider-opencode/src/server/v2/adapter.ts +++ b/packages/provider-opencode/src/server/v2/adapter.ts @@ -2925,12 +2925,15 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr ); }); - // The API accepts one message type; boundaries require both user and synthetic messages. + // OpenCode 2.2 compactions no longer match the SDK schema; boundaries need only prompts. const userMessages = (sessionId: string) => - OpenCode2Client.paginate( - { sessionID: Session.ID.make(sessionId), limit: 100 }, - client.message.list, - ).pipe( + Stream.fromIterable(["user", "synthetic"] as const).pipe( + Stream.flatMap((type) => + OpenCode2Client.paginate( + { sessionID: Session.ID.make(sessionId), limit: 100, type }, + client.message.list, + ), + ), Stream.runCollect, Effect.map( (messages) => From d8cce86e8796bb6019cd1617492bf34c8895827c Mon Sep 17 00:00:00 2001 From: Lakshmi Tanmay Date: Mon, 2 Nov 2026 15:20:10 +0530 Subject: [PATCH 2/3] test(opencode): replay filtered fork and rollback boundary queries --- .../fixtures/opencode2_fork/opencode_transcript.ndjson | 6 ++++-- .../fixtures/opencode2_revert/opencode_transcript.ndjson | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_fork/opencode_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_fork/opencode_transcript.ndjson index 57cbd14853460..a483f04813b31 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_fork/opencode_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_fork/opencode_transcript.ndjson @@ -41,10 +41,12 @@ {"type": "emit_inbound", "label": "session.step.ended.2", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7b2461001qEeIO2dKdf1y1Y", "created": 1790657111137, "type": "session.step.ended", "location": {"directory": ""}, "data": {"sessionID": "ses_f1484ea8affe5aTo8CcgbGIszj", "assistantMessageID": "msg_0eb7b1fb30011jHuDOQ5YiY6eH", "finish": "stop", "rawFinish": "stop", "cost": 0, "tokens": {"input": 16, "output": 3, "reasoning": 0, "cache": {"read": 9327, "write": 0}}, "snapshot": "99482b54df6361365fa0ed7363ccf07edaa7fc93", "files": []}, "durable": {"aggregateID": "ses_f1484ea8affe5aTo8CcgbGIszj", "seq": 20, "version": 1}}}} {"type": "emit_inbound", "label": "session.usage.updated.2", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7b2465001TtO12Ofsv2KZTq", "created": 1790657111141, "type": "session.usage.updated", "data": {"sessionID": "ses_f1484ea8affe5aTo8CcgbGIszj", "cost": 0, "tokens": {"input": 8759, "output": 4, "reasoning": 99, "cache": {"read": 9813, "write": 0}}}}}} {"type": "emit_inbound", "label": "session.execution.succeeded.2", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7b24660011TVz6Q64I8aAQ7", "created": 1790657111142, "type": "session.execution.succeeded", "data": {"sessionID": "ses_f1484ea8affe5aTo8CcgbGIszj"}, "durable": {"aggregateID": "ses_f1484ea8affe5aTo8CcgbGIszj", "seq": 21, "version": 1}}}} -{"type": "expect_outbound", "label": "message.list", "frame": {"type": "message.list", "input": ""}} +{"type": "expect_outbound", "label": "message.list", "frame": {"type": "message.list", "input": {"sessionID": "ses_f1484ea8affe5aTo8CcgbGIszj", "limit": "100", "type": "user"}}} {"type": "emit_inbound", "label": "message.list.response", "frame": {"type": "sdk.response", "operation": "message.list", "data": {"data": [{"id": "msg_0eb7b1f9f001g95Bduh3bi8syS", "time": {"created": 1790657109932}, "text": "Reply exactly TWO.", "type": "user"}, {"id": "msg_0eb7b1597001e14VBV3nmQYdFS", "time": {"created": 1790657107378}, "text": "Reply exactly ONE.", "type": "user"}], "cursor": {"next": "cursor-after-first-page"}}}} -{"type": "expect_outbound", "label": "message.list.2", "frame": {"type": "message.list", "input": ""}} +{"type": "expect_outbound", "label": "message.list.2", "frame": {"type": "message.list", "input": {"sessionID": "ses_f1484ea8affe5aTo8CcgbGIszj", "limit": "100", "type": "user", "cursor": "cursor-after-first-page"}}} {"type": "emit_inbound", "label": "message.list.response.2", "frame": {"type": "sdk.response", "operation": "message.list", "data": {"data": [], "cursor": {}}}} +{"type": "expect_outbound", "label": "message.list.synthetic", "frame": {"type": "message.list", "input": {"sessionID": "ses_f1484ea8affe5aTo8CcgbGIszj", "limit": "100", "type": "synthetic"}}} +{"type": "emit_inbound", "label": "message.list.synthetic.response", "frame": {"type": "sdk.response", "operation": "message.list", "data": {"data": [], "cursor": {}}}} {"type": "expect_outbound", "label": "session.fork", "frame": {"type": "session.fork", "input": {"sessionID": "ses_f1484ea8affe5aTo8CcgbGIszj", "before": "msg_0eb7b1f9f001g95Bduh3bi8syS"}}} {"type": "emit_inbound", "label": "session.fork.response", "frame": {"type": "sdk.response", "operation": "session.fork", "data": {"data": {"id": "ses_f1484db83ffeLGtrRCFimo1H0e", "fork": {"sessionID": "ses_f1484ea8affe5aTo8CcgbGIszj", "boundary": {"type": "before", "messageID": "msg_0eb7b1f9f001g95Bduh3bi8syS"}}, "projectID": "global", "model": {"id": "big-pickle", "providerID": "opencode", "variant": "default"}, "cost": 0, "tokens": {"input": 0, "output": 0, "reasoning": 0, "cache": {"read": 0, "write": 0}}, "time": {"created": 1790657111165, "updated": 1790657111165}, "title": "spike fork (fork #1)", "location": {"directory": ""}, "permissions": [{"action": "*", "resource": "*", "effect": "allow"}, {"action": "subagent", "resource": "*", "effect": "deny"}]}}}} {"type": "expect_outbound", "label": "session.update", "frame": {"type": "session.update", "input": {"sessionID": "ses_f1484db83ffeLGtrRCFimo1H0e", "permissions": [{"action": "*", "resource": "*", "effect": "allow"}, {"action": "t3-code-*", "resource": "*", "effect": "deny"}, {"action": "t3-code-thread_opencode2-fork_target_*", "resource": "*", "effect": "allow"}]}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_revert/opencode_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_revert/opencode_transcript.ndjson index bbf51a68926f6..4c78cf6ba426c 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_revert/opencode_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_revert/opencode_transcript.ndjson @@ -51,10 +51,12 @@ {"type": "emit_inbound", "label": "session.step.ended.4", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7b5328001InlyUFAKg7pkcR", "created": 1790657123112, "type": "session.step.ended", "location": {"directory": ""}, "data": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "assistantMessageID": "msg_0eb7b4fc1001jd3wUmiUPduL54", "finish": "stop", "rawFinish": "stop", "cost": 0, "tokens": {"input": 124, "output": 4, "reasoning": 0, "cache": {"read": 9216, "write": 0}}, "snapshot": "aff42fe121ca6ba7ff4cd8ff03b92c4653937c1e", "files": []}, "durable": {"aggregateID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "seq": 32, "version": 1}}}} {"type": "emit_inbound", "label": "session.usage.updated.4", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7b532c001iSqv9ja5oJJh5r", "created": 1790657123116, "type": "session.usage.updated", "data": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "cost": 0, "tokens": {"input": 9018, "output": 87, "reasoning": 0, "cache": {"read": 28032, "write": 0}}}}}} {"type": "emit_inbound", "label": "session.execution.succeeded.2", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7b532e001ZS3mBEHiZ7I7cn", "created": 1790657123118, "type": "session.execution.succeeded", "data": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus"}, "durable": {"aggregateID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "seq": 33, "version": 1}}}} -{"type": "expect_outbound", "label": "message.list.users.1", "frame": {"type": "message.list", "input": ""}} +{"type": "expect_outbound", "label": "message.list.users.1", "frame": {"type": "message.list", "input": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "limit": "100", "type": "user"}}} {"type": "emit_inbound", "label": "message.list.users.response.1", "frame": {"type": "sdk.response", "operation": "message.list", "data": {"data": [{"id": "msg_0eb7b4b79001y4nhrY7WvzIH4b", "time": {"created": 1790657121160}, "text": "Overwrite reverted.txt so it contains the word BETA using the write tool, then reply DONE.", "type": "user"}, {"id": "msg_0eb7b4129001B7iaT1f3yLkR28", "time": {"created": 1790657118526}, "text": "Create a file named reverted.txt containing the word ALPHA using the write tool, then reply DONE.", "type": "user"}], "cursor": {"next": "cursor-after-first-page"}}}} -{"type": "expect_outbound", "label": "message.list.users.1b", "frame": {"type": "message.list", "input": ""}} +{"type": "expect_outbound", "label": "message.list.users.1b", "frame": {"type": "message.list", "input": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "limit": "100", "type": "user", "cursor": "cursor-after-first-page"}}} {"type": "emit_inbound", "label": "message.list.users.response.1b", "frame": {"type": "sdk.response", "operation": "message.list", "data": {"data": [], "cursor": {}}}} +{"type": "expect_outbound", "label": "message.list.synthetic", "frame": {"type": "message.list", "input": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "limit": "100", "type": "synthetic"}}} +{"type": "emit_inbound", "label": "message.list.synthetic.response", "frame": {"type": "sdk.response", "operation": "message.list", "data": {"data": [], "cursor": {}}}} {"type": "expect_outbound", "label": "session.revert.stage.1", "frame": {"type": "session.revert.stage", "input": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "messageID": "msg_0eb7b4b79001y4nhrY7WvzIH4b", "files": false}}} {"type": "emit_inbound", "label": "session.revert.stage.response.1", "frame": {"type": "sdk.response", "operation": "session.revert.stage", "data": {"data": {"messageID": "msg_0eb7b4b79001y4nhrY7WvzIH4b", "snapshot": "aff42fe121ca6ba7ff4cd8ff03b92c4653937c1e", "files": []}}}} {"type": "emit_inbound", "label": "session.revert.staged", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7b5373001KgvYcl3wXRr4ok", "created": 1790657123187, "type": "session.revert.staged", "location": {"directory": ""}, "data": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "revert": {"messageID": "msg_0eb7b4b79001y4nhrY7WvzIH4b", "snapshot": "aff42fe121ca6ba7ff4cd8ff03b92c4653937c1e", "files": []}}, "durable": {"aggregateID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "seq": 34, "version": 1}}}} From f5d71667095d293d06b64c480da30724dc233aa1 Mon Sep 17 00:00:00 2001 From: Lakshmi Tanmay Date: Mon, 2 Nov 2026 15:29:28 +0530 Subject: [PATCH 3/3] fix(opencode): preserve rollback validation before cutting history --- .../Adapters/OpenCode2AdapterV2.test.ts | 73 ++++++++++++++----- .../opencode_transcript.ndjson | 6 +- .../src/server/v2/adapter.ts | 14 ++-- 3 files changed, 66 insertions(+), 27 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts index f07a671e4a98f..ecb05a5a02a15 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts @@ -3288,12 +3288,61 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { }).pipe(Effect.scoped), ); + it.effect("rejects unreadable rollback history before cutting the native session", () => + Effect.gen(function* () { + const { runtime, thread } = yield* resumed([ + out("message.list", { sessionID: SESSION, limit: "100", type: undefined }), + reply("message.list", { + data: [ + { id: "msg_kept", time: { created: 1 }, text: "first", type: "user" }, + { + id: "msg_compaction", + time: { created: 2 }, + type: "compaction", + status: "completed", + reason: "auto", + summary: "First turn summary", + }, + { id: "msg_dropped", time: { created: 3 }, text: "second", type: "user" }, + ], + cursor: {}, + }), + ]); + const now = yield* DateTime.now; + const turns = ["msg_kept", "msg_dropped"].map((nativeId, index) => ({ + id: ProviderTurnId.make(`provider-turn:rollback-history:${index}`), + providerThreadId: thread.id, + nodeId: NodeId.make(`node:rollback-history:${index}`), + runAttemptId: RunAttemptId.make(`attempt:rollback-history:${index}`), + nativeTurnRef: { driver: OPENCODE_PROVIDER, nativeId, strength: "weak" as const }, + ordinal: index + 1, + status: "completed" as const, + startedAt: now, + completedAt: now, + })); + const failure = yield* runtime + .rollbackThread({ + providerThread: thread, + target: { + type: "provider_turn", + checkpointId: CheckpointId.make("checkpoint:rollback-history"), + appRunOrdinal: 1, + providerTurn: turns[0]!, + }, + providerThreadTurns: turns, + }) + .pipe(Effect.flip); + assert.equal(failure._tag, "ProviderAdapterRollbackThreadError"); + assert.include(Cause.pretty(Cause.fail(failure)), '["recent"]'); + }).pipe(Effect.scoped), + ); + it.effect("clears a staged revert whose commit failed, before the next prompt too", () => Effect.gen(function* () { const kept = "msg_t3_turn_run-attempt:kept:1"; const dropped = "msg_t3_turn_run-attempt:dropped:1"; const { runtime, thread } = yield* resumed([ - out("message.list", { sessionID: SESSION, limit: "100", type: "user" }), + out("message.list", ""), reply("message.list", { data: [ { id: dropped, time: { created: 2 }, text: "second", type: "user" }, @@ -3301,8 +3350,6 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { ], cursor: {}, }), - out("message.list", { sessionID: SESSION, limit: "100", type: "synthetic" }), - reply("message.list", { data: [], cursor: {} }), out("session.revert.stage", { sessionID: SESSION, messageID: dropped, files: false }), replyData("session.revert.stage", { messageID: dropped, files: [] }), out("session.revert.commit", { sessionID: SESSION }), @@ -3364,7 +3411,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { const dropped = "msg_t3_turn_run-attempt:dropped:1"; const offers: Array = []; const { runtime, thread } = yield* resumed([ - out("message.list", { sessionID: SESSION, limit: "100", type: "user" }), + out("message.list", ""), reply("message.list", { data: [ { id: dropped, time: { created: 2 }, text: "second", type: "user" }, @@ -3372,8 +3419,6 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { ], cursor: {}, }), - out("message.list", { sessionID: SESSION, limit: "100", type: "synthetic" }), - reply("message.list", { data: [], cursor: {} }), out("session.revert.stage", { sessionID: SESSION, messageID: dropped, files: false }), replyData("session.revert.stage", { messageID: dropped, files: [] }), out("session.revert.commit", { sessionID: SESSION }), @@ -3596,13 +3641,11 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { ...(running ? [] : [ - out("message.list", { sessionID: SESSION, limit: "100", type: "user" }), + out("message.list", ""), reply("message.list", { data: [{ id: prompt, time: { created: 1 }, text: "hi", type: "user" }], cursor: {}, }), - out("message.list", { sessionID: SESSION, limit: "100", type: "synthetic" }), - reply("message.list", { data: [], cursor: {} }), out("session.revert.stage", { sessionID: SESSION, messageID: prompt, @@ -3758,7 +3801,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { promptAccepted, event("session.execution.started", { sessionID: SESSION }), event("session.execution.succeeded", { sessionID: SESSION }), - out("message.list", { sessionID: SESSION, limit: "100", type: "user" }), + out("message.list", ""), reply("message.list", { data: [ { id: dropped, time: { created: 2 }, text: "second", type: "user" }, @@ -3766,8 +3809,6 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { ], cursor: {}, }), - out("message.list", { sessionID: SESSION, limit: "100", type: "synthetic" }), - reply("message.list", { data: [], cursor: {} }), out("session.revert.stage", { sessionID: SESSION, messageID: dropped, files: false }), replyData("session.revert.stage", { messageID: dropped, files: [] }), out("session.revert.commit", { sessionID: SESSION }), @@ -3820,13 +3861,11 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { Effect.gen(function* () { const prompted = `msg_t3_turn_${SESSION}:attempt:earlier`; const { runtime, thread } = yield* resumed([ - out("message.list", { sessionID: SESSION, limit: "100", type: "user" }), + out("message.list", ""), reply("message.list", { data: [{ id: prompted, time: { created: 1 }, text: "earlier", type: "user" }], cursor: {}, }), - out("message.list", { sessionID: SESSION, limit: "100", type: "synthetic" }), - reply("message.list", { data: [], cursor: {} }), out("session.revert.stage", { sessionID: SESSION, messageID: prompted, files: false }), replyData("session.revert.stage", { messageID: prompted, files: [] }), out("session.revert.commit", { sessionID: SESSION }), @@ -3888,7 +3927,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { out("session.get", { sessionID: SESSION }), replyData("session.get", sessionInfo()), ...noOpenRequests, - out("message.list", { sessionID: SESSION, limit: "100", type: "user" }), + out("message.list", ""), reply("message.list", { data: [ { id: second, time: { created: 2 }, text: "second", type: "user" }, @@ -3896,8 +3935,6 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { ], cursor: {}, }), - out("message.list", { sessionID: SESSION, limit: "100", type: "synthetic" }), - reply("message.list", { data: [], cursor: {} }), out("session.revert.stage", { sessionID: SESSION, messageID: second, diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_revert/opencode_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_revert/opencode_transcript.ndjson index 4c78cf6ba426c..bbf51a68926f6 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_revert/opencode_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_revert/opencode_transcript.ndjson @@ -51,12 +51,10 @@ {"type": "emit_inbound", "label": "session.step.ended.4", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7b5328001InlyUFAKg7pkcR", "created": 1790657123112, "type": "session.step.ended", "location": {"directory": ""}, "data": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "assistantMessageID": "msg_0eb7b4fc1001jd3wUmiUPduL54", "finish": "stop", "rawFinish": "stop", "cost": 0, "tokens": {"input": 124, "output": 4, "reasoning": 0, "cache": {"read": 9216, "write": 0}}, "snapshot": "aff42fe121ca6ba7ff4cd8ff03b92c4653937c1e", "files": []}, "durable": {"aggregateID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "seq": 32, "version": 1}}}} {"type": "emit_inbound", "label": "session.usage.updated.4", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7b532c001iSqv9ja5oJJh5r", "created": 1790657123116, "type": "session.usage.updated", "data": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "cost": 0, "tokens": {"input": 9018, "output": 87, "reasoning": 0, "cache": {"read": 28032, "write": 0}}}}}} {"type": "emit_inbound", "label": "session.execution.succeeded.2", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7b532e001ZS3mBEHiZ7I7cn", "created": 1790657123118, "type": "session.execution.succeeded", "data": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus"}, "durable": {"aggregateID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "seq": 33, "version": 1}}}} -{"type": "expect_outbound", "label": "message.list.users.1", "frame": {"type": "message.list", "input": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "limit": "100", "type": "user"}}} +{"type": "expect_outbound", "label": "message.list.users.1", "frame": {"type": "message.list", "input": ""}} {"type": "emit_inbound", "label": "message.list.users.response.1", "frame": {"type": "sdk.response", "operation": "message.list", "data": {"data": [{"id": "msg_0eb7b4b79001y4nhrY7WvzIH4b", "time": {"created": 1790657121160}, "text": "Overwrite reverted.txt so it contains the word BETA using the write tool, then reply DONE.", "type": "user"}, {"id": "msg_0eb7b4129001B7iaT1f3yLkR28", "time": {"created": 1790657118526}, "text": "Create a file named reverted.txt containing the word ALPHA using the write tool, then reply DONE.", "type": "user"}], "cursor": {"next": "cursor-after-first-page"}}}} -{"type": "expect_outbound", "label": "message.list.users.1b", "frame": {"type": "message.list", "input": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "limit": "100", "type": "user", "cursor": "cursor-after-first-page"}}} +{"type": "expect_outbound", "label": "message.list.users.1b", "frame": {"type": "message.list", "input": ""}} {"type": "emit_inbound", "label": "message.list.users.response.1b", "frame": {"type": "sdk.response", "operation": "message.list", "data": {"data": [], "cursor": {}}}} -{"type": "expect_outbound", "label": "message.list.synthetic", "frame": {"type": "message.list", "input": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "limit": "100", "type": "synthetic"}}} -{"type": "emit_inbound", "label": "message.list.synthetic.response", "frame": {"type": "sdk.response", "operation": "message.list", "data": {"data": [], "cursor": {}}}} {"type": "expect_outbound", "label": "session.revert.stage.1", "frame": {"type": "session.revert.stage", "input": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "messageID": "msg_0eb7b4b79001y4nhrY7WvzIH4b", "files": false}}} {"type": "emit_inbound", "label": "session.revert.stage.response.1", "frame": {"type": "sdk.response", "operation": "session.revert.stage", "data": {"data": {"messageID": "msg_0eb7b4b79001y4nhrY7WvzIH4b", "snapshot": "aff42fe121ca6ba7ff4cd8ff03b92c4653937c1e", "files": []}}}} {"type": "emit_inbound", "label": "session.revert.staged", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7b5373001KgvYcl3wXRr4ok", "created": 1790657123187, "type": "session.revert.staged", "location": {"directory": ""}, "data": {"sessionID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "revert": {"messageID": "msg_0eb7b4b79001y4nhrY7WvzIH4b", "snapshot": "aff42fe121ca6ba7ff4cd8ff03b92c4653937c1e", "files": []}}, "durable": {"aggregateID": "ses_f1484bef1ffeGfSJ1e2FXLCaus", "seq": 34, "version": 1}}}} diff --git a/packages/provider-opencode/src/server/v2/adapter.ts b/packages/provider-opencode/src/server/v2/adapter.ts index 60e035745529f..40fdb20ff4d8d 100644 --- a/packages/provider-opencode/src/server/v2/adapter.ts +++ b/packages/provider-opencode/src/server/v2/adapter.ts @@ -2925,12 +2925,16 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr ); }); - // OpenCode 2.2 compactions no longer match the SDK schema; boundaries need only prompts. - const userMessages = (sessionId: string) => - Stream.fromIterable(["user", "synthetic"] as const).pipe( + // Rollback must validate full history before native mutation; forks only need prompts. + const userMessages = (sessionId: string, types?: ReadonlyArray<"user" | "synthetic">) => + Stream.fromIterable(types ?? [undefined]).pipe( Stream.flatMap((type) => OpenCode2Client.paginate( - { sessionID: Session.ID.make(sessionId), limit: 100, type }, + { + sessionID: Session.ID.make(sessionId), + limit: 100, + ...(type === undefined ? {} : { type }), + }, client.message.list, ), ), @@ -4011,7 +4015,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr forkInput.sourceProviderTurns ?? [], source.id, selected, - yield* userMessages(sourceId), + yield* userMessages(sourceId, ["user", "synthetic"]), ); const forked = yield* client.session.fork({ sessionID: Session.ID.make(sourceId),