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:
@@ -72,6 +72,10 @@ type DesignService struct {
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type agentTaskCancelHandle struct {
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type GenerateResult struct {
|
||||
Project design.Project
|
||||
Message design.Message
|
||||
@@ -537,14 +541,7 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des
|
||||
if err != nil {
|
||||
return design.AgentThread{}, err
|
||||
}
|
||||
if threadIsCancelled(project.Messages, threadID) || s.isAgentTaskCancelled(project.ID, threadID) {
|
||||
return design.AgentThread{
|
||||
ThreadID: threadID,
|
||||
Status: "cancelled",
|
||||
ThreadIDType: threadIDType,
|
||||
Project: project,
|
||||
}, nil
|
||||
}
|
||||
s.clearAgentTaskCancellation(project.ID, threadID)
|
||||
|
||||
now := s.now()
|
||||
actionID := newID()
|
||||
@@ -1700,7 +1697,7 @@ func (s *DesignService) isUserCancelledGeneration(ctx context.Context, projectID
|
||||
if getErr != nil {
|
||||
return false
|
||||
}
|
||||
return threadIsCancelled(project.Messages, threadID)
|
||||
return threadHasCancelledMessage(project.Messages, threadID)
|
||||
}
|
||||
|
||||
func detachedUserContext(ctx context.Context) context.Context {
|
||||
@@ -1737,13 +1734,16 @@ func (s *DesignService) withAgentTaskCancellation(ctx context.Context, projectID
|
||||
return ctx, func() {}
|
||||
}
|
||||
taskCtx, cancel := context.WithCancel(ctx)
|
||||
s.agentTaskCancels.Store(key, context.CancelFunc(cancel))
|
||||
handle := &agentTaskCancelHandle{cancel: cancel}
|
||||
s.agentTaskCancels.Store(key, handle)
|
||||
if s.isAgentTaskCancelled(projectID, threadID) {
|
||||
cancel()
|
||||
}
|
||||
return taskCtx, func() {
|
||||
cancel()
|
||||
s.agentTaskCancels.Delete(key)
|
||||
if current, ok := s.agentTaskCancels.Load(key); ok && current == handle {
|
||||
s.agentTaskCancels.Delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1754,7 +1754,9 @@ func (s *DesignService) cancelAgentTask(projectID string, threadID string) {
|
||||
}
|
||||
s.cancelledAgentTasks.Store(key, s.now())
|
||||
if cancel, ok := s.agentTaskCancels.Load(key); ok {
|
||||
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
|
||||
if handle, ok := cancel.(*agentTaskCancelHandle); ok && handle != nil {
|
||||
handle.cancel()
|
||||
} else if cancelFunc, ok := cancel.(context.CancelFunc); ok {
|
||||
cancelFunc()
|
||||
}
|
||||
}
|
||||
@@ -1763,6 +1765,14 @@ func (s *DesignService) cancelAgentTask(projectID string, threadID string) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DesignService) clearAgentTaskCancellation(projectID string, threadID string) {
|
||||
key := agentTaskKey(projectID, threadID)
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
s.cancelledAgentTasks.Delete(key)
|
||||
}
|
||||
|
||||
func (s *DesignService) isAgentTaskCancelled(projectID string, threadID string) bool {
|
||||
key := agentTaskKey(projectID, threadID)
|
||||
if key == "" {
|
||||
@@ -3122,6 +3132,30 @@ func (s *DesignService) applyIncrementalGeneratedNode(ctx context.Context, proje
|
||||
}
|
||||
|
||||
func threadIsCancelled(messages []design.Message, threadID string) bool {
|
||||
threadID = strings.TrimSpace(threadID)
|
||||
if threadID == "" {
|
||||
return false
|
||||
}
|
||||
for index := len(messages) - 1; index >= 0; index-- {
|
||||
message := messages[index]
|
||||
if strings.TrimSpace(message.ThreadID) != threadID {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(message.Status), "cancelled") {
|
||||
return true
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(message.Role), "user") {
|
||||
return false
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(message.Status)) {
|
||||
case "running", "streaming", "submitted", "success", "failed":
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func threadHasCancelledMessage(messages []design.Message, threadID string) bool {
|
||||
threadID = strings.TrimSpace(threadID)
|
||||
if threadID == "" {
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user