feat: add session-owned one-to-one dm open capability - #201
kalvinnchau wants to merge 1 commit into
Conversation
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>
35fb97c to
ab0e871
Compare
wesbillman
left a comment
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?.(); |
There was a problem hiding this comment.
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.
|
🤖 Closing as superseded by #156, which landed |
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
dev/relay-broker.mjs,dev/session-commands.mjs)channelCreationauthority on the same sign/publish gate as 9000/9007.content: "", one lowercase-hexptag, a UUIDdtag, and an optional trailingclient-id.src/features/relay/session.ts, FOUNDATION)needsReceipt/onReceiptalongside the existing workflow receipts.src/features/relay/direct-messages.ts)open()sends 41010. Reopening a hidden DM is the relay's job, not a client-side cache decision.response:{channel_id}is only a candidate ID.open()resolves only once the relay-signed roster lists that exact ID as a non-archiveddmwhose members are exactly the viewer and the peer.send. Nothing is published after disposal.Alignment with repo vision and Buzz
openDmand 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:Validation
At
ab0e871(base12a957c): full Vitest 2568/2568 (BUZZ_TEST_WORKERS=2), integration 127/127,tsc,design:typecheck,design:check,biome --error-on-warningsandgit diff --checkpass. The unit, live and security evidence below was gathered at35fb97c(base79a0282); the rebase onto #185 only added a test-file conflict indev/relay-broker-api.test.mjs.direct-messages.test.ts, 14): run against a real session and outbox, with a signed relay fixture and gated persistence/metadata.buzz-ws-e2e:sha-b082ae71f80d) through this commit's broker, 8/8 passing. Covered:d, and same-peer sharing.Known gaps
read-state/unread-startup/traffic.integration, a jsdomscrollIntoViewerror, 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.useHiddenDmsunhide, scope/generation-fenced navigation, and the browser profile → conversation test.