Repository navigation
[Bug] Renderer crashes with React error #31 on session reload when transcript contains malformed AskUserQuestion #1400
Description
Claude Code Haha — Renderer Crash + Sidecar BOM Bug Report
- Date: 2026-09-29 (CST)
- Reporter: kizemo (via MiniMax Code diagnostic session)
- App version:
claude-code-desktop/0.6.7,Electron/42.7.0 - Sidecar binary:
claude-sidecar-x86_64-pc-windows-msvc.exe(Tauri + Bun, ~115 MB) - Affected repo:
github.com/NanmiCoder/cc-haha
TL;DR
cc-haha shows a blank "出现异常。错误已记录到诊断日志。" error page on every launch when the user has at least one session whose transcript contains a malformed AskUserQuestion tool_use whose options[*].description is an object instead of a string. The root cause is a missing defensive render in the renderer that propagates ErrorBoundary to the whole tree; a side-channel bug (scheduled_tasks.json corrupted to NUL/BOM) is unrelated to the user-visible crash but produces noise in logs.
Two distinct bugs are reported in this document:
- Renderer crash (primary, user-visible) — React error 调用工具有问题? #31,
{path, $text}hast object rendered as a child. - Sidecar JSON parsing (secondary, log noise) —
CronSchedulerfails to readscheduled_tasks.jsonwhen the file is empty / NUL-filled / BOM-prefixed.
1. Environment
| Item | Value |
|---|---|
| OS | Windows 10/11 (NTFS) |
| Install dir | D:\Program Files\Claude Code Haha\ |
| User data (active) | E:\Users\Duanyi\.claude\ (mirrored to C:\Users\Duanyi\.claude\) |
USERPROFILE |
C:\Users\Duanyi |
CLAUDE_CONFIG_DIR |
not set (sidecar falls back to os.homedir() + "/.claude") |
LLM provider (per cc-haha/settings.json) |
https://api.minimaxi.com/anthropic (third-party proxy, not Anthropic direct) |
| Models in use | MiniMax-M3[1m] / MiniMax-M2.7-highspeed |
| Last opened session | 91f665d5-efe2-4f00-a9f7-314fc8730052 ("扩展json撰写", 458 messages, project F--soft-00selfmade-filemanager) |
2. User-Visible Symptoms
- App launched; sidecar started; HTTP server bound to
0.0.0.0:64467. - WebSocket connected to session
91f665d5.... - UI rendered most of the message history normally, then showed:
- "出现异常。"
- "错误已记录到诊断日志。"
- "重试" button (no effect on click)
- Doctor card + "重置安全 UI 状态" button (resets UI fields but does not clear transcript)
- Renderer process repeatedly restarted:
[renderer] [process-gone] reason=killed exitCode=1→[renderer] [recovery-started] trigger=process-gone:killed→ recovery completes → same error again.
3. Diagnostic Logs
3.1 ~/.claude/cc-haha/diagnostics/runtime-errors.log
[2026-09-28T23:49:24.266Z] ERROR client_react_error_boundary
summary: Minified React error #31; visit https://reactjs.org/docs/error-decoder.html
?invariant=31&args[]=object with keys {path, $text}
componentStack:
at p / div / div / button / div / div / div
at pU (App-DWep8kRD.js:345:388)
at App-DWep8kRD.js:346:97765
...
at u (ErrorBoundary-BnweEjNj.js:1:292)
(5 occurrences total, all with the same {path, $text} signature; timestamps cluster at 2026-09-28T23:49:24–23:49:42.)
3.2 ~/.claude/cc-haha/diagnostics/electron-host.log
[claude-server] [stderr] [CronScheduler] Error during tick:
ApiError: Failed to read scheduled tasks:
JSON Parse error: Unrecognized token '<NUL>' (or '\uFEFF' after attempted repair)
at readTasksFile (claude-sidecar.js:11102:18809)
at async listTasks (claude-sidecar.js:11102:17406)
at async tick (claude-sidecar.js:11231:3122)
[claude-server] [stderr] [CronScheduler] Error cleaning up stale runs:
SyntaxError: JSON Parse error: Unrecognized token '...'
at Fe5 (claude-sidecar.js:11230:1203)
at e_8 (claude-sidecar.js:11230:1325)
at async cleanupStaleRuns (claude-sidecar.js:11232:3904)
[renderer] [process-gone] reason=killed exitCode=1
[renderer] [recovery-started] trigger=process-gone:killed
3.3 ~/.claude/scheduled_tasks.json (corrupted)
Original (before any repair):
size: 713 bytes
hex: 00 00 00 00 00 00 ... 00 (all NUL)
JSON parse: ✗ Unrecognized token '\u0000'
last write: 2026-09-26 12:30:31
4. Root Cause Analysis
Bug A — Renderer crash on {path, $text} (PRIMARY)
A.1 Trigger
A model output produced a malformed AskUserQuestion tool_use where one option's description field is an object instead of a string. Sidecar's runtime validation rejects the call and emits InputValidationError:
<tool_use_error>InputValidationError: AskUserQuestion failed due to the following issue:
The parameter `questions[0].options[0].description` type is expected as `string`
but provided as `object`</tool_use_error>
A.2 Transcript evidence
Path: ~/.claude/projects/F--soft-00selfmade-filemanager/<session>.jsonl, line 515 (the offending assistant tool_use):
{
"type": "tool_use",
"name": "AskUserQuestion",
"input": {
"questions": [
{
"question": "上述 5 个文件确认删除吗?",
"header": "删除确认",
"options": [
{
"label": "确认删除全部 5 个",
"description": { ← BUG: should be string
"path": "`(一次一个,不用 wildcard)。CLAUDE.md 禁止 wildcard 删多个文件。",
"$text": "我会逐个执行 `rm "
}
},
{
"label": "先别删,再想想",
"description": "你会修改选择,或先备份再说。" ← correct: string
}
],
"multiSelect": false
}
]
}
}The description value is a hast (hypertext AST) node — {path, $text} is the diagnostic React error #31 gives when an mdast/hast object is mistakenly passed as a React child. The model appears to have wrapped part of its prose in an AST literal mid-stream instead of producing a string.
A.3 Renderer behavior
App-DWep8kRD.js:345:388 (pU) and App-DWep8kRD.js:419:4400 (mge) consume tool_use.input.questions[].options[].description and pass it directly to a markdown renderer. The renderer returns a hast tree; when this tree is placed inside JSX as a child, React throws #31: Objects are not valid as a React child (found: object with keys {path, $text}).
The top-level ErrorBoundary (ErrorBoundary-BnweEjNj.js:1:292) catches the throw and replaces the entire UI with the screenshot's "出现异常" page — including the unrelated Doctor card — making recovery without manually clearing transcript impossible from the UI.
A.4 Why it persists across restarts
desktop-ui.json (in ~/.claude/cc-haha/) stores pet.lastSessionId = "91f665d5-...". Every launch re-opens this session and re-renders the same line, triggering the same crash. The "重置安全 UI 状态" button only clears UI-state fields, not transcript content (per the error page's own copy: "聊天历史、模型配置、Skills、MCP、IM 和 OAuth 始终受保护").
A.5 Side note — type=queue-operation
The transcript contains {"type":"queue-operation","operation":"remove","timestamp":"...22:47:32.351Z"} immediately after the error result. This indicates the runtime flagged the tool_use as failed and removed it from the execution queue, but the transcript (and thus the renderer replay) still contains it.
Bug B — Sidecar CronScheduler JSON parse (SECONDARY)
B.1 Trigger
scheduled_tasks.json becomes an invalid JSON file (NUL-filled or BOM-prefixed). Each CronScheduler tick (every 60 s) tries to read it via:
async readTasksFile() {
try {
let q = await ud.readFile(this.getTasksFilePath(), "utf-8"),
K = JSON.parse(q);
if (!Array.isArray(K.tasks)) return {tasks: []};
return K
} catch (q) {
if (q.code === "ENOENT") return {tasks: []};
throw B6.internal(`Failed to read scheduled tasks: ${q.message}`);
}
}Path resolution:
function getTasksFilePath() {
let q = process.env.CLAUDE_CONFIG_DIR || $e5.homedir() + "/.claude";
return $e5.join(q, "scheduled_tasks.json");
}With CLAUDE_CONFIG_DIR unset and USERPROFILE=C:\Users\Duanyi, this resolves to C:\Users\Duanyi\.claude\scheduled_tasks.json — which is mirrored/identical to the one under E:\Users\Duanyi\.claude\.
B.2 Reproduction
- Overwrite
scheduled_tasks.jsonwith 713 NUL bytes (any cause — power loss during write, antivirus quarantine, fs corruption, etc.). - Launch app.
- Observe
CronSchedulerlog spam every 60 s.
B.3 BOM regression (during this user's attempted repair)
PowerShell 5.1's Set-Content -Encoding UTF8 writes a BOM (EF BB BF) by default. The user repaired the file with PowerShell; Bun's JSON.parse is strict and rejects BOM. The sidecar then logs Unrecognized token '\uFEFF' instead of '\u0000'. A re-write with System.Text.UTF8Encoding(false) (no BOM) clears the symptom. This is a Windows-onboarder footgun; not the user's fault.
5. Evidence Files (paths only, not contents)
~/.claude/cc-haha/diagnostics/runtime-errors.log (timestamps: 2026-09-28T23:23..23:49)
~/.claude/cc-haha/diagnostics/electron-host.log (process-gone, sidecar exit/entry)
~/.claude/cc-haha/diagnostics/diagnostics.jsonl (consolidated structured events)
~/.claude/projects/F--soft-00selfmade-filemanager/<sid>.jsonl (offending transcript, 521 lines)
~/.claude/scheduled_tasks.json.bak.20260929_073521 (713 NUL bytes, pre-repair snapshot)
~/.claude/scheduled_tasks_log.json.bak.20260929_073521 (20810 bytes, pre-repair snapshot)
~/.claude/projects/.../<sid>.jsonl.bak.20260929_084704 (transcript snapshot before description fix)
6. Suggested Fixes (upstream)
Fix A — Defensive rendering (PRIMARY)
In App-DWep8kRD.js around line 345 (pU) and 419 (mge), before passing option.description to any markdown / hast pipeline:
const desc = option.description;
if (typeof desc !== 'string') {
// Render label only, or a sanitized placeholder, or the error reason.
// Never forward a non-string into the markdown pipeline.
return <p className="opt-desc-broken">{option.label} <em>(description unavailable)</em></p>;
}Or, more conservatively: skip the option entirely from the rendered UI but keep the label, since the upstream model already received the validation error and won't act on it.
Also: do not let the ErrorBoundary replace the whole tree on a single tool_use render failure. Either scope it to the message bubble, or auto-recover by skipping the offending option in a componentDidCatch handler.
Fix B — Sidecar JSON hardening
- Read
scheduled_tasks.jsonwith explicit BOM stripping:const text = await Bun.file(path).text(); // Bun auto-strips BOM // or, in Node-style: text = Buffer.from(buf).toString('utf8').replace(/^\uFEFF/, '');
- On
JSON.parsefailure, treat as{tasks: []}rather than throwing — but log the bad payload tocorruption-evidence.json(the existingdiagnostics/corruption-evidence.jsonis already wired for this; just call it on parse error too). - On empty file (0 bytes), do NOT re-throw. Re-write a fresh
{"tasks":[]}to disk and return that, instead of looping forever on parse errors. - Document that PowerShell
Set-Content -Encoding UTF8writes a BOM; ship a tinycli-diagnosticsnote warning Windows users.
Fix C — Transcript schema validation at write time
When persisting an assistant tool_use into *.jsonl, validate that the shape matches the tool's JSON schema (Zod or hand-written). If a model produces description: {…} for AskUserQuestion, rewrite it to description: "" before persistence, so the transcript never carries a poison record.
Fix D — desktop-ui.json recovery
Add a checkbox to the Doctor card: "Skip last session on next launch" — sets a flag the renderer honors to land the user on the project list instead of auto-opening the crashing session. This unblocks users without requiring manual desktop-ui.json edits.
7. User Workarounds (until upstream fix)
- Clear the offending tool_use from transcript: locate the
<session>.jsonlline containing"description": {followed by"path"and"$text", replace it with"description": "..."(string) or delete the whole tool_use entry. Restart the app. - Edit
~/.claude/cc-haha/desktop-ui.json: change or removepet.lastSessionId. Restart the app; it will open the project list instead of the crashing session. - Repair
scheduled_tasks.jsonif it gets corrupted again: write{"tasks":[]}without a BOM. On Windows, useSystem.IO.File]::WriteAllText($path, $json, (New-Object System.Text.UTF8Encoding $false))from PowerShell — notSet-Content -Encoding UTF8.
8. Environment Snapshot at Time of Diagnosis
electron host version: claude-code-desktop/0.6.7
chrome: 148.0.7778.280
electron: 42.7.0
node_pty: 1.23.251008001
sidecar stack: B:/~BUN/root/claude-sidecar.js (Bun runtime, source maps present in asar)
sidecar binary: resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-x86_64-pc-windows-msvc.exe (115,294,208 bytes)
db files:
index-v1.sqlite 124,239,872 bytes
search-index-v1.sqlite 188,358,656 bytes
trace-index-v1.sqlite 136,228,864 bytes
9. Minimal Reproduction for Upstream
- Add to a new session, ask the model to use
AskUserQuestionwith a Chinese-language description that contains backticks and code fragments. - Confirm model emits
description: {path: ..., $text: ...}(object) instead ofdescription: "..."(string). - Sidecar returns
InputValidationError, model retries. - Renderer renders the failed tool_use from transcript on session reopen → React error 调用工具有问题? #31 → ErrorBoundary → "出现异常".
A unit test could simply feed { description: {path: "x", $text: "y"} } into the pU component and assert no crash.
Prepared for upstream issue tracker. All sensitive content (auth tokens, user-specific paths) redacted.
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Activity
fix(desktop): keep malformed AskUserQuestion input from crashing the …
fix(server): tolerate BOM and blank scheduled task files
kizemo commentedon Sep 30, 2026
Additional diagnostic finding (2026-09-30)
After submitting this issue, I hit a second startup failure on the same
cc-haha 0.6.7install. Different symptom, same family of timing bugs in the sidecar startup path.Symptom
UI shows:
Server log:
Sidecar binds the port successfully but
/healthnever returns inside the 500ms × 60 = 30s window.Investigation
On the same machine
~/.claude/cc-haha/diagnostics/runtime-errors.logshows:SQLite WAL on this machine:
After force-checkpointing all three with
PRAGMA wal_checkpoint(TRUNCATE), the WALs dropped to 0 bytes andcc-hahastarted successfully on the very next launch.Root cause
Sidecar's startup sequence blocks the Bun event loop while:
~/.claude/session-env/(53 entries on this machine)/api/sessions/.../turn-checkpoints,/api/teams/.../plan,/api/tasks/lists/...,/api/scheduled-tasksTotal observed stall: 0.6 – 3.7 s on event loop, but individual request handlers take 9 – 16 seconds during warm-up. The
/healthendpoint, which is supposed to return fast, can't respond inside its 500 ms window because the event loop is parked on this blocking work.Suggested fixes (priority order)
/healthimmediately on bind, even with a{status:"warming"}payload, and run heavy init in a worker afterlisten()returns. This is the single highest-impact change./healthround-trip.PRAGMA journal_size_limitso older pages get auto-checkpointed.Workaround for affected users (works as of today)
Force-checkpoint the WALs after fully closing cc-haha:
Also reset
~/.claude/desktop-server-state.jsonto{"lastPort": null}if the previous port is stuck in TIME_WAIT. Restart cc-haha afterwards.Happy to open a separate PR for any of these if useful — especially the
/healthearly-return change, since that's the lowest-risk, highest-impact one.kizemo commentedon Sep 30, 2026
Adding one more failure mode to this issue for completeness:
New symptom (2026-09-30, later today): After sidecar fails with the 500ms healthcheck timeout described above, if the user closes cc-haha and tries to relaunch it, the next launch fails differently — the sidecar process exits with
code=1immediately after spawn and produces zero lines of stdout. The error page then reads:This is much harder to diagnose than the 500ms timeout case, because there's no log trail to inspect. Manual investigation showed:
cc-haha/db/*.sqlite-walwas holding 24 MB (14+10+0) at the time of failure.PRAGMA wal_checkpoint(TRUNCATE)drops all three WALs to 0.~/.claude/desktop-server-state.jsonhadlastPort: 64467pointing at a leftover socket inCLOSE_WAITfrom a previous run. Resetting to{"lastPort": null}lets cc-haha pick a fresh port.Claude Code Haha.exeprocesses (47052, 5012) from earlier failed launches were still in the process table withHasExited=True(Windows hadn't reaped them).After applying all three workarounds (kill zombies, checkpoint WAL, reset port state), cc-haha launched successfully on the very next attempt.
Workaround for users hitting this ECONNREFUSED variant (in addition to the previous workaround):
Suggestion to upstream: when the sidecar exits with code=1 and no stdout, surface a clearer error in the UI ("sidecar exited during initialization, see /path/to/electron-host.log for details") instead of just "ECONNREFUSED". Right now users have no idea whether it's a port conflict, a missing dependency, or a DB lock.
NanmiCoder commentedon Oct 1, 2026
已在 v0.6.8 修复,请升级后再试。
原因是被服务端判为非法的工具调用会留在对话记录里,AskUserQuestion 卡片每次回放都用那份输入重建,而选项描述返回的是对象时会抛 React error #31,把整个窗口换成错误页。因为打开的标签会被恢复,所以每次启动都复现。
修复
顺带修的
如果升级后还有崩溃,麻烦把诊断包发我,里面有崩溃时的会话状态。
— @NanmiCoder