Run and test
Expiry and the local teardown runner
How a session deadline becomes a destroyed server: the local runner, extending and renewing a session, and reading a failed scheduled teardown.
Reviewed 2026-10-04
session.expiresAfterMinutes is a deadline for a demo or test environment. It
does not make Vercel or the server delete anything: neither ever receives a
provider write credential, and Vercel cannot use the local Pulumi state of the
machine that created the server. Something on that machine has to act when the
deadline passes. This page covers that runner, how to extend or renew a
session, and how to read a failed scheduled teardown.
For keeping a server past the work on purpose, see Warm boxes and reuse. The normal sink is in Lifecycle.
The commands assume export DEMO_CONFIG="/path/to/project/chalupa.yml".
The local teardown runner
Registration is explicit and happens only after the exact compute stack exists:
chalupa teardown register --config "$DEMO_CONFIG"
Namespaced actions keep their exact Taskfile name in the allowlist; the CLI
accepts the group and the member as two words, so chalupa teardown register
delegates to the teardown:register action.
The runner stores an operator-private local record containing the canonical config
path, SHA-256 of that config, the compute stack name, its current provider ID,
and its deadline. It rejects names ending in -data, never enumerates provider
resources, and never receives a data-stack name. Before acting, it rechecks the
canonical config path, stack name and provider ID. An expired registration
still tears down after YAML edits when the path and stack name match; the log
notes the changed file. The deadline was authorized at registration, so editing
the file cannot cancel it. ci.onFinish still requires the exact registered
file digest. A changed path or stack name, or an unreadable config, blocks
teardown and keeps the registration for recovery. A newer deployment is never
destroyed by the old registration; that stale registration is removed.
Review due work without changing anything with the teardown:run target, and
destroy compute only with the explicitly armed teardown:run-armed target.
Neither has a chalupa verb yet; both are in
Advanced.
The armed command delegates to the normal chalupa down path. The provider
wrapper acquires the DigitalOcean credential only in its short-lived local
subprocess, checks the registered provider ID immediately before destruction,
and leaves <stack>-data untouched. It also preserves the normal bounded log
and telemetry flush plus signed sunk event behavior.
On macOS, Chalupa can install a user-level launchd job after you have reviewed the dry run. First start a same-user TinyVault agent that remains unlocked for the whole scheduled horizon. The agent runs in the foreground, so keep its terminal or trusted supervisor alive:
tvault agent start --idle 0
Then, in another terminal, install the schedule and check it:
SSH_IDENTITY="$HOME/.ssh/id_ed25519_chalupa" chalupa teardown schedule:install --config "$DEMO_CONFIG"
chalupa teardown schedule:doctor --config "$DEMO_CONFIG"
The default interval is five minutes. To choose another bounded interval, set
TEARDOWN_INTERVAL_SECONDS=600 in the environment of the install command. The
teardown:schedule:status target prints the installed job (see
Advanced).
Installation is explicit and local. Before writing anything, the installer
requires the same-user agent to report idle_remaining_seconds: 0 and proves
that it can read exactly DIGITALOCEAN_API_TOKEN from the direct chalupa
project without prompts. That probe discards both credential output and
diagnostics and does not inherit TVAULT_PASSPHRASE, TVAULT_IDENTITY,
TVAULT_IDENTITY_KEY, or TVAULT_AGENT_TOKEN. The generated job contains no
TinyVault unlock material, private-key material, provider credential, or target
stack.
The job records the absolute Task binary, the Taskfile, the current executable
search path, and the already resolved TEARDOWN_STATE_DIR—whether the path was
explicit, XDG-derived, or the default. When provided, it also retains only the
validated local SSH_IDENTITY path so a non-interactive final flush can use the
dedicated key. At each interval it runs the armed Taskfile path, which still
rechecks every registration and obtains a provider credential only if an exact,
expired compute deployment remains eligible. The installer refuses to replace
a different job at the same path.
Inspect schedule.stdout.log and schedule.stderr.log in the resolved state
directory after installation and after the next deadline. That is the explicit
TEARDOWN_STATE_DIR, $XDG_STATE_HOME/chalupa/teardown, or
~/.local/state/chalupa/teardown, in that order. The schedule runs only while
that user's launchd domain is available, so it is a best-effort cost guard, not
a real-time destruction guarantee. The TinyVault agent must also remain alive;
if it exits, a due run cannot acquire the credential, no remote shutdown flush
starts, and the scheduler logs the failure for operator recovery. Once the
credential is acquired, log and Monitor flushes run in separate subprocesses
that cannot inherit it; the provider-bound parent rechecks compute identity and
performs the destroy. Remove and reinstall the schedule to change its interval,
state directory, TinyVault directory, Taskfile, Task/Bun path, or SSH
identity—the installed job pins all of those values.
Disable future scheduled runs without deleting registrations or logs with the
teardown:schedule:remove target (see
Advanced). Removal disables the label before inspecting it. If launchd still reports a
running PID or state, Chalupa leaves the job file in place and asks you to retry
after that run exits; only an idle job is unloaded and removed. --idle 0
keeps TinyVault's key-encryption key in memory for the life of the agent, and
other same-user processes that can reach its protected socket share that trust
boundary. Use it only on the trusted operator machine, and stop it after the
schedule is removed and the teardown:run review confirms that no registered compute
remains:
tvault agent stop
Linux and other platforms continue to require an operator-owned user timer or
cron entry that invokes the armed teardown:run-armed target. Keep it on the
same trusted machine that owns the local Pulumi backend and preserve the same
resolved state-directory setting. That timer also needs an equivalently
long-lived, same-user, non-interactive TinyVault agent; its process lifetime and
the timer service availability impose the same best-effort limit. The registry
can be retained for audit or removed only after confirming no registered
compute remains.
Extend or renew a session
Extending a running session is an explicit lifecycle act:
chalupa session extend --hours 2 --config ./chalupa.yml
# Non-interactive authorization:
chalupa session extend --hours 2 --config ./chalupa.yml --confirm "extend <environment>"
Before confirmation, Chalupa prints the new UTC deadline and the estimated added
provider cost using the running host's hourly rate (or says the cost is unknown).
The provider invoice wins. Extending is never a Chalupa charge.
Each extension adds 1–8 whole hours to the current deadline, with at most two
extensions per registration by default (session.maxExtensions, up to 12). Total lifetime, counted from original registration,
is capped at 12 hours for inference and 24 hours otherwise; set
session.maxLifetimeHours (1–168) to choose a different cap. Expired sessions cannot
be extended. The local registration and the console's session clock both receive
the new deadline through a signed session-extended lifecycle event.
An expired session whose host is still running (for example, because a scheduled teardown failed) can be renewed instead:
chalupa session renew --config ./chalupa.yml
# Non-interactive authorization:
chalupa session renew --config ./chalupa.yml --confirm "renew <environment>"
Renewal is billable and confirmed separately from extension. It refuses a session
that has not expired yet (use extend), a config that changed since registration,
and a deployment other than the registered one. Right before writing, it asks the
provider for the registered server: only a server the provider reports as running
(active on DigitalOcean, running on Hetzner) is renewed. An absent, stopped or
unverifiable server (no token, API unreachable) is refused. The new deadline is a
fresh session.expiresAfterMinutes from now, clamped to the total-lifetime cap
counted from the first registration, so renewals cannot keep a session alive
forever. When less than 15 minutes of the cap remain, renewal is refused. The
registration records how many times it was renewed. The console receives the new
absolute deadline through the same session-extended event, with its whole-hour
count rounded up for the timeline.
chalupa teardown register is idempotent for a live deployment: re-registering
an expired session keeps its old deadline and prints a warning pointing at
session renew.
Scheduled teardown failures
When the armed runner's chalupa down fails, the schedule log carries more than
down-failed. The lines under the decision, prefixed with |, name the stage
that failed (the local identity check or chalupa down), its exit code, and the last
20 lines of its stderr. Each line is limited in length, stripped of control
characters, and long token-like strings are redacted. chalupa teardown schedule:doctor shows them as the last failure detail.
The doctor also runs its checks in the scheduled job's own context. It reads the
installed launchd job's PATH, HOME and TVAULT_DIR, looks for go-task, bun,
pulumi and tvault on that PATH, and probes the TinyVault agent with the same
environment. A TinyVault that answers in your shell but not under launchd shows up
here. A missing tool makes the doctor exit non-zero; reinstall the schedule from a
shell whose PATH has every tool.
If the provider no longer lists the server (it was deleted in the provider console
or by maintenance), down asks the provider about that exact id first. Only when
the answer is "absent" does it skip the log and telemetry flushes (they would
only time out) and refresh the compute stack's state with pulumi refresh before
pulumi destroy. Refresh only rewrites local state; it creates and deletes
nothing. If the provider check fails or there is no token, down behaves as
before.
If console delivery fails, the local extension still stands, but the console will
stop the host at the original deadline unless reporting is retried. Run the printed
retry command; it resends the same deadline without consuming another extension.
Idle shutdown and ci.onFinish remain separate stop conditions.