返回 DeepSeek-Reasonix
synchronous_turn.go
根目录 / internal / control / synchronous_turn.go
1 package control
2
3 import (
4 "context"
5 "errors"
6
7 "reasonix/internal/event"
8 "reasonix/internal/session"
9 )
10
11 // runSynchronousTurn owns the blocking transport lifecycle. Durable steer
12 // acknowledgement is intentionally shared with the asynchronous completion
13 // path, while follow-up dispatch remains owned by the synchronous frontend so
14 // its response sink stays bound for every queued turn.
15 func (c *Controller) runSynchronousTurn(
16 ctx context.Context,
17 onAdmitted func() error,
18 run func(context.Context) error,
19 ) error {
20 releaseAdmission := c.trySubmissionAdmissionLock()
21 if releaseAdmission == nil {
22 return ErrTurnRunning
23 }
24 defer releaseAdmission()
25 if err := c.authentication.admissionError(); err != nil {
26 return err
27 }
28 if err := c.ensureWriteAuthorityReady(); err != nil {
29 return err
30 }
31 if ledger := c.turnEventLedger(); ledger != nil && ledger.CurrentStatus() == event.TurnRecoveryRequired {
32 return ErrRecoveryRequired
33 }
34 parent := ctx
35 c.mu.Lock()
36 // Finishing is part of the gate: TurnDone is still fanning out. Closed
37 // seals a torn-down controller. Blocking callers get an error rather than
38 // parking because they already own and enforce the request boundary.
39 if c.bodyActiveLocked() || c.finalizingLocked() || c.rotating || c.closed || c.recoveryRequiredLocked() {
40 c.mu.Unlock()
41 return ErrTurnRunning
42 }
43 if c.rejectDrainingGenerationLocked() {
44 c.mu.Unlock()
45 c.emitDrainingNotice()
46 return ErrRuntimeDraining
47 }
48 ctx, cancel, admitted := c.startTurnLocked(ctx, queuedTurn{})
49 if !admitted {
50 c.mu.Unlock()
51 c.emitDrainingNotice()
52 return ErrRuntimeDraining
53 }
54 c.mu.Unlock()
55 if parent != nil {
56 stop := context.AfterFunc(parent, func() { c.signalTurnCancel() })
57 defer stop()
58 }
59 c.refreshRuntimeState(event.Event{})
60 finish := func() {
61 c.mu.Lock()
62 if c.turns.done != nil {
63 close(c.turns.done)
64 c.turns.done = nil
65 }
66 c.turns.cancel = nil
67 c.turns.cancelRequested = false
68 closing := c.closed
69 recovery := c.turns.phase == session.RuntimeRecoveryRequired
70 c.turns.finishingBound.end()
71 c.turns.finishingBound.endIdle()
72 if !recovery {
73 c.turns.lastToken = c.turns.token
74 if closing {
75 c.turns.phase = session.RuntimeClosed
76 } else {
77 c.turns.phase = session.RuntimeIdle
78 }
79 c.turns.turnID = ""
80 c.noteExecutionLocked(session.RuntimeIdle, "")
81 }
82 c.mu.Unlock()
83 if closing {
84 c.finalizeControllerClose()
85 }
86 c.refreshRuntimeState(event.Event{})
87 c.kickGoalDriver()
88 cancel()
89 }
90 if onAdmitted != nil {
91 if err := onAdmitted(); err != nil {
92 finish()
93 return err
94 }
95 }
96 defer event.RecordTurnCompletion(c.sink)
97 defer func() {
98 finish()
99 c.onInboxTurnDone()
100 }()
101 // Blocking transports use the same host-owned turn boundary as interactive
102 // submissions. The agent's TurnStarted notification is deliberately a
103 // duplicate display event; it cannot create runtime ownership or a durable
104 // turn by itself.
105 run = c.prepareTurnAdmission(run)
106 releaseAdmission()
107 runErr := run(ctx)
108 c.authentication.recordFailure(runErr, c.ModelRef())
109 // Keep the execution binding through the synchronous terminal commit just
110 // like the asynchronous loop. Close may make the public controller view
111 // closed here, but it cannot release the ledger/session underneath TurnDone.
112 c.mu.Lock()
113 if c.turns.done != nil {
114 close(c.turns.done)
115 c.turns.done = nil
116 }
117 c.turns.cancel = nil
118 if c.turns.phase != session.RuntimeRecoveryRequired {
119 c.turns.phase = session.RuntimeFinalizing
120 c.turns.finishingBound.begin(true)
121 c.noteExecutionLocked(session.RuntimeFinalizing, "turn")
122 }
123 c.mu.Unlock()
124 c.refreshRuntimeState(event.Event{})
125 if ledger := c.turnEventLedger(); ledger != nil && ledger.ActiveTurnID() != "" && !ledger.CurrentStatus().Terminal() {
126 cancelled := errors.Is(ctx.Err(), context.Canceled)
127 done := event.Event{Kind: event.TurnDone, Err: runErr, Cancelled: cancelled, Outcome: turnOutcome(runErr)}
128 if cancelled {
129 done.Status = event.TurnInterrupted
130 } else if runErr != nil {
131 done.Status = event.TurnFailed
132 } else {
133 done.Status = event.TurnCompleted
134 }
135 if terminalErr := c.emitTurnEventChecked(done); terminalErr != nil {
136 runErr = errors.Join(runErr, terminalErr)
137 }
138 }
139 return runErr
140 }
141
141 lines GO