SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
2.4 KB · 79 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: src/workspace/file-times.ts4 * Description: FileTimeRegistry — read-before-write and external-modification detection (TOOL_PROTOCOL §1.4).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { createHash } from "node:crypto";11import { statSync } from "node:fs";12import * as path from "node:path";1314/** Freshness of a file relative to what the agent last saw. */15export type FileFreshness = "unread" | "clean" | "externally-modified";1617/** Session-scoped record of a file as last read or written by the agent. */18export interface FileStamp {19  /** Absolute, resolved path. */20  path: string;21  mtimeMs: number;22  size: number;23  /** Of content as read/written. */24  sha256: string;25  /** Event timestamp. */26  at: number;27}2829/**30 * Session-scoped record of every file the agent has read or written.31 * `write`/`edit` enforce: existing files must have been read this session32 * before modification, and must not have changed externally since33 * (Absolute Rule #5). Rebuilt on resume by replaying FileRead/FileModified.34 */35export interface FileTimeRegistry {36  /** Called by read/write/edit on success. */37  stamp(path: string, content: string): void;38  get(path: string): FileStamp | undefined;39  /** "unread" | "clean" | "externally-modified" */40  check(path: string, currentContent: string): FileFreshness;41}4243function sha256Of(content: string): string {44  return createHash("sha256").update(content, "utf8").digest("hex");45}4647/** In-memory FileTimeRegistry — one instance per session. */48export class InMemoryFileTimeRegistry implements FileTimeRegistry {49  private readonly stamps = new Map<string, FileStamp>();5051  stamp(p: string, content: string): void {52    const abs = path.resolve(p);53    const at = Date.now();54    let mtimeMs = at;55    try {56      mtimeMs = statSync(abs).mtimeMs;57    } catch {58      // File vanished between operation and stamp — keep the event timestamp.59    }60    this.stamps.set(abs, {61      path: abs,62      mtimeMs,63      size: Buffer.byteLength(content, "utf8"),64      sha256: sha256Of(content),65      at,66    });67  }6869  get(p: string): FileStamp | undefined {70    return this.stamps.get(path.resolve(p));71  }7273  check(p: string, currentContent: string): FileFreshness {74    const stamp = this.stamps.get(path.resolve(p));75    if (stamp === undefined) return "unread";76    return sha256Of(currentContent) === stamp.sha256 ? "clean" : "externally-modified";77  }78}79