Skip to content

bird-agents: drop --ingest-on-startup on OTF slayer MCP launch (DEV-1508) - #11

Merged
ZmeiGorynych merged 9 commits into
mainfrom
egor/dev-1508-slayer-mcp-fails-to-start-on-livesqlbench-museum-tasks-4-of
May 30, 2026
Merged

bird-agents: drop --ingest-on-startup on OTF slayer MCP launch (DEV-1508)#11
ZmeiGorynych merged 9 commits into
mainfrom
egor/dev-1508-slayer-mcp-fails-to-start-on-livesqlbench-museum-tasks-4-of

Conversation

@ZmeiGorynych

@ZmeiGorynych ZmeiGorynych commented May 30, 2026

Copy link
Copy Markdown
Member

Summary

  • Fixes DEV-1508: per-task slayer stdio MCP stuck in status='pending' for 5 of 10 LiveSQLBench museum tasks under claude_sdk_otf. Agent silently lost every mcp__slayer__* tool; ~$17 / $39 burned on a single run.
  • Root cause: slayer_mcp_stdio_config unconditionally passed --ingest-on-startup. On a warm OTF cache that's 30-50s of wasted schema reflection + SELECT DISTINCT sample-value refresh + embedding hash-walks. The Claude Agent SDK has no MCP startup-timeout knob (unlike pydantic-ai's MCPServerStdio(timeout=…)), so it proceeds with pending — no recovery.
  • Fix: opt-out ingest_on_startup kwarg (default True; committed-reference adapters unchanged). Three OTF call sites pass False: claude_sdk_otf, pydantic_ai_otf_encode, pydantic_ai_recursive (gated on slayer_setup).
  • Safety net (Codex review): persist a second _impl_fp.txt marker 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

  • Unit: tests/test_slayer_mcp_config.py — 4 new tests pin the kwarg contract (default includes flag; False omits it; env unaffected; keyword-only).
  • Unit: 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).
  • Unit: tests/test_mcp_startup_timeout.py_build_shared_slayer_server tests for pydantic_ai_otf_encode and pydantic_ai_recursive; AST-level call-site test on pydantic_ai_recursive.run_task that enforces the slayer_setup != "on-the-fly" polarity.
  • Unit: tests/test_claude_sdk_otf_agent.py — drives run_task with a spy that captures slayer_mcp_stdio_config(**kw); asserts ingest_on_startup is False.
  • Full non-integration suite: 1515 passed, 94 skipped, 19 deselected.
  • Cloud smoke (per acceptance criteria): re-run museum_{1,2,3,4,10} with claude_sdk_otf + Opus + one-shot + --slayer-setup on-the-fly. Assert every init SystemMessage reports mcp_servers.slayer.status == 'connected'; ≥1 mcp__slayer__inspect_model per trajectory; no Ingesting datasource 'museum'… lines in slayer stderr.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor

    • Make SLAYER server startup ingestion configurable (can disable ingest-on-startup)
    • Tighten cache reuse with an implementation-fingerprint marker to avoid incompatible reuse
  • Tests

    • Add/expand tests covering startup-ingest behavior, server config, and cache impl-fingerprint races
    • Add import-isolation and performance helpers to test harness
  • Documentation

    • Clarify startup timeout and parameter behavior in SLAYER startup docs

Review Change Stack

…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>
@linear

linear Bot commented May 30, 2026

Copy link
Copy Markdown

DEV-1508

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a keyword-only ingest_on_startup flag to slayer MCP stdio config (default True) and threads False from on-the-fly agent paths; introduces an implementation-only _impl_fp.txt marker and matching reuse logic in the OTF cache; updates tests and fixtures accordingly.

Changes

On-the-fly encoding optimization

