Skip to content

feat: server-render sessions with snapshot handoff - #1321

Merged
ColeMurray merged 14 commits into
mainfrom
feat/session-view-delta-foundation
Aug 8, 2026
Merged

feat: server-render sessions with snapshot handoff#1321
ColeMurray merged 14 commits into
mainfrom
feat/session-view-delta-foundation

Conversation

@ColeMurray

@ColeMurray ColeMurray commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • serve the canonical, secret-free session representation from GET /sessions/:id for server rendering
  • send the same SessionSnapshot shape on every WebSocket subscribe or reconnect, then continue with the existing semantic live messages
  • keep stable eventId and timelineSequence envelopes for deterministic replay and history pagination
  • fetch code-server and terminal credentials through the separate authenticated, no-store /sessions/:id/sandbox-access endpoint
  • keep rendered session content visible while reconnecting and gate live actions until the socket snapshot is installed

The canonical representation is { session, artifacts, timeline, spawnError }. It has no duplicate session ID, bootstrap lifecycle terminology, or parallel public state endpoint. Sandbox access is explicitly limited to interactive sandbox services; integration and SCM credentials retain their own domain-specific flows.

Simplified architecture

  1. The protected Next.js session route fetches and validates GET /sessions/:id on the server.
  2. A route-local provider gives the existing client page that snapshot for its initial render.
  3. The browser opens the existing session WebSocket.
  4. After authentication and async enrichment, SessionDO performs a final canonical SQLite snapshot read.
  5. A synchronous handoff sends subscribed, registers the socket, and persists its identity without an await between those operations.
  6. A mutation is therefore either included in the snapshot or delivered afterward as an ordered semantic WebSocket message.
  7. Reconnect repeats the same bounded full-snapshot handoff; there is no retained view-delta log or revision recovery state machine.

Removed from the previous design

  • session_view_metadata and session_view_deltas storage
  • view revisions, per-socket applied revisions, and catch-up byte/revision limits
  • delta/snapshot/ready synchronization messages and reducer recovery branches
  • capability negotiation and legacy dual-protocol fan-out
  • the duplicate /sessions/:id/bootstrap resource, duplicate sessionId, and identity refinements
  • the generic /sessions/:id/access name and session_access_changed protocol event
  • raw replay duplicates alongside stable timeline envelopes
  • credential-bearing snapshots and WebSocket messages
  • app-wide auth hydration and a duplicated 700+ line session client
  • client-side timeline sorting, copied event envelopes, and redundant integration scenarios

The simplification passes reduce the review surface from 4,147 changed lines across 51 files to 1,975 lines across 41 files: 2,172 fewer changed lines (52%).

Correctness and security

  • HTTP SSR and WebSocket subscribe share one schema and one SessionDO snapshot builder
  • the snapshot read, send, socket registration, and mapping persistence form one synchronous handoff
  • duplicate subscribe attempts are rejected, including sockets already synchronizing
  • malformed server messages enter bounded reconnect instead of leaving the client connected but unready
  • session snapshots never contain passwords or bearer tokens
  • sandbox credentials are decrypted only for the authenticated sandbox-access endpoint, with a post-decryption row recheck to reject concurrent sandbox replacement
  • sandbox credentials are cleared before reconnect/access invalidation refetches and when sandbox lifecycle state makes them unusable
  • stable event envelopes and cursors preserve ordered replay/history pagination without a client revision state machine

Verification

  • shared, control-plane, and web TypeScript typechecks
  • shared tests: 549 passed
  • control-plane unit tests: 2,334 passed
  • control-plane integration tests: 746 passed
  • web tests: 911 passed
  • shared and control-plane production builds; optimized Next.js production build
  • shared, control-plane, and web ESLint
  • Prettier and git diff --check

The client and control-plane protocol changes are intentionally coordinated; no legacy compatibility path remains.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change introduces validated session bootstrap snapshots, separate authenticated sandbox access, revisioned timeline events, transactional session persistence, WebSocket synchronization readiness, and a server-rendered session client.

Changes

Session synchronization

