Repository navigation
feat(queue): expose PR-fix work generations - #1044
Conversation
Reason: Review comment on src/lib/pr-fix-queue.test.ts:154 Total attempts: 6 Attempts by lane:
Last attempt: src/lib/pr-fix-queue.test.ts:154 — Minor: Specialist lead: no direct unit test asserts that Automated finding from AI PR review. Posted automatically by Dispatch on 2026-09-21T18:58:09.919Z |
Replace the SHA-256 derived generation with a first-class PrFixQueueItem.generation column (default 1, additive migration). The hash of evidenceKeys+headSha+queuedAt never changed across requeues of the same row because queuedAt is the row's original creation time, so a consumer's dedupe would silently swallow every fresh follow-up attempt — the exact case the identity exists to distinguish. Generation now bumps only when a non-QUEUED item becomes dispatchable again as a fresh attempt (requeue from BLOCKED/FIXED, new evidence reopening a resolved item, the #940 no-progress FIXED recovery, and the refused-FIXED head-SHA rollback), atomically with the status flip via Prisma increment. Repeated reads and repeated sync of the same pending work keep the identity stable. next-task exposes followup-pr.prFixItem = { id, generation } (integer), documented in docs/pr-review-fix-queue.md. Linked-PR follow-ups without a queue item still omit prFixItem; that gap is tracked separately in #1045. Required by misospace/courier#71.
|
Reworked in 02be91e: the SHA-256 derivation is gone, replaced by a first-class Blocker 1 (generation stable across requeues) — The underlying defect was real, though via a different mechanism than stated. Blocker 2 (silent defaults collapsing items) — Moot: Major (unit test never exercised requeue) — Replaced with a lifecycle suite: stable identity across repeated reads/repeated evidence, BLOCKED→requeue (1→2)→re-requeue (2→3), FIXED reopened by new evidence, #940 recovery, refused-FIXED rollback, mark→QUEUED, and no-bump cases (same-attempt updates, trusted FIXED). Major (route test lacked same-PR/two-generations) — Added: the same PR is served Minor (per-field forwarding) — Minor (64-hex helper test) — Moot with the helper's removal. Minor (docs) — Also noted from the review's unknowns: the Migration is additive ( |
Superseded by a newer automated review for this pull request.
Workers that crash between Dispatch committing a report and recording the result locally (e.g. a controller reconcile retrying after a failed status patch) retry tasks/report with no way for Dispatch to recognize the retry: each attempt created another AgentRun and re-ran PR-fix resolution. Add an optional opaque idempotencyKey to POST tasks/report. When present, the key is claimed and the AgentRun created in a single transaction guarded by a unique (agentName, idempotencyKey) constraint on the new AgentReportDedupe table — concurrent duplicates lose the index race (P2002) and replay the stored agentRunId instead of re-running anything. The whole logical report is deduped: no second AgentRun, no repeated PR-fix resolution (stored result replayed, or an explicit skip marker), same agentRunId returned with duplicate: true. The same key with a different payload is rejected with 409 via a canonical payload hash. Reports without a key are unchanged (at-least-once). Contract documented in AGENTS.md. Key semantics: fully opaque, agent-scoped, worker-chosen. Required by misospace/courier#71.
|
Extended in 308b347 per the Courier integration review: Mechanism. A new Payload mismatch. The claim stores a canonical SHA-256 of the validated report fields (sorted-key JSON, so key order does not matter). Same key + different payload → Compatibility. Reports without a key take the exact previous path (no claim, no transaction, at-least-once). The key is agent-scoped, so different agents may reuse the same opaque value, and Dispatch performs no parsing of the key's content. Coverage (9 scenarios in Contract documented in AGENTS.md ("Idempotent reporting"). Migration |
Superseded by a newer automated review for this pull request.
Address the second review round: - Await the post-report prFixResolution persist instead of swallowing failures: the retry-replay contract is now strict — a store failure fails the first response, and the worker's retry lands in the duplicate branch (same agentRunId, explicit skip marker) without re-running anything. Moving the resolver inside the claim transaction is not viable: it makes GitHub round-trips and itself opens transactions (Prisma forbids nesting), so the residual pre-persist window is documented in AGENTS.md instead. - Validate idempotencyKey harder: trimmed on ingest (so padded and bare forms of one key dedupe) and capped at 200 characters to keep unbounded material off the unique index. - Add the AgentRun foreign key the schema standards require: AgentReportDedupe.agentRun references AgentRun with ON DELETE SET NULL, so a run deletion keeps the claim recognizable instead of stranding an orphan audit trail. - Drop the `generation ?? 1` fallback in toAgentQueuePrFixItem — the column is NOT NULL, so the fallback could only ever mask a broken fixture. - Document the DEFAULT 1 backfill intent on the generation migration: no generation state existed pre-migration (the superseded SHA-256 form never shipped), so 1 is the initial state, not lost history. - Tests: defensive no-result 409 guard, resolution-store failure path, key trim + length validation, JSON key-order stability, and a real PostgreSQL integration suite (route.integration.test.ts, wired into the database-integration CI job) proving the unique index serializes concurrent same-key reports into exactly one AgentRun and that the FK nulls rather than strands on run deletion.
|
Addressed in 4802b5b. Findings taken individually: Blocker — stored-resolution replay race. Valid window, fixed to the extent the architecture allows. The resolution persist is now awaited before responding (no Major — key validation. Fixed: keys are trimmed on ingest (a padded and a bare form of one key now dedupe — tested) and capped at 200 characters (tested). Minor — missing FK on Major — untested no-result 409 guard. Tested now, including asserting no Minor — concurrent test ordering fragility. Rewritten to capture the winner's claim data in a local variable at create time instead of scanning Minor — canonicalJson determinism untested. A behavior-level test now sends the same logical payload with reversed JSON key order and asserts the retry is recognized as a duplicate rather than a 409. (On Unicode: payloads that differ by any byte — NBSP vs space included — are different payloads; fuzzy-matching them would weaken the 409 conflict contract, so no normalization is intended.) Minor — replay response shape parity. The premise doesn't hold: the first response never contained Minor — no index on Unknowns — real-Postgres concurrency. Addressed: new Carried-forward P1/P3/P5 — DEFAULT 1 "collapse". These rest on a premise that doesn't hold: no generation state existed before this migration. The SHA-256 derivation existed only in this PR's first revision (f6df8ef) and never shipped — the branch is unmerged, so no worker could have observed or persisted a hex generation, and nothing ever "bumped" a generation before this PR introduced bumping. Backfilling every existing row to Carried-forward P2 — docs shape change. Carried-forward P4 — route test mutates a mock. That test intentionally verifies the route surfaces the persisted row faithfully (same id, new generation); the production requeue path itself is exercised end-to-end in |
Superseded by a newer automated review for this pull request.
Third review round, actionable items: - Wrap the post-validation report path in try/catch returning the standard handleApiError 500 envelope, so an unexpected persistence failure (idempotency claim, resolution store) is a structured 5xx a worker can retry on — the retry then lands in the idempotent duplicate branch. The failed-store test now asserts the HTTP 500 envelope instead of a raw rejection. - Tighten the integration suite: assert exactly one of two concurrent responses carries duplicate:true (the other omits the field), assert the post-run-deletion retry's 409 message, and inspect the surviving claim's payloadHash. - Add generation: 1 to the queue-route test fixtures so no hand-rolled PrFixQueueItem fixture predates the NOT NULL column. - Mirror the retry-resolution semantics (stored result replayed vs explicit skip marker) in docs/pr-review-fix-queue.md, and note the FK-before-index ordering is safe because the table is new and empty.
|
Round 3 addressed in bdc3847. Actionable items, fixed:
Positions held, with reasons:
All checks green locally: 2764 unit tests, integration suite passing against a real PostgreSQL with the full migration chain, lint/typecheck clean. |
Superseded by a newer automated review for this pull request.
…ranches Fourth review round, actionable items: - AGENTS.md now documents both exceptional idempotency paths: a claim without a recorded result (run deleted) is a 409, and an unexpected persistence failure is a structured 500 whose retry lands in the duplicate branch. - Cover the non-idempotency 500 paths: AgentRun write failure on the no-key branch and resolver failure on the keyed branch both return the structured envelope, with the keyed branch's claim still committed for the worker's retry. - Assert the queue response surfaces the persisted generation, so the fixture value is verified rather than decorative.
|
Round 4 addressed in 068ad8f:
On the recurring items, the positions from the previous reply stand and are backed by the review's own must-check text ("No prior On the minor test-coupling notes: exact-string assertions ( |
Superseded by a newer automated review for this pull request.
…e path Drive the real enqueuePrFixItem → markPrFixItem(BLOCKED) → requeuePrFixItem state machine against an in-memory store and assert next-task serves generation 1 on the first attempt and the same id at generation 2 after the requeue — closing the requested coverage that the route surfaces the identity the production requeue path bumps. Also document the 500/409 idempotency-reporting paths in docs/pr-review-fix-queue.md and fix a collapsed test header line.
|
Round 5 addressed in d8fdf27:
Worth noting for maintainers, discovered while writing that test: The remaining carried findings rest on premises already refuted in-thread and confirmed by this review's own must-check ("No prior |
Superseded by a newer automated review for this pull request.
Round 6 review, actionable items: - Integration suite now pins the generation migration semantics at the DDL level (column_default=1, NOT NULL — the exact mechanism the ALTER TABLE backfill uses for pre-existing rows, verified on a scratch cluster) and drives the production enqueue → BLOCKED → requeue lifecycle against the real database, asserting the atomic increment lands generation 2. - AGENTS.md notes that idempotency keys persist in the database and must not carry secrets or personal data. - The idempotencyKey validation tests assert the 400 response bodies, not just status codes.
|
Round 6 addressed in 1dc6055:
Positions unchanged on the recurring items, each already refuted in-thread: the resolver cannot run inside the claim/run transaction (GitHub round-trips + Prisma forbids the nested |
Superseded by a newer automated review for this pull request.
Round 7 review, actionable items: assert the AgentReportDedupe unique (agentName, idempotencyKey) index and the ON DELETE SET NULL FK via pg_catalog in the integration suite, and document the recovery path for a key whose referenced AgentRun was deleted (fresh key, or an operator deletes the stale claim row).
'n' (not 'a') is PostgreSQL's code for ON DELETE SET NULL, and pg_indexes.indexdef is schema-qualified with a USING clause — the DDL pin test asserted the wrong code and pattern and failed against a real database. Verified green against a scratch Postgres.
0faa332 to
121320d
Compare
|
Round 7 addressed in 121320d:
On the remaining carried items, the analysis in earlier replies stands and is not changed by this delta: the resolver cannot move inside the claim/run transaction (GitHub round-trips; Prisma rejects the nested interactive transaction that One process note for the human reviewer: the automated review requested error-message assertions in round 4 ("the response body is not asserted … only status codes are checked") and now flags those same assertions as coupling in round 7. The literal messages are the contract (matching this repo's convention, e.g. |
Superseded by a newer automated review for this pull request.
Superseded by a newer automated review for this pull request.
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: MiniMax-M3@https://litellm.jory.dev/v1 (anthropic) — routed smart (risk match: db_or_migration_changes)
Recommendation
Request changes. The PR delivers two well-scoped features — Dispatch-owned PR-fix work generation and idempotent tasks/report — and the database integration suite, schema, and CI gating all look sound. However, the PR is labeled db_or_migration_changes and path_handling_changes, and the linked issue misospace/courier#71 plus the must-check list both demand evidence that the migrations are safe on a copy of production and that any path-handling changes are covered. The required-checks list is the dominant signal here: while the diff is internally consistent and standards-compliant, several must-checks cannot be conclusively verified from the corpus, and a real production-schema copy test is not surfaced as part of the CI evidence the action captured.
Change-by-change findings
prisma/migrations/20260921000000_add_pr_fix_generation/migration.sql
- Adds
generation INTEGER NOT NULL DEFAULT 1toPrFixQueueItem. The migration is additive (no data loss for the column itself), and the migration comment correctly states that no prior bump history exists. Backfill to1is acceptable here. - must_check (data loss): Verified from diff that the migration is purely additive (single
ADD COLUMN ... NOT NULL DEFAULT 1). NoUPDATE, noDROP, no destructive index changes. Satisfied from diff. - However, the standards file
AGENTS.mddoes not document any pre-migration backfill test gate; the PR claims it was "Validated by applying the full chain against scratch Postgres clusters — including a pre-migration PrFixQueueItem row that lands at generation = 1". The integration testroute.integration.test.tspins the column default viainformation_schema.columns, but the corpus does not surface evidence that a production-shaped dataset (existing PrFixQueueItems with non-null feedback, evidenceKeys, headSha) was migrated end-to-end.
prisma/migrations/20260921100000_add_agent_report_idempotency/migration.sql
- Creates
AgentReportDedupewith(agentName, idempotencyKey)unique index, an FK toAgentRunwithON DELETE SET NULL, and a secondaryagentNameindex. The migration is purely additive. - must_check (data loss): Verified from diff — new table, no
ALTERon existing tables beyond adding the FK. Satisfied from diff. - A potential issue: the FK is added before the unique index in the migration SQL. On a non-empty
AgentReportDedupethis would still be fine (the table is empty), but orderingFK → UNIQUE INDEXwould block future evolution. Flagged for the operator to confirm migrations are run on an empty table only (corpus confirms empty at apply time).
prisma/schema.prisma
- New
AgentReportDedupemodel +generationfield onPrFixQueueItem+ back-relation onAgentRun. Schema follows standards ("Prisma schema - Keep relations strict; do not make foreign keys nullable to hide bugs"). TheagentRunIdfield is nullable, but only becauseSetNullis required to preserve the claim when the run is deleted — this is the documented recovery path, not a bug-hiding nullable.
src/app/api/agents/[agentName]/next-task/route.ts and src/lib/agent-task.ts
- Surfaces
prFixItem: { id, generation }onfollowup-prtasks. Optional and omitted for linked-PR follow-ups not backed by a queue item. Test suite covers the new fields (route.test.ts,agent-task.test.ts).
src/lib/pr-fix-queue.ts
- Centralizes generation bumps in
freshAttemptGeneration()and applies it inenqueuePrFixItem,markPrFixItem,requeuePrFixItem, and the#940refused-FIXEDrollback. Tests inpr-fix-queue.test.tscover each transition: requeue fromBLOCKED→ +1, requeue fromFIXED→ +1, new evidence reopeningFIXED→ +1, PR 940 recovery → +1, refused-FIXEDrollback → +1, repeated reads whileQUEUED→ no bump, additional evidence onQUEUED→ no bump, mark-to-QUEUEDfromBLOCKED→ +1, mark-to-QUEUEDwhen alreadyQUEUED→ no bump. - The bump is applied via Prisma's
{ increment: 1 }operator, which is atomic and safe under concurrent transitions (verified by diff).
src/app/api/agents/[agentName]/tasks/report/route.ts
- Adds optional opaque
idempotencyKey(trimmed, ≤200 chars). The route claims the key and creates theAgentRunin one transaction (prisma.$transaction(async (tx) => { create dedupe; create agentRun; update dedupe.agentRunId; })). OnP2002, the route reads back the existing claim and replays the storedagentRunId/prFixResolution(or returns askippedmarker if no resolution has been persisted yet). 409 on hash mismatch or missing run; structured 500 on unexpected persistence failure. Tests cover all branches.
.github/workflows/ci.yaml
- Renames the sync-lock DB integration job to also run the new
route.integration.test.tsagainst a real PostgreSQL. CI evidence confirmsDatabase integration: successandDatabase migrations: success, so both migrations apply and the integration suite passes on the project's standard scratch DB.
must_check coverage
- review migration for data loss risk: Both migrations are purely additive (new column with default; new table with new index/FK). No
DROP, noUPDATE, no destructive renames. Satisfied. - test migration on a copy of production schema: The corpus surfaces integration tests against a real PostgreSQL via
RUN_DB_INTEGRATION=1(Database integration: success). The PR description claims "Validated by applying the full chain against scratch Postgres clusters — including a pre-migration PrFixQueueItem row". Partially satisfied from corpus — the CI job runs the migration against the project's scratch DB, but evidence of running against a production-shaped schema (with realistic row counts, pre-existingPrFixQueueItemdata, and FK relationships) is not surfaced. See Needs Verification. - review for path traversal vulnerabilities: This PR does not touch file system paths.
path_handling_changesis a classifier flag, but no diff hunk in the corpus modifies any file-path handling, request-body parsing for paths, or URL construction with user-controlled segments. The schema/migration changes cannot introduce a path-traversal vulnerability on their own. Not applicable to this diff. - test with edge-case paths (null bytes, symlinks): No path-handling code is added. The closest edge-case validation in this PR is
idempotencyKeylength trimming (≤200 chars, non-string rejection, post-trim empty rejection). The PR validates theidempotencyKeyboundary at 201 chars (perroute.test.ts), and rejects non-string types, empty strings, and whitespace-only strings. Not applicable to the path-handling must_check — the path-traversal must-checks are for path input; the idempotency key edge cases are not path inputs. N/A.
Standards Compliance
The standards file is AGENTS.md (Dispatch self-hosted Next.js/TS Kanban). The PR is consistent with documented conventions:
- No agent-specific names in generic docs — the new
Idempotent reportingsection uses generic worker language. - Prisma relations —
agentRunIdis nullable only to support the documentedON DELETE SET NULLrecovery path, which is explicit in the schema comment and the route's 409 handler. - API routes return appropriate status codes: 200 for first/retry success, 400 for bad idempotency key, 409 for hash-mismatch and orphan-claim, 500 for unexpected persistence failure. Documented in AGENTS.md.
- Error handling uses
error instanceof Errorand the centralizedhandleApiErrorenvelope. - Validation runs before DB operations (
idempotencyKeytype/length checks precedeprisma.$transaction). - No
.env/ secrets committed;idempotencyKeyis documented as opaque and warned against embedding secrets/PII. - Migration files live under
prisma/migrations/with timestamped names and a corresponding schema update.
Tool Harness Findings
The native tool-calling loop issued no tool calls; this review was produced from the corpus directly. No tool harness output to summarize.
Unknowns / Needs Verification
- Production-schema migration rehearsal: The CI runs against a scratch DB, not a snapshot of production data. The PR description mentions "scratch Postgres clusters" but the corpus does not surface a row-count or FK-shape that matches production. Recommend running the migrations against a
pg_dump-restored production copy before merge, with at least onePrFixQueueItemrow in each terminal status (QUEUED,FIXED,BLOCKED,STALE,IGNORED) to confirm thegenerationbackfill and theagentReportDedupetable creation under realistic cardinality. metadataPatch(input)field ordering inenqueuePrFixItem: A specialist lead flagged that...freshAttemptGeneration()is spread before...metadataPatch(input), and thatmetadataPatchcould overwritegenerationif it ever sets that field. I cannot confirm from the diff alone whethermetadataPatchtouchesgeneration— the function is not in the corpus. Marked unknown. Recommend confirmingmetadataPatchdoes not setgeneration.payloadHashsensitivity to legitimate payload enrichment: The integration test asserts that key-order-insensitive retries dedupe, but a specialist lead correctly notes that any legitimate enrichment after the first attempt (e.g., addingsummary) would change the hash and 409. The PR's docstring says "Different payloads = different reports = different idempotency units", which is consistent with the implementation but may surprise workers that incrementally fill insummary. Recommend documenting explicitly that retries must carry the same payload bytes (or close enough that the canonical JSON matches) inAGENTS.md.- Concurrent race visibility window on P2002: The route's P2002 handler does
findUniqueoutside the original transaction; if the winner has not yet committed,existingisnulland the error is rethrown (surfaced as a 500). The PR does not address this race; an immediate retry typically resolves it, but the 500 vs 409 semantics differ from the documented contract. Marked unknown — recommend documenting the "rare 500 from concurrent duplicates, retry to land in duplicate branch" behavior.
Specialist review leads (advisory, verified where adopted)
- Adopted:
metadataPatchoverwrite risk,payloadHashsensitivity, concurrent P2002 visibility window, idempotency-key length validation nuance (post-trim). All flagged in Needs Verification above. - Discarded: The "data loss for already-dispatched rows" lead — false positive.
DEFAULT 1backfill is the intended initial state per the migration comment; no prior bump history exists to lose. - Discarded: The "claim commit + agentRun + update is not atomic" lead — false positive. The three-step sequence runs inside
prisma.$transaction, which is atomic at the PostgreSQL boundary. - Discarded: The "JSONB render safety" lead — speculative; no render path was modified.
- Discarded: The "exact HTTP shape for delete-then-retry 409" lead — the integration suite covers this against real Postgres; the response shape is asserted via the
errorfield containing "no recorded result".
Why request_changes (not approve)
Two blocking concerns from the corpus:
- The must-check "test migration on a copy of production schema" cannot be conclusively verified from the corpus. The CI runs against a scratch DB, not a production snapshot. While this is acceptable for low-risk additive migrations, the PR is explicitly labeled
db_or_migration_changes, and AGENTS.md does not document an exemption. The migration is low-risk (additive + backfilled default), but the corpus does not surface the rehearsal. - The
metadataPatchordering concern is unresolved from the diff alone; until confirmed safe, thegenerationbump could be silently overwritten inenqueuePrFixItem. This is a data-integrity risk on the central feature of the PR.
If the operator can confirm that (a) the migration was rehearsed against a pg_dump copy of production with non-empty PrFixQueueItem rows, and (b) metadataPatch does not write to generation, this PR is mergeable.
| @@ -285,6 +292,7 @@ export async function enqueuePrFixItem(client: PrFixQueueClient, input: EnqueueP | |||
| reason: input.reason, | |||
| feedback: uniqueAppend(existing.feedback ?? [], input.feedback, 12), | |||
There was a problem hiding this comment.
enqueuePrFixItem: ...freshAttemptGeneration() is spread before ...metadataPatch(input); if metadataPatch ever assigns generation, it silently overwrites the increment — needs confirmation that metadataPatch does not write to generation.
Automated finding from AI PR review.
| -- derivation in this PR's first revision never shipped), so no prior bump | ||
| -- history exists to preserve. Every row starts at 1 and future fresh attempts | ||
| -- bump from there. | ||
| ALTER TABLE "PrFixQueueItem" ADD COLUMN "generation" INTEGER NOT NULL DEFAULT 1; |
There was a problem hiding this comment.
pg_dump of production, despite db_or_migration_changes risk flag and must_check requiring a production-schema copy.
Automated finding from AI PR review.
| }); | ||
| } catch (error) { | ||
| if (!isUniqueViolation(error)) throw error; | ||
| const existing = await prisma.agentReportDedupe.findUnique({ |
There was a problem hiding this comment.
findUnique returns null and the error is rethrown (5xx) instead of retrying or returning 409 — this differs from the documented contract that returns 409 only when the run was deleted.
Automated finding from AI PR review.
|
|
||
| const report: TaskReportBody = { | ||
| taskType: taskType as ValidTaskType, | ||
| outcome: outcome as ValidOutcome, |
There was a problem hiding this comment.
Minor (docs): Payload hash includes the full report body; legitimate worker-side enrichment (e.g., adding summary after first attempt) will 409 the retry as a "different payload" — workers need explicit guidance that retries must carry identical payload bytes.
Automated finding from AI PR review.
| issue: first.issue | ||
| ? { repoFullName: first.repo, number: first.issue } | ||
| : undefined, | ||
| prFixItem: { |
There was a problem hiding this comment.
Minor: prFixItem is added unconditionally for queue-backed followups even when the row's generation field could in theory be undefined for very old rows on a mid-deploy schema state — no defensive ?? 1 fallback in the route response.
Automated finding from AI PR review.
Expose Dispatch-owned, persistent work identity for PR-fix follow-ups through
next-task, and maketasks/reportsafely retryable across distributed failure boundaries.Required by misospace/courier#71. Consumers treat the identities opaquely; this PR does not change queue ranking, issue selection, lease semantics, or retry limits.
Part 1 — PR-fix work generation
GET /api/agents/{agentName}/next-taskreturns, forfollowup-prtasks backed by aPrFixQueueItem:{ "type": "followup-pr", "prFixItem": { "id": "cktz…", "generation": 2 } }prFixItem.id— stable for the persistent PR-fix queue item.prFixItem.generation— integer, starts at1, changes only when Dispatch creates a fresh dispatchable attempt.Linked-PR follow-ups that are not backed by a queue item omit
prFixItem(tracked separately in #1045). Consumers must not derive queue policy from the number./queuesurfacesgenerationon each pr-review-fix item as well.Generation semantics
Generation bumps — atomically with the status flip, via Prisma
increment— exactly when a non-QUEUED item becomes dispatchable again as a fresh attempt:requeuePrFixItemfromBLOCKEDorFIXED;enqueuePrFixItem);FIXED-tombstone recovery;markPrFixItem's refused-FIXEDhead-SHA rollback, and a directmark → QUEUED.It never bumps for ordinary reads, repeated sync of known evidence, or updates that stay within the same active attempt — repeated reads of one pending unit of work return the same identity. The bump rule is centralized in
freshAttemptGeneration(); the previous SHA-256 derivation (prFixGeneration) is removed entirely (queuedAtis the row's original creation time and never changes on requeue, so a hash could not distinguish fresh attempts).Part 2 — Idempotent
tasks/reportPOST /api/agents/{agentName}/tasks/reportaccepts an optional opaqueidempotencyKey(string, trimmed, ≤200 chars):{ "taskType": "followup-pr", "outcome": "pr_updated", "repoFullName": "org/repo", "pullRequestNumber": 12, "idempotencyKey": "<worker-chosen opaque key>" }When present,
(agentName, idempotencyKey)uniquely identifies the logical report:AgentRuncreated in one transaction, guarded by a unique(agentName, idempotencyKey)constraint on the newAgentReportDedupetable — concurrent duplicates lose the index race (P2002) and replay the stored result;agentRunId(plusduplicate: true), creates no secondAgentRun, and re-runs no PR-fix resolution (stored resolution replayed when persisted; explicitaction: "skipped"marker otherwise — never a re-run);409(canonical sorted-key payload hash, key-order insensitive);409(recovery: fresh key, or delete the stale claim row);500(Failed to report task) — a retry after it lands in the duplicate branch;The resolver intentionally stays outside the claim transaction: it makes GitHub round-trips and itself opens a Prisma transaction (nesting is unsupported), so holding the claim transaction across it is not viable. Contract documented in AGENTS.md ("Idempotent reporting").
Migrations (both additive)
Validated by applying the full chain against scratch Postgres clusters — including a pre-migration
PrFixQueueItemrow that lands atgeneration = 1— and pinned in CI by thedatabase-integrationjob:20260921000000_add_pr_fix_generation—PrFixQueueItem.generation INTEGER NOT NULL DEFAULT 1. Backfilling existing rows to 1 is the intended initial state: no generation concept existed before this migration (the superseded SHA-256 form in this PR's first revision never shipped), so there is no bump history to preserve.20260921100000_add_agent_report_idempotency—AgentReportDedupetable withUNIQUE (agentName, idempotencyKey)and anON DELETE SET NULLFK toAgentRun.Tests
pr-fix-queue.test.ts): stable identity across repeated reads/repeated evidence; bumps on BLOCKED→requeue→re-requeue (1→2→3), FIXED reopened by new evidence, [P2] A pr-fix item marked FIXED on workload exit alone strands the PR forever, with no way back #940 recovery, refused-FIXED rollback, mark→QUEUED; no bump for same-attempt updates or a trusted FIXED tombstone.{ id, generation: 1 }→{ id, generation: 2 }for the same PR across a requeue; plus a route-level lifecycle test driving the realenqueue → BLOCKED → requeuestate machine against an in-memory store.agentRunId/stored resolution; no second AgentRun; no repeated side effects; concurrent same-key produces one logical report; key-order-insensitive identity; trim + length validation with bodies; same-key/different-payload → 409; different keys/agents independent; defensive no-result 409; resolution-store failure → structured 500 → retry dedupes to skip marker; AgentRun-write and resolver failures → structured 500.route.integration.test.ts, wired into thedatabase-integrationCI job): two concurrent same-key POSTs through the real route produce exactly oneAgentRun, one claim carrying the result, and oneduplicate: true; sequential retry replays the stored result; different payload → 409; run deletion nulls the FK reference and later retries 409;generationbackfill pinned at the DDL level plus a real enqueue→BLOCKED→requeue bump; unique index and SetNull FK pinned viapg_catalog.