Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/decay/src/pi-ext-debug.ts

Raw
// Generated by scripts/codegen.mjs. DO NOT EDIT.
// ============================================================================
// THIS FILE IS GENERATED. DO NOT EDIT IT DIRECTLY.
// Instead, edit scripts/codegen/templates/pi-ext-debug.ts.
// Then run: mise run //:codegen
// ============================================================================
import { randomBytes } from "node:crypto";
import {
	closeSync,
	fstatSync,
	lstatSync,
	mkdirSync,
	openSync,
	readFileSync,
	writeSync,
} from "node:fs";
import { homedir } from "node:os";
import { isAbsolute, join } from "node:path";

export type DebugDetails = Record<string, unknown>;
export type Debug = (event: string, details?: DebugDetails) => void;
export type SpanEnd = (
	outcome?: "finish" | "error",
	details?: DebugDetails,
) => void;
export type Span = (event: string, details?: DebugDetails) => SpanEnd;

export type DebugSchema = {
	events: readonly string[];
	spans: readonly string[];
	fields: Readonly<Record<string, "number" | "boolean" | readonly string[]>>;
};

type FieldValue<T> = T extends "number"
	? number
	: T extends "boolean"
		? boolean
		: T extends readonly (infer V)[]
			? V
			: never;
type BoundDetails<S extends DebugSchema> = Partial<{
	[K in keyof S["fields"]]: FieldValue<S["fields"][K]>;
}>;
type BoundDebug<S extends DebugSchema> = (
	event: S["events"][number],
	details?: BoundDetails<S>,
) => void;
type BoundSpan<S extends DebugSchema> = (
	event: S["spans"][number],
	details?: BoundDetails<S>,
) => (outcome?: "finish" | "error", details?: BoundDetails<S>) => void;

const extension = "decay";
const maxFileBytes = 8 * 1024 * 1024;
const maxMetadataFields = 24;
const extensionDirectory = extension.toUpperCase().replaceAll("-", "_");
const classification = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
const eventName = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
const metadataKey = /^[a-z][a-z0-9]{0,63}$/i;
const reservedFields = new Set([
	"timestamp",
	"extension",
	"pid",
	"event",
	"spanId",
	"durationMs",
	"terminal",
]);
const stringFields = new Set([
	"api",
	"arch",
	"field",
	"kind",
	"mode",
	"modelId",
	"platform",
	"provider",
	"selector",
	"spanId",
	"status",
	"stopReason",
	"subtype",
	"type",
]);
const numericFields = new Set([
	"attempt",
	"budget",
	"ceiling",
	"count",
	"durationMs",
	"generation",
	"index",
	"length",
	"messageCount",
	"retries",
	"timeoutMs",
]);

interface Writer {
	bytes: number;
	fd: number;
}

function userHome(): string {
	if (process.platform === "win32")
		return process.env.USERPROFILE ?? process.env.HOME ?? homedir();
	return process.env.HOME ?? homedir();
}

function globalDebugSelection(): boolean {
	try {
		const settings = JSON.parse(
			readFileSync(
				join(
					process.env.PI_CODING_AGENT_DIR || join(userHome(), ".pi", "agent"),
					"settings.json",
				),
				"utf8",
			),
		) as { "pi-ext"?: { debug?: unknown } };
		const debug = settings["pi-ext"]?.debug;
		return (
			debug === true ||
			debug === extension ||
			(Array.isArray(debug) &&
				debug.every((value) => typeof value === "string") &&
				debug.includes(extension))
		);
	} catch {
		return false;
	}
}

function isEnabled(): boolean {
	switch (process.env[`PI_${extensionDirectory}_DEBUG`]) {
		case "1":
			return true;
		case "0":
			return false;
		default:
			return globalDebugSelection();
	}
}

function debugRoot(): { base: string; parts: string[] } {
	if (process.platform === "darwin")
		return {
			base: join(userHome(), "Library", "Logs"),
			parts: ["pi-ext", extension],
		};
	if (process.platform === "win32")
		return {
			base: process.env.LOCALAPPDATA || join(userHome(), "AppData", "Local"),
			parts: ["pi-ext", "debug", extension],
		};
	return {
		base:
			process.env.XDG_STATE_HOME && isAbsolute(process.env.XDG_STATE_HOME)
				? process.env.XDG_STATE_HOME
				: join(userHome(), ".local", "state"),
		parts: ["pi-ext", "debug", extension],
	};
}

