Skip to content

Capture saves to the cloud library while signed in, through a pending capture queue - #294

Merged
SunkenInTime merged 6 commits into
mainfrom
t3/capture-follows-library
Oct 13, 2026
Merged

SunkenInTime merged 6 commits into
mainfrom
t3/capture-follows-library

Conversation

@SunkenInTime

@SunkenInTime SunkenInTime commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Signed in, sends held: three captures land on the card at once, Open waits with "Sending…", sends resume and the editor opens on the last page; then a second client with an empty local library opens the strategy and steps through its pages

Capture in the replay viewer always saved to the local library. A signed-in player's captured strategy ended up on this device while the rest of their library lived in the cloud, so it never showed on their other devices. Now, while signed in, Capture saves to the cloud library the way New Strategy does: in the cloud folder open in My Library when that folder is the player's own (never a shared one), or at the top level. Signed out, or on a build without cloud, nothing changes.

The first version of this PR wrote each capture straight to the server and kept Capture disabled until the server answered. Astra's second review showed that path could forget its strategy or make a second one, and it put the network in the player's way. Dara chose a client-only queue with no server change. This is that queue.

How a capture gets to the cloud

Capture waits only on this device. Each capture becomes one versioned JSON record in its own Hive box (replay_captures_box, opened with the other outboxes). The record holds every id the server will see, fixed at capture time: the strategy, the page, each item, each op, and a client id kept for the whole replay session. Capture returns as soon as the record is written, about 100 ms in the release build from click to "N pages captured" on screen. Rapid captures are written one after another in the order they were made.

ReplayCaptureQueue sends captures of one strategy in order (lib/providers/collab/replay_capture_queue_provider.dart), and only while the record's account is signed in and online:

  • The first capture calls createWithInitialPage with its fixed ids. When it's sent again, the server answers with the strategy it already has.
  • Each later capture sends a PageAddOp against the revision it just read. If someone changed the strategy in between, it reads again and resends under a new op id, since the server answers a repeated op id with its first answer.
  • Then the page's items go out in batches, and each op's answer is recorded. A capture leaves the queue only when its page and every item are accepted.
  • If an answer is lost (timeout, connection), the same ops are sent again with the same ids. The server answers them without writing again, so nothing lands twice.
  • If the server refuses, the capture waits for the user. Retry gives whatever didn't land new op ids. A refused capture holds back the later captures of its strategy, never another strategy's.
  • Before the page and before each batch of items, every time (first send, resend, Retry), the queue reads the strategy again. Item adds aren't checked against the map by the server, so this is what keeps old-map positions off a strategy that moved. If it was deleted or moved to another map, that capture and the later ones for the same strategy fail with that reason, and the next capture starts a new strategy, even if the failed ones were discarded. If the page itself was deleted after it landed, the capture says so.
  • If writing a capture's progress to this device fails, the queue doesn't stop. That capture is held as stopped on this device: the capture card gives the reason ("Icarus couldn't store its progress on this device") with Retry, the library card offers Retry, and sign-out is blocked. Other strategies' captures keep going.
  • A server that needs a newer Icarus is reported to the app, so its update notice shows. Sending stops until the server accepts this build again, and the card says "Update Icarus to finish saving N".
  • If the folder a capture was made for is gone or isn't the player's when it's sent, the strategy goes to the top level and a toast says so.
  • A record this build can't read stays in the box untouched. It counts as unreliable cloud work, which the library card reports and which blocks sign-out.

Where the player sees it:

  • Capture card: "3 pages captured", then "3 sending" (or "3 waiting for the connection" when offline, or "3 sending. Couldn't confirm the last send, trying again." after a lost answer), then "Saved to your cloud library". A refused capture shows "1 capture couldn't be saved" with the reason, Retry and Discard. A strategy that was deleted or moved to another map offers only Discard.
  • Open waits with "Sending…" until the captured page is in the cloud, then opens the editor on it. Pressing it again stops waiting. If a capture failed, a toast says why the strategy can't open.
  • The library's cloud work card and the sign-out check both count captures still on their way or refused. The library card offers "Retry replay capture" / "Discard replay capture".
  • Discard removes the record from this device. A capture that was already sent can't be taken back, so in that case the toast says "Discarded. It may still appear in your library."

Kept from the first version: items get fresh ids, because the server keeps one row per item id across all strategies and a replay's ids repeat in every strategy captured from it. Each player keeps one root id across pages (<root>~cp1~<uuid>, via move-to-cloud's MovedItemIds). transition_planner.dart now pairs an agent with its copy whether or not it shows a view cone, so a player who died between two captures moves instead of fading out and back in. That also affects copy-to-page: an agent whose copy gained or lost its view cone now glides instead of fading, which local strategies already did. openCloudStrategy takes a pageId, so Open lands on the captured page.

No server change. No convex/ change, no new op type, no payload change. The queue uses createWithInitialPage, fetchShell and applyBatch with PageAddOp/ElementAddOp, all of which shipped clients already send.

Ground truth

Windows release build against the dev deployment, test account +icarus-l2c-final4, driven from inside the app. To stand in for "can't reach the server", a scratch switch in my build (not in this PR) held the queue's sends.

  • Rapid captures stay instant. With sends held, three captures each showed on the card about 100 ms after the click (logged: 103, 103, 104 ms). The card read "3 pages captured · 3 sending", and Open turned into "Sending…" (card). The library card said "Syncing cloud work: 3 saved changes are being sent from 1 strategy" (library).
  • Quit with pending captures, relaunch. I quit with the three records still on disk and nothing in the cloud. On relaunch they were delivered within the first 3 s: one strategy, 3 pages (13, 14 and 13 items), with items on pages 2 and 3 carrying <page-1 id>~cp1~<uuid> ids. The queue was empty afterwards.
  • Delivered later, no duplicates. With sends held, two captures and Open showed "Sending…". Releasing the hold delivered both, and the editor opened on the last page ("Round 4 · 0:16"). Back in the replay the card read "2 pages captured · Saved to your cloud library", and the server held exactly 2 pages.
  • Second client (fresh --hive-store-dir, same account): one strategy with all three pages, an empty local library, and players moving between pages (the second half of the GIF).
  • Strategy deleted meanwhile. One capture delivered. I deleted the strategy on the server, then made two captures while sends were held, then released. The card showed "2 captures couldn't be saved · Its strategy was deleted. · Discard". After Discard, the next capture made a new strategy and the card said "Saved to your cloud library" (states).
  • Rechecked after the Astra round 3 fixes on a fresh build: three captures at 103 ms each with sends held, quit, relaunch delivered one strategy with 3 pages (13, 14, 13 items); then two held captures with Open waiting, released, editor opened on "Round 4 · 0:16", card "Saved to your cloud library", server holding exactly 2 pages.
  • Signed out (session storage swapped for an empty one by a scratch switch): two captures made a local strategy with 2 pages, and nothing went into the queue (card, editor).
  • Before (main, signed in): the capture went to the local library (card).

Tests:

  • test/providers/replay_capture_queue_test.dart runs against a fake server that dedupes by op id, answers identical pages and items without writing, keeps one row per item id, and checks page revisions. It covers: offline captures sent in order once online; lost answers (to the create, a page add, or a batch of items) resent without duplicates; items accepted beside a refused one kept, with Retry sending only the refused item; a map change landing between the page and its items stopping the items; a retry rechecking the strategy (moved map, deleted) before sending; a storage failure holding one capture while another strategy's goes, then Retry; a folder the account can't use, and the toast saying so; a server needing a newer Icarus, with sending stopped; records surviving a restart; a revision conflict resent under a new op id; a refusal held, then retried under new op ids; a deleted strategy failing its captures, with the next capture starting a new strategy even after discard; a moved-map strategy left untouched; discarding the first capture so the next one makes the strategy; discard reporting that a sent capture may still land; an unreadable record kept.
  • The banner and sign-out tests check that captures are counted, and replay_capture_test.dart checks a shared folder is never the destination. page_copy_id_test.dart checks that an alive player pairs with its dead copy.
  • The full suite passes locally.

