Add agent conversation runtime foundation

This commit is contained in:
klopez4212
2026-06-30 13:33:32 +01:00
parent 325b3fdb0d
commit aba7bdf45c
21 changed files with 661 additions and 423 deletions
+38 -6
View File
@@ -60,7 +60,10 @@ const overrides = new Map([
// config-bridge: get_agent_config_surface/write_agent_config_field/put_agent_session_config
// commands add ~40 lines. Queued to split.
// branch cut; override bumped to cover the merged total. Queued to split.
["src-tauri/src/commands/agents.rs", 1437],
// continued-agent-conversations: refreshes the owner auth tag before
// starting/restoring/deploying agents so staged identities keep working.
// latest-main rebase adds the config-bridge and task-review fixes together.
["src-tauri/src/commands/agents.rs", 1467],
// Residual repos_dir integration in ensure_nest_at: REPOS is provisioned
// outside NEST_DIRS (it may be a symlink), so it needs its own create +
// chmod-only-when-real-dir handling plus integration test coverage. The
@@ -73,7 +76,12 @@ const overrides = new Map([
// unify refactor followup. +26 for resolve_effective_prompt_model_provider
// re-introduced after 826d735fe removal (config-bridge caller still needs it).
// PGID resolution helper + PID-recycling safety guard added for orphan sweep.
["src-tauri/src/managed_agents/runtime.rs", 2150],
// continued-agent-conversations: owner-scoped auth tag refresh is threaded
// through the runtime env builder and covered by regression tests.
// latest-main rebase adds the config-bridge and task-review fixes together.
// latest main added runtime restore plumbing on top of the task anchor review fixes.
["src-tauri/src/managed_agents/runtime.rs", 2174],
["src-tauri/src/managed_agents/personas.rs", 1080],
// Phase-2 inbound reconcile + review-fix cycle: reconcile_inbound_persona_event
// dispatches 30175/30176/30177 inbound plus kind:5 tombstone consume
// (reconcile_inbound_tombstone), the two apply_inbound_* fns, the
@@ -82,11 +90,12 @@ const overrides = new Map([
// queued to split with the list. The two `agents-data-changed` emits (live
// UI refresh on inbound reconcile + tombstone) add the latest growth.
["src-tauri/src/commands/personas.rs", 1279],
["src-tauri/src/managed_agents/persona_card.rs", 1050],
// applyWorkspace reposDir parameter plus the validateReposDir binding,
// threaded through Tauri invokes for configurable repos_dir, plus the
// harness-persona-sync `harnessOverride` create-input bit — load-bearing
// parameter plumbing, not generic debt growth. Approved override; still
// queued to split.
// parameter plumbing, plus continued-agent-conversations client task-anchor
// tags on message sends. Approved override; still queued to split.
["src/shared/api/tauri.ts", 1235],
// harness-persona-sync feature growth, queued to split in the resolver-unify
// refactor followup. discovery.rs is dominated by the new test module
@@ -95,7 +104,11 @@ const overrides = new Map([
// agents keep an installed runtime alias when the primary command is absent.
// Load-bearing, not generic debt.
// config-bridge: schema-driven field extraction adds ~26 lines. Queued to split.
["src-tauri/src/managed_agents/discovery.rs", 1111],
// latest-main rebase adds the config-bridge and task-review fixes together.
["src-tauri/src/managed_agents/discovery.rs", 1131],
// types.rs adds the persona/instance harness fields. Load-bearing, not
// generic debt.
["src-tauri/src/managed_agents/types.rs", 1037],
// migration_tests.rs carries the harness-sync migration coverage plus the
// patch_json_records owner-only writeback regression test (SECURITY.md:90
// crash-safe 0o600 fallback). Load-bearing security + feature coverage, not
@@ -115,6 +128,24 @@ const overrides = new Map([
// +135 for AgentInfoFocusedView/DiagnosticsFocusedView/ChannelsFocusedView
// props restored after 826d735fe removal (UserProfilePanel.tsx still needs them).
["src/features/profile/ui/UserProfilePanelSections.tsx", 1140],
// useDueReminderBadgeCount hook call + sum to wire due-reminder count into
// the Inbox nav badge — a small overage from load-bearing badge plumbing,
// not generic debt growth. Approved override; still queued to split.
// continued-agent-conversations: persisted channel-scoped conversation state
// and route wiring. Queued to split with the rest of AppShell state.
["src/app/AppShell.tsx", 1060],
// continued-agent-conversations: marker filtering, thread handoff, and
// activity handoff props live at the channel surface for now.
["src/features/channels/ui/ChannelPane.tsx", 1107],
// continued-agent-conversations: channel task/message surface routing is
// threaded through the screen while the pane split follow-up is pending.
["src/features/channels/ui/ChannelScreen.tsx", 1027],
// continued-agent-conversations: composer notice banner for read-only agent
// conversations.
["src/features/messages/ui/MessageComposer.tsx", 1010],
// continued-agent-conversations: channel sidebar children and active
// conversation unread suppression. Queued to split with sidebar sections.
["src/features/sidebar/ui/AppSidebar.tsx", 1081],
// PersistBackend enum + marker-on-keyring-success plumbing and its three
// fail-closed regression tests (silent identity rotation on keyring outage).
// A small overage from load-bearing security plumbing on a file already at
@@ -136,7 +167,8 @@ const overrides = new Map([
// catalog module; agent_models.rs retains the thin wrapper (~50 lines).
// File still exceeds 1000 due to OpenAI/Anthropic discovery + subprocess
// fallback. Queued to split into dedicated discovery modules.
["src-tauri/src/commands/agent_models.rs", 1066],
// latest main rebase adds the provider fallback guard.
["src-tauri/src/commands/agent_models.rs", 1068],
]);
await runFileSizeCheck({
@@ -176,6 +176,7 @@ pub async fn get_agent_config_surface(
record.persona_id.as_deref(),
&personas,
record.agent_command_override.as_deref(),
Some(&record.agent_command),
);
let runtime_meta = known_acp_runtime(&effective_cmd);
let session_cache = state.get_session_cache(&pubkey);
@@ -66,6 +66,7 @@ pub async fn get_agent_models(
record.persona_id.as_deref(),
&personas,
record.agent_command_override.as_deref(),
Some(&record.agent_command),
);
let args = normalize_agent_args(&effective_command, record.agent_args.clone());
@@ -929,6 +930,7 @@ pub async fn update_managed_agent(
record.persona_id.as_deref(),
&personas,
record.agent_command_override.as_deref(),
Some(&record.agent_command),
);
let avatar_url = record
.avatar_url
@@ -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}"
);
}
}
+37 -7
View File
@@ -241,6 +241,11 @@ async fn start_local_agent_with_preflight(
ensure_relay_mesh_for_record(app, &record_snapshot, allow_fresh_create_start).await?;
let refreshed_auth_tag = {
let owner_keys = state.keys.lock().map_err(|e| e.to_string())?;
crate::managed_agents::managed_agent_auth_tag_for_owner(&owner_keys, pubkey)?
};
let _store_guard = state
.managed_agents_store_lock
.lock()
@@ -254,6 +259,14 @@ async fn start_local_agent_with_preflight(
if record.backend != BackendKind::Local {
return Err(format!("agent {pubkey} is no longer a local agent"));
}
if !crate::managed_agents::auth_tag_matches_owner(
record.auth_tag.as_deref(),
&record.pubkey,
Some(owner_hex),
) {
record.auth_tag = refreshed_auth_tag;
record.updated_at = crate::util::now_iso();
}
// Re-snapshot the persona onto the record at every spawn so the agent always
// starts with the current persona config (system_prompt, model, provider,
// env_vars). This clears the "out of date" drift badge without requiring a
@@ -655,6 +668,7 @@ pub async fn create_managed_agent(
requested_persona_id.as_deref(),
&personas,
agent_command_override.as_deref(),
None,
);
let agent_args = normalize_agent_args(
&agent_command,
@@ -976,6 +990,10 @@ pub async fn start_managed_agent(
// Snapshot the workspace owner pubkey for the legacy auth_tag fallback.
// Read outside the records lock to keep lock ordering simple.
let owner_hex = workspace_owner_hex(&state)?;
let refreshed_auth_tag = {
let owner_keys = state.keys.lock().map_err(|error| error.to_string())?;
crate::managed_agents::managed_agent_auth_tag_for_owner(&owner_keys, &pubkey)?
};
enum StartTarget {
Local,
Provider {
@@ -986,8 +1004,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()
@@ -1000,14 +1017,21 @@ pub async fn start_managed_agent(
let (sync_changed, exited_pubkeys) =
sync_managed_agent_processes(&mut records, &mut runtimes, &current_instance_id(&app));
if sync_changed {
save_managed_agents(&app, &records)?;
}
let mut records_changed = sync_changed;
for pubkey in &exited_pubkeys {
state.clear_session_cache(pubkey);
}
let record = find_managed_agent_mut(&mut records, &pubkey)?;
if !crate::managed_agents::auth_tag_matches_owner(
record.auth_tag.as_deref(),
&record.pubkey,
Some(&owner_hex),
) {
record.auth_tag = refreshed_auth_tag;
record.updated_at = crate::util::now_iso();
records_changed = true;
}
// Resolve the effective harness for the avatar-fallback derivation in
// profile reconcile (the create-time snapshot may be empty or stale for
@@ -1017,6 +1041,7 @@ pub async fn start_managed_agent(
record.persona_id.as_deref(),
&reconcile_personas,
record.agent_command_override.as_deref(),
Some(&record.agent_command),
);
let reconcile = ProfileReconcileData {
@@ -1040,6 +1065,10 @@ pub async fn start_managed_agent(
}
};
if records_changed {
save_managed_agents(&app, &records)?;
}
(target, reconcile)
};
@@ -1088,9 +1117,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 {
+7 -1
View File
@@ -283,6 +283,7 @@ pub async fn send_channel_message(
media_tags: Option<Vec<Vec<String>>>,
emoji_tags: Option<Vec<Vec<String>>>,
mention_tags: Option<Vec<Vec<String>>>,
client_tags: Option<Vec<Vec<String>>>,
mention_pubkeys: Option<Vec<String>>,
kind: Option<u32>,
state: State<'_, AppState>,
@@ -294,7 +295,11 @@ pub async fn send_channel_message(
let media = media_tags.unwrap_or_default();
let emoji = emoji_tags.unwrap_or_default();
let mention_refs_only = mention_tags.unwrap_or_default();
let client = client_tags.unwrap_or_default();
let kind_num = kind.unwrap_or(buzz_core_pkg::kind::KIND_STREAM_MESSAGE);
if kind_num != buzz_core_pkg::kind::KIND_STREAM_MESSAGE && !client.is_empty() {
return Err("client tags are only supported on stream messages".into());
}
let mut resolved_root: Option<String> = None;
@@ -330,7 +335,7 @@ pub async fn send_channel_message(
}
None => None,
};
events::build_message(
events::build_message_with_client_tags(
channel_uuid,
content.trim(),
thread_ref.as_ref(),
@@ -338,6 +343,7 @@ pub async fn send_channel_message(
&media,
&emoji,
&mention_refs_only,
&client,
)?
}
};
+1
View File
@@ -1,6 +1,7 @@
mod agent_config;
mod agent_discovery;
mod agent_models;
mod agent_profile_reconcile;
mod agent_settings;
mod agents;
mod canvas;
@@ -279,11 +279,14 @@ pub fn default_agent_command() -> String {
/// Resolution order:
/// 1. explicit override (non-empty) — a deliberate per-instance pin;
/// 2. the linked persona's `runtime` id mapped to its primary command;
/// 3. `default_agent_command()` — no persona/runtime, or persona deleted.
/// 3. the record's stored `agent_command` snapshot for legacy records whose
/// persona has no runtime field;
/// 4. `default_agent_command()` — no persona/runtime/snapshot, or persona deleted.
pub fn effective_agent_command(
persona_id: Option<&str>,
personas: &[crate::managed_agents::types::PersonaRecord],
agent_command_override: Option<&str>,
record_agent_command: Option<&str>,
) -> String {
if let Some(pin) = agent_command_override
.map(str::trim)
@@ -298,6 +301,12 @@ pub fn effective_agent_command(
.and_then(known_acp_runtime_exact)
.and_then(|r| r.commands.first().copied())
.map(str::to_string)
.or_else(|| {
record_agent_command
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
})
.unwrap_or_else(default_agent_command)
}
@@ -320,7 +329,7 @@ pub fn divergent_agent_command_override(
let picked = picked_command
.map(str::trim)
.filter(|value| !value.is_empty())?;
let persona_command = effective_agent_command(persona_id, personas, None);
let persona_command = effective_agent_command(persona_id, personas, None, None);
let same_runtime = match (
known_acp_runtime(picked),
known_acp_runtime(&persona_command),
@@ -373,7 +382,7 @@ pub fn create_time_agent_command_override(
let picked = picked_command
.map(str::trim)
.filter(|value| !value.is_empty())?;
let inherited_command = effective_agent_command(persona_id, personas, None);
let inherited_command = effective_agent_command(persona_id, personas, None, None);
return (picked != inherited_command).then(|| picked.to_string());
}
@@ -927,7 +936,7 @@ mod tests {
// An explicit pin beats the persona's runtime.
let personas = vec![persona_with_runtime("p1", Some("claude"))];
assert_eq!(
effective_agent_command(Some("p1"), &personas, Some("codex-acp")),
effective_agent_command(Some("p1"), &personas, Some("codex-acp"), Some("goose")),
"codex-acp"
);
}
@@ -937,7 +946,7 @@ mod tests {
// No override → persona runtime id maps to its primary command.
let personas = vec![persona_with_runtime("p1", Some("claude"))];
assert_eq!(
effective_agent_command(Some("p1"), &personas, None),
effective_agent_command(Some("p1"), &personas, None, Some("goose")),
"claude-agent-acp"
);
}
@@ -947,26 +956,37 @@ mod tests {
// A blank/whitespace override is treated as "inherit", not a pin.
let personas = vec![persona_with_runtime("p1", Some("goose"))];
assert_eq!(
effective_agent_command(Some("p1"), &personas, Some(" ")),
effective_agent_command(Some("p1"), &personas, Some(" "), Some("buzz-agent")),
"goose"
);
}
#[test]
fn effective_agent_command_falls_back_to_record_snapshot() {
// Legacy records may predate persona runtime fields. Preserve their
// stored harness instead of silently changing them to the new default.
let personas = vec![persona_with_runtime("p1", None)];
assert_eq!(
effective_agent_command(Some("p1"), &personas, None, Some("goose")),
"goose"
);
}
#[test]
fn effective_agent_command_falls_back_to_default() {
// No override, no persona runtime, and a deleted persona all fall back
// to the bundled default.
// No override, no persona runtime, no record snapshot, and a deleted
// persona all fall back to the bundled default.
let personas = vec![persona_with_runtime("p1", None)];
assert_eq!(
effective_agent_command(Some("p1"), &personas, None),
effective_agent_command(Some("p1"), &personas, None, None),
default_agent_command()
);
assert_eq!(
effective_agent_command(Some("gone"), &personas, None),
effective_agent_command(Some("gone"), &personas, None, None),
default_agent_command()
);
assert_eq!(
effective_agent_command(None, &personas, None),
effective_agent_command(None, &personas, None, None),
default_agent_command()
);
}
@@ -197,15 +197,46 @@ pub async fn restore_managed_agents_on_launch(
return Ok(());
}
// Snapshot the workspace owner pubkey once for the legacy auth_tag fallback.
// Read outside the per-agent spawn loop so all parallel spawns see the same
// value and we don't lock `state.keys` repeatedly.
let owner_hex: Option<String> = state
.keys
.lock()
.map_err(|e| e.to_string())
.ok()
.map(|k| k.public_key().to_hex());
// Snapshot the workspace owner once and refresh each restored agent's
// NIP-OA auth tag when the app identity changed since the record was
// created. Without this, owner-only agents can start successfully but ignore
// the current user's messages because the harness resolves an old owner.
let (owner_hex, auth_tag_updates): (Option<String>, Vec<(String, Option<String>, String)>) = {
let owner_keys = state.keys.lock().map_err(|e| e.to_string())?;
let owner_hex = owner_keys.public_key().to_hex();
let mut updates = Vec::new();
for record in &mut agents_to_start {
if super::auth_tag_matches_owner(
record.auth_tag.as_deref(),
&record.pubkey,
Some(&owner_hex),
) {
continue;
}
let refreshed_auth_tag =
super::managed_agent_auth_tag_for_owner(&owner_keys, &record.pubkey)?;
let updated_at = util::now_iso();
record.auth_tag = refreshed_auth_tag.clone();
record.updated_at = updated_at.clone();
updates.push((record.pubkey.clone(), refreshed_auth_tag, updated_at));
}
(Some(owner_hex), updates)
};
if !auth_tag_updates.is_empty() {
let _store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|error| error.to_string())?;
let mut records = load_managed_agents(app)?;
for (pubkey, auth_tag, updated_at) in &auth_tag_updates {
if let Ok(record) = find_managed_agent_mut(&mut records, pubkey) {
record.auth_tag = auth_tag.clone();
record.updated_at = updated_at.clone();
}
}
save_managed_agents(app, &records)?;
}
#[cfg(feature = "mesh-llm")]
let agents_to_start = {
@@ -310,6 +341,7 @@ pub async fn restore_managed_agents_on_launch(
record.persona_id.as_deref(),
&reconcile_personas,
record.agent_command_override.as_deref(),
Some(&record.agent_command),
);
Some((
pubkey.clone(),
@@ -1517,6 +1517,7 @@ pub fn build_managed_agent_summary(
record.persona_id.as_deref(),
personas,
record.agent_command_override.as_deref(),
Some(&record.agent_command),
);
let effective_args = normalize_agent_args(&effective_command, record.agent_args.clone());
let effective_mcp_command = known_acp_runtime(&effective_command)
@@ -1611,12 +1612,10 @@ pub(crate) fn build_respond_to_env(
remove.push("BUZZ_ACP_RESPOND_TO_ALLOWLIST");
}
// Legacy fallback: agents created before NIP-OA lack `auth_tag`. Without
// it the harness can't resolve the owner, and owner-dependent gate modes
// would drop every event. Forwarding the workspace owner pubkey via
// BUZZ_ACP_AGENT_OWNER keeps those records functional. Modern records
// (`auth_tag = Some(...)`) use `BUZZ_AUTH_TAG` as before.
if record.auth_tag.is_none() {
// Fallback: if the record has no usable NIP-OA `auth_tag` for the current
// workspace owner, forward the owner pubkey explicitly. This covers both
// pre-NIP-OA records and records restored after the local identity changed.
if !auth_tag_matches_owner(record.auth_tag.as_deref(), &record.pubkey, owner_hex) {
if let Some(owner) = owner_hex {
set.push(("BUZZ_ACP_AGENT_OWNER", owner.to_string()));
} else {
@@ -1629,6 +1628,53 @@ pub(crate) fn build_respond_to_env(
Ok((set, remove))
}
pub(crate) fn auth_tag_owner_hex(auth_tag: &str, agent_pubkey_hex: &str) -> Option<String> {
let trimmed = auth_tag.trim();
if trimmed.is_empty() {
return None;
}
let agent_pubkey = nostr::PublicKey::from_hex(agent_pubkey_hex).ok()?;
buzz_sdk_pkg::nip_oa::verify_auth_tag(trimmed, &agent_pubkey)
.ok()
.map(|owner| owner.to_hex().to_ascii_lowercase())
}
pub(crate) fn auth_tag_matches_owner(
auth_tag: Option<&str>,
agent_pubkey_hex: &str,
owner_hex: Option<&str>,
) -> bool {
let Some(tag) = auth_tag.map(str::trim).filter(|tag| !tag.is_empty()) else {
return false;
};
match owner_hex {
Some(owner) => auth_tag_owner_hex(tag, agent_pubkey_hex)
.is_some_and(|tag_owner| tag_owner.eq_ignore_ascii_case(owner)),
None => true,
}
}
pub(crate) fn managed_agent_auth_tag_for_owner(
owner_keys: &nostr::Keys,
agent_pubkey_hex: &str,
) -> Result<Option<String>, String> {
if owner_keys
.public_key()
.to_hex()
.eq_ignore_ascii_case(agent_pubkey_hex)
{
return Ok(None);
}
let compat_owner = nostr::Keys::parse(&owner_keys.secret_key().to_secret_hex())
.map_err(|error| format!("failed to bridge owner keys: {error}"))?;
let compat_agent = nostr::PublicKey::from_hex(agent_pubkey_hex)
.map_err(|error| format!("failed to bridge agent pubkey: {error}"))?;
buzz_sdk_pkg::nip_oa::compute_auth_tag(&compat_owner, &compat_agent, "")
.map(Some)
.map_err(|error| format!("failed to compute NIP-OA auth tag: {error}"))
}
/// Spawn an agent process without holding any locks on records or runtimes.
/// Returns the child process and log path on success. The caller is responsible
/// for updating `ManagedAgentRecord` fields and inserting into the runtimes map.
@@ -1668,6 +1714,7 @@ pub fn spawn_agent_child(
record.persona_id.as_deref(),
&personas,
record.agent_command_override.as_deref(),
Some(&record.agent_command),
);
let agent_args = normalize_agent_args(&effective_command, record.agent_args.clone());
let resolved_acp_command = resolve_command(&record.acp_command)
@@ -1822,7 +1869,13 @@ pub fn spawn_agent_child(
command.env_remove("BUZZ_ACP_API_TOKEN");
command.env_remove("BUZZ_API_TOKEN");
if let Some(ref auth_tag) = record.auth_tag {
let auth_tag_for_child = record
.auth_tag
.as_deref()
.map(str::trim)
.filter(|tag| !tag.is_empty())
.filter(|tag| auth_tag_matches_owner(Some(tag), &record.pubkey, owner_hex));
if let Some(auth_tag) = auth_tag_for_child {
command.env("BUZZ_AUTH_TAG", auth_tag);
} else {
command.env_remove("BUZZ_AUTH_TAG");
@@ -159,8 +159,16 @@ fn fixture(
#[test]
fn build_env_owner_only_sets_mode_and_removes_others() {
let rec = fixture(RespondTo::OwnerOnly, vec![], Some("tag".into()));
let (set, remove) = build_respond_to_env(&rec, Some("owner")).unwrap();
let owner = nostr::Keys::generate();
let agent = nostr::Keys::generate();
let agent_pubkey = agent.public_key().to_hex();
let auth_tag = super::managed_agent_auth_tag_for_owner(&owner, &agent_pubkey)
.unwrap()
.unwrap();
let mut rec = fixture(RespondTo::OwnerOnly, vec![], Some(auth_tag));
rec.pubkey = agent_pubkey;
let owner_hex = owner.public_key().to_hex();
let (set, remove) = build_respond_to_env(&rec, Some(&owner_hex)).unwrap();
let set_map: std::collections::HashMap<_, _> = set.into_iter().collect();
assert_eq!(
set_map.get("BUZZ_ACP_RESPOND_TO").map(String::as_str),
@@ -172,6 +180,27 @@ fn build_env_owner_only_sets_mode_and_removes_others() {
assert!(remove.contains(&"BUZZ_ACP_AGENT_OWNER"));
}
#[test]
fn build_env_stale_auth_tag_emits_current_owner() {
let previous_owner = nostr::Keys::generate();
let current_owner = nostr::Keys::generate();
let agent = nostr::Keys::generate();
let agent_pubkey = agent.public_key().to_hex();
let stale_auth_tag = super::managed_agent_auth_tag_for_owner(&previous_owner, &agent_pubkey)
.unwrap()
.unwrap();
let mut rec = fixture(RespondTo::OwnerOnly, vec![], Some(stale_auth_tag));
rec.pubkey = agent_pubkey;
let current_owner_hex = current_owner.public_key().to_hex();
let (set, remove) = build_respond_to_env(&rec, Some(&current_owner_hex)).unwrap();
let set_map: std::collections::HashMap<_, _> = set.into_iter().collect();
assert_eq!(
set_map.get("BUZZ_ACP_AGENT_OWNER").map(String::as_str),
Some(current_owner_hex.as_str())
);
assert!(!remove.contains(&"BUZZ_ACP_AGENT_OWNER"));
}
#[test]
fn build_env_allowlist_sets_both_envs_and_joins() {
let a = "a".repeat(64);
+2
View File
@@ -717,6 +717,7 @@ export async function sendChannelMessage(
kind?: number,
emojiTags?: string[][],
mentionTags?: string[][],
clientTags?: string[][],
): Promise<SendChannelMessageResult> {
const response = await invokeTauri<RawSendChannelMessageResult>(
"send_channel_message",
@@ -727,6 +728,7 @@ export async function sendChannelMessage(
mediaTags: mediaTags ?? null,
emojiTags: emojiTags ?? null,
mentionTags: mentionTags ?? null,
clientTags: clientTags ?? null,
mentionPubkeys: mentionPubkeys ?? null,
kind: kind ?? null,
},
+12
View File
@@ -6,7 +6,10 @@ export const KIND_STREAM_MESSAGE = 9;
export const KIND_NIP29_DELETE_EVENT = 9005;
export const KIND_STREAM_MESSAGE_V2 = 40002;
export const KIND_STREAM_MESSAGE_EDIT = 40003;
export const KIND_STREAM_MESSAGE_PINNED = 40004;
export const KIND_STREAM_MESSAGE_DIFF = 40008;
export const KIND_AGENT_CONVERSATION = 40010;
export const KIND_AGENT_CONVERSATION_COMPAT = KIND_STREAM_MESSAGE_PINNED;
export const KIND_REMINDER = 40007;
export const KIND_SYSTEM_MESSAGE = 40099;
export const KIND_JOB_REQUEST = 43001;
@@ -71,7 +74,9 @@ export const CHANNEL_EVENT_KINDS = [
...CHANNEL_MESSAGE_EVENT_KINDS,
40001, // legacy: pre-migration stream messages
KIND_STREAM_MESSAGE_EDIT, // 40003 — message edits
KIND_AGENT_CONVERSATION_COMPAT, // 40004 — staging-compatible focused agent conversation marker
KIND_STREAM_MESSAGE_DIFF, // 40008 — message diffs
KIND_AGENT_CONVERSATION, // 40010 — focused agent conversation marker
KIND_SYSTEM_MESSAGE, // 40099 — system messages (join, leave, etc.)
KIND_HUDDLE_STARTED, // 48100 — visible huddle session card
KIND_HUDDLE_PARTICIPANT_JOINED, // 48101 — huddle lifecycle overlay
@@ -79,6 +84,13 @@ export const CHANNEL_EVENT_KINDS = [
KIND_HUDDLE_ENDED, // 48103 — huddle lifecycle overlay
] as const;
// Stored channel-scoped state that should be fetched with timeline history but
// should not render as a message row or count against unread message tallies.
export const CHANNEL_TIMELINE_STATE_KINDS = [
KIND_AGENT_CONVERSATION_COMPAT, // 40004 — staging-compatible focused agent conversation marker
KIND_AGENT_CONVERSATION, // 40010 — focused agent conversation marker
] as const;
// Auxiliary (non-row) timeline kinds: events that overlay onto or hide an
// existing message rather than rendering their own row — reactions, edits, and
// deletions. History fetches request the visible content kinds only, so the