@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.
Files changed (60) hide show
  1. package/README.md +156 -111
  2. package/dist/adapters/ollamaClient.d.ts +11 -0
  3. package/dist/adapters/ollamaClient.d.ts.map +1 -0
  4. package/dist/adapters/ollamaClient.js +122 -0
  5. package/dist/adapters/ollamaClient.js.map +1 -0
  6. package/dist/adapters/openaiCompatClient.d.ts +11 -0
  7. package/dist/adapters/openaiCompatClient.d.ts.map +1 -0
  8. package/dist/adapters/openaiCompatClient.js +118 -0
  9. package/dist/adapters/openaiCompatClient.js.map +1 -0
  10. package/dist/cli.js +2 -2
  11. package/dist/cli.js.map +1 -1
  12. package/dist/fallback/sessionIndex.d.ts.map +1 -1
  13. package/dist/fallback/sessionIndex.js +90 -25
  14. package/dist/fallback/sessionIndex.js.map +1 -1
  15. package/dist/fallback/sessionSearch.d.ts +1 -1
  16. package/dist/fallback/sessionSearch.d.ts.map +1 -1
  17. package/dist/fallback/sessionSearch.js +101 -28
  18. package/dist/fallback/sessionSearch.js.map +1 -1
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/local/graphQuery.d.ts +21 -0
  24. package/dist/local/graphQuery.d.ts.map +1 -0
  25. package/dist/local/graphQuery.js +170 -0
  26. package/dist/local/graphQuery.js.map +1 -0
  27. package/dist/paths.js +1 -1
  28. package/dist/paths.js.map +1 -1
  29. package/dist/pi-extension.d.ts.map +1 -1
  30. package/dist/pi-extension.js +57 -17
  31. package/dist/pi-extension.js.map +1 -1
  32. package/dist/service.d.ts +10 -10
  33. package/dist/service.d.ts.map +1 -1
  34. package/dist/service.js +72 -30
  35. package/dist/service.js.map +1 -1
  36. package/dist/settings.d.ts +38 -0
  37. package/dist/settings.d.ts.map +1 -0
  38. package/dist/settings.js +68 -0
  39. package/dist/settings.js.map +1 -0
  40. package/dist/sidecar/process.d.ts.map +1 -1
  41. package/dist/sidecar/process.js +16 -4
  42. package/dist/sidecar/process.js.map +1 -1
  43. package/dist/trainer/sessionLoader.d.ts +2 -2
  44. package/dist/trainer/sessionLoader.d.ts.map +1 -1
  45. package/dist/trainer/sessionLoader.js +115 -39
  46. package/dist/trainer/sessionLoader.js.map +1 -1
  47. package/package.json +5 -4
  48. package/src/adapters/ollamaClient.ts +179 -0
  49. package/src/adapters/openaiCompatClient.ts +155 -0
  50. package/src/cli.ts +4 -3
  51. package/src/fallback/sessionIndex.ts +78 -40
  52. package/src/fallback/sessionSearch.ts +107 -27
  53. package/src/index.ts +26 -0
  54. package/src/local/graphQuery.ts +228 -0
  55. package/src/paths.ts +1 -1
  56. package/src/pi-extension.ts +79 -17
  57. package/src/service.ts +78 -31
  58. package/src/settings.ts +126 -0
  59. package/src/sidecar/process.ts +19 -4
  60. 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
@@ -24,5 +24,5 @@ export function defaultSocketPath(): string {
24
24
  }
25
25
 
26
26
  export function defaultSessionsDir(): string {
27
- return path.join(defaultPiHome(), "sessions");
27
+ return path.join(defaultPiHome(), "agent", "sessions");
28
28
  }
@@ -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 { defaultMemoryConfig, type MemoryConfig } from "./config.js";
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
- sharedHelper = await resolveMemoryHelperLLM(ctx, pi.getFlag("memory-helper-model"));
70
- sharedLLMClient = createPiLLMClient(ctx, pi.getFlag("memory-helper-model"));
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 cfg = defaultMemoryConfig();
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 (err) {
134
- const message = err instanceof Error ? err.message : String(err);
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: defaultMemoryConfig().sessionsDir,
162
- memoryMdPaths: defaultMemoryConfig().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 = defaultMemoryConfig().memoryMdPaths[0];
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
- * Mode B local memory: spawn tlm sidecar, query via Unix socket.
30
- * No Cloud pullerbundle must exist under bundleRoot/current.
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.serviceStatus = "unavailable";
88
- this.reason = "tlm_binary_missing";
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.signal);
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
- } catch (err) {
99
- this.serviceStatus = "unavailable";
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
- * Start interval-based auto-training. If already running, stops and restarts.
110
- * Uses config.trainer.auto_interval.
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 dbPath = path.join(this.cfg.bundleRoot, "sessions.db");
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" || !this.client) {
211
+ if (this.serviceStatus !== "ready") {
185
212
  return { env: null, errorClass: "unavailable" };
186
213
  }
187
- const timeout = AbortSignal.timeout(this.cfg.queryTimeoutMs);
188
- const combined = signal
189
- ? AbortSignal.any([signal, timeout])
190
- : timeout;
191
- return this.client.query(intent, combined);
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 (!this.client) return null;
196
- try {
197
- return await this.client.health();
198
- } catch {
199
- return null;
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
  }