import assert from "node:assert/strict"; import { appendFileSync, closeSync, mkdirSync, mkdtempSync, openSync, renameSync, rmSync, statSync, truncateSync, unlinkSync, utimesSync, writeFileSync, writeSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { createJiti } from "jiti"; import fs from "node:fs"; import { syncBuiltinESMExports } from "node:module"; const jiti = createJiti(import.meta.url); const { sessionPathKey } = await jiti.import("./paths.ts"); const { forEachFileLineSync, serializeTitleSlot } = await jiti.import("./omp/session-files.ts"); const { SessionFileTooLargeError } = await jiti.import("./session-reader.ts"); const { subscribeSessionFileChanges } = await jiti.import("./session-watcher.ts"); const { buildSessionContext, cacheSessionPath, getSessionEntries, getSessionEntriesForDisplay, getTodoPhasesFromEntries, getSessionHistoryPage, getSessionContextBoundary, invalidateSessionCaches, getSessionEntriesForDisplayAsync, invalidateSessionListCache, invalidateSessionListMeta, invalidateSessionPathCache, listAllSessions, readSessionHeader, resolveSessionIdByPath, resolveSessionPath, } = await jiti.import("./session-reader.ts"); function userEntry(id, parentId, content, timestamp = "2026-01-01T00:00:00.000Z") { return { type: "message", id, parentId, timestamp, message: { role: "user", content, }, }; } function assistantEntry(id, parentId, text, timestamp = "2026-01-01T00:00:00.000Z") { return { type: "message", id, parentId, timestamp, message: { role: "assistant", provider: "test", model: "test-model", content: [{ type: "text", text }], }, }; } test("renders the SDK compaction-aware context with aligned entry IDs", () => { const entries = [ userEntry("u1", null, "old user request"), assistantEntry("a1", "u1", "old assistant answer"), userEntry("u2", "a1", "kept user request"), { type: "compaction", id: "cmp", parentId: "u2", timestamp: "2026-01-01T00:00:03.000Z", summary: "old exchange summary", firstKeptEntryId: "u2", tokensBefore: 123, }, userEntry("u3", "cmp", "after compaction"), ]; const context = buildSessionContext(entries); assert.deepEqual(context.entryIds, ["cmp", "u2", "u3"]); assert.deepEqual( context.messages.map((message) => [message.role, message.customType, message.content]), [ ["custom", "compaction", "old exchange summary"], ["user", undefined, "kept user request"], ["user", undefined, "after compaction"], ], ); }); test("can expose pre-compaction entries for read-only transcript browsing", () => { const entries = [ userEntry("u1", null, "old user request"), assistantEntry("a1", "u1", "old assistant answer"), userEntry("u2", "a1", "kept user request"), { type: "compaction", id: "cmp", parentId: "u2", timestamp: "2026-01-01T00:00:03.000Z", summary: "old exchange summary", firstKeptEntryId: "u2", tokensBefore: 123, }, userEntry("u3", "cmp", "after compaction"), ]; const context = buildSessionContext(entries, undefined, { includePreCompaction: true }); assert.deepEqual(context.entryIds, ["u1", "a1", "u2", "cmp", "u3"]); assert.deepEqual( context.messages.map((message) => message.role === "custom" ? message.customType : message.content), ["old user request", [{ type: "text", text: "old assistant answer" }], "kept user request", "compaction", "after compaction"], ); }); test("uses only the latest compaction on the active path", () => { const entries = [ userEntry("u1", null, "old request"), assistantEntry("a1", "u1", "old answer"), userEntry("u2", "a1", "first kept request"), { type: "compaction", id: "cmp1", parentId: "u2", timestamp: "2026-01-01T00:00:03.000Z", summary: "first summary", firstKeptEntryId: "u2", tokensBefore: 100, }, assistantEntry("a2", "cmp1", "second kept answer"), userEntry("u3", "a2", "second kept request"), { type: "compaction", id: "cmp2", parentId: "u3", timestamp: "2026-01-01T00:00:06.000Z", summary: "latest summary", firstKeptEntryId: "a2", tokensBefore: 200, }, assistantEntry("a3", "cmp2", "latest answer"), ]; const context = buildSessionContext(entries); assert.deepEqual(context.entryIds, ["cmp2", "a2", "u3", "a3"]); assert.equal(context.messages[0].role, "custom"); assert.equal(context.messages[0].content, "latest summary"); assert.equal(context.messages.length, context.entryIds.length); }); test("uses the selected leaf's path before a later compaction", () => { const entries = [ userEntry("u1", null, "root request"), assistantEntry("a1", "u1", "root answer"), userEntry("u2", "a1", "main branch"), { type: "compaction", id: "cmp", parentId: "u2", timestamp: "2026-01-01T00:00:03.000Z", summary: "main branch summary", firstKeptEntryId: "u2", tokensBefore: 100, }, userEntry("alt", "a1", "alternate branch"), ]; const context = buildSessionContext(entries, "alt"); assert.deepEqual(context.entryIds, ["u1", "a1", "alt"]); assert.equal(context.messages.some((message) => message.role === "custom"), false); }); test("returns an empty context for a null leaf", () => { const context = buildSessionContext([ userEntry("u1", null, "not active"), ], null); assert.deepEqual(context.messages, []); assert.deepEqual(context.entryIds, []); }); test("reads the latest valid persisted todo snapshot from the selected branch", () => { const entries = [ userEntry("u1", null, "start"), { type: "message", id: "todo-main", parentId: "u1", timestamp: "2026-01-01T00:00:01.000Z", message: { role: "toolResult", toolName: "todo", content: [], details: { phases: [{ name: "Main", tasks: [{ content: "Keep", status: "in_progress" }] }] }, }, }, { type: "custom", id: "todo-alt", parentId: "u1", timestamp: "2026-01-01T00:00:02.000Z", customType: "user_todo_edit", data: { phases: [{ name: "Alternate", tasks: [{ content: "Use this", status: "pending" }] }] }, }, ]; assert.deepEqual(getTodoPhasesFromEntries(entries, "todo-main"), [ { name: "Main", tasks: [{ content: "Keep", status: "in_progress" }] }, ]); assert.deepEqual(buildSessionContext(entries, "todo-alt").todoPhases, [ { name: "Alternate", tasks: [{ content: "Use this", status: "pending" }] }, ]); }); test("reads the latest todo snapshot on one chain, not the oldest", () => { const entries = [ { type: "message", id: "todo-early", parentId: null, message: { role: "toolResult", toolName: "todo", details: { phases: [{ name: "Early", tasks: [{ content: "step", status: "pending" }] }] }, }, }, { type: "message", id: "todo-late", parentId: "todo-early", message: { role: "toolResult", toolName: "todo", details: { phases: [{ name: "Late", tasks: [{ content: "step", status: "completed" }] }] }, }, }, ]; assert.deepEqual(getTodoPhasesFromEntries(entries, "todo-late"), [ { name: "Late", tasks: [{ content: "step", status: "completed" }] }, ]); }); test("defers historical thinking without changing live-session content", () => { const entries = [ userEntry("u1", null, "start"), { ...assistantEntry("a1", "u1", "answer"), message: { role: "assistant", provider: "test", model: "test-model", content: [ { type: "thinking", thinking: "large reasoning" }, { type: "text", text: "answer" }, ], }, }, ]; const deferred = buildSessionContext(entries, undefined, { deferThinking: true }); assert.deepEqual(deferred.messages[1].content[0], { type: "thinking", thinking: "", deferred: true, }); const full = buildSessionContext(entries); assert.equal(full.messages[1].content[0].thinking, "large reasoning"); }); test("does not defer empty historical thinking blocks", () => { const entries = [ userEntry("u1", null, "start"), { ...assistantEntry("a1", "u1", "answer"), message: { role: "assistant", provider: "test", model: "test-model", content: [ { type: "thinking", thinking: "" }, { type: "text", text: "answer" }, ], }, }, ]; const context = buildSessionContext(entries, undefined, { deferThinking: true }); assert.deepEqual(context.messages[1].content[0], { type: "thinking", thinking: "" }); }); test("deferThinking tolerates string-content assistant entries without crashing", () => { // normalizeToolCalls passes non-array content through unchanged; the // defer-thinking branch must not 500 the context route for legacy entries. const entries = [ userEntry("u1", null, "start"), { type: "message", id: "a1", parentId: "u1", timestamp: "2026-01-01T00:00:00.000Z", message: { role: "assistant", content: "legacy plain-text answer" }, }, ]; const context = buildSessionContext(entries, undefined, { deferThinking: true }); assert.equal(context.messages[1].role, "assistant"); assert.equal(context.messages[1].content, "legacy plain-text answer"); }); test("defers only base64 images from historical tool results", () => { const userImage = { type: "image", source: { type: "base64", media_type: "image/png", data: "QUJDRA==" }, }; const toolImage = { type: "image", source: { type: "base64", media_type: "image/jpeg", data: "QUJDRA==" }, }; const toolUrlImage = { type: "image", source: { type: "url", url: "https://example.com/result.png" }, }; const flatToolImage = { type: "image", data: "QUJDRA==", mimeType: "image/png", }; const entries = [ userEntry("u1", null, [{ type: "text", text: "inspect this" }, userImage]), assistantEntry("a1", "u1", "reading"), { type: "message", id: "tr1", parentId: "a1", timestamp: "2026-01-01T00:00:01.000Z", message: { role: "toolResult", toolCallId: "call1", content: [ { type: "text", text: "Read image file" }, toolImage, flatToolImage, toolUrlImage, ], }, }, ]; const deferred = buildSessionContext(entries, undefined, { deferToolResultImages: true }); assert.deepEqual(deferred.messages[0].content[1], userImage); assert.deepEqual(deferred.messages[2].content[0], { type: "text", text: "Read image file" }); assert.deepEqual(deferred.messages[2].content[1], toolUrlImage); assert.match(deferred.messages[2].content[2].text, /2 tool result images omitted.*image\/jpeg, image\/png.*~8 bytes/); const full = buildSessionContext(entries); assert.deepEqual(full.messages[2].content[1], toolImage); assert.deepEqual(full.messages[2].content[2], flatToolImage); assert.deepEqual(full.messages[2].content[3], toolUrlImage); }); test("preserves hidden custom messages so the UI can render them collapsed", () => { const entries = [ userEntry("u1", null, "start"), { type: "custom_message", id: "c1", parentId: "u1", timestamp: "2026-01-01T00:00:01.000Z", customType: "extension_debug", content: "hidden extension payload", display: false, details: { source: "test" }, }, assistantEntry("a1", "c1", "done"), ]; const context = buildSessionContext(entries); assert.deepEqual(context.entryIds, ["u1", "c1", "a1"]); assert.equal(context.messages[1].role, "custom"); assert.equal(context.messages[1].customType, "extension_debug"); assert.equal(context.messages[1].display, false); assert.equal(context.messages[1].content, "hidden extension payload"); }); test("preserves valid epoch timestamps on synthetic UI messages", () => { const entries = [ userEntry("u1", null, "start"), { type: "compaction", id: "cmp", parentId: "u1", timestamp: "1970-01-01T00:00:00.000Z", summary: "epoch summary", firstKeptEntryId: "u1", tokensBefore: 10, }, ]; const context = buildSessionContext(entries); assert.equal(context.messages[0].role, "custom"); assert.equal(context.messages[0].customType, "compaction"); assert.equal(context.messages[0].timestamp, 0); }); test("reads only a bounded session header, including headers larger than 4 KiB", () => { const dir = mkdtempSync(join(tmpdir(), "pi-web-header-")); const filePath = join(dir, "session.jsonl"); const parentSession = `/tmp/${"p".repeat(5_000)}.jsonl`; writeFileSync(filePath, `${JSON.stringify({ type: "session", version: 3, id: "session", timestamp: "2026-01-01T00:00:00.000Z", cwd: dir, parentSession, })}\n${JSON.stringify(userEntry("u1", null, "message"))}\n`); try { assert.equal(readSessionHeader(filePath)?.parentSession, parentSession); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("returns null for malformed or unbounded session headers", () => { const dir = mkdtempSync(join(tmpdir(), "pi-web-header-invalid-")); const malformedPath = join(dir, "malformed.jsonl"); const oversizedPath = join(dir, "oversized.jsonl"); writeFileSync(malformedPath, "{not-json}\n"); writeFileSync(oversizedPath, "x".repeat(64 * 1024)); try { assert.equal(readSessionHeader(malformedPath), null); assert.equal(readSessionHeader(oversizedPath), null); } finally { rmSync(dir, { recursive: true, force: true }); } }); /** Build a throwaway agent dir with one project directory of session files. */ function withAgentDir(run) { const agentDir = mkdtempSync(join(tmpdir(), "omp-web-agent-")); const projectDir = join(agentDir, "sessions", "-project"); mkdirSync(projectDir, { recursive: true }); const previous = process.env.PI_CODING_AGENT_DIR; process.env.PI_CODING_AGENT_DIR = agentDir; invalidateSessionListCache(); return Promise.resolve(run(projectDir)).finally(() => { if (previous === undefined) delete process.env.PI_CODING_AGENT_DIR; else process.env.PI_CODING_AGENT_DIR = previous; invalidateSessionListCache(); rmSync(agentDir, { recursive: true, force: true }); }); } function writeSessionFile(dir, name, header, entries = []) { const filePath = join(dir, name); const lines = [JSON.stringify({ type: "session", version: 3, ...header })]; for (const entry of entries) lines.push(JSON.stringify(entry)); writeFileSync(filePath, `${lines.join("\n")}\n`); return filePath; } // omp writes header.parentSession as a file path (RPC branch) OR as a bare // session id (TUI /fork, `omp --fork`, /tan). Both must link to the parent. test("links forked children whose parentSession is a bare session id", async () => { await withAgentDir(async (dir) => { const cwd = join(tmpdir(), "omp-web-missing-project"); const parentPath = writeSessionFile(dir, "2026-01-01_parent.jsonl", { id: "parent-id", cwd, timestamp: "2026-01-01T00:00:00.000Z", }, [userEntry("u1", null, "root")]); writeSessionFile(dir, "2026-01-02_by-path.jsonl", { id: "child-by-path", cwd, timestamp: "2026-01-02T00:00:00.000Z", parentSession: parentPath, }, [userEntry("u1", null, "branched")]); writeSessionFile(dir, "2026-01-03_by-id.jsonl", { id: "child-by-id", cwd, timestamp: "2026-01-03T00:00:00.000Z", parentSession: "parent-id", }, [userEntry("u1", null, "forked")]); writeSessionFile(dir, "2026-01-04_orphan.jsonl", { id: "orphan", cwd, timestamp: "2026-01-04T00:00:00.000Z", parentSession: "gone-id", }, [userEntry("u1", null, "orphan")]); const byId = new Map((await listAllSessions()).map((s) => [s.id, s])); assert.equal(byId.get("child-by-path")?.parentSessionId, "parent-id"); assert.equal(byId.get("child-by-id")?.parentSessionId, "parent-id"); assert.equal(byId.get("orphan")?.parentSessionId, undefined); }); }); test("stops resolving a session path once the file is deleted", async () => { await withAgentDir(async (dir) => { const filePath = writeSessionFile(dir, "2026-01-01_doomed.jsonl", { id: "doomed", cwd: join(tmpdir(), "omp-web-missing-project"), timestamp: "2026-01-01T00:00:00.000Z", }, [userEntry("u1", null, "hello")]); assert.equal(await resolveSessionPath("doomed"), filePath); unlinkSync(filePath); // A stale cache hit here would make the agent routes spawn omp with // --resume against a missing file, silently creating a new session. assert.equal(await resolveSessionPath("doomed"), null); assert.equal(globalThis.__piSessionPathCache?.has("doomed"), false); }); }); test("loads sessions larger than the line-reader chunk without corrupting text", () => { const dir = mkdtempSync(join(tmpdir(), "omp-web-large-")); const filePath = join(dir, "large.jsonl"); // Multi-byte content and >1 MiB of it, so lines and characters both straddle // the reader's chunk boundaries. const count = 900; const lines = [JSON.stringify({ type: "session", version: 3, id: "large", cwd: dir, timestamp: "2026-01-01T00:00:00.000Z", })]; for (let i = 0; i < count; i++) { lines.push(JSON.stringify(userEntry(`u${i}`, i === 0 ? null : `u${i - 1}`, `${"あ".repeat(500)}${i}`))); } writeFileSync(filePath, `${lines.join("\n")}\n`); try { const entries = getSessionEntries(filePath); assert.equal(entries.length, count); assert.equal(entries[450].message.content, `${"あ".repeat(500)}450`); assert.equal(entries[count - 1].message.content, `${"あ".repeat(500)}${count - 1}`); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("keeps forward and reverse session path caches in sync", async () => { const sessionId = "cache-test-session"; const filePath = join(tmpdir(), "pi-web-cache-test", "..", "cache-test", "session.jsonl"); cacheSessionPath(sessionId, filePath); try { assert.equal( await resolveSessionIdByPath(filePath), sessionId, ); } finally { invalidateSessionPathCache(sessionId); } assert.equal(globalThis.__piSessionPathCache?.has(sessionId), false); assert.equal(globalThis.__piPathToSessionIdCache?.has(sessionPathKey(filePath)), false); }); // The session-file walk cache keys on the sessions-root mtime, which does not // change when a file is added inside an existing project subdirectory on some // filesystems (Windows/NTFS). invalidateSessionListCache() must clear it so a // brand-new session appears in the sidebar without a full app reload. test("invalidateSessionListCache clears the session-file walk cache", async () => { await withAgentDir(async (dir) => { writeSessionFile(dir, "2026-01-01_first.jsonl", { id: "first", cwd: join(tmpdir(), "omp-web-missing-project"), timestamp: "2026-01-01T00:00:00.000Z", }, [userEntry("u1", null, "first")]); const before = await listAllSessions(); assert.equal(before.some((s) => s.id === "first"), true); // Simulate the walk cache being populated; the root mtime does not change // when the new file lands inside the project subdirectory. writeSessionFile(dir, "2026-01-02_second.jsonl", { id: "second", cwd: join(tmpdir(), "omp-web-missing-project"), timestamp: "2026-01-02T00:00:00.000Z", }, [userEntry("u1", null, "second")]); // Without invalidation the stale walk cache would hide the new session. invalidateSessionListCache(); const after = await listAllSessions(); assert.equal(after.some((s) => s.id === "second"), true); }); }); test("invalidateSessionCaches(path) drops only that file's parse caches", async () => { await withAgentDir(async (dir) => { const fileA = writeSessionFile(dir, "2026-01-01_a.jsonl", { id: "a", cwd: join(tmpdir(), "omp-web-missing-project"), timestamp: "2026-01-01T00:00:00.000Z", }, [userEntry("u1", null, "a")]); const fileB = writeSessionFile(dir, "2026-01-01_b.jsonl", { id: "b", cwd: join(tmpdir(), "omp-web-missing-project"), timestamp: "2026-01-01T00:00:00.000Z", }, [userEntry("u1", null, "b")]); // Prime both per-file parse caches. getSessionEntries(fileA); getSessionEntries(fileB); const cacheKey = (p) => sessionPathKey(p); assert.equal(globalThis.__ompSessionEntriesCache?.has(cacheKey(fileA)), true); assert.equal(globalThis.__ompSessionEntriesCache?.has(cacheKey(fileB)), true); // Only A's entry is dropped; B's parse stays cached. invalidateSessionCaches(fileA); assert.equal(globalThis.__ompSessionEntriesCache?.has(cacheKey(fileA)), false, "A parse cache must drop"); assert.equal(globalThis.__ompSessionEntriesCache?.has(cacheKey(fileB)), true, "B parse cache must survive"); // Same-size + same-mtime replacement of A must be re-parsed after the // targeted invalidation: the targeted invalidation is the ONLY signal the // (size, mtimeMs) key cannot detect. const entries = getSessionEntries(fileA); assert.equal(entries.length, 1, "A re-parses to fresh contents after targeted invalidation"); }); }); test("targeted invalidation catches same-size + same-mtime content changes", async () => { await withAgentDir(async (dir) => { const filePath = writeSessionFile(dir, "2026-01-01_t.jsonl", { id: "t", cwd: join(tmpdir(), "omp-web-missing-project"), timestamp: "2026-01-01T00:00:00.000Z", }, [userEntry("u1", null, "version-one")]); // Normalize mtime to a whole-millisecond value FIRST so the restore below // is exact on filesystems with finer precision (NTFS 100ns): with a // fractional original, JS Date truncation would make mtimeMs differ after // utimesSync and the stat key would invalidate "for free", masking the // very staleness this test must catch. const FIXED_MTIME = new Date("2026-01-02T00:00:00.000Z"); utimesSync(filePath, FIXED_MTIME, FIXED_MTIME); const originalStat = statSync(filePath); const originalSize = originalStat.size; const originalMtime = originalStat.mtimeMs; assert.equal(originalMtime, FIXED_MTIME.getTime(), "mtime normalized to whole ms"); // Prime the parse cache keyed on (size, originalMtime). const cachedFirst = getSessionEntries(filePath); assert.equal(cachedFirst[0].message.content, "version-one"); // Rewrite with DIFFERENT content of EXACTLY the same byte length, then // restore the mtime: (size, mtimeMs) now matches the cached entry again, // so only an explicit invalidation can see through. const rewrittenBody = JSON.stringify({ type: "session", version: 3, id: "t", cwd: join(tmpdir(), "omp-web-missing-project"), timestamp: "2026-01-01T00:00:00.000Z" }) + "\n" + JSON.stringify(userEntry("u1", null, "version-two")) + "\n"; assert.equal( Buffer.byteLength(rewrittenBody, "utf8"), originalSize, "rewrite must preserve byte length so the stat key cannot detect it", ); writeFileSync(filePath, rewrittenBody); utimesSync(filePath, FIXED_MTIME, FIXED_MTIME); const restoredStat = statSync(filePath); assert.equal(restoredStat.size, originalSize, "size restored"); assert.equal(restoredStat.mtimeMs, originalMtime, "mtime restored to pre-rewrite value"); // WITHOUT invalidation the memoized parse must serve the STALE snapshot — // this proves the stat key alone cannot detect the change. Then the // targeted invalidation must force a fresh parse. const stale = getSessionEntries(filePath); assert.equal(stale[0].message.content, "version-one", "cached snapshot served without invalidation (stale)"); invalidateSessionCaches(filePath); const fresh = getSessionEntries(filePath); assert.equal(fresh[0].message.content, "version-two", "same-size+mtime change must be picked up after targeted invalidation"); }); }); test("task toolResults keep size-bounded details; other toolResults stay stripped", () => { const entries = [ { type: "message", id: "t1", parentId: null, timestamp: "2026-01-01T00:00:00.000Z", message: { role: "toolResult", toolCallId: "tc1", toolName: "task", content: [], details: { projectAgentsDir: "C:\work\agents", totalDurationMs: 360000, progress: [{ id: "Scout", agent: "scout", status: "running", task: "Map", tokens: 100, cost: 0.001 }], results: [{ id: "Scout", agent: "scout", task: "Map", exitCode: 0, tokens: 999000, cost: 1.25, output: "x".repeat(5000), stderr: "err", aborted: false, resolvedModel: "m".repeat(5000), outputPath: "p".repeat(5000), }], async: { state: "completed", jobId: "Scout", type: "task", message: "x".repeat(10000) }, agent: "a".repeat(5000), }, }, }, { type: "message", id: "t2", parentId: "t1", timestamp: "2026-01-01T00:00:00.000Z", message: { role: "toolResult", toolCallId: "tc2", toolName: "edit", content: [], details: { huge: "x".repeat(10000), patch: "the patch" }, }, }, ]; const context = buildSessionContext(entries); const taskMessage = context.messages.find((m) => m.role === "toolResult" && m.toolCallId === "tc1"); const editMessage = context.messages.find((m) => m.role === "toolResult" && m.toolCallId === "tc2"); assert.ok(taskMessage); const taskDetails = taskMessage.details ?? {}; assert.ok(Array.isArray(taskDetails.results)); assert.equal(taskDetails.results[0].tokens, 999000); // bulky output is not shipped to the client assert.equal("output" in taskDetails.results[0], false); assert.equal("stderr" in taskDetails.results[0], false); assert.equal(taskDetails.progress[0].id, "Scout"); assert.equal(taskDetails.async.jobId, "Scout"); // async is projected: extra payload fields never ride the response assert.equal("message" in taskDetails.async, false); // long scalar strings are truncated to the 240-char bound assert.equal(taskDetails.results[0].resolvedModel.length <= 241, true); assert.equal(taskDetails.results[0].outputPath.length <= 241, true); // agent is an allowlisted (truncated) field — present but bounded assert.equal(taskDetails.results[0].agent.length <= 241, true); // non-task details keep only the allowlisted patch/diff keys assert.deepEqual(editMessage.details, { patch: "the patch" }); }); test("hub send/jobs toolResults keep bounded roster details; other hub ops stay stripped", () => { const entries = [ { type: "message", id: "h1", parentId: null, timestamp: "2026-01-01T00:00:00.000Z", message: { role: "toolResult", toolCallId: "hc1", toolName: "hub", content: [], details: { op: "send", to: "VisualFix", receipts: [{ to: "VisualFix", outcome: "injected", extra: "x".repeat(5000) }], noise: "dropped", }, }, }, { type: "message", id: "h2", parentId: "h1", timestamp: "2026-01-01T00:00:00.000Z", message: { role: "toolResult", toolCallId: "hc2", toolName: "hub", content: [], details: { op: "jobs", jobs: [{ id: "VisualFix", type: "task", status: "running", label: "VisualFix", durationMs: 1890000, secret: "dropped" }], }, }, }, { type: "message", id: "h3", parentId: "h2", timestamp: "2026-01-01T00:00:00.000Z", message: { role: "toolResult", toolCallId: "hc3", toolName: "hub", content: [], details: { op: "list", peers: [], counts: { running: 0 } }, }, }, ]; const context = buildSessionContext(entries); const send = context.messages.find((m) => m.role === "toolResult" && m.toolCallId === "hc1"); const jobs = context.messages.find((m) => m.role === "toolResult" && m.toolCallId === "hc2"); const list = context.messages.find((m) => m.role === "toolResult" && m.toolCallId === "hc3"); assert.ok(send); assert.deepEqual(send.details, { op: "send", to: ["VisualFix"], receipts: [{ to: "VisualFix", outcome: "injected" }] }); assert.ok(jobs); assert.deepEqual(jobs.details, { op: "jobs", jobs: [{ id: "VisualFix", type: "task", status: "running", label: "VisualFix", durationMs: 1890000 }] }); // list carries no chat rendering — nothing allowlisted, details dropped assert.ok(list); assert.equal("details" in list, false); }); test("message entries without a message object are skipped, not fatal", () => { // Imports and hand-edited files can contain type:"message" entries whose // `message` object is missing entirely. Every reader (context build, todo // phases, model inference) must skip them instead of throwing. const entries = [ { type: "session", version: 3, id: "s1", timestamp: "2026-01-01T00:00:00.000Z", cwd: "/tmp", }, { type: "message", id: "bad1", parentId: null, timestamp: "2026-01-01T00:00:01.000Z", }, userEntry("u1", "bad1", "still readable"), { type: "message", id: "bad2", parentId: "u1", timestamp: "2026-01-01T00:00:02.000Z", message: null, }, ]; const context = buildSessionContext(entries); assert.equal(context.messages.length, 1); assert.equal(context.messages[0].role, "user"); assert.deepEqual(context.entryIds, ["u1"]); assert.doesNotThrow(() => getTodoPhasesFromEntries(entries, "u1")); }); test("single files larger than the cache budget are not cached, so they cannot starve peers", async () => { await withAgentDir(async (dir) => { // Tiny session to confirm the parse cache works normally. const smallPath = writeSessionFile(dir, "2026-01-01_small.jsonl", { id: "small", cwd: join(tmpdir(), "omp-web-missing-project"), timestamp: "2026-01-01T00:00:00.000Z", }, [userEntry("u1", null, "small")]); getSessionEntries(smallPath); assert.equal(globalThis.__ompSessionEntriesCache?.has(sessionPathKey(smallPath)), true, "normal files cache"); // A file bigger than the whole budget must NOT be injected into the cache // (it would pin MAX_SESSION_ENTRIES_CACHE_BYTES for itself and evict every // peer on the next insert). The budget is 256 MiB; sparse-file it: write a // real 1-byte file then truncate to budget+1 so stat.size reflects a giant // without materializing it. const giantPath = join(dir, "2026-01-01_giant.jsonl"); writeFileSync(giantPath, "{\"type\":\"session\",\"version\":3,\"id\":\"giant\",\"cwd\":\"/tmp\",\"timestamp\":\"2026-01-01T00:00:00.000Z\"}\n"); // truncate to just past the budget const budget = 256 * 1024 * 1024; truncateSync(giantPath, budget + 1); getSessionEntries(giantPath); assert.equal(globalThis.__ompSessionEntriesCache?.has(sessionPathKey(giantPath)), false, "giant does not cache"); // The small file's parse must still be cached (the giant did not evict it). assert.equal(globalThis.__ompSessionEntriesCache?.has(sessionPathKey(smallPath)), true, "peer still cached after giant"); }); }); test("getSessionEntriesForDisplay resolves blobs on copies without contaminating the shared cache", async () => { await withAgentDir(async (dir) => { // A blob-bearing assistant image entry: data is a blob:sha256 ref. // The blob store lives under the agent dir (/blobs/). const blobHash = "a".repeat(64); // getBlobsDir() = /blobs — withAgentDir sets agentDir; `dir` // is /sessions/-project, so ../.. is the agentDir root. const blobDir = join(dir, "..", "..", "blobs"); mkdirSync(blobDir, { recursive: true }); writeFileSync(join(blobDir, blobHash), "fake-image-bytes"); const filePath = writeSessionFile(dir, "2026-01-01_blob.jsonl", { id: "blob", cwd: join(tmpdir(), "omp-web-missing-project"), timestamp: "2026-01-01T00:00:00.000Z", }, [ userEntry("u1", null, "show me"), { type: "message", id: "a1", parentId: "u1", timestamp: "2026-01-01T00:00:00.000Z", message: { role: "assistant", provider: "t", model: "m", content: [{ type: "image", data: `blob:sha256:${blobHash}`, mimeType: "image/png" }], }, }, ]); // Prime the shared cache WITHOUT blob resolution. const cached = getSessionEntries(filePath); assert.equal(cached[1].message.content[0].data, `blob:sha256:${blobHash}`, "cache holds the unresolved ref"); // Display read resolves the blob to inline base64... const display = getSessionEntriesForDisplay(filePath); assert.equal( display[1].message.content[0].data, Buffer.from("fake-image-bytes").toString("base64"), "display path resolves the blob to base64", ); // ...and leaves the shared cache untouched. const cachedAfter = getSessionEntries(filePath); assert.equal(cachedAfter[1].message.content[0].data, `blob:sha256:${blobHash}`, "cache is NOT mutated by display read"); assert.notEqual(cachedAfter[1], display[1], "display read must not share the blob entry object"); }); }); // ============================================================================ // Targeted invalidation + concurrency (watcher hot-path regression coverage) // ============================================================================ // Invalidation that lands WHILE a list scan is still in flight must never let // the stale scan result repopulate the cache (generation gate); callers that // arrive after the invalidation must start a fresh scan instead of reusing // the stale in-flight promise. test("invalidation during an in-flight scan does not repopulate the list cache", async () => { await withAgentDir(async (dir) => { writeSessionFile(dir, "2026-01-01_gen.jsonl", { id: "gen", cwd: join(tmpdir(), "omp-web-missing-project"), timestamp: "2026-01-01T00:00:00.000Z", }, [userEntry("u1", null, "gen")]); // Concurrent callers within one generation share one in-flight scan: // listAllSessions is async, so each call wraps the shared tracked scan // promise in its own async wrapper — the shared scan is observable as an // identical result array and a single tracked promise on globalThis. const p1 = listAllSessions(); const sharedScan = globalThis.__piSessionListPromise; assert.notEqual(sharedScan, undefined, "listAllSessions must track its in-flight scan"); const p2 = listAllSessions(); const [r1, r2] = await Promise.all([p1, p2]); assert.equal(r1, r2, "concurrent callers must share one in-flight scan (identical result array)"); // Invalidate SYNCHRONOUSLY after starting a scan, before it settles: the // stale result must be discarded by the generation gate. invalidateSessionListCache(); await sharedScan; assert.equal( globalThis.__piSessionListCache, undefined, "a scan invalidated mid-flight must not repopulate the cache", ); // A caller after the invalidation starts a NEW scan, not the stale one. const staleScan = sharedScan; await listAllSessions(); assert.notEqual( globalThis.__piSessionListPromise, staleScan, "post-invalidation caller must not reuse the stale scan promise", ); const fresh = await listAllSessions(); assert.ok(fresh.some((s) => s.id === "gen"), "fresh scan sees the session"); assert.ok(globalThis.__piSessionListCache, "a clean scan repopulates the cache"); }); }); // The per-file scan memo is keyed on (size, mtimeMs), which a title-slot // rewrite does NOT change: the 256-byte slot is rewritten in place with equal // byte length and the mtime restored. Only targeted invalidation of the scan // memo lets the sidebar pick up the new title. test("same-size + same-mtime title-slot rewrite is caught by targeted scan invalidation", async () => { await withAgentDir(async (dir) => { const cwd = join(tmpdir(), "omp-web-missing-project"); const titleSlot = serializeTitleSlot({ title: "title-original", updatedAt: "2026-01-01T00:00:00.000Z" }); const body = JSON.stringify({ type: "session", version: 3, id: "titled", cwd, timestamp: "2026-01-01T00:00:00.000Z" }) + "\n" + JSON.stringify(userEntry("u1", null, "hello")) + "\n"; const filePath = join(dir, "2026-01-01_titled.jsonl"); writeFileSync(filePath, titleSlot + body); const FIXED_MTIME = new Date("2026-01-02T00:00:00.000Z"); utimesSync(filePath, FIXED_MTIME, FIXED_MTIME); const first = await listAllSessions(); assert.equal(first.find((s) => s.id === "titled")?.name, "title-original"); // In-place rewrite, same byte length (both titles are 14 ASCII chars), // mtime restored: the (size, mtimeMs) scan key cannot detect this. const newSlot = serializeTitleSlot({ title: "title-CHANGED", updatedAt: "2026-01-01T00:00:00.000Z" }); assert.equal(Buffer.byteLength(newSlot), Buffer.byteLength(titleSlot), "rewrite must keep the slot byte length"); const fd = openSync(filePath, "r+"); writeSync(fd, newSlot, 0, Buffer.byteLength(newSlot), 0); closeSync(fd); utimesSync(filePath, FIXED_MTIME, FIXED_MTIME); // Clear only the list TTL cache: the scan memo now serves the stale // title — exactly the staleness window the watcher's targeted flush // must close. invalidateSessionListMeta(); const stale = await listAllSessions(); assert.equal( stale.find((s) => s.id === "titled")?.name, "title-original", "scan memo must serve the old title before targeted invalidation", ); // Watcher-style known-path flush: drop the list metadata AND only this // file's parse + scan memos. invalidateSessionCaches(filePath); const fresh = await listAllSessions(); assert.equal(fresh.find((s) => s.id === "titled")?.name, "title-CHANGED"); }); }); // Real fs.watch integration: continuous appends to one session must collapse // into a debounced flush that resolves the session id, targets ONLY that // file's parse caches, and leaves unrelated sessions cached. test("watcher coalesces continuous writes into targeted invalidation, keeping unrelated caches", { timeout: 15000 }, async () => { await withAgentDir(async (dir) => { const cwd = join(tmpdir(), "omp-web-missing-project"); const fileA = writeSessionFile(dir, "2026-01-01_wa.jsonl", { id: "watch-a", cwd, timestamp: "2026-01-01T00:00:00.000Z", }, [userEntry("u1", null, "a")]); const fileB = writeSessionFile(dir, "2026-01-01_wb.jsonl", { id: "watch-b", cwd, timestamp: "2026-01-01T00:00:00.000Z", }, [userEntry("u1", null, "b")]); // Prime both per-file parse caches and resolve watch-a's id so the flush // path exercises the cached id resolution. getSessionEntries(fileA); getSessionEntries(fileB); assert.equal(await resolveSessionIdByPath(fileA), "watch-a"); const notified = []; const unsubscribe = subscribeSessionFileChanges((ids) => notified.push(...ids)); try { // Continuous writes: two appends inside the 250ms debounce window must // collapse into ONE flush that targets watch-a only. appendFileSync(fileA, JSON.stringify(userEntry("u2", "u1", "a2")) + "\n"); appendFileSync(fileA, JSON.stringify(userEntry("u3", "u2", "a3")) + "\n"); const deadline = Date.now() + 10_000; while (!notified.includes("watch-a")) { if (Date.now() > deadline) { assert.fail(`watcher never notified for watch-a; got: ${JSON.stringify(notified)}`); } await new Promise((resolve) => setTimeout(resolve, 50)); } assert.equal( globalThis.__ompSessionEntriesCache?.has(sessionPathKey(fileA)), false, "written session's parse cache must be dropped", ); assert.equal( globalThis.__ompSessionEntriesCache?.has(sessionPathKey(fileB)), true, "unrelated session's parse cache must survive the targeted flush", ); assert.equal(getSessionEntries(fileA).length, 3, "appended entries are visible after invalidation"); } finally { unsubscribe(); } }); }); // ============================================================================ // High-cost read dedup + oversize cache hygiene (TODO §2) // ============================================================================ const DISPLAY_LOAD_CEILING = 1024 * 1024 * 1024; // MAX_SESSION_LOAD_BYTES // A cached small file that grows past the load ceiling can never match its // (size, mtime) cache key again — the stale entry must be dropped when the // 413 gate fires, not left to linger until LRU eviction. test("oversize cache entry is dropped when a cached file outgrows the load ceiling", async () => { const dir = mkdtempSync(join(tmpdir(), "omp-web-oversize-")); try { const filePath = join(dir, "grown.jsonl"); writeFileSync(filePath, [ JSON.stringify({ type: "session", version: 3, id: "grown", cwd: dir, timestamp: "2026-01-01T00:00:00.000Z" }), JSON.stringify(userEntry("u1", null, "small")), ].join("\n") + "\n"); // Prime the parse cache. assert.equal(getSessionEntries(filePath).length, 1); assert.equal(globalThis.__ompSessionEntriesCache?.has(sessionPathKey(filePath)), true); // Grow to past the load ceiling (sparse truncate — no real bytes written). truncateSync(filePath, DISPLAY_LOAD_CEILING + 1); // Sync display read: 413 semantics preserved AND the stale entry is gone. assert.throws(() => getSessionEntriesForDisplay(filePath), SessionFileTooLargeError); assert.equal( globalThis.__ompSessionEntriesCache?.has(sessionPathKey(filePath)), false, "stale cache entry must be dropped at the 413 gate", ); // Async display read: same 413 semantics, same cleanup. await assert.rejects( () => getSessionEntriesForDisplayAsync(filePath), SessionFileTooLargeError, "async display read must preserve the structured 413", ); assert.equal(globalThis.__ompSessionEntriesCache?.has(sessionPathKey(filePath)), false); assert.equal( globalThis.__ompEntriesInFlight?.size ?? 0, 0, "a rejected read must not linger in the in-flight map", ); } finally { rmSync(dir, { recursive: true, force: true }); } }); // Concurrent same-tick display reads for one file must share ONE parse: both // callers resolve to the identical entry array (separate parses would produce // distinct arrays), and the in-flight bookkeeping clears after settling. test("concurrent display reads share one in-flight parse", async () => { const dir = mkdtempSync(join(tmpdir(), "omp-web-dedup-")); try { const filePath = join(dir, "shared.jsonl"); writeFileSync(filePath, [ JSON.stringify({ type: "session", version: 3, id: "shared", cwd: dir, timestamp: "2026-01-01T00:00:00.000Z" }), JSON.stringify(userEntry("u1", null, "hello")), JSON.stringify(assistantEntry("a1", "u1", "world")), ].join("\n") + "\n"); // Two calls in the same tick, before any microtask runs. const p1 = getSessionEntriesForDisplayAsync(filePath); const p2 = getSessionEntriesForDisplayAsync(filePath); // The second call must have joined the first one's parse, not started // its own (and the map entry is visible while the parse is pending). assert.equal(globalThis.__ompEntriesInFlight?.size, 1, "exactly one in-flight parse"); const [r1, r2] = await Promise.all([p1, p2]); assert.equal(r1.length, 2); // The transform returns a fresh array per caller, but the entries inside // come from the ONE shared parse: separate parses would produce distinct // entry objects. assert.equal(r1[0], r2[0], "entries must come from the same shared parse"); assert.equal(r1[1], r2[1], "entries must come from the same shared parse"); assert.equal( globalThis.__ompEntriesInFlight?.size, 0, "in-flight entry must be removed once the parse settles", ); // A settled file is a stat cache hit afterwards: no in-flight entry. const again = await getSessionEntriesForDisplayAsync(filePath); assert.equal(again.length, 2); assert.equal(globalThis.__ompEntriesInFlight?.size, 0); } finally { rmSync(dir, { recursive: true, force: true }); } }); // Distinct files must not collide in the dedup map, and a rejection (here: // the 413 gate) must not poison a later read after the file shrank back. test("dedup is per-file and rejections are not memoized", async () => { const dir = mkdtempSync(join(tmpdir(), "omp-web-dedup2-")); try { const fileA = join(dir, "a.jsonl"); const fileB = join(dir, "b.jsonl"); for (const [filePath, id] of [[fileA, "a"], [fileB, "b"]]) { writeFileSync(filePath, [ JSON.stringify({ type: "session", version: 3, id, cwd: dir, timestamp: "2026-01-01T00:00:00.000Z" }), JSON.stringify(userEntry("u1", null, id)), ].join("\n") + "\n"); } const [ra, rb] = await Promise.all([ getSessionEntriesForDisplayAsync(fileA), getSessionEntriesForDisplayAsync(fileB), ]); assert.equal(ra.length, 1); assert.equal(rb.length, 1); assert.equal(ra[0].message.content, "a"); assert.equal(rb[0].message.content, "b"); assert.equal(globalThis.__ompEntriesInFlight?.size, 0); // Oversize B past the ceiling, then shrink it back: the first (oversize) // read rejects, the second (shrunk) read succeeds — no memoized failure. truncateSync(fileB, DISPLAY_LOAD_CEILING + 1); await assert.rejects(() => getSessionEntriesForDisplayAsync(fileB), SessionFileTooLargeError); truncateSync(fileB, 0); writeFileSync(fileB, [ JSON.stringify({ type: "session", version: 3, id: "b", cwd: dir, timestamp: "2026-01-01T00:00:00.000Z" }), JSON.stringify(userEntry("u1", null, "shrunk-back")), ].join("\n") + "\n"); const recovered = await getSessionEntriesForDisplayAsync(fileB); assert.equal(recovered.length, 1); assert.equal(recovered[0].message.content, "shrunk-back"); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("history catch-up pages persisted IDs contiguously without resolving old blobs", async (t) => { await withAgentDir(async (dir) => { const hash = "b".repeat(64); const blobDir = join(dir, "..", "..", "blobs"); mkdirSync(blobDir, { recursive: true }); const blobPath = join(blobDir, hash); writeFileSync(blobPath, "old image"); const filePath = writeSessionFile(dir, "2026-01-01_catch-up.jsonl", { id: "catch-up", cwd: dir, timestamp: "2026-01-01T00:00:00.000Z", }, [ { ...userEntry("image", null, ""), message: { role: "user", content: [{ type: "image", data: `blob:sha256:${hash}`, mimeType: "image/png" }] } }, userEntry("repeat-1", "image", "same"), userEntry("repeat-2", "repeat-1", "same"), assistantEntry("answer", "repeat-2", "answer"), ]); const blobReads = []; const originalRead = fs.readFileSync; const readMock = t.mock.method(fs, "readFileSync", (path, ...args) => { if (String(path) === blobPath) blobReads.push(String(path)); return originalRead(path, ...args); }); syncBuiltinESMExports(); t.after(() => { readMock.mock.restore(); syncBuiltinESMExports(); }); const isolated = createJiti(import.meta.url, { moduleCache: false, tryNative: false }); const { getSessionHistoryPage: readPage } = await isolated.import("./session-reader.ts"); const first = readPage(filePath, null, 1); assert.deepEqual(blobReads, [blobPath], "positive control: the observer sees a real image-page blob read"); blobReads.length = 0; assert.equal(first.history.mode, "replace"); assert.equal(first.history.hasMore, true); assert.deepEqual(first.history.context.entryIds, ["image"]); assert.equal(first.history.context.messages[0].content[0].data, Buffer.from("old image").toString("base64")); assert.equal(getSessionEntries(filePath)[0].message.content[0].data, `blob:sha256:${hash}`); const second = readPage(filePath, first.history.cursor, 2); assert.equal(second.history.mode, "append"); assert.equal(second.history.baseEntryId, "image"); assert.deepEqual(second.history.context.entryIds, ["repeat-1", "repeat-2"]); assert.deepEqual(second.history.context.messages.map((message) => message.content), ["same", "same"]); assert.equal(second.history.hasMore, true); const third = readPage(filePath, second.history.cursor, 2); assert.deepEqual(third.history.context.entryIds, ["answer"]); assert.equal(third.history.hasMore, false); assert.equal(third.history.context.model.modelId, "test-model", "page metadata describes the whole selected context"); const quiet = readPage(filePath, third.history.cursor, 2); assert.equal(quiet.history.mode, "append"); assert.deepEqual(quiet.history.context.entryIds, []); assert.deepEqual(quiet.history.cursor, third.history.cursor); assert.deepEqual(blobReads, [], "delivered image prefixes must not perform blob I/O on later pages"); }); }); test("persisted history catch-up resets after unknown cursors, branch changes and compaction", async () => { await withAgentDir(async (dir) => { const entries = [ userEntry("root", null, "root"), assistantEntry("left", "root", "left"), assistantEntry("right", "root", "right"), ]; const filePath = writeSessionFile(dir, "2026-01-01_reset.jsonl", { id: "catch-up-reset", cwd: dir, timestamp: "2026-01-01T00:00:00.000Z", }, entries); const left = getSessionHistoryPage(filePath, null, 200, "left"); const right = getSessionHistoryPage(filePath, left.history.cursor, 200, "right"); assert.equal(right.history.mode, "replace"); assert.deepEqual(right.history.context.entryIds, ["root", "right"]); assert.equal(right.leafId, "right"); const unknown = getSessionHistoryPage(filePath, { firstEntryId: "root", lastEntryId: "missing" }, 200); assert.equal(unknown.history.mode, "replace"); assert.equal(unknown.history.baseEntryId, null); appendFileSync(filePath, JSON.stringify({ type: "compaction", id: "summary", parentId: "right", timestamp: "2026-01-01T00:00:04.000Z", summary: "condensed", firstKeptEntryId: "right", tokensBefore: 100, }) + "\n"); const compacted = getSessionHistoryPage(filePath, right.history.cursor, 200); assert.equal(compacted.history.mode, "replace"); assert.deepEqual(compacted.history.context.entryIds, ["summary", "right"]); assert.equal(compacted.history.context.messages[0].content, "condensed"); const transcript = getSessionHistoryPage(filePath, compacted.history.cursor, 200, undefined, { includePreCompaction: true }); assert.equal(transcript.history.mode, "replace"); assert.deepEqual(transcript.history.context.entryIds, ["root", "right", "summary"]); }); }); test("history pages seek only new bodies even above the raw-cache budget", async (t) => { await withAgentDir(async (dir) => { const entries = [ assistantEntry("page-one", null, "前文 ".repeat(1000)), assistantEntry("page-two", "page-one", "second"), assistantEntry("page-three", "page-two", "third"), ]; const filePath = writeSessionFile(dir, "indexed.jsonl", { id: "indexed", cwd: dir, timestamp: "2026-01-01T00:00:00.000Z", }, entries); const fds = new Set(); const reads = []; const touchedBodies = new Set(); const entryIds = new Set(entries.map((entry) => entry.id)); const original = { open: fs.openSync, close: fs.closeSync, read: fs.readSync, stat: fs.statSync, fstat: fs.fstatSync, parse: JSON.parse }; // Report a >256 MiB file, but perform every observed read on the small // physical fixture. Both stat paths describe the same artificial version. const reported = (stat) => Object.assign(stat, { size: 256 * 1024 * 1024 + 1 }); const mocks = [ t.mock.method(fs, "openSync", (path, ...args) => { const fd = original.open(path, ...args); if (String(path) === filePath) fds.add(fd); return fd; }), t.mock.method(fs, "closeSync", (fd) => { fds.delete(fd); return original.close(fd); }), t.mock.method(fs, "statSync", (path, ...args) => { const stat = original.stat(path, ...args); return String(path) === filePath ? reported(stat) : stat; }), t.mock.method(fs, "fstatSync", (fd, ...args) => { const stat = original.fstat(fd, ...args); return fds.has(fd) ? reported(stat) : stat; }), t.mock.method(fs, "readSync", (fd, buffer, offset, length, position) => { const count = original.read(fd, buffer, offset, length, position); if (fds.has(fd) && count > 0) reads.push({ position, count, text: buffer.subarray(offset, offset + count).toString("utf8") }); return count; }), t.mock.method(JSON, "parse", (text, ...args) => { const value = original.parse(text, ...args); if (value?.type === "message" && entryIds.has(value.id)) { const content = value.message.content; Object.defineProperty(value.message, "content", { enumerable: true, configurable: true, get() { touchedBodies.add(value.id); return content; }, }); } return value; }), ]; syncBuiltinESMExports(); t.after(() => { for (const mock of mocks) mock.mock.restore(); syncBuiltinESMExports(); }); const isolated = createJiti(import.meta.url, { moduleCache: false, tryNative: false }); const { getSessionHistoryPage: readPage } = await isolated.import("./session-reader.ts"); const first = readPage(filePath, null, 1); assert.ok(reads.some((read) => read.position === null && read.text.includes('"page-three"')), "positive control: index construction scanned the real fixture"); assert.deepEqual([...touchedBodies], ["page-one"], "indexing does not transform off-page bodies"); assert.equal(globalThis.__ompSessionEntriesCache?.has(sessionPathKey(filePath)) ?? false, false, "oversized raw bodies are never cached"); let cursor = first.history.cursor; const messages = [...first.history.context.messages]; for (const id of ["page-two", "page-three"]) { reads.length = 0; touchedBodies.clear(); const page = readPage(filePath, cursor, 1); assert.equal(page.history.mode, "append"); assert.deepEqual(page.history.context.entryIds, [id]); assert.deepEqual([...touchedBodies], [id], "delivered bodies must not be converted again"); assert.ok(reads.length > 0, "positive control: each uncached page body was read from disk"); assert.ok(reads.every((read) => Number.isInteger(read.position) && read.position > 0), "later pages seek, never scan from the start"); const readEntries = reads.map((read) => original.parse(read.text)); assert.deepEqual(readEntries.map((entry) => entry.id), [id], "only requested JSONL bodies are read"); messages.push(...page.history.context.messages); cursor = page.history.cursor; } assert.deepEqual(messages, buildSessionContext(entries).messages); reads.length = 0; touchedBodies.clear(); const quiet = readPage(filePath, cursor, 1); assert.deepEqual(quiet.history.context.messages, []); assert.deepEqual(reads, [], "an unchanged completed cursor needs no body I/O"); assert.deepEqual([...touchedBodies], []); }); }); test("active-context boundaries reuse the history index without reading or hydrating bodies", async (t) => { await withAgentDir(async (dir) => { const hash = "c".repeat(64); const blobDir = join(dir, "..", "..", "blobs"); mkdirSync(blobDir, { recursive: true }); const blobPath = join(blobDir, hash); writeFileSync(blobPath, "boundary image"); const entries = [ userEntry("omitted", null, "old context"), { ...userEntry("image", "omitted", ""), message: { role: "user", content: [{ type: "image", data: `blob:sha256:${hash}`, mimeType: "image/png" }] } }, { type: "compaction", id: "summary", parentId: "image", summary: "condensed", firstKeptEntryId: "image", tokensBefore: 50 }, assistantEntry("inactive", "summary", "other branch"), assistantEntry("active", "summary", "current branch"), ]; const filePath = writeSessionFile(dir, "boundary.jsonl", { id: "boundary", cwd: dir }, entries); const expected = buildSessionContext(getSessionEntriesForDisplay(filePath)).entryIds; assert.deepEqual(expected, ["summary", "image", "active"]); invalidateSessionCaches(filePath); const fds = new Set(); const reads = []; const blobReads = []; const touchedBodies = new Set(); const original = { open: fs.openSync, close: fs.closeSync, read: fs.readSync, readFile: fs.readFileSync, parse: JSON.parse }; const mocks = [ t.mock.method(fs, "openSync", (path, ...args) => { const fd = original.open(path, ...args); if (String(path) === filePath) fds.add(fd); return fd; }), t.mock.method(fs, "closeSync", (fd) => { fds.delete(fd); return original.close(fd); }), t.mock.method(fs, "readSync", (fd, buffer, offset, length, position) => { const count = original.read(fd, buffer, offset, length, position); if (fds.has(fd) && count > 0) reads.push({ position, count }); return count; }), t.mock.method(fs, "readFileSync", (path, ...args) => { if (String(path) === blobPath) blobReads.push(String(path)); return original.readFile(path, ...args); }), t.mock.method(JSON, "parse", (text, ...args) => { const value = original.parse(text, ...args); if (value?.type === "message" && entries.some((entry) => entry.id === value.id)) { const content = value.message.content; Object.defineProperty(value.message, "content", { enumerable: true, configurable: true, get() { touchedBodies.add(value.id); return content; }, }); } return value; }), ]; syncBuiltinESMExports(); t.after(() => { for (const mock of mocks) mock.mock.restore(); syncBuiltinESMExports(); }); const isolated = createJiti(import.meta.url, { moduleCache: false, tryNative: false }); const { getSessionContextBoundary: readBoundary, getSessionHistoryPage: readPage } = await isolated.import("./session-reader.ts"); assert.deepEqual(readBoundary(filePath), { entryIds: expected }); assert.ok(reads.some((read) => read.position === null), "positive control: a cold boundary scans the real file"); assert.deepEqual([...touchedBodies], [], "index projection does not hydrate message content"); assert.deepEqual(blobReads, [], "even a cold boundary does not resolve blobs"); assert.equal(globalThis.__ompSessionEntriesCache?.has(sessionPathKey(filePath)) ?? false, false); reads.length = 0; const page = readPage(filePath, null, 200); assert.deepEqual(page.history.context.entryIds, expected); assert.equal(page.history.context.messages[1].content[0].data, Buffer.from("boundary image").toString("base64")); assert.deepEqual(blobReads, [blobPath], "positive control: the page really reads its image blob"); assert.deepEqual([...touchedBodies], ["image", "active"], "positive control: the page really converts its bodies"); assert.ok(reads.length > 0 && reads.every((read) => read.position > 0), "the page reuses the boundary index and seeks bodies"); reads.length = 0; blobReads.length = 0; touchedBodies.clear(); assert.deepEqual(readBoundary(filePath), { entryIds: expected }); assert.deepEqual(reads, [], "an unchanged boundary performs no transcript-body I/O"); assert.deepEqual(blobReads, []); assert.deepEqual([...touchedBodies], []); }); }); test("active-context boundaries refresh after append and prefix rewrites, including file growth", async () => { await withAgentDir(async (dir) => { const header = { id: "boundary-changes", cwd: dir }; const entries = [userEntry("root", null, "first"), userEntry("child", "root", "second")]; const filePath = writeSessionFile(dir, "boundary-changes.jsonl", header, entries); assert.deepEqual(getSessionContextBoundary(filePath).entryIds, ["root", "child"]); entries.push(userEntry("appended", "child", "third")); appendFileSync(filePath, JSON.stringify(entries.at(-1)) + "\n"); assert.deepEqual(getSessionContextBoundary(filePath).entryIds, ["root", "child", "appended"]); const before = statSync(filePath); entries[1] = userEntry("other", null, "rewritten branch with a longer body"); entries[2].parentId = "other"; writeSessionFile(dir, "boundary-changes.jsonl", header, entries); assert.ok(statSync(filePath).size > before.size, "the prefix rewrite grows the file; growth is not append-only evidence"); assert.deepEqual(getSessionContextBoundary(filePath).entryIds, ["other", "appended"]); const rewritten = statSync(filePath); entries[1].id = "fresh"; entries[2].parentId = "fresh"; writeSessionFile(dir, "boundary-changes.jsonl", header, entries); utimesSync(filePath, rewritten.atime, rewritten.mtime); assert.equal(statSync(filePath).size, rewritten.size); assert.deepEqual(getSessionContextBoundary(filePath).entryIds, ["fresh", "appended"]); assert.deepEqual(getSessionHistoryPage(filePath, null, 200).history.context, buildSessionContext(entries)); }); }); test("indexed pages trim the same Unicode line whitespace as the lenient full reader", async () => { await withAgentDir(async (dir) => { const entries = [userEntry("one", null, "first"), assistantEntry("two", "one", "second")]; const filePath = join(dir, "trimmed.jsonl"); writeFileSync(filePath, [ JSON.stringify({ type: "session", version: 3, id: "trimmed", cwd: dir }), "\ufeff" + JSON.stringify(entries[0]) + "\u00a0", "{torn write", "\u2003" + JSON.stringify(entries[1]) + "\ufeff", ].join("\r\n")); const expected = buildSessionContext(getSessionEntriesForDisplay(filePath)); assert.deepEqual(expected.entryIds, ["one", "two"]); assert.deepEqual(getSessionContextBoundary(filePath), { entryIds: expected.entryIds }); assert.deepEqual(getSessionHistoryPage(filePath, null, 200).history.context, expected); }); }); test("indexed pages concatenate to full Unicode/CRLF compaction and branch context", async () => { await withAgentDir(async (dir) => { const phases = [{ name: "最新", tasks: [{ content: "finish", status: "completed" }] }]; const entries = [ userEntry("root", null, "漢字 ".repeat(300_000)), { type: "compaction", id: "old-summary", parentId: "root", timestamp: "2026-01-01T00:00:00.000Z", summary: "旧 summary", firstKeptEntryId: "root", tokensBefore: 500 }, { type: "thinking_level_change", id: "thinking", parentId: "old-summary", thinkingLevel: "high" }, { type: "model_change", id: "model", parentId: "thinking", model: "provider/model/with/slashes", role: "default" }, { type: "custom", customType: "user_todo_edit", id: "todo", parentId: "model", data: { phases } }, { type: "branch_summary", id: "branch-summary", parentId: "todo", timestamp: "2026-01-01T00:00:00.000Z", summary: "旁支" }, { type: "message", id: "invalid-message", parentId: "branch-summary" }, { type: "compaction", id: "new-summary", parentId: "invalid-message", timestamp: "2026-01-01T00:00:01.000Z", summary: "最新 summary", firstKeptEntryId: "old-summary", tokensBefore: 1000, tokensAfter: 50, method: "manual" }, assistantEntry("left", "new-summary", "左"), assistantEntry("right", "new-summary", "右"), ]; const filePath = join(dir, "unicode.jsonl"); const records = [ { type: "session", version: 3, id: "unicode", cwd: dir }, ...entries, ]; // The first body spans scanner chunks; CRLF and an unterminated last line // make byte offsets differ from both JS character and LF-only offsets. writeFileSync(filePath, serializeTitleSlot({ title: "題名", updatedAt: "2026-01-01T00:00:00.000Z" }) + records.map((entry) => JSON.stringify(entry)).join("\r\n")); for (const leafId of ["left", "right", "missing"]) { for (const includePreCompaction of [false, true]) { const options = { includePreCompaction, deferThinking: true, deferToolResultImages: true }; const expected = buildSessionContext(getSessionEntriesForDisplay(filePath), leafId, options); const actual = { ...expected, messages: [], entryIds: [] }; let cursor = null; for (;;) { const { history, tipId, leafId: selectedLeaf } = getSessionHistoryPage(filePath, cursor, 2, leafId, options); assert.equal(tipId, "right"); assert.equal(selectedLeaf, leafId === "missing" ? "right" : leafId); assert.deepEqual(history.context.todoPhases, leafId === "missing" ? [] : phases); assert.deepEqual(history.context.model, expected.model); assert.equal(history.context.thinkingLevel, "high"); actual.messages.push(...history.context.messages); actual.entryIds.push(...history.context.entryIds); if (!history.hasMore) break; cursor = history.cursor; } assert.deepEqual(actual, expected); } } }); }); test("indexed legacy pages preserve migrations and compaction links without reassigning IDs", async () => { await withAgentDir(async (dir) => { for (const version of [1, 2]) { const entries = [ userEntry("root", null, "旧 request"), { ...userEntry("hook", "root", "legacy hook"), message: { role: "hookMessage", customType: "hook", content: "legacy hook", display: false } }, { type: "compaction", id: "summary", parentId: "hook", timestamp: "2026-01-01T00:00:00.000Z", summary: "short", firstKeptEntryId: "hook", firstKeptEntryIndex: 2, tokensBefore: 50 }, assistantEntry("answer", "summary", "response"), ]; if (version === 1) for (const entry of entries) { delete entry.id; delete entry.parentId; } const filePath = join(dir, `v${version}.jsonl`); writeFileSync(filePath, [ { type: "session", version, id: `legacy-${version}`, cwd: dir }, ...entries, ].map((entry) => JSON.stringify(entry)).join("\n")); const expected = buildSessionContext(getSessionEntriesForDisplay(filePath)); const pages = []; let cursor = null; for (;;) { const { history } = getSessionHistoryPage(filePath, cursor, 1); assert.equal(history.mode, cursor ? "append" : "replace"); pages.push(history); if (!history.hasMore) break; cursor = history.cursor; } const messages = pages.flatMap((page) => page.context.messages); const ids = pages.flatMap((page) => page.context.entryIds); // v1 has no persisted IDs: native-compatible migration assigns fresh // IDs on a cold load, but those IDs and firstKept links survive paging. assert.equal(messages[0].details.firstKeptEntryId, ids[1]); if (version === 1) expected.messages[0].details.firstKeptEntryId = ids[1]; else assert.deepEqual(ids, expected.entryIds); assert.deepEqual(messages, expected.messages); assert.equal(new Set(ids).size, ids.length); assert.deepEqual(getSessionHistoryPage(filePath, pages.at(-1).cursor, 1).history.context.messages, []); assert.deepEqual(getSessionContextBoundary(filePath).entryIds, ids, "boundary and pages share migrated IDs"); } }); }); test("history offsets invalidate for append, same-size rewrite, replacement, truncation and explicit invalidation", async () => { await withAgentDir(async (dir) => { const filePath = writeSessionFile(dir, "changing.jsonl", { id: "changing", cwd: dir }, [ userEntry("one", null, "original"), userEntry("two", "one", "version-one"), ]); const initial = getSessionHistoryPage(filePath, null, 1); const before = statSync(filePath); const header = JSON.stringify({ type: "session", version: 3, id: "changing", cwd: dir }); const body = (text) => [header, JSON.stringify(userEntry("one", null, "original")), JSON.stringify(userEntry("two", "one", text))].join("\n") + "\n"; // A write with the original mtime still changes ctime. writeFileSync(filePath, body("version-two")); utimesSync(filePath, before.atime, before.mtime); assert.equal(getSessionHistoryPage(filePath, initial.history.cursor, 1).history.context.messages[0].content, "version-two"); const replacement = join(dir, "replacement.jsonl"); writeFileSync(replacement, body("version-new")); utimesSync(replacement, before.atime, before.mtime); renameSync(replacement, filePath); assert.equal(getSessionHistoryPage(filePath, initial.history.cursor, 1).history.context.messages[0].content, "version-new"); appendFileSync(filePath, JSON.stringify(userEntry("three", "two", "appended")) + "\n"); const appended = getSessionHistoryPage(filePath, { firstEntryId: "one", lastEntryId: "two" }, 1); assert.deepEqual(appended.history.context.entryIds, ["three"]); writeFileSync(filePath, header + "\n" + JSON.stringify(userEntry("one", null, "original")) + "\n"); const truncated = getSessionHistoryPage(filePath, appended.history.cursor, 1); assert.equal(truncated.history.mode, "replace"); assert.deepEqual(truncated.history.context.entryIds, ["one"]); invalidateSessionCaches(filePath); assert.deepEqual(getSessionHistoryPage(filePath, null, 1).history.context.messages, truncated.history.context.messages); truncateSync(filePath, DISPLAY_LOAD_CEILING + 1); assert.throws(() => getSessionHistoryPage(filePath, null, 1), SessionFileTooLargeError); writeFileSync(filePath, body("after-shrink")); assert.equal(getSessionHistoryPage(filePath, initial.history.cursor, 1).history.context.messages[0].content, "after-shrink"); }); }); test("an over-budget metadata index is not retained and does not evict cached peers", async (t) => { await withAgentDir(async (dir) => { const peer = writeSessionFile(dir, "peer.jsonl", { id: "peer", cwd: dir }, [ userEntry("peer-one", null, "first"), userEntry("peer-two", "peer-one", "second"), ]); const filePath = writeSessionFile(dir, "large-metadata.jsonl", { id: "large-metadata", cwd: dir }, [ { type: "custom", customType: "user_todo_edit", id: "todo", parentId: null, data: { phases: [{ name: "large", tasks: [{ content: "x".repeat(17 * 1024 * 1024), status: "pending" }] }], } }, userEntry("one", "todo", "first"), userEntry("two", "one", "second"), ]); let scannedBytes = 0; const originalRead = fs.readSync; const readMock = t.mock.method(fs, "readSync", (fd, buffer, offset, length, position) => { const count = originalRead(fd, buffer, offset, length, position); if (position === null) scannedBytes += count; return count; }); syncBuiltinESMExports(); t.after(() => { readMock.mock.restore(); syncBuiltinESMExports(); }); const isolated = createJiti(import.meta.url, { moduleCache: false, tryNative: false }); const { getSessionHistoryPage: readPage } = await isolated.import("./session-reader.ts"); const peerFirst = readPage(peer, null, 1); scannedBytes = 0; const first = readPage(filePath, null, 1); assert.ok(scannedBytes >= statSync(filePath).size, "positive control: the real metadata-heavy fixture was scanned"); scannedBytes = 0; const second = readPage(filePath, first.history.cursor, 1); assert.deepEqual(second.history.context.entryIds, ["two"], "the memory bound never truncates history"); assert.equal(second.history.context.todoPhases[0].tasks[0].content.length, 17 * 1024 * 1024, "metadata is returned in full, not clipped to fit the cache"); assert.ok(scannedBytes >= statSync(filePath).size, "UTF-16 metadata larger than the entire 32 MiB index budget is not retained"); scannedBytes = 0; assert.deepEqual(readPage(peer, peerFirst.history.cursor, 1).history.context.entryIds, ["peer-two"]); assert.equal(scannedBytes, 0, "an uncacheable file must not evict an unrelated reusable index"); }); }); test("history never advances a cursor through offsets invalidated during a real page read", async (t) => { await withAgentDir(async (dir) => { const filePath = writeSessionFile(dir, "race.jsonl", { id: "race", cwd: dir }, [ userEntry("one", null, "first"), userEntry("two", "one", "second"), ]); const first = getSessionHistoryPage(filePath, null, 1); const originalRead = fs.readSync; let changed = false; const readMock = t.mock.method(fs, "readSync", (fd, buffer, offset, length, position) => { const count = originalRead(fd, buffer, offset, length, position); if (!changed && position !== null && buffer.subarray(offset, offset + count).toString("utf8").includes('"id":"two"')) { changed = true; appendFileSync(filePath, JSON.stringify(userEntry("three", "two", "newer")) + "\n"); } return count; }); syncBuiltinESMExports(); t.after(() => { readMock.mock.restore(); syncBuiltinESMExports(); }); const isolated = createJiti(import.meta.url, { moduleCache: false, tryNative: false }); const { getSessionHistoryPage: readPage } = await isolated.import("./session-reader.ts"); assert.throws(() => readPage(filePath, first.history.cursor, 1), /Session changed/); assert.equal(changed, true, "positive control: mutation happened inside the actual page read"); const recovered = readPage(filePath, first.history.cursor, 2); assert.equal(recovered.history.mode, "append"); assert.deepEqual(recovered.history.context.entryIds, ["two", "three"]); }); }); test("streamed line ranges count physical UTF-8 bytes across chunks, CRLF and malformed bytes", () => { const dir = mkdtempSync(join(tmpdir(), "omp-web-line-ranges-")); try { const first = Buffer.from("a".repeat(1024 * 1024 - 1) + "界\r\n"); const invalid = Buffer.from([0xff, 0x0d, 0x0a]); const last = Buffer.from("末尾"); const body = Buffer.concat([first, invalid, last]); const filePath = join(dir, "lines.jsonl"); writeFileSync(filePath, body); const lines = []; forEachFileLineSync(filePath, (line, offset, length) => lines.push({ line, offset, length })); assert.deepEqual(lines.map(({ offset, length }) => [offset, length]), [ [0, first.length - 1], [first.length, invalid.length - 1], [first.length + invalid.length, last.length], ]); for (const { line, offset, length } of lines) { assert.equal(line, body.subarray(offset, offset + length).toString("utf8"), "a sought range decodes to the exact streamed line"); } assert.ok(lines[0].line.endsWith("界\r"), "multi-byte character split across chunks is preserved"); } finally { rmSync(dir, { recursive: true, force: true }); } });