

























@@ -2,7 +2,11 @@ import fsSync from "node:fs";
22import os from "node:os";
33import path from "node:path";
44import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
5-import { buildSessionEntry, listSessionFilesForAgent } from "./session-files.js";
5+import {
6+buildSessionEntry,
7+listSessionFilesForAgent,
8+sessionPathForFile,
9+} from "./session-files.js";
610711let fixtureRoot: string;
812let tmpDir: string;
@@ -61,6 +65,28 @@ describe("listSessionFilesForAgent", () => {
6165});
6266});
636768+describe("sessionPathForFile", () => {
69+it("includes the owning agent id when the transcript lives under an agent sessions dir", () => {
70+const absPath = path.join(
71+tmpDir,
72+"agents",
73+"main",
74+"sessions",
75+"deleted-session.jsonl.deleted.2026-02-16T22-27-33.000Z",
76+);
77+78+expect(sessionPathForFile(absPath)).toBe(
79+"sessions/main/deleted-session.jsonl.deleted.2026-02-16T22-27-33.000Z",
80+);
81+});
82+83+it("keeps the legacy basename-only path when the agent owner cannot be derived", () => {
84+expect(sessionPathForFile(path.join(tmpDir, "loose-session.jsonl"))).toBe(
85+"sessions/loose-session.jsonl",
86+);
87+});
88+});
89+6490describe("buildSessionEntry", () => {
6591it("returns lineMap tracking original JSONL line numbers", async () => {
6692// Simulate a real session JSONL file with metadata records interspersed
@@ -116,30 +142,92 @@ describe("buildSessionEntry", () => {
116142expect(entry!.lineMap).toEqual([]);
117143});
118144119-it("skips deleted and checkpoint transcripts for dreaming ingestion", async () => {
145+it("indexes usage-counted reset/deleted archives but still skips bak and checkpoint artifacts", async () => {
146+const resetPath = path.join(tmpDir, "ordinary.jsonl.reset.2026-02-16T22-26-33.000Z");
120147const deletedPath = path.join(tmpDir, "ordinary.jsonl.deleted.2026-02-16T22-27-33.000Z");
148+const bakPath = path.join(tmpDir, "ordinary.jsonl.bak.2026-02-16T22-28-33.000Z");
121149const checkpointPath = path.join(
122150tmpDir,
123151"ordinary.checkpoint.11111111-1111-4111-8111-111111111111.jsonl",
124152);
125153const content = JSON.stringify({
126154type: "message",
127-message: { role: "user", content: "This should never reach the dreaming corpus." },
155+message: { role: "user", content: "Archived hello" },
128156});
157+fsSync.writeFileSync(resetPath, content);
129158fsSync.writeFileSync(deletedPath, content);
159+fsSync.writeFileSync(bakPath, content);
130160fsSync.writeFileSync(checkpointPath, content);
131161162+const resetEntry = await buildSessionEntry(resetPath);
132163const deletedEntry = await buildSessionEntry(deletedPath);
164+const bakEntry = await buildSessionEntry(bakPath);
133165const checkpointEntry = await buildSessionEntry(checkpointPath);
134166135-expect(deletedEntry).not.toBeNull();
136-expect(deletedEntry?.content).toBe("");
137-expect(deletedEntry?.lineMap).toEqual([]);
167+// Usage-counted archives (reset, deleted) must surface real content so
168+// post-reset memory_search can recover prior session history.
169+expect(resetEntry?.content).toContain("User: Archived hello");
170+expect(resetEntry?.lineMap).toEqual([1]);
171+expect(deletedEntry?.content).toContain("User: Archived hello");
172+expect(deletedEntry?.lineMap).toEqual([1]);
173+174+// .bak and compaction checkpoints remain opaque pre-archive / snapshot
175+// artifacts and stay empty so they do not get double-indexed.
176+expect(bakEntry).not.toBeNull();
177+expect(bakEntry?.content).toBe("");
178+expect(bakEntry?.lineMap).toEqual([]);
138179expect(checkpointEntry).not.toBeNull();
139180expect(checkpointEntry?.content).toBe("");
140181expect(checkpointEntry?.lineMap).toEqual([]);
141182});
142183184+it("keeps cron-run deleted archives opaque when the live session store entry is gone", async () => {
185+const archivePath = path.join(tmpDir, "cron-run.jsonl.deleted.2026-02-16T22-27-33.000Z");
186+const jsonlLines = [
187+JSON.stringify({
188+type: "message",
189+message: {
190+role: "user",
191+content: "[cron:job-1 Codex Sessions Sync] Run internal sync.",
192+},
193+}),
194+JSON.stringify({
195+type: "message",
196+message: { role: "assistant", content: "Internal cron output that must stay out." },
197+}),
198+];
199+fsSync.writeFileSync(archivePath, jsonlLines.join("\n"));
200+201+const entry = await buildSessionEntry(archivePath);
202+203+expect(entry).not.toBeNull();
204+expect(entry?.content).toBe("");
205+expect(entry?.lineMap).toEqual([]);
206+expect(entry?.generatedByCronRun).toBe(true);
207+});
208+209+it("keeps cron-run reset archives opaque when session metadata preserves the cron key", async () => {
210+const archivePath = path.join(tmpDir, "cron-run.jsonl.reset.2026-02-16T22-26-33.000Z");
211+const jsonlLines = [
212+JSON.stringify({
213+type: "session-meta",
214+data: { sessionKey: "agent:main:cron:job-1:run:run-1" },
215+}),
216+JSON.stringify({
217+type: "message",
218+message: { role: "assistant", content: "Internal cron output that must stay out." },
219+}),
220+];
221+fsSync.writeFileSync(archivePath, jsonlLines.join("\n"));
222+223+const entry = await buildSessionEntry(archivePath);
224+225+expect(entry).not.toBeNull();
226+expect(entry?.content).toBe("");
227+expect(entry?.lineMap).toEqual([]);
228+expect(entry?.generatedByCronRun).toBe(true);
229+});
230+143231it("skips blank lines and invalid JSON without breaking lineMap", async () => {
144232const jsonlLines = [
145233"",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。