From 7b8eb3af8d1f50ea2a14604897c1cdacdbd4f1db Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 12 May 2026 14:45:46 +0900 Subject: [PATCH 1/2] feat(pod): wire knowledge slugs into # completion --- crates/memory/src/lib.rs | 2 +- crates/memory/src/resident.rs | 91 +++++++++++++++++++++++++++------- crates/pod/src/controller.rs | 6 +++ crates/pod/src/ipc/server.rs | 10 +++- crates/pod/src/pod.rs | 7 +++ crates/pod/src/shared_state.rs | 58 ++++++++++++++++++++++ 6 files changed, 155 insertions(+), 19 deletions(-) diff --git a/crates/memory/src/lib.rs b/crates/memory/src/lib.rs index 596cf49a..2b3d51b8 100644 --- a/crates/memory/src/lib.rs +++ b/crates/memory/src/lib.rs @@ -21,7 +21,7 @@ pub mod workspace; pub use error::{LintError, LintWarning, MemoryError}; pub use extract::ExtractPointerPayload; pub use linter::{LintReport, Linter}; -pub use resident::{ResidentKnowledgeEntry, collect_resident_knowledge}; +pub use resident::{ResidentKnowledgeEntry, collect_resident_knowledge, list_knowledge_slugs}; pub use scope::deny_write_rules; pub use slug::Slug; pub use usage::{ diff --git a/crates/memory/src/resident.rs b/crates/memory/src/resident.rs index 89878d3f..08c06765 100644 --- a/crates/memory/src/resident.rs +++ b/crates/memory/src/resident.rs @@ -1,10 +1,14 @@ -//! Collect resident-injection candidates from the workspace. +//! Workspace knowledge enumeration helpers. //! -//! Walks `/knowledge/*.md`, returns the records whose -//! frontmatter has `model_invokation: true` as `(slug, description)` -//! pairs sorted by slug. The Pod system-prompt assembler appends them -//! into the trailing section so descriptions sit next to the scope -//! summary and AGENTS.md. +//! Two surfaces, both walking `/.insomnia/knowledge/*.md`: +//! +//! - [`collect_resident_knowledge`] — resident-injection candidates +//! (`model_invokation: true`) returned as `(slug, description)` pairs +//! for the Pod system-prompt assembler. +//! - [`list_knowledge_slugs`] — every slug whose file parses, regardless +//! of `model_invokation`. Used by the Pod IPC layer to answer TUI `#` +//! completion (`model_invokation` is a resident-injection flag, not a +//! user-visibility flag). //! //! Files that fail to read or parse are skipped silently — the Linter //! enforces shape on write, so a malformed file here means external @@ -23,13 +27,36 @@ pub struct ResidentKnowledgeEntry { /// frontmatter has `model_invokation: true`, sorted by slug. A missing /// `knowledge/` directory yields an empty vec. pub fn collect_resident_knowledge(layout: &WorkspaceLayout) -> Vec { + let mut out: Vec = Vec::new(); + walk_knowledge(layout, |slug, fm| { + if fm.model_invokation { + out.push(ResidentKnowledgeEntry { + slug, + description: fm.description, + }); + } + }); + out.sort_by(|a, b| a.slug.cmp(&b.slug)); + out +} + +/// Walk `/knowledge/*.md` and return every slug whose +/// frontmatter parses, sorted ascending. Does not filter on +/// `model_invokation`. A missing `knowledge/` directory yields an empty +/// vec. +pub fn list_knowledge_slugs(layout: &WorkspaceLayout) -> Vec { + let mut out: Vec = Vec::new(); + walk_knowledge(layout, |slug, _fm| out.push(slug)); + out.sort(); + out +} + +fn walk_knowledge(layout: &WorkspaceLayout, mut visit: impl FnMut(String, KnowledgeFrontmatter)) { let dir = layout.knowledge_dir(); let entries = match std::fs::read_dir(&dir) { Ok(it) => it, - Err(_) => return Vec::new(), + Err(_) => return, }; - - let mut out: Vec = Vec::new(); for entry in entries.flatten() { let path = entry.path(); if !path.is_file() { @@ -55,15 +82,8 @@ pub fn collect_resident_knowledge(layout: &WorkspaceLayout) -> Vec f, Err(_) => continue, }; - if fm.model_invokation { - out.push(ResidentKnowledgeEntry { - slug, - description: fm.description, - }); - } + visit(slug, fm); } - out.sort_by(|a, b| a.slug.cmp(&b.slug)); - out } #[cfg(test)] @@ -164,4 +184,41 @@ mod tests { let got = collect_resident_knowledge(&layout); assert_eq!(got.len(), 1); } + + #[test] + fn list_slugs_missing_dir_returns_empty() { + let dir = TempDir::new().unwrap(); + let layout = WorkspaceLayout::new(dir.path().to_path_buf()); + assert!(list_knowledge_slugs(&layout).is_empty()); + } + + #[test] + fn list_slugs_returns_all_regardless_of_model_invokation() { + let (dir, layout) = setup(); + write_knowledge(dir.path(), "alpha", "a", true, ""); + write_knowledge(dir.path(), "beta", "b", false, ""); + write_knowledge(dir.path(), "gamma", "g", true, ""); + + let got = list_knowledge_slugs(&layout); + assert_eq!(got, vec!["alpha", "beta", "gamma"]); + } + + #[test] + fn list_slugs_skips_malformed_and_non_md() { + let (dir, layout) = setup(); + write_knowledge(dir.path(), "good", "ok", true, ""); + std::fs::write( + dir.path().join(".insomnia/knowledge/bad.md"), + "---\nthis is not yaml: : :\n---\nbody\n", + ) + .unwrap(); + std::fs::write( + dir.path().join(".insomnia/knowledge/note.txt"), + "not markdown\n", + ) + .unwrap(); + + let got = list_knowledge_slugs(&layout); + assert_eq!(got, vec!["good"]); + } } diff --git a/crates/pod/src/controller.rs b/crates/pod/src/controller.rs index 8ae9b8b4..7e754988 100644 --- a/crates/pod/src/controller.rs +++ b/crates/pod/src/controller.rs @@ -388,6 +388,12 @@ impl PodController { .map(|slug| crate::shared_state::WorkflowCandidate { slug }) .collect(), ); + shared_state.set_knowledge( + pod.knowledge_completions() + .into_iter() + .map(|slug| crate::shared_state::KnowledgeCandidate { slug }) + .collect(), + ); runtime_dir.write_manifest(&manifest_toml).await?; runtime_dir.write_status(&shared_state).await?; runtime_dir.write_history(&shared_state).await?; diff --git a/crates/pod/src/ipc/server.rs b/crates/pod/src/ipc/server.rs index ffea034d..1a52b364 100644 --- a/crates/pod/src/ipc/server.rs +++ b/crates/pod/src/ipc/server.rs @@ -102,7 +102,15 @@ async fn handle_connection(stream: tokio::net::UnixStream, handle: PodHandle) { is_dir: c.is_dir, }) .collect(), - protocol::CompletionKind::Knowledge => Vec::new(), + protocol::CompletionKind::Knowledge => handle + .shared_state + .list_knowledge_completions(&prefix) + .into_iter() + .map(|c| protocol::CompletionEntry { + value: c.slug, + is_dir: false, + }) + .collect(), protocol::CompletionKind::Workflow => handle .shared_state .list_workflow_completions(&prefix) diff --git a/crates/pod/src/pod.rs b/crates/pod/src/pod.rs index 51bea2f0..afc44105 100644 --- a/crates/pod/src/pod.rs +++ b/crates/pod/src/pod.rs @@ -1237,6 +1237,13 @@ impl Pod { self.workflow_registry.list_user_invocable("") } + pub fn knowledge_completions(&self) -> Vec { + self.memory_layout + .as_ref() + .map(memory::list_knowledge_slugs) + .unwrap_or_default() + } + /// Flatten a typed segment list into the single string the Worker /// receives as the user message, and emit user-facing alerts for /// segments that fall through to placeholder (knowledge / workflow diff --git a/crates/pod/src/shared_state.rs b/crates/pod/src/shared_state.rs index 04dd8200..b86b4b48 100644 --- a/crates/pod/src/shared_state.rs +++ b/crates/pod/src/shared_state.rs @@ -12,6 +12,11 @@ pub struct WorkflowCandidate { pub slug: String, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct KnowledgeCandidate { + pub slug: String, +} + /// Shared state between PodController and runtime directory. /// /// Controller updates this in-memory; RuntimeDir writes it to disk. @@ -37,6 +42,7 @@ pub struct PodSharedState { /// directly without spinning up a controller). fs_view: OnceLock, workflows: OnceLock>, + knowledge: OnceLock>, } impl PodSharedState { @@ -56,6 +62,7 @@ impl PodSharedState { user_segments: RwLock::new(Vec::new()), fs_view: OnceLock::new(), workflows: OnceLock::new(), + knowledge: OnceLock::new(), } } @@ -88,6 +95,23 @@ impl PodSharedState { .unwrap_or_default() } + pub fn set_knowledge(&self, knowledge: Vec) { + let _ = self.knowledge.set(knowledge); + } + + pub fn list_knowledge_completions(&self, prefix: &str) -> Vec { + self.knowledge + .get() + .map(|items| { + items + .iter() + .filter(|candidate| candidate.slug.starts_with(prefix)) + .cloned() + .collect() + }) + .unwrap_or_default() + } + pub fn user_segments(&self) -> Vec> { self.user_segments .read() @@ -230,4 +254,38 @@ mod tests { assert!(parsed.is_array()); assert_eq!(parsed[0]["role"], "assistant"); } + + #[test] + fn knowledge_completions_empty_when_unset() { + let state = test_state(); + assert!(state.list_knowledge_completions("").is_empty()); + assert!(state.list_knowledge_completions("foo").is_empty()); + } + + #[test] + fn knowledge_completions_filter_by_prefix() { + let state = test_state(); + state.set_knowledge(vec![ + KnowledgeCandidate { + slug: "alpha".into(), + }, + KnowledgeCandidate { + slug: "alphabet".into(), + }, + KnowledgeCandidate { + slug: "beta".into(), + }, + ]); + let all = state.list_knowledge_completions(""); + assert_eq!(all.len(), 3); + let alpha = state.list_knowledge_completions("alpha"); + assert_eq!( + alpha + .iter() + .map(|c| c.slug.as_str()) + .collect::>(), + vec!["alpha", "alphabet"] + ); + assert!(state.list_knowledge_completions("zzz").is_empty()); + } } From f7f59dd30cfa1a776108e4647dd7363a705636c3 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 12 May 2026 15:07:39 +0900 Subject: [PATCH 2/2] docs(memory): fix knowledge dir path in collect_resident_knowledge doc --- crates/memory/src/resident.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/memory/src/resident.rs b/crates/memory/src/resident.rs index 08c06765..6905f4df 100644 --- a/crates/memory/src/resident.rs +++ b/crates/memory/src/resident.rs @@ -23,9 +23,9 @@ pub struct ResidentKnowledgeEntry { pub description: String, } -/// Walk `/knowledge/*.md` and return entries whose +/// Walk `/.insomnia/knowledge/*.md` and return entries whose /// frontmatter has `model_invokation: true`, sorted by slug. A missing -/// `knowledge/` directory yields an empty vec. +/// directory yields an empty vec. pub fn collect_resident_knowledge(layout: &WorkspaceLayout) -> Vec { let mut out: Vec = Vec::new(); walk_knowledge(layout, |slug, fm| {