返回 DeepSeek-Reasonix
topic_state_mutations.go
根目录 / desktop / topic_state_mutations.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "log/slog"
9 "maps"
10 "strings"
11
12 "reasonix/internal/topicstate"
13 )
14
15 func (m *topicStateManager) createTopic(workspaceRoot, topicID, title, source string, createdAt int64) error {
16 topicID, title, source = strings.TrimSpace(topicID), strings.TrimSpace(title), strings.TrimSpace(source)
17 return m.mutate(workspaceRoot, func(ctx context.Context, store *topicstate.Store) (topicstate.State, error) {
18 return store.Update(ctx, topicID, func(record *topicstate.Record) {
19 applyTopicTitle(record, title, source)
20 record.CreatedAtMS = createdAt
21 })
22 }, func() error {
23 if err := setLegacyTopicTitle(workspaceRoot, topicID, title, source); err != nil {
24 return err
25 }
26 return setLegacyTopicCreatedAt(workspaceRoot, topicID, createdAt)
27 })
28 }
29
30 func (m *topicStateManager) applyAutoTitle(workspaceRoot, topicID, title string, value topicAutoTitleMeta) (bool, error) {
31 topicID, title = strings.TrimSpace(topicID), strings.TrimSpace(title)
32 applied := false
33 err := m.mutate(workspaceRoot, func(ctx context.Context, store *topicstate.Store) (topicstate.State, error) {
34 return store.Update(ctx, topicID, func(record *topicstate.Record) {
35 if record.TitleSource != topicTitleSourceAuto {
36 return
37 }
38 applyTopicTitle(record, title, topicTitleSourceAuto)
39 record.AutoMeta = mergeKnownAutoMeta(record.AutoMeta, value)
40 applied = true
41 })
42 }, func() error {
43 sources, err := loadLegacyStringMap(topicTitleSourcesPath(workspaceRoot))
44 if err != nil {
45 return err
46 }
47 if sources[topicID] != topicTitleSourceAuto {
48 return nil
49 }
50 if err := setLegacyTopicTitle(workspaceRoot, topicID, title, topicTitleSourceAuto); err != nil {
51 return err
52 }
53 if err := setLegacyTopicAutoMeta(workspaceRoot, topicID, &value); err != nil {
54 return err
55 }
56 applied = true
57 return nil
58 })
59 return applied, err
60 }
61
62 func applyTopicTitle(record *topicstate.Record, title, source string) {
63 record.Title = title
64 if title == "" || source == "" {
65 record.TitleSource = ""
66 } else {
67 record.TitleSource = source
68 }
69 if source == topicTitleSourceManual || (source == topicTitleSourceAuto && isDefaultTopicTitle(title)) {
70 record.AutoMeta = nil
71 }
72 }
73
74 func logTopicStateReadFallback(workspaceRoot string, stateErr, legacyErr error, legacyExists bool) {
75 attrs := []any{"scope", topicScopeKind(workspaceRoot), "error_type", topicStateErrorType(stateErr), "legacy_available", legacyExists}
76 if legacyErr != nil {
77 attrs = append(attrs, "legacy_error_type", topicStateErrorType(legacyErr))
78 }
79 slog.Warn("desktop: topic state read fallback", attrs...)
80 }
81
82 func topicStateReadable(workspaceRoot string) error {
83 if _, err := desktopTopicState.snapshot(workspaceRoot); err != nil {
84 legacy, legacyErr := readLegacyTopicSnapshot(workspaceRoot)
85 if legacyErr == nil && legacy.exists {
86 return nil
87 }
88 logTopicStateReadFallback(workspaceRoot, err, legacyErr, legacy.exists)
89 var future *topicstate.FutureSchemaError
90 if errors.As(err, &future) {
91 return errors.New("topic metadata was written by a newer Reasonix version; upgrade Reasonix to open it safely")
92 }
93 return fmt.Errorf("topic metadata is unavailable (%s); retry or check the Reasonix state directory permissions", topicStateErrorType(err))
94 }
95 return nil
96 }
97
98 func mergeLegacyAutoMeta(existing, legacy json.RawMessage) json.RawMessage {
99 fields := map[string]json.RawMessage{}
100 _ = json.Unmarshal(existing, &fields)
101 legacyFields := map[string]json.RawMessage{}
102 if json.Unmarshal(legacy, &legacyFields) != nil {
103 return append(json.RawMessage(nil), existing...)
104 }
105 for _, key := range []string{"stage", "userTurns", "basisHash", "updatedAt"} {
106 delete(fields, key)
107 }
108 maps.Copy(fields, legacyFields)
109 data, _ := json.Marshal(fields)
110 return data
111 }
112
113 func mergeLegacyMissingTitleIndex(workspaceRoot string, titles, sources map[string]string) error {
114 currentTitles, err := loadLegacyStringMap(topicTitlesPath(workspaceRoot))
115 if err != nil {
116 return err
117 }
118 currentSources, err := loadLegacyStringMap(topicTitleSourcesPath(workspaceRoot))
119 if err != nil {
120 return err
121 }
122 deleted := deletedTopicSet()
123 for id := range deleted {
124 delete(currentTitles, id)
125 delete(currentSources, id)
126 }
127 for id, title := range titles {
128 if deleted[id] || strings.TrimSpace(currentTitles[id]) != "" {
129 continue
130 }
131 currentTitles[id] = title
132 }
133 for id, source := range sources {
134 if deleted[id] || strings.TrimSpace(currentSources[id]) != "" {
135 continue
136 }
137 currentSources[id] = source
138 }
139 if err := writeLegacyStringMap(workspaceRoot, topicTitlesPath(workspaceRoot), currentTitles); err != nil {
140 return err
141 }
142 return writeLegacyStringMap(workspaceRoot, topicTitleSourcesPath(workspaceRoot), currentSources)
143 }
144
144 lines GO