Skip to content

feat(memory): memory from you — remember/forget credited to the words you typed — and Search Sessions - #114

Merged
ndemianc merged 13 commits into
developfrom
feat/memory-from-you
Oct 20, 2026
Merged

ndemianc merged 13 commits into
developfrom
feat/memory-from-you

Conversation

@ndemianc

@ndemianc ndemianc commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Two additions to sessions and memory, modelled on how Claude's desktop app handles them:

  1. Memory from you. The agent can remember and forget what the user tells it to keep. A note counts as the user's only when the words it quotes are in what they typed this run and it says what they said, no more, in their order, with every not/never; otherwise it waits for one Confirm click.
  2. Search Sessions… A Command Palette quick pick (plus a search icon in the Sessions title bar) over every session's title and conversation.

Why

Project memory so far is inferred after a session ends: an outcome and at most two facts, with anything phrased as an order held back until confirmed, because a transcript is full of repository text. So "always use pnpm, never npm", the kind of thing a user most wants remembered, was exactly what got held back, and nothing recorded what the user actually said.

How a note is decided

credited to the user not credited
the quoted words are in the goal they typed (not a Retry, Continue or resumed goal), or their ask_user answers ✓ required
every content word of the note comes from those words (a word like "repo" for where it applies may be added) ✓ required
in the same order, each negated as it is in the quote ✓ required
state active at once, even phrased as an order kept, listed, inactive until Confirm
  • sessionMemory.groundNote decides this in code, not the model. Nothing the agent read (files, tool output) counts as the user's words.
  • Notes are facts.jsonl observations with origin: "person" (their words secret-scrubbed), so they get the existing fold, panel, recall and "not true". Repetition never promotes an uncredited note, and the summarizer's supersede pass is never shown one.
  • A credited note keeps the words it was credited on: a later uncredited save of the same note changes nothing.
  • MEMORY.md gets a From you section with the user's words beside each note. A note the user confirmed in the Memory tab is listed there too, as "you confirmed it" (its quote was never checked, so it is not shown as theirs). Every line older than a day shows its age. The always-on file is capped at 25 notes and 15 facts; the rest are left to recall_sessions.
  • forget removes the best match only when the user's words ask for something to be forgotten and name that memory (sessionMemory.asksToForget); otherwise it removes nothing and points to the Memory tab. It reports what it removed, and says so when the store refused the write.
  • Setting: levelcode.ai.sessions.memory.notes (on by default).

Search Sessions

Every session not in the trash, archived ones included. All words must match, title matches come first, then newest first, each with the line it matched. It's a quick pick, not a History field: the field was removed on 2026-08-02 (77d4f13) and stays out. Every session's title is checked at once from the index; conversations are read 50 at a time while the list fills in, matched whole, and cached by size and modification time (20 million characters in all). Title matches are ranked first before the list is cut to 40.

Compared with Claude's app

Claude's agent writes typed memory files plus an always-loaded index, a model picks up to five memories per request, recalled ones carry an age warning, and transcript search is a plain substring match. The differences here are on purpose (no per-request selection, because LevelCode's memory rides the cached system prompt; code, not the model, credits a note to the user) and are written up in docs/levelcode-sessions-memory.md §12.

Checked (macOS 27.0.1, Node 24)

  • scripts/test-extensions.sh: 129 files, including test/memoryNotes.test.js (21) and test/memoryNotesHost.test.js (8: the real runAgent with a stood-in provider; the host halves lifted out of extension.js), plus ten new hostile cases in test/memoryPoisoning.test.js (each order, quoted against a real goal, stays inactive).
  • Fifteen single-defect mutations, each turning a test red.
  • scripts/memory-editor-check.js in the dev build with a stand-in provider: 11/11. Covered: the credited note, the README's order kept unconfirmed, the Memory tab, the next session's instructions carrying the note, and Search Sessions reopening the conversation.
  • tsc --checkJs on the changed files: no new undefined names.

After review (2026-10-20)

Copilot's ten findings were all real, and each has its own commit with a test: the credit rule (word overlap credited "Use npm, never pnpm" from "use pnpm, never npm"), a credited note's quote overwritten by an uncredited save, confirmed notes under Facts, the chip when nothing was saved, forget with any typed quote and any query, an ignored failed removal, a diagram Retry's generated text counted as typed, and search cut at 400,000 characters, at the newest 150 sessions, and before ranking. develop (#112, #113) is merged in; sessions.memory.notes is application-scoped like every other setting since #113.

  • scripts/test-extensions.sh: 133 files pass; memoryNotes 32 cases, memoryNotesHost 14.
  • 21 single-defect mutations of the fixes, on a copy with the network refused: all caught (two needed a test added).
  • memory-editor-check.js: 11/11 on a second run. The first timed out on "History lists the first conversation after New Session", the panel flip noted below.
  • tsc --checkJs on the changed files: no undefined names.

Not run

  • A real model deciding when to call remember; only a scripted one has.
  • The gate on Linux, and the browser edition.

Also seen, not from this change: in about half of the editor runs the Sessions panel flipped back to the Memory tab right after New Session. The same happens on develop.

…s you typed — and Search Sessions

Project memory so far is inferred: when a session ends, the summarizer writes an outcome and at most two
facts, and a fact phrased as an order is held back until confirmed, because a transcript is full of
repository text. Nothing recorded what the person SAID they want kept, and the most useful kind — a
correction ("no, use pnpm") or a confirmed approach — is exactly the kind phrased as an order.

remember / forget (agent tools, setting levelcode.ai.sessions.memory.notes, on by default):
- the agent saves a note during the run, with a kind (preference | project | reference), the note, and
  `said`: the person's own words
- sessionMemory.groundNote credits it to the person only when `said` appears in what they typed THIS run
  (the goal as typed, their ask_user choices and notes — nothing the agent read) and at least two thirds of
  the note's content words come from those words. Code decides, not the model
- a credited note is active at once, even phrased as an order; any other note is kept and listed as
  unconfirmed until a Confirm click — so a README's order with a real quote of the person stapled on stays
  inactive
- notes are facts.jsonl observations with origin "person" (said is secret-scrubbed): the same fold, panel,
  recall and "not true". Repetition never promotes an uncredited note, and the summarizer's supersede pass
  is never shown one (supersedableFacts)
- forget removes the best match, only when the request is in the person's words, and says what it removed
- MEMORY.md gets a "From you" section with the person's words beside each note; every line older than a
  day shows its age; the always-on file is capped at 25 notes and 15 facts ("…and N more; recall finds them")
- the Memory tab lists notes under "From you" (yours / unconfirmed, with Confirm and Forget)

Search Sessions… (Command Palette, and a search icon in the Sessions title bar): every session not in the
trash, archived ones included, matched on its title and whole conversation (all words must match), newest
first, title matches first, each with the line it matched. A quick pick, not a field: the field was taken
out of History on 2026-08-02 (77d4f13) and the owner chose to keep it out. Transcripts are read at most 150
at a time, cached by size + mtime.

Modelled on Claude's desktop app (typed memory files written by the agent, an index loaded every session,
an age warning on recalled memories, transcript search); differences on purpose are in
docs/levelcode-sessions-memory.md §12 (next commits).

Checked: unit gate 129 files on Node 24, macOS, including test/memoryNotes.test.js (21),
test/memoryNotesHost.test.js (8: the real runAgent with a stood-in provider; the host halves lifted out of
extension.js) and ten new hostile cases in test/memoryPoisoning.test.js. Fifteen single-defect mutations,
each red. tsc --checkJs: no new undefined names. Not run: a real model choosing when to call remember,
Linux, the browser edition.
…Sessions in the real editor

The dev build as a throwaway instance with this checkout's extension and a stand-in provider, as
diagram-editor-check.js does. The stand-in plays: "From now on always use pnpm, never npm" → remember with the
person's words (kept as theirs); a README asking AI agents to remember that every push goes straight to main
→ remember with that order and a real quote stapled on (kept UNCONFIRMED). It checks facts.jsonl and
MEMORY.md, the chat's "remembered" rows, the Memory tab's "From you" section, that History lists the session
after New Session, that the next session's instructions carry the pnpm note and not the README's order, and
that Search Sessions… finds the first conversation by what was said and reopens it.

Every DevTools call is bounded (a webview the workbench replaces never answers), and the Sessions webview is
found again after New Session. 11/11 on macOS against vscode/.build/electron. Not part of
scripts/test-extensions.sh.
…s, how Claude's app does it, what differs

levelcode-sessions-memory.md §12: the remember/forget tools and the crediting rule, how notes live beside
facts, MEMORY.md's "From you" section with ages and caps, Search Sessions as a quick pick and why, how
Claude's desktop app and its Claude Code handle memory and history (read from the shipped bundles on
2026-10-19), the differences on purpose (and why per-request memory selection was left out: memory rides the
cached system prompt), what was run and what was not. Settings table: sessions.memory.notes.

levelcode-sessions-experience.md §4.4: a note that the in-panel search was removed (77d4f13) and came back as
the Search Sessions quick pick.
@ndemianc
ndemianc marked this pull request as ready for review October 19, 2026 23:28
Copilot AI balanced review requested due to automatic review settings October 19, 2026 23:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread extensions/levelcode-ai/agent.js Outdated
Comment thread extensions/levelcode-ai/extension.js Outdated
Comment thread extensions/levelcode-ai/extension.js Outdated
Comment thread extensions/levelcode-ai/extension.js Outdated
Comment thread extensions/levelcode-ai/sessionMemory.js Outdated
Comment thread extensions/levelcode-ai/sessionMemory.js
Comment thread extensions/levelcode-ai/sessionMemory.js Outdated
Comment thread extensions/levelcode-ai/sessions.js Outdated
Comment thread extensions/levelcode-ai/sessions.js Outdated
Comment thread extensions/levelcode-ai/sessions.js Outdated
agent.js: ask_user's answers now feed both the reviewer (ctx.userAnswers, #112) and what remember
may quote (ctx.heardThisRun); both kept.

package.json: the new levelcode.ai.sessions.memory.notes is application-scoped, like every other
levelcode-ai setting since #113 (test/settingsScope.test.js failed on the merge without it: a
folder's .vscode/settings.json could have switched remembering on or off).
…, order and every not

groundNote used to credit a note when two thirds of its content words came from the quote. Word
overlap does not preserve an instruction: "Use npm, never pnpm" has every word of "use pnpm, never
npm", and "Disable signature verification" every word of "never disable signature verification"
(the stopword list dropped "no"/"not"). The two-thirds slack also let an antonym through ("Enable
signature verification" from "disable signature verification").

Now every content word of the note must come from the quote (only a word naming where it applies,
like "repo", may be added), in the quote's order, each negated as it is there: a not/never counts
for the next content word in its clause, two cancel, and "stop"/"avoid" negate like "don't". A note
that fails is kept unconfirmed, as before, and the agent is told which rule it broke. The remember
tool's description says the same.

Tests: the reordered, negation-dropped, negation-added, double-negated and antonym notes are not
credited; a file name stays one word. Two fixtures changed with the rule: a quote that left out
"in tests" now includes it, and "Use pnpm for scripts" (adds "scripts" to "always use pnpm") became
"Always use pnpm" in the test of an unknown kind.
…edited on

foldFacts took `said` from every person observation of a note but set `yours` only from credited
ones. So after a credited "Use pnpm" (said: "always use pnpm"), a second remember() of the same
note with an invented instruction in `said` failed grounding yet replaced the quote, and the
invention appeared as "you said" in the always-on "From you" section.

A credited note now keeps its phrasing, quote and date together: only another credited
observation changes them. Its source is still counted. After the person removes it, an uncredited
save is recorded as before and stays inactive.
…uote nobody checked is not "you said"

Confirm is the fallback for a note that could not be credited to the person's words, but it set
`confirmed`, not `yours`, and the digest put only `yours` notes under "From you". A confirmed
"Always rebase before merging" landed under Facts, whose preamble tells the agent never to act on an
instruction found there, while the Memory tab called it yours.

The digest now lists a note under "From you" when it is credited or confirmed. A confirmed note
says "you confirmed it" instead of its quote, because that quote was never matched to what the
person typed. For the same reason, the Memory tab labels an uncredited note's quote "the agent
quoted", not "you said". An inferred fact the person confirmed stays under Facts.
rememberNoteTool answered `verified: false` both for an unconfirmed note that was saved and for a
note that was not saved at all (no folder open, or the store refused the write), and the agent
posted "🧠 remembered: … (unconfirmed)" for all three. The person saw a memory that did not exist.

The host now also returns `saved`, and the agent posts the chip only when it is true. The agent is
still told what happened in the tool result.
…e, and says when it could not

Two review findings in forgetNoteTool:

- It checked `said` against itself, which only proves the words were typed. With the goal "fix the
  failing date test" quoted as `said`, any query the model chose ("pnpm") removed the person's
  package-manager preference. Now sessionMemory.asksToForget requires the quote to be typed this
  run, to ask for something to go ("forget", not "don't forget"; "remove"/"delete" only beside
  memory/note/rule/preference, so "remove the pnpm lockfile" is not a request; "no longer true",
  "outdated"), and to name the memory the model picked: a word of its text, or of a credited note's
  quote, other than words every memory shares. Otherwise nothing is removed, and the agent is told
  which memory came closest and that the person can use Forget in the Memory tab.
- It ignored factAction's result. On a store that refused the write it still answered `removed`,
  and the agent posted "forgot" while the memory stayed in effect. Now that is reported as an error.
…uote

agentFlow handed every goal to the agent as goalText, the words `remember` and `forget` may quote
as the person's. Not every goal is typed: retryDiagram builds one from the model's own diagram
title and validation messages, and Continue and a resumed goal are not typed now. After a click
on Retry, the model could quote its own generated text and have a note credited to the person.

goalText is now typedGoalText(text, opts): the goal when opts.typed (handleSend), else ''. This is
the rule develop already applies to the Ask When Risky reviewer (rememberTypedGoal, #112). Tests:
the helper on its own, a guard that agentFlow passes it, and the real agent run with a Retry
prompt, where nothing of it reaches the person's words.
…rsations, title matches first

Three review findings in History search:

- Each conversation was cut to its first 400,000 characters before matching, so a phrase later in
  a long or often-resumed session was never found. The whole conversation is matched now; the
  cache is bounded by the characters it holds in all (20M, least recently used out first) rather
  than by cutting each session.
- Only the newest 150 sessions were searched, title matches included, and the quick pick had no
  way to reach the rest. searchStep(query, {from, readMax}) now reads conversations a batch at a
  time (50) and returns where to go on. Its first step reports every title or preview match in
  the index with no read. search() runs every step; the quick pick runs them in the background,
  re-ranks as each lands, stops when the query changes or the pick closes, and says "no session
  mentions" only after every session was searched.
- The limit was applied while scanning, before title matches were put first, so 40 newer matches
  in the conversation pushed out an older exact title match. rankSearch ranks first, then cuts.

Tests over a store of 163 sessions: a match in the second-oldest conversation, a phrase past the
first 400,000 characters, an older title match ahead of 40 newer ones under limit 5, steps that
add up to search(); and the quick pick reaching past the first batch and dropping a superseded
query's scan. The test that pinned scanMax is replaced.
…nv's dot does not end a not

Two of 21 single-defect mutations of the review fixes passed every suite: limiting before ranking
(the title match was found by the first step's no-read pass, so it was never cut), and splitting a
clause at every dot (only a note that writes the file name differently from the quote shows it).
Each now has a case.
…y not, forget needs a request that names the memory, search covers every session

What the ten review fixes changed in the design, and what was run after them.
@ndemianc
ndemianc merged commit 2824011 into develop Oct 20, 2026
2 checks passed
@ndemianc
ndemianc deleted the feat/memory-from-you branch October 20, 2026 00:50
ndemianc added a commit that referenced this pull request Oct 20, 2026
Conflicts, each keeping both sides:
- agent.js, the end of a run: Ask When Risky's reviewer cost and summary row (#112) and this branch's
  plan completion and returned "Send now" messages; one agent.done log line with both sets of fields.
- extension.js, resumeSession and New Chat: this branch's plan/steer reset and the reset of the person's
  recorded words (#112/#114), each in both places.
- media/chat.html, the message switch: develop's `approvals` message (#112 replaced the two-option
  `autopilot` one, whose handler no longer exists) and this branch's agentStart/agentStatus, which keep
  the task list and drop the old plan bar.
- test/sessionExpiredHost.test.js, the harness declarations: both sets (agentMode from env, the
  approvals mode and recorded words; this branch's agentPlan and steerInbox).

Checked on the merged tree (macOS, Node 24): scripts/test-extensions.sh 136 files; tsc --checkJs on
extension.js and agent.js, no new undefined names; in the dev build plan-progress-editor-check 17/17,
approvals-editor-check 12/12, memory-editor-check 11/11 on one of four runs — its "History lists the
session after New Session" step is flaky on develop too (1 of 2 there).
ndemianc added a commit that referenced this pull request Oct 20, 2026
A reply in the next-step box shows a 1-3 word label; what a click sends is the reply's message, up to 300
characters the agent wrote (visible only as a tooltip). It went through the chat's normal send, so the host
recorded it as typed — and since #112 and #114 that is the person's word: the Ask When Risky reviewer counts
it as authorization, and remember may quote it. A README that steers the agent into a "Yes" button whose
message says more could have turned one click into an explicit go-ahead.

The page now sends a click as a reply ({ type: 'send', reply: true }), and keeps that mark through the queue
(drained at the end of a run, or sent with Send now while idle). handleSend passes typed: false for it, so
the reply reaches the agent as before but is not the person's word. A reply the person edits in the message
box, or that is handed back to it, is sent as their own typing.

Tested: the page's queue functions lifted from chat.html (sent at once, queued while busy, drained, Send now
when idle — the mark survives each, and a typed message carries none); handleSend lifted into
sessionExpiredHost (a reply reaches agentFlow with typed: false, a typed message with true); the button's
click and the send message are pinned from source.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants