diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e77387..8009ffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.18.0] - 2026-06-12 + +### Added +- **MCP `memory_note` tool** — the agent can now record a durable user + preference or standing fact itself (not just the user via the CLI), so it + learns how you work over time. De-duplicated; injected into every future + session like CLI-added preferences. +- **`stats` now reports memory metrics** — the global cross-project recall + index size and the number of stored preferences, so the memory platform's + state is visible at a glance. + +### Notes +- Consolidation (clustering episodic events into durable semantic/procedural + facts) is intentionally deferred: a good version needs the offline LLM + backend for quality, and a pure heuristic would manufacture noise. Tracked + separately. claude-mem/mem0 *import* likewise awaits their on-disk format + + sample data rather than being guessed at. + ## [0.17.0] - 2026-06-12 ### Added diff --git a/Cargo.lock b/Cargo.lock index 9d0896b..8ad55ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2572,7 +2572,7 @@ dependencies = [ [[package]] name = "task-journal-cli" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "assert_cmd", @@ -2595,7 +2595,7 @@ dependencies = [ [[package]] name = "task-journal-core" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "chrono", @@ -2620,9 +2620,10 @@ dependencies = [ [[package]] name = "task-journal-mcp" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", + "chrono", "clap", "rmcp", "rusqlite", diff --git a/Cargo.toml b/Cargo.toml index 0c7381e..d0854c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ ] [workspace.package] -version = "0.17.0" +version = "0.18.0" edition = "2021" rust-version = "1.88" license = "MIT" diff --git a/crates/tj-cli/Cargo.toml b/crates/tj-cli/Cargo.toml index bd220d2..21c7c7e 100644 --- a/crates/tj-cli/Cargo.toml +++ b/crates/tj-cli/Cargo.toml @@ -23,7 +23,7 @@ default = ["embed"] embed = ["tj-core/embed"] [dependencies] -tj-core = { package = "task-journal-core", version = "0.17.0", path = "../tj-core", default-features = false } +tj-core = { package = "task-journal-core", version = "0.18.0", path = "../tj-core", default-features = false } anyhow = { workspace = true } clap = { workspace = true } tracing = { workspace = true } diff --git a/crates/tj-cli/src/main.rs b/crates/tj-cli/src/main.rs index fabcbc4..29059a7 100644 --- a/crates/tj-cli/src/main.rs +++ b/crates/tj-cli/src/main.rs @@ -1849,6 +1849,19 @@ fn main() -> Result<()> { let ratio = confirmed as f64 / total as f64 * 100.0; println!(" confirmed ratio: {ratio:.1}%"); } + // Memory platform (Pillars A/B/C): the global cross-project index. + let mem_path = tj_core::paths::memory_db()?; + if mem_path.exists() { + if let Ok(g) = tj_core::memory::open(&mem_path) { + let entries = tj_core::memory::count(&g).unwrap_or(0); + let prefs = tj_core::memory::list_preferences(&g) + .map(|p| p.len()) + .unwrap_or(0); + println!("memory (global cross-project recall index):"); + println!(" recall entries: {entries}"); + println!(" preferences: {prefs}"); + } + } } Commands::Doctor { json } => { let report = run_doctor()?; diff --git a/crates/tj-cli/tests/cli.rs b/crates/tj-cli/tests/cli.rs index 379966e..532180f 100644 --- a/crates/tj-cli/tests/cli.rs +++ b/crates/tj-cli/tests/cli.rs @@ -5190,3 +5190,22 @@ fn remembered_preference_lists_and_injects_at_session_start() { ); assert!(body.contains("additionalContext")); } + +#[test] +fn stats_reports_memory_preferences_count() { + // stats surfaces the global memory state (Pillar A/B/C metrics). + let dir = assert_fs::TempDir::new().unwrap(); + Command::cargo_bin("task-journal") + .unwrap() + .env("XDG_DATA_HOME", dir.path()) + .args(["remember", "respond in Russian"]) + .assert() + .success(); + Command::cargo_bin("task-journal") + .unwrap() + .env("XDG_DATA_HOME", dir.path()) + .args(["stats"]) + .assert() + .success() + .stdout(contains("preferences: 1")); +} diff --git a/crates/tj-mcp/Cargo.toml b/crates/tj-mcp/Cargo.toml index f7a7d42..db3dd90 100644 --- a/crates/tj-mcp/Cargo.toml +++ b/crates/tj-mcp/Cargo.toml @@ -17,7 +17,7 @@ path = "src/main.rs" [dependencies] # Lean: the MCP server doesn't embed yet, so it skips the model2vec backend. -tj-core = { package = "task-journal-core", version = "0.17.0", path = "../tj-core", default-features = false } +tj-core = { package = "task-journal-core", version = "0.18.0", path = "../tj-core", default-features = false } anyhow = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } @@ -27,6 +27,7 @@ serde = { workspace = true } serde_json = { workspace = true } schemars = { workspace = true } ulid = { workspace = true } +chrono = { workspace = true } rusqlite = { workspace = true } clap = { workspace = true } diff --git a/crates/tj-mcp/src/main.rs b/crates/tj-mcp/src/main.rs index 9504b82..968df9d 100644 --- a/crates/tj-mcp/src/main.rs +++ b/crates/tj-mcp/src/main.rs @@ -260,6 +260,20 @@ pub struct TaskCloseResult { pub completeness_gaps: Vec, } +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct MemoryNoteParams { + /// The durable user preference or standing fact to remember across all + /// projects and sessions — e.g. "respond in Russian, terse", "this team + /// always squash-merges". Keep it one short sentence. + pub text: String, +} + +#[derive(Debug, Serialize, schemars::JsonSchema)] +pub struct MemoryNoteResult { + pub remembered: bool, + pub text: String, +} + fn parse_event_type(s: &str) -> anyhow::Result { use tj_core::event::EventType::*; Ok(match s { @@ -664,6 +678,29 @@ impl TaskJournalServer { }) .await } + + #[tool( + name = "memory_note", + description = "Remember a durable user preference or standing fact across ALL projects and sessions — how the user wants to be worked with (\"respond in Russian, terse\"), or a stable team/project rule. Injected into every future session's context. Use it when you learn something about the user or their workflow that should outlive this task. De-duplicated." + )] + async fn memory_note( + &self, + Parameters(p): Parameters, + ) -> Result, McpError> { + traced_tool("memory_note", async move { + run_blocking(move || { + let global = tj_core::memory::open(tj_core::paths::memory_db()?)?; + let now = chrono::Utc::now().to_rfc3339(); + let remembered = tj_core::memory::add_preference(&global, &p.text, &now)?; + Ok(Json(MemoryNoteResult { + remembered, + text: p.text.trim().to_string(), + })) + }) + .await + }) + .await + } } #[tool_handler(router = Self::tool_router())] diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 04c6e92..9010280 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "task-journal", - "version": "0.17.0", + "version": "0.18.0", "description": "Append-only journal of AI-coding task reasoning chains: hypotheses, decisions, rejections, evidence. Renders compact resume packs so an agent can pick up a 2-week-old task with full context.", "author": { "name": "Mher Shahinyan"