bash sleep 300 que bloqueia 5 minutos a tela inteira, o agent emite um único tool call @park que:
- Snapshota o estado do loop (history, counters, modo) em disco.
- Libera o terminal — você pode chatar, listar jobs, abrir outro
/coder. - Agenda o resume no scheduler durável (sobrevive a crash/restart).
- Auto-retoma sozinho quando o timer/polling completa, sem você apertar Enter.
Disponível desde chatcli 1.111.x (PR #879). Funciona em modo
/coder, /agent e /run. Auto-resume via TIOCSTI no Unix e WriteConsoleInputW no Windows; fallback transparente quando o sistema operacional restringe a injeção.Visão geral do fluxo
Por que isso é necessário
Antes do park, esperar dentro de um/coder significava:
- Terminal bloqueado os 300 s inteiros.
- Cada
bashconsome um turn do orçamento de turns do agent (default 100). - Crash do CLI = perde o sleep e o estado.
- Sem audit trail — só fica no histórico do shell.
@park:
- Terminal liberado imediatamente; você usa o CLI normalmente.
- Park ocupa um único turn, independente da duração (10 s ou 14 dias).
- Crash-safe — snapshot em disco + scheduler WAL replay no boot.
- Audit completo via
/jobs logse/parked.
Quatro modos do @park
- delay
- until
- for_url
- for_cmd
Timer fixo. Single-shot. Ideal para “espere antes de checar”.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
duration | string | ✅ | Go duration: 30s, 5m, 1h. Máximo 14 dias. |
note | string | — | Label humano mostrado no /parked. |
Matchers do success_when
DSL livre. Vazio assume “sucesso default” (HTTP 2xx ou exit 0).
| Forma | Exemplo | Significado |
|---|---|---|
status=N | status=200 | HTTP status exato |
status=lo..hi | status=200..299 | HTTP status no range |
exit=N | exit=0 | Shell exit code exato |
body contains:<str> | body contains:completed | Substring no body/stdout |
body matches:<re> | body matches:^OK$ | Regex (Go regexp) no body/stdout |
body matches: se precisar de lógica.
Comandos de gerenciamento
- /parked
- /resume
- /cancel-park
Lista todos os parks pendentes em disco com cross-check do scheduler job.Subcomandos:
| Comando | Descrição |
|---|---|
/parked | Lista (default) |
/parked prune | Remove snapshots cujo job está em estado terminal (completed/failed/cancelled/timed_out) — limpa após resume |
/parked gc <duration> | Remove snapshots mais antigos que <duration> independente de status (ex: /parked gc 24h) |
/parked help | Mostra usage |
Auto-resume — como o terminal “acorda sozinho”
Aqui está a parte que diferencia park de uma simples scheduled-task: quando o wait completa, o agent volta ao foreground sem você fazer nada.Por que TIOCSTI
TIOCSTI é um ioctl POSIX que injeta bytes no input buffer da TTY como se o usuário tivesse digitado. Funciona com qualquer aplicação que lê stdin no controlling tty — não precisa modificar o go-prompt.
Por que dois bursts (body + 15ms + \r)
go-prompt v0.2.6 usabytes.Equal para classificar keys (input.go:24). Um buffer multi-byte como /resume abc\r não casa com nenhuma sequência da tabela ASCII e cai no branch default que insere como texto — incluindo o \r final, que vira literal e nunca submete. Solução: split.
- Body do comando vai num burst (multi-byte → text insert).
- Pause de 15 ms (acima do poll de 10 ms do
readBuffer). \rsolo num segundo burst (1 byte → matcheia ControlM → submete).
Windows usa WriteConsoleInputW
Sem TIOCSTI no Windows — o kernel32.dll expõeWriteConsoleInputW que aceita INPUT_RECORD estruturados. Cada char vira um par key-down/key-up; o Enter final usa VirtualKeyCode=VK_RETURN para go-prompt classificar como Enter nativo.
Matriz de plataformas
- Linux
- macOS
- Windows
- BSDs
TIOCSTI com gate
Para reabilitar (root):Trade-off: re-habilita uma feature que distros desabilitaram por causa de CVE-2017-5226 (escape de sandboxes via injeção). Em ambiente dev pessoal é seguro; em servidores compartilhados, considere o fallback.
/proc/sys/dev/tty/legacy_tiocsti:| Kernel | Default | Auto-resume |
|---|---|---|
| Pre-5.16 | TIOCSTI sempre habilitado | ✅ funciona |
| 5.16+ servidor (Ubuntu LTS, RHEL) | legacy_tiocsti=0 | ✅ funciona |
| 6.x+ desktop, Docker Desktop linuxkit | legacy_tiocsti=1 | ❌ EPERM, fallback ativa |
Fallback quando TIOCSTI/WriteConsoleInput não disponível
Quando a injeção é rejeitada, o🔔 park ready ainda aparece e o token entra na pendingResumeQueue. Você precisa digitar qualquer caractere + Enter no prompt — o executor consome a queue antes de processar seu input. UX equivalente, com um clique extra.
O prompt prefix mostra [🅿️ resume ready: N] ❯ enquanto há resume pendente, então é difícil esquecer.
Exemplos práticos
CI do GitHub Actions
/coder pra refatorar testes em paralelo. ~15 minutos depois:
Terraform apply lento
terraform plan -detailed-exitcode retorna 0 se não há diff, 2 se há diff, 1 em erro. Aqui esperamos 0 (convergência). Para esperar “diff aplicado”, troque pra success_when:exit=2.
Janela de deploy noturna
Health check pós-rollout
Modelo de segurança
Aprovação no @park = aprovação do polling
Quando o agent emite@park for_cmd cmd="echo done", o /coder mostra a security check com os args completos, incluindo o cmd embedded:
[y], está pré-autorizando o polling shell rodar aquele cmd específico que você acabou de ver. O ChatCLI propaga DangerousConfirmed=true no scheduler job, então o poll fire não tropeça em ShellPolicyAsk (não há humano no fire-time pra aprovar de novo).
Snapshots têm permissão 0o600
~/.chatcli/parked/<token>.json contém o histórico de chat completo do park. Arquivos são criados com 0o600 (owner-only) e o diretório com 0o700. Snapshots nunca vazam pra outros usuários do host.
Token não permite path traversal
Tokens são gerados comcrypto/rand (16 bytes hex = 32 chars) e validados contra regex [a-zA-Z0-9._-]{8,128}. Não há como /resume ../etc/passwd escapar do diretório.
Variáveis de ambiente
| Variável | Default | Descrição |
|---|---|---|
CHATCLI_PARK_DIR | $XDG_CONFIG_HOME/chatcli/parked | Override do diretório de snapshots — útil em testes |
Internos — para quem quer hackear
Snapshot format
JSON serializado comjson.MarshalIndent. Schema versionado (SchemaVersion = 1). Campos principais:
pending_tool_call_id é o native tool_use ID da Anthropic — preservado pra reconstruir o pairing tool_use/tool_result no resume (caso contrário a próxima request à API rejeita por unmatched tool_call).
Action types do scheduler
Park introduz 2 action types:| Type | Payload | Dispara |
|---|---|---|
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 se auto-reschedula a cada interval até casar ou deadline elapsar. Crash-safe via WAL replay — uma iteração interrompida volta no boot.
Idempotência do /resume
O auto-resume injeta/resume <token>\r via TIOCSTI. Mas o executor já fez o resume na primeira linha (drainPendingResumes), então quando o /resume <token> chega no command handler o snapshot já foi deletado.
Solução: markRecentlyResumed(token) no drain (TTL 30s) e wasRecentlyResumed(token) no handleResumeCommand → no-op silencioso. Tokens realmente inválidos (typos do user) ainda surgem como erro porque o TTL é curto.
Troubleshooting
Auto-resume não dispara — vejo 🔔 banner mas nada acontece
Auto-resume não dispara — vejo 🔔 banner mas nada acontece
park: nenhum snapshot bate com '<token>'
park: nenhum snapshot bate com '<token>'
Você está usando o job ID (segunda coluna do
/parked) em vez do token (primeira coluna). Tokens têm 8 chars visíveis no /parked; job IDs são do scheduler interno.Cole sempre da primeira coluna do /parked ou use auto-complete (Tab).Park ficou em (failed) no /parked
Park ficou em (failed) no /parked
Veja
/jobs show <job_id>. Causas comuns:echo done(ou cmd qualquer) com policy Ask + sem DangerousConfirmed: deveria ser propagado automaticamente; reporte como bug.- HTTP 5xx persistente em for_url: cada poll falha, scheduler pode marcar job como failed depois de N retries. Aumente
intervaloudeadline. - Comando shell denylist: regra
Denyna coder policy bate sempre, mesmo com aprovação. Veja/config security rules.
Snapshot acumulando em ~/.chatcli/parked/
Snapshot acumulando em ~/.chatcli/parked/
Use
/parked prune para remover snapshots cujo job está terminal (completed/failed/cancelled/timed_out). Em sistemas longevos, considere /parked gc 24h periódico.Como saber qual é o controlling TTY que recebe a injeção?
Como saber qual é o controlling TTY que recebe a injeção?
/dev/pts/N (Linux) ou /dev/ttysNNN (macOS). É essa fd que injectTTYLine abre via /dev/tty.Reference rápida
@coder exec, veja Coder Security.