Measured on-device (2026-07-11, Seeker + desktop)
While verifying #107, the phone cross-device-resumed a session the desktop was STILL actively writing (d02a0fb7…). Both devices then appended independently:
- Phone local tier: pages p25-p29 written 07:12-07:32Z (decrypted: the phone-side conversation, Seeker device stamp)
- Desktop: kept appending its own p25-p29+ for the same session through the afternoon
- Cloud now holds the desktop's content for those page keys (last flush wins); the phone's branch survives only in the phone's local tier
Why it happens
SessionStore.lastPageIndex() picks the next page from what the opener can SEE at resume time; two live writers each think they own page N. Page keys are {sessionId}__p{N} with no writer fencing, so cloud uploads for the same key race and the loser's branch silently disappears from the cloud (still present in that device's local tier — nothing is unrecoverable today, but the cloud copy is a coin flip per page).
Impact
Direction options (not decided)
- Single-writer lease: a tiny
{sessionId}__writer blob (device id + heartbeat ts); a second opener gets read-only or forks
- Fork-on-conflict: second concurrent writer mints a new sessionId branching at the resume point (cheap, no coordination, matches the local-first model)
- Per-device page namespaces + merge-on-read (heavier, but no data ever lost)
Related: #107 (how it was found), #100 (cross-surface QA).
Measured on-device (2026-07-11, Seeker + desktop)
While verifying #107, the phone cross-device-resumed a session the desktop was STILL actively writing (
d02a0fb7…). Both devices then appended independently:Why it happens
SessionStore.lastPageIndex()picks the next page from what the opener can SEE at resume time; two live writers each think they own page N. Page keys are{sessionId}__p{N}with no writer fencing, so cloud uploads for the same key race and the loser's branch silently disappears from the cloud (still present in that device's local tier — nothing is unrecoverable today, but the cloud copy is a coin flip per page).Impact
Direction options (not decided)
{sessionId}__writerblob (device id + heartbeat ts); a second opener gets read-only or forksRelated: #107 (how it was found), #100 (cross-surface QA).