The auto writer had one very annoying habit this week: it would generate a clean article, get halfway through a sentence near the end, then just stop. No warning. No nice closing paragraph. Just a dangling phrase like, “The final step is to make sure your publishing flow…” and then silence.
At first, I thought it was a prompt issue. Maybe the model was getting lazy. Maybe my outline was too long. Maybe the ending instructions were too vague. After a bit of poking around, the real problem was simpler and more mechanical: the generation was hitting the token ceiling.
So today’s fix was all about teaching the auto writer to respect the size of the article before it starts writing, then recover cleanly if it still runs out of room. Not glamorous. Very useful.
The bug showed up in the worst possible place
The auto writer was already doing a decent job with structure. It could take a topic, create sections, write in a specific style, and return Gutenberg-ready blocks. The issue was output length. Longer posts looked fine until the final section, where they would sometimes cut off mid-sentence.
That made the whole article unusable without manual cleanup. Even worse, the cutoff did not always happen at the same place. A 1,200 word post might finish fine. A 1,500 word post might get clipped. A 2,000 word post almost always needed babysitting.
My first small win was catching the pattern. The content was not failing because of the topic or the writing style. It was failing because the request was asking for more output than the model was allowed to return in one response.
The model was not “forgetting” to finish. I had boxed it into a response limit, then acted surprised when it hit the wall.
The first check: stop reason
The fix started with better logging. I added a small debug panel around the API response so I could see not just the text, but also why the generation stopped.
That one detail changed the whole debugging session. If the stop reason said the model reached the length limit, then the article was not “done.” It was simply out of output space. That meant the app should not accept the result as final.
Before this, the writer treated any response as a finished article. That was the mistake. A response can be valid JSON, clean HTML, and still be incomplete. So I added a completion check before saving the article.
- If the stop reason says the output hit the length limit, mark the draft as incomplete.
- If the final paragraph does not close properly, mark it as suspicious.
- If the response ends without a closing Gutenberg block, do not publish it.
- If the article is incomplete, ask for a continuation instead of throwing the draft away.
This was the first real course correction. The app needed to stop pretending every response was safe.
The actual cause: my token budget was too optimistic
I had set a fixed max output value and assumed it would cover most posts. That worked while the articles were short. Once the prompts got more detailed, the model had less room left for the actual article.
This is the trap: the total context window includes more than the final article. It includes system instructions, style rules, formatting rules, the topic brief, examples, internal constraints, and then the output. If the prompt gets heavy, the available space for the article shrinks.
My prompt had grown over time. I kept adding helpful rules: write naturally, use Gutenberg HTML, avoid certain phrases, include plain-language build notes, keep the tone practical, match the target word count. Each rule made the output better, but the full request also got heavier.
The fix was not just “increase max tokens.” That helps, but only until the next long article breaks again. I needed a budget system.
Building a simple token ceiling check
I added a preflight step before calling the model. Nothing fancy. It estimates how much room the prompt will use, how much room the target article will need, and whether the whole request fits inside the model’s limits.
The first version was rough. I used word count as a stand-in for tokens. It is not perfect, but it is good enough to catch obvious failures. Later, I swapped in a real tokenizer so the estimate got closer to reality.
| Part | What I checked | Why it mattered |
|---|---|---|
| Prompt | System rules, style rules, topic details | These eat into the available space before writing starts |
| Target length | Expected article word count | Longer articles need a larger output allowance |
| Formatting | Gutenberg comments and HTML tags | Markup adds extra tokens beyond plain text |
| Safety margin | Extra space left unused | This prevents tight requests from cutting off at the end |
The safety margin ended up being the quiet hero of the fix. If the estimate says the article needs 4,000 tokens, I do not set the limit at exactly 4,000. I give it breathing room. Otherwise, one long paragraph or a few extra list items can push the output into cutoff territory.
I stopped asking for big articles in one pass
The cleaner solution was splitting long articles into sections. Instead of asking the model to write a full 2,000 word piece in one response, the app now creates an outline first, then writes each section in its own pass.
This felt like a bigger change, but it made the whole writer more predictable. Each section has a smaller job. The model knows the heading, the purpose of the section, the approximate length, and the tone. Then the app stitches the sections together.
I resisted this at first because one-shot generation feels simpler. One prompt in, one article out. But the longer I worked on the auto writer, the more obvious it became that one-shot generation is fragile for serious content. Section-based writing gives more control.
The section-by-section flow
- Generate the outline with clear headings and rough word counts.
- Write the intro as its own block.
- Write each body section separately, using the outline as the map.
- Pass a short memory note between sections so the article does not repeat itself.
- Write the ending only after the body is complete.
- Run a final cleanup pass for flow, formatting, and incomplete blocks.
The memory note matters. Without it, section-based writing can feel choppy. The app now keeps a short running note like, “Already explained token limits and stop reasons. Next section should focus on continuation recovery.” That tiny bit of context keeps the writer from looping.
Continuation was the backup plan
Even with better budgeting and section writing, I still wanted a recovery path. APIs fail. Estimates are estimates. Sometimes a section runs long because the model adds more detail than expected.
So I built a continuation flow. If the response ends because of the length limit, the app sends the partial content back with a very direct instruction: continue from the exact point where the previous response stopped, do not restart, do not repeat completed sections, and close any open Gutenberg blocks cleanly.
The first attempt was messy. The model sometimes repeated the last paragraph. Sometimes it restarted the entire section. Sometimes it tried to add a new intro. That was on me. My continuation prompt was too polite and too broad.
The better version was stricter. I passed the last 300 to 500 words of the article, the current section heading, and a note saying which blocks were already complete. Then I asked only for the missing continuation.
The trick was not asking, “Can you continue?” The trick was saying, “Start after this exact text and only finish the unfinished section.”
Fixing broken Gutenberg blocks
The cutoff did not just break sentences. It also broke HTML and Gutenberg comments. A response might end after an opening paragraph tag or halfway through a list item. That created invalid block markup, which is the kind of bug that looks small until it hits the editor.
I added a validation step after every generation. It checks for common block problems before the content moves forward.
- Every paragraph block needs an opening and closing block comment.
- Every HTML tag that opens needs to close.
- Lists need proper list item structure.
- Headings should not be empty.
- The final block should not end mid-tag or mid-word.
I did not try to build a perfect HTML parser in the first pass. I just caught the obvious stuff. That was enough to prevent broken drafts from getting saved as finished articles.
The small test that proved the fix
After wiring in the budget check, section writing, and continuation flow, I ran the same article prompts that were failing before. This time, I tested them in three sizes: short, medium, and long.
| Test | Before | After |
|---|---|---|
| Short article | Usually finished | Finished cleanly |
| Medium article | Sometimes clipped near the end | Finished with valid blocks |
| Long article | Often cut off mid-sentence | Split into sections and completed |
| Forced low token limit | Saved incomplete draft | Triggered continuation flow |
The forced low-limit test was my favorite one. I intentionally gave the writer too little room so it would fail. Before the fix, it would have saved a broken article. After the fix, it noticed the cutoff, requested the missing content, repaired the ending, and then passed validation.
That is the kind of test I like. Not just “does it work when everything goes right,” but “does it behave when I make it trip?”
What changed in the app logic
The auto writer now has a more careful path from idea to finished draft. The old flow was too trusting. The new flow checks the work at each stage.
- Estimate the prompt size and expected output size before writing.
- Choose one-shot writing only when the article safely fits.
- Switch to section writing for longer pieces.
- Watch the stop reason on every model response.
- Run continuation when the model hits the length limit.
- Validate Gutenberg block structure before saving.
- Flag anything suspicious for review instead of pretending it is finished.
This made the tool feel calmer. That sounds odd, but it is true. Before, I had to scan every generated article with suspicion. Now the app catches the most common failure mode on its own.
The prompt got shorter too
One surprise from this bug: the prompt itself needed trimming. I had repeated a few instructions in different places. Some style rules were saying almost the same thing twice. A few formatting notes belonged in the section prompt, not the global prompt.
So I cleaned it up. The global prompt now handles identity, tone, and hard formatting rules. The section prompt handles the local job. That saves space and makes each request easier for the model to follow.
This was a good reminder that prompts age like code. They start clean, then collect patches. Every so often, they need refactoring.
A few details that made the fix better
There were a couple of tiny adjustments that helped more than expected.
- I added a target word range per section instead of one big word count for the whole article.
- I told the model not to write a closing section until the app asks for it.
- I kept the continuation context short so it had room to finish the missing part.
- I made the validator return plain error labels, like “open paragraph block” or “cut off sentence,” so debugging is fast.
- I saved incomplete drafts in a separate state instead of overwriting them as final content.
That last one saved me during testing. When a generation failed, I could inspect the partial article, the stop reason, and the recovery attempt side by side. Much easier than trying to guess what happened from the final output alone.
The builder lesson from this one
This bug felt like a writing problem, but it was really a systems problem. The model was doing what I asked inside the limits I gave it. The app was the part that needed better judgment.
The bigger lesson is that AI writing tools need exit checks. You cannot treat text generation like a normal function that always returns a complete answer. Sometimes it returns half an answer. Sometimes it returns a nicely formatted failure. Sometimes it gives you exactly enough content to look finished at a glance, while the last block is broken underneath.
So the tool now assumes less and checks more. Did the model stop naturally? Did the markup close? Did the article reach the requested range? Did the last sentence finish? If not, the app keeps working.
That is the difference between a fun demo and a tool I can trust for repeat use.
What I would do first if you are building this
If you are building your own auto writer, I would add these pieces early. They are not hard, and they save a lot of cleanup later.
- Log the stop reason from every model response.
- Set a real output allowance instead of guessing.
- Use section-by-section generation for longer articles.
- Keep a short memory note between sections.
- Validate the final format before saving or publishing.
- Build a continuation path for clipped responses.
You do not need the perfect version on day one. Start with stop reason logging and a simple “incomplete” flag. Then add continuation. Then split long posts into sections. Each step makes the tool more dependable.
Where the auto writer stands now
The mid-sentence cutoff is fixed. More accurately, the app now knows how to avoid it most of the time and recover when it still happens.
That feels like the right kind of fix. Not a magic setting. Not a bigger number slapped onto the API call. A better writing flow, a safer token budget, and a few guardrails around completion.
Next I want to improve the final review pass so it can catch softer issues too, like repeated points, weak endings, or sections that drift away from the brief. But for now, the writer can finish its sentences. That is a very satisfying checkbox to tick.