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/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 = "session-layers-v2";
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 session (${session.project}). The session may span multiple local dates in ${session.timezone}.`,
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 = note.sessionUsage || formatUsage({ ...note, ...metrics });
255
- if (usage && !note.sessionUsage) header += `sessionUsage: ${JSON.stringify(usage)}\n`;
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 cacheParts = [SUMMARY_VERSION, session.sourceFingerprint, session.timezone];
313
- if (modelClient.cacheKey && modelClient.cacheKey !== "Pi default") cacheParts.push(modelClient.cacheKey);
314
- const cacheFingerprint = hashValue(cacheParts);
315
- const sessionPath = path.join(cacheDirectory, "Sessions", `${safeFileName(session.header.id)}.md`);
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
- const enriched = enrichSessionNote(markdown, session.sourceFingerprint, sessionMetrics(session));
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 markdown = renderSessionNote(session, summary, cacheFingerprint, modelClient.modelLabel || "Pi default", logMetrics);
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 { sessionPath, summary, cacheFingerprint, reused: false };
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, projectEntries.length === 0);
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
- return hashValue([entry.sessionId, entry.cacheFingerprint, entry.summaryLevel, heading]);
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, inline) {
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 summary = entry.summaryOverride ?? formatDailySummary(entry.summary);
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 inline ? `${timestamp}: ${summary}` : `${timestamp}\n\n${summary}`;
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.displayedSummary.trim(),
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 updated = oldBlocks.length > 0 || sameIdBlocks.length > 1 || (!present && sameIdBlocks.length > 0);
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.displayedSummary.trim())
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, false);
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 ${MODEL_TIMEOUT_MS / 1000} seconds`));
58
- }, MODEL_TIMEOUT_MS);
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
- cacheKey: settings.model || "Pi default",
127
+ cachePolicy: summaryCachePolicy(settings),
125
128
  complete: (prompt) => runPiPrompt(prompt, settings, options.spawnProcess),
126
129
  };
127
130
  }
@@ -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 { eventsForSession, fingerprintSession, sessionFromJsonl, sessionMetrics } from "./transcript.js";
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
- const events = eventsForSession(session, settings.timeZone);
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
- const advanced = await ask("Change history folder, Pi executable, or model? (yes/no)", "no", yesNo);
80
- let { sessionDirectory, piCommand, model } = settings;
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.",