Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions crates/buzz-acp/src/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3785,6 +3785,33 @@ fn parse_dm_response(json: serde_json::Value, limit: u32) -> Option<Conversation
})
}

fn json_board_references(obj: &serde_json::Value) -> Vec<buzz_sdk::BoardReference> {
let Some(raw_tags) = obj.get("tags").and_then(|value| value.as_array()) else {
return Vec::new();
};
let tags = raw_tags
.iter()
.filter_map(|value| {
let parts = value
.as_array()?
.iter()
.map(|part| part.as_str().map(str::to_string))
.collect::<Option<Vec<_>>>()?;
nostr::Tag::parse(parts).ok()
})
.collect::<Vec<_>>();
let tags = nostr::Tags::from_list(tags);
let workstream_id = tags.iter().find_map(|tag| {
let values = tag.as_slice();
(values.first().map(String::as_str) == Some("h"))
.then(|| values.get(1))
.flatten()
});
workstream_id
.map(|id| buzz_sdk::parse_board_references_for_workstream(&tags, id))
.unwrap_or_default()
}

/// Extract a `ContextMessage` from a JSON message object.
///
/// Works with both thread reply objects and channel message objects.
Expand Down Expand Up @@ -3819,6 +3846,7 @@ fn json_to_context_message(obj: &serde_json::Value) -> Option<ContextMessage> {
pubkey: pubkey.to_string(),
timestamp,
content: content.to_string(),
board_references: json_board_references(obj),
})
}

Expand Down Expand Up @@ -5846,6 +5874,54 @@ mod tests {
assert!(json_to_context_message(&obj).is_none());
}

#[test]
fn test_json_to_context_message_projects_only_strict_board_references() {
let workstream_id = "123e4567-e89b-12d3-a456-426614174000";
let valid = json!({
"kind": "workstream",
"identity": workstream_id,
"snapshot": { "label": "Checkout recovery" },
"placement": {
"workstreamId": workstream_id,
"workstreamLabel": "checkout-recovery",
"sectionId": "workstream",
"sectionLabel": "Workstream"
}
});
let mut unknown = valid.clone();
unknown["snapshot"]["extra"] = json!(true);
let mut controlled = valid.clone();
controlled["snapshot"]["label"] = json!("bad\u{0085}label");
let mut forged = valid.clone();
forged["identity"] = json!("123e4567-e89b-12d3-a456-426614174001");
let tag = |value: serde_json::Value| {
json!([
buzz_sdk::BOARD_REFERENCE_TAG,
buzz_sdk::BOARD_REFERENCE_VERSION,
serde_json::to_string(&value).unwrap()
])
};
let obj = json!({
"pubkey": "abc",
"content": "hello",
"created_at": 1710518400,
"tags": [
["h", workstream_id],
tag(unknown),
tag(controlled),
tag(forged),
tag(valid.clone())
]
});

let msg = json_to_context_message(&obj).expect("message survives bad references");
assert_eq!(msg.board_references.len(), 1);
assert_eq!(
serde_json::to_value(&msg.board_references[0]).unwrap(),
valid
);
}

#[test]
fn test_collect_prompt_pubkeys_includes_authors_mentions_and_context() {
let keys = Keys::generate();
Expand Down Expand Up @@ -5875,6 +5951,7 @@ mod tests {
pubkey: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".into(),
timestamp: "2026-03-25T05:51:25Z".into(),
content: "follow up".into(),
board_references: vec![],
}],
total: 1,
truncated: false,
Expand Down Expand Up @@ -5951,6 +6028,7 @@ mod tests {
pubkey: "author".into(),
timestamp: "2026-08-09T00:00:00Z".into(),
content: content.into(),
board_references: vec![],
}
}

Expand Down
43 changes: 42 additions & 1 deletion crates/buzz-acp/src/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,8 @@ pub struct ContextMessage {
pub pubkey: String,
pub timestamp: String,
pub content: String,
/// Validated, ordered Board references; raw reference tags are never projected.
pub board_references: Vec<buzz_sdk::BoardReference>,
}

/// Channel metadata for prompt formatting.
Expand Down Expand Up @@ -1107,6 +1109,17 @@ fn format_prompt_actor(pubkey: &str, profile_lookup: Option<&PromptProfileLookup
}
}

/// Append a validated machine-readable projection associated with one message.
fn append_board_references(output: &mut String, references: &[buzz_sdk::BoardReference]) {
if references.is_empty() {
return;
}
if let Ok(json) = serde_json::to_string(references) {
output.push_str("\nBoard references (validated JSON): ");
output.push_str(&json);
}
}

/// Format the per-event `[Event]` block for a single [`BatchEvent`].
///
/// Includes: event_id, channel (name + UUID), kind, sender (hex + npub),
Expand Down Expand Up @@ -1152,10 +1165,31 @@ pub(crate) fn format_event_block(
);

// Always include tags — they carry structural information.
let tags_json: Vec<&[String]> = be.event.tags.iter().map(|t| t.as_slice()).collect();
let tags_json: Vec<&[String]> = be
.event
.tags
.iter()
.map(|t| t.as_slice())
.filter(|tag| tag.first().map(String::as_str) != Some(buzz_sdk::BOARD_REFERENCE_TAG))
.collect();
if let Ok(tags_str) = serde_json::to_string(&tags_json) {
block.push_str(&format!("\nTags: {tags_str}"));
}
let board_references = be
.event
.tags
.iter()
.find_map(|tag| {
let values = tag.as_slice();
(values.first().map(String::as_str) == Some("h"))
.then(|| values.get(1))
.flatten()
})
.map(|workstream_id| {
buzz_sdk::parse_board_references_for_workstream(&be.event.tags, workstream_id)
})
.unwrap_or_default();
append_board_references(&mut block, &board_references);

// Parsed structural fields.
let thread = parse_thread_tags(&be.event);
Expand Down Expand Up @@ -1440,6 +1474,7 @@ fn format_conversation_context(
msg.timestamp,
msg.content,
));
append_board_references(&mut s, &msg.board_references);
}
s
}
Expand Down Expand Up @@ -2744,6 +2779,7 @@ mod tests {
event_id: String::new(),
pubkey: "npub1test".into(),
content: "prior message".into(),
board_references: vec![],
timestamp: "2024-01-01T00:00:00Z".into(),
}],
total: 1,
Expand Down Expand Up @@ -3362,12 +3398,14 @@ mod tests {
pubkey: "npub1xyz".into(),
timestamp: "2026-03-15T16:30:00Z".into(),
content: "Let's refactor auth".into(),
board_references: vec![],
},
ContextMessage {
event_id: String::new(),
pubkey: "npub1def".into(),
timestamp: "2026-03-15T16:35:00Z".into(),
content: "yes go ahead".into(),
board_references: vec![],
},
],
total: 5,
Expand Down Expand Up @@ -3412,6 +3450,7 @@ mod tests {
pubkey: "npub1abc".into(),
timestamp: "2026-03-15T16:00:00Z".into(),
content: "Can you deploy?".into(),
board_references: vec![],
}],
total: 1,
truncated: false,
Expand Down Expand Up @@ -3458,6 +3497,7 @@ mod tests {
pubkey: author_hex.clone(),
timestamp: "2026-03-25T05:51:25Z".into(),
content: "follow up".into(),
board_references: vec![],
}],
total: 1,
truncated: false,
Expand Down Expand Up @@ -3672,6 +3712,7 @@ mod tests {
pubkey: "npub1xyz".into(),
timestamp: "2026-03-15T16:30:00Z".into(),
content: "Should I deploy?".into(),
board_references: vec![],
}],
total: 1,
truncated: false,
Expand Down
Loading
Loading