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