The background server
The optional background server.
ubq server starts it. ubq server stop stops it. It is optional
(01-agent-ideation.md, section 13). Decisions: D36 and D37.
What it does#
- It keeps 8 test processes that are started. Each one ran
the coverage preload and waits, in a read of stdin, for a test
file (
src/warm.ts). A runner that finds the server asks it for a process for each test file. With no server, or with no process for the request, the runner starts the process itself. - It makes the counters of a file when the file changes. It watches the project folder. For a changed source file it writes the cache entry of the counters, with the source with no types. The first test process that loads the file after an edit then needs no parser, and Node does not load its code that removes types.
It does not run tests, it does not keep results, and it loads no file of the project. A result is never reused (D8).
Rule: the same result with and without the server#
The runner is the same process and the same code in the two cases. Only the start of a test process differs. So the output, the exit code, and the store are the same by construction.
A process of the server is equal to a process that the runner starts, but for these points:
- It gets the environment of the runner after it started. A
runtime reads some variables only at its start (
NODE_OPTIONS,TZ). So the server gives out a process only if the environment of the runner is equal to the one that the process started with (_,SHLVL,PWD, andOLDPWDcan differ). If not, the runner starts its own process, and the server starts new ones with the environment of that runner. - Its stdin is a pipe at its end, not
/dev/null.
The server stops, and the runner starts its own processes, if the
runner has a different runtime or a different ubq (stamp in
src/pool.ts: the path and version of the runtime, and the
newest change time of the files of ubq).
Parts#
| File | Contents |
|---|---|
src/server.ts |
the server command, the server process, the watcher |
src/pool.ts |
the socket path, the request, the frames, warmProcess for the runner |
src/warm.ts |
the program of a process that waits |
The socket is .ubq/server.sock, with mode 600. A runner can ask
the server to run any file as the user of the server, so only
that user can connect. Errors of the server go to
.ubq/server.log. The state is in the project folder, so two git
worktrees have two servers.
A request is one line of JSON. The answer to run is a stream of
frames (one tag character, a length of 4 bytes, the data): i
the process ID, o stdout, e stderr, p the events of fd 3,
x the exit code and signal, n no process. The runner sends
signals to the process itself: it has the process ID, and the
user is the same.
The server stops after 30 minutes with no request and no change of a file.
Measured (2026-11-12)#
Script: experiments/server/warm.mjs, and the benchmark suite.
| Case | No server | Server |
|---|---|---|
| One test process, start to end | 45 ms | 17 ms |
ubq with 3 files |
80 ms | 55 ms |
ubq with 2 files, first run after an edit of a source file |
120 ms | 75 ms |
ubq --repeat 50 of one file with 9 tests |
2200 ms | 760 ms |
ubq mutate of one file, 12 changes |
930 ms | 630 ms |
ubq with 200 files |
850 ms | 850 ms |
ubq with 1 file (runs in the process of the runner) |
51 ms | 51 ms |
A run of many files does not get faster: the server starts a new
process for each one that it gives out, and that start costs the
same in the server as in the runner. The gain is for small runs,
for --repeat, and for ubq mutate.
Why no MCP interface, and no HTTP (D37)#
The interface for an agent is the ubq command and its text.
- Each agent has a shell. An MCP server needs a setup step in
each agent client, and its tool descriptions use context in
each session. The command needs no setup, and
ubq guidecosts context only when the agent reads it. - The step 8 check passed with the command and the guide alone, on the smallest model, with no wrong call.
- The time of a tool call of an agent is seconds. A command that takes 50 ms is not the slow part, so a connection that stays open gives an agent nothing.
- What MCP would add is typed arguments. The one argument that is
hard to write in a shell is a test ID (
'a.test.ts > group > name'). The failure block printsubq <file>:<line>for this reason. - HTTP needs a port. Two worktrees then need two ports, and a
port is open to each user of the machine. A socket file in
.ubq/has neither problem.
The socket protocol is internal. It can change with each version.
If an MCP interface is wanted later, it is a small adapter: one tool for each command, which calls the same functions as the CLI and returns the same text.
Not built#
- The server does not start by itself. A test run that leaves a process behind is a surprise, and in CI it is an error.
- No process of the server for a run of one file: that file runs in the process of the runner.
- On Windows, the server has a named pipe in place of a socket
file, and
taskkillin place of a process group. The file.ubq/server.sockthere only tells that a server started.