It was late, and I was prototyping a tiny screen in Rork: pick a sound to fall asleep to, set a number of minutes, and have it stop on its own. I've run a healing-sounds app for a long time as an indie developer, which means I carry a lot of assumptions about how "stop when the time is up" should work. That's exactly why I wanted to watch an AI build it from nothing.
The first generation was lovely. Fifteen minutes in, fifteen minutes later, the sound stopped while I stared at the screen. Then I went back to the home screen, reopened the timer screen, and sometimes it never stopped at all. That's where three rounds of fix requests began.
The first time I wrote "it sometimes doesn't stop." The second time, "it still doesn't stop." Halfway through typing the third message, my hands paused. Do I send this as the next message in the same thread, or do I go back to the version that worked and ask in a different way? I ended up doing both, so here's what happened and the line I've drawn since.
The short version: from the third attempt on, I go back
Let me put the conclusion first. When a fix request for the same bug reaches its third round, I stop asking in the same thread. I restore the last version that worked and ask again with different words.
Rork's FAQ says the Restore feature lets you revert to any previous version, and that you can preview each version before restoring so you pick the right one. The same FAQ lists three ways out of an error loop: ask Rork to search the web, switch to a different model, or revert to a working version and try the feature again with a different approach.
One thing I had misread at first: the FAQ also says AI errors are not charged as credits. The way I read it now, that covers cases where the generation itself fails. A fix that runs but does the wrong thing, like mine, costs a normal round trip. With the Free plan at 5 credits per day and Pro at 100 per month, rounds that "didn't actually fix it" eat the balance faster than you'd expect.
I've written separately about how the free allowance behaves in Rork's Free Tier Gives You 35 Credits a Month, but the Number That Shapes Your Work Is 5 a Day, so here I'll only count round trips.
Path one: keep fixing in the same thread, and what the code looked like after
First, I sent the third message anyway: "Make sure it always stops at the set time, even if I reopen the screen."
The fix came back and it worked. But when I opened the code in Dev mode, this is roughly what was left. I've trimmed it to the essentials.
// Before: what remained after three rounds of "please fix it" (essentials only)
final class SleepTimer: ObservableObject {
@Published var isPlaying = false
private var timer: Timer?
private var backupTimer: Timer? // added in round two
private var shouldStop = false // added in round three
func start(minutes: Int) {
isPlaying = true
let seconds = Double(minutes) * 60
timer = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false) { _ in
self.isPlaying = false
}
backupTimer = Timer.scheduledTimer(withTimeInterval: seconds + 1, repeats: false) { _ in
self.shouldStop = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
if self.shouldStop { self.isPlaying = false }
}
}
}Three separate mechanisms for stopping, all alive at once: the original timer, a backup timer from round two, and a flag plus a delayed block from round three. The real cause, that start ran every time the screen reappeared and scheduled a new deadline without cancelling the old one, was never touched.
Here's how I understand why this happens. When you keep asking in the same thread, the AI treats the conversation so far as a correct premise. It's unlikely to conclude that its own previous fix was wrong; adding one more layer on top is the more natural continuation of the dialogue. I suspect a human colleague told "still broken" three times would react the same way.
Three round trips, working code, and something I could no longer read. If another bug showed up, I had no confidence I could tell which of the three paths was the culprit.
Path two: restore, then rephrase
So I opened Restore, previewed the version from just before my first fix request, and reverted to it. That version still had the reopen bug, untouched.
Then I asked about the same bug exactly once, in different words. Looking at the three failed messages, what stood out was that I'd been describing a symptom rather than the state I wanted. I narrowed the rephrasing down to three moves.
# Three rephrasing moves (I rebuilt my actual message along these lines)
1. Describe the outcome, not the symptom
"it sometimes doesn't stop" → "once the stop time has passed, playback must be
stopped no matter which screen transitions happened in between"
2. Name the single place where the decision lives
"keep the stop time as one value, and make the only stop check a comparison
against that value"
3. Say what not to do
"do not create more than one timer, do not add flags, and if a stop time already
exists when the screen reopens, do not extend it"What came back looked like this.
// After: one stop time, one place that checks it
import Foundation
import Combine
@MainActor
final class SleepTimer: ObservableObject {
@Published private(set) var isPlaying = false
private var stopAt: Date?
private var ticker: AnyCancellable?
func start(minutes: Int) {
// Never extend an existing schedule (prevents double-start on reopen)
guard stopAt == nil else { return }
stopAt = Date().addingTimeInterval(TimeInterval(minutes * 60))
isPlaying = true
ticker = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] now in self?.check(now) }
}
func stop() {
isPlaying = false
stopAt = nil
ticker?.cancel()
ticker = nil
}
private func check(_ now: Date) {
guard let stopAt, now >= stopAt else { return }
stop()
}
}I prefer this shape because a single date, stopAt, tells me what will happen next. If the sound doesn't stop, there are only two places to suspect: stopAt is nil, or check isn't being called. Compared with the three-layer version, the load on future me is far lighter.
Round trips: one restore, one rephrased request. The restore itself didn't consume credits, so one round trip was all it cost.
The two paths side by side
Same bug, same evening, both paths. These numbers come from my own notes and will obviously vary with the project and the kind of bug.
| What to look at | Keep fixing in the thread | Restore and rephrase |
|---|---|---|
| Fix-request round trips | 3 (fixed on the third) | 1 (restore is separate and costs no credits) |
| Stopping mechanisms in the code | 3 running in parallel | 1 |
| Touches the real cause? | No; each fix patches the symptom | Yes; a guard prevents the double start |
| Debugging the next failure | Trace three paths | Two places: stopAt and check |
| Work lost | None | Anything after the restored version (zero this time) |
| My own understanding | Can't explain what was fixed | My prompting mistake became words |
The table makes restore look like a landslide, but I don't think it's that simple. There are situations where staying in the thread is clearly the better path.
One is when the error messages are specific and each round visibly moves forward. If build errors change line numbers and shrink in count, continuing is faster. The other is when you've stacked several other features on top since the bug appeared. Restore rewinds those too, so you either export that work first, or give up on restoring and narrow the symptom description instead.
Where I draw the line
I don't want to end on the contrast alone, so here's how I keep both paths and choose between them.
Two rounds in the thread; on the third, go back and rephrase. The reason is simple. After two failures I have enough material to put into words what was wrong with my request. Going back after one failure is too early and I have nothing to learn from. Waiting until the fourth lets the layers pile up and makes it harder to pick which version to return to.
And the one request after restoring must not repeat the symptom. I pull the desired state, the single decision point, and the things not to do from the three failures, and fold them into one message. Working alone, I have no one at the next desk to ask, so this rephrasing step doubles as an inventory of what I actually understand.
Since drawing this line, the sinking feeling in my stomach when I look at the credit balance after a string of fixes has mostly gone. Walking both paths on the same night is still what my judgement rests on.
One next step
If you have a bug that "three requests couldn't fix," open Restore and preview the version that worked, just once. You don't have to revert. Seeing the working screen makes it much easier to say, in words, where the current behavior departs from what it should be.
Deciding whether to actually revert can wait until after that.
One more thing. The line between what I hand to the AI and what I keep as my own decision got another notch more complicated once Rork started producing two separate native apps for iPhone and Android. I've written up how I actually split that in Where the AI decision lives once Rork splits your app into two native codebases.