feat(orchestrator): milestone rotation and thrash detection (S11)
Closes the last two S11 triggers. internal/herdr/activity.go normalizes tool/function calls per harness (ClaudeActivity verified against the existing transcript format, CodexActivity best-effort/unverified, OpenCodeActivity refuses — no confirmed per-tool-call source exists) and implements the three thrash rules plus a narrow milestone check (successful git commit as the last call). CLIAdapter.RequestHandoffReason asks the agent to write a handoff with meta.reason set, same "ask, don't invent" pattern as the existing handoff/ report requests. rotate() and TurnDecision generalize the manual-bypass shortcut to manual/milestone/thrash and request (never directly release) on a detected trigger. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
This commit is contained in:
@@ -174,7 +174,7 @@ worth a deliberate decision rather than an accident.)
|
||||
| S8 | §3.1 | No compensation-event mechanism exists. Append-only holds, but the spec's *correction* path ("a compensating event is appended") has no implementation. |
|
||||
| S9 | `main.go:482` | Two independent lease-expiry loops (`main.go` 1s ticker and `Coordinator.expire` 30s) race on the same reclaim. Harmless today only because version CAS rejects the loser. |
|
||||
| S10 | federation | `federation.Registry.Register` accepts a self-declared `id` + self-chosen `token` from any caller — registration is admission-control-free. |
|
||||
| S11 | §5.3 | Thrash detection, the soft ~55% threshold, milestone rotation, and agent-initiated `ROTATE` are all absent. Only the hard threshold exists, and it's unreachable (B1). |
|
||||
| S11 | §5.3 | ~~Thrash detection, the soft ~55% threshold, milestone rotation, and agent-initiated `ROTATE` are all absent. Only the hard threshold exists, and it's unreachable (B1).~~ Closed 2026-07-28 — soft threshold, agent-initiated `ROTATE`, milestone, and thrash detection all landed. Codex's activity parser is unverified against a live rollout; opencode has no verified tool-call source and refuses rather than guess. |
|
||||
|
||||
---
|
||||
|
||||
@@ -942,8 +942,66 @@ boundary=false (both would refuse/continue under every other path), a
|
||||
worktree, and asserts `TurnRotateNow` + `Release` invoked + task state
|
||||
`StateQueued`.
|
||||
|
||||
**Still open from S11:** milestone rotation and thrash detection — both need
|
||||
a source of transcript/tool-call data this repo doesn't have yet.
|
||||
**Still open from S11 (at this point):** milestone rotation and thrash
|
||||
detection — both need a source of transcript/tool-call data this repo
|
||||
doesn't have yet.
|
||||
|
||||
`go build ./...`, `go vet ./...`, `go test ./...` all pass.
|
||||
|
||||
## S11 — milestone + thrash detection, closed, 2026-07-28
|
||||
|
||||
The transcript/tool-call source both preceding entries said this repo
|
||||
lacked now exists: `internal/herdr/activity.go`. `ToolCall{Name, Kind, Key,
|
||||
Success, IsTest}` normalizes one tool/function call, harness-agnostically.
|
||||
`ClaudeActivity` reads the same transcript file `ClaudeSessionFile`/
|
||||
`ClaudeUsage` already open, pairing `tool_use`/`tool_result` blocks by
|
||||
`tool_use_id` (an unresolved tool_use is dropped, not reported — the session
|
||||
is still mid-turn). `CodexActivity` mirrors the `payload.type` wrapper
|
||||
`CodexUsage` already reads, parsing `function_call`/`function_call_output`
|
||||
pairs — **explicitly marked best-effort/unverified**, same bar Phase 0 set
|
||||
for herdr methods: not yet checked against a live rollout. `OpenCodeActivity`
|
||||
**refuses outright** — opencode's on-disk message storage is only confirmed
|
||||
to carry aggregate token counts, not per-tool-call records, so this doesn't
|
||||
guess at an unconfirmed shape (same convention as
|
||||
`CLIAdapter.resolveSessionFile`'s opencode case).
|
||||
|
||||
`DetectThrash(calls, ThrashConfig)` implements all three §5.3 rules — N
|
||||
consecutive failed test runs, the same file edited M times (edit tools only,
|
||||
explicitly excluding `Read`), and an identical tool call repeated K times
|
||||
back-to-back (excluding test re-runs and file reads, since those are
|
||||
expected, not thrashing). Two false positives were caught by tests before
|
||||
being trusted: rule 3 initially tripped on repeated test-command re-runs and
|
||||
on repeated `Read`s of the same file; both fixed by narrowing rule 3's
|
||||
"relevant" calls to non-test commands and edit-tool file calls only.
|
||||
`DetectMilestone(calls)` is deliberately narrow — only "the last call was a
|
||||
successful `git commit`" — fuzzier definitions (a passing suite, a finished
|
||||
subtask) were not guessed at.
|
||||
|
||||
New `ReasonedHandoffRequester`/`CLIAdapter.RequestHandoffReason`
|
||||
(adapter.go) — like `RequestHandoff` but names the specific reason
|
||||
(thrash's dead ends, or the milestone framing) and asks the agent to write
|
||||
`meta.reason` accordingly, continuing the "ask, don't invent" pattern
|
||||
already used for `.orchestra-report.md` and `.orchestra-handoff.json`.
|
||||
`Coordinator.rotate()` and `TurnDecision` generalize the existing
|
||||
"`reason=manual` bypasses occupancy" shortcut to `manual`/`milestone`/
|
||||
`thrash` alike, and both call new `checkActivityTriggers` (skips cleanly if
|
||||
the adapter doesn't implement `ActivityReader`) before falling through to
|
||||
occupancy/soft/hard; a hit calls `requestReasonedHandoff` (same
|
||||
`HandoffRequested`-guarded ask-once pattern the occupancy path already
|
||||
uses) — request only, never release, matching the soft-threshold path's
|
||||
`prepare_handoff` behavior.
|
||||
|
||||
Covered by `internal/herdr/activity_test.go` (parser + all three detector
|
||||
rules, including the two cases above that caught real bugs) and
|
||||
`internal/orchestrator/rotation_test.go`'s
|
||||
`TestActivityTriggersRequestReasonedHandoffWithoutReleasing` (thrash and
|
||||
milestone each request-without-releasing via `TurnDecision`; a
|
||||
thrash-reasoned handoff already on disk bypasses occupancy and releases;
|
||||
`rotate()`'s periodic path does the same request-without-release).
|
||||
|
||||
**Still open:** verifying `CodexActivity`'s parser shape against a live
|
||||
rollout, and finding a real per-tool-call source for opencode (currently
|
||||
refuses rather than guessing).
|
||||
|
||||
`go build ./...`, `go vet ./...`, `go test ./...` all pass.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user