hobots 2.3.0 → 3.0.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,155 @@
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).
115
-
116
- ### Como funciona (e por que não trava a run)
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 }>(…)`.
117
40
 
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.
119
-
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.
121
-
122
- ### Limites
123
-
124
- | Limite | Valor |
41
+ | Membro | Notas |
125
42
  | --- | --- |
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 |
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 |
52
+
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.
130
55
 
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.
56
+ > **Dados sensíveis:** logs, itens, erros e prints são enviados ao servidor e ficam visíveis no app.
132
57
 
133
- ## Progresso da execução (`ctx.progress`)
58
+ ## Itens de execução (`hobots.items`)
134
59
 
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).
60
+ A **entrega do robô**: cada item processado, com status e atributos livres (viram colunas configuráveis por formulário no app), totalizadores na solicitação.
136
61
 
137
62
  ```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
- }
145
- });
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
146
67
  ```
147
68
 
148
- ### API
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).
149
73
 
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
74
+ ## Progresso (`hobots.progress`)
75
+
76
+ ```js
77
+ hobots.progress.total(notas.length);
78
+ for (const nota of notas) {
79
+ await baixar(nota);
80
+ hobots.progress.advance(); // barra, percentual e tempo restante no app
157
81
  }
158
82
  ```
159
83
 
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`.
84
+ 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
85
 
171
86
  ## Anexos (screenshots)
172
87
 
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
88
  ```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
- });
89
+ await hobots.attach(await page.screenshot(), { caption: 'portal após o login' });
181
90
 
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
- });
91
+ try {
92
+ await emitir(params);
93
+ } catch (erro) {
94
+ hobots.captureException(erro, { attachment: { image: await page.screenshot(), caption: 'tela no momento do erro' } });
95
+ throw erro;
96
+ }
192
97
  ```
193
98
 
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…".
241
-
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).
99
+ 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).
243
100
 
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.
101
+ ## Cancelamento cooperativo
245
102
 
246
- ## Performance da run (`ctx.tx`)
103
+ 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
104
 
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:
105
+ - `hobots.items.*`, `hobots.progress.total/advance/set`, `hobots.transaction.startChild()`, `hobots.sleep()` e cada tentativa de `hobots.retry()`.
106
+ - **Nunca `hobots.log.*`** — log em `finally` de limpeza não pode explodir.
249
107
 
250
108
  ```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();
109
+ hobots.register('processar-nfe', async params => {
110
+ const browser = await abrir();
111
+ try {
112
+ for (const nota of params.notas) {
113
+ await processar(nota);
114
+ hobots.items.succeeded({ numero_nota: nota }, { id: nota }); // para aqui se cancelada
115
+ }
116
+ } finally {
117
+ hobots.log.info('fechando navegador'); // seguro: log nunca lança
118
+ await browser.close();
261
119
  }
262
120
  });
263
121
  ```
264
122
 
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.
123
+ `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
124
 
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'`).
125
+ > Não use `if (hobots.signal.aborted) return;` — retornar reporta `succeeded`. É preciso lançar.
271
126
 
272
- ## Cancelamento cooperativo
127
+ ## Retry
128
+
129
+ 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.
273
130
 
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()`):
131
+ 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).
132
+
133
+ Para uma chamada instável dentro do handler:
275
134
 
276
135
  ```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
- });
136
+ const dados = await hobots.retry(() => portal.buscar(nota), { retries: 3, delayMs: attempt => attempt * 500, shouldRetry: e => e.code !== 'ENOTFOUND' });
283
137
  ```
284
138
 
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.
286
-
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.
139
+ `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.
288
140
 
289
- ## Encerramento inesperado
141
+ ## Concorrência (`register`)
290
142
 
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:
143
+ `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
144
 
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).
145
+ ## Timeout
295
146
 
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.
147
+ `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
148
 
298
149
  ## Idempotência (at-least-once até `running`)
299
150
 
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.
151
+ 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
152
 
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)
153
+ ## Encerramento inesperado
313
154
 
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).
155
+ 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.3.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.0.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",
package/docs/telemetry.md DELETED
@@ -1,162 +0,0 @@
1
- # Telemetria
2
-
3
- Capability de telemetria: erros (módulo **Issues**), performance e uptime (módulo **Agentes**). Sempre ativa no `init()` único:
4
-
5
- ```js
6
- import * as Hobots from 'hobots';
7
- ```
8
-
9
- Todas as funções abaixo são no-op seguros se `Hobots.init()` não foi chamado (com aviso no console). Assinaturas completas em [Referência de API](./api-reference.md); o que cada conceito **é**, em [Conceitos](./concepts.md).
10
-
11
- ## init
12
-
13
- ```js
14
- Hobots.init({
15
- clientSecret: 'hb_<64 hex>',
16
- environment: 'production',
17
- release: 'robo-nfe@1.4.2',
18
- instanceId: 'vm-cliente-01',
19
- tags: { squad: 'automacoes' },
20
- heartbeat: true,
21
- });
22
- ```
23
-
24
- Cria o cliente, instala handlers globais de erro (a menos que `captureUnhandled: false`) e inicia o heartbeat se configurado. Chamar de novo substitui o cliente. Todos os campos em [Referência de API → InitOptions](./api-reference.md#initoptions).
25
-
26
- ## Captura de erros e mensagens
27
-
28
- ```js
29
- // exceções
30
- Hobots.captureException(error, {
31
- tags: { cliente: 'acme' },
32
- type: 'error',
33
- fingerprint: ['sefaz-timeout'], // agrupa manualmente; sem isso o servidor agrupa pelo stack
34
- });
35
-
36
- // mensagens sem exceção
37
- Hobots.captureMessage('Lote processado com sucesso', 'info');
38
- ```
39
-
40
- Ambas retornam o `event_id` (ou `undefined` se não inicializado). O segundo argumento é um [`CaptureHint`](./api-reference.md#capturehint) (`type`, `tags`, `extra`, `fingerprint`).
41
-
42
- > **Atenção:** o `event_id` retornado **não é confirmação de envio** — se o evento foi descartado por amostragem ou por `beforeSend`, você recebe um id normalmente e nada foi enfileirado.
43
-
44
- Erros **não tratados** (`uncaughtException` / `unhandledRejection`) são capturados e flushados automaticamente antes do processo sair — desligue com `captureUnhandled: false`.
45
-
46
- Conceitos: [Evento](./concepts.md#evento) · [Type](./concepts.md#type-tipo-do-evento) · [Fingerprint](./concepts.md#fingerprint).
47
-
48
- ## Breadcrumbs
49
-
50
- Trilha do que aconteceu antes do erro; enviada junto do próximo evento.
51
-
52
- ```js
53
- Hobots.addBreadcrumb({ category: 'nfe', message: 'Baixou XML da nota 123', level: 'info' });
54
- ```
55
-
56
- O buffer é limitado por `maxBreadcrumbs` (default 100, mais antigos descartados).
57
-
58
- Dois pontos que costumam surpreender: a lista **não é limpa** ao enviar um evento (o próximo evento leva as mesmas migalhas de novo), e ela é **global** — para uma trilha que não vaze, adicione dentro de um `withScope`. Conceito: [Breadcrumb](./concepts.md#breadcrumb).
59
-
60
- ## Escopo: tags, usuário, contexto, extras
61
-
62
- Dados anexados a **todos** os eventos seguintes:
63
-
64
- ```js
65
- Hobots.setTag('regiao', 'nordeste');
66
- Hobots.setTags({ squad: 'automacoes', cliente: 'acme' });
67
- Hobots.setUser({ id: 'op-42', email: 'op@acme.com' });
68
- Hobots.setContext('vm', { cpu: 4, ram: '8GB' });
69
- Hobots.setExtra('ultimoLote', 128);
70
- ```
71
-
72
- Qual usar para quê: **tag** é rótulo curto e indexável (você vai filtrar por isso), **context** é grupo nomeado de dados que andam juntos (você vai ler), **extra** é depósito livre para o resto. Detalhes e critérios em [Tag](./concepts.md#tag) · [Context](./concepts.md#context) · [Extra](./concepts.md#extra).
73
-
74
- Três comportamentos que valem saber:
75
-
76
- - Tags podem vir de quatro lugares e a última vence: `init({ tags })` → scope → evento → `hint` da chamada.
77
- - `setContext(nome, null)` **apaga** o grupo. O SDK já injeta os grupos `runtime` e `os` em todo evento.
78
- - `setExtra` não é indexado — se você quiser filtrar por aquele campo, ele deveria ser uma tag.
79
-
80
- Para um escopo **isolado** (sem vazar para o global), use `withScope`:
81
-
82
- ```js
83
- Hobots.withScope((scope) => {
84
- scope.setTag('lote', '128').setFingerprint(['reprocessamento']);
85
- Hobots.captureMessage('Reprocessando lote 128');
86
- });
87
- ```
88
-
89
- > **Atenção:** `withScope` é **síncrono** — ele troca o scope global e restaura no fim do callback. Se você der `await` lá dentro, o scope isolado vale para qualquer código que capturar eventos enquanto aquele `await` está pendente. Em código concorrente (várias runs em paralelo), passe as tags no `hint`: `captureException(error, { tags: { lote: '128' } })`. Conceito: [withScope](./concepts.md#withscope).
90
-
91
- ## Performance: transactions e etapas
92
-
93
- Uma **transaction** é uma unidade de trabalho nomeada e medida ("quanto minha rotina demorou"); as **etapas** são as unidades dentro dela ("onde o tempo foi gasto"). Conceitos: [Transaction](./concepts.md#transaction) · [Step](./concepts.md#step).
94
-
95
- ```js
96
- const tx = Hobots.startTransaction('processar-lote', 'rpa.job');
97
-
98
- const step = tx.startChild('http.client', 'GET /api/notas');
99
- await api.buscarNotas();
100
- step.setData('notas', 42).finish();
101
-
102
- const filho = step.startChild('db.query', 'INSERT INTO notas'); // etapas aninham
103
- await gravar();
104
- filho.finish();
105
-
106
- tx.setStatus('ok'); // ou 'error'
107
- tx.finish(); // só aqui a transaction é enviada (respeitando tracesSampleRate)
108
- ```
109
-
110
- - `startChild(op?, description?)` existe tanto em `Transaction` quanto em `Step` (aninhamento arbitrário).
111
- - Métodos encadeáveis: `setStatus`, `setData` (step), `setTag`/`setName` (transaction).
112
- - Status de step: `'ok'` (default), `'warning'` (atenção, não é falha) ou `'error'` — cores distintas no waterfall.
113
- - `finish()` na transaction fecha etapas abertas e envia o payload; nada é enviado se ela não foi amostrada. É **idempotente** — chamar duas vezes não envia duas.
114
- - A decisão de amostragem acontece no **`startTransaction()`**, não no `finish()`. Esquecer o `finish()` de uma step não perde o dado: ela é fechada no instante em que a transaction fecha.
115
- - Uma transaction leva as tags do `init()` e as que você marcar com `tx.setTag()` — ela **não** herda as tags do scope.
116
-
117
- Dentro de um handler de task, `startTransaction()` **retorna a transaction da própria run** ([`ctx.tx`](./concepts.md#transaction-de-run-ctxtx)) em vez de criar uma segunda — cada execução tem exatamente **uma** transaction. Fora de um handler, ela cria uma transaction nova, sem correlação com run nenhuma.
118
-
119
- ## Heartbeat (uptime)
120
-
121
- Bate "estou vivo" periodicamente; o servidor acusa o robô **offline** quando os batimentos param.
122
-
123
- ```js
124
- Hobots.init({
125
- clientSecret,
126
- heartbeat: { intervalMs: 30_000, name: 'Robô NFe — VM 01' },
127
- });
128
- ```
129
-
130
- `heartbeat: true` usa os defaults. A identidade é o `instanceId` do `init()`. Campos em [HeartbeatOptions](./api-reference.md#heartbeatoptions); conceito em [Heartbeat](./concepts.md#heartbeat).
131
-
132
- O primeiro batimento sai **na hora** em que o `init()` roda — não depois do primeiro intervalo. O batimento também carrega o intervalo configurado: é assim que o servidor sabe quanto silêncio é normal antes de declarar o robô offline.
133
-
134
- Essa identidade é a mesma que o poll de tasks envia (`agent.instance_id`) — os dois canais derivam do `instanceId` do `init()`, então o robô aparece como **um agente só** no módulo Agentes automaticamente. Por isso, com `tasks: true` o heartbeat é **opcional** (o poll já marca a presença do agente); ele é essencial para agentes só de telemetria.
135
-
136
- ## Recursos da máquina (CPU, memória, disco)
137
-
138
- Ligado por padrão em Node (`machineMetrics: true`), o SDK coleta CPU do sistema, memória, disco do volume do bot e RSS/CPU do próprio processo — **sem dependência nova** e sem timer próprio: a leitura pega carona em cada heartbeat e em cada poll de tasks (série da instância em **Instâncias → recursos da máquina**) e vai como `contexts.machine` em cada evento (card **"máquina no momento do erro"** no detalhe da issue). Assim dá para responder se o ambiente tem recurso suficiente e se um erro coincidiu com CPU/memória no limite.
139
-
140
- ```js
141
- Hobots.init({ clientSecret, instanceId: 'vm-01', heartbeat: true }); // coleta ligada
142
- Hobots.init({ clientSecret, instanceId: 'vm-01', machineMetrics: false }); // desliga
143
- ```
144
-
145
- O snapshot tem ~600 bytes (teto do servidor: 4 KiB). Formato em [Protocolo → MachineSnapshot](./protocol.md#machinesnapshot).
146
-
147
- ## Flush e close
148
-
149
- Nada é enviado na hora: cada evento, transaction e heartbeat entra numa **fila em memória** e sai em lote. É por isso que essas duas funções existem — quando o processo morre, a fila morre com ele. Conceito: [Fila, flush e close](./concepts.md#fila-flush-e-close).
150
-
151
- ```js
152
- await Hobots.flush(2000); // envia o que está em buffer, mantém o SDK vivo
153
- await Hobots.close(2000); // para o polling de tasks (se ativo) + heartbeat + flush final do transport
154
- ```
155
-
156
- O `timeoutMs` é opcional nas duas — o default é 5s (veja [Tempos e intervalos](./api-reference.md#tempos-e-intervalos)). As duas devolvem `false` quando estouram o prazo, o que significa que o SDK parou de esperar, **não** que os dados foram perdidos: os envios em voo continuam tentando enquanto o processo existir.
157
-
158
- Chame `close` antes do processo sair. Os timers da telemetria são `unref`'d, então ela **não** segura o processo vivo sozinha (só o `start()` das tasks segura).
159
-
160
- > **Atenção:** `close()` é **terminal**. Depois dele, todo evento, transaction ou heartbeat novo é **descartado** com aviso no console — não existe `reopen()`. Se o programa precisar voltar a emitir telemetria, chame `Hobots.init()` de novo.
161
-
162
- > **Atenção:** o `timeoutMs` do `close()` é gasto **duas vezes, em sequência** — uma esperando as runs de tasks, outra no flush final. `close(15_000)` pode levar até ~30s no pior caso. Dimensione por aí se o seu SIGTERM tem prazo curto.