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)
| 322 | /// Best-effort: any failure leaves the turn recording what the plugin |
| 323 | /// sent. |
| 324 | pub(crate) fn enrich_opencode_turn(&self, session: &mut AgentSession, event: &mut TurnEvent) { |
| 325 | if session.agent_name != "opencode" { |
| 326 | return; |
| 327 | } |
| 328 | |
| 329 | let Some(data) = transcript::opencode::read_turn(&session.session_id, &self.repo_root) |
| 330 | else { |
| 331 | return; |
| 332 | }; |
| 333 | |
| 334 | // 1. Transcript file → transcript_path (unblocks agent_turn). |
| 335 | // Rewritten on every turn (whole-session record); a plugin-supplied |
| 336 | // transcript path is never clobbered. |
| 337 | if !data.transcript_jsonl.is_empty() { |
| 338 | let dir = self.session_graph_dir(&session.session_id); |
| 339 | let synthesized = dir.join("opencode-transcript.jsonl"); |
| 340 | let ours = session.transcript_path.is_none() |
| 341 | || session.transcript_path.as_deref() == Some(synthesized.as_path()); |
| 342 | if ours { |
| 343 | let _ = std::fs::create_dir_all(&dir); |
| 344 | match std::fs::write(&synthesized, &data.transcript_jsonl) { |
| 345 | Ok(()) => session.set_transcript_path(&synthesized), |
| 346 | Err(e) => log::warn!( |
| 347 | "Failed to write opencode transcript for session {}: {}", |
| 348 | session.session_id, |
| 349 | e |
| 350 | ), |
| 351 | } |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | // 2. Stop-payload fields the plugin didn't send. |
| 356 | let Some(raw) = event.raw_json.as_mut() else { |
| 357 | return; |
| 358 | }; |
| 359 | let Some(obj) = raw.as_object_mut() else { |
| 360 | return; |
| 361 | }; |
| 362 | |
| 363 | if !obj.contains_key("reasoning_blocks") |
| 364 | && !obj.contains_key("reasoning_text") |
| 365 | && !data.reasoning_blocks.is_empty() |
| 366 | { |
| 367 | let blocks: Vec<serde_json::Value> = data |
| 368 | .reasoning_blocks |
| 369 | .iter() |
| 370 | .map(|b| { |
| 371 | serde_json::json!({ |
| 372 | "text": b.text, |
| 373 | "duration_ms": b.duration_ms, |
| 374 | }) |
| 375 | }) |
| 376 | .collect(); |
| 377 | obj.insert( |
| 378 | "reasoning_blocks".to_string(), |
| 379 | serde_json::Value::Array(blocks), |
| 380 | ); |
| 381 | } |
no test coverage detected