You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
buzz-acp: DM replies anchor to the latest message instead of the thread root — every exchange nests one level deeper #2748
In a 1:1 DM with a managed agent, once a thread exists, every agent reply anchors to the human's latest comment rather than the thread root. Each exchange therefore nests one level deeper — the user has to click-expand a sub-thread of a sub-thread of a sub-thread to follow a simple back-and-forth conversation with their own agent.
Root cause
crates/buzz-acp/src/queue.rs — the human-aware reply-anchor logic documents the right intent directly above the code:
Human-facing turns are anchored so replies stay readable at layer 1:
in a thread → anchor to the thread ROOT (no depth-2 nesting)
…and resolve_reply_anchor implements exactly that for group channels. But the DM branch contradicts it:
let reply_anchor = if is_dm {
thread_tags
.root_event_id.is_some().then(|| last_event.event.id.to_hex())// ← anchors to the LATEST message, not the root}else{resolve_reply_anchor(...)// ← group channels: anchors to ROOT};
When the triggering DM message is inside a thread, the anchor is last_event.event.id — the user's newest comment — so the agent's reply becomes a child of a child, and the nesting compounds every round.
Propose
In DMs, match the documented layer-1 rule: anchor to thread_tags.root_event_id when inside a thread (same as the group-channel human path). Worth discussing whether top-level DM exchanges should thread at all — a 1:1 DM arguably reads best flat, with threading reserved for explicit user-initiated threads — but the root-anchoring fix alone eliminates the compounding sub-chat nesting.
Observed on a self-hosted relay (ghcr.io/block/buzz:main 2026-07-24, desktop 0.4.24), desktop-managed agents, default settings. Sibling issues: #2270 (thread deafness without re-mention), and the DM mention-gate issue filed alongside this one.
Independent reproduction on macOS (Darwin 25.4.0), desktop-managed agent, self-hosted relay, main at 63c62fc — and I arrived at the same line before finding this issue.
What the user sees: in a 1:1 DM, a reply typed into the thread composer gets answered one
level deeper — the human's in-thread message grows its own 1 reply sub-thread, and every round
adds another level. Reading a simple back-and-forth means expanding a sub-thread of a sub-thread.
The client is not at fault. The desktop thread composer already anchors to the thread head, not
to the latest message — desktop/src/features/messages/ui/MessageThreadPanel.tsx:257:
So the human's reply lands correctly at layer 1; only the agent's reply nests. That isolates the
defect to the harness anchor exactly as described here.
Worth quoting as the contract this violates — crates/buzz-acp/src/base_prompt.md:52-56
already tells agents what to expect:
Use the reply destination supplied in the [Context] block for ordinary replies in this turn.
[…] For human-facing work, keep the conversation flat and easy to read. The app/harness will
choose the correct reply destination: the root of the triggering thread when the turn is
already threaded, or the triggering top-level event when the human started a new thread.
The agent is doing what it was told; the harness told it the wrong thing in the DM branch.
Nothing downstream catches it.buzz messages send --reply-to <parent> faithfully reconstructs
NIP-10 tags from the parent's ancestry (crates/buzz-cli/src/commands/messages.rs:521-527), and
the relay only validates that the submitted root matches real ancestry — depth is allowed up to
100 (crates/buzz-relay/src/handlers/ingest.rs:630-679). The prompt anchor is the only thing
keeping human-facing conversations flat, which is why this compounds silently.
On the two open PRs (#2766 and #2778): both make the identical production change
(thread_tags.root_event_id.clone()), which matches what I independently landed on. If a
maintainer is picking one, #2778 has the stronger test story — it adds two fresh regression tests
(threaded-anchors-to-root, top-level-stays-flat) alongside correcting the pre-existing test, where #2766 only rewrites the existing test in place. Either fixes the bug.
Follow-up on the parked paragraph. The issue notes:
Worth discussing whether top-level DM exchanges should thread at all […]
I filed that separately as #3075 with a PR, since it's a behavior question rather than a bug in the
anchor, and it's the reason the first answer in a DM also ends up hidden. It touches a different
branch of format_context_hints, so it does not conflict with either PR here.
This still reproduces on current main (8599efb6a): in format_prompt (crates/buzz-acp/src/queue.rs), a DM message inside a thread anchors the reply to routing_event_id, the triggering message, so every exchange nests one level deeper.
There are now six open PRs for this issue, all with merge conflicts and none merged. They split into two designs:
One point in favour of root-anchoring: the native-steer guard already assumes it. ReplyRoute::accepts_steer treats two DM messages with the same thread root as sharing a reply destination, and reply_anchor_is_trigger returns false for every DM. With today's trigger anchor, a message steered into a running DM-thread turn is answered as a reply to the earlier message. With a root anchor, that assumption becomes true and the guard needs no change. Flat DMs also stop the nesting, but they move the agent's answer out of a thread the human chose to type in.
Could a maintainer say which design you want? I'm happy to rebase the chosen PR onto main and add tests covering the steer interaction, so this can land.
Activity
muqsitnawaz commentedon Jul 27, 2026
Independent reproduction on macOS (Darwin 25.4.0), desktop-managed agent, self-hosted relay,
mainat 63c62fc — and I arrived at the same line before finding this issue.What the user sees: in a 1:1 DM, a reply typed into the thread composer gets answered one
level deeper — the human's in-thread message grows its own
1 replysub-thread, and every roundadds another level. Reading a simple back-and-forth means expanding a sub-thread of a sub-thread.
The client is not at fault. The desktop thread composer already anchors to the thread head, not
to the latest message —
desktop/src/features/messages/ui/MessageThreadPanel.tsx:257:So the human's reply lands correctly at layer 1; only the agent's reply nests. That isolates the
defect to the harness anchor exactly as described here.
Worth quoting as the contract this violates —
crates/buzz-acp/src/base_prompt.md:52-56already tells agents what to expect:
The agent is doing what it was told; the harness told it the wrong thing in the DM branch.
Nothing downstream catches it.
buzz messages send --reply-to <parent>faithfully reconstructsNIP-10 tags from the parent's ancestry (
crates/buzz-cli/src/commands/messages.rs:521-527), andthe relay only validates that the submitted
rootmatches real ancestry — depth is allowed up to100 (
crates/buzz-relay/src/handlers/ingest.rs:630-679). The prompt anchor is the only thingkeeping human-facing conversations flat, which is why this compounds silently.
On the two open PRs (#2766 and #2778): both make the identical production change
(
thread_tags.root_event_id.clone()), which matches what I independently landed on. If amaintainer is picking one, #2778 has the stronger test story — it adds two fresh regression tests
(threaded-anchors-to-root, top-level-stays-flat) alongside correcting the pre-existing test, where
#2766 only rewrites the existing test in place. Either fixes the bug.
Follow-up on the parked paragraph. The issue notes:
I filed that separately as #3075 with a PR, since it's a behavior question rather than a bug in the
anchor, and it's the reason the first answer in a DM also ends up hidden. It touches a different
branch of
format_context_hints, so it does not conflict with either PR here.ddfourtwo commentedon Oct 14, 2026
This still reproduces on current
main(8599efb6a): informat_prompt(crates/buzz-acp/src/queue.rs), a DM message inside a thread anchors the reply torouting_event_id, the triggering message, so every exchange nests one level deeper.There are now six open PRs for this issue, all with merge conflicts and none merged. They split into two designs:
One point in favour of root-anchoring: the native-steer guard already assumes it.
ReplyRoute::accepts_steertreats two DM messages with the same thread root as sharing a reply destination, andreply_anchor_is_triggerreturnsfalsefor every DM. With today's trigger anchor, a message steered into a running DM-thread turn is answered as a reply to the earlier message. With a root anchor, that assumption becomes true and the guard needs no change. Flat DMs also stop the nesting, but they move the agent's answer out of a thread the human chose to type in.Could a maintainer say which design you want? I'm happy to rebase the chosen PR onto
mainand add tests covering the steer interaction, so this can land.