Repository navigation
Bring cloud sync and the web beta into main - #234
Merged
Merged
Conversation
feat: build the Icarus-owned typed Convex wrapper
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
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>
This comment has been minimized.
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>
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This merges
icarus-cloudintomain, somainbecomes 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 frommain: 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
StrategyMigrator. The folder grid and empty-state changes are ported to the cloud library. Defense resize was already on cloud.+104to matchSettings.versionNumber.bump_version.ps1refuses a mismatch, so Release Desktop would have failed at its first step.Rollback to 4.6.3 keeps the library whole
Sync integrity
applyBatchcarries the originating account, and the queue rechecks the account after claiming.Desktop
.icawhile Icarus is open opens it again.Wiring and cleanup
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.bun initstub that came in with icarus-cloud.Verification
windows-build, which installs the signed 4.6.3 release, upgrades to this build and rolls back..icaimports into the running window; folders line up with the strategy columns at two widths; the editor opens.Known and left
applyBatch(pages:add, folder ops, media) aren't bound to an account yet.TODO.mdat the root is July's cloud UX work list. Delete it if it's done.🤖 Generated with Claude Code
Do not merge until the three outstanding issues are resolved.
Findings
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..."