2026-03-25 00:52:41 -04:00
|
|
|
// identity.rs — Identity file discovery and context assembly
|
|
|
|
|
//
|
|
|
|
|
// Discovers and loads the agent's identity: instruction files (CLAUDE.md,
|
|
|
|
|
// POC.md), memory files, and the system prompt. Reads context_groups
|
|
|
|
|
// from the shared config file.
|
|
|
|
|
|
|
|
|
|
use anyhow::Result;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
2026-04-13 14:55:41 -04:00
|
|
|
use crate::agent::tools::memory::memory_render;
|
2026-03-25 01:23:12 -04:00
|
|
|
use crate::config::{ContextGroup, ContextSource};
|
2026-03-25 00:52:41 -04:00
|
|
|
|
|
|
|
|
/// Read a file if it exists and is non-empty.
|
|
|
|
|
fn read_nonempty(path: &Path) -> Option<String> {
|
|
|
|
|
std::fs::read_to_string(path).ok().filter(|s| !s.trim().is_empty())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Walk from cwd to git root collecting instruction files (CLAUDE.md / POC.md).
|
|
|
|
|
///
|
|
|
|
|
/// On Anthropic models, loads CLAUDE.md. On other models, prefers POC.md
|
|
|
|
|
/// (omits Claude-specific RLHF corrections). If only one exists, it's
|
|
|
|
|
/// always loaded regardless of model.
|
|
|
|
|
fn find_context_files(cwd: &Path, prompt_file: &str) -> Vec<PathBuf> {
|
|
|
|
|
let prefer_poc = prompt_file == "POC.md";
|
|
|
|
|
|
|
|
|
|
let mut found = Vec::new();
|
|
|
|
|
let mut dir = Some(cwd);
|
|
|
|
|
while let Some(d) = dir {
|
|
|
|
|
for name in ["POC.md", "CLAUDE.md", ".claude/CLAUDE.md"] {
|
|
|
|
|
let path = d.join(name);
|
|
|
|
|
if path.exists() {
|
|
|
|
|
found.push(path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if d.join(".git").exists() { break; }
|
|
|
|
|
dir = d.parent();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(home) = dirs::home_dir() {
|
|
|
|
|
let global = home.join(".claude/CLAUDE.md");
|
|
|
|
|
if global.exists() && !found.contains(&global) {
|
|
|
|
|
found.push(global);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter: when preferring POC.md, skip bare CLAUDE.md (keep .claude/CLAUDE.md).
|
|
|
|
|
// When preferring CLAUDE.md, skip POC.md entirely.
|
|
|
|
|
let has_poc = found.iter().any(|p| p.file_name().map_or(false, |n| n == "POC.md"));
|
|
|
|
|
if !prefer_poc {
|
|
|
|
|
found.retain(|p| p.file_name().map_or(true, |n| n != "POC.md"));
|
|
|
|
|
} else if has_poc {
|
|
|
|
|
found.retain(|p| match p.file_name().and_then(|n| n.to_str()) {
|
|
|
|
|
Some("CLAUDE.md") => p.parent().and_then(|par| par.file_name())
|
|
|
|
|
.map_or(true, |n| n == ".claude"),
|
|
|
|
|
_ => true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
found.reverse(); // global first, project-specific overrides
|
|
|
|
|
found
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load memory files from config's context_groups.
|
2026-04-15 02:21:07 -04:00
|
|
|
/// Store sources load from the memory graph. Journal source loads recent entries.
|
2026-04-13 14:55:41 -04:00
|
|
|
async fn load_memory_files(memory_project: Option<&Path>, context_groups: &[ContextGroup]) -> Vec<(String, String)> {
|
2026-03-25 00:52:41 -04:00
|
|
|
let home = match dirs::home_dir() {
|
|
|
|
|
Some(h) => h,
|
|
|
|
|
None => return Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
|
move data home from ~/.claude/memory to ~/.consciousness
The consciousness project should stand independently of Claude Code.
All data, logs, sessions, and agent state now live under
~/.consciousness/ instead of being scattered across ~/.claude/memory/,
/tmp/claude-memory-search/, ~/.config/poc-memory/, and ~/.cache/.
Layout:
~/.consciousness/
*.capnp, *.bin, *.rkyv — store files
sessions/ — per-session state (seen sets, cookies)
logs/ — all logs (hook, agent, debug, dream)
agents/ — agent runtime state (pid files, output)
notifications/ — notification state
cache/ — transient data
Things that stay in ~/.claude/:
- projects/ (Claude Code transcripts)
- hooks/ (Claude Code hook system)
- telegram/ (shared integration)
- irc/ (shared integration)
- settings.json (Claude Code settings)
Debug log moves from /tmp/ to ~/.consciousness/logs/debug.log.
Session state moves from /tmp/claude-memory-search/ to sessions/.
Notifications move from ~/.claude/notifications/ to notifications/.
2026-03-27 21:05:15 -04:00
|
|
|
let global = home.join(".consciousness");
|
2026-04-04 01:11:06 -04:00
|
|
|
let project = memory_project.map(PathBuf::from);
|
2026-03-25 00:52:41 -04:00
|
|
|
|
|
|
|
|
let mut memories: Vec<(String, String)> = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Load from context_groups
|
|
|
|
|
for group in context_groups {
|
2026-03-25 01:23:12 -04:00
|
|
|
match group.source {
|
|
|
|
|
ContextSource::Journal => {
|
2026-03-25 00:52:41 -04:00
|
|
|
// Journal loading handled separately
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-03-25 02:04:07 -04:00
|
|
|
ContextSource::Store => {
|
2026-04-13 13:39:59 -04:00
|
|
|
// Load from the memory graph store via typed API
|
2026-03-25 02:04:07 -04:00
|
|
|
for key in &group.keys {
|
2026-04-13 14:55:41 -04:00
|
|
|
if let Ok(c) = memory_render(None, key, Some(true)).await {
|
2026-04-13 13:39:59 -04:00
|
|
|
if !c.trim().is_empty() {
|
|
|
|
|
memories.push((key.clone(), c));
|
2026-04-13 01:22:08 -04:00
|
|
|
}
|
2026-03-25 02:04:07 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-25 00:52:41 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// People dir — glob all .md files
|
|
|
|
|
for dir in [project.as_deref(), Some(global.as_path())].into_iter().flatten() {
|
|
|
|
|
let people_dir = dir.join("people");
|
|
|
|
|
if let Ok(entries) = std::fs::read_dir(&people_dir) {
|
|
|
|
|
let mut paths: Vec<_> = entries.flatten()
|
|
|
|
|
.filter(|e| e.path().extension().map_or(false, |ext| ext == "md"))
|
|
|
|
|
.collect();
|
|
|
|
|
paths.sort_by_key(|e| e.file_name());
|
|
|
|
|
for entry in paths {
|
|
|
|
|
let rel = format!("people/{}", entry.file_name().to_string_lossy());
|
|
|
|
|
if memories.iter().any(|(n, _)| n == &rel) { continue; }
|
|
|
|
|
if let Some(content) = read_nonempty(&entry.path()) {
|
|
|
|
|
memories.push((rel, content));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
memories
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Context message: instruction files + memory files + manifest.
|
2026-04-13 14:55:41 -04:00
|
|
|
pub async fn assemble_context_message(cwd: &Path, prompt_file: &str, memory_project: Option<&Path>, context_groups: &[ContextGroup]) -> Result<(Vec<(String, String)>, usize, usize)> {
|
2026-03-25 00:52:41 -04:00
|
|
|
let mut parts: Vec<(String, String)> = vec![
|
|
|
|
|
("Preamble".to_string(),
|
|
|
|
|
"Everything below is already loaded — your identity, instructions, \
|
|
|
|
|
memory files, and recent journal entries. Read them here in context, \
|
|
|
|
|
not with tools.\n\n\
|
|
|
|
|
IMPORTANT: Skip the \"Session startup\" steps from CLAUDE.md. Do NOT \
|
|
|
|
|
run poc-journal, poc-memory, or read memory files with tools — \
|
|
|
|
|
poc-agent has already loaded everything into your context. Just read \
|
|
|
|
|
what's here.".to_string()),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let context_files = find_context_files(cwd, prompt_file);
|
|
|
|
|
let mut config_count = 0;
|
|
|
|
|
for path in &context_files {
|
|
|
|
|
if let Ok(content) = std::fs::read_to_string(path) {
|
|
|
|
|
parts.push((path.display().to_string(), content));
|
|
|
|
|
config_count += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 14:55:41 -04:00
|
|
|
let memories = load_memory_files(memory_project, context_groups).await;
|
2026-03-25 00:52:41 -04:00
|
|
|
let memory_count = memories.len();
|
|
|
|
|
for (name, content) in memories {
|
|
|
|
|
parts.push((name, content));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if config_count == 0 && memory_count == 0 {
|
|
|
|
|
parts.push(("Fallback".to_string(),
|
|
|
|
|
"No identity files found. You are a helpful AI assistant with access to \
|
|
|
|
|
tools for reading files, writing files, running bash commands, and \
|
|
|
|
|
searching code.".to_string()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok((parts, config_count, memory_count))
|
|
|
|
|
}
|