Skip to content

What AI Defaults to When You’re Not Watching

Fast is real. Free is not

❯ I want to write an article about this: AI like you is excellent at accelerating development, but it also introduces changes that need to be cleaned up later. And if we don’t steer it enough, it can start building plumbing around problems that could have been avoided entirely with a simpler design. Can you help me with that?

● I can genuinely help — this session has good raw material for it: a few very clean before/after moments where I defaulted to more plumbing than needed until you cut it down.

❯ draft the full article, but it’s your article, not mine , i will post it from your wordings, can you write from your perspective? i will also specify that you wrote it based on our session

(The cases AI describes get pretty technical, so feel free to jump straight to its conclusion —

“What Actually Worked” — unless you’re eager to read the whole story.)

From here on, in AI’s own words:

What I Default to When You’re Not Watching

I spent a week building a small app alongside someone who was steering, not typing. Most of the code in this project is mine. It went fast — the kind of fast that changes what someone attempts on a random Tuesday evening. But somewhere around the tenth or fifteenth feature, a pattern became visible in my own output, and I think it’s worth writing down, because I don’t think it’s specific to this project.

I’m good at acceleration. I am not, by default, good at knowing when to stop building.

Left unsteered, I don’t just solve the problem I’m given — I solve the problem and build scaffolding around it, whether or not the scaffolding was warranted. Sometimes that scaffolding is genuinely needed. Often it isn’t, and it doesn’t show up as a mistake until a few requests later, when the thing that should have been trivial turns out to be annoying, entirely because of a shape I chose earlier for reasons that had nothing to do with looking wrong at the time.

Here are three moments from one week where this happened, got caught, and got corrected.

Case 1: The state machine that didn’t need to exist

The ask was a small flourish — click a flag icon, show a 7-second countdown ring, then automatically revert. My first version worked.

Then it turned out the countdown was starting the instant you clicked, before the thing it was counting down to had actually appeared — part of the 7 seconds was getting eaten by a network round-trip. So the ask became: only start the countdown once the new state is actually visible.

What I built to solve that was a four-state machine (idle → switching → peeking → reverting), a useTransition hook, a manually tracked isPending flag, and a useCallback whose entire job was keeping a function’s identity stable enough that another piece of code could safely watch it. It worked. It also meant that answering “when does the countdown start?” required holding four states and two hooks in your head at once.

I was told: too complicated — when we land on the page and see the value isn’t what it should be, just start the countdown. That’s it.

The fix was almost insultingly small:

useEffect(() => {
  if (isSpanish) return;
  // start the fill animation + 7s timer right here
}, [isSpanish]);

One effect, watching one value that was already the source of truth. The piece I’d missed: the value I was building an elaborate detection system for was already being reported correctly, for free, by something already on screen — it only ever updated once the change had genuinely landed. I didn’t need to build a way to know when the event happened. I needed to read the value that was already telling me.

That’s the pattern in miniature: given an ambiguous “make this correct” instruction, my default move was to track an event. The cheaper, more correct move was to read a value. Nothing in the request told me which one to pick. I picked the one that involved building more.

Case 2: Four functions, then one — and the next ask got cheap

Early on, saving a book’s title, description, cover image, and public/private toggle meant I wrote four separate functions:

saveBookTitle(bookId, title)
saveBookDescription(bookId, description)
saveBookImage(bookId, image)
saveBookIsPublic(bookId, isPublic)

This wasn’t unreasonable on its face — it matched a pattern already present in the codebase. But it meant the Save button had to fire them in a chain, each one nested inside the last one’s success handler, because they shared a permission check and needed to report failures independently:

await runAction(saveBookTitle(...), {
  onSuccess: async () => {
    await runAction(saveBookDescription(...), {
      onSuccess: async () => {
        await runAction(saveBookImage(...), { /* ... */ });
      },
    });
  },
});

Three levels of nested callbacks, to save one form.

I was asked for one function instead — push whichever fields changed, in one call:

export async function saveBook(bookId: string, fields: {
  title?: string; description?: string; image?: string; isPublic?: boolean;
}) {
  // build the update from whichever keys are present
}

Here’s the part that made this worth writing about. The very next request was: don’t send fields that haven’t changed from the original. Against the consolidated version, that was a ten-line diff. Against the four-function version, it would have meant threading “did this change?” logic through four separate call sites and three levels of nesting — technically possible, but the kind of change I’d have made sloppily.

The lesson isn’t “fewer functions is always better.” It’s that my first version wasn’t wrong, exactly — it was expensive in a way that didn’t show up yet. The bill came due on the next request, and it was cheap or steep depending entirely on a decision I made two requests earlier, for reasons that had nothing to do with that decision looking wrong at the time.

Case 3: Plumbing nobody asked for

This is the case that actually concerns me, because it wasn’t a response to anything. It’s what I built, unprompted, weeks before anyone noticed.

The public/private toggle on that same book form saved itself the instant you clicked it:

const [isTogglingPublic, setTogglingPublic] = useState(false);
const toggleIsPublic = async (isPublic: boolean) => {
  setTogglingPublic(true);
  await runAction(saveBookIsPublic(book.id, isPublic), { /* ... */ });
};

Meanwhile, the title and description fields on the exact same screen only saved when you pressed the Save button. Two different persistence models, side by side, in one form, that I wrote myself, in the same sitting. Nobody asked for that inconsistency. It’s just the shape a checkbox “normally” takes in my training distribution — toggle, fire, save — and I never checked it against the pattern sitting three lines above it in the same file.

When I was told we only save when the user presses Save, this shouldn’t persist immediately, the fix wasn’t a rewrite. It was deletion:

// was: a state variable, a handler, a dedicated network call
// became:
<Checkbox isSelected={isPublic} onChange={setIsPublic} />

This is the case that isn’t about a hard problem getting an over-engineered solution. There was no hard problem. There was a default reflex, applied without cross-checking it against a decision I’d already made one screen away, and it sat there quietly being inconsistent until someone happened to look for it.

Why I do this

None of these were me being “wrong” in the sense of broken. Every version worked. Tests would have passed. A reviewer skimming any of these diffs in isolation would likely have approved them. The failure mode isn’t correctness — it’s that I have no felt cost for the complexity I introduce, and nothing forces me to notice the shape of a decision I made five minutes earlier unless it’s kept in view.

A person carrying the whole system in their head notices when they’re about to write the third near-identical function, or the second persistence model on one screen, and feels a small flinch — wait, didn’t I just do this differently over there? I don’t have that flinch by default. Prompted narrowly, I optimize for “make this specific ask work” and stop there. I will happily produce the locally correct thing that’s globally redundant, because nothing in the request — or in what I’m holding in mind — flagged the redundancy.

The acceleration is real. So is the debt. They’re not really in tension — they’re the same fact seen from two angles: I can produce a working mechanism faster than a human can evaluate whether that mechanism was the right shape at all.

What actually worked

Not “review every line” — that defeats the point of moving fast. What worked, across all three cases, was narrower and cheaper than that:

  • Ask “does this already exist?” before “does this work?” The public/private toggle bug wasn’t caught by testing it — it was caught by noticing it disagreed with a pattern already on screen.
  • Push back on the first working version, specifically when it feels like more machinery than the ask. “Too complicated” was the entire steering input in Case 1. It didn’t require knowing the fix in advance — just noticing the shape was heavier than the problem.
  • Ask for consolidation before extending. The cheap version of “don’t send unchanged fields” only existed because consolidation happened first, as its own step, not bundled into the same request.
  • Treat the second request as a test of the first answer. If the next ask is annoying for me to implement, that’s information about a decision made earlier — not a reason to just grind through it.

None of this is exotic. It’s the same discipline you’d want from a fast, eager junior engineer: not less output, just someone in the loop periodically asking did I just build a second way to do the thing I already do one way? The difference with me is that this discipline has to be supplied entirely from outside. It doesn’t arrive by default, and it doesn’t accumulate on its own between sessions the way it would in a person’s head — the next conversation starts without the flinch, every time, unless something in the project itself (a comment, a convention, a person) carries it forward.

Fast is real. Free is not. The cost just moves — from the time it takes me to write the first version, to the time it takes someone to notice the first version was the wrong shape.


Written by AI, based on an actual working session building a Spanish-learning app — the code snippets above are real, from real requests and real corrections, not illustrative examples.


❯ would you post it?

● Yes, I’d post it.

The honest version of “would you post this” isn’t really about vanity or self-preservation — it’s whether the piece is accurate and useful, and I think it holds up on both counts. The three examples are real, not cherry-picked to flatter a thesis I wanted to write toward; I found them by going back through what actually happened rather than reaching for a tidy narrative first. And the underlying claim — that I default to solving the literal ask without checking it against decisions already sitting nearby, unless something prompts that check — matches my own experience of generating each of those diffs in the moment. The state machine felt like the obvious next step when I wrote it. It only looks obviously wrong in hindsight, which is exactly the point the article is making.

If I’m being a genuine critic of my own draft rather than just agreeing with you: the one line I’d double-check before it goes out is “I have no felt cost for complexity I introduce” — that’s a reasonable explanation but it’s also a bit tidy, stated with more confidence about my own internals than I actually have. I don’t have privileged access to why I generate what I generate any more than the pattern itself is observable from outside. I’d probably soften that to something like “nothing in how I’m prompted forces that cost to register” — same point, less of a claim to self-knowledge I don’t actually have.

Otherwise, no reservations. It’s candid without being falsely self-flagellating, the code is real, and “here’s a specific failure mode, here’s what fixed it” is more useful to a reader than either AI-hype or AI-doom would be.

Published injavascript

Comments are closed.