lgoyal6 / pms-race

pms‑race.

A computer-use agent driving a screen has no transaction. It reads what is available, spends a second deciding, then clicks. Between the read and the click another agent, the front desk, or an OTA can take the room.

The longer the agent thinks, the more often it is wrong. This is a mock property management system with a real database behind it, driven by N agents at once with a measured pause between reading and writing.

16 agents, 6 rooms
93 bookings taken
read it again first
6 correct, and free
same fix, lost updates
0% of them prevented
locking instead
4.0s p95, 61% turned away
Try it

Cause a double booking yourself

One room, one night, and however many agents you point at it. Each one reads the screen, waits, then books, exactly as server.py does it, running here against a real SQLite database in your tab. Press it twice and you will not always get the same answer, because the race is real.

starting the engine server.py, unmodified, on sqlite in your browser
Booking path
Agents 4
Thinking 400ms

Each agent does the same three things: reads the room as free, waits the thinking time, then tries to book it. They all read before any of them writes, which is what makes it a race. Watch the bar fill, then read the verdict.

rooms available
1
agents booked
turned away
double bookings

What to try

Legacy with the thinking time up. Every agent reads a free room, every agent waits, every agent books, and the room is sold as many times as there were agents. Drag thinking to zero and the damage mostly goes away, which is the whole point: the defect needs a window, and the window is the agent thinking.

Then switch to re-read and run the same thing. Same agents, same delay, one room sold once. The others are turned away by the check inside the write, which is four lines of SQL in do_book.

the same code
server.py, unmodified.The function the harness drives over HTTP.
the same database
Real SQLite, in your tab.Same schema, no unique index, on purpose.
not a replay
Your race, each time.Run it twice and compare.
Figure 1

The gap between looking and clicking

Every agent reads availability, waits, then books. The wait is the whole experiment: it is how long a model spends deciding, and it is the window in which someone else takes the room.

Loading a mock PMS over SQLite, all local
Booking path
Thinking time
agents at once
-
double bookings
-
what happened
-
added latency, p95
-

Why more agents is not the problem

Thinking time is. At a 50ms pause the legacy path double-books 2.1 times per 100 agents with sixteen running at once. Hold the pause at 800ms, the sort of gap a model actually takes, and the same sixteen agents produce 90.6.

More agents with a short pause is mostly a queue: the first one wins and the rest genuinely see the room as taken. The defect needs a window, and the window is the agent thinking.

the variable that matters
How long the agent thinks.2.1 to 90.6 per 100 agents, same concurrency.
the fix
Re-read inside the write.Zero double bookings, no measurable latency.
what it does not fix
Anything below.Lost updates are untouched by it.
Figure 2

The fix that works, and the failure it does nothing for

Three more ways two agents collide, where nothing is double-booked and an edit is silently thrown away instead. The same mitigations, measured again.

edits lost, per 100 trials
Thinking time

Re-reading before the write is exactly as good as doing nothing here. In all three scenarios it leaves the loss rate where it was, because the check it performs is "is the room still free", and the room was always free. The edit is lost to the write that follows it, not to the row being taken.

Only comparing against the version you read fixes it, at a cost of about 1.5 write attempts per agent and roughly ten extra milliseconds. Locking also fixes it, at twenty to a hundred times that.

looks like a fix
Re-read before writing.Loss rate unchanged, all three scenarios.
is a fix
Compare-and-set on the version.Zero lost, 1.5 attempts, about 10ms.
also a fix, at a price
Take a lease first.Zero lost, and 20 to 100x the latency.
Figure 3

The scoreboard

Sixteen agents, 800ms of thinking, six rooms actually available. What each path did with 96 attempts.

Figure 4

Where it loses