claude-mem-lite 2.80.1 → 2.82.1
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/adopt-cli.mjs +126 -9
- package/hook.mjs +23 -11
- package/lib/cite-back-hint.mjs +70 -0
- package/package.json +2 -1
- package/scripts/pre-tool-recall.js +17 -3
- package/source-files.mjs +1 -0
package/adopt-cli.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// --dry-run = print intent without writing
|
|
10
10
|
// --status = list all adopted projects + versions
|
|
11
11
|
|
|
12
|
-
import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync } from 'fs';
|
|
12
|
+
import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
|
|
13
13
|
import { homedir } from 'os';
|
|
14
14
|
import { join } from 'path';
|
|
15
15
|
import {
|
|
@@ -49,6 +49,24 @@ function listAllMemdirs() {
|
|
|
49
49
|
|
|
50
50
|
function hasFlag(args, flag) { return Array.isArray(args) && args.includes(flag); }
|
|
51
51
|
|
|
52
|
+
// ─── Per-project auto-adopt opt-out sentinel ─────────────────────────────────
|
|
53
|
+
// `<memdir>/.mem-no-auto-adopt` is the durable, project-scoped escape hatch.
|
|
54
|
+
// Survives marker deletion, sentinel removal, and plugin reinstalls — that's
|
|
55
|
+
// the point: "user said no for this project" should not be reversible by
|
|
56
|
+
// `rm ~/.claude-mem-lite/runtime/.auto-adopt-*`. Managed via
|
|
57
|
+
// `claude-mem-lite adopt --disable` / `--enable`. silentAutoAdopt checks it
|
|
58
|
+
// at entry and skips WITHOUT writing the runtime marker, so toggling
|
|
59
|
+
// `--enable` re-arms auto-adopt on the next SessionStart.
|
|
60
|
+
const DISABLE_SENTINEL_BASENAME = '.mem-no-auto-adopt';
|
|
61
|
+
|
|
62
|
+
export function disableSentinelPath(memdir) {
|
|
63
|
+
return join(memdir, DISABLE_SENTINEL_BASENAME);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isAutoAdoptDisabled(memdir) {
|
|
67
|
+
return existsSync(disableSentinelPath(memdir));
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
/**
|
|
53
71
|
* cmdAdopt — write sentinel section + plugin doc to memdir.
|
|
54
72
|
* Exit code 1 on any hard failure; skipped (--all + UserEditedError) doesn't
|
|
@@ -56,6 +74,8 @@ function hasFlag(args, flag) { return Array.isArray(args) && args.includes(flag)
|
|
|
56
74
|
*/
|
|
57
75
|
export function cmdAdopt(args = []) {
|
|
58
76
|
if (hasFlag(args, '--status')) return statusAll();
|
|
77
|
+
if (hasFlag(args, '--disable')) return cmdDisable(args);
|
|
78
|
+
if (hasFlag(args, '--enable')) return cmdEnable(args);
|
|
59
79
|
|
|
60
80
|
const all = hasFlag(args, '--all');
|
|
61
81
|
const force = hasFlag(args, '--force');
|
|
@@ -122,14 +142,18 @@ function adoptOne(memdir, { force, dryRun, all }) {
|
|
|
122
142
|
}
|
|
123
143
|
|
|
124
144
|
/**
|
|
125
|
-
* silentAutoAdopt — plugin-mode first-run auto-adopt helper (v2.33.0).
|
|
145
|
+
* silentAutoAdopt — plugin-mode first-run auto-adopt helper (v2.33.0+).
|
|
126
146
|
*
|
|
127
147
|
* Preconditions (caller must gate): CLAUDE_PLUGIN_ROOT set, MEM_NO_AUTO_ADOPT!=1,
|
|
128
|
-
*
|
|
129
|
-
*
|
|
148
|
+
* first-attempt marker absent. This helper does NOT re-check those — it only
|
|
149
|
+
* does the write + marker persistence. (v2.82.0: dropped MEM_QUIET_HOOKS gate;
|
|
150
|
+
* quiet is a stdout control, not a side-effect control.)
|
|
130
151
|
*
|
|
131
152
|
* Behavior:
|
|
132
|
-
* -
|
|
153
|
+
* - If `<memdir>/.mem-no-auto-adopt` exists: skip silently, do NOT write the
|
|
154
|
+
* runtime marker. This keeps `--enable` re-armable: deleting the disable
|
|
155
|
+
* sentinel lets the next SessionStart try again.
|
|
156
|
+
* - Else: writes plugin sentinel + detail doc to the memdir for `cwd`.
|
|
133
157
|
* - Writes a per-project first-attempt marker under `markerDir` so a later
|
|
134
158
|
* `/unadopt` is respected (no re-adopt loop).
|
|
135
159
|
* - Silent: never logs, never throws. Returns structured result.
|
|
@@ -139,6 +163,9 @@ function adoptOne(memdir, { force, dryRun, all }) {
|
|
|
139
163
|
export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
|
|
140
164
|
const memdir = memdirPath(cwd);
|
|
141
165
|
try {
|
|
166
|
+
if (isAutoAdoptDisabled(memdir)) {
|
|
167
|
+
return { ok: true, action: 'disabled', reason: 'disabled-by-sentinel' };
|
|
168
|
+
}
|
|
142
169
|
if (isAdopted(memdir, PLUGIN_SLUG)) {
|
|
143
170
|
writeMarker(markerDir, markerKey);
|
|
144
171
|
return { ok: true, action: 'already-adopted' };
|
|
@@ -173,20 +200,110 @@ export function hasAutoAdoptMarker(markerDir, markerKey) {
|
|
|
173
200
|
return existsSync(join(markerDir, `.auto-adopt-${markerKey}`));
|
|
174
201
|
}
|
|
175
202
|
|
|
203
|
+
/**
|
|
204
|
+
* cmdDisable — `claude-mem-lite adopt --disable [--all]`.
|
|
205
|
+
*
|
|
206
|
+
* Writes `<memdir>/.mem-no-auto-adopt` so SessionStart auto-adopt skips this
|
|
207
|
+
* project permanently. Idempotent: re-running on an already-disabled memdir is
|
|
208
|
+
* a no-op. Does NOT remove an existing sentinel — pair with `unadopt` if you
|
|
209
|
+
* want both. The two operations are deliberately separate:
|
|
210
|
+
* - `unadopt` = "remove the contract now"
|
|
211
|
+
* - `adopt --disable` = "and don't auto-write it back"
|
|
212
|
+
*/
|
|
213
|
+
function cmdDisable(args) {
|
|
214
|
+
const all = hasFlag(args, '--all');
|
|
215
|
+
const targets = all
|
|
216
|
+
? listAllMemdirs().map((m) => m.memdir)
|
|
217
|
+
: [memdirPath(detectCwd())];
|
|
218
|
+
|
|
219
|
+
if (targets.length === 0) {
|
|
220
|
+
log('[adopt --disable] no memdirs found');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let disabled = 0, already = 0;
|
|
225
|
+
for (const memdir of targets) {
|
|
226
|
+
if (!existsSync(memdir)) mkdirSync(memdir, { recursive: true });
|
|
227
|
+
const path = disableSentinelPath(memdir);
|
|
228
|
+
if (existsSync(path)) {
|
|
229
|
+
log(`[adopt --disable] ${memdir} → already-disabled`);
|
|
230
|
+
already++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
writeFileSync(path, JSON.stringify({ disabledAt: new Date().toISOString() }) + '\n');
|
|
234
|
+
log(`[adopt --disable] ${memdir} → disabled`);
|
|
235
|
+
disabled++;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
log('');
|
|
239
|
+
log(`[adopt --disable] ${targets.length} target(s): ${disabled} newly disabled, ${already} already disabled`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* cmdEnable — `claude-mem-lite adopt --enable [--all]`.
|
|
244
|
+
*
|
|
245
|
+
* Removes the `<memdir>/.mem-no-auto-adopt` sentinel so the next SessionStart
|
|
246
|
+
* can auto-adopt again. Idempotent. Does NOT trigger an immediate adoption —
|
|
247
|
+
* run plain `claude-mem-lite adopt` if you want that now.
|
|
248
|
+
*/
|
|
249
|
+
function cmdEnable(args) {
|
|
250
|
+
const all = hasFlag(args, '--all');
|
|
251
|
+
const targets = all
|
|
252
|
+
? listAllMemdirs().map((m) => m.memdir)
|
|
253
|
+
: [memdirPath(detectCwd())];
|
|
254
|
+
|
|
255
|
+
if (targets.length === 0) {
|
|
256
|
+
log('[adopt --enable] no memdirs found');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let enabled = 0, absent = 0;
|
|
261
|
+
for (const memdir of targets) {
|
|
262
|
+
const path = disableSentinelPath(memdir);
|
|
263
|
+
if (!existsSync(path)) {
|
|
264
|
+
log(`[adopt --enable] ${memdir} → absent`);
|
|
265
|
+
absent++;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
try { unlinkSync(path); } catch { /* best-effort */ }
|
|
269
|
+
log(`[adopt --enable] ${memdir} → enabled`);
|
|
270
|
+
enabled++;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
log('');
|
|
274
|
+
log(`[adopt --enable] ${targets.length} target(s): ${enabled} re-enabled, ${absent} not-disabled`);
|
|
275
|
+
}
|
|
276
|
+
|
|
176
277
|
function statusAll() {
|
|
177
278
|
const dirs = listAllMemdirs();
|
|
178
279
|
log('[adopt --status] scanning ~/.claude/projects/*/memory/');
|
|
179
280
|
if (dirs.length === 0) { log(' (no memdirs found)'); return; }
|
|
180
|
-
let adopted = 0;
|
|
281
|
+
let adopted = 0, disabled = 0;
|
|
181
282
|
for (const { projectSlug, memdir } of dirs) {
|
|
182
|
-
|
|
283
|
+
const isAdoptedHere = isAdopted(memdir, PLUGIN_SLUG);
|
|
284
|
+
const isDisabledHere = isAutoAdoptDisabled(memdir);
|
|
285
|
+
if (isAdoptedHere) {
|
|
183
286
|
const idx = readMemoryIndex(memdir, PLUGIN_SLUG);
|
|
184
|
-
|
|
287
|
+
const suffix = isDisabledHere ? ' [auto-adopt disabled]' : '';
|
|
288
|
+
log(` ✓ ${projectSlug} (${idx.version})${suffix}`);
|
|
185
289
|
adopted++;
|
|
290
|
+
if (isDisabledHere) disabled++;
|
|
291
|
+
} else if (isDisabledHere) {
|
|
292
|
+
log(` ✗ ${projectSlug} (auto-adopt disabled, no sentinel)`);
|
|
293
|
+
disabled++;
|
|
186
294
|
}
|
|
187
295
|
}
|
|
188
296
|
log('');
|
|
189
|
-
log(`[adopt --status] ${adopted}/${dirs.length} adopted`);
|
|
297
|
+
log(`[adopt --status] ${adopted}/${dirs.length} adopted${disabled > 0 ? `, ${disabled} disabled` : ''}`);
|
|
298
|
+
|
|
299
|
+
// Gating snapshot — helps debug "why didn't auto-adopt fire?"
|
|
300
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ? 'set' : 'unset';
|
|
301
|
+
const noAutoAdopt = process.env.MEM_NO_AUTO_ADOPT === '1' ? '1 (opt-out)' : 'unset';
|
|
302
|
+
log('');
|
|
303
|
+
log('Auto-adopt gates (next SessionStart will fire only if both pass):');
|
|
304
|
+
log(` CLAUDE_PLUGIN_ROOT = ${pluginRoot} (plugin-mode install required; npx stays opt-in)`);
|
|
305
|
+
log(` MEM_NO_AUTO_ADOPT = ${noAutoAdopt} (global escape hatch)`);
|
|
306
|
+
log('Per-project opt-out: `claude-mem-lite adopt --disable` (run --enable to re-arm).');
|
|
190
307
|
}
|
|
191
308
|
|
|
192
309
|
/**
|
package/hook.mjs
CHANGED
|
@@ -61,6 +61,7 @@ import { checkForUpdate } from './hook-update.mjs';
|
|
|
61
61
|
import { handleLLMOptimize } from './hook-optimize.mjs';
|
|
62
62
|
import { silentAutoAdopt, hasAutoAdoptMarker } from './adopt-cli.mjs';
|
|
63
63
|
import { emitV270UpgradeBanner } from './lib/upgrade-banner.mjs';
|
|
64
|
+
import { loadCiteBackForEpisode } from './lib/cite-back-hint.mjs';
|
|
64
65
|
// plugin-cache-guard.mjs loaded dynamically — pre-2.31.2 installs that auto-upgraded
|
|
65
66
|
// from an older hook-update.mjs SOURCE_FILES (which did not list this module) would
|
|
66
67
|
// crash on static import. Degrade gracefully to no-op when the module is absent.
|
|
@@ -196,6 +197,12 @@ function flushEpisode(episode, hookEventName = 'PostToolUse') {
|
|
|
196
197
|
const filesHint = uniqueFiles.length > 0 ? ` (${uniqueFiles.join(', ')})` : '';
|
|
197
198
|
lines.push(`[mem] 💡 error→fix pattern${filesHint} — consider: mem_save(type="bugfix", lesson_learned="<root cause + fix>")`);
|
|
198
199
|
}
|
|
200
|
+
// v2.81: cite-back hint — fires when this episode edits a file that
|
|
201
|
+
// PreToolUse:Read/Edit nudged earlier in the same session. Precision
|
|
202
|
+
// signal (we know the file was warned about); orthogonal to the
|
|
203
|
+
// error→fix pattern above and may co-fire.
|
|
204
|
+
const citeBack = loadCiteBackForEpisode(episode, RUNTIME_DIR);
|
|
205
|
+
if (citeBack) lines.push(citeBack);
|
|
199
206
|
process.stdout.write(JSON.stringify({
|
|
200
207
|
suppressOutput: true,
|
|
201
208
|
hookSpecificOutput: {
|
|
@@ -645,22 +652,27 @@ async function handleSessionStart() {
|
|
|
645
652
|
}
|
|
646
653
|
} catch (e) { debugCatch(e, 'session-start-cache-heal'); }
|
|
647
654
|
|
|
648
|
-
// v2.33.0
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
//
|
|
655
|
+
// First-run auto-adopt (v2.33.0 plugin-mode → v2.82.1 install-mode-agnostic).
|
|
656
|
+
// ANY install path — `/plugin install`, `npm install -g`, `npx`, manual — is
|
|
657
|
+
// consent to integration. Writing the MEMORY.md sentinel once per project on
|
|
658
|
+
// first SessionStart avoids the opt-in friction that left ~zero users on
|
|
659
|
+
// auto-adopt (runtime-marker directory was empty machine-wide despite v2.33
|
|
660
|
+
// shipping ~5 weeks earlier — `install.mjs`-written hooks don't propagate
|
|
661
|
+
// ${CLAUDE_PLUGIN_ROOT}, so the v2.33.0 gate was a no-op for npm/manual
|
|
662
|
+
// installs, which is most of them). Scope is now:
|
|
663
|
+
// - gated by !MEM_NO_AUTO_ADOPT (explicit global escape hatch)
|
|
664
|
+
// - per-project opt-out via `<memdir>/.mem-no-auto-adopt` sentinel
|
|
665
|
+
// (managed by `claude-mem-lite adopt --disable / --enable`); checked
|
|
666
|
+
// inside silentAutoAdopt so the helper is safe to call directly too.
|
|
654
667
|
// - first-attempt marker persists in RUNTIME_DIR so a subsequent /unadopt
|
|
655
668
|
// is respected (no re-adopt loop).
|
|
669
|
+
// Note v2.82.0: removed MEM_QUIET_HOOKS gate. That env var suppresses stdout
|
|
670
|
+
// noise; it must NOT also disable side-effect work (PostToolUse writes the
|
|
671
|
+
// DB unconditionally — auto-adopt should follow the same rule).
|
|
656
672
|
// Failures (user-edited sentinel, budget exceeded, FS errors) are swallowed;
|
|
657
673
|
// the marker is still written so we don't retry on every SessionStart.
|
|
658
674
|
try {
|
|
659
|
-
if (
|
|
660
|
-
process.env.CLAUDE_PLUGIN_ROOT
|
|
661
|
-
&& process.env.MEM_NO_AUTO_ADOPT !== '1'
|
|
662
|
-
&& process.env.MEM_QUIET_HOOKS !== '1'
|
|
663
|
-
) {
|
|
675
|
+
if (process.env.MEM_NO_AUTO_ADOPT !== '1') {
|
|
664
676
|
const project = inferProject();
|
|
665
677
|
if (!hasAutoAdoptMarker(RUNTIME_DIR, project)) {
|
|
666
678
|
const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// claude-mem-lite: PostToolUse cite-back hint builder.
|
|
2
|
+
//
|
|
3
|
+
// Fires when a flushed episode edits a file that PreToolUse:Read/Edit had
|
|
4
|
+
// nudged earlier in the same session — the canonical "you fixed something we
|
|
5
|
+
// warned about, save the lesson?" moment.
|
|
6
|
+
//
|
|
7
|
+
// Pure function: takes an episode + the session-scoped pre-recall cooldown
|
|
8
|
+
// object and returns a hint string (or null). Cooldown I/O lives elsewhere so
|
|
9
|
+
// this stays unit-testable without disk fixtures.
|
|
10
|
+
//
|
|
11
|
+
// Cooldown schema (post-v2.81): { "<path>": { ts: <number>, lessonIds: [#NN, ...] } }
|
|
12
|
+
// Legacy schema (pre-v2.81): { "<path>": <number> } — tolerated, never emits.
|
|
13
|
+
|
|
14
|
+
import { basename, join } from 'path';
|
|
15
|
+
import { readFileSync } from 'fs';
|
|
16
|
+
import { EDIT_TOOLS } from '../utils.mjs';
|
|
17
|
+
|
|
18
|
+
const MAX_FILES = 2;
|
|
19
|
+
|
|
20
|
+
export function buildCiteBackHint(episode, cooldown) {
|
|
21
|
+
if (!episode || !cooldown) return null;
|
|
22
|
+
const entries = episode.entries;
|
|
23
|
+
if (!Array.isArray(entries) || entries.length === 0) return null;
|
|
24
|
+
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const matches = [];
|
|
27
|
+
for (const e of entries) {
|
|
28
|
+
if (!EDIT_TOOLS.has(e.tool)) continue;
|
|
29
|
+
for (const file of e.files || []) {
|
|
30
|
+
if (seen.has(file)) continue;
|
|
31
|
+
const entry = cooldown[file];
|
|
32
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
33
|
+
const ids = Array.isArray(entry.lessonIds) ? entry.lessonIds : null;
|
|
34
|
+
if (!ids || ids.length === 0) continue;
|
|
35
|
+
seen.add(file);
|
|
36
|
+
matches.push({ file, ids });
|
|
37
|
+
if (matches.length >= MAX_FILES) break;
|
|
38
|
+
}
|
|
39
|
+
if (matches.length >= MAX_FILES) break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (matches.length === 0) return null;
|
|
43
|
+
|
|
44
|
+
const lines = ['[mem] 💡 Cite-back: you edited file(s) that received PreToolUse lessons this session.'];
|
|
45
|
+
for (const m of matches) {
|
|
46
|
+
const fname = basename(m.file);
|
|
47
|
+
const idList = m.ids.map(id => `#${id}`).join(', ');
|
|
48
|
+
lines.push(` • ${fname} ← ${idList} — if you fixed it: /lesson --file ${fname} "<root cause + fix>"`);
|
|
49
|
+
}
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Path scheme MUST mirror scripts/pre-tool-recall.js cooldownPathFor() — drift
|
|
54
|
+
// silently zeros cite-back across the release. Pinned by tests/cite-back-hint.test.mjs
|
|
55
|
+
// 'sanitizes the sessionId the same way pre-tool-recall.js does'.
|
|
56
|
+
function cooldownPathFor(sessionId, runtimeDir) {
|
|
57
|
+
const safe = String(sessionId).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 64);
|
|
58
|
+
return join(runtimeDir, `pre-recall-cooldown-${safe}.json`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function loadCiteBackForEpisode(episode, runtimeDir) {
|
|
62
|
+
if (!episode || !episode.sessionId || !runtimeDir) return null;
|
|
63
|
+
let cooldown;
|
|
64
|
+
try {
|
|
65
|
+
cooldown = JSON.parse(readFileSync(cooldownPathFor(episode.sessionId, runtimeDir), 'utf8'));
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return buildCiteBackHint(episode, cooldown);
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.82.1",
|
|
4
4
|
"description": "Lightweight persistent memory system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"lib/low-signal-patterns.mjs",
|
|
58
58
|
"lib/private-strip.mjs",
|
|
59
59
|
"lib/citation-tracker.mjs",
|
|
60
|
+
"lib/cite-back-hint.mjs",
|
|
60
61
|
"lib/summary-extractor.mjs",
|
|
61
62
|
"lib/id-routing.mjs",
|
|
62
63
|
"lib/err-sampler.mjs",
|
|
@@ -49,6 +49,15 @@ function readCooldown(cooldownPath) {
|
|
|
49
49
|
try { return JSON.parse(readFileSync(cooldownPath, 'utf8')); } catch { return {}; }
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// v2.81: cooldown entries are {ts, lessonIds} objects so the PostToolUse
|
|
53
|
+
// cite-back hint can name the lessons that were nudged. Legacy entries
|
|
54
|
+
// (pre-v2.81) are bare numbers — entryTimestamp() reads both shapes.
|
|
55
|
+
function entryTimestamp(v) {
|
|
56
|
+
if (typeof v === 'number') return v;
|
|
57
|
+
if (v && typeof v === 'object' && typeof v.ts === 'number') return v.ts;
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
function writeCooldown(cooldownPath, data, isSessionScoped) {
|
|
53
62
|
try {
|
|
54
63
|
mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
@@ -59,7 +68,8 @@ function writeCooldown(cooldownPath, data, isSessionScoped) {
|
|
|
59
68
|
const cleaned = isSessionScoped ? data : {};
|
|
60
69
|
if (!isSessionScoped) {
|
|
61
70
|
for (const [k, v] of Object.entries(data)) {
|
|
62
|
-
|
|
71
|
+
const ts = entryTimestamp(v);
|
|
72
|
+
if (ts && now - ts < STALE_MS) cleaned[k] = v;
|
|
63
73
|
}
|
|
64
74
|
}
|
|
65
75
|
writeFileSync(cooldownPath, JSON.stringify(cleaned));
|
|
@@ -124,7 +134,8 @@ try {
|
|
|
124
134
|
if (isSessionScoped) {
|
|
125
135
|
if (cooldown[filePath]) process.exit(0); // already recalled this file in-session
|
|
126
136
|
} else {
|
|
127
|
-
|
|
137
|
+
const ts = entryTimestamp(cooldown[filePath]);
|
|
138
|
+
if (ts && (now - ts) < COOLDOWN_MS) process.exit(0);
|
|
128
139
|
}
|
|
129
140
|
|
|
130
141
|
// Open DB readonly
|
|
@@ -275,7 +286,10 @@ try {
|
|
|
275
286
|
// Cooldown applies on ALL branches (including silent-Read) so subsequent
|
|
276
287
|
// calls on the same file in the same session don't re-query — preserving
|
|
277
288
|
// the per-filePath invariant that underpins Read→Edit dedup.
|
|
278
|
-
|
|
289
|
+
// v2.81: record the emitted lesson IDs so flushEpisode (hook.mjs) can
|
|
290
|
+
// build the PostToolUse cite-back hint when the user actually edits the
|
|
291
|
+
// file. Empty array on no-lesson branches keeps the schema uniform.
|
|
292
|
+
cooldown[filePath] = { ts: now, lessonIds: allRows.map(r => r.id) };
|
|
279
293
|
writeCooldown(cooldownPath, cooldown, isSessionScoped);
|
|
280
294
|
} catch (e) {
|
|
281
295
|
// Silent failure — never block editing, but record for self-observation.
|
package/source-files.mjs
CHANGED