Hmbown/CodeWhale · error · ExecError

durationSec must be positive

Error message

durationSec must be positive

What it means

The record action validates the optional durationSec: when provided, it must be a finite number greater than 0. Null/undefined (unbounded recording) is allowed; NaN, Infinity, zero, negatives, or non-numbers throw before the native recorder is spawned.

Solutions

  1. Pass a positive finite number, e.g. record({ durationSec: 30 }).
  2. Use null/undefined for unbounded recording instead of 0.
  3. Coerce and validate: Number(durationSec) then check Number.isFinite and > 0 before calling.

Example fix

// before
await record({ durationSec: "30" });
// after
await record({ durationSec: 30 });
Defensive patterns

Strategy: validation

Validate before calling

if (durationSec != null && !(Number.isFinite(durationSec) && durationSec > 0))
  throw new Error("durationSec must be a positive finite number or null");

Type guard

const isValidDuration = (d) => d == null || (typeof d === "number" && Number.isFinite(d) && d > 0);

Try / catch

try { await record({ durationSec }) } catch (e) {
  if (String(e.message).includes("durationSec must be")) {
    await record({ durationSec: Number(durationSec) > 0 ? Number(durationSec) : null });
  } else throw e;
}

Prevention

When it happens

Trigger: Calling record({ durationSec: 0 }) or a negative value; durationSec arriving as a string ('30') or as null-coerced value from config; passing Infinity from a computed timeout.

Common situations: Config files parsed as strings; an agent computing duration via division producing NaN; UI input allowing 0 meaning 'default' while the API expects null for unbounded.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

    // start through the same window_info the AX path uses, so a background
    // window records behind the user's work. The rect is fixed at start —
    // it does not track later moves or resizes.
    let window = null;
    if (app_ref !== undefined || window_id != null) {
      window = await native("window_info", { app_ref: app_ref === undefined ? state.inputApp ?? undefined : app_ref, window_id });
      if (!window?.points || !(window.points.w > 0) || !(window.points.h > 0)) throw new ExecError("the selected application has no capturable window — call list_windows");
      if (region) throw new ExecError("choose app_ref or region, not both");
      region = [window.points.x, window.points.y, window.points.w, window.points.h];
    }
    let disp = display ?? state.activeDisplay;
    if (window && display == null) {
      const cx = region[0] + region[2] / 2, cy = region[1] + region[3] / 2;
      const host = displays.find(d => d.points && cx >= d.points.x && cx < d.points.x + d.points.w && cy >= d.points.y && cy < d.points.y + d.points.h);
      if (host) disp = host.index;
    }
    const selected = displays.find(d => d.index === disp);
    if (!selected) throw new ExecError("choose one available display for recording");
    if (durationSec != null && (!Number.isFinite(durationSec) || durationSec <= 0)) throw new ExecError("durationSec must be positive");
    const capabilities = await native("input_capabilities");
    if (capabilities?.record_owner_pipe !== 1) throw new ExecError("native screen recorder cannot own its client lifetime; update Computer Use before recording");
    const helper = await nativeHelper();
    throwIfAborted();
    const child = spawn(helper, [JSON.stringify({ tool: "record", args: { file, displayID: selected.id, region, durationSec, owner_pipe: true } })], { stdio: ["pipe", "pipe", "pipe"] });
    child.stdin.on("error", () => {});
    const startedAt = new Date().toISOString();
    let stderr = "", output = "", ready = false;
    const completion = new Promise(resolve => {
      child.once("error", error => resolve({ code: -1, error: error.message }));
      child.once("close", code => resolve({ code, error: stderr.trim() }));
    });
    child.stderr.on("data", chunk => { stderr = (stderr + chunk).slice(-4000); });
    const recording = { child, completion, pid: child.pid, file, startedAt, mode: "ScreenCaptureKit", display: disp };
    rec.set(id, recording);
    const signal = currentSignal();
    let timer, abort;
    try {

View on GitHub (pinned to 73e0f67d83)