Layer / File(s) Summary
Session protocol contracts
packages/shared/src/types/server-messages.ts, packages/shared/src/types/index.ts, packages/shared/src/types/*test.ts
Adds sanitized bootstrap schemas, revisioned timeline events, tolerant replay validation, and session_access_changed.
Control-plane snapshot and access flow
packages/control-plane/src/session/durable-object.ts, packages/control-plane/src/routes/session-runtime-proxy.ts, packages/control-plane/src/session/http/routes.ts, packages/control-plane/src/session/contracts.ts
Adds bootstrap and access endpoints, authenticated proxying, transactional snapshot synchronization, duplicate-subscription handling, and private credential access.
Transactional persistence and event projection
packages/control-plane/src/session/repository.ts, packages/control-plane/src/session/sandbox-events.ts, packages/control-plane/src/sandbox/lifecycle/manager.ts, packages/control-plane/src/session/event-stream.ts
Adds repository projections and transactional writes, validates replay and history rows, and replaces credential broadcasts with access-state notifications.
Web bootstrap and socket state
packages/web/src/lib/session-bootstrap.ts, packages/web/src/hooks/use-session-access.ts, packages/web/src/hooks/use-session-socket.ts, packages/web/src/lib/session-socket/*, packages/web/src/hooks/use-session-transport.ts
Loads validated bootstrap data, fetches private access data, initializes ordered timeline state, tracks readiness, manages reconnect health, and revalidates dependent caches.
Server-rendered session surface
packages/web/src/app/(app)/layout.tsx, packages/web/src/app/(app)/session/[id]/*, packages/web/src/app/(app)/session/[id]/session-client.tsx, packages/web/src/lib/auth-session.tsx
Adds server authentication hydration, dynamic bootstrap rendering, loading and error boundaries, and the client session workspace.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: open-inspect

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: server-rendered sessions and snapshot handoff.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/session-view-delta-foundation

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.

❤️ Share

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

Comment thread packages/shared/src/types/server-messages.ts Outdated

@open-inspect open-inspect Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary

Reviewed PR #1321, feat: add session view delta foundation, by @ColeMurray. The PR adds shared V2 session bootstrap/delta contracts, SessionDO-local revision/delta storage primitives, retention/replay helpers, architecture documentation, and focused tests across 13 files (+1255/-5). Overall the foundation is coherent and keeps the production protocol unwired as described.

Critical Issues

None found.

Suggestions

  • Added one inline non-blocking suggestion on packages/shared/src/types/server-messages.ts:116 to consider rejecting empty state_patch objects so no-op deltas do not consume revisions.

Nitpicks

None.

Positive Feedback

  • The new repository API validates deltas before mutating projections and keeps projection writes plus revision/delta insertion inside a synchronous transaction boundary.
  • Contiguous replay and prefix-only retention behavior are covered by both mock repository tests and real Durable Object SQLite integration tests.
  • The docs clearly separate legacy compatibility, V2 synchronization ordering, credentials handling, and rollout/rollback constraints.

Questions

None.

Verification

  • Ran gh pr diff 1321 and reviewed the changed files from a detached PR worktree.
  • Attempted focused shared/control-plane Vitest runs, but dependencies were not installed in the temporary PR worktree (vitest unavailable), so tests were not executed locally.

Verdict

Approve.

@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: 3

🤖 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 `@packages/control-plane/test/integration/session-view-deltas.test.ts`:
- Around line 16-17: Add per-test cleanup for the shared D1 state in the
“session view delta storage” suite, using the existing cleanup helper if
available or clearing the affected tables in beforeEach or afterEach. Ensure
cleanup runs for every test and preserves the current atomic revision
assertions.

In `@packages/shared/src/types/server-messages.ts`:
- Around line 116-145: The sessionStatePatchSchema currently accepts empty
objects, allowing non-changing state patches to be persisted. Require at least
one patch field in sessionStatePatchSchema while preserving the existing
optional field semantics, and add a regression test verifying that an empty
state_patch is rejected.

In `@packages/shared/src/types/websocket.ts`:
- Around line 7-14: Update the subscribe schema object containing viewProtocol,
resumeRevision, and forceSnapshot with object-level validation requiring
viewProtocol to equal 2 whenever either V2 field is present, while preserving
legacy subscriptions without those fields. Add rejection tests covering
resumeRevision and forceSnapshot supplied without viewProtocol.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 856703a4-f7d6-42f2-b8d4-5939d7f6796c

📥 Commits

Reviewing files that changed from the base of the PR and between b63d017 and d38efa6.

📒 Files selected for processing (13)
  • docs/plans/server-rendered-session-websocket-deltas.md
  • packages/control-plane/src/session/repository.test.ts
  • packages/control-plane/src/session/repository.ts
  • packages/control-plane/src/session/schema.test.ts
  • packages/control-plane/src/session/schema.ts
  • packages/control-plane/src/session/types.ts
  • packages/control-plane/test/integration/session-view-deltas.test.ts
  • packages/shared/src/types/boundary-schemas.test.ts
  • packages/shared/src/types/index.ts
  • packages/shared/src/types/server-messages.test.ts
  • packages/shared/src/types/server-messages.ts
  • packages/shared/src/types/type-contracts.test.ts
  • packages/shared/src/types/websocket.ts

Comment thread packages/control-plane/test/integration/session-view-deltas.test.ts Outdated
Comment thread packages/shared/src/types/server-messages.ts Outdated
Comment thread packages/shared/src/types/websocket.ts Outdated
Comment thread packages/control-plane/src/session/repository.ts Outdated
Comment thread packages/control-plane/src/session/repository.ts Outdated
Comment thread packages/shared/src/types/server-messages.ts Outdated
Comment thread packages/shared/src/types/server-messages.ts Outdated
…ta-foundation

# Conflicts:
#	packages/control-plane/src/session/repository.ts
#	packages/shared/src/types/type-contracts.test.ts
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @open-inspect[bot], Action: pull_request

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/control-plane/src/session/repository.ts (1)

386-402: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Derive the age cutoff from the first non-expired revision.

MAX(revision) over expired rows can select a later revision after an unexpired row. For example, if revisions 1 and 3 are expired but revision 2 is not, this code deletes through revision 3 and removes revision 2 early.

Use the revision immediately before the first row where created_at >= createdBefore. If no such row exists, use currentRevision. Add a test with out-of-order created_at values.

The PR objective requires prefix-only retention.

Proposed SQL direction
- SELECT MAX(revision) AS revision FROM session_view_deltas WHERE created_at < ?
+ SELECT COALESCE(MIN(revision) - 1, ?) AS revision
+ FROM session_view_deltas
+ WHERE created_at >= ?
🤖 Prompt for 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.

In `@packages/control-plane/src/session/repository.ts` around lines 386 - 402,
Update the retention logic in the transactionSync callback to derive ageCutoff
from the revision immediately preceding the first session_view_deltas row with
created_at >= options.createdBefore, rather than MAX(revision) among expired
rows. If no non-expired row exists, use currentRevision, preserving prefix-only
deletion and existing cutoff validation; add coverage for out-of-order
created_at values where an expired later revision must not delete an unexpired
earlier revision.
🤖 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 `@packages/control-plane/src/session/repository.ts`:
- Around line 331-337: Update updateSessionTitleWithViewDelta and the underlying
updateSessionTitle transaction flow to verify that exactly one session row is
affected, throwing inside transactionSync when the count is not one so the
revision and delta insertion roll back. Add a test covering an absent or invalid
sessionId that confirms current_revision and the session delta log remain
unchanged.

---

Outside diff comments:
In `@packages/control-plane/src/session/repository.ts`:
- Around line 386-402: Update the retention logic in the transactionSync
callback to derive ageCutoff from the revision immediately preceding the first
session_view_deltas row with created_at >= options.createdBefore, rather than
MAX(revision) among expired rows. If no non-expired row exists, use
currentRevision, preserving prefix-only deletion and existing cutoff validation;
add coverage for out-of-order created_at values where an expired later revision
must not delete an unexpired earlier revision.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4adcea36-7fb8-427c-bc97-16ef74f46d32

📥 Commits

Reviewing files that changed from the base of the PR and between d38efa6 and 8367532.

📒 Files selected for processing (10)
  • packages/control-plane/src/session/repository.test.ts
  • packages/control-plane/src/session/repository.ts
  • packages/control-plane/src/session/types.ts
  • packages/control-plane/test/integration/session-view-deltas.test.ts
  • packages/shared/src/types/boundary-schemas.test.ts
  • packages/shared/src/types/index.ts
  • packages/shared/src/types/server-messages.test.ts
  • packages/shared/src/types/server-messages.ts
  • packages/shared/src/types/type-contracts.test.ts
  • packages/shared/src/types/websocket.ts
💤 Files with no reviewable changes (1)
  • packages/shared/src/types/index.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/control-plane/src/session/types.ts
  • packages/shared/src/types/type-contracts.test.ts
  • packages/control-plane/src/session/repository.test.ts
  • packages/control-plane/test/integration/session-view-deltas.test.ts
  • packages/shared/src/types/server-messages.ts

Comment thread packages/control-plane/src/session/repository.ts Outdated
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @open-inspect[bot], Action: pull_request

@open-inspect open-inspect Bot changed the title feat: add session view delta foundation feat: server-render sessions with WebSocket deltas Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @open-inspect[bot], Action: pull_request

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @open-inspect[bot], Action: pull_request

@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: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/control-plane/src/session/pull-request-service.ts (1)

102-128: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Optional view-delta methods let the pull-request flow persist state without a delta. PullRequestRepository declares updateSessionBranchWithViewDelta, updateSessionRepositoryBranchWithViewDelta, and createArtifactWithViewDelta as optional. Each call site therefore carries a fallback that writes the projection and records no delta. A protocol-v2 client that misses those deltas keeps a stale view until the next snapshot. SessionRepository implements all three, so the optionality exists only to accommodate test doubles; other doubles in this pull request alias the delta methods onto the double instead.

  • packages/control-plane/src/session/pull-request-service.ts#L102-L128: remove the ? from all three method declarations so every implementation must record a delta.
  • packages/control-plane/src/session/pull-request-service.ts#L302-L326: delete both fallback branches and call updateSessionRepositoryBranchWithViewDelta and updateSessionBranchWithViewDelta directly.
  • packages/control-plane/src/session/pull-request-service.ts#L373-L376: delete the ?? fallback and call createArtifactWithViewDelta directly.
🤖 Prompt for 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.

In `@packages/control-plane/src/session/pull-request-service.ts` around lines 102
- 128, Make all three view-delta methods required in PullRequestRepository by
removing their optional markers at
packages/control-plane/src/session/pull-request-service.ts:102-128. At
packages/control-plane/src/session/pull-request-service.ts:302-326, remove both
fallback branches and call updateSessionRepositoryBranchWithViewDelta and
updateSessionBranchWithViewDelta directly; at
packages/control-plane/src/session/pull-request-service.ts:373-376, remove the
?? fallback and call createArtifactWithViewDelta directly.
packages/web/src/lib/session-socket/reducer.ts (1)

285-309: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reset lastAppliedRevision when the connection falls back to the legacy protocol.

This case sets protocol: "legacy" and clears viewEvents, but it keeps lastAppliedRevision from the previous state. After a V2 session downgrades to legacy, the ref in packages/web/src/hooks/use-session-socket.ts still reports that revision through getResumeRevision, and the next subscribe frame carries a resumeRevision that no longer describes any state the client holds.

Clear the field so the two protocols cannot mix revision state.

🐛 Proposed fix
         ready: true,
         protocol: "legacy",
+        lastAppliedRevision: null,
         recovering: false,
         sync: null,
🤖 Prompt for 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.

In `@packages/web/src/lib/session-socket/reducer.ts` around lines 285 - 309, Reset
lastAppliedRevision to its initial empty value in the reducer branch that
constructs the legacy protocol state alongside protocol: "legacy" and the
cleared viewEvents. Ensure the legacy fallback no longer preserves the previous
revision used by getResumeRevision and subsequent subscribe frames.
🧹 Nitpick comments (19)
packages/control-plane/src/session/repository.ts (3)

56-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename SESSION_VIEW_RETENTION_INTERVAL to state its unit.

The constant counts revisions, not milliseconds. The name reads like a duration next to DEFAULT_SESSION_VIEW_RETENTION_AGE_MS. Rename it to SESSION_VIEW_RETENTION_INTERVAL_REVISIONS.

As per coding guidelines: "Use milliseconds for TypeScript durations and timeouts, and encode the unit in names such as timeoutMs or INACTIVITY_TIMEOUT_MS; never use a bare timeout."

🤖 Prompt for 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.

In `@packages/control-plane/src/session/repository.ts` around lines 56 - 58,
Rename the constant SESSION_VIEW_RETENTION_INTERVAL to
SESSION_VIEW_RETENTION_INTERVAL_REVISIONS and update every reference to it,
preserving its value and revision-count semantics.

Source: Coding guidelines


1434-1454: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the event-row validation and drop the unreachable null return.

Two points:

  1. The "read the row, validate timeline_sequence, throw" block is repeated in createGitSyncEventWithViewDelta (lines 1319-1324) and createArtifactAndEventWithViewDelta (lines 1572-1577). Extract one private helper that returns the validated SessionViewEvent.
  2. writeEventViewDelta is declared number | null, but appendSessionViewDelta returns number and throws on any failure. The null branch is unreachable, so createEventWithViewDelta, upsertTokenEventWithViewDelta, upsertToolCallEventWithViewDelta, and upsertExecutionCompleteEventWithViewDelta all advertise a result their callers can never receive. Narrow all five signatures to number.
🤖 Prompt for 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.

In `@packages/control-plane/src/session/repository.ts` around lines 1434 - 1454,
Extract the repeated event-row lookup and timeline_sequence validation from
createGitSyncEventWithViewDelta, writeEventViewDelta, and
createArtifactAndEventWithViewDelta into one private helper returning
SessionViewEvent, and reuse it when constructing event upsert items. Since
appendSessionViewDelta and writeEventViewDelta do not return null, narrow
writeEventViewDelta plus createEventWithViewDelta,
upsertTokenEventWithViewDelta, upsertToolCallEventWithViewDelta, and
upsertExecutionCompleteEventWithViewDelta from number | null to number.

485-495: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Session-scoped delta wrappers do not verify the affected row count. updateSessionBranch and updateSessionStatus both accept a sessionId and return void. If the id does not match the stored session row, the projection stays unchanged while the transaction still advances the revision and stores a state patch. updateSessionTitleWithViewDelta already guards against this by consuming rowsWritten and throwing inside the transaction. Apply the same invariant to the remaining session-scoped wrappers.

  • packages/control-plane/src/session/repository.ts#L485-L495: make updateSessionBranch return whether exactly one row changed, and throw inside updateSessionBranchWithViewDelta when it does not.
  • packages/control-plane/src/session/repository.ts#L538-L548: make updateSessionStatus return whether exactly one row changed, and throw inside updateSessionStatusWithViewDelta when it does not.
🤖 Prompt for 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.

In `@packages/control-plane/src/session/repository.ts` around lines 485 - 495,
Update packages/control-plane/src/session/repository.ts lines 485-495 and
538-548: make updateSessionBranch and updateSessionStatus return whether exactly
one row was affected, then have updateSessionBranchWithViewDelta and
updateSessionStatusWithViewDelta validate that result and throw inside the
transaction when it is not exactly one, matching
updateSessionTitleWithViewDelta.
packages/control-plane/test/integration/helpers.ts (1)

383-394: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update the stale comment above the collector.

The comment at lines 379-381 states that the helper terminates on the subscribed message. The predicate now terminates on session_ready for protocol-v2 clients. Extend the comment to cover both cases.

🤖 Prompt for 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.

In `@packages/control-plane/test/integration/helpers.ts` around lines 383 - 394,
Update the comment above the collector in the subscription helper to state that
collection terminates on “subscribed” for non-v2 clients and “session_ready” for
protocol-v2 clients; leave the collector predicate and surrounding logic
unchanged.
packages/control-plane/src/session/durable-object.ts (3)

1720-1742: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use satisfies ServerMessage instead of as ServerMessage.

Both branches cast with as, which suppresses shape checking. The new v2 synchronization path in the same file uses satisfies ServerMessage (lines 1592, 1601, 1611, 1625). Use satisfies here so a future change to session_history_page or history_page fails at compile time.

🤖 Prompt for 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.

In `@packages/control-plane/src/session/durable-object.ts` around lines 1720 -
1742, Replace the `as ServerMessage` assertions in both `safeSend` calls within
the history-page branch of the session handler with `satisfies ServerMessage`,
preserving the existing `session_history_page` and `history_page` payloads while
enabling compile-time shape checking.

1547-1562: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The snapshot retry loop cannot change its outcome.

No await occurs between getSessionBootstrap() and readBoundedViewCatchUp(...). A Durable Object processes one request at a time, so no mutation can land between those two reads. If the first attempt returns null deltas, the second attempt observes the same state and returns null again.

Replace the loop with a single attempt, or document the concurrency the retry is intended to absorb.

🤖 Prompt for 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.

In `@packages/control-plane/src/session/durable-object.ts` around lines 1547 -
1562, Remove the two-attempt retry loop around getSessionBootstrap and
readBoundedViewCatchUp, making the snapshot path perform one read and return its
result directly. Preserve the existing null handling when bootstrap or deltas
are unavailable, and keep sendV2SyncMessages unchanged.

1841-1845: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the dead updateSessionTitleIfUnset path.

updateSessionTitleIfUnset has no production callers; production code now uses onlyIfUnset plus updateSessionTitleWithViewDelta, which preserves the title-matches-session invariant without a separate conditional update. Keeping the dead method invites future writes that update the title without a matching view delta.

🤖 Prompt for 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.

In `@packages/control-plane/src/session/durable-object.ts` around lines 1841 -
1845, Remove the unused updateSessionTitleIfUnset method and its
conditional-update path from the session title handling in durable-object.ts.
Ensure production callers continue using onlyIfUnset together with
updateSessionTitleWithViewDelta so title changes always produce the matching
view delta.
packages/control-plane/src/routes/session-runtime-proxy.test.ts (1)

64-86: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert that the access response stays uncacheable.

The access route returns decrypted code-server and ttyd credentials. SessionDO.handleSessionAccess sets Cache-Control: private, no-store (packages/control-plane/src/session/durable-object.ts line 2016). This test does not verify that the proxy preserves that header on the accepted response.

Add an assertion on accepted.headers.get("Cache-Control") so a future proxy change cannot silently make credentials cacheable.

🤖 Prompt for 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.

In `@packages/control-plane/src/routes/session-runtime-proxy.test.ts` around lines
64 - 86, Add an assertion after the accepted response in the “keeps session
access client-only” test, verifying accepted.headers.get("Cache-Control") equals
"private, no-store".
packages/control-plane/src/session/event-stream.ts (1)

56-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reduce the duplication between the v1 and v2 read paths.

getViewReplay duplicates getReplay and getViewHistoryPage duplicates getHistoryPage. Only the row-parsing function differs. The cursor-mapping expression is now written twice, verbatim.

Two suggestions:

  1. Extract the page.nextCursor mapping into one helper and share it between getHistoryPage and getViewHistoryPage.
  2. Name the getViewHistoryPage return type, as getHistoryPage does with SessionHistoryPage. The inline object type cannot be referenced by callers.

Also applies to: 92-120

🤖 Prompt for 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.

In `@packages/control-plane/src/session/event-stream.ts` around lines 56 - 63,
Reduce duplication in getReplay/getViewReplay and
getHistoryPage/getViewHistoryPage by extracting the shared
page.nextCursor-to-cursor mapping into a helper and reusing it in both read
paths, while keeping only the row-parsing function different. Define and use a
named return type for getViewHistoryPage, matching getHistoryPage’s
SessionHistoryPage pattern, so callers can reference its result type.
packages/control-plane/src/session/websocket-manager.ts (1)

247-283: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use named defaults for client view state.

Lines 251-252 and Lines 281-282 repeat the protocol and revision defaults. Define named shared constants for these defaults, then use them for persistence and recovery.

As per coding guidelines, “Define each TypeScript default value exactly once as a named constant and import it wherever needed.”

🤖 Prompt for 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.

In `@packages/control-plane/src/session/websocket-manager.ts` around lines 247 -
283, Define shared named constants for the default client view protocol and
applied view revision, then use them in persistClientMapping and
getClientViewState for method defaults and recovered mapping fallbacks. Ensure
each default value is declared exactly once and reused consistently.

Source: Coding guidelines

packages/web/src/app/(app)/session/[id]/error.tsx (1)

6-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Report the error instead of discarding it.

The component declares error but never uses it. Bootstrap failures, schema-validation failures, and unexpected control-plane statuses all reach this boundary and disappear silently. Log the error on mount and show error.digest, so a user can quote the digest and an operator can correlate it with server logs.

♻️ Proposed refactor
 "use client";
 
+import { useEffect } from "react";
 import { Button } from "`@/components/ui/button`";
 import { ErrorBanner } from "`@/components/ui/error-banner`";
 
 export default function SessionError({
+  error,
   reset,
 }: {
   error: Error & { digest?: string };
   reset: () => void;
 }) {
+  useEffect(() => {
+    console.error("session route error", error);
+  }, [error]);
+
   return (
     <div className="flex h-full items-center justify-center p-6">
       <ErrorBanner
         role="alert"
         className="flex max-w-md flex-col items-center gap-4 p-6 text-center"
       >
         <p>Session data is temporarily unavailable.</p>
+        {error.digest ? (
+          <p className="text-xs opacity-70">Reference: {error.digest}</p>
+        ) : null}
         <Button type="button" onClick={reset}>
           Try again
         </Button>
       </ErrorBanner>
     </div>
   );
 }
🤖 Prompt for 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.

In `@packages/web/src/app/`(app)/session/[id]/error.tsx around lines 6 - 25,
Update SessionError to use the declared error: log it when the component mounts
and render error.digest when available so users can report the identifier.
Preserve the existing retry behavior and error banner messaging.
packages/web/src/app/(app)/layout.test.tsx (1)

27-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the AuthenticationUnavailableError branch.

The suite covers the authenticated and unauthenticated paths. It does not cover the third branch in AppLayout, which renders the unavailability banner instead of redirecting or hydrating. That branch also decides that unrelated errors are rethrown. Add two cases to lock this behavior.

🧪 Proposed additional tests
+  it("renders the unavailable state instead of redirecting", async () => {
+    mocks.getServerAuthSession.mockRejectedValue(new AuthenticationUnavailableError());
+
+    const result = await AppLayout({ children: <div>protected</div> });
+
+    expect(result.type).not.toBe(AuthSessionHydration);
+    expect(mocks.redirect).not.toHaveBeenCalled();
+  });
+
+  it("rethrows unrelated failures", async () => {
+    mocks.getServerAuthSession.mockRejectedValue(new Error("boom"));
+    await expect(AppLayout({ children: <div>protected</div> })).rejects.toThrow("boom");
+  });

Import AuthenticationUnavailableError from @/lib/authentication-unavailable-error at the top of the file.

🤖 Prompt for 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.

In `@packages/web/src/app/`(app)/layout.test.tsx around lines 27 - 47, Extend the
“protected app layout” suite around AppLayout with two tests: verify
AuthenticationUnavailableError from getServerAuthSession renders the
authentication-unavailability banner, and verify any unrelated error is
rethrown. Import AuthenticationUnavailableError from the indicated
authentication-unavailable-error module and preserve the existing authenticated
and redirect expectations.
packages/web/src/app/api/sessions/[id]/access/route.ts (1)

5-23: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Wrap the proxy call in the conventional outer try/catch.

controlPlaneUserFetch rejects on transport failure. This handler has no error handling, so the rejection escapes to Next.js and produces an unstructured 500. Sibling proxy routes under packages/web/src/app/api/ use a single outer try/catch that returns HTTP 500. Align this route with that convention.

🛡️ Proposed fix
 export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
   const session = await getServerAuthSession();
   if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
 
-  const { id } = await params;
-  const response = await controlPlaneUserFetch(`/sessions/${encodeURIComponent(id)}/access`, {
-    cache: "no-store",
-    headers: { Accept: "application/json" },
-  });
-  return new Response(response.body, {
-    status: response.status,
-    statusText: response.statusText,
-    headers: {
-      "Content-Type": response.headers.get("Content-Type") ?? "application/json",
-      "Cache-Control": "private, no-store",
-      Vary: "Cookie",
-    },
-  });
+  try {
+    const { id } = await params;
+    const response = await controlPlaneUserFetch(`/sessions/${encodeURIComponent(id)}/access`, {
+      cache: "no-store",
+      headers: { Accept: "application/json" },
+    });
+    return new Response(response.body, {
+      status: response.status,
+      statusText: response.statusText,
+      headers: {
+        "Content-Type": response.headers.get("Content-Type") ?? "application/json",
+        "Cache-Control": "private, no-store",
+        Vary: "Cookie",
+      },
+    });
+  } catch {
+    return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
+  }
 }

Based on learnings: for Next.js API route handlers under packages/web/src/app/api/, use a single outer try/catch that returns HTTP 500 for any thrown error.

🤖 Prompt for 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.

In `@packages/web/src/app/api/sessions/`[id]/access/route.ts around lines 5 - 23,
Wrap the authenticated proxy flow in GET with a single outer try/catch,
including the controlPlaneUserFetch call and response construction. Return a
structured HTTP 500 response from the catch block when transport or other errors
are thrown, while preserving the existing unauthorized response and successful
proxy status, headers, and body behavior.

Source: Learnings

packages/web/src/lib/session-socket/reducer.test.ts (1)

298-325: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the ready state before the close, and cover the snapshot rejection guards.

snapshot holds the state after socket_closed, so expect(snapshot.ready).toBe(false) passes whether or not session_ready ever took effect. Capture the state before the close to prove the snapshot path reached readiness.

The session_snapshot guards at packages/web/src/lib/session-socket/reducer.ts Lines 327-337 are untested. Each one drives a full reconnect, so a regression there is costly.

💚 Proposed additions
-      const snapshot = reduce(
-        started,
+      const synced = reduce(
+        started,
         serverMessage({
           type: "session_snapshot",
           bootstrap: createBootstrap({
             viewRevision: 8,
             state: createSessionState({ title: "Snapshot" }),
             replay: { events: [], hasMore: false, cursor: null },
           }),
         }),
         serverMessage({
           type: "session_ready",
           sessionId: "session-1",
           participantId: "participant-1",
           appliedRevision: 8,
-        }),
-        { type: "socket_closed" }
+        })
       );
+      const snapshot = reduce(synced, { type: "socket_closed" });
 
+      expect(synced.ready).toBe(true);
       expect(snapshot.ready).toBe(false);
       expect(snapshot.sessionState?.title).toBe("Snapshot");
       expect(snapshot.events).toEqual([]);
     });
+
+    it("rejects a snapshot for a different session", () => {
+      const started = reduce(
+        createSessionSocketState(createBootstrap()),
+        serverMessage({ type: "session_sync_started", mode: "snapshot", targetRevision: 8 })
+      );
+      const rejected = reduce(
+        started,
+        serverMessage({
+          type: "session_snapshot",
+          bootstrap: createBootstrap({
+            sessionId: "session-2",
+            viewRevision: 8,
+            state: createSessionState({ id: "session-2" }),
+          }),
+        })
+      );
+
+      expect(rejected.recoveryNonce).toBe(1);
+      expect(rejected.sessionState?.id).toBe("session-1");
+    });
+
+    it("rejects a snapshot that arrives without an active sync", () => {
+      const rejected = reduce(
+        createSessionSocketState(createBootstrap()),
+        serverMessage({ type: "session_snapshot", bootstrap: createBootstrap({ viewRevision: 8 }) })
+      );
+
+      expect(rejected.recoveryNonce).toBe(1);
+    });
🤖 Prompt for 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.

In `@packages/web/src/lib/session-socket/reducer.test.ts` around lines 298 - 325,
Update the test around the snapshot flow to capture and assert the reducer state
immediately after session_ready, before applying socket_closed, verifying ready
is true along with the snapshot state. Add focused coverage for each
session_snapshot rejection guard in the reducer, exercising full reconnect
scenarios and asserting invalid snapshots are rejected without replacing the
existing canonical state.
packages/web/src/hooks/use-session-transport.test.tsx (1)

115-136: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a case for reconnect() without arguments.

This test only exercises reconnect(true). The unforced path is untested. A test that calls reconnect() and asserts forceSnapshot: false with resumeRevision: 7 would fail if a caller accidentally supplies a truthy first argument.

That failure mode is live today: packages/web/src/app/(app)/session/[id]/session-client.tsx Line 331 passes the hook reconnect straight to onClick, so a MouseEvent becomes forceSnapshot.

💚 Proposed additional case
     expect(FakeWebSocket.instances[1].sentMessages[0]).not.toHaveProperty("resumeRevision");
+
+    act(() => rendered.result.current.reconnect());
+    await waitFor(() => expect(FakeWebSocket.instances).toHaveLength(3));
+    act(() => FakeWebSocket.instances[2].open());
+    expect(FakeWebSocket.instances[2].sentMessages[0]).toEqual(
+      expect.objectContaining({ viewProtocol: 2, resumeRevision: 7, forceSnapshot: false })
+    );
   });
🤖 Prompt for 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.

In `@packages/web/src/hooks/use-session-transport.test.tsx` around lines 115 -
136, Add a separate assertion path in the “sends the current resume revision and
can force snapshot recovery” test that invokes
rendered.result.current.reconnect() without arguments, opens the resulting
websocket, and verifies the initial message contains forceSnapshot: false and
resumeRevision: 7. Keep the existing reconnect(true) coverage unchanged.
packages/web/src/hooks/use-session-socket.ts (2)

171-183: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the access-clearing sandbox status set into one shared constant.

The list ["spawning", "stale", "stopped", "failed"] is repeated here at Lines 175 and 181. packages/web/src/lib/session-socket/reducer.ts repeats the same set twice more, in applyDelta (Lines 184-188) and in the sandbox_status case (Lines 422-427). A new status value must be added in four places.

Export one constant and a predicate from packages/web/src/lib/session-socket/reducer.ts, then use it at all sites.

♻️ Proposed shared predicate
// packages/web/src/lib/session-socket/reducer.ts
export const ACCESS_CLEARING_SANDBOX_STATUSES = ["spawning", "stale", "stopped", "failed"] as const;

export function clearsSandboxAccess(status: string | undefined): boolean {
  return (
    status !== undefined &&
    (ACCESS_CLEARING_SANDBOX_STATUSES as readonly string[]).includes(status)
  );
}
-      const clearsAccess =
-        message.type === "sandbox_spawning" ||
-        message.type === "sandbox_error" ||
-        (message.type === "sandbox_status" &&
-          ["spawning", "stale", "stopped", "failed"].includes(message.status)) ||
-        (message.type === "session_delta" &&
-          message.delta.operations.some(
-            (operation) =>
-              operation.type === "state_patch" &&
-              operation.patch.sandboxStatus !== undefined &&
-              ["spawning", "stale", "stopped", "failed"].includes(operation.patch.sandboxStatus)
-          ));
+      const clearsAccess =
+        message.type === "sandbox_spawning" ||
+        message.type === "sandbox_error" ||
+        (message.type === "sandbox_status" && clearsSandboxAccess(message.status)) ||
+        (message.type === "session_delta" &&
+          message.delta.operations.some(
+            (operation) =>
+              operation.type === "state_patch" && clearsSandboxAccess(operation.patch.sandboxStatus)
+          ));
🤖 Prompt for 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.

In `@packages/web/src/hooks/use-session-socket.ts` around lines 171 - 183, Export
ACCESS_CLEARING_SANDBOX_STATUSES and clearsSandboxAccess from reducer.ts, then
replace every inline ["spawning", "stale", "stopped", "failed"] check in
applyDelta, the sandbox_status case, and the use-session-socket clearsAccess
logic with the shared predicate. Remove the duplicated status arrays while
preserving the existing undefined and access-clearing behavior.

85-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider deriving the resume revision only from reducer state.

revisionRef has two writers. The effect at Line 97 copies state.lastAppliedRevision. handleMessage also writes it directly at Lines 149, 155, and 158. When the reducer rejects a message, handleMessage can still advance the ref, and the effect does not re-sync because state.lastAppliedRevision did not change.

The current flow recovers, because every reducer rejection raises recoveryNonce and triggers reconnect(true), and a forced snapshot omits resumeRevision. The redundancy is still a latent hazard if the recovery path changes later.

A single writer removes the hazard. Keep the effect and delete the direct writes in handleMessage. The ref then always matches committed reducer state.

🤖 Prompt for 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.

In `@packages/web/src/hooks/use-session-socket.ts` around lines 85 - 99, Make the
reducer state the sole writer of revisionRef: retain the useEffect that assigns
state.lastAppliedRevision and remove all direct revisionRef.current assignments
from handleMessage. Preserve the existing message handling and recovery behavior
while ensuring rejected messages cannot advance the resume revision.
packages/web/src/hooks/use-session-transport.ts (1)

202-217: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Derive the V2 message-type list from the shared contract.

This literal list must stay in step with the V2 message types in @open-inspect/shared. If the shared package adds a revisioned message type and this list is not updated, a malformed frame of that type is dropped silently and the client keeps a stale revision without recovering.

Export a set of revisioned message type names from the shared server-messages module and use it here.

♻️ Proposed change
// packages/shared/src/types/server-messages.ts
export const REVISIONED_MESSAGE_TYPES = new Set([
  "session_sync_started",
  "session_delta",
  "session_snapshot",
  "session_ready",
  "session_history_page",
]);
-        if (
-          type === "session_sync_started" ||
-          type === "session_delta" ||
-          type === "session_snapshot" ||
-          type === "session_ready" ||
-          type === "session_history_page"
-        ) {
+        if (typeof type === "string" && REVISIONED_MESSAGE_TYPES.has(type)) {
           handlersRef.current.onProtocolError?.();
         }
🤖 Prompt for 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.

In `@packages/web/src/hooks/use-session-transport.ts` around lines 202 - 217,
Export a shared REVISIONED_MESSAGE_TYPES set from the server-messages module
containing all revisioned V2 message names, then import and use it in the
malformed-data handling of the session transport hook instead of the local
literal type comparisons. Preserve invoking onProtocolError only when the
extracted message type belongs to that shared set.
packages/web/src/lib/session-socket/reducer.ts (1)

103-130: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Batch the upserts and use a locale-independent tie-break.

Two concerns in these helpers.

First, ordering. localeCompare resolves against the runtime default locale and its collation data. Two browsers can order the same two eventId values differently. The client timeline then differs from the server order for events that share a timelineSequence. Use a plain comparison for opaque identifiers.

Second, cost. upsertViewEvent re-sorts the whole array for each single item. session_history_page at Lines 380-382 calls it in a loop, so a page of k items over n existing events costs O(k · n log n). withViewEvents then remaps every event through toUiSandboxEvent.

Add a batch upsert and sort once.

♻️ Proposed refactor
 function sortedViewEvents(items: SessionViewEvent[]): SessionViewEvent[] {
   return [...items].sort(
-    (a, b) => a.timelineSequence - b.timelineSequence || a.eventId.localeCompare(b.eventId)
+    (a, b) =>
+      a.timelineSequence - b.timelineSequence ||
+      (a.eventId < b.eventId ? -1 : a.eventId > b.eventId ? 1 : 0)
   );
 }
 
+function upsertViewEvents(
+  items: SessionViewEvent[],
+  nextItems: SessionViewEvent[]
+): SessionViewEvent[] {
+  if (nextItems.length === 0) return items;
+  const byId = new Map(items.map((item) => [item.eventId, item]));
+  for (const nextItem of nextItems) byId.set(nextItem.eventId, nextItem);
+  return sortedViewEvents([...byId.values()]);
+}
+
 function upsertViewEvent(
   items: SessionViewEvent[],
   nextItem: SessionViewEvent
 ): SessionViewEvent[] {
-  const existingIndex = items.findIndex((item) => item.eventId === nextItem.eventId);
-  if (existingIndex === -1) return sortedViewEvents([...items, nextItem]);
-  const next = [...items];
-  next[existingIndex] = nextItem;
-  return sortedViewEvents(next);
+  return upsertViewEvents(items, [nextItem]);
 }

Then replace the loop in the session_history_page case:

-      let viewEvents = state.viewEvents;
-      for (const item of message.items) viewEvents = upsertViewEvent(viewEvents, item);
+      const viewEvents = upsertViewEvents(state.viewEvents, message.items);
🤖 Prompt for 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.

In `@packages/web/src/lib/session-socket/reducer.ts` around lines 103 - 130,
Update sortedViewEvents to use a deterministic plain comparison for eventId ties
instead of localeCompare. Add a batch upsert helper that applies all incoming
SessionViewEvent items, resolves existing eventIds, and sorts once; have the
session_history_page case use it instead of repeatedly calling upsertViewEvent.
Ensure withViewEvents performs the final sort and toUiSandboxEvent mapping only
once for the batch.
🤖 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 `@packages/control-plane/src/routes/session-runtime-proxy.ts`:
- Around line 243-248: Replace simpleProxyRoute for the /sessions/:id/bootstrap
route with the authentication-aware proxy that validates ctx.principal, while
preserving the existing SessionInternalPaths.bootstrap target and custom
“Session not found” response.

In `@packages/control-plane/src/session/durable-object.ts`:
- Around line 1565-1571: Update readBoundedViewCatchUp to measure each
serialized delta’s UTF-8 encoded byte length rather than
JSON.stringify(...).length before comparing the total with
MAX_VIEW_CATCH_UP_BYTES. Preserve the existing revision and null-result checks
and the byte-limit behavior.
- Around line 1966-1969: Update the bootstrap construction around
getSessionState so environmentName is resolved with resolveEnvironmentName
outside the synchronous transactionSync block instead of being hardcoded to
null. Thread the asynchronous result through sendV2Synchronization and
handleSubscribe as needed, while preserving null only when the environment has
been deleted; ensure handleBootstrap and WebSocket synchronization receive the
resolved name.

In `@packages/control-plane/src/session/repository.ts`:
- Around line 640-662: Extract the repository-state projection logic from
getSessionRepositoryStateProjection and SessionDO.getSessionRepositoryStates
into one shared function, then call it from both snapshot and live-delta paths.
Preserve the Durable Object semantics for repoId when a member row exists, and
filter out artifacts with null URLs before findPrArtifactForRepo so both callers
produce identical results.
- Around line 1694-1701: Update parseArtifactMetadata to catch JSON.parse
failures and return null for malformed metadata, while preserving the existing
object-validation behavior for valid JSON. Ensure artifactFromRow no longer
propagates parsing errors into the appendSessionViewDelta transaction.
- Around line 341-347: Wrap the periodic pruneSessionViewDeltas call in
appendSessionViewDelta with failure containment so pruning errors do not
propagate after the mutation is committed. Preserve the existing retention
arguments and always return the committed revision, including when pruning
fails.

In `@packages/control-plane/src/session/websocket-manager.ts`:
- Around line 286-292: Update advanceClientViewRevision to ignore revisions that
are less than or equal to the client’s current appliedViewRevision, and ensure
updateWsClientViewRevision only persists a revision when it is greater than the
stored value. Preserve the existing state lookup and client/repository update
flow for newer revisions.

In `@packages/control-plane/test/integration/session-bootstrap-v2.test.ts`:
- Line 11: Add an afterEach cleanup hook within the “session bootstrap and V2
synchronization” describe block to remove retained session fixtures or clear the
relevant shared D1 tables after every test, preventing state from leaking
between tests.

In `@packages/web/src/app/`(app)/session/[id]/session-client.tsx:
- Around line 717-725: Extract the 300-millisecond delay used by
handleInputChange into a module-level constant named to include its millisecond
unit, then use that constant in setTimeout instead of the bare literal.

In `@packages/web/src/hooks/use-session-socket.ts`:
- Around line 208-215: Update packages/web/src/hooks/use-session-socket.ts lines
208-215 around the recoveryNonce effect to track recovery attempts, apply
exponential backoff, enforce a maximum retry limit, and reset the counter when
state.ready becomes true. In packages/web/src/lib/session-socket/reducer.ts
lines 327-337, update the bootstrap state.id validation to compare against the
existing session id only when state.sessionState is non-null, and apply the same
guard to the session_ready validation around line 365. In
packages/web/src/hooks/use-session-transport.ts lines 344-369, preserve
reconnectAttempts.current = 0 for user-initiated reconnects but skip that reset
when forceSnapshot is true so recovery reconnects retain transport backoff.
- Line 352: Wrap the returned reconnect handler in a zero-argument function so
React click events cannot be passed as forceSnapshot; update
UseSessionSocketReturn and the hook return around reconnect while preserving the
internal recovery effect’s explicit reconnect(true) call.

In `@packages/web/src/lib/session-socket/swr-revalidation.ts`:
- Around line 41-50: Update the session_access_changed and session_ready
branches in the SWR revalidation key builder to use the shared sessionAccessKey
helper, ensuring the access key applies encodeURIComponent to sessionId
consistently with use-session-access.ts. If the helper cannot be imported from
the hook module, move it to a shared key module and reuse it from both
locations.

---

Outside diff comments:
In `@packages/control-plane/src/session/pull-request-service.ts`:
- Around line 102-128: Make all three view-delta methods required in
PullRequestRepository by removing their optional markers at
packages/control-plane/src/session/pull-request-service.ts:102-128. At
packages/control-plane/src/session/pull-request-service.ts:302-326, remove both
fallback branches and call updateSessionRepositoryBranchWithViewDelta and
updateSessionBranchWithViewDelta directly; at
packages/control-plane/src/session/pull-request-service.ts:373-376, remove the
?? fallback and call createArtifactWithViewDelta directly.

In `@packages/web/src/lib/session-socket/reducer.ts`:
- Around line 285-309: Reset lastAppliedRevision to its initial empty value in
the reducer branch that constructs the legacy protocol state alongside protocol:
"legacy" and the cleared viewEvents. Ensure the legacy fallback no longer
preserves the previous revision used by getResumeRevision and subsequent
subscribe frames.

---

Nitpick comments:
In `@packages/control-plane/src/routes/session-runtime-proxy.test.ts`:
- Around line 64-86: Add an assertion after the accepted response in the “keeps
session access client-only” test, verifying
accepted.headers.get("Cache-Control") equals "private, no-store".

In `@packages/control-plane/src/session/durable-object.ts`:
- Around line 1720-1742: Replace the `as ServerMessage` assertions in both
`safeSend` calls within the history-page branch of the session handler with
`satisfies ServerMessage`, preserving the existing `session_history_page` and
`history_page` payloads while enabling compile-time shape checking.
- Around line 1547-1562: Remove the two-attempt retry loop around
getSessionBootstrap and readBoundedViewCatchUp, making the snapshot path perform
one read and return its result directly. Preserve the existing null handling
when bootstrap or deltas are unavailable, and keep sendV2SyncMessages unchanged.
- Around line 1841-1845: Remove the unused updateSessionTitleIfUnset method and
its conditional-update path from the session title handling in
durable-object.ts. Ensure production callers continue using onlyIfUnset together
with updateSessionTitleWithViewDelta so title changes always produce the
matching view delta.

In `@packages/control-plane/src/session/event-stream.ts`:
- Around line 56-63: Reduce duplication in getReplay/getViewReplay and
getHistoryPage/getViewHistoryPage by extracting the shared
page.nextCursor-to-cursor mapping into a helper and reusing it in both read
paths, while keeping only the row-parsing function different. Define and use a
named return type for getViewHistoryPage, matching getHistoryPage’s
SessionHistoryPage pattern, so callers can reference its result type.

In `@packages/control-plane/src/session/repository.ts`:
- Around line 56-58: Rename the constant SESSION_VIEW_RETENTION_INTERVAL to
SESSION_VIEW_RETENTION_INTERVAL_REVISIONS and update every reference to it,
preserving its value and revision-count semantics.
- Around line 1434-1454: Extract the repeated event-row lookup and
timeline_sequence validation from createGitSyncEventWithViewDelta,
writeEventViewDelta, and createArtifactAndEventWithViewDelta into one private
helper returning SessionViewEvent, and reuse it when constructing event upsert
items. Since appendSessionViewDelta and writeEventViewDelta do not return null,
narrow writeEventViewDelta plus createEventWithViewDelta,
upsertTokenEventWithViewDelta, upsertToolCallEventWithViewDelta, and
upsertExecutionCompleteEventWithViewDelta from number | null to number.
- Around line 485-495: Update packages/control-plane/src/session/repository.ts
lines 485-495 and 538-548: make updateSessionBranch and updateSessionStatus
return whether exactly one row was affected, then have
updateSessionBranchWithViewDelta and updateSessionStatusWithViewDelta validate
that result and throw inside the transaction when it is not exactly one,
matching updateSessionTitleWithViewDelta.

In `@packages/control-plane/src/session/websocket-manager.ts`:
- Around line 247-283: Define shared named constants for the default client view
protocol and applied view revision, then use them in persistClientMapping and
getClientViewState for method defaults and recovered mapping fallbacks. Ensure
each default value is declared exactly once and reused consistently.

In `@packages/control-plane/test/integration/helpers.ts`:
- Around line 383-394: Update the comment above the collector in the
subscription helper to state that collection terminates on “subscribed” for
non-v2 clients and “session_ready” for protocol-v2 clients; leave the collector
predicate and surrounding logic unchanged.

In `@packages/web/src/app/`(app)/layout.test.tsx:
- Around line 27-47: Extend the “protected app layout” suite around AppLayout
with two tests: verify AuthenticationUnavailableError from getServerAuthSession
renders the authentication-unavailability banner, and verify any unrelated error
is rethrown. Import AuthenticationUnavailableError from the indicated
authentication-unavailable-error module and preserve the existing authenticated
and redirect expectations.

In `@packages/web/src/app/`(app)/session/[id]/error.tsx:
- Around line 6-25: Update SessionError to use the declared error: log it when
the component mounts and render error.digest when available so users can report
the identifier. Preserve the existing retry behavior and error banner messaging.

In `@packages/web/src/app/api/sessions/`[id]/access/route.ts:
- Around line 5-23: Wrap the authenticated proxy flow in GET with a single outer
try/catch, including the controlPlaneUserFetch call and response construction.
Return a structured HTTP 500 response from the catch block when transport or
other errors are thrown, while preserving the existing unauthorized response and
successful proxy status, headers, and body behavior.

In `@packages/web/src/hooks/use-session-socket.ts`:
- Around line 171-183: Export ACCESS_CLEARING_SANDBOX_STATUSES and
clearsSandboxAccess from reducer.ts, then replace every inline ["spawning",
"stale", "stopped", "failed"] check in applyDelta, the sandbox_status case, and
the use-session-socket clearsAccess logic with the shared predicate. Remove the
duplicated status arrays while preserving the existing undefined and
access-clearing behavior.
- Around line 85-99: Make the reducer state the sole writer of revisionRef:
retain the useEffect that assigns state.lastAppliedRevision and remove all
direct revisionRef.current assignments from handleMessage. Preserve the existing
message handling and recovery behavior while ensuring rejected messages cannot
advance the resume revision.

In `@packages/web/src/hooks/use-session-transport.test.tsx`:
- Around line 115-136: Add a separate assertion path in the “sends the current
resume revision and can force snapshot recovery” test that invokes
rendered.result.current.reconnect() without arguments, opens the resulting
websocket, and verifies the initial message contains forceSnapshot: false and
resumeRevision: 7. Keep the existing reconnect(true) coverage unchanged.

In `@packages/web/src/hooks/use-session-transport.ts`:
- Around line 202-217: Export a shared REVISIONED_MESSAGE_TYPES set from the
server-messages module containing all revisioned V2 message names, then import
and use it in the malformed-data handling of the session transport hook instead
of the local literal type comparisons. Preserve invoking onProtocolError only
when the extracted message type belongs to that shared set.

In `@packages/web/src/lib/session-socket/reducer.test.ts`:
- Around line 298-325: Update the test around the snapshot flow to capture and
assert the reducer state immediately after session_ready, before applying
socket_closed, verifying ready is true along with the snapshot state. Add
focused coverage for each session_snapshot rejection guard in the reducer,
exercising full reconnect scenarios and asserting invalid snapshots are rejected
without replacing the existing canonical state.

In `@packages/web/src/lib/session-socket/reducer.ts`:
- Around line 103-130: Update sortedViewEvents to use a deterministic plain
comparison for eventId ties instead of localeCompare. Add a batch upsert helper
that applies all incoming SessionViewEvent items, resolves existing eventIds,
and sorts once; have the session_history_page case use it instead of repeatedly
calling upsertViewEvent. Ensure withViewEvents performs the final sort and
toUiSandboxEvent mapping only once for the batch.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f360b30-0bbf-4632-808c-12648a719ac3

📥 Commits

Reviewing files that changed from the base of the PR and between 8367532 and 240f7da.

📒 Files selected for processing (52)
  • packages/control-plane/src/router.ts
  • packages/control-plane/src/routes/session-runtime-proxy.test.ts
  • packages/control-plane/src/routes/session-runtime-proxy.ts
  • packages/control-plane/src/session/contracts.ts
  • packages/control-plane/src/session/diffs/service.test.ts
  • packages/control-plane/src/session/diffs/service.ts
  • packages/control-plane/src/session/durable-object.ts
  • packages/control-plane/src/session/event-stream.ts
  • packages/control-plane/src/session/http/handlers/sandbox.handler.test.ts
  • packages/control-plane/src/session/http/handlers/sandbox.handler.ts
  • packages/control-plane/src/session/http/routes.test.ts
  • packages/control-plane/src/session/http/routes.ts
  • packages/control-plane/src/session/message-queue.test.ts
  • packages/control-plane/src/session/message-queue.ts
  • packages/control-plane/src/session/messenger.test.ts
  • packages/control-plane/src/session/messenger.ts
  • packages/control-plane/src/session/presence-service.test.ts
  • packages/control-plane/src/session/pull-request-refresh.ts
  • packages/control-plane/src/session/pull-request-service.ts
  • packages/control-plane/src/session/repository.test.ts
  • packages/control-plane/src/session/repository.ts
  • packages/control-plane/src/session/sandbox-events.test.ts
  • packages/control-plane/src/session/sandbox-events.ts
  • packages/control-plane/src/session/schema.test.ts
  • packages/control-plane/src/session/schema.ts
  • packages/control-plane/src/session/session-status-service.test.ts
  • packages/control-plane/src/session/session-status-service.ts
  • packages/control-plane/src/session/websocket-manager.test.ts
  • packages/control-plane/src/session/websocket-manager.ts
  • packages/control-plane/src/types.ts
  • packages/control-plane/test/integration/helpers.ts
  • packages/control-plane/test/integration/session-bootstrap-v2.test.ts
  • packages/control-plane/test/integration/session-view-deltas.test.ts
  • packages/web/src/app/(app)/layout.test.tsx
  • packages/web/src/app/(app)/layout.tsx
  • packages/web/src/app/(app)/session/[id]/error.tsx
  • packages/web/src/app/(app)/session/[id]/loading.tsx
  • packages/web/src/app/(app)/session/[id]/page.tsx
  • packages/web/src/app/(app)/session/[id]/session-client.tsx
  • packages/web/src/app/api/sessions/[id]/access/route.ts
  • packages/web/src/hooks/use-session-access.test.tsx
  • packages/web/src/hooks/use-session-access.ts
  • packages/web/src/hooks/use-session-socket.ts
  • packages/web/src/hooks/use-session-transport.test.tsx
  • packages/web/src/hooks/use-session-transport.ts
  • packages/web/src/lib/auth-session.tsx
  • packages/web/src/lib/session-bootstrap.test.ts
  • packages/web/src/lib/session-bootstrap.ts
  • packages/web/src/lib/session-socket/reducer.test.ts
  • packages/web/src/lib/session-socket/reducer.ts
  • packages/web/src/lib/session-socket/swr-revalidation.test.ts
  • packages/web/src/lib/session-socket/swr-revalidation.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/control-plane/src/session/repository.test.ts

Comment thread packages/control-plane/src/routes/session-runtime-proxy.ts Outdated
Comment thread packages/control-plane/src/session/durable-object.ts Outdated
Comment thread packages/control-plane/src/session/durable-object.ts Outdated
Comment thread packages/control-plane/src/session/repository.ts Outdated
Comment thread packages/control-plane/src/session/repository.ts Outdated
Comment thread packages/control-plane/test/integration/session-bootstrap-v2.test.ts Outdated
Comment thread packages/web/src/app/(app)/session/[id]/session-client.tsx Outdated
Comment thread packages/web/src/hooks/use-session-socket.ts Outdated
Comment thread packages/web/src/hooks/use-session-socket.ts
Comment thread packages/web/src/lib/session-socket/swr-revalidation.ts Outdated
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @open-inspect[bot], Action: pull_request

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @ColeMurray, Action: pull_request

@ColeMurray ColeMurray changed the title feat: server-render sessions with WebSocket deltas feat: server-render sessions with snapshot handoff Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @ColeMurray, Action: pull_request

@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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/web/src/hooks/use-session-transport.ts (1)

186-195: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Recover from invalid server messages.

Line 191 drops schema-invalid payloads while keeping the socket open. A corrupt initial subscribed message then leaves connected true and ready false indefinitely. Live actions remain blocked until the user manually reconnects.

Route JSON and schema failures through the bounded reconnect path. Do not silently ignore a failed synchronization message.

🤖 Prompt for 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.

In `@packages/web/src/hooks/use-session-transport.ts` around lines 186 - 195,
Update handleSocketMessage to route both JSON parsing and parseWsMessage schema
failures, including the current !data case, through the existing bounded
reconnect path instead of silently returning or only logging. Ensure an invalid
initial subscribed message triggers recovery so connection state cannot remain
connected but unready, while preserving normal onMessage handling for valid
payloads.
packages/control-plane/src/session/durable-object.ts (1)

1802-1812: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Set Cache-Control: private, no-store on the early 404 and 409 responses.

Lines 1804, 1808, and 1811 return without cache headers. The success path and the conflict path at line 1830 both set private, no-store. A cached 409 would pin a client to "Sandbox access is unavailable" after the sandbox reaches ready.

Hoist the header object and reuse it for every response in this handler.

🐛 Proposed fix
   private async handleSessionAccess(): Promise<Response> {
+    const headers = { "Cache-Control": "private, no-store" };
     if (!this.getSession()) {
-      return Response.json({ error: "Session not found" }, { status: 404 });
+      return Response.json({ error: "Session not found" }, { status: 404, headers });
     }
     const sandbox = this.getSandbox();
     if (!sandbox) {
-      return Response.json({ error: "Sandbox access is unavailable" }, { status: 409 });
+      return Response.json({ error: "Sandbox access is unavailable" }, { status: 409, headers });
     }
     if (sandbox.status !== "ready" && sandbox.status !== "running") {
-      return Response.json({ error: "Sandbox access is unavailable" }, { status: 409 });
+      return Response.json({ error: "Sandbox access is unavailable" }, { status: 409, headers });
     }
🤖 Prompt for 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.

In `@packages/control-plane/src/session/durable-object.ts` around lines 1802 -
1812, Update handleSessionAccess to define a shared Cache-Control private,
no-store header object and reuse it on the early 404 session-not-found response
and both 409 sandbox-unavailable responses, matching the existing success and
conflict response behavior.
🧹 Nitpick comments (1)
packages/control-plane/src/session/durable-object.ts (1)

1717-1725: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Log the decryption failure before returning null.

decryptStoredAccessValue swallows every decryption error. The access endpoint then reports codeServer: null or ttyd: null, which is indistinguishable from "no credential stored". A rotated or mismatched REPO_SECRETS_ENCRYPTION_KEY would silently disable code-server and terminal access with no operator signal.

Add a warning log in the catch branch. Do not log the ciphertext or the plaintext.

♻️ Proposed change
     } catch {
+      this.log.warn("Failed to decrypt stored sandbox access value");
       return null;
     }
🤖 Prompt for 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.

In `@packages/control-plane/src/session/durable-object.ts` around lines 1717 -
1725, Update decryptStoredAccessValue to log a warning in its decryption-error
catch branch before returning null, including useful failure context while
excluding both the encrypted ciphertext and decrypted plaintext.
🤖 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 `@docs/adr/0003-session-snapshot-handoff.md`:
- Around line 19-33: Restore the V2 revisioned synchronization contract: in
docs/adr/0003-session-snapshot-handoff.md lines 19-33, document bootstrap
revision resume, ordered V2 delta delivery, and snapshot recovery when a
revision gap is detected; in packages/control-plane/src/session/schema.test.ts
lines 246-252, replace assertions that remove revision and delta persistence
with assertions protecting the required revision and delta schema contract.

In `@packages/control-plane/test/integration/session-from-environment.test.ts`:
- Around line 63-66: Update getSessionState to assert response.ok after fetching
/internal/bootstrap, then validate the parsed bootstrap payload before accessing
and returning state. Ensure endpoint errors and malformed responses fail at this
helper with a clear assertion rather than relying on the TypeScript JSON type
assertion.

In `@packages/shared/src/types/server-messages.ts`:
- Around line 73-98: Update the live sandbox event schema and inferred type
around the sandbox_event server message to use the same timeline envelope as
sessionTimelineEventSchema, including eventId, timelineSequence, and event.
Preserve sandbox event validation while ensuring live events expose stable
identities consistent with bootstrap and history events.
- Around line 117-130: Update the subscribed branch of serverMessageSchema to
refine the parsed object so its sessionId equals state.id, matching
sessionBootstrapSchema; reject mismatches while preserving valid messages. Add a
regression test covering a subscribed message with differing identities and
assert validation fails.

In `@packages/web/src/hooks/use-session-socket.ts`:
- Around line 72-80: Update useSessionSocket to reset all session-scoped state
when sessionId changes, including the sessionSocketReducer state, pending prompt
ref, and subscription waiters; ensure the new state is initialized from the
current initialBootstrap while preserving existing behavior within one session.

In `@packages/web/src/lib/session-socket/reducer.ts`:
- Around line 81-95: Normalize replay token events consistently in
createSessionSocketState and the history_page ingestion path by applying
collapseReplayTokenEvents after converting and sorting timeline items, matching
the subscribed path. Ensure timelineEvents and derived events use the collapsed
results, and add coverage for history pages containing multiple token events.

---

Outside diff comments:
In `@packages/control-plane/src/session/durable-object.ts`:
- Around line 1802-1812: Update handleSessionAccess to define a shared
Cache-Control private, no-store header object and reuse it on the early 404
session-not-found response and both 409 sandbox-unavailable responses, matching
the existing success and conflict response behavior.

In `@packages/web/src/hooks/use-session-transport.ts`:
- Around line 186-195: Update handleSocketMessage to route both JSON parsing and
parseWsMessage schema failures, including the current !data case, through the
existing bounded reconnect path instead of silently returning or only logging.
Ensure an invalid initial subscribed message triggers recovery so connection
state cannot remain connected but unready, while preserving normal onMessage
handling for valid payloads.

---

Nitpick comments:
In `@packages/control-plane/src/session/durable-object.ts`:
- Around line 1717-1725: Update decryptStoredAccessValue to log a warning in its
decryption-error catch branch before returning null, including useful failure
context while excluding both the encrypted ciphertext and decrypted plaintext.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fe569bf6-52b7-415e-9667-c09a065a08a5

📥 Commits

Reviewing files that changed from the base of the PR and between 1c6bb3a and b467b31.

📒 Files selected for processing (34)
  • docs/adr/0003-session-snapshot-handoff.md
  • packages/control-plane/src/sandbox/lifecycle/manager.ts
  • packages/control-plane/src/session/durable-object.ts
  • packages/control-plane/src/session/event-stream.test.ts
  • packages/control-plane/src/session/event-stream.ts
  • packages/control-plane/src/session/http/handlers/sandbox.handler.test.ts
  • packages/control-plane/src/session/http/handlers/sandbox.handler.ts
  • packages/control-plane/src/session/message-queue.test.ts
  • packages/control-plane/src/session/repository.test.ts
  • packages/control-plane/src/session/repository.ts
  • packages/control-plane/src/session/sandbox-events.test.ts
  • packages/control-plane/src/session/sandbox-events.ts
  • packages/control-plane/src/session/schema.test.ts
  • packages/control-plane/src/session/websocket-manager.ts
  • packages/control-plane/test/integration/create-pr.test.ts
  • packages/control-plane/test/integration/helpers.ts
  • packages/control-plane/test/integration/session-bootstrap.test.ts
  • packages/control-plane/test/integration/session-from-environment.test.ts
  • packages/control-plane/test/integration/websocket-client.test.ts
  • packages/shared/src/types/boundary-schemas.test.ts
  • packages/shared/src/types/index.ts
  • packages/shared/src/types/server-messages.test.ts
  • packages/shared/src/types/server-messages.ts
  • packages/shared/src/types/type-contracts.test.ts
  • packages/web/src/app/(app)/session/[id]/session-client.tsx
  • packages/web/src/hooks/use-session-socket.test.tsx
  • packages/web/src/hooks/use-session-socket.ts
  • packages/web/src/hooks/use-session-transport.test.tsx
  • packages/web/src/hooks/use-session-transport.ts
  • packages/web/src/lib/session-bootstrap.test.ts
  • packages/web/src/lib/session-socket/reducer.test.ts
  • packages/web/src/lib/session-socket/reducer.ts
  • packages/web/src/lib/session-socket/swr-revalidation.test.ts
  • packages/web/src/lib/session-socket/swr-revalidation.ts
💤 Files with no reviewable changes (2)
  • packages/web/src/hooks/use-session-transport.test.tsx
  • packages/control-plane/src/session/message-queue.test.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/web/src/lib/session-bootstrap.test.ts
  • packages/shared/src/types/type-contracts.test.ts
  • packages/control-plane/src/session/http/handlers/sandbox.handler.test.ts
  • packages/control-plane/src/session/sandbox-events.test.ts
  • packages/web/src/lib/session-socket/swr-revalidation.test.ts
  • packages/web/src/lib/session-socket/reducer.test.ts
  • packages/web/src/app/(app)/session/[id]/session-client.tsx

Comment thread docs/adr/0003-session-snapshot-handoff.md
Comment thread packages/control-plane/test/integration/session-from-environment.test.ts Outdated
Comment thread packages/shared/src/types/server-messages.ts Outdated
Comment thread packages/shared/src/types/server-messages.ts Outdated
Comment thread packages/web/src/hooks/use-session-socket.ts
Comment thread packages/web/src/lib/session-socket/reducer.ts Outdated
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @ColeMurray, Action: pull_request

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @ColeMurray, Action: pull_request

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @ColeMurray, Action: pull_request

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @ColeMurray, Action: pull_request

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