# Living memory: why memory that isn't tied to your code rots, and how we keep it honest

A note bound to a function should die the moment that function does, not quietly lie to your next agent. Here's how SecondOS anchors memory to real symbols, flags it stale on drift, and revives it when the code comes back.

The tempting way to give an AI tool memory of your codebase is to write it down: a summary, a note, a pile of embeddings. It works for a while, then it rots. Prose describes code at a moment in time, and code moves. A note that says "auth lives in `hashPassword`" is wrong the instant someone renames that function, and nothing tells you. Your next agent reads the stale note and answers confidently about code that no longer exists.

That is the actual hard problem with codebase memory. Retrieval is the easy half. Staying correct as the code changes underneath you is the half that matters, and it is only solvable if the memory is anchored to your code's real *structure*, not to text.

## A memory bound to a symbol dies when the symbol dies

In SecondOS every memory carries a scope: it is bound to the whole repo, to a file, or to a specific symbol. That anchor is what makes staleness possible.

After each sync, SecondOS re-resolves every file-scoped and symbol-scoped memory against the current map. We call it the K-S34 mechanic, and the rule is simple:

- a memory whose anchor is **no longer present** in the map flips to `stale`
- a stale memory whose anchor **comes back** flips to `active`
- repo-scoped memories are never stale, because their anchor is the repo itself

So a decision recorded against `hashPassword` is flagged stale the moment `hashPassword` is refactored away, instead of quietly describing code that is gone. Rename it back, or restore the file, and the memory revives on the next sync. The matching is tolerant (it resolves `path:name` or a bare `name`) so a memory anchors and un-stales consistently however it was written.

Two things make this trustworthy rather than annoying. First, staleness is a **status, not a deletion**: the memory stays in history, is de-prioritized in recall by decay, and can revive. SecondOS accretes, it does not overwrite. Second, the flip is **never silent**: `/sync/commit` returns how many memories staled and how many revived, and the CLI prints it. You see the change, you are not surprised by it.

## A concrete story: a memory sourced from a pull request

Here is the mechanic end to end. SecondOS can (opt-in, manual, read-only) ingest the discussion on a GitHub issue or PR and distill it into a memory *proposal*. The proposal is grounded against the stored map, so the memory binds to the file the PR actually touched, and it carries the source link, so when your agent later recalls it, it shows `from #N` pointing at the real discussion. Nothing about your source leaves the machine: the GitHub App holds no `contents` permission and reads only the issue and PR text.

Now that memory lives its life like any other. It is anchored to a real file. Months later someone refactors that file away. On the next sync the anchor no longer resolves, the memory flips to `stale`, and it stops surfacing in recall as if it were current. The decision is not lost, it is just no longer presented as live truth. If that code path returns, the memory revives. That is what "living" means here: the memory tracks the state of the code it describes, instead of freezing at the moment it was written.

## Three-axis drift: the bug in the gap between two files

Anchoring memory to structure buys a second thing: SecondOS can see mismatches that live *between* files, where the nastiest bugs hide. It surfaces three, all **read-only alerts** (K-S24) that never touch your code, and each reports both sides of the mismatch.

| Axis | Compares | Flags |
|---|---|---|
| Code vs DB schema | Drizzle writes in code | a column written in code the schema does not have |
| Env vars vs .env.example | `process.env` reads in code | a var read but documented in no example or schema file |
| Imports vs package.json | imports in code | a package imported but declared in no manifest on its path |

The design principle across all three is the same one that makes staleness trustworthy: **prove it or stay quiet**. Dynamic or spread writes are skipped and counted, never guessed. A dynamic `process.env[expr]` read is counted, not flagged. If there is no `.env.example` at all there is no baseline, so the used-but-undeclared check is skipped entirely rather than crying wolf. The dependency check is monorepo-aware: an import resolves against the nearest `package.json` and its ancestors, so a hoisted root dependency counts as declared. A memory or an alert you cannot trust is worse than none.

## Why vectors can't do this

A vector store can tell you a file *mentions* authentication. It cannot tell you that `hashPassword` was renamed, because a vector does not know a symbol from a substring: it just knows some numbers used to sit near some other numbers. A symbol knows. A call graph knows. A schema knows. That is the whole difference between memory that decays gracefully and memory that lies with confidence.

And the trade to be honest about: to anchor memory this way, SecondOS stores your code as a structural map (symbols, signatures, routes), never the full body (K-S23). For code that is a privacy win, the source never leaves your machine. It is a genuine trade, and it is the one we chose on purpose.

See [how the structure fits together](/product), [the drift axes](/product), or [reproduce the benchmark](/benchmark).
