返回 DeepSeek-Reasonix
session_catalog_lifecycle.go
根目录 / desktop / session_catalog_lifecycle.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "log/slog"
7 "os"
8 "strings"
9 "time"
10
11 "golang.org/x/sync/errgroup"
12
13 "reasonix/internal/history"
14 "reasonix/internal/sessioncatalog"
15 "reasonix/internal/taskcatalog"
16 )
17
18 func (a *App) runSessionCatalog(ctx context.Context, initialReconcileDone chan struct{}) {
19 initialReconcileFinished := false
20 defer func() {
21 if !initialReconcileFinished {
22 close(initialReconcileDone)
23 }
24 }()
25 path := sessioncatalog.DefaultPath()
26 freshGeneration := false
27 if strings.TrimSpace(path) != "" {
28 _, statErr := os.Stat(path)
29 freshGeneration = errors.Is(statErr, os.ErrNotExist)
30 }
31 targets := a.sessionCatalogTargets()
32 history.RegisterCatalogRoots(historyCatalogRoots(targets))
33 projects := loadProjectsFile()
34 taskcatalog.RegisterSharedProject(globalWorkspaceRoot(), projects.GlobalTitle)
35 for _, project := range projects.Projects {
36 taskcatalog.RegisterSharedProject(project.Root, projectDisplayName(project))
37 }
38 catalog, err := sessioncatalog.Open(ctx, sessioncatalog.Options{
39 Path: path,
40 OnRevision: func(revision uint64, roots []string, reason string) {
41 a.emitProjectTreeChangedV2(revision, roots, reason)
42 },
43 })
44 if err != nil {
45 slog.Warn("desktop: open session catalog", "err", err)
46 return
47 }
48 if ctx.Err() != nil || a.shuttingDown.Load() {
49 closeCtx, closeCancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
50 _ = catalog.Close(closeCtx)
51 closeCancel()
52 return
53 }
54 a.sessionCatalog.Store(catalog)
55 if err := a.syncSessionCatalogMetadataBounded(ctx, catalog); err != nil && !errors.Is(err, context.Canceled) {
56 slog.Warn("desktop: sync session catalog metadata", "err", err)
57 }
58 select {
59 case <-a.tabsRestoredSignal():
60 case <-ctx.Done():
61 return
62 }
63 // Restored tabs can reveal a project absent from the initial registry.
64 targets = a.sessionCatalogTargets()
65 history.RegisterCatalogRoots(historyCatalogRoots(targets))
66 a.indexRestoredSessionPaths(ctx, catalog)
67 // Directory scans are independent and internally batched/resumable; keep
68 // startup work bounded so a large project set cannot starve the UI.
69 var reconcileGroup errgroup.Group
70 reconcileGroup.SetLimit(4)
71 for _, target := range targets {
72 if ctx.Err() != nil || a.shuttingDown.Load() {
73 return
74 }
75 reconcileGroup.Go(func() error {
76 if migrated := migrateLegacySessionsIntoGlobalTopics(target.Path); len(migrated) > 0 {
77 _ = a.syncSessionCatalogMetadataBounded(ctx, catalog)
78 }
79 if err := catalog.ReconcileDirectory(ctx, target); err != nil && !errors.Is(err, context.Canceled) {
80 slog.Debug("desktop: reconcile session catalog directory", "dir", target.Path, "err", err)
81 }
82 return nil
83 })
84 }
85 _ = reconcileGroup.Wait()
86 close(initialReconcileDone)
87 initialReconcileFinished = true
88 if freshGeneration {
89 catalog.MarkRepairReason("generation_upgrade")
90 }
91 a.retargetOpenTabsToContinuations()
92 a.runSessionCatalogRefreshLoop(ctx, catalog)
93 }
94
95 func (a *App) runSessionCatalogRefreshLoop(ctx context.Context, catalog *sessioncatalog.Catalog) {
96 ticker := time.NewTicker(30 * time.Second)
97 defer ticker.Stop()
98 for {
99 select {
100 case <-ticker.C:
101 if err := a.syncSessionCatalogMetadataBounded(ctx, catalog); err != nil && !errors.Is(err, context.Canceled) {
102 slog.Debug("desktop: refresh session catalog metadata", "err", err)
103 }
104 for _, target := range a.sessionCatalogTargets() {
105 if migrated := migrateLegacySessionsIntoGlobalTopics(target.Path); len(migrated) > 0 {
106 _ = a.syncSessionCatalogMetadataBounded(ctx, catalog)
107 }
108 catalog.RequestReconcile(target)
109 // Count sweep rides the periodic reconcile tick; it only moves
110 // provably redundant copies into the recoverable trash.
111 a.sweepExcessRecoveryCopies(catalog, target)
112 }
113 case <-ctx.Done():
114 return
115 }
116 }
117 }
118
118 lines GO