Hmbown/CodeWhale · error · ExecError

unknown or already-finished recording

Error message

unknown or already-finished recording "${id}"

What it means

recordingStop({id}) looks up the recording handle in an in-memory map; this error means the id is not present, so it was never started by this session or its stop already completed (the map entry is deleted on successful stop). The library throws rather than guessing which stale or fabricated id you meant.

Solutions

  1. Check the id against the value returned by the recording start call in the CURRENT session.
  2. If the stop may have already succeeded, treat the recording as stopped and read the file from the previously returned result instead of stopping again.
  3. If the session restarted, the recording is unmanageable via this API; re-start a new recording if one is still needed.
  4. Catch the error and call the recording status/inspect surface to confirm no live recording with that id exists before retrying.

Example fix

// before
await backend.recordingStop({ id: rememberedIdFromOldSession });
// after
if (sessionRecording && sessionRecording.id === id) {
  const out = await backend.recordingStop({ id });
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (!currentRecording || currentRecording.id !== id) {
  throw new Error(`no live recording id ${id} in this session`);
}

Try / catch

try {
  const out = await backend.recordingStop({ id });
} catch (e) {
  if (String(e.message).includes('unknown or already-finished recording')) {
    // already stopped or stale id — treat as no-op, use retained file
  } else throw e;
}

Prevention

When it happens

Trigger: Calling screen-recording stop with an id returned by a previous session, an id already stopped successfully (the entry is deleted from the rec map at the end of recordingStop), a typo'd id, or after the plugin/session was restarted and its in-memory recording registry was cleared.

Common situations: An agent retries a stop call after a first stop already succeeded; the TUI restarted between recording start and stop so all ids became invalid; the id was hallucinated or copied from a log of an old session.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/8bc8544984fb556e. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/darwin.mjs:687

        completion.then(result => { if (!ready) reject(new ExecError(result.error || "screen recorder exited before capture started")); });
      });
      throwIfAborted();
      return { id, pid: child.pid, file, display: disp, durationSec: durationSec ?? null, region: region ?? null, fps: 30, mode: "ScreenCaptureKit", startedAt,
        ...(window ? { window: { id: window.window_id ?? null, name: window.name ?? null }, note: "Recording the window's rect as it was at start; it does not track moves or resizes." } : {}) };
    } catch (error) {
      requestRecordingStop(recording);
      const result = await waitForRecordingStop(recording, 2_000);
      if (result.terminated !== false) rec.delete(id);
      throw error;
    } finally {
      clearTimeout(timer);
      signal?.removeEventListener("abort", abort);
    }
  }

  async function recordingStop({ id }) {
    const r = rec.get(id);
    if (!r) throw new ExecError(`unknown or already-finished recording "${id}"`);
    requestRecordingStop(r);
    const result = await waitForRecordingStop(r, 20_000);
    if (result.code !== 0) throw new ExecError(result.error || "screen recorder failed; partial file retained");
    const size = fs.existsSync(r.file) ? fs.statSync(r.file).size : 0;
    if (!size) throw new ExecError("screen recorder produced no video");
    rec.delete(id);
    return { id, file: r.file, mp4: null, bytes: size, mode: r.mode, startedAt: r.startedAt, stoppedAt: new Date().toISOString() };
  }

  async function closeSession() {
    // The preview this session showed must not outlive the session; a panel
    // from a dead session has no owner to refresh or hide it.
    stopPreviewLoop();
    await quiescePreview();
    if (state.previewEnabled && state.inputApp) {
      try { await native("preview_notify", { enabled: false }); } catch { /* hiding is best-effort */ }
    }
    state.previewEnabled = false;

View on GitHub (pinned to 73e0f67d83)