Files
mopac-pmo/archive/REPORT-20260829-1800-fleet-unlocks.md

120 lines
6.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# REPORT — fleet unlocks: turn pool (529) + Redmine-claim dispatch (530)
- date: 2026-08-29T23:00:00Z (lane: dedicated fleet-unlocks lane, off loop rotation)
- repo: ukrrs/MOPAC harness @ main, pushed through `dca9b06`
- baseline: `a3d312c` → 5 commits, 17 files, +1550/61
- verification: `./dev.sh check` green (build + vet + test, 12 packages ok) on the
final tree; pushed HEAD builds standalone
- Redmine writes during this lane: **NONE** (all testing against in-process fakes)
## Commits (in order, all pushed to origin/main)
| commit | what |
|---|---|
| `cfdfaf7` | 529 config knobs: `[loop] max_concurrent_turns` + `[loop.repo_map]` (+2 tests) |
| `8b27127` | 529 turn pool: semaphore + per-repo mutex, scan integration, heartbeat active/pending, fixes a REPORT tmp-file race the pool exposed (+7 tests) |
| `d71bc91` | 530 journals read primitive: `IssueJournals` via `?include=journals` (+1 test) |
| `82cfb01` | 530 claim dispatch: claim-before-turn, epoch race resolution, stale sweep, failure/park release (+9 tests) |
| `dca9b06` | fixup: pool.go reserve/launch refactor that `82cfb01` calls (see Incident note) |
### Incident note (transparency)
`82cfb01` was pushed with `daemon.go` staged but the matching `pool.go`
refactor left unstaged — pushed HEAD did not compile for ~4 minutes until
`dca9b06` landed (verified broken, then verified building after). No other
tree was affected; the final HEAD is build+check green.
## Part 1 — 529 turn pool
- `[loop] max_concurrent_turns` (default 1 = sequential v0; fleet 2-3).
Bounded worker pool: channel semaphore caps concurrent turns per scan;
tasks that don't fit stay **pending** and are reconsidered next scan
(never dropped, never marked processed).
- Per-repo mutex: one turn per work repo at a time, different repos in
parallel. Repo derived from `[loop.repo_map]` by task category (Class
field) or task-id prefix; unmapped tasks share the harness root
(work_root). Slot + repo lock are reserved *before* claiming/dispatch so
a claimed issue always gets its turn immediately.
- Scan-end barrier: a scan returns only after every dispatched turn
settles (dedup consistency, clean `--once` exits).
- status.json additions: `active_turns` (live), `pending` (per scan),
`max_concurrent_turns`.
- Bonus fix the pool exposed: concurrent REPORT writes into one directory
raced on a shared `.tmp` name (one rename stole another turn's file);
tmp names are now unique per write.
## Part 2 — 530 Redmine-claim dispatch
- `[redmine] claim_enabled` (default off = single-loop v0), `claim_status`
(default "In Progress"), `claimable_status` (default "New").
- On dispatch, BEFORE the turn: PUT status → In Progress + journal note
`claimed by <vertical>@<host> at <RFC3339>`.
- Race resolution (no CAS in the Redmine API): the **first claimant of the
current epoch** wins. Epoch = journal tail since the last boundary note
(a `claim released…` or `# REPORT -…` note). Losers see a foreign first
claim, clear their own junk claim with a concede note, and skip to the
next candidate. Boundaries keep history from blocking future claims.
- Success → existing `[redmine.status_map]` keyed on the claim status
("In Progress" → Resolved/Done). Failure/timeout/park → claim released
back to New + explanatory note (next scan retries; a release that can't
land because Redmine is down is backstopped by the stale sweep).
- Stale-claim release: claim older than **2× `turn_timeout_secs`** with no
REPORT after it → status back to New + release note, swept every scan.
Issues with no loop-claim note (humans moving cards) are never touched.
Turn timeout 0 disables the sweep.
- New JSONL events: `claim`, `claim_lost`, `release`.
## Tests (18 new, full suite green)
- pool (5): bound respected (peak concurrency == 2, backlog drains), per-repo
serialization (peak == 1 with pool 3), cross-repo parallelism
(deterministic rendezvous), repo_map mapping table, heartbeat counters.
- claim (9): two-loop race exactly-one-wins (both claims interleave via a
response-hold in the fake; asserts 1 turn, 1 REPORT, 1 concede), stale
release + redispatch, fresh-claim untouched, human-In-Progress untouched,
claim-write failure skips candidate (next candidate still dispatched),
lost-race skip + concede, turn-failure release + retry, epoch parsing table.
- writeback (2): journals decode (object-shaped user, note-less entries
skipped), concurrent REPORT writes in a shared dir.
- config (2): pool knobs parse/defaults/validation, claim knobs
parse/defaults/validation.
- Fixed one latent test race surfaced by mid-scan heartbeat beats
(TestLoopStatusFileHeartbeat now waits for a completed scan).
## Smoke evidence — live daemon, max_concurrent_turns=2, fake Redmine+LLM
Real `RunLoop` (`--once` scans), 3 tasks across 3 repos, pool 2:
```
scan 1: 3 task(s) in scope, 3 new/updated
dispatch 803 ... dispatch 801 <- two turns interleaved
REPORT 801 / REPORT 803 <- both land while scan holds
pool: 2 dispatched, 1 pending (max 2 turn(s) at once; pending reconsidered next scan)
scan 2: 1 new/updated -> dispatch 802 -> noted -> Done <- backlog drained
peak concurrent LLM requests: 2 (pool bound 2)
status.json: {"active_turns":0,"pending":0,"max_concurrent_turns":2,
"dispatched":1,"reports":1,"running":false,...}
```
Two-loop race test output evidence (from the suite): both verticals claim,
exactly one turn runs, loser logs `claim lost #700 ... skipping to next
candidate`, winner completes `In Progress -> Done`.
## Config knobs (fleet rollout)
```toml
[loop]
max_concurrent_turns = 2 # or 3
[loop.repo_map]
code = "/srv/work/harness"
study = "/srv/work/notes"
[redmine]
claim_enabled = true
claim_status = "In Progress" # default
claimable_status = "New" # default
```
Fleet loops: set both blocks; keep `[redmine.status_map] "In Progress" =
"Resolved"` (it now keys off the claim status). Single-loop hosts need no
config change (both features default off/1).