{"record":{"id":"c9481332d5b4bac4","repo":"gitbutlerapp/gitbutler","slug":"transcript-contains-malformed-json-before-the-fina","errorCode":null,"errorMessage":"transcript contains malformed JSON before the final record","messagePattern":"transcript contains malformed JSON before the final record","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-agentlog/src/transcript.rs","lineNumber":47,"sourceCode":"            session_id: None,\n            provider: match agent {\n                Agent::Codex => None,\n                Agent::Claude => Some(\"anthropic\".to_string()),\n            },\n            model: None,\n            tool_version: None,\n            thread_source: None,\n            records: Vec::new(),\n        };\n        let mut codex_tool_names = HashMap::new();\n        let mut codex_spawn_prompts = HashSet::new();\n        let mut claude_tool_names = HashMap::new();\n\n        while let Some((index, trimmed)) = raw_records.next() {\n            let parsed = match serde_json::from_slice::<Value>(trimmed) {\n                Ok(parsed) => parsed,\n                Err(_) if raw_records.peek().is_none() => continue,\n                Err(_) => bail!(\"transcript contains malformed JSON before the final record\"),\n            };\n\n            let record = match agent {\n                Agent::Codex => {\n                    transcript.apply_codex_metadata(&parsed);\n                    ParsedRecord::from_codex_source(\n                        index,\n                        trimmed,\n                        parsed,\n                        &mut codex_tool_names,\n                        &mut codex_spawn_prompts,\n                        transcript.thread_source.as_deref(),\n                    )\n                }\n                Agent::Claude => {\n                    transcript.apply_claude_metadata(&parsed);\n                    ParsedRecord::from_claude_source(index, trimmed, parsed, &mut claude_tool_names)\n                }","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-agentlog/src/transcript.rs#L29-L65","documentation":"`TranscriptBatch::parse` splits the raw agent transcript snapshot (JSONL from Claude Code or Codex) into lines and parses each as JSON. The final line may be malformed because the agent is still appending to the file (writers append whole lines, so only the tail can be partial); any malformed line before the last one bails with this message. It therefore signals mid-file corruption, not an in-progress writer.","triggerScenarios":"Feeding `but agentlog hook` (capture) a transcript snapshot where a non-final line fails `serde_json::from_slice` — e.g. two processes interleaving appends to the same session file, a rotated/truncated file, or a non-JSONL file passed as the snapshot.","commonSituations":"Parallel agent runs sharing one session/transcript file; a log-rotation or backup tool truncating or interleaving the file mid-write; capturing a file that is not NDJSON (e.g. a JSON array export); disk corruption.","solutions":["Check whether the file is genuinely JSONL — one JSON object per line — and convert non-JSONL inputs before capture","If two agents write the same file, give each its own session file and re-run capture; interleaved lines cannot be recovered automatically","Recover by discarding or repairing the specific malformed lines (jq -c per line, or a repair script), then re-run the hook","If the corruption is a single truncation point, split at the last valid line and capture the valid prefix"],"exampleFix":"// before\nErr(_) => bail!(\"transcript contains malformed JSON before the final record\"),\n\n// after: skip corrupt non-final lines with a warning instead of aborting\nErr(_) => {\n    warn!(\"skipping malformed transcript record at index {index}\");\n    continue;\n}","handlingStrategy":"validation","validationCode":"// Validate the snapshot is JSONL before capture\nfor (i, line) in snapshot.split(|b| *b == b'\\n').enumerate() {\n    let last = /* caller knows if this is the final line */;\n    if !last && serde_json::from_slice::<serde_json::Value>(line.trim_ascii_end()).is_err() {\n        eprintln!(\"snapshot has malformed JSON at line {i}; repair before capture\");\n    }\n}","typeGuard":"fn jsonl_line_is_valid(line: &[u8]) -> bool {\n    line.iter().all(|b| b.is_ascii_whitespace())\n        || serde_json::from_slice::<serde_json::Value>(line).is_ok()\n}","tryCatchPattern":"match TranscriptBatch::parse(agent, &snapshot) {\n    Ok(batch) => batch,\n    Err(err) if err.to_string().contains(\"malformed JSON before the final record\") => {\n        // repair or split the file at the last valid line and retry\n        retry_with_valid_prefix(snapshot)\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["One agent process per transcript file; never let two writers append concurrently","Only append whole lines to JSONL transcripts (flush per line)","Validate third-party transcript files with a per-line JSON check before feeding them to the hook","Capture atomically: copy the snapshot and parse the copy so in-progress tail writes do not matter"],"tags":["json","jsonl","transcript-parsing","corruption","capture","rust"],"backgroundTag":"malformed-json-input","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}