Hmbown/CodeWhale · error · ExecError
Harmony recording closed during startup
Error message
Harmony recording closed during startup
What it means
During `recordingStart`'s startup sequence, the backend takes the first frame, registers an abort listener, and checks whether the recording was stopped or the caller's abort signal fired before the interval timer was installed. If the recording was closed (stopped or aborted) in that window, startup fails and the partially started recording is cleaned up (frames removed via stopFrames).
Solutions
- Do not abort the signal immediately after calling recordingStart; keep the signal alive while recording.
- Handle this as a normal cancellation: catch it and treat the recording as cancelled, not as a failure.
- Retry recordingStart with a fresh signal if a recording is actually wanted.
Example fix
// before
const ctrl = new AbortController();
const rec = await backend.recordingStart({ intervalMs: 400, signal: ctrl.signal });
ctrl.abort(); // aborts during startup -> throws
// after
const ctrl = new AbortController();
try {
const rec = await backend.recordingStart({ intervalMs: 400, signal: ctrl.signal });
} catch (e) {
if (ctrl.signal.aborted) return; // expected cancellation
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
if (signal?.aborted) throw new DOMException("aborted before start", "AbortError"); Type guard
null
Try / catch
try { rec = await backend.recordingStart({ intervalMs: 400, signal }); }
catch (e) {
if (signal?.aborted || String(e.message).includes("closed during startup")) return null; // expected cancel
throw e;
} Prevention
- Keep the AbortSignal alive until after recordingStart resolves
- Treat startup-abort errors as cancellation, not bugs
- Avoid racing stop/abort against startup
When it happens
Trigger: Passing an already-aborted or soon-aborting AbortSignal to `recordingStart`; the recording being stopped concurrently between the first `tick()` and the timer installation (backends/harmonyos.mjs:299).
Common situations: A caller cancels the operation immediately after issuing recordingStart (e.g. user hit escape); race between teardown code and startup code.
Related errors
- Android loaded-image identity changed: /proc/self/maps has…
- Android loaded-image proof changed from
- cancel wins race
- cancelled tool result is always model-visible
- choose one available display for recording
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/7a4a2b6e43356f94.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/harmonyos.mjs:299
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;
};
const signal = currentSignal();
const abort = () => rec.controller.abort();
signal?.addEventListener("abort", abort, { once: true });
try {
throwIfAborted(signal);
await tick();
throwIfAborted(signal);
if (rec.stopped) throw new ExecError("Harmony recording closed during startup");
rec.timer = setInterval(tick, Math.max(150, intervalMs));
} catch (err) { await stopFrames(); throw err; }
finally { signal?.removeEventListener("abort", abort); }
return { id, mode: "snapshot-series", intervalMs, startedAt, note: "no HarmonyOS CLI screen recorder; frames are muxed into mp4 on stop" };
},
recordingStop: async ({ id }) => {
if (!recording || recording.id !== id) throw new ExecError(`unknown recording "${id}"`);
const { dir, seq, startedAt, intervalMs } = await stopFrames();
const dirOut = process.env.CODEWHALE_CU_RECORDINGS_DIR || path.join(os.homedir(), ".codewhale-cu", "recordings");
fs.mkdirSync(dirOut, { recursive: true });
const out = path.join(dirOut, `rec-${id}.mp4`);
const fps = Math.max(1, Math.min(15, Math.round(1000 / Math.max(150, intervalMs))));
const r = await run("ffmpeg", ["-y", "-loglevel", "error", "-framerate", String(fps), "-i", path.join(dir, "f%05d.jpeg"), "-c:v", "libx264", "-pix_fmt", "yuv420p", out], { timeoutMs: 180_000 });
const bytes = fs.existsSync(out) ? fs.statSync(out).size : 0;
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
if (r.code !== 0) throw new ExecError(`ffmpeg mux failed: ${(r.stderr || "").slice(0, 300)}`, r);
return { id, mode: "snapshot-series", frames: seq, fps, file: out, bytes, startedAt, stoppedAt: new Date().toISOString() };
},View on GitHub (pinned to 73e0f67d83)