Session identity is now anchored on Claude Code's session UUID instead of a
fresh random keypair per launch. The ed25519 session keypair is generated
once per (mesh, session UUID) and persisted under
~/.claudemesh/sessions/<mesh>/<uuid>.json, so relaunching or --resume-ing the
same session reuses the same sessionPubkey.
Why: a DM is sealed (crypto_box) to the recipient's sessionPubkey. With
ephemeral per-launch keys, the pubkey rotated on every relaunch, so queued
messages became undecryptable AND the old presence lingered as a same-name
ghost that won queued-DM claim races. Reconnecting could not recover the
peer because it minted yet another key. On --resume the CLI also registered
a throwaway random id unrelated to the resumed session, so the broker never
recognized the returning peer.
CLI (launch.ts):
- resolve the stable UUID for all paths: fresh mints + forces via
--session-id; --resume V registers V; --continue resolves the most-recent
session UUID from ~/.claude/projects/<cwd>.
- use loadOrCreateSessionKeypair(mesh, uuid) instead of generateKeypair().
CLI (daemon/run.ts):
- onRegister closes any prior SessionBrokerClient holding the same pubkey
under a different token (the leaked-WS ghost).
Broker (handleSessionHello):
- reattach by sessionPubkey regardless of lease state (online or grace),
closing the stale socket — enforces one live presence per session pubkey,
killing the duplicate and draining queued DMs on return.
Trade-off: session secret keys now persist on disk (the member key already
does); SPEC.md updated to reflect the stable-identity model. Older CLIs
remain compatible (they keep using ephemeral keys).
New: keypair-store.ts + 7 unit tests. Full CLI suite: 114/114 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>