Skip to content

Sync each cloud lineup group as one row - #236

Merged
SunkenInTime merged 14 commits into
t3code/unify-cloud-mainfrom
lineups-one-row
Oct 3, 2026
Merged

SunkenInTime merged 14 commits into
t3code/unify-cloud-mainfrom
lineups-one-row

Conversation

@SunkenInTime

@SunkenInTime SunkenInTime commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #234. Merge this into #234's branch before #234 merges, so they deploy together.

Before this PR, a lineup synced as three kinds of cloud row: its standing spot, its landing spot, and the link holding its details, each pointing at the others. The server's conflict unit was one part, but what the user saved needed all three. That mismatch caused the long run of lineup edge cases: links arriving before landings, "end in use" and "end missing" refusals, delete ordering, rows that were held back or couldn't sync.

Now each lineup group is one cloud row. A group is the set of lineups on a page connected through a shared standing or landing spot. Its row holds those lineups and their spots: {kind: "lineups", data: {id, origins, landings, links}}.

  • A shared spot exists in exactly one place, so it can't disagree with itself.
  • Nothing points across rows.
  • The row's single revision guards every lineup on it, so two people changing the same group get an ordinary conflict.
  • A group only grows or merges; it never splits. A new group is named after its smallest lineup id that no group on the page has used.

An earlier version of this PR stored a copy of each shared spot in every lineup's row. It failed review three times because copies can drift apart, so it's replaced here.

Conflicts on lineup groups. The sync popover lists the lineups each side changed and offers three choices:

  • Keep mine
  • Use cloud
  • Keep both: the cloud's group stays, and your version is added beside it under fresh ids. The cloud version is loaded first, so being offline changes nothing.

There is no edit lock. A lock can't cover offline editing, which is where group conflicts actually happen, and it would put the network in front of every edit.

Also in this PR

  • settleAttention now only drops work the server refused because a teammate deleted the item.
  • A replayed op with no recorded revision now answers with no revision, so a queued successor is never promoted onto a guessed revision.
  • The server keeps one lineupAgents row per origin and counts images across every lineup in a group. Duplicating a strategy copies groups.

Unchanged: the local model, UI, undo, Hive and .ica.

Why now: production has zero lineup rows (read-only check, 2026-10-02), so the schema change needs no migration. Re-check right before merging.

Protocol 5: protocol-4 tabs can't read group rows, so the server asks them to reload.

Verified

  • npm run test:convex: 233 passed
  • npx tsc --noEmit -p convex: clean
  • flutter test: 1541 passed, 5 skipped
  • New tests cover:
    • two saves to one group at once
    • reconnecting after offline edits
    • stale replays
    • a teammate deleting a group while you edit it offline
    • Keep both, including when the cloud version can't load
    • a .ica round trip after Keep both
    • pages duplicated before cloud sync

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

No outstanding blocking findings remain; this review does not identify a reason to delay merging.

Summary

The PR syncs each cloud lineup group as one row and updates conflict handling and server-side checks. The latest change registers the new lineup helper in the generated Convex API. No new findings remain.

Reviews (10) · Last reviewed commit: "Regenerate Convex API types for lib/line..."

@coderabbitai

coderabbitai Bot commented Oct 1, 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: Advanced
  • Run ID: b394a31a-50d6-4298-a251-8f5ea7233ee3

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
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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 lib/collab/cloud_lineup_rows.dart Outdated
Comment thread lib/strategy/remote_page_merge.dart
SunkenInTime and others added 4 commits October 1, 2026 19:45
A lineup synced as an origin row, a landing row and a link row, and the
server checked across them: a link needed live ends (LINEUP_LINK_END_MISSING),
an end could not go while a link used it (LINEUP_END_IN_USE), and clients
opted in to both checks with applyBatch flags. The conflict unit was one
part while the user's lineup needs all three.

A lineup row now holds the whole lineup, kind "lineup", keyed by its id:
name, video, notes, images, and full copies of its origin and landing.
Lineups sharing a spot each carry their own copy; the client draws them as
one spot again. Every row stands on its own, so the cross-row checks, their
flags and error codes, and the endpoint index go. Page ownership
(LINEUP_PAGE_MISMATCH) and strategy-scoped keys stay.

Duplicating a strategy copies each row under fresh ids from one shared map,
so copied lineups that shared a spot still share it. The agent summary
counts each origin once per page, read from live lineups through a new
by_strategyId_and_deleted index. Image references read data.images of any
lineup row.

Protocol 5: a client on 4 cannot read the new rows and writes rows the
server no longer takes, so it is refused until it reloads. Production holds
no lineup rows (checked 2026-10-01) and dev's lineups table is empty, so
the narrowed schema needs no backfill.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The client side of one row per lineup. Live sync turns the page's graph
into one desired row per link, each carrying its origin and landing as
they are now, so moving a shared spot patches every lineup on it, each as
its own op against its own revision, and deleting a spot deletes the rows
of the lineups on it. Hydration rebuilds the graph from the rows: links
from rows, origins and landings deduplicated by id. When copies of one
spot disagree (only after concurrent edits) every client draws the copy
from the row with the highest revision, ties to the greatest publicId, and
live sync compares rows in that drawn form so the choice never authors a
write. The user's own pending rows outrank the server until they land.

