import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { fauxAssistantMessage, fauxProvider, fauxToolCall, } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createTestSession, sandboxEnv, type TestSession, withProcessEnv, } from "../../../test/harness"; describe("decay debug", () => { let root: string | undefined; let session: TestSession | undefined; afterEach(() => { session?.dispose(); if (root) rmSync(root, { recursive: true, force: true }); session = undefined; root = undefined; }); it("writes its lifecycle JSONL through the standalone factory", async () => { root = mkdtempSync(path.join(tmpdir(), "pi-ext-decay-debug-")); const env = sandboxEnv(root, { env: { PI_DECAY_DEBUG: "1" } }); vi.resetModules(); const factory = await withProcessEnv( env, async () => (await import("../index.ts")).default, ); session = await createTestSession({ cwd: root, extensionFactories: [factory], env: { PI_DECAY_DEBUG: "1" }, }); await session.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); await session.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); if (!env.XDG_STATE_HOME) throw new Error("missing sandbox state home"); const directory = path.join(env.XDG_STATE_HOME, "pi-ext", "debug", "decay"); const [file] = readdirSync(directory); const events = readFileSync(path.join(directory, file), "utf8") .trim() .split("\n") .map((line) => JSON.parse(line).event); expect(events).toEqual( expect.arrayContaining(["session.start", "session.shutdown"]), ); }); it("logs bounded shard, retry, allocation, and model usage metadata without source content", async () => { root = mkdtempSync(path.join(tmpdir(), "pi-ext-decay-debug-")); const env = sandboxEnv(root, { env: { PI_DECAY_DEBUG: "1" } }); vi.resetModules(); const factory = await withProcessEnv( env, async () => (await import("../index.ts")).default, ); session = await createTestSession({ cwd: root, extensionFactories: [factory], env: { PI_DECAY_DEBUG: "1" }, }); const faux = fauxProvider({ provider: "openai", models: [{ id: "gpt-4o", maxTokens: 16_384 }], }); session.session.extensionRunner .createContext() .modelRegistry.registerProvider(faux.provider); const first = fauxToolCall("record_chunk", { chunk: 1, of: 2, atoms: [ { key: "SECRET_KEY", text: "SECRET_ATOM", kind: "state", status: "active", }, ], }); const second = fauxToolCall("record_chunk", { chunk: 2, of: 2, atoms: [ { key: "second", text: "Done", kind: "decision", status: "active" }, ], }); faux.setResponses([ fauxAssistantMessage(first, { stopReason: "toolUse" }), fauxAssistantMessage(second, { stopReason: "length" }), fauxAssistantMessage(second, { stopReason: "toolUse" }), fauxAssistantMessage("## Goal\n\nSECRET_SUMMARY"), ]); const event = { type: "session_before_compact" as const, reason: "manual" as const, willRetry: false, signal: new AbortController().signal, customInstructions: "SECRET_INSTRUCTION", branchEntries: [], preparation: { firstKeptEntryId: "kept", messagesToSummarize: [ { role: "user", content: "SECRET_USER_ONE", timestamp: 1 }, { role: "assistant", content: [{ type: "text", text: "done" }], timestamp: 2, }, { role: "user", content: "SECRET_USER_TWO", timestamp: 3 }, ] as never, turnPrefixMessages: [], isSplitTurn: false, tokensBefore: 42_000, previousSummary: undefined, fileOps: { read: new Set(["SECRET_PATH"]), written: new Set(), edited: new Set(), }, settings: { enabled: true, reserveTokens: 16_384, keepRecentTokens: 20_000, }, }, }; const result = await session.session.extensionRunner.emit(event); expect(result.compaction.summary).toContain("SECRET_SUMMARY"); faux.setResponses([ fauxAssistantMessage([], { stopReason: "error", errorMessage: "SECRET_PROVIDER_ERROR", }), ]); expect(await session.session.extensionRunner.emit(event)).toBeUndefined(); await session.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); if (!env.XDG_STATE_HOME) throw new Error("missing sandbox state home"); const directory = path.join(env.XDG_STATE_HOME, "pi-ext", "debug", "decay"); const text = readdirSync(directory) .map((file) => readFileSync(path.join(directory, file), "utf8")) .join("\n"); expect(text).not.toMatch( /SECRET_(USER|INSTRUCTION|ATOM|KEY|PATH|SUMMARY|PROVIDER)/, ); expect(text).not.toContain("gpt-4o"); const entries = text .trim() .split("\n") .filter(Boolean) .map((line) => JSON.parse(line)); expect( entries.find((entry) => entry.event === "compaction.plan"), ).toMatchObject({ runIndex: 1, chunkCount: 2, previousAtoms: 0, tokensBefore: 42_000, }); const starts = entries.filter( (entry) => entry.event === "classifier.request.start" && entry.runIndex === 1, ); const finishes = entries.filter( (entry) => entry.event === "classifier.request.finish" && entry.runIndex === 1, ); const errors = entries.filter( (entry) => entry.event === "classifier.request.error" && entry.runIndex === 1, ); expect(starts).toHaveLength(3); expect(starts[0]).toMatchObject({ catalogAvailableAtoms: 0, catalogAtoms: 0, catalogChars: 2, }); expect(starts[1]).toMatchObject({ catalogAvailableAtoms: 1, catalogAtoms: 1, catalogChars: expect.any(Number), }); expect(finishes).toHaveLength(2); expect(errors).toHaveLength(1); expect(errors[0]).toMatchObject({ runIndex: 1, chunkIndex: 2, attempt: 1, requestResult: "output_limit", validRecords: 1, committedRecords: 0, stopReason: "length", }); expect(finishes[0]).toEqual( expect.objectContaining({ requestResult: "accepted", recordAtoms: 1, reportedInputTokens: expect.any(Number), reportedCost: expect.any(Number), }), ); expect(starts[0].spanId).toBe(finishes[0].spanId); expect(starts[1].spanId).toBe(errors[0].spanId); expect(entries).toEqual( expect.arrayContaining([ expect.objectContaining({ event: "classifier.decision", decision: "retry", chunkIndex: 2, }), expect.objectContaining({ event: "compaction.memory", memoryAtoms: 2, activeAtoms: 2, atomStates: 1, atomDecisions: 1, }), expect.objectContaining({ event: "summarizer.request.finish", requestResult: "accepted", }), expect.objectContaining({ event: "compaction.outcome", outcome: "success", }), expect.objectContaining({ event: "classifier.request.error", runIndex: 2, requestResult: "provider_error", }), expect.objectContaining({ event: "compaction.outcome", runIndex: 2, outcome: "fallback", stage: "classifier", }), ]), ); }); });