Skip to content

fix(opencode): show compaction progress with OpenCode 2.1+ - #26206

Open
juliusmarminge wants to merge 3 commits into
mainfrom
fix/issue-26193
Open

juliusmarminge wants to merge 3 commits into
mainfrom
fix/issue-26193

Conversation

@juliusmarminge

Copy link
Copy Markdown
Member

Problem

OpenCode 2.1+ no longer sends data.recent on session.compaction.started and session.compaction.ended. T3 bundles the 2.0.28 OpenCode protocol, which still requires it, so both live /api/event frames fail decoding and are silently skipped. The adapter never sees them, and an OpenCode thread never shows "Compacting context" or "Context compacted". Reproduced by driving real opencode serve 2.1.4 and 2.2.0 through T3's OpenCode2Client and running /compact (client.session.compact): both events appear on the wire, and T3 delivers neither. On 2.0.28 it delivers both.

Change

When a frame fails the normal decode, and it is a compaction start or end event with no recent key, OpenCode2Client decodes it again with recent: "". T3 never reads that field. Frames that decode today, including 2.0.x frames carrying recent, keep the original path. Any other failure still falls through to the existing undecodable handling. No dependency bump. Message-history decoding is untouched.

Scope and approval

Fixes #26193

Triaged via the automated issue flow.

Verification

  • vp test run src/server/v2/OpenCode2Client.test.ts in packages/provider-opencode:
    • The new test delivers compaction events from OpenCode 2.1 feeds the two frames captured from a real 2.1.4 server through OpenCode2Client.layer.
    • On main it fails with expected [] to deeply equal [ 'session.compaction.started', 'session.compaction.ended' ]. With this change, 2/2 pass.
  • vp run check:changed: format, lint, knip, typecheck (10 packages) and tests all pass.
  • Runtime transcript: a scratch driver using OpenCode2Client.layer + FetchHttpClient against real @opencode/cli servers. It creates a session, sends one prompt, then runs client.session.compact, while curl captures the raw SSE stream.
OpenCode Wire (started / ended, recent present?) T3 delivered at main b88da91397 T3 delivered at 6aae4aa4f4
2.0.28 1 / 1, yes 1 / 1 — (unchanged path)
2.1.4 1 / 1, no 0 / 0 1 / 1
2.2.0 1 / 1, no 0 / 0 1 / 1

There is no visible surface beyond the existing compaction item, so this PR has no captures. Not checked: the adapter rendering of the compaction item in a running T3 thread. That adapter branch is unchanged and is reached once the event decodes.

