Skip to content

fix(record): replay the finished export from a retried record stop - #2534

Merged
thymikee merged 9 commits into
mainfrom
t3code/fix-issue-2281-root-cause
Sep 13, 2026
Merged

fix(record): replay the finished export from a retried record stop#2534
thymikee merged 9 commits into
mainfrom
t3code/fix-issue-2281-root-cause

Conversation

@thymikee

@thymikee thymikee commented Sep 12, 2026

Copy link
Copy Markdown
Member

Summary

record stop against a remote daemon can run out of its 90 s client window while the daemon is still exporting. The manifest then reaches completed, but a repeated record stop read it as no active recording, and the stored completion carried no caller-side output path — the recording existed on the daemon host with no CLI way to collect it (#2281).

A repeated record stop now serves that export from the durable manifest: session and device guards still apply, the file must still exist, and the caller gets the ordinary stop response, including the caller-side paths that make it downloadable. The manifest stores the finished response as the one object record stop returned, under a single key, so a replay cannot lose a field, and recovery refuses a response whose served or caller-side paths are not whole. The replay records no second session stop action, and a remote timeout now names that retry.

agent-device record stop --session demo --out ./capture.mp4  # client window expired
agent-device record stop --session demo --out ./capture.mp4  # returns the completed recording

16 files touched. No new command, flag, or timeout change; the 90 s envelope is untouched. Addresses #2281 — its needs-info request for customer evidence is still open.

Validation

pnpm check:affected --run passed at 0cb272ae553a9b48c2189a8b8ce2dda8d63e9bcd (format, lint, typecheck, layering, fallow, build, related vitest, daemon wire compat, command docs), and all PR checks including Coverage pass on that head.

Regression proof: removing the completed-manifest replay fails record stop returns the export whose response never reached the caller and its downloadable-path sibling in src/daemon/handlers/__tests__/record-runtime-stop-recovery.test.ts.

Two gates shaped the code: session teardown's eager-closure budget keeps the completion writers inside the resource definition, and the test-size ratchet moved hint assertions to src/daemon-client/__tests__/daemon-client-timeout.test.ts.

Live evidence for the readiness case is in a comment on this PR, captured at 8139aeb1ea; the reuse commits after it declare no behavior change. On an iPhone 16 Pro simulator behind agent-device proxy, record stop timed out at 90 s, the manifest turned completed 97 s later, and the retried record stop returned in under a second and downloaded a recording whose SHA-256 matches the host file, with one export in the host log.

A remote record stop can outlive its client window while the daemon is still exporting. The finished manifest was then read as no active recording, and its metadata carried no client output path, so the caller had no way to collect the file. A repeated record stop now serves the completed export and says so in the timeout hint.
A mapped codec per completion property drives encoding and decoding from one declaration, and the declaration fails to typecheck if a property has no codec.
@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-13 11:54 UTC

@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.50 MB 4.50 MB +2.8 kB
Package (unpacked) 4.50 MB 4.50 MB +2.8 kB
Package (download) 1.32 MB 1.33 MB +868 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 21.5 ms 22.8 ms +1.3 ms
CLI --help 59.4 ms 61.5 ms +2.0 ms

Session teardown reaches the recording resource definition while it loads, and that eager closure takes no new module. Writing a completion is property reads only, so the field map and writers now live with the resource definition; reading one back needs the recording vocabulary and stays behind the stop path.
The hint assertions had outgrown the aggregate client test past its size ratchet; they mirror src/daemon-client/daemon-client-timeout.ts, so they move rather than shrink.
… key

The manifest now holds the completion as the one object record stop returned, so a replay cannot lose a field between an encoder and a decoder, and the reader lives with the stop path that needs it. Recovery still refuses a response whose served path or caller-side paths are not whole.
@thymikee

Copy link
Copy Markdown
Member Author

No actionable code findings on 8139aeb. The simplified completion envelope validates stored metadata and the matching session/device before recovering an existing artifact. Current checks pass, including coverage, and there are no conflicts. Readiness still needs the live remote-export case: an export lasting beyond 90 seconds, a completed manifest after client timeout, and a retry that downloads the completed recording without starting another export.

@thymikee

Copy link
Copy Markdown
Member Author

Live remote-export evidence on 8139aeb1ea6ca6e5b60e8b113113e1abf4908d8a — iPhone 16 Pro simulator (iOS 26.2), daemon behind agent-device proxy so the client drove the remote HTTP path.

  1. record start (backend simctl recordVideo), then record stop: the client failed at its 90 s envelope exactly as reported — COMMAND_FAILED: Daemon request timed out with details.timeoutMs: 90000 and the new hint naming the retry and session.
  2. The daemon kept exporting. 94 s after the client gave up the session manifest still read lifecycle: open, phase: completing; it turned completed 97 s after the timeout, storing the caller-side path — "clientOutPath": "/private/tmp/ad-2281-out/capture.mp4". That caller directory was still empty.
  3. Re-ran record stop: returned in under a second with recording: stopped, durationMs: 135888, and artifacts carrying localPath for the video and its telemetry. capture.mp4 then existed caller-side, 58,703 bytes, SHA-256 906ec2d6…de016563 matching the host file /tmp/agent-device-recording-1789280208874-hd9vfq.mp4.
  4. No second export: the host daemon log holds exactly one export pair (… start / done) for the whole run, and the retry resolved from the completed manifest without binding a runtime.

Method note: to make the export deterministically exceed the 90 s window I ran this verification build with a temporary 120 s delay in the recording finalizer host adapter, gated on an environment variable. That patch is not part of this PR — src/platform-runtime-screen-recording-host.ts is unchanged at this head. Daemon, proxy, client, manifest, and artifact download are all this branch.

Cleanup: session closed, proxy and host daemon stopped with --clean, scratch state dirs removed, verification patch reverted and rebuilt.

…declare

A stored scope is checked by isRecordingScope next to the vocabulary it validates, and a session's durable record path comes from the factory that names it instead of being re-derived at each read.
Command, session, and action all come from the same request, so they are passed as one request instead of three more positional arguments.
The client timeout handler stays off the daemon request shape: R10 daemon-modularity holds external importers of that module at the merge-base count, so the fields arrive as named properties instead of the request object.
@thymikee

Copy link
Copy Markdown
Member Author

No actionable findings on 811e4a3. The reported live remote-HTTP run at 8139aeb proves timeout, completed manifest, retry and SHA-matched download without a second export; the temporary finalizer delay tests recovery, not ordinary export latency. The later refactors preserve that behavior. Required live evidence is satisfied; merge readiness follows the current checks.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 13, 2026
…nsports

A socket timeout and an HTTP timeout described the same request with two copies of the same mapping.
@thymikee

Copy link
Copy Markdown
Member Author

No actionable findings on 0cb272a. The shared timeout-context helper preserves the same fields for socket and HTTP requests, so the earlier live timeout/retry/download proof still applies. Current checks pass and there are no conflicts; ready for human review and merge.

@thymikee
thymikee merged commit 7a25a02 into main Sep 13, 2026
20 checks passed
@thymikee
thymikee deleted the t3code/fix-issue-2281-root-cause branch September 13, 2026 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant