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:
+94
-2
@@ -307,8 +307,100 @@ Fixed so far:
|
||||
`queued`, an unknown-`corrects` rejection, and both events surviving a
|
||||
snapshot+replay reopen.
|
||||
|
||||
Not yet started: Codex/opencode Stop-hook-equivalent scripts, S11's
|
||||
milestone/thrash pieces. See `AUDIT.md` for the full plan.
|
||||
- **Codex/opencode Stop-hook-equivalent scripts** — the server side
|
||||
(`/v1/harness/turn`, `/v1/harness/complete` dispatching by `harness`) has
|
||||
existed since the turn-decision endpoint and the codex/opencode dispatch
|
||||
commit, but nothing called it for either harness: both lack a native Stop
|
||||
hook, so `orchestra-stop.sh`'s per-turn-boundary call never had an
|
||||
equivalent. Added `deploy/hooks/orchestra-codex-poll.sh` and
|
||||
`deploy/hooks/orchestra-opencode-poll.sh` — background poll loops (default
|
||||
60s, `ORCHESTRA_POLL_INTERVAL`) meant to run alongside the harness process
|
||||
in its pane. Each tick: find the newest session-state file (codex: newest
|
||||
`rollout-*.jsonl` under `~/.codex/sessions`, matching `CodexActiveUsage`'s
|
||||
own "most recently touched" heuristic; opencode: newest file under
|
||||
`~/.local/share/opencode/storage/message/`, the same backstop
|
||||
`OpenCodeUsage` reads, since a session id for the SSE fast path isn't
|
||||
reliably available outside the opencode process itself); if
|
||||
`.orchestra-report.md` exists, POST it plus the discovered path to
|
||||
`/v1/harness/complete` with the right `harness` value and remove the
|
||||
marker; otherwise POST task id to `/v1/harness/turn` and log (not act on)
|
||||
`refuse`/`rotate_now` — unlike the Claude Stop hook's `exit 2`, there's no
|
||||
turn boundary to refuse *at* from outside the harness process for either
|
||||
of these, so this is advisory-only until real app-server/SSE integration
|
||||
exists. No Go changes; nothing new to build/vet/test.
|
||||
|
||||
- **S11 (milestone + thrash detection) — closed, 2026-07-28.** The last two
|
||||
of S11's four rotation triggers, previously deferred as needing "transcript/
|
||||
tool-call introspection this repo doesn't have a source for" — that source
|
||||
now exists. New `internal/herdr/activity.go`:
|
||||
- `ToolCall{Name, Kind, Key, Success, IsTest}` normalizes one tool/function
|
||||
call across harnesses (`Kind` is `"file"` or `"command"`, `Key` the path
|
||||
or shell command — the same two field names, `file_path`/`command`, both
|
||||
Claude's `tool_use.input` and Codex's function-call `arguments` use).
|
||||
- `ClaudeActivity` parses the same transcript `ClaudeUsage`/`ClaudeSessionFile`
|
||||
already open, pairing `tool_use`/`tool_result` blocks by `tool_use_id`
|
||||
(an unresolved tool_use — mid-turn — is dropped, not reported).
|
||||
- `CodexActivity` parses the rollout's `function_call`/`function_call_output`
|
||||
payload pairs, mirroring `CodexUsage`'s existing `payload.type` wrapper —
|
||||
**explicitly marked best-effort/unverified** in its doc comment, same bar
|
||||
AUDIT.md's Phase 0 set for herdr methods: this hasn't been checked against
|
||||
a live rollout, only against `CodexUsage`'s already-confirmed shape.
|
||||
- `OpenCodeActivity` **refuses outright** rather than guess: opencode's
|
||||
on-disk message storage is only confirmed to carry aggregate token counts
|
||||
(what `OpenCodeUsage` already reads), not per-tool-call records, so
|
||||
fabricating a parser against an unconfirmed shape would repeat the exact
|
||||
mistake this repo's own audit exists to catch.
|
||||
- `DetectThrash(calls, ThrashConfig)` implements all three §5.3 rules — N
|
||||
consecutive failed test runs (regex-matched shell commands: go/pytest/npm/
|
||||
yarn/cargo/make/jest/mvn/rspec/ctest), the same file edited M times
|
||||
(`Edit`/`Write`/`MultiEdit`/`NotebookEdit`, explicitly *not* `Read` —
|
||||
caught by a test that initially failed because rule 3 didn't exclude
|
||||
reads), and the identical tool call repeated K times back-to-back
|
||||
(explicitly excluding test re-runs and file reads, since re-running the
|
||||
same test after a fix attempt is expected behavior, not thrashing — caught
|
||||
by another initially-failing test). Each rule that trips populates a
|
||||
`continuity.DeadEnd`, handed straight to the new prompt below rather than
|
||||
left for the agent to invent. Defaults: 3/5/4, overridable via
|
||||
`Coordinator.Thrash`.
|
||||
- `DetectMilestone(calls)` — deliberately narrow: only "the most recent
|
||||
call was a successful `git commit`". Spec-fuzzier definitions (a passing
|
||||
test suite, a finished subtask) were **not** guessed at; same restraint
|
||||
the rest of this audit has applied to unverified behavior.
|
||||
- `CLIAdapter.Activity` (adapter.go) resolves the session file the same way
|
||||
`Occupancy` does and dispatches to the right parser; `ActivityReader` is
|
||||
an optional capability like every other Face-B interface in this package.
|
||||
- New `ReasonedHandoffRequester`/`CLIAdapter.RequestHandoffReason` — like
|
||||
`RequestHandoff` but names *why* (thrash's specific dead ends, or the
|
||||
milestone reasoning) instead of the generic "context budget reached"
|
||||
framing, and asks the agent to write `meta.reason` accordingly.
|
||||
- `Coordinator.rotate()` and `TurnDecision()`: the existing "reason=manual
|
||||
bypasses occupancy" shortcut generalized to `manual`/`milestone`/`thrash`
|
||||
alike — once a handoff exists carrying one of these reasons, that **is**
|
||||
the boundary signal, same treatment as agent-initiated ROTATE already
|
||||
got. Before falling through to occupancy/soft/hard, both now call new
|
||||
`checkActivityTriggers` (adapter implements `ActivityReader`? read
|
||||
calls, thrash takes priority over milestone) and, on a hit,
|
||||
`requestReasonedHandoff` (same `HandoffRequested`-guarded ask-once pattern
|
||||
the occupancy path already uses) — request only, never release, exactly
|
||||
like the soft-threshold path's `prepare_handoff`.
|
||||
- Tests: `internal/herdr/activity_test.go` (parser + all three detector
|
||||
rules, including the two cases that caught real bugs above) and
|
||||
`internal/orchestrator/rotation_test.go`'s new
|
||||
`TestActivityTriggersRequestReasonedHandoffWithoutReleasing` (thrash and
|
||||
milestone each request-without-releasing via `TurnDecision`, a
|
||||
thrash-reasoned handoff already on disk bypasses occupancy and releases,
|
||||
and `rotate()`'s periodic path does the same request-without-release).
|
||||
- Codex/opencode Stop-hook-equivalent poll scripts
|
||||
(`deploy/hooks/orchestra-codex-poll.sh`,
|
||||
`orchestra-opencode-poll.sh`, added earlier this session) already call
|
||||
`/v1/harness/turn`, so `TurnDecision`'s new thrash/milestone checks reach
|
||||
those harnesses for free wherever `Activity` is implemented (Codex, once
|
||||
its unverified parser is checked; opencode not until a real per-tool-call
|
||||
source is found).
|
||||
|
||||
Not yet started: live verification of Phase 1 occupancy against a real
|
||||
session, verifying `CodexActivity`'s parser shape against a live rollout, and
|
||||
the two-machine federation run. See `AUDIT.md` for the full plan.
|
||||
|
||||
**Phase 0 done (2026-07-27):** this box has live TCP reachability to the real
|
||||
herdr instance at `192.168.1.105:9245` — verified by hand (raw JSON-RPC
|
||||
|
||||
Reference in New Issue
Block a user