Runtime transcript
# #26193 runtime transcript
Driver: driver.ts (T3's OpenCode2Client.layer + FetchHttpClient) against a real 'opencode serve' from @opencode/cli; create session, prompt claude-haiku-5-5, then client.session.compact. Raw /api/event SSE captured in parallel with curl. Synthetic scratch project; no real T3 data.

## OpenCode 2.1.4 @ main-b88da91397
wire: started=1 ended=1; frames carrying data.recent=0
T3 decoded: started=0 ended=0
started frame on wire:
"type":"session.compaction.started","location":{"directory":"/tmp/issue-lead-26193-r1/oc/proj"},"data":{"sessionID":"ses_e80415922ffetjml4RUW7tRPQL","reason":"manual","inputID":"msg_17fbf08b10019CxokkaEaAa

## OpenCode 2.2.0 @ main-b88da91397
wire: started=1 ended=1; frames carrying data.recent=0
T3 decoded: started=0 ended=0
started frame on wire:
"type":"session.compaction.started","location":{"directory":"/tmp/issue-lead-26193-r1/oc/proj2.2.0"},"data":{"sessionID":"ses_e803eadaeffejfXv1twitAWhI1","reason":"manual","inputID":"msg_17fc1b70b001k1g7bS

## OpenCode 2.0.28 @ main-b88da91397
wire: started=1 ended=1; frames carrying data.recent=2
T3 decoded: started=1 ended=1
started frame on wire:
"type":"session.compaction.started","location":{"directory":"/tmp/issue-lead-26193-r1/oc/proj2.0.28"},"data":{"sessionID":"ses_e803eadb0ffeOa2YNsXs5w08MP","reason":"manual","recent":"","inputID":"msg_17fc1

## OpenCode 2.1.4 @ fix-6aae4aa4f4
wire: started=1 ended=1; frames carrying data.recent=0
T3 decoded: started=1 ended=1
started frame on wire:
"type":"session.compaction.started","location":{"directory":"/tmp/issue-lead-26193-r1/oc/proj2.1.4"},"data":{"sessionID":"ses_e8039023effep7TFg7SOOYbckq","reason":"manual","inputID":"msg_17fc760f6001jb3s5c

## OpenCode 2.2.0 @ fix-6aae4aa4f4
wire: started=1 ended=1; frames carrying data.recent=0
T3 decoded: started=1 ended=1
started frame on wire:
"type":"session.compaction.started","location":{"directory":"/tmp/issue-lead-26193-r1/oc/proj2.2.0"},"data":{"sessionID":"ses_e8039022dffevNL5eo9BNOq02Q","reason":"manual","inputID":"msg_17fc75fa2001tM6S0o

## Decoder check of the 2.1.4 frames on main
session.compaction.started data keys: sessionID,reason,inputID | as sent: REJECTED SchemaError(Missing key   at ["data"]["recent"] Expected a string matching template literal parts | with recent: ok
session.compaction.ended data keys: sessionID,reason,model,text,tokens,cost | as sent: REJECTED SchemaError(Missing key   at ["data"]["recent"] Expected a string matching template literal parts | with recent: ok
Scratch driver (not committed)
// Scratch repro for #26193: drives a real OpenCode 2.x server through T3's OpenCode2Client
// and prints every event T3's decoder hands to the adapter. Run from packages/provider-opencode.
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as Stream from "effect/Stream";
import * as FetchHttpClient from "effect/http/FetchHttpClient";
import { AbsolutePath, Location, Session } from "@opencode/client/effect";
import * as OpenCode2Client from "./src/server/v2/OpenCode2Client.ts";

const [baseUrl, password, directory] = process.argv.slice(2);

const program = Effect.gen(function* () {
  const opencode = yield* OpenCode2Client.OpenCode2Client;
  const { client, events } = yield* opencode.connect({ baseUrl, password });
  const stream = yield* events;
  const seen: string[] = [];
  const fiber = yield* stream.pipe(
    Stream.runForEach((event) =>
      Effect.sync(() => {
        if (event.type.startsWith("session.") || event.type.startsWith("unreadable.")) {
          seen.push(event.type);
          console.log("T3 decoded:", event.type);
        }
      }),
    ),
    Effect.forkChild,
  );
  const created = yield* client.session.create({
    location: Location.PublicRef.make({ directory: AbsolutePath.make(directory) }),
    model: { providerID: "anthropic", id: "claude-haiku-5-5" } as never,
  });
  console.log("session", created.id);
  const sessionID = Session.ID.make(created.id);
  yield* client.session.prompt({ sessionID, text: "Reply with the single word: ready." } as never);
  yield* Effect.sleep("25 seconds");
  console.log("--- sending /compact");
  yield* client.session.compact({ sessionID } as never);
  yield* Effect.sleep("60 seconds");
  yield* Fiber.interrupt(fiber);
  console.log(
    "compaction events T3 decoded:",
    seen.filter((type) => type.includes("compaction")),
  );
});

program.pipe(
  Effect.provide(OpenCode2Client.layer),
  Effect.provide(FetchHttpClient.layer),
  Effect.runPromise,
).catch((error) => {
  console.error(error);
  process.exit(1);
});

Implemented and verified by Claude Opus 5.5 (lead, red test, fix, reviewer B) and GPT-6.1 Sol (reviewer A), in T3 Code through Claude Code and Codex.

🤖 Generated with Claude Code

juliusmarminge and others added 3 commits October 27, 2026 23:45
…#26193)

These tests reproduce #26193: OpenCode 2.1+ omits data.recent on
session.compaction.started/ended, so the bundled protocol schema rejects
the frames and the events stream drops them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenCode 2.1 stopped sending data.recent on session.compaction.started
and session.compaction.ended. The bundled 2.0.28 protocol schema still
requires it, so both frames were rejected and silently dropped, and the
thread never showed compaction.

When an event fails to decode and is one of those two types without a
recent key, decode it again with recent filled as an empty string. T3
never reads recent. Frames that decode today take the same path as before.

Fixes #26193

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Oct 28, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at b996e8f

Macroscope's review found this PR approvable — Restores compaction progress reporting for OpenCode 2.1+ events while retaining existing schema validation. The change is limited to interpreting progress events and does not alter compaction operations or product defaults.

You can add or adjust custom eligibility rules. Learn more.

@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB +6 B (+0.1%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.3 KiB +6 B (+0.5%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.6 KiB 21.6 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 2 2 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.1 KiB +6 B (+0.1%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.3 KiB 1.3 KiB +6 B (+0.5%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 2 2 0 (0.0%) 8 ✅

Baseline: 735cc3c · PR result: b996e8f · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@coderabbitai

coderabbitai Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 735cc3c and b996e8f.

Files selected for processing: 2
  • packages/provider-opencode/src/server/v2/OpenCode2Client.test.ts
  • packages/provider-opencode/src/server/v2/OpenCode2Client.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

Walkthrough

The OpenCode 2 client now decodes compaction-started and compaction-ended events that omit data.recent. A test verifies that the event stream emits both events in order.

Changes: OpenCode compaction events

Layer / File(s) Summary
Compaction event fallback decoding
packages/provider-opencode/src/server/v2/OpenCode2Client.ts
When normal decoding fails, the client checks for compaction-started or compaction-ended events with object-valued data and no recent. It inserts an empty string and returns the event only if the existing schema accepts it.
Event-stream handling and test
packages/provider-opencode/src/server/v2/OpenCode2Client.ts, packages/provider-opencode/src/server/v2/OpenCode2Client.test.ts
The event stream tries the fallback before marking an event undecodable. The test checks that both compaction event types are emitted in order.

Priority: Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Possibly related PRs

Suggested labels: size:M

Suggested reviewers: maria-rcks

Merge Risk: Minimal · up to b996e

The change supports live compaction events from OpenCode 2.1+ without an identified regression. It is mergeable after normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title is concise, conventional, and accurately describes the main change: restoring compaction progress for OpenCode 2.1 and newer versions.
Description check Passed The description is complete and relevant. It explains the problem, the compatibility fix, scope and linked issue, focused tests, broader verification, runtime results, and limitations.
Linked Issues check Passed Linked issue #26193 is the only active direct target. Its coding requirement is that T3 decodes live OpenCode 2.1 compaction frames, so the adapter can create the native compaction item. The PR meets …
Out of Scope Changes check Passed The whole-PR diff against merge base 735cc3c changes two files. (1) packages/provider-opencode/src/server/v2/OpenCode2Client.ts adds a Predicate import, decodeJson, decodeEventValue, and decodeCompac…
Full details: Linked Issues check

Explanation

Linked issue #26193 is the only active direct target. Its coding requirement is that T3 decodes live OpenCode 2.1 compaction frames, so the adapter can create the native compaction item. The PR meets this in OpenCode2Client.ts. When the normal decodeEvent fails, decodeCompactionWithoutRecent parses the frame. If the frame is session.compaction.started or session.compaction.ended and has no data.recent key, it adds recent: "" and decodes again. Frames that fail for any other reason still reach the existing undecodable(frame.data) path. The new test 'delivers compaction events from OpenCode 2.1' in OpenCode2Client.test.ts feeds two 2.1.4 frames without recent through OpenCode2Client.layer and asserts both event types are delivered. The PR also reports runtime evidence against real OpenCode 2.1.4 and 2.2.0 servers: both events were delivered after the change. The issue states that the adapter branches for these events already exist. The UI result ('Compacting context' in a running thread) was not exercised by a test or a runtime check in this PR. The PR states this, so the UI outcome rests on the existing adapter code that the issue describes. Message-history decoding is not changed. #22656 and PR #22661 are history-record work and are not a requirement here.

Full details: Out of Scope Changes check

Explanation

The whole-PR diff against merge base 735cc3c changes two files. (1) packages/provider-opencode/src/server/v2/OpenCode2Client.ts adds a Predicate import, decodeJson, decodeEventValue, and decodeCompactionWithoutRecent. It also changes the filterMapEffect branch in readEvents to try the compaction fallback before undecodable handling. All of these changes serve the #26193 decode fix. (2) OpenCode2Client.test.ts adds the Stream import, a COMPACTION_FRAMES fixture taken from real 2.1.4 frames, a layerEventStream helper, and one test. The fixture, helper, and test all support the same fix. No dependency, schema, adapter, or history-decoding file changed. The ignored_files list is empty, so no excluded file affects this verdict.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@juliusmarminge juliusmarminge added bugfix Fix PR opened by an issue lead; enters the PR loop pr:gate-failed A land gate failed labels Oct 28, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fix PR opened by an issue lead; enters the PR loop pr:gate-failed A land gate failed size:S 10-29 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenCode 2.1 compaction events are discarded, so the thread never shows compacting context

1 participant