Skip to content

[fix] Rebase #5589 onto #5569 and fix the two blocking review findings - #5608

Merged
mmabrouk merged 5 commits into
fix/5530-agent-session-multi-tabfrom
fix/5530-rebase-onto-5569
Jul 31, 2026
Merged

[fix] Rebase #5589 onto #5569 and fix the two blocking review findings#5608
mmabrouk merged 5 commits into
fix/5530-agent-session-multi-tabfrom
fix/5530-rebase-onto-5569

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

Rebase and review fixes for #5589, as part of the 0.106.2 release pass. Targets fix/5530-agent-session-multi-tab so it lands as part of that PR.

Arda is away, so this resolves the conflict and takes the two Codex findings that block the feature. The other two findings are filed as issues rather than fixed here, per the reasoning below.

Context

#5589 conflicted with its own base. It was rebased onto fe-chore/agent-conversation-split, and two more commits then landed on that branch: 01f11b7ba5 (closes five latent defects in the agent chat slice) and 79056cbc4c (drops per-node scroll measurements). One file conflicted, useSessionHydration.ts, in three places.

Only 01f11b7ba5 was ever involved. 79056cbc4c touches a different file and merged cleanly. Of 01f11b7ba5's five fixes, exactly one lands in this file: routing the hydration path's initial result through the same guard as its refetch, with a synchronous high-water mark instead of a ref that lags a React commit.

The conflict resolution

Git auto-merged the code sitting between the conflict markers, so the merged file already referenced adoptedLength from 01f11b7ba5. Taking #5589's side wholesale would leave a dangling reference that does not compile, and the obvious way to "fix" that (dropping the condition) would silently revert one of the five defect fixes with no test to catch it.

So the resolution keeps #5589's structure and ports the guard into it, using a local adopted flag set when the refetch adopts.

For the third hunk the two sides differ in mechanism, not goal. It resolves to #5589's watermark, because adoptServerTranscript writes the watermark synchronously before any React commit, so a racing delivery with equal-or-fewer records is rejected regardless of arrival order. That is the property 01f11b7ba5 was adding, reached a different way. The invariant is now written as a comment on the watermark write, so it does not stay tribal knowledge.

The two review fixes

The poll timer reset itself every render. useScrollIntent returns a fresh object each render, so adoptServerTranscript was rebuilt each render and the polling effect tore down its 15 second timer and re-armed with reset backoff. The hook re-renders on the liveness query's own 15 second beat, so during a remote run the record catch-up raced its own canceller at the same period. When the remote run ends, polling stops for good, so a starved poll leaves the transcript stale until the session is reopened. That is the feature this PR ships.

The deps now name stable members (intent.armJump, a useCallback([]), and intent.stickRef, a ref) rather than the per-render object. The poll effect also reads the adopter through a ref and keys only on [runningElsewhere, sessionId], so the timer's stability no longer depends on auditing every other dependency.

The poll was deliberately not rewritten as atomWithQuery. That section of web/AGENTS.md prescribes it for data fetching; this is a side-effecting adoption loop with growth-reset backoff, and the chained-timeout shape is intentional so a slow fetch cannot stack on itself.

A skipped transcript write still saved its watermark. When a session is too large for localStorage the write is skipped, but persistSessionMessagesAtom filed the new recordCount anyway. After a reload the old or missing messages pair with a newer watermark, so shouldAdoptServerTranscript rejects the complete server log as not newer and the cache stays stale, potentially forever for a session that never runs again. This breaks the invariant the same file documents on dropSessionMessages, that the two stores must never diverge.

The quota guard now returns {evicted, persisted}, and the atom deletes that session's watermark when persisted is false. An absent watermark reads as 0, so the next open re-syncs from the durable log.

Filed rather than fixed

Two Codex findings are real but were left alone on purpose, because fixing either means a design decision Arda should own. Both are tracked as issues.

The first is that liveness stops polling when its result is empty, so a tab that never regains focus will not notice a run starting elsewhere. The stop is deliberate and Arda's comment cites the request budget. Clicking into the tab heals it and nothing is lost.

The second is that a watermark-less transcript can be overwritten by a lagging server copy with the same message count, during the ingest lag after your own run. It self-heals on the next open. Any real fix adds a completion guard to the shared adoption rule and changes its test contract.

Tests

  • tsc --noEmit on the oss project: clean.
  • eslint on the three changed files: clean. Worth noting because the poll effect deliberately omits the adopter from its dependency list.
  • @agenta/entities unit suite: 931 tests across 65 files pass, including session-transcript-adoption.test.ts. The adoption rule itself is untouched.

Neither useSessionHydration nor the session persistence atoms have direct tests, so these checks prove nothing broke, not that the two fixes work. The quota guard is the testable one if someone wants positive coverage.

What to QA

  • Open a session in two browsers. Drive it from the first through a tool call and an approval. Refresh the second mid-turn. It should converge on the complete transcript rather than caching a partial one.
  • With a run happening in another browser, watch the strip above the composer. It should appear, and the transcript should catch up while the run continues, not only when it ends.
  • Regression: run a normal single-browser session with a tool approval. The transcript follows the streaming answer and the approval resumes in place.

ardaerzin and others added 5 commits July 31, 2026 14:13
Review pass on the AgentConversation split. All five predate the split; the
move only made them legible.

- Attachment `uid` was derived from name+mtime+size, so the same file attached
  twice produced two tray rows with one identity — and `uid` is the React key,
  the preview-URL key, and the remove/view/retry handle. Mint it once and
  enqueue the minted value instead of re-deriving the string.
- `filesToParts` used `Promise.all`, so one unreadable file rejected the whole
  batch. Every one of `handleSubmit`'s four call sites drops its promise, so
  that surfaced as an unhandled rejection and a send that silently did nothing.
  Report failures as data (`allSettled`), hold the send, and name the file
  through the inline rejection channel the other refusals already use.
- The IDE-bubble typewriter kept one timer handle, so a second run (Start over
  mid-animation) stranded the first chain past every cleanup. One cancel, called
  on start, on Start over, and on unmount.
- Session hydration adopted the initial result unguarded while its refresh
  sibling was guarded. Route both through one adopter that tracks what it has
  already adopted, so the guard no longer depends on React commit timing.
- SC-4 follow-release bound listeners to whatever scroll node existed at mount.
  Toggling virtualization off remounts that container, leaving the handlers on a
  detached node for the rest of the session. Track the node as state and key the
  node-bound effects on it; `attachScroll` is now the only way to attach it.
…remounts

The scroll container can be replaced within one session (virtualization toggled,
or an empty conversation rendering the plain node under Virtuoso). Three pieces
of state are measured against whichever node was live and none of them were
reset on the swap:

- the scroll baseline, so the first scroll-down-to-edge on the new node compared
  against the old node's scrollTop and could fail `scrollTop > prevTop` — follow
  then stayed off at the bottom until the next scroll,
- the SC-3 anchor, whose offset was taken in the old node's coordinate space,
- an in-flight glide, still holding listeners on the node that went away.

Clear all three where the node changes. The anchor is dropped rather than
recaptured: its offset has no meaning in the new container, and the next scroll
or pointer-down re-anchors.
…session-multi-tab

Resolves the useSessionHydration.ts conflict between the watermark-based
adoption rework (#5589) and the latent-defect review pass (01f11b7 on
#5569) so both intents survive:

- Keep #5589's single `adoptServerTranscript` adopter (record-count
  watermark via `shouldAdoptServerTranscript`) for the hydration path,
  the SWR revalidation path, and the remote-run poll.
- Port 01f11b7's hydration fix into that structure: a local `adopted`
  flag so a stale empty first result cannot set the "history unavailable"
  notice after the background refetch already adopted real history.
- The rest of 01f11b7's commit-timing guard (local high-water mark /
  adopted snapshot) is subsumed by the watermark ref, which the adopter
  updates synchronously before any React commit; documented that
  invariant on the write so it is not lost.
- The count-based guard and paused-tail special case from the old SWR
  effect are intentionally gone — #5589 replaced them with the watermark
  ("the special case is gone rather than extended").
`useScrollIntent` returns a fresh object every render, and `intent` sat in
`adoptServerTranscript`'s dependency list — so the callback was recreated on
every render, and the remote-run poll effect (keyed on it) tore down and
re-armed its timer each time. The liveness query alone re-renders the hook
~every 15s while a run is live elsewhere, i.e. on the same cadence as the
poll it was cancelling: the record-log catch-up could be starved for the
whole run and its backoff never accumulated.

Two layers, so the timer's stability is true by construction, not by audit:
depend on `intent.armJump` / `intent.stickRef` (both stable) instead of the
per-render `intent` object, and have the poll effect read the current
adopter through a ref, keyed only on `runningElsewhere` + `sessionId`.
… skipped

When even the active session alone overflows the localStorage quota,
`writeMessagesWithQuotaGuard` skips the transcript write — but
`persistSessionMessagesAtom` still wrote the NEW `recordCount`. After a
reload that pairs the OLD (or missing) cached messages with the newer
watermark, so `shouldAdoptServerTranscript` rejects the complete server log
as "not newer" on every open and the stale cache is frozen until the log
happens to outgrow the bogus number.

Surface `persisted` from the quota guard and drop the session's watermark
when the write was skipped, enforcing the invariant already documented on
`dropSessionMessages`: the transcript store and the watermark store must
never diverge.
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jul 31, 2026
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview Jul 31, 2026 6:27pm

Request Review

@dosubot dosubot Bot added the Frontend label Jul 31, 2026
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b5881a9-128a-4dcd-a69b-2b086e8a494b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Status Destroyed (PR closed)

Updated at 2026-07-31T19:18:39.505Z

@mmabrouk
mmabrouk merged commit 6febfe2 into fix/5530-agent-session-multi-tab Jul 31, 2026
39 of 40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Frontend size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants