idle-draft: clear attempts on success so forced redos start fresh
Claude-Session: https://claude.ai/code/session_01YQDoWNM7XPPii28khFWoMc
This commit is contained in:
@@ -1096,11 +1096,30 @@ def log_gates(repo: Path, gates: dict, credential_ok: dict) -> None:
|
||||
|
||||
|
||||
def apply_success_state(state: dict, candidate: dict) -> None:
|
||||
# Successful dispatch doesn't itself set a human gate; nothing to flip here.
|
||||
# (human_edit_done / research_sampled / approved are human-only via `mark`.)
|
||||
# Present for symmetry with apply_failure_state and as the extension point
|
||||
# if a future work type needs to record something on success.
|
||||
"""On success, clear any failed-attempt history for this (item, work_type).
|
||||
Without this, a success following one or more content failures leaves
|
||||
attempts[work_type] at its prior nonzero value; if a human later deletes
|
||||
the produced file to force a redo, the item would start already one
|
||||
content failure away from `blocked` instead of fresh. Pruning (rather
|
||||
than zeroing) the key/subkey keeps state minimal and matches how
|
||||
get_item_state()/validate_state() already treat an absent attempts entry
|
||||
as equivalent to zero -- nothing else (human_edit_done / research_sampled
|
||||
/ approved) is a success-path concern; those are human-only via `mark`."""
|
||||
if candidate["kind"] != "item":
|
||||
return
|
||||
key = candidate["item_key"]
|
||||
work_type = candidate["work_type"]
|
||||
if work_type not in ALLOWED_ATTEMPT_WORK_TYPES:
|
||||
return
|
||||
entry = state.get("items", {}).get(key)
|
||||
if entry is None:
|
||||
return
|
||||
attempts = entry.get("attempts")
|
||||
if not attempts or work_type not in attempts:
|
||||
return
|
||||
del attempts[work_type]
|
||||
if not attempts:
|
||||
del entry["attempts"]
|
||||
|
||||
|
||||
def apply_failure_state(state: dict, candidate: dict, max_attempts: int) -> str | None:
|
||||
|
||||
@@ -265,7 +265,14 @@ For the selected `(item_or_dossier, work_type, provider)`:
|
||||
`max_attempts` (2) content failures for that work type, set
|
||||
`state.items[item].blocked` with a reason (the item is then excluded from all
|
||||
further dispatch until a human runs `mark unblock`).
|
||||
- **Exit 0 and validation passes** → success.
|
||||
- **Exit 0 and validation passes** → success. Clears (deletes, not zeroes)
|
||||
`state.items[item].attempts[work_type]` if present, and drops `attempts`
|
||||
entirely if it's now empty — a success following one or more prior content
|
||||
failures for that work type resets the item's attempt history for that work
|
||||
type, so a later human-forced redo (deleting the produced file and letting
|
||||
idle-draft regenerate it) starts fresh rather than already one content
|
||||
failure from `blocked`. Other work types' counters on the same item are
|
||||
untouched.
|
||||
7. Validation (on exit 0, before promotion): the temp output file must be non-empty and
|
||||
its first non-blank line must be a top-level Markdown heading (`# ...`). For
|
||||
`research` work type specifically: every absolute path matching `/home/\S+` cited in
|
||||
@@ -400,6 +407,7 @@ a lighter-touch check); `draft` and `review` carry no `allowed_tools` key.
|
||||
|---|---|---|---|
|
||||
| Transient | timeout, HTTP 429/5xx, network reset | not incremented | retried next eligible cycle |
|
||||
| Content | empty output, missing heading, dead citation path, non-retryable non-zero exit | incremented | `.rejected` kept; `blocked` at `max_attempts` (2) |
|
||||
| Success | — | cleared (key deleted, `attempts` dropped if now empty) for that work type only | output promoted, committed |
|
||||
|
||||
## Exit codes
|
||||
|
||||
|
||||
@@ -670,6 +670,40 @@ check("signal handling: install_signal_handlers installs a non-default SIGINT ha
|
||||
_signal.signal(_signal.SIGTERM, _signal.SIG_DFL)
|
||||
_signal.signal(_signal.SIGINT, _signal.SIG_DFL)
|
||||
|
||||
# --- apply_success_state: clears attempts for the succeeded work type only ---
|
||||
success_state = {"items": {"ai/01-topic-one": {"human_edit_done": True, "attempts": {"draft": 1, "research": 2}}}}
|
||||
success_candidate = {"kind": "item", "item_key": "ai/01-topic-one", "work_type": "draft"}
|
||||
m.apply_success_state(success_state, success_candidate)
|
||||
entry_after = success_state["items"]["ai/01-topic-one"]
|
||||
check("apply_success_state: clears the succeeded work type's attempts key",
|
||||
"draft" not in entry_after.get("attempts", {}), entry_after)
|
||||
check("apply_success_state: leaves an unrelated work type's attempts counter untouched",
|
||||
entry_after.get("attempts", {}).get("research") == 2, entry_after)
|
||||
check("apply_success_state: leaves other item fields (e.g. human_edit_done) untouched",
|
||||
entry_after.get("human_edit_done") is True, entry_after)
|
||||
|
||||
# When the succeeded work type was the item's only attempts entry, the whole
|
||||
# `attempts` key is dropped (not left behind as an empty {}).
|
||||
success_state2 = {"items": {"ai/01-topic-one": {"attempts": {"draft": 1}}}}
|
||||
m.apply_success_state(success_state2, success_candidate)
|
||||
entry_after2 = success_state2["items"]["ai/01-topic-one"]
|
||||
check("apply_success_state: drops the attempts key entirely once it's empty",
|
||||
"attempts" not in entry_after2, entry_after2)
|
||||
|
||||
# No prior attempts entry at all -> no-op, no KeyError, no spurious key created.
|
||||
success_state3 = {"items": {"ai/01-topic-one": {}}}
|
||||
m.apply_success_state(success_state3, success_candidate)
|
||||
check("apply_success_state: no-op when the item has no attempts history",
|
||||
success_state3["items"]["ai/01-topic-one"] == {}, success_state3)
|
||||
|
||||
# Result is well-formed state per validate_state (schema still satisfied post-clear).
|
||||
try:
|
||||
m.validate_state(success_state, repo)
|
||||
validate_ok = True
|
||||
except m.StateValidationError:
|
||||
validate_ok = False
|
||||
check("apply_success_state: resulting state still passes validate_state", validate_ok, success_state)
|
||||
|
||||
for status, name, detail in results:
|
||||
print(f"{status}: {name}", detail if detail else "")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user