@chendpoc/pi-memory 0.1.0 → 0.1.11
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 +156 -111
- package/dist/adapters/ollamaClient.d.ts +11 -0
- package/dist/adapters/ollamaClient.d.ts.map +1 -0
- package/dist/adapters/ollamaClient.js +122 -0
- package/dist/adapters/ollamaClient.js.map +1 -0
- package/dist/adapters/openaiCompatClient.d.ts +11 -0
- package/dist/adapters/openaiCompatClient.d.ts.map +1 -0
- package/dist/adapters/openaiCompatClient.js +118 -0
- package/dist/adapters/openaiCompatClient.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/fallback/sessionIndex.d.ts.map +1 -1
- package/dist/fallback/sessionIndex.js +90 -25
- package/dist/fallback/sessionIndex.js.map +1 -1
- package/dist/fallback/sessionSearch.d.ts +1 -1
- package/dist/fallback/sessionSearch.d.ts.map +1 -1
- package/dist/fallback/sessionSearch.js +101 -28
- package/dist/fallback/sessionSearch.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/local/graphQuery.d.ts +21 -0
- package/dist/local/graphQuery.d.ts.map +1 -0
- package/dist/local/graphQuery.js +170 -0
- package/dist/local/graphQuery.js.map +1 -0
- package/dist/paths.js +1 -1
- package/dist/paths.js.map +1 -1
- package/dist/pi-extension.d.ts.map +1 -1
- package/dist/pi-extension.js +57 -17
- package/dist/pi-extension.js.map +1 -1
- package/dist/service.d.ts +10 -10
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +72 -30
- package/dist/service.js.map +1 -1
- package/dist/settings.d.ts +38 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +68 -0
- package/dist/settings.js.map +1 -0
- package/dist/sidecar/process.d.ts.map +1 -1
- package/dist/sidecar/process.js +16 -4
- package/dist/sidecar/process.js.map +1 -1
- package/dist/trainer/sessionLoader.d.ts +2 -2
- package/dist/trainer/sessionLoader.d.ts.map +1 -1
- package/dist/trainer/sessionLoader.js +115 -39
- package/dist/trainer/sessionLoader.js.map +1 -1
- package/package.json +5 -4
- package/src/adapters/ollamaClient.ts +179 -0
- package/src/adapters/openaiCompatClient.ts +155 -0
- package/src/cli.ts +4 -3
- package/src/fallback/sessionIndex.ts +78 -40
- package/src/fallback/sessionSearch.ts +107 -27
- package/src/index.ts +26 -0
- package/src/local/graphQuery.ts +228 -0
- package/src/paths.ts +1 -1
- package/src/pi-extension.ts +79 -17
- package/src/service.ts +78 -31
- package/src/settings.ts +126 -0
- package/src/sidecar/process.ts +19 -4
- package/src/trainer/sessionLoader.ts +128 -42
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
ErrorClass,
|
|
6
|
+
MemoryBlock,
|
|
7
|
+
MemoryCandidateGroup,
|
|
8
|
+
QueryCandidate,
|
|
9
|
+
QueryIntent,
|
|
10
|
+
ResponseEnvelope,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
|
|
13
|
+
interface BundleEntity {
|
|
14
|
+
entity_id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
type: string;
|
|
17
|
+
aliases: string[];
|
|
18
|
+
mention_count: number;
|
|
19
|
+
distinct_session_count: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface BundleEdge {
|
|
23
|
+
head_entity_id: string;
|
|
24
|
+
relation: string;
|
|
25
|
+
tail_entity_id: string;
|
|
26
|
+
supporting_event_ids: string[];
|
|
27
|
+
evidence: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface BundleEvent {
|
|
31
|
+
event_id: string;
|
|
32
|
+
description: string;
|
|
33
|
+
session_id: string;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface GraphData {
|
|
38
|
+
entities: BundleEntity[];
|
|
39
|
+
edges: BundleEdge[];
|
|
40
|
+
events: BundleEvent[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class LocalGraphQuerier {
|
|
44
|
+
private entities: BundleEntity[] = [];
|
|
45
|
+
private edges: BundleEdge[] = [];
|
|
46
|
+
private events: BundleEvent[] = [];
|
|
47
|
+
private entityById = new Map<string, BundleEntity>();
|
|
48
|
+
private entityByLabel = new Map<string, BundleEntity>();
|
|
49
|
+
private loaded = false;
|
|
50
|
+
|
|
51
|
+
constructor(private readonly bundleRoot: string) {}
|
|
52
|
+
|
|
53
|
+
load(): boolean {
|
|
54
|
+
const graphPath = path.join(this.bundleRoot, "current", "graph.json");
|
|
55
|
+
try {
|
|
56
|
+
const raw = fs.readFileSync(graphPath, "utf8");
|
|
57
|
+
const data = JSON.parse(raw) as GraphData;
|
|
58
|
+
this.entities = data.entities ?? [];
|
|
59
|
+
this.edges = data.edges ?? [];
|
|
60
|
+
this.events = data.events ?? [];
|
|
61
|
+
|
|
62
|
+
this.entityById.clear();
|
|
63
|
+
this.entityByLabel.clear();
|
|
64
|
+
for (const e of this.entities) {
|
|
65
|
+
this.entityById.set(e.entity_id, e);
|
|
66
|
+
this.entityByLabel.set(e.label.toLowerCase(), e);
|
|
67
|
+
for (const alias of e.aliases) {
|
|
68
|
+
this.entityByLabel.set(alias.toLowerCase(), e);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.loaded = true;
|
|
72
|
+
return true;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
isLoaded(): boolean {
|
|
79
|
+
return this.loaded;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private findEntity(mention: string): BundleEntity | null {
|
|
83
|
+
const key = mention.toLowerCase().trim();
|
|
84
|
+
return this.entityByLabel.get(key) ?? null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private findEntities(mentions: string[]): BundleEntity[] {
|
|
88
|
+
const found: BundleEntity[] = [];
|
|
89
|
+
for (const m of mentions) {
|
|
90
|
+
const e = this.findEntity(m);
|
|
91
|
+
if (e) found.push(e);
|
|
92
|
+
}
|
|
93
|
+
return found;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
query(intent: QueryIntent): {
|
|
97
|
+
env: ResponseEnvelope | null;
|
|
98
|
+
errorClass: ErrorClass;
|
|
99
|
+
} {
|
|
100
|
+
if (!this.loaded) {
|
|
101
|
+
return { env: null, errorClass: "unavailable" };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const anchors = this.findEntities(intent.anchor_mentions);
|
|
105
|
+
if (anchors.length === 0) {
|
|
106
|
+
return {
|
|
107
|
+
env: this.makeEnvelope(intent, [], [], "no matching entities found"),
|
|
108
|
+
errorClass: "ok",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const limit = intent.result_limit && intent.result_limit > 0 ? intent.result_limit : 10;
|
|
113
|
+
const candidates: QueryCandidate[] = [];
|
|
114
|
+
const groups: MemoryCandidateGroup[] = [];
|
|
115
|
+
|
|
116
|
+
for (const anchor of anchors) {
|
|
117
|
+
const relatedEdges = this.edges.filter((e) => {
|
|
118
|
+
const matchesHead = e.head_entity_id === anchor.entity_id;
|
|
119
|
+
const matchesTail = e.tail_entity_id === anchor.entity_id;
|
|
120
|
+
if (!matchesHead && !matchesTail) return false;
|
|
121
|
+
|
|
122
|
+
if (intent.relation_constraints?.length) {
|
|
123
|
+
return intent.relation_constraints.some((rc) => {
|
|
124
|
+
const clean = rc.replace(/\^-1$/, "").replace(/^\^/, "");
|
|
125
|
+
return e.relation === clean;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
for (const edge of relatedEdges) {
|
|
132
|
+
if (candidates.length >= limit) break;
|
|
133
|
+
const isHead = edge.head_entity_id === anchor.entity_id;
|
|
134
|
+
const otherId = isHead ? edge.tail_entity_id : edge.head_entity_id;
|
|
135
|
+
const other = this.entityById.get(otherId);
|
|
136
|
+
if (!other) continue;
|
|
137
|
+
|
|
138
|
+
if (intent.candidate_type) {
|
|
139
|
+
if (other.type.toLowerCase() !== intent.candidate_type.toLowerCase()) continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const score = other.mention_count + (other.distinct_session_count * 2);
|
|
143
|
+
|
|
144
|
+
candidates.push({
|
|
145
|
+
value: other.label,
|
|
146
|
+
score,
|
|
147
|
+
evidence: edge.evidence.slice(0, 200),
|
|
148
|
+
supporting_event_ids: edge.supporting_event_ids,
|
|
149
|
+
entity_id: other.entity_id,
|
|
150
|
+
scope: `via_${anchor.label}`,
|
|
151
|
+
support_count: other.mention_count,
|
|
152
|
+
distinct_session_count: other.distinct_session_count,
|
|
153
|
+
observed_path: [{
|
|
154
|
+
from_entity_id: anchor.entity_id,
|
|
155
|
+
from_label: anchor.label,
|
|
156
|
+
relation: edge.relation,
|
|
157
|
+
direction: isHead ? "forward" : "inverse",
|
|
158
|
+
to_entity_id: other.entity_id,
|
|
159
|
+
to_label: other.label,
|
|
160
|
+
supporting_event_ids: edge.supporting_event_ids,
|
|
161
|
+
}],
|
|
162
|
+
path_collision_count: 0,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
groups.push({
|
|
166
|
+
value: other.label,
|
|
167
|
+
score,
|
|
168
|
+
evidence: edge.evidence.slice(0, 200),
|
|
169
|
+
support_count: other.mention_count,
|
|
170
|
+
supporting_event_ids: edge.supporting_event_ids,
|
|
171
|
+
entity_ids: [other.entity_id],
|
|
172
|
+
scopes: [`via_${anchor.label}`],
|
|
173
|
+
via_relations: [edge.relation],
|
|
174
|
+
via_anchor_entity_ids: [anchor.entity_id],
|
|
175
|
+
observed_path: [{
|
|
176
|
+
from_entity_id: anchor.entity_id,
|
|
177
|
+
from_label: anchor.label,
|
|
178
|
+
relation: edge.relation,
|
|
179
|
+
direction: isHead ? "forward" : "inverse",
|
|
180
|
+
to_entity_id: other.entity_id,
|
|
181
|
+
to_label: other.label,
|
|
182
|
+
supporting_event_ids: edge.supporting_event_ids,
|
|
183
|
+
}],
|
|
184
|
+
path_collision_count: 0,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
190
|
+
groups.sort((a, b) => b.score - a.score);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
env: this.makeEnvelope(intent, candidates.slice(0, limit), groups.slice(0, limit)),
|
|
194
|
+
errorClass: "ok",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private makeEnvelope(
|
|
199
|
+
intent: QueryIntent,
|
|
200
|
+
candidates: QueryCandidate[],
|
|
201
|
+
groups: MemoryCandidateGroup[],
|
|
202
|
+
noDataReason?: string,
|
|
203
|
+
): ResponseEnvelope {
|
|
204
|
+
const manifestPath = path.join(this.bundleRoot, "current", "manifest.json");
|
|
205
|
+
let bundleVersion: string | undefined;
|
|
206
|
+
try {
|
|
207
|
+
const m = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as { bundle_version?: string };
|
|
208
|
+
bundleVersion = m.bundle_version;
|
|
209
|
+
} catch { /* ok */ }
|
|
210
|
+
|
|
211
|
+
const memoryBlock: MemoryBlock = {
|
|
212
|
+
groups,
|
|
213
|
+
notes: [],
|
|
214
|
+
...(noDataReason ? { no_data_reason: noDataReason } : {}),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
protocol_version: 1,
|
|
219
|
+
bundle_version: bundleVersion,
|
|
220
|
+
request_id: `local-${Date.now()}`,
|
|
221
|
+
candidates,
|
|
222
|
+
memory_block: memoryBlock,
|
|
223
|
+
warnings: [],
|
|
224
|
+
reason: candidates.length > 0 ? "ok" : "no_data",
|
|
225
|
+
latency_ms: 0,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
package/src/paths.ts
CHANGED
package/src/pi-extension.ts
CHANGED
|
@@ -5,13 +5,29 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
5
5
|
import { truncateHead } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
createOllamaLLMClient,
|
|
10
|
+
createOllamaMemoryHelper,
|
|
11
|
+
ollamaHealthCheck,
|
|
12
|
+
type OllamaConfig,
|
|
13
|
+
} from "./adapters/ollamaClient.js";
|
|
14
|
+
import {
|
|
15
|
+
createOpenAICompatLLMClient,
|
|
16
|
+
createOpenAICompatMemoryHelper,
|
|
17
|
+
openaiCompatHealthCheck,
|
|
18
|
+
type OpenAICompatConfig,
|
|
19
|
+
} from "./adapters/openaiCompatClient.js";
|
|
8
20
|
import {
|
|
9
21
|
createPiLLMClient,
|
|
10
22
|
DEFAULT_HELPER_MODEL,
|
|
11
23
|
DEFAULT_HELPER_PROVIDER,
|
|
12
24
|
resolveMemoryHelperLLM,
|
|
13
25
|
} from "./adapters/piComplete.js";
|
|
14
|
-
import {
|
|
26
|
+
import type { MemoryConfig } from "./config.js";
|
|
27
|
+
import {
|
|
28
|
+
loadMemorySettings,
|
|
29
|
+
resolveHelperModelSpec,
|
|
30
|
+
} from "./settings.js";
|
|
15
31
|
import { createFallbackQuery } from "./fallback/index.js";
|
|
16
32
|
import type { RerankOptions } from "./fallback/llmRerank.js";
|
|
17
33
|
import type { MemoryHelperLLM } from "./preflight/detectIntents.js";
|
|
@@ -57,6 +73,9 @@ const RECALL_MAX_BYTES = 32_000;
|
|
|
57
73
|
|
|
58
74
|
let sharedService: MemoryService | null = null;
|
|
59
75
|
let sessionCfg: MemoryConfig | null = null;
|
|
76
|
+
let settingsHelperModel: string | undefined;
|
|
77
|
+
let settingsOllama: OllamaConfig | null = null;
|
|
78
|
+
let settingsVllm: OpenAICompatConfig | null = null;
|
|
60
79
|
let sharedHelper: MemoryHelperLLM | null = null;
|
|
61
80
|
let sharedLLMClient: LLMClient | null = null;
|
|
62
81
|
let preflightCache: { userText: string; privateContext: string } | null = null;
|
|
@@ -65,9 +84,37 @@ export function getSharedMemoryService(): MemoryService | null {
|
|
|
65
84
|
return sharedService;
|
|
66
85
|
}
|
|
67
86
|
|
|
87
|
+
function getHelperModelSpec(pi: ExtensionAPI): string | undefined {
|
|
88
|
+
return resolveHelperModelSpec(pi.getFlag("memory-helper-model"), settingsHelperModel);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isOllamaModel(spec: string | undefined): boolean {
|
|
92
|
+
return !!spec?.startsWith("ollama/");
|
|
93
|
+
}
|
|
94
|
+
|
|
68
95
|
async function refreshMemoryHelper(ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
|
69
|
-
|
|
70
|
-
|
|
96
|
+
const helperModel = getHelperModelSpec(pi);
|
|
97
|
+
|
|
98
|
+
if (settingsVllm) {
|
|
99
|
+
const vllmOk = await openaiCompatHealthCheck(settingsVllm.baseUrl);
|
|
100
|
+
if (vllmOk) {
|
|
101
|
+
sharedHelper = createOpenAICompatMemoryHelper(settingsVllm);
|
|
102
|
+
sharedLLMClient = createOpenAICompatLLMClient(settingsVllm);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if ((isOllamaModel(helperModel) || settingsOllama) && settingsOllama) {
|
|
108
|
+
const ollamaOk = await ollamaHealthCheck(settingsOllama.baseUrl);
|
|
109
|
+
if (ollamaOk) {
|
|
110
|
+
sharedHelper = createOllamaMemoryHelper(settingsOllama);
|
|
111
|
+
sharedLLMClient = createOllamaLLMClient(settingsOllama);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
sharedHelper = await resolveMemoryHelperLLM(ctx, helperModel);
|
|
117
|
+
sharedLLMClient = createPiLLMClient(ctx, helperModel);
|
|
71
118
|
}
|
|
72
119
|
|
|
73
120
|
function getRerankOpts(): RerankOptions | null {
|
|
@@ -100,7 +147,13 @@ function formatMemoryStatus(service: MemoryService): string {
|
|
|
100
147
|
const snap = service.getStatus();
|
|
101
148
|
const lines = [
|
|
102
149
|
`status: ${snap.status}`,
|
|
150
|
+
snap.mode ? `mode: ${snap.mode}` : null,
|
|
103
151
|
snap.reason ? `reason: ${snap.reason}` : null,
|
|
152
|
+
sharedHelper
|
|
153
|
+
? `helper: ${settingsVllm ? `vllm/${settingsVllm.model}` : settingsOllama ? `ollama/${settingsOllama.model}` : (settingsHelperModel ?? "pi-ai")}`
|
|
154
|
+
: "helper: none (regex only)",
|
|
155
|
+
settingsVllm ? `vllm: ${settingsVllm.baseUrl} (${settingsVllm.model})` : null,
|
|
156
|
+
settingsOllama ? `ollama: ${settingsOllama.baseUrl} (${settingsOllama.model})` : null,
|
|
104
157
|
snap.health ? `health: ${JSON.stringify(snap.health)}` : null,
|
|
105
158
|
].filter(Boolean);
|
|
106
159
|
return lines.join("\n");
|
|
@@ -113,7 +166,11 @@ export default function piMemoryExtension(pi: ExtensionAPI): void {
|
|
|
113
166
|
});
|
|
114
167
|
|
|
115
168
|
pi.on("session_start", async (_event, ctx) => {
|
|
116
|
-
const
|
|
169
|
+
const loaded = loadMemorySettings();
|
|
170
|
+
const cfg = loaded.config;
|
|
171
|
+
settingsHelperModel = loaded.helperModel;
|
|
172
|
+
settingsOllama = loaded.ollama;
|
|
173
|
+
settingsVllm = loaded.vllm;
|
|
117
174
|
sessionCfg = cfg;
|
|
118
175
|
preflightCache = null;
|
|
119
176
|
sharedHelper = null;
|
|
@@ -123,18 +180,15 @@ export default function piMemoryExtension(pi: ExtensionAPI): void {
|
|
|
123
180
|
const service = new MemoryService(cfg);
|
|
124
181
|
sharedService = service;
|
|
125
182
|
|
|
183
|
+
await service.start();
|
|
184
|
+
service.startSessionIndex();
|
|
185
|
+
if (cfg.trainer.auto_interval) {
|
|
186
|
+
service.startAutoTrainer();
|
|
187
|
+
}
|
|
126
188
|
try {
|
|
127
|
-
await service.start();
|
|
128
|
-
service.startSessionIndex();
|
|
129
|
-
if (cfg.trainer.auto_interval) {
|
|
130
|
-
service.startAutoTrainer();
|
|
131
|
-
}
|
|
132
189
|
await refreshMemoryHelper(ctx, pi);
|
|
133
|
-
} catch
|
|
134
|
-
|
|
135
|
-
if (ctx.hasUI) {
|
|
136
|
-
ctx.ui.notify(`pi-memory: sidecar start failed (${message}) — fallback mode active`, "warning");
|
|
137
|
-
}
|
|
190
|
+
} catch {
|
|
191
|
+
/* helper unavailable — regex-only preflight */
|
|
138
192
|
}
|
|
139
193
|
});
|
|
140
194
|
|
|
@@ -144,6 +198,9 @@ export default function piMemoryExtension(pi: ExtensionAPI): void {
|
|
|
144
198
|
sharedService = null;
|
|
145
199
|
}
|
|
146
200
|
sessionCfg = null;
|
|
201
|
+
settingsHelperModel = undefined;
|
|
202
|
+
settingsOllama = null;
|
|
203
|
+
settingsVllm = null;
|
|
147
204
|
sharedHelper = null;
|
|
148
205
|
sharedLLMClient = null;
|
|
149
206
|
preflightCache = null;
|
|
@@ -157,9 +214,14 @@ export default function piMemoryExtension(pi: ExtensionAPI): void {
|
|
|
157
214
|
preflightCache = null;
|
|
158
215
|
});
|
|
159
216
|
|
|
217
|
+
const initialSettings = loadMemorySettings();
|
|
218
|
+
settingsHelperModel = initialSettings.helperModel;
|
|
219
|
+
settingsOllama = initialSettings.ollama;
|
|
220
|
+
settingsVllm = initialSettings.vllm;
|
|
221
|
+
|
|
160
222
|
const fallback = createFallbackQuery({
|
|
161
|
-
sessionsDir:
|
|
162
|
-
memoryMdPaths:
|
|
223
|
+
sessionsDir: initialSettings.config.sessionsDir,
|
|
224
|
+
memoryMdPaths: initialSettings.config.memoryMdPaths,
|
|
163
225
|
});
|
|
164
226
|
|
|
165
227
|
pi.registerCommand("memory", {
|
|
@@ -208,7 +270,7 @@ export default function piMemoryExtension(pi: ExtensionAPI): void {
|
|
|
208
270
|
},
|
|
209
271
|
});
|
|
210
272
|
|
|
211
|
-
const memoryMdPath =
|
|
273
|
+
const memoryMdPath = initialSettings.config.memoryMdPaths[0];
|
|
212
274
|
if (memoryMdPath) {
|
|
213
275
|
const appendTool = createMemoryAppendTool(memoryMdPath);
|
|
214
276
|
pi.registerTool({
|
package/src/service.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import type { MemoryConfig } from "./config.js";
|
|
4
|
+
import { LocalGraphQuerier } from "./local/graphQuery.js";
|
|
3
5
|
import { currentBundleReadable } from "./sidecar/bundle.js";
|
|
4
6
|
import { SidecarClient } from "./sidecar/client.js";
|
|
5
7
|
import { SidecarProcess } from "./sidecar/process.js";
|
|
@@ -16,6 +18,7 @@ import type {
|
|
|
16
18
|
export interface MemoryServiceStatus {
|
|
17
19
|
status: ServiceStatus;
|
|
18
20
|
reason?: string;
|
|
21
|
+
mode?: "sidecar" | "local_graph";
|
|
19
22
|
health?: HealthPayload | null;
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -26,14 +29,19 @@ export interface QueryBatchResult {
|
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
32
|
+
* Local memory service with two query backends:
|
|
33
|
+
* 1. tlm sidecar (Unix socket) — when tlm binary is available
|
|
34
|
+
* 2. LocalGraphQuerier — direct graph.json query when tlm is missing
|
|
35
|
+
*
|
|
36
|
+
* Both require a bundle at bundleRoot/current.
|
|
31
37
|
*/
|
|
32
38
|
export class MemoryService {
|
|
33
39
|
private serviceStatus: ServiceStatus = "disabled";
|
|
34
40
|
private reason = "";
|
|
41
|
+
private mode: "sidecar" | "local_graph" | null = null;
|
|
35
42
|
private process: SidecarProcess | null = null;
|
|
36
43
|
private client: SidecarClient | null = null;
|
|
44
|
+
private localQuerier: LocalGraphQuerier | null = null;
|
|
37
45
|
private abort: AbortController | null = null;
|
|
38
46
|
private scheduler: TrainScheduler | null = null;
|
|
39
47
|
private sessionIndex: SessionIndex | null = null;
|
|
@@ -52,6 +60,7 @@ export class MemoryService {
|
|
|
52
60
|
return {
|
|
53
61
|
status: this.serviceStatus,
|
|
54
62
|
reason: this.reason || undefined,
|
|
63
|
+
mode: this.mode ?? undefined,
|
|
55
64
|
};
|
|
56
65
|
}
|
|
57
66
|
|
|
@@ -79,36 +88,50 @@ export class MemoryService {
|
|
|
79
88
|
|
|
80
89
|
this.serviceStatus = "initializing";
|
|
81
90
|
this.abort = new AbortController();
|
|
82
|
-
this.process = new SidecarProcess(this.cfg);
|
|
83
91
|
|
|
92
|
+
if (await this.trySidecar()) return;
|
|
93
|
+
|
|
94
|
+
if (this.tryLocalGraph()) return;
|
|
95
|
+
|
|
96
|
+
this.serviceStatus = "unavailable";
|
|
97
|
+
this.reason = "no_query_backend";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async trySidecar(): Promise<boolean> {
|
|
101
|
+
this.process = new SidecarProcess(this.cfg);
|
|
84
102
|
try {
|
|
85
103
|
await this.process.resolveBinary();
|
|
86
104
|
} catch {
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
return;
|
|
105
|
+
this.process = null;
|
|
106
|
+
return false;
|
|
90
107
|
}
|
|
91
108
|
|
|
92
109
|
try {
|
|
93
110
|
await this.process.spawn();
|
|
94
|
-
await this.process.waitReady(this.abort
|
|
111
|
+
await this.process.waitReady(this.abort!.signal);
|
|
95
112
|
this.client = this.process.getClient();
|
|
96
113
|
this.serviceStatus = "ready";
|
|
114
|
+
this.mode = "sidecar";
|
|
97
115
|
this.reason = "";
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.reason =
|
|
101
|
-
err instanceof Error ? err.message : "sidecar_startup_failed";
|
|
116
|
+
return true;
|
|
117
|
+
} catch {
|
|
102
118
|
await this.process.stop();
|
|
103
119
|
this.process = null;
|
|
104
120
|
this.client = null;
|
|
121
|
+
return false;
|
|
105
122
|
}
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
private tryLocalGraph(): boolean {
|
|
126
|
+
const querier = new LocalGraphQuerier(this.cfg.bundleRoot);
|
|
127
|
+
if (!querier.load()) return false;
|
|
128
|
+
this.localQuerier = querier;
|
|
129
|
+
this.serviceStatus = "ready";
|
|
130
|
+
this.mode = "local_graph";
|
|
131
|
+
this.reason = "";
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
112
135
|
startAutoTrainer(logger?: (log: SchedulerLog) => void): void {
|
|
113
136
|
this.scheduler?.stop();
|
|
114
137
|
this.scheduler = createTrainScheduler(
|
|
@@ -123,12 +146,14 @@ export class MemoryService {
|
|
|
123
146
|
);
|
|
124
147
|
}
|
|
125
148
|
|
|
126
|
-
/**
|
|
127
|
-
* Trigger incremental session index build in the background (non-blocking).
|
|
128
|
-
* Opens (or creates) the SQLite FTS5 DB at ~/.pi/memory/sessions.db.
|
|
129
|
-
*/
|
|
130
149
|
startSessionIndex(): void {
|
|
131
|
-
const
|
|
150
|
+
const dbDir = this.cfg.bundleRoot;
|
|
151
|
+
try {
|
|
152
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
153
|
+
} catch {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const dbPath = path.join(dbDir, "sessions.db");
|
|
132
157
|
const idx = openSessionIndex(dbPath);
|
|
133
158
|
if (!idx) return;
|
|
134
159
|
this.sessionIndex = idx;
|
|
@@ -148,6 +173,8 @@ export class MemoryService {
|
|
|
148
173
|
await this.process?.stop();
|
|
149
174
|
this.process = null;
|
|
150
175
|
this.client = null;
|
|
176
|
+
this.localQuerier = null;
|
|
177
|
+
this.mode = null;
|
|
151
178
|
if (this.cfg.provider === "disabled") {
|
|
152
179
|
this.serviceStatus = "disabled";
|
|
153
180
|
} else {
|
|
@@ -181,22 +208,42 @@ export class MemoryService {
|
|
|
181
208
|
errorClass: ErrorClass;
|
|
182
209
|
transportError?: Error;
|
|
183
210
|
}> {
|
|
184
|
-
if (this.serviceStatus !== "ready"
|
|
211
|
+
if (this.serviceStatus !== "ready") {
|
|
185
212
|
return { env: null, errorClass: "unavailable" };
|
|
186
213
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
214
|
+
|
|
215
|
+
if (this.client && this.mode === "sidecar") {
|
|
216
|
+
const timeout = AbortSignal.timeout(this.cfg.queryTimeoutMs);
|
|
217
|
+
const combined = signal
|
|
218
|
+
? AbortSignal.any([signal, timeout])
|
|
219
|
+
: timeout;
|
|
220
|
+
return this.client.query(intent, combined);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (this.localQuerier && this.mode === "local_graph") {
|
|
224
|
+
return this.localQuerier.query(intent);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { env: null, errorClass: "unavailable" };
|
|
192
228
|
}
|
|
193
229
|
|
|
194
230
|
async health(): Promise<HealthPayload | null> {
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
231
|
+
if (this.client) {
|
|
232
|
+
try {
|
|
233
|
+
return await this.client.health();
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (this.localQuerier) {
|
|
239
|
+
return {
|
|
240
|
+
ready: true,
|
|
241
|
+
compatibility: "local_graph",
|
|
242
|
+
protocol_version: 1,
|
|
243
|
+
uptime_secs: 0,
|
|
244
|
+
status_message: `local graph query (${this.localQuerier.isLoaded() ? "loaded" : "not loaded"})`,
|
|
245
|
+
};
|
|
200
246
|
}
|
|
247
|
+
return null;
|
|
201
248
|
}
|
|
202
249
|
}
|