OpenCode: recover transcript, reasoning, and response from OpenCode's local SQLite store and fold them into the session and the stop payload before recording. OpenCode writes no transcript file, so its sessions never carry a `transcript_path`, and thin plugin versions forward only session/model metadata. Without this step an OpenCode turn could never carry `agent_turn` data, reasoning, or an `llm
(
&self,
session: &mut AgentSession,
event: &mut TurnEvent,
)
| 334 | /// journal then commits the enriched terminal envelopes. |
| 335 | /// |
| 336 | pub(crate) fn enrich_opencode_turn( |
| 337 | &self, |
| 338 | session: &mut AgentSession, |
| 339 | event: &mut TurnEvent, |
| 340 | ) -> AgentResult<()> { |
| 341 | if session.agent_name != "opencode" { |
| 342 | return Ok(()); |
| 343 | } |
| 344 | |
| 345 | let Some(data) = transcript::opencode::read_turn(&session.session_id, &self.repo_root) |
| 346 | else { |
| 347 | return Ok(()); |
| 348 | }; |
| 349 | |
| 350 | // 1. Transcript file → transcript_path (unblocks agent_turn). |
| 351 | // Rewritten on every turn (whole-session record); a plugin-supplied |
| 352 | // transcript path is never clobbered. |
| 353 | if !data.transcript_jsonl.is_empty() { |
| 354 | let dir = self.session_graph_dir(&session.session_id); |
| 355 | let synthesized = dir.join("opencode-transcript.jsonl"); |
| 356 | let ours = session.transcript_path.is_none() |
| 357 | || session.transcript_path.as_deref() == Some(synthesized.as_path()); |
| 358 | if ours { |
| 359 | let _ = std::fs::create_dir_all(&dir); |
| 360 | match std::fs::write(&synthesized, &data.transcript_jsonl) { |
| 361 | Ok(()) => session.set_transcript_path(&synthesized), |
| 362 | Err(e) => log::warn!( |
| 363 | "Failed to write opencode transcript for session {}: {}", |
| 364 | session.session_id, |
| 365 | e |
| 366 | ), |
| 367 | } |
| 368 | } |
| 369 | } |
| 370 | |
| 371 | // 2. Stop-payload fields the plugin didn't send. |
| 372 | let Some(raw) = event.raw_json.as_mut() else { |
| 373 | return Ok(()); |
| 374 | }; |
| 375 | let Some(obj) = raw.as_object_mut() else { |
| 376 | return Ok(()); |
| 377 | }; |
| 378 | |
| 379 | if !obj.contains_key("reasoning_blocks") |
| 380 | && !obj.contains_key("reasoning_text") |
| 381 | && !data.reasoning_blocks.is_empty() |
| 382 | { |
| 383 | let blocks: Vec<serde_json::Value> = data |
| 384 | .reasoning_blocks |
| 385 | .iter() |
| 386 | .map(|b| { |
| 387 | serde_json::json!({ |
| 388 | "text": b.text, |
| 389 | "duration_ms": b.duration_ms, |
| 390 | }) |
| 391 | }) |
| 392 | .collect(); |
| 393 | obj.insert( |
no test coverage detected