返回 DeepSeek-Reasonix
fork_operations.go
根目录 / desktop / fork_operations.go
1 package main
2
3 import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "sync"
10
11 "reasonix/internal/config"
12 "reasonix/internal/fileutil"
13 )
14
15 const forkOperationsSchemaVersion = 1
16
17 var forkOperationsMu sync.Mutex
18
19 type forkOperation struct {
20 OperationID string `json:"operationId"`
21 Surface string `json:"surface"`
22 TabID string `json:"tabId"`
23 SourceHostID string `json:"sourceHostId,omitempty"`
24 SourceSessionID string `json:"sourceSessionId"`
25 TurnID string `json:"turnId"`
26 BoundarySequence uint64 `json:"boundarySequence"`
27 State string `json:"state"`
28 ChildSessionID string `json:"childSessionId,omitempty"`
29 }
30
31 type forkOperationJournal struct {
32 SchemaVersion int `json:"schemaVersion"`
33 Operations []forkOperation `json:"operations"`
34 }
35
36 func forkOperationsPath() string {
37 return filepath.Join(config.MemoryUserDir(), "fork-operations.json")
38 }
39
40 func loadForkOperations(path string) (forkOperationJournal, error) {
41 journal := forkOperationJournal{SchemaVersion: forkOperationsSchemaVersion, Operations: []forkOperation{}}
42 body, err := os.ReadFile(path)
43 if os.IsNotExist(err) {
44 return journal, nil
45 }
46 if err != nil {
47 return journal, err
48 }
49 if err := json.Unmarshal(body, &journal); err != nil {
50 return journal, fmt.Errorf("decode fork operation journal: %w", err)
51 }
52 if journal.SchemaVersion != forkOperationsSchemaVersion {
53 return journal, fmt.Errorf("unsupported fork operation journal schema %d", journal.SchemaVersion)
54 }
55 if journal.Operations == nil {
56 journal.Operations = []forkOperation{}
57 }
58 return journal, nil
59 }
60
61 func saveForkOperations(path string, journal forkOperationJournal) error {
62 journal.SchemaVersion = forkOperationsSchemaVersion
63 if journal.Operations == nil {
64 journal.Operations = []forkOperation{}
65 }
66 body, err := json.MarshalIndent(journal, "", " ")
67 if err != nil {
68 return err
69 }
70 body = append(body, '\n')
71 return fileutil.AtomicWriteFile(path, body, 0o600)
72 }
73
74 func sameForkOperation(left, right forkOperation) bool {
75 return left.SourceHostID == right.SourceHostID && left.SourceSessionID == right.SourceSessionID &&
76 left.TurnID == right.TurnID && left.BoundarySequence == right.BoundarySequence
77 }
78
79 func (a *App) beginForkOperation(template forkOperation) (forkOperation, error) {
80 forkOperationsMu.Lock()
81 defer forkOperationsMu.Unlock()
82 path := forkOperationsPath()
83 journal, err := loadForkOperations(path)
84 if err != nil {
85 return forkOperation{}, err
86 }
87 for _, existing := range journal.Operations {
88 if sameForkOperation(existing, template) {
89 return existing, nil
90 }
91 }
92 template.OperationID = "fork_" + strings.TrimPrefix(newTabID(), "tab_")
93 template.State = "pending"
94 journal.Operations = append(journal.Operations, template)
95 if err := saveForkOperations(path, journal); err != nil {
96 return forkOperation{}, err
97 }
98 return template, nil
99 }
100
101 func (a *App) completeForkOperation(operationID, childSessionID string) error {
102 forkOperationsMu.Lock()
103 defer forkOperationsMu.Unlock()
104 path := forkOperationsPath()
105 journal, err := loadForkOperations(path)
106 if err != nil {
107 return err
108 }
109 for index := range journal.Operations {
110 if journal.Operations[index].OperationID == operationID {
111 journal.Operations[index].State = "completed"
112 journal.Operations[index].ChildSessionID = childSessionID
113 return saveForkOperations(path, journal)
114 }
115 }
116 return fmt.Errorf("fork operation %q is missing", operationID)
117 }
118
119 func (a *App) discardForkOperation(operationID string) error {
120 return removeForkOperation(operationID, false)
121 }
122
123 // AcknowledgeForkOperation removes a durable result only after the UI has
124 // opened or adopted the child. Operation ids are host-unique, so acknowledgement
125 // remains valid when Desktop restart restored the source under another tab id.
126 // A pending operation cannot be acknowledged away, and repeating an
127 // acknowledgement is harmless.
128 func (a *App) AcknowledgeForkOperation(tabID, operationID string) error {
129 return removeForkOperation(operationID, true)
130 }
131
132 func removeForkOperation(operationID string, completedOnly bool) error {
133 operationID = strings.TrimSpace(operationID)
134 if operationID == "" {
135 return nil
136 }
137 forkOperationsMu.Lock()
138 defer forkOperationsMu.Unlock()
139 path := forkOperationsPath()
140 journal, err := loadForkOperations(path)
141 if err != nil {
142 return err
143 }
144 next := journal.Operations[:0]
145 for _, operation := range journal.Operations {
146 if operation.OperationID == operationID && (!completedOnly || operation.State == "completed") {
147 continue
148 }
149 next = append(next, operation)
150 }
151 if len(next) == len(journal.Operations) {
152 return nil
153 }
154 journal.Operations = next
155 return saveForkOperations(path, journal)
156 }
157
157 lines GO