Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,12 @@ DISPATCH_AGENT_TOKEN="your_agent_token_here"
# worker-side readiness check. Bounded to 60 000–604 800 000; default 21 600 000 (6h).
# PR_FIX_EXTERNAL_WAIT_COOLDOWN_MS=21600000

# Bounded cooldown (ms) applied to an explicit capacity-exhaustion handoff
# (#1256). Scoped to the exhausted capacity on the handed-off item only — the
# work stays dispatchable to unrelated capacities. Bounded to 60 000–21 600 000;
# default 900 000 (15 min). See docs/capacity-handoff.md.
# DISPATCH_CAPACITY_COOLDOWN_MS=900000

# --- Legacy aliases (do not set) ---

# MISSION_CONTROL_URL and MISSION_CONTROL_AGENT_TOKEN were renamed to
Expand Down
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ npm run db:deploy # Deploy migrations (prod)
| `DISPATCH_GROOMER_INTERVAL_MS` | No | Groomer run interval (default 600000 = 10m; `0` disables) |
| `DISPATCH_PR_FOLLOWUP_INTERVAL_MS` | No | PR-followup sync interval (default 900000 = 15m; `0` disables) |
| `DISPATCH_PRUNE_CLOSED_INTERVAL_MS` | No | Closed-issue prune interval (default 86400000 = 24h; `0` disables) |
| `DISPATCH_CAPACITY_COOLDOWN_MS` | No | Bounded cooldown for explicit capacity-exhaustion handoffs (default 900000 = 15m, clamped 1m–6h). Scoped to the exhausted capacity on the handed item only; unrelated capacities keep dispatch. See [docs/capacity-handoff.md](./docs/capacity-handoff.md) |
| `DISPATCH_QUEUE_ADMISSION_MODE` | No | Grooming freshness admission for worker pickup: `off` (default, unchanged), `audit` (annotate queue items, filter nothing) or `enforce` (withhold ready issues without a current applied grooming decision or operator override). See [docs/hosted-groomer.md](./docs/hosted-groomer.md#worker-admission). |

Resolution order: `DATABASE_URL` > `DISPATCH_DATABASE_URL`. `DISPATCH_AGENT_TOKEN` for agent API bearer auth.
Expand Down Expand Up @@ -211,6 +212,7 @@ Cross-cutting concerns and where they live. Read these first when working in tha
| Agent claim / unclaim (lease + status) | `app/api/issues/claim/route.ts`, `app/api/issues/unclaim/route.ts`, `lib/lease.ts` |
| Assign / unassign (label-only, no lease) | `app/api/issues/actions/route.ts`, `app/api/issues/unassign/route.ts`, `lib/assignment-conflicts.ts` |
| AgentWork lifecycle | `app/api/agent-work/route.ts`, `app/api/agent-work/{start,checkpoint,finish}/route.ts`, `lib/agent-work.ts` |
| Fenced capacity handoff (#1256) | `app/api/issues/capacity-handoff/route.ts`, `lib/issue-capacity-handoff.ts`, `lib/capacity-handoff.ts`, `lib/pr-fix-queue.ts` (report resolver), `lib/lease.ts` (claim token), [docs/capacity-handoff.md](./docs/capacity-handoff.md) |
| Auth & audit actor identity | `lib/auth.ts` (`authorizeRequest`, `getAuthorizedActor`) |
| Lifecycle event stream (claim/status evidence, #1202) | `lib/issue-lifecycle.ts`, `docs/issue-lifecycle-events.md` |
| Board UI component tree | `app/board/page.tsx` → `kanban-board-client.tsx` → `components/kanban-board.tsx` → `components/issue-card.tsx` |
Expand Down Expand Up @@ -317,9 +319,14 @@ The `tasks/report` endpoint accepts these outcomes:
| `no_changes_needed` | No action was required |
| `already_addressed` | A PR-fix attempt whose feedback was already handled — settles the queue item with an optional evidence string (commit SHAs/paths) and no new push (#1121). Still gated on PR merge state; a repeated same-evidence disagreement with an unchanged head reopens via #940 and bounds to BLOCKED at `PR_FIX_MAX_ATTEMPTS`. |
| `handed-off` | The worker accepted the task but handed it to another process; records a neutral terminal run status and preserves supplied issue/PR identity without settling the PR-fix queue. Independent pr-followup sync owns ingestion. |
| `capacity-exhausted` | Explicit recognition that the worker's execution capacity cannot run the handed PR-fix attempt (#1256). Requires the issued `prFixItem` token (including its per-hand-out `handoutToken` receipt) and a `capacity` identifier; releases exactly the issued hand-out without spending repair budget and records a capacity-scoped cooldown. For claimed issue work use `POST /api/issues/capacity-handoff` instead. |

Queue-backed `followup-pr` reports must echo the `prFixItem.{id, generation}` token issued by `next-task` on the task. Reports without it still record the AgentRun, but never settle PR-fix queue state — the item stays queued and the report is a no-op against the queue (#1074).

Workers may report `outcome: "capacity-exhausted"` only when a recognized execution capacity is genuinely unavailable, with a non-empty `capacity` identifier (an opaque worker-chosen name, for example a provider/model or runner pool) and the issued `prFixItem: { id, generation, handoutToken }` token. Dispatch fences the settlement to the **exact issued hand-out**: the `handoutToken` is a per-hand-out receipt next-task mints (rotated on every issuance), and the release removes only the reporting agent's receipt/hand-out/ack — other agents' live hand-outs at that generation are preserved, and the hand-out stamps clear only when the last live hand-out is consumed. While the item is still `QUEUED` the attempt token is consumed but the item **stays `QUEUED` at the same generation with the same `fixAttempts` count, branch, and PR identity** — no repair budget is spent and there is no global wait. The suppression is **capacity-scoped**: Dispatch records a bounded cooldown (`DISPATCH_CAPACITY_COOLDOWN_MS`, default 15m, clamped to 1m–6h) for that capacity on that item only. `next-task` requests may declare `?capacity=`; a request whose capacity matches the exhausted one skips the item until the cooldown expires, an unrelated capacity is never suppressed, and a request that declares no capacity conservatively skips cooled items. The capacity is caller-declared trusted *scoping*, never identity: settlement authority comes only from the issued receipt token, the cooldown deliberately does not filter by agent (the same worker with a different, unrelated capacity is legitimately allowed), and lying about a capacity grants nothing beyond re-acquiring work for the capacity one claims to have exhausted. A `capacity_handoff` history row and a `CapacityHandoff` row record the transition. A changed generation, an already-settled status, or a human hold (BLOCKED/PARKED/NEEDS_HUMAN) is never overwritten — the report settles as a no-op. Do not use this outcome for worker-local failures, ordinary errors, or an unrecognized capacity.

For **claimed issue work** the fenced counterpart is `POST /api/issues/capacity-handoff` (bearer-auth; bound workers only for their own agent). The worker presents the `claimToken` that `POST /api/issues/claim` issued on the claim (also surfaced by `active-work` resume context and the MCP `claim_issue` result). A persistent per-issue `IssueMutationGuard` serializes cooperating claim, `upsertLease`, and release mutations under a PostgreSQL advisory lock. Dispatch fences release to that exact claim (agent + lease row + token, verified against live GitHub labels); if ownership changed during verification, cleanup removes only this worker's agent label and never changes status. Human holds (closed/done, any live `status/blocked`, a cached block carrying a `blockedReason`) and changed/missing ownership are refused. The lease remains until remote label cleanup succeeds; then it is consumed, active AgentWork rows are retired with history, an audit row is written, and the same capacity-scoped cooldown applies to issue pickup. Failed cleanup retains the lease and guard so an exact-token retry can resume. Guards are persistent and have no automatic timeout: after a process crash, an operator must confirm the process stopped and reconcile live GitHub state before removing one. Branch/PR identity on the lease and work history is retained; nothing is re-pushed and no repair budget exists for issue work. See [docs/capacity-handoff.md](./docs/capacity-handoff.md) for the lease/label effects and the GitHub-vs-database consistency boundary.

The report body also accepts an optional `startedAt` — an extended ISO 8601 timestamp with timezone (`Z` or `±HH:MM`), for example `2026-10-03T04:20:58Z` or `2026-10-03T04:20:58.123456+00:00`, marking when the worker began the task. When it parses, is not in the future, and is no more than 24 hours before the report, it is stored as the run's start time so the run records a real duration; missing, malformed, future, or too-old values silently fall back to the report time (never a 400). The raw body value participates in the idempotency payload identity, so an identical-body retry always replays as a duplicate. An accepted `startedAt` is normalized to UTC (`Z`) form when stored and echoed back in the response. The echo reflects re-validation of the current request, so on an idempotent duplicate replay the echoed `startedAt` may be absent even though the original accepted run stored it — for example when an identical-body retry lands more than 24 hours after the reported start (the stored AgentRun is unaffected).

#### Idempotent reporting (optional)
Expand Down
Loading
Loading