Nobody holds it
The queue is empty, the lock is FREE, and the sweeper has nothing to do. It still ticks — every second, transactionally, taking the current time as a parameter rather than reading a clock of its own.
One box. Four people. Throughput is fine for one user at a time and falls apart under concurrency. Kuncen serialises access to it: exactly one person holds an exclusive lock, everyone else waits in an ordered queue, and the handoff happens on a rule rather than an argument in chat.
Two concurrent heavy requests is exactly the pathology we are eliminating — so N = 2 is not a compromise, it is the problem with extra steps. And a semaphore cannot tell a one-line question from a 30,000-token agentic run, so no value of N describes the load it is supposed to be limiting.
So Kuncen takes the other option: strict exclusive access. The price is that the GPU will sometimes sit near-idle under lock. That is a bet, and it is a measurable one — every session records who held it, for how long, how many requests they sent, and how much of that hold the box was actually busy. After two weeks the logs answer it.
“If the logs show frequent queueing with low utilization, revisit.” PLAN.md — The bet
Seven ticks of the sweeper, which runs inside the proxy once a second. The panel is the state it is reading and writing.
The queue is empty, the lock is FREE, and the sweeper has nothing to do. It still ticks — every second, transactionally, taking the current time as a parameter rather than reading a clock of its own.
The lock is free, so she gets it on the spot. The write is UPDATE lock_state … WHERE status = 'FREE' inside an immediate transaction, with the affected row count checked — two people clicking in the same second must not both win.
He is appended at position 1, and Alice's browser says Bob is requesting access — the whole social contract is that she hands it over, and she cannot do that if nobody tells her. Pressing it twice is a no-op; an impatient double-click must not send you to the back.
The timers are contention-gated: they only run while somebody is waiting. And idle is measured retroactively — Alice went to lunch eight minutes ago, so she is already past the five-minute line the instant Bob arrives. An absent holder does not get a fresh five minutes for showing up to nothing.
New requests get 423 immediately, but anything already streaming is allowed to finish — up to a 120-second ceiling. Without the ceiling a single max_tokens: 100000 generation could stretch a session arbitrarily past its cap, which defeats the cap.
At the ceiling the proxy aborts the connection to the model. Dropping only the client would leave the GPU generating a reply nobody will ever read, on time that now belongs to Bob — he would get exclusive access to a box secretly still working on Alice's prompt.
No accept step and no claim window: the sweeper promotes the queue head the moment the lock frees. If Bob is away, the ordinary idle timeout passes it along to whoever is next. One timer, one rule.
Every one of these exists because the obvious alternative fails in a way that is quiet, plausible, and unfair to somebody.
Every deadline is an epoch millisecond, and the sweeper tick takes the time as an argument. A restart, a suspended VM or a nine-hour gap between ticks produces exactly the same state as a punctual one.
With nobody waiting, expiring a lock accomplishes nothing except sending the holder back to a web page to press a button and resume what they were already doing. The timers exist to stop one person starving the others — so they run when there is somebody to starve.
last_activity_at is stamped when the last token flushes, not when the request starts. A six-minute agentic generation must not drop the lock out from under itself. Only the session cap can ever catch you mid-stream.
Polling the dashboard does not reset anything, and there is no “extend me” button. With one, the queue would stop advancing at the only moment it matters.
openai-python silently auto-retries a 429, turning a clean “you're queued” into a hang and then a failure. A 423 fails fast and legibly, carrying Retry-After, the current holder, and your position in the body.
The proxy sees every prompt your colleagues write. The lock machinery records only structured metadata it built itself. Full request tracing exists for debugging, but it is off by default, announces itself in a banner the whole time it runs, and needs a restart rather than a checkbox.
A force-release requires a written reason and shows up on the dashboard. Not for audit — for friction. There is no queue reordering and no handing the lock to a named person, because that turns a mechanical system into a political one.
Up, down, and unknown. “We found nowhere to ask” is not “it is dead” — an OpenAI-compatible gateway happily 404s /health while serving inference, and a badge that cries wolf gets ignored. Backend health is shown separately from lock state, so “it's broken” and “it's taken” never look alike.
The API key answers who you are; the lock answers whether you
may go right now. Keeping those separate is what lets Cline, a script
or plain curl be configured a single time instead of
re-pasting a token every session.
export OPENAI_BASE_URL=http://spark.local:8080/v1 export OPENAI_API_KEY=kuncen_…
The proxy will not enqueue you. Joining the queue is a deliberate button press on the dashboard, always — so the waiting list is a list of people who actually want the box, not of retry loops.
{
"error": {
"message": "Kuncen: the DGX Spark is held by
Alice. You are #2 of 3 in the queue.",
"type": "kuncen_locked"
},
"kuncen": {
"status": "HELD",
"holder": "Alice",
"expires_in_seconds": 240,
"expiry_reason": "idle",
"queue_position": 2,
"queue_length": 3,
"dashboard": "http://spark.local:3000"
}
}
/v1/models and /health need a key but not the
lock. Everything else under /v1/* needs both.
The proxy holds streaming connections open for minutes; the dashboard churns for months. Under one process, every CSS tweak restarts the thing currently streaming somebody's generation. So the enforcing half is small, boring, and left alone.
The sweeper lives inside the proxy for the same kind of reason: if the sweeper is dead the proxy is dead, so nothing can reach the resource anyway. There is no state in which locks quietly stop expiring while people are still burning GPU time — the failure is total and obvious rather than partial and unfair.
Kuncen began as a lock on one DGX Spark, but nothing in the lock, the queue or the timers knows what sits behind the proxy. Name it once and it reaches the login page, the dashboard headlines, the browser notifications, and the 423 body a blocked tool prints.
The article is a separate setting because English does not survive a find-and-replace: the DGX Spark is free and Build Server 3 is free are both correct and differ by more than the noun.
# a GPU box KUNCEN_RESOURCE_NAME=DGX Spark KUNCEN_RESOURCE_ARTICLE=the # or anything else that speaks HTTP KUNCEN_RESOURCE_NAME=Build Server 3 KUNCEN_RESOURCE_ARTICLE= KUNCEN_UPSTREAM=http://127.0.0.1:8000 KUNCEN_DB=./data/kuncen.db
Node 22, one SQLite file, no build step and no linter — both services run
TypeScript straight through tsx. Accounts are provisioned by
hand; each one prints a password and an API key exactly once.
git clone https://github.com/awirayaksa/kuncen npm install npm run migrate npm run admin -- user add alice@example.com Alice --admin # then, in two terminals npm run start:proxy # :8080 npm run start:web # :3000