Skip to content

fix(github): keep project availability live on busy repositories - #132

Merged
kixelated merged 4 commits into
mainfrom
fix/github-observe-liveness
Oct 21, 2026
Merged

kixelated merged 4 commits into
mainfrom
fix/github-observe-liveness

Conversation

@kixelated

@kixelated kixelated commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes two bugs from kixelated/quest#129 in cloudflare/src/github, adapted to main's jobs (kixelated/quest#135) and decision inbox (kixelated/quest#144).

1. Availability stuck on pending

Every observed webhook (check_run, check_suite, status, push, comments...) set invalidated=1 and bumped revision. Two things followed:

  • The next refresh restarted the scan from page 1. A scan pages all issues and PRs at 50 per refresh, so on a repo with more than one page of history a single CI status reset progress and availability never left pending.
  • Any CI event that landed mid-refresh failed it with "Project changed during refresh".

Since main, the same gate also blocks every new job and the inbox's start actions, so a busy repository could never start work.

The fix splits the scan into two passes, stored in the existing scan column:

  • History pages every issue and PR, closed ones included, for activity and deletion detection. It keeps its cursors through webhooks.
  • Occupancy pages open PRs (states:OPEN, 100 per page) and quest/ branches, which is all availability depends on. It restarts on every refresh after it completes. It is usually one page, so candidates are current after a single refresh however long the history is.

Only events that can change availability or a PR head invalidate it: PR opened, closed, reopened, or synchronized; a quest/ branch created or deleted; a push to the default branch. Other events still record activity, but they no longer touch github_project_state.

changes counts only PRs seen in the latest occupancy pass. Without that, a closed PR whose activity row still said OPEN would block its quest until history reached it again.

Jobs (jobSource, the start resolver) and the inbox (start actions, "Refresh needed") now gate on known availability, project.available !== null, instead of a completed history pass. ProjectState.complete means the history pass finished and only drives the activity page's "Continue reconciliation".

Migration 0011_project_scan.sql sets old-shape scan rows to NULL. They're an observation cache, so the next refresh rebuilds them.

Since closing an issue or PR no longer restarts the scan, its activity row can still say OPEN until history rereads it. jobSource therefore rereads a job's issue or pull request from GitHub (/issues/N or /pulls/N) and refuses one that closed, or a PR whose head moved, since observations read it.

2. Paused projects offered /quest-start

#observations cleared ProjectState.available, but the board reads project.available. I removed the duplicate top-level field, so the paused path now clears the value the board, jobs, and inbox all read.

Decisions

Taken unattended in one /quest-iterate round, recommended option each time:

  1. Where do jobs and the inbox read availability?
    • ✅ state.project.available, the single field (removes the duplicate that caused bug 2)
    • Keep a top-level ProjectState.available and clear both when paused
  2. What gates starting new work?
    • ✅ Known availability (project.available !== null): occupancy complete and no invalidating webhook since
    • A completed history pass (ProjectState.complete), which reintroduces the busy-repository bug for jobs
    • A new ProjectState.current flag, which duplicates project.available
  3. Does pull_request synchronize invalidate?
    • ✅ Yes: an iteration binds the PR head the maintainer saw, as main's inbox test expects. One refresh, reading one occupancy page, restores it.
    • No: rely on publication refusing a moved branch, after the job has already run on a stale head
  4. Migration number: ✅ renumbered 0007_project_scan.sql to 0011, after main's 0010_inbox.sql.
  5. How are plan and iterate jobs kept from closed or moved targets (Codex finding)?
    • ✅ Reread the target live at job creation; the job token adds issues: read and pull_requests: read, which refresh already needs
    • Gate them on a completed history pass, which a busy repository may never reach and which stays stale after a non-invalidating close
    • Add open issues to the occupancy pass and invalidate on issue closes, so availability waits on a larger pass
  6. Webhook test helper: ✅ dropped this PR's test/webhook.ts for main's identical delivery() in test/project-fixtures.ts.

Tests

  • keeps candidates and history progress through activity that cannot occupy a quest: CI, comment, review, PR edits, and non-quest push events leave candidates known, the history cursor resumes, and a status event mid-refresh doesn't fail it.
  • withholds candidates after ...: each invalidating event (now including synchronize) makes candidates pending until the next refresh, and history still resumes.
  • starts work while history is still paging, but not before a refresh reads a new pull request head (jobs): a start succeeds with history incomplete, and an iterate after a synchronize webhook is refused until a refresh.
  • refuses an issue or pull request that closed or moved since it was read (jobs): plan on a closed issue, iterate on a closed PR, and iterate on a moved head are refused while observations still say open.
  • The paused test checks project.available and the board's available count.
  • maintainerOf() in test/project-fixtures.ts uses the two-pass scan shape.

