Skip to content

DEV-1609: make claude_sdk_otf_encode a first-class OTF encoder - #64

Merged
ZmeiGorynych merged 12 commits into
mainfrom
egor/dev-1609-make-claude_sdk-the-default-otf-encoder-everywhere-wire
Jun 29, 2026
Merged

DEV-1609: make claude_sdk_otf_encode a first-class OTF encoder#64
ZmeiGorynych merged 12 commits into
mainfrom
egor/dev-1609-make-claude_sdk-the-default-otf-encoder-everywhere-wire

Conversation

@ZmeiGorynych

@ZmeiGorynych ZmeiGorynych commented Jun 27, 2026

Copy link
Copy Markdown
Member

Goal

Make claude_sdk_otf_encode the default OTF reference encoder, reachable from the cloud submit path. Previously the superior claude_sdk encoder (DEV-1589) was only reachable via local scripts/build_otf_references.py --encoder-framework claude_sdk; the cloud submit --framework could only encode with the legacy path. This unblocks claude_sdk encoding of Postgres benchmarks (e.g. bird-interact-lite-exp), where the cloud auto-loads Postgres but the dev box has none.

Design — build-only, cloud == local

An OTF-encode run's only durable output is the per-DB slayer_models_otf/<benchmark>/<db> reference that the cloud merge-back uploads home. The new ClaudeSDKOtfEncodeAgent.run_task does exactly what the local script does: construct the claude_sdk build-encoder and call ensure_db_reference. No per-task HARD-8 masking, no agentic eval loop — so cloud encoding is behaviourally identical to local encoding (modulo Postgres loading + GCS transport).

Changes

  • New ClaudeSDKOtfEncodeAgent (agents/claude_sdk_otf_encode/agent.py), exported from the package.
  • Register the framework in run._make_runner + both --framework choice lists (run.py + cloud/cli.py), and in the --otf-rebuild force-wipe gate (_maybe_force_wipe_otf).
  • Single source of truth frameworks.is_otf_encode_framework replaces the 5 scattered framework == "pydantic_ai_otf_encode" literals: driver uploads, ray_app artifacts + seed snapshot, gcs artifact name, upload_back guard. The legacy string stays recognised for back-compat (existing manifests / resubmit).
  • Default + guidance: repoint the ~5 "use pydantic_ai_otf_encode" strings, the harness storage-opt-out note, and the CLAUDE.md smoke recipes to the new default.
  • Auth: already covered via the claude_sdk* prefix gating (run._apply_subscription_auth_env / prereqs._is_claude_sdk_framework) — no prod change, pinned by tests.

Telemetry

Encode result-row telemetry is read from the returned ReferenceEntry (setup_resultskb_encoded, best-effort _setup_usage.jsonusage; zero usage on a cache-reuse where no LLM ran) — not from the build-encoder factory.

Tests

New: framework registration + _make_runner dispatch, build-only run_task wiring, telemetry-from-ReferenceEntry (+ reuse zero-usage), error-row on build failure, constructor/run_task guardrails, force-wipe gate, subscription-auth explicit-choice, and full cloud keying parity (mini-interact + LiveSQLBench root parity + non-encode negatives). Full non-integration suite green (3557 passed).

Out of scope

  • Retiring the legacy encoder (kept recognised for back-compat).
  • Post-merge cloud validation smoke (separate).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added claude_sdk_otf_encode as a first-class OTF reference encoding framework for cloud submissions.
    • Extended CLI/runner dispatch to support build-only OTF encoding for claude_sdk_otf_encode, including rebuild-from-scratch with --otf-rebuild.
  • Bug Fixes
    • Unified OTF encode artifact selection/upload/download/seeding/reference shipping across OTF encode frameworks; token usage now reports zero on reference reuse.
    • Improved SDK/session timeout handling to prevent hangs during encoder setup and per-KB encoding.
  • Documentation
    • Updated CLAUDE.md and submit instructions to default to claude_sdk_otf_encode and require explicit --subscription-auth.
  • Tests
    • Added/expanded DEV-1609 test coverage for framework eligibility, artifact keying parity, and timeout behavior.

Add `claude_sdk_otf_encode` as the default OTF *reference* encoder, reachable
from the cloud submit path (previously only the local
`scripts/build_otf_references.py --encoder-framework claude_sdk` could drive
the claude_sdk encoder; the cloud could only encode with the legacy path).

* New build-only `ClaudeSDKOtfEncodeAgent` (agents/claude_sdk_otf_encode/
  agent.py): its `run_task` constructs the claude_sdk build-encoder (DEV-1589)
  and calls `ensure_db_reference` to build the canonical per-DB
  `slayer_models_otf/<benchmark>/<db>` reference that the cloud merge-back
  uploads home — behaviourally identical to the local script. No per-task
  HARD-8 masking, no agentic eval loop.
* Register the framework in `run._make_runner` + both `--framework` choice
  lists (run.py + cloud/cli.py), and in the `--otf-rebuild` force-wipe gate.
* Single source of truth `frameworks.is_otf_encode_framework` replaces the
  five scattered `framework == "pydantic_ai_otf_encode"` literals
  (driver uploads, ray_app artifacts + seed snapshot, gcs artifact name,
  upload_back guard). Legacy string stays recognised for back-compat.
* Repoint the ~5 "use pydantic_ai_otf_encode" guidance strings + the harness
  storage-opt-out note + CLAUDE.md smoke recipes to the new default.
* Subscription-auth already covers it via the `claude_sdk*` prefix gating.

Tests: new framework registration + dispatch, build-only run_task wiring
(telemetry from ReferenceEntry, zero-usage reuse path, error-row on failure),
guardrails, force-wipe gate, and cloud keying parity (incl. LiveSQLBench
root parity + non-encode negatives). Full non-integration suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@linear

linear Bot commented Jun 27, 2026

Copy link
Copy Markdown
DEV-1609 Make claude_sdk the default OTF encoder everywhere (wire claude_sdk_otf_encode into run.py + cloud)

Goal

Make the claude_sdk OTF encoder (DEV-1589) the default encoder everywhere, including the cloud. Today the superior claude_sdk encoder is only reachable via the local scripts/build_otf_references.py --encoder-framework claude_sdk; the cloud submit path can ONLY encode with pydantic_ai_otf_encode.

Why now (motivating case)

We want to OTF-encode the bird-interact-lite-exp databases (Postgres) using the claude_sdk encoder, reusing the encoding work already done for mini-interact (the per-DB KB content is byte-identical modulo line endings — verified across all 18 overlapping DBs). The mini-interact references are NOT directly reusable because they are SQLite-dialect (JSON_EXTRACT, CHAR(9)) while bird-interact-lite is Postgres, so re-encoding against the Postgres cache is required.

  • Local re-encode is blocked: no Postgres is installed on the dev box (the lite cache was synced from cloud); installing it needs sudo, and sequential local encode is ~24-30h for 18 DBs.
  • Cloud auto-handles Postgres (_ensure_postgres_loaded per VM) but the cloud submit --framework has no claude_sdk encoder — only pydantic_ai_otf_encode.

So claude_sdk encoding of any Postgres benchmark is currently impossible without local Postgres. This issue removes that gap.

Diagnosis — architectural mismatch

The two encoders are shaped differently:

  • pydantic_ai_otf_encode is a full agent framework: class PydanticAIOtfEncodeAgent (agents/pydantic_ai_otf_encode/agent.py:976) with .run_task(td, data_dir, budget, query_mode, ...). It plugs into the per-task runner loop via run.py::_make_runner (run.py:913). Internally it calls ensure_db_reference(... build_encoder=make_setup_build_encoder(...)) (agent.py:908,1113). The cloud merge-back machinery then uploads slayer_models_otf/<benchmark>/<db> back.
  • claude_sdk_otf_encode (DEV-1589, agents/claude_sdk_otf_encode/setup_encoder.py) is a build-time encoder factory: make_claude_sdk_build_encoder(*, model, reasoning_effort, self_model_id) -> build_encoder(storage, build_dir, db, sessions_dir) -> run_one(kb_id, row, ...). Per-KB, NO .run_task(), NO agent class, NOT registered as a framework. Only scripts/build_otf_references.py drives it (via ensure_db_reference).

