idle-draft: add sample_override config flag to bypass cold-start throttle

Optional config key (default false). When true, research candidates are
offered even at max_unreviewed_research_per_dossier; research_sampled
bookkeeping, mark sampled, and status flags are unchanged so unsampled
items stay visible for later human verification.

Claude-Session: https://claude.ai/code/session_01Lgv4Qn82boNFC1jn8QXSNw
This commit is contained in:
Paul O'Reilly
2026-08-03 07:26:41 +12:00
parent 29618f4319
commit bdd10cbbc1
3 changed files with 21 additions and 1 deletions

View File

@@ -683,6 +683,7 @@ def count_open_topic_proposals(repo: Path, dossier: str) -> int:
def build_ready_queue(repo: Path, config: dict, state: dict, in_flight: set) -> list[dict]: def build_ready_queue(repo: Path, config: dict, state: dict, in_flight: set) -> list[dict]:
candidates: list[dict] = [] candidates: list[dict] = []
dossiers = config["dossiers"] dossiers = config["dossiers"]
sample_override = bool(config.get("sample_override", False))
for idx, dossier in enumerate(dossiers): for idx, dossier in enumerate(dossiers):
unreviewed = count_unreviewed_research(repo, dossier, state) unreviewed = count_unreviewed_research(repo, dossier, state)
max_unreviewed = config["max_unreviewed_research_per_dossier"] max_unreviewed = config["max_unreviewed_research_per_dossier"]
@@ -693,7 +694,7 @@ def build_ready_queue(repo: Path, config: dict, state: dict, in_flight: set) ->
files = item_files(repo, dossier, f"{nn}-{slug}") files = item_files(repo, dossier, f"{nn}-{slug}")
entry = get_item_state(state, key) entry = get_item_state(state, key)
wtype, reason = next_work_type_for_item(files, entry) wtype, reason = next_work_type_for_item(files, entry)
if wtype == "research" and unreviewed >= max_unreviewed: if wtype == "research" and unreviewed >= max_unreviewed and not sample_override:
continue continue
if wtype is None: if wtype is None:
continue continue

View File

@@ -86,6 +86,12 @@ Parse `--config` as JSON. Required top-level keys: `parallel`, `providers`, `wor
verbatim. Missing required keys or malformed structure → log loudly, exit **2**. verbatim. Missing required keys or malformed structure → log loudly, exit **2**.
`--parallel` on the CLI overrides the config value. `--parallel` on the CLI overrides the config value.
Optional key `sample_override` (bool, default `false`): when `true`, the cold-start
throttle (§ dispatch, below) is bypassed — `research` candidates are offered even when
the dossier is at `max_unreviewed_research_per_dossier`. Nothing else changes:
`research_sampled` bookkeeping, `mark … sampled`, and the `status` flags are unaffected,
so unsampled research remains visible and awaiting human verification.
### 4. State load and validation ### 4. State load and validation
Read `<repo>/idle-draft.state.json`. Missing file is not an error — treat as Read `<repo>/idle-draft.state.json`. Missing file is not an error — treat as
@@ -184,6 +190,8 @@ from consideration for further dispatch until the in-flight task completes.
whose state entry does **not** have `research_sampled: true`. Once that count reaches whose state entry does **not** have `research_sampled: true`. Once that count reaches
`max_unreviewed_research_per_dossier`, no further `research` candidates are offered for `max_unreviewed_research_per_dossier`, no further `research` candidates are offered for
that dossier this cycle (draft/review candidates in that dossier are unaffected). that dossier this cycle (draft/review candidates in that dossier are unaffected).
If config `sample_override` is `true`, this throttle is skipped entirely; the unsampled
counts and `research_sampled` state are still maintained exactly as above.
**`topic_ideas`:** dossier-level (not tied to an `NN-slug`), considered only when the **`topic_ideas`:** dossier-level (not tied to an `NN-slug`), considered only when the
combined candidate list above (across all dossiers) is empty. For each dossier, eligible combined candidate list above (across all dossiers) is empty. For each dossier, eligible

View File

@@ -570,6 +570,17 @@ draft_candidates = [c["slug"] for c in tqueue if c["work_type"] == "draft"]
check("cold-start throttle: draft candidates for 01/02 unaffected by the research throttle", check("cold-start throttle: draft candidates for 01/02 unaffected by the research throttle",
set(draft_candidates) == {"01-a", "02-b"}, draft_candidates) set(draft_candidates) == {"01-a", "02-b"}, draft_candidates)
# sample_override: throttle bypassed, sampling state untouched
override_config = dict(throttle_config)
override_config["sample_override"] = True
oqueue = m.build_ready_queue(throttle_repo, override_config, throttle_state, set())
override_research = [c["slug"] for c in oqueue if c["work_type"] == "research"]
check("sample_override: item 03's research is offered despite the dossier being at the cap",
"03-c" in override_research, override_research)
check("sample_override: research_sampled state is not mutated by queue building",
not any(m.get_item_state(throttle_state, f"ai/{s}")["research_sampled"]
for s in ("01-a", "02-b", "03-c")), throttle_state)
# topic_ideas: only offered when nothing else is eligible # topic_ideas: only offered when nothing else is eligible
idle_repo = tmpdir / "idle-repo" idle_repo = tmpdir / "idle-repo"
(idle_repo / "ai").mkdir(parents=True, exist_ok=True) (idle_repo / "ai").mkdir(parents=True, exist_ok=True)