Skip to main content
Awaiting runWorkflow is not a process lifecycle boundary. The first task dispatch lazily boots a process-local Effect Cluster “SingleRunner”: an in-process task routing stack that forks repeating daemon fibers (shard lock refresh, shard assignment refresh, message polling, runner health). Their timers keep the event loop alive, so a finite Bun or Node program can finish every run and still hang. closeSingleRunnerRuntime() is the teardown boundary. Calling process.exit() instead is unsafe: a hard exit skips your own database flushes and cleanup.
Signatures:
Nothing in Smithers calls either function for you. Gateway, up --serve, and the HTTP server deliberately keep the runtime open across many runs, so Gateway.close() does not close it.

Semantics

Construction failures are unaffected: a build that throws while the runtime is open stays retryable and the next dispatch rebuilds, exactly as before.

Busy is a lease, not a guess

Two levels of lease keep a close from landing in the middle of live work:
  • A run lease spans the entirety of one runWorkflow call: validation, every task attempt, the driver’s retry backoff between attempts, and cleanup.
  • A dispatch lease is taken at the top of every task dispatch, before the runtime is even built.
The run lease is the important one. Between two attempts of a retrying task there is no in-flight dispatch at all, so an “are any tasks running” check would report the runtime as idle and tear it down under a live run.

Ordering for a process owner

Only the component that owns the process should close the runtime, and only after it has stopped admitting work:
  1. Stop accepting new runs.
  2. Abort in-flight runs through RunOptions.signal.
  3. Await settlement of every runWorkflow promise you started.
  4. await closeSingleRunnerRuntime().
  5. Clean up your own resources: database handles, servers, backends.
Smithers installs no SIGINT or SIGTERM handlers of its own, so this ordering is entirely yours to choose.

Reopening

reopenSingleRunnerRuntime() exists for long-lived hosts that embed a component which closes the runtime at its own shutdown. It returns a closed runtime to idle so the next dispatch rebuilds it. It is a no-op when the runtime is still usable, and it throws while a close is in flight: await closeSingleRunnerRuntime() first.
Do not use it to paper over a misplaced close during shutdown. Rebuilding the runtime restarts every daemon fiber, and the process stops being able to exit again.