Repository navigation
Await process snapshots on the read-only status path - #40
Merged
euforic merged 2 commits intoOct 30, 2026
Merged
Conversation
Desktop status refreshes blocked a launcher actor's cooperative thread for up to 3 s per `ps` call. The read-only status reads (`statuses`, `statusBatch`, `stockStatus`, and Claude's `status(for:appURL:)`) now await an async snapshot, so the wait suspends. `CodexProcessSnapshotProviding` gains `suspendingProcessSnapshot()`, whose default calls the synchronous snapshot, so existing synchronous conformers still conform. The system Codex and Claude providers implement it with the async `BoundedSubprocess.run` and share one set of `ps` arguments, timeout, and output bound with their synchronous reads. Open, close, and focus keep synchronous snapshots and gain no suspension point. Official Codex open/close use a private synchronous stock status. Both actors hold only immutable state. Status reads derive results from the snapshot alone (Codex), or run every helper and main-process identity check after it returns with no suspension between them (Claude). Status results never authorize a lifecycle action. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…evisions The runner's baseline branch calls the older per-provider status APIs, so its status rows cannot compare two revisions that both have `statusBatch`. `AGENTDOCK_BENCHMARK_BASELINE_DEFINE=CANDIDATE` opts a baseline into the candidate's calls; the default is unchanged. Record the installed-status comparison for suspending snapshots: no regression, same `ps` launches and validator entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Stacked on #38 (
euforic/wonderful-mcnulty-8b981e); it needs that PR's asyncBoundedSubprocess.runoverload. Retarget tomainafter #38 merges.What changes
The status refresh no longer blocks a cooperative thread for up to 3 s per
pscall. The read-only status methods on both launcher actors now await an async snapshot, so thepswait suspends:CodexInstanceController:statuses,statusBatch,stockStatusClaudeInstanceController:statuses,statusBatch,stockStatus, andstatus(for:appURL:), which wrapsstatusesCodexProcessSnapshotProvidinggainssuspendingProcessSnapshot() async throws. A protocol extension provides a default that calls the synchronousprocessSnapshot(), so existing synchronous conformers (FixedProcessSnapshotProvider,SequencedProcessSnapshotProvider) conform unchanged.SystemCodexProcessSnapshotProviderandSystemClaudeProcessSnapshotProviderimplement the async read with the asyncBoundedSubprocess.run. Both overloads share one set ofpsarguments, the 3 s timeout, the 8 MiB output bound and the result checks.The new requirement has a distinct name rather than an
asyncoverload ofprocessSnapshot(). In an async context Swift resolves to the async overload, soopenandcloseProcesses, which are async functions that read the snapshot directly, would have been forced ontoawait.Reentrancy review
Open, close and focus keep their synchronous snapshots: Codex
open,closeProcesses,status(configuration:), andopenStock/closeStock. The last two now call a private synchronouslifecycleStockStatus, because the publicstockStatusis async. Claude's open and close flows useverifiedStatus,verifiedStockStatus,activateVerified*andisVerifiedProfileProcess, and those stay synchronous too. None of these flows gains a suspension point. The code between a lifecycle snapshot and the focus, terminate or signal that acts on it still runs without other actor work in between.The new await is the only new interleaving, and it lets other actor work run while a status read waits on
ps:letproperties, so an interleaving cannot corrupt anything.isRunningfrom them, andAgentDockModel.refreshInstanceStatusesalready re-reads when a lifecycle action happens during a batch. Every open, close or focus takes its own synchronous snapshot and re-verifies identity.The one effect: a status read can resume a little later when the actor is busy, for example with a lifecycle step's blocking
ps, so it reflects a slightly older snapshot. That is display staleness, the same as a read finishing just before a close. It does not weaken any check.Cancellation checks before and after each snapshot stay where they were. A cancelled async
psstill terminates its process group and surfaces asprocessInspectionUnavailable(Codex) orBoundedSubprocessError.timedOut(Claude), as the polled synchronous wait did.DesktopInstanceController's per-provider failure isolation is unchanged.Tests
testConcurrentStatusBatchesDoNotHoldCooperativeThreads: 3 × core-countstatusBatchcalls on separate controllers. The snapshot provider runs a real child process that prints a synthetic snapshot after one second. The calls finish in about 1.06 s and resolve the expected managed (222) and official (111) PIDs.testStatusReadsOnOneControllerOverlapWhileWaitingForSnapshots:statusBatch,statusesandstockStatuson one controller overlap, finishing in about 1.02 s with matching results.testSuspendingSystemSnapshotsListCurrentProcess: the real asyncpsreads for both providers include the test process, and Claude's parsed parent PID matches.I checked that the first two tests catch the regression: with the read paths pointed back at the blocking
processSnapshot(), both failed at about 3.06 s and 3.04 s against a 2.5 s limit.swift test: passes. AgentDockCoreTests ran 340 tests (17 skipped), AgentDockAppTests 63 (3 skipped), TranscriptRendererTests 16, with 0 failures.Benchmark
AGENTDOCK_BENCHMARK_BASELINE_DEFINE=CANDIDATE ./script/benchmark_resources.sh df0ad30 <out>. The new opt-in makes the #38 baseline run the samestatusBatchcall; by default the baseline uses the older per-provider APIs, which would measure a different change. Both apps are installed and signed; nine installed-status samples per revision:pslaunches, validator entriesAn earlier run with the same flags on a less loaded machine gave 421.81 → 334.47 ms. No regression; I'm not claiming a sequential latency improvement. Results are recorded in
docs/resource-usage-benchmarks.md.🤖 Generated with Claude Code