Repository navigation
perf(terminals): spend one inspection start on a whole cadence round - #18438
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Team Run ID: 📥 CommitsReviewing files that changed from the base of the PR and between 265f6a7b4caf5a7c69a8ace0bac7082691114f0c and ca019fb. 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe inspection queue batches tasks that share a local host observation into one round. It limits remote inspections individually. The queue computes concurrent and per-second admission capacity, removes disposed tasks, preserves pending-title priority, and defers pumping through a deduplicated microtask. Tests cover shared-task bursts, remote limits, priority handling, and disposed-task filtering. Merge Risk: ⚪ Minimal · up to Local terminal inspections now share a single observation round while remote panes retain individual admission. The change preserves rate limiting and priority behavior with coverage for the updated scheduling paths, leaving no actionable current-head merge risk. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Description checkExplanation The description explains the problem, implementation, testing, performance impact, and visual proof in detail. However, it omits several required template sections, including Testing with the required checkboxes, AI Disclosure, Review, Agent skill upstream boundary, Notes, and Checklist. It also provides "N/A" for Linked Issue even though the template requires an issue link. Resolution Add all required template sections and complete them. Provide a valid linked issue instead of "N/A", or obtain an explicit repository exception. Add the Testing checkboxes, AI Disclosure, Review, Agent skill upstream boundary, Notes, and Checklist sections.
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Batched inspection IPC —
pty:inspectProcessBatchfans the per-paneinspectOnePtyProcess(verbatim extraction of the oldpty:inspectProcessbody) out over a batch, settling each entry independently as{ inspection } | { error }with no shared state between entries. - Queue round coalescing — the inspection queue now drains all
batched: truetasks as one round (one "start"), keeps unbatched/remote panes admitted one-at-a-time, and drops disposed tasks in a single compacting pass instead of splice-per-drop. - Renderer coalescer —
inspectLocalTerminalProcessCoalescedmerges same-tick local reads into one microtask-flushed IPC call, chunked atTERMINAL_PROCESS_INSPECTION_BATCH_LIMIT(256), with a per-pane fallback wheninspectProcessBatchis absent (web/older preload). - Routing —
inspectRuntimeTerminalProcesskeeps remote ids (remote:/SSH) on the per-pane path and only routes local ids through the coalescer; the monitor setsbatchedfromisRemotePtyId.
I traced the verdict-integrity, per-entry incarnation, and capability-fallback paths end-to-end and found them sound:
inspectPtyProviderProcessForRendererclassifies known failures intoclientOnlyUnverifiableInspectionbefore throwing, so the batch's{ error: message }carry is equivalent to the single-pane IPC rejection when the renderer re-classifies —codeis never the deciding factor for errors that reach the catch.- The 300-pane regression test is a real guard: it asserts an exact count (
toBe(300)) plus a round-trip budget, not a truthiness stand-in. availableInspectionStartsaccounting is consistent — a batched round and each unbatched task each consume exactly one start, soactiveInspections ≤ MAX_CONCURRENT_INSPECTIONSandinspectionStarts.length ≤ MAX_INSPECTION_STARTS_PER_SECONDboth hold after a full pump.
One thing I verified but chose not to flag as blocking: remote panes only ever enqueue pending-title (their cadence polls are gated off), and batching reduces local start consumption, so the priority-order change (batched rounds drain before unbatched pending-title) has no meaningful starvation effect — the queue re-pumps on the next tick rather than 100ms when a start is freed mid-round.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
4381a9a to
265f6a7
Compare
Design changed since the bot pass — this PR is now renderer-only@pullfrog reviewed Gone: On the one point it raised but did not block (batched rounds draining before unbatched The framing cost the batch was buying has been measured rather than assumed — real @coderabbitai was rate-limited on the previous head and produced no findings; it is welcome to On the
|
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Team
Run ID: 5bd011e5-cca4-45c5-a200-bae94b6336da
📥 Commits
Reviewing files that changed from the base of the PR and between 7106101 and 265f6a7b4caf5a7c69a8ace0bac7082691114f0c.
📒 Files selected for processing (3)
src/renderer/src/components/terminal-pane/agent-completion-process-monitor.tssrc/renderer/src/components/terminal-pane/agent-process-inspection-queue.tssrc/renderer/src/components/terminal-pane/agent-process-inspection-round.test.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== Changed files ==='
git diff --name-status origin/main...HEAD
echo '=== Diff ==='
git diff --find-renames --find-copies --unified=80 origin/main...HEAD -- \
src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts \
src/renderer/src/components/terminal-pane/agent-process-inspection-queue.ts \
src/renderer/src/components/terminal-pane/agent-process-inspection-round.test.ts
echo '=== Queue outline ==='
ast-grep outline src/renderer/src/components/terminal-pane/agent-process-inspection-queue.ts --items all
echo '=== Monitor outline ==='
ast-grep outline src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts --items all
echo '=== Test outline ==='
ast-grep outline src/renderer/src/components/terminal-pane/agent-process-inspection-round.test.ts --items all
echo '=== Existing queue test references ==='
rg -n -C 3 --glob '*agent-process-inspection*.test.ts' \
'(enqueueAgentProcessInspection|requestInspection|pending-title|sharesHostObservation|dispose)' \
src/renderer/src/components/terminal-paneLength of output: 34426 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== All enqueue call sites ==='
rg -n -C 5 '\benqueueAgentProcessInspection\s*\(' src
echo '=== Inspection option and callback definitions ==='
rg -n -C 8 '\binspectProcess\s*[:=]|\binspectProcess\s*\(' \
src/renderer/src/components/terminal-pane \
-g '*.ts' -g '*.tsx'
echo '=== Process inspection/cache implementation candidates ==='
rg -n -C 8 \
'(inspectProcess|process-table|process table|inFlight|in-flight|TTL|ttl|dedup)' \
src/renderer/src \
-g '*.ts' -g '*.tsx'
echo '=== Queue rejection-containment test ==='
cat -n src/renderer/src/components/terminal-pane/agent-process-inspection-queue-rejection-containment.test.ts
echo '=== Poll scheduler request behavior ==='
ast-grep outline src/renderer/src/components/terminal-pane/agent-completion-poll-scheduler.ts --items all
sed -n '1,260p' src/renderer/src/components/terminal-pane/agent-completion-poll-scheduler.tsLength of output: 201406 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== Files mentioning pty:inspectProcess ==='
rg -l -g '*.ts' -g '*.tsx' -g '!**/*.test.*' "'?pty:inspectProcess'?|inspectProcess" \
src/main src/preload src/renderer/src/components/terminal-pane 2>/dev/null || true
echo '=== Exact inspectProcess registrations and invokes ==='
rg -n -C 12 -g '*.ts' -g '*.tsx' \
"(handle|invoke|register).*['\"]pty:inspectProcess['\"]|['\"]pty:inspectProcess['\"]" \
src/main src/preload src/renderer/src/components/terminal-pane 2>/dev/null || true
echo '=== Candidate local inspection cache files ==='
fd -t f -e ts -e tsx . src/main src/renderer/src/components/terminal-pane 2>/dev/null \
| rg '(inspect|inspection|process).*\.(ts|tsx)$' \
| rg -v '\.test\.' \
| head -80
echo '=== Monitor factory construction and inspectProcess binding ==='
rg -n -C 15 -g '*.ts' -g '*.tsx' \
"createAgentCompletionProcessMonitor\s*\(|inspectProcess:\s*" \
src/renderer/src/components/terminal-pane
echo '=== Current changed-file line numbers ==='
git diff --unified=3 origin/main...HEAD -- \
src/renderer/src/components/terminal-pane/agent-process-inspection-queue.ts \
src/renderer/src/components/terminal-pane/agent-process-inspection-round.test.tsLength of output: 201745
I found one queue-admission issue.
The new test permits this behavior with You are interacting with an AI system. |
The inspection rate limiter counted panes when it should have counted host observations. `MAX_INSPECTION_STARTS_PER_SECOND = 8` is global, and it was spent one pane at a time, so N due panes meant an effective per-pane period of max(tier, N/8 seconds) — ~37.5s at 300 panes for a pane the code polls at 750ms. Agent-completion latency degraded monotonically as panes were added. Every local pane's inspection resolves out of the same TTL-and-in-flight- deduped process-table capture, so a whole round of them is one host observation. The queue now drains all shared-observation tasks as one round on one start, launched in a single tick. Remote panes each cost their own execution-host round trip and stay admitted one at a time. Both the budget and the cadence tiers are numerically unchanged. Disposed tasks are also compacted out in one pass instead of a splice per drop, so the per-round predicate cost is linear rather than quadratic at pane scale. No IPC, preload, wire, or main-process change: each pane keeps its existing per-pane `pty:inspectProcess` invoke.
265f6a7 to
ca019fb
Compare
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Dropped the batch IPC entirely —
pty:inspectProcessBatch, the preload method, and the renderer coalescer are gone. The current head is renderer-only: three files, no wire/preload/main surface. The prior review's batch-shaped concerns (positional short-reply semantics, capability fallback,Promise.allhead-of-line stall) no longer apply. - Coalesced local inspections in the queue —
InspectionTaskgainedsharesHostObservation, set fromisRemotePtyId, andpumpInspectionQueuenow drains every shared-observation task as one round on one start, then serves unshared (remote) tasks one-per-start withpending-titlepriority. Rate and concurrency caps still bound rounds, andMAX_INSPECTION_STARTS_PER_SECOND/MAX_CONCURRENT_INSPECTIONSare numerically unchanged. - Added a regression guard —
agent-process-inspection-round.test.tscovers the 300-pane round, single-tick launch, remote one-at-a-time admission,pending-titlepriority, and disposed-pane drop. It fails pre-fix (8 panes inspected instead of 300).
I traced the load-bearing invariant end-to-end and it holds: every non-remote pane's inspection resolves out of one TTL-and-in-flight-deduped capture (getProcessTableSnapshot on POSIX, readWindowsProcessTable on Windows via snapshotReader), while remote:/SSH panes each cost their own host round trip and stay sharesHostObservation: false. The round's synchronous launch, the outstanding-tracked concurrency slot (held until the last task settles), and the availableInspectionStarts count accounting are all consistent — no over-subscription of the 8/s or 4-concurrent budgets. The rejection-containment guard still passes against the rewritten queue.
The one rough edge — the first MAX_CONCURRENT_INSPECTIONS panes of a synchronous burst each start eagerly rather than riding the coalesced round — is explicitly priced in the PR body (5 starts for 300 panes, still within the 8/s budget) and was already raised by another reviewer; I don't consider it a must-fix.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
|
Note for @pullfrog: the incremental review above was written against
Details in the reply on the original thread. |
|
No new review warranted. The head ( Task list (5/5 completed)
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
queue_file="src/renderer/src/components/terminal-pane/agent-process-inspection-queue.ts"
test_file="src/renderer/src/components/terminal-pane/agent-process-inspection-round.test.ts"
echo '=== Current head ==='
git rev-parse --short HEAD
git show -s --format='%h %s' HEAD
echo '=== Changed files ==='
git diff --name-status origin/main...HEAD
echo '=== Queue structure ==='
ast-grep outline "$queue_file" --items all
echo '=== Queue implementation ==='
rg -n -C 18 \
'\benqueueAgentProcessInspection\s*\(|\bscheduleInspectionPump\s*\(|\bpumpInspectionQueue\s*\(|\bstartInspectionRound\s*\(|\bMAX_(CONCURRENT_INSPECTIONS|INSPECTION_STARTS_PER_SECOND)\b|sharesHostObservation' \
"$queue_file"
echo '=== Round test structure ==='
ast-grep outline "$test_file" --items all
echo '=== Relevant regression tests ==='
rg -n -C 20 \
'(same.tick|same tick|unshared|remote|pending-title|pending title|300|800|microtask|round|start)' \
"$test_file"
echo '=== Current diff ==='
git diff --unified=40 origin/main...HEAD -- "$queue_file" "$test_file"Length of output: 27139
The regression test enqueues 300 shared tasks plus seven unshared tasks. It verifies one shared launch tick and that all seven unshared tasks run in the test window. This covers the admission-cost regression that caused the original finding. If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! You are interacting with an AI system. |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Coalesced the enqueue-time burst —
enqueueAgentProcessInspectionnow pumps through a deduplicated microtask (queueInspectionPump+inspectionPumpQueued) instead of inline, so a synchronous 300-pane burst forms one round rather than spending a start per pane until the concurrency slots fill and parking the rest on the 100 ms retry. This addresses the earlier "coalesce the initial burst" feedback. - Tightened the round-launch guard — the test now asserts exactly one tick carrying
PANES(toEqual([PANES]), no longer the loose>= PANES - 4) and adds seven unshared panes that must still fit in the same window, which is what proves the round cost one of the eight starts.
I traced the microtask pump against the existing timer pump (scheduleInspectionPump) and the settle path: both re-read availableInspectionStarts and re-account activeInspections/inspectionStarts fresh, so a redundant pump can't over-spend the budget, and the 100 ms timer remains a backstop when starts are exhausted. pending-title priority and the disposed-drop are unchanged, and the whole-round synchronous launch still lands every read in one tick. The round, rejection-containment, and no-evidence cadence guards all pass against the new code.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior pullfrog review of 4381a9a, the batched IPC half has been dropped entirely and the branch is now renderer-only (git diff origin/main is three files, no IPC/preload/wire/main surface):
- Removed the batch IPC surface —
pty:inspectProcessBatch, thePreloadApimethod,local-terminal-inspection-batch.ts,TerminalProcessInspectionBatchEntry,TERMINAL_PROCESS_INSPECTION_BATCH_LIMIT, theinspectOnePtyProcessextraction, and theruntime-terminal-inspection.tsrouting change are all gone. Every pane keeps its per-panepty:inspectProcessinvoke. - Renamed
batched→sharesHostObservation— the monitor now sets the flag fromisRemotePtyId(options.isRemotePtyId?.(ptyId) !== true), so local reads coalesce into a shared round and remote reads stay on the one-start-each path. - Reworked admission to a count —
canStartInspectionbecameavailableInspectionStarts(min(concurrency headroom, rate headroom)), andstartInspectionRoundlaunches a whole shared round synchronously on a single start, holding its slot until the last task settles. - Added
agent-process-inspection-round.test.ts— five guards, including the new remotepending-titleregression test.
The three findings traced end-to-end in the prior review (batch verdict integrity, per-entry incarnation threading, capability fallback) have no code to apply to after the split. The one non-blocking point about shared-round-vs-pending-title ordering still holds, and its reasoning is now backed by the new remote regression test. Queue accounting (concurrency slot bounded by MAX_CONCURRENT_INSPECTIONS, rate window bounded by MAX_INSPECTION_STARTS_PER_SECOND) stays consistent after a full pump, and the disposed-task compaction is correctly linear. The measured framing tradeoff is now stated plainly in the negative-trade-offs section rather than claimed as "None".
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior pullfrog review (265f6a7), one commit (ca019fb) landed:
- Coalesced the initial burst via a deduplicated microtask —
enqueueAgentProcessInspectionno longer pumps inline; it defers toqueueInspectionPump, aqueueMicrotaskguarded byinspectionPumpQueued, so a synchronous burst of enqueues forms one round instead of spending a start per pane until the concurrency slots fill and parking the remainder on the 100ms retry.resetAgentProcessInspectionQueueForTestsclears the new flag. - Strengthened the single-start guard — the "launches the whole round" test now enqueues 7 unshared panes alongside the 300 shared ones and asserts
[...launchesPerTick.values()] === [PANES]plusunshared === 7, which is what proves the shared round consumed exactly one of the eight starts (the 7 unshared still fit in the remaining budget) rather than the looser>= PANES - 4.
The microtask path is correct: a synchronous burst queues once, pumpInspectionQueue drains the shared round and up to the concurrency headroom of unshared tasks, then re-pumps via the existing 0ms/100ms timer path with the inspectionPumpTimer !== null guard still preventing double scheduling. The settle path and rate/concurrency accounting are unchanged, tasks are removed from the queue before run() so nothing double-runs, and the flag reset is safe because a stale pending microtask only re-enters an empty queue. queueMicrotask is already idiomatic across the renderer. I ran agent-process-inspection-round.test.ts, agent-process-inspection-queue-rejection-containment.test.ts, and agent-completion-no-evidence-cadence.test.ts — all pass.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
|
Cleared the two @coderabbitai pre-merge warnings:
|
|
Tip For best results, initiate chat on the files or code changes.
The PR description now satisfies the template. I withdraw the docstring-coverage concern. The stated 🐇 ✏️ Learnings added
You are interacting with an AI system. |
…tablyai#18438) The inspection rate limiter counted panes when it should have counted host observations. `MAX_INSPECTION_STARTS_PER_SECOND = 8` is global, and it was spent one pane at a time, so N due panes meant an effective per-pane period of max(tier, N/8 seconds) — ~37.5s at 300 panes for a pane the code polls at 750ms. Agent-completion latency degraded monotonically as panes were added. Every local pane's inspection resolves out of the same TTL-and-in-flight- deduped process-table capture, so a whole round of them is one host observation. The queue now drains all shared-observation tasks as one round on one start, launched in a single tick. Remote panes each cost their own execution-host round trip and stay admitted one at a time. Both the budget and the cadence tiers are numerically unchanged. Disposed tasks are also compacted out in one pass instead of a splice per drop, so the per-round predicate cost is linear rather than quadratic at pane scale. No IPC, preload, wire, or main-process change: each pane keeps its existing per-pane `pty:inspectProcess` invoke.

ELI5
Orca watches the programs running in your terminals so it can tell you when an agent
finishes. To avoid hammering the app it only allows itself 8 of those checks per second,
across every pane you have open — and it spent them one pane at a time.
That works fine with a handful of panes. With three hundred, 8 checks a second means each
individual pane gets looked at about once every forty seconds, even though the code believes
it is checking that pane every three quarters of a second. So the more agents you run, the
longer Orca takes to notice one has finished — exactly backwards from what you want.
But those checks were never eight separate things. Every local pane's answer is read out of
one snapshot of the machine's process list, which Orca already captures at most twice a
second no matter how many panes ask. Eight panes asking is one look at the process list; three
hundred panes asking is also one look at the process list.
So the allowance was counting the wrong thing. It now counts looks at the process list instead
of panes. The number stays at 8 per second; it just buys three hundred panes now instead of
eight.
Why it matters
The budget is a hard global cap:
MAX_INSPECTION_STARTS_PER_SECOND = 8insrc/renderer/src/components/terminal-pane/agent-process-inspection-queue.ts, shared byevery pane in the app.
750ms/ idle2000/ hidden3000/no-evidence
15000(agent-completion-poll-cadence.ts).A pane re-enqueues once per tier interval, and the queue was FIFO one-at-a-time, so the
effective per-pane period was
max(tier, N/8 seconds). Every pane past the eighth was servedat the cap's rate, not its tier's. The reporting session had 905 terminal leaves; at a few
hundred panes holding agent evidence the effective period is tens of seconds for a pane the
code polls at 750ms–3000ms. That is user-visible as "Orca didn't tell me my agent finished",
and it gets worse monotonically as panes are added.
Note on what this is not: it is not a subprocess-churn fix.
getProcessTableSnapshot()already sits behind a 500ms TTL plus in-flight dedupe (
src/shared/process-table-snapshot-reader.ts),on Windows too (
readWindowsProcessTable), so theps/process-table capture rate is ~2/sapp-wide regardless of pane count. That TTL is exactly what makes a whole round of local reads
one host observation, and it is what the limiter should have been counting all along.
What changed
Three renderer files. No IPC, preload, wire, or main-process change — each pane keeps its
existing per-pane
pty:inspectProcessinvoke.agent-process-inspection-queue.ts—canStartInspectionbecomesavailableInspectionStarts,the pump drains every
sharesHostObservationtask as one round on one start, and starts theround's tasks synchronously so they all land in the same tick (and therefore the same
process-table capture window).
enqueueAgentProcessInspectionnow pumps on a deduplicatedmicrotask instead of inline, so a synchronous burst of enqueues forms one round — pumping
inline spent a start per pane until the concurrency slots filled and then parked the rest of
the burst on the 100ms retry. Unshared tasks keep the old one-start-each admission and the
old
pending-titlepriority. Disposed tasks are compacted out in one pass instead of asplice per drop, so the per-round predicate cost is linear rather than quadratic at pane scale.
agent-completion-process-monitor.ts— setssharesHostObservationfromisRemotePtyId.agent-process-inspection-round.test.ts— new guard.An earlier revision of this PR also added a
pty:inspectProcessBatchIPC channel, a preloadmethod, a renderer coalescer, a batch entry type and a 256-entry chunk limit. That is dropped.
It carried all of the merge risk (new wire surface, per-entry guard threading, positional
short-reply semantics, capability fallback) and a failure mode that does not exist today — one
hung entry inside a
Promise.allchunk withholding up to 255 sibling results — while main'sbatch handler was itself only
Promise.allover the identical per-entry inspection. It savedinvoke framing and nothing else. The measurement section below prices that framing.
Measurement
Deterministic counter guard, no wall-clock assertion. 300 panes enqueue a cadence
inspection; advance fake timers 200ms (well inside one rate-limiter window) and count how
many panes were actually inspected.
Before (queue file reverted to
origin/main, everything else this branch):After:
Test Files 1 passed (1) / Tests 5 passed (5)Starts spent, same 200ms window, same 300 panes:
Verified at 50 / 300 / 800 panes:
inspected=50 bursts=[50],inspected=300 bursts=[300],inspected=800 bursts=[800]— one start, one tick, full coverage, at every scale.What dropping the batch IPC costs
The one thing the batch bought was invoke framing, so it was priced directly rather than
argued about. Real Electron 43 IPC, hidden
BrowserWindow,process.cpuUsage()deltassampled in both the renderer and the main process (this machine is loaded, so wall clock
is reported but not relied on), 60 rounds per measurement × 2 interleaved passes, 15 warmup
rounds. Both shapes run the identical per-entry host work behind a shared TTL'd snapshot, so
the only variable is framing. Harness:
/tmp/orcaperf/ipc-bench-18438.So the batch was worth ~10x on framing: ~30µs of aggregate CPU per pane-inspection versus
~3µs. Converted to steady state — inspections/s is
N / tier, identical under both shapes,because both inspect the whole round inside one window:
Three things put that in scale:
so the jank-relevant number is how long that burst holds the renderer main thread. It is
0.85 ms at p95 even when all 800 panes ride a single round — 5% of a 16.7 ms frame. The
batch's is 0.20 ms. No frame is dropped at any tested pane count; the rest of the cost is
completion handling spread across later ticks.
ps -axo pid=,ppid=,pgid=,tpgid=,stat=,tty=,lstart=,command=on this machine (1814processes, 453 KB) costs 254 ms CPU (30 ms user + 224 ms sys) in the forked
ps, plus2.3 ms in the main process to drain the pipe. The 500 ms TTL caps that at 2/s app-wide, so
the feature already spends ~510 ms CPU/s there at any pane count, on
maintoday. Theframing delta is 2.2% of that at 300 active panes and 5.6% at 800. (Both the numerator and
that denominator are inflated on this box — 1814 processes is ~3x a normal machine — and
they inflate together.)
panes inspected inside one rate-limiter window either way. The batch only ever made the
mechanism cheaper, never faster.
Against that, the batch cost a new IPC channel, a preload method with a capability fallback,
per-entry incarnation-guard threading, positional short-reply semantics, and a
Promise.allchunk in which one hung entry withholds up to 255 sibling results — a head-of-line stall that
does not exist when each pane owns its own invoke. Trading a few percent of one core for that
surface is the wrong side of the trade, so it is dropped. If the CPU ever becomes the binding
constraint, the batch is a self-contained follow-up that needs none of this PR undone.
For reference,
mainspends only ~0.24 ms CPU/s on inspection round trips at any N — becauseit only ever performs 8 of them per second, which is the bug.
Correctness
No wire, IPC, preload or main-process surface changes.
git diff origin/main --statisthree files, all under
src/renderer/src/components/terminal-pane/. Every pane still callsthe same
pty:inspectProcesswith the same arguments and gets the same reply; nothing aboutwhat is computed, by whom, or how it is transported changes. Only when the renderer is
allowed to issue the call changes.
Verdict integrity (
docs/reference/ssh-execution-boundary.md). Untouched. Each pane'sinspection is its own promise, its own rejection, and its own
classifyTerminalProcessInspectionFailure. There is no shared reply, no positional decoding,and no path by which one pane's outcome is derived from a sibling's. The vocabulary stays
live/unverifiable/exited.Per-pane incarnation guard.
expectedIncarnationIdis captured per request and recheckedafter the await (
getExpectedIncarnationId() === expectedIncarnationIdAtRequestinagent-completion-process-monitor.ts), exactly as before. The queue never inspects, reordersor merges a task's arguments — it only decides which tick a task's
run()is invoked on, andrun()closes over its own pane's state.Remote wire compatibility (
docs/reference/remote-wire-compatibility.md). No wire changeand no behavior change for remote panes.
shouldRunCadenceInspection()already returnsfalsefor remote PTY ids, so SSH and paired-runtime panes never cadence-poll; their only inspections
are pending-title ones, they are flagged
sharesHostObservation: false, and they keep theexisting one-start-each admission and the existing
pending-titlepriority. Old and newclients are indistinguishable on the wire. The remote plan stays the one already recorded in
agent-process-inspection-cost.ts— consume the batched foreground evidence hosts alreadypublish on the
pty.listProcessesinventory (#17525) — which this PR does not pre-empt.src/main/providers/ssh-pty-provider-rpc-operations.tsis untouched.Does the 8/s cap still need to exist? Yes, and it is numerically unchanged. What was wrong
was its unit: it counted panes when it should have counted host observations. A start is now
one observation — a shared round costs one however many panes ride it, an unshared (remote)
task costs one each.
MAX_CONCURRENT_INSPECTIONS = 4is likewise unchanged and now boundsconcurrent rounds. A round holds its concurrency slot until its last task settles, so a slow
pane cannot let the next round overlap it.
Priority ordering. The shared round drains before the unshared
pending-titlescan withinthe same pump. This is not starvation: a local pending-title read is itself
sharesHostObservation: trueand rides that very round, and a remote pending-title read isserved in the same pump from the 7 remaining starts. Raised (non-blocking) by pullfrog; the
split does not change the ordering it reviewed, and the reasoning still holds.
Edge cases. SSH and paired-runtime panes: unchanged path, unchanged admission, still
hidden/no-cadence. Folder workspaces: this path keys on PTY id only and never touchesworktree state. Windows:
isAgentProcessInspectionCostlyand theno-evidence15s tier areuntouched,
readWindowsProcessTablehas the same TTL+dedupe that makes the round shared, andthe existing
agent-completion-no-evidence-cadence.test.tsguard still passes. Empty queue,all-disposed queue, and clock-jump-backwards (
now < inspectionStarts[0]) all keep theirexisting handling.
Negative user-facing trade-offs
None user-facing. Every pane still gets the same inspection, computed by the same code,
transported the same way, with the same guard; both the admission budget and the cadence tiers
are numerically unchanged, and no pane waits on a window its own cadence did not already
contain.
One measured non-user-facing cost, stated plainly: because each pane keeps its own invoke,
this spends ~10x the IPC framing CPU that the dropped batch revision would have — ~30µs of
aggregate renderer+main CPU per pane-inspection instead of ~3µs. In absolute terms that is
+11.4 ms CPU/s at 300 panes all holding a live agent, and +28.6 ms/s at an adversarial 800
(1.3% and 3.2% of one core, split across two processes). It does not cost a frame — the
synchronous launch burst is 0.85 ms at p95 with all 800 panes in one round — and it is a few
percent of the ~510 ms CPU/s the process-table captures behind this feature already cost on
mainat any pane count. It is a real number and it is the price of not adding the batch'swire surface and head-of-line-stall failure mode. Table and method above.
Linked Issue
N/A — no tracking issue. This came out of a performance session on a report with 905 terminal
leaves, where the per-pane cadence tiers were silently degrading to
max(tier, N/8 seconds).Visual Proof
N/A— nothing rendered changes. There is no new UI, no layout, color, typography or componentchange, and no new interaction: the same inspection runs for the same panes with the same tiers
and the same global budget. The only observable difference is when an existing completion
signal fires at high pane counts, which is not screenshottable at 300 panes; it is pinned instead
by the deterministic counters in
agent-process-inspection-round.test.ts(
inspected=300,bursts=[300], red onorigin/mainwithexpected 8 to be 300) and by theCPU-delta table under Measurement.
Test plan
nice -n 10 node_modules/.bin/vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane src/renderer/src/runtime— 596 files / 5474 tests pass (and 667 files / 6141 withsrc/main/ipc/ptyadded).... src/main/ipc/pty-session-liveness-and-ownership.test.ts src/main/providers— 85 files / 868 tests pass.agent-process-inspection-round.test.ts: 300-pane coverage guard (red proof above),single-tick round launch on a single start (with 7 unshared panes still fitting in the same
window, which is what proves the round cost one start), remote panes still admitted one start
at a time at N=300,
pending-titlestill prioritized, disposed panes dropped out of the round.tsc --noEmit -p config/tsconfig.node.jsonand-p config/tsconfig.tc.web.jsonboth clean.pnpm run check:code-quality:changed— 0 new findings across 3 changed files.oxfmt --write+oxlintclean on all changed files.