{"record":{"id":"ab36915dde830a61","repo":"Hmbown/CodeWhale","slug":"sub-agent-transcript-history-shrank-from-to","errorCode":null,"errorMessage":"sub-agent transcript history shrank from {} to {} messages","messagePattern":"sub-agent transcript history shrank from (.+?) to (.+?) messages","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/subagent/mod.rs","lineNumber":7518,"sourceCode":"        let relative_path = subagent_transcript_artifact_relative_path(agent_id);\n        let path = checked_subagent_transcript_artifact_path(&state_root, agent_id)?;\n        let header = json!({\n            \"kind\": \"subagent_transcript_header\",\n            \"schema_version\": SUBAGENT_TRANSCRIPT_ARTIFACT_SCHEMA_VERSION,\n            \"agent_id\": agent_id,\n        });\n        create_private_subagent_transcript(&state_root, &path, &json_line(&header)?)?;\n        Ok(Self {\n            state_root,\n            path,\n            relative_path,\n            persisted_messages: 0,\n        })\n    }\n\n    fn sync_messages(&mut self, messages: &[Message], durable: bool) -> Result<()> {\n        if messages.len() < self.persisted_messages {\n            return Err(anyhow!(\n                \"sub-agent transcript history shrank from {} to {} messages\",\n                self.persisted_messages,\n                messages.len()\n            ));\n        }\n\n        let mut encoded = Vec::new();\n        for (index, message) in messages.iter().enumerate().skip(self.persisted_messages) {\n            encoded.extend(json_line(&json!({\n                \"kind\": \"message\",\n                \"index\": index,\n                \"message\": message,\n            }))?);\n        }\n\n        if !encoded.is_empty() || durable {\n            append_private_subagent_transcript(&self.state_root, &self.path, &encoded, durable)?;\n        }","sourceCodeStart":7500,"sourceCodeEnd":7536,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/tools/subagent/mod.rs#L7500-L7536","documentation":"The sub-agent transcript persister enforces an append-only invariant: `persisted_messages` counts the JSONL lines already on disk, and `sync_messages` errors when the incoming message slice is shorter than that count. Hitting it means the message history handed to the persister dropped messages that were already written — an integrity violation, not normal compaction. Treat it as a bug in history management or in a fork/replay path.","triggerScenarios":"Calling sync_messages with a Message vec shorter than persisted_messages: in-place history compaction that trims old messages, a fork/checkpoint restore that rebuilds a truncated context, or a refactor that filters messages before syncing.","commonSituations":"Wiring context-window compaction into the sub-agent loop; resuming from checkpoints that reconstruct fewer messages than the transcript contains; changing message filtering between syncs.","solutions":["Never remove or reorder messages passed to sync_messages — append only.","If on-disk history must shrink, create a fresh transcript artifact (new header, persisted_messages reset to 0) instead of syncing a shorter slice.","Audit compaction/fork paths: either preserve the full message count or fork before the first sync.","After an upgrade, confirm persisted_messages is initialized from the existing artifact's line count, not from an empty default."],"exampleFix":"// before: compaction trims history in place, then syncs\nlet compacted = messages[len/2..].to_vec();\npersister.sync_messages(&compacted, true)?; // Err 1202\n\n// after: shrinking on disk requires a new artifact\nlet fresh = TranscriptArtifact::create(state_root, agent_id, &header)?;\nfresh.sync_messages(&full_history, true)?; // full slice, append-only","handlingStrategy":"try-catch","validationCode":"// Before syncing, assert the append-only invariant yourself:\ndebug_assert!(messages.len() >= persister.persisted_messages(),\n    \"history would shrink: {} -> {}\", persister.persisted_messages(), messages.len());","typeGuard":"fn history_is_append_only(persisted: usize, incoming: &[Message]) -> bool {\n    incoming.len() >= persisted\n}","tryCatchPattern":"if let Err(e) = persister.sync_messages(&messages, durable) {\n    if e.to_string().contains(\"history shrank\") {\n        // integrity bug: do NOT truncate the file to match; log, keep the artifact,\n        // and rebuild the in-memory history from the artifact instead.\n    }\n}","preventionTips":["Treat transcript vectors as append-only; never compact in place.","Fork before the first sync, or create a fresh artifact when shrinking on disk.","Initialize persisted_messages from the artifact's line count on reopen."],"tags":["subagent","transcript","append-only","integrity","rust"],"backgroundTag":"append-only-log-violation","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}