tinyhumansai/openhuman · error

agentWorkApi.control: runId is required

Error message

agentWorkApi.control: runId is required

What it means

Guard inside agentWorkApi.control: runId is trimmed and must be non-empty before the openhuman.agent_work_control RPC applies the control verb (continue / follow_up / stop). A blank id cannot identify a run, so the client rejects it before the network hop.

Source

Thrown at app/src/services/api/agentWorkApi.ts:107

    log('list limit=%o', limit);
    const response = await callCoreRpc<AgentWorkResponse>({
      method: 'openhuman.agent_work_list',
      params: limit === undefined ? {} : { limit },
    });
    log('list received groups=%d total=%d', response.groups.length, response.total);
    return response;
  },

  /**
   * Apply a control verb to one background agent run, returning the updated row.
   *
   * `continue` and `follow_up` carry the user's message; the client rejects an
   * empty message for those verbs before hitting core (the Rust handler also
   * enforces it). `stop` may carry an optional `reason`.
   */
  control: async (args: AgentWorkControlArgs): Promise<AgentWorkRow> => {
    const runId = args.runId?.trim();
    if (!runId) throw new Error('agentWorkApi.control: runId is required');
    const message = args.message?.trim();
    if ((args.action === 'continue' || args.action === 'follow_up') && !message) {
      throw new Error(`agentWorkApi.control: ${args.action} requires a message`);
    }
    const params: Record<string, unknown> = { runId, action: args.action };
    if (message) params.message = message;
    const reason = args.reason?.trim();
    if (reason) params.reason = reason;
    log('control runId=%s action=%s', runId, args.action);
    const response = await callCoreRpc<AgentWorkControlResponse>({
      method: 'openhuman.agent_work_control',
      params,
    });
    log('control received status=%s', response.row.status);
    return response.row;
  },
};

View on GitHub (pinned to a221052e0d)

Solutions

  1. Disable control buttons until a run row is selected
  2. Read the runId from the row the action was triggered on, not from a possibly-stale selected id
  3. Treat a vanished run as a no-op in the UI (list refresh) rather than calling with an empty id

Example fix

// before
onStop={() => agentWorkApi.control({ runId: selectedRunId ?? '', action: 'stop' })}

// after
onStop={() => {
  if (!selectedRunId) return;
  agentWorkApi.control({ runId: selectedRunId, action: 'stop' }).then(refresh).catch(notify);
}}
Defensive patterns

Strategy: validation

Validate before calling

const runId = args?.runId?.trim();
if (runId) await agentWorkApi.control({ runId, action });

Type guard

const isNonEmptyTrimmed = (v: unknown): v is string => typeof v === 'string' && v.trim().length > 0;

Try / catch

try { await agentWorkApi.control({ runId, action }); }
catch (e) { if (String(e.message).includes('runId is required')) refreshRuns(); else throw e; }

Prevention

When it happens

Trigger: Calling control({ runId: '', action: 'stop' }) or control({ runId: ' ', action: 'continue', message: 'hi' }) — whitespace-only ids are trimmed to empty and rejected; also control({ action: 'stop' }) with no runId at all.

Common situations: A control button wired to a selected-run state that is null until a row is clicked; the run finishing and being removed from the list between selection and action; ids read from an optional column that some rows lack.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/b5146dd4f8ce4a61. Report an issue: GitHub.