One Timeout Can't Answer Two Questions: Why AI Write Confirmation Needs Dual Windows

AI write confirmation needs two separate timeout windows plus atomic conditional updates as the sole arbitration point.
The article dissects a common design mistake in human confirmation of AI data-write requests: treating "how long the conversation waits" and "whether a pending record can still be acted on" as the same timeout problem. Their consumers differ—one affects experience, the other consistency—and sharing a value causes the list to show expired records as pending, making clicks inevitably fail. KeelBase's fix splits into a 60-second conversation wait and a 24-hour offline window, computes each boundary with only one function, makes all decision entry points compete for a single database conditional update as the arbitration point, and insists on fail-closed when decision persistence fails. The author also draws the line between at-most-once decision and at-most-once execution, and offers four self-check questions.
Title
One Timeout Can't Answer Two Questions: Why AI Write Confirmation Needs Dual Windows
Meta Description
A common timeout design mistake in AI write confirmation: using the same timeout value to answer both "should the conversation keep waiting" and "can this record still be acted on." This article breaks down the open-source runtime KeelBase's dual-window approach: separating a 60-second conversation wait from a 24-hour decision window, single-function boundary computation, and conditional-update single-point arbitration—plus four self-check questions.
Article Content
When an AI requests to write data and a human needs to click confirm—this scenario hides two completely different "expiration" problems: should the conversation keep waiting? and can this pending record still be acted on? Using the same timeout value to answer both things costs you this: the list shows "waiting for your confirmation," but clicking it inevitably fails. The author of the open-source runtime KeelBase breaks down this dual-window + single-point arbitration design and offers four questions you can use to self-check.

