{"record":{"id":"6d9217357a74dae0","repo":"Hmbown/CodeWhale","slug":"initialized-audio-cursor","errorCode":null,"errorMessage":"initialized audio cursor","messagePattern":"initialized audio cursor","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/tui/pet_watch/worker.rs","lineNumber":248,"sourceCode":"                        frame.host_time_ms = time_ms;\n                        if let Some(target) = audio.filter(Target::active) {\n                            *deadline.lock().map_err(|_| rquickjs::Error::Unknown)? =\n                                Instant::now() + Duration::from_millis(500);\n                            if audio_cursor\n                                .as_ref()\n                                .is_none_or(|c| !c.target.same_stream(&target))\n                            {\n                                audio_cursor = Some(AudioCursor {\n                                    target: target.clone(),\n                                    sample: (frame.time_ms * audio::SAMPLE_RATE as f64 / 1000.0)\n                                        .floor()\n                                        as usize,\n                                    voices: Vec::new(),\n                                });\n                            }\n                            if audio_cursor\n                                .as_mut()\n                                .expect(\"initialized audio cursor\")\n                                .present(&ctx, &target, frame.time_ms)\n                                .is_err()\n                            {\n                                // Sound failure cannot stop telemetry or its recording.\n                                let _ = ctx.catch();\n                                target.fail();\n                                audio_cursor = None;\n                            }\n                            *deadline.lock().map_err(|_| rquickjs::Error::Unknown)? =\n                                Instant::now() + Duration::from_secs(5);\n                        } else {\n                            audio_cursor = None;\n                        }\n                        if let Ok(mut slot) = output.lock() {\n                            *slot = Some(Ok(frame));\n                        }\n                    }\n                }","sourceCodeStart":230,"sourceCodeEnd":266,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/tui/pet_watch/worker.rs#L230-L266","documentation":"Panic from `.expect(\"initialized audio cursor\")` on `audio_cursor.as_mut()` in the pet-watch worker's Advance command handler (crates/tui/src/tui/pet_watch/worker.rs:248). The cursor is guaranteed `Some` at that point because the branch directly above constructs it when it is `None` or when the audio target's stream changed. The expect documents an internal invariant: any `None` here means the initialization logic above was skipped or reordered.","triggerScenarios":"Only from refactoring: moving the `is_none_or(|c| !c.target.same_stream(&target))` initialization branch away from the `present(...)` call, changing `Target::active` filtering so the branch is bypassed while audio is still active, or introducing an early `continue`/`return` between construction and use.","commonSituations":"A code review change touching the Advance arm of `run()`; merging conflicting edits to the audio-cursor lifecycle; mistakenly treating the `else { audio_cursor = None }` reset as also covering the active-audio path.","solutions":["Restore the invariant: the cursor must be constructed immediately before use whenever `audio.filter(Target::active)` yields a target.","Replace the Option dance with a definite value — e.g. `let cursor = audio_cursor.take().unwrap_or_else(|| AudioCursor{...})` recomputed from the target — eliminating the expect.","If the panic reproduces on stock code, file a bug with the command sequence; it is a genuine invariant violation.","Add a comment or debug_assert linking the construction branch and this expect so future edits keep them together."],"exampleFix":"// before\nif audio_cursor.as_mut().expect(\"initialized audio cursor\").present(&ctx, &target, frame.time_ms).is_err() { ... }\n// after\nlet cursor = audio_cursor.get_or_insert_with(|| AudioCursor { target: target.clone(), sample: 0, voices: Vec::new() });\nif cursor.present(&ctx, &target, frame.time_ms).is_err() { ... }","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"let Some(cursor) = audio_cursor.as_mut() else {\n    unreachable!(\"audio cursor constructed in branch above whenever target is active\");\n};","tryCatchPattern":"let cursor = audio_cursor.get_or_insert_with(|| AudioCursor { target: target.clone(), sample: 0, voices: Vec::new() });\nif cursor.present(&ctx, &target, frame.time_ms).is_err() { /* fail target */ }","preventionTips":["Keep cursor construction and use in the same branch; do not insert early returns between them.","Prefer get_or_insert_with over Option + expect for just-in-time invariants.","Add a debug_assert documenting the pairing when refactoring this arm."],"tags":["invariant","panic","audio","worker"],"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-22T11:17:16.035Z"}