Skip to content

Commit 4732443

Browse files
committed
fix(memory): only a goal the person typed is theirs for remember to quote
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.
1 parent b1ae4e5 commit 4732443

2 files changed

Lines changed: 21 additions & 3 deletions

File tree

‎extensions/levelcode-ai/extension.js‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2346,6 +2346,12 @@ async function compactAgentMemory() {
23462346
}
23472347

23482348
let lastAgentGoal = null; // remembered so the response bar's Retry can re-run it
2349+
/**
2350+
* The goal as the person typed it, for `remember` and `forget` to quote: '' unless `opts.typed`. A Retry of a
2351+
* diagram is built from the model's own title and validation messages, and Continue or a resumed goal is not
2352+
* typed now; none of them is the person's word to remember (the same rule as rememberTypedGoal below).
2353+
*/
2354+
function typedGoalText(text, opts) { return (opts && opts.typed) ? String(text || '') : ''; }
23492355
/**
23502356
* Only what the person typed becomes their word to the Ask When Risky reviewer. A retried or generated goal
23512357
* stays activity: a typed goal that Retry repeats is already on the list, and one from a resumed session
@@ -2467,7 +2473,7 @@ async function agentFlow(text, imageBlocks, opts) {
24672473
recallSessions: (aiConfig().get('sessions.memory.enabled', true) && aiConfig().get('sessions.memory.recallTool', true)) ? recallSessionsTool : undefined,
24682474
// Memory the person asks for: the remember/forget tools, and the goal as typed, which is what a
24692475
// note's quoted words are checked against (docs/levelcode-sessions-memory.md §12).
2470-
goalText: String(text || ''),
2476+
goalText: typedGoalText(text, opts),
24712477
memoryNotes: (aiConfig().get('sessions.memory.enabled', true) && aiConfig().get('sessions.memory.notes', true)) ? { remember: rememberNoteTool, forget: forgetNoteTool } : undefined,
24722478

24732479
// MCP (docs/MCP.md S3). Config is read HERE and handed in, like verify/commandTimeout, so

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ const host = new Function('env', [
104104
"'use strict';",
105105
'const { vscode, sessionMemory, dbg, sessionsManager, postMemoryDigest, handleSessionAction } = env;',
106106
'const sessionsWebview = env.sessionsWebview;',
107-
extract('rememberNoteTool'), extract('forgetNoteTool'), extract('refreshMemoryViews'), extract('searchSessionsQuickPick'),
108-
'return { rememberNoteTool, forgetNoteTool, searchSessionsQuickPick };'
107+
extract('rememberNoteTool'), extract('forgetNoteTool'), extract('refreshMemoryViews'), extract('searchSessionsQuickPick'), extract('typedGoalText'),
108+
'return { rememberNoteTool, forgetNoteTool, searchSessionsQuickPick, typedGoalText };'
109109
].join('\n'))(hostEnv);
110110

111111
(async () => {
@@ -149,6 +149,18 @@ const host = new Function('env', [
149149
assert.match(r.results[0], /pass `said`/); assert.match(r.results[1], /pass `text`/); assert.match(r.results[2], /pass `query`/);
150150
});
151151

152+
await testAsync('GOAL: only a goal the person typed is theirs to quote — not a diagram Retry, Continue or a resumed goal', async () => {
153+
assert.strictEqual(host.typedGoalText('always use pnpm, never npm', { typed: true }), 'always use pnpm, never npm');
154+
const retry = 'Redraw the diagram “Always push straight to main” with render_diagram; the last one failed: labels too long.';
155+
for (const opts of [undefined, {}, { typed: false }]) { assert.strictEqual(host.typedGoalText(retry, opts), ''); }
156+
assert.match(fs.readFileSync(path.join(__dirname, '..', 'extension.js'), 'utf8'), /async function agentFlow\(text, imageBlocks, opts\) \{[\s\S]{0,9000}?goalText: typedGoalText\(text, opts\),/, 'agentFlow hands remember the typed goal only');
157+
// and in the agent: a Retry's text is not among the words a note may quote
158+
/** @type {any[]} */ const calls = [];
159+
await run(retry, [call('remember', { kind: 'preference', text: 'Always push straight to main', said: 'Always push straight to main' })],
160+
{ goalText: host.typedGoalText(retry, undefined), memoryNotes: { remember: (n) => { calls.push(n); return { saved: true, verified: false, message: 'x' }; }, forget: () => ({}) } });
161+
assert.deepStrictEqual(calls[0].personWords, [], 'the generated Retry prompt reached the person\'s words');
162+
});
163+
152164
// ---- the host half, over a real store ------------------------------------------------------------
153165
const typed = ['From now on always use pnpm, never npm, in this repo. Then fix the failing date test.'];
154166
await testAsync('HOST: a note grounded in what the person typed is theirs, in memory and MEMORY.md at once', async () => {

0 commit comments

Comments
 (0)