diff --git a/src/hooks/pre-read.ts b/src/hooks/pre-read.ts index 335b140..988c7e5 100644 --- a/src/hooks/pre-read.ts +++ b/src/hooks/pre-read.ts @@ -5,10 +5,11 @@ import { estimateTokens, readStdin, normalizePath, getProjectDir } from "./shared.js"; import { lookupEntry } from "./anatomy-store.js"; +import { trackRead, type FileReadEntry } from "./read-tracking.js"; interface SessionData { session_id: string; - files_read: Record; + files_read: Record; anatomy_hits: number; anatomy_misses: number; repeated_reads_warned: number; @@ -51,13 +52,20 @@ async function main(): Promise { repeated_reads_warned: 0, }); - // Check if already read this session - if (session.files_read[normalizedFile]) { - const prev = session.files_read[normalizedFile]; + let currentMtime: number | undefined; + try { + const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath); + currentMtime = fs.statSync(absolutePath).mtimeMs; + } catch {} + + const previous = session.files_read[normalizedFile]; + const read = trackRead(previous, currentMtime, new Date().toISOString()); + + if (previous && read.repeated) { process.stderr.write( - `⚡ OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tokens). Consider using your existing knowledge of this file.\n` + `⚡ OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${previous.tokens} tokens). Consider using your existing knowledge of this file.\n` ); - session.files_read[normalizedFile].count++; + session.files_read[normalizedFile] = read.entry; session.repeated_reads_warned++; writeJSON(sessionFile, session); process.exit(0); @@ -99,11 +107,7 @@ async function main(): Promise { } // Record initial read entry (tokens will be updated in post-read) - session.files_read[normalizedFile] = { - count: 1, - tokens: 0, - first_read: new Date().toISOString(), - }; + session.files_read[normalizedFile] = read.entry; writeJSON(sessionFile, session); process.exit(0); diff --git a/src/hooks/read-tracking.ts b/src/hooks/read-tracking.ts new file mode 100644 index 0000000..d96cfd3 --- /dev/null +++ b/src/hooks/read-tracking.ts @@ -0,0 +1,43 @@ +export interface FileReadEntry { + count: number; + tokens: number; + first_read: string; + read_mtime?: number; +} + +interface ReadTransition { + entry: FileReadEntry; + repeated: boolean; +} + +export function trackRead( + previous: FileReadEntry | undefined, + currentMtime: number | undefined, + readAt: string +): ReadTransition { + const changed = + previous?.read_mtime !== undefined && + currentMtime !== undefined && + previous.read_mtime !== currentMtime; + + if (previous && !changed) { + return { + repeated: true, + entry: { + ...previous, + count: previous.count + 1, + read_mtime: previous.read_mtime ?? currentMtime, + }, + }; + } + + return { + repeated: false, + entry: { + count: 1, + tokens: 0, + first_read: readAt, + read_mtime: currentMtime, + }, + }; +} diff --git a/tests/pre-read.test.ts b/tests/pre-read.test.ts new file mode 100644 index 0000000..70c018b --- /dev/null +++ b/tests/pre-read.test.ts @@ -0,0 +1,57 @@ +import { test } from "node:test"; +import * as assert from "node:assert"; + +import { trackRead, type FileReadEntry } from "../src/hooks/read-tracking.ts"; + +const firstRead: FileReadEntry = { + count: 1, + tokens: 42, + first_read: "2026-07-15T00:00:00.000Z", + read_mtime: 1_700_000_000_000, +}; + +test("starts a new read entry with the current mtime", () => { + const result = trackRead(undefined, 1_700_000_000_000, "2026-07-15T00:00:00.000Z"); + + assert.strictEqual(result.repeated, false); + assert.deepStrictEqual(result.entry, { + count: 1, + tokens: 0, + first_read: "2026-07-15T00:00:00.000Z", + read_mtime: 1_700_000_000_000, + }); +}); + +test("warns and preserves the baseline when the mtime is unchanged", () => { + const result = trackRead(firstRead, firstRead.read_mtime, "2026-07-15T00:01:00.000Z"); + + assert.strictEqual(result.repeated, true); + assert.deepStrictEqual(result.entry, { ...firstRead, count: 2 }); +}); + +test("starts a fresh baseline when the mtime changed", () => { + const result = trackRead(firstRead, 1_700_000_100_000, "2026-07-15T00:01:00.000Z"); + + assert.strictEqual(result.repeated, false); + assert.deepStrictEqual(result.entry, { + count: 1, + tokens: 0, + first_read: "2026-07-15T00:01:00.000Z", + read_mtime: 1_700_000_100_000, + }); +}); + +test("warns for a legacy entry and records its current mtime", () => { + const { read_mtime: _readMtime, ...legacyEntry } = firstRead; + const result = trackRead(legacyEntry, 1_700_000_000_000, "2026-07-15T00:01:00.000Z"); + + assert.strictEqual(result.repeated, true); + assert.deepStrictEqual(result.entry, { ...firstRead, count: 2 }); +}); + +test("warns conservatively when the current mtime is unavailable", () => { + const result = trackRead(firstRead, undefined, "2026-07-15T00:01:00.000Z"); + + assert.strictEqual(result.repeated, true); + assert.deepStrictEqual(result.entry, { ...firstRead, count: 2 }); +});