{"record":{"id":"09e97e3400a15084","repo":"Hmbown/CodeWhale","slug":"lifecycle-outbox-writer-task-is-gone","errorCode":null,"errorMessage":"lifecycle outbox writer task is gone","messagePattern":"lifecycle outbox writer task is gone","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/hooks/src/lifecycle_outbox.rs","lineNumber":245,"sourceCode":"    /// is bounded: a wedged consumer drops further events with a warning\n    /// (observability, not control flow) but fails a flush fast instead of\n    /// dropping its reply channel.\n    fn enqueue(self: &Arc<Self>, command: OutboxCommand) -> Result<()> {\n        let is_flush = matches!(command, OutboxCommand::Flush(_));\n        match self.sender.try_send(command) {\n            Ok(()) => {}\n            Err(TrySendError::Full(_)) if !is_flush => {\n                tracing::warn!(\n                    target: \"lifecycle_outbox\",\n                    queue_capacity = OUTBOX_QUEUE_CAPACITY,\n                    \"lifecycle event dropped: outbox queue is full\"\n                );\n            }\n            Err(TrySendError::Full(_)) => {\n                anyhow::bail!(\"lifecycle outbox queue is full; flush rejected\");\n            }\n            Err(TrySendError::Closed(_)) => {\n                anyhow::bail!(\"lifecycle outbox writer task is gone\");\n            }\n        }\n        self.ensure_writer_spawned();\n        Ok(())\n    }\n\n    fn ensure_writer_spawned(self: &Arc<Self>) {\n        if self.writer_spawned.load(Ordering::Acquire) {\n            return;\n        }\n        let _guard = self\n            .spawn_lock\n            .lock()\n            .unwrap_or_else(|poisoned| poisoned.into_inner());\n        if self.writer_spawned.load(Ordering::Acquire) {\n            return;\n        }\n        let Ok(handle) = tokio::runtime::Handle::try_current() else {","sourceCodeStart":227,"sourceCodeEnd":263,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/hooks/src/lifecycle_outbox.rs#L227-L263","documentation":"enqueue's bounded channel send returned TrySendError::Closed, meaning the spawned lifecycle outbox writer task no longer exists (it panicked, was aborted, or the receiver was dropped). The library surfaces this instead of silently losing events.","triggerScenarios":"Calling enqueue after the writer task died — e.g. runtime shutdown, a panic in the writer's flush loop, or explicit abort of the task.","commonSituations":"Application shutting down while events are still emitted; a bug or I/O failure panicking the writer task; runtime teardown ordering issues.","solutions":["Restart the outbox/writer (ensure_writer_spawned re-arms it; recreate the outbox instance if it stays closed)","Find and fix the panic in the writer task's flush loop from the task's logs","Avoid emitting events after runtime shutdown; gate emitters on shutdown signals"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"if outbox.writer_closed() {\n    eprintln!(\"outbox writer gone; re-spawning before emitting\");\n    outbox.ensure_writer_spawned();\n}","typeGuard":null,"tryCatchPattern":"match outbox.enqueue(event) {\n    Err(e) if e.to_string().contains(\"writer task is gone\") => {\n        outbox.ensure_writer_spawned();\n        outbox.enqueue(event).or_else(|_| { dead_letter_log(event); Ok(()) })?;\n        Ok(())\n    }\n    other => other,\n}","preventionTips":["Wrap the writer task's body so panics are caught and logged, then respawn","Gate event emitters on runtime shutdown state","Alert on writer task exit in long-running sessions"],"tags":["queue","async","task-panic"],"backgroundTag":"internal-invariant-violation","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}