gnosys 5.11.4 → 5.12.2

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 (265) hide show
  1. package/dist/cli.js +377 -5162
  2. package/dist/index.js +542 -244
  3. package/dist/lib/addCommand.d.ts +9 -0
  4. package/dist/lib/addCommand.js +102 -0
  5. package/dist/lib/addStructuredCommand.d.ts +16 -0
  6. package/dist/lib/addStructuredCommand.js +103 -0
  7. package/dist/lib/ambiguityCommand.d.ts +4 -0
  8. package/dist/lib/ambiguityCommand.js +36 -0
  9. package/dist/lib/apiKeyVault.d.ts +78 -0
  10. package/dist/lib/apiKeyVault.js +447 -0
  11. package/dist/lib/archive.js +0 -2
  12. package/dist/lib/askCommand.d.ts +13 -0
  13. package/dist/lib/askCommand.js +145 -0
  14. package/dist/lib/attachCommand.d.ts +17 -0
  15. package/dist/lib/attachCommand.js +66 -0
  16. package/dist/lib/attachments.d.ts +43 -2
  17. package/dist/lib/attachments.js +81 -2
  18. package/dist/lib/audioExtract.js +4 -1
  19. package/dist/lib/auditCommand.d.ts +7 -0
  20. package/dist/lib/auditCommand.js +27 -0
  21. package/dist/lib/backupCommand.d.ts +6 -0
  22. package/dist/lib/backupCommand.js +54 -0
  23. package/dist/lib/bootstrapCommand.d.ts +15 -0
  24. package/dist/lib/bootstrapCommand.js +51 -0
  25. package/dist/lib/briefingCommand.d.ts +7 -0
  26. package/dist/lib/briefingCommand.js +92 -0
  27. package/dist/lib/centralizeCommand.d.ts +5 -0
  28. package/dist/lib/centralizeCommand.js +16 -0
  29. package/dist/lib/chat/choose.js +2 -2
  30. package/dist/lib/chatCommand.d.ts +12 -0
  31. package/dist/lib/chatCommand.js +46 -0
  32. package/dist/lib/checkCommand.d.ts +4 -0
  33. package/dist/lib/checkCommand.js +133 -0
  34. package/dist/lib/clientReadOverlay.d.ts +27 -0
  35. package/dist/lib/clientReadOverlay.js +76 -0
  36. package/dist/lib/clientReadResolve.d.ts +32 -0
  37. package/dist/lib/clientReadResolve.js +84 -0
  38. package/dist/lib/commitContextCommand.d.ts +9 -0
  39. package/dist/lib/commitContextCommand.js +142 -0
  40. package/dist/lib/config.d.ts +41 -48
  41. package/dist/lib/config.js +58 -57
  42. package/dist/lib/configCommand.d.ts +10 -0
  43. package/dist/lib/configCommand.js +321 -0
  44. package/dist/lib/connectCommand.d.ts +8 -0
  45. package/dist/lib/connectCommand.js +19 -0
  46. package/dist/lib/db.d.ts +68 -1
  47. package/dist/lib/db.js +385 -120
  48. package/dist/lib/dbWrite.d.ts +1 -1
  49. package/dist/lib/dearchiveCommand.d.ts +7 -0
  50. package/dist/lib/dearchiveCommand.js +41 -0
  51. package/dist/lib/discoverCommand.d.ts +9 -0
  52. package/dist/lib/discoverCommand.js +87 -0
  53. package/dist/lib/doctorCommand.d.ts +6 -0
  54. package/dist/lib/doctorCommand.js +256 -0
  55. package/dist/lib/docxExtract.js +1 -1
  56. package/dist/lib/dream.d.ts +50 -2
  57. package/dist/lib/dream.js +324 -30
  58. package/dist/lib/dreamCommand.d.ts +10 -0
  59. package/dist/lib/dreamCommand.js +195 -0
  60. package/dist/lib/dreamLaunchd.d.ts +2 -0
  61. package/dist/lib/dreamLaunchd.js +72 -0
  62. package/dist/lib/dreamLogCommand.d.ts +10 -0
  63. package/dist/lib/dreamLogCommand.js +58 -0
  64. package/dist/lib/dreamReport.d.ts +7 -0
  65. package/dist/lib/dreamReport.js +114 -0
  66. package/dist/lib/dreamRunLog.d.ts +121 -0
  67. package/dist/lib/dreamRunLog.js +234 -0
  68. package/dist/lib/embeddings.js +3 -3
  69. package/dist/lib/exportCommand.d.ts +18 -0
  70. package/dist/lib/exportCommand.js +101 -0
  71. package/dist/lib/exportProject.d.ts +3 -2
  72. package/dist/lib/exportProject.js +2 -1
  73. package/dist/lib/federated.js +1 -1
  74. package/dist/lib/fsearchCommand.d.ts +8 -0
  75. package/dist/lib/fsearchCommand.js +44 -0
  76. package/dist/lib/graphCommand.d.ts +4 -0
  77. package/dist/lib/graphCommand.js +68 -0
  78. package/dist/lib/helperGenerateCommand.d.ts +5 -0
  79. package/dist/lib/helperGenerateCommand.js +27 -0
  80. package/dist/lib/historyCommand.d.ts +5 -0
  81. package/dist/lib/historyCommand.js +51 -0
  82. package/dist/lib/hybridSearchCommand.d.ts +12 -0
  83. package/dist/lib/hybridSearchCommand.js +95 -0
  84. package/dist/lib/importCommand.d.ts +16 -0
  85. package/dist/lib/importCommand.js +89 -0
  86. package/dist/lib/importProject.js +2 -1
  87. package/dist/lib/importProjectCommand.d.ts +6 -0
  88. package/dist/lib/importProjectCommand.js +43 -0
  89. package/dist/lib/ingestCommand.d.ts +13 -0
  90. package/dist/lib/ingestCommand.js +95 -0
  91. package/dist/lib/installOutput.d.ts +36 -0
  92. package/dist/lib/installOutput.js +55 -0
  93. package/dist/lib/lensCommand.d.ts +20 -0
  94. package/dist/lib/lensCommand.js +61 -0
  95. package/dist/lib/lensing.d.ts +1 -0
  96. package/dist/lib/lensing.js +50 -9
  97. package/dist/lib/linksCommand.d.ts +7 -0
  98. package/dist/lib/linksCommand.js +48 -0
  99. package/dist/lib/listCommand.d.ts +8 -0
  100. package/dist/lib/listCommand.js +74 -0
  101. package/dist/lib/llm.d.ts +1 -1
  102. package/dist/lib/llm.js +27 -9
  103. package/dist/lib/localDiskCheck.d.ts +17 -0
  104. package/dist/lib/localDiskCheck.js +54 -0
  105. package/dist/lib/lock.d.ts +1 -1
  106. package/dist/lib/lock.js +5 -3
  107. package/dist/lib/machineConfig.d.ts +11 -1
  108. package/dist/lib/machineConfig.js +16 -0
  109. package/dist/lib/machineRegistry.d.ts +61 -0
  110. package/dist/lib/machineRegistry.js +80 -0
  111. package/dist/lib/maintainCommand.d.ts +8 -0
  112. package/dist/lib/maintainCommand.js +34 -0
  113. package/dist/lib/masterLease.d.ts +20 -0
  114. package/dist/lib/masterLease.js +68 -0
  115. package/dist/lib/migrate.js +0 -1
  116. package/dist/lib/migrateCommand.d.ts +7 -0
  117. package/dist/lib/migrateCommand.js +158 -0
  118. package/dist/lib/migrateDbCommand.d.ts +9 -0
  119. package/dist/lib/migrateDbCommand.js +94 -0
  120. package/dist/lib/modelValidation.d.ts +5 -0
  121. package/dist/lib/modelValidation.js +27 -0
  122. package/dist/lib/multimodalIngest.js +1 -1
  123. package/dist/lib/openrouterTiers.d.ts +29 -0
  124. package/dist/lib/openrouterTiers.js +113 -0
  125. package/dist/lib/platform.d.ts +0 -6
  126. package/dist/lib/platform.js +0 -28
  127. package/dist/lib/prefCommand.d.ts +10 -0
  128. package/dist/lib/prefCommand.js +118 -0
  129. package/dist/lib/projectsCommand.d.ts +8 -0
  130. package/dist/lib/projectsCommand.js +131 -0
  131. package/dist/lib/readCommand.d.ts +7 -0
  132. package/dist/lib/readCommand.js +63 -0
  133. package/dist/lib/recall.d.ts +3 -0
  134. package/dist/lib/recall.js +19 -4
  135. package/dist/lib/recallCommand.d.ts +11 -0
  136. package/dist/lib/recallCommand.js +112 -0
  137. package/dist/lib/reflectCommand.d.ts +8 -0
  138. package/dist/lib/reflectCommand.js +61 -0
  139. package/dist/lib/reindexCommand.d.ts +4 -0
  140. package/dist/lib/reindexCommand.js +34 -0
  141. package/dist/lib/reindexGraphCommand.d.ts +4 -0
  142. package/dist/lib/reindexGraphCommand.js +12 -0
  143. package/dist/lib/reinforceCommand.d.ts +8 -0
  144. package/dist/lib/reinforceCommand.js +40 -0
  145. package/dist/lib/remote.d.ts +5 -1
  146. package/dist/lib/remote.js +5 -1
  147. package/dist/lib/remoteWizard.d.ts +24 -5
  148. package/dist/lib/remoteWizard.js +308 -319
  149. package/dist/lib/restoreCommand.d.ts +5 -0
  150. package/dist/lib/restoreCommand.js +35 -0
  151. package/dist/lib/rulesGen.d.ts +8 -0
  152. package/dist/lib/rulesGen.js +16 -0
  153. package/dist/lib/sandboxStartCommand.d.ts +6 -0
  154. package/dist/lib/sandboxStartCommand.js +25 -0
  155. package/dist/lib/sandboxStatusCommand.d.ts +4 -0
  156. package/dist/lib/sandboxStatusCommand.js +24 -0
  157. package/dist/lib/sandboxStopCommand.d.ts +4 -0
  158. package/dist/lib/sandboxStopCommand.js +21 -0
  159. package/dist/lib/search.d.ts +0 -2
  160. package/dist/lib/search.js +0 -7
  161. package/dist/lib/searchCommand.d.ts +9 -0
  162. package/dist/lib/searchCommand.js +90 -0
  163. package/dist/lib/semanticSearchCommand.d.ts +8 -0
  164. package/dist/lib/semanticSearchCommand.js +52 -0
  165. package/dist/lib/setup/configSetRender.js +2 -0
  166. package/dist/lib/setup/providerGlyphs.d.ts +19 -0
  167. package/dist/lib/setup/providerGlyphs.js +42 -0
  168. package/dist/lib/setup/remoteRender.d.ts +31 -1
  169. package/dist/lib/setup/remoteRender.js +95 -4
  170. package/dist/lib/setup/sections/providers.d.ts +17 -0
  171. package/dist/lib/setup/sections/providers.js +307 -0
  172. package/dist/lib/setup/sections/routing.d.ts +2 -6
  173. package/dist/lib/setup/sections/routing.js +67 -82
  174. package/dist/lib/setup/sections/taskRoutingEditor.d.ts +13 -0
  175. package/dist/lib/setup/sections/taskRoutingEditor.js +139 -0
  176. package/dist/lib/setup/summary.d.ts +9 -0
  177. package/dist/lib/setup/summary.js +51 -37
  178. package/dist/lib/setup/ui/header.js +0 -1
  179. package/dist/lib/setup.d.ts +105 -15
  180. package/dist/lib/setup.js +747 -287
  181. package/dist/lib/setupKeys.d.ts +42 -0
  182. package/dist/lib/setupKeys.js +564 -0
  183. package/dist/lib/setupRemoteCommand.d.ts +4 -0
  184. package/dist/lib/setupRemoteCommand.js +28 -0
  185. package/dist/lib/setupRemotePullCommand.d.ts +5 -0
  186. package/dist/lib/setupRemotePullCommand.js +52 -0
  187. package/dist/lib/setupRemotePushCommand.d.ts +5 -0
  188. package/dist/lib/setupRemotePushCommand.js +57 -0
  189. package/dist/lib/setupRemoteResolveCommand.d.ts +4 -0
  190. package/dist/lib/setupRemoteResolveCommand.js +48 -0
  191. package/dist/lib/setupRemoteStatusCommand.d.ts +4 -0
  192. package/dist/lib/setupRemoteStatusCommand.js +73 -0
  193. package/dist/lib/setupRemoteSyncCommand.d.ts +6 -0
  194. package/dist/lib/setupRemoteSyncCommand.js +65 -0
  195. package/dist/lib/setupSyncProjectsCommand.d.ts +4 -0
  196. package/dist/lib/setupSyncProjectsCommand.js +292 -0
  197. package/dist/lib/staleCommand.d.ts +8 -0
  198. package/dist/lib/staleCommand.js +34 -0
  199. package/dist/lib/statsCommand.d.ts +6 -0
  200. package/dist/lib/statsCommand.js +142 -0
  201. package/dist/lib/statusCommand.d.ts +18 -0
  202. package/dist/lib/statusCommand.js +250 -0
  203. package/dist/lib/storesCommand.d.ts +2 -0
  204. package/dist/lib/storesCommand.js +4 -0
  205. package/dist/lib/syncClient.d.ts +41 -0
  206. package/dist/lib/syncClient.js +234 -0
  207. package/dist/lib/syncCommand.d.ts +6 -0
  208. package/dist/lib/syncCommand.js +57 -0
  209. package/dist/lib/syncDoctorCommand.d.ts +5 -0
  210. package/dist/lib/syncDoctorCommand.js +100 -0
  211. package/dist/lib/syncIngest.d.ts +30 -0
  212. package/dist/lib/syncIngest.js +175 -0
  213. package/dist/lib/syncIngestLaunchd.d.ts +8 -0
  214. package/dist/lib/syncIngestLaunchd.js +93 -0
  215. package/dist/lib/syncIngestStartup.d.ts +5 -0
  216. package/dist/lib/syncIngestStartup.js +29 -0
  217. package/dist/lib/syncIngestSystemd.d.ts +10 -0
  218. package/dist/lib/syncIngestSystemd.js +97 -0
  219. package/dist/lib/syncIngestTimer.d.ts +8 -0
  220. package/dist/lib/syncIngestTimer.js +27 -0
  221. package/dist/lib/syncIngestTimerCommand.d.ts +7 -0
  222. package/dist/lib/syncIngestTimerCommand.js +83 -0
  223. package/dist/lib/syncLock.d.ts +6 -0
  224. package/dist/lib/syncLock.js +74 -0
  225. package/dist/lib/syncSnapshot.d.ts +32 -0
  226. package/dist/lib/syncSnapshot.js +188 -0
  227. package/dist/lib/syncStaging.d.ts +79 -0
  228. package/dist/lib/syncStaging.js +237 -0
  229. package/dist/lib/tagsAddCommand.d.ts +8 -0
  230. package/dist/lib/tagsAddCommand.js +18 -0
  231. package/dist/lib/tagsCommand.d.ts +4 -0
  232. package/dist/lib/tagsCommand.js +16 -0
  233. package/dist/lib/timelineCommand.d.ts +7 -0
  234. package/dist/lib/timelineCommand.js +49 -0
  235. package/dist/lib/traceCommand.d.ts +6 -0
  236. package/dist/lib/traceCommand.js +39 -0
  237. package/dist/lib/traverseCommand.d.ts +6 -0
  238. package/dist/lib/traverseCommand.js +58 -0
  239. package/dist/lib/updateCommand.d.ts +13 -0
  240. package/dist/lib/updateCommand.js +67 -0
  241. package/dist/lib/updateStatusCommand.d.ts +5 -0
  242. package/dist/lib/updateStatusCommand.js +38 -0
  243. package/dist/lib/webAddCommand.d.ts +8 -0
  244. package/dist/lib/webAddCommand.js +55 -0
  245. package/dist/lib/webBuildCommand.d.ts +10 -0
  246. package/dist/lib/webBuildCommand.js +65 -0
  247. package/dist/lib/webBuildIndexCommand.d.ts +8 -0
  248. package/dist/lib/webBuildIndexCommand.js +37 -0
  249. package/dist/lib/webIndex.js +0 -1
  250. package/dist/lib/webIngestCommand.d.ts +11 -0
  251. package/dist/lib/webIngestCommand.js +51 -0
  252. package/dist/lib/webInitCommand.d.ts +9 -0
  253. package/dist/lib/webInitCommand.js +167 -0
  254. package/dist/lib/webRemoveCommand.d.ts +5 -0
  255. package/dist/lib/webRemoveCommand.js +41 -0
  256. package/dist/lib/webStatusCommand.d.ts +5 -0
  257. package/dist/lib/webStatusCommand.js +94 -0
  258. package/dist/lib/webUpdateCommand.d.ts +7 -0
  259. package/dist/lib/webUpdateCommand.js +72 -0
  260. package/dist/lib/workingSetCommand.d.ts +6 -0
  261. package/dist/lib/workingSetCommand.js +37 -0
  262. package/dist/sandbox/client.js +1 -1
  263. package/dist/sandbox/manager.js +1 -14
  264. package/dist/sandbox/server.js +3 -5
  265. package/package.json +6 -2
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@
10
10
  // MCP stdio JSON protocol. parse() is a pure function with no side effects.
