返回 DeepSeek-Reasonix
session_clear.go
根目录 / internal / control / session_clear.go
1 package control
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7
8 "reasonix/internal/agent"
9 "reasonix/internal/event"
10 "reasonix/internal/extension"
11 "reasonix/internal/extension/dispatch"
12 )
13
14 // ClearSession discards the current conversation without preserving it in
15 // resume/history, then rotates to a clean session carrying the same base system
16 // prompt and no pinned context.
17 func (c *Controller) ClearSession() error {
18 if c.executor == nil {
19 return nil
20 }
21 // Same rotation gate as NewSession: hold it across the whole
22 // destroy-then-swap so a turn cannot start during the sequence and have its
23 // live session replaced.
24 if err := c.beginRotation(); err != nil {
25 if errors.Is(err, errTurnRunningRotation) {
26 return fmt.Errorf("cannot clear while a turn is running: %w", err)
27 }
28 return err
29 }
30 defer c.endRotation()
31 if c.sessionEngineEnabled() {
32 return c.rotateExclusiveSession(true)
33 }
34 c.mu.Lock()
35 oldPath := c.sessionPath
36 c.mu.Unlock()
37 preMarkedCleanup := c.hasUnfinishedSessionJobs(oldPath)
38 if preMarkedCleanup {
39 if err := agent.MarkCleanupPending(oldPath, "clear"); err != nil {
40 return err
41 }
42 }
43 // Retire the old recovery state before deleting its artifacts. Async gate
44 // snapshots are path-bound, so wait for every already-scheduled old-path
45 // write; otherwise one can recreate the sidecar after removeSessionArtifacts.
46 c.loadRecoveryState("")
47 c.flushRecoveryPersistence(oldPath)
48 // Let session_policy rule before destroying artifacts. A required failure
49 // keeps the old session intact; the fresh path arrives with session.start.
50 if err := c.extensionSessionPhase(context.Background(), extension.PointSessionRotate, dispatch.PhaseRotate, oldPath); err != nil {
51 return err
52 }
53 // Hold snapshotMu from artifact removal through the swap: a save slipping
54 // in between would resurrect the just-removed transcript, and one that
55 // overlapped the swap could pair the old path with the fresh session.
56 c.snapshotMu.Lock()
57 c.detachInboxForDiscard(oldPath)
58 destroy := c.BeginDestroySession(oldPath)
59 if !destroy.Async {
60 if err := removeSessionArtifacts(oldPath); err != nil {
61 destroy.Finish()
62 c.snapshotMu.Unlock()
63 c.rebindInbox()
64 return err
65 }
66 destroy.Finish()
67 }
68 freshPath := oldPath
69 if c.sessionDir != "" {
70 freshPath = agent.NewSessionPath(c.sessionDir, c.label)
71 }
72 freshSession := agent.NewSession(c.basePrompt())
73 commitTransition, err := c.prepareSessionTransition(freshPath, "clear", freshSession)
74 if err != nil {
75 if destroy.Async {
76 destroy.Finish()
77 }
78 c.snapshotMu.Unlock()
79 c.rebindInbox()
80 return fmt.Errorf("bind cleared session: %w", err)
81 }
82 c.hooks.SessionEnd(context.Background(), "clear")
83 c.extensionSessionEvent(extension.PointSessionEnd, dispatch.PhaseEnd, oldPath)
84 commitTransition.publish()
85 c.bindExecutorProjection(c.SessionPath(), false)
86 if c.guardianSess != nil {
87 c.guardianSess.Reset()
88 }
89 c.ResetPlannerSession()
90 c.rebindCheckpoints(freshPath)
91 seedErr := c.seedSessionEventsFromExecutor("session-clear")
92 c.resetRecoveryForNewSession(freshPath)
93 c.rotateSessionTemp()
94 c.snapshotMu.Unlock()
95 c.rebindInbox()
96 // Same contract as NewSession: the fresh session starts with no active goal.
97 c.ClearGoal()
98 c.mu.Lock()
99 c.startedOnce = true
100 c.mu.Unlock()
101 c.hooks.SetSessionID(c.parentSessionID())
102 c.enqueueHookContexts(c.hooks.SessionStart(context.Background(), "clear"))
103 c.extensionSessionEvent(extension.PointSessionStart, dispatch.PhaseStart, c.SessionPath())
104 c.clearSessionWriteAccess()
105 if destroy.Async {
106 go func() {
107 result := destroy.Wait()
108 if result.HasTimedOut() && destroy.WaitAll != nil {
109 if err := agent.MarkCleanupPending(oldPath, "clear"); err != nil {
110 c.sink.Emit(event.Event{Kind: event.Notice, Level: event.LevelWarn, Text: "mark cleanup pending failed: " + err.Error()})
111 }
112 destroy.WaitAll()
113 }
114 if err := removeSessionArtifacts(oldPath); err != nil {
115 c.sink.Emit(event.Event{Kind: event.Notice, Level: event.LevelWarn, Text: "clear session cleanup failed: " + err.Error()})
116 }
117 destroy.Finish()
118 }()
119 }
120 if seedErr != nil {
121 return fmt.Errorf("seed cleared session events: %w", seedErr)
122 }
123 return nil
124 }
125
126 // detachInboxForDiscard releases the old inbox transaction lock before a
127 // destructive session clear. Windows does not permit removing an open lock
128 // file, while Unix silently unlinks it, so the ordering must be explicit.
129 func (c *Controller) detachInboxForDiscard(sessionPath string) {
130 c.inbox.scanMu.Lock()
131 defer c.inbox.scanMu.Unlock()
132 c.inbox.mu.Lock()
133 defer c.inbox.mu.Unlock()
134 if c.inbox.store == nil || c.inbox.store.SessionPath() != sessionPath {
135 return
136 }
137 c.inbox.store.Close()
138 c.inbox.store = nil
139 c.inbox.clearActive()
140 }
141
141 lines GO