Skip to content

fix(agents): normalize mangled tool names and IDs from OpenAI-compati…#39245

Open
deepujain wants to merge 2 commits intoopenclaw:mainfrom
deepujain:fix/39091-normalize-mangled-tool-names-ids
Open

fix(agents): normalize mangled tool names and IDs from OpenAI-compati…#39245
deepujain wants to merge 2 commits intoopenclaw:mainfrom
deepujain:fix/39091-normalize-mangled-tool-names-ids

Conversation

@deepujain
Copy link
Copy Markdown

@deepujain deepujain commented Mar 7, 2026

Summary

  • Problem: Some OpenAI-compatible providers (e.g. Kimi) send tool call IDs like functions.exec:0 as functions exec:0 (space instead of dot) and tool names like exec as functions exec. Tool result matching fails, causing "Tool functions exec not found" errors.
  • Why it matters: Users on Kimi and similar providers cannot complete tool-using flows; matching is done by ID and name, and the mangled forms did not match.
  • What changed: Added normalizeMangledToolCallId() in tool-call-id.ts to rewrite functions -> functions. in IDs; use it in extractToolResultId() and as the key in sanitizeToolCallIdsForCloudCodeAssist() so mangled and canonical IDs map to the same sanitized value. Added normalizePrefixedToolName() in session-transcript-repair.ts to strip functions. / functions from tool names; applied in assistant block handling, hasToolCallName, and normalizeToolResultName() so tool-result names are normalized and pairing works.
  • What did NOT change (scope boundary): No change to provider clients or wire format; only transcript repair and ID sanitization for matching.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

Tool-using flows with OpenAI-compatible providers that mangle tool IDs/names (e.g. Kimi) now match tool results to tool calls correctly; "Tool functions exec not found" is resolved.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: any
  • Provider: OpenAI-compatible (e.g. Kimi) that sends functions exec:0 / functions exec style IDs and names

Steps

  1. Run agent with such a provider; trigger a tool call (e.g. exec).
  2. Before fix: tool result may not match, "Tool functions exec not found" or similar.
  3. After fix: IDs and names normalize; pairing and sanitization map mangled and canonical forms to the same value.

Expected

Tool results pair with assistant tool calls; no spurious "tool not found" from ID/name mangling.

Actual

As expected after the fix.

Evidence

  • normalizeMangledToolCallId and extractToolResultId / sanitizeToolCallIdsForCloudCodeAssist tests in tool-call-id.test.ts.
  • sanitizeToolUseResultPairing test with mangled ID and prefixed name in session-transcript-repair.test.ts.

Human Verification (required)

  • Verified scenarios: Unit tests added; logic follows issue and Greptile feedback (compare original rawToolName in normalizeToolResultName, use normalized ID as map key).
  • Edge cases checked: Empty string, already-canonical IDs, names without prefix.
  • What you did not verify: Live Kimi or other provider E2E.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Failure Recovery (if this breaks)

  • Revert the commit.
  • Files: src/agents/tool-call-id.ts, src/agents/session-transcript-repair.ts.

Risks and Mitigations

None. Normalization is additive and only affects matching/sanitization; no new execution paths.

…ble providers

Add normalizeMangledToolCallId (functions  -> functions.) and use in
extractToolResultId and sanitizeToolCallIdsForCloudCodeAssist. Add
normalizePrefixedToolName in session-transcript-repair for assistant
and tool-result names. Fixes Tool functions exec not found matching.
Fixes openclaw#39091
@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: S labels Mar 7, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR fixes tool call ID and name mismatches caused by OpenAI-compatible providers (e.g. Kimi) that send "functions exec:0" (space-separated) instead of the canonical "functions.exec:0" (dot-separated) for tool call IDs, and similarly prefix tool names with "functions " or "functions.". Without this fix, the transcript repair logic fails to match assistant tool calls with their results, producing unpairable tool call/result entries that cause provider API errors.

Key changes:

  • Added normalizeMangledToolCallId in tool-call-id.ts (replaces functions\s+functions. globally in IDs) and applied it in extractToolResultId and sanitizeToolCallIdsForCloudCodeAssist
  • Added normalizePrefixedToolName in session-transcript-repair.ts (strips "functions." / "functions " prefix from tool names) and applied it in hasToolCallName, normalizeToolResultName, and repairToolCallInputs
  • Added focused unit tests for both helpers and end-to-end pairing coverage
  • One minor issue: the else if branch in repairToolCallInputs introduced at line 314 becomes unreachable dead code after the new normalization path was introduced

Confidence Score: 4/5

  • Safe to merge; normalization logic is correct and well-tested with no runtime risks. One unreachable dead code branch warrants cleanup.
  • The changes are well-implemented with correct control flow and comprehensive test coverage. The normalization functions correctly handle OpenAI-compatible provider name/ID mangling across transcript repair and tool result pairing. The only non-trivial issue is an unreachable else if branch at line 314 in session-transcript-repair.ts that should be removed for code clarity, but poses no runtime risk.
  • src/agents/session-transcript-repair.ts (line 314–321): Remove unreachable else if branch to simplify logic.

Comments Outside Diff (1)

  1. src/agents/session-transcript-repair.ts, line 314-321 (link)

    The else if (rawName !== trimmedName && trimmedName) branch is unreachable after introducing normalizePrefixedToolName.

    Since normalizedName is always either trimmedName itself (when no prefix) or a trimmed, prefix-stripped version of trimmedName, any rawName that differs from normalizedName will be caught by the first if condition. The only way to reach this else if would require rawName === normalizedName !== trimmedName, which is impossible because:

    • If they're equal (rawName === normalizedName), then rawName === normalizedName = normalizePrefixedToolName(trimmedName), meaning rawName === trimmedName (no prefix difference; .trim() preserves the name structure)
    • If normalizedName is falsy, then trimmedName is also falsy, and the else if condition fails anyway

    Consider simplifying by removing this dead branch:

Last reviewed commit: 7de7ed8

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7de7ed80a6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +306 to +310
const normalizedName = trimmedName
? normalizePrefixedToolName(trimmedName)
: trimmedName;
if (rawName !== normalizedName && normalizedName) {
const renamed = { ...(block as object), name: normalizedName } as typeof block;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact prefixed sessions_spawn calls before renaming

When a provider emits a prefixed tool name like functions.sessions_spawn, the blockName check has already skipped the sessions_spawn redaction path, and this new branch only renames the tool to sessions_spawn. Because sanitizeToolCallBlock is never invoked here, input.attachments[].content is persisted unredacted for these prefixed calls, which regresses transcript privacy for OpenAI-compatible providers that mangle tool names.

Useful? React with 👍 / 👎.

@deepujain
Copy link
Copy Markdown
Author

Codex P1: redaction now runs for prefixed tool names (e.g. functions.sessions_spawn).

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b488bcaaf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const toolCallId = (msg as { toolCallId?: unknown }).toolCallId;
if (typeof toolCallId === "string" && toolCallId) {
return toolCallId;
return normalizeMangledToolCallId(toolCallId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve raw tool result IDs for pending-state lookups

Normalizing IDs inside extractToolResultId makes downstream code see a different key than what was tracked from assistant tool calls. In installSessionToolResultGuard, pending calls are stored by raw call.id (session-tool-result-state.ts) but removed via extractToolResultId (session-tool-result-guard.ts), so a mangled ID like functions exec:0 now gets looked up as functions.exec:0 and is never deleted. When the next non-tool message arrives, the stale pending entry can trigger a synthetic "missing tool result" even though the real tool result was already written.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant