Skip to content

fix(tracking-mirror): idempotent setState with marker-based orphan adoption (#109) - #111

Merged
chrisleekr merged 3 commits into
mainfrom
fix/tracking-mirror-marker-idempotency-109
May 8, 2026
Merged

fix(tracking-mirror): idempotent setState with marker-based orphan adoption (#109)#111
chrisleekr merged 3 commits into
mainfrom
fix/tracking-mirror-marker-idempotency-109

Conversation

@chrisleekr

@chrisleekr chrisleekr commented May 8, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #109 — duplicate tracking comments on issue/PR threads. Root cause: octokit v5.0.5 bundles @octokit/plugin-retry, which silently retries createComment POSTs on transient 5xx. When the upstream commits the comment but times out before responding, the retry posts a second identical comment that the existing CAS reservation cannot reconcile (CAS only protects the in-process race, not server-side duplicates from a single caller).

This PR makes setState() idempotent by embedding a hidden HTML-comment marker (<!-- workflow-run:{id} -->) in every tracking-comment body, then adopting any pre-existing marker comment on first touch and reconciling duplicates after create. The DB row remains the authority for the canonical tracking_comment_id via the unchanged CAS reservation.

Diagram

flowchart TD
    subgraph BeforeAndAfter["Before vs After"]
        direction TB

        StartA["setState first touch<br/>tracking_comment_id NULL"]:::ctx
        PostA["POST createComment"]:::ctx
        RetryA["octokit plugin-retry<br/>silent duplicate POST<br/>on transient 5xx"]:::bug
        DupeA["2+ comments<br/>only one CAS-reserved<br/>extras are orphan duplicates"]:::bug
        StartA --> PostA --> RetryA --> DupeA

        StartB["setState first touch<br/>tracking_comment_id NULL"]:::ctx
        PreScanB["pre-scan: findCommentsByMarker<br/>since=row.created_at"]:::fix
        AdoptB["orphan found<br/>adopt + delete extras<br/>skip POST"]:::fix
        PostB["no orphan<br/>POST createComment<br/>marker embedded in body"]:::fix
        PostScanB["post-scan: re-list<br/>oldest wins<br/>delete loser duplicates"]:::fix
        ReserveB["CAS tryReserveTrackingCommentId<br/>row authoritative"]:::fix
        StartB --> PreScanB
        PreScanB -->|hit| AdoptB
        PreScanB -->|miss| PostB --> PostScanB --> ReserveB
        AdoptB --> ReserveB
    end

    classDef ctx fill:#1f2937,stroke:#9ca3af,color:#f9fafb
    classDef bug fill:#7f1d1d,stroke:#fca5a5,color:#fef2f2
    classDef fix fill:#14532d,stroke:#86efac,color:#f0fdf4
Loading

Changes

  • src/workflows/tracking-mirror.ts:
    • Embed <!-- workflow-run:{id} --> marker in every comment body via runMarker() + renderCommentBody().
    • New findCommentsByMarker()listComments scoped by since=row.created_at, filters bodies containing the run marker.
    • Extract first-touch path into createOrAdoptTrackingComment(): pre-scan adopt → POST create → post-scan reconcile (oldest wins, delete extras best-effort) → CAS reservation → re-render against freshest row.
    • New tryAdoptExistingMarkerComment() for the pre-scan branch (recovers from pod crash between create and CAS, or octokit retry that already committed before the previous run died).
  • test/workflows/tracking-mirror.test.ts (new, 7 tests):

Related Issues

Test plan

  • Tested locally — bun run typecheck clean, bun run lint clean (0 errors), bun test test/workflows/tracking-mirror.test.ts 7/7 pass
  • Added/updated tests — new test/workflows/tracking-mirror.test.ts
  • All existing tests pass — baseline 101 pre-existing failures in workflow+daemon suite confirmed unchanged on main

Summary by CodeRabbit

  • Bug Fixes

    • Improved tracking comment creation with enhanced duplicate prevention and reconciliation logic for more reliable workflow run tracking.
  • Tests

    • Added comprehensive test suite covering tracking comment creation, adoption, and duplicate recovery scenarios.

Copilot AI review requested due to automatic review settings May 8, 2026 04:50
@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Rate limit exceeded

@chrisleekr has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 57 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f33aeb71-393b-41e7-a173-1fba8e2fabb8

📥 Commits

Reviewing files that changed from the base of the PR and between e81b3f4 and a7e1a14.

📒 Files selected for processing (3)
  • docs/use/workflows/index.md
  • src/workflows/tracking-mirror.ts
  • test/workflows/tracking-mirror.test.ts
📝 Walkthrough

Walkthrough

The PR introduces a marker-based approach to prevent duplicate tracking comments in workflow runs. It embeds a hidden HTML marker (<!-- workflow-run:<runId> -->) in each tracking comment body, enabling reconciliation through multi-stage creation, adoption, and duplicate deletion logic. The implementation adds helper functions for marker scanning, comment creation with error tolerance, and adoption of existing markers; the existing update path and composite rendering remain functional.

Changes

Tracking Comment Marker and Adoption Logic

Layer / File(s) Summary
Marker Contract and Helpers
src/workflows/tracking-mirror.ts
runMarker(runId) helper generates the marker string; renderCommentBody prepends the marker to all tracking comment bodies; findCommentsByMarker scans recent comments via listComments since run creation and filters for those containing the marker.
Create/Adopt/Reconcile Flow
src/workflows/tracking-mirror.ts
createOrAdoptTrackingComment performs pre-scan adoption, resilient creation with error capture, post-create re-scan, deletion of duplicate losers (best-effort), DB reservation of the canonical tracking_comment_id, and final update of the winner comment. tryAdoptExistingMarkerComment handles adoption of existing marker comments without creating new ones.
setState and Rendering Integration
src/workflows/tracking-mirror.ts
setState refactored to delegate first-touch creation/reconciliation to the new helper; the existing update path for already-reserved comments and parent cascade refresh remain unchanged. Composite rendering continues to read _lastHumanMessage from persisted state, now benefiting from the marker being present in underlying comment bodies.
Test Infrastructure
test/workflows/tracking-mirror.test.ts
Mocks runs-store methods (findById, listChildrenByParent, mergeState, tryReserveTrackingCommentId) and Octokit REST issue methods (listComments, createComment, updateComment, deleteComment). Test helpers construct WorkflowRunRow fixtures and an Octokit test double factory with call-log assertions.
Test Coverage
test/workflows/tracking-mirror.test.ts
Validates first-touch creation (two scans, one create, one reservation), pre-existing marker adoption (no create, one update), reconciliation of retry duplicates (deletion of losers, adoption of oldest), create failure with existing marker (skips deletion, adopts existing), create failure with no marker (error rethrow, no reservation), comment scan scoping via since parameter, and update-only path when tracking_comment_id is already set.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

type: fix 🐞

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR addresses tracking-mirror comment deduplication (#109 root cause) but does not address the security sanitization requirements (formatChangedFiles filename sanitization) specified in the linked issue objectives. Implement the filename sanitization fix in src/core/formatter.ts and add regression tests in test/core/formatter.test.ts as documented in issue #109's suggested next steps.
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title correctly describes the main change: marker-based idempotent comment adoption to fix duplicate tracking comments caused by octokit retries.
Out of Scope Changes check ✅ Passed The code changes remain scoped to tracking-mirror comment creation/adoption logic; no out-of-scope formatter or prompt-builder changes were introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses duplicate workflow tracking comments by making setState() idempotent: each tracking comment body embeds a hidden run marker and the first-touch path scans for an existing marker comment to adopt and reconciles duplicates created by retried POSTs.

Changes:

  • Add a per-run hidden HTML marker to tracking-comment bodies and implement marker-based comment discovery.
  • Refactor first-touch setState() into a create-or-adopt flow with pre-scan + post-scan reconciliation and best-effort deletion of duplicates.
  • Add a new unit test suite covering create/adopt/reconcile scenarios for tracking-mirror.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/workflows/tracking-mirror.ts Adds marker embedding, marker scanning, and first-touch create/adopt/reconcile logic for idempotent tracking comments.
test/workflows/tracking-mirror.test.ts Adds unit tests for the new marker-based first-touch behavior and scan argument shaping.

Comment thread src/workflows/tracking-mirror.ts Outdated
Comment thread src/workflows/tracking-mirror.ts Outdated
Comment thread test/workflows/tracking-mirror.test.ts
Comment thread test/workflows/tracking-mirror.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/workflows/tracking-mirror.ts`:
- Around line 66-81: findCommentsByMarker currently fetches only one page and
relies on listComments with since (which filters by updated_at), so it can miss
markers on later pages; replace the single listComments call with
deps.octokit.paginate(deps.octokit.rest.issues.listComments, { owner:
row.target_owner, repo: row.target_repo, issue_number: row.target_number,
per_page: 100, since: row.created_at.toISOString() }) and iterate pages/items,
checking each comment.body for runMarker(row.id) and verifying
comment.created_at is >= row.created_at.toISOString(); stop pagination as soon
as you find a match (return the id/created_at) to avoid extra work, and
otherwise return an empty array—update the function findCommentsByMarker to use
this pagination and early-exit logic.
- Around line 78-80: The filtered comment list needs an explicit deterministic
sort so "oldest wins" is stable; after the filter and before the map in the
return expression in tracking-mirror.ts, sort resp.data entries by numeric
created_at (ascending) and use numeric id (ascending) as a tiebreaker so
matches[0] remains the oldest; update the pipeline that currently ends with
.filter(...).map(...) to insert a .sort(...) step that compares created_at
(parse to timestamp) then id (parse to integer) before mapping to { id,
created_at } to ensure deterministic behavior for the code paths that depend on
matches[0].
- Around line 178-184: The new direct GitHub writes in tracking-mirror.ts
(octokit.rest.issues.createComment, updateComment, deleteComment) send
humanMessage directly and bypass
src/utils/github-output-guard.ts:safePostToGitHub/reductSecrets; change these
calls to route through safePostToGitHub (or the module's approved wrapper) so
the body is passed to redactSecrets and the optional LLM scanner before calling
octokit; for createComment and updateComment pass the humanMessage/body via
safePostToGitHub and for deleteComment use the same wrapper or a no-body
safePostToGitHub invocation so all git writes in the file go through the
chokepoint.

In `@test/workflows/tracking-mirror.test.ts`:
- Around line 11-17: Add a test covering the lost-CAS path by setting
mockReservation = { won: false, trackingCommentId: <oldest_id> } and seeding two
pre-scan marker rows so createOrAdoptTrackingComment /
tryAdoptExistingMarkerComment run the branch where another process reserved a
tracking_comment_id; assert that the local canonical winner is not deleted and
that updateComment is called with reservation.trackingCommentId (the other
process's id) rather than a local id, and use the existing mocks
(mockReservation, findByIdMock, tryReserveMock, mergeStateMock) to simulate the
scenario.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: be196eec-ecb8-405d-a71f-0c365f91e9f6

📥 Commits

Reviewing files that changed from the base of the PR and between 29ad885 and e81b3f4.

📒 Files selected for processing (2)
  • src/workflows/tracking-mirror.ts
  • test/workflows/tracking-mirror.test.ts

Comment thread src/workflows/tracking-mirror.ts
Comment thread src/workflows/tracking-mirror.ts Outdated
Comment thread src/workflows/tracking-mirror.ts
Comment thread test/workflows/tracking-mirror.test.ts
@chrisleekr
chrisleekr merged commit 7e44417 into main May 8, 2026
22 checks passed
@chrisleekr
chrisleekr deleted the fix/tracking-mirror-marker-idempotency-109 branch May 8, 2026 06:03
chrisleekr pushed a commit that referenced this pull request May 8, 2026
## [1.11.1](v1.11.0...v1.11.1) (2026-05-08)

### Bug Fixes

* **tracking-mirror:** idempotent setState with marker-based orphan adoption ([#109](#109)) ([#111](#111)) ([7e44417](7e44417))
@chrisleekr

Copy link
Copy Markdown
Owner Author

🎉 This PR is included in version 1.11.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(pipeline): formatChangedFiles skips sanitizeContent on attacker-controlled filenames

2 participants