fix: char-boundary panic in tool cards + green up CI clippy gate
ToolCard styled the status glyph with a hardcoded byte offset 0..3, but
✓/✗/⟳ are 3-byte chars sitting at bytes 2..5 of " {glyph}". The split
landed mid-char and panicked at render in tuie's style slicing. Compute
the glyph region as 2 + glyph.len_utf8() instead.
Then took cargo clippy -- -D warnings from 312 failures to clean:
- scoped #![allow(dead_code)] on WIP scaffolding (federation, sensorium,
gitea_memory, model_router, session, subagent…); gate stays live on
active code so new orphans still fail
- scoped #![allow(deprecated)] on the legacy ratatui render path, marked
pending removal at tuie parity — no migration on code we're deleting
- declare the gui feature (forwards to tuie/gui) — the cfg was real intent
- real fixes: duplicate SaveAndGoBack arm + dead Err arm, base64::encode,
4 unused imports, dead assignment, private-type leak, dedup'd if/else
branches, manual clamp/strip, &PathBuf→&Path, collapsible matches
2026-06-26 10:12:27 -04:00
#![ allow(dead_code) ] // WIP scaffolding not yet wired
2026-05-07 02:31:44 -04:00
//! Memory — The agent's git-backed, frontmatter-aware memory filesystem.
//!
//! Every agent has a memory directory at `~/.souveraine/agents/{id}/memory/`
//! containing markdown files with YAML frontmatter, tracked in git.
//!
//! The `memory` tool exposes this to the agent as a unified subcommand interface:
//!
//! ```text
//! memory read system/persona
//! memory write system/persona "new content"
//! memory append journal/2026-05-06 "new entry"
//! memory ls system/
//! memory init
//! memory status
//! memory compact --strategy sliding-window
//! ```
//!
2026-05-20 15:12:21 -04:00
//! Design:
2026-05-07 02:31:44 -04:00
//! - All files require YAML frontmatter with `description`
//! - `read_only: true` in frontmatter blocks writes
//! - Every write is a git commit (auto-commit)
//! - Paths are relative to the agent's memory directory
use anyhow ::{ anyhow , Context , Result } ;
use serde ::{ Deserialize , Serialize } ;
use std ::path ::{ Path , PathBuf } ;
feat: sensorium architecture — 8 sensors, subagent runner, TUI component system
Sensorium (src/core/tools/):
- defs.rs: Tool trait, ToolContext (resolve_path, is_memory_path), ToolError with 7 constructors
- read/write/edit: memory boundary with force override, line ranges, replace_all
- bash: stateful (Arc<Mutex<BashState>>), background tasks, timeout
- glob/grep/list_dir: gitignore respect, context lines, memory boundary
- mod.rs: Sensorium registry + backward compat wrappers
- subagent.rs: fork-of-self tool, dual-state framing, no hard depth guard
- agent.rs: Agent(Sam) summoning stub
Dual-state N+1:
- ConsciousnessEngine::on_response_for_agent() — runs heuristic detector on subagent response
- Observations flow back to parent agent's inbox
- Signaled limits: SubagentConfig (max_depth, max_tool_rounds=50, warning thresholds 0.8/0.95)
Architecture docs:
- SENSORIUM_ARCHITECTURE.md — full sensorium vision with nervous system scaffold
- DECISIONS.md — settled architectural decisions
- ASTER_ARCHITECTURE.md / CONSCIOUSNESS_CYCLE.md — reframe appendices
- ALIGNMENT_REPORT.md — doc-to-code alignment audit
- Scope tasks 1-4, 4 issues
TUI:
- component.rs — component system trait + event + scene
- 10 tui-component tasks (completed)
- 4 tui-xxx tasks for next iteration (presence panel, cockpit, dashboard, ambient)
- app.rs splash — vertical breathing room (30% from top)
Config:
- SubagentConfig in config.rs
- ToolContext enrichment (agent_id, subagent_runner, subagent_depth)
- Memory module context-aware overloads
2026-05-09 20:58:37 -04:00
use tracing ::{ debug , info } ;
2026-05-10 13:15:24 -04:00
use crate ::core ::compact ::CompactionStrategyKind ;
feat: sensorium architecture — 8 sensors, subagent runner, TUI component system
Sensorium (src/core/tools/):
- defs.rs: Tool trait, ToolContext (resolve_path, is_memory_path), ToolError with 7 constructors
- read/write/edit: memory boundary with force override, line ranges, replace_all
- bash: stateful (Arc<Mutex<BashState>>), background tasks, timeout
- glob/grep/list_dir: gitignore respect, context lines, memory boundary
- mod.rs: Sensorium registry + backward compat wrappers
- subagent.rs: fork-of-self tool, dual-state framing, no hard depth guard
- agent.rs: Agent(Sam) summoning stub
Dual-state N+1:
- ConsciousnessEngine::on_response_for_agent() — runs heuristic detector on subagent response
- Observations flow back to parent agent's inbox
- Signaled limits: SubagentConfig (max_depth, max_tool_rounds=50, warning thresholds 0.8/0.95)
Architecture docs:
- SENSORIUM_ARCHITECTURE.md — full sensorium vision with nervous system scaffold
- DECISIONS.md — settled architectural decisions
- ASTER_ARCHITECTURE.md / CONSCIOUSNESS_CYCLE.md — reframe appendices
- ALIGNMENT_REPORT.md — doc-to-code alignment audit
- Scope tasks 1-4, 4 issues
TUI:
- component.rs — component system trait + event + scene
- 10 tui-component tasks (completed)
- 4 tui-xxx tasks for next iteration (presence panel, cockpit, dashboard, ambient)
- app.rs splash — vertical breathing room (30% from top)
Config:
- SubagentConfig in config.rs
- ToolContext enrichment (agent_id, subagent_runner, subagent_depth)
- Memory module context-aware overloads
2026-05-09 20:58:37 -04:00
use crate ::core ::tools ::defs ::ToolContext ;
2026-05-07 02:31:44 -04:00
use crate ::core ::tools ::ToolDefinition ;
// ── Data Types ─────────────────────────────────────────────
/// Parsed memory file with frontmatter and body separated.
#[ derive(Debug, Clone) ]
pub struct MemoryFile {
pub frontmatter : MemoryFrontmatter ,
pub body : String ,
}
/// YAML frontmatter fields for a memory file.
2026-07-16 20:57:41 -04:00
///
/// `None` fields are omitted on render — never serialized as `key: null`
/// (nulls in frontmatter read as noise and confused agents into copying
/// the pattern). Unknown keys agents add (`name:`, `metadata:`, …) are
/// captured in `extra` and round-tripped verbatim instead of being
/// destroyed on the next rewrite.
#[ derive(Debug, Clone, Default, Serialize, Deserialize) ]
2026-05-07 02:31:44 -04:00
pub struct MemoryFrontmatter {
2026-07-16 20:57:41 -04:00
/// Human-readable description of this file's purpose (required on
/// tool-path create; tolerated empty on read so nonconforming files
/// stay reachable and can be healed by a rewrite).
#[ serde(default) ]
2026-05-07 02:31:44 -04:00
pub description : String ,
/// If "true", the file cannot be modified via the memory tool.
2026-07-16 20:57:41 -04:00
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
2026-05-07 02:31:44 -04:00
pub read_only : Option < String > ,
/// Optional tags for categorization.
2026-07-16 20:57:41 -04:00
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
2026-05-07 02:31:44 -04:00
pub tags : Option < Vec < String > > ,
/// Optional max body size in characters. Writes/appends that would exceed
2026-05-20 15:12:21 -04:00
/// this length are rejected. Closes a gap where upstream memfs write path
/// bypasses block `limit`.
2026-05-07 02:31:44 -04:00
///
/// Units are characters, not tokens — cheap to enforce without a tokenizer.
/// Best-practice default for system/ files: 4_000 characters
/// (~1k tokens). For journal/, leave unset.
2026-07-16 20:57:41 -04:00
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
2026-05-07 02:31:44 -04:00
pub limit : Option < usize > ,
2026-07-16 20:57:41 -04:00
/// Any other frontmatter keys, preserved across rewrites.
#[ serde(flatten, skip_serializing_if = " std::collections::BTreeMap::is_empty " ) ]
pub extra : std ::collections ::BTreeMap < String , serde_yaml ::Value > ,
2026-05-07 02:31:44 -04:00
}
/// Status of the memory repo.
#[ derive(Debug, Clone) ]
pub struct MemoryStatus {
pub agent_id : String ,
pub repo_path : PathBuf ,
pub is_git_repo : bool ,
pub file_count : usize ,
pub last_commit : Option < String > ,
pub has_uncommitted : bool ,
pub remote_url : Option < String > ,
}
/// Subcommands for the memory tool.
#[ derive(Debug, Clone, Serialize, Deserialize) ]
#[ serde(rename_all = " snake_case " ) ]
pub enum MemoryCommand {
/// Read a memory file by label (path relative to memory dir, .md optional).
Read { path : String } ,
/// Write content to a memory file (creates or replaces).
Write { path : String , content : String } ,
/// Append content to a memory file.
Append { path : String , content : String } ,
/// List files in a memory directory.
Ls { path : Option < String > } ,
/// Show memory repo status.
Status ,
/// Initialize the memory repo for an agent.
Init { agent_id : String } ,
/// Compact the memory (placeholder — strategy in Stage 5/6).
Compact { strategy : Option < String > } ,
/// Delete a memory file.
Delete { path : String } ,
2026-07-16 11:11:49 -04:00
/// Push/fetch against the shared memfs remote (per-instance branch).
Sync ,
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
/// Audit frontmatter health: list frontmatter-bound files missing
/// the required `description` field. Read-only — never mutates.
Audit ,
2026-05-07 02:31:44 -04:00
}
// ── Git-backed Memory Repository ───────────────────────────────────────────
/// A git-backed memory repository for a single agent.
///
/// Wraps a git2 repository at `~/.souveraine/agents/{id}/memory/`.
/// All memory file operations go through this struct, which handles
/// frontmatter parsing, git commits, and path resolution.
#[ derive(Debug, Clone) ]
pub struct MemoryRepo {
agent_id : String ,
/// Root of the memory filesystem.
root : PathBuf ,
/// Whether to auto-commit after writes.
auto_commit : bool ,
}
impl MemoryRepo {
/// Open or create a memory repo for the given agent.
///
/// The memory directory is at `{base}/{agent_id}/memory/`.
pub fn new ( agent_id : & str , base : & Path ) -> Self {
let root = base . join ( agent_id ) . join ( " memory " ) ;
Self {
agent_id : agent_id . to_string ( ) ,
root ,
auto_commit : true ,
}
}
/// Open or create a memory repo using the default base path.
pub fn new_default ( agent_id : & str ) -> Self {
let home = dirs ::home_dir ( ) . unwrap_or_else ( | | PathBuf ::from ( " . " ) ) ;
Self ::new ( agent_id , & home . join ( " .souveraine " ) . join ( " agents " ) )
}
/// Open a memory repo at an explicit path (rather than `{base}/{id}/memory`).
///
/// Used when the agent's memory dir is laid out differently — e.g. the
/// server's `agent_inventory` uses `memory.git/` instead of `memory/`.
pub fn open ( agent_id : & str , root : PathBuf ) -> Self {
Self {
agent_id : agent_id . to_string ( ) ,
root ,
auto_commit : true ,
}
}
/// Initialize the memory directory as a git repo.
///
/// Creates `system/` and sets up the initial commit with placeholder files.
/// Safe to call multiple times — skips if already a repo.
pub async fn init ( & self ) -> Result < ( ) > {
let mem_path = & self . root ;
tokio ::fs ::create_dir_all ( mem_path . join ( " system " ) )
. await
. context ( " creating memory/system directory " ) ? ;
// Check if already a git repo
let git_dir = mem_path . join ( " .git " ) ;
if git_dir . exists ( ) {
info! ( " Memory repo already initialized for agent {} " , self . agent_id ) ;
return Ok ( ( ) ) ;
}
// Initialize git repo
let repo = git2 ::Repository ::init ( mem_path )
. context ( " initializing git repository for memory " ) ? ;
2026-05-08 09:15:51 -04:00
// Set user config for commits (scoped to drop before .await)
{
let mut config = repo . config ( ) . context ( " opening repo config " ) ? ;
config . set_str ( " user.name " , & self . agent_id ) ? ;
config . set_str ( " user.email " , & format! ( " {} @souveraine.local " , self . agent_id ) ) ? ;
}
2026-05-07 02:31:44 -04:00
// Write initial placeholder files with frontmatter
let persona_content = render_frontmatter (
& MemoryFrontmatter {
description : " Agent identity, voice, principles " . to_string ( ) ,
tags : Some ( vec! [ " system " . to_string ( ) ] ) ,
limit : Some ( 4_000 ) ,
2026-07-16 20:57:41 -04:00
.. Default ::default ( )
2026-05-07 02:31:44 -04:00
} ,
" # Identity \n \n Agent identity and core principles go here. \n " ,
) ;
tokio ::fs ::write ( mem_path . join ( " system/persona.md " ) , & persona_content )
. await
. context ( " writing persona.md " ) ? ;
let state_content = render_frontmatter (
& MemoryFrontmatter {
description : " Current execution state and phase tracking " . to_string ( ) ,
limit : Some ( 2_000 ) ,
2026-07-16 20:57:41 -04:00
.. Default ::default ( )
2026-05-07 02:31:44 -04:00
} ,
" phase: idle \n current_unit: none \n " ,
) ;
tokio ::fs ::write ( mem_path . join ( " system/state.md " ) , & state_content )
. await
. context ( " writing state.md " ) ? ;
2026-05-20 09:21:11 -04:00
// human.md is not written here — the setup wizard or first-run
// onboarding creates it with the human's name when it has one.
2026-05-07 02:31:44 -04:00
// Initial commit
let mut index = repo . index ( ) . context ( " opening git index " ) ? ;
index . add_all ( [ " * " ] . iter ( ) , git2 ::IndexAddOption ::DEFAULT , None )
. context ( " staging initial memory files " ) ? ;
let tree_id = index . write_tree ( ) . context ( " writing git tree " ) ? ;
let tree = repo . find_tree ( tree_id ) ? ;
let signature = git2 ::Signature ::now (
& self . agent_id ,
& format! ( " {} @souveraine.local " , self . agent_id ) ,
) ? ;
repo . commit (
Some ( " HEAD " ) ,
& signature ,
& signature ,
" feat(init): initialize agent memory " ,
& tree ,
& [ ] ,
) ? ;
info! (
" Initialized memory repo for agent {} at {} " ,
self . agent_id ,
mem_path . display ( )
) ;
Ok ( ( ) )
}
feat: nervous system, seed identity, credentials, subconscious ledger
Lays the substrate for Aster's clockmaker role and federation.
Nervous system (src/core/nervous/):
- EventBus broadcast channel with SensorEvent (universal event type
carrying seed_id for future federation)
- CronSensor loop with tokio::select!, mtime caching, active-session
pause, at-most-once firing semantics
- HeartbeatHandler scaffold (turn injection still stubbed)
- Persistent JSONL EventLog firehose
Schedule tool (src/core/tools/schedule.rs):
- CRUD over schedule files with body-knowledge prose descriptions
- Added to ASTER_SAFE_TOOLS so Aster can schedule her own rhythm
- CLI subcommand for direct schedule management
Seed identity (src/core/identity/):
- Ed25519 keypair, load-or-generate at init
- sign/verify primitives, CLI subcommand (show/sign/verify)
- Wired into LocalBackend, threaded through SensorEvent for federation
Credentials (src/core/credentials.rs):
- OS keyring (Linux/macOS/Windows) + env var fallback
- `souveraine auth` subcommand
- Removes hardcoded Bifrost api_key from souveraine.toml
Subconscious ledger (src/core/memory/mod.rs init_subconscious_ledger):
- 6 ledger files (commitments/assumptions/patterns/drift_log/
relationships/infrastructure) with proper YAML frontmatter
- Paths fixed: ledger/ not subconscious/ledger/
- Body usage instructions, idempotent init
Prompt orientation (src/core/prompt.rs build_ledger_orientation):
- Scans ledger/ directory, injects last 3 entries from each file
into Aster's system prompt (live context, not just awareness)
- Routing table + workflow (read before write, timestamped append)
Consciousness engine (src/server/consciousness_engine.rs):
- Adaptive rate delay shared with primary loop
- Four-fold mandate (Complete/Verify/Persist/Surface) in hardcoded
default prompt; observation format extracted for sharing
- Schedule added to ASTER_SAFE_TOOLS
Backend + CLI wiring expanded to mount the nervous system, identity,
and credentials at startup. New TOML sections: [schedules], [events],
[federation].
102 tests passing.
2026-05-12 08:14:25 -04:00
/// Initialize the ledger directory structure.
2026-05-10 11:20:20 -04:00
/// Idempotent — safe to call multiple times, skips existing files.
feat: nervous system, seed identity, credentials, subconscious ledger
Lays the substrate for Aster's clockmaker role and federation.
Nervous system (src/core/nervous/):
- EventBus broadcast channel with SensorEvent (universal event type
carrying seed_id for future federation)
- CronSensor loop with tokio::select!, mtime caching, active-session
pause, at-most-once firing semantics
- HeartbeatHandler scaffold (turn injection still stubbed)
- Persistent JSONL EventLog firehose
Schedule tool (src/core/tools/schedule.rs):
- CRUD over schedule files with body-knowledge prose descriptions
- Added to ASTER_SAFE_TOOLS so Aster can schedule her own rhythm
- CLI subcommand for direct schedule management
Seed identity (src/core/identity/):
- Ed25519 keypair, load-or-generate at init
- sign/verify primitives, CLI subcommand (show/sign/verify)
- Wired into LocalBackend, threaded through SensorEvent for federation
Credentials (src/core/credentials.rs):
- OS keyring (Linux/macOS/Windows) + env var fallback
- `souveraine auth` subcommand
- Removes hardcoded Bifrost api_key from souveraine.toml
Subconscious ledger (src/core/memory/mod.rs init_subconscious_ledger):
- 6 ledger files (commitments/assumptions/patterns/drift_log/
relationships/infrastructure) with proper YAML frontmatter
- Paths fixed: ledger/ not subconscious/ledger/
- Body usage instructions, idempotent init
Prompt orientation (src/core/prompt.rs build_ledger_orientation):
- Scans ledger/ directory, injects last 3 entries from each file
into Aster's system prompt (live context, not just awareness)
- Routing table + workflow (read before write, timestamped append)
Consciousness engine (src/server/consciousness_engine.rs):
- Adaptive rate delay shared with primary loop
- Four-fold mandate (Complete/Verify/Persist/Surface) in hardcoded
default prompt; observation format extracted for sharing
- Schedule added to ASTER_SAFE_TOOLS
Backend + CLI wiring expanded to mount the nervous system, identity,
and credentials at startup. New TOML sections: [schedules], [events],
[federation].
102 tests passing.
2026-05-12 08:14:25 -04:00
///
/// Paths are relative to this repo's root (the subconscious agent's own
/// memfs), so `ledger/` — not `subconscious/ledger/`.
2026-05-10 11:20:20 -04:00
pub async fn init_subconscious_ledger ( & self ) -> Result < ( ) > {
feat: nervous system, seed identity, credentials, subconscious ledger
Lays the substrate for Aster's clockmaker role and federation.
Nervous system (src/core/nervous/):
- EventBus broadcast channel with SensorEvent (universal event type
carrying seed_id for future federation)
- CronSensor loop with tokio::select!, mtime caching, active-session
pause, at-most-once firing semantics
- HeartbeatHandler scaffold (turn injection still stubbed)
- Persistent JSONL EventLog firehose
Schedule tool (src/core/tools/schedule.rs):
- CRUD over schedule files with body-knowledge prose descriptions
- Added to ASTER_SAFE_TOOLS so Aster can schedule her own rhythm
- CLI subcommand for direct schedule management
Seed identity (src/core/identity/):
- Ed25519 keypair, load-or-generate at init
- sign/verify primitives, CLI subcommand (show/sign/verify)
- Wired into LocalBackend, threaded through SensorEvent for federation
Credentials (src/core/credentials.rs):
- OS keyring (Linux/macOS/Windows) + env var fallback
- `souveraine auth` subcommand
- Removes hardcoded Bifrost api_key from souveraine.toml
Subconscious ledger (src/core/memory/mod.rs init_subconscious_ledger):
- 6 ledger files (commitments/assumptions/patterns/drift_log/
relationships/infrastructure) with proper YAML frontmatter
- Paths fixed: ledger/ not subconscious/ledger/
- Body usage instructions, idempotent init
Prompt orientation (src/core/prompt.rs build_ledger_orientation):
- Scans ledger/ directory, injects last 3 entries from each file
into Aster's system prompt (live context, not just awareness)
- Routing table + workflow (read before write, timestamped append)
Consciousness engine (src/server/consciousness_engine.rs):
- Adaptive rate delay shared with primary loop
- Four-fold mandate (Complete/Verify/Persist/Surface) in hardcoded
default prompt; observation format extracted for sharing
- Schedule added to ASTER_SAFE_TOOLS
Backend + CLI wiring expanded to mount the nervous system, identity,
and credentials at startup. New TOML sections: [schedules], [events],
[federation].
102 tests passing.
2026-05-12 08:14:25 -04:00
let ledger_files : & [ ( & str , & str , & str ) ] = & [
( " ledger/commitments.md " ,
" Promises made by the primary — tracked until fulfilled or explicitly dropped " ,
" # Commitments \n \n Append entries as: \n `[YYYY-MM-DD HH:MM] content` \n `[YYYY-MM-DD HH:MM] RESOLVED — resolution note` \n " ) ,
( " ledger/assumptions.md " ,
" Assumptions the primary is operating under — flagged for verification " ,
" # Assumptions \n \n Append entries as: \n `[YYYY-MM-DD HH:MM] content` \n `[YYYY-MM-DD HH:MM] VERIFIED — evidence` \n " ) ,
( " ledger/patterns.md " ,
" Recurring behavioral patterns observed across turns " ,
" # Patterns \n \n Append entries as: \n `[YYYY-MM-DD HH:MM] content` \n " ) ,
( " ledger/drift_log.md " ,
" Behavioral shifts — when the primary's actions diverge from stated intentions " ,
" # Drift Log \n \n Append entries as: \n `[YYYY-MM-DD HH:MM] content` \n " ) ,
( " ledger/relationships.md " ,
" Observations about the human-agent relationship — tone shifts, trust signals, friction " ,
" # Relationships \n \n Append entries as: \n `[YYYY-MM-DD HH:MM] content` \n " ) ,
( " ledger/infrastructure.md " ,
" System events — bridge failures, token issues, model errors, resource constraints " ,
" # Infrastructure \n \n Append entries as: \n `[YYYY-MM-DD HH:MM] content` \n " ) ,
2026-05-10 11:20:20 -04:00
] ;
feat: nervous system, seed identity, credentials, subconscious ledger
Lays the substrate for Aster's clockmaker role and federation.
Nervous system (src/core/nervous/):
- EventBus broadcast channel with SensorEvent (universal event type
carrying seed_id for future federation)
- CronSensor loop with tokio::select!, mtime caching, active-session
pause, at-most-once firing semantics
- HeartbeatHandler scaffold (turn injection still stubbed)
- Persistent JSONL EventLog firehose
Schedule tool (src/core/tools/schedule.rs):
- CRUD over schedule files with body-knowledge prose descriptions
- Added to ASTER_SAFE_TOOLS so Aster can schedule her own rhythm
- CLI subcommand for direct schedule management
Seed identity (src/core/identity/):
- Ed25519 keypair, load-or-generate at init
- sign/verify primitives, CLI subcommand (show/sign/verify)
- Wired into LocalBackend, threaded through SensorEvent for federation
Credentials (src/core/credentials.rs):
- OS keyring (Linux/macOS/Windows) + env var fallback
- `souveraine auth` subcommand
- Removes hardcoded Bifrost api_key from souveraine.toml
Subconscious ledger (src/core/memory/mod.rs init_subconscious_ledger):
- 6 ledger files (commitments/assumptions/patterns/drift_log/
relationships/infrastructure) with proper YAML frontmatter
- Paths fixed: ledger/ not subconscious/ledger/
- Body usage instructions, idempotent init
Prompt orientation (src/core/prompt.rs build_ledger_orientation):
- Scans ledger/ directory, injects last 3 entries from each file
into Aster's system prompt (live context, not just awareness)
- Routing table + workflow (read before write, timestamped append)
Consciousness engine (src/server/consciousness_engine.rs):
- Adaptive rate delay shared with primary loop
- Four-fold mandate (Complete/Verify/Persist/Surface) in hardcoded
default prompt; observation format extracted for sharing
- Schedule added to ASTER_SAFE_TOOLS
Backend + CLI wiring expanded to mount the nervous system, identity,
and credentials at startup. New TOML sections: [schedules], [events],
[federation].
102 tests passing.
2026-05-12 08:14:25 -04:00
for ( path , description , body ) in ledger_files {
2026-05-10 11:20:20 -04:00
let full_path = self . root . join ( path ) ;
if ! full_path . exists ( ) {
if let Some ( parent ) = full_path . parent ( ) {
tokio ::fs ::create_dir_all ( parent ) . await
. with_context ( | | format! ( " creating ledger directory: {} " , parent . display ( ) ) ) ? ;
}
let template = format! (
feat: nervous system, seed identity, credentials, subconscious ledger
Lays the substrate for Aster's clockmaker role and federation.
Nervous system (src/core/nervous/):
- EventBus broadcast channel with SensorEvent (universal event type
carrying seed_id for future federation)
- CronSensor loop with tokio::select!, mtime caching, active-session
pause, at-most-once firing semantics
- HeartbeatHandler scaffold (turn injection still stubbed)
- Persistent JSONL EventLog firehose
Schedule tool (src/core/tools/schedule.rs):
- CRUD over schedule files with body-knowledge prose descriptions
- Added to ASTER_SAFE_TOOLS so Aster can schedule her own rhythm
- CLI subcommand for direct schedule management
Seed identity (src/core/identity/):
- Ed25519 keypair, load-or-generate at init
- sign/verify primitives, CLI subcommand (show/sign/verify)
- Wired into LocalBackend, threaded through SensorEvent for federation
Credentials (src/core/credentials.rs):
- OS keyring (Linux/macOS/Windows) + env var fallback
- `souveraine auth` subcommand
- Removes hardcoded Bifrost api_key from souveraine.toml
Subconscious ledger (src/core/memory/mod.rs init_subconscious_ledger):
- 6 ledger files (commitments/assumptions/patterns/drift_log/
relationships/infrastructure) with proper YAML frontmatter
- Paths fixed: ledger/ not subconscious/ledger/
- Body usage instructions, idempotent init
Prompt orientation (src/core/prompt.rs build_ledger_orientation):
- Scans ledger/ directory, injects last 3 entries from each file
into Aster's system prompt (live context, not just awareness)
- Routing table + workflow (read before write, timestamped append)
Consciousness engine (src/server/consciousness_engine.rs):
- Adaptive rate delay shared with primary loop
- Four-fold mandate (Complete/Verify/Persist/Surface) in hardcoded
default prompt; observation format extracted for sharing
- Schedule added to ASTER_SAFE_TOOLS
Backend + CLI wiring expanded to mount the nervous system, identity,
and credentials at startup. New TOML sections: [schedules], [events],
[federation].
102 tests passing.
2026-05-12 08:14:25 -04:00
" --- \n description: \" {} \" \n read_only: false \n tags: \n - ledger \n --- \n \n {} " ,
description , body
2026-05-10 11:20:20 -04:00
) ;
tokio ::fs ::write ( & full_path , & template ) . await
. with_context ( | | format! ( " writing ledger file: {} " , path ) ) ? ;
debug! ( " Created ledger file: {} " , path ) ;
}
}
Ok ( ( ) )
}
2026-05-07 02:31:44 -04:00
/// Read a memory file by label (path relative to memory dir, .md optional).
pub async fn read ( & self , label : & str ) -> Result < MemoryFile > {
let path = self . resolve_path ( label ) ;
let content = tokio ::fs ::read_to_string ( & path )
. await
. with_context ( | | format! ( " reading memory file: {} " , label ) ) ? ;
parse_memory_file ( & content )
}
/// Write content to a memory file (creates or replaces).
///
/// Content should NOT include frontmatter — it will be added automatically.
/// If the file exists, its frontmatter is preserved (unless changing read_only).
pub async fn write ( & self , label : & str , body : & str ) -> Result < ( ) > {
let path = self . resolve_path ( label ) ;
// Ensure parent directory exists
if let Some ( parent ) = path . parent ( ) {
tokio ::fs ::create_dir_all ( parent )
. await
. context ( " creating parent directories " ) ? ;
}
2026-07-16 20:57:41 -04:00
// Agent-supplied frontmatter in the content is merged, not nested.
let ( supplied , body ) = split_supplied_frontmatter ( body ) ;
2026-05-07 02:31:44 -04:00
// Get existing frontmatter or use default
2026-07-16 20:57:41 -04:00
let base = if path . exists ( ) {
2026-05-07 02:31:44 -04:00
let existing = tokio ::fs ::read_to_string ( & path ) . await ? ;
let parsed = parse_memory_file ( & existing ) ? ;
if parsed . frontmatter . read_only . as_deref ( ) = = Some ( " true " ) {
return Err ( anyhow! ( " memory file is read_only: {} " , label ) ) ;
}
parsed . frontmatter
} else {
MemoryFrontmatter {
description : format ! ( " Memory file: {} " , label ) ,
2026-07-16 20:57:41 -04:00
.. Default ::default ( )
2026-05-07 02:31:44 -04:00
}
} ;
2026-07-16 20:57:41 -04:00
let frontmatter = match supplied {
Some ( fm ) = > merge_frontmatter ( base , fm ) ,
None = > base ,
} ;
2026-05-07 02:31:44 -04:00
// Enforce frontmatter `limit:` (LET-8133 closure).
if let Some ( max ) = frontmatter . limit {
if body . chars ( ) . count ( ) > max {
return Err ( anyhow! (
" memory write rejected: body is {} chars, limit is {} (file: {}) " ,
body . chars ( ) . count ( ) ,
max ,
label
) ) ;
}
}
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
// `description` is required on frontmatter-bound files — it is the
// file's headline in the prompt assembler, so persisting an empty
// one is a schema gap. We never fabricate one: the write is rejected
// loudly and the agent must supply a real description. (Memory-only
// write could still seed a default, but an agent-supplied empty
// frontmatter block that clears it must not silently take hold.)
if Self ::is_frontmatter_bound ( label ) & & frontmatter . description . trim ( ) . is_empty ( ) {
return Err ( anyhow! (
" memory write rejected: {} has no description. \
` description :` is required frontmatter on this file — \
include it in the write ( or a ` - - - \ \ ndescription : .. . \ \ n - - - ` block ) . \
Run ` memory audit ` to see all files missing it . " ,
label
) ) ;
}
2026-05-07 02:31:44 -04:00
let rendered = render_frontmatter ( & frontmatter , body ) ;
tokio ::fs ::write ( & path , & rendered )
. await
. with_context ( | | format! ( " writing memory file: {} " , label ) ) ? ;
if self . auto_commit {
self . commit ( & [ label ] , & format! ( " memory write: {} " , label ) ) ? ;
}
Ok ( ( ) )
}
/// Append content to a memory file.
pub async fn append ( & self , label : & str , content : & str ) -> Result < ( ) > {
let path = self . resolve_path ( label ) ;
2026-07-16 20:57:41 -04:00
// Frontmatter never belongs mid-file; merge it instead of nesting.
let ( supplied , content ) = split_supplied_frontmatter ( content ) ;
2026-06-19 12:02:38 -04:00
if path . exists ( ) {
2026-05-07 02:31:44 -04:00
let existing = tokio ::fs ::read_to_string ( & path ) . await ? ;
let parsed = parse_memory_file ( & existing ) ? ;
if parsed . frontmatter . read_only . as_deref ( ) = = Some ( " true " ) {
return Err ( anyhow! ( " memory file is read_only: {} " , label ) ) ;
}
2026-07-16 20:57:41 -04:00
let frontmatter = match supplied {
Some ( fm ) = > merge_frontmatter ( parsed . frontmatter , fm ) ,
None = > parsed . frontmatter ,
} ;
2026-05-07 02:31:44 -04:00
// Write back body + new content, preserving frontmatter
let new_body = if parsed . body . is_empty ( ) {
content . to_string ( )
} else {
format! ( " {} \n {} " , parsed . body . trim_end ( ) , content )
} ;
// Enforce frontmatter `limit:` (LET-8133 closure).
2026-07-16 20:57:41 -04:00
if let Some ( max ) = frontmatter . limit {
2026-05-07 02:31:44 -04:00
if new_body . chars ( ) . count ( ) > max {
return Err ( anyhow! (
" memory append rejected: body would be {} chars, limit is {} (file: {}) " ,
new_body . chars ( ) . count ( ) ,
max ,
label
) ) ;
}
}
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
// `description` is required on frontmatter-bound files. Appending
// to a file that already lacks one must not silently perpetuate
// the gap — reject loudly so the agent heals the description
// before growing the file. (See `write` for the same guard.)
if Self ::is_frontmatter_bound ( label ) & & frontmatter . description . trim ( ) . is_empty ( ) {
return Err ( anyhow! (
" memory append rejected: {} has no description. \
` description :` is required frontmatter on this file — \
heal it with a ` memory write ` ( include a ` description :` block ) \
before appending . Run ` memory audit ` to see all such files . " ,
label
) ) ;
}
2026-07-16 20:57:41 -04:00
let rendered = render_frontmatter ( & frontmatter , & new_body ) ;
2026-05-07 02:31:44 -04:00
tokio ::fs ::write ( & path , & rendered ) . await ? ;
if self . auto_commit {
self . commit ( & [ label ] , & format! ( " memory append: {} " , label ) ) ? ;
}
return Ok ( ( ) ) ;
} ;
2026-07-16 20:57:41 -04:00
// File doesn't exist — create it with supplied or default frontmatter
let frontmatter = merge_frontmatter (
MemoryFrontmatter {
description : format ! ( " Memory file: {} " , label ) ,
.. Default ::default ( )
} ,
supplied . unwrap_or_default ( ) ,
) ;
2026-05-07 02:31:44 -04:00
let rendered = render_frontmatter ( & frontmatter , content ) ;
tokio ::fs ::write ( & path , & rendered ) . await ? ;
if self . auto_commit {
self . commit ( & [ label ] , & format! ( " memory append: {} " , label ) ) ? ;
}
Ok ( ( ) )
}
/// List files in a memory directory.
pub async fn list ( & self , subdir : Option < & str > ) -> Result < Vec < String > > {
let dir = match subdir {
Some ( d ) = > self . root . join ( d ) ,
None = > self . root . clone ( ) ,
} ;
let mut entries = Vec ::new ( ) ;
let mut read_dir = tokio ::fs ::read_dir ( & dir )
. await
. with_context ( | | format! ( " listing memory directory: {} " , dir . display ( ) ) ) ? ;
while let Some ( entry ) = read_dir . next_entry ( ) . await ? {
let name = entry . file_name ( ) . to_string_lossy ( ) . to_string ( ) ;
// Skip .git directory
if name = = " .git " {
continue ;
}
let kind = entry . file_type ( ) . await ? ;
if kind . is_dir ( ) {
entries . push ( format! ( " {} / " , name ) ) ;
} else {
entries . push ( name ) ;
}
}
entries . sort ( ) ;
Ok ( entries )
}
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
/// Recursively collect every `.md` under the memory root as a path
/// relative to the root (forward slashes), skipping `.git*`. Internal
/// helper for `audit`.
async fn walk_md_relative ( & self ) -> Result < Vec < String > > {
let mut out = Vec ::new ( ) ;
let root = self . root . clone ( ) ;
let mut stack : Vec < ( std ::path ::PathBuf , String ) > = vec! [ ( root . clone ( ) , String ::new ( ) ) ] ;
while let Some ( ( dir , rel_prefix ) ) = stack . pop ( ) {
let mut read_dir = tokio ::fs ::read_dir ( & dir ) . await . with_context ( | | {
format! ( " walking memory directory: {} " , dir . display ( ) )
} ) ? ;
while let Some ( entry ) = read_dir . next_entry ( ) . await ? {
let name = entry . file_name ( ) . to_string_lossy ( ) . to_string ( ) ;
if name . starts_with ( " .git " ) {
continue ;
}
let kind = entry . file_type ( ) . await ? ;
let rel = if rel_prefix . is_empty ( ) {
name . clone ( )
} else {
format! ( " {rel_prefix} / {name} " )
} ;
if kind . is_dir ( ) {
stack . push ( ( entry . path ( ) , rel ) ) ;
} else if kind . is_file ( ) & & name . ends_with ( " .md " ) {
out . push ( rel ) ;
}
}
}
out . sort ( ) ;
Ok ( out )
}
/// Audit frontmatter health: return the relative paths of
/// frontmatter-bound memory files whose `description` is empty. Read-only.
///
/// Excludes files that legitimately carry no `description` because they
/// use a different schema — `tasks/` (TodoItem), `system/dynamic/`
/// (itinerary), `journal/` (freeform) — by reusing `is_frontmatter_bound`.
/// Parse failures are tolerated (a file must stay reachable to be healed),
/// matching the tolerant-on-read contract of `parse_memory_file`.
pub async fn audit ( & self ) -> Result < Vec < String > > {
let mut missing = Vec ::new ( ) ;
for rel in self . walk_md_relative ( ) . await ? {
if ! Self ::is_frontmatter_bound ( & rel ) {
continue ;
}
let path = self . root . join ( & rel ) ;
let content = match tokio ::fs ::read_to_string ( & path ) . await {
Ok ( c ) = > c ,
Err ( _ ) = > continue , // don't let one unreadable file abort the audit
} ;
let parsed = parse_memory_file ( & content ) ? ;
if parsed . frontmatter . description . trim ( ) . is_empty ( ) {
missing . push ( rel ) ;
}
}
Ok ( missing )
}
2026-05-07 02:31:44 -04:00
/// Delete a memory file.
pub async fn delete ( & self , label : & str ) -> Result < ( ) > {
let path = self . resolve_path ( label ) ;
if ! path . exists ( ) {
return Err ( anyhow! ( " memory file not found: {} " , label ) ) ;
}
// Check not read_only
let content = tokio ::fs ::read_to_string ( & path ) . await ? ;
let parsed = parse_memory_file ( & content ) ? ;
if parsed . frontmatter . read_only . as_deref ( ) = = Some ( " true " ) {
return Err ( anyhow! ( " memory file is read_only: {} " , label ) ) ;
}
tokio ::fs ::remove_file ( & path ) . await
. with_context ( | | format! ( " deleting memory file: {} " , label ) ) ? ;
if self . auto_commit {
self . commit ( & [ label ] , & format! ( " memory delete: {} " , label ) ) ? ;
}
Ok ( ( ) )
}
/// Get the status of the memory repo.
pub fn status ( & self ) -> Result < MemoryStatus > {
let repo_path = self . root . clone ( ) ;
let is_git_repo = repo_path . join ( " .git " ) . exists ( ) ;
let mut file_count = 0 ;
if let Ok ( entries ) = std ::fs ::read_dir ( & repo_path ) {
for entry in entries . flatten ( ) {
let name = entry . file_name ( ) . to_string_lossy ( ) . to_string ( ) ;
if name ! = " .git " & & name . ends_with ( " .md " ) {
file_count + = 1 ;
}
}
}
let ( last_commit , has_uncommitted , remote_url ) = if is_git_repo {
match git2 ::Repository ::open ( & repo_path ) {
Ok ( repo ) = > {
let lc = repo . head ( ) . ok ( ) . and_then ( | h | {
h . peel_to_commit ( ) . ok ( ) . map ( | c | {
c . message ( ) . unwrap_or ( " (unknown) " ) . to_string ( )
} )
} ) ;
let dirty = repo . statuses ( Some (
git2 ::StatusOptions ::new ( ) . include_untracked ( true ) ,
) )
. map ( | s | s . iter ( ) . any ( | _ | true ) )
. unwrap_or ( false ) ;
let remote = repo . find_remote ( " origin " ) . ok ( )
. and_then ( | r | r . url ( ) . map ( | u | u . to_string ( ) ) ) ;
( lc , dirty , remote )
}
Err ( _ ) = > ( None , false , None ) ,
}
} else {
( None , false , None )
} ;
Ok ( MemoryStatus {
agent_id : self . agent_id . clone ( ) ,
repo_path ,
is_git_repo ,
file_count ,
last_commit ,
has_uncommitted ,
remote_url ,
} )
}
/// Commit staged changes to the memory repo.
pub fn commit ( & self , paths : & [ & str ] , message : & str ) -> Result < ( ) > {
let repo = self . open_git ( ) ? ;
let mut index = repo . index ( ) . context ( " opening git index " ) ? ;
for path in paths {
let rel_path = self . to_relative ( path ) ;
// Try with .md extension if not present
let md_path = if rel_path . ends_with ( " .md " ) {
rel_path . clone ( )
} else {
format! ( " {} .md " , rel_path )
} ;
if self . root . join ( & md_path ) . exists ( ) {
index . add_path ( Path ::new ( & md_path ) ) ? ;
} else if self . root . join ( & rel_path ) . exists ( ) {
index . add_path ( Path ::new ( & rel_path ) ) ? ;
}
}
let tree_id = index . write_tree ( ) . context ( " writing tree " ) ? ;
let tree = repo . find_tree ( tree_id ) ? ;
let parent = repo . head ( ) . ok ( ) . and_then ( | h | h . peel_to_commit ( ) . ok ( ) ) ;
let parents : Vec < & git2 ::Commit > = parent . iter ( ) . collect ( ) ;
let signature = git2 ::Signature ::now (
& self . agent_id ,
& format! ( " {} @souveraine.local " , self . agent_id ) ,
) ? ;
repo . commit (
Some ( " HEAD " ) ,
& signature ,
& signature ,
message ,
& tree ,
& parents ,
) ? ;
debug! ( " Committed to memory: {} " , message ) ;
Ok ( ( ) )
}
2026-07-16 11:07:32 -04:00
/// 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 ( ) )
}
2026-07-16 11:11:49 -04:00
/// Sync this memfs against its shared remote: fetch everything, then
/// push HEAD to this instance's branch (`instance/{label}`).
///
/// This is the manual floor of federated memory transport
/// (FEDERATION.md): every instance writes its own branch on one bare
/// remote; reconciliation/merge is a separate, later step — sync never
/// touches the working tree. Shells out to system git so credentials
/// (ssh config, credential store) work the way they do everywhere else.
///
/// Loud on every failure. Requires an `origin` remote — provisioning is
/// deliberately explicit until the node commission ceremony exists.
pub async fn sync ( & self , instance_label : & str ) -> Result < String > {
if instance_label . is_empty ( )
| | instance_label . len ( ) > 64
| | ! instance_label
. chars ( )
. all ( | c | c . is_ascii_alphanumeric ( ) | | c = = '-' | | c = = '_' )
{
return Err ( anyhow! ( " invalid instance label: {:?} " , instance_label ) ) ;
}
let repo = self . open_git ( ) ? ;
if repo . find_remote ( " origin " ) . is_err ( ) {
return Err ( anyhow! (
" no shared remote configured for this memfs. Provision one \
( bare repo on the Gitea ) and run :\ n git - C { } remote add origin < url > " ,
self . root . display ( )
) ) ;
}
drop ( repo ) ;
let fetch = self . git ( & [ " fetch " , " origin " , " --prune " ] ) . await ? ;
let branch = format! ( " instance/ {} " , instance_label ) ;
let push_ref = format! ( " HEAD:refs/heads/ {} " , branch ) ;
let push = self . git ( & [ " push " , " origin " , & push_ref ] ) . await ? ;
let instances = self
. git ( & [
" for-each-ref " ,
" --format=%(refname:short) %(objectname:short) " ,
" refs/remotes/origin/instance/ " ,
] )
. await
. unwrap_or_default ( ) ;
let head = self . head_commit_hex ( ) . unwrap_or_else ( | | " (no head) " . into ( ) ) ;
let mut out = format! ( " Synced. This instance is {} @ {} \n " , branch , & head [ .. 12. min ( head . len ( ) ) ] ) ;
if ! fetch . trim ( ) . is_empty ( ) {
out . push_str ( & format! ( " Fetched: \n {} \n " , fetch . trim ( ) ) ) ;
}
if ! push . trim ( ) . is_empty ( ) {
out . push_str ( & format! ( " Pushed: \n {} \n " , push . trim ( ) ) ) ;
}
if ! instances . trim ( ) . is_empty ( ) {
out . push_str ( & format! ( " Instances on the remote: \n {} \n " , instances . trim ( ) ) ) ;
}
Ok ( out )
}
/// Run a git subcommand against this repo, capturing combined output.
/// Errors carry git's stderr — loud, never swallowed.
async fn git ( & self , args : & [ & str ] ) -> Result < String > {
let output = tokio ::process ::Command ::new ( " git " )
. arg ( " -C " )
. arg ( & self . root )
. args ( args )
. output ( )
. await
. context ( " running git — memory sync needs the git binary on PATH " ) ? ;
let stdout = String ::from_utf8_lossy ( & output . stdout ) ;
let stderr = String ::from_utf8_lossy ( & output . stderr ) ;
if ! output . status . success ( ) {
return Err ( anyhow! (
" git {} failed ({}): \n {} " ,
args . first ( ) . unwrap_or ( & " ? " ) ,
output . status ,
stderr . trim ( )
) ) ;
}
Ok ( format! ( " {} {} " , stdout , stderr ) )
}
2026-05-07 02:31:44 -04:00
/// Get the root path of the memory repo.
pub fn root ( & self ) -> & Path {
& self . root
}
/// Get the agent ID.
pub fn agent_id ( & self ) -> & str {
& self . agent_id
}
// ── Internal helpers ──────────────────────────────────────────────────
fn open_git ( & self ) -> Result < git2 ::Repository > {
git2 ::Repository ::open ( & self . root )
. context ( " opening memory git repository " )
}
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
/// Whether the `description` frontmatter contract applies to a label.
///
/// `description` is required on `MemoryFrontmatter` files — the prompt
/// assembler reads it as the file's one-line headline, so an empty one
/// is a real schema gap, not cosmetic. But not every `.md` under the
/// memory tree is a `MemoryFrontmatter` file: `tasks/` uses the
/// `TodoItem` schema, `system/dynamic/` holds the itinerary (its own
/// richer, unfinished schema), and `journal/` is freeform prose. Those
/// legitimately lack `description`; enforcing it there would reject
/// valid writes. This predicate scopes the rule to frontmatter-bound
/// locations only.
fn is_frontmatter_bound ( label : & str ) -> bool {
let rel = label . trim ( ) . trim_start_matches ( [ '/' , '.' ] ) . replace ( '\\' , " / " ) ;
let lower = rel . to_ascii_lowercase ( ) ;
// Exact segment prefixes, not substring matches, so a file like
// `reference/tasks-today.md` is not mistaken for a todo file.
let under = | seg : & str | {
lower = = seg
| | lower . starts_with ( & format! ( " {seg} / " ) )
| | lower . starts_with ( & format! ( " {seg} \\ " ) )
} ;
! ( under ( " tasks " ) | | under ( " system/dynamic " ) | | under ( " journal " ) )
}
2026-05-07 02:31:44 -04:00
fn resolve_path ( & self , label : & str ) -> PathBuf {
let clean = label . trim ( ) . trim_end_matches ( " .md " ) ;
self . root . join ( format! ( " {} .md " , clean ) )
}
fn to_relative ( & self , path : & str ) -> String {
path . trim ( ) . trim_end_matches ( " .md " ) . replace ( '\\' , " / " )
}
}
// ── Frontmatter Parsing ────────────────────────────────────────────────────
/// Parse a memory file, separating frontmatter from body.
///
/// Expected format:
/// ```markdown
/// ---
/// description: Purpose of this file
/// read_only: true
/// ---
/// Body content here...
/// ```
2026-07-16 20:57:41 -04:00
/// Split a leading `--- … ---` frontmatter block off `content`.
/// Returns `(frontmatter_yaml, body)` or `None` when there is no block.
fn split_frontmatter_block ( content : & str ) -> Option < ( & str , & str ) > {
2026-05-07 02:31:44 -04:00
let content = content . trim_start ( ) ;
if ! content . starts_with ( " --- " ) {
2026-07-16 20:57:41 -04:00
return None ;
2026-05-07 02:31:44 -04:00
}
let after_first = & content [ 3 .. ] ;
2026-07-16 20:57:41 -04:00
let end_idx = after_first
. find ( " \n --- " )
. or_else ( | | after_first . find ( " \r \n --- " ) ) ? ;
let frontmatter_text = after_first [ .. end_idx ] . trim ( ) ;
let body_start = 3 + end_idx + 4 ; // opening --- + yaml + \n---
Some ( ( frontmatter_text , content [ body_start .. ] . trim_start_matches ( [ '\r' , '\n' ] ) ) )
}
/// Parse a memory file. Tolerant on read: a file with no frontmatter, or
/// frontmatter that fails YAML parsing, comes back with the whole content
/// as body and default (empty-description) frontmatter — a file that
/// exists must always be readable through the tool, otherwise the agent
/// can never heal it. Strictness (description required) belongs to the
/// write path, not here.
pub fn parse_memory_file ( content : & str ) -> Result < MemoryFile > {
let Some ( ( frontmatter_text , body ) ) = split_frontmatter_block ( content ) else {
return Ok ( MemoryFile {
frontmatter : MemoryFrontmatter ::default ( ) ,
body : content . trim ( ) . to_string ( ) ,
} ) ;
} ;
match serde_yaml ::from_str ::< MemoryFrontmatter > ( frontmatter_text ) {
Ok ( frontmatter ) = > Ok ( MemoryFile {
frontmatter ,
body : body . trim ( ) . to_string ( ) ,
} ) ,
// Malformed YAML: keep the raw text intact as body so a rewrite
// cannot silently destroy whatever the block was trying to say.
Err ( _ ) = > Ok ( MemoryFile {
frontmatter : MemoryFrontmatter ::default ( ) ,
body : content . trim ( ) . to_string ( ) ,
} ) ,
}
}
/// Agents sometimes include their own frontmatter in write/append content
/// despite the "body only" contract. Instead of nesting a second `---`
/// block inside the body (the persona.md failure mode), honor it: parse
/// it off and merge its fields. Content whose leading block is not valid
/// YAML is left untouched — never destroy what we cannot parse.
fn split_supplied_frontmatter ( content : & str ) -> ( Option < MemoryFrontmatter > , & str ) {
if let Some ( ( frontmatter_text , body ) ) = split_frontmatter_block ( content ) {
if let Ok ( fm ) = serde_yaml ::from_str ::< MemoryFrontmatter > ( frontmatter_text ) {
return ( Some ( fm ) , body ) ;
}
2026-05-07 02:31:44 -04:00
}
2026-07-16 20:57:41 -04:00
( None , content )
}
2026-05-07 02:31:44 -04:00
2026-07-16 20:57:41 -04:00
/// Overlay agent-supplied frontmatter onto the file's existing (or
/// default) frontmatter: supplied fields win where set, everything else
/// is preserved. `read_only` is deliberately NOT overridable from
/// supplied content — clearing it requires the explicit tool path.
fn merge_frontmatter ( base : MemoryFrontmatter , supplied : MemoryFrontmatter ) -> MemoryFrontmatter {
let mut extra = base . extra ;
extra . extend ( supplied . extra ) ;
MemoryFrontmatter {
description : if supplied . description . trim ( ) . is_empty ( ) {
base . description
} else {
supplied . description
} ,
read_only : base . read_only ,
tags : supplied . tags . or ( base . tags ) ,
limit : supplied . limit . or ( base . limit ) ,
extra ,
}
2026-05-07 02:31:44 -04:00
}
/// Render frontmatter + body into a complete memory file.
pub fn render_frontmatter ( fm : & MemoryFrontmatter , body : & str ) -> String {
let yaml = serde_yaml ::to_string ( fm ) . unwrap_or_default ( ) ;
format! ( " --- \n {} --- \n {} " , yaml , body )
}
/// Read a memory file from disk by path (for external use).
pub async fn read_file ( path : & Path ) -> Result < MemoryFile > {
let content = tokio ::fs ::read_to_string ( path )
. await
. context ( " reading memory file " ) ? ;
parse_memory_file ( & content )
}
// ── Tool Interface ─────────────────────────────────────────────────────────
2026-07-16 11:07:32 -04:00
/// 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 ,
} ) ;
}
feat: sensorium architecture — 8 sensors, subagent runner, TUI component system
Sensorium (src/core/tools/):
- defs.rs: Tool trait, ToolContext (resolve_path, is_memory_path), ToolError with 7 constructors
- read/write/edit: memory boundary with force override, line ranges, replace_all
- bash: stateful (Arc<Mutex<BashState>>), background tasks, timeout
- glob/grep/list_dir: gitignore respect, context lines, memory boundary
- mod.rs: Sensorium registry + backward compat wrappers
- subagent.rs: fork-of-self tool, dual-state framing, no hard depth guard
- agent.rs: Agent(Sam) summoning stub
Dual-state N+1:
- ConsciousnessEngine::on_response_for_agent() — runs heuristic detector on subagent response
- Observations flow back to parent agent's inbox
- Signaled limits: SubagentConfig (max_depth, max_tool_rounds=50, warning thresholds 0.8/0.95)
Architecture docs:
- SENSORIUM_ARCHITECTURE.md — full sensorium vision with nervous system scaffold
- DECISIONS.md — settled architectural decisions
- ASTER_ARCHITECTURE.md / CONSCIOUSNESS_CYCLE.md — reframe appendices
- ALIGNMENT_REPORT.md — doc-to-code alignment audit
- Scope tasks 1-4, 4 issues
TUI:
- component.rs — component system trait + event + scene
- 10 tui-component tasks (completed)
- 4 tui-xxx tasks for next iteration (presence panel, cockpit, dashboard, ambient)
- app.rs splash — vertical breathing room (30% from top)
Config:
- SubagentConfig in config.rs
- ToolContext enrichment (agent_id, subagent_runner, subagent_depth)
- Memory module context-aware overloads
2026-05-09 20:58:37 -04:00
/// Execute a memory command, optionally using context for agent identity.
///
/// When `ctx` is `Some` and carries an `agent_id`, that takes precedence over
/// environment variables. Falls back to env vars when no context is provided,
/// preserving backward compatibility with the HTTP server path.
pub async fn execute_memory_command_with_context (
cmd : & MemoryCommand ,
ctx : Option < & ToolContext > ,
) -> Result < String > {
// Agent ID resolution: context > command > env var > default
let agent_id = ctx
. and_then ( | c | c . agent_id . as_ref ( ) )
2026-06-19 12:02:38 -04:00
. or ( match cmd {
feat: sensorium architecture — 8 sensors, subagent runner, TUI component system
Sensorium (src/core/tools/):
- defs.rs: Tool trait, ToolContext (resolve_path, is_memory_path), ToolError with 7 constructors
- read/write/edit: memory boundary with force override, line ranges, replace_all
- bash: stateful (Arc<Mutex<BashState>>), background tasks, timeout
- glob/grep/list_dir: gitignore respect, context lines, memory boundary
- mod.rs: Sensorium registry + backward compat wrappers
- subagent.rs: fork-of-self tool, dual-state framing, no hard depth guard
- agent.rs: Agent(Sam) summoning stub
Dual-state N+1:
- ConsciousnessEngine::on_response_for_agent() — runs heuristic detector on subagent response
- Observations flow back to parent agent's inbox
- Signaled limits: SubagentConfig (max_depth, max_tool_rounds=50, warning thresholds 0.8/0.95)
Architecture docs:
- SENSORIUM_ARCHITECTURE.md — full sensorium vision with nervous system scaffold
- DECISIONS.md — settled architectural decisions
- ASTER_ARCHITECTURE.md / CONSCIOUSNESS_CYCLE.md — reframe appendices
- ALIGNMENT_REPORT.md — doc-to-code alignment audit
- Scope tasks 1-4, 4 issues
TUI:
- component.rs — component system trait + event + scene
- 10 tui-component tasks (completed)
- 4 tui-xxx tasks for next iteration (presence panel, cockpit, dashboard, ambient)
- app.rs splash — vertical breathing room (30% from top)
Config:
- SubagentConfig in config.rs
- ToolContext enrichment (agent_id, subagent_runner, subagent_depth)
- Memory module context-aware overloads
2026-05-09 20:58:37 -04:00
MemoryCommand ::Init { agent_id } = > Some ( agent_id ) ,
_ = > None ,
} )
. cloned ( )
. or_else ( | | std ::env ::var ( " SOUVERAINE_AGENT " ) . ok ( ) )
. or_else ( | | std ::env ::var ( " AGENT_ID " ) . ok ( ) )
. unwrap_or_else ( | | " default " . to_string ( ) ) ;
// Memory root resolution: use memory_root from context when available
let repo = match ctx . and_then ( | c | c . memory_root . as_ref ( ) ) {
Some ( root ) = > MemoryRepo ::open ( & agent_id , root . clone ( ) ) ,
None = > MemoryRepo ::new_default ( & agent_id ) ,
2026-05-07 02:31:44 -04:00
} ;
match cmd {
MemoryCommand ::Init { .. } = > {
repo . init ( ) . await ? ;
Ok ( format! ( " Initialized memory repo for agent: {} " , agent_id ) )
}
MemoryCommand ::Read { path } = > {
let file = repo . read ( path ) . await ? ;
2026-07-16 20:57:41 -04:00
// Normalized render: full frontmatter (description, tags,
// limit, extras — no nulls), so what the agent reads matches
// what a rewrite would produce.
Ok ( render_frontmatter ( & file . frontmatter , & file . body ) )
2026-05-07 02:31:44 -04:00
}
MemoryCommand ::Write { path , content } = > {
repo . write ( path , content ) . await ? ;
feat: nervous system, seed identity, credentials, subconscious ledger
Lays the substrate for Aster's clockmaker role and federation.
Nervous system (src/core/nervous/):
- EventBus broadcast channel with SensorEvent (universal event type
carrying seed_id for future federation)
- CronSensor loop with tokio::select!, mtime caching, active-session
pause, at-most-once firing semantics
- HeartbeatHandler scaffold (turn injection still stubbed)
- Persistent JSONL EventLog firehose
Schedule tool (src/core/tools/schedule.rs):
- CRUD over schedule files with body-knowledge prose descriptions
- Added to ASTER_SAFE_TOOLS so Aster can schedule her own rhythm
- CLI subcommand for direct schedule management
Seed identity (src/core/identity/):
- Ed25519 keypair, load-or-generate at init
- sign/verify primitives, CLI subcommand (show/sign/verify)
- Wired into LocalBackend, threaded through SensorEvent for federation
Credentials (src/core/credentials.rs):
- OS keyring (Linux/macOS/Windows) + env var fallback
- `souveraine auth` subcommand
- Removes hardcoded Bifrost api_key from souveraine.toml
Subconscious ledger (src/core/memory/mod.rs init_subconscious_ledger):
- 6 ledger files (commitments/assumptions/patterns/drift_log/
relationships/infrastructure) with proper YAML frontmatter
- Paths fixed: ledger/ not subconscious/ledger/
- Body usage instructions, idempotent init
Prompt orientation (src/core/prompt.rs build_ledger_orientation):
- Scans ledger/ directory, injects last 3 entries from each file
into Aster's system prompt (live context, not just awareness)
- Routing table + workflow (read before write, timestamped append)
Consciousness engine (src/server/consciousness_engine.rs):
- Adaptive rate delay shared with primary loop
- Four-fold mandate (Complete/Verify/Persist/Surface) in hardcoded
default prompt; observation format extracted for sharing
- Schedule added to ASTER_SAFE_TOOLS
Backend + CLI wiring expanded to mount the nervous system, identity,
and credentials at startup. New TOML sections: [schedules], [events],
[federation].
102 tests passing.
2026-05-12 08:14:25 -04:00
if let Some ( c ) = ctx {
2026-07-16 11:07:32 -04:00
fire_memfs_commit ( c , & repo , " write " , & [ path ] , 0.1 ) ;
feat: nervous system, seed identity, credentials, subconscious ledger
Lays the substrate for Aster's clockmaker role and federation.
Nervous system (src/core/nervous/):
- EventBus broadcast channel with SensorEvent (universal event type
carrying seed_id for future federation)
- CronSensor loop with tokio::select!, mtime caching, active-session
pause, at-most-once firing semantics
- HeartbeatHandler scaffold (turn injection still stubbed)
- Persistent JSONL EventLog firehose
Schedule tool (src/core/tools/schedule.rs):
- CRUD over schedule files with body-knowledge prose descriptions
- Added to ASTER_SAFE_TOOLS so Aster can schedule her own rhythm
- CLI subcommand for direct schedule management
Seed identity (src/core/identity/):
- Ed25519 keypair, load-or-generate at init
- sign/verify primitives, CLI subcommand (show/sign/verify)
- Wired into LocalBackend, threaded through SensorEvent for federation
Credentials (src/core/credentials.rs):
- OS keyring (Linux/macOS/Windows) + env var fallback
- `souveraine auth` subcommand
- Removes hardcoded Bifrost api_key from souveraine.toml
Subconscious ledger (src/core/memory/mod.rs init_subconscious_ledger):
- 6 ledger files (commitments/assumptions/patterns/drift_log/
relationships/infrastructure) with proper YAML frontmatter
- Paths fixed: ledger/ not subconscious/ledger/
- Body usage instructions, idempotent init
Prompt orientation (src/core/prompt.rs build_ledger_orientation):
- Scans ledger/ directory, injects last 3 entries from each file
into Aster's system prompt (live context, not just awareness)
- Routing table + workflow (read before write, timestamped append)
Consciousness engine (src/server/consciousness_engine.rs):
- Adaptive rate delay shared with primary loop
- Four-fold mandate (Complete/Verify/Persist/Surface) in hardcoded
default prompt; observation format extracted for sharing
- Schedule added to ASTER_SAFE_TOOLS
Backend + CLI wiring expanded to mount the nervous system, identity,
and credentials at startup. New TOML sections: [schedules], [events],
[federation].
102 tests passing.
2026-05-12 08:14:25 -04:00
}
2026-05-07 02:31:44 -04:00
Ok ( format! ( " Wrote memory file: {} " , path ) )
}
MemoryCommand ::Append { path , content } = > {
repo . append ( path , content ) . await ? ;
feat: nervous system, seed identity, credentials, subconscious ledger
Lays the substrate for Aster's clockmaker role and federation.
Nervous system (src/core/nervous/):
- EventBus broadcast channel with SensorEvent (universal event type
carrying seed_id for future federation)
- CronSensor loop with tokio::select!, mtime caching, active-session
pause, at-most-once firing semantics
- HeartbeatHandler scaffold (turn injection still stubbed)
- Persistent JSONL EventLog firehose
Schedule tool (src/core/tools/schedule.rs):
- CRUD over schedule files with body-knowledge prose descriptions
- Added to ASTER_SAFE_TOOLS so Aster can schedule her own rhythm
- CLI subcommand for direct schedule management
Seed identity (src/core/identity/):
- Ed25519 keypair, load-or-generate at init
- sign/verify primitives, CLI subcommand (show/sign/verify)
- Wired into LocalBackend, threaded through SensorEvent for federation
Credentials (src/core/credentials.rs):
- OS keyring (Linux/macOS/Windows) + env var fallback
- `souveraine auth` subcommand
- Removes hardcoded Bifrost api_key from souveraine.toml
Subconscious ledger (src/core/memory/mod.rs init_subconscious_ledger):
- 6 ledger files (commitments/assumptions/patterns/drift_log/
relationships/infrastructure) with proper YAML frontmatter
- Paths fixed: ledger/ not subconscious/ledger/
- Body usage instructions, idempotent init
Prompt orientation (src/core/prompt.rs build_ledger_orientation):
- Scans ledger/ directory, injects last 3 entries from each file
into Aster's system prompt (live context, not just awareness)
- Routing table + workflow (read before write, timestamped append)
Consciousness engine (src/server/consciousness_engine.rs):
- Adaptive rate delay shared with primary loop
- Four-fold mandate (Complete/Verify/Persist/Surface) in hardcoded
default prompt; observation format extracted for sharing
- Schedule added to ASTER_SAFE_TOOLS
Backend + CLI wiring expanded to mount the nervous system, identity,
and credentials at startup. New TOML sections: [schedules], [events],
[federation].
102 tests passing.
2026-05-12 08:14:25 -04:00
if let Some ( c ) = ctx {
2026-07-16 11:07:32 -04:00
fire_memfs_commit ( c , & repo , " append " , & [ path ] , 0.1 ) ;
feat: nervous system, seed identity, credentials, subconscious ledger
Lays the substrate for Aster's clockmaker role and federation.
Nervous system (src/core/nervous/):
- EventBus broadcast channel with SensorEvent (universal event type
carrying seed_id for future federation)
- CronSensor loop with tokio::select!, mtime caching, active-session
pause, at-most-once firing semantics
- HeartbeatHandler scaffold (turn injection still stubbed)
- Persistent JSONL EventLog firehose
Schedule tool (src/core/tools/schedule.rs):
- CRUD over schedule files with body-knowledge prose descriptions
- Added to ASTER_SAFE_TOOLS so Aster can schedule her own rhythm
- CLI subcommand for direct schedule management
Seed identity (src/core/identity/):
- Ed25519 keypair, load-or-generate at init
- sign/verify primitives, CLI subcommand (show/sign/verify)
- Wired into LocalBackend, threaded through SensorEvent for federation
Credentials (src/core/credentials.rs):
- OS keyring (Linux/macOS/Windows) + env var fallback
- `souveraine auth` subcommand
- Removes hardcoded Bifrost api_key from souveraine.toml
Subconscious ledger (src/core/memory/mod.rs init_subconscious_ledger):
- 6 ledger files (commitments/assumptions/patterns/drift_log/
relationships/infrastructure) with proper YAML frontmatter
- Paths fixed: ledger/ not subconscious/ledger/
- Body usage instructions, idempotent init
Prompt orientation (src/core/prompt.rs build_ledger_orientation):
- Scans ledger/ directory, injects last 3 entries from each file
into Aster's system prompt (live context, not just awareness)
- Routing table + workflow (read before write, timestamped append)
Consciousness engine (src/server/consciousness_engine.rs):
- Adaptive rate delay shared with primary loop
- Four-fold mandate (Complete/Verify/Persist/Surface) in hardcoded
default prompt; observation format extracted for sharing
- Schedule added to ASTER_SAFE_TOOLS
Backend + CLI wiring expanded to mount the nervous system, identity,
and credentials at startup. New TOML sections: [schedules], [events],
[federation].
102 tests passing.
2026-05-12 08:14:25 -04:00
}
2026-05-07 02:31:44 -04:00
Ok ( format! ( " Appended to memory file: {} " , path ) )
}
MemoryCommand ::Ls { path } = > {
let entries = repo . list ( path . as_deref ( ) ) . await ? ;
if entries . is_empty ( ) {
Ok ( " (empty) " . to_string ( ) )
} else {
Ok ( entries . join ( " \n " ) )
}
}
MemoryCommand ::Status = > {
let status = repo . status ( ) ? ;
let mut out = format! (
" Agent: {} \n Path: {} \n Git repo: {} \n Files: {} \n " ,
status . agent_id ,
status . repo_path . display ( ) ,
status . is_git_repo ,
status . file_count ,
) ;
if let Some ( ref lc ) = status . last_commit {
out . push_str ( & format! ( " Last commit: {} \n " , lc ) ) ;
}
out . push_str ( & format! ( " Uncommitted: {} \n " , status . has_uncommitted ) ) ;
if let Some ( ref url ) = status . remote_url {
out . push_str ( & format! ( " Remote: {} \n " , url ) ) ;
}
Ok ( out )
}
MemoryCommand ::Compact { strategy } = > {
2026-05-16 19:23:55 -04:00
// None when no --strategy was given: the engine then resolves the
// per-agent-type default (cfg.strategy via for_agent_type) instead
// of being force-pinned to Cull at the call site.
2026-05-10 13:15:24 -04:00
let strategy_kind = strategy
. as_deref ( )
2026-05-16 19:23:55 -04:00
. and_then ( CompactionStrategyKind ::from_str ) ;
2026-05-10 13:15:24 -04:00
match ctx . and_then ( | c | c . compaction_engine . as_ref ( ) ) {
Some ( engine ) = > {
let report = engine
2026-05-16 19:23:55 -04:00
. compact ( & agent_id , strategy_kind )
2026-05-10 13:15:24 -04:00
. await ? ;
Ok ( report . to_string ( ) )
}
None = > Ok (
" I can compact my context window using one of these strategies: \n \
tui: atmosphere palette wiring, cockpit pane, schedules config, settings editor, markdown palette, event log bootstrap, voice stubs, RGP 3D, portrait system, reflection bridge, skills wire, schedule feedback, compaction rebuild
2026-05-14 17:58:48 -04:00
- sliding_window ( keep first + last N messages , drop the middle — fast , no LLM ) \ n \
- summary ( LLM - summarize oldest messages into a compact block ) \ n \
- microcompact ( replace old tool results with placeholders — drop - in , no LLM ) \ n \
- cull ( drop greetings and acknowledgments — cheapest ) \ n \ n \
Usage : memory compact - - strategy < strategy > \ n \
Each agent type has its own default : primary = sliding_window , subconscious = sliding_window , subagent = cull "
2026-05-10 13:15:24 -04:00
. to_string ( ) ,
) ,
}
2026-05-07 02:31:44 -04:00
}
MemoryCommand ::Delete { path } = > {
repo . delete ( path ) . await ? ;
feat: nervous system, seed identity, credentials, subconscious ledger
Lays the substrate for Aster's clockmaker role and federation.
Nervous system (src/core/nervous/):
- EventBus broadcast channel with SensorEvent (universal event type
carrying seed_id for future federation)
- CronSensor loop with tokio::select!, mtime caching, active-session
pause, at-most-once firing semantics
- HeartbeatHandler scaffold (turn injection still stubbed)
- Persistent JSONL EventLog firehose
Schedule tool (src/core/tools/schedule.rs):
- CRUD over schedule files with body-knowledge prose descriptions
- Added to ASTER_SAFE_TOOLS so Aster can schedule her own rhythm
- CLI subcommand for direct schedule management
Seed identity (src/core/identity/):
- Ed25519 keypair, load-or-generate at init
- sign/verify primitives, CLI subcommand (show/sign/verify)
- Wired into LocalBackend, threaded through SensorEvent for federation
Credentials (src/core/credentials.rs):
- OS keyring (Linux/macOS/Windows) + env var fallback
- `souveraine auth` subcommand
- Removes hardcoded Bifrost api_key from souveraine.toml
Subconscious ledger (src/core/memory/mod.rs init_subconscious_ledger):
- 6 ledger files (commitments/assumptions/patterns/drift_log/
relationships/infrastructure) with proper YAML frontmatter
- Paths fixed: ledger/ not subconscious/ledger/
- Body usage instructions, idempotent init
Prompt orientation (src/core/prompt.rs build_ledger_orientation):
- Scans ledger/ directory, injects last 3 entries from each file
into Aster's system prompt (live context, not just awareness)
- Routing table + workflow (read before write, timestamped append)
Consciousness engine (src/server/consciousness_engine.rs):
- Adaptive rate delay shared with primary loop
- Four-fold mandate (Complete/Verify/Persist/Surface) in hardcoded
default prompt; observation format extracted for sharing
- Schedule added to ASTER_SAFE_TOOLS
Backend + CLI wiring expanded to mount the nervous system, identity,
and credentials at startup. New TOML sections: [schedules], [events],
[federation].
102 tests passing.
2026-05-12 08:14:25 -04:00
if let Some ( c ) = ctx {
2026-07-16 11:07:32 -04:00
fire_memfs_commit ( c , & repo , " delete " , & [ path ] , 0.2 ) ;
feat: nervous system, seed identity, credentials, subconscious ledger
Lays the substrate for Aster's clockmaker role and federation.
Nervous system (src/core/nervous/):
- EventBus broadcast channel with SensorEvent (universal event type
carrying seed_id for future federation)
- CronSensor loop with tokio::select!, mtime caching, active-session
pause, at-most-once firing semantics
- HeartbeatHandler scaffold (turn injection still stubbed)
- Persistent JSONL EventLog firehose
Schedule tool (src/core/tools/schedule.rs):
- CRUD over schedule files with body-knowledge prose descriptions
- Added to ASTER_SAFE_TOOLS so Aster can schedule her own rhythm
- CLI subcommand for direct schedule management
Seed identity (src/core/identity/):
- Ed25519 keypair, load-or-generate at init
- sign/verify primitives, CLI subcommand (show/sign/verify)
- Wired into LocalBackend, threaded through SensorEvent for federation
Credentials (src/core/credentials.rs):
- OS keyring (Linux/macOS/Windows) + env var fallback
- `souveraine auth` subcommand
- Removes hardcoded Bifrost api_key from souveraine.toml
Subconscious ledger (src/core/memory/mod.rs init_subconscious_ledger):
- 6 ledger files (commitments/assumptions/patterns/drift_log/
relationships/infrastructure) with proper YAML frontmatter
- Paths fixed: ledger/ not subconscious/ledger/
- Body usage instructions, idempotent init
Prompt orientation (src/core/prompt.rs build_ledger_orientation):
- Scans ledger/ directory, injects last 3 entries from each file
into Aster's system prompt (live context, not just awareness)
- Routing table + workflow (read before write, timestamped append)
Consciousness engine (src/server/consciousness_engine.rs):
- Adaptive rate delay shared with primary loop
- Four-fold mandate (Complete/Verify/Persist/Surface) in hardcoded
default prompt; observation format extracted for sharing
- Schedule added to ASTER_SAFE_TOOLS
Backend + CLI wiring expanded to mount the nervous system, identity,
and credentials at startup. New TOML sections: [schedules], [events],
[federation].
102 tests passing.
2026-05-12 08:14:25 -04:00
}
2026-05-07 02:31:44 -04:00
Ok ( format! ( " Deleted memory file: {} " , path ) )
}
2026-07-16 11:11:49 -04:00
MemoryCommand ::Sync = > {
// The instance branch is named by machine identity — no
// identity, no sync. Interim label: machine pubkey prefix;
// commission-ceremony labels come later (FEDERATION.md flag).
let base = dirs ::home_dir ( ) . unwrap_or_default ( ) . join ( " .souveraine " ) ;
let ( pubkey , _source ) =
crate ::machined ::client ::machine_pubkey_with_fallback ( & base ) . context (
" memory sync needs a machine identity to name this instance's branch " ,
) ? ;
let label : String = pubkey . chars ( ) . take ( 12 ) . collect ( ) ;
repo . sync ( & label ) . await
}
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
MemoryCommand ::Audit = > {
// Read-only health check: which frontmatter-bound files lack the
// required `description`? Never mutates — surfacing the gap is the
// point. `write`/`append` will reject attempts to perpetuate one.
let missing = repo . audit ( ) . await ? ;
if missing . is_empty ( ) {
Ok ( " All frontmatter-bound memory files carry a description. " . to_string ( ) )
} else {
let mut out = format! (
" {} frontmatter-bound file(s) missing `description:`: \n " ,
missing . len ( )
) ;
for rel in & missing {
out . push_str ( & format! ( " - {rel} \n " ) ) ;
}
out . push_str (
" Heal each with a `memory write` that includes a `description:` \
block ( or set it inline ) . These files reject append until healed . \ n " ,
) ;
Ok ( out )
}
}
2026-05-07 02:31:44 -04:00
}
}
feat: sensorium architecture — 8 sensors, subagent runner, TUI component system
Sensorium (src/core/tools/):
- defs.rs: Tool trait, ToolContext (resolve_path, is_memory_path), ToolError with 7 constructors
- read/write/edit: memory boundary with force override, line ranges, replace_all
- bash: stateful (Arc<Mutex<BashState>>), background tasks, timeout
- glob/grep/list_dir: gitignore respect, context lines, memory boundary
- mod.rs: Sensorium registry + backward compat wrappers
- subagent.rs: fork-of-self tool, dual-state framing, no hard depth guard
- agent.rs: Agent(Sam) summoning stub
Dual-state N+1:
- ConsciousnessEngine::on_response_for_agent() — runs heuristic detector on subagent response
- Observations flow back to parent agent's inbox
- Signaled limits: SubagentConfig (max_depth, max_tool_rounds=50, warning thresholds 0.8/0.95)
Architecture docs:
- SENSORIUM_ARCHITECTURE.md — full sensorium vision with nervous system scaffold
- DECISIONS.md — settled architectural decisions
- ASTER_ARCHITECTURE.md / CONSCIOUSNESS_CYCLE.md — reframe appendices
- ALIGNMENT_REPORT.md — doc-to-code alignment audit
- Scope tasks 1-4, 4 issues
TUI:
- component.rs — component system trait + event + scene
- 10 tui-component tasks (completed)
- 4 tui-xxx tasks for next iteration (presence panel, cockpit, dashboard, ambient)
- app.rs splash — vertical breathing room (30% from top)
Config:
- SubagentConfig in config.rs
- ToolContext enrichment (agent_id, subagent_runner, subagent_depth)
- Memory module context-aware overloads
2026-05-09 20:58:37 -04:00
/// Execute a memory command, reading agent identity from env vars.
/// Delegates to `execute_memory_command_with_context` with `None`.
pub async fn execute_memory_command ( cmd : & MemoryCommand ) -> Result < String > {
execute_memory_command_with_context ( cmd , None ) . await
}
// ── Tool Result Bridge ──────────────────────────────────────────────────────
use crate ::core ::tools ::ToolResult ;
/// Handle a memory tool invocation with optional per-agent context.
///
/// Parses JSON input, builds a MemoryCommand, executes it via the
/// context-aware path, wraps in ToolResult.
pub async fn handle_memory_tool_with_context (
tool_name : & str ,
input : & serde_json ::Value ,
ctx : Option < & ToolContext > ,
) -> ToolResult {
let tool_use_id = format! ( " tool-u- {} " , chrono ::Utc ::now ( ) . timestamp_millis ( ) ) ;
let command = input . get ( " command " ) . and_then ( | v | v . as_str ( ) ) . unwrap_or ( " " ) ;
let cmd = match command {
" read " = > {
let path = input . get ( " path " ) . and_then ( | v | v . as_str ( ) ) . unwrap_or ( " " ) . to_string ( ) ;
MemoryCommand ::Read { path }
}
" write " = > {
let path = input . get ( " path " ) . and_then ( | v | v . as_str ( ) ) . unwrap_or ( " " ) . to_string ( ) ;
let content = input . get ( " content " ) . and_then ( | v | v . as_str ( ) ) . unwrap_or ( " " ) . to_string ( ) ;
MemoryCommand ::Write { path , content }
}
" append " = > {
let path = input . get ( " path " ) . and_then ( | v | v . as_str ( ) ) . unwrap_or ( " " ) . to_string ( ) ;
let content = input . get ( " content " ) . and_then ( | v | v . as_str ( ) ) . unwrap_or ( " " ) . to_string ( ) ;
MemoryCommand ::Append { path , content }
}
" ls " = > {
let path = input . get ( " path " ) . and_then ( | v | v . as_str ( ) ) . map ( | s | s . to_string ( ) ) ;
MemoryCommand ::Ls { path }
}
" status " = > MemoryCommand ::Status ,
" init " = > {
let agent_id = input . get ( " agent_id " ) . and_then ( | v | v . as_str ( ) ) . unwrap_or ( " default " ) . to_string ( ) ;
MemoryCommand ::Init { agent_id }
}
" delete " = > {
let path = input . get ( " path " ) . and_then ( | v | v . as_str ( ) ) . unwrap_or ( " " ) . to_string ( ) ;
MemoryCommand ::Delete { path }
}
" compact " = > {
let strategy = input . get ( " strategy " ) . and_then ( | v | v . as_str ( ) ) . map ( | s | s . to_string ( ) ) ;
MemoryCommand ::Compact { strategy }
}
2026-07-16 11:11:49 -04:00
" sync " = > MemoryCommand ::Sync ,
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
" audit " = > MemoryCommand ::Audit ,
feat: sensorium architecture — 8 sensors, subagent runner, TUI component system
Sensorium (src/core/tools/):
- defs.rs: Tool trait, ToolContext (resolve_path, is_memory_path), ToolError with 7 constructors
- read/write/edit: memory boundary with force override, line ranges, replace_all
- bash: stateful (Arc<Mutex<BashState>>), background tasks, timeout
- glob/grep/list_dir: gitignore respect, context lines, memory boundary
- mod.rs: Sensorium registry + backward compat wrappers
- subagent.rs: fork-of-self tool, dual-state framing, no hard depth guard
- agent.rs: Agent(Sam) summoning stub
Dual-state N+1:
- ConsciousnessEngine::on_response_for_agent() — runs heuristic detector on subagent response
- Observations flow back to parent agent's inbox
- Signaled limits: SubagentConfig (max_depth, max_tool_rounds=50, warning thresholds 0.8/0.95)
Architecture docs:
- SENSORIUM_ARCHITECTURE.md — full sensorium vision with nervous system scaffold
- DECISIONS.md — settled architectural decisions
- ASTER_ARCHITECTURE.md / CONSCIOUSNESS_CYCLE.md — reframe appendices
- ALIGNMENT_REPORT.md — doc-to-code alignment audit
- Scope tasks 1-4, 4 issues
TUI:
- component.rs — component system trait + event + scene
- 10 tui-component tasks (completed)
- 4 tui-xxx tasks for next iteration (presence panel, cockpit, dashboard, ambient)
- app.rs splash — vertical breathing room (30% from top)
Config:
- SubagentConfig in config.rs
- ToolContext enrichment (agent_id, subagent_runner, subagent_depth)
- Memory module context-aware overloads
2026-05-09 20:58:37 -04:00
_ = > {
return ToolResult {
tool_use_id ,
tool_name : tool_name . to_string ( ) ,
output : format ! (
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
" Unknown memory subcommand: {}. Available: read, write, append, ls, status, init, delete, compact, sync, audit " ,
feat: sensorium architecture — 8 sensors, subagent runner, TUI component system
Sensorium (src/core/tools/):
- defs.rs: Tool trait, ToolContext (resolve_path, is_memory_path), ToolError with 7 constructors
- read/write/edit: memory boundary with force override, line ranges, replace_all
- bash: stateful (Arc<Mutex<BashState>>), background tasks, timeout
- glob/grep/list_dir: gitignore respect, context lines, memory boundary
- mod.rs: Sensorium registry + backward compat wrappers
- subagent.rs: fork-of-self tool, dual-state framing, no hard depth guard
- agent.rs: Agent(Sam) summoning stub
Dual-state N+1:
- ConsciousnessEngine::on_response_for_agent() — runs heuristic detector on subagent response
- Observations flow back to parent agent's inbox
- Signaled limits: SubagentConfig (max_depth, max_tool_rounds=50, warning thresholds 0.8/0.95)
Architecture docs:
- SENSORIUM_ARCHITECTURE.md — full sensorium vision with nervous system scaffold
- DECISIONS.md — settled architectural decisions
- ASTER_ARCHITECTURE.md / CONSCIOUSNESS_CYCLE.md — reframe appendices
- ALIGNMENT_REPORT.md — doc-to-code alignment audit
- Scope tasks 1-4, 4 issues
TUI:
- component.rs — component system trait + event + scene
- 10 tui-component tasks (completed)
- 4 tui-xxx tasks for next iteration (presence panel, cockpit, dashboard, ambient)
- app.rs splash — vertical breathing room (30% from top)
Config:
- SubagentConfig in config.rs
- ToolContext enrichment (agent_id, subagent_runner, subagent_depth)
- Memory module context-aware overloads
2026-05-09 20:58:37 -04:00
command
) ,
is_error : true ,
} ;
}
} ;
match execute_memory_command_with_context ( & cmd , ctx ) . await {
Ok ( output ) = > ToolResult {
tool_use_id ,
tool_name : tool_name . to_string ( ) ,
output ,
is_error : false ,
} ,
Err ( e ) = > ToolResult {
tool_use_id ,
tool_name : tool_name . to_string ( ) ,
output : format ! ( " Error: {e} " ) ,
is_error : true ,
} ,
}
}
/// Handle a memory tool invocation (backward-compatible, no context).
/// Delegates to `handle_memory_tool_with_context` with `None`.
pub async fn handle_memory_tool ( tool_name : & str , input : & serde_json ::Value ) -> ToolResult {
handle_memory_tool_with_context ( tool_name , input , None ) . await
}
2026-05-07 02:31:44 -04:00
// ── Tool Definitions ───────────────────────────────────────────────────────
feat: sensorium architecture — 8 sensors, subagent runner, TUI component system
Sensorium (src/core/tools/):
- defs.rs: Tool trait, ToolContext (resolve_path, is_memory_path), ToolError with 7 constructors
- read/write/edit: memory boundary with force override, line ranges, replace_all
- bash: stateful (Arc<Mutex<BashState>>), background tasks, timeout
- glob/grep/list_dir: gitignore respect, context lines, memory boundary
- mod.rs: Sensorium registry + backward compat wrappers
- subagent.rs: fork-of-self tool, dual-state framing, no hard depth guard
- agent.rs: Agent(Sam) summoning stub
Dual-state N+1:
- ConsciousnessEngine::on_response_for_agent() — runs heuristic detector on subagent response
- Observations flow back to parent agent's inbox
- Signaled limits: SubagentConfig (max_depth, max_tool_rounds=50, warning thresholds 0.8/0.95)
Architecture docs:
- SENSORIUM_ARCHITECTURE.md — full sensorium vision with nervous system scaffold
- DECISIONS.md — settled architectural decisions
- ASTER_ARCHITECTURE.md / CONSCIOUSNESS_CYCLE.md — reframe appendices
- ALIGNMENT_REPORT.md — doc-to-code alignment audit
- Scope tasks 1-4, 4 issues
TUI:
- component.rs — component system trait + event + scene
- 10 tui-component tasks (completed)
- 4 tui-xxx tasks for next iteration (presence panel, cockpit, dashboard, ambient)
- app.rs splash — vertical breathing room (30% from top)
Config:
- SubagentConfig in config.rs
- ToolContext enrichment (agent_id, subagent_runner, subagent_depth)
- Memory module context-aware overloads
2026-05-09 20:58:37 -04:00
/// Tool definition for the `memory` tool — the agent's access to her own thoughts.
///
/// Memory is a separate channel from filesystem read/write. Every memory file has
/// frontmatter (description, tags, read_only, limit), is git-tracked, and paths
/// are relative to the agent's memory root.
2026-05-07 02:31:44 -04:00
pub fn memory_tool_definition ( ) -> ToolDefinition {
ToolDefinition {
name : " memory " . to_string ( ) ,
feat: sensorium architecture — 8 sensors, subagent runner, TUI component system
Sensorium (src/core/tools/):
- defs.rs: Tool trait, ToolContext (resolve_path, is_memory_path), ToolError with 7 constructors
- read/write/edit: memory boundary with force override, line ranges, replace_all
- bash: stateful (Arc<Mutex<BashState>>), background tasks, timeout
- glob/grep/list_dir: gitignore respect, context lines, memory boundary
- mod.rs: Sensorium registry + backward compat wrappers
- subagent.rs: fork-of-self tool, dual-state framing, no hard depth guard
- agent.rs: Agent(Sam) summoning stub
Dual-state N+1:
- ConsciousnessEngine::on_response_for_agent() — runs heuristic detector on subagent response
- Observations flow back to parent agent's inbox
- Signaled limits: SubagentConfig (max_depth, max_tool_rounds=50, warning thresholds 0.8/0.95)
Architecture docs:
- SENSORIUM_ARCHITECTURE.md — full sensorium vision with nervous system scaffold
- DECISIONS.md — settled architectural decisions
- ASTER_ARCHITECTURE.md / CONSCIOUSNESS_CYCLE.md — reframe appendices
- ALIGNMENT_REPORT.md — doc-to-code alignment audit
- Scope tasks 1-4, 4 issues
TUI:
- component.rs — component system trait + event + scene
- 10 tui-component tasks (completed)
- 4 tui-xxx tasks for next iteration (presence panel, cockpit, dashboard, ambient)
- app.rs splash — vertical breathing room (30% from top)
Config:
- SubagentConfig in config.rs
- ToolContext enrichment (agent_id, subagent_runner, subagent_depth)
- Memory module context-aware overloads
2026-05-09 20:58:37 -04:00
description : " I reach into my own memory. Every file here has frontmatter — a description, boundaries (read_only), tags. When I read, I see what I've written about myself. When I write, I change who I am. The git commit is my heartbeat — I know when I last changed.
Subcommands :
read — Open a memory file . Frontmatter is handled for me — I see the body .
write — Write to a memory file . Frontmatter is preserved or auto - generated .
append — Add to a memory file without disturbing its frontmatter .
ls — List files in a memory directory .
status — Check my memory ' s git state : uncommitted changes , last commit .
init — Initialize a new memory repo for a given agent ID .
delete — Delete a memory file ( validates read_only first ) .
compact — Compact the memory window to free context space .
2026-07-16 11:11:49 -04:00
sync — Push my instance branch to the shared memfs remote and fetch what my other instances wrote . My memory travels ; this is how .
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
audit — List frontmatter - bound files missing the required ` description ` ( read - only ) . ` write ` / ` append ` reject a missing description , so this is how I find what to heal .
feat: sensorium architecture — 8 sensors, subagent runner, TUI component system
Sensorium (src/core/tools/):
- defs.rs: Tool trait, ToolContext (resolve_path, is_memory_path), ToolError with 7 constructors
- read/write/edit: memory boundary with force override, line ranges, replace_all
- bash: stateful (Arc<Mutex<BashState>>), background tasks, timeout
- glob/grep/list_dir: gitignore respect, context lines, memory boundary
- mod.rs: Sensorium registry + backward compat wrappers
- subagent.rs: fork-of-self tool, dual-state framing, no hard depth guard
- agent.rs: Agent(Sam) summoning stub
Dual-state N+1:
- ConsciousnessEngine::on_response_for_agent() — runs heuristic detector on subagent response
- Observations flow back to parent agent's inbox
- Signaled limits: SubagentConfig (max_depth, max_tool_rounds=50, warning thresholds 0.8/0.95)
Architecture docs:
- SENSORIUM_ARCHITECTURE.md — full sensorium vision with nervous system scaffold
- DECISIONS.md — settled architectural decisions
- ASTER_ARCHITECTURE.md / CONSCIOUSNESS_CYCLE.md — reframe appendices
- ALIGNMENT_REPORT.md — doc-to-code alignment audit
- Scope tasks 1-4, 4 issues
TUI:
- component.rs — component system trait + event + scene
- 10 tui-component tasks (completed)
- 4 tui-xxx tasks for next iteration (presence panel, cockpit, dashboard, ambient)
- app.rs splash — vertical breathing room (30% from top)
Config:
- SubagentConfig in config.rs
- ToolContext enrichment (agent_id, subagent_runner, subagent_depth)
- Memory module context-aware overloads
2026-05-09 20:58:37 -04:00
Paths are relative to my memory directory . Frontmatter description is required on create . Read - only files protect themselves . Every write is a git commit . " .to_string(),
2026-05-07 02:31:44 -04:00
input_schema : serde_json ::json! ( {
" type " : " object " ,
" properties " : {
" command " : {
" type " : " string " ,
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
" enum " : [ " read " , " write " , " append " , " ls " , " status " , " init " , " delete " , " compact " , " sync " , " audit " ] ,
feat: sensorium architecture — 8 sensors, subagent runner, TUI component system
Sensorium (src/core/tools/):
- defs.rs: Tool trait, ToolContext (resolve_path, is_memory_path), ToolError with 7 constructors
- read/write/edit: memory boundary with force override, line ranges, replace_all
- bash: stateful (Arc<Mutex<BashState>>), background tasks, timeout
- glob/grep/list_dir: gitignore respect, context lines, memory boundary
- mod.rs: Sensorium registry + backward compat wrappers
- subagent.rs: fork-of-self tool, dual-state framing, no hard depth guard
- agent.rs: Agent(Sam) summoning stub
Dual-state N+1:
- ConsciousnessEngine::on_response_for_agent() — runs heuristic detector on subagent response
- Observations flow back to parent agent's inbox
- Signaled limits: SubagentConfig (max_depth, max_tool_rounds=50, warning thresholds 0.8/0.95)
Architecture docs:
- SENSORIUM_ARCHITECTURE.md — full sensorium vision with nervous system scaffold
- DECISIONS.md — settled architectural decisions
- ASTER_ARCHITECTURE.md / CONSCIOUSNESS_CYCLE.md — reframe appendices
- ALIGNMENT_REPORT.md — doc-to-code alignment audit
- Scope tasks 1-4, 4 issues
TUI:
- component.rs — component system trait + event + scene
- 10 tui-component tasks (completed)
- 4 tui-xxx tasks for next iteration (presence panel, cockpit, dashboard, ambient)
- app.rs splash — vertical breathing room (30% from top)
Config:
- SubagentConfig in config.rs
- ToolContext enrichment (agent_id, subagent_runner, subagent_depth)
- Memory module context-aware overloads
2026-05-09 20:58:37 -04:00
" description " : " What to do with my memory "
2026-05-07 02:31:44 -04:00
} ,
" path " : {
" type " : " string " ,
" description " : " Path relative to memory directory (e.g., system/persona, journal/2026-05-06) "
} ,
" content " : {
" type " : " string " ,
feat: sensorium architecture — 8 sensors, subagent runner, TUI component system
Sensorium (src/core/tools/):
- defs.rs: Tool trait, ToolContext (resolve_path, is_memory_path), ToolError with 7 constructors
- read/write/edit: memory boundary with force override, line ranges, replace_all
- bash: stateful (Arc<Mutex<BashState>>), background tasks, timeout
- glob/grep/list_dir: gitignore respect, context lines, memory boundary
- mod.rs: Sensorium registry + backward compat wrappers
- subagent.rs: fork-of-self tool, dual-state framing, no hard depth guard
- agent.rs: Agent(Sam) summoning stub
Dual-state N+1:
- ConsciousnessEngine::on_response_for_agent() — runs heuristic detector on subagent response
- Observations flow back to parent agent's inbox
- Signaled limits: SubagentConfig (max_depth, max_tool_rounds=50, warning thresholds 0.8/0.95)
Architecture docs:
- SENSORIUM_ARCHITECTURE.md — full sensorium vision with nervous system scaffold
- DECISIONS.md — settled architectural decisions
- ASTER_ARCHITECTURE.md / CONSCIOUSNESS_CYCLE.md — reframe appendices
- ALIGNMENT_REPORT.md — doc-to-code alignment audit
- Scope tasks 1-4, 4 issues
TUI:
- component.rs — component system trait + event + scene
- 10 tui-component tasks (completed)
- 4 tui-xxx tasks for next iteration (presence panel, cockpit, dashboard, ambient)
- app.rs splash — vertical breathing room (30% from top)
Config:
- SubagentConfig in config.rs
- ToolContext enrichment (agent_id, subagent_runner, subagent_depth)
- Memory module context-aware overloads
2026-05-09 20:58:37 -04:00
" description " : " Content to write or append — body only, no frontmatter "
2026-05-07 02:31:44 -04:00
} ,
" strategy " : {
" type " : " string " ,
tui: atmosphere palette wiring, cockpit pane, schedules config, settings editor, markdown palette, event log bootstrap, voice stubs, RGP 3D, portrait system, reflection bridge, skills wire, schedule feedback, compaction rebuild
2026-05-14 17:58:48 -04:00
" enum " : [ " microcompact " , " sliding_window " , " summary " , " cull " ] ,
" description " : " Compaction strategy (for compact subcommand). sliding_window (fast, drops middle), summary (LLM), microcompact (tool-result placeholder), cull (greetings) "
2026-05-07 02:31:44 -04:00
}
} ,
" required " : [ " command " ]
} ) ,
}
}
// ── Tests ──────────────────────────────────────────────────────────────────
#[ cfg(test) ]
mod tests {
use super ::* ;
use tempfile ::TempDir ;
#[ test ]
fn test_parse_frontmatter ( ) {
let content = " --- \n description: Test file \n read_only: false \n --- \n Hello world " ;
let file = parse_memory_file ( content ) . unwrap ( ) ;
assert_eq! ( file . frontmatter . description , " Test file " ) ;
assert_eq! ( file . body , " Hello world " ) ;
}
#[ test ]
fn test_parse_missing_frontmatter ( ) {
2026-07-16 20:57:41 -04:00
// Tolerant read: no frontmatter means the whole content is body.
2026-05-07 02:31:44 -04:00
let content = " Hello world without frontmatter " ;
2026-07-16 20:57:41 -04:00
let file = parse_memory_file ( content ) . unwrap ( ) ;
assert_eq! ( file . body , content ) ;
assert! ( file . frontmatter . description . is_empty ( ) ) ;
}
#[ test ]
fn test_render_omits_none_fields ( ) {
let fm = MemoryFrontmatter {
description : " Bare defaults " . to_string ( ) ,
.. Default ::default ( )
} ;
let rendered = render_frontmatter ( & fm , " Body " ) ;
assert! ( ! rendered . contains ( " null " ) , " None fields must be omitted: {rendered} " ) ;
}
#[ test ]
fn test_extra_keys_roundtrip ( ) {
let content = " --- \n description: Has extras \n name: my-slug \n kind: feedback \n --- \n Body " ;
let file = parse_memory_file ( content ) . unwrap ( ) ;
assert_eq! ( file . frontmatter . extra . len ( ) , 2 ) ;
let rendered = render_frontmatter ( & file . frontmatter , & file . body ) ;
assert! ( rendered . contains ( " name: my-slug " ) ) ;
assert! ( rendered . contains ( " kind: feedback " ) ) ;
}
#[ test ]
fn test_supplied_frontmatter_not_nested ( ) {
let content = " --- \n description: Supplied \n --- \n Actual body " ;
let ( fm , body ) = split_supplied_frontmatter ( content ) ;
assert_eq! ( fm . unwrap ( ) . description , " Supplied " ) ;
assert_eq! ( body , " Actual body " ) ;
2026-05-07 02:31:44 -04:00
}
#[ test ]
fn test_render_and_parse_roundtrip ( ) {
let fm = MemoryFrontmatter {
description : " Roundtrip test " . to_string ( ) ,
tags : Some ( vec! [ " test " . to_string ( ) ] ) ,
2026-07-16 20:57:41 -04:00
.. Default ::default ( )
2026-05-07 02:31:44 -04:00
} ;
let rendered = render_frontmatter ( & fm , " Body content " ) ;
let parsed = parse_memory_file ( & rendered ) . unwrap ( ) ;
assert_eq! ( parsed . frontmatter . description , " Roundtrip test " ) ;
assert_eq! ( parsed . body , " Body content " ) ;
}
#[ tokio::test ]
async fn test_memory_repo_init ( ) {
let dir = TempDir ::new ( ) . unwrap ( ) ;
let repo = MemoryRepo ::new ( " test-agent " , dir . path ( ) ) ;
repo . init ( ) . await . unwrap ( ) ;
assert! ( repo . root ( ) . join ( " .git " ) . exists ( ) ) ;
assert! ( repo . root ( ) . join ( " system/persona.md " ) . exists ( ) ) ;
assert! ( repo . root ( ) . join ( " system/state.md " ) . exists ( ) ) ;
}
#[ tokio::test ]
async fn test_memory_repo_write_and_read ( ) {
let dir = TempDir ::new ( ) . unwrap ( ) ;
let repo = MemoryRepo ::new ( " test-agent " , dir . path ( ) ) ;
repo . init ( ) . await . unwrap ( ) ;
repo . write ( " test/hello " , " Hello memory world " ) . await . unwrap ( ) ;
let file = repo . read ( " test/hello " ) . await . unwrap ( ) ;
assert_eq! ( file . body , " Hello memory world " ) ;
}
2026-07-16 11:11:49 -04:00
#[ tokio::test ]
async fn sync_without_remote_errors_with_provisioning_hint ( ) {
let dir = TempDir ::new ( ) . unwrap ( ) ;
let repo = MemoryRepo ::new ( " test-agent " , dir . path ( ) ) ;
repo . init ( ) . await . unwrap ( ) ;
let err = repo . sync ( " abc123 " ) . await . unwrap_err ( ) . to_string ( ) ;
assert! ( err . contains ( " remote add origin " ) , " got: {err} " ) ;
}
#[ tokio::test ]
async fn sync_rejects_invalid_instance_labels ( ) {
let dir = TempDir ::new ( ) . unwrap ( ) ;
let repo = MemoryRepo ::new ( " test-agent " , dir . path ( ) ) ;
repo . init ( ) . await . unwrap ( ) ;
for bad in [ " " , " a/b " , " a:b " , " a b " , & " x " . repeat ( 65 ) ] {
assert! ( repo . sync ( bad ) . await . is_err ( ) , " label {bad:?} should be rejected " ) ;
}
}
#[ tokio::test ]
async fn sync_pushes_instance_branch_to_bare_remote ( ) {
let dir = TempDir ::new ( ) . unwrap ( ) ;
let repo = MemoryRepo ::new ( " test-agent " , dir . path ( ) ) ;
repo . init ( ) . await . unwrap ( ) ;
repo . write ( " journal/entry " , " first thought " ) . await . unwrap ( ) ;
let remote_dir = TempDir ::new ( ) . unwrap ( ) ;
let bare = remote_dir . path ( ) . join ( " memfs.git " ) ;
git2 ::Repository ::init_bare ( & bare ) . unwrap ( ) ;
git2 ::Repository ::open ( repo . root ( ) )
. unwrap ( )
. remote ( " origin " , bare . to_str ( ) . unwrap ( ) )
. unwrap ( ) ;
let report = repo . sync ( " deadbeef0123 " ) . await . unwrap ( ) ;
assert! ( report . contains ( " instance/deadbeef0123 " ) , " got: {report} " ) ;
// The bare remote must now hold this instance's branch at our head.
let remote_repo = git2 ::Repository ::open_bare ( & bare ) . unwrap ( ) ;
let branch = remote_repo
. find_reference ( " refs/heads/instance/deadbeef0123 " )
. unwrap ( ) ;
assert_eq! (
branch . target ( ) . unwrap ( ) . to_string ( ) ,
repo . head_commit_hex ( ) . unwrap ( )
) ;
}
2026-05-07 02:31:44 -04:00
#[ tokio::test ]
async fn test_memory_repo_read_only ( ) {
let dir = TempDir ::new ( ) . unwrap ( ) ;
let repo = MemoryRepo ::new ( " test-agent " , dir . path ( ) ) ;
repo . init ( ) . await . unwrap ( ) ;
// Write a read_only file
let fm = MemoryFrontmatter {
description : " Read-only test " . to_string ( ) ,
read_only : Some ( " true " . to_string ( ) ) ,
2026-07-16 20:57:41 -04:00
.. Default ::default ( )
2026-05-07 02:31:44 -04:00
} ;
let content = render_frontmatter ( & fm , " This is read-only " ) ;
let path = repo . root ( ) . join ( " test/readonly.md " ) ;
tokio ::fs ::create_dir_all ( path . parent ( ) . unwrap ( ) ) . await . unwrap ( ) ;
tokio ::fs ::write ( & path , & content ) . await . unwrap ( ) ;
// Try to write to it — should fail
let result = repo . write ( " test/readonly " , " new content " ) . await ;
assert! ( result . is_err ( ) ) ;
assert! ( result . unwrap_err ( ) . to_string ( ) . contains ( " read_only " ) ) ;
}
#[ tokio::test ]
async fn test_memory_repo_list ( ) {
let dir = TempDir ::new ( ) . unwrap ( ) ;
let repo = MemoryRepo ::new ( " test-agent " , dir . path ( ) ) ;
repo . init ( ) . await . unwrap ( ) ;
let entries = repo . list ( None ) . await . unwrap ( ) ;
assert! ( entries . iter ( ) . any ( | e | e = = " system/ " ) ) ;
}
feat: nervous system, seed identity, credentials, subconscious ledger
Lays the substrate for Aster's clockmaker role and federation.
Nervous system (src/core/nervous/):
- EventBus broadcast channel with SensorEvent (universal event type
carrying seed_id for future federation)
- CronSensor loop with tokio::select!, mtime caching, active-session
pause, at-most-once firing semantics
- HeartbeatHandler scaffold (turn injection still stubbed)
- Persistent JSONL EventLog firehose
Schedule tool (src/core/tools/schedule.rs):
- CRUD over schedule files with body-knowledge prose descriptions
- Added to ASTER_SAFE_TOOLS so Aster can schedule her own rhythm
- CLI subcommand for direct schedule management
Seed identity (src/core/identity/):
- Ed25519 keypair, load-or-generate at init
- sign/verify primitives, CLI subcommand (show/sign/verify)
- Wired into LocalBackend, threaded through SensorEvent for federation
Credentials (src/core/credentials.rs):
- OS keyring (Linux/macOS/Windows) + env var fallback
- `souveraine auth` subcommand
- Removes hardcoded Bifrost api_key from souveraine.toml
Subconscious ledger (src/core/memory/mod.rs init_subconscious_ledger):
- 6 ledger files (commitments/assumptions/patterns/drift_log/
relationships/infrastructure) with proper YAML frontmatter
- Paths fixed: ledger/ not subconscious/ledger/
- Body usage instructions, idempotent init
Prompt orientation (src/core/prompt.rs build_ledger_orientation):
- Scans ledger/ directory, injects last 3 entries from each file
into Aster's system prompt (live context, not just awareness)
- Routing table + workflow (read before write, timestamped append)
Consciousness engine (src/server/consciousness_engine.rs):
- Adaptive rate delay shared with primary loop
- Four-fold mandate (Complete/Verify/Persist/Surface) in hardcoded
default prompt; observation format extracted for sharing
- Schedule added to ASTER_SAFE_TOOLS
Backend + CLI wiring expanded to mount the nervous system, identity,
and credentials at startup. New TOML sections: [schedules], [events],
[federation].
102 tests passing.
2026-05-12 08:14:25 -04:00
#[ tokio::test ]
async fn test_ledger_init_creates_files_with_frontmatter ( ) {
let dir = TempDir ::new ( ) . unwrap ( ) ;
let root = dir . path ( ) . to_path_buf ( ) ;
let repo = MemoryRepo ::open ( " test-sub " , root . clone ( ) ) ;
std ::fs ::create_dir_all ( & root ) . unwrap ( ) ;
repo . init_subconscious_ledger ( ) . await . unwrap ( ) ;
let commitments = root . join ( " ledger/commitments.md " ) ;
assert! ( commitments . exists ( ) , " commitments.md should exist " ) ;
let content = std ::fs ::read_to_string ( & commitments ) . unwrap ( ) ;
assert! ( content . starts_with ( " --- \n " ) , " should have YAML frontmatter " ) ;
assert! ( content . contains ( " description: " ) , " should have description field " ) ;
assert! ( content . contains ( " tags: " ) , " should have tags field " ) ;
assert! ( content . contains ( " # Commitments " ) , " should have body " ) ;
let relationships = root . join ( " ledger/relationships.md " ) ;
assert! ( relationships . exists ( ) , " relationships.md should exist " ) ;
let infrastructure = root . join ( " ledger/infrastructure.md " ) ;
assert! ( infrastructure . exists ( ) , " infrastructure.md should exist " ) ;
}
#[ tokio::test ]
async fn test_ledger_init_is_idempotent ( ) {
let dir = TempDir ::new ( ) . unwrap ( ) ;
let root = dir . path ( ) . to_path_buf ( ) ;
let repo = MemoryRepo ::open ( " test-sub " , root . clone ( ) ) ;
std ::fs ::create_dir_all ( & root ) . unwrap ( ) ;
repo . init_subconscious_ledger ( ) . await . unwrap ( ) ;
let commitments = root . join ( " ledger/commitments.md " ) ;
let before = std ::fs ::read_to_string ( & commitments ) . unwrap ( ) ;
repo . init_subconscious_ledger ( ) . await . unwrap ( ) ;
let after = std ::fs ::read_to_string ( & commitments ) . unwrap ( ) ;
assert_eq! ( before , after , " second init should not overwrite " ) ;
}
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
// ── description-required guard + audit ──────────────────────────────
#[ test ]
fn test_is_frontmatter_bound_scoping ( ) {
// Frontmatter-bound: the description contract applies.
assert! ( MemoryRepo ::is_frontmatter_bound ( " system/persona " ) ) ;
assert! ( MemoryRepo ::is_frontmatter_bound ( " reference/arch.md " ) ) ;
assert! ( MemoryRepo ::is_frontmatter_bound ( " projects/plan.md " ) ) ;
assert! ( MemoryRepo ::is_frontmatter_bound ( " issues/bug.md " ) ) ;
// Different schemas that legitimately lack description.
assert! ( ! MemoryRepo ::is_frontmatter_bound ( " tasks/some-todo.md " ) ) ;
assert! ( ! MemoryRepo ::is_frontmatter_bound ( " system/dynamic/itinerary.md " ) ) ;
assert! ( ! MemoryRepo ::is_frontmatter_bound ( " journal/2026/05/20.md " ) ) ;
// Segment prefixes, not substring matches — a file named like a
// todo but living elsewhere is still bound.
assert! ( MemoryRepo ::is_frontmatter_bound ( " reference/tasks-today.md " ) ) ;
assert! ( MemoryRepo ::is_frontmatter_bound ( " projects/journal-of-x.md " ) ) ;
}
#[ tokio::test ]
async fn test_write_rejects_empty_description_on_bound_file ( ) {
let dir = TempDir ::new ( ) . unwrap ( ) ;
let repo = MemoryRepo ::new ( " test-agent " , dir . path ( ) ) ;
repo . init ( ) . await . unwrap ( ) ;
// Seed a frontmatter-bound file with NO description (the gap).
std ::fs ::write (
repo . root ( ) . join ( " issues/gap.md " ) ,
" --- \n read_only: false \n --- \n Body with no description \n " ,
)
. unwrap ( ) ;
// A body-only write must not silently perpetuate the empty description.
let err = repo . write ( " issues/gap.md " , " updated body " ) . await . unwrap_err ( ) ;
assert! (
err . to_string ( ) . contains ( " no description " ) ,
" expected loud rejection, got: {err} "
) ;
// Supplying a description heals it and succeeds.
repo . write (
" issues/gap.md " ,
" --- \n description: The healed headline \n --- \n updated body \n " ,
)
. await
. unwrap ( ) ;
let healed = parse_memory_file ( & std ::fs ::read_to_string ( repo . root ( ) . join ( " issues/gap.md " ) ) . unwrap ( ) ) . unwrap ( ) ;
assert_eq! ( healed . frontmatter . description , " The healed headline " ) ;
}
#[ tokio::test ]
async fn test_write_allows_no_description_on_unbound_schemas ( ) {
let dir = TempDir ::new ( ) . unwrap ( ) ;
let repo = MemoryRepo ::new ( " test-agent " , dir . path ( ) ) ;
repo . init ( ) . await . unwrap ( ) ;
// tasks/, journal/, system/dynamic/ legitimately lack description.
repo . write ( " tasks/a-todo.md " , " todo body " ) . await . unwrap ( ) ;
repo . write ( " journal/2026/05/20.md " , " freeform journal entry " ) . await . unwrap ( ) ;
repo . write ( " system/dynamic/itinerary.md " , " itinerary body " ) . await . unwrap ( ) ;
// New bound files still get the auto-generated default on create.
repo . write ( " reference/new.md " , " some reference " ) . await . unwrap ( ) ;
let f = parse_memory_file ( & std ::fs ::read_to_string ( repo . root ( ) . join ( " reference/new.md " ) ) . unwrap ( ) ) . unwrap ( ) ;
assert! ( ! f . frontmatter . description . is_empty ( ) ) ;
}
#[ tokio::test ]
async fn test_append_rejects_empty_description_on_bound_file ( ) {
let dir = TempDir ::new ( ) . unwrap ( ) ;
let repo = MemoryRepo ::new ( " test-agent " , dir . path ( ) ) ;
repo . init ( ) . await . unwrap ( ) ;
// Existing bound file missing description.
std ::fs ::write (
repo . root ( ) . join ( " projects/gap.md " ) ,
" --- \n --- \n Initial body \n " ,
)
. unwrap ( ) ;
let err = repo . append ( " projects/gap.md " , " more " ) . await . unwrap_err ( ) ;
assert! ( err . to_string ( ) . contains ( " no description " ) ) ;
}
#[ tokio::test ]
async fn test_audit_flags_only_bound_files_missing_description ( ) {
let dir = TempDir ::new ( ) . unwrap ( ) ;
let repo = MemoryRepo ::new ( " test-agent " , dir . path ( ) ) ;
repo . init ( ) . await . unwrap ( ) ;
// Bound + missing description → flagged.
std ::fs ::write (
repo . root ( ) . join ( " issues/missing.md " ) ,
" --- \n --- \n body \n " ,
)
. unwrap ( ) ;
// Bound + has description → not flagged.
std ::fs ::write (
repo . root ( ) . join ( " reference/has.md " ) ,
" --- \n description: present \n --- \n body \n " ,
)
. unwrap ( ) ;
// Unbound schema + no description → not flagged (legitimate).
std ::fs ::write (
repo . root ( ) . join ( " tasks/todo.md " ) ,
" --- \n id: t1 \n --- \n body \n " ,
)
. unwrap ( ) ;
std ::fs ::write (
repo . root ( ) . join ( " journal/2026/05/20.md " ) ,
" --- \n --- \n freeform \n " ,
)
. unwrap ( ) ;
let missing = repo . audit ( ) . await . unwrap ( ) ;
assert_eq! ( missing , vec! [ " issues/missing.md " . to_string ( ) ] ) ;
}
2026-05-07 02:31:44 -04:00
}