Read the runner’s log
The runner prints one line per event, timestamp first. Failure lines also go to standard error.#3 is the item number. A line ending continuing or retrying next tick
is a failure the runner absorbed. unexpected error: <message> is one it
did not expect; it keeps going.
A repeating failure is logged once, then every tenth tick as
still failing: <message> (N ticks), then recovered: when it stops. A drain says what it is waiting on, and
repeats it every thirty seconds.
To stop taking work, run outerlayer runner stop. It waits for running
builds to finish. outerlayer runner stop --now ends them as stopped.
Find out why a build ended
Start from the work item, not the host. A host build records how it ended, the session that ran in it, and what the runner did around the agent.- Run
outerlayer work status --item <n> --json, or ask the MCP server forget_work_item. The item’sclaimslist every attempt, newest first. Each has anid, anoutcome, thestageit ended in, anexitCodeand areason. Outcomes and reasons says what they mean. - Find the attempt’s session. Each session on the item has a
claimId. The one that matches the attempt’sidis the session that ran in it. It shows what the agent was doing when the attempt ended. A session linked by a person or a pull request has aclaimIdofnull. - Print the runner’s log for the attempt with
outerlayer work logs --item <n>. It prints the newest attempt’s log. Add--attempt <id>for another attempt. The MCP tool isget_runner_log. When the attempt ended in provision, sync, cleanup or report, the log ends with the last 200 lines of that stage’s log. - Read the item’s review threads with
outerlayer work threads --item <n>, orget_work_item_threads, to see what a person asked for.
outerlayer work logs exits with an error when the attempt is still running,
because the runner sends its log when it releases the attempt. It also exits
with an error for an attempt that a runner older than this feature released.
Log in to the host only when these do not answer your question. Never signal a
build’s processes by hand on a runner host. A build ended that way is lost, and
nothing on the item can show who did it.
When the runner did not end the command
A command can die of a signal the runner never sent. An operator’skill, the
kernel’s out-of-memory killer and a supervisor all do this. The runner then
records one of two reasons:
the command exited 137, the status of a process killed by SIGKILL; the runner did not end it. The status is 128 plus the signal number. A process killed by a signal reports that status, and a program can exit with it on its own, so the runner cannot prove a kill.the command was killed by SIGTERM on the host; the runner did not end it. The command’s process on the host died of a signal, and there is no exit code.
timed_out, idle, lease_lost or
stopped. A build the kernel killed for memory keeps the
out-of-memory reason.
Outcomes and reasons
Every build ends with an outcome. A failed build often also records areason naming the cause. outerlayer runner logs <item> prints the newest
build’s log.
Other reasons are a sentence, or a line a hook wrote; see
The cleanup hook.
When nothing is building an item
An item that is not being built can carry awaiting reason. The item page
shows it under Not building, and the item reads Not picked up when a
build request waits and no runner takes it. outerlayer work status --item <n> --json
and get_work_item return it as waiting. It is null while the item is
building, or queued with a runner to take it.
A runner that has not heartbeated within three of its poll intervals is not
counted.
When a lease expired
A host build can end without a release: the machine lost power, or the runner was killed. Nothing writes the end, so the lease runs out. The item page lists the attempt in Last build with the outcome lease expired, the runner’s name, and the time of the runner’s last renewal.claims in
outerlayer work status --item <n> --json marks it with leaseExpired: true and
lastRenewedAt.
A lease that ran out uses up its build request like any other claim that was
not released as interrupted or lease_lost. To retry, run
outerlayer work build or select Rerun on a host.
When the gateway refuses a signature
Each refusal is a401, logged to the organization audit log as
runner_key_signature_refused.
When a call to the platform does not land
The runner retries the claim, renew and release for up to two minutes when the gateway does not answer. Refusals are never retried. A release that still fails is logged withoutreleased:
runner status --recent marks it (release failed), and the next
runner start sends it again. The claim lapses fifteen minutes after the
last renewal, and the item returns to the queue.
If someone released the claim first, the runner logs
lease was already released, with the outcome the platform stored and the
outcome this attempt ended with. The platform keeps the outcome it stored
first.
When the runner holds off for disk
A runner that takes no work and logs this has less free disk than it needs:outerlayer runner status shows the same line, and
outerlayer doctor warns about it. GET /v1/hosts lists the runner with
lowDisk, the free and needed bytes. Its status is not changed by this.
The runner measures the file system that holds its own directory,
<config dir>/runner. Free space there, or lower
runner.housekeeping.minFreeDisk. When there is room the runner logs
free disk is back above the minimum · taking work again and claims on its
next poll, with no restart. By default the minimum equals
runner.build.disk: room for one more build.
Housekeeping removes what builds leave behind, which is usually enough. If
the disk is full of something else, such as Docker images a person pulled,
the runner does not remove it. See Housekeeping.
When the runner is blocked
A runner that takes no work and logsclaiming nothing: followed by a reason
has failed one of its own checks. What the runner checks, and how it reports
a failed check, is in
When the runner claims nothing.
A typical line:
the runner can run its work again · taking work and claims on its next
poll. Two fixes take effect only after a service restart: /dev/kvm access
and the Claude credential. The others take effect on the next poll.
outerlayer doctor reports the KVM group and the credential of the running
service, not of your shell.
When the Image build limit check fails
The runner builds every image in its own builder, a container with a memory limit ofrunner.build.memory, which needs Docker’s Buildx plugin. When
Buildx or its docker-container driver is missing, or older than 0.18,
outerlayer doctor fails the “Image build limit” check, and the runner
claims nothing. It logs claiming nothing: with the reason, and its heartbeat
carries the same reason.
Install the plugin: docker-buildx-plugin from Docker’s apt repository, or
docker-buildx from Ubuntu’s own packages. Then run outerlayer doctor
again. The runner takes work again by itself, with no restart.
A runner that has Buildx but is still starting its builder logs
the runner is still preparing its image builder. The first start pulls the
BuildKit image, which takes a moment. If the reason does not change, it
names why the builder did not start, for example that Docker Hub cannot be
reached. The builder container does not use a proxy on the host’s loopback
address. So a daemon that reaches the network only through one needs a proxy
the container can reach.
When an image build runs out of memory
A recipe step that uses more thanrunner.build.memory is stopped by the
kernel, in the runner’s image builder. The build is released failed at the
provision stage with this reason:
runner.build.memory, and check
that concurrency times build.memory still fits in the host’s memory less
2 GiB. When the limit changes, the runner replaces its builder once the image
builds it is running have ended. The next image build starts with an empty
cache.
When a build runs out of memory
In a service that setsDelegate=yes, each microVM build’s Firecracker
process runs in a cgroup of its own. When the kernel kills it at the cgroup’s
limit, the build is released failed with this reason:
runner.build.memory, and check that concurrency times build.memory still
fits in the host’s memory less 2 GiB. A build killed below its limit means
this host needs more reserved memory. It needs more than the 1 GiB reserved
for Firecracker or the 2 GiB reserved for the system. Lower runner.concurrency to leave more room.
A service without Delegate=yes gives microVM builds no limit of their own,
and outerlayer doctor warns. Run outerlayer runner install --service --replace to write the unit this CLI generates. A container build’s limit is
Docker’s --memory at build.memory.
When an item comes back
A build request is used up by any claim after it: a host build releasedok, incomplete, failed, timed_out or idle, a claim that lapsed, or a local
session. So a failed build is not claimed again. To retry, run
outerlayer work build, or select Rerun on a host in the Last
build block of the item page. The control is offered when no build is running
and no request is unused. It also needs either no open pull request on the
item, or a review that waits on an agent. With an open pull request, the
rerun starts an amend instead of a build. work build --local does not queue the item.
interrupted, lease_lost and exit 75 leave the request unused, so the item
stays available. So does stopped when the operator ended the build on the
host. A build stopped through OuterLayer uses its request up.
An item that still needs work comes back through the amend queue when a
person sends notes or fails it, not through the implement queue. A failed
amend build is not retried until a person fails the item again, or a member
selects Rerun on a host.
While no session is linked, the item’s Sessions list names who holds a
live claim. It names a runner by name, or a person’s name followed by “(local session)”. When nothing is
queued it names outerlayer work build. Run it yourself gives the
command that starts a session by hand.