Repository navigation
fix(record): warm the MP4 validator without blocking capture - #4248
Conversation
Size Report
Startup median (7 runs, lower is better):
|
|
At cf964a3, I found two problems to fix before merge. All 18 checks pass; I did not run the tests myself. On a Linux, WSL or other non-macOS daemon host, every MP4 A transient compile failure now becomes permanent. The Map keeps the wrapped promise whatever the outcome, and the Could this be simpler? Keep the Map-of-promises core, drop a rejected promise, and compute Not blocking, and you can take or leave these: the warm compile at video.ts:51 has no signal or owner, so if the daemon exits mid-compile, For validation, the live iPhone Air run covers the Apple simulator route only, and the shared |
There was a problem hiding this comment.
All reported issues were addressed across 8 files
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
Apex SentinelVerdict: Since the last review: 2 new · 0 still open · 0 no longer raised 2 finding(s): 2 minor. 🟡 Minor (2)
2 findings below Reviewed with this repository's own Sentinel configuration ( Reply under an inline comment with Earlier reviewsReview of
|
| Where | Finding |
|---|---|
packages/capture-kit/src/recording/video.ts:146 |
Compiler stderr never reaches the validator-unavailable details, so the xcrun hint is dead |
Reviewed with this repository's own Sentinel configuration (e6ac165961de).
Review of 823b8d1d · 05.11.2026 20:38 UTC
Apex Sentinel
Verdict: approve
Since the last review: 3 new · 0 still open · 2 no longer raised
3 finding(s): 3 minor.
🟡 Minor (3)
| Where | Finding |
|---|---|
packages/capture-kit/src/recording/video.ts:188 |
🆕 Validator stderr is dropped before classification, so the compile-error regex is dead |
src/platform-runtime-screen-recording-host.ts:26 |
🆕 record start spawns swiftc on non-Apple backends and on hosts without xcrun |
packages/capture-kit/src/recording/video.ts:46 |
🆕 Background validator compile is no longer cancellable, so a daemon exit orphans swiftc and its cache lock |
1 finding below minor not shown (user_settings.min_severity).
Reviewed with this repository's own Sentinel configuration (e6ac165961de).
Review of cf964a31 · 05.11.2026 20:08 UTC
Apex Sentinel
Verdict: request_changes
2 finding(s): 1 major, 1 minor.
Required fixes
- One transient compile failure disables MP4 verification for the daemon's lifetime
🟠 Major (1)
| Where | Finding |
|---|---|
packages/capture-kit/src/recording/video.ts:43 |
One transient compile failure disables MP4 verification for the daemon's lifetime |
🟡 Minor (1)
| Where | Finding |
|---|---|
packages/capture-kit/src/recording/video.ts:134 |
record stop fails on daemon hosts without xcrun, where it used to succeed |
Reviewed with this repository's own Sentinel configuration (e6ac165961de).
There was a problem hiding this comment.
Apex Sentinel: 2 new findings on cf964a3. The verdict and the full list are in the summary comment.
| const videoValidatorCompiles = new Map<string, Promise<string>>(); | ||
|
|
||
| // A compiled helper is valid for the Swift cache it was built into, so each cache root keeps its own. | ||
| // Callers share one compile, and each waits for it only within its own budget. The last caller to | ||
| // stop waiting cancels an unfinished compile: that kills this process's `swiftc`, or stops waiting | ||
| // for another process's compile. Nothing owns `swiftc` once its requests end, so a daemon that | ||
| // exits would leave it running. | ||
| const videoValidatorCompiles = new Map<string, SharedValidatorCompile>(); | ||
| /** Starts one cached compile per Swift cache root. Callers may warm it without awaiting it. */ | ||
| export function prepareVideoValidator(host: HostEnvironment): Promise<string> { | ||
| const cacheRoot = getSwiftCacheRoot(host.env); | ||
| let compiled = videoValidatorCompiles.get(cacheRoot); | ||
| if (!compiled) { | ||
| compiled = compileSwiftSourceText({ | ||
| host, | ||
| source: VIDEO_VALIDATION_SCRIPT, | ||
| cacheName: 'video-validator', | ||
| }).catch((error: unknown) => { | ||
| throw unavailableVideoValidator(error); | ||
| }); | ||
| videoValidatorCompiles.set(cacheRoot, compiled); | ||
| } | ||
| return compiled; | ||
| } |
There was a problem hiding this comment.
No longer raised on 823b8d1: the code it pointed at changed.
🟠 Major · One transient compile failure disables MP4 verification for the daemon's lifetime
A transient failure in ensureSwiftExecutable (Swift cache lock held by another process, or xcrun swiftc hitting its 120 s timeout) permanently disables verification: the rejected promise is cached in videoValidatorCompiles under that cache root and never removed, so every subsequent call rejects with video_validator_unavailable and retriable: false for the daemon's lifetime. The predecessor deleted the failed entry so the next caller retried; the new test locks in the no-retry behavior.
| const videoValidatorCompiles = new Map<string, Promise<string>>(); | |
| // A compiled helper is valid for the Swift cache it was built into, so each cache root keeps its own. | |
| // Callers share one compile, and each waits for it only within its own budget. The last caller to | |
| // stop waiting cancels an unfinished compile: that kills this process's `swiftc`, or stops waiting | |
| // for another process's compile. Nothing owns `swiftc` once its requests end, so a daemon that | |
| // exits would leave it running. | |
| const videoValidatorCompiles = new Map<string, SharedValidatorCompile>(); | |
| /** Starts one cached compile per Swift cache root. Callers may warm it without awaiting it. */ | |
| export function prepareVideoValidator(host: HostEnvironment): Promise<string> { | |
| const cacheRoot = getSwiftCacheRoot(host.env); | |
| let compiled = videoValidatorCompiles.get(cacheRoot); | |
| if (!compiled) { | |
| compiled = compileSwiftSourceText({ | |
| host, | |
| source: VIDEO_VALIDATION_SCRIPT, | |
| cacheName: 'video-validator', | |
| }).catch((error: unknown) => { | |
| throw unavailableVideoValidator(error); | |
| }); | |
| videoValidatorCompiles.set(cacheRoot, compiled); | |
| } | |
| return compiled; | |
| } | |
| const videoValidatorCompiles = new Map<string, Promise<string>>(); | |
| /** Starts one cached compile per Swift cache root. Callers may warm it without awaiting it. */ | |
| export function prepareVideoValidator(host: HostEnvironment): Promise<string> { | |
| const cacheRoot = getSwiftCacheRoot(host.env); | |
| let compiled = videoValidatorCompiles.get(cacheRoot); | |
| if (!compiled) { | |
| compiled = compileSwiftSourceText({ | |
| host, | |
| source: VIDEO_VALIDATION_SCRIPT, | |
| cacheName: 'video-validator', | |
| }).catch((error: unknown) => { | |
| // Only a toolchain that cannot provide AVFoundation is permanently unavailable; a lock or | |
| // compile timeout must let the next caller try again. | |
| if (videoValidatorCompiles.get(cacheRoot) === compiled) videoValidatorCompiles.delete(cacheRoot); | |
| throw unavailableVideoValidator(error); | |
| }); | |
| videoValidatorCompiles.set(cacheRoot, compiled); | |
| } | |
| return compiled; | |
| } |
Suggested change
Drop the map entry when the failure is transient (anything not isSwiftVideoValidatorError), re-check isExecutableFile/the cache before refusing so an executable another process published later is usable, keep unconditional refusal only for a confirmed missing toolchain, and consider retriable: true for the transient class.
Details
Also raised here
Also raised at this location by persona maintainability: A transient validator compile failure poisons the cache for the whole daemon lifetime — If the one compile started at record start fails for a transient reason — the daemon under load hits the xcrun timeout, the Swift cache lock is held by a sibling process, an I/O hiccup — every later record stop in that daemon rejects with video_validator_unavailable, retriable: false, and the hint "Install the Swift toolchain with AVFoundation", which is wrong advice for a timeout and never clears short of a daemon restart. The rejected promise is what gets stored in videoValidatorCompiles, so the map caches the failure permanently; the predecessor deleted the entry on rejection (if (videoValidatorCompiles.get(cacheRoot) === shared) videoValidatorCompiles.delete(cacheRoot)) so the next caller recompiled. Keep the entry only on success, or store a wrapper that clears the key on rejection, so a retriable cause is retried at the next stop.
persona: senior-developer, javascript, maintainability
Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.
Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.
| if (isSwiftVideoValidatorUnavailable(result.stderr, result.stdout)) { | ||
| throw unavailableVideoValidator(new AppError('COMMAND_FAILED', result.stderr)); | ||
| } | ||
| return false; | ||
| } catch (error) { | ||
| if (isSwiftVideoValidatorError(error)) { | ||
| return true; | ||
| throw unavailableVideoValidator(error); | ||
| } | ||
| throw error; | ||
| } |
There was a problem hiding this comment.
No longer raised on 823b8d1, with the code it pointed at unchanged: withdrawn, not fixed.
🟡 Minor · record stop fails on daemon hosts without xcrun, where it used to succeed
record stop for an MP4 recording on a host without xcrun (Linux, Windows, or macOS with only Command Line Tools) now fails with COMMAND_FAILED / details.reason: video_validator_unavailable, retriable: false; previously a missing validator meant "trust the container sniff" and the recording finalized. isPlayableVideo runs runCmd('xcrun', ...) to compile the validator, gets TOOL_MISSING, and throws instead of returning true as the predecessor's isSwiftVideoValidatorError branch did. The failure is cached per cache root, so no retry clears it without a daemon restart.
Suggested change
Decide whether a host with no Swift toolchain may still finalize recordings: if yes, treat TOOL_MISSING as "accept the container sniff" as before and reserve video_validator_unavailable for a toolchain that exists but fails; either way state the decision in website/docs/docs/commands.md.
Details
Also raised here
Also raised at this location by persona senior-developer: Missing xcrun now hard-fails Android/HarmonyOS record stop instead of finalizing — record stop on an Android or HarmonyOS recording fails with non-retriable video_validator_unavailable on any host without a working Swift toolchain — e.g. a Linux daemon, the common setup for ADB recording. xcrun is absent there, so the compile throws TOOL_MISSING, isSwiftVideoValidatorError matches it, and isPlayableVideo now throws where it previously returned true; the Android pull path (src/platform-runtime-screen-recording-android-host.ts:112) and the finalizer propagate that error, so the MP4 the device already produced is never reported. The hint then tells the operator to install Swift with AVFoundation on the host, which is not how an Android recording is verified. The error should stay scoped to hosts that actually need the MP4 validator, or the WebM/absent-toolchain case must keep its old verdict.
persona: javascript, senior-developer
Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.
Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.
|
Review round at 823b8d1: evicted rejected compiles, preserved main's missing-xcrun completion, added TOOL_MISSING/warm-order coverage, and moved/narrowed recovery docs. Round: +100/−54 across 6 files; PR: +332/−87 across 8 files. The same compatibility test passes on main e6ac165 and head. All 22 focused tests and the exact-head affected gate pass ( |
There was a problem hiding this comment.
Apex Sentinel: 3 new findings on 823b8d1. The verdict and the full list are in the summary comment.
| prepare: async (outputPath) => { | ||
| if (!outputPath.toLowerCase().endsWith('.webm')) { | ||
| const { prepareVideoValidator } = await import('@agent-device/capture-kit/recording-video'); | ||
| void prepareVideoValidator(host).catch(() => {}); | ||
| } | ||
| const { createScreenRecordingOutputHost } = | ||
| await import('./platform-runtime-screen-recording-output-host.ts'); | ||
| await createScreenRecordingOutputHost().prepare(outputPath); |
There was a problem hiding this comment.
No longer raised on 666fbf1, with the code it pointed at unchanged: withdrawn, not fixed.
🆕 🟡 Minor · record start spawns swiftc on non-Apple backends and on hosts without xcrun
record start clip.mp4 on Android, Web or other non-Apple backends calls prepareVideoValidator on the host, which on macOS launches a ~30s swiftc compile during recording prep and on Linux/CI spawns a command that fails with ENOENT on every start (repeats because the failure is evicted from the map). outputs.prepare is the shared hook for every backend, but only Apple's finalize path and Android's pull probe need the validator.
Suggested change
Warm the validator only for the Apple backend, instead of for every non-.webm output path prepared by any backend.
Details
Also raised here
Also raised at this location by persona javascript: Validator warm-up spawns xcrun and creates Swift cache dirs on hosts that have no Apple toolchain — The new warm-up fires for every non-.webm output before the backend is known, so an Android, HarmonyOS or Limrun recording on a Linux daemon host runs getSwiftCacheRoot plus buildSwiftToolEnv (two mkdirSync calls creating home/ and module-cache/ under the cache root, via swift-cache.ts:81-82) and then spawns xcrun, whose ENOENT is dropped by the detached .catch(() => {}). Nothing observable is wrong today, but the work is unconditional for backends and hosts where the Apple validator is not what produced the file, and the spawned-process and directory side effects are silent. Consider gating on the platform (darwin) or letting the finalize path, which already knows the container, start the compile.
persona: senior-developer, javascript
Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.
Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.
| export function prepareVideoValidator(host: HostEnvironment): Promise<string> { | ||
| const cacheRoot = getSwiftCacheRoot(host.env); | ||
| let compiled = videoValidatorCompiles.get(cacheRoot); | ||
| if (!compiled) { | ||
| compiled = compileSwiftSourceText({ | ||
| host, | ||
| source: VIDEO_VALIDATION_SCRIPT, | ||
| cacheName: 'video-validator', | ||
| }).catch((error: unknown) => { | ||
| if (videoValidatorCompiles.get(cacheRoot) === compiled) | ||
| videoValidatorCompiles.delete(cacheRoot); | ||
| throw unavailableVideoValidator(error); | ||
| }); | ||
| videoValidatorCompiles.set(cacheRoot, compiled); | ||
| } | ||
| return compiled; | ||
| } |
There was a problem hiding this comment.
No longer raised on 666fbf1, with the code it pointed at unchanged: withdrawn, not fixed.
🆕 🟡 Minor · Background validator compile is no longer cancellable, so a daemon exit orphans swiftc and its cache lock
prepareVideoValidator fires a detached compile with no signal or owner (compileSwiftSourceText called without timeoutMs, falling back to the 120s default). A daemon exit during warm-up orphans swiftc holding <executablePath>.lock; the next daemon's warm-up waits on that lock and can report a non-retriable video_validator_unavailable caused by a dead process.
Suggested change
Keep an owner for the background compile (a daemon-lifetime AbortController passed as signal, or an unref'd bound timeout), or state in CLI text/docs that a daemon exiting mid-warm-up leaves swiftc running.
Details
persona: javascript
Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.
Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.
|
At 823b8d1, the code looks good, and both problems from the earlier review are fixed: a missing validator tool now falls back to the container-only completion main uses, so non-macOS hosts no longer fail every MP4 Not blocking, take or leave: the provider-scenario harness at The open inline threads on rejected-compile eviction and TOOL_MISSING handling no longer apply at this head and can be resolved. |
|
Pushed
|
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
There was a problem hiding this comment.
Apex Sentinel: 1 finding moved to a new line on 666fbf1. The verdict and the full list are in the summary comment.
| if (isSwiftVideoValidatorUnavailable(result.stderr, result.stdout)) { | ||
| throw unavailableVideoValidator(new AppError('COMMAND_FAILED', result.stderr)); | ||
| } |
There was a problem hiding this comment.
No longer raised on ab5a5e5: the code it pointed at changed.
🟡 Minor · Compiler stderr never reaches the validator-unavailable details, so the xcrun hint is dead
Validator writes no such module 'AVFoundation' on stderr; record stop returns video_validator_unavailable with the generic repair hint and no stderr in details. unavailableVideoValidator reads cause.details?.stderr to pick the hint, but the only caller throws new AppError('COMMAND_FAILED', result.stderr) with details undefined, so that branch is unreachable for a run failure. The unavailable.mp4 tests assert only reason and retriable, which is why coverage misses it.
| if (isSwiftVideoValidatorUnavailable(result.stderr, result.stdout)) { | |
| throw unavailableVideoValidator(new AppError('COMMAND_FAILED', result.stderr)); | |
| } | |
| if (isSwiftVideoValidatorUnavailable(result.stderr, result.stdout)) { | |
| throw unavailableVideoValidator( | |
| new AppError('COMMAND_FAILED', result.stderr, { | |
| stderr: result.stderr, | |
| stdout: result.stdout, | |
| exitCode: result.exitCode, | |
| }), | |
| ); | |
| } |
Details
Also raised here
Also raised at this location by persona maintainability: Validator run failure loses stderr in details, so the AVFoundation hint is never used — When the compiled validator itself runs and reports no such module 'AVFoundation' on stderr, the user gets the generic "Repair the Swift toolchain or validator executable" hint and no details.stderr, even though the code just classified it as an AVFoundation/xcrun problem. Pass stderr/stdout/exitCode as details of the thrown AppError. This is also why none of the existing tests catch it — the unavailable.mp4 cases assert only reason and retriable, not hint or stderr, unlike the compiler-diagnostics case which does assert both.
Also raised at this location by persona senior-developer: Validator's own stderr is passed as the message, so the AVFoundation hint never fires — The cause is that line 147 puts the validator's stderr into the AppError message (new AppError('COMMAND_FAILED', result.stderr)) and not into details.stderr, so the classifier at lines 200-203, which only reads details?.stderr/details?.stdout, cannot see it and picks the other branch. The compile-failure path does populate details.stderr (the new preserves compiler stderr test covers it), which is why the hint is right there and wrong here. Pass the diagnostics as details so both paths classify identically.
persona: maintainability, senior-developer
Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.
Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.
|
At 666fbf1, the code still looks good and the review at 823b8d1 stands. The new commit only carries compiler stderr/stdout, exit details and recovery hints through the typed validator error, and the stderr hint is now tested in The inline threads on non-macOS warm-up, the detached compile on daemon exit and the stderr in the cause message still stand but need no change before merge. The threads on rejected-compile eviction, the TOOL_MISSING fallback and the forwarded stderr hint are fixed and can be resolved. |
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
There was a problem hiding this comment.
Apex Sentinel: 2 new findings on 9dd99cf. The verdict and the full list are in the summary comment.
| return new AppError( | ||
| 'COMMAND_FAILED', | ||
| 'The video validator is unavailable; MP4 playability could not be verified.', | ||
| { | ||
| ...details, | ||
| reason: 'video_validator_unavailable', | ||
| retriable: false, | ||
| hint: | ||
| details?.hint ?? | ||
| (isSwiftVideoValidatorUnavailable(details?.stderr ?? '', details?.stdout ?? '') | ||
| ? 'Ensure Swift and AVFoundation are available on the daemon host, then retry record stop.' | ||
| : 'Repair the Swift toolchain or validator executable on the daemon host, then retry record stop.'), | ||
| }, | ||
| cause, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🆕 🟡 Minor · video_validator_unavailable is reported non-retriable while docs and hint say to retry record stop
A compile failure (e.g. xcrun can't find swiftc due to a wrong DEVELOPER_DIR, or the 120s timeout) returns details.retriable:false, but the docs bullet added here and the generated hint both say "Repair the reported failure and retry record stop", and prepareVideoValidator evicts the rejected compile so the next attempt re-runs it. A client honouring retriable:false will therefore not retry a transient failure the code path supports retrying. Cause: unavailableVideoValidator hardcodes retriable:false for every cause without distinguishing transient compile failures from a permanently broken validator executable.
Suggested change
Derive retriable from the cause—true for a compile failure or timeout that the next attempt re-runs, false only for a validator executable that cannot work—and make the docs and hint agree with that signal.
Details
Also raised here
Also raised at this location by persona maintainability: A retriable compile failure is reported as non-retriable, contradicting the retry guidance — A transient compile failure (swiftc killed by OOM, a cache-lock contention failure) is reported as retriable: false, yet the same change evicts the rejected compile so the next attempt recompiles, and the new cliDetail and commands.md both say "Repair the reported failure and retry record stop; a failed compile is retried on the next start or stop". unavailableVideoValidator hard-codes retriable: false for every cause, so a client that honours details.retriable refuses the retry the docs instruct, and the flag contradicts the author's own guidance for the compile-failure case. Carry the cause's retriability through (or mark compile failures retriable) instead of forcing false.
persona: javascript, senior-developer, maintainability
Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.
Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.
| if (isSwiftVideoValidatorUnavailable(result.stderr, result.stdout)) { | ||
| throw unavailableVideoValidator( | ||
| new AppError('COMMAND_FAILED', result.stderr, execFailureDetails(result)), | ||
| ); | ||
| } | ||
| return false; | ||
| } catch (error) { | ||
| if (isSwiftVideoValidatorError(error)) { | ||
| return true; | ||
| throw unavailableVideoValidator(error); | ||
| } | ||
| throw error; | ||
| } |
There was a problem hiding this comment.
🆕 🟡 Minor · Unplayable recording is now reported as non-retriable validator-unavailable after one validator refusal
A corrupt MP4 (ftyp+moov present but unplayable) whose validator exits non-zero with an 'unavailable' string (e.g. stderr containing xcrun: error: unable to find utility "swiftc") fails record stop with retriable:false / reason video_validator_unavailable instead of the retriable RECORDING_OUTPUT_UNPLAYABLE_REASON. isPlayableVideo now throws out of the exitCode !== 0 branch (video.ts:146-151), and finalizeScreenRecording (platform-runtime-screen-recording-finalizer-host.ts:38-40) has no catch for it, so the unplayable-recording refusal is never reached.
Suggested change
Either classify a non-zero validator exit as an unplayable recording and keep the throw for launch-time failures only, or catch video_validator_unavailable in finalizeScreenRecording and re-raise it with retriable:true.
Details
persona: javascript
Reply @apex-sentinel-staging note ... to add context, or @apex-sentinel-staging pivot ... if this is wrong — I will re-check the cited code.
Useful? Reply @apex-sentinel-staging like, or @apex-sentinel-staging dislike <why> if it is not.
|
I found no problems in the code at 9dd99cf, and the earlier clean review at 823b8d1 still holds. The validator failure now passes its stderr and stdout as error details, so the hint classifier can read them, and the unavailable cases in All 19 checks pass at this commit, and there are no conflicts. The live iPhone Air check at 823b8d1 still covers this head, because the delta only changes error details on the unavailable path. Nothing blocks merge. Of the open threads, r4486721790 (retriable on a compile timeout) still applies but is optional: either keep compile timeouts retriable or reword the docs line. r4486721798 (unplayable versus unavailable) does not apply, because a corrupt MP4 still returns false and raises the unplayable error; only missing-toolchain output throws. The other open threads are fixed or harmless and can be resolved. |
Summary
Warm one MP4 validator compile per Swift cache root before output preparation without delaying capture. Stop shares it within its finalize budget without cancellation. Evict rejected compiles for later retries. Preserve main's container-only completion without xcrun. Retain compile/runtime diagnostics and recovery hints through typed failures; normalized process-exit messages include stderr excerpts. Generic Swift-cache locking is unchanged.
8 files, +413/−89 (net +324). Final round: 2 files, +24/−12; one details-factory call replaces the handwritten process-exit details. No lifecycle expansion. Replaces #4188.
Validation
Head
9dd99cfb2be5a3712aa95adaaab1b1f845138d5b; merge base22bd671066.pnpm check:affected --run --head 9dd99cfb2be5a3712aa95adaaab1b1f845138d5bpassed, receiptrun-8262HV,exactCommit: true, unchanged checkout. 347 related and 17,380 unit tests passed; one unit skipped. Both daemon-registration cases and the WebDriver timing assertion passed this round.e6ac165961and earlier head823b8d1d01.823b8d1d01, required iPhone Air: cold start 2.237s; verified early stop 10.435s/70s; warm stop 0.794s. Simulator/daemon shut down; isolated cleanup complete.