# 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
- **This device's session** — `kai.lastView`, scroll, reading mark. Local, never
  synced. This is the "carry on where I was on *this* machine" track.
- **The account track** — drafts, and reading marks if you want them to travel.
  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:

- this device's last session, if it has one
- the newest account-track item that is **from a different device** and **newer**
  than this device's own

Then:

- neither → nothing shown (the common case; no new noise)
- one → a single chip: *"Continue reading — page 65"* or *"Draft from your Phone, 4 min ago"*
- both → two chips, yours first

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

- **Draft format migration** is the one thing that can lose work. The reader must
  accept both shapes forever, not for a transition window — old clients keep
  writing bare strings for as long as one exists unrefreshed.
- **Clock skew** between devices makes "newer" wrong. Prefer server-stamped
  times where the sync round-trip already provides one.
- **Privacy**: device labels are account data visible to that account only. Do
  not store raw user-agent strings; derive a label and keep the string local.
- **Never sync `_kai_device_secret`**, and never sync `kai.lastView` again — that
  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.
