Debugging a failed CI job often means adding a print statement, pushing a commit, waiting for a new job, and repeating. A normal CI job ends when it fails or times out, taking its machine, files, and processes with it, so there is nothing left for a debugger to attach to.
Reruns lose the failure state
A rerun rebuilds the environment, repeats the steps before the failure, and reaches the failure without the original workspace or processes. Files written by the failed attempt are gone, and the log is the only remaining witness.
A rerun also gives weak evidence about flaky failures. A race may pass on the rerun and leave the bug unresolved. A deterministic failure repeats without adding information.
What pause means
Start the job with --debug, and a step that exits non-zero pauses the job instead of ending it. The microVM keeps its filesystem, environment, and processes. The run stays held until a controller continues, retries, or aborts it.
Three controls make the paused session usable:
The machine suspends without an attached controller. It resumes when a client attaches. A paused job therefore does not occupy an active slot indefinitely.
One writer controls the session. The controller with the writer lease can mutate the workspace and issue a retry. Other clients attach read-only. This prevents concurrent sync and retry operations from producing an unintended workspace.
Pause credit is bounded. Credit accrues only while a controller is attached, with a ceiling per job. A polling client cannot hold a machine forever.
The session
The session is available through the CLI:
$ plp ci debug 0x2f19
attached · test (parser) · paused at step 6 of 9
ci> pytest tests/parser -x -q
FAILED tests/parser/test_offsets.py::test_offsets
AssertionError: 41 != 42
ci> :sync
pushed 3 files to the job workspace
ci> :retry --from 6
step 6 passed
step 7 passed
step 8 passed
resumedIllustrative session.
The job and the local machine have separate filesystems and may use different architectures or build outputs. :sync therefore pushes local edits into the job explicitly. --export brings job changes back as a patch, and --patch-only writes that patch without applying it. If both sides changed the same file, export stops instead of choosing a winner.
Retry from the failed step
The failed step may have written files, appended to $GITHUB_ENV, started a database, or made an external API request. Rewinding a filesystem would not undo those other effects, so preloop does not pretend to restore the earlier state.
:retry reruns from the failed step against the current workspace. :retry --from <step> can retry from any selected step, including the failed step. --revert provides deliberate cleanup with none, untracked, or all; it defaults to none. Removing changes from the failed attempt is an explicit choice.
The session record also distinguishes a job that passed after a controller changed its environment from a job that passed on its first attempt.
Use the debugger you already have
The job exposes a debug adapter over WebSocket. Use plp ci dap or an editor's remote-attach flow to step through a job: next, step in, step out. There are no breakpoints to set; the debugger stops at step boundaries and lets you inspect the workflow context: event, environment, job, and completed steps.
Agents attach through the same session
The CLI and API expose the same operations:
- List sessions.
- Take the writer lease.
- Poll for a verdict.
- Issue retry, retry --from <step>, continue, or abort.
- Close the session.
A verdict poll returns null on timeout. null does not mean abort, so an agent must keep the session alive rather than destroy it. Every mutating operation is retained in the session audit trail, including retries that change a build's result.
Why the pause is practical in CI
Hosted runners are shared machines, so holding one for a person is expensive and affects other jobs. preloop gives each job a single-use microVM that is cheap to create and destroy. Pausing keeps one job's machine available without holding a shared runner, while bounded pause accounting limits capacity and cost.