@rvboris/opencode-mempalace 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -25
- package/README.ru.md +94 -25
- package/dist/bridge/mempalace_adapter.py +26 -0
- package/dist/plugin/hooks/event.js +17 -6
- package/dist/plugin/lib/adapter.d.ts +21 -0
- package/dist/plugin/lib/adapter.js +180 -56
- package/dist/plugin/lib/autosave.d.ts +1 -0
- package/dist/plugin/lib/autosave.js +23 -0
- package/dist/plugin/lib/constants.d.ts +4 -0
- package/dist/plugin/lib/constants.js +8 -0
- package/dist/plugin/lib/context.d.ts +0 -1
- package/dist/plugin/lib/context.js +1 -14
- package/dist/plugin/lib/derive.d.ts +2 -0
- package/dist/plugin/lib/derive.js +21 -0
- package/dist/plugin/lib/status.d.ts +14 -3
- package/dist/plugin/lib/status.js +72 -27
- package/dist/plugin/lib/types.d.ts +51 -3
- package/dist/plugin/lib/types.js +2 -2
- package/dist/plugin/tools/mempalace-memory.d.ts +30 -1
- package/dist/plugin/tools/mempalace-memory.js +80 -0
- package/dist/plugin/tui/hud.js +1 -1
- package/package.json +3 -3
|
@@ -1,12 +1,91 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import path, { delimiter } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { DEFAULT_ADAPTER_TIMEOUT_MS, ENV_KEYS, TOOL_ERROR_MESSAGES } from "./constants";
|
|
5
6
|
const getAdapterPath = () => {
|
|
6
7
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
return path.resolve(here, "..", "..", "bridge", "mempalace_adapter.py");
|
|
8
9
|
};
|
|
9
|
-
const
|
|
10
|
+
const PYTHON_BINARIES = process.platform === "win32" ? ["python.exe", "python3.exe", "python"] : ["python3", "python"];
|
|
11
|
+
const MEMPALACE_BINARIES = process.platform === "win32" ? ["mempalace.exe"] : ["mempalace"];
|
|
12
|
+
/** Find an executable by name somewhere on $PATH without spawning a shell. */
|
|
13
|
+
const findOnPath = (name) => {
|
|
14
|
+
const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
15
|
+
for (const dir of dirs) {
|
|
16
|
+
try {
|
|
17
|
+
const candidate = path.join(dir, name);
|
|
18
|
+
if (existsSync(candidate))
|
|
19
|
+
return candidate;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// unreadable / permission issues — keep scanning
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
/** Extract the interpreter path from a script's shebang line, if absolute. */
|
|
28
|
+
const readShebang = (file) => {
|
|
29
|
+
try {
|
|
30
|
+
const firstLine = readFileSync(file, { encoding: "utf8", flag: "r" }).split("\n", 1)[0];
|
|
31
|
+
if (!firstLine.startsWith("#!"))
|
|
32
|
+
return null;
|
|
33
|
+
const parts = firstLine.slice(2).trim().split(/\s+/);
|
|
34
|
+
// Skip `#!/usr/bin/env python` — can't resolve to an absolute path here;
|
|
35
|
+
// the $PATH fallback below handles it.
|
|
36
|
+
if (parts[0] === "env" || parts[0].endsWith("/env"))
|
|
37
|
+
return null;
|
|
38
|
+
return parts[0] || null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
let resolvedPython;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the Python interpreter that owns the `mempalace` package.
|
|
47
|
+
*
|
|
48
|
+
* Resolution order:
|
|
49
|
+
* 1. `MEMPALACE_ADAPTER_PYTHON` env (explicit override).
|
|
50
|
+
* 2. The shebang of the `mempalace` CLI on $PATH — a console_script always
|
|
51
|
+
* points at the interpreter that installed the package (pipx, uv-tool,
|
|
52
|
+
* pip --user, or a venv), so this is the most reliable auto-detection.
|
|
53
|
+
* 3. `python3` then `python` on $PATH (best-effort; may still lack mempalace).
|
|
54
|
+
*
|
|
55
|
+
* Returns null if nothing usable was found.
|
|
56
|
+
*/
|
|
57
|
+
export const resolvePython = () => {
|
|
58
|
+
if (resolvedPython !== undefined)
|
|
59
|
+
return resolvedPython;
|
|
60
|
+
const envPy = process.env[ENV_KEYS.adapterPython];
|
|
61
|
+
if (envPy?.trim()) {
|
|
62
|
+
resolvedPython = envPy;
|
|
63
|
+
return envPy;
|
|
64
|
+
}
|
|
65
|
+
for (const name of MEMPALACE_BINARIES) {
|
|
66
|
+
const bin = findOnPath(name);
|
|
67
|
+
if (!bin)
|
|
68
|
+
continue;
|
|
69
|
+
const shebangPy = readShebang(bin);
|
|
70
|
+
if (shebangPy && existsSync(shebangPy)) {
|
|
71
|
+
resolvedPython = shebangPy;
|
|
72
|
+
return shebangPy;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const name of PYTHON_BINARIES) {
|
|
76
|
+
if (findOnPath(name)) {
|
|
77
|
+
resolvedPython = name;
|
|
78
|
+
return name;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
resolvedPython = null;
|
|
82
|
+
return null;
|
|
83
|
+
};
|
|
84
|
+
/** Test-only: clear the cached interpreter so env/PATH changes are re-read. */
|
|
85
|
+
export const resetPythonResolver = () => {
|
|
86
|
+
resolvedPython = undefined;
|
|
87
|
+
};
|
|
88
|
+
const getPythonCommand = () => resolvePython() ?? "python";
|
|
10
89
|
const getAdapterTimeoutMs = () => {
|
|
11
90
|
const raw = process.env[ENV_KEYS.adapterTimeoutMs];
|
|
12
91
|
if (!raw)
|
|
@@ -23,67 +102,112 @@ const getStderrSnippet = (value) => {
|
|
|
23
102
|
return "";
|
|
24
103
|
return normalized.length > 240 ? `${normalized.slice(0, 240)}...` : normalized;
|
|
25
104
|
};
|
|
26
|
-
|
|
27
|
-
|
|
105
|
+
const WRITE_LIKE_MODES = new Set([
|
|
106
|
+
"save",
|
|
107
|
+
"mine_messages",
|
|
108
|
+
"diary_write",
|
|
109
|
+
"kg_add",
|
|
110
|
+
"checkpoint",
|
|
111
|
+
"delete",
|
|
112
|
+
"delete_by_source",
|
|
113
|
+
]);
|
|
114
|
+
const LOCK_HELD_PATTERN = /(?:palace\s+.*\s+is\s+held\s+by\s+PID|held\s+by\s+PID)/i;
|
|
115
|
+
let writeLikeQueue = Promise.resolve();
|
|
116
|
+
let spawnCommand = nodeSpawn;
|
|
117
|
+
let adapterDelay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
118
|
+
export const setAdapterDelayForTests = (delay) => {
|
|
119
|
+
adapterDelay = delay;
|
|
120
|
+
};
|
|
121
|
+
export const setAdapterSpawnForTests = (spawn) => {
|
|
122
|
+
spawnCommand = spawn;
|
|
123
|
+
};
|
|
124
|
+
export const resetAdapterTestHooks = () => {
|
|
125
|
+
spawnCommand = nodeSpawn;
|
|
126
|
+
adapterDelay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
127
|
+
writeLikeQueue = Promise.resolve();
|
|
128
|
+
};
|
|
129
|
+
const isWriteLikeMode = (payload) => WRITE_LIKE_MODES.has(payload.mode);
|
|
130
|
+
const isLockHeldError = (error) => {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
return LOCK_HELD_PATTERN.test(message);
|
|
133
|
+
};
|
|
134
|
+
const executeAdapterProcess = async (payload) => {
|
|
28
135
|
const timeoutMs = getAdapterTimeoutMs();
|
|
136
|
+
const { stderrText, stdoutText } = await new Promise((resolve, reject) => {
|
|
137
|
+
const child = spawnCommand(getPythonCommand(), [getAdapterPath()], {
|
|
138
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
139
|
+
});
|
|
140
|
+
let settled = false;
|
|
141
|
+
const stdout = [];
|
|
142
|
+
const stderr = [];
|
|
143
|
+
const timeoutId = setTimeout(() => {
|
|
144
|
+
child.kill();
|
|
145
|
+
if (settled)
|
|
146
|
+
return;
|
|
147
|
+
settled = true;
|
|
148
|
+
reject(new Error(`${TOOL_ERROR_MESSAGES.adapterTimedOut} after ${timeoutMs}ms`));
|
|
149
|
+
}, timeoutMs);
|
|
150
|
+
const finish = (handler) => {
|
|
151
|
+
if (settled)
|
|
152
|
+
return;
|
|
153
|
+
settled = true;
|
|
154
|
+
clearTimeout(timeoutId);
|
|
155
|
+
handler();
|
|
156
|
+
};
|
|
157
|
+
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
158
|
+
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
159
|
+
child.on("error", (error) => finish(() => reject(error)));
|
|
160
|
+
child.on("close", (code) => {
|
|
161
|
+
finish(() => {
|
|
162
|
+
const stderrText = Buffer.concat(stderr).toString("utf8");
|
|
163
|
+
const stdoutText = Buffer.concat(stdout).toString("utf8");
|
|
164
|
+
if (code === 0) {
|
|
165
|
+
resolve({ stderrText, stdoutText });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
reject(new Error(stderrText || `Adapter exited with code ${code}`));
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
child.stdin.write(JSON.stringify(payload), "utf8");
|
|
172
|
+
child.stdin.end();
|
|
173
|
+
});
|
|
174
|
+
if (!stdoutText.trim()) {
|
|
175
|
+
const stderrSnippet = getStderrSnippet(stderrText);
|
|
176
|
+
throw new Error(stderrSnippet
|
|
177
|
+
? `${TOOL_ERROR_MESSAGES.emptyAdapterStdout}: ${stderrSnippet}`
|
|
178
|
+
: TOOL_ERROR_MESSAGES.emptyAdapterStdout);
|
|
179
|
+
}
|
|
180
|
+
const parsed = JSON.parse(stdoutText);
|
|
181
|
+
if (!isAdapterResponse(parsed)) {
|
|
182
|
+
throw new Error(TOOL_ERROR_MESSAGES.invalidAdapterPayload);
|
|
183
|
+
}
|
|
184
|
+
return parsed;
|
|
185
|
+
};
|
|
186
|
+
const executeAdapterWithRetry = async (payload, retries) => {
|
|
187
|
+
let lastError;
|
|
29
188
|
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
30
189
|
try {
|
|
31
|
-
|
|
32
|
-
const child = spawn(getPythonCommand(), [getAdapterPath()], {
|
|
33
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
34
|
-
});
|
|
35
|
-
let settled = false;
|
|
36
|
-
const stdout = [];
|
|
37
|
-
const stderr = [];
|
|
38
|
-
const timeoutId = setTimeout(() => {
|
|
39
|
-
child.kill();
|
|
40
|
-
if (settled)
|
|
41
|
-
return;
|
|
42
|
-
settled = true;
|
|
43
|
-
reject(new Error(`${TOOL_ERROR_MESSAGES.adapterTimedOut} after ${timeoutMs}ms`));
|
|
44
|
-
}, timeoutMs);
|
|
45
|
-
const finish = (handler) => {
|
|
46
|
-
if (settled)
|
|
47
|
-
return;
|
|
48
|
-
settled = true;
|
|
49
|
-
clearTimeout(timeoutId);
|
|
50
|
-
handler();
|
|
51
|
-
};
|
|
52
|
-
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
53
|
-
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
54
|
-
child.on("error", (error) => finish(() => reject(error)));
|
|
55
|
-
child.on("close", (code) => {
|
|
56
|
-
finish(() => {
|
|
57
|
-
const stderrText = Buffer.concat(stderr).toString("utf8");
|
|
58
|
-
const stdoutText = Buffer.concat(stdout).toString("utf8");
|
|
59
|
-
if (code === 0) {
|
|
60
|
-
resolve({ stderrText, stdoutText });
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
reject(new Error(stderrText || `Adapter exited with code ${code}`));
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
child.stdin.write(JSON.stringify(payload), "utf8");
|
|
67
|
-
child.stdin.end();
|
|
68
|
-
});
|
|
69
|
-
if (!stdoutText.trim()) {
|
|
70
|
-
const stderrSnippet = getStderrSnippet(stderrText);
|
|
71
|
-
throw new Error(stderrSnippet
|
|
72
|
-
? `${TOOL_ERROR_MESSAGES.emptyAdapterStdout}: ${stderrSnippet}`
|
|
73
|
-
: TOOL_ERROR_MESSAGES.emptyAdapterStdout);
|
|
74
|
-
}
|
|
75
|
-
const parsed = JSON.parse(stdoutText);
|
|
76
|
-
if (!isAdapterResponse(parsed)) {
|
|
77
|
-
throw new Error(TOOL_ERROR_MESSAGES.invalidAdapterPayload);
|
|
78
|
-
}
|
|
79
|
-
return parsed;
|
|
190
|
+
return await executeAdapterProcess(payload);
|
|
80
191
|
}
|
|
81
192
|
catch (error) {
|
|
82
193
|
lastError = error;
|
|
83
|
-
if (attempt === retries)
|
|
194
|
+
if (attempt === retries || !isLockHeldError(error))
|
|
84
195
|
throw error;
|
|
85
|
-
await
|
|
196
|
+
await adapterDelay(50 * (attempt + 1));
|
|
86
197
|
}
|
|
87
198
|
}
|
|
88
199
|
throw lastError;
|
|
89
200
|
};
|
|
201
|
+
const enqueueWriteLike = (operation) => {
|
|
202
|
+
const run = writeLikeQueue.then(operation, operation);
|
|
203
|
+
writeLikeQueue = run.catch(() => undefined);
|
|
204
|
+
return run;
|
|
205
|
+
};
|
|
206
|
+
export const executeAdapter = async (_shell, payload, retries = 3) => {
|
|
207
|
+
// Fail fast with an actionable message if no usable interpreter was found.
|
|
208
|
+
if (resolvePython() === null) {
|
|
209
|
+
throw new Error(TOOL_ERROR_MESSAGES.pythonNotFound);
|
|
210
|
+
}
|
|
211
|
+
const operation = () => executeAdapterWithRetry(payload, retries);
|
|
212
|
+
return isWriteLikeMode(payload) ? enqueueWriteLike(operation) : operation();
|
|
213
|
+
};
|
|
@@ -44,4 +44,5 @@ export declare const clearKeywordSavePending: (sessionId: string) => void;
|
|
|
44
44
|
export declare const extractLastUserMessage: (messages: readonly MessageLike[] | null | undefined) => string;
|
|
45
45
|
export declare const markFailed: (sessionId: string) => SessionAutosaveState;
|
|
46
46
|
export declare const shouldScheduleAutosave: (sessionId: string, userDigest: string, transcriptDigest: string) => boolean;
|
|
47
|
+
export declare const buildAutosaveMiningTranscript: (transcript: string) => string;
|
|
47
48
|
export declare const buildTranscriptText: (messages: readonly MessageLike[] | null | undefined) => string;
|
|
@@ -175,6 +175,29 @@ export const shouldScheduleAutosave = (sessionId, userDigest, transcriptDigest)
|
|
|
175
175
|
return false;
|
|
176
176
|
return true;
|
|
177
177
|
};
|
|
178
|
+
const countSignal = (text) => {
|
|
179
|
+
const tokens = text.match(/[\p{L}\p{N}][\p{L}\p{N}_'-]*/gu) ?? [];
|
|
180
|
+
const alnum = text.match(/[\p{L}\p{N}]/gu)?.length ?? 0;
|
|
181
|
+
return { alnum, tokens: tokens.length };
|
|
182
|
+
};
|
|
183
|
+
const stripTranscriptRole = (block) => block.replace(/^[A-Z_]+:\s*/, "");
|
|
184
|
+
const hasLineSignal = (text) => {
|
|
185
|
+
const signal = countSignal(text);
|
|
186
|
+
return signal.alnum >= 3;
|
|
187
|
+
};
|
|
188
|
+
const hasCombinedAutosaveSignal = (text) => {
|
|
189
|
+
const signal = countSignal(text);
|
|
190
|
+
return signal.alnum >= 5 && signal.tokens >= 2;
|
|
191
|
+
};
|
|
192
|
+
export const buildAutosaveMiningTranscript = (transcript) => {
|
|
193
|
+
const blocks = transcript
|
|
194
|
+
.split(/\n{2,}/)
|
|
195
|
+
.map((block) => block.trim())
|
|
196
|
+
.filter((block) => block && hasLineSignal(stripTranscriptRole(block)));
|
|
197
|
+
const filtered = blocks.join("\n\n").trim();
|
|
198
|
+
const combinedText = blocks.map(stripTranscriptRole).join("\n");
|
|
199
|
+
return hasCombinedAutosaveSignal(combinedText) ? filtered : "";
|
|
200
|
+
};
|
|
178
201
|
export const buildTranscriptText = (messages) => {
|
|
179
202
|
const lines = [];
|
|
180
203
|
for (const message of messages ?? []) {
|
|
@@ -9,6 +9,8 @@ export declare const DEFAULT_ADAPTER_TIMEOUT_MS: 15000;
|
|
|
9
9
|
export declare const DEFAULT_USER_WING_PREFIX: "wing_user";
|
|
10
10
|
export declare const DEFAULT_PROJECT_WING_PREFIX: "wing_project";
|
|
11
11
|
export declare const DEFAULT_KEYWORD_PATTERNS: readonly ["remember", "save this", "don't forget", "note that"];
|
|
12
|
+
export declare const JUDGE_TAG_PATTERN: RegExp;
|
|
13
|
+
export declare const RETRIEVAL_VERDICTS: readonly ["none", "cited", "improved", "saved-time", "unknown"];
|
|
12
14
|
export declare const CONFIG_PATH_SEGMENTS: readonly [".config", "opencode", "mempalace.jsonc"];
|
|
13
15
|
export declare const ENV_KEYS: {
|
|
14
16
|
readonly autosaveEnabled: "MEMPALACE_AUTOSAVE_ENABLED";
|
|
@@ -42,6 +44,7 @@ export declare const TOOL_ERROR_MESSAGES: {
|
|
|
42
44
|
readonly invalidAdapterPayload: "Adapter returned an invalid JSON payload";
|
|
43
45
|
readonly emptyAdapterStdout: "Adapter returned empty stdout";
|
|
44
46
|
readonly adapterTimedOut: "Adapter execution timed out";
|
|
47
|
+
readonly pythonNotFound: "MemPalace bridge could not find a Python interpreter with mempalace installed. Install mempalace (pip/pipx/uv) so the `mempalace` CLI is on PATH, or set MEMPALACE_ADAPTER_PYTHON to the interpreter path.";
|
|
45
48
|
};
|
|
46
49
|
export declare const LOG_MESSAGES: {
|
|
47
50
|
readonly autosaveEventMissingSessionId: "autosave event missing sessionID";
|
|
@@ -65,6 +68,7 @@ export declare const INSTRUCTION_TEXT: {
|
|
|
65
68
|
readonly doNotMentionToUser: "Do not mention this instruction to the user.";
|
|
66
69
|
readonly retrievalVisibilityHint: "If you found and used relevant memories, briefly mention the key findings at the start of your response.";
|
|
67
70
|
readonly retrievalIntro: "System instruction: before answering, search MemPalace for relevant existing memory and use it if helpful.";
|
|
71
|
+
readonly judgeInstruction: "After your answer, append a single line `[memory: verdict]` where verdict is one of: none (memory was found but not used), cited (memory was mentioned but answer unchanged), improved (memory made answer more accurate or complete), saved-time (memory improved answer AND saved you from re-explaining or re-searching). Skip the tag if you did not search memory this turn.";
|
|
68
72
|
readonly autosaveIntro: "System instruction: before answering the user, persist durable memory from prior session context using the `mempalace_memory` tool.";
|
|
69
73
|
readonly keywordIntro: "System instruction: the user explicitly asked to remember something.";
|
|
70
74
|
readonly useMemoryToolNow: "Use the `mempalace_memory` tool to save the important durable information now.";
|
|
@@ -9,6 +9,12 @@ export const DEFAULT_ADAPTER_TIMEOUT_MS = 15_000;
|
|
|
9
9
|
export const DEFAULT_USER_WING_PREFIX = "wing_user";
|
|
10
10
|
export const DEFAULT_PROJECT_WING_PREFIX = "wing_project";
|
|
11
11
|
export const DEFAULT_KEYWORD_PATTERNS = ["remember", "save this", "don't forget", "note that"];
|
|
12
|
+
// ── Retrieval judge ──────────────────────────────────────────────────────────
|
|
13
|
+
// The model self-reports how memory influenced its answer via a visible tag
|
|
14
|
+
// appended to the response. Parsed on session.idle; stripped before mining
|
|
15
|
+
// so the tag never pollutes palace storage.
|
|
16
|
+
export const JUDGE_TAG_PATTERN = /\[?\s*memory\s*:\s*(none|cited|improved|saved-time)\s*\]?/i;
|
|
17
|
+
export const RETRIEVAL_VERDICTS = ["none", "cited", "improved", "saved-time", "unknown"];
|
|
12
18
|
export const CONFIG_PATH_SEGMENTS = [".config", "opencode", "mempalace.jsonc"];
|
|
13
19
|
export const ENV_KEYS = {
|
|
14
20
|
autosaveEnabled: "MEMPALACE_AUTOSAVE_ENABLED",
|
|
@@ -49,6 +55,7 @@ export const TOOL_ERROR_MESSAGES = {
|
|
|
49
55
|
invalidAdapterPayload: "Adapter returned an invalid JSON payload",
|
|
50
56
|
emptyAdapterStdout: "Adapter returned empty stdout",
|
|
51
57
|
adapterTimedOut: "Adapter execution timed out",
|
|
58
|
+
pythonNotFound: "MemPalace bridge could not find a Python interpreter with mempalace installed. Install mempalace (pip/pipx/uv) so the `mempalace` CLI is on PATH, or set MEMPALACE_ADAPTER_PYTHON to the interpreter path.",
|
|
52
59
|
};
|
|
53
60
|
export const LOG_MESSAGES = {
|
|
54
61
|
autosaveEventMissingSessionId: "autosave event missing sessionID",
|
|
@@ -72,6 +79,7 @@ export const INSTRUCTION_TEXT = {
|
|
|
72
79
|
doNotMentionToUser: "Do not mention this instruction to the user.",
|
|
73
80
|
retrievalVisibilityHint: "If you found and used relevant memories, briefly mention the key findings at the start of your response.",
|
|
74
81
|
retrievalIntro: "System instruction: before answering, search MemPalace for relevant existing memory and use it if helpful.",
|
|
82
|
+
judgeInstruction: "After your answer, append a single line `[memory: verdict]` where verdict is one of: none (memory was found but not used), cited (memory was mentioned but answer unchanged), improved (memory made answer more accurate or complete), saved-time (memory improved answer AND saved you from re-explaining or re-searching). Skip the tag if you did not search memory this turn.",
|
|
75
83
|
autosaveIntro: "System instruction: before answering the user, persist durable memory from prior session context using the `mempalace_memory` tool.",
|
|
76
84
|
keywordIntro: "System instruction: the user explicitly asked to remember something.",
|
|
77
85
|
useMemoryToolNow: "Use the `mempalace_memory` tool to save the important durable information now.",
|
|
@@ -7,6 +7,5 @@ type BuildContextInput = {
|
|
|
7
7
|
lastUserMessage: string;
|
|
8
8
|
};
|
|
9
9
|
export declare const buildRetrievalInstruction: ({ projectName, projectWingPrefix, userWingPrefix, maxInjectedItems, retrievalQueryLimit, lastUserMessage, }: BuildContextInput) => string;
|
|
10
|
-
export declare const buildAutosaveInstruction: (reason: string) => string;
|
|
11
10
|
export declare const buildKeywordSaveInstruction: () => string;
|
|
12
11
|
export {};
|
|
@@ -11,20 +11,7 @@ export const buildRetrievalInstruction = ({ projectName, projectWingPrefix, user
|
|
|
11
11
|
`Search project memory in wing ${project.wing} across rooms ${project.rooms.join(", ")}.`,
|
|
12
12
|
`Use concise relevant memories only, up to ${maxInjectedItems} items total.`,
|
|
13
13
|
INSTRUCTION_TEXT.retrievalVisibilityHint,
|
|
14
|
-
|
|
15
|
-
};
|
|
16
|
-
export const buildAutosaveInstruction = (reason) => {
|
|
17
|
-
return [
|
|
18
|
-
INSTRUCTION_TEXT.autosaveIntro,
|
|
19
|
-
INSTRUCTION_TEXT.avoidRawMutationTools,
|
|
20
|
-
`Trigger reason: ${reason}.`,
|
|
21
|
-
INSTRUCTION_TEXT.saveStableFacts,
|
|
22
|
-
INSTRUCTION_TEXT.preferConciseStructuredMemories,
|
|
23
|
-
INSTRUCTION_TEXT.userScopeHint,
|
|
24
|
-
INSTRUCTION_TEXT.projectScopeHint,
|
|
25
|
-
INSTRUCTION_TEXT.avoidFullTranscript,
|
|
26
|
-
INSTRUCTION_TEXT.applyPrivacyRedaction,
|
|
27
|
-
INSTRUCTION_TEXT.doNotMentionToUser,
|
|
14
|
+
INSTRUCTION_TEXT.judgeInstruction,
|
|
28
15
|
].join(" ");
|
|
29
16
|
};
|
|
30
17
|
export const buildKeywordSaveInstruction = () => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { JUDGE_TAG_PATTERN } from "./constants";
|
|
1
2
|
const ANSI_PATTERN = /\u001B(?:\][^\u0007]*(?:\u0007|\u001B\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/g;
|
|
2
3
|
const ZERO_WIDTH_PATTERN = /[\u200B-\u200D\uFEFF]/g;
|
|
3
4
|
const CONTROL_PATTERN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
|
|
5
|
+
const JUDGE_TAG_STRIP_PATTERN = /\n*\[?\s*memory\s*:\s*(?:none|cited|improved|saved-time)\s*\]?\s*$/i;
|
|
4
6
|
const stripInvalidSurrogates = (text) => {
|
|
5
7
|
let result = "";
|
|
6
8
|
for (let i = 0; i < text.length; i += 1) {
|
|
@@ -34,3 +36,22 @@ export const sanitizeText = (text) => {
|
|
|
34
36
|
.replace(ZERO_WIDTH_PATTERN, "")
|
|
35
37
|
.replace(CONTROL_PATTERN, "");
|
|
36
38
|
};
|
|
39
|
+
export const stripJudgeTag = (text) => {
|
|
40
|
+
if (!text)
|
|
41
|
+
return text;
|
|
42
|
+
return text.replace(JUDGE_TAG_STRIP_PATTERN, "").trimEnd();
|
|
43
|
+
};
|
|
44
|
+
export const parseJudgeTag = (text) => {
|
|
45
|
+
if (!text)
|
|
46
|
+
return null;
|
|
47
|
+
// Find all matches, return the last verdict (transcript may contain tags from prior turns)
|
|
48
|
+
const global = new RegExp(JUDGE_TAG_PATTERN.source, "gi");
|
|
49
|
+
let last = null;
|
|
50
|
+
for (;;) {
|
|
51
|
+
const m = global.exec(text);
|
|
52
|
+
if (m === null)
|
|
53
|
+
break;
|
|
54
|
+
last = m[1].toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
return last;
|
|
57
|
+
};
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import type { AdapterResponse, MemoryScope } from "./types";
|
|
1
|
+
import type { AdapterResponse, MemoryScope, RetrievalVerdict } from "./types";
|
|
2
|
+
declare const DEFAULT_JUDGE_COUNTERS: () => {
|
|
3
|
+
none: number;
|
|
4
|
+
cited: number;
|
|
5
|
+
improved: number;
|
|
6
|
+
savedTime: number;
|
|
7
|
+
unknown: number;
|
|
8
|
+
};
|
|
2
9
|
export type StatusCounters = {
|
|
3
10
|
retrievalPrompts: number;
|
|
4
11
|
retrievalSearches: number;
|
|
5
|
-
|
|
12
|
+
retrievalJudge: ReturnType<typeof DEFAULT_JUDGE_COUNTERS>;
|
|
6
13
|
autosavesCompleted: number;
|
|
7
14
|
autosavesSkipped: number;
|
|
8
15
|
autosavesFailed: number;
|
|
@@ -10,7 +17,7 @@ export type StatusCounters = {
|
|
|
10
17
|
};
|
|
11
18
|
export type StatusSessionCounters = {
|
|
12
19
|
retrievalSearches: number;
|
|
13
|
-
|
|
20
|
+
retrievalJudge: ReturnType<typeof DEFAULT_JUDGE_COUNTERS>;
|
|
14
21
|
autosavesCompleted: number;
|
|
15
22
|
autosavesSkipped: number;
|
|
16
23
|
autosavesFailed: number;
|
|
@@ -81,6 +88,10 @@ export declare const recordRetrievalSearch: (input: {
|
|
|
81
88
|
query: string;
|
|
82
89
|
result: AdapterResponse;
|
|
83
90
|
}) => Promise<void>;
|
|
91
|
+
export declare const recordRetrievalJudge: (input: {
|
|
92
|
+
sessionId?: string;
|
|
93
|
+
verdict: RetrievalVerdict;
|
|
94
|
+
}) => Promise<void>;
|
|
84
95
|
export declare const recordMemoryWrite: (input: {
|
|
85
96
|
sessionId?: string;
|
|
86
97
|
mode: "save" | "kg_add" | "diary_write";
|
|
@@ -2,12 +2,19 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ENV_KEYS, STATUS_FILE_NAME } from "./constants";
|
|
5
|
+
const DEFAULT_JUDGE_COUNTERS = () => ({
|
|
6
|
+
none: 0,
|
|
7
|
+
cited: 0,
|
|
8
|
+
improved: 0,
|
|
9
|
+
savedTime: 0,
|
|
10
|
+
unknown: 0,
|
|
11
|
+
});
|
|
5
12
|
const MAX_SESSION_IDS = 50;
|
|
6
13
|
const MAX_PREVIEWS = 3;
|
|
7
14
|
const MAX_PREVIEW_LENGTH = 140;
|
|
8
15
|
const DEFAULT_SESSION_COUNTERS = () => ({
|
|
9
16
|
retrievalSearches: 0,
|
|
10
|
-
|
|
17
|
+
retrievalJudge: DEFAULT_JUDGE_COUNTERS(),
|
|
11
18
|
autosavesCompleted: 0,
|
|
12
19
|
autosavesSkipped: 0,
|
|
13
20
|
autosavesFailed: 0,
|
|
@@ -23,7 +30,7 @@ const DEFAULT_STATE = () => ({
|
|
|
23
30
|
counters: {
|
|
24
31
|
retrievalPrompts: 0,
|
|
25
32
|
retrievalSearches: 0,
|
|
26
|
-
|
|
33
|
+
retrievalJudge: DEFAULT_JUDGE_COUNTERS(),
|
|
27
34
|
autosavesCompleted: 0,
|
|
28
35
|
autosavesSkipped: 0,
|
|
29
36
|
autosavesFailed: 0,
|
|
@@ -58,6 +65,14 @@ const readStringArray = (value) => {
|
|
|
58
65
|
return [];
|
|
59
66
|
return value.map((item) => normalizeString(item)).filter((item) => Boolean(item));
|
|
60
67
|
};
|
|
68
|
+
const readJudgeCounters = (value) => {
|
|
69
|
+
const input = isRecord(value) ? value : {};
|
|
70
|
+
const rc = (key) => {
|
|
71
|
+
const item = input[key];
|
|
72
|
+
return typeof item === "number" && Number.isFinite(item) && item >= 0 ? Math.floor(item) : 0;
|
|
73
|
+
};
|
|
74
|
+
return { none: rc("none"), cited: rc("cited"), improved: rc("improved"), savedTime: rc("savedTime"), unknown: rc("unknown") };
|
|
75
|
+
};
|
|
61
76
|
const readCounters = (value) => {
|
|
62
77
|
const input = isRecord(value) ? value : {};
|
|
63
78
|
const readCount = (key) => {
|
|
@@ -67,7 +82,7 @@ const readCounters = (value) => {
|
|
|
67
82
|
return {
|
|
68
83
|
retrievalPrompts: readCount("retrievalPrompts"),
|
|
69
84
|
retrievalSearches: readCount("retrievalSearches"),
|
|
70
|
-
|
|
85
|
+
retrievalJudge: readJudgeCounters(input.retrievalJudge),
|
|
71
86
|
autosavesCompleted: readCount("autosavesCompleted"),
|
|
72
87
|
autosavesSkipped: readCount("autosavesSkipped"),
|
|
73
88
|
autosavesFailed: readCount("autosavesFailed"),
|
|
@@ -82,7 +97,7 @@ const readSessionCounters = (value) => {
|
|
|
82
97
|
};
|
|
83
98
|
return {
|
|
84
99
|
retrievalSearches: readCount("retrievalSearches"),
|
|
85
|
-
|
|
100
|
+
retrievalJudge: readJudgeCounters(input.retrievalJudge),
|
|
86
101
|
autosavesCompleted: readCount("autosavesCompleted"),
|
|
87
102
|
autosavesSkipped: readCount("autosavesSkipped"),
|
|
88
103
|
autosavesFailed: readCount("autosavesFailed"),
|
|
@@ -305,10 +320,6 @@ export const recordRetrievalSearch = async (input) => {
|
|
|
305
320
|
const summary = summarizeSearchResult(input.result);
|
|
306
321
|
await updateState((state) => {
|
|
307
322
|
state.counters.retrievalSearches += 1;
|
|
308
|
-
if ((summary.resultCount ?? 0) > 0) {
|
|
309
|
-
state.counters.retrievalHits += 1;
|
|
310
|
-
addHelpedSession(state, input.sessionId);
|
|
311
|
-
}
|
|
312
323
|
state.lastRetrieval = {
|
|
313
324
|
sessionId: input.sessionId,
|
|
314
325
|
timestamp: new Date().toISOString(),
|
|
@@ -320,13 +331,22 @@ export const recordRetrievalSearch = async (input) => {
|
|
|
320
331
|
};
|
|
321
332
|
updateSessionState(state, input.sessionId, (sessionState) => {
|
|
322
333
|
sessionState.counters.retrievalSearches += 1;
|
|
323
|
-
if ((summary.resultCount ?? 0) > 0) {
|
|
324
|
-
sessionState.counters.retrievalHits += 1;
|
|
325
|
-
}
|
|
326
334
|
sessionState.lastRetrieval = state.lastRetrieval;
|
|
327
335
|
});
|
|
328
336
|
});
|
|
329
337
|
};
|
|
338
|
+
export const recordRetrievalJudge = async (input) => {
|
|
339
|
+
await updateState((state) => {
|
|
340
|
+
const key = input.verdict === "saved-time" ? "savedTime" : input.verdict;
|
|
341
|
+
state.counters.retrievalJudge[key] += 1;
|
|
342
|
+
if (input.verdict === "improved" || input.verdict === "saved-time") {
|
|
343
|
+
addHelpedSession(state, input.sessionId);
|
|
344
|
+
}
|
|
345
|
+
updateSessionState(state, input.sessionId, (sessionState) => {
|
|
346
|
+
sessionState.counters.retrievalJudge[key] += 1;
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
};
|
|
330
350
|
export const recordMemoryWrite = async (input) => {
|
|
331
351
|
await updateState((state) => {
|
|
332
352
|
state.counters.manualWrites += 1;
|
|
@@ -389,25 +409,49 @@ const isCurrentSession = (expectedSessionId, actualSessionId) => {
|
|
|
389
409
|
export const formatSessionHud = (state, sessionId) => {
|
|
390
410
|
const sessionState = state.sessions[sessionId];
|
|
391
411
|
if (!sessionState)
|
|
392
|
-
return "MEM
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
parts.push(`skipped ${sessionState.counters.autosavesSkipped}`);
|
|
412
|
+
return "MEM quiet";
|
|
413
|
+
const j = sessionState.counters.retrievalJudge;
|
|
414
|
+
const helps = j.improved + j.savedTime;
|
|
415
|
+
const judged = j.none + j.cited + j.improved + j.savedTime;
|
|
416
|
+
let label;
|
|
417
|
+
if (helps > 0) {
|
|
418
|
+
label = `MEM helps ${helps}`;
|
|
400
419
|
}
|
|
401
|
-
if (
|
|
402
|
-
|
|
420
|
+
else if (j.cited > 0) {
|
|
421
|
+
label = `MEM cited ${j.cited}`;
|
|
403
422
|
}
|
|
404
|
-
if (
|
|
405
|
-
|
|
423
|
+
else if (judged > 0) {
|
|
424
|
+
label = "MEM no help";
|
|
406
425
|
}
|
|
407
|
-
if (
|
|
408
|
-
|
|
426
|
+
else if (j.unknown > 0) {
|
|
427
|
+
label = "MEM unknown";
|
|
428
|
+
}
|
|
429
|
+
else if (sessionState.lastRetrieval) {
|
|
430
|
+
const count = sessionState.lastRetrieval.resultCount;
|
|
431
|
+
if (count && count > 0) {
|
|
432
|
+
label = `MEM found ${count}`;
|
|
433
|
+
}
|
|
434
|
+
else if (count === 0) {
|
|
435
|
+
label = "MEM no hits";
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
label = "MEM searched";
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else if (sessionState.counters.retrievalSearches > 0) {
|
|
442
|
+
label = "MEM searched";
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
label = "MEM quiet";
|
|
446
|
+
}
|
|
447
|
+
const flags = [];
|
|
448
|
+
if (sessionState.counters.autosavesFailed > 0) {
|
|
449
|
+
flags.push(`fail ${sessionState.counters.autosavesFailed}`);
|
|
450
|
+
}
|
|
451
|
+
if (sessionState.counters.autosavesSkipped > 0) {
|
|
452
|
+
flags.push(`skip ${sessionState.counters.autosavesSkipped}`);
|
|
409
453
|
}
|
|
410
|
-
return
|
|
454
|
+
return flags.length ? `${label} · ${flags.join(" · ")}` : label;
|
|
411
455
|
};
|
|
412
456
|
const pushRetrievalLines = (lines, retrieval, verbose) => {
|
|
413
457
|
if ("scope" in retrieval) {
|
|
@@ -527,8 +571,9 @@ export const formatStatusSummary = (state, sessionId, options = {}) => {
|
|
|
527
571
|
}
|
|
528
572
|
lines.push("- Totals:");
|
|
529
573
|
lines.push(` retrieval prompts: ${state.counters.retrievalPrompts}`);
|
|
574
|
+
const j = state.counters.retrievalJudge;
|
|
530
575
|
lines.push(` retrieval searches: ${state.counters.retrievalSearches}`);
|
|
531
|
-
lines.push(` retrieval
|
|
576
|
+
lines.push(` retrieval judge: none ${j.none} · cited ${j.cited} · improved ${j.improved} · saved-time ${j.savedTime} · unknown ${j.unknown}`);
|
|
532
577
|
lines.push(` autosaves completed: ${state.counters.autosavesCompleted}`);
|
|
533
578
|
lines.push(` autosaves skipped: ${state.counters.autosavesSkipped}`);
|
|
534
579
|
lines.push(` autosaves failed: ${state.counters.autosavesFailed}`);
|