@pcircle/memesh 4.5.0 → 4.6.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 (256) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +6 -4
  3. package/.mcp.json +1 -1
  4. package/AGENTS.md +95 -0
  5. package/README.de.md +130 -39
  6. package/README.md +188 -43
  7. package/README.zh-TW.md +133 -41
  8. package/dashboard/dist/index.html +9 -9
  9. package/dist/cli/view-live.js +3 -3
  10. package/dist/core/analytics.d.ts +3 -3
  11. package/dist/core/analytics.d.ts.map +1 -1
  12. package/dist/core/analytics.js.map +1 -1
  13. package/dist/core/auto-tagger.d.ts.map +1 -1
  14. package/dist/core/auto-tagger.js +4 -9
  15. package/dist/core/auto-tagger.js.map +1 -1
  16. package/dist/core/briefing.d.ts +8 -0
  17. package/dist/core/briefing.d.ts.map +1 -0
  18. package/dist/core/briefing.js +91 -0
  19. package/dist/core/briefing.js.map +1 -0
  20. package/dist/core/capture-flag.d.ts +5 -0
  21. package/dist/core/capture-flag.d.ts.map +1 -0
  22. package/dist/core/capture-flag.js +10 -0
  23. package/dist/core/capture-flag.js.map +1 -0
  24. package/dist/core/config.d.ts +0 -1
  25. package/dist/core/config.d.ts.map +1 -1
  26. package/dist/core/config.js +2 -1
  27. package/dist/core/config.js.map +1 -1
  28. package/dist/core/conflict-candidates.d.ts +20 -0
  29. package/dist/core/conflict-candidates.d.ts.map +1 -0
  30. package/dist/core/conflict-candidates.js +79 -0
  31. package/dist/core/conflict-candidates.js.map +1 -0
  32. package/dist/core/conflict-judge.d.ts +47 -0
  33. package/dist/core/conflict-judge.d.ts.map +1 -0
  34. package/dist/core/conflict-judge.js +189 -0
  35. package/dist/core/conflict-judge.js.map +1 -0
  36. package/dist/core/demo.d.ts +2 -2
  37. package/dist/core/demo.d.ts.map +1 -1
  38. package/dist/core/demo.js.map +1 -1
  39. package/dist/core/digest-validator.d.ts.map +1 -1
  40. package/dist/core/digest-validator.js +3 -5
  41. package/dist/core/digest-validator.js.map +1 -1
  42. package/dist/core/doctor.d.ts +3 -0
  43. package/dist/core/doctor.d.ts.map +1 -1
  44. package/dist/core/doctor.js +207 -85
  45. package/dist/core/doctor.js.map +1 -1
  46. package/dist/core/dreamer.d.ts +20 -9
  47. package/dist/core/dreamer.d.ts.map +1 -1
  48. package/dist/core/dreamer.js +416 -58
  49. package/dist/core/dreamer.js.map +1 -1
  50. package/dist/core/embedder.d.ts +7 -5
  51. package/dist/core/embedder.d.ts.map +1 -1
  52. package/dist/core/embedder.js +38 -9
  53. package/dist/core/embedder.js.map +1 -1
  54. package/dist/core/extractor.d.ts.map +1 -1
  55. package/dist/core/extractor.js +2 -1
  56. package/dist/core/extractor.js.map +1 -1
  57. package/dist/core/failure-analyzer.d.ts.map +1 -1
  58. package/dist/core/failure-analyzer.js +7 -12
  59. package/dist/core/failure-analyzer.js.map +1 -1
  60. package/dist/core/graph.d.ts +4 -4
  61. package/dist/core/graph.d.ts.map +1 -1
  62. package/dist/core/graph.js.map +1 -1
  63. package/dist/core/install-channel.d.ts +1 -1
  64. package/dist/core/install-channel.d.ts.map +1 -1
  65. package/dist/core/install-channel.js +16 -5
  66. package/dist/core/install-channel.js.map +1 -1
  67. package/dist/core/install-hooks.d.ts +6 -0
  68. package/dist/core/install-hooks.d.ts.map +1 -1
  69. package/dist/core/install-hooks.js +0 -0
  70. package/dist/core/install-hooks.js.map +1 -1
  71. package/dist/core/json-utils.d.ts +1 -0
  72. package/dist/core/json-utils.d.ts.map +1 -1
  73. package/dist/core/json-utils.js +19 -10
  74. package/dist/core/json-utils.js.map +1 -1
  75. package/dist/core/kg-backfill.d.ts +3 -4
  76. package/dist/core/kg-backfill.d.ts.map +1 -1
  77. package/dist/core/kg-backfill.js +1 -4
  78. package/dist/core/kg-backfill.js.map +1 -1
  79. package/dist/core/lesson-engine.d.ts +1 -0
  80. package/dist/core/lesson-engine.d.ts.map +1 -1
  81. package/dist/core/lesson-engine.js +1 -0
  82. package/dist/core/lesson-engine.js.map +1 -1
  83. package/dist/core/lifecycle.d.ts +4 -4
  84. package/dist/core/lifecycle.d.ts.map +1 -1
  85. package/dist/core/lifecycle.js +15 -22
  86. package/dist/core/lifecycle.js.map +1 -1
  87. package/dist/core/llm-client.d.ts.map +1 -1
  88. package/dist/core/llm-client.js +3 -6
  89. package/dist/core/llm-client.js.map +1 -1
  90. package/dist/core/llm-telemetry.d.ts +4 -4
  91. package/dist/core/llm-telemetry.d.ts.map +1 -1
  92. package/dist/core/llm-telemetry.js +1 -1
  93. package/dist/core/llm-telemetry.js.map +1 -1
  94. package/dist/core/memory-tool.d.ts.map +1 -1
  95. package/dist/core/memory-tool.js +8 -4
  96. package/dist/core/memory-tool.js.map +1 -1
  97. package/dist/core/operations.d.ts.map +1 -1
  98. package/dist/core/operations.js +41 -17
  99. package/dist/core/operations.js.map +1 -1
  100. package/dist/core/paths.d.ts +3 -0
  101. package/dist/core/paths.d.ts.map +1 -1
  102. package/dist/core/paths.js +67 -1
  103. package/dist/core/paths.js.map +1 -1
  104. package/dist/core/patterns.d.ts +2 -2
  105. package/dist/core/patterns.d.ts.map +1 -1
  106. package/dist/core/patterns.js.map +1 -1
  107. package/dist/core/project-tags.d.ts +3 -3
  108. package/dist/core/project-tags.d.ts.map +1 -1
  109. package/dist/core/project-tags.js.map +1 -1
  110. package/dist/core/projects.d.ts +2 -2
  111. package/dist/core/projects.d.ts.map +1 -1
  112. package/dist/core/projects.js.map +1 -1
  113. package/dist/core/prompt-safety.d.ts +1 -0
  114. package/dist/core/prompt-safety.d.ts.map +1 -1
  115. package/dist/core/prompt-safety.js +7 -0
  116. package/dist/core/prompt-safety.js.map +1 -1
  117. package/dist/core/schema-export.d.ts.map +1 -1
  118. package/dist/core/schema-export.js +27 -30
  119. package/dist/core/schema-export.js.map +1 -1
  120. package/dist/core/serializer.d.ts.map +1 -1
  121. package/dist/core/serializer.js +45 -4
  122. package/dist/core/serializer.js.map +1 -1
  123. package/dist/core/setup.d.ts +29 -0
  124. package/dist/core/setup.d.ts.map +1 -0
  125. package/dist/core/setup.js +127 -0
  126. package/dist/core/setup.js.map +1 -0
  127. package/dist/core/stats.d.ts +2 -2
  128. package/dist/core/stats.d.ts.map +1 -1
  129. package/dist/core/stats.js.map +1 -1
  130. package/dist/core/task-state-store.d.ts +17 -0
  131. package/dist/core/task-state-store.d.ts.map +1 -0
  132. package/dist/core/task-state-store.js +45 -0
  133. package/dist/core/task-state-store.js.map +1 -0
  134. package/dist/core/task-state.d.ts +19 -0
  135. package/dist/core/task-state.d.ts.map +1 -0
  136. package/dist/core/task-state.js +91 -0
  137. package/dist/core/task-state.js.map +1 -0
  138. package/dist/core/time-utils.d.ts +2 -0
  139. package/dist/core/time-utils.d.ts.map +1 -0
  140. package/dist/core/time-utils.js +14 -0
  141. package/dist/core/time-utils.js.map +1 -0
  142. package/dist/core/title.d.ts +5 -0
  143. package/dist/core/title.d.ts.map +1 -0
  144. package/dist/core/title.js +14 -0
  145. package/dist/core/title.js.map +1 -0
  146. package/dist/core/transcript-extractor.d.ts +5 -6
  147. package/dist/core/transcript-extractor.d.ts.map +1 -1
  148. package/dist/core/transcript-extractor.js +4 -24
  149. package/dist/core/transcript-extractor.js.map +1 -1
  150. package/dist/core/transcript-source.d.ts.map +1 -1
  151. package/dist/core/transcript-source.js +2 -3
  152. package/dist/core/transcript-source.js.map +1 -1
  153. package/dist/core/types.d.ts +21 -7
  154. package/dist/core/types.d.ts.map +1 -1
  155. package/dist/core/types.js +2 -0
  156. package/dist/core/types.js.map +1 -1
  157. package/dist/core/work-topology.d.ts +33 -0
  158. package/dist/core/work-topology.d.ts.map +1 -0
  159. package/dist/core/work-topology.js +183 -0
  160. package/dist/core/work-topology.js.map +1 -0
  161. package/dist/db.d.ts +5 -10
  162. package/dist/db.d.ts.map +1 -1
  163. package/dist/db.js +194 -196
  164. package/dist/db.js.map +1 -1
  165. package/dist/knowledge-graph.d.ts +4 -2
  166. package/dist/knowledge-graph.d.ts.map +1 -1
  167. package/dist/knowledge-graph.js +68 -49
  168. package/dist/knowledge-graph.js.map +1 -1
  169. package/dist/mcp/server.js +2 -1
  170. package/dist/mcp/server.js.map +1 -1
  171. package/dist/skills-manifest.json +61 -36
  172. package/dist/storage/conflicts.d.ts +3 -3
  173. package/dist/storage/conflicts.d.ts.map +1 -1
  174. package/dist/storage/conflicts.js +2 -7
  175. package/dist/storage/conflicts.js.map +1 -1
  176. package/dist/storage/fts-index.d.ts +6 -4
  177. package/dist/storage/fts-index.d.ts.map +1 -1
  178. package/dist/storage/fts-index.js +16 -4
  179. package/dist/storage/fts-index.js.map +1 -1
  180. package/dist/storage/schema.d.ts +20 -0
  181. package/dist/storage/schema.d.ts.map +1 -0
  182. package/dist/storage/schema.js +274 -0
  183. package/dist/storage/schema.js.map +1 -0
  184. package/dist/storage/sqlite.d.ts +20 -0
  185. package/dist/storage/sqlite.d.ts.map +1 -0
  186. package/dist/storage/sqlite.js +64 -0
  187. package/dist/storage/sqlite.js.map +1 -0
  188. package/dist/storage/vector-index.d.ts +3 -0
  189. package/dist/storage/vector-index.d.ts.map +1 -0
  190. package/dist/storage/vector-index.js +7 -0
  191. package/dist/storage/vector-index.js.map +1 -0
  192. package/dist/transports/cli/cli.d.ts +1 -4
  193. package/dist/transports/cli/cli.d.ts.map +1 -1
  194. package/dist/transports/cli/cli.js +494 -76
  195. package/dist/transports/cli/cli.js.map +1 -1
  196. package/dist/transports/http/retired-routes.d.ts.map +1 -1
  197. package/dist/transports/http/retired-routes.js +1 -0
  198. package/dist/transports/http/retired-routes.js.map +1 -1
  199. package/dist/transports/http/server.d.ts.map +1 -1
  200. package/dist/transports/http/server.js +243 -323
  201. package/dist/transports/http/server.js.map +1 -1
  202. package/dist/transports/mcp/handlers.d.ts +48 -94
  203. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  204. package/dist/transports/mcp/handlers.js +77 -56
  205. package/dist/transports/mcp/handlers.js.map +1 -1
  206. package/dist/transports/schemas.d.ts +31 -40
  207. package/dist/transports/schemas.d.ts.map +1 -1
  208. package/dist/transports/schemas.js +31 -37
  209. package/dist/transports/schemas.js.map +1 -1
  210. package/hooks/hooks.json +0 -10
  211. package/llms-install.md +138 -0
  212. package/package.json +19 -18
  213. package/scripts/hooks/_generated/capture-flag.js +17 -0
  214. package/scripts/hooks/_generated/core-paths.js +67 -1
  215. package/scripts/hooks/_generated/fts-index.js +16 -4
  216. package/scripts/hooks/_generated/schema.js +281 -0
  217. package/scripts/hooks/_generated/sqlite.js +71 -0
  218. package/scripts/hooks/_generated/task-state.js +98 -0
  219. package/scripts/hooks/_generated/time-utils.js +21 -0
  220. package/scripts/hooks/_generated/title.js +21 -0
  221. package/scripts/hooks/_generated/work-topology.js +190 -0
  222. package/scripts/hooks/_shared.js +269 -534
  223. package/scripts/hooks/post-commit.js +55 -10
  224. package/scripts/hooks/pre-compact.js +22 -7
  225. package/scripts/hooks/pre-edit-recall.js +9 -11
  226. package/scripts/hooks/session-start.js +230 -106
  227. package/scripts/hooks/session-summary.js +176 -73
  228. package/scripts/hooks/user-prompt-intent.js +3 -2
  229. package/skills/memesh/SKILL.md +97 -77
  230. package/README.es.md +0 -470
  231. package/README.fr.md +0 -462
  232. package/README.ja.md +0 -470
  233. package/README.ko.md +0 -470
  234. package/README.pt.md +0 -462
  235. package/README.th.md +0 -463
  236. package/README.vi.md +0 -462
  237. package/README.zh-CN.md +0 -469
  238. package/dist/cli/view.d.ts +0 -3
  239. package/dist/cli/view.d.ts.map +0 -1
  240. package/dist/cli/view.js +0 -523
  241. package/dist/cli/view.js.map +0 -1
  242. package/dist/core/skill-usage-log.d.ts +0 -11
  243. package/dist/core/skill-usage-log.d.ts.map +0 -1
  244. package/dist/core/skill-usage-log.js +0 -125
  245. package/dist/core/skill-usage-log.js.map +0 -1
  246. package/dist/core/verifier.d.ts +0 -40
  247. package/dist/core/verifier.d.ts.map +0 -1
  248. package/dist/core/verifier.js +0 -206
  249. package/dist/core/verifier.js.map +0 -1
  250. package/dist/mcp/launcher.d.ts +0 -3
  251. package/dist/mcp/launcher.d.ts.map +0 -1
  252. package/dist/mcp/launcher.js +0 -37
  253. package/dist/mcp/launcher.js.map +0 -1
  254. package/scripts/hooks/pre-bash-orchestration-nudge.js +0 -155
  255. package/scripts/postinstall-rebuild.mjs +0 -41
  256. package/skills/agentic-orchestration/SKILL.md +0 -399
