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

  1. Do not abort the signal immediately after calling recordingStart; keep the signal alive while recording.
  2. Handle this as a normal cancellation: catch it and treat the recording as cancelled, not as a failure.
  3. 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

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


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)