{"record":{"id":"7a4a2b6e43356f94","repo":"Hmbown/CodeWhale","slug":"harmony-recording-closed-during-startup","errorCode":null,"errorMessage":"Harmony recording closed during startup","messagePattern":"Harmony recording closed during startup","errorType":"exception","errorClass":"ExecError","httpStatus":null,"severity":"error","filePath":"crates/tui/plugins/computer-use/src/backends/harmonyos.mjs","lineNumber":299,"sourceCode":"        rec.pending = (async () => {\n          const opts = { timeoutMs: 15_000, signal: rec.controller.signal };\n          const remote = `${DEVICE_TMP}-rec-${id}-${String(rec.seq).padStart(5, \"0\")}.jpeg`;\n          try {\n            await deviceOut([\"snapshot_display\", \"-f\", remote], opts);\n            await exec.pullFile(remote, path.join(dir, `f${String(rec.seq).padStart(5, \"0\")}.jpeg`), opts);\n            rec.seq++;\n          } finally { await shell([\"rm\", \"-f\", remote], opts).catch(() => {}); }\n        })().catch(() => {}).finally(() => { rec.pending = null; });\n        return rec.pending;\n      };\n      const signal = currentSignal();\n      const abort = () => rec.controller.abort();\n      signal?.addEventListener(\"abort\", abort, { once: true });\n      try {\n        throwIfAborted(signal);\n        await tick();\n        throwIfAborted(signal);\n        if (rec.stopped) throw new ExecError(\"Harmony recording closed during startup\");\n        rec.timer = setInterval(tick, Math.max(150, intervalMs));\n      } catch (err) { await stopFrames(); throw err; }\n      finally { signal?.removeEventListener(\"abort\", abort); }\n      return { id, mode: \"snapshot-series\", intervalMs, startedAt, note: \"no HarmonyOS CLI screen recorder; frames are muxed into mp4 on stop\" };\n    },\n    recordingStop: async ({ id }) => {\n      if (!recording || recording.id !== id) throw new ExecError(`unknown recording \"${id}\"`);\n      const { dir, seq, startedAt, intervalMs } = await stopFrames();\n      const dirOut = process.env.CODEWHALE_CU_RECORDINGS_DIR || path.join(os.homedir(), \".codewhale-cu\", \"recordings\");\n      fs.mkdirSync(dirOut, { recursive: true });\n      const out = path.join(dirOut, `rec-${id}.mp4`);\n      const fps = Math.max(1, Math.min(15, Math.round(1000 / Math.max(150, intervalMs))));\n      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 });\n      const bytes = fs.existsSync(out) ? fs.statSync(out).size : 0;\n      try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}\n      if (r.code !== 0) throw new ExecError(`ffmpeg mux failed: ${(r.stderr || \"\").slice(0, 300)}`, r);\n      return { id, mode: \"snapshot-series\", frames: seq, fps, file: out, bytes, startedAt, stoppedAt: new Date().toISOString() };\n    },","sourceCodeStart":281,"sourceCodeEnd":317,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/plugins/computer-use/src/backends/harmonyos.mjs#L281-L317","documentation":"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).","triggerScenarios":"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).","commonSituations":"A caller cancels the operation immediately after issuing recordingStart (e.g. user hit escape); race between teardown code and startup code.","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."],"exampleFix":"// before\nconst ctrl = new AbortController();\nconst rec = await backend.recordingStart({ intervalMs: 400, signal: ctrl.signal });\nctrl.abort(); // aborts during startup -> throws\n// after\nconst ctrl = new AbortController();\ntry {\n  const rec = await backend.recordingStart({ intervalMs: 400, signal: ctrl.signal });\n} catch (e) {\n  if (ctrl.signal.aborted) return; // expected cancellation\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"if (signal?.aborted) throw new DOMException(\"aborted before start\", \"AbortError\");","typeGuard":"null","tryCatchPattern":"try { rec = await backend.recordingStart({ intervalMs: 400, signal }); }\ncatch (e) {\n  if (signal?.aborted || String(e.message).includes(\"closed during startup\")) return null; // expected cancel\n  throw e;\n}","preventionTips":["Keep the AbortSignal alive until after recordingStart resolves","Treat startup-abort errors as cancellation, not bugs","Avoid racing stop/abort against startup"],"tags":["abort","race-condition","recording"],"backgroundTag":"operation-aborted","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}