Time and ParkingWaiting is free — four wait reasons, the wake sources that end them, and day-scale work as ordinary rows.

The Harness's answer to long-running work is that waiting is free. A run that parks holds no process, no memory, no connection — it is a row with a wait reason, and the dispatch loop wakes it when the reason ends. Kill the server mid-sleep and restart it; the timer sweep finds the due rows and continues.

The four wait reasons

type RunWait = | {reason: 'children'; toolCallIds: string[]} | {reason: 'timer'; wakeAt: number} | {reason: 'event'; waitId: string; timeoutAt?: number; label?: string; answerWith?: string} | {reason: 'budget-pause'; note?: string}


    children — the run delegated and is waiting on the tool calls it spawned.

    timerctx.sleep(ms) persisted a wake time. Parallel sleeps keep the earliest deadline; overwriting would silently delay the shorter sleep's continuation to the longer one's wake. Sleeps under 60 seconds hold the VM with a real timer instead of parking, since the round trip would cost more than the wait.

    eventctx.waitForEvent(match, {timeout}) registered a durable wait in run_event_waits. It resumes on a matching activity event, a trigger's wake, or a human SignalRun.

    budget-pause — the run exceeded its wall-clock budget and is holding for a human Resume.

Only timer and event have a clock wake; children and budget-pause are ended by something else happening. That is why parkWakeAt maps a timer to its wakeAt, an event to its optional timeoutAt, and everything else to null — the single not_before column then drives one sweep for both sleeps and timed-out event waits.

A wait is not a trigger

ctx.waitForEvent gets its own table rather than reusing the trigger machinery, and the distinction is deliberate: a trigger is standing user configuration a person lists and edits, while a wait is transient state belonging to one run. Same matching code, different lifecycle — and because both ask the same shared matcher, they can never disagree about what an event means.

A timeout is not special machinery either: it is a timer wait racing the event wait, resolving to null when the clock wins.

Every park is legible

A parked run's card says why, in words: "sleeping until 08:00", "waiting for approval", "paused: daily budget". An event park advertises the signal name that would actually answer it — and there is a test asserting that the advertised signal is one the wait would genuinely accept, so the Answer button can never send something the wait rejects.

A parked parent also keeps conversing: a run waiting on children does not count as "live" for session status, so background work never stalls the chat.

Budgets

A run's budget can bound wall time, children, and depth. Only wall time triggers a pause, measured from the run's creation — so time spent parked counts against it. Resuming drops the wall-clock budget rather than extending it, so the run does not immediately re-pause; every other budget dimension stays in force.

Staying bounded over weeks

A script that loops for days accumulates journal. continueAsNew is the escape: finalize, enqueue a successor carrying declared state only, keep the same place in the tree.

Related

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime