Bug: RecordScreen SAVE_RECORDING fails with kill ESRCH after ffmpeg has already exited, so polished demo (cursor overlay) never runs
Summary
On a Cloud Agent VM, RecordScreen START_RECORDING captured a complete desktop proxy MP4. When the agent later called SAVE_RECORDING, the executor tried to SIGTERM ffmpeg, the PID was already gone, and SAVE failed with:
Failed to send SIGTERM: kill ESRCH
A retry then returned:
No active recording to save
The raw proxy file was already a valid, faststart-finalized MP4 on disk. The save path never reached renderPolished, so there is no official demo artifact and no composited mouse cursor.
This looks like a robustness bug in LocalRecordScreenExecutor (exec-daemon record-screen): it treats “ffmpeg already exited” as a hard failure even when the proxy file is complete. ffmpegDiedEarly is recorded on the child exit handler but is not used to skip SIGTERM and continue polishing.
Incident (this run)
- Cloud Agent run ID:
bc-14f0acfe-baba-4673-b490-399ebaf43431 - Date: 2026-09-14 (UTC)
- VM: Cursor Cloud Agent Linux desktop (TigerVNC / AnyOS),
--record-screen-enabled,--computer-use-enabled - Display:
1920x1200@ 96 DPI,:1 - Product under test: a local web app in Chrome (details omitted)
Reproduction (generic)
- Start a Cloud Agent with computer use and RecordScreen enabled.
- Open a GUI (e.g. Chrome) on the agent desktop.
- Call
RecordScreenwithmode=START_RECORDING. - Drive the UI with the
computerUsesubagent for several minutes (this incident: ~8.5 minutes of clicks/typing). - Immediately after computer-use returns, call
RecordScreenwithmode=SAVE_RECORDINGand asave_as_filename.
Expected: SAVE stops ffmpeg if needed, writes recording-data.json, runs polished rendering (large cursor overlay), and copies recording_demo.mp4 (or the named file) into /opt/cursor/artifacts.
Actual: SAVE fails with Failed to send SIGTERM: kill ESRCH. Retry: No active recording to save. Staging still has only the raw proxy. No polished demo. No official cursor overlay.
Errors (verbatim)
First SAVE:
Failed to send SIGTERM: kill ESRCH
Second SAVE (same turn, after the first failure):
No active recording to save
No OOM in dmesg. Disk had hundreds of GB free. Memory was not exhausted (≈15 GiB RAM, several GiB still available).
Capture that did succeed
Staging session (name redacted to the pattern only):
/opt/cursor/recording-staging/session-<timestamp>-call-<id>/recording/recording_render_proxy_1080p.mp4
| Fact | Value |
|---|---|
Birth (stat) |
2026-09-14 11:52:15 UTC (matches START) |
Last modify (stat) |
2026-09-14 12:00:53 UTC |
Duration (ffprobe) |
509.966667 s (~8:30) |
| Resolution | 1920×1200 |
| Frame rate | 60 fps |
| Size | 4 169 746 193 bytes (~3.88 GiB) |
| Codec | H.264 High, all-I (keyint=1) |
moov atom |
present in the first 2 MB, not in the last 2 MB (+faststart already applied) |
nb_frames |
30598 |
Timeline fits a clean stop: START at 11:52:16 + 510 s ≈ 12:00:46, then a few seconds for the faststart rewrite, mtime 12:00:53. The proxy is a finished encode, not a truncated capture.
SAVE was invoked only after the computer-use turn returned, i.e. after that mtime.
Executor behavior (from this VM’s exec-daemon)
Relevant implementation: LocalRecordScreenExecutor in exec-daemon (record-screen.js / bundled index.js).
Capture
ffmpeg is spawned detached, roughly:
-f x11grab -i <display>-draw_mouse 0(OS cursor is intentionally not burned into the proxy)scale=1920:-2,fps=60- libx264
veryfast, CRF 17,keyint=1:min-keyint=1:scenecut=0:bframes=0 -movflags +faststart
On child exit, the executor sets:
activeRecording.ffmpegDiedEarly = trueffmpegDiedTime,ffmpegExitCode,ffmpegExitSignal
SAVE
Pseudocode of the failing path:
if (!activeRecording) → "No active recording to save"
inputEventLogger.stop()
invokeStoppedCallbackOnce()
if (!childProcess.killed && childProcess.pid)
stopFfmpegRecording() // SIGTERM process group, wait for exit
// on kill() throw → "Failed to send SIGTERM: …"
// only then: access proxy, ffprobe, save recording-data.json, renderPolished()
stopFfmpegRecording:
if (childProcess.killed || !childProcess.pid) return
process.kill(-pid, "SIGTERM") // or process.kill(pid, "SIGTERM")
// catch → reject Failed to send SIGTERM: <error>
Node ChildProcess.killed is false if the process exited on its own (it is only true if this handle called .kill()). .pid remains set after exit. process.kill(deadPid) → ESRCH.
SAVE catch:
cleanupRecording(state)
invokeStoppedCallbackOnce()
activeRecording = null
return failure
So:
- ffmpeg already exited and finalized the proxy (
+faststart,moovat start). - SAVE still takes the SIGTERM branch because
killed === falseandpidis set. kill ESRCH.- Catch nulls
activeRecordingeven though the proxy is valid. renderPolishednever runs.- Retry cannot save either.
ffmpegDiedEarly is unused on this path. cleanupRecording already treats a dead process as best-effort; SAVE’s stopFfmpegRecording does not.
Why the official cursor is missing
By design the proxy is recorded with -draw_mouse 0. Mouse motion/clicks are supposed to live in recording-data.json (InputEventLogger) and be composited in renderPolished onto recording_demo.mp4.
Because SAVE never got past SIGTERM:
- no
recording-data.jsonin the stagingrecording/directory (only the proxy mp4 was present) - no polished demo
- ffmpeg-transcoded copies of the proxy have at most a tiny OS pointer, if any — not the Cloud Agent demo cursor shown in Cursor docs
CLI even documents that polished rendering is coupled to the default computer-use API width (1280):
--computer-use-api-width … WARNING: do not change this when --record-screen-enabled
— RecordScreen polished rendering only works at the default 1280 API width.
Likely trigger for ffmpeg already being dead
Not confirmed from logs (no recorder stderr persisted in staging). Consistent with:
- ffmpeg already received a stop and completed
+faststartbefore SAVE - timing lined up with computer-use finishing (a separate ~1280×720 encode also appeared under
/tmpat ~12:00)
Unrelated to disk full or kernel OOM.
Even if something else stopped ffmpeg, SAVE should still polish a complete proxy.
Suggested fix
On SAVE:
- If
ffmpegDiedEarlyorkillwould beESRCHor the child has already exited, do not fail. Treat ffmpeg as stopped. - If
recording_render_proxy_1080p.mp4exists andffprobereturns a sane duration, continue: writerecording-data.json,renderPolished, move to artifacts. - Only fail if the proxy is missing, unreadable, or duration is 0.
- Do not clear
activeRecordingin the SIGTERM catch until polish has been attempted (or split “stop ffmpeg” errors from “cannot polish” errors).
Optional: if computer-use teardown can SIGTERM the RecordScreen ffmpeg process group, stop doing that, or have teardown call the same “already dead → continue SAVE” path.
Impact
- Cloud Agent walkthrough videos fall back to the unpolished proxy (or a 1280×720 computer-use encode).
- Docs-style cursor overlay never appears.
- Agents that follow the walkthrough skill (
START→ computer use →SAVE) hit this whenever ffmpeg exits before SAVE, including after a successful capture.
Attachments Cursor can pull from this VM / run
- Run
bc-14f0acfe-baba-4673-b490-399ebaf43431 - Staging proxy path above (still on disk at report time)
- Tool results:
START_RECORDINGsuccess;SAVE_RECORDINGESRCH; secondSAVE_RECORDING“No active recording to save”