@pcircle/memesh 4.2.8 → 4.2.10
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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +15 -15
- package/dashboard/dist/index.html +7 -7
- package/dist/core/auto-tagger.d.ts.map +1 -1
- package/dist/core/auto-tagger.js +4 -3
- package/dist/core/auto-tagger.js.map +1 -1
- package/dist/core/consolidator.d.ts.map +1 -1
- package/dist/core/consolidator.js +4 -3
- package/dist/core/consolidator.js.map +1 -1
- package/dist/core/digest-validator.d.ts.map +1 -1
- package/dist/core/digest-validator.js +4 -3
- package/dist/core/digest-validator.js.map +1 -1
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +7 -6
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/graph.d.ts.map +1 -1
- package/dist/core/graph.js +4 -4
- package/dist/core/graph.js.map +1 -1
- package/dist/core/json-utils.d.ts +2 -0
- package/dist/core/json-utils.d.ts.map +1 -0
- package/dist/core/json-utils.js +37 -0
- package/dist/core/json-utils.js.map +1 -0
- package/dist/core/operations.d.ts +4 -0
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +6 -0
- package/dist/core/operations.js.map +1 -1
- package/dist/knowledge-graph.d.ts +1 -0
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +18 -12
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/skills-manifest.json +21 -11
- package/dist/transports/cli/cli.d.ts +4 -1
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +38 -13
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +30 -86
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +3 -9
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/package.json +2 -2
- package/scripts/hooks/_generated/core-paths.js +77 -0
- package/scripts/hooks/_generated/fts-index.js +25 -0
- package/scripts/hooks/_shared.js +90 -148
- package/scripts/hooks/post-commit.js +25 -46
- package/scripts/hooks/pre-compact.js +9 -32
- package/scripts/hooks/session-summary.js +36 -13
package/scripts/hooks/_shared.js
CHANGED
|
@@ -1,148 +1,42 @@
|
|
|
1
1
|
import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
-
import { spawn
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
|
-
import {
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
7
7
|
|
|
8
|
-
const require = createRequire(import.meta.url);
|
|
9
|
-
|
|
10
8
|
// =============================================================================
|
|
11
|
-
// Path helpers —
|
|
9
|
+
// Path helpers + FTS primitives — GENERATED from src/core (do not hand-mirror)
|
|
12
10
|
// =============================================================================
|
|
13
11
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// rules in that file MUST be reflected here too.
|
|
12
|
+
// These were once a 965-line hand-mirror of `src/core`, kept in lockstep by
|
|
13
|
+
// human review — until the copies drifted and shipped the P0 FTS bug (a hook
|
|
14
|
+
// wrote an entity+observations but the mirror's reindex step diverged, leaving
|
|
15
|
+
// the memory unrecallable).
|
|
19
16
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// handle HOME="" environments. `os.homedir()` itself reads HOME on
|
|
42
|
-
// POSIX, so HOME="" makes it return "". `os.userInfo().homedir`
|
|
43
|
-
// reads pw_dir via getpwuid syscall, bypassing env vars entirely.
|
|
44
|
-
const home = process.env.HOME;
|
|
45
|
-
if (home && home.length > 0) return home;
|
|
46
|
-
const fromOs = homedir();
|
|
47
|
-
if (fromOs && fromOs.length > 0) return fromOs;
|
|
48
|
-
// userInfo is the final defence — re-import here to keep the
|
|
49
|
-
// top-of-file `import { homedir } from 'os'` line stable.
|
|
50
|
-
return require('os').userInfo().homedir;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Resolve the memesh data directory.
|
|
55
|
-
*
|
|
56
|
-
* Precedence: MEMESH_DIR env var > <home>/.memesh.
|
|
57
|
-
* Mirror of: src/core/paths.ts → memeshDir()
|
|
58
|
-
*
|
|
59
|
-
* No-arg to mirror the core helper exactly. Earlier drafts accepted a
|
|
60
|
-
* custom `env` parameter for symmetry with `getMemeshDirFromDbPath(env)`,
|
|
61
|
-
* but the inner `homeDir()` only ever read `process.env.HOME`, so a
|
|
62
|
-
* caller that passed `{HOME: '/tmp/x'}` would be silently ignored — a
|
|
63
|
-
* footgun. Tests redirect via `process.env.HOME`; that's the supported
|
|
64
|
-
* extension point.
|
|
65
|
-
*
|
|
66
|
-
* @returns {string}
|
|
67
|
-
*/
|
|
68
|
-
export function memeshDir() {
|
|
69
|
-
return process.env.MEMESH_DIR ?? join(homeDir(), '.memesh');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Resolve the active memesh DB path.
|
|
74
|
-
*
|
|
75
|
-
* Precedence: MEMESH_DB_PATH env var > <memeshDir>/knowledge-graph.db.
|
|
76
|
-
* Mirror of: src/core/paths.ts → getDbPath() — no-arg, see memeshDir().
|
|
77
|
-
*
|
|
78
|
-
* @returns {string}
|
|
79
|
-
*/
|
|
80
|
-
export function getDbPath() {
|
|
81
|
-
return process.env.MEMESH_DB_PATH ?? join(memeshDir(), 'knowledge-graph.db');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Derive the project name from a working directory.
|
|
86
|
-
*
|
|
87
|
-
* Hooks historically used `basename(data.cwd || process.cwd())`. Core
|
|
88
|
-
* had two variants (`basename(context.cwd)` and `basename(process.cwd())`).
|
|
89
|
-
* This helper unifies the contract — explicit cwd wins, falls through to
|
|
90
|
-
* process.cwd() — matching the most permissive caller's behaviour.
|
|
91
|
-
*
|
|
92
|
-
* Mirror of: src/core/paths.ts → getProjectName() + resolveProjectIdentity().
|
|
93
|
-
* The layered git resolution MUST stay identical to that file — a divergence
|
|
94
|
-
* means hooks (which write project tags) and core (which reads them) would
|
|
95
|
-
* disagree on identity, re-creating the split this change fixes.
|
|
96
|
-
*
|
|
97
|
-
* @param {string|null|undefined} [cwdInput]
|
|
98
|
-
* @returns {string}
|
|
99
|
-
*/
|
|
100
|
-
const _projectNameCache = new Map();
|
|
101
|
-
|
|
102
|
-
export function getProjectName(cwdInput) {
|
|
103
|
-
const cwd = cwdInput && cwdInput.length > 0 ? cwdInput : process.cwd();
|
|
104
|
-
const cached = _projectNameCache.get(cwd);
|
|
105
|
-
if (cached !== undefined) return cached;
|
|
106
|
-
const resolved = _resolveProjectIdentity(cwd);
|
|
107
|
-
_projectNameCache.set(cwd, resolved);
|
|
108
|
-
return resolved;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Layered identity: git remote slug > git repo root basename > cwd basename.
|
|
112
|
-
// See src/core/paths.ts resolveProjectIdentity for the full rationale. git
|
|
113
|
-
// failures at any layer fall through to the next; capture must never break.
|
|
114
|
-
function _resolveProjectIdentity(cwd) {
|
|
115
|
-
const remote = _tryGit(cwd, ['config', '--get', 'remote.origin.url']);
|
|
116
|
-
if (remote) {
|
|
117
|
-
const slug = slugFromRemoteUrl(remote);
|
|
118
|
-
if (slug) return slug;
|
|
119
|
-
}
|
|
120
|
-
const root = _tryGit(cwd, ['rev-parse', '--show-toplevel']);
|
|
121
|
-
if (root) return basename(root);
|
|
122
|
-
return basename(cwd);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function _tryGit(cwd, args) {
|
|
126
|
-
try {
|
|
127
|
-
const out = execFileSync('git', ['-C', cwd, ...args], {
|
|
128
|
-
encoding: 'utf8',
|
|
129
|
-
timeout: 2000,
|
|
130
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
131
|
-
});
|
|
132
|
-
const trimmed = out.trim();
|
|
133
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
134
|
-
} catch {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
17
|
+
// `src/core/paths.ts` and `src/storage/fts-index.ts` are runtime-LEAF modules
|
|
18
|
+
// (paths.ts imports only node builtins; fts-index.ts has only a type-only
|
|
19
|
+
// import), so `tsc` emits self-contained JS for them. `scripts/generate-hook-core.mjs`
|
|
20
|
+
// copies that compiled JS to `_generated/` at build time — committed, shipped in
|
|
21
|
+
// the tarball, and version-locked to its own install. So the hook path still
|
|
22
|
+
// survives a missing/stale `dist/` (the F5 constraint) exactly as the hand-mirror
|
|
23
|
+
// did, but the copy is byte-locked to core and CI-gated (`git diff` on rebuild +
|
|
24
|
+
// `tests/hooks/mirror-parity.test.ts`), making drift structurally impossible.
|
|
25
|
+
//
|
|
26
|
+
// Re-exported here so all 7 hooks keep importing these names from `_shared.js`
|
|
27
|
+
// unchanged.
|
|
28
|
+
import {
|
|
29
|
+
memeshDir,
|
|
30
|
+
getDbPath,
|
|
31
|
+
getMemeshDirFromDbPath,
|
|
32
|
+
getProjectName,
|
|
33
|
+
slugFromRemoteUrl,
|
|
34
|
+
} from './_generated/core-paths.js';
|
|
35
|
+
import { removeFromFts, insertFtsRow } from './_generated/fts-index.js';
|
|
36
|
+
|
|
37
|
+
export { memeshDir, getDbPath, getMemeshDirFromDbPath, getProjectName, slugFromRemoteUrl };
|
|
138
38
|
|
|
139
|
-
|
|
140
|
-
export function slugFromRemoteUrl(url) {
|
|
141
|
-
const cleaned = url.trim().replace(/\.git$/i, '').replace(/[/\\]+$/, '');
|
|
142
|
-
if (!cleaned) return null;
|
|
143
|
-
const seg = cleaned.split(/[/:\\]/).filter(Boolean).pop();
|
|
144
|
-
return seg && seg.length > 0 ? seg : null;
|
|
145
|
-
}
|
|
39
|
+
const require = createRequire(import.meta.url);
|
|
146
40
|
|
|
147
41
|
/**
|
|
148
42
|
* Resolve the package root from a hook file's `import.meta.url`.
|
|
@@ -623,25 +517,73 @@ export function openHookDb(env = process.env, opts = {}) {
|
|
|
623
517
|
return { db, dbPath };
|
|
624
518
|
}
|
|
625
519
|
|
|
626
|
-
const PRIVATE_DIR_MODE = 0o700;
|
|
627
|
-
const PRIVATE_FILE_MODE = 0o600;
|
|
628
|
-
|
|
629
520
|
/**
|
|
630
|
-
*
|
|
521
|
+
* Single owner of the hook-side entity write dance: upsert entity, append
|
|
522
|
+
* observations + tags, and — critically — keep the contentless `entities_fts`
|
|
523
|
+
* index in sync so the memory is recallable via the FTS keyword hot path.
|
|
524
|
+
*
|
|
525
|
+
* Why this exists: post-commit.js, pre-compact.js, and session-summary.js each
|
|
526
|
+
* hand-rolled this dance inline. Three copies drifted — session-summary's copy
|
|
527
|
+
* omitted the FTS reindex entirely, so every `session-insight` memory it wrote
|
|
528
|
+
* was invisible to `recall` and pre-edit-recall (no FTS trigger, no self-heal
|
|
529
|
+
* rebuild on open back it up). Centralising the dance here makes the FTS step
|
|
530
|
+
* impossible to forget in a future hook. This mirrors, on the hook side, what
|
|
531
|
+
* `src/storage/fts-index.ts` already does for core (the F5 boundary keeps them
|
|
532
|
+
* as two implementations of the same contract).
|
|
631
533
|
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
534
|
+
* The caller MUST open its DB with `openHookDb(env, { fts: true })` so the FTS
|
|
535
|
+
* table is guaranteed present. Embeddings + auto-tagging + signal-scoring are
|
|
536
|
+
* deliberately NOT done here: hooks are cheap always-on capture, and those are
|
|
537
|
+
* the heavier, user-initiated `remember` concerns (core owns them).
|
|
635
538
|
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
* in lockstep.
|
|
539
|
+
* @param {import('better-sqlite3').Database} db - an open hook DB handle
|
|
540
|
+
* @param {{name: string, type: string, observations?: string[], tags?: string[]}} entity
|
|
541
|
+
* @returns {{ id: number, isNew: boolean } | null} null if the row could not be resolved
|
|
640
542
|
*/
|
|
641
|
-
export function
|
|
642
|
-
|
|
543
|
+
export function captureEntity(db, { name, type, observations = [], tags = [] }) {
|
|
544
|
+
const insertResult = db
|
|
545
|
+
.prepare('INSERT OR IGNORE INTO entities (name, type) VALUES (?, ?)')
|
|
546
|
+
.run(name, type);
|
|
547
|
+
const isNew = insertResult.changes > 0;
|
|
548
|
+
const row = db.prepare('SELECT id FROM entities WHERE name = ?').get(name);
|
|
549
|
+
if (!row) return null;
|
|
550
|
+
const id = row.id;
|
|
551
|
+
|
|
552
|
+
// Capture the previously-indexed observation text BEFORE inserting new rows,
|
|
553
|
+
// so the contentless-FTS 'delete' below matches what was indexed. Only for
|
|
554
|
+
// existing entities — a brand-new row has no prior FTS entry to remove.
|
|
555
|
+
const prevObsText = isNew
|
|
556
|
+
? undefined
|
|
557
|
+
: db
|
|
558
|
+
.prepare('SELECT content FROM observations WHERE entity_id = ?')
|
|
559
|
+
.all(id)
|
|
560
|
+
.map((o) => o.content)
|
|
561
|
+
.join(' ');
|
|
562
|
+
|
|
563
|
+
const insertObs = db.prepare('INSERT INTO observations (entity_id, content) VALUES (?, ?)');
|
|
564
|
+
for (const obs of observations) insertObs.run(id, obs);
|
|
565
|
+
const insertTag = db.prepare('INSERT OR IGNORE INTO tags (entity_id, tag) VALUES (?, ?)');
|
|
566
|
+
for (const tag of tags) insertTag.run(id, tag);
|
|
567
|
+
|
|
568
|
+
// Reindex FTS: delete the stale entry (if any) then insert the full,
|
|
569
|
+
// current observation set. Uses the generated copy of src/storage/fts-index.ts
|
|
570
|
+
// so the contentless-FTS5 delete+insert dance can no longer drift from core.
|
|
571
|
+
if (prevObsText !== undefined) {
|
|
572
|
+
removeFromFts(db, id, name, prevObsText);
|
|
573
|
+
}
|
|
574
|
+
const allObsText = db
|
|
575
|
+
.prepare('SELECT content FROM observations WHERE entity_id = ?')
|
|
576
|
+
.all(id)
|
|
577
|
+
.map((o) => o.content)
|
|
578
|
+
.join(' ');
|
|
579
|
+
insertFtsRow(db, id, name, allObsText);
|
|
580
|
+
|
|
581
|
+
return { id, isNew };
|
|
643
582
|
}
|
|
644
583
|
|
|
584
|
+
const PRIVATE_DIR_MODE = 0o700;
|
|
585
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
586
|
+
|
|
645
587
|
export function ensurePrivateDir(dirPath) {
|
|
646
588
|
mkdirSync(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
647
589
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execFileSync } from 'child_process';
|
|
4
|
-
import { getProjectName, openHookDb } from './_shared.js';
|
|
4
|
+
import { captureEntity, getProjectName, openHookDb } from './_shared.js';
|
|
5
5
|
|
|
6
6
|
let input = '';
|
|
7
7
|
process.stdin.setEncoding('utf8');
|
|
@@ -77,53 +77,32 @@ process.stdin.on('end', () => {
|
|
|
77
77
|
try {
|
|
78
78
|
const entityName = `commit-${commitHash}`;
|
|
79
79
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// Add richer diff stats as an observation (backward compatible — failures are silently ignored)
|
|
96
|
-
try {
|
|
97
|
-
const stat = execFileSync('git', ['show', '--stat', '--format=', commitHash], {
|
|
98
|
-
cwd: data.cwd || process.cwd(),
|
|
99
|
-
encoding: 'utf8',
|
|
100
|
-
timeout: 5000,
|
|
101
|
-
}).trim();
|
|
102
|
-
if (stat) {
|
|
103
|
-
// Last non-empty line is the summary, e.g. "3 files changed, 45 insertions(+), 12 deletions(-)"
|
|
104
|
-
const statLines = stat.split('\n').filter(l => l.trim());
|
|
105
|
-
const summary = statLines[statLines.length - 1]?.trim() || '';
|
|
106
|
-
if (summary) {
|
|
107
|
-
db.prepare('INSERT INTO observations (entity_id, content) VALUES (?, ?)').run(entity.id, `Diff stats: ${summary}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
} catch {
|
|
111
|
-
// git show failed — no diff stats recorded, existing behavior unchanged
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Add project tag
|
|
115
|
-
const projectTag = `project:${projectName}`;
|
|
116
|
-
db.prepare('INSERT OR IGNORE INTO tags (entity_id, tag) VALUES (?, ?)').run(entity.id, projectTag);
|
|
117
|
-
|
|
118
|
-
// Update FTS index — delete old entry first if entity existed
|
|
119
|
-
if (prevObsText !== undefined) {
|
|
120
|
-
db.prepare("INSERT INTO entities_fts(entities_fts, rowid, name, observations) VALUES('delete', ?, ?, ?)").run(entity.id, entityName, prevObsText);
|
|
80
|
+
// Build the observation set: commit message, branch, and — best-effort —
|
|
81
|
+
// richer diff stats (git failures are silently ignored, unchanged behavior).
|
|
82
|
+
const observations = [commitMsg, `Branch: ${branch}`];
|
|
83
|
+
try {
|
|
84
|
+
const stat = execFileSync('git', ['show', '--stat', '--format=', commitHash], {
|
|
85
|
+
cwd: data.cwd || process.cwd(),
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
timeout: 5000,
|
|
88
|
+
}).trim();
|
|
89
|
+
if (stat) {
|
|
90
|
+
// Last non-empty line is the summary, e.g. "3 files changed, 45 insertions(+), 12 deletions(-)"
|
|
91
|
+
const statLines = stat.split('\n').filter(l => l.trim());
|
|
92
|
+
const summary = statLines[statLines.length - 1]?.trim() || '';
|
|
93
|
+
if (summary) observations.push(`Diff stats: ${summary}`);
|
|
121
94
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const allObsText = allObs.map(o => o.content).join(' ');
|
|
125
|
-
db.prepare('INSERT INTO entities_fts(rowid, name, observations) VALUES(?, ?, ?)').run(entity.id, entityName, allObsText);
|
|
95
|
+
} catch {
|
|
96
|
+
// git show failed — no diff stats recorded, existing behavior unchanged
|
|
126
97
|
}
|
|
98
|
+
|
|
99
|
+
// Shared write dance — upsert entity + observations + tags AND reindex FTS.
|
|
100
|
+
captureEntity(db, {
|
|
101
|
+
name: entityName,
|
|
102
|
+
type: 'commit',
|
|
103
|
+
observations,
|
|
104
|
+
tags: [`project:${projectName}`],
|
|
105
|
+
});
|
|
127
106
|
} finally {
|
|
128
107
|
db.close();
|
|
129
108
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { basename } from 'path';
|
|
4
4
|
import { existsSync, readFileSync } from 'fs';
|
|
5
|
-
import { getProjectName, isAutoCaptureEnabled, openHookDb } from './_shared.js';
|
|
5
|
+
import { captureEntity, getProjectName, isAutoCaptureEnabled, openHookDb } from './_shared.js';
|
|
6
6
|
|
|
7
7
|
// Timeout guard: always exit within 10 seconds
|
|
8
8
|
const TIMEOUT_MS = 10000;
|
|
@@ -99,37 +99,14 @@ process.stdin.on('end', () => {
|
|
|
99
99
|
if (!handle) return;
|
|
100
100
|
const { db } = handle;
|
|
101
101
|
try {
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
? []
|
|
111
|
-
: db.prepare('SELECT content FROM observations WHERE entity_id = ?').all(entity.id);
|
|
112
|
-
const prevObsText = isNew ? undefined : prevObs.map(o => o.content).join(' ');
|
|
113
|
-
|
|
114
|
-
// Insert each observation line
|
|
115
|
-
for (const line of obsLines) {
|
|
116
|
-
db.prepare('INSERT INTO observations (entity_id, content) VALUES (?, ?)').run(entity.id, line);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Add tags
|
|
120
|
-
const tags = ['source:auto-capture', 'urgency:pre-compact', `project:${projectName}`];
|
|
121
|
-
for (const tag of tags) {
|
|
122
|
-
db.prepare('INSERT OR IGNORE INTO tags (entity_id, tag) VALUES (?, ?)').run(entity.id, tag);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Update FTS
|
|
126
|
-
if (prevObsText !== undefined) {
|
|
127
|
-
db.prepare("INSERT INTO entities_fts(entities_fts, rowid, name, observations) VALUES('delete', ?, ?, ?)").run(entity.id, entityName, prevObsText);
|
|
128
|
-
}
|
|
129
|
-
const allObs = db.prepare('SELECT content FROM observations WHERE entity_id = ?').all(entity.id);
|
|
130
|
-
const allObsText = allObs.map(o => o.content).join(' ');
|
|
131
|
-
db.prepare('INSERT INTO entities_fts(rowid, name, observations) VALUES(?, ?, ?)').run(entity.id, entityName, allObsText);
|
|
132
|
-
}
|
|
102
|
+
// Shared write dance — upsert entity + observations + tags AND reindex FTS
|
|
103
|
+
// so the pre-compact memory is recallable via the FTS keyword path.
|
|
104
|
+
captureEntity(db, {
|
|
105
|
+
name: entityName,
|
|
106
|
+
type: 'session-summary',
|
|
107
|
+
observations: obsLines,
|
|
108
|
+
tags: ['source:auto-capture', 'urgency:pre-compact', `project:${projectName}`],
|
|
109
|
+
});
|
|
133
110
|
} finally {
|
|
134
111
|
db.close();
|
|
135
112
|
}
|
|
@@ -11,6 +11,7 @@ import { spawn } from 'child_process';
|
|
|
11
11
|
import os from 'os';
|
|
12
12
|
import { pathToFileURL } from 'url';
|
|
13
13
|
import {
|
|
14
|
+
captureEntity,
|
|
14
15
|
decideAutoUpdateHook,
|
|
15
16
|
getMemeshDirFromDbPath,
|
|
16
17
|
getProjectName,
|
|
@@ -233,7 +234,9 @@ process.stdin.on('end', async () => {
|
|
|
233
234
|
// Open DB via shared helper — applies SCHEMA_SQL + status migration.
|
|
234
235
|
// sqlite-vec is loaded separately because only this hook needs it
|
|
235
236
|
// (for embedding-aware recall-effectiveness tracking).
|
|
236
|
-
|
|
237
|
+
// { fts: true } guarantees the entities_fts table exists so captureEntity()
|
|
238
|
+
// can keep it in sync — session-insight memories must be FTS-recallable.
|
|
239
|
+
const handle = openHookDb(process.env, { fts: true });
|
|
237
240
|
if (!handle) {
|
|
238
241
|
// Native module unavailable (plugin-marketplace cache install with no
|
|
239
242
|
// node_modules). Skip session-capture work, but still let the
|
|
@@ -291,17 +294,12 @@ process.stdin.on('end', async () => {
|
|
|
291
294
|
return [...tags];
|
|
292
295
|
}
|
|
293
296
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
// Delegate the write to the shared captureEntity() so entities land in
|
|
298
|
+
// entities_fts too. This copy used to insert entity + observations + tags
|
|
299
|
+
// only, skipping the FTS reindex the sibling hooks did — which left every
|
|
300
|
+
// session-insight memory unrecallable via the FTS keyword path.
|
|
299
301
|
function storeMemory(name, type, observations, tags) {
|
|
300
|
-
|
|
301
|
-
const row = selectEntity.get(name);
|
|
302
|
-
if (!row) return;
|
|
303
|
-
for (const obs of observations) insertObs.run(row.id, obs);
|
|
304
|
-
for (const tag of tags) insertTag.run(row.id, tag);
|
|
302
|
+
captureEntity(db, { name, type, observations, tags });
|
|
305
303
|
}
|
|
306
304
|
|
|
307
305
|
// Rule 1: File editing session summary
|
|
@@ -426,8 +424,11 @@ process.stdin.on('end', async () => {
|
|
|
426
424
|
|
|
427
425
|
for (let i = 0; i < entityIds.length; i++) {
|
|
428
426
|
const name = (entityNames[i] || '').toLowerCase();
|
|
429
|
-
// Skip
|
|
430
|
-
|
|
427
|
+
// Skip names that carry no recall signal: too short, or a
|
|
428
|
+
// machine identifier (auto-capture entities) that can never
|
|
429
|
+
// substring-match prose. Scoring those would be a guaranteed
|
|
430
|
+
// unearned miss — see isMeasurableRecallName.
|
|
431
|
+
if (!isMeasurableRecallName(name)) continue;
|
|
431
432
|
if (isRecallHit(sessionText, name)) {
|
|
432
433
|
updateHit.run(entityIds[i]);
|
|
433
434
|
} else {
|
|
@@ -761,6 +762,28 @@ export function isRecallHit(sessionText, name) {
|
|
|
761
762
|
return String(sessionText ?? '').toLowerCase().includes(String(name).toLowerCase());
|
|
762
763
|
}
|
|
763
764
|
|
|
765
|
+
/**
|
|
766
|
+
* Whether an injected entity's NAME can serve as a recall-effectiveness signal.
|
|
767
|
+
*
|
|
768
|
+
* Recall-effectiveness decides "was this injected memory used?" by substring-
|
|
769
|
+
* matching the entity NAME in the session transcript (isRecallHit). That only
|
|
770
|
+
* works for names a human might type. Auto-capture entities are named with
|
|
771
|
+
* machine identifiers — `session-<pid>-<ts>-files`, `commit-<hash>`,
|
|
772
|
+
* `pre-compact-<id>` — which never appear verbatim in conversation prose, so
|
|
773
|
+
* they take a `recall_miss` they didn't earn on every injection. Over repeated
|
|
774
|
+
* sessions that drags their Laplace-smoothed impact factor (scoring.ts, 10%
|
|
775
|
+
* weight) down and quietly suppresses auto-captured memories from future recall.
|
|
776
|
+
*
|
|
777
|
+
* We can't measure their usefulness by name, so we don't count them either way —
|
|
778
|
+
* they keep the neutral 0.5 impact. The prefix set is coupled to the auto-capture
|
|
779
|
+
* producers' `<kind>-<id>` naming (post-commit / session-summary / pre-compact);
|
|
780
|
+
* a new auto-capture producer should add its prefix here.
|
|
781
|
+
*/
|
|
782
|
+
export function isMeasurableRecallName(name) {
|
|
783
|
+
if (!name || name.length < 4) return false;
|
|
784
|
+
return !/^(session-|commit-|pre-compact-)/i.test(name);
|
|
785
|
+
}
|
|
786
|
+
|
|
764
787
|
export function maybeTriggerDream(projectName, config, pluginRoot) {
|
|
765
788
|
dreamTrigTrace('enter', { projectName, hasLlm: Boolean(config?.llm) });
|
|
766
789
|
if (!projectName || projectName === 'unknown') {
|