From 66feb7e9ff24cd286a544a130de005b42d300616 Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 2 Aug 2026 14:13:26 +0400 Subject: [PATCH] chore: add TASK.md for #460 Co-Authored-By: Claude Opus 5 --- TASK.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 TASK.md diff --git a/TASK.md b/TASK.md new file mode 100644 index 00000000..867bde5a --- /dev/null +++ b/TASK.md @@ -0,0 +1,54 @@ +# Vikunja: #460 — Retry budget: track all seen gate fingerprints, charge on repeat (cycle detection) + +**Problem** + +`DefaultRetryCoordinator.decide` compares the current failure fingerprint against only the *previous* one (`core/kernel/src/main/kotlin/com/correx/core/kernel/retry/DefaultRetryCoordinator.kt:29`): + +```kotlin +val prevFingerprint = state.gateFailureFingerprints[gate] +val progressed = prevFingerprint != fingerprint +val charged = !progressed +``` + +A single slot cannot see a cycle. Whack-a-mole (fix A breaks B, fix B breaks A) alternates two fingerprints forever, every round reads as progress, nothing is ever charged, and the per-gate budget never triggers. `DefaultSessionOrchestratorStep.kt:296` already acknowledges this ("can be fooled into treating as still progressing indefinitely") but guards only inside the recovery stage. A normal implementer stage has no equivalent guard. + +**Fix** + +Make `OrchestrationState.gateFailureFingerprints[gate]` a `Set<String>` of every fingerprint seen for that gate, and charge when the current one is a repeat: + +```kotlin +val seen = state.gateFailureFingerprints[gate] +val charged = fingerprint in seen +``` + +Touches: `OrchestrationState` field type, the reducer that writes it, `DefaultRetryCoordinator.decide`. + +**Why not count-based progress** + +Rejected: "charge unless the failing-item count went down". A syntax error masks later errors — fix `'}' expected` and tsc parses further, reporting 5 real type errors where there was 1. Count goes 1 to 5 on the most productive edit of the run, and the guard would charge the budget for genuine progress. + +**Cases** + +- Unmasking, `{syntax}` to `{5 type errors}`: new fingerprint, unseen, free. Correct. +- Grinding down, 3 to 2 to 1 errors: each state distinct, all free. Correct. +- Whack-a-mole, A/B/A: round 3 repeats round 1, charged. This is the case the single slot misses today. + +**Known limit** + +Detects cycles, not progress. A stage emitting a fresh distinct failure every round is still unbounded; only `stageCount` catches it. Worth measuring with `scripts/artread.py` over a few failed sessions before assuming that pattern matters. + +## Acceptance criteria + +- [ ] (fill in before starting) + +## Quality gate + +```sh +# the command that must pass, e.g. make check +``` + +## Rules + +- Do not edit this file. +- One task, one session, one PR. Keep the diff under ~300 lines. +- Finish with `task pr`.