@tekmidian/pai 0.5.4 → 0.5.6
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/dist/hooks/cleanup-session-files.mjs.map +1 -1
- package/dist/hooks/context-compression-hook.mjs +240 -132
- package/dist/hooks/context-compression-hook.mjs.map +3 -3
- package/dist/hooks/initialize-session.mjs.map +1 -1
- package/dist/hooks/load-project-context.mjs.map +1 -1
- package/dist/hooks/stop-hook.mjs.map +1 -1
- package/dist/hooks/sync-todo-to-md.mjs.map +1 -1
- package/package.json +1 -1
- package/src/hooks/ts/lib/project-utils.ts +52 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +273 -140
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* PreCompact Hook - Triggered before context compression
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* 1. Save checkpoint to session note
|
|
7
|
-
* 2.
|
|
8
|
-
*
|
|
9
|
-
* summary retains awareness of what was being worked on.
|
|
5
|
+
* Three critical jobs:
|
|
6
|
+
* 1. Save rich checkpoint to session note — work items, state, meaningful rename
|
|
7
|
+
* 2. Update TODO.md with a proper ## Continue section for the next session
|
|
8
|
+
* 3. Save session state to temp file for post-compact injection via SessionStart(compact)
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Uses a CUMULATIVE state file (.compact-state.json) that persists across
|
|
11
|
+
* compactions. This ensures that even after multiple compactions (where the
|
|
12
|
+
* transcript becomes thin), we still have rich data for titles, summaries,
|
|
13
|
+
* and work items from earlier in the session.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
16
17
|
import { basename, dirname, join } from 'path';
|
|
17
18
|
import { tmpdir } from 'os';
|
|
18
19
|
import {
|
|
@@ -22,8 +23,8 @@ import {
|
|
|
22
23
|
appendCheckpoint,
|
|
23
24
|
addWorkToSessionNote,
|
|
24
25
|
findNotesDir,
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
renameSessionNote,
|
|
27
|
+
updateTodoContinue,
|
|
27
28
|
calculateSessionTokens,
|
|
28
29
|
WorkItem,
|
|
29
30
|
} from '../lib/project-utils';
|
|
@@ -37,6 +38,16 @@ interface HookInput {
|
|
|
37
38
|
trigger?: string;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/** Structured data extracted from a transcript in a single pass. */
|
|
42
|
+
interface TranscriptData {
|
|
43
|
+
userMessages: string[];
|
|
44
|
+
summaries: string[];
|
|
45
|
+
captures: string[];
|
|
46
|
+
lastCompleted: string;
|
|
47
|
+
filesModified: string[];
|
|
48
|
+
workItems: WorkItem[];
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
// ---------------------------------------------------------------------------
|
|
41
52
|
// Helpers
|
|
42
53
|
// ---------------------------------------------------------------------------
|
|
@@ -80,24 +91,23 @@ function getTranscriptStats(transcriptPath: string): { messageCount: number; isL
|
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
// ---------------------------------------------------------------------------
|
|
83
|
-
//
|
|
94
|
+
// Unified transcript parser — single pass extracts everything
|
|
84
95
|
// ---------------------------------------------------------------------------
|
|
85
96
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
function parseTranscript(transcriptPath: string): TranscriptData {
|
|
98
|
+
const data: TranscriptData = {
|
|
99
|
+
userMessages: [],
|
|
100
|
+
summaries: [],
|
|
101
|
+
captures: [],
|
|
102
|
+
lastCompleted: '',
|
|
103
|
+
filesModified: [],
|
|
104
|
+
workItems: [],
|
|
105
|
+
};
|
|
106
|
+
|
|
92
107
|
try {
|
|
93
108
|
const raw = readFileSync(transcriptPath, 'utf-8');
|
|
94
109
|
const lines = raw.trim().split('\n');
|
|
95
|
-
|
|
96
|
-
const userMessages: string[] = [];
|
|
97
|
-
const summaries: string[] = [];
|
|
98
|
-
const captures: string[] = [];
|
|
99
|
-
let lastCompleted = '';
|
|
100
|
-
const filesModified = new Set<string>();
|
|
110
|
+
const seenSummaries = new Set<string>();
|
|
101
111
|
|
|
102
112
|
for (const line of lines) {
|
|
103
113
|
if (!line.trim()) continue;
|
|
@@ -107,150 +117,223 @@ function extractSessionState(transcriptPath: string, cwd?: string): string | nul
|
|
|
107
117
|
// --- User messages ---
|
|
108
118
|
if (entry.type === 'user' && entry.message?.content) {
|
|
109
119
|
const text = contentToText(entry.message.content).slice(0, 300);
|
|
110
|
-
if (text) userMessages.push(text);
|
|
120
|
+
if (text) data.userMessages.push(text);
|
|
111
121
|
}
|
|
112
122
|
|
|
113
|
-
// --- Assistant
|
|
123
|
+
// --- Assistant content ---
|
|
114
124
|
if (entry.type === 'assistant' && entry.message?.content) {
|
|
115
125
|
const text = contentToText(entry.message.content);
|
|
116
126
|
|
|
127
|
+
// Summaries → also create work items
|
|
117
128
|
const summaryMatch = text.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
|
|
118
129
|
if (summaryMatch) {
|
|
119
130
|
const s = summaryMatch[1].trim();
|
|
120
|
-
if (s.length > 5 && !summaries.includes(s))
|
|
131
|
+
if (s.length > 5 && !data.summaries.includes(s)) {
|
|
132
|
+
data.summaries.push(s);
|
|
133
|
+
if (!seenSummaries.has(s)) {
|
|
134
|
+
seenSummaries.add(s);
|
|
135
|
+
const details: string[] = [];
|
|
136
|
+
const actionsMatch = text.match(/ACTIONS:\s*(.+?)(?=\n[A-Z]+:|$)/is);
|
|
137
|
+
if (actionsMatch) {
|
|
138
|
+
const actionLines = actionsMatch[1].split('\n')
|
|
139
|
+
.map(l => l.replace(/^[-*•]\s*/, '').replace(/^\d+\.\s*/, '').trim())
|
|
140
|
+
.filter(l => l.length > 3 && l.length < 100);
|
|
141
|
+
details.push(...actionLines.slice(0, 3));
|
|
142
|
+
}
|
|
143
|
+
data.workItems.push({ title: s, details: details.length > 0 ? details : undefined, completed: true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
121
146
|
}
|
|
122
147
|
|
|
148
|
+
// Captures
|
|
123
149
|
const captureMatch = text.match(/CAPTURE:\s*(.+?)(?:\n|$)/i);
|
|
124
150
|
if (captureMatch) {
|
|
125
151
|
const c = captureMatch[1].trim();
|
|
126
|
-
if (c.length > 5 && !captures.includes(c)) captures.push(c);
|
|
152
|
+
if (c.length > 5 && !data.captures.includes(c)) data.captures.push(c);
|
|
127
153
|
}
|
|
128
154
|
|
|
155
|
+
// Completed
|
|
129
156
|
const completedMatch = text.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
|
|
130
157
|
if (completedMatch) {
|
|
131
|
-
lastCompleted = completedMatch[1].trim().replace(/\*+/g, '');
|
|
158
|
+
data.lastCompleted = completedMatch[1].trim().replace(/\*+/g, '');
|
|
159
|
+
if (data.workItems.length === 0 && !seenSummaries.has(data.lastCompleted) && data.lastCompleted.length > 5) {
|
|
160
|
+
seenSummaries.add(data.lastCompleted);
|
|
161
|
+
data.workItems.push({ title: data.lastCompleted, completed: true });
|
|
162
|
+
}
|
|
132
163
|
}
|
|
133
|
-
}
|
|
134
164
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
// File modifications (from tool_use blocks)
|
|
166
|
+
if (Array.isArray(entry.message.content)) {
|
|
167
|
+
for (const block of entry.message.content) {
|
|
168
|
+
if (block.type === 'tool_use') {
|
|
169
|
+
const tool = block.name;
|
|
170
|
+
if ((tool === 'Edit' || tool === 'Write') && block.input?.file_path) {
|
|
171
|
+
if (!data.filesModified.includes(block.input.file_path)) {
|
|
172
|
+
data.filesModified.push(block.input.file_path);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
142
175
|
}
|
|
143
176
|
}
|
|
144
177
|
}
|
|
145
178
|
}
|
|
146
179
|
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.error(`parseTranscript error: ${err}`);
|
|
182
|
+
}
|
|
147
183
|
|
|
148
|
-
|
|
149
|
-
|
|
184
|
+
return data;
|
|
185
|
+
}
|
|
150
186
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Format session state as human-readable string
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
154
190
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (recentUser.length > 0) {
|
|
158
|
-
parts.push('\nRecent user requests:');
|
|
159
|
-
for (const msg of recentUser) {
|
|
160
|
-
// Trim to first line or 200 chars
|
|
161
|
-
const firstLine = msg.split('\n')[0].slice(0, 200);
|
|
162
|
-
parts.push(`- ${firstLine}`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
191
|
+
function formatSessionState(data: TranscriptData, cwd?: string): string | null {
|
|
192
|
+
const parts: string[] = [];
|
|
165
193
|
|
|
166
|
-
|
|
167
|
-
const recentSummaries = summaries.slice(-3);
|
|
168
|
-
if (recentSummaries.length > 0) {
|
|
169
|
-
parts.push('\nWork summaries:');
|
|
170
|
-
for (const s of recentSummaries) {
|
|
171
|
-
parts.push(`- ${s.slice(0, 150)}`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
194
|
+
if (cwd) parts.push(`Working directory: ${cwd}`);
|
|
174
195
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
parts.push(`- ${c.slice(0, 150)}`);
|
|
181
|
-
}
|
|
196
|
+
const recentUser = data.userMessages.slice(-3);
|
|
197
|
+
if (recentUser.length > 0) {
|
|
198
|
+
parts.push('\nRecent user requests:');
|
|
199
|
+
for (const msg of recentUser) {
|
|
200
|
+
parts.push(`- ${msg.split('\n')[0].slice(0, 200)}`);
|
|
182
201
|
}
|
|
202
|
+
}
|
|
183
203
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
parts.push(`- ${f}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
204
|
+
const recentSummaries = data.summaries.slice(-3);
|
|
205
|
+
if (recentSummaries.length > 0) {
|
|
206
|
+
parts.push('\nWork summaries:');
|
|
207
|
+
for (const s of recentSummaries) parts.push(`- ${s.slice(0, 150)}`);
|
|
208
|
+
}
|
|
192
209
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
210
|
+
const recentCaptures = data.captures.slice(-5);
|
|
211
|
+
if (recentCaptures.length > 0) {
|
|
212
|
+
parts.push('\nCaptured context:');
|
|
213
|
+
for (const c of recentCaptures) parts.push(`- ${c.slice(0, 150)}`);
|
|
214
|
+
}
|
|
196
215
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
216
|
+
const files = data.filesModified.slice(-10);
|
|
217
|
+
if (files.length > 0) {
|
|
218
|
+
parts.push('\nFiles modified this session:');
|
|
219
|
+
for (const f of files) parts.push(`- ${f}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (data.lastCompleted) {
|
|
223
|
+
parts.push(`\nLast completed: ${data.lastCompleted.slice(0, 150)}`);
|
|
202
224
|
}
|
|
225
|
+
|
|
226
|
+
const result = parts.join('\n');
|
|
227
|
+
return result.length > 50 ? result : null;
|
|
203
228
|
}
|
|
204
229
|
|
|
205
230
|
// ---------------------------------------------------------------------------
|
|
206
|
-
//
|
|
231
|
+
// Derive a meaningful title for the session note
|
|
207
232
|
// ---------------------------------------------------------------------------
|
|
208
233
|
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
const raw = readFileSync(transcriptPath, 'utf-8');
|
|
212
|
-
const lines = raw.trim().split('\n');
|
|
213
|
-
const workItems: WorkItem[] = [];
|
|
214
|
-
const seenSummaries = new Set<string>();
|
|
234
|
+
function deriveTitle(data: TranscriptData): string {
|
|
235
|
+
let title = '';
|
|
215
236
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
237
|
+
// 1. Last work item title (most descriptive of what was accomplished)
|
|
238
|
+
if (data.workItems.length > 0) {
|
|
239
|
+
title = data.workItems[data.workItems.length - 1].title;
|
|
240
|
+
}
|
|
241
|
+
// 2. Last summary
|
|
242
|
+
else if (data.summaries.length > 0) {
|
|
243
|
+
title = data.summaries[data.summaries.length - 1];
|
|
244
|
+
}
|
|
245
|
+
// 3. Last completed marker
|
|
246
|
+
else if (data.lastCompleted && data.lastCompleted.length > 5) {
|
|
247
|
+
title = data.lastCompleted;
|
|
248
|
+
}
|
|
249
|
+
// 4. Last substantive user message
|
|
250
|
+
else if (data.userMessages.length > 0) {
|
|
251
|
+
for (let i = data.userMessages.length - 1; i >= 0; i--) {
|
|
252
|
+
const msg = data.userMessages[i].split('\n')[0].trim();
|
|
253
|
+
if (msg.length > 10 && msg.length < 80 &&
|
|
254
|
+
!msg.toLowerCase().startsWith('yes') &&
|
|
255
|
+
!msg.toLowerCase().startsWith('ok')) {
|
|
256
|
+
title = msg;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// 5. Derive from files modified
|
|
262
|
+
if (!title && data.filesModified.length > 0) {
|
|
263
|
+
const basenames = data.filesModified.slice(-5).map(f => {
|
|
264
|
+
const b = basename(f);
|
|
265
|
+
return b.replace(/\.[^.]+$/, '');
|
|
266
|
+
});
|
|
267
|
+
const unique = [...new Set(basenames)];
|
|
268
|
+
title = unique.length <= 3
|
|
269
|
+
? `Updated ${unique.join(', ')}`
|
|
270
|
+
: `Modified ${data.filesModified.length} files`;
|
|
271
|
+
}
|
|
220
272
|
|
|
221
|
-
|
|
222
|
-
|
|
273
|
+
// Clean up for filename use
|
|
274
|
+
return title
|
|
275
|
+
.replace(/[^\w\s-]/g, ' ') // Remove special chars
|
|
276
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
277
|
+
.trim()
|
|
278
|
+
.substring(0, 60);
|
|
279
|
+
}
|
|
223
280
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (summary && !seenSummaries.has(summary) && summary.length > 5) {
|
|
228
|
-
seenSummaries.add(summary);
|
|
229
|
-
const details: string[] = [];
|
|
230
|
-
const actionsMatch = text.match(/ACTIONS:\s*(.+?)(?=\n[A-Z]+:|$)/is);
|
|
231
|
-
if (actionsMatch) {
|
|
232
|
-
const actionLines = actionsMatch[1].split('\n')
|
|
233
|
-
.map(l => l.replace(/^[-*•]\s*/, '').replace(/^\d+\.\s*/, '').trim())
|
|
234
|
-
.filter(l => l.length > 3 && l.length < 100);
|
|
235
|
-
details.push(...actionLines.slice(0, 3));
|
|
236
|
-
}
|
|
237
|
-
workItems.push({ title: summary, details: details.length > 0 ? details : undefined, completed: true });
|
|
238
|
-
}
|
|
239
|
-
}
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Cumulative state — persists across compactions in .compact-state.json
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
240
284
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
285
|
+
const CUMULATIVE_STATE_FILE = '.compact-state.json';
|
|
286
|
+
|
|
287
|
+
function loadCumulativeState(notesDir: string): TranscriptData | null {
|
|
288
|
+
try {
|
|
289
|
+
const filePath = join(notesDir, CUMULATIVE_STATE_FILE);
|
|
290
|
+
if (!existsSync(filePath)) return null;
|
|
291
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
292
|
+
return {
|
|
293
|
+
userMessages: raw.userMessages || [],
|
|
294
|
+
summaries: raw.summaries || [],
|
|
295
|
+
captures: raw.captures || [],
|
|
296
|
+
lastCompleted: raw.lastCompleted || '',
|
|
297
|
+
filesModified: raw.filesModified || [],
|
|
298
|
+
workItems: raw.workItems || [],
|
|
299
|
+
};
|
|
252
300
|
} catch {
|
|
253
|
-
return
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function mergeTranscriptData(accumulated: TranscriptData | null, current: TranscriptData): TranscriptData {
|
|
306
|
+
if (!accumulated) return current;
|
|
307
|
+
|
|
308
|
+
const mergeArrays = (a: string[], b: string[]): string[] => {
|
|
309
|
+
const seen = new Set(a);
|
|
310
|
+
return [...a, ...b.filter(x => !seen.has(x))];
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const seenTitles = new Set(accumulated.workItems.map(w => w.title));
|
|
314
|
+
const newWorkItems = current.workItems.filter(w => !seenTitles.has(w.title));
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
userMessages: mergeArrays(accumulated.userMessages, current.userMessages).slice(-20),
|
|
318
|
+
summaries: mergeArrays(accumulated.summaries, current.summaries),
|
|
319
|
+
captures: mergeArrays(accumulated.captures, current.captures),
|
|
320
|
+
lastCompleted: current.lastCompleted || accumulated.lastCompleted,
|
|
321
|
+
filesModified: mergeArrays(accumulated.filesModified, current.filesModified),
|
|
322
|
+
workItems: [...accumulated.workItems, ...newWorkItems],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function saveCumulativeState(notesDir: string, data: TranscriptData, notePath: string | null): void {
|
|
327
|
+
try {
|
|
328
|
+
const filePath = join(notesDir, CUMULATIVE_STATE_FILE);
|
|
329
|
+
writeFileSync(filePath, JSON.stringify({
|
|
330
|
+
...data,
|
|
331
|
+
notePath,
|
|
332
|
+
lastUpdated: new Date().toISOString(),
|
|
333
|
+
}, null, 2));
|
|
334
|
+
console.error(`Cumulative state saved (${data.workItems.length} work items, ${data.filesModified.length} files)`);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
console.error(`Failed to save cumulative state: ${err}`);
|
|
254
337
|
}
|
|
255
338
|
}
|
|
256
339
|
|
|
@@ -289,23 +372,42 @@ async function main() {
|
|
|
289
372
|
: String(tokenCount);
|
|
290
373
|
|
|
291
374
|
// -----------------------------------------------------------------
|
|
292
|
-
//
|
|
375
|
+
// Single-pass transcript parsing + cumulative state merge
|
|
293
376
|
// -----------------------------------------------------------------
|
|
294
|
-
const
|
|
377
|
+
const data = parseTranscript(hookInput.transcript_path);
|
|
295
378
|
|
|
379
|
+
// Find notes directory early — needed for cumulative state
|
|
380
|
+
let notesInfo: { path: string; isLocal: boolean };
|
|
296
381
|
try {
|
|
297
|
-
|
|
298
|
-
const notesInfo = hookInput.cwd
|
|
382
|
+
notesInfo = hookInput.cwd
|
|
299
383
|
? findNotesDir(hookInput.cwd)
|
|
300
384
|
: { path: join(dirname(hookInput.transcript_path), 'Notes'), isLocal: false };
|
|
301
|
-
|
|
385
|
+
} catch {
|
|
386
|
+
notesInfo = { path: join(dirname(hookInput.transcript_path), 'Notes'), isLocal: false };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Load accumulated state from previous compactions and merge
|
|
390
|
+
const accumulated = loadCumulativeState(notesInfo.path);
|
|
391
|
+
const merged = mergeTranscriptData(accumulated, data);
|
|
392
|
+
const state = formatSessionState(merged, hookInput.cwd);
|
|
393
|
+
|
|
394
|
+
if (accumulated) {
|
|
395
|
+
console.error(`Loaded cumulative state: ${accumulated.workItems.length} work items, ${accumulated.filesModified.length} files from previous compaction(s)`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// -----------------------------------------------------------------
|
|
399
|
+
// Persist session state to numbered session note (like "pause session")
|
|
400
|
+
// -----------------------------------------------------------------
|
|
401
|
+
let notePath: string | null = null;
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
notePath = getCurrentNotePath(notesInfo.path);
|
|
302
405
|
|
|
303
406
|
// If no note found, or the latest note is completed, create a new one
|
|
304
407
|
if (!notePath) {
|
|
305
408
|
console.error('No session note found — creating one for checkpoint');
|
|
306
409
|
notePath = createSessionNote(notesInfo.path, 'Recovered Session');
|
|
307
410
|
} else {
|
|
308
|
-
// Check if the found note is already completed — don't write to completed notes
|
|
309
411
|
try {
|
|
310
412
|
const noteContent = readFileSync(notePath, 'utf-8');
|
|
311
413
|
if (noteContent.includes('**Status:** Completed') || noteContent.includes('**Completed:**')) {
|
|
@@ -321,11 +423,29 @@ async function main() {
|
|
|
321
423
|
: `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.`;
|
|
322
424
|
appendCheckpoint(notePath, checkpointBody);
|
|
323
425
|
|
|
324
|
-
// 2. Write work items to "Work Done" section (
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
426
|
+
// 2. Write work items to "Work Done" section (uses merged cumulative data)
|
|
427
|
+
if (merged.workItems.length > 0) {
|
|
428
|
+
addWorkToSessionNote(notePath, merged.workItems, `Pre-Compact (~${tokenDisplay} tokens)`);
|
|
429
|
+
console.error(`Added ${merged.workItems.length} work item(s) to session note`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 3. Rename session note with a meaningful title (uses merged data for richer titles)
|
|
433
|
+
const title = deriveTitle(merged);
|
|
434
|
+
if (title) {
|
|
435
|
+
const newPath = renameSessionNote(notePath, title);
|
|
436
|
+
if (newPath !== notePath) {
|
|
437
|
+
// Update H1 title inside the note to match
|
|
438
|
+
try {
|
|
439
|
+
let noteContent = readFileSync(newPath, 'utf-8');
|
|
440
|
+
noteContent = noteContent.replace(
|
|
441
|
+
/^(# Session \d+:)\s*.*$/m,
|
|
442
|
+
`$1 ${title}`
|
|
443
|
+
);
|
|
444
|
+
writeFileSync(newPath, noteContent);
|
|
445
|
+
console.error(`Updated note H1 to match rename`);
|
|
446
|
+
} catch { /* ignore */ }
|
|
447
|
+
notePath = newPath;
|
|
448
|
+
}
|
|
329
449
|
}
|
|
330
450
|
|
|
331
451
|
console.error(`Rich checkpoint saved: ${basename(notePath)}`);
|
|
@@ -333,13 +453,17 @@ async function main() {
|
|
|
333
453
|
console.error(`Could not save checkpoint: ${noteError}`);
|
|
334
454
|
}
|
|
335
455
|
|
|
456
|
+
// Save cumulative state for next compaction
|
|
457
|
+
saveCumulativeState(notesInfo.path, merged, notePath);
|
|
458
|
+
|
|
336
459
|
// -----------------------------------------------------------------
|
|
337
|
-
// Update TODO.md with
|
|
460
|
+
// Update TODO.md with proper ## Continue section (like "pause session")
|
|
338
461
|
// -----------------------------------------------------------------
|
|
339
|
-
if (hookInput.cwd &&
|
|
462
|
+
if (hookInput.cwd && notePath) {
|
|
340
463
|
try {
|
|
341
|
-
|
|
342
|
-
|
|
464
|
+
const noteFilename = basename(notePath);
|
|
465
|
+
updateTodoContinue(hookInput.cwd, noteFilename, state, tokenDisplay);
|
|
466
|
+
console.error('TODO.md ## Continue section updated');
|
|
343
467
|
} catch (todoError) {
|
|
344
468
|
console.error(`Could not update TODO.md: ${todoError}`);
|
|
345
469
|
}
|
|
@@ -352,13 +476,22 @@ async function main() {
|
|
|
352
476
|
// Instead, we write the injection payload to a temp file keyed by
|
|
353
477
|
// session_id. The SessionStart(compact) hook reads it and outputs
|
|
354
478
|
// to stdout, which IS injected into the post-compaction context.
|
|
479
|
+
//
|
|
480
|
+
// Always fires (even with thin state) — includes note path so the AI
|
|
481
|
+
// can enrich the session note post-compaction using its own context.
|
|
355
482
|
// -----------------------------------------------------------------------
|
|
356
|
-
if (
|
|
483
|
+
if (hookInput.session_id) {
|
|
484
|
+
const stateText = state || `Working directory: ${hookInput.cwd || 'unknown'}`;
|
|
485
|
+
const noteInfo = notePath
|
|
486
|
+
? `\nSESSION NOTE: ${notePath}\nIf this note still has a generic title (e.g. "New Session", "Context Compression"),\nrename it based on actual work done and add a rich summary.`
|
|
487
|
+
: '';
|
|
488
|
+
|
|
357
489
|
const injection = [
|
|
358
490
|
'<system-reminder>',
|
|
359
491
|
`SESSION STATE RECOVERED AFTER COMPACTION (${compactType}, ~${tokenDisplay} tokens)`,
|
|
360
492
|
'',
|
|
361
|
-
|
|
493
|
+
stateText,
|
|
494
|
+
noteInfo,
|
|
362
495
|
'',
|
|
363
496
|
'IMPORTANT: This session state was captured before context compaction.',
|
|
364
497
|
'Use it to maintain continuity. Continue the conversation from where',
|