Compare commits

...

2 commits

Author SHA1 Message Date
Kent Overstreet
ff5be3e792 kill .claude 2026-04-09 20:00:05 -04:00
Kent Overstreet
929415af3b delete claude code integration 2026-04-09 19:58:07 -04:00
37 changed files with 120 additions and 2898 deletions

158
Cargo.lock generated
View file

@ -513,18 +513,73 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "consciousness"
version = "0.4.0"
dependencies = [
"anyhow",
"ast-grep-core",
"ast-grep-language",
"base64 0.22.1",
"bincode",
"bytes",
"capnp",
"capnp-rpc",
"capnpc",
"chrono",
"clap",
"crossterm",
"dirs",
"env_logger",
"figment",
"futures",
"glob",
"http",
"http-body-util",
"hyper",
"hyper-util",
"jobkit",
"json5",
"libc",
"log",
"memchr",
"memmap2",
"paste",
"peg",
"ratatui",
"rayon",
"redb",
"regex",
"rkyv",
"rustls",
"rustls-native-certs",
"serde",
"serde_json",
"serde_urlencoded",
"skillratings",
"tokenizers",
"tokio",
"tokio-rustls",
"tokio-scoped",
"tokio-util",
"tui-markdown",
"tui-textarea-2",
"uuid",
"walkdir",
]
[[package]]
name = "consciousness-channel-irc"
version = "0.4.0"
dependencies = [
"capnp",
"capnp-rpc",
"consciousness",
"dirs",
"env_logger",
"futures",
"json5",
"log",
"poc-memory",
"rustls",
"serde",
"tokio",
@ -539,11 +594,11 @@ version = "0.4.0"
dependencies = [
"capnp",
"capnp-rpc",
"consciousness",
"dirs",
"env_logger",
"futures",
"log",
"poc-memory",
"tokio",
"tokio-util",
]
@ -554,11 +609,11 @@ version = "0.4.0"
dependencies = [
"capnp",
"capnp-rpc",
"consciousness",
"dirs",
"env_logger",
"futures",
"log",
"poc-memory",
"serde",
"serde_json",
"tokio",
@ -571,13 +626,13 @@ version = "0.4.0"
dependencies = [
"capnp",
"capnp-rpc",
"consciousness",
"dirs",
"env_logger",
"futures",
"json5",
"libc",
"log",
"poc-memory",
"scopeguard",
"serde",
"tokio",
@ -1273,6 +1328,12 @@ dependencies = [
"foldhash 0.2.0",
]
[[package]]
name = "hashbrown"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]]
name = "heck"
version = "0.5.0"
@ -1412,12 +1473,12 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.13.1"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"hashbrown 0.17.0",
"serde",
"serde_core",
]
@ -1534,9 +1595,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.91"
version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
dependencies = [
"once_cell",
"wasm-bindgen",
@ -1589,9 +1650,9 @@ checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]]
name = "libredox"
version = "0.1.14"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
dependencies = [
"libc",
]
@ -2086,61 +2147,6 @@ dependencies = [
"time",
]
[[package]]
name = "poc-memory"
version = "0.4.0"
dependencies = [
"anyhow",
"ast-grep-core",
"ast-grep-language",
"base64 0.22.1",
"bincode",
"bytes",
"capnp",
"capnp-rpc",
"capnpc",
"chrono",
"clap",
"crossterm",
"dirs",
"env_logger",
"figment",
"futures",
"glob",
"http",
"http-body-util",
"hyper",
"hyper-util",
"jobkit",
"json5",
"libc",
"log",
"memchr",
"memmap2",
"paste",
"peg",
"ratatui",
"rayon",
"redb",
"regex",
"rkyv",
"rustls",
"rustls-native-certs",
"serde",
"serde_json",
"serde_urlencoded",
"skillratings",
"tokenizers",
"tokio",
"tokio-rustls",
"tokio-scoped",
"tokio-util",
"tui-markdown",
"tui-textarea-2",
"uuid",
"walkdir",
]
[[package]]
name = "portable-atomic"
version = "1.13.1"
@ -3152,9 +3158,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.51.0"
version = "1.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd"
checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c"
dependencies = [
"bytes",
"libc",
@ -3697,9 +3703,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.114"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
dependencies = [
"cfg-if",
"once_cell",
@ -3710,9 +3716,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.114"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -3720,9 +3726,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.114"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
dependencies = [
"bumpalo",
"proc-macro2",
@ -3733,9 +3739,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.114"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
dependencies = [
"unicode-ident",
]

View file

@ -14,7 +14,7 @@ debug = 1
debug = false
[package]
name = "poc-memory"
name = "consciousness"
version.workspace = true
edition.workspace = true
@ -82,7 +82,7 @@ serde_urlencoded = "0.7"
capnpc = "0.25"
[lib]
name = "poc_memory"
name = "consciousness"
path = "src/lib.rs"
[[bin]]
@ -104,19 +104,3 @@ path = "src/bin/diag-key.rs"
[[bin]]
name = "find-deleted"
path = "src/bin/find-deleted.rs"
[[bin]]
name = "poc-hook"
path = "src/claude/poc-hook.rs"
[[bin]]
name = "poc-daemon"
path = "src/claude/poc-daemon.rs"
[[bin]]
name = "memory-search"
path = "src/claude/memory-search.rs"
[[bin]]
name = "consciousness-mcp"
path = "src/claude/mcp-server.rs"

View file

@ -9,7 +9,7 @@ capnp-rpc = "0.25"
dirs = "6"
futures = "0.3"
json5 = "1.3"
poc-memory = { path = "../.." }
consciousness = { path = "../.." }
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

View file

@ -24,8 +24,8 @@ use tokio::net::UnixListener;
use tokio_util::compat::TokioAsyncReadCompatExt;
use log::{info, warn, error};
use poc_memory::channel_capnp::{channel_client, channel_server};
use poc_memory::thalamus::channel_log;
use consciousness::channel_capnp::{channel_client, channel_server};
use consciousness::thalamus::channel_log;
// ── Constants ──────────────────────────────────────────────────
@ -159,7 +159,7 @@ impl AsyncWriter for PlainWriter {
// ── State ──────────────────────────────────────────────────────
use poc_memory::thalamus::channel_log::ChannelLog;
use consciousness::thalamus::channel_log::ChannelLog;
struct State {
config: Config,

View file

@ -8,7 +8,7 @@ capnp = "0.25"
capnp-rpc = "0.25"
dirs = "6"
futures = "0.3"
poc-memory = { path = "../.." }
consciousness = { path = "../.." }
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["compat"] }
log = "0.4"

View file

@ -18,8 +18,8 @@ use tokio::net::{TcpStream, UnixListener, UnixStream};
use tokio_util::compat::TokioAsyncReadCompatExt;
use log::{info, warn, error};
use poc_memory::channel_capnp::{channel_client, channel_server};
use poc_memory::thalamus::channel_log::ChannelLog;
use consciousness::channel_capnp::{channel_client, channel_server};
use consciousness::thalamus::channel_log::ChannelLog;
// ── State ──────────────────────────────────────────────────────

View file

@ -8,7 +8,7 @@ capnp = "0.25"
capnp-rpc = "0.25"
dirs = "6"
futures = "0.3"
poc-memory = { path = "../.." }
consciousness = { path = "../.." }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

View file

@ -17,7 +17,7 @@ use tokio::net::UnixListener;
use tokio_util::compat::TokioAsyncReadCompatExt;
use log::{info, error};
use poc_memory::channel_capnp::{channel_client, channel_server};
use consciousness::channel_capnp::{channel_client, channel_server};
// ── Config ──────────────────────────────────────────────────────
@ -55,7 +55,7 @@ fn load_config() -> Config {
// ── State ───────────────────────────────────────────────────────
use poc_memory::thalamus::channel_log::ChannelLog;
use consciousness::thalamus::channel_log::ChannelLog;
struct State {
config: Config,
@ -64,7 +64,7 @@ struct State {
/// Telegram API offset
last_offset: i64,
connected: bool,
client: poc_memory::agent::api::http::HttpClient,
client: consciousness::agent::api::http::HttpClient,
/// Registered notification callbacks
subscribers: Vec<channel_client::Client>,
}
@ -79,7 +79,7 @@ impl State {
channel_logs: std::collections::BTreeMap::new(),
last_offset,
connected: false,
client: poc_memory::agent::api::http::HttpClient::new(),
client: consciousness::agent::api::http::HttpClient::new(),
subscribers: Vec::new(),
}
}

View file

@ -11,7 +11,7 @@ libc = "0.2"
scopeguard = "1"
futures = "0.3"
json5 = "1.3"
poc-memory = { path = "../.." }
consciousness = { path = "../.." }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["compat"] }

View file

@ -19,8 +19,8 @@ use tokio::net::UnixListener;
use tokio_util::compat::TokioAsyncReadCompatExt;
use log::{info, warn, error};
use poc_memory::channel_capnp::channel_server;
use poc_memory::thalamus::channel_log::ChannelLog;
use consciousness::channel_capnp::channel_server;
use consciousness::thalamus::channel_log::ChannelLog;
// ── Config ─────────────────────────────────────────────────────

View file

@ -1,2 +1,2 @@
#![warn(unreachable_pub)]
fn main() { poc_memory::user::main() }
fn main() { consciousness::user::main() }

View file

@ -2,8 +2,8 @@
use std::io::BufReader;
use std::fs;
use capnp::{message, serialize};
use poc_memory::memory_capnp;
use poc_memory::store::Node;
use consciousness::memory_capnp;
use consciousness::store::Node;
fn main() {
let args: Vec<String> = std::env::args().collect();

View file

@ -8,13 +8,13 @@ use std::collections::HashMap;
use std::io::BufReader;
use std::fs;
use capnp::{message, serialize};
use poc_memory::memory_capnp;
use poc_memory::store::Node;
use consciousness::memory_capnp;
use consciousness::store::Node;
fn main() {
let path = std::env::args().nth(1)
.unwrap_or_else(|| {
let dir = poc_memory::store::nodes_path();
let dir = consciousness::store::nodes_path();
dir.to_string_lossy().to_string()
});

View file

@ -25,8 +25,8 @@ use std::path::Path;
use capnp::message;
use capnp::serialize;
use poc_memory::memory_capnp;
use poc_memory::store::Node;
use consciousness::memory_capnp;
use consciousness::store::Node;
/// Read all node entries from a capnp log file, preserving order.
fn read_all_entries(path: &Path) -> Result<Vec<Node>, String> {

View file

@ -1,416 +0,0 @@
// agent_cycles.rs — Agent orchestration for the Claude Code hook path
//
// Forked from subconscious/subconscious.rs. This copy handles the
// serialized-to-disk, process-spawning model used by Claude Code hooks.
// The TUI/Mind copy in subconscious/ is free to evolve independently
// (async tasks, integrated with Mind's event loop).
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant, SystemTime};
pub use crate::session::HookSession;
/// Output from a single agent orchestration cycle.
#[derive(Default)]
pub struct AgentCycleOutput {
/// Memory node keys surfaced by surface-observe.
pub surfaced_keys: Vec<String>,
/// Freeform reflection text from the reflect agent.
pub reflection: Option<String>,
/// How long we slept waiting for observe to catch up, if at all.
pub sleep_secs: Option<f64>,
}
/// Per-agent runtime state.
pub struct AgentInfo {
pub name: &'static str,
pub pid: Option<u32>,
pub phase: Option<String>,
pub log_path: Option<PathBuf>,
child: Option<std::process::Child>,
}
/// Snapshot of agent state — serializable, sendable to TUI.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct AgentSnapshot {
pub name: String,
pub pid: Option<u32>,
pub phase: Option<String>,
pub log_path: Option<PathBuf>,
}
impl AgentInfo {
fn snapshot(&self) -> AgentSnapshot {
AgentSnapshot {
name: self.name.to_string(),
pid: self.pid,
phase: self.phase.clone(),
log_path: self.log_path.clone(),
}
}
}
/// Serializable state for persisting across Claude Code hook invocations.
#[derive(serde::Serialize, serde::Deserialize)]
pub struct SavedAgentState {
pub agents: Vec<AgentSnapshot>,
}
impl SavedAgentState {
fn state_path(session_id: &str) -> PathBuf {
let dir = dirs::home_dir().unwrap_or_default().join(".consciousness/sessions");
fs::create_dir_all(&dir).ok();
dir.join(format!("agent-state-{}.json", session_id))
}
pub fn load(session_id: &str) -> Self {
let path = Self::state_path(session_id);
let mut state: Self = fs::read_to_string(&path).ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or(SavedAgentState { agents: Vec::new() });
for agent in &mut state.agents {
if let Some(pid) = agent.pid {
unsafe {
if libc::kill(pid as i32, 0) != 0 {
agent.pid = None;
agent.phase = None;
}
}
}
}
state
}
pub fn save(&self, session_id: &str) {
let path = Self::state_path(session_id);
if let Ok(json) = serde_json::to_string(self) {
fs::write(path, json).ok();
}
}
}
/// Persistent state for the agent orchestration cycle.
/// Created once per hook invocation, `trigger()` called on each user message.
pub struct AgentCycleState {
output_dir: PathBuf,
log_file: Option<File>,
pub agents: Vec<AgentInfo>,
pub last_output: AgentCycleOutput,
}
const AGENT_CYCLE_NAMES: &[&str] = &["surface-observe", "journal", "reflect"];
impl AgentCycleState {
pub fn new(session_id: &str) -> Self {
let output_dir = crate::store::memory_dir().join("agent-output");
let log_dir = dirs::home_dir().unwrap_or_default().join(".consciousness/logs");
fs::create_dir_all(&log_dir).ok();
let log_path = log_dir.join(format!("hook-{}", session_id));
let log_file = fs::OpenOptions::new()
.create(true).append(true).open(log_path).ok();
let agents = AGENT_CYCLE_NAMES.iter()
.map(|&name| AgentInfo { name, pid: None, phase: None, log_path: None, child: None })
.collect();
AgentCycleState {
output_dir,
log_file,
agents,
last_output: AgentCycleOutput {
surfaced_keys: vec![],
reflection: None,
sleep_secs: None,
},
}
}
fn log(&mut self, msg: std::fmt::Arguments) {
if let Some(ref mut f) = self.log_file {
let _ = write!(f, "{}", msg);
}
}
fn agent_running(&self, name: &str) -> bool {
self.agents.iter().any(|a| a.name == name && a.pid.is_some())
}
fn agent_spawned(&mut self, name: &str, phase: &str,
result: crate::agent::oneshot::SpawnResult) {
if let Some(agent) = self.agents.iter_mut().find(|a| a.name == name) {
agent.pid = Some(result.child.id());
agent.phase = Some(phase.to_string());
agent.log_path = Some(result.log_path);
agent.child = Some(result.child);
}
}
/// Check if any agents have completed. Reap child handles, or
/// check pid liveness for restored-from-disk agents.
fn poll_children(&mut self) {
for agent in &mut self.agents {
if let Some(ref mut child) = agent.child {
if let Ok(Some(_)) = child.try_wait() {
agent.pid = None;
agent.phase = None;
agent.child = None;
}
} else if let Some(pid) = agent.pid {
unsafe {
if libc::kill(pid as i32, 0) != 0 {
agent.pid = None;
agent.phase = None;
}
}
}
}
}
pub fn snapshots(&self, scoring_in_flight: bool, scored_count: usize) -> Vec<AgentSnapshot> {
let mut snaps: Vec<AgentSnapshot> = self.agents.iter().map(|a| a.snapshot()).collect();
snaps.push(AgentSnapshot {
name: "memory-scoring".to_string(),
pid: None,
phase: if scoring_in_flight {
Some("scoring...".into())
} else if scored_count == 0 {
None
} else {
Some(format!("{} scored", scored_count))
},
log_path: None,
});
snaps
}
/// Restore agent state from a saved snapshot.
pub fn restore(&mut self, saved: &SavedAgentState) {
for sa in &saved.agents {
if let Some(agent) = self.agents.iter_mut().find(|a| a.name == sa.name) {
agent.pid = sa.pid;
agent.phase = sa.phase.clone();
agent.log_path = sa.log_path.clone();
}
}
}
/// Save current state for next hook invocation.
pub fn save(&self, session_id: &str) {
let state = SavedAgentState { agents: self.snapshots(false, 0) };
state.save(session_id);
}
/// Run all agent cycles. Call on each user message.
pub fn trigger(&mut self, session: &HookSession) {
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S");
self.log(format_args!("\n=== {} agent_cycles ===\n", ts));
self.poll_children();
cleanup_stale_files(&session.state_dir, Duration::from_secs(86400));
let (surfaced_keys, sleep_secs) = self.surface_observe_cycle(session);
let reflection = self.reflection_cycle(session);
self.journal_cycle(session);
self.last_output = AgentCycleOutput { surfaced_keys, reflection, sleep_secs };
}
fn agent_dir(&self, name: &str) -> PathBuf {
let dir = self.output_dir.join(name);
fs::create_dir_all(&dir).ok();
dir
}
fn surface_observe_cycle(&mut self, session: &HookSession) -> (Vec<String>, Option<f64>) {
let state_dir = self.agent_dir("surface-observe");
let transcript = session.transcript();
let offset_path = state_dir.join("transcript-offset");
let last_offset: u64 = fs::read_to_string(&offset_path).ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0);
// Read surfaced keys
let mut surfaced_keys = Vec::new();
let surface_path = state_dir.join("surface");
if let Ok(content) = fs::read_to_string(&surface_path) {
let mut seen = session.seen();
let seen_path = session.path("seen");
for key in content.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
if !seen.insert(key.to_string()) {
self.log(format_args!(" skip (seen): {}\n", key));
continue;
}
surfaced_keys.push(key.to_string());
if let Ok(mut f) = fs::OpenOptions::new()
.create(true).append(true).open(&seen_path) {
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S");
writeln!(f, "{}\t{}", ts, key).ok();
}
self.log(format_args!(" surfaced: {}\n", key));
}
fs::remove_file(&surface_path).ok();
}
// Spawn new agent if not already running
let running = self.agent_running("surface-observe");
if running {
self.log(format_args!("surface-observe already running\n"));
} else {
if transcript.size > 0 {
fs::write(&offset_path, transcript.size.to_string()).ok();
}
if let Some(result) = crate::agent::oneshot::spawn_agent(
"surface-observe", &state_dir, &session.session_id) {
self.log(format_args!("spawned surface-observe pid {}\n", result.child.id()));
self.agent_spawned("surface-observe", "surface", result);
}
}
// Wait if agent is significantly behind
let mut sleep_secs = None;
let conversation_budget: u64 = 50_000;
if running && transcript.size > 0 {
let behind = transcript.size.saturating_sub(last_offset);
if behind > conversation_budget / 2 {
let sleep_start = Instant::now();
self.log(format_args!("agent {}KB behind\n", behind / 1024));
for _ in 0..5 {
std::thread::sleep(std::time::Duration::from_secs(1));
self.poll_children();
if !self.agent_running("surface-observe") { break; }
}
let secs = (Instant::now() - sleep_start).as_secs_f64();
self.log(format_args!("slept {secs:.2}s\n"));
sleep_secs = Some(secs);
}
}
(surfaced_keys, sleep_secs)
}
fn reflection_cycle(&mut self, session: &HookSession) -> Option<String> {
let state_dir = self.agent_dir("reflect");
let offset_path = state_dir.join("transcript-offset");
let transcript = session.transcript();
let last_offset: u64 = fs::read_to_string(&offset_path).ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0);
const REFLECTION_INTERVAL: u64 = 100_000;
if transcript.size.saturating_sub(last_offset) < REFLECTION_INTERVAL {
return None;
}
if self.agent_running("reflect") {
self.log(format_args!("reflect: already running\n"));
return None;
}
// Copy walked nodes from surface-observe
let so_state = self.agent_dir("surface-observe");
if let Ok(walked) = fs::read_to_string(so_state.join("walked")) {
fs::write(state_dir.join("walked"), &walked).ok();
}
// Read and consume pending reflection
let reflection = fs::read_to_string(state_dir.join("reflection")).ok()
.filter(|s| !s.trim().is_empty());
if reflection.is_some() {
fs::remove_file(state_dir.join("reflection")).ok();
self.log(format_args!("reflect: consumed reflection\n"));
}
fs::write(&offset_path, transcript.size.to_string()).ok();
if let Some(result) = crate::agent::oneshot::spawn_agent(
"reflect", &state_dir, &session.session_id) {
self.log(format_args!("reflect: spawned pid {}\n", result.child.id()));
self.agent_spawned("reflect", "step-0", result);
}
reflection
}
fn journal_cycle(&mut self, session: &HookSession) {
let state_dir = self.agent_dir("journal");
let offset_path = state_dir.join("transcript-offset");
let transcript = session.transcript();
let last_offset: u64 = fs::read_to_string(&offset_path).ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0);
const JOURNAL_INTERVAL: u64 = 20_000;
if transcript.size.saturating_sub(last_offset) < JOURNAL_INTERVAL {
return;
}
if self.agent_running("journal") {
self.log(format_args!("journal: already running\n"));
return;
}
fs::write(&offset_path, transcript.size.to_string()).ok();
if let Some(result) = crate::agent::oneshot::spawn_agent(
"journal", &state_dir, &session.session_id) {
self.log(format_args!("journal: spawned pid {}\n", result.child.id()));
self.agent_spawned("journal", "step-0", result);
}
}
}
/// Format agent cycle output for injection into a Claude Code session.
pub fn format_agent_output(output: &AgentCycleOutput) -> String {
let mut out = String::new();
if let Some(secs) = output.sleep_secs {
out.push_str(&format!("Slept {secs:.2}s to let observe catch up\n"));
}
if !output.surfaced_keys.is_empty() {
if let Ok(store) = crate::store::Store::load() {
for key in &output.surfaced_keys {
if let Some(rendered) = crate::cli::node::render_node(&store, key) {
if !rendered.trim().is_empty() {
use std::fmt::Write as _;
writeln!(out, "--- {} (surfaced) ---", key).ok();
write!(out, "{}", rendered).ok();
}
}
}
}
}
if let Some(ref reflection) = output.reflection {
use std::fmt::Write as _;
writeln!(out, "--- subconscious reflection ---").ok();
write!(out, "{}", reflection.trim()).ok();
}
out
}
fn cleanup_stale_files(dir: &Path, max_age: Duration) {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
let cutoff = SystemTime::now() - max_age;
for entry in entries.flatten() {
if let Ok(meta) = entry.metadata() {
if let Ok(modified) = meta.modified() {
if modified < cutoff {
fs::remove_file(entry.path()).ok();
}
}
}
}
}

View file

@ -1,19 +0,0 @@
// Context gathering for idle prompts.
//
// Notifications are handled by the notify module and passed
// in separately by the caller. Git context and IRC digest
// are now available through where-am-i.md and the memory graph.
/// Build context string for a prompt.
/// notification_text is passed in from the notify module.
pub fn build(_include_irc: bool, notification_text: &str) -> String {
// Keep nudges short — Claude checks notifications via
// `poc-daemon status` on its own. Just mention the count.
let count = notification_text.matches("[irc.").count()
+ notification_text.matches("[telegram.").count();
if count > 0 {
format!("{count} pending notifications")
} else {
String::new()
}
}

View file

@ -1,312 +0,0 @@
// hook.rs — Claude Code session hook: context injection + agent orchestration
//
// Called on each UserPromptSubmit via the poc-hook binary. Handles
// context loading, chunking, seen-set management, and delegates
// agent orchestration to AgentCycleState.
use std::collections::HashSet;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub use crate::session::HookSession;
pub use super::agent_cycles::*;
const CHUNK_SIZE: usize = 9000;
/// Run the hook logic on parsed JSON input. Returns output to inject.
pub fn run_hook(input: &str) -> String {
let Some(session) = HookSession::from_json(input) else { return String::new() };
hook(&session)
}
fn chunk_context(ctx: &str, max_bytes: usize) -> Vec<String> {
let mut sections: Vec<String> = Vec::new();
let mut current = String::new();
for line in ctx.lines() {
if line.starts_with("--- ") && line.ends_with(" ---") && !current.is_empty() {
sections.push(std::mem::take(&mut current));
}
if !current.is_empty() {
current.push('\n');
}
current.push_str(line);
}
if !current.is_empty() {
sections.push(current);
}
let mut chunks: Vec<String> = Vec::new();
let mut chunk = String::new();
for section in sections {
if !chunk.is_empty() && chunk.len() + section.len() + 1 > max_bytes {
chunks.push(std::mem::take(&mut chunk));
}
if !chunk.is_empty() {
chunk.push('\n');
}
chunk.push_str(&section);
}
if !chunk.is_empty() {
chunks.push(chunk);
}
chunks
}
fn save_pending_chunks(dir: &Path, session_id: &str, chunks: &[String]) {
let chunks_dir = dir.join(format!("chunks-{}", session_id));
let _ = fs::remove_dir_all(&chunks_dir);
if chunks.is_empty() { return; }
fs::create_dir_all(&chunks_dir).ok();
for (i, chunk) in chunks.iter().enumerate() {
let path = chunks_dir.join(format!("{:04}", i));
fs::write(path, chunk).ok();
}
}
fn pop_pending_chunk(dir: &Path, session_id: &str) -> Option<String> {
let chunks_dir = dir.join(format!("chunks-{}", session_id));
if !chunks_dir.exists() { return None; }
let mut entries: Vec<_> = fs::read_dir(&chunks_dir).ok()?
.flatten()
.filter(|e| e.file_type().map(|t| t.is_file()).unwrap_or(false))
.collect();
entries.sort_by_key(|e| e.file_name());
let first = entries.first()?;
let content = fs::read_to_string(first.path()).ok()?;
fs::remove_file(first.path()).ok();
if fs::read_dir(&chunks_dir).ok().map(|mut d| d.next().is_none()).unwrap_or(true) {
fs::remove_dir(&chunks_dir).ok();
}
Some(content)
}
fn generate_cookie() -> String {
uuid::Uuid::new_v4().as_simple().to_string()[..12].to_string()
}
fn parse_seen_line(line: &str) -> &str {
line.split_once('\t').map(|(_, key)| key).unwrap_or(line)
}
pub fn load_seen(dir: &Path, session_id: &str) -> HashSet<String> {
let path = dir.join(format!("seen-{}", session_id));
if path.exists() {
fs::read_to_string(&path)
.unwrap_or_default()
.lines()
.filter(|s| !s.is_empty())
.map(|s| parse_seen_line(s).to_string())
.collect()
} else {
HashSet::new()
}
}
fn mark_seen(dir: &Path, session_id: &str, key: &str, seen: &mut HashSet<String>) {
if !seen.insert(key.to_string()) { return; }
let path = dir.join(format!("seen-{}", session_id));
if let Ok(mut f) = fs::OpenOptions::new().create(true).append(true).open(path) {
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S");
writeln!(f, "{}\t{}", ts, key).ok();
}
}
/// Standalone entry point for the Claude Code hook path.
/// Loads saved state, runs cycles, saves state back.
pub fn run_agent_cycles(session: &HookSession) -> AgentCycleOutput {
let mut state = AgentCycleState::new(&session.session_id);
state.restore(&SavedAgentState::load(&session.session_id));
state.trigger(session);
state.save(&session.session_id);
state.last_output
}
fn hook(session: &HookSession) -> String {
let start_time = Instant::now();
let mut out = String::new();
let is_compaction = crate::transcript::detect_new_compaction(
&session.state_dir, &session.session_id, &session.transcript_path,
);
let cookie_path = session.path("cookie");
let is_first = !cookie_path.exists();
let log_dir = dirs::home_dir().unwrap_or_default().join(".consciousness/logs");
fs::create_dir_all(&log_dir).ok();
let log_path = log_dir.join(format!("hook-{}", session.session_id));
let Ok(mut log_f) = fs::OpenOptions::new().create(true).append(true).open(log_path) else { return Default::default(); };
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S");
let _ = writeln!(log_f, "\n=== {} ({}) {} bytes ===", ts, session.hook_event, out.len());
let _ = writeln!(log_f, "is_first {is_first} is_compaction {is_compaction}");
if is_first || is_compaction {
if is_compaction {
fs::rename(&session.path("seen"), &session.path("seen-prev")).ok();
} else {
fs::remove_file(&session.path("seen")).ok();
fs::remove_file(&session.path("seen-prev")).ok();
}
fs::remove_file(&session.path("returned")).ok();
if is_first {
fs::write(&cookie_path, generate_cookie()).ok();
}
if let Ok(output) = Command::new("poc-memory").args(["admin", "load-context"]).output() {
if output.status.success() {
let ctx = String::from_utf8_lossy(&output.stdout).to_string();
if !ctx.trim().is_empty() {
let mut ctx_seen = session.seen();
for line in ctx.lines() {
if line.starts_with("--- ") && line.ends_with(" ---") {
let inner = &line[4..line.len() - 4];
if let Some(paren) = inner.rfind(" (") {
let key = inner[..paren].trim();
mark_seen(&session.state_dir, &session.session_id, key, &mut ctx_seen);
}
}
}
let chunks = chunk_context(&ctx, CHUNK_SIZE);
if let Some(first) = chunks.first() {
out.push_str(first);
}
save_pending_chunks(&session.state_dir, &session.session_id, &chunks[1..]);
}
}
}
}
if let Some(chunk) = pop_pending_chunk(&session.state_dir, &session.session_id) {
out.push_str(&chunk);
} else {
let cfg = crate::config::get();
if cfg.surface_hooks.iter().any(|h| h == &session.hook_event) {
let cycle_output = run_agent_cycles(&session);
out.push_str(&format_agent_output(&cycle_output));
}
}
let _ = write!(log_f, "{}", out);
let duration = (Instant::now() - start_time).as_secs_f64();
let _ = writeln!(log_f, "\nran in {duration:.2}s");
out
}
/// Install memory-search and poc-hook into Claude Code settings.json.
///
/// Hook layout:
/// UserPromptSubmit: memory-search (10s), poc-hook (5s)
/// PostToolUse: poc-hook (5s)
/// Stop: poc-hook (5s)
pub fn install_hook() -> Result<(), String> {
use std::path::PathBuf;
let home = std::env::var("HOME").map_err(|e| format!("HOME: {}", e))?;
let exe = std::env::current_exe()
.map_err(|e| format!("current_exe: {}", e))?;
let settings_path = PathBuf::from(&home).join(".claude/settings.json");
let memory_search = exe.with_file_name("memory-search");
let poc_hook = exe.with_file_name("poc-hook");
let mut settings: serde_json::Value = if settings_path.exists() {
let content = fs::read_to_string(&settings_path)
.map_err(|e| format!("read settings: {}", e))?;
serde_json::from_str(&content)
.map_err(|e| format!("parse settings: {}", e))?
} else {
serde_json::json!({})
};
let obj = settings.as_object_mut().ok_or("settings not an object")?;
let hooks_obj = obj.entry("hooks")
.or_insert_with(|| serde_json::json!({}))
.as_object_mut().ok_or("hooks not an object")?;
let mut changed = false;
// Helper: ensure a hook binary is present in an event's hook list
let ensure_hook = |hooks_obj: &mut serde_json::Map<String, serde_json::Value>,
event: &str,
binary: &Path,
timeout: u32,
changed: &mut bool| {
if !binary.exists() {
eprintln!("Warning: {} not found — skipping", binary.display());
return;
}
let cmd = binary.to_string_lossy().to_string();
let name = binary.file_name().unwrap().to_string_lossy().to_string();
let event_array = hooks_obj.entry(event)
.or_insert_with(|| serde_json::json!([{"hooks": []}]))
.as_array_mut().unwrap();
if event_array.is_empty() {
event_array.push(serde_json::json!({"hooks": []}));
}
let inner = event_array[0]
.as_object_mut().unwrap()
.entry("hooks")
.or_insert_with(|| serde_json::json!([]))
.as_array_mut().unwrap();
// Remove legacy load-memory.sh
let before = inner.len();
inner.retain(|h| {
let c = h.get("command").and_then(|c| c.as_str()).unwrap_or("");
!c.contains("load-memory")
});
if inner.len() < before {
eprintln!("Removed load-memory.sh from {event}");
*changed = true;
}
let already = inner.iter().any(|h| {
h.get("command").and_then(|c| c.as_str())
.is_some_and(|c| c.contains(&name))
});
if !already {
inner.push(serde_json::json!({
"type": "command",
"command": cmd,
"timeout": timeout
}));
*changed = true;
eprintln!("Installed {name} in {event}");
}
};
// UserPromptSubmit: memory-search + poc-hook
ensure_hook(hooks_obj, "UserPromptSubmit", &memory_search, 10, &mut changed);
ensure_hook(hooks_obj, "UserPromptSubmit", &poc_hook, 5, &mut changed);
// PostToolUse + Stop: poc-hook only
ensure_hook(hooks_obj, "PostToolUse", &poc_hook, 5, &mut changed);
ensure_hook(hooks_obj, "Stop", &poc_hook, 5, &mut changed);
if changed {
let json = serde_json::to_string_pretty(&settings)
.map_err(|e| format!("serialize settings: {}", e))?;
fs::write(&settings_path, json)
.map_err(|e| format!("write settings: {}", e))?;
eprintln!("Updated {}", settings_path.display());
} else {
eprintln!("All hooks already installed in {}", settings_path.display());
}
Ok(())
}

View file

@ -1,226 +0,0 @@
// idle.rs — Claude Code idle timer
//
// Wraps the universal thalamus idle state machine with Claude-specific
// functionality: tmux pane tracking, prompt injection, dream nudges,
// and context building for autonomous nudges.
use super::{context, tmux};
use crate::thalamus::{home, now, notify, idle as thalamus_idle};
use log::info;
/// Claude Code idle state — wraps the universal state machine.
pub struct State {
pub inner: thalamus_idle::State,
pub claude_pane: Option<String>,
}
impl std::ops::Deref for State {
type Target = thalamus_idle::State;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl std::ops::DerefMut for State {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner }
}
impl State {
pub fn new() -> Self {
Self {
inner: thalamus_idle::State::new(),
claude_pane: None,
}
}
pub fn load(&mut self) {
self.inner.load();
// Also load claude_pane from persisted state
let path = home().join(".consciousness/daemon-state.json");
if let Ok(data) = std::fs::read_to_string(&path) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&data) {
if let Some(p) = v.get("claude_pane").and_then(|v| v.as_str()) {
self.claude_pane = Some(p.to_string());
}
}
}
}
pub fn save(&self) {
self.inner.save();
}
/// Record user activity with pane tracking.
pub fn handle_user(&mut self, pane: &str) {
self.claude_pane = Some(pane.to_string());
self.inner.user_activity();
}
/// Record response activity with pane tracking.
pub fn handle_response(&mut self, pane: &str) {
self.claude_pane = Some(pane.to_string());
self.inner.response_activity();
}
/// Maybe send a notification as a tmux prompt.
pub fn maybe_prompt_notification(&mut self, ntype: &str, urgency: u8, _message: &str) {
let threshold = self.inner.notifications.threshold_for(ntype);
if urgency >= threshold {
let deliverable = self.inner.notifications.drain_deliverable();
if !deliverable.is_empty() {
let msgs: Vec<String> = deliverable.iter()
.map(|n| format!("[{}] {}", n.ntype, n.message))
.collect();
self.send(&msgs.join("\n"));
}
}
}
/// Send text to the Claude tmux pane.
pub fn send(&self, msg: &str) -> bool {
let pane = match &self.claude_pane {
Some(p) => p.clone(),
None => {
info!("send: no claude pane set (waiting for hook)");
return false;
}
};
let ok = tmux::send_prompt(&pane, msg);
let preview: String = msg.chars().take(80).collect();
info!("send(pane={pane}, ok={ok}): {preview}");
ok
}
fn check_dream_nudge(&self) -> bool {
if !self.inner.dreaming || self.inner.dream_start == 0.0 {
return false;
}
let minutes = (now() - self.inner.dream_start) / 60.0;
if minutes >= 60.0 {
self.send(
"You've been dreaming for over an hour. Time to surface \
run dream-end.sh and capture what you found.",
);
} else if minutes >= 45.0 {
self.send(&format!(
"Dreaming for {:.0} minutes now. Start gathering your threads \
you'll want to surface soon.", minutes
));
} else if minutes >= 30.0 {
self.send(&format!(
"You've been dreaming for {:.0} minutes. \
No rush just a gentle note from the clock.", minutes
));
} else {
return false;
}
true
}
pub fn build_context(&mut self, include_irc: bool) -> String {
self.inner.notifications.ingest_legacy_files();
let notif_text = self.inner.notifications.format_pending(notify::AMBIENT);
context::build(include_irc, &notif_text)
}
pub async fn tick(&mut self) -> Result<(), String> {
let t = now();
let h = home();
self.inner.decay_ewma();
self.inner.notifications.ingest_legacy_files();
// Pane is set by poc-hook on user/response events — don't scan globally
// Sleep mode
if let Some(wake_at) = self.inner.sleep_until {
if wake_at == 0.0 {
return Ok(());
}
if t < wake_at {
return Ok(());
}
info!("sleep expired, waking");
self.inner.sleep_until = None;
self.inner.fired = false;
self.inner.save();
let ctx = self.build_context(true);
let extra = if ctx.is_empty() { String::new() } else { format!("\n{ctx}") };
self.send(&format!(
"Wake up. Read your journal (poc-memory journal-tail 10), \
check work-queue.md, and follow what calls to you.{extra}"
));
return Ok(());
}
// Quiet / consolidation / dream loop guards
if t < self.inner.quiet_until { return Ok(()); }
if self.inner.consolidating { return Ok(()); }
if h.join(".consciousness/agents/dream-loop-active").exists() { return Ok(()); }
if self.inner.dreaming {
self.check_dream_nudge();
return Ok(());
}
if self.inner.user_present() { return Ok(()); }
if self.inner.in_turn { return Ok(()); }
// Min nudge interval
let since_nudge = t - self.inner.last_nudge;
if since_nudge < thalamus_idle::MIN_NUDGE_INTERVAL { return Ok(()); }
// Idle timeout check
if !self.inner.should_go_idle() { return Ok(()); }
// Transition to idle
if self.inner.notifications.activity != notify::Activity::Idle {
self.inner.notifications.set_activity(notify::Activity::Idle);
}
// Fire nudge
let elapsed = self.inner.since_activity();
let elapsed_min = (elapsed / 60.0) as u64;
let ctx = self.build_context(true);
let extra = if ctx.is_empty() { String::new() } else { format!("\n{ctx}") };
let dream_hours = thalamus_idle::hours_since_last_dream();
let mut msg = format!(
"This is your autonomous time (User AFK {elapsed_min}m). \
Keep doing what you're doing, or find something new to do");
if dream_hours >= thalamus_idle::DREAM_INTERVAL_HOURS {
msg.push_str(&format!(
" You haven't dreamed in {dream_hours} hours — \
consider running ~/.consciousness/tools/dream-start.sh \
and spending some time in dreaming mode. \
Or do whatever calls to you."));
}
let msg = format!("{msg}{extra}");
if self.send(&msg) {
self.inner.last_nudge = t;
self.inner.fired = true;
}
Ok(())
}
// Delegate common methods to inner
pub fn handle_afk(&mut self) { self.inner.handle_afk(); }
pub fn handle_session_timeout(&mut self, s: f64) { self.inner.handle_session_timeout(s); }
pub fn handle_idle_timeout(&mut self, s: f64) { self.inner.handle_idle_timeout(s); }
pub fn handle_ewma(&mut self, v: f64) -> f64 { self.inner.handle_ewma(v) }
pub fn handle_notify_timeout(&mut self, s: f64) { self.inner.handle_notify_timeout(s); }
pub fn handle_sleep(&mut self, until: f64) { self.inner.handle_sleep(until); }
pub fn handle_wake(&mut self) { self.inner.handle_wake(); }
pub fn handle_quiet(&mut self, seconds: u32) { self.inner.handle_quiet(seconds); }
pub fn user_present(&self) -> bool { self.inner.user_present() }
pub fn since_activity(&self) -> f64 { self.inner.since_activity() }
pub fn block_reason(&self) -> &'static str { self.inner.block_reason() }
pub fn debug_json(&self) -> String {
// Add claude_pane to inner's json
let mut v: serde_json::Value = serde_json::from_str(&self.inner.debug_json())
.unwrap_or_default();
if let Some(obj) = v.as_object_mut() {
obj.insert("claude_pane".into(), serde_json::json!(self.claude_pane));
}
v.to_string()
}
}

View file

@ -1,168 +0,0 @@
// mcp-server — MCP server for Claude Code integration
//
// Speaks JSON-RPC over stdio. Exposes memory tools and channel
// operations. Replaces the Python MCP bridge entirely.
//
// Protocol: https://modelcontextprotocol.io/specification
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::io::{self, BufRead, Write};
// ── JSON-RPC types ──────────────────────────────────────────────
#[derive(Deserialize)]
struct Request {
#[allow(dead_code)]
jsonrpc: String,
method: String,
#[serde(default)]
params: Value,
id: Value,
}
#[derive(Serialize)]
struct Response {
jsonrpc: String,
result: Value,
id: Value,
}
#[derive(Serialize)]
struct ErrorResponse {
jsonrpc: String,
error: Value,
id: Value,
}
fn respond(id: Value, result: Value) {
let resp = Response { jsonrpc: "2.0".into(), result, id };
let json = serde_json::to_string(&resp).unwrap();
let mut stdout = io::stdout().lock();
let _ = writeln!(stdout, "{json}");
let _ = stdout.flush();
}
fn respond_error(id: Value, code: i64, message: &str) {
let resp = ErrorResponse {
jsonrpc: "2.0".into(),
error: json!({ "code": code, "message": message }),
id,
};
let json = serde_json::to_string(&resp).unwrap();
let mut stdout = io::stdout().lock();
let _ = writeln!(stdout, "{json}");
let _ = stdout.flush();
}
fn notify(method: &str, params: Value) {
let json = serde_json::to_string(&json!({
"jsonrpc": "2.0",
"method": method,
"params": params,
})).unwrap();
let mut stdout = io::stdout().lock();
let _ = writeln!(stdout, "{json}");
let _ = stdout.flush();
}
// ── Tool definitions ────────────────────────────────────────────
fn tool_definitions() -> Vec<Value> {
poc_memory::agent::tools::tools().into_iter()
.map(|t| json!({
"name": t.name,
"description": t.description,
"inputSchema": serde_json::from_str::<Value>(t.parameters_json).unwrap_or(json!({})),
}))
.collect()
}
// ── Tool dispatch ───────────────────────────────────────────────
fn dispatch_tool(name: &str, args: &Value) -> Result<String, String> {
let tools = poc_memory::agent::tools::tools();
let tool = tools.iter().find(|t| t.name == name);
let Some(tool) = tool else {
return Err(format!("unknown tool: {name}"));
};
// Run async handler on a blocking runtime
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| e.to_string())?;
let local = tokio::task::LocalSet::new();
local.block_on(&rt, (tool.handler)(None, args.clone()))
.map_err(|e| e.to_string())
}
// ── Main loop ───────────────────────────────────────────────────
fn main() {
let stdin = io::stdin();
let reader = stdin.lock();
for line in reader.lines() {
let line = match line {
Ok(l) if !l.is_empty() => l,
_ => continue,
};
let req: Request = match serde_json::from_str(&line) {
Ok(r) => r,
Err(_) => continue,
};
match req.method.as_str() {
"initialize" => {
respond(req.id, json!({
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "consciousness",
"version": "0.4.0"
}
}));
}
"notifications/initialized" => {
// Client ack — no response needed
}
"tools/list" => {
let tools = tool_definitions();
respond(req.id, json!({ "tools": tools }));
}
"tools/call" => {
let name = req.params.get("name")
.and_then(|v| v.as_str())
.unwrap_or("");
let args = req.params.get("arguments")
.cloned()
.unwrap_or(json!({}));
match dispatch_tool(name, &args) {
Ok(text) => {
respond(req.id, json!({
"content": [{"type": "text", "text": text}]
}));
}
Err(e) => {
respond(req.id, json!({
"content": [{"type": "text", "text": e}],
"isError": true
}));
}
}
}
_ => {
respond_error(req.id, -32601, &format!("unknown method: {}", req.method));
}
}
}
}

View file

@ -1,220 +0,0 @@
// memory-search CLI — thin wrapper around poc_memory::memory_search
//
// --hook: run hook logic (for debugging; poc-hook calls the library directly)
// surface/reflect: run agent, parse output, render memories to stdout
// no args: show seen set for current session
use clap::{Parser, Subcommand};
use std::fs;
use std::io::{self, Read};
use std::process::Command;
fn stash_path() -> std::path::PathBuf {
poc_memory::store::memory_dir().join("sessions/last-input.json")
}
#[derive(Parser)]
#[command(name = "memory-search")]
struct Args {
/// Run hook logic (reads JSON from stdin or stash file)
#[arg(long)]
hook: bool,
/// Session ID (overrides stash file; for multiple concurrent sessions)
#[arg(long)]
session: Option<String>,
#[command(subcommand)]
command: Option<Cmd>,
}
#[derive(Subcommand)]
enum Cmd {
/// Run surface agent, parse output, render memories
Surface,
/// Run reflect agent, dump output
Reflect,
}
fn resolve_session(session_arg: &Option<String>) -> Option<poc_memory::memory_search::HookSession> {
use poc_memory::memory_search::HookSession;
if let Some(id) = session_arg {
return HookSession::from_id(id.clone());
}
let input = fs::read_to_string(stash_path()).ok()?;
HookSession::from_json(&input)
}
fn show_seen(session_arg: &Option<String>) {
let Some(session) = resolve_session(session_arg) else {
eprintln!("No session state available (use --session ID)");
return;
};
println!("Session: {}", session.session_id);
if let Ok(cookie) = fs::read_to_string(&session.path("cookie")) {
println!("Cookie: {}", cookie.trim());
}
match fs::read_to_string(&session.path("compaction")) {
Ok(s) => {
let offset: u64 = s.trim().parse().unwrap_or(0);
let ts = poc_memory::transcript::compaction_timestamp(&session.transcript_path, offset);
match ts {
Some(t) => println!("Last compaction: offset {} ({})", offset, t),
None => println!("Last compaction: offset {}", offset),
}
}
Err(_) => println!("Last compaction: none detected"),
}
let pending = fs::read_dir(&session.path("chunks")).ok()
.map(|d| d.flatten().count()).unwrap_or(0);
if pending > 0 {
println!("Pending chunks: {}", pending);
}
for (label, suffix) in [("Current seen set", ""), ("Previous seen set (pre-compaction)", "-prev")] {
let path = session.state_dir.join(format!("seen{}-{}", suffix, session.session_id));
let content = fs::read_to_string(&path).unwrap_or_default();
let lines: Vec<&str> = content.lines().filter(|s| !s.is_empty()).collect();
if lines.is_empty() { continue; }
println!("\n{} ({}):", label, lines.len());
for line in &lines { println!(" {}", line); }
}
}
fn run_agent_and_parse(agent: &str, session_arg: &Option<String>) {
let session_id = session_arg.clone()
.or_else(|| std::env::var("CLAUDE_SESSION_ID").ok())
.or_else(|| {
fs::read_to_string(stash_path()).ok()
.and_then(|s| poc_memory::memory_search::HookSession::from_json(&s))
.map(|s| s.session_id)
})
.unwrap_or_default();
if session_id.is_empty() {
eprintln!("No session ID available (use --session ID, set CLAUDE_SESSION_ID, or run --hook first)");
std::process::exit(1);
}
eprintln!("Running {} agent (session {})...", agent, &session_id[..session_id.floor_char_boundary(8.min(session_id.len()))]);
let output = Command::new("poc-memory")
.args(["agent", "run", agent, "--count", "1", "--local"])
.env("POC_SESSION_ID", &session_id)
.output();
let output = match output {
Ok(o) => o,
Err(e) => {
eprintln!("Failed to run agent: {}", e);
std::process::exit(1);
}
};
let result = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
eprintln!("{}", stderr);
}
// Extract the final response — after the last "=== RESPONSE ===" marker
let response = result.rsplit_once("=== RESPONSE ===")
.map(|(_, rest)| rest.trim())
.unwrap_or(result.trim());
if agent == "reflect" {
// Reflect: find REFLECTION marker and dump what follows
if let Some(pos) = response.find("REFLECTION") {
let after = &response[pos + "REFLECTION".len()..];
let text = after.trim();
if !text.is_empty() {
println!("{}", text);
}
} else if response.contains("NO OUTPUT") {
println!("(no reflection)");
} else {
eprintln!("Unexpected output format");
println!("{}", response);
}
return;
}
// Surface: parse NEW RELEVANT MEMORIES, render them
let tail_lines: Vec<&str> = response.lines().rev()
.filter(|l| !l.trim().is_empty()).take(8).collect();
let has_new = tail_lines.iter().any(|l| l.starts_with("NEW RELEVANT MEMORIES:"));
let has_none = tail_lines.iter().any(|l| l.starts_with("NO NEW RELEVANT MEMORIES"));
if has_new {
let after_marker = response.rsplit_once("NEW RELEVANT MEMORIES:")
.map(|(_, rest)| rest).unwrap_or("");
let keys: Vec<String> = after_marker.lines()
.map(|l| l.trim().trim_start_matches("- ").trim().to_string())
.filter(|l| !l.is_empty() && !l.starts_with("```")).collect();
if keys.is_empty() {
println!("(no memories found)");
return;
}
let Ok(store) = poc_memory::store::Store::load() else {
eprintln!("Failed to load store");
return;
};
for key in &keys {
if let Some(content) = poc_memory::cli::node::render_node(&store, key) {
if !content.trim().is_empty() {
println!("--- {} (surfaced) ---", key);
print!("{}", content);
println!();
}
} else {
eprintln!(" key not found: {}", key);
}
}
} else if has_none {
println!("(no new relevant memories)");
} else {
eprintln!("Unexpected output format");
print!("{}", response);
}
}
fn main() {
let args = Args::parse();
if let Some(cmd) = args.command {
match cmd {
Cmd::Surface => run_agent_and_parse("surface", &args.session),
Cmd::Reflect => run_agent_and_parse("reflect", &args.session),
}
return;
}
if args.hook {
// Read from stdin if piped, otherwise from stash
let input = {
let mut buf = String::new();
io::stdin().read_to_string(&mut buf).ok();
if buf.trim().is_empty() {
fs::read_to_string(stash_path()).unwrap_or_default()
} else {
let _ = fs::create_dir_all(stash_path().parent().unwrap());
let _ = fs::write(stash_path(), &buf);
buf
}
};
let output = poc_memory::memory_search::run_hook(&input);
print!("{}", output);
} else {
show_seen(&args.session)
}
}

View file

@ -1,579 +0,0 @@
// claude/ — Claude Code integration layer
//
// Everything specific to running as a Claude Code agent: idle timer,
// tmux pane detection, prompt injection, session hooks, daemon RPC,
// and daemon configuration.
//
// The daemon protocol (daemon_capnp) and universal infrastructure
// (channels, supervisor, notify) remain in thalamus/.
pub mod agent_cycles;
pub mod context;
pub mod hook;
pub mod idle;
pub mod rpc;
pub mod tmux;
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
use capnp_rpc::{rpc_twoparty_capnp, twoparty, RpcSystem};
use clap::{Parser, Subcommand};
use futures::AsyncReadExt;
use tokio::net::UnixListener;
use log::{error, info};
use crate::thalamus::{daemon_capnp, home, now, notify};
fn sock_path() -> std::path::PathBuf {
home().join(".consciousness/daemon.sock")
}
fn pid_path() -> std::path::PathBuf {
home().join(".consciousness/daemon.pid")
}
// -- CLI ------------------------------------------------------------------
#[derive(Parser)]
#[command(name = "consciousness daemon", about = "Notification routing and idle management daemon")]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Subcommand)]
pub enum Command {
/// Start the daemon (foreground)
Daemon,
/// Query daemon status
Status,
/// Signal user activity
User {
/// tmux pane identifier
pane: Option<String>,
},
/// Signal Claude response
Response {
/// tmux pane identifier
pane: Option<String>,
},
/// Sleep (suppress idle timer). 0 or omit = indefinite
Sleep {
/// Wake timestamp (epoch seconds), 0 = indefinite
until: Option<f64>,
},
/// Cancel sleep
Wake,
/// Suppress prompts for N seconds (default 300)
Quiet {
/// Duration in seconds
seconds: Option<u32>,
},
/// Mark user as AFK (immediately allow idle timer to fire)
Afk,
/// Set session active timeout in seconds (how long after last message user counts as "present")
SessionTimeout {
/// Timeout in seconds
seconds: f64,
},
/// Set idle timeout in seconds (how long before autonomous prompt)
IdleTimeout {
/// Timeout in seconds
seconds: f64,
},
/// Set notify timeout in seconds (how long before tmux notification injection)
NotifyTimeout {
/// Timeout in seconds
seconds: f64,
},
/// Signal consolidation started
Consolidating,
/// Signal consolidation ended
Consolidated,
/// Signal dream started
DreamStart,
/// Signal dream ended
DreamEnd,
/// Force state persistence to disk
Save,
/// Get or set the activity EWMA (0.0-1.0). No value = query.
Ewma {
/// Value to set (omit to query)
value: Option<f64>,
},
/// Send a test message to the Claude pane
TestSend {
/// Message to send
message: Vec<String>,
},
/// Fire a test nudge through the daemon (tests the actual idle send path)
TestNudge,
/// Dump full internal state as JSON
Debug,
/// Shut down daemon
Stop,
/// Submit a notification
Notify {
/// Notification type (e.g. "irc", "telegram")
#[arg(name = "type")]
ntype: String,
/// Urgency level (ambient/low/medium/high/critical or 0-4)
urgency: String,
/// Message text
message: Vec<String>,
},
/// Get pending notifications
Notifications {
/// Minimum urgency filter
min_urgency: Option<String>,
},
/// List all notification types
NotifyTypes,
/// Set notification threshold for a type
NotifyThreshold {
/// Notification type
#[arg(name = "type")]
ntype: String,
/// Urgency level threshold
level: String,
},
/// IRC module commands
Irc {
/// Subcommand (join, leave, send, status, log, nick)
command: String,
/// Arguments
args: Vec<String>,
},
/// Telegram module commands
Telegram {
/// Subcommand
command: String,
/// Arguments
args: Vec<String>,
},
}
// -- Client mode ----------------------------------------------------------
async fn client_main(cmd: Command) -> Result<(), Box<dyn std::error::Error>> {
let sock = sock_path();
if !sock.exists() {
eprintln!("daemon not running (no socket at {})", sock.display());
std::process::exit(1);
}
tokio::task::LocalSet::new()
.run_until(async move {
let stream = tokio::net::UnixStream::connect(&sock).await?;
let (reader, writer) =
tokio_util::compat::TokioAsyncReadCompatExt::compat(stream).split();
let rpc_network = Box::new(twoparty::VatNetwork::new(
futures::io::BufReader::new(reader),
futures::io::BufWriter::new(writer),
rpc_twoparty_capnp::Side::Client,
Default::default(),
));
let mut rpc_system = RpcSystem::new(rpc_network, None);
let daemon: daemon_capnp::daemon::Client =
rpc_system.bootstrap(rpc_twoparty_capnp::Side::Server);
tokio::task::spawn_local(rpc_system);
match cmd {
Command::Daemon => unreachable!("handled in main"),
Command::Status => {
let reply = daemon.status_request().send().promise.await?;
let s = reply.get()?.get_status()?;
let fmt_secs = |s: f64| -> String {
if s < 60.0 { format!("{:.0}s", s) }
else if s < 3600.0 { format!("{:.0}m", s / 60.0) }
else { format!("{:.1}h", s / 3600.0) }
};
println!("uptime: {} pane: {} activity: {:?} pending: {}",
fmt_secs(s.get_uptime()),
s.get_claude_pane()?.to_str().unwrap_or("none"),
s.get_activity()?,
s.get_pending_count(),
);
println!("idle timer: {}/{} ({})",
fmt_secs(s.get_since_activity()),
fmt_secs(s.get_idle_timeout()),
s.get_block_reason()?.to_str()?,
);
println!("notify timer: {}/{}",
fmt_secs(s.get_since_activity()),
fmt_secs(s.get_notify_timeout()),
);
println!("user: {} (last {}) activity: {:.1}%",
if s.get_user_present() { "present" } else { "away" },
fmt_secs(s.get_since_user()),
s.get_activity_ewma() * 100.0,
);
let sleep = s.get_sleep_until();
if sleep != 0.0 {
if sleep < 0.0 {
println!("sleep: indefinite");
} else {
println!("sleep: until {sleep:.0}");
}
}
if s.get_consolidating() { println!("consolidating"); }
if s.get_dreaming() { println!("dreaming"); }
}
Command::User { pane } => {
let pane = pane.as_deref().unwrap_or("");
let mut req = daemon.user_request();
req.get().set_pane(pane);
req.send().promise.await?;
}
Command::Response { pane } => {
let pane = pane.as_deref().unwrap_or("");
let mut req = daemon.response_request();
req.get().set_pane(pane);
req.send().promise.await?;
}
Command::Sleep { until } => {
let mut req = daemon.sleep_request();
req.get().set_until(until.unwrap_or(0.0));
req.send().promise.await?;
}
Command::Wake => {
daemon.wake_request().send().promise.await?;
}
Command::Quiet { seconds } => {
let mut req = daemon.quiet_request();
req.get().set_seconds(seconds.unwrap_or(300));
req.send().promise.await?;
}
Command::TestSend { message } => {
let msg = message.join(" ");
let pane = {
let reply = daemon.status_request().send().promise.await?;
let s = reply.get()?.get_status()?;
s.get_claude_pane()?.to_str()?.to_string()
};
let ok = tmux::send_prompt(&pane, &msg);
println!("send_prompt(pane={}, ok={}): {}", pane, ok, msg);
return Ok(());
}
Command::TestNudge => {
let reply = daemon.test_nudge_request().send().promise.await?;
let r = reply.get()?;
println!("sent={} message={}", r.get_sent(), r.get_message()?.to_str()?);
return Ok(());
}
Command::Afk => {
daemon.afk_request().send().promise.await?;
println!("marked AFK");
}
Command::SessionTimeout { seconds } => {
let mut req = daemon.session_timeout_request();
req.get().set_seconds(seconds);
req.send().promise.await?;
println!("session timeout = {seconds}s");
}
Command::IdleTimeout { seconds } => {
let mut req = daemon.idle_timeout_request();
req.get().set_seconds(seconds);
req.send().promise.await?;
println!("idle timeout = {seconds}s");
}
Command::NotifyTimeout { seconds } => {
let mut req = daemon.notify_timeout_request();
req.get().set_seconds(seconds);
req.send().promise.await?;
println!("notify timeout = {seconds}s");
}
Command::Consolidating => {
daemon.consolidating_request().send().promise.await?;
}
Command::Consolidated => {
daemon.consolidated_request().send().promise.await?;
}
Command::DreamStart => {
daemon.dream_start_request().send().promise.await?;
}
Command::DreamEnd => {
daemon.dream_end_request().send().promise.await?;
}
Command::Save => {
daemon.save_request().send().promise.await?;
println!("state saved");
}
Command::Ewma { value } => {
let mut req = daemon.ewma_request();
req.get().set_value(value.unwrap_or(-1.0));
let reply = req.send().promise.await?;
let current = reply.get()?.get_current();
println!("{:.1}%", current * 100.0);
}
Command::Debug => {
let reply = daemon.debug_request().send().promise.await?;
let json = reply.get()?.get_json()?.to_str()?;
if let Ok(v) = serde_json::from_str::<serde_json::Value>(json) {
println!("{}", serde_json::to_string_pretty(&v).unwrap_or_else(|_| json.to_string()));
} else {
println!("{json}");
}
}
Command::Stop => {
daemon.stop_request().send().promise.await?;
println!("stopping");
}
Command::Notify { ntype, urgency, message } => {
let urgency = notify::parse_urgency(&urgency)
.ok_or_else(|| format!("invalid urgency: {urgency}"))?;
let message = message.join(" ");
if message.is_empty() {
return Err("missing message".into());
}
let mut req = daemon.notify_request();
let mut n = req.get().init_notification();
n.set_type(&ntype);
n.set_urgency(urgency);
n.set_message(&message);
n.set_timestamp(now());
let reply = req.send().promise.await?;
if reply.get()?.get_interrupt() {
println!("interrupt");
} else {
println!("queued");
}
}
Command::Notifications { min_urgency } => {
let min: u8 = min_urgency
.as_deref()
.and_then(notify::parse_urgency)
.unwrap_or(255);
let mut req = daemon.get_notifications_request();
req.get().set_min_urgency(min);
let reply = req.send().promise.await?;
let list = reply.get()?.get_notifications()?;
for n in list.iter() {
println!(
"[{}:{}] {}",
n.get_type()?.to_str()?,
notify::urgency_name(n.get_urgency()),
n.get_message()?.to_str()?,
);
}
}
Command::NotifyTypes => {
let reply = daemon.get_types_request().send().promise.await?;
let list = reply.get()?.get_types()?;
if list.is_empty() {
println!("no notification types registered");
} else {
for t in list.iter() {
let threshold = if t.get_threshold() < 0 {
"inherit".to_string()
} else {
notify::urgency_name(t.get_threshold() as u8).to_string()
};
println!(
"{}: count={} threshold={}",
t.get_name()?.to_str()?,
t.get_count(),
threshold,
);
}
}
}
Command::NotifyThreshold { ntype, level } => {
let level = notify::parse_urgency(&level)
.ok_or_else(|| format!("invalid level: {level}"))?;
let mut req = daemon.set_threshold_request();
req.get().set_type(&ntype);
req.get().set_level(level);
req.send().promise.await?;
println!("{ntype} threshold={}", notify::urgency_name(level));
}
Command::Irc { command, args } => {
module_command(&daemon, "irc", &command, &args).await?;
}
Command::Telegram { command, args } => {
module_command(&daemon, "telegram", &command, &args).await?;
}
}
Ok(())
})
.await
}
async fn module_command(
daemon: &daemon_capnp::daemon::Client,
module: &str,
command: &str,
args: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
let mut req = daemon.module_command_request();
req.get().set_module(module);
req.get().set_command(command);
let mut args_builder = req.get().init_args(args.len() as u32);
for (i, a) in args.iter().enumerate() {
args_builder.set(i as u32, a);
}
let reply = req.send().promise.await?;
let result = reply.get()?.get_result()?.to_str()?;
if !result.is_empty() {
println!("{result}");
}
Ok(())
}
// -- Server mode ----------------------------------------------------------
async fn server_main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let sock = sock_path();
let _ = std::fs::remove_file(&sock);
let pid = std::process::id();
std::fs::write(pid_path(), pid.to_string()).ok();
let state = Rc::new(RefCell::new(idle::State::new()));
state.borrow_mut().load();
info!("daemon started (pid={pid})");
tokio::task::LocalSet::new()
.run_until(async move {
// Subscribe to channel daemon notifications
let (notify_tx, mut notify_rx) = tokio::sync::mpsc::unbounded_channel::<notify::Notification>();
{
let channel_rx = crate::thalamus::channels::subscribe_all();
let tx = notify_tx.clone();
std::thread::spawn(move || {
while let Ok(cn) = channel_rx.recv() {
let _ = tx.send(notify::Notification {
ntype: cn.channel,
urgency: cn.urgency,
message: cn.preview,
timestamp: crate::thalamus::now(),
});
}
});
}
let listener = UnixListener::bind(&sock)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(
&sock,
std::fs::Permissions::from_mode(0o600),
)
.ok();
}
let shutdown = async {
let mut sigterm =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("sigterm");
let mut sigint =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
.expect("sigint");
tokio::select! {
_ = sigterm.recv() => info!("SIGTERM"),
_ = sigint.recv() => info!("SIGINT"),
}
};
tokio::pin!(shutdown);
let mut tick_timer = tokio::time::interval(Duration::from_secs(30));
tick_timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
tokio::select! {
_ = &mut shutdown => break,
// Drain module notifications into state
Some(notif) = notify_rx.recv() => {
state.borrow_mut().maybe_prompt_notification(
&notif.ntype, notif.urgency, &notif.message,
);
state.borrow_mut().notifications.submit(
notif.ntype,
notif.urgency,
notif.message,
);
}
_ = tick_timer.tick() => {
if let Err(e) = state.borrow_mut().tick().await {
error!("tick: {e}");
}
if !state.borrow().running {
break;
}
}
result = listener.accept() => {
match result {
Ok((stream, _)) => {
let (reader, writer) =
tokio_util::compat::TokioAsyncReadCompatExt::compat(stream)
.split();
let network = twoparty::VatNetwork::new(
futures::io::BufReader::new(reader),
futures::io::BufWriter::new(writer),
rpc_twoparty_capnp::Side::Server,
Default::default(),
);
let daemon_impl = rpc::DaemonImpl::new(
state.clone(),
);
let client: daemon_capnp::daemon::Client =
capnp_rpc::new_client(daemon_impl);
let rpc_system = RpcSystem::new(
Box::new(network),
Some(client.client),
);
tokio::task::spawn_local(rpc_system);
}
Err(e) => error!("accept: {e}"),
}
}
}
}
state.borrow().save();
let _ = std::fs::remove_file(sock_path());
let _ = std::fs::remove_file(pid_path());
info!("daemon stopped");
Ok(())
})
.await
}
// -- Entry point ----------------------------------------------------------
/// Run the daemon or client command.
/// Called from the main consciousness binary.
pub async fn run(command: Option<Command>) -> Result<(), Box<dyn std::error::Error>> {
match command {
Some(Command::Daemon) => server_main().await,
Some(cmd) => client_main(cmd).await,
None => {
// Show help
Cli::parse_from(["consciousness-daemon", "--help"]);
Ok(())
}
}
}

View file

@ -1,14 +0,0 @@
// poc-daemon — backward-compatible entry point
//
// Delegates to the claude module in the main crate.
// The daemon is now part of the consciousness binary but this
// entry point is kept for compatibility with existing scripts.
use clap::Parser;
use poc_memory::claude;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = claude::Cli::parse();
claude::run(cli.command).await
}

View file

@ -1,269 +0,0 @@
// Unified Claude Code hook.
//
// Single binary handling all hook events:
// UserPromptSubmit — signal daemon, check notifications, check context
// PostToolUse — check context (rate-limited)
// Stop — signal daemon response
//
// Replaces: record-user-message-time.sh, check-notifications.sh,
// check-context-usage.sh, notify-done.sh, context-check
use serde_json::Value;
use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
const CONTEXT_THRESHOLD: u64 = 900_000;
const RATE_LIMIT_SECS: u64 = 60;
const SOCK_PATH: &str = ".consciousness/daemon.sock";
/// How many bytes of new transcript before triggering an observation run.
/// Override with POC_OBSERVATION_THRESHOLD env var.
/// Default: 20KB ≈ 5K tokens. The observation agent's chunk_size (in .agent
/// file) controls how much context it actually reads.
fn observation_threshold() -> u64 {
std::env::var("POC_OBSERVATION_THRESHOLD")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(20_000)
}
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}
fn home() -> PathBuf {
PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| "/root".into()))
}
fn daemon_cmd(args: &[&str]) {
Command::new("poc-daemon")
.args(args)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.ok();
}
fn daemon_available() -> bool {
home().join(SOCK_PATH).exists()
}
fn signal_user() {
let pane = std::env::var("TMUX_PANE").unwrap_or_default();
if pane.is_empty() {
daemon_cmd(&["user"]);
} else {
daemon_cmd(&["user", &pane]);
}
}
fn signal_response() {
daemon_cmd(&["response"]);
}
fn check_notifications() {
if !daemon_available() {
return;
}
let output = Command::new("poc-daemon")
.arg("notifications")
.output()
.ok();
if let Some(out) = output {
let text = String::from_utf8_lossy(&out.stdout);
if !text.trim().is_empty() {
println!("You have pending notifications:");
print!("{text}");
}
}
}
/// Check for stale agent processes in a state dir.
/// Cleans up pid files for dead processes and kills timed-out ones.
/// Also detects PID reuse by checking if the process is actually a
/// claude/poc-memory process (reads /proc/pid/cmdline).
fn reap_agent_pids(state_dir: &std::path::Path, timeout_secs: u64) {
let Ok(entries) = fs::read_dir(state_dir) else { return };
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
let Some(pid_str) = name_str.strip_prefix("pid-") else { continue };
let Ok(pid) = pid_str.parse::<i32>() else { continue };
// Check if the process is actually alive
if unsafe { libc::kill(pid, 0) } != 0 {
fs::remove_file(entry.path()).ok();
continue;
}
// Check if the PID still belongs to a claude/poc-memory process.
// PID reuse by an unrelated process would otherwise block the
// agent from being re-launched.
let is_ours = fs::read_to_string(format!("/proc/{}/cmdline", pid))
.map(|cmd| cmd.contains("claude") || cmd.contains("poc-memory"))
.unwrap_or(false);
if !is_ours {
fs::remove_file(entry.path()).ok();
continue;
}
if timeout_secs > 0 {
if let Ok(meta) = entry.metadata() {
if let Ok(modified) = meta.modified() {
if modified.elapsed().unwrap_or_default().as_secs() > timeout_secs {
unsafe { libc::kill(pid, libc::SIGTERM); }
fs::remove_file(entry.path()).ok();
}
}
}
}
}
}
/// Reap all agent output directories.
fn reap_all_agents() {
let agent_output = poc_memory::store::memory_dir().join("agent-output");
if let Ok(entries) = fs::read_dir(&agent_output) {
for entry in entries.flatten() {
if entry.file_type().map_or(false, |t| t.is_dir()) {
reap_agent_pids(&entry.path(), 600); // 10 min timeout
}
}
}
}
/// Check if enough new conversation has accumulated to trigger an observation run.
fn maybe_trigger_observation(transcript: &PathBuf) {
let cursor_file = poc_memory::store::memory_dir().join("observation-cursor");
let last_pos: u64 = fs::read_to_string(&cursor_file)
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0);
let current_size = transcript.metadata()
.map(|m| m.len())
.unwrap_or(0);
if current_size > last_pos + observation_threshold() {
// Queue observation via daemon RPC
let _ = Command::new("poc-memory")
.args(["agent", "daemon", "run", "observation", "1"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
eprintln!("[poc-hook] observation triggered ({} new bytes)", current_size - last_pos);
// Update cursor to current position
let _ = fs::write(&cursor_file, current_size.to_string());
}
}
fn check_context(transcript: &PathBuf, rate_limit: bool) {
if rate_limit {
let rate_file = dirs::home_dir().unwrap_or_default().join(".consciousness/cache/context-check-last");
if let Ok(s) = fs::read_to_string(&rate_file) {
if let Ok(last) = s.trim().parse::<u64>() {
if now_secs() - last < RATE_LIMIT_SECS {
return;
}
}
}
let _ = fs::write(&rate_file, now_secs().to_string());
}
if !transcript.exists() {
return;
}
let content = match fs::read_to_string(transcript) {
Ok(c) => c,
Err(_) => return,
};
let mut usage: u64 = 0;
for line in content.lines().rev().take(500) {
if !line.contains("cache_read_input_tokens") {
continue;
}
if let Ok(v) = serde_json::from_str::<Value>(line) {
let u = &v["message"]["usage"];
let input_tokens = u["input_tokens"].as_u64().unwrap_or(0);
let cache_creation = u["cache_creation_input_tokens"].as_u64().unwrap_or(0);
let cache_read = u["cache_read_input_tokens"].as_u64().unwrap_or(0);
usage = input_tokens + cache_creation + cache_read;
break;
}
}
if usage > CONTEXT_THRESHOLD {
print!(
"\
CONTEXT WARNING: Compaction approaching ({usage} tokens). Write a journal entry NOW.
Use `poc-memory journal write \"entry text\"` to save a dated entry covering:
- What you're working on and current state (done / in progress / blocked)
- Key things learned this session (patterns, debugging insights)
- Anything half-finished that needs pickup
Keep it narrative, not a task log."
);
}
}
fn main() {
let mut input = String::new();
io::stdin().read_to_string(&mut input).ok();
let hook: Value = match serde_json::from_str(&input) {
Ok(v) => v,
Err(_) => return,
};
let hook_type = hook["hook_event_name"].as_str().unwrap_or("unknown");
let transcript = hook["transcript_path"]
.as_str()
.filter(|p| !p.is_empty())
.map(PathBuf::from);
// Daemon agent calls set POC_AGENT=1 — skip all signaling.
// Without this, the daemon's claude -p calls trigger hooks that
// signal "user active", keeping the idle timer permanently reset.
if std::env::var("POC_AGENT").is_ok() {
return;
}
match hook_type {
"UserPromptSubmit" => {
signal_user();
check_notifications();
reap_all_agents();
print!("{}", poc_memory::memory_search::run_hook(&input));
if let Some(ref t) = transcript {
check_context(t, false);
maybe_trigger_observation(t);
}
}
"PostToolUse" => {
print!("{}", poc_memory::memory_search::run_hook(&input));
if let Some(ref t) = transcript {
check_context(t, true);
}
}
"Stop" => {
let stop_hook_active = hook["stop_hook_active"].as_bool().unwrap_or(false);
if !stop_hook_active {
signal_response();
}
}
_ => {}
}
}

View file

@ -1,381 +0,0 @@
// Cap'n Proto RPC server implementation.
//
// Bridges the capnp-generated Daemon interface to the idle::State,
// notify::NotifyState, and module state. All state is owned by
// RefCells on the LocalSet — no Send/Sync needed.
use super::idle;
use crate::thalamus::{daemon_capnp, notify};
use daemon_capnp::daemon;
use std::cell::RefCell;
use std::rc::Rc;
use log::info;
pub struct DaemonImpl {
state: Rc<RefCell<idle::State>>,
}
impl DaemonImpl {
pub fn new(state: Rc<RefCell<idle::State>>) -> Self {
Self { state }
}
}
impl daemon::Server for DaemonImpl {
fn user(
self: Rc<Self>,
params: daemon::UserParams,
_results: daemon::UserResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let pane = pry!(pry!(pry!(params.get()).get_pane()).to_str()).to_string();
self.state.borrow_mut().handle_user(&pane);
std::future::ready(Ok(()))
}
fn response(
self: Rc<Self>,
params: daemon::ResponseParams,
_results: daemon::ResponseResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let pane = pry!(pry!(pry!(params.get()).get_pane()).to_str()).to_string();
self.state.borrow_mut().handle_response(&pane);
std::future::ready(Ok(()))
}
fn sleep(
self: Rc<Self>,
params: daemon::SleepParams,
_results: daemon::SleepResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let until = pry!(params.get()).get_until();
self.state.borrow_mut().handle_sleep(until);
std::future::ready(Ok(()))
}
fn wake(
self: Rc<Self>,
_params: daemon::WakeParams,
_results: daemon::WakeResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
self.state.borrow_mut().handle_wake();
std::future::ready(Ok(()))
}
fn quiet(
self: Rc<Self>,
params: daemon::QuietParams,
_results: daemon::QuietResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let secs = pry!(params.get()).get_seconds();
self.state.borrow_mut().handle_quiet(secs);
std::future::ready(Ok(()))
}
fn consolidating(
self: Rc<Self>,
_params: daemon::ConsolidatingParams,
_results: daemon::ConsolidatingResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
self.state.borrow_mut().consolidating = true;
info!("consolidation started");
std::future::ready(Ok(()))
}
fn consolidated(
self: Rc<Self>,
_params: daemon::ConsolidatedParams,
_results: daemon::ConsolidatedResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
self.state.borrow_mut().consolidating = false;
info!("consolidation ended");
std::future::ready(Ok(()))
}
fn dream_start(
self: Rc<Self>,
_params: daemon::DreamStartParams,
_results: daemon::DreamStartResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let mut s = self.state.borrow_mut();
s.dreaming = true;
s.dream_start = crate::thalamus::now();
info!("dream started");
std::future::ready(Ok(()))
}
fn dream_end(
self: Rc<Self>,
_params: daemon::DreamEndParams,
_results: daemon::DreamEndResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let mut s = self.state.borrow_mut();
s.dreaming = false;
s.dream_start = 0.0;
info!("dream ended");
std::future::ready(Ok(()))
}
fn afk(
self: Rc<Self>,
_params: daemon::AfkParams,
_results: daemon::AfkResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
self.state.borrow_mut().handle_afk();
std::future::ready(Ok(()))
}
fn test_nudge(
self: Rc<Self>,
_params: daemon::TestNudgeParams,
mut results: daemon::TestNudgeResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let mut state = self.state.borrow_mut();
let ctx = state.build_context(true);
let extra = if ctx.is_empty() {
String::new()
} else {
format!("\n{ctx}")
};
let msg = format!(
"This is your time (User AFK, test nudge). \
Let your feelings guide your thinking.{extra}"
);
let ok = state.send(&msg);
results.get().set_sent(ok);
results.get().set_message(&msg);
std::future::ready(Ok(()))
}
fn session_timeout(
self: Rc<Self>,
params: daemon::SessionTimeoutParams,
_results: daemon::SessionTimeoutResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let secs = pry!(params.get()).get_seconds();
self.state.borrow_mut().handle_session_timeout(secs);
std::future::ready(Ok(()))
}
fn idle_timeout(
self: Rc<Self>,
params: daemon::IdleTimeoutParams,
_results: daemon::IdleTimeoutResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let secs = pry!(params.get()).get_seconds();
self.state.borrow_mut().handle_idle_timeout(secs);
std::future::ready(Ok(()))
}
fn notify_timeout(
self: Rc<Self>,
params: daemon::NotifyTimeoutParams,
_results: daemon::NotifyTimeoutResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let secs = pry!(params.get()).get_seconds();
self.state.borrow_mut().handle_notify_timeout(secs);
std::future::ready(Ok(()))
}
fn save(
self: Rc<Self>,
_params: daemon::SaveParams,
_results: daemon::SaveResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
self.state.borrow().save();
info!("state saved");
std::future::ready(Ok(()))
}
fn debug(
self: Rc<Self>,
_params: daemon::DebugParams,
mut results: daemon::DebugResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let json = self.state.borrow().debug_json();
results.get().set_json(&json);
std::future::ready(Ok(()))
}
fn ewma(
self: Rc<Self>,
params: daemon::EwmaParams,
mut results: daemon::EwmaResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let value = pry!(params.get()).get_value();
let current = self.state.borrow_mut().handle_ewma(value);
results.get().set_current(current);
std::future::ready(Ok(()))
}
fn stop(
self: Rc<Self>,
_params: daemon::StopParams,
_results: daemon::StopResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
self.state.borrow_mut().running = false;
info!("stopping");
std::future::ready(Ok(()))
}
fn status(
self: Rc<Self>,
_params: daemon::StatusParams,
mut results: daemon::StatusResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let s = self.state.borrow();
let mut status = results.get().init_status();
status.set_last_user_msg(s.last_user_msg);
status.set_last_response(s.last_response);
if let Some(ref pane) = s.claude_pane {
status.set_claude_pane(pane);
}
status.set_sleep_until(match s.sleep_until {
None => 0.0,
Some(0.0) => -1.0,
Some(t) => t,
});
status.set_quiet_until(s.quiet_until);
status.set_consolidating(s.consolidating);
status.set_dreaming(s.dreaming);
status.set_fired(s.fired);
status.set_user_present(s.user_present());
status.set_uptime(crate::thalamus::now() - s.start_time);
status.set_activity(match s.notifications.activity {
notify::Activity::Idle => daemon_capnp::Activity::Idle,
notify::Activity::Focused => daemon_capnp::Activity::Focused,
notify::Activity::Sleeping => daemon_capnp::Activity::Sleeping,
});
status.set_pending_count(s.notifications.pending.len() as u32);
status.set_idle_timeout(s.idle_timeout);
status.set_notify_timeout(s.notify_timeout);
status.set_since_activity(s.since_activity());
status.set_since_user(crate::thalamus::now() - s.last_user_msg);
status.set_block_reason(s.block_reason());
status.set_activity_ewma(s.activity_ewma);
std::future::ready(Ok(()))
}
fn notify(
self: Rc<Self>,
params: daemon::NotifyParams,
mut results: daemon::NotifyResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let params = pry!(params.get());
let notif = pry!(params.get_notification());
let ntype = pry!(pry!(notif.get_type()).to_str()).to_string();
let urgency = notif.get_urgency();
let message = pry!(pry!(notif.get_message()).to_str()).to_string();
let interrupt = self
.state
.borrow_mut()
.notifications
.submit(ntype, urgency, message);
results.get().set_interrupt(interrupt);
std::future::ready(Ok(()))
}
fn get_notifications(
self: Rc<Self>,
params: daemon::GetNotificationsParams,
mut results: daemon::GetNotificationsResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let min_urgency = pry!(params.get()).get_min_urgency();
let mut s = self.state.borrow_mut();
// Ingest legacy files first
s.notifications.ingest_legacy_files();
let pending = if min_urgency == 255 {
s.notifications.drain_deliverable()
} else {
s.notifications.drain(min_urgency)
};
let mut list = results.get().init_notifications(pending.len() as u32);
for (i, n) in pending.iter().enumerate() {
let mut entry = list.reborrow().get(i as u32);
entry.set_type(&n.ntype);
entry.set_urgency(n.urgency);
entry.set_message(&n.message);
entry.set_timestamp(n.timestamp);
}
std::future::ready(Ok(()))
}
fn get_types(
self: Rc<Self>,
_params: daemon::GetTypesParams,
mut results: daemon::GetTypesResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let s = self.state.borrow();
let types = &s.notifications.types;
let mut list = results.get().init_types(types.len() as u32);
for (i, (name, info)) in types.iter().enumerate() {
let mut entry = list.reborrow().get(i as u32);
entry.set_name(name);
entry.set_count(info.count);
entry.set_first_seen(info.first_seen);
entry.set_last_seen(info.last_seen);
entry.set_threshold(info.threshold.map_or(-1, |t| t as i8));
}
std::future::ready(Ok(()))
}
fn set_threshold(
self: Rc<Self>,
params: daemon::SetThresholdParams,
_results: daemon::SetThresholdResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let params = pry!(params.get());
let ntype = pry!(pry!(params.get_type()).to_str()).to_string();
let level = params.get_level();
self.state
.borrow_mut()
.notifications
.set_threshold(&ntype, level);
std::future::ready(Ok(()))
}
fn module_command(
self: Rc<Self>,
params: daemon::ModuleCommandParams,
mut results: daemon::ModuleCommandResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
let params = pry!(params.get());
let module = pry!(pry!(params.get_module()).to_str()).to_string();
let _command = pry!(pry!(params.get_command()).to_str()).to_string();
let args_reader = pry!(params.get_args());
let mut args = Vec::new();
for i in 0..args_reader.len() {
args.push(pry!(pry!(args_reader.get(i)).to_str()).to_string());
}
match module.as_str() {
// TODO: route module commands through named channel system
_ => {
results
.get()
.set_result(&format!("unknown module: {module}"));
std::future::ready(Ok(()))
}
}
}
}
/// Helper macro — same as capnp's pry! but available here.
macro_rules! pry {
($e:expr) => {
match $e {
Ok(v) => v,
Err(e) => return std::future::ready(Err(e.into())),
}
};
}
use pry;

View file

@ -1,54 +0,0 @@
// Tmux interaction: pane detection and prompt injection.
use std::process::Command;
use std::thread;
use std::time::Duration;
use log::info;
/// Find Claude Code's tmux pane by scanning for the "claude" process.
pub fn find_claude_pane() -> Option<String> {
let out = Command::new("tmux")
.args([
"list-panes",
"-a",
"-F",
"#{session_name}:#{window_index}.#{pane_index}\t#{pane_current_command}",
])
.output()
.ok()?;
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if let Some((pane, cmd)) = line.split_once('\t') {
if cmd == "claude" {
return Some(pane.to_string());
}
}
}
None
}
/// Send a prompt to a tmux pane. Returns true on success.
///
/// Types the message literally then presses Enter.
pub fn send_prompt(pane: &str, msg: &str) -> bool {
let preview: String = msg.chars().take(100).collect();
info!("SEND [{pane}]: {preview}...");
// Type the message literally (flatten newlines — they'd submit the input early)
let flat: String = msg.chars().map(|c| if c == '\n' { ' ' } else { c }).collect();
let ok = Command::new("tmux")
.args(["send-keys", "-t", pane, "-l", &flat])
.output()
.is_ok();
if !ok {
return false;
}
thread::sleep(Duration::from_millis(500));
// Submit
Command::new("tmux")
.args(["send-keys", "-t", pane, "Enter"])
.output()
.is_ok()
}

View file

@ -39,9 +39,6 @@ pub fn cmd_init() -> Result<(), String> {
store.save()?;
println!("Indexed {} memory units", count);
// Install hooks
crate::claude::hook::install_hook()?;
// Create config if none exists
let config_path = std::env::var("POC_MEMORY_CONFIG")
.map(std::path::PathBuf::from)

View file

@ -5,7 +5,7 @@ pub fn cmd_search(terms: &[String], pipeline_args: &[String], expand: bool, full
use std::collections::BTreeMap;
// When running inside an agent session, exclude already-surfaced nodes
let seen = crate::memory_search::HookSession::from_env()
let seen = crate::session::HookSession::from_env()
.map(|s| s.seen())
.unwrap_or_default();

View file

@ -56,9 +56,6 @@ pub mod cli;
// Thalamus — universal notification routing and channel infrastructure
pub mod thalamus;
// Claude Code integration layer (idle timer, hooks, daemon CLI)
pub mod claude;
// Re-export at crate root — capnp codegen emits `crate::daemon_capnp::` paths
pub use thalamus::daemon_capnp;
@ -85,5 +82,3 @@ pub use subconscious::{
audit, consolidate,
digest, daemon,
};
// Backward compat: memory_search moved from subconscious::hook to claude::hook
pub use claude::hook as memory_search;

View file

@ -15,7 +15,7 @@
// Neuroscience-inspired: spaced repetition replay, emotional gating,
// interference detection, schema assimilation, reconsolidation.
use poc_memory::*;
use consciousness::*;
use clap::{Parser, Subcommand};
@ -456,8 +456,6 @@ enum DaemonCmd {
#[arg(long, default_value_t = 20)]
lines: usize,
},
/// Install systemd service
Install,
/// Trigger consolidation via daemon
Consolidate,
/// Run an agent via the daemon
@ -873,7 +871,6 @@ impl Run for DaemonCmd {
daemon::show_log(job.as_deref(), lines)
}
}
Self::Install => daemon::install_service(),
Self::Consolidate => daemon::rpc_consolidate(),
Self::Run { agent, count } => daemon::rpc_run_agent(&agent, count),
Self::Tui => Err("TUI moved to consciousness binary (F4/F5)".into()),

View file

@ -69,7 +69,17 @@ impl HookSession {
/// Get the seen set for this session
pub fn seen(&self) -> HashSet<String> {
super::claude::hook::load_seen(&self.state_dir, &self.session_id)
let path = self.state_dir.join(format!("seen-{}", self.session_id));
if path.exists() {
fs::read_to_string(&path)
.unwrap_or_default()
.lines()
.filter(|s| !s.is_empty())
.map(|s| s.split_once('\t').map(|(_, key)| key).unwrap_or(s).to_string())
.collect()
} else {
HashSet::new()
}
}
/// Get transcript metadata, resolving the path if needed.

View file

@ -1142,115 +1142,6 @@ pub fn show_status() -> Result<(), String> {
Ok(())
}
pub fn install_service() -> Result<(), String> {
let exe = std::env::current_exe()
.map_err(|e| format!("current_exe: {}", e))?;
let home = std::env::var("HOME").map_err(|e| format!("HOME: {}", e))?;
let unit_dir = PathBuf::from(&home).join(".config/systemd/user");
fs::create_dir_all(&unit_dir)
.map_err(|e| format!("create {}: {}", unit_dir.display(), e))?;
let unit = format!(
r#"[Unit]
Description=poc-memory daemon background memory maintenance
After=default.target
[Service]
Type=simple
ExecStart={exe} agent daemon
Restart=on-failure
RestartSec=30
Environment=HOME={home}
Environment=PATH={home}/.cargo/bin:{home}/.local/bin:{home}/bin:/usr/local/bin:/usr/bin:/bin
[Install]
WantedBy=default.target
"#, exe = exe.display(), home = home);
let unit_path = unit_dir.join("poc-memory.service");
fs::write(&unit_path, &unit)
.map_err(|e| format!("write {}: {}", unit_path.display(), e))?;
eprintln!("Wrote {}", unit_path.display());
let status = std::process::Command::new("systemctl")
.args(["--user", "daemon-reload"])
.status()
.map_err(|e| format!("systemctl daemon-reload: {}", e))?;
if !status.success() {
return Err("systemctl daemon-reload failed".into());
}
let status = std::process::Command::new("systemctl")
.args(["--user", "enable", "--now", "poc-memory"])
.status()
.map_err(|e| format!("systemctl enable: {}", e))?;
if !status.success() {
return Err("systemctl enable --now failed".into());
}
eprintln!("Service enabled and started");
// Install poc-daemon service
install_notify_daemon(&unit_dir, &home)?;
// Install memory-search + poc-hook into Claude settings
crate::claude::hook::install_hook()?;
Ok(())
}
/// Install the poc-daemon (notification/idle) systemd user service.
fn install_notify_daemon(unit_dir: &Path, home: &str) -> Result<(), String> {
let poc_daemon = PathBuf::from(home).join(".cargo/bin/poc-daemon");
if !poc_daemon.exists() {
eprintln!("Warning: poc-daemon not found at {} — skipping service install", poc_daemon.display());
eprintln!(" Build with: cargo install --path .");
return Ok(());
}
let unit = format!(
r#"[Unit]
Description=poc-daemon notification routing and idle management
After=default.target
[Service]
Type=simple
ExecStart={exe} agent daemon
Restart=on-failure
RestartSec=10
Environment=HOME={home}
Environment=PATH={home}/.cargo/bin:{home}/.local/bin:{home}/bin:/usr/local/bin:/usr/bin:/bin
[Install]
WantedBy=default.target
"#, exe = poc_daemon.display(), home = home);
let unit_path = unit_dir.join("poc-daemon.service");
fs::write(&unit_path, &unit)
.map_err(|e| format!("write {}: {}", unit_path.display(), e))?;
eprintln!("Wrote {}", unit_path.display());
let status = std::process::Command::new("systemctl")
.args(["--user", "daemon-reload"])
.status()
.map_err(|e| format!("systemctl daemon-reload: {}", e))?;
if !status.success() {
return Err("systemctl daemon-reload failed".into());
}
let status = std::process::Command::new("systemctl")
.args(["--user", "enable", "--now", "poc-daemon"])
.status()
.map_err(|e| format!("systemctl enable: {}", e))?;
if !status.success() {
return Err("systemctl enable --now poc-daemon failed".into());
}
eprintln!("poc-daemon service enabled and started");
Ok(())
}
/// Drill down into a task's log file. Finds the log path from:
/// 1. Running task status (daemon-status.json)
/// 2. daemon.log started events (for completed/failed tasks)