mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Fix task scoping and agent start reconciliation
This commit is contained in:
@@ -1385,10 +1385,7 @@ pub fn format_prompt(batch: &FlushBatch, args: &FormatPromptArgs<'_>) -> Vec<Str
|
||||
// there. DMs are always 1:1 with a human, so they always anchor.
|
||||
let sender_pubkey = last_event.event.pubkey.to_hex();
|
||||
let reply_anchor = if is_dm {
|
||||
thread_tags
|
||||
.root_event_id
|
||||
.is_some()
|
||||
.then(|| last_event.event.id.to_hex())
|
||||
thread_tags.root_event_id.clone()
|
||||
} else {
|
||||
resolve_reply_anchor(
|
||||
&sender_pubkey,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::{app_state::AppState, managed_agents::load_managed_agents};
|
||||
|
||||
use super::agents::ProfileReconcileData;
|
||||
|
||||
pub(super) fn refresh_auth_tag(
|
||||
app: &AppHandle,
|
||||
state: &AppState,
|
||||
pubkey: &str,
|
||||
data: &mut ProfileReconcileData,
|
||||
) {
|
||||
let result = (|| -> Result<(), String> {
|
||||
let _store_guard = state
|
||||
.managed_agents_store_lock
|
||||
.lock()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let records = load_managed_agents(app)?;
|
||||
let record = records
|
||||
.iter()
|
||||
.find(|record| record.pubkey == pubkey)
|
||||
.ok_or_else(|| format!("agent {pubkey} not found"))?;
|
||||
data.auth_tag = record.auth_tag.clone();
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
if let Err(error) = result {
|
||||
eprintln!(
|
||||
"buzz-desktop: profile reconciliation using pre-start auth tag for agent {pubkey}: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1000,8 +1000,7 @@ pub async fn start_managed_agent(
|
||||
}
|
||||
|
||||
// Collect backend info under lock; async preflight/spawn happens below.
|
||||
// Also snapshot profile reconciliation data for the background task.
|
||||
let (target, reconcile_data) = {
|
||||
let (target, mut reconcile_data) = {
|
||||
let _store_guard = state
|
||||
.managed_agents_store_lock
|
||||
.lock()
|
||||
@@ -1103,9 +1102,10 @@ pub async fn start_managed_agent(
|
||||
// ── Profile reconciliation (fire-and-forget) ────────────────────────────
|
||||
// On successful start, spawn a background task to ensure the agent's kind:0
|
||||
// profile is published on the relay. This self-heals cases where the initial
|
||||
// profile sync at creation time failed silently. For legacy records (pre-PR-921)
|
||||
// with no persisted avatar, this also backfills the avatar from the relay.
|
||||
// profile sync at creation time failed silently.
|
||||
if result.is_ok() {
|
||||
use super::agent_profile_reconcile as reconcile;
|
||||
reconcile::refresh_auth_tag(&app, &state, &pubkey, &mut reconcile_data);
|
||||
let reconcile_pubkey = pubkey.clone();
|
||||
let reconcile_app = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod agent_config;
|
||||
mod agent_discovery;
|
||||
mod agent_models;
|
||||
mod agent_profile_reconcile;
|
||||
mod agent_settings;
|
||||
mod agents;
|
||||
mod canvas;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
readPersistedAgentConversations,
|
||||
writePersistedAgentConversations,
|
||||
} from "./agentConversations.ts";
|
||||
import { isConversationMessage } from "./ui/AgentConversationScreen.helpers.ts";
|
||||
|
||||
function message({ body, createdAt, id, pubkey = "human" }) {
|
||||
return {
|
||||
@@ -491,3 +492,75 @@ test("continued conversation markers keep later task anchors visible", () => {
|
||||
|
||||
assert.deepEqual([...hiddenIds], ["hidden", "later"]);
|
||||
});
|
||||
|
||||
test("dedicated conversation view stops at the next task anchor", () => {
|
||||
const root = message({
|
||||
body: "Can you look into the data model?",
|
||||
createdAt: 1,
|
||||
id: "root",
|
||||
});
|
||||
const firstAnchor = message({
|
||||
body: "I'll look into it.",
|
||||
createdAt: 2,
|
||||
id: "agent-reply",
|
||||
pubkey: "agent",
|
||||
});
|
||||
const firstTaskReply = message({
|
||||
body: "This belongs in the first task.",
|
||||
createdAt: 3,
|
||||
id: "first-task-reply",
|
||||
});
|
||||
const secondAnchor = message({
|
||||
body: "Let's split this into another task.",
|
||||
createdAt: 4,
|
||||
id: "second-anchor",
|
||||
pubkey: "agent",
|
||||
});
|
||||
const secondTaskReply = message({
|
||||
body: "This belongs in the second task.",
|
||||
createdAt: 5,
|
||||
id: "second-task-reply",
|
||||
});
|
||||
const messages = [
|
||||
root,
|
||||
firstAnchor,
|
||||
firstTaskReply,
|
||||
secondAnchor,
|
||||
secondTaskReply,
|
||||
];
|
||||
const conversation = buildAgentConversation({
|
||||
agentName: "Fizz",
|
||||
agentPubkey: "agent",
|
||||
agentReply: firstAnchor,
|
||||
channel: { id: "channel", name: "general" },
|
||||
contextMessages: messages,
|
||||
parentMessage: root,
|
||||
threadRootMessage: root,
|
||||
});
|
||||
const firstMarker = parseAgentConversationMarker(
|
||||
markerEvent({ content: { startedAt: 2 }, createdAt: 2 }),
|
||||
);
|
||||
const secondMarker = parseAgentConversationMarker({
|
||||
...markerEvent({
|
||||
content: { agentReplyId: "second-anchor", startedAt: 4 },
|
||||
createdAt: 4,
|
||||
id: "second-marker",
|
||||
}),
|
||||
tags: [
|
||||
["h", "channel"],
|
||||
["e", "root", "", "root"],
|
||||
["e", "second-anchor", "", "agent-reply"],
|
||||
["p", "agent"],
|
||||
["title", "Second task"],
|
||||
],
|
||||
});
|
||||
const markers = [firstMarker, secondMarker].filter(Boolean);
|
||||
|
||||
const visibleIds = messages
|
||||
.filter((entry) =>
|
||||
isConversationMessage(entry, conversation, markers, messages),
|
||||
)
|
||||
.map((entry) => entry.id);
|
||||
|
||||
assert.deepEqual(visibleIds, ["root", "agent-reply", "first-task-reply"]);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { AgentConversation } from "@/features/agents/agentConversations";
|
||||
import type {
|
||||
AgentConversation,
|
||||
AgentConversationMarker,
|
||||
} from "@/features/agents/agentConversations";
|
||||
import { collectMessageMentionPubkeys } from "@/features/messages/lib/formatTimelineMessages";
|
||||
import type {
|
||||
TimelineMessage,
|
||||
@@ -74,12 +77,90 @@ export function formatAgentParticipantNames(
|
||||
export function isConversationMessage(
|
||||
message: TimelineMessage,
|
||||
conversation: AgentConversation,
|
||||
markers: readonly AgentConversationMarker[] = [],
|
||||
messages: readonly TimelineMessage[] = [],
|
||||
) {
|
||||
return (
|
||||
if (
|
||||
message.id === conversation.threadRootId ||
|
||||
message.id === conversation.agentReply.id ||
|
||||
message.rootId === conversation.threadRootId ||
|
||||
message.parentId === conversation.threadRootId
|
||||
message.id === conversation.parentMessage?.id ||
|
||||
message.id === conversation.agentReply.id
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const messageThreadRootId = message.rootId ?? message.parentId ?? null;
|
||||
if (messageThreadRootId !== conversation.threadRootId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const orderedThreadMessages =
|
||||
messages.length > 0
|
||||
? messages.filter(
|
||||
(candidate) =>
|
||||
candidate.id === conversation.threadRootId ||
|
||||
candidate.rootId === conversation.threadRootId ||
|
||||
candidate.parentId === conversation.threadRootId,
|
||||
)
|
||||
: [];
|
||||
const messageIndexById = new Map(
|
||||
orderedThreadMessages.map((candidate, index) => [candidate.id, index]),
|
||||
);
|
||||
const anchorIndex = messageIndexById.get(conversation.agentReply.id);
|
||||
const messageIndex = messageIndexById.get(message.id);
|
||||
|
||||
if (anchorIndex !== undefined && messageIndex !== undefined) {
|
||||
if (messageIndex < anchorIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let nextAnchorIndex = Number.POSITIVE_INFINITY;
|
||||
for (const marker of markers) {
|
||||
if (
|
||||
marker.channelId !== conversation.channelId ||
|
||||
marker.threadRootId !== conversation.threadRootId ||
|
||||
marker.agentReplyId === conversation.agentReply.id
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const markerAnchorIndex = messageIndexById.get(marker.agentReplyId);
|
||||
if (
|
||||
markerAnchorIndex !== undefined &&
|
||||
markerAnchorIndex > anchorIndex &&
|
||||
markerAnchorIndex < nextAnchorIndex
|
||||
) {
|
||||
nextAnchorIndex = markerAnchorIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return messageIndex < nextAnchorIndex;
|
||||
}
|
||||
|
||||
const currentMarker =
|
||||
markers.find(
|
||||
(marker) =>
|
||||
marker.channelId === conversation.channelId &&
|
||||
marker.threadRootId === conversation.threadRootId &&
|
||||
marker.agentReplyId === conversation.agentReply.id,
|
||||
) ?? null;
|
||||
const selectedStartedAt =
|
||||
currentMarker?.startedAt ?? conversation.agentReply.createdAt;
|
||||
if (message.createdAt < selectedStartedAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nextMarkerStartedAt = markers
|
||||
.filter(
|
||||
(marker) =>
|
||||
marker.channelId === conversation.channelId &&
|
||||
marker.threadRootId === conversation.threadRootId &&
|
||||
marker.agentReplyId !== conversation.agentReply.id &&
|
||||
marker.startedAt > selectedStartedAt,
|
||||
)
|
||||
.sort((left, right) => left.startedAt - right.startedAt)[0]?.startedAt;
|
||||
|
||||
return (
|
||||
nextMarkerStartedAt === undefined || message.createdAt < nextMarkerStartedAt
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,10 @@ export function AgentConversationScreen({
|
||||
const sendMessageMutation = useSendMessageMutation(channel, currentIdentity);
|
||||
|
||||
const relayMessages = messagesQuery.data ?? [];
|
||||
const agentConversationMarkers = React.useMemo(
|
||||
() => buildAgentConversationMarkers(relayMessages),
|
||||
[relayMessages],
|
||||
);
|
||||
const {
|
||||
getMessageReadAt,
|
||||
isThreadMuted,
|
||||
@@ -211,7 +215,12 @@ export function AgentConversationScreen({
|
||||
profiles,
|
||||
);
|
||||
const scoped = formatted.filter((message) =>
|
||||
isConversationMessage(message, conversation),
|
||||
isConversationMessage(
|
||||
message,
|
||||
conversation,
|
||||
agentConversationMarkers,
|
||||
formatted,
|
||||
),
|
||||
);
|
||||
const sourceMessages =
|
||||
scoped.length > 0
|
||||
@@ -231,6 +240,7 @@ export function AgentConversationScreen({
|
||||
);
|
||||
}, [
|
||||
channel,
|
||||
agentConversationMarkers,
|
||||
conversation,
|
||||
currentIdentity?.pubkey,
|
||||
currentProfile?.avatarUrl,
|
||||
@@ -477,10 +487,6 @@ export function AgentConversationScreen({
|
||||
React.useEffect(() => {
|
||||
lastPublishedThreadRecapRef.current = null;
|
||||
}, [conversation.id]);
|
||||
const agentConversationMarkers = React.useMemo(
|
||||
() => buildAgentConversationMarkers(relayMessages),
|
||||
[relayMessages],
|
||||
);
|
||||
const currentConversationMarker = React.useMemo(
|
||||
() =>
|
||||
agentConversationMarkers.find(
|
||||
|
||||
Reference in New Issue
Block a user