Forgejo/Gitea forge parity: pr-search, pr-diff, and the pr-exists hang - #19
Merged
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…minutes
Forgejo charges the /pulls list endpoint per RETURNED PR OBJECT — measured
0.78s at limit=1 and 32.8s at limit=50 against Forgejo 15.x. Walking
state=all over a 1599-PR repo therefore cost ~17 minutes for one yes/no
question, which read as a hang.
GET pulls/{base}/{head} answers it in ~1.2s, and answers it better: the old
scan matched .head.ref, which Gitea rewrites to refs/pull/N/head once a
merged PR's branch is deleted, so merged PRs could not be found by branch
name at all. .head.label keeps the branch name and this endpoint matches on
it.
Also adds pr-search and pr-diff for gitea, replaces recently-merged's
26-minute walk with the cheap issues index plus bounded concurrent head
lookups, and gives every gitea script a wall-clock timeout so a future hang
surfaces as a named error rather than a stuck phase.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- forge.ts: pr-search and pr-diff are no longer disabled for gitea. - spawn-worktree: the collision query says 'is:open' instead of leaning on pr-search's old open-only default. Without this, an all-states search aborts every re-spawn of an issue that ever had a merged PR with a factually wrong 'Found N open PR(s)' (upstream cluesmith#1331's review). - github/team-github: a concept disabled by a PROVIDER PRESET was invisible to forgeConfig lookups, so on-it-timestamps returned an empty map with nothing on stderr. Both callers now name the provider and the consequence. - checks.ts: a null from the pr-exists concept is reported as 'could not answer', not as 'no PR exists'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… is:open Assertions are anchored to command lines rather than matched with toContain, so the explanatory comments — which quote the very strings under test — cannot keep a test green after the code it pins is removed (cluesmith#1331's review caught exactly that). bugfix-1137's fixtures move with the endpoints they mock. Its guarantee is unchanged and still asserted: reads go through tea api, never tea pulls list, and a paged read is not silently truncated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r case Found by the acceptance run against entriq. extractExecutable reads a script's first substantive line, which answered 'echo' for issue-view and 'case' for pr-list — so `codev doctor` on a Forgejo repo told the user to install `echo`, and a genuinely missing `tea` went unreported. That is the cluesmith#1455 defect class, and the '# forge-executable:' declaration is its remedy. The test now asserts it across the whole preset rather than concept by concept, so a new gitea script cannot reintroduce it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Routes the findings by tier: the Forgejo per-object pricing and the head.label survival go to arch.md as system-shape properties, the partial-result rule is promoted to the hot lessons tier (displacing the permissions/booleans lesson into cold), and three debugging lessons land in lessons-learned.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. A Gitea error body is a JSON object, which reached tea_api_paged's `jq -s 'add'` before any caller could classify it — an object cannot be added to an array, so the walk died on a raw jq diagnostic and the script's own 'PR #N not found' message was unreachable. Reproduced against live Forgejo. Classify before accumulating; status 4 carries the body out to the caller. Never returned a wrong answer, only a worse message — and the name-only path was the untested one, which is why it was the broken one. 2. The plan called for a SKILL.md byte-identity test that was never written. 3. checks.ts's null-vs-false change had no test; three added. 4. gitea_timeout: the marker's path was predictable in a world-writable tmpdir (both files now in a private mktemp -d), and the watchdog claimed a timeout for a command that finished in the same instant — it checks kill -0 first now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parseVerdict knows APPROVE/REQUEST_CHANGES/COMMENT and nothing else, so the VERDICT: SKIPPED that the NOT-RUN convention writes falls through to the COMMENT default, and allApproved counts COMMENT as approval. porch therefore printed 'All reviewers approved!' for a codex lane that never ran. Not fixed here — it is porch behaviour affecting every project that records a skip, not something in this diff. Recorded in the review so a human reading the gate summary is not misled, and in lessons-learned so the next person writing an absence-recording convention checks who reads it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Architect verified in source: besides parseVerdict falling through to COMMENT, allApprove separately returns true when reviews.length === 0, so zero reviewers approves too. And the fall-through's own comment says 'but the consult ran' — the vocabulary was built for a reviewer that ran and emitted something unparseable, so a reviewer that never ran is not merely unhandled, it is inexpressible. Widening a default would not have fixed it. Also records that #2, #4 and #11 merged under the same false summary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pseudoseed
added a commit
that referenced
this pull request
Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PIR Review: Forgejo/Gitea forge parity — pr-search, pr-diff, and the pr-exists "hang"
Fixes #12
Summary
A Forgejo repository can now run on the bare
forge.provider: giteapreset with no per-concept overrides and noghshim.pr-searchandpr-diffare implemented for gitea and no longer disabled;pr-existsstopped enumerating pull requests and answers in one request instead of about seventeen minutes; andrecently-merged, which had been silently returning nothing on everyafx statusfor months, went from about twenty-six minutes to roughly one second.The reported bug was a hang. It was not one — it was a ~17-minute loop, and finding that out changed the fix from "add a timeout" to "stop enumerating."
Files Changed
packages/codev/scripts/forge/gitea/_lib.sh(+245 / -0) — timeout, bounded-concurrency fetch, repo/default-branch resolution, paged-walk deadlinepackages/codev/scripts/forge/gitea/pr-exists.sh(+104 / -0) — rewritten around the base/head lookuppackages/codev/scripts/forge/gitea/pr-search.sh(+199 / -0) — newpackages/codev/scripts/forge/gitea/pr-diff.sh(+76 / -0) — newpackages/codev/scripts/forge/gitea/recently-merged.sh(+174 / -0) — rewritten around the cheap issues indexpackages/codev/scripts/forge/gitea/pr-list.sh(+14 / -0) — truncation is no longer swallowed by a pipepackages/codev/scripts/forge/gitea/issue-view.sh(+1 / -0) —# forge-executable: teapackages/codev/src/lib/forge.ts(+57 / -0) — gitea preset enables both concepts; shared "concept unavailable" reportingpackages/codev/src/lib/forge-contracts.ts(+17 / -0) —PrSearchItemgainsstate,baseRefName,title,urlpackages/codev/src/lib/github.ts(+23 / -0) — a preset-disabledon-it-timestampswarns instead of returning emptypackages/codev/src/lib/team-github.ts(+19 / -0) — names the provider instead of "returned no data"packages/codev/src/commands/porch/checks.ts(+17 / -0) —nullfrompr-existsis "could not answer", not "no PR"packages/codev/src/agent-farm/commands/spawn-worktree.ts(+12 / -0) — the collision query saysis:openpackages/codev/src/__tests__/pir-12-gitea-pr-concepts.test.ts(+~670 / -0) — newpackages/codev/src/commands/porch/__tests__/pir-12-pr-exists-null-vs-false.test.ts(+77 / -0) — newpackages/codev/src/__tests__/bugfix-1137-gitea-tea-api.test.ts(+110 / -0) — fixtures follow the endpointspackages/codev/src/agent-farm/__tests__/spawn-worktree.test.ts(+18 / -0).claude/skills/forge/SKILL.md,.codex/skills/forge/SKILL.md(+50 each) — kept byte-identicalcodev/resources/arch.md(+11 / -0),codev/resources/lessons-learned.md(+6 / -0),codev/resources/lessons-critical.md(+1 / -1)codev/plans/12-forgejo-gitea-forge-parity.md(+217 / -0),codev/state/pir-12_thread.md(+111 / -0)24 files changed, 2108 insertions, 75 deletions.
Commits
28e764f8c[PIR Forgejo/Gitea forge parity: implement pr-search and pr-diff, fix the pr-exists hang #12] Plan draft: gitea pr-search, pr-diff, and the pr-exists hang63c980789[PIR Forgejo/Gitea forge parity: implement pr-search and pr-diff, fix the pr-exists hang #12] fix(gitea): answer pr-exists in one request instead of ~17 minutesd0666871f[PIR Forgejo/Gitea forge parity: implement pr-search and pr-diff, fix the pr-exists hang #12] fix: enable the gitea concepts, and stop degrading silentlybbc4ebe4a[PIR Forgejo/Gitea forge parity: implement pr-search and pr-diff, fix the pr-exists hang #12] test+docs: pin the endpoints, the head.label behaviour, and is:open64cb220d0[PIR Forgejo/Gitea forge parity: implement pr-search and pr-diff, fix the pr-exists hang #12] fix(doctor): report tea for every gitea concept, not echo or casee018fee98[PIR Forgejo/Gitea forge parity: implement pr-search and pr-diff, fix the pr-exists hang #12] docs: builder thread for the implement phaseThe measurements that decided the design
Everything here follows from timings taken against a live Forgejo 15.x (
git.pseudoseed.com/pseudoseed/entriq, 1599 PRs) withtea0.14.2, not from reasoning about the API.GET /repos/{o}/{r}/issues/1GET .../pulls?state=all&limit=1GET .../pulls?state=all&limit=50GET .../pulls?state=closed&limit=50GET .../issues?type=pulls&state=all&limit=50GET .../pulls/{base}/{head}GET .../pulls/{n}.diffThe
/pullslist is priced per returned PR object, not per request — roughly 0.65 s each, because Gitea materialises head and base commit info for every row.pr-existswalkedstate=allat 50 per page, so it cost 32 pages × ~33 s ≈ 17 minutes;recently-mergedwalkedstate=closedat 48 s a page ≈ 26 minutes. Raising the page size cannot help a per-item cost, and neither can a cache that has to be filled once. The fix had to stop enumerating.Two properties of Gitea made that possible, and one of them contradicts a caveat the old code documented as unfixable:
GET /pulls/{base}/{head}answers "is there a PR from this branch" in one request, 404s when there is none, and takes slashes in the head branch unescaped.head.labelsurvives branch deletion. The old scan matched.head.ref, and its comment correctly noted that Gitea rewrites that torefs/pull/N/headonce a merged PR's branch is deleted — concluding that a merged PR could not be found by branch name. But.head.labelkeeps the original name and the base/head endpoint matches on the stored head branch. Verified: PR 3869, merged with its branch gone, reportshead.ref = refs/pull/3869/headandhead.label = builder/aspir-3860, andpulls/main/builder/aspir-3860returns it. So the new implementation is not only ~900× faster, it answers a question the old one could not.recently-mergeduses a different lever: theissues?type=pullsindex is nineteen times cheaper than the/pullsview of the same rows because it skips that commit materialisation, and it accepts a server-sidesincefilter. It carries everythingMergedPrItemneeds except the head branch, which is then fetched per match, eight at a time.The
codev doctorfix, found during the acceptance runSeparate from the above and worth its own heading, because it silenced the one diagnostic that should have caught this class of problem.
extractExecutablereads a script's first substantive line to decide what must be onPATH. For the gitea preset it was answeringechoforissue-viewandcaseforpr-list— socodev doctoron a Forgejo repo told the user to installecho, and a genuinely missingteawent unreported on exactly the repositories that need it. This is the defect class cluesmith#1455 exists to close, and the remedy it established is the# forge-executable:declaration; two scripts were missing one. The test now asserts it across the entire preset rather than concept by concept, so a new gitea script cannot reintroduce it.What upstream cluesmith#1331 contributed, and what its review contributed
cluesmith#1331 (open, unmerged; no PR against it in this fork) fixes
pr-searchto span all PR states, becausegh pr list --searchdefaults to open-only and post-mergeconsult --type prtherefore fails with "No PR found for branch". The gitea implementation defaults to all states for that reason.Its review turned out to matter more than its diff. Making
pr-searchall-states breaksspawn-worktree.ts, which queriesin:body #Nand leaned on the old open-only default to mean "open PRs". Under an all-states search that becomes "did this issue ever have a PR", and every re-spawn, every follow-up to a partial fix, and every retry after a closed PR aborts with a factually wrong "Found N open PR(s)". The call site now saysis:openexplicitly, and the gitea script honoursis:qualifiers so that saying it works. The review's second point — thatPrSearchItemgave callers nostatewith which to defend themselves — is addressed by addingstateto the contract and ordering results open-first.github/pr-search.shandgitlab/pr-search.share deliberately not touched here. They carry the same cluesmith#759 bug, but fixing them is upstream cluesmith#1331's diff and would conflict on every future sync. The architect is filing that as an adopt-the-PR job, the way cluesmith#1146 and cluesmith#1458 were handled.The truncation contract
recently-mergedbounds itself three ways — a default 7-day window whenCODEV_SINCE_DATEis absent (announced on stderr; never "all time", which is the 26-minute walk), a 300-PR ceiling, and the paged walk's wall-clock deadline. When a bound bites it exits 3 with empty stdout.That emptiness is the point. A short list and a truncated list are indistinguishable once printed, so
[]with status 0 means "nothing merged" and status 3 means "I stopped looking", and stderr names which bound bit. This is now the documented exit-status contract for forge concepts generally.Test Results
npm run build: ✓ passnpm test: 5498 passed, 2 failed — both pre-existing and unrelated (see Flaky Tests). 45 tests are new across two files (pir-12-gitea-pr-concepts.test.ts,pir-12-pr-exists-null-vs-false.test.ts), plus one behavioural spawn assertion and the reworkedbugfix-1137cases.Live verification against
git.pseudoseed.com/pseudoseed/entriqwith all three overrides deleted, driven through the real dispatcher (config load → preset → env → script → JSON parse), not by invoking scripts by hand:pr-existsopen branchtruepr-existsmerged branch, deletedtruepr-existsabsent branchfalsepr-search head:<branch>pr-search <issue number>mergedpr-search in:body #3860 is:open[]— spawn not blockedpr-diffname-only / fullissue-viewpr-listrecently-merged24 huser-identitypseudoseedteam-activity,on-it-timestampsdisabledentriq's config was then restored and diff-verified byte-identical to both the pre-test copy and the architect's 11:20 backup. It runs the globally installed codev 3.3.1, whose preset still disables
pr-search/pr-diff, so its overrides remain load-bearing until this ships. Delete them after this merges and entriq updates.Read this before trusting the review depth.
Verbatim provider message, both attempts:
The codex lane was skipped on explicit architect instruction rather than hold a fork-local change for six days. The absence is recorded as a NOT-RUN file at
codev/projects/12-forgejo-gitea-forge-parity-imp/12-review-iter1-codex.txtcarryingVERDICT: SKIPPEDandCONFIDENCE: NONE, so it cannot be misread as a review that happened — but that path is gitignored (.gitignore:65), which is why the coverage is stated here too. The same quota blocked codex on #2, #4 and #11 earlier the same day.Porch's own gate summary says otherwise, and it is wrong.
porch next 12reportscodex: COMMENTand prints "All reviewers approved!".parseVerdict(porch/verdict.ts:41-47) recognises onlyAPPROVE,REQUEST_CHANGESandCOMMENT;VERDICT: SKIPPEDmatches none of them, falls through to the "no valid VERDICT line found — treat as COMMENT" default, andallApprovedcountsCOMMENTas approval. So a lane that never ran is indistinguishable from one that approved, in the exact summary a human reads when deciding to merge.There is a second, independent route to the same false statement, found by the architect when verifying this in the source:
allApprovealso returnstruewhenreviews.length === 0, so zero reviewers approves as well. And the fall-through's own comment states the bug aloud — "no valid VERDICT line found but the consult ran" — because the vocabulary was built for a reviewer that ran and emitted something unparseable. A reviewer that never ran cannot be expressed in it at all, which is why widening a default would not have been enough.That is this PR's own lesson pointed back at the tooling — a sixth arrival at it in one day, and the one with the largest blast radius, since it affects every project that records a skip. #2, #4 and #11 all merged with porch reporting unanimous approval while codex had read nothing; those merges were sound because the human approving each gate knew the coverage was two of three and each PR body said so, but that was the human channel working despite the tool. It is porch behaviour rather than anything in this diff, so it is not fixed here — tracked as #20. Read the table above, not porch's summary line.
Weigh this specifically. The absent lane is the one that most often catches shell-quoting and POSIX-portability defects, and this diff is five POSIX
shscripts, a hand-rolled process watchdog, and a pile of jq. That is close to the worst pairing of which lane is missing against what the change is made of. Two of the three bugs found during implementation were exactly that class, and both were caught by tests rather than by reading.What the Claude lane found, and what changed because of it
All four findings were reproduced before being fixed; none were argued down.
gitea_api_errorchecks were unreachable (pr-diff.shname-only,recently-merged.sh). Gitea answers a 404 with a JSON object, which reachedtea_api_paged'sjq -s 'add'first — and an object cannot be added to an array, so the walk died onjq: error … array ([]) and object ({...}) cannot be addedbefore the script's own classification ran. Reproduced against live Forgejo exactly as reported. The exit status was non-zero either way, so no wrong answer was ever returned; what was lost was the sentence naming the missing PR.tea_api_pagednow classifies before accumulating and returns a distinct status 4 with the body on stdout, so the caller's message finally fires. Both paths now tested — the full-diff 404 was covered and the name-only one was not, which is why only one of them was broken.SKILL.mdtwins and it was never written. The files were identical; nothing stopped them drifting. Now pinned.checks.ts's null-vs-false change had no test. It is a real behaviour change —nullfrom the concept means "could not answer", not "no PR exists" — and it mattered concretely, since before this PR the gitea script blew the 30 s ceiling on every run, makingnullthe normal outcome on that provider. Three tests added in a separate file, because mocking the forge layer is somethingchecks.test.tsdeliberately avoids.gitea_timeoutnits, both fixed. The marker file's path was derived from the output file's ("$_tf.fired"), whichmktempdoes not reserve — a predictable name in a world-writable tmpdir that anyone could pre-create to force every call to report a timeout. Both files now live in a privatemktemp -d. And the watchdog claimed the timeout unconditionally, so a command finishing in the same instant the deadline passed was reported as timed out, discarding a good answer; it now checkskill -0first. That narrows the window rather than closing it — a lock would be needed for that — and a false timeout is a retryable error rather than a wrong answer.Architecture Updates
COLD —
codev/resources/arch.md, § Integration Points → Forge Concept Commands. Four additions, all current-state reference rather than changelog:packages/codev/scripts/forge/<provider>/, single-source with no skeleton mirror, andnullin a preset is a deliberate refusal rather than "no script".forgeConfiglookups —forgeConfig?.['x']reads user config only. Availability must be asked ofgetForgeCommand/isConceptDisabled. This is the system-shape surprise that madeon-it-timestampsreturn an empty map silently on every gitea repo.nullfromexecuteForgeCommandmeans "could not answer"./pulls, andhead.labelsurviving branch deletion. Both filed here rather than in lessons-learned because they are properties of a system, not general engineering advice — the "looks like X but is actually Y" routing.Nothing was promoted to
arch-critical.md: all of it is reference detail that matters only when writing a forge concept.Lessons Learned Updates
HOT —
codev/resources/lessons-critical.md, one promotion with displacement, since the file was at its cap of ten:It earns the hot slot because the same principle has now been arrived at independently three times in this codebase —
afx send's render gate, #13's log extraction, and this PR's exit-3 contract — which is the signal that it should be changing decisions before they are made rather than being rediscovered.Displaced into
lessons-learned.md§ Architecture: "Model permissions as roles/capabilities, not booleans." Still true and still worth reading; it is the narrowest of the ten, applying only when designing a permission system, where the promoted rule applies to any list, log, render, or fetch that can stop early. Reviewers should push back if they disagree with that trade — it is the judgement call in this diff that is least supported by evidence.COLD —
codev/resources/lessons-learned.md§ Debugging and Root Cause Analysis, three entries:limit=1vslimit=50timing tells you which regime you are in and takes a minute; against a per-item cost, every page-size instinct is useless.pr-existswas being killed at 30 s and returningnull, which the caller rendered as "no PR exists" — same root cause, different bug, and only one of them was the one reported.Things to Look At During PR Review
gitea_timeoutin_lib.shis the subtlest thing here, and it was wrong twice. First it killed the command and left the caller blocked anyway: every caller runs it inside$(...), and a grandchild kept the write end of that pipe open, so the timeout message printed at 3 s and the script was still hung two minutes later. The command now writes to a temp file rather than the caller's pipe, and the watchdog's own stdout goes to/dev/nullfor the same reason. Then it classified timeouts by exit status (143/137), which a wrapper using operand-lesswaitdefeats — POSIX makes that always return 0, so a process killed by SIGTERM reported success with an empty body and the caller diagnosed an unreadable repository instead. The watchdog now records that it fired and nothing is inferred. Both failure modes have tests.pr-exists/pr-search head:. The endpoint needs a base; it uses the repository default unlessCODEV_PR_BASEis set, so a PR against an integration branch needs that variable. A miss printsfalse, which fails the porchpr_existsgate loudly. Falling back to a list scan on the 404 path was rejected deliberately — it would reintroduce the ~17-minute walk on the failure path, where it would be least expected.pr-search's query grammar is a parser, not a pass-through, covering exactly the five query strings this codebase builds. A sixth would return[]with a stderr note. The five and their call sites are tabulated at the top of the script.bugfix-1137's fixtures moved. Its guarantee is unchanged and still asserted — reads go throughtea api, nevertea pulls list, and a paged read is not silently truncated — butpr-existsnow pages nothing at all, which is the strongest form of that. The faketeano longer serves anypulls?state=…fixture, so a reintroduced scan fails there with "no fixture for".toContain. Fix #759: pr-search includes all PR states so merged PRs are found cluesmith/codev#1331's review caught assertions that passed against the explanatory comment quoting the flag under test, staying green with the flag deleted from the command. The comments here quote the very endpoints being pinned, so the same trap was live.How to Test Locally
pir-12→ Review Diffafx dev pir-12teaauthenticated:CODEV_BRANCH_NAME=<open-branch> sh packages/codev/scripts/forge/gitea/pr-exists.shreturnstruein about a second, and issues exactly two requestsfalse; a badCODEV_REPOerrors rather than returningfalseCODEV_SEARCH_QUERY='in:body #<n> is:open'returns[]for an issue whose PR is merged, i.e.afx spawnis not blockedCODEV_SINCE_DATEunset onrecently-merged.shannounces a 7-day window rather than walking everything;CODEV_FORGE_MERGED_MAX=2over a busy window exits 3 with empty stdoutcodev doctorreportsteafor every enabled gitea concept anddisabledforteam-activity/on-it-timestampsFlaky Tests
None skipped or annotated. Two pre-existing failures were left untouched (which of them fires varies per run — they are contention-sensitive, and a third from the same file appeared in an earlier run):
packages/codev/src/__tests__/spec-1280-measurement-instrument.test.ts—emits byte-identical output twice at the same commit,reports the same total under a C locale as under UTF-8, andPHASE_ITERS is a linear comparison constant. Each invokesscripts/measure-prompt-surface.shtwice against a 60 s budget, and that script takes ~31 s per invocation on this machine. Proven pre-existing by running the same test against the unmodified main checkout, where it fails identically at 77 s; the architect independently reproduced it there past 300 s. The script costs the same against either root (31.3 s vs 33.0 s), so this is machine speed, not diff content.packages/codev/src/terminal/__tests__/session-manager.test.tsalso drops one timing-sensitive stderr-tail case under full-suite load; the file passes 91/91 in isolation, and a different case from it failed on an earlier run, which is the signature of contention rather than a defect.This is further evidence for #8 — porch's check timeout is a hardcoded 300 s with no override key, and entriq's config records the same class of problem at 460 s quiet / 859 s contended.