Framework registration gaps

claude_sdk_otf_encode must be added as a framework in:

  • run.py::_make_runner — new dispatch branch (mirrors the pydantic_ai_otf_encode branch at run.py:913-938).
  • run.py argparse --framework choices (run.py:1729).
  • cloud/cli.py --framework choices (cli.py:39).

Cloud merge-back is keyed on the LITERAL string framework == "pydantic_ai_otf_encode" in 5 places

These decide that a run is an "encode" run whose slayer_models_otf/<benchmark>/<db> must be merged back:

  • cloud/driver.py:489_slayer_uploads_for (which artifacts to upload).
  • cloud/ray_app.py:303 — artifacts list; and ray_app.py:969 — collect encoded refs.
  • cloud/gcs.py:396(slayer_setup, framework) -> artifact-dir mapping.
  • cloud/upload_back.py:233 — stale-ref guard.

All five must recognise BOTH encode frameworks. Replace the scattered literals with a single source of truth, e.g. OTF_ENCODE_FRAMEWORKS = frozenset({"pydantic_ai_otf_encode", "claude_sdk_otf_encode"}) / is_otf_encode_framework(fw) in one module, and call it at every site.

"Use pydantic_ai_otf_encode" guidance strings

~5 agent files tell users to "use --framework pydantic_ai_otf_encode for non-supported models" (claude_sdk_otf/agent.py:448, claude_sdk_otf_ainteract/agent.py:307, claude_sdk_otf_ainteract_v1/agent.py:295, claude_sdk_otf_v1/agent.py:355, _pre_encoded.py:166). Update to point at the new default.

Storage opt-out note

harness.py:1575 notes pydantic_ai_otf_encode does NOT opt out of storage; the new framework needs the same treatment.

