tinyhumansai/openhuman · error
Failed to parse service CLI output as JSON: parsed value…
Error message
Failed to parse service CLI output as JSON: parsed value does not match CommandResponse shape
What it means
parseServiceCliOutput succeeded at JSON.parse but the value failed isCommandResponse: the contract requires a non-array object carrying BOTH a `result` key and a `logs` key, with `logs` an array in which every entry is a string. The CLI emitted valid JSON of a different shape — a bare result payload, an error object, or an envelope from a different contract version.
Solutions
- Log the parsed value's top-level keys to see exactly which shape came back
- Align versions: rebuild openhuman-core (or the shell's bundled core) to match the UI expecting `{ result, logs }`
- Check the CLI's exit status separately — an error envelope on stdout usually accompanies a non-zero exit and should be handled as a command failure, not parsed as a response
- If the contract legitimately changed, update CommandResponse/isCommandResponse in tauriCommands/common.ts in the same commit
Example fix
// before
const resp = parseServiceCliOutput(raw); // throws on {"error":"boom"} with exit 0
// after (branch on exit code before parsing the success envelope)
if (exitCode !== 0) {
throw new Error(`core CLI failed (exit ${exitCode}): ${raw}`);
}
const resp = parseServiceCliOutput(raw); Defensive patterns
Strategy: type-guard
Validate before calling
const matchesEnvelope = (raw: string): boolean => {
try {
const v = JSON.parse(raw) as { result?: unknown; logs?: unknown };
return !!v && typeof v === 'object' && !Array.isArray(v)
&& 'result' in v && 'logs' in v
&& Array.isArray(v.logs) && v.logs.every(e => typeof e === 'string');
} catch {
return false;
}
};
if (!matchesEnvelope(raw)) {
// different shape (e.g. {error: ...}) — handle as a command failure
throw new Error(`Unexpected core CLI payload: ${raw.slice(0, 200)}`);
} Type guard
function isCommandResponse<T>(value: unknown): value is CommandResponse<T> {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
const c = value as { result?: unknown; logs?: unknown };
return 'result' in c && 'logs' in c
&& Array.isArray(c.logs)
&& c.logs.every(e => typeof e === 'string');
} Try / catch
try {
const resp = parseServiceCliOutput<MyResult>(raw);
} catch (err) {
if (err instanceof Error && err.message.includes('CommandResponse shape')) {
// Valid JSON, wrong contract — check exit code and version skew first
await verifyCoreCliVersion();
throw err;
}
throw err;
} Prevention
- Version-lock the core CLI and the UI so the { result, logs } envelope cannot drift
- Check the subprocess exit code before parsing — error envelopes on stdout usually pair with non-zero exits
- When the envelope contract changes, update isCommandResponse and all fixtures in the same commit
When it happens
Trigger: The CLI subprocess returns valid JSON like `{"ok":true}`, a raw result without the `{ result, logs }` envelope, or `{"error":"..."}` from a failure path that still exits with parseable output; also version skew where a newer/older CLI changed the envelope, or tests feeding sample JSON that omits `logs`.
Common situations: App/UI and core CLI built from different commits so the CommandResponse envelope drifted; a CLI failure path that prints its error JSON to stdout with exit code 0; a new subcommand not wrapping output in CommandResponse; fixtures recorded before `logs` was added to the envelope.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Failed to parse service CLI output as JSON
- At least one of --expression, --tz, --command, or --name…
- mascot manifest: missing mascots array
- agent ` ` is a `worker` tier and must not list ` ` in its…
- API request failed
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/2198dbfe59d1ff4c.
Report an issue: GitHub.
Appendix: source
Thrown at app/src/utils/tauriCommands/common.ts:97
return false;
}
if (!Array.isArray(candidate.logs)) {
return false;
}
return candidate.logs.every(entry => typeof entry === 'string');
}
export function parseServiceCliOutput<T>(raw: string): CommandResponse<T> {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (err) {
throw new Error(
`Failed to parse service CLI output as JSON: ${err instanceof Error ? err.message : String(err)}`
);
}
if (!isCommandResponse<T>(parsed)) {
throw new Error(
'Failed to parse service CLI output as JSON: parsed value does not match CommandResponse shape'
);
}
return parsed;
}
/**
* Typed marker for the CEF "IPC bridge not wired" failure mode. The vendored
* `app/src-tauri/vendor/tauri-cef/crates/tauri/scripts/ipc-protocol.js` falls
* back to `window.ipc.postMessage(...)` whenever the custom-protocol fetch
* rejects (network blip, navigation interrupt, mid-session re-entry). On CEF
* `window.ipc` is never wired — `app/src-tauri/src/cef_impl.rs` drops the
* `ipc_handler` registration — so the fallback throws
* `TypeError: Cannot read properties of undefined (reading 'postMessage')`
* **synchronously**, before the underlying `invoke()` constructs its Promise.
* The throw escapes the Promise executor and lands on `onunhandledrejection`,
* which Sentry then captures as `TAURI-REACT-7` / `TAURI-REACT-6` with no user
* impact recorded because the call sites never caught it.View on GitHub (pinned to 7491200858)