Pular para o conteúdo
MÓDULO 2.4

⏱️ Trabalho fora do dispatch

O session.start acende, o relógio mantém. Um hook tem 10 segundos por dispatch. O que precisa durar mais (vigiar um arquivo, atualizar um painel, rodar um programa) começa em outro lugar. Este módulo mostra onde, e o que o mod pode tocar fora do Claude.

6
Tópicos
~35
Minutos
Médio
Nível
Prático
Tipo
0 de 60%
1

Comece no session.start

O session.start dispara uma vez por mod quando a sessão fica pronta, antes do primeiro prompt, e o engine espera por ele. Tudo o que você registra ali já existe no turno 1: um comando, uma ferramenta para o modelo, um timer.

Ele dispara de novo quando o mod é ligado no meio da sessão e quando ele recarrega (só os módulos que mudaram). Não dispara no /clear: ali chega um session.end com reason: 'clear', e nenhum session.start depois.

🆕 Novo aqui? Fora do dispatch

Um dispatch é uma passagem de evento pela cadeia, com orçamento de 10 s (módulo 2.2). "Fora do dispatch" é o trabalho que não cabe nisso: começa num hook, mas segue rodando depois que o hook devolveu. Quem mantém esse trabalho vivo é o relógio ($.clock), não o hook.

📄 Do comentário de 'session.start' nos tipos
on("session.start", ($, e, next) => $.tool.register(t).then(() => next(e)))
O que olhar: a ferramenta é registrada e só depois o hook passa adiante. Como o primeiro session.start é aguardado, o modelo já vê essa ferramenta na primeira resposta. Ferramentas do mod são o assunto do módulo 3.4.
🎯 Objetivo: provar que o comando nasce no session.start

Na pasta do Claude Mods Starter Kit (da Prompt Advisers, licença MIT; o comando responde sem chamar o modelo):

claude -p "/readcount" --plugin-dir templates/starter-mod

Saída real (05/10/2026):

read-counter-example: Successful reads observed: 0
Como verificar: a resposta aparece mesmo sendo o primeiro (e único) prompt da sessão. O /readcount só existe porque o session.start do starter-mod chamou $.command.register antes desse prompt.
2

Agende com clock.every e clock.after

Dois timers. $.clock.every(ms, fn) chama fn a cada período. $.clock.after(ms, fn) chama uma vez. Os dois devolvem um objeto com cancel(). Eles rodam até você cancelar ou até o mod recarregar.

No reload os timers morrem junto com o ambiente antigo. Como o session.start dispara de novo para o módulo que mudou (segundo os tipos), quem liga o timer ali o religa sozinho. Esse é o motivo de não ligar timer em nenhum outro lugar.

