Skip to content

Commit f9e77eb

Browse files
claude-code-bestglm-5.2
andcommitted
fix: 修复 ExecuteTool 委托 null result 导致的 worktree UI 崩溃及 debug 日志 ENOENT
ExecuteTool 在内部工具调用失败时产出 result: null,其 renderToolResultMessage 通过 as never 将 null 直接传给 EnterWorktreeTool 的 UI,导致读取 output.worktreeBranch 时抛出 TypeError。修复方案:在 ExecuteTool 委托前增加 null 检查,同时为 EnterWorktreeTool/ExitWorktreeTool 的 renderToolResultMessage 增加 !output 防护。 debug.ts 中 getDebugFilePath 返回相对路径,appendFileSync 在某些场景下 因 CWD 变化抛出 ENOENT 击穿到 RootREPLBoundary。修复方案:路径 resolve 为 绝对路径,writeFn 增加重试机制,二次失败静默吞错不阻塞 UI。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
1 parent 7680c29 commit f9e77eb

4 files changed

Lines changed: 35 additions & 6 deletions

File tree

packages/builtin-tools/src/tools/EnterWorktreeTool/UI.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,17 @@ export function renderToolResultMessage(
1414
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
1515
_options: { theme: ThemeName },
1616
): React.ReactNode {
17+
if (!output) return null;
1718
return (
1819
<Box flexDirection="column">
1920
<Text>
20-
Switched to worktree on branch <Text bold>{output.worktreeBranch}</Text>
21+
Switched to worktree
22+
{output.worktreeBranch ? (
23+
<>
24+
{' '}
25+
on branch <Text bold>{output.worktreeBranch}</Text>
26+
</>
27+
) : null}
2128
</Text>
2229
<Text dimColor>{output.worktreePath}</Text>
2330
</Box>

packages/builtin-tools/src/tools/ExecuteTool/ExecuteTool.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ export const ExecuteTool = buildTool({
250250
renderToolResultMessage(content, progressMessages, options) {
251251
const innerTool = options.tools.find(t => t.name === content.tool_name)
252252
if (!innerTool?.renderToolResultMessage) return null
253+
// Guard against null/undefined result — several error branches in this
254+
// tool (tool-not-found, validation-failed, permission-denied, etc.) set
255+
// result: null, and delegating null to the inner tool's UI would crash
256+
// on any property access (e.g. output.worktreeBranch).
257+
if (content.result == null || typeof content.result !== 'object')
258+
return null
253259
const innerInput = (options.input as { params?: unknown } | undefined)
254260
?.params
255261
return innerTool.renderToolResultMessage(

packages/builtin-tools/src/tools/ExitWorktreeTool/UI.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function renderToolResultMessage(
1414
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
1515
_options: { theme: ThemeName },
1616
): React.ReactNode {
17+
if (!output) return null;
1718
const actionLabel = output.action === 'keep' ? 'Kept worktree' : 'Removed worktree';
1819
return (
1920
<Box flexDirection="column">

src/utils/debug.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { appendFile, mkdir, symlink, unlink } from 'fs/promises'
22
import memoize from 'lodash-es/memoize.js'
3-
import { dirname, join } from 'path'
3+
import { dirname, join, resolve } from 'path'
44
import { getSessionId } from 'src/bootstrap/state.js'
55

66
import { type BufferedWriter, createBufferedWriter } from './bufferedWriter.js'
@@ -90,10 +90,10 @@ export const getDebugFilePath = memoize((): string | null => {
9090
for (let i = 0; i < process.argv.length; i++) {
9191
const arg = process.argv[i]!
9292
if (arg.startsWith('--debug-file=')) {
93-
return arg.substring('--debug-file='.length)
93+
return resolve(arg.substring('--debug-file='.length))
9494
}
9595
if (arg === '--debug-file' && i + 1 < process.argv.length) {
96-
return process.argv[i + 1]!
96+
return resolve(process.argv[i + 1]!)
9797
}
9898
}
9999
return null
@@ -170,7 +170,20 @@ function getDebugWriter(): BufferedWriter {
170170
// Directory already exists
171171
}
172172
}
173-
getFsImplementation().appendFileSync(path, content)
173+
try {
174+
getFsImplementation().appendFileSync(path, content)
175+
} catch (e) {
176+
// If the directory was removed between mkdirSync and
177+
// appendFileSync (or the path was relative and CWD changed),
178+
// retry once with a fresh mkdirSync. Swallow on second failure
179+
// to avoid crashing the React render tree via the error boundary.
180+
try {
181+
getFsImplementation().mkdirSync(dir)
182+
getFsImplementation().appendFileSync(path, content)
183+
} catch {
184+
// Best-effort: debug log write failed, don't crash the UI
185+
}
186+
}
174187
void updateLatestDebugLogSymlink()
175188
return
176189
}
@@ -228,7 +241,9 @@ export function logForDebugging(
228241
export function getDebugLogPath(): string {
229242
return (
230243
getDebugFilePath() ??
231-
process.env.CLAUDE_CODE_DEBUG_LOGS_DIR ??
244+
(process.env.CLAUDE_CODE_DEBUG_LOGS_DIR
245+
? resolve(process.env.CLAUDE_CODE_DEBUG_LOGS_DIR)
246+
: null) ??
232247
join(getClaudeConfigHomeDir(), 'debug', `${getSessionId()}.txt`)
233248
)
234249
}

0 commit comments

Comments
 (0)