fix(agent): let stopped threads start a new task and scope thread state to the latest task

Replace the stored cancel func with a handle so a stale deferred cleanup
can't delete a newer task's cancellation, and clear a thread's cancelled
flag when it submits fresh work instead of refusing to restart. Thread
progress/finished state now derives from the latest user task in the
thread rather than the whole history. Adds coverage for continuing a
stopped thread and for sanitized generation-failure messages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 12:06:50 +08:00
parent 7679854b5d
commit d13a4c9a48
4 changed files with 151 additions and 23 deletions
@@ -100,6 +100,18 @@ func TestProjectThreadStateFinishesAfterGenerationToolSucceeds(t *testing.T) {
}
}
func TestProjectThreadStateKeepsNewTaskOpenAfterStoppedTask(t *testing.T) {
started, finished, failed := projectThreadState([]design.Message{
{ID: "old-user", Role: "user", Type: "user", Content: "生成三张图", ThreadID: "thread-agent", CreatedAt: time.Now().Add(-3 * time.Second)},
{ID: "old-stop", Role: "assistant", Type: "assistant", Content: "已停止", ThreadID: "thread-agent", Status: "cancelled", CreatedAt: time.Now().Add(-2 * time.Second)},
{ID: "new-user", Role: "user", Type: "user", Content: "继续生成一张", ThreadID: "thread-agent", CreatedAt: time.Now().Add(-time.Second)},
{ID: "planner", Role: "assistant", Type: "assistant", Name: "agent_planner", ThreadID: "thread-agent", Status: "success", CreatedAt: time.Now()},
}, "thread-agent")
if !started || finished || failed {
t.Fatalf("expected new task to keep stopped thread open, got started=%v finished=%v failed=%v", started, finished, failed)
}
}
func TestProjectEventFingerprintIgnoresMessageOnlyChanges(t *testing.T) {
now := time.Now()
project := design.Project{