Skip to content

Proposal: implement the send_dm workflow action (WF-07 TODO) #2628

Description

@SeanGearin

What

crates/buzz-workflow/src/executor.rs:582 carries a maintainer TODO:

SendDm { to, text: _ } => {
    warn!(run_id = %run_id, step = step_id, "SendDm not yet implemented (to={to})");
    // TODO (WF-07): emit DM event.
    Err(WorkflowError::NotImplemented("SendDm".into()))
}

The send_dm action is already part of the workflow schema (ActionDef::SendDm
in schema.rs, documented as "Recipient — pubkey hex or {{trigger.author}}"),
already flows through template resolution (resolve_step_templates), and any
workflow that uses it today fails at dispatch with NotImplemented.

I would like to implement it, scoped to send_dm only (the neighbouring
SetChannelTopic TODO at :588 is left for a separate change).

Proposed design — reuse the two patterns the codebase already establishes

  1. Emission path: mirror the sibling SendMessage arm exactly.
    The executor loads the run + workflow (community-scoped) to get the owner
    pubkey, then calls a new ActionSink::send_dm method. The relay-side
    RelayActionSink builds a kind:9 event, signs it with the relay keypair,
    attributes the workflow owner via a p tag, persists via
    insert_event_with_thread_metadata, and dispatches post-persist side
    effects via dispatch_persistent_event — byte-for-byte the same flow as
    send_message in crates/buzz-relay/src/workflow_sink.rs.

  2. DM encoding: Buzz DMs are not NIP-17 wrapped events — they are private
    channels (channel_type='dm') opened idempotently by participant-set hash
    (buzz_db::dm::open_dm, the same path the kind:41010 handle_dm_open
    command handler uses). send_dm opens (or reuses) the owner↔recipient DM
    channel and posts the kind:9 into it. When the open creates a new channel,
    the post-create side effects of handle_dm_open are mirrored (membership
    cache invalidation, dm_created system message, group discovery events,
    membership notifications); on re-open, the owner's NIP-DV visibility
    snapshot is refreshed.

The recipient is additionally p-tagged on the message event, because agent
wake (event_mentions_agent in buzz-acp) and push delivery are p-tag gated —
a DM that does not wake its recipient would be dead on arrival.

Decisions flagged for maintainers

  • Whose key signs a workflow DM? This implementation follows the pattern
    send_message already establishes: the relay keypair signs, the workflow
    owner is attributed via p tag, and the DM channel participant set is
    {workflow owner, recipient}. If the intent for WF-07 was instead
    owner-key-signed events (or a dedicated workflow identity), the sink method
    is the single place to change.
  • Self-DM (to == workflow owner) is rejected as invalid input rather than
    creating a 1-participant channel (create_dm requires ≥2 participants).
  • Privacy: unlike SendMessage, the DM body is not written to relay logs.

Tests

  • Pure unit tests for recipient normalization (hex, npub, uppercase,
    whitespace, malformed, unresolved-template literals) following the existing
    resolve_send_message_channel test pattern in executor.rs.
  • A Postgres-gated (#[ignore]) integration test in workflow_sink.rs
    mirroring workflow_send_message_p_tags_mentioned_member: asserts channel
    type/visibility, both p tags, idempotent channel reuse across sends, and
    self-DM rejection.

I've put the implementation up as draft PR #2614 for reference. Per
CONTRIBUTING's guidance to open an issue first for significant changes, I'm
keeping it in draft and treating this issue as the place to settle the design —
especially the signing-key question above — before it goes up for review.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions