You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit fc5116d
Browse filesBrowse the repository at this point in the historyBrowse files
feat(memory): memory from you — remember/forget, credited to the words 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.
// wires ctx.recallSessions (memory + the recall setting on), so it costs nothing otherwise. Read-only and
174
174
// instant like use_skill — it searches the project's OWN past-session outcomes, never the file system.
175
175
constRECALL_TOOL={name: 'recall_sessions',description: 'Search THIS project\'s past sessions for relevant earlier work — their recorded outcomes (what was built/fixed/decided), the files they touched, and the conversations themselves. Use when the user refers to earlier work ("what did we do about refunds?", "did we already fix the webhook?") or when prior context would clearly help before you start. Returns cited past-session outcomes (with a matching transcript snippet when the match is deep in the conversation) and dates; treat them as memory — informative but possibly stale, so verify against the current code.',input_schema: {type: 'object',properties: {query: {type: 'string',description: 'keywords or a short question, e.g. "refund retries" or "MCP timeline rail"'}},required: ['query']}};
176
+
// Memory the person asks for (docs/levelcode-sessions-memory.md §12). The host decides whether a note can be
177
+
// credited to the person (sessionMemory.groundNote); the agent only proposes it, with their words.
178
+
constREMEMBER_TOOL={name: 'remember',description: 'Save something the user told you, so future sessions in THIS project start knowing it. Use it when the user asks you to remember something; when they correct how you work ("no, use pnpm", "don\'t mock the database") or confirm an approach that was not obvious ("yes, keep doing that"); when they state a decision or constraint (write dates as absolute dates); or when they say where something lives (a dashboard, a ticket tracker, a doc). Do NOT save what the code or git history already shows, or details of the current task. `said` MUST be the user\'s own words, copied exactly from their message — the note is credited to them only when those words are found in what they typed and the note says no more than they did; otherwise it is kept unconfirmed until they confirm it. Keep `text` short and close to their words.',input_schema: {type: 'object',properties: {kind: {type: 'string',enum: ['preference','project','reference'],description: 'preference = how the user wants you to work (corrections and confirmed approaches); project = a decision, constraint or deadline; reference = where to find something outside the code'},text: {type: 'string',description: 'the note, one sentence, in the user\'s terms, e.g. "Use pnpm, never npm, in this repo"'},said: {type: 'string',description: 'the user\'s exact words this note rests on, copied from their message'}},required: ['kind','text','said']}};
179
+
constFORGET_TOOL={name: 'forget',description: 'Remove a note or fact from this project\'s memory when the user asks you to forget it or says it is no longer true. `said` MUST be the user\'s own words asking for it, copied exactly. Removes the best match for `query` and tells you what it removed.',input_schema: {type: 'object',properties: {query: {type: 'string',description: 'words that identify the memory, e.g. "pnpm"'},said: {type: 'string',description: 'the user\'s exact words asking you to forget it'}},required: ['query','said']}};
176
180
177
181
// How much of a background command's accumulated output the sniffers re-read on each chunk. Generous
178
182
// next to any single log line, so a url split across chunk boundaries is still found, yet small enough
@@ -736,6 +740,11 @@ async function runTool(tu, ctx) {
return'The user answered your questions (these are their recorded decisions — honor them and do NOT ask again):\n'+lines.join('\n')+'\n\nProceed with the work using these answers.';
740
749
}
741
750
if(tu.name==='use_skill'){
@@ -749,6 +758,27 @@ async function runTool(tu, ctx) {
749
758
ctx.post({type: 'agentTool',icon: 'sparkle',text: '🧩 using skill: '+name});// quiet chip — only on success (🧩 is the marker; 'sparkle' falls back cleanly)
750
759
returnbody;// SKILL.md body → tool_result, steers the next turns
751
760
}
761
+
if(tu.name==='remember'||tu.name==='forget'){
762
+
if(!ctx.memoryNotes){return'ERROR: project memory is off in this session.';}
763
+
// What the person typed this run: their goal and their answers to ask_user. The host checks `said` against it.
dbg('sessions.memory.remember',{ kind,verified: g.verified,reason: g.reason,grounded: Math.round(g.grounded*100)/100, ok });
1509
+
if(!ok){return{verified: false,message: 'ERROR: the note could not be saved.'};}
1510
+
refreshMemoryViews();
1511
+
if(g.verified){
1512
+
return{verified: true,message: 'Remembered as the user\'s '+kind+' note. It is in this project\'s memory now and in every future session. No need to repeat it back at length.'};
1513
+
}
1514
+
constwhy=g.reason==='not-said' ? 'the quoted words were not found in what the user typed this run'
1515
+
: g.reason==='adds-to-what-was-said' ? 'the note says more than the quoted words do'
1516
+
: 'the quoted words are too short to credit';
1517
+
return{verified: false,message: 'Saved as UNCONFIRMED ('+why+'). It is listed in the Memory tab and has no effect until the user confirms it there. If the user did ask for this, quote their exact words in `said` and keep the note to what they said.'};
1518
+
}
1519
+
// The forget tool's host half: removes the best-matching note or fact, when the person's own words ask for it.
1520
+
functionforgetNoteTool(req){
1521
+
constm=sessionsManager();
1522
+
if(!m){return{removed: '',message: 'No project memory is available in this workspace.'};}
return{removed: '',message: 'Not removed: the quoted words were not found in what the user typed this run. Tell the user they can remove it from the Memory tab.'};
return{removed: hit.text,message: 'Removed from memory: "'+hit.text+'". Tell the user what you removed, in case it was the wrong one (saying it again with remember brings it back).'};
1534
+
}
1535
+
// After memory changes from the chat: the Memory tab's list and the welcome-back strip.
0 commit comments