Blocked-Work Release Monitor

mandatory

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:

  1. Lock de outra sessão em meta/context/notices/active/lock-<slug>.md no

    componente 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 do concurrent-lock.md §4: armar o monitor é parte do caminho "esperar/abortar", não substitui a pergunta ao usuário.

  2. 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-after cobre 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.khost1 60s; nunca <30s (cache de prompt

    + rate-limit do sshd). Não usar sleep em 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 num meta/context/handoff/release-pings.log local — nunca silencioso.

  • Para processo longo no s.khost1, ALÉM do watcher laptop, lançar um watcher

    destacado no próprio s.khost1 (setsid nohup ... </dev/null &) que faz o mesmo poll e dispara o koder-notify quando 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 deadline vence), o watcher

    sai 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:

    1. Re-avaliar a saúde do bloqueio. Lock: o lock-<slug>.md ainda 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 §4 opção b — take-over). Processo: o PID ainda avança (log cresce, CPU>0) ou está hung/zumbi?

    2. 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.

    3. 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.

    4. 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.

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 lock

    sumiu? 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 until ancora 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 deadline com ⏰ EXPIROU se a

    condiçã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.