claude-recall 0.21.0 → 0.21.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/skills/auto-preferences/SKILL.md +5 -5
- package/.claude/skills/auto-preferences/manifest.json +7 -7
- package/.claude/skills/memory-management/SKILL.md +4 -2
- package/README.md +17 -0
- package/dist/cli/claude-recall-cli.js +15 -1
- package/dist/cli/commands/hook-commands.js +11 -1
- package/dist/hooks/llm-classifier.js +81 -0
- package/dist/hooks/session-end-checkpoint-worker.js +122 -0
- package/dist/hooks/session-end-checkpoint.js +64 -0
- package/dist/mcp/tools/memory-tools.js +62 -5
- package/dist/pi/extension.js +36 -0
- package/dist/shared/event-processors.js +99 -0
- package/package.json +1 -1
|
@@ -8,14 +8,14 @@ source: claude-recall
|
|
|
8
8
|
|
|
9
9
|
# Preferences
|
|
10
10
|
|
|
11
|
-
Auto-generated from 5 memories. Last updated: 2026-04-
|
|
11
|
+
Auto-generated from 5 memories. Last updated: 2026-04-11.
|
|
12
12
|
|
|
13
13
|
## Rules
|
|
14
14
|
|
|
15
|
-
- Session test preference
|
|
16
|
-
- Test preference
|
|
17
|
-
- Test preference
|
|
18
|
-
- Test preference
|
|
15
|
+
- Session test preference 1775900146096
|
|
16
|
+
- Test preference 1775900146036-2
|
|
17
|
+
- Test preference 1775900146036-1
|
|
18
|
+
- Test preference 1775900146036-0
|
|
19
19
|
- Test memory content
|
|
20
20
|
|
|
21
21
|
---
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"topicId": "preferences",
|
|
3
|
-
"sourceHash": "
|
|
3
|
+
"sourceHash": "32712ec321c1c8e831bfb1d227b5682434e69ab3a0934115453c984b36866477",
|
|
4
4
|
"memoryCount": 5,
|
|
5
|
-
"generatedAt": "2026-04-
|
|
5
|
+
"generatedAt": "2026-04-11T09:35:46.110Z",
|
|
6
6
|
"memoryKeys": [
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
7
|
+
"memory_1775900146097_ap6ffit4i",
|
|
8
|
+
"memory_1775900146071_8y0wnmbu6",
|
|
9
|
+
"memory_1775900146056_0tbld53h7",
|
|
10
|
+
"memory_1775900146038_czc25c8ra",
|
|
11
|
+
"memory_1775900145994_1cvxoyda8"
|
|
12
12
|
]
|
|
13
13
|
}
|
|
@@ -137,13 +137,14 @@ a SKILL.md file that Claude Code loads automatically.
|
|
|
137
137
|
|
|
138
138
|
## Automatic Capture Hooks
|
|
139
139
|
|
|
140
|
-
Claude Recall registers hooks on
|
|
140
|
+
Claude Recall registers hooks on four Claude Code events to capture memories automatically — no MCP tool call needed:
|
|
141
141
|
|
|
142
142
|
| Hook | Event | What it captures |
|
|
143
143
|
|------|-------|-----------------|
|
|
144
144
|
| `correction-detector` | UserPromptSubmit | User corrections, preferences, and project knowledge from natural language |
|
|
145
145
|
| `memory-stop` | Stop | Corrections, preferences, failures, and devops patterns from the last 6 transcript entries |
|
|
146
146
|
| `precompact-preserve` | PreCompact | Broader sweep of up to 50 transcript entries before context compression |
|
|
147
|
+
| `session-end-checkpoint` | SessionEnd | Auto-saves a `{completed, remaining, blockers}` task checkpoint when the session ends voluntarily (`clear`, `prompt_input_exit`, `logout`). Spawns a detached worker so it stays within Claude Code's 1.5s SessionEnd timeout. Pi has the equivalent via the `session_shutdown` event handler. |
|
|
147
148
|
|
|
148
149
|
**Key behaviors:**
|
|
149
150
|
- **LLM-first classification** via Claude Haiku — detects natural statements like "we use tabs here" or "tests go in \_\_tests\_\_/" that regex would miss
|
|
@@ -152,9 +153,10 @@ Claude Recall registers hooks on three Claude Code events to capture memories au
|
|
|
152
153
|
- Batch classification: Stop and PreCompact hooks send all texts in a single API call
|
|
153
154
|
- Near-duplicate detection via Jaccard similarity (55% threshold) prevents redundant storage
|
|
154
155
|
- Per-event limits: 3 (Stop), 5 (PreCompact) to prevent DB flooding
|
|
156
|
+
- Auto-checkpoint quality gate: refuses to save when the LLM detects the task was already complete — manual checkpoints stay sticky
|
|
155
157
|
- Always exits 0 — hooks never block Claude
|
|
156
158
|
|
|
157
|
-
**Setup:** Run `npx claude-recall setup --install` to register hooks in `.claude/settings.json`.
|
|
159
|
+
**Setup:** Run `npx claude-recall setup --install` to register hooks in `.claude/settings.json`. After upgrading to v0.21.2, re-run `setup --install` in each project to pick up the new SessionEnd hook (the `hooksVersion` bump to `13.0.0` signals that registration changed).
|
|
158
160
|
|
|
159
161
|
## Example Workflows
|
|
160
162
|
|
package/README.md
CHANGED
|
@@ -90,6 +90,7 @@ Once installed, Claude Recall works automatically in the background:
|
|
|
90
90
|
7. **After context compression** (Claude Code only) — rules are automatically re-injected into context so they're not lost when the window shrinks
|
|
91
91
|
8. **Sub-agent recall** (Claude Code only) — when sub-agents are spawned, active rules are injected into their context automatically. Sub-agent outcomes (completed/failed/killed) are captured as events
|
|
92
92
|
9. **Rules sync** (Claude Code only) — top 30 rules are exported as typed `.md` files to Claude Code's native memory directory
|
|
93
|
+
10. **Auto-checkpoint on session exit** — when a session ends (Pi shutdown or Claude Code's `SessionEnd` for `clear`/`prompt_input_exit`/`logout`), the most recent task is extracted via Haiku into a structured `{completed, remaining, blockers}` checkpoint and saved for the next session. Critical for Pi (which has no `--resume` flag); a useful safety net for Claude Code users who exit without resuming. Conservative quality gate refuses to save when the LLM detects the task was already complete — manual checkpoints are never clobbered with garbage
|
|
93
94
|
|
|
94
95
|
Classification uses Claude Haiku (via `ANTHROPIC_API_KEY`) with silent regex fallback. No configuration needed.
|
|
95
96
|
|
|
@@ -198,6 +199,22 @@ claude-recall checkpoint clear # Delete the checkpoint
|
|
|
198
199
|
|
|
199
200
|
Agents can also save/load checkpoints via MCP tools (`mcp__claude-recall__save_checkpoint` / `mcp__claude-recall__load_checkpoint`) or Pi tools (`recall_save_checkpoint` / `recall_load_checkpoint`).
|
|
200
201
|
|
|
202
|
+
#### Auto-checkpoint on session exit (v0.21.2+)
|
|
203
|
+
|
|
204
|
+
Manual `checkpoint save` is the explicit path. **Auto-checkpoint** is the safety net: when a session ends, the most recent task is extracted into a checkpoint automatically so the next session can resume.
|
|
205
|
+
|
|
206
|
+
- **Pi** — fires from the `session_shutdown` event handler. In-process synchronous call, runs as part of the existing session-end pipeline. **Critical for Pi: there is no `pi --resume` equivalent, so without this, restarting Pi loses all session context.**
|
|
207
|
+
- **Claude Code** — fires from the `SessionEnd` hook for voluntary exit reasons (`clear`, `prompt_input_exit`, `logout`). Spawns a detached background worker (fork+unref) so it stays well within Claude Code's tight 1.5s `SessionEnd` timeout. Skips `bypass_permissions_disabled` and `other` reasons (those are system-driven, not user intent). Useful for users who exit and start fresh instead of using `claude --resume`.
|
|
208
|
+
|
|
209
|
+
Both runtimes share the same Haiku-backed extraction (`extractCheckpointWithLLM`) and the same quality gate:
|
|
210
|
+
|
|
211
|
+
- **Quality gate**: refuses to save if the LLM returns an empty or trivially-short `remaining` field. The model is prompted to detect completion signals (assistant said "Done.", user said "thanks", no follow-up question) and return empty `remaining` when the task is finished. **An empty checkpoint is far better than a fabricated one** — manual checkpoints are never overwritten with garbage.
|
|
212
|
+
- **Notes tag**: auto-saved checkpoints include `[auto-saved on <pi|cc> session exit at <iso-timestamp>]` in the notes field, so you can tell auto from manual via `checkpoint load`.
|
|
213
|
+
- **Requires `ANTHROPIC_API_KEY`**. Without it, `extractCheckpointWithLLM` returns `null` (graceful fallback) and no auto-checkpoint is saved. Manual `checkpoint save` still works.
|
|
214
|
+
- **Disable**: remove the `SessionEnd` block from `.claude/settings.json` (Claude Code) or, for Pi, no per-project disable flag exists yet — open an issue if you need one.
|
|
215
|
+
|
|
216
|
+
The auto-checkpoint never clobbers a useful manual checkpoint because of the quality gate. If the LLM doesn't see clear unfinished work, it returns empty and the gate refuses the save. Manual checkpoints stay sticky until you explicitly save over them.
|
|
217
|
+
|
|
201
218
|
### Troubleshooting
|
|
202
219
|
|
|
203
220
|
```bash
|
|
@@ -809,7 +809,7 @@ async function main() {
|
|
|
809
809
|
// This avoids registry lookups on every hook invocation.
|
|
810
810
|
const cliScript = path.join(packageDir, 'dist', 'cli', 'claude-recall-cli.js');
|
|
811
811
|
const hookCmd = `node ${cliScript} hook run`;
|
|
812
|
-
settings.hooksVersion = '
|
|
812
|
+
settings.hooksVersion = '13.0.0'; // v13 = add SessionEnd for auto-checkpoint on session exit
|
|
813
813
|
settings.hooks = {
|
|
814
814
|
SubagentStart: [
|
|
815
815
|
{
|
|
@@ -919,6 +919,20 @@ async function main() {
|
|
|
919
919
|
}
|
|
920
920
|
]
|
|
921
921
|
}
|
|
922
|
+
],
|
|
923
|
+
// Auto-checkpoint on voluntary session exits. Worker is fire-and-forget,
|
|
924
|
+
// so the synchronous handler returns instantly (well within CC's tight
|
|
925
|
+
// 1.5s SessionEnd timeout). Symmetric with Pi's session_shutdown handler.
|
|
926
|
+
SessionEnd: [
|
|
927
|
+
{
|
|
928
|
+
hooks: [
|
|
929
|
+
{
|
|
930
|
+
type: "command",
|
|
931
|
+
command: `${hookCmd} session-end-checkpoint`,
|
|
932
|
+
timeout: 5
|
|
933
|
+
}
|
|
934
|
+
]
|
|
935
|
+
}
|
|
922
936
|
]
|
|
923
937
|
};
|
|
924
938
|
if (!fs.existsSync(claudeDir)) {
|
|
@@ -106,9 +106,19 @@ class HookCommands {
|
|
|
106
106
|
await handleBashFailureWatcher(input);
|
|
107
107
|
break;
|
|
108
108
|
}
|
|
109
|
+
case 'session-end-checkpoint': {
|
|
110
|
+
const { handleSessionEndCheckpoint } = await Promise.resolve().then(() => __importStar(require('../../hooks/session-end-checkpoint')));
|
|
111
|
+
await handleSessionEndCheckpoint(input);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case 'session-end-checkpoint-worker': {
|
|
115
|
+
const { handleSessionEndCheckpointWorker } = await Promise.resolve().then(() => __importStar(require('../../hooks/session-end-checkpoint-worker')));
|
|
116
|
+
await handleSessionEndCheckpointWorker(input);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
109
119
|
default:
|
|
110
120
|
console.error(`Unknown hook: ${name}`);
|
|
111
|
-
console.error('Available: correction-detector, memory-stop, precompact-preserve, memory-sync, tool-outcome-watcher');
|
|
121
|
+
console.error('Available: correction-detector, memory-stop, precompact-preserve, memory-sync, tool-outcome-watcher, session-end-checkpoint');
|
|
112
122
|
}
|
|
113
123
|
}
|
|
114
124
|
catch {
|
|
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
9
|
exports.classifyWithLLM = classifyWithLLM;
|
|
10
10
|
exports.extractHindsightHint = extractHindsightHint;
|
|
11
11
|
exports.extractSessionLearningsWithLLM = extractSessionLearningsWithLLM;
|
|
12
|
+
exports.extractCheckpointWithLLM = extractCheckpointWithLLM;
|
|
12
13
|
exports.classifyBatchWithLLM = classifyBatchWithLLM;
|
|
13
14
|
// Lazy singleton — avoid import cost when API key is absent
|
|
14
15
|
let clientInstance; // undefined = not yet checked
|
|
@@ -217,6 +218,86 @@ async function extractSessionLearningsWithLLM(summary, existingMemories) {
|
|
|
217
218
|
return null;
|
|
218
219
|
}
|
|
219
220
|
}
|
|
221
|
+
const CHECKPOINT_EXTRACTION_PROMPT = `You are extracting a "where I left off" checkpoint from a coding session that just ended. The next session — possibly minutes from now, possibly days later — needs a brief, accurate hint to resume from. Your output will overwrite any existing checkpoint, so it MUST be either accurate or empty. NEVER fabricate.
|
|
222
|
+
|
|
223
|
+
You will see the FINAL portion of a session transcript. Your job is to extract THREE fields:
|
|
224
|
+
|
|
225
|
+
- completed: what the user/agent finished in THIS recent task (concrete, brief, max 200 chars). Empty string if nothing was clearly completed.
|
|
226
|
+
- remaining: what was still in flight when the session ended — the actual hand-off (concrete, brief, max 300 chars). MUST be non-empty if there is real unfinished work. EMPTY STRING if the task is done.
|
|
227
|
+
- blockers: anything that was blocking progress (tools failing, decisions pending, dependencies). "none" if no blockers.
|
|
228
|
+
|
|
229
|
+
THE MOST IMPORTANT RULE — completion detection:
|
|
230
|
+
If the transcript ends with ANY signal that the task is finished, return remaining="". Completion signals include:
|
|
231
|
+
- assistant says "Done.", "All set.", "All done.", "Finished.", "That's it.", "Complete.", or similar terminal acknowledgement
|
|
232
|
+
- last user message is thanks/acknowledgement ("thanks", "perfect", "great") with no follow-up question
|
|
233
|
+
- tool calls succeeded and there is no explicit next step in the user's most recent prompt
|
|
234
|
+
- the conversation has reached a natural stopping point
|
|
235
|
+
|
|
236
|
+
When in doubt, prefer remaining="". An empty checkpoint is far better than a fabricated one.
|
|
237
|
+
|
|
238
|
+
THE SECOND MOST IMPORTANT RULE — no fabrication:
|
|
239
|
+
- ONLY use information present in the transcript. Do NOT extrapolate, do NOT invent follow-up work.
|
|
240
|
+
- If the most recent task is clearly complete, do NOT manufacture next steps from your imagination.
|
|
241
|
+
- If you cannot determine what was happening with high confidence, return all-empty: {"completed":"","remaining":"","blockers":""}
|
|
242
|
+
- "remaining" must be a SPECIFIC unfinished item visible in the transcript. Never generic ("continue work", "more testing", "documentation").
|
|
243
|
+
|
|
244
|
+
Other rules:
|
|
245
|
+
- Focus ONLY on the most recent coherent task. Ignore earlier work in the session.
|
|
246
|
+
- Return JSON: {"completed":"...","remaining":"...","blockers":"..."}
|
|
247
|
+
- Be terse and specific. Each field should help the future session pick up the thread.
|
|
248
|
+
- Do NOT include markdown fences. Respond with raw JSON only.
|
|
249
|
+
|
|
250
|
+
Examples of GOOD output:
|
|
251
|
+
|
|
252
|
+
Scenario A — task finished, agent said "Done.":
|
|
253
|
+
{"completed":"Copied cc-source-code into a dedicated dir under claude-recall/cc-source-code/","remaining":"","blockers":"none"}
|
|
254
|
+
|
|
255
|
+
Scenario B — task in progress, midway through implementation:
|
|
256
|
+
{"completed":"Added saveCheckpoint() to storage and MemoryService","remaining":"Wire CLI checkpoint command and add MCP/Pi tool wrappers","blockers":"none"}
|
|
257
|
+
|
|
258
|
+
Scenario C — task in progress, blocked on something:
|
|
259
|
+
{"completed":"Diagnosed [object Object] rendering bug in handleLoadRules","remaining":"Write failing test, extract formatRuleValue helper, replace 5 call sites","blockers":"none"}
|
|
260
|
+
|
|
261
|
+
Scenario D — uncertain, sparse context, can't tell what's happening:
|
|
262
|
+
{"completed":"","remaining":"","blockers":""}
|
|
263
|
+
|
|
264
|
+
Scenario E — just a question, no work done:
|
|
265
|
+
{"completed":"","remaining":"","blockers":""}
|
|
266
|
+
|
|
267
|
+
Examples of BAD output (DO NOT DO THIS):
|
|
268
|
+
{"completed":"various changes","remaining":"more work","blockers":"none"} # too vague
|
|
269
|
+
{"completed":"explored architecture","remaining":"document findings","blockers":"none"} # FABRICATED — there was no documentation task
|
|
270
|
+
{"completed":"finished everything","remaining":"finish everything","blockers":"none"} # nonsense filler`;
|
|
271
|
+
async function extractCheckpointWithLLM(conversationSummary) {
|
|
272
|
+
const client = getClient();
|
|
273
|
+
if (!client)
|
|
274
|
+
return null;
|
|
275
|
+
if (!conversationSummary || conversationSummary.trim().length < 30) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const response = await client.messages.create({
|
|
280
|
+
model: MODEL,
|
|
281
|
+
max_tokens: 600,
|
|
282
|
+
system: CHECKPOINT_EXTRACTION_PROMPT,
|
|
283
|
+
messages: [{ role: 'user', content: conversationSummary }],
|
|
284
|
+
});
|
|
285
|
+
const content = response.content?.[0];
|
|
286
|
+
if (content?.type !== 'text')
|
|
287
|
+
return null;
|
|
288
|
+
const result = parseJSON(content.text);
|
|
289
|
+
if (typeof result !== 'object' || result === null)
|
|
290
|
+
return null;
|
|
291
|
+
return {
|
|
292
|
+
completed: typeof result.completed === 'string' ? result.completed : '',
|
|
293
|
+
remaining: typeof result.remaining === 'string' ? result.remaining : '',
|
|
294
|
+
blockers: typeof result.blockers === 'string' ? result.blockers : '',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
220
301
|
async function classifyBatchWithLLM(texts) {
|
|
221
302
|
if (texts.length === 0)
|
|
222
303
|
return [];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* session-end-checkpoint-worker — detached background worker spawned by the
|
|
4
|
+
* session-end-checkpoint hook handler.
|
|
5
|
+
*
|
|
6
|
+
* Runs OUTSIDE Claude Code's 1.5s SessionEnd hook timeout. Reads the transcript,
|
|
7
|
+
* extracts a most-recent-task checkpoint via Haiku, and saves it via
|
|
8
|
+
* MemoryService.saveCheckpoint(). The next CC session that calls load_rules
|
|
9
|
+
* will see the checkpoint hint.
|
|
10
|
+
*
|
|
11
|
+
* This worker is the symmetric counterpart to Pi's in-process auto-checkpoint
|
|
12
|
+
* (src/pi/extension.ts session_shutdown handler). Both call the same shared
|
|
13
|
+
* extractCheckpoint() function from event-processors.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.handleSessionEndCheckpointWorker = handleSessionEndCheckpointWorker;
|
|
17
|
+
const shared_1 = require("./shared");
|
|
18
|
+
const event_processors_1 = require("../shared/event-processors");
|
|
19
|
+
const config_1 = require("../services/config");
|
|
20
|
+
const TRANSCRIPT_TAIL_SIZE = 30;
|
|
21
|
+
async function handleSessionEndCheckpointWorker(input) {
|
|
22
|
+
// Wire event-processor logs through hookLog so extractCheckpoint diagnostics
|
|
23
|
+
// (LLM null, quality gate filter, save failure) end up in
|
|
24
|
+
// ~/.claude-recall/hook-logs/session-end-checkpoint-worker.log instead of
|
|
25
|
+
// being silently dropped by the default no-op logFn.
|
|
26
|
+
(0, event_processors_1.setLogFunction)((source, msg) => (0, shared_1.hookLog)('session-end-checkpoint-worker', `[${source}] ${msg}`));
|
|
27
|
+
const transcriptPath = input?.transcript_path ?? '';
|
|
28
|
+
if (!transcriptPath) {
|
|
29
|
+
(0, shared_1.hookLog)('session-end-checkpoint-worker', 'No transcript_path provided');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const cwd = input?.cwd;
|
|
33
|
+
if (cwd) {
|
|
34
|
+
try {
|
|
35
|
+
config_1.ConfigService.getInstance().updateConfig({ project: { rootDir: cwd } });
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Non-critical — getProjectId will fall back to process.cwd() basename
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const projectId = config_1.ConfigService.getInstance().getProjectId();
|
|
42
|
+
if (!projectId) {
|
|
43
|
+
(0, shared_1.hookLog)('session-end-checkpoint-worker', 'No project_id resolved — aborting');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const entries = (0, shared_1.readTranscriptTail)(transcriptPath, TRANSCRIPT_TAIL_SIZE);
|
|
47
|
+
if (entries.length === 0) {
|
|
48
|
+
(0, shared_1.hookLog)('session-end-checkpoint-worker', 'No transcript entries found');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Convert raw transcript JSONL entries to the ConversationEntry shape that
|
|
52
|
+
// extractCheckpoint expects. We include user prompts, assistant text blocks,
|
|
53
|
+
// AND tool interactions — Haiku needs the assistant's reasoning (especially
|
|
54
|
+
// completion signals like "Done.") to know whether the most recent task is
|
|
55
|
+
// finished or still in flight. Without assistant text the model hallucinates
|
|
56
|
+
// follow-up work from sparse tool-call context.
|
|
57
|
+
const converted = [];
|
|
58
|
+
for (let i = 0; i < entries.length; i++) {
|
|
59
|
+
const entry = entries[i];
|
|
60
|
+
const role = entry?.message?.role ?? entry?.role;
|
|
61
|
+
if (role === 'user') {
|
|
62
|
+
// Skip user entries that are pure tool_result wrappers — those will be
|
|
63
|
+
// captured via extractToolInteractions below
|
|
64
|
+
const content = entry?.message?.content ?? entry?.content;
|
|
65
|
+
const isPureToolResult = Array.isArray(content) && content.every((b) => b?.type === 'tool_result');
|
|
66
|
+
if (isPureToolResult)
|
|
67
|
+
continue;
|
|
68
|
+
const text = (0, shared_1.extractTextFromEntry)(entry);
|
|
69
|
+
if (text && text.trim().length > 0) {
|
|
70
|
+
converted.push({
|
|
71
|
+
entry: { role: 'user', text: text.trim() },
|
|
72
|
+
index: i,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Add assistant text blocks with their original index — these include
|
|
78
|
+
// completion signals ("Done.", "All set", etc.) that help Haiku judge
|
|
79
|
+
// whether the most recent task is actually finished.
|
|
80
|
+
const assistantTexts = (0, shared_1.extractAssistantTexts)(entries);
|
|
81
|
+
for (const at of assistantTexts) {
|
|
82
|
+
if (!at.text || !at.text.trim())
|
|
83
|
+
continue;
|
|
84
|
+
converted.push({
|
|
85
|
+
entry: { role: 'assistant', text: at.text.trim().substring(0, 400) },
|
|
86
|
+
index: at.entryIndex,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// Add tool interactions as tool_result entries with their original index
|
|
90
|
+
const interactions = (0, shared_1.extractToolInteractions)(entries);
|
|
91
|
+
for (const interaction of interactions) {
|
|
92
|
+
if (!interaction.result)
|
|
93
|
+
continue;
|
|
94
|
+
const text = interaction.result.content.substring(0, 300);
|
|
95
|
+
if (!text)
|
|
96
|
+
continue;
|
|
97
|
+
converted.push({
|
|
98
|
+
entry: {
|
|
99
|
+
role: 'tool_result',
|
|
100
|
+
text,
|
|
101
|
+
toolName: interaction.call.name,
|
|
102
|
+
isError: interaction.result.isError,
|
|
103
|
+
},
|
|
104
|
+
index: interaction.result.entryIndex,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Sort by original transcript index to preserve chronological order
|
|
108
|
+
converted.sort((a, b) => a.index - b.index);
|
|
109
|
+
const conversationEntries = converted.map(c => c.entry);
|
|
110
|
+
if (conversationEntries.length < 3) {
|
|
111
|
+
(0, shared_1.hookLog)('session-end-checkpoint-worker', `Too few entries (${conversationEntries.length}) — skipping`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
(0, shared_1.hookLog)('session-end-checkpoint-worker', `Extracting checkpoint from ${conversationEntries.length} entries (project=${projectId})`);
|
|
115
|
+
const saved = await (0, event_processors_1.extractCheckpoint)(conversationEntries, projectId, 'cc');
|
|
116
|
+
if (saved) {
|
|
117
|
+
(0, shared_1.hookLog)('session-end-checkpoint-worker', `Auto-checkpoint saved for ${projectId}`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
(0, shared_1.hookLog)('session-end-checkpoint-worker', `No checkpoint saved (LLM null, quality gate, or save failure)`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* session-end-checkpoint hook — fires on Claude Code SessionEnd event.
|
|
4
|
+
*
|
|
5
|
+
* Goal: auto-save a "where I left off" checkpoint when the session ends, so
|
|
6
|
+
* the next session can pick up from the most recent task. Less critical for
|
|
7
|
+
* Claude Code (which has `claude --resume`) than for Pi, but still useful
|
|
8
|
+
* for users who exit and start fresh instead of resuming.
|
|
9
|
+
*
|
|
10
|
+
* Architecture: this handler is the SYNCHRONOUS gate that Claude Code's hook
|
|
11
|
+
* runner waits on. SessionEnd hooks have a tight default timeout (1.5s — see
|
|
12
|
+
* cc-source-code/utils/hooks.ts:175 SESSION_END_HOOK_TIMEOUT_MS_DEFAULT) which
|
|
13
|
+
* is too short for transcript-read + Haiku call + DB write.
|
|
14
|
+
*
|
|
15
|
+
* Solution: this handler spawns a DETACHED worker process that does the real
|
|
16
|
+
* work in the background, then returns instantly. The worker survives the
|
|
17
|
+
* parent's exit and writes the checkpoint asynchronously. Worst case race:
|
|
18
|
+
* if the user starts a new session before the worker finishes, the new
|
|
19
|
+
* session sees no checkpoint — graceful degradation, not data loss.
|
|
20
|
+
*
|
|
21
|
+
* Reason filter: only fires for voluntary user exits (clear, prompt_input_exit,
|
|
22
|
+
* logout, resume). Skips system-driven exits (bypass_permissions_disabled, other)
|
|
23
|
+
* because those don't represent user intent to pause.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.handleSessionEndCheckpoint = handleSessionEndCheckpoint;
|
|
27
|
+
const child_process_1 = require("child_process");
|
|
28
|
+
const shared_1 = require("./shared");
|
|
29
|
+
const SKIP_REASONS = new Set(['bypass_permissions_disabled', 'other']);
|
|
30
|
+
async function handleSessionEndCheckpoint(input) {
|
|
31
|
+
const reason = input?.reason ?? 'unknown';
|
|
32
|
+
if (SKIP_REASONS.has(reason)) {
|
|
33
|
+
(0, shared_1.hookLog)('session-end-checkpoint', `Skipping (reason=${reason})`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!input?.transcript_path) {
|
|
37
|
+
(0, shared_1.hookLog)('session-end-checkpoint', 'No transcript_path in input — nothing to extract');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
// process.argv[1] is the absolute path to claude-recall-cli.js (whichever
|
|
42
|
+
// entry the parent hook ran from). Reuse the same binary so the worker
|
|
43
|
+
// picks up the same dist version, MemoryService singleton path, env, etc.
|
|
44
|
+
const cliPath = process.argv[1];
|
|
45
|
+
const child = (0, child_process_1.spawn)(process.execPath, // node binary path
|
|
46
|
+
[cliPath, 'hook', 'run', 'session-end-checkpoint-worker'], {
|
|
47
|
+
detached: true,
|
|
48
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
49
|
+
// Inherit cwd so getProjectId() resolves correctly in the worker
|
|
50
|
+
});
|
|
51
|
+
if (child.stdin) {
|
|
52
|
+
child.stdin.write(JSON.stringify(input));
|
|
53
|
+
child.stdin.end();
|
|
54
|
+
}
|
|
55
|
+
// Detach so the worker outlives the parent. The hook handler returns
|
|
56
|
+
// immediately, well within Claude Code's 1.5s SessionEnd timeout.
|
|
57
|
+
child.unref();
|
|
58
|
+
(0, shared_1.hookLog)('session-end-checkpoint', `Spawned detached worker (pid=${child.pid}, reason=${reason})`);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
(0, shared_1.hookLog)('session-end-checkpoint', `Failed to spawn worker: ${err?.message ?? err}`);
|
|
62
|
+
// Best-effort — never block the hook
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1,10 +1,67 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MemoryTools = void 0;
|
|
4
|
+
exports.formatRuleValue = formatRuleValue;
|
|
4
5
|
const config_1 = require("../../services/config");
|
|
5
6
|
const search_monitor_1 = require("../../services/search-monitor");
|
|
6
7
|
const skill_generator_1 = require("../../services/skill-generator");
|
|
7
8
|
const outcome_storage_1 = require("../../services/outcome-storage");
|
|
9
|
+
/**
|
|
10
|
+
* Render any memory.value shape as a readable string for load_rules output.
|
|
11
|
+
*
|
|
12
|
+
* Memory values land in the DB in several historical shapes. The previous
|
|
13
|
+
* rendering used `m.value.content || m.value.value || JSON.stringify(m.value)`
|
|
14
|
+
* which short-circuited on truthy non-string objects, producing "[object Object]"
|
|
15
|
+
* when string interpolation eventually called toString() on the returned object.
|
|
16
|
+
*
|
|
17
|
+
* Rules:
|
|
18
|
+
* 1. strings/numbers pass through (or coerce)
|
|
19
|
+
* 2. null/undefined → empty string
|
|
20
|
+
* 3. objects: prefer the first STRING field in order: content, value, title, description
|
|
21
|
+
* (only string — non-string `content` falls through to title)
|
|
22
|
+
* 4. nested failure shapes: extract `what_failed` (top-level or under `content`)
|
|
23
|
+
* 5. last resort: truncated JSON.stringify (never raw object)
|
|
24
|
+
*
|
|
25
|
+
* Exported for direct unit testing in tests/unit/format-rule-value.test.ts.
|
|
26
|
+
*/
|
|
27
|
+
function formatRuleValue(value) {
|
|
28
|
+
if (value == null)
|
|
29
|
+
return '';
|
|
30
|
+
if (typeof value === 'string')
|
|
31
|
+
return value;
|
|
32
|
+
if (typeof value !== 'object')
|
|
33
|
+
return String(value);
|
|
34
|
+
const v = value;
|
|
35
|
+
// Prefer the first non-empty string field. Order matters:
|
|
36
|
+
// - `content` covers legacy hook failures and promoted lessons (lesson text)
|
|
37
|
+
// - `value` covers preference shape
|
|
38
|
+
// - `title` covers tool-outcome-watcher failures whose `content` is a nested object
|
|
39
|
+
// - `description` is a last-ditch human label
|
|
40
|
+
for (const field of ['content', 'value', 'title', 'description']) {
|
|
41
|
+
const candidate = v[field];
|
|
42
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
43
|
+
return candidate;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Nested failure object — extract what_failed if present
|
|
47
|
+
if (typeof v.what_failed === 'string' && v.what_failed.trim()) {
|
|
48
|
+
return v.what_failed;
|
|
49
|
+
}
|
|
50
|
+
if (v.content && typeof v.content === 'object') {
|
|
51
|
+
const inner = v.content;
|
|
52
|
+
if (typeof inner.what_failed === 'string' && inner.what_failed.trim()) {
|
|
53
|
+
return inner.what_failed;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Last resort: truncated JSON. Never return a raw object.
|
|
57
|
+
try {
|
|
58
|
+
const json = JSON.stringify(value);
|
|
59
|
+
return json.length > 200 ? json.substring(0, 200) + '…' : json;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return String(value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
8
65
|
class MemoryTools {
|
|
9
66
|
constructor(memoryService, logger, onMemoryChanged) {
|
|
10
67
|
this.memoryService = memoryService;
|
|
@@ -287,7 +344,7 @@ class MemoryTools {
|
|
|
287
344
|
const sections = [];
|
|
288
345
|
if (rules.preferences.length > 0) {
|
|
289
346
|
sections.push('## Preferences\n' + rules.preferences.map(m => {
|
|
290
|
-
const val =
|
|
347
|
+
const val = formatRuleValue(m.value);
|
|
291
348
|
// Only show key prefix if it's a meaningful name (not auto-generated)
|
|
292
349
|
const key = m.preference_key || m.key || '';
|
|
293
350
|
const isAutoKey = key.startsWith('memory_') || key.startsWith('auto_') || key.startsWith('pref_');
|
|
@@ -296,7 +353,7 @@ class MemoryTools {
|
|
|
296
353
|
}
|
|
297
354
|
if (rules.corrections.length > 0) {
|
|
298
355
|
sections.push('## Corrections\n' + rules.corrections.map(m => {
|
|
299
|
-
const val =
|
|
356
|
+
const val = formatRuleValue(m.value);
|
|
300
357
|
const isPromoted = m.key.startsWith('promoted_') || m.value?.source === 'promotion-engine';
|
|
301
358
|
const evidence = isPromoted && m.value?.evidence_count ? ` (learned from ${m.value.evidence_count} observations)` : '';
|
|
302
359
|
return isPromoted ? `- [promoted lesson] ${val}${evidence}` : `- ${val}`;
|
|
@@ -308,21 +365,21 @@ class MemoryTools {
|
|
|
308
365
|
const regularFailures = rules.failures.filter(m => !m.key.startsWith('promoted_') && m.value?.source !== 'promotion-engine');
|
|
309
366
|
if (promotedLessons.length > 0) {
|
|
310
367
|
sections.push('## Promoted Lessons (learned from repeated outcomes)\n' + promotedLessons.map(m => {
|
|
311
|
-
const val =
|
|
368
|
+
const val = formatRuleValue(m.value);
|
|
312
369
|
const evidence = m.value?.evidence_count ? ` (seen ${m.value.evidence_count}x)` : '';
|
|
313
370
|
return `- ${val}${evidence}`;
|
|
314
371
|
}).join('\n'));
|
|
315
372
|
}
|
|
316
373
|
if (regularFailures.length > 0) {
|
|
317
374
|
sections.push('## Failures\n' + regularFailures.map(m => {
|
|
318
|
-
const val =
|
|
375
|
+
const val = formatRuleValue(m.value);
|
|
319
376
|
return `- ${val}`;
|
|
320
377
|
}).join('\n'));
|
|
321
378
|
}
|
|
322
379
|
}
|
|
323
380
|
if (rules.devops.length > 0) {
|
|
324
381
|
sections.push('## DevOps Rules\n' + rules.devops.map(m => {
|
|
325
|
-
const val =
|
|
382
|
+
const val = formatRuleValue(m.value);
|
|
326
383
|
return `- ${val}`;
|
|
327
384
|
}).join('\n'));
|
|
328
385
|
}
|
package/dist/pi/extension.js
CHANGED
|
@@ -67,6 +67,12 @@ function default_1(pi) {
|
|
|
67
67
|
const collectedToolResults = [];
|
|
68
68
|
let rulesLoaded = false;
|
|
69
69
|
const collectedUserTexts = [];
|
|
70
|
+
// Chronologically interleaved entries (user input + tool results in order)
|
|
71
|
+
// for auto-checkpoint extraction at session end. Distinct from the two
|
|
72
|
+
// arrays above which are kept for backward compat with processSessionEnd
|
|
73
|
+
// and extractSessionLearnings.
|
|
74
|
+
const collectedEntries = [];
|
|
75
|
+
const MAX_COLLECTED_ENTRIES = 100;
|
|
70
76
|
// Route logs through Pi's UI when available
|
|
71
77
|
(0, event_processors_1.setLogFunction)((source, msg) => {
|
|
72
78
|
try {
|
|
@@ -80,6 +86,7 @@ function default_1(pi) {
|
|
|
80
86
|
rulesLoaded = false;
|
|
81
87
|
collectedUserTexts.length = 0;
|
|
82
88
|
collectedToolResults.length = 0;
|
|
89
|
+
collectedEntries.length = 0;
|
|
83
90
|
(0, event_processors_1.resetPendingFailures)();
|
|
84
91
|
try {
|
|
85
92
|
config_1.ConfigService.getInstance().updateConfig({
|
|
@@ -121,6 +128,16 @@ function default_1(pi) {
|
|
|
121
128
|
toolName: event.toolName,
|
|
122
129
|
isError: event.isError,
|
|
123
130
|
});
|
|
131
|
+
// Append to chronologically interleaved log for auto-checkpoint
|
|
132
|
+
collectedEntries.push({
|
|
133
|
+
role: 'tool_result',
|
|
134
|
+
text: output.substring(0, 300),
|
|
135
|
+
toolName: event.toolName,
|
|
136
|
+
isError: event.isError,
|
|
137
|
+
});
|
|
138
|
+
if (collectedEntries.length > MAX_COLLECTED_ENTRIES) {
|
|
139
|
+
collectedEntries.splice(0, collectedEntries.length - MAX_COLLECTED_ENTRIES);
|
|
140
|
+
}
|
|
124
141
|
if (ctx.hasUI) {
|
|
125
142
|
const label = event.input?.command
|
|
126
143
|
? truncateStr(event.input.command, 40)
|
|
@@ -139,6 +156,10 @@ function default_1(pi) {
|
|
|
139
156
|
// --- Event: detect corrections from user input ---
|
|
140
157
|
pi.on('input', (event, ctx) => {
|
|
141
158
|
collectedUserTexts.push(event.text);
|
|
159
|
+
collectedEntries.push({ role: 'user', text: event.text });
|
|
160
|
+
if (collectedEntries.length > MAX_COLLECTED_ENTRIES) {
|
|
161
|
+
collectedEntries.splice(0, collectedEntries.length - MAX_COLLECTED_ENTRIES);
|
|
162
|
+
}
|
|
142
163
|
(0, event_processors_1.processUserInput)(event.text, sessionId).then(msg => {
|
|
143
164
|
if (msg && ctx.hasUI) {
|
|
144
165
|
try {
|
|
@@ -179,6 +200,21 @@ function default_1(pi) {
|
|
|
179
200
|
}
|
|
180
201
|
}).catch(() => { });
|
|
181
202
|
}
|
|
203
|
+
// Auto-checkpoint: extract "where I left off" hint for next Pi session.
|
|
204
|
+
// Critical for Pi which has no `--resume` flag — without this, the next
|
|
205
|
+
// Pi session has no memory of what came before. Uses chronologically
|
|
206
|
+
// interleaved entries (collectedEntries) so the LLM sees the actual
|
|
207
|
+
// most-recent task, not user texts followed by all tool results.
|
|
208
|
+
if (collectedEntries.length >= 3 && projectId) {
|
|
209
|
+
(0, event_processors_1.extractCheckpoint)(collectedEntries, projectId, 'pi').then(saved => {
|
|
210
|
+
if (saved && ctx.hasUI) {
|
|
211
|
+
try {
|
|
212
|
+
ctx.ui.notify('📌 Recall: saved task checkpoint for next session', 'info');
|
|
213
|
+
}
|
|
214
|
+
catch { /* non-critical */ }
|
|
215
|
+
}
|
|
216
|
+
}).catch(() => { });
|
|
217
|
+
}
|
|
182
218
|
});
|
|
183
219
|
// --- Event: pre-compaction — aggressive capture ---
|
|
184
220
|
pi.on('session_before_compact', (event, _ctx) => {
|
|
@@ -48,6 +48,8 @@ exports.processSessionEnd = processSessionEnd;
|
|
|
48
48
|
exports.processPreCompact = processPreCompact;
|
|
49
49
|
exports.buildSummary = buildSummary;
|
|
50
50
|
exports.extractSessionLearnings = extractSessionLearnings;
|
|
51
|
+
exports.buildRecentTaskSummary = buildRecentTaskSummary;
|
|
52
|
+
exports.extractCheckpoint = extractCheckpoint;
|
|
51
53
|
const shared_1 = require("../hooks/shared");
|
|
52
54
|
const llm_classifier_1 = require("../hooks/llm-classifier");
|
|
53
55
|
const memory_1 = require("../services/memory");
|
|
@@ -493,3 +495,100 @@ async function extractSessionLearnings(entries, sessionId, projectId, maxStore =
|
|
|
493
495
|
return 0;
|
|
494
496
|
}
|
|
495
497
|
}
|
|
498
|
+
// --- Auto-Checkpoint Extraction ---
|
|
499
|
+
const CHECKPOINT_SUMMARY_MAX_CHARS = 4000;
|
|
500
|
+
const CHECKPOINT_MIN_ENTRIES = 3;
|
|
501
|
+
const CHECKPOINT_MIN_REMAINING_LEN = 10;
|
|
502
|
+
/**
|
|
503
|
+
* Build a summary of the MOST RECENT entries (not the start) for checkpoint extraction.
|
|
504
|
+
* Walks backward from the end, accumulates lines until char budget is exhausted,
|
|
505
|
+
* then returns them in chronological order.
|
|
506
|
+
*
|
|
507
|
+
* Distinct from buildSummary() which prefers the START of the session.
|
|
508
|
+
*/
|
|
509
|
+
function buildRecentTaskSummary(entries) {
|
|
510
|
+
const lines = [];
|
|
511
|
+
let totalChars = 0;
|
|
512
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
513
|
+
const entry = entries[i];
|
|
514
|
+
let line;
|
|
515
|
+
if (entry.role === 'tool_result') {
|
|
516
|
+
const status = entry.isError ? ' [ERROR]' : '';
|
|
517
|
+
const tool = entry.toolName ? `${entry.toolName}` : 'tool';
|
|
518
|
+
line = `[${tool}${status}] ${truncate(entry.text, 200)}`;
|
|
519
|
+
}
|
|
520
|
+
else if (entry.role === 'assistant' && entry.toolName) {
|
|
521
|
+
line = `[assistant → ${entry.toolName}] ${truncate(entry.text, 150)}`;
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
line = `[${entry.role}] ${truncate(entry.text, 200)}`;
|
|
525
|
+
}
|
|
526
|
+
if (totalChars + line.length > CHECKPOINT_SUMMARY_MAX_CHARS)
|
|
527
|
+
break;
|
|
528
|
+
lines.unshift(line);
|
|
529
|
+
totalChars += line.length + 1;
|
|
530
|
+
}
|
|
531
|
+
return lines.join('\n');
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Auto-extract a "where I left off" checkpoint from a session that is ending.
|
|
535
|
+
*
|
|
536
|
+
* Built for two callers with different runtime constraints:
|
|
537
|
+
* - Pi: in-process synchronous call from session_shutdown handler
|
|
538
|
+
* - Claude Code: detached worker process spawned from a SessionEnd hook
|
|
539
|
+
*
|
|
540
|
+
* Both runtimes pass the same ConversationEntry[] shape and get the same
|
|
541
|
+
* quality-gated behavior. Pi is the primary use case because Pi has no
|
|
542
|
+
* `--resume` equivalent — without this, restarted Pi sessions have no
|
|
543
|
+
* memory of what came before.
|
|
544
|
+
*
|
|
545
|
+
* Quality gate: skips the save if the LLM extraction has an empty/short
|
|
546
|
+
* `remaining` field. The whole point of a checkpoint is "what to resume
|
|
547
|
+
* from"; saving empty resumes would clobber any manual checkpoint with
|
|
548
|
+
* useless garbage.
|
|
549
|
+
*
|
|
550
|
+
* @param entries Recent conversation entries (last ~30 from session)
|
|
551
|
+
* @param projectId Current project ID
|
|
552
|
+
* @param runtime Tag for the notes field — distinguishes Pi vs CC auto-saves
|
|
553
|
+
* @returns true if a checkpoint was saved, false otherwise (no API key,
|
|
554
|
+
* insufficient entries, empty extraction, save failure)
|
|
555
|
+
*/
|
|
556
|
+
async function extractCheckpoint(entries, projectId, runtime) {
|
|
557
|
+
if (!entries || entries.length < CHECKPOINT_MIN_ENTRIES) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
const summary = buildRecentTaskSummary(entries);
|
|
562
|
+
if (!summary || summary.trim().length === 0)
|
|
563
|
+
return false;
|
|
564
|
+
const extraction = await (0, llm_classifier_1.extractCheckpointWithLLM)(summary);
|
|
565
|
+
if (extraction === null) {
|
|
566
|
+
logFn('event-processor', 'extractCheckpoint: LLM returned null (no API key, parse error, or empty extraction)');
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
// Quality gate: must have meaningful `remaining` content to be worth saving
|
|
570
|
+
const remaining = (extraction.remaining || '').trim();
|
|
571
|
+
if (remaining.length < CHECKPOINT_MIN_REMAINING_LEN) {
|
|
572
|
+
logFn('event-processor', `extractCheckpoint: skipping save — remaining field empty or too short (${remaining.length} chars)`);
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
const completed = (extraction.completed || '').trim();
|
|
576
|
+
const blockers = (extraction.blockers || '').trim();
|
|
577
|
+
const timestamp = new Date().toISOString();
|
|
578
|
+
const notes = `[auto-saved on ${runtime} session exit at ${timestamp}]`;
|
|
579
|
+
try {
|
|
580
|
+
const ms = memory_1.MemoryService.getInstance();
|
|
581
|
+
ms.saveCheckpoint(projectId, { completed, remaining, blockers, notes });
|
|
582
|
+
logFn('event-processor', `extractCheckpoint: saved auto-checkpoint for ${projectId} (runtime=${runtime})`);
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
catch (saveErr) {
|
|
586
|
+
logFn('event-processor', `extractCheckpoint: saveCheckpoint failed: ${(0, shared_1.safeErrorMessage)(saveErr)}`);
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
logFn('event-processor', `extractCheckpoint error: ${(0, shared_1.safeErrorMessage)(err)}`);
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
}
|
package/package.json
CHANGED