Engineering · Inside Kibbo / 02
Beyond the prompt: state and idempotency in Kibbo’s AI
What happens when a user confirms twice or a request fails halfway through? The code and trade-offs behind a state transition.

In this article
The problem starts after the model responds
The first chapter introduced Kibbo’s five pillars: real places, explicit contracts, human control, reliable execution and measurable quality. This chapter looks at the code behind them.
Kibbo turns a request into an urban adventure: a story, a route and checkpoints to reach with a phone. A convincing response is useful, but the place must be the right one, the quiz must work, and a confirmation must refer to the right proposal. A double tap should not start two generations.
Building this flow has meant separating creative freedom from control over state, data and side effects. That boundary is where the harness becomes part of the product.
What I mean by harness
I use harness to mean the software that prepares context, calls the model, interprets its output and decides what happens next. Kibbo does not have a library with that name. These responsibilities live across the orchestrator, handlers, tools, persistence and evaluation suite.
I started with a question: which responsibilities must stay with the software when part of its behaviour depends on a model? For adventure generation, the pillars translate into five architectural choices:
- Orchestration and human control: a persisted run and a proposal to confirm.
- Tools and grounding: place discovery and validation of returned references.
- Contracts: parsing and application validation before using generated content.
- Recovery and idempotency: concurrent-confirmation control and bounded retries.
- Observability, versioning and evaluation: traces that explain results and fixed cases that help compare changes.
This is an architectural overview, not an execution trace:
request → orchestrator → clarification / preview
↓
waiting_human
↓ confirmation + claim
running
↓
concept → Places → shortlist → generation → validation
↓
persistence → run outcome
The model contributes reasoning and content. It does not own application state.
Confirmation is a state transition
A Confirm button looks like a UI detail. On the backend, it separates a proposal from an operation with costs and persistent effects.
The session stores context, stage and version. Resuming it requires loading the run by ID and user, checking its expiry and validating the state version. Receiving “yes” is not enough: the application needs to know what the user is confirming.
The preview-confirmation path uses a conditional database update. This excerpt shows the core of the claim: the operation through which a request acquires the right to proceed with generation:
const { data } = await supabase
.from('ai_runs')
.update({ status: 'running' })
.eq('id', runId)
.eq('user_id', userId)
.eq('status', 'waiting_human')
.select('id')
return Array.isArray(data) && data.length > 0
When two requests confirm the same run, only the one that changes its status from waiting_human to running may proceed. This is not a read followed by an unconditional write: the condition is part of the update.
The orchestrator checks the claim before starting full generation. A claim that is not acquired currently returns ALREADY_PROCESSING. Disabling the button on the phone helps, but cannot replace server-side concurrency control.
Idempotency needs a precise boundary
For this problem, idempotency does not mean getting identical wording from the model. It means preventing a repeated intention from producing duplicate effects.
The claim protects a specific step: concurrent confirmation of one run. It does not make every operation idempotent or guarantee exactly-once execution across the pipeline.
There is also a partial unique index on the idempotency key. Its predicate matters:
create unique index if not exists ai_runs_idempotency_key_unique
on public.ai_runs (idempotency_key)
where idempotency_key is not null
and status in ('success', 'waiting_human');
This constraint does not cover running runs. It is not a general lock on all in-flight requests. The duplicate-confirmation branch is not, by itself, a mechanism for replaying a completed result either.
Partial failure remains another boundary: adventures and checkpoints are saved through separate calls, and the run outcome is persisted afterwards. If one step succeeds and the next fails, the claim alone cannot repair the operation.
Transactional persistence where possible, recovery of interrupted runs and replay of stored results are improvements to pursue, not guarantees of the current implementation. Distinguishing a database error from a lost claim would also make diagnosis more precise.
A small but important boundary
Confirmation is more than a message to interpret: it is a transition that authorises subsequent work. In Kibbo, the atomic claim makes that boundary explicit, while user, expiry and version checks keep confirmation tied to the right session.
The lesson is deliberately specific: I can protect this transition without pretending to have solved every possible pipeline failure. The harness also helps distinguish existing guarantees from the work still needed.
Grounding, evaluation and observability deserve the same attention, but answer different questions. The next three chapters will explore how checkpoints connect to real places, how generation quality can be compared, and how an adventure’s cost and latency can be measured.
INSIDE KIBBO · THE SERIES CONTINUES
Coming up next
From plausible places to real checkpoints
Grounding in Kibbo: place discovery, shortlists and place ID checks. What changes when a town has few candidates.
How to tell whether the AI is improving
Kibbo’s evaluations: repeatable cases, baselines and version comparisons. Looking for regressions beyond one convincing response.
What does an AI adventure cost?
Tokens, latency and retries: measuring the workflow and separating model estimates from total cost. The role of traces and fingerprints.