@pcircle/memesh 4.5.1 → 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/AGENTS.md +95 -0
- package/README.de.md +129 -35
- package/README.md +161 -34
- package/README.zh-TW.md +130 -35
- package/dashboard/dist/index.html +10 -10
- package/dist/cli/view-live.js +3 -3
- 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/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/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 +2 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +34 -56
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +5 -2
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +108 -25
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/embedder.d.ts +5 -4
- package/dist/core/embedder.d.ts.map +1 -1
- package/dist/core/embedder.js +16 -8
- package/dist/core/embedder.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/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 +5 -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 +0 -1
- package/dist/core/kg-backfill.d.ts.map +1 -1
- package/dist/core/kg-backfill.js +0 -3
- package/dist/core/kg-backfill.js.map +1 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +14 -21
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/memory-tool.d.ts.map +1 -1
- package/dist/core/memory-tool.js +4 -4
- package/dist/core/memory-tool.js.map +1 -1
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +22 -13
- package/dist/core/operations.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 +31 -0
- package/dist/core/schema-export.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/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-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 +4 -0
- package/dist/core/types.d.ts.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 +2 -7
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +144 -284
- package/dist/db.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 +50 -40
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/skills-manifest.json +48 -18
- 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 +4 -2
- 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/transports/cli/cli.d.ts +1 -4
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +382 -6
- 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 +208 -307
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +46 -0
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +57 -2
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +21 -10
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +26 -8
- package/dist/transports/schemas.js.map +1 -1
- package/llms-install.md +138 -0
- package/package.json +14 -9
- package/scripts/hooks/_generated/capture-flag.js +17 -0
- package/scripts/hooks/_generated/fts-index.js +16 -4
- package/scripts/hooks/_generated/schema.js +281 -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 +122 -478
- package/scripts/hooks/post-commit.js +4 -1
- package/scripts/hooks/pre-compact.js +13 -1
- package/scripts/hooks/pre-edit-recall.js +5 -3
- package/scripts/hooks/session-start.js +135 -59
- package/scripts/hooks/session-summary.js +59 -24
- package/skills/memesh/SKILL.md +97 -76
- package/README.es.md +0 -467
- package/README.fr.md +0 -459
- package/README.ja.md +0 -467
- package/README.ko.md +0 -467
- package/README.pt.md +0 -459
- package/README.th.md +0 -460
- package/README.vi.md +0 -459
- package/README.zh-CN.md +0 -466
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execFileSync } from 'child_process';
|
|
4
|
-
import { AUTO_CAPTURE_TAG, captureEntity, getProjectName, isAutoCaptureEnabled, openHookDb, recordHookRun } from './_shared.js';
|
|
4
|
+
import { AUTO_CAPTURE_TAG, captureEntity, getProjectName, isAutoCaptureEnabled, openHookDb, recordHookRun, truncateTitle } from './_shared.js';
|
|
5
5
|
|
|
6
6
|
let input = '';
|
|
7
7
|
process.stdin.setEncoding('utf8');
|
|
@@ -140,11 +140,14 @@ process.stdin.on('end', () => {
|
|
|
140
140
|
// this one did not). `memesh doctor` counts it to answer "is the
|
|
141
141
|
// auto-capture loop alive" — a question it used to answer from entity
|
|
142
142
|
// TYPE, which a hand-typed `memesh learn` satisfied all by itself.
|
|
143
|
+
// The commit subject IS the title — git authors already wrote a
|
|
144
|
+
// one-line human summary; nothing to synthesize.
|
|
143
145
|
const written = captureEntity(db, {
|
|
144
146
|
name: entityName,
|
|
145
147
|
type: 'commit',
|
|
146
148
|
observations,
|
|
147
149
|
tags: [AUTO_CAPTURE_TAG, `project:${projectName}`],
|
|
150
|
+
title: truncateTitle(commitMsg),
|
|
148
151
|
});
|
|
149
152
|
|
|
150
153
|
// Heartbeat AFTER capture, so the stamp certifies "the capture loop
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { basename } from 'path';
|
|
4
4
|
import { existsSync, readFileSync } from 'fs';
|
|
5
|
-
import { AUTO_CAPTURE_TAG, captureEntity, getProjectName, isAutoCaptureEnabled, openHookDb, recordHookRun } from './_shared.js';
|
|
5
|
+
import { AUTO_CAPTURE_TAG, captureEntity, getProjectName, isAutoCaptureEnabled, openHookDb, recordHookRun, truncateTitle } from './_shared.js';
|
|
6
6
|
|
|
7
7
|
// Timeout guard: always exit within 10 seconds
|
|
8
8
|
const TIMEOUT_MS = 10000;
|
|
@@ -95,6 +95,17 @@ process.stdin.on('end', () => {
|
|
|
95
95
|
|
|
96
96
|
// Build observation content
|
|
97
97
|
const obsLines = [`Compaction reason: ${reason}`, `Tool calls: ${toolCallCount}`];
|
|
98
|
+
|
|
99
|
+
// No free-form human text exists for a pre-compact save (unlike a commit
|
|
100
|
+
// subject) — same date+project+verb heuristic as session-summary.js.
|
|
101
|
+
// This hook was the one missed by the original design (it hand-rolls the
|
|
102
|
+
// same captureEntity dance as its two siblings but was overlooked as a
|
|
103
|
+
// "write hook" until a code audit found it), so its entities were the
|
|
104
|
+
// clearest case of a raw machine key (`pre-compact-<sessionId>`) with a
|
|
105
|
+
// terse observation ("Compaction reason: manual") standing in as the
|
|
106
|
+
// display label.
|
|
107
|
+
const titleDate = new Date().toISOString().slice(0, 10);
|
|
108
|
+
const title = truncateTitle(`${titleDate} ${projectName}: ${reason} compaction (${toolCallCount} tool calls)`);
|
|
98
109
|
if (editedFiles.size > 0) {
|
|
99
110
|
obsLines.push(`Files edited: ${Array.from(editedFiles).join(', ')}`);
|
|
100
111
|
}
|
|
@@ -111,6 +122,7 @@ process.stdin.on('end', () => {
|
|
|
111
122
|
type: 'session-summary',
|
|
112
123
|
observations: obsLines,
|
|
113
124
|
tags: [AUTO_CAPTURE_TAG, 'urgency:pre-compact', `project:${projectName}`],
|
|
125
|
+
title,
|
|
114
126
|
});
|
|
115
127
|
|
|
116
128
|
// Heartbeat AFTER capture, so the stamp certifies "the capture loop
|
|
@@ -43,9 +43,6 @@ process.stdin.on('end', () => {
|
|
|
43
43
|
return pass();
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Get project name from cwd for project-scoped filtering
|
|
47
|
-
const projectName = getProjectName(data.cwd);
|
|
48
|
-
|
|
49
46
|
// Throttle: skip if we already recalled for this file
|
|
50
47
|
const fileKey = filePath.toLowerCase();
|
|
51
48
|
let seenFiles = [];
|
|
@@ -64,6 +61,11 @@ process.stdin.on('end', () => {
|
|
|
64
61
|
|
|
65
62
|
if (!existsSync(dbPath)) return pass();
|
|
66
63
|
|
|
64
|
+
// Get project name from cwd for project-scoped filtering.
|
|
65
|
+
// After the throttle/db checks: this spawns 1-2 git subprocesses, and the
|
|
66
|
+
// throttled path (every repeat edit of the same file) must not pay for it.
|
|
67
|
+
const projectName = getProjectName(data.cwd);
|
|
68
|
+
|
|
67
69
|
// `readOnly`, not `readonly`: node:sqlite ignores the lowercase spelling
|
|
68
70
|
// and hands back a WRITABLE handle. This hook only reads.
|
|
69
71
|
const db = new MemeshDatabase(dbPath, { readOnly: true });
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { spawn } from 'child_process';
|
|
5
|
-
import { homedir } from 'os';
|
|
6
5
|
import { join } from 'path';
|
|
7
6
|
import { pathToFileURL } from 'url';
|
|
8
7
|
import { existsSync, readFileSync, unlinkSync, mkdirSync, accessSync, constants as fsConstants } from 'fs';
|
|
@@ -13,10 +12,22 @@ import {
|
|
|
13
12
|
getMemeshDirFromDbPath,
|
|
14
13
|
getProjectName,
|
|
15
14
|
importFromPluginRoot,
|
|
15
|
+
assembleTopologyBlock,
|
|
16
|
+
DEFAULT_TOPOLOGY_BUDGET,
|
|
17
|
+
SNIPPET_FETCH_CHARS,
|
|
18
|
+
TOPOLOGY_CANDIDATE_CAP,
|
|
16
19
|
isTrustedForAutoContext,
|
|
20
|
+
parseEntityMetadata,
|
|
21
|
+
// Aliased: this file already has a local `const memeshDir` (a resolved
|
|
22
|
+
// db-path-derived directory string) — the helper here is the MEMESH_DIR/
|
|
23
|
+
// home resolver the update-check cache itself uses.
|
|
24
|
+
memeshDir as memeshHomeDir,
|
|
25
|
+
parseTaskState,
|
|
17
26
|
readUpdateCheckCache,
|
|
18
27
|
resolvePluginRoot,
|
|
19
28
|
resolveSessionLimit,
|
|
29
|
+
taskStateLines,
|
|
30
|
+
taskStateName,
|
|
20
31
|
writePrivateJson,
|
|
21
32
|
} from './_shared.js';
|
|
22
33
|
import { MemeshDatabase } from './_generated/sqlite.js';
|
|
@@ -192,7 +203,7 @@ function isStrictlyOlder(a, b) {
|
|
|
192
203
|
return pa.tail < pb.tail;
|
|
193
204
|
}
|
|
194
205
|
|
|
195
|
-
function buildUpdateAvailableBanner(currentVersion, cache,
|
|
206
|
+
function buildUpdateAvailableBanner(currentVersion, cache, getChannel) {
|
|
196
207
|
if (!cache || cache.currentVersion !== currentVersion) return [];
|
|
197
208
|
// Deprecation banner takes precedence — when set, it owns the
|
|
198
209
|
// session-start real estate. Skip the soft banner so the user sees
|
|
@@ -215,7 +226,10 @@ function buildUpdateAvailableBanner(currentVersion, cache, channel) {
|
|
|
215
226
|
// this user a banner today?"
|
|
216
227
|
try {
|
|
217
228
|
const fs = require('fs');
|
|
218
|
-
|
|
229
|
+
// memeshHomeDir(), not join(homedir(), '.memesh'): the throttle marker
|
|
230
|
+
// must sit next to the update-check cache it throttles when MEMESH_DIR
|
|
231
|
+
// is set (readUpdateCheckCache resolves its path with this same helper).
|
|
232
|
+
const dir = memeshHomeDir();
|
|
219
233
|
try { ensurePrivateDir(dir); } catch { /* best-effort */ }
|
|
220
234
|
const versionTag = /^[0-9A-Za-z.+-]+$/.test(currentVersion) ? currentVersion : 'unknown';
|
|
221
235
|
const markerPath = join(dir, `last-update-banner.${versionTag}.lock`);
|
|
@@ -234,6 +248,13 @@ function buildUpdateAvailableBanner(currentVersion, cache, channel) {
|
|
|
234
248
|
} catch { /* best-effort */ }
|
|
235
249
|
} catch { /* best-effort */ }
|
|
236
250
|
|
|
251
|
+
// Channel detection spawns `npm root -g` (50-200ms) — resolve it only
|
|
252
|
+
// here, after every early return above has had its chance to suppress
|
|
253
|
+
// the banner. The guards fire on ~every session; the banner at most
|
|
254
|
+
// once per 24h.
|
|
255
|
+
let channel = 'unknown';
|
|
256
|
+
try { channel = getChannel(); } catch { /* best-effort */ }
|
|
257
|
+
|
|
237
258
|
const lines = [
|
|
238
259
|
'',
|
|
239
260
|
`ℹ️ MeMesh update available: ${cache.latestVersion} (you're on ${currentVersion}).`,
|
|
@@ -241,7 +262,7 @@ function buildUpdateAvailableBanner(currentVersion, cache, channel) {
|
|
|
241
262
|
if (channel === 'npm-global') {
|
|
242
263
|
lines.push(` Run: memesh update`);
|
|
243
264
|
} else if (channel === 'plugin-marketplace') {
|
|
244
|
-
lines.push(` Run:
|
|
265
|
+
lines.push(` Run: memesh upgrade-plugin (no CLI? npx @pcircle/memesh upgrade-plugin — or reinstall from /plugin UI)`);
|
|
245
266
|
} else if (channel === 'source-checkout') {
|
|
246
267
|
lines.push(` Source checkout: \`git pull && npm install && npm run build\`.`);
|
|
247
268
|
} else if (channel === 'npm-local') {
|
|
@@ -309,7 +330,9 @@ function spawnFreshUpdateCheck(installedVersion) {
|
|
|
309
330
|
const cliPath = join(pluginRoot, 'dist/transports/cli/cli.js');
|
|
310
331
|
if (!existsSync(cliPath)) return false;
|
|
311
332
|
const fs = require('fs');
|
|
312
|
-
|
|
333
|
+
// memeshHomeDir(), not join(homedir(), '.memesh') — same reasoning as
|
|
334
|
+
// the banner marker above: marker and cache must share a directory.
|
|
335
|
+
const dir = memeshHomeDir();
|
|
313
336
|
try { ensurePrivateDir(dir); } catch { /* best-effort */ }
|
|
314
337
|
// Codex round 37: scope the throttle marker to the installed
|
|
315
338
|
// version. The marker was machine-global, so a refresh started
|
|
@@ -491,8 +514,8 @@ function combineWithBanner(baseMessage) {
|
|
|
491
514
|
if (deprecation.length > 0) {
|
|
492
515
|
lines = deprecation;
|
|
493
516
|
} else {
|
|
494
|
-
|
|
495
|
-
|
|
517
|
+
lines = buildUpdateAvailableBanner(
|
|
518
|
+
installedVersion, cache, () => detectInstallChannelHook(pluginRoot));
|
|
496
519
|
}
|
|
497
520
|
}
|
|
498
521
|
} catch {
|
|
@@ -571,6 +594,10 @@ process.stdin.on('end', async () => {
|
|
|
571
594
|
// property of the database file that the writing side already set, and a
|
|
572
595
|
// reader opens a WAL database perfectly well without asking for it.
|
|
573
596
|
const db = new MemeshDatabase(dbPath, { readOnly: true });
|
|
597
|
+
// Whether the noise-compression epilogue below should run at all —
|
|
598
|
+
// pre-read from this readonly handle before it closes. Defaults to
|
|
599
|
+
// true so any early exit still lets the epilogue's own throttle decide.
|
|
600
|
+
let noiseCompressDue = true;
|
|
574
601
|
try {
|
|
575
602
|
// Check if tables exist (db may exist but be empty)
|
|
576
603
|
const tableCheck = db.prepare(
|
|
@@ -633,7 +660,12 @@ process.stdin.on('end', async () => {
|
|
|
633
660
|
// doesn't have confidence/access_count/last_accessed_at, so the
|
|
634
661
|
// SELECT can't reference them. Build the column list to match
|
|
635
662
|
// what the schema actually supports.
|
|
636
|
-
|
|
663
|
+
// `title` is an ALTER-added column (UX-1) and gets the same
|
|
664
|
+
// legacy-schema guard as the scoring columns: a database that predates
|
|
665
|
+
// it must still produce an injection, falling back to the observation
|
|
666
|
+
// snippet for its display text.
|
|
667
|
+
const hasTitle = colNames.has('title');
|
|
668
|
+
const baseCols = `e.id, e.name, e.type,${hasTitle ? ' e.title,' : ''} e.created_at, e.metadata`;
|
|
637
669
|
const scoringCols = hasScoringCols
|
|
638
670
|
? `, e.confidence, e.access_count, e.last_accessed_at`
|
|
639
671
|
: '';
|
|
@@ -650,7 +682,7 @@ process.stdin.on('end', async () => {
|
|
|
650
682
|
pool_stats AS (
|
|
651
683
|
SELECT COALESCE(MAX(access_count), 0) AS max_access FROM pool
|
|
652
684
|
)
|
|
653
|
-
SELECT p.id, p.name, p.type, p.created_at, p.metadata
|
|
685
|
+
SELECT p.id, p.name, p.type,${hasTitle ? ' p.title,' : ''} p.created_at, p.metadata
|
|
654
686
|
FROM pool p, pool_stats s
|
|
655
687
|
ORDER BY
|
|
656
688
|
COALESCE(p.confidence, 1.0) * 0.2833
|
|
@@ -679,7 +711,19 @@ process.stdin.on('end', async () => {
|
|
|
679
711
|
`JOIN tags t ON t.entity_id = e.id`,
|
|
680
712
|
`WHERE t.tag = ? ${statusFilter}`,
|
|
681
713
|
);
|
|
682
|
-
|
|
714
|
+
// Over-fetch WIDE, then filter. The window used to be `sessionLimit * 3`
|
|
715
|
+
// and the trust filter ran after it — so a class of entity that ranks
|
|
716
|
+
// high can consume the entire window and leave nothing. That was not
|
|
717
|
+
// hypothetical: measured on a real graph, all 30 top-ranked rows were
|
|
718
|
+
// filtered out and the "project memory" section rendered empty while 92
|
|
719
|
+
// eligible entities sat below the cut. The filter is a JS predicate with
|
|
720
|
+
// one owner (`isTrustedForAutoContext`); rather than restate it as SQL
|
|
721
|
+
// and own it twice, the window is made wide enough that the filtered
|
|
722
|
+
// class cannot fill it. CANDIDATE_CAP bounds the work for a large graph.
|
|
723
|
+
// Shared with the briefing surface via the leaf, so the two sides'
|
|
724
|
+
// candidate windows cannot drift apart.
|
|
725
|
+
const CANDIDATE_CAP = TOPOLOGY_CANDIDATE_CAP;
|
|
726
|
+
const projectEntities = db.prepare(projectQuery).all(projectTag, CANDIDATE_CAP)
|
|
683
727
|
.filter(entity => isTrustedForAutoContext(entity.metadata))
|
|
684
728
|
.slice(0, sessionLimit);
|
|
685
729
|
|
|
@@ -688,7 +732,7 @@ process.stdin.on('end', async () => {
|
|
|
688
732
|
// so rewrite to e.status for consistency.
|
|
689
733
|
const recentWhere = hasStatus ? "WHERE e.status = 'active'" : '';
|
|
690
734
|
const recentQuery = buildScoringQuery('', recentWhere);
|
|
691
|
-
const recentEntities = db.prepare(recentQuery).all(
|
|
735
|
+
const recentEntities = db.prepare(recentQuery).all(CANDIDATE_CAP)
|
|
692
736
|
.filter(entity => isTrustedForAutoContext(entity.metadata))
|
|
693
737
|
.slice(0, 5);
|
|
694
738
|
|
|
@@ -701,7 +745,7 @@ process.stdin.on('end', async () => {
|
|
|
701
745
|
let lessonEntities = [];
|
|
702
746
|
try {
|
|
703
747
|
const lessonRows = db.prepare(`
|
|
704
|
-
SELECT DISTINCT e.id, e.name, e.type, e.metadata
|
|
748
|
+
SELECT DISTINCT e.id, e.name, e.type,${hasTitle ? ' e.title,' : ''} e.metadata
|
|
705
749
|
FROM entities e
|
|
706
750
|
JOIN tags t ON t.entity_id = e.id
|
|
707
751
|
WHERE e.type = 'lesson_learned'
|
|
@@ -754,8 +798,9 @@ process.stdin.on('end', async () => {
|
|
|
754
798
|
// stay far under that on purpose — session start should prime the
|
|
755
799
|
// model, not consume its working context. Snippets are truncated per
|
|
756
800
|
// observation and the whole block is hard-capped.
|
|
757
|
-
|
|
758
|
-
|
|
801
|
+
// The budget itself comes from the leaf (DEFAULT_TOPOLOGY_BUDGET) at
|
|
802
|
+
// the assembleTopologyBlock call — "the same block" depends on the two
|
|
803
|
+
// surfaces agreeing, so neither side restates the numbers.
|
|
759
804
|
|
|
760
805
|
const memoryLines = [];
|
|
761
806
|
try {
|
|
@@ -788,33 +833,59 @@ process.stdin.on('end', async () => {
|
|
|
788
833
|
// later ones are refinements.
|
|
789
834
|
if (snippets.has(row.entity_id)) continue;
|
|
790
835
|
const text = String(row.content ?? '').replace(/\s+/g, ' ').trim();
|
|
791
|
-
|
|
836
|
+
// A few line-widths, not the exact line cap: the final cut is
|
|
837
|
+
// clip()'s, on a word boundary — a hard slice at the line cap
|
|
838
|
+
// would hand it a string with nothing left to trim and ship
|
|
839
|
+
// mid-word fragments again.
|
|
840
|
+
if (text) snippets.set(row.entity_id, text.slice(0, SNIPPET_FETCH_CHARS));
|
|
792
841
|
}
|
|
793
842
|
}
|
|
794
843
|
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
812
|
-
memoryLines.push('');
|
|
813
|
-
};
|
|
844
|
+
// "Where we left off" leads the block. It is the one memory a new
|
|
845
|
+
// session needs before any other: everything below is context for
|
|
846
|
+
// work, this IS the work. It is also the only line here a human
|
|
847
|
+
// (or an agent acting for one) stated on purpose — the rest is
|
|
848
|
+
// ranked, and ranking cannot know what you meant to do next.
|
|
849
|
+
//
|
|
850
|
+
// Read from metadata, not from the observation trail: observations
|
|
851
|
+
// are the CHANGE history, and picking "the current goal" out of them
|
|
852
|
+
// means guessing which line is newest. Metadata holds one answer.
|
|
853
|
+
const taskRow = db
|
|
854
|
+
.prepare('SELECT metadata FROM entities WHERE name = ?')
|
|
855
|
+
.get(taskStateName(projectName));
|
|
856
|
+
const stateLines = taskStateLines(
|
|
857
|
+
parseTaskState(parseEntityMetadata(taskRow?.metadata)),
|
|
858
|
+
projectName,
|
|
859
|
+
);
|
|
814
860
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
861
|
+
// The pools overlap by construction (a lesson tagged to this project
|
|
862
|
+
// is in lessonEntities AND projectEntities); the shared assembler
|
|
863
|
+
// dedupes across them in claim order, so a project-scoped row is
|
|
864
|
+
// never marked foreign by the cross-project recent pool, and the
|
|
865
|
+
// topology grouping decides where each one belongs. This mapping —
|
|
866
|
+
// raw row → TopologyEntity — is the only part this hook owns; the
|
|
867
|
+
// assembly order, the spacer discipline, the budget and the
|
|
868
|
+
// task-state exclusion live in the leaf, shared with `briefing`.
|
|
869
|
+
const toEntity = (e) => {
|
|
870
|
+
const meta = parseEntityMetadata(e.metadata);
|
|
871
|
+
return {
|
|
872
|
+
name: e.name,
|
|
873
|
+
type: e.type || 'memory',
|
|
874
|
+
title: e.title ?? null,
|
|
875
|
+
snippet: snippets.get(e.id) ?? null,
|
|
876
|
+
signalScore: meta && typeof meta.signal_score === 'number' ? meta.signal_score : null,
|
|
877
|
+
};
|
|
878
|
+
};
|
|
879
|
+
memoryLines.push(...assembleTopologyBlock(
|
|
880
|
+
stateLines,
|
|
881
|
+
[
|
|
882
|
+
{ entities: topLessons.map(toEntity), foreign: false },
|
|
883
|
+
{ entities: projectEntities.map(toEntity), foreign: false },
|
|
884
|
+
{ entities: recentEntities.map(toEntity), foreign: true },
|
|
885
|
+
],
|
|
886
|
+
projectName,
|
|
887
|
+
DEFAULT_TOPOLOGY_BUDGET,
|
|
888
|
+
));
|
|
818
889
|
} catch (err) {
|
|
819
890
|
// Snippet enrichment is best-effort. A failure here must not stop
|
|
820
891
|
// the banner or the session — but trace it, because a silent break
|
|
@@ -830,22 +901,10 @@ process.stdin.on('end', async () => {
|
|
|
830
901
|
// attacker-influenced in the general case (anything the agent has
|
|
831
902
|
// ever been told can end up in an observation), so it must be
|
|
832
903
|
// delimited the same way on every injection path — not hand-rolled
|
|
833
|
-
// per hook.
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
|
|
837
|
-
// its delimiter.
|
|
838
|
-
const budgeted = [];
|
|
839
|
-
let used = 0;
|
|
840
|
-
for (const line of memoryLines) {
|
|
841
|
-
if (used + line.length + 1 > MAX_CONTEXT_CHARS) {
|
|
842
|
-
budgeted.push('… (truncated)');
|
|
843
|
-
break;
|
|
844
|
-
}
|
|
845
|
-
budgeted.push(line);
|
|
846
|
-
used += line.length + 1;
|
|
847
|
-
}
|
|
848
|
-
memoryContext = buildReferenceContext(budgeted);
|
|
904
|
+
// per hook. The lines arrive already budgeted — assembleTopologyBlock
|
|
905
|
+
// charges the task-state block and the sections against ONE ceiling
|
|
906
|
+
// and returns whole lines only, so the closing fence cannot be cut.
|
|
907
|
+
memoryContext = buildReferenceContext(memoryLines);
|
|
849
908
|
}
|
|
850
909
|
|
|
851
910
|
// --- Record injected entity IDs for recall effectiveness tracking ---
|
|
@@ -925,12 +984,8 @@ process.stdin.on('end', async () => {
|
|
|
925
984
|
if (deprecation.length > 0) {
|
|
926
985
|
bannerLines = deprecation;
|
|
927
986
|
} else {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
931
|
-
channel = detectInstallChannelHook(pluginRoot);
|
|
932
|
-
} catch { /* best-effort */ }
|
|
933
|
-
bannerLines = buildUpdateAvailableBanner(installedVersion, updateCache, channel);
|
|
987
|
+
bannerLines = buildUpdateAvailableBanner(installedVersion, updateCache,
|
|
988
|
+
() => detectInstallChannelHook(resolvePluginRoot(import.meta.url)));
|
|
934
989
|
}
|
|
935
990
|
}
|
|
936
991
|
const finalMessage = bannerLines.length > 0
|
|
@@ -938,6 +993,25 @@ process.stdin.on('end', async () => {
|
|
|
938
993
|
: summary;
|
|
939
994
|
|
|
940
995
|
output(withCaptureWarning(finalMessage), memoryContext);
|
|
996
|
+
|
|
997
|
+
// Pre-read the noise-compression throttle on the handle we already
|
|
998
|
+
// hold. compressWeeklyNoise() re-checks under its own connection, but
|
|
999
|
+
// ~364/365 sessions are inside the 24h window — and the full path
|
|
1000
|
+
// costs two dist module-graph imports plus a write-capable
|
|
1001
|
+
// migration-chain open (WAL writer lock) that must stay off the
|
|
1002
|
+
// SessionStart hot path. Missing table / any error ⇒ due (the full
|
|
1003
|
+
// path owns schema creation).
|
|
1004
|
+
noiseCompressDue = (() => {
|
|
1005
|
+
try {
|
|
1006
|
+
const row = db.prepare(
|
|
1007
|
+
"SELECT value FROM memesh_metadata WHERE key = 'last_noise_compress_at'"
|
|
1008
|
+
).get();
|
|
1009
|
+
if (!row) return true;
|
|
1010
|
+
return Date.now() - new Date(row.value).getTime() >= 24 * 60 * 60 * 1000;
|
|
1011
|
+
} catch {
|
|
1012
|
+
return true;
|
|
1013
|
+
}
|
|
1014
|
+
})();
|
|
941
1015
|
} finally {
|
|
942
1016
|
db.close();
|
|
943
1017
|
}
|
|
@@ -945,6 +1019,7 @@ process.stdin.on('end', async () => {
|
|
|
945
1019
|
// Opens a separate read-write connection via the core module.
|
|
946
1020
|
// Throttled to once per 24h inside compressWeeklyNoise().
|
|
947
1021
|
try {
|
|
1022
|
+
if (noiseCompressDue) {
|
|
948
1023
|
// F5: derive pluginRoot strictly from this file's location.
|
|
949
1024
|
// See `resolvePluginRoot` for the full reasoning.
|
|
950
1025
|
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
@@ -956,6 +1031,7 @@ process.stdin.on('end', async () => {
|
|
|
956
1031
|
} finally {
|
|
957
1032
|
dbMod.closeDatabase();
|
|
958
1033
|
}
|
|
1034
|
+
}
|
|
959
1035
|
} catch (err) {
|
|
960
1036
|
// Non-critical — noise compression failed, will retry next session.
|
|
961
1037
|
// Trace because this catch previously hid an off-by-one regression
|
|
@@ -24,7 +24,6 @@ import { createRequire } from 'module';
|
|
|
24
24
|
import { basename, join } from 'path';
|
|
25
25
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
|
|
26
26
|
import { spawn } from 'child_process';
|
|
27
|
-
import os from 'os';
|
|
28
27
|
import { pathToFileURL } from 'url';
|
|
29
28
|
import {
|
|
30
29
|
AUTO_CAPTURE_TAG,
|
|
@@ -41,6 +40,7 @@ import {
|
|
|
41
40
|
resolveAutoUpdatePolicy,
|
|
42
41
|
resolvePluginRoot,
|
|
43
42
|
spawnAutoUpdate,
|
|
43
|
+
truncateTitle,
|
|
44
44
|
} from './_shared.js';
|
|
45
45
|
|
|
46
46
|
const require = createRequire(import.meta.url);
|
|
@@ -90,9 +90,15 @@ function parseTranscript(transcriptPath) {
|
|
|
90
90
|
const errorsEncountered = [];
|
|
91
91
|
let toolCallCount = 0;
|
|
92
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 = '';
|
|
93
98
|
|
|
94
99
|
try {
|
|
95
|
-
|
|
100
|
+
rawText = readFileSync(transcriptPath, 'utf8');
|
|
101
|
+
const lines = rawText.split('\n').filter(l => l.trim());
|
|
96
102
|
for (const line of lines) {
|
|
97
103
|
try {
|
|
98
104
|
const entry = JSON.parse(line);
|
|
@@ -171,7 +177,7 @@ function parseTranscript(transcriptPath) {
|
|
|
171
177
|
}
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
return { filesEdited: [...filesEdited], bashCommands, errorsEncountered, toolCallCount, readFailed };
|
|
180
|
+
return { filesEdited: [...filesEdited], bashCommands, errorsEncountered, toolCallCount, readFailed, rawText };
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
// Main: read stdin, extract insights, store in DB
|
|
@@ -268,8 +274,9 @@ process.stdin.on('end', async () => {
|
|
|
268
274
|
return exit0();
|
|
269
275
|
}
|
|
270
276
|
|
|
271
|
-
// Parse transcript
|
|
272
|
-
|
|
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);
|
|
273
280
|
|
|
274
281
|
// An unreadable transcript is NOT a quiet session: the capture was
|
|
275
282
|
// LOST (permissions, I/O), and a heartbeat here would keep doctor green
|
|
@@ -354,14 +361,21 @@ process.stdin.on('end', async () => {
|
|
|
354
361
|
// entities_fts too. This copy used to insert entity + observations + tags
|
|
355
362
|
// only, skipping the FTS reindex the sibling hooks did — which left every
|
|
356
363
|
// session-insight memory unrecallable via the FTS keyword path.
|
|
357
|
-
function storeMemory(name, type, observations, tags) {
|
|
364
|
+
function storeMemory(name, type, observations, tags, title) {
|
|
358
365
|
// null = the entity row could not be resolved = this write did NOT
|
|
359
366
|
// happen (captureEntity's contract). A run with a failed write must
|
|
360
367
|
// not stamp the heartbeat below — "alive" would be a lie about the
|
|
361
368
|
// exact thing the heartbeat certifies.
|
|
362
|
-
if (!captureEntity(db, { name, type, observations, tags })) writeFailed = true;
|
|
369
|
+
if (!captureEntity(db, { name, type, observations, tags, title })) writeFailed = true;
|
|
363
370
|
}
|
|
364
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
|
+
|
|
365
379
|
// Rule 1: File editing session summary
|
|
366
380
|
if (filesEdited.length > 0) {
|
|
367
381
|
storeMemory(
|
|
@@ -371,7 +385,8 @@ process.stdin.on('end', async () => {
|
|
|
371
385
|
`Session edited ${filesEdited.length} file(s): ${filesEdited.join(', ')}`,
|
|
372
386
|
`Total tool calls: ${toolCallCount}`,
|
|
373
387
|
],
|
|
374
|
-
[...baseTags, ...fileTagsFor(filesEdited)]
|
|
388
|
+
[...baseTags, ...fileTagsFor(filesEdited)],
|
|
389
|
+
truncateTitle(`${titlePrefix}: edited ${filesEdited.length} file(s)`)
|
|
375
390
|
);
|
|
376
391
|
}
|
|
377
392
|
|
|
@@ -384,7 +399,8 @@ process.stdin.on('end', async () => {
|
|
|
384
399
|
`Fixed ${errorsEncountered.length} error(s) by editing ${filesEdited.join(', ')}`,
|
|
385
400
|
...errorsEncountered.slice(0, 3).map(e => `Error: ${e.slice(0, 100)}`),
|
|
386
401
|
],
|
|
387
|
-
[...baseTags, 'type:bugfix', ...fileTagsFor(filesEdited)]
|
|
402
|
+
[...baseTags, 'type:bugfix', ...fileTagsFor(filesEdited)],
|
|
403
|
+
truncateTitle(`${titlePrefix}: fixed ${errorsEncountered.length} error(s)`)
|
|
388
404
|
);
|
|
389
405
|
}
|
|
390
406
|
|
|
@@ -397,7 +413,8 @@ process.stdin.on('end', async () => {
|
|
|
397
413
|
`Significant session: ${toolCallCount} tool calls, ${filesEdited.length} files edited`,
|
|
398
414
|
...bashCommands.slice(0, 3).map(c => `Command: ${c}`),
|
|
399
415
|
],
|
|
400
|
-
[...baseTags, 'type:heavy-session']
|
|
416
|
+
[...baseTags, 'type:heavy-session'],
|
|
417
|
+
truncateTitle(`${titlePrefix}: significant session (${toolCallCount} tool calls)`)
|
|
401
418
|
);
|
|
402
419
|
}
|
|
403
420
|
|
|
@@ -471,7 +488,9 @@ process.stdin.on('end', async () => {
|
|
|
471
488
|
// undocumented internal — get it wrong and every entity scores
|
|
472
489
|
// a hit instead of a miss. Structural removal is copy-count
|
|
473
490
|
// and encoding independent.
|
|
474
|
-
|
|
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();
|
|
475
494
|
|
|
476
495
|
// Hit/miss decision lives in `isRecallHit` (exported, unit-tested).
|
|
477
496
|
|
|
@@ -482,14 +501,25 @@ process.stdin.on('end', async () => {
|
|
|
482
501
|
'UPDATE entities SET recall_misses = COALESCE(recall_misses, 0) + 1 WHERE id = ?'
|
|
483
502
|
);
|
|
484
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 = ?');
|
|
485
511
|
for (let i = 0; i < entityIds.length; i++) {
|
|
486
512
|
const name = (entityNames[i] || '').toLowerCase();
|
|
487
513
|
// Skip names that carry no recall signal: too short, or a
|
|
488
514
|
// machine identifier (auto-capture entities) that can never
|
|
489
515
|
// substring-match prose. Scoring those would be a guaranteed
|
|
490
516
|
// unearned miss — see isMeasurableRecallName.
|
|
491
|
-
|
|
492
|
-
|
|
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)) {
|
|
493
523
|
updateHit.run(entityIds[i]);
|
|
494
524
|
} else {
|
|
495
525
|
updateMiss.run(entityIds[i]);
|
|
@@ -671,15 +701,15 @@ function dreamHistoryPath() {
|
|
|
671
701
|
// is in pure string comparisons, which we don't do here. The trace
|
|
672
702
|
// below shows the resolved value verbatim so a Windows diagnosis run
|
|
673
703
|
// can confirm what actually arrived.
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
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();
|
|
680
710
|
dreamTrigTrace('resolve', {
|
|
681
|
-
src:
|
|
682
|
-
MEMESH_DIR:
|
|
711
|
+
src: process.env.MEMESH_DB_PATH ? 'db-path' : (process.env.MEMESH_DIR ? 'env' : 'home'),
|
|
712
|
+
MEMESH_DIR: process.env.MEMESH_DIR,
|
|
683
713
|
MEMESH_DB_PATH: process.env.MEMESH_DB_PATH,
|
|
684
714
|
dir,
|
|
685
715
|
platform: process.platform,
|
|
@@ -822,12 +852,17 @@ export function stripHookEchoes(rawTranscript) {
|
|
|
822
852
|
* which silently failed on JSON-encoded transcripts and scored every entity a
|
|
823
853
|
* hit — see the callsite comment.)
|
|
824
854
|
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
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.
|
|
827
862
|
*/
|
|
828
863
|
export function isRecallHit(sessionText, name) {
|
|
829
864
|
if (!name || name.length < 4) return false;
|
|
830
|
-
return String(sessionText ?? '').
|
|
865
|
+
return String(sessionText ?? '').includes(String(name).toLowerCase());
|
|
831
866
|
}
|
|
832
867
|
|
|
833
868
|
/**
|