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,180 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ function isFileMissingError(error) {
5
+ return error?.code === "ENOENT";
6
+ }
7
+ export function hashText(value) {
8
+ return crypto.createHash("sha256").update(value).digest("hex");
9
+ }
10
+ function normalizePath(value) {
11
+ return value.replace(/\\/g, "/");
12
+ }
13
+ export function resolveExtraPaths(workspaceDir, extraPaths) {
14
+ if (!extraPaths?.length)
15
+ return [];
16
+ const resolved = extraPaths
17
+ .map((value) => value.trim())
18
+ .filter(Boolean)
19
+ .map((value) => path.isAbsolute(value)
20
+ ? path.resolve(value)
21
+ : workspaceDir
22
+ ? path.resolve(workspaceDir, value)
23
+ : path.resolve(value));
24
+ return Array.from(new Set(resolved));
25
+ }
26
+ async function walkDir(dir, files) {
27
+ const entries = await fs.readdir(dir, { withFileTypes: true });
28
+ for (const entry of entries) {
29
+ const full = path.join(dir, entry.name);
30
+ if (entry.isSymbolicLink())
31
+ continue;
32
+ if (entry.isDirectory()) {
33
+ if (entry.name === ".state")
34
+ continue;
35
+ await walkDir(full, files);
36
+ continue;
37
+ }
38
+ if (!entry.isFile())
39
+ continue;
40
+ if (!entry.name.endsWith(".md"))
41
+ continue;
42
+ files.push(full);
43
+ }
44
+ }
45
+ export async function listMemoryFiles(params) {
46
+ const result = [];
47
+ if (params.includeMemoryDir !== false && params.memoryDir) {
48
+ try {
49
+ const dirStat = await fs.lstat(params.memoryDir);
50
+ if (!dirStat.isSymbolicLink() && dirStat.isDirectory()) {
51
+ await walkDir(params.memoryDir, result);
52
+ }
53
+ }
54
+ catch { }
55
+ }
56
+ if (params.includeSessionTranscriptsDir !== false && params.sessionTranscriptsDir) {
57
+ try {
58
+ const transcriptsStat = await fs.lstat(params.sessionTranscriptsDir);
59
+ if (!transcriptsStat.isSymbolicLink() && transcriptsStat.isDirectory()) {
60
+ await walkDir(params.sessionTranscriptsDir, result);
61
+ }
62
+ }
63
+ catch { }
64
+ }
65
+ const normalizedExtraPaths = params.includeMemoryDir === false
66
+ ? []
67
+ : resolveExtraPaths(params.workspaceDir, params.extraPaths);
68
+ for (const inputPath of normalizedExtraPaths) {
69
+ try {
70
+ const stat = await fs.lstat(inputPath);
71
+ if (stat.isSymbolicLink())
72
+ continue;
73
+ if (stat.isDirectory()) {
74
+ await walkDir(inputPath, result);
75
+ continue;
76
+ }
77
+ if (stat.isFile() && inputPath.endsWith(".md")) {
78
+ result.push(inputPath);
79
+ }
80
+ }
81
+ catch { }
82
+ }
83
+ return Array.from(new Set(result));
84
+ }
85
+ export async function buildFileEntry(absPath) {
86
+ let stat;
87
+ try {
88
+ stat = await fs.stat(absPath);
89
+ }
90
+ catch (err) {
91
+ if (isFileMissingError(err))
92
+ return null;
93
+ throw err;
94
+ }
95
+ let content;
96
+ try {
97
+ content = await fs.readFile(absPath, "utf-8");
98
+ }
99
+ catch (err) {
100
+ if (isFileMissingError(err))
101
+ return null;
102
+ throw err;
103
+ }
104
+ const hash = hashText(content);
105
+ return {
106
+ path: normalizePath(path.resolve(absPath)),
107
+ absPath,
108
+ mtimeMs: stat.mtimeMs,
109
+ size: stat.size,
110
+ hash,
111
+ };
112
+ }
113
+ export function chunkMarkdown(content, chunking) {
114
+ const lines = content.split("\n");
115
+ if (lines.length === 0)
116
+ return [];
117
+ const maxChars = Math.max(32, chunking.tokens * 4);
118
+ const overlapChars = Math.max(0, chunking.overlap * 4);
119
+ const chunks = [];
120
+ let current = [];
121
+ let currentChars = 0;
122
+ const flush = () => {
123
+ if (current.length === 0)
124
+ return;
125
+ const firstEntry = current[0];
126
+ const lastEntry = current[current.length - 1];
127
+ if (!firstEntry || !lastEntry)
128
+ return;
129
+ const text = current.map((entry) => entry.line).join("\n");
130
+ const startLine = firstEntry.lineNo;
131
+ const endLine = lastEntry.lineNo;
132
+ chunks.push({ startLine, endLine, text, hash: hashText(text) });
133
+ };
134
+ const carryOverlap = () => {
135
+ if (overlapChars <= 0 || current.length === 0) {
136
+ current = [];
137
+ currentChars = 0;
138
+ return;
139
+ }
140
+ let acc = 0;
141
+ const kept = [];
142
+ for (let i = current.length - 1; i >= 0; i -= 1) {
143
+ const entry = current[i];
144
+ if (!entry)
145
+ continue;
146
+ acc += entry.line.length + 1;
147
+ kept.unshift(entry);
148
+ if (acc >= overlapChars)
149
+ break;
150
+ }
151
+ current = kept;
152
+ currentChars = kept.reduce((sum, entry) => sum + entry.line.length + 1, 0);
153
+ };
154
+ for (let i = 0; i < lines.length; i += 1) {
155
+ const line = lines[i] ?? "";
156
+ const lineNo = i + 1;
157
+ const lineSize = line.length + 1;
158
+ if (lineSize > maxChars) {
159
+ if (current.length > 0) {
160
+ flush();
161
+ carryOverlap();
162
+ }
163
+ chunks.push({
164
+ startLine: lineNo,
165
+ endLine: lineNo,
166
+ text: line,
167
+ hash: hashText(line),
168
+ });
169
+ continue;
170
+ }
171
+ if (currentChars + lineSize > maxChars && current.length > 0) {
172
+ flush();
173
+ carryOverlap();
174
+ }
175
+ current.push({ line, lineNo });
176
+ currentChars += lineSize;
177
+ }
178
+ flush();
179
+ return chunks;
180
+ }
@@ -0,0 +1,105 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { chunkMarkdown, hashText } from "./files.js";
3
+ import { CHUNKS_TABLE, FTS_TABLE, VECTOR_TABLE } from "../store/schema.js";
4
+ import { embedChunks } from "./embeddings.js";
5
+ const DEFAULT_MODEL_ID = "fts";
6
+ export async function indexMemoryFile(params) {
7
+ const content = await readContent(params.entry);
8
+ const chunks = chunkMarkdown(content, params.chunking).filter((chunk) => chunk.text.trim().length > 0);
9
+ const now = Date.now();
10
+ const modelId = params.embeddings ? params.embeddings.provider.model : DEFAULT_MODEL_ID;
11
+ const vectorEnabled = params.embeddings?.vector.enabled ?? false;
12
+ if (vectorEnabled) {
13
+ try {
14
+ params.db
15
+ .prepare(`DELETE FROM ${VECTOR_TABLE} WHERE id IN (SELECT id FROM ${CHUNKS_TABLE} WHERE path = ? AND source = ? AND agent_id = ?)`)
16
+ .run(params.entry.path, params.source, params.agentId);
17
+ }
18
+ catch {
19
+ // Ignore vector cleanup errors.
20
+ }
21
+ }
22
+ if (params.ftsAvailable) {
23
+ try {
24
+ params.db
25
+ .prepare(`DELETE FROM ${FTS_TABLE} WHERE id IN (SELECT id FROM ${CHUNKS_TABLE} WHERE path = ? AND source = ? AND agent_id = ?)`)
26
+ .run(params.entry.path, params.source, params.agentId);
27
+ }
28
+ catch {
29
+ // Ignore FTS cleanup errors.
30
+ }
31
+ }
32
+ params.db
33
+ .prepare(`DELETE FROM ${CHUNKS_TABLE} WHERE path = ? AND source = ? AND agent_id = ?`)
34
+ .run(params.entry.path, params.source, params.agentId);
35
+ let indexedChunks = chunks;
36
+ let embeddings = indexedChunks.map(() => []);
37
+ if (params.embeddings) {
38
+ const embedded = await embedChunks({
39
+ db: params.db,
40
+ provider: params.embeddings.provider,
41
+ providerKey: params.embeddings.providerKey,
42
+ chunks: indexedChunks,
43
+ cache: params.embeddings.cache,
44
+ });
45
+ indexedChunks = embedded.chunks;
46
+ embeddings = embedded.embeddings;
47
+ }
48
+ const sampleEmbedding = embeddings.find((embedding) => embedding.length > 0);
49
+ const vectorReady = vectorEnabled && params.embeddings && sampleEmbedding
50
+ ? await params.embeddings.vector.ensureReady(sampleEmbedding.length)
51
+ : false;
52
+ for (let i = 0; i < indexedChunks.length; i += 1) {
53
+ const chunk = indexedChunks[i];
54
+ if (!chunk)
55
+ continue;
56
+ const embedding = embeddings[i] ?? [];
57
+ const id = buildChunkId({
58
+ agentId: params.agentId,
59
+ source: params.source,
60
+ path: params.entry.path,
61
+ chunk,
62
+ modelId,
63
+ });
64
+ params.db
65
+ .prepare(`INSERT INTO ${CHUNKS_TABLE} (id, path, agent_id, source, start_line, end_line, hash, model, text, embedding, updated_at)
66
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
67
+ ON CONFLICT(id) DO UPDATE SET
68
+ hash=excluded.hash,
69
+ model=excluded.model,
70
+ text=excluded.text,
71
+ embedding=excluded.embedding,
72
+ updated_at=excluded.updated_at`)
73
+ .run(id, params.entry.path, params.agentId, params.source, chunk.startLine, chunk.endLine, chunk.hash, modelId, chunk.text, JSON.stringify(embedding), now);
74
+ if (vectorReady && embedding.length > 0) {
75
+ try {
76
+ params.db
77
+ .prepare(`INSERT INTO ${VECTOR_TABLE} (id, embedding) VALUES (?, ?)`)
78
+ .run(id, vectorToBlob(embedding));
79
+ }
80
+ catch {
81
+ // Ignore vector insert errors.
82
+ }
83
+ }
84
+ if (params.ftsAvailable) {
85
+ try {
86
+ params.db
87
+ .prepare(`INSERT INTO ${FTS_TABLE} (text, id, path, source, model, start_line, end_line)
88
+ VALUES (?, ?, ?, ?, ?, ?, ?)`)
89
+ .run(chunk.text, id, params.entry.path, params.source, modelId, chunk.startLine, chunk.endLine);
90
+ }
91
+ catch {
92
+ // Ignore FTS insert errors.
93
+ }
94
+ }
95
+ }
96
+ }
97
+ function buildChunkId(params) {
98
+ return hashText(`${params.source}:${params.agentId}:${params.path}:${params.chunk.startLine}:${params.chunk.endLine}:${params.chunk.hash}:${params.modelId}`);
99
+ }
100
+ async function readContent(entry) {
101
+ return await readFile(entry.absPath, "utf-8");
102
+ }
103
+ function vectorToBlob(embedding) {
104
+ return Buffer.from(new Float32Array(embedding).buffer);
105
+ }
@@ -0,0 +1,38 @@
1
+ import { appendFile, mkdir } from "node:fs/promises";
2
+ import { getMemoryLogPath, getMemoryLogsDir } from "./paths.js";
3
+ let writeQueue = Promise.resolve();
4
+ function formatLine(level, message, data) {
5
+ const timestamp = new Date().toISOString();
6
+ const payload = data ? ` ${safeJson(data)}` : "";
7
+ return `${timestamp} [${level}] ${message}${payload}`;
8
+ }
9
+ function safeJson(data) {
10
+ try {
11
+ return JSON.stringify(data);
12
+ }
13
+ catch {
14
+ return "[unserializable]";
15
+ }
16
+ }
17
+ async function appendLogLine(line) {
18
+ await mkdir(getMemoryLogsDir(), { recursive: true });
19
+ await appendFile(getMemoryLogPath(), `${line}\n`, { encoding: "utf-8" });
20
+ }
21
+ function enqueue(level, message, data) {
22
+ const line = formatLine(level, message, data);
23
+ writeQueue = writeQueue.then(() => appendLogLine(line)).catch(() => { });
24
+ }
25
+ export const memoryLog = {
26
+ debug(message, data) {
27
+ enqueue("debug", message, data);
28
+ },
29
+ info(message, data) {
30
+ enqueue("info", message, data);
31
+ },
32
+ warn(message, data) {
33
+ enqueue("warn", message, data);
34
+ },
35
+ error(message, data) {
36
+ enqueue("error", message, data);
37
+ },
38
+ };
@@ -0,0 +1,15 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ export function getMemoryRootDir() {
4
+ return join(homedir(), ".mono-pilot", "memory");
5
+ }
6
+ export function getMemoryIndexPath() {
7
+ return join(getMemoryRootDir(), "index.sqlite");
8
+ }
9
+ export function getMemoryLogsDir() {
10
+ return join(homedir(), ".mono-pilot", "logs");
11
+ }
12
+ export function getMemoryLogPath(date = new Date()) {
13
+ const stamp = date.toISOString().slice(0, 10);
14
+ return join(getMemoryLogsDir(), `memory.${stamp}.log`);
15
+ }
@@ -0,0 +1,299 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { readFile } from "node:fs/promises";
5
+ import { loadResolvedMemorySearchConfig } from "../config/loader.js";
6
+ import { publishSystemEvent } from "../../extensions/system-events.js";
7
+ // When running via tsx (dev), import.meta.url points to src/ but worker threads
8
+ // need compiled JS. Detect this and redirect to dist/.
9
+ function resolveWorkerPath() {
10
+ const thisFile = fileURLToPath(import.meta.url);
11
+ const thisDir = dirname(thisFile);
12
+ const threadFile = join(thisDir, "thread.js");
13
+ if (thisDir.includes(`${join("dist", "src")}`)) {
14
+ return threadFile;
15
+ }
16
+ const projectRoot = resolve(thisDir, "../../..");
17
+ return join(projectRoot, "dist/src/memory/runtime/thread.js");
18
+ }
19
+ class WorkerMemoryProxy {
20
+ worker;
21
+ nextId = 1;
22
+ pending = new Map();
23
+ syncWorkDetectedHandlers = new Map();
24
+ dirty = true;
25
+ ready;
26
+ readyReject = null;
27
+ embeddingProvider = null;
28
+ constructor(params) {
29
+ const initData = {
30
+ agentId: params.agentId,
31
+ workspaceDir: params.workspaceDir,
32
+ settings: params.settings,
33
+ embedModel: params.embedModel,
34
+ };
35
+ this.worker = new Worker(resolveWorkerPath(), {
36
+ workerData: initData,
37
+ execArgv: ["--no-warnings"],
38
+ });
39
+ // Don't let the worker prevent process exit as a last resort.
40
+ this.worker.unref();
41
+ this.ready = new Promise((resolve, reject) => {
42
+ this.readyReject = reject;
43
+ const onMessage = (msg) => {
44
+ if (msg.type === "ready") {
45
+ this.worker.removeListener("message", onMessage);
46
+ this.readyReject = null;
47
+ resolve();
48
+ }
49
+ };
50
+ this.worker.on("message", onMessage);
51
+ });
52
+ this.worker.on("message", (msg) => {
53
+ if (msg.type === "dirty") {
54
+ this.dirty = msg.value;
55
+ return;
56
+ }
57
+ if (msg.type === "autoSyncEvent") {
58
+ this.publishAutoSyncEvent(msg.event);
59
+ return;
60
+ }
61
+ if (msg.type === "syncWorkDetected") {
62
+ const handler = this.syncWorkDetectedHandlers.get(msg.requestId);
63
+ if (!handler) {
64
+ return;
65
+ }
66
+ this.syncWorkDetectedHandlers.delete(msg.requestId);
67
+ handler();
68
+ return;
69
+ }
70
+ if (msg.type === "ready")
71
+ return;
72
+ // Embed request from worker — forward to main-thread provider
73
+ if (msg.type === "embed") {
74
+ void this.handleEmbedRequest(msg);
75
+ return;
76
+ }
77
+ // WorkerResponse
78
+ const pending = this.pending.get(msg.id);
79
+ if (!pending)
80
+ return;
81
+ this.pending.delete(msg.id);
82
+ this.syncWorkDetectedHandlers.delete(msg.id);
83
+ if (msg.type === "error") {
84
+ pending.reject(new Error(msg.message));
85
+ }
86
+ else {
87
+ pending.resolve(msg.data);
88
+ }
89
+ });
90
+ this.worker.on("error", (err) => {
91
+ this.rejectReady(err);
92
+ this.rejectAll(err);
93
+ });
94
+ this.worker.on("exit", (code) => {
95
+ const err = new Error(`Memory worker exited with code ${code}`);
96
+ this.rejectReady(err);
97
+ if (code !== 0) {
98
+ this.rejectAll(err);
99
+ }
100
+ });
101
+ }
102
+ async search(query, opts) {
103
+ await this.ready;
104
+ return this.send({ type: "search", query, opts });
105
+ }
106
+ // File read only — no DB, runs on main thread.
107
+ async get(path, from, lines) {
108
+ const raw = await readFile(path, "utf-8");
109
+ if (from === undefined && lines === undefined) {
110
+ return { path, text: raw };
111
+ }
112
+ const startLine = Math.max(1, Math.floor(from ?? 1));
113
+ const maxLines = lines !== undefined ? Math.max(0, Math.floor(lines)) : undefined;
114
+ const allLines = raw.split("\n");
115
+ const startIndex = Math.min(allLines.length, Math.max(0, startLine - 1));
116
+ const endIndex = maxLines === undefined ? allLines.length : Math.min(allLines.length, startIndex + maxLines);
117
+ return { path, text: allLines.slice(startIndex, endIndex).join("\n") };
118
+ }
119
+ async sync(opts) {
120
+ await this.ready;
121
+ const onWorkDetected = opts?.onWorkDetected;
122
+ let requestOpts;
123
+ if (opts) {
124
+ const { onWorkDetected: _ignored, ...rest } = opts;
125
+ requestOpts = rest;
126
+ }
127
+ const id = this.nextId++;
128
+ await new Promise((resolve, reject) => {
129
+ if (onWorkDetected) {
130
+ this.syncWorkDetectedHandlers.set(id, onWorkDetected);
131
+ }
132
+ this.pending.set(id, {
133
+ resolve: () => {
134
+ this.syncWorkDetectedHandlers.delete(id);
135
+ resolve();
136
+ },
137
+ reject: (err) => {
138
+ this.syncWorkDetectedHandlers.delete(id);
139
+ reject(err);
140
+ },
141
+ });
142
+ const req = {
143
+ id,
144
+ type: "sync",
145
+ opts: requestOpts,
146
+ notifyWorkDetected: Boolean(onWorkDetected),
147
+ };
148
+ this.worker.postMessage(req);
149
+ });
150
+ }
151
+ async syncDirty() {
152
+ await this.ready;
153
+ return this.send({ type: "syncDirty" });
154
+ }
155
+ isDirty() {
156
+ return this.dirty;
157
+ }
158
+ /** Set the embedding provider used to serve worker embed requests. */
159
+ setEmbeddingProvider(provider) {
160
+ this.embeddingProvider = provider;
161
+ }
162
+ async handleEmbedRequest(req) {
163
+ try {
164
+ if (!this.embeddingProvider) {
165
+ throw new Error("No embedding provider available");
166
+ }
167
+ const data = await this.embeddingProvider.embedBatch(req.texts);
168
+ this.worker.postMessage({ type: "embedResult", id: req.id, data });
169
+ }
170
+ catch (err) {
171
+ this.worker.postMessage({
172
+ type: "embedResult",
173
+ id: req.id,
174
+ error: err instanceof Error ? err.message : String(err),
175
+ });
176
+ }
177
+ }
178
+ async close() {
179
+ const CLOSE_TIMEOUT_MS = 3000;
180
+ try {
181
+ await Promise.race([
182
+ this.send({ type: "close" }),
183
+ new Promise((_, reject) => {
184
+ const timer = setTimeout(() => reject(new Error("close timeout")), CLOSE_TIMEOUT_MS);
185
+ timer.unref();
186
+ }),
187
+ ]);
188
+ }
189
+ catch {
190
+ // Worker may already be gone.
191
+ }
192
+ // Force-terminate without waiting — don't block process exit.
193
+ this.worker.terminate().catch(() => { });
194
+ this.pending.clear();
195
+ this.syncWorkDetectedHandlers.clear();
196
+ }
197
+ send(partial) {
198
+ const id = this.nextId++;
199
+ const req = { ...partial, id };
200
+ return new Promise((resolve, reject) => {
201
+ this.pending.set(id, { resolve, reject });
202
+ this.worker.postMessage(req);
203
+ });
204
+ }
205
+ rejectReady(err) {
206
+ if (this.readyReject) {
207
+ this.readyReject(err);
208
+ this.readyReject = null;
209
+ }
210
+ }
211
+ publishAutoSyncEvent(event) {
212
+ const reasonLabel = event.reason === "search" ? "on-search" : event.reason;
213
+ if (event.phase === "start") {
214
+ publishSystemEvent({
215
+ source: "memory",
216
+ level: "info",
217
+ message: `Memory sync in progress (${reasonLabel}).`,
218
+ dedupeKey: `memory|auto_sync|${event.reason}|start`,
219
+ toast: false,
220
+ });
221
+ return;
222
+ }
223
+ if (event.phase === "complete") {
224
+ const indexed = event.indexed ?? 0;
225
+ const removed = event.staleDeleted ?? 0;
226
+ publishSystemEvent({
227
+ source: "memory",
228
+ level: "info",
229
+ message: `Memory sync complete (${reasonLabel}): indexed=${indexed}, removed=${removed}.`,
230
+ dedupeKey: `memory|auto_sync|${event.reason}|complete`,
231
+ toast: false,
232
+ });
233
+ return;
234
+ }
235
+ publishSystemEvent({
236
+ source: "memory",
237
+ level: "warning",
238
+ message: `Memory sync failed (${reasonLabel}): ${event.error ?? "unknown error"}`,
239
+ dedupeKey: `memory|auto_sync|${event.reason}|failed`,
240
+ toast: false,
241
+ });
242
+ }
243
+ rejectAll(err) {
244
+ this.syncWorkDetectedHandlers.clear();
245
+ for (const pending of this.pending.values()) {
246
+ pending.reject(err);
247
+ }
248
+ this.pending.clear();
249
+ }
250
+ }
251
+ // --- Singleton registry ---
252
+ const managerCache = new Map();
253
+ let defaultEmbeddingProvider = null;
254
+ export async function getMemorySearchManager(params) {
255
+ const settings = await loadResolvedMemorySearchConfig();
256
+ if (!settings.enabled)
257
+ return null;
258
+ if (!settings.sources.includes("memory") && !settings.sources.includes("sessions"))
259
+ return null;
260
+ const key = params.agentId;
261
+ const cached = managerCache.get(key);
262
+ if (cached)
263
+ return cached;
264
+ const manager = new WorkerMemoryProxy({
265
+ agentId: params.agentId,
266
+ workspaceDir: params.workspaceDir,
267
+ settings,
268
+ embedModel: defaultEmbeddingProvider?.model,
269
+ });
270
+ if (defaultEmbeddingProvider) {
271
+ manager.setEmbeddingProvider(defaultEmbeddingProvider);
272
+ }
273
+ managerCache.set(key, manager);
274
+ return manager;
275
+ }
276
+ export async function closeMemorySearchManagers() {
277
+ const managers = Array.from(managerCache.values());
278
+ managerCache.clear();
279
+ for (const manager of managers) {
280
+ if (manager.close) {
281
+ await manager.close();
282
+ }
283
+ }
284
+ }
285
+ export function peekMemorySearchManager(params) {
286
+ const key = params.agentId;
287
+ return managerCache.get(key) ?? null;
288
+ }
289
+ /** Set the embedding provider on all active worker proxies. */
290
+ export function setMemoryWorkersEmbeddingProvider(provider) {
291
+ defaultEmbeddingProvider = provider;
292
+ for (const proxy of managerCache.values()) {
293
+ proxy.setEmbeddingProvider(provider);
294
+ }
295
+ }
296
+ /** Get the current default embedding provider (if any). */
297
+ export function getDefaultEmbeddingProvider() {
298
+ return defaultEmbeddingProvider;
299
+ }