claude-mem-lite 3.63.0 → 3.65.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/deep-search.mjs +2 -1
- package/hook-context.mjs +23 -18
- package/hook-handoff.mjs +3 -4
- package/hook-memory.mjs +6 -9
- package/hook-optimize.mjs +4 -6
- package/hook-precompact.mjs +15 -2
- package/hook-shared.mjs +6 -0
- package/hook.mjs +59 -31
- package/lib/browse-core.mjs +67 -0
- package/lib/delete-core.mjs +18 -1
- package/lib/get-core.mjs +33 -0
- package/lib/inject-search-core.mjs +87 -0
- package/lib/injected-ids.mjs +49 -0
- package/lib/maintain-core.mjs +3 -3
- package/lib/observation-write.mjs +38 -2
- package/lib/recall-core.mjs +2 -2
- package/lib/recent-core.mjs +3 -1
- package/lib/search-core.mjs +14 -4
- package/lib/stats-core.mjs +2 -1
- package/lib/timeline-core.mjs +7 -6
- package/mem-cli.mjs +75 -141
- package/npm-shrinkwrap.json +2 -2
- package/package.json +5 -1
- package/registry.mjs +53 -1
- package/scoring-sql.mjs +5 -4
- package/scripts/pre-skill-bridge.js +3 -1
- package/scripts/pre-tool-recall.js +13 -9
- package/scripts/prompt-search-utils.mjs +3 -0
- package/scripts/user-prompt-search.js +26 -25
- package/search-engine.mjs +21 -15
- package/search-scoring.mjs +2 -2
- package/server.mjs +43 -112
- package/source-files.mjs +14 -0
- package/tfidf.mjs +3 -3
package/mem-cli.mjs
CHANGED
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { ensureDbWithWalRecovery, DB_PATH, DB_DIR, REGISTRY_DB_PATH } from './schema.mjs';
|
|
7
|
-
import { truncate, typeIcon, inferProject, scrubSecrets } from './utils.mjs';
|
|
7
|
+
import { truncate, typeIcon, inferProject, scrubSecrets, COMPRESSED_PENDING_PURGE } from './utils.mjs';
|
|
8
8
|
import { resolveProject } from './project-utils.mjs';
|
|
9
|
-
import { TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
10
9
|
import { _resetVocabCache, vecTextForRow, vectorsEnabled } from './tfidf.mjs';
|
|
11
|
-
import {
|
|
10
|
+
import { reRankWithContext } from './search-scoring.mjs';
|
|
12
11
|
import { searchObservationsHybrid } from './search-engine.mjs';
|
|
12
|
+
import { fetchObsDetail, OBS_FIELDS, SESSION_DETAIL_FIELDS } from './lib/get-core.mjs';
|
|
13
|
+
import { collectBrowseTiers, getActiveMemorySessionId, BROWSE_TIERS, BROWSE_TIER_LABELS } from './lib/browse-core.mjs';
|
|
13
14
|
import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
|
|
14
|
-
import { ensureRegistryDb, upsertResource } from './registry.mjs';
|
|
15
|
+
import { ensureRegistryDb, upsertResource, collectRegistryStats, listResourcesRanked, formatRegistryListLine } from './registry.mjs';
|
|
15
16
|
import { searchResources } from './registry-retriever.mjs';
|
|
16
17
|
import { computeFunnel, formatFunnel, computeSweep, formatSweep, DEFAULT_SWEEP_FLOORS, DEFAULT_SWEEP_MARGINS } from './registry-recommend.mjs';
|
|
17
18
|
import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
OP_CAP, STALE_AGE_MS, PINNED_INJ_THRESHOLD,
|
|
24
25
|
} from './lib/maintain-core.mjs';
|
|
25
26
|
import { snapshotDb, listSnapshots, backupBudgetBytes } from './lib/db-backup.mjs';
|
|
26
|
-
import { deleteObservations } from './lib/delete-core.mjs';
|
|
27
|
+
import { deleteObservations, previewDeleteRows } from './lib/delete-core.mjs';
|
|
27
28
|
import { OBS_TYPE_SET } from './lib/obs-types.mjs';
|
|
28
29
|
import { computeStatsFeed } from './lib/stats-core.mjs';
|
|
29
30
|
import { buildLessonNudge } from './lib/save-nudge.mjs';
|
|
@@ -31,6 +32,7 @@ import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
|
|
|
31
32
|
import { buildSessionContextLines } from './hook-context.mjs';
|
|
32
33
|
import { cmdAdopt, cmdUnadopt } from './adopt-cli.mjs';
|
|
33
34
|
import { parseIntFlag, isNumericToken } from './lib/cli-flags.mjs';
|
|
35
|
+
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
34
36
|
import { auditMemdir, memdirPath } from './memdir.mjs';
|
|
35
37
|
import { aggregateProjectCiteRecall } from './lib/citation-tracker.mjs';
|
|
36
38
|
import { probeOtherSources as probeIdSources, bucketIdTokens, splitDeferredTokens } from './lib/id-routing.mjs';
|
|
@@ -45,7 +47,7 @@ import { isNativeBindingError, healAndReexec } from './lib/binding-probe.mjs';
|
|
|
45
47
|
import { CLI_PATH, CLI_INVOKE } from './cli-path.mjs';
|
|
46
48
|
import { parseArgs, out, outVerbatim, fail, relativeTime, fmtDateShort, parseIdToken, formatProbeHints, rejectBareStringFlags, resolvePositionalAlias, suggestUnknownFlags, OBS_TIME_FIELDS, formatObsFieldValue, obsFieldLabel, formatPendingPurgeLine } from './cli/common.mjs';
|
|
47
49
|
import { saveObservation } from './lib/save-observation.mjs';
|
|
48
|
-
import {
|
|
50
|
+
import { normalizeScope, insertObservationVector, applyObsUpdate } from './lib/observation-write.mjs';
|
|
49
51
|
import { EXPORT_COLUMNS_SQL } from './lib/export-columns.mjs';
|
|
50
52
|
import { recallByFile } from './lib/recall-core.mjs';
|
|
51
53
|
import { fetchRecent, RECENT_MAX } from './lib/recent-core.mjs';
|
|
@@ -500,7 +502,6 @@ function cmdRecall(db, args) {
|
|
|
500
502
|
}
|
|
501
503
|
}
|
|
502
504
|
|
|
503
|
-
const OBS_FIELDS = ['id', 'type', 'title', 'subtitle', 'narrative', 'text', 'facts', 'concepts', 'lesson_learned', 'search_aliases', 'files_read', 'files_modified', 'project', 'created_at', 'memory_session_id', 'prompt_number', 'importance', 'related_ids', 'access_count', 'branch', 'superseded_at', 'superseded_by', 'last_accessed_at'];
|
|
504
505
|
|
|
505
506
|
// Time-field formatting moved to cli/common.mjs so the CLI `get` and the MCP
|
|
506
507
|
// `mem_get` (server.mjs) share one source and can't drift (the drift bug:
|
|
@@ -513,13 +514,8 @@ export { OBS_TIME_FIELDS, formatObsFieldValue };
|
|
|
513
514
|
export async function cmdSearchForTest(db, args, opts) { return cmdSearch(db, args, opts); }
|
|
514
515
|
|
|
515
516
|
function renderObsRows(db, ids, requestedFields) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
db.prepare(`UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id IN (${placeholders})`).run(Date.now(), ...ids);
|
|
519
|
-
autoBoostIfNeeded(db, ids);
|
|
520
|
-
} catch { /* non-critical: FTS5 trigger may fail on corrupted index */ }
|
|
521
|
-
|
|
522
|
-
const rows = db.prepare(`SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch ASC`).all(...ids);
|
|
517
|
+
// Access-bump + fetch via the shared get-core (P2-12) — single source with mem_get.
|
|
518
|
+
const rows = fetchObsDetail(db, ids);
|
|
523
519
|
if (rows.length === 0) return null;
|
|
524
520
|
const fields = requestedFields || OBS_FIELDS;
|
|
525
521
|
const parts = [];
|
|
@@ -547,12 +543,15 @@ function renderSessionRows(db, ids) {
|
|
|
547
543
|
const parts = [];
|
|
548
544
|
for (const r of rows) {
|
|
549
545
|
const lines = [`S#${r.id} ${fmtDateShort(r.created_at)}`];
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
546
|
+
// SESSION_DETAIL_FIELDS (get-core, P2-12): the FULL render set — the old
|
|
547
|
+
// 6-field subset made remaining_items/notes/files_* searchable-but-invisible.
|
|
548
|
+
for (const f of SESSION_DETAIL_FIELDS) {
|
|
549
|
+
if (f === 'id' || f === 'created_at') continue; // already in the header
|
|
550
|
+
const val = r[f];
|
|
551
|
+
if (val === null || val === undefined || val === '') continue;
|
|
552
|
+
const label = f[0].toUpperCase() + f.slice(1).replace(/_/g, ' ');
|
|
553
|
+
lines.push(`${label}: ${val}`);
|
|
554
|
+
}
|
|
556
555
|
parts.push(lines.join('\n'));
|
|
557
556
|
}
|
|
558
557
|
return { text: parts.join('\n\n'), count: rows.length };
|
|
@@ -1366,53 +1365,13 @@ function cmdBrowse(db, args) {
|
|
|
1366
1365
|
const jsonOutput = flags.json === true || flags.json === 'true';
|
|
1367
1366
|
const now = Date.now();
|
|
1368
1367
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
currentSessionId:
|
|
1373
|
-
};
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
const tiers = ['working', 'active', 'archive'];
|
|
1377
|
-
const tierLabels = { working: '🔴 Working Memory', active: '🟡 Active Memory', archive: '🔵 Archive' };
|
|
1378
|
-
const showTiers = tierFilter ? [tierFilter] : tiers;
|
|
1379
|
-
|
|
1380
|
-
// Collect data first (for JSON), then format. The text path also prints
|
|
1381
|
-
// tier headers as it walks; refactored to two passes so the JSON shape can
|
|
1382
|
-
// include row arrays alongside totals.
|
|
1383
|
-
const tierData = {};
|
|
1384
|
-
const tierCounts = {};
|
|
1385
|
-
let grandTotal = 0;
|
|
1386
|
-
|
|
1387
|
-
for (const tier of showTiers) {
|
|
1388
|
-
const countRow = db.prepare(`
|
|
1389
|
-
SELECT COUNT(*) as c FROM (
|
|
1390
|
-
SELECT ${TIER_CASE_SQL} as tier FROM observations
|
|
1391
|
-
WHERE project = ? AND COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL
|
|
1392
|
-
) WHERE tier = ?
|
|
1393
|
-
`).get(...params, project, tier);
|
|
1394
|
-
const count = countRow?.c ?? 0;
|
|
1395
|
-
tierCounts[tier] = count;
|
|
1396
|
-
grandTotal += count;
|
|
1397
|
-
|
|
1398
|
-
// Archive in unfiltered view: keep count but skip row fetch (matches text path).
|
|
1399
|
-
const skipRows = tier === 'archive' && !tierFilter;
|
|
1400
|
-
if (count === 0 || skipRows) {
|
|
1401
|
-
tierData[tier] = { count, rows: [] };
|
|
1402
|
-
continue;
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
const rows = db.prepare(`
|
|
1406
|
-
SELECT * FROM (
|
|
1407
|
-
SELECT id, type, title, importance, created_at_epoch, created_at, ${TIER_CASE_SQL} as tier
|
|
1408
|
-
FROM observations
|
|
1409
|
-
WHERE project = ? AND COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL
|
|
1410
|
-
) WHERE tier = ?
|
|
1411
|
-
ORDER BY created_at_epoch DESC
|
|
1412
|
-
LIMIT ?
|
|
1413
|
-
`).all(...params, project, tier, limit);
|
|
1414
|
-
tierData[tier] = { count, rows };
|
|
1415
|
-
}
|
|
1368
|
+
// Shared collection (lib/browse-core, P2-12) — single source with mem_browse.
|
|
1369
|
+
const { showTiers, tierData, tierCounts, grandTotal } = collectBrowseTiers(db, {
|
|
1370
|
+
project, tierFilter, limit, now,
|
|
1371
|
+
currentSessionId: getActiveMemorySessionId(db, project),
|
|
1372
|
+
});
|
|
1373
|
+
const tiers = BROWSE_TIERS;
|
|
1374
|
+
const tierLabels = BROWSE_TIER_LABELS;
|
|
1416
1375
|
|
|
1417
1376
|
if (jsonOutput) {
|
|
1418
1377
|
const tiersOut = {};
|
|
@@ -1470,13 +1429,6 @@ function cmdBrowse(db, args) {
|
|
|
1470
1429
|
}
|
|
1471
1430
|
}
|
|
1472
1431
|
|
|
1473
|
-
function getActiveSessionId(db, project) {
|
|
1474
|
-
const row = db.prepare(
|
|
1475
|
-
"SELECT memory_session_id FROM sdk_sessions WHERE project = ? AND status = 'active' ORDER BY started_at_epoch DESC LIMIT 1"
|
|
1476
|
-
).get(project);
|
|
1477
|
-
return row?.memory_session_id ?? '';
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
1432
|
// ─── Delete ──────────────────────────────────────────────────────────────────
|
|
1481
1433
|
|
|
1482
1434
|
function cmdDelete(db, args) {
|
|
@@ -1508,8 +1460,8 @@ function cmdDelete(db, args) {
|
|
|
1508
1460
|
}
|
|
1509
1461
|
|
|
1510
1462
|
const confirm = flags.confirm === true || flags.confirm === 'true';
|
|
1511
|
-
|
|
1512
|
-
const rows
|
|
1463
|
+
// Shared preview body (lib/delete-core, P2-12) — single source with mem_delete.
|
|
1464
|
+
const { rows, lines: previewLines } = previewDeleteRows(db, ids);
|
|
1513
1465
|
|
|
1514
1466
|
if (rows.length === 0) {
|
|
1515
1467
|
fail('[mem] No observations found for given IDs');
|
|
@@ -1518,9 +1470,7 @@ function cmdDelete(db, args) {
|
|
|
1518
1470
|
|
|
1519
1471
|
if (!confirm) {
|
|
1520
1472
|
out(`[mem] Preview: ${rows.length} observation(s) will be deleted:`);
|
|
1521
|
-
for (const
|
|
1522
|
-
out(` #${r.id} [${r.type}] ${truncate(r.title || '(untitled)', 80)} | ${r.project}`);
|
|
1523
|
-
}
|
|
1473
|
+
for (const line of previewLines) out(line);
|
|
1524
1474
|
out('[mem] Run with --confirm to execute deletion.');
|
|
1525
1475
|
return;
|
|
1526
1476
|
}
|
|
@@ -1569,8 +1519,7 @@ function cmdUpdate(db, args) {
|
|
|
1569
1519
|
// (#8470). Reject cleanly via the shared guard — single source with the other commands.
|
|
1570
1520
|
if (rejectBareStringFlags(flags, ['title', 'narrative', 'lesson', 'lesson-learned', 'concepts'])) return;
|
|
1571
1521
|
|
|
1572
|
-
const
|
|
1573
|
-
const params = [];
|
|
1522
|
+
const fields = {};
|
|
1574
1523
|
if (flags.title !== undefined) {
|
|
1575
1524
|
// Reject empty title — clears the observation's identifier and would render it
|
|
1576
1525
|
// as `(untitled)` in every listing. Almost always an accidental shell-stripped arg.
|
|
@@ -1578,7 +1527,7 @@ function cmdUpdate(db, args) {
|
|
|
1578
1527
|
fail('[mem] --title cannot be empty. Pass a non-empty string or omit the flag to leave the title unchanged.');
|
|
1579
1528
|
return;
|
|
1580
1529
|
}
|
|
1581
|
-
|
|
1530
|
+
fields.title = flags.title;
|
|
1582
1531
|
}
|
|
1583
1532
|
if (flags.narrative !== undefined) {
|
|
1584
1533
|
// Reject empty (mirror --title): an explicit '' would blank the narrative
|
|
@@ -1587,7 +1536,7 @@ function cmdUpdate(db, args) {
|
|
|
1587
1536
|
fail('[mem] --narrative cannot be empty. Omit the flag to leave it unchanged.');
|
|
1588
1537
|
return;
|
|
1589
1538
|
}
|
|
1590
|
-
|
|
1539
|
+
fields.narrative = flags.narrative;
|
|
1591
1540
|
}
|
|
1592
1541
|
if (flags.type) {
|
|
1593
1542
|
const validTypes = OBS_TYPE_SET;
|
|
@@ -1595,7 +1544,7 @@ function cmdUpdate(db, args) {
|
|
|
1595
1544
|
fail(`[mem] Invalid type "${flags.type}". Valid: ${[...validTypes].join(', ')}`);
|
|
1596
1545
|
return;
|
|
1597
1546
|
}
|
|
1598
|
-
|
|
1547
|
+
fields.type = flags.type;
|
|
1599
1548
|
}
|
|
1600
1549
|
if (flags.importance) {
|
|
1601
1550
|
const imp = parseInt(flags.importance, 10);
|
|
@@ -1605,7 +1554,7 @@ function cmdUpdate(db, args) {
|
|
|
1605
1554
|
fail(`[mem] Invalid importance "${flags.importance}". Must be 1, 2, or 3.`);
|
|
1606
1555
|
return;
|
|
1607
1556
|
}
|
|
1608
|
-
|
|
1557
|
+
fields.importance = imp;
|
|
1609
1558
|
}
|
|
1610
1559
|
if (flags.lesson !== undefined || flags['lesson-learned'] !== undefined) {
|
|
1611
1560
|
const rawLesson = flags.lesson ?? flags['lesson-learned'] ?? '';
|
|
@@ -1620,8 +1569,7 @@ function cmdUpdate(db, args) {
|
|
|
1620
1569
|
fail('[mem] --lesson cannot be empty. Omit the flag to leave it unchanged.');
|
|
1621
1570
|
return;
|
|
1622
1571
|
}
|
|
1623
|
-
|
|
1624
|
-
params.push(scrubSecrets(rawLesson));
|
|
1572
|
+
fields.lesson_learned = rawLesson;
|
|
1625
1573
|
}
|
|
1626
1574
|
// Scrub like the sibling text fields above (title/narrative/lesson) and the MCP twin
|
|
1627
1575
|
// mem_update — concepts is a scrub-target + FTS-indexed column, so a raw secret here
|
|
@@ -1631,24 +1579,18 @@ function cmdUpdate(db, args) {
|
|
|
1631
1579
|
fail('[mem] --concepts cannot be empty. Omit the flag to leave it unchanged.');
|
|
1632
1580
|
return;
|
|
1633
1581
|
}
|
|
1634
|
-
|
|
1582
|
+
fields.concepts = flags.concepts;
|
|
1635
1583
|
}
|
|
1636
1584
|
|
|
1637
|
-
|
|
1585
|
+
// Shared mutation (lib/observation-write applyObsUpdate, P2-12): scrub + UPDATE +
|
|
1586
|
+
// derived-column rebuild in one transaction — single source with MCP mem_update.
|
|
1587
|
+
const updatedCols = applyObsUpdate(db, id, fields);
|
|
1588
|
+
if (updatedCols.length === 0) {
|
|
1638
1589
|
fail('[mem] No fields to update. Use --title, --type, --importance, --lesson/--lesson-learned, --narrative, --concepts');
|
|
1639
1590
|
return;
|
|
1640
1591
|
}
|
|
1641
1592
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
// Atomic: update fields + rebuild derived columns (FTS text + vector) via the
|
|
1645
|
-
// shared core — single source with MCP mem_update (lib/observation-write.mjs).
|
|
1646
|
-
db.transaction(() => {
|
|
1647
|
-
db.prepare(`UPDATE observations SET ${updates.join(', ')} WHERE id = ?`).run(...params);
|
|
1648
|
-
rebuildObservationDerived(db, id);
|
|
1649
|
-
})();
|
|
1650
|
-
|
|
1651
|
-
out(`[mem] Updated #${id}: ${updates.map(u => u.split(' =')[0]).join(', ')}`);
|
|
1593
|
+
out(`[mem] Updated #${id}: ${updatedCols.join(', ')}`);
|
|
1652
1594
|
}
|
|
1653
1595
|
|
|
1654
1596
|
// ─── Export ──────────────────────────────────────────────────────────────────
|
|
@@ -1663,11 +1605,14 @@ function cmdExport(db, args) {
|
|
|
1663
1605
|
if (rejectBareStringFlags(flags, ['project', 'type', 'from', 'to'])) return;
|
|
1664
1606
|
const wheres = [];
|
|
1665
1607
|
const params = [];
|
|
1666
|
-
// --include-compressed: include compressed observations (aligned with MCP mem_export)
|
|
1667
|
-
|
|
1668
|
-
|
|
1608
|
+
// --include-compressed: include compressed observations (aligned with MCP mem_export).
|
|
1609
|
+
// Superseded rows are excluded either way; the flag only toggles the compressed half
|
|
1610
|
+
// of the live-row pair (backup/export of tombstones is opt-in, retractions are not).
|
|
1611
|
+
if (flags['include-compressed'] === true || flags['include-compressed'] === 'true') {
|
|
1612
|
+
wheres.push('superseded_at IS NULL');
|
|
1613
|
+
} else {
|
|
1614
|
+
wheres.push(liveObsFilterSql(''));
|
|
1669
1615
|
}
|
|
1670
|
-
wheres.push('superseded_at IS NULL');
|
|
1671
1616
|
|
|
1672
1617
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
1673
1618
|
if (project) { wheres.push('project = ?'); params.push(project); }
|
|
@@ -1822,7 +1767,14 @@ function cmdRestore(db, argv) {
|
|
|
1822
1767
|
// weekly-summary keeper already absorbed (duplicate search hits, and the marker's
|
|
1823
1768
|
// target id is meaningless in this store). Reject rather than remap; the content
|
|
1824
1769
|
// lives on in the keeper.
|
|
1825
|
-
|
|
1770
|
+
// D#122: COMPRESSED_PENDING_PURGE (-2) is NOT keeper-absorbed — those rows have
|
|
1771
|
+
// no keeper, so rejecting them silently loses their only copy. Restore them
|
|
1772
|
+
// live; the maintain decay pipeline will re-evaluate them like any other row.
|
|
1773
|
+
// COMPRESSED_AUTO (-1) stays REJECTED by design (review 2026-08-16): those rows
|
|
1774
|
+
// were deliberately retired by the quality pipeline (idle-cleanup/optimize mark
|
|
1775
|
+
// importance-1 aged rows) — restoring them live would resurrect adjudicated
|
|
1776
|
+
// noise, unlike -2 (a purge QUEUE the user may still be racing to undo).
|
|
1777
|
+
if (r.compressed_into && r.compressed_into !== COMPRESSED_PENDING_PURGE) { tombstoned++; continue; }
|
|
1826
1778
|
const project = projOverride || r.project || inferProject();
|
|
1827
1779
|
const createdEpoch = Number.isFinite(Number(r.created_at_epoch)) ? Number(r.created_at_epoch) : Date.now();
|
|
1828
1780
|
// Durable exact-dup guard — saveObservation's 5-min Jaccard window can't catch a
|
|
@@ -1889,7 +1841,7 @@ function cmdRestore(db, argv) {
|
|
|
1889
1841
|
// ones that parsed.
|
|
1890
1842
|
const totalMalformed = malformed + parseFailures;
|
|
1891
1843
|
const totalLines = rows.length + parseFailures;
|
|
1892
|
-
const tombstoneNote = tombstoned > 0 ? `, ${tombstoned} compressed member(s) rejected (
|
|
1844
|
+
const tombstoneNote = tombstoned > 0 ? `, ${tombstoned} compressed member(s) rejected (keeper-absorbed or auto-retired tombstones)` : '';
|
|
1893
1845
|
out(`[mem] Restore${dryRun ? ' (dry-run)' : ''}: ${restored} restored, ${skipped} duplicate(s) skipped${tombstoneNote}, ${totalMalformed} malformed/failed from ${totalLines} row(s).`);
|
|
1894
1846
|
// Name the lossiness where the user meets it. Export omits related_ids and drops
|
|
1895
1847
|
// superseded rows, and restore re-inserts under fresh AUTOINCREMENT ids — so no
|
|
@@ -2305,18 +2257,13 @@ function cmdRegistry(_memDb, args) {
|
|
|
2305
2257
|
if (action === 'list') {
|
|
2306
2258
|
const typeFilter = flags.type;
|
|
2307
2259
|
const listLimit = parseIntFlag(flags.limit, { name: '--limit', defaultValue: 20, max: 1000 });
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
const allResources = rdb
|
|
2311
|
-
SELECT name, type, invocation_name, recommend_count, adopt_count, capability_summary
|
|
2312
|
-
FROM resources ${where} ORDER BY adopt_count DESC, recommend_count DESC, type, name
|
|
2313
|
-
`).all(...params);
|
|
2260
|
+
// Shared ranked query + row line (registry.mjs, P2-12): COALESCE ordering and
|
|
2261
|
+
// the 80-char/adopt:0 row shape had drifted between the faces.
|
|
2262
|
+
const allResources = listResourcesRanked(rdb, { type: typeFilter });
|
|
2314
2263
|
if (allResources.length === 0) { out('[mem] No resources found.'); return; }
|
|
2315
2264
|
const resources = allResources.slice(0, listLimit);
|
|
2316
2265
|
out(`[mem] Resources (showing ${resources.length} of ${allResources.length}):`);
|
|
2317
|
-
for (const r of resources) {
|
|
2318
|
-
out(` ${r.type === 'skill' ? 'S' : 'A'} ${r.name}${r.invocation_name ? ` (${r.invocation_name})` : ''} — rec:${r.recommend_count} adopt:${r.adopt_count} — ${truncate(r.capability_summary || '', 50)}`);
|
|
2319
|
-
}
|
|
2266
|
+
for (const r of resources) out(` ${formatRegistryListLine(r)}`);
|
|
2320
2267
|
if (allResources.length > listLimit) {
|
|
2321
2268
|
out(`[mem] Use --limit N to see more, or "registry search <query>" to find specific resources.`);
|
|
2322
2269
|
}
|
|
@@ -2324,25 +2271,17 @@ function cmdRegistry(_memDb, args) {
|
|
|
2324
2271
|
}
|
|
2325
2272
|
|
|
2326
2273
|
if (action === 'stats') {
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
const
|
|
2330
|
-
'SELECT name, type, adopt_count, recommend_count FROM resources WHERE status = ? AND adopt_count > 0 ORDER BY adopt_count DESC LIMIT 10'
|
|
2331
|
-
).all('active');
|
|
2332
|
-
const zeroAdopt = rdb.prepare(
|
|
2333
|
-
'SELECT COUNT(*) as c FROM resources WHERE status = ? AND recommend_count > 0 AND adopt_count = 0'
|
|
2334
|
-
).get('active');
|
|
2335
|
-
const userAdded = rdb.prepare(
|
|
2336
|
-
"SELECT COUNT(*) as c FROM resources WHERE status = ? AND source = 'user'"
|
|
2337
|
-
).get('active');
|
|
2274
|
+
// Shared collection (registry.mjs collectRegistryStats, P2-12) — single
|
|
2275
|
+
// source with the MCP mem_registry stats action.
|
|
2276
|
+
const s = collectRegistryStats(rdb);
|
|
2338
2277
|
out(`[mem] Registry Stats:`);
|
|
2339
|
-
out(` Total active: ${total
|
|
2340
|
-
for (const t of byType) out(` ${t.type}: ${t.c}`);
|
|
2341
|
-
out(` User-added: ${userAdded
|
|
2342
|
-
out(` Zero adoption (recommended but never adopted): ${zeroAdopt
|
|
2343
|
-
if (topAdopted.length > 0) {
|
|
2278
|
+
out(` Total active: ${s.total}`);
|
|
2279
|
+
for (const t of s.byType) out(` ${t.type}: ${t.c}`);
|
|
2280
|
+
out(` User-added: ${s.userAdded}`);
|
|
2281
|
+
out(` Zero adoption (recommended but never adopted): ${s.zeroAdopt}`);
|
|
2282
|
+
if (s.topAdopted.length > 0) {
|
|
2344
2283
|
out(' Top adopted:');
|
|
2345
|
-
for (const r of topAdopted) out(` ${r.name} (${r.type}): ${r.adopt_count}/${r.recommend_count}`);
|
|
2284
|
+
for (const r of s.topAdopted) out(` ${r.name} (${r.type}): ${r.adopt_count}/${r.recommend_count}`);
|
|
2346
2285
|
}
|
|
2347
2286
|
return;
|
|
2348
2287
|
}
|
|
@@ -2533,8 +2472,7 @@ function cmdCitationStats(db, args) {
|
|
|
2533
2472
|
SUM(CASE WHEN uncited_streak >= 2 THEN 1 ELSE 0 END) AS at_risk
|
|
2534
2473
|
FROM observations
|
|
2535
2474
|
WHERE created_at_epoch >= ?
|
|
2536
|
-
AND
|
|
2537
|
-
AND superseded_at IS NULL
|
|
2475
|
+
AND ${liveObsFilterSql('')}
|
|
2538
2476
|
GROUP BY project
|
|
2539
2477
|
ORDER BY resolved DESC
|
|
2540
2478
|
`).all(cutoff);
|
|
@@ -2543,8 +2481,7 @@ function cmdCitationStats(db, args) {
|
|
|
2543
2481
|
SELECT id, project, type, title, importance, uncited_streak, cited_count
|
|
2544
2482
|
FROM observations
|
|
2545
2483
|
WHERE uncited_streak >= 2
|
|
2546
|
-
AND
|
|
2547
|
-
AND superseded_at IS NULL
|
|
2484
|
+
AND ${liveObsFilterSql('')}
|
|
2548
2485
|
ORDER BY uncited_streak DESC, importance ASC
|
|
2549
2486
|
LIMIT 20
|
|
2550
2487
|
`).all();
|
|
@@ -2553,8 +2490,7 @@ function cmdCitationStats(db, args) {
|
|
|
2553
2490
|
SELECT id, project, type, title, importance, cited_count
|
|
2554
2491
|
FROM observations
|
|
2555
2492
|
WHERE importance >= 3 AND cited_count >= 1
|
|
2556
|
-
AND
|
|
2557
|
-
AND superseded_at IS NULL
|
|
2493
|
+
AND ${liveObsFilterSql('')}
|
|
2558
2494
|
ORDER BY cited_count DESC
|
|
2559
2495
|
LIMIT 10
|
|
2560
2496
|
`).all();
|
|
@@ -2564,8 +2500,7 @@ function cmdCitationStats(db, args) {
|
|
|
2564
2500
|
FROM observations
|
|
2565
2501
|
WHERE demoted_at IS NOT NULL
|
|
2566
2502
|
AND demoted_at >= ?
|
|
2567
|
-
AND
|
|
2568
|
-
AND superseded_at IS NULL
|
|
2503
|
+
AND ${liveObsFilterSql('')}
|
|
2569
2504
|
ORDER BY demoted_at DESC
|
|
2570
2505
|
LIMIT 10
|
|
2571
2506
|
`).all(cutoff);
|
|
@@ -2580,8 +2515,7 @@ function cmdCitationStats(db, args) {
|
|
|
2580
2515
|
const pollutedRows = db.prepare(`
|
|
2581
2516
|
SELECT COUNT(*) AS n FROM observations
|
|
2582
2517
|
WHERE cited_count > decay_seen_count
|
|
2583
|
-
AND
|
|
2584
|
-
AND superseded_at IS NULL
|
|
2518
|
+
AND ${liveObsFilterSql('')}
|
|
2585
2519
|
`).get();
|
|
2586
2520
|
const dataPollutionNote = pollutedRows.n > 0
|
|
2587
2521
|
? `${pollutedRows.n} obs have cited_count > decay_seen_count (pre-v34 backfill — invariant holds for new data).`
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.65.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "3.
|
|
9
|
+
"version": "3.65.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
12
12
|
"better-sqlite3": "^12.6.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.65.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",
|
|
@@ -82,6 +82,10 @@
|
|
|
82
82
|
"lib/atomic-write.mjs",
|
|
83
83
|
"lib/release-digest.mjs",
|
|
84
84
|
"lib/mem-override.mjs",
|
|
85
|
+
"lib/injected-ids.mjs",
|
|
86
|
+
"lib/inject-search-core.mjs",
|
|
87
|
+
"lib/get-core.mjs",
|
|
88
|
+
"lib/browse-core.mjs",
|
|
85
89
|
"lib/save-observation.mjs",
|
|
86
90
|
"lib/observation-write.mjs",
|
|
87
91
|
"lib/recall-core.mjs",
|
package/registry.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import Database from 'better-sqlite3';
|
|
5
5
|
import { existsSync, mkdirSync } from 'fs';
|
|
6
6
|
import { dirname } from 'path';
|
|
7
|
-
import { debugCatch } from './utils.mjs';
|
|
7
|
+
import { debugCatch, truncate } from './utils.mjs';
|
|
8
8
|
|
|
9
9
|
// ─── Schema ──────────────────────────────────────────────────────────────────
|
|
10
10
|
|
|
@@ -513,3 +513,55 @@ export function upsertResource(db, r) {
|
|
|
513
513
|
return row?.id || 0;
|
|
514
514
|
})();
|
|
515
515
|
}
|
|
516
|
+
|
|
517
|
+
// ─── P2-12: registry stats/list twin cores (audit 2026-08-14) ────────────────
|
|
518
|
+
// The five stats statements + the list row line were duplicated in mem-cli.mjs
|
|
519
|
+
// and server.mjs and had already drifted (truncate 50 vs 80; `adopt:null` on
|
|
520
|
+
// one face; the COALESCE ordering fix landed on the MCP face only). Data
|
|
521
|
+
// collection + row-line shape live here; faces keep their own headers/limits.
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Collect the five stat groups the `registry stats` twin renders.
|
|
525
|
+
* @param {import('better-sqlite3').Database} rdb registry DB handle
|
|
526
|
+
* @returns {{total:number, byType:Array<{type:string,c:number}>, topAdopted:object[], zeroAdopt:number, userAdded:number}}
|
|
527
|
+
*/
|
|
528
|
+
export function collectRegistryStats(rdb) {
|
|
529
|
+
const total = rdb.prepare('SELECT COUNT(*) as c FROM resources WHERE status = ?').get('active');
|
|
530
|
+
const byType = rdb.prepare('SELECT type, COUNT(*) as c FROM resources WHERE status = ? GROUP BY type').all('active');
|
|
531
|
+
const topAdopted = rdb.prepare(
|
|
532
|
+
'SELECT name, type, adopt_count, recommend_count FROM resources WHERE status = ? AND adopt_count > 0 ORDER BY adopt_count DESC LIMIT 10'
|
|
533
|
+
).all('active');
|
|
534
|
+
const zeroAdopt = rdb.prepare(
|
|
535
|
+
'SELECT COUNT(*) as c FROM resources WHERE status = ? AND recommend_count > 0 AND adopt_count = 0'
|
|
536
|
+
).get('active');
|
|
537
|
+
const userAdded = rdb.prepare(
|
|
538
|
+
"SELECT COUNT(*) as c FROM resources WHERE status = ? AND source = 'user'"
|
|
539
|
+
).get('active');
|
|
540
|
+
return { total: total.c, byType, topAdopted, zeroAdopt: zeroAdopt.c, userAdded: userAdded.c };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Ranked resource listing for the `registry list` twin: adoption first, then
|
|
545
|
+
* recommendation, NULL counts coalesced (the un-coalesced face sorted NULLs
|
|
546
|
+
* apart AND rendered "adopt:null").
|
|
547
|
+
* @param {import('better-sqlite3').Database} rdb
|
|
548
|
+
* @param {{type?: string}} [opts]
|
|
549
|
+
* @returns {object[]}
|
|
550
|
+
*/
|
|
551
|
+
export function listResourcesRanked(rdb, { type } = {}) {
|
|
552
|
+
const where = type ? 'WHERE type = ? AND status = ?' : 'WHERE status = ?';
|
|
553
|
+
const params = type ? [type, 'active'] : ['active'];
|
|
554
|
+
return rdb.prepare(`
|
|
555
|
+
SELECT name, type, invocation_name, recommend_count, adopt_count, capability_summary
|
|
556
|
+
FROM resources ${where}
|
|
557
|
+
ORDER BY COALESCE(adopt_count, 0) DESC, COALESCE(recommend_count, 0) DESC, type, name
|
|
558
|
+
`).all(...params);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** One list row, shared shape: `S name (invocation) — rec:N adopt:N — summary…` (80-char summary cap).
|
|
562
|
+
* truncate(), not a hand-rolled slice (adversarial review 2026-08-16): the shared
|
|
563
|
+
* helper flattens newlines (this is a one-row-per-resource listing) and never
|
|
564
|
+
* splits a UTF-16 surrogate pair — a bare slice emitted lone surrogates. */
|
|
565
|
+
export function formatRegistryListLine(r) {
|
|
566
|
+
return `${r.type === 'skill' ? 'S' : 'A'} ${r.name}${r.invocation_name ? ` (${r.invocation_name})` : ''} — rec:${r.recommend_count ?? 0} adopt:${r.adopt_count ?? 0} — ${truncate(r.capability_summary || '', 80)}`;
|
|
567
|
+
}
|
package/scoring-sql.mjs
CHANGED
|
@@ -23,10 +23,11 @@ import { buildNotLowSignalSql } from './lib/low-signal-patterns.mjs';
|
|
|
23
23
|
// Where a multiplier reads 0 it is a benchmark-MISMATCH artifact (the instrument
|
|
24
24
|
// can't vary that axis), NOT proven dead weight. Decision: KEEP them; do NOT
|
|
25
25
|
// delete on "0 lift". Guardrail: the ci-gate `hybrid_over_bm25 >= -0.05` floor
|
|
26
|
-
// (benchmark/ci-gate.mjs) covers the full modelled chain
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
26
|
+
// (benchmark/ci-gate.mjs) covers the full modelled chain — D#121: cite + noise
|
|
27
|
+
// joined the matrix MULT_EXPR after M-3 put them in FULL_SCORE (fixture carries
|
|
28
|
+
// zero cite/noise state, so both read 0 by construction, same caveat as lesson;
|
|
29
|
+
// their real-SQL direction pins live in benchmark/events-pipeline-probes.mjs).
|
|
30
|
+
// Genuine validation of the prior-encoding axes needs a labeled real-dev-memory eval.
|
|
30
31
|
|
|
31
32
|
// ─── Type-Differentiated Recency Decay ──────────────────────────────────────
|
|
32
33
|
|
|
@@ -104,7 +104,9 @@ try {
|
|
|
104
104
|
let additionalContext;
|
|
105
105
|
if (content.length > 16000) {
|
|
106
106
|
const summary = defang(content.slice(0, 800));
|
|
107
|
-
|
|
107
|
+
// D#122 ③: the path interpolates OUTSIDE the wrapper — defang it too (a
|
|
108
|
+
// locally-created managed dir can carry delimiter chars in its name).
|
|
109
|
+
additionalContext = `<skill-bridge name="${safeName}" source="managed" truncated="true">\n${summary}\n...\n</skill-bridge>\n\nSkill content truncated. Read("${defang(portablePath)}") to load full content.`;
|
|
108
110
|
} else {
|
|
109
111
|
additionalContext = `<skill-bridge name="${safeName}" source="managed">\n${defang(content)}\n</skill-bridge>\n\nThis skill was loaded from the managed registry. Follow the instructions above.`;
|
|
110
112
|
}
|
|
@@ -8,6 +8,8 @@ import { existsSync, readFileSync, mkdirSync } from 'fs';
|
|
|
8
8
|
import { basename, join } from 'path';
|
|
9
9
|
import { resolveDataDir } from '../lib/resolve-data-dir.mjs';
|
|
10
10
|
import { atomicWriteFileSync } from '../lib/atomic-write.mjs';
|
|
11
|
+
import { injectedIdsFileName } from '../lib/injected-ids.mjs';
|
|
12
|
+
import { liveObsFilterSql } from '../lib/inject-search-core.mjs';
|
|
11
13
|
import { buildNotLowSignalSql } from '../lib/low-signal-patterns.mjs';
|
|
12
14
|
import { recordHookError } from '../lib/hook-telemetry.mjs';
|
|
13
15
|
import { citeFactorClause } from '../scoring-sql.mjs';
|
|
@@ -153,11 +155,14 @@ function entryTimestamp(v) {
|
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
// A3 (v2.83): cross-hook injected-IDs store. UPS writes
|
|
156
|
-
// `runtime/.claude-mem-injected-<project>` with {ids, ts, count}. We
|
|
157
|
-
// inside the staleness window, filter overlaps from PreToolUse output,
|
|
158
|
-
// merge back so the next UPS sees what we emitted too.
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
// `runtime/.claude-mem-injected-<project>-<session>` with {ids, ts, count}. We
|
|
159
|
+
// read inside the staleness window, filter overlaps from PreToolUse output,
|
|
160
|
+
// then merge back so the next UPS sees what we emitted too.
|
|
161
|
+
// D#120: the file is keyed per SESSION (payload-only keying let two concurrent
|
|
162
|
+
// windows clobber each other's marker). Derivation shared via lib/injected-ids.mjs
|
|
163
|
+
// (pure, no deps — within the standalone fast-path budget, #8447).
|
|
164
|
+
function crossHookInjectedFile(project, sessionId) {
|
|
165
|
+
return join(RUNTIME_DIR, injectedIdsFileName(project, sessionId));
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
// M-6 (audit 2026-08-14): the marker file is keyed by PROJECT, so two concurrent
|
|
@@ -168,7 +173,7 @@ function crossHookInjectedFile(project) {
|
|
|
168
173
|
// session-key fix. Legacy payloads without `session` keep the old behavior.
|
|
169
174
|
function readCrossHookInjected(project, sessionId) {
|
|
170
175
|
try {
|
|
171
|
-
const raw = readFileSync(crossHookInjectedFile(project), 'utf8');
|
|
176
|
+
const raw = readFileSync(crossHookInjectedFile(project, sessionId), 'utf8');
|
|
172
177
|
const { ids, ts, session } = JSON.parse(raw);
|
|
173
178
|
if (session && sessionId && session !== sessionId) return new Set();
|
|
174
179
|
if (!ts || Date.now() - ts > CROSS_HOOK_DEDUP_MS) return new Set();
|
|
@@ -181,7 +186,7 @@ function mergeCrossHookInjected(project, newIds, sessionId) {
|
|
|
181
186
|
if (!newIds || newIds.length === 0) return;
|
|
182
187
|
try {
|
|
183
188
|
mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
184
|
-
const file = crossHookInjectedFile(project);
|
|
189
|
+
const file = crossHookInjectedFile(project, sessionId);
|
|
185
190
|
let prev = { ids: [], ts: 0, count: 0 };
|
|
186
191
|
try {
|
|
187
192
|
const raw = readFileSync(file, 'utf8');
|
|
@@ -428,8 +433,7 @@ try {
|
|
|
428
433
|
JOIN observation_files of2 ON of2.obs_id = o.id
|
|
429
434
|
WHERE o.project = ?
|
|
430
435
|
AND o.importance >= 2
|
|
431
|
-
AND
|
|
432
|
-
AND o.superseded_at IS NULL
|
|
436
|
+
AND ${liveObsFilterSql('o')}
|
|
433
437
|
AND o.created_at_epoch > ?
|
|
434
438
|
AND ${fileMatch}
|
|
435
439
|
${edgeDecayFilter}
|
|
@@ -149,6 +149,9 @@ export function extractErrorSignature(text) {
|
|
|
149
149
|
|
|
150
150
|
export const MAX_SESSION_INJECTIONS = 15;
|
|
151
151
|
export const DEDUP_STALE_MS = 300_000; // 5 minutes
|
|
152
|
+
// Marker file-name derivation lives in lib/injected-ids.mjs (D#120) — lib/, not
|
|
153
|
+
// here, so hook.mjs can import it (scripts/ static imports from hook.mjs break
|
|
154
|
+
// under the installExtractedRelease scripts-dir rename).
|
|
152
155
|
|
|
153
156
|
/**
|
|
154
157
|
* Check if injection should be skipped based on deduplication state.
|