hobots 2.4.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/tasks.md CHANGED
@@ -1,314 +1,169 @@
1
- # Tasks (execução sob demanda)
1
+ # Tasks
2
2
 
3
- Capability de **execução remota sob demanda**, habilitada com `tasks: true` (ou opções) no `init()` único. O agente faz polling no app, executa os handlers registrados quando aparece trabalho e reporta status e logs incrementais — é o que alimenta o módulo **Solicitações** do app. Usa o mesmo Client Secret, o mesmo `debug` e o `instanceId` como identidade — nenhum handler global extra é instalado.
3
+ Uma **task** é o que o robô sabe fazer, identificada por um slug. Cada rodada é uma **execução** (run) na plataforma, com logs, itens, progresso, anexos e resultado. Há dois modos de registrar uma task; tudo o mais (o objeto `hobots`, cancelamento, retry) é igual nos dois.
4
4
 
5
- O vocabulário desta página ([run](./concepts.md#run), [claim](./concepts.md#claim), [drain mode](./concepts.md#drain-mode), [ctx.tx](./concepts.md#transaction-de-run-ctxtx)) está explicado em [Conceitos](./concepts.md).
6
-
7
- ## Ciclo de vida
5
+ ## Os dois modos
8
6
 
9
7
  ```js
10
- import * as Hobots from 'hobots';
11
-
12
- Hobots.init({
13
- clientSecret: 'hb_<64 hex>',
14
- instanceId: 'vm-cliente-01',
15
- tasks: true, // ou { pollIntervalMs, concurrency, taskTimeoutMs, logFlushIntervalMs }
16
- });
17
-
18
- Hobots.register('processar-nfe', async (params, ctx) => {
19
- ctx.log.info(`processando lote ${params.lote}…`);
20
- ctx.throwIfCanceled(); // cancelamento cooperativo
21
- ctx.log.info('12 notas processadas');
22
- });
8
+ // sob demanda: uma execução por solicitação do app (formulário com este task_slug ou agendamento)
9
+ hobots.register('processar-nfe', async params => { /* … */ }, { concurrency: 2 });
23
10
 
24
- Hobots.start(); // começa o polling e MANTÉM O PROCESSO VIVO
25
-
26
- process.on('SIGINT', () => {
27
- void Hobots.close(15_000).then(() => process.exit(0));
28
- });
11
+ // sempre ligado: o robô abre a própria execução e roda o handler (o seu loop) uma vez
12
+ hobots.start('monitor-pasta', async () => { /* while (true) { … } */ });
29
13
  ```
30
14
 
31
- - `init({ tasks })` — cria o agente. Chamar `init()` de novo para o agente anterior e o substitui (os handlers registrados são preservados). Se havia runs em execução, elas são interrompidas **sem espera** e reportadas como `AgentTerminated` na hora — `init()` não é um jeito gracioso de reconfigurar um agente ocupado; para isso use `stop()` antes.
32
- - `register(task, handler)` — `task` é o `task_slug` configurado no formulário do app (**Configurações → Formulários**). Registre **antes** de `start()`. Pode ser chamado antes do `init()`; se o `init()` não habilitou as tasks, o SDK avisa no console.
33
- - `start()` — começa o polling. Lança se o `init()` não configurou `tasks` ou se nenhuma task foi registrada.
34
- - `stop(timeoutMs?)` — para **só o polling de tasks**: encerra o polling e aguarda as runs ativas ([default](./api-reference.md#tempos-e-intervalos)); a telemetria continua. Retorna `false` se estourou o timeout.
35
- - `close(timeoutMs?)` — encerra **tudo**: agente (aguardando runs em voo) → heartbeat → flush final.
15
+ - `register` — o robô faz polling na plataforma; cada solicitação vira uma execução com os `params` do formulário. Registre antes de haver solicitações; o polling começa sozinho.
16
+ - `start` — ao iniciar, o SDK cria a execução (`POST /v1/tasks/runs`) e roda o handler com `params = {}`. Enquanto a plataforma não responde, insiste com backoff — o handler só começa quando a execução existe. Quando o handler retorna, a execução fecha como `succeeded` (ou `failed` após os retries) e o processo segue vivo pelo heartbeat. **Uma execução por processo** — para "recomeçar", reinicie o robô.
17
+ - Os dois coexistem no mesmo processo. Um slug só pode ser registrado uma vez (lança).
36
18
 
37
- O `instanceId` (obrigatório) é a identidade do agente no painel — o poll o envia como `agent.instance_id`, a **mesma identidade** do heartbeat de telemetria. Com o `init()` único, os dois canais convergem em **um agente** no módulo Agentes automaticamente. Com `tasks: true`, o heartbeat é **opcional**: o próprio poll já marca a presença do agente; o heartbeat continua essencial para agentes só de telemetria.
19
+ > No modo start, lembre do teto de **1000 linhas de log por execução** no servidor: logue novidades e use `hobots.items` para o trabalho feito (até 10.000 itens).
38
20
 
39
- > **Atenção:** ao contrário da telemetria, `start()` **segura o processo vivo** aguardando trabalho até `stop()`/`close()` — o agente é a razão do processo existir.
40
-
41
- ## O handler e o `RunContext`
21
+ ## O handler e o objeto `hobots`
42
22
 
43
23
  ```ts
44
- type TaskHandler<P = Record<string, unknown>> =
45
- (params: P, ctx: RunContext) => void | Promise<void>;
46
-
47
- interface RunContext {
48
- runId: string;
49
- task: string;
50
- log: RunLogger; // ctx.log.debug/info/warn/error/success(message)
51
- items: RunItems; // ctx.items.succeeded/failed/occurrence — a entrega do bot
52
- tx: Transaction;
53
- signal: AbortSignal;
54
- throwIfCanceled(): void;
55
- sleep(ms: number): Promise<void>;
56
- }
24
+ type TaskHandler<P = Record<string, unknown>> = (params: P, ctx: RunContext) => void | Promise<void>;
57
25
  ```
58
26
 
59
- - **`params`** — os dados do formulário configurado no app (o JSON validado).
60
- - **`ctx.log.<level>(message)`** — níveis `debug`, `info`, `warn`, `error` e `success`; logs enviados **incrementalmente** ao app (PATCHes com throttle), visíveis na solicitação em tempo quase real. Cada linha também é **impressa no terminal local** como `[hobots:<task>] INFO mensagem`, colorida por nível — útil para acompanhar o robô rodando na VM. A cor é desligada automaticamente fora de um TTY e quando a env `NO_COLOR` está definida; o eco local é independente do envio ao servidor.
61
- - **`ctx.items`** — registro dos **itens que a execução processou/gerou** (notas fiscais baixadas, boletos emitidos…), com status e atributos. Aparecem na aba "itens" da solicitação, com totalizadores (veja a seção abaixo).
62
- - **`ctx.tx`** — a [transaction de performance](./concepts.md#transaction-de-run-ctxtx) desta run, já correlacionada e finalizada pelo SDK. Use `ctx.tx.startChild()` para medir as etapas (veja abaixo).
63
- - **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**. Cancelamento é cooperativo (veja abaixo).
64
- - **`ctx.throwIfCanceled()`** — lança `RunCanceled` se o `signal` já disparou. O ponto de parada de 1 linha para chamar entre as etapas do trabalho.
65
- - **`ctx.sleep(ms)`** — sleep que respeita o `signal`: rejeita com `RunCanceled` no cancelamento em vez de esperar até o fim. Use no lugar do seu `sleep` em esperas longas.
66
- - **Retorno** — **ignorado**. O SDK não captura nem envia o valor retornado pelo handler ao servidor — resultados de execução podem conter dados sensíveis e não são coletados.
67
- - **Erro lançado** — a run é reportada como `failed` com tipo/mensagem/stack (mensagem cap 10k, stack 20k).
68
-
69
- Você pode tipar os params: `Hobots.register<{ lote: number }>(...)`.
70
-
71
- > **Dados sensíveis:** o que você escreve em `ctx.log` e as mensagens/stack de erros lançados **são enviados ao servidor** e ficam visíveis na execução no app. Não inclua senhas, tokens, documentos, dados pessoais ou qualquer informação sensível em logs e mensagens de erro.
72
-
73
- ## Itens de execução (`ctx.items`)
74
-
75
- Logs contam **como** o bot trabalhou; itens contam **o que** ele entregou. Cada item é uma unidade de negócio processada pela execução — uma nota fiscal baixada, um boleto emitido — registrada com um dos três status:
76
-
77
- - **`succeeded`** — processado com sucesso.
78
- - **`failed`** — falha técnica ao processar o item (ex.: o download quebrou).
79
- - **`occurrence`** — ocorrência de negócio, que não é falha do bot (ex.: "nota nº 10 não encontrada", "CNPJ do prestador incorreto").
27
+ Dentro do handler — e em **qualquer módulo** chamado por ele — o objeto global `hobots` resolve a execução atual:
80
28
 
81
29
  ```js
82
- Hobots.register('baixar-notas', async (params, ctx) => {
83
- for (const nota of await listarNotas(params)) {
84
- try {
85
- if (!(await existeNoPortal(nota))) {
86
- ctx.items.occurrence(`nota nº ${nota.numero} não encontrada`, { numero_nota: nota.numero }, { id: nota.numero });
87
- continue;
88
- }
89
- await baixar(nota);
90
- ctx.items.succeeded({ numero_nota: nota.numero, cnpj_prestador: nota.cnpj }, { id: nota.numero });
91
- } catch (erro) {
92
- ctx.items.failed(String(erro), { numero_nota: nota.numero }, { id: nota.numero });
93
- }
94
- }
95
- });
96
- ```
97
-
98
- Na plataforma, os itens aparecem na aba **itens** da solicitação, com totalizadores (total, sucesso, falhas, ocorrências), filtro por status e paginação. As chaves do `payload` que viram **colunas** da tabela são configuradas por formulário em Configurações → Formulários → itens da execução — o `payload` aceita qualquer atributo que você julgar útil (só as chaves configuradas são exibidas; o restante fica no registro).
99
-
100
- ### API
101
-
102
- ```ts
103
- interface RunItems {
104
- add(item: { status: 'succeeded' | 'failed' | 'occurrence'; id?: string; message?: string; payload?: Record<string, unknown> }): void;
105
- succeeded(payload?, options?: { id?: string }): void;
106
- failed(message, payload?, options?: { id?: string }): void;
107
- occurrence(message, payload?, options?: { id?: string }): void;
30
+ // notas.js — não recebe ctx nenhum
31
+ export async function baixar(nota) {
32
+ const step = hobots.transaction.startChild('http.client', `GET /notas/${nota}`);
33
+ const xml = await portal.baixar(nota);
34
+ step.setData('bytes', xml.length).finish();
35
+ hobots.items.succeeded({ numero_nota: nota }, { id: nota });
108
36
  }
109
37
  ```
110
38
 
111
- - **`id`** (opcional) — identidade de negócio do item **dentro da run** (ex.: nº da nota). Registrado uma vez: um `id` repetido na mesma run é **ignorado pelo servidor** (primeiro vence), o que torna o retry de rede idempotente. Sem `id`, o SDK gera um único e cada chamada vira um item novo.
112
- - **`message`** — o motivo, esperado em `failed`/`occurrence` (cap 2.000 caracteres). Passa por `scrubSecrets`, como os logs.
113
- - **`payload`** — atributos livres do item (cap 16 KB serializado; acima disso o payload é descartado com aviso e o item mantido). Strings passam por `scrubSecrets`.
114
- - **Registro único** — o item chega com o status final; não existe "atualizar item". Se o bot reprocessa, use outra run (retry cria uma run nova, com itens próprios).
39
+ Isso funciona com `concurrency > 1` porque cada execução roda no seu contexto async (`AsyncLocalStorage`). Com uma única execução ativa, funciona até fora da cadeia async (um listener criado antes do handler, por exemplo). O `ctx` do 2º argumento é o mesmo objeto — use se preferir explícito. Tipar params: `hobots.register<{ lote: number }>(…)`.
115
40
 
116
- ### Como funciona (e por que não trava a run)
41
+ | Membro | Notas |
42
+ | --- | --- |
43
+ | `hobots.log.<debug\|info\|warn\|error\|success>(msg)` | Enviado incrementalmente (PATCHes com throttle) e ecoado no terminal. Nunca lança. |
44
+ | `hobots.items` | Ver [Itens](#itens-de-execução-hobotsitems) |
45
+ | `hobots.progress` | Ver [Progresso](#progresso-hobotsprogress) |
46
+ | `hobots.transaction` | Transaction da execução, já correlacionada e finalizada pelo SDK. `startChild(op, descrição)` = uma step no waterfall. |
47
+ | `hobots.attach(imagem, opts?)` | Ver [Anexos](#anexos-screenshots) |
48
+ | `hobots.captureException(erro, hint?)` | Issue no módulo Issues; `hint.attachment` anexa um print à linha de log |
49
+ | `hobots.retry(fn, opts?)` | Ver [Retry](#retry) |
50
+ | `hobots.sleep(ms)` · `hobots.signal` · `hobots.throwIfCanceled()` | Ver [Cancelamento](#cancelamento-cooperativo) |
51
+ | `hobots.runId` · `hobots.task` · `hobots.params` | Identidade da execução |
117
52
 
118
- As chamadas são **síncronas** (nunca esperam rede): os itens entram num buffer próprio, separado da fila de status/logs, e sobem em lotes (flush a cada 2s ou 100 itens, POSTs de até 500). Antes do reporte final (`succeeded`/`failed`), o SDK **drena o buffer** (teto de 15s) — os totalizadores da solicitação ficam completos quando ela termina.
53
+ - **Retorno** do handler: **ignorado** (resultados podem conter dados sensíveis). Reporte por logs e itens.
54
+ - **Erro lançado**: nova tentativa (ver Retry) e, esgotadas, `failed` com tipo, mensagem (cap 10k) e stack (cap 20k), passando pelo scrub de segredos.
119
55
 
120
- Diferente de logs e status, **uma run cancelada continua aceitando itens**: item é fato de trabalho já feito, e o handler pode continuar rodando após o cancelamento cooperativo.
56
+ > **Dados sensíveis:** logs, itens, erros e prints são enviados ao servidor e ficam visíveis no app.
121
57
 
122
- ### Limites
58
+ ## Itens de execução (`hobots.items`)
123
59
 
124
- | Limite | Valor |
125
- | --- | --- |
126
- | Itens por execução | **10.000** (acima disso o SDK descarta com um aviso) |
127
- | Itens por request | 500 (o SDK fatia sozinho) |
128
- | `payload` por item | 16 KB serializado |
129
- | `message` | 2.000 caracteres |
60
+ A **entrega do robô**: cada item processado, com status e atributos livres (viram colunas na aba de itens da execução no app), totalizadores na solicitação.
61
+
62
+ ```js
63
+ hobots.items.succeeded({ numero_nota: nota.numero, cnpj: nota.cnpj }, { id: nota.numero });
64
+ hobots.items.failed('portal fora do ar', { numero_nota: nota.numero }, { id: nota.numero }); // falha técnica
65
+ hobots.items.occurrence('nota não encontrada', { numero_nota: '10' }, { id: '10' }); // ocorrência de negócio
66
+ hobots.items.add({ status: 'succeeded', id: '11', payload: { … } }); // forma genérica
67
+ ```
130
68
 
131
- > **Dados sensíveis:** como os logs, `message` e `payload` **são enviados ao servidor** e ficam visíveis na solicitação. Não inclua senhas, tokens ou dados pessoais desnecessários.
69
+ - Chamadas síncronas e bufferizadas (lotes de até 500); o SDK drena o buffer antes do reporte final.
70
+ - `id` é a identidade de negócio do item na execução — repetido, o servidor ignora (retry de rede e retry do handler idempotentes). Sem `id`, cada chamada é um item novo.
71
+ - Limites: 10.000 itens/execução, payload 16 KB, mensagem 2.000 caracteres. Uma execução cancelada ainda aceita os itens já bufferizados (trabalho feito).
72
+ - Cada chamada é um **ponto de parada cooperativo** (lança `RunCanceled` se a execução foi cancelada).
132
73
 
133
- ## Progresso da execução (`ctx.progress`)
74
+ ### Colunas da aba de itens
134
75
 
135
- Itens contam **o que** foi entregue; progresso conta **quanto falta**. Quando a run percorre uma lista de tamanho conhecido, declare o total e avance a cada item — a solicitação no app mostra barra, percentual (`10/100 · 10%`) e **tempo restante estimado** (calculado pela taxa média desde o primeiro reporte).
76
+ As colunas vêm do código, não do formulário — valem para `register()` (com ou sem formulário) e `start()`:
136
77
 
137
78
  ```js
138
- Hobots.register('baixar-notas', async (params, ctx) => {
139
- const notas = await listarNotas(params);
140
- ctx.progress.total(notas.length); // antes do loop
141
- for (const nota of notas) {
142
- await baixar(nota);
143
- ctx.progress.advance(); // a cada item
144
- }
79
+ register('processar-nfe', handler, {
80
+ items: { columns: ['numero_nota', { key: 'cnpj', label: 'CNPJ' }, { key: 'valor', label: 'valor (R$)' }] },
145
81
  });
146
82
  ```
147
83
 
148
- ### API
84
+ - Ordem da lista = ordem das colunas; até 12; chave com letras, números, `.`, `-`, `_` (inválidas são descartadas com aviso em `debug`).
85
+ - A declaração vai na presença da instância (poll / abertura de run). No app, **Configurações → agente → itens da execução** pode personalizar as colunas (e restaurar as do código).
86
+ - Sem declaração nem personalização, o app mostra as chaves encontradas nos próprios payloads.
149
87
 
150
- ```ts
151
- interface RunProgressReporter {
152
- total(total: number): void; // declara/ajusta o total (inteiro >= 1; idempotente)
153
- advance(count?: number): void; // incrementa os processados (padrão 1; clampa em total)
154
- set(current: number, total?: number): void; // valor absoluto (ex.: retomada de um lote)
155
- readonly current: number; // processados até agora
156
- readonly snapshot: { current: number; total: number } | null; // null sem total declarado
88
+ ## Progresso (`hobots.progress`)
89
+
90
+ ```js
91
+ hobots.progress.total(notas.length);
92
+ for (const nota of notas) {
93
+ await baixar(nota);
94
+ hobots.progress.advance(); // barra, percentual e tempo restante no app
157
95
  }
158
96
  ```
159
97
 
160
- - **Opcional** — sem `total()` nada é enviado e a UI não mostra barra nem estimativa. `advance()` antes do `total()` só guarda o contador.
161
- - **Independente de `ctx.items`** — use um, outro ou ambos; nenhum avança o outro (um item pode gerar vários registros, ou nenhum).
162
- - **Total inválido** (`0`, negativo, não-inteiro) é ignorado com um aviso único no terminal.
163
- - **Semântica de snapshot** — o servidor sobrescreve `current`/`total` com o último valor recebido; não há soma. Ajustar o `total` no meio da run (a lista cresceu) é permitido.
164
-
165
- ### Como funciona (e por que não gera tráfego)
166
-
167
- As chamadas são **síncronas e baratas**: o snapshot mais recente pega carona em **qualquer** PATCH que saia — logs incrementais, heartbeat de cancelamento, reporte terminal. Só quando nada mais está a caminho o SDK dispara um PATCH `running` próprio, no máximo **1 por segundo**. `advance()` num loop de 10.000 itens em 5 segundos gera ~5 PATCHes, não 10.000. O PATCH terminal (`succeeded`/`failed`) **sempre** leva o valor final — uma run que falhou aparece como `87/100`.
168
-
169
- O servidor grava `progress_started_at` no primeiro PATCH com progresso (relógio do servidor) — é a âncora da estimativa de término, calculada no app: `restante = decorrido × (total − current) ÷ current`.
98
+ Opcional e independente de `items`. Sem `total()` nada é enviado. Snapshot absoluto que pega carona nos PATCHes de log (no pior caso 1/s); o PATCH terminal leva o valor final. `set(atual, total?)`, `current` e `snapshot` completam a API. `total/advance/set` são pontos de parada cooperativos; `current`/`snapshot` nunca lançam.
170
99
 
171
100
  ## Anexos (screenshots)
172
101
 
173
- Texto de log conta o que o robô fez; um print conta **como estava a tela**. Os métodos de captura — `Hobots.captureException(error, hint)` e `Hobots.captureMessage(message, level, hint)` — aceitam um **`attachment`** no hint. Dentro de uma execução de task, a imagem vira anexo da linha de log da run que espelha a captura, e aparece como miniatura logo abaixo dela na aba "logs do agente" da execução — clicável para ampliar.
174
-
175
102
  ```js
176
- Hobots.register('processar-nfe', async (params, ctx) => {
177
- await entrar();
178
- Hobots.captureMessage('portal após o login', 'info', {
179
- attachment: await page.screenshot(),
180
- });
103
+ await hobots.attach(await page.screenshot(), { caption: 'portal após o login' });
181
104
 
182
- try {
183
- await emitir(params);
184
- } catch (erro) {
185
- // numa falha é justamente quando o print importa
186
- Hobots.captureException(erro, {
187
- attachment: { image: await page.screenshot(), caption: 'tela no momento do erro' },
188
- });
189
- throw erro;
190
- }
191
- });
105
+ try {
106
+ await emitir(params);
107
+ } catch (erro) {
108
+ hobots.captureException(erro, { attachment: { image: await page.screenshot(), caption: 'tela no momento do erro' } });
109
+ throw erro;
110
+ }
192
111
  ```
193
112
 
194
- **O SDK não tira o print.** Ele recebe a imagem pronta — de onde você quiser: `page.screenshot()` do Playwright/Puppeteer, uma biblioteca de captura de desktop, um arquivo que o próprio sistema gerou. Isso mantém o SDK sem dependências nativas e funcionando em qualquer ambiente.
195
-
196
- ### O que aceita
197
-
198
- O `attachment` pode ser a imagem direto, ou um objeto com metadados:
199
-
200
- | Entrada | Exemplo |
201
- | --- | --- |
202
- | bytes | `Uint8Array`, `ArrayBuffer` ou `Buffer` — `await page.screenshot()` |
203
- | caminho de arquivo | `'C:\\prints\\tela.png'` (só em Node) |
204
- | objeto | `{ image, caption?, contentType? }` |
205
-
206
- | Campo do objeto | Tipo | Default | O que faz |
207
- | --- | --- | --- | --- |
208
- | `image` | bytes ou caminho | — | a imagem em si |
209
- | `caption` | `string` | — | legenda curta exibida sob a imagem ampliada (máx. 200 caracteres) |
210
- | `contentType` | `'image/png' \| 'image/jpeg' \| 'image/webp'` | detectado | só é usado quando os *magic bytes* não bastam |
211
-
212
- O texto da linha de log é a própria mensagem capturada (`message` do `captureMessage`, ou `Nome: mensagem` do erro no `captureException`); o nível vem do `type` da captura (`'fatal'` vira `'error'` no log da run).
213
-
214
- ### Limites
215
-
216
- | Limite | Valor |
217
- | --- | --- |
218
- | formatos | PNG, JPEG e WEBP |
219
- | tamanho por imagem | 5 MB |
220
- | anexos por execução | 20 |
221
- | anexos por linha de log | 4 |
222
- | uploads simultâneos | 3 (o resto fica na fila) |
223
-
224
- ### Como funciona (e por que não trava a run)
225
-
226
- A linha de log é registrada **na hora**, com o id do anexo; o upload roda em segundo plano. O binário vai direto para o S3 por uma URL presignada — não passa pela API do Hobots. Enquanto o upload não termina, o app mostra a miniatura como "enviando imagem…".
227
-
228
- O anexo **nunca faz a captura lançar**. Todo insucesso vira um aviso no log da run:
229
-
230
- | Motivo | Quando |
231
- | --- | --- |
232
- | `too-large` | acima de 5 MB |
233
- | `quota` | passou de 20 anexos na execução |
234
- | `unsupported-format` | não é PNG/JPEG/WEBP |
235
- | `read-failed` | o caminho de arquivo não pôde ser lido |
236
- | `unsupported` | a instalação do Hobots não tem anexos habilitados |
237
- | `upload-failed` | falha de rede depois dos retries |
238
- | `canceled` | a run foi cancelada ou encerrada durante o upload |
239
-
240
- O upload é sempre fire-and-forget — a captura retorna na hora. Antes de reportar o desfecho da run, o SDK aguarda os uploads pendentes por até 15 s, para a execução não terminar com miniaturas em "enviando…".
113
+ O SDK não tira o print: aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo. A linha de log entra na hora; o upload sobe em segundo plano direto para o S3 (URL presignada) e **nunca lança nem bloqueia** — falhas viram um aviso no log da execução. Limites: PNG/JPEG/WEBP, 5 MB, 20 por execução, 4 por linha de log. Fora de uma execução não há onde pendurar a imagem (descartada com aviso).
241
114
 
242
- > **Fora de uma execução:** `attachment` só funciona dentro de um handler de task — fora dele não existe log de run para pendurar a imagem, e o SDK descarta o anexo com um aviso no console (o evento em si é capturado normalmente).
243
-
244
- > **Dados sensíveis:** o print é enviado ao servidor **exatamente como está** — o SDK não borra nem recorta nada. Uma tela de portal costuma conter CPF, nome, saldo, número de documento. Capture apenas o necessário e evite prints de telas com dados de terceiros. Solicitantes (papel `requester`) só veem os anexos quando o projeto está com "mostrar logs para solicitantes" ligado, o mesmo gate dos logs.
115
+ ## Cancelamento cooperativo
245
116
 
246
- ## Performance da run (`ctx.tx`)
117
+ Cancelar a solicitação no app **não mata** o handler: o SDK detecta (o PATCH periódico recebe 409), dispara `hobots.signal` e o próximo **ponto de parada** lança `RunCanceled`:
247
118
 
248
- O SDK já abre uma [transaction](./concepts.md#transaction-de-run-ctxtx) para cada run e entrega em `ctx.tx`. Ela é correlacionada à execução, recebe o status conforme o desfecho (`ok`, `error` ou `cancelled`) e é **finalizada automaticamente** quando o handler termina. Você não cria e não finaliza — só marca as etapas:
119
+ - `hobots.items.*`, `hobots.progress.total/advance/set`, `hobots.transaction.startChild()`, `hobots.sleep()` e cada tentativa de `hobots.retry()`.
120
+ - **Nunca `hobots.log.*`** — log em `finally` de limpeza não pode explodir.
249
121
 
250
122
  ```js
251
- Hobots.register('processar-nfe', async (params, ctx) => {
252
- const login = ctx.tx.startChild('browser', 'login no portal');
253
- await entrar();
254
- login.finish();
255
-
256
- for (const nota of params.notas) {
257
- const step = ctx.tx.startChild('nfe', `nota ${nota.numero}`);
258
- ctx.throwIfCanceled();
259
- await processar(nota);
260
- step.setData('valor', nota.valor).finish();
123
+ hobots.register('processar-nfe', async params => {
124
+ const browser = await abrir();
125
+ try {
126
+ for (const nota of params.notas) {
127
+ await processar(nota);
128
+ hobots.items.succeeded({ numero_nota: nota }, { id: nota }); // para aqui se cancelada
129
+ }
130
+ } finally {
131
+ hobots.log.info('fechando navegador'); // seguro: log nunca lança
132
+ await browser.close();
261
133
  }
262
134
  });
263
135
  ```
264
136
 
265
- Cada `startChild` vira uma barra no waterfall da aba de performance daquela execução no app — é como você responde "onde foram os 4 minutos". Sem nenhuma step, a transaction ainda mede o tempo total da run; o waterfall só fica com uma barra.
137
+ `hobots.throwIfCanceled()` e `hobots.signal` (um `AbortSignal`, útil para passar a `fetch`) ficam para controle fino. O `RunCanceled` de um cancelamento vindo do app é suprimido — nada é reportado por cima. Se o handler ignorar o signal por `cancelGraceMs` (10 s), o SDK libera o slot e o handler segue em segundo plano, com todos os reportes descartados.
266
138
 
267
- - **`startTransaction()` dentro do handler devolve a própria `ctx.tx`.** Cada execução tem exatamente **uma** transaction: se o seu código (ou uma lib sua já instrumentada) chamar `Hobots.startTransaction()` dentro de um handler, o SDK detecta o contexto da run e retorna a transaction dela em vez de criar uma segunda — as etapas caem no mesmo waterfall. Fora de um handler, `startTransaction()` cria uma transaction nova, sem `run_id`.
268
- - `ctx.tx` **não respeita o `tracesSampleRate`** — toda run gera uma transaction. A amostragem vale só para as transactions abertas com `startTransaction()` fora de uma run.
269
- - Step que você esquecer aberta é fechada no `finish()` da transaction, então o dado não se perde.
270
- - Marque etapas com ressalva usando `step.setStatus('warning')` (ex.: retry que deu certo, resultado parcial): elas aparecem em âmbar no waterfall, distintas de sucesso e de falha (`'error'`).
139
+ > Não use `if (hobots.signal.aborted) return;` — retornar reporta `succeeded`. É preciso lançar.
271
140
 
272
- ## Cancelamento cooperativo
141
+ ## Retry
273
142
 
274
- Uma run cancelada no app **não mata** o seu handler — o SDK não interrompe código em execução. Ele apenas dispara `ctx.signal`. Enquanto o handler roda, o SDK mantém um **heartbeat de cancelamento** (a cada `cancelCheckIntervalMs`, veja [Tempos e intervalos](./api-reference.md#tempos-e-intervalos)): a run cancelada no app é detectada em segundos, mesmo que o handler nunca chame `ctx.log`. Chame **`ctx.throwIfCanceled()`** em loops longos e pontos de parada (e troque esperas por `ctx.sleep()`):
143
+ O handler que lança é **re-invocado na mesma execução**: aviso no log da execução (`tentativa 1/2 falhou: <erro> — nova tentativa em 5000ms`), espera cancelável, mesmos `params`. Default `retries: 1` (2 tentativas), `retryDelayMs: 5_000` (ms ou `(tentativa) => ms`). Esgotadas → `failed` com o último erro.
144
+
145
+ Nunca há retry em `RunCanceled`, `TaskTimeout` (o timeout é por tentativa e terminal) ou execução já cancelada no app. Itens e progresso já enviados permanecem — o handler precisa ser **idempotente** (use o `id` de negócio nos itens).
146
+
147
+ Para uma chamada instável dentro do handler:
275
148
 
276
149
  ```js
277
- Hobots.register('processar-nfe', async (params, ctx) => {
278
- for (const nota of params.notas) {
279
- ctx.throwIfCanceled(); // lança RunCanceled se cancelada — o handler para aqui
280
- await processar(nota);
281
- }
282
- });
150
+ const dados = await hobots.retry(() => portal.buscar(nota), { retries: 3, delayMs: attempt => attempt * 500, shouldRetry: e => e.code !== 'ENOTFOUND' });
283
151
  ```
284
152
 
285
- Quando o cancelamento veio do app, a run já está `cancelada` no servidor — o erro `RunCanceled` lançado pelo handler é suprimido, nada é reportado por cima. Para lógica de limpeza antes de parar (fechar navegador, desfazer estado), use `try/finally` ou escute `ctx.signal` diretamente.
153
+ `hobots.retry` usa o signal da execução: cancelada, ele lança `RunCanceled` em vez de insistir. Fora de uma execução funciona igual, sem cancelamento.
286
154
 
287
- Se o handler **nunca** checar `ctx.signal` (loop sem `throwIfCanceled()`, `sleep()` genérico em vez de `ctx.sleep()`), o SDK ainda assim não trava o agente: depois de `cancelGraceMs` (default `10_000`) a partir do disparo de `ctx.signal`, o agente desiste de esperar aquele handler e libera o slot de concorrência para novas runs — logando um aviso. Isso não mata o handler (ele pode continuar rodando em segundo plano, sem efeito no app já que a run está cancelada), só evita que uma run "zumbi" trave o agente inteiro num `concurrency` baixo.
155
+ ## Concorrência (`register`)
288
156
 
289
- ## Encerramento inesperado
290
-
291
- Se o processo do agente **morre no meio de uma run**, a solicitação vira **falha** no app com erro `AgentTerminated` ("execução encerrada inesperadamente") — sem re-execução automática. Dois caminhos garantem isso:
157
+ `concurrency` (1..5, default 1) por task. O agente só pede à plataforma o que consegue rodar (tasks com slot livre, `max` = soma dos slots, teto 5) e entra em **drain mode** — re-pesquisa imediatamente enquanto vem trabalho; sem trabalho, espera `pollIntervalMs`.
292
158
 
293
- - **Morte capturável** (SIGINT/SIGTERM sem handler próprio do programa, erro não tratado): o SDK reporta a falha na hora (~2s, um PATCH best-effort com timeout curto) e então deixa o processo morrer com a semântica default do sinal. Os handlers são instalados no `start()` e **nunca interferem** quando o programa tem handler próprio para o sinal (o shutdown gracioso via `close()` continua sendo o caminho normal). Desative com `tasks: { installExitHandlers: false }`.
294
- - **Morte dura** (kill -9, queda de energia, rede fora): nada roda no cliente. O heartbeat de cancelamento é também o **sinal de vida da run** — o servidor marca a run como falha ao notar silêncio prolongado (o [sweep](./api-reference.md#tempos-e-intervalos) server-side).
159
+ ## Timeout
295
160
 
296
- `close(timeoutMs)` que estoura o timeout com runs ainda em execução também reporta essas runs como `AgentTerminated` — o processo vai morrer em seguida e o app não deve mostrar a run como "executando" para sempre.
161
+ `timeoutMs` (default: nenhum) é por tentativa e **client-side**: no estouro o SDK dispara o signal, libera o slot e reporta `failed`/`TaskTimeout` — sem nova tentativa. O handler não é morto; respeite o signal para parar de verdade.
297
162
 
298
163
  ## Idempotência (at-least-once até `running`)
299
164
 
300
- Se o agente morrer logo **após capturar** uma run (antes de reportar `running`), ela é re-entregue quando um agente voltar (até 3 tentativas; depois `AgentLost`). A partir do momento em que a run está `running`, a morte do agente **não re-entrega** — a run vira falha `AgentTerminated`, porque o trabalho pode ter efeitos colaterais parciais e re-executar automaticamente não é seguro (reexecute manualmente pelo app). Ainda assim, escreva handlers **idempotentes** — a janela de re-entrega pós-claim existe e processar a mesma run duas vezes não pode causar efeito duplicado (use uma chave de deduplicação sua, como o número da nota).
301
-
302
- ## Concorrência
303
-
304
- `concurrency` (default 1, máx 5) controla quantas runs o agente executa em paralelo. Com 1, as runs são sequenciais. O agente só reivindica o que consegue rodar (`max = concurrency - runs ativas`) e entra em **[drain mode](./concepts.md#drain-mode)**: enquanto vêm runs, re-pesquisa imediatamente; quando não vem nada, espera `pollIntervalMs`. Ou seja, o intervalo de poll é o teto da espera **quando não há trabalho** — não a latência típica para começar uma run.
165
+ Se o agente morrer logo após capturar uma execução (antes de reportar `running`), ela é re-entregue quando um agente voltar (até 3 tentativas; depois `AgentLost`). A partir de `running`, a morte do agente **não** re-entrega — a execução vira `AgentTerminated`, porque o trabalho pode ter efeitos parciais. Somado ao retry do handler, a regra é uma: processar a mesma coisa duas vezes não pode duplicar efeito.
305
166
 
306
- ## Timeout por run
307
-
308
- `taskTimeoutMs` é um timeout **client-side**. No estouro o SDK **não mata** o handler — ele dispara `ctx.signal`, libera o slot de concorrência e reporta a run como falha (`TaskTimeout`). O handler pode continuar rodando em background; respeite o `signal` para parar de verdade. Sem `taskTimeoutMs`, o backstop é o servidor.
309
-
310
- Todas as opções em [Referência de API → TasksOptions](./api-reference.md#tasksoptions).
311
-
312
- ## Como o claim funciona (resumo)
167
+ ## Encerramento inesperado
313
168
 
314
- O SDK é um cliente HTTP: ele faz `POST /v1/tasks/poll` e o **servidor** reivindica as runs pendentes de forma atômica com `FOR UPDATE SKIP LOCKED` — dois agentes nunca pegam a mesma run. Conceito em [Claim](./concepts.md#claim), detalhes em [Protocolo & transporte](./protocol.md).
169
+ Sem handler próprio de `SIGINT`/`SIGTERM` (ou em erro fatal), o SDK reporta as execuções ativas como `failed`/`AgentTerminated` antes de sair. Com handler próprio, chame `hobots.close()` nele. Se o processo morrer sem reportar, o servidor marca a execução após ~120 s sem sinal de vida.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hobots",
3
- "version": "2.4.0",
4
- "description": "SDK JavaScript da Hobots — telemetria (erros, transactions e heartbeats) e execução remota de tarefas sob demanda (tasks) para RPAs e integrações. Um único import e um único init().",
3
+ "version": "3.2.0",
4
+ "description": "SDK JavaScript da Hobots — tudo é uma task: register() atende solicitações da plataforma, start() roda sempre e reporta logs, itens e progresso. Credenciais por env, heartbeat sempre ligado, objeto global `hobots`.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "license": "MIT",