{"record":{"id":"bed28518ca0de387","repo":"pydantic/monty","slug":"checked-ready-above","errorCode":null,"errorMessage":"checked Ready above","messagePattern":"checked Ready above","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/monty-proto/src/worker.rs","lineNumber":522,"sourceCode":"    fn handle_repl_feed(&mut self, feed: pb::Feed, sink: &mut dyn EventSink) -> pb::ChildEvent {\n        if let Err(event) = self.ensure_repl() {\n            return *event;\n        }\n        if !matches!(self.state, SessionState::Ready(_)) {\n            // ensure_repl left it un-Ready only when mid-suspension\n            return protocol_violation(\"Feed without a session ready for input\");\n        }\n        if !feed.skip_type_check\n            && let Some(event) = self.type_check_feed(&feed.code)\n        {\n            return event;\n        }\n        let inputs = match named_inputs(feed.inputs) {\n            Ok(inputs) => inputs,\n            Err(event) => return *event,\n        };\n        let SessionState::Ready(mut repl) = mem::replace(&mut self.state, SessionState::Configured(None)) else {\n            unreachable!(\"checked Ready above\");\n        };\n        // The working directory persists in the REPL (including `os.chdir`);\n        // the parent sends one only to switch it, and an older parent never does.\n        if !feed.cwd.is_empty() {\n            repl.set_cwd(&feed.cwd);\n        }\n        // snippets fed with skip_type_check never become type-check context:\n        // the caller explicitly excluded them from checking, so later snippets\n        // must not be checked against their (unchecked) bindings either\n        if !feed.skip_type_check\n            && let Some(state) = &mut self.type_check\n        {\n            state.pending_snippet = Some(feed.code.clone());\n        }\n        let mut print = ProtoPrint::new(sink, self.print_flush_interval);\n        let result = repl.feed_start(&feed.code, inputs, PrintWriter::Callback(&mut print));\n        let event = self.drive(result, &mut print);\n        print.drain();","sourceCodeStart":504,"sourceCodeEnd":540,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty-proto/src/worker.rs#L504-L540","documentation":"In `handle_repl_feed` (crates/monty-proto/src/worker.rs:522) the worker asserts the session state is `Ready` via `unreachable!(\"checked Ready above\")` after having already matched on the state earlier in the same request. The `mem::replace` pattern swaps the state out to `Configured(None)` and pattern-matches; if the state is anything but `Ready` here the worker panics. This is a child-side internal invariant: the earlier check and this extraction are expected to agree.","triggerScenarios":"A `ReplFeed` request handled by `handle` → `handle_repl_feed` where the state was `Ready` at the first check but the second `let SessionState::Ready(...) = mem::replace(...)` fails — only possible if the state machine was mutated between the two checks or the earlier check was removed/changed during refactoring.","commonSituations":"Protocol state-machine refactors in monty-proto; adding a new request kind that mutates `self.state` without updating the check order; a compromised/buggy child implementation diverging from the documented alternation contract.","solutions":["Read the surrounding `handle_repl_feed` code and restore the invariant: an `is Ready` check must immediately precede the `mem::replace` extraction with no intervening state mutation.","Replace the fragile double-check with a single extraction that returns `protocol_violation(...)` instead of panicking on mismatch.","Run the monty-proto worker tests (`cargo test -p monty-proto`) to confirm the REPL feed/resume alternation still holds.","If seen in a deployed worker, upgrade/replace the worker: a panic here crashes the subprocess and the pool discards it."],"exampleFix":"// before\nlet SessionState::Ready(mut repl) = mem::replace(&mut self.state, SessionState::Configured(None)) else {\n    unreachable!(\"checked Ready above\");\n};\n// after\nlet SessionState::Ready(mut repl) = mem::replace(&mut self.state, SessionState::Configured(None)) else {\n    return protocol_violation(\"feed requires a Ready session\");\n};","handlingStrategy":"validation","validationCode":"// Parent: only feed while the session is ready\nif (sessionState !== 'ready') throw new Error('feed requires a ready session');","typeGuard":"fn is_ready(s: &SessionState) -> bool { matches!(s, SessionState::Ready(_)) }","tryCatchPattern":"match worker_result { Ok(ev) => .., Err(e) if e.is_crash() => pool.replace_worker(), }","preventionTips":["Keep the state check and mem::replace extraction adjacent — no mutation between them.","Prefer protocol_violation returns over unreachable! in the child so bad frames cannot crash workers.","Test the feed/resume alternation explicitly in monty-proto worker tests."],"tags":["state-machine","internal-panic","worker"],"backgroundTag":"internal-invariant-violation","analyzedSha":"adc986b362e3961f407868cb118a99fe831b9e61","analyzedAt":"2026-09-13T19:19:18.698Z","contentChangedAt":"2026-09-13T19:19:18.698Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}