Skip to content

feat(capture): bound sync's download by max_cases; fill slots with leading turns - #58

Merged
babaliauskas merged 3 commits into
mainfrom
feat/capture-fetch-budget
Oct 19, 2026
Merged

babaliauskas merged 3 commits into
mainfrom
feat/capture-fetch-budget

Conversation

@babaliauskas

Copy link
Copy Markdown
Collaborator

What this changes

This follows up #57 (unreleased) after its first sync against a real bucket: 52 captures, max_cases: 10.

  • Download budget. With captures.max_cases set, sync's automatic fetch takes only each suite's newest 3 × max_cases captures inside max_age. "Newest" goes by the bucket's last-modified time; an unknown time counts as oldest, and ties go by key. Captures already local or promoted count toward the budget, so a re-sync with nothing new downloads nothing. Before this, sync downloaded every capture in the window even though it uses at most max_cases per suite. At thousands of captures a day, that meant thousands of downloads per sync. The summary names the cut: (skipped 2 beyond each suite's newest 30). --since replaces the age cutoff, not the budget. --repair still fetches what its rebuilds need.
  • capture list fetches what sync would, applying the same window and budget. Before, it fetched the entire bucket. capture fetch is unchanged.
  • Leading-turn fill. Once no whole conversation fits a suite's free slots, the slots left take the next conversation's leading turns. Before, a lone capture plus a 10-turn conversation under max_cases: 10 produced a 1-case suite.
  • Missing earlier turns. Under the limits, a conversation turn that recorded no messages list of its own rebuilds its history from the turns before it. Such a turn is now eligible only when all of those earlier turns are local; previously it was promoted with a truncated history. Sync counts these as not eligible: N missing earlier turns and warns once per suite with the likely causes:
    • the fetch window or budget cut the earlier turns off;
    • they were promoted on another machine, or removed by capture clean;
    • the SDK's per-turn sample rate dropped them;
    • the app numbers turn_index from 1.
  • Quieter output. With max_cases set, build warnings print only for captures that become cases, after the case file is written.

With neither limit set, nothing changes.

Why

The first real sync showed both problems: a fetch that grows with the bucket, and a capped suite left almost empty.

Test plan

  • make ci: 3288 passed, 1 skipped (the existing evalshift-server skip), coverage 95.19%. ruff, ruff format, mypy --strict and pre-commit run --all-files are clean.
  • New tests:
    • budget ranking: per suite; local and promoted captures count; the window takes no slots; forced ids, unknown times and ties;
    • --since with the budget;
    • capture list fetching within the limits;
    • leading-turn fill, and a stable re-sync;
    • missing earlier turns: the warning, max_age alone, turn_index None, and an earlier turn that is local but ineligible;
    • warnings only for written cases.
  • Offline, on a copy of testApp in the state feat(capture): bounded capture suites (captures.max_age / captures.max_cases) #57's sync left it: converter went from 1 case to 10 (the lone case plus turns 0–8). Only those 9 cases printed warnings. A re-sync left every suite file byte-identical.
  • Read-only sync from the real bucket into an empty folder: fetched 50 capture(s) … (skipped 2 beyond each suite's newest 30), giving converter 10, planner 10 and triage 9 cases.
  • Independent review of the branch. All findings were fixed: the sampling and turn_index caveats, the capture clean wording, stale docs, and warning order.

Follow-up (not in this PR)

The SDK's EVALSHIFT_SAMPLE_RATE draws once per agent run, which means once per turn, so a sampled conversation keeps some turns and loses others. Drawing per conversation_id (hash-based) in the SDK would keep conversations whole.

🤖 Generated with Claude Code

babaliauskas and others added 3 commits October 19, 2026 22:36
With captures.max_cases set, the fetch takes only each suite's newest
3 x max_cases captures inside max_age (by the store's last-modified
time, ties by key). Captures already local or promoted count toward the
budget, so a re-run with nothing new downloads nothing. capture list
now fetches the same window and budget instead of the whole bucket.

Under limits, a turn whose history would be rebuilt from earlier turns
that are not on disk (cut by the budget or window, or promoted on
another machine) is no longer eligible: "missing earlier turns".

When no whole conversation fits a suite's free slots, the slots left
take the leading turns of the next one, so a long conversation can no
longer leave a capped suite nearly empty. Build warnings now print only
for captures that became cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…write

Sync now prints one warning per suite when turns are not promoted for
missing earlier turns, naming the causes: the fetch window or budget,
turns promoted elsewhere or removed by capture clean, the SDK's per-turn
sample rate, or a turn_index numbered from 1. A deferred build warning
now prints only after its case file is written.

Docs: the budget bounds each sync, not the local mirror; a part-added
conversation's later turns can only enter while its earlier captures are
local; EVALSHIFT_SAMPLE_RATE draws per turn; capture list's fetch and the
max_cases download cap in the field table and capture fetch entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@babaliauskas
babaliauskas merged commit 91a8ff8 into main Oct 19, 2026
4 checks passed
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