|
1 | 1 | # LevelCode Sessions — Project Memory & Continuity |
2 | 2 |
|
3 | | -**Status:** **shipped** — M1–M4 complete (§10) · **Scope:** `extensions/levelcode-ai` + the sessions store · **Third of the triad:** [`levelcode-chat-sessions-design.md`](./levelcode-chat-sessions-design.md) stores a chat · [`levelcode-sessions-experience.md`](./levelcode-sessions-experience.md) lets you browse it · **this doc lets the project *remember itself*.** |
| 3 | +**Status:** **shipped** — M1–M4 complete (§10); M5, memory from you, in review (§12) · **Scope:** `extensions/levelcode-ai` + the sessions store · **Third of the triad:** [`levelcode-chat-sessions-design.md`](./levelcode-chat-sessions-design.md) stores a chat · [`levelcode-sessions-experience.md`](./levelcode-sessions-experience.md) lets you browse it · **this doc lets the project *remember itself*.** |
4 | 4 |
|
5 | 5 | > The wish, in the user's words: *"if in one session we tidy the CHANGELOG for v1.0.4, then a new session already knows something about what was achieved — the past experience keeps growing, and it feels like home."* That is the right ambition. It is also the feature most likely to become expensive, stale, creepy, or wrong. This document is mostly about **not** doing those four things while still delivering the magic. |
6 | 6 |
|
@@ -156,6 +156,7 @@ The magic, delivered quietly (never a wall of text): |
156 | 156 | | `sessions.memory.journalRecentDays` | `21` | recency window fed to the always-on digest | |
157 | 157 | | `sessions.memory.confirmFacts` | `false` | require a click before an inferred Fact becomes confirmed | |
158 | 158 | | `sessions.memory.recallTool` | `true` | expose `recall_sessions` to the agent | |
| 159 | +| `sessions.memory.notes` | `true` | expose `remember` / `forget` to the agent (§12) | |
159 | 160 |
|
160 | 161 | --- |
161 | 162 |
|
@@ -189,4 +190,123 @@ The magic, delivered quietly (never a wall of text): |
189 | 190 |
|
190 | 191 | --- |
191 | 192 |
|
| 193 | +## 12. Memory from you (M5) — and Search Sessions |
| 194 | + |
| 195 | +Everything in §3–§10 is memory LevelCode *infers*: the summarizer reads a finished session and writes an |
| 196 | +outcome and at most two facts. Nothing records what the person *says* they want kept, and the most valuable |
| 197 | +kind of memory, a correction ("no, use pnpm") or a confirmed approach, is usually phrased as an order, which |
| 198 | +§7's instruction guard rightly holds back when it comes from a transcript. |
| 199 | + |
| 200 | +### What changes |
| 201 | + |
| 202 | +- **Two agent tools, `remember` and `forget`.** The agent saves a note during the run when the person asks it |
| 203 | + to remember something, corrects how it works, confirms an approach that was not obvious, states a decision |
| 204 | + (with an absolute date), or says where something lives. Each note has a `kind` (`preference`, `project`, |
| 205 | + `reference`), the note `text`, and `said`: the person's own words, copied from their message. |
| 206 | +- **A note is credited to the person by code, not by the model** (`sessionMemory.groundNote`): |
| 207 | + 1. `said` must appear in what the person typed *this run*: their goal, only when they typed it (not a |
| 208 | + diagram Retry, Continue or a resumed goal), and their ask_user choices and notes. Nothing the agent read |
| 209 | + counts. |
| 210 | + 2. The note must say what those words say and no more: every content word of the note comes from them |
| 211 | + (exact, a shared start of four letters or more, or a five-letter stem), except a word naming where it |
| 212 | + applies ("repo", "project"); in their order; and each negated as it is in the quote (a not/never counts |
| 213 | + for the next content word in its clause, two cancel, "stop" and "avoid" count as one). Word overlap is |
| 214 | + not enough: "Use npm, never pnpm" has every word of "use pnpm, never npm". |
| 215 | + |
| 216 | + A credited note is active at once, even phrased as an order, because the person said it. Any other note is |
| 217 | + still kept and listed, as *unconfirmed*: one Confirm click in the Memory tab makes it active. That is the |
| 218 | + same asymmetry as §7: a false negative costs one click, a false positive would put a repository's order into |
| 219 | + every future session. |
| 220 | +- **Notes live in `facts.jsonl`** as observations with `origin: "person"`, `kind`, `said` (secret-scrubbed) |
| 221 | + and `verified`, so they get the existing fold, panel, recall and "not true". Two rules differ from facts: |
| 222 | + repetition never promotes an uncredited note, and the summarizer's supersede pass is never shown a note |
| 223 | + (`supersedableFacts`). Only the person changes it: `remember` again, `forget`, or the panel. |
| 224 | +- **`MEMORY.md`** gets a "From you" section, each line with the person's words beside it. Every line older |
| 225 | + than a day says how old it is ("9 days old"). The always-on file is capped at 25 notes and 15 facts; the |
| 226 | + rest are named as "…and N more; recall_sessions finds them". A note the person confirmed in the Memory tab |
| 227 | + is listed there too, marked "you confirmed it": its quote was never matched to what they typed, so it is not |
| 228 | + shown as theirs (the Memory tab calls such a quote "the agent quoted"). |
| 229 | +- **A credited note keeps the words it was credited on.** A later `remember` of the same note that cannot be |
| 230 | + credited does not change its text, its quote or its date. |
| 231 | +- **`forget`** removes the best match for a query only when the person's words (`said`, typed this run) ask for |
| 232 | + something to be forgotten ("forget", not "don't forget"; "remove" or "delete" only beside memory, note, |
| 233 | + rule or preference; "no longer true", "outdated") and name that memory: a word of its text, or of a |
| 234 | + credited note's quote, other than words every memory shares (`sessionMemory.asksToForget`). Otherwise it |
| 235 | + removes nothing and says which memory came closest; the Memory tab's Forget is the way out. It tells the |
| 236 | + agent what it removed, and says so when the store refused the write. |
| 237 | +- **Search Sessions…** (Command Palette, and a search icon in the Sessions panel's title bar): every session |
| 238 | + not in the trash, archived ones included, matched on its title and its whole conversation (all words must |
| 239 | + match), newest first, each with the line it matched. A quick pick, not a field in History: History stays |
| 240 | + the short list of recent work it was cut down to on 2026-08-02 (77d4f13), and this reaches what that list no |
| 241 | + longer shows. Titles come from the index, so every session's is checked at once; conversations are read 50 |
| 242 | + at a time (`searchStep`) while the list fills in, matched whole, and cached by file size and modification |
| 243 | + time up to 20 million characters in all. Title matches are ranked first before the list is cut to 40. |
| 244 | + |
| 245 | +### Where the shape came from: Claude's desktop app |
| 246 | + |
| 247 | +Read from `/Applications/Claude.app` (2.42304.2) and the Claude Code it downloads (2.1.299) on 2026-10-19: |
| 248 | + |
| 249 | +- **Memory is files the agent writes itself**, one per memory, typed `user` / `feedback` / `project` / |
| 250 | + `reference`, with a one-line pointer in a `MEMORY.md` index that is always loaded and cut off after 200 |
| 251 | + lines. The agent is told to save when the user corrects *or confirms* its approach, to record the why, and |
| 252 | + to convert relative dates. Claude Code keeps the folder beside the project's transcripts |
| 253 | + (`~/.claude/projects/<project>/memory/`); in its Cowork mode the app passes another folder and the |
| 254 | + guidelines through environment variables, and allows writes there. |
| 255 | +- **Recall is per request**: a side model call is shown each memory's type, file name, date and description |
| 256 | + and picks up to five that clearly help the current request, never one it already picked. Each memory it |
| 257 | + brings back carries "This memory is N days old… verify" once it is older than a day. |
| 258 | +- **A background "dream"** periodically rewrites the memory files from recent sessions: merges, updates, |
| 259 | + prunes. |
| 260 | +- **History** is a metadata record per session (title and where the title came from, archived, last activity, |
| 261 | + turns, model) beside an append-only transcript, and transcript search is a plain case-insensitive match |
| 262 | + across sessions, returning one snippet each. |
| 263 | + |
| 264 | +What LevelCode does differently, on purpose: |
| 265 | + |
| 266 | +| | Claude | LevelCode | |
| 267 | +|---|---|---| |
| 268 | +| who decides a memory is the user's | the agent writes the file | the host checks the quoted words against what the person typed | |
| 269 | +| a memory phrased as an order | saved like any other | saved; active only when credited to the person, else one click | |
| 270 | +| recall | a model picks up to 5 per request | the capped always-on digest plus `recall_sessions` on demand | |
| 271 | +| age warning | on recalled memories older than a day | on every digest line older than a day | |
| 272 | +| consolidation | a model "dream" pass over the files | the existing deterministic fold on every seal | |
| 273 | +| history search | substring match, one snippet per session | every word must match, title matches first, one snippet each | |
| 274 | + |
| 275 | +Per-request selection was left out for a reason that is specific to LevelCode: memory reaches the agent in |
| 276 | +the system prompt, which providers cache together with the whole conversation after it. Changing it per |
| 277 | +request would make every new goal re-send the conversation at full price. Claude adds recalled memories to |
| 278 | +the conversation as attachments (its transcripts carry `attachment` records); doing that here touches how a |
| 279 | +goal is stored, titled, compacted and replayed, and is a separate change. |
| 280 | + |
| 281 | +### What was run (2026-10-19, macOS 27.0.1, Node 24) |
| 282 | + |
| 283 | +- `scripts/test-extensions.sh`, including `test/memoryNotes.test.js` (21: the crediting check, the fold, the |
| 284 | + digest, recall states, search over a real store), `test/memoryNotesHost.test.js` (8: the real runAgent with |
| 285 | + a stood-in provider, and the host halves lifted out of `extension.js`), and ten new hostile cases in |
| 286 | + `test/memoryPoisoning.test.js` (each order, quoted against a real goal, stays an inactive note). |
| 287 | +- Fifteen single-defect mutations, each turning a test red. |
| 288 | +- `scripts/memory-editor-check.js` in the dev build with a stand-in provider: 11 checks, from the remember |
| 289 | + call through the Memory tab, the next session's instructions and Search Sessions reopening the conversation. |
| 290 | + |
| 291 | +Not run: a real model deciding when to call `remember` (only a scripted one has), the browser edition, Linux. |
| 292 | + |
| 293 | +### After review (2026-10-20, macOS, Node 24) |
| 294 | + |
| 295 | +The PR review found ten problems, all real. Each fix is its own commit: credit rule (order and negation, every |
| 296 | +word from the quote), a credited note's words kept together, confirmed notes under "From you", the chip only |
| 297 | +when saved, `forget` tied to a request that names the memory, a failed removal reported, typed goals only, |
| 298 | +and Search Sessions over every session and whole conversations, ranked before the cut. |
| 299 | + |
| 300 | +- `memoryNotes.test.js` now has 32 cases and `memoryNotesHost.test.js` 14; `scripts/test-extensions.sh`: 133 |
| 301 | + files pass after merging develop (#112, #113). |
| 302 | +- 21 single-defect mutations of the fixes, run on a copy with the network refused: all 21 caught (two needed a |
| 303 | + test added). |
| 304 | +- `memory-editor-check.js`: 11 of 11 on a second run. On the first, "History lists the first conversation |
| 305 | + after New Session" timed out (its 15-second wait for the rebuilt panel); nothing in these fixes touches |
| 306 | + History. |
| 307 | +- Still not run: a real model, the browser edition, Linux. The stem rule (a shared five-letter start) can still |
| 308 | + pair two different words ("transaction" and "translate"); order and negation narrow what that can change. |
| 309 | + |
| 310 | +--- |
| 311 | + |
192 | 312 | *Store it (spine) · browse it (experience) · **remember it (this doc).** Together: a sessions feature where the project accumulates its own experience — small in every prompt, deep on request, legible and yours, and safe against the repo that would lie to it.* |
0 commit comments