Skip to content

Commit 5de58ec

Browse files
ScriptedAlchemyScriptedAlchemy
andauthored
feat: inject fact-store memory into Codex and Cursor hook context (#229)
Implements the top-ranked design (A+B) from the agent-memory interception audit, plus the proactive storage wording change (C): - New hooks::memory_inject module: trust-filtered, char-budgeted, secret-scanned digest rendering (session digests <= 2000 chars, per-prompt recall <= 800 chars, one sanitized line per fact). - Codex: SessionStart/SubagentStart inject a durable-project-memory digest; UserPromptSubmit injects prompt-relevance-gated recall (in-process FactRetriever search, lexical + score gate), deduped per session via .tracedecay/memory_inject_seen.json. - Cursor: sessionStart additional_context carries the same digest, and an always-applied rules/tracedecay-memory.mdc is materialized from the fact store (content-hash gated, regenerated on sessionStart / workspaceOpen / post-install; embedded placeholder keeps the file managed for uninstall). - Recall runs through TraceDecay's own store at injection time; no writes to host-native memory stores. Codex doctor now suggests memories.use_memories=false (advisory only). - Config gate: user_config memory_injection_enabled (default on) with TRACEDECAY_MEMORY_INJECTION env override. - Rule/skill text now instructs proactive fact storage with an explicit do-NOT-capture list instead of "only when the user asks". Co-authored-by: ScriptedAlchemy <zack@module-federation.io>
1 parent 34d0305 commit 5de58ec

12 files changed

Lines changed: 1055 additions & 16 deletions

File tree

codex-plugin/skills/recalling-project-memory/SKILL.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ Recall memory **before** reaching for external or web search — prior sessions
1616
2. **Durable facts → `tracedecay_fact_store`** with `action: "search"` (or `"probe"` / `"reason"`), plus `query` and `min_trust`.
1717
3. **If the user asks to inspect or repair memory health → `tracedecay_memory_status`** (repairs derived vectors/banks; returns fact/entity counts + trust distribution).
1818
4. **If the user rates a recalled fact → `tracedecay_fact_feedback`** (`helpful` / `unhelpful`) to tune its trust score.
19-
5. **Persist a new durable decision → `tracedecay_fact_store`** `action: "add"` (`content`, `category`, `tags`, `trust`) only when the user asks to remember it.
19+
5. **Persist a new durable decision → `tracedecay_fact_store`** `action: "add"` (`content`, `category`, `tags`, `trust`) proactively whenever a durable decision, user preference, correction, or pitfall surfaces — do not wait for the user to ask. The add path already rejects secrets and reports near-duplicates/conflicts.
2020

2121
## Guardrails
2222

23-
- `tracedecay_message_search` and `fact_store` searches are read-only. `fact_store` adds, `fact_feedback`, and `memory_status` mutate memory state; use them only for explicit user requests or ratings.
23+
- `tracedecay_message_search` and `fact_store` searches are read-only. `fact_feedback` and `memory_status` mutate memory state; use them for explicit user ratings or health checks.
24+
- Do NOT capture: secrets/credentials, transient errors, environment-specific failures, one-off narratives, task progress, or soon-stale session outcomes — recover those from transcripts instead.
2425

2526
## Handoff
2627

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
description: Durable project memory from the tracedecay fact store
3+
alwaysApply: true
4+
---
5+
6+
<!-- generated by tracedecay from the project fact store; do not edit by hand -->
7+
8+
# Project memory (tracedecay)
9+
10+
No durable facts stored yet. As decisions, preferences, and corrections surface, store them with `tracedecay_fact_store` (action "add") and they will appear here.
11+
12+
Curate via `tracedecay_fact_store` (update/remove), `tracedecay_fact_feedback`, or the tracedecay dashboard.

cursor-plugin/rules/tracedecay.mdc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ alwaysApply: true
1111
- **Write:** before adding a new helper/util, run the `tracedecay:finding-duplicate-logic` pre-write probe — it may already exist.
1212
- **Edit:** for anchored or structural source edits, follow the `tracedecay:atomic-code-edits` skill (`tracedecay_str_replace`, `tracedecay_replace_symbol`, `tracedecay_ast_grep_rewrite`, …) — the graph re-indexes itself after each write.
1313
- **Recall:** for prior decisions or past conversations, use `tracedecay_message_search` / `tracedecay_fact_store` search (skills: `tracedecay:recalling-project-memory`, `tracedecay:recalling-session-context`); for updating, deleting, or curating stored facts, use `tracedecay:curating-project-memory`.
14+
- **Memory:** when a durable decision, user preference, correction, or pitfall surfaces, store it proactively with `tracedecay_fact_store` (action "add") with calibrated trust — do not wait to be asked. Do NOT store secrets/credentials, transient errors, environment-specific failures, one-off narratives, or task progress.
1415
- **Truncated MCP responses:** if a tracedecay response has `truncated: true` plus `handle`, narrow the query first when that answers the task; if the omitted details are needed, call `tracedecay_retrieve` with the `handle` instead of guessing or re-running a broad query.
1516
- **MCP errors/timeouts:** if a tracedecay MCP call errors, times out, or the server is disconnected, every tool is also available as a shell command — `tracedecay tool <name> --key value` (`tracedecay tool` lists all tools, `tracedecay tool <name> --help` shows parameters; see `tracedecay:using-the-cli`). Do not query `.tracedecay` databases directly and do not abandon tracedecay because the MCP transport failed.
1617
- Every common workflow has a `tracedecay:*` skill (searching-for-code, tracing-functions, finding-impacted-areas, running-impacted-tests, reviewing-a-diff, code-health-report, …) — reach for the matching skill before improvising.

cursor-plugin/skills/recalling-project-memory/SKILL.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ Recall memory **before** reaching for external or web search — prior sessions
1616
2. **Durable facts → `tracedecay_fact_store`** with `action: "search"` (or `"probe"` / `"reason"`), plus `query` and `min_trust`.
1717
3. **If the user asks to inspect or repair memory health → `tracedecay_memory_status`** (repairs derived vectors/banks; returns fact/entity counts + trust distribution).
1818
4. **If the user rates a recalled fact → `tracedecay_fact_feedback`** (`helpful` / `unhelpful`) to tune its trust score.
19-
5. **Persist a new durable decision → `tracedecay_fact_store`** `action: "add"` (`content`, `category`, `tags`, `trust`) only when the user asks to remember it.
19+
5. **Persist a new durable decision → `tracedecay_fact_store`** `action: "add"` (`content`, `category`, `tags`, `trust`) proactively whenever a durable decision, user preference, correction, or pitfall surfaces — do not wait for the user to ask. The add path already rejects secrets and reports near-duplicates/conflicts.
2020

2121
## Guardrails
2222

23-
- `tracedecay_message_search` and `fact_store` searches are read-only. `fact_store` adds, `fact_feedback`, and `memory_status` mutate memory state; use them only for explicit user requests or ratings.
23+
- `tracedecay_message_search` and `fact_store` searches are read-only. `fact_feedback` and `memory_status` mutate memory state; use them for explicit user ratings or health checks.
24+
- Do NOT capture: secrets/credentials, transient errors, environment-specific failures, one-off narratives, task progress, or soon-stale session outcomes — recover those from transcripts instead.
2425

2526
## Handoff
2627

src/agents/codex.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ impl AgentIntegration for CodexIntegration {
162162
} else {
163163
doctor_check_plugin(dc, &ctx.home);
164164
}
165+
doctor_suggest_native_memories_off(dc, &ctx.home);
165166
}
166167

167168
fn is_detected(&self, home: &Path) -> bool {
@@ -1386,6 +1387,51 @@ fn doctor_check_hooks(dc: &mut DoctorCounters, hooks_path: &Path) {
13861387
}
13871388
}
13881389

1390+
/// Suggests turning off Codex's native memories *injection* when tracedecay's
1391+
/// fact-store injection is active, so the model does not receive two parallel
1392+
/// memory systems built from the same sessions. This is advisory only: the
1393+
/// user's `config.toml` is never edited, and tracedecay never writes into
1394+
/// `~/.codex/memories/` — the holographic fact store stays the single source
1395+
/// of truth and delivery is rendered prompt context only.
1396+
fn doctor_suggest_native_memories_off(dc: &mut DoctorCounters, home: &Path) {
1397+
if !crate::hooks::memory_inject::memory_injection_enabled() {
1398+
return;
1399+
}
1400+
let config_path = home.join(".codex/config.toml");
1401+
let Ok(config) = load_toml_file(&config_path) else {
1402+
return;
1403+
};
1404+
if codex_native_memories_injection_enabled(&config) {
1405+
dc.info(
1406+
"Codex native memories injection is enabled alongside tracedecay's \
1407+
fact-store injection; consider setting `memories.use_memories = false` \
1408+
in ~/.codex/config.toml so per-project memory comes from the tracedecay \
1409+
fact store only (tracedecay never edits this setting itself)",
1410+
);
1411+
}
1412+
}
1413+
1414+
/// True when Codex's experimental memories feature is on and session-start
1415+
/// memory injection (`memories.use_memories`, default true) is not disabled.
1416+
fn codex_native_memories_injection_enabled(config: &toml::Value) -> bool {
1417+
let memories_feature_on = config
1418+
.get("features")
1419+
.and_then(|features| features.get("memories"))
1420+
.is_some_and(|memories| {
1421+
// `memories = true` (bool) or the nested `[features.memories]` table
1422+
// form both mean the feature is enabled.
1423+
memories.as_bool().unwrap_or(memories.is_table())
1424+
});
1425+
if !memories_feature_on {
1426+
return false;
1427+
}
1428+
config
1429+
.get("memories")
1430+
.and_then(|memories| memories.get("use_memories"))
1431+
.and_then(toml::Value::as_bool)
1432+
.unwrap_or(true)
1433+
}
1434+
13891435
fn codex_hook_present(hooks: &serde_json::Value, event: &str, command: &str) -> bool {
13901436
hooks["hooks"][event].as_array().is_some_and(|groups| {
13911437
groups.iter().any(|group| {
@@ -1409,6 +1455,28 @@ mod tests {
14091455
Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf()
14101456
}
14111457

1458+
#[test]
1459+
fn native_memories_injection_detection_covers_config_shapes() {
1460+
let parse = |raw: &str| toml::from_str::<toml::Value>(raw).unwrap();
1461+
// Feature on (bool form), use_memories defaulting to true.
1462+
assert!(codex_native_memories_injection_enabled(&parse(
1463+
"[features]\nmemories = true\n"
1464+
)));
1465+
// Feature on (nested table form).
1466+
assert!(codex_native_memories_injection_enabled(&parse(
1467+
"[features.memories]\ncustom_tools = true\n"
1468+
)));
1469+
// Injection explicitly disabled.
1470+
assert!(!codex_native_memories_injection_enabled(&parse(
1471+
"[features]\nmemories = true\n[memories]\nuse_memories = false\n"
1472+
)));
1473+
// Feature off or absent.
1474+
assert!(!codex_native_memories_injection_enabled(&parse(
1475+
"[features]\nmemories = false\n"
1476+
)));
1477+
assert!(!codex_native_memories_injection_enabled(&parse("")));
1478+
}
1479+
14121480
#[test]
14131481
fn remove_legacy_codex_native_automation_deletes_stale_record() {
14141482
let home = tempfile::tempdir().expect("tempdir should create");

src/agents/cursor.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ async fn track_branch_after_install(project_path: Option<&Path>) {
141141
let Some(project_path) = project_path else {
142142
return;
143143
};
144+
// Materialize the always-applied memory rule from this project's fact
145+
// store so install/update-plugin leaves fresh memory in place instead of
146+
// waiting for the first sessionStart hook. Fail-open, no-op when the
147+
// project has no initialized store.
148+
crate::hooks::memory_inject::regenerate_cursor_memory_rule(project_path).await;
144149
let Some(branch_name) = crate::branch::current_branch(project_path) else {
145150
return;
146151
};
@@ -189,6 +194,10 @@ const EMBEDDED_PLUGIN_FILES: &[(&str, &str)] = &[
189194
"rules/tracedecay.mdc",
190195
include_str!("../../cursor-plugin/rules/tracedecay.mdc"),
191196
),
197+
(
198+
"rules/tracedecay-memory.mdc",
199+
include_str!("../../cursor-plugin/rules/tracedecay-memory.mdc"),
200+
),
192201
(
193202
"skills/architecture-overview/SKILL.md",
194203
include_str!("../../cursor-plugin/skills/architecture-overview/SKILL.md"),
@@ -379,6 +388,14 @@ fn cursor_plugin_manifest_path(home: &Path) -> PathBuf {
379388
cursor_plugin_install_dir(home).join(".cursor-plugin/plugin.json")
380389
}
381390

391+
/// Path of the materialized always-applied memory rule rendered from the
392+
/// project fact store (see `hooks::memory_inject::regenerate_cursor_memory_rule`).
393+
/// The install path writes the embedded placeholder; hooks rewrite it in
394+
/// place with rendered facts.
395+
pub fn cursor_memory_rule_path(home: &Path) -> PathBuf {
396+
cursor_plugin_install_dir(home).join("rules/tracedecay-memory.mdc")
397+
}
398+
382399
fn install_cursor_plugin(home: &Path, tracedecay_bin: &str) -> Result<()> {
383400
let install_dir = cursor_plugin_install_dir(home);
384401
if let Some(parent) = install_dir.parent() {

src/hook_cmd.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ pub(crate) async fn handle_hook_command(command: Commands) -> tracedecay::errors
5757
exit_if_nonzero(tracedecay::hooks::hook_codex_user_prompt_submit().await);
5858
}
5959
Commands::HookCodexSubagentStart => {
60-
exit_if_nonzero(tracedecay::hooks::hook_codex_subagent_start());
60+
exit_if_nonzero(tracedecay::hooks::hook_codex_subagent_start().await);
6161
}
6262
Commands::HookCodexPostToolUse => {
6363
exit_if_nonzero(tracedecay::hooks::hook_codex_post_tool_use().await);

0 commit comments

Comments
 (0)