feat(sessions): durable reconnect with a per-session sequence, snapshot, and replay - #6524
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
mmabrouk
left a comment
There was a problem hiding this comment.
Agent-generated review, low weight. One human should confirm every point below before acting on it.
Whole-PR review of the durable-reconnect unit (6 commits over feat/session-live-relay, 46 files). The design contract is docs/design/session-control-and-live-events/contracts/{persistence,events,public-api}.md.
Reading guide (5 lines):
- Sequence allocation and idempotency:
api/oss/src/dbs/postgres/sessions/records/dao.py_append_sequencedplus the migration. - Post-commit publish and the subscribe-before-query replay:
records_worker.py,core/sessions/records/events.py,apis/fastapi/sessions/live_events.py. - Snapshot, read state, and watermark paging:
router.py get_session_snapshot,dao.py get_read_state/get_records_page. - Client side:
web/packages/agenta-chat/src/model/durableEvents.tsreducer andhooks/useSessionLivePreview.tsreconnect. - Coordination with PR #6517:
history_incompleteis read defensively; its migration is on the core_oss chain, so no migration-file clash there.
Verdict: ship after fixes. The core invariants hold and are well tested (concurrent distinct sequences, retry does not consume a sequence, post-commit publish, subscribe-before-query no-gap with a direct test, legacy sessions flagged incomplete without backfill, idempotent client reducer, Fern for HTTP with EventSource as the SSE exception, Storybook present). One hard blocker: the migration revision id collides with feat/session-execution-watchdog. See inline comments.
mmabrouk
left a comment
There was a problem hiding this comment.
Agent-generated follow-up, low weight. Round 2 (head 44ed249812) resolves the P1 migration collision (renumbered to oss000000006 on 005, with a chain guard asserting one head 004->005->006) and project-scopes the cursor PK, the unique index (project_id, session_id, sequence), and the DAO upsert; 13 unit tests pass here. One caveat, not a blocker: this branch does not carry #6501's 005, so it must merge as a set with #6501 (alembic on this branch alone dangles on the missing 005). Postgres integration tests were skipped in my session (no reachable analytics DB); those DB assertions are author-claimed, not re-verified by me. Ship as a merge set with #6501.
44ed249 to
0045c04
Compare
|
Round 3 review (commit 7c11118, "accept sparse durable event sequences") — verdict: ship. The core fix is correct and well-tested. The client reducer no longer demands a contiguous chain: it applies any event with Correctness checks (all pass):
Wire contract: the watermark producer change is in this same commit — no separate API change is pending. Tests run locally:
Non-blocking findings (nits, posted inline):
No blockers. |
mmabrouk
left a comment
There was a problem hiding this comment.
Agent-generated review, low weight. One human should confirm each finding before acting on it.
Reviewed the complete 47-file delta at 664d1b7d208abbeda008a9095d216353625bff0d, including the existing reviews and comments.
Review summary
Verdict: do not merge yet. I found two blocking data-loss risks and one incomplete client integration.
P1: Durable event publication can trim unprocessed records
publish_durable_event calls XADD MAXLEN directly on the shared streams:records stream. If that stream exceeds the configured limit while the records consumer has pending work or lag, Redis can trim durable record entries that have not reached Postgres. The existing trim_live_stream helper deliberately guards the count trim behind the records consumer's acknowledgement frontier, but this new path bypasses that protection.
The loss is silent because a removed record never receives a database sequence, so history_complete cannot detect the missing input.
Smallest fix: append the relay envelope without MAXLEN, then use trim_live_stream, or move relay envelopes to a separate bounded stream. Add a test where the records group has lag and verify publication cannot cross its acknowledgement boundary.
Code: api/oss/src/core/sessions/records/streaming.py:276-281.
P1: Opposite multi-session batch orders can deadlock
The sequenced append_many path locks session cursor rows in input order inside one project transaction. Multiple records-worker replicas can therefore process batches [session A, session B] and [session B, session A] concurrently. Each transaction can lock its first cursor and wait for the other, causing Postgres to abort one as a deadlock.
The worker currently catches the append failure but still returns the message IDs collected before the write, so the rejected batch can then be acknowledged and permanently lost.
Smallest fix: group records by session and acquire cursor locks in one stable session order while preserving record order within each session. Add a Postgres test with two concurrent batches containing the same sessions in opposite order. Failed database batches must remain pending.
Code: api/oss/src/dbs/postgres/sessions/records/dao.py:130-140.
P2: The client does not use the snapshot-bound transcript
useSessionLivePreview fetches the snapshot and passes it to onDisconnect, but the caller ignores that argument and runs the old loadSessionMessages path. That path still calls the unbounded querySessionRecords; the new querySessionTranscript(throughSequence) function has no caller.
The production client therefore does not load a transcript fixed at snapshot.read.latest_sequence, still relies on legacy timestamp ordering, and never surfaces snapshot.read.history_complete=false. SessionHistoryNotice is only used by Storybook, so a partially preserved session with some records appears complete.
Smallest fix: hydrate through querySessionTranscript with the snapshot watermark, expose reconnecting and incomplete states from the hook, and render SessionHistoryNotice in the production chat surfaces. Add a hook-level reconnect test.
Code: web/packages/agenta-chat/src/hooks/useSessionLivePreview.ts:75-85, web/packages/agenta-chat/src/hooks/useAgentConversation.ts:704-714, and web/packages/agenta-entities/src/session/api/api.ts:98-140.
Verification
git diff --checkpassed.- All 25 changed Python files compiled successfully.
- Exact-head formatting, lint, secret scanning, and contribution checks passed.
- API, web, runner, and integration test jobs were skipped in the reported exact-head check run.
- I could not independently run pytest or pnpm tests because the temporary checkout had no installed dependencies.
- The known migration dependency remains: this branch's tracing revision
006requires PR #6501's revision005.
Railway Preview Environment
Updated at 2026-09-05T10:45:16.920Z |
mmabrouk
left a comment
There was a problem hiding this comment.
🤖 The AI agent says:
This PR adds durable sequence cursors, snapshot recovery, and replay into the live client.
Focused simplification review using the new simplify skill: one non-blocking suggestion below. This is a static design/code review, not a rerun of the live release gate or a replacement for existing correctness reviews.
mmabrouk
left a comment
There was a problem hiding this comment.
🤖 The AI agent says:
Correctness follow-up: one additional replay failure reproduced below. I checked the existing review and am not duplicating its record-trimming, transaction-order, or snapshot-integration findings.
mmabrouk
left a comment
There was a problem hiding this comment.
Agent-generated Codex review, low weight.
CHANGES NEEDED
The replay handoff itself is correctly ordered: subscription precedes the Postgres replay, replay is bounded by the captured watermark, and ready is queued only after replay. The reducer also separates the reconnect cursor from the last accepted event sequence and avoids watermark-only refetches.
Four defects remain at this head: two paths can silently lose record history, the client does not actually consume the snapshot-bounded transcript, and the fixed reconnect loop can amplify Redis/Postgres failures. Details are inline.
The migration is additive and nullable, and flag-off preserves the old runtime path once the migration is present. The migration must precede this binary because the ORM maps records.sequence even when allocation is disabled. I did not repeat the resolved oss000000005 dependency thread.
Validation: reviewed 2146213306...fc02d8554b as one 47-file delta, read all ten resolved threads, and ran git diff --check. No test file was rerun; the readiness report already covers the focused replay/API/frontend tests, while these findings need missing concurrency and hook-integration coverage.
fc02d85 to
d0c98b1
Compare
mmabrouk
left a comment
There was a problem hiding this comment.
Agent-generated Codex review, low weight.
CHANGES NEEDED
The rebase delta is scoped to durable reconnect and its five new base-integration/test files. The base's separate live stream, record-ingest MAXLEN 100000, isolated relay startup, and gap-rejecting reducer remain intact. Durable events no longer touch streams:records; cursor locks are stable and failed project batches are not acknowledged; reconnect delay resets only on ready.
The reconnect order is snapshot N, bounded transcript through N, awaited host callback, then SSE after N. That costs one snapshot request plus max(1, ceil(records/100)) transcript requests per open; a reconnect also triggers one immediate host records refresh before the delayed bounded hydration. The two cursor-handoff problems and the fresh-migration failure are inline.
Validation on dedicated fresh databases: core legacy/OSS and tracing legacy reached head; tracing OSS failed because revision oss000000005 is absent. API sessions: 550 passed, 4 failed, 9 warnings. Sequence: 0 passed, 3 failed, 3 warnings. Snapshot: 0 passed, 1 failed, 3 warnings. Replay: 0 passed, 2 failed, 3 warnings. All backend failures follow from the tracing migration failure. Focused live-preview hook: 2 passed. git diff --check passed.
38df392 to
3cfea3a
Compare
d0c98b1 to
2d30bf3
Compare
The two Postgres-backed records tests declared an async autouse fixture with the plain pytest decorator, which pytest-asyncio 1.x does not collect, so both files errored at setup before any query ran. With the pytest_asyncio decorator both files run: 3 passed against the proof stack database. Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
Await replay queue capacity so a finite durable backlog cannot be mistaken for a live producer outrunning its reader. Keep the existing drop-and-reconnect policy for live frames and cover replay larger than the configured buffer. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Route live frames and committed durable events through one bounded relay-stream append path while leaving record ingest retention unchanged. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Acquire per-session sequence cursors in a stable order while preserving record order inside each session. Leave every project batch message pending when its transactional append fails. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Load the transcript through the snapshot sequence and await host adoption before opening the live event stream after that cursor. Freshen later transcript reads so durable events cannot restore a stale cached copy. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Retry snapshot hydration and live reconnects with a capped exponential delay aligned to the server SSE retry default. Reset the delay only after the relay reports ready. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Read both checked-in tracing migrations so the chain assertion cannot invent the watchdog link. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Require the host to confirm adoption before following a snapshot cursor, and preserve the snapshot watermark when retention shortens the bounded page. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Map bounded snapshot records with durable interaction lifecycle so terminal approvals and client tools do not reopen as pending. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Filter quarantined records from bounded transcript pages, durable replay reads, and worker event projection while preserving the committed sequence watermark. Add database and worker regressions for the refused late tail. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Make the desktop refresh adapter report adoption or existing coverage and return false when a handoff is declined. Cover the production adapter with a snapshot-to-SSE reconnect regression. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Carry the durable sequence cursor separately from retained row counts, derive it from ordinary hydration, and compare each transcript against the matching host watermark. Cover sparse sequences and fixed-count retention on desktop and mobile. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Reject missing or malformed transcript reads before adoption and keep the current conversation intact when a watch-triggered refresh fails. Route failed live-preview hydration through the existing reconnect backoff on desktop and mobile. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
579c76b to
1281c60
Compare
Apply the repository Prettier layout to the chained transcript mock so the TypeScript format gate accepts the durable reconnect lane. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Give the sequence cursor table the complete nullable lifecycle shape required by the API schema convention. Keep the unreleased tracing migration and DBE aligned. Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
74df4cf to
c672dc5
Compare
Context
A client that disconnects during a run has no cursor to resume from: the watch route sends change notices, and the client reloads the whole transcript. This is increment 4, part two, of the session-control design on PR #6495, stacked on the live relay in PR #6522. It gives every durable fact a database-defined per-session order, a snapshot that carries that order, and a replay that continues from it.
Changes
session_sequence_cursors) and a nullablesequencecolumn on records, allocated in the same transaction as the record insert, so concurrent inserts get distinct increasing sequences per session and a retry keeps its sequence. Old rows keep null. Behind the history-writes switch; off means nothing changes.execution.started,execution.stopped,execution.failed,execution.lost,message.completed,tool.completed) are published askind: eventenvelopes on the same stream the relay tails, after commit.GET /sessions/{id}returns the grouped shape withread.latest_sequenceandread.history_complete, with a paged transcript.GET /sessions/{id}/events?after=Nsends committed events after N in order, then follows live, subscribing before it queries so nothing is missed; a legacy session with null sequences loads in created-at order and is flagged incomplete.@agenta/chatand@agenta/entitiesloads the snapshot, follows after its sequence, applies durable events idempotently, discards previews on reconnect and refetches, and ignores unknown event types. Storybook stories for the incomplete and reconnecting states.Tests
@agenta/chat645;@agenta/entities1,469; type checks and lint clean; Storybook builds.7ae0792252); one test-file conflict resolved.44ed249812) renumbered the tracing migration to 006 on top of fix(sessions): settle executions after runner or sandbox loss #6501's 005 (with a one-head guard test) and scoped the cursor key, the unique index, and the read state by project. Re-review: ship as a merge set with fix(sessions): settle executions after runner or sandbox loss #6501, because 006 chains onto fix(sessions): settle executions after runner or sandbox loss #6501's 005 and Alembic fails on this branch alone. The setting is namedAGENTA_SESSIONS_SEQUENCE_WRITEShere and folds intoAGENTA_SESSIONS_HISTORY_WRITESwhen this PR and PR feat(sessions): separate session history from tracing retention and gate immutable record writes #6517 merge, so there is one switch per increment. Live proof, first pass (relay stack on the merge set with fix(sessions): settle executions after runner or sandbox loss #6501): replay after N delivered the typed events in order with no duplicates (sequences 3, 6, 7, 9 after a snapshot at 1), but the client reducer demanded a contiguous chain and never applied them, because every record consumes a sequence and only the six typed events are relayed; a reconnect masked it by resetting the watermark. Round 3 in progress: every event envelope carries the session's currentwatermark, the catch-up ends with the watermark, and the reducer applies any newer event and advances to the watermark. Legacy session: pass (a session from before the branch loads withlatest_sequence0,history_completefalse, eleven null-sequence records in creation order, and the reducer skips null sequences). Concurrent writes: pass (two sessions writing at once keep separate, strictly increasing counters, and each events connection carries only its own session). Reconnect: pass at the protocol level (the durable path advanced from 31 to 34 while the reader was gone; the reconnect after 34 delivered 37 and 39 with nothing repeated). Correction to the masking: a reconnect does not rescue the reducer either; what hides the defect is the unconditional transcript refetch on every connect, which reads the records query with a full contiguous sequence. The reducer fix in round 3 is therefore required, not optional. Round 3 is pushed (head 7c11118): the reducer applies any event newer than its watermark, so a sparse sequence such as 1, 3, 6, 7, 9 applies each event once, and duplicates and older events are dropped. The round 3 review verdict was ship with three small findings; round 4 (head bcc4d52) fixes them: no transcript refetch on a watermark-only advance, the two watermark meanings documented at each site, and the wire field described in the DTO and the zod schema. Scenario 1 re-ran on the proof stack with this head: pass. The second reader rendered tool progress within about a second of the sender, a reconnect after a gap delivered the sparse sequences 61 to 85 without loss or repeat, and the transcript refetch fired six times over about seventy seconds of a turn that wrote dozens of records, not once per write. The two Postgres-backed tests (replay and snapshot) run against the proof stack database: 3 passed, after their fixture was switched to the pytest_asyncio decorator.Agent-generated, low weight. Not merged.
https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
Merge notes: landing this after PR #6501
A throwaway merge of this branch with #6501 (for the live proof) conflicted in five files; the resolutions to repeat when this PR lands after #6501:
apis/fastapi/sessions/models.py: keep this PR's live-frame fields on the ingest request and fix(sessions): settle executions after runner or sandbox loss #6501's cancel, command, execution, and settlement models.apis/fastapi/sessions/router.py: keep this PR's root router wiring and mount fix(sessions): settle executions after runner or sandbox loss #6501's control router.core/sessions/records/service.py: keepdurable_events_from_recordsfor replay together with fix(sessions): settle executions after runner or sandbox loss #6501's settled-execution terminal guard, late-record quarantine, and ending-written marker.dbs/postgres/sessions/records/dao.py: union the SQLAlchemy primitives; keep sequence allocation in the same analytics transaction as the insert; a quarantined record gets NO sequence (stored as evidence, never advances the replay watermark), a redelivered already-sequenced record keeps its sequence, and the quarantine filter applies to every transcript-facing reader (snapshot pages, replay after N, history completeness); a Postgres regression test covers it.engines/sandbox_agent/errors.ts: keep both error classifications.env.pyand the env examples merge to the union. Migration heads after the merge: core026, tracing006. The merged tree passed ruff, the runner suite (2,751), and the API session suite (580 outside the migration-dependent set, which needs the migrated database).