@@ -3,14 +3,30 @@
3
3
  // Session Auto-Capture — Stop hook
4
4
  // Extracts knowledge from completed Claude Code sessions
5
5
  // and stores as session-insight entities in MeMesh.
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.
6
22
 
7
23
  import { createRequire } from 'module';
8
24
  import { basename, join } from 'path';
9
25
  import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
10
26
  import { spawn } from 'child_process';
11
- import os from 'os';
12
27
  import { pathToFileURL } from 'url';
13
28
  import {
29
+ AUTO_CAPTURE_TAG,
14
30
  captureEntity,
15
31
  decideAutoUpdateHook,
16
32
  getMemeshDirFromDbPath,
@@ -19,9 +35,12 @@ import {
19
35
  isAutoCaptureEnabled,
20
36
  openHookDb,
21
37
  readUpdateCheckCache,
38
+ recordHookRun,
39
+ stampHookRunOnly,
22
40
  resolveAutoUpdatePolicy,
23
41
  resolvePluginRoot,
24
42
  spawnAutoUpdate,
43
+ truncateTitle,
25
44
  } from './_shared.js';
26
45
 
27
46
  const require = createRequire(import.meta.url);
@@ -70,9 +89,16 @@ function parseTranscript(transcriptPath) {
70
89
  const bashCommands = [];
71
90
  const errorsEncountered = [];
72
91
  let toolCallCount = 0;
92
+ let readFailed = false;
93
+ // The raw file content, returned so downstream consumers (the
94
+ // recall-effectiveness block) reuse this single read instead of a second
95
+ // readFileSync — real transcripts reach 47MB, so a second full read plus
96
+ // re-parse doubles the Stop hook's dominant I/O cost.
97
+ let rawText = '';
73
98
 
74
99
  try {
75
- const lines = readFileSync(transcriptPath, 'utf8').split('\n').filter(l => l.trim());
100
+ rawText = readFileSync(transcriptPath, 'utf8');
101
+ const lines = rawText.split('\n').filter(l => l.trim());
76
102
  for (const line of lines) {
77
103
  try {
78
104
  const entry = JSON.parse(line);
@@ -133,10 +159,15 @@ function parseTranscript(transcriptPath) {
133
159
  } catch (err) {
134
160
  // The transcript file itself could not be read, which empties this
135
161
  // session's entire capture — filesEdited/errors/toolCallCount all return
136
- // zero, so downstream `toolCallCount < 3` bails and no session insight,
137
- // failure analysis or lesson is produced. An absent file is the normal
138
- // "not written yet" case; anything else is a real fault worth a trace.
162
+ // zero. Left unflagged, those zeros are indistinguishable from a
163
+ // genuinely quiet session, and the light-session bail downstream would
164
+ // STAMP the heartbeat repeated permission/I-O failures keeping doctor
165
+ // green while every session's capture is lost. `readFailed` is the
166
+ // distinct signal: capture was lost, not skipped. An absent file
167
+ // (ENOENT) is the vanished-transcript race, which the caller already
168
+ // treats as a correct nothing-to-do decision.
139
169
  if (err?.code !== 'ENOENT') {
170
+ readFailed = true;
140
171
  try {
141
172
  process.stderr.write(
142
173
  `[memesh session-summary] transcript ${transcriptPath} unreadable ` +
@@ -146,7 +177,7 @@ function parseTranscript(transcriptPath) {
146
177
  }
147
178
  }
148
179
 
149
- return { filesEdited: [...filesEdited], bashCommands, errorsEncountered, toolCallCount };
180
+ return { filesEdited: [...filesEdited], bashCommands, errorsEncountered, toolCallCount, readFailed, rawText };
150
181
  }
151
182
 
152
183
  // Main: read stdin, extract insights, store in DB
@@ -176,7 +207,23 @@ process.stdin.on('end', async () => {
176
207
 
177
208
  const sessionId = inputData.session_id || 'unknown';
178
209
  const transcriptPath = inputData.transcript_path;
179
- const cwd = inputData.cwd || process.cwd();
210
+
211
+ // `cwd` decides the project tag, and the project tag decides which
212
+ // sessions `session-start` injects and which memories `pre-edit-recall`
213
+ // surfaces. Falling back to `process.cwd()` — the hook process's launch
214
+ // directory, which is unspecified for a Stop hook — tagged the session with
215
+ // whatever happened to be current. Measured: a payload with no `cwd` filed
216
+ // the whole session under `project:memesh-llm-memory`, a project it had
217
+ // nothing to do with, silently. That leaks one project's file names, bash
218
+ // commands and error text into another project's context.
219
+ //
220
+ // `post-commit` refuses this exact case and says why: better to miss one
221
+ // capture than to file it under the wrong project. Same rule here.
222
+ if (!inputData.cwd) {
223
+ try { process.stderr.write(`[memesh session-summary] cwd absent in payload (keys: ${Object.keys(inputData).join(',')}); cannot resolve project, skipping capture\n`); } catch {}
224
+ return exit0();
225
+ }
226
+ const cwd = inputData.cwd;
180
227
  // Default-allow: when Claude Code's Stop payload omits
181
228
  // `was_in_agentic_loop` (it has been silently absent in production
182
229
  // for an unknown number of releases — symptom: zero session-insight
@@ -198,7 +245,13 @@ process.stdin.on('end', async () => {
198
245
  // but did nothing, the exact sibling of the `was_in_agentic_loop` absence
199
246
  // above. Removed; the `toolCallCount < 3` check below is the real
200
247
  // low-signal filter.
201
- if (!wasAgenticLoop) return exit0();
248
+ // From here down the payload is well-formed and attributable — every
249
+ // bail is the hook deciding "nothing worth saving", which is a
250
+ // successful run and stamps the heartbeat. The bails ABOVE this line
251
+ // (empty stdin, malformed JSON, missing cwd) are schema-flip shapes: if
252
+ // Claude Code's payload changed under us, capture is effectively dead,
253
+ // and a heartbeat would mask exactly that.
254
+ if (!wasAgenticLoop) { stampHookRunOnly(process.env, 'session-summary'); return exit0(); }
202
255
  // Trace why we're skipping. Two failure modes:
203
256
  // (a) transcript_path absent — schema flip, Claude Code stopped
204
257
  // sending the field. Same bug shape as `was_in_agentic_loop`
@@ -214,14 +267,26 @@ process.stdin.on('end', async () => {
214
267
  }
215
268
  if (!existsSync(transcriptPath)) {
216
269
  try { process.stderr.write(`[memesh session-summary] transcript_path ${transcriptPath} does not exist; skipping capture\n`); } catch {}
270
+ // The payload named a transcript and the FILE is gone (log rotation
271
+ // race) — the hook itself ran fine, so this stamps. A payload that
272
+ // never carried the field at all (schema flip) bails above, unstamped.
273
+ stampHookRunOnly(process.env, 'session-summary');
217
274
  return exit0();
218
275
  }
219
276
 
220
- // Parse transcript
221
- const { filesEdited, bashCommands, errorsEncountered, toolCallCount } = parseTranscript(transcriptPath);
277
+ // Parse transcript (single read — rawText is reused by the
278
+ // recall-effectiveness block below)
279
+ const { filesEdited, bashCommands, errorsEncountered, toolCallCount, readFailed, rawText: transcriptRawText } = parseTranscript(transcriptPath);
280
+
281
+ // An unreadable transcript is NOT a quiet session: the capture was
282
+ // LOST (permissions, I/O), and a heartbeat here would keep doctor green
283
+ // through exactly the repeated failure it exists to expose. No stamp —
284
+ // parseTranscript already traced the fault to stderr.
285
+ if (readFailed) return exit0();
222
286
 
223
- // Skip sessions with too little activity
224
- if (toolCallCount < 3) return exit0();
287
+ // Skip sessions with too little activity — the single most common
288
+ // healthy exit, so it MUST stamp (see stampHookRunOnly).
289
+ if (toolCallCount < 3) { stampHookRunOnly(process.env, 'session-summary'); return exit0(); }
225
290
 
226
291
  // Hoisted to outer-try scope so the LLM failure-analysis block
227
292
  // below (which runs AFTER db.close()) can reference it. Earlier
@@ -232,36 +297,27 @@ process.stdin.on('end', async () => {
232
297
  const projectName = getProjectName(cwd);
233
298
 
234
299
  // Open DB via shared helper — applies SCHEMA_SQL + status migration.
235
- // sqlite-vec is loaded separately because only this hook needs it
236
- // (for embedding-aware recall-effectiveness tracking).
237
300
  // { fts: true } guarantees the entities_fts table exists so captureEntity()
238
301
  // can keep it in sync — session-insight memories must be FTS-recallable.
239
- const handle = openHookDb(process.env, { fts: true });
240
- if (!handle) {
241
- // Native module unavailable (plugin-marketplace cache install with no
242
- // node_modules). Skip session-capture work, but still let the
243
- // auto-update tail below run auto-update is a separate concern from
244
- // session-capture, and a transient DB-availability blip should not
245
- // mask a security-override patch upgrade. Throwing this sentinel
246
- // routes through the existing catch, which already stderr-traces;
247
- // execution then falls through to runAutoUpdateAtStop().
248
- throw new Error('skip-session-capture: better-sqlite3 unavailable');
249
- }
250
- const { db } = handle;
251
- // sqlite-vec is also a native module; same plugin-cache scenario applies
252
- // (the cache tarball ships neither node_module). Resolve through a
253
- // try/require here so a missing vec module degrades the same way as a
254
- // missing better-sqlite3 instead of throwing into the outer catch as a
255
- // bug-shaped error.
256
- let sqliteVec;
257
- try {
258
- sqliteVec = require('sqlite-vec');
259
- } catch {
260
- throw new Error('skip-session-capture: sqlite-vec unavailable');
261
- }
302
+ //
303
+ // sqlite-vec is NOT loaded here, and used to be. The comment said it was
304
+ // needed "for embedding-aware recall-effectiveness tracking" but this
305
+ // hook runs exactly two statements, `PRAGMA table_info(entities)` and
306
+ // `SELECT id FROM entities WHERE name = ?`, and `captureEntity` in
307
+ // _shared.js touches no vectors either. Nothing here has ever used the
308
+ // extension.
309
+ //
310
+ // It was not free. sqlite-vec ships its engine as a per-platform file
311
+ // through optionalDependencies, and on a platform it does not publish the
312
+ // load threw — past the `require` guard, which never fired because the JS
313
+ // wrapper resolves fine and the throw happens later inside
314
+ // `sqliteVec.load()`. Measured with the platform binary hidden: the whole
315
+ // Stop capture vanished (0 entities against a control run's 1) and the
316
+ // user got a `Require stack:` dump on stderr. An extension nobody calls
317
+ // was silently costing every session on those platforms its memory.
318
+ const { db } = openHookDb(process.env, { fts: true });
319
+ let writeFailed = false;
262
320
  try {
263
- sqliteVec.load(db);
264
-
265
321
  // Duplicate detection: if we already captured this session, bail.
266
322
  //
267
323
  // Use the FULL session_id rather than the first 8 chars: real
@@ -270,11 +326,18 @@ process.stdin.on('end', async () => {
270
326
  // share the prefix and silently skipped the second session
271
327
  // entirely. The contract is one stored capture per distinct
272
328
  // session_id, so the dedup key has to be the full id.
329
+ //
330
+ // A dedup bail is a SUCCESSFUL run — the loop executed and correctly
331
+ // decided there was nothing to do — so it stamps the heartbeat like
332
+ // the capture path below does. Only a throw leaves no stamp.
273
333
  const alreadyCaptured = db.prepare("SELECT id FROM entities WHERE name = ?").get(`session-${sessionId}-files`);
274
- if (alreadyCaptured) return exit0();
334
+ if (alreadyCaptured) {
335
+ recordHookRun(db, 'session-summary');
336
+ return exit0();
337
+ }
275
338
 
276
339
  // Build and store session memories
277
- const baseTags = ['source:auto-capture', `session:${sessionId}`, `project:${projectName}`];
340
+ const baseTags = [AUTO_CAPTURE_TAG, `session:${sessionId}`, `project:${projectName}`];
278
341
 
279
342
  // Producer for pre-edit-recall's Strategy 1 (`file:<name>` tag lookup).
280
343
  // That read path queries both the full basename and the extension-less
@@ -298,10 +361,21 @@ process.stdin.on('end', async () => {
298
361
  // entities_fts too. This copy used to insert entity + observations + tags
299
362
  // only, skipping the FTS reindex the sibling hooks did — which left every
300
363
  // session-insight memory unrecallable via the FTS keyword path.
301
- function storeMemory(name, type, observations, tags) {
302
- captureEntity(db, { name, type, observations, tags });
364
+ function storeMemory(name, type, observations, tags, title) {
365
+ // null = the entity row could not be resolved = this write did NOT
366
+ // happen (captureEntity's contract). A run with a failed write must
367
+ // not stamp the heartbeat below — "alive" would be a lie about the
368
+ // exact thing the heartbeat certifies.
369
+ if (!captureEntity(db, { name, type, observations, tags, title })) writeFailed = true;
303
370
  }
304
371
 
372
+ // No free-form human text exists for these three entities the way a
373
+ // commit subject does — title is synthesized from the same structured
374
+ // counts the observations already report. date+project+verb, per the
375
+ // heuristic the design settled on for hooks with no natural title source.
376
+ const titleDate = new Date().toISOString().slice(0, 10);
377
+ const titlePrefix = `${titleDate} ${projectName}`;
378
+
305
379
  // Rule 1: File editing session summary
306
380
  if (filesEdited.length > 0) {
307
381
  storeMemory(
@@ -311,7 +385,8 @@ process.stdin.on('end', async () => {
311
385
  `Session edited ${filesEdited.length} file(s): ${filesEdited.join(', ')}`,
312
386
  `Total tool calls: ${toolCallCount}`,
313
387
  ],
314
- [...baseTags, ...fileTagsFor(filesEdited)]
388
+ [...baseTags, ...fileTagsFor(filesEdited)],
389
+ truncateTitle(`${titlePrefix}: edited ${filesEdited.length} file(s)`)
315
390
  );
316
391
  }
317
392
 
@@ -324,7 +399,8 @@ process.stdin.on('end', async () => {
324
399
  `Fixed ${errorsEncountered.length} error(s) by editing ${filesEdited.join(', ')}`,
325
400
  ...errorsEncountered.slice(0, 3).map(e => `Error: ${e.slice(0, 100)}`),
326
401
  ],
327
- [...baseTags, 'type:bugfix', ...fileTagsFor(filesEdited)]
402
+ [...baseTags, 'type:bugfix', ...fileTagsFor(filesEdited)],
403
+ truncateTitle(`${titlePrefix}: fixed ${errorsEncountered.length} error(s)`)
328
404
  );
329
405
  }
330
406
 
@@ -337,7 +413,8 @@ process.stdin.on('end', async () => {
337
413
  `Significant session: ${toolCallCount} tool calls, ${filesEdited.length} files edited`,
338
414
  ...bashCommands.slice(0, 3).map(c => `Command: ${c}`),
339
415
  ],
340
- [...baseTags, 'type:heavy-session']
416
+ [...baseTags, 'type:heavy-session'],
417
+ truncateTitle(`${titlePrefix}: significant session (${toolCallCount} tool calls)`)
341
418
  );
342
419
  }
343
420
 
@@ -411,7 +488,9 @@ process.stdin.on('end', async () => {
411
488
  // undocumented internal — get it wrong and every entity scores
412
489
  // a hit instead of a miss. Structural removal is copy-count
413
490
  // and encoding independent.
414
- const sessionText = stripHookEchoes(readFileSync(transcriptPath, 'utf8')).toLowerCase();
491
+ // Reuse the raw text parseTranscript already read — a second
492
+ // readFileSync doubles the Stop hook's I/O on 47MB transcripts.
493
+ const sessionText = stripHookEchoes(transcriptRawText).toLowerCase();
415
494
 
416
495
  // Hit/miss decision lives in `isRecallHit` (exported, unit-tested).
417
496
 
@@ -422,14 +501,25 @@ process.stdin.on('end', async () => {
422
501
  'UPDATE entities SET recall_misses = COALESCE(recall_misses, 0) + 1 WHERE id = ?'
423
502
  );
424
503
 
504
+ // The injected block shows an entity's TITLE, not its name
505
+ // (A1 — a machine key like `commit-a1b2c3d` cost tokens and
506
+ // taught the model nothing). Matching on the name alone would
507
+ // therefore score a miss against a string the session was never
508
+ // shown, and a miss is not inert: it lowers the entity's impact
509
+ // factor in core ranking. Match either.
510
+ const titleStmt = db.prepare('SELECT title FROM entities WHERE id = ?');
425
511
  for (let i = 0; i < entityIds.length; i++) {
426
512
  const name = (entityNames[i] || '').toLowerCase();
427
513
  // Skip names that carry no recall signal: too short, or a
428
514
  // machine identifier (auto-capture entities) that can never
429
515
  // substring-match prose. Scoring those would be a guaranteed
430
516
  // unearned miss — see isMeasurableRecallName.
431
- if (!isMeasurableRecallName(name)) continue;
432
- if (isRecallHit(sessionText, name)) {
517
+ let title = null;
518
+ try { title = titleStmt.get(entityIds[i])?.title ?? null; } catch { /* pre-title schema */ }
519
+ // A row is measurable if EITHER string could plausibly appear
520
+ // in prose; a machine-named row with a human title now can.
521
+ if (!isMeasurableRecallName(name) && !isMeasurableRecallName(title)) continue;
522
+ if (isRecallHit(sessionText, name) || isRecallHit(sessionText, title)) {
433
523
  updateHit.run(entityIds[i]);
434
524
  } else {
435
525
  updateMiss.run(entityIds[i]);
@@ -446,6 +536,14 @@ process.stdin.on('end', async () => {
446
536
  // missing-column failure is visible without crashing the hook.
447
537
  try { process.stderr.write(`[memesh session-summary] recall-effectiveness write: ${err?.message || err}\n`); } catch {}
448
538
  }
539
+
540
+ // Heartbeat AFTER capture, so the stamp certifies "the capture loop
541
+ // completed", not "a database handle existed". A throw above skips it,
542
+ // and so does a captureEntity null return (writeFailed) — a run whose
543
+ // write did not land must not read as alive. (The recall-effectiveness
544
+ // block catches its own errors — session memories were already stored
545
+ // by then, so the run still counts.)
546
+ if (!writeFailed) recordHookRun(db, 'session-summary');
449
547
  } finally {
450
548
  db.close();
451
549
  }
@@ -502,12 +600,12 @@ process.stdin.on('end', async () => {
502
600
  }
503
601
  } catch (err) {
504
602
  // Never crash Claude Code — leave a trace for debugging.
505
- // Suppress the expected "skip-session-capture" sentinel (raised when
506
- // better-sqlite3 is unavailable in a marketplace-cache install). All
507
- // other errors are real bugs and deserve a trace.
508
- if (!String(err?.message || '').startsWith('skip-session-capture:')) {
509
- try { process.stderr.write(`[memesh session-summary] ${err?.message || err}\n`); } catch {}
510
- }
603
+ //
604
+ // Every error is traced now. There used to be a suppression branch for a
605
+ // `skip-session-capture:` sentinel, thrown when sqlite-vec was missing
606
+ // an extension this hook never used. The thrower is gone, so the branch
607
+ // could only ever hide a real error from here on.
608
+ try { process.stderr.write(`[memesh session-summary] ${err?.message || err}\n`); } catch {}
511
609
  }
512
610
 
513
611
  // Spawn auto-update if policy + cache permit. Runs after all session work
@@ -603,15 +701,15 @@ function dreamHistoryPath() {
603
701
  // is in pure string comparisons, which we don't do here. The trace
604
702
  // below shows the resolved value verbatim so a Windows diagnosis run
605
703
  // can confirm what actually arrived.
606
- const fromEnv = process.env.MEMESH_DIR;
607
- const fromDbPath = !fromEnv ? getMemeshDirFromDbPath() : null;
608
- const fromHome = (!fromEnv && !fromDbPath)
609
- ? join(os.homedir() || (os.userInfo()?.homedir ?? '.'), '.memesh')
610
- : null;
611
- const dir = fromEnv || fromDbPath || fromHome;
704
+ // The helper IS the precedence (MEMESH_DB_PATH > MEMESH_DIR > home).
705
+ // A hand-rolled version here inverted it (MEMESH_DIR won over
706
+ // MEMESH_DB_PATH), so with both set, dream history landed in a different
707
+ // directory than every sibling state file — plus a dead home-fallback
708
+ // branch, since the helper always returns a string.
709
+ const dir = getMemeshDirFromDbPath();
612
710
  dreamTrigTrace('resolve', {
613
- src: fromEnv ? 'env' : (fromDbPath ? 'db-path' : 'home'),
614
- MEMESH_DIR: fromEnv,
711
+ src: process.env.MEMESH_DB_PATH ? 'db-path' : (process.env.MEMESH_DIR ? 'env' : 'home'),
712
+ MEMESH_DIR: process.env.MEMESH_DIR,
615
713
  MEMESH_DB_PATH: process.env.MEMESH_DB_PATH,
616
714
  dir,
617
715
  platform: process.platform,
@@ -643,18 +741,18 @@ function writeDreamHistory(history) {
643
741
 
644
742
  /**
645
743
  * Count episodic entities for the given project over the configured
646
- * window. Read-only uses the hook's _shared openHookDb helper which
647
- * is already on the import path.
744
+ * window. Only issues a SELECT, but the handle is openHookDb's ordinary
745
+ * read-write one (it runs the schema/migration chain) — there is no
746
+ * read-only variant on the hook side, and this call site must not stamp
747
+ * the heartbeat (recordHookRun is per-hook-exit, never per-open).
648
748
  */
649
749
  function countEpisodicEntities(projectName) {
650
750
  let handle;
651
751
  try {
652
752
  handle = openHookDb();
653
- // openHookDb returns null if better-sqlite3 isn't loadable;
654
- // otherwise { db, dbPath }. Don't try to call db.prepare() on
655
- // the wrapper itself (the previous version did and silently
656
- // skipped the gate, defeating the trigger's whole purpose).
657
- if (!handle?.db) return 0;
753
+ // { db, dbPath } don't call db.prepare() on the wrapper itself (an
754
+ // earlier version did, and silently skipped the gate, defeating the
755
+ // trigger's whole purpose).
658
756
  const db = handle.db;
659
757
  const since = new Date(Date.now() - DREAM_WINDOW_DAYS * 86400000).toISOString();
660
758
  const types = DREAM_EPISODIC_TYPES.map(() => '?').join(',');
@@ -754,12 +852,17 @@ export function stripHookEchoes(rawTranscript) {
754
852
  * which silently failed on JSON-encoded transcripts and scored every entity a
755
853
  * hit — see the callsite comment.)
756
854
  *
757
- * Self-contained for its unit tests: lowercases both sides and ignores names
758
- * shorter than 4 chars (too generic to match reliably).
855
+ * Ignores names shorter than 4 chars (too generic to match reliably).
856
+ *
857
+ * CONTRACT: `sessionText` must already be lowercased. The haystack is a
858
+ * multi-megabyte transcript and this runs twice per injected entity (name +
859
+ * title) — re-lowercasing it inside the function copied the whole transcript
860
+ * on every call, hundreds of MB of transient allocation in the Stop hook.
861
+ * The caller lowercases once; only the needle is normalized here.
759
862
  */
760
863
  export function isRecallHit(sessionText, name) {
761
864
  if (!name || name.length < 4) return false;
762
- return String(sessionText ?? '').toLowerCase().includes(String(name).toLowerCase());
865
+ return String(sessionText ?? '').includes(String(name).toLowerCase());
763
866
  }
764
867
 
765
868
  /**
@@ -81,7 +81,7 @@ export function buildHint() {
81
81
  '1. Decide WHAT to remember from the conversation context. Be specific — pick observations',
82
82
  ' that will be useful in *future* sessions, not session-local state.',
83
83
  '',
84
- '2. Decide the SCOPE (this drives namespace + tags):',
84
+ '2. Decide the SCOPE for a NEW memory (this drives namespace + tags):',
85
85
  ' • Machine-level / cross-project / preferences → memesh namespace=personal',
86
86
  ' • Project-internal decision / pattern / lesson → memesh + project tag (e.g. tag:project:memesh)',
87
87
  ' • Universal / public best practice → memesh namespace=global (rare)',
@@ -91,7 +91,8 @@ export function buildHint() {
91
91
  ' • type: one of (decision, pattern, lesson_learned, bug, process, preference, etc.)',
92
92
  ' • observations: array of specific facts / steps / rationale',
93
93
  ' • tags: relevant tags (programming language, framework, domain)',
94
- ' • namespace: personal (default) | team | global',
94
+ ' • namespace: personal | team | global — OMIT for a memory that already exists.',
95
+ ' Supplying it MOVES that memory out of the scope it is in.',
95
96
  '',
96
97
  '4. Confirm to the user with: entity name + memesh ID returned by the tool.',
97
98
  '</memesh-remember-intent>',