Layer / File(s) Summary
MCP ingest_on_startup flag implementation
src/bird_interact_agents/harness.py
slayer_mcp_stdio_config now accepts keyword-only ingest_on_startup: bool = True and appends --ingest-on-startup to args only when True; startup timeout docs updated.
MCP ingest_on_startup wiring across agent adapters
src/bird_interact_agents/agents/claude_sdk_otf/agent.py, src/bird_interact_agents/agents/claude_sdk_otf_ainteract/agent.py, src/bird_interact_agents/agents/pydantic_ai_otf_encode/agent.py, src/bird_interact_agents/agents/pydantic_ai_recursive/agent.py
Claude OTF adapters call slayer_mcp_stdio_config(..., ingest_on_startup=False) for per-task MCP servers; pydantic recursive helper accepts ingest_on_startup and passes self.slayer_setup != "on-the-fly" to it; pydantic OTF encode adds DEV-1508 comments.
MCP configuration unit tests
tests/test_slayer_mcp_config.py
Tests added for default inclusion of --ingest-on-startup, omission when ingest_on_startup=False, env/command invariance, and keyword-only enforcement.
Claude SDK OTF agent MCP tests
tests/test_claude_sdk_otf_agent.py
Test stubs updated to capture slayer_mcp_stdio_config kwargs and a new test asserts the agent sets ingest_on_startup=False; import-isolation test refactored to use fixture.
Claude OTF ainteract agent tests
tests/test_claude_sdk_otf_ainteract_agent.py
Stubs capture MCP kwargs; new test asserts ingest_on_startup=False; import-isolation test refactored to shared fixture.
MCP ingest_on_startup integration tests
tests/test_mcp_startup_timeout.py
Monkeypatch spy helper and tests verify pydantic OTF encode default True, recursive helper forwards False for on-the-fly, preserves True by default, and an AST-based test confirms the self.slayer_setup != "on-the-fly" wiring in run_task.
Test fixtures and import isolation
tests/conftest.py
Adds session-scoped caching for YAMLStorage reads and an import_isolation_results fixture that runs three import checks in a subprocess and reports results to tests.
Implementation fingerprint marker and reuse validation
src/bird_interact_agents/slayer_otf/cache.py
Adds _impl_fp.txt (_IMPL_MARKER) and _impl_fingerprint_of() (slayer version + embedding model); ensure_db_cache requires both _cache_fp.txt and matching _impl_fp.txt for reuse; writes _impl_fp.txt before _cache_fp.txt and tightens concurrency/rename handling.
Implementation fingerprint marker tests
tests/test_slayer_otf_cache.py
Extensive tests added: peer-rename race cases that require matching impl marker, stale-peer re-raise, marker write and migration tests, reuse rebuilds on slayer/version or embedding-model mismatches, fast-path no-rebuild when impl matches, and legacy-cache migration behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • MotleyAI/bird-agents#10: touches Claude SDK on-the-fly agent implementations and MCP wiring similar to the ingest_on_startup changes.
  • MotleyAI/bird-agents#9: earlier PR introducing ClaudeSDKOtfAgent; related to the same run_task MCP setup path.
  • MotleyAI/bird-agents#2: modifies on-the-fly SLAYER cache reuse logic and is code-level related to the impl-fingerprint changes.

Poem

🐰 I dug through caches, flags in paw,
Wrote tiny markers that say what I saw,
MCP whispers "don't ingest" for OTF runs,
Fingerprints split so reuse hums,
Hooray — faster hops for encoding fun!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective: dropping the --ingest-on-startup flag for on-the-fly (OTF) slayer MCP launches, and references the ticket DEV-1508. The changeset shows this flag is being made configurable and disabled for OTF workflows in multiple adapters, which directly aligns with the title's claim.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch egor/dev-1508-slayer-mcp-fails-to-start-on-livesqlbench-museum-tasks-4-of

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_slayer_otf_cache.py (1)

559-570: 💤 Low value

Consider adding a test for legacy cache migration (no _impl_fp.txt).

The _impl_ok() helper returns False when _impl_fp.txt is missing (treating legacy caches as mismatched). An explicit test for this scenario would strengthen coverage: create a cache dir with only _cache_fp.txt and _kb_rows.json (simulating a pre-split cache), then verify ensure_db_cache triggers 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0fb3ad5 and 6a02056.

📒 Files selected for processing (9)
  • src/bird_interact_agents/agents/claude_sdk_otf/agent.py
  • src/bird_interact_agents/agents/pydantic_ai_otf_encode/agent.py
  • src/bird_interact_agents/agents/pydantic_ai_recursive/agent.py
  • src/bird_interact_agents/harness.py
  • src/bird_interact_agents/slayer_otf/cache.py
  • tests/test_claude_sdk_otf_agent.py
  • tests/test_mcp_startup_timeout.py
  • tests/test_slayer_mcp_config.py
  • tests/test_slayer_otf_cache.py

ZmeiGorynych and others added 8 commits May 30, 2026 14:13
…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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/bird_interact_agents/harness.py (1)

699-699: 💤 Low value

Consider 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 via bird_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 or git 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6a02056 and 6bc84d3.

📒 Files selected for processing (11)
  • src/bird_interact_agents/agents/claude_sdk_otf/agent.py
  • src/bird_interact_agents/agents/claude_sdk_otf_ainteract/agent.py
  • src/bird_interact_agents/agents/pydantic_ai_otf_encode/agent.py
  • src/bird_interact_agents/harness.py
  • src/bird_interact_agents/slayer_otf/cache.py
  • tests/conftest.py
  • tests/test_claude_sdk_otf_agent.py
  • tests/test_claude_sdk_otf_ainteract_agent.py
  • tests/test_harness_imports.py
  • tests/test_mcp_startup_timeout.py
  • tests/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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant