bird-agents: drop --ingest-on-startup on OTF slayer MCP launch (DEV-1508) - #11
Conversation
…508) Cloud run on LiveSQLBench museum (claude_sdk_otf, Opus, one-shot) had the per-task slayer stdio MCP stuck in status='pending' for 5 of 10 museum tasks; the agent silently lost every mcp__slayer__* tool and burned ~$17 / $39. Root cause: slayer_mcp_stdio_config unconditionally passed --ingest-on-startup, which on a warm OTF cache wastes 30-50s on schema reflection + sampled-value SELECT DISTINCTs + embedding hash-walks. The Claude Agent SDK has no MCP startup-timeout knob (unlike pydantic-ai's MCPServerStdio timeout), so it proceeds with whatever's connected and reports pending — no recovery. Fix: ingest_on_startup kwarg (default True; committed-reference adapters unchanged). The three OTF call sites pass False -- the OTF cache copied into per-task storage IS the post-ingestion state. To keep the safety net Codex flagged (a cache built under a different slayer or embedding model would now silently serve stale storage), persist a second _impl_fp.txt marker holding only the impl-version components (slayer __version__ + embedding model). On reuse, the full fingerprint stays marker-presence-gated (cloud lifecycle preserved) but the impl half IS recomputed and forces a rebuild on drift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds a keyword-only ChangesOn-the-fly encoding optimization
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/test_slayer_otf_cache.py (1)
559-570: 💤 Low valueConsider adding a test for legacy cache migration (no
_impl_fp.txt).The
_impl_ok()helper returnsFalsewhen_impl_fp.txtis missing (treating legacy caches as mismatched). An explicit test for this scenario would strengthen coverage: create a cache dir with only_cache_fp.txtand_kb_rows.json(simulating a pre-split cache), then verifyensure_db_cachetriggers a rebuild.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_slayer_otf_cache.py` around lines 559 - 570, Add a test that simulates a legacy cache missing IMPL_MARKER by creating a temp cache directory containing only the old `_cache_fp.txt` and `_kb_rows.json` files and asserting that `_impl_ok()` returns False and `ensure_db_cache(...)` performs a rebuild (e.g., replaces files or returns the rebuild indicator). Locate helpers `_impl_ok()` and `ensure_db_cache()` in the test module, create the legacy files with valid contents matching the old fingerprint, call `_impl_ok(cache_dir)` to assert False, then call `ensure_db_cache(cache_dir, ...)` and assert the expected rebuild behavior (new `_impl_fp.txt` created or rebuild flag/side-effect observed).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/test_slayer_otf_cache.py`:
- Around line 559-570: Add a test that simulates a legacy cache missing
IMPL_MARKER by creating a temp cache directory containing only the old
`_cache_fp.txt` and `_kb_rows.json` files and asserting that `_impl_ok()`
returns False and `ensure_db_cache(...)` performs a rebuild (e.g., replaces
files or returns the rebuild indicator). Locate helpers `_impl_ok()` and
`ensure_db_cache()` in the test module, create the legacy files with valid
contents matching the old fingerprint, call `_impl_ok(cache_dir)` to assert
False, then call `ensure_db_cache(cache_dir, ...)` and assert the expected
rebuild behavior (new `_impl_fp.txt` created or rebuild flag/side-effect
observed).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1dc01174-8d44-489f-9eda-026fb9ebbbd9
📒 Files selected for processing (9)
src/bird_interact_agents/agents/claude_sdk_otf/agent.pysrc/bird_interact_agents/agents/pydantic_ai_otf_encode/agent.pysrc/bird_interact_agents/agents/pydantic_ai_recursive/agent.pysrc/bird_interact_agents/harness.pysrc/bird_interact_agents/slayer_otf/cache.pytests/test_claude_sdk_otf_agent.pytests/test_mcp_startup_timeout.pytests/test_slayer_mcp_config.pytests/test_slayer_otf_cache.py
…EV-1508) Codex review of PR #11 caught: when `_impl_ok()` returns False but the marker is present, the rebuild builds into tmp_dir but `os.rename(tmp_dir, target)` fails with ENOTEMPTY (target's old marked dir is non-empty). The OSError handler then treats the still-stale marked target as a "peer won the race" success and returns it — defeating the impl-fp drift protection on the very first reuse after a slayer-version / embedding-model bump or on any legacy cache built before the impl-fp split landed. Two-part fix: - Pre-rename wipe condition also fires when `_impl_ok()` is False, so the drift case takes the same path as the legacy / force / no-marker case. - OSError handler additionally re-checks `_impl_ok()` on the peer's dir; a peer that built under a stale impl is rejected by re-raising. Three new tests pin the contract: - impl mismatch must REPLACE on-disk content (was passing trivially before: phase counters incremented because work happened in tmp_dir, but the returned cache_dir still pointed at the stale marked target). - Legacy cache without _impl_fp.txt triggers rebuild + marker repopulation. - Cross-process peer with mismatched impl fp is rejected, not silently used. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex second-pass review of PR #11 flagged: pydantic_ai_otf_encode operates against an `ensure_db_reference` artifact (NOT `ensure_db_cache`), and the reference's reuse path is presence-gated on `_reference_fp.txt` with no impl-fingerprint check. The DEV-1508 split only landed in `cache.py`, so dropping `--ingest-on-startup` here removes the last refresh path for a reference built under an older slayer / embedding model (e.g. a downloaded artifact from cloud combo 3). Revert this one call site. The encoder isn't on the field-bug critical path (claude_sdk_otf is); pydantic-ai's 1800s MCP startup timeout absorbs the 30-50s wait. When the impl-fp split is extended to `ensure_db_reference` in a follow-up, this caller can flip back. The corresponding test flips polarity: encoder MUST keep ingest_on_startup=True until reference_build also carries the impl guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…coder (DEV-1508) Codex follow-up of PR #11: the harness docstring listed pydantic_ai_otf_encode among the OTF callers that pass ingest_on_startup=False, but the previous commit reverted that adapter to the default ingest path. Update the docstring so the contract is consistent across files and the next person editing this won't re-introduce the encoder change without first extending the impl-fp split to ensure_db_reference. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mcp-fails-to-start-on-livesqlbench-museum-tasks-4-of # Conflicts: # src/bird_interact_agents/harness.py # tests/test_claude_sdk_otf_agent.py
…act sibling (DEV-1508) Codex review of PR #11 caught a regression I introduced during the origin/main merge: I had `cp`'d a pre-merge backup of `claude_sdk_otf_ainteract/agent.py` over the merged file, clobbering the DEV-1511 diagnostic-fields fix that had just landed on main. Restore the DEV-1511 contract: the happy path AND the exception path both propagate `submission_status`, `predicted_result_json`, `gold_result_json`, `phase1_observation`, `phase2_observation`, and the audited/original variants from `_ctx_var["result"]`; the exception path reads from the LOCAL `ctx_dict` (not `_ctx_var.get()`) to avoid stale-ContextVar leakage from a prior task in the same async context. Keeps the DEV-1508 `ingest_on_startup=False` change on the slayer MCP launch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Profiling the full non-integration suite (pytest under cProfile) showed two dominant costs that aren't inherent test work: 1. ~23s of repeated `yaml.safe_load` on the committed `slayer_models/<db>/` trees. The five-adapter prompt-parity test alone was loading the same models off disk 5x per parametrize, and `build_slayer_c_interact_prompt` was the single biggest call site (286 get_model calls, 5.7s cumulative). 2. ~18s spent spawning three near-identical Python sub-interpreters just to inspect sys.modules for adapter import-isolation boundaries. Add a session-scoped autouse fixture in tests/conftest.py that memoizes `YAMLStorage.get_model` / `get_datasource` by `(abs_path, st_mtime_ns)`. Mtime-keyed so any `save_model` / `edit_model` write naturally invalidates its entry on the next read. Returned by reference; no test today mutates the returned Pydantic model in place, and the cache docstring flags the contract for future tests. Consolidate the three adapter import-isolation subprocess tests (`test_pydantic_ai_agent_imports_without_claude_sdk` and the two `test_import_does_not_pull_pydantic_ai_adapter_packages` siblings) so they share one `import_isolation_results` fixture that runs all three boundary checks in a single child interpreter with sys.modules cleared between checks. Each test keeps its own assertion and ID so a failure still attributes to the right boundary. Suite wall-time: 118s -> 58.5s (51% faster); all 1613 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mcp-fails-to-start-on-livesqlbench-museum-tasks-4-of
…(DEV-1508) Codex follow-up of PR #11 caught that the seven DEV-1511 tests covering diagnostic-field propagation on the ainteract Claude-SDK adapter were missing from this branch — collateral from an earlier `cp /tmp/...` backup-overwrite during the previous origin/main merge that clobbered not just the production code (already fixed in a7ec0a1) but also the test fixture infrastructure (`_make_fake_client` prefill kwargs and `_stub_env` prefill kwargs) plus the seven DEV-1511 tests built on top. Restore them verbatim from origin/main while keeping this PR's `_fake_slayer_mcp` `slayer_mcp_kw` capture (needed for `test_run_task_passes_ingest_on_startup_false_to_slayer_mcp`). Coverage re-added: - test_run_task_propagates_diagnostic_fields_on_happy_path - test_run_task_propagates_phase2_observation - test_run_task_propagation_defaults_to_none_when_never_submitted - test_run_task_exception_path_propagates_partial_result - test_run_task_exception_before_ctx_set_yields_empty_diagnostics - test_run_task_exception_path_isolated_from_stale_context - plus the `_full_prefill` helper Suite: 1680 passed in 57.5s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/bird_interact_agents/harness.py (1)
699-699: 💤 Low valueConsider using
paths.*_root()for repo root resolution.Line 699 manually calculates the repo root as
Path(__file__).resolve().parent.parent.parent. As per coding guidelines, reachable-from-Python paths should be resolved viabird_interact_agents.paths.*_root()helpers which anchor at the main checkout via git's common dir. This ensures the code works correctly from any checkout (canonical orgit worktree).♻️ Proposed refactor using paths helper
Check if there's an existing
paths.repo_root()or similar helper. If available:- repo_root = Path(__file__).resolve().parent.parent.parent + from bird_interact_agents import paths + repo_root = paths.repo_root()If no such helper exists, the current approach is acceptable for this localized use case.
As per coding guidelines: "All reachable-from-Python data paths must be resolved via
bird_interact_agents.paths.*_root()helpers which anchor at the main checkout via git's common dir."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/bird_interact_agents/harness.py` at line 699, Replace the manual repo root computation (repo_root = Path(__file__).resolve().parent.parent.parent) with the central path helper; locate where repo_root is set in harness.py and call the appropriate bird_interact_agents.paths.*_root() helper (e.g., bird_interact_agents.paths.repo_root()) so the repo root is anchored via git common dir and works with worktrees.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/bird_interact_agents/harness.py`:
- Line 699: Replace the manual repo root computation (repo_root =
Path(__file__).resolve().parent.parent.parent) with the central path helper;
locate where repo_root is set in harness.py and call the appropriate
bird_interact_agents.paths.*_root() helper (e.g.,
bird_interact_agents.paths.repo_root()) so the repo root is anchored via git
common dir and works with worktrees.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 19e8e88a-bcbf-44ca-84dc-03e8f211cdba
📒 Files selected for processing (11)
src/bird_interact_agents/agents/claude_sdk_otf/agent.pysrc/bird_interact_agents/agents/claude_sdk_otf_ainteract/agent.pysrc/bird_interact_agents/agents/pydantic_ai_otf_encode/agent.pysrc/bird_interact_agents/harness.pysrc/bird_interact_agents/slayer_otf/cache.pytests/conftest.pytests/test_claude_sdk_otf_agent.pytests/test_claude_sdk_otf_ainteract_agent.pytests/test_harness_imports.pytests/test_mcp_startup_timeout.pytests/test_slayer_otf_cache.py
✅ Files skipped from review due to trivial changes (1)
- src/bird_interact_agents/agents/pydantic_ai_otf_encode/agent.py
🚧 Files skipped from review as they are similar to previous changes (2)
- src/bird_interact_agents/agents/claude_sdk_otf/agent.py
- src/bird_interact_agents/slayer_otf/cache.py
Summary
status='pending'for 5 of 10 LiveSQLBench museum tasks underclaude_sdk_otf. Agent silently lost everymcp__slayer__*tool; ~$17 / $39 burned on a single run.slayer_mcp_stdio_configunconditionally passed--ingest-on-startup. On a warm OTF cache that's 30-50s of wasted schema reflection +SELECT DISTINCTsample-value refresh + embedding hash-walks. The Claude Agent SDK has no MCP startup-timeout knob (unlike pydantic-ai'sMCPServerStdio(timeout=…)), so it proceeds withpending— no recovery.ingest_on_startupkwarg (defaultTrue; committed-reference adapters unchanged). Three OTF call sites passFalse:claude_sdk_otf,pydantic_ai_otf_encode,pydantic_ai_recursive(gated onslayer_setup)._impl_fp.txtmarker carrying only impl-version components (slayer.__version__+ active embedding model). On reuse, the full fingerprint stays presence-gated (cloud lifecycle preserved), but the impl half IS recomputed and forces a rebuild on drift.Test plan
tests/test_slayer_mcp_config.py— 4 new tests pin the kwarg contract (default includes flag;Falseomits it; env unaffected; keyword-only).tests/test_slayer_otf_cache.py— 7 new tests cover the impl-fp split (marker written; helper excludes inputs/root; embed-model change; slayer-version mismatch rebuild — all 3 phases; embed-model mismatch rebuild — all 3 phases; happy-path fast reuse; no full-fp recompute regression).tests/test_mcp_startup_timeout.py—_build_shared_slayer_servertests forpydantic_ai_otf_encodeandpydantic_ai_recursive; AST-level call-site test onpydantic_ai_recursive.run_taskthat enforces theslayer_setup != "on-the-fly"polarity.tests/test_claude_sdk_otf_agent.py— drivesrun_taskwith a spy that capturesslayer_mcp_stdio_config(**kw); assertsingest_on_startup is False.museum_{1,2,3,4,10}withclaude_sdk_otf+ Opus +one-shot+--slayer-setup on-the-fly. Assert every initSystemMessagereportsmcp_servers.slayer.status == 'connected'; ≥1mcp__slayer__inspect_modelper trajectory; noIngesting datasource 'museum'…lines in slayer stderr.🤖 Generated with Claude Code
Summary by CodeRabbit
Refactor
Tests
Documentation