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
- Disable control buttons until a run row is selected
- Read the runId from the row the action was triggered on, not from a possibly-stale selected id
- 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
- Disable control buttons until a run row is selected
- Pass the row's id directly from the action event, not a stale selection variable
- Refresh the list when a run disappears — treat missing runs as no-ops
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
- agentWorkApi.control: ${args.action} requires a message
- agentTeamApi.get: teamId is required
- agentTeamApi.listMessages: teamId is required
- agentTeamApi.completeTask: teamId, taskId and memberId are r
- agentTeamApi.shutdownMember: teamId and memberId are require
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/b5146dd4f8ce4a61.
Report an issue: GitHub.