claude-mem-lite 3.22.0 → 3.23.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 +1 -1
- package/bash-utils.mjs +15 -1
- package/hook-llm.mjs +8 -1
- package/hook.mjs +16 -1
- package/lib/cite-back-hint.mjs +8 -2
- package/lib/compress-core.mjs +4 -0
- package/lib/maintain-core.mjs +24 -0
- package/mem-cli.mjs +15 -2
- package/package.json +1 -1
- package/server.mjs +12 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.23.0",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.23.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/bash-utils.mjs
CHANGED
|
@@ -13,6 +13,16 @@ const CMD_WRAPPERS = new Set(['sudo', 'doas', 'env', 'time', 'command', 'nice',
|
|
|
13
13
|
// git read subcommands whose output contains commit/log/match text, not failures.
|
|
14
14
|
const GIT_READ_SUBCMDS = new Set(['grep', 'log', 'show', 'diff', 'blame', 'ls-files', 'cat-file', 'whatchanged', 'shortlog', 'reflog', 'status']);
|
|
15
15
|
|
|
16
|
+
// Hard failure fingerprints — a real crash / thrown exception / non-zero-exit marker,
|
|
17
|
+
// as opposed to output that merely CONTAINS the word "error" (search results, log
|
|
18
|
+
// scans, prose). Deliberately strong/narrow: a JS stack frame (`\n at fn (…)`),
|
|
19
|
+
// panic/traceback/segfault, ENOENT/command-not-found, AssertionError, or a *named*
|
|
20
|
+
// error class (TypeError:/ReferenceError:/…). Generic `Error:`/`exception` are
|
|
21
|
+
// intentionally excluded — they appear too often in benign search/log output. Gates
|
|
22
|
+
// the bugfix-shape save-nudge (lib/cite-back-hint.mjs) so `node cli.mjs search "error"`
|
|
23
|
+
// + an edit in the same episode no longer looks like an unsaved fix.
|
|
24
|
+
const HARD_ERROR_RE = /\bERR!|\bpanic\b|traceback|segfault|core dumped|\benoent\b|command not found|assertion\s?error|\n\s+at\s+\S|(?:type|reference|range|syntax|eval|uri)error:/i;
|
|
25
|
+
|
|
16
26
|
// True when the command's PRIMARY operation (left of the first pipe, past any
|
|
17
27
|
// env-assignments / wrapper like `sudo`/`env`/`time`) is a read/search — including
|
|
18
28
|
// `git grep`/`git log`. Anchoring on the primary command (not "search verb appears
|
|
@@ -69,6 +79,10 @@ export function detectBashSignificance(input, response) {
|
|
|
69
79
|
const hasHardErrorSignal = hasGreenTestSummary
|
|
70
80
|
&& /\bERR!|panic|traceback|enoent|command not found|exception|AssertionError|TypeError:|SyntaxError:/i.test(response);
|
|
71
81
|
const isError = looksLikeError && !(hasGreenTestSummary && !hasHardErrorSignal);
|
|
82
|
+
// Strict subset of isError: a genuine failure fingerprint, not just the word "error"
|
|
83
|
+
// in benign output. Consumers that must avoid false positives (the bugfix-shape
|
|
84
|
+
// save-nudge) gate on this instead of isError.
|
|
85
|
+
const isHardError = isError && HARD_ERROR_RE.test(response);
|
|
72
86
|
// Match actual test runner invocations, not commands that merely reference "test" as a keyword
|
|
73
87
|
const isTest = /\b(npm\s+test|npm\s+run\s+test|yarn\s+test|pnpm\s+test|pnpm\s+run\s+test|bun\s+test|go\s+test|cargo\s+test)\b/i.test(cmd)
|
|
74
88
|
|| /\b(jest|pytest|vitest|mocha|cypress|playwright)\b/i.test(cmd);
|
|
@@ -85,7 +99,7 @@ export function detectBashSignificance(input, response) {
|
|
|
85
99
|
|| /\bgh\s+release\s+(?:create|edit|upload|delete)\b/i.test(cmd)
|
|
86
100
|
|| /\btwine\s+upload\b/i.test(cmd);
|
|
87
101
|
return {
|
|
88
|
-
isError, isTest, isBuild, isGit, isDeploy,
|
|
102
|
+
isError, isHardError, isTest, isBuild, isGit, isDeploy,
|
|
89
103
|
isSignificant: isError || isTest || isBuild || isGit || isDeploy,
|
|
90
104
|
};
|
|
91
105
|
}
|
package/hook-llm.mjs
CHANGED
|
@@ -803,9 +803,16 @@ ${actionList}`;
|
|
|
803
803
|
// retry's entire purpose (a recovered bugfix lesson would silently drop
|
|
804
804
|
// out of --importance 2 searches and the working tier). Gate the cap on
|
|
805
805
|
// the *effective* low-signal state, not the pre-retry flag.
|
|
806
|
+
// v3.23: cap the FILE-PATH heuristic's contribution at 2. computeRuleImportance
|
|
807
|
+
// returns 3 for any entry touching schema./migration/prisma/.env/.key paths; via the
|
|
808
|
+
// Math.max below that force-promoted ordinary has-lesson episodes to "critical" imp=3
|
|
809
|
+
// even when Haiku judged them 1-2 (audit: auto imp=3 = 34.8%, e.g. a thin-lesson edit
|
|
810
|
+
// to schema.mjs). Haiku's OWN importance can still reach 3 (genuine judgment); only the
|
|
811
|
+
// path heuristic is capped. The isLessonLowSignal branch still floors no-lesson
|
|
812
|
+
// non-decision autos at ≤1; manual mem_save uses a different path and is unaffected.
|
|
806
813
|
importance: isLessonLowSignal && !retryRecovered && parsed.type !== 'decision'
|
|
807
814
|
? Math.min(ruleImportance, 1)
|
|
808
|
-
: Math.max(ruleImportance, clampImportance(parsed.importance)),
|
|
815
|
+
: Math.max(Math.min(ruleImportance, 2), clampImportance(parsed.importance)),
|
|
809
816
|
lessonLearned,
|
|
810
817
|
searchAliases,
|
|
811
818
|
};
|
package/hook.mjs
CHANGED
|
@@ -47,7 +47,7 @@ import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObse
|
|
|
47
47
|
import { scrubRecord } from './lib/scrub-record.mjs';
|
|
48
48
|
import { formatHookError } from './lib/native-binding-hint.mjs';
|
|
49
49
|
import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
|
|
50
|
-
import { cleanupBroken, decayAndMarkIdle, boostAccessed, selectFuzzyDedupeIds, hardDeleteCandidateCount, purgeStale } from './lib/maintain-core.mjs';
|
|
50
|
+
import { cleanupBroken, decayAndMarkIdle, boostAccessed, selectFuzzyDedupeIds, hardDeleteCandidateCount, purgeStale, recoverOrphanedChildren } from './lib/maintain-core.mjs';
|
|
51
51
|
import { snapshotDb } from './lib/db-backup.mjs';
|
|
52
52
|
import {
|
|
53
53
|
extractCitationsFromTranscript,
|
|
@@ -286,6 +286,9 @@ async function handlePostToolUse() {
|
|
|
286
286
|
files,
|
|
287
287
|
ts: Date.now(),
|
|
288
288
|
isError: bashSig?.isError || false,
|
|
289
|
+
// isHardError gates the bugfix-shape save-nudge (lib/cite-back-hint.mjs): a real
|
|
290
|
+
// failure fingerprint, not just "error" appearing in search/log output.
|
|
291
|
+
isHardError: bashSig?.isHardError || false,
|
|
289
292
|
isSignificant: EDIT_TOOLS.has(tool_name) ||
|
|
290
293
|
bashSig?.isSignificant || false,
|
|
291
294
|
bashSig: bashSig || null,
|
|
@@ -711,6 +714,12 @@ function runSessionStartDbMutations(db, { sessionId, project, prevSessionId, now
|
|
|
711
714
|
WHERE COALESCE(compressed_into, 0) = 0
|
|
712
715
|
AND COALESCE(importance, 1) <= 1
|
|
713
716
|
AND COALESCE(injection_count, 0) = 0
|
|
717
|
+
-- v3.23: never auto-hide a row that carries a real lesson. compression folds
|
|
718
|
+
-- sources into a title-only summary (lesson lost), and COMPRESSED_AUTO hides the
|
|
719
|
+
-- row from search entirely. A low-importance obs whose lesson_learned is the
|
|
720
|
+
-- distilled value must stay findable (audit: 62 lessons buried this way). The 7d
|
|
721
|
+
-- noise block below already excludes lessons; this 30d block had drifted.
|
|
722
|
+
AND (lesson_learned IS NULL OR lesson_learned = '' OR lesson_learned = 'none')
|
|
714
723
|
AND created_at_epoch < ?
|
|
715
724
|
AND project = ?
|
|
716
725
|
`).run(autoCompressAge, project);
|
|
@@ -779,6 +788,12 @@ function runSessionStartAutoMaintain(db) {
|
|
|
779
788
|
const cleaned = cleanupBroken(db, mctx);
|
|
780
789
|
if (cleaned > 0) debugLog('DEBUG', 'auto-maintain', `cleaned ${cleaned} broken observations`);
|
|
781
790
|
|
|
791
|
+
// Self-heal legacy orphans: children whose compression keeper was hard-deleted
|
|
792
|
+
// before recoverChildrenOf existed are hidden + queue-less (unreachable). Resurface
|
|
793
|
+
// them so normal decay/GC handles them on merit. Non-destructive (un-hide only).
|
|
794
|
+
const orphansRecovered = recoverOrphanedChildren(db, mctx);
|
|
795
|
+
if (orphansRecovered > 0) debugLog('DEBUG', 'auto-maintain', `recovered ${orphansRecovered} orphaned compression children`);
|
|
796
|
+
|
|
782
797
|
const { decayed, idleMarked } = decayAndMarkIdle(db, mctx);
|
|
783
798
|
if (decayed > 0) debugLog('DEBUG', 'auto-maintain', `decayed ${decayed} stale observations`);
|
|
784
799
|
if (idleMarked > 0) debugLog('DEBUG', 'auto-maintain', `marked ${idleMarked} idle as pending-purge`);
|
package/lib/cite-back-hint.mjs
CHANGED
|
@@ -68,11 +68,17 @@ export function buildCiteBackHint(episode, cooldown) {
|
|
|
68
68
|
// the lib so all save-prompt hints share one home + the same wording rules.
|
|
69
69
|
//
|
|
70
70
|
// Detection (mirrors pre-v2.83 hook.mjs:194 heuristic):
|
|
71
|
-
// • has at least one entry with
|
|
71
|
+
// • has at least one entry with a HARD failure fingerprint (isHardError) — NOT just
|
|
72
|
+
// isError. `node cli.mjs search "error"`, `npx vitest` green output, or any command
|
|
73
|
+
// whose output merely MENTIONS "error"/"failed" sets isError=true but is not a fix;
|
|
74
|
+
// gating on isError nagged on non-fixes (audit: fired on a read-only session + a
|
|
75
|
+
// scratch write). isHardError requires a real crash/exception/stack (bash-utils.mjs).
|
|
72
76
|
// • has at least one entry using an edit tool
|
|
73
77
|
// • entries.length >= 3 (rules out single-typo fixes that don't need a lesson)
|
|
74
78
|
// Returns null when any condition fails or when no edited files are recoverable
|
|
75
79
|
// from the entry list (defensive — episodes flushed mid-tool can have empties).
|
|
80
|
+
// Legacy fallback: entries written before isHardError existed lack the field, so fall
|
|
81
|
+
// back to isError for them rather than silently regress the nudge to never-fire.
|
|
76
82
|
const MIN_BUGFIX_ENTRIES = 3;
|
|
77
83
|
const MAX_DISPLAY_FILES = 3;
|
|
78
84
|
|
|
@@ -86,7 +92,7 @@ export function buildUnsavedBugfixHint(episode) {
|
|
|
86
92
|
const editedFiles = new Set();
|
|
87
93
|
for (const e of entries) {
|
|
88
94
|
if (!e) continue;
|
|
89
|
-
if (e.isError) hasError = true;
|
|
95
|
+
if (e.isHardError !== undefined ? e.isHardError : e.isError) hasError = true;
|
|
90
96
|
if (EDIT_TOOLS.has(e.tool)) {
|
|
91
97
|
hasEdit = true;
|
|
92
98
|
for (const f of e.files || []) editedFiles.add(f);
|
package/lib/compress-core.mjs
CHANGED
|
@@ -40,6 +40,10 @@ export function selectCompressionCandidates(db, { cutoff, project = null, includ
|
|
|
40
40
|
FROM observations
|
|
41
41
|
WHERE COALESCE(importance, 1) <= 1
|
|
42
42
|
AND COALESCE(access_count, 0) = 0
|
|
43
|
+
-- v3.23: exclude rows carrying a real lesson — folding them into a title-only
|
|
44
|
+
-- weekly summary discards the lesson (the distilled value of a lessons store).
|
|
45
|
+
-- Mirrors the hook auto-compress lesson guard so neither path buries a lesson.
|
|
46
|
+
AND (lesson_learned IS NULL OR lesson_learned = '' OR lesson_learned = 'none')
|
|
43
47
|
AND created_at_epoch < ?
|
|
44
48
|
${compressedFilter}
|
|
45
49
|
${projectFilter}
|
package/lib/maintain-core.mjs
CHANGED
|
@@ -98,6 +98,25 @@ export function recoverChildrenOf(db, ids) {
|
|
|
98
98
|
).run(...ids, ...ids).changes;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// Resurface children orphaned by a keeper hard-deleted BEFORE recoverChildrenOf existed
|
|
102
|
+
// (legacy data). recoverChildrenOf only fires at delete time for the keepers being deleted
|
|
103
|
+
// in that call; rows whose keeper vanished in a past release are missed forever. A child
|
|
104
|
+
// with compressed_into = <positive keeperId> whose keeper row no longer exists is hidden
|
|
105
|
+
// from every COALESCE(compressed_into,0)=0 view AND sits in no maintenance queue (not
|
|
106
|
+
// COMPRESSED_AUTO, not COMPRESSED_PENDING_PURGE), so nothing ever resurfaces or GCs it —
|
|
107
|
+
// it leaks its full narrative out of reach. Setting compressed_into = NULL makes it live
|
|
108
|
+
// again; normal decay/GC then handles it on merit. `compressed_into > 0` excludes the
|
|
109
|
+
// negative sentinels (intentional states, not orphans). NON-DESTRUCTIVE: only un-hides
|
|
110
|
+
// rows, never deletes — safe to run unconditionally, no snapshot needed.
|
|
111
|
+
export function recoverOrphanedChildren(db, { projectFilter = '', baseParams = [] } = {}) {
|
|
112
|
+
return db.prepare(`
|
|
113
|
+
UPDATE observations SET compressed_into = NULL
|
|
114
|
+
WHERE compressed_into > 0
|
|
115
|
+
AND NOT EXISTS (SELECT 1 FROM observations k WHERE k.id = observations.compressed_into)
|
|
116
|
+
${projectFilter}
|
|
117
|
+
`).run(...baseParams).changes;
|
|
118
|
+
}
|
|
119
|
+
|
|
101
120
|
export function cleanupBroken(db, { projectFilter, baseParams, opCap = OP_CAP }) {
|
|
102
121
|
const doomed = db.prepare(`
|
|
103
122
|
SELECT id FROM observations
|
|
@@ -133,6 +152,11 @@ export function decayAndMarkIdle(db, { projectFilter, baseParams, staleAge, opCa
|
|
|
133
152
|
AND COALESCE(importance, 1) = 1
|
|
134
153
|
AND COALESCE(access_count, 0) = 0
|
|
135
154
|
AND COALESCE(injection_count, 0) = 0
|
|
155
|
+
-- v3.23: never mark a lesson-bearing row idle→pending-purge. A lesson is the
|
|
156
|
+
-- distilled value of a lessons store; auto-GC must not silently purge it (parity
|
|
157
|
+
-- with the compress lesson guards in hook.mjs + compress-core.mjs). Truly stale
|
|
158
|
+
-- lessons are removed by explicit delete, not background decay.
|
|
159
|
+
AND (lesson_learned IS NULL OR lesson_learned = '' OR lesson_learned = 'none')
|
|
136
160
|
AND created_at_epoch < ?
|
|
137
161
|
${projectFilter} LIMIT ${opCap}
|
|
138
162
|
)
|
package/mem-cli.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from '
|
|
|
18
18
|
import { buildNotLowSignalSql } from './lib/low-signal-patterns.mjs';
|
|
19
19
|
import {
|
|
20
20
|
cleanupBroken, decayAndMarkIdle, boostAccessed, demotePinned, mergeDuplicates,
|
|
21
|
+
recoverOrphanedChildren,
|
|
21
22
|
purgeStale, purgeStalePreview, findDuplicates, maintenanceStats, rebuildVectors, vacuum,
|
|
22
23
|
recoverChildrenOf, hardDeleteCandidateCount,
|
|
23
24
|
OP_CAP, STALE_AGE_MS, PINNED_INJ_THRESHOLD,
|
|
@@ -1096,9 +1097,17 @@ async function cmdStats(db, args) {
|
|
|
1096
1097
|
`SELECT AVG(COALESCE(importance,1)) as v FROM observations WHERE 1=1 ${projectFilter}`
|
|
1097
1098
|
).get(...baseParams);
|
|
1098
1099
|
const thirtyDaysAgo = now - 30 * 86400000;
|
|
1100
|
+
// v3.23 noise-gauge de-blinding: the prior `importance = 1` predicate was
|
|
1101
|
+
// structurally blind to imp=0 — decay's floor + the LLM low-signal filter push
|
|
1102
|
+
// dormant rows to 0, which on a real store is ~half the live corpus. The gauge
|
|
1103
|
+
// therefore reported "0.0% noise" while the store was dormant-heavy. `<= 1` makes
|
|
1104
|
+
// imp=0 visible; injection_count=0 mirrors decay's NEVER-INJECTED guard so an
|
|
1105
|
+
// injected-but-decayed row (pinned noise, tracked separately) is not miscounted
|
|
1106
|
+
// as "never used".
|
|
1099
1107
|
const lowVal = db.prepare(`
|
|
1100
1108
|
SELECT COUNT(*) as c FROM observations
|
|
1101
|
-
WHERE COALESCE(importance,1)
|
|
1109
|
+
WHERE COALESCE(importance,1) <= 1 AND COALESCE(access_count,0) = 0
|
|
1110
|
+
AND COALESCE(injection_count,0) = 0
|
|
1102
1111
|
AND COALESCE(compressed_into, 0) = 0
|
|
1103
1112
|
AND created_at_epoch < ? ${projectFilter}
|
|
1104
1113
|
`).get(thirtyDaysAgo, ...baseParams);
|
|
@@ -1197,7 +1206,7 @@ async function cmdStats(db, args) {
|
|
|
1197
1206
|
out('Data Health:');
|
|
1198
1207
|
out(` Est. tokens: ${tokenEst.t ?? 0}`);
|
|
1199
1208
|
out(` Avg importance: ${(avgImp.v ?? 1).toFixed(2)}`);
|
|
1200
|
-
out(` Low-value (imp
|
|
1209
|
+
out(` Low-value (imp≤1, never used, >30d): ${lowVal.c} (${(noiseRatio * 100).toFixed(1)}% noise)`);
|
|
1201
1210
|
out(` Low-signal titles (Modified/Error/Worked on…): ${lowSignalTitle.c} (${(lowSignalRatio * 100).toFixed(1)}%)`);
|
|
1202
1211
|
out(` Compressed: ${compressedCount.c}`);
|
|
1203
1212
|
out(` Hook errors (last 24h): ${hookErrors24h}${hookErrors24h > 0 ? ` ← tail ${join(DB_DIR, 'runtime/hook-errors')}` : ''}`);
|
|
@@ -1944,6 +1953,10 @@ function cmdMaintain(db, args) {
|
|
|
1944
1953
|
if (ops.includes('cleanup')) {
|
|
1945
1954
|
const deleted = cleanupBroken(db, mctx);
|
|
1946
1955
|
results.push(`Cleaned up ${deleted} broken observations${capHint(deleted)}`);
|
|
1956
|
+
// Self-heal legacy orphans (keeper hard-deleted pre-recoverChildrenOf): resurface
|
|
1957
|
+
// unreachable children. Non-destructive — un-hide only, no delete.
|
|
1958
|
+
const orphans = recoverOrphanedChildren(db, mctx);
|
|
1959
|
+
if (orphans > 0) results.push(`Recovered ${orphans} orphaned compression children`);
|
|
1947
1960
|
}
|
|
1948
1961
|
|
|
1949
1962
|
if (ops.includes('decay')) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.23.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
package/server.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { resolveAnchorToken, formatAnchorError, resolveQueryAnchor, fetchRecentT
|
|
|
17
17
|
import { buildSearchFtsQuery, parseDateBounds, coreRunSearchPipeline } from './lib/search-core.mjs';
|
|
18
18
|
import {
|
|
19
19
|
cleanupBroken, decayAndMarkIdle, boostAccessed, demotePinned, mergeDuplicates,
|
|
20
|
+
recoverOrphanedChildren,
|
|
20
21
|
purgeStale, purgeStalePreview, findDuplicates, maintenanceStats, rebuildVectors, vacuum,
|
|
21
22
|
recoverChildrenOf, hardDeleteCandidateCount,
|
|
22
23
|
OP_CAP, STALE_AGE_MS,
|
|
@@ -863,9 +864,14 @@ server.registerTool(
|
|
|
863
864
|
`).get(...baseParams);
|
|
864
865
|
|
|
865
866
|
const thirtyDaysAgo = Date.now() - 30 * 86400000;
|
|
867
|
+
// v3.23 noise-gauge de-blinding (mirrors mem-cli cmdStats): `<= 1` makes the
|
|
868
|
+
// imp=0 dormant population visible (decay floor + LLM low-signal filter push
|
|
869
|
+
// ~half the live corpus to 0); injection_count=0 mirrors decay's NEVER-INJECTED
|
|
870
|
+
// guard so injected-but-decayed pinned noise isn't miscounted as "never used".
|
|
866
871
|
const lowVal = db.prepare(`
|
|
867
872
|
SELECT COUNT(*) as c FROM observations
|
|
868
|
-
WHERE COALESCE(importance,1)
|
|
873
|
+
WHERE COALESCE(importance,1) <= 1 AND COALESCE(access_count,0) = 0
|
|
874
|
+
AND COALESCE(injection_count,0) = 0
|
|
869
875
|
AND COALESCE(compressed_into, 0) = 0
|
|
870
876
|
AND created_at_epoch < ? ${projectFilter}
|
|
871
877
|
`).get(thirtyDaysAgo, ...baseParams);
|
|
@@ -915,7 +921,7 @@ server.registerTool(
|
|
|
915
921
|
'Data Health:',
|
|
916
922
|
` Est. tokens: ${tokenEst.t ?? 0}`,
|
|
917
923
|
` Avg importance: ${(avgImp.v ?? 1).toFixed(2)}`,
|
|
918
|
-
` Low-value (imp
|
|
924
|
+
` Low-value (imp≤1, never used, >30d): ${lowVal.c} (${(noiseRatio * 100).toFixed(1)}% noise)`,
|
|
919
925
|
` Low-signal titles (Modified/Error/Worked on…): ${lowSignalTitle.c} (${(lowSignalRatio * 100).toFixed(1)}%)`,
|
|
920
926
|
` Compressed: ${compressedCount.c}`,
|
|
921
927
|
...((noiseRatio > 0.6 || lowSignalRatio > 0.3) ? [' ⚠️ High noise ratio — consider running mem_compress / maintain'] : []),
|
|
@@ -1109,6 +1115,10 @@ server.registerTool(
|
|
|
1109
1115
|
if (ops.includes('cleanup')) {
|
|
1110
1116
|
const deleted = cleanupBroken(db, mctx);
|
|
1111
1117
|
results.push(`Cleaned up ${deleted} broken observations` + (deleted >= OP_CAP ? ' (cap reached, re-run for more)' : ''));
|
|
1118
|
+
// Self-heal legacy orphans (keeper hard-deleted pre-recoverChildrenOf):
|
|
1119
|
+
// resurface unreachable children. Non-destructive — un-hide only, no delete.
|
|
1120
|
+
const orphans = recoverOrphanedChildren(db, mctx);
|
|
1121
|
+
if (orphans > 0) results.push(`Recovered ${orphans} orphaned compression children`);
|
|
1112
1122
|
}
|
|
1113
1123
|
|
|
1114
1124
|
if (ops.includes('decay')) {
|