Skip to content

feat: seq-based incremental resume + send idempotency (protocol P0, 2/2) - #103

Merged
saucam merged 2 commits into
mainfrom
feat/protocol-seq-resume-idempotency
Jul 5, 2026
Merged

feat: seq-based incremental resume + send idempotency (protocol P0, 2/2)#103
saucam merged 2 commits into
mainfrom
feat/protocol-seq-resume-idempotency

Conversation

@saucam

@saucam saucam commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #102 (which stacks on #101). Merge order: #101#102 → this; I'll retarget as each merges. Completes the protocol P0 hardening from the world-class-protocol audit. All changes additive — no PROTOCOL_VERSION bump; legacy clients unaffected.

Incremental resume (replay.resume) — cheap reconnects

Every reconnect previously re-transferred the entire scrollback (chunked since #84, but still full) — the dominant reconnect cost on flaky/mobile links, and the top finding of the audit.

  • ScrollbackBuffer owns a monotonic mutation counter; entries remember their last-mutation seq. Outbound frames carry the cursor: full messages stamped at push, streaming deltas via an O(1) touch() in the broadcast path (no re-serialization per token).
  • session.attach accepts resume {key, sinceSeq}: on a resumeKey match the daemon replays only entries mutated after the cursor (mode: "incremental" — client appends/upserts, never resets), including older messages grown by deltas or tool-state transitions since the cursor. Key mismatch (daemon restart rebuilt the buffer) → authoritative snapshot. Caught-up → empty ack frame. Replay frames now carry mode/resumeKey/maxSeq.
  • Web client tracks per-session cursors (state/resume.ts), resumes on re-attach, resets the cursor when the daemon's key changes, clears on session destroy. Safety property documented + tested: the cursor may lag reality (safe — resends get deduped by upsert) but can never lead it.
  • Replaces the never-wired timestamp readSince() with seq-based readChunkedSince() (shared partition logic with readChunked).

Send idempotency (send.idempotency) — the duplicate-turn guard

A send whose ack is lost to a socket drop is ambiguous; a retry could run and bill the same prompt twice. session.send now takes an optional clientMsgId (minted once per user action): the daemon acks duplicates ({duplicate: true}) instead of dispatching a second turn, over a bounded FIFO window (256 ids/session). The web composer mints one UUID per submit.

Daemon now advertises replay.chunked + replay.resume + send.idempotency on auth.ok.

Tests (new)

  • Scrollback units: seq assignment/stamping (live vs restore paths), touch semantics, upsert/update advancing entries past a cursor, readChunkedSince ordering + byte-budget partition.
  • Session integration (T5c): snapshot frames carry resume meta; tail-only resume; wrong-key snapshot fallback; caught-up empty ack (and legacy silent attach preserved); a live streamed turn stamps deltas/messages and resuming from the pre-turn cursor returns exactly the turn's messages.
  • Idempotency: first/duplicate/FIFO-eviction semantics + manager-guard flow (two deliveries of one action → exactly one user turn in scrollback).
  • Web: cursor unit tests (establish/raise-only/key-change reset/live-seq anchoring/per-session isolation + destroy).

Verification

Full daemon suite 842 pass · web 171 pass · typecheck (root + package) clean · biome clean · build green.

Note for review

ScrollbackReplayMsg.seq (chunk index, from #84) and SessionMessage.seq (session cursor) are different domains — called out in the type docs on both fields.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added incremental session resume using a resume cursor, so reconnects can replay only new scrollback updates.
    • Added send idempotency via an optional client message id, allowing safe retries without re-executing turns.
  • Bug Fixes
    • Improved consistency between live message sequencing and replay ordering.
    • When resume metadata doesn’t match, replay now safely falls back to a full snapshot.
  • Tests
    • Expanded unit and integration tests for incremental resume behavior, replay metadata handling, cursor tracking, and duplicate-send prevention.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 89842d4e-e2ef-4231-b947-7615c20bd485

📥 Commits

Reviewing files that changed from the base of the PR and between bc1c6cb and 0a996ab.

📒 Files selected for processing (2)
  • src/daemon/scrollback.ts
  • src/tests/scrollback.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/tests/scrollback.test.ts
  • src/daemon/scrollback.ts

📝 Walkthrough

Walkthrough

This PR adds sequence-based scrollback resume and send idempotency across the protocol, daemon, and web client. Protocol contracts gain resume, clientMsgId, seq, mode, resumeKey, and maxSeq fields. The daemon tracks replay sequences and duplicate sends. The web app stores resume cursors and sends clientMsgId on prompt submits.

Changes

Incremental resume & send idempotency

Layer / File(s) Summary
Protocol schema and type contracts for resume/idempotency
packages/protocol/src/schemas.ts, packages/protocol/src/types.ts, web/src/protocol/types.ts
Adds resume and clientMsgId validation to session.attach and session.send, and mirrors the new sequence and replay metadata fields in shared protocol types.
ScrollbackBuffer sequence tracking and chunked resume reads
src/daemon/scrollback.ts, src/tests/scrollback.test.ts
Adds monotonic seq tracking, maxSeq, readChunkedSince, and seq updates on push, touch, and updateMessage, with tests covering incremental replay and chunking behavior.
Daemon attach/send resume and duplicate-send handling
src/daemon/server.ts, src/daemon/session-manager.ts, src/daemon/session.ts, src/tests/session-integration.test.ts
Advertises new capabilities, forwards resume data through attach, emits snapshot or incremental replay with metadata, suppresses duplicate sends via markClientMsgSeen, and validates the flows in integration tests.
Web client resume cursor module and wiring
web/src/state/resume.ts, web/src/state/resume.test.ts, web/src/state/connection.ts, web/src/state/sessions.ts
Adds per-session resume cursor state, updates broadcast handling to track replay/live seqs, and clears cursor state when sessions are removed.
Web protocol types and UI usage
web/src/App.tsx, web/src/components/prompt/PromptBox.tsx
Includes resume cursors in attach requests and generates a clientMsgId for each send request.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WebClient
  participant SessionManager
  participant Session
  participant ScrollbackBuffer

  WebClient->>SessionManager: session.attach(resume)
  SessionManager->>Session: attach(client, resume)
  Session->>ScrollbackBuffer: readChunkedSince(sinceSeq) or full snapshot
  Session-->>WebClient: scrollback.replay(mode, resumeKey, maxSeq)

  WebClient->>SessionManager: session.send(clientMsgId)
  SessionManager->>Session: markClientMsgSeen(clientMsgId)
  Session-->>WebClient: response.ok(duplicate=true) or dispatch send
Loading

Possibly related PRs

  • saucam/codeoid#74: Both PRs touch ScrollbackBuffer sequencing and session replay behavior.
  • saucam/codeoid#96: Both PRs modify ScrollbackBuffer.push and related byte-accounting behavior.
  • saucam/codeoid#100: Both PRs update the scrollback replay and chunking path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: seq-based incremental resume and send idempotency.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/protocol-seq-resume-idempotency

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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

@saucam
saucam force-pushed the feat/protocol-handshake-validation branch from 05ec861 to f32f7f8 Compare July 5, 2026 08:05
@saucam
saucam force-pushed the feat/protocol-seq-resume-idempotency branch from 1141bf0 to 7119962 Compare July 5, 2026 08:05
Protocol P0 hardening, part 2 of 2 (all additive — no version bump):

Incremental resume (`replay.resume`): every reconnect previously
re-transferred the ENTIRE scrollback (chunked since #84, but still full) —
the dominant reconnect cost on flaky/mobile links. Now:
- ScrollbackBuffer owns a monotonic mutation counter; every push / update /
  touch advances it. Entries remember their last-mutation seq.
- Outbound frames carry the cursor: messages via scrollback.push stamping,
  streaming deltas via an O(1) touch() in #broadcastRaw (no re-serialization
  on the per-token hot path).
- session.attach accepts resume {key, sinceSeq}; on a resumeKey match the
  daemon replays only entries mutated after the cursor (mode "incremental",
  append/upsert client-side) — including older messages grown by deltas or
  tool-state transitions. Any mismatch (restart rebuilt the buffer) falls
  back to the authoritative snapshot; caught-up resumes get an empty ack.
  Replay frames now carry mode/resumeKey/maxSeq (additive).
- Web client tracks per-session cursors (new state/resume.ts), resumes on
  re-attach, resets cursors on daemon-key change, clears on destroy.
- Replaces the never-wired timestamp readSince() with seq-based
  readChunkedSince() (shared partition logic with readChunked).

Send idempotency (`send.idempotency`): a send whose ack is lost to a socket
drop is ambiguous — a retry could run (and bill) the same prompt twice.
session.send now takes an optional clientMsgId; the daemon acks duplicates
({duplicate: true}) instead of dispatching a second turn (bounded FIFO
window of 256 ids/session). Web mints one UUID per composer submit.

Daemon advertises replay.resume + send.idempotency on auth.ok.

Tests: scrollback seq/touch/readChunkedSince units; session-level resume
matrix (snapshot meta, tail-only resume, wrong-key fallback, caught-up ack,
live-turn cursor advance incl. delta stamping); idempotency (first/dup/FIFO
eviction + manager-guard semantics: two deliveries -> one turn); web cursor
unit tests (key-change reset, monotonic raise, per-session isolation).
Full suite 842 pass; web 171 pass; build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@saucam
saucam force-pushed the feat/protocol-seq-resume-idempotency branch from 7119962 to bc1c6cb Compare July 5, 2026 08:39
@saucam
saucam changed the base branch from feat/protocol-handshake-validation to main July 5, 2026 08:39
@saucam saucam closed this Jul 5, 2026
@saucam saucam reopened this Jul 5, 2026
@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.67123% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.38%. Comparing base (d5a12b3) to head (0a996ab).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/daemon/session-manager.ts 10.00% 9 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #103      +/-   ##
==========================================
+ Coverage   76.34%   76.38%   +0.04%     
==========================================
  Files          70       70              
  Lines       11539    11597      +58     
==========================================
+ Hits         8809     8858      +49     
- Misses       2730     2739       +9     
Flag Coverage Δ
daemon 76.38% <87.67%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/protocol/src/schemas.ts 100.00% <100.00%> (ø)
packages/protocol/src/types.ts 100.00% <ø> (ø)
src/daemon/scrollback.ts 100.00% <100.00%> (ø)
src/daemon/session.ts 75.53% <100.00%> (+0.43%) ⬆️
src/daemon/session-manager.ts 48.19% <10.00%> (-0.38%) ⬇️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/daemon/scrollback.ts`:
- Around line 133-138: The scrollback cursor update in touch() advances the
stored entry seq but leaves the buffered SessionMessage stale, so replay paths
can emit an outdated per-message seq. Update touch() in the Scrollback class to
keep the replayed message object in sync by stamping the message’s seq whenever
entry.seq is incremented, ensuring readChunked*() replays the updated sequence
value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b87d37e4-3f81-4558-b102-d44fd6432082

📥 Commits

Reviewing files that changed from the base of the PR and between d5a12b3 and bc1c6cb.

📒 Files selected for processing (15)
  • packages/protocol/src/schemas.ts
  • packages/protocol/src/types.ts
  • src/daemon/scrollback.ts
  • src/daemon/server.ts
  • src/daemon/session-manager.ts
  • src/daemon/session.ts
  • src/tests/scrollback.test.ts
  • src/tests/session-integration.test.ts
  • web/src/App.tsx
  • web/src/components/prompt/PromptBox.tsx
  • web/src/protocol/types.ts
  • web/src/state/connection.ts
  • web/src/state/resume.test.ts
  • web/src/state/resume.ts
  • web/src/state/sessions.ts

Comment thread src/daemon/scrollback.ts
Address CodeRabbit review on #103: touch() advanced the entry cursor but
left the buffered SessionMessage's wire seq at its push-time value, so a
later replay could emit a stale per-message seq. Clients that follow the
documented contract (cursor = max of frame maxSeq + live seqs) were safe —
stale-low can never over-advance — but self-consistency (entry.seq ===
msg.seq) removes the footgun for clients deriving cursors from per-message
seqs alone. Same stamping added to updateMessage(), where it is fully
byte-accounted; in touch() the ~15-byte one-time drift is deliberate to
keep re-serialization off the per-token hot path (documented inline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@saucam
saucam merged commit f2239d9 into main Jul 5, 2026
5 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