返回 DeepSeek-Reasonix
session_source_rows.go
根目录 / desktop / session_source_rows.go
1 package main
2
3 import (
4 "fmt"
5 "os"
6 "reasonix/desktop/internal/workspacestate"
7 "reasonix/internal/agent"
8 "sync"
9 )
10
11 type sourceHeadObservation struct {
12 stamp string
13 heads []agent.SessionHead
14 err error
15 }
16
17 var sourceHeadRows sync.Map
18
19 // A single-head DAG is displayed by path, while upgrades record its head ID.
20 // Use the same path alias in every projection so retained originals cannot
21 // reappear after their canonical session is archived. Multi-head rows keep
22 // independent identities; adopting one must never hide its siblings.
23 func sourceMappingHasPathAlias(mapping workspacestate.SourceMapping) bool {
24 if mapping.HeadID == "" {
25 return true
26 }
27 heads, err := sessionSourceHeads(mapping.Path)
28 if err != nil {
29 return false
30 }
31 visible := 0
32 selected := false
33 for _, head := range heads {
34 if head.Retired {
35 continue
36 }
37 if head.Kind != agent.HeadKindConcurrent {
38 visible++
39 }
40 if head.Selected && head.ID == mapping.HeadID {
41 selected = true
42 }
43 }
44 return selected && visible <= 1
45 }
46
47 // Listing consumes only the published head index. Replaying an event log here
48 // would make sidebar pagination perform content work and contend with writers.
49 // Missing/stale indices degrade to one path row and are repaired separately.
50 func sessionSourceHeads(path string) ([]agent.SessionHead, error) {
51 info, err := os.Stat(path)
52 if err != nil {
53 return nil, err
54 }
55 stamp := fmt.Sprint(info.Size(), ":", info.ModTime().UnixNano())
56 if cached, ok := sourceHeadRows.Load(path); ok && cached.(sourceHeadObservation).stamp == stamp {
57 entry := cached.(sourceHeadObservation)
58 return entry.heads, entry.err
59 }
60 index, err := agent.ReadSessionHeadIndex(path)
61 var heads []agent.SessionHead
62 if err == nil && index != nil && index.Current(path) {
63 heads = index.Heads
64 }
65 sourceHeadRows.Store(path, sourceHeadObservation{stamp, heads, err})
66 return heads, err
67 }
68
69 func expandSessionSourceRows(node ProjectNode) []ProjectNode {
70 if node.Session != nil || node.SessionPath == "" || node.RecoveryState == "recovery_only" {
71 return []ProjectNode{node}
72 }
73 heads, err := sessionSourceHeads(node.SessionPath)
74 if err != nil {
75 node.Health = "degraded"
76 return []ProjectNode{node}
77 }
78 live := []agent.SessionHead{}
79 for _, head := range heads {
80 if !head.Retired && head.Kind != agent.HeadKindConcurrent {
81 live = append(live, head)
82 }
83 }
84 if len(live) <= 1 {
85 headID := ""
86 if len(live) == 1 {
87 headID = live[0].ID
88 }
89 node.Source = &SessionSourceRef{HostID: localDesktopHostID, Path: node.SessionPath, HeadID: headID, SourceKey: desktopSourceKey(node.SessionPath, headID)}
90 node.Historical = true
91 return []ProjectNode{node}
92 }
93 rows := []ProjectNode{}
94 for _, head := range live {
95 row := node
96 row.Source = &SessionSourceRef{HostID: localDesktopHostID, Path: node.SessionPath, HeadID: head.ID, SourceKey: desktopSourceKey(node.SessionPath, head.ID)}
97 row.Historical, row.HistoricalBranch = true, true
98 row.Key = "source_" + row.Source.SourceKey
99 row.Turns, row.Preview = head.Turns, head.Preview
100 if !head.LastActivity.IsZero() {
101 row.LastActivityAt = head.LastActivity.UnixMilli()
102 }
103 if !head.CreatedAt.IsZero() {
104 row.CreatedAt = head.CreatedAt.UnixMilli()
105 }
106 rows = append(rows, row)
107 }
108 return rows
109 }
110
110 lines GO