fix(behaviors): unify case-resolution disposition; fix stale ledger override (#3101) - #3160
fix(behaviors): unify case-resolution disposition; fix stale ledger override (#3101)#3160sei-ahouseholder wants to merge 4 commits into
Conversation
…verride Closes #3101. FilterCsEmDimensionNode returning FAILURE on a missing case short-circuited the memory=False add_case_status Sequence before FinalizeCsFilterNode could clear BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE, stranding it on the process-global blackboard so a later execution could commit a ledger entry against another case's override payload. The root cause was that "what to do when a case evaporates underneath a node" was re-decided at ~78 call sites, drifting apart on both verdict (SUCCESS/FAILURE/fall-through) and loudness (silent/debug/ warning/error) with no unifying principle. Rather than patch the one node, unify the decision (ADR-0087): - Two canonical helpers in behaviors/helpers.py — require_case (Regime 1, authoritative coordination: one FAILURE at error level with a canonical feedback_message) and resolve_case_replica (Regime 2, replica-apply: absent local case is a routine SUCCESS-skip). Exposed as _require_case / _resolve_case_replica on all four DataLayer BT base classes. require_case's first tuple element is typed VulnerabilityCase (not Optional) so callers need no post-guard narrowing; the contract-unsoundness is contained to three type: ignore returns in that one helper. - Migrated every Regime-1 site across the behaviors tree to the shared helper. - Examined every remaining direct read_case and either migrated it or marked it with a category comment: Regime 2 replica/seed, Regime 3 case-under- construction, lenient guards, module-level resolvers, audit-best-effort ledger appends. - Complementary blackboard carve-out: BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE is execution-scoped and now reset in BTBridge.execute_with_setup's finally block via managed_keys on every outcome, so no short-circuit path can leak it forward (the actual #3101 fix; disposition policy prevents recurrence). Guard against re-drift: test/architecture/test_case_resolution_uses_helpers.py is an exact ratchet over the sanctioned direct-read_case allowlist (new site or stale entry both fail CI), plus synthetic detector-validation tests. Docs: ADR-0087 (case-resolution disposition policy), index entry, mkdocs nav, bt-pitfalls tick-boundary note, and the BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE lifetime docstring. Full suite green (8492 passed, 0 failed); flake8/mypy/black clean across the behaviors tree and the new test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two fan-out recipient collectors (CollectLogEntryRecipientsNode, CollectNonClosedLogEntryRecipientsNode) were flipped from a silent zero-recipient SUCCESS to a Regime-1 hard FAILURE on an unresolvable case (ADR-0087). The other three presumptive-bug flips were already pinned by tests (cs_invariant_guards x2, TransitionCStoFixReady); these two were not. Add TestFanoutCollectorsRequireCase: runs each collector with a valid log_entry but no seeded case and asserts FAILURE, the canonical "case '<id>' not found in DataLayer" feedback, and that no fanout_recipients list is written (the flip is a hard fail, not a silent []). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sei-ahouseholder
left a comment
There was a problem hiding this comment.
PR Triage: #3160 — fix(behaviors): unify case-resolution disposition; fix stale ledger override (#3101)
Linked issues: #3101 (FilterCsEmDimensionNode case-not-found FAILURE leaves BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE stale)
Changed files: 55 files — core/behaviors, docs, notes
CI status: ❌ failing (single failure — see below; pre-existing, resolved by sync)
Merge state: ✅ mergeable (UNSTABLE — the failing non-required check; branch is 36 commits behind main)
Base branch: main
Needs integration tests: yes
Findings
| # | Phase | Severity | Description | Outcome |
|---|---|---|---|---|
| phase11-ci-pytest-agents-md-ratchet-stale-base-0 | linter-ci | ❌ FAIL | CI pytest fails only on test_root_agents_md_within_target (AGENTS.md 405 > 400). PR does not touch AGENTS.md; already fixed on origin/main (391 lines). Merge with main is clean. Sync clears it. |
fix-now |
| phase8-ratchet-scope-granularity-blindspot-0 | code-review | Ratchet keys by (path, scope): a 2nd read_case in an already-allowlisted scope evades detection, weakening the "exact" claim |
fix-now | |
| phase8-require-case-datalayer-none-no-log-0 | code-review | require_case datalayer-None branch omits the logger.error its two sibling failure branches emit (helpers.py:191) |
fix-now | |
| phase8-nested-exec-nonmanaged-key-leak-0 | code-review | 🎫 NEW-ISSUE | Pre-existing/out-of-scope: execute_with_setup restores only managed_keys; nested executions clobber /activity and context keys without restore. Not introduced by this PR. |
defer-ask |
Total: 1 FAIL · 2 IMPROVE · 1 NEW-ISSUE
Assessment: The core fix is sound. The code-review sub-agent found no hard correctness or security bug — the require_case/resolve_case_replica design and its contained type: ignore unsoundness are correct, the ledger_payload_object_override reset is correct on all exit paths (including nested RLock executions), the ~50 node migrations preserve or intentionally-and-consistently tighten control flow, and the ratchet catches both drift directions. ADR-0087 follows the template; index + nav + bt-pitfalls.md are updated. The sole CI failure is a stale-base artifact already fixed on main.
Triage artifact: .claude/pr-3160-triage.json
Next step: /pr-execute or /pr-ship applies fixes.
- code-review — require_case datalayer-None branch now logs at error level, matching its two sibling failure branches (canonical single-report goal) - code-review — case-resolution ratchet counts read_case calls per scope, so a second unsanctioned call inside an already-allowlisted scope trips CI (closes the set-collapse blind spot in the test's 'exact' guarantee) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…imension-stale-override
sei-ahouseholder
left a comment
There was a problem hiding this comment.
PR Execute: #3160 — fix(behaviors): unify case-resolution disposition; fix stale ledger override (#3101)
Fixes applied: 2 commits (2 IMPROVE findings + base sync)
Excursions filed and fixed: 0
Deferred (you approved): 0
Halted (inversion, awaiting you): 0
Tests run: unit + integration
CI status: ✅ passing (22/22 checks)
Base sync: ✅ merged main @ fb2b89743 — clean, 0 conflicts
Fixed
| Finding | Commit | Closes |
|---|---|---|
phase11-ci-pytest-agents-md-ratchet-stale-base-0: CI Tests (pytest) failed only on test_root_agents_md_within_target (AGENTS.md 405 > 400); PR never touched AGENTS.md — resolved by syncing main (391 lines) |
fb2b89743 |
— |
phase8-ratchet-scope-granularity-blindspot-0: Harden the read_case ratchet to count calls per scope so a second unsanctioned read_case in an already-allowlisted scope trips CI |
25c0ad71a |
— |
phase8-require-case-datalayer-none-no-log-0: Add the missing logger.error(...) to require_case's datalayer-None failure branch (ADR-0087 canonical error-level report) |
25c0ad71a |
— |
Skipped
| Finding | Reason |
|---|---|
phase8-nested-exec-nonmanaged-key-leak-0: execute_with_setup restores only managed_keys; /activity + **context_data can leak across nested executions |
Pre-existing and out of scope of #3101 (whose ledger_payload_object_override reset is complete). Latent-only — every current nested execute_with_setup site passes only tree=/actor_id=/sync_port=, never activity=/**context_data, so nothing is clobbered today. The general fix (snapshot/restore all setup_tree-written keys) needs domain judgment beyond #3101. Filed as type:Concern #3161 (Project #24, Backlog) with evidence, per BW-07-009. |
Execute artifact: .claude/pr-3160-execute.json
Next step: run /pr-verify, or /pr-ship will continue automatically.
sei-ahouseholder
left a comment
There was a problem hiding this comment.
PR Verify: #3160 — fix(behaviors): unify case-resolution disposition; fix stale ledger override (#3101)
Overall verdict: ✅ READY-TO-MERGE
CI status: ✅ passing (22/22 checks)
Merge state: ✅ MERGEABLE (CLEAN) — base main
Base sync in execute: ✅ merged @ fb2b89743 (0 conflicts resolved)
Integrity check: ✅ all 4 findings accounted for
Finding Verdicts
| Finding | Severity | Outcome | Verdict |
|---|---|---|---|
| phase11-ci-pytest-agents-md-ratchet-stale-base-0 | ❌ FAIL | fixed @ fb2b89743 (sync main) |
✅ CONFIRMED |
| phase8-ratchet-scope-granularity-blindspot-0 | fixed @ 25c0ad71a |
✅ CONFIRMED | |
| phase8-require-case-datalayer-none-no-log-0 | fixed @ 25c0ad71a |
✅ CONFIRMED | |
| phase8-nested-exec-nonmanaged-key-leak-0 | 🎫 NEW-ISSUE | skipped — pre-existing Concern #3161 | 📋 NOTED |
Spot-check detail (verified at HEAD, not just the commit diff):
- AGENTS.md ratchet: the sole CI failure (
test_root_agents_md_within_target, 405 > 400) was a stale-base artifact — the PR never touched AGENTS.md, andmainhad already condensed it. At HEADAGENTS.mdis 391 lines andTests (pytest)is green. - read_case ratchet hardening:
test_case_resolution_uses_helpers.pynow countsread_casecalls per scope (Counter,_read_case_counts,self.counts) so a second unsanctioned call in an already-allowlisted scope trips CI. Present at HEAD. - require_case log line: the datalayer-None failure branch now emits
node.logger.error(...)(helpers.py:193), matching the other two failure branches. Present at HEAD. - Nested-execution key leak: pre-existing, out of scope of #3101, latent-only (no current nested
execute_with_setupsite passesactivity=/**context_data). Tracked as type:Concern #3161 (open, Project #24) with evidence per BW-07-009 — correctly not folded into a #3101 fix.
Artifacts cleaned up.
Closes #3101.
The defect
FilterCsEmDimensionNodereturningFAILUREon a missing case short-circuited thememory=Falseadd_case_statusSequence beforeFinalizeCsFilterNodecould clearBB_LEDGER_PAYLOAD_OBJECT_OVERRIDE. The override was left stranded on the process-global blackboard, so a subsequent execution could commit a ledger entry against another case's override payload.The root cause was structural: "what to do when a case evaporates underneath a node" was re-decided at ~78 call sites across
vultron/core/behaviors/, drifting apart on both verdict (SUCCESS / FAILURE / fall-through) and loudness (silent / debug / warning / error) with no unifying principle.The fix — unify the decision (ADR-0087)
Rather than patch the one node, this unifies the disposition into a single, role-indexed decision:
behaviors/helpers.py, exposed as_require_case/_resolve_case_replicaon all four DataLayer BT base classes:require_case— Regime 1 (authoritative coordination): one canonicalFAILUREaterrorlevel with a canonicalfeedback_message. Its first tuple element is typedVulnerabilityCase(notOptional) so callers need no post-guard narrowing; the contract-unsoundness is contained to threetype: ignorereturns in that one helper.resolve_case_replica— Regime 2 (replica-apply): an absent local case is a routineSUCCESS-skip (partial replica; SYNC-02-002 / ADR-0073).read_caseand either migrated it or marked it with a category comment: Regime 2 replica/seed, Regime 3 case-under-construction, lenient guards, module-level resolvers, audit-best-effort ledger appends.BB_LEDGER_PAYLOAD_OBJECT_OVERRIDEis execution-scoped and is now reset inBTBridge.execute_with_setup'sfinallyblock viamanaged_keyson every outcome — so no short-circuit path can leak it forward. The disposition policy then prevents the class of bug from recurring.Guard against re-drift
test/architecture/test_case_resolution_uses_helpers.pyis an exact ratchet over the sanctioned direct-read_caseallowlist: a new site or a stale allowlist entry both fail CI. Includes synthetic detector-validation tests.Docs
ADR-0087 (case-resolution disposition policy) + index entry + mkdocs nav;
bt-pitfalls.mdtick-boundary note;BB_LEDGER_PAYLOAD_OBJECT_OVERRIDElifetime docstring.Verification
flake8/mypy(193 files) /blackclean across the behaviors tree and the new test.🤖 Generated with Claude Code