Skip to content

Fix dev:collab reviewer flow: room replay on resume, invite validation, harness repair#9

Merged
angusbezzina merged 9 commits into
mainfrom
angus/attn-6dd-room-replay
Jun 11, 2026
Merged

Fix dev:collab reviewer flow: room replay on resume, invite validation, harness repair#9
angusbezzina merged 9 commits into
mainfrom
angus/attn-6dd-room-replay

Conversation

@angusbezzina

Copy link
Copy Markdown
Collaborator

Summary

Root-caused and fixed the "reviewer never enters edit mode" reports around task dev:collab. Three independent bugs, none introduced by the recent UI branch (verified by empirical bisect — fresh-state joins pass at every commit including origin/main):

1. Persisted-session replay gap (attn-6dd) — the real long-standing bug. On any session after the first over persisted state, the reviewer daemon resumes its mailbox at the stored cursor, the relay never re-delivers the snapshot, and nothing replayed the on-disk room state to the webview — the reviewer hung on "Waiting for the shared document…" forever. Fixes:

  • ReviewManager::replay_room_to_webview: replays persisted events (snapshots rehydrated from the blob store) through the same EventImported lane the live pipeline uses; called on resume and on join.
  • resume_known_rooms emits role-accurate lifecycles (Live for locally-shared rooms, Joined otherwise) instead of the passive Resumed, so the reviewer's window actually re-activates the room.
  • Re-join no longer clobbers room.json with an empty record.

2. Garbage invites accepted silently — the Share dialog's primary copyable is the npx attnmd review join '…' one-liner; pasting it into dev-collab's invite prompt sent the whole string to the daemon, which failed silently while the CLI printed "join request sent" and the script claimed success. Fixes:

  • review join validates the invite client-side with the daemon's own parse_invite; garbage exits 1 with the expected attn://review/… shape.
  • dev-collab.sh extracts the invite URL from whatever is pasted (bare, quoted, or npx-wrapped).

3. Editorial E2E silently broken since May 28 (attn-ry9) — the suite's raw postMessage calls lack the IPC capability token added in ba19671, so every privileged call was rejected and 11 assertions cascade-failed on every commit since (which is why none of this was caught). Fixes: daemon-routed CLI join + a debug-build-only token bridge (window.__attn_ipc_token__, gated on cfg!(debug_assertions) via the init payload — release builds never expose it) for the remaining sites.

Verification

  • cargo: 422 review-lib + 477 bin tests, 0 failures (new tests for replay, re-join preservation, invite validation)
  • web: svelte-check 0 errors, 33/33 unit files; review E2E 41 PASS / 0 FAIL
  • scripts/test-editorial-e2e.sh: 18 / 18 (was 7 / 11-failed)
  • Acceptance probe of the reported symptom: share → join → kill both daemons → reboot on the same state → reviewer re-enters the shared doc; 5/5 assertions, run twice

🤖 Generated with Claude Code

angusbezzina and others added 4 commits June 11, 2026 11:45
On any second session over persisted state, the WS mailbox reconnects at
its stored cursor, so the relay never re-delivers session-1 envelopes —
the on-disk store had everything but nothing pushed it to the webview,
leaving a restarted reviewer on "Waiting for the shared document…"
forever.

- ReviewManager::replay_room_to_webview: re-emit every persisted event
  through the same ReviewUpdate::EventImported push the live inbound
  pipeline uses, in events.jsonl order, with SnapshotCreated plaintext
  rehydrated from the local blob store (rehydrate_snapshot_event), so
  snapshots ride the reviewEvent lane exactly like a fresh import. Safe
  against double-delivery: the frontend dedups by eventId/snapshotId.
  Called from both resume_known_rooms and the join path.
- resume_known_rooms now emits a role-accurate lifecycle ("Live" for
  rooms we shared, "Joined" for rooms we joined) instead of the passive
  "Resumed", so the frontend activates the room and a restarted
  reviewer flips back into the shared-doc view; owners never flip
  (role gate, attn-0wa).
- join_with_identity step 5 no longer clobbers room.json on re-join:
  the fresh empty ReviewRoom is only written when none exists, so
  accumulated documents/snapshots/event_heads survive a repeat join.

Tests: replay unit test (manager), re-join preservation test
(bootstrap); verified end-to-end with a persisted-second-session probe
(owner shares, reviewer joins, both daemons killed and rebooted on the
same ATTN_HOMEs — reviewer rehydrates the shared doc without a new
join/share).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-ry9)

The suite drove four privileged IPC messages via raw
window.ipc.postMessage, all rejected since the capability-token gate
landed ("rejected privileged IPC 'review_join' without a valid token"):

- The join now goes through the daemon-routed CLI (`attn review join`),
  which rides the unix socket straight into the ReviewManager — same
  path dev-collab uses, no token needed.
- The remaining three sites (reply, resolve, stop) exercise the exact
  wire messages their buttons send, so they keep the raw postMessage
  form and now pass the session token. Debug builds expose it to the
  automation bridge: the daemon marks the init payload with
  debugBuild = cfg!(debug_assertions), and App.svelte then mirrors the
  token to window.__attn_ipc_token__. Release builds never set the
  flag, so the token stays confined to the init payload there;
  sandboxed HtmlViewer iframes have their own window and never see it.

Suite: 18 passed, 0 failed (was 14/4 with the CLI join alone, 7 before).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aemon

