{"record":{"id":"76abdba9743f350e","repo":"zeroclaw-labs/zeroclaw","slug":"needs-reassembly-implies-a-step-agent-alias","errorCode":null,"errorMessage":"needs_reassembly implies a step agent alias","messagePattern":"needs_reassembly implies a step agent alias","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/agent/turn/mod.rs","lineNumber":1953,"sourceCode":"                    // A step that delegates to a different agent must run AS that\n                    // agent — with that agent's own gated tools, policy, MCP\n                    // scope, provider binding, and runtime controls — not the\n                    // parent turn's. When the step names a different agent and a\n                    // reassembly handle is available, re-assemble (and memoize)\n                    // that agent's execution context; same-agent steps keep the\n                    // parent context unchanged.\n                    let step_alias = step.agent.as_deref();\n                    // `agent_alias` is this loop's EFFECTIVE identity: a\n                    // re-assembled sub-loop runs with its step agent as its own\n                    // alias, so this comparison is correct at every nesting\n                    // depth — a depth >= 2 step naming the outer agent compares\n                    // against the re-assembled child's alias and re-assembles\n                    // instead of inheriting the child's scope.\n                    let needs_reassembly = step_needs_reassembly(agent_alias, step_alias);\n                    let mut assembly_error: Option<anyhow::Error> = None;\n                    if needs_reassembly {\n                        let alias =\n                            step_alias.expect(\"needs_reassembly implies a step agent alias\");\n                        if let Some(reassembly) = sop_reassembly {\n                            if !exec_cache.contains_key(alias) {\n                                match assemble_owned_execution(\n                                    reassembly.config,\n                                    alias,\n                                    Arc::clone(&queued.engine),\n                                    queued.audit.clone(),\n                                    approval,\n                                )\n                                .await\n                                {\n                                    Ok(owned) => {\n                                        exec_cache.insert(alias.to_string(), owned);\n                                    }\n                                    Err(e) => assembly_error = Some(e),\n                                }\n                            }\n                        } else {","sourceCodeStart":1935,"sourceCodeEnd":1971,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/agent/turn/mod.rs#L1935-L1971","documentation":"While executing nested SOP steps, the turn engine computes needs_reassembly = step_needs_reassembly(agent_alias, step_alias), which is true only when step_alias is Some and differs from the current agent (turn/mod.rs:1603). This expect() asserts that consequence; a panic means the reassembly gate and the alias plumbing have desynchronized, an internal engine bug rather than user configuration.","triggerScenarios":"Running an SOP flow where a step delegates to a different agent alias. The panic would require needs_reassembly to be true while step_alias is None, impossible given the current gate implementation; realistically it appears only in forks where the gate or Option plumbing was modified.","commonSituations":"Running a patched zeroclaw-runtime after refactors to SOP step routing; version skew between crates that changed the reassembly gate contract; custom step-resolution logic injected before the turn loop.","solutions":["Reproduce with the SOP definition that triggers it and capture the backtrace, then check step_needs_reassembly and its callers around turn/mod.rs:1953 for divergence.","Upgrade zeroclaw to the latest version where the gate and invariants are kept in sync.","Report the issue upstream with the SOP config and stack trace since this is an engine invariant violation."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before running an SOP, validate that every delegating step names a defined agent:\nfor step in &sop.steps {\n    if let Some(alias) = &step.agent {\n        anyhow::ensure!(agent_registry.contains(alias),\n            \"SOP step '{}' delegates to unknown agent '{alias}'\", step.name);\n    }\n}","typeGuard":null,"tryCatchPattern":"// Contain engine invariant panics per turn so one bad flow cannot kill the daemon:\nlet outcome = std::panic::catch_unwind(std::assert_unwind_safe(|| run_sop(sop).await));\nif outcome.is_err() {\n    tracing::error!(\"SOP '{}' hit an internal invariant; report with backtrace\", sop.name);\n}","preventionTips":["Validate SOP step agent references against the agent registry before execution.","Run unmodified upstream releases; these expects guard engine-internal invariants, not config.","Capture backtraces (RUST_BACKTRACE=1) when reproducing to include in an upstream report."],"tags":["rust","sop","agent-turn","invariant","panic","internal"],"backgroundTag":"internal-invariant-violation","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}