Hmbown/CodeWhale · error · ExecError
recording already running
Error message
recording ${recording.id} already running What it means
`recordingStart` refuses to start a second screen recording while one is already active in this backend instance. The HarmonyOS backend supports exactly one snapshot-series recording at a time (`recording` singleton); starting another without stopping the first throws with the existing recording's id.
Solutions
- Call `recordingStop({ id: <running id from the error message> })` before starting a new recording.
- Check `recordingStatus()`/`recordingList()` before calling recordingStart.
- Reuse the existing recording instead of starting a new one.
Example fix
// before
await backend.recordingStart({ intervalMs: 400 }); // throws if one is running
// after
const st = backend.recordingStatus?.({ id: lastRecordingId });
if (st?.running) await backend.recordingStop({ id: lastRecordingId });
const rec = await backend.recordingStart({ intervalMs: 400 }); Defensive patterns
Strategy: validation
Validate before calling
const st = backend.recordingStatus?.({ id: activeRecordingId });
if (st?.running) throw new Error("a recording is already active: " + activeRecordingId); Type guard
const canStart = !recordingState.active;
Try / catch
try { rec = await backend.recordingStart({ intervalMs: 400 }); }
catch (e) {
const m = String(e.message).match(/recording (\S+) already running/);
if (m) await backend.recordingStop({ id: m[1] }).catch(() => {});
else throw e;
} Prevention
- Track recording state in one place in your app
- Always pair recordingStart with recordingStop in finally blocks
- Parse the running id from the message to recover
When it happens
Trigger: Calling `recordingStart()` when a previous `recordingStart()` has not yet been finished with `recordingStop()` (backends/harmonyos.mjs:273). The message embeds the currently running recording's id.
Common situations: Retrying a failed recordingStop and then starting again; two parallel automation tasks both starting recordings; forgetting to stop a recording after capturing an action sequence.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- aa start failed
- bundle already installed; use
- cannot discard a Runtime thread that owns turns
- choose one available display for recording
- clipboard read is not exposed by hdc on current HarmonyOS…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/89c1b0348db86406.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/harmonyos.mjs:273
},
set_value: async ({ target, value }) => {
const b = await centerOf(target);
await uiInput(["click", String(b.cx), String(b.cy)]);
await new Promise((r) => setTimeout(r, 300));
await uiInput(["inputText", String(b.cx), String(b.cy), escDeviceText(String(value))]);
return { action_sent: true, strategy: "uitest-element" };
},
select_text: async () => { throw new ExecError("select_text is not exposed by uitest dumpLayout/uiInput on the harmony backend"); },
perform_action: async ({ target, action }) => {
const b = await centerOf(target);
if (action === "longClick") return uiInput(["longClick", String(b.cx), String(b.cy)]);
return uiInput(["click", String(b.cx), String(b.cy)]);
},
read_clipboard: async () => { throw new ExecError("clipboard read is not exposed by hdc on current HarmonyOS builds"); },
write_clipboard: async () => { throw new ExecError("clipboard write is not exposed by hdc on current HarmonyOS builds"); },
cursor_position: async () => { throw new ExecError("cursor position does not exist on touch devices"); },
recordingStart: async ({ intervalMs = 400 } = {}) => {
if (recording) throw new ExecError(`recording ${recording.id} already running`);
if (!(await have("ffmpeg"))) throw new ExecError("ffmpeg is required on the host to mux harmony snapshot-series recordings");
const id = crypto.randomBytes(4).toString("hex");
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `cu-rec-${id}-`));
const startedAt = new Date().toISOString();
const rec = recording = { id, dir, startedAt, intervalMs, seq: 0, controller: new AbortController() };
const tick = () => {
if (rec.stopped || rec.pending) return rec.pending;
rec.pending = (async () => {
const opts = { timeoutMs: 15_000, signal: rec.controller.signal };
const remote = `${DEVICE_TMP}-rec-${id}-${String(rec.seq).padStart(5, "0")}.jpeg`;
try {
await deviceOut(["snapshot_display", "-f", remote], opts);
await exec.pullFile(remote, path.join(dir, `f${String(rec.seq).padStart(5, "0")}.jpeg`), opts);
rec.seq++;
} finally { await shell(["rm", "-f", remote], opts).catch(() => {}); }
})().catch(() => {}).finally(() => { rec.pending = null; });
return rec.pending;
};View on GitHub (pinned to 73e0f67d83)