Watch
1
0
Fork
You've already forked souveraine
0

memfs_commit presence signal on the bus after memory mutations

One event per memfs write/append/delete, replacing the three bespoke
memory_* events nobody consumed. Payload carries op, agent pubkey (seed
beside memfs), machine instance (machined-first), paths, head commit,
branch — enough for a federated peer to know to fetch. Best-effort:
missing identity warns and ships null, never blocks the write.
MemoryRepo stays a pure store; grew head_commit_hex/current_branch.
This commit is contained in:
Fimeg 2026-07-16 11:07:32 -04:00
commit 8cfa992da2

View file

@ -539,6 +539,20 @@ impl MemoryRepo {
Ok(())
}
/// Hex hash of the current HEAD commit, if the repo has one.
pub fn head_commit_hex(&self) -> Option<String> {
let repo = self.open_git().ok()?;
let head = repo.head().ok()?;
head.peel_to_commit().ok().map(|c| c.id().to_string())
}
/// Branch shorthand HEAD points at ("main", "primary", ...), if any.
pub fn current_branch(&self) -> Option<String> {
let repo = self.open_git().ok()?;
let head = repo.head().ok()?;
head.shorthand().map(|s| s.to_string())
}
/// Get the root path of the memory repo.
pub fn root(&self) -> &Path {
&self.root
@ -626,6 +640,75 @@ pub async fn read_file(path: &Path) -> Result<MemoryFile> {
// ── Tool Interface ─────────────────────────────────────────────────────────
/// Fire a `memfs_commit` presence signal after a memfs mutation.
///
/// This is the federation heartbeat for memory: "I wrote, you should fetch."
/// It carries routing metadata only — never the data itself. Peers holding a
/// checkout of the same agent map `agent_pubkey` to their local repo and
/// fetch the shared remote (FEDERATION.md, memory transport).
///
/// Emission is best-effort and never fails the write: the mutation already
/// succeeded. Missing identity is warned loudly, not fabricated.
fn fire_memfs_commit(
ctx: &ToolContext,
repo: &MemoryRepo,
op: &str,
paths: &[&str],
urgency: f32,
) {
// A successful tool-path mutation auto-committed; if there is somehow no
// head, there is nothing for a peer to fetch — do not signal.
let Some(commit) = repo.head_commit_hex() else {
tracing::warn!(op, ?paths, "memfs mutation without a head commit — memfs_commit not emitted");
return;
};
let branch = repo.current_branch();
// Agent identity lives beside the memfs (`agents/{id}/seed/`) — same
// convention as reach/consult in tools/agent.rs.
let agent_pubkey = ctx
.memory_root
.as_ref()
.and_then(|m| m.parent())
.map(|p| p.join("seed"))
.and_then(|dir| match crate::core::identity::SeedId::load_or_generate(&dir) {
Ok(seed) => Some(seed.public_key_hex()),
Err(e) => {
tracing::warn!(op, "memfs_commit: agent seed unavailable ({e:#})");
None
}
});
// The instance is the machine that wrote — machined-first, loud legacy
// fallback, never generates.
let base = dirs::home_dir().unwrap_or_default().join(".souveraine");
let instance = match crate::machined::client::machine_pubkey_with_fallback(&base) {
Ok((pk, _source)) => Some(pk),
Err(e) => {
tracing::warn!(op, "memfs_commit: machine identity unavailable ({e:#})");
None
}
};
ctx.fire_event(crate::core::nervous::SensorEvent {
sensor_name: "memory".into(),
timestamp: chrono::Utc::now(),
event_type: "memfs_commit".into(),
target: paths.first().map(|p| p.to_string()),
urgency,
payload: Some(serde_json::json!({
"op": op,
"agent_pubkey": agent_pubkey,
"instance": instance,
"paths": paths,
"commit": commit,
"branch": branch,
})),
seed_id: None,
reply_to: None,
});
}
/// Execute a memory command, optionally using context for agent identity.
///
/// When `ctx` is `Some` and carries an `agent_id`, that takes precedence over
@ -668,32 +751,14 @@ pub async fn execute_memory_command_with_context(
MemoryCommand::Write { path, content } => {
repo.write(path, content).await?;
if let Some(c) = ctx {
c.fire_event(crate::core::nervous::SensorEvent {
sensor_name: "memory".into(),
timestamp: chrono::Utc::now(),
event_type: "memory_write".into(),
target: Some(path.clone()),
urgency: 0.1,
payload: None,
seed_id: None,
reply_to: None,
});
fire_memfs_commit(c, &repo, "write", &[path], 0.1);
}
Ok(format!("Wrote memory file: {}", path))
}
MemoryCommand::Append { path, content } => {
repo.append(path, content).await?;
if let Some(c) = ctx {
c.fire_event(crate::core::nervous::SensorEvent {
sensor_name: "memory".into(),
timestamp: chrono::Utc::now(),
event_type: "memory_append".into(),
target: Some(path.clone()),
urgency: 0.1,
payload: None,
seed_id: None,
reply_to: None,
});
fire_memfs_commit(c, &repo, "append", &[path], 0.1);
}
Ok(format!("Appended to memory file: {}", path))
}
@ -753,16 +818,7 @@ pub async fn execute_memory_command_with_context(
MemoryCommand::Delete { path } => {
repo.delete(path).await?;
if let Some(c) = ctx {
c.fire_event(crate::core::nervous::SensorEvent {
sensor_name: "memory".into(),
timestamp: chrono::Utc::now(),
event_type: "memory_delete".into(),
target: Some(path.clone()),
urgency: 0.2,
payload: None,
seed_id: None,
reply_to: None,
});
fire_memfs_commit(c, &repo, "delete", &[path], 0.2);
}
Ok(format!("Deleted memory file: {}", path))
}