mono-pilot 0.2.9 → 0.2.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 (158) hide show
  1. package/README.md +270 -7
  2. package/dist/src/agents-paths.js +36 -0
  3. package/dist/src/brief/blocks.js +83 -0
  4. package/dist/src/brief/defaults.js +60 -0
  5. package/dist/src/brief/frontmatter.js +53 -0
  6. package/dist/src/brief/paths.js +10 -0
  7. package/dist/src/brief/reflection.js +27 -0
  8. package/dist/src/cli.js +62 -5
  9. package/dist/src/cluster/bus.js +102 -0
  10. package/dist/src/cluster/follower.js +137 -0
  11. package/dist/src/cluster/init.js +182 -0
  12. package/dist/src/cluster/leader.js +97 -0
  13. package/dist/src/cluster/log.js +49 -0
  14. package/dist/src/cluster/protocol.js +34 -0
  15. package/dist/src/cluster/services/bus.js +243 -0
  16. package/dist/src/cluster/services/embedding.js +12 -0
  17. package/dist/src/cluster/socket.js +86 -0
  18. package/dist/src/cluster/test-bus.js +175 -0
  19. package/dist/src/cluster_v2/connection-lifecycle.js +31 -0
  20. package/dist/src/cluster_v2/connection-lifecycle.test.js +24 -0
  21. package/dist/src/cluster_v2/connection.js +159 -0
  22. package/dist/src/cluster_v2/connection.test.js +55 -0
  23. package/dist/src/cluster_v2/events.js +102 -0
  24. package/dist/src/cluster_v2/index.js +2 -0
  25. package/dist/src/cluster_v2/observability.js +99 -0
  26. package/dist/src/cluster_v2/observability.test.js +46 -0
  27. package/dist/src/cluster_v2/rpc.js +389 -0
  28. package/dist/src/cluster_v2/rpc.test.js +110 -0
  29. package/dist/src/cluster_v2/runtime.failover.integration.test.js +156 -0
  30. package/dist/src/cluster_v2/runtime.js +531 -0
  31. package/dist/src/cluster_v2/runtime.lease-compromise.integration.test.js +91 -0
  32. package/dist/src/cluster_v2/runtime.lifecycle.integration.test.js +225 -0
  33. package/dist/src/cluster_v2/services/bus.integration.test.js +140 -0
  34. package/dist/src/cluster_v2/services/bus.js +450 -0
  35. package/dist/src/cluster_v2/services/discord/auth-store.js +82 -0
  36. package/dist/src/cluster_v2/services/discord/collector.js +569 -0
  37. package/dist/src/cluster_v2/services/discord/index.js +1 -0
  38. package/dist/src/cluster_v2/services/discord/oauth.js +87 -0
  39. package/dist/src/cluster_v2/services/discord/rpc-client.js +325 -0
  40. package/dist/src/cluster_v2/services/embedding.js +66 -0
  41. package/dist/src/cluster_v2/services/registry-cache.js +107 -0
  42. package/dist/src/cluster_v2/services/registry-cache.test.js +66 -0
  43. package/dist/src/cluster_v2/services/registry.js +36 -0
  44. package/dist/src/cluster_v2/services/twitter/collector.js +1055 -0
  45. package/dist/src/cluster_v2/services/twitter/index.js +1 -0
  46. package/dist/src/config/digest.js +78 -0
  47. package/dist/src/config/discord.js +143 -0
  48. package/dist/src/config/image-gen.js +48 -0
  49. package/dist/src/config/mono-pilot.js +31 -0
  50. package/dist/src/config/twitter.js +100 -0
  51. package/dist/src/extensions/cluster.js +311 -0
  52. package/dist/src/extensions/commands/build-memory.js +76 -0
  53. package/dist/src/extensions/commands/digest/backfill.js +779 -0
  54. package/dist/src/extensions/commands/digest/index.js +1133 -0
  55. package/dist/src/extensions/commands/image-model.js +214 -0
  56. package/dist/src/extensions/game/bus-injection.js +47 -0
  57. package/dist/src/extensions/game/identity.js +83 -0
  58. package/dist/src/extensions/game/mailbox.js +61 -0
  59. package/dist/src/extensions/game/system-prompt.js +134 -0
  60. package/dist/src/extensions/game/tools.js +28 -0
  61. package/dist/src/extensions/lifecycle.js +337 -0
  62. package/dist/src/extensions/mode-runtime.js +26 -2
  63. package/dist/src/extensions/mono-game.js +66 -0
  64. package/dist/src/extensions/mono-pilot.js +100 -18
  65. package/dist/src/extensions/nvim.js +47 -0
  66. package/dist/src/extensions/session-hints.js +60 -35
  67. package/dist/src/extensions/sftp.js +897 -0
  68. package/dist/src/extensions/status.js +676 -0
  69. package/dist/src/extensions/system-events.js +478 -0
  70. package/dist/src/extensions/system-prompt.js +24 -14
  71. package/dist/src/extensions/user-message.js +94 -50
  72. package/dist/src/lsp/client.js +235 -0
  73. package/dist/src/lsp/index.js +165 -0
  74. package/dist/src/lsp/runtime.js +67 -0
  75. package/dist/src/lsp/server.js +242 -0
  76. package/dist/src/mcp/config.js +112 -0
  77. package/dist/src/{utils/mcp-client.js → mcp/protocol.js} +1 -100
  78. package/dist/src/mcp/servers.js +90 -0
  79. package/dist/src/memory/build-memory.js +103 -0
  80. package/dist/src/memory/config/defaults.js +55 -0
  81. package/dist/src/memory/config/loader.js +29 -0
  82. package/dist/src/memory/config/paths.js +9 -0
  83. package/dist/src/memory/config/resolve.js +90 -0
  84. package/dist/src/memory/config/types.js +1 -0
  85. package/dist/src/memory/embeddings/batch-runner.js +39 -0
  86. package/dist/src/memory/embeddings/cache.js +47 -0
  87. package/dist/src/memory/embeddings/chunk-limits.js +26 -0
  88. package/dist/src/memory/embeddings/input-limits.js +48 -0
  89. package/dist/src/memory/embeddings/local.js +108 -0
  90. package/dist/src/memory/embeddings/types.js +1 -0
  91. package/dist/src/memory/index-manager.js +552 -0
  92. package/dist/src/memory/indexing/embeddings.js +67 -0
  93. package/dist/src/memory/indexing/files.js +180 -0
  94. package/dist/src/memory/indexing/index-file.js +105 -0
  95. package/dist/src/memory/log.js +38 -0
  96. package/dist/src/memory/paths.js +15 -0
  97. package/dist/src/memory/runtime/index.js +299 -0
  98. package/dist/src/memory/runtime/thread.js +116 -0
  99. package/dist/src/memory/search/fts.js +57 -0
  100. package/dist/src/memory/search/hybrid.js +50 -0
  101. package/dist/src/memory/search/text.js +30 -0
  102. package/dist/src/memory/search/vector.js +43 -0
  103. package/dist/src/memory/session/content-hash.js +7 -0
  104. package/dist/src/memory/session/entry.js +33 -0
  105. package/dist/src/memory/session/flush-policy.js +34 -0
  106. package/dist/src/memory/session/hook.js +191 -0
  107. package/dist/src/memory/session/paths.js +15 -0
  108. package/dist/src/memory/session/session-reader.js +88 -0
  109. package/dist/src/memory/session/transcript/content-hash.js +7 -0
  110. package/dist/src/memory/session/transcript/entry.js +28 -0
  111. package/dist/src/memory/session/transcript/flush.js +56 -0
  112. package/dist/src/memory/session/transcript/paths.js +28 -0
  113. package/dist/src/memory/session/transcript/reader.js +112 -0
  114. package/dist/src/memory/session/transcript/state.js +31 -0
  115. package/dist/src/memory/store/schema.js +89 -0
  116. package/dist/src/memory/store/sqlite.js +89 -0
  117. package/dist/src/memory/types.js +1 -0
  118. package/dist/src/memory/warm.js +25 -0
  119. package/dist/src/rules/discovery.js +41 -0
  120. package/dist/{tools → src/tools}/README.md +29 -3
  121. package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
  122. package/dist/{tools → src/tools}/apply-patch.js +174 -104
  123. package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
  124. package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
  125. package/dist/src/tools/ast-grep.js +357 -0
  126. package/dist/src/tools/brief-write.js +122 -0
  127. package/dist/src/tools/bus-send.js +100 -0
  128. package/dist/{tools → src/tools}/call-mcp-tool.js +40 -124
  129. package/dist/src/tools/codex-apply-patch-description.md +52 -0
  130. package/dist/src/tools/codex-apply-patch.js +540 -0
  131. package/dist/{tools → src/tools}/delete.js +24 -0
  132. package/dist/src/tools/exit-plan-mode.js +83 -0
  133. package/dist/{tools → src/tools}/fetch-mcp-resource.js +56 -100
  134. package/dist/src/tools/generate-image.js +567 -0
  135. package/dist/{tools → src/tools}/glob.js +55 -1
  136. package/dist/{tools → src/tools}/list-mcp-resources.js +46 -57
  137. package/dist/{tools → src/tools}/list-mcp-tools.js +52 -63
  138. package/dist/src/tools/ls.js +48 -0
  139. package/dist/src/tools/lsp-diagnostics.js +67 -0
  140. package/dist/src/tools/lsp-symbols.js +54 -0
  141. package/dist/src/tools/mailbox.js +85 -0
  142. package/dist/src/tools/memory-get.js +90 -0
  143. package/dist/src/tools/memory-search.js +180 -0
  144. package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
  145. package/dist/{tools → src/tools}/read-file.js +8 -19
  146. package/dist/{tools → src/tools}/rg.js +10 -20
  147. package/dist/{tools → src/tools}/shell.js +19 -42
  148. package/dist/{tools → src/tools}/subagent.js +255 -6
  149. package/dist/{tools → src/tools}/switch-mode.js +37 -6
  150. package/dist/{tools → src/tools}/web-fetch.js +105 -7
  151. package/dist/{tools → src/tools}/web-search.js +29 -1
  152. package/package.json +21 -9
  153. /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
  154. /package/dist/{tools → src/tools}/rg.test.js +0 -0
  155. /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
  156. /package/dist/{tools → src/tools}/semantic-search.js +0 -0
  157. /package/dist/{tools → src/tools}/shell-description.md +0 -0
  158. /package/dist/{tools → src/tools}/subagent-description.md +0 -0
@@ -0,0 +1,552 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { relative } from "node:path";
3
+ import chokidar from "chokidar";
4
+ import { getAgentMemoryDir } from "../agents-paths.js";
5
+ import { getMemoryIndexPath } from "./paths.js";
6
+ import { getSessionTranscriptsRootDir } from "./session/transcript/paths.js";
7
+ import { memoryLog } from "./log.js";
8
+ import { openSqliteDatabase } from "./store/sqlite.js";
9
+ import { ensureMemoryIndexSchema } from "./store/schema.js";
10
+ import { FILES_TABLE, CHUNKS_TABLE, FTS_TABLE, VECTOR_TABLE } from "./store/schema.js";
11
+ import { buildFileEntry, hashText, listMemoryFiles, resolveExtraPaths } from "./indexing/files.js";
12
+ import { indexMemoryFile } from "./indexing/index-file.js";
13
+ import { searchFts } from "./search/fts.js";
14
+ import { searchVector } from "./search/vector.js";
15
+ import { mergeHybridResults } from "./search/hybrid.js";
16
+ import { loadSqliteVecExtension } from "./store/sqlite.js";
17
+ const SNIPPET_MAX_CHARS = 400;
18
+ export class MemoryIndexManager {
19
+ agentId;
20
+ workspaceDir;
21
+ settings;
22
+ db;
23
+ memoryDir;
24
+ sessionTranscriptsDir;
25
+ includeMemorySource;
26
+ includeSessionsSource;
27
+ ftsAvailable;
28
+ provider = null;
29
+ providerKey = null;
30
+ vectorAvailable = null;
31
+ vectorDims;
32
+ dirty = false;
33
+ watcher = null;
34
+ watchTimer = null;
35
+ intervalTimer = null;
36
+ syncInProgress = false;
37
+ onAutoSyncEvent;
38
+ embedFn = null;
39
+ embedModel = null;
40
+ constructor(params) {
41
+ this.agentId = params.agentId;
42
+ this.workspaceDir = params.workspaceDir;
43
+ this.settings = params.settings;
44
+ this.embedFn = params.embedFn ?? null;
45
+ this.embedModel = params.embedModel ?? null;
46
+ this.onAutoSyncEvent = params.onAutoSyncEvent;
47
+ this.memoryDir = getAgentMemoryDir(params.agentId);
48
+ this.sessionTranscriptsDir = getSessionTranscriptsRootDir(params.agentId);
49
+ this.includeMemorySource = this.settings.sources.includes("memory");
50
+ this.includeSessionsSource = this.settings.sources.includes("sessions");
51
+ this.db = openSqliteDatabase(getMemoryIndexPath(), true);
52
+ const schema = ensureMemoryIndexSchema({ db: this.db, ftsEnabled: true });
53
+ this.ftsAvailable = schema.ftsAvailable;
54
+ if (schema.ftsError) {
55
+ console.warn(`[memory] FTS unavailable: ${schema.ftsError}`);
56
+ memoryLog.warn("fts unavailable", { error: schema.ftsError, agentId: this.agentId });
57
+ }
58
+ this.dirty = true;
59
+ this.ensureWatcher();
60
+ this.ensureIntervalSync();
61
+ memoryLog.info("manager initialized", {
62
+ agentId: this.agentId,
63
+ memoryDir: this.memoryDir,
64
+ sessionTranscriptsDir: this.sessionTranscriptsDir,
65
+ includeMemorySource: this.includeMemorySource,
66
+ includeSessionsSource: this.includeSessionsSource,
67
+ indexPath: getMemoryIndexPath(),
68
+ });
69
+ }
70
+ async search(query, opts) {
71
+ const cleaned = query.trim();
72
+ if (!cleaned)
73
+ return [];
74
+ // Qwen3-Embedding instruct format: only on query side, documents stay raw.
75
+ // Format: "Instruct: {task}\nQuery:{text}" (no space after "Query:")
76
+ const DEFAULT_INSTRUCT = "Retrieve relevant code snippets or documentation passages that match the query";
77
+ const instruct = opts?.instruct?.trim() || DEFAULT_INSTRUCT;
78
+ const instructQuery = `Instruct: ${instruct}\nQuery:${cleaned}`;
79
+ if (this.settings.sync.onSearch && this.dirty) {
80
+ await this.sync({ reason: "search" }).catch((err) => {
81
+ console.warn(`[memory] sync failed (search): ${String(err)}`);
82
+ memoryLog.warn("sync failed (search)", { agentId: this.agentId, error: String(err) });
83
+ });
84
+ }
85
+ const maxResults = opts?.maxResults ?? this.settings.query.maxResults;
86
+ const minScore = opts?.minScore ?? this.settings.query.minScore;
87
+ const scope = opts?.scope ?? "self";
88
+ const targetAgentId = opts?.targetAgentId;
89
+ const agentFilter = scope === "all" ? undefined : scope === "agent" ? targetAgentId : this.agentId;
90
+ if (scope === "agent" && !targetAgentId) {
91
+ return [];
92
+ }
93
+ const defaultAgentId = scope === "all" ? undefined : agentFilter ?? this.agentId;
94
+ const resolveAgentId = (rowAgentId) => rowAgentId ?? defaultAgentId;
95
+ const querySnippet = cleaned.length > 200 ? `${cleaned.slice(0, 200)}...` : cleaned;
96
+ memoryLog.info("search start", {
97
+ agentId: this.agentId,
98
+ scope,
99
+ targetAgentId,
100
+ query: querySnippet,
101
+ });
102
+ const finish = (results, mode) => {
103
+ memoryLog.info("search complete", {
104
+ agentId: this.agentId,
105
+ scope,
106
+ targetAgentId,
107
+ mode,
108
+ resultCount: results.length,
109
+ });
110
+ return results;
111
+ };
112
+ const provider = await this.getProvider();
113
+ if (provider) {
114
+ try {
115
+ const queryEmbedding = await provider.embedQuery(instructQuery);
116
+ const vectorReady = await this.ensureVectorReady(queryEmbedding.length);
117
+ if (vectorReady) {
118
+ const hybrid = this.settings.query.hybrid;
119
+ const candidateLimit = Math.min(200, Math.max(1, Math.floor(maxResults * hybrid.candidateMultiplier)));
120
+ const vectorSources = [];
121
+ if (this.includeMemorySource)
122
+ vectorSources.push("memory");
123
+ if (this.includeSessionsSource)
124
+ vectorSources.push("sessions");
125
+ const vectorResults = (await Promise.all(vectorSources.map((source) => searchVector({
126
+ db: this.db,
127
+ queryVec: queryEmbedding,
128
+ limit: hybrid.enabled ? candidateLimit : maxResults,
129
+ snippetMaxChars: SNIPPET_MAX_CHARS,
130
+ model: provider.model,
131
+ agentId: agentFilter,
132
+ source,
133
+ })))).flat();
134
+ if (!hybrid.enabled || !this.ftsAvailable) {
135
+ return finish(vectorResults
136
+ .map((row) => ({
137
+ path: row.path,
138
+ startLine: row.startLine,
139
+ endLine: row.endLine,
140
+ score: row.vectorScore,
141
+ snippet: row.snippet,
142
+ source: row.source,
143
+ agentId: resolveAgentId(row.agentId),
144
+ }))
145
+ .filter((row) => row.score >= minScore)
146
+ .slice(0, maxResults), "vector");
147
+ }
148
+ const keywordSources = [];
149
+ if (this.includeMemorySource)
150
+ keywordSources.push("memory");
151
+ if (this.includeSessionsSource)
152
+ keywordSources.push("sessions");
153
+ const keywordResults = (await Promise.all(keywordSources.map((source) => Promise.resolve(searchFts({
154
+ db: this.db,
155
+ query: cleaned,
156
+ limit: candidateLimit,
157
+ minScore: 0,
158
+ snippetMaxChars: SNIPPET_MAX_CHARS,
159
+ model: provider.model,
160
+ agentId: agentFilter,
161
+ source,
162
+ }))))).flat();
163
+ const merged = mergeHybridResults({
164
+ vector: vectorResults,
165
+ keyword: keywordResults,
166
+ vectorWeight: hybrid.vectorWeight,
167
+ textWeight: hybrid.textWeight,
168
+ });
169
+ return finish(merged
170
+ .filter((row) => row.score >= minScore)
171
+ .sort((a, b) => b.score - a.score)
172
+ .slice(0, maxResults)
173
+ .map((row) => ({
174
+ path: row.path,
175
+ startLine: row.startLine,
176
+ endLine: row.endLine,
177
+ score: row.score,
178
+ snippet: row.snippet,
179
+ source: row.source,
180
+ agentId: resolveAgentId(row.agentId),
181
+ })), "hybrid");
182
+ }
183
+ }
184
+ catch (error) {
185
+ console.warn(`[memory] vector search failed: ${String(error)}`);
186
+ memoryLog.warn("vector search failed", {
187
+ agentId: this.agentId,
188
+ scope,
189
+ targetAgentId,
190
+ error: String(error),
191
+ });
192
+ }
193
+ }
194
+ if (!this.ftsAvailable)
195
+ return [];
196
+ const ftsSources = [];
197
+ if (this.includeMemorySource)
198
+ ftsSources.push("memory");
199
+ if (this.includeSessionsSource)
200
+ ftsSources.push("sessions");
201
+ const rows = (await Promise.all(ftsSources.map((source) => Promise.resolve(searchFts({
202
+ db: this.db,
203
+ query: cleaned,
204
+ limit: maxResults,
205
+ minScore,
206
+ snippetMaxChars: SNIPPET_MAX_CHARS,
207
+ model: provider?.model,
208
+ agentId: agentFilter,
209
+ source,
210
+ }))))).flat();
211
+ return finish(rows.map((row) => ({
212
+ path: row.path,
213
+ startLine: row.startLine,
214
+ endLine: row.endLine,
215
+ score: row.score,
216
+ snippet: row.snippet,
217
+ source: row.source,
218
+ agentId: resolveAgentId(row.agentId),
219
+ })), "fts");
220
+ }
221
+ async get(path, from, lines) {
222
+ const raw = await readFile(path, "utf-8");
223
+ if (from === undefined && lines === undefined) {
224
+ return { path, text: raw };
225
+ }
226
+ const startLine = Math.max(1, Math.floor(from ?? 1));
227
+ const maxLines = lines !== undefined ? Math.max(0, Math.floor(lines)) : undefined;
228
+ const allLines = raw.split("\n");
229
+ const startIndex = Math.min(allLines.length, Math.max(0, startLine - 1));
230
+ const endIndex = maxLines === undefined ? allLines.length : Math.min(allLines.length, startIndex + maxLines);
231
+ return { path, text: allLines.slice(startIndex, endIndex).join("\n") };
232
+ }
233
+ isDirty() {
234
+ return this.dirty;
235
+ }
236
+ async sync(params) {
237
+ const reason = params?.reason ?? "unknown";
238
+ const isAutoSyncReason = reason === "search" || reason === "watch" || reason === "interval";
239
+ if (this.syncInProgress) {
240
+ memoryLog.debug("sync skipped (already running)", {
241
+ agentId: this.agentId,
242
+ reason,
243
+ });
244
+ return;
245
+ }
246
+ this.syncInProgress = true;
247
+ let workDetected = false;
248
+ const notifyWorkDetected = () => {
249
+ if (workDetected) {
250
+ return;
251
+ }
252
+ workDetected = true;
253
+ if (isAutoSyncReason) {
254
+ this.onAutoSyncEvent?.({ reason, phase: "start" });
255
+ }
256
+ params?.onWorkDetected?.();
257
+ };
258
+ const start = Date.now();
259
+ memoryLog.info("sync start", {
260
+ agentId: this.agentId,
261
+ reason,
262
+ force: params?.force ?? false,
263
+ });
264
+ try {
265
+ const provider = await this.getProvider();
266
+ if (provider && !this.providerKey) {
267
+ this.providerKey = hashText(provider.model);
268
+ }
269
+ const embeddingsContext = provider
270
+ ? {
271
+ provider,
272
+ providerKey: this.providerKey ?? hashText(provider.model),
273
+ cache: this.settings.cache,
274
+ vector: {
275
+ enabled: this.settings.store.vector.enabled,
276
+ ensureReady: this.ensureVectorReady.bind(this),
277
+ },
278
+ }
279
+ : undefined;
280
+ const files = await listMemoryFiles({
281
+ memoryDir: this.memoryDir,
282
+ sessionTranscriptsDir: this.sessionTranscriptsDir,
283
+ includeMemoryDir: this.includeMemorySource,
284
+ includeSessionTranscriptsDir: this.includeSessionsSource,
285
+ extraPaths: this.settings.extraPaths,
286
+ workspaceDir: this.workspaceDir,
287
+ });
288
+ const entries = (await Promise.all(files.map((file) => buildFileEntry(file)))).filter((entry) => entry !== null);
289
+ const activePaths = new Set(entries.map((entry) => entry.path));
290
+ let indexed = 0;
291
+ for (const entry of entries) {
292
+ const source = this.resolveSourceForPath(entry.path);
293
+ if (!source)
294
+ continue;
295
+ const record = this.db
296
+ .prepare(`SELECT hash FROM ${FILES_TABLE} WHERE path = ? AND source = ? AND agent_id = ?`)
297
+ .get(entry.path, source, this.agentId);
298
+ if (!params?.force && record?.hash === entry.hash) {
299
+ continue;
300
+ }
301
+ notifyWorkDetected();
302
+ await indexMemoryFile({
303
+ db: this.db,
304
+ agentId: this.agentId,
305
+ entry,
306
+ source,
307
+ chunking: this.settings.chunking,
308
+ ftsAvailable: this.ftsAvailable,
309
+ embeddings: embeddingsContext,
310
+ });
311
+ indexed += 1;
312
+ this.db
313
+ .prepare(`INSERT INTO ${FILES_TABLE} (path, agent_id, source, hash, mtime, size)
314
+ VALUES (?, ?, ?, ?, ?, ?)
315
+ ON CONFLICT(path, agent_id) DO UPDATE SET
316
+ hash=excluded.hash,
317
+ mtime=excluded.mtime,
318
+ size=excluded.size`)
319
+ .run(entry.path, this.agentId, source, entry.hash, Math.round(entry.mtimeMs), entry.size);
320
+ }
321
+ const trackedSources = [];
322
+ if (this.includeMemorySource)
323
+ trackedSources.push("memory");
324
+ if (this.includeSessionsSource)
325
+ trackedSources.push("sessions");
326
+ let staleDeleted = 0;
327
+ for (const source of trackedSources) {
328
+ const staleRows = this.db
329
+ .prepare(`SELECT path FROM ${FILES_TABLE} WHERE source = ? AND agent_id = ?`)
330
+ .all(source, this.agentId);
331
+ for (const stale of staleRows) {
332
+ const staleSource = this.resolveSourceForPath(stale.path) ?? source;
333
+ if (activePaths.has(stale.path) && staleSource === source)
334
+ continue;
335
+ notifyWorkDetected();
336
+ this.db
337
+ .prepare(`DELETE FROM ${FILES_TABLE} WHERE path = ? AND source = ? AND agent_id = ?`)
338
+ .run(stale.path, source, this.agentId);
339
+ if (this.settings.store.vector.enabled) {
340
+ try {
341
+ this.db
342
+ .prepare(`DELETE FROM ${VECTOR_TABLE} WHERE id IN (SELECT id FROM ${CHUNKS_TABLE} WHERE path = ? AND source = ? AND agent_id = ?)`)
343
+ .run(stale.path, source, this.agentId);
344
+ }
345
+ catch { }
346
+ }
347
+ if (this.ftsAvailable) {
348
+ try {
349
+ this.db
350
+ .prepare(`DELETE FROM ${FTS_TABLE} WHERE id IN (SELECT id FROM ${CHUNKS_TABLE} WHERE path = ? AND source = ? AND agent_id = ?)`)
351
+ .run(stale.path, source, this.agentId);
352
+ }
353
+ catch { }
354
+ }
355
+ this.db
356
+ .prepare(`DELETE FROM ${CHUNKS_TABLE} WHERE path = ? AND source = ? AND agent_id = ?`)
357
+ .run(stale.path, source, this.agentId);
358
+ staleDeleted += 1;
359
+ }
360
+ }
361
+ this.dirty = false;
362
+ const durationMs = Date.now() - start;
363
+ memoryLog.info("sync complete", {
364
+ agentId: this.agentId,
365
+ reason,
366
+ indexed,
367
+ staleDeleted,
368
+ files: entries.length,
369
+ durationMs,
370
+ });
371
+ if (isAutoSyncReason && workDetected) {
372
+ this.onAutoSyncEvent?.({
373
+ reason,
374
+ phase: "complete",
375
+ indexed,
376
+ staleDeleted,
377
+ files: entries.length,
378
+ durationMs,
379
+ });
380
+ }
381
+ }
382
+ catch (error) {
383
+ if (isAutoSyncReason) {
384
+ this.onAutoSyncEvent?.({
385
+ reason,
386
+ phase: "failed",
387
+ error: error instanceof Error ? error.message : String(error),
388
+ });
389
+ }
390
+ throw error;
391
+ }
392
+ finally {
393
+ this.syncInProgress = false;
394
+ }
395
+ }
396
+ async close() {
397
+ if (this.watchTimer) {
398
+ clearTimeout(this.watchTimer);
399
+ this.watchTimer = null;
400
+ }
401
+ if (this.intervalTimer) {
402
+ clearInterval(this.intervalTimer);
403
+ this.intervalTimer = null;
404
+ }
405
+ if (this.watcher) {
406
+ void this.watcher.close();
407
+ this.watcher = null;
408
+ }
409
+ this.provider = null;
410
+ this.db.close();
411
+ }
412
+ async getProvider() {
413
+ if (this.provider)
414
+ return this.provider;
415
+ if (!this.embedFn)
416
+ return null;
417
+ const embedFn = this.embedFn;
418
+ const model = this.embedModel ?? "local";
419
+ const provider = {
420
+ id: "local",
421
+ model,
422
+ embedQuery: async (text) => {
423
+ const [vec] = await embedFn([text]);
424
+ return vec;
425
+ },
426
+ embedBatch: embedFn,
427
+ };
428
+ this.provider = provider;
429
+ this.providerKey = hashText(model);
430
+ return provider;
431
+ }
432
+ async ensureVectorReady(dimensions) {
433
+ if (!this.settings.store.vector.enabled)
434
+ return false;
435
+ if (!dimensions || dimensions <= 0)
436
+ return false;
437
+ if (this.vectorAvailable === false)
438
+ return false;
439
+ if (this.vectorAvailable === null) {
440
+ const loaded = await loadSqliteVecExtension({
441
+ db: this.db,
442
+ extensionPath: this.settings.store.vector.extensionPath,
443
+ });
444
+ if (!loaded.ok) {
445
+ this.vectorAvailable = false;
446
+ console.warn(`[memory] sqlite-vec unavailable: ${loaded.error ?? "unknown"}`);
447
+ memoryLog.warn("sqlite-vec unavailable", {
448
+ agentId: this.agentId,
449
+ error: loaded.error ?? "unknown",
450
+ });
451
+ return false;
452
+ }
453
+ this.vectorAvailable = true;
454
+ }
455
+ this.ensureVectorTable(dimensions);
456
+ return true;
457
+ }
458
+ ensureVectorTable(dimensions) {
459
+ if (this.vectorDims === dimensions)
460
+ return;
461
+ if (this.vectorDims && this.vectorDims !== dimensions) {
462
+ try {
463
+ this.db.exec(`DROP TABLE IF EXISTS ${VECTOR_TABLE}`);
464
+ }
465
+ catch { }
466
+ }
467
+ this.db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS ${VECTOR_TABLE} USING vec0(\n` +
468
+ ` id TEXT PRIMARY KEY,\n` +
469
+ ` embedding FLOAT[${dimensions}]\n` +
470
+ `)`);
471
+ this.vectorDims = dimensions;
472
+ }
473
+ ensureWatcher() {
474
+ if (!this.settings.sync.watch || this.watcher)
475
+ return;
476
+ const watchPaths = new Set();
477
+ if (this.includeMemorySource) {
478
+ watchPaths.add(this.memoryDir);
479
+ for (const extra of resolveExtraPaths(this.workspaceDir, this.settings.extraPaths)) {
480
+ watchPaths.add(extra);
481
+ }
482
+ }
483
+ if (this.includeSessionsSource) {
484
+ watchPaths.add(this.sessionTranscriptsDir);
485
+ }
486
+ if (watchPaths.size === 0)
487
+ return;
488
+ this.watcher = chokidar.watch(Array.from(watchPaths), {
489
+ ignoreInitial: true,
490
+ awaitWriteFinish: {
491
+ stabilityThreshold: this.settings.sync.watchDebounceMs,
492
+ pollInterval: 100,
493
+ },
494
+ });
495
+ const markDirty = () => {
496
+ this.dirty = true;
497
+ this.scheduleWatchSync();
498
+ };
499
+ this.watcher.on("add", markDirty);
500
+ this.watcher.on("change", markDirty);
501
+ this.watcher.on("unlink", markDirty);
502
+ }
503
+ scheduleWatchSync() {
504
+ if (!this.settings.sync.watch)
505
+ return;
506
+ if (this.watchTimer) {
507
+ clearTimeout(this.watchTimer);
508
+ }
509
+ this.watchTimer = setTimeout(() => {
510
+ this.watchTimer = null;
511
+ void this.sync({ reason: "watch" }).catch((err) => {
512
+ console.warn(`[memory] sync failed (watch): ${String(err)}`);
513
+ memoryLog.warn("sync failed (watch)", {
514
+ agentId: this.agentId,
515
+ error: String(err),
516
+ });
517
+ });
518
+ }, this.settings.sync.watchDebounceMs);
519
+ }
520
+ resolveSourceForPath(absPath) {
521
+ const relativeToSessions = relative(this.sessionTranscriptsDir, absPath);
522
+ const isSessionTranscript = relativeToSessions.length > 0 &&
523
+ !relativeToSessions.startsWith("..") &&
524
+ !relativeToSessions.startsWith(".");
525
+ if (isSessionTranscript) {
526
+ return this.includeSessionsSource ? "sessions" : null;
527
+ }
528
+ return this.includeMemorySource ? "memory" : null;
529
+ }
530
+ ensureIntervalSync() {
531
+ const minutes = this.settings.sync.intervalMinutes;
532
+ if (!minutes || minutes <= 0)
533
+ return;
534
+ if (this.intervalTimer)
535
+ return;
536
+ const intervalMs = Math.max(1, Math.floor(minutes * 60_000));
537
+ this.intervalTimer = setInterval(() => {
538
+ void this.sync({ reason: "interval" }).catch((err) => {
539
+ console.warn(`[memory] sync failed (interval): ${String(err)}`);
540
+ memoryLog.warn("sync failed (interval)", {
541
+ agentId: this.agentId,
542
+ error: String(err),
543
+ });
544
+ });
545
+ }, intervalMs);
546
+ this.intervalTimer.unref();
547
+ memoryLog.info("interval sync scheduled", {
548
+ agentId: this.agentId,
549
+ intervalMinutes: minutes,
550
+ });
551
+ }
552
+ }
@@ -0,0 +1,67 @@
1
+ import { enforceEmbeddingMaxInputTokens } from "../embeddings/chunk-limits.js";
2
+ import { runEmbeddingBatches } from "../embeddings/batch-runner.js";
3
+ import { readEmbeddingCache, writeEmbeddingCache, pruneEmbeddingCache } from "../embeddings/cache.js";
4
+ const DEFAULT_BATCH_SIZE = 2;
5
+ const DEFAULT_BATCH_CONCURRENCY = 2;
6
+ export async function embedChunks(params) {
7
+ const limited = enforceEmbeddingMaxInputTokens(params.provider, params.chunks);
8
+ const embeddings = Array.from({ length: limited.length });
9
+ const pendingTexts = [];
10
+ const pendingIndices = [];
11
+ for (let i = 0; i < limited.length; i += 1) {
12
+ const chunk = limited[i];
13
+ if (!chunk)
14
+ continue;
15
+ if (params.cache.enabled) {
16
+ const cached = readEmbeddingCache({
17
+ db: params.db,
18
+ provider: params.provider.id,
19
+ model: params.provider.model,
20
+ providerKey: params.providerKey,
21
+ hash: chunk.hash,
22
+ });
23
+ if (cached) {
24
+ embeddings[i] = cached;
25
+ continue;
26
+ }
27
+ }
28
+ pendingTexts.push(chunk.text);
29
+ pendingIndices.push(i);
30
+ }
31
+ if (pendingTexts.length > 0) {
32
+ const embedded = await runEmbeddingBatches({
33
+ items: pendingTexts,
34
+ maxBatchSize: DEFAULT_BATCH_SIZE,
35
+ concurrency: DEFAULT_BATCH_CONCURRENCY,
36
+ runBatch: async (batch) => params.provider.embedBatch(batch),
37
+ });
38
+ for (let i = 0; i < pendingIndices.length; i += 1) {
39
+ const index = pendingIndices[i];
40
+ const embedding = embedded[i] ?? [];
41
+ embeddings[index] = embedding;
42
+ if (params.cache.enabled) {
43
+ const chunk = limited[index];
44
+ if (chunk) {
45
+ writeEmbeddingCache({
46
+ db: params.db,
47
+ provider: params.provider.id,
48
+ model: params.provider.model,
49
+ providerKey: params.providerKey,
50
+ hash: chunk.hash,
51
+ embedding,
52
+ });
53
+ }
54
+ }
55
+ }
56
+ if (params.cache.enabled) {
57
+ pruneEmbeddingCache({
58
+ db: params.db,
59
+ provider: params.provider.id,
60
+ model: params.provider.model,
61
+ providerKey: params.providerKey,
62
+ maxEntries: params.cache.maxEntries,
63
+ });
64
+ }
65
+ }
66
+ return { chunks: limited, embeddings };
67
+ }