Skip to content

fix(sdk): store and report one spelling of an upload's filename - #30

Merged
matej21 merged 1 commit into
mainfrom
fix/upload-filename-nfc
Aug 31, 2026
Merged

fix(sdk): store and report one spelling of an upload's filename#30
matej21 merged 1 commit into
mainfrom
fix/upload-filename-nfc

Conversation

@matej21

@matej21 matej21 commented Aug 31, 2026

Copy link
Copy Markdown
Member

Unicode lets the same filename be two different byte strings. macOS hands over the decomposed form — á as a + U+0301 — and that is what the uploads plugin wrote to disk and put in the <attachment filename="…"> tag it shows the agent. A model asked to use that name writes the composed form back, because that is what models produce. The two do not compare equal, so every path the agent builds from the name it was given misses the file that is actually there:

cp: cannot stat '…/uploads/<id>/Veronika Paulova'$'\303\241''_ucitelka.jpg': No such file or directory

Observed on a production session with Czech filenames: 21 of its 95 tool failures, second only to a path-resolution bug. There is nothing the agent can do about it — it never sees the bytes, and retrying produces the same string. It spent turns on ls, find and a second cp before giving up on the attachment entirely.

The fix

Compose on the way in, once, in prepareUpload. The stored name is carried on PreparedUpload, and both upload handlers now build their UploadDescriptor from that rather than from their own copy of the input — so the metadata, the attachment_uploaded event and the directory entry agree by construction instead of by coincidence.

Only the spelling changes. NFC cannot introduce a path separator, and validateUploadInput runs on the normalized name, so the basename check still guards what is written.

Verification

bun run ts:build clean. bun test packages/sdk/src/plugins/uploads/ 61 pass / 0 fail.

The new test fails without the fix (verified by neutering toStoredFilename), and the wider packages/sdk suite has an identical failure set on this branch and on main — 26 either way, all in services/skills/mailbox/agent-shutdown, none touched here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MjPDwksb3bAUC4fCVi3QXS

Unicode lets the same name be two different byte strings. macOS hands over the
decomposed form — `á` as `a` + U+0301 — and that is what the plugin wrote to
disk and put in the `<attachment filename="…">` tag. A model asked to use that
name writes the composed form back, because that is what it produces. The two
do not compare equal, so every path the agent builds from the name it was given
misses the file that is actually there:

    cp: cannot stat '…/Veronika Paulova'$'\303\241''_ucitelka.jpg': No such file

Observed on a production session with Czech filenames: 21 of its 95 tool
failures, second only to a path-resolution bug. Nothing the agent can do about
it — it never sees the bytes, and retrying produces the same string. It burned
turns on `ls`, `find` and a second `cp` before giving up on the attachment.

So the plugin now composes on the way in, once, in `prepareUpload`. The stored
name is carried on `PreparedUpload` and both upload handlers build their
`UploadDescriptor` from it rather than from their own input, which is what makes
the metadata, the event and the directory entry agree by construction instead of
by coincidence.

Only the spelling changes: NFC cannot introduce a path separator, and validation
runs on the normalized name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPDwksb3bAUC4fCVi3QXS
@matej21
matej21 merged commit 1b31bc8 into main Aug 31, 2026
1 check passed
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