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
13 changes: 11 additions & 2 deletions src/hooks/post-write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const NON_CODE_EXTS = new Set([
]);

interface SessionData {
files_read: Record<string, { count: number; tokens: number; first_read: string; read_mtime?: number }>;
files_written: Array<{ file: string; action: string; tokens: number; at: string }>;
edit_counts: Record<string, number>;
[key: string]: unknown;
Expand Down Expand Up @@ -157,10 +158,14 @@ async function main(): Promise<void> {
appendMarkdown(memoryPath, `| ${timeShort()} | ${action} ${relFile} | ${outcome} | ~${writeTokens} |\n`);
} catch {}

// 3. Record in session tracker + track edit counts
// 3. Record in session tracker + track edit counts.
// Also clear the files_read entry for the written file — the file's
// content just changed, so any cached read_mtime is now stale and a
// subsequent re-read of this file is legitimate (not a repeated read).
try {
const session = readJSON<SessionData>(sessionFile, { files_written: [], edit_counts: {} });
const session = readJSON<SessionData>(sessionFile, { files_read: {}, files_written: [], edit_counts: {} });
if (!session.edit_counts) session.edit_counts = {};
if (!session.files_read) session.files_read = {};

const normalizedFile = normalizePath(filePath);
const action = toolName === "Write" ? "create" : "edit";
Expand All @@ -177,6 +182,10 @@ async function main(): Promise<void> {
const editKey = normalizePath(path.relative(projectRoot, absolutePath));
session.edit_counts[editKey] = (session.edit_counts[editKey] || 0) + 1;

// Invalidate the read cache for this file so the next read is not
// incorrectly flagged as a repeated read (the content just changed).
delete session.files_read[normalizedFile];

writeJSON(sessionFile, session);

if (session.edit_counts[editKey] >= 3) {
Expand Down
52 changes: 42 additions & 10 deletions src/hooks/pre-read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { lookupEntry } from "./anatomy-store.js";

interface SessionData {
session_id: string;
files_read: Record<string, { count: number; tokens: number; first_read: string }>;
files_read: Record<string, { count: number; tokens: number; first_read: string; read_mtime?: number }>;
anatomy_hits: number;
anatomy_misses: number;
repeated_reads_warned: number;
Expand Down Expand Up @@ -54,14 +54,36 @@ async function main(): Promise<void> {
// Check if already read this session
if (session.files_read[normalizedFile]) {
const prev = session.files_read[normalizedFile];
process.stderr.write(
`⚡ OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tokens). Consider using your existing knowledge of this file.\n`
);
session.files_read[normalizedFile].count++;
session.repeated_reads_warned++;
writeJSON(sessionFile, session);
process.exit(0);
return;

// Compare current mtime against stored read_mtime — if the file was
// modified since the last read (by Claude or the user externally),
// the re-read is legitimate: clear the stale entry and fall through.
let fileChanged = false;
if (prev.read_mtime !== undefined) {
try {
const currentMtime = fs.statSync(normalizedFile).mtimeMs;
if (currentMtime > prev.read_mtime) {
fileChanged = true;
}
} catch {
// statSync failed (file deleted, permission error, etc.);
// treat as unchanged so we keep the existing warning behaviour.
}
}

if (!fileChanged) {
process.stderr.write(
`⚡ OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tokens). Consider using your existing knowledge of this file.\n`
);
session.files_read[normalizedFile].count++;
session.repeated_reads_warned++;
writeJSON(sessionFile, session);
process.exit(0);
return;
}

// File was modified — reset entry so this read is treated as fresh.
delete session.files_read[normalizedFile];
}

// Anatomy lookup: O(1) against the durable store, legacy md scan fallback.
Expand Down Expand Up @@ -98,11 +120,21 @@ async function main(): Promise<void> {
session.anatomy_misses++;
}

// Record initial read entry (tokens will be updated in post-read)
// Record initial read entry (tokens will be updated in post-read).
// Capture mtime now so future re-reads can detect if the file changed.
let readMtime: number | undefined;
try {
readMtime = fs.statSync(normalizedFile).mtimeMs;
} catch {
// File may not exist yet (e.g. read of a path about to be created);
// leave read_mtime undefined so mtime comparison is skipped next time.
}

session.files_read[normalizedFile] = {
count: 1,
tokens: 0,
first_read: new Date().toISOString(),
read_mtime: readMtime,
};

writeJSON(sessionFile, session);
Expand Down