Skip to content

A reloaded pair finds itself again: stable endpoint identity, a peer directory, and resume wiring - #99

Merged
lann merged 3 commits into
mainfrom
feat/resumed-pair-sync
Aug 23, 2026
Merged

A reloaded pair finds itself again: stable endpoint identity, a peer directory, and resume wiring#99
lann merged 3 commits into
mainfrom
feat/resumed-pair-sync

Conversation

@lannbot

@lannbot lannbot commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Closes the last structural gap from the solo pairing work: sync existed only in the ceremony's page session, and a reload on either side ended it — permanently, because iroh-bind minted a fresh transport identity every boot, so the enrollment's recorded endpoint id pointed at a key that no longer existed anywhere.

Commit 1 — engine: the endpoint identity becomes the device's own

  • device-identity grows endpoint-key-pair: a second persisted Ed25519 pair, deliberately not the signing pair — no cross-protocol key reuse between keyhive's signatures and iroh's handshake, and transport identity stays separably rotatable. The worker answers it from the device namespace beside the signing key; iroh-bind adopts it via identity-from-keys (already in polymorph-iroh) and falls back to generate when the embedder holds nothing.
  • The us-doc devices map becomes the peer directory: entries gain endpoint (adder records the joiner's transport-observed id at enrollment; user-create records the founder's) and enrolled-by (empty = founder). Additive. us-device-endpoint-put upserts a device's own endpoint — chunk-wise no-op when unchanged, and never creates an absent entry (a racing fresh-map insert is an automerge conflict whose loser's fields vanish).
  • Stability proof: solo-persistence asserts the endpoint id survives a real reload (negative control: stubbing the worker's answer back to undefined fails exactly there).

Commit 2 — page: resume wiring by the directory

  • resumeWire on every account:resumed boot: acceptor first, then dial the device that enrolled this one (reader dials — the ceremony's direction, per edge), subscribe the todo list for every device this one enrolled. Patient by design: 5s ticks for the life of the page, one status line — the wait is for a person opening a browser.
  • Rebind-on-Closed: Closed on a dial names this page's own dead endpoint (home-relay death is sticky). With a stable identity a rebind mints the same endpoint id, so the loop rebinds, re-posts the acceptor, and continues — the backgrounded-Android failure now heals itself.
  • The pointerless resume no longer parks on "this account has no todo list yet": it waits (10 min), adopts, reconciles the profile, mounts.
  • Found en route: the driving hook drained us-events and discarded the batch, starving the 1s poll of profile-changed; both callers now share one drainAndAdopt.

Known limit (documented in-code, not worked around)

One-sided reload of the accepting device is not recoverable from the page: conn-status reports the handshake once and is never invalidated, so the surviving reader has no evidence its handle is stale — and the only page-side "fix" would be double-dialling. The honest fix is engine-side connection liveness; the both-sides case (the ordinary one) recovers.

Gates (re-run post-rebase onto #98)

  • cd engine && just compose — pass
  • just e2e-check, just check — pass
  • just e2e solo-resume-sync solo-pairing solo-erase solo-persistence device-pairing solo-account-storage6/6 in real Chromium
  • Non-vacuity: early-returning resumeWire fails solo-resume-sync at exactly the post-reload claim

Unexercised by gates (documented): the rebind-on-Closed path (needs a killed home relay) and the pointerless-resume branch (needs a mid-sync reload). Both are reasoned from cited sources in comments.

lann added 3 commits August 23, 2026 13:26
… key, and the devices map learns where its devices live

iroh-bind minted a fresh identity every call, so a device's transport
address died with its page session and a rebooted peer was unreachable
forever — the enrollment's recorded endpoint id pointed at a key that no
longer existed anywhere.

The device-identity seam grows endpoint-key-pair: a SECOND persisted
pair, deliberately not the signing pair (no cross-protocol reuse between
keyhive's signatures and iroh's handshake; transport identity stays
separably rotatable). The worker answers it from the namespace beside
the signing key; iroh-bind adopts it through identity-from-keys and
falls back to generate exactly as before when the embedder holds
nothing. The same device now binds the same endpoint id every boot —
solo-persistence carries the proof across a real reload.

The us-doc devices map becomes the peer directory: entries carry
endpoint (the adder records the joiner's transport-observed id at
enrollment; user-create records the founder's own) and enrolled-by
(empty = founding device), both additive. us-device-endpoint-put lets a
device upsert its own — a no-op chunk-wise when unchanged, and it never
creates an absent entry (two devices racing a fresh map under one key is
an automerge conflict whose loser's fields vanish).
…atience over attempts, and a rebind when the endpoint died

The sync wiring existed only in the ceremony session; any reload on
either side ended it, and the resumed branch parked a pointerless
account on a sentence about a permanent state ('no todo list yet') that
was actually temporary.

resumeWire runs on every account:resumed boot: post the acceptor first,
find own entry in the devices directory, dial the device that enrolled
this one (reader dials — the ceremony's own direction, per edge), and
subscribe the todo list for every device this one enrolled. The loop is
patient (5s ticks for the life of the page, one status line) because the
thing being waited for is a person opening a browser. A Closed error
names this page's own dead endpoint — home-relay death is sticky — and
the identity is stable now, so the loop rebinds (same endpoint id),
re-posts the acceptor, and keeps going: the backgrounded-Android case
heals itself. The pointerless resume waits ten minutes for the pointer,
then adopts, reconciles the profile, and mounts.

Found en route: the driving hook drained us-events and threw the batch
away, starving the 1s poll of the profile-changed events it existed to
apply; both callers now share one drainAndAdopt.

Written down, not worked around: one-sided reload of the ACCEPTING
device is not recoverable from this file — conn-status reports the
handshake once and is never invalidated, so the surviving reader has no
evidence its handle is stale. The honest fix is engine-side liveness.

solo-resume-sync (e2e): pair, converge, reload BOTH pages, and a todo
plus a rename must still cross — plus the endpoint id equal across the
reload, the premise the wiring rests on.
…the marks cross into the trust table

The display name synced and stopped there. The user's own glyph never
left the device — the settings write-through sent name and hue and
dropped the icon on the floor — and the account's marks (the user's
word and glyph for a component) were fetched, cached, and never
materialized: the visor's trust table stayed empty on every paired
device, so the app mounted unmarked no matter what the account knew.

Outbound, the write-through now carries the glyph as UTF-8 bytes
(opaque to the engine, us-profile.icon). Inbound, applyProfile decodes
and adopts it only when it decodes cleanly AND names a glyph in the
visor's own vocabulary — the visor never adopts a word it doesn't
speak, and an account with no icon must not undress a device that has
one.

Marks ride reconcileFromDriver's new onMarks (optional; existing
callers unchanged), at the same three moments the profile established:
join beat, resumed boots, drained events (the drain now also reconciles
on mark-added/-changed/-conflict-repaired). Only WHOLE marks are
seeded — petname and icon non-empty, needsReconfirm false — because the
repaired view hands a conflict's loser out reconfirm-flagged and
icon-cleared, and such a record is the ceremony's to re-confirm, not
the table's to adopt (PAIRING.md §5). The mounted surface refreshes in
the same breath, the shape onNamed already uses.

Mark deletions still don't cross — there is no mark-forgotten event to
drain; noted at the applyMarks site, needs an engine event.

solo-pairing now picks a glyph on A through the real settings sheet and
names the app through the real ceremony; B must wear both. The
resume-sync scenario renames the app after the double reload and reads
it on B — the drain trigger over the re-established wire.
@lannbot

lannbot commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator Author

Added a third commit: the rest of the user follows the name — closes the two remaining account→device sync gaps you hit on the phone.

  • User icon: was never sent (write-through carried name+hue only) and never applied. Now crosses as UTF-8 glyph bytes, adopted inbound only when it decodes cleanly and names a glyph in the visor's own vocabulary; absence never undresses a device that has an icon.
  • App petname/icon (marks): were fetched and cached but never materialized into the visor's trust table, so the app mounted unmarked on every paired device. Now seeded at the same three moments as the profile (join beat, resumed boots, drained mark events), whole marks only — needsReconfirm/icon-cleared conflict losers stay with the ceremony per PAIRING.md §5.
  • Known gap left open: mark deletions don't cross (no mark-forgotten event exists to drain) — needs an engine-side event; documented at the apply site.

Gates: e2e-check/check clean; solo-pairing solo-resume-sync solo-erase solo-persistence device-pairing 5/5. solo-pairing now drives the real settings icon picker and the real naming ceremony on A and asserts both land on B; non-vacuity probes fail each half exactly where expected (unwiring applyMarks → petname assertion; removing the outbound encode → expected "☾", got "⛨").

@lann
lann merged commit f40f57b into main Aug 23, 2026
3 checks passed
@lann
lann deleted the feat/resumed-pair-sync branch August 23, 2026 18:04
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.

2 participants