just check and just test pass locally after merging main (232 CLI tests, 257 Worker tests).

Not addressed

  • readState still reads every activity row, unpaginated, on each /board, /state, /data, and inbox view, and rows are never pruned. That needs a decision on retention for closed entities.
  • The inbox lists an issue or pull request as open while its activity row still says OPEN, until history rereads it. Starting work on it is refused with "refresh and review it again", but the inbox still offers it.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits October 16, 2026 11:41
Every observed webhook, CI status included, invalidated the scan and
restarted it from page 1, so a repository with more than one page of
history never completed and availability stayed pending. Split the scan:
a history pass pages every issue and PR and resumes through webhooks,
while availability depends only on an occupancy pass over open PRs and
quest branches, restarted only by events that can open, close, or claim
a quest.

Paused projects also cleared only ProjectState.available while the board
read project.available, so they still offered /quest-start. Drop the
duplicate field so the board reads the cleared value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adapt the history/occupancy split to main's jobs and decision inbox:

- Renumber the scan migration to 0011, after main's 0007_jobs..0010_inbox.
- Jobs and the inbox gate new work on known availability
  (project.available !== null) instead of a completed history pass, so a
  busy repository with a long history can still start work.
- They read project.available, the single availability field.
- A pull_request synchronize webhook invalidates, so an iteration still
  binds the PR head the maintainer saw.
- Use main's shared delivery() fixture in place of test/webhook.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T17:52:14.410545Z 821965c New commits
🔒 Security Review ✅ Completed 2026-10-21T17:53:04.039234Z 821965c New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c630dc045f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +555 to +556
if (!state || state.project.available === null)
throw new GitHubError("Refresh project activity before starting work");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep entity jobs gated on fresh entity observations

This gate now treats completed quest occupancy as sufficient for every workflow, although resolve() still authorizes issue and pull jobs from state.activity. For example, an issues: closed webhook only updates changedAt, leaving cached data.state === "OPEN" and availability non-null, so the jobs route can start a planning job for the closed issue; similarly, after a PR closes during a long history pass, one occupancy refresh restores availability while the stale activity row can still authorize iterate. Gate entity-targeted workflows on their authoritative pass or expose and check the current occupancy membership before creating the job.

Useful? React with 👍 / 👎.

@kixelated kixelated Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed. Fixed in f58a5bc: jobSource now rereads a job's issue or pull request from GitHub (/issues/N or /pulls/N) and refuses one that is no longer open, or a PR whose head is not the observed headRefOid. This covers both cases without gating on the history pass, which a busy repository may never finish, and without making issue webhooks invalidate availability. Regression test: refuses an issue or pull request that closed or moved since it was read.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 21, 2026 10:48
Webhooks that close an issue or PR no longer restart the scan, so its
activity row can still say open until history rereads it. jobSource now
reads the target from GitHub and refuses one that closed or whose head
moved since it was observed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Merging via /quest-merge.

Review round 1 (Codex, review): one P1 finding, fixed. With history no longer restarting on every webhook, a closed issue or PR could keep an OPEN activity row and still authorize plan or iterate. jobSource now rereads the target from GitHub (/issues/N or /pulls/N) and refuses one that closed, or a PR whose head moved, in f58a5bc with a regression test. Codex code and security reviews of 821965c found nothing further.

Main: merged again (no new migrations; 0011_project_scan.sql stays unique). just check and just test pass locally (232 CLI, 257 Worker tests), and CI is green on 821965c.

Decision: the live reread costs one REST call per issue or PR job and adds issues: read and pull_requests: read to the job token, which refresh already requires. Gating on a completed history pass would reintroduce the busy-repository stall, and putting open issues in the occupancy pass would make availability wait on a larger pass.

Follow-ups (in the PR body's "Not addressed"): unpaginated, never-pruned activity reads; the inbox still lists a closed issue or PR as open until history rereads it, though starting work on it is now refused.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit 69b2ff5 into main Oct 21, 2026
5 checks passed
@kixelated
kixelated deleted the fix/github-observe-liveness branch October 21, 2026 21:05
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.

1 participant