Blocked-Work Release Monitor
Sempre que Claude (qualquer IA da Stack) concluir que NÃO pode prosseguir numa atividade porque (a) o componente que precisa editar está com lock ativo de outra sessão, ou (b) um teste/build/processo pesado cujo resultado ou cujo recurso ela precisa está rodando — seja da própria sessão, seja de outra IA — ela DEVE, antes de ceder o turno, armar automaticamente um monitor de liberação. O monitor é híbrido: um watcher laptop-side que RE-ACIONA a sessão quando a condição limpa, MAIS um ping durável via koder-notify que alcança o operador mesmo se a sessão tiver encerrado antes da liberação.
Quando uma sessão fica bloqueada por uma condição que vai se resolver sozinha (um lock que outra sessão vai arquivar; um teste/processo que vai terminar), parar sem deixar um gatilho de retomada desperdiça a janela: o trabalho fica esquecido até alguém checar na mão. Esta policy torna o armar-monitor um reflexo automático — nunca perguntar antes, sempre armar.
Quando dispara (R1)
A sessão DEVE armar o monitor sempre que, para prosseguir numa atividade concreta, depender de uma das condições abaixo e decidir esperar em vez de seguir por outro caminho:
- Lock de outra sessão em
meta/context/notices/active/lock-<slug>.mdnocomponente que ela precisa editar o código (não cobre carve-out: se a ação for collision-free — inserir ticket novo etc. — faça-a agora, não espere;
lock-carve-out.kmd). Vale depois do prompt de conflito doconcurrent-lock.md §4: armar o monitor é parte do caminho "esperar/abortar", não substitui a pergunta ao usuário. - Testebuildregressãosoakemulador/processo pesado em execução cujo
resultado ou cujo recurso (VM, porta, registry, binário) a sessão precisa — independente do dono: processo da própria sessão OU de outra IA. Tipico no
s.khost1(heavy-work-isolation.kmd), mas vale local também.
Não dispara quando o bloqueio é resolvível agora (carve-out, outro caminho de trabalho, outro módulo livre) — nesse caso, prossiga; o monitor é para quando a decisão real é esperar.
O que armar — monitor híbrido (R2 + R3)
Wait-state compartilhado: ao armar o monitor, dropar também um marker
koder-job(~/.claude/hooks/koder-job.sh start "<label>" [--notify-after 10m]) — a status line mostra o LED do wait e o--notify-aftercobre o ping R3 de job longo. Uma fonte de verdade, duas superfícies (meta/context#742).
R2 — Watcher laptop-side (auto-reinvoke) — SEMPRE
Um Bash com run_in_background: true rodando um until-loop que sai quando a condição limpa. A saída faz o harness re-invocar a sessão automaticamente (o turno volta sozinho) — foi assim que o monitor de lock funcionou em 2026-06-02. Padrões:
Todo watcher tem prazo de expiração (R5): o loop sai tanto quando a condição limpa quanto quando o deadline vence, com mensagens distintas, pra nunca vigiar para sempre um lock/processo que travou.
- Lock (arquivo local; poll 30s; TTL default 1h):
TTL=${TTL:-3600}; deadline=$(( $(date +%s) + TTL )) while [ -f ~/dev/koder/meta/context/notices/active/lock-<slug>.md ]; do if [ "$(date +%s)" -ge "$deadline" ]; then ~/dev/koder/meta/context/bin/notify-release.sh "⏰ monitor lock <slug> EXPIROU (ainda travado)" || true echo "⏰ EXPIROU: lock <slug> ainda ativo após $((TTL/60))min — renovar prazo ou remover monitor?"; exit 0 fi sleep 30 done ~/dev/koder/meta/context/bin/notify-release.sh "lock <slug> liberado" || true echo "✅ LIBERADO: lock <slug> em $(date '+%H:%M:%S') — retomar <atividade>" - Processo no s.khost1 (poll 60s — evita martelar o sshd, que reseta com
reconexão rápida; ancore num PIDpatternmarcador de log estável; TTL à espera esperada — p.ex. soak de 18h ⇒ TTL > 18h):
TTL=${TTL:-3600}; deadline=$(( $(date +%s) + TTL )) while ssh s.khost1 'pgrep -f "<pattern-do-processo>" >/dev/null'; do if [ "$(date +%s)" -ge "$deadline" ]; then ~/dev/koder/meta/context/bin/notify-release.sh "⏰ monitor <pattern> EXPIROU (ainda rodando)" || true echo "⏰ EXPIROU: <pattern> ainda rodando no s.khost1 após $((TTL/60))min — renovar ou remover?"; exit 0 fi sleep 60 done ~/dev/koder/meta/context/bin/notify-release.sh "processo <pattern> terminou no s.khost1" || true echo "✅ TERMINOU: <pattern> no s.khost1 em $(date '+%H:%M:%S') — retomar <atividade>"
Regras do watcher:
- Cobertura, não só happy-path: para processo, o loop deve sair tanto no
término normal quanto na morte/erro (sair quando o PID some cobre os dois). Para condições com estado terminal (PASSFAILSTALLED num log), poll o marcador e emita o estado, não só "sumiu".
- Cadência: lock/local 30s;
ssh s.khost160s; nunca <30s (cache de prompt+ rate-limit do sshd). Não usar
sleepem foreground (bloqueado pelo harness); érun_in_background. - Um por condição: não armar duplicatas para o mesmo lock/processo.
reason/descrição específica: "lock slug" / "soak LEASE-005 no s.khost1",não "aguardando".
R3 — Ping durável via koder-notify (fallback) — SEMPRE
O watcher, ao sair, e (para processos longos no s.khost1) um watcher destacado server-side, disparam um alerta via koder-notify (infra/observe/notify, 10.0.1.27:7950, key em credentials/koder-notify-static-key.txt) no canal do operador (Koder Chat — canal kchat do FLOW-215). Isso garante que a liberação alcança o operador mesmo se a sessão Claude tiver encerrado antes da condição limpar (o auto-reinvoke do R2 só vale com a sessão viva).
- O helper
meta/context/bin/notify-release.sh "<msg>"encapsula o POST(fail-soft:
|| true, nunca quebra o watcher). Se o canal do operador ainda não existir (FLOW-215 pendente), o helper degrada para append nummeta/context/handoff/release-pings.loglocal — nunca silencioso. - Para processo longo no
s.khost1, ALÉM do watcher laptop, lançar um watcherdestacado no próprio s.khost1 (
setsid nohup ... </dev/null &) que faz o mesmo poll e dispara okoder-notifyquando terminar — sobrevive ao fim da sessão laptop. Locks são arquivos locais do laptop: o s.khost1 não os enxerga, então para lock a durabilidade vem só do ping-on-exit do watcher laptop (R2), que dispara o koder-notify no momento da saída.
Prazo de expiração e renovação (R5)
Todo monitor nasce com um prazo de expiração (TTL). Sem isso, um lock abandonado ou um processo travado seria vigiado para sempre. O TTL é a cadência de re-avaliação, não a duração esperada da espera:
- Default 1h. Para esperas sabidamente longas (soak de 18h, build de horas),
setar o TTL acima da duração esperada (
TTL=$((19*3600))), pra não expirar no meio à toa. Para espera de duração desconhecida (lock de outra sessão), 1h é o ponto de re-checagem. - Ao expirar (condição ainda ativa quando o
deadlinevence), o watchersai com a mensagem
⏰ EXPIROU(R2) — o harness re-invoca a sessão e dispara o ping durável (R3). Claude então decide entre renovar ou remover:- Re-avaliar a saúde do bloqueio. Lock: o
lock-<slug>.mdainda existe?Há quanto tempo (mtime)? A sessão dona ainda dá sinal de vida (commits recentes, atualizou o lock)? Um lock muito antigo sem atividade = provável stale (
concurrent-lock §4opção b — take-over). Processo: o PID ainda avança (log cresce, CPU>0) ou está hung/zumbi? - Renovar se o bloqueio está legitimamente ativo e progredindo: re-armar
o watcher (R2+R3) com um TTL novo (igual ou maior) e ceder o turno de novo.
- Remover se: o bloqueio travou (lock stale → propor take-over ao usuário;
processo hung → propor matar/triar), OU a atividade não precisa mais do recurso, OU o trabalho foi feito por outro caminho. Remover = não re-armar; anunciar por que (lock stale / processo hung / não mais necessário) e seguir.
- Ambiguidade real (não dá pra dizer se está progredindo ou travado) →
perguntar ao usuário (renovar vs investigar/remover), com a evidência coletada no passo 1. Não renovar cegamente em loop infinito.
- Re-avaliar a saúde do bloqueio. Lock: o
Ao ser re-invocado (R4)
Quando o watcher sai e o harness re-invoca a sessão, Claude lê qual ramo disparou:
✅ LIBERADO/✅ TERMINOU→ re-checa a condição de fato (o arquivo de locksumiu? o processo saiu com que estado — PASSFAILSTALLED?) e retoma a atividade bloqueada. Se a condição reapareceu (lock re-locado por terceira sessão; processo re-lançado), re-armar o monitor (R2+R3) e ceder de novo.
⏰ EXPIROU→ seguir o R5 (renovar ou remover).
Não tratar o re-invoke como input do usuário.
Por que (T1)
- T1.1 Antes de ceder o turno por lock/processo, existe um monitor armado
cobrindo aquela condição (watcher laptop + ping durável). Verificável: a resposta que cede o turno contém a chamada
run_in_background+ a linha de ping. - T1.2 O monitor sai e re-aciona/alerta no término real da condição (não em
timeout cego nem nunca). Verificável: o
untilancora na condição, não num relógio fixo. - T1.3 Nunca pergunta "quer que eu monitore?" — arma direto (a pergunta
legítima é a do
concurrent-lock §4: esperarabortarignorar; o monitor entra no ramo "esperar"). - T1.4 Todo monitor tem TTL: o loop sai no
deadlinecom⏰ EXPIROUse acondição não limpou. Verificável: o snippet tem o ramo
date +%s >= deadline. - T1.5 Na expiração, Claude re-avalia a saúde do bloqueio (mtime do lock /
liveness da sessão dona / avanço do processo) e renova OU remove com justificativa — nunca renova cegamente em loop nem deixa o monitor morrer silenciosamente. Ambiguidade real → pergunta ao usuário com a evidência.