tinyhumansai/openhuman · error

agentWorkApi.control: ${args.action} requires a message

Error message

agentWorkApi.control: ${args.action} requires a message

What it means

Guard inside agentWorkApi.control: the verbs 'continue' and 'follow_up' semantically require a user message (the run cannot be continued with nothing to say), so the client rejects them when message is missing or whitespace-only after trimming. The Rust handler enforces the same rule server-side; this is the early client check.

Source

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

      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. Validate non-empty trimmed input in the dialog before calling (and disable submit when empty)
  2. Trim the textarea value at the call boundary: const msg = input.trim(); if (!msg) return;
  3. For 'stop', keep using the optional reason field — no message needed there

Example fix

// before
await agentWorkApi.control({ runId, action: 'follow_up', message: draft }); // draft === '  '

// after
const msg = draft.trim();
if (!msg) return showError('Enter a message to continue this run');
await agentWorkApi.control({ runId, action: 'follow_up', message: msg });
Defensive patterns

Strategy: validation

Validate before calling

const message = args?.message?.trim() ?? '';
if (action === 'stop' || message) {
  await agentWorkApi.control({ runId, action, ...(message ? { message } : {}) });
}

Type guard

const isActionMessage = (a: string, m: unknown): boolean =>
  a === 'continue' || a === 'follow_up' ? typeof m === 'string' && m.trim().length > 0 : true;

Try / catch

try { await agentWorkApi.control({ runId, action, message }); }
catch (e) { if (String(e.message).includes('requires a message')) showInputError('Enter a message'); else throw e; }

Prevention

When it happens

Trigger: Calling control({ runId, action: 'continue' }) with no message, or with message: ' ' — e.g. a dialog whose textarea is submitted empty, or a send handler that forwards an unvalidated string.

Common situations: An 'Ask follow-up' modal whose submit button is not disabled on empty input; prefilled draft cleared by the user; message passed as an optional field and left undefined by a wrapper function.

Related errors


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