返回 DeepSeek-Reasonix
task_machine.go
根目录 / internal / cli / task_machine.go
1 package cli
2
3 import (
4 "fmt"
5 "io"
6 "os"
7 "sort"
8 "strings"
9 "time"
10
11 "reasonix/internal/agent"
12 "reasonix/internal/jobs"
13 )
14
15 type machineTask struct {
16 ID string `json:"id"`
17 SessionID string `json:"session_id"`
18 Kind string `json:"kind"`
19 Status string `json:"status"`
20 StartedAt string `json:"started_at"`
21 FinishedAt string `json:"finished_at,omitempty"`
22 ArtifactComplete bool `json:"artifact_complete"`
23 }
24
25 type machineTaskList struct {
26 SchemaVersion int `json:"schema_version"`
27 Command string `json:"command"`
28 Tasks []machineTask `json:"tasks"`
29 }
30
31 type machineTaskShow struct {
32 SchemaVersion int `json:"schema_version"`
33 Command string `json:"command"`
34 Task machineTask `json:"task"`
35 }
36
37 type taskMachineOptions struct {
38 dir string
39 projectRoot string
40 sessionID string
41 target string
42 json bool
43 }
44
45 func runTaskCommand(args []string, out io.Writer) int {
46 command := "task"
47 if len(args) == 0 {
48 return writeMachineError(out, command, "invalid_argument", "a task operation is required")
49 }
50 operation := args[0]
51 command = "task." + operation
52 if operation != "list" && operation != "show" {
53 return writeMachineError(out, command, "unknown_command", fmt.Sprintf("unknown task operation %q; expected list or show", operation))
54 }
55 options, code, message := parseTaskMachineOptions(args[1:], operation)
56 if code != "" {
57 return writeMachineError(out, command, code, message)
58 }
59 if !options.json {
60 return writeMachineError(out, command, "invalid_argument", "--json is required")
61 }
62 options.dir = resolveMachineSessionDir(options.dir, options.projectRoot)
63 identityKey, err := loadMachineIdentityKey()
64 if err != nil {
65 return writeMachineError(out, command, "machine_identity_unavailable", "machine identity is unavailable")
66 }
67 tasks, err := machineTasks(options.dir, options.sessionID, identityKey)
68 if err != nil {
69 return writeMachineError(out, command, "task_state_unavailable", "task state is unavailable")
70 }
71 if operation == "list" {
72 return writeMachineJSON(out, machineTaskList{SchemaVersion: machineSchemaVersion, Command: command, Tasks: tasks})
73 }
74 var found *machineTask
75 for i := range tasks {
76 if tasks[i].ID != options.target {
77 continue
78 }
79 if found != nil {
80 return writeMachineError(out, command, "task_ambiguous", "task identifier is ambiguous")
81 }
82 found = &tasks[i]
83 }
84 if found == nil {
85 return writeMachineError(out, command, "task_not_found", "task was not found")
86 }
87 return writeMachineJSON(out, machineTaskShow{SchemaVersion: machineSchemaVersion, Command: command, Task: *found})
88 }
89
90 func parseTaskMachineOptions(args []string, operation string) (taskMachineOptions, string, string) {
91 var options taskMachineOptions
92 for i := 0; i < len(args); i++ {
93 switch args[i] {
94 case "--json":
95 options.json = true
96 case "--dir":
97 if i+1 >= len(args) || strings.TrimSpace(args[i+1]) == "" {
98 return options, "invalid_argument", "--dir requires a value"
99 }
100 i++
101 options.dir = args[i]
102 case "--project-root":
103 if i+1 >= len(args) || strings.TrimSpace(args[i+1]) == "" {
104 return options, "invalid_argument", "--project-root requires a value"
105 }
106 i++
107 options.projectRoot = args[i]
108 case "--session":
109 if i+1 >= len(args) || !validMachineID(args[i+1]) {
110 return options, "invalid_argument", "--session requires a valid identifier"
111 }
112 i++
113 options.sessionID = args[i]
114 case "--help", "-h":
115 return options, "invalid_argument", "use the documented machine interface"
116 default:
117 arg := strings.TrimSpace(args[i])
118 if strings.HasPrefix(arg, "-") {
119 return options, "invalid_argument", "unknown task option"
120 }
121 if operation == "list" || options.target != "" || !validMachineID(arg) {
122 return options, "invalid_argument", "invalid task identifier"
123 }
124 options.target = arg
125 }
126 }
127 if options.dir != "" && options.projectRoot != "" {
128 return options, "invalid_argument", "--dir and --project-root cannot be combined"
129 }
130 if operation == "show" && options.target == "" {
131 return options, "invalid_argument", "a task identifier is required"
132 }
133 return options, "", ""
134 }
135
136 func machineTasks(dir, sessionFilter string, identityKey []byte) ([]machineTask, error) {
137 ordered, err := agent.ListSessionOrder(dir)
138 if err != nil {
139 return nil, err
140 }
141 out := make([]machineTask, 0)
142 for _, session := range ordered {
143 rawSessionID := agent.BranchID(session.Path)
144 sessionID := machineSessionIDWithKey(rawSessionID, identityKey)
145 if sessionFilter != "" && sessionID != sessionFilter {
146 continue
147 }
148 sessionActive := agent.SessionLeaseHeld(session.Path)
149 views, err := jobs.ListArtifactViews(session.Path)
150 if err != nil {
151 return nil, err
152 }
153 for _, view := range views {
154 if view.Kind != "task" {
155 continue
156 }
157 status := view.Status
158 finishedAt := machineUnixMillis(view.FinishedAt)
159 artifactComplete := view.ArtifactComplete
160 if status == jobs.Running && !sessionActive {
161 status = jobs.Interrupted
162 finishedAt = ""
163 artifactComplete = false
164 }
165 out = append(out, machineTask{
166 ID: view.ID,
167 SessionID: sessionID,
168 Kind: "background",
169 Status: string(status),
170 StartedAt: machineUnixMillis(view.StartedAt),
171 FinishedAt: finishedAt,
172 ArtifactComplete: artifactComplete,
173 })
174 }
175 artifacts, err := agent.ListSubagentsByParent(dir, rawSessionID)
176 if err != nil {
177 return nil, err
178 }
179 for _, artifact := range artifacts {
180 if artifact.Meta.Kind != "task" {
181 continue
182 }
183 status := artifact.Meta.Status
184 finishedAt := ""
185 artifactComplete := false
186 if status == agent.SubagentRunning {
187 if !sessionActive {
188 status = agent.SubagentInterrupted
189 }
190 } else {
191 finishedAt = machineTime(artifact.Meta.UpdatedAt)
192 artifactComplete = machineArtifactComplete(artifact.SessionPath)
193 }
194 out = append(out, machineTask{
195 ID: artifact.Ref,
196 SessionID: sessionID,
197 Kind: "subagent",
198 Status: string(status),
199 StartedAt: machineTime(artifact.Meta.CreatedAt),
200 FinishedAt: finishedAt,
201 ArtifactComplete: artifactComplete,
202 })
203 }
204 }
205 sort.SliceStable(out, func(i, j int) bool {
206 if out[i].StartedAt != out[j].StartedAt {
207 return out[i].StartedAt > out[j].StartedAt
208 }
209 if out[i].SessionID != out[j].SessionID {
210 return out[i].SessionID < out[j].SessionID
211 }
212 return out[i].ID < out[j].ID
213 })
214 return out, nil
215 }
216
217 func machineArtifactComplete(path string) bool {
218 info, err := os.Stat(path)
219 return err == nil && info.Mode().IsRegular() && info.Size() > 0
220 }
221
222 func validMachineID(value string) bool {
223 value = strings.TrimSpace(value)
224 return value != "" && !strings.ContainsAny(value, `/\\`)
225 }
226
227 func machineUnixMillis(value int64) string {
228 if value <= 0 {
229 return ""
230 }
231 return machineTime(time.UnixMilli(value))
232 }
233
233 lines GO