Chapter 6 of 7 · 8 min read
The turn worth deleting is the one asking which project you meant
On this page
A developer-tooling company's assistant opened every session with the same line: Which project are you working on? It was there for a good reason — the answer scopes every subsequent lookup, and getting it wrong returns results from the wrong repository. Over one month the assistant handled 12,400 sessions, so the question was asked 12,400 times. Median time from the question appearing to the user's answer arriving was 9.2 seconds, plus 1.4 seconds for the round trip that acknowledged it, which comes to roughly 38 hours of collective waiting for one fact.
The interesting number came from joining the answers against the session context. In 96.3 percent of sessions, the project the user named was the project already open in the IDE panel the assistant was embedded in. The application knew the answer before it asked, and it asked anyway, first, every time.
Every session pays for a fact the interface already holds
This is the most common removable turn and it is worth understanding as an instance rather than as a one-off bug. The assistant was written by a team who thought about the model's inputs and not about where those inputs would come from. Something has to supply the project scope, so the prompt asks for it, and asking is the one supply mechanism that always works regardless of context. It is also the only one that charges the user.
Every input a system needs is bound from exactly one of four places. It comes from the request text, where the user has said it. It comes from interface context — the record open, the file focused, the row selected, the tenant in the URL. It comes from the user's own state: their profile, their team, their permissions, what they did ten minutes ago. Or it comes from a question, which is the fallback. Inputs bound at design time to one of the first three cost the user nothing. Inputs left unbound do not fail during development, because during development you are the one typing and you supply everything without noticing. They fail in production, and they fail in one of three shapes.
The first shape is a clarification loop: the assistant asks, the user answers, the session is one turn longer, and this repeats for every session forever. The second is malformed output: nothing asks, the model guesses, and the result is confidently scoped to the wrong project — worse than the loop, because it is silent. The third is abandonment, when the user hits a question they cannot answer without leaving to look something up, and does not come back. The purchase-request session in the first chapter ended at exactly that point.
Trace what the person does before the request and after it
The turn count inside the conversation is the visible part of the cost and usually not the largest part. Sit with three users and write down everything they do from deciding to ask to having the answer where they need it. The tooling company's trace, for a single "why is the build failing" request, looked like this.
| Step | Where | Time | Removable? |
|---|---|---|---|
| Switch to assistant panel | Before | 2 s | No — it is the entry point |
| Answer "which project?" | Before | 9.2 s | Yes — the panel knows it |
| Copy the build ID from the CI tab | Before | 14 s | Yes — the failing build is on screen |
| Type the question | Before | 21 s | Partly — a starter for the common case |
| Wait for the answer | During | 6.8 s | No |
| Re-ask, scoped to the right branch | During | 18 s | Yes — branch is in context |
| Copy the answer into the incident ticket | After | 26 s | Yes — one button |
| Reformat the pasted markdown | After | 11 s | Yes — same button |
Producing that table takes about an hour and no tooling. Sit behind three users doing the task for real, record the screen, and timestamp every action from the moment they decide to ask to the moment the answer is where it needs to be — including the tab switches, the copy-pastes and the reformatting, which users perform so automatically that they will not mention them if you merely interview them. Three users is enough because you are looking for steps rather than for a distribution; the fourth user reveals almost nothing the first three did not.
Two minutes for a question whose answer takes under seven seconds to produce. About 80 seconds of that is the user acting as a courier for facts and formats the system already holds on both ends. The three turns inside the conversation are a minority of the cost, and a team optimising only the model latency would be attacking the 6.8-second row.
The after-column deserves particular attention because it is invisible in every metric a chat surface emits by default. Logs end when the answer is delivered. The user's work does not: they copy, they paste, they reformat, they verify against a dashboard, and if the answer needs to reach a colleague they retype it in Slack. Whatever the answer is for is where the surface should deliver it, and "post to the incident channel" as a button next to the response removes 37 seconds that no dashboard was counting.
Resolve the input, show where it came from, and let them override
The fix for the opening question is not to guess silently. It is a precedence chain: an ordered list of places the value might come from, tried best first, taking the first one that answers. The chain reports which rung it landed on, so the interface can display the resolution and offer a one-click correction.
type Resolution<T> = { value: T; source: Source; confidence: number };
type Source = "explicit" | "active_view" | "recent_activity" | "default" | "asked";
async function resolveProject(req: Request, ctx: Context): Promise<Resolution<Project>> {
const named = extractProjectMention(req.text); // "the billing repo"
if (named) return { value: named, source: "explicit", confidence: 0.99 };
if (ctx.activeView?.projectId) // the panel's own state
return { value: ctx.activeView.project, confidence: 0.96, source: "active_view" };
const recent = await lastTouchedProject(ctx.userId, { within: "2h" });
if (recent) return { value: recent, source: "recent_activity", confidence: 0.71 };
const only = await soleProjectIfSingle(ctx.userId); // many users have exactly one
if (only) return { value: only, source: "default", confidence: 0.88 };
return { value: await askUser("Which project?"), source: "asked", confidence: 1.0 };
}The source field is what makes this safe to ship. The response header renders Project: billing-api — from your open editor · change, so the user sees the assumption without being asked to confirm it, and correcting it is one click rather than a turn. Silent inference with no visible provenance — no statement anywhere on screen of where a value came from — is how you get a user reading a confident answer about the wrong repository; visible inference with a cheap override is how you get the same speed without the failure mode.
The same chain applies to every other input the assistant needs: the branch, the time window, the environment. Write them down as an inventory, bind each one to a rung, and let the question rung hold only what genuinely has nowhere else to come from.
The arithmetic of asking versus inferring
Whether to ask or infer is an arithmetic question and it is usually not close. Asking costs every session: 12,400 sessions × 10.6 seconds is about 36.5 hours a month. Inferring wrongly costs only the sessions where the inference is wrong, times the cost of correcting it: 3.7 percent of 12,400 is 459 sessions, and if correction is a visible chip and one click at 4 seconds, that is about 30 minutes a month. The inference is roughly seventy times cheaper, and the ratio is not sensitive to the exact numbers — it holds anywhere the inference is right most of the time and correction is cheap.
The ratio inverts in one situation, and it inverts hard: when a wrong inference has effects you cannot take back. Reading from the wrong project wastes a few seconds. Deploying to the wrong environment, emailing the wrong customer or refunding the wrong charge is a different category, and there the correct comparison is not against seconds saved but against the cost of the bad outcome times its probability. The rule that falls out is about the action rather than the confidence: infer freely for anything reversible, confirm explicitly before anything that is not — and note that a confirmation is different from a question, because it presents the inferred value for approval rather than asking the user to produce it.
When asking is still right, ask once
Three cases keep the question rung honest. Ask when the input genuinely does not exist anywhere in the system, which is rarer than it looks but real — the intent behind an ambiguous request, a preference nobody has expressed. Ask before irreversible actions, in the confirm-the-value form. And ask when confidence is genuinely low and the failure would be silent: a resolution at 0.4 confidence is a coin flip dressed as a fact, and inferring it produces the malformed-output failure rather than the clarification loop.
When you do ask, store the answer. A question whose answer is not persisted is a question you will ask again next session, which converts a one-time cost into a recurring one and is the version of this mistake that survives the first round of fixes. If the user tells you their default environment is staging, that belongs in their profile, not in a conversation that ends in twenty minutes.
Removing turns has a second-order effect that catches teams out, so it is worth anticipating. Every fact the user used to supply by answering a question was also, incidentally, arriving in the model's context. Delete the question and the model loses it unless the resolver puts it back deliberately, which means the resolution step and the prompt-assembly step are now coupled: whatever the chain resolves has to be serialised into the request alongside the user's text. The tooling company found this the direct way, by shipping the inference and watching answers get vaguer, because the project was being used to scope retrieval and was no longer being stated anywhere the model could see. Resolve, then inject; a turn you removed from the interface still has to exist inside the request.
Nothing here is specific to conversational surfaces. A form field the system could have prefilled is the same defect in a different rendering — it is a question, asked visually, every time — and the resolver above is exactly what turns an empty select into a prefilled one. The audit generalises: list the inputs, bind each to the cheapest rung that can supply it, then trace the work on both sides of the request and delete the courier steps.
Removing a turn produces a better flow and, on its own, no evidence. The team that did this work still has to face the stakeholder who asked for a chatbot and now sees a panel with one input and a button, and "it feels faster" will not survive that meeting. What survives is a small set of measurements taken on both designs, which is what the last chapter builds.
Not everything should be a chatbot — read the current version at https://daviddominguez.dev/learn/en/not-everything-should-be-a-chatbot/the-turn-worth-deleting-is-the-one-asking-which-project-you-meant