Skip to content

CL-6478: Sanitize tool-call names before persisting turn history - #210

Merged
TheGreatAxios merged 3 commits into
mainfrom
cl-6478-tool-name
Aug 21, 2026
Merged

TheGreatAxios merged 3 commits into
mainfrom
cl-6478-tool-name

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

Summary

  • A model can leak wire fragments into a tool-call name (observed: qwen3.8:27b emitted \n</parameter inside a function name). @intx/inference's decodeToolName is deliberately total and returns such a name verbatim rather than throwing, but encodeToolName is not total -- it throws when that same name is later put back on the wire to build the next turn's outbound request. Persisting the decoded name unchecked meant the fault surfaced one turn late, after the bad data was already durable: every following turn in the room died rebuilding its request, permanently.
  • vendor/intx/hub-sessions/src/event-collector.ts is the one place a decoded tool-call name is accepted into turn history (turnPart). It now runs the name through a new sanitizeToolNameForPersistence (vendor/intx/hub-sessions/src/sanitize-tool-name.ts) before writing it: a name encodeToolName can re-invert persists as-is, anything else collapses to a stable malformed_tool_call placeholder. A bad tool-call name now fails its own turn cleanly instead of wedging the room.
  • encodeToolName/decodeToolName themselves live only in the published @intx/inference npm package (already retired from vendoring in VENDORED.md), so this fixes the boundary on our side rather than touching that pair. vendor/intx/hub-sessions is already a vendored, editable tree (VENDORED.md), so the fix, its delta line, and the kill-date tree hash land together.

Not done (out of timebox)

  • Recovery for already-poisoned rooms: this fix stops new malformed names from being persisted, but any room whose history already has a bad name from prior testing is not backfilled/sanitized retroactively. That would need a one-off migration over existing turnPart rows (or a read-side sanitize wherever persisted turns are reloaded to build the next request) and I did not implement or verify it.
  • I did not trace the exact call site inside @intx/agent/@intx/inference where a persisted turn's tool-call name is re-encoded for the next outbound request (that code lives entirely in the npm-only package, not in this repo), so the "request-building dies in ~40ms" failure is reproduced by direct assertion (encodeToolName on the previously-malformed persisted name no longer throws) rather than an end-to-end run through the real agent/harness.
  • bun run check:killdates reports one pre-existing failure, vendor/intx/db hash drift -- confirmed present on a pristine origin/main checkout untouched by this branch, unrelated to this change, not fixed here.

Test plan

  • bun test vendor/intx/hub-sessions/src/ -- 59 pass, including the new CL-6478 regression suite (malformed name persists as a safe placeholder, encodeToolName on the persisted name doesn't throw, and the collector accepts a second turn afterward) and the sanitize-tool-name round-trip property tests
  • bun test packages/tool-registry-publish/src/tool-name-limits.test.ts -- 15 pass (the existing 64-char shipped-tool-name guard, unchanged)
  • bun run lint from repo root -- 0 errors (12 pre-existing warnings in files this PR doesn't touch)
  • tsc --noEmit in vendor/intx/hub-sessions -- clean
  • Not run against a live stack (per instructions, no stack booted)

DO NOT MERGE -- for peer review.

Fixes CL-6478.

decodeToolName is deliberately total: a hallucinated or provider-mangled
tool name comes back verbatim instead of throwing. encodeToolName is not
total -- it throws when a name can't be put back on the wire. Add
sanitizeToolNameForPersistence so any name that fails that round trip
collapses to a stable placeholder instead of being written down as-is.
A malformed tool-call name (e.g. a small model leaking wire fragments
into a function name) was being persisted verbatim. The next turn's
outbound request re-encodes every persisted tool-call name, and that
encode throws on a name it can't re-invert -- so the room died rebuilding
its request, permanently, every turn after the bad one landed.

event-collector.ts now runs a tool_call block's name through
sanitizeToolNameForPersistence before it reaches turnPart, so a bad name
fails its own turn cleanly instead of wedging the room.
Ledger row, VENDORED-FROM delta line, and kill-date tree hash for the
sanitize-tool-name.ts addition to vendor/intx/hub-sessions.
@TheGreatAxios
TheGreatAxios merged commit 37268d2 into main Aug 21, 2026
0 of 2 checks passed
@TheGreatAxios
TheGreatAxios deleted the cl-6478-tool-name branch August 25, 2026 15:29
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.

1 participant