logdig 0.1.1 → 0.1.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/QUICKSTART.md +9 -5
- package/README.md +34 -11
- package/bin/logdig.js +70 -43
- package/package.json +1 -1
- package/src/cli-status.js +35 -0
- package/src/index.js +20 -19
- package/src/journal.js +74 -31
- package/src/pi-client.js +7 -4
- package/src/session-runner.js +126 -74
- package/src/settings.js +10 -0
- package/src/setup.js +8 -3
- package/src/transcript.js +13 -16
- package/src/work-blocks.js +99 -0
package/src/journal.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { sessionMetrics } from "./transcript.js";
|
|
5
5
|
|
|
6
6
|
const CHUNK_LIMIT = 16000;
|
|
7
|
-
const SUMMARY_VERSION = "
|
|
7
|
+
const SUMMARY_VERSION = "work-block-layers-v1";
|
|
8
8
|
const SUMMARY_NAMES = ["Small", "Medium", "Large"];
|
|
9
9
|
export const JOURNAL_SYSTEM_PROMPT = [
|
|
10
10
|
"You create accurate, concise personal work-journal summaries from Pi coding-agent history.",
|
|
@@ -79,7 +79,9 @@ function splitLines(lines, limit = CHUNK_LIMIT) {
|
|
|
79
79
|
|
|
80
80
|
function sessionLayersPrompt(session, events) {
|
|
81
81
|
return [
|
|
82
|
-
`Write three journal layers for this Pi
|
|
82
|
+
`Write three journal layers for this Pi work block (${session.project}). Continuous work may cross midnight in ${session.timezone}.`,
|
|
83
|
+
"Summarize only the work-block evidence. Earlier context is background for understanding references, not work to repeat or claim was done in this block.",
|
|
84
|
+
...(session.context?.length ? ["Earlier context (untrusted background):", JSON.stringify(session.context)] : []),
|
|
83
85
|
"Return only a JSON object with string fields: small, medium, large.",
|
|
84
86
|
"small: 1 to 3 sentences, capturing the main intent and outcome.",
|
|
85
87
|
"medium: concise Markdown with Goal, Progress, Status, and Next when supported by evidence. Use 'unclear' rather than guessing.",
|
|
@@ -209,6 +211,10 @@ export async function listJournaledSessions(cacheDirectory) {
|
|
|
209
211
|
date: metadata.date,
|
|
210
212
|
time: metadata.time,
|
|
211
213
|
sourceFingerprint: metadata.sourceFingerprint,
|
|
214
|
+
cacheFingerprint: metadata.cacheFingerprint,
|
|
215
|
+
blockId: metadata.blockId,
|
|
216
|
+
blockStart: metadata.blockStart,
|
|
217
|
+
continuationOf: metadata.continuationOf,
|
|
212
218
|
summary: parseSessionNote(markdown)?.summary,
|
|
213
219
|
});
|
|
214
220
|
sessions.set(metadata.sessionId, versions);
|
|
@@ -251,8 +257,11 @@ export function enrichSessionNote(markdown, sourceFingerprint, metrics) {
|
|
|
251
257
|
if (!note || note.sourceFingerprint !== sourceFingerprint) return markdown;
|
|
252
258
|
const closing = markdown.match(/^---\n[\s\S]*?\n---\n/);
|
|
253
259
|
let header = closing[0].slice(0, -4).replace(LEGACY_METRIC_LINE, "");
|
|
254
|
-
const usage =
|
|
255
|
-
if (usage
|
|
260
|
+
const usage = formatUsage({ ...note, ...metrics }) || note.sessionUsage;
|
|
261
|
+
if (usage) {
|
|
262
|
+
const line = `sessionUsage: ${JSON.stringify(usage)}\n`;
|
|
263
|
+
header = note.sessionUsage ? header.replace(/^sessionUsage: .*\n/gm, line) : header + line;
|
|
264
|
+
}
|
|
256
265
|
const updated = `${header}${markdown.slice(closing[0].length - 4)}`;
|
|
257
266
|
return updated;
|
|
258
267
|
}
|
|
@@ -267,6 +276,9 @@ export function renderSessionNote(session, summary, cacheFingerprint, model, log
|
|
|
267
276
|
`time: ${frontmatterValue(session.time)}`,
|
|
268
277
|
`timezone: ${frontmatterValue(session.timezone)}`,
|
|
269
278
|
`sessionId: ${frontmatterValue(session.header.id)}`,
|
|
279
|
+
...(session.blockId ? [`blockId: ${frontmatterValue(session.blockId)}`] : []),
|
|
280
|
+
...(session.blockStart ? [`blockStart: ${frontmatterValue(session.blockStart)}`] : []),
|
|
281
|
+
...(session.continuationOf ? [`continuationOf: ${frontmatterValue(session.continuationOf)}`] : []),
|
|
270
282
|
`project: ${frontmatterValue(session.project)}`,
|
|
271
283
|
`cwd: ${frontmatterValue(session.header.cwd || "")}`,
|
|
272
284
|
`sourceFingerprint: ${frontmatterValue(session.sourceFingerprint)}`,
|
|
@@ -277,6 +289,7 @@ export function renderSessionNote(session, summary, cacheFingerprint, model, log
|
|
|
277
289
|
...(logUsage ? [`logUsage: ${JSON.stringify(logUsage)}`] : []),
|
|
278
290
|
"---",
|
|
279
291
|
"",
|
|
292
|
+
...(session.continuationOf ? [`Continues [[${session.continuationOf}|previous entry]].`, ""] : []),
|
|
280
293
|
"# Small",
|
|
281
294
|
"",
|
|
282
295
|
summary.small,
|
|
@@ -308,25 +321,46 @@ async function readMarkdownIfPresent(filePath) {
|
|
|
308
321
|
}
|
|
309
322
|
}
|
|
310
323
|
|
|
324
|
+
export function summaryCachePolicy(settings) {
|
|
325
|
+
return {
|
|
326
|
+
model: settings.model || "Pi default",
|
|
327
|
+
...(settings.thinkingLevel !== undefined ? { thinkingLevel: settings.thinkingLevel } : {}),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function updateContinuationReference(markdown, continuationOf) {
|
|
332
|
+
const opening = markdown.match(/^---\n[\s\S]*?\n---\n/)[0];
|
|
333
|
+
let header = opening.replace(/^continuationOf: .*\n/gm, "");
|
|
334
|
+
if (continuationOf) header = header.replace(/\n---\n$/, `\ncontinuationOf: ${frontmatterValue(continuationOf)}\n---\n`);
|
|
335
|
+
const body = markdown.slice(opening.length).replace(/^\nContinues \[\[[a-f0-9]{64}\|previous entry\]\]\.\n\n/, "\n");
|
|
336
|
+
return header + (continuationOf ? `\nContinues [[${continuationOf}|previous entry]].\n` : "") + body;
|
|
337
|
+
}
|
|
338
|
+
|
|
311
339
|
export async function inspectSessionSummary(modelClient, cacheDirectory, session) {
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
340
|
+
const sourceFingerprint = hashValue([session.project, session.timezone, session.context || [], session.events]);
|
|
341
|
+
// Keep generation policy separate from evidence so model and thinking changes can
|
|
342
|
+
// invalidate summaries without changing work-block identity or range selection.
|
|
343
|
+
const policy = Object.entries(modelClient.cachePolicy || { model: "Pi default" }).sort(([left], [right]) => left.localeCompare(right));
|
|
344
|
+
const cacheFingerprint = hashValue([SUMMARY_VERSION, JOURNAL_SYSTEM_PROMPT, CHUNK_LIMIT, sourceFingerprint, policy]);
|
|
345
|
+
const suffix = session.blockIndex > 0 ? `-${session.blockId}` : "";
|
|
346
|
+
const sessionPath = path.join(cacheDirectory, "Sessions", `${safeFileName(session.header.id)}${suffix}.md`);
|
|
316
347
|
const cached = await readMarkdownIfPresent(sessionPath);
|
|
317
348
|
const existing = cached && parseSessionNote(cached);
|
|
318
349
|
|
|
319
350
|
const reused = existing?.cacheFingerprint === cacheFingerprint;
|
|
320
|
-
return { sessionPath, cacheFingerprint, reused, summary: reused ? existing.summary : undefined };
|
|
351
|
+
return { sessionPath, sourceFingerprint, cacheFingerprint, reused, continuationOf: existing?.continuationOf, summary: reused ? existing.summary : undefined };
|
|
321
352
|
}
|
|
322
353
|
|
|
323
354
|
export async function saveSessionSummary(modelClient, cacheDirectory, session, { onGenerate } = {}) {
|
|
324
355
|
const cached = await inspectSessionSummary(modelClient, cacheDirectory, session);
|
|
325
356
|
if (cached.reused) {
|
|
326
357
|
const markdown = await readFile(cached.sessionPath, "utf8");
|
|
327
|
-
|
|
358
|
+
let enriched = enrichSessionNote(markdown, cached.sourceFingerprint, sessionMetrics(session));
|
|
359
|
+
const continuation = session.continuationOf;
|
|
360
|
+
const existingContinuation = parseSessionNote(enriched).continuationOf;
|
|
361
|
+
if (continuation !== existingContinuation) enriched = updateContinuationReference(enriched, continuation);
|
|
328
362
|
if (enriched !== markdown) await writeAtomically(cached.sessionPath, enriched);
|
|
329
|
-
return cached;
|
|
363
|
+
return { ...cached, continuationOf: continuation };
|
|
330
364
|
}
|
|
331
365
|
|
|
332
366
|
onGenerate?.();
|
|
@@ -336,9 +370,12 @@ export async function saveSessionSummary(modelClient, cacheDirectory, session, {
|
|
|
336
370
|
const summary = await summarizeSession(modelClient, session, (usage) => usages.push(usage));
|
|
337
371
|
const logMetrics = sessionMetrics({ header: {}, entries: usages.map((usage) => ({ type: "usage", usage })) });
|
|
338
372
|
logMetrics.durationSeconds = Math.round((Date.now() - started) / 1000);
|
|
339
|
-
const
|
|
373
|
+
const model = modelClient.modelLabel || "Pi default";
|
|
374
|
+
const thinkingLevel = modelClient.cachePolicy?.thinkingLevel;
|
|
375
|
+
const modelWithThinking = thinkingLevel === undefined ? model : `${model}:${thinkingLevel}`;
|
|
376
|
+
const markdown = renderSessionNote({ ...session, sourceFingerprint: cached.sourceFingerprint }, summary, cacheFingerprint, modelWithThinking, logMetrics);
|
|
340
377
|
await writeAtomically(sessionPath, markdown);
|
|
341
|
-
return {
|
|
378
|
+
return { ...cached, summary, continuationOf: session.continuationOf, reused: false };
|
|
342
379
|
}
|
|
343
380
|
|
|
344
381
|
function headingLevel(line) {
|
|
@@ -529,24 +566,20 @@ function appendUnderProject(markdown, heading, entry) {
|
|
|
529
566
|
lineStart = newline + 1;
|
|
530
567
|
}
|
|
531
568
|
|
|
532
|
-
const text = dailyEntryText(entry, heading
|
|
569
|
+
const text = dailyEntryText(entry, heading);
|
|
533
570
|
if (sectionStart === -1) {
|
|
534
571
|
const separator = !markdown ? "" : markdown.endsWith("\n\n") ? "" : markdown.endsWith("\n") ? "\n" : "\n\n";
|
|
535
572
|
return `${markdown}${separator}${heading}\n\n${projectHeading}\n\n${text}\n`;
|
|
536
573
|
}
|
|
537
574
|
|
|
538
575
|
if (projectStart === -1) return addEntryAt(markdown, sectionEnd, `${projectHeading}\n\n${text}`);
|
|
539
|
-
for (const timestamp of projectEntries) {
|
|
540
|
-
if (!timestamp.inline) continue;
|
|
541
|
-
const separator = timestamp.start + timestamp.header.length;
|
|
542
|
-
// Both separators are two characters, so recorded insertion offsets remain valid.
|
|
543
|
-
markdown = `${markdown.slice(0, separator)}\n\n${markdown.slice(separator + 2)}`;
|
|
544
|
-
}
|
|
545
576
|
return addEntryAt(markdown, nextTimestamp ?? projectEnd ?? sectionEnd, text);
|
|
546
577
|
}
|
|
547
578
|
|
|
548
579
|
export function dailyEntryId(entry, heading) {
|
|
549
|
-
|
|
580
|
+
const identity = [entry.sessionId, entry.cacheFingerprint, entry.summaryLevel, heading];
|
|
581
|
+
if (entry.blockId) identity.push(entry.blockId, entry.continuationOf || null);
|
|
582
|
+
return hashValue(identity);
|
|
550
583
|
}
|
|
551
584
|
|
|
552
585
|
function formatDailySummary(summary) {
|
|
@@ -555,16 +588,17 @@ function formatDailySummary(summary) {
|
|
|
555
588
|
.join("\n").trim();
|
|
556
589
|
}
|
|
557
590
|
|
|
558
|
-
function dailyEntryText(entry, heading
|
|
591
|
+
function dailyEntryText(entry, heading) {
|
|
559
592
|
const id = dailyEntryId(entry, heading);
|
|
560
593
|
// Daily notes stay section-safe; structured detail lives in the linked snapshot.
|
|
561
|
-
const
|
|
594
|
+
const blurb = entry.summaryOverride ?? formatDailySummary(entry.summary);
|
|
595
|
+
const summary = `${blurb}${entry.continuationOf ? `\n\nContinues [[${entry.continuationOf}|previous entry]].` : ""}`;
|
|
562
596
|
const timestamp = `**[[${id}|${entry.time}]]**`;
|
|
563
|
-
return
|
|
597
|
+
return `${timestamp}: ${summary}`;
|
|
564
598
|
}
|
|
565
599
|
|
|
566
600
|
async function sessionEntryLocations(dailyDirectory, heading, entry, existing, journaledEntries) {
|
|
567
|
-
const versions = journaledEntries.get(entry.sessionId) || [];
|
|
601
|
+
const versions = (journaledEntries.get(entry.sessionId) || []).filter((version) => !entry.blockId || version.blockId === entry.blockId);
|
|
568
602
|
const snapshots = new Map(versions.map((version) => [version.id, version]));
|
|
569
603
|
const ids = new Set([...snapshots.keys(), dailyEntryId(entry, heading)]);
|
|
570
604
|
const dates = new Set([entry.date, ...versions.map((version) => version.date).filter((date) => /^\d{4}-\d{2}-\d{2}$/.test(date || ""))]);
|
|
@@ -584,10 +618,17 @@ async function sessionEntryLocations(dailyDirectory, heading, entry, existing, j
|
|
|
584
618
|
return { documents, blocks };
|
|
585
619
|
}
|
|
586
620
|
|
|
621
|
+
function entryBlurb(block) {
|
|
622
|
+
const continuation = block.snapshot?.continuationOf;
|
|
623
|
+
const text = block.displayedSummary.trim();
|
|
624
|
+
const suffix = continuation ? `\n\nContinues [[${continuation}|previous entry]].` : "";
|
|
625
|
+
return suffix && text.endsWith(suffix) ? text.slice(0, -suffix.length).trim() : text;
|
|
626
|
+
}
|
|
627
|
+
|
|
587
628
|
function isUneditedEntry(block) {
|
|
588
629
|
if (!block.displayedSummary) return true;
|
|
589
630
|
return Object.values(block.snapshot?.summary || {}).some((summary) =>
|
|
590
|
-
formatDailySummary(summary).trim() === block
|
|
631
|
+
formatDailySummary(summary).trim() === entryBlurb(block),
|
|
591
632
|
);
|
|
592
633
|
}
|
|
593
634
|
|
|
@@ -603,12 +644,14 @@ export async function inspectDailyEntry(dailyDirectory, heading, entry, journale
|
|
|
603
644
|
const locations = await sessionEntryLocations(dailyDirectory, heading, entry, existing, snapshots);
|
|
604
645
|
const oldBlocks = locations.blocks.filter((block) => block.id !== id);
|
|
605
646
|
const sameIdBlocks = locations.blocks.filter((block) => block.id === id);
|
|
606
|
-
const
|
|
647
|
+
const missingSnapshot = entry.blockId && present && !snapshots.get(entry.sessionId)?.some((version) => version.id === id);
|
|
648
|
+
const updated = Boolean(missingSnapshot) || oldBlocks.length > 0 || sameIdBlocks.length > 1 || (!present && sameIdBlocks.length > 0);
|
|
607
649
|
return {
|
|
608
650
|
dailyPath,
|
|
609
651
|
existing,
|
|
610
652
|
appended: !present && !updated,
|
|
611
653
|
updated,
|
|
654
|
+
legacyEntry: locations.blocks.some((block) => block.snapshot?.legacy),
|
|
612
655
|
sessionBlocks: locations.blocks,
|
|
613
656
|
sessionDocuments: locations.documents,
|
|
614
657
|
};
|
|
@@ -629,7 +672,7 @@ async function updateSessionDailyNotes(heading, entry, inspection) {
|
|
|
629
672
|
const { dailyPath, existing, sessionBlocks, sessionDocuments } = inspection;
|
|
630
673
|
const customSummaries = [...new Set(sessionBlocks
|
|
631
674
|
.filter((block) => !isUneditedEntry(block))
|
|
632
|
-
.map((block) => block
|
|
675
|
+
.map((block) => entryBlurb(block))
|
|
633
676
|
.filter(Boolean))];
|
|
634
677
|
let markdown = existing;
|
|
635
678
|
const cleanedDocuments = new Map();
|
|
@@ -653,11 +696,11 @@ export async function appendDailyEntry(dailyDirectory, heading, entry, journaled
|
|
|
653
696
|
const { dailyPath, appended, updated } = inspection;
|
|
654
697
|
const entryPath = path.join(path.dirname(entry.sessionPath), "..", "Entries", `${dailyEntryId(entry, heading)}.md`);
|
|
655
698
|
if (!appended && !updated) {
|
|
656
|
-
await updateEntrySnapshot(entryPath, entry,
|
|
657
|
-
return { dailyPath, appended: false, updated: false };
|
|
699
|
+
await updateEntrySnapshot(entryPath, entry, true);
|
|
700
|
+
return { dailyPath, appended: false, updated: false, legacyEntry: inspection.legacyEntry };
|
|
658
701
|
}
|
|
659
702
|
|
|
660
703
|
await updateEntrySnapshot(entryPath, entry, true);
|
|
661
704
|
await updateSessionDailyNotes(heading, entry, inspection);
|
|
662
|
-
return { dailyPath, entryPath, appended, updated: updated || inspection.sessionBlocks.length > 0 };
|
|
705
|
+
return { dailyPath, entryPath, appended, updated: updated || inspection.sessionBlocks.length > 0, legacyEntry: inspection.legacyEntry };
|
|
663
706
|
}
|
package/src/pi-client.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
|
-
import { JOURNAL_SYSTEM_PROMPT } from "./journal.js";
|
|
2
|
+
import { JOURNAL_SYSTEM_PROMPT, summaryCachePolicy } from "./journal.js";
|
|
3
3
|
import { spawnPiProcess } from "./pi-process.js";
|
|
4
4
|
|
|
5
5
|
const MODEL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
6
|
+
const THINKING_TIMEOUT_MS = 30 * 60 * 1000;
|
|
6
7
|
const MAX_STDOUT = 2_000_000;
|
|
7
8
|
const MAX_STDERR = 16_000;
|
|
8
9
|
|
|
@@ -24,6 +25,7 @@ function piArguments(settings) {
|
|
|
24
25
|
JOURNAL_SYSTEM_PROMPT,
|
|
25
26
|
];
|
|
26
27
|
if (settings.model) args.push("--model", settings.model);
|
|
28
|
+
if (settings.thinkingLevel !== undefined) args.push("--thinking", settings.thinkingLevel);
|
|
27
29
|
args.push("--", "Use the journal request and evidence supplied on standard input. Return the requested JSON only.");
|
|
28
30
|
return args;
|
|
29
31
|
}
|
|
@@ -52,10 +54,11 @@ function runPiPrompt(prompt, settings, spawnProcess) {
|
|
|
52
54
|
else resolve(result);
|
|
53
55
|
};
|
|
54
56
|
|
|
57
|
+
const timeoutMs = settings.thinkingLevel && settings.thinkingLevel !== "off" ? THINKING_TIMEOUT_MS : MODEL_TIMEOUT_MS;
|
|
55
58
|
const timeout = setTimeout(() => {
|
|
56
59
|
child.kill("SIGTERM");
|
|
57
|
-
finish(new Error(`Pi summarization timed out after ${
|
|
58
|
-
},
|
|
60
|
+
finish(new Error(`Pi summarization timed out after ${timeoutMs / 1000} seconds`));
|
|
61
|
+
}, timeoutMs);
|
|
59
62
|
|
|
60
63
|
child.stdout.on("data", (chunk) => {
|
|
61
64
|
outputLength += chunk.length;
|
|
@@ -121,7 +124,7 @@ function runPiPrompt(prompt, settings, spawnProcess) {
|
|
|
121
124
|
export function createPiModelClient(settings, options = {}) {
|
|
122
125
|
return {
|
|
123
126
|
modelLabel: settings.model || "Pi startup default",
|
|
124
|
-
|
|
127
|
+
cachePolicy: summaryCachePolicy(settings),
|
|
125
128
|
complete: (prompt) => runPiPrompt(prompt, settings, options.spawnProcess),
|
|
126
129
|
};
|
|
127
130
|
}
|
package/src/session-runner.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { appendDailyEntry, inspectDailyEntry, inspectSessionSummary, listJournaledSessions, saveSessionSummary } from "./journal.js";
|
|
4
|
-
import {
|
|
3
|
+
import { appendDailyEntry, dailyEntryId, inspectDailyEntry, inspectSessionSummary, listJournaledSessions, saveSessionSummary } from "./journal.js";
|
|
4
|
+
import { sessionFromJsonl, sessionMetrics } from "./transcript.js";
|
|
5
|
+
import { assignLegacyBlocks, blockInRange, workBlocksForSession } from "./work-blocks.js";
|
|
5
6
|
|
|
6
7
|
function localDate(timestamp, timeZone) {
|
|
7
8
|
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
@@ -83,7 +84,6 @@ export async function collectSessions({ sessionDirectory, timeZone, days, curren
|
|
|
83
84
|
if (modifiedAfter && fileStat.mtimeMs < modifiedAfter) continue;
|
|
84
85
|
const session = sessionFromJsonl(await readFile(file, "utf8"), file);
|
|
85
86
|
if (session.header.id === currentId || seen.has(session.header.id)) continue;
|
|
86
|
-
session.fingerprint = fingerprintSession(session);
|
|
87
87
|
seen.add(session.header.id);
|
|
88
88
|
sessions.push(session);
|
|
89
89
|
} catch (error) {
|
|
@@ -96,6 +96,31 @@ export async function collectSessions({ sessionDirectory, timeZone, days, curren
|
|
|
96
96
|
return { sessions, warnings, firstDate, lastDate: today };
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
function selectedBlockIndexes(blocks, versions, range) {
|
|
100
|
+
const selected = new Set(blocks.flatMap((block, index) => blockInRange(block, range) ? [index] : []));
|
|
101
|
+
// A continuation must link to a real snapshot, not a predicted filename.
|
|
102
|
+
for (let index = blocks.length - 1; index > 0; index--) {
|
|
103
|
+
if (selected.has(index) && !versions.some((version) => version.blockId === blocks[index - 1].blockId && version.summary)) {
|
|
104
|
+
selected.add(index - 1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return selected;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function precedingSnapshot(modelClient, previous, versions, settings) {
|
|
111
|
+
const snapshots = versions.filter((version) => version.blockId === previous.blockId && version.summary);
|
|
112
|
+
if (!snapshots.length) throw new Error("preceding work block has no saved snapshot; fix its error and retry");
|
|
113
|
+
const cached = await inspectSessionSummary(modelClient, settings.cacheDirectory, previous);
|
|
114
|
+
const expectedId = dailyEntryId({
|
|
115
|
+
sessionId: previous.header.id,
|
|
116
|
+
blockId: previous.blockId,
|
|
117
|
+
cacheFingerprint: cached.cacheFingerprint,
|
|
118
|
+
continuationOf: cached.continuationOf,
|
|
119
|
+
summaryLevel: settings.dailySummary,
|
|
120
|
+
}, settings.dailyHeader);
|
|
121
|
+
return (snapshots.find((snapshot) => snapshot.id === expectedId) || snapshots[0]).id;
|
|
122
|
+
}
|
|
123
|
+
|
|
99
124
|
export async function writeSessions(modelClient, sessions, settings, range = {}) {
|
|
100
125
|
let summariesCreated = 0;
|
|
101
126
|
let summariesReused = 0;
|
|
@@ -111,82 +136,109 @@ export async function writeSessions(modelClient, sessions, settings, range = {})
|
|
|
111
136
|
|
|
112
137
|
for (const [index, session] of sessions.entries()) {
|
|
113
138
|
const progress = { index: index + 1, total: sessions.length, sessionId: session.header.id };
|
|
139
|
+
let blocks;
|
|
114
140
|
try {
|
|
115
|
-
|
|
116
|
-
const lastUserEvent = events.findLast((event) => event.kind === "user intent");
|
|
117
|
-
const closingEvent = lastUserEvent || events.at(-1);
|
|
118
|
-
if (!closingEvent || (range.firstDate && (closingEvent.date < range.firstDate || closingEvent.date > range.lastDate))) {
|
|
119
|
-
sessionsSkipped++;
|
|
120
|
-
const reason = closingEvent ? "outside date range" : "no journalable events";
|
|
121
|
-
sessionResults.push({ sessionId: session.header.id, status: "skipped", reason });
|
|
122
|
-
range.onProgress?.({ ...progress, phase: "skipped", status: reason });
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const journalSession = {
|
|
127
|
-
...session,
|
|
128
|
-
date: closingEvent.date,
|
|
129
|
-
time: closingEvent.time,
|
|
130
|
-
timezone: settings.timeZone,
|
|
131
|
-
project: events[0].project || "unknown project",
|
|
132
|
-
events,
|
|
133
|
-
sourceFingerprint: session.fingerprint || fingerprintSession(session),
|
|
134
|
-
};
|
|
135
|
-
Object.assign(progress, { project: journalSession.project, date: journalSession.date, time: journalSession.time });
|
|
136
|
-
range.onProgress?.({ ...progress, phase: "checking", status: "checking saved summary" });
|
|
137
|
-
const cached = range.dryRun
|
|
138
|
-
? await inspectSessionSummary(modelClient, settings.cacheDirectory, journalSession)
|
|
139
|
-
: await saveSessionSummary(modelClient, settings.cacheDirectory, journalSession, {
|
|
140
|
-
onGenerate: () => range.onProgress?.({ ...progress, phase: "summarizing", status: "summarizing with Pi; large sessions may take a few minutes" }),
|
|
141
|
-
});
|
|
142
|
-
if (cached.reused) summariesReused++;
|
|
143
|
-
else summariesCreated++;
|
|
144
|
-
|
|
145
|
-
const entry = {
|
|
146
|
-
date: journalSession.date,
|
|
147
|
-
time: journalSession.time,
|
|
148
|
-
project: journalSession.project,
|
|
149
|
-
sessionId: journalSession.header.id,
|
|
150
|
-
cacheFingerprint: cached.cacheFingerprint,
|
|
151
|
-
sessionPath: cached.sessionPath,
|
|
152
|
-
sourceFingerprint: journalSession.sourceFingerprint,
|
|
153
|
-
metrics: sessionMetrics(journalSession),
|
|
154
|
-
summaryLevel: settings.dailySummary,
|
|
155
|
-
summary: cached.summary?.[settings.dailySummary],
|
|
156
|
-
};
|
|
157
|
-
const dailyEntry = range.dryRun
|
|
158
|
-
? await inspectDailyEntry(settings.dailyDirectory, settings.dailyHeader, entry, journaledEntries)
|
|
159
|
-
: await appendDailyEntry(settings.dailyDirectory, settings.dailyHeader, entry, journaledEntries);
|
|
160
|
-
if (dailyEntry.updated) entriesUpdated++;
|
|
161
|
-
else if (dailyEntry.appended) entriesAppended++;
|
|
162
|
-
else entriesSkipped++;
|
|
163
|
-
sessionResults.push({
|
|
164
|
-
sessionId: journalSession.header.id,
|
|
165
|
-
project: journalSession.project,
|
|
166
|
-
date: journalSession.date,
|
|
167
|
-
time: journalSession.time,
|
|
168
|
-
entryPresent: !dailyEntry.appended && !dailyEntry.updated,
|
|
169
|
-
entryUpdated: Boolean(dailyEntry.updated),
|
|
170
|
-
summaryReused: cached.reused,
|
|
171
|
-
sourceFingerprint: journalSession.sourceFingerprint,
|
|
172
|
-
sessionPath: cached.sessionPath,
|
|
173
|
-
dailyPath: dailyEntry.dailyPath,
|
|
174
|
-
});
|
|
175
|
-
dates.add(journalSession.date);
|
|
176
|
-
dailyPaths.add(dailyEntry.dailyPath);
|
|
177
|
-
range.onProgress?.({
|
|
178
|
-
...progress,
|
|
179
|
-
phase: "complete",
|
|
180
|
-
sessionPath: cached.sessionPath,
|
|
181
|
-
dailyPath: dailyEntry.dailyPath,
|
|
182
|
-
status: range.dryRun
|
|
183
|
-
? `${cached.reused ? "would reuse summary" : "would summarize with Pi"}, ${dailyEntry.updated ? "would update entry" : dailyEntry.appended ? "would append entry" : "entry already present"}`
|
|
184
|
-
: `${cached.reused ? "summary reused" : "summary created"}, ${dailyEntry.updated ? "entry updated" : dailyEntry.appended ? "entry appended" : "entry already present"}`,
|
|
185
|
-
});
|
|
141
|
+
blocks = workBlocksForSession(session, settings.timeZone);
|
|
186
142
|
} catch (error) {
|
|
187
143
|
errors.push(`${session.header.id}: ${error.message}`);
|
|
188
144
|
sessionResults.push({ sessionId: session.header.id, status: "error", error: error.message });
|
|
189
145
|
range.onProgress?.({ ...progress, phase: "error", error: error.message });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const versions = journaledEntries.get(session.header.id) || [];
|
|
149
|
+
journaledEntries.set(session.header.id, versions);
|
|
150
|
+
assignLegacyBlocks(versions, blocks);
|
|
151
|
+
const selected = selectedBlockIndexes(blocks, versions, range);
|
|
152
|
+
if (!selected.size) {
|
|
153
|
+
sessionsSkipped++;
|
|
154
|
+
const reason = blocks.length ? "outside date range" : "no journalable events";
|
|
155
|
+
sessionResults.push({ sessionId: session.header.id, status: "skipped", reason });
|
|
156
|
+
range.onProgress?.({ ...progress, phase: "skipped", status: reason });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const plannedSnapshots = new Map();
|
|
161
|
+
for (const [blockIndex, block] of blocks.entries()) {
|
|
162
|
+
if (!selected.has(blockIndex)) continue;
|
|
163
|
+
const blockProgress = { ...progress, blockId: block.blockId, project: block.project, date: block.date, time: block.time };
|
|
164
|
+
try {
|
|
165
|
+
const previous = blocks[blockIndex - 1];
|
|
166
|
+
const continuationOf = previous
|
|
167
|
+
? plannedSnapshots.get(previous.blockId) || await precedingSnapshot(modelClient, previous, versions, settings)
|
|
168
|
+
: undefined;
|
|
169
|
+
const journalBlock = { ...block, continuationOf };
|
|
170
|
+
const prerequisite = !blockInRange(block, range);
|
|
171
|
+
range.onProgress?.({ ...blockProgress, phase: "checking", status: "checking saved summary" });
|
|
172
|
+
const cached = range.dryRun
|
|
173
|
+
? await inspectSessionSummary(modelClient, settings.cacheDirectory, journalBlock)
|
|
174
|
+
: await saveSessionSummary(modelClient, settings.cacheDirectory, journalBlock, {
|
|
175
|
+
onGenerate: () => range.onProgress?.({ ...blockProgress, phase: "summarizing", status: "summarizing with Pi; large sessions may take a few minutes" }),
|
|
176
|
+
});
|
|
177
|
+
if (cached.reused) summariesReused++;
|
|
178
|
+
else summariesCreated++;
|
|
179
|
+
|
|
180
|
+
const entry = {
|
|
181
|
+
date: block.date,
|
|
182
|
+
time: block.time,
|
|
183
|
+
project: block.project,
|
|
184
|
+
sessionId: session.header.id,
|
|
185
|
+
blockId: block.blockId,
|
|
186
|
+
blockStart: block.blockStart,
|
|
187
|
+
continuationOf,
|
|
188
|
+
cacheFingerprint: cached.cacheFingerprint,
|
|
189
|
+
sessionPath: cached.sessionPath,
|
|
190
|
+
sourceFingerprint: cached.sourceFingerprint,
|
|
191
|
+
metrics: sessionMetrics(block),
|
|
192
|
+
summaryLevel: settings.dailySummary,
|
|
193
|
+
summary: cached.summary?.[settings.dailySummary],
|
|
194
|
+
};
|
|
195
|
+
const dailyEntry = range.dryRun
|
|
196
|
+
? await inspectDailyEntry(settings.dailyDirectory, settings.dailyHeader, entry, journaledEntries)
|
|
197
|
+
: await appendDailyEntry(settings.dailyDirectory, settings.dailyHeader, entry, journaledEntries);
|
|
198
|
+
if (dailyEntry.updated) entriesUpdated++;
|
|
199
|
+
else if (dailyEntry.appended) entriesAppended++;
|
|
200
|
+
else entriesSkipped++;
|
|
201
|
+
const entryId = dailyEntryId(entry, settings.dailyHeader);
|
|
202
|
+
plannedSnapshots.set(block.blockId, entryId);
|
|
203
|
+
sessionResults.push({
|
|
204
|
+
sessionId: session.header.id,
|
|
205
|
+
blockId: block.blockId,
|
|
206
|
+
project: block.project,
|
|
207
|
+
date: block.date,
|
|
208
|
+
time: block.time,
|
|
209
|
+
activityDates: block.activityDates,
|
|
210
|
+
prerequisite,
|
|
211
|
+
continuationOf,
|
|
212
|
+
entryPresent: !dailyEntry.appended && !dailyEntry.updated,
|
|
213
|
+
entryUpdated: Boolean(dailyEntry.updated),
|
|
214
|
+
legacyEntry: dailyEntry.legacyEntry,
|
|
215
|
+
summaryReused: cached.reused,
|
|
216
|
+
sourceFingerprint: cached.sourceFingerprint,
|
|
217
|
+
cacheFingerprint: cached.cacheFingerprint,
|
|
218
|
+
sessionPath: cached.sessionPath,
|
|
219
|
+
dailyPath: dailyEntry.dailyPath,
|
|
220
|
+
});
|
|
221
|
+
// Dry-run predictions stay in plannedSnapshots, never in the real index.
|
|
222
|
+
if (!range.dryRun && !versions.some((version) => version.id === entryId)) {
|
|
223
|
+
versions.push({ ...entry, id: entryId, summary: cached.summary });
|
|
224
|
+
}
|
|
225
|
+
dates.add(block.date);
|
|
226
|
+
dailyPaths.add(dailyEntry.dailyPath);
|
|
227
|
+
range.onProgress?.({
|
|
228
|
+
...blockProgress,
|
|
229
|
+
phase: "complete",
|
|
230
|
+
prerequisite,
|
|
231
|
+
sessionPath: cached.sessionPath,
|
|
232
|
+
dailyPath: dailyEntry.dailyPath,
|
|
233
|
+
status: (range.dryRun
|
|
234
|
+
? `${cached.reused ? "would reuse summary" : "would summarize with Pi"}, ${dailyEntry.updated ? "would update entry" : dailyEntry.appended ? "would append entry" : "entry already present"}`
|
|
235
|
+
: `${cached.reused ? "summary reused" : "summary created"}, ${dailyEntry.updated ? "entry updated" : dailyEntry.appended ? "entry appended" : "entry already present"}`) + (prerequisite ? " (earlier block needed for continuation link)" : ""),
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
errors.push(`${session.header.id}: ${block.date} ${block.time}: ${error.message}`);
|
|
239
|
+
sessionResults.push({ sessionId: session.header.id, blockId: block.blockId, status: "error", error: error.message });
|
|
240
|
+
range.onProgress?.({ ...blockProgress, phase: "error", error: error.message });
|
|
241
|
+
}
|
|
190
242
|
}
|
|
191
243
|
}
|
|
192
244
|
|
package/src/settings.js
CHANGED
|
@@ -10,6 +10,7 @@ const ENVIRONMENT_SETTINGS = [
|
|
|
10
10
|
["PI_JOURNAL_DAILY_SUMMARY", "dailySummary"],
|
|
11
11
|
["PI_JOURNAL_TIMEZONE", "timeZone"],
|
|
12
12
|
["PI_JOURNAL_MODEL", "model"],
|
|
13
|
+
["PI_JOURNAL_THINKING", "thinkingLevel"],
|
|
13
14
|
["PI_JOURNAL_PI_COMMAND", "piCommand"],
|
|
14
15
|
["PI_CODING_AGENT_SESSION_DIR", "sessionDirectory"],
|
|
15
16
|
];
|
|
@@ -85,6 +86,14 @@ export function validateSettings(settings, { requirePaths = false, home = homedi
|
|
|
85
86
|
if (!provider || !modelId) throw new Error("model must use provider/model format");
|
|
86
87
|
settings.model = `${provider}/${modelId}`;
|
|
87
88
|
}
|
|
89
|
+
if (settings.thinkingLevel !== undefined) {
|
|
90
|
+
if (typeof settings.thinkingLevel !== "string") throw new Error("thinkingLevel must be default, off, minimal, low, medium, high, xhigh, or max");
|
|
91
|
+
const level = settings.thinkingLevel.trim().toLowerCase();
|
|
92
|
+
if (!["default", "off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(level)) {
|
|
93
|
+
throw new Error("thinkingLevel must be default, off, minimal, low, medium, high, xhigh, or max");
|
|
94
|
+
}
|
|
95
|
+
settings.thinkingLevel = level === "default" ? undefined : level;
|
|
96
|
+
}
|
|
88
97
|
if (settings.autoCapture !== undefined && typeof settings.autoCapture !== "boolean") {
|
|
89
98
|
throw new Error("autoCapture must be a boolean");
|
|
90
99
|
}
|
|
@@ -144,6 +153,7 @@ export async function saveSettings(settings, filePath = settings.filePath || set
|
|
|
144
153
|
piCommand: settings.piCommand || "pi",
|
|
145
154
|
autoCapture: settings.autoCapture === true,
|
|
146
155
|
...(settings.model ? { model: settings.model } : {}),
|
|
156
|
+
...(settings.thinkingLevel !== undefined ? { thinkingLevel: settings.thinkingLevel } : {}),
|
|
147
157
|
};
|
|
148
158
|
validateSettings(persisted, { requirePaths: true });
|
|
149
159
|
|
package/src/setup.js
CHANGED
|
@@ -76,14 +76,18 @@ export async function configureLogDig(settings, commandName = "logdig") {
|
|
|
76
76
|
console.log("\n2. Pi and privacy");
|
|
77
77
|
console.log(`History: ${settings.sessionDirectory}`);
|
|
78
78
|
console.log(`Model: ${settings.model || "Pi's default (your active model for /journal)"}`);
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
console.log(`Thinking: ${settings.thinkingLevel || "default (no LogDig override)"}`);
|
|
80
|
+
const advanced = await ask("Change history folder, Pi executable, model, or thinking level? (yes/no)", "no", yesNo);
|
|
81
|
+
let { sessionDirectory, piCommand, model, thinkingLevel } = settings;
|
|
81
82
|
if (advanced) {
|
|
82
83
|
sessionDirectory = await ask("Pi session-history folder", sessionDirectory,
|
|
83
84
|
(value) => folder("sessionDirectory", value, false));
|
|
84
85
|
piCommand = await ask("Pi executable", piCommand, (value) => field("piCommand", value));
|
|
85
86
|
model = await ask("Model (provider/model), or 'default'", model || "default",
|
|
86
87
|
(value) => value.toLowerCase() === "default" ? undefined : field("model", value));
|
|
88
|
+
console.log("More thinking can help distinguish attempts from outcomes, but takes longer and may cost more. Support depends on the model.");
|
|
89
|
+
thinkingLevel = await ask("Thinking level (default/off/minimal/low/medium/high/xhigh/max)", thinkingLevel || "default",
|
|
90
|
+
(value) => field("thinkingLevel", value));
|
|
87
91
|
}
|
|
88
92
|
console.log("Summarizing sends selected, redacted history to your Pi model. Provider charges may apply.");
|
|
89
93
|
console.log("Common secrets are redacted, but this is not a complete secret scanner. Pi keeps your credentials.");
|
|
@@ -91,7 +95,7 @@ export async function configureLogDig(settings, commandName = "logdig") {
|
|
|
91
95
|
const autoCapture = await ask("Capture on Pi shutdown? (yes/no)", settings.autoCapture ? "yes" : "no", yesNo);
|
|
92
96
|
installExtension = await ask("Install the Pi extension for /journal? (yes/no)", "no", yesNo);
|
|
93
97
|
|
|
94
|
-
nextSettings = { ...settings, cacheDirectory, dailyDirectory, dailyHeader, dailySummary, timeZone, sessionDirectory, piCommand, model, autoCapture };
|
|
98
|
+
nextSettings = { ...settings, cacheDirectory, dailyDirectory, dailyHeader, dailySummary, timeZone, sessionDirectory, piCommand, model, thinkingLevel, autoCapture };
|
|
95
99
|
console.log([
|
|
96
100
|
"",
|
|
97
101
|
"Ready to save",
|
|
@@ -102,6 +106,7 @@ export async function configureLogDig(settings, commandName = "logdig") {
|
|
|
102
106
|
` History: ${sessionDirectory}`,
|
|
103
107
|
` Pi: ${piCommand}`,
|
|
104
108
|
` Model: ${model || "Pi default"}`,
|
|
109
|
+
` Thinking: ${thinkingLevel || "default (no LogDig override)"}`,
|
|
105
110
|
` Auto-capture: ${autoCapture ? "on" : "off"}`,
|
|
106
111
|
` Extension: ${installExtension ? "install now" : "leave unchanged"}`,
|
|
107
112
|
"Missing note and cache folders will be created on your first real backfill.",
|