@pcircle/memesh 4.4.0 → 4.5.1

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 (198) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.mcp.json +1 -1
  4. package/README.de.md +32 -12
  5. package/README.es.md +13 -12
  6. package/README.fr.md +31 -11
  7. package/README.ja.md +13 -12
  8. package/README.ko.md +14 -13
  9. package/README.md +41 -16
  10. package/README.pt.md +30 -10
  11. package/README.th.md +16 -9
  12. package/README.vi.md +32 -12
  13. package/README.zh-CN.md +14 -13
  14. package/README.zh-TW.md +14 -13
  15. package/dashboard/dist/index.html +10 -10
  16. package/dist/cli/view-live.js +1 -1
  17. package/dist/cli/view.d.ts.map +1 -1
  18. package/dist/cli/view.js +2 -2
  19. package/dist/cli/view.js.map +1 -1
  20. package/dist/core/analytics.d.ts +7 -3
  21. package/dist/core/analytics.d.ts.map +1 -1
  22. package/dist/core/analytics.js +8 -8
  23. package/dist/core/analytics.js.map +1 -1
  24. package/dist/core/auto-tagger.d.ts.map +1 -1
  25. package/dist/core/auto-tagger.js.map +1 -1
  26. package/dist/core/config.d.ts +5 -3
  27. package/dist/core/config.d.ts.map +1 -1
  28. package/dist/core/config.js +30 -14
  29. package/dist/core/config.js.map +1 -1
  30. package/dist/core/demo.d.ts +2 -2
  31. package/dist/core/demo.d.ts.map +1 -1
  32. package/dist/core/demo.js.map +1 -1
  33. package/dist/core/digest-validator.d.ts.map +1 -1
  34. package/dist/core/digest-validator.js +3 -1
  35. package/dist/core/digest-validator.js.map +1 -1
  36. package/dist/core/doctor.d.ts +3 -0
  37. package/dist/core/doctor.d.ts.map +1 -1
  38. package/dist/core/doctor.js +255 -101
  39. package/dist/core/doctor.js.map +1 -1
  40. package/dist/core/dreamer.d.ts +35 -7
  41. package/dist/core/dreamer.d.ts.map +1 -1
  42. package/dist/core/dreamer.js +394 -41
  43. package/dist/core/dreamer.js.map +1 -1
  44. package/dist/core/embedder.d.ts +3 -5
  45. package/dist/core/embedder.d.ts.map +1 -1
  46. package/dist/core/embedder.js +26 -95
  47. package/dist/core/embedder.js.map +1 -1
  48. package/dist/core/extractor.d.ts.map +1 -1
  49. package/dist/core/extractor.js +2 -1
  50. package/dist/core/extractor.js.map +1 -1
  51. package/dist/core/failure-analyzer.d.ts.map +1 -1
  52. package/dist/core/failure-analyzer.js +2 -1
  53. package/dist/core/failure-analyzer.js.map +1 -1
  54. package/dist/core/graph.d.ts +4 -4
  55. package/dist/core/graph.d.ts.map +1 -1
  56. package/dist/core/graph.js.map +1 -1
  57. package/dist/core/install-hooks.d.ts +1 -0
  58. package/dist/core/install-hooks.d.ts.map +1 -1
  59. package/dist/core/install-hooks.js +0 -0
  60. package/dist/core/install-hooks.js.map +1 -1
  61. package/dist/core/kg-backfill.d.ts +4 -4
  62. package/dist/core/kg-backfill.d.ts.map +1 -1
  63. package/dist/core/kg-backfill.js +1 -1
  64. package/dist/core/kg-backfill.js.map +1 -1
  65. package/dist/core/lesson-engine.d.ts +1 -0
  66. package/dist/core/lesson-engine.d.ts.map +1 -1
  67. package/dist/core/lesson-engine.js +1 -0
  68. package/dist/core/lesson-engine.js.map +1 -1
  69. package/dist/core/lifecycle.d.ts +4 -4
  70. package/dist/core/lifecycle.d.ts.map +1 -1
  71. package/dist/core/lifecycle.js +1 -1
  72. package/dist/core/lifecycle.js.map +1 -1
  73. package/dist/core/llm-client.d.ts.map +1 -1
  74. package/dist/core/llm-client.js +3 -6
  75. package/dist/core/llm-client.js.map +1 -1
  76. package/dist/core/llm-telemetry.d.ts +4 -4
  77. package/dist/core/llm-telemetry.d.ts.map +1 -1
  78. package/dist/core/llm-telemetry.js +1 -1
  79. package/dist/core/llm-telemetry.js.map +1 -1
  80. package/dist/core/llm-validator.d.ts +1 -0
  81. package/dist/core/llm-validator.d.ts.map +1 -1
  82. package/dist/core/llm-validator.js +33 -10
  83. package/dist/core/llm-validator.js.map +1 -1
  84. package/dist/core/memory-tool.d.ts.map +1 -1
  85. package/dist/core/memory-tool.js +4 -0
  86. package/dist/core/memory-tool.js.map +1 -1
  87. package/dist/core/operations.d.ts.map +1 -1
  88. package/dist/core/operations.js +23 -8
  89. package/dist/core/operations.js.map +1 -1
  90. package/dist/core/output-language.d.ts +6 -0
  91. package/dist/core/output-language.d.ts.map +1 -0
  92. package/dist/core/output-language.js +25 -0
  93. package/dist/core/output-language.js.map +1 -0
  94. package/dist/core/paths.d.ts +3 -0
  95. package/dist/core/paths.d.ts.map +1 -1
  96. package/dist/core/paths.js +67 -1
  97. package/dist/core/paths.js.map +1 -1
  98. package/dist/core/patterns.d.ts +2 -3
  99. package/dist/core/patterns.d.ts.map +1 -1
  100. package/dist/core/patterns.js +1 -5
  101. package/dist/core/patterns.js.map +1 -1
  102. package/dist/core/project-tags.d.ts +3 -3
  103. package/dist/core/project-tags.d.ts.map +1 -1
  104. package/dist/core/project-tags.js.map +1 -1
  105. package/dist/core/projects.d.ts +2 -2
  106. package/dist/core/projects.d.ts.map +1 -1
  107. package/dist/core/projects.js.map +1 -1
  108. package/dist/core/schema-export.d.ts.map +1 -1
  109. package/dist/core/schema-export.js +0 -34
  110. package/dist/core/schema-export.js.map +1 -1
  111. package/dist/core/serializer.d.ts.map +1 -1
  112. package/dist/core/serializer.js +45 -4
  113. package/dist/core/serializer.js.map +1 -1
  114. package/dist/core/stats.d.ts +2 -2
  115. package/dist/core/stats.d.ts.map +1 -1
  116. package/dist/core/stats.js.map +1 -1
  117. package/dist/core/transcript-extractor.d.ts +88 -0
  118. package/dist/core/transcript-extractor.d.ts.map +1 -0
  119. package/dist/core/transcript-extractor.js +417 -0
  120. package/dist/core/transcript-extractor.js.map +1 -0
  121. package/dist/core/transcript-source.d.ts +21 -0
  122. package/dist/core/transcript-source.d.ts.map +1 -0
  123. package/dist/core/transcript-source.js +142 -0
  124. package/dist/core/transcript-source.js.map +1 -0
  125. package/dist/core/types.d.ts +17 -7
  126. package/dist/core/types.d.ts.map +1 -1
  127. package/dist/core/types.js +2 -0
  128. package/dist/core/types.js.map +1 -1
  129. package/dist/db.d.ts +5 -5
  130. package/dist/db.d.ts.map +1 -1
  131. package/dist/db.js +165 -17
  132. package/dist/db.js.map +1 -1
  133. package/dist/knowledge-graph.d.ts +3 -2
  134. package/dist/knowledge-graph.d.ts.map +1 -1
  135. package/dist/knowledge-graph.js +19 -10
  136. package/dist/knowledge-graph.js.map +1 -1
  137. package/dist/mcp/server.js +2 -1
  138. package/dist/mcp/server.js.map +1 -1
  139. package/dist/skills-manifest.json +28 -33
  140. package/dist/storage/conflicts.d.ts +3 -3
  141. package/dist/storage/conflicts.d.ts.map +1 -1
  142. package/dist/storage/conflicts.js.map +1 -1
  143. package/dist/storage/fts-index.d.ts +4 -4
  144. package/dist/storage/fts-index.d.ts.map +1 -1
  145. package/dist/storage/fts-index.js.map +1 -1
  146. package/dist/storage/sqlite.d.ts +20 -0
  147. package/dist/storage/sqlite.d.ts.map +1 -0
  148. package/dist/storage/sqlite.js +64 -0
  149. package/dist/storage/sqlite.js.map +1 -0
  150. package/dist/storage/vector-index.d.ts +3 -0
  151. package/dist/storage/vector-index.d.ts.map +1 -0
  152. package/dist/storage/vector-index.js +7 -0
  153. package/dist/storage/vector-index.js.map +1 -0
  154. package/dist/transports/cli/cli.d.ts.map +1 -1
  155. package/dist/transports/cli/cli.js +278 -91
  156. package/dist/transports/cli/cli.js.map +1 -1
  157. package/dist/transports/http/retired-routes.d.ts.map +1 -1
  158. package/dist/transports/http/retired-routes.js +1 -0
  159. package/dist/transports/http/retired-routes.js.map +1 -1
  160. package/dist/transports/http/server.d.ts +7 -0
  161. package/dist/transports/http/server.d.ts.map +1 -1
  162. package/dist/transports/http/server.js +165 -79
  163. package/dist/transports/http/server.js.map +1 -1
  164. package/dist/transports/mcp/handlers.d.ts +4 -96
  165. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  166. package/dist/transports/mcp/handlers.js +26 -59
  167. package/dist/transports/mcp/handlers.js.map +1 -1
  168. package/dist/transports/schemas.d.ts +10 -30
  169. package/dist/transports/schemas.d.ts.map +1 -1
  170. package/dist/transports/schemas.js +5 -29
  171. package/dist/transports/schemas.js.map +1 -1
  172. package/hooks/hooks.json +0 -10
  173. package/package.json +6 -19
  174. package/scripts/hooks/_generated/core-paths.js +67 -1
  175. package/scripts/hooks/_generated/sqlite.js +71 -0
  176. package/scripts/hooks/_shared.js +286 -195
  177. package/scripts/hooks/post-commit.js +52 -10
  178. package/scripts/hooks/pre-compact.js +10 -7
  179. package/scripts/hooks/pre-edit-recall.js +4 -8
  180. package/scripts/hooks/session-start.js +95 -47
  181. package/scripts/hooks/session-summary.js +120 -52
  182. package/scripts/hooks/user-prompt-intent.js +3 -2
  183. package/skills/memesh/SKILL.md +3 -2
  184. package/dist/core/skill-usage-log.d.ts +0 -11
  185. package/dist/core/skill-usage-log.d.ts.map +0 -1
  186. package/dist/core/skill-usage-log.js +0 -125
  187. package/dist/core/skill-usage-log.js.map +0 -1
  188. package/dist/core/verifier.d.ts +0 -40
  189. package/dist/core/verifier.d.ts.map +0 -1
  190. package/dist/core/verifier.js +0 -206
  191. package/dist/core/verifier.js.map +0 -1
  192. package/dist/mcp/launcher.d.ts +0 -3
  193. package/dist/mcp/launcher.d.ts.map +0 -1
  194. package/dist/mcp/launcher.js +0 -37
  195. package/dist/mcp/launcher.js.map +0 -1
  196. package/scripts/hooks/pre-bash-orchestration-nudge.js +0 -155
  197. package/scripts/postinstall-rebuild.mjs +0 -41
  198. package/skills/agentic-orchestration/SKILL.md +0 -399