`attn review join <garbage>` printed "join request sent to the running
attn daemon" and exited 0 — the daemon parses the invite asynchronously
and a malformed one (e.g. the ShareDialog's full `npx attnmd review
join 'attn://…'` one-liner pasted as the invite) failed silently while
the CLI claimed success.

The daemon-routed join now runs the same parse_invite the daemon uses
before sending; on failure it prints the expected invite shape
(attn://review/<roomId>#key=<secret>) and exits non-zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Share dialog's primary copyable is the full `npx attnmd review join
'attn://…'` one-liner, but the script's prompt passed the pasted text to
`review join` verbatim — the daemon failed to parse it while the script
still claimed "Reviewer joined". Extract the attn://review/… URL from
the paste (bare URL, quoted, or npx-wrapped all work) and error clearly
when none is present. Pairs with the new client-side invite validation
in the CLI, which makes the script's success message truthful.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
attn Ready Ready Preview, Comment Jun 11, 2026 6:30pm

Request Review

…o active rooms

- Rail: dropped the aside border-t (it stacked with the rail header's
  border-b into one thick line); cards/chips now clip 8px above the
  rail's bottom edge (overflow wrapper + mb-2) — the bottom counterpart
  of the top breathing-room clamp.
- Textareas (reply, comment, suggestion fields) show the theme's
  border-ring + ring-ring/50 focus treatment instead of the native blue
  WebKit ring, mirroring the shared Input component. Plain :focus, not
  :focus-visible — WebKit doesn't reliably match the latter on
  textareas.
- Renaming yourself now reaches rooms you're already in: the
  ParticipantJoined announce was frozen at share/join time, and the
  onboarding NamePrompt fires AFTER a room is entered, so a name typed
  there ("Reader") never replaced the git default on existing comments.
  ReviewSetDisplayName now also submits a ReannounceIdentity manager
  command that re-announces the identity (kind derived from the local
  share binding) into every active room and drains each outbox
  immediately; frontends key names by participantId, last write wins.
  announce_owner generalized to announce_participant; new unit test
  decrypts the re-announce envelope and asserts the fresh name + kind.

Verified: 423 cargo review tests, svelte-check 0 errors, 33/33 web unit
files, review E2E 41/41, editorial relay E2E 18/18. Live checks: rail
border-top 0px, bottom clip gap 8px, compiled :focus rule present
(window focus can't be held by the probe environment; the ring shows
under real focus, same condition as the old native ring).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the Rust Quality (Linux) CI failure on PR #9 — the reannounce
changes weren't rustfmt-clean. Clippy (-D warnings) verified too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… hairline

- The live-collab caret chip kept showing the construction-time label
  (the git default) after a NamePrompt rename — the prompt fires AFTER
  collab starts. CollabController.setSelfLabel updates the label and
  re-broadcasts the last caret position so peers update immediately; an
  App effect feeds it userProfile.effectiveName. (Comment cards were
  already fixed by the ParticipantJoined re-announce; this is the caret
  pipeline, which is collab-presence, not review events.)
- Wheel gestures over the comments rail now forward to the document
  viewport — the rail deliberately never scrolls itself (attn-23m), so
  the pointer crossing it used to dead-stop reading flow.
- The rail gets its top border back at the header divider's hairline
  weight (border/40); the earlier complaint was the heavier /60 line
  stacking with the header border.

Verified live: wheel event on the rail scrolls the viewport 1:1;
border-top 1px. svelte-check 0 errors; 33/33 unit files; review E2E
41/41.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rail bottom

- The rail's top-left corner is rounded (rounded-tl-lg; overflow-hidden
  clips the header/content to it).
- New fitBottom pass in margin-layout: the down-pass never places a card
  above its anchor, so a card whose anchored text sat near the viewport
  bottom extended past the rail and was cut off. fitBottom walks
  placements bottom-up and lifts any card whose ANCHOR is on screen fully
  into view — above its anchor if needed, Google-Docs style — cascading
  the lift so cards never overlap, and flagging lifted cards `offset` so
  the connector line shows the displacement. Anchors below the fold keep
  tracking off-screen with their text (attn-23m). Applied to both the
  expanded cards and the collapsed gutter chips.
- Four new margin-layout unit tests; the E2E scroll-tracking probe now
  centers the card before sampling (strict 1:1 only holds outside the
  fit bands) with a settle pause (the margin repositions asynchronously
  on scroll), plus a new assertion that a bottom-anchored card is lifted
  fully into view. 43 PASS / 0 FAIL.

Verified live: border-radius ~10px; a card anchored at the document tail
sits fully visible at max scroll (bottom 660 vs clip line 712).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… corners

The author/kind/state accent was a 3px border-left against the card's
6px corner radius, which renders pointy color overshoot past the
rounded corners (user-reported offset). The accent is now an inset
box-shadow strip driven by a --rmc-accent custom property (inline
author color; kind/state CSS fallbacks) — inset shadows clip to the
rounded padding box, so the strip's ends follow the curve. Uniform 1px
border all around; padding-left 13px keeps content at the same x.

Verified live: border-left 1px, inset strip present, padding 13px,
corners clean. svelte-check 0 errors, 33/33 unit files, E2E 43/43.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@angusbezzina angusbezzina marked this pull request as ready for review June 11, 2026 18:33
@angusbezzina angusbezzina merged commit f5c38b2 into main Jun 11, 2026
6 checks passed
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