Repository navigation
fix(analytics): stop /report and /duplicates hanging, and scope the ownership panels (#13602) - #14014
Merged
Merged
Conversation
`duplicate_detector._get_files_to_scan` documented `.worktrees/` as pruned since #12779. It never was — the name was not in SKIP_DIRS. Measured on a working checkout: 160,105 of 167,072 code files (96%) lived under `.worktrees` and `.claude/worktrees`, so every analytics walk scanned the repo ~26 times over. 167,072 files / 5.46s becomes 6,967 / 0.06s. Adding the name is not enough on its own, and the reason is the interesting part. Five call sites tested `SKIP_DIRS & set(path.parts)` against an ABSOLUTE path, which asks whether a skip name appears anywhere in it — including inside the scan root. CI and development both run from inside `.worktrees/`, so adding the name would have made a scan rooted there classify every file below it as skippable: no error, no log, zero results, indistinguishable from a clean repo. That is the failure mode umbrella #13852 exists to remove, so shipping it as the fix was not an option. `is_skipped_path(path, root)` relativises first, applying the pattern #7128b already established in pipeline-scripts after hitting this exact trap. All five consumers migrated: file_counter, file_pipeline, file_analyzer, file_cache, and duplicate_detector's own `_should_skip_path` — which had NO callers, so its absolute comparison was a trap armed for whoever wired it in next. Wired in rather than deleted. Guards: _tree() now builds both worktree layouts (it never did, which is why the existing pruning tests passed throughout). Mutations verified — dropping the names fails 5 tests, restoring the absolute check fails 1, unwiring _should_skip_path fails 1.
…n stop (#13602) `/report` never responded and logged nothing while not responding, which is the hardest failure to diagnose from outside. Mechanism: `_build_analysis_task_list` fans out five analyses. Four wrapped themselves in `asyncio.wait_for`; `api_endpoint` did not. The shared analytics executor has 8 workers, so once slower analyses occupied them all, that unbounded submission queued and never started — `gather` never returned, the socket stayed open, and no handler ever ran to log anything. Not a slow endpoint: a hang. Adding a fifth `wait_for` is the pattern that let one be forgotten, so the deadline now rides in the task tuple and `_run_parallel_analyses` applies it. A task without a deadline is a type error rather than an oversight. The margin lets an analysis that bounds itself report its own, better-named timeout first; the outer bound only fires where there is no inner one. An analysis abandoned at its deadline now says so — previously the section simply vanished from the report with no explanation. Second half: #12779 added a cancel token but consulted it in exactly one place, inside the os.walk loop — 0.14s of a run whose remaining phases measured over six minutes on a single tree (105,441 blocks, still building MinHash signatures at 6m15s, RSS 2.0 GB, against a 120s timeout). So every timed-out scan kept a worker thread until it finished naturally, and that is the starvation above. The token is now polled in block extraction and in signature building — the phase that dominates the runtime — plus at the phase boundary between them. Mutations verified: removing the per-task deadline fails 2 tests (and without the bound the test hangs, which is the defect itself); removing the abandonment log fails 1; setting api_endpoint's deadline to 0 fails 2. The existing scoping test caught the tuple shape change on its own.
…ht guard (#13602) #12779 added a lock and a cancel token so abandoned duplicate scans could not accumulate. Two holes let the accumulation straight back in. `/report` submitted its own scan directly to the shared analytics executor with neither the lock nor the token — same executor, same 8 workers. So a report queued a full walk regardless of one already running, and its orphan could not be told to stop. It now delegates to `_run_standard_analysis`, passing LOW_SIMILARITY_THRESHOLD, which is the detector default this call site was already relying on implicitly; behaviour is unchanged. The lock was released in a `finally` that ran on timeout while the orphan thread was still going, so the next poll acquired it immediately and queued a second scan on top of the first. It is now released by a done-callback on the future. That is only bounded because the previous commit made the token effective in the long phases — without it this would be a permanent block, which is why the guard test asserts the lock reopens as well as that it stays shut. Both holes are invisible from outside: the endpoint returns, the log says the scan timed out, and nothing reports that the work is still running or that scans are stacking. Mutations verified: restoring the direct submission fails 2, releasing the lock on timeout fails 1, dropping the cancel signal fails 1.
…blocking the loop (#13602) `/ownership/analysis` returned 400 "Invalid path: must be within project root" for its own source. It resolved the source's clone path correctly and then validated against `_get_project_root()` — the root it had just replaced, on the next line but one. A clone path lives under data/code-sources/<id> while the project root resolves to the checkout: sibling subtrees that never nest, so the containment check rejected EVERY source-scoped request. Deterministic, not layout-dependent, and not #13149 — the resolver works; the wrong one was called. Now uses `resolve_scan_root` (#12330), as environment.py, call_graph.py and dependencies.py already do, rather than hand-rolling the lookup. `/ownership/expertise` and `/ownership/knowledge-gaps` had it worse. They accepted `source_id` "for API consistency" and ignored it, so every source's panel silently analysed AutoBot's own tree — plausible data for the wrong project, which is why nobody noticed. Both now resolve the same way. The rejection was logged as "Path traversal attempt blocked", so ordinary use filled the security log with false attack alerts and echoed the internal clone path while doing it. A path that stays inside its root after normalisation is a scope mismatch; only one that climbs out earns an attack-shaped warning. The test asserts both directions — a real `../../etc/passwd` must still be caught, since a check that only proves the false alert is gone passes equally against a detector that says nothing. Fixing the 400 makes this panel reachable for the first time, which exposed what it would then do: `git blame` ran as a bare synchronous subprocess inside `async def`, one per file, up to 30s each, blocking the event loop — the reason unrelated endpoints returned 000 while an analytics panel loaded. It also globbed a tree with no `.worktrees` in its skip list and no ceiling. Moved off the loop, worktrees skipped, capped at 2000 files with the truncation logged; a silent cap would read as "we analysed everything". Mutations verified: restoring the wrong validation root fails 1, siblings ignoring source_id fails 2, treating every rejection as an attack fails 2, blame back on the event loop fails 1, dropping the analyzer skip fails 1. Not fixed here: ownership_analyzer.py has two pre-existing flake8 findings in a lint-excluded directory (an unused `recency_cutoff` left over from #1183's extraction — the helper computes its own and uses it, so it is redundant rather than unwired, and a long line). Out of scope for this issue.
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
Blocker 1 is one I manufactured while fixing the walk. Adding `.worktrees` to SKIP_DIRS was safe because `is_skipped_path` relativises — but I also added it to `_SKIP_DIRECTORIES`, a SECOND skip list in ownership_analyzer.py that still matched the ABSOLUTE path, with exactly the trap my own docstring documents. `resolve_project_root()` lands inside a worktree on any dev checkout, so `/ownership/analysis` — the endpoint this issue is about — returned 200 with zero contributors and no error. Measured: 4,725 .py files globbed, 0 surviving. I replaced a loud 400 with a silent empty 200, which is the defect class this umbrella exists to remove. `_should_skip_file` now takes the root. Blocker 2: `CancelledError` is not `TimeoutError`, so it skipped the timeout handler, fell through to `finally`, and released the lock with the token never set — while the shield guaranteed the orphan kept burning an executor thread. Reachable via uvicorn graceful shutdown and via any outer deadline tighter than the inner one, which /report's now is, separated only by the grace margin. Same treatment as a timeout, then re-raised. Blocker 3: seven lines shipped with no test that could fail — every cancel check in the detector, the file cap, and the new fallback. The detector half could be deleted wholesale with the suite green, and the cap test asserted `isinstance(int) and > 0`, which cannot fail for any positive integer. Replaced with behavioural guards; all seven now fail under mutation. Also from the review: The `.resolve()` fast path I added for perf accepted a symlink pointing OUT of the scan root as scannable. The 167k-file measurement that justified it is the number this PR removes — at 6,967 files it saves ~0.5s on a scan that was taking six minutes. Reverted; correctness is the better trade here. The fallback returned the absolute check, i.e. the bug, on precisely the inputs hardest to reason about. A path that cannot be placed under the scan root is not in the scan: return True. A 2000-file cap at the measured 0.45s per `git blame` is a 15-minute request — a hang traded for a wait nobody sits through. Added a wall-clock budget, made it cover all patterns rather than giving each a fresh one, and surfaced `truncated`/`truncated_reason` in the response: server-side logging is not enough when the caller is the one drawing conclusions from the numbers. The traversal warning logged the internal root and dropped the attacker-supplied path — the wrong one of the two. Inverted, and the non-attack branch stays at WARNING so an alert keyed on a rejected path keeps firing; only the misleading "attack" wording is scoped to an actual climb out of the root. /report mapped "scan in flight" and "scan timed out" to the same None, so the duplicate section vanished without comment whenever the GUI's /duplicates poll held the lock — now the common case, since they share one. Logged.
10 of 11 tasks
#13602) Round 2 review returned "land it" with one regression I had introduced and one comment that overstated a guarantee. The `resolve()`-on-both-sides check cost 98us per call against 4.5us, and the cost is paid per GLOBBED path, not per surviving file. My comment used the wrong denominator and understated it by ~30x: 214k globbed paths on this repo, not 6,967 files. Measured 2.6-5x slower walks — 9.1s to 24.0s on the shared FileListCache — in a PR whose first commit is labelled `perf`. Three tiers now. Lexically inside a skip dir returns immediately with no stat at all, and that is 96% of paths: a symlink cannot change the answer for a file already under a skipped directory. Lexically clean and not a symlink returns kept, paying one lstat — glob/rglob do not recurse symlinked directories, so only a symlinked LEAF can escape, and that is what the earlier lexical shortcut got wrong. Everything else resolves properly. Tuple slicing rather than relative_to(), which measured ~28us of the ~29us total. 98us -> 12.6us, against a 1.8us floor for the unsafe original. The wall-clock budget did not cover the walk: the deadline check sat behind the skip `continue`, so globbing and skipping were outside it entirely — 18s of walk on this repo with blame costing nothing. Moved ahead of the continue, so the budget is now what the comment claims it is. Also from the review: the `test_one_budget_covers_all_patterns` docstring named the outer `break` as what prevents a fresh budget per pattern. It is not — the counters are hoisted above the pattern loop, so removing the break leaves an equivalent mutant, and the test could not fail for the reason it gave. It guards the hoisting; reworded to say so. Dropped the `startswith` clause in _should_skip_file, subsumed by the prefixed form. The traversal warning now logs %r truncated to 200 chars — the attacker controls that string and an unescaped newline in it forges log lines. Mutations re-verified after the rework: removing the lstat guard fails 1, disabling the tier-1 short circuit fails 3, slicing against the absolute path fails 3, disabling the time budget fails 1.
…13602) CI's hardcoded-value checker (#694) flagged `timeout=30` on the git blame subprocess. The literal was pre-existing, but wrapping it in `asyncio.to_thread` re-added the line, so it counts as mine — correctly. The inconsistency it exposed is the more interesting half: a single blame was allowed 30s inside a phase budget of 20s, so one slow file could consume the entire budget and overrun it. The per-blame timeout is now 10s, deliberately below the budget that contains it, with a test asserting that relationship rather than the values. All three bounds now come from env via the repo's `env_float`/`env_int` pattern (AUTOBOT_OWNERSHIP_MAX_FILES, AUTOBOT_OWNERSHIP_BUDGET_SECONDS, AUTOBOT_OWNERSHIP_BLAME_TIMEOUT_SECONDS) instead of module literals. Also this round: the PR body needed a literal `Refs #13602` for the issue- linkage check. `Refs`, not `Closes` — the endpoints are fixed in code but the issue's measurements came from a running instance and closure needs the same.
mrveiss
added a commit
that referenced
this pull request
Aug 14, 2026
Review blocker, and it inverted the change on four routes: a blanket 60s outer
bound sat BELOW pre-existing internal timeouts, so requests that previously
succeeded slowly would have become guaranteed 504s.
/analysis 60s outer over ANALYSIS_TIMEOUT = 180s
/env-analysis 60s outer over LLM_ANALYSIS_TIMEOUT = 240s (use_llm_filter is
a caller-facing Query param, so that path is reachable on
request, not theoretical)
/report 180s outer under its own fan-out ceiling of 195s
The /report case indicts the design rather than a number. That file's docstring
— written in #14014 — says the fan-out deadlines are OUTER bounds with a grace
margin so an analysis that times out reports its own, better-named message and
wins the race. A route bound of 180s under a 195s ceiling means the generic
"report did not complete within 180s" wins instead, which is exactly what that
comment says must not happen.
Deadlines are now derived, not guessed: the three constants are hoisted to
module level and each route is `@bounded(THAT_TIMEOUT + ROUTE_DEADLINE_GRACE)`.
A change to the internal budget carries the route bound with it, which a magic
number would not.
The checker now enforces the relationship, and that immediately found FOUR more
routes the review had not flagged — siblings in the same two files. It is a
file-level heuristic: proving which budget a route can reach needs call-graph
analysis, but noticing that a file declares 240s and bounds one of its own
routes at 60s is cheap and catches the real shape. Where it cannot prove
reachability I raised the bound rather than exempting the route, because being
wrong in the tight direction breaks requests while being generous only costs a
longer worst case.
Also from the review: mutating `_is_bounded` to `return True` — so the checker
would accept ANY route — left the suite green. Every test asserted the checker
is happy with a repo that happens to be fully bounded, which is not the same as
asserting it catches an omission. Five tests now plant a synthetic unbounded
route in a tmpdir and assert exit 1, plus the direction that must stay true and
the declared-with-a-reason path.
mrveiss
added a commit
that referenced
this pull request
Aug 15, 2026
* chore: claim worktree for #14015 * feat(api): a request deadline that cannot be forgotten (#14015) #13602 was an endpoint that held the socket open past 180s and logged nothing — no exception, no timeout, no trace. Not a slow handler; one that never ran, because it queued behind eight busy executor workers on the single analysis in its fan-out with no asyncio.wait_for. The defect is the pattern, not that one omission. When bounding is opt-in and per-call-site, an unbounded path is invisible: it looks exactly like every other handler until it hangs. report.py alone carried three different timeout constants and one analysis with none, and nothing about the file made that visible. Measured on this surface: 85 routes, 0 bounded. Two halves, both load-bearing. `bounded(seconds)` stacks with with_error_handling and turns the hang into a 504 naming the handler and the limit, plus a WARNING naming it in the log — because a 504 alone tells the caller nothing about which of many analyses ran long. A non-positive deadline raises at decoration time rather than on the first request: wait_for(0) expires immediately, and an always-on failure reads as a broken endpoint rather than a misconfiguration. It sits OUTSIDE with_error_handling so error-response construction is inside the deadline too; that decorator already re-raises HTTPException, so the 504 survives either way. `tools/lint/check_route_deadlines.py` is the half that makes omission impossible. A route in a covered package must carry @bounded or appear in UNBOUNDED_BY_DESIGN **with a reason** — so an unbounded route is a declaration a reviewer can see and argue with, never a default nobody chose. Widening coverage is adding a path, at which point every unbounded route under it fails until it is bounded or declared: the gap gets enumerated, never stays silent. It also fails when it matches zero routes, because an empty result otherwise reads as a clean one. Deadlines come from #13602's own measurements: 180s for report and duplicates (the two that hung), 120s for call-graph (36.4s cold, over a 45s client cap under contention), 60s default for the rest, which were sub-2s in that sweep. Mutations verified with the restore in a trap rather than a following command: dropping wait_for fails 4, accepting a non-positive deadline fails 3, the log no longer naming the handler fails 4, the payload losing the name fails 2, one route losing @bounded fails the checker, a stale UNBOUNDED_BY_DESIGN entry fails it, and a broken matcher fails it — where the same broken matcher with the empty-result guard removed exits 0 and reports a clean tree. * fix(api): a route deadline must clear the budget it can reach (#14015) Review blocker, and it inverted the change on four routes: a blanket 60s outer bound sat BELOW pre-existing internal timeouts, so requests that previously succeeded slowly would have become guaranteed 504s. /analysis 60s outer over ANALYSIS_TIMEOUT = 180s /env-analysis 60s outer over LLM_ANALYSIS_TIMEOUT = 240s (use_llm_filter is a caller-facing Query param, so that path is reachable on request, not theoretical) /report 180s outer under its own fan-out ceiling of 195s The /report case indicts the design rather than a number. That file's docstring — written in #14014 — says the fan-out deadlines are OUTER bounds with a grace margin so an analysis that times out reports its own, better-named message and wins the race. A route bound of 180s under a 195s ceiling means the generic "report did not complete within 180s" wins instead, which is exactly what that comment says must not happen. Deadlines are now derived, not guessed: the three constants are hoisted to module level and each route is `@bounded(THAT_TIMEOUT + ROUTE_DEADLINE_GRACE)`. A change to the internal budget carries the route bound with it, which a magic number would not. The checker now enforces the relationship, and that immediately found FOUR more routes the review had not flagged — siblings in the same two files. It is a file-level heuristic: proving which budget a route can reach needs call-graph analysis, but noticing that a file declares 240s and bounds one of its own routes at 60s is cheap and catches the real shape. Where it cannot prove reachability I raised the bound rather than exempting the route, because being wrong in the tight direction breaks requests while being generous only costs a longer worst case. Also from the review: mutating `_is_bounded` to `return True` — so the checker would accept ANY route — left the suite green. Every test asserted the checker is happy with a repo that happens to be fully bounded, which is not the same as asserting it catches an omission. Five tests now plant a synthetic unbounded route in a tmpdir and assert exit 1, plus the direction that must stay true and the declared-with-a-reason path. * fix(lint): emit through the streams, not builtin print (#14015) code-quality flagged 10 newly-added print() calls in the new checker. The sibling checkers use print too, but they predate the rule and only NEW lines are flagged — so the grandfathering, not the file type, is what makes them clean. Two _err/_out helpers writing to sys.stderr/sys.stdout instead. The documented escape is a per-line noqa, and ten of those would be noise where one helper is not. Verified against the repo's own hook rather than by reading the rule: 22 files scanned, no violations. * fix(lint): resolve derived deadlines instead of exempting them (#14015) Round-2 review returned "land it" with two findings that are the same hole this PR's own commit message criticised, reappearing on the newer half of the checker. `_literal_deadline` exempted every computed expression, on the reasoning that deriving from a budget beats guessing past it. That reasoning is right about the pattern and wrong as a check: it assumes the arithmetic is correct. `@bounded(PATTERN_ANALYSIS_TIMEOUT - 170.0)` resolves to 10 seconds under a 195s ceiling and passed unchallenged — demonstrated by the reviewer, reproduced here. "Derived" and "correct" are different claims, and conflating them is the shape this whole issue is about. Expressions are now evaluated: literals, module constants, and additive arithmetic over them. An unresolvable name contributes ZERO rather than making the whole expression unresolvable — ROUTE_DEADLINE_GRACE is imported, not a module constant, so returning None there would silently skip every route this check exists for. Zero under-estimates the deadline, so the error runs toward flagging: a false flag is argued down in review, a missed one ships a 504. Second finding: disabling the too-tight branch entirely left all 17 tests green. Coverage for "the checker enforces the ceiling" came only from the real repo happening to have no too-tight routes today. Five synthetic-route tests now exercise it directly — a literal under the budget, a derived expression that computes under it, one that clears it, an unresolvable name, and a file with no budget at all. Mutations verified: disabling the ceiling branch fails 3, making unknown names unresolvable fails 1, dropping arithmetic resolution fails 11.
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.
Thinking Path
#13602 reports two endpoints that never respond — 180s, 0 bytes, and nothing in the log. No exception, no timeout, no trace. That last part is what made it worth chasing carefully: a hang with no output means the handler never ran, not that it ran and failed.
It turned out to be four defects that compound, plus a fifth in a sibling panel.
The hang itself.
_build_analysis_task_listfans out five analyses. Four wrap themselves inasyncio.wait_for;api_endpointdoes not. The shared analytics executor has 8 workers, so once the slower analyses occupy them all, that unbounded submission queues and never starts —gathernever returns, the socket stays open, and no handler ever runs to log anything. Not a slow endpoint: a hang, and a silent one by construction.Why the workers were all busy.
duplicate_detector._get_files_to_scanhas documented.worktrees/as pruned since #12779. It never was — the name was not inSKIP_DIRS. Measured on a working checkout: 160,105 of 167,072 code files (96%) lived under.worktreesand.claude/worktrees, so every analytics walk scanned the repo ~26 times over.Why abandoned scans stayed. #12779 added a cancel token but consulted it in exactly one place, inside the
os.walkloop — 0.14s of a run whose remaining phases measured over six minutes on a single tree (105,441 blocks, still building MinHash signatures at 6m15s, RSS 2.0 GB, against a 120s timeout). Every timed-out scan held a worker until it finished naturally.Why they stacked.
/reportsubmitted its own scan straight to the same executor with neither the single-flight lock nor the token, andduplicates.pyreleased that lock in afinallythat ran on timeout while the orphan was still going. Both re-created the accumulation #12779 set out to stop.And the separate 400.
/ownership/analysisresolved its source's clone path and then validated against_get_project_root()— the root it had just replaced, on the next line but one.What Changed
Bounded execution. The deadline now rides in the task tuple and
_run_parallel_analysesapplies it, so a task without one is a type error rather than an oversight — adding a fifthwait_foris the pattern that let one be forgotten. An analysis abandoned at its deadline now says so; previously the section simply vanished from the report with no explanation.Cancellation that works. The token is polled in block extraction and in signature building — the phase that dominates the runtime — plus at the boundary between them.
One guarded path.
/reportdelegates to_run_standard_analysis, passing the detector default it was already relying on implicitly. The lock is released by a done-callback on the future, so it outlasts the orphan. That is only bounded because the token now works, which is why the test asserts the lock reopens as well as that it stays shut.The walk.
.worktrees/worktreesadded toSKIP_DIRS— 167,072 files / 5.46s becomes 6,967 / 0.06s.Adding the name is not enough on its own, and this is the part worth reading. Five call sites tested
SKIP_DIRS & set(path.parts)against an absolute path, which asks whether a skip name appears anywhere in it — including inside the scan root. CI and development both run from inside.worktrees/, so adding the name would have made a scan rooted there classify every file below it as skippable: no error, no log, zero results, indistinguishable from a clean repo. That is precisely the failure mode this umbrella exists to remove, so shipping it as the fix was not an option.is_skipped_path(path, root)relativises first, applying the pattern #7128b already established after hitting this exact trap. All five consumers migrated — includingduplicate_detector._should_skip_path, which had no callers, so its absolute comparison was a trap armed for whoever wired it in next. Wired in rather than deleted.Ownership scoping. All three endpoints now use
resolve_scan_root(#12330)./expertiseand/knowledge-gapswere worse than the 400: they acceptedsource_idand ignored it, silently analysing AutoBot's own tree for every source — plausible data for the wrong project, which is why nobody noticed. The false"Path traversal attempt blocked"warning is gone, and it no longer echoes the internal clone path.Fixing the 400 makes that panel reachable for the first time, which exposed what it would then do:
git blameas a bare synchronous subprocess insideasync def, one per file, up to 30s each, blocking the event loop. Moved off the loop, worktrees skipped, capped at 2000 files with the truncation logged — a silent cap reads as "we analysed everything".Verification
Every guard mutation-tested — the fix reverted, the test confirmed to fail, the fix restored. 14 mutations, 14 caught:
.worktreesdropped from SKIP_DIRSis_skipped_pathback to the absolute check_should_skip_pathunwiredapi_endpointdeadline set to 0/reportsubmitting directly againsource_idTwo of those deserve a note. The deadline mutation originally hung rather than failed, because one test had no outer bound — a test that hangs burns the CI job timeout instead of reporting, so it is now bounded and fails cleanly in 21s. And the traversal-warning guard asserts both directions: a real
../../etc/passwdmust still be caught, since a test proving only that the false alert is gone passes equally against a detector that says nothing.Not in this PR
timeout_keep_alive; every handler hand-rolls its ownwait_forwith three different constants. This PR makes the deadline structural for/report's fan-out, which is where the reported hang lives. A universal one is an architectural change with a much wider blast radius, filed separately with options.ownership_analyzer.py, which sits in a lint-excluded directory: an unusedrecency_cutoffleft over from refactor(code_analysis): fix pre-existing function length violations in analysis scripts #1183's extraction (the helper computes its own and uses it, so it is redundant rather than unwired) and a long line. Unrelated to this issue.Issue stays open pending host evidence — the endpoints are fixed in code, but #13602's measurements were taken against a running instance and closure needs the same.
Refs #13602 — deliberately not
Closes. The endpoints are fixed in code, but #13602'smeasurements were taken against a running instance, and closure needs the same numbers
from a host. Part of umbrella #13852.
Model Used
Opus 5 (1M context)