revert(accumulator): drop desktop Settings tab + Tauri exec seam

The standalone daemon (buzz-fold) plus a future standalone UI supersede
the in-app control surface; the branch no longer touches desktop/.
Also drops the 'accumulator' preview flag entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>
This commit is contained in:
Riley Crane
2026-08-31 15:43:57 -07:00
committed by Sol
parent 6c8b1c865a
commit 15dfe9f3e8
10 changed files with 1 additions and 570 deletions

View File

@@ -1,236 +0,0 @@
//! Tauri command backing the Accumulator settings panel.
//!
//! The panel is a thin control plane over the bundled `buzz` CLI: every
//! action runs `buzz folds <verb> …` with the desktop identity's key and the
//! active workspace relay, and returns the CLI's output verbatim for the UI
//! to render. All fold state lives on the relay as the user's own signed
//! events — this command holds none.
use std::io::Read;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use nostr::ToBech32;
use serde::Serialize;
use tauri::State;
use crate::app_state::AppState;
use crate::managed_agents::resolve_command;
/// Verbs the panel may pass through. Everything runs under the `folds`
/// group (prepended here, never caller-supplied), so the desktop identity's
/// key is only ever handed to the accumulator control plane.
const ALLOWED_VERBS: &[&str] = &[
"set", "list", "get", "delete", "estimate", "run", "artifact", "share",
];
const MAX_ARGS: usize = 64;
const MAX_ARG_LEN: usize = 16 * 1024;
/// `folds run` invokes a model subprocess (the fold runner's own timeout is
/// 600s, plus relay reads and the publish); every other verb is relay I/O
/// only. Both leave generous headroom over the inner timeouts so the CLI's
/// own error reporting wins the race against this outer kill.
const RUN_TIMEOUT: Duration = Duration::from_secs(900);
const QUERY_TIMEOUT: Duration = Duration::from_secs(120);
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FoldsCliOutput {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
fn validate_args(args: &[String]) -> Result<(), String> {
let Some(verb) = args.first() else {
return Err("folds command requires a verb".to_string());
};
if !ALLOWED_VERBS.contains(&verb.as_str()) {
return Err(format!("unsupported folds verb: {verb}"));
}
if args.len() > MAX_ARGS {
return Err(format!("too many arguments ({} > {MAX_ARGS})", args.len()));
}
for arg in args {
if arg.len() > MAX_ARG_LEN {
return Err(format!(
"argument too long ({} > {MAX_ARG_LEN} bytes)",
arg.len()
));
}
if arg.contains('\0') {
return Err("argument contains a NUL byte".to_string());
}
}
Ok(())
}
/// Resolve the bundled `buzz` CLI: exe-sibling sidecar first (DMG install),
/// then the general command resolver (dev builds, PATH installs).
fn resolve_buzz_cli() -> Result<std::path::PathBuf, String> {
std::env::current_exe()
.map(|path| path.with_file_name(format!("buzz{}", std::env::consts::EXE_SUFFIX)))
.ok()
.filter(|path| path.exists())
.or_else(|| resolve_command("buzz"))
.ok_or_else(|| "buzz CLI not found".to_string())
}
/// Kill the child — on unix the entire process group it leads, so the model
/// subprocess a `folds run` spawned dies with it.
fn kill_group(child: &mut std::process::Child) {
#[cfg(unix)]
// SAFETY: plain syscall; a negative pid signals the process group.
unsafe {
libc::kill(-(child.id() as i32), libc::SIGKILL);
}
let _ = child.kill();
let _ = child.wait();
}
fn read_pipe_lossy(pipe: Option<impl Read>) -> String {
let Some(mut pipe) = pipe else {
return String::new();
};
let mut bytes = Vec::new();
let _ = pipe.read_to_end(&mut bytes);
String::from_utf8_lossy(&bytes).to_string()
}
fn run_folds_cli_blocking(
args: Vec<String>,
nsec: String,
relay_url: String,
) -> Result<FoldsCliOutput, String> {
validate_args(&args)?;
let cli_path = resolve_buzz_cli()?;
let timeout = if args[0] == "run" {
RUN_TIMEOUT
} else {
QUERY_TIMEOUT
};
let mut command = Command::new(&cli_path);
command.arg("folds").args(&args);
command.env("BUZZ_PRIVATE_KEY", &nsec);
command.env("BUZZ_RELAY_URL", &relay_url);
// Scrub ambient credentials the desktop process may carry: an inherited
// BUZZ_AUTH_TAG would be silently injected into every event this CLI
// signs (folds are personal, never membership-delegated).
command.env_remove("BUZZ_AUTH_TAG");
command.env_remove("BUZZ_API_TOKEN");
command.env_remove("NOSTR_PRIVATE_KEY");
// The fold runner shells out to the user's model CLI (`claude` by
// default); the augmented PATH makes that resolvable from a DMG launch.
if let Some(path) = crate::managed_agents::readiness::cli_probe::augmented_path() {
command.env("PATH", path);
}
if let Some(home) = dirs::home_dir() {
command.current_dir(home);
}
command.stdin(Stdio::null());
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
// Lead a fresh process group so a timeout kill reaches the model CLI the
// fold runner spawned, not just the `buzz` wrapper.
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
command.process_group(0);
}
crate::util::configure_no_window(&mut command);
let mut child = command
.spawn()
.map_err(|error| format!("failed to run buzz folds: {error}"))?;
// Drain the pipes on background threads so a chatty run can't deadlock
// on a full pipe while we poll for exit below.
let stdout_pipe = child.stdout.take();
let stderr_pipe = child.stderr.take();
let stdout_thread = std::thread::spawn(move || read_pipe_lossy(stdout_pipe));
let stderr_thread = std::thread::spawn(move || read_pipe_lossy(stderr_pipe));
let started = Instant::now();
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break status,
Ok(None) => {
if started.elapsed() > timeout {
kill_group(&mut child);
let _ = stdout_thread.join();
let _ = stderr_thread.join();
return Err(format!("buzz folds timed out after {}s", timeout.as_secs()));
}
std::thread::sleep(Duration::from_millis(50));
}
Err(error) => {
kill_group(&mut child);
return Err(format!("failed to wait for buzz folds: {error}"));
}
}
};
Ok(FoldsCliOutput {
stdout: stdout_thread.join().unwrap_or_default(),
stderr: stderr_thread.join().unwrap_or_default(),
exit_code: status.code().unwrap_or(-1),
})
}
/// Run `buzz folds <args…>` as the desktop identity against the active
/// workspace relay. Nonzero exits are returned as data (the CLI's stderr
/// carries the actionable message); `Err` is reserved for launch failures,
/// timeouts, and validation.
#[tauri::command]
pub async fn run_folds_cli(
args: Vec<String>,
state: State<'_, AppState>,
) -> Result<FoldsCliOutput, String> {
validate_args(&args)?;
let nsec = state
.signing_keys()?
.secret_key()
.to_bech32()
.map_err(|error| format!("failed to encode signing key: {error}"))?;
let relay_url = crate::relay::relay_ws_url_with_override(&state);
tokio::task::spawn_blocking(move || run_folds_cli_blocking(args, nsec, relay_url))
.await
.map_err(|error| format!("folds task failed: {error}"))?
}
#[cfg(test)]
mod tests {
use super::*;
fn args(list: &[&str]) -> Vec<String> {
list.iter().map(|s| s.to_string()).collect()
}
#[test]
fn rejects_empty_and_unknown_verbs() {
assert!(validate_args(&[]).is_err());
assert!(validate_args(&args(&["frobnicate"])).is_err());
// Group escape attempts are just unknown verbs.
assert!(validate_args(&args(&["--help"])).is_err());
assert!(validate_args(&args(&["mem"])).is_err());
}
#[test]
fn accepts_every_folds_verb() {
for verb in ALLOWED_VERBS {
assert!(validate_args(&args(&[verb])).is_ok(), "verb {verb}");
}
assert!(validate_args(&args(&["estimate", "my-fold", "--limit", "50"])).is_ok());
}
#[test]
fn rejects_oversized_and_nul_arguments() {
assert!(validate_args(&args(&["run", &"x".repeat(MAX_ARG_LEN + 1)])).is_err());
assert!(validate_args(&args(&["get", "bad\0name"])).is_err());
let mut many = vec!["list".to_string()];
many.extend(std::iter::repeat_n("x".to_string(), MAX_ARGS));
assert!(validate_args(&many).is_err());
}
}

View File

@@ -21,7 +21,6 @@ mod clipboard;
mod dms;
mod engrams;
mod export_util;
mod folds_exec;
mod global_agent_config;
mod identity;
mod identity_archive;
@@ -95,7 +94,6 @@ pub use channels::*;
pub use clipboard::*;
pub use dms::*;
pub use engrams::*;
pub use folds_exec::*;
pub use global_agent_config::*;
pub use identity::*;
pub use identity_archive::*;

View File

@@ -839,7 +839,6 @@ pub fn run() {
fetch_join_policy,
set_prevent_sleep_active,
get_agent_memory,
run_folds_cli,
relay_reconnect_hook,
relay_reconnect_hook_configured,
observer_archive_default_enabled,

View File

@@ -1,261 +0,0 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { RefreshCw } from "lucide-react";
import * as React from "react";
import { runFoldsCli, runFoldsCliJson } from "@/shared/api/tauriFolds";
import { Button } from "@/shared/ui/button";
import {
SettingsOptionGroup,
SettingsOptionGroupList,
SettingsOptionRow,
} from "@/features/settings/ui/SettingsOptionGroup";
import { SettingsSectionHeader } from "@/features/settings/ui/SettingsSectionHeader";
// JSON shapes printed by the `buzz folds` CLI (crates/buzz-cli/commands/folds.rs).
type FoldSummary = {
fold: string;
selection: string;
schema: string;
model: string;
latest_version: number | null;
covered_signals: number;
coverage_until: number | null;
};
type FoldPlan = {
fold: string;
action: "cached" | "stalled" | "ready" | "run";
note?: string;
reason?: string;
version?: number;
pending_signals?: number;
would_show?: number;
truncated_to_budget?: boolean;
est_input_tokens?: number;
est_cost_usd?: number | null;
next_version?: number;
shown_signals?: number;
coverage_until?: number | null;
// Ready plans carry the exact half-open window the estimate priced, so Run
// can replay the same fetch instead of racing new arrivals.
since?: number;
until_exclusive?: number;
};
const foldsListQueryKey = ["accumulator", "folds"] as const;
function formatTimestamp(seconds: number | null | undefined): string {
if (!seconds) return "never";
return new Date(seconds * 1000).toLocaleString();
}
function formatCost(costUsd: number | null | undefined): string {
if (costUsd === null || costUsd === undefined) return "unknown cost";
return `~$${costUsd.toFixed(4)}`;
}
function planSummary(plan: FoldPlan): string {
switch (plan.action) {
case "cached":
return `Up to date — v${plan.version ?? "?"} already covers everything new.`;
case "stalled":
return `Stalled: ${plan.reason ?? "cannot fit the pending events"} (${plan.pending_signals ?? 0} pending).`;
case "ready":
return `${plan.pending_signals ?? 0} new signal(s); would fold ${plan.would_show ?? 0} into v${plan.next_version ?? "?"} for ${formatCost(plan.est_cost_usd)} (${plan.est_input_tokens ?? "?"} input tokens${plan.truncated_to_budget ? ", chunked to fit the context budget" : ""}).`;
case "run":
return `Folded ${plan.shown_signals ?? 0} signal(s) into v${plan.version ?? "?"} (coverage through ${formatTimestamp(plan.coverage_until)}).`;
}
}
function FoldRow({ fold }: { fold: FoldSummary }) {
const queryClient = useQueryClient();
const [plan, setPlan] = React.useState<FoldPlan | null>(null);
const [error, setError] = React.useState<string | null>(null);
const [showArtifact, setShowArtifact] = React.useState(false);
const artifactQuery = useQuery({
queryKey: ["accumulator", "artifact", fold.fold],
queryFn: () => runFoldsCli(["artifact", fold.fold, "--raw"]),
enabled: showArtifact,
});
const estimateMutation = useMutation({
mutationFn: () => runFoldsCliJson<FoldPlan>(["estimate", fold.fold]),
onSuccess: (result) => {
setPlan(result);
setError(null);
},
onError: (mutationError: Error) => setError(mutationError.message),
});
const runMutation = useMutation({
// Replay exactly the window the preflight priced — arrivals since then
// stay pending for the next preflight instead of silently joining a run
// priced without them.
mutationFn: (priced: FoldPlan) =>
runFoldsCliJson<FoldPlan>([
"run",
fold.fold,
...(priced.since !== undefined
? ["--since", String(priced.since)]
: []),
...(priced.until_exclusive !== undefined
? ["--until", String(priced.until_exclusive)]
: []),
]),
onSuccess: async (result) => {
setPlan(result);
setError(null);
await Promise.all([
queryClient.invalidateQueries({ queryKey: foldsListQueryKey }),
queryClient.invalidateQueries({
queryKey: ["accumulator", "artifact", fold.fold],
}),
]);
},
onError: (mutationError: Error) => {
// A failed run invalidates the priced plan: Run re-locks until the
// next preflight rather than offering a stale "ready".
setPlan(null);
setError(mutationError.message);
},
});
const busy = estimateMutation.isPending || runMutation.isPending;
// Priced-before-spend: Run only unlocks after a preflight priced this
// exact pending set. Anything that changes the set re-requires preflight.
const runReady = plan?.action === "ready" && !busy;
return (
<SettingsOptionRow data-testid={`accumulator-fold-${fold.fold}`}>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">{fold.fold}</p>
<p
className="text-sm font-normal text-muted-foreground/70"
data-settings-subcopy
>
{`${fold.selection} · ${fold.schema} · ${fold.model} · ${
fold.latest_version
? `v${fold.latest_version}, covered through ${formatTimestamp(fold.coverage_until)}`
: "never run"
}`}
</p>
{plan ? (
<p className="mt-1 text-sm text-muted-foreground">
{planSummary(plan)}
</p>
) : null}
{error ? (
<p className="mt-1 text-sm text-destructive">{error}</p>
) : null}
{showArtifact ? (
<pre className="mt-2 max-h-80 overflow-auto whitespace-pre-wrap rounded-md border border-border/50 bg-muted/30 p-3 text-xs">
{artifactQuery.isPending
? "Loading artifact…"
: artifactQuery.isError
? String(artifactQuery.error)
: artifactQuery.data.exitCode === 0
? artifactQuery.data.stdout
: artifactQuery.data.stderr.trim() ||
"This fold has no artifact yet."}
</pre>
) : null}
</div>
<div className="flex shrink-0 items-center gap-2">
<Button
data-testid={`accumulator-artifact-${fold.fold}`}
onClick={() => setShowArtifact((visible) => !visible)}
size="sm"
variant="ghost"
>
{showArtifact ? "Hide digest" : "View digest"}
</Button>
<Button
data-testid={`accumulator-estimate-${fold.fold}`}
disabled={busy}
onClick={() => estimateMutation.mutate()}
size="sm"
variant="outline"
>
{estimateMutation.isPending ? "Pricing…" : "Preflight"}
</Button>
<Button
data-testid={`accumulator-run-${fold.fold}`}
disabled={!runReady}
onClick={() => plan && runMutation.mutate(plan)}
size="sm"
>
{runMutation.isPending
? "Folding…"
: plan?.action === "ready"
? `Run ${formatCost(plan.est_cost_usd)}`
: "Run"}
</Button>
</div>
</SettingsOptionRow>
);
}
export function AccumulatorSettingsCard() {
const foldsQuery = useQuery({
queryKey: foldsListQueryKey,
queryFn: () => runFoldsCliJson<FoldSummary[]>(["list"]),
});
return (
<section className="min-w-0" data-testid="settings-accumulator">
<SettingsSectionHeader
action={
<Button
data-testid="accumulator-refresh"
disabled={foldsQuery.isFetching}
onClick={() => void foldsQuery.refetch()}
size="sm"
variant="outline"
>
<RefreshCw
className={foldsQuery.isFetching ? "animate-spin" : undefined}
/>
Refresh
</Button>
}
title="Accumulator"
description="Standing digests over saved relay selections. Each fold keeps one always-current artifact; preflight prices a run exactly before anything is spent."
/>
<SettingsOptionGroupList>
<SettingsOptionGroup title="Folds">
{foldsQuery.isPending ? (
<SettingsOptionRow>
<p className="text-sm text-muted-foreground">Loading folds…</p>
</SettingsOptionRow>
) : foldsQuery.isError ? (
<SettingsOptionRow>
<p className="text-sm text-destructive">
{String(foldsQuery.error)}
</p>
</SettingsOptionRow>
) : foldsQuery.data.length === 0 ? (
<SettingsOptionRow data-testid="accumulator-empty">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">No folds yet</p>
<p
className="text-sm font-normal text-muted-foreground/70"
data-settings-subcopy
>
Create one from a terminal, then manage it here:
</p>
<pre className="mt-2 overflow-x-auto rounded-md border border-border/50 bg-muted/30 p-3 text-xs">
{"buzz folds set my-channel-digest --channel <channel-uuid>"}
</pre>
</div>
</SettingsOptionRow>
) : (
foldsQuery.data.map((fold) => (
<FoldRow fold={fold} key={fold.fold} />
))
)}
</SettingsOptionGroup>
</SettingsOptionGroupList>
</section>
);
}

View File

@@ -9,7 +9,6 @@ import {
Download,
FlaskConical,
Keyboard,
Layers,
LayoutTemplate,
MessagesSquare,
MonitorCog,
@@ -63,7 +62,6 @@ import {
ThreadLayoutSetting,
} from "./AppearanceSettingsControls";
import { ChannelTemplatesSettingsCard } from "./ChannelTemplatesSettingsCard";
import { AccumulatorSettingsCard } from "@/features/accumulator/ui/AccumulatorSettingsCard";
import { ExperimentalFeaturesCard } from "./ExperimentalFeaturesCard";
import { KeyboardShortcutsCard } from "./KeyboardShortcutsCard";
import { MeshComputeSettingsCard } from "@/features/mesh-compute/ui/MeshComputeSettingsCard";
@@ -85,7 +83,6 @@ import { VoiceSettingsCard } from "./VoiceSettingsCard";
export type SettingsSection =
| "profile"
| "accumulator"
| "notifications"
| "voice"
| "experimental"
@@ -106,7 +103,6 @@ export const DEFAULT_SETTINGS_SECTION: SettingsSection = "profile";
const SETTINGS_SECTION_VALUES: readonly SettingsSection[] = [
"profile",
"accumulator",
"notifications",
"voice",
"experimental",
@@ -192,12 +188,6 @@ export const settingsSections: SettingsSectionDescriptor[] = [
icon: LayoutTemplate,
featureGate: "channel-templates",
},
{
value: "accumulator",
label: "Accumulator",
icon: Layers,
featureGate: "accumulator",
},
{
value: "compute",
label: "Compute",
@@ -846,8 +836,6 @@ export function renderSettingsSection(
return <AgentsSettingsPanel />;
case "channel-templates":
return <ChannelTemplatesSettingsCard />;
case "accumulator":
return <AccumulatorSettingsCard />;
case "compute":
return <MeshComputeSettingsCard />;
case "appearance":

View File

@@ -63,7 +63,6 @@ const settingsNavGroups: Array<{
"custom-emoji",
"local-archive",
"channel-templates",
"accumulator",
],
},
{

View File

@@ -1,41 +0,0 @@
import { invokeTauri } from "@/shared/api/tauri";
/** Output of one `buzz folds` CLI invocation, verbatim. */
export type FoldsCliOutput = {
stdout: string;
stderr: string;
exitCode: number;
};
/**
* Run `buzz folds <args…>` (bundled CLI, desktop identity, active workspace
* relay). Nonzero exits resolve normally with the CLI's stderr message —
* rejection is reserved for launch failures, timeouts, and arg validation.
*/
export async function runFoldsCli(args: string[]): Promise<FoldsCliOutput> {
return invokeTauri<FoldsCliOutput>("run_folds_cli", { args });
}
/**
* Run a folds verb that prints JSON on success and parse it. Throws with the
* CLI's stderr (which carries the actionable message) on nonzero exit. A
* failed `run` may put a structured salvage report on stdout (the paid-for
* model output that was refused or could not be published); its reason is
* folded into the thrown message so the UI surfaces why.
*/
export async function runFoldsCliJson<T>(args: string[]): Promise<T> {
const result = await runFoldsCli(args);
if (result.exitCode !== 0) {
let detail = result.stderr.trim();
try {
const salvage = JSON.parse(result.stdout) as { reason?: string };
if (salvage.reason && !detail.includes(salvage.reason)) {
detail = detail ? `${detail} (${salvage.reason})` : salvage.reason;
}
} catch {
// stdout was not a salvage report; stderr already tells the story.
}
throw new Error(detail || `buzz folds exited with code ${result.exitCode}`);
}
return JSON.parse(result.stdout) as T;
}

View File

@@ -82,6 +82,7 @@ export const KIND_GIT_STATUS_DRAFT = 1633;
// NIP-DV: relay-signed per-viewer DM visibility snapshot (d=viewer pubkey,
// h-tags = currently-hidden DM channel ids).
export const KIND_DM_VISIBILITY = 30622;
// Human-visible "new content" message kinds. Used as the unread trigger set
// (sidebar badges, catch-up queries) and as the Home-feed mention query.
// Reactions, edits, diffs, deletions, and system messages are deliberately

View File

@@ -13476,16 +13476,6 @@ export function maybeInstallE2eTauriMocks() {
payload as { runtimeId?: string },
activeConfig,
);
case "run_folds_cli": {
// Accumulator panel: default to an empty fold list so the gated
// section renders its empty state without a real CLI spawn.
const args = (payload as { args?: string[] })?.args ?? [];
return {
stdout: args[0] === "list" ? "[]" : "{}",
stderr: "",
exitCode: 0,
};
}
case "discover_backend_providers":
return activeConfig?.mock?.backendProviders ?? [];
case "probe_backend_provider": {

View File

@@ -36,12 +36,6 @@
"name": "Agent-managed profiles",
"description": "Let agents manage their own relay name and avatar instead of restoring the desktop copy",
"platforms": ["desktop"]
},
{
"id": "accumulator",
"name": "Accumulator",
"description": "Standing fold digests over saved relay selections, run manually via buzz folds",
"platforms": ["desktop"]
}
]
}