11
11
  import dotenv from "dotenv";
12
12
  import path from "path";
13
+ import { AsyncLocalStorage } from "async_hooks";
13
14
  import { readFileSync, realpathSync } from "fs";
14
15
  import { fileURLToPath } from "url";
15
16
  const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
@@ -49,15 +50,20 @@ import { groupByPeriod, computeStats } from "./lib/timeline.js";
49
50
  import { buildLinkGraph, getBacklinks, getOutgoingLinks, formatGraphSummary } from "./lib/wikilinks.js";
50
51
  import { loadConfig, DEFAULT_CONFIG } from "./lib/config.js";
51
52
  import { getLLMProvider } from "./lib/llm.js";
52
- import { recall, formatRecall } from "./lib/recall.js";
53
+ import { recall, formatRecall, } from "./lib/recall.js";
53
54
  import { initAudit, readAuditLog, formatAuditTimeline } from "./lib/audit.js";
55
+ import { logError } from "./lib/log.js";
54
56
  import { GnosysDB } from "./lib/db.js";
55
57
  import { syncMemoryToDb, syncUpdateToDb, syncDearchiveToDb, syncReinforcementToDb, auditToDb } from "./lib/dbWrite.js";
56
- import { createProjectIdentity, readProjectIdentity } from "./lib/projectIdentity.js";
58
+ import { createProjectIdentity, readProjectIdentity, } from "./lib/projectIdentity.js";
57
59
  import { setPreference, getPreference, getAllPreferences, deletePreference, KNOWN_PREFERENCE_KEYS, suggestPreferenceKey } from "./lib/preferences.js";
58
- import { syncRules, generateRulesBlock } from "./lib/rulesGen.js";
60
+ import { syncRules, generateRulesBlock, } from "./lib/rulesGen.js";
59
61
  import { federatedSearch, detectAmbiguity, generateBriefing, generateAllBriefings, getWorkingSet, formatWorkingSet, detectCurrentProject } from "./lib/federated.js";
60
62
  import { generatePortfolio, formatPortfolioCompact, formatPortfolioMarkdown, generateStatusPrompt } from "./lib/portfolio.js";
63
+ import { applyPendingOverlay, mergeOverlayDiscoverResults, mergeOverlaySearchResults, pendingAddToDbMemory, } from "./lib/clientReadOverlay.js";
64
+ import { readMachineConfig } from "./lib/machineConfig.js";
65
+ import { getConfiguredRemotePath } from "./lib/remote.js";
66
+ import { closeClientReadContext, openClientReadContext, } from "./lib/syncClient.js";
61
67
  // Initialize resolver (discovers all layered stores)
62
68
  const resolver = new GnosysResolver();
63
69
  let config = DEFAULT_CONFIG;
@@ -67,9 +73,47 @@ const server = new McpServer({
67
73
  version: "2.0.0",
68
74
  });
69
75
  const _registrations = [];
76
+ // v5.12.1 reliability: every tool handler resolves a ToolContext whose
77
+ // clientRead (v13 sync) may own a DB handle. Historically only 8 of 52
78
+ // handlers released it, leaking handles on every early return. Enforce
79
+ // release centrally: resolveToolContext() registers each context in the
80
+ // per-call AsyncLocalStorage store, and withContextRelease() (wrapped around
81
+ // every tool handler at registration) releases them when the handler settles
82
+ // — every return path, every throw, every future tool, no per-handler code.
83
+ const activeToolContexts = new AsyncLocalStorage();
84
+ function withContextRelease(handler, toolName) {
85
+ return (...hargs) => {
86
+ const opened = [];
87
+ return activeToolContexts.run(opened, async () => {
88
+ try {
89
+ return await handler(...hargs);
90
+ }
91
+ catch (err) {
92
+ // v5.12.x observability: last-resort error envelope. 19 of 52 tools
93
+ // had no catch at all — a throw reached the SDK as a raw JSON-RPC
94
+ // error with no corruption-recovery guidance. Per-tool catches with
95
+ // more specific messages still take precedence; this only sees what
96
+ // they let through. Logged to stderr (stdout is JSON-RPC).
97
+ logError(err, { module: "mcp", op: toolName });
98
+ return {
99
+ content: [{ type: "text", text: formatMcpError(`in ${toolName}`, err) }],
100
+ isError: true,
101
+ };
102
+ }
103
+ finally {
104
+ for (const c of opened)
105
+ releaseClientReadFromContext(c);
106
+ }
107
+ });
108
+ };
109
+ }
70
110
  // Typed to the McpServer methods so call-site generic inference (Zod schema →
71
111
  // handler arg types) is preserved; the body just collects a replay thunk.
72
112
  const regTool = ((...args) => {
113
+ const last = args.length - 1;
114
+ if (typeof args[last] === "function") {
115
+ args[last] = withContextRelease(args[last], typeof args[0] === "string" ? args[0] : "tool");
116
+ }
73
117
  _registrations.push((s) => s.tool(...args));
74
118
  });
75
119
  const regPrompt = ((...args) => {
@@ -136,14 +180,46 @@ let askEngine = null;
136
180
  let gnosysDb = null;
137
181
  /** v3.0: Central DB at ~/.gnosys/gnosys.db */
138
182
  let centralDb = null;
139
- /** v2.0: Dream scheduler (idle-time consolidation) */
140
- let dreamScheduler = null;
141
183
  // ─── Multi-Project Support ───────────────────────────────────────────────
142
184
  // Each tool call can optionally pass a `projectRoot` to target a specific
143
185
  // project's .gnosys store. This is STATELESS — no race conditions when
144
186
  // multiple agents call tools in parallel.
145
187
  /** Common Zod schema fragment for projectRoot parameter */
146
188
  const projectRootParam = z.string().optional().describe("Optional project root path for multi-project support. When provided, this tool operates on projectRoot/.gnosys instead of the default store. Use gnosys_stores to see all available stores.");
189
+ function applyClientReadToCentralDb(localDb) {
190
+ if (!localDb?.isAvailable()) {
191
+ return { centralDb: localDb, clientRead: null };
192
+ }
193
+ const mc = readMachineConfig();
194
+ if (!mc?.remote.enabled || mc.remote.role !== "client") {
195
+ return { centralDb: localDb, clientRead: null };
196
+ }
197
+ const masterPath = getConfiguredRemotePath(localDb);
198
+ if (!masterPath)
199
+ return { centralDb: localDb, clientRead: null };
200
+ const clientRead = openClientReadContext(localDb, masterPath, mc.machineId);
201
+ return { centralDb: clientRead.db, clientRead };
202
+ }
203
+ /** Idempotent: safe to call from both a handler's own finally and the central
204
+ * withContextRelease wrapper. */
205
+ function releaseClientReadFromContext(ctx) {
206
+ if (ctx.clientRead) {
207
+ closeClientReadContext(ctx.clientRead);
208
+ ctx.clientRead = null;
209
+ }
210
+ // v5.12.x perf/leak: projectRoot-scoped contexts open their own
211
+ // GnosysSearch (SQLite handle to <store>/.config/search.db) per call.
212
+ if (ctx.ownsSearch && ctx.search) {
213
+ try {
214
+ ctx.search.close();
215
+ }
216
+ catch {
217
+ // already closed / never opened — fine
218
+ }
219
+ ctx.search = null;
220
+ ctx.ownsSearch = false;
221
+ }
222
+ }
147
223
  async function resolveToolContext(projectRoot) {
148
224
  if (!projectRoot) {
149
225
  // Default context — use module-level state
@@ -155,16 +231,20 @@ async function resolveToolContext(projectRoot) {
155
231
  const identity = await readProjectIdentity(parentDir);
156
232
  projectId = identity?.projectId || null;
157
233
  }
158
- return {
234
+ const applied = applyClientReadToCentralDb(centralDb);
235
+ const ctx = {
159
236
  resolver,
160
237
  store: writeTarget?.store || null,
161
238
  storePath: writeTarget?.store.getStorePath() || "",
162
239
  config,
163
240
  search,
164
241
  gnosysDb,
165
- centralDb,
242
+ centralDb: applied.centralDb,
166
243
  projectId,
244
+ clientRead: applied.clientRead,
167
245
  };
246
+ activeToolContexts.getStore()?.push(ctx);
247
+ return ctx;
168
248
  }
169
249
  // Scoped context — resolve for this specific project
170
250
  const scopedResolver = await GnosysResolver.resolveForProject(projectRoot);
@@ -192,16 +272,21 @@ async function resolveToolContext(projectRoot) {
192
272
  // Removed: new GnosysDB(scopedStorePath) which created an empty
193
273
  // gnosys.db in the project's .gnosys/ directory.
194
274
  }
195
- return {
275
+ const applied = applyClientReadToCentralDb(centralDb);
276
+ const ctx = {
196
277
  resolver: scopedResolver,
197
278
  store: scopedWriteTarget?.store || null,
198
279
  storePath: scopedStorePath,
199
280
  config: scopedConfig,
200
281
  search: scopedSearch,
201
282
  gnosysDb: scopedDb,
202
- centralDb,
283
+ centralDb: applied.centralDb,
203
284
  projectId,
285
+ clientRead: applied.clientRead,
286
+ ownsSearch: scopedSearch !== null,
204
287
  };
288
+ activeToolContexts.getStore()?.push(ctx);
289
+ return ctx;
205
290
  }
206
291
  /**
207
292
  * v5.7.1 (#13): Resolve scope + projectId for a memory write.
@@ -244,50 +329,65 @@ regTool("gnosys_discover", "Discover relevant memories by describing what you're
244
329
  projectRoot: projectRootParam,
245
330
  }, async ({ query, limit, projectRoot }) => {
246
331
  const ctx = await resolveToolContext(projectRoot);
247
- // v2.0 DB-backed fast path
248
- if (ctx.centralDb?.isAvailable() && ctx.centralDb?.isMigrated()) {
249
- const results = ctx.centralDb.discoverFts(query, limit || 20);
332
+ try {
333
+ // v2.0 DB-backed fast path
334
+ if (ctx.centralDb?.isAvailable() && ctx.centralDb?.isMigrated()) {
335
+ const lim = limit || 20;
336
+ let results = ctx.centralDb.discoverFts(query, lim);
337
+ if (ctx.clientRead?.pendingOverlay.length) {
338
+ results = mergeOverlayDiscoverResults(results, ctx.clientRead.pendingOverlay, query, lim, (p) => ({
339
+ id: p.id,
340
+ title: p.title,
341
+ relevance: "",
342
+ rank: 0,
343
+ project_id: p.project_id,
344
+ }));
345
+ }
346
+ if (results.length === 0) {
347
+ return {
348
+ content: [{ type: "text", text: `No memories found for "${query}". Try different keywords.` }],
349
+ };
350
+ }
351
+ const formatted = results
352
+ .map((r) => `**${r.title}**\n ID: ${r.id}${r.relevance ? `\n Relevance: ${r.relevance}` : ""}`)
353
+ .join("\n\n");
354
+ return {
355
+ content: [{ type: "text", text: `Found ${results.length} relevant memories for "${query}":\n\n${formatted}\n\nUse gnosys_read to load any of these.` }],
356
+ };
357
+ }
358
+ // v1.x legacy path
359
+ if (!ctx.search) {
360
+ return {
361
+ content: [{ type: "text", text: "Search index not initialized." }],
362
+ isError: true,
363
+ };
364
+ }
365
+ const results = ctx.search.discover(query, limit || 20);
250
366
  if (results.length === 0) {
251
367
  return {
252
- content: [{ type: "text", text: `No memories found for "${query}". Try different keywords.` }],
368
+ content: [
369
+ {
370
+ type: "text",
371
+ text: `No memories found for "${query}". Try different keywords or use gnosys_search for full-text search.`,
372
+ },
373
+ ],
253
374
  };
254
375
  }
255
376
  const formatted = results
256
- .map((r) => `**${r.title}**\n ID: ${r.id}${r.relevance ? `\n Relevance: ${r.relevance}` : ""}`)
377
+ .map((r) => `**${r.title}**\n Path: ${r.relative_path}${r.relevance ? `\n Relevance: ${r.relevance}` : ""}`)
257
378
  .join("\n\n");
258
- return {
259
- content: [{ type: "text", text: `Found ${results.length} relevant memories for "${query}":\n\n${formatted}\n\nUse gnosys_read to load any of these.` }],
260
- };
261
- }
262
- // v1.x legacy path
263
- if (!ctx.search) {
264
- return {
265
- content: [{ type: "text", text: "Search index not initialized." }],
266
- isError: true,
267
- };
268
- }
269
- const results = ctx.search.discover(query, limit || 20);
270
- if (results.length === 0) {
271
379
  return {
272
380
  content: [
273
381
  {
274
382
  type: "text",
275
- text: `No memories found for "${query}". Try different keywords or use gnosys_search for full-text search.`,
383
+ text: `Found ${results.length} relevant memories for "${query}":\n\n${formatted}\n\nUse gnosys_read to load any of these.`,
276
384
  },
277
385
  ],
278
386
  };
279
387
  }
280
- const formatted = results
281
- .map((r) => `**${r.title}**\n Path: ${r.relative_path}${r.relevance ? `\n Relevance: ${r.relevance}` : ""}`)
282
- .join("\n\n");
283
- return {
284
- content: [
285
- {
286
- type: "text",
287
- text: `Found ${results.length} relevant memories for "${query}":\n\n${formatted}\n\nUse gnosys_read to load any of these.`,
288
- },
289
- ],
290
- };
388
+ finally {
389
+ releaseClientReadFromContext(ctx);
390
+ }
291
391
  });
292
392
  // ─── Tool: gnosys_read ───────────────────────────────────────────────────
293
393
  regTool("gnosys_read", "Read a specific memory. Accepts a memory ID (e.g., 'arch-012') or layer-prefixed path (e.g., 'project:decisions/why-not-rag.md'). Without a prefix, searches all stores in precedence order.", {
@@ -295,62 +395,72 @@ regTool("gnosys_read", "Read a specific memory. Accepts a memory ID (e.g., 'arch
295
395
  projectRoot: projectRootParam,
296
396
  }, async ({ path: memPath, projectRoot }) => {
297
397
  const ctx = await resolveToolContext(projectRoot);
298
- // v2.0 DB-backed fast path: try reading by memory ID from gnosys.db first
299
- if (ctx.centralDb?.isAvailable() && ctx.centralDb?.isMigrated()) {
300
- const dbMem = ctx.centralDb.getMemory(memPath);
301
- if (dbMem) {
302
- const tags = dbMem.tags || "[]";
303
- const headerLines = [
304
- `---`,
305
- `id: ${dbMem.id}`,
306
- `title: '${dbMem.title}'`,
307
- `category: ${dbMem.category}`,
308
- `tags: ${tags}`,
309
- `relevance: ${dbMem.relevance}`,
310
- `author: ${dbMem.author}`,
311
- `authority: ${dbMem.authority}`,
312
- `confidence: ${dbMem.confidence}`,
313
- `status: ${dbMem.status}`,
314
- `tier: ${dbMem.tier}`,
315
- `created: '${dbMem.created}'`,
316
- `modified: '${dbMem.modified}'`,
317
- ];
318
- if (dbMem.source_file) {
319
- headerLines.push(`source_file: ${dbMem.source_file}${dbMem.source_page != null ? ` (page ${Number(dbMem.source_page)})` : ""}`);
398
+ try {
399
+ // v2.0 DB-backed fast path: try reading by memory ID from gnosys.db first
400
+ if (ctx.centralDb?.isAvailable() && ctx.centralDb?.isMigrated()) {
401
+ let dbMem = ctx.centralDb.getMemory(memPath);
402
+ if (!dbMem && ctx.clientRead?.pendingOverlay.length) {
403
+ const pending = ctx.clientRead.pendingOverlay.find((p) => p.id === memPath);
404
+ if (pending)
405
+ dbMem = pendingAddToDbMemory(pending);
320
406
  }
321
- if (dbMem.source_path)
322
- headerLines.push(`source_path: ${dbMem.source_path}`);
323
- headerLines.push(`---`);
324
- const header = headerLines.join("\n");
407
+ if (dbMem) {
408
+ const tags = dbMem.tags || "[]";
409
+ const headerLines = [
410
+ `---`,
411
+ `id: ${dbMem.id}`,
412
+ `title: '${dbMem.title}'`,
413
+ `category: ${dbMem.category}`,
414
+ `tags: ${tags}`,
415
+ `relevance: ${dbMem.relevance}`,
416
+ `author: ${dbMem.author}`,
417
+ `authority: ${dbMem.authority}`,
418
+ `confidence: ${dbMem.confidence}`,
419
+ `status: ${dbMem.status}`,
420
+ `tier: ${dbMem.tier}`,
421
+ `created: '${dbMem.created}'`,
422
+ `modified: '${dbMem.modified}'`,
423
+ ];
424
+ if (dbMem.source_file) {
425
+ headerLines.push(`source_file: ${dbMem.source_file}${dbMem.source_page != null ? ` (page ${Number(dbMem.source_page)})` : ""}`);
426
+ }
427
+ if (dbMem.source_path)
428
+ headerLines.push(`source_path: ${dbMem.source_path}`);
429
+ headerLines.push(`---`);
430
+ const header = headerLines.join("\n");
431
+ return {
432
+ content: [{ type: "text", text: `[Source: gnosys.db]\n\n${header}\n\n${dbMem.content}` }],
433
+ };
434
+ }
435
+ // Not found in db — fall through to legacy path
436
+ }
437
+ // v1.x legacy path
438
+ const memory = await ctx.resolver.readMemory(memPath);
439
+ if (!memory) {
325
440
  return {
326
- content: [{ type: "text", text: `[Source: gnosys.db]\n\n${header}\n\n${dbMem.content}` }],
441
+ content: [{ type: "text", text: `Memory not found: ${memPath}` }],
442
+ isError: true,
327
443
  };
328
444
  }
329
- // Not found in db — fall through to legacy path
330
- }
331
- // v1.x legacy path
332
- const memory = await ctx.resolver.readMemory(memPath);
333
- if (!memory) {
445
+ let raw;
446
+ try {
447
+ raw = await fs.readFile(memory.filePath, "utf-8");
448
+ }
449
+ catch (err) {
450
+ return { content: [{ type: "text", text: formatMcpError("reading memory", err) }], isError: true };
451
+ }
334
452
  return {
335
- content: [{ type: "text", text: `Memory not found: ${memPath}` }],
336
- isError: true,
453
+ content: [
454
+ {
455
+ type: "text",
456
+ text: `[Source: ${memory.sourceLabel}]\n\n${raw}`,
457
+ },
458
+ ],
337
459
  };
338
460
  }
339
- let raw;
340
- try {
341
- raw = await fs.readFile(memory.filePath, "utf-8");
342
- }
343
- catch (err) {
344
- return { content: [{ type: "text", text: formatMcpError("reading memory", err) }], isError: true };
461
+ finally {
462
+ releaseClientReadFromContext(ctx);
345
463
  }
346
- return {
347
- content: [
348
- {
349
- type: "text",
350
- text: `[Source: ${memory.sourceLabel}]\n\n${raw}`,
351
- },
352
- ],
353
- };
354
464
  });
355
465
  // ─── Tool: gnosys_search ─────────────────────────────────────────────────
356
466
  regTool("gnosys_search", "Search memories by keyword across all stores. Returns matching file paths with relevance snippets.", {
@@ -359,50 +469,65 @@ regTool("gnosys_search", "Search memories by keyword across all stores. Returns
359
469
  projectRoot: projectRootParam,
360
470
  }, async ({ query, limit, projectRoot }) => {
361
471
  const ctx = await resolveToolContext(projectRoot);
362
- // v2.0 DB-backed fast path
363
- if (ctx.centralDb?.isAvailable() && ctx.centralDb?.isMigrated()) {
364
- const results = ctx.centralDb.searchFts(query, limit || 20);
472
+ try {
473
+ // v2.0 DB-backed fast path
474
+ if (ctx.centralDb?.isAvailable() && ctx.centralDb?.isMigrated()) {
475
+ const lim = limit || 20;
476
+ let results = ctx.centralDb.searchFts(query, lim);
477
+ if (ctx.clientRead?.pendingOverlay.length) {
478
+ results = mergeOverlaySearchResults(results, ctx.clientRead.pendingOverlay, query, lim, (p) => ({
479
+ id: p.id,
480
+ title: p.title,
481
+ snippet: p.content.substring(0, 200),
482
+ rank: 0,
483
+ project_id: p.project_id,
484
+ }));
485
+ }
486
+ if (results.length === 0) {
487
+ return {
488
+ content: [{ type: "text", text: `No results for "${query}". Try different keywords.` }],
489
+ };
490
+ }
491
+ const formatted = results
492
+ .map((r) => `**${r.title}** (${r.id})\n${r.snippet.replace(/>>>/g, "**").replace(/<<</g, "**")}`)
493
+ .join("\n\n");
494
+ return {
495
+ content: [{ type: "text", text: `Found ${results.length} results for "${query}":\n\n${formatted}` }],
496
+ };
497
+ }
498
+ // v1.x legacy path
499
+ if (!ctx.search) {
500
+ return {
501
+ content: [{ type: "text", text: "Search index not initialized." }],
502
+ isError: true,
503
+ };
504
+ }
505
+ const results = ctx.search.search(query, limit || 20);
365
506
  if (results.length === 0) {
366
507
  return {
367
- content: [{ type: "text", text: `No results for "${query}". Try different keywords.` }],
508
+ content: [
509
+ {
510
+ type: "text",
511
+ text: `No results for "${query}". Try different keywords or use gnosys_discover.`,
512
+ },
513
+ ],
368
514
  };
369
515
  }
370
516
  const formatted = results
371
- .map((r) => `**${r.title}** (${r.id})\n${r.snippet.replace(/>>>/g, "**").replace(/<<</g, "**")}`)
517
+ .map((r) => `**${r.title}** (${r.relative_path})\n${r.snippet.replace(/>>>/g, "**").replace(/<<</g, "**")}`)
372
518
  .join("\n\n");
373
- return {
374
- content: [{ type: "text", text: `Found ${results.length} results for "${query}":\n\n${formatted}` }],
375
- };
376
- }
377
- // v1.x legacy path
378
- if (!ctx.search) {
379
- return {
380
- content: [{ type: "text", text: "Search index not initialized." }],
381
- isError: true,
382
- };
383
- }
384
- const results = ctx.search.search(query, limit || 20);
385
- if (results.length === 0) {
386
519
  return {
387
520
  content: [
388
521
  {
389
522
  type: "text",
390
- text: `No results for "${query}". Try different keywords or use gnosys_discover.`,
523
+ text: `Found ${results.length} results for "${query}":\n\n${formatted}`,
391
524
  },
392
525
  ],
393
526
  };
394
527
  }
395
- const formatted = results
396
- .map((r) => `**${r.title}** (${r.relative_path})\n${r.snippet.replace(/>>>/g, "**").replace(/<<</g, "**")}`)
397
- .join("\n\n");
398
- return {
399
- content: [
400
- {
401
- type: "text",
402
- text: `Found ${results.length} results for "${query}":\n\n${formatted}`,
403
- },
404
- ],
405
- };
528
+ finally {
529
+ releaseClientReadFromContext(ctx);
530
+ }
406
531
  });
407
532
  // ─── Tool: gnosys_list ───────────────────────────────────────────────────
408
533
  regTool("gnosys_list", "List memories across all stores, optionally filtered by category, tag, or store layer.", {
@@ -413,41 +538,76 @@ regTool("gnosys_list", "List memories across all stores, optionally filtered by
413
538
  projectRoot: projectRootParam,
414
539
  }, async ({ category, tag, store: storeFilter, status, projectRoot }) => {
415
540
  const ctx = await resolveToolContext(projectRoot);
416
- // DB-first: read from central DB instead of scanning markdown files
417
- const db = ctx.centralDb;
418
- if (db?.isAvailable()) {
419
- let dbMemories = status === "active" || !status
420
- ? db.getActiveMemories()
421
- : db.getAllMemories();
422
- // Apply filters on DB results
423
- if (status && status !== "active") {
424
- dbMemories = dbMemories.filter((m) => m.status === status);
541
+ try {
542
+ // DB-first: read from central DB instead of scanning markdown files
543
+ const db = ctx.centralDb;
544
+ if (db?.isAvailable()) {
545
+ let dbMemories = status === "active" || !status
546
+ ? db.getActiveMemories()
547
+ : db.getAllMemories();
548
+ if (ctx.clientRead?.pendingOverlay.length && (status === "active" || !status)) {
549
+ dbMemories = applyPendingOverlay(dbMemories, ctx.clientRead.pendingOverlay, new Set()).memories;
550
+ }
551
+ // Apply filters on DB results
552
+ if (status && status !== "active") {
553
+ dbMemories = dbMemories.filter((m) => m.status === status);
554
+ }
555
+ if (storeFilter) {
556
+ dbMemories = dbMemories.filter((m) => m.scope === storeFilter);
557
+ }
558
+ if (category) {
559
+ dbMemories = dbMemories.filter((m) => m.category === category);
560
+ }
561
+ if (tag) {
562
+ dbMemories = dbMemories.filter((m) => {
563
+ try {
564
+ const parsed = JSON.parse(m.tags || "[]");
565
+ const tagList = Array.isArray(parsed)
566
+ ? parsed
567
+ : Object.values(parsed).flat();
568
+ return tagList.includes(tag);
569
+ }
570
+ catch {
571
+ return false;
572
+ }
573
+ });
574
+ }
575
+ // Filter by project if we have a project ID (so scoped queries only see their project)
576
+ if (ctx.projectId && !storeFilter) {
577
+ dbMemories = dbMemories.filter((m) => m.project_id === ctx.projectId || m.scope !== "project");
578
+ }
579
+ const lines = dbMemories.map((m) => `- [${m.scope}] **${m.title}** (${m.category}/${m.id}) [${m.status}]`);
580
+ return {
581
+ content: [
582
+ {
583
+ type: "text",
584
+ text: lines.length > 0
585
+ ? `${lines.length} memories:\n\n${lines.join("\n")}`
586
+ : "No memories match the filter.",
587
+ },
588
+ ],
589
+ };
425
590
  }
591
+ // Fallback: read from markdown files if central DB unavailable
592
+ let memories = await ctx.resolver.getAllMemories();
426
593
  if (storeFilter) {
427
- dbMemories = dbMemories.filter((m) => m.scope === storeFilter);
594
+ memories = memories.filter((m) => m.sourceLayer === storeFilter || m.sourceLabel === storeFilter);
428
595
  }
429
596
  if (category) {
430
- dbMemories = dbMemories.filter((m) => m.category === category);
597
+ memories = memories.filter((m) => m.frontmatter.category === category);
431
598
  }
432
599
  if (tag) {
433
- dbMemories = dbMemories.filter((m) => {
434
- try {
435
- const parsed = JSON.parse(m.tags || "[]");
436
- const tagList = Array.isArray(parsed)
437
- ? parsed
438
- : Object.values(parsed).flat();
439
- return tagList.includes(tag);
440
- }
441
- catch {
442
- return false;
443
- }
600
+ memories = memories.filter((m) => {
601
+ const tags = Array.isArray(m.frontmatter.tags)
602
+ ? m.frontmatter.tags
603
+ : Object.values(m.frontmatter.tags).flat();
604
+ return tags.includes(tag);
444
605
  });
445
606
  }
446
- // Filter by project if we have a project ID (so scoped queries only see their project)
447
- if (ctx.projectId && !storeFilter) {
448
- dbMemories = dbMemories.filter((m) => m.project_id === ctx.projectId || m.scope !== "project");
607
+ if (status) {
608
+ memories = memories.filter((m) => m.frontmatter.status === status);
449
609
  }
450
- const lines = dbMemories.map((m) => `- [${m.scope}] **${m.title}** (${m.category}/${m.id}) [${m.status}]`);
610
+ const lines = memories.map((m) => `- [${m.sourceLabel}] **${m.frontmatter.title}** (${m.relativePath}) [${m.frontmatter.status}]`);
451
611
  return {
452
612
  content: [
453
613
  {
@@ -459,36 +619,9 @@ regTool("gnosys_list", "List memories across all stores, optionally filtered by
459
619
  ],
460
620
  };
461
621
  }
462
- // Fallback: read from markdown files if central DB unavailable
463
- let memories = await ctx.resolver.getAllMemories();
464
- if (storeFilter) {
465
- memories = memories.filter((m) => m.sourceLayer === storeFilter || m.sourceLabel === storeFilter);
466
- }
467
- if (category) {
468
- memories = memories.filter((m) => m.frontmatter.category === category);
469
- }
470
- if (tag) {
471
- memories = memories.filter((m) => {
472
- const tags = Array.isArray(m.frontmatter.tags)
473
- ? m.frontmatter.tags
474
- : Object.values(m.frontmatter.tags).flat();
475
- return tags.includes(tag);
476
- });
477
- }
478
- if (status) {
479
- memories = memories.filter((m) => m.frontmatter.status === status);
622
+ finally {
623
+ releaseClientReadFromContext(ctx);
480
624
  }
481
- const lines = memories.map((m) => `- [${m.sourceLabel}] **${m.frontmatter.title}** (${m.relativePath}) [${m.frontmatter.status}]`);
482
- return {
483
- content: [
484
- {
485
- type: "text",
486
- text: lines.length > 0
487
- ? `${lines.length} memories:\n\n${lines.join("\n")}`
488
- : "No memories match the filter.",
489
- },
490
- ],
491
- };
492
625
  });
493
626
  // ─── Tool: gnosys_add ────────────────────────────────────────────────────
494
627
  regTool("gnosys_add", "Add a new memory. Accepts raw text — an LLM structures it into an atomic memory. Writes to the project store by default. Use store='personal' for cross-project knowledge, or store='global' to explicitly write to shared org knowledge.", {
@@ -1560,7 +1693,7 @@ regTool("gnosys_import", "Bulk import structured data (CSV, JSON, JSONL) into Gn
1560
1693
  const effectiveMode = mode || "structured";
1561
1694
  try {
1562
1695
  // v5.9.1 (#100): import.js pulls mammoth + pdf-parse + turndown.
1563
- const { performImport, formatImportSummary, estimateDuration } = await import("./lib/import.js");
1696
+ const { performImport, formatImportSummary } = await import("./lib/import.js");
1564
1697
  const result = await performImport(writeTarget.store, ingestion, {
1565
1698
  format: format,
1566
1699
  data,
@@ -1585,7 +1718,6 @@ regTool("gnosys_import", "Bulk import structured data (CSV, JSON, JSONL) into Gn
1585
1718
  // Smart threshold guidance
1586
1719
  if (effectiveMode === "llm" &&
1587
1720
  result.totalProcessed > 100) {
1588
- const estimate = estimateDuration(result.totalProcessed, "llm", concurrency || 5);
1589
1721
  response += `\n\n💡 Tip: For large LLM imports, the CLI offers progress tracking and resume:\n gnosys import ${data.length < 100 ? data : "<file>"} --format ${format} --mode llm --skip-existing`;
1590
1722
  }
1591
1723
  return { content: [{ type: "text", text: response }] };
@@ -1895,7 +2027,7 @@ regTool("gnosys_dream", "Run a Dream Mode cycle — idle-time consolidation that
1895
2027
  }, async (params) => {
1896
2028
  try {
1897
2029
  const ctx = await resolveToolContext(params.projectRoot);
1898
- if (!ctx.centralDb || !ctx.centralDb.isAvailable() || !ctx.centralDb.isMigrated()) {
2030
+ if (!ctx.centralDb?.isAvailable() || !ctx.centralDb.isMigrated()) {
1899
2031
  return {
1900
2032
  content: [
1901
2033
  {
@@ -1905,8 +2037,6 @@ regTool("gnosys_dream", "Run a Dream Mode cycle — idle-time consolidation that
1905
2037
  ],
1906
2038
  };
1907
2039
  }
1908
- // Record activity to reset idle timer (if scheduler is running)
1909
- dreamScheduler?.recordActivity();
1910
2040
  const dreamConfig = {
1911
2041
  enabled: true,
1912
2042
  idleMinutes: 0, // Run immediately (manual trigger)
@@ -1924,6 +2054,34 @@ regTool("gnosys_dream", "Run a Dream Mode cycle — idle-time consolidation that
1924
2054
  const report = await engine.dream((phase, detail) => {
1925
2055
  console.error(`[dream:${phase}] ${detail}`);
1926
2056
  });
2057
+ const { appendDreamRun } = await import("./lib/dreamRunLog.js");
2058
+ appendDreamRun({
2059
+ ...report,
2060
+ id: report.id || `dream-${Date.now()}`,
2061
+ trigger: report.trigger || "manual",
2062
+ status: report.aborted ? "aborted" : report.errors.length > 0 ? "failed" : "completed",
2063
+ machine: report.machine || { hostname: "unknown" },
2064
+ provider: report.provider || dreamConfig.provider,
2065
+ phases: report.phases || [],
2066
+ llmCalls: report.llmCalls || [],
2067
+ totals: report.totals || {
2068
+ llmCallsMade: 0,
2069
+ llmCallsSkipped: 0,
2070
+ estimatedInputTokens: 0,
2071
+ estimatedOutputTokens: 0,
2072
+ estimatedCostUsd: 0,
2073
+ },
2074
+ effectiveness: report.effectiveness || {
2075
+ usefulOutputScore: 0,
2076
+ costPerUsefulOutput: null,
2077
+ decaysApplied: report.decayUpdated,
2078
+ summariesGenerated: report.summariesGenerated,
2079
+ summariesUpdated: report.summariesUpdated,
2080
+ reviewSuggestions: report.reviewSuggestions.length,
2081
+ relationshipsDiscovered: report.relationshipsDiscovered,
2082
+ },
2083
+ gates: [],
2084
+ });
1927
2085
  return {
1928
2086
  content: [
1929
2087
  {
@@ -1949,7 +2107,7 @@ regTool("gnosys_export", "Export gnosys.db to Obsidian-compatible vault — atom
1949
2107
  }, async (params) => {
1950
2108
  try {
1951
2109
  const ctx = await resolveToolContext(params.projectRoot);
1952
- if (!ctx.centralDb || !ctx.centralDb.isAvailable() || !ctx.centralDb.isMigrated()) {
2110
+ if (!ctx.centralDb?.isAvailable() || !ctx.centralDb.isMigrated()) {
1953
2111
  return {
1954
2112
  content: [
1955
2113
  {
@@ -2072,8 +2230,6 @@ regResource("gnosys_recall", "gnosys://recall", {
2072
2230
  priority: 1, // Highest priority — always inject
2073
2231
  },
2074
2232
  }, async () => {
2075
- // Record activity for dream scheduler (this fires on every turn)
2076
- dreamScheduler?.recordActivity();
2077
2233
  if (!search) {
2078
2234
  return {
2079
2235
  contents: [
@@ -2086,23 +2242,31 @@ regResource("gnosys_recall", "gnosys://recall", {
2086
2242
  };
2087
2243
  }
2088
2244
  const storePath = resolver.getWriteTarget()?.store.getStorePath() || "";
2089
- const result = await recall("*", {
2090
- limit: config.recall?.maxMemories || 8,
2091
- search,
2092
- resolver,
2093
- storePath,
2094
- recallConfig: config.recall,
2095
- gnosysDb: gnosysDb || undefined,
2096
- });
2097
- return {
2098
- contents: [
2099
- {
2100
- uri: "gnosys://recall",
2101
- mimeType: "text/markdown",
2102
- text: formatRecall(result),
2103
- },
2104
- ],
2105
- };
2245
+ const applied = applyClientReadToCentralDb(centralDb);
2246
+ try {
2247
+ const result = await recall("*", {
2248
+ limit: config.recall?.maxMemories || 8,
2249
+ search,
2250
+ resolver,
2251
+ storePath,
2252
+ recallConfig: config.recall,
2253
+ gnosysDb: applied.centralDb || undefined,
2254
+ pendingOverlay: applied.clientRead?.pendingOverlay,
2255
+ });
2256
+ return {
2257
+ contents: [
2258
+ {
2259
+ uri: "gnosys://recall",
2260
+ mimeType: "text/markdown",
2261
+ text: formatRecall(result),
2262
+ },
2263
+ ],
2264
+ };
2265
+ }
2266
+ finally {
2267
+ if (applied.clientRead)
2268
+ closeClientReadContext(applied.clientRead);
2269
+ }
2106
2270
  });
2107
2271
  // ─── Tool: gnosys_recall (query-specific fallback) ──────────────────────
2108
2272
  // For hosts that don't support MCP Resources, or when the agent wants to
@@ -2118,28 +2282,34 @@ regTool("gnosys_recall", "Fast memory recall — inject relevant memories as con
2118
2282
  }, async ({ query, limit, traceId, aggressive, projectRoot }) => {
2119
2283
  try {
2120
2284
  const ctx = await resolveToolContext(projectRoot);
2121
- if (!ctx.search) {
2285
+ try {
2286
+ if (!ctx.search) {
2287
+ return {
2288
+ content: [{ type: "text", text: "<gnosys: no-strong-recall-needed>" }],
2289
+ };
2290
+ }
2291
+ const storePath = ctx.resolver.getWriteTarget()?.store.getStorePath() || "";
2292
+ const recallConfig = {
2293
+ ...ctx.config.recall,
2294
+ ...(aggressive !== undefined ? { aggressive } : {}),
2295
+ };
2296
+ const result = await recall(query, {
2297
+ limit: Math.min(limit || recallConfig.maxMemories, 15),
2298
+ search: ctx.search,
2299
+ resolver: ctx.resolver,
2300
+ storePath,
2301
+ traceId,
2302
+ recallConfig,
2303
+ gnosysDb: ctx.centralDb || undefined,
2304
+ pendingOverlay: ctx.clientRead?.pendingOverlay,
2305
+ });
2122
2306
  return {
2123
- content: [{ type: "text", text: "<gnosys: no-strong-recall-needed>" }],
2307
+ content: [{ type: "text", text: formatRecall(result) }],
2124
2308
  };
2125
2309
  }
2126
- const storePath = ctx.resolver.getWriteTarget()?.store.getStorePath() || "";
2127
- const recallConfig = {
2128
- ...ctx.config.recall,
2129
- ...(aggressive !== undefined ? { aggressive } : {}),
2130
- };
2131
- const result = await recall(query, {
2132
- limit: Math.min(limit || recallConfig.maxMemories, 15),
2133
- search: ctx.search,
2134
- resolver: ctx.resolver,
2135
- storePath,
2136
- traceId,
2137
- recallConfig,
2138
- gnosysDb: ctx.centralDb || undefined,
2139
- });
2140
- return {
2141
- content: [{ type: "text", text: formatRecall(result) }],
2142
- };
2310
+ finally {
2311
+ releaseClientReadFromContext(ctx);
2312
+ }
2143
2313
  }
2144
2314
  catch (err) {
2145
2315
  return { content: [{ type: "text", text: formatMcpError("recalling memories", err) }], isError: true };
@@ -2383,28 +2553,34 @@ regTool("gnosys_federated_search", "Search across all scopes (project → user
2383
2553
  projectRoot: z.string().optional().describe("Project root directory for context detection"),
2384
2554
  includeGlobal: z.boolean().optional().describe("Include global-scope memories (default: true)"),
2385
2555
  }, async ({ query, limit, projectRoot, includeGlobal }) => {
2386
- if (!centralDb?.isAvailable()) {
2387
- return { content: [{ type: "text", text: "Central DB not available. Run gnosys_init first." }], isError: true };
2556
+ const ctx = await resolveToolContext(projectRoot);
2557
+ try {
2558
+ if (!ctx.centralDb?.isAvailable()) {
2559
+ return { content: [{ type: "text", text: "Central DB not available. Run gnosys_init first." }], isError: true };
2560
+ }
2561
+ // Auto-detect current project
2562
+ const projectId = await detectCurrentProject(ctx.centralDb, projectRoot || undefined);
2563
+ const results = federatedSearch(ctx.centralDb, query, {
2564
+ limit: limit || 20,
2565
+ projectId,
2566
+ includeGlobal: includeGlobal !== false,
2567
+ });
2568
+ if (results.length === 0) {
2569
+ return { content: [{ type: "text", text: `No results for "${query}" across any scope.` }] };
2570
+ }
2571
+ const lines = results.map((r, i) => {
2572
+ const projectLabel = r.projectName ? ` [${r.projectName}]` : "";
2573
+ const boostLabel = r.boosts.length > 0 ? ` (${r.boosts.join(", ")})` : "";
2574
+ return `${i + 1}. **${r.title}** (${r.category})${projectLabel}\n scope: ${r.scope} | score: ${r.score.toFixed(4)}${boostLabel}\n ${r.snippet}`;
2575
+ });
2576
+ const contextNote = projectId ? `Context: project ${projectId}` : "Context: no project detected";
2577
+ return {
2578
+ content: [{ type: "text", text: `${contextNote}\n\n${lines.join("\n\n")}` }],
2579
+ };
2388
2580
  }
2389
- // Auto-detect current project
2390
- const projectId = await detectCurrentProject(centralDb, projectRoot || undefined);
2391
- const results = federatedSearch(centralDb, query, {
2392
- limit: limit || 20,
2393
- projectId,
2394
- includeGlobal: includeGlobal !== false,
2395
- });
2396
- if (results.length === 0) {
2397
- return { content: [{ type: "text", text: `No results for "${query}" across any scope.` }] };
2581
+ finally {
2582
+ releaseClientReadFromContext(ctx);
2398
2583
  }
2399
- const lines = results.map((r, i) => {
2400
- const projectLabel = r.projectName ? ` [${r.projectName}]` : "";
2401
- const boostLabel = r.boosts.length > 0 ? ` (${r.boosts.join(", ")})` : "";
2402
- return `${i + 1}. **${r.title}** (${r.category})${projectLabel}\n scope: ${r.scope} | score: ${r.score.toFixed(4)}${boostLabel}\n ${r.snippet}`;
2403
- });
2404
- const contextNote = projectId ? `Context: project ${projectId}` : "Context: no project detected";
2405
- return {
2406
- content: [{ type: "text", text: `${contextNote}\n\n${lines.join("\n\n")}` }],
2407
- };
2408
2584
  });
2409
2585
  // ─── Tool: gnosys_detect_ambiguity ──────────────────────────────────────
2410
2586
  regTool("gnosys_detect_ambiguity", "Check if a query matches memories in multiple projects. Use before write operations to confirm the target project when ambiguity exists.", {
@@ -2500,7 +2676,8 @@ regTool("gnosys_remote_status", "Check the status of remote sync (multi-machine)
2500
2676
  if (!localDb.isAvailable()) {
2501
2677
  return { content: [{ type: "text", text: "Local DB not available." }], isError: true };
2502
2678
  }
2503
- const remotePath = localDb.getMeta("remote_path");
2679
+ const { getConfiguredRemotePath } = await import("./lib/remote.js");
2680
+ const remotePath = getConfiguredRemotePath(localDb);
2504
2681
  if (!remotePath) {
2505
2682
  return {
2506
2683
  content: [{
@@ -2509,6 +2686,14 @@ regTool("gnosys_remote_status", "Check the status of remote sync (multi-machine)
2509
2686
  }],
2510
2687
  };
2511
2688
  }
2689
+ const { readMachineConfig } = await import("./lib/machineConfig.js");
2690
+ if (readMachineConfig()?.remote.role) {
2691
+ const { getV13SyncStatus } = await import("./lib/syncClient.js");
2692
+ const v13 = getV13SyncStatus(localDb);
2693
+ return {
2694
+ content: [{ type: "text", text: JSON.stringify(v13, null, 2) }],
2695
+ };
2696
+ }
2512
2697
  const { RemoteSync } = await import("./lib/remote.js");
2513
2698
  const sync = new RemoteSync(localDb, remotePath);
2514
2699
  const status = await sync.getStatus();
@@ -2595,6 +2780,106 @@ regTool("gnosys_remote_resolve", "Resolve a sync conflict by choosing which vers
2595
2780
  localDb.close();
2596
2781
  }
2597
2782
  });
2783
+ // ─── Tool: gnosys_attach ────────────────────────────────────────────────
2784
+ regTool("gnosys_attach", "Attach a small binary file (logo, diagram, screenshot, small PDF) directly to a memory. The bytes are stored inline in the memory row, so the attachment travels machine-to-machine over the normal sync and works with a remote/dockerized server (no shared filesystem). Limit ~10MB — use gnosys_ingest_file for large media.", {
2785
+ memoryId: z.string().describe("Memory ID to attach the file to (e.g., 'deci-052')"),
2786
+ filePath: z.string().describe("Absolute path to the file to attach"),
2787
+ projectRoot: projectRootParam,
2788
+ }, async ({ memoryId, filePath, projectRoot }) => {
2789
+ const ctx = await resolveToolContext(projectRoot);
2790
+ if (!ctx.centralDb?.isAvailable()) {
2791
+ return {
2792
+ content: [{ type: "text", text: "Database not available. Cannot attach file." }],
2793
+ isError: true,
2794
+ };
2795
+ }
2796
+ try {
2797
+ const { attachFileToMemory } = await import("./lib/attachments.js");
2798
+ const result = await attachFileToMemory(ctx.centralDb, memoryId, filePath);
2799
+ auditToDb(ctx.centralDb, "write", memoryId, {
2800
+ tool: "gnosys_attach",
2801
+ name: result.name,
2802
+ mime: result.mime,
2803
+ sizeBytes: result.sizeBytes,
2804
+ unchanged: result.unchanged,
2805
+ });
2806
+ const sizeKb = (result.sizeBytes / 1024).toFixed(1);
2807
+ const verb = result.unchanged ? "already attached (no change)" : "attached";
2808
+ return {
2809
+ content: [
2810
+ {
2811
+ type: "text",
2812
+ text: `File ${verb}: ${result.name} (${result.mime}, ${sizeKb} KB)\nMemory: ${memoryId}`,
2813
+ },
2814
+ ],
2815
+ };
2816
+ }
2817
+ catch (err) {
2818
+ return {
2819
+ content: [{ type: "text", text: formatMcpError("attaching file", err) }],
2820
+ isError: true,
2821
+ };
2822
+ }
2823
+ });
2824
+ // ─── Tool: gnosys_get_attachment ────────────────────────────────────────
2825
+ regTool("gnosys_get_attachment", "Retrieve the binary attachment stored on a memory. By default returns the bytes (base64, plus an inline image when the attachment is an image). Pass outputPath to write the file to disk instead.", {
2826
+ memoryId: z.string().describe("Memory ID that holds the attachment"),
2827
+ outputPath: z.string().optional().describe("If provided, write the attachment to this absolute path instead of returning bytes"),
2828
+ projectRoot: projectRootParam,
2829
+ }, async ({ memoryId, outputPath, projectRoot }) => {
2830
+ const ctx = await resolveToolContext(projectRoot);
2831
+ if (!ctx.centralDb?.isAvailable()) {
2832
+ return {
2833
+ content: [{ type: "text", text: "Database not available." }],
2834
+ isError: true,
2835
+ };
2836
+ }
2837
+ try {
2838
+ const { getMemoryAttachment } = await import("./lib/attachments.js");
2839
+ const att = getMemoryAttachment(ctx.centralDb, memoryId);
2840
+ if (!att) {
2841
+ return {
2842
+ content: [{ type: "text", text: `No attachment found on memory: ${memoryId}` }],
2843
+ isError: true,
2844
+ };
2845
+ }
2846
+ if (outputPath) {
2847
+ const { writeFile } = await import("fs/promises");
2848
+ await writeFile(outputPath, att.data);
2849
+ return {
2850
+ content: [
2851
+ {
2852
+ type: "text",
2853
+ text: `Wrote ${att.name} (${att.mime}, ${att.data.length} bytes) to ${outputPath}`,
2854
+ },
2855
+ ],
2856
+ };
2857
+ }
2858
+ const base64 = att.data.toString("base64");
2859
+ if (att.mime.startsWith("image/")) {
2860
+ return {
2861
+ content: [
2862
+ { type: "image", data: base64, mimeType: att.mime },
2863
+ { type: "text", text: `${att.name} (${att.mime}, ${att.data.length} bytes)` },
2864
+ ],
2865
+ };
2866
+ }
2867
+ return {
2868
+ content: [
2869
+ {
2870
+ type: "text",
2871
+ text: `${att.name} (${att.mime}, ${att.data.length} bytes)\n\nbase64:\n${base64}`,
2872
+ },
2873
+ ],
2874
+ };
2875
+ }
2876
+ catch (err) {
2877
+ return {
2878
+ content: [{ type: "text", text: formatMcpError("reading attachment", err) }],
2879
+ isError: true,
2880
+ };
2881
+ }
2882
+ });
2598
2883
  // ─── Tool: gnosys_update_status ─────────────────────────────────────────
2599
2884
  regTool("gnosys_update_status", "Get the prompt/template for writing a dashboard-compatible status memory for this project. Returns instructions for creating a landscape memory with the correct heading format so the portfolio dashboard can parse it. Run this, then follow the instructions to analyze and write the status.", {
2600
2885
  projectRoot: z.string().optional().describe("Project root for auto-detection"),
@@ -2887,11 +3172,10 @@ async function initHeavyDeps() {
2887
3172
  const embCount = embeddings.hasEmbeddings() ? embeddings.count() : 0;
2888
3173
  console.error(`Hybrid search: ${embCount > 0 ? `ready (${embCount} embeddings)` : "available (run gnosys_reindex to build embeddings)"}`);
2889
3174
  console.error(`Ask engine: ${askEngine.isLLMAvailable ? `ready (${askEngine.providerName}/${askEngine.modelName})` : "disabled (configure LLM provider)"}`);
2890
- // Dream mode (only constructed if enabled; designation gate inside start()).
3175
+ // Dream mode scheduling is machine-level now (`gnosys dream run --scheduled`
3176
+ // via launchd). MCP may still run manual dream tool calls, but it no longer
3177
+ // owns an idle timer per connection.
2891
3178
  if (gnosysDb && config.dream?.enabled) {
2892
- const { GnosysDreamEngine, DreamScheduler } = await import("./lib/dream.js");
2893
- const dreamEngine = new GnosysDreamEngine(gnosysDb, config, config.dream);
2894
- dreamScheduler = new DreamScheduler(dreamEngine, config.dream);
2895
3179
  // Layer 3: probe the dream provider if this machine is the dream node.
2896
3180
  try {
2897
3181
  const designated = gnosysDb.getDreamMachineId();
@@ -2933,9 +3217,8 @@ async function initHeavyDeps() {
2933
3217
  }
2934
3218
  }
2935
3219
  catch {
2936
- // Probe failed — non-fatal. Continue with scheduler start.
3220
+ // Probe failed — non-fatal.
2937
3221
  }
2938
- dreamScheduler.start();
2939
3222
  const designated = gnosysDb.getDreamMachineId();
2940
3223
  const localId = gnosysDb.getMeta("machine_id");
2941
3224
  if (!designated) {
@@ -2945,7 +3228,7 @@ async function initHeavyDeps() {
2945
3228
  console.error(`Dream Mode: enabled — designated to '${designated}'. This machine (${localId || "?"}) will not dream.`);
2946
3229
  }
2947
3230
  else {
2948
- console.error(`Dream Mode: enabled on this machine (idle ${config.dream.idleMinutes}min, max ${config.dream.maxRuntimeMinutes}min)`);
3231
+ console.error(`Dream Mode: enabled on this machine (scheduled outside MCP via launchd, max ${config.dream.maxRuntimeMinutes}min)`);
2949
3232
  }
2950
3233
  }
2951
3234
  else {
@@ -2957,6 +3240,15 @@ async function initHeavyDeps() {
2957
3240
  // ─── Start the server ────────────────────────────────────────────────────
2958
3241
  /** Start the MCP server (stdio or http). Called by `gnosys serve` and when invoked as `gnosys-mcp`. */
2959
3242
  export async function startMcpServer() {
3243
+ // v5.12.1 reliability: an escaped async error must not kill the server.
3244
+ // Log to stderr (stdout is JSON-RPC in stdio mode) and keep serving —
3245
+ // all persistent state is transactional SQLite, so survivors are safe.
3246
+ process.on("unhandledRejection", (reason) => {
3247
+ console.error(`Gnosys MCP: unhandled rejection — ${reason instanceof Error ? reason.stack || reason.message : String(reason)}`);
3248
+ });
3249
+ process.on("uncaughtException", (err) => {
3250
+ console.error(`Gnosys MCP: uncaught exception — ${err.stack || err.message}`);
3251
+ });
2960
3252
  // v5.7.1 (#15): start the upgrade-marker watcher BEFORE anything else.
2961
3253
  // If `gnosys upgrade` was run on this machine while the MCP was idle,
2962
3254
  // pick that up immediately instead of serving stale tool handlers.
@@ -2985,6 +3277,12 @@ export async function startMcpServer() {
2985
3277
  console.error("Gnosys MCP server starting.");
2986
3278
  console.error("Active stores:");
2987
3279
  console.error(resolver.getSummary());
3280
+ // v13: background ingest sweep on master-role MCP startup (non-blocking).
3281
+ void import("./lib/syncIngestStartup.js")
3282
+ .then(({ maybeRunStartupIngestSweep }) => maybeRunStartupIngestSweep())
3283
+ .catch((err) => {
3284
+ console.error(`[sync] Startup ingest sweep failed: ${err instanceof Error ? err.message : err}`);
3285
+ });
2988
3286
  // Initialize search from the first writable store. Everything in this
2989
3287
  // block is FAST — opening the search index + tag registry + loading
2990
3288
  // gnosys.json. The slow stuff (LLM providers, transformers embeddings,