pi-hermes-memory 0.7.23 → 0.8.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 +6 -5
- package/package.json +2 -2
- package/src/config.ts +6 -0
- package/src/constants.ts +53 -11
- package/src/extension-root-migration.ts +429 -5
- package/src/handlers/auto-consolidate.ts +118 -8
- package/src/handlers/background-review.ts +29 -23
- package/src/handlers/child-process-watchdog.mjs +90 -0
- package/src/handlers/correction-detector.ts +52 -36
- package/src/handlers/pi-child-process.ts +270 -37
- package/src/handlers/review-memory-ops.ts +18 -116
- package/src/handlers/session-flush.ts +71 -4
- package/src/handlers/sync-markdown-memories.ts +143 -51
- package/src/index.ts +32 -16
- package/src/store/atomic-lock-coordinator.ts +252 -0
- package/src/store/canonical-storage-path.ts +54 -0
- package/src/store/db.ts +244 -9
- package/src/store/markdown-mutation-lock.ts +38 -0
- package/src/store/memory-store.ts +455 -57
- package/src/store/sqlite-memory-store.ts +121 -4
- package/src/tools/memory-tool.ts +48 -9
- package/src/tools/session-search-tool.ts +70 -12
- package/src/types.ts +2 -0
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
2
4
|
import { dirname, join, resolve } from "node:path";
|
|
3
5
|
import { fileURLToPath } from "node:url";
|
|
4
6
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
7
|
import type { MemoryConfig, ThinkingLevel } from "../types.js";
|
|
8
|
+
import { AGENT_ROOT } from "../paths.js";
|
|
6
9
|
|
|
7
|
-
type ChildLlmConfig = Pick<MemoryConfig, "llmModelOverride" | "llmThinkingOverride">;
|
|
10
|
+
type ChildLlmConfig = Pick<MemoryConfig, "llmModelOverride" | "llmThinkingOverride" | "childExtensionPaths">;
|
|
8
11
|
|
|
9
12
|
interface PiExecResult {
|
|
10
13
|
code: number;
|
|
11
14
|
stdout?: string;
|
|
12
15
|
stderr?: string;
|
|
16
|
+
killed?: boolean;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
interface ExecChildPromptOptions {
|
|
@@ -18,6 +22,20 @@ interface ExecChildPromptOptions {
|
|
|
18
22
|
retryWithoutOverrides?: boolean;
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
interface ExecChildPromptDependencies {
|
|
26
|
+
removeTemporaryDirectory: (dir: string) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_EXEC_CHILD_PROMPT_DEPENDENCIES: ExecChildPromptDependencies = {
|
|
30
|
+
removeTemporaryDirectory: async (dir) => {
|
|
31
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const WATCHDOG_EXIT_GRACE_MS = 5000;
|
|
36
|
+
const CHILD_PROCESS_WATCHDOG_PATH = fileURLToPath(
|
|
37
|
+
new URL("./child-process-watchdog.mjs", import.meta.url),
|
|
38
|
+
);
|
|
21
39
|
export interface ChildPiInvocation {
|
|
22
40
|
command: string;
|
|
23
41
|
args: string[];
|
|
@@ -80,34 +98,158 @@ export function inheritedExtensionArgs(argv: string[] = process.argv.slice(2)):
|
|
|
80
98
|
return args;
|
|
81
99
|
}
|
|
82
100
|
|
|
83
|
-
|
|
101
|
+
// Provider-auth-adapter packages (e.g. Anthropic/xAI/Codex OAuth) inject
|
|
102
|
+
// subscription billing headers via pi.registerProvider(). --no-extensions
|
|
103
|
+
// strips these from child `pi -p` subprocesses, which silently rebills
|
|
104
|
+
// subscription usage as pay-as-you-go "extra usage" instead (see issue #94).
|
|
105
|
+
//
|
|
106
|
+
// pi has no runtime API to enumerate loaded extensions or map a registered
|
|
107
|
+
// provider back to its extension file, so we can't ask pi "what adapter is
|
|
108
|
+
// active" directly. Instead we mirror pi's OWN static package-discovery
|
|
109
|
+
// convention (package.json -> "pi": { "extensions": [...] }, the same field
|
|
110
|
+
// pi-hermes-memory's own package.json declares) and match sibling package
|
|
111
|
+
// names against a naming convention, so a future xai-oauth-adapter or
|
|
112
|
+
// pi-codex-oauth-adapter is picked up automatically without a code change
|
|
113
|
+
// here — no code execution, just JSON reads of sibling package.json files.
|
|
114
|
+
const AUTH_ADAPTER_NAME_PATTERNS: readonly RegExp[] = [
|
|
115
|
+
/(^|[-/])oauth-adapter$/,
|
|
116
|
+
/(^|[-/])auth-adapter$/,
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
function isAuthAdapterPackageName(name: string): boolean {
|
|
120
|
+
return AUTH_ADAPTER_NAME_PATTERNS.some((pattern) => pattern.test(name));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Read a sibling package's "pi": { "extensions": [...] } manifest field —
|
|
124
|
+
// the same field pi's own loader reads — and resolve declared paths
|
|
125
|
+
// relative to that package's directory. Mirrors loader.js#resolveExtensionEntries.
|
|
126
|
+
function readPackageExtensionEntries(packageDir: string): string[] {
|
|
127
|
+
const packageJsonPath = join(packageDir, "package.json");
|
|
128
|
+
if (!existsSync(packageJsonPath)) return [];
|
|
129
|
+
|
|
130
|
+
let manifest: unknown;
|
|
131
|
+
try {
|
|
132
|
+
manifest = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
133
|
+
} catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const declaredExtensions = (manifest as { pi?: { extensions?: unknown } } | null)?.pi?.extensions;
|
|
138
|
+
if (!Array.isArray(declaredExtensions)) return [];
|
|
139
|
+
|
|
140
|
+
const entries: string[] = [];
|
|
141
|
+
for (const relativePath of declaredExtensions) {
|
|
142
|
+
if (typeof relativePath !== "string") continue;
|
|
143
|
+
const resolved = resolve(packageDir, relativePath);
|
|
144
|
+
if (existsSync(resolved)) entries.push(resolved);
|
|
145
|
+
}
|
|
146
|
+
return entries;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function scanRootForAuthAdapters(root: string): string[] {
|
|
150
|
+
let entries: string[];
|
|
151
|
+
try {
|
|
152
|
+
entries = readdirSync(root);
|
|
153
|
+
} catch {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const detected: string[] = [];
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
if (entry.startsWith("@")) {
|
|
160
|
+
// Scoped org, e.g. @xai/pi-oauth-adapter — one extra level, no deeper.
|
|
161
|
+
const scopeDir = join(root, entry);
|
|
162
|
+
let scopedPackages: string[];
|
|
163
|
+
try {
|
|
164
|
+
scopedPackages = readdirSync(scopeDir);
|
|
165
|
+
} catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
for (const scopedName of scopedPackages) {
|
|
169
|
+
if (!isAuthAdapterPackageName(scopedName)) continue;
|
|
170
|
+
detected.push(...readPackageExtensionEntries(join(scopeDir, scopedName)));
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!isAuthAdapterPackageName(entry)) continue;
|
|
176
|
+
detected.push(...readPackageExtensionEntries(join(root, entry)));
|
|
177
|
+
}
|
|
178
|
+
return detected;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// `roots` is overridable so tests can point this at fixture directories
|
|
182
|
+
// instead of the real sibling-packages trees. Two roots are scanned by
|
|
183
|
+
// default: packages installed alongside pi-hermes-memory's own package
|
|
184
|
+
// (the common npm-managed-extensions layout), and AGENT_ROOT's npm
|
|
185
|
+
// directory (covers pi-hermes-memory being loaded from elsewhere, e.g. a
|
|
186
|
+
// local dev checkout via -e, while the adapter is still npm-managed).
|
|
187
|
+
export function detectAuthAdapterExtensionPaths(roots?: string[]): string[] {
|
|
188
|
+
const searchRoots = roots ?? [
|
|
189
|
+
OWN_EXTENSION_PATH ? resolve(dirname(dirname(OWN_EXTENSION_PATH)), "..") : "",
|
|
190
|
+
join(AGENT_ROOT, "npm", "node_modules"),
|
|
191
|
+
].filter((root) => root.length > 0);
|
|
192
|
+
|
|
193
|
+
const seenRoots: string[] = [];
|
|
194
|
+
const detected: string[] = [];
|
|
195
|
+
for (const root of searchRoots) {
|
|
196
|
+
if (seenRoots.includes(root)) continue;
|
|
197
|
+
seenRoots.push(root);
|
|
198
|
+
detected.push(...scanRootForAuthAdapters(root));
|
|
199
|
+
}
|
|
200
|
+
return detected;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function childExtensionPaths(config: ChildLlmConfig): string[] {
|
|
204
|
+
const candidates = [
|
|
205
|
+
OWN_EXTENSION_PATH,
|
|
206
|
+
...(config.childExtensionPaths ?? []),
|
|
207
|
+
...detectAuthAdapterExtensionPaths(),
|
|
208
|
+
];
|
|
209
|
+
const seen = new Set<string>();
|
|
210
|
+
const paths: string[] = [];
|
|
211
|
+
for (const candidate of candidates) {
|
|
212
|
+
const trimmed = candidate?.trim();
|
|
213
|
+
if (!trimmed) continue;
|
|
214
|
+
const normalized = resolve(trimmed);
|
|
215
|
+
if (seen.has(normalized) || !existsSync(normalized)) continue;
|
|
216
|
+
seen.add(normalized);
|
|
217
|
+
paths.push(normalized);
|
|
218
|
+
}
|
|
219
|
+
return paths;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function appendOwnExtensionArgs(args: string[], config: ChildLlmConfig): void {
|
|
84
223
|
// Skip all packages from settings.json (--no-extensions) — the subprocess
|
|
85
|
-
// only
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
args.push("--no-extensions", "-e", OWN_EXTENSION_PATH);
|
|
224
|
+
// loads only Hermes and explicitly required provider adapters.
|
|
225
|
+
args.push("--no-extensions");
|
|
226
|
+
for (const extensionPath of childExtensionPaths(config)) {
|
|
227
|
+
args.push("-e", extensionPath);
|
|
90
228
|
}
|
|
91
229
|
}
|
|
92
230
|
|
|
93
|
-
export function buildChildPiPromptArgs(
|
|
231
|
+
export function buildChildPiPromptArgs(
|
|
232
|
+
prompt: string,
|
|
233
|
+
config: ChildLlmConfig,
|
|
234
|
+
_argv: string[] = process.argv.slice(2),
|
|
235
|
+
): string[] {
|
|
94
236
|
const args = ["-p", "--no-session"];
|
|
95
237
|
const model = normalizedModelOverride(config);
|
|
96
238
|
const thinking = effectiveThinkingOverride(config);
|
|
97
239
|
|
|
98
240
|
if (model) args.push("--model", model);
|
|
99
241
|
if (thinking) args.push("--thinking", thinking);
|
|
100
|
-
appendOwnExtensionArgs(args);
|
|
242
|
+
appendOwnExtensionArgs(args, config);
|
|
101
243
|
args.push(prompt);
|
|
102
244
|
|
|
103
245
|
return args;
|
|
104
246
|
}
|
|
105
247
|
|
|
106
|
-
function basePromptArgs(prompt: string): string[] {
|
|
248
|
+
function basePromptArgs(prompt: string, config: ChildLlmConfig): string[] {
|
|
107
249
|
// Always use --no-extensions + own path so the retry also avoids loading
|
|
108
250
|
// all settings.json packages — matching the primary code path.
|
|
109
251
|
const args = ["-p", "--no-session"];
|
|
110
|
-
appendOwnExtensionArgs(args);
|
|
252
|
+
appendOwnExtensionArgs(args, config);
|
|
111
253
|
args.push(prompt);
|
|
112
254
|
return args;
|
|
113
255
|
}
|
|
@@ -142,6 +284,40 @@ function resolvedPiCliPath(options: ResolveChildPiInvocationOptions): string | u
|
|
|
142
284
|
return resolvedInstalledPiCliPath();
|
|
143
285
|
}
|
|
144
286
|
|
|
287
|
+
function resolvedWindowsPiInvocation(
|
|
288
|
+
args: string[],
|
|
289
|
+
execPath: string,
|
|
290
|
+
): ChildPiInvocation | undefined {
|
|
291
|
+
const pathEntries = (process.env.PATH ?? process.env.Path ?? "")
|
|
292
|
+
.split(";")
|
|
293
|
+
.map((entry) => entry.trim().replace(/^"|"$/g, ""))
|
|
294
|
+
.filter(Boolean);
|
|
295
|
+
|
|
296
|
+
for (const directory of pathEntries) {
|
|
297
|
+
for (const executableName of ["pi.exe", "pi.com"]) {
|
|
298
|
+
const executablePath = join(directory, executableName);
|
|
299
|
+
if (existsSync(executablePath)) {
|
|
300
|
+
return { command: executablePath, args };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!existsSync(join(directory, "pi.cmd")) && !existsSync(join(directory, "pi.bat"))) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const cliPath of [
|
|
309
|
+
join(directory, "node_modules", "@earendil-works", "pi-coding-agent", "dist", "cli.js"),
|
|
310
|
+
join(directory, "node_modules", "@earendil-works", "pi-coding-agent", "cli.js"),
|
|
311
|
+
]) {
|
|
312
|
+
if (existsSync(cliPath)) {
|
|
313
|
+
return { command: execPath, args: [cliPath, ...args] };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
145
321
|
export function resolveChildPiInvocation(
|
|
146
322
|
args: string[],
|
|
147
323
|
options: ResolveChildPiInvocationOptions = {},
|
|
@@ -152,13 +328,33 @@ export function resolveChildPiInvocation(
|
|
|
152
328
|
}
|
|
153
329
|
|
|
154
330
|
const piCliPath = resolvedPiCliPath(options);
|
|
155
|
-
if (
|
|
156
|
-
return {
|
|
331
|
+
if (piCliPath) {
|
|
332
|
+
return {
|
|
333
|
+
command: options.execPath ?? process.execPath,
|
|
334
|
+
args: [piCliPath, ...args],
|
|
335
|
+
};
|
|
157
336
|
}
|
|
158
337
|
|
|
338
|
+
const fallback = resolvedWindowsPiInvocation(args, options.execPath ?? process.execPath);
|
|
339
|
+
if (fallback) return fallback;
|
|
340
|
+
|
|
341
|
+
throw new Error("Unable to resolve a directly executable Pi CLI on Windows");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function resolveWatchedChildPiInvocation(
|
|
345
|
+
invocation: ChildPiInvocation,
|
|
346
|
+
timeoutMs: number,
|
|
347
|
+
cancellationPath = "-",
|
|
348
|
+
): ChildPiInvocation {
|
|
159
349
|
return {
|
|
160
|
-
command:
|
|
161
|
-
args: [
|
|
350
|
+
command: process.execPath,
|
|
351
|
+
args: [
|
|
352
|
+
CHILD_PROCESS_WATCHDOG_PATH,
|
|
353
|
+
String(timeoutMs),
|
|
354
|
+
cancellationPath,
|
|
355
|
+
invocation.command,
|
|
356
|
+
...invocation.args,
|
|
357
|
+
],
|
|
162
358
|
};
|
|
163
359
|
}
|
|
164
360
|
|
|
@@ -175,38 +371,75 @@ function shouldRetryWithoutOverridesForError(error: unknown): boolean {
|
|
|
175
371
|
return shouldRetryWithoutOverridesFromText(String(error));
|
|
176
372
|
}
|
|
177
373
|
|
|
374
|
+
async function writePromptToTemporaryFile(prompt: string): Promise<{ dir: string; filePath: string }> {
|
|
375
|
+
const dir = await fs.mkdtemp(join(os.tmpdir(), "pi-hermes-prompt-"));
|
|
376
|
+
const filePath = join(dir, "prompt.md");
|
|
377
|
+
try {
|
|
378
|
+
await fs.writeFile(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
|
|
379
|
+
return { dir, filePath };
|
|
380
|
+
} catch (error) {
|
|
381
|
+
try { await fs.rm(dir, { recursive: true, force: true }); } catch {}
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
178
386
|
export async function execChildPrompt(
|
|
179
387
|
pi: Pick<ExtensionAPI, "exec">,
|
|
180
388
|
prompt: string,
|
|
181
389
|
config: ChildLlmConfig,
|
|
182
390
|
options: ExecChildPromptOptions,
|
|
391
|
+
dependencies: ExecChildPromptDependencies = DEFAULT_EXEC_CHILD_PROMPT_DEPENDENCIES,
|
|
183
392
|
): Promise<PiExecResult> {
|
|
184
393
|
const execOptions = {
|
|
185
|
-
|
|
186
|
-
|
|
394
|
+
timeout: options.timeoutMs + WATCHDOG_EXIT_GRACE_MS,
|
|
395
|
+
};
|
|
396
|
+
const temporaryPrompt = await writePromptToTemporaryFile(prompt);
|
|
397
|
+
const promptReference = `@${temporaryPrompt.filePath}`;
|
|
398
|
+
const cancellationPath = join(temporaryPrompt.dir, "cancel");
|
|
399
|
+
const requestCancellation = () => {
|
|
400
|
+
void fs.writeFile(cancellationPath, "", { mode: 0o600 }).catch(() => {});
|
|
187
401
|
};
|
|
402
|
+
options.signal?.addEventListener("abort", requestCancellation, { once: true });
|
|
403
|
+
if (options.signal?.aborted) requestCancellation();
|
|
188
404
|
|
|
189
405
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
406
|
+
try {
|
|
407
|
+
const invocation = resolveWatchedChildPiInvocation(
|
|
408
|
+
resolveChildPiInvocation(buildChildPiPromptArgs(promptReference, config)),
|
|
409
|
+
options.timeoutMs,
|
|
410
|
+
cancellationPath,
|
|
411
|
+
);
|
|
412
|
+
const result = await pi.exec(invocation.command, invocation.args, execOptions) as PiExecResult;
|
|
413
|
+
if (
|
|
414
|
+
result.code === 0 ||
|
|
415
|
+
!options.retryWithoutOverrides ||
|
|
416
|
+
!hasChildLlmOverrides(config) ||
|
|
417
|
+
!shouldRetryWithoutOverrides(result)
|
|
418
|
+
) {
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (
|
|
423
|
+
!options.retryWithoutOverrides ||
|
|
424
|
+
!hasChildLlmOverrides(config) ||
|
|
425
|
+
!shouldRetryWithoutOverridesForError(error)
|
|
426
|
+
) {
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
199
429
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
)
|
|
206
|
-
|
|
430
|
+
|
|
431
|
+
const retryInvocation = resolveWatchedChildPiInvocation(
|
|
432
|
+
resolveChildPiInvocation(basePromptArgs(promptReference, config)),
|
|
433
|
+
options.timeoutMs,
|
|
434
|
+
cancellationPath,
|
|
435
|
+
);
|
|
436
|
+
return await pi.exec(retryInvocation.command, retryInvocation.args, execOptions) as PiExecResult;
|
|
437
|
+
} finally {
|
|
438
|
+
options.signal?.removeEventListener("abort", requestCancellation);
|
|
439
|
+
try {
|
|
440
|
+
await dependencies.removeTemporaryDirectory(temporaryPrompt.dir);
|
|
441
|
+
} catch {
|
|
442
|
+
try { await fs.unlink(temporaryPrompt.filePath); } catch {}
|
|
207
443
|
}
|
|
208
444
|
}
|
|
209
|
-
|
|
210
|
-
const retryInvocation = resolveChildPiInvocation(basePromptArgs(prompt));
|
|
211
|
-
return pi.exec(retryInvocation.command, retryInvocation.args, execOptions) as Promise<PiExecResult>;
|
|
212
445
|
}
|
|
@@ -5,16 +5,8 @@
|
|
|
5
5
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
6
6
|
import { completeSimple, type Message, type SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
|
|
7
7
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { DIRECT_REVIEW_SYSTEM_PROMPT } from "../constants.js";
|
|
9
8
|
import { MemoryStore } from "../store/memory-store.js";
|
|
10
|
-
import { DatabaseManager } from "../store/db.js";
|
|
11
|
-
import {
|
|
12
|
-
formatFailureMemoryContent,
|
|
13
|
-
removeExactSyncedMemories,
|
|
14
|
-
removeSyncedMemories,
|
|
15
|
-
replaceSyncedMemories,
|
|
16
|
-
syncMemoryEntry,
|
|
17
|
-
} from "../store/sqlite-memory-store.js";
|
|
9
|
+
import type { DatabaseManager } from "../store/db.js";
|
|
18
10
|
import type { MemoryCategory, MemoryConfig, MemoryResult, ThinkingLevel } from "../types.js";
|
|
19
11
|
|
|
20
12
|
export interface ReviewMemoryOperation {
|
|
@@ -38,13 +30,21 @@ export interface DirectReviewResult {
|
|
|
38
30
|
error?: string;
|
|
39
31
|
}
|
|
40
32
|
|
|
41
|
-
export interface
|
|
33
|
+
export interface RunDirectMemoryCompletionOptions {
|
|
42
34
|
userPrompt: string;
|
|
35
|
+
systemPrompt: string;
|
|
43
36
|
config: Pick<MemoryConfig, "llmModelOverride" | "llmThinkingOverride">;
|
|
44
37
|
timeoutMs?: number;
|
|
45
38
|
signal?: AbortSignal;
|
|
46
39
|
}
|
|
47
40
|
|
|
41
|
+
/** Shared transport gate: review/flush/consolidation/correction all default to
|
|
42
|
+
* the in-process direct completion path and fall back to a `pi -p` subprocess
|
|
43
|
+
* only on failure, unless the user forces `reviewTransport: "subprocess"`. */
|
|
44
|
+
export function usesDirectTransport(config: Pick<MemoryConfig, "reviewTransport">): boolean {
|
|
45
|
+
return (config.reviewTransport ?? "direct") === "direct";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
48
|
type ReviewLlmConfig = Pick<MemoryConfig, "llmModelOverride" | "llmThinkingOverride">;
|
|
49
49
|
|
|
50
50
|
function findExactModelReferenceMatch(modelReference: string, availableModels: Model<Api>[]): Model<Api> | undefined {
|
|
@@ -203,107 +203,12 @@ export function parseReviewOperations(text: string): ReviewMemoryOperation[] | n
|
|
|
203
203
|
return parsed;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
function sqliteProjectFor(
|
|
207
|
-
rawTarget: ReviewMemoryOperation["target"],
|
|
208
|
-
projectName?: string | null,
|
|
209
|
-
): string | null | undefined {
|
|
210
|
-
if (rawTarget === "project") return projectName?.trim() || null;
|
|
211
|
-
if (rawTarget === "memory" || rawTarget === "user" || rawTarget === "failure") return null;
|
|
212
|
-
return undefined;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function sqliteTargetFor(rawTarget: ReviewMemoryOperation["target"]): "memory" | "user" | "failure" {
|
|
216
|
-
return rawTarget === "user" ? "user" : rawTarget === "failure" ? "failure" : "memory";
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function syncAdd(
|
|
220
|
-
rawTarget: ReviewMemoryOperation["target"],
|
|
221
|
-
content: string,
|
|
222
|
-
category: MemoryCategory | undefined,
|
|
223
|
-
failureReason: string | undefined,
|
|
224
|
-
dbManager: DatabaseManager | null,
|
|
225
|
-
projectName?: string | null,
|
|
226
|
-
): Promise<void> {
|
|
227
|
-
if (!dbManager) return;
|
|
228
|
-
|
|
229
|
-
const sqliteTarget = sqliteTargetFor(rawTarget);
|
|
230
|
-
const sqliteProject = sqliteProjectFor(rawTarget, projectName);
|
|
231
|
-
|
|
232
|
-
if (rawTarget === "failure") {
|
|
233
|
-
const failureCategory = category ?? "failure";
|
|
234
|
-
syncMemoryEntry(dbManager, {
|
|
235
|
-
content: formatFailureMemoryContent(content, {
|
|
236
|
-
category: failureCategory,
|
|
237
|
-
failureReason,
|
|
238
|
-
}),
|
|
239
|
-
target: "failure",
|
|
240
|
-
project: sqliteProject ?? null,
|
|
241
|
-
category: failureCategory,
|
|
242
|
-
failureReason,
|
|
243
|
-
});
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
syncMemoryEntry(dbManager, {
|
|
248
|
-
content,
|
|
249
|
-
target: sqliteTarget,
|
|
250
|
-
project: sqliteProject ?? null,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function syncReplace(
|
|
255
|
-
rawTarget: ReviewMemoryOperation["target"],
|
|
256
|
-
oldText: string,
|
|
257
|
-
newContent: string,
|
|
258
|
-
dbManager: DatabaseManager | null,
|
|
259
|
-
projectName?: string | null,
|
|
260
|
-
): Promise<void> {
|
|
261
|
-
if (!dbManager) return;
|
|
262
|
-
replaceSyncedMemories(dbManager, oldText, {
|
|
263
|
-
content: newContent,
|
|
264
|
-
target: sqliteTargetFor(rawTarget),
|
|
265
|
-
project: sqliteProjectFor(rawTarget, projectName),
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async function syncRemove(
|
|
270
|
-
rawTarget: ReviewMemoryOperation["target"],
|
|
271
|
-
oldText: string,
|
|
272
|
-
dbManager: DatabaseManager | null,
|
|
273
|
-
projectName?: string | null,
|
|
274
|
-
): Promise<void> {
|
|
275
|
-
if (!dbManager) return;
|
|
276
|
-
removeSyncedMemories(dbManager, oldText, {
|
|
277
|
-
target: sqliteTargetFor(rawTarget),
|
|
278
|
-
project: sqliteProjectFor(rawTarget, projectName),
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async function syncEvictions(
|
|
283
|
-
rawTarget: ReviewMemoryOperation["target"],
|
|
284
|
-
evictedEntries: string[] | undefined,
|
|
285
|
-
dbManager: DatabaseManager | null,
|
|
286
|
-
projectName?: string | null,
|
|
287
|
-
): Promise<void> {
|
|
288
|
-
if (!dbManager || !evictedEntries?.length) return;
|
|
289
|
-
for (const entry of evictedEntries) {
|
|
290
|
-
try {
|
|
291
|
-
removeExactSyncedMemories(dbManager, entry, {
|
|
292
|
-
target: sqliteTargetFor(rawTarget),
|
|
293
|
-
project: sqliteProjectFor(rawTarget, projectName),
|
|
294
|
-
});
|
|
295
|
-
} catch {
|
|
296
|
-
// best effort
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
206
|
export async function applyReviewOperations(
|
|
302
207
|
store: MemoryStore,
|
|
303
208
|
projectStore: MemoryStore | null,
|
|
304
209
|
operations: ReviewMemoryOperation[],
|
|
305
|
-
|
|
306
|
-
|
|
210
|
+
_dbManager: DatabaseManager | null = null,
|
|
211
|
+
_projectName?: string | null,
|
|
307
212
|
): Promise<ApplyReviewOperationsResult> {
|
|
308
213
|
let appliedCount = 0;
|
|
309
214
|
let skippedCount = 0;
|
|
@@ -332,7 +237,6 @@ export async function applyReviewOperations(
|
|
|
332
237
|
failureReason: op.failure_reason,
|
|
333
238
|
});
|
|
334
239
|
if (result.success) {
|
|
335
|
-
await syncAdd(rawTarget, op.content, category, op.failure_reason, dbManager, projectName);
|
|
336
240
|
appliedCount++;
|
|
337
241
|
} else {
|
|
338
242
|
skippedCount++;
|
|
@@ -340,8 +244,6 @@ export async function applyReviewOperations(
|
|
|
340
244
|
} else {
|
|
341
245
|
result = await activeStore.add(memoryTarget, op.content);
|
|
342
246
|
if (result.success) {
|
|
343
|
-
await syncEvictions(rawTarget, result.evicted_entries, dbManager, projectName);
|
|
344
|
-
await syncAdd(rawTarget, op.content, undefined, undefined, dbManager, projectName);
|
|
345
247
|
appliedCount++;
|
|
346
248
|
} else {
|
|
347
249
|
skippedCount++;
|
|
@@ -356,7 +258,6 @@ export async function applyReviewOperations(
|
|
|
356
258
|
}
|
|
357
259
|
result = await activeStore.replace(memoryTarget, op.old_text, op.content);
|
|
358
260
|
if (result.success) {
|
|
359
|
-
await syncReplace(rawTarget, op.old_text, op.content, dbManager, projectName);
|
|
360
261
|
appliedCount++;
|
|
361
262
|
} else {
|
|
362
263
|
skippedCount++;
|
|
@@ -370,7 +271,6 @@ export async function applyReviewOperations(
|
|
|
370
271
|
}
|
|
371
272
|
result = await activeStore.remove(memoryTarget, op.old_text);
|
|
372
273
|
if (result.success) {
|
|
373
|
-
await syncRemove(rawTarget, op.old_text, dbManager, projectName);
|
|
374
274
|
appliedCount++;
|
|
375
275
|
} else {
|
|
376
276
|
skippedCount++;
|
|
@@ -379,7 +279,9 @@ export async function applyReviewOperations(
|
|
|
379
279
|
}
|
|
380
280
|
default:
|
|
381
281
|
skippedCount++;
|
|
282
|
+
continue;
|
|
382
283
|
}
|
|
284
|
+
|
|
383
285
|
}
|
|
384
286
|
|
|
385
287
|
return { appliedCount, skippedCount };
|
|
@@ -395,11 +297,11 @@ function responseText(content: unknown): string {
|
|
|
395
297
|
.join("\n");
|
|
396
298
|
}
|
|
397
299
|
|
|
398
|
-
export async function
|
|
300
|
+
export async function runDirectMemoryCompletion(
|
|
399
301
|
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
400
302
|
store: MemoryStore,
|
|
401
303
|
projectStore: MemoryStore | null,
|
|
402
|
-
options:
|
|
304
|
+
options: RunDirectMemoryCompletionOptions,
|
|
403
305
|
dbManager: DatabaseManager | null = null,
|
|
404
306
|
projectName?: string | null,
|
|
405
307
|
): Promise<DirectReviewResult> {
|
|
@@ -435,7 +337,7 @@ export async function runDirectBackgroundReview(
|
|
|
435
337
|
try {
|
|
436
338
|
const response = await completeSimple(
|
|
437
339
|
model,
|
|
438
|
-
{ systemPrompt:
|
|
340
|
+
{ systemPrompt: options.systemPrompt, messages: [userMessage] },
|
|
439
341
|
buildDirectReviewCompletionOptions(
|
|
440
342
|
model,
|
|
441
343
|
{ apiKey: auth.apiKey, headers: auth.headers, env: auth.env },
|
|
@@ -478,4 +380,4 @@ export async function runDirectBackgroundReview(
|
|
|
478
380
|
} finally {
|
|
479
381
|
clearTimeout(timeout);
|
|
480
382
|
}
|
|
481
|
-
}
|
|
383
|
+
}
|