tinyhumansai/openhuman · error

Finish choosing how OpenHuman runs (tap Continue on the setu

Error message

Finish choosing how OpenHuman runs (tap Continue on the setup screen), then try signing in again.

What it means

Thrown by the `todo` agent tool's argument parser (src/openhuman/agent/tools/todo.rs, optional_string_array) when an optional array-typed field is present in the tool-call JSON but its value is not a JSON array. The keys affected are `plan`, `allowedTools`, `acceptanceCriteria`, and `evidence`, all parsed inside patch_from_args for op=add/edit. The function short-circuits Ok(None) only when the key is absent (args.get(key) returns None), so any present-but-non-array value — a string, number, object, or an explicit JSON null — reaches value.as_array() and fails there. The error propagates out of Tool::execute via `?` at the patch_from_args call sites, so the tool call aborts before any board mutation happens.

Source

Thrown at app/src/components/oauth/oauthAuthReadiness.ts:165

        'OpenHuman could not reach its local runtime. Quit and reopen the app, ' +
        'then try signing in again.'
      );
    }
    default:
      return 'Sign-in is still starting up. Wait a few seconds and try again.';
  }
}

/**
 * Lightweight preflight before opening the system browser for OAuth.
 * Blocks browser launch when the local auth runtime is not ready yet.
 * `waitForOAuthAuthReadiness()` starts the local core when needed.
 */
export async function prepareOAuthLoginLaunch(): Promise<void> {
  const quick = await waitForOAuthAuthReadiness(8_000);
  if (!quick.ready) {
    warnLog(`${logPrefix} pre-launch readiness`, quick);
    throw new Error(oauthAuthReadinessUserMessage(quick.reason));
  }
}

View on GitHub (pinned to a221052e0d)

Solutions

  1. Make the value a JSON array of strings: pass "plan": ["step 1", "step 2"] instead of "plan": "step 1; step 2" — check the tool's parameters_schema (todo.rs:78-102) which declares each of these fields as {type: array, items: {type: string}}.
  2. Omit the key entirely rather than passing null: optional_string_array returns Ok(None) when args.get(key) is None, but an explicit null fails the as_array() check. Delete the field from the payload to leave the patch slot unset.
  3. If you control the caller and cannot fix the payload shape, coerce before sending: split delimited strings on ';' / newline into arrays, and drop null-valued keys from the JSON object.
  4. As a library maintainer, make null benign by adding a null arm mirroring the approvalMode handling at todo.rs:253 — `if value.is_null() { return Ok(None); }` before the as_array() call — so models that emit explicit nulls do not fail the call.

Example fix

// before — tool call args (op=add)
{ "op": "add", "content": "Ship fix", "plan": "repro; fix; test", "allowedTools": null }
// -> Err: `plan` must be an array of strings

// after
{ "op": "add", "content": "Ship fix", "plan": ["repro", "fix", "test"] }
// allowedTools omitted -> Ok(None), patch leaves tools unconstrained
Defensive patterns

Strategy: validation

Validate before calling

// TypeScript — run before invoking the `todo` tool via RPC/relay
const STRING_ARRAY_KEYS = ["plan", "allowedTools", "acceptanceCriteria", "evidence"] as const;

function normalizeTodoArgs(args: Record<string, unknown>): Record<string, unknown> {
  for (const key of STRING_ARRAY_KEYS) {
    const v = args[key];
    if (v === undefined) continue;      // absent -> Ok(None), fine
    if (v === null) { delete args[key]; continue; } // explicit null would fail as_array()
    if (typeof v === "string") {        // delimited string -> split into array
      args[key] = v.split(/[;\n]/).map((s) => s.trim()).filter(Boolean);
      continue;
    }
    if (!Array.isArray(v)) throw new Error(`\`${key}\` must be an array of strings`);
  }
  return args;
}

Type guard

function isOptionalStringArray(v: unknown): v is string[] | undefined {
  if (v === undefined) return true;
  return Array.isArray(v) && v.every((item) => typeof item === "string");
}

// usage: if (!isOptionalStringArray(args.plan)) { /* coerce or reject before calling */ }

Prevention

When it happens

Trigger: Calling the `todo` tool with op=add or op=edit and one of: (1) `"plan": "step 1; step 2"` — a single string instead of an array; (2) `"allowedTools": null` — explicit null is Some(Value::Null) in serde_json, not a missing key, so as_array() returns None; (3) `"acceptanceCriteria": ["a"], "evidence": {"link": "..."}` — object where an array is expected; (4) a model serializing a comma-joined string or a map of step->text instead of a list. Each of plan/allowedTools/acceptanceCriteria/evidence at todo.rs:274-279 hits the same check at line 295.

Common situations: LLM tool-calling is the usual source: models frequently collapse `plan` into one delimited string, emit null for 'no value' instead of omitting the key, or nest objects (e.g. numbered steps as {"1": "..."}). Also hit when hand-writing RPC/JSON-RPC payloads to the core's tool surface, when a schema-drift between frontend expectations and the tool's declared JSON Schema (which says type: array, items: string for these four fields) goes unnoticed, or when an upstream orchestrator forwards user-typed free text verbatim into an array slot.

Related errors


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