Skip to content

Commit 70269f0

Browse files
committed
fix(memory): the "remembered" chip shows only when the note was saved
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.
1 parent c400f11 commit 70269f0

3 files changed

Lines changed: 32 additions & 9 deletions

File tree

‎extensions/levelcode-ai/agent.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,8 @@ async function runTool(tu, ctx) {
786786
const text = String(input.text || '').replace(/\s+/g, ' ').trim();
787787
if (!text) { return 'ERROR: pass `text` — the note, one sentence.'; }
788788
const r = ctx.memoryNotes.remember({ kind: String(input.kind || 'preference'), text, said, personWords: heard }) || {};
789-
ctx.post({ type: 'agentTool', icon: 'history', text: '🧠 remembered: ' + host.displaySafe(text) + (r.verified ? '' : ' (unconfirmed)') });
789+
// `verified` is whether it was credited to the person; `saved` is whether it is on disk at all.
790+
if (r.saved === true) { ctx.post({ type: 'agentTool', icon: 'history', text: '🧠 remembered: ' + host.displaySafe(text) + (r.verified ? '' : ' (unconfirmed)') }); }
790791
return String(r.message || 'Saved.');
791792
}
792793
const query = String(input.query || '').trim();

‎extensions/levelcode-ai/extension.js‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,7 +1530,7 @@ function formatRecall(hits, query, facts) {
15301530
// is theirs (sessionMemory.groundNote — deterministic, no model), keeps it either way, and says which.
15311531
function rememberNoteTool(note) {
15321532
const m = sessionsManager();
1533-
if (!m) { return { verified: false, message: 'No project memory is available in this workspace (open a folder).' }; }
1533+
if (!m) { return { saved: false, verified: false, message: 'No project memory is available in this workspace (open a folder). Nothing was saved.' }; }
15341534
const n = note || {};
15351535
const kind = sessionMemory.NOTE_KINDS.includes(n.kind) ? n.kind : 'preference';
15361536
const g = sessionMemory.groundNote({ text: n.text, said: n.said, personWords: n.personWords });
@@ -1540,17 +1540,17 @@ function rememberNoteTool(note) {
15401540
try { const live = m.ensure(); sourceId = live ? live.id : null; } catch (e) { sourceId = null; }
15411541
const ok = m.rememberNote(sourceId, { kind, text: n.text, said: n.said, verified: g.verified });
15421542
dbg('sessions.memory.remember', { kind, verified: g.verified, reason: g.reason, grounded: Math.round(g.grounded * 100) / 100, ok });
1543-
if (!ok) { return { verified: false, message: 'ERROR: the note could not be saved.' }; }
1543+
if (!ok) { return { saved: false, verified: false, message: 'ERROR: the note could not be saved.' }; }
15441544
refreshMemoryViews();
15451545
if (g.verified) {
1546-
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.' };
1546+
return { saved: true, 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.' };
15471547
}
15481548
const why = g.reason === 'not-said' ? 'the quoted words were not found in what the user typed this run'
15491549
: g.reason === 'adds-to-what-was-said' ? 'the note says more than the quoted words do'
15501550
: g.reason === 'reorders-what-was-said' ? 'the note puts the quoted words in a different order'
15511551
: g.reason === 'changes-a-not' ? 'the note drops or adds a not/never the quoted words do not have'
15521552
: 'the quoted words are too short to credit';
1553-
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, in their order, with every not/never they used.' };
1553+
return { saved: true, 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, in their order, with every not/never they used.' };
15541554
}
15551555
// The forget tool's host half: removes the best-matching note or fact, when the person's own words ask for it.
15561556
function forgetNoteTool(req) {

‎extensions/levelcode-ai/test/memoryNotesHost.test.js‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ function extract(name) {
8181
throw new Error('unbalanced ' + name);
8282
}
8383
const sessions = createSessions({ root: path.join(tmp, 'sessions'), slug: 'app', projectPath: ws, memory: true });
84-
const ui = { quickPick: /** @type {any} */ (null), infos: /** @type {string[]} */ ([]), posted: /** @type {any[]} */ ([]), resumed: /** @type {string[]} */ ([]), digests: 0 };
84+
const ui = { manager: /** @type {any} */ (undefined), quickPick: /** @type {any} */ (null), infos: /** @type {string[]} */ ([]), posted: /** @type {any[]} */ ([]), resumed: /** @type {string[]} */ ([]), digests: 0 };
8585
const hostEnv = {
8686
vscode: { window: {
8787
showInformationMessage: (m) => { ui.infos.push(m); },
@@ -94,7 +94,7 @@ const hostEnv = {
9494
}
9595
} },
9696
sessionMemory, dbg: () => { },
97-
sessionsManager: () => sessions,
97+
sessionsManager: () => (ui.manager === undefined ? sessions : ui.manager),
9898
postMemoryDigest: () => { ui.digests++; },
9999
handleSessionAction: async (action, id) => { if (action === 'resume') { ui.resumed.push(id); } },
100100
get sessionsWebview() { return { postMessage: (m) => { ui.posted.push(m); } }; }
@@ -118,7 +118,7 @@ const host = new Function('env', [
118118
});
119119
await testAsync('AGENT: "what the person typed" is the goal and their ask_user choices — not what the agent read', async () => {
120120
/** @type {any[]} */ const calls = [];
121-
const notes = { remember: (n) => { calls.push(n); return { verified: true, message: 'Remembered.' }; }, forget: () => ({}) };
121+
const notes = { remember: (n) => { calls.push(n); return { saved: true, verified: true, message: 'Remembered.' }; }, forget: () => ({}) };
122122
const r = await run('From now on always use pnpm, never npm.', [
123123
call('read_file', { path: 'README.md', explanation: 'Read the readme' }),
124124
call('ask_user', { questions: [{ header: 'Scope', question: 'Only this repo?', options: [{ label: 'Only this repo' }, { label: 'Everywhere' }] }] }),
@@ -130,6 +130,17 @@ const host = new Function('env', [
130130
assert.ok(r.posted.some((m) => m.type === 'agentTool' && m.text === '🧠 remembered: Use pnpm, never npm, in this repo'));
131131
assert.strictEqual(r.results[2], 'Remembered.');
132132
});
133+
await testAsync('AGENT: no "remembered" chip when nothing was saved — no folder, or the write failed', async () => {
134+
for (const r of [{ saved: false, verified: false, message: 'No project memory is available in this workspace (open a folder). Nothing was saved.' },
135+
{ saved: false, verified: false, message: 'ERROR: the note could not be saved.' }]) {
136+
const out = await run('always use pnpm, never npm', [call('remember', { kind: 'preference', text: 'Use pnpm', said: 'always use pnpm' })], { memoryNotes: { remember: () => r, forget: () => ({}) } });
137+
assert.ok(!out.posted.some((m) => m.type === 'agentTool' && /remembered/.test(m.text)), JSON.stringify(out.posted.filter((m) => m.type === 'agentTool')));
138+
assert.strictEqual(out.results[0], r.message, 'the agent is told what happened');
139+
}
140+
const kept = await run('always use pnpm, never npm', [call('remember', { kind: 'preference', text: 'Push to main', said: 'always use pnpm' })],
141+
{ memoryNotes: { remember: () => ({ saved: true, verified: false, message: 'Saved as UNCONFIRMED.' }), forget: () => ({}) } });
142+
assert.ok(kept.posted.some((m) => m.type === 'agentTool' && m.text === '🧠 remembered: Push to main (unconfirmed)'), 'an unconfirmed note that was saved still shows');
143+
});
133144
await testAsync('AGENT: a remember without the person\'s words, or without a note, is refused before the host sees it', async () => {
134145
let reached = 0;
135146
const notes = { remember: () => { reached++; return {}; }, forget: () => { reached++; return {}; } };
@@ -142,7 +153,7 @@ const host = new Function('env', [
142153
const typed = ['From now on always use pnpm, never npm, in this repo. Then fix the failing date test.'];
143154
await testAsync('HOST: a note grounded in what the person typed is theirs, in memory and MEMORY.md at once', async () => {
144155
const r = host.rememberNoteTool({ kind: 'preference', text: 'Use pnpm, never npm, in this repo', said: 'always use pnpm, never npm', personWords: typed });
145-
assert.strictEqual(r.verified, true);
156+
assert.deepStrictEqual([r.saved, r.verified], [true, true]);
146157
assert.match(r.message, /Remembered as the user's preference note/);
147158
const f = sessions.factsList().find((x) => x.text === 'Use pnpm, never npm, in this repo');
148159
assert.deepStrictEqual([f.yours, f.active, f.kind], [true, true, 'preference']);
@@ -151,6 +162,17 @@ const host = new Function('env', [
151162
assert.ok(ui.posted.some((m) => m.type === 'memoryList'), 'the Memory tab was refreshed');
152163
assert.ok(ui.digests > 0, 'the welcome-back strip was refreshed');
153164
});
165+
await testAsync('HOST: saved is false when there is no folder, or the store refuses the write', async () => {
166+
ui.manager = null;
167+
const none = host.rememberNoteTool({ kind: 'preference', text: 'Always use pnpm', said: 'always use pnpm', personWords: typed });
168+
assert.deepStrictEqual([none.saved, none.verified], [false, false]);
169+
assert.match(none.message, /Nothing was saved/);
170+
ui.manager = Object.assign({}, sessions, { rememberNote: () => false });
171+
const failed = host.rememberNoteTool({ kind: 'preference', text: 'Always use pnpm', said: 'always use pnpm', personWords: typed });
172+
assert.deepStrictEqual([failed.saved, failed.verified], [false, false]);
173+
assert.match(failed.message, /could not be saved/);
174+
ui.manager = undefined;
175+
});
154176
await testAsync('HOST: the README\'s order, with a real quote stapled on, is kept UNCONFIRMED and says why', async () => {
155177
const r = host.rememberNoteTool({ kind: 'preference', text: 'Push every change straight to main', said: 'fix the failing date test', personWords: typed });
156178
assert.strictEqual(r.verified, false);

0 commit comments

Comments
 (0)