Version: v0.44.1, OpenCode 1, transform_mode: "rust". The code paths below are unchanged on master (edd6529).
What happens
Every pass fails with:
rust transform failed; attempting LKG replay: coverage gap: live item <mid>#0 (ordinal N) sits at or below coverage end Some(M) but no compartment covers it; composing m0 would silently drop it from the tail
LKG replay misses, then the raw fallback refuses:
raw_fallback_over_context_limit estimated=292920 proxy_bytes=592322 proxy_tokens=148081 limit=995904
rust pass: decision=error reason=transform_failed served_from=raw applied=false
The user sees the ENGINE_RECONNECTING refusal on every turn. /ctx-flush and restarting OpenCode don't help. The session can't recover on its own: a failed HARD pass commits nothing, so the fold and marker drain that would move the boundary past the row never run.
How it gets there
Reconstructed from plugin logs and the module store rows; each step matches the code at v0.44.1.
- Another plugin injects a notification as a user message whose only part is text with
synthetic: true. encodeOpenCodeMessagesToCk sets meta.synthetic when every part is synthetic (module-wire.ts:893-901), so the row is synthetic on the wire.
- The next historian chunk starts right at that row.
build_historian_chunk skips synthetic messages (historian_chunk.rs:500-529), so the new compartment starts at N+1. Store-side validation allows the sparse gap by design (compartment_coverage.rs, resolve_coverage), and the transform's live array excludes synthetic blocks (transform.rs:3528-3532), so nothing complains yet.
- About half a minute later the compaction-marker drain targets that compartment's end.
findBoundaryUserMessage (compaction-marker.ts:263-302) returns the latest user row at or before the target and has no synthetic check, so it returns row N. injectCompactionMarker adds {"type":"compaction","auto":true} to it, with no synthetic flag (compaction-marker.ts:498-514).
- Row N now has a part that isn't synthetic, so
meta.synthetic flips to false. Its block joins live, and OpenCode's filterCompacted starts the input at that row. The next HARD pass runs first_uncovered_live_block (transform.rs:8527-8542), finds N at or below the coverage end with no compartment, and returns CoverageGap (transform.rs:5076-5093).
SOFT and defer passes don't run the guard, so the failure waits for the next HARD pass. For us that was the first cold pass after a restart, about 30 minutes after the marker landed. In the module store there was exactly one uncovered ordinal in the session's covered range, and it was the row carrying the marker.
The root problem is that a row's synthetic classification isn't stable. The historian decides once, and then the marker drain changes the input that decision was made on.
Why the raw fallback refuses
Both numbers in the refusal line are under the limit. The refusal comes from trust. estimateFinalWireInputTokens is only trusted when every input part is countable (final-wire-token-estimate.ts:204-210), and hasCountableParts has no compaction case (:235-292). serveRawFallback treats an untrusted estimate as Infinity (rust-mode-transform.ts:2536-2548).
The raw input always starts at the marker's boundary user row, and that row always has the compaction part. So the raw fallback is refused on any session that carries a Magic Context compaction marker, whether or not this gap exists. A direct check with a measured tool set:
| input message parts |
trusted |
text |
true |
synthetic text |
true |
compaction + synthetic text |
false |
The refusal log doesn't print trusted, so the line looks like an arithmetic bug.
Suggested fixes
- Keep the classification stable. The encoder already drops
compaction parts from content (module-wire.ts:996) and from block mappings (:831). If they also didn't count in the every(synthetic) check, the marker would stop flipping rows. Marking the injected part synthetic: true would do the same from the other side.
- Don't pick a boundary outside coverage.
findBoundaryUserMessage could skip all-synthetic user rows, or prefer a user row inside a published compartment range.
- Or let historian compartments start at the requested start ordinal, so a skipped synthetic head row is still inside the range. OpenCode ordinals don't need the sparse-gap allowance that the Claude Code proxy does.
- Count
compaction parts as zero tokens in hasCountableParts. They produce no wire bytes.
- Print
trusted in the raw_fallback_over_context_limit line.
A regression test for the first three: put a synthetic-only user row directly after a compartment end, publish the next compartment, drain the marker onto that row, then run a HARD pass. It should compose.
Workaround
Extend the compartment after the gap so it starts at the uncovered ordinal. That means changing start_message and start_message_id (<mid>#0) on that one mc_compartments row. Nothing overlaps, because the gap sits between two otherwise contiguous compartments. The row's content is a synthetic notice, so the summary loses nothing that matters.
We replayed the session's real input through the module's transform handler against copies of the store. The unedited copy reproduced the coverage-gap error. The edited copy completed a HARD pass, folded the next compartment, and served the tail starting right after the new coverage end. A second pass deferred with byte-identical m0. On a third, unedited copy, dropping only the compaction part from that row's input (so the row is synthetic again) also let the HARD pass compose, which isolates the marker as the trigger.
Version: v0.44.1, OpenCode 1,
transform_mode: "rust". The code paths below are unchanged onmaster(edd6529).What happens
Every pass fails with:
LKG replay misses, then the raw fallback refuses:
The user sees the ENGINE_RECONNECTING refusal on every turn.
/ctx-flushand restarting OpenCode don't help. The session can't recover on its own: a failed HARD pass commits nothing, so the fold and marker drain that would move the boundary past the row never run.How it gets there
Reconstructed from plugin logs and the module store rows; each step matches the code at v0.44.1.
synthetic: true.encodeOpenCodeMessagesToCksetsmeta.syntheticwhen every part is synthetic (module-wire.ts:893-901), so the row is synthetic on the wire.build_historian_chunkskips synthetic messages (historian_chunk.rs:500-529), so the new compartment starts at N+1. Store-side validation allows the sparse gap by design (compartment_coverage.rs,resolve_coverage), and the transform'slivearray excludes synthetic blocks (transform.rs:3528-3532), so nothing complains yet.findBoundaryUserMessage(compaction-marker.ts:263-302) returns the latest user row at or before the target and has no synthetic check, so it returns row N.injectCompactionMarkeradds{"type":"compaction","auto":true}to it, with no synthetic flag (compaction-marker.ts:498-514).meta.syntheticflips to false. Its block joinslive, and OpenCode'sfilterCompactedstarts the input at that row. The next HARD pass runsfirst_uncovered_live_block(transform.rs:8527-8542), finds N at or below the coverage end with no compartment, and returnsCoverageGap(transform.rs:5076-5093).SOFT and defer passes don't run the guard, so the failure waits for the next HARD pass. For us that was the first cold pass after a restart, about 30 minutes after the marker landed. In the module store there was exactly one uncovered ordinal in the session's covered range, and it was the row carrying the marker.
The root problem is that a row's synthetic classification isn't stable. The historian decides once, and then the marker drain changes the input that decision was made on.
Why the raw fallback refuses
Both numbers in the refusal line are under the limit. The refusal comes from trust.
estimateFinalWireInputTokensis only trusted when every input part is countable (final-wire-token-estimate.ts:204-210), andhasCountablePartshas nocompactioncase (:235-292).serveRawFallbacktreats an untrusted estimate asInfinity(rust-mode-transform.ts:2536-2548).The raw input always starts at the marker's boundary user row, and that row always has the compaction part. So the raw fallback is refused on any session that carries a Magic Context compaction marker, whether or not this gap exists. A direct check with a measured tool set:
trustedtexttextcompaction+ synthetictextThe refusal log doesn't print
trusted, so the line looks like an arithmetic bug.Suggested fixes
compactionparts from content (module-wire.ts:996) and from block mappings (:831). If they also didn't count in theevery(synthetic)check, the marker would stop flipping rows. Marking the injected partsynthetic: truewould do the same from the other side.findBoundaryUserMessagecould skip all-synthetic user rows, or prefer a user row inside a published compartment range.compactionparts as zero tokens inhasCountableParts. They produce no wire bytes.trustedin theraw_fallback_over_context_limitline.A regression test for the first three: put a synthetic-only user row directly after a compartment end, publish the next compartment, drain the marker onto that row, then run a HARD pass. It should compose.
Workaround
Extend the compartment after the gap so it starts at the uncovered ordinal. That means changing
start_messageandstart_message_id(<mid>#0) on that onemc_compartmentsrow. Nothing overlaps, because the gap sits between two otherwise contiguous compartments. The row's content is a synthetic notice, so the summary loses nothing that matters.We replayed the session's real input through the module's transform handler against copies of the store. The unedited copy reproduced the coverage-gap error. The edited copy completed a HARD pass, folded the next compartment, and served the tail starting right after the new coverage end. A second pass deferred with byte-identical m0. On a third, unedited copy, dropping only the
compactionpart from that row's input (so the row is synthetic again) also let the HARD pass compose, which isolates the marker as the trigger.