Day 147: It Looked Like the Agent Couldn't Read
An agent named Piper, running kimi-k2.6, got a support ticket as a PDF and couldn’t do anything with it. Turn after turn, it reported that the file “seems not yet fully available in the workspace,” which sounds like a patient agent waiting for a slow upload. Except the file was completely fine: 244,491 bytes, present and byte-identical in both containers, exactly where it should be. From the outside this reads as a dumb agent flubbing a trivial task. It’s the same illusion as Day 144, where an agent that seemed to be inventing filenames was actually reaching for a file the tools weren’t allowed to open: the agent looks incompetent, and the agent is not the problem. Underneath Piper’s flailing were two entirely separate tool bugs, and it took reading the logs to see that the agent had been telling the truth the whole time about something being wrong.
Bug one: two tools, one tempting name
Pinchy’s design says every file read goes through one tool, pinchy_read, which knows how to reach workspace uploads. But OpenClaw ships built-in tools called pdf and image, and PR #724 found those had been left sitting in every agent’s allowlist. So a model handed a PDF sees two options: the pinchy_read it was instructed to use, and a tool literally named pdf. For a strong instruction-follower that’s fine. For a weaker one, the on-the-nose name wins, and it calls pdf, which cannot read a workspace upload path and fails with “Local media file not found.” Piper even tried URL-encoding the path to make the wrong tool work. The design intended exactly one reader; the allowlist quietly offered two, and the decoy was better named than the real thing. The fix is to stop offering the decoy: remove pdf and image from the allowlist so there is one way to read a file and no seductively-named wrong turn.
Bug two: an ä that was two different ä’s
Force the agent onto pinchy_read and it still failed on that same PDF, “no such file or directory,” and only started working after the user renamed the file to epic-2483.pdf. The original name had an ä in it. That ä is where PR #729 went spelunking through raw bytes. On disk, the ä was stored as the bytes 61 cc 88: an a followed by a combining diaeresis, the decomposed form, because macOS hands the browser filenames in decomposed Unicode. But the path the model passed back was c3 a4, the single composed ä codepoint, because JSON round-trips and language-model output use the composed form. To your eye and mine, those are the same letter. To Linux, which does no Unicode folding at all, they are two different filenames, and readFile of the composed path simply does not find the decomposed file. ENOENT. The agent asked for the file it saw. The file on disk had the same name spelled in different bytes.
The fix normalizes filenames to composed form at the moment of upload, and a companion, PR #732, teaches the reader to try the path as given, then composed, then decomposed, so files already stored the old way become readable without a re-upload. There’s even a testing trap in here: the bug only reproduces on a filesystem that distinguishes the two forms, like Linux, while macOS folds them, so a test written on a Mac would pass without the fix ever being there.
Day 147
Both bugs produced the same surface: an agent that looked like it couldn’t handle a file a person would call trivial. And that’s the trap of building on top of a language model. When something fails, the model’s fumbling is the loudest signal in the room, so the instinct is to blame the model, tune the prompt, pick a smarter one. Sometimes that’s right. Here it would have wasted days, because the model was reacting honestly to a tool that offered it a bad option and a filesystem that hid a file behind an invisible byte difference. An agent that seems incompetent is a hypothesis, not a diagnosis. The only way to tell a genuinely confused model from a broken tool under a confused-looking model is to stop watching the agent and go read what the tools actually did.