pi-hermes-memory 0.7.22 → 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 +18 -2
- package/package.json +3 -2
- package/src/config.ts +14 -1
- package/src/constants.ts +72 -0
- package/src/extension-root-migration.ts +429 -5
- package/src/handlers/auto-consolidate.ts +118 -8
- package/src/handlers/background-review.ts +164 -64
- 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 +383 -0
- package/src/handlers/session-flush.ts +71 -4
- package/src/handlers/sync-markdown-memories.ts +143 -51
- package/src/index.ts +36 -17
- 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 +6 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse and apply structured memory operations from direct background review.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
6
|
+
import { completeSimple, type Message, type SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
|
|
7
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { MemoryStore } from "../store/memory-store.js";
|
|
9
|
+
import type { DatabaseManager } from "../store/db.js";
|
|
10
|
+
import type { MemoryCategory, MemoryConfig, MemoryResult, ThinkingLevel } from "../types.js";
|
|
11
|
+
|
|
12
|
+
export interface ReviewMemoryOperation {
|
|
13
|
+
action: "add" | "replace" | "remove";
|
|
14
|
+
target: "memory" | "user" | "project" | "failure";
|
|
15
|
+
content?: string;
|
|
16
|
+
old_text?: string;
|
|
17
|
+
category?: MemoryCategory;
|
|
18
|
+
failure_reason?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ApplyReviewOperationsResult {
|
|
22
|
+
appliedCount: number;
|
|
23
|
+
skippedCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DirectReviewResult {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
appliedCount: number;
|
|
29
|
+
fallbackReason?: "no_model" | "no_auth" | "aborted" | "parse_error" | "provider_error" | "empty";
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RunDirectMemoryCompletionOptions {
|
|
34
|
+
userPrompt: string;
|
|
35
|
+
systemPrompt: string;
|
|
36
|
+
config: Pick<MemoryConfig, "llmModelOverride" | "llmThinkingOverride">;
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
}
|
|
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
|
+
type ReviewLlmConfig = Pick<MemoryConfig, "llmModelOverride" | "llmThinkingOverride">;
|
|
49
|
+
|
|
50
|
+
function findExactModelReferenceMatch(modelReference: string, availableModels: Model<Api>[]): Model<Api> | undefined {
|
|
51
|
+
const trimmedReference = modelReference.trim();
|
|
52
|
+
if (!trimmedReference) return undefined;
|
|
53
|
+
|
|
54
|
+
const normalizedReference = trimmedReference.toLowerCase();
|
|
55
|
+
const canonicalMatches = availableModels.filter(
|
|
56
|
+
(model) => `${model.provider}/${model.id}`.toLowerCase() === normalizedReference,
|
|
57
|
+
);
|
|
58
|
+
if (canonicalMatches.length === 1) return canonicalMatches[0];
|
|
59
|
+
if (canonicalMatches.length > 1) return undefined;
|
|
60
|
+
|
|
61
|
+
const slashIndex = trimmedReference.indexOf("/");
|
|
62
|
+
if (slashIndex !== -1) {
|
|
63
|
+
const provider = trimmedReference.substring(0, slashIndex).trim();
|
|
64
|
+
const modelId = trimmedReference.substring(slashIndex + 1).trim();
|
|
65
|
+
if (provider && modelId) {
|
|
66
|
+
const providerMatches = availableModels.filter(
|
|
67
|
+
(model) => model.provider.toLowerCase() === provider.toLowerCase()
|
|
68
|
+
&& model.id.toLowerCase() === modelId.toLowerCase(),
|
|
69
|
+
);
|
|
70
|
+
if (providerMatches.length === 1) return providerMatches[0];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const idMatches = availableModels.filter((model) => model.id.toLowerCase() === normalizedReference);
|
|
75
|
+
return idMatches.length === 1 ? idMatches[0] : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizedModelOverride(config: ReviewLlmConfig): string | undefined {
|
|
79
|
+
const trimmed = config.llmModelOverride?.trim();
|
|
80
|
+
return trimmed ? trimmed : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function effectiveThinkingOverride(config: ReviewLlmConfig): ThinkingLevel | undefined {
|
|
84
|
+
return config.llmThinkingOverride ?? (normalizedModelOverride(config) ? "off" : undefined);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type ReviewModelRegistry = ExtensionContext["modelRegistry"];
|
|
88
|
+
|
|
89
|
+
export function buildDirectReviewCompletionOptions(
|
|
90
|
+
model: Model<Api>,
|
|
91
|
+
auth: {
|
|
92
|
+
apiKey: string;
|
|
93
|
+
headers?: Record<string, string>;
|
|
94
|
+
env?: Record<string, string>;
|
|
95
|
+
},
|
|
96
|
+
thinking: ThinkingLevel | undefined,
|
|
97
|
+
signal: AbortSignal,
|
|
98
|
+
): SimpleStreamOptions {
|
|
99
|
+
const options: SimpleStreamOptions = {
|
|
100
|
+
apiKey: auth.apiKey,
|
|
101
|
+
headers: auth.headers,
|
|
102
|
+
env: auth.env,
|
|
103
|
+
signal,
|
|
104
|
+
};
|
|
105
|
+
if (model.reasoning && thinking && thinking !== "off") {
|
|
106
|
+
options.reasoning = thinking;
|
|
107
|
+
}
|
|
108
|
+
return options;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function resolveReviewModel(
|
|
112
|
+
ctxModel: Model<Api> | undefined,
|
|
113
|
+
modelRegistry: ReviewModelRegistry,
|
|
114
|
+
config: ReviewLlmConfig,
|
|
115
|
+
): Model<Api> | undefined {
|
|
116
|
+
const override = normalizedModelOverride(config);
|
|
117
|
+
if (override) {
|
|
118
|
+
const matched = findExactModelReferenceMatch(override, modelRegistry.getAll());
|
|
119
|
+
if (matched) return matched;
|
|
120
|
+
}
|
|
121
|
+
return ctxModel;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function extractJsonPayload(text: string): unknown {
|
|
125
|
+
const trimmed = text.trim();
|
|
126
|
+
if (!trimmed) return null;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(trimmed);
|
|
130
|
+
} catch {
|
|
131
|
+
// continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
135
|
+
if (fenced?.[1]) {
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(fenced[1].trim());
|
|
138
|
+
} catch {
|
|
139
|
+
// continue
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const start = trimmed.indexOf("{");
|
|
144
|
+
const end = trimmed.lastIndexOf("}");
|
|
145
|
+
if (start >= 0 && end > start) {
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(trimmed.slice(start, end + 1));
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isMemoryCategory(value: unknown): value is MemoryCategory {
|
|
157
|
+
return value === "failure"
|
|
158
|
+
|| value === "correction"
|
|
159
|
+
|| value === "insight"
|
|
160
|
+
|| value === "preference"
|
|
161
|
+
|| value === "convention"
|
|
162
|
+
|| value === "tool-quirk";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isReviewTarget(value: unknown): value is ReviewMemoryOperation["target"] {
|
|
166
|
+
return value === "memory" || value === "user" || value === "project" || value === "failure";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isReviewAction(value: unknown): value is ReviewMemoryOperation["action"] {
|
|
170
|
+
return value === "add" || value === "replace" || value === "remove";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function parseReviewOperations(text: string): ReviewMemoryOperation[] | null {
|
|
174
|
+
if (/nothing to save/i.test(text) && !text.includes("{")) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const payload = extractJsonPayload(text);
|
|
179
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const operations = (payload as { operations?: unknown }).operations;
|
|
184
|
+
if (!Array.isArray(operations)) return null;
|
|
185
|
+
|
|
186
|
+
const parsed: ReviewMemoryOperation[] = [];
|
|
187
|
+
for (const item of operations) {
|
|
188
|
+
if (!item || typeof item !== "object") continue;
|
|
189
|
+
const op = item as Record<string, unknown>;
|
|
190
|
+
if (!isReviewAction(op.action) || !isReviewTarget(op.target)) continue;
|
|
191
|
+
|
|
192
|
+
const operation: ReviewMemoryOperation = {
|
|
193
|
+
action: op.action,
|
|
194
|
+
target: op.target,
|
|
195
|
+
};
|
|
196
|
+
if (typeof op.content === "string") operation.content = op.content;
|
|
197
|
+
if (typeof op.old_text === "string") operation.old_text = op.old_text;
|
|
198
|
+
if (isMemoryCategory(op.category)) operation.category = op.category;
|
|
199
|
+
if (typeof op.failure_reason === "string") operation.failure_reason = op.failure_reason;
|
|
200
|
+
parsed.push(operation);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return parsed;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function applyReviewOperations(
|
|
207
|
+
store: MemoryStore,
|
|
208
|
+
projectStore: MemoryStore | null,
|
|
209
|
+
operations: ReviewMemoryOperation[],
|
|
210
|
+
_dbManager: DatabaseManager | null = null,
|
|
211
|
+
_projectName?: string | null,
|
|
212
|
+
): Promise<ApplyReviewOperationsResult> {
|
|
213
|
+
let appliedCount = 0;
|
|
214
|
+
let skippedCount = 0;
|
|
215
|
+
|
|
216
|
+
for (const op of operations) {
|
|
217
|
+
if (op.target === "project" && !projectStore) {
|
|
218
|
+
skippedCount++;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const rawTarget = op.target;
|
|
223
|
+
const memoryTarget = rawTarget === "project" ? "memory" : rawTarget === "failure" ? "failure" : rawTarget;
|
|
224
|
+
const activeStore = rawTarget === "project" ? projectStore! : store;
|
|
225
|
+
|
|
226
|
+
let result: MemoryResult;
|
|
227
|
+
switch (op.action) {
|
|
228
|
+
case "add": {
|
|
229
|
+
if (!op.content?.trim()) {
|
|
230
|
+
skippedCount++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (rawTarget === "failure") {
|
|
234
|
+
const category = op.category ?? "failure";
|
|
235
|
+
result = await activeStore.addFailure(op.content, {
|
|
236
|
+
category,
|
|
237
|
+
failureReason: op.failure_reason,
|
|
238
|
+
});
|
|
239
|
+
if (result.success) {
|
|
240
|
+
appliedCount++;
|
|
241
|
+
} else {
|
|
242
|
+
skippedCount++;
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
result = await activeStore.add(memoryTarget, op.content);
|
|
246
|
+
if (result.success) {
|
|
247
|
+
appliedCount++;
|
|
248
|
+
} else {
|
|
249
|
+
skippedCount++;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case "replace": {
|
|
255
|
+
if (!op.old_text || !op.content?.trim()) {
|
|
256
|
+
skippedCount++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
result = await activeStore.replace(memoryTarget, op.old_text, op.content);
|
|
260
|
+
if (result.success) {
|
|
261
|
+
appliedCount++;
|
|
262
|
+
} else {
|
|
263
|
+
skippedCount++;
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
case "remove": {
|
|
268
|
+
if (!op.old_text) {
|
|
269
|
+
skippedCount++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
result = await activeStore.remove(memoryTarget, op.old_text);
|
|
273
|
+
if (result.success) {
|
|
274
|
+
appliedCount++;
|
|
275
|
+
} else {
|
|
276
|
+
skippedCount++;
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
default:
|
|
281
|
+
skippedCount++;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { appliedCount, skippedCount };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function responseText(content: unknown): string {
|
|
291
|
+
if (!Array.isArray(content)) return "";
|
|
292
|
+
return content
|
|
293
|
+
.filter((block): block is { type: "text"; text: string } => (
|
|
294
|
+
!!block && typeof block === "object" && (block as { type?: string }).type === "text"
|
|
295
|
+
))
|
|
296
|
+
.map((block) => block.text)
|
|
297
|
+
.join("\n");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function runDirectMemoryCompletion(
|
|
301
|
+
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
302
|
+
store: MemoryStore,
|
|
303
|
+
projectStore: MemoryStore | null,
|
|
304
|
+
options: RunDirectMemoryCompletionOptions,
|
|
305
|
+
dbManager: DatabaseManager | null = null,
|
|
306
|
+
projectName?: string | null,
|
|
307
|
+
): Promise<DirectReviewResult> {
|
|
308
|
+
const model = resolveReviewModel(ctx.model, ctx.modelRegistry, options.config);
|
|
309
|
+
if (!model) {
|
|
310
|
+
return { ok: false, appliedCount: 0, fallbackReason: "no_model" };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
314
|
+
if (!auth.ok || !auth.apiKey) {
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
appliedCount: 0,
|
|
318
|
+
fallbackReason: "no_auth",
|
|
319
|
+
error: auth.ok ? `No API key for ${model.provider}` : auth.error,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const controller = new AbortController();
|
|
324
|
+
const timeoutMs = options.timeoutMs ?? 120000;
|
|
325
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
326
|
+
if (options.signal) {
|
|
327
|
+
options.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const thinking = effectiveThinkingOverride(options.config);
|
|
331
|
+
const userMessage: Message = {
|
|
332
|
+
role: "user",
|
|
333
|
+
content: [{ type: "text", text: options.userPrompt }],
|
|
334
|
+
timestamp: Date.now(),
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const response = await completeSimple(
|
|
339
|
+
model,
|
|
340
|
+
{ systemPrompt: options.systemPrompt, messages: [userMessage] },
|
|
341
|
+
buildDirectReviewCompletionOptions(
|
|
342
|
+
model,
|
|
343
|
+
{ apiKey: auth.apiKey, headers: auth.headers, env: auth.env },
|
|
344
|
+
thinking,
|
|
345
|
+
controller.signal,
|
|
346
|
+
),
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
if (response.stopReason === "aborted") {
|
|
350
|
+
return { ok: false, appliedCount: 0, fallbackReason: "aborted" };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const text = responseText(response.content);
|
|
354
|
+
const operations = parseReviewOperations(text);
|
|
355
|
+
if (operations === null) {
|
|
356
|
+
return { ok: false, appliedCount: 0, fallbackReason: "parse_error" };
|
|
357
|
+
}
|
|
358
|
+
if (operations.length === 0) {
|
|
359
|
+
return { ok: true, appliedCount: 0, fallbackReason: "empty" };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const { appliedCount } = await applyReviewOperations(
|
|
363
|
+
store,
|
|
364
|
+
projectStore,
|
|
365
|
+
operations,
|
|
366
|
+
dbManager,
|
|
367
|
+
projectName,
|
|
368
|
+
);
|
|
369
|
+
return { ok: true, appliedCount };
|
|
370
|
+
} catch (err) {
|
|
371
|
+
if (controller.signal.aborted) {
|
|
372
|
+
return { ok: false, appliedCount: 0, fallbackReason: "aborted" };
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
ok: false,
|
|
376
|
+
appliedCount: 0,
|
|
377
|
+
fallbackReason: "provider_error",
|
|
378
|
+
error: err instanceof Error ? err.message : String(err),
|
|
379
|
+
};
|
|
380
|
+
} finally {
|
|
381
|
+
clearTimeout(timeout);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
@@ -2,29 +2,73 @@
|
|
|
2
2
|
* Session flush — gives the agent one turn to save memories before context is lost.
|
|
3
3
|
* Ported from hermes-agent/run_agent.py (flush_memories).
|
|
4
4
|
* See PLAN.md → "Hermes Source File Reference Map" for source lines.
|
|
5
|
+
*
|
|
6
|
+
* Default transport: in-process direct completion (same mechanism as
|
|
7
|
+
* background review — see review-memory-ops.ts). Falls back to a `pi -p`
|
|
8
|
+
* subprocess only if direct mode fails or reviewTransport forces subprocess.
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
12
|
import { MemoryStore } from "../store/memory-store.js";
|
|
9
|
-
import {
|
|
13
|
+
import { DatabaseManager } from "../store/db.js";
|
|
14
|
+
import { DIRECT_FLUSH_SYSTEM_PROMPT, ENTRY_DELIMITER, FLUSH_PROMPT } from "../constants.js";
|
|
10
15
|
import type { MemoryConfig } from "../types.js";
|
|
11
16
|
import { collectMessageParts } from "./message-parts.js";
|
|
12
17
|
import { execChildPrompt } from "./pi-child-process.js";
|
|
18
|
+
import { runDirectMemoryCompletion, usesDirectTransport } from "./review-memory-ops.js";
|
|
19
|
+
|
|
20
|
+
function buildDirectFlushUserPrompt(
|
|
21
|
+
store: MemoryStore,
|
|
22
|
+
projectStore: MemoryStore | null,
|
|
23
|
+
parts: string[],
|
|
24
|
+
): string {
|
|
25
|
+
const sections = [
|
|
26
|
+
"--- Current Memory ---",
|
|
27
|
+
store.getMemoryEntries().join(ENTRY_DELIMITER) || "(empty)",
|
|
28
|
+
"",
|
|
29
|
+
"--- Current User Profile ---",
|
|
30
|
+
store.getUserEntries().join(ENTRY_DELIMITER) || "(empty)",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
if (projectStore) {
|
|
34
|
+
sections.push(
|
|
35
|
+
"",
|
|
36
|
+
"--- Current Project Memory ---",
|
|
37
|
+
projectStore.getMemoryEntries().join(ENTRY_DELIMITER) || "(empty)",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
sections.push(
|
|
42
|
+
"",
|
|
43
|
+
"--- Conversation ---",
|
|
44
|
+
parts.join("\n\n"),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return sections.join("\n");
|
|
48
|
+
}
|
|
13
49
|
|
|
14
50
|
export function setupSessionFlush(
|
|
15
51
|
pi: ExtensionAPI,
|
|
16
52
|
store: MemoryStore,
|
|
17
53
|
projectStore: MemoryStore | null,
|
|
18
54
|
config: MemoryConfig,
|
|
55
|
+
dbManager: DatabaseManager | null = null,
|
|
56
|
+
projectName?: string | null,
|
|
57
|
+
deps: { runDirectMemoryCompletion?: typeof runDirectMemoryCompletion } = {},
|
|
19
58
|
): void {
|
|
20
59
|
let userTurnCount = 0;
|
|
60
|
+
const runDirect = deps.runDirectMemoryCompletion ?? runDirectMemoryCompletion;
|
|
21
61
|
|
|
22
62
|
pi.on("message_end", async (event, _ctx) => {
|
|
23
63
|
if (event.message.role === "user") userTurnCount++;
|
|
24
64
|
});
|
|
25
65
|
|
|
26
|
-
/** Shared flush logic — builds conversation snapshot and
|
|
27
|
-
async function flush(
|
|
66
|
+
/** Shared flush logic — builds conversation snapshot and saves memories */
|
|
67
|
+
async function flush(
|
|
68
|
+
ctx: Pick<ExtensionContext, "sessionManager" | "model" | "modelRegistry">,
|
|
69
|
+
signal?: AbortSignal,
|
|
70
|
+
timeoutMs = 30000,
|
|
71
|
+
): Promise<void> {
|
|
28
72
|
if (userTurnCount < config.flushMinTurns) return;
|
|
29
73
|
|
|
30
74
|
let entries;
|
|
@@ -35,6 +79,29 @@ export function setupSessionFlush(
|
|
|
35
79
|
}
|
|
36
80
|
|
|
37
81
|
const parts = collectMessageParts(entries, config.flushRecentMessages);
|
|
82
|
+
|
|
83
|
+
if (usesDirectTransport(config)) {
|
|
84
|
+
try {
|
|
85
|
+
const directResult = await runDirect(
|
|
86
|
+
ctx,
|
|
87
|
+
store,
|
|
88
|
+
projectStore,
|
|
89
|
+
{
|
|
90
|
+
systemPrompt: DIRECT_FLUSH_SYSTEM_PROMPT,
|
|
91
|
+
userPrompt: buildDirectFlushUserPrompt(store, projectStore, parts),
|
|
92
|
+
config,
|
|
93
|
+
timeoutMs,
|
|
94
|
+
signal,
|
|
95
|
+
},
|
|
96
|
+
dbManager,
|
|
97
|
+
projectName,
|
|
98
|
+
);
|
|
99
|
+
if (directResult.ok) return;
|
|
100
|
+
} catch {
|
|
101
|
+
// Fall through to subprocess below.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
38
105
|
const flushMessage = [
|
|
39
106
|
FLUSH_PROMPT,
|
|
40
107
|
"",
|