claude-mem-lite 5.3.0 → 5.4.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 +8 -5
- package/hook-handoff.mjs +20 -10
- package/hook.mjs +57 -15
- package/install.mjs +35 -5
- package/lib/maintain-core.mjs +15 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/hook-launcher.mjs +17 -6
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "5.
|
|
13
|
+
"version": "5.4.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.4.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
|
@@ -120,7 +120,7 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
|
|
|
120
120
|
- **Schema auto-migration** -- Idempotent `ALTER TABLE` migrations run on every startup, safely adding new columns and indexes without data loss
|
|
121
121
|
- **LLM concurrency control** -- File-based semaphore limits background workers to 2 concurrent LLM calls, preventing resource contention
|
|
122
122
|
- **stdin overflow protection** -- Hook input truncated at 256KB with regex-based action salvage for oversized tool outputs
|
|
123
|
-
- **Cross-session handoff** -- Captures session state (request, completed work, next steps, key files) on `/exit`, then injects context when the next session detects continuation intent via explicit keywords or FTS5 term overlap. **The `/clear` and `/compact` arm
|
|
123
|
+
- **Cross-session handoff** -- Captures session state (request, completed work, next steps, key files) on `/exit`, then injects context when the next session detects continuation intent via explicit keywords or FTS5 term overlap. **The `/clear` and `/compact` arm fires since v5.4.0** (R10-P1-1); before that it had never once written a row — `session_handoffs` on the maintainer's install held 4 `exit` rows and **0** `clear` rows. Two host facts settled it, both measured rather than assumed. (1) `Stop` runs at the end of every assistant *turn*, not once per session, and it deleted the session file that SessionStart reads to learn which session just ended — so the branch was unreachable, and mem sessions were minted per turn (58 prompts over 16 host sessions produced 56 mem sessions and 56 summary rows, 2026-09-07). (2) Claude Code **rotates its session id across `/clear`**: of 21 real transcripts, 12 carry a `/clear` command record, and in 12/12 that record's timestamp precedes its own file's first record by ~0.1s — the command is issued in the old session and replayed into a new file under a new id. So `Stop` no longer deletes the file, SessionStart asks the host's `source` (`startup`/`clear`/`compact`/`resume`) instead of guessing from the file, and the handoff's prompt lookup falls back to the unscoped set when the new session's id matches none. Revert path: `CLAUDE_MEM_LEGACY_STOP_UNLINK=1`
|
|
124
124
|
- **Git-SHA continuation anchor** (v2.31.0) -- Handoff rows include `git_sha_at_handoff`; any handoff matching the current `HEAD` counts as continuation regardless of TTL. Code state is a stronger continuation signal than wall-clock time
|
|
125
125
|
- **Startup dashboard** (v2.31.0) -- SessionStart hook aggregates `git status` + `~/.claude/tasks/*.json` + `~/.claude/plans/*.md` + most-recent exit handoff + recent event count into a single structured block injected via `hookSpecificOutput.additionalContext`
|
|
126
126
|
- **Activity namespace** (v2.31.0) -- Dedicated `events` table + FTS5 for non-memdir types (`bugfix`, `lesson`, `bug`, `discovery`, `refactor`, `feature`, `observation`, `decision`) that don't compete with `WHAT_NOT_TO_SAVE` semantics on the observations table. CLI: `claude-mem-lite activity save|search|recent|show`. `hook-llm` routes non-memdir summary types through `persistHaikuSummary` so upgrades from observations→events are atomic. (v3.39: the `/lesson` and `/bug` slash commands were redirected from this events table to searchable **observations** — `mem_search` never read the events table, so explicit saves were unfindable; the events table remains the auto-capture activity log.)
|
|
@@ -431,9 +431,10 @@ FTS5 indexes: `observations_fts` (title, subtitle, narrative, text, facts, conce
|
|
|
431
431
|
|
|
432
432
|
```
|
|
433
433
|
SessionStart
|
|
434
|
-
->
|
|
435
|
-
|
|
436
|
-
|
|
434
|
+
-> Read the host's `source` (startup | clear | compact | resume) from stdin
|
|
435
|
+
-> On clear/compact: read the outgoing session from the session file, save its
|
|
436
|
+
'clear' handoff, emit the Working State block (R10-P1-1, fixed v5.4.0)
|
|
437
|
+
-> Generate session ID (overwrites the session file)
|
|
437
438
|
-> Mark stale sessions (>24h active) as abandoned
|
|
438
439
|
-> Clean orphaned/stale lock files
|
|
439
440
|
-> Query recent observations (24h)
|
|
@@ -462,8 +463,9 @@ Stop
|
|
|
462
463
|
-> Flush final episode buffer
|
|
463
464
|
-> Save handoff snapshot (type 'exit')
|
|
464
465
|
-> Mark session completed
|
|
465
|
-
-> Delete the session file <- what makes the SessionStart /clear branch unreachable
|
|
466
466
|
-> Spawn LLM summary worker (poll-based wait)
|
|
467
|
+
-> Keep the session file <- Stop fires per TURN; deleting it here re-minted a mem
|
|
468
|
+
session every turn and left the SessionStart /clear branch unreachable (v5.4.0)
|
|
467
469
|
```
|
|
468
470
|
|
|
469
471
|
|
|
@@ -828,6 +830,7 @@ what is already stored — only whether new work runs.
|
|
|
828
830
|
| Variable | Description | Default |
|
|
829
831
|
|----------|-------------|---------|
|
|
830
832
|
| `CLAUDE_MEM_SKIP_SUMMARY` | Skip the background LLM session summary at **both** of its spawn sites — `Stop`, and the SessionStart `/clear`-handoff path. Until v5.3.0 only the `Stop` one honoured it. | _(runs)_ |
|
|
833
|
+
| `CLAUDE_MEM_LEGACY_STOP_UNLINK` | Restore the pre-v5.4.0 behaviour where `Stop` deletes the session file. Documented revert path for the session-lifecycle change, not a supported configuration: it re-mints a mem session per turn and makes the `/clear` handoff unreachable again. Only reach for it on a host that fires `Stop` once per session rather than once per turn. | _(file kept)_ |
|
|
831
834
|
| `CLAUDE_MEM_SKIP_EPISODE_LLM` | Skip LLM extraction on episode flush — observations are still batched, just not summarized. | _(runs)_ |
|
|
832
835
|
| `CLAUDE_MEM_SKIP_SAVE_ENRICH` | Skip the background Haiku call that backfills `lesson_learned` / search aliases after a save. | _(runs)_ |
|
|
833
836
|
| `CLAUDE_MEM_SKIP_COMPRESS` | Skip auto-compression of old observations. | _(runs)_ |
|
package/hook-handoff.mjs
CHANGED
|
@@ -56,25 +56,35 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
|
|
|
56
56
|
// scopeSessionId is absent or == sessionId (legacy/test/no-stdin), fall back to the
|
|
57
57
|
// unfiltered query (identical to pre-D#26 behavior).
|
|
58
58
|
const ccScope = scopeSessionId && scopeSessionId !== sessionId ? scopeSessionId : null;
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
const unscopedPrompts = () =>
|
|
60
|
+
db
|
|
61
|
+
.prepare(
|
|
62
|
+
`
|
|
63
63
|
SELECT prompt_text FROM user_prompts
|
|
64
|
-
WHERE content_session_id = ?
|
|
64
|
+
WHERE content_session_id = ?
|
|
65
65
|
ORDER BY prompt_number ASC LIMIT 5
|
|
66
66
|
`,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
)
|
|
68
|
+
.all(sessionId);
|
|
69
|
+
let prompts = ccScope
|
|
70
|
+
? db
|
|
70
71
|
.prepare(
|
|
71
72
|
`
|
|
72
73
|
SELECT prompt_text FROM user_prompts
|
|
73
|
-
WHERE content_session_id = ?
|
|
74
|
+
WHERE content_session_id = ? AND (cc_session_id = ? OR cc_session_id IS NULL)
|
|
74
75
|
ORDER BY prompt_number ASC LIMIT 5
|
|
75
76
|
`,
|
|
76
77
|
)
|
|
77
|
-
.all(sessionId)
|
|
78
|
+
.all(sessionId, ccScope)
|
|
79
|
+
: unscopedPrompts();
|
|
80
|
+
// R10-P1-1: on the /clear path the scope is the NEW session's CC id while the prompts
|
|
81
|
+
// being handed off belong to the OLD one, and the host rotates that id across /clear
|
|
82
|
+
// (measured 12/12 on real transcripts, 2026-09-07) — so the scoped query returns 0 and
|
|
83
|
+
// the whole handoff was silently skipped. Fall back to the unscoped set when, and only
|
|
84
|
+
// when, the scoped one is EMPTY: D#26 exists to stop two live sessions being MERGED into
|
|
85
|
+
// one working_on, and there is nothing to merge with when this session contributed no
|
|
86
|
+
// prompts. The alternative at that point is not a cleaner row, it is no row at all.
|
|
87
|
+
if (ccScope && prompts.length === 0) prompts = unscopedPrompts();
|
|
78
88
|
if (prompts.length === 0) return; // Empty session — nothing to hand off
|
|
79
89
|
|
|
80
90
|
// Filter prompts whose only content is workflow/control language ("继续",
|
package/hook.mjs
CHANGED
|
@@ -1537,10 +1537,32 @@ async function handleStop() {
|
|
|
1537
1537
|
// recreate at 432ms and watched a 300ms grace lose.
|
|
1538
1538
|
if (!process.env.CLAUDE_MEM_SKIP_SUMMARY) spawnBackground('llm-summary', sessionId, project);
|
|
1539
1539
|
|
|
1540
|
-
//
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1540
|
+
// The session file deliberately SURVIVES Stop (R10-P1-1). It used to be unlinked here,
|
|
1541
|
+
// on the model "Stop = /exit = the session is over". The host does not work that way:
|
|
1542
|
+
// Stop fires at the end of EVERY assistant turn, so the unlink minted a fresh mem
|
|
1543
|
+
// session on the next event and cost two things at once —
|
|
1544
|
+
//
|
|
1545
|
+
// • `sdk_sessions` / `session_summaries` counted turns, not sessions. Measured on the
|
|
1546
|
+
// maintainer's live DB 2026-09-07: 58 prompts over 16 host sessions produced 56
|
|
1547
|
+
// distinct mem sessions and 56 summary rows, 0 of which carried the LLM-only fields.
|
|
1548
|
+
// • handleSessionStart's mid-restart probe reads this file to learn which session just
|
|
1549
|
+
// ended. With it deleted every turn the probe never fired, so the /clear handoff
|
|
1550
|
+
// branch was unreachable in production — 0 `clear` rows against 21 real sessions.
|
|
1551
|
+
//
|
|
1552
|
+
// Lifetime is bounded by SESSION_EXPIRY_MS (12h) in getSessionId(), and every
|
|
1553
|
+
// SessionStart overwrites it via createSessionId(), so "one mem session per host
|
|
1554
|
+
// session" holds without anything having to delete it.
|
|
1555
|
+
//
|
|
1556
|
+
// CLAUDE_MEM_LEGACY_STOP_UNLINK=1 restores the pre-v5.4.0 unlink. It exists because the
|
|
1557
|
+
// measurements above are from ONE host build; a host that fires Stop once per session
|
|
1558
|
+
// instead of once per turn would be better served by the old shape, and a user who hits
|
|
1559
|
+
// that has no other lever. It is not a supported configuration — it re-breaks the /clear
|
|
1560
|
+
// handoff by design.
|
|
1561
|
+
if (process.env.CLAUDE_MEM_LEGACY_STOP_UNLINK === '1') {
|
|
1562
|
+
try {
|
|
1563
|
+
unlinkSync(sessionFile());
|
|
1564
|
+
} catch {}
|
|
1565
|
+
}
|
|
1544
1566
|
}
|
|
1545
1567
|
|
|
1546
1568
|
// ─── SessionStart Handler + CLAUDE.md Persistence (Tier 1 A, E) ─────────────
|
|
@@ -2336,13 +2358,20 @@ async function handleSessionStart() {
|
|
|
2336
2358
|
|
|
2337
2359
|
// Read CC real session_id from hook stdin — used to scope handoff rows so parallel
|
|
2338
2360
|
// sessions for the same project don't clobber each other (see docs/bug.txt).
|
|
2361
|
+
// `source` (startup | clear | compact | resume) is read here too: since Stop stopped
|
|
2362
|
+
// deleting the session file, the file's survival no longer tells us WHY this session
|
|
2363
|
+
// started, and the host's own word is the only non-guess (R10-P1-1).
|
|
2339
2364
|
let ccSessionId = null;
|
|
2365
|
+
let startSource = null;
|
|
2340
2366
|
try {
|
|
2341
2367
|
const raw = await readStdin();
|
|
2342
2368
|
const hookData = JSON.parse(raw.text);
|
|
2343
2369
|
if (typeof hookData?.session_id === 'string' && hookData.session_id.length > 0) {
|
|
2344
2370
|
ccSessionId = hookData.session_id;
|
|
2345
2371
|
}
|
|
2372
|
+
if (typeof hookData?.source === 'string' && hookData.source.length > 0) {
|
|
2373
|
+
startSource = hookData.source;
|
|
2374
|
+
}
|
|
2346
2375
|
} catch {
|
|
2347
2376
|
/* stdin unavailable — legacy behavior */
|
|
2348
2377
|
}
|
|
@@ -2390,19 +2419,32 @@ async function handleSessionStart() {
|
|
|
2390
2419
|
}
|
|
2391
2420
|
}
|
|
2392
2421
|
|
|
2393
|
-
// Detect mid-session restart (/clear or /compact)
|
|
2394
|
-
//
|
|
2395
|
-
//
|
|
2396
|
-
//
|
|
2422
|
+
// Detect mid-session restart (/clear or /compact) and carry the ending session forward.
|
|
2423
|
+
// Read BEFORE createSessionId() overwrites the session file.
|
|
2424
|
+
//
|
|
2425
|
+
// The discriminator is the host's `source`, NOT the session file's survival. The old
|
|
2426
|
+
// comment here read "normal /exit deletes the file, so this only triggers for /clear,
|
|
2427
|
+
// /compact, or crash recovery" — but the deleter was Stop, which fires every turn, so
|
|
2428
|
+
// the file was always gone and this branch never triggered (R10-P1-1). Now that Stop
|
|
2429
|
+
// keeps the file, the file is always THERE, and asking it "why did this session start"
|
|
2430
|
+
// would answer /clear for a plain launch too. Only the host knows.
|
|
2431
|
+
//
|
|
2432
|
+
// `startup` and `resume` mean the previous session ended on its own terms and already
|
|
2433
|
+
// wrote its per-turn `exit` handoff, which UserPromptSubmit reads back — no clear
|
|
2434
|
+
// snapshot is owed. A null source (no stdin: tests, legacy hosts) keeps the old
|
|
2435
|
+
// file-presence behavior so nothing that used to reach this branch stops reaching it.
|
|
2436
|
+
const isMidSessionRestart = startSource !== 'startup' && startSource !== 'resume';
|
|
2397
2437
|
let prevSessionId = null;
|
|
2398
2438
|
let prevProject = null;
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2439
|
+
if (isMidSessionRestart) {
|
|
2440
|
+
try {
|
|
2441
|
+
const data = JSON.parse(readFileSync(sessionFile(), 'utf8'));
|
|
2442
|
+
if (Date.now() - data.startedAt < SESSION_EXPIRY_MS) {
|
|
2443
|
+
prevSessionId = data.id;
|
|
2444
|
+
prevProject = data.project;
|
|
2445
|
+
}
|
|
2446
|
+
} catch {} // No session file = fresh startup, nothing to recover
|
|
2447
|
+
}
|
|
2406
2448
|
|
|
2407
2449
|
// Tier 1 A: Create unique session ID
|
|
2408
2450
|
const sessionId = createSessionId();
|
package/install.mjs
CHANGED
|
@@ -509,7 +509,7 @@ function registerMcpServer() {
|
|
|
509
509
|
}
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
-
function dedupePluginCacheAndHooks({ managedHooks } = {}) {
|
|
512
|
+
export function dedupePluginCacheAndHooks({ managedHooks, isDev = false } = {}) {
|
|
513
513
|
// 3b. Deduplicate: if marketplace plugin also registers MCP + hooks,
|
|
514
514
|
// clear them to prevent double execution. install.mjs hooks (in settings.json)
|
|
515
515
|
// point to ~/.claude-mem-lite/ (latest code in dev mode via symlinks),
|
|
@@ -593,17 +593,47 @@ function dedupePluginCacheAndHooks({ managedHooks } = {}) {
|
|
|
593
593
|
const cacheBase = join(homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_KEY, 'claude-mem-lite');
|
|
594
594
|
if (existsSync(cacheBase)) {
|
|
595
595
|
const launchSyncFiles = ['launch.mjs', 'launch-preflight.mjs'];
|
|
596
|
+
// Read, not remembered: the cache dir names ARE versions, so the comparison has to
|
|
597
|
+
// be against what this installer actually is. A stale constant here would re-open
|
|
598
|
+
// R10-P2-11 on the next release without changing a line of this block.
|
|
599
|
+
let selfVersion = null;
|
|
600
|
+
try {
|
|
601
|
+
selfVersion = JSON.parse(readFileSync(join(PROJECT_DIR, 'package.json'), 'utf8')).version;
|
|
602
|
+
} catch {
|
|
603
|
+
/* no readable package.json — treat every version as non-matching (sync nothing) */
|
|
604
|
+
}
|
|
596
605
|
let clearedHooks = 0;
|
|
597
606
|
for (const ver of readdirSync(cacheBase)) {
|
|
598
607
|
const verDir = join(cacheBase, ver);
|
|
599
608
|
|
|
600
|
-
// Sync launch.mjs + its preflight companion (issue #15)
|
|
601
|
-
|
|
609
|
+
// Sync launch.mjs + its preflight companion (issue #15).
|
|
610
|
+
//
|
|
611
|
+
// R10-P2-11: this used to run for EVERY cached version. Issue #15 is a dev-mode
|
|
612
|
+
// routing fix — the point is that a dev tree's launch.mjs reaches the cache the
|
|
613
|
+
// MCP server starts from — but nothing gated it, so a plain `install` (and the
|
|
614
|
+
// repair that SessionStart spawns in the background) pushed the installer's entry
|
|
615
|
+
// point into every OLD version dir, where it runs against that version's own
|
|
616
|
+
// `lib/`. Entry point and library are versioned together: HEAD's launch.mjs:72-73
|
|
617
|
+
// destructures `nativeBindingRepairHint` from ../lib/binding-probe.mjs, which
|
|
618
|
+
// v3.95.0 does not export, so :110 throws inside a catch and the user's repair
|
|
619
|
+
// hint disappears — a silent downgrade of the one message that tells them how to
|
|
620
|
+
// fix a dead binding. Reproduced in tests/sandbox/phaseB-npm.mjs §B9 (the old
|
|
621
|
+
// dir came back 9802B with `nativeBindingRepairHint` in it), which is the
|
|
622
|
+
// reproduction R10 §8 required before touching install().
|
|
623
|
+
//
|
|
624
|
+
// Dev mode still syncs everything: that is the fix's whole purpose, and a dev
|
|
625
|
+
// tree has no old versions to protect. Otherwise only the version dir that
|
|
626
|
+
// matches this installer — same release, so same expectations of `lib/`.
|
|
627
|
+
const versionMatches = isDev || ver === selfVersion;
|
|
628
|
+
if (versionMatches && existsSync(join(verDir, 'scripts'))) {
|
|
602
629
|
for (const f of launchSyncFiles) {
|
|
603
630
|
const src = join(PROJECT_DIR, 'scripts', f);
|
|
604
631
|
if (existsSync(src)) {
|
|
605
632
|
try {
|
|
606
|
-
|
|
633
|
+
// Atomic for the same reason the two hooks.json writes above are: a
|
|
634
|
+
// torn launch.mjs is the MCP server's entry point, and the reader is
|
|
635
|
+
// Claude Code starting it, not us.
|
|
636
|
+
atomicWriteFileSync(join(verDir, 'scripts', f), readFileSync(src));
|
|
607
637
|
} catch {
|
|
608
638
|
/* keep going */
|
|
609
639
|
}
|
|
@@ -959,7 +989,7 @@ async function install() {
|
|
|
959
989
|
// re-read settings.json) is what keeps a future reorder from silently turning the
|
|
960
990
|
// dedup off — the dependency is data, not sequence.
|
|
961
991
|
const managedHooks = configureHooks();
|
|
962
|
-
dedupePluginCacheAndHooks({ managedHooks });
|
|
992
|
+
dedupePluginCacheAndHooks({ managedHooks, isDev: IS_DEV });
|
|
963
993
|
backupLegacyClaudeMemData();
|
|
964
994
|
verifyDatabase();
|
|
965
995
|
await dogfoodAutoAdopt();
|
package/lib/maintain-core.mjs
CHANGED
|
@@ -394,6 +394,21 @@ export function cleanupBroken(db, { projectFilter, baseParams, opCap = OP_CAP })
|
|
|
394
394
|
-- lesson). Parity with the "lessons never auto-GC" guards in
|
|
395
395
|
-- decayAndMarkIdle / selectCompressionCandidates / findSmartCompressCandidates.
|
|
396
396
|
AND (lesson_learned IS NULL OR lesson_learned = '' OR lesson_learned = 'none')
|
|
397
|
+
-- D#4. The one HARD DELETE in this family, and until now the only site whose
|
|
398
|
+
-- exemption from liveObsFilterSql was a LIKELIHOOD argument rather than an
|
|
399
|
+
-- inertness proof: these rows have no title, narrative or lesson, so they are
|
|
400
|
+
-- absent from every injection surface, so an id that was never injected is not one
|
|
401
|
+
-- a #NN cites. Narrow, but reachable -- a hand-typed #NN, or a numeric
|
|
402
|
+
-- "save --supersedes" chain later blanked by a degenerate cluster-merge -- and what
|
|
403
|
+
-- the delete takes with it is the superseded_by that
|
|
404
|
+
-- citation-tracker.redirectSupersededIds (:1363) follows to credit a corrected
|
|
405
|
+
-- memory's #NN to its successor.
|
|
406
|
+
--
|
|
407
|
+
-- Deliberately NOT the full liveObsFilterSql. A retired row whose superseded_by is
|
|
408
|
+
-- null hands that redirect nothing: it falls through to out.add(id) (:1379-1381),
|
|
409
|
+
-- the same answer a missing row produces. Filtering on superseded_at instead would
|
|
410
|
+
-- strand every empty retired row here forever for no gain.
|
|
411
|
+
AND superseded_by IS NULL
|
|
397
412
|
${projectFilter} LIMIT ${opCap}
|
|
398
413
|
`,
|
|
399
414
|
)
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.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.4.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.4.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",
|
|
@@ -124,12 +124,23 @@ if (!entryArg) {
|
|
|
124
124
|
|
|
125
125
|
const entryAbs = entryArg.startsWith('/') ? entryArg : join(INSTALL_DIR, entryArg);
|
|
126
126
|
|
|
127
|
-
// Swap barrier. An auto-update
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
127
|
+
// Swap barrier. An auto-update renames files into the install dir one at a time —
|
|
128
|
+
// atomic per file, not per file SET — so a hook process that starts mid-swap can
|
|
129
|
+
// resolve its entry from the old version and an import from the new one.
|
|
130
|
+
// hook-update.mjs marks that window; skip the fire instead of importing a mixed
|
|
131
|
+
// module graph. Hooks are best-effort and the swap lasts ~a second, so the next
|
|
132
|
+
// fire runs against a settled install.
|
|
133
|
+
//
|
|
134
|
+
// This covers the AUTO-UPDATE path only, and the distinction is not pedantic:
|
|
135
|
+
// `hook-update.mjs:719` is the sole writer of this marker in the whole tree (name set,
|
|
136
|
+
// 2026-09-07), so the barrier is never armed for `install.mjs install` — which copies the
|
|
137
|
+
// same file set in place with copyFileSync (`install.mjs:341-350`) and is what
|
|
138
|
+
// `install.mjs repair` ends up executing (`:2388`, after verifying the release). A repair
|
|
139
|
+
// spawned in the background at SessionStart therefore overwrites this tree while hooks
|
|
140
|
+
// keep firing into it. That is R10 P2-12, still open: the mechanism is settled, the runtime
|
|
141
|
+
// symptom is not reproduced, and R10 §8 asks for a repro in tests/sandbox/phaseB-npm.mjs
|
|
142
|
+
// before install()'s main path is touched. An earlier draft of this comment said "auto-update
|
|
143
|
+
// / repair", which reads as though the repair path were already covered — it is not.
|
|
133
144
|
//
|
|
134
145
|
// Stale-guarded on BOTH pid and ts: an updater killed mid-swap leaves the marker
|
|
135
146
|
// behind, and a marker that outlives its writer must never mute hooks permanently.
|