@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.
Files changed (67) 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 +8 -4
  48. package/src/adapters/ollamaClient.ts +179 -0
  49. package/src/adapters/openaiCompatClient.ts +155 -0
  50. package/src/cache/memoryCaches.ts +72 -0
  51. package/src/cli.ts +4 -3
  52. package/src/fallback/llmRerank.ts +8 -1
  53. package/src/fallback/sessionIndex.ts +78 -40
  54. package/src/fallback/sessionSearch.ts +107 -27
  55. package/src/index.ts +28 -0
  56. package/src/local/graphQuery.ts +252 -0
  57. package/src/paths.ts +1 -1
  58. package/src/pi-extension.ts +164 -36
  59. package/src/preflight/detectIntents.ts +6 -0
  60. package/src/preflight/hook.ts +68 -5
  61. package/src/preflight/render.ts +28 -3
  62. package/src/service.ts +133 -29
  63. package/src/settings.ts +126 -0
  64. package/src/sidecar/process.ts +19 -4
  65. package/src/tools/memoryRecall.ts +33 -9
  66. package/src/trainer/scheduler.ts +3 -0
  67. 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
- * Mode B local memory: spawn tlm sidecar, query via Unix socket.
30
- * No Cloud pullerbundle must exist under bundleRoot/current.
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.serviceStatus = "unavailable";
88
- this.reason = "tlm_binary_missing";
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.signal);
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
- } catch (err) {
99
- this.serviceStatus = "unavailable";
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
- * Start interval-based auto-training. If already running, stops and restarts.
110
- * Uses config.trainer.auto_interval.
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
- * Trigger incremental session index build in the background (non-blocking).
128
- * Opens (or creates) the SQLite FTS5 DB at ~/.pi/memory/sessions.db.
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 dbPath = path.join(this.cfg.bundleRoot, "sessions.db");
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" || !this.client) {
267
+ if (this.serviceStatus !== "ready") {
185
268
  return { env: null, errorClass: "unavailable" };
186
269
  }
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);
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 (!this.client) return null;
196
- try {
197
- return await this.client.health();
198
- } catch {
199
- return null;
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
  }
@@ -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 };
@@ -27,8 +27,9 @@ export class SidecarProcess {
27
27
  await access(bin, constants.X_OK);
28
28
  return bin;
29
29
  }
30
- // PATH lookup: spawn with shell on unix
31
- return bin;
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", (err) => {
61
- console.error("[pi-memory] sidecar spawn error:", err.message);
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
- let result = await this.service.query(intent, signal);
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
- await sleep(500, signal);
85
- result = await this.service.query(intent, signal);
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
- result.transportError ||
88
- result.errorClass === "unavailable" ||
89
- result.errorClass === "retryable"
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") {
@@ -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
  }