Add POST/GET/POST cancel task run APIs, background worker with event replay, workspace reconcile, runner child cancel, and chat message metadata updates. Co-authored-by: Cursor <cursoragent@cursor.com>
91 lines
2.3 KiB
Go
91 lines
2.3 KiB
Go
package squad
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ReconcileReport 工作区状态对账结果。
|
|
type ReconcileReport struct {
|
|
WorkDir string `json:"work_dir"`
|
|
PreviousStatus string `json:"previous_status"`
|
|
NewStatus string `json:"new_status"`
|
|
ExecutionOK bool `json:"execution_ok"`
|
|
HasOutput bool `json:"has_output"`
|
|
Reconciled bool `json:"reconciled"`
|
|
Reason string `json:"reason,omitempty"`
|
|
}
|
|
|
|
// ReconcileWorkspaceStatus 检测外层 failed 但 execution.log 为 SUCCESS 的孤儿任务并修复。
|
|
func ReconcileWorkspaceStatus(workDir string) (*ReconcileReport, error) {
|
|
workDir = strings.TrimSpace(workDir)
|
|
if workDir == "" {
|
|
return nil, fmt.Errorf("work_dir required")
|
|
}
|
|
metaPath := filepath.Join(workDir, "workspace.json")
|
|
metaBytes, err := os.ReadFile(metaPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var meta WorkspaceMetadata
|
|
if err := json.Unmarshal(metaBytes, &meta); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
report := &ReconcileReport{
|
|
WorkDir: workDir,
|
|
PreviousStatus: meta.Status,
|
|
NewStatus: meta.Status,
|
|
}
|
|
|
|
execOK := scanExecutionSuccess(workDir)
|
|
report.ExecutionOK = execOK
|
|
report.HasOutput = fileExists(filepath.Join(workDir, "tasks", "01_organize", "output.md")) ||
|
|
fileExists(filepath.Join(workDir, "output.md"))
|
|
|
|
if meta.Status != "failed" || !execOK || !report.HasOutput {
|
|
report.Reason = "no reconciliation needed"
|
|
return report, nil
|
|
}
|
|
|
|
meta.Status = "completed"
|
|
now := time.Now()
|
|
meta.UpdatedAt = now
|
|
meta.CompletedAt = &now
|
|
if err := writeJSON(metaPath, meta); err != nil {
|
|
return nil, err
|
|
}
|
|
report.NewStatus = "completed"
|
|
report.Reconciled = true
|
|
report.Reason = "execution log SUCCESS with output present while workspace was failed"
|
|
return report, nil
|
|
}
|
|
|
|
func scanExecutionSuccess(workDir string) bool {
|
|
var found bool
|
|
_ = filepath.Walk(filepath.Join(workDir, "tasks"), func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() || info.Name() != "execution.log" {
|
|
return nil
|
|
}
|
|
b, readErr := os.ReadFile(path)
|
|
if readErr != nil {
|
|
return nil
|
|
}
|
|
if strings.Contains(string(b), "Outcome: SUCCESS") {
|
|
found = true
|
|
return filepath.SkipAll
|
|
}
|
|
return nil
|
|
})
|
|
return found
|
|
}
|
|
|
|
func fileExists(path string) bool {
|
|
fi, err := os.Stat(path)
|
|
return err == nil && !fi.IsDir()
|
|
}
|