Documentation
¶
Overview ¶
Package pausereason gives a paused run a machine-readable explanation.
Before this existed, every one of the ~26 sites that paused a run wrote the same opaque string into ProjectState.Status: "paused". An approval gate waiting on a human, a budget that had run out, an operator's Ctrl-C and an exhausted subscription window were indistinguishable in the API, in the dashboard and in the audit trail. An autonomous fleet could stall for a week and the only way to learn why was to read the terminal scrollback of a process that had already exited.
The package is deliberately a leaf: pkg/state imports pkg/statedb, so the type cannot live in either of them if both are to name it. It depends on nothing but the standard library.
Index ¶
Constants ¶
const ExitStateNotPersisted = 74
ExitStateNotPersisted is the exit status of a `cloop run` that stopped for CodeStateNotPersisted: 74, sysexits' EX_IOERR. It is the account that survives when the database takes nothing at all — not even the run's own pause — so the hub, reading it from the finished workload, can still say why the run ended rather than only that it did. It lives here, beside the code it stands for, so the hub need not import the orchestrator to read it.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Code ¶
type Code string
Code names why a run paused. It is the field a caller switches on, so the set is closed — an unrecognised code is a bug, not an extension point.
const ( // CodeUsageCap is a Claude Code subscription window whose configured // per-project cap has been reached, or that is exhausted outright while // budget.block_extra_usage keeps the run from spilling into extra usage // (Task 20440). This is the one pause that reliably ends on its own, which // is why it carries ResumesAt. CodeUsageCap Code = "usage_cap" // CodeBudget is a configured spend limit: the daily/global budget in // pkg/budget, or a per-task cost ceiling. CodeBudget Code = "budget" // CodeTokenBudget is the --token-budget cumulative token ceiling. CodeTokenBudget Code = "token_budget" // CodeStepLimit is the --steps or per-project max-steps ceiling. CodeStepLimit Code = "step_limit" // CodeApproval is a human-in-the-loop gate: an approval declined, or an // interactive review the operator quit. CodeApproval Code = "approval" // CodeAbort is a provider or harness refusal that waiting will not fix — // a hard quota, a rejected credential, a harness that would not start. CodeAbort Code = "abort" // CodeCancelled is a context cancellation: Ctrl-C, a deadline, or the // hub stopping the run. CodeCancelled Code = "cancelled" // CodePlanOnly is --plan-only: the plan was decomposed and shown, and // execution was never intended. CodePlanOnly Code = "plan_only" // CodeIdle is the ordinary end of a run — every runnable task is done and // auto-evolve is off, so there is nothing left to do until someone adds // work. It is a pause rather than a completion because the plan may still // grow. CodeIdle Code = "idle" // CodeOperator is a stop the operator asked for through the hub or CLI. CodeOperator Code = "operator" // CodeStale is a run that died without saying so — the process vanished // and the hub reconciled its status on the way back up. CodeStale Code = "stale" // CodeStateNotPersisted is a run that stopped itself because the project // database refused a write it could not do without: a task's outcome, a // pause, a plan change. Carrying on would have reported work the database // does not hold. Like CodeAbort it needs a human — free the disk, fix the // permissions, repair the row — and the next run recovers the tasks this // one left in progress. CodeStateNotPersisted Code = "state_not_persisted" // CodeUncommittedWork is a run that stopped because its tasks kept ending // with their changes uncommitted — or committed but not pushed — under a // project's "done means committed" setting (Task 20370): each such attempt // goes back to pending with its work left in the tree, and after the run's // consecutive-abort ceiling it stops rather than loop. A human decides: the // work is there to finish, commit or revert. CodeUncommittedWork Code = "uncommitted_work" // CodeDiskLow is a run that stopped starting work because a volume it // writes to — the one holding the project's .cloop, or the working // tree's — has less free space than orchestrator.min_free_disk_mb // (Task 20381), or because a task's outcome only reached the disk by // spending the space .cloop/reserve held back for it. A write that fails // for want of space is how a run used to learn this, and the write that // fails first is often the one recording what a task did. // // It is the one pause the run waits out itself: the process stays up, // checks again every minute, and carries on once the volume is back above // the floor plus a tenth — see RunWaits. CodeDiskLow Code = "disk_low" // CodeExecutorLost is a run whose executor stopped answering mid-run and // that nothing carried on (Task 20396): the supervisor declared the node // lost and claimed its session, and either no executor could take the run, // executors.failover.max_attempts allowed no further re-dispatch, a task it // was running was quarantined as a suspected node killer, or the // replacement could not be started. The detail names the lost executor and // why the run stopped there. A human decides: bring the executor back or // bind the project to another, then press Run. CodeExecutorLost Code = "executor_lost" // CodeDiskLimit is a run whose executor stopped it because its workspace // grew past its disk limit — .cloop/sandbox.yaml resources.disk, or an // operator's disk ceiling (Task 20405). The detail names both sizes. The // run is not retried: the task it was running goes back to pending, and a // human decides — free space in the workspace or raise the limit, then // press Run. A workspace still over the limit is refused at the next start. CodeDiskLimit Code = "disk_limit" // — an automatic resume after a usage cap, a failover's replacement — but // could not act for the person who started it (Task 20443): on a hub with // single sign-on their Claude Code home is gone (they were offboarded), they // are denied on the hub, or nobody can be named. Carrying on would have // spent the hub's shared login, or another person's. The detail names who // and why. A human decides: start the run as themselves, which acts on // their own account. CodeStarterUnavailable Code = "starter_unavailable" )
type Reason ¶
type Reason struct {
// Code is the machine-readable category. Always set.
Code Code `json:"code"`
// Detail is one operator-facing sentence fragment naming the specific
// cause — "5-hour cap reached", "daily budget of $20 spent". It is
// prose for a human; never switch on it.
Detail string `json:"detail,omitempty"`
// ResumesAt is when the condition lifts on its own, for the pauses where
// that instant is actually known. Only CodeUsageCap sets it today: the
// OAuth usage API reports each window's reset, so the hub can resume
// without a human. Nil means "nobody knows; a human decides".
ResumesAt *time.Time `json:"resumes_at,omitempty"`
}
Reason is the structured explanation persisted alongside Status.
func NewUntil ¶
NewUntil builds a Reason that lifts by itself at resumesAt. A zero time is treated as unknown, so callers may pass a reset time straight through without first checking whether the provider supplied one.
func Normalize ¶
Normalize returns the reason that should be stored against a run in the given status, or nil when none should be.
It enforces the invariant that a pause reason exists only for a paused run. Persisting it at exactly one funnel — rather than expecting ~26 call sites to clear the field on the way out of "paused" — is what keeps a resumed run from still claiming it is waiting on a cap that lifted hours ago.
func (*Reason) AutoResumable ¶
AutoResumable reports whether this pause has cleared by now and can be restarted without asking a human.
Only CodeUsageCap qualifies. Every other code either needs a decision (a declined approval, a raised budget) or describes a run that was not meant to continue at all (plan-only, operator stop) — resuming those on a timer would override the very intent that stopped them.
func (*Reason) Label ¶
Label is the human detail, falling back to the code's own noun when the caller supplied none.
func (*Reason) RunWaits ¶ added in v0.0.5
RunWaits reports whether the run that recorded this pause is still alive, waiting the condition out itself.
Only CodeDiskLow qualifies (Task 20381). Every other pause ends the run that wrote it, so "paused" means nothing is executing and a Start is in order. A disk_low pause does not: the run holds its place, checks free space every minute and carries on unprompted, and the operator's move is Stop. Whatever shows a project as running, or decides that a run claiming to be alive has died, has to count this pause as a live run — otherwise the dashboard offers Start on a project that is executing, and a run killed while it waited keeps saying it is waiting forever.
Deliberately not AutoResumable: the hub restarting a run that is already waiting would put a second harness in the same working tree.
func (*Reason) Summary ¶
Summary renders the one-line form the CLI and logs use:
5-hour cap reached, resumes 14:50
The clock is rendered in loc so a terminal shows the operator's own wall time. The Web UI does its own formatting from ResumesAt instead, because only the browser knows the reader's timezone.