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:
@@ -198,7 +198,12 @@ func projectThreadState(messages []design.Message, threadID string) (started boo
|
||||
if threadID == "" {
|
||||
return false, false, false
|
||||
}
|
||||
for _, message := range messages {
|
||||
startIndex := latestThreadTaskStartIndex(messages, threadID)
|
||||
if startIndex < 0 {
|
||||
return false, false, false
|
||||
}
|
||||
for index := startIndex; index < len(messages); index++ {
|
||||
message := messages[index]
|
||||
if strings.TrimSpace(message.ThreadID) != threadID {
|
||||
continue
|
||||
}
|
||||
@@ -211,7 +216,7 @@ func projectThreadState(messages []design.Message, threadID string) (started boo
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
if hasUnfinishedThreadWork(messages, threadID) {
|
||||
if hasUnfinishedThreadWorkFrom(messages, threadID, startIndex) {
|
||||
finished = false
|
||||
}
|
||||
return started, finished, failed
|
||||
@@ -222,8 +227,16 @@ func hasUnfinishedThreadWork(messages []design.Message, threadID string) bool {
|
||||
if threadID == "" {
|
||||
return false
|
||||
}
|
||||
return hasUnfinishedThreadWorkFrom(messages, threadID, latestThreadTaskStartIndex(messages, threadID))
|
||||
}
|
||||
|
||||
func hasUnfinishedThreadWorkFrom(messages []design.Message, threadID string, startIndex int) bool {
|
||||
if startIndex < 0 {
|
||||
return false
|
||||
}
|
||||
running := make(map[string]bool)
|
||||
for _, message := range messages {
|
||||
for index := startIndex; index < len(messages); index++ {
|
||||
message := messages[index]
|
||||
if strings.TrimSpace(message.ThreadID) != threadID {
|
||||
continue
|
||||
}
|
||||
@@ -242,6 +255,30 @@ func hasUnfinishedThreadWork(messages []design.Message, threadID string) bool {
|
||||
return len(running) > 0
|
||||
}
|
||||
|
||||
func latestThreadTaskStartIndex(messages []design.Message, threadID string) int {
|
||||
threadID = strings.TrimSpace(threadID)
|
||||
if threadID == "" {
|
||||
return -1
|
||||
}
|
||||
firstIndex := -1
|
||||
latestUserIndex := -1
|
||||
for index, message := range messages {
|
||||
if strings.TrimSpace(message.ThreadID) != threadID {
|
||||
continue
|
||||
}
|
||||
if firstIndex < 0 {
|
||||
firstIndex = index
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(message.Role), "user") {
|
||||
latestUserIndex = index
|
||||
}
|
||||
}
|
||||
if latestUserIndex >= 0 {
|
||||
return latestUserIndex
|
||||
}
|
||||
return firstIndex
|
||||
}
|
||||
|
||||
func threadWorkKey(message design.Message) string {
|
||||
role := strings.ToLower(strings.TrimSpace(message.Role))
|
||||
if role == "" {
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user