gitbutlerapp/gitbutler · error

transcript contains malformed JSON before the final record

Error message

transcript contains malformed JSON before the final record

What it means

`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.

Source

Thrown at crates/but-agentlog/src/transcript.rs:47

            session_id: None,
            provider: match agent {
                Agent::Codex => None,
                Agent::Claude => Some("anthropic".to_string()),
            },
            model: None,
            tool_version: None,
            thread_source: None,
            records: Vec::new(),
        };
        let mut codex_tool_names = HashMap::new();
        let mut codex_spawn_prompts = HashSet::new();
        let mut claude_tool_names = HashMap::new();

        while let Some((index, trimmed)) = raw_records.next() {
            let parsed = match serde_json::from_slice::<Value>(trimmed) {
                Ok(parsed) => parsed,
                Err(_) if raw_records.peek().is_none() => continue,
                Err(_) => bail!("transcript contains malformed JSON before the final record"),
            };

            let record = match agent {
                Agent::Codex => {
                    transcript.apply_codex_metadata(&parsed);
                    ParsedRecord::from_codex_source(
                        index,
                        trimmed,
                        parsed,
                        &mut codex_tool_names,
                        &mut codex_spawn_prompts,
                        transcript.thread_source.as_deref(),
                    )
                }
                Agent::Claude => {
                    transcript.apply_claude_metadata(&parsed);
                    ParsedRecord::from_claude_source(index, trimmed, parsed, &mut claude_tool_names)
                }

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Check whether the file is genuinely JSONL — one JSON object per line — and convert non-JSONL inputs before capture
  2. If two agents write the same file, give each its own session file and re-run capture; interleaved lines cannot be recovered automatically
  3. Recover by discarding or repairing the specific malformed lines (jq -c per line, or a repair script), then re-run the hook
  4. If the corruption is a single truncation point, split at the last valid line and capture the valid prefix

Example fix

// before
Err(_) => bail!("transcript contains malformed JSON before the final record"),

// after: skip corrupt non-final lines with a warning instead of aborting
Err(_) => {
    warn!("skipping malformed transcript record at index {index}");
    continue;
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate the snapshot is JSONL before capture
for (i, line) in snapshot.split(|b| *b == b'\n').enumerate() {
    let last = /* caller knows if this is the final line */;
    if !last && serde_json::from_slice::<serde_json::Value>(line.trim_ascii_end()).is_err() {
        eprintln!("snapshot has malformed JSON at line {i}; repair before capture");
    }
}

Type guard

fn jsonl_line_is_valid(line: &[u8]) -> bool {
    line.iter().all(|b| b.is_ascii_whitespace())
        || serde_json::from_slice::<serde_json::Value>(line).is_ok()
}

Try / catch

match TranscriptBatch::parse(agent, &snapshot) {
    Ok(batch) => batch,
    Err(err) if err.to_string().contains("malformed JSON before the final record") => {
        // repair or split the file at the last valid line and retry
        retry_with_valid_prefix(snapshot)
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/c9481332d5b4bac4. Report an issue: GitHub.