After lesson 07, Redact works well when everything goes right. But local AI has its own ways of going wrong: the service isn’t running, the first model load takes minutes, a page is slow, you change your mind halfway. In this lesson we make sure none of that ever costs you the document. Checkpoint: redact-08-failure-recovery, after redact-07-review

What fails when AI runs locally

With a cloud API, someone else keeps the model loaded and the server up. Locally, that’s your machine’s job, and each part can be missing or slow. The rule behind all of them is the same. The AI is an extra, not a requirement: if it fails, you’re left with the page you prepared and the manual tools from lesson 01.

The stale-answer problem

Cancelling raises a subtle problem. You cancel, start a new analysis, and then the old one finishes. Without care, its suggestions would land on top of your new review. Redact gives each analysis a number. When an answer comes back, it’s only used if its number is still the latest; anything older is ignored. It’s a small idea that prevents a confusing class of bugs.

1. Get this lesson’s code

Stop the app and the service if they’re running. From the repository root, redact-course, bring in this lesson’s commit:
No packages change, so there’s nothing to install. To see every change, open the comparison on GitHub.

2. Read the browser’s timeout

Open src/features/redact/analysis-client.ts. Every request now goes through one helper:
An AbortController is the browser’s way to stop a request. This one stops for either of two reasons: the app asked it to (you cancelled), or 190 seconds passed. The catch that follows turns every failure into a message a person can read:
When nothing answers at all, the browser only says something like “Failed to fetch”, as a TypeError. Here that becomes “The local AI service isn’t running”.

3. Read the stale-answer guard

Open src/features/redact/RedactApp.tsx and find prepare. Each run takes the next number:
After every await, it checks the number is still current before touching the screen:
Cancelling simply moves the counter on, which makes every answer from the old run stale:
Notice the last line: it opens review with the pages already prepared, plus any evidence that arrived before you cancelled.

4. Read the service’s single slot

Open qvac-service/server.ts and find createQvacService. The service runs one analysis at a time:
And it only frees the slot when the model really stops, not when the request gives up:
withTimeout answers the request with 504 after 180 seconds. But the model can’t be interrupted mid-page, so busy stays true until operation finishes. Two analyses fighting over the same model would only make both slower.

5. Fail with the service stopped

Start only the app:
Upload public/test-files/redact/chat-private.png, choose Private, and select Prepare for review. Don’t start the service. The review screen opens anyway, with the banner The local AI service isn’t running. You can still redact by hand. and a Retry local analysis button. There are no suggestions, but the page is there. Drag a mark over Maya Chen, select Done, then Save copy: you get a normal chat-private.redacted.png.

6. Retry once the service is back

Select Return to review. The banner and Retry local analysis are still there: saving a copy doesn’t clear an analysis that never ran. In a second terminal, start the service:
Wait for Redact QVAC service listening on http://127.0.0.1:4317, then select Retry local analysis. After about half a minute the suggestions appear next to the mark you already drew, six of them with no direction typed, and the banner goes away.

7. Cancel a run

Select Change settings, then Prepare for review again. While the screen says Building your review copy and Reading page 1 of 1 locally…, select Continue without local analysis. The review screen opens straight away, with the banner Local analysis was cancelled. You can still redact by hand. Your page is ready for manual marks.

8. See the single slot

Cancelling stopped the browser waiting, but the model may still be finishing that page. Ask the service straight away:
If the run is still going you’ll see "busy":true. While it’s busy, any new analysis is refused:
Run both again once the model has finished that page, about 15 seconds in our testing. Health shows "busy":false, and the POST now returns {"error":{"code":"invalid_request",...}}, because {} isn’t a page.

If something goes wrong

  • The banner says “Local analyzer is busy”. A cancelled run is still finishing. Wait about 15 seconds, then Retry local analysis.
  • curl says connection refused. The service isn’t running; start it with npm run qvac:service.
  • You never see "busy":true. The page finished before you asked. That’s fine; the second response proves the slot is free again.

9. Check your work

Stop both terminals, then:
npm test reports Tests 76 passed (76). These tests also simulate a timeout, a malformed response and a stale answer, which are hard to trigger by hand.

So what just happened?

Every way local analysis can fail now ends in the same place: your prepared page, with manual tools and a way to retry. Old answers can’t overwrite new ones, and the service never runs two analyses on top of each other.

Try it yourself

Start an analysis and cancel it, then immediately select Retry local analysis. You’ll probably see “Local analyzer is busy”. Retry again after a few seconds and it works; lesson 11 makes the app wait for the slot on its own.

Where to next

Next: Measure suggestions against labelled documents (lesson 09)