Removed with the split rows: kind-prefixed keys and the drawn-row join,
held-back and unsyncable lineups, endpoint restore, undrawn remote rows,
endpoint-first and link-first outbox scheduling, the missing-end Keep mine
path and its messages, the sync status's unsyncable source, the graph id
rewrite on upload, and the Paranoia correction for landings (lineup rows
are written at the in-game size and are never corrected).

A remote merge no longer holds the whole graph while the user holds any
part of it: only lineups touching what is held, closed over the spots they
share, keep their screen copy; the rest of the page's lineups update.

An outbox record from an older build can still hold an origin, landing or
link op. It is never sent: it waits in attention with its own reason until
the user discards it, and Keep mine leaves it there.

The local model, UI, Hive, .ica and undo are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rites

Copies of a shared spot can drift apart (a move that reached only some of
its lineups), and the row revision is no measure of which copy is newest.
Each end of a lineup row now carries its spot's version, an integer from
1; clients draw the copy with the highest version, then the greatest
lineup id. Duplicate copies versions as they are, so a copy draws every
spot where the original does.

A patch or reorder of a deleted element or lineup was acked onto the
tombstone and the edit vanished on the next load. It is now rejected with
the new reason "deleted"; an add expecting the tombstone's revision
brings the row back. A write that changes nothing on a live row stays a
noop whatever revision it expects, so two clients healing the same row
never conflict.

Clients on protocol 4 still send checkLineupLinkEnds/checkLineupEndDeletes
and graph rows. The args are accepted and ignored, and the old kinds pass
argument validation to be refused per op in the handler, so such a client
gets CLIENT_UPGRADE_REQUIRED rather than a validation error. Storage takes
only lineups.

The agent summary is refreshed only after a batch that touched a page, an
agent element or a lineup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Live sync compared lineup rows in their drawn form, so a fan-out patch
still queued for one row looked satisfied once the others landed, and its
record was dropped; deleting the lineup holding the drawn copy then showed
the stale one. Desired rows now carry each spot's drawn value and version
(unchanged keeps the drawn version, a change goes one past it, a new spot
starts at 1), versions taken from the rows the canvas was hydrated from,
and are compared with the rows as stored. Any row holding another copy is
patched until it really holds the drawn one, and a page is healed as soon
as it is drawn. Versions never reach the canvas, Hive or .ica.

Outbox records holding protocol 4 lineup ops are put in attention when the
outbox loads, whatever state an older build left them in, so reconciling
the canvas never removes them.

Keep mine after a teammate's delete sends an add over the tombstone with
the user's payload; a change it cannot bring back says so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SunkenInTime and others added 3 commits October 2, 2026 11:44
…e agents from a side table

A shared spot no longer carries a version. Each lineup row owns its copy
of its spots and only ever changes when the user changes that lineup or
its spot, so the server stores what it is sent and joins nothing.

An accepted op whose answer was lost is now replayed as a noop at the
revision it actually landed at, not the row's latest. Answering with the
latest let a waiting successor rebase onto a teammate's later edit and
overwrite it without a conflict.

The strategy agent summary reads a small lineupAgents table, kept in step
on every accepted lineup op, duplicate and purge, instead of every lineup
payload, so its reads stay small however large lineups get. Production
and dev held no lineup rows when this shape shipped (2026-10-01), so
nothing needs backfilling.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Shared spots were resolved by version and stale copies healed on page
open, which meant writing rows the user never touched: a heal could
replace recovered outbox work, and a rejected heal next to an accepted
delete moved a spot backwards.

Now copies of a spot that agree draw as one shared spot, as before.
Copies that disagree, which only happens after a partial sync or a
conflict, draw as separate spots: the value the row with the smallest
lineup id carries keeps the real id, every other value gets a local
alias. Every client draws the same thing and nothing is written; moving
one of those spots patches only the rows carrying it, under the real id.

Also: a change refused because a teammate deleted the item is settled,
not kept, once the user deletes the item too, so Keep mine can't bring
it back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Lineups that share a standing or landing spot now live in one cloud row
with those spots (kind "lineups": origins, landings, links). A shared spot
exists once, so copies can no longer disagree, and the row's one revision
guards every lineup on it: two people changing the same group meet as an
ordinary conflict. Nothing points across rows. A group only grows or
merges, never splits, and a new group is named after its smallest lineup
id that no group of the page has used.

This replaces the copy-per-lineup model of the earlier commits, which
failed review three times because copies of a spot can drift apart.

