@pcircle/memesh 4.8.3 → 4.9.0

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 (254) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.codex-plugin/mcp.json +6 -4
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/AGENTS.md +68 -17
  6. package/README.de.md +83 -555
  7. package/README.md +83 -581
  8. package/README.zh-TW.md +84 -572
  9. package/dashboard/dist/index.html +10 -10
  10. package/dist/cli/view-live.d.ts.map +1 -1
  11. package/dist/cli/view-live.js +154 -384
  12. package/dist/cli/view-live.js.map +1 -1
  13. package/dist/core/agent-message-inbox.d.ts +2 -1
  14. package/dist/core/agent-message-inbox.d.ts.map +1 -1
  15. package/dist/core/agent-message-inbox.js +23 -4
  16. package/dist/core/agent-message-inbox.js.map +1 -1
  17. package/dist/core/agent-messaging.d.ts.map +1 -1
  18. package/dist/core/agent-messaging.js +23 -15
  19. package/dist/core/agent-messaging.js.map +1 -1
  20. package/dist/core/agent-router.d.ts +11 -10
  21. package/dist/core/agent-router.d.ts.map +1 -1
  22. package/dist/core/agent-router.js +29 -14
  23. package/dist/core/agent-router.js.map +1 -1
  24. package/dist/core/agent-scope-id.d.ts +11 -0
  25. package/dist/core/agent-scope-id.d.ts.map +1 -0
  26. package/dist/core/agent-scope-id.js +40 -0
  27. package/dist/core/agent-scope-id.js.map +1 -0
  28. package/dist/core/analytics.d.ts.map +1 -1
  29. package/dist/core/analytics.js.map +1 -1
  30. package/dist/core/briefing.d.ts.map +1 -1
  31. package/dist/core/briefing.js +8 -2
  32. package/dist/core/briefing.js.map +1 -1
  33. package/dist/core/config.d.ts +4 -40
  34. package/dist/core/config.d.ts.map +1 -1
  35. package/dist/core/config.js +75 -141
  36. package/dist/core/config.js.map +1 -1
  37. package/dist/core/demo.d.ts.map +1 -1
  38. package/dist/core/demo.js +6 -6
  39. package/dist/core/demo.js.map +1 -1
  40. package/dist/core/doctor.d.ts +2 -6
  41. package/dist/core/doctor.d.ts.map +1 -1
  42. package/dist/core/doctor.js +120 -154
  43. package/dist/core/doctor.js.map +1 -1
  44. package/dist/core/dreamer.d.ts +32 -47
  45. package/dist/core/dreamer.d.ts.map +1 -1
  46. package/dist/core/dreamer.js +214 -704
  47. package/dist/core/dreamer.js.map +1 -1
  48. package/dist/core/install-channel.d.ts.map +1 -1
  49. package/dist/core/install-channel.js +4 -47
  50. package/dist/core/install-channel.js.map +1 -1
  51. package/dist/core/install-id.d.ts.map +1 -1
  52. package/dist/core/install-id.js.map +1 -1
  53. package/dist/core/kg-backfill.d.ts.map +1 -1
  54. package/dist/core/kg-backfill.js.map +1 -1
  55. package/dist/core/lesson-engine.d.ts +0 -5
  56. package/dist/core/lesson-engine.d.ts.map +1 -1
  57. package/dist/core/lesson-engine.js +0 -25
  58. package/dist/core/lesson-engine.js.map +1 -1
  59. package/dist/core/lifecycle.d.ts.map +1 -1
  60. package/dist/core/lifecycle.js +58 -49
  61. package/dist/core/lifecycle.js.map +1 -1
  62. package/dist/core/memory-tool.d.ts.map +1 -1
  63. package/dist/core/memory-tool.js +20 -18
  64. package/dist/core/memory-tool.js.map +1 -1
  65. package/dist/core/operations.d.ts +3 -27
  66. package/dist/core/operations.d.ts.map +1 -1
  67. package/dist/core/operations.js +10 -245
  68. package/dist/core/operations.js.map +1 -1
  69. package/dist/core/paths.d.ts +4 -1
  70. package/dist/core/paths.d.ts.map +1 -1
  71. package/dist/core/paths.js +88 -14
  72. package/dist/core/paths.js.map +1 -1
  73. package/dist/core/product-improvements.js +2 -2
  74. package/dist/core/product-improvements.js.map +1 -1
  75. package/dist/core/project-tags.d.ts +2 -0
  76. package/dist/core/project-tags.d.ts.map +1 -1
  77. package/dist/core/project-tags.js +29 -1
  78. package/dist/core/project-tags.js.map +1 -1
  79. package/dist/core/schema-export.d.ts.map +1 -1
  80. package/dist/core/schema-export.js +13 -3
  81. package/dist/core/schema-export.js.map +1 -1
  82. package/dist/core/semver.d.ts +7 -0
  83. package/dist/core/semver.d.ts.map +1 -0
  84. package/dist/core/semver.js +49 -0
  85. package/dist/core/semver.js.map +1 -0
  86. package/dist/core/serializer.d.ts.map +1 -1
  87. package/dist/core/serializer.js +69 -57
  88. package/dist/core/serializer.js.map +1 -1
  89. package/dist/core/signal-scorer.d.ts.map +1 -1
  90. package/dist/core/signal-scorer.js.map +1 -1
  91. package/dist/core/transcript-extractor.d.ts +1 -85
  92. package/dist/core/transcript-extractor.d.ts.map +1 -1
  93. package/dist/core/transcript-extractor.js +5 -364
  94. package/dist/core/transcript-extractor.js.map +1 -1
  95. package/dist/core/transcript-source.d.ts +22 -6
  96. package/dist/core/transcript-source.d.ts.map +1 -1
  97. package/dist/core/transcript-source.js +108 -69
  98. package/dist/core/transcript-source.js.map +1 -1
  99. package/dist/core/types.d.ts +1 -17
  100. package/dist/core/types.d.ts.map +1 -1
  101. package/dist/core/version-check.d.ts +1 -0
  102. package/dist/core/version-check.d.ts.map +1 -1
  103. package/dist/core/version-check.js +46 -1
  104. package/dist/core/version-check.js.map +1 -1
  105. package/dist/db.d.ts +0 -34
  106. package/dist/db.d.ts.map +1 -1
  107. package/dist/db.js +6 -287
  108. package/dist/db.js.map +1 -1
  109. package/dist/host-runtime/acp.d.ts.map +1 -1
  110. package/dist/host-runtime/acp.js +4 -3
  111. package/dist/host-runtime/acp.js.map +1 -1
  112. package/dist/host-runtime/claude.d.ts.map +1 -1
  113. package/dist/host-runtime/claude.js +11 -11
  114. package/dist/host-runtime/claude.js.map +1 -1
  115. package/dist/host-runtime/codex-session.d.ts +9 -1
  116. package/dist/host-runtime/codex-session.d.ts.map +1 -1
  117. package/dist/host-runtime/codex-session.js +474 -29
  118. package/dist/host-runtime/codex-session.js.map +1 -1
  119. package/dist/host-runtime/codex.d.ts.map +1 -1
  120. package/dist/host-runtime/codex.js +4 -3
  121. package/dist/host-runtime/codex.js.map +1 -1
  122. package/dist/host-runtime/config.d.ts +1 -0
  123. package/dist/host-runtime/config.d.ts.map +1 -1
  124. package/dist/host-runtime/config.js +4 -0
  125. package/dist/host-runtime/config.js.map +1 -1
  126. package/dist/host-runtime/entry.d.ts +5 -0
  127. package/dist/host-runtime/entry.d.ts.map +1 -0
  128. package/dist/host-runtime/entry.js +11 -0
  129. package/dist/host-runtime/entry.js.map +1 -0
  130. package/dist/host-runtime/router-client.d.ts.map +1 -1
  131. package/dist/host-runtime/router-client.js +62 -15
  132. package/dist/host-runtime/router-client.js.map +1 -1
  133. package/dist/host-runtime/router.js +2 -2
  134. package/dist/host-runtime/router.js.map +1 -1
  135. package/dist/knowledge-graph.d.ts +0 -1
  136. package/dist/knowledge-graph.d.ts.map +1 -1
  137. package/dist/knowledge-graph.js +77 -60
  138. package/dist/knowledge-graph.js.map +1 -1
  139. package/dist/mcp/THIRD_PARTY_NOTICES.txt +217 -0
  140. package/dist/mcp/server.js +30685 -38
  141. package/dist/mcp/server.js.map +6 -1
  142. package/dist/skills-manifest.json +39 -34
  143. package/dist/storage/entity-index.d.ts +3 -0
  144. package/dist/storage/entity-index.d.ts.map +1 -0
  145. package/dist/storage/entity-index.js +8 -0
  146. package/dist/storage/entity-index.js.map +1 -0
  147. package/dist/storage/fts-index.d.ts.map +1 -1
  148. package/dist/storage/fts-index.js +11 -7
  149. package/dist/storage/fts-index.js.map +1 -1
  150. package/dist/storage/graph-repairs.d.ts +7 -2
  151. package/dist/storage/graph-repairs.d.ts.map +1 -1
  152. package/dist/storage/graph-repairs.js +89 -13
  153. package/dist/storage/graph-repairs.js.map +1 -1
  154. package/dist/storage/schema.d.ts +1 -1
  155. package/dist/storage/schema.d.ts.map +1 -1
  156. package/dist/storage/schema.js +1 -2
  157. package/dist/storage/schema.js.map +1 -1
  158. package/dist/storage/sqlite.d.ts +0 -1
  159. package/dist/storage/sqlite.d.ts.map +1 -1
  160. package/dist/storage/sqlite.js.map +1 -1
  161. package/dist/transports/agent-messaging.d.ts.map +1 -1
  162. package/dist/transports/agent-messaging.js +5 -7
  163. package/dist/transports/agent-messaging.js.map +1 -1
  164. package/dist/transports/cli/cli.d.ts.map +1 -1
  165. package/dist/transports/cli/cli.js +75 -686
  166. package/dist/transports/cli/cli.js.map +1 -1
  167. package/dist/transports/http/retired-routes.js +1 -1
  168. package/dist/transports/http/retired-routes.js.map +1 -1
  169. package/dist/transports/http/server.d.ts.map +1 -1
  170. package/dist/transports/http/server.js +13 -232
  171. package/dist/transports/http/server.js.map +1 -1
  172. package/dist/transports/mcp/handlers.d.ts +130 -4
  173. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  174. package/dist/transports/mcp/handlers.js +56 -7
  175. package/dist/transports/mcp/handlers.js.map +1 -1
  176. package/dist/transports/schemas.d.ts +71 -19
  177. package/dist/transports/schemas.d.ts.map +1 -1
  178. package/dist/transports/schemas.js +57 -11
  179. package/dist/transports/schemas.js.map +1 -1
  180. package/docs/platforms/README.md +6 -5
  181. package/docs/platforms/agent-messaging.md +241 -20
  182. package/hooks/hooks.json +23 -2
  183. package/llms-install.md +62 -30
  184. package/package.json +12 -9
  185. package/scripts/hooks/_generated/agent-message-inbox.js +23 -4
  186. package/scripts/hooks/_generated/core-paths.js +88 -14
  187. package/scripts/hooks/_generated/fts-index.js +11 -7
  188. package/scripts/hooks/_generated/schema.js +1 -2
  189. package/scripts/hooks/_shared.js +65 -23
  190. package/scripts/hooks/decision-nudge.js +152 -0
  191. package/scripts/hooks/post-commit.js +11 -0
  192. package/scripts/hooks/pre-compact.js +12 -4
  193. package/scripts/hooks/session-start.js +32 -9
  194. package/scripts/hooks/session-summary.js +19 -374
  195. package/scripts/upgrade-plugin.sh +71 -2
  196. package/skills/memesh/SKILL.md +24 -15
  197. package/skills/memesh-review/SKILL.md +7 -6
  198. package/dist/core/auto-tagger.d.ts +0 -10
  199. package/dist/core/auto-tagger.d.ts.map +0 -1
  200. package/dist/core/auto-tagger.js +0 -63
  201. package/dist/core/auto-tagger.js.map +0 -1
  202. package/dist/core/conflict-candidates.d.ts +0 -20
  203. package/dist/core/conflict-candidates.d.ts.map +0 -1
  204. package/dist/core/conflict-candidates.js +0 -71
  205. package/dist/core/conflict-candidates.js.map +0 -1
  206. package/dist/core/conflict-judge.d.ts +0 -58
  207. package/dist/core/conflict-judge.d.ts.map +0 -1
  208. package/dist/core/conflict-judge.js +0 -189
  209. package/dist/core/conflict-judge.js.map +0 -1
  210. package/dist/core/digest-validator.d.ts +0 -18
  211. package/dist/core/digest-validator.d.ts.map +0 -1
  212. package/dist/core/digest-validator.js +0 -85
  213. package/dist/core/digest-validator.js.map +0 -1
  214. package/dist/core/embedder.d.ts +0 -20
  215. package/dist/core/embedder.d.ts.map +0 -1
  216. package/dist/core/embedder.js +0 -242
  217. package/dist/core/embedder.js.map +0 -1
  218. package/dist/core/failure-analyzer.d.ts +0 -19
  219. package/dist/core/failure-analyzer.d.ts.map +0 -1
  220. package/dist/core/failure-analyzer.js +0 -83
  221. package/dist/core/failure-analyzer.js.map +0 -1
  222. package/dist/core/json-utils.d.ts +0 -3
  223. package/dist/core/json-utils.d.ts.map +0 -1
  224. package/dist/core/json-utils.js +0 -46
  225. package/dist/core/json-utils.js.map +0 -1
  226. package/dist/core/llm-client.d.ts +0 -22
  227. package/dist/core/llm-client.d.ts.map +0 -1
  228. package/dist/core/llm-client.js +0 -203
  229. package/dist/core/llm-client.js.map +0 -1
  230. package/dist/core/llm-telemetry.d.ts +0 -47
  231. package/dist/core/llm-telemetry.d.ts.map +0 -1
  232. package/dist/core/llm-telemetry.js +0 -117
  233. package/dist/core/llm-telemetry.js.map +0 -1
  234. package/dist/core/llm-validator.d.ts +0 -20
  235. package/dist/core/llm-validator.d.ts.map +0 -1
  236. package/dist/core/llm-validator.js +0 -231
  237. package/dist/core/llm-validator.js.map +0 -1
  238. package/dist/core/ollama-host.d.ts +0 -6
  239. package/dist/core/ollama-host.d.ts.map +0 -1
  240. package/dist/core/ollama-host.js +0 -30
  241. package/dist/core/ollama-host.js.map +0 -1
  242. package/dist/core/output-language.d.ts +0 -6
  243. package/dist/core/output-language.d.ts.map +0 -1
  244. package/dist/core/output-language.js +0 -25
  245. package/dist/core/output-language.js.map +0 -1
  246. package/dist/core/prompt-safety.d.ts +0 -4
  247. package/dist/core/prompt-safety.d.ts.map +0 -1
  248. package/dist/core/prompt-safety.js +0 -20
  249. package/dist/core/prompt-safety.js.map +0 -1
  250. package/dist/storage/vector-index.d.ts +0 -3
  251. package/dist/storage/vector-index.d.ts.map +0 -1
  252. package/dist/storage/vector-index.js +0 -13
  253. package/dist/storage/vector-index.js.map +0 -1
  254. /package/{.mcp.json → .claude-plugin/mcp.json} +0 -0
@@ -4,26 +4,9 @@
4
4
  // Extracts knowledge from completed Claude Code sessions
5
5
  // and stores as session-insight entities in MeMesh.
6
6
  //
7
- // THIS HOOK STAYS, and that was an open question rather than an assumption.
8
- // `dream run --from-transcripts` reads the same sessions from their raw JSONL
9
- // and does not depend on this hook having fired, so the obvious next step was
10
- // to retire the hook as redundant. Measured first, on a real graph
11
- // (2026-08-09, 214 active entities, ollama nomic-embed-text, L2 over the same
12
- // `name + observations` text the runtime embeds):
13
- //
14
- // every transcript-mined memory -> its nearest hook-captured entity
15
- // min 0.784 p25 0.821 p50 0.865 max 0.946
16
- // within 0.55: 0 of 47. within 0.70: 0 of 47.
17
- //
18
- // Nothing the transcript miner produced came within 0.78 of anything this hook
19
- // recorded. They are not two views of the same material: this hook records what
20
- // HAPPENED (files touched, commands run, commits), the miner extracts what was
21
- // DECIDED and what was LEARNED. Retiring either one loses a whole category.
22
-
23
7
  import { createRequire } from 'module';
24
8
  import { basename, join } from 'path';
25
- import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
26
- import { spawn } from 'child_process';
9
+ import { existsSync, readFileSync } from 'fs';
27
10
  import { pathToFileURL } from 'url';
28
11
  import {
29
12
  AUTO_CAPTURE_TAG,
@@ -32,7 +15,6 @@ import {
32
15
  extractCitedMemoryIds,
33
16
  getMemeshDirFromDbPath,
34
17
  getProjectName,
35
- importFromPluginRoot,
36
18
  isAutoCaptureEnabled,
37
19
  openHookDb,
38
20
  readUpdateCheckCache,
@@ -47,24 +29,17 @@ import {
47
29
 
48
30
  const require = createRequire(import.meta.url);
49
31
 
50
- // Pre-load dist/core/install-channel.js for auto-update channel detection.
51
- // Same pattern as session-start.js: async ESM import at process init,
52
- // falls back to null if dist is missing (source checkout pre-build).
53
- let _installChannelMod = null;
32
+ let installChannel = null;
54
33
  try {
55
- const _pluginRootForInit = resolvePluginRoot(import.meta.url);
56
- const _modPath = join(_pluginRootForInit, 'dist/core/install-channel.js');
57
- if (existsSync(_modPath)) {
58
- _installChannelMod = await import(pathToFileURL(_modPath).href);
34
+ const pluginRoot = resolvePluginRoot(import.meta.url);
35
+ const modulePath = join(pluginRoot, 'dist/core/install-channel.js');
36
+ if (existsSync(modulePath)) {
37
+ installChannel = await import(pathToFileURL(modulePath).href);
59
38
  }
60
- } catch { /* best-effort */ }
39
+ } catch {
40
+ // Best-effort: source checkouts may not have built dist output yet.
41
+ }
61
42
 
62
- /**
63
- * Run auto-update at Stop hook: reads cache, evaluates policy, and dispatches
64
- * the detached updater runner if warranted. Runs after all session work
65
- * completes, avoiding the TOCTOU race where install would overwrite dist/
66
- * mid-session.
67
- */
68
43
  async function runAutoUpdateAtStop() {
69
44
  try {
70
45
  const pluginRoot = resolvePluginRoot(import.meta.url);
@@ -75,11 +50,9 @@ async function runAutoUpdateAtStop() {
75
50
  const cache = readUpdateCheckCache(installedVersion);
76
51
  const policy = resolveAutoUpdatePolicy(process.env);
77
52
  const decision = decideAutoUpdateHook(installedVersion, cache, policy);
78
- if (decision.run) {
79
- await spawnAutoUpdate(decision.latest, _installChannelMod);
80
- }
53
+ if (decision.run) await spawnAutoUpdate(decision.latest, installChannel);
81
54
  } catch {
82
- // Best-effort never crash the hook.
55
+ // Best-effort: update failures must never break session capture.
83
56
  }
84
57
  }
85
58
 
@@ -183,13 +156,9 @@ function parseTranscript(transcriptPath) {
183
156
  const text = typeof block.content === 'string'
184
157
  ? block.content
185
158
  : JSON.stringify(block.content);
186
- // Same reason as the bash branch, and one more: this array is
187
- // ALSO the payload `analyzeFailure` sends to the configured LLM
188
- // provider. A failed request that echoes its own Authorization
189
- // header — the ordinary shape of an auth error — would be stored
190
- // and then transmitted off the machine. Redacted once here, at
191
- // the point the text enters the process, so every downstream use
192
- // inherits it.
159
+ // Same reason as the bash branch: a failed request may echo its
160
+ // own Authorization header. Redact once, where text enters the
161
+ // process, so every downstream use inherits it.
193
162
  errorsEncountered.push(redactSecrets(text).slice(0, 200));
194
163
  }
195
164
  }
@@ -338,33 +307,12 @@ process.stdin.on('end', async () => {
338
307
  // healthy exit, so it MUST stamp (see stampHookRunOnly).
339
308
  if (toolCallCount < 3) { stampHookRunOnly(process.env, 'session-summary'); return exit0(); }
340
309
 
341
- // Hoisted to outer-try scope so the LLM failure-analysis block
342
- // below (which runs AFTER db.close()) can reference it. Earlier
343
- // version defined projectName inside the inner try-finally and the
344
- // LLM path threw `projectName is not defined` silently — caught by
345
- // the LLM try/catch but logged to stderr. Result: lesson_learned
346
- // creation never actually happened in production.
347
310
  const projectName = getProjectName(cwd);
348
311
 
349
312
  // Open DB via shared helper — applies SCHEMA_SQL + status migration.
350
313
  // { fts: true } guarantees the entities_fts table exists so captureEntity()
351
314
  // can keep it in sync — session-insight memories must be FTS-recallable.
352
315
  //
353
- // sqlite-vec is NOT loaded here, and used to be. The comment said it was
354
- // needed "for embedding-aware recall-effectiveness tracking" — but this
355
- // hook runs exactly two statements, `PRAGMA table_info(entities)` and
356
- // `SELECT id FROM entities WHERE name = ?`, and `captureEntity` in
357
- // _shared.js touches no vectors either. Nothing here has ever used the
358
- // extension.
359
- //
360
- // It was not free. sqlite-vec ships its engine as a per-platform file
361
- // through optionalDependencies, and on a platform it does not publish the
362
- // load threw — past the `require` guard, which never fired because the JS
363
- // wrapper resolves fine and the throw happens later inside
364
- // `sqliteVec.load()`. Measured with the platform binary hidden: the whole
365
- // Stop capture vanished (0 entities against a control run's 1) and the
366
- // user got a `Require stack:` dump on stderr. An extension nobody calls
367
- // was silently costing every session on those platforms its memory.
368
316
  const { db } = openHookDb(process.env, { fts: true });
369
317
  let writeFailed = false;
370
318
  try {
@@ -628,68 +576,17 @@ process.stdin.on('end', async () => {
628
576
  db.close();
629
577
  }
630
578
 
631
- // ── LLM-powered failure analysis (Level 1 only) ──────────────────────
632
- // Runs AFTER the hook's own DB is closed.
633
- // Uses the core module's DB singleton (openDatabase/closeDatabase).
634
- // Wrapped in its own try/catch — never blocks rule-based extraction.
635
- if (errorsEncountered.length > 0 && filesEdited.length > 0) {
636
- try {
637
- // F5: derive pluginRoot strictly from this file's location.
638
- // See `resolvePluginRoot` for the full reasoning.
639
- const pluginRoot = resolvePluginRoot(import.meta.url);
640
- const configMod = await importFromPluginRoot(pluginRoot, 'dist/core/config.js');
641
- const config = configMod.readConfig();
642
-
643
- if (config.llm) {
644
- const { openDatabase, closeDatabase } = await importFromPluginRoot(pluginRoot, 'dist/db.js');
645
- const { analyzeFailure } = await importFromPluginRoot(pluginRoot, 'dist/core/failure-analyzer.js');
646
- const { createLesson } = await importFromPluginRoot(pluginRoot, 'dist/core/lesson-engine.js');
647
-
648
- openDatabase();
649
- try {
650
- // Pass cross-provider failover chain so a stale Anthropic key
651
- // doesn't silently disable Stop-hook lesson generation.
652
- const lesson = await analyzeFailure(errorsEncountered, filesEdited, config.llm, { fallbacks: config.llmFallbacks });
653
- if (lesson) {
654
- createLesson(lesson, projectName);
655
- }
656
- } finally {
657
- closeDatabase();
658
- }
659
- }
660
- } catch (llmErr) {
661
- // LLM analysis failed — rule-based extraction already captured the session.
662
- // Log to stderr so config issues (e.g. invalid API key) are visible.
663
- try { process.stderr.write(`[memesh] LLM failure analysis skipped: ${llmErr?.message || llmErr}\n`); } catch {}
664
- }
665
- }
666
-
667
- // Auto-trigger dream — solves the "Insights tab is empty for users
668
- // who don't know `memesh dream run` exists" problem. Throttled to
669
- // once per project per 24h, gated by minimum activity threshold.
670
- // Background-detached spawn so the hook exits immediately even if
671
- // the LLM call takes 30-60s. See `maybeTriggerDream` for the gate
672
- // logic and dream-history.json schema.
673
- try {
674
- const pluginRoot = resolvePluginRoot(import.meta.url);
675
- const configMod = await importFromPluginRoot(pluginRoot, 'dist/core/config.js');
676
- const config = configMod.readConfig();
677
- maybeTriggerDream(projectName, config, pluginRoot);
678
- } catch (dreamErr) {
679
- try { process.stderr.write(`[memesh] dream auto-trigger skipped: ${dreamErr?.message || dreamErr}\n`); } catch {}
680
- }
681
579
  } catch (err) {
682
580
  // Never crash Claude Code — leave a trace for debugging.
683
581
  //
684
- // Every error is traced now. There used to be a suppression branch for a
685
- // `skip-session-capture:` sentinel, thrown when sqlite-vec was missing
686
- // an extension this hook never used. The thrower is gone, so the branch
687
- // could only ever hide a real error from here on.
582
+ // Every error is traced. A retired suppression sentinel once hid setup
583
+ // failures from this hook; with that branch gone, real capture errors stay
584
+ // visible without crashing the host session.
688
585
  try { process.stderr.write(`[memesh session-summary] ${err?.message || err}\n`); } catch {}
689
586
  }
690
587
 
691
- // Dispatch auto-update if policy + cache permit. Runs after all session work
692
- // so the runner cannot overwrite dist/ while peer hooks are reading it.
588
+ // Update only after all session work so installed files cannot change while
589
+ // this hook is still reading them.
693
590
  await runAutoUpdateAtStop();
694
591
 
695
592
  // Emit NOTHING on success — not `{"suppressOutput": true}`.
@@ -711,173 +608,6 @@ process.stdin.on('end', async () => {
711
608
  function exit0() {
712
609
  process.exit(0);
713
610
  }
714
-
715
- // =============================================================================
716
- // Dream auto-trigger (Phase 2 / Phase 3 background runner)
717
- // =============================================================================
718
- //
719
- // Without an automated trigger, `memesh dream` only runs when the user
720
- // types it into a terminal — and most users never read the docs that
721
- // far. Result: Insights tab stays empty and the KG accumulates 89.7%
722
- // orphan rate (the maintainer's own observation on this DB).
723
- //
724
- // This trigger fires at the END of every Stop hook (after rule-based
725
- // session capture + optional LLM failure analysis). It:
726
- // 1. Loads ~/.memesh/dream-history.json — a per-project record of
727
- // the last dream run timestamp + outcome.
728
- // 2. Throttles: skip if < THROTTLE_HOURS since the project's last
729
- // run, even if the previous run produced zero proposals.
730
- // 3. Activity gate: skip if the project has < MIN_EPISODIC episodic
731
- // entities to draw from in the last WINDOW_DAYS days.
732
- // 4. LLM gate: skip if no LLM provider configured (Phase 2 needs
733
- // Smart Mode — Phase 3 patterns same).
734
- // 5. Spawns `node <pluginRoot>/dist/transports/cli/cli.js dream run
735
- // --project <name> --max-llm-calls 2 --window-days 14` as a
736
- // detached child process. Stdout/stderr go to a per-project log
737
- // under ~/.memesh/dream-runs/ so the user can `tail -f` to see
738
- // progress without the hook blocking.
739
- // 6. Records the run start in dream-history.json BEFORE spawning so
740
- // a long-running spawn doesn't get re-triggered on the next
741
- // Stop within the same window.
742
- //
743
- // The spawned process inherits config from disk, including the
744
- // `llmFallbacks` chain wired in commit 883abd4d, so a primary
745
- // outage falls through to Ollama automatically.
746
-
747
- const DREAM_THROTTLE_HOURS = 24;
748
- const DREAM_MIN_EPISODIC = 10;
749
- const DREAM_WINDOW_DAYS = 14;
750
- const DREAM_MAX_LLM_CALLS = 2;
751
- const DREAM_HISTORY_BASENAME = 'dream-history.json';
752
- const DREAM_LOG_DIRNAME = 'dream-runs';
753
-
754
- const DREAM_EPISODIC_TYPES = [
755
- 'commit',
756
- 'session_keypoint',
757
- 'session-insight',
758
- 'workflow_checkpoint',
759
- 'weekly-summary',
760
- 'weekly_summary',
761
- ];
762
-
763
- // Debug-trace gate. Set MEMESH_DREAM_TRIGGER_DEBUG=1 to emit a stderr
764
- // breadcrumb at every gate decision in maybeTriggerDream. Contract:
765
- // [memesh dream-trigger] <stage>=<value>...
766
- // Matches one line per stage (resolve, llm-gate, throttle-gate,
767
- // activity-gate, history-write, spawn). Stable across releases — if
768
- // you rename a stage, update the matching test fixture too.
769
- function dreamTrigTrace(stage, fields) {
770
- if (process.env.MEMESH_DREAM_TRIGGER_DEBUG !== '1') return;
771
- try {
772
- const parts = Object.entries(fields || {}).map(([k, v]) => {
773
- if (v === undefined) return `${k}=<undef>`;
774
- if (v === null) return `${k}=<null>`;
775
- return `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`;
776
- });
777
- process.stderr.write(`[memesh dream-trigger] ${stage} ${parts.join(' ')}\n`);
778
- } catch {}
779
- }
780
-
781
- function dreamHistoryPath() {
782
- // Test isolation: tests set MEMESH_DB_PATH to a tmp file, expecting
783
- // sibling state (history, logs) to land beside it. Without honouring
784
- // that, real-home `~/.memesh/dream-history.json` would be polluted
785
- // by every test run. Same precedence as memesh's other state files —
786
- // see `getMemeshDirFromDbPath` in `_shared.js`.
787
- //
788
- // Windows note: on Windows under execFileSync, env vars propagate as
789
- // plain strings (no canonicalisation). If a caller passed MEMESH_DIR
790
- // with mixed separators (e.g. `C:/Users/.../tmp/.memesh`), join() will
791
- // happily mix `/` and `\`. Node fs accepts either — the only divergence
792
- // is in pure string comparisons, which we don't do here. The trace
793
- // below shows the resolved value verbatim so a Windows diagnosis run
794
- // can confirm what actually arrived.
795
- // The helper IS the precedence (MEMESH_DB_PATH > MEMESH_DIR > home).
796
- // A hand-rolled version here inverted it (MEMESH_DIR won over
797
- // MEMESH_DB_PATH), so with both set, dream history landed in a different
798
- // directory than every sibling state file — plus a dead home-fallback
799
- // branch, since the helper always returns a string.
800
- const dir = getMemeshDirFromDbPath();
801
- dreamTrigTrace('resolve', {
802
- src: process.env.MEMESH_DB_PATH ? 'db-path' : (process.env.MEMESH_DIR ? 'env' : 'home'),
803
- MEMESH_DIR: process.env.MEMESH_DIR,
804
- MEMESH_DB_PATH: process.env.MEMESH_DB_PATH,
805
- dir,
806
- platform: process.platform,
807
- });
808
- return { dir, historyFile: join(dir, DREAM_HISTORY_BASENAME), logDir: join(dir, DREAM_LOG_DIRNAME) };
809
- }
810
-
811
- function readDreamHistory() {
812
- try {
813
- const { historyFile } = dreamHistoryPath();
814
- if (!existsSync(historyFile)) return {};
815
- const raw = readFileSync(historyFile, 'utf8');
816
- const parsed = JSON.parse(raw);
817
- return (parsed && typeof parsed === 'object') ? parsed : {};
818
- } catch { return {}; }
819
- }
820
-
821
- function writeDreamHistory(history) {
822
- try {
823
- const { dir, historyFile } = dreamHistoryPath();
824
- mkdirSync(dir, { recursive: true });
825
- writeFileSync(historyFile, JSON.stringify(history, null, 2));
826
- dreamTrigTrace('history-write', { historyFile, ok: true, projects: Object.keys(history) });
827
- } catch (err) {
828
- dreamTrigTrace('history-write', { ok: false, err: err?.message || String(err) });
829
- try { process.stderr.write(`[memesh dream-history write] ${err?.message || err}\n`); } catch {}
830
- }
831
- }
832
-
833
- /**
834
- * Count episodic entities for the given project over the configured
835
- * window. Only issues a SELECT, but the handle is openHookDb's ordinary
836
- * read-write one (it runs the schema/migration chain) — there is no
837
- * read-only variant on the hook side, and this call site must not stamp
838
- * the heartbeat (recordHookRun is per-hook-exit, never per-open).
839
- */
840
- function countEpisodicEntities(projectName) {
841
- let handle;
842
- try {
843
- handle = openHookDb();
844
- // { db, dbPath } — don't call db.prepare() on the wrapper itself (an
845
- // earlier version did, and silently skipped the gate, defeating the
846
- // trigger's whole purpose).
847
- const db = handle.db;
848
- const since = new Date(Date.now() - DREAM_WINDOW_DAYS * 86400000).toISOString();
849
- const types = DREAM_EPISODIC_TYPES.map(() => '?').join(',');
850
- // Project-membership is determined by the `project:<name>` tag only.
851
- // The previous implementation also OR'd on `e.name LIKE 'project-%'`
852
- // as a fallback for legacy entities, but that branch over-counts when
853
- // two projects share a name prefix (e.g. counting `memesh` would
854
- // sweep in `memesh-cloud-keypoint-*` entities). Post-v3.0 episodic
855
- // entities all carry the project tag — the auto-tagger writes it on
856
- // every session_keypoint / commit / session_insight / session_lesson
857
- // / failure_pattern / decision_anchor — so the tag-only path is the
858
- // accurate signal. See v4.2.1 CHANGELOG known-limitations note.
859
- const sql = `SELECT COUNT(DISTINCT e.id) AS n
860
- FROM entities e
861
- INNER JOIN tags t ON t.entity_id = e.id AND t.tag = ?
862
- WHERE e.status = 'active'
863
- AND e.type IN (${types})
864
- AND e.created_at >= ?`;
865
- const projectTag = `project:${projectName}`;
866
- const row = db.prepare(sql).get(projectTag, ...DREAM_EPISODIC_TYPES, since);
867
- return row?.n ?? 0;
868
- } catch (err) {
869
- try { process.stderr.write(`[memesh dream-trigger count] ${err?.message || err}\n`); } catch {}
870
- return 0;
871
- } finally {
872
- try { handle?.db?.close(); } catch {}
873
- }
874
- }
875
-
876
- /**
877
- * Decide whether to fire dream for `projectName` and, if so, spawn
878
- * the detached background runner. Pure side effect — no return value
879
- * used by callers.
880
- */
881
611
  /**
882
612
  * Attachment record types Claude Code uses to persist a hook's own output
883
613
  * into the transcript. Anything memesh injected reaches the transcript
@@ -927,88 +657,3 @@ export function stripHookEchoes(rawTranscript) {
927
657
  }
928
658
  return kept.join('\n');
929
659
  }
930
-
931
- export function maybeTriggerDream(projectName, config, pluginRoot) {
932
- dreamTrigTrace('enter', { projectName, hasLlm: Boolean(config?.llm) });
933
- if (!projectName || projectName === 'unknown') {
934
- dreamTrigTrace('exit', { reason: 'no-project-name', projectName });
935
- return;
936
- }
937
-
938
- // Phase 2 & 3 both need a configured LLM. Without it the dreamer's
939
- // first action is to push `{reason: 'no LLM configured'}` into
940
- // skipped[] and exit, which would still bump our throttle clock
941
- // for no value — so gate here.
942
- if (!config?.llm) {
943
- dreamTrigTrace('exit', { reason: 'llm-gate-fail' });
944
- return;
945
- }
946
- dreamTrigTrace('llm-gate', { ok: true });
947
-
948
- const history = readDreamHistory();
949
- const last = history[projectName];
950
- if (last?.last_run_iso) {
951
- const ageMs = Date.now() - new Date(last.last_run_iso).getTime();
952
- if (Number.isFinite(ageMs) && ageMs < DREAM_THROTTLE_HOURS * 3600 * 1000) {
953
- dreamTrigTrace('exit', { reason: 'throttle-gate-fail', ageMs, last_run_iso: last.last_run_iso });
954
- return; // throttled
955
- }
956
- }
957
- dreamTrigTrace('throttle-gate', { ok: true, prior_last_run_iso: last?.last_run_iso });
958
-
959
- const episodicCount = countEpisodicEntities(projectName);
960
- if (episodicCount < DREAM_MIN_EPISODIC) {
961
- dreamTrigTrace('exit', { reason: 'activity-gate-fail', episodicCount, threshold: DREAM_MIN_EPISODIC });
962
- return;
963
- }
964
- dreamTrigTrace('activity-gate', { ok: true, episodicCount });
965
-
966
- // Activity gate passed — record start BEFORE spawning so we don't
967
- // re-trigger on any subsequent Stop within the window even if the
968
- // child takes 30-60s.
969
- history[projectName] = {
970
- last_run_iso: new Date().toISOString(),
971
- last_episodic_count: episodicCount,
972
- last_window_days: DREAM_WINDOW_DAYS,
973
- };
974
- writeDreamHistory(history);
975
-
976
- // Spawn detached so the hook exits immediately. Stdio routes to a
977
- // per-project log file so the user can inspect dream output without
978
- // the hook blocking on the LLM call.
979
- try {
980
- const { dir, logDir } = dreamHistoryPath();
981
- mkdirSync(logDir, { recursive: true });
982
- const safeProj = projectName.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 64);
983
- const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
984
- const logFile = join(logDir, `${safeProj}-${ts}.log`);
985
-
986
- // Append a header so a tail -f shows context for the run.
987
- appendFileSync(logFile, `[memesh dream] ${new Date().toISOString()} project=${projectName} episodic_count=${episodicCount} window=${DREAM_WINDOW_DAYS}d max_llm_calls=${DREAM_MAX_LLM_CALLS}\n`);
988
-
989
- const cliPath = join(pluginRoot, 'dist/transports/cli/cli.js');
990
- if (!existsSync(cliPath)) {
991
- appendFileSync(logFile, `[memesh dream] cli.js missing at ${cliPath}, skipping\n`);
992
- return;
993
- }
994
-
995
- const args = [
996
- cliPath, 'dream', 'run',
997
- '--project', projectName,
998
- '--max-llm-calls', String(DREAM_MAX_LLM_CALLS),
999
- '--window-days', String(DREAM_WINDOW_DAYS),
1000
- ];
1001
-
1002
- const logFd = require('fs').openSync(logFile, 'a');
1003
- const child = spawn(process.execPath, args, {
1004
- detached: true,
1005
- stdio: ['ignore', logFd, logFd],
1006
- env: { ...process.env, MEMESH_DIR: dir },
1007
- });
1008
- child.unref();
1009
- dreamTrigTrace('spawn', { ok: true, pid: child.pid, logFile, MEMESH_DIR: dir });
1010
- } catch (err) {
1011
- dreamTrigTrace('spawn', { ok: false, err: err?.message || String(err) });
1012
- try { process.stderr.write(`[memesh dream-trigger spawn] ${err?.message || err}\n`); } catch {}
1013
- }
1014
- }
@@ -30,6 +30,54 @@
30
30
 
31
31
  set -uo pipefail
32
32
 
33
+ # D9: removes every OTHER stale version directory under $root, not just the
34
+ # one an upgrade just swapped out. Before the atomic-swap rename elsewhere in
35
+ # this script, an interrupted or pre-this-mechanism upgrade could leave a
36
+ # version directory behind with nothing left to remove it later — measured
37
+ # on a real machine: 9 old version directories, 1.2 GB, accumulated with no
38
+ # bound. Only entries whose full name is exactly `<major>.<minor>.<patch>`
39
+ # are touched, so a `.staging-*`/`.previous-*` marker from a genuinely
40
+ # concurrent run (should be impossible under $LOCK_DIR, but this check does
41
+ # not rely on that) or anything else unexpected under $root is left alone.
42
+ # $keep_version is always excluded; an optional $also_keep protects a second
43
+ # name — the registry's OWN recorded install path (however it is spelled),
44
+ # so the noncanonical-path repair in section 2 below can keep leaving that
45
+ # one directory alone for a human to clean up, exactly as it already did
46
+ # before this function existed.
47
+ #
48
+ # Defined this early, and callable on its own, so
49
+ # `tests/upgrade-plugin-cache-sweep.test.ts` can source this file with
50
+ # MEMESH_UPGRADE_PLUGIN_SOURCE_ONLY=1 (below) and call it directly against a
51
+ # throwaway root — testing the shipped function, not a hand-copied
52
+ # reimplementation of it — without running the rest of this script, which
53
+ # talks to a real Claude Code marketplace checkout.
54
+ sweep_stale_cache_versions() {
55
+ local root="$1" keep_version="$2" also_keep="${3:-}" entry name
56
+ [ -d "$root" ] || return 0
57
+ for entry in "$root"/*; do
58
+ [ -d "$entry" ] || continue
59
+ name="$(basename "$entry")"
60
+ [ "$name" = "$keep_version" ] && continue
61
+ [ -n "$also_keep" ] && [ "$name" = "$also_keep" ] && continue
62
+ if printf '%s' "$name" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
63
+ rm -rf "$entry" 2>/dev/null
64
+ if [ -e "$entry" ]; then
65
+ echo "WARNING: could not remove stale cached version at $entry — remove it manually." >&2
66
+ fi
67
+ fi
68
+ done
69
+ }
70
+
71
+ # Test-only escape hatch: source this file with this variable set to load
72
+ # `sweep_stale_cache_versions` (and any other function defined above this
73
+ # guard) without running the rest of the script. `return` exits a sourced
74
+ # file without killing the parent shell; `|| exit 0` is the fallback for the
75
+ # (unsupported, but harmless to guard) case of someone executing the script
76
+ # directly with the variable set.
77
+ if [ "${MEMESH_UPGRADE_PLUGIN_SOURCE_ONLY:-0}" = "1" ]; then
78
+ return 0 2>/dev/null || exit 0
79
+ fi
80
+
33
81
  if [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
34
82
  CLAUDE_CONFIG_ROOT="$CLAUDE_CONFIG_DIR"
35
83
  elif [ -n "${HOME:-}" ]; then
@@ -282,12 +330,28 @@ ENTRY_SNAPSHOT="$(INSTALL_REGISTRY="$INSTALL_REGISTRY" CACHE_ROOT="$CACHE_ROOT"
282
330
  && path.resolve(entry.installPath) === expectedPath
283
331
  ? 'canonical'
284
332
  : 'noncanonical';
285
- process.stdout.write(['selected', index, version, sha, registrySha256, installPathState, String(opened.dev), String(opened.ino)].join('\\t'));
333
+ // D9's cache sweep must never remove a directory the registry ITSELF
334
+ // still points to, canonical or not — this is the one directory the
335
+ // 'noncanonical … repairing it' path above (section 2) deliberately
336
+ // leaves in place. Reported as a bare basename, and only when that
337
+ // basename resolves back under this same cache root with no '..'
338
+ // segment, so a crafted absolute installPath from a tampered registry
339
+ // cannot smuggle an arbitrary path into the sweep's exclusion list.
340
+ const rootDir = path.resolve(process.env.CACHE_ROOT);
341
+ let recordedBasename = '';
342
+ if (typeof entry.installPath === 'string' && path.isAbsolute(entry.installPath)) {
343
+ const resolved = path.resolve(entry.installPath);
344
+ const base = path.basename(resolved);
345
+ if (path.join(rootDir, base) === resolved && !/[\\r\\n\\t]/.test(base)) {
346
+ recordedBasename = base;
347
+ }
348
+ }
349
+ process.stdout.write(['selected', index, version, sha, registrySha256, installPathState, String(opened.dev), String(opened.ino), recordedBasename].join('\\t'));
286
350
  ")" || {
287
351
  echo "ERROR: could not read the installed memesh entries from $INSTALL_REGISTRY" >&2
288
352
  exit 1
289
353
  }
290
- IFS=$'\t' read -r ENTRY_STATE ENTRY_INDEX CURRENT_VERSION INSTALLED_SHA ORIGINAL_REGISTRY_SHA256 INSTALL_PATH_STATE ORIGINAL_REGISTRY_DEV ORIGINAL_REGISTRY_INO <<< "$ENTRY_SNAPSHOT"
354
+ IFS=$'\t' read -r ENTRY_STATE ENTRY_INDEX CURRENT_VERSION INSTALLED_SHA ORIGINAL_REGISTRY_SHA256 INSTALL_PATH_STATE ORIGINAL_REGISTRY_DEV ORIGINAL_REGISTRY_INO RECORDED_INSTALL_BASENAME <<< "$ENTRY_SNAPSHOT"
291
355
  case "$ENTRY_STATE" in
292
356
  identity-changed)
293
357
  echo "ERROR: installed_plugins.json changed file identity while this upgrade was reading it — refusing to continue." >&2
@@ -620,6 +684,11 @@ if [ -e "$PREVIOUS_PATH" ] || [ -L "$PREVIOUS_PATH" ]; then
620
684
  echo " Remove it manually when no Claude Code process is using it: rm -rf \"$PREVIOUS_PATH\"" >&2
621
685
  fi
622
686
 
687
+ # D9: sweep every OTHER stale version directory under $CACHE_ROOT, not just
688
+ # the one this run just swapped out — see `sweep_stale_cache_versions`'s own
689
+ # definition near the top of this file for why.
690
+ sweep_stale_cache_versions "$CACHE_ROOT" "$NEW_VERSION" "$RECORDED_INSTALL_BASENAME"
691
+
623
692
  # ─── 7. Done ─────────────────────────────────────────────────────────────
624
693
  echo ""
625
694
  echo "✓ MeMesh upgraded: $CURRENT_VERSION (${INSTALLED_SHA:0:8}) -> $NEW_VERSION (${MARKETPLACE_SHA:0:8})"