Repository navigation
THI-450: build_state forks less than half as many processes per refresh - #55
Merged
tdody merged 46 commits intoNov 6, 2026
Conversation
…napshot, git in the background, gh without keychain reads (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…utables and C locale for ps, fresh list_windows over a shared snapshot, git fallbacks and keep-on-failure, gh token TTLs and 401 shapes, interleaved measurement (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…over a rolling minute (THI-450) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…; proc_table and live claudes read it (THI-450) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nd the pane pid map (THI-450) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nt says one row per window; the spec states the snapshot's staleness (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… session_status's ps eww can't drift from the snapshot's locale; lstart parses as UTC (THI-450) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d for 10 minutes (THI-450) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… s, first sight still synchronous (THI-450) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d update check off (THI-450) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t runs under LC_ALL=C; isdir stat moves outside the refresh lock; _GIT_TTL renamed to _GIT_REFRESH_S (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he short sha's real growth rule, the identity-TTL and clean-filter assumptions (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… gh in the module table (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…igration notes; comments without history; two snapshot assumptions (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…v (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… without it (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d reviews' own (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ound (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ows, not dropped (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… guard; _git_fetching/_fetch_git_state_into_cache match the PR cache's names (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oes, not an invented history (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts own global (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sets LC_ALL (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…a ps slower than the TTL still serves the callers after it (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the ps table it took, so a build slower than the 1 s TTLs still forks each once (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… it is under 15 s old, with a warning at most once a minute, instead of blanking the refresh (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…process snapshot changes, not on a clock of its own (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…de test; the pane walk uses it over descendants (now nearest first), and proc_table becomes command_lines (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… and kept 15 s on failure, one claude test, the loaded ps timings (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…not a refresh; its fork counts are server-wide (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s as visible rows, not a hover-only tooltip (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ter the poll's start (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…OKEN forever (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…h, no can't-start guard, four assumptions (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ach twice (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ended (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…(THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…es (THI-450) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…build_state-runs-too-many-subprocesses-per-tick-so-the # Conflicts: # static/dist/app.js
…build_state-runs-too-many-subprocesses-per-tick-so-the # Conflicts: # static/dist/app.js
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.
Fixes THI-450. Also fixes the main case of THI-453: Codex panes were missed because
pscut the process name to 16 characters. Executables are now read from argv[0].build_stateruns about once a second. On a loaded machine each poll started hundreds of processes per minute, and the dashboard fell 3–13 s behind. This PR makes each poll read the system once and share the result:pstable per poll.agent_processes.process_snapshot()is the only process table. It runsps -axo pid=,ppid=,lstart=,state=,args=underLC_ALL=C/TZ=UTC.agent_processes.is_claude_executableis the one claude test.psfails or times out (2 s), the last table stands in while it is under 15 s old.tmux list-panes -a(tmux.list_panes) replaceslist-windows. A newline inside a field is rejoined, not dropped.list_windowsstill reads fresh, so a window that was just stamped or created reads back at once.build_statepins thelist-panesrows and thepstable it took (tmux.pinned_snapshot,agent_processes.pinned_snapshot, both ContextVars). Every reader in that poll, the fan-out threads included, gets them however long the poll runs. Callers outside a poll get a 1 s cache (tmux.panes_snapshot).git_state_forrunsstatus --porcelain=v2 --branch, thendiff-index -M --shortstat HEAD. Both run with--no-optional-locksand underLC_ALL=C, and neither writes the index.cached_git_stateserves the cached value and refreshes it in the background every 15 s ± 5 s. A cwd seen for the first time is still read on the spot, because pid rebinding needs its branch.reviewsalready uses.periscope/gh.py. Everyghcall gets aGH_TOKENread every 10 minutes, so gh's keyring no longer runs/usr/bin/securityon each call.gh.AVAILABLEis the one check for whether gh is installed.periscope/forks.pycounts processes through a Python audit hook./api/stategains abuild_statsblock: build time and the gap between polls, both per poll; processes per minute and top commands, both server-wide over 60 s./api/healthzhas the full map. A poll slower than 3 s logs "slow poll", at most once a minute.poll 0.4 s · every 1.1 s · server 140 forks/min, then the slowest build and the top commands._channel_gc) keeps a pane's notification if it arrived during the current poll, since the poll's pane list can be as old as the poll.Before / after
Dev server on :17375, 10 panes, 90 s per run. "Before" is
acd98472: it already reports the poll's cost but still uses the old commands. A dev server skips the narrator, the session poke and the MCP listener, so compare these runs with each other, not with prod.Under the ticket's loaded subprocess timings. Wrapper scripts on the server's PATH delay
ps1.0 s,tmux0.2 s andgit1.5 s. The ticket measuredpsat 0.5–1.7 s,list-panesat 0.15–0.26 s andgit statusat 1.4–3.3 s.In the slow "after" runs,
psandtmux list-panescame to about one per poll (19–20 per minute at ~20 polls per minute). That held even with builds over 1 s.No added delay:
Processes per minute by command, idle runs:
I checked gh's
securitychild directly: 10gh api rate_limitcalls without a token showed one, and withGH_TOKENand the quiet flags they showed none.Assumptions a reviewer may want to challenge
statuscounts untracked files across the whole repo, so a cwd in a subdirectory can go fromcleanto?3.@plus 7 characters of the commit. Before, git's abbreviation chose the length, which here was 8, so a detached pane's branch changes once on deploy.\x1fin a window name or path still drops that pane's row. The full-roster passes then treat the window as gone: its track row is pruned, and boot prunes its session rows. A newline in those fields is rejoined.ps: if the 2 spstimes out, the last table (under 15 s old) stands in. A claude that exited or started in that time reads as it did then; session ids still come from the fresh 1 s index. Past 15 s, the four readers blank for one poll.Checks
uv run pytest -q: 2168 passednpm test: 1321 passedbin/check: exit 0static/distup to dateSpec:
docs/superpowers/specs/2026-11-02-fewer-forks-per-refresh-design.md· plan:docs/superpowers/plans/2026-11-02-fewer-forks-per-refresh.md· review: two rounds, every finding fixed, the second round found nothing blocking🤖 Generated with Claude Code