feat(harbor): feedback levers: failure transcripts, multi-fidelity screening, per-attempt detail#30
Conversation
…ck_transcripts) When enabled, collation attaches the last feedback_max_bytes bytes of a failed sample's trial transcript (agent/terminus_2.pane, falling back to agent/trajectory.json) to SampleResult.feedback: the first failed attempt only, passed samples carry nothing, missing transcripts are omitted silently. Exposure to the agent stays gated by the sidecar's tier routing (per-sample files are written only for viewable splits), so nothing can leak to non_viewable or no_access tiers. Plumbed build.yaml -> serve.json -> ServeConfig -> HarborRunner, mirroring score_baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…multifidelity) When enabled, the compiled instruction gains a section teaching the optimizer to triage rough ideas on subset evals (--num-samples / --sample-ids) and spend full-split evals only on survivors, stating the true economics: every eval debits one run-budget unit regardless of size, while the sample budget is debited only for the samples actually run, and subset aggregates are noisier. The section is gated on introspecting EvalRequest for the subset-eval fields (the same merge-order-truthfulness pattern as the free-baseline bullet), so it can never render against a sidecar that lacks subset evals. Plumbed build.yaml -> serve.json -> ServeConfig for contract parity; consumption is compile-time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_detail)
When enabled, each collated sample's output carries an attempts list, one
{reward, exception} entry per attempt in stable attempt order: reward is
None when the attempt died before the verifier scored it, exception is the
recorded exception class name (None for clean attempts). Collation now
loads the attempt groups under 'best' aggregation too when a lever needs
them, without touching best-mode scoring. Exposure rides the same
viewable-only per-sample files as transcript feedback, so nothing reaches
non_viewable or no_access tiers. Plumbed build.yaml -> serve.json ->
ServeConfig -> HarborRunner, mirroring score_baseline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tlist pollution, and feedback-cap edge cases Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| continue | ||
| # errors="replace": a multibyte char straddling the cap boundary is | ||
| # rendered as U+FFFD rather than crashing the collation. | ||
| return data[-self.feedback_max_bytes :].decode("utf-8", errors="replace") |
There was a problem hiding this comment.
Empty transcript file silently produces
"" feedback
data[-self.feedback_max_bytes :].decode(...) on an empty file returns "". Because the check in _failure_feedback is if tail is not None, an empty string passes and the function immediately returns "" as feedback — rather than continuing to try trajectory.json (or skipping the attempt altogether). A caller checking if result.feedback will treat it as absent, but the JSON serialization will emit "feedback": "" instead of "feedback": null, which could confuse an optimizer. Adding if not data: continue before the decode would skip empty files and let the fallback candidate run.
Prompt To Fix With AI
This is a comment left during a code review.
Path: vero/src/vero/harbor/runner.py
Line: 480
Comment:
**Empty transcript file silently produces `""` feedback**
`data[-self.feedback_max_bytes :].decode(...)` on an empty file returns `""`. Because the check in `_failure_feedback` is `if tail is not None`, an empty string passes and the function immediately returns `""` as feedback — rather than continuing to try `trajectory.json` (or skipping the attempt altogether). A caller checking `if result.feedback` will treat it as absent, but the JSON serialization will emit `"feedback": ""` instead of `"feedback": null`, which could confuse an optimizer. Adding `if not data: continue` before the decode would skip empty files and let the fallback candidate run.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Fixed at the stack tip in bd1d1fe (harbor-9-ops, PR #36 branch): _read_transcript_tail now skips empty transcript files, so an empty pane falls through to the trajectory, and if every candidate is empty it returns None and the search moves to the next failed attempt instead of short-circuiting on "". Regression tests: test_empty_pane_falls_through_to_trajectory, test_all_empty_transcripts_yield_no_feedback.
| if not rewards: | ||
| return SampleResult( | ||
| error=f"No verifier rewards for task '{task_name}'.", | ||
| output={"task_name": task_name, "trial_name": trial.get("trial_name")}, | ||
| output=_out( | ||
| {"task_name": task_name, "trial_name": trial.get("trial_name")} | ||
| ), | ||
| **common, |
There was a problem hiding this comment.
Error-path SampleResults never receive a feedback transcript
The error branch (trial exists but verifier produced no rewards — agent died before scoring) calls _out (so expose_attempt_detail still populates output["attempts"]), but never calls _failure_feedback, leaving feedback at its implicit None. Since _failure_feedback only iterates attempts whose rewards are 0.0, the crash transcript of an attempt that died before the verifier ran is also unreachable through the scored-failure path. Users who set feedback_transcripts: true expressly to understand agent crashes will get no transcript for the most common failure mode (process exit / timeout before the verifier runs).
Prompt To Fix With AI
This is a comment left during a code review.
Path: vero/src/vero/harbor/runner.py
Line: 368-374
Comment:
**Error-path SampleResults never receive a feedback transcript**
The error branch (trial exists but verifier produced no rewards — agent died before scoring) calls `_out` (so `expose_attempt_detail` still populates `output["attempts"]`), but never calls `_failure_feedback`, leaving `feedback` at its implicit `None`. Since `_failure_feedback` only iterates attempts whose rewards are 0.0, the crash transcript of an attempt that died before the verifier ran is also unreachable through the scored-failure path. Users who set `feedback_transcripts: true` expressly to understand agent crashes will get no transcript for the most common failure mode (process exit / timeout before the verifier runs).
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Fixed at the stack tip in bd1d1fe (harbor-9-ops, PR #36 branch): the no-verifier-rewards error branch now calls _failure_feedback(0.0, attempts), so a sample whose agent died before scoring carries the crash transcript. You are right that this branch matters: a candidate edit that crashes the agent lands exactly here, and without the transcript the optimizer has no way to see the crash it caused. Regression test: test_no_rewards_error_sample_carries_crash_transcript.
Two Greptile P2s on #30, fixed at the stack tip: - An empty transcript file no longer surfaces as "" feedback: empty candidates are skipped (an empty pane falls through to the trajectory), and if everything is empty the search moves to the next failed attempt rather than short-circuiting on "". - The no-verifier-rewards error branch (agent died before scoring) now attaches the failure transcript like any failed sample: a candidate edit that crashes the agent lands exactly here, and the transcript is the only way the optimizer can see the crash it caused. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds three opt-in feedback levers to Mode B, answering the question this stack kept circling: the optimizer only gets one score per paid eval, which is very thin signal. Each lever is a
build.yamlswitch, default off, byte-identical behavior when off.feedback_transcripts(+feedback_max_bytes, default 3000): every FAILED sample of an eval carries the tail of its rollout transcript in the per-samplefeedbackfield. Rides the per-sample result files the sidecar writes ONLY forviewablesplits, so it can never surface fornon_viewable/no_accesstiers.instruct_multifidelity: the compiled instruction teaches subset-eval screening (triage rough ideas cheaply, confirm survivors on the full split). Renders only when a viewable split exists (see leak fix below).expose_attempt_detail: per-sampleattemptslist with{reward, exception}per attempt, same viewable-only exposure.The final commit closes what adversarial review found: a multi-fidelity hidden-split leak (1-sample subset evals could recover per-sample labels on
non_viewablesplits; the instruction is now gated to viewable splits), subset evals polluting the auto_best shortlist (ranking now prefers full-split evals),feedback_max_bytes <= 0returning the whole transcript (now means "no feedback"), symlink/path-escape containment on transcript reads, typo-safe config keys (extra="forbid"), and attempt-ordering.Live experimental evidence (2026-07-06). Three arms on the same substrate (the VeRO paper's minimal Pawn agent on gpt-4.1-mini, baseline 0.20, ceiling ~0.43), same optimizer (opus), same 10-eval budget, only the levers differ:
Transcripts changed the optimizer from prompt-tweaker (which never moves the score; see the campaign doc's 6-model flatness result) into code-fixer, which is where all measured gains come from. n=1 per arm; a replication and a 25-eval run are in flight.
Stacked on the harbor stack (base:
harbor-all-fixes, the integration union of the open PRs; will re-target as the stack merges).🤖 Generated with Claude Code
Greptile Summary
Adds three opt-in feedback levers to Mode B (
feedback_transcripts,instruct_multifidelity,expose_attempt_detail), all off by default and byte-identical when disabled. The PR also closes several adversarial-review findings: a hidden-split leak through 1-sample subset evals, subset evals inflating theauto_bestshortlist, transcript symlink/path-escape exfiltration,feedback_max_bytes <= 0emitting the whole transcript, and typo-safe config keys viaextra="forbid".feedback_transcripts): Attaches the tail of a failed trial's transcript (terminus_2.pane→trajectory.jsonfallback) toSampleResult.feedbackfor samples scoring exactly 0.0. Includes byte-cap, symlink rejection, path-confinement, and multibyte-boundary safety. Exposure is gated upstream by the sidecar writing per-sample files only for viewable splits.instruct_multifidelity): Renders a subset-eval screening section in the compiledinstruction.md, gated on both a viewable split existing andEvalRequestactually acceptingsample_ids/num_samplesin the current tree — prevents teaching a knob the sidecar would 400 on.expose_attempt_detail) + verifier fix: Populatesoutput["attempts"]with per-attempt{reward, exception}entries;auto_bestranking now prefers full-split evals over subset evals when any exist, preventing a lucky 1-sample eval from displacing a genuinely better candidate.Confidence Score: 4/5
Safe to merge. All three levers are default-off and gated behind explicit flags, preserving byte-identical behavior for existing deployments.
The implementation is thorough: symlink/path-escape containment, feedback_max_bytes<=0 guard, attempt sort stability, tier-gate tests, and the multi-fidelity hidden-split leak fix are all present and well-tested. Two edge cases in runner.py are worth a follow-up: an empty transcript file (b"") is decoded to "" and returned immediately rather than falling through to trajectory.json, and the error-path SampleResult (agent died before the verifier ran) never calls _failure_feedback, so feedback_transcripts=true provides no transcript for the most common crash scenario.
vero/src/vero/harbor/runner.py — _read_transcript_tail (empty-file early-return) and _sample_result's error branch (feedback not populated)
Important Files Changed
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(harbor): close multi-fidelity hidden..." | Re-trigger Greptile