/** * KHAELOR * File: src/workspace/file-times.ts * Description: FileTimeRegistry โ€” read-before-write and external-modification detection (TOOL_PROTOCOL ยง1.4). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { createHash } from "node:crypto"; import { statSync } from "node:fs"; import * as path from "node:path"; /** Freshness of a file relative to what the agent last saw. */ export type FileFreshness = "unread" | "clean" | "externally-modified"; /** Session-scoped record of a file as last read or written by the agent. */ export interface FileStamp { /** Absolute, resolved path. */ path: string; mtimeMs: number; size: number; /** Of content as read/written. */ sha256: string; /** Event timestamp. */ at: number; } /** * Session-scoped record of every file the agent has read or written. * `write`/`edit` enforce: existing files must have been read this session * before modification, and must not have changed externally since * (Absolute Rule #5). Rebuilt on resume by replaying FileRead/FileModified. */ export interface FileTimeRegistry { /** Called by read/write/edit on success. */ stamp(path: string, content: string): void; get(path: string): FileStamp | undefined; /** "unread" | "clean" | "externally-modified" */ check(path: string, currentContent: string): FileFreshness; } function sha256Of(content: string): string { return createHash("sha256").update(content, "utf8").digest("hex"); } /** In-memory FileTimeRegistry โ€” one instance per session. */ export class InMemoryFileTimeRegistry implements FileTimeRegistry { private readonly stamps = new Map(); stamp(p: string, content: string): void { const abs = path.resolve(p); const at = Date.now(); let mtimeMs = at; try { mtimeMs = statSync(abs).mtimeMs; } catch { // File vanished between operation and stamp โ€” keep the event timestamp. } this.stamps.set(abs, { path: abs, mtimeMs, size: Buffer.byteLength(content, "utf8"), sha256: sha256Of(content), at, }); } get(p: string): FileStamp | undefined { return this.stamps.get(path.resolve(p)); } check(p: string, currentContent: string): FileFreshness { const stamp = this.stamps.get(path.resolve(p)); if (stamp === undefined) return "unread"; return sha256Of(currentContent) === stamp.sha256 ? "clean" : "externally-modified"; } }