Skip to content

fix: prepare video validation without delaying capture - #4188

Open
thymikee wants to merge 28 commits into
mainfrom
fix/prepared-video-validator
Open

thymikee wants to merge 28 commits into
mainfrom
fix/prepared-video-validator

Conversation

@thymikee

@thymikee thymikee commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Summary

MP4 validator preparation runs in the background without delaying capture. Stop uses cached or started preparation without canceling it at its deadline. macOS uses AVFoundation; other hosts validate the container. Missing authority returns typed errors; recover with agent-device prepare ios-runner --platform ios on the recording host using the same Swift cache, then retry stop in the original session.

Shutdown cancels and awaits owned unfinished compilation and cleanup; published executables remain reusable. A second same-cache owner can continue after the first closes. Cleanup failures remain typed. Non-macOS touch visualization returns raw video and telemetry with overlayWarning. Launch waits are bounded; Android smoke uses settled Back and confirmed fill. Follow-up to #4150.

Scope: lifecycle, recording, tests and docs; 56 files, +2,136/-427 against c8ad8fb1257787ce0973cf74271cb9a4fafb51d9. The final f026759..b1150a8 change is test-only: bounded close-stall cleanup and regression coverage; no runtime behavior changes.

Validation

At b1150a8715055777d9f9788c521f4374cd313c19, pnpm check:affected --run --head b1150a8715055777d9f9788c521f4374cd313c19 passed (exact commit, unchanged checkout): 13 passed, 8 skipped, 1 deduplicated; 1,230 related and 17,188 unit tests passed, 1 unit skipped. See the evidence comment for the regression proof and limits.

Hosted coverage and device checks remain pending at this head. The startup CI increase at 7ff did not reproduce in bounded macOS base/head/base comparisons; its cause remains unproven.

@vercel

vercel Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agent-device-website Ready Ready Preview Nov 3, 2026 10:54pm UTC

Request Review

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.71 MB 5.71 MB +5.0 kB
Package (unpacked) 5.71 MB 5.71 MB +5.0 kB
Package (download) 1.74 MB 1.74 MB +1.5 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 54.6 ms 55.5 ms +0.9 ms
CLI --help 105.8 ms 84.3 ms -21.5 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 40 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@thymikee
thymikee force-pushed the fix/prepared-video-validator branch from f989dc2 to 7e440f1 Compare November 3, 2026 12:40
@thymikee thymikee changed the title fix: prepare the video validator before recording fix: prepare video validation without delaying capture Nov 3, 2026
@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Updated at 7e440f1 and rebased onto c8ad8fb; range-diff preserves both commits. Warming is best-effort and non-blocking. Stop uses a deadline race without cancelling the single cached compile; unavailable tooling is reported only for MP4 verification.

The local affected gate passed (exactCommit=true, run-QoD9cv), including 17,147 unit tests. The full provider lane passed all 245 tests with unchanged assertions, and ten regression mutations were caught. Cold Air run: start 4.21s with compilation pending, compile 17.87s, early stop 16.92s; later/warm stops 1.09s/0.96s. Daemon and simulator are off, pgrep is empty, and isolated caches are removed.

Size: 41 files, +592/-328 (net +264; production +61). CI is pending; the parent is handling watches.

@apex-sentinel-staging

apex-sentinel-staging Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Apex Sentinel

Verdict: approve

Since the last review: 2 new · 5 still open · 2 no longer raised

7 finding(s): 1 minor, 6 nit. Verdict: approve.

🟡 Minor (1)

Where Finding
src/platform-runtime-screen-recording-android-host.ts:16 Android/Harmony MP4 stops on a macOS daemon now depend on Apple validator preparation and lose the old graceful pass
🔵 Nit (6)
Where Finding
packages/capture-kit/src/recording/overlay.ts:121 Overlay discards a successfully exported composite when validator preparation is still pending
packages/capture-kit/src/recording/swift-cache.ts:190 ensureSwiftExecutable now waits for a canceled compile instead of releasing the Swift cache lock
packages/capture-kit/src/recording/video.ts:213 🆕 isUnavailableVideoValidatorError reuses arbitrary compile stderr as the reason an MP4 is not playable
packages/capture-kit/src/recording/video.ts:254 A stop whose preparation never started returns retriable video_validator_not_prepared, but nothing on the stop path ever starts it — retries cannot recover
packages/capture-kit/src/screen-recording-transport.ts:102 Container kind is inferred from the output path in two places that will drift
src/platform-runtime-screen-recording-host.ts:28 🆕 Validator preparation selects container vs AVFoundation from process.platform at call time, not from the host it was built with

Reply under an inline comment with @apex-sentinel-staging note ... or @apex-sentinel-staging pivot ... to re-check that finding.
Comment @apex-sentinel-staging review on this conversation to run a full review.

Earlier reviews

Review of 9bf65057 · 03.11.2026 17:18 UTC

Apex Sentinel

Verdict: request_changes

Since the last review: 5 new · 2 still open · 7 no longer raised

7 finding(s): 1 major, 3 minor, 3 nit. Verdict: request_changes.

Required fixes

  • Android recording stop on a macOS host fails when the background validator compile needs more than 10s

🟠 Major (1)

Where Finding
src/platform-runtime-screen-recording-android-host.ts:112 🆕 Android recording stop on a macOS host fails when the background validator compile needs more than 10s

🟡 Minor (3)

Where Finding
src/platform-runtime-screen-recording-finalizer-host.ts:63 Overlay no longer verifies the published video after renaming the composite over the original
packages/capture-kit/src/recording/video.ts:57 Failed validator preparation is never cleared unless a new prepare() runs, so stop reports the stale failure with a hint to run prepare
src/platform-runtime-screen-recording-android-host.ts:112 🆕 Android transport probe now throws video_validator_not_prepared where it used to return playable: false
🔵 Nit (3)
Where Finding
packages/capture-kit/src/recording/swift-cache.ts:153 🆕 Validator compile can no longer be cancelled; swiftc is left unowned again
packages/provider-limrun/src/recording-runtime.ts:62 🆕 Duplicated webm-extension check in startTransportScreenRecording and startLimrunRecording
src/platform-runtime-apple-application-tools.ts:99 🆕 Background validator preparation is fired and swallowed twice
Review of d8bc80e0 · 03.11.2026 16:44 UTC

Apex Sentinel

Verdict: request_changes

Since the last review: 8 new · 1 still open · 8 no longer raised

9 finding(s): 2 major, 3 minor, 4 nit. Verdict: request_changes.

Required fixes

  • A failed validator preparation is cached forever, so the documented "retry record stop" can never succeed
  • Android host probe now throws instead of reporting an unvalidated recording, changing the Android stop/recovery path

🟠 Major (2)

Where Finding
packages/capture-kit/src/recording/video.ts:57 🆕 A failed validator preparation is cached forever, so the documented "retry record stop" can never succeed
src/platform-runtime-screen-recording-android-host.ts:110 🆕 Android host probe now throws instead of reporting an unvalidated recording, changing the Android stop/recovery path

🟡 Minor (3)

Where Finding
website/docs/docs/commands.md:1068 🆕 Docs still say the overlay budget covers a video check that runs after the overlay, and the removed post-overlay playability probe is undocumented
packages/capture-kit/src/recording/video.ts:226 🆕 Behavior change: an unavailable validator used to make MP4s read as playable, now it hard-fails the stop
src/platform-runtime-screen-recording-finalizer-host.ts:54 Post-overlay playability check was dropped: an unplayable published composite now exits 0 with no warning
🔵 Nit (4)
Where Finding
packages/capture-kit/src/recording/video.ts:50 🆕 videoValidatorPreparations grows one entry per Swift cache root for the life of the process
packages/capture-kit/src/recording/video.ts:75 🆕 Removed the now-dead buildSwiftToolEnv import path from video.ts's validator run while keeping the import
packages/test-kit/src/recording-video.ts:12 🆕 prepareRecordingValidatorFixture reads the -o argument with an index expression that passes its own guard when -o is missing
src/platform-runtime-apple-application-tools.ts:96 🆕 prepareRunner now charges dynamic module loading to startedAtMs
Review of 776aab84 · 03.11.2026 15:37 UTC

Apex Sentinel

Verdict: request_changes

Since the last review: 7 new · 2 still open · 8 no longer raised

9 finding(s): 1 major, 3 minor, 5 nit. Verdict: request_changes.

Required fixes

  • Android pullPlayable now throws instead of reporting playable:false when no validator was prepared in that process

🟠 Major (1)

Where Finding
src/platform-runtime-screen-recording-android-host.ts:108 Android pullPlayable now throws instead of reporting playable:false when no validator was prepared in that process

🟡 Minor (3)

Where Finding
src/platform-runtime-screen-recording-finalizer-host.ts:73 🆕 Budget-exhausted post-overlay check reports the overlay as dropped when the burned video is what shipped
packages/capture-kit/src/screen-recording-transport.ts:99 🆕 Background validator preparation is started before the unsupported-option check, so an invalid --fps still spawns swiftc
src/platform-runtime-screen-recording-finalizer-host.ts:22 🆕 Validator preparation reads platform and cache env from the recording client's process, not the capture host's
🔵 Nit (5)
Where Finding
packages/capture-kit/src/recording/swift-cache.ts:137 🆕 Unenforced invariant: preparation and reading hash only the Swift source, while ensureSwiftExecutable compiles the written file path
packages/capture-kit/src/recording/video.ts:226 🆕 Validator-unavailable errors from a failed run drop the stderr evidence
packages/capture-kit/src/recording/video.ts:267 🆕 Trailing \b fix leaves a redundant regex alternative
packages/platform-android/src/recording/runtime.ts:68 WebM output still requests MP4 validator preparation on three platforms
src/platform-runtime-apple-application-tools.ts:96 🆕 prepareRunner records startedAtMs before an untracked validator launch, so the timing fields can attribute the wrong wall clock
Review of 49b95be5 · 03.11.2026 14:44 UTC

Apex Sentinel

Verdict: request_changes

Since the last review: 5 new · 5 still open · 4 no longer raised

10 finding(s): 1 major, 2 minor, 7 nit. Verdict: request_changes.

Required fixes

  • record stop on a non-macOS host now hard-fails MP4 finalization where it previously tolerated the missing Swift toolchain

🟠 Major (1)

Where Finding
packages/capture-kit/src/recording/video.ts:195 record stop on a non-macOS host now hard-fails MP4 finalization where it previously tolerated the missing Swift toolchain

🟡 Minor (2)

Where Finding
src/platform-runtime-screen-recording-android-host.ts:108 Android pulled-chunk playability now throws instead of reporting playable:false, breaking MP4 stops on hosts without the Swift validator
packages/provider-limrun/src/recording-runtime.ts:58 🆕 Limrun now refuses any .webm output path although its own help says WebM needs no Swift validator
🔵 Nit (7)
Where Finding
packages/capture-kit/src/recording/overlay.ts:160 🆕 Inconsistent validator parameter: explicitly undefined in 14 test call sites while TypeScript infers it
packages/capture-kit/src/recording/video.ts:57 Background validator preparation can never be retried once the in-process map holds a failure, for every caller except the finalizer
packages/capture-kit/src/recording/video.ts:155 🆕 readPreparedVideoValidator returns a rejected promise when preparation previously failed and no cached executable exists
packages/capture-kit/src/recording/video.ts:185 🆕 Duplicated video_validator_not_prepared error with divergent hints inside isPlayableVideo
packages/capture-kit/src/recording/video.ts:264 Removed trailing \b from the validator-unavailable regex silently widens matching
packages/capture-kit/src/screen-recording-transport.ts:102 Container detection logic for finalize.prepare is duplicated in three places with different strategies
src/platform-runtime-screen-recording-finalizer-host.ts:26 🆕 void prepareVideoValidator(host) in finalizer prepare swallows the rejection, so the failed flag in the module-level map is the only retry signal
Review of a5c82f55 · 03.11.2026 13:23 UTC

Apex Sentinel

Verdict: request_changes

Since the last review: 7 new · 2 still open · 6 no longer raised

9 finding(s): 2 major, 2 minor, 5 nit. Verdict: request_changes.

Required fixes

  • record stop can never recover from a failed validator preparation in the same session (stale failed promise, wrong hint)
  • record stop now fails hard on hosts without AVFoundation/swiftc, where the old code accepted the MP4

🟠 Major (2)

Where Finding
packages/capture-kit/src/recording/video.ts:57 record stop can never recover from a failed validator preparation in the same session (stale failed promise, wrong hint)
packages/capture-kit/src/recording/video.ts:130 🆕 record stop now fails hard on hosts without AVFoundation/swiftc, where the old code accepted the MP4

🟡 Minor (2)

