← all releases

v1.0.0

on GitHub

Not the newest. v1.5.1 is out →

Release notes

Added

  • Update check — the daemon checks for a newer release and shows an amber indicator next to the version in the Web UI and TUI. Never auto-updates; opt out with [daemon] check_updates = false.
  • Per-service stop and restart from the CLI — runwisp stop <service> stops one service and runwisp restart <service> restarts it (starting it if it was stopped), leaving the rest of the daemon running.
  • [services.*] can set manual_trigger = false to lock a service against manual stop, restart, and start, from the CLI, REST API, Web UI, and TUI, until you edit runwisp.toml and reload.
  • runwisp import systemd converts systemd .service units into runwisp.toml — a unit with Restart= becomes a service, a Type=oneshot unit a task — flagging anything it can’t model (multiple ExecStart, Type=notify, sandboxing, socket activation) with inline # TODOs. See From systemd.
  • [daemon] trusted_proxies sets the reverse-proxy CIDR allowlist in TOML; the RUNWISP_TRUSTED_PROXIES env var still works and overrides it. Catch-all ranges (0.0.0.0/0, ::/0) are rejected at config load.
  • runwisp service install now sets a stable Web UI password. With auth on, it persists a password into a 0600 drop-in beside the unit — a freshly generated one (printed once), or your own RUNWISP_PASSWORD if you set it at install time — so a managed daemon no longer mints a new password — logging every session out — on each restart. Re-installing never rotates it.
  • runwisp service install now carries your RUNWISP_* environment into the service — RUNWISP_AUTH, RUNWISP_TLS, RUNWISP_CLOUD_TOKEN, and the rest, into a 0600 drop-in beside the unit, refreshed on every re-install. A systemd unit or launchd plist never inherited these before, so they were silently dropped.
  • [defaults] now accepts restart_delay, restart_backoff, catch_up, and graceful_stop, so you can set these fleet-wide once instead of repeating them on every unit.
  • Compose per-service overrides ([compose.<alias>.<svc>]) can now set failures, matching what [services.*] already allows.

Changed

  • The agent-guide subcommand is replaced by a machine-readable docs pointer. Running runwisp --help in a non-interactive shell now appends a link to https://docs.runwisp.com/llms.txt for AI coding agents.
  • runwisp reload now warns when it leaves a service stopped whose definition has autostart = true, pointing you at runwisp restart <service> — a reload reconciles definitions but never starts a stopped service.
  • The dashboard drops the runner-facts strip. The instance fingerprint now sits in the sidebar footer beside the version, and uptime is now one of the overview stat panes.
  • The dashboard’s “running now” stat pane is now “total runs”, showing every run ever recorded with compact large numbers (e.g. 55.6k, 1.4m). The overview “Up next” list shows the next five upcoming tasks with a +N more footer for the rest.
  • treat_missed_as_failure and exit_codes are replaced by one failures key on [defaults], [tasks.*], and [services.*]. List the outcomes that count as a failure — end-reason names (failed, timeout, crashed, missed, stopped, …) and exit codes or inclusive ranges ("42", "1-23") — to drive the failed stat, UI badges, the runs-browser Failed filter, and per-task notify. A bare list replaces the inherited set; prefix every token with +/- (["-missed"], ["+stopped"]) to add or drop a single outcome without restating the default. Exit 0 is always success and any non-zero exit is failed; there’s no success-code allowlist anymore. See What counts as a failure.
  • [compose.*] include and exclude are replaced by one services key. Bare or +name entries keep only those services; -name entries drop them; the two polarities are mutually exclusive.
  • notify_on_failure and notify_on_success are replaced by one notify key on [tasks.*], [services.*], and compose overrides. It pages when the unit’s failures policy classifies a run as failed; route any non-failure outcome (a success ping, a timeout-only escalation) with an explicit [[route]]. See Per-task notifications.
  • restart and restart_attempts are no longer accepted on [tasks.*] — both are now rejected at load. A task re-runs a failed run with retry_attempts/retry_delay/retry_backoff, which are fully tunable; restart stays on [services.*] for instance supervision.
  • [[route]] match.kinds now speaks the same outcome vocabulary as failures (failed, timeout, crashed, log_overflow, stopped, missed, … plus service.fatal, log.disk_pressure) instead of the run.* stream names, added a first-class match.failure = true that matches any run classified as a failure, and dropped the derived match.severity axis. See Notification rules.
  • Run history now distinguishes ui (Web UI / TUI “Run Now”) and cli (runwisp run) from a raw api REST call, instead of tagging all three the same way.
  • on_overlap is now rejected on [services.*] (and compose per-service overrides): it was a silent no-op there, since a service’s concurrency is instances, not overlap.
  • [scheduler] timezone moved to [daemon] timezone. The single-key [scheduler] table is gone; a config that still has one fails to load with a hint pointing at [daemon].
  • Every task/service-only key now rejects with a pointer to the right section (e.g. restart under [tasks.*], cron under [services.*]), not just the handful that already did.
  • [services.*] can now set restart (never / on_failure / always, default always), matching what a compose-imported service’s per-service override already allowed.
  • Every duration field (timeout, retry_delay, graceful_stop, jitter, etc.) now accepts d (days) and w (weeks), not just keep_for — matching what the schema already advertised.
  • GET /api/daemon’s tasks list now returns the same full task shape as GET /api/tasks (description, timeout, retry/restart settings, and more), instead of a separately-trimmed subset.
  • GET /api/runs gained an isFailure query parameter, matching the isFailure field already accepted in bulk-operation request bodies.
  • POST /api/runs/bulk/delete now reports the runs it skipped for being active in a skipped list, instead of silently dropping them from the affected count; deleting a single active run still returns a 409.
  • The auth endpoints (/api/auth/status, /api/auth/challenge, /api/auth/login, /api/auth/launch-ticket) are now documented in the OpenAPI spec (runwisp openapi), so API clients get generated request/response types instead of guessing the shape.
  • GET /api/notifications/stream has been removed. Notification events already ride the unified GET /api/events/stream.
  • GET /api/notifications/unreadCount is renamed to GET /api/notifications/unread-count, so every REST path uses kebab-case. The unreadCount JSON field is unchanged.
  • catch_up is now an integer and max_catch_up_runs is gone. The value is how many missed cron ticks to re-fire on startup: 0 (was "skip"), 1 (was "latest", the default), or N to replay up to N (replacing "all" + its separate cap). Anything above 1 still requires on_overlap = "queue". See Missed ticks.
  • notify.coalesce_outbound is gone; coalesce_window = "0s" now disables outbound coalescing (one message per event). The bell still coalesces on its default window.
  • [notify] keep_occurrences is renamed to coalesce_limit. It also controls how often a suppressed event is force-forwarded to outbound channels, which is now documented.
  • graceful_stop = "0s" and retry_delay = "0s" are now honored literally instead of silently falling back to the defaults — so graceful_stop = "0s" means kill immediately and retry_delay = "0s" means retry with no delay.
  • stop_signal now takes only the canonical SIGxxx spelling; bare names like TERM are rejected. Use SIGTERM, SIGINT, etc.
  • Setting [daemon] metrics_listen now enables metrics on its own — you no longer also need metrics_enabled = true.
  • [notifiers.*] now rejects fields that don’t belong to the notifier’s type (e.g. host/port on a type = "slack" block) instead of silently ignoring them.
  • GET /api/tasks/{taskName}/runs has been removed. Use GET /api/runs?taskName=<name>, which returns the same result.

Fixed

  • A daemon crash no longer leaves an attached TUI’s terminal filled with garbled escape sequences. A panic in a daemon goroutine is now turned into a clean shutdown that restores the terminal, and runwisp cloud runs its daemon as a separate process so a crash can never corrupt the terminal the TUI owns.
  • Config validation errors for [services.*] entries now say “service” instead of mislabeling the entry as a “task”.
  • The dashboard System resources chart now backfills its history on load instead of only drawing new samples as they stream in.
  • An ad-hoc dispatch request can no longer trigger a [services.*] entry, which could have reserved it an extra instance outside its restart policy — it only checked manual_trigger (always true internally for services), the same gate the REST/UI/CLI trigger paths already close with a service-kind check.
  • A remote service:remove request could delete a TOML-defined [services.*] entry, desyncing the running task set from runwisp.toml with no way back short of a daemon restart. It now only removes services that were remotely declared in the first place.
  • runwisp run --standalone now honors manual_trigger = false and refuses to run a [services.*] entry, matching the guard the daemon already enforces.
  • tls = "off" is now rejected when tls_cert/tls_key are also set, instead of being silently overridden into HTTPS.
  • The failure badge and “Failed” filter could go stale for a run updated live over SSE, since the push validation was silently dropping the run’s failure classification. Fixed by validating the full run shape.
  • A daemon booted with runwisp cloud never resolved a run left pending by a prior crash. Only standalone boot reconciled crash-orphaned pending runs; a cloud-mode restart now marks them interrupted like every other boot path.
  • A transient database error while marking crashed runs at boot is now retried a few times before giving up, instead of silently skipping crash recovery for that boot.
  • Cron jitter for a task without its own timezone was placed using the host OS’s timezone instead of [daemon] timezone. The two now agree, so jittered tasks land on the intended point of the schedule.
  • Run filters and retention cutoffs compared timestamps as text instead of as time. A run created in a timezone other than the daemon’s own could sort or filter incorrectly around a createdAfter/createdBefore boundary, or near the host’s own DST transition. Timestamps are now normalized to UTC before being stored or compared.
  • Retention pruning a run now publishes run.deleted, so an open dashboard reflects the deletion live instead of only after a refresh.
  • GET /api/runs/{runId}/log/raw no longer buffers the whole log in memory before responding, closing off a memory-exhaustion risk on very large logs.
  • Daemon shutdown now closes open SSE connections (/api/events/stream and the log streams) instead of leaving their handlers running past the shutdown deadline while other subsystems tear down.
  • runwisp import supervisord could write an invalid stop_signal (e.g. from stopsignal=CONT) with no warning, instead of flagging it like every other field it can’t map.
  • A run rejected for a full queue (queue_full) no longer offers a “Rerun” action in the TUI. Like a skipped or missed run, it was never actually executed.
  • The TUI now shows an error when a service stop or restart is rejected, instead of silently dropping it into the debug log.
  • Status labels for underscore end reasons (e.g. log_overflow) rendered as one unbroken word. They’re now humanized the same as every other status.

Security

  • Secret values (secrets / secrets_file) are now redacted from a run’s captured output before it’s persisted, streamed over the API, or pushed to the control plane, swapping the literal value for [redacted]. Best-effort: a secret the process transforms or splits across lines before printing can still slip through.
All releases