Skip to content

fix(rooms): carry cross-segment turn bookends in historyPolicy: full#59

Merged
lsaether merged 2 commits into
mainfrom
fix/cross-segment-turn-bookends
May 27, 2026
Merged

fix(rooms): carry cross-segment turn bookends in historyPolicy: full#59
lsaether merged 2 commits into
mainfrom
fix/cross-segment-turn-bookends

Conversation

@lsaether
Copy link
Copy Markdown
Owner

Context

After #58 shipped rooms with mid-turn rotation deliberately preserved (an active turn is not torn down across segment boundaries), historyPolicy: full could leave a late joiner with an orphan: amux/turn_complete lands in the active segment, but amux/turn_started for the same turn lives in the closed prior segment and was filtered out by the current-segment-only rule. Coterie- and vibe-style correlators either drop the orphan or render the finished turn as if the agent were still typing.

Scoped subset of #57 (the broader "orphan chunks outside any turn" case remains open).

Fix

history_full and replay_entries_for_policy now compute a cross_segment_turn_carry set:

  • Every amux/turn_started / amux/turn_complete / amux/turn_cancelled frame found in the active segment contributes its amuxTurnId.
  • The currently active turn's amuxTurnId (from active_amux_turn_id) is also added so an in-flight cross-segment turn's turn_started gets carried even before it completes.

Frames that are turn-lifecycle frames with an amuxTurnId in the carry set are then included in the full view even when they sit in a prior segment. Non-lifecycle frames (chunks) from prior segments are still excluded — those belong to full_lineage.

Frames stay in original replaySeq order, so the carried turn_started appears temporally before the active-segment frames and the bracket reads naturally on the client.

Wire impact

None — strictly additive. The full view returns more frames it should have been returning all along. No new fields, no new methods, no enum variants. Today's clients just stop seeing the orphan they couldn't handle.

Tests

Four new unit tests in src/room/state.rs::tests:

  1. history_full_carries_turn_started_when_turn_completed_across_segments — turn_started in seg-1, hermes compaction mid-turn, turn_complete in seg-2; full view contains both bookends in order.
  2. history_full_carries_turn_started_when_turn_is_still_active_across_segments — turn_started in seg-1, rotation, turn still in flight; carry pulls turn_started forward via active_amux_turn_id.
  3. history_full_does_not_carry_turn_that_completed_before_active_segment — a fully prior-segment turn must NOT appear in full view.
  4. history_full_lineage_returns_every_segment_unchanged — regression guard that lineage view is unaffected.

cargo fmt --check, cargo clippy --all-targets -- -D warnings, 63 lib + 78 integration tests all pass.

Test plan

  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test (63 lib + 78 integration)
  • Manual smoke against real hermes that compacts mid-turn — deferred; covered by mock-based tests since hermes-acp 0.14 doesn't expose _meta.hermes yet anyway.

lsaether added 2 commits May 27, 2026 16:59
If a turn straddles a segment boundary (hermes compaction mid-turn,
or session/load with an in-flight turn that completes after the load
response), historyPolicy: full would surface `amux/turn_complete`
without a matching `amux/turn_started` to a late joiner — coterie-
style correlators drop the orphan or render the turn as still typing.

Both `history_full` and `replay_entries_for_policy` now compute a
`cross_segment_turn_carry` set: turn ids whose lifecycle frames touch
the active segment OR match the currently active turn. Frames in the
log that are turn-lifecycle frames (`amux/turn_started`,
`amux/turn_complete`, `amux/turn_cancelled`) with a turn id in the
carry set are included in the `full` view even when they live in a
prior segment.

Behavior:
- pre-segment bootstrap and active-segment frames still appear,
- turn-lifecycle bookends from prior segments are carried iff their
  turn touches the active segment,
- non-bookend frames from prior segments are still excluded (chunks
  before rotation stay out of `full`; they're available via
  `full_lineage`),
- frames remain in original `replaySeq` order — carried bookends
  appear temporally before active-segment frames, so the bracket
  reads naturally on the client.

No wire-shape changes. Today's clients just stop seeing the orphan
they couldn't render. Scoped subset of #57 (the broader "orphan
chunks outside any turn" case remains open).
…ends

The implementation in 7306469 now carries amux/turn_started,
amux/turn_complete, and amux/turn_cancelled from prior segments
into the `full` view when their amuxTurnId brackets the active
segment, but the docs still described `full` as strictly current
segment only. Three touch points updated:

- docs/design/rooms.md `## historyPolicy` — spell out the
  lifecycle-frame carve-out and the reason (mid-rotation turn
  staying bracketed), plus the non-lifecycle-frames-still-excluded
  guarantee.
- src/protocol/attach.rs `HistoryPolicy::Full` doc — same in
  shorter form for the rustdoc surface.
- README.md replay-log bullet and session/attach coverage row — short
  carve-out mention so the README isn't ahead/behind of rooms.md.

CHANGELOG: new Fixed entry under Unreleased calling out the
cross-segment turn bookend fix as a scoped subset of #57.
@lsaether lsaether marked this pull request as ready for review May 27, 2026 22:09
@lsaether
Copy link
Copy Markdown
Owner Author

Pushed fe167c8 on top — additive, no force.

Docs

  • docs/design/rooms.md ## historyPolicyfull description now spells out the lifecycle-frame carve-out (amux/turn_started / amux/turn_complete / amux/turn_cancelled from prior segments carried when bracketing the active segment) and the reason (mid-rotation turn stays bracketed), plus the guarantee that non-lifecycle frames stay excluded.
  • src/protocol/attach.rs HistoryPolicy::Full rustdoc — same point in shorter form for the rustdoc surface.
  • README.md — replay-log bullet and session/attach coverage row both call out the carve-out so the README doesn't drift from rooms.md.

CHANGELOG

Marking the PR ready for review.

@lsaether lsaether merged commit d815644 into main May 27, 2026
1 check passed
lsaether added a commit that referenced this pull request May 27, 2026
Cuts v0.1.3 — rooms + RFD #533 attach polish.

Highlights since v0.1.2:
- Rooms abstraction with compaction-aware transcripts (#58) and
  cross-segment turn-bookend carry in historyPolicy: full (#59).
  Closes #56.
- RFD #533-inspired session/attach + session/detach foundation (#46)
  with replay ordering (#47), streamed history delivery (#53), and
  ?replay=skip opt-out for attach-aware clients (#50).
- amux/* active-turn controls (steer #41, queue #41, cancel #30)
  plus replay-safe agent request lifecycle openings (#33).
- Safe-by-default client-tool blocking (fs/* + terminal/*) with
  --unsafe-debug-client-tool-broadcast escape hatch (#36).
- amux/session_context surfaces the mux/agent cwd to subscribers (#35).

Validation:
- cargo fmt --check
- cargo clippy --all-targets -- -D warnings
- cargo test (63 unit + 78 integration = 141 tests)

Co-authored-by: lsaether <25539605+lsaether@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant