bash sleep 300 pattern that locks the screen for 5 minutes, the agent emits a single @park tool call that:
- Snapshots the loop state (history, counters, mode) to disk.
- Frees the terminal — you can chat, list jobs, open another
/coder. - Schedules the resume on the durable scheduler (survives crash/restart).
- Auto-resumes by itself once the timer/poll completes, without you pressing Enter.
Available since chatcli 1.111.x (PR #879). Works in
/coder, /agent and /run modes. Auto-resume uses TIOCSTI on Unix and WriteConsoleInputW on Windows; transparent fallback when the OS restricts the injection.Flow overview
Why this is needed
Before park, waiting inside a/coder meant:
- Terminal blocked for the full 300 s.
- Each
bashcall burns a turn of the agent’s turn budget (default 100). - CLI crash = lose the sleep and the state.
- No audit trail — only in shell history.
@park:
- Terminal freed immediately; you keep using the CLI.
- Park takes a single turn regardless of duration (10 s or 14 days).
- Crash-safe — disk snapshot + scheduler WAL replay on boot.
- Full audit via
/jobs logsand/parked.
Four modes of @park
- delay
- until
- for_url
- for_cmd
Fixed timer. Single-shot. Ideal for “wait before checking again”.
| Field | Type | Required | Description |
|---|---|---|---|
duration | string | ✅ | Go duration: 30s, 5m, 1h. Max 14 days. |
note | string | — | Human label shown in /parked. |
success_when matchers
Free-form DSL. Empty assumes “default success” (HTTP 2xx or exit 0).
| Form | Example | Meaning |
|---|---|---|
status=N | status=200 | Exact HTTP status |
status=lo..hi | status=200..299 | HTTP status in range |
exit=N | exit=0 | Exact shell exit code |
body contains:<str> | body contains:completed | Substring on body/stdout |
body matches:<re> | body matches:^OK$ | Regex (Go regexp) on body/stdout |
body matches: if you need logic.
Management commands
- /parked
- /resume
- /cancel-park
Lists all on-disk parks with cross-checked scheduler job status.Subcommands:
| Command | Description |
|---|---|
/parked | List (default) |
/parked prune | Remove snapshots whose scheduler job is in a terminal state (completed/failed/cancelled/timed_out) — cleanup after resume |
/parked gc <duration> | Remove snapshots older than <duration> regardless of status (e.g. /parked gc 24h) |
/parked help | Show usage |
Auto-resume — how the terminal “wakes up by itself”
This is the part that distinguishes park from a plain scheduled task: when the wait completes, the agent returns to foreground without you doing anything.Why TIOCSTI
TIOCSTI is a POSIX ioctl that injects bytes into the TTY’s input buffer as if the user had typed them. It works with any application reading stdin from the controlling tty — no need to modify go-prompt.
Why two bursts (body + 15 ms + \r)
go-prompt v0.2.6 usesbytes.Equal to classify keys (input.go:24). A multi-byte buffer like /resume abc\r doesn’t match any sequence in the ASCII table and falls into the default branch which inserts as text — including the trailing \r, which becomes literal and never submits. Solution: split.
- Command body in one burst (multi-byte → text insertion).
- 15 ms pause (above
readBuffer’s 10 ms poll cycle). - Lone
\rin a second burst (single byte → matches ControlM → submits).
Windows uses WriteConsoleInputW
No TIOCSTI on Windows — kernel32.dll exposesWriteConsoleInputW which accepts structured INPUT_RECORD events. Each char becomes a key-down/key-up pair; the trailing Enter uses VirtualKeyCode=VK_RETURN so go-prompt’s reader classifies it as a native Enter.
Platform support matrix
- Linux
- macOS
- Windows
- BSDs
TIOCSTI gated by
To re-enable (root):Trade-off: re-enables a feature distros disabled because of CVE-2017-5226 (sandbox escape via injection). Safe in personal dev environments; on shared servers, prefer the fallback.
/proc/sys/dev/tty/legacy_tiocsti:| Kernel | Default | Auto-resume |
|---|---|---|
| Pre-5.16 | TIOCSTI always enabled | ✅ works |
| 5.16+ server (Ubuntu LTS, RHEL) | legacy_tiocsti=0 | ✅ works |
| 6.x+ desktop, Docker Desktop linuxkit | legacy_tiocsti=1 | ❌ EPERM, fallback active |
Fallback when TIOCSTI/WriteConsoleInput is unavailable
When the injection is rejected, the🔔 park ready banner still shows up and the token enters the pendingResumeQueue. You need to type any character + Enter at the prompt — the executor consumes the queue before processing your input. Equivalent UX with one extra keystroke.
The prompt prefix shows [🅿️ resume ready: N] ❯ while a resume is pending, so it’s hard to forget.
Real-world examples
GitHub Actions CI
/coder to refactor tests in parallel. ~15 minutes later:
Slow terraform apply
terraform plan -detailed-exitcode returns 0 on no diff, 2 on diff present, 1 on error. Here we wait for 0 (convergence). To wait for “diff applied”, swap to success_when:exit=2.
Off-peak deploy window
Post-rollout health check
Security model
Approving @park = approving the polling
When the agent emits@park for_cmd cmd="echo done", /coder shows the security check with the full args, including the embedded cmd:
[y], you are pre-authorizing the polling shell to run that specific cmd you just saw. ChatCLI propagates DangerousConfirmed=true on the scheduler job, so the poll’s fire-time recheck does not stumble on ShellPolicyAsk (no human at the keyboard at fire time to approve again).
Snapshots are 0o600
~/.chatcli/parked/<token>.json contains the park’s full chat history. Files are created with 0o600 (owner-only) and the directory with 0o700. Snapshots never leak to other users on the host.
Token cannot path-traverse
Tokens are generated withcrypto/rand (16 bytes hex = 32 chars) and validated against regex [a-zA-Z0-9._-]{8,128}. There is no way for /resume ../etc/passwd to escape the directory.
Environment variables
| Variable | Default | Description |
|---|---|---|
CHATCLI_PARK_DIR | $XDG_CONFIG_HOME/chatcli/parked | Override of the snapshot directory — useful for tests |
Internals — for hackers
Snapshot format
JSON serialized withjson.MarshalIndent. Versioned schema (SchemaVersion = 1). Main fields:
pending_tool_call_id is the Anthropic native tool_use ID — preserved to reconstruct the tool_use/tool_result pairing on resume (otherwise the next API request rejects with unmatched tool_call).
Scheduler action types
Park introduces 2 action types:| Type | Payload | Triggers |
|---|---|---|
agent_resume | {resume_token, outcome, detail} | Bridge.NotifyParkComplete → drainPendingResumes → RunResumed |
park_poll | {resume_token, mode, url|cmd, interval, deadline_unix, success_when, ...} | Probe → matched? AgentResume : reschedule self |
park_poll self-reschedules every interval until match or deadline elapses. Crash-safe via WAL replay — an interrupted iteration resumes on boot.
/resume idempotency
Auto-resume injects/resume <token>\r via TIOCSTI. But the executor already ran the resume on the first line (drainPendingResumes), so when the /resume <token> command reaches the handler the snapshot was already deleted.
Solution: markRecentlyResumed(token) on drain (TTL 30 s) and wasRecentlyResumed(token) in handleResumeCommand → silent no-op. Genuinely invalid tokens (user typos) still surface as errors because the TTL is short.
Troubleshooting
Auto-resume doesn't fire — I see the 🔔 banner but nothing happens
Auto-resume doesn't fire — I see the 🔔 banner but nothing happens
park: no snapshot matching '<token>'
park: no snapshot matching '<token>'
You’re using the job ID (second column of
/parked) instead of the token (first column). Tokens have 8 visible chars in /parked; job IDs are scheduler-internal.Always copy from the first column of /parked or use auto-complete (Tab).Park stuck in (failed) on /parked
Park stuck in (failed) on /parked
Check
/jobs show <job_id>. Common causes:echo done(or any cmd) classified Ask without DangerousConfirmed: should be propagated automatically; report as a bug.- Persistent HTTP 5xx in for_url: each poll fails, scheduler may mark the job as failed after N retries. Increase
intervalordeadline. - Denylisted shell command: a
Denyrule in the coder policy always wins, even with approval. See/config security rules.
Snapshots accumulating in ~/.chatcli/parked/
Snapshots accumulating in ~/.chatcli/parked/
Use
/parked prune to remove snapshots whose job is terminal (completed/failed/cancelled/timed_out). On long-running systems, consider periodic /parked gc 24h.How do I tell which controlling TTY receives the injection?
How do I tell which controlling TTY receives the injection?
/dev/pts/N (Linux) or /dev/ttysNNN (macOS). That’s the fd injectTTYLine opens via /dev/tty.Quick reference
@coder exec, see Coder Security.