Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions src/hooks/pre-read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { count: number; tokens: number; first_read: string }>;
files_read: Record<string, FileReadEntry>;
anatomy_hits: number;
anatomy_misses: number;
repeated_reads_warned: number;
Expand Down Expand Up @@ -51,13 +52,20 @@ async function main(): Promise<void> {
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);
Expand Down Expand Up @@ -99,11 +107,7 @@ async function main(): Promise<void> {
}

// 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);
Expand Down
43 changes: 43 additions & 0 deletions src/hooks/read-tracking.ts
Original file line number Diff line number Diff line change
@@ -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,
},
};
}
57 changes: 57 additions & 0 deletions tests/pre-read.test.ts
Original file line number Diff line number Diff line change
@@ -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 });
});