@@ -1,13 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execFileSync } from 'child_process';
4
- import { captureEntity, getProjectName, openHookDb } from './_shared.js';
4
+ import { AUTO_CAPTURE_TAG, captureEntity, getProjectName, isAutoCaptureEnabled, openHookDb, recordHookRun } from './_shared.js';
5
5
 
6
6
  let input = '';
7
7
  process.stdin.setEncoding('utf8');
8
8
  process.stdin.on('data', (chunk) => { input += chunk; });
9
9
  process.stdin.on('end', () => {
10
10
  try {
11
+ // Opt-out check (env > config > default-on). This hook skipped it for
12
+ // years while its two siblings honoured it — with capture disabled it
13
+ // kept writing commit entities AND stamping the heartbeat, which made
14
+ // doctor's "capture is off, hook silence is expected" message false.
15
+ if (!isAutoCaptureEnabled(process.env)) return exit0();
16
+
11
17
  const data = JSON.parse(input);
12
18
 
13
19
  // tool_name absent is a schema-flip signal (Claude Code has done
@@ -50,6 +56,20 @@ process.stdin.on('end', () => {
50
56
  const commitMatch = toolOutput.match(/\[[\w/.-]+(?: \([\w -]+\))? ([a-f0-9]{7,})\] (.+)/);
51
57
  if (!commitMatch) return exit0();
52
58
 
59
+ // The OUTPUT looking like a commit is not evidence that a commit happened.
60
+ // This hook stopped at the regex above, so any Bash output containing a
61
+ // commit-shaped line produced a permanent memory. Measured: a payload whose
62
+ // command was `cat docs/release-notes.md` wrote entity `commit-9f3c2a1`
63
+ // for a hash `git cat-file -t` rejects as "Not a valid object name".
64
+ // Reading a changelog, tailing a build log, or quoting a commit line was
65
+ // enough — and the fake then surfaced through session-start and
66
+ // pre-edit-recall as if it had happened.
67
+ const issuedCommand = typeof data.tool_input?.command === 'string' ? data.tool_input.command : '';
68
+ if (!/\bgit\b[^|;&]*\bcommit\b/.test(issuedCommand)) {
69
+ try { process.stderr.write(`[memesh post-commit] output looks like a commit but the command was not a git commit; skipping ${commitMatch[1]}\n`); } catch {}
70
+ return exit0();
71
+ }
72
+
53
73
  const branchMatch = commitMatch[0].match(/^\[([^\s]+)\s/);
54
74
  const branch = branchMatch ? branchMatch[1] : 'unknown';
55
75
 
@@ -66,17 +86,28 @@ process.stdin.on('end', () => {
66
86
  try { process.stderr.write(`[memesh post-commit] data.cwd absent — cannot resolve project / repo; skipping commit ${commitHash}\n`); } catch {}
67
87
  return exit0();
68
88
  }
89
+ // And the commit has to actually be in THIS repository.
90
+ //
91
+ // `cat-file -e <hash>^{commit}` answers exactly one question — does this
92
+ // resolve to a commit object here — in milliseconds, before anything is
93
+ // written. Deliberately separate from the `git show` below, whose failure
94
+ // (git absent, timeout on a huge diff) says nothing about the commit and
95
+ // must NOT veto.
96
+ try {
97
+ execFileSync('git', ['-C', data.cwd, 'cat-file', '-e', `${commitHash}^{commit}`], {
98
+ timeout: 5000,
99
+ stdio: ['ignore', 'ignore', 'pipe'],
100
+ });
101
+ } catch {
102
+ try { process.stderr.write(`[memesh post-commit] ${commitHash} is not a commit in ${data.cwd}; nothing written\n`); } catch {}
103
+ return exit0();
104
+ }
105
+
69
106
  const projectName = getProjectName(data.cwd);
70
107
 
71
108
  // Open DB via shared helper — applies SCHEMA_SQL + status migration.
72
109
  // Pass fts:true so the FTS5 entity-search index is also available.
73
- // Returns null when the better-sqlite3 native module is unavailable
74
- // (e.g. plugin-marketplace cache install with no node_modules); in
75
- // that case silently skip — a sibling registered hook copy with
76
- // proper deps still records the commit.
77
- const handle = openHookDb(process.env, { fts: true });
78
- if (!handle) return;
79
- const { db } = handle;
110
+ const { db } = openHookDb(process.env, { fts: true });
80
111
  try {
81
112
  const entityName = `commit-${commitHash}`;
82
113
 
@@ -103,12 +134,23 @@ process.stdin.on('end', () => {
103
134
  }
104
135
 
105
136
  // Shared write dance — upsert entity + observations + tags AND reindex FTS.
106
- captureEntity(db, {
137
+ //
138
+ // `source:auto-capture` is the provenance marker every capture hook
139
+ // writes (session-summary, pre-compact and the extractor already did;
140
+ // this one did not). `memesh doctor` counts it to answer "is the
141
+ // auto-capture loop alive" — a question it used to answer from entity
142
+ // TYPE, which a hand-typed `memesh learn` satisfied all by itself.
143
+ const written = captureEntity(db, {
107
144
  name: entityName,
108
145
  type: 'commit',
109
146
  observations,
110
- tags: [`project:${projectName}`],
147
+ tags: [AUTO_CAPTURE_TAG, `project:${projectName}`],
111
148
  });
149
+
150
+ // Heartbeat AFTER capture, so the stamp certifies "the capture loop
151
+ // completed", not "a database handle existed". A throw above skips it,
152
+ // and so does a null return (the write did not land).
153
+ if (written) recordHookRun(db, 'post-commit');
112
154
  } finally {
113
155
  db.close();
114
156
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { basename } from 'path';
4
4
  import { existsSync, readFileSync } from 'fs';
5
- import { captureEntity, getProjectName, isAutoCaptureEnabled, openHookDb } from './_shared.js';
5
+ import { AUTO_CAPTURE_TAG, captureEntity, getProjectName, isAutoCaptureEnabled, openHookDb, recordHookRun } from './_shared.js';
6
6
 
7
7
  // Timeout guard: always exit within 10 seconds
8
8
  const TIMEOUT_MS = 10000;
@@ -101,11 +101,7 @@ process.stdin.on('end', () => {
101
101
 
102
102
  // Open DB via shared helper — applies SCHEMA_SQL + status migration.
103
103
  // FTS5 needed for the entity-search index updates below.
104
- // Returns null on plugin-marketplace cache installs without node_modules;
105
- // silently skip in that case (sibling registered copy handles it).
106
- const handle = openHookDb(process.env, { fts: true });
107
- if (!handle) return;
108
- const { db } = handle;
104
+ const { db } = openHookDb(process.env, { fts: true });
109
105
  let written = null;
110
106
  try {
111
107
  // Shared write dance — upsert entity + observations + tags AND reindex FTS
@@ -114,8 +110,15 @@ process.stdin.on('end', () => {
114
110
  name: entityName,
115
111
  type: 'session-summary',
116
112
  observations: obsLines,
117
- tags: ['source:auto-capture', 'urgency:pre-compact', `project:${projectName}`],
113
+ tags: [AUTO_CAPTURE_TAG, 'urgency:pre-compact', `project:${projectName}`],
118
114
  });
115
+
116
+ // Heartbeat AFTER capture, so the stamp certifies "the capture loop
117
+ // completed", not "a database handle existed". A throw above skips it,
118
+ // and so does a null return — captureEntity's null means the write did
119
+ // not land, and this very hook tells the user "could not save" below;
120
+ // stamping would say "alive" to doctor about the same failed run.
121
+ if (written) recordHookRun(db, 'pre-compact');
119
122
  } finally {
120
123
  db.close();
121
124
  }
@@ -13,10 +13,10 @@ import {
13
13
  getMemeshDirFromDbPath,
14
14
  getProjectName,
15
15
  isTrustedForAutoContext,
16
- tryRequireBetterSqlite,
17
16
  writePrivateJson,
18
17
  hookMatchExpression,
19
18
  } from './_shared.js';
19
+ import { MemeshDatabase } from './_generated/sqlite.js';
20
20
 
21
21
  const dbPath = getDbPath();
22
22
  const memeshDir = getMemeshDirFromDbPath();
@@ -64,13 +64,9 @@ process.stdin.on('end', () => {
64
64
 
65
65
  if (!existsSync(dbPath)) return pass();
66
66
 
67
- // tryRequireBetterSqlite() returns null on plugin-marketplace cache
68
- // installs that ship without node_modules; pass-through silently in
69
- // that case so a sibling registered hook copy can still inject
70
- // recall context.
71
- const Database = tryRequireBetterSqlite();
72
- if (!Database) return pass();
73
- const db = new Database(dbPath, { readonly: true });
67
+ // `readOnly`, not `readonly`: node:sqlite ignores the lowercase spelling
68
+ // and hands back a WRITABLE handle. This hook only reads.
69
+ const db = new MemeshDatabase(dbPath, { readOnly: true });
74
70
  try {
75
71
 
76
72
  // Check if entities table exists
@@ -5,7 +5,7 @@ import { spawn } from 'child_process';
5
5
  import { homedir } from 'os';
6
6
  import { join } from 'path';
7
7
  import { pathToFileURL } from 'url';
8
- import { existsSync, readFileSync, unlinkSync, rmSync, appendFileSync, chmodSync } from 'fs';
8
+ import { existsSync, readFileSync, unlinkSync, mkdirSync, accessSync, constants as fsConstants } from 'fs';
9
9
  import {
10
10
  buildReferenceContext,
11
11
  ensurePrivateDir,
@@ -13,14 +13,13 @@ import {
13
13
  getMemeshDirFromDbPath,
14
14
  getProjectName,
15
15
  importFromPluginRoot,
16
- isAgenticOrchestrationEnabled,
17
16
  isTrustedForAutoContext,
18
17
  readUpdateCheckCache,
19
18
  resolvePluginRoot,
20
19
  resolveSessionLimit,
21
- tryRequireBetterSqlite,
22
20
  writePrivateJson,
23
21
  } from './_shared.js';
22
+ import { MemeshDatabase } from './_generated/sqlite.js';
24
23
 
25
24
  const require = createRequire(import.meta.url);
26
25
 
@@ -44,7 +43,6 @@ try {
44
43
  const dbPath = getDbPath();
45
44
  const memeshDir = getMemeshDirFromDbPath();
46
45
  const throttlePath = join(memeshDir, 'session-recalled-files.json');
47
- const nudgeFlagsDir = join(memeshDir, 'agent-nudge-flags');
48
46
 
49
47
  /**
50
48
  * Build the strong deprecation warning lines to prepend to the
@@ -430,6 +428,48 @@ function runPostBannerUpdateTasks() {
430
428
  }
431
429
  }
432
430
 
431
+ /**
432
+ * Can the capture hooks actually write? Returns the offending path, or null.
433
+ *
434
+ * Probes the two things a capture hook needs and nothing else: the memesh
435
+ * directory has to exist and be writable, and where the database file already
436
+ * exists, that file has to be writable too. The second half matters on its
437
+ * own — a writable directory holding a read-only database is a state the old
438
+ * mkdir-only probe called healthy, and it is exactly what a botched `sudo`
439
+ * leaves behind.
440
+ *
441
+ * `accessSync(W_OK)` rather than opening a handle: this is the SessionStart
442
+ * hot path, and a read-write open would run the whole migration chain here
443
+ * just to answer a permissions question. It is one syscall and it fails in
444
+ * the same cases EACCES would.
445
+ *
446
+ * Deliberately not detected: a full disk. No cheap probe finds it, and
447
+ * claiming otherwise would be worse than the honest gap.
448
+ */
449
+ function captureTargetUnwritable() {
450
+ try {
451
+ mkdirSync(memeshDir, { recursive: true });
452
+ accessSync(memeshDir, fsConstants.W_OK);
453
+ } catch {
454
+ return memeshDir;
455
+ }
456
+ // The WAL/SHM sidecars are probed too: an interrupted `sudo` run leaves a
457
+ // user-owned database next to root-owned `-wal`/`-shm` files, and SQLite
458
+ // then fails every write with EACCES while the db file itself probes
459
+ // writable — the most common botched-sudo residue, and exactly the state
460
+ // the db-file probe alone called healthy.
461
+ for (const suffix of ['', '-wal', '-shm']) {
462
+ const p = `${dbPath}${suffix}`;
463
+ if (!existsSync(p)) continue;
464
+ try {
465
+ accessSync(p, fsConstants.W_OK);
466
+ } catch {
467
+ return p;
468
+ }
469
+ }
470
+ return null;
471
+ }
472
+
433
473
  /**
434
474
  * Build a "base message + optional deprecation banner" combined
435
475
  * single-line systemMessage payload. Keeps stdout a single JSON
@@ -466,6 +506,14 @@ let input = '';
466
506
  process.stdin.setEncoding('utf8');
467
507
  process.stdin.on('data', (chunk) => { input += chunk; });
468
508
  process.stdin.on('end', async () => {
509
+ // Hoisted above both try blocks: the recall-failure catch below must be
510
+ // able to lead with this warning too, or a capture-dead session that ALSO
511
+ // hits a recall error silently drops the more important half of the story.
512
+ let captureWarning = null;
513
+ const withCaptureWarning = (msg) => {
514
+ if (!captureWarning) return msg;
515
+ return `${captureWarning}\n${msg.replace(/^◉ MeMesh ready · /, '◉ MeMesh · ')}`;
516
+ };
469
517
  try {
470
518
  try {
471
519
  const data = JSON.parse(input);
@@ -479,38 +527,57 @@ process.stdin.on('end', async () => {
479
527
  } catch {
480
528
  // Non-critical
481
529
  }
482
- try {
483
- if (existsSync(nudgeFlagsDir)) {
484
- rmSync(nudgeFlagsDir, { recursive: true, force: true });
485
- }
486
- } catch {
487
- // Non-critical
488
- }
530
+
531
+ // Every banner below this line is a PROMISE that memories will be saved,
532
+ // so check that it can be kept before making any of them.
533
+ //
534
+ // This probe used to live INSIDE the `!existsSync(dbPath)` branch below,
535
+ // which meant it only ever ran before the database existed — while the
536
+ // failure it detects has nothing to do with first runs. A `~/.memesh` that
537
+ // became unwritable later (permissions changed, a read-only mount, a
538
+ // directory that changed owner) produced the cheerful green count banner
539
+ // on every session, forever, while every capture hook failed with EACCES.
540
+ // Fixing the first-run case and leaving the steady-state case is how a
541
+ // detector ends up covering the one day the bug is least likely to happen.
542
+ // The warning does NOT return: this hook's other job is recall, which
543
+ // opens the database read-only and works fine on an unwritable target.
544
+ // Returning here turned "capture is off" into "your memory is gone" —
545
+ // every existing memory silently withheld exactly when the user needs
546
+ // the context to notice something is wrong. Warn, then keep reading.
547
+ const unwritable = captureTargetUnwritable();
548
+ // "ready" is a promise about capture — when the warning is present,
549
+ // withCaptureWarning (hoisted above) demotes it instead of contradicting
550
+ // it one line later.
551
+ captureWarning = unwritable
552
+ ? `◉ MeMesh cannot write to ${unwritable} — memories will NOT be saved this session (recall still works). Run 'memesh doctor'.`
553
+ : null;
489
554
 
490
555
  if (!existsSync(dbPath)) {
491
556
  // Combine deprecation banner (if any) into the same
492
557
  // systemMessage so stdout stays a single JSON document. Outer
493
558
  // finally runs runPostBannerUpdateTasks().
494
- output(combineWithBanner('◉ MeMesh ready · no database yet, memories will be created as you work'));
559
+ // With no database there is nothing to recall either the warning IS
560
+ // the whole truth, and "memories will be created as you work" would
561
+ // contradict it one line later.
562
+ output(combineWithBanner(captureWarning ?? '◉ MeMesh ready · no database yet, memories will be created as you work'));
495
563
  return;
496
564
  }
497
565
 
498
- // Native module unavailable (typical for plugin-marketplace cache
499
- // installs that ship without node_modules). Silently skip the
500
- // plugin's own MCP server runs via npx and a sibling registered
501
- // copy of this hook (npm-global / dev path) supplies the summary.
502
- const Database = tryRequireBetterSqlite();
503
- if (!Database) return;
504
- const db = new Database(dbPath, { readonly: true });
566
+ // `readOnly`, not `readonly`: node:sqlite ignores the lowercase spelling
567
+ // and hands back a WRITABLE handle. This hook only reads.
568
+ //
569
+ // No `journal_mode = WAL` here. Setting it is a write, so a read-only
570
+ // connection refuses it — and it was never doing anything: the mode is a
571
+ // property of the database file that the writing side already set, and a
572
+ // reader opens a WAL database perfectly well without asking for it.
573
+ const db = new MemeshDatabase(dbPath, { readOnly: true });
505
574
  try {
506
- db.pragma('journal_mode = WAL');
507
-
508
575
  // Check if tables exist (db may exist but be empty)
509
576
  const tableCheck = db.prepare(
510
577
  "SELECT name FROM sqlite_master WHERE type='table' AND name='entities'"
511
578
  ).get();
512
579
  if (!tableCheck) {
513
- output(combineWithBanner('◉ MeMesh ready · database initialised but no memories stored yet'));
580
+ output(combineWithBanner(captureWarning ?? '◉ MeMesh ready · database initialised but no memories stored yet'));
514
581
  return;
515
582
  }
516
583
 
@@ -545,11 +612,10 @@ process.stdin.on('end', async () => {
545
612
  // Functions:
546
613
  // - frequency: log(c+1) / log(max(maxAccess,1) + 1) (matches frequencyScore)
547
614
  // - recency: exp(-(now - lastAccessed_days) / 30) (matches recencyScore)
548
- // SQLite >= 3.35 with -DSQLITE_ENABLE_MATH_FUNCTIONS provides exp/log;
549
- // better-sqlite3 v8+ ships with this flag enabled by default. We probe
550
- // once per process and fall back to the legacy linear/rational forms
551
- // if a stripped-down build is detected, so ranking degrades gracefully
552
- // rather than throwing.
615
+ // SQLite >= 3.35 with -DSQLITE_ENABLE_MATH_FUNCTIONS provides exp/log,
616
+ // which Node's bundled SQLite has. We probe once per process and fall
617
+ // back to the legacy linear/rational forms if a stripped-down build is
618
+ // detected, so ranking degrades gracefully rather than throwing.
553
619
  // Test-only seam: force the legacy linear/rational fallback so the
554
620
  // pre-math-functions code path is reachable in CI on builds where
555
621
  // exp/log ARE available. Production callers never set this.
@@ -842,24 +908,6 @@ process.stdin.on('end', async () => {
842
908
  try { process.stderr.write(`[memesh session-start] sessions-write: ${err?.message || err}\n`); } catch {}
843
909
  }
844
910
 
845
- // --- Agentic-orchestration mode (opt-in) ---
846
- // Banner kept short. Telemetry write preserved for protocol validation.
847
- if (isAgenticOrchestrationEnabled(process.env)) {
848
- summary += '\n[AO opt-in: dispatch verifiable work as background agent · skill: agentic-orchestration]';
849
- try {
850
- const usagePath = join(homedir(), '.memesh', 'skill-usage.jsonl');
851
- // Only { ts, event } — an earlier `payload: { cwd_hashed }` was never
852
- // read by summariseSkillUsage (counts by event name only), so it was
853
- // write-only privacy-adjacent data. Removed.
854
- const line = JSON.stringify({
855
- ts: new Date().toISOString(),
856
- event: 'agentic_orchestration_banner_injected',
857
- }) + '\n';
858
- appendFileSync(usagePath, line);
859
- try { chmodSync(usagePath, 0o600); } catch { /* non-POSIX */ }
860
- } catch { /* swallow — telemetry must not break session-start */ }
861
- }
862
-
863
911
  // Deprecation banner (security advisory) — surfaced even with the
864
912
  // short summary so flagged installs still warn on every session.
865
913
  let installedVersion = null;
@@ -889,7 +937,7 @@ process.stdin.on('end', async () => {
889
937
  ? [...bannerLines.filter(l => l.length > 0), '', summary].join('\n')
890
938
  : summary;
891
939
 
892
- output(finalMessage, memoryContext);
940
+ output(withCaptureWarning(finalMessage), memoryContext);
893
941
  } finally {
894
942
  db.close();
895
943
  }
@@ -921,7 +969,7 @@ process.stdin.on('end', async () => {
921
969
  // Hooks must never crash Claude Code — but report honestly.
922
970
  // Inner catch so the outer finally can still run the post-
923
971
  // banner update tasks even when the recall flow blew up.
924
- console.log(JSON.stringify({ systemMessage: `MeMesh: memories not loaded this session (${err?.message || 'unknown error'}) — everything else works; run \`memesh doctor\` if this repeats.` }));
972
+ console.log(JSON.stringify({ systemMessage: withCaptureWarning(`MeMesh: memories not loaded this session (${err?.message || 'unknown error'}) — everything else works; run \`memesh doctor\` if this repeats.`) }));
925
973
  }
926
974
  } finally {
927
975
  // ── Auto-update + cache refresh ──────────────────────────────