Skip to content

A file is read into words once, and the words are what travels - #34

Merged
thankywal merged 1 commit into
mainfrom
a-file-is-read-once
Sep 3, 2026
Merged

thankywal merged 1 commit into
mainfrom
a-file-is-read-once

Conversation

@thankywal

Copy link
Copy Markdown
Owner

What the owner saw

On a fresh account, a price-list PDF sent in the conversation was read fine on arrival — the assistant knew the file by name — and then every attempt to file it under the business came back:

Hanbit Beauty is set up, but the PDF failed to add to its knowledge again due to a conversion error…

The mechanism, from the code

add_file_to_business had the kept text and handed it to addDocument encoded back into bytes, under the file's own name ….pdf, with contentType: "text/plain":

contentType: "text/plain",
body: new TextEncoder().encode(file.text).buffer

readUpload decides what a file is from its name, so it sent a plain-text "PDF" to env.AI.toMarkdown. Cloudflare's supported formats table has no plain text, so the converter answered format: "error"unwrapConversion threw conversion failed → the loop gave the model Failed: conversion failed → "conversion error".

  • Deterministic → "again".
  • Account-independent → the fresh account was a coincidence. The main account had only ever filed files through the console's Add data screen, which sends real bytes.
  • Invisible to CI → the covering test mocked addDocument wholesale, so it proved the tool's intent and nothing about what the reader did next.

The change

IngestInput is now IngestFile | IngestText. Words already read travel as text, are never converted and never archived (there is no original to archive), and are recorded as text/plain at the text's own byte size under the owner's filename. The tool hands on text: file.text. Bytes from the console's Add data screen and from Telegram go through the reader exactly as before.

Tests — 956 passing (+5)

a-file-is-read-once.test.ts runs the real ingestDocument with only the platform faked, and its toMarkdown throws the moment it is reached:

  • words under a .pdf name are filed without a second reading;
  • they are recorded as the text they are, not as the file they came from;
  • empty text is refused in the same words a file is;
  • a real PDF body still goes to the converter unchanged;
  • no tool re-encodes kept text into a body (source-asserted across tools.ts).

The old test's mock now throws if handed bytes, and says where the real check lives.

Not proved here

Workers AI cannot be exercised from the build machine (wrangler whoami: not logged in; no token in the environment), so the live proof is the owner's deployment after this lands: send the PDF in the conversation, say yes to filing it, and the prices should follow.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NDkiHs276KQhSWxQnHQdLg

The owner sent a price list in the conversation. It was read on arrival, the
text was kept, and the assistant proposed filing it under the business. Every
attempt came back "conversion failed", so the assistant told the owner the PDF
could not be added and asked for a different format.

`add_file_to_business` had encoded the kept text back into bytes and handed
them to `addDocument` under the file's own name, `….pdf`, with
`contentType: "text/plain"`. `readUpload` decides what a file is from its
name, so it sent a plain-text "PDF" to `env.AI.toMarkdown`. The platform's
supported-formats table has no plain text, so the converter answered
`format: "error"`, `unwrapConversion` threw `conversion failed`, and the loop
handed the model `Failed: conversion failed`. Deterministic, so "again";
account-independent, so the fresh account was a coincidence — the main account
had only ever filed files through the console's Add data screen, which sends
real bytes.

The covering test mocked `addDocument` wholesale, so it proved the tool's
intent and nothing about what the reader did next. A mock at the door hides
what the door does.

- `IngestInput` is now `IngestFile | IngestText`. Words already read travel as
  `text`, are never converted and never archived (there is no original to
  archive; a copy of the text under the file's name would be an archive of
  something that is not the file). They are recorded as `text/plain` at the
  text's own byte size, under the owner's filename.
- The tool hands on `text: file.text`.
- `a-file-is-read-once.test.ts` runs the REAL `ingestDocument` with only the
  platform faked, and its converter throws the moment it is reached. It also
  proves bytes still go to the reader unchanged, and holds the whole of the
  assistant's tools to never re-encoding kept text into a body.

956 tests, lint and typecheck clean. Workers AI cannot be exercised from this
machine (wrangler is not logged in), so the live proof is the owner's own
deployment after this lands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDkiHs276KQhSWxQnHQdLg
@thankywal
thankywal merged commit 0d1017b into main Sep 3, 2026
2 checks passed
@thankywal
thankywal deleted the a-file-is-read-once branch September 3, 2026 13:47
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