function ensurePrivateDirectory(): string {
	const { base, parts } = debugRoot();
	mkdirSync(base, { recursive: true, mode: 0o700 });
	let directory = base;
	for (const part of parts) {
		directory = join(directory, part);
		// Existing directories are the common case; probing first avoids a thrown EEXIST per level.
		let stat = lstatSync(directory, { throwIfNoEntry: false });
		if (!stat) {
			try {
				mkdirSync(directory, { mode: 0o700 });
			} catch (error) {
				if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
			}
			stat = lstatSync(directory);
		}
		if (
			!stat.isDirectory() ||
			stat.isSymbolicLink() ||
			(process.platform !== "win32" && (stat.mode & 0o077) !== 0)
		)
			throw new Error("debug directory is not private");
	}
	return directory;
}

function createWriter(): Writer | undefined {
	try {
		const directory = ensurePrivateDirectory();
		for (let attempt = 0; attempt < 3; attempt += 1) {
			const path = join(
				directory,
				`${new Date().toISOString().replace(/[.:]/g, "-")}-${process.pid}-${randomBytes(6).toString("hex")}.jsonl`,
			);
			try {
				const fd = openSync(path, "wx", 0o600);
				const stat = fstatSync(fd);
				if (
					!stat.isFile() ||
					(process.platform !== "win32" && (stat.mode & 0o077) !== 0)
				) {
					closeSync(fd);
					throw new Error("debug file is not private");
				}
				return { bytes: 0, fd };
			} catch {
				// Random suffixes make collisions improbable; debug failures stay silent.
			}
		}
	} catch {
		// Debug logging must never affect extension work or Pi output.
	}
	return undefined;
}

function safeDetails(details: DebugDetails): DebugDetails {
	const safe: DebugDetails = {};
	for (const [key, value] of Object.entries(details)) {
		if (Object.keys(safe).length >= maxMetadataFields) break;
		if (!metadataKey.test(key)) continue;
		if (typeof value === "boolean") {
			safe[key] = value;
			continue;
		}
		if (
			typeof value === "number" &&
			Number.isFinite(value) &&
			Math.abs(value) <= Number.MAX_SAFE_INTEGER &&
			numericFields.has(key)
		) {
			safe[key] = value;
			continue;
		}
		if (
			typeof value === "string" &&
			stringFields.has(key) &&
			classification.test(value)
		)
			safe[key] = value;
	}
	return safe;
}

type FieldRule = "number" | "boolean" | Set<string>;

function compileSchema(schema: DebugSchema):
	| {
			events: Set<string>;
			spans: Set<string>;
			fields: Map<string, FieldRule>;
	  }
	| undefined {
	try {
		if (!Array.isArray(schema.events) || !Array.isArray(schema.spans))
			return undefined;
		const events = new Set<string>();
		const spans = new Set<string>();
		for (const name of schema.events) {
			if (
				typeof name !== "string" ||
				!eventName.test(name) ||
				name.startsWith("debug.")
			)
				return undefined;
			events.add(name);
		}
		for (const name of schema.spans) {
			if (
				typeof name !== "string" ||
				!eventName.test(`${name}.finish`) ||
				name.startsWith("debug.")
			)
				return undefined;
			spans.add(name);
		}
		const fields = new Map<string, FieldRule>();
		if (!schema.fields || typeof schema.fields !== "object") return undefined;
		for (const [key, rule] of Object.entries(schema.fields)) {
			if (!metadataKey.test(key) || reservedFields.has(key)) return undefined;
			if (rule === "number" || rule === "boolean") fields.set(key, rule);
			else if (
				Array.isArray(rule) &&
				rule.length > 0 &&
				rule.every(
					(value) => typeof value === "string" && classification.test(value),
				)
			)
				fields.set(key, new Set(rule));
			else return undefined;
		}
		return { events, spans, fields };
	} catch {
		return undefined;
	}
}

function boundDetails(
	details: DebugDetails | undefined,
	fields: Map<string, FieldRule>,
): DebugDetails {
	const safe: DebugDetails = {};
	if (!details || typeof details !== "object") return safe;
	for (const [key, rule] of fields) {
		if (Object.keys(safe).length >= maxMetadataFields) break;
		const descriptor = Object.getOwnPropertyDescriptor(details, key);
		if (!descriptor || !("value" in descriptor)) continue;
		const value = descriptor.value;
		if (rule === "boolean" && typeof value === "boolean") safe[key] = value;
		else if (
			rule === "number" &&
			typeof value === "number" &&
			Number.isFinite(value) &&
			Math.abs(value) <= Number.MAX_SAFE_INTEGER
		)
			safe[key] = value;
		else if (
			rule instanceof Set &&
			typeof value === "string" &&
			rule.has(value)
		)
			safe[key] = value;
	}
	return safe;
}

