Skip to content

Messenger signals (read receipts, typing), field diagnostics, honest channel probe, and stream-carried attachments #2

Description

@ForeverInLaw

Spec: messenger signals, field diagnostics, and the moss capability adoption

Problem Statement

Mosh conversations feel inert and the field is undiagnosable.

When a message shows two ticks, the sender cannot tell whether the
counterpart ever opened the conversation. There is no "typing" hint, so a
live back-and-forth gives no sense of the other person being there. For a
privacy-first messenger this is the most-requested liveness surface, and
today it simply does not exist.

When something breaks for a real user, we are blind. The Rust core emits
error lines to process stderr, which a release Windows build has no
console for — every dropped frame, failed handshake, and stalled resend
is silently lost. The diagnostics panel shows a mesh event ring, but it
carries no library version, so after a moss bump a stale library on a
user machine is indistinguishable from a real regression. There is no
file a user can attach to a bug report.

The channel probe used for release verification measures the wrong thing
and gives false negatives: it waits for "any substrate peer" before
sending, but substrate peers are strangers — the probe reports failure
for channels whose actual rendezvous simply needs more time, so channel
delivery has never been measurable end to end.

Finally, attachments ride the same pubsub room wire as everything else.
On a direct session that wire is slower than it needs to be for bulk
transfers, and moss's stream API (direct-session fast path with relay
fallback) goes unused.

Solution

Four capabilities, one branch, each independently shippable as an ordered
commit:

  1. Signals. A [[Read receipt]] — MLS-authenticated proof the user
    opened the conversation — rendered as the existing two delivery ticks
    changing color, never a third tick. Off by default, symmetric: a user
    who does not send receipts does not see others'. DM only. A
    [[Typing indicator]] — live "the counterpart is typing" — DM and
    groups, on by default, emit-on-input with a ~3s refresh and 5s expiry,
    MLS-encrypted. Channels never carry either.
  2. Field diagnostics. A plain file log under the app's private data
    directory (rotated, small), carrying the errors the core already
    emits plus key session transitions (handshake land, resend attempts,
    delivery settlement). The diagnostics panel gains the loaded moss
    library version and per-peer RTT.
  3. Probe honesty. The channel probe drops the substrate-peer gate
    and retries the send itself until the listener has the frame or the
    timeout is spent — the send becomes the rendezvous probe, and the
    verdict measures what users actually experience.
  4. Stream transport for attachments. The attachment blob channel
    keeps its chunk protocol (requests, retry, dedup) but rides moss
    streams: direct-session fast path, wrapped-relay fallback. User-
    visible as faster attachment transfers on direct sessions; no
    protocol change for counterpart devices.

User Stories

  1. As a DM participant, I want the two delivery ticks on my message to
    change color when the counterpart opens the conversation, so that I
    know a human actually saw it without asking "did you see it?".
  2. As a privacy-conscious DM participant, I want read receipts off by
    default, so that my reading behavior is never reported without my
    explicit consent.
  3. As a privacy-conscious DM participant, I want the read-receipt
    setting to be symmetric — if I do not send receipts I do not see
    others' — so that I cannot free-ride on a feature I refuse to give.
  4. As a DM participant who has enabled read receipts, I want my choice
    to apply to every DM I have, so that I do not have to decide it per
    conversation.
  5. As a DM participant, I want the receipt to be MLS-authenticated, so
    that no mesh bystander can forge "read" and lie to me about my
    counterpart.
  6. As a DM participant, I want an old counterpart client that does not
    know receipts to simply never send one, so that mixed-version
    conversations degrade silently instead of breaking.
  7. As a DM participant typing a message, I want the counterpart to see
    "typing" within a few seconds, so that the conversation feels live.
  8. As a DM participant, I want the typing hint to disappear within five
    seconds of the counterpart stopping, so that it never lies about
    continued activity.
  9. As a DM participant, I want the typing hint to stop the moment my
    counterpart sends or clears their draft, so that "typing" never
    contradicts what arrives (or does not).
  10. As a group member, I want the same typing hint in private groups,
    so that groups feel as live as DMs.
  11. As a group member, I want the typing hint to identify which member
    is typing, so that in a multi-party room the hint is informative
    rather than ambiguous.
  12. As a participant in a public channel, I never want typing or
    receipts in channels, so that strangers cannot probe my activity.
  13. As a user, I want typing to work without any setting, so that the
    feature needs no onboarding to be useful.
  14. As a user, I want typing frames MLS-encrypted, so that a mesh
    bystander cannot fake "someone is typing" in my conversation.
  15. As a user with a broken conversation, I want the app to write a
    log file in its private data directory, so that a support
    conversation can end with "attach this file".
  16. As a user, I want the log rotated so it never grows unbounded, so
    that diagnostics do not cost disk.
  17. As a user, I want the log to record dropped frames, failed
    handshakes, resend attempts and delivery settlements, so that
    "it didn't send" has a post-mortem.
  18. As a user, I want the diagnostics panel to show the loaded moss
    library version, so that I can tell whether my install actually has
    the new core.
  19. As a user reporting a bug, I want the log to include the moss
    library version, so that the maintainer knows what was running.
  20. As a maintainer verifying a release, I want the channel probe to
    retry its send until the listener has the frame or the timeout is
    spent, so that a channel verdict reflects delivery and not a
    substrate-peer race.
  21. As a maintainer, I want the probe timeline to show the retry
    attempts, so that rendezvous latency for channels is finally
    measurable.
  22. As a maintainer, I want probe verdicts comparable across the dm,
    group and channel kinds, so that a release gate reads uniformly.
  23. As a sender of an attachment on a direct session, I want chunks to
    ride the stream fast path, so that a large file arrives noticeably
    sooner.
  24. As a receiver of an attachment, I want the existing chunk protocol
    (requests, retries, dedup) preserved behind the new carrier, so that
    a mid-transfer network wobble still recovers.
  25. As a user on a relayed (no direct session) path, I want
    attachments to still transfer via the wrapped relay fallback, so
    that streams are an acceleration, not a requirement.
  26. As a user with an older counterpart client, I want my stream sends
    to fall back gracefully to the room wire, so that mixed versions
    keep transferring.
  27. As a developer, I want read-receipt state derived from the same
    snapshot polling as everything else, so that the UI has one
    data-flow pattern and no new push infrastructure.
  28. As a developer, I want typing state in the session/group snapshot,
    so that the poll cycle the UI already runs renders it.
  29. As a developer, I want app-level event codes 8–11 synthesized into
    the existing event ring by the runtime that owns the fact, so that
    the diagnostics panel shows "message_read"/"typing" like any mesh
    event.
  30. As a developer, I want the 52 error sites to move onto the file
    log through one call, so that no call site decides rotation or
    path policy.
  31. As a developer, I want log calls structured (level, kind, context
    id), so that the file is greppable and machine-diffable.
  32. As a developer, I want the library version loaded through the same
    dynamic-symbol table as every other moss call, so that a library
    missing the symbol degrades instead of crashing.
  33. As a developer, I want the probe to stay a pure observer of the
    production runtimes, so that verification code never forks runtime
    behavior.
  34. As a tester, I want a state-machine test pair that proves the
    receipt frame settles Sent→Delivered→Read exactly like the
    existing ack tests, so that the new state extension is proven at
    the same bar.
  35. As a tester, I want a state-machine test that proves a forged
    (non-MLS) read receipt is dropped, so that the color change cannot
    be faked.
  36. As a tester, I want widget tests that render the tick color change
    and the typing hint from a scripted snapshot, so that the UI
    contract is pinned without a live network.
  37. As a tester, I want the file log covered by tests that assert
    rotation and content at the existing storage-test seam, so that
    the sink itself is not trusted on faith.
  38. As a tester, I want a probe-e2e channel run on the stand to
    exercise retry-on-no-peers end to end, so that the fix is proven
    on the network shape that exposed it.
  39. As a tester, I want attachment transfer tests to run the chunk
    protocol over the new carrier with the real library dlopened, so
    that the wire change is proven against the real moss transport.
  40. As a tester, I want mixed-version tolerance (old client drops
    unknown envelope variants) covered by decode-drop tests, so that
    upgrades never break running conversations.

Implementation Decisions

  • Ordered commits on one branch, one PR at the end: probe fix first
    (it gates how everything else is measured), then diagnostics (it
    makes the rest field-diagnosable), then signals, then transport.
  • Signals are mosh's own protocol, not moss events. The mesh
    runtime never dispatches event codes 8–11; it pins the numbering for
    hosts. The messenger layer (mosh) carries its own frames and
    synthesizes its own events. The runtime that owns the fact (a DM
    runtime marking a message read) files the event into the existing
    event ring — the same seam the diagnostics panel already polls.
  • Envelope variants are named after the pinned glossary terms
    (ReadReceipt, TypingIndicator) so code vocabulary and the domain
    glossary cannot drift. "Delivered" stays taken by the runtime-level
    delivery ack; the read state is a distinct concept, never a third
    tick — the two existing ticks change color.
  • MLS encryption is mandatory for both signals. The existing
    delivery ack's rationale applies verbatim: a plaintext signal could
    be forged by any mesh member; only the MLS peer can produce a
    ciphertext the session accepts. The ack-decrypt pattern (drop on
    failed decrypt, keep state unchanged) is the template.
  • Mixed-version tolerance rides serde's unknown-variant drop, the
    same pattern every prior envelope addition used: an old client that
    fails to decode simply never sends receipts and never shows typing;
    nothing breaks.
  • Read-receipt semantics: auto-trigger when the conversation is
    open and a not-yet-read message is rendered; default off; symmetric
    exchange; DM only in this slice. Groups ("read by N") and channels
    are explicitly later/never.
  • Typing semantics: emit-on-input, ~3s refresh while input
    continues, 5s expiry, stop on send/clear; DM + groups; on by
    default; group hint identifies the member. State reaches Dart
    through the existing snapshot polling — no new push channel.
  • Log sink shape: one plain file under the app-private data
    directory (the ADR-0010 injected path), simple size rotation
    (a few MB per file, a few files). No tracing crate, no new
    dependencies. Call sites pass (level, kind, context id, message)
    to one sink call; the sink owns path and rotation policy. The
    existing stderr lines move onto it; the stderr behavior may remain
    for debug builds. A later swap to a structured subscriber stays
    possible without touching call sites.
  • Library version and peer RTT load through the same dynamic-symbol
    table
    every other moss call uses. A missing symbol degrades to
    "unknown" rather than failing the node.
  • Probe fix: remove the substrate-peer gate for the channel kind;
    retry the send on the moss "no peers" refusal until timeout. The
    probe stays a pure observer — no runtime behavior changes for
    production. The listener-presence protocol variant stays parked
    unless the retry alone proves unable to measure rendezvous.
  • Transport: the attachment blob channel keeps its chunk protocol
    (request/retry/dedup) and changes only its carrier: moss streams,
    which give a direct-session fast path and a wrapped-relay fallback
    inside the library. Fallback to the room wire covers peers whose
    library predates streams. Directed sends for message data are
    explicitly out of this slice (measure on attachments first).
  • No telemetry. Network statistics require opting the node into
    moss's telemetry gossip; that stays off by design, consistent with
    the Axiom opt-out.
  • Snapshot contracts extend, not fork: read and typing state ride
    the existing session and group snapshot structs (optional fields,
    skip-when-none), so codegen drift stays additive and old snapshots
    stay valid.

