feat(memory): add local memory provider#41
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8fdfec4e8b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let profile = if query.include_profile { | ||
| self.profile(&query.scope)? |
There was a problem hiding this comment.
Budget profile facts before returning context
With the default include_profile: true, self.profile() reads global/project memory and returns those facts in the context bundle, but total_chars and the section budget only account for sections. A single very long non-heading line in USER.md or .codra/MEMORY.md is therefore returned and printed by codra memory context --budget ... outside the requested budget, even when all sections are capped.
Useful? React with 👍 / 👎.
| pub fn task_dir(project_path: &Path, task_id: &str) -> PathBuf { | ||
| workspace_codra_dir(project_path) | ||
| .join("tasks") | ||
| .join(task_id) | ||
| } |
There was a problem hiding this comment.
Reject path separators in task IDs
When --task or another caller supplies a task id containing path components, join(task_id) is honored and the provider can read progress.md / plan.md / decisions.md outside .codra/tasks. That breaks task scoping for memory retrieval; task IDs need to be normalized or rejected before building filesystem paths.
Useful? React with 👍 / 👎.
| fn forget(&self, id: &str) -> MemoryResult<()> { | ||
| let forgotten_dir = crate::paths::global_codra_dir().join("memory/.forgotten"); | ||
| fs::create_dir_all(&forgotten_dir)?; | ||
| let tombstone = forgotten_dir.join(format!("{}.txt", sanitize_id(id))); | ||
| fs::write( | ||
| &tombstone, | ||
| format!("forgotten_at={}\n", Utc::now().to_rfc3339()), | ||
| )?; |
There was a problem hiding this comment.
Make forget affect future retrievals
Calling forget() only writes a tombstone file, but profile(), recall(), and context() never consult that directory, so the same memory can still be returned immediately afterward. For any consumer that exposes the MemoryProvider delete/forget action, this gives a false success while leaving the forgotten content active.
Useful? React with 👍 / 👎.
| fn add(&self, input: MemoryAddInput) -> MemoryResult<MemoryRecord> { | ||
| let notes_path = workspace_notes_file(&input.scope.project_path); |
There was a problem hiding this comment.
Preserve static memories as durable profile facts
When a caller adds memory with is_static: true, this path still appends it to notes.md, so profile() only treats it as one of the last few dynamic notes instead of a durable project/user fact. Consumers using the new MemoryAddInput API to save long-term preferences or architecture facts will see those memories age out of the profile even though they explicitly marked them static.
Useful? React with 👍 / 👎.
Summary
Supermemory-inspired but Codra-native local-first memory foundation.
MemoryProvidertrait andLocalMarkdownMemoryProvider.codra/markdown + global~/.codra/(with/root/USER.md//root/MEMORY.mdfallback)context()bundle — never injects all memory blindlycodra memory status,codra memory contextIntentionally out of scope
Validation
Full workspace
cargo testnot run (disk constraints during build); targeted crate checks passed.Architecture rules documented