Skip to content

feat: add session-owned one-to-one dm open capability - #201

Closed
kalvinnchau wants to merge 1 commit into
mainfrom
peon/open-dm
Closed

kalvinnchau wants to merge 1 commit into
mainfrom
peon/open-dm

Conversation

@kalvinnchau

@kalvinnchau kalvinnchau commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Core communication layer for opening one-to-one DMs. It adds session.directMessages.open(peer): Promise<string>. There is no UI and no caller yet; the profile Message button lands in a follow-up.

Changes

  • Broker (dev/relay-broker.mjs, dev/session-commands.mjs)
    • Advertises and admits kind 41010, behind the existing channelCreation authority on the same sign/publish gate as 9000/9007.
    • The validator accepts only content: "", one lowercase-hex p tag, a UUID d tag, and an optional trailing client-id.
  • Session (src/features/relay/session.ts, FOUNDATION)
    • Builds the capability from the session's viewer, outbox, channel queries, write-readiness promise and lifetime signal.
    • Chains needsReceipt/onReceipt alongside the existing workflow receipts.
  • Capability (src/features/relay/direct-messages.ts)
    • Every open() sends 41010. Reopening a hidden DM is the relay's job, not a client-side cache decision.
    • A receipt's response:{channel_id} is only a candidate ID. open() resolves only once the relay-signed roster lists that exact ID as a non-archived dm whose members are exactly the viewer and the peer.
    • A missing, malformed or unknown receipt rejects as unconfirmed. There is no automatic retry, and a user retry sends a fresh command.
    • A leftover journal intent for the same peer is dismissed before sending. Dismissal failures are best-effort.
    • Session disposal rejects with "connection changed" at every await point, including just before send. Nothing is published after disposal.
    • Concurrent opens for the same peer share one promise.

Alignment with repo vision and Buzz

  • Buzz Desktop / NIP-DV: Desktop's profile Message action always calls openDm and navigates to the ID the relay returns. Under NIP-DV, hide is viewer-side presentation and 41010 is the reopen command.
  • docs/plugin-architecture.md: reuses the shared session capability. It adds no second connection, cache or outbox.
  • docs/relay-queries.md:
    • The outbox owns durable intent, signing and delivery.
    • Disposal stops late state changes from landing; it does not roll back writes the relay already accepted.
    • An unknown delivery stays unconfirmed until the user retries.

Validation

At ab0e871 (base 12a957c): full Vitest 2568/2568 (BUZZ_TEST_WORKERS=2), integration 127/127, tsc, design:typecheck, design:check, biome --error-on-warnings and git diff --check pass. The unit, live and security evidence below was gathered at 35fb97c (base 79a0282); the rebase onto #185 only added a test-file conflict in dev/relay-broker-api.test.mjs.

  • Unit tests (direct-messages.test.ts, 14): run against a real session and outbox, with a signed relay fixture and gated persistence/metadata.
    • 5 of these fail against the earlier reuse-based implementation.
    • The pre-send cancellation test fails without its guard.
  • Live: an isolated Compose relay (buzz-ws-e2e:sha-b082ae71f80d) through this commit's broker, 8/8 passing. Covered:
    • Canonical ID ≠ request d, and same-peer sharing.
    • Listed and hidden reopen, including 41012 → 41010 clearing 30622.
    • Restart.
    • Receipts that are rejected, lost or malformed, followed by retry.
    • Journal failure and disposal.
  • Security probes: signer/authority, tag-shape, receipt replay, scoped socket, cross-community and lifetime matrices. No findings.

Known gaps

  • Full Vitest is not deterministically green under load. Timeouts in read-state/unread-startup/traffic.integration, a jsdom scrollIntoView error, and a broker timing assertion have failed in some runs. Each reviewer had a later clean 2553/2553 run. Similar timeouts occur on base, but that does not prove these are unrelated.
  • Two-session first open: one live run returned a broker 503 with unknown delivery to one of the two sessions. The retry converged on the same canonical DM. The cause is undiagnosed.
  • Out of scope: useHiddenDms unhide, scope/generation-fenced navigation, and the browser profile → conversation test.

@kalvinnchau
kalvinnchau marked this pull request as ready for review September 24, 2026 02:56
@kalvinnchau
kalvinnchau requested review from a team, comp615 and wesbillman as code owners September 24, 2026 02:56
Expose kind 41010 through the relay broker behind channel-creation
authority and add session.directMessages.open(peer). Every open sends
41010, so the relay owns reopening hidden DMs. The canonical channel_id
comes from the ephemeral receipt, and open resolves only after the
signed roster lists that exact non-archived DM with exactly viewer and
peer. Journal cleanup is best-effort, and a session change rejects
promptly without publishing.

Co-authored-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

No blocking defects found for the capability-only scope at ab0e871e4e89b1b7bbfc4486f33d5bdbfde5900b. The command admission, durable delivery, receipt-as-candidate rule, signed DM/participant checks, explicit retry and session-disposal paths hold up in source review. Two nonblocking follow-ups are inline; neither requires expanding this PR into the profile UI.

Validation: source-only review with an independent lifecycle/retry lane. Existing exact-head CI passed, including Chromium and WebKit; Windows native validation was skipped. No PR code was checked out or executed by this review. Relay hide/reopen and the author-reported two-session 503 were not independently exercised. Local sidebar unhide and generation-fenced navigation remain outside this PR.

// A failed, unconfirmed or restored intent has no canonical ID. The relay
// resolves a participant set to one DM, so a fresh command cannot duplicate it.
for (const item of source.snapshot())
if (peerOf(item.event) === peer) await discard(source, item.event.id);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nonblocking recovery follow-up: generic Outbox Retry can restart an old 41010 while a new open(peer) reaches this loop. outbox.dismiss() returns without removing an in-flight attempt (outbox.ts:521–522), so the old and fresh commands can coexist. This does not bypass the authorized destination returned by open(), and canonical DM resolution is intended to be idempotent. Before wiring the profile UI, decide how 41010 recovery should appear in the generic outbox (currently an empty-content row with Retry) and cover the Retry → open interleaving. No broader retry abstraction is needed for this PR.

signal.addEventListener("abort", abort, { once: true });
if (signal.aborted) return abort();
inspect(false); // An earlier roster error does not decide this attempt.
channels.refreshList?.();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low-priority optimization: inspect(false) can already resolve from a matching ready DM projection, but this still forces discovery afterward. refreshList() calls discover(true), rereading the viewer roster and metadata for every returned channel (store.ts:791–869). Consider skipping the refresh when the cached projection has just satisfied this attempt, while keeping it for destinations not yet confirmed. A listed-DM test that records discovery reads would preserve that distinction. This is avoidable I/O, not a demonstrated blocking performance issue.

@kalvinnchau

Copy link
Copy Markdown
Contributor Author

🤖 Closing as superseded by #156, which landed session.directMessages.open(pubkeys, signal) on main (broker-signed 41010 with signed-membership confirmation). Profile Message work will build on that API.

@kalvinnchau
kalvinnchau deleted the peon/open-dm branch September 24, 2026 21:14
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