- Server: group validation (every lineup aims inside its row), one
  lineupAgents row per origin, images across all links, duplicate copies
  groups. A replayed op with no recorded revision answers with none, so
  no successor is promoted onto a guessed revision.
- Client: per-page group memory in live sync, holds take whole groups,
  migration uploads groups.
- Conflicts on lineup groups list what each side changed, and offer Keep
  both: the cloud's group stays and the user's version is added beside it
  under fresh ids. It loads the cloud version first, so failing offline
  changes nothing.
- settleAttention only drops work refused because a teammate deleted it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@SunkenInTime SunkenInTime changed the title Store each cloud lineup as one row Sync each cloud lineup group as one row Oct 2, 2026
Comment thread lib/providers/strategy_page_session_provider.dart
From review of the group rows:

- Groups no longer merge. A lineup stays in the group it was written to,
  new lineups join the group of a spot they share, and a row goes only
  when its last lineup does. A merge deleted the other group's row, and a
  teammate's edit to it could be lost even after Use cloud. A stored row
  whose lineups are still drawn is written back as stored, never deleted.
- Keep both discards nothing unless the waiting work, the page and the
  copy's save all still hold: an edit or a page switch while the cloud
  version loads stops it before any copy is made, and a copy that failed
  to save stops it before anything is discarded. It resolves only the
  conflicts it copied, and pressing it again never copies twice.
- The conflict popover tells the sides' changes apart only while the
  version both started from is the one the refused change was made
  against; once the page is drawn again it lists how the user's version
  differs from the cloud's instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread lib/collab/cloud_lineup_rows.dart
Comment thread lib/providers/collab/active_page_live_sync_provider.dart Outdated
SunkenInTime and others added 2 commits October 2, 2026 18:56
From the second review of the group rows:

- No two live lineup group rows of a page may hold the same lineup or
  spot: the server refuses such a write (INVALID_LINEUP_PAYLOAD_DATA), so
  rows can never disagree about one spot. The check reads a small
  lineupItems side table, one row per lineup and spot, kept in step with
  every lineup write like lineupAgents. The client-side repair of
  overlapping rows is gone with the state it repaired.
- Discarding refused work is conditional: discardRejected takes the
  refused and successor op ids the user saw, and leaves a key whose work
  has changed since. Use cloud for Keep both saves and re-checks the
  waiting work and the page right before redrawing it.
- Keep both is not offered, and changes nothing, while the outbox cannot
  save, and stops before discarding if a copy failed to save.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the third review of the group rows:

- Use cloud resolves only the work waiting as the user saw it once the
  page's edits are saved, re-checks it right before redrawing, and
  discards conditionally, so an edit made while the cloud version loads
  is neither discarded nor drawn over.
- Before redrawing, the canvas must be as it was before the last save:
  an edit made since stops the redraw (nothing awaits between the check
  and it).
- Keep both discards nothing unless each copy's group is queued or on the
  server, so a save skipped without failing (edit access lost) cannot
  leave the copy only on the canvas.
- A lineup group refused for overlapping another says so, instead of
  reading as a teammate conflict Keep mine could win.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread lib/providers/strategy_page_session_provider.dart
Comment thread lib/providers/strategy_page_session_provider.dart Outdated
SunkenInTime and others added 2 commits October 2, 2026 19:35
From the fourth review of the group rows:

- Keep both remembers each copy it made, and every press checks all of
  them are queued before anything is discarded, so a retry cannot drop
  the original while an earlier copy is still unsaved.
- Use cloud stops when its last save fails: the queue may not have
  recorded a newer edit, so nothing is discarded or drawn over it.
- The canvas check runs where the redraw happens (after the page list it
  awaits), and the redraw after a partial discard has one too; a skipped
  redraw discards nothing.
- An overlap refusal says Keep mine is refused while the spot is still
  shared, rather than hiding it (the user may have fixed the overlap, or
  other waiting work may need it). Two different specific refusals no
  longer read as one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the fifth review of the group rows:

- The redraw after a partial discard compares the canvas with what the
  first redraw left, so an edit made while discarding is not drawn over.
- Use cloud stops only when its own last save fails (the queue now counts
  persistence failures), so a failure left from an earlier discard no
  longer blocks it forever; and it reports no success when its save
  turned the conflicts back into queued work.
- Keep both makes a copy again when the one an earlier press made is no
  longer on the canvas.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread lib/providers/strategy_page_session_provider.dart Outdated
SunkenInTime and others added 2 commits October 2, 2026 19:59
From the sixth review of the group rows (no blockers):

- Keep both remembers all of a copy's lineups, so deleting one of them
  does not make the next press copy the whole group again.
- Use cloud whose save settles a conflict (gone on both sides) reports
  success; only conflicts turned back into queued work count against it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@SunkenInTime
SunkenInTime merged commit 96aa682 into t3code/unify-cloud-main Oct 3, 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