← all documents · raw markdown · 6 KB

Scope — device persistence and a resume you choose

Goal, in your words: account data lives on the server; each device keeps its

own sense of where *you* were on *it*; drafts cross between them; and when you

come back you pick — carry on where this device was, or pick up what you left

on the other one.

---

What already exists

Most of the plumbing is built. Worth knowing before scoping the rest.

| Piece | Where | State |
|---|---|---|
| Account sync | _syncKeys / _syncPrefixes, [internal endpoint] | Working. Server publishes the key list so client and server can't drift. |
| Draft sync | prefix kai.draft. | Working. Drafts already follow the account. |
| Draft capture | _autoDraftSave / _autoDraftRestore | Working. Excludes passwords and data-nodraft. |
| Reading position | _leoMarks(), {page, at} per document | Working, device-local. |
| Device-local view | kai.lastView, oracle.lastChat | Fixed 2026-08-18 (v9.10.720) — was account-synced, which is why the Codex followed you between devices. |
| A device secret | _kai_device_secret | Exists, but it is an auth secret. Not usable as an identity we display. |

So the model you described is roughly half-implemented already. The missing

half is not sync. It is provenance and choice.

---

What is actually missing

1. Nothing records which device wrote anything. Drafts and reading marks

carry no device id. So "continue the draft from your phone" cannot be built —

not because syncing is hard, but because nothing knows it came from a phone.

2. Drafts have no timestamp. saveDraft stores a bare string. Two devices

with two drafts cannot be ordered, so "which is newer" is unanswerable, and

last-write-wins silently eats the other.

3. Restore is silent and unconditional. _autoDraftRestore fills every

empty eligible input at 120ms and 700ms after load. It never asks. That is fine

when the draft is yours from this device and wrong when it came from elsewhere —

it is the same class of problem as the Codex jump: a decision taken for you.

4. There is no device list. Nothing to name, see or forget a device with.

---

Design

Identity

Add kai.device.id (random, non-secret, device-local, never synced as a

preference) and kai.device.label, defaulted from the user agent — "Phone",

"Windows PC" — and user-editable. Registered to the account so other devices can

render a name instead of a hash. Not the existing _kai_device_secret;

reusing an auth secret as a display id would leak it into UI and logs.

Provenance

Draft values become { v, at, dev } instead of a bare string, read

back-compatibly: a bare string is treated as {v, at:0, dev:null} so nothing

written by an older client is lost, and old clients keep working because the

server's allowlist is back-compat by design. Reading marks gain dev alongside

the at they already have.

The two tracks

synced. This is the "carry on where I was on *this* machine" track.

This is the "pick up what I left elsewhere" track.

Both are kept. The point is that you choose, rather than one silently winning.

The resume affordance

Replace the silent jump with an offer. On boot, compute candidates:

than this device's own

Then:

Dismiss is permanent for that item. A chip that reappears after you decline is a

nag, and a nag gets ignored, and an ignored control is the same as a broken one.

Drafts specifically

Same-device drafts keep filling the box silently — that behaviour is right and

people rely on it. A draft from *another* device stops overwriting and offers a

chip instead. That is the whole behavioural change.

---

Phases

| # | Work | Visible change | Risk |
|---|---|---|---|
| 1 | Device id + label; stamp drafts and marks with {v, at, dev}; back-compat readers | none | low — additive, reversible |
| 2 | Reading resume becomes a chip instead of setView('codex') | the Codex stops grabbing you | low |
| 3 | Cross-device draft chip; same-device restore unchanged | drafts stop silently overwriting | medium — touches the composer |
| 4 | Devices list in settings: name, last seen, forget | new settings pane | low |

Phase 1 is the only hard dependency. 2, 3 and 4 are independent after it and can

ship in any order or not at all.

---

Risks

accept both shapes forever, not for a transition window — old clients keep

writing bare strings for as long as one exists unrefreshed.

times where the sync round-trip already provides one.

not store raw user-agent strings; derive a label and keep the string local.

regression is what this whole thread started from.

---

Decisions I need from you

1. Should reading marks sync across devices? If yes you get "continue reading

from your phone" on the desktop; if no, each device remembers only its own

place. Your description implies yes, but it is the one place the two tracks

genuinely overlap.

2. Chip on every load, or only on conflict? Only-on-conflict is quieter and

is what I would build unless you say otherwise.

3. Same-device drafts — keep filling silently, or always ask? I would keep

silent; asking about your own draft on your own machine is friction with no

information in it.