Skip to content

fix(github): Reconcile open pull requests and keep closes without timestamps - #3216

Merged
BYK merged 5 commits into
mainfrom
fix/github-reconcile-open-prs
Nov 11, 2026
Merged

BYK merged 5 commits into
mainfrom
fix/github-reconcile-open-prs

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Only GitHub pull_request webhooks changed the stored pull request state. When GitHub did not deliver a close, or the handler failed, the pull request stayed open in Junior forever.

  • JUNIOR-BC: a close without a valid closed_at or merged_at threw before the write.
  • JUNIOR-TD: a chip update kept facts for another object type, so the annotation schema threw.

Fix

  • A daily check for each repository reads the stored open pull requests from GitHub. It replays each close through the existing outcome path.
  • The pull_request.merged and pull_request.closed_unmerged events have one key for each close. The webhook and the check use the same key, so a Watch gets each close once.
  • Merge events from the check have the same people fields as webhook merge events.
  • JUNIOR-BC: a close without a valid terminal time uses updated_at.
  • JUNIOR-TD: a chip update keeps saved facts only when they fit the new object type. Chip updates use Promise.allSettled and log each failure, so one bad chip does not stop the others.
  • Core change: the plugin heartbeat context gets annotations and codeChanges.

Limits

  • 1 repository for each run. One GraphQL query reads up to 100 stored open pull requests. A repository with more uses more queries. There is no overall cap.
  • Up to 3 commit pages for each merged pull request, only when a Watch matches.
  • A close without a merge can fire twice when the webhook has no closed_at.

Tests

  • merge-conflicts.test.ts:
    • The check corrects a lost merge and a lost close, and updates their chips. It skips open and deleted pull requests.
    • A late webhook for the same merge has the same event key and the same people fields.
    • When an event publish fails, the next check records the close.
  • webhook-outcomes.test.ts:
    • A close without a terminal time is recorded.
    • A stored chip with facts for another object type changes to merged. The other chip and the commit classification also complete.

Each test fails without its fix.

Results: the GitHub plugin tests passed 351/351 with the people change. merge-conflicts.test.ts passed 14/14. Typecheck, lint and file-length passed.

Reported by Cramer.

via @BYK.

--

View Session [Sentry]

…estamps

Only pull_request webhooks change stored pull request state. A lost close,
or a close that the handler rejects, left the row and the conversation
chip open forever.

The GitHub heartbeat now reconciles once a day per repository. It reads
GitHub's open search, reads each stored-open pull request that the search
does not list, and replays a close through the webhook write path. The
heartbeat context now has annotations and codeChanges, as a route has.

A close without a valid closed_at or merged_at now uses updated_at instead
of throwing (JUNIOR-BC).

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
@vercel

vercel Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
junior-docs Ready Ready Preview Nov 11, 2026 10:43pm UTC

Request Review

@cursor cursor 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.

Stale Bugbot comment from a previous run.

A repository with more open Junior pull requests than the search reads
listed open pull requests as missing. The reads found the same open pull
requests every day, so later closes were never read.
The reconcile now publishes pull_request.merged or closed_unmerged when it
corrects a close. The webhook and the reconcile key these events by the
pull request close, not the delivery, so a Watch gets each close once.

A chip update keeps saved facts only when they fit the new object type
(JUNIOR-TD). Chip updates run with allSettled and log each failure, so one
bad chip does not stop the other chips or commit classification.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we use graphql here again, like we did for merge-conflict detection so we get the precise information for the PRs we are interested in in one go? I'm also not very happy about the arbitrary 500 PR limit without clear documentation and constants.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e4aa4d1. Configure here.

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

218 screenshot changes — 0 changed · 218 added · 0 removed

Review screenshots in Frameshift

The reconcile no longer compares the stored rows with the open search and
its 500 pull request limit. It reads the lifecycle of the stored open pull
requests directly, 100 for each query, so no row waits for others.

