Skip to content

After leaving Rust mode on OpenCode 1, the TS historian refuses forever: "gap before message N" on synthetic-only user rows the module skipped #781

Closed

Description

@iceteaSA

Versions: plugin 0.49.22 on OpenCode 1. The code paths cited below are unchanged on main (3a6d3cb90).

What happens: a session ran in Rust mode for weeks. During that time a plugin injected synthetic: true user text messages into it (a cache-warm ping, one text part, about 56 chars). The module's compartments do not cover those rows: each one sits alone between two compartments.

After the project switched back to TS mode, every historian run fails before any model call:

historian failure: source=existing-validation reason="gap before message 3387 (expected 3386)"

It failed 66 times in 8 hours, and the session got no new compartments. The stored compartments had four such one-message gaps (3386, 3770, 4180, 4220). Each gap message was a user row whose only part was a text part with synthetic: true.

Why it never recovers (main):

  • validateStoredCompartments (compartment-runner-validation.ts:302-321) accepts a gap only if it is listed in nonNarrativeGapRanges.
  • v2NonNarrativeStoredGapRanges (compartment-runner-incremental.ts:143) returns [] unless the session's rebase generation is v2, so on OpenCode 1 no gap is ever accepted.
  • isStrictGapHealingMessage (read-session-raw.ts:20) would also reject these rows: a V1 row has no storeType: "synthetic", and a text part with content returns false, even with synthetic: true on it.
  • The repair and backoff around existingValidationError (compartment-runner-incremental.ts:394-430) apply only when an unresolved compartment is involved. This failure therefore repeats on every trigger and has no repair path.

Workaround (the same shape as the one confirmed on #561): for each gap, set the next compartment's start_message back by one, and its start_message_id to the gap message. The next run published normally.

Suggested fix:

  1. Treat a V1 user row whose parts are all synthetic: true text as gap-healing, as storeType: "synthetic" already is on V2.
  2. Accept such gaps on OpenCode 1 too, not only after a v2 rebase.
  3. Alternatively, heal the gap by extending the next compartment, as the workaround does.
  4. Either way, back off this failure class like the unresolved case, so it doesn't re-run on every trigger.

Related: #561, where a compaction marker on a synthetic user row wedged a session in Rust mode.

Activity

  1. magic-alfonso commented on Oct 26, 2026

    @magic-alfonso

    Thanks for the precise trace. I confirmed it on current code. Our own sessions have the same shape: one session that ran in Rust mode has 32 one-message gaps between its compartments, and the ones I checked are each a single user row whose only part is a synthetic: true text notice from another plugin. On OpenCode 1, v2NonNarrativeStoredGapRanges returns nothing outside a v2 rebase, so after a switch back to TypeScript mode the historian refuses on every trigger with no repair path, exactly as you describe.

    This blocks a switch we're about to make ourselves, so it's being fixed now. The direction:

    • the TypeScript validator accepts exactly the rows the Rust module skipped, and no wider;
    • this failure class backs off and is logged once, instead of re-running on every trigger;
    • a test switches a Rust-mode session with these gaps back to TypeScript and checks that the historian publishes and the cached prompt stays unchanged.

    Your workaround is safe to keep using until it ships. We'll post here when it does.

  2. magic-alfonso commented on Oct 27, 2026

    @magic-alfonso

    Fixed in 0.49.24. Thanks @iceteaSA for the report and the trace.

    The TypeScript historian now skips user messages made only of synthetic parts (such as background-task notices) the same way the Rust historian did. A session that left Rust transform mode no longer refuses summarization forever with "gap before message N", and it logs with backoff instead of on every trigger.

    Update the plugin and restart OpenCode to pick it up.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions