@pcircle/memesh 4.8.3 → 4.9.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 +1 -1
- package/.claude-plugin/plugin.json +2 -1
- package/.codex-plugin/mcp.json +6 -4
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +68 -17
- package/README.de.md +83 -555
- package/README.md +83 -581
- package/README.zh-TW.md +84 -572
- package/dashboard/dist/index.html +10 -10
- package/dist/cli/view-live.d.ts.map +1 -1
- package/dist/cli/view-live.js +154 -384
- package/dist/cli/view-live.js.map +1 -1
- package/dist/core/agent-message-inbox.d.ts +2 -1
- package/dist/core/agent-message-inbox.d.ts.map +1 -1
- package/dist/core/agent-message-inbox.js +23 -4
- package/dist/core/agent-message-inbox.js.map +1 -1
- package/dist/core/agent-messaging.d.ts.map +1 -1
- package/dist/core/agent-messaging.js +23 -15
- package/dist/core/agent-messaging.js.map +1 -1
- package/dist/core/agent-router.d.ts +11 -10
- package/dist/core/agent-router.d.ts.map +1 -1
- package/dist/core/agent-router.js +29 -14
- package/dist/core/agent-router.js.map +1 -1
- package/dist/core/agent-scope-id.d.ts +11 -0
- package/dist/core/agent-scope-id.d.ts.map +1 -0
- package/dist/core/agent-scope-id.js +40 -0
- package/dist/core/agent-scope-id.js.map +1 -0
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/briefing.d.ts.map +1 -1
- package/dist/core/briefing.js +8 -2
- package/dist/core/briefing.js.map +1 -1
- package/dist/core/config.d.ts +4 -40
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +75 -141
- package/dist/core/config.js.map +1 -1
- package/dist/core/demo.d.ts.map +1 -1
- package/dist/core/demo.js +6 -6
- package/dist/core/demo.js.map +1 -1
- package/dist/core/doctor.d.ts +2 -6
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +120 -154
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +32 -47
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +214 -704
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/install-channel.d.ts.map +1 -1
- package/dist/core/install-channel.js +4 -47
- package/dist/core/install-channel.js.map +1 -1
- package/dist/core/install-id.d.ts.map +1 -1
- package/dist/core/install-id.js.map +1 -1
- package/dist/core/kg-backfill.d.ts.map +1 -1
- package/dist/core/kg-backfill.js.map +1 -1
- package/dist/core/lesson-engine.d.ts +0 -5
- package/dist/core/lesson-engine.d.ts.map +1 -1
- package/dist/core/lesson-engine.js +0 -25
- package/dist/core/lesson-engine.js.map +1 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +58 -49
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/memory-tool.d.ts.map +1 -1
- package/dist/core/memory-tool.js +20 -18
- package/dist/core/memory-tool.js.map +1 -1
- package/dist/core/operations.d.ts +3 -27
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +10 -245
- package/dist/core/operations.js.map +1 -1
- package/dist/core/paths.d.ts +4 -1
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +88 -14
- package/dist/core/paths.js.map +1 -1
- package/dist/core/product-improvements.js +2 -2
- package/dist/core/product-improvements.js.map +1 -1
- package/dist/core/project-tags.d.ts +2 -0
- package/dist/core/project-tags.d.ts.map +1 -1
- package/dist/core/project-tags.js +29 -1
- package/dist/core/project-tags.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +13 -3
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/semver.d.ts +7 -0
- package/dist/core/semver.d.ts.map +1 -0
- package/dist/core/semver.js +49 -0
- package/dist/core/semver.js.map +1 -0
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +69 -57
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/signal-scorer.d.ts.map +1 -1
- package/dist/core/signal-scorer.js.map +1 -1
- package/dist/core/transcript-extractor.d.ts +1 -85
- package/dist/core/transcript-extractor.d.ts.map +1 -1
- package/dist/core/transcript-extractor.js +5 -364
- package/dist/core/transcript-extractor.js.map +1 -1
- package/dist/core/transcript-source.d.ts +22 -6
- package/dist/core/transcript-source.d.ts.map +1 -1
- package/dist/core/transcript-source.js +108 -69
- package/dist/core/transcript-source.js.map +1 -1
- package/dist/core/types.d.ts +1 -17
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/version-check.d.ts +1 -0
- package/dist/core/version-check.d.ts.map +1 -1
- package/dist/core/version-check.js +46 -1
- package/dist/core/version-check.js.map +1 -1
- package/dist/db.d.ts +0 -34
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +6 -287
- package/dist/db.js.map +1 -1
- package/dist/host-runtime/acp.d.ts.map +1 -1
- package/dist/host-runtime/acp.js +4 -3
- package/dist/host-runtime/acp.js.map +1 -1
- package/dist/host-runtime/claude.d.ts.map +1 -1
- package/dist/host-runtime/claude.js +11 -11
- package/dist/host-runtime/claude.js.map +1 -1
- package/dist/host-runtime/codex-session.d.ts +9 -1
- package/dist/host-runtime/codex-session.d.ts.map +1 -1
- package/dist/host-runtime/codex-session.js +474 -29
- package/dist/host-runtime/codex-session.js.map +1 -1
- package/dist/host-runtime/codex.d.ts.map +1 -1
- package/dist/host-runtime/codex.js +4 -3
- package/dist/host-runtime/codex.js.map +1 -1
- package/dist/host-runtime/config.d.ts +1 -0
- package/dist/host-runtime/config.d.ts.map +1 -1
- package/dist/host-runtime/config.js +4 -0
- package/dist/host-runtime/config.js.map +1 -1
- package/dist/host-runtime/entry.d.ts +5 -0
- package/dist/host-runtime/entry.d.ts.map +1 -0
- package/dist/host-runtime/entry.js +11 -0
- package/dist/host-runtime/entry.js.map +1 -0
- package/dist/host-runtime/router-client.d.ts.map +1 -1
- package/dist/host-runtime/router-client.js +62 -15
- package/dist/host-runtime/router-client.js.map +1 -1
- package/dist/host-runtime/router.js +2 -2
- package/dist/host-runtime/router.js.map +1 -1
- package/dist/knowledge-graph.d.ts +0 -1
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +77 -60
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/mcp/THIRD_PARTY_NOTICES.txt +217 -0
- package/dist/mcp/server.js +30685 -38
- package/dist/mcp/server.js.map +6 -1
- package/dist/skills-manifest.json +39 -34
- package/dist/storage/entity-index.d.ts +3 -0
- package/dist/storage/entity-index.d.ts.map +1 -0
- package/dist/storage/entity-index.js +8 -0
- package/dist/storage/entity-index.js.map +1 -0
- package/dist/storage/fts-index.d.ts.map +1 -1
- package/dist/storage/fts-index.js +11 -7
- package/dist/storage/fts-index.js.map +1 -1
- package/dist/storage/graph-repairs.d.ts +7 -2
- package/dist/storage/graph-repairs.d.ts.map +1 -1
- package/dist/storage/graph-repairs.js +89 -13
- package/dist/storage/graph-repairs.js.map +1 -1
- package/dist/storage/schema.d.ts +1 -1
- package/dist/storage/schema.d.ts.map +1 -1
- package/dist/storage/schema.js +1 -2
- package/dist/storage/schema.js.map +1 -1
- package/dist/storage/sqlite.d.ts +0 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/transports/agent-messaging.d.ts.map +1 -1
- package/dist/transports/agent-messaging.js +5 -7
- package/dist/transports/agent-messaging.js.map +1 -1
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +75 -686
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/retired-routes.js +1 -1
- 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 +13 -232
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +130 -4
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +56 -7
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +71 -19
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +57 -11
- package/dist/transports/schemas.js.map +1 -1
- package/docs/platforms/README.md +6 -5
- package/docs/platforms/agent-messaging.md +241 -20
- package/hooks/hooks.json +23 -2
- package/llms-install.md +62 -30
- package/package.json +12 -9
- package/scripts/hooks/_generated/agent-message-inbox.js +23 -4
- package/scripts/hooks/_generated/core-paths.js +88 -14
- package/scripts/hooks/_generated/fts-index.js +11 -7
- package/scripts/hooks/_generated/schema.js +1 -2
- package/scripts/hooks/_shared.js +65 -23
- package/scripts/hooks/decision-nudge.js +152 -0
- package/scripts/hooks/post-commit.js +11 -0
- package/scripts/hooks/pre-compact.js +12 -4
- package/scripts/hooks/session-start.js +32 -9
- package/scripts/hooks/session-summary.js +19 -374
- package/scripts/upgrade-plugin.sh +71 -2
- package/skills/memesh/SKILL.md +24 -15
- package/skills/memesh-review/SKILL.md +7 -6
- package/dist/core/auto-tagger.d.ts +0 -10
- package/dist/core/auto-tagger.d.ts.map +0 -1
- package/dist/core/auto-tagger.js +0 -63
- package/dist/core/auto-tagger.js.map +0 -1
- package/dist/core/conflict-candidates.d.ts +0 -20
- package/dist/core/conflict-candidates.d.ts.map +0 -1
- package/dist/core/conflict-candidates.js +0 -71
- package/dist/core/conflict-candidates.js.map +0 -1
- package/dist/core/conflict-judge.d.ts +0 -58
- package/dist/core/conflict-judge.d.ts.map +0 -1
- package/dist/core/conflict-judge.js +0 -189
- package/dist/core/conflict-judge.js.map +0 -1
- package/dist/core/digest-validator.d.ts +0 -18
- package/dist/core/digest-validator.d.ts.map +0 -1
- package/dist/core/digest-validator.js +0 -85
- package/dist/core/digest-validator.js.map +0 -1
- package/dist/core/embedder.d.ts +0 -20
- package/dist/core/embedder.d.ts.map +0 -1
- package/dist/core/embedder.js +0 -242
- package/dist/core/embedder.js.map +0 -1
- package/dist/core/failure-analyzer.d.ts +0 -19
- package/dist/core/failure-analyzer.d.ts.map +0 -1
- package/dist/core/failure-analyzer.js +0 -83
- package/dist/core/failure-analyzer.js.map +0 -1
- package/dist/core/json-utils.d.ts +0 -3
- package/dist/core/json-utils.d.ts.map +0 -1
- package/dist/core/json-utils.js +0 -46
- package/dist/core/json-utils.js.map +0 -1
- package/dist/core/llm-client.d.ts +0 -22
- package/dist/core/llm-client.d.ts.map +0 -1
- package/dist/core/llm-client.js +0 -203
- package/dist/core/llm-client.js.map +0 -1
- package/dist/core/llm-telemetry.d.ts +0 -47
- package/dist/core/llm-telemetry.d.ts.map +0 -1
- package/dist/core/llm-telemetry.js +0 -117
- package/dist/core/llm-telemetry.js.map +0 -1
- package/dist/core/llm-validator.d.ts +0 -20
- package/dist/core/llm-validator.d.ts.map +0 -1
- package/dist/core/llm-validator.js +0 -231
- package/dist/core/llm-validator.js.map +0 -1
- package/dist/core/ollama-host.d.ts +0 -6
- package/dist/core/ollama-host.d.ts.map +0 -1
- package/dist/core/ollama-host.js +0 -30
- package/dist/core/ollama-host.js.map +0 -1
- package/dist/core/output-language.d.ts +0 -6
- package/dist/core/output-language.d.ts.map +0 -1
- package/dist/core/output-language.js +0 -25
- package/dist/core/output-language.js.map +0 -1
- package/dist/core/prompt-safety.d.ts +0 -4
- package/dist/core/prompt-safety.d.ts.map +0 -1
- package/dist/core/prompt-safety.js +0 -20
- package/dist/core/prompt-safety.js.map +0 -1
- package/dist/storage/vector-index.d.ts +0 -3
- package/dist/storage/vector-index.d.ts.map +0 -1
- package/dist/storage/vector-index.js +0 -13
- package/dist/storage/vector-index.js.map +0 -1
- /package/{.mcp.json → .claude-plugin/mcp.json} +0 -0
|
@@ -4,26 +4,9 @@
|
|
|
4
4
|
// Extracts knowledge from completed Claude Code sessions
|
|
5
5
|
// and stores as session-insight entities in MeMesh.
|
|
6
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.
|
|
22
|
-
|
|
23
7
|
import { createRequire } from 'module';
|
|
24
8
|
import { basename, join } from 'path';
|
|
25
|
-
import { existsSync, readFileSync
|
|
26
|
-
import { spawn } from 'child_process';
|
|
9
|
+
import { existsSync, readFileSync } from 'fs';
|
|
27
10
|
import { pathToFileURL } from 'url';
|
|
28
11
|
import {
|
|
29
12
|
AUTO_CAPTURE_TAG,
|
|
@@ -32,7 +15,6 @@ import {
|
|
|
32
15
|
extractCitedMemoryIds,
|
|
33
16
|
getMemeshDirFromDbPath,
|
|
34
17
|
getProjectName,
|
|
35
|
-
importFromPluginRoot,
|
|
36
18
|
isAutoCaptureEnabled,
|
|
37
19
|
openHookDb,
|
|
38
20
|
readUpdateCheckCache,
|
|
@@ -47,24 +29,17 @@ import {
|
|
|
47
29
|
|
|
48
30
|
const require = createRequire(import.meta.url);
|
|
49
31
|
|
|
50
|
-
|
|
51
|
-
// Same pattern as session-start.js: async ESM import at process init,
|
|
52
|
-
// falls back to null if dist is missing (source checkout pre-build).
|
|
53
|
-
let _installChannelMod = null;
|
|
32
|
+
let installChannel = null;
|
|
54
33
|
try {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
if (existsSync(
|
|
58
|
-
|
|
34
|
+
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
35
|
+
const modulePath = join(pluginRoot, 'dist/core/install-channel.js');
|
|
36
|
+
if (existsSync(modulePath)) {
|
|
37
|
+
installChannel = await import(pathToFileURL(modulePath).href);
|
|
59
38
|
}
|
|
60
|
-
} catch {
|
|
39
|
+
} catch {
|
|
40
|
+
// Best-effort: source checkouts may not have built dist output yet.
|
|
41
|
+
}
|
|
61
42
|
|
|
62
|
-
/**
|
|
63
|
-
* Run auto-update at Stop hook: reads cache, evaluates policy, and dispatches
|
|
64
|
-
* the detached updater runner if warranted. Runs after all session work
|
|
65
|
-
* completes, avoiding the TOCTOU race where install would overwrite dist/
|
|
66
|
-
* mid-session.
|
|
67
|
-
*/
|
|
68
43
|
async function runAutoUpdateAtStop() {
|
|
69
44
|
try {
|
|
70
45
|
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
@@ -75,11 +50,9 @@ async function runAutoUpdateAtStop() {
|
|
|
75
50
|
const cache = readUpdateCheckCache(installedVersion);
|
|
76
51
|
const policy = resolveAutoUpdatePolicy(process.env);
|
|
77
52
|
const decision = decideAutoUpdateHook(installedVersion, cache, policy);
|
|
78
|
-
if (decision.run)
|
|
79
|
-
await spawnAutoUpdate(decision.latest, _installChannelMod);
|
|
80
|
-
}
|
|
53
|
+
if (decision.run) await spawnAutoUpdate(decision.latest, installChannel);
|
|
81
54
|
} catch {
|
|
82
|
-
// Best-effort
|
|
55
|
+
// Best-effort: update failures must never break session capture.
|
|
83
56
|
}
|
|
84
57
|
}
|
|
85
58
|
|
|
@@ -183,13 +156,9 @@ function parseTranscript(transcriptPath) {
|
|
|
183
156
|
const text = typeof block.content === 'string'
|
|
184
157
|
? block.content
|
|
185
158
|
: JSON.stringify(block.content);
|
|
186
|
-
// Same reason as the bash branch
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
// header — the ordinary shape of an auth error — would be stored
|
|
190
|
-
// and then transmitted off the machine. Redacted once here, at
|
|
191
|
-
// the point the text enters the process, so every downstream use
|
|
192
|
-
// inherits it.
|
|
159
|
+
// Same reason as the bash branch: a failed request may echo its
|
|
160
|
+
// own Authorization header. Redact once, where text enters the
|
|
161
|
+
// process, so every downstream use inherits it.
|
|
193
162
|
errorsEncountered.push(redactSecrets(text).slice(0, 200));
|
|
194
163
|
}
|
|
195
164
|
}
|
|
@@ -338,33 +307,12 @@ process.stdin.on('end', async () => {
|
|
|
338
307
|
// healthy exit, so it MUST stamp (see stampHookRunOnly).
|
|
339
308
|
if (toolCallCount < 3) { stampHookRunOnly(process.env, 'session-summary'); return exit0(); }
|
|
340
309
|
|
|
341
|
-
// Hoisted to outer-try scope so the LLM failure-analysis block
|
|
342
|
-
// below (which runs AFTER db.close()) can reference it. Earlier
|
|
343
|
-
// version defined projectName inside the inner try-finally and the
|
|
344
|
-
// LLM path threw `projectName is not defined` silently — caught by
|
|
345
|
-
// the LLM try/catch but logged to stderr. Result: lesson_learned
|
|
346
|
-
// creation never actually happened in production.
|
|
347
310
|
const projectName = getProjectName(cwd);
|
|
348
311
|
|
|
349
312
|
// Open DB via shared helper — applies SCHEMA_SQL + status migration.
|
|
350
313
|
// { fts: true } guarantees the entities_fts table exists so captureEntity()
|
|
351
314
|
// can keep it in sync — session-insight memories must be FTS-recallable.
|
|
352
315
|
//
|
|
353
|
-
// sqlite-vec is NOT loaded here, and used to be. The comment said it was
|
|
354
|
-
// needed "for embedding-aware recall-effectiveness tracking" — but this
|
|
355
|
-
// hook runs exactly two statements, `PRAGMA table_info(entities)` and
|
|
356
|
-
// `SELECT id FROM entities WHERE name = ?`, and `captureEntity` in
|
|
357
|
-
// _shared.js touches no vectors either. Nothing here has ever used the
|
|
358
|
-
// extension.
|
|
359
|
-
//
|
|
360
|
-
// It was not free. sqlite-vec ships its engine as a per-platform file
|
|
361
|
-
// through optionalDependencies, and on a platform it does not publish the
|
|
362
|
-
// load threw — past the `require` guard, which never fired because the JS
|
|
363
|
-
// wrapper resolves fine and the throw happens later inside
|
|
364
|
-
// `sqliteVec.load()`. Measured with the platform binary hidden: the whole
|
|
365
|
-
// Stop capture vanished (0 entities against a control run's 1) and the
|
|
366
|
-
// user got a `Require stack:` dump on stderr. An extension nobody calls
|
|
367
|
-
// was silently costing every session on those platforms its memory.
|
|
368
316
|
const { db } = openHookDb(process.env, { fts: true });
|
|
369
317
|
let writeFailed = false;
|
|
370
318
|
try {
|
|
@@ -628,68 +576,17 @@ process.stdin.on('end', async () => {
|
|
|
628
576
|
db.close();
|
|
629
577
|
}
|
|
630
578
|
|
|
631
|
-
// ── LLM-powered failure analysis (Level 1 only) ──────────────────────
|
|
632
|
-
// Runs AFTER the hook's own DB is closed.
|
|
633
|
-
// Uses the core module's DB singleton (openDatabase/closeDatabase).
|
|
634
|
-
// Wrapped in its own try/catch — never blocks rule-based extraction.
|
|
635
|
-
if (errorsEncountered.length > 0 && filesEdited.length > 0) {
|
|
636
|
-
try {
|
|
637
|
-
// F5: derive pluginRoot strictly from this file's location.
|
|
638
|
-
// See `resolvePluginRoot` for the full reasoning.
|
|
639
|
-
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
640
|
-
const configMod = await importFromPluginRoot(pluginRoot, 'dist/core/config.js');
|
|
641
|
-
const config = configMod.readConfig();
|
|
642
|
-
|
|
643
|
-
if (config.llm) {
|
|
644
|
-
const { openDatabase, closeDatabase } = await importFromPluginRoot(pluginRoot, 'dist/db.js');
|
|
645
|
-
const { analyzeFailure } = await importFromPluginRoot(pluginRoot, 'dist/core/failure-analyzer.js');
|
|
646
|
-
const { createLesson } = await importFromPluginRoot(pluginRoot, 'dist/core/lesson-engine.js');
|
|
647
|
-
|
|
648
|
-
openDatabase();
|
|
649
|
-
try {
|
|
650
|
-
// Pass cross-provider failover chain so a stale Anthropic key
|
|
651
|
-
// doesn't silently disable Stop-hook lesson generation.
|
|
652
|
-
const lesson = await analyzeFailure(errorsEncountered, filesEdited, config.llm, { fallbacks: config.llmFallbacks });
|
|
653
|
-
if (lesson) {
|
|
654
|
-
createLesson(lesson, projectName);
|
|
655
|
-
}
|
|
656
|
-
} finally {
|
|
657
|
-
closeDatabase();
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
} catch (llmErr) {
|
|
661
|
-
// LLM analysis failed — rule-based extraction already captured the session.
|
|
662
|
-
// Log to stderr so config issues (e.g. invalid API key) are visible.
|
|
663
|
-
try { process.stderr.write(`[memesh] LLM failure analysis skipped: ${llmErr?.message || llmErr}\n`); } catch {}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Auto-trigger dream — solves the "Insights tab is empty for users
|
|
668
|
-
// who don't know `memesh dream run` exists" problem. Throttled to
|
|
669
|
-
// once per project per 24h, gated by minimum activity threshold.
|
|
670
|
-
// Background-detached spawn so the hook exits immediately even if
|
|
671
|
-
// the LLM call takes 30-60s. See `maybeTriggerDream` for the gate
|
|
672
|
-
// logic and dream-history.json schema.
|
|
673
|
-
try {
|
|
674
|
-
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
675
|
-
const configMod = await importFromPluginRoot(pluginRoot, 'dist/core/config.js');
|
|
676
|
-
const config = configMod.readConfig();
|
|
677
|
-
maybeTriggerDream(projectName, config, pluginRoot);
|
|
678
|
-
} catch (dreamErr) {
|
|
679
|
-
try { process.stderr.write(`[memesh] dream auto-trigger skipped: ${dreamErr?.message || dreamErr}\n`); } catch {}
|
|
680
|
-
}
|
|
681
579
|
} catch (err) {
|
|
682
580
|
// Never crash Claude Code — leave a trace for debugging.
|
|
683
581
|
//
|
|
684
|
-
// Every error is traced
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
// could only ever hide a real error from here on.
|
|
582
|
+
// Every error is traced. A retired suppression sentinel once hid setup
|
|
583
|
+
// failures from this hook; with that branch gone, real capture errors stay
|
|
584
|
+
// visible without crashing the host session.
|
|
688
585
|
try { process.stderr.write(`[memesh session-summary] ${err?.message || err}\n`); } catch {}
|
|
689
586
|
}
|
|
690
587
|
|
|
691
|
-
//
|
|
692
|
-
//
|
|
588
|
+
// Update only after all session work so installed files cannot change while
|
|
589
|
+
// this hook is still reading them.
|
|
693
590
|
await runAutoUpdateAtStop();
|
|
694
591
|
|
|
695
592
|
// Emit NOTHING on success — not `{"suppressOutput": true}`.
|
|
@@ -711,173 +608,6 @@ process.stdin.on('end', async () => {
|
|
|
711
608
|
function exit0() {
|
|
712
609
|
process.exit(0);
|
|
713
610
|
}
|
|
714
|
-
|
|
715
|
-
// =============================================================================
|
|
716
|
-
// Dream auto-trigger (Phase 2 / Phase 3 background runner)
|
|
717
|
-
// =============================================================================
|
|
718
|
-
//
|
|
719
|
-
// Without an automated trigger, `memesh dream` only runs when the user
|
|
720
|
-
// types it into a terminal — and most users never read the docs that
|
|
721
|
-
// far. Result: Insights tab stays empty and the KG accumulates 89.7%
|
|
722
|
-
// orphan rate (the maintainer's own observation on this DB).
|
|
723
|
-
//
|
|
724
|
-
// This trigger fires at the END of every Stop hook (after rule-based
|
|
725
|
-
// session capture + optional LLM failure analysis). It:
|
|
726
|
-
// 1. Loads ~/.memesh/dream-history.json — a per-project record of
|
|
727
|
-
// the last dream run timestamp + outcome.
|
|
728
|
-
// 2. Throttles: skip if < THROTTLE_HOURS since the project's last
|
|
729
|
-
// run, even if the previous run produced zero proposals.
|
|
730
|
-
// 3. Activity gate: skip if the project has < MIN_EPISODIC episodic
|
|
731
|
-
// entities to draw from in the last WINDOW_DAYS days.
|
|
732
|
-
// 4. LLM gate: skip if no LLM provider configured (Phase 2 needs
|
|
733
|
-
// Smart Mode — Phase 3 patterns same).
|
|
734
|
-
// 5. Spawns `node <pluginRoot>/dist/transports/cli/cli.js dream run
|
|
735
|
-
// --project <name> --max-llm-calls 2 --window-days 14` as a
|
|
736
|
-
// detached child process. Stdout/stderr go to a per-project log
|
|
737
|
-
// under ~/.memesh/dream-runs/ so the user can `tail -f` to see
|
|
738
|
-
// progress without the hook blocking.
|
|
739
|
-
// 6. Records the run start in dream-history.json BEFORE spawning so
|
|
740
|
-
// a long-running spawn doesn't get re-triggered on the next
|
|
741
|
-
// Stop within the same window.
|
|
742
|
-
//
|
|
743
|
-
// The spawned process inherits config from disk, including the
|
|
744
|
-
// `llmFallbacks` chain wired in commit 883abd4d, so a primary
|
|
745
|
-
// outage falls through to Ollama automatically.
|
|
746
|
-
|
|
747
|
-
const DREAM_THROTTLE_HOURS = 24;
|
|
748
|
-
const DREAM_MIN_EPISODIC = 10;
|
|
749
|
-
const DREAM_WINDOW_DAYS = 14;
|
|
750
|
-
const DREAM_MAX_LLM_CALLS = 2;
|
|
751
|
-
const DREAM_HISTORY_BASENAME = 'dream-history.json';
|
|
752
|
-
const DREAM_LOG_DIRNAME = 'dream-runs';
|
|
753
|
-
|
|
754
|
-
const DREAM_EPISODIC_TYPES = [
|
|
755
|
-
'commit',
|
|
756
|
-
'session_keypoint',
|
|
757
|
-
'session-insight',
|
|
758
|
-
'workflow_checkpoint',
|
|
759
|
-
'weekly-summary',
|
|
760
|
-
'weekly_summary',
|
|
761
|
-
];
|
|
762
|
-
|
|
763
|
-
// Debug-trace gate. Set MEMESH_DREAM_TRIGGER_DEBUG=1 to emit a stderr
|
|
764
|
-
// breadcrumb at every gate decision in maybeTriggerDream. Contract:
|
|
765
|
-
// [memesh dream-trigger] <stage>=<value>...
|
|
766
|
-
// Matches one line per stage (resolve, llm-gate, throttle-gate,
|
|
767
|
-
// activity-gate, history-write, spawn). Stable across releases — if
|
|
768
|
-
// you rename a stage, update the matching test fixture too.
|
|
769
|
-
function dreamTrigTrace(stage, fields) {
|
|
770
|
-
if (process.env.MEMESH_DREAM_TRIGGER_DEBUG !== '1') return;
|
|
771
|
-
try {
|
|
772
|
-
const parts = Object.entries(fields || {}).map(([k, v]) => {
|
|
773
|
-
if (v === undefined) return `${k}=<undef>`;
|
|
774
|
-
if (v === null) return `${k}=<null>`;
|
|
775
|
-
return `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`;
|
|
776
|
-
});
|
|
777
|
-
process.stderr.write(`[memesh dream-trigger] ${stage} ${parts.join(' ')}\n`);
|
|
778
|
-
} catch {}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function dreamHistoryPath() {
|
|
782
|
-
// Test isolation: tests set MEMESH_DB_PATH to a tmp file, expecting
|
|
783
|
-
// sibling state (history, logs) to land beside it. Without honouring
|
|
784
|
-
// that, real-home `~/.memesh/dream-history.json` would be polluted
|
|
785
|
-
// by every test run. Same precedence as memesh's other state files —
|
|
786
|
-
// see `getMemeshDirFromDbPath` in `_shared.js`.
|
|
787
|
-
//
|
|
788
|
-
// Windows note: on Windows under execFileSync, env vars propagate as
|
|
789
|
-
// plain strings (no canonicalisation). If a caller passed MEMESH_DIR
|
|
790
|
-
// with mixed separators (e.g. `C:/Users/.../tmp/.memesh`), join() will
|
|
791
|
-
// happily mix `/` and `\`. Node fs accepts either — the only divergence
|
|
792
|
-
// is in pure string comparisons, which we don't do here. The trace
|
|
793
|
-
// below shows the resolved value verbatim so a Windows diagnosis run
|
|
794
|
-
// can confirm what actually arrived.
|
|
795
|
-
// The helper IS the precedence (MEMESH_DB_PATH > MEMESH_DIR > home).
|
|
796
|
-
// A hand-rolled version here inverted it (MEMESH_DIR won over
|
|
797
|
-
// MEMESH_DB_PATH), so with both set, dream history landed in a different
|
|
798
|
-
// directory than every sibling state file — plus a dead home-fallback
|
|
799
|
-
// branch, since the helper always returns a string.
|
|
800
|
-
const dir = getMemeshDirFromDbPath();
|
|
801
|
-
dreamTrigTrace('resolve', {
|
|
802
|
-
src: process.env.MEMESH_DB_PATH ? 'db-path' : (process.env.MEMESH_DIR ? 'env' : 'home'),
|
|
803
|
-
MEMESH_DIR: process.env.MEMESH_DIR,
|
|
804
|
-
MEMESH_DB_PATH: process.env.MEMESH_DB_PATH,
|
|
805
|
-
dir,
|
|
806
|
-
platform: process.platform,
|
|
807
|
-
});
|
|
808
|
-
return { dir, historyFile: join(dir, DREAM_HISTORY_BASENAME), logDir: join(dir, DREAM_LOG_DIRNAME) };
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function readDreamHistory() {
|
|
812
|
-
try {
|
|
813
|
-
const { historyFile } = dreamHistoryPath();
|
|
814
|
-
if (!existsSync(historyFile)) return {};
|
|
815
|
-
const raw = readFileSync(historyFile, 'utf8');
|
|
816
|
-
const parsed = JSON.parse(raw);
|
|
817
|
-
return (parsed && typeof parsed === 'object') ? parsed : {};
|
|
818
|
-
} catch { return {}; }
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
function writeDreamHistory(history) {
|
|
822
|
-
try {
|
|
823
|
-
const { dir, historyFile } = dreamHistoryPath();
|
|
824
|
-
mkdirSync(dir, { recursive: true });
|
|
825
|
-
writeFileSync(historyFile, JSON.stringify(history, null, 2));
|
|
826
|
-
dreamTrigTrace('history-write', { historyFile, ok: true, projects: Object.keys(history) });
|
|
827
|
-
} catch (err) {
|
|
828
|
-
dreamTrigTrace('history-write', { ok: false, err: err?.message || String(err) });
|
|
829
|
-
try { process.stderr.write(`[memesh dream-history write] ${err?.message || err}\n`); } catch {}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/**
|
|
834
|
-
* Count episodic entities for the given project over the configured
|
|
835
|
-
* window. Only issues a SELECT, but the handle is openHookDb's ordinary
|
|
836
|
-
* read-write one (it runs the schema/migration chain) — there is no
|
|
837
|
-
* read-only variant on the hook side, and this call site must not stamp
|
|
838
|
-
* the heartbeat (recordHookRun is per-hook-exit, never per-open).
|
|
839
|
-
*/
|
|
840
|
-
function countEpisodicEntities(projectName) {
|
|
841
|
-
let handle;
|
|
842
|
-
try {
|
|
843
|
-
handle = openHookDb();
|
|
844
|
-
// { db, dbPath } — don't call db.prepare() on the wrapper itself (an
|
|
845
|
-
// earlier version did, and silently skipped the gate, defeating the
|
|
846
|
-
// trigger's whole purpose).
|
|
847
|
-
const db = handle.db;
|
|
848
|
-
const since = new Date(Date.now() - DREAM_WINDOW_DAYS * 86400000).toISOString();
|
|
849
|
-
const types = DREAM_EPISODIC_TYPES.map(() => '?').join(',');
|
|
850
|
-
// Project-membership is determined by the `project:<name>` tag only.
|
|
851
|
-
// The previous implementation also OR'd on `e.name LIKE 'project-%'`
|
|
852
|
-
// as a fallback for legacy entities, but that branch over-counts when
|
|
853
|
-
// two projects share a name prefix (e.g. counting `memesh` would
|
|
854
|
-
// sweep in `memesh-cloud-keypoint-*` entities). Post-v3.0 episodic
|
|
855
|
-
// entities all carry the project tag — the auto-tagger writes it on
|
|
856
|
-
// every session_keypoint / commit / session_insight / session_lesson
|
|
857
|
-
// / failure_pattern / decision_anchor — so the tag-only path is the
|
|
858
|
-
// accurate signal. See v4.2.1 CHANGELOG known-limitations note.
|
|
859
|
-
const sql = `SELECT COUNT(DISTINCT e.id) AS n
|
|
860
|
-
FROM entities e
|
|
861
|
-
INNER JOIN tags t ON t.entity_id = e.id AND t.tag = ?
|
|
862
|
-
WHERE e.status = 'active'
|
|
863
|
-
AND e.type IN (${types})
|
|
864
|
-
AND e.created_at >= ?`;
|
|
865
|
-
const projectTag = `project:${projectName}`;
|
|
866
|
-
const row = db.prepare(sql).get(projectTag, ...DREAM_EPISODIC_TYPES, since);
|
|
867
|
-
return row?.n ?? 0;
|
|
868
|
-
} catch (err) {
|
|
869
|
-
try { process.stderr.write(`[memesh dream-trigger count] ${err?.message || err}\n`); } catch {}
|
|
870
|
-
return 0;
|
|
871
|
-
} finally {
|
|
872
|
-
try { handle?.db?.close(); } catch {}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* Decide whether to fire dream for `projectName` and, if so, spawn
|
|
878
|
-
* the detached background runner. Pure side effect — no return value
|
|
879
|
-
* used by callers.
|
|
880
|
-
*/
|
|
881
611
|
/**
|
|
882
612
|
* Attachment record types Claude Code uses to persist a hook's own output
|
|
883
613
|
* into the transcript. Anything memesh injected reaches the transcript
|
|
@@ -927,88 +657,3 @@ export function stripHookEchoes(rawTranscript) {
|
|
|
927
657
|
}
|
|
928
658
|
return kept.join('\n');
|
|
929
659
|
}
|
|
930
|
-
|
|
931
|
-
export function maybeTriggerDream(projectName, config, pluginRoot) {
|
|
932
|
-
dreamTrigTrace('enter', { projectName, hasLlm: Boolean(config?.llm) });
|
|
933
|
-
if (!projectName || projectName === 'unknown') {
|
|
934
|
-
dreamTrigTrace('exit', { reason: 'no-project-name', projectName });
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// Phase 2 & 3 both need a configured LLM. Without it the dreamer's
|
|
939
|
-
// first action is to push `{reason: 'no LLM configured'}` into
|
|
940
|
-
// skipped[] and exit, which would still bump our throttle clock
|
|
941
|
-
// for no value — so gate here.
|
|
942
|
-
if (!config?.llm) {
|
|
943
|
-
dreamTrigTrace('exit', { reason: 'llm-gate-fail' });
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
dreamTrigTrace('llm-gate', { ok: true });
|
|
947
|
-
|
|
948
|
-
const history = readDreamHistory();
|
|
949
|
-
const last = history[projectName];
|
|
950
|
-
if (last?.last_run_iso) {
|
|
951
|
-
const ageMs = Date.now() - new Date(last.last_run_iso).getTime();
|
|
952
|
-
if (Number.isFinite(ageMs) && ageMs < DREAM_THROTTLE_HOURS * 3600 * 1000) {
|
|
953
|
-
dreamTrigTrace('exit', { reason: 'throttle-gate-fail', ageMs, last_run_iso: last.last_run_iso });
|
|
954
|
-
return; // throttled
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
dreamTrigTrace('throttle-gate', { ok: true, prior_last_run_iso: last?.last_run_iso });
|
|
958
|
-
|
|
959
|
-
const episodicCount = countEpisodicEntities(projectName);
|
|
960
|
-
if (episodicCount < DREAM_MIN_EPISODIC) {
|
|
961
|
-
dreamTrigTrace('exit', { reason: 'activity-gate-fail', episodicCount, threshold: DREAM_MIN_EPISODIC });
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
dreamTrigTrace('activity-gate', { ok: true, episodicCount });
|
|
965
|
-
|
|
966
|
-
// Activity gate passed — record start BEFORE spawning so we don't
|
|
967
|
-
// re-trigger on any subsequent Stop within the window even if the
|
|
968
|
-
// child takes 30-60s.
|
|
969
|
-
history[projectName] = {
|
|
970
|
-
last_run_iso: new Date().toISOString(),
|
|
971
|
-
last_episodic_count: episodicCount,
|
|
972
|
-
last_window_days: DREAM_WINDOW_DAYS,
|
|
973
|
-
};
|
|
974
|
-
writeDreamHistory(history);
|
|
975
|
-
|
|
976
|
-
// Spawn detached so the hook exits immediately. Stdio routes to a
|
|
977
|
-
// per-project log file so the user can inspect dream output without
|
|
978
|
-
// the hook blocking on the LLM call.
|
|
979
|
-
try {
|
|
980
|
-
const { dir, logDir } = dreamHistoryPath();
|
|
981
|
-
mkdirSync(logDir, { recursive: true });
|
|
982
|
-
const safeProj = projectName.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 64);
|
|
983
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
984
|
-
const logFile = join(logDir, `${safeProj}-${ts}.log`);
|
|
985
|
-
|
|
986
|
-
// Append a header so a tail -f shows context for the run.
|
|
987
|
-
appendFileSync(logFile, `[memesh dream] ${new Date().toISOString()} project=${projectName} episodic_count=${episodicCount} window=${DREAM_WINDOW_DAYS}d max_llm_calls=${DREAM_MAX_LLM_CALLS}\n`);
|
|
988
|
-
|
|
989
|
-
const cliPath = join(pluginRoot, 'dist/transports/cli/cli.js');
|
|
990
|
-
if (!existsSync(cliPath)) {
|
|
991
|
-
appendFileSync(logFile, `[memesh dream] cli.js missing at ${cliPath}, skipping\n`);
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
const args = [
|
|
996
|
-
cliPath, 'dream', 'run',
|
|
997
|
-
'--project', projectName,
|
|
998
|
-
'--max-llm-calls', String(DREAM_MAX_LLM_CALLS),
|
|
999
|
-
'--window-days', String(DREAM_WINDOW_DAYS),
|
|
1000
|
-
];
|
|
1001
|
-
|
|
1002
|
-
const logFd = require('fs').openSync(logFile, 'a');
|
|
1003
|
-
const child = spawn(process.execPath, args, {
|
|
1004
|
-
detached: true,
|
|
1005
|
-
stdio: ['ignore', logFd, logFd],
|
|
1006
|
-
env: { ...process.env, MEMESH_DIR: dir },
|
|
1007
|
-
});
|
|
1008
|
-
child.unref();
|
|
1009
|
-
dreamTrigTrace('spawn', { ok: true, pid: child.pid, logFile, MEMESH_DIR: dir });
|
|
1010
|
-
} catch (err) {
|
|
1011
|
-
dreamTrigTrace('spawn', { ok: false, err: err?.message || String(err) });
|
|
1012
|
-
try { process.stderr.write(`[memesh dream-trigger spawn] ${err?.message || err}\n`); } catch {}
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
@@ -30,6 +30,54 @@
|
|
|
30
30
|
|
|
31
31
|
set -uo pipefail
|
|
32
32
|
|
|
33
|
+
# D9: removes every OTHER stale version directory under $root, not just the
|
|
34
|
+
# one an upgrade just swapped out. Before the atomic-swap rename elsewhere in
|
|
35
|
+
# this script, an interrupted or pre-this-mechanism upgrade could leave a
|
|
36
|
+
# version directory behind with nothing left to remove it later — measured
|
|
37
|
+
# on a real machine: 9 old version directories, 1.2 GB, accumulated with no
|
|
38
|
+
# bound. Only entries whose full name is exactly `<major>.<minor>.<patch>`
|
|
39
|
+
# are touched, so a `.staging-*`/`.previous-*` marker from a genuinely
|
|
40
|
+
# concurrent run (should be impossible under $LOCK_DIR, but this check does
|
|
41
|
+
# not rely on that) or anything else unexpected under $root is left alone.
|
|
42
|
+
# $keep_version is always excluded; an optional $also_keep protects a second
|
|
43
|
+
# name — the registry's OWN recorded install path (however it is spelled),
|
|
44
|
+
# so the noncanonical-path repair in section 2 below can keep leaving that
|
|
45
|
+
# one directory alone for a human to clean up, exactly as it already did
|
|
46
|
+
# before this function existed.
|
|
47
|
+
#
|
|
48
|
+
# Defined this early, and callable on its own, so
|
|
49
|
+
# `tests/upgrade-plugin-cache-sweep.test.ts` can source this file with
|
|
50
|
+
# MEMESH_UPGRADE_PLUGIN_SOURCE_ONLY=1 (below) and call it directly against a
|
|
51
|
+
# throwaway root — testing the shipped function, not a hand-copied
|
|
52
|
+
# reimplementation of it — without running the rest of this script, which
|
|
53
|
+
# talks to a real Claude Code marketplace checkout.
|
|
54
|
+
sweep_stale_cache_versions() {
|
|
55
|
+
local root="$1" keep_version="$2" also_keep="${3:-}" entry name
|
|
56
|
+
[ -d "$root" ] || return 0
|
|
57
|
+
for entry in "$root"/*; do
|
|
58
|
+
[ -d "$entry" ] || continue
|
|
59
|
+
name="$(basename "$entry")"
|
|
60
|
+
[ "$name" = "$keep_version" ] && continue
|
|
61
|
+
[ -n "$also_keep" ] && [ "$name" = "$also_keep" ] && continue
|
|
62
|
+
if printf '%s' "$name" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
63
|
+
rm -rf "$entry" 2>/dev/null
|
|
64
|
+
if [ -e "$entry" ]; then
|
|
65
|
+
echo "WARNING: could not remove stale cached version at $entry — remove it manually." >&2
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
done
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Test-only escape hatch: source this file with this variable set to load
|
|
72
|
+
# `sweep_stale_cache_versions` (and any other function defined above this
|
|
73
|
+
# guard) without running the rest of the script. `return` exits a sourced
|
|
74
|
+
# file without killing the parent shell; `|| exit 0` is the fallback for the
|
|
75
|
+
# (unsupported, but harmless to guard) case of someone executing the script
|
|
76
|
+
# directly with the variable set.
|
|
77
|
+
if [ "${MEMESH_UPGRADE_PLUGIN_SOURCE_ONLY:-0}" = "1" ]; then
|
|
78
|
+
return 0 2>/dev/null || exit 0
|
|
79
|
+
fi
|
|
80
|
+
|
|
33
81
|
if [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
|
|
34
82
|
CLAUDE_CONFIG_ROOT="$CLAUDE_CONFIG_DIR"
|
|
35
83
|
elif [ -n "${HOME:-}" ]; then
|
|
@@ -282,12 +330,28 @@ ENTRY_SNAPSHOT="$(INSTALL_REGISTRY="$INSTALL_REGISTRY" CACHE_ROOT="$CACHE_ROOT"
|
|
|
282
330
|
&& path.resolve(entry.installPath) === expectedPath
|
|
283
331
|
? 'canonical'
|
|
284
332
|
: 'noncanonical';
|
|
285
|
-
|
|
333
|
+
// D9's cache sweep must never remove a directory the registry ITSELF
|
|
334
|
+
// still points to, canonical or not — this is the one directory the
|
|
335
|
+
// 'noncanonical … repairing it' path above (section 2) deliberately
|
|
336
|
+
// leaves in place. Reported as a bare basename, and only when that
|
|
337
|
+
// basename resolves back under this same cache root with no '..'
|
|
338
|
+
// segment, so a crafted absolute installPath from a tampered registry
|
|
339
|
+
// cannot smuggle an arbitrary path into the sweep's exclusion list.
|
|
340
|
+
const rootDir = path.resolve(process.env.CACHE_ROOT);
|
|
341
|
+
let recordedBasename = '';
|
|
342
|
+
if (typeof entry.installPath === 'string' && path.isAbsolute(entry.installPath)) {
|
|
343
|
+
const resolved = path.resolve(entry.installPath);
|
|
344
|
+
const base = path.basename(resolved);
|
|
345
|
+
if (path.join(rootDir, base) === resolved && !/[\\r\\n\\t]/.test(base)) {
|
|
346
|
+
recordedBasename = base;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
process.stdout.write(['selected', index, version, sha, registrySha256, installPathState, String(opened.dev), String(opened.ino), recordedBasename].join('\\t'));
|
|
286
350
|
")" || {
|
|
287
351
|
echo "ERROR: could not read the installed memesh entries from $INSTALL_REGISTRY" >&2
|
|
288
352
|
exit 1
|
|
289
353
|
}
|
|
290
|
-
IFS=$'\t' read -r ENTRY_STATE ENTRY_INDEX CURRENT_VERSION INSTALLED_SHA ORIGINAL_REGISTRY_SHA256 INSTALL_PATH_STATE ORIGINAL_REGISTRY_DEV ORIGINAL_REGISTRY_INO <<< "$ENTRY_SNAPSHOT"
|
|
354
|
+
IFS=$'\t' read -r ENTRY_STATE ENTRY_INDEX CURRENT_VERSION INSTALLED_SHA ORIGINAL_REGISTRY_SHA256 INSTALL_PATH_STATE ORIGINAL_REGISTRY_DEV ORIGINAL_REGISTRY_INO RECORDED_INSTALL_BASENAME <<< "$ENTRY_SNAPSHOT"
|
|
291
355
|
case "$ENTRY_STATE" in
|
|
292
356
|
identity-changed)
|
|
293
357
|
echo "ERROR: installed_plugins.json changed file identity while this upgrade was reading it — refusing to continue." >&2
|
|
@@ -620,6 +684,11 @@ if [ -e "$PREVIOUS_PATH" ] || [ -L "$PREVIOUS_PATH" ]; then
|
|
|
620
684
|
echo " Remove it manually when no Claude Code process is using it: rm -rf \"$PREVIOUS_PATH\"" >&2
|
|
621
685
|
fi
|
|
622
686
|
|
|
687
|
+
# D9: sweep every OTHER stale version directory under $CACHE_ROOT, not just
|
|
688
|
+
# the one this run just swapped out — see `sweep_stale_cache_versions`'s own
|
|
689
|
+
# definition near the top of this file for why.
|
|
690
|
+
sweep_stale_cache_versions "$CACHE_ROOT" "$NEW_VERSION" "$RECORDED_INSTALL_BASENAME"
|
|
691
|
+
|
|
623
692
|
# ─── 7. Done ─────────────────────────────────────────────────────────────
|
|
624
693
|
echo ""
|
|
625
694
|
echo "✓ MeMesh upgraded: $CURRENT_VERSION (${INSTALLED_SHA:0:8}) -> $NEW_VERSION (${MARKETPLACE_SHA:0:8})"
|