Where Finding
packages/provider-limrun/src/recording-runtime.ts:58 🆕 Limrun .webm refusal added, but the WebM branch in startTransportScreenRecording is dead code for Limrun
src/platform-runtime-screen-recording-android-host.ts:112 🆕 A stop that never saw a prepare (daemon-restart recovery) fails forever on a hint that says to wait for a background preparation that never starts
🔵 Nit (5)
Where Finding
packages/capture-kit/src/recording/overlay.ts:28 🆕 Stale comment still claims a cold host compiles inside the overlay budget
packages/capture-kit/src/recording/swift-cache.ts:153 🆕 compileSwiftSourceText no longer accepts a cancellation signal; the stale doc comment describes a caller-less feature
packages/capture-kit/src/recording/video.ts:256 🆕 isSwiftVideoValidatorUnavailable: trailing \b after a closing quote never matches
packages/capture-kit/src/screen-recording-transport.ts:102 The .webm container is inferred from the extension in three places
src/platform-runtime-screen-recording-host.ts:11 🆕 prepare's doc says it does not wait, but the host wrapper awaits a dynamic import
Review of 7e440f19 · 03.11.2026 12:49 UTC

Apex Sentinel

Verdict: request_changes

8 finding(s): 2 major, 2 minor, 4 nit. Verdict: request_changes.

Required fixes

  • isPlayableVideo now throws for a missing/unavailable validator on a best-effort poll path (Android pull retries, overlay verification)

🟠 Major (2)

Where Finding
packages/capture-kit/src/recording/video.ts:76 Failed validator preparation is cached forever per cache root: no retry ever re-prepares it
packages/capture-kit/src/recording/video.ts:177 isPlayableVideo now throws for a missing/unavailable validator on a best-effort poll path (Android pull retries, overlay verification)

🟡 Minor (2)

Where Finding
packages/platform-android/src/recording/runtime.ts:65 MP4 toolchain requirement is enforced only at stop; record start still accepts MP4 on hosts that cannot finalize it
src/platform-runtime-screen-recording-finalizer-host.ts:73 Post-overlay playability check now fails a successful export as 'unplayable' when the budget is spent
🔵 Nit (4)
Where Finding
packages/capture-kit/src/recording/video.ts:52 Unbounded videoValidatorPreparations map keyed by caller-controlled cache root
packages/capture-kit/src/screen-recording-transport.ts:102 Container sniffing by .webm filename suffix is duplicated in two packages
packages/provider-limrun/src/recording-runtime.ts:62 startLimrunRecording gains a new user-facing refusal — an INVALID_ARGS error for a .webm output path — but neit…
packages/test-kit/src/recording-video.ts:12 prepareRecordingValidatorFixture reads the -o operand by scanning the whole argument list

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 8 new findings on 7e440f1. The verdict and the full list are in the summary comment.

const validatorPath = await waitForValidatorCompile(
host,
deadline?.remainingMs() ?? VALIDATOR_COMPILE_WAIT_MS,
const notPrepared = () =>

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on a5c82f5.

🟠 Major · isPlayableVideo now throws for a missing/unavailable validator on a best-effort poll path (Android pull retries, overlay verification)

The new isPlayableVideo throws AppError(COMMAND_FAILED, reason: 'video_validator_not_prepared') when validator is undefined (video.ts:187) and throws video_validator_unavailable when the validator run reports a missing toolchain (video.ts:218-219). Its documented contract is a boolean answer — waitForPlayableVideo 'resolves whether it played' — and several unchanged callers poll it as a cheap check and handle false by retrying, not by aborting:

Suggested change

Keep isPlayableVideo total for these paths: return false when no validator is prepared and let the caller's own deadline/poll budget decide, or expose an explicit assertVideoValidatorReady for the finalizer and have the Android transport and overlay verification use the boolean form so their retry/overlayWarning handling still runs.

Details
  • pullPlayableChunk (packages/platform-android/src/recording/chunks.ts:240-251) calls transport.pullPlayable up to PLAYABLE_PULL_ATTEMPTS times, expecting { playable: false } while a device-side recorder finishes writing its moov, and keeps the remote evidence on exhaustion (pinned by 'retries a pulled MP4 until its moov is playable and retains remote evidence on exhaustion' and 'does not remove native evidence when bounded playable pulls are exhausted'). The Android transport now answers with readPreparedVideoValidator(host) (src/platform-runtime-screen-recording-android-host.ts:115-117), which returns undefined when no warm has run — every daemon-restart recovery/reattach stop, and any first stop after a warm that already failed (see the poisoned-cache finding) — so the first pullPlayable rejects with video_validator_not_prepared instead of returning { playable: false }, the bounded retry never runs, and the pull fails on attempt 1.
  • overlayTouches (src/platform-runtime-screen-recording-finalizer-host.ts:75) catches everything into an overlayWarning, so a not-prepared/unavailable throw now silently discards a composited overlay as 'failed to overlay recording touches: The video validator is not ready...' — the message blames the overlay for a validator-state problem, and the raw recording is returned without any hint that record stop should be retried.

Predecessor behavior: it never threw for a missing validator (it compiled one) and it returned true when the toolchain was unavailable. Both escape routes are gone and the boolean-only callers were not updated.

Also raised here

Also raised at this location by javascript: Android pullPlayable now throws instead of reporting playable: false, bypassing the pull retry loop — pullPlayable computes playable: result.exitCode === 0 && (await isPlayableVideo(outputPath, readPreparedVideoValidator(host))). readPreparedVideoValidator returns undefined when this process never started a preparation for that cache root and no executable is published, and isPlayableVideo throws a video_validator_not_prepared AppError for an undefined validator. The predecessor never threw here for a cold cache: it compiled the validator lazily from host and returned a boolean, and the caller pullPlayableChunk (packages/platform-android/src/recording/chunks.ts:240-251) retries transport.pullPlayable for PLAYABLE_PULL_ATTEMPTS rounds and only then reports 'failed to retrieve playable Android recording'. …

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.

@@ -65,6 +65,7 @@ async function startAndroidRecording(params: {
}) {

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on a5c82f5.

🟡 Minor · MP4 toolchain requirement is enforced only at stop; record start still accepts MP4 on hosts that cannot finalize it

The PR now documents that 'MP4 verification requires macOS with Xcode command line tools and AVFoundation; unavailable tooling returns video_validator_unavailable only at stop' (packages/command-surface/src/recording/index.ts cliDetail, website/docs/docs/commands.md:1068). On a non-Apple host — the common Android/Harmony setup on Linux CI — the background warm at record start (packages/platform-android/src/recording/runtime.ts:68, packages/platform-harmonyos/src/recording/runtime.ts:80) fails with TOOL_MISSING, the promise is swallowed by void ...prepare(...).catch(() => {}), and the user only learns after recording up to 180s of footage that stop can never produce a validated export. startAndroidRecording and startHarmonyRecording have all the inputs needed to classify this at start (the host env plus the container decided from input.outputPath), and they already refuse other unsupported combinations up front via assertScreenRecordingOptionsSupported. Reporting a non-retriable condition as early as possible is the convention this repository follows elsewhere, and the failure is strictly more expensive after capture.

Suggested change

Either surface the warm's typed unavailable outcome at record start for MP4 destinations (or record the outcome on the session and disclose it in the start result), or state in the MP4 requirement note that record start does not check it so users on Linux hosts know to expect a stop-time failure.

Details

persona: 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.

(command, args) => {
if (command !== 'xcrun' || args[0] !== 'swiftc') {
throw new Error(`Unexpected validator preparation command: ${command}`);
}

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on 9bf6505.

🆕 🔵 Nit · prepareRecordingValidatorFixture reads the -o argument with an index expression that passes its own guard when -o is missing

The new exported helper derives the compiler's output path with args[args.indexOf('-o') + 1]. When -o is absent, indexOf returns -1, so this evaluates to args[0] — the string 'swiftc' — which is truthy and so slips past the if (!output) guard; the helper then writes an executable stub to ./swiftc in the harness cwd and prepareVideoValidator resolves against a path the cache never looks at. The caller (packages/capture-kit/src/recording/swift-cache.ts:213) builds the argument list as ['swiftc', ...sourcePaths, '-o', tempExecutablePath], so today it happens to work; nothing ties the fixture to that shape. Look up the index first and reject when it is -1.

Suggested change
}
const outputIndex = args.indexOf('-o');
const output = outputIndex === -1 ? undefined : args[outputIndex + 1];
if (!output) throw new Error('Swift compiler fixture received no executable output');
Details

persona: 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.

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Review follow-up at a5c82f554ffcead13180ee79352da828c428b275 (previous published head 7e440f1).

Accepted fixes:

  • A failed background preparation no longer poisons the cache root. One entry retains its promise and failure state: stop still reads the original typed diagnostic, while the next explicit preparation replaces a failed attempt. Pending attempts share one compile; stop cannot compile. This avoids both permanent failure memoization and losing video_validator_unavailable by deleting the error too early.
  • Post-overlay verification already returned overlayWarning; it did not fatally fail the stop. Its deadline-expiry message now names the finalization budget instead of claiming the video is unplayable. The budget is unchanged.
  • Owning CLI help and commands docs now state Limrun's existing .webm output refusal.

Other finding dispositions:

  • Missing/unavailable validation remains a typed error, not false. An unavailable validator is different from a file whose media is not ready. Returning false would mask the actionable validator result behind Android pull exhaustion or an unplayable-file error. Actual negative playability results still use the existing bounded retries. No fail-open playable verdict is restored.
  • Background warming must not block or fail recording start. The help already explicitly places unavailable-tooling errors at stop. Requiring successful compilation at start would undo the reviewed non-blocking contract.
  • The map keeps one authority/outcome per host Swift-cache root, as the memoization contract requires. Entries do not store compiler process handles. No second session-lifecycle owner or speculative arbitrary eviction was added.
  • The two suffix checks serve distinct policies (warming versus Limrun format refusal); actual media validation still reads bytes. No new helper for a one-expression check was introduced.
  • The fixture already uses array indexOf('-o'), which matches an exact argument token; it cannot match -Onone as claimed.

Validation:

  • Four focused suites: 56 passed.
  • Same two regression-suite commands: fixed head 31 passed; disposable checkout with old production video/finalizer code restored: 3 failed, 28 passed. Both cache-failure classes fail the successful-retry assertion; the budget case fails its warning expectation. The experiment was removed and the implementation checkout stayed clean.
  • Independent read-only review of the revised retry owner: no findings. The warning fix was self-reviewed. pnpm format passed.
  • Six files changed in this round, +78/-10. Previous device evidence remains attributed to 7e440f1; this round changes host preparation recovery and diagnostic wording, with no new device dispatch/capture route.

Exact-head affected-gate receipt and publication status follow below.

Final exact-head validation:

  • Tested commit: a5c82f554ffcead13180ee79352da828c428b275; merge base: c8ad8fb1257787ce0973cf74271cb9a4fafb51d9.
  • AGENT_DEVICE_VITEST_MAX_WORKERS=2 pnpm check:affected --run --head a5c82f554ffcead13180ee79352da828c428b275: passed (exit 0). Receipt: .tmp/validation/run-tXjX2I/receipt.json; exactCommit=true; checkoutChanged=false; initial/final checkout clean. Run log: .tmp/pr-4188-review/check-affected-publication.log.
  • 11 runnable checks passed. Related tests: 1,230 passed; unit tests: 17,150 passed, one skipped; command-doc tests: 12 passed. No test failures or unhandled errors. Provider integration was deduplicated as covered by related tests.
  • Eight checks were skipped: GitHub owns coverage, web smoke, macOS/Linux replay, and Linux command evidence; iOS/iOS-device/Android replay is parked for manual workflows. These are not local passes. CI/native/device obligations remain with GitHub.
  • Gate outcome: validated at the exact commit; no validation failure. Independent review of the revised retry owner had no findings; the warning-message change was self-reviewed.

Publication: pushed once with the explicit lease from 7e440f19f80576cc14cf8870092363d488c1302a to a5c82f554ffcead13180ee79352da828c428b275; GitHub confirms the PR is open at that head. No Apex threads were replied to or resolved. No labels or merge were requested. CI and review follow-up remain with the parent.

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 7 new findings and 1 moved to a new line on a5c82f5. The verdict and the full list are in the summary comment.

@@ -109,7 +112,9 @@ export async function createAndroidScreenRecordingTransport(
});

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on 9bf6505.

🆕 🟠 Major · Android host probe now throws instead of reporting an unvalidated recording, changing the Android stop/recovery path

probeArtifact (or its equivalent) used to be lenient: isPlayableVideo(outputPath, host) returned true when the Swift validator was unavailable, because the old catch mapped any isSwiftVideoValidatorError (TOOL_MISSING, or stderr mentioning a missing swiftc/AVFoundation module) to true — "we cannot verify, treat as playable". Now the call is isPlayableVideo(outputPath, readPreparedVideoValidator(host)), and readPreparedVideoValidator returns undefined (never prepared, no published executable) or a rejected promise (preparation failed). isPlayableVideo throws video_validator_not_prepared for undefined (video.ts:198-204) and video_validator_unavailable for a rejected preparation. So the probe's playable expression rejects instead of resolving true/false.

Suggested change

At the Android probe call site, decide what an absent/failed authority means (treat as not verified rather than throwing), or pass a deadline and catch the typed not-prepared/unavailable errors so the probe still resolves a boolean for the recovery read-back.

Details

Concrete case: macOS host, Android recording, no Xcode/AVFoundation (or a preparation that failed). The old code returned playable: true and the stop proceeded; the new code aborts the probe with a non-retriable video_validator_unavailable in the middle of the Android artifact probe / recovery read-back, where the probe's purpose is only to report whether a device-side file can still become an export. Nothing in this PR's tests covers an unprepared/failed validator reaching this Android call site (the Android host tests mock the video module's old signature).

Also raised here

Also raised at this location by javascript: isPlayableVideo now throws for an unprepared validator, which escapes pullPlayable's bounded retry loop on Android — src/platform-runtime-screen-recording-android-host.ts:115-117 passes readPreparedVideoValidator(host) into isPlayableVideo, which now throws video_validator_not_prepared when the argument is undefined (video.ts:198-204) instead of producing a boolean. pullPlayable is consumed in a bounded retry loop at packages/platform-android/src/recording/chunks.ts:240-250 (if (pulled.exitCode === 0 && pulled.playable) return; ... throw new Error('failed to retrieve playable Android recording')), which was written assuming a pull answers playable: false. …

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.

@@ -240,7 +255,7 @@ export async function waitForPlayableVideo(

function isSwiftVideoValidatorUnavailable(stderr: string, stdout: string): boolean {

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on 776aab8.

🔵 Nit · Removed trailing \b from the validator-unavailable regex silently widens matching

The diff deletes only the trailing word boundary in isSwiftVideoValidatorUnavailable (video.ts:266): ...swiftc?["'])\b became ...swiftc?["']). The change is invisible in the description and untested in that direction: the new tests cover no such module 'AVFoundationExtras' (rejected, but that alternative is anchored by the closing quote, so it is unaffected), while nothing covers the two swiftc alternatives. Dropping the trailing boundary is what makes the unavailable classification fire on any output with text appended after the quoted swiftc/swiftc? token, so those two alternatives now match a strictly larger set of compiler output — every output that gets classified as video_validator_unavailable is reported non-retriable and the recording is refused outright, and with the new code that classification also fires on the validator's own stderr (video.ts:232-233). Either restore the boundary, or add the case the relaxation was meant to catch so the intent is encoded.

Suggested change

Restore the trailing \b, or state which real message the relaxation handles and cover it with a test each side (matching and non-matching output).

Details

persona: 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.

host: HostEnvironment;
}): ScreenRecordingRuntimeHost {
const host = options.host;
const loadFinalizer = async () => {

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on 49b95be.

🆕 🔵 Nit · prepare's doc says it does not wait, but the host wrapper awaits a dynamic import

ScreenRecordingFinalizer.prepare is documented as 'Starts best-effort helper preparation without waiting for it', while createScreenRecordingRuntimeHost implements it as async (input) => (await loadFinalizer()).prepare(input). That await pulls in the whole finalizer module (and its capture-kit imports) before the promise settles, so the fire-and-forget callers added in this PR actually pay module-load cost inside their .catch(() => {}) chain, and prepare is not the non-blocking call the contract advertises. Start the import eagerly, or say in the contract that prepare may await module loading.

Details

persona: 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.

@thymikee

thymikee commented Nov 3, 2026 •

Copy link
Copy Markdown
Member Author

At a5c82f5 two open readiness threads still stand, two regression tests would pass without the code they protect, and the Android and iOS device runs are missing. All other checks pass; the only one not passing is the external review bot status, which runs no repository code. There are no conflicts.

The pending-preparation test never observes its callback being entered, so it passes even if start never warms the validator and cold stop has no prepared authority. Can the test record entry before recorder launch and prove start completes while preparation remains unresolved? Deleting the invocation and awaiting preparation must each fail for the intended reason. Test

The runner-preparation test checks only the mocked runner result, so it passes when the preparation call is deleted and does not protect runner warming. Can it assert preparation entry and successful runner output, and cover production finalizer preparation with a rejected compiler outcome that preserves the typed failure without failing runner preparation? Removing the preparation call must fail the test. Test

The changed Android pull consumes prepared authority, but the packet contains no Android device run; the iOS Air report lacks commands, a named device, and observable output. Please run Android and iOS with an isolated cold daemon state directory without test-process prewarming, and provide commands, source SHA, device, output or artifacts, and cleanup evidence. On Android, show capture starting while compilation is pending, adb pullPlayable and finalization producing a playable MP4, a typed early readiness timeout, a successful retry without a second compile, and daemon-restart manifest recovery with no published validator. On iOS, supply auditable evidence for the 7e440f1 Air run or repeat cold start, early stop, and warm stop on the final source, including touch telemetry and compositing. Show compilation beginning before or alongside capture, none beginning from stop, and a same-session retry succeeding after video_validator_not_prepared if readiness expires. Android pull

Could the existing readiness owner and Swift executable store adopt an already published executable before replaying a failed attempt, preserving the diagnostic only when no executable exists? Could MP4 resource admission and restoration initiate the same best-effort preparation outside stop, with accurate pending, absent, and failed outcomes, while readers and overlays remain compile-free?

The open threads on published executable recovery and restored recording preparation still stand; no lower-priority threads still apply. The following can be resolved: playability would mask typed failure; start failure conflicts with best-effort preparation; overlay warning is not a fatal stop failure; executable lifetime has no demonstrated memory failure; format checks have distinct responsibilities; Limrun documentation now describes refusal; compiler arguments use exact token matching; unavailable outcome is required; WebM branch remains live; overlay compilation uses a separate helper; cancellation remains supported; module matching already excludes AVFoundationExtras; capture delay does not await preparation.

The regression findings follow from source inspection; no mutation experiment was run. The packet does not establish live validation for the other native and provider MP4 consumers.

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Recovery follow-up published at 49b95be5a1cda52f609c559e410c1ab710eb3ab4:

  1. Fixed stale failed-promise recovery: cache-only reads now see externally published executables. Cache misses preserve the original typed rejection; stop never compiles.
  2. Distinguished missing authority from pending preparation in the hint, retaining the existing retriable reason.
  3. Fixed unavailable-tooling recovery and owning help/docs: explicit agent-device prepare ios-runner --platform ios on the recording host with the same cache, then retry stop in the original session after background preparation is ready.
  4. Kept intentional hard failure for unavailable MP4 semantic validation. WebM bypass remains correct.
  5. Kept the live transport WebM branch used by platform-web and the container field. Limrun refusal and transport selection remain separate policy owners; no suffix abstraction added.
  6. Kept cold overlay-helper compile claims; fixed comment grammar. That helper is separate from the validator.
  7. Kept the already-correct classifier regex and positive/negative tests; no speculative comment added.
  8. Kept lazy finalizer loading: callers start preparation without awaiting it.
  9. Removed the production-unused Swift cache signal input and two cancellation tests. General process-lock cancellation and timeout/SIGINT/grace cleanup remain covered; the timeout policy mutation was detected.

The exact-commit affected gate passed on an unchanged retry after an unrelated temp-path assertion failure was reproduced on base. Expanded focused tests passed 107 cases; real host-only cross-process recovery passed. Independent read-only review found no issues. CI/device obligations and review approval remain. Full evidence and both gate receipts are in the local recovery handoff. No Apex thread was replied to or resolved.

Validation detail: receipt run-mJP5jC passed with exactCommit=true and checkoutChanged=false: 11 checks passed, eight skipped, one deduplicated. The initial failed receipt run-c4g6p8 remains recorded. Head and unchanged a5c82f5 replayed the same cli-config-trust.test.ts:40 assertion failure when the temporary path contained run-123; ordinary setup passed 19 tests and the unchanged full gate rerun passed. No test assertion, timeout, selection, allowlist or source was changed to get that pass. The pre-existing test-path collision remains a separate risk.

Real host scope: the same long-lived reader retained the failed preparation, another process published the real AVFoundation executable in the isolated cache, and the reader discovered/executed it with its compiler PATH unavailable. This proves helper recovery, not a new live recording or end-to-end CLI prepare run. Earlier live recording evidence remains at its original SHA. No daemon or device session was opened for this round.

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Integration timeout diagnosis at 49b95be5a1cda52f609c559e410c1ab710eb3ab4 remains unresolved; no source change or CI retry was made.

CI job: proxy/direct publication parity hit its 10,000 ms test timeout; 244 other provider tests passed. The log identifies no unfinished request phase or infrastructure error.

Sequential four-worker comparison in an isolated checkout:

  • Head: focused parity file passed 7/7. Full lane failed a different android-alert.test.ts:128 5,000 ms timeout, with 244 passed.
  • Unchanged PR base c8ad8fb1257787ce0973cf74271cb9a4fafb51d9: focused parity passed 7/7; full provider lane passed 245/245.
  • Head again, unmodified: parity passed; full lane failed a different ios-lifecycle.test.ts:20 5,000 ms timeout, with 244 passed.

Temporary phase instrumentation measured parity at 3.44 s on base and 3.56 s on head, with millisecond teardown. A diagnostic full head run passed, but those probes changed the tree; that is not an unmodified validation pass. All probes were removed, and the checkout is clean. No timeout, retry, skip, pool/isolation, assertion, or gate selection was changed.

The parity scenario issues no recording or runner-prepare command. Its two worlds and lifecycle script are byte-identical between base and head; first-use setup/open/snapshot imports and the unchanged post-open settle dominate the measured work. This evidence establishes neither a recording regression nor a host-contention cause. The changing local timeout locations also do not establish a common cause.

Local Darwin/arm64, Node 26.11.1 and pnpm 11.25.0 differ from CI Ubuntu/x64, Node 24.13.1 and pnpm 11.17.0. The next diagnostic needs that matching toolchain plus setup/request/teardown and worker delay evidence when the timeout occurs. No matching Linux runtime was available locally. The independent Android smoke assertion investigation continues separately.

The passing affected receipt remains attributed to its recorded selected checks; this failed full provider lane is not overridden by that receipt. The PR is not merge-ready.

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 4 new findings and 2 moved to a new line on 49b95be. The verdict and the full list are in the summary comment.

@@ -59,6 +59,9 @@ async function startLimrunRecording(
params: Parameters<typeof createLimrunScreenRecordingOperations>[0],

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on 776aab8.

🆕 🟡 Minor · Limrun now refuses any .webm output path although its own help says WebM needs no Swift validator

startLimrunRecording refuses every path ending in .webm (case-insensitively) with INVALID_ARGS. Nothing in the diff explains this with a WebM-only Limrun limitation: the change's own documentation says the opposite — the new record cliDetail says "WebM uses container validation without Swift" and commands.md:1069 says "WebM validation needs no Swift helper" — and WebM finalization really does need no Swift (isPlayableVideo returns true on the WebM container sniff, video.ts:184). The new test only asserts the refusal, not its reason. So the change adds a hard refusal of an input class whose stated justification does not hold. If the intent was only to stop .webm being silently recorded as MP4 (which is what the code does today), that belongs in the refusal message and the docs, which currently say the opposite. Also note the check sits before params.getDeviceSession, so an unknown-device call about to fail with INVALID_ARGS now answers the container error instead.

Suggested change

Either keep WebM working for Limrun (its finalization needs no Swift helper, as the new docs state) or state the real Limrun-specific reason for the refusal in the error message and in the record help/commands.md, which currently claim WebM needs no Swift tooling.

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.

@@ -158,6 +159,7 @@ function temporarySiblingVideoPath(videoPath: string): string {

export async function overlayRecordingTouches(params: {

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on 776aab8.

🆕 🔵 Nit · Inconsistent validator parameter: explicitly undefined in 14 test call sites while TypeScript infers it

The overlayRecordingTouches signature adds validator: VideoValidator | undefined as a required parameter (not optional), which forces every call site to pass it. In overlay.test.ts, 14 call sites add validator: undefined where the value is unused. If the parameter were optional (validator?: VideoValidator), existing callers would not need a mechanical change and the tests that exercise the validator path would stand out. The required-but-always-undefined pattern makes the diff larger and hides the two call sites that actually pass a real validator.

Suggested change
export async function overlayRecordingTouches(params: {
export async function overlayRecordingTouches(params: {
host: HostEnvironment;
validator?: VideoValidator;
videoPath: string;
Details

persona: 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.

const runTimeoutMs = Math.min(
VALIDATOR_RUN_TIMEOUT_MS,
deadline?.remainingMs() ?? VALIDATOR_RUN_TIMEOUT_MS,
const notPrepared = () =>

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on 776aab8.

🆕 🔵 Nit · Duplicated video_validator_not_prepared error with divergent hints inside isPlayableVideo

notPrepared() (lines 185-194) builds the video_validator_not_prepared AppError used for the timeout, while lines 196-200 build a second, near-identical AppError with the same reason but a different message and hint for the !validator case. The two hints give the operator different recovery instructions for the same reason code, so which remedy the operator sees depends on which branch fired.

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.

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Follow-up at 776aab8492c636dafba14eebe7f39bfe3e2d910c, based on published 49b95be5a1cda52f609c559e410c1ab710eb3ab4.

Accepted platform findings:

  • Mandatory AVFoundation on every MP4 host was a regression. The pre-existing non-macOS behavior is restored through an explicit authority union: container validation or a prepared AVFoundation executable. Only the authority constructors select host capability. Linux/Windows preparation and reads touch neither compiler nor Swift cache; the validator honors supplied authority after structural checks. macOS missing/pending/unavailable outcomes, cache recovery and deadlines remain strict. There is no generic exception-to-playable fallback and stop cannot compile.
  • The Android pulled-chunk concern follows the same host authority correction. Android/Linux uses structural validation; genuine macOS infrastructure/validator failures retain their typed errors rather than being mislabeled as media retry results.

Other current review dispositions:

  • Failed preparations retry on the next explicit preparation; all recording starts call that owner. Cache-only stop readers deliberately do not prepare. External publication is re-read after a failed entry, and a missing cache preserves the original typed rejection.
  • Unavailable tooling is non-retriable until tooling/preparation is repaired. The recovery hint explicitly states those preconditions before retrying stop; this is not an automatic retry instruction.
  • Missing authority and pending preparation have different recovery actions, so their messages/hints differ under the shared not-prepared reason.
  • Limrun explicitly records MP4 and its refusal already says so; owning help/commands contain the Limrun-specific WebM refusal. WebM-without-Swift describes a supported format's validation, not Limrun format support.
  • Container decisions remain owned by the producing backend/transport. A hypothetical future native WebM backend does not justify a shared suffix abstraction now.
  • Overlay authority stays explicit at each call, including intentionally absent test authority. Optionality would weaken the new construction contract.
  • The unavailable regex already has real positive Swift/AVFoundation diagnostics and the adjacent AVFoundationExtras negative. Restoring the trailing boundary would fail the real diagnostic cases.
  • Preparation's internal rejection observer owns failed-state retention and diagnostics. The finalizer starts it without awaiting it; runtime catches cover host-loading failure separately. Adding another no-op catch supplies no new guarantee.

Android smoke repair:

  • CI captured Back mid-transition. A destination text wait before public diff would overwrite its baseline; the live negative control demonstrated that loss.
  • Back now uses existing --settle frozen-baseline evidence and retains both original route changes. Independent review caught a similar async assumption in the added press canary; public diff coverage was moved to a text fill whose native postcondition is confirmed. Both fills must be confirmed, and the diff must remove Grace Hopper and add Ada Łovelace before any post-fill read replaces the baseline. Scenario and command coverage ownership moved together.
  • Combined local smoke passed in 56.9 s on ReactNative_API_35B/API35/arm64 with fresh root CLI/helper builds. APK SHA256: 53d94d5abc23b71f503f802042a509c8ef43229cad9643e334f5e2a4ab1e7cbf; fixture source is unchanged from the separately built release APK. All six smoke scenarios plus bootstrap passed. The task's emulator was shut down; harness cleanup closed sessions and reaped its daemon. Natural CI transition timing was not reproduced.

Focused validation: 61 platform/cache/finalizer/overlay tests; 62 final platform/settle/wait boundaries; 11 Node coverage/report contracts. Both Linux/Windows cases exercise missing Swift tooling without compilation; malformed MP4 and missing authority still refuse. Independent platform and final combined review found no issues; the authority union was assessed as the smallest coherent design. Isolated regression proof in this checkout applied only the reviewed video.test.ts patch to 49b95be5a1cda52f609c559e410c1ab710eb3ab4: 2 failed, 24 skipped. Both Linux/Windows cases attempted Swift and rejected video_validator_unavailable from TOOL_MISSING (no xcrun), before the authority assertion. The patch was reversed and the old checkout verified clean. At clean 776aab8492c636dafba14eebe7f39bfe3e2d910c, the identical command passed both cases, with 24 skipped by the explicit diagnostic filter:

AGENT_DEVICE_VITEST_MAX_WORKERS=2 pnpm exec vitest run --project unit-core packages/capture-kit/src/recording/video.test.ts -t 'recording validation uses container authority without Swift preparation'

Exact affected gate passed using AGENT_DEVICE_VITEST_MAX_WORKERS=2 pnpm check:affected --run --head 776aab8492c636dafba14eebe7f39bfe3e2d910c. Receipt run-AVDG40: passed, exit 0, exactCommit true, checkoutChanged false; initial/final clean status and fingerprints match. Related: 1,230 passed/0 skipped; unit: 17,155 passed/1 skipped; smoke contracts: 76 passed/6 skipped; Node integration: 149 passed/13 skipped; command docs: 12 passed. All 13 runnable checks passed. Provider integration was deduplicated through related tests, not rerun as a full lane. Eight checks were skipped: GitHub owns coverage, web-smoke, replay-macos, replay-linux and linux-command-evidence; replay-ios, replay-ios-device and replay-android remain manual. These skips are not passes. Four-worker provider lane: all 245 passed across 68 files in 40.35 s at the combined head.

The old CI parity timeout and different local lifecycle timeouts remain transparently recorded in the earlier diagnosis. The earlier local provider result was 244/245; its lifecycle failure logs retain that attribution. The prior 49b95be5 exact gate initially hit a temp-path regex collision reproduced on unchanged base; an unchanged rerun passed. No timeout, sleep, retry, skip, pool, isolation, assertion relaxation or allowlist was introduced. New-head CI remains required; no old failure is called fixed merely because another test run passes. No inline bot replies or thread resolutions were performed.

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 6 new findings and 2 moved to a new line on 776aab8. The verdict and the full list are in the summary comment.

@@ -99,6 +99,11 @@ export async function startTransportScreenRecording<Collectible>(params: {
const { host, device, owner, input, signal, transport } = params;

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on d8bc80e.

🆕 🟡 Minor · Background validator preparation is started before the unsupported-option check, so an invalid --fps still spawns swiftc

startTransportScreenRecording fires host.screenRecording.finalize.prepare(...) with .catch(() => {}) immediately (lines 102-106). assertScreenRecordingOptionsSupported runs just before it, but the Apple and HarmonyOS runtimes call finalize.prepare in their own start paths after their own validation, and prepareRunner in createAppleApplicationTools fires it unconditionally. Consequence: a start request that is later refused (for example the signal aborts at signal.throwIfAborted(), or a start that throws before capture) leaves a swiftc compile running to its own 120 s bound with nobody owning it — this is exactly the orphaned-compiler situation the removed signal/SharedValidatorCompile cancellation existed to prevent (the deleted test said "Nothing owns swiftc once its requests end"). With cancellation gone there is no path that stops a preparation started by a request that was abandoned.

Suggested change

Bound the background compile (pass a timeoutMs to prepareVideoValidator / compileSwiftSourceText) and state in the comment why an abandoned start is allowed to leave swiftc running, since the PR also removes the cancellation input that used to cover this.

Details

persona: 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.

@@ -20,6 +23,9 @@ export function createScreenRecordingFinalizer(
host: HostEnvironment,

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on d8bc80e.

🆕 🟡 Minor · Validator preparation reads platform and cache env from the recording client's process, not the capture host's

startTransportScreenRecording, startAndroidRecording, startHarmonyRecording and both Apple start paths call host.screenRecording.finalize.prepare({ container }) (packages/capture-kit/src/screen-recording-transport.ts:102-106, packages/platform-android/src/recording/runtime.ts:68, packages/platform-harmonyos/src/recording/runtime.ts:80, packages/platform-apple/src/recording/runtime.ts:101 and 293), and createScreenRecordingFinalizer passes the host it was built with to prepareVideoValidator (src/platform-runtime-screen-recording-finalizer-host.ts:26-28). prepareVideoValidator/readPreparedVideoValidator then branch on process.platform and getSwiftCacheRoot(host.env) (video.ts:61-64, 82-85). So the container-vs-Swift choice and the cache root are properties of whichever process owns the PlatformRuntimeHost, while the recorded bytes may come from a remote transport (Limrun instances, the WebDriver/remote host). A remote or forwarded AGENT_DEVICE_SWIFT_CACHE_DIR therefore warms — and later reads authority from — a Swift cache that has nothing to do with the machine that will finalize that recording; the stop then reports video_validator_not_prepared even though a prepare ios-runner was run on the machine the hint names. The docs now tell users to run that command "on the recording host using the same Swift cache" (website/docs/docs/commands.md:1068), which the code cannot check.

Suggested change

Say explicitly which process the preparation targets (the daemon's host environment) in the prepare contract and in the video_validator_not_prepared hint, or key the preparation on the transport's host so a remote recording warms the cache its own stop will read.

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.

}): Promise<string> {
}>;

function swiftSourceTextPaths(params: SwiftSourceTextInput) {

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on d8bc80e.

🆕 🔵 Nit · Unenforced invariant: preparation and reading hash only the Swift source, while ensureSwiftExecutable compiles the written file path

swiftSourceTextPaths keys the executable on [SWIFT_CACHE_VERSION, platform, arch, source], so readCompiledSwiftSourceText resolves the same video-validator-<key> binary that compileSwiftSourceText publishes. ensureSwiftExecutable, however, runs xcrun swiftc <absoluteSourcePath> -o <temp> and the sibling compileSwiftSourceFile path hashes path.resolve(sourcePath) precisely because the absolute path is part of the produced binary. The read path is only correct while the cache root — hence the source file path — is stable for a given cache key. Two processes using different AGENT_DEVICE_SWIFT_CACHE_DIR values (test fixtures and the macos-world change now create per-run temp caches, and the PR's own first CI failure was "a temp-path regex collision") produce binaries with the same name and key but compiled from different paths; a process with cache root B can then read an executable published by a process with cache root A only if it is pointed at the same root, but nothing in the key documents the dependency. If compileSwiftSourceFile's treatment of paths is the correct one, the text path's key is missing that input.

Suggested change

Either include the resolved source path (or the cache root) in the source-text cache key, matching compileSwiftSourceFile, or add a comment at swiftSourceTextPaths stating that the source-text binary must not depend on its absolute path so the shorter key is intentional.

Details

persona: 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.

);
if (runTimeoutMs <= 0) return false;
const result = await runCmd(prepared.executablePath, [filePath], {
allowFailure: true,

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on d8bc80e.

🆕 🔵 Nit · Validator-unavailable errors from a failed run drop the stderr evidence

unavailableVideoValidator(cause?) accepts a cause, and the compile path uses it (throw unavailableVideoValidator(error)). The run path throws unavailableVideoValidator() with no cause, so when the published validator itself reports no such module 'AVFoundation' or unable to find utility "swiftc", the message that named the missing toolchain is discarded and the operator only sees the generic hint — the one case where the device-less reproduction in this PR's notes was possible is exactly the one that loses its evidence.

Suggested change
allowFailure: true,
if (isSwiftVideoValidatorUnavailable(result.stderr, result.stdout))
throw unavailableVideoValidator(
new AppError('COMMAND_FAILED', result.stderr || result.stdout),
);
Details

persona: 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.

@@ -240,7 +266,7 @@ export async function waitForPlayableVideo(

function isSwiftVideoValidatorUnavailable(stderr: string, stdout: string): boolean {

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on d8bc80e.

🆕 🔵 Nit · Trailing \b fix leaves a redundant regex alternative

Removing the trailing \b (which could never match after the closing quote, so no such module 'AVFoundation' at end of line went undetected) is the right fix, but it makes the third alternative xcrun: error: unable to find utility ["']swiftc?["'] dead: it is already a strict substring of the second alternative unable to find utility ["']swiftc?["'], so it can never match anything the second one does not. Drop it, or the next reader will assume the two differ and keep one for the wrong reason.

Suggested change
function isSwiftVideoValidatorUnavailable(stderr: string, stdout: string): boolean {
function isSwiftVideoValidatorUnavailable(stderr: string, stdout: string): boolean {
const combined = `${stderr}\n${stdout}`;
return /\b(no such module ['"]AVFoundation['"]|unable to find utility ["']swiftc?["'])/i.test(
combined,
);
}
Details

persona: 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.

}) {
const { host, transport, device, owner, input, signal } = params;
signal.throwIfAborted();
void host.screenRecording.finalize.prepare({ container: 'mp4' }).catch(() => {});

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on d8bc80e.

🔵 Nit · WebM output still requests MP4 validator preparation on three platforms

startTransportScreenRecording derives the container from the output path (input.outputPath.toLowerCase().endsWith('.webm') ? 'webm' : 'mp4') so a WebM recording does not start a Swift compile, but startAndroidRecording (packages/platform-android/src/recording/runtime.ts:68), startHarmonyRecording (packages/platform-harmonyos/src/recording/runtime.ts:80) and startAppleRunnerRecording/startAppleSimulatorRecording (packages/platform-apple/src/recording/runtime.ts:101, 293) hardcode { container: 'mp4' }. On a macOS host a .webm output path through one of those runtimes therefore spends a background swiftc compile on a validator that the stop will never consult, because readVideoContainerKind returns 'webm' and isPlayableVideo resolves it from the container alone (video.ts:186).

Suggested change

Derive the container from input.outputPath in these call sites too (or move the one call into a shared start helper), so a WebM destination never arms a Swift compile.

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.

@@ -92,9 +96,10 @@ export function createAppleApplicationTools(env: Readonly<EnvMap>): AppleApplica
await releaseIosRunnerOnClose(deviceId, options);

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on d8bc80e.

🆕 🔵 Nit · prepareRunner records startedAtMs before an untracked validator launch, so the timing fields can attribute the wrong wall clock

startedAtMs is now taken before void prepareRecording(...) and the two await load*() calls (lines 97-101). The runner diagnostic timing therefore includes time spent in work whose failure is deliberately swallowed and never reported. If the intent is that validator warm-up is invisible to the runner timings, capture startedAtMs after those two lines as before; if it is meant to be included, the prepare command metadata should say the reported phases include validator warm-up.

Details

persona: 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.

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

At 776aab8 non-macOS container authority and published-executable recovery are fixed. Two regression tests still pass without the code they protect, the macOS restoration thread still applies, and the device runs are missing. Smoke Tests and Coverage were still running, and there are no conflicts.

Could the simulator start test observe preparation entry before recorder launch and prove start resolves while preparation remains pending? The callback currently records no entry, so deleting the invocation still satisfies the assertions by source inspection. Both deleting and awaiting preparation must fail the test; keep production finalizer coverage separate from the port double. https://github.com/callstack/agent-device/blob/776aab8/packages/platform-apple/src/recording/runtime.test.ts#L638

Could the runner preparation test assert that preparation runs and the runner succeeds? Deleting the preparation call still passes by source inspection. Exercise the production finalizer with a compiler failure at its real seam too, and prove runner success and the retained typed validator failure. Deleting preparation must fail the regression test. https://github.com/callstack/agent-device/blob/776aab8/src/platform-runtime-apple-application-tools.test.ts#L119

Please provide an isolated cold daemon state run on macOS without harness prewarming for Android recording. Show capture during compilation, typed readiness expiry, successful same-session retry without another compile, playable adb pull and finalization, and daemon-restart manifest recovery without a published validator. Include commands, source SHA, device, backend and helper version, output or artifacts, and cleanup. Retain final-head hosted Linux MP4 evidence without Swift. The posted smoke covers a prewarmed normal recording, so it does not establish these paths. https://github.com/callstack/agent-device/blob/776aab8/src/platform-runtime-screen-recording-android-host.ts#L117

Please provide auditable evidence for the earlier iOS run with its SHA, or repeat it on the final source with an isolated cold daemon state. Include commands and device, compilation beginning before or alongside capture, no compilation initiated by stop, playable early and warm exports, touch telemetry and compositing, and cleanup. If readiness expires, show the typed error and successful same-session retry. The posted timing summary and prewarmed runner with hidden touches do not establish these states. https://github.com/callstack/agent-device/blob/776aab8/packages/platform-apple/src/recording/runtime.ts#L101

Could host-validator preparation be separated from Apple runner and device admission, then connected to recording admission and restored MP4 resources? Keep the authority union and executable store, prepare restored resources outside stop, and keep stop, readers, and overlays compile-free. This would allow recovery on an Android-only Mac without an iOS target and avoid compilation during stop.

The other open threads no longer apply at this head and can be resolved.

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Validation and review evidence for 6829cbf8892bd9161f0b77c958f5822312a56d78 (prior head 776aab8492c636dafba14eebe7f39bfe3e2d910c; full PR base c8ad8fb1257787ce0973cf74271cb9a4fafb51d9). This round changes four files, +40/-52; the full PR touches 46 files.

Experimental work ran only in the test worker's tool-provided isolated checkout. The parent implementation checkout stayed read-only. Setup used pnpm install --frozen-lockfile && pnpm build. No source fix, device run, or delegated work was added.

The same command ran in sequence with the supplied two-file test patch on 776aab8, then at clean 6829cbf8:

AGENT_DEVICE_VITEST_MAX_WORKERS=2 pnpm exec vitest run --project unit-core src/platform-runtime-screen-recording-finalizer-host.test.ts packages/capture-kit/src/recording/video.test.ts -t 'a verified overlay is published without another check|an unavailable validator at execution'
  • Prior: 3 failed, 34 skipped, exit 1. The 0ms case returned an unplayable-overlay warning; the 70000ms case returned a budget warning after successful publication. The diagnostic case lost stdout/stderr/exitCode and cause.
  • Head: 3 passed, 34 skipped, exit 0. The experimental test patch was reversed before returning to head.

Deletion audit used the retained suite, with the obsolete post-publication canary absent:

AGENT_DEVICE_VITEST_MAX_WORKERS=2 pnpm exec vitest run --project unit-core packages/capture-kit/src/recording/__tests__/overlay.test.ts

Baseline: 12 passed. Mutating only exportProcessedVideo's pre-rename playable guard to if (false) produced 3 failed, 9 passed. Both retained cases failed because an invalid composite was published instead of rejected:

  • overlay keeps the video as recorded when the export never plays within its budget
  • overlay keeps the video as recorded when verifying the export spent its budget

The prepared-validator forwarding assertion also failed. Reversing only this worker's mutation restored 12 passed, with clean unchanged head. These retained tests cover the boundary that can preserve the raw recording before atomic rename.

The unchanged-head gate passed under the normal shared validation lock:

AGENT_DEVICE_VITEST_MAX_WORKERS=2 pnpm check:affected --run --head 6829cbf8892bd9161f0b77c958f5822312a56d78

Receipt run-4rycRW: status: passed, exitCode: 0, exactCommit: true, checkoutChanged: false; initial/final SHA both 6829cbf8892bd9161f0b77c958f5822312a56d78, both statuses empty. Related: 1,230 passed; unit: 17,155 passed, 1 skipped; smoke contracts: 76 passed, 6 skipped; Node integration: 149 passed, 13 skipped; macOS coverage contracts: 7 passed; command docs: 12 passed. Format, lint, typecheck, layering, Fallow, build, documentation links/prose also passed. Full hosted coverage and device lanes remain GitHub-authoritative; provider integration was deduplicated against related tests. Parent-reported focused suites had 49 passed, with full format/typecheck passed.

Latest review dispositions: three fixed, six rejected.

  1. Fixed — post-publication overlay validation. The compositor verifies its temporary sibling under the deadline before atomic rename. The finalizer's second check could report overlay failure after a verified burned file was shipped. It was removed; parameterized 0/70000ms success tests replace the obsolete canary.
  2. Fixed — lost validator-run diagnostics. Unavailable errors preserve the run result's stdout, stderr, exitCode, and cause.
  3. Fixed — redundant regex branch. The utility-prefixed alternative was subsumed by the existing utility match and was removed.
  4. Rejected — Android pull must always return false without authority. Invalid media returns false before awaiting authority. Non-Darwin always selects container authority. On macOS, missing authority explicitly requires preparation using the same cache and retrying stop in the original session; compiling at stop would violate the intended policy.
  5. Rejected — invalid/aborted transport start prepares too early. Unsupported-option validation and the initial abort check precede preparation. Shared best-effort preparation remains bounded and independent of an individual request's later lifetime.
  6. Rejected — validator preparation targets a different host from finalization. Preparation and reads use the same daemon host environment. Remote bytes are downloaded locally before local finalization.
  7. Rejected — new cross-root Swift cache collision. Mutable source-file behavior predates this change. Distinct cache roots have distinct executable paths; this change does not create a cross-root read collision.
  8. Rejected — native producers should select WebM authority from a suffix. Android, HarmonyOS, and Apple native producers emit actual MP4 regardless of destination suffix. Preparation follows the producer's container.
  9. Rejected — runner timing misattributes validator work. prepareRunner's startedAtMs is the deadline origin and intentionally includes launch/import overhead; it is not a reported phase timing.

The independent size review at 776aab8 assessed +3.0 kB unpacked / +803 B compressed, approximately +75 production lines, no new dependencies, and accepted the coherent smallest design. This round removes redundant code. Size measurements belong to 776aab8.

Prior evidence remains attributed to 776aab8: hosted integration/coverage and Apple/Android/Linux smoke passed; local provider integration had 245 passed; live Android had six scenarios plus bootstrap passed. No fresh device/provider run or natural-deadline/race reproduction is claimed. New-head hosted CI and review approval remain pending.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 2 new findings and 5 moved to a new line on d8bc80e. The verdict and the full list are in the summary comment.

Comment thread website/docs/docs/commands.md Outdated
- iOS recording works on simulators and CoreDevice-backed physical devices. Physical devices capture runner screenshots, not a native video stream: no audio, dropped frames are expected, and the default is 15 FPS. `--fps <n>` (1-120) caps the rate; a busy app or long command yields fewer frames.
- `--quality medium|high` (default `medium`) sets export quality on Android and Limrun; Apple export always keeps the captured resolution. Numeric `5`-`7` map to `medium` and `8`-`10` to `high`.
- With touch visualization on, recordings also produce a gesture telemetry sidecar. Burned-in touch overlays export only on macOS hosts; elsewhere `record stop` returns the raw video and telemetry with `overlayWarning`. The overlay renders at most 30 FPS (or `--fps` if lower) within a fixed budget, which also covers the video checks that run before and after it; an export past the budget returns the raw video with `overlayWarning`. On a host that has not run a recording before, part of that budget goes to one-time tool setup, so the first `record stop` of a long recording is more likely to return the raw video.
- On macOS, MP4 `record start` warms the video validator in the background without delaying capture. `agent-device prepare ios-runner --platform ios` also warms it without waiting or failing on build errors. `record stop` waits within its finalization budget. If preparation is still pending, stop returns retriable `video_validator_not_prepared`; preparation continues, so retry stop in the same session. macOS MP4 verification requires Xcode command line tools and AVFoundation; unavailable tooling returns `video_validator_unavailable` only at stop. Other hosts validate the MP4 container without Swift. If preparation is missing or failed, install any missing tooling, then run `agent-device prepare ios-runner --platform ios` on the recording host using the same Swift cache. Preparation runs in the background; once it is ready, retry `record stop` in the original recording session. WebM validation needs no Swift helper.

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on 9bf6505.

🆕 🟡 Minor · Docs still say the overlay budget covers a video check that runs after the overlay, and the removed post-overlay playability probe is undocumented

src/platform-runtime-screen-recording-finalizer-host.ts no longer calls isPlayableVideo after overlayRecordingTouches (the if (!(await isPlayableVideo(...))) throw new Error('recording post-processing produced an unplayable video') block is deleted), and the rewritten finalizer test now asserts expect(video.isPlayable).not.toHaveBeenCalled() after the overlay. But website/docs/docs/commands.md:1068-1069 still says the fixed budget "also covers the video checks that run before and after it" — a check that no longer runs. The same section keeps the sentence "On a host that has not run a recording before, part of that budget goes to one-time tool setup, so the first record stop of a long recording is more likely to return the raw video", which the PR's own overlay.ts comment rewrite (validator preparation no longer comes out of the budget) makes stale, and which now duplicates the new validator-warming bullet directly above it.

Suggested change
- On macOS, MP4 `record start` warms the video validator in the background without delaying capture. `agent-device prepare ios-runner --platform ios` also warms it without waiting or failing on build errors. `record stop` waits within its finalization budget. If preparation is still pending, stop returns retriable `video_validator_not_prepared`; preparation continues, so retry stop in the same session. macOS MP4 verification requires Xcode command line tools and AVFoundation; unavailable tooling returns `video_validator_unavailable` only at stop. Other hosts validate the MP4 container without Swift. If preparation is missing or failed, install any missing tooling, then run `agent-device prepare ios-runner --platform ios` on the recording host using the same Swift cache. Preparation runs in the background; once it is ready, retry `record stop` in the original recording session. WebM validation needs no Swift helper.
- With touch visualization on, recordings also produce a gesture telemetry sidecar. Burned-in touch overlays export only on macOS hosts. The overlay renders at most 30 FPS (or `--fps` if lower) within a fixed budget, which also covers the video check that runs on the composited output before it replaces the raw; an export past the budget returns the raw video with `overlayWarning`.

Suggested change

Update the record touch-overlay bullet to say the budget covers the video checks that run before the overlay (the composited output is verified before it is renamed over the raw), and drop or rewrite the one-time-tool-setup sentence now that validator preparation is warmed at start rather than paid at stop.

Details

Also raised here

Also raised at this location by maintainability: Updated commands.md still says the overlay budget covers video checks "after" the overlay, which this change removed — The rewritten bullet keeps "which also covers the video checks that run before and after it" (website/docs/docs/commands.md:1069), but this change deleted the post-overlay isPlayableVideo call from overlayTouches in src/platform-runtime-screen-recording-finalizer-host.ts and renamed its test to "a verified overlay is published without another check". AGENTS.md: "When behavior changes, update the owning help and user docs." Drop "and after" so the page does not promise a verification that no longer runs.

persona: 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.

deadline?.remainingMs() ?? VALIDATOR_RUN_TIMEOUT_MS,
);
if (runTimeoutMs <= 0) return false;
const result = await runCmd(prepared.executablePath, [filePath], {

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on 9bf6505.

🆕 🟡 Minor · Behavior change: an unavailable validator used to make MP4s read as playable, now it hard-fails the stop

The predecessor's isPlayableVideo ended with return isSwiftVideoValidatorUnavailable(result.stderr, result.stdout) for a non-zero exit, and its catch returned true for any TOOL_MISSING / missing-module error — i.e. "cannot verify, assume playable", so a macOS host without working Swift tools still completed stops. The new code inverts that: a non-zero validator run whose stderr matches the unavailable pattern throws unavailableVideoValidator (retriable: false, video.ts:236-240), a thrown TOOL_MISSING likewise (video.ts:232), and an unprepared authority throws video_validator_not_prepared (video.ts:198-204). waitForPlayableVideo does not catch these, so finalizeScreenRecording aborts with them instead of returning the recording.

Suggested change

Add a line to the record help/docs noting that a validator that cannot run now fails the stop instead of publishing the recording unverified, and add a finalizer test for the unavailable-at-execution error reaching the caller.

Details

Concrete case: a macOS host where AVFoundation is broken (e.g. a Swift cache whose validator binary exists but fails to load the module). Every MP4 record stop on that host now fails with non-retriable video_validator_unavailable and the recording is never published, where the previous build published it with an unverified-compliance note. The new command help mentions video_validator_unavailable, but no doc line or migration note says previously-succeeding stops on such hosts now fail, and no finalizer test covers video_validator_unavailable propagating through complete (only not_prepared is covered there).

persona: 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.

// 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>();
export type VideoValidator = PreparedVideoValidator | Promise<PreparedVideoValidator>;

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on 9bf6505.

🆕 🔵 Nit · videoValidatorPreparations grows one entry per Swift cache root for the life of the process

packages/capture-kit/src/recording/video.ts:50-54 keys videoValidatorPreparations by getSwiftCacheRoot(host.env) and never evicts: a failed entry is only marked failed and replaced, and a successful entry keeps its resolved promise plus the frozen env from buildSwiftToolEnv. The removed videoValidatorCompiles map deleted its entry once a compile settled. A daemon that serves sessions with different AGENT_DEVICE_SWIFT_CACHE_DIR values (per-session or per-test-harness caches) retains every one of them plus a rejected promise whose only handler is the internal flag setter.

Suggested change

Bound the map (delete a failed entry once it has been observed, or key/evict per session) so long-lived daemons do not retain every cache root, env and settled promise they have ever prepared.

Details

persona: 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.

: preparing?.promise;
}

function preparedVideoValidator(

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on 9bf6505.

🆕 🔵 Nit · Removed the now-dead buildSwiftToolEnv import path from video.ts's validator run while keeping the import

packages/capture-kit/src/recording/video.ts still imports buildSwiftToolEnv (line 6) and uses it only inside preparedVideoValidator (line 79), while isPlayableVideo now runs the executable with prepared.env captured at preparation time. That is correct, but it means the env captured by a background preparation is reused for a validator run arbitrarily much later; a validator run no longer sees the caller host's env. Worth one comment on preparedVideoValidator saying the env is the preparation host's, not the recording host's, since the two differ whenever the daemon serves per-session envs.

Suggested change

Add a short comment on preparedVideoValidator naming that env is frozen from the preparation host's environment and reused by every later validation run, or rebuild the tool env per run from the recording host.

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.

@@ -92,9 +96,10 @@ export function createAppleApplicationTools(env: Readonly<EnvMap>): AppleApplica
await releaseIosRunnerOnClose(deviceId, options);

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on 9bf6505.

🆕 🔵 Nit · prepareRunner now charges dynamic module loading to startedAtMs

src/platform-runtime-apple-application-tools.ts:99-100 hoists const startedAtMs = Date.now(); above the await loadRetry() / await loadRunnerOperations() imports that used to precede it, so the first cold call folds @agent-device/host-kit/retry and the runner-operations module load into the wall-clock figure the prepare output reports. The prepare metadata this PR just edited spells out which timings are additive, so an inflated phase total is worth a deliberate call.

Suggested change

If startedAtMs is meant to bound runner work only, keep it after the module loads; if the intent is full prepare wall-clock, note that in the prepare command help so the timing fields stay comparable across versions.

Details

persona: 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.

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Validation/publication evidence for e6b8b205087372975bc4a319b45d20eaba0b84c0 (base c8ad8fb1257787ce0973cf74271cb9a4fafb51d9, previously published d8bc80e0163ef0942a9d34ff03917dc71b6ebb89). All work used the isolated checkout /Users/michal/.t3/worktrees/agent-device/t3-27ede1e0. The parent checkout was read-only. No devices or subagents were used.

The original d8 Coverage job failed in launch-confirmation.test.ts: untilSpawned expected 2 spawns but received 1 (lines 259/297). The run had 18,355 other tests passed and 60 skipped; test execution aborted before coverage completion. This was not a coverage-threshold failure. The natural CI cause remains unproven; no infrastructure attribution or unchanged CI rerun is claimed.

Controlled synchronization proof used the same command and environment at d8 and e6:

AGENT_DEVICE_VITEST_MAX_WORKERS=2 pnpm exec vitest run --project unit-core packages/platform-apple/src/launch-confirmation.test.ts -t 'one Info.plist read hangs after the app list answered'

A temporary partial vi.mock('@agent-device/host-kit/host-file', async importOriginal => ...) preserved every actual export and wrapped only hostFileStat('/apps/Example.app/Info.plist'): await 2,000 real setImmediate turns, then call the actual stat. Real file I/O was retained. Fake timers still covered only setTimeout/clearTimeout/Date. The identical mock was used at both heads, without a timeout/budget increase.

  • Unmodified d8 control: 33 passed.
  • d8 plus delay: exit 1, 1 failed/32 skipped; untilSpawned expected 2, received 1 after its 1,000-turn polling limit.
  • e6 plus identical delay: exit 0, 1 passed/32 skipped; second-spawn assertion verified 2.
  • Mock removed: 33 passed.

Retained deadline proof at e6 used the same two-worker command with -t 'the owner lookup is bounded as a whole': baseline 2 passed/31 skipped. A temporary production mutation changed only createLaunchConfirmationPort.resolveUrlOwner timeout from URL_OWNER_LOOKUP_TIMEOUT_MS to URL_OWNER_LOOKUP_TIMEOUT_MS * 2. Both cases failed (exit 1, 2 failed/31 skipped), expecting { outcome: 'unreadable', step: 'url-owner' } but receiving pending at the original 10-second budget plus 1 ms. After removing only that mutation, the full launch suite passed 33/33. No experimental patch or budget change is published.

Setup/install/build and full-repository format passed. Exact unchanged-head gate:

AGENT_DEVICE_VITEST_MAX_WORKERS=2 pnpm check:affected --run --head e6b8b205087372975bc4a319b45d20eaba0b84c0

Receipt run-TTDHa4: status passed, exit 0, exactCommit: true, checkoutChanged: false; initial/final head e6 and empty status, merge base c8ad8fb. Counts: related 1,230 passed/0 skipped; unit 17,155 passed/1 skipped; Node integration 149 passed/13 skipped; smoke 76 passed/6 skipped; macOS manifest check 7 passed/0 skipped; command docs 12 passed/0 skipped. Format, lint, typecheck, layering, Fallow, build, doc links and prose all passed. Coverage, web smoke, macOS/Linux replay and Linux command evidence were skipped as GitHub-authoritative; iOS/iOS-device/Android replay were parked manual lanes. Provider integration was deduplicated into related tests, not rerun as a separate provider lane.

The independent read-only d8..e6 review was clear. This scope expansion changes only CI launch-test synchronization in one file (+12/-10); production is unchanged. Earlier review dispositions remain recorded, and the already-resolved Cubic invocation thread is unchanged. No Apex/Cubic threads were touched.

Prior 776aab8 hosted integration/coverage, Apple/Android/Linux smoke, 245 provider tests, and live Android six scenarios plus bootstrap retain that earlier SHA attribution. The prior narrowed 33-test coverage diagnostic exited 1 on global thresholds and is not a full coverage pass. New-head full coverage, hosted CI/device lanes, and review approval remain pending.

Logs, exact experimental patches, drafts, receipt and handoff are saved under .tmp/4188-launch-sync/ in the isolated checkout.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Validated and published 9bf650575c73cdefdb9db8af6fe8a1c66126a946 from my isolated checkout, replacing only the expected remote e6b8b205087372975bc4a319b45d20eaba0b84c0 with one explicit force-with-lease push.

This round adds two parent-authored cleanups across two files (+4/-3); overall scope is 47 files (+841/-436). Production behavior is unchanged from e6. The parent supplied the cold review of these two files; I did not perform a new independent review. Earlier architecture and launch-test reviews remain attributed to their original runs.

Nine Apex dispositions

These are the parent's dispositions of the nine findings at d8, carried forward with the two accepted cleanups in 9bf:

  1. Failed preparation cached forever — rejected. prepareVideoValidator replaces a failed entry on explicit preparation; readPreparedVideoValidator checks disk again. Existing contention/unavailable-compiler tests cover retry and publication by another process. Documented recovery requires explicit preparation; stop must not compile.
  2. Android probe throws — rejected. Non-macOS uses container authority. On macOS, absent authority or unavailable tools produces the documented typed preparation error; invalid media returns false before authority lookup. Treating the prerequisite error as a media retry would lose actionable information.
  3. Overlay budget wording — accepted and clarified. Commands documentation now states that raw video and the exported composite are validated before saving. Cold overlay helper setup still consumes the budget because exportProcessedVideo compiles that helper.
  4. Unavailable execution fails stop — rejected as a new omission. Existing help/docs require Xcode/AVFoundation on macOS and describe video_validator_unavailable at stop. Video tests cover unavailable stderr and a removed prepared executable; the finalizer propagates the wait error.
  5. Removed post-overlay probe — rejected. exportProcessedVideo validates the temporary composite before atomic rename. Retained raw-preservation tests detected three failures when that boundary was bypassed. Rechecking after publication can exhaust the deadline and misreport a successful overlay; a hypothetical external writer does not justify a second check.
  6. Map grows per cache root — rejected for the claimed production trigger. The daemon captures one host environment and does not supply per-session cache roots. Test roots do not justify a competing session eviction owner.
  7. Dead buildSwiftToolEnv import — rejected. preparedVideoValidator uses it to bind executable authority and environment; prepare/read share the daemon host environment.
  8. Compiler fixture missing -o — accepted and fixed. The fixture now requires a final -o plus nonempty output path. A missing flag cannot select args[0] and write ./swiftc.
  9. Preparation module-loading time — rejected. startedAtMs defines the deadline origin, including launch/import overhead, while phase timings remain separate.

Validation and attribution

  • Own checkout install, build, and full format passed; formatting left it clean.
  • Fixture-consuming provider files ios-record-trace.test.ts, macos-recording.test.ts, and android-recording.test.ts: 3 files, 14 tests passed, two workers. No devices used.
  • Optional temporary malformed-compiler regression experiment omitted; no independent old-fail/new-pass claim for this cleanup.
  • AGENT_DEVICE_VITEST_MAX_WORKERS=2 pnpm check:affected --run --head 9bf650575c73cdefdb9db8af6fe8a1c66126a946: passed, exit 0; receipt run-JBYTJV, exactCommit: true, checkoutChanged: false. Related 1,230 passed; unit 17,155 passed/1 skipped; Node 149/13; smoke 76/6; macOS manifest 7 passed; command docs 12 passed. All other runnable checks passed (format, lint, typecheck, layering, Fallow, build, docs links/prose).
  • Prior e6 publication and launch-test proof: controlled identical slow-stat command produced d8 1 failed/32 skipped (expected two spawns, got one), e6 1 passed/32 skipped; deadline baseline 2 passed/31 skipped, 2× timeout mutation 2 failed/31 skipped, then restored full launch suite 33 passed. These are prior isolated worker results, not new experiments here. Prior 776aab8 hosted integration/coverage and Apple/Android/Linux smoke passed; providers 245 passed, live Android six scenarios plus bootstrap passed. Those runs were not repeated here.
  • Original d8 Coverage job failed in the launch test. Its natural CI cause is unproven. The e6 launch-test synchronization fix preserves the deadline; no natural Linux race or new-head coverage pass is claimed. New-head full coverage, hosted/device/compatibility obligations and approval remain pending.

Apex threads were left untouched. The already-resolved Cubic invocation thread was left untouched. No CI polling, watches, merge, labels, bot messages, or thread replies/resolutions were performed.

Exact-head local receipt and skips
  • Receipt: run-JBYTJV, status passed, exit 0.
  • Requested/resolved head: 9bf650575c73cdefdb9db8af6fe8a1c66126a946.
  • Merge-base: c8ad8fb1257787ce0973cf74271cb9a4fafb51d9; resolved origin/main: a2c169fa502a53f1f2a1da25b1cd16b9c731af39.
  • Initial/final checkout head matches; status empty; exactCommit=true, checkoutChanged=false.
  • Provider integration deduplicated: covered by related tests.
  • Coverage skipped locally: GitHub-authoritative (Coverage job).
  • Web smoke skipped locally: GitHub-authoritative (Apple / Smoke Tests, Integration Tests, Lint, Typecheck & Package, Perf Nightly / Android Command Perf, Perf Nightly / iOS Command Perf).
  • macOS replay skipped locally: GitHub-authoritative (Apple / Smoke Tests).
  • Linux replay and command evidence skipped locally: GitHub-authoritative (Linux / Smoke Tests).
  • iOS replay, iOS device replay, Android replay: parked, workflow_dispatch only (Replay Manual suites).

Full receipt, test reports, logs and drafts retained in own checkout under .tmp/4188-final-cleanup/.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread website/docs/docs/commands.md Resolved

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 1 new finding and 3 moved to a new line on 9bf6505. The verdict and the full list are in the summary comment.

@@ -109,7 +112,9 @@ export async function createAndroidScreenRecordingTransport(
});

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on f026759.

🆕 🟠 Major · Android recording stop on a macOS host fails when the background validator compile needs more than 10s

isPlayableVideo now bounds its wait for a pending validator by deadline?.remainingMs() ?? VALIDATOR_RUN_TIMEOUT_MS (10s, video.ts:213). The Android pull path passes no deadline at all, so a record stop that arrives while the background compile started at record start is still running waits at most 10s and then throws video_validator_not_prepared. This diff's own removed comment states a cold Swift module compile takes over 30s on a loaded runner, so on a macOS host with a cold Swift cache the first (short) Android recording reliably fails: pullPlayable rejects, pullPlayableChunk propagates that rejection out of its retry loop (chunks.ts:246-250) instead of its own 'failed to retrieve playable Android recording', and stop returns an error. The predecessor waited for the shared compile (up to the caller deadline, or 30s without one) and succeeded. record stop retries hit the same 10s bound until the compile happens to finish, so the failing window is the whole cold compile. Please give this call site an explicit deadline (or let the pull path wait the pull budget) instead of the 10s validator-run constant.

Suggested change

Either pass an explicit Deadline (the stop/pull budget) to isPlayableVideo from the Android pullPlayable transport, or keep a longer default wait for callers without a deadline, and add a unit test that a pending compile does not fail pullPlayable inside the pull budget.

Details

persona: 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.

await releaseIosRunnerOnClose(deviceId, options);
},
prepareRunner: async (device, input, signal) => {
const startedAtMs = Date.now();

@apex-sentinel-staging apex-sentinel-staging Bot Nov 3, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer raised on f026759.

🆕 🔵 Nit · Background validator preparation is fired and swallowed twice

prepareRunner wraps an already-best-effort call in a second silent catch: void prepareRecording({ container: 'mp4' }).catch(() => {}), while createScreenRecordingFinalizer.prepare itself never awaits and never rejects (if (input.container === 'mp4') void prepareVideoValidator(host);). Same double void + .catch(() => {}) pattern appears at every start site (transport, Android, Apple simulator, Apple runner, HarmonyOS). The contract in ScreenRecordingFinalizer already says preparation is best-effort, so the extra swallow hides where the failure is deliberately dropped and makes it look as if prepare can reject. Keep the swallow at exactly one owner (the contract implementation) and let callers just void ...prepare(...), or let prepare return the promise so callers can await it in tests without the void.

Details

persona: 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.

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

At 9bf6505, the earlier review’s transport ordering, authority forwarding and error handling issues are fixed, but the preparation regressions and live evidence remain outstanding. The ownership design adds no new follow-up complexity, though the restoration issue remains.

The Apple start test never observes the pending preparation callback. Source deletion analysis shows that removing the invocation leaves its assertions passing, so cold-start warming remains unprotected as at d8bc80e. Can the test observe preparation before recorder launch and prove start succeeds while it remains unresolved? Deleting or awaiting the invocation must fail for the intended reason.

The runner preparation test does not assert that preparation is entered, so removing warming still passes the runner assertions. The finalizer test separately mocks the validator and does not prove compiler failure through composition. Can the test assert preparation entry and runner success, and can the production finalizer and video owner be tested with a rejected compiler seam to prove a retained typed validation failure without failing runner preparation? Removing warming must fail the regression.

The Android pull route lacks an auditable cold macOS run through pull and finalization; the reported smoke prepares authority before capture. Please post the source SHA, exact commands, named device, android-helper backend and helperVersion, outputs, artifacts and cleanup. Show capture during pending compilation, typed early readiness expiry, successful same-session retry without a second compile, playable adb pull and finalization, and manifest recovery after daemon restart before publication. Retain attribution for hosted non-macOS MP4 validation without Swift.

The earlier Air timing summary lacks an auditable transcript, and the posted iOS harness prepares the validator before capture and hides touches. Please supply its source SHA, named device, commands, outputs, artifacts and cleanup, or repeat on final production source with an isolated cold daemon state. Show preparation before or alongside capture, no compile initiated by stop, playable early and warm exports, and touch telemetry and compositing. If readiness expiry is induced, show its typed reason and successful same-session retry.

The open threads on restoration, restoration admission, test diagnostics, overlay documentation, and Android readiness budget still stand. The other open threads no longer apply at this head and can be resolved.

A background video-validator compile kept running after the daemon
exited. process.exit() ended the supervisor while swiftc was still a
live child, so the daemon left an orphaned compiler and a held cache
lock. The shutdown path had no owner for the preparation because the
module-level prepare/read API did not track the running promise.

Give the preparation one owner per runtime host.
createVideoValidatorPreparation() holds the promise and an
AbortController, so close() can cancel it and await the settlement.
The host closes that owner while it shuts down, before the Apple runner
sweep. Late host imports close the owner the import produced, so a
warm-up that starts after shutdown compiles nothing.

Restore signal propagation from compileSwiftSourceText through
ensureSwiftExecutable into the cache lock and runCmd, so cancellation
terminates the compiler child and removes the lock and temp paths.
Cross-process dedup stays with the filesystem cache lock. Stop no
longer looks like a cancelled compile, and one host closing does not
cancel another host's compiler.
@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Validation evidence for 7ff691ca6cf6d8473c32f3ce5531f42e084faf38 (base c8ad8fb1257787ce0973cf74271cb9a4fafb51d9): check:affected --run --head receipt run-5sEC4z passed with exactCommit: true, checkoutChanged: false; 13 passed, 8 skipped, 1 deduplicated. Related tests: 1,230 passed; unit: 17,181 passed, 1 skipped. Focused finalizer/shutdown tests: 21 passed. Frozen install, build and format passed.

At 4394ea2c5b74b1fb1008c89d0280af52fa870438, whose production code is unchanged at this head, real-child regression replay against e5cde023d17ed17656af2fe5d9f5a955e052f085 yielded 3 failed/1 passed on base and 4 passed on head; owned PIDs and temporary roots were absent afterwards. Cleanup suite baseline: 93 passed; three mutants produced 3, 2 and 1 failures respectively, with restored tests green. Independent lifecycle and normalized-output reviews at 4394 and final-diff review at 7ff found no concrete regressions.

Hosted coverage, device and startup validation remain pending at this head. Older b92 startup/hash evidence does not validate this production-changing head.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 21 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/owned-fake-compiler.fixtures.ts Resolved
Comment thread website/docs/docs/commands.md Resolved
@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Validation evidence for f026759d6f4a06d460a7a9e566e03f5836a62e3e (base c8ad8fb1257787ce0973cf74271cb9a4fafb51d9): the run-meToRk affected-check receipt passed at the exact commit with unchanged checkout: 13 passed, 8 skipped, 1 deduplicated; related 1,230 passed; unit 17,187 passed, 1 skipped. Hosted coverage and device lanes remain pending. The startup CI increase at 7ff did not reproduce in bounded macOS base/head/base comparisons; its cause is unproven.

Independent reviews cleared the final delta, including the prior cleanup finding. The real-lock test covers two in-flight owners and warm reuse. Baseline and restored proof runs passed both tests. Removing second-compiler abort produced the explicit bounded cleanup failure and the exact cache was removed. A shared-controller mutation was detected by the bounded compiler-count poll (expected 2, got 1) and the slow-test guard; this is timing-based detection, not an immediate non-timeout failure. All eight roots recorded in the final proof were absent. The implementation handoff also checked its recorded real-child roots and PIDs absent; this does not account for all historical experiments.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 2 new findings and 5 moved to a new line on f026759. The verdict and the full list are in the summary comment.

} from '@agent-device/capture-kit/recording-video';
import { loadAndroidMechanics } from './platform-runtime-android-mechanics.ts';

const ANDROID_MANIFEST_NAME = 'agent-device-recording-active.json';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor · Android/Harmony MP4 stops on a macOS daemon now depend on Apple validator preparation and lose the old graceful pass

createAndroidScreenRecordingTransport now hands the Apple validator owner to isPlayableVideo (src/platform-runtime-screen-recording-android-host.ts:16,114): isPlayableVideo(outputPath, validator.read()), with no deadline. The host it used to receive is gone, so an Android (or HarmonyOS, whose collected MP4 goes through the same finalizer) recording validated on a macOS daemon is validated by the AVFoundation executable chosen by createVideoValidatorPreparation for the daemon's Swift cache. Two behaviors the predecessor had are lost for that caller: (1) it compiled the validator on demand (old waitForValidatorCompile, bounded by 30 s or the caller's deadline), whereas now read() answers undefined when this daemon never prepared, so the Android transport's playable check throws video_validator_not_prepared and the stop fails; (2) a TOOL_MISSING / missing-AVFoundation host previously returned true from isPlayableVideo (old isSwiftVideoValidatorError → return true), so a machine without Xcode tooling still reported and kept its Android recording; now video.ts:264-270, 296-306 raise video_validator_unavailable with retriable: false, and in waitForPlayableVideo that throw escapes the polling loop and fails the whole stop. So an Android record stop on a macOS host that lacks AVFoundation, or whose Apple preparation never ran, now errors out on a perfectly good MP4 — and the hint it shows tells the operator to run agent-device prepare ios-runner --platform ios, an Apple-runner command, for an Android capture. The docs sentence "Other hosts validate the MP4 container without Swift" describes non-macOS hosts only, so it does not cover this daemon. Consider letting non-Apple transports request container-only authority ({ kind: 'container' }) instead of the shared AVFoundation owner, and returning the old graceful true where an unavailable validator previously degraded, or making video_validator_unavailable retriable.

Details

persona: 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.

throw error;
}
if (!(await waitForPlayableVideo(outputPath, { host, deadline }))) {
if (!(await waitForPlayableVideo(outputPath, { validator: params.validator, deadline }))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit · Overlay discards a successfully exported composite when validator preparation is still pending

finalizeScreenRecording reads the preparation once (const prepared = validator.read()) and hands that same promise to waitForPlayableVideo both for the raw check and, through overlayTouches, to exportProcessedVideo. The predecessor verified the composited output with the host (waitForPlayableVideo(outputPath, { host, deadline })), which compiled the validator on demand, so the check always had an authority by the time the export finished.

Suggested change
if (!(await waitForPlayableVideo(outputPath, { validator: params.validator, deadline }))) {
if (!(await waitForPlayableVideo(outputPath, { validator: params.validator, deadline }))) {
exportOverran = deadline.isExpired();
throw new AppError(
'COMMAND_FAILED',
isVideoValidatorNotPreparedError(error) ? error.message : 'The composited video was not verified as playable',
);
}

Suggested change

Do not reuse the pending preparation promise as the authority for verifying the composited output. Either re-read validator.read() after the export (so an executable published during the export is used), or skip the composite verification with an explicit typed reason instead of deleting a composite the helper already produced.

Details

Now, after a long overlay export the deadline has little left, and isPlayableVideo races the still-pending preparation promise against deadline.remainingMs(). If preparation has not settled within what the overlay left of OVERLAY_BUDGET_MS, the race rejects with video_validator_not_prepared, exportProcessedVideo treats it as The composited video was not verified as playable, deletes the exported file in its finally, and the stop returns the raw video with an overlayWarning — even though the helper exported the composite successfully. The same happens when read() answered undefined (nothing prepared and nothing published): the raw-video check in finalizeScreenRecording throws out of complete, so showTouches stops can only ever keep the raw video.

The verified composite is thrown away for a reason unrelated to the export, and the user-visible fix (retry stop) re-runs the whole export.

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.

@@ -177,9 +190,16 @@ async function ensureSwiftExecutable(params: {
const executableDir = path.dirname(params.executablePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit · ensureSwiftExecutable now waits for a canceled compile instead of releasing the Swift cache lock

swift-cache.ts replaced withProcessLock with a manual acquireSwiftCacheLock + try/finally-style release, and the release now happens after the compile task settles. Under the old withProcessLock, acquire and task were separate, so canceling a compile (AbortSignal, or the timeout in the new test at swift-cache.test.ts:175) released the lock while swiftc was still being killed. Now a canceled or timed-out compile holds ${executablePath}.lock until runCmd's SIGINT grace (graceMs: 2_000 per the new assertion) plus process teardown have passed, and only then gives the lock back. createVideoValidatorPreparation.close() and compileSwiftSourceText's own timeoutMs both take that path, so a second owner waiting on that cache must now wait the full kill-grace on top of timeoutMs (default 120_000) before it can compile — and the lock-acquire wait inside it uses the same timeoutMs, so it can time out with process_lock_timeout where it previously succeeded. The video-validator owner deliberately cancels its compile on shutdown (video.ts close), which is exactly the case that leaves the lock held for seconds.

Suggested change

Release the cache lock as soon as the compile stops waiting (before awaiting the killed compiler's exit), or document and bound the extra lock hold time a canceled compile now imposes on the next owner.

Details

persona: 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.

}

export async function waitForStableFile(
filePath: string,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆕 🔵 Nit · isUnavailableVideoValidatorError reuses arbitrary compile stderr as the reason an MP4 is not playable

The new check accepts any AppError whose details.stderr happens to contain the AVFoundation/swiftc phrase (isSwiftVideoValidatorUnavailable(String(error.details?.stderr ?? ''), …)) and rethrows it through unavailableVideoValidator, whose spread ...(cause instanceof AppError ? cause.details : {}) copies that untrusted stderr/stdout straight into the user-facing failure details (the new video.test.ts case asserts details.stderr is the whole message). The same classifier is then applied to the validator run's output at video.ts:296 to decide whether the video is playable. A command failure or a wrapper error that echoes a compiler log containing no such module 'AVFoundation' — e.g. a cached stderr from a different source file, or a build log with several modules — is reported as video_validator_unavailable, retriable: false and permanently refuses the recording until the toolchain is re-prepared, even though the container itself was fine.

Details

persona: 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.

const validatorPath = await waitForValidatorCompile(
host,
deadline?.remainingMs() ?? VALIDATOR_COMPILE_WAIT_MS,
const notPrepared = () =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit · A stop whose preparation never started returns retriable video_validator_not_prepared, but nothing on the stop path ever starts it — retries cannot recover

isPlayableVideo has two different video_validator_not_prepared errors. The deadline one (video.ts:254-263) says "Preparation continues in the background. Retry record stop in this session once the validator is ready." — true only when a preparation is actually in flight. The no-validator one (video.ts:264-270) is raised when VideoValidatorPreparation.read() answered undefined, which by its own contract (video.ts:105-117) means this host never prepared anything and no executable is published in the cache. createVideoValidatorPreparation.prepare() is only ever reached from a recording start (createScreenRecordingFinalizer.prepare is called from startTransportScreenRecording, startAndroidRecording, startAppleSimulatorRecording, startAppleRunnerRecording, startHarmonyRecording, prepareRunner). So a recording recovered by a restarted daemon — the documented recovery path where record stop finishes a recording a previous daemon left — has a host whose validator promise was never created, read() returns undefined, and every retry of record stop re-raises the same retriable error forever: no stop-path code calls prepare(), so the background preparation the error tells the user to wait for does not exist. Sequence: macOS daemon starts an MP4 recording, is killed mid-compile (its unpublished output and lock are reclaimed but nothing was published), the new daemon's record stop on the recovered recording fails with video_validator_not_prepared / retriable: true, and record stop retried N times produces the identical error; only an unrelated Apple command (prepare ios-runner --platform ios) or a fresh record start clears it. Either start preparation from finalizeScreenRecording when read() answers undefined (bounded by the finalization deadline, as before), or mark that outcome non-retriable and name the command that actually starts it, as the sibling error already does.

Details

persona: 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.

const { host, device, owner, input, signal, transport } = params;
assertScreenRecordingOptionsSupported(input, params.support, params.unsupported);
signal.throwIfAborted();
void host.screenRecording.finalize

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit · Container kind is inferred from the output path in two places that will drift

input.outputPath.toLowerCase().endsWith('.webm') ? 'webm' : 'mp4' is added in screen-recording-transport.ts:102-106, and provider-limrun/recording-runtime.ts:62 adds the same input.outputPath.toLowerCase().endsWith('.webm') test as a rejection condition. hasVideoContainer already owns container naming in capture-kit/recording-video. Keep one helper (e.g. recordingContainerFor(outputPath)) next to the container reader so a third consumer — or a change to what counts as WebM — cannot make the transport warm the validator for a container the backend refuses.

Suggested change

Extract the path-to-container decision into one shared helper and use it from the transport warm-up and the Limrun argument check.

Details

persona: 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.

const host = options.host;
// One preparation owner for this host, created when a recording first asks for it. Compilation
// across runtimes and processes stays with the Swift cache lock, not a second lock database.
let validator: Promise<VideoValidatorPreparation> | undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆕 🔵 Nit · Validator preparation selects container vs AVFoundation from process.platform at call time, not from the host it was built with

createVideoValidatorPreparation(host) captures a HostEnvironment (its env selects the Swift cache root) but chooses container-vs-AVFoundation authority from the live process.platform on every prepare()/read() call, while swiftSourceTextPaths hashes process.platform and process.arch into the cache key. createScreenRecordingRuntimeHost builds exactly one owner from the host under construction and keeps it for the host's lifetime, so the authority a stop uses depends on process.platform at the moment of the call rather than on the host the owner was created for.

Suggested change

Decide the validation kind once, from the host the preparation was created for (e.g. keep a platform captured at construction alongside host), and use that same value for both prepare() and read() and for the cache key, so the authority read back always matches the authority prepared.

Details

A host object captured for a different platform (the tests in this PR exercise exactly this by redefining process.platform, and platform-runtime-operation-host-validator-shutdown.test.ts does so for the whole file) therefore answers prepare() with an AVFoundation compile while read() returns container authority — or vice versa — and the compile that prepare() started is never read back. Nothing in the type or the code ties the chosen kind to the host the owner was constructed with, so the invariant holds only while process.platform never changes for that host.

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.

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

The review of f026759 still finds gaps in warming coverage, shutdown diagnostics, and live recording evidence. The bounded launch assertion, non-macOS documentation, owner isolation, and compiler fixture issues from the earlier review are fixed; the warming regressions remain.

The pending-preparation test never observes entry into preparation, so deleting the start invocation still satisfies its assertions. A future loss of warming could make cold macOS MP4 stops fail while the test passes. Please assert preparation entry before recorder launch and that capture start resolves while preparation remains pending; verify that deleting or awaiting the invocation fails for the intended reason.

The runner test also never asserts entry into its throwing preparation callback, so deleting warming still passes. The requested compiler-failure composition remains absent: the finalizer test substitutes a preparation stub, and the new deadline test covers pending or successful compilation. Please assert entry and runner success, exercise runner warming through the production finalizer and video owner with failure at runCmd, and assert the retained typed stop error. Verify that deleting warming makes the regression fail.

The changed Android pull path lacks auditable cold macOS recording evidence. The live harness prepares before recording, so it cannot establish the pending compilation, pull readiness deadline, or restart behavior. Please post a final-production-SHA run with an isolated cold daemon and storage, exact commands, named Android device, android-helper backend and helperVersion, outputs, artifacts, and cleanup. Show capture during compilation, typed readiness expiry, successful same-session retry without another compile, playable adb pull and finalization, and manifest recovery after daemon restart before publication. Retain separately attributed hosted non-macOS MP4 and no-Swift evidence.

The iOS capture change also lacks an auditable cold, early, and warm recording transcript with touch compositing. The earlier Air report has timings only, and the simulator harness prepares first and hides touches. Please provide the complete earlier transcript with its source SHA or repeat on final production source with an isolated cold daemon and storage, named device, exact commands, outputs, artifacts, and cleanup. Show preparation before or alongside capture, no compile initiated by stop, playable early and warm exports, touch telemetry and composite, unfinished-compile shutdown cleanup, and reuse of the published executable. If readiness expires, show its typed reason and a successful same-session retry.

Lifecycle finalization can throw an AggregateError when cleanup fails, but daemon close calls it directly after platform shutdown. The platform shutdown normalizer therefore does not handle these errors, and daemon diagnostics lose the nested typed validator, temporary-file, and lock failures. The new tests insert a platform-owner wrapper that production does not use. Please preserve sanitized typed cleanup details at the lifecycle boundary and expose them through actual daemon close diagnostics, with coverage of that route.

Could the lifecycle own the typed sanitized cleanup failure and use existing daemon normalization, while keeping the per-runtime preparation owner and filesystem lock? Please replace the artificial owner-wrapper tests with actual daemon shutdown coverage; the restored-recording admission question remains open.

The threads on cold restoration, pull readiness, cleanup bounds, and restored preparation still stand.

All 21 checks pass at this head; the earlier startup regression is not yet shown to be fixed. No conflicts are known. Before merge, please fix the warming tests and lifecycle diagnostics, address the remaining restoration, readiness, and cleanup threads, and provide the cold Android and iOS evidence.

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Validation at b1150a8715055777d9f9788c521f4374cd313c19: pnpm check:affected --run --head b1150a8715055777d9f9788c521f4374cd313c19 passed with exact commit and unchanged checkout (13 passed, 8 skipped, 1 deduplicated; 1,230 related tests and 17,188 unit tests passed, 1 unit skipped). Coverage and device lanes are GitHub-authoritative or parked, not local passes.

Close-stall regression: focused baseline 3 passed; restoring the unbounded re-await mutant produced 1 failure and 2 skips at the explicit three-second status assertion, before Vitest's ten-second timeout; restored code passed all 3. The mutant's cache root was absent after runner fixture cleanup, not the stalled scenario's own cleanup. The test invokes the real second close and withholds its result; it does not prove arbitrary internal owner stalls. Independent static reviews of the cleanup and final bound found no issues. Prior CI and approval at f026759d6f4a06d460a7a9e566e03f5836a62e3e are not evidence for this head.

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

At b1150a8, the fixture guard, diagnostic matching, format validation and overlay documentation address parts of the earlier review, but the findings below remain. CI reports 20 passing checks. No conflicts are known. Before merge, repair warming coverage and shutdown diagnostics, resolve restoration readiness and cleanup bounds, and provide the cold device evidence below.

The pending callback test does not observe preparation, so deleting its start call leaves the assertions passing. Lost background warming could regress cold MP4 stop unnoticed. Assert preparation enters before recorder launch and capture resolves while it remains pending; verify that deleting or awaiting preparation makes the test fail.

The throwing callback test does not observe warming, so deleting it still passes. Assert callback entry, then exercise runner warming through the actual finalizer and owner with a runCmd failure and assert the typed stop error.

Could the lifecycle boundary normalize sanitized typed nested failures when runner and validator cleanup both fail? AggregateError currently escapes the platform shutdown normalizer, and the test uses a platform owner that production does not, so daemon diagnostics can lose validator, temporary-file and lock details. Cover actual daemon close diagnostics.

The Android pull path still needs an auditable final-production cold macOS daemon run. Please record the device, helper backend and version, commands, outputs, artifacts and cleanup; show capture during compilation, typed expiry, same-session retry without a new compile, playable pull and finalization, and restart recovery. Keep non-macOS MP4 and no-Swift evidence separately attributed.

The iOS recording path still needs an attributable cold, early and warm transcript: the available simulator harness prepares first and hides touches. Please show capture alongside preparation, no stop compile, playable early and warm exports, touch composite and telemetry, shutdown cleanup and published executable reuse; show typed expiry and retry if reached.

The cleanup bound thread still applies, along with 12 lower-priority threads.

The new stalled-close test settles the real close first, so it does not cover a preparation that never settles.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant