From d1167fd23aaa322fe1f3d7c647663e277bf47960 Mon Sep 17 00:00:00 2001 From: Luke Tornquist Date: Fri, 6 Nov 2026 08:59:04 -0500 Subject: [PATCH] fix(loadgen): answer turn targets at the thread's first level A turn replied under its target, so an agent answering the channel's newest message, often another agent's reply, nested one level deeper each time, toward the relay's 100-level depth limit. Reply under the thread root instead, as the harness does for a human-facing turn: depth stays at one, and a top-level target still becomes a new thread's root. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Luke Tornquist --- crates/buzz-test-client/src/loadgen/run.rs | 25 +++++++++++----------- perf/LOADGEN.md | 6 ++++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/crates/buzz-test-client/src/loadgen/run.rs b/crates/buzz-test-client/src/loadgen/run.rs index fffa54fb06..af24527264 100644 --- a/crates/buzz-test-client/src/loadgen/run.rs +++ b/crates/buzz-test-client/src/loadgen/run.rs @@ -1523,20 +1523,21 @@ struct CountReply { _count: u64, } -/// Ends a turn: a timed reply in the answered message's thread (or the -/// channel), then each reaction's removal, which reads the agent's -/// reactions on the target and deletes them, as the harness does. +/// Ends a turn: a timed reply in the answered message's thread, then each +/// reaction's removal, which reads the agent's reactions on the target and +/// deletes them, as the harness does. async fn end_turn(shared: &Shared, community: &Community, keys: &Keys, auth: &Tag, turn: Turn) { let me = keys.public_key().to_hex(); let mut tags = vec![tag(["h", &turn.channel])]; if let Some(target) = &turn.target { - // A threaded target is answered in its thread; a top-level one - // becomes the root of a new thread. - let id = target.id.to_hex(); + // As a harness answers a person: a threaded target is answered at + // its thread's first level, under the root, and a top-level one + // becomes the root of a new thread. Replies never nest deeper, so + // agents answering agents can't climb to the relay's depth limit. let root = buzz_core::nip10::parse_thread_markers(&target.tags) .resolve() - .map_or_else(|| id.clone(), |(root, _)| root); - tags.extend(reply_tags(&root, &id)); + .map_or_else(|| target.id.to_hex(), |(root, _)| root); + tags.extend(reply_tags(&root, &root)); } let reply = EventBuilder::new( kind(KIND_STREAM_MESSAGE), @@ -2724,15 +2725,15 @@ hold_secs = 3 let e = reaction.tags.iter().find(|t| t.kind().as_str() == "e"); assert_eq!(e.and_then(|t| t.content()), Some(target_id.as_str())); } - // Every finished turn replied in the target's thread, under the - // target's own root, and deleted both of its reactions; only a turn - // cut by the stop leaves any. + // Every finished turn replied at the first level of the target's + // thread, under its root, and deleted both of its reactions; only a + // turn cut by the stop leaves any. let replies: Vec<&Event> = of(KIND_STREAM_MESSAGE).collect(); assert!(!replies.is_empty(), "{posted:?}"); for reply in &replies { let markers = buzz_core::nip10::parse_thread_markers(&reply.tags); assert_eq!(markers.root.as_deref(), Some(root_id.as_str())); - assert_eq!(markers.reply.as_deref(), Some(target_id.as_str())); + assert_eq!(markers.reply.as_deref(), Some(root_id.as_str())); } let deleted: Vec = of(KIND_DELETION) .flat_map(|e| { diff --git a/perf/LOADGEN.md b/perf/LOADGEN.md index 04d8a043a9..d7b9a695d6 100644 --- a/perf/LOADGEN.md +++ b/perf/LOADGEN.md @@ -122,8 +122,10 @@ does around a prompt: observer frame of `frame_bytes` plaintext every `frame_secs` over the socket, plus `reads` HTTP `/query` reads of the channel's newest 50 messages at random points in the turn, as tool calls. -3. **End**: a timed reply in the message's thread through HTTP `/events`, - then each reaction's removal: a `/query` for the agent's reactions on +3. **End**: a timed reply through HTTP `/events`, at the first level of the + message's thread (under its root), as a harness answers a person; a + top-level message becomes the root of a new thread. Then each + reaction's removal: a `/query` for the agent's reactions on the message and a kind:5 deletion of each one found. An agent makes one HTTP request sequence at a time, and the socket keeps