Not covered, and gaps Dara accepted

  • A refused capture (Retry) wasn't seen in the running build. I had no way to make dev Convex refuse an item, so it's covered by unit tests only.
  • Accepted gaps, from the design: (a) if the user deletes the strategy while a capture's outcome is unknown, resending can recreate it; (b) a capture left unsent for over 30 days loses the server's op dedupe, though id reuse still prevents ordinary duplicates.
  • Account switch in the middle of a capture session isn't handled (Dara's ruling). A record is only sent while its own account is signed in.
  • A cold start that can't reach Convex leaves the cloud unavailable, so captures go to the local library, as New Strategy does in that state.
  • test/widgets/web_beta_library_test.dart: one test's name claimed a replay capture "still starts a local strategy" while signed in. It still tests createNewStrategy(local: true), so I renamed it to say what it checks.

ci: test replay

🤖 Generated with Claude Code

A replay's Capture always wrote to the local library, so a signed-in
user's captured strategy landed on this device while the rest of their
work was in the cloud. Signed in, Capture now makes the strategy in the
cloud (the open cloud folder, else the top level) and adds each captured
page there; signed out it stays local as before.

Each cloud capture is one page and its items sent together; one that
doesn't land is taken back and the user is told. Offline it refuses with
a message rather than queueing: a cloud strategy can't be made offline,
and the replay is still there to capture from. Items get fresh ids (the
server keys them globally) with one root per player across pages, so
page transitions still move players between captured moments. Open
lands on the last captured page for cloud strategies too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a920a15b-1d7f-4d89-8ac7-c04d202f3f54
📥 Commits

Reviewing files that changed from the base of the PR and between 798ec71 and 9257881.

📒 Files selected for processing: 20
  • lib/collab/convex_strategy_repository.dart
  • lib/collab/replay_capture_record.dart
  • lib/const/hive_boxes.dart
  • lib/main.dart
  • lib/page_transition/transition_planner.dart
  • lib/providers/collab/replay_capture_queue_provider.dart
  • lib/providers/strategy_provider.dart
  • lib/replay/replay_capture.dart
  • lib/services/cloud_sign_out_coordinator.dart
  • lib/startup/cloud_bootstrap.dart
  • lib/strategy_view.dart
  • lib/widgets/cloud_outbox_summary_banner.dart
  • lib/widgets/replay/replay_match_card.dart
  • test/cloud_sign_out_coordinator_test.dart
  • test/mouse_navigation_test.dart
  • test/page_copy_id_test.dart
  • test/providers/replay_capture_queue_test.dart
  • test/replay_capture_test.dart
  • test/widgets/cloud_outbox_summary_banner_test.dart
  • test/widgets/web_beta_library_test.dart

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

Replay captures now use local or cloud-backed stores. Cloud captures persist in a queue until delivery, and replay, outbox, and sign-out interfaces reflect queued work. Page-copy pairing also now matches agent variants by shared kind.

Changes: Replay capture delivery

Layer / File(s) Summary
Capture records and cloud queue
lib/collab/replay_capture_record.dart, lib/collab/convex_strategy_repository.dart, lib/const/hive_boxes.dart, lib/providers/collab/replay_capture_queue_provider.dart, lib/startup/cloud_bootstrap.dart, lib/main.dart, test/providers/replay_capture_queue_test.dart
Versioned records track capture operations, landing state, uncertainty, and failures. The persistent queue stores captures, sends them when connection and authentication conditions allow, and records delivery progress. Tests cover queue delivery, retries, persistence, and failure cases.
Local and cloud capture stores
lib/replay/replay_capture.dart, lib/providers/strategy_provider.dart, lib/strategy_view.dart, test/replay_capture_test.dart, test/mouse_navigation_test.dart, test/widgets/web_beta_library_test.dart
ReplayCapture uses a store interface for local or cloud strategies. Cloud captures enter the queue, and cloud strategy loading can target a requested page. Tests cover store selection, folder ownership, and local capture behavior.
Capture status and account lifecycle
lib/widgets/replay/replay_match_card.dart, lib/widgets/cloud_outbox_summary_banner.dart, lib/services/cloud_sign_out_coordinator.dart, test/cloud_sign_out_coordinator_test.dart, test/widgets/cloud_outbox_summary_banner_test.dart
Replay and outbox interfaces display capture status and expose retry or discard actions. Sign-out confirmation includes account captures and blocks sign-out when queue state is unreliable. Widget tests cover pending work, attention states, and retry actions.

Page-copy pairing

Layer / File(s) Summary
Agent copy matching
lib/page_transition/transition_planner.dart, test/page_copy_id_test.dart
Page-copy matching treats agent-node variants as the same kind. A test checks pairing a view-cone agent with its dead copy.

Priority: ➖ Normal

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

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant ReplayMatchCard
  participant ReplayCapture
  participant CloudReplayCaptureStore
  participant ReplayCaptureQueue
  participant CloudStrategyAPI
  ReplayMatchCard->>ReplayCapture: capture replay frame
  ReplayCapture->>CloudReplayCaptureStore: create or append capture
  CloudReplayCaptureStore->>ReplayCaptureQueue: persist capture record
  ReplayCaptureQueue->>CloudStrategyAPI: send strategy, page, and item operations
  ReplayCaptureQueue-->>ReplayMatchCard: publish capture queue state
Loading
Merge Risk: ⚪ Minimal · up to 92578

Signed-in replay captures now go through a persistent queue that delivers them to the cloud library, with retry, discard, and sign-out safeguards. The one concern raised about re-creating a strategy after a discard does not apply, because the server reuses the existing strategy. No blocking risks remain.

Pre-merge checks: Passed 5
Passed Passed checks: 5 passed
Check name Status Explanation
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
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 change: signed-in replay captures are saved to the cloud library through a pending capture queue.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

SunkenInTime and others added 3 commits October 13, 2026 15:49
Astra's review: a capture whose answer was lost (a timeout) may still
land, so it no longer says the capture failed; it says it couldn't
confirm, and the next capture adds to the same strategy instead of
starting another. A capture the server took without some items stays
and says so, rather than deleting the strategy or page with a revision
read later, which could take a teammate's edits with it.

A player alive on one captured page and dead on the next now glides
between them in a cloud strategy too: copies of an agent pair whether
or not they show a view cone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The capture's server-error check moves to isConvexFunctionError beside
the repository's other error helpers (convex_architecture_test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Astra's second review showed the direct-to-server capture could forget
its strategy or make a second one, and it kept Capture waiting on the
network. Dara chose a client-only queue with no server change.

Each capture is now one versioned JSON record in its own Hive box, with
every id fixed when the moment is captured (strategy, pages, items, op
ids, client id). Capture returns once the record is written. The queue
sends captures of one strategy in order: createWithInitialPage, which
the server answers with the strategy it already has; then PageAddOp
under a fresh revision (a revision conflict resends under a new op id);
then the items. A lost answer resends the same ops; a refusal waits for
Retry, which gives what didn't land new op ids. A strategy deleted or
moved to another map fails its captures, and the next capture starts a
new one. The capture card, the library's cloud work card and the
sign-out check count captures still on their way or refused, and Open
waits until the page is in the cloud.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SunkenInTime SunkenInTime changed the title Capture saves to the cloud library while signed in Capture saves to the cloud library while signed in, through a pending capture queue Oct 13, 2026
SunkenInTime and others added 2 commits October 13, 2026 17:44
Astra round 3:
- Item delivery that resumes after the page landed (a retry, a lost
  answer) now reads the strategy again first, so a strategy deleted or
  moved to another map meanwhile fails the capture instead of taking the
  positions; a page deleted meanwhile says so. A whole batch refused as
  not found marks the strategy lost.
- A failed write of a capture's progress no longer escapes delivery: the
  capture is kept in memory as stopped on this device, shown with Retry
  on the capture card and the library card, and blocks sign-out; other
  strategies' captures keep going.
- A capture goes into the open cloud folder only when it is this
  account's own; a shared folder, or one the server refuses, leaves it
  at the top level.
- A server that needs a newer Icarus is noted, so the app's update notice
  shows, and the card says to update Icarus to finish saving.
- The card shows a send whose answer was lost as unconfirmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Astra round 4:
- Items aren't checked against the strategy's map, so the strategy and
  page are read again before each batch of items, not only before the
  page: a map change landing between the page and its items now fails
  the capture instead of placing old-map positions.
- A server that needs a newer Icarus now stops sending until it accepts
  this build again, instead of retrying every 15 s.
- A capture moved to the top level because its folder was gone or not
  this account's says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SunkenInTime
SunkenInTime merged commit 08fbb20 into main Oct 13, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant