Skip to content

Drop garbled functions.<name>({...}) wrappers from replay history#515

Open
aj47 wants to merge 1 commit into
mainfrom
claude/confident-noether-95nb4
Open

Drop garbled functions.<name>({...}) wrappers from replay history#515
aj47 wants to merge 1 commit into
mainfrom
claude/confident-noether-95nb4

Conversation

@aj47

@aj47 aj47 commented May 28, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the defensive-guard gap called out at the end of #401: when the model emits a plain-text functions.respond_to_user({...}) (or similar) wrapper alongside a real structured tool call, persisting that text into conversation history replays it back to the model as if the user had pasted a tool transcript. That's what produced the malformed functions.respond_to_user({ ... }) assistant turns the issue documented.

The primary "mobile branch endpoint" part of #401 is already implemented (remote-server POST /v1/conversations/:id/branch at apps/desktop/src/main/remote-server.ts:6008 and mobile client branchConversation at apps/mobile/src/lib/settingsApi.ts:1094), so this PR focuses on the related guard.

What changed

apps/desktop/src/main/llm-continuation-guards.ts

  • Extend GARBLED_TOOL_CALL_TEXT_REGEX to match bare top-level functions.<name>({ wrappers (no to= prefix or [Calling tools: …] placeholder needed). The existing alternatives already covered multi_tool_use.*, to=functions.*, etc.; this adds the bare-wrapper case the issue called out.
  • Add sanitizeAssistantContentForToolCalls(content, toolCalls) — returns "" when structured tool calls are present AND the content matches isGarbledToolCallText, otherwise passes content through unchanged. Plain prose alongside tool calls (e.g. "Running the tests now.") is preserved.

apps/desktop/src/main/llm.ts

  • Use sanitizeAssistantContentForToolCalls at the single persist site that records llmResponse.content alongside llmResponse.toolCalls (was line 3061). Garbled text now drops out before it reaches addMessage / disk / replay; the structured tool call itself is untouched.

apps/desktop/src/main/llm.continuation-guards.test.ts

  • Adds a isGarbledToolCallText case covering functions.respond_to_user({...}) and functions.execute_command({...}), plus a negative case for prose mentioning "functions" without the call+object shape.
  • Adds a sanitizeAssistantContentForToolCalls case that asserts strip-on-tool-calls, pass-through without tool calls, prose preserved alongside tool calls, and empty/undefined input.

Test plan

  • pnpm --filter @dotagents/desktop exec vitest run src/main/llm.continuation-guards.test.ts — 23/23 pass
  • pnpm --filter @dotagents/desktop exec vitest run src/main/llm.respond-to-user-history.test.ts — 47/47 pass
  • pnpm --filter @dotagents/desktop run typecheck:node — clean
  • Manual: in a session that produces both a structured mark_work_complete tool call and garbled functions.respond_to_user({…}) plain-text content, confirm the persisted assistant message has empty content and the next turn does not see the wrapper in replay history.

Out of scope

  • The broader #401 work (a server-side branch endpoint reachable from mobile) is already done — this PR only closes the related defensive-guard gap mentioned in the same issue.
  • Mobile-side sanitization (apps/mobile/src/lib/openaiClient.ts:sanitizeMessagesForRequest) does not currently inspect content. If mobile reads back a contaminated server conversation, the desktop fix above already prevents new contamination at the source; remediating already-persisted bad history is a separate cleanup.

https://claude.ai/code/session_01RSWSRZsLGgkLdhZ67zfqzT


Generated by Claude Code

Closes the defensive-guard gap called out in #401: the model occasionally
emits plain-text `functions.respond_to_user({...})` (or similar) alongside a
real structured tool call. Persisting that text into conversation history
replays it back to the model as if the user had pasted a tool transcript,
which destabilises later turns.

- Extend `GARBLED_TOOL_CALL_TEXT_REGEX` to match bare top-level
  `functions.<name>({` wrappers that show up without a `to=` prefix or a
  `[Calling tools: …]` placeholder.
- Add `sanitizeAssistantContentForToolCalls` and use it at the persist site
  in `llm.ts` so garbled text is stripped only when real structured tool
  calls are present; plain prose alongside tool calls is preserved.
- Cover both behaviours in `llm.continuation-guards.test.ts`.

https://claude.ai/code/session_01RSWSRZsLGgkLdhZ67zfqzT

@augment-app-staging augment-app-staging Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

if (hasStructuredToolCalls && isGarbledToolCallText(raw)) {
return ""
}
return raw

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Because isGarbledToolCallText matches substrings, sanitizeAssistantContentForToolCalls will drop the entire assistant content if it contains a functions.<name>({ snippet anywhere, even when there’s legitimate prose in the same message. This could unintentionally remove user-facing progress/explanation text whenever structured tool calls are present.

Severity: medium

🤖 Was this useful? React with 👍 or 👎

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants