Skip to content

DEV-1550: cap removal + autopsy retry + N1=ex_base + embedding truncation - #46

Merged
ZmeiGorynych merged 18 commits into
mainfrom
egor/dev-1550-bird-agents-integrate-slayer-compact-by-default-populate
Jun 19, 2026
Merged

DEV-1550: cap removal + autopsy retry + N1=ex_base + embedding truncation#46
ZmeiGorynych merged 18 commits into
mainfrom
egor/dev-1550-bird-agents-integrate-slayer-compact-by-default-populate

Conversation

@ZmeiGorynych

@ZmeiGorynych ZmeiGorynych commented Jun 11, 2026

Copy link
Copy Markdown
Member

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 permanent eval_failed rows. BIRD_INTERACT_PER_TASK_TIMEOUT_S still re-enables the cap when needed.
  • tests/cloud/test_run_one_task.py: new regression test_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 from AutopsyLLMOutput / AutopsyLLMOutputOneShot via _pydantic_to_tool_schema ($defs/$ref inlined). Pydantic is the single source of truth.
  • run_autopsy does one corrective retry on pydantic.ValidationError, echoing the validation message back as a tool_result with is_error=True so the model can self-correct. Repro: archeology_10 with 353 k-char prompt dropping the required pattern field — recovers cleanly.
  • tests/test_autopsy.py: three new tests (retry recovers, retry exhaustion still surfaces validation_error, generated schemas match Pydantic is_required() + no $ref/$defs leftovers).

3. Cascade N1 = upstream ex_base (mini-interact + livesqlbench)

  • New src/bird_interact_agents/eval/upstream_ex_base.py wraps both upstream graders (mini-interact / livesqlbench test_utils.py) behind compare_pred_vs_gold_ex_base(). Applies the full test_case_default cleanup (remove_comments / remove_distinct / remove_round), dispatches on benchmark, and conn.rollback()s after every Postgres-side call so pred mutations cannot leak.
  • N1 dispatch in tolerant_grader.py calls the shim for {mini-interact, livesqlbench-base-lite-sqlite, livesqlbench-base-lite, livesqlbench-base-full, livesqlbench-large}; falls back to legacy _set_equal on ExBaseUnavailableError, missing SQL, or missing local DB (preserves stubbed-executor test paths).
  • Deliberate deviation from upstream: BOTH preprocessed lists empty → True (matches legacy _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_sql at statement start) with status state-sensitive, skips missing DB / annotation as missing_inputs. --dry-run flag. Idempotent.

4. Embedding-text truncation

  • src/bird_interact_agents/slayer_otf/cache.py: new _truncate_for_embedding pre-truncates rendered memory text to 7800 tokens via tiktoken before embed_batch. Strips provider prefix (openai/...), falls back to cl100k_base for unknown models, fails closed with a logged warning + 28 000-char hard cap when tiktoken is unavailable or the encoder load fails.
  • _EMBEDDING_BUILDER_VERSION hashed into _impl_fingerprint_of so already-built caches invalidate automatically on bump — no manual rm -rf _cache_fp.txt.
  • Per-truncation logger.warning with memory id, db, original/capped char counts, and model.

Test plan

  • pytest tests/cloud/test_run_one_task.py — 10/10
  • pytest 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)
  • Full non-integration suite — 2848 passed, 94 skipped, 50 deselected
  • Local backfill on archeology_10 (validation_error) recovered with a populated autopsy.analysis.
  • Cloud verification: re-submit on one of the 6 embedding-error DBs after rm -rf slayer_otf_cache/<db>/_cache_fp.txt to confirm the truncation path + rebuild.
  • Mini-interact N1 backfill: uv run python scripts/regrade_n1_ex_base.py --dry-run, then apply, then re-run aggregate_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

    • Autopsy now surfaces and classifies LLM/SDK failures, validates tool inputs, performs one corrective retry on schema validation, and returns richer capped error reports.
    • Per-task wall-clock timeout is uncapped by default.
  • New Features

    • CLI to backfill/regrade stored mini-interact results.
    • Optional upstream grading shim to route N1 comparisons to upstream semantics when safe.
    • Pre-truncation of texts before embedding to bound token usage; cache fingerprinting updated.
    • Grade pipeline now forwards optional grading conditions.
  • Tests

    • Expanded coverage for autopsy, timeout default, N1 dispatch/upstream shim, regrade script, embedding truncation, and related integration tests.

ZmeiGorynych and others added 2 commits June 11, 2026 17:00
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>
@linear

linear Bot commented Jun 11, 2026

Copy link
Copy Markdown
DEV-1550 bird-agents: integrate slayer compact-by-default — populate Memory.description + drill-in prompt nudge

Context

Follow-up to DEV-1549 (slayer compact-by-default search + models_summary + Memory.description field). This issue tracks the bird-agents-side changes that depend on that SLayer release being pinned in uv.lock.

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 changes

A2. Populate Memory.description during OTF ingestion

Site: src/bird_interact_agents/slayer_otf/kb_memory_encoder.py:104-132 (_build_one).

The function currently builds memory dicts from KB jsonl rows. KB rows already carry a description field (verified by inspection of <db>_kb.jsonl shape). Add one key to the returned dict:

"description": row.get("description", ""),

That's the entire data plumbing — row["description"] already exists; we just thread it through.

Why this matters: DEV-1549's compact mode renders MemoryHit.text as Memory.description (with a first-paragraph fallback when description is empty). Without populating description during OTF ingestion, every bird-interact memory hits the fallback path — wasteful and fragile. Populating it gives the agent the clean, intentional one-line summary on every search hit.

Empty fallback: rows missing description (defensive — shouldn't happen in practice for bird-interact KBs, but possible for adapters consuming other corpora) get an empty string, NOT a KeyError. SLayer's compact renderer then falls back to first-paragraph extraction from learning.

A3. Prompt nudge for entity-filter drill-in

Once search defaults to compact=True, the agent's drill-in pattern (when it needs the full Memory.learning body for a specific memory it has already identified) is:

search(entities=["memory:<id>"], max_memories=1, compact=False)

Three things make this pattern correct:

  • entities=["memory:<id>"] — search is already indexed under canonical id memory:<id> (slayer/search/index.py:142), so this acts as an id filter without a new tool.
  • max_memories=1 — without this, search returns up to the default 5 hits, polluting the response with adjacent matches.
  • compact=False — opts out of the compact default to get the full learning body.

Add a sentence documenting this drill-in pattern to _AINTERACT_SLAYER_TOOLS in src/bird_interact_agents/agents/claude_sdk_otf_ainteract/prompts.py near line 74 (currently the constant that tells the agent to "Call help FIRST to learn the query syntax").

If claude_sdk_otf/prompts.py (one-shot) shares the same SLayer-tools nudge constant or has an analogous one, mirror the addition there.

Snapshot tests in tests/test_shared_otf_prompts.py get rebaselined — SHA-256 of SLAYER_OTF_AINTERACT (and SLAYER_OTF_ONE_SHOT if also touched).

Per feedback_no_prompt_content_tests in memory: no prompt-content behaviour test (no grep on the new wording). The snapshot hash bump is the contract.

Tests

A2 tests

New tests/test_kb_memory_encoder_description.py:

  • description field is populated from row["description"] on a happy-path KB row.
  • Empty fallback: row without description key produces description: "" (NOT KeyError).
  • description is independent of learning — the learning body still gets the verbatim KB block, unchanged from before.
  • No regression on entities / id / query / created_at shape.

A3 tests

  • Snapshot rebase in tests/test_shared_otf_prompts.py — update both _AINTERACT_SHA256 and (if applicable) _ONE_SHOT_SHA256.
  • No new prompt-content assertions.

Verification

  1. Full non-integration pytest suite passes:

    env -u SSH_AUTH_SOCK uv run --extra all --extra dev --extra pydantic-ai pytest
    
  2. Cloud smoke with the new SLayer version pinned:

    bird-interact-cloud submit --subscription-auth \
      --framework claude_sdk --query-mode slayer --mode a-interact \
      --dataset mini-interact --agent-model anthropic/claude-opus-4-7 \
      --user-sim-model anthropic/claude-sonnet-4-6 --reasoning-effort high \
      --use-audited-gold-sql --slayer-setup on-the-fly \
      --instance-ids alien_1 --workers 1 --actors-per-worker 1 \
      --worker-type e2-standard-8 --max-runtime-hours 1 --detach
    

    Once the run lands:

    • Confirm search tool results in the trajectory show only MemoryHit.description-sized payloads (~one-line summary per hit), NOT the verbose verbatim KB body.
    • Confirm the agent's drill-in pattern (when it needs a full memory) uses search(entities=["memory:<id>"], max_memories=1, compact=False) and gets the full learning body back.
  3. Cost comparison vs the pre-DEV-1549 slayer/correct baseline:

    • Expected ~25-30% combined cache_read reduction (DEV-1548 ~4% + DEV-1549's compact ~21-26%).
    • Expected ~$0.50 / correct slayer task savings.
    • Caveat: the savings number depends on DEV-1549's compaction actually being as tight as projected; informational, not a gate.

Critical files

  • src/bird_interact_agents/slayer_otf/kb_memory_encoder.py — A2 (one-line change in _build_one)
  • src/bird_interact_agents/agents/claude_sdk_otf_ainteract/prompts.py — A3
  • src/bird_interact_agents/agents/claude_sdk_otf/prompts.py — A3 (only if the relevant constant lives there too)
  • tests/test_kb_memory_encoder_description.py (new) — A2 pinning
  • tests/test_shared_otf_prompts.py — A3 snapshot rebase

Coordination with DEV-1549 release

Order of operations:

  1. DEV-1549 lands in SLayer + a release is cut.
  2. uv.lock bumps to the new SLayer version on this branch.
  3. A2 + A3 land.
  4. Verification smoke + cost-comparison.

If uv.lock bump produces unexpected breakage outside the compact-mode change (e.g., other SLayer API changes), file a fix issue on the SLayer side rather than working around in bird-agents.

DEV-1557 slayer.embeddings.client.embed_batch: over-cap inputs wipe the whole batch — needs per-text truncation + per-input fallback

Problem

slayer.embeddings.client.embed_batch (slayer/embeddings/client.py:91-130) calls litellm.aembedding(model=..., input=texts) once for the whole batch and catches any exception by returning [None] * len(texts). A single over-cap input poisons the entire batch — every memory loses its embedding row, not just the offender.

Hit this on the bird-interact-agents side for six content-heavy mini-interact databases (robot, fake, crypto, cold_chain_pharma_compliance, exchange_traded_funds, reverse_logistics). OpenAI's text-embedding-3-small caps at 8192 tokens; one rendered memory per DB crosses the line. litellm propagates the OpenAI 400:

litellm.BadRequestError: OpenAIException - Error code: 400 -
{'error': {'message': "Invalid 'input[N]': maximum input length is 8192 tokens."}}

…the bare except Exception swallows it, embed_batch returns all-None. Downstream OTF cache build (bird_interact_agents.slayer_otf.cache._materialise_cache_memories) sees no embeddings persisted, the agent task then fails with eval_failed: no submitted_sql / selected_database because the SLayer setup wasn't created.

We band-aided bird-agents-side by pre-truncating each text via tiktoken to 7800 tokens before calling embed_batch (MotleyAI/bird-agents#46). That's a workaround — SLayer is the right place to handle this. Downstream callers shouldn't need to know token budgets for every embedding model.

Suggested fixes (one of these, or a combination)

1. Per-text token-aware truncation inside embed_batch

  • Look up the model's context cap via litellm.utils.get_max_tokens(resolved_model) — fall back to 8192 for unknown embedding models.
  • Per text, encode with tiktoken (encoding_for_model → fall back to cl100k_base), slice tokens to cap - margin, decode.
  • Identity-return when already short, so short inputs incur no encode/decode round-trip.

2. Per-input retry on BadRequestError (length-related only)

When the batch raises litellm.BadRequestError, fall back to embedding each text individually so good inputs survive. Optionally pair with (1) so retry sees pre-truncated text.

3. Surface the failure shape so callers can disambiguate

Catch length errors separately from rate-limit / auth / network errors, log distinctly, and consider returning a more typed result (e.g. EmbeddingInputTooLong(idx)) so the cache builder can act per-input.

Repro

import asyncio
from slayer.embeddings.client import embed_batch

short = "hi"
overlong = "word " * 12_000  # > 8192 tokens for text-embedding-3-small

vectors = asyncio.run(embed_batch([short, overlong], model="openai/text-embedding-3-small"))
print(vectors)  # both None — `short` is collateral damage

Cross-reference

  • bird-agents workaround: bird_interact_agents/slayer_otf/cache.py:_truncate_for_embedding + per-batch warning log with memory id + db. PR: MotleyAI/bird-agents#46

Why this matters operationally

The all-None batch silently turns into an eval_failed task downstream, which on a long benchmark run looks like an agent failure (no submitted SQL) when it's actually an embedding-time pipeline failure. Six entire DBs of our mini-interact benchmark got knocked out by this until the workaround landed.

Review in Linear

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Updates 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.

Changes

Autopsy validation retry and schema generation

Layer / File(s) Summary
Pydantic-derived tool schema generation
src/bird_interact_agents/eval/autopsy.py
Adds helpers that inline Pydantic JSON schema refs and derive Anthropic tool input schemas for the standard and one-shot autopsy outputs.
run_autopsy retry and error handling
src/bird_interact_agents/eval/autopsy.py
Preinitializes reporting state, guards setup in a prep phase, validates tool input with Pydantic, retries once with an error tool_result after the first validation failure, and returns categorized AutopsyError results for failures.
Autopsy retry and validation tests
tests/test_autopsy.py
Adds coverage for the one-shot corrective retry, repeated validation failure returning kind="validation_error", and schema generation matching Pydantic-required fields without unresolved refs.

Per-task timeout reconfiguration

Layer / File(s) Summary
Default timeout configuration
src/bird_interact_agents/run.py
Sets _DEFAULT_PER_TASK_TIMEOUT_S to 0.0 instead of 900.0 and updates the timeout docstring to describe uncapped default behavior.
Timeout default test
tests/cloud/test_run_one_task.py
Adds a test verifying _per_task_timeout_s() stays uncapped when BIRD_INTERACT_PER_TASK_TIMEOUT_S is unset.

EX-Base shim, tolerant grader dispatch, and regrade script

Layer / File(s) Summary
EX-Base shim and mutation detection
src/bird_interact_agents/eval/upstream_ex_base.py
Adds the upstream EX-Base shim, benchmark-specific lazy loading, mutation SQL detection, loader cache isolation for db_utils, rollback and commit handling, and the empty-results pass case.
N1 dispatch into upstream ex_base
src/bird_interact_agents/eval/tolerant_grader.py
Adds _compute_n1, forwards conditions, and routes supported N1 comparisons through the upstream shim while falling back to legacy _set_equal when gating checks fail.
Regrade CLI and core processing
scripts/regrade_n1_ex_base.py
Adds a CLI that scans stored mini-interact results, reloads task inputs from task JSON or canonical JSONL, skips missing or state-sensitive cases, recomputes grading, and rewrites changed JSON unless --dry-run is used.
N1 dispatch tests
tests/eval/test_n1_dispatch.py
Adds tests for supported benchmark routing, unsupported benchmark fallback, unavailable EX-Base fallback, forwarded conditions, Postgres db_path.stem naming, and mutation-based skip behavior.
Regrade script tests
tests/eval/test_regrade_n1_ex_base.py
Adds fixtures and subprocess-based coverage for cascade flips, idempotency, mutation skips, non-mini-interact refusal, DB path resolution, dry-run behavior, stale tolerance recomputation, annotation fallback, and missing DB handling.
EX-Base shim tests
tests/eval/test_upstream_ex_base.py
Adds tests for comparison semantics, cleanup symmetry, dispatch routing, rollback behavior, loader error mapping, and module cache isolation across upstream trees.

Embedding truncation and cache fingerprinting

Layer / File(s) Summary
Embedding truncation and fingerprint
src/bird_interact_agents/slayer_otf/cache.py
Adds _truncate_for_embedding, truncates rendered memory text before embed_batch, logs truncation warnings, and includes the embed-builder version in _impl_fingerprint_of.
Embedding truncation tests
tests/test_slayer_otf_cache_embed_truncate.py
Adds unit and integration tests for token-budget truncation, fallback behavior when tiktoken is unavailable or fails, warning logs, truncated embed inputs, and fingerprint stability and invalidation.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • MotleyAI/bird-agents#39: The main PR’s run_autopsy changes (Pydantic-derived one-shot tool schemas, typed AutopsyError, and retry/validation-error handling in src/bird_interact_agents/eval/autopsy.py) are directly aligned with DEV-1541’s autopsy one-shot split and error capture.
  • MotleyAI/bird-agents#20: Both PRs modify grade_and_write/grade_one_submission wiring; this PR adds conditions forwarding which touches the same callsites as the referenced PR.

"I'm a rabbit in a noisy code glen,
I inlined schemas and fixed the when.
One retry later the output was right,
N1 got routed and regraded at night.
Embeds trimmed short, caches rebuilt bright."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.95% 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 clearly and specifically summarizes the four main changes: cap removal, autopsy retry logic, N1 grading via upstream ex_base, and embedding truncation.
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.


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.

Actionable comments posted: 1

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

1459-1460: 💤 Low value

Consider 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

📥 Commits

Reviewing files that changed from the base of the PR and between 710ed30 and 62f7d16.

📒 Files selected for processing (4)
  • src/bird_interact_agents/eval/autopsy.py
  • src/bird_interact_agents/run.py
  • tests/cloud/test_run_one_task.py
  • tests/test_autopsy.py

Comment thread src/bird_interact_agents/eval/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>
@ZmeiGorynych ZmeiGorynych changed the title Remove per-task wall-clock cap + autopsy schema-from-Pydantic with retry DEV-1550: cap removal + autopsy retry + N1=ex_base + embedding truncation Jun 11, 2026

@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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 62f7d16 and 397493c.

📒 Files selected for processing (8)
  • scripts/regrade_n1_ex_base.py
  • src/bird_interact_agents/eval/tolerant_grader.py
  • src/bird_interact_agents/eval/upstream_ex_base.py
  • src/bird_interact_agents/slayer_otf/cache.py
  • tests/eval/test_n1_dispatch.py
  • tests/eval/test_regrade_n1_ex_base.py
  • tests/eval/test_upstream_ex_base.py
  • tests/test_slayer_otf_cache_embed_truncate.py

Comment thread scripts/regrade_n1_ex_base.py
Comment thread src/bird_interact_agents/eval/tolerant_grader.py
Comment thread src/bird_interact_agents/eval/upstream_ex_base.py Outdated
Comment thread tests/test_slayer_otf_cache_embed_truncate.py Outdated
…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>

@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 (2)
tests/eval/test_n1_dispatch.py (1)

183-220: 💤 Low value

Minor comment inaccuracy on line 205.

The comment states "presence required by the conn-fallback guard" but since conn=MagicMock() is truthy, the if conn is None block at _compute_n1 line 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 win

Save 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-existing sys.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_utils module in sys.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

📥 Commits

Reviewing files that changed from the base of the PR and between 397493c and 7983a15.

📒 Files selected for processing (8)
  • scripts/regrade_n1_ex_base.py
  • src/bird_interact_agents/eval/autopsy.py
  • src/bird_interact_agents/eval/tolerant_grader.py
  • src/bird_interact_agents/eval/upstream_ex_base.py
  • tests/eval/test_n1_dispatch.py
  • tests/eval/test_regrade_n1_ex_base.py
  • tests/eval/test_upstream_ex_base.py
  • tests/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>

@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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/eval/test_regrade_n1_ex_base.py (1)

552-552: 💤 Low value

Optional: Make assertion more explicit for easier debugging.

The combined and assertion 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7983a15 and 4e847b2.

📒 Files selected for processing (6)
  • scripts/regrade_n1_ex_base.py
  • src/bird_interact_agents/eval/grade_in_place.py
  • src/bird_interact_agents/eval/upstream_ex_base.py
  • tests/eval/test_n1_dispatch.py
  • tests/eval/test_regrade_n1_ex_base.py
  • tests/eval/test_upstream_ex_base.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/regrade_n1_ex_base.py

Comment thread tests/eval/test_regrade_n1_ex_base.py Outdated
ZmeiGorynych and others added 13 commits June 12, 2026 11:18
…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
@ZmeiGorynych
ZmeiGorynych merged commit ac42586 into main Jun 19, 2026
1 check passed
ZmeiGorynych added a commit that referenced this pull request Jun 19, 2026
…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.
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