Skip to content

Single-inbox identity refactor (ADR 011)#33

Open
yewreeka wants to merge 4 commits intomainfrom
feat/single-inbox-identity-refactor
Open

Single-inbox identity refactor (ADR 011)#33
yewreeka wants to merge 4 commits intomainfrom
feat/single-inbox-identity-refactor

Conversation

@yewreeka
Copy link
Copy Markdown
Collaborator

@yewreeka yewreeka commented Apr 21, 2026

Summary

Mirrors the convos-ios single-inbox refactor (xmtplabs/convos-ios#713). Every CLI install now has one XMTP inbox shared across every conversation and DM, replacing the per-conversation identity model (ADR 002 → ADR 011).

For the primary CLI user — agents and bots — this means one agent identity across every conversation it participates in, matching the iOS app's behavior and eliminating the multi-identity coordination layer.

Wire protocols unchanged. Invites, join requests, ProfileUpdate/ProfileSnapshot, ExplodeSettings, TypingIndicator, and JoinRequest all interop with older CLI clients and with iOS on either side of its refactor. An ExplodeSettingsCodec is now registered so the CLI decodes the sender-hint message instead of logging "No codec found".

Version bumped to 0.8.0 (breaking).

Breaking changes

  • Data layout: ~/.convos/identity.json + db/<env>/main.db3 replaces identities/<id>.json + db/<env>/<id>.db3. Existing layouts are not auto-migrated; run convos reset to wipe legacy files.
  • Commands: conversations create|join stop minting per-conversation identities; identity list|info|remove operate on zero-or-one; identity create errors if an identity exists; conversations list output shape changed and filters isActive: false by default; agent serve drops --identity.
  • Explode: sends ExplodeSettings + removeMembers(all others). Does not destroy the install's identity (per ADR 004 C9 amendment). The leaveGroup() step from the iOS spec isn't available in @xmtp/node-sdk; receivers still drop on the MLS remove commit.
  • Device sync: disableDeviceSync: true is gone — XMTP history sync now works across installs sharing the same inbox.
  • Library API: createClientForIdentitygetClient / getIdentityAndClient (cached singleton per (home, env)). IdentityStore collapsed to load / loadOrUpsert / exists / update / delete / getDbPath. New exports for the ExplodeSettings codec.

What's on the branch

Core refactor (commit 1)

  • Single-slot IdentityStore backed by ~/.convos/identity.json. All 22 conversation commands migrated to getClient / getIdentityAndClient. Explode rewritten to remove-all-and-hang-up. README, CHANGELOG, skill, and docstrings rewritten for the new model.

Post-review hardening (commit 1, after four parallel code reviews)

  • File locking on identity.json — advisory lock around read-modify-write so two CLI processes against the same CONVOS_HOME can't lose updates. 10s timeout, 30s stale-lock break, Atomics.wait sleep. Writes are atomic (write-then-rename) so readers never see a truncated file.
  • Stream catchup coalescingonRestart used to drop a second restart if one was already in flight. Now uses a scheduler + pending-flag pattern so nothing is lost.
  • process-join-requests --watch catchup — previously had no onRestart handler; now matches the agent-serve pattern.
  • Stream shutdown — awaits stream.return() instead of fire-and-forget.
  • Stream errors — surfaced as structured error events instead of silent catch {}.
  • Deduped encodeExplodeSettings into src/utils/explodeSettings.ts.
  • loadOrCreateloadOrUpsert to make field-overwrite semantics explicit.
  • Docs/comments audit (ADR 002 stragglers, stale gravestones, ALL-CAPS shouting, pointless section dividers).

iOS interop follow-ups (commit 3)

Informed by a cross-client test pass between the iOS single-inbox build and CLI 0.8.0.

  • ExplodeSettingsCodec registered on the XMTP client. Filtered out of displayable messages. agent serve emits a new explode_notice ndjson event when one arrives.
  • Join-request dedupeprocess-join-requests tracks processed DM message IDs in a bounded LRU Set. Batch pass + live stream can no longer double-fire for the same invite (fixes a --timeout 60 mis-synchronization symptom the iOS agent saw).
  • Watch observabilityprocess-join-requests --watch now emits structured stream_started / stream_restarted / stream_ended events and per-DM verbose trace lines (--verbose). Aimed at making a silent stall diagnosable (one was reported but not repro'd).
  • New command convos conversation explode-status <id> — dumps appData expiresAtUnix + the most recent ExplodeSettings message. Unblocks "iOS sets expiresAt on invite → CLI joins → CLI reads it back" verification.
  • New command convos conversation download-profile-image <conv-id> <inbox-id> — fetches a member's encrypted profile image and decrypts via the group's imageEncryptionKey.
  • conversations list filters isActive: false by default--include-inactive opts them back in.
  • convos init + README — documents CONVOS_API_KEY as required for attachments over 1MB and profile images; init writes a commented line with a hint.

Known limitations

  • CLI-side explode can't call leaveGroup() (not exposed in @xmtp/node-sdk). iOS receivers drop on the ExplodeSettings message or MLS remove commit either way, but the CLI creator's local state stays as a non-empty group.
  • Existing 0.7.x installs are not auto-migrated. Legacy identities/*.json files are ignored by the new store; convos reset cleans them up.
  • No iOS-produced wire-format fixtures in the test suite. Interop is validated via manual test runs (see PR comments) rather than automated regression coverage.

Test plan

  • npm run typecheck — clean
  • npm test — 275/275 pass
  • iOS interop smoke test (invite round-trip, profile round-trip, explode notification, attachments) — results feeding the follow-up commit
  • iOS interop re-run on the follow-up commit (ExplodeSettings decode, dedup'd join events, explode-status / download-profile-image new commands)
  • Two CLI processes against the same CONVOS_HOME — verify lock contention surfaces a clear error rather than silent corruption
  • Stall reproducer for process-join-requests --watch after lock sequence — the new stream_* events should make the cause observable

Related

🤖 Generated with Claude Code

Note

Refactor all CLI commands to use a single install-wide XMTP inbox identity (ADR 011)

  • Replaces per-conversation identity resolution (createClientForIdentity + identity store lookups) with a singleton getClient/getIdentityAndClient across all conversation/* and conversations/* commands, agent serve, and identity management commands.
  • Rewrites src/utils/identities.ts with a new single-identity store API (load, loadOrUpsert, exists, update, delete, getDbPath(env)) backed by a single identity.json file and a shared DB path per install/env; concurrent CLI processes are serialized via an advisory file lock.
  • Adds process-level client caching in src/utils/client.ts so repeated getClient calls for the same (homeDir, env) reuse a cached XMTP client, and registers ExplodeSettingsCodec at client creation.
  • Introduces ExplodeSettings content type support in src/utils/explodeSettings.ts with codec, encode/detect/extract helpers, and filters these messages from displayable chat content.
  • Adds two new commands: conversation download-profile-image (decrypt and save a member's profile image) and conversation explode-status (report explode state from metadata and recent messages).
  • The conversations list command now fetches directly from the singleton inbox with new --include-dms and --include-inactive flags; output fields are conversation-centric rather than identity-centric.
  • Risk: breaking change — the IdentityStore and Identity APIs are incompatible with 0.7.x; per-conversation identity fields (conversationId, inviteTag) are removed and existing multi-identity stores require migration per the 0.8.0 changelog.

Macroscope summarized e35d5d6.

yewreeka and others added 2 commits April 20, 2026 21:30
Mirrors the convos-ios single-inbox refactor (xmtplabs/convos-ios#713):
one XMTP inbox per install, shared across every conversation and DM.
Replaces the per-conversation identity model (ADR 002).

Wire protocols are unchanged — invites, join requests, profile
messages, and ExplodeSettings all interop with older CLI clients
and with the iOS app before and after its own refactor.

Version bumped to 0.8.0 (breaking).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread src/commands/conversation/explode.ts Outdated
Comment thread src/commands/conversations/process-join-requests.ts
Comment thread src/commands/conversations/create.ts Outdated
@macroscopeapp
Copy link
Copy Markdown

macroscopeapp Bot commented Apr 21, 2026

Approvability

Verdict: Needs human review

This PR implements a fundamental architectural change (ADR 011) replacing the per-conversation identity model with a single-inbox identity model per CLI install. The refactor touches core abstractions including identity storage, client creation/caching, and most conversation commands. While well-documented and internally consistent, major refactors that restructure shared infrastructure warrant human review.

You can customize Macroscope's approvability policy. Learn more.

yewreeka and others added 2 commits April 21, 2026 08:53
…llow-ups

Addresses the follow-ups surfaced by the convos-ios interop test run.

Wire-level:
- Register an ExplodeSettingsCodec so the CLI can decode the sender-hint
  message instead of logging "No codec found for content type". Filter
  ExplodeSettings out of displayable messages and emit an explode_notice
  event in agent serve when one is received.
- Stop double-firing join_request_accepted: process-join-requests now
  tracks processed DM message IDs with a bounded LRU Set, so the batch
  pass and the live stream can't both emit for the same request.

Watch mode observability (toward diagnosing the post-lock stall):
- process-join-requests --watch now emits structured stream_started /
  stream_restarted / stream_ended events and verbose per-DM trace logs,
  so a silent stall is observable instead of invisible.

New commands:
- conversation explode-status: dumps appData expiresAtUnix plus the most
  recent ExplodeSettings message for interop verification.
- conversation download-profile-image: fetches a member's encrypted
  profile image and decrypts it using the group's imageEncryptionKey.

UX:
- conversations list hides inactive conversations by default; pass
  --include-inactive to include exploded / removed groups.
- convos init now writes a commented CONVOS_API_KEY line and prints a
  hint about it. README documents that the key is required for >1MB
  attachments and for profile images.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- explode.ts: confirmation message claimed "and leave the group" but
  the CLI can't call leaveGroup() (node-sdk limitation). Corrected the
  wording and surface the limitation explicitly.
- README.md: drop the stale `convos identity info <id>` example — the
  command takes no positional arg after the single-inbox refactor.
- conversations/create.ts + conversation/invite.ts: gate QR output on
  this.jsonOutput instead of flags.json so CONVOS_JSON_OUTPUT and
  --fields can't corrupt the JSON stream with a pre-flight QR code.
- process-join-requests.ts: batch mode now updates lastDmTimestampNs
  so an onRestart that fires before any live-stream message lands has
  a non-zero sinceNs and doesn't silently skip catchup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant