Unexpected end of JSON input appeared only after the next step opened a file that was 8,192 bytes on disk instead of the expected ~20 KB.
read artifact: run-output.json
size on disk: 8192 bytes
parse result: SyntaxError: Unexpected end of JSON input
preceding tool result: MCP tool call timed out after 60s
The confusing part was the order of evidence. The write step did not say “I left a corrupt file.” It said the tool call timed out. That sounded transient, so the agent moved to the next planned action and opened run-output.json. Only then did the real damage become visible: the file existed, had fresh mtime, and began with valid JSON, but it ended halfway through a string inside a Markdown payload. There was no closing quote, no closing object, and no separate error tying the broken syntax back to the interrupted write.
The write path had one unsafe assumption: once bytes were flushed to the requested path, cancellation could still be treated like a failed operation with no durable side effects. That is false for direct destination writes. At 60 seconds, tool_timeout_sec cancelled the MCP call while the tool process was still serializing a roughly 20 KB artifact. The kernel had already accepted the first 8 KB, so the filesystem state after cancellation was not “old file” or “no file”; it was the prefix of the new file.
A slow or killed writer can now abandon only a temp inode. The visible artifact changes state in one operation, after the complete JSON has been written, flushed, and renamed into place. Every reader finds one complete state on disk: either the whole previous file or the whole replacement file, never the truncated prefix of a cancelled stream.