import { appendFile, mkdir, open, stat } from "node:fs/promises"; import { homedir } from "node:os"; import { basename, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; const LOG_LIMIT_BYTES = 1024 * 1024; const MARKER_RESERVE_BYTES = 512; const VALUE_LIMIT = 12_000; function cacheRoot(): string { if (process.platform === "win32") return process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"); if (process.platform === "darwin") return join(homedir(), "Library", "Caches"); return process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"); } function bounded(value: unknown): unknown { let encoded: string; try { encoded = JSON.stringify(value); } catch { encoded = String(value); } return encoded.length <= VALUE_LIMIT ? value : { truncated: true, preview: encoded.slice(0, VALUE_LIMIT) }; } export function dynamicExtensionRevision(moduleUrl: string): string { return basename(dirname(fileURLToPath(moduleUrl))); } export function dynamicExtensionLogPath( sessionId: string, extensionName: string, ): string { return join( cacheRoot(), "pi", "ultra", "dynamic-extensions", "logs", sessionId, `${extensionName}.jsonl`, ); } async function hasTruncationMarker(path: string, size: number): Promise { if (size === 0) return false; const handle = await open(path, "r"); try { const length = Math.min(size, 1024); const bytes = Buffer.alloc(length); await handle.read(bytes, 0, length, size - length); return bytes.toString("utf8").includes('"event":"log_truncated"'); } finally { await handle.close(); } } export async function logDynamicExtensionEvent(args: { name: string; revision: string; ctx: ExtensionContext; event: string; durationMs?: number; context?: unknown; }): Promise { const sessionId = args.ctx.sessionManager.getSessionId(); const path = dynamicExtensionLogPath(sessionId, args.name); await mkdir(dirname(path), { recursive: true }); const size = await stat(path).then((value) => value.size, () => 0); if (size >= LOG_LIMIT_BYTES) return; const record = { timestamp: new Date().toISOString(), extension: { name: args.name, revision: args.revision }, childSessionId: sessionId, event: args.event, ...(args.durationMs === undefined ? {} : { durationMs: args.durationMs }), ...(args.context === undefined ? {} : { context: bounded(args.context) }), }; const line = `${JSON.stringify(record)}\n`; if ( size < LOG_LIMIT_BYTES - MARKER_RESERVE_BYTES && size + Buffer.byteLength(line) <= LOG_LIMIT_BYTES - MARKER_RESERVE_BYTES ) { await appendFile(path, line, "utf8"); return; } if (await hasTruncationMarker(path, size)) return; const marker = { timestamp: new Date().toISOString(), extension: { name: args.name, revision: args.revision }, childSessionId: sessionId, event: "log_truncated", context: { limitBytes: LOG_LIMIT_BYTES }, }; await appendFile(path, `${JSON.stringify(marker)}\n`, "utf8"); }