@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.
- package/.claude-plugin/marketplace.json +5 -3
- package/.claude-plugin/plugin.json +6 -4
- package/.mcp.json +1 -1
- package/AGENTS.md +95 -0
- package/README.de.md +130 -39
- package/README.md +188 -43
- package/README.zh-TW.md +133 -41
- package/dashboard/dist/index.html +9 -9
- package/dist/cli/view-live.js +3 -3
- package/dist/core/analytics.d.ts +3 -3
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/auto-tagger.d.ts.map +1 -1
- package/dist/core/auto-tagger.js +4 -9
- package/dist/core/auto-tagger.js.map +1 -1
- package/dist/core/briefing.d.ts +8 -0
- package/dist/core/briefing.d.ts.map +1 -0
- package/dist/core/briefing.js +91 -0
- package/dist/core/briefing.js.map +1 -0
- package/dist/core/capture-flag.d.ts +5 -0
- package/dist/core/capture-flag.d.ts.map +1 -0
- package/dist/core/capture-flag.js +10 -0
- package/dist/core/capture-flag.js.map +1 -0
- package/dist/core/config.d.ts +0 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +2 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/conflict-candidates.d.ts +20 -0
- package/dist/core/conflict-candidates.d.ts.map +1 -0
- package/dist/core/conflict-candidates.js +79 -0
- package/dist/core/conflict-candidates.js.map +1 -0
- package/dist/core/conflict-judge.d.ts +47 -0
- package/dist/core/conflict-judge.d.ts.map +1 -0
- package/dist/core/conflict-judge.js +189 -0
- package/dist/core/conflict-judge.js.map +1 -0
- package/dist/core/demo.d.ts +2 -2
- package/dist/core/demo.d.ts.map +1 -1
- package/dist/core/demo.js.map +1 -1
- package/dist/core/digest-validator.d.ts.map +1 -1
- package/dist/core/digest-validator.js +3 -5
- package/dist/core/digest-validator.js.map +1 -1
- package/dist/core/doctor.d.ts +3 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +207 -85
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +20 -9
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +416 -58
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/embedder.d.ts +7 -5
- package/dist/core/embedder.d.ts.map +1 -1
- package/dist/core/embedder.js +38 -9
- package/dist/core/embedder.js.map +1 -1
- package/dist/core/extractor.d.ts.map +1 -1
- package/dist/core/extractor.js +2 -1
- package/dist/core/extractor.js.map +1 -1
- package/dist/core/failure-analyzer.d.ts.map +1 -1
- package/dist/core/failure-analyzer.js +7 -12
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/graph.d.ts +4 -4
- package/dist/core/graph.d.ts.map +1 -1
- package/dist/core/graph.js.map +1 -1
- package/dist/core/install-channel.d.ts +1 -1
- package/dist/core/install-channel.d.ts.map +1 -1
- package/dist/core/install-channel.js +16 -5
- package/dist/core/install-channel.js.map +1 -1
- package/dist/core/install-hooks.d.ts +6 -0
- package/dist/core/install-hooks.d.ts.map +1 -1
- package/dist/core/install-hooks.js +0 -0
- package/dist/core/install-hooks.js.map +1 -1
- package/dist/core/json-utils.d.ts +1 -0
- package/dist/core/json-utils.d.ts.map +1 -1
- package/dist/core/json-utils.js +19 -10
- package/dist/core/json-utils.js.map +1 -1
- package/dist/core/kg-backfill.d.ts +3 -4
- package/dist/core/kg-backfill.d.ts.map +1 -1
- package/dist/core/kg-backfill.js +1 -4
- package/dist/core/kg-backfill.js.map +1 -1
- package/dist/core/lesson-engine.d.ts +1 -0
- package/dist/core/lesson-engine.d.ts.map +1 -1
- package/dist/core/lesson-engine.js +1 -0
- package/dist/core/lesson-engine.js.map +1 -1
- package/dist/core/lifecycle.d.ts +4 -4
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +15 -22
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/llm-client.d.ts.map +1 -1
- package/dist/core/llm-client.js +3 -6
- package/dist/core/llm-client.js.map +1 -1
- package/dist/core/llm-telemetry.d.ts +4 -4
- package/dist/core/llm-telemetry.d.ts.map +1 -1
- package/dist/core/llm-telemetry.js +1 -1
- package/dist/core/llm-telemetry.js.map +1 -1
- package/dist/core/memory-tool.d.ts.map +1 -1
- package/dist/core/memory-tool.js +8 -4
- package/dist/core/memory-tool.js.map +1 -1
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +41 -17
- package/dist/core/operations.js.map +1 -1
- package/dist/core/paths.d.ts +3 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +67 -1
- package/dist/core/paths.js.map +1 -1
- package/dist/core/patterns.d.ts +2 -2
- package/dist/core/patterns.d.ts.map +1 -1
- package/dist/core/patterns.js.map +1 -1
- package/dist/core/project-tags.d.ts +3 -3
- package/dist/core/project-tags.d.ts.map +1 -1
- package/dist/core/project-tags.js.map +1 -1
- package/dist/core/projects.d.ts +2 -2
- package/dist/core/projects.d.ts.map +1 -1
- package/dist/core/projects.js.map +1 -1
- package/dist/core/prompt-safety.d.ts +1 -0
- package/dist/core/prompt-safety.d.ts.map +1 -1
- package/dist/core/prompt-safety.js +7 -0
- package/dist/core/prompt-safety.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +27 -30
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +45 -4
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/setup.d.ts +29 -0
- package/dist/core/setup.d.ts.map +1 -0
- package/dist/core/setup.js +127 -0
- package/dist/core/setup.js.map +1 -0
- package/dist/core/stats.d.ts +2 -2
- package/dist/core/stats.d.ts.map +1 -1
- package/dist/core/stats.js.map +1 -1
- package/dist/core/task-state-store.d.ts +17 -0
- package/dist/core/task-state-store.d.ts.map +1 -0
- package/dist/core/task-state-store.js +45 -0
- package/dist/core/task-state-store.js.map +1 -0
- package/dist/core/task-state.d.ts +19 -0
- package/dist/core/task-state.d.ts.map +1 -0
- package/dist/core/task-state.js +91 -0
- package/dist/core/task-state.js.map +1 -0
- package/dist/core/time-utils.d.ts +2 -0
- package/dist/core/time-utils.d.ts.map +1 -0
- package/dist/core/time-utils.js +14 -0
- package/dist/core/time-utils.js.map +1 -0
- package/dist/core/title.d.ts +5 -0
- package/dist/core/title.d.ts.map +1 -0
- package/dist/core/title.js +14 -0
- package/dist/core/title.js.map +1 -0
- package/dist/core/transcript-extractor.d.ts +5 -6
- package/dist/core/transcript-extractor.d.ts.map +1 -1
- package/dist/core/transcript-extractor.js +4 -24
- package/dist/core/transcript-extractor.js.map +1 -1
- package/dist/core/transcript-source.d.ts.map +1 -1
- package/dist/core/transcript-source.js +2 -3
- package/dist/core/transcript-source.js.map +1 -1
- package/dist/core/types.d.ts +21 -7
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/work-topology.d.ts +33 -0
- package/dist/core/work-topology.d.ts.map +1 -0
- package/dist/core/work-topology.js +183 -0
- package/dist/core/work-topology.js.map +1 -0
- package/dist/db.d.ts +5 -10
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +194 -196
- package/dist/db.js.map +1 -1
- package/dist/knowledge-graph.d.ts +4 -2
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +68 -49
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/mcp/server.js +2 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/skills-manifest.json +61 -36
- package/dist/storage/conflicts.d.ts +3 -3
- package/dist/storage/conflicts.d.ts.map +1 -1
- package/dist/storage/conflicts.js +2 -7
- package/dist/storage/conflicts.js.map +1 -1
- package/dist/storage/fts-index.d.ts +6 -4
- package/dist/storage/fts-index.d.ts.map +1 -1
- package/dist/storage/fts-index.js +16 -4
- package/dist/storage/fts-index.js.map +1 -1
- package/dist/storage/schema.d.ts +20 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +274 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/sqlite.d.ts +20 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +64 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/storage/vector-index.d.ts +3 -0
- package/dist/storage/vector-index.d.ts.map +1 -0
- package/dist/storage/vector-index.js +7 -0
- package/dist/storage/vector-index.js.map +1 -0
- package/dist/transports/cli/cli.d.ts +1 -4
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +494 -76
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/retired-routes.d.ts.map +1 -1
- package/dist/transports/http/retired-routes.js +1 -0
- package/dist/transports/http/retired-routes.js.map +1 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +243 -323
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +48 -94
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +77 -56
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +31 -40
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +31 -37
- package/dist/transports/schemas.js.map +1 -1
- package/hooks/hooks.json +0 -10
- package/llms-install.md +138 -0
- package/package.json +19 -18
- package/scripts/hooks/_generated/capture-flag.js +17 -0
- package/scripts/hooks/_generated/core-paths.js +67 -1
- package/scripts/hooks/_generated/fts-index.js +16 -4
- package/scripts/hooks/_generated/schema.js +281 -0
- package/scripts/hooks/_generated/sqlite.js +71 -0
- package/scripts/hooks/_generated/task-state.js +98 -0
- package/scripts/hooks/_generated/time-utils.js +21 -0
- package/scripts/hooks/_generated/title.js +21 -0
- package/scripts/hooks/_generated/work-topology.js +190 -0
- package/scripts/hooks/_shared.js +269 -534
- package/scripts/hooks/post-commit.js +55 -10
- package/scripts/hooks/pre-compact.js +22 -7
- package/scripts/hooks/pre-edit-recall.js +9 -11
- package/scripts/hooks/session-start.js +230 -106
- package/scripts/hooks/session-summary.js +176 -73
- package/scripts/hooks/user-prompt-intent.js +3 -2
- package/skills/memesh/SKILL.md +97 -77
- package/README.es.md +0 -470
- package/README.fr.md +0 -462
- package/README.ja.md +0 -470
- package/README.ko.md +0 -470
- package/README.pt.md +0 -462
- package/README.th.md +0 -463
- package/README.vi.md +0 -462
- package/README.zh-CN.md +0 -469
- package/dist/cli/view.d.ts +0 -3
- package/dist/cli/view.d.ts.map +0 -1
- package/dist/cli/view.js +0 -523
- package/dist/cli/view.js.map +0 -1
- package/dist/core/skill-usage-log.d.ts +0 -11
- package/dist/core/skill-usage-log.d.ts.map +0 -1
- package/dist/core/skill-usage-log.js +0 -125
- package/dist/core/skill-usage-log.js.map +0 -1
- package/dist/core/verifier.d.ts +0 -40
- package/dist/core/verifier.d.ts.map +0 -1
- package/dist/core/verifier.js +0 -206
- package/dist/core/verifier.js.map +0 -1
- package/dist/mcp/launcher.d.ts +0 -3
- package/dist/mcp/launcher.d.ts.map +0 -1
- package/dist/mcp/launcher.js +0 -37
- package/dist/mcp/launcher.js.map +0 -1
- package/scripts/hooks/pre-bash-orchestration-nudge.js +0 -155
- package/scripts/postinstall-rebuild.mjs +0 -41
- 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
|
-
|
|
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
|
|
137
|
-
//
|
|
138
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
//
|
|
252
|
-
// (
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
let
|
|
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)
|
|
334
|
+
if (alreadyCaptured) {
|
|
335
|
+
recordHookRun(db, 'session-summary');
|
|
336
|
+
return exit0();
|
|
337
|
+
}
|
|
275
338
|
|
|
276
339
|
// Build and store session memories
|
|
277
|
-
const baseTags = [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
|
|
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
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
const dir =
|
|
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:
|
|
614
|
-
MEMESH_DIR:
|
|
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.
|
|
647
|
-
*
|
|
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
|
-
//
|
|
654
|
-
//
|
|
655
|
-
//
|
|
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
|
-
*
|
|
758
|
-
*
|
|
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 ?? '').
|
|
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
|
|
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>',
|