diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs
index 3085493a307..cce1716dcc7 100644
--- a/desktop/src-tauri/build.rs
+++ b/desktop/src-tauri/build.rs
@@ -13,6 +13,7 @@ fn main() {
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_BUZZ_AGENT_MODEL");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AGENT_ENV");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_RELAY_RECONNECT_CMD");
+ println!("cargo:rerun-if-env-changed=BUZZ_BUILD_OBSERVER_ARCHIVE_DEFAULT");
println!("cargo:rustc-check-cfg=cfg(buzz_updater_enabled)");
if let Ok(relay_url) = std::env::var("BUZZ_RELAY_URL") {
@@ -72,6 +73,14 @@ fn main() {
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_RELAY_RECONNECT_CMD={val}");
}
+ // Presence-only flag: when set (any non-empty value), observer-feed archive
+ // defaults to ON for the current identity on first run. OSS builds leave
+ // this unset → default OFF. No JSON validation needed — the command only
+ // checks `.is_some()`.
+ if std::env::var("BUZZ_BUILD_OBSERVER_ARCHIVE_DEFAULT").is_ok() {
+ println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_OBSERVER_ARCHIVE_DEFAULT=1");
+ }
+
let updater_public_key = std::env::var("BUZZ_UPDATER_PUBLIC_KEY")
.ok()
.map(|value| value.trim().to_string())
diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs
index af30c3d8b91..92fd821ab51 100644
--- a/desktop/src-tauri/src/commands/mod.rs
+++ b/desktop/src-tauri/src/commands/mod.rs
@@ -33,6 +33,7 @@ mod project_repo_paths;
mod project_terminal;
mod relay_members;
mod relay_reconnect;
+mod observer_archive;
mod social;
mod teams;
mod updater;
@@ -70,6 +71,7 @@ pub use project_git_diff::*;
pub use project_terminal::*;
pub use relay_members::*;
pub use relay_reconnect::*;
+pub use observer_archive::*;
pub use social::*;
pub use teams::*;
pub use updater::*;
diff --git a/desktop/src-tauri/src/commands/observer_archive.rs b/desktop/src-tauri/src/commands/observer_archive.rs
new file mode 100644
index 00000000000..cdd932a0c69
--- /dev/null
+++ b/desktop/src-tauri/src/commands/observer_archive.rs
@@ -0,0 +1,36 @@
+//! Build-time flag for observer-feed archive default.
+//!
+//! When `BUZZ_BUILD_OBSERVER_ARCHIVE_DEFAULT` is set at build time (internal
+//! builds), `observer_archive_default_enabled()` returns `true` and the
+//! frontend auto-seeds an `owner_p` save subscription for the current identity
+//! on first run.
+//!
+//! OSS builds (env var unset) return `false` — no auto-seeding, user opts in
+//! manually via the Local Archive settings card.
+
+/// Returns `true` when an internal build has observer-feed archive default-on.
+///
+/// The frontend calls this once at startup to decide whether to seed the
+/// `owner_p` save subscription. The result is stable for the lifetime of the
+/// binary — it is baked at compile time.
+#[tauri::command]
+pub fn observer_archive_default_enabled() -> bool {
+ option_env!("BUZZ_DESKTOP_BUILD_OBSERVER_ARCHIVE_DEFAULT").is_some()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_observer_archive_default_enabled_returns_bool() {
+ // The command must return a plain bool without panicking.
+ // Whether it's true or false depends on the build environment;
+ // what we assert here is just that the return type is correct and
+ // the function is callable.
+ let result = observer_archive_default_enabled();
+ // In a standard OSS/test build (no BUZZ_DESKTOP_BUILD_OBSERVER_ARCHIVE_DEFAULT
+ // baked in), this should be false.
+ assert!(!result, "expected false in OSS/test build");
+ }
+}
diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs
index 673364703f5..7386dd7c569 100644
--- a/desktop/src-tauri/src/lib.rs
+++ b/desktop/src-tauri/src/lib.rs
@@ -622,6 +622,7 @@ pub fn run() {
get_agent_memory,
relay_reconnect_hook,
relay_reconnect_hook_configured,
+ observer_archive_default_enabled,
archive::archive_events,
archive::create_save_subscription,
archive::list_save_subscriptions,
diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx
index da9f6b65a38..712d0675836 100644
--- a/desktop/src/app/AppShell.tsx
+++ b/desktop/src/app/AppShell.tsx
@@ -50,6 +50,7 @@ import {
} from "@/features/user-status/hooks";
import { useWorkspaceEmojiLiveUpdates } from "@/features/custom-emoji/hooks";
import { useArchiveSync } from "@/features/local-archive/archiveSyncManager";
+import { useObserverArchiveSeed } from "@/features/local-archive/useObserverArchiveSeed";
import { useProfileQuery } from "@/features/profile/hooks";
import {
DEFAULT_SETTINGS_SECTION,
@@ -149,6 +150,7 @@ export function AppShell() {
usePersonaSync(identityQuery.data?.pubkey);
useAgentsDataRefresh();
useArchiveSync();
+ useObserverArchiveSeed(identityQuery.data?.pubkey);
const profileQuery = useProfileQuery();
const deferredPubkey = startupReady ? identityQuery.data?.pubkey : undefined;
useRelayAutoHeal();
diff --git a/desktop/src/features/local-archive/observerArchivePreference.ts b/desktop/src/features/local-archive/observerArchivePreference.ts
new file mode 100644
index 00000000000..824e44a0a7f
--- /dev/null
+++ b/desktop/src/features/local-archive/observerArchivePreference.ts
@@ -0,0 +1,71 @@
+/**
+ * Persists whether the user has made an explicit choice about the
+ * observer-feed archive default-on feature.
+ *
+ * The key is identity-scoped so toggling off on one identity doesn't suppress
+ * the default-on for another identity. The value is:
+ * "1" → user explicitly enabled (or accepted the default)
+ * "0" → user explicitly disabled
+ * null → no explicit choice yet (default-on seeding may still fire)
+ *
+ * Device-level localStorage — intentionally not reset on workspace switch
+ * (the archive subscription itself is identity-scoped in SQLite; this flag
+ * is just the UI gate that prevents re-seeding after an explicit opt-out).
+ */
+
+const KEY_PREFIX = "buzz:observer-archive-default-seeded";
+
+function storageKey(identityPubkey: string): string {
+ return `${KEY_PREFIX}:${identityPubkey}`;
+}
+
+/**
+ * Returns `true` if the user has already made an explicit choice for this
+ * identity (either opted in or opted out). When `false`, the seeding path
+ * may fire.
+ */
+export function hasExplicitObserverArchiceChoice(
+ identityPubkey: string,
+): boolean {
+ if (typeof window === "undefined") return true; // SSR/test: treat as set
+ try {
+ return window.localStorage.getItem(storageKey(identityPubkey)) !== null;
+ } catch {
+ return true; // storage error → treat as set, never auto-seed
+ }
+}
+
+/**
+ * Mark that the user has made an explicit choice for this identity.
+ * `enabled` should reflect whether the `owner_p` subscription exists after
+ * the action (true = seeded/enabled, false = opted out).
+ */
+export function setExplicitObserverArchiveChoice(
+ identityPubkey: string,
+ enabled: boolean,
+): void {
+ if (typeof window === "undefined") return;
+ try {
+ window.localStorage.setItem(
+ storageKey(identityPubkey),
+ enabled ? "1" : "0",
+ );
+ } catch {
+ // Best-effort — the seeding guard will re-fire on next startup if storage
+ // is unavailable, but that is safe (create_save_subscription is idempotent).
+ }
+}
+
+/**
+ * Clear the explicit choice for this identity (for testing / reset flows).
+ */
+export function clearExplicitObserverArchiveChoice(
+ identityPubkey: string,
+): void {
+ if (typeof window === "undefined") return;
+ try {
+ window.localStorage.removeItem(storageKey(identityPubkey));
+ } catch {
+ // ignore
+ }
+}
diff --git a/desktop/src/features/local-archive/ui/LocalArchiveSettingsCard.tsx b/desktop/src/features/local-archive/ui/LocalArchiveSettingsCard.tsx
index 3ee4e960182..192dec5fc07 100644
--- a/desktop/src/features/local-archive/ui/LocalArchiveSettingsCard.tsx
+++ b/desktop/src/features/local-archive/ui/LocalArchiveSettingsCard.tsx
@@ -14,11 +14,15 @@ import { useChannelsQuery } from "@/features/channels/hooks";
import { useIdentityQuery } from "@/shared/api/hooks";
import { Button } from "@/shared/ui/button";
import { Checkbox } from "@/shared/ui/checkbox";
+import { Switch } from "@/shared/ui/switch";
import {
SettingsOptionGroup,
SettingsOptionRow,
} from "@/features/settings/ui/SettingsOptionGroup";
import { SettingsSectionHeader } from "@/features/settings/ui/SettingsSectionHeader";
+import {
+ setExplicitObserverArchiveChoice,
+} from "../observerArchivePreference";
import {
buildSubscriptionRequest,
@@ -51,61 +55,51 @@ function kindSummary(kinds: number[]): string {
return `${kinds.slice(0, 3).join(", ")} +${kinds.length - 3} more`;
}
-// ── Source selector (Step 1) ──────────────────────────────────────────────────
-
-type AddSource = "channel_h" | "owner_p";
+// ── Observer-feed archive section ─────────────────────────────────────────────
-type SourceSelectorProps = {
- ownerPAlreadySubscribed: boolean;
- onSelect: (source: AddSource) => void;
+type ObserverSectionProps = {
+ enabled: boolean;
+ toggling: boolean;
+ onToggle: (checked: boolean) => void;
};
-function SourceSelector({
- ownerPAlreadySubscribed,
- onSelect,
-}: SourceSelectorProps) {
+function ObserverArchiveSection({
+ enabled,
+ toggling,
+ onToggle,
+}: ObserverSectionProps) {
return (
-
-
-
-
Channel
-
- Archive events from a channel you're a member of. Access is verified
- against the relay at subscribe and persist time.
-
-
-
-
-
-
-
My agents' observer feed
-
- Archive kind {KIND_AGENT_OBSERVER_FRAME} observer frames addressed
- to your pubkey. Routed by pubkey, not stored by the relay.
-
-
-
-
-
+
+
Agent observer feed
+
+
+
+
+
+ Saves kind {KIND_AGENT_OBSERVER_FRAME} observer frames addressed
+ to your pubkey. These are ephemeral — not stored by the relay —
+ so local archiving is the only way to retain them.
+