@chendpoc/pi-memory 0.1.0 → 0.1.12
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 +8 -4
- package/src/adapters/ollamaClient.ts +179 -0
- package/src/adapters/openaiCompatClient.ts +155 -0
- package/src/cache/memoryCaches.ts +72 -0
- package/src/cli.ts +4 -3
- package/src/fallback/llmRerank.ts +8 -1
- package/src/fallback/sessionIndex.ts +78 -40
- package/src/fallback/sessionSearch.ts +107 -27
- package/src/index.ts +28 -0
- package/src/local/graphQuery.ts +252 -0
- package/src/paths.ts +1 -1
- package/src/pi-extension.ts +164 -36
- package/src/preflight/detectIntents.ts +6 -0
- package/src/preflight/hook.ts +68 -5
- package/src/preflight/render.ts +28 -3
- package/src/service.ts +133 -29
- package/src/settings.ts +126 -0
- package/src/sidecar/process.ts +19 -4
- package/src/tools/memoryRecall.ts +33 -9
- package/src/trainer/scheduler.ts +3 -0
- package/src/trainer/sessionLoader.ts +128 -42
package/src/service.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import type { MemoryConfig } from "./config.js";
|
|
4
|
+
import { invalidateMemoryCaches } from "./cache/memoryCaches.js";
|
|
5
|
+
import { LocalGraphQuerier } from "./local/graphQuery.js";
|
|
3
6
|
import { currentBundleReadable } from "./sidecar/bundle.js";
|
|
4
7
|
import { SidecarClient } from "./sidecar/client.js";
|
|
5
8
|
import { SidecarProcess } from "./sidecar/process.js";
|
|
@@ -16,6 +19,7 @@ import type {
|
|
|
16
19
|
export interface MemoryServiceStatus {
|
|
17
20
|
status: ServiceStatus;
|
|
18
21
|
reason?: string;
|
|
22
|
+
mode?: "sidecar" | "local_graph";
|
|
19
23
|
health?: HealthPayload | null;
|
|
20
24
|
}
|
|
21
25
|
|
|
@@ -26,17 +30,25 @@ export interface QueryBatchResult {
|
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
33
|
+
* Local memory service with two query backends:
|
|
34
|
+
* 1. tlm sidecar (Unix socket) — when tlm binary is available
|
|
35
|
+
* 2. LocalGraphQuerier — direct graph.json query when tlm is missing
|
|
36
|
+
*
|
|
37
|
+
* Both require a bundle at bundleRoot/current.
|
|
31
38
|
*/
|
|
32
39
|
export class MemoryService {
|
|
33
40
|
private serviceStatus: ServiceStatus = "disabled";
|
|
34
41
|
private reason = "";
|
|
42
|
+
private mode: "sidecar" | "local_graph" | null = null;
|
|
35
43
|
private process: SidecarProcess | null = null;
|
|
36
44
|
private client: SidecarClient | null = null;
|
|
45
|
+
private localQuerier: LocalGraphQuerier | null = null;
|
|
37
46
|
private abort: AbortController | null = null;
|
|
38
47
|
private scheduler: TrainScheduler | null = null;
|
|
39
48
|
private sessionIndex: SessionIndex | null = null;
|
|
49
|
+
private bundleMtimes: { graph: number; manifest: number } | null = null;
|
|
50
|
+
private lastBundleCheckMs = 0;
|
|
51
|
+
private static readonly BUNDLE_CHECK_INTERVAL_MS = 5_000;
|
|
40
52
|
|
|
41
53
|
constructor(private cfg: MemoryConfig) {}
|
|
42
54
|
|
|
@@ -52,6 +64,7 @@ export class MemoryService {
|
|
|
52
64
|
return {
|
|
53
65
|
status: this.serviceStatus,
|
|
54
66
|
reason: this.reason || undefined,
|
|
67
|
+
mode: this.mode ?? undefined,
|
|
55
68
|
};
|
|
56
69
|
}
|
|
57
70
|
|
|
@@ -79,36 +92,50 @@ export class MemoryService {
|
|
|
79
92
|
|
|
80
93
|
this.serviceStatus = "initializing";
|
|
81
94
|
this.abort = new AbortController();
|
|
82
|
-
this.process = new SidecarProcess(this.cfg);
|
|
83
95
|
|
|
96
|
+
if (await this.trySidecar()) return;
|
|
97
|
+
|
|
98
|
+
if (this.tryLocalGraph()) return;
|
|
99
|
+
|
|
100
|
+
this.serviceStatus = "unavailable";
|
|
101
|
+
this.reason = "no_query_backend";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private async trySidecar(): Promise<boolean> {
|
|
105
|
+
this.process = new SidecarProcess(this.cfg);
|
|
84
106
|
try {
|
|
85
107
|
await this.process.resolveBinary();
|
|
86
108
|
} catch {
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
return;
|
|
109
|
+
this.process = null;
|
|
110
|
+
return false;
|
|
90
111
|
}
|
|
91
112
|
|
|
92
113
|
try {
|
|
93
114
|
await this.process.spawn();
|
|
94
|
-
await this.process.waitReady(this.abort
|
|
115
|
+
await this.process.waitReady(this.abort!.signal);
|
|
95
116
|
this.client = this.process.getClient();
|
|
96
117
|
this.serviceStatus = "ready";
|
|
118
|
+
this.mode = "sidecar";
|
|
97
119
|
this.reason = "";
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.reason =
|
|
101
|
-
err instanceof Error ? err.message : "sidecar_startup_failed";
|
|
120
|
+
return true;
|
|
121
|
+
} catch {
|
|
102
122
|
await this.process.stop();
|
|
103
123
|
this.process = null;
|
|
104
124
|
this.client = null;
|
|
125
|
+
return false;
|
|
105
126
|
}
|
|
106
127
|
}
|
|
107
128
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
private tryLocalGraph(): boolean {
|
|
130
|
+
const querier = new LocalGraphQuerier(this.cfg.bundleRoot);
|
|
131
|
+
if (!querier.load()) return false;
|
|
132
|
+
this.localQuerier = querier;
|
|
133
|
+
this.serviceStatus = "ready";
|
|
134
|
+
this.mode = "local_graph";
|
|
135
|
+
this.reason = "";
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
112
139
|
startAutoTrainer(logger?: (log: SchedulerLog) => void): void {
|
|
113
140
|
this.scheduler?.stop();
|
|
114
141
|
this.scheduler = createTrainScheduler(
|
|
@@ -118,17 +145,71 @@ export class MemoryService {
|
|
|
118
145
|
sessionsDir: this.cfg.sessionsDir,
|
|
119
146
|
bundleRoot: this.cfg.bundleRoot,
|
|
120
147
|
},
|
|
148
|
+
onSuccess: () => { void this.notifyBundleUpdated(); },
|
|
121
149
|
},
|
|
122
150
|
logger,
|
|
123
151
|
);
|
|
124
152
|
}
|
|
125
153
|
|
|
126
154
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
155
|
+
* Check if the on-disk bundle has changed and hot-reload if needed.
|
|
156
|
+
* Debounced to at most once per BUNDLE_CHECK_INTERVAL_MS to avoid excess stat calls.
|
|
157
|
+
* For local_graph, reloads LocalGraphQuerier in-process.
|
|
158
|
+
* For sidecar, issues a /bundle/reload request.
|
|
159
|
+
* Invalidates all memory caches on successful reload.
|
|
160
|
+
*/
|
|
161
|
+
async ensureFreshBundle(): Promise<void> {
|
|
162
|
+
if (this.serviceStatus !== "ready") return;
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
if (now - this.lastBundleCheckMs < MemoryService.BUNDLE_CHECK_INTERVAL_MS) return;
|
|
165
|
+
this.lastBundleCheckMs = now;
|
|
166
|
+
|
|
167
|
+
if (this.mode === "local_graph" && this.localQuerier) {
|
|
168
|
+
const reloaded = this.localQuerier.reloadIfStale();
|
|
169
|
+
if (reloaded) invalidateMemoryCaches();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (this.mode === "sidecar" && this.client) {
|
|
174
|
+
const graphPath = path.join(this.cfg.bundleRoot, "current", "graph.json");
|
|
175
|
+
const manifestPath = path.join(this.cfg.bundleRoot, "current", "manifest.json");
|
|
176
|
+
try {
|
|
177
|
+
const graphMtime = fs.statSync(graphPath).mtimeMs;
|
|
178
|
+
const manifestMtime = fs.statSync(manifestPath).mtimeMs;
|
|
179
|
+
if (!this.bundleMtimes) {
|
|
180
|
+
this.bundleMtimes = { graph: graphMtime, manifest: manifestMtime };
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const changed =
|
|
184
|
+
graphMtime !== this.bundleMtimes.graph ||
|
|
185
|
+
manifestMtime !== this.bundleMtimes.manifest;
|
|
186
|
+
if (!changed) return;
|
|
187
|
+
this.bundleMtimes = { graph: graphMtime, manifest: manifestMtime };
|
|
188
|
+
await this.client.reload();
|
|
189
|
+
invalidateMemoryCaches();
|
|
190
|
+
} catch {
|
|
191
|
+
/* stat or reload failure — continue with current bundle */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Force an immediate bundle freshness check (bypasses the debounce).
|
|
198
|
+
* Called by the auto-trainer scheduler after a successful train run.
|
|
129
199
|
*/
|
|
200
|
+
async notifyBundleUpdated(): Promise<void> {
|
|
201
|
+
this.lastBundleCheckMs = 0;
|
|
202
|
+
await this.ensureFreshBundle();
|
|
203
|
+
}
|
|
204
|
+
|
|
130
205
|
startSessionIndex(): void {
|
|
131
|
-
const
|
|
206
|
+
const dbDir = this.cfg.bundleRoot;
|
|
207
|
+
try {
|
|
208
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
209
|
+
} catch {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const dbPath = path.join(dbDir, "sessions.db");
|
|
132
213
|
const idx = openSessionIndex(dbPath);
|
|
133
214
|
if (!idx) return;
|
|
134
215
|
this.sessionIndex = idx;
|
|
@@ -148,6 +229,8 @@ export class MemoryService {
|
|
|
148
229
|
await this.process?.stop();
|
|
149
230
|
this.process = null;
|
|
150
231
|
this.client = null;
|
|
232
|
+
this.localQuerier = null;
|
|
233
|
+
this.mode = null;
|
|
151
234
|
if (this.cfg.provider === "disabled") {
|
|
152
235
|
this.serviceStatus = "disabled";
|
|
153
236
|
} else {
|
|
@@ -181,22 +264,43 @@ export class MemoryService {
|
|
|
181
264
|
errorClass: ErrorClass;
|
|
182
265
|
transportError?: Error;
|
|
183
266
|
}> {
|
|
184
|
-
if (this.serviceStatus !== "ready"
|
|
267
|
+
if (this.serviceStatus !== "ready") {
|
|
185
268
|
return { env: null, errorClass: "unavailable" };
|
|
186
269
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
270
|
+
await this.ensureFreshBundle();
|
|
271
|
+
|
|
272
|
+
if (this.client && this.mode === "sidecar") {
|
|
273
|
+
const timeout = AbortSignal.timeout(this.cfg.queryTimeoutMs);
|
|
274
|
+
const combined = signal
|
|
275
|
+
? AbortSignal.any([signal, timeout])
|
|
276
|
+
: timeout;
|
|
277
|
+
return this.client.query(intent, combined);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (this.localQuerier && this.mode === "local_graph") {
|
|
281
|
+
return this.localQuerier.query(intent);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { env: null, errorClass: "unavailable" };
|
|
192
285
|
}
|
|
193
286
|
|
|
194
287
|
async health(): Promise<HealthPayload | null> {
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
288
|
+
if (this.client) {
|
|
289
|
+
try {
|
|
290
|
+
return await this.client.health();
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (this.localQuerier) {
|
|
296
|
+
return {
|
|
297
|
+
ready: true,
|
|
298
|
+
compatibility: "local_graph",
|
|
299
|
+
protocol_version: 1,
|
|
300
|
+
uptime_secs: 0,
|
|
301
|
+
status_message: `local graph query (${this.localQuerier.isLoaded() ? "loaded" : "not loaded"})`,
|
|
302
|
+
};
|
|
200
303
|
}
|
|
304
|
+
return null;
|
|
201
305
|
}
|
|
202
306
|
}
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
defaultMemoryConfig,
|
|
8
|
+
normalizeMemoryConfig,
|
|
9
|
+
type ExtractorType,
|
|
10
|
+
type MemoryConfig,
|
|
11
|
+
type MemoryProvider,
|
|
12
|
+
type TrainerConfig,
|
|
13
|
+
} from "./config.js";
|
|
14
|
+
import type { OllamaConfig } from "./adapters/ollamaClient.js";
|
|
15
|
+
import type { OpenAICompatConfig } from "./adapters/openaiCompatClient.js";
|
|
16
|
+
|
|
17
|
+
export interface MemorySettingsFile {
|
|
18
|
+
provider?: MemoryProvider;
|
|
19
|
+
tlmPath?: string;
|
|
20
|
+
socketPath?: string;
|
|
21
|
+
bundleRoot?: string;
|
|
22
|
+
sidecarReadyTimeoutMs?: number;
|
|
23
|
+
queryTimeoutMs?: number;
|
|
24
|
+
clientRequestTimeoutMs?: number;
|
|
25
|
+
sessionsDir?: string;
|
|
26
|
+
memoryMdPaths?: string[];
|
|
27
|
+
/** LLM for intent detection, rerank, and LLM extraction. Overrides default helper model. */
|
|
28
|
+
helperModel?: string;
|
|
29
|
+
/** Ollama local API config for helper / trainer / rerank. */
|
|
30
|
+
ollama?: Partial<OllamaConfig>;
|
|
31
|
+
/** OpenAI-compatible endpoint config (vLLM, SGLang, LM Studio, etc.) */
|
|
32
|
+
vllm?: Partial<OpenAICompatConfig>;
|
|
33
|
+
trainer?: Partial<TrainerConfig>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LoadedMemorySettings {
|
|
37
|
+
config: MemoryConfig;
|
|
38
|
+
helperModel: string | undefined;
|
|
39
|
+
ollama: OllamaConfig | null;
|
|
40
|
+
vllm: OpenAICompatConfig | null;
|
|
41
|
+
configPath: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Default path: ~/.pi/agent/memory.json */
|
|
45
|
+
export function defaultMemoryConfigPath(): string {
|
|
46
|
+
return path.join(getAgentDir(), "memory.json");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readMemorySettingsFile(configPath = defaultMemoryConfigPath()): MemorySettingsFile {
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
52
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
53
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
return parsed as MemorySettingsFile;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Failed to load pi-memory config from ${configPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Load pi-memory config from ~/.pi/agent/memory.json. */
|
|
68
|
+
export function loadMemorySettings(
|
|
69
|
+
overrides: Partial<MemoryConfig> = {},
|
|
70
|
+
configPath = defaultMemoryConfigPath(),
|
|
71
|
+
): LoadedMemorySettings {
|
|
72
|
+
const fileSettings = readMemorySettingsFile(configPath);
|
|
73
|
+
const { helperModel, ollama, vllm, trainer, ...configFields } = fileSettings;
|
|
74
|
+
|
|
75
|
+
const config = normalizeMemoryConfig({
|
|
76
|
+
...configFields,
|
|
77
|
+
...(trainer ? { trainer } : {}),
|
|
78
|
+
...overrides,
|
|
79
|
+
} as Partial<MemoryConfig> & Record<string, unknown>);
|
|
80
|
+
|
|
81
|
+
let resolvedOllama: OllamaConfig | null = null;
|
|
82
|
+
if (ollama && ollama.model) {
|
|
83
|
+
resolvedOllama = {
|
|
84
|
+
baseUrl: ollama.baseUrl?.trim() || "http://localhost:11434",
|
|
85
|
+
model: ollama.model.trim(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let resolvedVllm: OpenAICompatConfig | null = null;
|
|
90
|
+
if (vllm && vllm.model) {
|
|
91
|
+
resolvedVllm = {
|
|
92
|
+
baseUrl: vllm.baseUrl?.trim() || "http://localhost:8000",
|
|
93
|
+
model: vllm.model.trim(),
|
|
94
|
+
apiKey: vllm.apiKey?.trim() || undefined,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
config,
|
|
100
|
+
helperModel: helperModel?.trim() || undefined,
|
|
101
|
+
ollama: resolvedOllama,
|
|
102
|
+
vllm: resolvedVllm,
|
|
103
|
+
configPath,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Convenience alias when only MemoryConfig is needed. */
|
|
108
|
+
export function loadMemoryConfig(
|
|
109
|
+
overrides: Partial<MemoryConfig> = {},
|
|
110
|
+
configPath = defaultMemoryConfigPath(),
|
|
111
|
+
): MemoryConfig {
|
|
112
|
+
return loadMemorySettings(overrides, configPath).config;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function resolveHelperModelSpec(
|
|
116
|
+
flagValue: string | boolean | undefined,
|
|
117
|
+
settingsHelperModel: string | undefined,
|
|
118
|
+
): string | undefined {
|
|
119
|
+
if (typeof flagValue === "string" && flagValue.trim()) {
|
|
120
|
+
return flagValue.trim();
|
|
121
|
+
}
|
|
122
|
+
return settingsHelperModel;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export { defaultMemoryConfig };
|
|
126
|
+
export type { ExtractorType, MemoryProvider, TrainerConfig };
|
package/src/sidecar/process.ts
CHANGED
|
@@ -27,8 +27,9 @@ export class SidecarProcess {
|
|
|
27
27
|
await access(bin, constants.X_OK);
|
|
28
28
|
return bin;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const resolved = await which(bin);
|
|
31
|
+
if (!resolved) throw new Error(`${bin} not found in PATH`);
|
|
32
|
+
return resolved;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
async spawn(): Promise<void> {
|
|
@@ -57,8 +58,8 @@ export class SidecarProcess {
|
|
|
57
58
|
detached: process.platform !== "win32",
|
|
58
59
|
});
|
|
59
60
|
|
|
60
|
-
this.child.on("error", (
|
|
61
|
-
|
|
61
|
+
this.child.on("error", () => {
|
|
62
|
+
/* handled by waitReady timeout */
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
this.child.unref?.();
|
|
@@ -126,6 +127,20 @@ export class SidecarProcess {
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
async function which(cmd: string): Promise<string | null> {
|
|
131
|
+
const { execFileSync } = await import("node:child_process");
|
|
132
|
+
try {
|
|
133
|
+
const result = execFileSync(
|
|
134
|
+
process.platform === "win32" ? "where" : "which",
|
|
135
|
+
[cmd],
|
|
136
|
+
{ encoding: "utf8", timeout: 3_000 },
|
|
137
|
+
).trim();
|
|
138
|
+
return result.split("\n")[0]?.trim() || null;
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
129
144
|
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
130
145
|
return new Promise((resolve, reject) => {
|
|
131
146
|
if (signal?.aborted) {
|
|
@@ -55,7 +55,7 @@ export class MemoryRecallTool {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
async run(argsJson: string, signal?: AbortSignal): Promise<ToolResult> {
|
|
58
|
+
async run(argsJson: string, signal?: AbortSignal, onProgress?: (msg: string) => void): Promise<ToolResult> {
|
|
59
59
|
const args = parseArgs(argsJson);
|
|
60
60
|
if ("error" in args) {
|
|
61
61
|
return { content: args.error, isError: true };
|
|
@@ -67,29 +67,53 @@ export class MemoryRecallTool {
|
|
|
67
67
|
return { content: validation, isError: true };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
return this.runIntent(intent, signal);
|
|
70
|
+
return this.runIntent(intent, signal, onProgress);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
async runIntent(intent: QueryIntent, signal?: AbortSignal): Promise<ToolResult> {
|
|
73
|
+
async runIntent(intent: QueryIntent, signal?: AbortSignal, onProgress?: (msg: string) => void): Promise<ToolResult> {
|
|
74
74
|
if (this.service.status() !== "ready") {
|
|
75
75
|
return this.fallbackResult(intent, "service_unavailable", "fallback");
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
// Emit a progress message if the sidecar takes longer than 500 ms.
|
|
79
|
+
let progressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
80
|
+
if (onProgress) {
|
|
81
|
+
progressTimer = setTimeout(() => {
|
|
82
|
+
onProgress("Querying episodic memory…");
|
|
83
|
+
}, 500);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let result;
|
|
87
|
+
try {
|
|
88
|
+
result = await this.service.query(intent, signal);
|
|
89
|
+
} finally {
|
|
90
|
+
if (progressTimer != null) clearTimeout(progressTimer);
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
if (result.transportError || result.errorClass === "unavailable") {
|
|
80
94
|
return this.fallbackResult(intent, "service_unavailable", "fallback");
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
if (result.errorClass === "retryable") {
|
|
84
|
-
|
|
85
|
-
|
|
98
|
+
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
99
|
+
if (onProgress) {
|
|
100
|
+
retryTimer = setTimeout(() => onProgress("Retrying memory query…"), 500);
|
|
101
|
+
}
|
|
102
|
+
let retryResult;
|
|
103
|
+
try {
|
|
104
|
+
await sleep(500, signal);
|
|
105
|
+
retryResult = await this.service.query(intent, signal);
|
|
106
|
+
} finally {
|
|
107
|
+
if (retryTimer != null) clearTimeout(retryTimer);
|
|
108
|
+
}
|
|
86
109
|
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
retryResult.transportError ||
|
|
111
|
+
retryResult.errorClass === "unavailable" ||
|
|
112
|
+
retryResult.errorClass === "retryable"
|
|
90
113
|
) {
|
|
91
114
|
return this.fallbackResult(intent, "retryable_failed", "fallback_after_retry");
|
|
92
115
|
}
|
|
116
|
+
result = retryResult;
|
|
93
117
|
}
|
|
94
118
|
|
|
95
119
|
if (result.errorClass === "permanent") {
|
package/src/trainer/scheduler.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface SchedulerConfig {
|
|
|
5
5
|
interval: string | null;
|
|
6
6
|
/** Passed through to trainBundle. */
|
|
7
7
|
trainConfig?: Omit<TrainBundleConfig, "full" | "dryRun">;
|
|
8
|
+
/** Called after a successful train tick (no error). Use to notify MemoryService. */
|
|
9
|
+
onSuccess?: () => void;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export interface TrainScheduler {
|
|
@@ -69,6 +71,7 @@ export function createTrainScheduler(
|
|
|
69
71
|
full: false,
|
|
70
72
|
dryRun: false,
|
|
71
73
|
});
|
|
74
|
+
config.onSuccess?.();
|
|
72
75
|
} catch (err) {
|
|
73
76
|
error = err instanceof Error ? err.message : String(err);
|
|
74
77
|
}
|