Testing Decisions

  • Good tests prove external behavior at the highest existing seams
    — the conversation bridge functions and the snapshot contracts; the
    event ring via the diagnostics surface; the probe via its timeline;
    the log via its written files. Implementation details (envelope
    serialization shapes, sink internals) are not asserted except where
    they are the contract (mixed-version decode-drop).
  • Modules tested: the DM runtime state machine (receipts, typing,
    log transitions), the group runtime (typing), the log sink
    (rotation, content), the probe (retry loop), the attachment runtime
    over the stream carrier, the Dart conversation UI (tick color,
    typing hint, composer emit), the diagnostics panel (version, RTT,
    events).
  • Prior art to follow:
    • In-crate state-machine tests that run two runtime instances
      against each other over the real dlopened library — the existing
      handshake/ack/duplicate-frame tests set the bar; the receipt
      tests extend the same harness (settle Sent→Delivered→Read; forged
      receipt dropped; old-client decode-drop).
    • Widget tests mounting screens via the shared pump helpers with a
      scriptable gateway — the existing delivered-tick and composer
      tests extend to the tick color and typing hint; the composer
      emit/stop logic is scripted through the same gateway.
    • Storage-seam tests for the log sink: write, rotate, re-open, and
      assert content — no mock filesystem.
    • probe-e2e on the stand for the channel retry: verdicts and the
      timeline must show retries, listener receipt, and a comparable
      shape across dm/group/channel.
    • The existing "no peers counts as failure" regression test in the
      channel runtime is the anchor the probe fix must not weaken —
      the runtime keeps refusing; only the probe retries.
  • Quality bar: every behavior change lands with its test in the
    same commit; the full local suites (core, flutter, analyze, fmt,
    clippy, format) plus CI stay green; the probe run on the stand is
    the release-gate proof for the transport and probe changes.

Out of Scope

  • App-level presence ("online" indicator) — parked until the masq
    rollout stabilizes sessions; connection-level presence already
    exists in the mesh events.
  • Group read receipts ("read by N") and any receipts/typing in
    channels.
  • Directed sends for message data (SendToPeer as the DM carrier)
    — measure the stream technique on attachments first.
  • Network statistics (GetNetworkStats) — telemetry is off by
    design.
  • Any moss submodule modification — the submodule stays a vendored
    pin; upstream work goes through the partner.
  • New settings UI beyond the single read-receipt toggle.
  • Localization beyond the existing ARB pattern (strings ride the
    existing gen-l10n flow).

Further Notes

  • The glossary terms [[Read receipt]], [[Delivery]] and
    [[Typing indicator]] are pinned in CONTEXT.md; the envelope variant
    names deliberately match them.
  • The event codes 8–11 are already named in the event-ring mapping
    (message_delivered / message_read / typing / presence); this slice
    makes the runtime actually file the first three of them.
  • The file-log decision was driven by a confirmed gap: in a release
    Windows build there is no console, so stderr is lost; debug prints
    only appear under a debug console. The app-private data directory is
    already injected once per process and is the natural anchor.
  • The probe fix unblocks a parked upstream report: once the partner's
    masq rollout reaches the relays, one fresh probe round decides
    whether to send the NAT-pair re-establishment report with both
    timelines; the UDP write-deadline escalation rides the same letter.
  • Branch name reserved: moss-capabilities. Ordered commits: probe,
    diagnostics, signals, transport. Docs (changelog, ADR updates) ride
    each commit rather than a final batch.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified and ready for an agent

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions