fix(kimi): preserve valid Anthropic-compatible toolCall arguments in malformed-args repair path#54491
Conversation
Greptile SummaryThis PR fixes a narrow but impactful bug in the Kimi/Anthropic-compatible stream-repair path: previously, Key changes:
Two non-blocking follow-ups worth considering:
Confidence Score: 4/5
|
| const parsed = JSON.parse(raw); | ||
| return { args: parsed as Record<string, unknown>, trailingSuffix: "" }; |
There was a problem hiding this comment.
Missing type guard for non-object valid JSON
The catch path (line 1169) guards against null, arrays, and primitives before casting to Record<string, unknown>:
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? { args: parsed as Record<string, unknown>, trailingSuffix: suffix }
: undefined;The new try path skips this guard and unconditionally casts. In practice, tool-call arguments are always objects, and shouldAttemptMalformedToolCallRepair only triggers on } / ] deltas, so the risk is low. However, if raw is a valid JSON array (e.g. [1,2]) the closing ] would trigger the repair path, and parsed as Record<string, unknown> would silently hold an array.
For consistency with the catch path and to avoid a surprising upstream typedBlock.arguments = [...] mutation:
| const parsed = JSON.parse(raw); | |
| return { args: parsed as Record<string, unknown>, trailingSuffix: "" }; | |
| const parsed = JSON.parse(raw) as unknown; | |
| if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { | |
| return undefined; | |
| } | |
| return { args: parsed as Record<string, unknown>, trailingSuffix: "" }; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.ts
Line: 1152-1153
Comment:
**Missing type guard for non-object valid JSON**
The `catch` path (line 1169) guards against `null`, arrays, and primitives before casting to `Record<string, unknown>`:
```typescript
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? { args: parsed as Record<string, unknown>, trailingSuffix: suffix }
: undefined;
```
The new `try` path skips this guard and unconditionally casts. In practice, tool-call arguments are always objects, and `shouldAttemptMalformedToolCallRepair` only triggers on `}` / `]` deltas, so the risk is low. However, if `raw` is a valid JSON array (e.g. `[1,2]`) the closing `]` would trigger the repair path, and `parsed as Record<string, unknown>` would silently hold an array.
For consistency with the catch path and to avoid a surprising upstream `typedBlock.arguments = [...]` mutation:
```suggestion
const parsed = JSON.parse(raw) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return undefined;
}
return { args: parsed as Record<string, unknown>, trailingSuffix: "" };
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7ea8415387
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const parsed = JSON.parse(raw); | ||
| return { args: parsed as Record<string, unknown>, trailingSuffix: "" }; |
There was a problem hiding this comment.
Reject non-object JSON in repaired tool arguments
tryParseMalformedToolCallArguments now treats any JSON.parse(raw) success as repairable and casts it to Record<string, unknown>, but this skips the object-shape guard used in the malformed-prefix path. If Kimi streams a valid non-object payload like [] (or another primitive containing ]/} in deltas), this code will overwrite toolCall.arguments with a non-object value, which violates the tool-call argument contract and can cause downstream tool validation/execution failures. Add the same typeof parsed === "object" && !Array.isArray(parsed) check before returning.
Useful? React with 👍 / 👎.
|
This fixed it! Hot-patched v3.24 locally and tool calls are working again. Much appreciated! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bca9b573bc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| expect(partialToolCall.arguments).toEqual({ path: "/tmp/report.txt" }); | ||
| expect(streamedToolCall.arguments).toEqual({ path: "/tmp/report.txt" }); | ||
| expect(endMessageToolCall.arguments).toEqual({ path: "/tmp/report.txt" }); | ||
| expect(finalToolCall.arguments).toEqual({ path: "/tmp/report.txt" }); |
There was a problem hiding this comment.
Include production fix for new no-junk toolcall test
This commit adds a test that expects complete JSON args to be preserved, but the implementation in attempt.tool-call-argument-repair.ts still returns undefined when JSON.parse(raw) succeeds and then clears tool arguments on that path, so this new assertion will fail. In other words, the commit introduces a failing test without the corresponding runtime change, which will block CI until the repair logic is updated in the same patch series.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2ab10b7576
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const SESSIONS_YIELD_ABORT_SETTLE_TIMEOUT_MS = process.env.OPENCLAW_TEST_FAST === "1" ? 250 : 2_000; | ||
|
|
||
| // Persist a hidden context reminder so the next turn knows why the runner stopped. | ||
| export function buildSessionsYieldContextMessage(message: string): string { |
There was a problem hiding this comment.
Remove redeclarations that shadow imported helpers
This commit pastes full helper implementations into attempt.ts without removing the existing imports of the same symbols (for example buildSessionsYieldContextMessage is imported from attempt.sessions-yield.js and then declared again here). In ESM/TypeScript modules, redeclaring an imported binding is a hard module parse error, so importing this file will fail before runtime and block the runner entirely.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ab91004a2f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const SESSIONS_YIELD_ABORT_SETTLE_TIMEOUT_MS = process.env.OPENCLAW_TEST_FAST === "1" ? 250 : 2_000; | ||
|
|
||
| // Persist a hidden context reminder so the next turn knows why the runner stopped. | ||
| export function buildSessionsYieldContextMessage(message: string): string { |
There was a problem hiding this comment.
Remove duplicated helper declarations in attempt runner
attempt.ts now redeclares helpers like buildSessionsYieldContextMessage that are already imported from ./attempt.sessions-yield.js, which creates duplicate top-level bindings in the same ES module. In this state, loading or typechecking the runner fails with an identifier-already-declared parse error before any runtime logic executes, so the embedded runner cannot start in any environment.
Useful? React with 👍 / 👎.
ab91004 to
947f2b2
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2fd49017f3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return parsed && typeof parsed === "object" && !Array.isArray(parsed) | ||
| ? { args: parsed as Record<string, unknown>, trailingSuffix: "" } |
There was a problem hiding this comment.
Invalidate cached repair when long trailing junk arrives
Returning a repair for any fully parseable object at this point caches arguments as soon as the first balanced } is seen, but that cache is not revoked if a later delta appends malformed trailing text longer than the 3-character allowance. In a stream like {"path":"/tmp/report.txt"} followed by a separate oops delta, shouldAttemptMalformedToolCallRepair skips re-validation on the second chunk (trimmedDelta.length > 3), so toolcall_end still applies the stale cached args even though total trailing junk is out of policy; this changes behavior from “do not repair when junk >3” to “repair anyway if junk arrives in a later long chunk.”
Useful? React with 👍 / 👎.
|
Confirmed this bug on OpenClaw 2026.3.24 running on a self-hosted VPS with Applied the fix from this PR manually to the dist file and it resolved the issue immediately. Would be great to get this merged and released. 🙏 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e203d731e0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const parsed = JSON.parse(raw) as unknown; | ||
| return parsed && typeof parsed === "object" && !Array.isArray(parsed) | ||
| ? { args: parsed as Record<string, unknown>, trailingSuffix: "" } |
There was a problem hiding this comment.
Revalidate cached args after long trailing junk deltas
This new JSON.parse(raw) success path returns a repair for already-valid JSON and therefore caches arguments immediately, but that cache is not invalidated when a later malformed suffix arrives as a single chunk longer than 3 characters. In wrapStreamRepairMalformedToolCallArguments, shouldAttemptMalformedToolCallRepair skips re-parse when trimmedDelta.length > 3, so a stream like valid object then oops keeps the stale cached args and still applies them at toolcall_end, which violates the “no repair when trailing junk exceeds allowance” policy.
Useful? React with 👍 / 👎.
|
Thanks man for having fixed this! That's a big one... Hopefully it will be applied to next release! |
Thanks for your comment. I hope this can be merged into the next release. This PR is base on OpenClaw main branch, If you’d like to apply the fix on v2026.3.24, you can clone my OpenClaw fork and check out the |
|
I think it is failing in the latest version as well v2026.3.28 |
e203d73 to
ce417d4
Compare
48cfe12 to
d9d7109
Compare
|
Merged in ec7f19e after verification with |
…malformed-args repair path (openclaw#54491) Verified: - pnpm build - pnpm check - pnpm test -- src/agents/pi-embedded-runner/run/attempt.test.ts Co-authored-by: yuanaichi <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
…malformed-args repair path (openclaw#54491) Verified: - pnpm build - pnpm check - pnpm test -- src/agents/pi-embedded-runner/run/attempt.test.ts Co-authored-by: yuanaichi <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
…malformed-args repair path (openclaw#54491) Verified: - pnpm build - pnpm check - pnpm test -- src/agents/pi-embedded-runner/run/attempt.test.ts Co-authored-by: yuanaichi <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
…malformed-args repair path (openclaw#54491) Verified: - pnpm build - pnpm check - pnpm test -- src/agents/pi-embedded-runner/run/attempt.test.ts Co-authored-by: yuanaichi <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
tryParseMalformedToolCallArguments 中 JSON.parse(raw) 成功后返回
undefined,触发 clearToolCallArgumentsInMessage 清空参数为 {}。
修复:合法 JSON 返回 { args: parsed, trailingSuffix: "" }。
补丁脚本从 5 步升级到 6 步(B/I/J/K/L)。
Co-Authored-By: Claude Opus 4.6 <[email protected]>
…malformed-args repair path (openclaw#54491) Verified: - pnpm build - pnpm check - pnpm test -- src/agents/pi-embedded-runner/run/attempt.test.ts Co-authored-by: yuanaichi <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
…malformed-args repair path (openclaw#54491) Verified: - pnpm build - pnpm check - pnpm test -- src/agents/pi-embedded-runner/run/attempt.test.ts Co-authored-by: yuanaichi <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
…malformed-args repair path (openclaw#54491) Verified: - pnpm build - pnpm check - pnpm test -- src/agents/pi-embedded-runner/run/attempt.test.ts Co-authored-by: yuanaichi <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
…malformed-args repair path (openclaw#54491) Verified: - pnpm build - pnpm check - pnpm test -- src/agents/pi-embedded-runner/run/attempt.test.ts Co-authored-by: yuanaichi <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
…malformed-args repair path (openclaw#54491) Verified: - pnpm build - pnpm check - pnpm test -- src/agents/pi-embedded-runner/run/attempt.test.ts Co-authored-by: yuanaichi <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
…malformed-args repair path (openclaw#54491) Verified: - pnpm build - pnpm check - pnpm test -- src/agents/pi-embedded-runner/run/attempt.test.ts Co-authored-by: yuanaichi <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
Summary
anthropic-messages) mode, the malformed toolCall-arguments repair logic could fail to preserve otherwise-valid JSON arguments (resulting in missing/empty tool arguments).toolCall.arguments; losing valid arguments can cause wrong tool behavior, failures, or unexpected “empty args” executions.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Closes: [Bug]: exec tool loses command parameter on consecutive calls #53813
Closes: [Bug]: kimi-coding/kimi-k2.5 tool calling broken on all versions after 3.13 — parameters never reach tools #54688
Closes: Regression in 2026.3.24: multiple tools receive empty argument payloads {} #54733
Closes: [Bug]: kimi/kimi-code LLM tool call validation errors when parameters are missing-bug #55005
Closes: [Bug]: Critical: All tools with required parameters receive empty {} arguments - Gateway parameter parsing failure #54975
Closes: [Bug]: read and write tools fail with "Missing required parameter" after session runs for a while #54920
Closes: [Bug]: Kimi 系列模型调用 read 工具时反复漏传 path 参数,导致循环报错 Missing required parameter: path alias #55039
Closes: [Bug]: Empty Parameter Tool Calling Failure #55930
Closes: AI agent sends empty {} instead of {"command": "..."} after extended session #55889
Closes: [Bug]: kimi, tools #58250
Related: [Bug]: Tool Parameter Serialization Failure #54978
This PR fixes a bug or regression
Root Cause / Regression History (if applicable)
git blame, prior PR, issue, or refactor if known): Unknown.}early / nested JSON / event chunking), which increased the chance the heuristic attempted repair at the wrong time.Regression Test Plan (if applicable)
src/agents/pi-embedded-runner/run/attempt.test.ts(malformed toolCall args repair tests)User-visible / Behavior Changes
None (except improved reliability of tool execution for Kimi Anthropic-compatible runs).
Security Impact (required)
Yes/No): NoYes/No): NoYes/No): NoYes/No): NoYes/No): NoYes, explain risk + mitigation: N/ARepro + Verification
Environment
kimi, model APIanthropic-messagesSteps
kimiwith Anthropic-compatible API (anthropic-messages).{"path":{"a":"/tmp/report.txt"}}) and/or where streaming deltas include early}tokens.Expected
Actual
Evidence
Attach at least one:
Human Verification (required)
Review Conversations
If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.
Compatibility / Migration
Yes/No): YesYes/No): NoYes/No): NoFailure Recovery (if this breaks)
src/agents/pi-embedded-runner/run/attempt.ts{}/missing required parameters; intermittent tool failures only on Kimi Anthropic-compatible runs.Risks and Mitigations
anthropic-messages, retain strict trailing-suffix limits, and add/extend unit coverage for representative delta sequences.