Plan

  1. Add ClaudeSDKOtfEncodeAgent (new agents/claude_sdk_otf_encode/agent.py) with .run_task(...) mirroring PydanticAIOtfEncodeAgent.run_task, but constructing the build-encoder via make_claude_sdk_build_encoder(model=..., self_model_id=native_model_id(model), reasoning_effort=...) instead of make_setup_build_encoder(...), then ensure_db_reference(...). Factor shared run_task scaffolding out of the pydantic agent where practical (avoid copy-paste of the merge/usage/guardrail logic).
  2. Register the framework in _make_runner (new branch) + both argparse choice lists (run.py + cloud/cli.py).
  3. Generalise the cloud encode keying: introduce is_otf_encode_framework(fw) (single module), apply at driver.py:489, ray_app.py:303 & :969, gcs.py:396, upload_back.py:233. Both encode frameworks produce identical artifact sets (slayer_otf_cache immutable + slayer_models_otf mutable merge-back), so this is parity, not new behaviour.
  4. Make claude_sdk the default: build_otf_references.py already defaults --encoder-framework claude_sdk (confirm/keep). Update the ~5 guidance strings + CLAUDE.md to recommend claude_sdk_otf_encode as the default cloud encode framework.
  5. Auth: claude_sdk_otf_encode is a claude_sdk* framework, so --subscription-auth now applies (verify the claude_sdk*-prefix gating in run.py::_apply_subscription_auth_env and the cloud driver's subscription-auth env wiring treat it as claude_sdk-family; it also routes through hermetic_claude_sdk_session per the DEV-1579 contract).
  6. Guardrails: mirror the pydantic agent's constraints (requires slayer_setup='on-the-fly', --query-mode slayer, --mode a-interact).

Tests (required — production change)

  • Framework registration: claude_sdk_otf_encode present in run.py + cloud cli --framework choices; _make_runner constructs the claude_sdk encode runner.
  • Generalised keying: _slayer_uploads_for, ray_app artifacts, gcs artifact-dir map, upload_back guard all return the encode artifacts for claude_sdk_otf_encode (parity with pydantic). Extend the existing tests that pin the pydantic_ai_otf_encode artifact behaviour.
  • Subscription-auth gating accepts claude_sdk_otf_encode (Anthropic) and the hermetic session is used.
  • Update tests/test_cloud_paths_unchanged.py and any framework-choice enumerations.
  • Run the FULL non-integration suite: env -u SSH_AUTH_SOCK uv run --extra all --extra dev --extra pydantic-ai pytest.

Post-merge validation (separate, not in this issue's diff)

  • Cloud smoke: bird-interact-cloud submit --framework claude_sdk_otf_encode --dataset bird-interact-lite-exp ... --instance-ids alien_1 (1 DB), confirm the reference merges back to slayer_models_otf/bird-interact-lite-exp/alien/_reference_fp.txt.
  • Then the remaining 17 overlapping DBs.

Out of scope

  • Retiring pydantic_ai_otf_encode (keep as a fallback for non-Anthropic / non-claude_sdk-capable models).
  • The KB-21/42 income-band over-encoding correctness bug (separate issue) — orthogonal to encoder selection.

Review in Linear

@coderabbitai

coderabbitai Bot commented Jun 27, 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

Adds claude_sdk_otf_encode as the default OTF reference encoder, wires it into runner and cloud selection paths, tracks fresh-build vs reuse metadata, and updates docs plus tests to reflect the new framework family.

Changes

Claude SDK OTF encode rollout

Layer / File(s) Summary
Framework token and guidance
CLAUDE.md, src/bird_interact_agents/frameworks.py, src/bird_interact_agents/run.py, src/bird_interact_agents/cloud/cli.py, src/bird_interact_agents/harness.py, src/bird_interact_agents/agents/_pre_encoded.py, src/bird_interact_agents/agents/claude_sdk_otf*/agent.py
claude_sdk_otf_encode is added to the shared framework set and CLI choices, docs and guidance now point to it, and --otf-rebuild now targets its OTF cache/reference pair.
Build-only encoder adapter and runner
src/bird_interact_agents/agents/claude_sdk_otf_encode/__init__.py, src/bird_interact_agents/agents/claude_sdk_otf_encode/agent.py, src/bird_interact_agents/run.py
ClaudeSDKOtfEncodeAgent is exported and wired into run.py as a build-only reference encoder that calls ensure_db_reference and returns finalized encode rows.
SDK session and encoder timeouts
src/bird_interact_agents/agents/claude_sdk/sdk_env.py, src/bird_interact_agents/agents/claude_sdk_otf_encode/setup_encoder.py, tests/test_sdk_subprocess_hermetic.py, tests/test_dev1589_setup_encoder.py
hermetic_claude_sdk_session gains enter-timeout handling, and the build encoder uses forced disconnects plus KB-level telemetry around each encode attempt.
Reference build metadata
src/bird_interact_agents/slayer_otf/reference_build.py, tests/test_slayer_otf_reference_build.py, tests/test_otf_encode_reference_root.py
ReferenceEntry now records whether the current caller actually built the reference, and reuse paths preserve that state through ensure_db_reference.
Cloud keying and upload
src/bird_interact_agents/cloud/driver.py, src/bird_interact_agents/cloud/gcs.py, src/bird_interact_agents/cloud/ray_app.py, src/bird_interact_agents/cloud/upload_back.py, tests/cloud/test_dev1609_encode_keying.py, tests/cloud/test_gcs.py, tests/cloud/test_ray_app.py
is_otf_encode_framework() now controls OTF artifact naming, upload selection, seed snapshots, and reference-delta shipping for the new framework family.
Runtime and cloud tests
tests/test_dev1609_claude_sdk_otf_encode.py
Tests cover framework recognition, parser choices, runner dispatch, build-only encoding, wipe behavior, and subscription-auth handling.

Possibly related PRs

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A bunny named Byte took one careful hop,
through caches and keys that would not stop.
claude_sdk_otf_encode gleamed in the night,
with tests like carrots all stitched up right.
🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.32% 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 matches the main change: promoting claude_sdk_otf_encode as the default OTF encoder.
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.

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

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CLAUDE.md (1)

196-204: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Drop the retired --slayer-setup flag from these smoke recipes.
bird-interact-cloud submit now uses --pre-encoded-models and derives slayer_setup, so the remaining --slayer-setup on-the-fly in both command blocks should be removed; otherwise the recipe will hit an unrecognized-argument error.

🤖 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 `@CLAUDE.md` around lines 196 - 204, The smoke recipe still passes the retired
--slayer-setup flag in the bird-interact-cloud submit command. Update the
command blocks in CLAUDE.md to remove --slayer-setup on-the-fly and rely on the
current --pre-encoded-models flow, since submit now derives slayer_setup
automatically. Make sure both smoke recipe examples are kept in sync so they do
not trigger an unrecognized-argument error.
🤖 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/agents/claude_sdk_otf_encode/agent.py`:
- Around line 125-129: The mode gate in claude_sdk_otf_encode.Agent currently
rejects anything except a-interact, which blocks one-shot benchmark runs coming
from run.py via eval_mode. Update the check in the agent’s encode path to accept
one-shot as well, or remap one-shot to a-interact before this validation so the
encoder can proceed; use the existing eval_mode branch in agent.py as the fix
point.

---

Outside diff comments:
In `@CLAUDE.md`:
- Around line 196-204: The smoke recipe still passes the retired --slayer-setup
flag in the bird-interact-cloud submit command. Update the command blocks in
CLAUDE.md to remove --slayer-setup on-the-fly and rely on the current
--pre-encoded-models flow, since submit now derives slayer_setup automatically.
Make sure both smoke recipe examples are kept in sync so they do not trigger an
unrecognized-argument error.
🪄 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: 1a3887a8-3585-4d39-a7f4-914c260c1493

📥 Commits

Reviewing files that changed from the base of the PR and between 2cfd5cf and 711dd3c.

📒 Files selected for processing (19)
  • CLAUDE.md
  • src/bird_interact_agents/agents/_pre_encoded.py
  • src/bird_interact_agents/agents/claude_sdk_otf/agent.py
  • src/bird_interact_agents/agents/claude_sdk_otf_ainteract/agent.py
  • src/bird_interact_agents/agents/claude_sdk_otf_ainteract_v1/agent.py
  • src/bird_interact_agents/agents/claude_sdk_otf_encode/__init__.py
  • src/bird_interact_agents/agents/claude_sdk_otf_encode/agent.py
  • src/bird_interact_agents/agents/claude_sdk_otf_v1/agent.py
  • src/bird_interact_agents/cloud/cli.py
  • src/bird_interact_agents/cloud/driver.py
  • src/bird_interact_agents/cloud/gcs.py
  • src/bird_interact_agents/cloud/ray_app.py
  • src/bird_interact_agents/cloud/upload_back.py
  • src/bird_interact_agents/frameworks.py
  • src/bird_interact_agents/harness.py
  • src/bird_interact_agents/run.py
  • tests/cloud/test_dev1609_encode_keying.py
  • tests/cloud/test_gcs.py
  • tests/test_dev1609_claude_sdk_otf_encode.py

Comment thread src/bird_interact_agents/agents/claude_sdk_otf_encode/agent.py Outdated
ZmeiGorynych and others added 6 commits June 27, 2026 11:56
…(Codex review)

* run_task now accepts --mode one-shot in addition to a-interact. One-shot
  benchmarks (LiveSQLBench + its Postgres variants this issue unblocks) have
  no a-interact mode, so rejecting one-shot made claude_sdk encoding of them
  impossible. Mode is irrelevant to what an encode run builds anyway.
* Encode telemetry: report the build's `_setup_usage.json` only when THIS
  task actually built the reference (no pre-existing `_reference_fp.txt`
  marker). On a marker-based reuse the persisted sidecar would otherwise be
  re-reported, double-counting tokens across tasks for the same DB.

Tests: one-shot acceptance + reuse-zero-usage regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (Codex r2)

The marker pre-check couldn't tell a fresh build from a cross-process peer-
reuse: when two Ray actors on one VM build the same DB concurrently, the
loser reuses the peer's reference under the flock but its pre-check saw no
marker, so it re-reported the peer's setup-encode tokens (double-count).

Make `ensure_db_reference` return a `ReferenceEntry.built` flag — True only
when THIS call ran the encoder, False on every reuse path (top marker check,
under-lock double-check, and the under-flock cross-process peer-reuse). The
encode agent now reads usage from `_setup_usage.json` only when
`entry.built`. `_build_reference[_inside_lock]` now return a ReferenceEntry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es (CodeRabbit)

bird-interact-cloud submit derives slayer_setup from --pre-encoded-models;
the retired --slayer-setup on-the-fly would error. on-the-fly is the default
(no --pre-encoded-models), so dropping the flag keeps the recipes correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aude_sdk-the-default-otf-encoder-everywhere-wire

# Conflicts:
#	src/bird_interact_agents/cloud/ray_app.py
…ces as an error

The maiden cloud on-the-fly claude_sdk_otf_encode run hung in
hermetic_claude_sdk_session.__aenter__ (bundled claude CLI + slayer stdio-MCP
handshake + get_mcp_status parity probe) — nothing bounded it, so the actor
stayed alive with zero output for 2h until the VM self-delete timer fired. The
existing per-attempt 300s timeout only covers query/receive, AFTER a successful
enter.

Add enter_timeout_s (default None = unchanged for every other claude_sdk agent)
to hermetic_claude_sdk_session, bounding both __aenter__ and the parity probe;
on expiry tear down the half-started subprocess (disconnect/ aexit) and raise
SdkSessionEnterError. setup_encoder passes 240s and emits per-KB otf_timing
milestone events, so a future cloud hang becomes a per-KB status=error plus a
visible log line instead of a silent 2h block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Local repro proved the claude_sdk_otf_encode path is healthy (cache reuse,
SDK enter <1s, KB encoded in 63s) — the cloud hang is environmental and was
masked by two gaps this commit closes:

1. Observability: the Ray actor's root log handler defaults to WARNING, so our
   INFO otf_timing kb.start/kb.done/sdk_client_enter breadcrumbs were dropped —
   a healthy-but-slow encode looked identical to a hang in ray job logs / the
   per-task debug log. _ensure_actor_logging() attaches one INFO StreamHandler
   to the bird_interact_agents package logger in the actor (idempotent).

2. Un-wedgeable timeout: the per-attempt asyncio.wait_for could hang forever
   when the SDK's anyio-threaded transport read never returns (the cloud
   failure mode) — wait_for cancels the inner coro and awaits a cancellation
   that never completes. _drive_with_timeout times the cycle with asyncio.wait
   (no cancel), then on expiry force-closes the client (disconnect) to break
   the stuck read, drains the task, and raises TimeoutError → a per-KB error
   row + an uploaded per-task log instead of a silent 2h block.

Together: a future cloud hang self-resolves into error rows whose uploaded
per-task log shows exactly which KB/stage stalled. Full suite green (3674).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@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: 3

🧹 Nitpick comments (2)
tests/test_dev1589_setup_encoder.py (1)

760-768: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the hang test cancellation-resistant.

asyncio.sleep(10) cancels cleanly, so this test does not catch _drive_with_timeout hanging while draining a task that ignores cancellation. Add a regression case where drive() stays pending after cancellation until the test releases it.

🤖 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_dev1589_setup_encoder.py` around lines 760 - 768, The hang
regression test using test_drive_with_timeout_force_closes_on_hang is too
cancellation-friendly because asyncio.sleep(10) exits on cancel, so it does not
exercise the wedged-drain path in _drive_with_timeout. Update drive() in this
test to remain pending after cancellation until the test explicitly releases it,
using a named synchronization primitive or event so the task ignores the initial
cancel and only finishes when the test allows it. Keep the existing timeout
assertion and disconnect check so the test verifies forced close behavior under
a truly cancellation-resistant hang.
src/bird_interact_agents/cloud/ray_app.py (1)

520-531: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reapply propagate=False on idempotent calls.

If another logging configuration leaves the flagged handler installed but flips propagation back on, the next per-task call returns at Line 523 and stops enforcing the duplicate-prevention invariant.

Proposed fix
     pkg = logging.getLogger("bird_interact_agents")
     pkg.setLevel(logging.INFO)
+    pkg.propagate = False
     if any(getattr(h, _ACTOR_LOG_HANDLER_FLAG, False) for h in pkg.handlers):
         return
     handler = logging.StreamHandler(sys.stdout)
     handler.setLevel(logging.INFO)
     handler.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(message)s"))
     setattr(handler, _ACTOR_LOG_HANDLER_FLAG, True)
     pkg.addHandler(handler)
-    # Don't ALSO propagate to the root WARNING handler — avoids duplicate
-    # WARNING/ERROR lines; INFO would be dropped there anyway.
-    pkg.propagate = False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bird_interact_agents/cloud/ray_app.py` around lines 520 - 531, The
per-task logging setup in the actor logger helper is not fully idempotent
because the early return prevents re-enforcing the no-duplicate logging
invariant. Update the logger configuration logic around the pkg handler check so
that pkg.propagate = False is applied on every call, even when the flagged
StreamHandler already exists, while still avoiding re-adding the handler.
🤖 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/agents/claude_sdk_otf_encode/setup_encoder.py`:
- Around line 359-363: The timeout failure path in `_drive_with_timeout` is
still unbounded because `client.disconnect()` and the subsequent task drain can
hang even after force-close. Update the teardown in `setup_encoder.py` around
the `client.disconnect()` and `task.cancel()`/`await task` sequence to apply a
secondary timeout or use a non-blocking force-close mechanism so shutdown cannot
stall indefinitely.

In `@src/bird_interact_agents/agents/claude_sdk/sdk_env.py`:
- Around line 433-436: Handle non-timeout failures during
ClaudeSDKClient.__aenter__() by disconnecting the client even when entered is
still false, so a partially started subprocess does not linger. Update the
BaseException cleanup path in the context manager around client_obj to call
_quiet_disconnect(client_obj) for failures before successful entry, while
preserving the existing _quiet_aexit(client_obj) behavior when entered is true.

In `@tests/cloud/test_ray_app.py`:
- Around line 2488-2526: The actor logging tests are leaking global logger state
because _clear_pkg_handlers only removes handlers and restores propagate, but
does not restore the bird_interact_agents logger level after
ray_app._ensure_actor_logging runs. Update the test setup/cleanup around
test_ensure_actor_logging_attaches_info_handler and
test_ensure_actor_logging_is_idempotent to use _isolated_actor_logging so the
original logger level is preserved, and keep _clear_pkg_handlers focused on
handler cleanup only.

---

Nitpick comments:
In `@src/bird_interact_agents/cloud/ray_app.py`:
- Around line 520-531: The per-task logging setup in the actor logger helper is
not fully idempotent because the early return prevents re-enforcing the
no-duplicate logging invariant. Update the logger configuration logic around the
pkg handler check so that pkg.propagate = False is applied on every call, even
when the flagged StreamHandler already exists, while still avoiding re-adding
the handler.

In `@tests/test_dev1589_setup_encoder.py`:
- Around line 760-768: The hang regression test using
test_drive_with_timeout_force_closes_on_hang is too cancellation-friendly
because asyncio.sleep(10) exits on cancel, so it does not exercise the
wedged-drain path in _drive_with_timeout. Update drive() in this test to remain
pending after cancellation until the test explicitly releases it, using a named
synchronization primitive or event so the task ignores the initial cancel and
only finishes when the test allows it. Keep the existing timeout assertion and
disconnect check so the test verifies forced close behavior under a truly
cancellation-resistant hang.
🪄 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: f23a44d0-2856-43a1-a1f4-6cde845444d9

📥 Commits

Reviewing files that changed from the base of the PR and between 18896d9 and 5119192.

📒 Files selected for processing (6)
  • src/bird_interact_agents/agents/claude_sdk/sdk_env.py
  • src/bird_interact_agents/agents/claude_sdk_otf_encode/setup_encoder.py
  • src/bird_interact_agents/cloud/ray_app.py
  • tests/cloud/test_ray_app.py
  • tests/test_dev1589_setup_encoder.py
  • tests/test_sdk_subprocess_hermetic.py

Comment thread src/bird_interact_agents/agents/claude_sdk_otf_encode/setup_encoder.py Outdated
Comment thread src/bird_interact_agents/agents/claude_sdk/sdk_env.py
Comment thread tests/cloud/test_ray_app.py Outdated
ZmeiGorynych and others added 5 commits June 29, 2026 15:00
…p, no logging leak)

- setup_encoder._drive_with_timeout: the force-close teardown (disconnect plus
  drain) was itself unbounded — the SDK disconnect has no internal timeout and a
  cancelled drive task may ignore cancellation, so the path meant to CONTAIN a
  hang could hang. Run teardown as a child task capped by TEARDOWN_TIMEOUT_S
  (asyncio.wait, no cancel); abandon it on stall and raise anyway, so the call
  always returns within budget. New cancellation-resistant regression test.
- sdk_env.hermetic_claude_sdk_session: the non-timeout enter-failure path now
  quiet-disconnects a half-started client when __aenter__ raised before the
  entered flag was set, symmetric with the timeout path (no lingering subprocess).
- ray_app._ensure_actor_logging: drop the propagate=False line — it globally
  suppressed propagation for all bird_interact_agents.* loggers, breaking
  caplog-based tests across the suite. INFO breadcrumbs still surface (handler
  on the package logger); a rare duplicate WARNING line in cloud is acceptable.
- test_ray_app: _isolated_actor_logging CM saves/restores logger level and
  propagate so the actor-logging tests do not leak global state.

Full non-integration suite green (3675 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codex flagged that the session-enter timeout path re-introduced the hang class
the PR contains: after asyncio.timeout fires, _quiet_aexit/_quiet_disconnect
awaited SDK teardown with NO bound, so a wedged __aexit__/disconnect on a
half-started CLI/MCP subprocess meant hermetic_claude_sdk_session never raised
SdkSessionEnterError — blocking indefinitely.

Add _bounded_teardown (asyncio.wait + abandon, NOT wait_for which can itself
hang cancelling an uncancellable teardown) capped at _SDK_TEARDOWN_TIMEOUT_S,
and route both _quiet_aexit and _quiet_disconnect through it. timeout_s reads
the module global at call time (a signature default binds once at import and is
unpatchable). Tests: _quiet_aexit/_quiet_disconnect return under the cap when
teardown hangs, and an enter timeout whose cleanup also wedges still raises
SdkSessionEnterError promptly. Full suite green (3678 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…out (Codex)

Codex: the timeout teardown awaited client.disconnect() BEFORE task.cancel(), so
if disconnect() hung, the drive coroutine was never even cancelled — it stayed
alive holding the SDK stream / MCP subprocess and could write into the build dir
after _encode_one_kb finalized/purged the KB. Reorder: cancel the drive task
first (always requested), then disconnect to unblock the read so the cancel
lands, then drain. Also attach a done-callback to the drive task so an
abandoned/late task never logs an unretrieved-exception warning. Full suite
green (3678 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…code mode (Codex)

- sdk_env.hermetic_claude_sdk_session: the enter_timeout_s path wrapped
  __aenter__/parity in asyncio.timeout, which cannot bound a
  cancellation-resistant CLI/MCP handshake (same class _drive_with_timeout was
  fixed for) — a wedged enter could hang before cleanup. Run enter+parity as a
  child task timed by asyncio.wait (no cancel); on expiry force-disconnect
  (entered-aware: aexit if entered else disconnect, both time-bounded) and raise
  SdkSessionEnterError. Isolated to the enter_timeout_s-is-set path, so the 8
  other claude_sdk agents (which pass None) are byte-for-byte unchanged. Removed
  the now-unused _maybe_timeout helper.
- run._validate_framework_mode: reject claude_sdk_otf_encode with any mode
  other than a-interact / one-shot at CLI/cloud validation, instead of passing
  validation and failing per-task after setup (agent.py guardrail). 4 new tests.

Full suite green (3682 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion (Codex)

Symmetric with the mode guard: the encoder builds the SLayer reference and
requires --query-mode slayer (agent.py guardrail), but _validate_slayer_setup
returned early for raw, so a raw encode run could start, build/upload setup, and
then fail every task. Reject raw for claude_sdk_otf_encode in
_validate_slayer_setup before the raw early-return (shared by local run.py and
cloud submit). 2 new tests. Full suite green (3684 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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