| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "reasonix/internal/agent" |
| 8 | "reasonix/internal/control" |
| 9 | "reasonix/internal/session" |
| 10 | "strings" |
| 11 | ) |
| 12 | |
| 13 | // nativeSessionSelection belongs to the durable source, including detached views. |
| 14 | type nativeSessionSelection struct { |
| 15 | SessionHeadID string `json:"sessionHeadId,omitempty"` |
| 16 | } |
| 17 | |
| 18 | // A historical directory (or its retained JSONL alias) is represented by a |
| 19 | // bound runtime whose SessionPath is empty. Compare both store and identity; |
| 20 | // comparing only the path would rebuild that runtime on every admission. |
| 21 | func (a *App) controllerMatchesSessionPath(ctrl control.SessionAPI, path string) bool { |
| 22 | if path == "" || sessionRuntimeKey(ctrl.SessionPath()) == sessionRuntimeKey(path) { |
| 23 | return true |
| 24 | } |
| 25 | service, runtime, bound := exclusiveSessionBinding(ctrl) |
| 26 | if !bound { |
| 27 | return false |
| 28 | } |
| 29 | root, id := desktopSessionRoot(filepath.Dir(path)), agent.BranchID(path) |
| 30 | if nativeStoredDirectory(path) { |
| 31 | root, id = filepath.Dir(path), filepath.Base(path) |
| 32 | } |
| 33 | expected, err := a.historicalSessionService(root) |
| 34 | return err == nil && service == expected && runtime.Ref().SessionID == id |
| 35 | } |
| 36 | |
| 37 | func nativeStoredDirectory(path string) bool { |
| 38 | info, err := os.Stat(path) |
| 39 | return err == nil && info.IsDir() && hasHistoricalSessionArtifacts(path) |
| 40 | } |
| 41 | |
| 42 | func (a *App) sessionServiceForSource(sessionDir, sessionID, path, storedDir string) (*session.Service, error) { |
| 43 | if storedDir != "" { |
| 44 | return a.historicalSessionService(filepath.Dir(storedDir)) |
| 45 | } |
| 46 | if sessionID == "" && path != "" { |
| 47 | return a.historicalSessionService(desktopSessionRoot(filepath.Dir(path))) |
| 48 | } |
| 49 | return a.desktopSessionService(sessionDir), nil |
| 50 | } |
| 51 | |
| 52 | func (a *App) bindNativeDirectoryForTab(buildCtx context.Context, tab *WorkspaceTab, ctrl control.SessionAPI, storedLegacyDir, rootKey string, buildGeneration uint64) bool { |
| 53 | identity := ctrl.(control.IdentityLifecycle) |
| 54 | ref := session.SessionRef{HostID: localDesktopHostID, SessionID: filepath.Base(storedLegacyDir)} |
| 55 | if _, bindErr := identity.OpenSession(buildCtx, ref); bindErr != nil { |
| 56 | a.recordTabStartupFailure(tab, buildGeneration, a.ctx, friendlySessionLoadError(bindErr)) |
| 57 | ctrl.Close() |
| 58 | a.releaseSharedHost(rootKey) |
| 59 | return false |
| 60 | } |
| 61 | a.mu.Lock() |
| 62 | if a.tabBuildSupersededLocked(tab, buildGeneration) { |
| 63 | a.mu.Unlock() |
| 64 | a.abandonSupersededBuild(tab, ctrl, rootKey, "") |
| 65 | return false |
| 66 | } |
| 67 | tab.SessionID, tab.SessionPath = "", storedLegacyDir |
| 68 | a.mu.Unlock() |
| 69 | tab.replaceTelemetry(loadTelemetry(storedLegacyDir+".telemetry.json"), sessionRuntimeKey(storedLegacyDir)) |
| 70 | return true |
| 71 | } |
| 72 | |
| 73 | func (a *App) validateNativeHeadLocked(path, headID string) error { |
| 74 | if owner := a.liveRuntimeTabMatchingLocked(nil, path); owner != nil && owner.SessionHeadID != headID { |
| 75 | return newSessionOperationError("busy", "Close the open head before opening another head of this historical session.") |
| 76 | } |
| 77 | return nil |
| 78 | } |
| 79 | |
| 80 | func (a *App) nativeStartupSource(tab *WorkspaceTab, root, tabScope, tabWorkspaceRoot, tabTopicID, tabSessionPath string) (string, string, string) { |
| 81 | sessionDir := desktopSessionDir(root) |
| 82 | if tabScope == "global" { |
| 83 | sessionDir = desktopSessionDir(globalWorkspaceRoot()) |
| 84 | } |
| 85 | topicID := strings.TrimSpace(tabTopicID) |
| 86 | pinnedPath, hasPinnedPath := pinnedTabSessionPathForBuild(tabScope, tabWorkspaceRoot, sessionDir, tabSessionPath) |
| 87 | if hasPinnedPath && agent.IsCleanupPending(pinnedPath) { |
| 88 | // Boot reconciliation may finish the pending deletion before the later |
| 89 | // resume step. Clear the local candidate now so the disappeared path is |
| 90 | // not mistaken for a deliberate empty placeholder afterward. |
| 91 | hasPinnedPath = false |
| 92 | pinnedPath = "" |
| 93 | } |
| 94 | catalogTopicPath := "" |
| 95 | if hasPinnedPath { |
| 96 | // A restored tab's exact path is already known state, not history |
| 97 | // discovery. Keep legacy-directory and empty placeholder paths usable |
| 98 | // while the catalog is still opening or rebuilding. |
| 99 | sessionDir = filepath.Dir(pinnedPath) |
| 100 | } else { |
| 101 | catalogTopicPath = a.catalogSessionPathForTopic(tabScope, tabWorkspaceRoot, topicID) |
| 102 | } |
| 103 | if !hasPinnedPath && catalogTopicPath != "" { |
| 104 | sessionDir = filepath.Dir(catalogTopicPath) |
| 105 | } |
| 106 | startupSessionPath := "" |
| 107 | if hasPinnedPath { |
| 108 | if !agent.IsCleanupPending(pinnedPath) { |
| 109 | startupSessionPath = pinnedPath |
| 110 | } |
| 111 | } else if catalogTopicPath != "" { |
| 112 | startupSessionPath = catalogTopicPath |
| 113 | } |
| 114 | if nativeStoredDirectory(tabSessionPath) { |
| 115 | startupSessionPath = tabSessionPath |
| 116 | sessionDir = filepath.Dir(tabSessionPath) |
| 117 | } |
| 118 | prepareStartupPinnedContext(tab, startupSessionPath, tabSessionPath) |
| 119 | storedLegacyDir := "" |
| 120 | if nativeStoredDirectory(startupSessionPath) { |
| 121 | storedLegacyDir = startupSessionPath |
| 122 | } |
| 123 | return sessionDir, startupSessionPath, storedLegacyDir |
| 124 | } |
| 125 |