pi-memory-stone 0.1.1 → 0.1.3
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 +53 -7
- package/package.json +7 -1
- package/src/commands/index.ts +326 -4
- package/src/config/index.ts +7 -0
- package/src/db/index.ts +15 -3
- package/src/index.ts +34 -51
- package/src/portable/index.ts +204 -0
- package/src/session-state/index.ts +88 -0
- package/test/indexing.test.ts +0 -97
- package/test/parser.test.ts +0 -261
- package/test/privacy.test.ts +0 -120
- package/test/ranking.test.ts +0 -403
- package/tsconfig.json +0 -13
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - SQLite schema + migrations
|
|
9
9
|
* - Deterministic turn_summary and file_activity capture on agent_end
|
|
10
10
|
* - FTS5 search
|
|
11
|
-
* - /memory-status, /memory-search, /memory-last commands
|
|
11
|
+
* - /memory-status, /memory-search, /memory-open, /memory-inject, /memory-last commands
|
|
12
12
|
* - memory_search, memory_open, memory_remember, memory_forget tools
|
|
13
13
|
* - Conservative same-project before_agent_start injection
|
|
14
14
|
*/
|
|
@@ -19,7 +19,8 @@ import { registerTools } from "./tools/index.js";
|
|
|
19
19
|
import { indexSessionOnAgentEnd } from "./indexing/index.js";
|
|
20
20
|
import { retrieve, buildInjectionPacket, formatInjectionForLlm } from "./retrieval/index.js";
|
|
21
21
|
import { getProjectId, getConfig, clearProjectCache } from "./config/index.js";
|
|
22
|
-
import { closeDb } from "./db/index.js";
|
|
22
|
+
import { closeDb, getRecord, insertInjection } from "./db/index.js";
|
|
23
|
+
import { getMemorySessionState, manualRecordsToRankedResults } from "./session-state/index.js";
|
|
23
24
|
import { createHash } from "node:crypto";
|
|
24
25
|
|
|
25
26
|
// ─── Session-scoped state ───────────────────────────────────────────
|
|
@@ -72,62 +73,54 @@ export default function (pi: ExtensionAPI) {
|
|
|
72
73
|
const config = getConfig(ctx.cwd);
|
|
73
74
|
if (!config.enabled) return;
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
for (const entry of ctx.sessionManager.getBranch()) {
|
|
78
|
-
if (
|
|
79
|
-
entry.type === "custom" &&
|
|
80
|
-
entry.customType === "memory-stone:session-toggle"
|
|
81
|
-
) {
|
|
82
|
-
const data = entry.data as { enabled?: boolean } | undefined;
|
|
83
|
-
if (typeof data?.enabled === "boolean") {
|
|
84
|
-
sessionEnabled = data.enabled;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
76
|
+
const sessionState = getMemorySessionState(ctx.sessionManager.getBranch());
|
|
77
|
+
sessionEnabled = sessionState.enabled;
|
|
88
78
|
|
|
89
79
|
if (!sessionEnabled) return;
|
|
90
80
|
|
|
91
81
|
const prompt = event.prompt || "";
|
|
92
|
-
if (!prompt.trim()) return;
|
|
93
|
-
|
|
94
|
-
// Hash prompt to detect repeated injections
|
|
95
82
|
const promptHash = createHash("sha256").update(prompt).digest("hex").slice(0, 12);
|
|
96
|
-
|
|
97
83
|
const projectId = getProjectId(ctx.cwd);
|
|
84
|
+
const injectionMode = sessionState.injectionMode ?? config.injectionMode;
|
|
85
|
+
|
|
86
|
+
const manualRecords = sessionState.manualRefs
|
|
87
|
+
.map((ref) => getRecord(ref))
|
|
88
|
+
.filter((record): record is NonNullable<typeof record> => Boolean(record));
|
|
89
|
+
const manualResults = manualRecordsToRankedResults(manualRecords, projectId);
|
|
90
|
+
const manualRefSet = new Set(manualResults.map((r) => r.record.id));
|
|
91
|
+
|
|
92
|
+
let autoResults: ReturnType<typeof retrieve> = [];
|
|
93
|
+
if (injectionMode === "auto" && prompt.trim()) {
|
|
94
|
+
const results = retrieve(prompt, projectId, [], {
|
|
95
|
+
limit: config.maxInjectedRecords,
|
|
96
|
+
crossProjectEnabled: config.crossProjectEnabled,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
autoResults = results
|
|
100
|
+
.filter((r) => !manualRefSet.has(r.record.id))
|
|
101
|
+
.filter((r) => !injectedRefsThisSession.has(r.record.id))
|
|
102
|
+
.filter((r) => r.score >= config.scoreThreshold);
|
|
103
|
+
}
|
|
98
104
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
limit: config.maxInjectedRecords,
|
|
102
|
-
crossProjectEnabled: config.crossProjectEnabled,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Filter: skip already-injected refs
|
|
106
|
-
const newResults = results.filter((r) => !injectedRefsThisSession.has(r.record.id));
|
|
107
|
-
|
|
108
|
-
// Score threshold
|
|
109
|
-
const thresholdResults = newResults.filter((r) => r.score >= config.scoreThreshold);
|
|
110
|
-
|
|
111
|
-
if (thresholdResults.length === 0) return;
|
|
105
|
+
const selectedResults = [...manualResults, ...autoResults];
|
|
106
|
+
if (selectedResults.length === 0) return;
|
|
112
107
|
|
|
113
|
-
|
|
114
|
-
const packet = buildInjectionPacket(thresholdResults);
|
|
108
|
+
const packet = buildInjectionPacket(selectedResults);
|
|
115
109
|
const formatted = formatInjectionForLlm(packet, config.maxInjectedTokens);
|
|
116
110
|
|
|
117
|
-
// Track
|
|
118
|
-
|
|
111
|
+
// Track only search-selected refs. Manually chosen refs are intentionally
|
|
112
|
+
// injected on every turn until /memory-clear-injected is used.
|
|
113
|
+
for (const r of autoResults) {
|
|
119
114
|
injectedRefsThisSession.add(r.record.id);
|
|
120
115
|
}
|
|
121
116
|
|
|
122
|
-
// Log injection to DB
|
|
123
|
-
const { insertInjection } = await import("./db/index.js");
|
|
124
117
|
insertInjection({
|
|
125
118
|
session_id: ctx.sessionManager.getSessionId(),
|
|
126
119
|
turn_entry_id: ctx.sessionManager.getLeafId() ?? undefined,
|
|
127
120
|
prompt_hash: promptHash,
|
|
128
|
-
injected_refs:
|
|
121
|
+
injected_refs: selectedResults.map((r) => r.record.id).join(","),
|
|
129
122
|
packet: formatted,
|
|
130
|
-
reasons:
|
|
123
|
+
reasons: selectedResults.map((r) => r.reasons.join(";")).join(" | "),
|
|
131
124
|
});
|
|
132
125
|
|
|
133
126
|
// Inject as a non-context audit custom entry (separate from LLM context)
|
|
@@ -156,17 +149,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
156
149
|
sessionEnabled = true;
|
|
157
150
|
|
|
158
151
|
// Restore session toggle from branch
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
entry.type === "custom" &&
|
|
162
|
-
entry.customType === "memory-stone:session-toggle"
|
|
163
|
-
) {
|
|
164
|
-
const data = entry.data as { enabled?: boolean } | undefined;
|
|
165
|
-
if (typeof data?.enabled === "boolean") {
|
|
166
|
-
sessionEnabled = data.enabled;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
152
|
+
sessionEnabled = getMemorySessionState(ctx.sessionManager.getBranch()).enabled;
|
|
170
153
|
|
|
171
154
|
// Clear project ID cache on session change
|
|
172
155
|
clearProjectCache();
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable export/import/backup helpers for memory records.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
7
|
+
import { getDb, getDbPath, listRecords, upsertRecord, type RecordRow } from "../db/index.js";
|
|
8
|
+
import { SCHEMA_VERSION, RECORD_KINDS, RECORD_SCOPES, RECORD_STATUSES, type RecordKind, type RecordScope, type RecordStatus } from "../db/schema.js";
|
|
9
|
+
|
|
10
|
+
export type ExportFormat = "json" | "md";
|
|
11
|
+
|
|
12
|
+
export interface PortableMemoryRecord {
|
|
13
|
+
id: string;
|
|
14
|
+
kind: RecordKind;
|
|
15
|
+
scope: RecordScope;
|
|
16
|
+
project_id: string | null;
|
|
17
|
+
text: string;
|
|
18
|
+
tags: string | null;
|
|
19
|
+
status: RecordStatus;
|
|
20
|
+
confidence: number;
|
|
21
|
+
importance: number;
|
|
22
|
+
created_at: number;
|
|
23
|
+
updated_at: number;
|
|
24
|
+
superseded_by: string | null;
|
|
25
|
+
derived_from_memory_refs: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PortableMemoryExport {
|
|
29
|
+
format: "pi-memory-stone-export";
|
|
30
|
+
version: 1;
|
|
31
|
+
exported_at: string;
|
|
32
|
+
schema_version: number;
|
|
33
|
+
records: PortableMemoryRecord[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ImportOptions {
|
|
37
|
+
/** Remap project-scoped records to this project id. Use undefined to preserve exported project ids. */
|
|
38
|
+
projectId?: string | null;
|
|
39
|
+
/** Force every imported record into a scope. */
|
|
40
|
+
scopeOverride?: RecordScope;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ImportResult {
|
|
44
|
+
imported: number;
|
|
45
|
+
skipped: number;
|
|
46
|
+
ids: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildMemoryExport(includeInactive = false): PortableMemoryExport {
|
|
50
|
+
return {
|
|
51
|
+
format: "pi-memory-stone-export",
|
|
52
|
+
version: 1,
|
|
53
|
+
exported_at: new Date().toISOString(),
|
|
54
|
+
schema_version: SCHEMA_VERSION,
|
|
55
|
+
records: listRecords({ includeInactive }).map(toPortableRecord),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function exportMemory(format: ExportFormat, includeInactive = false): string {
|
|
60
|
+
const payload = buildMemoryExport(includeInactive);
|
|
61
|
+
if (format === "json") {
|
|
62
|
+
return JSON.stringify(payload, null, 2) + "\n";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return exportMarkdown(payload);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function writeMemoryExport(path: string, format: ExportFormat, includeInactive = false): number {
|
|
69
|
+
const payload = buildMemoryExport(includeInactive);
|
|
70
|
+
const content = format === "json" ? JSON.stringify(payload, null, 2) + "\n" : exportMarkdown(payload);
|
|
71
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
72
|
+
writeFileSync(path, content, "utf8");
|
|
73
|
+
return payload.records.length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function importMemoryJsonFile(path: string, options: ImportOptions = {}): ImportResult {
|
|
77
|
+
const raw = readFileSync(path, "utf8");
|
|
78
|
+
return importMemoryJson(raw, options);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function importMemoryJson(raw: string, options: ImportOptions = {}): ImportResult {
|
|
82
|
+
const parsed = JSON.parse(raw) as Partial<PortableMemoryExport>;
|
|
83
|
+
if (parsed.format !== "pi-memory-stone-export" || parsed.version !== 1 || !Array.isArray(parsed.records)) {
|
|
84
|
+
throw new Error("Unsupported memory export file. Expected pi-memory-stone-export version 1 JSON.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result: ImportResult = { imported: 0, skipped: 0, ids: [] };
|
|
88
|
+
for (const candidate of parsed.records) {
|
|
89
|
+
const record = normalizePortableRecord(candidate);
|
|
90
|
+
if (!record) {
|
|
91
|
+
result.skipped += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const scope = options.scopeOverride ?? record.scope;
|
|
96
|
+
const projectId = scope === "global" ? null : (options.projectId !== undefined ? options.projectId : record.project_id);
|
|
97
|
+
const id = upsertRecord({
|
|
98
|
+
kind: record.kind,
|
|
99
|
+
scope,
|
|
100
|
+
project_id: projectId,
|
|
101
|
+
text: record.text,
|
|
102
|
+
tags: record.tags,
|
|
103
|
+
status: record.status,
|
|
104
|
+
confidence: record.confidence,
|
|
105
|
+
importance: record.importance,
|
|
106
|
+
created_at: record.created_at,
|
|
107
|
+
updated_at: record.updated_at,
|
|
108
|
+
superseded_by: record.superseded_by,
|
|
109
|
+
derived_from_memory_refs: record.derived_from_memory_refs,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
result.imported += 1;
|
|
113
|
+
result.ids.push(id);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function backupMemoryDatabase(path: string): void {
|
|
120
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
121
|
+
getDb().exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
122
|
+
copyFileSync(getDbPath(), path);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function defaultPortablePath(cwd: string, prefix: string, extension: string): string {
|
|
126
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
127
|
+
return resolve(cwd, `${prefix}-${stamp}.${extension}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function resolvePortablePath(cwd: string, path: string): string {
|
|
131
|
+
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function toPortableRecord(row: RecordRow): PortableMemoryRecord {
|
|
135
|
+
return {
|
|
136
|
+
id: row.id,
|
|
137
|
+
kind: row.kind,
|
|
138
|
+
scope: row.scope,
|
|
139
|
+
project_id: row.project_id,
|
|
140
|
+
text: row.text,
|
|
141
|
+
tags: row.tags,
|
|
142
|
+
status: row.status,
|
|
143
|
+
confidence: row.confidence,
|
|
144
|
+
importance: row.importance,
|
|
145
|
+
created_at: row.created_at,
|
|
146
|
+
updated_at: row.updated_at,
|
|
147
|
+
superseded_by: row.superseded_by,
|
|
148
|
+
derived_from_memory_refs: row.derived_from_memory_refs,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function exportMarkdown(payload: PortableMemoryExport): string {
|
|
153
|
+
const lines: string[] = [];
|
|
154
|
+
lines.push("# Memory Stone Export");
|
|
155
|
+
lines.push("");
|
|
156
|
+
lines.push(`Exported: ${payload.exported_at}`);
|
|
157
|
+
lines.push(`Records: ${payload.records.length}`);
|
|
158
|
+
lines.push("");
|
|
159
|
+
|
|
160
|
+
for (const record of payload.records) {
|
|
161
|
+
lines.push(`## [${record.kind}] ${record.id}`);
|
|
162
|
+
lines.push("");
|
|
163
|
+
lines.push(`- Scope: ${record.scope}`);
|
|
164
|
+
lines.push(`- Project: ${record.project_id ?? "global"}`);
|
|
165
|
+
lines.push(`- Status: ${record.status}`);
|
|
166
|
+
lines.push(`- Importance: ${record.importance}`);
|
|
167
|
+
lines.push(`- Created: ${new Date(record.created_at).toISOString()}`);
|
|
168
|
+
if (record.tags) lines.push(`- Tags: ${record.tags}`);
|
|
169
|
+
lines.push("");
|
|
170
|
+
lines.push(record.text);
|
|
171
|
+
lines.push("");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return lines.join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizePortableRecord(candidate: unknown): PortableMemoryRecord | null {
|
|
178
|
+
if (!candidate || typeof candidate !== "object") return null;
|
|
179
|
+
const r = candidate as Record<string, unknown>;
|
|
180
|
+
if (typeof r.text !== "string" || r.text.trim() === "") return null;
|
|
181
|
+
if (!isStringMember(r.kind, RECORD_KINDS)) return null;
|
|
182
|
+
if (!isStringMember(r.scope, RECORD_SCOPES)) return null;
|
|
183
|
+
if (!isStringMember(r.status, RECORD_STATUSES)) return null;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
id: typeof r.id === "string" ? r.id : "",
|
|
187
|
+
kind: r.kind,
|
|
188
|
+
scope: r.scope,
|
|
189
|
+
project_id: typeof r.project_id === "string" ? r.project_id : null,
|
|
190
|
+
text: r.text,
|
|
191
|
+
tags: typeof r.tags === "string" ? r.tags : null,
|
|
192
|
+
status: r.status,
|
|
193
|
+
confidence: typeof r.confidence === "number" ? r.confidence : 1,
|
|
194
|
+
importance: typeof r.importance === "number" ? r.importance : 0.5,
|
|
195
|
+
created_at: typeof r.created_at === "number" ? r.created_at : Date.now(),
|
|
196
|
+
updated_at: typeof r.updated_at === "number" ? r.updated_at : Date.now(),
|
|
197
|
+
superseded_by: typeof r.superseded_by === "string" ? r.superseded_by : null,
|
|
198
|
+
derived_from_memory_refs: typeof r.derived_from_memory_refs === "string" ? r.derived_from_memory_refs : null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isStringMember<T extends readonly string[]>(value: unknown, allowed: T): value is T[number] {
|
|
203
|
+
return typeof value === "string" && (allowed as readonly string[]).includes(value);
|
|
204
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { RecordRow } from "../db/index.js";
|
|
2
|
+
import type { RankedResult } from "../retrieval/index.js";
|
|
3
|
+
|
|
4
|
+
export type InjectionMode = "auto" | "manual";
|
|
5
|
+
|
|
6
|
+
export const SESSION_TOGGLE_ENTRY = "memory-stone:session-toggle";
|
|
7
|
+
export const INJECTION_MODE_ENTRY = "memory-stone:injection-mode";
|
|
8
|
+
export const MANUAL_INJECTION_ENTRY = "memory-stone:manual-injection";
|
|
9
|
+
|
|
10
|
+
export interface MemorySessionState {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
injectionMode?: InjectionMode;
|
|
13
|
+
manualRefs: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isInjectionMode(value: unknown): value is InjectionMode {
|
|
17
|
+
return value === "auto" || value === "manual";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getMemorySessionState(branch: unknown[]): MemorySessionState {
|
|
21
|
+
let enabled = true;
|
|
22
|
+
let injectionMode: InjectionMode | undefined;
|
|
23
|
+
let manualRefs: string[] = [];
|
|
24
|
+
|
|
25
|
+
for (const entry of branch) {
|
|
26
|
+
if (!isCustomEntry(entry)) continue;
|
|
27
|
+
|
|
28
|
+
if (entry.customType === SESSION_TOGGLE_ENTRY) {
|
|
29
|
+
const data = entry.data as { enabled?: unknown } | undefined;
|
|
30
|
+
if (typeof data?.enabled === "boolean") {
|
|
31
|
+
enabled = data.enabled;
|
|
32
|
+
}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (entry.customType === INJECTION_MODE_ENTRY) {
|
|
37
|
+
const data = entry.data as { mode?: unknown } | undefined;
|
|
38
|
+
if (isInjectionMode(data?.mode)) {
|
|
39
|
+
injectionMode = data.mode;
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (entry.customType === MANUAL_INJECTION_ENTRY) {
|
|
45
|
+
const data = entry.data as { action?: unknown; refs?: unknown } | undefined;
|
|
46
|
+
if (data?.action === "clear") {
|
|
47
|
+
manualRefs = [];
|
|
48
|
+
} else if (data?.action === "add" && Array.isArray(data.refs)) {
|
|
49
|
+
for (const ref of data.refs) {
|
|
50
|
+
if (typeof ref === "string" && ref.trim() && !manualRefs.includes(ref)) {
|
|
51
|
+
manualRefs.push(ref);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { enabled, injectionMode, manualRefs };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parseRefArgs(args: string): string[] {
|
|
62
|
+
return (args ?? "")
|
|
63
|
+
.trim()
|
|
64
|
+
.split(/\s+/)
|
|
65
|
+
.map((part) => part.trim())
|
|
66
|
+
.filter((part) => part.length > 0 && !part.startsWith("--"));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isRecordVisibleInProject(record: RecordRow, currentProjectId: string | null): boolean {
|
|
70
|
+
return record.scope === "global" || record.project_id === null || record.project_id === currentProjectId;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function manualRecordsToRankedResults(
|
|
74
|
+
records: RecordRow[],
|
|
75
|
+
currentProjectId: string | null,
|
|
76
|
+
): RankedResult[] {
|
|
77
|
+
return records
|
|
78
|
+
.filter((record) => record.status === "active" && isRecordVisibleInProject(record, currentProjectId))
|
|
79
|
+
.map((record) => ({
|
|
80
|
+
record,
|
|
81
|
+
score: Number.POSITIVE_INFINITY,
|
|
82
|
+
reasons: ["manual-ref"],
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isCustomEntry(entry: unknown): entry is { type?: string; customType?: string; data?: unknown } {
|
|
87
|
+
return typeof entry === "object" && entry !== null && (entry as { type?: unknown }).type === "custom";
|
|
88
|
+
}
|
package/test/indexing.test.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for incremental session indexing.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, beforeEach, after } from "node:test";
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import { existsSync, unlinkSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { tmpdir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import { indexSessionOnAgentEnd } from "../src/indexing/index.js";
|
|
11
|
-
import { closeDb, getDb, getDbPath } from "../src/db/index.js";
|
|
12
|
-
|
|
13
|
-
const testMemoryDir = mkdtempSync(join(tmpdir(), "pi-memory-stone-indexing-"));
|
|
14
|
-
process.env.PI_MEMORY_STONE_DB_PATH = join(testMemoryDir, "memory.db");
|
|
15
|
-
|
|
16
|
-
function cleanDb() {
|
|
17
|
-
const dbPath = getDbPath();
|
|
18
|
-
closeDb();
|
|
19
|
-
for (const f of [dbPath, dbPath + "-wal", dbPath + "-shm"]) {
|
|
20
|
-
try { if (existsSync(f)) unlinkSync(f); } catch {}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function makeUserEntry(id: string, text: string) {
|
|
25
|
-
return {
|
|
26
|
-
type: "message",
|
|
27
|
-
id,
|
|
28
|
-
parentId: null,
|
|
29
|
-
message: {
|
|
30
|
-
role: "user",
|
|
31
|
-
content: text,
|
|
32
|
-
timestamp: Date.now(),
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function makeAssistantEntry(id: string, text: string) {
|
|
38
|
-
return {
|
|
39
|
-
type: "message",
|
|
40
|
-
id,
|
|
41
|
-
parentId: null,
|
|
42
|
-
message: {
|
|
43
|
-
role: "assistant",
|
|
44
|
-
content: [{ type: "text", text }],
|
|
45
|
-
timestamp: Date.now(),
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function makeContext(sessionFile: string, branch: unknown[]) {
|
|
51
|
-
return {
|
|
52
|
-
cwd: testMemoryDir,
|
|
53
|
-
sessionManager: {
|
|
54
|
-
getSessionFile: () => sessionFile,
|
|
55
|
-
getSessionId: () => "session-1",
|
|
56
|
-
getBranch: () => branch,
|
|
57
|
-
getLeafId: () => (branch.at(-1) as { id?: string } | undefined)?.id,
|
|
58
|
-
},
|
|
59
|
-
} as any;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
describe("indexSessionOnAgentEnd", () => {
|
|
63
|
-
beforeEach(() => cleanDb());
|
|
64
|
-
|
|
65
|
-
after(() => {
|
|
66
|
-
cleanDb();
|
|
67
|
-
rmSync(testMemoryDir, { recursive: true, force: true });
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("indexes only entries after the last indexed entry when timestamps are absent", async () => {
|
|
71
|
-
const sessionFile = join(testMemoryDir, "session.jsonl");
|
|
72
|
-
writeFileSync(sessionFile, "");
|
|
73
|
-
|
|
74
|
-
const firstBranch = [
|
|
75
|
-
makeUserEntry("001", "First question"),
|
|
76
|
-
makeAssistantEntry("002", "First answer"),
|
|
77
|
-
];
|
|
78
|
-
const first = await indexSessionOnAgentEnd(makeContext(sessionFile, firstBranch), {});
|
|
79
|
-
assert.equal(first.errors.length, 0);
|
|
80
|
-
|
|
81
|
-
const fullBranch = [
|
|
82
|
-
...firstBranch,
|
|
83
|
-
makeUserEntry("003", "Second question"),
|
|
84
|
-
makeAssistantEntry("004", "Second answer"),
|
|
85
|
-
];
|
|
86
|
-
const second = await indexSessionOnAgentEnd(makeContext(sessionFile, fullBranch), {});
|
|
87
|
-
assert.equal(second.errors.length, 0);
|
|
88
|
-
|
|
89
|
-
const rows = getDb()
|
|
90
|
-
.prepare("SELECT text FROM records WHERE kind = 'turn_summary' ORDER BY created_at")
|
|
91
|
-
.all() as Array<{ text: string }>;
|
|
92
|
-
|
|
93
|
-
assert.equal(rows.length, 2);
|
|
94
|
-
assert.ok(rows.some((r) => r.text.includes("First question")));
|
|
95
|
-
assert.ok(rows.some((r) => r.text.includes("Second question")));
|
|
96
|
-
});
|
|
97
|
-
});
|