{"record":{"id":"978e37c1f364d424","repo":"pydantic/monty","slug":"checked-above","errorCode":null,"errorMessage":"checked above","messagePattern":"checked above","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/monty-pool/src/checkout.rs","lineNumber":806,"sourceCode":"                    Ok(obj) => ResumeValue::Return(obj),\n                    Err(err) => ResumeValue::Error(err.into_exception()),\n                };\n                match self.resume(value, &mut *on_print).await {\n                    // The result never reached the child (too large or too deep\n                    // to encode), so the call is still suspended — answer it\n                    // with that error instead, letting the sandbox raise a\n                    // catchable exception rather than stranding the feed. Only\n                    // a rejection before the frame is written leaves `pending`\n                    // set, so this cannot catch a genuine sandbox exception.\n                    Err(PoolError::Runtime(exc)) if self.pending.is_some() => {\n                        self.resume(ResumeValue::Error(exc), on_print).await.map(Some)\n                    }\n                    other => other.map(Some),\n                }\n            }\n            MountCallOutcome::NotHandled(call) => {\n                let Some(Pending::Call { os_call, .. }) = &mut self.pending else {\n                    unreachable!(\"checked above\");\n                };\n                *os_call = Some(Box::new(call));\n                Ok(None)\n            }\n        }\n    }\n\n    /// Answers a [`TurnEvent::NameLookup`] with a [`NameLookupResult`] (or a\n    /// `MontyObject`, an `Option<MontyObject>` where `None` is `Undefined`, or\n    /// a `MontyException` for `Error`): a value resolves the name; `Undefined`\n    /// makes the sandbox raise `NameError` for a plain lookup, or\n    /// `AttributeError` when the lookup carried an `object_id` (a lazy\n    /// attribute on a host-backed object — a class instance or class type);\n    /// `Error` raises the host's exception in the sandbox, bypassing\n    /// `hasattr()` / `getattr()` defaults the way a raising property does.\n    pub async fn resume_name_lookup(\n        &mut self,\n        result: impl Into<NameLookupResult>,","sourceCodeStart":788,"sourceCodeEnd":824,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty-pool/src/checkout.rs#L788-L824","documentation":"This `unreachable!()` in `MountTable`-backed session resume (`resume_from_mounts`) asserts that when a `MountCallOutcome::NotHandled` arrives, `self.pending` is still the `Pending::Call` variant holding the suspended `os_call`. The variant was checked earlier in the same method, so firing indicates the pending state was mutated between the check and this arm — a broken state machine invariant in `monty-pool`. It is unreachable for library users driving the pool through its public API.","triggerScenarios":"Only when code modifies `self.pending` (e.g. resolving/clearing a pending call) between the earlier `Pending::Call` check and the `NotHandled` arm, or when a new `Pending` variant is introduced without updating this method.","commonSituations":"Hit during development of `monty-pool`: adding new pending-turn kinds (timers, futures), refactoring the suspension/resume state machine, or reordering the outcome match arms.","solutions":["Re-read `self.pending` immediately before the `NotHandled` handling instead of relying on an earlier check","Audit the method for any path that replaces or clears `self.pending` between the first match and this arm","Replace the assertion with a logged internal-error / crashed-worker result so a latent bug cannot panic the parent pool","Run `cargo test -p monty-pool` including crash/recovery tests"],"exampleFix":"// before\nlet Some(Pending::Call { os_call, .. }) = &mut self.pending else {\n    unreachable!(\"checked above\");\n};\n// after\nlet Some(Pending::Call { os_call, .. }) = &mut self.pending else {\n    return Err(PoolError::internal(\n        \"pending turn is not a call when handling NotHandled\"));\n};","handlingStrategy":"type-guard","validationCode":"// Check pending state right before use, not earlier\nif !matches!(self.pending, Some(Pending::Call { .. })) {\n    return Err(PoolError::internal(\"pending turn is not a call\"));\n}","typeGuard":"fn pending_call(pending: &Option<Pending>) -> Option<&OsCall> {\n    match pending {\n        Some(Pending::Call { os_call, .. }) => os_call.as_ref(),\n        _ => None,\n    }\n}","tryCatchPattern":"// Replace the panic with a recoverable internal error so the pool can replace the worker\nlet Some(Pending::Call { os_call, .. }) = &mut self.pending else {\n    return Err(PoolError::internal(\"NotHandled with no pending call\"));\n};","preventionTips":["Re-verify invariants at the point of use rather than trusting earlier checks in long methods","Model the pending state as an enum consumed by value (`std::mem::take`) so the compiler enforces single-handling","Add pool tests that cover NotHandled outcomes for every pending-turn kind","Never mutate `self.pending` between the initial dispatch and outcome handling"],"tags":["rust","async","state-machine","pool","internal-assert"],"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"}