claude-mem-lite 5.5.1 → 5.6.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/hook.mjs +27 -6
- package/lib/citation-tracker.mjs +76 -5
- package/lib/cite-back-hint.mjs +46 -0
- package/lib/export-columns.mjs +4 -2
- package/lib/search-core.mjs +11 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/schema.mjs +21 -1
- package/search-engine.mjs +3 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "5.
|
|
13
|
+
"version": "5.6.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": "5.
|
|
3
|
+
"version": "5.6.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/hook.mjs
CHANGED
|
@@ -145,7 +145,7 @@ import {
|
|
|
145
145
|
buildUnsavedBugfixHint,
|
|
146
146
|
countUnsavedBugfixShape,
|
|
147
147
|
buildCiteRecallNudge as libBuildCiteRecallNudge,
|
|
148
|
-
|
|
148
|
+
nextCiteStreakState,
|
|
149
149
|
} from './lib/cite-back-hint.mjs';
|
|
150
150
|
import { citeRecallPathFor } from './lib/cite-recall-path.mjs';
|
|
151
151
|
import { detectUnpersistedDecision } from './lib/persist-reminder.mjs';
|
|
@@ -1182,7 +1182,11 @@ function trackCitationsAtStop(db, { sessionId, project, ccSessionId, transcriptP
|
|
|
1182
1182
|
sessionId: ccSessionId,
|
|
1183
1183
|
subagentInjected: sub.injected,
|
|
1184
1184
|
});
|
|
1185
|
-
|
|
1185
|
+
// sessionId is the ACCESS channel's idempotency key (R11-B-P1-1). Stop fires once
|
|
1186
|
+
// per assistant turn and `ids` above is a rescan of the WHOLE transcript, so
|
|
1187
|
+
// without it every later turn re-credited the same citation — 7.86x on the real
|
|
1188
|
+
// corpus, straight into boostAccessed's `access_count > 3`.
|
|
1189
|
+
const n = bumpCitationAccess(db, ids, project, relevant, { sessionId: ccSessionId });
|
|
1186
1190
|
debugLog(
|
|
1187
1191
|
'DEBUG',
|
|
1188
1192
|
'handleStop',
|
|
@@ -1407,11 +1411,19 @@ function trackCitationsAtStop(db, { sessionId, project, ccSessionId, transcriptP
|
|
|
1407
1411
|
const dest = citeRecallPathFor(RUNTIME_DIR, project);
|
|
1408
1412
|
// Carry the consecutive-low-cite streak forward so the SessionStart
|
|
1409
1413
|
// nag can self-silence after the project has ignored it N times.
|
|
1410
|
-
let
|
|
1414
|
+
let prevPayload = null;
|
|
1411
1415
|
try {
|
|
1412
|
-
|
|
1416
|
+
prevPayload = JSON.parse(readFileSync(dest, 'utf8'));
|
|
1413
1417
|
} catch {}
|
|
1414
|
-
|
|
1418
|
+
// R11-B-P1-2: the streak's unit is the SESSION, which is what the docblock has
|
|
1419
|
+
// always said. Stop fires once per assistant TURN, so incrementing here silenced
|
|
1420
|
+
// the nudge inside the first session — this machine read lowStreak 58 against 26
|
|
1421
|
+
// transcripts before the fix.
|
|
1422
|
+
const { lowStreak, streakBase, lastStreakSession } = nextCiteStreakState(
|
|
1423
|
+
prevPayload,
|
|
1424
|
+
ccSessionId,
|
|
1425
|
+
stats,
|
|
1426
|
+
);
|
|
1415
1427
|
// G3: finalized-in-conversation + zero deliberate persistence →
|
|
1416
1428
|
// decisionSignal rides the payload; next SessionStart reminds once.
|
|
1417
1429
|
let decisionSignal = null;
|
|
@@ -1432,7 +1444,16 @@ function trackCitationsAtStop(db, { sessionId, project, ccSessionId, transcriptP
|
|
|
1432
1444
|
} catch (e) {
|
|
1433
1445
|
debugCatch(e, 'handleStop-persist-reminder');
|
|
1434
1446
|
}
|
|
1435
|
-
const payload = {
|
|
1447
|
+
const payload = {
|
|
1448
|
+
...stats,
|
|
1449
|
+
...bugfixStats,
|
|
1450
|
+
lowStreak,
|
|
1451
|
+
streakBase,
|
|
1452
|
+
lastStreakSession,
|
|
1453
|
+
decisionSignal,
|
|
1454
|
+
project,
|
|
1455
|
+
savedAt: Date.now(),
|
|
1456
|
+
};
|
|
1436
1457
|
writeFileSync(dest, JSON.stringify(payload), { mode: 0o600 });
|
|
1437
1458
|
} catch (e) {
|
|
1438
1459
|
debugCatch(e, 'handleStop-cite-recall-persist');
|
package/lib/citation-tracker.mjs
CHANGED
|
@@ -46,9 +46,21 @@ export const OBS_ID_DIGITS = '\\d{1,7}';
|
|
|
46
46
|
* Returned fresh per call rather than shared: a `/g` regex carries `lastIndex`, so one
|
|
47
47
|
* exported instance reused by two scanners silently starts mid-string in whichever one
|
|
48
48
|
* runs second.
|
|
49
|
+
*
|
|
50
|
+
* R11-B-P2-3: the lookbehind is what makes "bare" true. This product renders — and
|
|
51
|
+
* teaches the agent and the user to type back — `E#N` (events), `P#N` (user_prompts),
|
|
52
|
+
* `D#N` (deferred) and `S#N` (sessions), and every INJECTED-side extractor already drops
|
|
53
|
+
* those by construction (they fail INJECTED_ROW_RE / FYI_LINE_ID_RE / UPS_ID_RE). The
|
|
54
|
+
* CITED side did not, so `E#501` was read as observation 501. On this machine 26/26 live
|
|
55
|
+
* observation ids are also event ids AND prompt ids, so the collision needs exactly one
|
|
56
|
+
* co-occurrence to land.
|
|
57
|
+
*
|
|
58
|
+
* It does NOT catch every false positive and is not meant to: `issue #1234` and
|
|
59
|
+
* `[link](#42)` are still matched, because a digit preceded by a space or `(` is
|
|
60
|
+
* indistinguishable from a citation at this layer.
|
|
49
61
|
*/
|
|
50
62
|
export function citationIdRe() {
|
|
51
|
-
return new RegExp(
|
|
63
|
+
return new RegExp(`(?<![A-Za-z0-9])#(${OBS_ID_DIGITS})\\b`, 'g');
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
/**
|
|
@@ -58,7 +70,16 @@ export function citationIdRe() {
|
|
|
58
70
|
* `citationIdRe()` above is a NUMERATOR caliber. On the numerator side a spurious `#1`
|
|
59
71
|
* costs nothing, because a cited id only counts once it intersects an injected set that
|
|
60
72
|
* WAS anchored — every injected-side extractor in this module matches a row shape
|
|
61
|
-
* (`INJECTED_ROW_RE`, `FYI_LINE_ID_RE`, `UPS_ID_RE`, `SUBAGENT_INJECT_ID_RE`).
|
|
73
|
+
* (`INJECTED_ROW_RE`, `FYI_LINE_ID_RE`, `UPS_ID_RE`, `SUBAGENT_INJECT_ID_RE`).
|
|
74
|
+
*
|
|
75
|
+
* That argument has ONE measured exception, and it is why citationIdRe now carries a
|
|
76
|
+
* lookbehind (R11-B-P2-3): `extractUserTypedIds` feeds the access allow-list through the
|
|
77
|
+
* SAME numerator regex, and nothing anchors a user's own message. So a user typing `D#9`
|
|
78
|
+
* and the assistant writing `D#9` intersect on an unanchored token — 8 of 43 credited
|
|
79
|
+
* (session, id) pairs on the real corpus were sourced only that way, 4 of them landing on
|
|
80
|
+
* live observations. Read the argument as "anchored on the injected side", not "safe".
|
|
81
|
+
*
|
|
82
|
+
* On the
|
|
62
83
|
* denominator side nothing anchors it, so a prose `#1` is a false positive by
|
|
63
84
|
* construction — it inflates "injected, never cited" and biases the measured rate DOWN.
|
|
64
85
|
*
|
|
@@ -408,7 +429,13 @@ export function assertRelevanceCoversAllFaces(sourced) {
|
|
|
408
429
|
* with ids the user typed themselves. Required.
|
|
409
430
|
* @returns {number} count of rows incremented
|
|
410
431
|
*/
|
|
411
|
-
export function bumpCitationAccess(
|
|
432
|
+
export function bumpCitationAccess(
|
|
433
|
+
db,
|
|
434
|
+
ids,
|
|
435
|
+
project,
|
|
436
|
+
relevantIds,
|
|
437
|
+
{ env = process.env, sessionId = null } = {},
|
|
438
|
+
) {
|
|
412
439
|
if (!db || !ids || !project) return 0;
|
|
413
440
|
// Revert path for the gate (CLAUDE_MEM_CITATION_RELEVANCE_GATE=off). It restores the
|
|
414
441
|
// pre-v3.84.0 behaviour — every mention credited — including the missing-argument hole,
|
|
@@ -428,7 +455,35 @@ export function bumpCitationAccess(db, ids, project, relevantIds, env = process.
|
|
|
428
455
|
// crediting a tombstone instead of the row that absorbed it) with no upside to restore.
|
|
429
456
|
const idList = gateOff ? [...cited] : [...cited].filter((id) => allowed.has(id));
|
|
430
457
|
if (idList.length === 0) return 0;
|
|
431
|
-
|
|
458
|
+
// R11-B-P1-1: credit once per CC SESSION, not once per call. `Stop` fires on every
|
|
459
|
+
// assistant turn and rescans the entire transcript, so without this key one citation
|
|
460
|
+
// was re-credited on every later turn of the same session — measured 7.86x over the
|
|
461
|
+
// real corpus (338 credits / 43 distinct (session, id) pairs), which then crosses
|
|
462
|
+
// boostAccessed's `access_count > 3` and lifts importance unattended.
|
|
463
|
+
//
|
|
464
|
+
// The key is the same shape the decay channel uses (last_cited_session_id /
|
|
465
|
+
// last_decided_session_id) and deliberately a SEPARATE column: decay resolves a
|
|
466
|
+
// mainOnly id set behind hasMainThreadAssistantText, this channel resolves the whole
|
|
467
|
+
// transcript, so sharing one key would let either channel silence the other.
|
|
468
|
+
//
|
|
469
|
+
// sessionId null → no session scope exists to be idempotent within (non-CC
|
|
470
|
+
// invocation), so the pre-R11 unconditional bump is the only defined behaviour.
|
|
471
|
+
//
|
|
472
|
+
// Scope the guarantee honestly: the column holds the LAST crediting session, not a set,
|
|
473
|
+
// so two same-project CC sessions interleaving their turns flip the stamp between them
|
|
474
|
+
// (A credits → B credits → A's next Stop sees B's stamp and credits again) and the key
|
|
475
|
+
// degrades toward per-turn counting for that pair. That is the same bound the decay
|
|
476
|
+
// channel accepts at :1634 and lib/edge-attribution.mjs:145, and widening it would mean
|
|
477
|
+
// storing a set per row. "Once per session" is exact for the ordinary single-session
|
|
478
|
+
// case and an upper bound of "once per session PAIR interleave" otherwise.
|
|
479
|
+
const scoped = typeof sessionId === 'string' && sessionId.length > 0;
|
|
480
|
+
const stmt = scoped
|
|
481
|
+
? db.prepare(`
|
|
482
|
+
UPDATE observations SET access_count = access_count + 1, last_accessed_at = ?,
|
|
483
|
+
last_access_session_id = ?
|
|
484
|
+
WHERE id = ? AND project = ? AND COALESCE(last_access_session_id, '') != ?
|
|
485
|
+
`)
|
|
486
|
+
: db.prepare(`
|
|
432
487
|
UPDATE observations SET access_count = access_count + 1, last_accessed_at = ?
|
|
433
488
|
WHERE id = ? AND project = ?
|
|
434
489
|
`);
|
|
@@ -436,7 +491,10 @@ export function bumpCitationAccess(db, ids, project, relevantIds, env = process.
|
|
|
436
491
|
let n = 0;
|
|
437
492
|
for (const id of idList) {
|
|
438
493
|
try {
|
|
439
|
-
|
|
494
|
+
// 0 changes is a SKIP, not a failure: either the row is not this project's, or
|
|
495
|
+
// this session already credited it. Both must leave `n` alone, because the caller
|
|
496
|
+
// logs it as "obs bumped".
|
|
497
|
+
const result = scoped ? stmt.run(now, sessionId, id, project, sessionId) : stmt.run(now, id, project);
|
|
440
498
|
if (result.changes > 0) n++;
|
|
441
499
|
} catch (e) {
|
|
442
500
|
debugCatch(e, `bumpCitationAccess-id-${id}`);
|
|
@@ -1427,6 +1485,19 @@ export function redirectSupersededIds(db, project, ids) {
|
|
|
1427
1485
|
* rows are currently boost-eligible; 25 of them are cited nowhere in the corpus, and
|
|
1428
1486
|
* at most 3 could have crossed `access_count > 3` on citations even under an upper
|
|
1429
1487
|
* bound that ignores the gate entirely. So the path is real and its effect is small.
|
|
1488
|
+
*
|
|
1489
|
+
* THAT BOUND IS RETRACTED (R11-B-P1-1, 2026-09-07). It was computed as "one citation
|
|
1490
|
+
* contributes at most 1", and that premise was false: `Stop` fires once per assistant
|
|
1491
|
+
* TURN and rescans the whole transcript, so before the `last_access_session_id` key one
|
|
1492
|
+
* citation contributed once per REMAINING TURN of its session — 338 credits over 43
|
|
1493
|
+
* distinct (session, id) pairs = 7.86x across 51 real transcripts, single-session worst
|
|
1494
|
+
* case 18.75x. The "at most 3" therefore understated its own quantity by an unmeasured
|
|
1495
|
+
* factor, and no replacement bound is stated here because none has been measured.
|
|
1496
|
+
*
|
|
1497
|
+
* The premise now holds going FORWARD only. Existing rows keep whatever inflated
|
|
1498
|
+
* `access_count` they accumulated — the true count is not recoverable from the inflated
|
|
1499
|
+
* one, so nothing is back-corrected. Any future reading of `access_count` as "how often
|
|
1500
|
+
* this was cited" must treat pre-v5.6.0 values as an upper bound, not a count.
|
|
1430
1501
|
* It is untouched here because `access_count` has other writers (explicit recall /
|
|
1431
1502
|
* get / timeline) and is also an input to noisePenaltyClause, so changing it is a
|
|
1432
1503
|
* different decision with a different blast radius.
|
package/lib/cite-back-hint.mjs
CHANGED
|
@@ -243,6 +243,52 @@ export function nextCiteLowStreak(priorStreak, stats, env = process.env) {
|
|
|
243
243
|
return ratioGateFires(stats, env) ? prior + 1 : 0;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/**
|
|
247
|
+
* R11-B-P1-2 — advance the self-silence streak by SESSION, which is the unit the
|
|
248
|
+
* docblock above has always claimed and the code never implemented.
|
|
249
|
+
*
|
|
250
|
+
* `Stop` fires once per assistant TURN, and the writer incremented on every fire, so
|
|
251
|
+
* with a median of ~6 turns per session the default silence-after-3 was reached inside
|
|
252
|
+
* the FIRST session and never recovered. Measured on this machine before the fix:
|
|
253
|
+
* `runtime/cite-recall-dev--claude-mem-lite.json` read lowStreak 58 for a project with
|
|
254
|
+
* 26 transcripts on disk — a per-session counter cannot exceed the session count — and
|
|
255
|
+
* 2 of 3 projects had the cite-#NN nudge permanently silenced.
|
|
256
|
+
*
|
|
257
|
+
* Recomputing from the value the session STARTED at (rather than freezing after the
|
|
258
|
+
* first fire) keeps two properties: a session advances the streak by at most 1, and a
|
|
259
|
+
* citation arriving in a later turn still resets it to 0 — which matters because the
|
|
260
|
+
* whole contract is "cite NEXT time you produce user-visible text".
|
|
261
|
+
*
|
|
262
|
+
* @param {object|null} prev parsed previous payload, or null when absent/unreadable
|
|
263
|
+
* @param {string|null} ccSessionId current CC session id
|
|
264
|
+
* @returns {{lowStreak: number, streakBase: number, lastStreakSession: string|null}}
|
|
265
|
+
*/
|
|
266
|
+
export function nextCiteStreakState(prev, ccSessionId, stats, env = process.env) {
|
|
267
|
+
let streakBase = 0;
|
|
268
|
+
if (prev && typeof prev === 'object') {
|
|
269
|
+
if (prev.lastStreakSession === undefined) {
|
|
270
|
+
// Written under the pre-R11 TURN semantics. The stored number counted Stop fires,
|
|
271
|
+
// so it is not convertible to sessions — discard it once, here, rather than
|
|
272
|
+
// leaving every existing install permanently silenced by a number that never
|
|
273
|
+
// meant what it was read as.
|
|
274
|
+
streakBase = 0;
|
|
275
|
+
} else if (ccSessionId && prev.lastStreakSession === ccSessionId) {
|
|
276
|
+
streakBase = Number(prev.streakBase) || 0;
|
|
277
|
+
} else {
|
|
278
|
+
streakBase = Number(prev.lowStreak) || 0;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
lowStreak: nextCiteLowStreak(streakBase, stats, env),
|
|
283
|
+
streakBase,
|
|
284
|
+
// null when there is no CC session to key on: the same-session branch above then
|
|
285
|
+
// never matches, so those invocations keep the old per-fire behaviour. There is no
|
|
286
|
+
// session identity available to do better, and the field's presence is what tells a
|
|
287
|
+
// later read that this file is post-R11.
|
|
288
|
+
lastStreakSession: ccSessionId || null,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
246
292
|
// Env opt-outs:
|
|
247
293
|
// • CLAUDE_MEM_NO_CITE_NUDGE=1 — disables BOTH gates (full silence)
|
|
248
294
|
// • CLAUDE_MEM_CITE_NUDGE_THRESHOLD — ratio gate threshold (default 0.6)
|
package/lib/export-columns.mjs
CHANGED
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
// Full round-trippable set: content + value-signals (access/cited/uncited/injection/decay)
|
|
14
14
|
// + branch + timing. `id` + `memory_session_id` are informational (restore remaps id and
|
|
15
15
|
// buckets under a synthetic restore session). Session-idempotency keys (last_decided/
|
|
16
|
-
//
|
|
17
|
-
// meaningless after a row is re-bucketed under a restore session.
|
|
16
|
+
// last_cited/last_access_session_id, demoted_at, optimized_at) are intentionally NOT
|
|
17
|
+
// exported — they are meaningless after a row is re-bucketed under a restore session.
|
|
18
|
+
// last_access_session_id (v48) joins that set for the same reason: it answers "did THIS cc
|
|
19
|
+
// session already credit this row", which no restored session can have done. Also intentionally NOT
|
|
18
20
|
// exported: `related_ids` (holds observation ids, stale/dangling after restore remaps ids)
|
|
19
21
|
// and `discovery_tokens` (a derived retrieval metric, rebuilt by the live system; exporting
|
|
20
22
|
// it would freeze a stale value into backups).
|
package/lib/search-core.mjs
CHANGED
|
@@ -261,7 +261,7 @@ export function searchPromptsFts(
|
|
|
261
261
|
${project ? 'AND s.project = ?' : ''}
|
|
262
262
|
${epochFrom !== null ? 'AND p.created_at_epoch >= ?' : ''}
|
|
263
263
|
${epochTo !== null ? 'AND p.created_at_epoch <= ?' : ''}
|
|
264
|
-
ORDER BY p.created_at_epoch DESC
|
|
264
|
+
ORDER BY p.created_at_epoch DESC, p.id DESC
|
|
265
265
|
LIMIT ? OFFSET ?
|
|
266
266
|
`,
|
|
267
267
|
)
|
|
@@ -977,13 +977,22 @@ export async function coreRunSearchPipeline(ctx, opts) {
|
|
|
977
977
|
typeWheres.push('COALESCE(importance, 1) >= ?');
|
|
978
978
|
typeParams.push(importance);
|
|
979
979
|
}
|
|
980
|
+
// R11-A-P1-1: `branch` was the one caller-supplied filter this rebuilt WHERE did not
|
|
981
|
+
// carry, and nothing downstream compensates — applyTierFilter only reads tier, and
|
|
982
|
+
// finalizeSearchPage only counts. The leak surfaced exactly when FTS matched nothing,
|
|
983
|
+
// so the wrong-branch rows were the ONLY rows the caller saw, with `total` clamped to
|
|
984
|
+
// them so the page read like a legitimate hit.
|
|
985
|
+
if (branch) {
|
|
986
|
+
typeWheres.push('branch = ?');
|
|
987
|
+
typeParams.push(branch);
|
|
988
|
+
}
|
|
980
989
|
typeParams.push(limit);
|
|
981
990
|
const typeRows = db
|
|
982
991
|
.prepare(
|
|
983
992
|
`
|
|
984
993
|
SELECT id, type, title, subtitle, project, created_at, importance, files_modified
|
|
985
994
|
FROM observations WHERE ${typeWheres.join(' AND ')}
|
|
986
|
-
ORDER BY created_at_epoch DESC LIMIT ?
|
|
995
|
+
ORDER BY created_at_epoch DESC, id DESC LIMIT ?
|
|
987
996
|
`,
|
|
988
997
|
)
|
|
989
998
|
.all(...typeParams);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "5.
|
|
9
|
+
"version": "5.6.0",
|
|
10
10
|
"os": [
|
|
11
11
|
"darwin",
|
|
12
12
|
"linux"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.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/schema.mjs
CHANGED
|
@@ -156,7 +156,18 @@ export const CODE_DIR = join(homedir(), '.claude-mem-lite');
|
|
|
156
156
|
// every existing install at v46 a new index there would simply never be created. Same trap
|
|
157
157
|
// the FTS5 migration hit — a DDL change that is not reachable from the version the DB
|
|
158
158
|
// already reports is a no-op with a convincing diff.
|
|
159
|
-
|
|
159
|
+
// v48 (R11-B-P1-1): observations.last_access_session_id — the THIRD per-row session key
|
|
160
|
+
// on this table, and it exists for the same reason as the other two. `Stop` fires once
|
|
161
|
+
// per assistant TURN and rescans the whole transcript, so `bumpCitationAccess` re-credited
|
|
162
|
+
// one citation on every later turn of the same session: real-corpus replay over 51
|
|
163
|
+
// transcripts read 338 credits across 43 distinct (session, id) pairs = 7.86x, single
|
|
164
|
+
// session worst case 18.75x. That feeds boostAccessed (access_count > 3 → importance + 1,
|
|
165
|
+
// in DEFAULT_MAINTAIN_OPS, unattended daily) and suppresses noisePenaltyClause, whose
|
|
166
|
+
// predicate reads `injection_count > access_count * 3`. Additive + nullable: legacy rows
|
|
167
|
+
// read NULL and are credited exactly once more, on their next citation, then stamp.
|
|
168
|
+
// The column holds the LAST crediting session, not a set, so two same-project sessions
|
|
169
|
+
// interleaving their turns flip it between them — see the scope note in bumpCitationAccess.
|
|
170
|
+
export const CURRENT_SCHEMA_VERSION = 48;
|
|
160
171
|
|
|
161
172
|
// Sentinel columns for the LATEST migration set(s). The fast-path uses these
|
|
162
173
|
// to self-heal half-migrated DBs — schema_version bumped but column ALTERs
|
|
@@ -177,6 +188,7 @@ export const CURRENT_SCHEMA_VERSION = 47;
|
|
|
177
188
|
// pragma_table_info on a missing table returns zero rows (it does not throw), so
|
|
178
189
|
// naming any column of the new table is a table-presence check.
|
|
179
190
|
const LATEST_MIGRATION_COLUMNS = [
|
|
191
|
+
{ table: 'observations', column: 'last_access_session_id' }, // v48
|
|
180
192
|
{ table: 'observations', column: 'decay_seen_at_first_cite' }, // v46
|
|
181
193
|
{ table: 'citation_surface_log', column: 'surface' }, // v45
|
|
182
194
|
{ table: 'observations', column: 'scope' }, // v44
|
|
@@ -397,6 +409,14 @@ const MIGRATIONS = [
|
|
|
397
409
|
// destroy the distinction the column exists to record. Legacy rows stay NULL — they
|
|
398
410
|
// are not evidence of anything and must not be read as first-cite-at-0.
|
|
399
411
|
'ALTER TABLE observations ADD COLUMN decay_seen_at_first_cite INTEGER DEFAULT NULL',
|
|
412
|
+
// v48 (R11-B-P1-1): the access-channel idempotency key. Sibling of
|
|
413
|
+
// last_decided_session_id (v40, uncited/streak arm) and last_cited_session_id (v41,
|
|
414
|
+
// promote arm) — three channels fire out of one Stop hook, each needs its own key
|
|
415
|
+
// because they resolve different id sets: decay reads mainOnly, access reads the whole
|
|
416
|
+
// transcript including sidechains, and the decay pair is additionally gated on
|
|
417
|
+
// hasMainThreadAssistantText, so a session can credit access while decay never runs.
|
|
418
|
+
// Sharing a key would make one channel silence the other.
|
|
419
|
+
'ALTER TABLE observations ADD COLUMN last_access_session_id TEXT DEFAULT NULL',
|
|
400
420
|
];
|
|
401
421
|
|
|
402
422
|
/**
|
package/search-engine.mjs
CHANGED
|
@@ -518,7 +518,8 @@ export function findFtsAnchor(
|
|
|
518
518
|
AND (? IS NULL OR o.project = ?)
|
|
519
519
|
AND ${liveObsFilterSql('o')}
|
|
520
520
|
ORDER BY ${OBS_BM25}
|
|
521
|
-
* ${recencyDecaySql({ tsExpr: 'o.created_at_epoch', halfLifeSql: `${halfLifeMs}.0` })}
|
|
521
|
+
* ${recencyDecaySql({ tsExpr: 'o.created_at_epoch', halfLifeSql: `${halfLifeMs}.0` })},
|
|
522
|
+
o.id DESC
|
|
522
523
|
LIMIT 1
|
|
523
524
|
`;
|
|
524
525
|
const stmt = db.prepare(sql);
|
|
@@ -579,7 +580,7 @@ export function searchObservationsHybrid(db, ctx) {
|
|
|
579
580
|
`
|
|
580
581
|
SELECT id, type, title, subtitle, project, created_at, created_at_epoch, files_modified, importance, lesson_learned
|
|
581
582
|
FROM observations ${where}
|
|
582
|
-
ORDER BY created_at_epoch DESC
|
|
583
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
583
584
|
LIMIT ? OFFSET ?
|
|
584
585
|
`,
|
|
585
586
|
)
|