Skip to content

bug: reviewer agent posts marker with id=reviewer:loopID (missing :headSHA suffix), daemon expects id=reviewer:loopID:headSHA — marker verification always fails #599

Description

@baderdean

Summary

The reviewer agent posts a review marker with id=reviewer:<loopID> (loop UUID only), but the daemon's verifyAgentNativeReviewMarker expects id=reviewer:<loopID>:<headSHA> (loop UUID + : + head SHA). The marker verification always fails, the reviewer loop never reaches completed, and it stays backing_off with "no matching GitHub review marker was found" — even though the review IS posted with real findings.

Evidence

Review posted on PR (ankaboot-source/m3llm#180)

The reviewer agent posted a review with this marker in the body:

<!-- looper:review id=reviewer:739a4c36-9ad9-4600-a475-7d72feb0f6bd head=d36b61c2b2a65bc2c77997ea5e4cd39c0d590131 outcome=actionable -->

Note: id=reviewer:739a4c36-9ad9-4600-a475-7d72feb0f6bdloop UUID only, no :headSHA suffix.

Daemon expects

agentNativeReviewID (reviewer/runner.go:6731-6733) constructs the expected id as reviewer:<loopID>:<headSHA>:

func agentNativeReviewID(loopID string, headSHA string) string {
    return fmt.Sprintf("reviewer:%s:%s", loopID, headSHA)
}

agentNativeReviewMarker (reviewer/runner.go:6735-6740) builds the expected marker with that idempotencyKey:

func agentNativeReviewMarker(loopID string, headSHA string, idempotencyKey string) string {
    if idempotencyKey == "" {
        idempotencyKey = fmt.Sprintf("reviewer:%s:%s", loopID, headSHA)
    }
    return fmt.Sprintf("looper:review id=%s head=%s", idempotencyKey, headSHA)
}

Prompt instructs the agent correctly

The prompt (reviewer/runner.go:6526) tells the agent:

Review idempotency marker prefix: <!-- looper:review id=reviewer:<loopID>:<headSHA> head=<headSHA> outcome=clean|non_blocking|blocking -->

So the prompt DOES include the :headSHA suffix in the id field. But the agent (opencode in our case) posts id=reviewer:<loopID> — dropping the :headSHA suffix.

Verification fails

verifyAgentNativeReviewMarker (reviewer/runner.go:3317-3322) calls agentNativeReviewMarker to build the expected marker, then FindReviewMarker (gateway.go:2322) → findAllowedReviewMarker (gateway.go:2363). The matches function (gateway.go:2514-2528) compares id=reviewer:loopID:headSHA (expected) vs m.ID=reviewer:loopID (actual) → no match.

The fallback agentNativeLoopReviewMarker (line 6742-6744) uses id_prefix=reviewer:loopID: but strings.HasPrefix("reviewer:loopID", "reviewer:loopID:")false (actual ID lacks the trailing :).

Loop status

status = backing_off
last_error = Reviewer agent completed but no matching GitHub review marker was found

The review IS posted with real findings (1 blocking + 3 non-blocking), the fixer IS auto-triggered by the review comments, but the reviewer loop never converges to completed.

Root cause

The agent does not faithfully reproduce the marker format from the prompt. The prompt shows id=reviewer:<loopID>:<headSHA> but the agent posts id=reviewer:<loopID>. This may be because:

  1. The agent interprets the id field as a stable identifier (loop UUID) and treats the :headSHA as redundant since head=<headSHA> is already a separate field.
  2. The marker format in the prompt is a "prefix" template, and the agent may not realize the :headSHA is a required part of the id value (not just the prefix example).

This is distinct from #225 (closed by #231): #225 addressed marker verification missing valid posted reviews that DID have the correct id=reviewer:loopID:headSHA format. Here the agent posts a DIFFERENT format (id=reviewer:loopID without the suffix) that the verification cannot match even with the #231 tolerance fixes.

Expected behavior

The daemon should tolerate the id=reviewer:<loopID> format (without :headSHA suffix) since:

  • The head=<headSHA> field is already present and verified separately.
  • The loop UUID alone is sufficient for idempotency within a single loop.
  • Agents cannot be guaranteed to faithfully reproduce the exact id format from the prompt template.

Proposed fix

Two non-exclusive options:

Option A — Tolerant matching in findAllowedReviewMarker: When the exact id=reviewer:loopID:headSHA match fails, fall back to matching id=reviewer:loopID (loop UUID only) as long as head=<headSHA> matches. This is a ~5-line change in matches (gateway.go:2514-2528).

Option B — Simplify the prompt template: Change the prompt to show id=reviewer:<loopID> (loop UUID only) and drop the :headSHA from the id field, since head=<headSHA> is already a separate field. Update agentNativeReviewID and agentNativeReviewMarker accordingly. This is a larger change but makes the marker format simpler and less error-prone for agents.

Environment

Reproduction

  1. Configure looper with opencode as agent vendor
  2. Label an issue looper:plan, let planner open a spec PR
  3. Promote to looper:worker-ready, let worker open an impl PR
  4. Run looper review <repo>#<pr> to trigger reviewer
  5. Reviewer posts review with id=reviewer:<loopID> (no :headSHA)
  6. Loop stays backing_off — "no matching GitHub review marker was found"

Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions