By Kodelyth · · 7 min read
ECC keeps a local memory store — solutions, gotchas, and patterns captured from past sessions, retrieved with BM25, and injected back at the start of a related session. It lives on your disk as an append-only JSONL log. Nothing is ever transmitted anywhere.
A store like that lives or dies on one question it has to answer silently: which memories belong to this project? Get that wrong and recall returns nothing, which — for a recall-driven store — is indistinguishable from having learned nothing at all.
The bug: identity by path
Memory used to key a project by sha256(absolute path). The observer that
captured sessions keyed it by git remote. The two disagreed the moment anything
moved: clone the repo to a new machine, move it to a different folder, rename the
directory — the absolute path changes, its hash changes, and every memory filed
under the old hash is orphaned. Still on disk. Never found again.
Identity by normalised git remote
The fix is to key on something stable across all of those moves: the git remote. But remotes come in many shapes for the same repo, so the resolver normalises first.
git@github.com:owner/repo.git ─┐
https://github.com/owner/repo/ ├─→ remote:github.com/owner/repo → one id
ssh://git@github.com/owner/repo ─┘That is not a diagram of intent — it is what the code actually does. Run the four forms (including one carrying embedded credentials) through the resolver:
github.com/sifxprime/kodelyth-ecc => sha12: 142359ac19f9
github.com/sifxprime/kodelyth-ecc => sha12: 142359ac19f9
github.com/sifxprime/kodelyth-ecc => sha12: 142359ac19f9
github.com/sifxprime/kodelyth-ecc => sha12: 142359ac19f9The normaliser strips the scheme, the scp-style git@ user, a trailing .git,
trailing slashes, and — importantly — any ://user:pass@ credentials, so a
credential-bearing remote and a clean one map to the same project and no secret
ever lands in a hash input that might be logged. A repo with no remote (local
only, or not a git directory at all) falls back to the path id, which is fine:
a repo that never moves cannot be orphaned by moving.
Read-both, so nothing already captured is lost
Changing the identity scheme is the kind of fix that quietly destroys the data it was meant to protect, unless you plan for the transition. New memories are written under the git-remote id. But recall doesn't only look there — it matches every id that could belong to this project:
function matchingProjectIds(projectRoot, opts = {}) {
const ids = new Set();
ids.add(resolveProjectId(projectRoot, opts)); // git-remote-first
ids.add(legacyPathId(projectRoot)); // the old sha256(path)
return [...ids];
}Memories captured under the old path scheme are still found via the legacy id; new ones accumulate under the stable one. When the repo has no remote the two ids are identical and this collapses to a single lookup. No migration, no loss.
Secrets are redacted before they are stored
A captured memory is written to disk, surfaced by recall, and injected into
future sessions. So a credential that slips into a captured problem or
approach doesn't just sit in a log — it re-surfaces, in context, in every
related session afterward. Capture redacts before writing:
problem: redact(String(problem)).slice(0, 500),
approach: redact(String(approach)).slice(0, 2000),redact matches high-confidence provider token formats anywhere in the text
(AWS, GitHub, OpenAI/Anthropic, Stripe, Slack, Google, npm, JWTs, PEM blocks) and
assignment forms (password=, Authorization:, api_key=). Real output:
fixed the 500: set api_key=<REDACTED:anthropic-key> and retry
pushed with token <REDACTED:github-token> by mistake
db works: password=<REDACTED> in the urlEvery pattern is linear — no nested quantifiers, no adjacent \s*\s* — so none
can backtrack catastrophically on hostile input. That is the same ReDoS lesson
the prompt-injection guard paid for,
applied preemptively here.
Recall fuses relevance, recency, and outcome
BM25 decides relevance and a relative floor gates it (that threshold has its own story). Fusion only reorders the survivors, so that among similarly-relevant memories a recent, proven one outranks an old or failed one:
function fuseScore(m) {
const base = m.score || 0; // BM25 relevance
let recency = 1, outcome = 1;
const ageDays = (Date.now() - Date.parse(m.captured_at)) / 86400000;
recency = 1 + 0.25 * Math.exp(-ageDays / 90); // +25% fresh, decays ~90d
if (m.resolved === true) outcome = 1.3; // confirmed to work
else if (m.resolved === false) outcome = 0.6; // didn't stick
return base * recency * outcome; // relevance × recency × outcome
}The ordering of effects is deliberate. Relevance gates; recency and outcome only
re-rank what was already relevant. An ancient, confirmed fix still beats a fresh
guess, because base dominates when the relevance gap is real — the multipliers
are tie-breakers, not overrides. It is a pure function of one memory, so it is
unit-tested directly.
The semantic tier is opt-in and on-device
The default recall path is pure BM25: synchronous, zero-dependency, zero-network, exactly as advertised. A semantic layer exists, but it is off unless you ask for it:
export KODELYTH_MEMORY_EMBEDDINGS=onWith the flag on and the optional @xenova/transformers dependency present,
recall embeds the query with a local model — inference happens on your device,
nothing leaves the machine — and fuses the cosine similarity with the normalised
BM25 score:
final = weight * semantic + (1 - weight) * bm25norm // weight defaults to 0.5A result with no stored embedding keeps its BM25-normalised score, so a missing vector never unfairly sinks a relevant memory. And the whole thing degrades cleanly: a disabled flag, a missing optional dependency, or a model-load failure all collapse to the same safe BM25-only behaviour. You never get a worse result for turning it on and having it fail to load — you get the default.
Using it
Recall and capture work from any MCP-capable IDE (the recall_memory and
capture_memory tools), or directly from the CLI:
kodelythecc memory capture --problem "CORS preflight failing on PUT" \
--approach "added OPTIONS handler + Access-Control-Allow-Methods" --tags cors,express✓ stored ad4e237fcedf0b58 — "CORS preflight failing on PUT"kodelythecc memory recall "how did I fix CORS preflight" --limit 3[0.70] CORS preflight failing on PUT
→ added OPTIONS handler + Access-Control-Allow-Methods (cors, express)Recall scopes to the current project's memories first — matching both ids —
falling back to global ones when project-specific hits are sparse. Everything
stays in ~/.kodelythecc/memory/: inspect it, edit it, or delete it whenever
you like.
The identity resolver, redaction, and fusion shipped across the ECC 2.2x line and are dependency-free Node in scripts/memory. The whole store is local by design — the honest answer to "how do you know that about me?" is that it's a file on your disk, and nothing was sent anywhere.
npm i -g kodelyth-ecc && kodelythecc