Skip to content

fix(record): record a remote client's default recording on the daemon host - #3696

Merged
thymikee merged 2 commits into
mainfrom
fix/remote-record-default-path
Oct 16, 2026
Merged

thymikee merged 2 commits into
mainfrom
fix/remote-record-default-path

Conversation

@thymikee

@thymikee thymikee commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Summary

Found while driving an EAS cloud simulator session. On a remote daemon (AGENT_DEVICE_DAEMON_BASE_URL), record start with no path and no --platform failed: the daemon resolved ./recording-<ts>.mp4 against the client's cwd on its own host (EACCES/ENOENT: mkdir '<client cwd>').

The client can't pick the extension here, because it depends on the session platform. It now sends its local destination without an extension (clientArtifactPaths.outPath = <cwd>/recording-<ts>). The daemon's record handler (prepareRecordingStart) adds the platform extension, records to a daemon-host temp path, and record stop downloads to the client path. resolveRecordingOutputPaths is unchanged and stays transport-free. Explicit paths were already redirected by the client. The docs now say where the default downloads. 5 files.

Validation

Head e166701772, base 2a2f3f2906. pnpm check:affected --run passed (308 files, 1970 tests, wire-compat unchanged). scripts/__tests__/eager-closure-budgets.test.ts passed (914). The new tests fail without the production change.

Live run: agent-device proxy on macOS, client in a colima Linux container, cwd /client.

Android, Pixel_9_Pro_XL_API_37:

base 2a2f3f2906 head 9c1e5585e9
record start Error (UNKNOWN): ENOENT: no such file or directory, mkdir '/client' /client/recording-1792092638666.mp4; stop downloads it (840 KB)
record start out/rel.mp4 and /client/out/abs.mp4 ok ok

iOS simulator, head e166701772: record start --json returns "outPath": "/client/recording-1792130997510.mp4". record stop downloads it: ls shows 1934217 bytes, an ftypmp42 header, and the telemetry file next to it.

Same-machine client: default and explicit recordings are still written to cwd.

Risks:

  • A new client with an older remote daemon and no --platform still fails, as before.
  • Remote record start clip (no extension) on web without --platform still fails. This was already the case before this change.

… host

A remote record start without a path left the default name to the daemon,
which resolved ./recording-<ts>.mp4 against the client's cwd and failed
with EACCES on the daemon host. The client now sends its local destination
without an extension; the daemon completes it with the session platform's
extension, records to a temp path on its own host, and the client downloads
the file there on record stop.

Found while driving an EAS cloud simulator session.

@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.

No issues found across 6 files

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

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.24 MB 5.24 MB +326 B
Package (unpacked) 5.24 MB 5.24 MB +326 B
Package (download) 1.58 MB 1.58 MB +93 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.7 ms 27.0 ms +0.4 ms
CLI --help 77.4 ms 76.9 ms -0.5 ms

@thymikee

Copy link
Copy Markdown
Member Author

The code change looks right, but I cannot call this ready yet: the Coverage check fails on this PR, and the remote runs are only partly covered (reviewed at 9c1e558).

Coverage fails in eager-closure-budgets. output-path.ts now imports buildRemoteTempArtifactPath from @agent-device/contracts/daemon-http, and that adds a static edge to the closure of output-path.ts and of its two replay-port importers. A transport convention (an extensionless client stem plus a host temp path) now sits inside capture-kit's path policy, in the new branch at https://github.com/callstack/agent-device/blob/9c1e558/packages/capture-kit/src/recording/output-path.ts#L36. The rule to keep: capture-kit picks the extension and resolves paths, and only the daemon knows a request came over remote HTTP. Could the choice of the remote default path move into prepareRecordingStart in record-runtime.ts? There, when no positional path is given and clientArtifactPaths.outPath is set, the daemon would take the platform extension, pass buildRemoteTempArtifactPath('recording', ext) as requestedPath, and set clientOutputPath to the client path with the extension appended if missing. resolveRecordingOutputPaths then stays unchanged and transport-free, output-path.ts returns to its base form, and the daemon already imports contracts/daemon-http. That is about the same line count, with no new capture-kit edge.

The Smoke Tests failure (iOS deep-link wait, "app com.callstack.agentdevicelab is not running", on a local daemon) looks unrelated, because this diff only touches the remote no-path record start route. I have not seen a conflict report for this PR.

Not blocking: line 127 of website/docs/docs/commands.md covers remote downloads only for explicit paths, so a short clause or a CHANGELOG line for the new default (download to <cwd>/recording-<ts>.<ext> on the client) would help, and you can take it or leave it.

For evidence, I did not rerun the remote flow. The Android proxy result and the 840 KB download come from the PR body only, and no remote web (.webm) or iOS default-recording run is reported. Please run one of those with record start and no path through a remote client, then record stop, and show the downloaded file with the right extension in the client working directory. Please also confirm that the record start response returns the client path in the default case.

Before merge, the eager-closure-budgets failure in Coverage needs to go green after the move into the daemon record handler.

…ecord handler

