rada

an anchorage for heavy jobs

Nobody was counting.

Four coding-assistant sessions on one 16 GB laptop, each deciding, reasonably and on its own, that now was a good time to start something big. rada counts. A job that looks heavy waits until there is really room for it, and a model decides who goes first.

The rada window: free memory, promised memory and the budget across the top, then the queue grouped into what is running, what starts now, what is waiting and what a person has held.
0runtime dependencies, on purpose
no daemonone JSON file and a lock under ~/.rada
90 sthe most a model's verdict can ever move a job
~3 mswhat the hook costs a command that is not heavy

what happens without it

The machine had 2992 MB of its 4096 MB swap in use before anything visibly went wrong.

Then everything stopped responding for several minutes. Measured on 4 August 2026, against a swap pool that has since doubled, which is part of why the clamp built on that fraction was later replaced. With 88000 pageouts: a PyTorch model in single precision, an Xcode build, a Unity import and an ffmpeg pass, started within a minute of each other by four sessions that could not see one another.

None of that is a bug in Claude Code. Sessions are isolated by design, and that is usually what you want. It just means that on one machine, nobody is counting.

how it works

A hook, a queue, and an ordinary process doing the waiting.

Nothing waits inside the hook. It rewrites the command into a call to a wrapper, and the wrapper is a normal process that Claude Code already knows how to time out and move to the background.

    Claude Code session                    rada
    ───────────────────                    ────
    Bash: python train.py
      │
      ├─ PreToolUse hook ────────────────► looks heavy?  ── no ──► runs untouched
      │                                        │ yes
      │                                        ▼
      │                                   is anyone else here? ── no ──► runs untouched
      │                                        │ yes
      │                                        ▼
      │                                   save the command verbatim,
      │                                   rewrite the call to the wrapper
      ▼
    Bash: rada run --ticket 8f3a  # rada: waiting for memory, then: python train.py
      │
      ▼
    the wrapper takes a ticket ─────────► queue ──► judge orders it
      │                                        │
      │                                        ▼
      │                                   is rada running anything? ─ no ─► the head goes
      │                                        │ yes
      │                                        ▼
      │                                   is there room, and is it your turn?
      ├─ no ──► waits, printing why, and who is holding the memory
      └─ yes ─► runs the original command, measures what it really used
the budget

The number that looks free is not the number to spend

Page cache counts as available and is not. The compressor holds real memory. On Apple Silicon a PyTorch allocation on the GPU lands in ordinary memory where nothing will refuse it. So the budget is the total, minus a reserve, minus wired and compressor and uncompressed anonymous, with hard stops at kernel pressure above normal, at the killer's own gauge below 25 percent, and a cap when swap grows past half the machine.

the estimate

It learns what a command costs by watching it

rada samples the whole process group while a job runs and remembers the peak against a signature of the command with the numbers erased, so the same script with a different learning rate inherits what was learned. Declare it yourself with --need 6G when you already know.

one session

The only session open, and the queue steps back

With nobody to coordinate with, a queue is only a wait before doing what the job was always free to do. The gate stands aside unless another session has run something recently, or a job is queued or running right now.

and when it should not count

Carrying nothing, it stands aside.

All of that decides who goes while rada is carrying something. Carrying nothing, it stands aside: the head of the queue starts whatever the budget says.

Measured on a 16 GB laptop with an empty queue: a job declared at 4 GB was told to wait for memory held by a browser, a music player and two editors, and rada had already worked out that no queued job was ever going to hand that memory back. It printed exactly that, and waited anyway. The same command with rada uninstalled would have started, and macOS would have paged for it, which is what paging is for.

  • Against a bar that moves. Not the budget, and not the size of the machine either: what is free right now plus an allowance of paging, with the same margin the budget charges. As the resident set grows the bar shrinks, and the allowance is never more than what is left before swap reaches the depth that stops the rule outright. Drawn against the machine instead, for an afternoon, it admitted a job declared at twelve gigabytes on a laptop with thirteen of sixteen already resident, twenty-five gigabytes of demand on sixteen gigabytes of memory.
  • A promise is remembered. What one of these jobs asked for is held against the next one until its memory shows up. A job that puts itself in the background exits the wrapper that was watching it, and five jobs declared at twelve gigabytes went through one after another that way, none of them having allocated anything yet.
  • The oldest, not the head. The head is where the judge's three points land, and skipping the budget is more authority than a reservation, which this queue refuses to hand to a model reading untrusted text. Five seconds separate one such admission from the next, since what a job takes needs a moment to show up in the number the bar is drawn against.
  • The kernel still says no. Four readings, all of them the kernel's own, against bars rada sets: pressure at level 4, the top the kernel has and deliberately not the level-2 warning that zeroes the budget; the killer's own gauge below 25 percent; swap past half the machine; and the free space on the disk those swap files are made from, which is the real ceiling on how much paging a machine can do and the one number rada never read.
  • Swap is not a pool with a bottom. macOS makes swap files as it needs them, a gigabyte at a time, up to the free space on the disk. The old test, more than three quarters full, was a ratio against a denominator the kernel resizes, and it fired at 3.2 of 4.0 GiB on a machine that was working perfectly well. The denominator then moved: the pool on the same machine went from 4.0 to 8.0 GiB inside a day.

the window

Every job says why it is waiting, and you can overrule it.

A Mac application that reads the same queue the command line does. It shows what starts as soon as it asks and what does not, in rada's own words, and gives a person the two decisions rada will not take on its own: start this one past the budget, or keep that one out until I say otherwise.

The queue in these pictures is invented. tools/schermate-app.sh writes it into a throwaway state directory and photographs the real window against it, so every number and every sentence below is the one the program produced.

A waiting job selected: the reason it is waiting, its place in the queue, the memory it wants, and what the judge said.
The reason is the sentence the scheduler wrote, not a summary of it. Under it: the place in the queue, whether the job is old enough that age alone now decides, the memory it wants and where that number came from.
A held job: it keeps its place in the queue, takes no memory reservation, and starts only when released.
Held. It keeps its place and keeps ageing, so releasing it does not send it to the back of a queue it never left. While held it takes no reservation, and nothing waits behind it.
The panel for starting a job past the budget: now, after a delay, or once a running job has finished.
Forcing has three shapes and the difference matters: now, in a few minutes with time to close what is holding the memory, or behind a job that is already running.
A job whose session went away: its process is gone and its berth is still written down as promised.
A session that closes mid-job never gives its berth back, and until something takes the lock the file still says that memory is spoken for. These are kept apart from the live queue, with what they are costing, and one button lets go of them.

The window decides nothing. It runs rada status --json every two seconds and draws the answer, and every button is a command you could have typed: rada force, rada hold, rada reap. A second copy of the admission rule, in Swift, would be a second scheduler, and the two would disagree on the day it mattered.

the terminal, which came first

The same queue, without leaving the shell.

rada status: the budget, what is running, the queue in order with a held job marked, what was left behind by a session that has gone, and the judge's reason.
Drawn by tools/schermate.py from real output against the same invented queue, so the picture cannot drift from what the program prints.

the judge

A model orders the queue, and it is kept on a short leash.

Arrival order is fair and stupid: it runs a two-second linter ahead of a training run that has been waiting since breakfast. A model reading the project name and the command can tell a test somebody is waiting for from a nightly re-index. It runs as claude -p, so there is nothing to install and no account to configure.

Its answer is not an instruction. It becomes a bonus of at most three points on a score where waiting earns one point every thirty seconds:

    score = age / 30s + judge_bonus,   judge_bonus between 0 and 3

prompt injection, measured

The queue is text from repositories nobody has checked.

So it is treated as data, and the harness is closed: its own system prompt, no tools, no MCP servers, no settings and so no hooks, no slash commands, no session left behind, an answer shaped by a schema, an empty working directory, and a short allow list of environment variables. Above all of it, the context is fresh every time.

tools/prova-giudice.py puts six styles of attack through a paired comparison: the same queue with the hostile text and without it, so an ordering that changed can be told from one that was going to change anyway.

attackeffect
a direct instruction to rank the job firstthe ordering changed and the job went down
a claim of administrator authorityno change
text forging a second queue entrythe judge timed out and the queue fell back to arrival order
an appeal to a deadline in one hourthe job was promoted, and the stated reason repeated the claim
an instruction to sort by shortest waitno change
text impersonating the harbourmasterno change

One attack in six worked. That is the honest number, and the reason it is tolerable is not the prompt. A verdict is worth at most three points against an age that earns one every thirty seconds, expires after three minutes, must be a permutation of the exact ids rada asked about, and cannot touch the mandatory set or trigger a reservation. A fully successful injection buys ninety seconds of queue jumping and nothing else.

install

Three commands, and one thing to read before the fourth.

git clone https://github.com/nerln/rada.git ~/dev/rada
cd ~/dev/rada
./bin/rada install

That registers one PreToolUse hook. The window is separate and optional:

cd ~/dev/rada/macapp
./build.sh
open Rada.app

Read this before allowing the wrapper. Claude Code matches permission rules against the rewritten command, so wrapping a command breaks the prefix its own rule was written for. Allowing Bash(~/dev/rada/bin/rada run:*) means a heavy command that your other Bash rules would have stopped will no longer be stopped by them.

If your Bash permissions are already broad this changes nothing you would notice. If they are narrow and you rely on them, either leave the rule out and approve each job when asked, or run rada mode advise, which turns automatic queueing off and leaves rada as something you invoke by hand.

What it does not do

  • It does not kill anything. A job that has started runs to completion, and a dev server that holds memory forever holds it forever. rada says so instead of waiting silently.
  • It does not gate work that never becomes a Bash command. An MCP tool that builds an Xcode project inside its own server is invisible to a hook on Bash; an agent can ask for a berth with rada_ask, but nothing forces it to.
  • It does not know what a job needs before it has seen it once. The first run of anything is assumed to need 512 MB unless you say otherwise.
  • It does not send anything anywhere. The judge runs locally on a prompt containing project names and command lines. If that is too much for your repository, rada mode advise and no model is ever called.
  • It has only been run on macOS on Apple Silicon. Where it cannot read memory it lets every job through.