alreadyFiled only ever sees 50 issues, so every autopilot dedupe key silently expires — #206 was QA'd twice #281
Labels
No labels
agent
agent:ci
agent:done
agent:failed
agent:needs-input
agent:refined
agent:refining
agent:running
agent:shipped
agent:skip
autonomous
autopilot
driven
local
plan
proposal
qa
qa-gap
research
retro
ship
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jeroen/cartopolis#281
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
alreadyFiledfetches the issue list once, unpaged, and Forgejo caps a page at 50 no matter whatlimitasks for:tools/autopilot.ts:606-612. Theapihelper is a bare singlefetchwith no paging of its own (tools/autopilot.ts:139-146).Measured against this server today (read-only GETs, no build):
So every
autopilot:<key>marker older than the 50 most recent issues is invisible, and nothing logs it — from the function's point of view the key genuinely is not there. All five filing paths depend on it:fileRedMainTicket(tools/autopilot.ts:541), the frontier loop and its refill (tools/autopilot.ts:682,:736), the QA re-verify and the dated QA rotation (tools/autopilot.ts:900,:917), and the retro period key (tools/autopilot.ts:980).Two duplicates already exist on the board, not one. I scanned every
autopilot:<key>marker across all 103 issues; exactly two keys appear twice:qa-reverify-206— #210 (2026-08-30) and #266 (2026-09-02), the case this ticket was filed for.log— #181 and #257 are both open, both titled "Autopilot log", both carrying<!-- autopilot:log -->.logIssuelooks the marker up through the same unpaged fetch (tools/autopilot.ts:1061-1063), did not find #181, and made a second one. #181's daily notes stop 2026-09-01; #257's start 2026-09-02. The lane's own "silence is readable" instrument split its history in half and said nothing.Three more call sites are one page away from the same failure and are not currently wrong only by luck:
tools/autopilot.ts:646— the open-ticket gate that enforcesmaxOpen. Truncation here undercounts in-flight tickets, so the lane files past its own quota. 47 open issues today, three short of the cap.tools/autopilot.ts:214(rearmParkedTickets) andtools/autopilot.ts:890(the QA open gate) read the same unpaged open list.tools/autopilot.ts:1107(digest) counts what was filed, closed and parked from the truncated list.The idiom to copy is already in the file —
sweepMergedBranchespages/branchescorrectly attools/autopilot.ts:936-942.The
q=route the ticket suggests is already rejected in the code, and the reason still holds: the comment attools/autopilot.ts:600-604records that Forgejo'sq=is a fuzzy match over title and body, thatautopilot:pdok-aerial-imageryreturns unrelated issues under it, and that a false positive makes the frontier look fully filed so nothing is ever proposed again. Page it; do not switch to search.Approach
One file:
tools/autopilot.ts. No Rust, no crate touched.api(tools/autopilot.ts:139) —pagedApi(path, token?)that appendspage=n&limit=50, concatenates, and stops on a short page, the same shape astools/autopilot.ts:938-942. Give it a page ceiling so a server that never returns a short page cannot loop for ever.allIssues()returning the raw objects, filled by one paged walk (3 requests today).alreadyFiledderives its bodies from it (replacing theissueBodiescache attools/autopilot.ts:604), andlogIssue(:1062) anddigest(:1107) read the same memo instead of each doing their own truncated fetch. This is the same intra-tick staleness the process already has —issueBodiesis cached for the life of the process today, and every filing path uses a distinct key — so nothing changes there.tools/autopilot.ts:214,:646and:890.state=allscope exactly as they are, and keep the comment attools/autopilot.ts:600-604(extend it to say the list is paged, and whyq=is still not the answer).tools/autopilot.ts:1193-1221) that readsprocess.argvand throws withoutFORGEJO_TOKEN, so importing it runs the autopilot. Wrap that block inif (import.meta.main)andexportthe functions under test. This is a mechanical change to the entrypoint; the four command paths must behave identically when run asbun tools/autopilot.ts <cmd>.tools/autopilot.test.ts— stubglobalThis.fetchwith a fake issue list of >50 entries carrying a marker on an old one, and assertalreadyFiledfinds it, that the walk stops on a short page, and that it issues more than one request.Leave
shippedRecently(tools/autopilot.ts:821) atlimit=30: it is label-filtered and cut to 7 days, and 16 such issues exist. Note it in the paging comment as a deliberate window, not an oversight.Acceptance criteria
alreadyFiledwalks every page of/issues?state=all&type=issuesand finds a marker on an issue more than 50 issues old.alreadyFiled("qa-reverify-206")returnstrueandalreadyFiled("log")returnstrue.alreadyFiled,logIssueanddigestshare it.logIssuefinds an existing "Autopilot log" issue regardless of its age, so no third one is created.tools/autopilot.ts:214,:646and:890are paged, so themaxOpengate counts every open autopilot ticket once the board passes 50 open issues.q=search is introduced, and the reason recorded attools/autopilot.ts:600-604is preserved.tools/autopilot.test.tsexists, stubsfetch, and fails if the fetch is reverted to a single unpaged request.tools/autopilot.tsruns no commands and requires no environment variables;bun tools/autopilot.ts status,ship,generateandtickstill dispatch as before.q=is not the fix.Verification
Live read-only check, with
FORGEJO_TOKENsourced from~/.config/cartopolis/forgejo.env(neverstatus-safe to print — it echoes config, not the token):A dry end-to-end check on the real board is
bun tools/autopilot.ts statusplus reading the log line fromgenerate— butgeneratefiles tickets, so do not run it against production to test this. Nothing here needs a GPU, a workstation,--shotorcargo shots.Out of scope
logIssuekeeps using whichever is newest-first (#257) and makes no more. Reconciling the two is a maintainer's call on the board, not a code change, and this branch should not touch either issue..forgejo/workflows/and nopackage.jsonin the repo, so gating this would mean addingsetup-bunor a runner-image change — a host concern, larger than the bug. The test is run by hand with the command above.frontier-refill-0. Same key family, different defect; fixing the paging does not fix it and this branch should not fold it in.shippedRecently'slimit=30(tools/autopilot.ts:821), which is a deliberate 7-day window over a label-filtered list.state=allstays the scope.api.Open questions
None.
Branch:
fix/281-autopilot-page-issue-listsOriginal request
Found by the 7-day retrospective on #279, while working out why one ticket was QA'd
twice.
What happens
alreadyFiledis the autopilot's only guard against filing the same ticket twice.Every filing path goes through it: the red-
mainticket, the frontier refills, theQA re-verifies and the QA rotation.
tools/autopilot.ts:606.Forgejo caps a page at 50 regardless of what
limitasks for. Measured againstthis server today:
So
alreadyFiledanswers "no" for every key older than the 50 most recent issues,and there is no second page. Every dedupe key in the system silently expires after
50 issues. Nothing logs it, because from the function's point of view the key
genuinely is not there.
The instance
#206 was QA'd twice. Both tickets carry the same marker:
<!-- autopilot:qa-reverify-206 --><!-- autopilot:qa-reverify-206 -->56 issues apart, which is past the window. The second pass cost a full session and
re-walked ground the first had already covered. (It was not wasted — it filed #268
— but nothing chose to spend a session that way.)
The same hole is open under the others:
main is red on <sha>can be filed twice for one commit once 50 issues havegone by, and its comment path says "a ticket for it exists — waiting", so a
duplicate does not just cost a ticket, it un-parks a stop.
frontier-refill-Nand the datedqa-<target>-<date>keys expire the same way.frontier-refill-0. Between the two, the frontier's memory is doubly unreliable.What would fix it
Page it, the way any other list in this file would have to be.
limit=50andpage=nuntil a short page comes back, or read only what is needed: the marker isa fixed string, so Forgejo's issue search (
q=) answers the same question in onerequest without holding 50 bodies in memory.
A test is available and cheap: assert that a key present on an issue more than 50
issues old is still found.
Where the seam is
tools/autopilot.ts:606—alreadyFiled. TheissueBodiescache above it is fine;it is the single unpaged fetch that fills it.
Filed by the 7-day retrospective (#279).
🤖 Refined by the viberfox issue agent. Reply with @agent refine and what is wrong to have this rewritten.
🤖 Refining this before anything gets built — reading the code, no branch, no commits. If it turns out I'd have to guess at the requirements I'll stop and ask instead.
🤖 Refined. The description above is now the spec; I'll pick it up for implementation on the next poll.
If the direction is wrong, say so now — @agent refine with a correction rewrites it, and
agent:skipstops me.🤖 viberfox issue agent on the VPS — 15 turns · $1.68 · session
09fdaa3a-dd01-425a-b3e7-654e02b8b2f7. Reply with @agent to continue this session.🤖 Picked this up — working on branch
fix/281-autopilot-page-issue-listsin a fresh worktree on the VPS. I'll report back here when I'm done.Label an issue
agent:skipto keep me off it, ordrivento keep it in a session you are driving.I fixed the paging and pushed the branch.
What changed — one file,
tools/autopilot.ts, plus a new test file. No Rust, no crate touched.The autopilot read the issue list with a single request asking for 100 issues. Forgejo answers that with 50 and does not say it truncated, so any
autopilot:<key>marker older than the 50 newest issues was invisible and the lane happily filed the ticket again. Confirmed against the live board today: the unpaged request returns 50 issues (#284 down to #231); walking it 50 at a time over three pages returns all 103.pagedApi, which walks a list 50 rows at a time until a short page comes back, with a 40-page stop so a misbehaving server cannot spin for ever.q=search is still not used, and the comment explaining why is kept.if (import.meta.main)and the tested functions are exported, so importing the file runs nothing and needs no credentials.One thing I found while verifying. A plain text search cannot tell an issue that quotes a marker from one that carries it, and issue #281 — the ticket for this bug — quotes
autopilot:login its body. Newest first, it won, so today's daily note would have been posted onto the bug report instead of onto #257. The log lookup now checks the title as well. I left the duplicate check alone: the same ambiguity there only ever declines to file something twice.How I verified it.
bun test tools/autopilot.test.ts— 7 tests pass; putting the single unpaged request back fails 4 of them.bun tools/autopilot.ts statusstill prints configuration and state and exits 0. Against the real board, read-only: 103 issues walked,alreadyFiled("qa-reverify-206")andalreadyFiled("log")both return true, and the log lookup now returns #257 "Autopilot log". I did not rungenerateortick— those file tickets.Not verified. There is no bun step in CI, so nothing runs this test automatically; it is a hand-run command, as the ticket says. Nothing here renders, so no visual check is needed anywhere.
For you to decide.
tools/autopilot.tsis a fenced path, so the lane will not ship this itself — it needs a person to land it. And the running autopilot uses its own copy oftools/, so the fix takes effect when that copy is refreshed; worth confirming that happens. #181 and #257 both still exist and this branch does not touch either, as the ticket asked.Branch
fix/281-autopilot-page-issue-lists(pushed)🤖 viberfox issue agent on the VPS — 46 turns · $4.27 · session
c194dddc-e424-4421-b6f3-88274c957e47. Reply with @agent to continue this session.🤖 Marked
agent:donewithout a CI verdict on537894e5— no workflow reported on this commit. The branch is waiting for review; check the run yourself before landing it.🤖 2026-09-04
main: green on6473ddb1🤖 2026-09-05
main: green on856f666b🤖 2026-09-06
main: green on1204b670🤖 2026-09-07
main: green on1204b670🤖 2026-09-08
main: green on1204b670🤖 2026-09-09
main: green oncd5d1b84🤖 2026-09-10
main: green on03512806🤖 2026-09-11
main: green on7e4441d9