Skip to content

Bring cloud sync and the web beta into main - #234

Merged
SunkenInTime merged 371 commits into
mainfrom
t3code/unify-cloud-main
Oct 3, 2026
Merged

SunkenInTime merged 371 commits into
mainfrom
t3code/unify-cloud-main

Conversation

@SunkenInTime

@SunkenInTime SunkenInTime commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

This merges icarus-cloud into main, so main becomes the only branch. It brings accounts, Convex sync, sharing, presence, page trash and the web beta, plus every fix found while getting it ready. Signed-out desktop users keep local mode exactly as 4.6.3 has it.

Merge with a merge commit, not a squash, so icarus-cloud's history stays intact. Merging deploys. Deploy Web now runs from main: production Convex first, then the web beta.

Old web tabs reload after this deploy (decided)

Protocol 4 refuses web tabs opened before the deploy (cbbdadf, 75b58e1). Cloud Paranoias are now stored at payload version 2. A protocol-3 tab would read one at the old size and, on its next edit to that page, write it back so new clients correct it a second time. Protocol 4 stops old tabs reading or writing until they reload. Their unsent edits stay on the device and send after the reload. A reload during the few minutes between the server deploy and the web deploy still gets the old build.

This breaks AGENTS.md's rule that a server change keeps working with the live web build. Dara approved it on 2026-10-01: an update means a reload, as in other apps. From this build on, a refusal says so on screen (6104d28): a reload prompt on web, an update prompt on desktop, and nothing discarded.

What changed beyond the merge

Bringing main across

  • Main's 7 commits. Paranoia's v104 migration now runs in StrategyMigrator. The folder grid and empty-state changes are ported to the cloud library. Defense resize was already on cloud.
  • Cloud Paranoias are corrected too. Cloud strategies never run local migrations, so every Paranoia on the server would have drawn with its Omen end 3 m off. A version-1 Paranoia row is corrected as it's read; nothing is rewritten on the server.
  • pubspec is now +104 to match Settings.versionNumber. bump_version.ps1 refuses a mismatch, so Release Desktop would have failed at its first step.

Rollback to 4.6.3 keeps the library whole

  • The hand-written Hive adapters saved for a rollback to 3.2.3: a subset of each page, plus JSON mirrors 4.6.3 ignores. Rolled back to 4.6.3, a library would have opened without its ellipses, view-cone and circle agents, firearms, lineup graph, ability visibility and folder icons, and 4.6.3's next save would have dropped them for good. Pages, agents, abilities, folders and drawings now save byte for byte as 4.6.3 does. Libraries saved by earlier cloud builds still load whole.
  • Tests decode with 4.6.3's own adapters, copied from the release tag. The Windows upgrade/rollback gate now starts from the 4.6.3 installer instead of 3.2.3.

Sync integrity

  • A recovered edit landing under a held item can be overwritten by the drop #220: a recovered edit is no longer silently replaced when you drop a held item over it. Astra reviewed the fix three times; each round found a race, and the third round came back clean.
  • One account's unsent work can no longer land as another account after a switch. applyBatch carries the originating account, and the queue rechecks the account after claiming.
  • A cloud failure at startup no longer locks you out of the local library. Signed out, the app no longer opens a Convex connection.
  • Save says "Saved" only when the save actually landed.
  • Leaving a cloud strategy with work pending (Home, or the new Reload) checks again at the moment you choose "Leave anyway", and never leaves quietly while a new draft is unsaved.

Desktop

  • Double-clicking an .ica while Icarus is open opens it again.

Wiring and cleanup

  • Deploy Web, CI and the contract gate run from main. Web deploys now queue instead of cancelling each other, so a cancelled run can't leave production on a new server with the old web build.
  • The upgrade/rollback gate gives each launch 30 seconds, not 12. A cold runner's first launch of 4.6.3 once missed 12, and the same gate runs before every desktop release.
  • Removed a 13 MB music video and a bun init stub that came in with icarus-cloud.

Verification

  • Local: 1491 Flutter tests pass, Convex 229/229, the contract audit passes, codegen shows no drift, and analyze reports infos only.
  • CI on this PR: every lane, including windows-build, which installs the signed 4.6.3 release, upgrades to this build and rolls back.
  • Real library: a copy of Dara's library (2 strategies, data version 100) upgrades to 104 and reads back whole through 4.6.3's adapters.
  • Release build, isolated store: the library loads signed out; a second launch with an .ica imports into the running window; folders line up with the strategy columns at two widths; the editor opens.
  • Reviews: Astra (static) reviewed every fix; findings were fixed until clean.

