claude-mem-lite 2.33.0 → 2.33.2
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/.mcp.json +0 -0
- package/LICENSE +0 -0
- package/README.md +0 -0
- package/README.zh-CN.md +0 -0
- package/adopt-cli.mjs +0 -0
- package/adopt-content.mjs +0 -0
- package/bash-utils.mjs +0 -0
- package/commands/adopt.md +0 -0
- package/commands/bug.md +0 -0
- package/commands/lesson.md +0 -0
- package/commands/mem.md +0 -0
- package/commands/memory.md +0 -0
- package/commands/tools.md +0 -0
- package/commands/unadopt.md +0 -0
- package/commands/update.md +0 -0
- package/format-utils.mjs +0 -0
- package/haiku-client.mjs +0 -0
- package/hash-utils.mjs +0 -0
- package/hook-context.mjs +0 -0
- package/hook-episode.mjs +0 -0
- package/hook-handoff.mjs +14 -3
- package/hook-llm.mjs +17 -5
- package/hook-memory.mjs +0 -0
- package/hook-optimize.mjs +0 -0
- package/hook-semaphore.mjs +0 -0
- package/hook-shared.mjs +0 -0
- package/hook-update.mjs +0 -0
- package/hook.mjs +54 -19
- package/hooks/hooks.json +0 -0
- package/install-metadata.mjs +0 -0
- package/install.mjs +0 -0
- package/lib/activity.mjs +0 -0
- package/lib/doctor-benchmark.mjs +0 -0
- package/lib/git-state.mjs +0 -0
- package/lib/plan-reader.mjs +0 -0
- package/lib/startup-dashboard.mjs +0 -0
- package/lib/task-reader.mjs +0 -0
- package/mem-cli.mjs +0 -0
- package/memdir.mjs +0 -0
- package/nlp.mjs +0 -0
- package/package.json +1 -1
- package/plugin-cache-guard.mjs +0 -0
- package/project-utils.mjs +0 -0
- package/registry/preinstalled.json +0 -0
- package/registry-enricher.mjs +0 -0
- package/registry-github.mjs +0 -0
- package/registry-importer.mjs +0 -0
- package/registry-indexer.mjs +0 -0
- package/registry-retriever.mjs +0 -0
- package/registry-scanner.mjs +0 -0
- package/registry.mjs +0 -0
- package/resource-discovery.mjs +0 -0
- package/schema.mjs +0 -0
- package/scoring-sql.mjs +0 -0
- package/scripts/launch.mjs +0 -0
- package/scripts/pre-skill-bridge.js +0 -0
- package/scripts/pre-tool-recall.js +58 -17
- package/scripts/prompt-search-utils.mjs +0 -0
- package/scripts/user-prompt-search.js +24 -3
- package/secret-scrub.mjs +0 -0
- package/server-internals.mjs +0 -0
- package/server.mjs +0 -0
- package/skill.md +0 -0
- package/skip-tools.mjs +0 -0
- package/source-files.mjs +0 -0
- package/stop-words.mjs +0 -0
- package/synonyms.mjs +0 -0
- package/tfidf.mjs +0 -0
- package/tier.mjs +0 -0
- package/tool-schemas.mjs +0 -0
- package/utils.mjs +0 -0
package/.mcp.json
CHANGED
|
File without changes
|
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
File without changes
|
package/README.zh-CN.md
CHANGED
|
File without changes
|
package/adopt-cli.mjs
CHANGED
|
File without changes
|
package/adopt-content.mjs
CHANGED
|
File without changes
|
package/bash-utils.mjs
CHANGED
|
File without changes
|
package/commands/adopt.md
CHANGED
|
File without changes
|
package/commands/bug.md
CHANGED
|
File without changes
|
package/commands/lesson.md
CHANGED
|
File without changes
|
package/commands/mem.md
CHANGED
|
File without changes
|
package/commands/memory.md
CHANGED
|
File without changes
|
package/commands/tools.md
CHANGED
|
File without changes
|
package/commands/unadopt.md
CHANGED
|
File without changes
|
package/commands/update.md
CHANGED
|
File without changes
|
package/format-utils.mjs
CHANGED
|
File without changes
|
package/haiku-client.mjs
CHANGED
|
File without changes
|
package/hash-utils.mjs
CHANGED
|
File without changes
|
package/hook-context.mjs
CHANGED
|
File without changes
|
package/hook-episode.mjs
CHANGED
|
File without changes
|
package/hook-handoff.mjs
CHANGED
|
@@ -16,13 +16,21 @@ import * as taskReaderModule from './lib/task-reader.mjs';
|
|
|
16
16
|
/**
|
|
17
17
|
* Build and save a handoff snapshot to session_handoffs table.
|
|
18
18
|
* Called synchronously during handleStop (/exit) or handleSessionStart (/clear).
|
|
19
|
+
*
|
|
20
|
+
* Dual id: `sessionId` is the mem-internal id that user_prompts / observations
|
|
21
|
+
* were written with (handleUserPrompt uses getSessionId()) — it drives all
|
|
22
|
+
* DB lookups. `scopeSessionId` is the CC UUID from hook stdin used to scope
|
|
23
|
+
* the stored row so parallel CC sessions don't clobber each other. When
|
|
24
|
+
* `scopeSessionId` is null/undefined, `sessionId` is used for both (legacy).
|
|
25
|
+
*
|
|
19
26
|
* @param {Database} db Opened main database
|
|
20
|
-
* @param {string} sessionId
|
|
27
|
+
* @param {string} sessionId Mem-internal session id (query key)
|
|
21
28
|
* @param {string} project Project identifier
|
|
22
29
|
* @param {'clear'|'exit'} type Handoff type
|
|
23
30
|
* @param {object|null} episodeSnapshot Episode buffer captured before flushing
|
|
31
|
+
* @param {string|null} [scopeSessionId=null] CC UUID for session_handoffs.session_id column
|
|
24
32
|
*/
|
|
25
|
-
export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapshot) {
|
|
33
|
+
export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapshot, scopeSessionId = null) {
|
|
26
34
|
// 1. Working objective — from user prompts
|
|
27
35
|
const prompts = db.prepare(`
|
|
28
36
|
SELECT prompt_text FROM user_prompts
|
|
@@ -122,6 +130,9 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
|
|
|
122
130
|
|
|
123
131
|
// UPSERT keyed on (project, type, session_id) — parallel sessions coexist.
|
|
124
132
|
// Same session re-writing its own handoff (e.g. repeated /clear) updates in place.
|
|
133
|
+
// `scopeSessionId` (CC UUID) tags the row for parallel scoping; falls back to
|
|
134
|
+
// the mem-internal `sessionId` when the caller didn't supply one (tests + legacy).
|
|
135
|
+
const storedSessionId = scopeSessionId || sessionId;
|
|
125
136
|
db.prepare(`
|
|
126
137
|
INSERT INTO session_handoffs (project, type, session_id, working_on, completed, unfinished, key_files, key_decisions, match_keywords, created_at_epoch, git_sha_at_handoff)
|
|
127
138
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -135,7 +146,7 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
|
|
|
135
146
|
created_at_epoch = excluded.created_at_epoch,
|
|
136
147
|
git_sha_at_handoff = excluded.git_sha_at_handoff
|
|
137
148
|
`).run(
|
|
138
|
-
project, type,
|
|
149
|
+
project, type, storedSessionId,
|
|
139
150
|
truncate(workingOn, 1000),
|
|
140
151
|
completed.map(c => `[${c.type}] ${c.title}`).join('\n'),
|
|
141
152
|
unfinished.length > 3000 ? unfinished.slice(0, 2999) + '…' : unfinished,
|
package/hook-llm.mjs
CHANGED
|
@@ -559,10 +559,17 @@ search_aliases: 2-6 alternative search terms someone might use to find this memo
|
|
|
559
559
|
return;
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
562
|
+
// v2.33.1: expanded low-signal filter. Historical data showed Haiku
|
|
563
|
+
// returns 'none'/''/'n/a'/'null'/'-'/'todo'/'tbd' ~95% of the time —
|
|
564
|
+
// all noise with no retrieval value. Also reject lessons <12 chars
|
|
565
|
+
// (e.g. "ok", "works", "fixed it") — too short to teach a future session.
|
|
566
|
+
// When filtered, downgrade importance to 0 so rule-based fallback in
|
|
567
|
+
// hook.mjs:saveObservation writes the obs but hook queries (which all
|
|
568
|
+
// require importance >= 1) ignore it.
|
|
569
|
+
const rawLesson = typeof parsed.lesson_learned === 'string' ? parsed.lesson_learned.trim() : '';
|
|
570
|
+
const lowSignalLesson = new Set(['none', '', 'n/a', 'null', 'todo', 'tbd', 'na', '-', 'nothing', 'nil']);
|
|
571
|
+
const isLessonLowSignal = lowSignalLesson.has(rawLesson.toLowerCase()) || rawLesson.length < 12;
|
|
572
|
+
const lessonLearned = isLessonLowSignal ? null : rawLesson.slice(0, 500);
|
|
566
573
|
const searchAliases = Array.isArray(parsed.search_aliases)
|
|
567
574
|
? parsed.search_aliases.slice(0, 6).join(' ')
|
|
568
575
|
: null;
|
|
@@ -576,7 +583,12 @@ search_aliases: 2-6 alternative search terms someone might use to find this memo
|
|
|
576
583
|
facts: Array.isArray(parsed.facts) ? parsed.facts.slice(0, 10) : [],
|
|
577
584
|
files: episode.files,
|
|
578
585
|
filesRead: episode.filesRead || [],
|
|
579
|
-
|
|
586
|
+
// v2.33.1: when lesson is low-signal, don't trust Haiku's importance
|
|
587
|
+
// inflation for noise-prone types. rule-based floor still applies so
|
|
588
|
+
// error-in-test (→3) / config-change (→2) keep their floor.
|
|
589
|
+
importance: isLessonLowSignal && (parsed.type === 'change' || parsed.type === 'discovery')
|
|
590
|
+
? Math.min(ruleImportance, 1)
|
|
591
|
+
: Math.max(ruleImportance, clampImportance(parsed.importance)),
|
|
580
592
|
lessonLearned,
|
|
581
593
|
searchAliases,
|
|
582
594
|
};
|
package/hook-memory.mjs
CHANGED
|
File without changes
|
package/hook-optimize.mjs
CHANGED
|
File without changes
|
package/hook-semaphore.mjs
CHANGED
|
File without changes
|
package/hook-shared.mjs
CHANGED
|
File without changes
|
package/hook-update.mjs
CHANGED
|
File without changes
|
package/hook.mjs
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
// Selective encoding, episodic batching, error-triggered recall
|
|
4
4
|
// Hooks (fast <100ms): post-tool-use, session-start, stop
|
|
5
5
|
// Background workers (slow): llm-episode, llm-summary
|
|
6
|
+
//
|
|
7
|
+
// ─── Session-id invariant (do not violate — see bf121aa / v2.33.2) ──────────
|
|
8
|
+
// Two session identifiers coexist in this codebase:
|
|
9
|
+
// • mem-internal id: `hook-<project>-<hash>`, produced by getSessionId().
|
|
10
|
+
// handleUserPrompt writes it into user_prompts / sdk_sessions.content_session_id
|
|
11
|
+
// / observations.memory_session_id. Treat as the ONLY valid WHERE / JOIN key
|
|
12
|
+
// for those three tables.
|
|
13
|
+
// • CC UUID: `hookData.session_id` from stdin. Use ONLY for
|
|
14
|
+
// session_handoffs.session_id (parallel-session scoping, per bf121aa).
|
|
15
|
+
// Mixing them silently breaks everything — UPDATE matches 0 rows, SELECT returns
|
|
16
|
+
// empty, buildAndSaveHandoff early-returns, no throw. Precedent: v2.33.1 shipped
|
|
17
|
+
// with the two mixed since 2026-04-12; 48 stale 'active' sessions + 0 handoffs
|
|
18
|
+
// for projects--mem went unnoticed for 4 days. Keep the split or document why
|
|
19
|
+
// you're changing it.
|
|
6
20
|
|
|
7
21
|
import { randomUUID } from 'crypto';
|
|
8
22
|
import { join } from 'path';
|
|
@@ -135,21 +149,36 @@ function flushEpisode(episode) {
|
|
|
135
149
|
if (isSignificant) {
|
|
136
150
|
spawnBackground('llm-episode', flushFile);
|
|
137
151
|
|
|
138
|
-
//
|
|
139
|
-
// and
|
|
152
|
+
// v2.33.1: structured flush receipt so Claude sees what mem just captured
|
|
153
|
+
// and the legacy error→fix nudge consolidates here. PostToolUse JSON with
|
|
154
|
+
// hookSpecificOutput.additionalContext reliably renders across CC variants;
|
|
155
|
+
// the old plain-text stdout write was invisible on some variants.
|
|
140
156
|
try {
|
|
141
157
|
const entries = episode.entries || [];
|
|
142
158
|
const hasError = entries.some(e => e.isError);
|
|
143
159
|
const hasEdit = entries.some(e => EDIT_TOOLS.has(e.tool));
|
|
160
|
+
const toolCounts = {};
|
|
161
|
+
for (const e of entries) toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1;
|
|
162
|
+
const toolSummary = Object.entries(toolCounts)
|
|
163
|
+
.sort((a, b) => b[1] - a[1])
|
|
164
|
+
.slice(0, 3)
|
|
165
|
+
.map(([t, n]) => `${t}×${n}`)
|
|
166
|
+
.join(', ');
|
|
167
|
+
const lines = [`[mem] episode flushed: ${entries.length} entries (${toolSummary})`];
|
|
144
168
|
if (hasError && hasEdit && entries.length >= 3) {
|
|
145
169
|
const editFiles = entries.filter(e => EDIT_TOOLS.has(e.tool)).flatMap(e => e.files || []);
|
|
146
170
|
const uniqueFiles = [...new Set(editFiles)].slice(0, 3);
|
|
147
|
-
const filesHint = uniqueFiles.length > 0 ? ` (
|
|
148
|
-
|
|
149
|
-
`[mem] 💡 Error→fix pattern detected${filesHint}. Consider: mem_save(type="bugfix", lesson_learned="root cause & fix")\n`,
|
|
150
|
-
);
|
|
171
|
+
const filesHint = uniqueFiles.length > 0 ? ` (${uniqueFiles.join(', ')})` : '';
|
|
172
|
+
lines.push(`[mem] 💡 error→fix pattern${filesHint} — consider: mem_save(type="bugfix", lesson_learned="<root cause + fix>")`);
|
|
151
173
|
}
|
|
152
|
-
|
|
174
|
+
process.stdout.write(JSON.stringify({
|
|
175
|
+
suppressOutput: true,
|
|
176
|
+
hookSpecificOutput: {
|
|
177
|
+
hookEventName: 'PostToolUse',
|
|
178
|
+
additionalContext: lines.join('\n'),
|
|
179
|
+
},
|
|
180
|
+
}));
|
|
181
|
+
} catch { /* never block on receipt */ }
|
|
153
182
|
} else {
|
|
154
183
|
try { unlinkSync(flushFile); } catch {}
|
|
155
184
|
}
|
|
@@ -311,9 +340,12 @@ async function handleStop() {
|
|
|
311
340
|
}
|
|
312
341
|
} catch { /* stdin unavailable — fall back to local session id */ }
|
|
313
342
|
|
|
314
|
-
// Capture session info BEFORE cleanup.
|
|
315
|
-
//
|
|
316
|
-
|
|
343
|
+
// Capture session info BEFORE cleanup. All DB lookups use the mem-internal id
|
|
344
|
+
// (that's what handleUserPrompt wrote into user_prompts / sdk_sessions / observations
|
|
345
|
+
// via getSessionId()). `ccSessionId` is used only to tag session_handoffs rows
|
|
346
|
+
// for parallel-session scoping — it must not be used as a query key, otherwise
|
|
347
|
+
// queries miss and UPDATE sdk_sessions becomes a no-op (v2.33.2 regression fix).
|
|
348
|
+
const sessionId = getSessionId();
|
|
317
349
|
const project = inferProject();
|
|
318
350
|
|
|
319
351
|
// Snapshot episode BEFORE flush for handoff extraction
|
|
@@ -366,8 +398,11 @@ async function handleStop() {
|
|
|
366
398
|
UPDATE sdk_sessions SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
|
367
399
|
WHERE content_session_id = ? AND status = 'active'
|
|
368
400
|
`).run(new Date().toISOString(), Date.now(), sessionId);
|
|
369
|
-
// Save handoff snapshot for cross-session continuity
|
|
370
|
-
|
|
401
|
+
// Save handoff snapshot for cross-session continuity.
|
|
402
|
+
// sessionId = mem-internal (query key); ccSessionId = CC UUID (scope key for
|
|
403
|
+
// parallel-safe row identity). Without the split, CC UUID-based queries miss
|
|
404
|
+
// user_prompts and the handoff row is silently skipped (see hook-handoff.mjs).
|
|
405
|
+
try { buildAndSaveHandoff(db, sessionId, project, 'exit', episodeSnapshot, ccSessionId || sessionId); }
|
|
371
406
|
catch (e) { debugCatch(e, 'handleStop-handoff'); }
|
|
372
407
|
|
|
373
408
|
// Fast summary baseline — ensures summary exists even if background LLM fails
|
|
@@ -649,12 +684,12 @@ async function handleSessionStart() {
|
|
|
649
684
|
|
|
650
685
|
if (prevSessionId) {
|
|
651
686
|
// Save handoff for cross-session continuity (/clear or /compact).
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
const
|
|
657
|
-
try { buildAndSaveHandoff(db,
|
|
687
|
+
// prevSessionId is the mem-internal id — use it to look up the finished session's
|
|
688
|
+
// user_prompts / observations. ccSessionId (same CC session across /clear) scopes
|
|
689
|
+
// the stored row so UserPromptSubmit can read its own handoff back.
|
|
690
|
+
// Legacy/test paths (no stdin) fall back to prevSessionId for both.
|
|
691
|
+
const handoffScopeId = ccSessionId || prevSessionId;
|
|
692
|
+
try { buildAndSaveHandoff(db, prevSessionId, prevProject || project, 'clear', episodeSnapshot, handoffScopeId); }
|
|
658
693
|
catch (e) { debugCatch(e, 'session-start-handoff'); }
|
|
659
694
|
|
|
660
695
|
// Read the just-saved handoff for downstream consumers (fast summary remaining, working state).
|
|
@@ -662,7 +697,7 @@ async function handleSessionStart() {
|
|
|
662
697
|
try {
|
|
663
698
|
prevClearHandoff = db.prepare(
|
|
664
699
|
'SELECT working_on, unfinished, key_files FROM session_handoffs WHERE project = ? AND type = ? AND session_id = ?'
|
|
665
|
-
).get(prevProject || project, 'clear',
|
|
700
|
+
).get(prevProject || project, 'clear', handoffScopeId);
|
|
666
701
|
} catch {}
|
|
667
702
|
|
|
668
703
|
// Generate session summary for previous session (background Haiku — richer version)
|
package/hooks/hooks.json
CHANGED
|
File without changes
|
package/install-metadata.mjs
CHANGED
|
File without changes
|
package/install.mjs
CHANGED
|
File without changes
|
package/lib/activity.mjs
CHANGED
|
File without changes
|
package/lib/doctor-benchmark.mjs
CHANGED
|
File without changes
|
package/lib/git-state.mjs
CHANGED
|
File without changes
|
package/lib/plan-reader.mjs
CHANGED
|
File without changes
|
|
File without changes
|
package/lib/task-reader.mjs
CHANGED
|
File without changes
|
package/mem-cli.mjs
CHANGED
|
File without changes
|
package/memdir.mjs
CHANGED
|
File without changes
|
package/nlp.mjs
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/plugin-cache-guard.mjs
CHANGED
|
File without changes
|
package/project-utils.mjs
CHANGED
|
File without changes
|
|
File without changes
|
package/registry-enricher.mjs
CHANGED
|
File without changes
|
package/registry-github.mjs
CHANGED
|
File without changes
|
package/registry-importer.mjs
CHANGED
|
File without changes
|
package/registry-indexer.mjs
CHANGED
|
File without changes
|
package/registry-retriever.mjs
CHANGED
|
File without changes
|
package/registry-scanner.mjs
CHANGED
|
File without changes
|
package/registry.mjs
CHANGED
|
File without changes
|
package/resource-discovery.mjs
CHANGED
|
File without changes
|
package/schema.mjs
CHANGED
|
File without changes
|
package/scoring-sql.mjs
CHANGED
|
File without changes
|
package/scripts/launch.mjs
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Lightweight standalone (~30ms): only imports better-sqlite3, fs, path, os
|
|
4
4
|
// Safety: readonly DB, exit 0 always, 3s timeout
|
|
5
5
|
|
|
6
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'fs';
|
|
7
7
|
import { basename, join } from 'path';
|
|
8
8
|
import { homedir } from 'os';
|
|
9
9
|
|
|
@@ -11,9 +11,20 @@ import { homedir } from 'os';
|
|
|
11
11
|
// point the hook at an isolated DB + cooldown dir without touching the user's real state.
|
|
12
12
|
const DB_PATH = process.env.CLAUDE_MEM_DB_PATH || join(homedir(), '.claude-mem-lite', 'claude-mem-lite.db');
|
|
13
13
|
const RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || join(homedir(), '.claude-mem-lite', 'runtime');
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// v2.33.1: cooldown path is session-scoped so same-file-twice within one
|
|
15
|
+
// session never re-injects (was: global file, 5-min window). Cross-session:
|
|
16
|
+
// fresh file, fresh nudges — this is intended. No session_id → fall back to
|
|
17
|
+
// legacy global path so env-less test harnesses still behave.
|
|
18
|
+
const LEGACY_COOLDOWN_PATH = join(RUNTIME_DIR, 'pre-recall-cooldown.json');
|
|
19
|
+
const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes (used only for legacy fallback)
|
|
20
|
+
const STALE_MS = 10 * 60 * 1000; // 10 minutes cleanup threshold for legacy file
|
|
21
|
+
const SESSION_COOLDOWN_STALE_MS = 24 * 60 * 60 * 1000; // 24h — drop session cooldown files older than this
|
|
22
|
+
|
|
23
|
+
function cooldownPathFor(sessionId) {
|
|
24
|
+
if (!sessionId) return LEGACY_COOLDOWN_PATH;
|
|
25
|
+
const safe = String(sessionId).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 64);
|
|
26
|
+
return join(RUNTIME_DIR, `pre-recall-cooldown-${safe}.json`);
|
|
27
|
+
}
|
|
17
28
|
|
|
18
29
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
19
30
|
|
|
@@ -27,20 +38,40 @@ function inferProject() {
|
|
|
27
38
|
return project;
|
|
28
39
|
}
|
|
29
40
|
|
|
30
|
-
function readCooldown() {
|
|
31
|
-
try { return JSON.parse(readFileSync(
|
|
41
|
+
function readCooldown(cooldownPath) {
|
|
42
|
+
try { return JSON.parse(readFileSync(cooldownPath, 'utf8')); } catch { return {}; }
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
function writeCooldown(data) {
|
|
45
|
+
function writeCooldown(cooldownPath, data, isSessionScoped) {
|
|
35
46
|
try {
|
|
36
47
|
mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
37
|
-
//
|
|
48
|
+
// Legacy (no session_id): stale entries trimmed to 10m window.
|
|
49
|
+
// Session-scoped: keep all entries for the session's lifetime — same-file-twice
|
|
50
|
+
// in one session never re-injects. Old session files GC'd on next write.
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const cleaned = isSessionScoped ? data : {};
|
|
53
|
+
if (!isSessionScoped) {
|
|
54
|
+
for (const [k, v] of Object.entries(data)) {
|
|
55
|
+
if (now - v < STALE_MS) cleaned[k] = v;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
writeFileSync(cooldownPath, JSON.stringify(cleaned));
|
|
59
|
+
} catch { /* silent */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Best-effort GC for session cooldown files older than 24h.
|
|
63
|
+
// Runs at most once per hook invocation, silent on any failure.
|
|
64
|
+
function gcOldSessionCooldowns() {
|
|
65
|
+
try {
|
|
38
66
|
const now = Date.now();
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
67
|
+
for (const name of readdirSync(RUNTIME_DIR)) {
|
|
68
|
+
if (!name.startsWith('pre-recall-cooldown-') || !name.endsWith('.json')) continue;
|
|
69
|
+
try {
|
|
70
|
+
const p = join(RUNTIME_DIR, name);
|
|
71
|
+
const st = statSync(p);
|
|
72
|
+
if (now - st.mtimeMs > SESSION_COOLDOWN_STALE_MS) unlinkSync(p);
|
|
73
|
+
} catch { /* silent per-entry */ }
|
|
42
74
|
}
|
|
43
|
-
writeFileSync(COOLDOWN_PATH, JSON.stringify(cleaned));
|
|
44
75
|
} catch { /* silent */ }
|
|
45
76
|
}
|
|
46
77
|
|
|
@@ -59,19 +90,29 @@ try {
|
|
|
59
90
|
|
|
60
91
|
// Parse event
|
|
61
92
|
let filePath;
|
|
93
|
+
let sessionId;
|
|
62
94
|
try {
|
|
63
95
|
const event = JSON.parse(input);
|
|
64
96
|
filePath = event.tool_input?.file_path;
|
|
97
|
+
sessionId = event.session_id || null;
|
|
65
98
|
} catch { process.exit(0); }
|
|
66
99
|
|
|
67
100
|
if (!filePath) process.exit(0);
|
|
68
101
|
|
|
69
|
-
//
|
|
70
|
-
|
|
102
|
+
// v2.33.1: session-scoped cooldown. Within one session, same file recalls
|
|
103
|
+
// once; cross-session, each session gets fresh nudges. Legacy 5-min global
|
|
104
|
+
// cooldown only applies when no session_id is present.
|
|
105
|
+
const cooldownPath = cooldownPathFor(sessionId);
|
|
106
|
+
const isSessionScoped = Boolean(sessionId);
|
|
107
|
+
const cooldown = readCooldown(cooldownPath);
|
|
71
108
|
const now = Date.now();
|
|
72
|
-
if (
|
|
73
|
-
process.exit(0);
|
|
109
|
+
if (isSessionScoped) {
|
|
110
|
+
if (cooldown[filePath]) process.exit(0); // already recalled this file in-session
|
|
111
|
+
} else {
|
|
112
|
+
if (cooldown[filePath] && (now - cooldown[filePath]) < COOLDOWN_MS) process.exit(0);
|
|
74
113
|
}
|
|
114
|
+
// Best-effort GC of old session cooldown files (cheap, once per invocation)
|
|
115
|
+
if (isSessionScoped) gcOldSessionCooldowns();
|
|
75
116
|
|
|
76
117
|
// Open DB readonly
|
|
77
118
|
const Database = (await import('better-sqlite3')).default;
|
|
@@ -177,7 +218,7 @@ try {
|
|
|
177
218
|
}));
|
|
178
219
|
// Cooldown applies to BOTH branches so the reminder doesn't spam every Edit.
|
|
179
220
|
cooldown[filePath] = now;
|
|
180
|
-
writeCooldown(cooldown);
|
|
221
|
+
writeCooldown(cooldownPath, cooldown, isSessionScoped);
|
|
181
222
|
} catch {
|
|
182
223
|
// Silent failure — never block editing
|
|
183
224
|
} finally {
|
|
File without changes
|
|
@@ -34,6 +34,22 @@ const BM25_MIN_SCORE = Number(process.env.CLAUDE_MEM_UPS_BM25_MIN || 1e-5);
|
|
|
34
34
|
// survive `shouldSkip` but carry too few tokens to justify an FTS lookup.
|
|
35
35
|
const PROMPT_MIN_LENGTH = 15;
|
|
36
36
|
|
|
37
|
+
// v2.33.1: follow-up prompts ("前面那个", "继续 X", "再看看 Y") are short by
|
|
38
|
+
// nature but semantically depend on prior turns. Once a session has injected
|
|
39
|
+
// memory at least once, relax gates so short follow-ups still get recall.
|
|
40
|
+
// Detection: INJECTED_IDS_FILE count > 0 within DEDUP_STALE_MS window.
|
|
41
|
+
const FOLLOWUP_PROMPT_MIN_LENGTH = 8;
|
|
42
|
+
const FOLLOWUP_BM25_MIN_SCORE = Number(process.env.CLAUDE_MEM_UPS_BM25_MIN_FOLLOWUP || 5e-6);
|
|
43
|
+
|
|
44
|
+
function isFollowUpSession() {
|
|
45
|
+
try {
|
|
46
|
+
const raw = readFileSync(INJECTED_IDS_FILE, 'utf8');
|
|
47
|
+
const { ts, count = 0 } = JSON.parse(raw);
|
|
48
|
+
if (!ts || Date.now() - ts > DEDUP_STALE_MS) return false;
|
|
49
|
+
return count > 0;
|
|
50
|
+
} catch { return false; }
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
// ─── DB Query Functions ─────────────────────────────────────────────────────
|
|
38
54
|
|
|
39
55
|
function searchByFts(db, queryText, project, limit, typeFilter) {
|
|
@@ -255,7 +271,12 @@ async function main() {
|
|
|
255
271
|
// T3 (v2.31): additional raw-length gate on top of shouldSkip's CJK-weighted
|
|
256
272
|
// effective-length check. Suppresses medium-short Latin prompts ("run tests",
|
|
257
273
|
// "fix bug now") that carry too few content tokens for a meaningful FTS lookup.
|
|
258
|
-
|
|
274
|
+
// v2.33.1: follow-up prompts in an already-active session get a lower gate —
|
|
275
|
+
// short continuations ("前面那个?", "does it work?") depend on prior context.
|
|
276
|
+
const followUp = isFollowUpSession();
|
|
277
|
+
const promptMinLen = followUp ? FOLLOWUP_PROMPT_MIN_LENGTH : PROMPT_MIN_LENGTH;
|
|
278
|
+
if (promptText.trim().length < promptMinLen) return;
|
|
279
|
+
const bm25Floor = followUp ? FOLLOWUP_BM25_MIN_SCORE : BM25_MIN_SCORE;
|
|
259
280
|
|
|
260
281
|
let db;
|
|
261
282
|
try {
|
|
@@ -275,7 +296,7 @@ async function main() {
|
|
|
275
296
|
const errSig = extractErrorSignature(promptText);
|
|
276
297
|
const sigRows = errSig
|
|
277
298
|
? searchByFts(db, errSig.signature, project, 2, 'bugfix').filter(r =>
|
|
278
|
-
typeof r.relevance === 'number' && Math.abs(r.relevance) >=
|
|
299
|
+
typeof r.relevance === 'number' && Math.abs(r.relevance) >= bm25Floor
|
|
279
300
|
)
|
|
280
301
|
: [];
|
|
281
302
|
|
|
@@ -299,7 +320,7 @@ async function main() {
|
|
|
299
320
|
// no relevance and are always kept — file-scoped recall is presumed
|
|
300
321
|
// intentional and has its own relevance signal (the file name match).
|
|
301
322
|
ftsRows = ftsRows.filter(r =>
|
|
302
|
-
typeof r.relevance === 'number' && Math.abs(r.relevance) >=
|
|
323
|
+
typeof r.relevance === 'number' && Math.abs(r.relevance) >= bm25Floor
|
|
303
324
|
);
|
|
304
325
|
|
|
305
326
|
// Merge: FTS results first, then file results, deduplicated
|
package/secret-scrub.mjs
CHANGED
|
File without changes
|
package/server-internals.mjs
CHANGED
|
File without changes
|
package/server.mjs
CHANGED
|
File without changes
|
package/skill.md
CHANGED
|
File without changes
|
package/skip-tools.mjs
CHANGED
|
File without changes
|
package/source-files.mjs
CHANGED
|
File without changes
|
package/stop-words.mjs
CHANGED
|
File without changes
|
package/synonyms.mjs
CHANGED
|
File without changes
|
package/tfidf.mjs
CHANGED
|
File without changes
|
package/tier.mjs
CHANGED
|
File without changes
|
package/tool-schemas.mjs
CHANGED
|
File without changes
|
package/utils.mjs
CHANGED
|
File without changes
|