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
|
@@ -74,6 +74,12 @@ export interface SqliteMemoryRemoveOptions {
|
|
|
74
74
|
project?: string | null;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export interface MarkdownMemoryReconcileResult {
|
|
78
|
+
inserted: number;
|
|
79
|
+
existing: number;
|
|
80
|
+
removed: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
export interface ParsedMarkdownMemoryEntry extends SqliteMemorySyncInput {}
|
|
78
84
|
|
|
79
85
|
function today(): string {
|
|
@@ -179,13 +185,18 @@ function escapeLikePattern(text: string): string {
|
|
|
179
185
|
return text.replace(/[\\%_]/g, '\\$&');
|
|
180
186
|
}
|
|
181
187
|
|
|
182
|
-
function parseMetadataComment(raw: string): { text: string; created: string; lastReferenced: string } {
|
|
183
|
-
const match = raw.match(/^(.*?)\s*<!--\s*created=([^,]+),\s*last=([
|
|
188
|
+
function parseMetadataComment(raw: string): { text: string; created: string; lastReferenced: string; project: string | null } {
|
|
189
|
+
const match = raw.match(/^(.*?)\s*<!--\s*created=([^,]+),\s*last=([^,>]+)(?:,\s*project64=([A-Za-z0-9_-]+))?\s*-->\s*$/);
|
|
184
190
|
if (match) {
|
|
191
|
+
let project: string | null = null;
|
|
192
|
+
if (match[4]) {
|
|
193
|
+
try { project = Buffer.from(match[4], 'base64url').toString('utf-8').trim() || null; } catch {}
|
|
194
|
+
}
|
|
185
195
|
return {
|
|
186
196
|
text: match[1].trim(),
|
|
187
197
|
created: match[2].trim(),
|
|
188
198
|
lastReferenced: match[3].trim(),
|
|
199
|
+
project,
|
|
189
200
|
};
|
|
190
201
|
}
|
|
191
202
|
|
|
@@ -194,6 +205,7 @@ function parseMetadataComment(raw: string): { text: string; created: string; las
|
|
|
194
205
|
text: raw.trim(),
|
|
195
206
|
created: fallback,
|
|
196
207
|
lastReferenced: fallback,
|
|
208
|
+
project: null,
|
|
197
209
|
};
|
|
198
210
|
}
|
|
199
211
|
|
|
@@ -251,7 +263,6 @@ export function formatFailureMemoryContent(
|
|
|
251
263
|
if (options.failureReason) parts.push(`Failed: ${options.failureReason}`);
|
|
252
264
|
if (options.toolState) parts.push(`Tool state: ${options.toolState}`);
|
|
253
265
|
if (options.correctedTo) parts.push(`Corrected to: ${options.correctedTo}`);
|
|
254
|
-
if (options.project) parts.push(`Project: ${options.project}`);
|
|
255
266
|
return parts.join(' — ');
|
|
256
267
|
}
|
|
257
268
|
|
|
@@ -265,7 +276,8 @@ export function parseMarkdownMemoryEntry(
|
|
|
265
276
|
target: 'memory' | 'user' | 'failure',
|
|
266
277
|
project: string | null = null,
|
|
267
278
|
): ParsedMarkdownMemoryEntry {
|
|
268
|
-
const
|
|
279
|
+
const metadata = parseMetadataComment(rawEntry);
|
|
280
|
+
const { text, created, lastReferenced } = metadata;
|
|
269
281
|
const parsedProject = normalizeNullable(project);
|
|
270
282
|
|
|
271
283
|
if (target !== 'failure') {
|
|
@@ -403,6 +415,111 @@ export function syncMemoryEntry(
|
|
|
403
415
|
};
|
|
404
416
|
}
|
|
405
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Make one exact Markdown target/project scope authoritative in SQLite.
|
|
420
|
+
* Upserts and orphan deletion are committed together when transactions are
|
|
421
|
+
* supported by the active SQLite driver.
|
|
422
|
+
*/
|
|
423
|
+
export function reconcileMarkdownMemoryScope(
|
|
424
|
+
dbManager: DatabaseManager,
|
|
425
|
+
rawEntries: string[],
|
|
426
|
+
target: 'memory' | 'user' | 'failure',
|
|
427
|
+
project: string | null = null,
|
|
428
|
+
): MarkdownMemoryReconcileResult {
|
|
429
|
+
const db = dbManager.getDb();
|
|
430
|
+
const normalizedProject = normalizeNullable(project);
|
|
431
|
+
|
|
432
|
+
const reconcile = (): MarkdownMemoryReconcileResult => {
|
|
433
|
+
let inserted = 0;
|
|
434
|
+
let existing = 0;
|
|
435
|
+
const desiredIdentities = new Set<string>();
|
|
436
|
+
|
|
437
|
+
for (const rawEntry of rawEntries) {
|
|
438
|
+
const parsed = parseMarkdownMemoryEntry(rawEntry, target, normalizedProject);
|
|
439
|
+
desiredIdentities.add(JSON.stringify([
|
|
440
|
+
normalizeCategory(parsed.category),
|
|
441
|
+
parsed.content.trim(),
|
|
442
|
+
]));
|
|
443
|
+
const result = syncMemoryEntry(dbManager, parsed);
|
|
444
|
+
if (result.action === 'inserted') inserted++;
|
|
445
|
+
else existing++;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const params: unknown[] = [];
|
|
449
|
+
const conditions = buildScopeConditions(params, target, normalizedProject);
|
|
450
|
+
const scopedRows = db.prepare(`
|
|
451
|
+
SELECT id, content, category
|
|
452
|
+
FROM memories
|
|
453
|
+
WHERE ${conditions.join(' AND ')}
|
|
454
|
+
ORDER BY id ASC
|
|
455
|
+
`).all(...params) as Array<{ id: number; content: string; category: MemoryCategory | null }>;
|
|
456
|
+
const retainedIdentities = new Set<string>();
|
|
457
|
+
const orphanIds: number[] = [];
|
|
458
|
+
for (const row of scopedRows) {
|
|
459
|
+
const identity = JSON.stringify([normalizeCategory(row.category), row.content.trim()]);
|
|
460
|
+
if (!desiredIdentities.has(identity) || retainedIdentities.has(identity)) {
|
|
461
|
+
orphanIds.push(row.id);
|
|
462
|
+
} else {
|
|
463
|
+
retainedIdentities.add(identity);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let removed = 0;
|
|
468
|
+
if (orphanIds.length > 0) {
|
|
469
|
+
const placeholders = orphanIds.map(() => '?').join(', ');
|
|
470
|
+
removed = db.prepare(`DELETE FROM memories WHERE id IN (${placeholders})`).run(...orphanIds).changes;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return { inserted, existing, removed };
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const transactional = db.transaction?.(reconcile);
|
|
477
|
+
return transactional ? transactional() : reconcile();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function failureProject(rawEntry: string): string | null {
|
|
481
|
+
return parseMetadataComment(rawEntry).project;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function reconcileMarkdownFailureScopes(
|
|
485
|
+
dbManager: DatabaseManager,
|
|
486
|
+
rawEntries: string[],
|
|
487
|
+
): MarkdownMemoryReconcileResult {
|
|
488
|
+
const entriesByProject = new Map<string | null, string[]>();
|
|
489
|
+
for (const rawEntry of rawEntries) {
|
|
490
|
+
const project = failureProject(rawEntry);
|
|
491
|
+
const entries = entriesByProject.get(project) ?? [];
|
|
492
|
+
entries.push(rawEntry);
|
|
493
|
+
entriesByProject.set(project, entries);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const mirroredProjects = dbManager.getDb().prepare(`
|
|
497
|
+
SELECT DISTINCT project
|
|
498
|
+
FROM memories
|
|
499
|
+
WHERE target = 'failure'
|
|
500
|
+
`).all() as Array<{ project: string | null }>;
|
|
501
|
+
const projects = new Set<string | null>([
|
|
502
|
+
null,
|
|
503
|
+
...entriesByProject.keys(),
|
|
504
|
+
...mirroredProjects.map(({ project }) => normalizeNullable(project)),
|
|
505
|
+
]);
|
|
506
|
+
const total: MarkdownMemoryReconcileResult = { inserted: 0, existing: 0, removed: 0 };
|
|
507
|
+
|
|
508
|
+
for (const project of projects) {
|
|
509
|
+
const result = reconcileMarkdownMemoryScope(
|
|
510
|
+
dbManager,
|
|
511
|
+
entriesByProject.get(project) ?? [],
|
|
512
|
+
'failure',
|
|
513
|
+
project,
|
|
514
|
+
);
|
|
515
|
+
total.inserted += result.inserted;
|
|
516
|
+
total.existing += result.existing;
|
|
517
|
+
total.removed += result.removed;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return total;
|
|
521
|
+
}
|
|
522
|
+
|
|
406
523
|
/**
|
|
407
524
|
* Best-effort substring replacement for SQLite-backed memory sync.
|
|
408
525
|
* Updates all matches in the scoped slice to recover from prior duplicate rows.
|
package/src/tools/memory-tool.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { MemoryStore } from "../store/memory-store.js";
|
|
|
11
11
|
import { DatabaseManager } from "../store/db.js";
|
|
12
12
|
import {
|
|
13
13
|
formatFailureMemoryContent,
|
|
14
|
+
reconcileMarkdownFailureScopes,
|
|
15
|
+
reconcileMarkdownMemoryScope,
|
|
14
16
|
removeExactSyncedMemories,
|
|
15
17
|
removeSyncedMemories,
|
|
16
18
|
replaceSyncedMemories,
|
|
@@ -185,6 +187,31 @@ async function syncEvictionsFromSqlite(
|
|
|
185
187
|
}
|
|
186
188
|
}
|
|
187
189
|
|
|
190
|
+
async function reconcileStoreScope(
|
|
191
|
+
entries: string[],
|
|
192
|
+
rawTarget: "memory" | "user" | "project" | "failure",
|
|
193
|
+
dbManager: DatabaseManager | null,
|
|
194
|
+
projectName?: string | null,
|
|
195
|
+
): Promise<string | null | undefined> {
|
|
196
|
+
if (!dbManager) return undefined;
|
|
197
|
+
try {
|
|
198
|
+
if (rawTarget === "failure") {
|
|
199
|
+
reconcileMarkdownFailureScopes(dbManager, entries);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const target = sqliteTargetFor(rawTarget);
|
|
203
|
+
reconcileMarkdownMemoryScope(
|
|
204
|
+
dbManager,
|
|
205
|
+
entries,
|
|
206
|
+
target,
|
|
207
|
+
sqliteProjectFor(rawTarget, projectName) ?? null,
|
|
208
|
+
);
|
|
209
|
+
return null;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return `Saved to Markdown, but SQLite search reconciliation failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
188
215
|
export function registerMemoryTool(
|
|
189
216
|
pi: ExtensionAPI,
|
|
190
217
|
store: MemoryStore,
|
|
@@ -192,6 +219,16 @@ export function registerMemoryTool(
|
|
|
192
219
|
dbManager: DatabaseManager | null = null,
|
|
193
220
|
projectName?: string | null,
|
|
194
221
|
): void {
|
|
222
|
+
const reconciledStores = new WeakSet<MemoryStore>();
|
|
223
|
+
if (typeof store.setMutationObserver === "function") {
|
|
224
|
+
store.setMutationObserver((target, entries) => reconcileStoreScope(entries, target, dbManager, projectName));
|
|
225
|
+
reconciledStores.add(store);
|
|
226
|
+
}
|
|
227
|
+
if (projectStore && typeof projectStore.setMutationObserver === "function") {
|
|
228
|
+
projectStore.setMutationObserver((_target, entries) => reconcileStoreScope(entries, "project", dbManager, projectName));
|
|
229
|
+
reconciledStores.add(projectStore);
|
|
230
|
+
}
|
|
231
|
+
|
|
195
232
|
pi.registerTool({
|
|
196
233
|
name: "memory",
|
|
197
234
|
label: "Memory",
|
|
@@ -244,6 +281,7 @@ export function registerMemoryTool(
|
|
|
244
281
|
|
|
245
282
|
let result: MemoryResult;
|
|
246
283
|
let syncWarning: string | null = null;
|
|
284
|
+
const syncHandled = reconciledStores.has(store_);
|
|
247
285
|
switch (action) {
|
|
248
286
|
case "add":
|
|
249
287
|
if (!content) {
|
|
@@ -267,12 +305,12 @@ export function registerMemoryTool(
|
|
|
267
305
|
category: memoryCategory,
|
|
268
306
|
failureReason: failure_reason,
|
|
269
307
|
});
|
|
270
|
-
if (result.success) {
|
|
308
|
+
if (result.success && !syncHandled) {
|
|
271
309
|
syncWarning = await syncAddToSqlite(rawTarget, content, memoryCategory, failure_reason, dbManager, projectName);
|
|
272
310
|
}
|
|
273
311
|
} else {
|
|
274
|
-
result = await store_.add(target, content);
|
|
275
|
-
if (result.success) {
|
|
312
|
+
result = await store_.add(target, content, signal);
|
|
313
|
+
if (result.success && !syncHandled) {
|
|
276
314
|
await syncEvictionsFromSqlite(rawTarget, result.evicted_entries, dbManager, projectName);
|
|
277
315
|
syncWarning = await syncAddToSqlite(rawTarget, content, undefined, undefined, dbManager, projectName);
|
|
278
316
|
}
|
|
@@ -309,9 +347,7 @@ export function registerMemoryTool(
|
|
|
309
347
|
};
|
|
310
348
|
}
|
|
311
349
|
result = await store_.replace(target, old_text, content);
|
|
312
|
-
if (result.success)
|
|
313
|
-
syncWarning = await syncReplaceToSqlite(rawTarget, old_text, content, dbManager, projectName);
|
|
314
|
-
}
|
|
350
|
+
if (result.success && !syncHandled) syncWarning = await syncReplaceToSqlite(rawTarget, old_text, content, dbManager, projectName);
|
|
315
351
|
break;
|
|
316
352
|
|
|
317
353
|
case "remove":
|
|
@@ -330,9 +366,7 @@ export function registerMemoryTool(
|
|
|
330
366
|
};
|
|
331
367
|
}
|
|
332
368
|
result = await store_.remove(target, old_text);
|
|
333
|
-
if (result.success)
|
|
334
|
-
syncWarning = await syncRemoveFromSqlite(rawTarget, old_text, dbManager, projectName);
|
|
335
|
-
}
|
|
369
|
+
if (result.success && !syncHandled) syncWarning = await syncRemoveFromSqlite(rawTarget, old_text, dbManager, projectName);
|
|
336
370
|
break;
|
|
337
371
|
|
|
338
372
|
default:
|
|
@@ -342,6 +376,11 @@ export function registerMemoryTool(
|
|
|
342
376
|
};
|
|
343
377
|
}
|
|
344
378
|
|
|
379
|
+
if (result.success && !syncHandled && typeof store_.getRawEntriesForSync === "function") {
|
|
380
|
+
const reconciliationWarning = await reconcileStoreScope(store_.getRawEntriesForSync(target), rawTarget, dbManager, projectName);
|
|
381
|
+
if (reconciliationWarning !== undefined) syncWarning = reconciliationWarning;
|
|
382
|
+
}
|
|
383
|
+
|
|
345
384
|
if (syncWarning && result.success) {
|
|
346
385
|
result = appendSyncWarning(result, syncWarning);
|
|
347
386
|
}
|
|
@@ -14,6 +14,10 @@ interface SearchResult {
|
|
|
14
14
|
count?: number;
|
|
15
15
|
message?: string;
|
|
16
16
|
output?: string;
|
|
17
|
+
outputChars?: number;
|
|
18
|
+
outputTruncated?: boolean;
|
|
19
|
+
snippetChars?: number;
|
|
20
|
+
truncatedCount?: number;
|
|
17
21
|
ranges?: SessionAnchorRange[];
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -22,6 +26,26 @@ interface SessionSearchToolOptions {
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
const DEFAULT_SESSIONS_DIR = path.join(AGENT_ROOT, 'sessions');
|
|
29
|
+
const DEFAULT_LEGACY_SNIPPET_CHARS = 1_200;
|
|
30
|
+
const MAX_LEGACY_SNIPPET_CHARS = 4_000;
|
|
31
|
+
const MAX_LEGACY_OUTPUT_CHARS = 50 * 1024;
|
|
32
|
+
|
|
33
|
+
function truncateLegacySnippet(text: string, maxChars: number): { text: string; truncated: boolean } {
|
|
34
|
+
if (text.length <= maxChars) return { text, truncated: false };
|
|
35
|
+
return {
|
|
36
|
+
text: `${text.slice(0, maxChars)}\n... (truncated, ${text.length} chars total — refine the query or increase snippetChars)`,
|
|
37
|
+
truncated: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function capLegacyOutput(text: string): { text: string; truncated: boolean } {
|
|
42
|
+
if (text.length <= MAX_LEGACY_OUTPUT_CHARS) return { text, truncated: false };
|
|
43
|
+
const suffix = `\n... (output truncated, ${text.length} chars total — refine the query or lower the result limit)`;
|
|
44
|
+
return {
|
|
45
|
+
text: `${text.slice(0, MAX_LEGACY_OUTPUT_CHARS - suffix.length)}${suffix}`,
|
|
46
|
+
truncated: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
25
49
|
|
|
26
50
|
export function registerSessionSearchTool(
|
|
27
51
|
pi: ExtensionAPI,
|
|
@@ -127,7 +151,7 @@ Examples:
|
|
|
127
151
|
- "Find the PR where we fixed the test hang"
|
|
128
152
|
- "What approach did we take for the database migration?"
|
|
129
153
|
|
|
130
|
-
Returns conversation snippets with session dates and project context.`,
|
|
154
|
+
Returns bounded conversation snippets with session dates and project context. Large messages are truncated with their original character count.`,
|
|
131
155
|
promptSnippet: 'Search past conversations for relevant context',
|
|
132
156
|
promptGuidelines: [
|
|
133
157
|
'Use session_search when the user asks about previous discussions or past work.',
|
|
@@ -137,13 +161,27 @@ Returns conversation snippets with session dates and project context.`,
|
|
|
137
161
|
query: Type.String({ description: 'Search query. Use natural language or specific terms.' }),
|
|
138
162
|
project: Type.Optional(Type.String({ description: 'Filter by project name (optional).' })),
|
|
139
163
|
role: Type.Optional(StringEnum(['user', 'assistant'] as const, { description: 'Filter by message role (optional).' })),
|
|
140
|
-
limit: Type.Optional(Type.Number({
|
|
164
|
+
limit: Type.Optional(Type.Number({
|
|
165
|
+
description: 'Maximum results to return (default: 10, min: 1, max: 20).',
|
|
166
|
+
minimum: 1,
|
|
167
|
+
maximum: 20,
|
|
168
|
+
})),
|
|
169
|
+
snippetChars: Type.Optional(Type.Number({
|
|
170
|
+
description: `Maximum characters per result snippet (default: ${DEFAULT_LEGACY_SNIPPET_CHARS}, max: ${MAX_LEGACY_SNIPPET_CHARS}).`,
|
|
171
|
+
minimum: 100,
|
|
172
|
+
maximum: MAX_LEGACY_SNIPPET_CHARS,
|
|
173
|
+
})),
|
|
141
174
|
}),
|
|
142
|
-
execute: async (_id: string, args: { query: string; project?: string; role?: string; limit?: number }) => {
|
|
175
|
+
execute: async (_id: string, args: { query: string; project?: string; role?: string; limit?: number; snippetChars?: number }) => {
|
|
143
176
|
const query = args.query;
|
|
144
177
|
const project = args.project;
|
|
145
178
|
const role = args.role;
|
|
146
|
-
const
|
|
179
|
+
const requestedLimit = Number.isFinite(args.limit) ? Math.floor(args.limit!) : 10;
|
|
180
|
+
const limit = Math.min(Math.max(requestedLimit, 1), 20);
|
|
181
|
+
const requestedSnippetChars = Number.isFinite(args.snippetChars)
|
|
182
|
+
? Math.floor(args.snippetChars!)
|
|
183
|
+
: DEFAULT_LEGACY_SNIPPET_CHARS;
|
|
184
|
+
const snippetChars = Math.min(Math.max(requestedSnippetChars, 100), MAX_LEGACY_SNIPPET_CHARS);
|
|
147
185
|
|
|
148
186
|
if (!query || query.trim().length === 0) {
|
|
149
187
|
const result: SearchResult = { success: false, message: 'query is required' };
|
|
@@ -159,11 +197,19 @@ Returns conversation snippets with session dates and project context.`,
|
|
|
159
197
|
const results = searchSessions(dbManager, query, { project, role, limit });
|
|
160
198
|
|
|
161
199
|
if (results.length === 0) {
|
|
162
|
-
const
|
|
163
|
-
|
|
200
|
+
const output = capLegacyOutput('No results found. Try a different search term or broader query.');
|
|
201
|
+
const result: SearchResult = {
|
|
202
|
+
success: true,
|
|
203
|
+
count: 0,
|
|
204
|
+
message: output.text,
|
|
205
|
+
outputChars: output.text.length,
|
|
206
|
+
outputTruncated: output.truncated,
|
|
207
|
+
};
|
|
208
|
+
return { content: [{ type: 'text' as const, text: output.text }], details: result };
|
|
164
209
|
}
|
|
165
210
|
|
|
166
|
-
|
|
211
|
+
const blocks: string[] = [`Found ${results.length} results for "${query}":`];
|
|
212
|
+
let truncatedCount = 0;
|
|
167
213
|
|
|
168
214
|
for (const r of results) {
|
|
169
215
|
const date = new Date(r.timestamp).toLocaleDateString('en-US', {
|
|
@@ -172,13 +218,25 @@ Returns conversation snippets with session dates and project context.`,
|
|
|
172
218
|
day: 'numeric',
|
|
173
219
|
});
|
|
174
220
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
221
|
+
const snippet = truncateLegacySnippet(r.snippet, snippetChars);
|
|
222
|
+
if (snippet.truncated) truncatedCount += 1;
|
|
223
|
+
blocks.push([
|
|
224
|
+
'---',
|
|
225
|
+
`📅 ${date} | 📁 ${r.project} | ${r.role === 'user' ? '👤 User' : '🤖 Assistant'}`,
|
|
226
|
+
snippet.text,
|
|
227
|
+
].join('\n'));
|
|
178
228
|
}
|
|
179
229
|
|
|
180
|
-
const
|
|
181
|
-
|
|
230
|
+
const output = capLegacyOutput(blocks.join('\n\n').trim());
|
|
231
|
+
const finalResult: SearchResult = {
|
|
232
|
+
success: true,
|
|
233
|
+
count: results.length,
|
|
234
|
+
truncatedCount,
|
|
235
|
+
snippetChars,
|
|
236
|
+
outputChars: output.text.length,
|
|
237
|
+
outputTruncated: output.truncated,
|
|
238
|
+
};
|
|
239
|
+
return { content: [{ type: 'text' as const, text: output.text }], details: finalResult };
|
|
182
240
|
},
|
|
183
241
|
});
|
|
184
242
|
}
|
package/src/types.ts
CHANGED
|
@@ -10,6 +10,8 @@ export type SessionSearchVariant = "legacy" | "anchors";
|
|
|
10
10
|
|
|
11
11
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
12
12
|
|
|
13
|
+
export type ReviewTransport = "direct" | "subprocess";
|
|
14
|
+
|
|
13
15
|
export interface SessionSearchConfig {
|
|
14
16
|
/** Session search implementation variant. Default: legacy */
|
|
15
17
|
variant: SessionSearchVariant;
|
|
@@ -34,6 +36,8 @@ export interface MemoryConfig {
|
|
|
34
36
|
reviewRecentMessages?: number;
|
|
35
37
|
/** Enable background learning loop. Default: true */
|
|
36
38
|
reviewEnabled: boolean;
|
|
39
|
+
/** How background review invokes the LLM. Default: direct */
|
|
40
|
+
reviewTransport?: ReviewTransport;
|
|
37
41
|
/** Flush memories before compaction. Default: true */
|
|
38
42
|
flushOnCompact: boolean;
|
|
39
43
|
/** Flush memories on session shutdown. Default: true */
|
|
@@ -52,6 +56,8 @@ export interface MemoryConfig {
|
|
|
52
56
|
llmModelOverride?: string;
|
|
53
57
|
/** Override thinking level used for child pi -p subprocess LLM calls. Default: unset */
|
|
54
58
|
llmThinkingOverride?: ThinkingLevel;
|
|
59
|
+
/** Extra extension entry paths required by child Pi processes, such as provider auth adapters. */
|
|
60
|
+
childExtensionPaths?: string[];
|
|
55
61
|
/** Strategy when memory is full. Default: auto-consolidate */
|
|
56
62
|
memoryOverflowStrategy?: MemoryOverflowStrategy;
|
|
57
63
|
/** Legacy alias for memoryOverflowStrategy. Default: true */
|