Skip to content

feat(kix): keep the board and inbox accurate on busy projects - #182

Merged
kixelated merged 4 commits into
mainfrom
quest/m0/kix/inbox-accuracy
Oct 22, 2026
Merged

kixelated merged 4 commits into
mainfrom
quest/m0/kix/inbox-accuracy

Conversation

@kixelated

@kixelated kixelated commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Implements and finishes quest/m0/kix/inbox-accuracy.md ("[M] Keep the inbox accurate on busy projects"). The user settled the retention period at 30 days, so the quest is deleted along with its link in quest/m0/kix/README.md.

Behavior

  • Draft claims. The activity query now reads each PR's body and closedAt. A PR's body is kept only as handedBack, meaning it has a ## Decisions needed section. The regex moved into quest/core (handedBack()), so the CLI's claim checks and the Worker read it the same way.
    • Board: a draft that its starter still holds (no section) shows as Claimed, and its panel links the draft PR. A ready PR, or a draft handed back with decisions, shows as Ready to turn in. Before this, every open quest PR showed as Ready to turn in.
    • Inbox: a held draft is listed In progress as "Claimed draft". It has no actions and is never unread. Once it is handed back or marked ready, it becomes a decision.
  • Unknown merge state. When a read sees mergeStateStatus: UNKNOWN on a recorded PR:
    • At the same head, the PR keeps its last known merge state, so nothing looks new and a clean PR keeps its Merge button.
    • At a new head, the read stores UNKNOWN but leaves changedAt unchanged. The item becomes unread once a definite state arrives.
    • A newly seen PR is unread either way.
    • All of this lives in the reconcile upsert's SQL, so it compares against the stored row with no extra read.
  • Paged reads. readState, which backs /board, /data, the inbox, and jobs, now reads only open entities and unread webhook pointers, through a new partial index. That read grows with open work, not with history. The activity log on /state pages 50 rows at a time with keyset paging and an "Older activity" link. Migration 0015_activity_reads.sql also adds a partial index for staleEntities. I checked the query plans with SQLite: the open-rows read uses the partial index whether or not table statistics exist. I dropped a separate log-order index because, without statistics, the planner preferred it over the partial index.
  • Pruning. A read that finds an entity closed longer than RETENTION deletes its row instead of upserting it. The exception is a row a webhook named again since the read, which keeps it until it is read by number. History rereads every entity, so each pass prunes every expired row. RETENTION is 30 days; see Decisions.
  • Seed. cloudflare/dev/seed.ts adds a held draft (feat(skills): rename quest-complete to quest-finish #17, on quest/m1/themes), a handed-back draft (feat(skills): add quest-audit #18, on a new quest/m1/search), and a PR (fix(skills): planning PRs leave issues open and summarize the decision #19) that is clean when first read and UNKNOWN when the webhook read rereads it. cloudflare/test/seed.test.ts checks each one on the inbox and the board.
  • Seed test slimmed. The seed test used to open every inbox item. Each of those pages rechecks GitHub authority, and that loop was about two thirds of the test's time. It now opens only the items it asserts on (feat(skills): rename quest-complete to quest-finish #17, fix(skills): planning PRs leave issues open and summarize the decision #19, feat(skills): quest-plan hands its PR to quest-merge #12); inbox.test.ts covers the other threads. Idle runs dropped from about 850 ms (with the larger seed) to about 360–570 ms.
  • Docs: cloudflare/GITHUB.md and design/theme.md describe the new states and retention. quest/m0/kix/execution.md notes that the held-draft half of "owned PRs" in the inbox is done and the quest:* label half remains.

Retention measurement

Activity rows mirror GitHub entities one to one: one row per issue or PR that the history pass reads. So I measured the counts on GitHub with the search API on 2026-10-21. No pilot deployment exists yet to measure D1 directly.

Repository Total Open Closed ≤7d ≤30d ≤90d ≤180d
moq-dev/moq 6,265 98 766 2,380 3,687 4,795
kixelated/quest 192 12 81 176 176 176

On moq-dev/moq, the steady-state rows per period are about 0.9k at 7 days, 2.5k at 30, and 3.8k at 90. Without pruning there are 6.3k rows today, growing by about 2.4k a month.

Decisions

The user accepted each recommendation:

  1. Retention period for closed activity: 30 days. About 2.5k rows on moq-dev/moq, covering a month of closed history in the activity log, the only view of closed rows. RETENTION is final, with that reason beside it. The alternatives were 7 days (about 0.9k rows, but last month's closed work leaves the log) and 90 days (about 3.8k rows). To check on a live pilot, run SELECT count(*), sum(json_extract(data,'$.state')='OPEN') FROM github_project_activity WHERE repository=<id> against D1 after a full history pass.
  2. Owned drafts in the inbox: a held draft shows under In progress with no actions, rather than being hidden. It matches "work in progress, which only informs".
  3. One-time unread bump: adding closedAt and handedBack to stored data changes every open item's observation on the next read, so open items show as new once, as in kixelated/quest#163. Accepted.

Validation

  • just check: passed.
  • just test: passed after merging main (382 CLI tests, 2 skipped; 312 Worker tests; the hook, installer, and lifecycle suites).
  • quest check --base origin/main on the head branch: passed.
  • New tests:
    • projects.test.ts: held and handed-back drafts (board status, body not stored); UNKNOWN at the same head, at a new head, and on a newly seen PR; pruning on a history read and on a read by number after a webhook; activity-log paging.
    • inbox.test.ts: a held draft is in progress, then becomes a decision once handed back.
    • seed.test.ts: the three seeded states, on both the inbox and the board.
  • Mutation check: seeding fix(skills): planning PRs leave issues open and summarize the decision #19 as BLOCKED instead of UNKNOWN fails the seed test's Merge-button assertion. That confirms the background read does reread fix(skills): planning PRs leave issues open and summarize the decision #19.

Suggested follow-ups

  • Notify when a draft is handed back. Push notifications still fire only on ready_for_review and non-draft opens, so adding ## Decisions needed to a draft makes it a decision without a push. Recommendation: fold this into Execution, which owns PR decision items.
  • Shrink the history pass's GitHub reads. Each history page now fetches up to 50 PR bodies to derive one boolean, and a full pass still pages every entity ever created (about 126 pages on moq-dev/moq). Recommendation: a /quest-plan quest to page history by UPDATED_AT and stop at the last pass's high-water mark, and to read bodies only for open drafts.
  • Cache installation tokens across an inbox load. Every inbox page mints App tokens per project, which dominated the seed test's time. Recommendation: plan it only if pilots show slow inbox loads.

(written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits October 21, 2026 22:38
A draft PR its starter still holds (no `## Decisions needed`) shows as
Claimed on the board and in progress in the inbox; a handed-back draft
or ready PR is turned in. The hand-back rule moves to quest/core so the
CLI and the Worker agree.

GitHub's UNKNOWN merge state keeps a recorded PR's last known state at
the same head, and defers its change time at a new head until a definite
state arrives.

readState reads only open entities and unread pointers through a partial
index, the activity log pages, and a closed entity's row is pruned once
it has been closed past RETENTION (30 days, pending the user's choice).

The seed adds a held draft, a handed-back draft, and an UNKNOWN PR, and
its test opens only the items it checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title Keep the inbox accurate on busy projects feat(kix): keep the board and inbox accurate on busy projects Oct 22, 2026
@kixelated

Copy link
Copy Markdown
Collaborator Author

Implementation pushed in dd2a391. just check and just test pass locally. This stays a draft until the user picks the retention period: 30 days is recommended, and 7 or 90 are the alternatives, with the measured row counts in the description. The other two decisions in ## Decisions needed were made without asking and are listed there for confirmation.

(written by Claude Opus 5.5)

kixelated and others added 2 commits October 22, 2026 08:39
The user chose 30 days, so RETENTION is final and the remaining
inbox-accuracy quest is deleted with its links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 22, 2026 15:41
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 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-22T15:44:34.783724Z a3ca546 Draft marked ready
🔒 Security Review ✅ Completed 2026-10-22T15:45:18.426359Z a3ca546 Draft marked ready
ℹ️ 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: a3ca546dcf

ℹ️ 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".

SELECT ?,json_extract(value,'$.kind'),json_extract(value,'$.item.databaseId'),json_extract(value,'$.item.number'),?,?,json_extract(value,'$.item'),?,json_extract(value,'$.stale')
FROM json_each(?) WHERE ${current}
ON CONFLICT(repository,kind,id) DO UPDATE SET number=excluded.number,changedAt=CASE WHEN github_project_activity.data IS NOT excluded.data THEN excluded.changedAt ELSE github_project_activity.changedAt END,data=excluded.data,seenIn=excluded.seenIn,missing=0,stale=CASE WHEN excluded.stale>0 AND github_project_activity.stale=excluded.stale THEN 0 ELSE github_project_activity.stale END`,
ON CONFLICT(repository,kind,id) DO UPDATE SET number=excluded.number,changedAt=CASE WHEN ${unknown} AND NOT (${keeps}) THEN github_project_activity.changedAt WHEN ${prior} IS NOT ${data} THEN excluded.changedAt ELSE github_project_activity.changedAt END,data=${data},seenIn=excluded.seenIn,missing=0,stale=CASE WHEN excluded.stale>0 AND github_project_activity.stale=excluded.stale THEN 0 ELSE github_project_activity.stale END`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the pre-webhook timestamp for UNKNOWN merge states

When an existing PR webhook triggers this reconciliation, webhook.ts has already overwritten the row's changedAt with the delivery time. Consequently, both UNKNOWN branches here retain that new timestamp rather than the timestamp from the last definite observation, so a synchronize delivery that is reread while mergeStateStatus is UNKNOWN still marks the PR unread immediately—the behavior this change is intended to prevent. The tests only exercise direct refreshes after manually resetting changedAt, so they do not cover the production webhook path; retain the pre-webhook version separately or avoid advancing the user-visible timestamp until reconciliation determines a definite change.

Useful? React with 👍 / 👎.

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.

I disagree that this is a regression here. webhook.ts (unchanged by this PR) sets changedAt on every named delivery by design: the delivery itself, such as a synchronize push, is the news, and that holds whatever the reread later finds, UNKNOWN or not. The UNKNOWN handling targets rereads that have no event of their own, such as history passes after the base branch moves, where GitHub flips mergeability to UNKNOWN and may never send a follow-up event. Those are the spurious unread marks the quest set out to remove.

Making webhook deliveries count as news only once the reread finds a definite change would redefine unread for every webhook event, not just UNKNOWN. I'm leaving that as a possible follow-up rather than widening this PR.

(written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merge summary for head a3ca546dcf17c6d165c324fc9c2e16a7e18ff917:

  • Changes: held drafts show as Claimed on the board and In progress in the inbox; handed-back drafts and ready PRs are decisions; UNKNOWN merge state no longer marks rereads unread; readState reads only open rows through a partial index; the /state log pages 50 rows at a time; closed rows are pruned after 30 days. The quest file and its link are deleted.
  • Decisions (user-settled): 30-day RETENTION; held drafts listed under In progress with no actions; one-time unread bump on the next read is accepted.
  • Codex P2 thread (discussion_r4350433938): kept as is. Webhook deliveries keep marking items unread as before; whether webhook-driven unread should wait for a definite change is a follow-up, not part of this PR.
  • Checks: all 17 required checks pass on this head; quest check --base origin/main --branch quest/m0/kix/inbox-accuracy passes (run with the base build).

Enabling auto-merge.

(written by Claude Opus 5.5)

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