From 942ff4b1a4e5088a4b3c1db9c8acfd4c55a13173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Thu, 14 May 2026 13:17:04 +0000 Subject: [PATCH] fix(cli): race condition in thread rm + flaky test (#265) Two fixes: 1. cmdThreadRemove: always call both removeThreadEntry and removeThreadHistoryEntries regardless of resolved source, preventing race where thread moves from active to history between resolve and delete. 2. Test: add waitUntilRunningFileAbsent before thread show/rm, matching the pattern used by adjacent test cases. Verified 5x consecutive runs with 0 failures. Closes #265 --- packages/cli-workflow/__tests__/thread-cli.test.ts | 3 +++ packages/cli-workflow/src/commands/thread/rm.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/cli-workflow/__tests__/thread-cli.test.ts b/packages/cli-workflow/__tests__/thread-cli.test.ts index f0db13d..edbf617 100644 --- a/packages/cli-workflow/__tests__/thread-cli.test.ts +++ b/packages/cli-workflow/__tests__/thread-cli.test.ts @@ -180,6 +180,9 @@ describe("cli thread commands", () => { } expect(threads.value.some((l) => l.includes(threadId))).toBe(true); + const runningPath = join(storageRoot, "logs", added.value.hash, `${threadId}.running`); + await waitUntilRunningFileAbsent(runningPath, 120); + const shown = await cmdThreadShow(storageRoot, threadId); expect(shown.ok).toBe(true); if (!shown.ok) { diff --git a/packages/cli-workflow/src/commands/thread/rm.ts b/packages/cli-workflow/src/commands/thread/rm.ts index 814e346..fb2d998 100644 --- a/packages/cli-workflow/src/commands/thread/rm.ts +++ b/packages/cli-workflow/src/commands/thread/rm.ts @@ -18,13 +18,13 @@ export async function cmdThreadRemove( return err(`thread not found: ${threadId}`); } - if (resolved.source === "active") { - await removeThreadEntry(resolved.bundleDir, threadId); - } else { - const hist = await removeThreadHistoryEntries(resolved.bundleDir, threadId); - if (!hist.ok) { - return hist; - } + // Always clear both stores: between resolve and delete the worker may finish and + // move the thread from threads.json into history; branching only on resolved.source + // would skip history removal and leave a dangling row. + await removeThreadEntry(resolved.bundleDir, threadId); + const hist = await removeThreadHistoryEntries(resolved.bundleDir, threadId); + if (!hist.ok) { + return hist; } const infoPath = join(storageRoot, "logs", resolved.bundleHash, `${threadId}.info.jsonl`);