@pcircle/memesh 4.6.1 → 4.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.de.md +22 -11
- package/README.md +22 -11
- package/README.zh-TW.md +21 -11
- package/dashboard/dist/index.html +6 -6
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js +1 -1
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/citation-rule.d.ts +24 -0
- package/dist/core/citation-rule.d.ts.map +1 -0
- package/dist/core/citation-rule.js +71 -0
- package/dist/core/citation-rule.js.map +1 -0
- package/dist/core/conflict-candidates.d.ts.map +1 -1
- package/dist/core/conflict-candidates.js +4 -12
- package/dist/core/conflict-candidates.js.map +1 -1
- package/dist/core/conflict-judge.d.ts +11 -0
- package/dist/core/conflict-judge.d.ts.map +1 -1
- package/dist/core/conflict-judge.js +1 -1
- package/dist/core/conflict-judge.js.map +1 -1
- package/dist/core/demo.d.ts.map +1 -1
- package/dist/core/demo.js +1 -1
- package/dist/core/demo.js.map +1 -1
- package/dist/core/digest-validator.d.ts.map +1 -1
- package/dist/core/digest-validator.js +3 -3
- package/dist/core/digest-validator.js.map +1 -1
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +100 -16
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +10 -6
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/install-channel.d.ts +2 -1
- package/dist/core/install-channel.d.ts.map +1 -1
- package/dist/core/install-channel.js +11 -3
- package/dist/core/install-channel.js.map +1 -1
- package/dist/core/install-hooks.d.ts +6 -0
- package/dist/core/install-hooks.d.ts.map +1 -1
- package/dist/core/install-hooks.js +0 -0
- package/dist/core/install-hooks.js.map +1 -1
- package/dist/core/kg-backfill.d.ts.map +1 -1
- package/dist/core/kg-backfill.js +1 -1
- package/dist/core/kg-backfill.js.map +1 -1
- package/dist/core/lesson-engine.d.ts.map +1 -1
- package/dist/core/lesson-engine.js +3 -3
- package/dist/core/lesson-engine.js.map +1 -1
- package/dist/core/lifecycle.js +2 -2
- package/dist/core/llm-client.d.ts.map +1 -1
- package/dist/core/llm-client.js +7 -3
- package/dist/core/llm-client.js.map +1 -1
- package/dist/core/llm-telemetry.d.ts.map +1 -1
- package/dist/core/llm-telemetry.js +5 -2
- package/dist/core/llm-telemetry.js.map +1 -1
- package/dist/core/operations.d.ts +2 -0
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +2 -2
- package/dist/core/operations.js.map +1 -1
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +3 -2
- package/dist/core/paths.js.map +1 -1
- package/dist/core/patterns.d.ts +0 -5
- package/dist/core/patterns.d.ts.map +1 -1
- package/dist/core/patterns.js +1 -43
- package/dist/core/patterns.js.map +1 -1
- package/dist/core/schema-export.js +1 -1
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +47 -9
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/signal-scorer.d.ts +0 -1
- package/dist/core/signal-scorer.d.ts.map +1 -1
- package/dist/core/signal-scorer.js +12 -8
- package/dist/core/signal-scorer.js.map +1 -1
- package/dist/core/transcript-extractor.d.ts +2 -0
- package/dist/core/transcript-extractor.d.ts.map +1 -1
- package/dist/core/transcript-extractor.js +10 -5
- package/dist/core/transcript-extractor.js.map +1 -1
- package/dist/core/types.d.ts +10 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/updater.d.ts.map +1 -1
- package/dist/core/updater.js +1 -1
- package/dist/core/updater.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1 -1
- package/dist/db.js.map +1 -1
- package/dist/knowledge-graph.d.ts +3 -1
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +44 -28
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/skills-manifest.json +20 -15
- package/dist/storage/schema.d.ts.map +1 -1
- package/dist/storage/schema.js +18 -22
- package/dist/storage/schema.js.map +1 -1
- package/dist/storage/vector-index.d.ts.map +1 -1
- package/dist/storage/vector-index.js +10 -4
- package/dist/storage/vector-index.js.map +1 -1
- package/dist/transports/cli/cli.js +105 -45
- 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 +56 -1
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +2 -2
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +15 -15
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +0 -1
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +2 -2
- package/dist/transports/schemas.js.map +1 -1
- package/hooks/hooks.json +4 -2
- package/llms-install.md +28 -0
- package/package.json +3 -2
- package/scripts/hooks/_generated/citation-rule.js +78 -0
- package/scripts/hooks/_generated/core-paths.js +3 -2
- package/scripts/hooks/_generated/schema.js +18 -22
- package/scripts/hooks/_shared.js +73 -4
- package/scripts/hooks/pre-compact.js +14 -7
- package/scripts/hooks/session-start.js +61 -2
- package/scripts/hooks/session-summary.js +41 -4
- package/scripts/upgrade-plugin.sh +9 -1
package/scripts/hooks/_shared.js
CHANGED
|
@@ -26,15 +26,18 @@ import { fileURLToPath, pathToFileURL } from 'url';
|
|
|
26
26
|
// Re-exported here so all 6 hooks keep importing these names from `_shared.js`
|
|
27
27
|
// unchanged.
|
|
28
28
|
import {
|
|
29
|
+
homeDir,
|
|
29
30
|
memeshDir,
|
|
30
31
|
getDbPath,
|
|
31
32
|
getMemeshDirFromDbPath,
|
|
32
33
|
getProjectName,
|
|
34
|
+
redactSecrets,
|
|
33
35
|
slugFromRemoteUrl,
|
|
34
36
|
} from './_generated/core-paths.js';
|
|
35
37
|
import { autoCaptureDecision } from './_generated/capture-flag.js';
|
|
36
38
|
export { assembleTopologyBlock, buildReferenceContext, extractCitedMemoryIds, DEFAULT_TOPOLOGY_BUDGET, SNIPPET_FETCH_CHARS, TOPOLOGY_CANDIDATE_CAP } from './_generated/work-topology.js';
|
|
37
39
|
export { matchingGuards, guardFromMetadata } from './_generated/guards.js';
|
|
40
|
+
export { writeCitationRule, citationRulePath, CITATION_RULE_BODY } from './_generated/citation-rule.js';
|
|
38
41
|
import { guardFromMetadata as guardFromMetadataLocal } from './_generated/guards.js';
|
|
39
42
|
|
|
40
43
|
/**
|
|
@@ -101,7 +104,16 @@ export function recordGuardFires(dbPath, lessonIds) {
|
|
|
101
104
|
} finally {
|
|
102
105
|
db.close();
|
|
103
106
|
}
|
|
104
|
-
} catch {
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// Never block the user's work — but say so. A read-only database file, a
|
|
109
|
+
// lost lock, or a schema drift silently stopped the fire counter, and
|
|
110
|
+
// guard ROI is judged on exactly this number: a guard that fires often
|
|
111
|
+
// and a guard whose counter never landed look identical in review. One
|
|
112
|
+
// line on stderr is what guard-check already does for its own failures.
|
|
113
|
+
try {
|
|
114
|
+
process.stderr.write(`[memesh guard-fires] not counted: ${err?.message || err}\n`);
|
|
115
|
+
} catch { /* stderr gone */ }
|
|
116
|
+
}
|
|
105
117
|
}
|
|
106
118
|
import { isAutoInjectable } from './_generated/work-topology.js';
|
|
107
119
|
export { parseTaskState, taskStateLines, taskStateName } from './_generated/task-state.js';
|
|
@@ -114,7 +126,7 @@ import {
|
|
|
114
126
|
tokenizeQuery,
|
|
115
127
|
} from './_generated/fts-index.js';
|
|
116
128
|
|
|
117
|
-
export { memeshDir, getDbPath, getMemeshDirFromDbPath, getProjectName, slugFromRemoteUrl };
|
|
129
|
+
export { homeDir, memeshDir, getDbPath, getMemeshDirFromDbPath, getProjectName, redactSecrets, slugFromRemoteUrl };
|
|
118
130
|
|
|
119
131
|
const require = createRequire(import.meta.url);
|
|
120
132
|
|
|
@@ -332,6 +344,10 @@ import {
|
|
|
332
344
|
// nothing. node:sqlite is part of the runtime: there is no binary to
|
|
333
345
|
// miss, so the failure mode and its whole recovery apparatus are gone.
|
|
334
346
|
|
|
347
|
+
/** See the pragma in `openHookDb` for why this is not the 30s the shared
|
|
348
|
+
* database class uses. */
|
|
349
|
+
const HOOK_BUSY_TIMEOUT_MS = 2000;
|
|
350
|
+
|
|
335
351
|
export function openHookDb(env = process.env, opts = {}) {
|
|
336
352
|
|
|
337
353
|
// Path helpers read process.env directly (no-arg). The `env` parameter
|
|
@@ -349,6 +365,23 @@ export function openHookDb(env = process.env, opts = {}) {
|
|
|
349
365
|
const db = new MemeshDatabase(dbPath, { allowExtension: true });
|
|
350
366
|
db.pragma('journal_mode = WAL');
|
|
351
367
|
db.pragma('foreign_keys = ON');
|
|
368
|
+
// A hook waits for a held write lock for less time than Claude Code will
|
|
369
|
+
// wait for the hook.
|
|
370
|
+
//
|
|
371
|
+
// `MemeshDatabase` sets `busy_timeout = 30000`, and that number is right
|
|
372
|
+
// for the processes it was chosen for: a 30k-vector `swapVectorGeneration`
|
|
373
|
+
// holds the write lock for ~9s, and the CLI, the MCP server and the HTTP
|
|
374
|
+
// server should WAIT for it rather than fail. A hook cannot. Its budget in
|
|
375
|
+
// `hooks/hooks.json` is 3s (UserPromptSubmit) to 10s (Stop, PreCompact),
|
|
376
|
+
// so a 30s wait has exactly one possible ending: the harness kills the
|
|
377
|
+
// hook. The capture is lost either way — the difference is that the user
|
|
378
|
+
// also gets a hook-timeout error, which is the failure mode that makes
|
|
379
|
+
// memesh something to switch off.
|
|
380
|
+
//
|
|
381
|
+
// 2s fits inside every budget with room for the hook's own work. On
|
|
382
|
+
// contention the capture is skipped quietly, this run's `hook_runs` stamp
|
|
383
|
+
// is not written, and `memesh doctor` reports the gap honestly.
|
|
384
|
+
db.pragma(`busy_timeout = ${HOOK_BUSY_TIMEOUT_MS}`);
|
|
352
385
|
// Bringing the schema current is a WRITE, and "cannot migrate" must not
|
|
353
386
|
// mean "cannot open": a database file that is read-only but behind on
|
|
354
387
|
// schema (a pre-upgrade backup, a permissions accident) dies on the
|
|
@@ -535,6 +568,23 @@ export { truncateTitle } from './_generated/title.js';
|
|
|
535
568
|
* @returns {{ id: number, isNew: boolean } | null} null if the row could not be resolved
|
|
536
569
|
*/
|
|
537
570
|
export function captureEntity(db, { name, type, observations = [], tags = [], title, metadata }) {
|
|
571
|
+
// One transaction, because this function performs six writes that only
|
|
572
|
+
// mean anything together: the entity row, its observations, its tags, and
|
|
573
|
+
// the contentless-FTS delete + insert that make them findable.
|
|
574
|
+
//
|
|
575
|
+
// Without it, a throw anywhere in the middle — a lock lost to the CLI, a
|
|
576
|
+
// full disk, an FTS corruption — committed the prefix and dropped the
|
|
577
|
+
// rest, and the two most likely resting places are both invisible:
|
|
578
|
+
// observations inserted with no FTS row (a memory that exists and can
|
|
579
|
+
// never be recalled), or the old FTS row deleted and the new one not
|
|
580
|
+
// written (a memory that just stopped being findable). Neither is
|
|
581
|
+
// retried, because the callers dedupe on the entity NAME existing —
|
|
582
|
+
// `INSERT OR IGNORE` reports "already there" on the next run and the
|
|
583
|
+
// half-written state is permanent.
|
|
584
|
+
return db.transaction(() => captureEntityInner(db, { name, type, observations, tags, title, metadata }))();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function captureEntityInner(db, { name, type, observations, tags, title, metadata }) {
|
|
538
588
|
// source_host provenance: these hooks only ever run under Claude Code (they
|
|
539
589
|
// are wired into ~/.claude/settings.json), so a hook-captured entity is by
|
|
540
590
|
// definition a claude-code capture. Stamped only on the INSERT — an OR
|
|
@@ -573,8 +623,11 @@ export function captureEntity(db, { name, type, observations = [], tags = [], ti
|
|
|
573
623
|
// it's corrupted — replace with {} and log the healing.
|
|
574
624
|
if (!meta && metaRow?.metadata) {
|
|
575
625
|
try {
|
|
626
|
+
// The id, not the name. The id is what a maintainer needs to look
|
|
627
|
+
// the row up; the name is user-authored content, and this line goes
|
|
628
|
+
// to a stderr stream the user may paste anywhere.
|
|
576
629
|
process.stderr.write(
|
|
577
|
-
`MeMesh: healed corrupted metadata for entity ${id}
|
|
630
|
+
`MeMesh: healed corrupted metadata for entity ${id}. ` +
|
|
578
631
|
`Original value was unparseable; replaced with {}.\n`,
|
|
579
632
|
);
|
|
580
633
|
} catch { /* stderr gone */ }
|
|
@@ -745,9 +798,25 @@ export function decideAutoUpdateHook(currentVersion, cache, policy) {
|
|
|
745
798
|
const policyAllows = (POLICY_RANK[policy] ?? 0) >= BUMP_RANK[bump];
|
|
746
799
|
if (policyAllows) return { run: true, latest, bump, deprecationOverride: false };
|
|
747
800
|
|
|
801
|
+
// The deprecation override does NOT apply when the policy is `off`.
|
|
802
|
+
//
|
|
803
|
+
// Look at what the override could ever do: it fires only for a `patch`
|
|
804
|
+
// bump, and any policy above `off` already permits a patch. So its ONLY
|
|
805
|
+
// effect was to defeat `off` — the one setting whose whole meaning is
|
|
806
|
+
// "never install anything without me asking".
|
|
807
|
+
//
|
|
808
|
+
// And its trigger is `currentVersionDeprecation`, a string the PUBLISHER
|
|
809
|
+
// writes into the npm registry. Anyone able to publish the package could
|
|
810
|
+
// therefore make every user who had turned auto-update OFF run a detached
|
|
811
|
+
// `npm install -g`, unattended, from a Stop hook. That is not a security
|
|
812
|
+
// override; it is a remote switch on a user's explicit refusal.
|
|
813
|
+
//
|
|
814
|
+
// A deprecated version still gets said out loud: `memesh doctor` escalates
|
|
815
|
+
// the update-status row to FAIL for it (there is a test named for that),
|
|
816
|
+
// and the session banner reports it. The user decides.
|
|
748
817
|
const deprecated = typeof cache.currentVersionDeprecation === 'string'
|
|
749
818
|
&& cache.currentVersionDeprecation.length > 0;
|
|
750
|
-
if (deprecated && bump === 'patch') {
|
|
819
|
+
if (deprecated && bump === 'patch' && policy !== 'off') {
|
|
751
820
|
return { run: true, latest, bump, deprecationOverride: true };
|
|
752
821
|
}
|
|
753
822
|
|
|
@@ -4,13 +4,20 @@ import { basename } from 'path';
|
|
|
4
4
|
import { existsSync, readFileSync } from 'fs';
|
|
5
5
|
import { AUTO_CAPTURE_TAG, captureEntity, getProjectName, isAutoCaptureEnabled, openHookDb, recordHookRun, truncateTitle } from './_shared.js';
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
// There is no in-process timeout guard, and its absence is deliberate.
|
|
8
|
+
//
|
|
9
|
+
// This file used to arm `setTimeout(() => process.exit(0), 10_000).unref()`.
|
|
10
|
+
// It could not fire. Everything after `stdin`'s `end` event is one
|
|
11
|
+
// synchronous block — no `await`, no callback — so the event loop never gets
|
|
12
|
+
// a turn between the handler starting and the process exiting, and a JS
|
|
13
|
+
// timer cannot interrupt a blocking SQLite call. The one window where it
|
|
14
|
+
// COULD have run is while stdin is still open, which is not where a hook
|
|
15
|
+
// hangs.
|
|
16
|
+
//
|
|
17
|
+
// The timeout that does work is external: `hooks/hooks.json` declares
|
|
18
|
+
// `"timeout": 10` for PreCompact, and the harness enforces it on the
|
|
19
|
+
// process. `openHookDb` additionally caps the SQLite lock wait at 2s so
|
|
20
|
+
// contention ends in a skipped capture rather than in that kill.
|
|
14
21
|
|
|
15
22
|
let input = '';
|
|
16
23
|
process.stdin.setEncoding('utf8');
|
|
@@ -27,7 +27,9 @@ import {
|
|
|
27
27
|
resolvePluginRoot,
|
|
28
28
|
resolveSessionLimit,
|
|
29
29
|
taskStateLines,
|
|
30
|
+
homeDir,
|
|
30
31
|
taskStateName,
|
|
32
|
+
writeCitationRule,
|
|
31
33
|
writePrivateJson,
|
|
32
34
|
} from './_shared.js';
|
|
33
35
|
import { MemeshDatabase } from './_generated/sqlite.js';
|
|
@@ -542,6 +544,36 @@ process.stdin.on('end', async () => {
|
|
|
542
544
|
const data = JSON.parse(input);
|
|
543
545
|
const projectName = getProjectName(data.cwd);
|
|
544
546
|
|
|
547
|
+
// Self-heal the citation contract.
|
|
548
|
+
//
|
|
549
|
+
// `install-hooks` writes it too, but a PLUGIN install never runs that
|
|
550
|
+
// command — and plugin is how most users arrive. Without this, the
|
|
551
|
+
// contract would reach only npm installs, which is the same shape as the
|
|
552
|
+
// bug it exists to fix: a mechanism that is correct on a path nobody
|
|
553
|
+
// takes. Idempotent (a byte-identical file is left alone), refuses to
|
|
554
|
+
// touch a file memesh did not write, and never blocks the session: a
|
|
555
|
+
// failure here traces and the hook carries on.
|
|
556
|
+
try {
|
|
557
|
+
// Scope comes from the install marker, NOT hardcoded to 'user'. A
|
|
558
|
+
// `--scope project` install keeps everything inside that project, and
|
|
559
|
+
// writing the contract to ~/.claude/rules/ anyway would leak it into
|
|
560
|
+
// every OTHER project on the machine — and survive
|
|
561
|
+
// `uninstall-hooks --scope project`, which only knows about the
|
|
562
|
+
// project path. No marker means a plugin install, which is user-level
|
|
563
|
+
// by construction.
|
|
564
|
+
let ruleScope = 'user';
|
|
565
|
+
try {
|
|
566
|
+
const markerPath = join(memeshHomeDir(), 'install-hooks.json');
|
|
567
|
+
if (existsSync(markerPath)) {
|
|
568
|
+
const marker = JSON.parse(readFileSync(markerPath, 'utf8'));
|
|
569
|
+
if (marker?.scope === 'project') ruleScope = 'project';
|
|
570
|
+
}
|
|
571
|
+
} catch { /* unreadable marker → user scope, the safe default */ }
|
|
572
|
+
writeCitationRule(ruleScope, homeDir(), data.cwd || process.cwd());
|
|
573
|
+
} catch (err) {
|
|
574
|
+
try { process.stderr.write(`[memesh session-start] citation rule: ${err?.message || err}\n`); } catch {}
|
|
575
|
+
}
|
|
576
|
+
|
|
545
577
|
// Clear per-session throttle files from previous session
|
|
546
578
|
try {
|
|
547
579
|
if (existsSync(throttlePath)) {
|
|
@@ -916,7 +948,19 @@ process.stdin.on('end', async () => {
|
|
|
916
948
|
// this line IS an instruction. One line is the entire write side of
|
|
917
949
|
// the injection-ROI signal; the Stop hook credits recall_hits only
|
|
918
950
|
// from these markers (self-reported: undercounts, never overcounts).
|
|
919
|
-
|
|
951
|
+
// The citation instruction used to be appended here, outside the
|
|
952
|
+
// fence, so it would read as an instruction rather than as data.
|
|
953
|
+
// It never worked: Claude Code wraps a hook's additionalContext in a
|
|
954
|
+
// system-reminder ending "you should not respond to this context
|
|
955
|
+
// unless it is highly relevant", so the whole block — instruction
|
|
956
|
+
// included — arrives as data. Measured on a real database:
|
|
957
|
+
// citation_sessions_total=4, sessions WITH a citation = 0.
|
|
958
|
+
//
|
|
959
|
+
// The contract now lives in `.claude/rules/memesh-citations.md`,
|
|
960
|
+
// which Claude Code loads as an instruction. Writing it is the
|
|
961
|
+
// self-heal below; the line here is gone rather than duplicated,
|
|
962
|
+
// because a per-session copy of an instruction that is read as data
|
|
963
|
+
// is a per-session cost with no effect.
|
|
920
964
|
}
|
|
921
965
|
|
|
922
966
|
// --- Record injected entity IDs for recall effectiveness tracking ---
|
|
@@ -1038,7 +1082,22 @@ process.stdin.on('end', async () => {
|
|
|
1038
1082
|
const lifecycleMod = await importFromPluginRoot(pluginRoot, 'dist/core/lifecycle.js');
|
|
1039
1083
|
dbMod.openDatabase();
|
|
1040
1084
|
try {
|
|
1041
|
-
|
|
1085
|
+
// Say what it did. This archives at least 20 of the user's memories
|
|
1086
|
+
// per week processed and the count was thrown away, so the one
|
|
1087
|
+
// operation in memesh that removes things from view was also the
|
|
1088
|
+
// only one that left no trace anywhere — not in the hook output, not
|
|
1089
|
+
// in doctor, not in the dashboard. Nothing to opt into: it runs at
|
|
1090
|
+
// most once a day and stays silent when it compresses nothing.
|
|
1091
|
+
const noise = lifecycleMod.compressWeeklyNoise(dbMod.getDatabase());
|
|
1092
|
+
if (noise && noise.compressed > 0) {
|
|
1093
|
+
try {
|
|
1094
|
+
process.stderr.write(
|
|
1095
|
+
`[memesh] archived ${noise.compressed} low-signal memor${noise.compressed === 1 ? 'y' : 'ies'} `
|
|
1096
|
+
+ `into ${noise.weeksProcessed} weekly summar${noise.weeksProcessed === 1 ? 'y' : 'ies'} `
|
|
1097
|
+
+ `(recover with \`memesh recall --include-archived\`)\n`,
|
|
1098
|
+
);
|
|
1099
|
+
} catch { /* stderr gone */ }
|
|
1100
|
+
}
|
|
1042
1101
|
} finally {
|
|
1043
1102
|
dbMod.closeDatabase();
|
|
1044
1103
|
}
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
isAutoCaptureEnabled,
|
|
37
37
|
openHookDb,
|
|
38
38
|
readUpdateCheckCache,
|
|
39
|
+
redactSecrets,
|
|
39
40
|
recordHookRun,
|
|
40
41
|
stampHookRunOnly,
|
|
41
42
|
resolveAutoUpdatePolicy,
|
|
@@ -117,7 +118,14 @@ function parseTranscript(transcriptPath) {
|
|
|
117
118
|
if (block.name === 'Bash') {
|
|
118
119
|
const cmd = block.input?.command ?? '';
|
|
119
120
|
if (typeof cmd === 'string' && cmd.length > 10 && !cmd.startsWith('ls') && !cmd.startsWith('cd')) {
|
|
120
|
-
|
|
121
|
+
// Redact BEFORE truncating. A bash command line is the single
|
|
122
|
+
// most likely place a credential appears in a transcript
|
|
123
|
+
// (`export ANTHROPIC_API_KEY=sk-...`, `curl -H "Authorization:
|
|
124
|
+
// Bearer ..."`), and this text is stored verbatim as an
|
|
125
|
+
// observation — a permanent, searchable, exportable copy.
|
|
126
|
+
// Truncating first would cut a token in half and leave the
|
|
127
|
+
// fragment unmatched by every pattern.
|
|
128
|
+
bashCommands.push(redactSecrets(cmd).slice(0, 100));
|
|
121
129
|
}
|
|
122
130
|
}
|
|
123
131
|
}
|
|
@@ -141,7 +149,14 @@ function parseTranscript(transcriptPath) {
|
|
|
141
149
|
const text = typeof block.content === 'string'
|
|
142
150
|
? block.content
|
|
143
151
|
: JSON.stringify(block.content);
|
|
144
|
-
|
|
152
|
+
// Same reason as the bash branch, and one more: this array is
|
|
153
|
+
// ALSO the payload `analyzeFailure` sends to the configured LLM
|
|
154
|
+
// provider. A failed request that echoes its own Authorization
|
|
155
|
+
// header — the ordinary shape of an auth error — would be stored
|
|
156
|
+
// and then transmitted off the machine. Redacted once here, at
|
|
157
|
+
// the point the text enters the process, so every downstream use
|
|
158
|
+
// inherits it.
|
|
159
|
+
errorsEncountered.push(redactSecrets(text).slice(0, 200));
|
|
145
160
|
}
|
|
146
161
|
}
|
|
147
162
|
|
|
@@ -511,8 +526,21 @@ process.stdin.on('end', async () => {
|
|
|
511
526
|
const updateHit = db.prepare(
|
|
512
527
|
'UPDATE entities SET recall_hits = COALESCE(recall_hits, 0) + 1 WHERE id = ?'
|
|
513
528
|
);
|
|
529
|
+
// Counted here, not recomputed below. The compliance
|
|
530
|
+
// numerator and `recall_hits` have to be the SAME
|
|
531
|
+
// measurement: `cited.size > 0` asked "did this transcript
|
|
532
|
+
// contain any [mem:N] at all", which counts a marker for an id
|
|
533
|
+
// this session never injected — one carried over from an
|
|
534
|
+
// earlier turn, or a number the agent invented — as compliance.
|
|
535
|
+
// The denominator counts sessions that received an injection,
|
|
536
|
+
// so the two halves of the rate were answering different
|
|
537
|
+
// questions.
|
|
538
|
+
let injectedAndCited = 0;
|
|
514
539
|
for (const id of entityIds) {
|
|
515
|
-
if (cited.has(id))
|
|
540
|
+
if (cited.has(id)) {
|
|
541
|
+
updateHit.run(id);
|
|
542
|
+
injectedAndCited++;
|
|
543
|
+
}
|
|
516
544
|
}
|
|
517
545
|
|
|
518
546
|
// Accounting-mode stamp (constant value, rewritten every
|
|
@@ -527,7 +555,16 @@ process.stdin.on('end', async () => {
|
|
|
527
555
|
ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT)`
|
|
528
556
|
);
|
|
529
557
|
bump.run('citation_sessions_total');
|
|
530
|
-
|
|
558
|
+
// Initialised unconditionally, then bumped. Writing it only on
|
|
559
|
+
// a citation made "zero sessions cited" and "this code never
|
|
560
|
+
// ran" the same absent key — and that is exactly what a real
|
|
561
|
+
// database showed on 2026-08-24: total=4, cited absent, with
|
|
562
|
+
// no way to tell a 0% compliance rate from a dead counter.
|
|
563
|
+
db.prepare(
|
|
564
|
+
`INSERT INTO memesh_metadata (key, value) VALUES ('citation_sessions_cited', '0')
|
|
565
|
+
ON CONFLICT(key) DO NOTHING`
|
|
566
|
+
).run();
|
|
567
|
+
if (injectedAndCited > 0) bump.run('citation_sessions_cited');
|
|
531
568
|
}
|
|
532
569
|
}
|
|
533
570
|
}
|
|
@@ -94,7 +94,15 @@ CURRENT_VERSION="$(INSTALL_REGISTRY="$INSTALL_REGISTRY" node -e "
|
|
|
94
94
|
const entries = (j.plugins && j.plugins['memesh@pcircle-memesh']) || [];
|
|
95
95
|
if (entries.length === 0) { process.stdout.write('none'); process.exit(0); }
|
|
96
96
|
process.stdout.write(entries[0].version || 'unknown');
|
|
97
|
-
")"
|
|
97
|
+
")" || {
|
|
98
|
+
# Its sibling twelve lines up has this guard; this read did not, so an
|
|
99
|
+
# unreadable or malformed installed_plugins.json made CURRENT_VERSION the
|
|
100
|
+
# empty string. That compares unequal to every target, so the script
|
|
101
|
+
# reported an upgrade from "" and carried on — on a registry it had just
|
|
102
|
+
# failed to parse.
|
|
103
|
+
echo "ERROR: could not read the installed memesh version from $INSTALL_REGISTRY" >&2
|
|
104
|
+
exit 1
|
|
105
|
+
}
|
|
98
106
|
|
|
99
107
|
echo "==> Currently installed: $CURRENT_VERSION"
|
|
100
108
|
|