claude-mem-lite 3.52.0 → 3.54.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/README.md +27 -25
- package/hook.mjs +26 -4
- package/lib/citation-tracker.mjs +28 -2
- package/lib/deferred-work.mjs +3 -1
- package/lib/persist-reminder.mjs +12 -1
- package/lib/search-core.mjs +9 -5
- package/mem-cli.mjs +13 -1
- package/package.json +1 -1
- package/scripts/pre-agent-inject.js +2 -1
- package/scripts/pre-tool-recall.js +13 -0
- package/scripts/user-prompt-search.js +5 -4
- package/utils.mjs +8 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.54.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.54.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/README.md
CHANGED
|
@@ -684,41 +684,43 @@ and `benchmark/longmemeval-rerank.mjs` (rerank).
|
|
|
684
684
|
|
|
685
685
|
| Retriever (zero embeddings) | @1 | @5 | @10 |
|
|
686
686
|
|---|---|---|---|
|
|
687
|
-
| Lexical hybrid — FTS5 + TF-IDF + RRF |
|
|
688
|
-
| + one top-20 LLM rerank pass |
|
|
689
|
-
|
|
690
|
-
*n = 500 questions
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
687
|
+
| Lexical hybrid — FTS5 + TF-IDF + RRF | **83.4%** | **95.2%** | **96.0%** |
|
|
688
|
+
| + one top-20 LLM rerank pass † | 92.8% | 96.8% | 97.4% |
|
|
689
|
+
|
|
690
|
+
*n = 500 questions.* The lexical row was re-measured 2026-07-18: the v3.39–v3.45
|
|
691
|
+
alias/synonym-pipeline work lifted it from the previously published 76.8/90.6/95.2
|
|
692
|
+
on the **same harness and dataset** (both unchanged since that run — the gain is
|
|
693
|
+
engine-side, not metric drift). † The rerank row is the 2026-06 measurement taken
|
|
694
|
+
against the *older* lexical baseline; with lexical now at 95.2 @5 its remaining
|
|
695
|
+
headroom is ~1.6pt and a re-measurement is pending. The rerank pass hands the top
|
|
696
|
+
20 lexical candidates to a single Haiku call (~1.4 s/query) that reorders them; it
|
|
697
|
+
is **never worse than the lexical baseline by construction** — any LLM or parse
|
|
698
|
+
failure falls back to the original candidate order.
|
|
694
699
|
|
|
695
700
|
**Stricter metric, for the record.** The rows above are `recall_any@k` — does *any*
|
|
696
701
|
gold session reach the top *k* — the metric agentmemory and MemPalace publish, so the
|
|
697
702
|
comparison is like-for-like. Under the stricter **standard recall@k** (`|gold ∩ top-k| /
|
|
698
703
|
|gold|`, the *fraction* of all gold sessions retrieved), the lexical stack scores
|
|
699
|
-
@1 =
|
|
704
|
+
@1 = 52.9% / @5 = 87.8% / @10 = 91.0%. The whole gap is the 65% of questions with
|
|
700
705
|
multiple gold sessions — any-hit needs one, fractional needs them all, and @1 is capped
|
|
701
706
|
at 1/|gold| there; single-gold question types score identically under both.
|
|
702
707
|
`benchmark/longmemeval.mjs` reports both columns (the rerank row's fractional is not yet
|
|
703
708
|
measured).
|
|
704
709
|
|
|
705
|
-
**On embeddings, honestly.** With no LLM in the loop,
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
our
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
is
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
single-session-user 91.4 → 98.6 · temporal-reasoning 89.5 → 97.7 · multi-session
|
|
720
|
-
95.5 → 97.7 · single-session-assistant 83.9 → 94.6 · single-session-preference
|
|
721
|
-
63.3 → 80.0. Every category improves; none regress.
|
|
710
|
+
**On embeddings, honestly.** With no LLM in the loop, our zero-embedding lexical
|
|
711
|
+
stack now **ties** the BM25 + vector + graph hybrid (agentmemory, 95.2% @5) at the
|
|
712
|
+
same retrieval stage; a dense-embedding baseline (MemPalace, ~96.6% @5) still leads
|
|
713
|
+
by ~1.4pt. The remaining gap concentrates in paraphrase (single-session-preference
|
|
714
|
+
is our lowest category at 80.0% @5). The rerank row's point stands: a *single cheap
|
|
715
|
+
LLM call* reorders the top-20 lexical candidates because the candidate set is
|
|
716
|
+
already rich enough that ranking, not recall, is the bottleneck. An
|
|
717
|
+
embedding-plus-rerank stack still leads when both sides spend an LLM call; the
|
|
718
|
+
takeaway is that claude-mem-lite reaches embedding-competitive recall with **no
|
|
719
|
+
vector model, no knowledge graph, no Python, and no external service**.
|
|
720
|
+
|
|
721
|
+
Per-category any@5, lexical (2026-07-18 run): knowledge-update 100.0 ·
|
|
722
|
+
single-session-user 100.0 · multi-session 94.7 · single-session-assistant 94.6 ·
|
|
723
|
+
temporal-reasoning 94.0 · single-session-preference 80.0.
|
|
722
724
|
|
|
723
725
|
## Development
|
|
724
726
|
|
package/hook.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
extractErrorKeywords, extractFilePaths, isRelatedToEpisode,
|
|
28
28
|
makeEntryDesc, scrubSecrets, stripPrivate, EDIT_TOOLS, debugCatch, debugLog,
|
|
29
29
|
COMPRESSED_AUTO, OBS_BM25, notLowSignalTitleClause, formatErrorRecallHints,
|
|
30
|
+
MAX_HOOK_STDIN_BYTES,
|
|
30
31
|
} from './utils.mjs';
|
|
31
32
|
import {
|
|
32
33
|
readEpisodeRaw, episodeFile,
|
|
@@ -64,7 +65,7 @@ import { searchRelevantMemories, formatMemoryLine, selectImperativeLesson } from
|
|
|
64
65
|
import { searchInjectableEvents, renderInjectableEvent } from './lib/events-injection.mjs';
|
|
65
66
|
import { formatTaskImperative } from './lib/task-imperative.mjs';
|
|
66
67
|
import { recordSkillAdoption, gcOldShadowShards } from './registry-recommend.mjs';
|
|
67
|
-
import { gcOldMetricShards } from './lib/metrics.mjs';
|
|
68
|
+
import { gcOldMetricShards, recordMetric } from './lib/metrics.mjs';
|
|
68
69
|
import { detectMemOverride } from './lib/mem-override.mjs';
|
|
69
70
|
import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection, pickHandoffToInject, extractUnfinishedSummary } from './hook-handoff.mjs';
|
|
70
71
|
import { checkForUpdate, getCachedUpdateBanner, isUpdateCheckDue } from './hook-update.mjs';
|
|
@@ -433,6 +434,10 @@ function triggerErrorRecall(db, toolInput, response) {
|
|
|
433
434
|
|
|
434
435
|
const out = formatErrorRecallHints(rows);
|
|
435
436
|
if (out) {
|
|
437
|
+
// G13: this surface feeds the citation denominator but had zero metering —
|
|
438
|
+
// the G8 gate change (isError→isHardError) could not be volume-verified
|
|
439
|
+
// from metrics. Counter only; no latency (query is bundled in the hook).
|
|
440
|
+
recordMetric(join(RUNTIME_DIR, '..'), { event: 'error_recall', returned: rows.length });
|
|
436
441
|
// MED-3 (full audit 2026-07-16): emit via the JSON envelope (trailing '\n'),
|
|
437
442
|
// NOT raw stdout. A raw multi-line write corrupts a co-emitted episode-flush
|
|
438
443
|
// receipt (both write to PostToolUse stdout in the same call → `<text>{json}`)
|
|
@@ -668,7 +673,13 @@ async function handleStop() {
|
|
|
668
673
|
} else {
|
|
669
674
|
const citedMain = extractCitationsFromTranscript(transcriptPath, { mainOnly: true });
|
|
670
675
|
for (const id of citeBackIds) citedMain.add(id);
|
|
671
|
-
|
|
676
|
+
// D#60: the idempotency key must be the CC session UUID, NOT the
|
|
677
|
+
// project-scoped memory sessionId — concurrent same-project CC
|
|
678
|
+
// sessions share the latter, so the second session's decay pass
|
|
679
|
+
// read "already decided" and silently undercounted decay_seen /
|
|
680
|
+
// streaks / adoption denominators. Fallback keeps legacy
|
|
681
|
+
// stdin-less invocations on the old key.
|
|
682
|
+
const r = applyCitationDecay(db, project, injected, citedMain, ccSessionId || sessionId);
|
|
672
683
|
debugLog('DEBUG', 'handleStop', `citation-decay: touched=${r.touched} promoted=${r.promoted} demoted=${r.demoted}`);
|
|
673
684
|
// R1: persist this session's invocation→cite funnel row. touched =
|
|
674
685
|
// obs resolved this run (denominator), promoted = obs cited this run
|
|
@@ -1621,8 +1632,19 @@ async function handleEnrichSave(rawId) {
|
|
|
1621
1632
|
if (!db) return;
|
|
1622
1633
|
try {
|
|
1623
1634
|
const { executeSaveEnrich } = await import('./lib/save-enrich.mjs');
|
|
1624
|
-
await executeSaveEnrich(db, id);
|
|
1635
|
+
const result = await executeSaveEnrich(db, id);
|
|
1636
|
+
// G13: the worker's outcome was previously discarded — "spawned but did it
|
|
1637
|
+
// work" was invisible (32% alias coverage with 3 indistinguishable failure
|
|
1638
|
+
// causes). reason 'filled-concurrently' = txn ran but a concurrent optimize/
|
|
1639
|
+
// update had already filled every empty field.
|
|
1640
|
+
recordMetric(join(RUNTIME_DIR, '..'), {
|
|
1641
|
+
event: 'enrich_save',
|
|
1642
|
+
id,
|
|
1643
|
+
enriched: result.enriched,
|
|
1644
|
+
reason: result.reason ?? (result.enriched ? 'enriched' : 'filled-concurrently'),
|
|
1645
|
+
});
|
|
1625
1646
|
} catch (e) {
|
|
1647
|
+
recordMetric(join(RUNTIME_DIR, '..'), { event: 'enrich_save', id, enriched: false, reason: 'worker-error' });
|
|
1626
1648
|
debugCatch(e, 'enrich-save');
|
|
1627
1649
|
} finally {
|
|
1628
1650
|
try { db.close(); } catch {}
|
|
@@ -1666,7 +1688,7 @@ function handleAutoCompress() {
|
|
|
1666
1688
|
// ─── Utilities ──────────────────────────────────────────────────────────────
|
|
1667
1689
|
|
|
1668
1690
|
function readStdin() {
|
|
1669
|
-
const MAX_STDIN =
|
|
1691
|
+
const MAX_STDIN = MAX_HOOK_STDIN_BYTES; // large tool responses are truncated (shared tier, utils.mjs)
|
|
1670
1692
|
return new Promise((resolve, reject) => {
|
|
1671
1693
|
let data = '';
|
|
1672
1694
|
const timeout = setTimeout(() => { debugLog('WARN', 'readStdin', 'stdin timeout after 3s — event dropped'); process.stdin.destroy(); reject(new Error('timeout')); }, 3000);
|
package/lib/citation-tracker.mjs
CHANGED
|
@@ -599,9 +599,35 @@ export function applyCitationDecay(db, project, injectedIds, citedIds, sessionId
|
|
|
599
599
|
const empty = { promoted: 0, demoted: 0, touched: 0 };
|
|
600
600
|
if (process.env.MEM_DISABLE_CITATION_DECAY === '1') return empty;
|
|
601
601
|
if (!db || !project || !sessionId) return empty;
|
|
602
|
-
|
|
602
|
+
let injected = injectedIds instanceof Set ? injectedIds : new Set(injectedIds || []);
|
|
603
603
|
if (injected.size === 0) return empty;
|
|
604
|
-
|
|
604
|
+
let cited = citedIds instanceof Set ? citedIds : new Set(citedIds || []);
|
|
605
|
+
|
|
606
|
+
// D#61: a lesson injected live and then superseded mid-session (auto-dedup /
|
|
607
|
+
// supersedes= save) leaves its citation crediting NOBODY — selectStmt below
|
|
608
|
+
// excludes superseded rows by design (defense-in-depth parity), so the keeper
|
|
609
|
+
// that now carries the lesson goes uncredited. Redirect such ids to their
|
|
610
|
+
// NUMERIC superseded_by keeper (one hop; superseded_by is polymorphic — the
|
|
611
|
+
// typeof guard mirrors timeline-core). Copies, not mutation: callers own the
|
|
612
|
+
// input sets.
|
|
613
|
+
const redirectStmt = db.prepare(
|
|
614
|
+
'SELECT superseded_by FROM observations WHERE id = ? AND project = ? AND superseded_at IS NOT NULL'
|
|
615
|
+
);
|
|
616
|
+
const redirectSet = (set) => {
|
|
617
|
+
const out = new Set();
|
|
618
|
+
for (const id of set) {
|
|
619
|
+
const r = redirectStmt.get(id, project);
|
|
620
|
+
if (r && typeof r.superseded_by === 'number' && Number.isInteger(r.superseded_by)
|
|
621
|
+
&& r.superseded_by > 0 && r.superseded_by !== id) {
|
|
622
|
+
out.add(r.superseded_by);
|
|
623
|
+
} else {
|
|
624
|
+
out.add(id);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return out;
|
|
628
|
+
};
|
|
629
|
+
injected = redirectSet(injected);
|
|
630
|
+
cited = redirectSet(cited);
|
|
605
631
|
|
|
606
632
|
// Adoption gate (snapshot taken before any mutation this run). Suppress only
|
|
607
633
|
// demotion; promotion always proceeds. Threshold overridable via env.
|
package/lib/deferred-work.mjs
CHANGED
|
@@ -61,7 +61,9 @@ export function listOpenWithOrdinal(db, project, limit = 10) {
|
|
|
61
61
|
// ─── G11: list age + stale refresh hint (roadmap 2026-07-18) ─────────────────
|
|
62
62
|
|
|
63
63
|
const DAY_MS = 86_400_000;
|
|
64
|
-
|
|
64
|
+
// Internal const (was exported at v3.51.0 birth with zero external importers —
|
|
65
|
+
// knip-baseline discipline: un-export rather than grow the unused-exports list).
|
|
66
|
+
const DEFER_STALE_DAYS = 30;
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
69
|
* Render one `defer list` row (shared by CLI cmdDeferList and MCP
|
package/lib/persist-reminder.mjs
CHANGED
|
@@ -48,6 +48,14 @@ const PERSIST_TOOL_NAMES = new Set([
|
|
|
48
48
|
'mcp__plugin_claude-mem-lite_mem-lite__mem_defer',
|
|
49
49
|
]);
|
|
50
50
|
const PERSIST_CLI_RE = /(?:cli\.mjs|claude-mem-lite)['"]?\s+(?:save\b|defer\s+add\b)/;
|
|
51
|
+
// G18: Skill-path persistence. /lesson /memory /bug land observations (or memdir
|
|
52
|
+
// files) just as deliberately as mem_save — without these, a session that
|
|
53
|
+
// finalized AND persisted via a skill still got the reminder (false positive).
|
|
54
|
+
// Skill values may be plugin-qualified ('claude-mem-lite:lesson') or bare.
|
|
55
|
+
const PERSIST_SKILL_RE = /(?:^|:)(?:lesson|memory|bug)$/;
|
|
56
|
+
// G18: memory-dir writes (~/.claude/projects/<slug>/memory/*.md, incl. MEMORY.md
|
|
57
|
+
// index updates) are the durable-layer persistence path for decisions.
|
|
58
|
+
const MEMDIR_WRITE_RE = /[\\/]\.claude[\\/]projects[\\/][^\\/]+[\\/]memory[\\/][^\\/]+\.md$/;
|
|
51
59
|
|
|
52
60
|
/**
|
|
53
61
|
* Count deliberate persistence calls (mem_save / mem_defer tool_use, CLI
|
|
@@ -68,7 +76,10 @@ export function countDeliberatePersistence(transcriptPath) {
|
|
|
68
76
|
for (const block of content) {
|
|
69
77
|
if (block.type !== 'tool_use') continue;
|
|
70
78
|
if (PERSIST_TOOL_NAMES.has(block.name)) { count++; continue; }
|
|
71
|
-
if (block.name === 'Bash' && PERSIST_CLI_RE.test(block.input?.command || '')) count++;
|
|
79
|
+
if (block.name === 'Bash' && PERSIST_CLI_RE.test(block.input?.command || '')) { count++; continue; }
|
|
80
|
+
if (block.name === 'Skill' && PERSIST_SKILL_RE.test(block.input?.skill || '')) { count++; continue; }
|
|
81
|
+
if ((block.name === 'Write' || block.name === 'Edit')
|
|
82
|
+
&& MEMDIR_WRITE_RE.test(block.input?.file_path || '')) count++;
|
|
72
83
|
}
|
|
73
84
|
}
|
|
74
85
|
return count;
|
package/lib/search-core.mjs
CHANGED
|
@@ -266,9 +266,11 @@ export function searchEventsFts(db, { ftsQuery, project = null, projectBoost = n
|
|
|
266
266
|
// ratio ≥ 0.5 → -0.75 comparable to the best: above neutral, below the leaders
|
|
267
267
|
// ratio ≥ 0.1 → -0.5 the MED-5 neutral mid
|
|
268
268
|
// ratio < 0.1 → -0.25 grazing match: sink it
|
|
269
|
-
// score === 0 rows
|
|
270
|
-
//
|
|
271
|
-
//
|
|
269
|
+
// score === 0 rows are excluded from normalization entirely and keep their 0,
|
|
270
|
+
// sorting last in the ascending merge (audit L3: the old clamp promoted a lone
|
|
271
|
+
// 0-score row to the neutral mid). Two producers emit score 0 — the prompts CJK
|
|
272
|
+
// LIKE fallback AND the obs type-list fallback below (both zero-confidence, both
|
|
273
|
+
// gated so they never coexist with scored rows of their own source).
|
|
272
274
|
const SINGLE_MATCH_BANDS = [
|
|
273
275
|
[1.0, -1.05],
|
|
274
276
|
[0.5, -0.75],
|
|
@@ -289,8 +291,10 @@ export function normalizeCrossSourceScores(results, sourceKey) {
|
|
|
289
291
|
const globalMaxAbs = scored.length ? Math.max(...scored.map((r) => Math.abs(r.score))) : 0;
|
|
290
292
|
for (const src of ['obs', 'session', 'prompt', 'event']) {
|
|
291
293
|
// score === 0 stays out: for multi-row sources 0/maxAbs would be 0 anyway, and a
|
|
292
|
-
// lone 0-score row must not be banded upward
|
|
293
|
-
//
|
|
294
|
+
// lone 0-score row must not be banded upward. Two 0-score producers exist (G19
|
|
295
|
+
// comment fix — "only the prompts LIKE fallback" was incomplete): the prompts CJK
|
|
296
|
+
// LIKE fallback (never coexists with real prompt FTS hits) and the obs type-list
|
|
297
|
+
// fallback (gated on results.length === 0, so it never coexists with ANY scored row).
|
|
294
298
|
const srcResults = results.filter((r) => r[sourceKey] === src && r.score !== null && r.score !== undefined && r.score !== 0);
|
|
295
299
|
if (srcResults.length === 0) continue;
|
|
296
300
|
if (srcResults.length === 1) {
|
package/mem-cli.mjs
CHANGED
|
@@ -50,7 +50,7 @@ import { buildSearchFtsQuery, parseDateBounds, parseDuration, coreRunSearchPipel
|
|
|
50
50
|
import { AUTO_MERGE_THRESHOLD } from './lib/dedup-constants.mjs';
|
|
51
51
|
import { countRecentHookErrors } from './lib/hook-telemetry.mjs';
|
|
52
52
|
import { computeCitationFunnelTrend } from './lib/citation-tracker.mjs';
|
|
53
|
-
import { aggregateMetrics } from './lib/metrics.mjs';
|
|
53
|
+
import { aggregateMetrics, readMetrics } from './lib/metrics.mjs';
|
|
54
54
|
import {
|
|
55
55
|
insertDeferred, listOpenWithOrdinal, dropDeferred,
|
|
56
56
|
resolveDeferredIds, closeDeferredItems,
|
|
@@ -1254,6 +1254,18 @@ async function cmdStats(db, args) {
|
|
|
1254
1254
|
const rrN = featAgg.reread_warn?.count ?? 0;
|
|
1255
1255
|
const metricsOn = process.env.CLAUDE_MEM_METRICS === '1';
|
|
1256
1256
|
out(` Feature injections (7d): 📄 file-intel ${fiN} · 🔁 reread-warn ${rrN}${(!metricsOn && fiN + rrN === 0) ? ' (set CLAUDE_MEM_METRICS=1 to record)' : ''}`);
|
|
1257
|
+
// G13: surface the recall/enrich metering so "did the worker succeed" is
|
|
1258
|
+
// readable from stats, not just raw jsonl. enrich-save shows ok/total; the
|
|
1259
|
+
// per-reason split (llm-null vs txn-failed …) stays a jq query on the jsonl.
|
|
1260
|
+
const prN = featAgg.pretool_recall?.count ?? 0;
|
|
1261
|
+
const erN = featAgg.error_recall?.count ?? 0;
|
|
1262
|
+
let esOk = 0, esN = 0;
|
|
1263
|
+
for (const r of readMetrics(DB_DIR, 7)) {
|
|
1264
|
+
if (r.event === 'enrich_save') { esN++; if (r.enriched) esOk++; }
|
|
1265
|
+
}
|
|
1266
|
+
if (prN + erN + esN > 0 || metricsOn) {
|
|
1267
|
+
out(` Recall metering (7d): 🧠 pretool ${prN} · ⛑ error-recall ${erN} · ✚ enrich-save ${esOk}/${esN} ok`);
|
|
1268
|
+
}
|
|
1257
1269
|
if (noiseRatio > 0.6 || lowSignalRatio > 0.3) out(' ⚠️ High noise ratio — consider running mem maintain / compress');
|
|
1258
1270
|
out('');
|
|
1259
1271
|
// Tier counts only live (uncompressed, non-superseded) observations — surface the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.54.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",
|
|
@@ -24,7 +24,8 @@ function readStdin() {
|
|
|
24
24
|
data += c;
|
|
25
25
|
// cap: agent prompts can be large. destroy() so the loop can drain and exit on
|
|
26
26
|
// its own (see the no-forced-exit note at the bottom) rather than streaming to
|
|
27
|
-
// the 1.5s timeout.
|
|
27
|
+
// the 1.5s timeout. 262144 = MAX_HOOK_STDIN_BYTES (utils.mjs) repeated as a
|
|
28
|
+
// literal ON PURPOSE: the default-off fast path above must stay import-free.
|
|
28
29
|
if (data.length > 262144) { clearTimeout(timer); try { process.stdin.destroy(); } catch { /* */ } resolve(data.slice(0, 262144)); }
|
|
29
30
|
});
|
|
30
31
|
process.stdin.on('end', () => { clearTimeout(timer); resolve(data); });
|
|
@@ -512,6 +512,19 @@ try {
|
|
|
512
512
|
// unreadable files. When present (first Read of a sizable file this session),
|
|
513
513
|
// it leads the injection, above any lessons.
|
|
514
514
|
const hasLessons = allRows.length > 0;
|
|
515
|
+
// G13: obs/event lesson recall is the largest injected_n contributor
|
|
516
|
+
// (session max observed: 105) yet had no firing counter — file_intel/
|
|
517
|
+
// reread_warn were metered while the actual #NN injections were invisible.
|
|
518
|
+
// Source split lets the D#78 per-surface attribution read obs vs events.
|
|
519
|
+
if (hasLessons) {
|
|
520
|
+
recordMetric(DATA_DIR, {
|
|
521
|
+
event: 'pretool_recall',
|
|
522
|
+
injected: allRows.length,
|
|
523
|
+
obs: allRows.filter(r => r.src === 'obs').length,
|
|
524
|
+
evt: allRows.filter(r => r.src === 'evt').length,
|
|
525
|
+
mode: isRead ? 'read' : 'edit',
|
|
526
|
+
});
|
|
527
|
+
}
|
|
515
528
|
const showFraming = hasLessons || Boolean(fileIntelLine)
|
|
516
529
|
|| (!isRead && process.env.CLAUDE_MEM_PRETOOL_NUDGE === '1');
|
|
517
530
|
if (showFraming) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Lightweight: only imports schema.mjs and utils.mjs, no MCP SDK
|
|
5
5
|
|
|
6
6
|
import { ensureDb, DB_DIR, REGISTRY_DB_PATH } from '../schema.mjs';
|
|
7
|
-
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, OBS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE, notLowSignalTitleClause, noisePenaltyClause, stripPrivate, neutralizeContextDelimiters } from '../utils.mjs';
|
|
7
|
+
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, OBS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE, notLowSignalTitleClause, noisePenaltyClause, stripPrivate, neutralizeContextDelimiters, MAX_UPS_PROMPT_BYTES } from '../utils.mjs';
|
|
8
8
|
import { citeFactorClause } from '../scoring-sql.mjs';
|
|
9
9
|
import { cjkPrecisionOk } from '../nlp.mjs';
|
|
10
10
|
import { writeFileSync, readFileSync, existsSync, renameSync } from 'fs';
|
|
@@ -446,11 +446,12 @@ function readStdin() {
|
|
|
446
446
|
process.stdin.setEncoding('utf8');
|
|
447
447
|
process.stdin.on('data', chunk => {
|
|
448
448
|
data += chunk;
|
|
449
|
-
// Cap
|
|
450
|
-
|
|
449
|
+
// Cap the prompt (#9494 huge-prompt guard) — deliberately tighter than the
|
|
450
|
+
// 256KB full-payload tier; both tiers live in utils.mjs (G19).
|
|
451
|
+
if (data.length > MAX_UPS_PROMPT_BYTES) {
|
|
451
452
|
process.stdin.destroy();
|
|
452
453
|
clearTimeout(timeout);
|
|
453
|
-
resolve(data.slice(0,
|
|
454
|
+
resolve(data.slice(0, MAX_UPS_PROMPT_BYTES));
|
|
454
455
|
}
|
|
455
456
|
});
|
|
456
457
|
process.stdin.on('end', () => { clearTimeout(timeout); resolve(data); });
|
package/utils.mjs
CHANGED
|
@@ -93,6 +93,14 @@ export function clampImportance(val) {
|
|
|
93
93
|
// Tools that produce file edits (used for significance detection, feedback, importance)
|
|
94
94
|
export const EDIT_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit']);
|
|
95
95
|
|
|
96
|
+
// Stdin caps for the hook entry points (G19). Two DELIBERATE tiers, not drift:
|
|
97
|
+
// full hook payloads carry tool_response bodies (256KB), while the UserPromptSubmit
|
|
98
|
+
// search surface caps the prompt itself at 64KB (#9494 huge-prompt guard — search
|
|
99
|
+
// relevance gains nothing past that). scripts/pre-agent-inject.js repeats the
|
|
100
|
+
// 256KB literal by design: its default-off path must stay import-free.
|
|
101
|
+
export const MAX_HOOK_STDIN_BYTES = 256 * 1024;
|
|
102
|
+
export const MAX_UPS_PROMPT_BYTES = 64 * 1024;
|
|
103
|
+
|
|
96
104
|
// Low-signal degraded title patterns — shared by hook-llm.mjs (dedup + importance cap) and hook-handoff.mjs (decision filter)
|
|
97
105
|
// Two top-level alternatives:
|
|
98
106
|
// 1. ^(prefix1|prefix2|...) — title starts with one of the hook-llm fallback prefixes
|