The reconcile publishes the close events before it writes the close. When
a publish fails, the row stays open and the next reconcile tries again.
The event keys are the same, so a Watch gets the close once.
The reconcile now adds the people of a merge with the webhook enrichment:
the opener, the assignees from the GraphQL read, and the commit authors and
co-authors. It reads the commits only when a Watch or event automation
matches, the same as the webhook.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
BYK added a commit that referenced this pull request Nov 11, 2026
…3221)

## Problem

PR bodies credit people by name, so GitHub does not notify them. For
example, #3216 says "Reported by Cramer", and getsentry/toolkit#1955
says "Sergiy found a real problem". Some PRs get it right
(getsentry/toolkit#2070: "Reported by @sergical"), but only when a login
happens to be in context.

There are three causes:

- The `github-issues` skill gives `Reported by Alice.` as the example,
with a plain name. `github-code` has no rule for credit.
- The model has no way to map a person to a GitHub login. `userLookup`
maps a GitHub login to Slack only. The runtime resolves a linked login
only for the requester's `via` line.
- A parent agent writes the name into the brief, and the child copies
it. The #3216 brief said "Reported by Cramer; via @BYK.", so the PR
shows "via @BYK." twice, because the runtime also adds it.

## Fix

- `userLookup` Slack results now include `linked_identities`: the
verified handles of other accounts that the person linked, such as `{
provider: "github", handle: "dcramer" }`. Only account-linking flows
write these, so the handles are verified.
- `github-code` and `github-issues` now say to credit other people by
`@login`. They get the login from `linked_identities`, then from a
GitHub link in Slack `profile_fields`. Otherwise they use the name, and
they never guess a login. A delegated brief carries the `@login`.
`github-code` also says not to write the requester `via` line.
- `github-issues` now strips "Slack user mentions" instead of all
"@mentions", so the session-framing rule does not remove GitHub logins.

## Remaining risk

People who have not linked GitHub and have no GitHub link in their Slack
profile still get their name, not a mention. No eval covers the credit
wording yet.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3Afb1f20a0ae8da9a914242b49 -->

--

[View Session in
Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3Afb1f20a0ae8da9a914242b49/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <126780+BYK@users.noreply.github.com>
@BYK
BYK merged commit a9123df into main Nov 11, 2026
46 checks passed
@BYK
BYK deleted the fix/github-reconcile-open-prs branch November 11, 2026 22:59
betegon added a commit that referenced this pull request Nov 12, 2026
Older open pull requests have no title in the code index, so the
dashboard home lists them without one, for example
getsentry/toolkit#1228.

The `0008_native_code_backfill` migration copied no titles, because
`junior_github_pull_requests` has no title column. A row gets a title
only from a later lifecycle webhook, and an idle pull request gets none.

The daily reconcile (#3216) already reads each stale open pull request
from GitHub with GraphQL, title included. Until now it wrote an open
pull request only when its draft state changed (#3269), and that write
already carries the title. The reconcile now also writes every other
open pull request to the code index with its current title and draft
state. The state stays open, and the empty conversation list keeps the
linked conversations. The write is guarded by `updatedAt` like the
others. It also picks up title edits, which send no lifecycle webhook.

Merged and closed rows from the backfill stay untitled. The reconcile
reads only open rows, and the home only names this week's closes.

**Checks:** `tests/merge-conflicts.test.ts` passes (17/17), including
the draft case from #3269. The reconcile case now checks the open pull
request's title. Typecheck and lint pass for `@sentry/junior-github`.

<!-- junior-request-attribution:start -->
via @betegon.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3A91685c2046963e29b62e4a20 -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/local%3Aweb%3A91685c2046963e29b62e4a20)
[[Sentry]](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A91685c2046963e29b62e4a20/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: bete <28983162+betegon@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview – junior-docs — 5f51c694 Deployed Nov 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: high PR risk score: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant