Summary
vector_core::community::v2::chat::build_comment_rumor builds a correct NIP-22 / CORD-03 §3
kind-1111 threaded reply, including the parent_root inheritance that keeps the root stable at
any depth. It has no production call site — only tests — and no public function on the send
surface reaches it. So a third-party client or bot built on the published vector_sdk (0.5.0) /
vector-core (0.4.0) can react to, edit and delete a kind-1111 reply, but cannot send one.
Everything below is from master @ fcd47a27. This is a missing-API report, not a bug report:
the hard part is already written and correct, it just is not reachable from outside the crate.
Who this is coming from
We run a read-only assistant bot in a Concord/Armada community (vector_sdk 0.5.0). It answers
questions in a channel. We went looking for how to make its answers thread properly and ended up
reading the client source, which is how we found the gap. Notes on rendering are included below
because they were needed to reach the conclusion, and because they might be useful to whoever
picks up the thread view.
Reproduction
A v1 community (COMMUNITY_MESSAGE, kind 3300). Owner opens a topic, bot answers, owner asks a
follow-up on the bot's answer, bot answers again. Four events, read from the local
vector.db events table (ids truncated to 8, tags verbatim):
| inner id |
author |
tags |
4bb21474 |
owner |
[["ms","53"]] |
128f93d6 |
bot |
[["ms","600"],["e","4bb21474…","","reply"]] |
f9c28dd5 |
owner |
[["ms","979"],["e","128f93d6…","","reply"]] |
2b2f59f4 |
bot |
[["ms","73"],["e","f9c28dd5…","","reply"]] |
Every event carries exactly one e tag marked reply, pointing at its immediate parent, and no
root marker. That is what build_inner_full produces, and — importantly — it is also exactly
what the Vector client itself sends in the same situation (see below), so nothing here is
malformed or foreign-client-specific.
The result is a four-message flat chain that no client can group, because the root is not
recoverable from any single event: you can only rebuild the topic by walking the chain, and the
walk breaks the moment one link is outside your window.
What the source says (the part that made us stop guessing)
1. What Vector sends for a reply, at any depth.
strCurrentReplyReference is the id of the row the user clicked reply on (src/main.js:6444,
used at src/main.js:12091). It goes to the send_community_message command
(src-tauri/src/commands/community.rs:931-939), which passes it straight through as the single
reference argument:
- v1:
envelope::build_inner_full(..., reply.as_deref(), ...)
(src-tauri/src/commands/community.rs:1116-1126) → one
["e", <parent>, "", "reply"] tag, no root marker
(crates/vector-core/src/community/envelope.rs:217-221).
- v2:
chat::build_message_rumor(..., reply_ref, ...)
(src-tauri/src/commands/community.rs:1011-1013) → a kind 9 with a NIP-C7 q quote tag
naming the immediate parent.
Either way it is the immediate parent only, at any depth. Vector's own reply is therefore the
same flat shape our bot produces — no divergence to fix on our side.
2. Vector never sends kind-1111.
build_comment_rumor (crates/vector-core/src/community/v2/chat.rs:153) is called from
crates/vector-core/src/community/v2/chat.rs:715 and :782 (unit tests),
crates/vector-core/src/community/v2/service.rs:7666 (an integration test), and
crates/vector-core/src/community/v2/inbound.rs:1045 — a fixture whose own comment says it is
building "the shape Armada sends" (:1040). No production path. send_chat_message /
send_chat_message_at (v2/service.rs:254, :271) always call build_message_rumor.
3. The read side handles 1111 fully, and says the thread view is still to come.
parse_chat_rumor accepts kind::COMMENT and lifts the lowercase e into the same reply_to
slot a kind-9 q uses (v2/chat.rs:479-497), with the comment:
A threaded reply (NIP-22, CORD-03 §3). Vector's timeline renders it INLINE: the immediate
parent (the lowercase e) becomes the reply context, exactly like a kind-9 quote — never
dropped. The uppercase root tags stay on the rumor for a future thread view.
and, on reactions to a received 1111 (crates/vector-core/src/lib.rs:1902-1905):
Stored rows don't keep wire-kind fidelity yet, so a reaction to a received kind-1111 thread
reply claims 9 — Armada's fold ignores reaction k, and exact threading lands with the
thread-aware GUI.
So interop is deliberate and already in place: Vector reads Armada's threads today and holds
the root tags for later. Only the send direction is missing.
4. Frontend, for completeness. The timeline is flat and chronological — renderMessage
appends one row per message (src/js/render/chat/message-row.js; append sites
src/main.js:6844, :6870, :6996), grouping is purely temporal
(shouldCollapseStreak, src/js/render/chat/message-streak.js:40-63), and a reply's only
affordance is a one-line quote of its immediate parent resolved by exact id
(_dmsgBuildReplyContext, message-row.js:452-460, inserted at :181-190). Nothing walks a
chain or reads an uppercase E. Consistent with the comments in (3): there is no thread view
yet, so a sender cannot make the current UI group anything — which is exactly why we are asking
for the send API rather than proposing a rendering change.
The gap
v2/service.rs exposes send_message (:242), send_chat_message (:254),
send_chat_message_at (:271), send_reaction (:291), send_edit (:311),
send_delete (:327) and moderation_delete (:349). Three of those already accept
kind::COMMENT as a target — send_reaction's doc explicitly says "kind::COMMENT for a
threaded reply" (:286-289) — so the crate can address a threaded reply it did not send. The two
helpers needed to send one, chat_send_context (:415) and publish_chat (:439), are private.
vector_sdk 0.5.0 inherits that: Channel::reply(replied_to, text) (src/lib.rs:833) and
IncomingMessage::reply(text) (src/lib.rs:935) both funnel into
VectorCore::send_community_message(channel_id, content, replied_to)
(crates/vector-core/src/lib.rs:1631), which has one reply slot and no notion of a root.
Net effect for a bot author: a kind-1111 threaded reply is unreachable, in every combination of
public calls, on both the SDK and vector-core.
Concrete request
Anything that reaches build_comment_rumor. In rough order of preference:
-
v2::service::send_comment(...) — mirror send_chat_message_at, forwarding
build_comment_rumor's arguments:
pub async fn send_comment<T: Transport + ?Sized>(
transport: &T,
community: &CommunityV2,
channel_id: &ChannelId,
content: &str,
parent_id_hex: &str,
parent_kind: u16,
parent_author_hex: &str,
parent_root: Option<(&str, u16, &str)>,
emoji: &[(&str, &str)],
) -> Result<String, String>;
plus a _at variant, for parity with the precomputed-rumor-id contract described at
v2/service.rs:266-269.
-
An SDK wrapper — e.g. Channel::reply_threaded(&self, parent: &ReplyTarget, text: &str),
where ReplyTarget carries (id, kind, author, root). Ideally IncomingMessage would expose
the root of the message it arrived on, since the parse side already has the uppercase tags on
the rumor (v2/chat.rs:483) — that is the one piece a bot cannot reconstruct locally.
-
Minimum viable: make chat_send_context and publish_chat pub (or
pub(crate) → pub on a #[doc(hidden)] escape hatch). Less pleasant, but it unblocks
external clients without new API surface to commit to.
A v1 equivalent is not requested — v1 has no comment kind, and if v1 threading ever matters, a
root slot on build_inner_full alongside the existing reference would be the natural shape.
Why a bot author cannot work around this
- Building the rumor by hand means reimplementing
channel_binding_tags, the group-key
derivation, the seal, and the epoch/session handling in publish_chat — i.e. reimplementing
the encryption layer against private helpers, then keeping it in step with every rekey change.
For a client whose entire value is that Vector's crypto is doing the work, that is the one
thing it must not do.
send_chat_message's reply_to cannot express a root: it emits a q tag on a kind 9, and the
parse side reads exactly one target from it (v2/chat.rs:463-474).
- Passing a root id as the
reply_to parent (which is what we tried first) is strictly worse: it
makes every answer quote the opening question instead of the one being answered, and it points
jump-to-message and the reply-to-me ping (message-row.js:139-144) at the wrong message. We
have reverted it and now send the same flat shape Vector does, which is the best available
behaviour until the send API exists.
Happy to test a branch against a live Concord community and report back, and happy to be told
this is already on the roadmap behind the thread-aware GUI — in that case, a note on
build_comment_rumor saying "no public sender yet, see #N" would have saved us the source dive.
Thanks for a genuinely pleasant codebase to read — the comments explaining why are the reason
this report could be specific.
Summary
vector_core::community::v2::chat::build_comment_rumorbuilds a correct NIP-22 / CORD-03 §3kind-1111 threaded reply, including the
parent_rootinheritance that keeps the root stable atany depth. It has no production call site — only tests — and no public function on the send
surface reaches it. So a third-party client or bot built on the published
vector_sdk(0.5.0) /vector-core(0.4.0) can react to, edit and delete a kind-1111 reply, but cannot send one.Everything below is from
master@fcd47a27. This is a missing-API report, not a bug report:the hard part is already written and correct, it just is not reachable from outside the crate.
Who this is coming from
We run a read-only assistant bot in a Concord/Armada community (
vector_sdk0.5.0). It answersquestions in a channel. We went looking for how to make its answers thread properly and ended up
reading the client source, which is how we found the gap. Notes on rendering are included below
because they were needed to reach the conclusion, and because they might be useful to whoever
picks up the thread view.
Reproduction
A v1 community (
COMMUNITY_MESSAGE, kind 3300). Owner opens a topic, bot answers, owner asks afollow-up on the bot's answer, bot answers again. Four events, read from the local
vector.dbeventstable (ids truncated to 8,tagsverbatim):tags4bb21474[["ms","53"]]128f93d6[["ms","600"],["e","4bb21474…","","reply"]]f9c28dd5[["ms","979"],["e","128f93d6…","","reply"]]2b2f59f4[["ms","73"],["e","f9c28dd5…","","reply"]]Every event carries exactly one
etag markedreply, pointing at its immediate parent, and noroot marker. That is what
build_inner_fullproduces, and — importantly — it is also exactlywhat the Vector client itself sends in the same situation (see below), so nothing here is
malformed or foreign-client-specific.
The result is a four-message flat chain that no client can group, because the root is not
recoverable from any single event: you can only rebuild the topic by walking the chain, and the
walk breaks the moment one link is outside your window.
What the source says (the part that made us stop guessing)
1. What Vector sends for a reply, at any depth.
strCurrentReplyReferenceis the id of the row the user clicked reply on (src/main.js:6444,used at
src/main.js:12091). It goes to thesend_community_messagecommand(
src-tauri/src/commands/community.rs:931-939), which passes it straight through as the singlereferenceargument:envelope::build_inner_full(..., reply.as_deref(), ...)(
src-tauri/src/commands/community.rs:1116-1126) → one["e", <parent>, "", "reply"]tag, no root marker(
crates/vector-core/src/community/envelope.rs:217-221).chat::build_message_rumor(..., reply_ref, ...)(
src-tauri/src/commands/community.rs:1011-1013) → a kind 9 with a NIP-C7qquote tagnaming the immediate parent.
Either way it is the immediate parent only, at any depth. Vector's own reply is therefore the
same flat shape our bot produces — no divergence to fix on our side.
2. Vector never sends kind-1111.
build_comment_rumor(crates/vector-core/src/community/v2/chat.rs:153) is called fromcrates/vector-core/src/community/v2/chat.rs:715and:782(unit tests),crates/vector-core/src/community/v2/service.rs:7666(an integration test), andcrates/vector-core/src/community/v2/inbound.rs:1045— a fixture whose own comment says it isbuilding "the shape Armada sends" (
:1040). No production path.send_chat_message/send_chat_message_at(v2/service.rs:254,:271) always callbuild_message_rumor.3. The read side handles 1111 fully, and says the thread view is still to come.
parse_chat_rumoracceptskind::COMMENTand lifts the lowercaseeinto the samereply_toslot a kind-9
quses (v2/chat.rs:479-497), with the comment:and, on reactions to a received 1111 (
crates/vector-core/src/lib.rs:1902-1905):So interop is deliberate and already in place: Vector reads Armada's threads today and holds
the root tags for later. Only the send direction is missing.
4. Frontend, for completeness. The timeline is flat and chronological —
renderMessageappends one row per message (
src/js/render/chat/message-row.js; append sitessrc/main.js:6844,:6870,:6996), grouping is purely temporal(
shouldCollapseStreak,src/js/render/chat/message-streak.js:40-63), and a reply's onlyaffordance is a one-line quote of its immediate parent resolved by exact id
(
_dmsgBuildReplyContext,message-row.js:452-460, inserted at:181-190). Nothing walks achain or reads an uppercase
E. Consistent with the comments in (3): there is no thread viewyet, so a sender cannot make the current UI group anything — which is exactly why we are asking
for the send API rather than proposing a rendering change.
The gap
v2/service.rsexposessend_message(:242),send_chat_message(:254),send_chat_message_at(:271),send_reaction(:291),send_edit(:311),send_delete(:327) andmoderation_delete(:349). Three of those already acceptkind::COMMENTas a target —send_reaction's doc explicitly says "kind::COMMENTfor athreaded reply" (
:286-289) — so the crate can address a threaded reply it did not send. The twohelpers needed to send one,
chat_send_context(:415) andpublish_chat(:439), are private.vector_sdk0.5.0 inherits that:Channel::reply(replied_to, text)(src/lib.rs:833) andIncomingMessage::reply(text)(src/lib.rs:935) both funnel intoVectorCore::send_community_message(channel_id, content, replied_to)(
crates/vector-core/src/lib.rs:1631), which has one reply slot and no notion of a root.Net effect for a bot author: a kind-1111 threaded reply is unreachable, in every combination of
public calls, on both the SDK and
vector-core.Concrete request
Anything that reaches
build_comment_rumor. In rough order of preference:v2::service::send_comment(...)— mirrorsend_chat_message_at, forwardingbuild_comment_rumor's arguments:plus a
_atvariant, for parity with the precomputed-rumor-id contract described atv2/service.rs:266-269.An SDK wrapper — e.g.
Channel::reply_threaded(&self, parent: &ReplyTarget, text: &str),where
ReplyTargetcarries(id, kind, author, root). IdeallyIncomingMessagewould exposethe root of the message it arrived on, since the parse side already has the uppercase tags on
the rumor (
v2/chat.rs:483) — that is the one piece a bot cannot reconstruct locally.Minimum viable: make
chat_send_contextandpublish_chatpub(orpub(crate)→pubon a#[doc(hidden)]escape hatch). Less pleasant, but it unblocksexternal clients without new API surface to commit to.
A v1 equivalent is not requested — v1 has no comment kind, and if v1 threading ever matters, a
rootslot onbuild_inner_fullalongside the existingreferencewould be the natural shape.Why a bot author cannot work around this
channel_binding_tags, the group-keyderivation, the seal, and the epoch/session handling in
publish_chat— i.e. reimplementingthe encryption layer against private helpers, then keeping it in step with every rekey change.
For a client whose entire value is that Vector's crypto is doing the work, that is the one
thing it must not do.
send_chat_message'sreply_tocannot express a root: it emits aqtag on a kind 9, and theparse side reads exactly one target from it (
v2/chat.rs:463-474).reply_toparent (which is what we tried first) is strictly worse: itmakes every answer quote the opening question instead of the one being answered, and it points
jump-to-message and the reply-to-me ping (
message-row.js:139-144) at the wrong message. Wehave reverted it and now send the same flat shape Vector does, which is the best available
behaviour until the send API exists.
Happy to test a branch against a live Concord community and report back, and happy to be told
this is already on the roadmap behind the thread-aware GUI — in that case, a note on
build_comment_rumorsaying "no public sender yet, see #N" would have saved us the source dive.Thanks for a genuinely pleasant codebase to read — the comments explaining why are the reason
this report could be specific.