function write(writer: Writer, bytes: Buffer): void {
	let offset = 0;
	while (offset < bytes.length) {
		const written = writeSync(writer.fd, bytes, offset, bytes.length - offset);
		if (written <= 0) throw new Error("debug write made no progress");
		offset += written;
	}
	writer.bytes += bytes.length;
}

function record(
	event: string,
	details: DebugDetails = {},
	safeDetailsOverride?: DebugDetails,
): void {
	if (stopped) return;
	writer ??= createWriter();
	if (!writer) {
		stopped = true;
		return;
	}
	try {
		const timestamp = new Date().toISOString();
		const safe = safeDetailsOverride ?? safeDetails(details);
		const line = Buffer.from(
			`${JSON.stringify({
				...safe,
				timestamp,
				extension,
				pid: process.pid,
				event: eventName.test(event) ? event : "debug.invalid_event",
			})}\n`,
			"utf8",
		);
		if (writer.bytes + line.length + terminalBytes > maxFileBytes) {
			if (writer.bytes + terminalBytes <= maxFileBytes)
				write(writer, terminalRecord(timestamp));
			stop();
			return;
		}
		write(writer, line);
	} catch {
		stop();
	}
}

function terminalRecord(timestamp: string): Buffer {
	return Buffer.from(
		`${JSON.stringify({
			timestamp,
			extension,
			pid: process.pid,
			event: "debug.limit",
			terminal: true,
		})}\n`,
		"utf8",
	);
}

// ISO timestamps have a fixed width, so the terminal record size is constant per process.
const terminalBytes = terminalRecord(new Date(0).toISOString()).length;

function closeWriter(): void {
	if (writer) {
		try {
			closeSync(writer.fd);
		} catch {
			// Debug failures never affect extension work.
		}
		writer = undefined;
	}
}

function stop(): void {
	stopped = true;
	closeWriter();
}

export const closeDebug = closeWriter;

/** Bind extension-owned event and metadata declarations to the local writer. */
export function defineDebug<const S extends DebugSchema>(
	schema: S,
): {
	dbg: BoundDebug<S> | undefined;
	span: BoundSpan<S> | undefined;
	closeDebug: typeof closeDebug;
} {
	if (!dbg) return { dbg: undefined, span: undefined, closeDebug };
	const compiled = compileSchema(schema);
	if (!compiled) return { dbg: undefined, span: undefined, closeDebug };
	const boundDbg: BoundDebug<S> = (event, details) => {
		if (!compiled.events.has(event)) return;
		try {
			record(event, {}, boundDetails(details, compiled.fields));
		} catch {
			// Bad metadata must not affect extension work or leak into diagnostics.
		}
	};
	const boundSpan: BoundSpan<S> = (event, details) => {
		if (!compiled.spans.has(event)) return () => {};
		const spanId = `${process.pid}-${++nextSpanId}`;
		const started = performance.now();
		try {
			record(
				`${event}.start`,
				{},
				{
					...boundDetails(details, compiled.fields),
					spanId,
				},
			);
		} catch {
			// Bad metadata must not affect extension work or leak into diagnostics.
		}
		let completed = false;
		return (outcome = "finish", finishDetails) => {
			if (completed) return;
			completed = true;
			if (outcome !== "finish" && outcome !== "error") return;
			try {
				record(
					`${event}.${outcome}`,
					{},
					{
						...boundDetails(finishDetails, compiled.fields),
						spanId,
						durationMs: Math.max(0, performance.now() - started),
					},
				);
			} catch {
				// Bad metadata must not affect extension work or leak into diagnostics.
			}
		};
	};
	return { dbg: boundDbg, span: boundSpan, closeDebug };
}

let writer: Writer | undefined;
let stopped = false;
let nextSpanId = 0;

/** Metadata-only extension tracing, selected through user settings or PI_<SLUG>_DEBUG. */
export const dbg: Debug | undefined = isEnabled() ? record : undefined;

/** Monotonic operation spans. Call through optional chaining to avoid disabled work. */
export const span: Span | undefined = dbg
	? (event, details = {}) => {
			const spanId = `${process.pid}-${++nextSpanId}`;
			const started = performance.now();
			record(`${event}.start`, { ...details, spanId });
			let completed = false;
			return (outcome = "finish", finishDetails = {}) => {
				if (completed) return;
				completed = true;
				record(`${event}.${outcome}`, {
					...finishDetails,
					spanId,
					durationMs: Math.max(0, performance.now() - started),
				});
			};
		}
	: undefined;