DEV-1550: cap removal + autopsy retry + N1=ex_base + embedding truncation - #46
Conversation
Was 900 s (DEV-1535), now 0 (uncapped). Rate-limited cloud runs were turning legitimate LLM back-offs into permanent eval_failed. The BIRD_INTERACT_PER_TASK_TIMEOUT_S env var still re-enables the cap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on error The autopsy tool schema was hand-mirrored alongside `AutopsyLLMOutput` / `AutopsyLLMOutputOneShot` and silently drifted from them, so an LLM output that the (looser) tool schema accepted could still fail Pydantic validation. Replace the hand-rolled `_AUTOPSY_TOOL_SCHEMA*` literals with `_pydantic_to_tool_schema(<model_cls>, ...)`, which derives the schema from `model_json_schema()` and inlines `$defs`/`$ref`. The Pydantic model is now the single source of truth. When validation still fails (Anthropic's `required` enforcement on tool input is best-effort and long prompts occasionally drop a leading field, as in archeology_10 with a 353k-char prompt), `run_autopsy` now sends the validation error back via a `tool_result` block with `is_error=True` and re-invokes the model once. Other failure kinds (API errors, missing tool_use, BadRequest) do NOT retry — they are not model self-correctable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DEV-1550 bird-agents: integrate slayer compact-by-default — populate Memory.description + drill-in prompt nudge
ContextFollow-up to DEV-1549 (slayer compact-by-default search + models_summary + Blocked by: DEV-1549. Do not start until that release ships and is consumable. Sibling: DEV-1548 (the SLayer-independent disallow list — ships independently, no dependency). Two coordinated changesA2. Populate
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughUpdates autopsy schema generation and retry/error handling, adds upstream EX-Base N1 grading plus a mini-interact regrade script and tests, removes the default per-task timeout cap, and truncates cache embedding text before embedding with fingerprint-aware rebuilds. ChangesAutopsy validation retry and schema generation
Per-task timeout reconfiguration
EX-Base shim, tolerant grader dispatch, and regrade script
Embedding truncation and cache fingerprinting
Sequence Diagram(s)sequenceDiagram
participant run_autopsy
participant Anthropic as AnthropicClient
participant Pydantic as PydanticValidator
run_autopsy->>run_autopsy: prepare KB, prompt, tool schema
run_autopsy->>Anthropic: send tool request
Anthropic-->>run_autopsy: tool_use output
run_autopsy->>Pydantic: validate tool input
alt first validation error
run_autopsy->>Anthropic: send tool_result is_error=True
Anthropic-->>run_autopsy: corrected tool_use output
run_autopsy->>Pydantic: validate retry input
end
alt retry still invalid
run_autopsy-->>run_autopsy: return validation_error result
else valid input
run_autopsy-->>run_autopsy: map output and return analysis
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/test_autopsy.py (1)
1459-1460: 💤 Low valueConsider splitting compound statements for readability.
While syntactically valid, combining statements with semicolons reduces readability in test code where clarity is paramount.
♻️ Optional refactor for clarity
- r1 = _stub_tool_use(bad_input_1); r1.content[0].id = "tu_1" - r2 = _stub_tool_use(bad_input_2); r2.content[0].id = "tu_2" + r1 = _stub_tool_use(bad_input_1) + r1.content[0].id = "tu_1" + r2 = _stub_tool_use(bad_input_2) + r2.content[0].id = "tu_2"🤖 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_autopsy.py` around lines 1459 - 1460, Split the compound statements into separate lines for clarity: replace "r1 = _stub_tool_use(bad_input_1); r1.content[0].id = 'tu_1'" with two statements assigning r1 then setting r1.content[0].id, and do the same for r2/_stub_tool_use(bad_input_2) so each operation is on its own line (references: _stub_tool_use, r1, r2, bad_input_1, bad_input_2).
🤖 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.
Inline comments:
In `@src/bird_interact_agents/eval/autopsy.py`:
- Around line 475-497: In _inline_refs, add defensive handling when resolving a
$ref that points to a non-existent $defs entry: inside resolve (used by
_inline_refs) check that the extracted key exists in defs before returning
defs[key]; if it's missing, raise a clear exception (e.g., ValueError or
KeyError) with a message that includes the missing ref key and the list of
available defs keys to aid debugging, or alternatively use defs.get and raise a
descriptive error; ensure this change references the existing symbols:
_inline_refs, defs, resolve, and the local variable key.
---
Nitpick comments:
In `@tests/test_autopsy.py`:
- Around line 1459-1460: Split the compound statements into separate lines for
clarity: replace "r1 = _stub_tool_use(bad_input_1); r1.content[0].id = 'tu_1'"
with two statements assigning r1 then setting r1.content[0].id, and do the same
for r2/_stub_tool_use(bad_input_2) so each operation is on its own line
(references: _stub_tool_use, r1, r2, bad_input_1, bad_input_2).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b8ed5e5d-0e29-4048-bd13-b10f7768c871
📒 Files selected for processing (4)
src/bird_interact_agents/eval/autopsy.pysrc/bird_interact_agents/run.pytests/cloud/test_run_one_task.pytests/test_autopsy.py
…ing truncation
Two unrelated correctness fixes bundled per direction.
N1 = upstream ex_base
---------------------
Tier N1 ("phase1_against_original_gold") used to be bag-equality on
repr(cell). Upstream mini-interact's grader (and the
algorithmically-identical livesqlbench grader) applies 2-dp Decimal/
float rounding, date normalisation, dict/list canonicalisation, and
set-dedup comparison via test_case_default + ex_base +
preprocess_results. The drift was demoting ~6 slayer + ~3 raw
mini-interact cases to N6 epsilon that should have already been N1
passes.
The new src/bird_interact_agents/eval/upstream_ex_base.py shim wraps
both upstream graders behind compare_pred_vs_gold_ex_base(), applies
the full test_case_default cleanup (remove_comments / remove_distinct
/ remove_round), and dispatches on benchmark. Postgres lsb variants
get an automatic conn.rollback() in try/finally so pred mutations
cannot leak into the next grade on the same conn. Loader-side
failures (missing tree, ImportError, ...) surface as a typed
ExBaseUnavailableError; the N1 dispatch in tolerant_grader catches it
and falls back to legacy _set_equal so a missing upstream tree never
crashes grading.
Deliberate deviation from upstream: when BOTH preprocessed result
lists are empty, the shim returns True (matches the legacy
_set_equal([], []) behavior). Documented and pinned by a regression
test.
is_mutation_sql() detects INSERT/UPDATE/DELETE/CREATE/DROP/ALTER/
TRUNCATE/REPLACE at statement-leading position (not as function
calls inside SELECT). The mini-interact backfill skips such tasks
as state-sensitive — the pristine local DB differs from the
inline-grader's post-mutation snapshot.
scripts/regrade_n1_ex_base.py is the one-shot mini-interact backfill:
walks runs/mini-interact/, re-runs the FULL cascade per task (so
every field downstream of N1 — audited tiers, tolerance booleans,
verdict, failure_classification — gets recomputed consistently),
and rewrites the JSON in place. Falls back to the canonical
mini_interact.jsonl when no annotation file exists; gracefully
skips with missing_inputs when the SQLite DB or required inputs
aren't present. --dry-run prints would-be flips without writing.
Idempotent.
Embedding-text truncation
-------------------------
On six content-heavy DBs (robot, fake, crypto,
cold_chain_pharma_compliance, exchange_traded_funds,
reverse_logistics) at least one rendered memory text exceeds the
8192-token cap of text-embedding-3-small; litellm aembedding raises
400 and embed_batch then returns [None] * len(texts) for the WHOLE
batch — every memory loses its embedding row, not just the offender.
Downstream: the OTF cache build silently aborts and the next agent
task on that DB lands eval_failed.
slayer_otf.cache now pre-truncates each rendered text to 7800
tokens (~400 token margin under 8192) via tiktoken before
embed_batch. Provider-prefixed model names (openai/...) get stripped
before encoding_for_model lookup; unknown models fall back to
cl100k_base; absent / failed tiktoken degrades to a char-cap
truncation (28_000 chars) with a logged warning instead of crashing
the cache builder. Per-truncation logger.warning carries the memory
id, db, original/capped char counts, and model.
_EMBEDDING_BUILDER_VERSION is hashed into _impl_fingerprint_of so
already-built caches invalidate automatically when the truncation
pipeline bumps — no manual rm -rf _cache_fp.txt required.
Tests
-----
73 new tests across 4 files (all behaviors in the spec are covered,
including Codex round-1 and round-2 findings: full cascade
recomputation in the regrade, conditions={'order': True} dispatch,
asymmetric DISTINCT/ROUND/comment stripping, ExBaseUnavailableError
on loader failure, all livesqlbench variants dispatching,
word-boundary adversarial mutation-detection cases, missing-DB and
missing-annotation backfill skip paths, dry-run write-counter
behaviour, exact strings reaching embed_batch, full warning-content
per truncation, char-cap fallback for short text, fingerprint
stability for unchanged inputs and movement on existing component
changes).
Full non-integration suite: 2848 passed, 94 skipped, 50 deselected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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.
Inline comments:
In `@scripts/regrade_n1_ex_base.py`:
- Around line 291-299: The code only increments report.processed after detecting
a flip, so unchanged (idempotent) regrades are not counted; update the logic in
the block around flipped/_evaluation_diffs so that report.processed is
incremented for every regrade attempt (either move report.processed += 1 before
the flipped check or add report.processed += 1 inside the branch where
report.regraded_unchanged is incremented), keeping the existing dry_run handling
(n1_before = before.evaluation.phase1_against_original_gold) intact; adjust in
scripts/regrade_n1_ex_base.py around the flipped variable and
report.regraded_unchanged/dry_run usage.
In `@src/bird_interact_agents/eval/tolerant_grader.py`:
- Around line 239-277: The call to compare_pred_vs_gold_ex_base in _compute_n1
currently hardcodes conditions=None and db_name=str(db_path); change it to
forward benchmark-specific EX-Base args by reading attributes off the benchmark
object (e.g., use db_name = getattr(benchmark, "db_name", None) or fallback to
str(db_path) only if benchmark.db_name is not set, and conditions =
getattr(benchmark, "ex_base_conditions", None)); then call
compare_pred_vs_gold_ex_base with those variables (keep conn, pred_sqls,
sol_sqls as-is) so ordered comparisons and Postgres/database-name semantics are
preserved.
In `@src/bird_interact_agents/eval/upstream_ex_base.py`:
- Around line 99-115: The loader currently mutates sys.path and then execs the
upstream module, causing bare "from db_utils import ..." to resolve to a shared
sys.modules["db_utils"]; modify _load_module_from_file to isolate each upstream
tree's db_utils by temporarily swapping sys.modules["db_utils"] (or inserting a
tree-specific key like f"db_utils_{name}") before calling
spec.loader.exec_module(mod) and restoring the original entry afterward (ensure
restoration on exceptions); reference _load_module_from_file,
sys.modules["db_utils"], and spec.loader.exec_module(mod) when making the
change.
In `@tests/test_slayer_otf_cache_embed_truncate.py`:
- Around line 428-431: The test currently uses substring checks if "a" in msg
and if "b" in msg which can match unrelated text; change these to exact ID
checks by matching the token or pattern for the memory id instead (replace if
"a" in msg / if "b" in msg with either equality checks against the specific log
token or a regex with word boundaries like r'\ba\b' / r'\bb\b'), updating the
ids_seen set only when the exact memory-id is found; refer to the existing
ids_seen and msg variables and replace those two conditional checks accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 16908019-80c2-4006-8d76-d695be3554ce
📒 Files selected for processing (8)
scripts/regrade_n1_ex_base.pysrc/bird_interact_agents/eval/tolerant_grader.pysrc/bird_interact_agents/eval/upstream_ex_base.pysrc/bird_interact_agents/slayer_otf/cache.pytests/eval/test_n1_dispatch.pytests/eval/test_regrade_n1_ex_base.pytests/eval/test_upstream_ex_base.pytests/test_slayer_otf_cache_embed_truncate.py
…olation CodeRabbit + Codex round 2 on PR #46 surfaced four real concerns. N1 dispatch (G1) - _compute_n1 now takes a conditions kwarg and forwards it to upstream ex_base, so order-sensitive tasks (conditions={"order": True}) are graded positionally instead of with set-dedup semantics. Plumbed through grade_submission's signature. - db_name argument branches on benchmark.db_backend: SQLite-backed benchmarks pass str(db_path), Postgres-backed livesqlbench variants pass db_path.stem (upstream's psycopg2 helper switches conns by name, not by filesystem path; a path would silently route to the wrong DB). - is_mutation_sql skip at the dispatch layer too: when either side mutates, fall back to legacy _set_equal on the cascade's pre-fetched rows. Prevents upstream's writeable execute path from committing pred-side state into the benchmark DB during cloud inline grading. (The backfill script keeps its own belt-and-braces skip.) Loader isolation (G2) - _load_module_from_file now snapshots + temporarily evicts sys.modules["db_utils"] around spec.loader.exec_module, so the second upstream tree's `from db_utils import ...` re-resolves to its own sibling instead of reusing the first tree's cached module. Restore runs in try/finally so a load failure also leaves the cache clean. Regrade counter (G3) - scripts/regrade_n1_ex_base.py now bumps report.processed on every graded task, flip or not. Idempotent re-runs now report processed=N + regraded_unchanged=N + regraded_flipped=0 instead of processed=0. Defensive cleanups (G4) - autopsy._inline_refs raises ValueError("Unresolved $ref: ...") with the available $defs keys instead of a bare KeyError on a malformed schema. - test_write_memories_and_embeddings_logs_full_warning_content_per_memory uses re.search(r"\\bmemory a\\b" / r"\\bmemory b\\b") instead of single- letter substring checks ("a" in msg would match "alien_db"). Tests: 6 new (forward conditions, Postgres db stem, mutation skip pred, mutation skip gold, loader isolation across trees, restore prior cache entry). Full non-integration suite: 2854 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
tests/eval/test_n1_dispatch.py (1)
183-220: 💤 Low valueMinor comment inaccuracy on line 205.
The comment states "presence required by the conn-fallback guard" but since
conn=MagicMock()is truthy, theif conn is Noneblock at_compute_n1line 290 is skipped entirely. The file creation is defensive (good habit) but not actually required for this test to pass.🤖 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/eval/test_n1_dispatch.py` around lines 183 - 220, The inline comment on the db_path.write_bytes call in test_n1_dispatch_uses_db_stem_for_postgres_benchmarks is inaccurate: because the test passes conn=MagicMock() the conn-is-None branch in _compute_n1 is never executed, so the file creation is only defensive, not required; update the comment to reflect that (e.g., "file presence is defensive; conn is provided so conn-fallback is skipped") or remove the write_bytes call if you prefer to keep the test minimal; reference the test function name test_n1_dispatch_uses_db_stem_for_postgres_benchmarks and the helper _compute_n1 when making the change.tests/eval/test_upstream_ex_base.py (1)
636-656: ⚡ Quick winSave and restore prior
sys.modules["db_utils"]state for consistency.Test 1 (
test_load_module_from_file_isolates_db_utils_between_upstream_trees) follows the defensive pattern of saving any pre-existingsys.modules["db_utils"]before manipulation and restoring it in the finally block. This test should adopt the same pattern for consistency and test isolation robustness.Currently, if another test leaves a
db_utilsmodule insys.modules, this test overwrites it (line 638) and never restores it (lines 655–656 unconditionally pop it).🛡️ Proposed fix to match test 1's save/restore pattern
+ prior_db_utils = sys.modules.get("db_utils") sentinel = types.ModuleType("db_utils") sentinel.ORIGIN = "caller_sentinel" # type: ignore[attr-defined] sys.modules["db_utils"] = sentinel tree = tmp_path / "tree" tree.mkdir() (tree / "db_utils.py").write_text("ORIGIN = 'tree_internal'\n") (tree / "test_utils.py").write_text( "from db_utils import ORIGIN\nWHICH = ORIGIN\n" ) try: m = mod._load_module_from_file( "test_utils_iso", tree / "test_utils.py", sys_path_addition=tree, ) assert m.WHICH == "tree_internal" # Post-load: the caller's sentinel is restored. assert sys.modules["db_utils"] is sentinel finally: - sys.modules.pop("db_utils", None) + if prior_db_utils is not None: + sys.modules["db_utils"] = prior_db_utils + else: + sys.modules.pop("db_utils", None) sys.modules.pop("test_utils_iso", None)🤖 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/eval/test_upstream_ex_base.py` around lines 636 - 656, This test overwrites sys.modules["db_utils"] without preserving any pre-existing module; save the prior value (e.g., original_db_utils = sys.modules.get("db_utils")) before assigning sentinel, then in the finally block restore it: if original_db_utils is not None reassign sys.modules["db_utils"] = original_db_utils else pop it (sys.modules.pop("db_utils", None)); keep the existing cleanup of "test_utils_iso" and ensure the restoration happens regardless of load success (the block around mod._load_module_from_file and the sentinel assignment should reference the sentinel and original_db_utils symbols).
🤖 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/eval/test_n1_dispatch.py`:
- Around line 183-220: The inline comment on the db_path.write_bytes call in
test_n1_dispatch_uses_db_stem_for_postgres_benchmarks is inaccurate: because the
test passes conn=MagicMock() the conn-is-None branch in _compute_n1 is never
executed, so the file creation is only defensive, not required; update the
comment to reflect that (e.g., "file presence is defensive; conn is provided so
conn-fallback is skipped") or remove the write_bytes call if you prefer to keep
the test minimal; reference the test function name
test_n1_dispatch_uses_db_stem_for_postgres_benchmarks and the helper _compute_n1
when making the change.
In `@tests/eval/test_upstream_ex_base.py`:
- Around line 636-656: This test overwrites sys.modules["db_utils"] without
preserving any pre-existing module; save the prior value (e.g.,
original_db_utils = sys.modules.get("db_utils")) before assigning sentinel, then
in the finally block restore it: if original_db_utils is not None reassign
sys.modules["db_utils"] = original_db_utils else pop it
(sys.modules.pop("db_utils", None)); keep the existing cleanup of
"test_utils_iso" and ensure the restoration happens regardless of load success
(the block around mod._load_module_from_file and the sentinel assignment should
reference the sentinel and original_db_utils symbols).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6a283353-508a-43ee-8e26-d6e495a5cba4
📒 Files selected for processing (8)
scripts/regrade_n1_ex_base.pysrc/bird_interact_agents/eval/autopsy.pysrc/bird_interact_agents/eval/tolerant_grader.pysrc/bird_interact_agents/eval/upstream_ex_base.pytests/eval/test_n1_dispatch.pytests/eval/test_regrade_n1_ex_base.pytests/eval/test_upstream_ex_base.pytests/test_slayer_otf_cache_embed_truncate.py
🚧 Files skipped from review as they are similar to previous changes (5)
- scripts/regrade_n1_ex_base.py
- src/bird_interact_agents/eval/autopsy.py
- src/bird_interact_agents/eval/upstream_ex_base.py
- tests/test_slayer_otf_cache_embed_truncate.py
- tests/eval/test_regrade_n1_ex_base.py
…ath order Codex round 3 caught three plumbing gaps that survived round 2's _compute_n1 rewrite. conditions threading (Codex round 3 #1 + #3) Round 2 added a `conditions` kwarg to `grade_submission`, but the production grading paths still dropped task-level conditions before reaching it. - `grade_in_place.grade_and_write` now accepts `conditions: Optional[dict] = None` and forwards to `grade_submission`. - `grade_in_place.grade_one_submission` reads `task_data.get("conditions")` and passes it to `grade_and_write`. - `scripts/regrade_n1_ex_base.py:_process_one` already loaded conditions from the annotation / canonical jsonl; now actually passes them to `grade_submission` so the backfill regrades ordered tasks with positional semantics. Without this, conditions={"order": True} tasks would silently fall back to set-dedup N1 from the cloud inline grader and the local regrade — even though `_compute_n1` now expects them. sys.path front-insert on every load (Codex round 3 #2) The round-1 cache-isolation fix snapshotted `sys.modules["db_utils"]` but the `sys.path.insert(0, sys_path_addition)` was conditional on absence. After loading mini-interact then livesqlbench, sys.path was `[lsb_dir, mini_dir, ...]`. A subsequent reload of mini-interact's test_utils ran a fresh `from db_utils import ...` that walked sys.path in order and bound to livesqlbench's `db_utils` (wrong driver). `_load_module_from_file` now removes any pre-existing occurrence of `sys_path_addition` and re-inserts at position 0, so the tree currently being loaded is always at the front. Tests 4 new regressions: - test_grade_and_write_forwards_conditions_to_grade_submission - test_grade_one_submission_forwards_task_data_conditions - test_regrade_script_forwards_task_conditions_to_grade_submission - test_load_module_from_file_reloading_first_tree_still_finds_own_db_utils Full non-integration suite: 2858 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/eval/test_regrade_n1_ex_base.py (1)
552-552: 💤 Low valueOptional: Make assertion more explicit for easier debugging.
The combined
andassertion works but can be split for clearer failure messages.💡 Proposed refactor for clarity
- assert seen_conditions and seen_conditions[0] == {"order": True} + assert seen_conditions, "grade_submission was never called" + assert seen_conditions[0] == {"order": True}, ( + f"Expected conditions={{'order': True}}, got {seen_conditions[0]}" + )This makes failures self-documenting: the first assertion explains that the function wasn't called, and the second shows exactly what was passed.
🤖 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/eval/test_regrade_n1_ex_base.py` at line 552, Split the combined assertion into two clear assertions: first check that seen_conditions is truthy (e.g., assert seen_conditions with a message like "handler was not called; seen_conditions is empty"), then assert the exact value with assert seen_conditions[0] == {"order": True} (optionally adding a message like f"unexpected conditions: {seen_conditions[0]}"). Update the assertions around seen_conditions in test_regrade_n1_ex_base.py so failures report whether the handler was never invoked versus the wrong payload.
🤖 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.
Inline comments:
In `@tests/eval/test_regrade_n1_ex_base.py`:
- Around line 537-540: The test mutates sys.path and imports regrade_n1_ex_base
without cleanup or reload, causing pollution and stale cached modules; fix by
inserting the path and importing inside a try/finally where you remove the
inserted path in the finally block, and before using
importlib.import_module("regrade_n1_ex_base") call importlib.reload if the
module is already present in sys.modules to ensure a fresh import (check
sys.modules for "regrade_n1_ex_base"), or pop the module from sys.modules before
importing; ensure these changes are applied around the existing
sys.path.insert(0, ...) and importlib.import_module calls to guarantee
isolation.
---
Nitpick comments:
In `@tests/eval/test_regrade_n1_ex_base.py`:
- Line 552: Split the combined assertion into two clear assertions: first check
that seen_conditions is truthy (e.g., assert seen_conditions with a message like
"handler was not called; seen_conditions is empty"), then assert the exact value
with assert seen_conditions[0] == {"order": True} (optionally adding a message
like f"unexpected conditions: {seen_conditions[0]}"). Update the assertions
around seen_conditions in test_regrade_n1_ex_base.py so failures report whether
the handler was never invoked versus the wrong payload.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 76b1f640-58b6-438e-af93-68d764cfb106
📒 Files selected for processing (6)
scripts/regrade_n1_ex_base.pysrc/bird_interact_agents/eval/grade_in_place.pysrc/bird_interact_agents/eval/upstream_ex_base.pytests/eval/test_n1_dispatch.pytests/eval/test_regrade_n1_ex_base.pytests/eval/test_upstream_ex_base.py
🚧 Files skipped from review as they are similar to previous changes (1)
- scripts/regrade_n1_ex_base.py
…etection, test isolation Codex round 4 + CodeRabbit caught three real concerns. Postgres N1 production path (Codex round 4 #1) `_compute_n1`'s file-existence guard fired for the cloud-Postgres shape (`db_path = Path(<db_name>)`, `conn = None`) and fell back to legacy `_set_equal` for every livesqlbench non-sqlite task — defeating the new ex_base dispatch in the very integration the PR advertised. The guard now only fires when the benchmark backend is SQLite; for Postgres the dispatcher trusts upstream `perform_query_on_postgresql_databases` to auto-open from the connection pool. Commented-mutation detection (Codex round 4 #2) `is_mutation_sql` only allowed whitespace before a statement-leading mutation keyword, so `-- explanation\nINSERT INTO ...` and `/* ... */ UPDATE ...` slipped past the dispatcher's mutation guard. `compare_pred_vs_gold_ex_base` then ran upstream `remove_comments` and executed the cleaned mutation against the per-task DB. `is_mutation_sql` now strips SQL comments (`-- ...`, `/* ... */`) before the regex match, mirroring upstream's pre-exec cleanup so the dispatcher and the exec path agree on what counts as statement start. Test isolation (CodeRabbit round 3) `test_regrade_script_forwards_task_conditions_to_grade_submission` mutated `sys.path` without cleanup and imported the script without reload-if-cached, leaking state to subsequent tests. The test now snapshots + restores `sys.path` and `sys.modules` and uses `importlib.reload` when the module is already cached. Same test splits the combined `and` assertion for clearer failure messages (CodeRabbit nitpick 3). Test comment accuracy (CodeRabbit nitpick 2) `test_n1_dispatch_uses_db_stem_for_postgres_benchmarks` claimed the db_path file was "presence required by the conn-fallback guard" — but `conn=MagicMock()` is truthy so the conn-is-None branch is skipped. Comment now reflects that file creation is defensive only. Tests: 5 new (4 parametrised commented-mutation cases, 1 Postgres file-existence-skip dispatch). Full non-integration suite: 2863 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o all callers Codex round 5 found two leaks the prior rounds left. Conn lifecycle in upstream shim (Codex round 5 #1) When the dispatcher passed `conn=None` (the cloud Postgres production shape), upstream `execute_queries` opened a fresh conn per call (sqlite3.connect for SQLite, pool.getconn for Postgres) and NEVER closed it. Every N1 comparison leaked at least one conn, and `_both_results_empty`'s re-execution could double-leak. Postgres eventually exhausts the pool; SQLite eventually exhausts file descriptors. `compare_pred_vs_gold_ex_base` now opens its own conn when the caller passes `conn=None` (sqlite3.connect for SQLite, _open_psycopg2_connection for Postgres) and closes it in a nested try/finally that fires on both the success path AND when upstream raises. Caller-supplied conns are left alone — that's the caller's lifecycle. `ExBaseUnavailableError` surfaces when conn-open fails so the dispatcher falls back to legacy `_set_equal` cleanly. Plumb conditions to remaining grade_submission callers (Codex round 5 #2) Round 2 added conditions to grade_submission; round 3 plumbed grade_in_place but left four direct callers still hardcoding `conditions=None` implicitly. Ordered-comparison tasks reaching N1 via those paths silently fell back to set-dedup. Updated: - src/bird_interact_agents/agents/claude_sdk_otf/agent.py (autopsy grading) - src/bird_interact_agents/agents/claude_sdk_otf_ainteract/agent.py (autopsy grading) - src/bird_interact_agents/eval/annotate.py (annotator-side grader closure) - src/bird_interact_agents/eval/regrade.py (regrade-run grader closure) Each call site now passes `conditions=task_data.get("conditions")` (or `task_row.get("conditions")` / `_row.get("conditions")`, matching the in-scope task-source dict). Tests 4 new regressions in tests/eval/: - test_compare_pred_vs_gold_ex_base_closes_owned_sqlite_conn_on_success - test_compare_pred_vs_gold_ex_base_closes_owned_sqlite_conn_on_exception - test_compare_pred_vs_gold_ex_base_does_not_close_caller_supplied_conn - test_remaining_grade_submission_callers_read_conditions_from_task_dict (grep-style across the four sites so a future caller is more likely to follow the same idiom) Full non-integration suite: 2867 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e failures Codex round 6 caught two more leak paths through the dispatch guards. CTE-prefixed mutations (Codex round 6 #1) `is_mutation_sql` matched mutation verbs only as the leading token of a statement (after `^` or `;`). SQLite + Postgres both accept CTE-prefixed mutations like WITH x AS (SELECT id FROM t WHERE v > 0) DELETE FROM t WHERE id IN (SELECT id FROM x) where the mutation verb follows a `WITH ... AS (...)` block. The statement-start regex sees `WITH` and reports "not a mutation", so the dispatcher routed the SQL to upstream's writeable exec path and the DELETE committed against the per-task DB. `is_mutation_sql` now also runs a verb-target regex (`INSERT INTO`, `UPDATE <ident> SET`, `DELETE FROM`, `CREATE TABLE/VIEW/INDEX/...`, `DROP TABLE/...`, `ALTER TABLE/...`, `TRUNCATE [TABLE] <ident>`, `REPLACE INTO`) anywhere in the cleaned SQL. The shapes are tight enough that ordinary SELECT clauses don't trip them (`SELECT INSERT_NUM` doesn't match `INSERT INTO`). tiktoken encode failures (Codex round 6 #2) `_truncate_for_embedding` caught `import tiktoken` and `encoding_for_model` failures, but `enc.encode(text)` itself raises `ValueError` when input contains disallowed special-token strings like `<|endoftext|>`. Memory text comes from arbitrary KB content, so a single offending memory could abort the whole cache build. Both `enc.encode` and `enc.decode` are now wrapped in try/except that falls back to the same logged char-cap path used when tiktoken is missing. Tests 7 new regressions: - test_is_mutation_sql_detects_cte_prefixed_mutations[*] (4 cases) - test_is_mutation_sql_negative_for_cte_select[*] (2 cases — a SELECT that happens to have a WITH clause must NOT be flagged) - test_truncate_for_embedding_special_token_text_falls_back_to_char_cap Full non-integration suite: 2874 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uncation Stage 2 wraps up the embedding-failure fix that PR #46 bandaged bird-agents-side. With SLayer 0.7.4 (DEV-1557) shipping per-text token truncation + per-input retry inside `slayer.embeddings.client.embed_batch`, the bird-agents-side workaround in `slayer_otf/cache.py` is redundant and the previously-`eval_failed` 10 mini-interact instances on six content-heavy DBs (robot, fake, crypto, cold_chain_pharma_compliance, exchange_traded_funds, reverse_logistics) become runnable. Changes * `pyproject.toml`: bump `motley-slayer[embedding-search]` to `>=0.7.4` in both pin sites; `uv lock --upgrade-package motley-slayer` regenerates the lockfile. * `slayer_otf/cache.py`: - delete `_truncate_for_embedding`, `_EMBEDDING_MAX_TOKENS = 7800`, `_EMBEDDING_FALLBACK_MAX_CHARS = 28_000` (~80 LoC of tiktoken truncation logic); - delete the per-memory truncation loop in `_materialise_cache_memories`; pass `texts` rendered by `render_memory_text_for_embedding` verbatim to `embed_batch`; - bump `_EMBEDDING_BUILDER_VERSION` from 2 to 3 (hashed into `_impl_fingerprint_of` — auto-invalidates already-built caches) and document the bird→slayer truncation handoff in the comment; - replace the deleted per-memory truncation WARNING with a passive per-memory `logger.info` line mapping `memory.id + db + chars + sha256[:16]` so SLayer's hashed-prefix truncation logs remain reverse-mappable to the offending memory / DB. DEV-1550 A3 (drill-in prompt nudge for `search(entities=["memory:<id>"], max_results=1, compact=False, cypher_filter=...)`) is already landed on this branch via `_shared_otf_prompts.py:359-367`; no new prompt edits. Tests (replaces the deleted `tests/test_slayer_otf_cache_embed_truncate.py` ~500 LoC) * migrated `test_embedding_builder_version_in_cache_fingerprint` * `test_embedding_builder_version_is_3_after_stage_2_delegation` — pins the literal version value so a partial implementation is caught. * `test_bird_side_truncation_helpers_were_deleted` — direct absence assertions on the removed symbols (no finite input can prove "no truncation ever"; absence does). * `test_materialise_cache_memories_passes_raw_text_to_embed_batch` — 50k-char synthetic memory must arrive at `embed_batch` verbatim. * `test_materialise_cache_memories_logs_per_memory_observability` — exactly one INFO log per memory, carries db + chars + a sha256 prefix computed against the rendered text (not just the literal word "sha256"). * `test_materialise_cache_memories_partial_batch_persists_good_skips_failed` — when `embed_batch` returns `[vec, None]`, the good row persists and the failed memory id surfaces in a WARNING. Audit pass (Stage 2 scope option 3): grep across `slayer_otf/`, `agents/`, `_shared_otf_prompts.py`, `_host_discovery_playbook.py` for SLayer-redundant retry / fallback code (`_truncate`, `tiktoken`, `embed_batch`, `embed_query`, `refresh_memory`, `SearchService`, `upsert_memory`, `BadRequestError`, `maximum input length`, `tantivy-only`, `max_memories` / `max_example_queries` / `max_entities`, `compact=False`, `cypher_filter`). Nothing further to remove — the only other `embed_batch`/`upsert_memory` callers are `reference_build._annotate_memories`'s defensive non-embedding guard (plan-noted non-target) and existing logging strings, neither redundant with DEV-1557. Verification * Full non-integration suite: 2864 passed. * Six-DB local cache rebuild: all 6 DBs (robot, fake, crypto, cold_chain_pharma_compliance, exchange_traded_funds, reverse_logistics) rebuild cleanly with `force=True`; `_cache_fp.txt` written for each; no `embed_batch failed for model=...` warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…top default paths
Codex found that `upstream_ex_base._DEFAULT_BIRD_INTERACT_ROOT` /
`_DEFAULT_LIVESQLBENCH_ROOT` were hardcoded to `/home/james/...`, and
`Dockerfile.cloud` did not bake those upstream trees. In the cloud actor
(and on every other dev machine), `_load_mini_interact_module` /
`_load_livesqlbench_module` raised `FileNotFoundError` →
`ExBaseUnavailableError`, and `_compute_n1` silently fell back to legacy
`_set_equal`. The PR's N1=ex_base promise never engaged in production.
Fix:
* `paths.bird_interact_upstream_root()` / `livesqlbench_upstream_root()`
return the host-side upstream tree root (sibling of the main checkout
by default; `BIRD_BIRD_INTERACT_ROOT` / `BIRD_LIVESQLBENCH_ROOT`
overrides). Spelled `_upstream_` to avoid colliding with the removed
`livesqlbench_root` shim guarded by `test_livesqlbench_root_shim_is_removed`.
* `Dockerfile.cloud` now `COPY --from=bird-interact-evaluation` /
`COPY --from=livesqlbench-evaluation` the upstream `evaluation/`
subtrees into `/app/upstream_graders/{bird-interact,livesqlbench}/`,
preserving the host directory structure so `_MINI_INTERACT_REL` /
`_LIVESQLBENCH_REL` resolve identically on cloud and local.
* `upstream_ex_base` replaces the `_DEFAULT_*_ROOT` strings with
`_CLOUD_*_ROOT` paths under `/app/upstream_graders/...` and a small
`_resolve_upstream_root` resolver: env override → in-image bake path
→ sibling-of-main-checkout discovery. No author-private absolute
paths anywhere.
* `cloud.image.build_and_push` wires both BuildKit `--build-context`
flags; `data_hash` / `image_tag` thread `bird_interact_evaluation_root`
/ `livesqlbench_evaluation_root` so an upstream grader edit forces a
rebuild (otherwise the cached image keeps stale grader code).
`_iter_upstream_grader_files` filters `__pycache__` so local imports
don't churn the hash. `default_grader_eval_roots()` centralises the
eval-dir derivation; the three callsites (`driver.submit`,
`driver.annotate`, `cli.build`) use it for both `image_tag` and
`build_and_push` so the two stay in lockstep.
Tests: `tests/eval/test_upstream_ex_base.py` pins the
`_CLOUD_*_ROOT` constants under `/app/upstream_graders/`, asserts no
`/home/...` defaults survive, and covers `_resolve_upstream_root`'s
env/cloud/sibling branches. `tests/cloud/test_image.py` pins the
Dockerfile COPY lines, the BuildKit context wiring, hash-on-edit, and
the `__pycache__` skip. Full non-integration suite: 2873 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
….py symlinks Codex round 6 flagged two follow-ups on the prior commit's upstream-grader bake: * **major** — `default_grader_eval_roots()` returned paths without validating the trees existed; a fresh checkout without `BIRD-Interact` / `livesqlbench` siblings hit `docker build --build-context bird-interact-evaluation=<nonexistent>` and failed with an opaque BuildKit error. Worse than the pre-PR posture for diagnostics (the user can't tell whether to set the env var or clone the tree). * **minor** — `_iter_upstream_grader_files()` excluded paths whose own `parts` contained `__pycache__`, but `Path.is_file()` and `read_bytes()` follow symlinks; a `foo.py` symlinking into `__pycache__/foo.pyc` (or outside the grader root entirely) would silently leak non-deterministic bytecode bytes into `data_hash`. Fix: * New `UpstreamGraderUnavailableError` + `_ensure_upstream_grader_tree_present` helper. `build_and_push` runs the guard right after resolving the default eval roots and BEFORE shelling out to docker. Marker file is `<eval_root>/test_utils.py` (the file the upstream loader actually imports). Error message names the path AND the env-var remediation (`BIRD_BIRD_INTERACT_ROOT` / `BIRD_LIVESQLBENCH_ROOT`). * `_iter_upstream_grader_files()` now also skips entries where `p.is_symlink()` is true. Tests: * `tests/cloud/test_image.py`: 4 new tests pinning each failure mode (missing-dir, dir-without-marker, symlink-skip in the iterator, and symlink-skip at the `data_hash` boundary). Existing `test_build_and_push_wires_upstream_grader_build_contexts` gets a 2-line setup addition writing the marker file — its assertions are unchanged (it still inspects the same docker build argv). Full non-integration suite: 2877 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d bake by marker file
Codex round 7 flagged three follow-ups on the round-6 prereq guard:
* **major** — `_ensure_upstream_grader_tree_present` only verified the
presence of `test_utils.py`, but the runtime loader also executes
`from db_utils import ...` (sibling resolved via sys.path injection).
A shallow upstream copy with only `test_utils.py` would pass the
guard, bake a degraded image, and crash at the first cascade-tier-N1
call inside the cloud actor — N1 then silently downgrades to
`_set_equal`.
* **major** — `_resolve_upstream_root` short-circuited on
`cloud_path.is_dir()`. A stale or partial bake that left the cloud
parent dir present but dropped the deeper `test_utils.py` marker
would bypass the sibling fallback; `_load_*_module()` then raised
`FileNotFoundError` through `ExBaseUnavailableError` and N1
downgraded with no operator-visible signal.
* **minor** — `tests/cloud/test_image_annotations.py::
test_build_and_push_passes_annotations_build_context` called
`build_and_push` without supplying explicit grader roots, making it
environment-dependent: hermetic CI without sibling `BIRD-Interact` /
`livesqlbench` checkouts would trip the round-6 guard before
reaching the annotations-context assertion.
Fix:
* `_REQUIRED_UPSTREAM_GRADER_MARKERS = ("test_utils.py", "db_utils.py")`.
`_ensure_upstream_grader_tree_present` now reports the full list of
missing markers in the error message.
* `_resolve_upstream_root` takes `marker_rel` and validates the deeper
marker file rather than just the parent directory. Both loaders
pass their `_MINI_INTERACT_REL` / `_LIVESQLBENCH_REL` constant so
the rel path stays the single source of truth.
* `test_image_annotations.py` supplies explicit grader eval roots with
marker files (same hermetic pattern as `test_image.py`). Existing
round-6 tests get `db_utils.py` markers added to their setup.
* New tests pin the round-7 tightening:
- `test_build_and_push_raises_when_grader_dir_lacks_db_utils`
(round-6 guard would have wrongly accepted this)
- `test_resolve_upstream_root_falls_through_on_partial_cloud_bake`
(round-6 resolver would have short-circuited on the partial dir).
The renamed `..._prefers_cloud_bake_when_marker_present` documents
the tightened cloud-bake acceptance criterion.
Full non-integration suite: 2879 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… complete marker set
Codex round 8 found three remaining silent-degrade paths in
`_resolve_upstream_root`: each branch (env override, cloud bake,
sibling discovery) accepted its candidate without validating the
complete marker set. So a stale `$BIRD_BIRD_INTERACT_ROOT`, a stale
sibling checkout, or a partial cloud bake with only `test_utils.py`
would each win resolution and downstream import would then crash on
`from db_utils import ...` — N1 silently downgrades to legacy
`_set_equal`. The round-7 fix tightened the cloud branch only; rounds
8 unifies the contract across all three.
Fix:
* Move `REQUIRED_UPSTREAM_GRADER_MARKERS = ("test_utils.py", "db_utils.py")`
into `upstream_ex_base` (next to the loader, the source of truth for
what the runtime needs). `image._ensure_upstream_grader_tree_present`
now imports it lazily so build-time guard and runtime resolver agree.
* `_resolve_upstream_root` builds a candidate list `[env, cloud, sibling]`
(each entry a lazy thunk so the sibling branch's import of
`bird_interact_agents.paths` only fires when actually consulted),
walks them in order, and returns the FIRST candidate whose
`<eval_dir>/test_utils.py` AND `<eval_dir>/db_utils.py` both exist.
If no candidate validates, raise `ExBaseUnavailableError` naming
every failed candidate + its missing markers, so the operator sees
exactly which tree to fix.
* Tests adjusted to populate both markers via a new
`_populate_grader_markers` helper. New tests pin the round-8
semantics:
- `..._falls_through_on_partial_env_override` — incomplete env
override no longer masks a complete sibling.
- `..._falls_through_on_partial_sibling` — incomplete sibling
raises actionably instead of silently degrading at load time.
- `..._raises_actionable_message_when_no_candidate_valid` —
failure path includes per-candidate details + env-var
remediation.
Existing `_prefers_cloud_bake_when_marker_present` keeps its
monkeypatch-explode guard, now safe under the lazy candidate list.
Full non-integration suite: 2882 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e error
Codex round 9 flagged that `tolerant_grader._compute_n1` still
catches `ExBaseUnavailableError` and silently downgrades to
`_set_equal`. The round-8 resolver builds a detailed actionable error
("Tried: ... missing markers ... clone the upstream repo or set
$BIRD_*_ROOT ..."), but this catch site swallowed it — so the operator
never saw why N1 was downgrading, and the test at
`test_n1_dispatch.py::test_n1_fallback_when_ex_base_unavailable_...`
locked in the silent behaviour.
Fix:
* The catch site now emits a `logger.warning` with the full exception
message: "cascade tier N1 is downgrading to legacy _set_equal
because the upstream ex_base grader is unavailable. Remediate to
engage strict N1 grading:\n<exc>".
* Per-process dedup via `_EX_BASE_UNAVAILABLE_SEEN: set[str]` keyed
on the rendered message. A run with 100 instances + a stale env
override would otherwise spam 100 identical warnings; one
surface-and-shut-up matches the "warn once per distinct shape" idiom.
* Grading itself still proceeds via the legacy `_set_equal` path —
the catch is preserved, just no longer silent. A missing upstream
tree never crashes a grading run.
* New test `..._warns_on_first_ex_base_unavailable_then_dedups` pins
exactly-one warning across two identical errors AND asserts the
warning carries the detailed message. Existing
`..._fallback_when_ex_base_unavailable_returns_legacy_verdict`
still pins the legacy verdict behaviour.
Full non-integration suite: 2883 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hread races Codex round 10 noted that the round-9 dedup against `_EX_BASE_UNAVAILABLE_SEEN` does a check-then-add without a lock — if grading ever runs in multiple threads in the same process, two workers can both pass `msg not in seen` and both log, violating the once-per-process contract. Cloud actors are process-parallel (Ray) today, so the realistic exposure is essentially zero, but the fix costs nothing: * Module-level `threading.Lock` wraps the membership/add step. * Logging happens OUTSIDE the lock (the `logger.warning` call can call into handler chains we don't control; holding a tiny lock across it would be a latent gotcha). * New regression test races 8 threads on a `threading.Barrier` through the same unseen error and asserts exactly one warning fires. Without the lock the test reliably observes duplicates. Full non-integration suite: 2884 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…st concurrent races Codex round 11 caught that `_load_module_from_file` mutates process-global `sys.path` and `sys.modules`, then runs `exec_module`, WITHOUT serialisation. Two threads concurrently loading DIFFERENT upstream graders (mini vs livesqlbench) can interleave the re-front / evict / exec steps; one tree's grader could bind the OTHER tree's `db_utils` and silently produce wrong cascade-tier-N1 results. Higher-impact than the round-10 warning dedup race (correctness, not just log noise). Fix: * `_UPSTREAM_LOAD_LOCK = threading.Lock()` at module level. * The lock scope covers the whole sys.path mutation → spec build → sys.modules eviction → exec_module → snapshot restore block. Any narrower scope leaks the race window. * Today's grading paths are process-parallel (Ray actors), so the realistic exposure is zero — but pre-empting a threadpool refactor here is cheap (one acquire per N1 invocation, contention only on cold loads against the SQL exec already happening downstream). * New regression `test_load_module_from_file_is_thread_safe_under_ concurrent_loads` races 8 threads × 6 iterations on a Barrier across two distinct upstream trees, asserts no cross-tree binding leaked (each load's `WHICH` matches its own tree's `db_utils.ORIGIN`). Without the lock this test reliably finds mismatches. Full non-integration suite: 2885 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…es[name] on exec failure Codex round 12 caught that `_load_module_from_file` was only restoring the sibling-module cache on exception; the prepended `sys_path_addition` lingered in `sys.path` and `sys.modules[name]` was left pointing at the partially initialised upstream module. The wrapper catches the exception and downgrades to legacy `_set_equal`, so callers proceed with process-global import state still polluted — an unrelated importer using the internal loader name would see the broken stub. Fix: * Snapshot `sys.path`, `sys.modules[name]`, and the sibling cache BEFORE the mutation block. On SUCCESS, sys.path keeps the prepended addition (sticky-by-design per Codex round 2 — a subsequent reload of the same tree re-fronts itself) and `sys.modules[name]` keeps the loaded module (the grader API holds references to its globals). On FAILURE, BOTH are rolled back to the snapshots so no half-state survives the failed call. * The sibling-module restore runs in the `finally` (unchanged from round 11 — already symmetric for success + failure). * New regression `test_load_module_from_file_rolls_back_state_on_ exec_module_failure`: synthetic `test_utils.py` raises RuntimeError at top-level exec, the loader call propagates the error, and the test asserts BOTH sys.path AND sys.modules[name] equal their pre-load snapshots. Full non-integration suite: 2886 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ents-integrate-slayer-compact-by-default-populate # Conflicts: # uv.lock
…ark-runs Reconciles this branch's DEV-1555 / DEV-1561 + CR r1 unification work with PR #46 (DEV-1550 SLayer compact-by-default), PR #48 (cascade_for_combo), PR #50 (query-syntax-rejection analysis), and DEV-1545 + DEV-1546 prompt additions on origin/main. Conflict resolutions: * ``_shared_otf_prompts.py`` — replaced HEAD's V1 helper section with origin/main's (it carries the new ``_DEDUP_VS_RAW_ROWS``, ``_TABLE_SET_PROBE``, ``_GRADER_ZERO_VS_ONE_DIAGNOSTIC``, ``_SLAYER_TOOLS_BLOCK`` etc. plus DEV-1550 ModelColumn / memory drill- in paragraphs), restored our ``_AFTER_REJECTED_DISCIPLINE`` (DEV-1555 stage-2), and kept our four ``*_V0`` snapshots appended at the bottom. Stripped lingering ``query_nested`` mentions from the V1 helpers so the unified-tool contract holds on both sides. * ``claude_sdk/agent.py`` — kept the unified ``query`` schema (``source_model`` + projection fields OR ``queries`` array; ``required: []``) and the runtime ``source_model XOR queries`` gate. The handler now builds the SlayerQuery JSON internally and forwards it to origin/main's DEV-1546 ``query_impl(query_json: str, …)`` for single-stage, or to ``query_nested_impl(queries=…)`` for the nested-DAG form — matching ``submit_query``'s pattern. Picked up origin/main's ``distinct_dimension_values`` field as an additional schema property so the DEV-1546 dim-only auto-dedup opt-out stays reachable through the unified shape. * ``eval/autopsy.py`` — combined origin/main's 2-attempt corrective- retry loop (sends Pydantic validation errors back as a ``tool_result`` so the model self-fixes the archeology_10 regression) with our DEV-1555 model-aware client (``_build_anthropic_client(model)`` + ``requires_thinking(model)`` thinking mode + auto tool_choice for Moonshot/Kimi) and the JSON-text fallback for third-party endpoints that don't honor forced tool_choice. The retry only fires when the model used the tool (the JSON-text-fallback path has no tool_use_id to bind the corrective ``tool_result`` to). * ``run.py._per_task_timeout_s`` — origin/main flipped the default to ``_DEFAULT_PER_TASK_TIMEOUT_S = 0.0`` (no cap). Adjusted our DEV-1555 grace logic so the runaway grace is only added when the operator explicitly opted in to a positive cap — the default-uncapped contract now holds. * V0 prompts kept as thin re-exports from ``_shared_otf_prompts.V0`` snapshots (origin/main had touched ``claude_sdk_otf*/prompts.py`` with DEV-1545/1550 helper composition, but our side reduced those files to one-line re-exports so the V0 snapshot is the single source of truth for the v0 surface). * ``test_shared_otf_prompts.py`` — re-baselined the V1 SHA pins to ``3fa05ac2…``/``65b8eb05…`` → final post-cleanse ``e671aea3…``/``a3fd695c…`` after stripping lingering ``query_nested`` mentions from the merged V1 helpers. * ``test_dev1534_query_wrapper.py`` — rewrote the schema pin to match the unified shape (``source_model``, ``queries``, ``required: []``); the prior DEV-1546 ``query_json``-only pin is superseded. * ``test_dev1546_distinct_dim_values.py`` — repointed the dedup composition tests at the ``SLAYER_OTF_*_V0`` snapshots (where the full ``_DEDUP_VS_RAW_ROWS`` body is inlined byte-for-byte); the v1 prompts teach the same guidance in a shorter form not assembled via the live constant. Rewrote the query-tool-schema test to assert the unified-shape surface. * ``test_dev1555_query_unified_schema.py`` — updated the mock ``query_impl`` to take ``query_json: str`` positional + kwargs (matches DEV-1546's signature) and parse it to verify the wrapper built the right SlayerQuery dict. Full non-integration suite: 3212 passed, 94 skipped, 50 deselected, 0 failed.
Summary
Three layered correctness fixes on top of the DEV-1550 branch.
1. Per-task wall-clock cap default → 0 (uncapped)
src/bird_interact_agents/run.py: was 900 s (DEV-1535), now 0 (no cap). Rate-limited cloud runs were converting recoverable LLM back-offs into permanenteval_failedrows.BIRD_INTERACT_PER_TASK_TIMEOUT_Sstill re-enables the cap when needed.tests/cloud/test_run_one_task.py: new regressiontest_per_task_timeout_default_is_uncapped.2. Autopsy: Pydantic-derived tool schema + corrective retry
src/bird_interact_agents/eval/autopsy.py: tool schemas now generated fromAutopsyLLMOutput/AutopsyLLMOutputOneShotvia_pydantic_to_tool_schema($defs/$refinlined). Pydantic is the single source of truth.run_autopsydoes one corrective retry onpydantic.ValidationError, echoing the validation message back as atool_resultwithis_error=Trueso the model can self-correct. Repro:archeology_10with 353 k-char prompt dropping the requiredpatternfield — recovers cleanly.tests/test_autopsy.py: three new tests (retry recovers, retry exhaustion still surfaces validation_error, generated schemas match Pydanticis_required()+ no$ref/$defsleftovers).3. Cascade N1 = upstream
ex_base(mini-interact + livesqlbench)src/bird_interact_agents/eval/upstream_ex_base.pywraps both upstream graders (mini-interact / livesqlbenchtest_utils.py) behindcompare_pred_vs_gold_ex_base(). Applies the fulltest_case_defaultcleanup (remove_comments/remove_distinct/remove_round), dispatches on benchmark, andconn.rollback()s after every Postgres-side call so pred mutations cannot leak.tolerant_grader.pycalls the shim for {mini-interact, livesqlbench-base-lite-sqlite, livesqlbench-base-lite, livesqlbench-base-full, livesqlbench-large}; falls back to legacy_set_equalonExBaseUnavailableError, missing SQL, or missing local DB (preserves stubbed-executor test paths)._set_equal([], [])); pinned by a regression test.scripts/regrade_n1_ex_base.py: one-shot mini-interact backfill. Re-runs the FULL cascade per stored result so audited tiers / tolerance booleans / verdict / failure_classification all get recomputed consistently. Skips mutation-bearing SQL (is_mutation_sqlat statement start) with statusstate-sensitive, skips missing DB / annotation asmissing_inputs.--dry-runflag. Idempotent.4. Embedding-text truncation
src/bird_interact_agents/slayer_otf/cache.py: new_truncate_for_embeddingpre-truncates rendered memory text to 7800 tokens via tiktoken beforeembed_batch. Strips provider prefix (openai/...), falls back tocl100k_basefor unknown models, fails closed with a logged warning + 28 000-char hard cap when tiktoken is unavailable or the encoder load fails._EMBEDDING_BUILDER_VERSIONhashed into_impl_fingerprint_ofso already-built caches invalidate automatically on bump — no manualrm -rf _cache_fp.txt.logger.warningwith memory id, db, original/capped char counts, and model.Test plan
pytest tests/cloud/test_run_one_task.py— 10/10pytest tests/test_autopsy.py— 123/123 (3 new)pytest tests/eval/test_upstream_ex_base.py— 44/44 (new)pytest tests/eval/test_n1_dispatch.py— 4/4 (new)pytest tests/eval/test_regrade_n1_ex_base.py— 10/10 (new)pytest tests/test_slayer_otf_cache_embed_truncate.py— 15/15 (new)archeology_10(validation_error) recovered with a populatedautopsy.analysis.rm -rf slayer_otf_cache/<db>/_cache_fp.txtto confirm the truncation path + rebuild.uv run python scripts/regrade_n1_ex_base.py --dry-run, then apply, then re-runaggregate_cascading_latest('mini-interact')and confirm the N6 epsilon delta drops to 0 (the 6+3 cases now pass at N1).🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
New Features
Tests