Why Two Questions Look Like One
Both questions look like "how long can a confirmation live," so the intuition is "one number, one column." But their consumers are completely different: the answer to the first question is consumed by the conversation stream that is currently waiting; getting it wrong costs experience—waiting a bit longer, or letting things through early. The answer to the second question is consumed by the data layer and every decision entry point; getting it wrong costs consistency—the user clicks a button that is guaranteed to fail, or a record that is already dead still appears actionable.
The original article lays out this asymmetry clearly with a table: the former errs by milliseconds, the latter errs because "the system manufactures its own failure." When the two share a single value, the answer to the second question gets dragged along by the needs of the first.
Scene Reconstruction: Two Computations Held Together by a Comment
The real problem the KeelBase author encountered was this: the offline window boundary was computed twice—the read path compared timestamps in memory, and the maintenance scan compared timestamps in SQL. What kept the two consistent was a single comment in the code, roughly meaning "the predicate here matches the one over there."
A comment is not a predicate.
Once the two sides drift, you get records like this: the list shows "waiting for your confirmation," but the scan job has already judged it expired. Clicking failure isn't sporadic—the list is mass-producing it by presenting a dead record to the user as a pending task. What makes this kind of bug hard to track down is that the symptom is "clicking occasionally fails," while the root cause is two pieces of code holding different definitions of the same moment.
Fix One: Two Windows, Each Boundary Computed by Only One Function
The fix comes in two halves. The first half is admitting these are two windows:
- In-conversation wait defaults to 60 seconds; expiration only ends that wait, lets the conversation continue, and doesn't touch data;
- Offline window defaults to 24 hours; the record remains decidable during this period, and only when it expires does the record flip to timed out.
The second half is making each boundary computed by only one function. Both the read path and the maintenance scan call it and get the same moment. This way, "shown as actionable" and "already expired" become structurally impossible to be true at the same time. The original article emphasizes not arithmetic correctness but "only one place is doing this"—with two implementations, all that's left is a comment promising they agree.
Fix Two: All Decisions Compete for the Same Arbitration Point
With two windows, there are two decision entry points: the button in the conversation, and the to-do list that appears later. They may compete.
The solution is to make them compete for the same thing: a single database conditional update, with a condition that includes "the current state is still pending." If the update hits one row, the decision is valid and executes; if it hits zero rows, someone has already decided, and this one doesn't execute.
The original article explicitly rejects "read first, then decide": there's a gap between read and write, and both decisions could read "still pending." A conditional update is atomic—whoever modifies the row first owns that decision. Two entry points, one arbitration point; that's the entirety of this section.
Conditional update and compare-and-swap (CAS)
A conditional update is an atomic operation at the database level: UPDATE ... WHERE id = ? AND status = 'pending'. Its key point is that "judging" and "writing" happen within the same atomic operation, with no time window for read-then-write. When executing, the database engine locks the target row (or uses an optimistic-lock version number), ensuring only one transaction can satisfy the status = 'pending' condition. CAS is the more general concept, meaning "write the new value only if the current value equals the expected value"; a conditional update is its implementation form in relational databases. The opposite is the two-step "SELECT then UPDATE" pattern: between the two steps, another transaction may have already modified the row, causing both decisions to believe they were first. In distributed systems, CAS equivalents also include Redis's SET NX, etcd's compare-and-swap transactions, and more. Understanding this makes clear why the original article emphasizes "in-memory checks are not arbitration"—an in-memory boolean flag provides no atomicity guarantee, and multiple concurrent paths can simultaneously read "undecided" and pass the check.
Second Scene: A "Don't Know" Read as "Yes"
Another real incident: a decision must be persisted before it counts—what if the write throws an exception? The old behavior swallowed the error and still released the in-memory wait, so the chain became: database hiccup, write fails → in-memory wait released, tool executes → record is still pending → the next decision (in the conversation or the list) sees "still pending" and executes again.
"We don't know whether the decision landed" was read as "yes." The fix is straightforward: with no evidence that the decision reached the database, don't release; let this attempt fail and let the human retry, rather than translating uncertainty into execution. The original article also specifically points out: for external tools without idempotency keys, the second run is a real replay—the target system genuinely receives it twice. On this path, failing closed isn't caution; it's mandatory.
Idempotency keys and fail-closed
An idempotency key is a unique identifier (such as a UUID) generated by the caller for each operation and sent to the target system with the request. The target system records processed idempotency keys and, when a duplicate request arrives, returns the first result directly without re-executing. Without an idempotency key, a retry or replay is a brand-new call to the target system—a transfer goes through twice, an email gets sent twice. This is exactly what the original article means by "the second run is a real replay." Fail-closed is a default policy: when the system cannot confirm whether an operation succeeded, it chooses to refuse to continue rather than default to success. The opposite, fail-open, lets things through when uncertain, trading consistency for availability. The essence of a confirmation mechanism is "nothing runs without confirmation," so when persistence fails, fail-closed isn't excessive caution—it's a direct corollary of that semantics. The cost is that when the database is unavailable, the confirmation feature grinds to a halt entirely; the original article explicitly acknowledges and accepts this trade-off.
The Boundary This Design Does Not Cross
The author deliberately draws a line: the arbitration point guarantees at-most-once decision, not at-most-once execution. If the same request is retried by the model or resent after a service restart, that's a different problem to be solved at a different layer. "The same decision can't be made twice" and "the same call can't be sent twice" must be stated separately; cramming them into one sentence like "we guarantee it executes only once" is overpromising. This is written into the result messages and documentation, rather than left for users to discover.
At-most-once decision vs at-most-once execution
These are two different levels of consistency guarantees in distributed systems. "At-most-once decision" is guaranteed by the arbitration point: the same pending record won't be simultaneously approved or rejected by two entry points; a conditional update hitting zero rows means the decision has been completed by someone else. "At-most-once execution" requires that the tool call itself not be triggered repeatedly, which needs end-to-end deduplication mechanisms—idempotency keys, call logs, and idempotency support in the target system—covering retries, restarts, network partitions, and other scenarios beyond the decision layer. Confusing the two leads to overpromising: the arbitration point can only control "who has the right to make this decision," not "how many times the request was resent after the decision was made." Engineering-wise, these two statements must be written separately into documentation and interface contracts, otherwise users will mistakenly believe the system provides guarantees it doesn't.
Four Questions You Can Use to Self-Check
- How many questions does your timeout answer? If one value simultaneously determines "should the UI keep waiting" and "can this record still be used," it's a shared value.
- How many places implement the boundary? Two implementations are just one comment away from drifting.
- What arbitrates competing decisions? An in-memory check is not arbitration; a conditional update or any atomic compare-and-swap is.
- What is the default behavior when decision persistence fails? If the next step after a write failure is "execute anyway," the default is to let things through.
Final Thoughts
Back to the beginning: one timeout can't answer two questions. KeelBase's approach is to split into two windows—a 60-second in-conversation wait and a 24-hour decidable to-do; each boundary computed by only one function; all decisions competing for a single conditional update; and nothing released without evidence of persistence.
This mechanism comes from KeelBase, an open-source runtime positioned between AI agents and business systems that re-validates identity, authorization, scope, risk, and confirmation status before tools actually execute. Repository addresses: GitHub (github.com/rain6fish/KeelBase) and the Gitee mirror (gitee.com/rain6fish/KeelBase); Gitee is faster to access from within China.
The author also candidly states a premise: the entire design assumes decisions can be persisted. If the database is unavailable, the system chooses to refuse rather than let through—for some systems, this means the confirmation feature grinds to a halt entirely. This is intentional: if confirmation means "nothing unconfirmed may run," then when nothing can be confirmed, nothing should run.
Related articles

AI Reverse-Engineers Monitor Firmware in 15 Minutes — Has the Hacking Barrier Been Flattened?
A security researcher used AI to reverse-engineer Dell monitor firmware in 15 minutes—entropy analysis, key extraction, MD5 verification—revealing encryption design flaws.

Mistral Voxtral 3: Real-Time Voice at 4 Cents per Minute, Challenging OpenAI
Mistral Voxtral 3 cuts real-time voice agent costs to 4 cents per minute, bundling three speech recognition models, a speech generation model, and a real-time voice toolkit, supporting 50+ languages and self-hosting. This article analyzes its pricing structure, sub-second response performance, and its strategic divergence from OpenAI and Gemini on data privacy versus out-of-the-box convenience.

WebMCP: Every Website as Its Own MCP Server, So AI Agents No Longer Rely on Screenshots
WebMCP lets websites expose tools via the document.modelcontext API, becoming directly callable MCP servers. This article explains its architecture, the Claude Code remote-debugging demo, Chrome's current support stage, and its impact on the agent ecosystem.