Skip to content

Resume the action stream across reconnects - #334

Merged
ndisidore merged 5 commits into
mainfrom
feat/action-reconnect-resume
Aug 25, 2026
Merged

Resume the action stream across reconnects#334
ndisidore merged 5 commits into
mainfrom
feat/action-reconnect-resume

Conversation

@ndisidore

@ndisidore ndisidore commented Aug 25, 2026

Copy link
Copy Markdown
Member

#298 covered the card half with a reconnect refetch sweep and left a TODO for the mechanism it had just added server-side: subscribeToActions takes a startAfter and replays every record changed since, off the last-change index.

This branch wires the client to it:

  • useWorkspaceOpen links every stub it mints to its workspace id. When a linked store closes settled, it parks its last change time (max appliedAt ?? createdAt received, the server's index key) under that id.
  • The next store for the workspace opens exactly like a cold one (subscribe, then page the pending filter) but passes the watermark as startAfter, so the gap replays through the subscription as upserts and live entries patch per-record consumers instead of each refetching. The watermark is the only state carried across stubs; the pending set re-pages fresh, which is small and indexed since Bound every action-log read path #298.
  • Only a settled session sets the watermark. After an unsettled or errored one the previous watermark stands, which replays more but stays correct.
  • The Activity pane keeps its loaded window and cursor across a resumed swap, and drops a page still in flight on the old stub.

Hook tests cover the watermark handoff (settled, unsettled, errored, release-and-reacquire) and the history window surviving the swap. No server changes.

@github-actions github-actions Bot added the workshop/frontend Changes to the Workshop frontend label Aug 25, 2026
@ndisidore
ndisidore marked this pull request as ready for review August 25, 2026 19:50
@github-actions

Copy link
Copy Markdown

Preview: pr334-feat-action-r-3e4416bc

https://pr334-feat-action-r-3e4416bc-router.cloudflare-os-previews.workers.dev

Dashboard · deleted when this PR closes

Base automatically changed from chore/scale-action-logs to main August 25, 2026 20:36
@ndisidore
ndisidore force-pushed the feat/action-reconnect-resume branch from 1002a93 to 9b4ad7a Compare August 25, 2026 20:36
@github-actions github-actions Bot added kernel Changes to the Workshop kernel workshop/shared Changes to shared Workshop APIs labels Aug 25, 2026
A settled shared action store now parks its pending set and change-time
watermark (max appliedAt ?? createdAt received) by workspace key when it
closes. The next store linked to the same key seeds from that carryover
and subscribes with startAfter, so the server replays only the gap as
upserts instead of the store re-paging the whole pending set — the
subscribe call resolving is the settled signal. Unsettled or errored
sessions never seed a resume, and unlinked stubs keep cold-open behavior.

useWorkspaceOpen links every minted Overseer stub to its workspace id.
useActionHistory keeps its loaded window and cursor across a resumed
swap (dropping in-flight old-stub pages), and ChatInterface's gap-refetch
effect is now only the cold-open safety net, resolving its startAfter TODO.
Drop the pending-set carryover: a settled store now parks just its last
change time by workspace key, and the next linked store opens exactly as
a cold one — subscribe, then page pending — passing the watermark as
startAfter. The gap still replays as upserts through the subscription
(which is what useActionHistory and ChatInterface rely on), the fresh
pages re-snapshot the pending set, and the page loop stays the single
settled signal. Buys back the seeding/skip-page dual path for the cost
of re-paging a set PR #298 already made cheap.
Review fixes for the reconnect-resume stack:

A store now parks its watermark only after a cleanly settled session:
pages drained AND the subscribe call resolved (the server delivers the
gap replay before resolving, so 'ready' alone can predate undelivered
replay records) AND no entry listener threw on a delivery. ChatInterface
carries cards a refetch run failed to repair (error, or cancelled
mid-loop) to the next reconnect and retries them even when the resume
replay covers everything else — restoring the repair channel the resume
skip removed.

Also: actionChangeTime() in workshop-shared now owns the
appliedAt ?? createdAt formula shared by the server's byLastChanged
index key, the client watermark, and Activity's display; a status-only
useActionStatus() spares ChatInterface/useActionHistory a re-render per
pending-set change; useActionHistory dedupes its session resets; the
ChatInterface action test uses the shared harness root (fixing a leaked
rAF queue) — plus regression tests for the three behavior changes.
@ndisidore
ndisidore force-pushed the feat/action-reconnect-resume branch from d542870 to cbda68a Compare August 25, 2026 21:25
The consumer-side recovery shell (useActionStatus, resumeFallbackRequired,
unrepairedCardsRef, entryListenerFailed) defended a resumed subscription
failing while the stub stays healthy — reachable only through a server-side
bug, since transport failures swap the stub. The store's invariant already
guarantees eventual healing without it: a failed session never parks a
watermark, so the next stub swap replays its entire gap from the last good
one. Until then the store shows status 'error', so nothing degrades silently.

entryListenerFailed additionally protected nothing real: the refetch path
applies updates through the same applyActionLogUpdateToCachedMessages the
listener uses, so a record that throws in one path throws in both.

Kept the subscription-resolved watermark gate — a page-only watermark is
poison (a pending record's createdAt can exceed a missed resolution's
appliedAt, hiding it from every future replay) — and documented on
actionLogResumed why consumers may trust it without a failure path.
@maxwellpeterson
maxwellpeterson self-requested a review August 25, 2026 21:51
@ndisidore
ndisidore merged commit 6223e26 into main Aug 25, 2026
10 of 12 checks passed
@ndisidore
ndisidore deleted the feat/action-reconnect-resume branch August 25, 2026 22:19
darjss pushed a commit to darjss/cloudflare-os-erxes that referenced this pull request Aug 26, 2026
* feat(frontend): resume action subscriptions with startAfter on reconnect

A settled shared action store now parks its pending set and change-time
watermark (max appliedAt ?? createdAt received) by workspace key when it
closes. The next store linked to the same key seeds from that carryover
and subscribes with startAfter, so the server replays only the gap as
upserts instead of the store re-paging the whole pending set — the
subscribe call resolving is the settled signal. Unsettled or errored
sessions never seed a resume, and unlinked stubs keep cold-open behavior.

useWorkspaceOpen links every minted Overseer stub to its workspace id.
useActionHistory keeps its loaded window and cursor across a resumed
swap (dropping in-flight old-stub pages), and ChatInterface's gap-refetch
effect is now only the cold-open safety net, resolving its startAfter TODO.

* refactor(frontend): carry only a resume watermark across reconnects

Drop the pending-set carryover: a settled store now parks just its last
change time by workspace key, and the next linked store opens exactly as
a cold one — subscribe, then page pending — passing the watermark as
startAfter. The gap still replays as upserts through the subscription
(which is what useActionHistory and ChatInterface rely on), the fresh
pages re-snapshot the pending set, and the page loop stays the single
settled signal. Buys back the seeding/skip-page dual path for the cost
of re-paging a set PR cloudflare#298 already made cheap.

* fix(frontend): recover from failed action log resume

* fix(frontend): harden the resume watermark and repair channel

Review fixes for the reconnect-resume stack:

A store now parks its watermark only after a cleanly settled session:
pages drained AND the subscribe call resolved (the server delivers the
gap replay before resolving, so 'ready' alone can predate undelivered
replay records) AND no entry listener threw on a delivery. ChatInterface
carries cards a refetch run failed to repair (error, or cancelled
mid-loop) to the next reconnect and retries them even when the resume
replay covers everything else — restoring the repair channel the resume
skip removed.

Also: actionChangeTime() in workshop-shared now owns the
appliedAt ?? createdAt formula shared by the server's byLastChanged
index key, the client watermark, and Activity's display; a status-only
useActionStatus() spares ChatInterface/useActionHistory a re-render per
pending-set change; useActionHistory dedupes its session resets; the
ChatInterface action test uses the shared harness root (fixing a leaked
rAF queue) — plus regression tests for the three behavior changes.

* refactor(frontend): drop the resume failure fallbacks

The consumer-side recovery shell (useActionStatus, resumeFallbackRequired,
unrepairedCardsRef, entryListenerFailed) defended a resumed subscription
failing while the stub stays healthy — reachable only through a server-side
bug, since transport failures swap the stub. The store's invariant already
guarantees eventual healing without it: a failed session never parks a
watermark, so the next stub swap replays its entire gap from the last good
one. Until then the store shows status 'error', so nothing degrades silently.

entryListenerFailed additionally protected nothing real: the refetch path
applies updates through the same applyActionLogUpdateToCachedMessages the
listener uses, so a record that throws in one path throws in both.

Kept the subscription-resolved watermark gate — a page-only watermark is
poison (a pending record's createdAt can exceed a missed resolution's
appliedAt, hiding it from every future replay) — and documented on
actionLogResumed why consumers may trust it without a failure path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kernel Changes to the Workshop kernel workshop/frontend Changes to the Workshop frontend workshop/shared Changes to shared Workshop APIs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants