feat(mesh): mesh-AI assistant scheduler + config panel (#50)

Adds the assistant scheduler, MeshAssistantPanel UI, and the remaining
config-RPC / live-toggle / Ollama-detect wiring on top of Phase 1.x.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-17 19:19:32 -04:00
co-authored by Claude Opus 4.8
parent 0947ecee11
commit 7a76d32e4b
10 changed files with 667 additions and 28 deletions
+40
View File
@@ -14,6 +14,7 @@ pub mod message_types;
pub mod outbox;
pub mod protocol;
pub mod ratchet;
pub mod scheduler;
pub mod serial;
pub mod session;
pub mod steganography;
@@ -187,6 +188,15 @@ pub struct MeshConfig {
/// any peer on the mesh may ask (spends this node's compute + airtime).
#[serde(default = "default_true")]
pub assistant_trusted_only: bool,
/// Which AI backend answers queries: "claude" (the shared Claude proxy
/// token at secrets/claude-api-key — default for now, works without a
/// local GPU) or "ollama" (a local model on this node).
#[serde(default = "default_assistant_backend")]
pub assistant_backend: String,
}
fn default_assistant_backend() -> String {
"claude".to_string()
}
fn default_true() -> bool {
@@ -208,6 +218,7 @@ impl Default for MeshConfig {
assistant_enabled: false,
assistant_model: None,
assistant_trusted_only: true,
assistant_backend: default_assistant_backend(),
}
}
}
@@ -336,6 +347,7 @@ pub struct MeshService {
deadman_handle: Option<tokio::task::JoinHandle<()>>,
block_announcer_handle: Option<tokio::task::JoinHandle<()>>,
presence_handle: Option<tokio::task::JoinHandle<()>>,
scheduler_handle: Option<tokio::task::JoinHandle<()>>,
cmd_rx: Option<tokio::sync::mpsc::Receiver<listener::MeshCommand>>,
// Crypto identity for this node
our_did: String,
@@ -349,6 +361,8 @@ pub struct MeshService {
pub block_header_cache: Arc<BlockHeaderCache>,
pub relay_tracker: Arc<RelayTracker>,
pub dead_man_switch: Arc<DeadManSwitch>,
/// Scheduled / queued outbound mesh messages (issue #50, phase 1.7).
pub scheduler: Arc<scheduler::MeshScheduler>,
}
impl MeshService {
@@ -380,6 +394,7 @@ impl MeshService {
enabled: config.assistant_enabled,
model: config.assistant_model.clone(),
trusted_only: config.assistant_trusted_only,
backend: config.assistant_backend.clone(),
},
data_dir.to_path_buf(),
);
@@ -438,6 +453,7 @@ impl MeshService {
deadman_handle: None,
block_announcer_handle: None,
presence_handle: None,
scheduler_handle: None,
cmd_rx: Some(cmd_rx),
our_did: did.to_string(),
our_ed_pubkey_hex: ed_pubkey_hex.to_string(),
@@ -448,6 +464,7 @@ impl MeshService {
block_header_cache,
relay_tracker,
dead_man_switch,
scheduler: Arc::new(scheduler::MeshScheduler::load(data_dir).await),
})
}
@@ -525,6 +542,20 @@ impl MeshService {
});
self.deadman_handle = Some(dms_handle);
// Scheduled-message task (issue #50, phase 1.7): fires queued messages
// when due, retrying peer DMs until the peer is back in range.
let sched = Arc::clone(&self.scheduler);
let sched_state = Arc::clone(&self.state);
let sched_shutdown = self
.shutdown_tx
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Shutdown channel not initialized"))?
.subscribe();
let sched_handle = tokio::spawn(async move {
scheduler::run_scheduler(sched, sched_state, sched_shutdown).await;
});
self.scheduler_handle = Some(sched_handle);
// Spawn block header announcer (internet-connected nodes only)
if self.config.announce_block_headers {
let bha_state = Arc::clone(&self.state);
@@ -675,6 +706,10 @@ impl MeshService {
handle.abort();
let _ = handle.await;
}
if let Some(handle) = self.scheduler_handle.take() {
handle.abort();
let _ = handle.await;
}
if let Some(handle) = self.block_announcer_handle.take() {
handle.abort();
let _ = handle.await;
@@ -1359,6 +1394,7 @@ impl MeshService {
enabled: Option<bool>,
model: Option<Option<String>>,
trusted_only: Option<bool>,
backend: Option<String>,
) -> Result<()> {
{
let mut a = self.state.assistant.write().await;
@@ -1371,6 +1407,9 @@ impl MeshService {
if let Some(t) = trusted_only {
a.trusted_only = t;
}
if let Some(b) = backend {
a.backend = b;
}
}
// Persist by updating the on-disk config (the in-memory `self.config`
// snapshot stays as-is; the live `state.assistant` is the runtime
@@ -1381,6 +1420,7 @@ impl MeshService {
cfg.assistant_enabled = a.enabled;
cfg.assistant_model = a.model.clone();
cfg.assistant_trusted_only = a.trusted_only;
cfg.assistant_backend = a.backend.clone();
}
save_config(&self.data_dir, &cfg).await?;
Ok(())