Skip to content

docs(recovery): ownership reconciliation design (#1245) - #1246

Merged
joryirving merged 8 commits into
mainfrom
courier/misospace/dispatch/issue-1245
Oct 14, 2026
Merged

joryirving merged 8 commits into
mainfrom
courier/misospace/dispatch/issue-1245

Conversation

@itsmiso-ai

@itsmiso-ai itsmiso-ai commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Design-only deliverable for #1245: reconcile abandoned claims without stealing work.

Adds docs/ownership-reconciliation.md and links it from the README. No implementation, no claim release, no label cleanup, no requeue, no credential migration, no deployment change — matching the issue's non-goals.

Review revision (head 086d6c9)

Main merged mid-review landed the fenced capacity-exhaustion handoffs (#1259), which shifts code the doc cites and — helpfully — already instantiates part of the §8.7 anchor shape. This revision is markdown-only:

Review revision (head 6d30488)

The human-review P1 on 5ef2d5e — a Postgres CAS cannot fence the external GitHub label deletion, so §8.6's atomic-release claim allowed a stale reconciler's removeIssueLabel to strip a same-agent reclaim's fresh label — is settled in this revision, design-only:

  • §8.6 now states the CAS's hard limit instead of claiming cross-system atomicity: GitHub label mutation has no conditional write, so a check-then-act "fence immediately before the write" is racy by definition.
  • §8.7 specifies the two-sided boundary: one per-(repo, issue) Postgres serialization anchor; Phase A fenced intent commit (retires attempt token g, opens the barrier, holds any claim on the issue, intent-only variant for legacy rowless claims); Phase B targeted forge delete issued only while a matching intent exists; Phase C settle under the same anchor, lifting the barrier and applying the §8.5 status selection.
  • Claim side is transactional, not a read: claim intent + fresh token g+1 commit under the anchor before the remote addIssueLabel, superseding the current route order (addIssueLabel at claim/route.ts:222 before upsertLease at :268, post-feat(recovery): add fenced capacity-exhaustion handoffs #1259 pins). Both lock orders resolve (claim-first ⇒ Phase A CAS fails; Phase-A-first ⇒ claim 409-held).
  • Crash/replay matrix covering all release crash points (GitHub-success/DB-failure replays act only on the pending intent with an idempotent 404-accepting delete; a settled release's stale retry issues zero forge calls) plus the claim-side row (intent committed, remote ADD missing).
  • Fail-closed path: forge outage keeps the intent pending with bounded retries, then parks HELD/needs-human; only an operator clears a stuck barrier.
  • Five pinned negative controls: renewed same-agent claim during stale release; changed-owner reassign (flagged new behavior — today reassignAgentWork writes no labels); GitHub delete succeeds / DB settle fails; old-deletion retry vs fresh live token; in-flight claim vs Phase A in both lock orders.
  • All file/line citations refreshed for drift introduced by merges of main, and independently audited line-by-line by the reviewer at head 85f479b.

Deliverable contents

Invariants this change must keep (and every code path enforcing the same rule)

This is a design doc; the invariants it establishes, and how each existing agent/*-label writer is accounted for:

  1. No automated path deletes a live claim's agent/* label. Enforced by: §8.7 Phase-A CAS + anchor-held claim path (new reconciler); releaseIssueClaim's existing newer-owner skip (src/lib/issue-claim.ts:144-153) — kept and cited; stale sweep (reuses the same core) — folded into the protocol by the implementation tickets.
  2. Every protocol label write is intent-backed and ordered under one per-issue anchor. Enforced by: §8.7 (reconciler delete; claim add). Existing claim route's add-before-lease order (claim/route.ts:222 before :268, post-feat(recovery): add fenced capacity-exhaustion handoffs #1259) is explicitly marked as superseded-by-design; no runtime change in this PR. feat(recovery): add fenced capacity-exhaustion handoffs #1259's IssueMutationGuard is cited as the landed per-issue serialization row the protocol reuses.
  3. Three direct writers stay intentionally exempt, audited, and anchor-aware (settle-or-supersede), not silently racing: owner-self-unclaim (POST /api/issues/unclaim), maintainer force-claim label cleanup (claim/route.ts:193-210), direct assignment (POST /api/issues/actions assign_agent).
  4. A release never forces status/ready. Enforced by reusing releaseIssueClaim's status-preserving policy (src/lib/issue-claim.ts:207-241) — §8.5.
  5. No blanket agent/courier* aliasing; identity migration is operator-declared and audited. §12; unchanged by this revision.
  6. Label writes are targeted adds/removes, never full-label-set writes (issue-claim.ts:244-249) — §8.6/§8.7 preserve this.

Settled decisions

Local validation receipts (coordinator, head 086d6c9, worktree merged with main incl. #1259)

Command cwd Result
npm ci /workspace clean install, Prisma client generated
npm run typecheck /workspace exit 0, no output
npm run lint /workspace 0 errors, 1 pre-existing unrelated warning (src/app/login/page.tsx)
npm test /workspace 4480 passed / 16 skipped / 1 todo; 0 failures

Revisions since the merge are markdown-only; no code paths, dependencies, or config touched, so the receipts remain valid.

Independent review outcome

Three adversarial review passes at pinned heads. Round 1 (8264af5): blockers — claim side still check-then-act; invariant contradicted by exempt direct writers; four minors — all fixed. Round 2 (85f479b): approve; two minors fixed in 6d30488. Round 3 (7c1f723, post-#1259 merge): all ~45 citations verified exact against the merged tree, §14 rows confirmed against real section content, requirement 5 of #1245 confirmed traceable; no blockers/majors; its two minors (cross-ref §7→§12, "accepted"→"designated") fixed in 086d6c9. Design-only scope confirmed; no implementation introduced.

Remaining external verification

CI checks, AI review, and human review re-verification are pending at head 086d6c9 — the operator verifies after this run exits. No known unresolved validation failures.

Closes #1245.

its-saffron[bot]

This comment was marked as outdated.

@joryirving joryirving left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 5ef2d5e against design issue #1245, existing Lease/AgentWork/releaseIssueClaim primitives, the claim/renew and stale-work paths, and the entire design. The document covers worker-neutral optional renewal, holds, linked PRs, conflict/unknown states, bounded confirmation, operator controls, and no wildcard agent/courier* migration. The additive README link and design-only scope are appropriate. CI, Security Audit, PR Smoke, Build and AI review passed, and the branch is mergeable.

REQUEST_CHANGES [P1]: the claimed atomic ownership fence does not cover the external GitHub label deletion. §8.6 says every release is CAS-conditioned on the current owner, attempt and lease/heartbeat versions, and that release + cache update + audit commit together, but then proposes targeted removeIssueLabel over the forge adapter. A Prisma CAS/transaction cannot atomically fence GitHub's remote label removal.

Concrete race: generation g is conclusively stale; the reconciler verifies agent/alpha, marks the old claim released and commits CAS/audit; before its removeIssueLabel(issue, "agent/alpha") request executes (or while the request is in flight), the same agent reclaims the issue with a new attempt token g+1, and the GitHub label is now valid for the new live claim. The old reconciler's targeted label delete then removes agent/alpha from GitHub, silently stealing the new owner's eligibility/visibility, even though its local CAS was valid when executed. If the external deletion happens first, the opposite failure window remains: a crash/timeout after a successful GitHub delete but before local commit requires replay without accidentally deleting a newly re-established claim.

The current §8.2 step 5 ('fencing check immediately before the write'), §8.4 ('release atomically with fence') and §10 concurrent-renewal example don't define a cross-system serialization protocol for this exact race. Please specify the two-sided release/renew/claim coordination boundary, including what can be fenced at GitHub vs only in Postgres, an explicit pending-release claim/admission barrier or other sound serialization strategy, the point at which a new attempt may issue/re-add the same label, and a crash-safe recovery algorithm for GitHub-success/DB-failure that cannot delete a later attempt. If GitHub cannot support the necessary conditional mutation, the safe design is to defer ambiguous automation/require an operator, not claim atomicity from the local DB alone.

Pin negative controls for (1) renewed same-agent claim during stale release, (2) changed-owner reassign, (3) GitHub remove succeeds but DB settlement fails, and (4) retry after an old deletion with a fresh live token. This is a design-only correction, not authorization to implement new infrastructure in this PR. Existing #1251/#1254 implementation tickets can consume the corrected boundary.

Other #1245 acceptance elements look satisfied. Not merged.

Comment thread docs/ownership-reconciliation.md Outdated
Courier added 4 commits October 14, 2026 15:28
…encing (#1245)

Address review P1 on PR #1246: a Postgres CAS cannot fence a remote
GitHub label deletion. Replace the atomicity claim in §8.6 with an
explicit statement of what the CAS cannot do, add §8.7 defining the
intent-based release protocol (fenced Phase A intent commit +
claim-admission barrier, serialized forge delete, settle), a
crash/replay matrix, fail-closed operator path, reassign handling, and
four pinned negative controls. Refresh file/line citations drifted by
the main merge.
Independent review found the claim path still held only by a
read-then-act check against the pending intent, and the invariant
overstated against owner-unclaim/force-claim direct writes. Define a
single per-issue serialization anchor: the claim path commits its own
claim intent + attempt token under the anchor before the remote add
(superseding the current add-before-lease route order), the barrier
holds any claim on the issue until settle, legacy rowless claims get
an intent-only Phase A, exempt owner/operator paths are named,
reassign label-routing is flagged as new behavior, and a fifth
negative control pins the in-flight-claim interleaving in both lock
orders. Refresh stale-work transaction citations.
#1245)

Follow-up review minors: scope the invariant sentence to protocol paths
and name direct assignment (POST /api/issues/actions assign_agent) as a
third audited exemption with settle-or-supersede under the anchor; move
claim-intent records into the §8.7 state paragraph with an explicit
replay owner; add the claim-side crash row (intent committed, remote ADD
missing) to the replay matrix.
its-saffron[bot]

This comment was marked as outdated.

Courier added 3 commits October 14, 2026 17:10
…merge (#1245)

Adds a requirements-traceability section (#14) mapping every #1245 requirement to its design section and negative controls, with the design-only enforcement boundary delegated to #1251/#1254. Re-pins all file:line citations drifted by the capacity-handoff (#1259) merge and notes that withIssueMutationGuard/Lease.claimToken already land the anchor shape the two-sided protocol builds on.
@itsmiso-ai

Copy link
Copy Markdown
Contributor Author

Addressing the automated review at 6d30488 (requirement-trace gap) — settled at head 086d6c9, markdown-only:

Independent review round 3 at 7c1f723: no blockers/majors; its two minors (cross-ref §7→§12, "accepted"→"designated") fixed in 086d6c9. Local validation on the merged tree: typecheck clean, lint 0 errors (1 pre-existing warning), tests 4480 passed / 0 failures.

@joryirving joryirving left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 086d6c9 against design issue #1245, the prior P1, and merged Dispatch #1259. APPROVED (design-only). The earlier Postgres-CAS-versus-GitHub-delete defect is now explicitly acknowledged in §8.6, and §8.7 specifies a two-sided intent/claim barrier with a per-issue serialization anchor, token fencing, idempotent crash replay, and the same-agent/new-owner interleaving tests. It reuses #1259's IssueMutationGuard/Lease.claimToken instead of introducing another lease system. §14 traces the design requirements to their sections and defers runtime enforcement to #1251/#1254, appropriately for this docs-only PR. No new actionable blocker in the requested scope. Implementation must enforce the specified anchor on all participating label-write paths, including privileged/operator paths and any in-flight remote writes; the document is not itself proof of cross-system atomicity. CI, Security Audit, Build, and PR Smoke passed; AI review was still running when submitted.

@joryirving
joryirving enabled auto-merge (squash) October 14, 2026 17:46
@its-saffron
its-saffron Bot dismissed their stale review October 14, 2026 17:53

Superseded by a newer automated review for this pull request.

its-saffron[bot]
its-saffron Bot previously requested changes Oct 14, 2026

@its-saffron its-saffron Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Automated Review

Full PR review.

Analysis engine: qwen3.8-flash-next-chat (openai) — fallback (primary failed) · pr-reviewer-action v3.4.1

Requirement traceability gap: the review itself completed, but one or more in-scope requirements could not be verified against enforcement or test evidence. Approval is withheld until that requirement is covered.

Findings (1 major)

Severity Location Finding
⚠️ Major requirement not enforced: Specify bounded reconciliation, expiration/grace periods, restart recovery and explicit operator controls. Evidence that a worker is unreachable or telemetry is missing must not immediately mean abandonment.

Recommendation

Approve. This is a design-only PR adding docs/ownership-reconciliation.md and a README link, addressing issue PR 1245. The document thoroughly inventories existing mechanisms, defines an ownership state model, evidence hierarchy, reconciliation protocol with fencing, and explicitly addresses the P1 concern raised in previous reviews regarding cross-system atomicity between Postgres CAS and GitHub label mutations.

The classifier flagged path_handling_changes, likely due to file path references in the documentation or generic keyword matching. However, the diff contains no executable code, no file system operations, no user input handling, and no path construction logic. It is purely Markdown documentation. Therefore, path traversal risks are not applicable to this specific change set.

Required Checks Disposition

  1. Review for path traversal vulnerabilities: not_applicable. The PR only modifies/adds Markdown files (README.md, docs/ownership-reconciliation.md). There is no code that processes paths, reads/writes files based on input, or constructs URLs from user-supplied data. The content discusses paths (e.g., src/lib/...) but does not execute them.
  2. Test with edge-case paths (null bytes, symlinks): not_applicable. As above, there is no runtime code introduced or modified that would interpret paths. Documentation cannot be vulnerable to null byte injection or symlink attacks in the context of execution.
  3. Treat as high priority — verify correctness carefully: satisfied. The design document has been reviewed against the linked issue PR 1245. It correctly identifies the limitations of the current lease/label system, proposes a robust two-sided boundary (§8.7) using Postgres intents to serialize access before remote GitHub writes, and explicitly handles the race conditions described in the human review (P1). The traceability matrix (§14) maps all requirements to sections in the doc. CI checks (Lint, Typecheck, Build, Tests) have passed.

Human Review Dispositions

  • Review ID 5510890265 (joryirving): addressed. The reviewer's P1 concern was that "a Postgres CAS cannot fence the external GitHub label deletion." The updated design (§8.6 and §8.7) explicitly acknowledges this limitation ("What the CAS cannot do") and introduces a new protocol: Phase A commits a "pending intent" in Postgres under a serialization anchor (withIssueMutationGuard), which blocks concurrent claims via a barrier. Phase B performs the remote delete only while the intent exists. Phase C settles the intent. This effectively serializes the remote write against local claim attempts, resolving the race condition where a stale reconciler could strip a fresh claim's label. The design now builds on landed machinery (PR 1259 IssueMutationGuard) rather than assuming atomicity across systems.

Linked Issue Fit

The PR fully addresses the design deliverables requested in PR 1245:

  • Inventory: Section 3 details current Lease, AgentWork, and label semantics.
  • State Model: Section 4 defines derived states like CLAIMED_ACTIVE, ABANDONED_CANDIDATE, CONFLICTED.
  • Evidence Hierarchy: Section 5 establishes tiers, ensuring missing telemetry alone never triggers abandonment.
  • Reconciliation & Fencing: Sections 8.1–8.7 specify bounded passes, grace periods, and the critical cross-system fencing protocol.
  • Operator Controls: Section 9 outlines escape hatches.
  • Non-goals: The PR strictly adheres to the "design only" constraint; no implementation code is changed.

Standards Compliance

The repository standards (AGENTS.md) emphasize strict Prisma relations and clear API contracts. While this PR adds no code, the design document aligns with these principles by proposing reuse of existing primitives (Lease, AgentWork, PrFixQueueItem patterns) rather than inventing parallel systems, and by emphasizing audit logging and fail-closed behavior.

Requirement trace

1 of 3 requirement(s) not fully traced to enforcement and a test:

  • req-f89d5bf2d727 — unmet (no valid enforcement location): The PR documents bounded reconciliation, grace windows, restart recovery, operator controls, and conservative handling of missing telemetry, but it is explicitly design-only. No production predicate implements these behaviors, and no actual regression test or fixture exercises them; the pinned negat

Outstanding Human Change Requests

  • @​joryirving's change request (5ef2d5e, head moved since) is not shown addressed at this head; it needs the reviewer's own re-review.

Verdict set from open findings (verdict_policy=strict): 1 blocker/major finding(s) out of 1 open; model verdict was 'approve'.

@joryirving
joryirving disabled auto-merge October 14, 2026 17:57
@joryirving
joryirving merged commit 3894ab1 into main Oct 14, 2026
13 checks passed
@joryirving
joryirving deleted the courier/misospace/dispatch/issue-1245 branch October 14, 2026 17:57
@its-miso its-miso Bot mentioned this pull request Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

design(recovery): reconcile abandoned claims without stealing work

2 participants