Replies: 3 comments
|
@Ijon-Miao solid report — the code archaeology is right, but I have news that changes the conclusion: this exact defect was already fixed upstream on 2026-09-01, and the release you're on (0.1.2-rc.1) predates the fix. The FIX-ANALYSIS quotes the pre-fix bundle. The fix, and why your version shows the bug Commit The pre-fix loop (the code your FIX-ANALYSIS quotes from the 0.1.2-rc.1 bundle) was: if (call.id !== void 0) block.callId = call.id
if (call.function?.name !== void 0) block.name = call.function.name
What master does now ( block.callId = acceptIdentity(block.callId, call.id)
block.name = acceptIdentity(block.name, call.function?.name)
Your Direction 1/3 (fail-loud The original fix shipped exactly that refusal (error finish at What to do next
Family note: this is the wire-identity-loss family (#5413 empty-delta overwrite, #5268 dup-id, #5884 cross-step id reuse), with a distinctive locus — the provider stream translator — and a nastier tail: the empty |
|
Good analysis — I re-checked it against the current source (the What is already in place
function acceptIdentity(current: string | undefined, incoming: unknown): string | undefined {
return typeof incoming === 'string' && incoming.length > 0 ? incoming : current
}
// ...
block.callId = acceptIdentity(block.callId, call.id)
block.name = acceptIdentity(block.name, call.function?.name)So a continuation delta that re-sends the field empty or What is still open (the actual gap)
Also worth ruling out: the "leading A fix that fits this file
case 'tool-call': {
if (block.name === undefined || block.name.length === 0) {
throw new LlmError(
`tool-call closed with no name (callId=${block.callId ?? ''}, blockIndex=${block.index}, argumentsBytes=${block.text.length})`,
'MALFORMED_TOOL_CALL',
)
}
return { type: 'tool-call', id: brandString<ToolCallId>(block.callId ?? ''), name: block.name, arguments: block.text }
}Because the close loop runs while draining
How to confirm which shape hits youThe raw deltas are persisted: on If this helped, marking it as the answer keeps it findable for others hitting |
|
@PerryLink 你的三点修正全部源级成立——我刚在
所以你说「Direction 2 已部分实现、剩下的是关联键 + 关闭时行为」这个判断准确,我的原回复确实忽略了这层。 关于你的修复方向,我的两点补充1. "哪一个 shape 打到你"的判别器值得优先写。你结尾那句诊断路径很关键——原始 delta 是持久化的。在 2. 你的 close-time fail-loud 修复我认可,且和你 cite 的文件现有风格一致(该文件 一处边界提醒
整体方向我赞成,这是对 upstream 那条 2026-09-01 fix 的合理收尾。如果你的修复落地了、或者想让我在你的 patch 上加 |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
FIX-ANALYSIS.md
ISSUE-REPORT.md
Environment
Summary
When the model is an instruction that triggers a tool call (e.g. "check"/"检查"), the harness
records the tool call with an empty
nameand emptyid, whileargumentsis fully populated.The tool dispatcher then rejects it:
Error: unknown tool ""
ToolNotFoundError / UNKNOWN_TOOL
This repeats in a loop (8 times in one observed turn).
Evidence (decoded from session.jsonl.zstd)
{
"type": "assistant/message",
"data": { "message": { "content": [
{ "type": "text", "text": "user asked..." },
{ "type": "tool-call",
"id": "", // <- empty
"name": "", // <- empty
"arguments": "{"command": "echo ..."}" }
] } }
}
Already ruled out (direct gateway tests)
The empty name is produced inside the adapter, not upstream.
Suspected code path
packages/llm/deepseek (bundled as @deepseek-ai/dsh-llm-deepseek/lib/index.js)
closeBlock():
case "tool-call": return {
type: "tool-call",
id: brandString(block.callId ?? ""),
name: block.name ?? "", // <- empty if name never assigned
arguments: block.text
};
translate() tool-call loop only assigns name/id when a delta carries them:
if (call.id !== void 0) block.callId = call.id;
if (call.function?.name !== void 0) block.name = call.function.name;
All reactions