MCPcopy Create free account
hub / github.com/atomicdotdev/atomic / handle_turn_end

Method handle_turn_end

atomic-agent/src/turn/orchestrator/turn.rs:134–330  ·  view source on GitHub ↗

Handle a TurnEnd event (Stop). Records an Atomic change for the turn (status → add → record), then transitions the session back to Idle. The recording workflow lets the repository figure out what changed: 1. `repo.status()` — find modified, deleted, and untracked files 2. `repo.add()` — track any new files the agent created 3. `repo.record(all: true)` — record everything that's dirty This avoid

(
        &mut self,
        event: TurnEvent,
    )

Source from the content-addressed store, hash-verified

132 /// snapshots between TurnStart and TurnEnd. Instead, we ask the
133 /// repository what changed since the last recorded state.
134 pub(super) async fn handle_turn_end(
135 &mut self,
136 event: TurnEvent,
137 ) -> AgentResult<DispatchResult> {
138 let session_id = &event.session_id;
139 let _turn_end_lock = match self.try_turn_end_lock(session_id) {
140 TurnEndLock::Acquired(guard) => Some(guard),
141 TurnEndLock::Busy => {
142 log::warn!(
143 "Turn end for session {} is already being recorded; skipping duplicate Stop hook",
144 session_id
145 );
146 return Ok(DispatchResult::new(session_id, phase::Phase::Idle)
147 .with_warning("duplicate Stop hook skipped: turn already recording"));
148 }
149 TurnEndLock::Unavailable => None,
150 };
151
152 // Fast gate: check if anything changed since the last record.
153 // This bypasses the entire status machinery (TREE scan, filesystem
154 // walk, etc.) and just checks the pristine database mtime.
155 // If the DB hasn't been written since the last record, nothing
156 // in the working copy could have been recorded — but files may
157 // have been edited. We check the working copy for recent mtimes
158 // by scanning only the repo root (not recursively) and common
159 // source directories.
160 if !self.has_working_copy_changes() {
161 log::info!(
162 "Turn end for session {} — no changes detected, skipping record",
163 session_id
164 );
165 return Ok(DispatchResult::new(session_id, phase::Phase::Idle));
166 }
167
168 let mut session = self.load_or_create_session(session_id, &event)?;
169
170 // Extract model/provider from the TurnEnd event's raw_json.
171 // OpenCode sends model and provider in every stop payload.
172 // This is the last chance to capture the info before recording,
173 // in case TurnStart didn't have it (e.g., session was created
174 // outside the plugin, or the chat.message hook didn't fire).
175 if let Some(ref raw) = event.raw_json {
176 if let Some(model) = raw.get("model").and_then(|v| v.as_str()) {
177 if !model.is_empty() {
178 session.model = model.to_string();
179 }
180 }
181 if let Some(provider) = raw.get("provider").and_then(|v| v.as_str()) {
182 if !provider.is_empty() {
183 session.agent_vendor = provider.to_string();
184 }
185 }
186 }
187
188 // Release the watcher if it was active (best-effort, ignore errors)
189 if self.watcher.is_active() {
190 let _ = self.watcher.cancel_turn().await;
191 }

Callers 1

dispatchMethod · 0.80

Calls 15

transitionFunction · 0.85
apply_common_actionsFunction · 0.85
record_turnFunction · 0.85
try_turn_end_lockMethod · 0.80
cancel_turnMethod · 0.80
as_refMethod · 0.80
as_u64Method · 0.80
recorded_file_listMethod · 0.80
add_files_touchedMethod · 0.80

Tested by

no test coverage detected