Known and left

  • Changing a strategy's map re-derives the correction for its untouched old cloud Paranoias, a shift of under a meter that every client agrees on.
  • Replayed transport mutations other than applyBatch (pages:add, folder ops, media) aren't bound to an account yet.
  • Leaving a strategy doesn't wait for lineup edits live sync withholds (a lineup missing its origin or landing). They're flagged for attention already, and blocking on them would trap the user.
  • During a refusal, Settings still shows "Cloud connection needs attention" with Reconnect.
  • TODO.md at the root is July's cloud UX work list. Delete it if it's done.

🤖 Generated with Claude Code

RetriggerConfidence Score: 2/5

Do not merge until the three outstanding issues are resolved.

Findings

  1. P1 Start the production backfill ▶
  2. P1 Keep shipped clients connected ▶
  3. P1 Older upload replaces newer image ▶

Summary

The PR adds cloud accounts, sync, sharing, presence, page trash, and the web beta. The latest changes scope lineup-editing presence to the dialog’s opening page and renew presence from the current socket state. No new findings were identified, but three earlier issues remain unresolved.

Reviews (11) · Last reviewed commit: "Renew presence from current state; scope..."

SunkenInTime and others added 30 commits August 27, 2026 02:43
feat: build the Icarus-owned typed Convex wrapper
All 40 items landed in #88-#94.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SunkenInTime and others added 16 commits October 1, 2026 19:31
Its fixtures were stamped when the file loaded, while the dialog reads
the clock when it builds, so a slow full run drifted across an "hours
ago" boundary and failed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
…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>
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>
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>
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>
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>
Sync each cloud lineup group as one row
Comment thread lib/providers/collab/strategy_op_queue_provider.dart
Brings in #235 (the test audit). Conflicts were all in tests:

- agent_dragable_lineup_state_test: main's real tap replaces the cloud
  side's direct onTap call; main waits for the agent icons, which was the
  reason for the workaround.
- strategy_import_version_guard_test: the duplicate guard test stays cut.
- strategy_integrity_test: main's real-path import/export harness, on
  this branch's StrategyImportExportService and StrategyMigrator, and its
  image model without `link`.
- screenshot_export_failure_test stays deleted; #229 replaced it.

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

This comment has been minimized.

Presence now relays which lineup groups each person is editing, so a
teammate about to change the same lineups sees it first: "Sam is editing
these lineups right now." in the lineup panel and edit dialog, and over
the media view. A group is one cloud row, so edits to it at the same
time conflict; this is the soft hint we chose over an edit lock.

- Room: a new `editing` message ({page, groups}), kept per peer, carried
  in welcome and join, broadcast when it changes. Old clients and an old
  room ignore it, so either can deploy first.
- Client: what this user edits is the groups of the lineup spots held,
  the spot a placement is pinned to, and the lineups an open dialog
  shows. It goes out throttled (the room ignores faster sends), at most
  eight groups, and again after a rejoin. The lineup group memory now
  says when it learns a group, so both sides recompute.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SunkenInTime and others added 3 commits October 3, 2026 13:13
From review:

- The room always relays a clear, however soon it follows a change: two
  messages the network delivered together could otherwise leave the
  notice on everyone's screen. A clear only broadcasts after an accepted
  change, so it cannot flood the room.
- Over the media view the notice shares one row with Delete, Edit and
  Close, so it gives way to them in a narrow window.
- The notice uses DESIGN.md's 8px step and the 12px/600 label role.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Show who else is editing a lineup group
… their page

From Greptile on #243:

- A renewal awaits its pass check, then saved the socket's state from
  before the wait: a clear (or cursor move) that arrived meanwhile was
  undone, and newcomers saw a stale "editing". It now renews what the
  socket holds after the wait, and nothing for a socket that closed.
- A lineup dialog edits the groups of the page it opened on. Left open
  across a page change, it no longer counts as editing whatever shares
  its item ids on the new page.

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

Renew presence from current state; scope open dialogs to their page
@SunkenInTime
SunkenInTime merged commit a91f179 into main Oct 3, 2026
13 of 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