diff --git a/.beans/claudbg-80fb--tui-sub-agent-transcript-always-empty-sessionid-se.md b/.beans/claudbg-80fb--tui-sub-agent-transcript-always-empty-sessionid-se.md new file mode 100644 index 0000000..4ec162d --- /dev/null +++ b/.beans/claudbg-80fb--tui-sub-agent-transcript-always-empty-sessionid-se.md @@ -0,0 +1,25 @@ +--- +# claudbg-80fb +title: 'TUI sub-agent transcript always empty: sessionId serde rename missing' +status: completed +type: bug +priority: normal +created_at: 2026-03-31T22:32:07Z +updated_at: 2026-03-31T22:33:04Z +--- + +When selecting a sub-agent in the TUI transcript screen, the transcript is always empty. + +Root cause: RawEntry.session_id has no #[serde(rename = "sessionId")] attribute. The JSONL field is camelCase ("sessionId") but serde only matches literal field names, so entry.session_id is always None after deserialization. + +This causes read_session_id_from_first_line (discovery.rs) to always return None, so AgentRef.session_id defaults to empty string. + +In load_transcript_for_agent, the find predicate uses s.session_id.starts_with(parent_session_id) — and in Rust, any_string.starts_with("") is always true. So it matches the first session in filesystem order (wrong session), fails to find the agent there, and returns with no entries loaded. + +Fix: Add #[serde(rename = "sessionId")] to RawEntry.session_id in models/session.rs. + +## Summary of Changes + +Added `#[serde(rename = "sessionId")]` to `RawEntry.session_id` in `src/models/session.rs`. + +This causes `read_session_id_from_first_line` to correctly parse the parent session UUID from agent JSONL files, so `AgentRef.session_id` is populated with the real UUID instead of defaulting to empty string. The TUI `load_transcript_for_agent` function then correctly locates the parent session and loads the agent transcript. diff --git a/.beans/claudbg-eej6--tui-session-select-panics-handleblock-on-called-fr.md b/.beans/claudbg-eej6--tui-session-select-panics-handleblock-on-called-fr.md new file mode 100644 index 0000000..027469a --- /dev/null +++ b/.beans/claudbg-eej6--tui-session-select-panics-handleblock-on-called-fr.md @@ -0,0 +1,21 @@ +--- +# claudbg-eej6 +title: 'TUI session select panics: Handle::block_on called from within async context' +status: completed +type: bug +priority: normal +created_at: 2026-03-31T22:15:18Z +updated_at: 2026-03-31T22:16:16Z +--- + +When selecting a session in the TUI, a panic occurs because load_transcript_for_session (and load_transcript_for_agent) calls tokio::runtime::Handle::current().block_on() from within the tokio::main async context. block_on cannot be called from within an async context — the thread is already driving async tasks. + +Root cause: transcript.rs lines 487-489 and 528-530 use Handle::current().block_on() which panics when the calling thread is already inside a tokio runtime (as it is, since main.rs uses #[tokio::main]). + +Fix: Wrap both block_on calls with tokio::task::block_in_place(), which tells the multi-thread scheduler to move other tasks off the current thread before blocking. + +## Summary of Changes + +Replaced with in both and (transcript.rs). + + signals the tokio multi-thread scheduler to move other tasks off the current OS thread before blocking it, which is required when blocking from synchronous code called within an async context. diff --git a/src/models/session.rs b/src/models/session.rs index 7502b87..1bd7931 100644 --- a/src/models/session.rs +++ b/src/models/session.rs @@ -18,6 +18,7 @@ pub struct RawEntry { #[serde(rename = "type")] pub entry_type: Option, /// UUID of this session. + #[serde(rename = "sessionId")] pub session_id: Option, /// UUID of the parent agent, if this is a sub-agent run. pub parent_session_id: Option, diff --git a/src/tui/screens/transcript.rs b/src/tui/screens/transcript.rs index be39853..1a308b2 100644 --- a/src/tui/screens/transcript.rs +++ b/src/tui/screens/transcript.rs @@ -483,10 +483,16 @@ pub fn load_transcript_for_session(session_id: &str, state: &mut AppState) { state.subagents = agents; } - // Read the JSONL file; block on the async reader using the current runtime. - let handle = tokio::runtime::Handle::current(); + // Read the JSONL file. We are in a synchronous function called from within + // the tokio::main async context, so Handle::block_on alone would panic. + // block_in_place tells the multi-thread scheduler to move other tasks off + // this thread before we block it. let file_path = sr.file_path.clone(); - if let Ok(entries) = handle.block_on(crate::parser::reader::read_session_file(&file_path)) { + let entries_result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(crate::parser::reader::read_session_file(&file_path)) + }); + if let Ok(entries) = entries_result { state.transcript_entries = entries; } } @@ -525,9 +531,12 @@ pub fn load_transcript_for_agent( let Some(ar) = agent_ref else { return }; - let handle = tokio::runtime::Handle::current(); let file_path = ar.file_path.clone(); - if let Ok(entries) = handle.block_on(crate::parser::reader::read_session_file(&file_path)) { + let entries_result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(crate::parser::reader::read_session_file(&file_path)) + }); + if let Ok(entries) = entries_result { state.transcript_entries = entries; } }