📄 inema-mods/mods/painel-longrun/hooks/register.tsx (trecho)
export const register: Register = (on, options) => {
  const fixa = String(options.pasta ?? '').trim()
  const seg = Number(options.intervalo)
  const intervalo = (Number.isFinite(seg) && seg >= 5 ? seg : 30) * 1000

  on('session.start', async ($, e, next) => {
    await update($, cwdAtom, () => e.cwd)
    await $.command.register({
      name: 'longrun',
      description: 'Painel da execução longa (args: todas | atualizar)',
    })
    await atualizar($, fixa).catch(() => null)
    $.clock.every(intervalo, () => {
      void atualizar($, fixa).catch(() => null)
    })
    return next(e)
  })
O que olhar: o intervalo vem do /config e tem piso de 5 s. A primeira atualização é aguardada; as seguintes rodam com void, cada uma num dispatch de clock.every só seu. O .catch garante que uma falha num ciclo não derrube os próximos.
session.startliga o every tick · tick · tick reloadtimer morre session.startliga de novo tick · tick · tick

Como ler o desenho: os pontos são as chamadas de fn. Na linha amarela tudo para. O segundo bloco azul é o mesmo hook rodando outra vez, no ambiente novo, e por isso os ticks voltam.

⚠️ Confira o religamento numa sessão real

O guia do kit INEMA marca esse ponto como "não confirmado ao vivo" (pegadinha 17): os tipos dizem que o session.start volta no reload, um agente observou o contrário. Ao fazer um mod com timer, salve o arquivo com o mod carregado e veja se o painel continua atualizando. Outro uso do after: $.command.run dentro de um hook que o turno espera é recusado; dispare com $.clock.after(0, ...) (pegadinha 16).

3

Leia e escreva arquivos com $.fs

O mod não tem fs do Node. Tem o $.fs, que lê, escreve, lista e examina caminhos. Caminho relativo é relativo à pasta de trabalho da sessão. Quase todo método rejeita quando o arquivo não existe, então o .catch é parte da chamada.

O painel de execução longa do Rafa lê os arquivos de uma pasta longrun/ a cada ciclo. Repare que nenhuma leitura derruba o painel: faltou o arquivo, vira texto vazio ou lista vazia.

📄 inema-mods/mods/painel-longrun/hooks/register.tsx (trecho)
async function lerTexto($: EngineInterface, caminho: string) {
  return $.fs.read(caminho).catch(() => '')
}

/** A pasta da execução: a fixada nas opções, ou a mais recente (pelo nome) em <cwd>/longrun/. */
async function acharPasta($: EngineInterface, cwd: string, fixa: string): Promise<string | null> {
  const base = fixa !== '' ? fixa : cwd
  if (fixa !== '' && (await $.fs.exists(juntar(fixa, 'goal.md')).catch(() => false))) return fixa
  const dir = juntar(base, 'longrun')
  const itens = await $.fs.list(dir).catch(() => [])
  // ... filtra kind === 'dir' e escolhe a mais recente pelo nome ...
O que olhar: as duas funções recebem $ e estão no topo do arquivo. Cada item de $.fs.list traz name, kind, size, mtimeMs e isLink.
ChamadaDevolveDetalhe dos tipos
$.fs.read(p) o textorejeita se faltar ou passar de 4 MiB
$.fs.read(p, { as: 'bytes' }) { base64 }para arquivo binário
$.fs.write(p, texto) nadacria o arquivo e as pastas; troca o conteúdo inteiro
$.fs.list(p) as entradassem p, a pasta de trabalho
$.fs.stat(p, { resolve: true }) kind, size, mtimeMs, isLink e realPathrealPath com links e .. resolvidos

✓ Leitura que não derruba

  • ✓ .catch em toda chamada de $.fs
  • ✓ padrão claro quando falta: texto vazio, lista vazia
  • ✓ caminho relativo pensado a partir da pasta de trabalho

✗ Leitura frágil

  • ✗ supor que o arquivo existe
  • ✗ ler arquivo grande inteiro (passa de 4 MiB, rejeita)
  • ✗ guardar por lista de nomes proibidos sem realPath

💡 Guarda de caminho: lista do que pode, sobre realPath

Para um mod que barra escrita fora do projeto, a referência diz qual é a forma robusta: resolver o caminho com stat(..., { resolve: true }) e aceitar só o que fica dentro de uma raiz resolvida do mesmo jeito. Uma lista do que é proibido, comparando texto do caminho, é "melhor esforço": um link simbólico passa por ela.

4

Rode processos locais com $.process

$.process.run roda um programa da máquina, como o usuário da sessão, e devolve { exitCode, stdout, stderr } quando ele termina. O comando vai como lista de argumentos (argv), sem shell no meio.

Sem shell quer dizer: nada de |, && ou * interpretados, e nada de um texto do modelo virar comando por engano. Cada argumento chega ao programa exatamente como você escreveu.

🆕 Novo aqui? argv

O argv é a lista que um programa recebe ao iniciar: o nome do programa e depois cada argumento separado. ['git', 'status', '--short'] é o mesmo que digitar git status --short, mas sem o shell decidir onde cortar as palavras.

📄 inema-mods/mods/faixa-publicacao/hooks/register.tsx (trecho)
async function git($: EngineInterface, cwd: string, args: string[]) {
  const r = await $.process.run(['git', ...args], { cwd, timeoutMs: 5_000 }).catch(() => undefined)
  return r && r.exitCode === 0 ? r.stdout.trim() : ''
}
O que olhar: tempo-limite curto, .catch para o caso de o git nem existir, e saída vazia quando o código de saída não é 0. O mod nunca fica preso esperando um programa.
Opção ou limiteValor nos tipos
cwd, env, stdinopcionais; cwd padrão é o da sessão
timeoutMs30 s por padrão, 10 minutos no máximo
saídaaté 4 MiB por stream; isStdoutTruncated avisa o corte
quando rejeitanão conseguiu iniciar, ou ainda rodava no fim do prazo
gitroda com os hooks do repositório desligados

✓ Bom uso

  • ✓ Programas de leitura: git status, git log
  • ✓ Abrir uma pasta (o abridor do recibo, vindo do /config)
  • ✓ Saída longa e contínua: $.process.spawn, que é stream

✗ Evite

  • ✗ Montar um sh -c com texto vindo do modelo
  • ✗ Programa que fica rodando em segundo plano com run
  • ✗ Chamar algo que acessa a rede (regra do kit INEMA)
5

Acorde a sessão com prompt.submit

Um timer descobriu algo e o Claude está parado esperando. O mod pode acordar a sessão: $.prompt.submit({ text }) põe um prompt na fila, que vira um turno próprio assim que a sessão fica livre.

Ele nunca entra no meio de um turno que está rodando. E a promessa resolve quando o turno começa, não quando termina. Por isso os tipos usam void na frente: o mod pede e segue.

1

O mod chama $.prompt.submit

De um timer, de um comando ou de um botão. O prompt entra na fila.

2

Passa pela cadeia de prompt.submit

Os outros hooks veem e.origin como { kind: 'plugin', name }: sabem que veio de um mod.

3

A sessão fica livre e o turno começa

Aqui a promessa resolve. O modelo lê o texto como mensagem enviada pelo mod.

📄 Duas formas: o exemplo dos tipos e a alternativa que não gasta modelo
// do comentário de prompt.submit nos tipos: começa um turno (gasta modelo)
void $.prompt.submit({ text: "List the TODOs you just mentioned." })

// da referência (comando /quote): só preenche a caixa; a pessoa decide
await $.prompt.fill({ text: quoted + '\n\n', mode: 'insert' })
O que olhar: o submit dispara um turno sozinho. O fill só escreve na caixa do prompt e espera o Enter da pessoa. No kit INEMA, a segunda é o padrão: o mod sugere, a pessoa decide gastar.

⚠️ Timer + submit = gasto sem ninguém olhando

Um $.clock.every que chama $.prompt.submit faz o Claude trabalhar sozinho, em laço, enquanto a pessoa está fora. Se usar, ponha um limite claro, mostre na tela que está ligado e deixe um comando para desligar.

6

Decida se o mod pode gastar modelo

O $ tem um noun model. $.model.complete({ model, prompt }) faz uma pergunta avulsa, sem histórico. $.model.fork({ prompt }) pergunta sobre a própria conversa, aproveitando o cache. As duas usam o cliente da sessão: gastam a cota da pessoa.

Elas sempre resolvem com um resultado, nunca com texto solto: isAnswered com text e usage, ou isAnswered: false com um reason (api-error, empty-reply, aborted; no fork, também nothing-to-fork).

ChamadaGasta cota?Kit INEMA
$.model.complete, $.model.fork simproibido
$.http.fetch rede, pode ser serviço pagoproibido
$.prompt.submit sim, um turno inteirosó a pedido da pessoa
$.prompt.fill, $.fs, $.process local, $.uinãoliberado
Precisa do modelo? não sim $.fs · $.process · $.uisem gasto: o normal de um mod A pessoa pediu agora? não sim sugira com prompt.fill comando dela → prompt.submit model.complete / fork: fora do kit INEMA

Como ler o desenho: comece em cima. A maior parte dos mods para no ramo da esquerda. Gastar modelo só aparece quando a pessoa pediu, e mesmo assim por um caminho que ela vê. A linha vermelha é a regra do kit INEMA: um mod nunca chama o modelo por conta própria.

🎯 Objetivo: ver o que um mod chama antes de ligar

Na pasta do Claude Mods Starter Kit:

claude plugin validate templates/starter-mod

Saída real (05/10/2026):

Validating plugin manifest: .../templates/starter-mod/.claude-plugin/plugin.json

Validating hooks: .../templates/starter-mod/hooks/hooks.json

  ❯ ./starter.mjs hooks: session.start, tool.call{tool=Read}, command.run{command=readcount}
  ❯ ./starter.mjs calls: $.command.register

✔ Validation passed
Como verificar: a linha calls: lista tudo o que o mod pede ao engine. Aqui só aparece $.command.register: nada de model, http ou prompt.submit. Rode o mesmo comando na pasta de qualquer mod de terceiro antes de instalar.

Teste rápido (opcional): o Rafa quer que um painel releia um arquivo a cada 30 segundos, a sessão inteira. Onde ele liga isso?

🎓 Resumo do módulo

✓
session.start acende — aguardado antes do 1º prompt; volta no reload, não no /clear.
✓
clock.every e clock.after mantêm — até cancel() ou reload.
✓
$.fs com .catch — read, write, list, stat; realPath para guardas.
✓
$.process.run por argv — sem shell, com tempo-limite.
✓
Modelo custa — prompt.fill sugere; no kit INEMA, mod não chama modelo nem rede.

Próximo módulo:

3.1 — ui.render e as superfícies (Trilha 3: Telas e comandos)