@tekmidian/pai 0.5.1 → 0.5.3
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/ARCHITECTURE.md +84 -0
- package/README.md +94 -0
- package/dist/cli/index.mjs +72 -12
- package/dist/cli/index.mjs.map +1 -1
- package/dist/daemon/index.mjs +3 -3
- package/dist/{daemon-a1W4KgFq.mjs → daemon-D9evGlgR.mjs} +8 -8
- package/dist/{daemon-a1W4KgFq.mjs.map → daemon-D9evGlgR.mjs.map} +1 -1
- package/dist/{factory-CeXQzlwn.mjs → factory-Bzcy70G9.mjs} +3 -3
- package/dist/{factory-CeXQzlwn.mjs.map → factory-Bzcy70G9.mjs.map} +1 -1
- package/dist/hooks/context-compression-hook.mjs +333 -33
- package/dist/hooks/context-compression-hook.mjs.map +3 -3
- package/dist/hooks/post-compact-inject.mjs +51 -0
- package/dist/hooks/post-compact-inject.mjs.map +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{indexer-CKQcgKsz.mjs → indexer-CMPOiY1r.mjs} +22 -1
- package/dist/{indexer-CKQcgKsz.mjs.map → indexer-CMPOiY1r.mjs.map} +1 -1
- package/dist/{indexer-backend-DQO-FqAI.mjs → indexer-backend-CIMXedqk.mjs} +26 -9
- package/dist/indexer-backend-CIMXedqk.mjs.map +1 -0
- package/dist/{postgres-CIxeqf_n.mjs → postgres-FXrHDPcE.mjs} +36 -13
- package/dist/postgres-FXrHDPcE.mjs.map +1 -0
- package/dist/{sqlite-CymLKiDE.mjs → sqlite-WWBq7_2C.mjs} +18 -1
- package/dist/{sqlite-CymLKiDE.mjs.map → sqlite-WWBq7_2C.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +292 -70
- package/src/hooks/ts/session-start/post-compact-inject.ts +85 -0
- package/dist/indexer-backend-DQO-FqAI.mjs.map +0 -1
- package/dist/postgres-CIxeqf_n.mjs.map +0 -1
|
@@ -7,8 +7,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// src/hooks/ts/pre-compact/context-compression-hook.ts
|
|
10
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
11
|
-
import {
|
|
10
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
11
|
+
import { basename as basename2, dirname, join as join3 } from "path";
|
|
12
|
+
import { tmpdir } from "os";
|
|
12
13
|
|
|
13
14
|
// src/hooks/ts/lib/project-utils.ts
|
|
14
15
|
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
|
|
@@ -74,6 +75,33 @@ validatePAIStructure();
|
|
|
74
75
|
|
|
75
76
|
// src/hooks/ts/lib/project-utils.ts
|
|
76
77
|
var PROJECTS_DIR = join2(PAI_DIR, "projects");
|
|
78
|
+
function encodePath(path) {
|
|
79
|
+
return path.replace(/\//g, "-").replace(/\./g, "-").replace(/ /g, "-");
|
|
80
|
+
}
|
|
81
|
+
function getProjectDir(cwd) {
|
|
82
|
+
const encoded = encodePath(cwd);
|
|
83
|
+
return join2(PROJECTS_DIR, encoded);
|
|
84
|
+
}
|
|
85
|
+
function getNotesDir(cwd) {
|
|
86
|
+
return join2(getProjectDir(cwd), "Notes");
|
|
87
|
+
}
|
|
88
|
+
function findNotesDir(cwd) {
|
|
89
|
+
const cwdBasename = basename(cwd).toLowerCase();
|
|
90
|
+
if (cwdBasename === "notes" && existsSync2(cwd)) {
|
|
91
|
+
return { path: cwd, isLocal: true };
|
|
92
|
+
}
|
|
93
|
+
const localPaths = [
|
|
94
|
+
join2(cwd, "Notes"),
|
|
95
|
+
join2(cwd, "notes"),
|
|
96
|
+
join2(cwd, ".claude", "Notes")
|
|
97
|
+
];
|
|
98
|
+
for (const path of localPaths) {
|
|
99
|
+
if (existsSync2(path)) {
|
|
100
|
+
return { path, isLocal: true };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { path: getNotesDir(cwd), isLocal: false };
|
|
104
|
+
}
|
|
77
105
|
function isWhatsAppEnabled() {
|
|
78
106
|
try {
|
|
79
107
|
const { homedir: homedir2 } = __require("os");
|
|
@@ -172,6 +200,43 @@ ${checkpoint}
|
|
|
172
200
|
writeFileSync(notePath, newContent);
|
|
173
201
|
console.error(`Checkpoint added to: ${basename(notePath)}`);
|
|
174
202
|
}
|
|
203
|
+
function addWorkToSessionNote(notePath, workItems, sectionTitle) {
|
|
204
|
+
if (!existsSync2(notePath)) {
|
|
205
|
+
console.error(`Note file not found: ${notePath}`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
let content = readFileSync2(notePath, "utf-8");
|
|
209
|
+
let workText = "";
|
|
210
|
+
if (sectionTitle) {
|
|
211
|
+
workText += `
|
|
212
|
+
### ${sectionTitle}
|
|
213
|
+
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
for (const item of workItems) {
|
|
217
|
+
const checkbox = item.completed !== false ? "[x]" : "[ ]";
|
|
218
|
+
workText += `- ${checkbox} **${item.title}**
|
|
219
|
+
`;
|
|
220
|
+
if (item.details && item.details.length > 0) {
|
|
221
|
+
for (const detail of item.details) {
|
|
222
|
+
workText += ` - ${detail}
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const workDoneMatch = content.match(/## Work Done\n\n(<!-- .*? -->)?/);
|
|
228
|
+
if (workDoneMatch) {
|
|
229
|
+
const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;
|
|
230
|
+
content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);
|
|
231
|
+
} else {
|
|
232
|
+
const nextStepsIndex = content.indexOf("## Next Steps");
|
|
233
|
+
if (nextStepsIndex !== -1) {
|
|
234
|
+
content = content.substring(0, nextStepsIndex) + workText + "\n" + content.substring(nextStepsIndex);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
writeFileSync(notePath, content);
|
|
238
|
+
console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);
|
|
239
|
+
}
|
|
175
240
|
function calculateSessionTokens(jsonlPath) {
|
|
176
241
|
if (!existsSync2(jsonlPath)) {
|
|
177
242
|
return 0;
|
|
@@ -199,8 +264,81 @@ function calculateSessionTokens(jsonlPath) {
|
|
|
199
264
|
return 0;
|
|
200
265
|
}
|
|
201
266
|
}
|
|
267
|
+
function findTodoPath(cwd) {
|
|
268
|
+
const localPaths = [
|
|
269
|
+
join2(cwd, "TODO.md"),
|
|
270
|
+
join2(cwd, "notes", "TODO.md"),
|
|
271
|
+
join2(cwd, "Notes", "TODO.md"),
|
|
272
|
+
join2(cwd, ".claude", "TODO.md")
|
|
273
|
+
];
|
|
274
|
+
for (const path of localPaths) {
|
|
275
|
+
if (existsSync2(path)) {
|
|
276
|
+
return path;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return join2(getNotesDir(cwd), "TODO.md");
|
|
280
|
+
}
|
|
281
|
+
function ensureTodoMd(cwd) {
|
|
282
|
+
const todoPath = findTodoPath(cwd);
|
|
283
|
+
if (!existsSync2(todoPath)) {
|
|
284
|
+
const parentDir = join2(todoPath, "..");
|
|
285
|
+
if (!existsSync2(parentDir)) {
|
|
286
|
+
mkdirSync(parentDir, { recursive: true });
|
|
287
|
+
}
|
|
288
|
+
const content = `# TODO
|
|
289
|
+
|
|
290
|
+
## Current Session
|
|
291
|
+
|
|
292
|
+
- [ ] (Tasks will be tracked here)
|
|
293
|
+
|
|
294
|
+
## Backlog
|
|
295
|
+
|
|
296
|
+
- [ ] (Future tasks)
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
*Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}*
|
|
301
|
+
`;
|
|
302
|
+
writeFileSync(todoPath, content);
|
|
303
|
+
console.error(`Created TODO.md: ${todoPath}`);
|
|
304
|
+
}
|
|
305
|
+
return todoPath;
|
|
306
|
+
}
|
|
307
|
+
function addTodoCheckpoint(cwd, checkpoint) {
|
|
308
|
+
const todoPath = ensureTodoMd(cwd);
|
|
309
|
+
let content = readFileSync2(todoPath, "utf-8");
|
|
310
|
+
content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)+$/g, "");
|
|
311
|
+
const backlogIndex = content.indexOf("## Backlog");
|
|
312
|
+
if (backlogIndex !== -1) {
|
|
313
|
+
const checkpointText = `
|
|
314
|
+
**Checkpoint (${(/* @__PURE__ */ new Date()).toISOString()}):** ${checkpoint}
|
|
315
|
+
|
|
316
|
+
`;
|
|
317
|
+
content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);
|
|
318
|
+
}
|
|
319
|
+
content = content.trimEnd() + `
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
*Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}*
|
|
324
|
+
`;
|
|
325
|
+
writeFileSync(todoPath, content);
|
|
326
|
+
console.error(`Checkpoint added to TODO.md`);
|
|
327
|
+
}
|
|
202
328
|
|
|
203
329
|
// src/hooks/ts/pre-compact/context-compression-hook.ts
|
|
330
|
+
function contentToText(content) {
|
|
331
|
+
if (typeof content === "string") return content;
|
|
332
|
+
if (Array.isArray(content)) {
|
|
333
|
+
return content.map((c) => {
|
|
334
|
+
if (typeof c === "string") return c;
|
|
335
|
+
if (c?.text) return c.text;
|
|
336
|
+
if (c?.content) return String(c.content);
|
|
337
|
+
return "";
|
|
338
|
+
}).join(" ").trim();
|
|
339
|
+
}
|
|
340
|
+
return "";
|
|
341
|
+
}
|
|
204
342
|
function getTranscriptStats(transcriptPath) {
|
|
205
343
|
try {
|
|
206
344
|
const content = readFileSync3(transcriptPath, "utf-8");
|
|
@@ -208,32 +346,165 @@ function getTranscriptStats(transcriptPath) {
|
|
|
208
346
|
let userMessages = 0;
|
|
209
347
|
let assistantMessages = 0;
|
|
210
348
|
for (const line of lines) {
|
|
211
|
-
if (line.trim())
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
assistantMessages++;
|
|
218
|
-
}
|
|
219
|
-
} catch {
|
|
220
|
-
}
|
|
349
|
+
if (!line.trim()) continue;
|
|
350
|
+
try {
|
|
351
|
+
const entry = JSON.parse(line);
|
|
352
|
+
if (entry.type === "user") userMessages++;
|
|
353
|
+
else if (entry.type === "assistant") assistantMessages++;
|
|
354
|
+
} catch {
|
|
221
355
|
}
|
|
222
356
|
}
|
|
223
357
|
const totalMessages = userMessages + assistantMessages;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
} catch (error) {
|
|
358
|
+
return { messageCount: totalMessages, isLarge: totalMessages > 50 };
|
|
359
|
+
} catch {
|
|
227
360
|
return { messageCount: 0, isLarge: false };
|
|
228
361
|
}
|
|
229
362
|
}
|
|
363
|
+
function extractSessionState(transcriptPath, cwd) {
|
|
364
|
+
try {
|
|
365
|
+
const raw = readFileSync3(transcriptPath, "utf-8");
|
|
366
|
+
const lines = raw.trim().split("\n");
|
|
367
|
+
const userMessages = [];
|
|
368
|
+
const summaries = [];
|
|
369
|
+
const captures = [];
|
|
370
|
+
let lastCompleted = "";
|
|
371
|
+
const filesModified = /* @__PURE__ */ new Set();
|
|
372
|
+
for (const line of lines) {
|
|
373
|
+
if (!line.trim()) continue;
|
|
374
|
+
let entry;
|
|
375
|
+
try {
|
|
376
|
+
entry = JSON.parse(line);
|
|
377
|
+
} catch {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (entry.type === "user" && entry.message?.content) {
|
|
381
|
+
const text = contentToText(entry.message.content).slice(0, 300);
|
|
382
|
+
if (text) userMessages.push(text);
|
|
383
|
+
}
|
|
384
|
+
if (entry.type === "assistant" && entry.message?.content) {
|
|
385
|
+
const text = contentToText(entry.message.content);
|
|
386
|
+
const summaryMatch = text.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
|
|
387
|
+
if (summaryMatch) {
|
|
388
|
+
const s = summaryMatch[1].trim();
|
|
389
|
+
if (s.length > 5 && !summaries.includes(s)) summaries.push(s);
|
|
390
|
+
}
|
|
391
|
+
const captureMatch = text.match(/CAPTURE:\s*(.+?)(?:\n|$)/i);
|
|
392
|
+
if (captureMatch) {
|
|
393
|
+
const c = captureMatch[1].trim();
|
|
394
|
+
if (c.length > 5 && !captures.includes(c)) captures.push(c);
|
|
395
|
+
}
|
|
396
|
+
const completedMatch = text.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
|
|
397
|
+
if (completedMatch) {
|
|
398
|
+
lastCompleted = completedMatch[1].trim().replace(/\*+/g, "");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (entry.type === "assistant" && entry.message?.content && Array.isArray(entry.message.content)) {
|
|
402
|
+
for (const block of entry.message.content) {
|
|
403
|
+
if (block.type === "tool_use") {
|
|
404
|
+
const tool = block.name;
|
|
405
|
+
if ((tool === "Edit" || tool === "Write") && block.input?.file_path) {
|
|
406
|
+
filesModified.add(block.input.file_path);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const parts = [];
|
|
413
|
+
if (cwd) {
|
|
414
|
+
parts.push(`Working directory: ${cwd}`);
|
|
415
|
+
}
|
|
416
|
+
const recentUser = userMessages.slice(-3);
|
|
417
|
+
if (recentUser.length > 0) {
|
|
418
|
+
parts.push("\nRecent user requests:");
|
|
419
|
+
for (const msg of recentUser) {
|
|
420
|
+
const firstLine = msg.split("\n")[0].slice(0, 200);
|
|
421
|
+
parts.push(`- ${firstLine}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const recentSummaries = summaries.slice(-3);
|
|
425
|
+
if (recentSummaries.length > 0) {
|
|
426
|
+
parts.push("\nWork summaries:");
|
|
427
|
+
for (const s of recentSummaries) {
|
|
428
|
+
parts.push(`- ${s.slice(0, 150)}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const recentCaptures = captures.slice(-5);
|
|
432
|
+
if (recentCaptures.length > 0) {
|
|
433
|
+
parts.push("\nCaptured context:");
|
|
434
|
+
for (const c of recentCaptures) {
|
|
435
|
+
parts.push(`- ${c.slice(0, 150)}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const files = Array.from(filesModified).slice(-10);
|
|
439
|
+
if (files.length > 0) {
|
|
440
|
+
parts.push("\nFiles modified this session:");
|
|
441
|
+
for (const f of files) {
|
|
442
|
+
parts.push(`- ${f}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (lastCompleted) {
|
|
446
|
+
parts.push(`
|
|
447
|
+
Last completed: ${lastCompleted.slice(0, 150)}`);
|
|
448
|
+
}
|
|
449
|
+
const result = parts.join("\n");
|
|
450
|
+
return result.length > 50 ? result : null;
|
|
451
|
+
} catch (err) {
|
|
452
|
+
console.error(`extractSessionState error: ${err}`);
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function extractWorkFromTranscript(transcriptPath) {
|
|
457
|
+
try {
|
|
458
|
+
const raw = readFileSync3(transcriptPath, "utf-8");
|
|
459
|
+
const lines = raw.trim().split("\n");
|
|
460
|
+
const workItems = [];
|
|
461
|
+
const seenSummaries = /* @__PURE__ */ new Set();
|
|
462
|
+
for (const line of lines) {
|
|
463
|
+
if (!line.trim()) continue;
|
|
464
|
+
let entry;
|
|
465
|
+
try {
|
|
466
|
+
entry = JSON.parse(line);
|
|
467
|
+
} catch {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (entry.type === "assistant" && entry.message?.content) {
|
|
471
|
+
const text = contentToText(entry.message.content);
|
|
472
|
+
const summaryMatch = text.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
|
|
473
|
+
if (summaryMatch) {
|
|
474
|
+
const summary = summaryMatch[1].trim();
|
|
475
|
+
if (summary && !seenSummaries.has(summary) && summary.length > 5) {
|
|
476
|
+
seenSummaries.add(summary);
|
|
477
|
+
const details = [];
|
|
478
|
+
const actionsMatch = text.match(/ACTIONS:\s*(.+?)(?=\n[A-Z]+:|$)/is);
|
|
479
|
+
if (actionsMatch) {
|
|
480
|
+
const actionLines = actionsMatch[1].split("\n").map((l) => l.replace(/^[-*•]\s*/, "").replace(/^\d+\.\s*/, "").trim()).filter((l) => l.length > 3 && l.length < 100);
|
|
481
|
+
details.push(...actionLines.slice(0, 3));
|
|
482
|
+
}
|
|
483
|
+
workItems.push({ title: summary, details: details.length > 0 ? details : void 0, completed: true });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const completedMatch = text.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
|
|
487
|
+
if (completedMatch && workItems.length === 0) {
|
|
488
|
+
const completed = completedMatch[1].trim().replace(/\*+/g, "");
|
|
489
|
+
if (completed && !seenSummaries.has(completed) && completed.length > 5) {
|
|
490
|
+
seenSummaries.add(completed);
|
|
491
|
+
workItems.push({ title: completed, completed: true });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return workItems;
|
|
497
|
+
} catch {
|
|
498
|
+
return [];
|
|
499
|
+
}
|
|
500
|
+
}
|
|
230
501
|
async function main() {
|
|
231
502
|
let hookInput = null;
|
|
232
503
|
try {
|
|
233
504
|
const decoder = new TextDecoder();
|
|
234
505
|
let input = "";
|
|
235
506
|
const timeoutPromise = new Promise((resolve2) => {
|
|
236
|
-
setTimeout(
|
|
507
|
+
setTimeout(resolve2, 500);
|
|
237
508
|
});
|
|
238
509
|
const readPromise = (async () => {
|
|
239
510
|
for await (const chunk of process.stdin) {
|
|
@@ -244,34 +515,63 @@ async function main() {
|
|
|
244
515
|
if (input.trim()) {
|
|
245
516
|
hookInput = JSON.parse(input);
|
|
246
517
|
}
|
|
247
|
-
} catch
|
|
518
|
+
} catch {
|
|
248
519
|
}
|
|
249
|
-
const compactType = hookInput?.compact_type || "auto";
|
|
250
|
-
let message = "Compressing context to continue";
|
|
520
|
+
const compactType = hookInput?.compact_type || hookInput?.trigger || "auto";
|
|
251
521
|
let tokenCount = 0;
|
|
252
|
-
if (hookInput
|
|
522
|
+
if (hookInput?.transcript_path) {
|
|
253
523
|
const stats = getTranscriptStats(hookInput.transcript_path);
|
|
254
524
|
tokenCount = calculateSessionTokens(hookInput.transcript_path);
|
|
255
525
|
const tokenDisplay = tokenCount > 1e3 ? `${Math.round(tokenCount / 1e3)}k` : String(tokenCount);
|
|
256
|
-
|
|
257
|
-
if (compactType === "manual") {
|
|
258
|
-
message = `Manually compressing ${stats.messageCount} messages (~${tokenDisplay} tokens)`;
|
|
259
|
-
} else {
|
|
260
|
-
message = stats.isLarge ? `Auto-compressing large context (~${tokenDisplay} tokens)` : `Compressing context (~${tokenDisplay} tokens)`;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
526
|
+
const state = extractSessionState(hookInput.transcript_path, hookInput.cwd);
|
|
263
527
|
try {
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
const currentNotePath = getCurrentNotePath(notesDir);
|
|
528
|
+
const notesInfo = hookInput.cwd ? findNotesDir(hookInput.cwd) : { path: join3(dirname(hookInput.transcript_path), "Notes"), isLocal: false };
|
|
529
|
+
const currentNotePath = getCurrentNotePath(notesInfo.path);
|
|
267
530
|
if (currentNotePath) {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
531
|
+
const checkpointBody = state ? `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.
|
|
532
|
+
|
|
533
|
+
${state}` : `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.`;
|
|
534
|
+
appendCheckpoint(currentNotePath, checkpointBody);
|
|
535
|
+
const workItems = extractWorkFromTranscript(hookInput.transcript_path);
|
|
536
|
+
if (workItems.length > 0) {
|
|
537
|
+
addWorkToSessionNote(currentNotePath, workItems, `Pre-Compact (~${tokenDisplay} tokens)`);
|
|
538
|
+
console.error(`Added ${workItems.length} work item(s) to session note`);
|
|
539
|
+
}
|
|
540
|
+
console.error(`Rich checkpoint saved: ${basename2(currentNotePath)}`);
|
|
271
541
|
}
|
|
272
542
|
} catch (noteError) {
|
|
273
543
|
console.error(`Could not save checkpoint: ${noteError}`);
|
|
274
544
|
}
|
|
545
|
+
if (hookInput.cwd && state) {
|
|
546
|
+
try {
|
|
547
|
+
addTodoCheckpoint(hookInput.cwd, `Pre-compact checkpoint (~${tokenDisplay} tokens):
|
|
548
|
+
${state}`);
|
|
549
|
+
console.error("TODO.md checkpoint added");
|
|
550
|
+
} catch (todoError) {
|
|
551
|
+
console.error(`Could not update TODO.md: ${todoError}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (state && hookInput.session_id) {
|
|
555
|
+
const injection = [
|
|
556
|
+
"<system-reminder>",
|
|
557
|
+
`SESSION STATE RECOVERED AFTER COMPACTION (${compactType}, ~${tokenDisplay} tokens)`,
|
|
558
|
+
"",
|
|
559
|
+
state,
|
|
560
|
+
"",
|
|
561
|
+
"IMPORTANT: This session state was captured before context compaction.",
|
|
562
|
+
"Use it to maintain continuity. Continue the conversation from where",
|
|
563
|
+
"it left off without asking the user to repeat themselves.",
|
|
564
|
+
"Continue with the last task that you were asked to work on.",
|
|
565
|
+
"</system-reminder>"
|
|
566
|
+
].join("\n");
|
|
567
|
+
try {
|
|
568
|
+
const stateFile = join3(tmpdir(), `pai-compact-state-${hookInput.session_id}.txt`);
|
|
569
|
+
writeFileSync2(stateFile, injection, "utf-8");
|
|
570
|
+
console.error(`Session state saved to ${stateFile} (${injection.length} chars)`);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
console.error(`Failed to save state file: ${err}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
275
575
|
}
|
|
276
576
|
const ntfyMessage = tokenCount > 0 ? `Auto-pause: ~${Math.round(tokenCount / 1e3)}k tokens` : "Context compressing";
|
|
277
577
|
await sendNtfyNotification(ntfyMessage);
|