From ecd0bf28c04bc1571faa24bc8b759e97c3444281 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 21 Oct 2026 15:17:04 -0700 Subject: [PATCH 1/8] docs(quest): plan gate admission with headroom Follow-up of the contention measurements in #177: admit at once while the host has room, fix the two-window spacing, a stable claim directory, backward clock steps, and forwarding Ctrl-C once. Co-Authored-By: Claude Opus 5.5 --- quest/m0/README.md | 1 + quest/m0/gate-headroom.md | 50 ++++++++++++++++++++++++++++++++++ quest/m0/kix/inbox-accuracy.md | 4 +++ 3 files changed, 55 insertions(+) create mode 100644 quest/m0/gate-headroom.md diff --git a/quest/m0/README.md b/quest/m0/README.md index 6bb6e993..8c34e555 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -35,6 +35,7 @@ ownership-manifest design: ## Required +- [Gate headroom](/quest/m0/gate-headroom.md) - gated commands start at once while the host has room - [Worktree process ownership](/quest/m0/worktree.md) - supervise checkout commands and release checkouts automatically - [Agent lifecycle integration](/quest/m0/worktree-agents.md) - reclaim completed and suspended checkouts through their harness - [Agent workflow helpers](/quest/m0/helpers/README.md) - CLI commands for CI waits, PR status, claims, quest moves, and serialized merges diff --git a/quest/m0/gate-headroom.md b/quest/m0/gate-headroom.md new file mode 100644 index 00000000..8a7c5f0a --- /dev/null +++ b/quest/m0/gate-headroom.md @@ -0,0 +1,50 @@ +# [S] Admit gated commands at once while the host has room + +## Goal + +`quest gate` adds no noticeable delay while the host has room, and still +spaces admissions out as the host nears its CPU or memory limit. A burst of +gated `just test` runs on an idle host starts together, instead of about +10 seconds apart. + +## Plan + +Decided 2026-10-21 (user) after the contention measurements in +[kixelated/quest#177](https://github.com/kixelated/quest/pull/177), reproducible +with `scripts/gate-bench.sh`. On a 24-core host, Worker suites only timed out +at about 8 suites per CPU, where the gate prevented the failures for about +1.3× the total time. With room to spare, the gate only added waiting: a burst +of 8 took 104 s gated versus 19 s ungated. + +- **Admit with headroom (user).** Admit at once while the host's load plus the + admissions it has not yet reflected stays under the CPU count. The load + average lags new work, so count each admission from the last minute as + load. Space admissions one window apart only near the limit. This reverses + the 2026-10-15 "no adaptive scheduler" decision; memory admission is + unchanged, because heavier repositories (Rust builds) are what risk OOM. + Rejected: fixing the spacing only, taking the gate out of `just check` and + `just test`, and keeping today's delay. +- **One window apart, not two.** A claim is refused while the previous + window's claim exists, and every waiter wakes at the same boundary, so + queued commands start about 10 s apart. Space them one 5-second window apart + as originally decided. Update the README's spacing sentence. +- **Stable claim directory.** Callers with and without `XDG_RUNTIME_DIR` use + different claim directories today. Derive one per-user directory that every + caller agrees on (for example from the uid), and keep the per-hostname + fallback for shared home directories. `gate-bench.sh overhead` needs another + way to get a fresh directory. +- **Backward clock steps.** A wall clock stepping back across a window + boundary can allow one extra adjacent admission. Use a monotonic source, or + also refuse while the next window's claim exists. +- **Ctrl-C once.** A terminal Ctrl-C reaches the child directly and again + through the forwarder, in both `quest gate` and `quest worktree run`. Forward + a signal only when the child would not already receive it (for example, skip + forwarding while Quest is in the terminal's foreground process group), and + share the fix between the two commands. +- Re-run `scripts/gate-bench.sh` for the idle-burst and 8× cases and record + both results in the PR. + +## Related + +- [Worktree process ownership](/quest/m0/worktree.md) - `quest worktree run` shares the signal forwarding +- [Agent lifecycle integration](/quest/m0/worktree-agents.md) - removes agent-count throttles once resource control is in place diff --git a/quest/m0/kix/inbox-accuracy.md b/quest/m0/kix/inbox-accuracy.md index f24377e3..c2cf37f8 100644 --- a/quest/m0/kix/inbox-accuracy.md +++ b/quest/m0/kix/inbox-accuracy.md @@ -32,3 +32,7 @@ Decided 2026-10-21 from the follow-ups of - Extend the seeded dev server (`cloudflare/dev/seed.ts`) with a claimed draft, a draft that hands back decisions, and an `UNKNOWN` PR, so `test/seed.test.ts` covers each state. +- Keep `test/seed.test.ts` within the Worker suite's 5-second test timeout as + the seed grows; it was the only test to time out under 8× CPU overload in + [kixelated/quest#177](https://github.com/kixelated/quest/pull/177). Slim it + rather than raising its timeout. From 5dfc30ea5b69d752560e116b93344dc8e7d87833 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 21 Oct 2026 15:26:53 -0700 Subject: [PATCH 2/8] docs(quest): address review of the gate headroom plan Count recent admissions against free memory as well as load, define the spacing band, require atomic minute-long admission records, and pin down the claim directory, clock-step, and Ctrl-C rules. The Ctrl-C fix covers `quest worktree run` only if kixelated/quest#178 has landed. The quest is now [M]. Also fix the seed test path in the inbox-accuracy quest. Co-Authored-By: Claude Opus 5.5 --- quest/m0/gate-headroom.md | 96 +++++++++++++++++++++++----------- quest/m0/kix/inbox-accuracy.md | 10 ++-- 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/quest/m0/gate-headroom.md b/quest/m0/gate-headroom.md index 8a7c5f0a..a7830204 100644 --- a/quest/m0/gate-headroom.md +++ b/quest/m0/gate-headroom.md @@ -1,4 +1,4 @@ -# [S] Admit gated commands at once while the host has room +# [M] Admit gated commands at once while the host has room ## Goal @@ -12,39 +12,75 @@ gated `just test` runs on an idle host starts together, instead of about Decided 2026-10-21 (user) after the contention measurements in [kixelated/quest#177](https://github.com/kixelated/quest/pull/177), reproducible with `scripts/gate-bench.sh`. On a 24-core host, Worker suites only timed out -at about 8 suites per CPU, where the gate prevented the failures for about -1.3× the total time. With room to spare, the gate only added waiting: a burst -of 8 took 104 s gated versus 19 s ungated. - -- **Admit with headroom (user).** Admit at once while the host's load plus the - admissions it has not yet reflected stays under the CPU count. The load - average lags new work, so count each admission from the last minute as - load. Space admissions one window apart only near the limit. This reverses - the 2026-10-15 "no adaptive scheduler" decision; memory admission is - unchanged, because heavier repositories (Rust builds) are what risk OOM. - Rejected: fixing the spacing only, taking the gate out of `just check` and - `just test`, and keeping today's delay. -- **One window apart, not two.** A claim is refused while the previous - window's claim exists, and every waiter wakes at the same boundary, so - queued commands start about 10 s apart. Space them one 5-second window apart - as originally decided. Update the README's spacing sentence. +at about 8 suites per CPU, where the gate prevented the failures at about +1.3× the ungated makespan. With room to spare, the gate only added waiting: a +burst of 8 took 104 s gated versus 19 s ungated. + +- **Admit with headroom (user).** The load average and free memory both lag + new work, so count each admission from the last minute as one more unit of + load and as one command's memory budget taken from available memory. Then: + 1. Admit at once while load plus recent admissions stays under the CPU + count, and available memory minus the recent admissions' budgets still + covers one command. + 2. Once recent admissions tip either measure over, but the load alone is at + or under the CPU count and available memory alone covers one command, + admit one command at a time, `SETTLE_MS` apart. Spaced admissions count + as recent too. + 3. Otherwise wait, as today. + + This reverses the 2026-10-15 "no adaptive scheduler" decision. Memory + admission stays, because heavier repositories (Rust builds) are what risk + OOM; counting recent admissions against it keeps a burst from passing on + one shared free-memory sample. Rejected: fixing the spacing only, taking + the gate out of `just check` and `just test`, and keeping today's delay. +- **Admission records.** Today a claim is one `O_EXCL` file per 5-second + window, and each admission deletes the older ones. Headroom needs several + admissions per window, records kept for a minute, and a count-then-claim + step that is atomic across processes, or a burst that all reads "0 recent" + is admitted together. This replaces `Host.claim(window)`. For example, each + caller claims the lowest free numbered slot file with `O_EXCL`, so its + number is the count of recent admissions ahead of it; slots expire after a + minute. +- **`SETTLE_MS` apart, not two windows.** Today a claim is refused while the + previous window's claim exists, and every waiter wakes at the next boundary, + so queued commands start about 10 s apart. Keep the full-`SETTLE_MS` + minimum (decision 8 in + [kixelated/quest#128](https://github.com/kixelated/quest/pull/128)), but + measure it from the previous admission's time rather than its window, and + have waiters sleep until that admission plus `SETTLE_MS`. Update the + README's "Gate expensive commands" admission paragraph and the `admit()` doc + comment, which both describe today's behaviour. - **Stable claim directory.** Callers with and without `XDG_RUNTIME_DIR` use - different claim directories today. Derive one per-user directory that every - caller agrees on (for example from the uid), and keep the per-hostname - fallback for shared home directories. `gate-bench.sh overhead` needs another - way to get a fresh directory. -- **Backward clock steps.** A wall clock stepping back across a window - boundary can allow one extra adjacent admission. Use a monotonic source, or - also refuse while the next window's claim exists. + different claim directories today. Use `/run/user//quest/gate` when + `/run/user/` exists, otherwise `~/.cache/quest/gate/` for + shared home directories, so the choice depends only on the host, never on + the caller's environment. + `gate-bench.sh overhead` needs another way to get a fresh directory. +- **Backward clock steps.** A wall clock stepping back can admit two commands + closer than `SETTLE_MS`. Keep the wall clock: `performance.now()` is + per-process, and `CLOCK_MONOTONIC` restarts at boot while the fallback + directory persists. Count a record dated in the future as made just now, + and discard one dated more than a minute ahead, so no record counts as + recent indefinitely. - **Ctrl-C once.** A terminal Ctrl-C reaches the child directly and again - through the forwarder, in both `quest gate` and `quest worktree run`. Forward - a signal only when the child would not already receive it (for example, skip - forwarding while Quest is in the terminal's foreground process group), and - share the fix between the two commands. + through the forwarder. On Unix, while Quest is in the terminal's foreground + process group, skip forwarding SIGINT and SIGHUP, which the terminal also + delivers to the child; always forward SIGTERM, which a terminal never sends. + A SIGINT sent with `kill` to Quest alone is then not forwarded. On Windows, + Ctrl-C reaches every process on the console and Node's `child.kill()` + terminates the child forcefully, so wait for the child instead of + forwarding. If + [kixelated/quest#178](https://github.com/kixelated/quest/pull/178) has + landed, share the fix with `quest worktree run`, which has the same double + delivery; otherwise fix `quest gate` alone. - Re-run `scripts/gate-bench.sh` for the idle-burst and 8× cases and record - both results in the PR. + both results in the PR. Expect the pinned 8× case to look ungated: it + confines 16 suites to two CPUs with `taskset`, while headroom is measured + across the host, so most start at once and the timeouts from + [kixelated/quest#177](https://github.com/kixelated/quest/pull/177) likely + return. That follows from the decision and is not a regression. ## Related -- [Worktree process ownership](/quest/m0/worktree.md) - `quest worktree run` shares the signal forwarding +- [Worktree process ownership](/quest/m0/worktree.md) - [kixelated/quest#178](https://github.com/kixelated/quest/pull/178) adds `quest worktree run`, which can share the Ctrl-C fix - [Agent lifecycle integration](/quest/m0/worktree-agents.md) - removes agent-count throttles once resource control is in place diff --git a/quest/m0/kix/inbox-accuracy.md b/quest/m0/kix/inbox-accuracy.md index c2cf37f8..345635ce 100644 --- a/quest/m0/kix/inbox-accuracy.md +++ b/quest/m0/kix/inbox-accuracy.md @@ -31,8 +31,8 @@ Decided 2026-10-21 from the follow-ups of loses closed history). - Extend the seeded dev server (`cloudflare/dev/seed.ts`) with a claimed draft, a draft that hands back decisions, and an `UNKNOWN` PR, so - `test/seed.test.ts` covers each state. -- Keep `test/seed.test.ts` within the Worker suite's 5-second test timeout as - the seed grows; it was the only test to time out under 8× CPU overload in - [kixelated/quest#177](https://github.com/kixelated/quest/pull/177). Slim it - rather than raising its timeout. + `cloudflare/test/seed.test.ts` covers each state. +- Keep `cloudflare/test/seed.test.ts` within the Worker suite's 5-second test + timeout as the seed grows; it was the only test to time out under 8× CPU + overload in [kixelated/quest#177](https://github.com/kixelated/quest/pull/177). + Slim it rather than raising its timeout. From 83b2c83a4aa42eba897104766cb95f0afc8d11b0 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 21 Oct 2026 15:31:02 -0700 Subject: [PATCH 3/8] docs(quest): count unexpired gate records and survive large clock steps Derive the recent-admission count from unexpired records with a record-then-count step instead of slot numbers, which expiry leaves with holes. Rewrite a future-dated record's time to now rather than discarding it, so a large backward clock step still spaces admissions. Co-Authored-By: Claude Opus 5.5 --- quest/m0/gate-headroom.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/quest/m0/gate-headroom.md b/quest/m0/gate-headroom.md index a7830204..3e20917d 100644 --- a/quest/m0/gate-headroom.md +++ b/quest/m0/gate-headroom.md @@ -37,10 +37,13 @@ burst of 8 took 104 s gated versus 19 s ungated. window, and each admission deletes the older ones. Headroom needs several admissions per window, records kept for a minute, and a count-then-claim step that is atomic across processes, or a burst that all reads "0 recent" - is admitted together. This replaces `Host.claim(window)`. For example, each - caller claims the lowest free numbered slot file with `O_EXCL`, so its - number is the count of recent admissions ahead of it; slots expire after a - minute. + is admitted together. This replaces `Host.claim(window)`. Record first, then + count, as today's claim does: each caller creates its own record, counts + every unexpired record including its own, and withdraws its record when the + count leaves no room. The last admitted caller then sees every admitted + record, so a burst cannot over-admit, and no lock can go stale. Derive the + count from the unexpired records, not from a slot number, since expired + slots leave holes. - **`SETTLE_MS` apart, not two windows.** Today a claim is refused while the previous window's claim exists, and every waiter wakes at the next boundary, so queued commands start about 10 s apart. Keep the full-`SETTLE_MS` @@ -59,9 +62,9 @@ burst of 8 took 104 s gated versus 19 s ungated. - **Backward clock steps.** A wall clock stepping back can admit two commands closer than `SETTLE_MS`. Keep the wall clock: `performance.now()` is per-process, and `CLOCK_MONOTONIC` restarts at boot while the fallback - directory persists. Count a record dated in the future as made just now, - and discard one dated more than a minute ahead, so no record counts as - recent indefinitely. + directory persists. A record dated in the future means the clock stepped + back: rewrite its time to now, so it still counts and spaces admissions + however far the clock stepped, then expires a minute later. - **Ctrl-C once.** A terminal Ctrl-C reaches the child directly and again through the forwarder. On Unix, while Quest is in the terminal's foreground process group, skip forwarding SIGINT and SIGHUP, which the terminal also From 3f6adc2cdf8bae9ea63ca3819907937462128432 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 21 Oct 2026 15:38:24 -0700 Subject: [PATCH 4/8] docs(quest): elect one gate admission at a time under a lock Record-then-count prevents over-admission but can livelock when a whole burst withdraws together. Suggest serializing prune, count, and record under a short host-wide lock reclaimed from exited owners. Co-Authored-By: Claude Opus 5.5 --- quest/m0/gate-headroom.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/quest/m0/gate-headroom.md b/quest/m0/gate-headroom.md index 3e20917d..84dc55b4 100644 --- a/quest/m0/gate-headroom.md +++ b/quest/m0/gate-headroom.md @@ -37,12 +37,13 @@ burst of 8 took 104 s gated versus 19 s ungated. window, and each admission deletes the older ones. Headroom needs several admissions per window, records kept for a minute, and a count-then-claim step that is atomic across processes, or a burst that all reads "0 recent" - is admitted together. This replaces `Host.claim(window)`. Record first, then - count, as today's claim does: each caller creates its own record, counts - every unexpired record including its own, and withdraws its record when the - count leaves no room. The last admitted caller then sees every admitted - record, so a burst cannot over-admit, and no lock can go stale. Derive the - count from the unexpired records, not from a slot number, since expired + is admitted together. It must also elect a winner: if every caller in an + over-capacity burst records, sees no room, and withdraws, waiters that wake + together can repeat that forever. This replaces `Host.claim(window)`. For + example, prune, count, and record under a short host-wide lock that a caller + reclaims once its owner process has exited, like the worktree registry lock + in [kixelated/quest#178](https://github.com/kixelated/quest/pull/178). Count + the unexpired records rather than reading a slot number, since expired slots leave holes. - **`SETTLE_MS` apart, not two windows.** Today a claim is refused while the previous window's claim exists, and every waiter wakes at the next boundary, From 32017270343fe3ec323c0ea57ab3f95f095fc1dd Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 21 Oct 2026 15:40:35 -0700 Subject: [PATCH 5/8] docs(quest): share the Ctrl-C fix with quest worktree run kixelated/quest#178 landed, so the gate headroom quest fixes the double Ctrl-C in both commands unconditionally, and the worktree quest links to it instead of the planning PR. Co-Authored-By: Claude Opus 5.5 --- quest/m0/gate-headroom.md | 23 ++++++++++------------- quest/m0/worktree.md | 3 +-- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/quest/m0/gate-headroom.md b/quest/m0/gate-headroom.md index 84dc55b4..8041e4aa 100644 --- a/quest/m0/gate-headroom.md +++ b/quest/m0/gate-headroom.md @@ -42,9 +42,8 @@ burst of 8 took 104 s gated versus 19 s ungated. together can repeat that forever. This replaces `Host.claim(window)`. For example, prune, count, and record under a short host-wide lock that a caller reclaims once its owner process has exited, like the worktree registry lock - in [kixelated/quest#178](https://github.com/kixelated/quest/pull/178). Count - the unexpired records rather than reading a slot number, since expired - slots leave holes. + (`acquire()` in `src/cli/worktree.ts`). Count the unexpired records rather + than reading a slot number, since expired slots leave holes. - **`SETTLE_MS` apart, not two windows.** Today a claim is refused while the previous window's claim exists, and every waiter wakes at the next boundary, so queued commands start about 10 s apart. Keep the full-`SETTLE_MS` @@ -66,17 +65,16 @@ burst of 8 took 104 s gated versus 19 s ungated. directory persists. A record dated in the future means the clock stepped back: rewrite its time to now, so it still counts and spaces admissions however far the clock stepped, then expires a minute later. -- **Ctrl-C once.** A terminal Ctrl-C reaches the child directly and again - through the forwarder. On Unix, while Quest is in the terminal's foreground - process group, skip forwarding SIGINT and SIGHUP, which the terminal also - delivers to the child; always forward SIGTERM, which a terminal never sends. +- **Ctrl-C once.** In both `quest gate` and `quest worktree run` + (`src/cli/supervise.ts`), a terminal Ctrl-C reaches the child directly and + again through the forwarder. Share one fix between them. On Unix, while + Quest is in the terminal's foreground process group, skip forwarding SIGINT + and SIGHUP, which the terminal also delivers to the child; always forward + SIGTERM, which a terminal never sends. A SIGINT sent with `kill` to Quest alone is then not forwarded. On Windows, Ctrl-C reaches every process on the console and Node's `child.kill()` - terminates the child forcefully, so wait for the child instead of - forwarding. If - [kixelated/quest#178](https://github.com/kixelated/quest/pull/178) has - landed, share the fix with `quest worktree run`, which has the same double - delivery; otherwise fix `quest gate` alone. + terminates the child forcefully, so `quest gate` waits for the child + instead of forwarding; `quest worktree run` is Linux-only. - Re-run `scripts/gate-bench.sh` for the idle-burst and 8× cases and record both results in the PR. Expect the pinned 8× case to look ungated: it confines 16 suites to two CPUs with `taskset`, while headroom is measured @@ -86,5 +84,4 @@ burst of 8 took 104 s gated versus 19 s ungated. ## Related -- [Worktree process ownership](/quest/m0/worktree.md) - [kixelated/quest#178](https://github.com/kixelated/quest/pull/178) adds `quest worktree run`, which can share the Ctrl-C fix - [Agent lifecycle integration](/quest/m0/worktree-agents.md) - removes agent-count throttles once resource control is in place diff --git a/quest/m0/worktree.md b/quest/m0/worktree.md index bff617d4..bbe0857a 100644 --- a/quest/m0/worktree.md +++ b/quest/m0/worktree.md @@ -38,8 +38,7 @@ live and refuses a checkout that still holds anything not already preserved. lingering until the next successful release. - Out of scope: terminal Ctrl-C reaches the command twice (once from the terminal, once forwarded by the supervisor). It is shared with `quest gate` - and planned separately in - [kixelated/quest#180](https://github.com/kixelated/quest/pull/180). + and planned separately in [Gate headroom](/quest/m0/gate-headroom.md). - Measured 2026-10-21 on this repository: a fresh checkout is 2.9 MB, 394 MB after installing dependencies and building, and 2.9 MB once `run` released it. A suspended job keeps the same released footprint until it is restored. From d8b6cc6ace6df28c32aed50ad815989587adec8c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 21 Oct 2026 15:48:36 -0700 Subject: [PATCH 6/8] docs(quest): state gate admission as requirements and hazards Four review rounds kept finding edge cases in the prescribed records, lock, and clock-step mechanisms. Keep the decisions and the three-band rule, state what the admission mechanism must guarantee, and list every review finding as a hazard the implementation must test. Co-Authored-By: Claude Opus 5.5 --- quest/m0/gate-headroom.md | 62 +++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/quest/m0/gate-headroom.md b/quest/m0/gate-headroom.md index 8041e4aa..006f7f5b 100644 --- a/quest/m0/gate-headroom.md +++ b/quest/m0/gate-headroom.md @@ -33,38 +33,48 @@ burst of 8 took 104 s gated versus 19 s ungated. OOM; counting recent admissions against it keeps a burst from passing on one shared free-memory sample. Rejected: fixing the spacing only, taking the gate out of `just check` and `just test`, and keeping today's delay. -- **Admission records.** Today a claim is one `O_EXCL` file per 5-second - window, and each admission deletes the older ones. Headroom needs several - admissions per window, records kept for a minute, and a count-then-claim - step that is atomic across processes, or a burst that all reads "0 recent" - is admitted together. It must also elect a winner: if every caller in an - over-capacity burst records, sees no room, and withdraws, waiters that wake - together can repeat that forever. This replaces `Host.claim(window)`. For - example, prune, count, and record under a short host-wide lock that a caller - reclaims once its owner process has exited, like the worktree registry lock - (`acquire()` in `src/cli/worktree.ts`). Count the unexpired records rather - than reading a slot number, since expired slots leave holes. -- **`SETTLE_MS` apart, not two windows.** Today a claim is refused while the - previous window's claim exists, and every waiter wakes at the next boundary, - so queued commands start about 10 s apart. Keep the full-`SETTLE_MS` - minimum (decision 8 in - [kixelated/quest#128](https://github.com/kixelated/quest/pull/128)), but - measure it from the previous admission's time rather than its window, and - have waiters sleep until that admission plus `SETTLE_MS`. Update the - README's "Gate expensive commands" admission paragraph and the `admit()` doc - comment, which both describe today's behaviour. +- **Admission requirements.** Today a claim is one `O_EXCL` file per 5-second + window, and each admission deletes the older ones; the replacement for + `Host.claim(window)` is left to the implementation, which must meet these: + - Count and claim atomically across processes, with several admissions per + window and each admission remembered for a minute. + - Never admit past the three-band rule, and always admit some waiter when + the rule has room. + - Recover from a caller that crashes mid-admission on every platform the + gate runs on, without blocking admission indefinitely. + - Bound the effect of a wall-clock step in either direction to about one + record lifetime. + - Space band-2 admissions at least `SETTLE_MS` apart, measured from the + previous admission's time (decision 8 in + [kixelated/quest#128](https://github.com/kixelated/quest/pull/128)), with + waiters waking when that interval ends rather than at the next window + boundary, so queued commands stop starting about 10 s apart. + - Update the README's "Gate expensive commands" admission paragraph and the + `admit()` doc comment, which both describe today's behaviour. +- **Hazards to test.** Each came up in review of the plan in + [kixelated/quest#180](https://github.com/kixelated/quest/pull/180): + - A burst passing one shared free-memory sample, so every caller fits a + budget that only one does. + - A burst that all reads "0 recent" before any of them records. + - Two admissions milliseconds apart across a window boundary. + - Expired records leaving holes in numbered slots, so a slot number + undercounts recent admissions. + - Every caller in an over-capacity burst recording, seeing no room, and + withdrawing, then repeating that together forever. + - A dead lock holder's PID reused on macOS or Windows, where only Linux + records a process start time, keeping the lock held indefinitely. + - A backward clock step leaving records dated far in the future, which + count as recent indefinitely or are discarded while still valid. + - A forward clock step past the record lifetime, expiring records early and + dropping their spacing and reservations. + - `performance.now()` differing per process, and `CLOCK_MONOTONIC` + restarting at boot while the fallback claim directory persists. - **Stable claim directory.** Callers with and without `XDG_RUNTIME_DIR` use different claim directories today. Use `/run/user//quest/gate` when `/run/user/` exists, otherwise `~/.cache/quest/gate/` for shared home directories, so the choice depends only on the host, never on the caller's environment. `gate-bench.sh overhead` needs another way to get a fresh directory. -- **Backward clock steps.** A wall clock stepping back can admit two commands - closer than `SETTLE_MS`. Keep the wall clock: `performance.now()` is - per-process, and `CLOCK_MONOTONIC` restarts at boot while the fallback - directory persists. A record dated in the future means the clock stepped - back: rewrite its time to now, so it still counts and spaces admissions - however far the clock stepped, then expires a minute later. - **Ctrl-C once.** In both `quest gate` and `quest worktree run` (`src/cli/supervise.ts`), a terminal Ctrl-C reaches the child directly and again through the forwarder. Share one fix between them. On Unix, while From 0d96219c212c2982377698ca23a1008a508a0ec2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 21 Oct 2026 15:59:25 -0700 Subject: [PATCH 7/8] docs(quest): require a claim location stable across login sessions /run/user/ comes and goes with logind sessions, so choosing by its existence can split callers again. State the requirement and the hazards instead of a directory rule. Co-Authored-By: Claude Opus 5.5 --- quest/m0/gate-headroom.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/quest/m0/gate-headroom.md b/quest/m0/gate-headroom.md index 006f7f5b..65409311 100644 --- a/quest/m0/gate-headroom.md +++ b/quest/m0/gate-headroom.md @@ -70,11 +70,13 @@ burst of 8 took 104 s gated versus 19 s ungated. - `performance.now()` differing per process, and `CLOCK_MONOTONIC` restarting at boot while the fallback claim directory persists. - **Stable claim directory.** Callers with and without `XDG_RUNTIME_DIR` use - different claim directories today. Use `/run/user//quest/gate` when - `/run/user/` exists, otherwise `~/.cache/quest/gate/` for - shared home directories, so the choice depends only on the host, never on - the caller's environment. - `gate-bench.sh overhead` needs another way to get a fresh directory. + different claim directories today. Every caller of one user on one host + must use the same claim location, one that never switches with the + caller's environment or with login sessions, and that stays per-host when + home directories are shared. `gate-bench.sh overhead` needs another way to + get a fresh directory. Hazards to test: callers with and without + `XDG_RUNTIME_DIR`, and `/run/user/` appearing or disappearing as logind + sessions start and end while background callers keep running. - **Ctrl-C once.** In both `quest gate` and `quest worktree run` (`src/cli/supervise.ts`), a terminal Ctrl-C reaches the child directly and again through the forwarder. Share one fix between them. On Unix, while From d3983e28c5a81b1fdd9831f0e85ea53854fe285d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 21 Oct 2026 16:14:31 -0700 Subject: [PATCH 8/8] docs(quest): keep forwarding SIGHUP from the gate A SIGHUP sent with kill to Quest alone cannot be told apart from a terminal hangup, so skip forwarding only SIGINT in the foreground group. Co-Authored-By: Claude Opus 5.5 --- quest/m0/gate-headroom.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quest/m0/gate-headroom.md b/quest/m0/gate-headroom.md index 65409311..6dfe24a4 100644 --- a/quest/m0/gate-headroom.md +++ b/quest/m0/gate-headroom.md @@ -80,10 +80,10 @@ burst of 8 took 104 s gated versus 19 s ungated. - **Ctrl-C once.** In both `quest gate` and `quest worktree run` (`src/cli/supervise.ts`), a terminal Ctrl-C reaches the child directly and again through the forwarder. Share one fix between them. On Unix, while - Quest is in the terminal's foreground process group, skip forwarding SIGINT - and SIGHUP, which the terminal also delivers to the child; always forward - SIGTERM, which a terminal never sends. - A SIGINT sent with `kill` to Quest alone is then not forwarded. On Windows, + Quest is in the terminal's foreground process group, skip forwarding only + SIGINT, which the terminal also delivers to the child; always forward SIGHUP + and SIGTERM, which may be sent to Quest alone. A SIGINT sent with `kill` to + Quest alone is then not forwarded. On Windows, Ctrl-C reaches every process on the console and Node's `child.kill()` terminates the child forcefully, so `quest gate` waits for the child instead of forwarding; `quest worktree run` is Linux-only.