Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions packages/coding-agent/src/memories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,13 @@ async function runPhase2(options: MemoryStartupOptions): Promise<void> {
await syncPhase2Artifacts(memoryRoot, outputs);
if (!isMemoryStartupActive(options)) return;
if (outputs.length === 0) {
await cleanupConsolidatedArtifacts(memoryRoot);
// Never wipe consolidated artifacts for an empty input: the scope
// may simply have no outputs yet (or its outputs landed under a
// sibling scope key, e.g. a case-variant cwd on Windows), while a
// live MEMORY.md / memory_summary.md / skills/ from a prior run —
// or a racing scope sharing this directory — is still valid. The
// destructive cleanup previously deleted those on every empty
// Phase 2, stranding and erasing real consolidations (#12596).
if (!isMemoryStartupActive(options)) return;
const marked = markGlobalPhase2Succeeded(db, {
ownershipToken: claim.ownershipToken,
Expand Down Expand Up @@ -861,12 +867,6 @@ async function syncPhase2Artifacts(memoryRoot: string, outputs: Stage1OutputRow[
await Bun.write(path.join(memoryRoot, "raw_memories.md"), rawBody);
}

async function cleanupConsolidatedArtifacts(memoryRoot: string): Promise<void> {
await fs.rm(path.join(memoryRoot, "MEMORY.md"), { force: true });
await fs.rm(path.join(memoryRoot, "memory_summary.md"), { force: true });
await fs.rm(path.join(memoryRoot, "skills"), { recursive: true, force: true });
}

function buildRawMemoriesMarkdown(outputs: Stage1OutputRow[]): string {
if (outputs.length === 0) {
return "# Raw Memories\n\nNo raw memories yet.\n";
Expand Down
13 changes: 8 additions & 5 deletions packages/coding-agent/test/memories-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ describe("memories runtime", () => {
expect(raw.indexOf("## thread-b")).toBeLessThan(raw.indexOf("## thread-a"));
});

test("phase2 empty-input cleanup removes consolidated files and skills dir", async () => {
test("phase2 empty-input preserves consolidated files and skills dir (issue #12596)", async () => {
const fx = await createFixture();
const memoryRoot = getMemoryRoot(fx.agentDir, fx.session.sessionManager.getCwd());
await fs.mkdir(path.join(memoryRoot, "skills", "legacy"), { recursive: true });
Expand All @@ -420,10 +420,13 @@ describe("memories runtime", () => {
taskDepth: 0,
});

await settle(fx.whenSettled, "phase2 empty-input cleanup");
expect(await Bun.file(path.join(memoryRoot, "MEMORY.md")).exists()).toBe(false);
expect(await Bun.file(path.join(memoryRoot, "memory_summary.md")).exists()).toBe(false);
expect(await Bun.file(path.join(memoryRoot, "skills")).exists()).toBe(false);
await settle(fx.whenSettled, "phase2 empty-input preservation");
// An empty Phase 2 must not wipe live consolidations: outputs may land
// later (or under a sibling scope key), and the artifacts on disk are
// still valid. Previously this path deleted them.
expect(await Bun.file(path.join(memoryRoot, "MEMORY.md")).exists()).toBe(true);
expect(await Bun.file(path.join(memoryRoot, "memory_summary.md")).exists()).toBe(true);
expect(await Bun.file(path.join(memoryRoot, "skills", "legacy", "SKILL.md")).exists()).toBe(true);
expect((await fs.readFile(path.join(memoryRoot, "raw_memories.md"), "utf8")).trim()).toBe(
"# Raw Memories\n\nNo raw memories yet.",
);
Expand Down