Keep resolveRecordingOutputPaths transport-free: capture-kit picks the
extension and resolves paths, and only the daemon knows a request came
from a remote client. Document where a remote record start without a
path downloads.
@thymikee

Copy link
Copy Markdown
Member Author

Addressed in e166701772:

  • Eager closure: output-path.ts and its test are back to the merge-base form. The remote default is now chosen in record-runtime.ts, in resolveRecordingStartPaths, called from prepareRecordingStart. It runs when there is no positional path and clientArtifactPaths.outPath is set. It passes buildRemoteTempArtifactPath('recording', ext) as requestedPath and appends the extension to the client path if it is missing. The handler test (record-runtime-start.test.ts) and the client-side integration test both fail with the production change reverted. scripts/__tests__/eager-closure-budgets.test.ts passes locally (914/914), and pnpm check:affected --run passes.
  • Docs: commands.md now says that a remote record start with no path downloads to recording-<timestamp>.mp4 (.webm on web) in your working directory.
  • Evidence: I ran a remote iOS default recording. The daemon was behind agent-device proxy on the Mac, the client ran in a colima Linux container with cwd /client, and the device was the iOS simulator rn-matrix-iphone17. record start --json returned "outPath": "/client/recording-1792130997510.mp4", and record stop downloaded that file (1934217 bytes, ftypmp42 header). The output is in the PR body.
  • Smoke Tests: I'll rerun the unrelated failure once if it recurs with the same signature.

@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.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/daemon/src/sessions/handlers/record-runtime.ts">

<violation number="1" location="packages/daemon/src/sessions/handlers/record-runtime.ts:222">
P2: Blank path arguments are treated as remote defaults by the client but not by the daemon. Treat an empty or whitespace-only `requestedPath` as absent here so the daemon uses its temp artifact and completes the client extension.</violation>
</file>

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

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

function resolveRecordingStartPaths(req: DaemonRequest, platform: Platform): RecordingStartPaths {
const requestedPath = req.positionals?.[1];
const clientOutputPath = req.meta?.clientArtifactPaths?.outPath;
if (requestedPath === undefined && clientOutputPath !== undefined) {

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.

P2: Blank path arguments are treated as remote defaults by the client but not by the daemon. Treat an empty or whitespace-only requestedPath as absent here so the daemon uses its temp artifact and completes the client extension.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/daemon/src/sessions/handlers/record-runtime.ts, line 222:

<comment>Blank path arguments are treated as remote defaults by the client but not by the daemon. Treat an empty or whitespace-only `requestedPath` as absent here so the daemon uses its temp artifact and completes the client extension.</comment>

<file context>
@@ -206,12 +210,30 @@ function prepareRecordingStart(params: RecordRuntimeHandlerParams, session: Sess
+function resolveRecordingStartPaths(req: DaemonRequest, platform: Platform): RecordingStartPaths {
+  const requestedPath = req.positionals?.[1];
+  const clientOutputPath = req.meta?.clientArtifactPaths?.outPath;
+  if (requestedPath === undefined && clientOutputPath !== undefined) {
+    // A remote caller that names no path cannot know the session platform's extension, so it sends
+    // its local destination without one; its cwd does not exist on this host.
</file context>
Suggested change
if (requestedPath === undefined && clientOutputPath !== undefined) {
if (
(requestedPath === undefined || requestedPath.trim().length === 0) &&
clientOutputPath !== undefined
) {

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-16 07:50 UTC

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed e166701. The earlier gap is closed: the default remote recording now lands on the daemon host. The iOS remote default run on e166701 shows this, and the Android run on 9c1e558 showed it too. Both runs are the author's reports. I did not rerun them or the eager-closure-budgets test. That test should pass, because capture-kit has no diff and the daemon already depends on contracts and daemon-http.

I found no new code problems at e166701.

Smoke Tests was still running when I checked. The earlier Smoke failure was an iOS deep-link wait on a local daemon. Local daemons never set clientArtifactPaths, so the changed branches in record-runtime.ts and daemon-artifacts.ts are not on that route. No overlap exists unless a remote record smoke lane fails.

The Cubic thread on the blank positional path still applies: #3696 (comment). It has low impact and does not block. The client treats a blank positional as absent and sends the extensionless stem, but it leaves positionals[1] as an empty string. The daemon tests requestedPath === undefined, so it resolves the empty string against its own cwd and fails as it did before. Could both sides use the same non-empty test, or could the client drop the blank positional?

Before merge, Smoke Tests needs to finish green, or fail only with the earlier local iOS deep-link signature. Answering the Cubic thread is optional.

@thymikee

Copy link
Copy Markdown
Member Author

Smoke Tests now passes at e166701, so the last open item is closed. The code is ready, all checks pass, and there are no conflicts. The Cubic thread on the blank positional path is still optional.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 16, 2026
@thymikee
thymikee merged commit cee2da1 into main Oct 16, 2026
20 checks passed
@thymikee
thymikee deleted the fix/remote-record-default-path branch October 16, 2026 07:49
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