channels: find_daemon path walking, consistent pane_id, auto-start

find_daemon() replaces daemon_sock() — walks the dot-delimited channel
path from most-specific to least looking for a daemon socket, and
auto-starts via the supervisor if none is found. All channel tools
(recv, send, open, close) use the same resolution path.

Fix tmux daemon to use pane_id consistently for both pipe-pane and
send-keys (send-keys -t <label> doesn't work, needs the %N pane id).
Store label→pane_id mapping in State instead of bare label vec.

Gracefully handle missing tmux.json5 — start with empty pane list
since panes are added dynamically via the open RPC.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-04 19:20:27 -04:00
parent c9b19dc3d7
commit 2a84fb325d
3 changed files with 103 additions and 33 deletions

View file

@ -67,7 +67,7 @@ fn default_min_count() -> u32 { 20 }
async fn channel_recv(args: &serde_json::Value) -> Result<String> {
let a: RecvArgs = serde_json::from_value(args.clone())
.context("invalid channel_recv arguments")?;
let sock = daemon_sock(&a.channel)?;
let (sock, _) = find_daemon(&a.channel)?;
let channel = a.channel;
let all_new = a.all_new;
let min_count = a.min_count;
@ -90,7 +90,7 @@ struct SendArgs {
async fn channel_send(args: &serde_json::Value) -> Result<String> {
let a: SendArgs = serde_json::from_value(args.clone())
.context("invalid channel_send arguments")?;
let sock = daemon_sock(&a.channel)?;
let (sock, _) = find_daemon(&a.channel)?;
let channel = a.channel;
let message = a.message;
tokio::task::spawn_blocking(move || {
@ -119,13 +119,12 @@ async fn channel_open(args: &serde_json::Value) -> Result<String> {
let label = args.get("label").and_then(|v| v.as_str())
.context("label is required")?
.to_string();
let prefix = label.split('.').next().unwrap_or("tmux");
let sock = daemon_sock(prefix)?;
let (sock, sublabel) = find_daemon(&label)?;
tokio::task::spawn_blocking(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all().build().unwrap();
let local = tokio::task::LocalSet::new();
local.block_on(&rt, rpc_open(&sock, &label))
local.block_on(&rt, rpc_open(&sock, &sublabel))
}).await?
.map_err(|e| anyhow::anyhow!("{}", e))
}
@ -134,7 +133,7 @@ async fn channel_close(args: &serde_json::Value) -> Result<String> {
let channel = args.get("channel").and_then(|v| v.as_str())
.context("channel is required")?
.to_string();
let sock = daemon_sock(&channel)?;
let (sock, _) = find_daemon(&channel)?;
tokio::task::spawn_blocking(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all().build().unwrap();
@ -144,7 +143,7 @@ async fn channel_close(args: &serde_json::Value) -> Result<String> {
.map_err(|e| anyhow::anyhow!("{}", e))
}
// ── Socket helpers ─────────────────────────────────────────────
// ── Daemon resolution ─────────────────────────────────────────
fn channels_dir() -> std::path::PathBuf {
dirs::home_dir()
@ -152,13 +151,64 @@ fn channels_dir() -> std::path::PathBuf {
.join(".consciousness/channels")
}
fn daemon_sock(channel: &str) -> Result<std::path::PathBuf> {
let prefix = channel.split('.').next().unwrap_or("");
let sock = channels_dir().join(format!("{}.sock", prefix));
if !sock.exists() {
anyhow::bail!("no daemon for channel: {}", channel);
/// Resolve a channel path to a daemon socket.
///
/// Walks the dot-delimited path from most-specific to least,
/// looking for a daemon socket at each level:
/// "tmux.ktest" → finds tmux.sock, returns ("tmux.sock", "ktest")
/// "irc.libera.#bcachefs" → finds irc.sock, returns ("irc.sock", "libera.#bcachefs")
///
/// If no daemon is running, tries to start one via the supervisor.
fn find_daemon(path: &str) -> Result<(std::path::PathBuf, String)> {
let dir = channels_dir();
// Returns the sub-path after the matched prefix
let rest_after = |prefix: &str| -> String {
if prefix.len() < path.len() {
path[prefix.len() + 1..].to_string()
} else {
String::new()
}
};
// Walk from most-specific to least, looking for a socket
let mut prefix = path;
loop {
let sock = dir.join(format!("{}.sock", prefix));
if sock.exists() {
return Ok((sock, rest_after(prefix)));
}
match prefix.rfind('.') {
Some(pos) => prefix = &prefix[..pos],
None => break,
}
}
Ok(sock)
// No running daemon found — register and start via supervisor
let top = path.split('.').next().unwrap_or(path);
let mut sup = crate::thalamus::supervisor::Supervisor::new();
sup.load_config();
if !sup.has_daemon(top) {
sup.add_daemon(top, crate::thalamus::supervisor::ChannelEntry {
binary: format!("consciousness-channel-{}", top),
enabled: true,
autostart: true,
});
}
sup.ensure_running();
// Wait for socket (up to 3 seconds)
let sock = dir.join(format!("{}.sock", top));
for _ in 0..30 {
if sock.exists() {
return Ok((sock, rest_after(top)));
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
anyhow::bail!("no daemon for channel path: {}", path)
}
// ── Channel RPC ────────────────────────────────────────────────

View file

@ -95,6 +95,22 @@ impl Supervisor {
}
}
/// Check if a daemon is in the config.
pub fn has_daemon(&self, name: &str) -> bool {
self.config.contains_key(name)
}
/// Add a daemon to the config and persist to channels.json5.
pub fn add_daemon(&mut self, name: &str, entry: ChannelEntry) {
self.config.insert(name.to_string(), entry);
let path = config_path();
if let Ok(json) = serde_json::to_string_pretty(&self.config) {
if let Err(e) = std::fs::write(&path, &json) {
error!("failed to write {}: {}", path.display(), e);
}
}
}
/// Check if a daemon is alive by testing its socket.
fn is_alive(name: &str) -> bool {
let sock = channels_dir().join(format!("{}.sock", name));