hobots 2.4.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/README.md CHANGED
@@ -1,11 +1,9 @@
1
1
  # hobots
2
2
 
3
- SDK JavaScript/TypeScript da **Hobots** para instrumentar agentes (RPAs) e integrações.
3
+ SDK JavaScript/TypeScript da **Hobots** para robôs (RPAs) e integrações. **Tudo é uma task**: você registra o que o robô faz e a plataforma passa a receber logs, itens processados, progresso, prints, erros e o "estou vivo" do robô.
4
4
 
5
- - **Telemetria** — o que o programa **emite** sozinho: erros com stack trace (módulo **Issues** do app), performance (transactions/etapas) e heartbeats de uptime (módulo **Agentes**).
6
- - **Tasks** — execução **sob demanda**: o agente faz polling no app da Hobots, executa handlers registrados e reporta status e logs (módulo **Solicitações**).
7
-
8
- **Um único import e um único `init()`** cobrem as duas capabilities. Cliente HTTP puro, **sem dependências de runtime**. Requer **Node 18+** (usa o `fetch` global); a telemetria também roda no browser para captura de erros.
5
+ - **`hobots.register(task, handler)`** — **sob demanda**: o robô fica online aguardando **solicitações** criadas no app (formulários e agendamentos) e executa o handler a cada uma. Módulo **Solicitações**.
6
+ - **`hobots.start(task, handler)`** — **sempre ligado**: o robô abre a própria execução ao iniciar e roda o handler (o seu loop) dentro dela; a plataforma só recebe logs, itens e progresso. Sem formulário, sem solicitante.
9
7
 
10
8
  ---
11
9
 
@@ -17,430 +15,175 @@ npm install hobots
17
15
  ```
18
16
 
19
17
  ```js
20
- import * as Hobots from 'hobots'; // um import só — telemetria e tasks
18
+ import hobots from 'hobots'; // ou: import { hobots, register, start } from 'hobots'
21
19
  ```
22
20
 
23
- Também funciona em CommonJS: `const Hobots = require('hobots')`.
21
+ CommonJS: `const { hobots } = require('hobots')`.
24
22
 
25
23
  ---
26
24
 
27
- ## Um SDK, duas capabilities, um `init()`
28
-
29
- O SDK expõe **duas capabilities** autenticadas pelo **mesmo Client Secret do agente**, configuradas em um único `init()`:
30
-
31
- | Capability | Como habilita | Para quê | Ciclo de vida |
32
- | --- | --- | --- | --- |
33
- | **Telemetria** | sempre ativa no `init()` | O que o programa emite sozinho: erros, transactions/etapas, heartbeat. | Timers `unref`'d — **não** segura o processo. Chame `close()` no fim. |
34
- | **Tasks** | `tasks: true` (ou opções) no `init()` | Execução remota sob demanda via polling. | `start()` **segura o processo vivo** até `stop()`/`close()`. |
35
-
36
- Você usa **uma, outra ou as duas**:
37
-
38
- - Só saber se o robô quebrou, quanto demora e se está vivo? → `init({ clientSecret, heartbeat: true })`.
39
- - Disparar o robô sob demanda a partir de um formulário no app? → `init({ clientSecret, tasks: true })` + `register()` + `start()`.
40
- - Ambos? Passe `heartbeat` e `tasks` no mesmo `init()` — o `instanceId` é a identidade única, e os dois canais convergem em **um agente** no painel. Com `tasks: true`, o heartbeat é **opcional**: o próprio poll de tasks já marca a presença do agente.
41
-
42
- ```js
43
- Hobots.init({
44
- clientSecret: 'hb_<64 hex>',
45
- instanceId: 'vm-cliente-01',
46
- heartbeat: true, // telemetria: "estou vivo" a cada 30s
47
- tasks: { pollIntervalMs: 2_000 }, // execução sob demanda
48
- });
49
-
50
- Hobots.register('processar-nfe', async (params, ctx) => {
51
- ctx.log.info('começando…');
52
- ctx.log.info('12 notas processadas'); // o retorno do handler é ignorado — reporte por log
53
- });
54
-
55
- Hobots.start(); // inicia o polling de tasks (segura o processo vivo)
56
- ```
57
-
58
- ---
59
-
60
- ## O Client Secret
61
-
62
- Cada agente tem **um único Client Secret** — a mesma credencial autentica a telemetria **e** as tasks:
63
-
64
- ```
65
- hb_<64 hex>
66
- ```
67
-
68
- A credencial é **secreta** e viaja sempre no header `Authorization: Bearer` — nunca na URL. Copie o Client Secret no app, em **Configurações → Agentes** — os snippets de integração já vêm prontos. O SDK fala com a API oficial `https://api2.hobots.app` automaticamente; em desenvolvimento, aponte para outro host com a env `HOBOTS_API_URL` (ex.: `HOBOTS_API_URL=http://localhost:3001`).
69
-
70
- ---
71
-
72
- ## Telemetria
73
-
74
- Todas as funções abaixo são **no-op seguras** se `Hobots.init()` não foi chamado (com aviso no console).
75
-
76
- ### `init(options)`
77
-
78
- Inicialize **uma vez**, o mais cedo possível no processo. Cria o cliente, instala handlers globais de erro (a menos que `captureUnhandled: false`) e inicia o heartbeat se configurado.
79
-
80
- ```js
81
- Hobots.init({
82
- clientSecret: 'hb_<64 hex>',
83
- environment: 'production',
84
- release: 'robo-nfe@1.4.2',
85
- instanceId: 'vm-cliente-01',
86
- tags: { squad: 'automacoes' },
87
- heartbeat: true, // "estou vivo" a cada 30s
88
- });
89
- ```
90
-
91
- | Opção | Tipo | Default | Descrição |
92
- | --- | --- | --- | --- |
93
- | `clientSecret` | `string` | **obrigatório** | o Client Secret do agente (`hb_<64 hex>`), copiado em **Configurações → Agentes** |
94
- | `environment` | `string` | `'production'` | ambiente lógico |
95
- | `release` | `string` | — | versão do programa, ex. `robo-nfe@1.4.2` |
96
- | `instanceId` | `string` | **obrigatório** | identidade única da instância — heartbeat e poll de tasks convergem nela |
97
- | `tags` | `Record<string,string>` | — | tags aplicadas a todos os eventos |
98
- | `sampleRate` | `number` | `1` | amostragem de erros (0..1) |
99
- | `tracesSampleRate` | `number` | `1` | amostragem de transactions (0..1) |
100
- | `maxBreadcrumbs` | `number` | `100` | tamanho do buffer de breadcrumbs |
101
- | `captureUnhandled` | `boolean` | `true` | handlers globais de erro (`uncaughtException`/`unhandledRejection`) |
102
- | `machineMetrics` | `boolean` | `true` | CPU/memória/disco no heartbeat, no poll e em `contexts.machine` dos eventos |
103
- | `heartbeat` | `boolean \| HeartbeatOptions` | `false` | heartbeat periódico (`true` usa defaults) |
104
- | `tasks` | `boolean \| TasksOptions` | `false` | habilita a execução sob demanda (`true` usa defaults) — veja [Tasks](#tasks-execução-sob-demanda) |
105
- | `beforeSend` | `(event) => event \| null` | — | edita/descarta evento (retorne `null` para descartar) |
106
- | `debug` | `boolean` | `false` | loga atividade do SDK |
107
- | `flushIntervalMs` | `number` | `2000` | intervalo do flush automático do transport |
108
- | `maxBatchSize` | `number` | `10` | itens por envelope antes do flush imediato |
109
-
110
- ### Captura de erros e mensagens
111
-
112
- ```js
113
- try {
114
- await processarNota(nota);
115
- } catch (error) {
116
- Hobots.captureException(error, {
117
- type: 'error',
118
- tags: { cliente: 'acme' },
119
- fingerprint: ['sefaz-timeout'], // agrupa manualmente; sem isso o servidor agrupa pelo stack
120
- });
121
- throw error;
122
- }
123
-
124
- Hobots.captureMessage('Lote processado com sucesso', 'success');
125
- ```
126
-
127
- Ambas retornam o `event_id` (ou `undefined` se não inicializado). O hint aceita `type`, `tags`, `extra` e `fingerprint`. Erros **não tratados** são capturados e flushados automaticamente antes do processo sair — desligue com `captureUnhandled: false`. Os eventos aparecem agrupados em issues no módulo **Issues** do app.
128
-
129
- `EventType = 'fatal' | 'error' | 'warning' | 'success'`. Não há `'info'`/`'debug'`: narração não abre issue — para isso existem o log da execução e as breadcrumbs. Use `'success'` para reportar itens concluídos com êxito — contam como eventos, mas **não** viram issue nem disparam alertas.
130
-
131
- ### Breadcrumbs
132
-
133
- Trilha do que aconteceu antes do erro; enviada junto do próximo evento (buffer limitado por `maxBreadcrumbs`, mais antigos descartados).
25
+ ## Em 30 segundos
134
26
 
135
- ```js
136
- Hobots.addBreadcrumb({ category: 'nfe', message: 'Baixou XML da nota 123', level: 'info' });
27
+ ```bash
28
+ export HOBOTS_CLIENT_SECRET=hb_<64 hex> # Configurações → Agentes
29
+ export HOBOTS_INSTANCE_ID=vm-cliente-01 # opcional — default: hostname da máquina
137
30
  ```
138
31
 
139
- A lista **não** é limpa ao enviar um evento e é global. Conceito: [Breadcrumb](./docs/concepts.md#breadcrumb).
140
-
141
- ### Escopo: tags, usuário, contexto, extras
142
-
143
- Dados anexados a **todos** os eventos seguintes:
144
-
145
32
  ```js
146
- Hobots.setTag('regiao', 'nordeste');
147
- Hobots.setTags({ squad: 'automacoes', cliente: 'acme' });
148
- Hobots.setUser({ id: 'op-42', email: 'op@acme.com' });
149
- Hobots.setContext('vm', { cpu: 4, ram: '8GB' });
150
- Hobots.setExtra('ultimoLote', 128);
151
- ```
33
+ import hobots from 'hobots';
152
34
 
153
- Para um escopo **isolado** (sem vazar para o global), use `withScope`:
154
-
155
- ```js
156
- Hobots.withScope((scope) => {
157
- scope.setTag('lote', '128').setFingerprint(['reprocessamento']);
158
- Hobots.captureMessage('Reprocessando lote 128');
35
+ // sob demanda: uma execução por solicitação do app (o task_slug casa com o formulário)
36
+ hobots.register('processar-nfe', async params => {
37
+ hobots.log.info(`lote ${params.lote}`);
38
+ hobots.progress.total(params.notas.length);
39
+ for (const nota of params.notas) {
40
+ await baixar(nota);
41
+ hobots.items.succeeded({ numero_nota: nota }, { id: nota });
42
+ hobots.progress.advance();
43
+ }
159
44
  });
160
- ```
161
45
 
162
- ### Performance: transactions e etapas
163
-
164
- ```js
165
- const tx = Hobots.startTransaction('processar-lote', 'rpa.job');
166
-
167
- const step = tx.startChild('http.client', 'GET /api/notas');
168
- await api.buscarNotas();
169
- step.setData('notas', 42).finish();
170
-
171
- const filho = step.startChild('db.query', 'INSERT INTO notas'); // etapas aninham
172
- await gravar();
173
- filho.finish();
174
-
175
- tx.setStatus('ok'); // ou 'error'
176
- tx.finish(); // só aqui a transaction é enviada (respeitando tracesSampleRate)
177
- ```
178
-
179
- - `startChild(op?, description?)` existe em `Transaction` **e** `Step` (aninhamento arbitrário).
180
- - Métodos encadeáveis: `setStatus`, `setData` (step), `setTag`/`setName` (transaction).
181
- - `finish()` na transaction fecha as etapas abertas e envia o payload; nada é enviado se ela não foi amostrada. A amostragem é decidida no `startTransaction()`.
182
-
183
- Dentro de um handler de task, `startTransaction()` **retorna a transaction da própria run** (`ctx.tx`) em vez de criar uma segunda — cada execução tem exatamente **uma** transaction. Conceitos: [Transaction](./docs/concepts.md#transaction) · [Step](./docs/concepts.md#step).
184
-
185
- ### Heartbeat (uptime)
186
-
187
- Bate "estou vivo" periodicamente; o servidor acusa o robô **offline** quando os batimentos param.
188
-
189
- ```js
190
- Hobots.init({
191
- clientSecret,
192
- heartbeat: { intervalMs: 30_000, name: 'Robô NFe — VM 01' },
46
+ // sempre ligado: o handler é o loop do robô; cada iteração reporta o que fez
47
+ hobots.start('monitor-pasta', async () => {
48
+ while (true) {
49
+ await hobots.sleep(60_000); // cancelável
50
+ for (const arquivo of await novosArquivos()) hobots.items.succeeded({ arquivo });
51
+ hobots.log.info('ciclo ok');
52
+ }
193
53
  });
194
- ```
195
-
196
- `heartbeat: true` usa os defaults. A identidade é o `instanceId` do `init()`; os campos são `intervalMs`, `name` (default: o `instanceId`) e `metadata` — valores em [Tempos e intervalos](./docs/api-reference.md#tempos-e-intervalos). O primeiro batimento sai **na hora** em que o `init()` roda, não depois do primeiro intervalo.
197
-
198
- O heartbeat é **essencial** para agentes só de telemetria; com `tasks: true` ele é **opcional**, porque o poll de tasks já marca a presença do agente no painel. Conceito: [Heartbeat](./docs/concepts.md#heartbeat).
199
54
 
200
- ### Recursos da máquina (CPU, memória, disco)
201
-
202
- 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.
203
-
204
- ```js
205
- Hobots.init({ clientSecret, instanceId: 'vm-01', heartbeat: true }); // coleta ligada
206
- Hobots.init({ clientSecret, instanceId: 'vm-01', machineMetrics: false }); // desliga
55
+ process.on('SIGINT', () => void hobots.close(15_000).then(() => process.exit(0)));
207
56
  ```
208
57
 
209
- O snapshot tem ~600 bytes (teto do servidor: 4 KiB). Formato em [Protocolo → MachineSnapshot](./docs/protocol.md#machinesnapshot).
210
-
211
- ### Flush e close
212
-
213
- ```js
214
- await Hobots.flush(2000); // envia o buffer, mantém o SDK vivo
215
- await Hobots.close(2000); // flush final + para o heartbeat + fecha o transport
216
- ```
217
-
218
- Chame `close` antes do processo sair. Os timers da telemetria são `unref`'d — ela **não** segura o processo vivo sozinha.
58
+ O objeto **`hobots`** é global: `hobots.log`, `hobots.items`, `hobots.progress`, `hobots.transaction`… resolvem a execução **atual** de qualquer módulo do programa — sem passar contexto de mão em mão, mesmo com várias execuções em paralelo. Depois da primeira task ele também existe como `globalThis.hobots`.
219
59
 
220
60
  ---
221
61
 
222
- ## Tasks (execução sob demanda)
62
+ ## Credenciais e identidade
223
63
 
224
- Capability de **execução remota sob demanda**, habilitada com `tasks: true` (ou opções) no `init()`. O agente faz polling no app, executa os handlers registrados quando aparece trabalho e reporta status e logs incrementais — cada execução aparece no módulo **Solicitações** do app. Mesmo Client Secret, mesmo `debug` e o `instanceId` como identidade — nenhum handler global extra é instalado.
64
+ | Env | Opção | O que é |
65
+ | --- | --- | --- |
66
+ | `HOBOTS_CLIENT_SECRET` | `clientSecret` | O **Client Secret do agente** (`hb_<64 hex>`), copiado em **Configurações → Agentes**. Viaja só no header `Authorization: Bearer`, nunca na URL. Ausente ou inválido → `register()`/`start()` lança na hora, citando a env (sem ecoar o valor). |
67
+ | `HOBOTS_INSTANCE_ID` | `instanceId` | Identidade **estável** desta instância do robô (`vm-cliente-01`). Sem ela, o hostname. Heartbeat e tasks convergem nela: **um agente só** no painel. |
68
+ | `HOBOTS_API_URL` | — | Só em desenvolvimento: aponta o SDK para outro host (`http://localhost:3001`). Em produção é sempre `https://api2.hobots.app`. |
225
69
 
226
- ### Ciclo de vida
70
+ A opção passada em `register()`/`start()` tem precedência sobre a env — útil para copiar e colar o snippet do app.
227
71
 
228
- ```js
229
- Hobots.init({
230
- clientSecret: 'hb_<64 hex>',
231
- instanceId: 'vm-cliente-01',
232
- tasks: true, // ou { pollIntervalMs, concurrency, ... }
233
- });
234
-
235
- Hobots.register('processar-nfe', async (params, ctx) => {
236
- ctx.log.info(`processando lote ${params.lote}…`);
237
- ctx.throwIfCanceled(); // cancelamento cooperativo
238
- ctx.log.info('12 notas processadas');
239
- });
240
-
241
- Hobots.start(); // começa o polling e MANTÉM O PROCESSO VIVO
242
-
243
- process.on('SIGINT', () => {
244
- void Hobots.close(15_000).then(() => process.exit(0));
245
- });
246
- ```
247
-
248
- - `init({ tasks })` — cria o agente. Chamar `init()` de novo para o agente anterior e o substitui (o registry de handlers é preservado).
249
- - `register(task, handler)` — `task` é o `task_slug` configurado no formulário do app (**Configurações → Formulários**). Registre **antes** de `start()`. Funciona até antes do `init()`; com as tasks desabilitadas, avisa no console.
250
- - `start()` — começa o polling. Lança se o `init()` não configurou `tasks` ou se nenhuma task foi registrada.
251
- - `stop(timeoutMs?)` — para **só o polling de tasks**: encerra o polling e aguarda as runs ativas (default 30s); a telemetria continua. Retorna `false` se estourou o timeout.
252
- - `close(timeoutMs?)` — encerra **tudo**: agente (aguardando runs em voo) → heartbeat → flush final do transport.
253
-
254
- | Opção (`TasksOptions`) | Tipo | Default | Descrição |
255
- | --- | --- | --- | --- |
256
- | `pollIntervalMs` | `number` | `2000` | intervalo entre polls sem trabalho (mínimo `1000`) |
257
- | `concurrency` | `number` | `1` | runs simultâneas (clamp 1..5) |
258
- | `taskTimeoutMs` | `number` | — | timeout client-side por run (não mata o handler; servidor é o backstop) |
259
- | `logFlushIntervalMs` | `number` | `2000` | intervalo do envio incremental de logs |
260
- | `cancelCheckIntervalMs` | `number` | `5000` | heartbeat de cancelamento por run (clamp 1000..15000) — também é o sinal de vida da run |
261
- | `installExitHandlers` | `boolean` | `true` | handlers de SIGINT/SIGTERM/uncaughtException que reportam as runs ativas como `AgentTerminated` antes do processo morrer; nunca interferem quando o programa tem handler próprio |
262
-
263
- O Client Secret, o `debug` e a identidade do agente (`instanceId`) vêm do `init()` — o heartbeat e o poll de tasks convergem em **um agente** no painel automaticamente.
264
-
265
- ### O handler e o `RunContext`
266
-
267
- ```ts
268
- type TaskHandler<P = Record<string, unknown>> =
269
- (params: P, ctx: RunContext) => void | Promise<void>;
270
-
271
- interface RunContext {
272
- runId: string;
273
- task: string;
274
- log: RunLogger; // ctx.log.debug/info/warn/error/success(message)
275
- items: RunItems; // ctx.items.succeeded/failed/occurrence — a entrega do bot
276
- progress: RunProgressReporter; // ctx.progress.total/advance/set — barra + tempo restante no app
277
- tx: Transaction; // transaction desta run — correlacionada e finalizada pelo SDK
278
- signal: AbortSignal;
279
- throwIfCanceled(): void; // lança RunCanceled se o signal já disparou
280
- sleep(ms: number): Promise<void>; // espera cancelável — rejeita com RunCanceled no abort
281
- }
282
- ```
283
-
284
- - **`params`** — os dados do formulário configurado no app (JSON validado). Tipável: `Hobots.register<{ lote: number }>(...)`.
285
- - **`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 quase em tempo real; também impressos no terminal local, coloridos por nível.
286
- - **`ctx.items`** — registra os **itens que a execução processou/gerou** (a entrega do bot: notas fiscais baixadas, boletos emitidos…), cada um com status `succeeded`, `failed` (falha técnica) ou `occurrence` (ocorrência de negócio, ex.: "nota nº 10 não encontrada") e um `payload` de atributos livres. Aparecem na aba **itens** da solicitação, com totalizadores; as colunas exibidas são configuradas por formulário no app. Guia: [Itens de execução](./docs/tasks.md#itens-de-execução-ctxitems).
287
- - **`ctx.progress`** — **opcional**: declare quantos itens a run vai processar (`ctx.progress.total(n)`) e avance a cada item (`ctx.progress.advance()`); o app mostra barra, percentual e tempo restante estimado na solicitação. Independente de `ctx.items`. Guia: [Progresso](./docs/tasks.md#progresso-da-execução-ctxprogress).
288
- - **`ctx.tx`** — a transaction de performance desta run, já correlacionada e finalizada pelo SDK. Use `ctx.tx.startChild(op, description)` para medir as etapas — elas viram o waterfall da execução no app. `startTransaction()` chamado dentro do handler retorna essa mesma transaction. Conceito: [ctx.tx](./docs/concepts.md#transaction-de-run-ctxtx).
289
- - **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**.
290
- - **`ctx.throwIfCanceled()`** — o ponto de parada de uma linha; lança `RunCanceled` se o `signal` já disparou.
291
- - **`ctx.sleep(ms)`** — espera que respeita o `signal` (rejeita com `RunCanceled` em vez de dormir até o fim).
292
- - **Retorno** — **ignorado**: o SDK não captura nem envia o valor retornado pelo handler (resultados podem conter dados sensíveis e não são coletados).
293
- - **Erro lançado** — a run é reportada como `failed` (mensagem cap 10k, stack 20k).
72
+ ---
294
73
 
295
- > **Dados sensíveis:** `ctx.log` e mensagens/stack de erros **são enviados ao servidor** e ficam visíveis no app. Não inclua senhas, tokens, dados pessoais ou qualquer informação sensível em logs e mensagens de erro.
74
+ ## Os dois modos
296
75
 
297
- ### Itens de execução (`ctx.items`)
76
+ | | `register(task, handler, options?)` | `start(task, handler, options?)` |
77
+ | --- | --- | --- |
78
+ | Quem dispara | Uma **solicitação** no app (formulário com esse `task_slug`, ou agendamento) | O próprio robô, ao iniciar |
79
+ | `params` do handler | O JSON validado do formulário | `{}` |
80
+ | Quantas execuções | Uma por solicitação; `concurrency` em paralelo (1..5) | **Uma só** — o handler é o seu loop; quando ele retorna, a execução fecha |
81
+ | O processo | Fica vivo fazendo polling | Fica vivo pelo heartbeat, mesmo depois do handler retornar |
82
+ | Reexecutar pelo app | Sim | Não — só reiniciando o robô |
298
83
 
299
- ```js
300
- Hobots.register('baixar-notas', async (params, ctx) => {
301
- for (const nota of await listarNotas(params)) {
302
- try {
303
- await baixar(nota);
304
- ctx.items.succeeded({ numero_nota: nota.numero, cnpj_prestador: nota.cnpj }, { id: nota.numero });
305
- } catch (erro) {
306
- ctx.items.failed(String(erro), { numero_nota: nota.numero }, { id: nota.numero });
307
- }
308
- }
309
- // ocorrência de negócio (não é falha do bot):
310
- // ctx.items.occurrence('nota nº 10 não encontrada', { numero_nota: '10' }, { id: '10' })
311
- });
312
- ```
84
+ Os dois podem coexistir no mesmo processo: um monitor sempre ligado + tasks sob demanda, um heartbeat, uma instância. Cada slug só pode ser registrado uma vez.
313
85
 
314
- Chamadas síncronas e bufferizadas (lotes de até 500; dreno antes do reporte final). O `id` opcional é a identidade de negócio do item na run — repetido, é ignorado pelo servidor (registro único, retry idempotente). Limites: 10.000 itens/run, payload 16 KB, mensagem 2.000 caracteres. Detalhes em [Itens de execução](./docs/tasks.md#itens-de-execução-ctxitems).
86
+ > **Modo start e o limite de logs:** o servidor guarda no máximo **1000 linhas de log por execução**. Num loop que roda por dias, logue o que importa (ciclos com novidade, avisos, erros) e registre o trabalho em `hobots.items` — itens não têm esse teto tão baixo (10.000 por execução).
315
87
 
316
- ### Progresso (`ctx.progress`)
88
+ ---
317
89
 
318
- Quando a run percorre uma lista conhecida, declare o total e avance a cada item — a solicitação no app ganha barra de progresso, percentual (`10/100 · 10%`) e **tempo restante estimado**:
90
+ ## O objeto `hobots`
319
91
 
320
- ```js
321
- Hobots.register('baixar-notas', async (params, ctx) => {
322
- const notas = await listarNotas(params);
323
- ctx.progress.total(notas.length);
324
- for (const nota of notas) {
325
- await baixar(nota);
326
- ctx.progress.advance();
327
- }
328
- });
329
- ```
330
-
331
- Opcional e independente de `ctx.items` (use um, outro ou ambos). Sem `total()` nada é enviado. Chamadas síncronas e baratas: o snapshot pega carona nos PATCHes de log e, no pior caso, sai 1 vez por segundo; o PATCH terminal sempre leva o valor final. `ctx.progress.set(current, total?)` define um valor absoluto; `ctx.progress.current` e `ctx.progress.snapshot` leem o estado. Detalhes em [Progresso da execução](./docs/tasks.md#progresso-da-execução-ctxprogress).
92
+ | Membro | O que faz |
93
+ | --- | --- |
94
+ | `hobots.log.debug/info/warn/error/success(msg)` | Log da execução, enviado incrementalmente ao app (e ecoado no terminal). **Nunca lança** — pode ir em `finally`. |
95
+ | `hobots.items.succeeded(payload?, { id }?)` · `.failed(msg, payload?, …)` · `.occurrence(msg, payload?, …)` · `.add({...})` | **A entrega do robô**: cada item processado, com status e atributos (viram colunas no app). Bufferizado, em lotes. |
96
+ | `hobots.progress.total(n)` · `.advance(k = 1)` · `.set(atual, total?)` · `.current` · `.snapshot` | Barra, percentual e tempo restante na solicitação. Sem `total()` nada é enviado. |
97
+ | `hobots.transaction.startChild(op, descrição)` | Uma **step** no waterfall de performance da execução. A transaction da execução já existe e é finalizada pelo SDK. |
98
+ | `hobots.attach(imagem, { caption?, level? })` | Pendura um **print** (PNG/JPEG/WEBP, até 5 MB, 20 por execução) na linha de log. Nunca lança nem bloqueia. |
99
+ | `hobots.captureException(erro, hint?)` | Abre uma **issue** (módulo Issues). `hint.attachment` anexa um print à linha de log correspondente. |
100
+ | `hobots.retry(fn, { retries = 3, delayMs = 1_000, shouldRetry? })` | async-retry ciente do cancelamento da execução. `delayMs` aceita função `(tentativa) => ms`. |
101
+ | `hobots.sleep(ms)` | Espera **cancelável**: rejeita com `RunCanceled` se a execução for cancelada. |
102
+ | `hobots.signal` · `hobots.throwIfCanceled()` | Cancelamento cooperativo manual, para controle fino. |
103
+ | `hobots.runId` · `hobots.task` · `hobots.params` | Identidade da execução atual. |
104
+ | `hobots.register(...)` · `hobots.start(...)` · `hobots.close(timeoutMs?)` | Ciclo de vida do processo. |
105
+
106
+ O handler também recebe o mesmo contexto como 2º argumento (`(params, ctx) => …`), para quem prefere explícito. Fora de qualquer execução (ex.: antes do boot), os membros de contexto caem em fallbacks seguros e avisam uma vez — `hobots.log` continua imprimindo no terminal. Evite `const { log } = hobots` no topo de um módulo: o valor capturado é o de fora de execução; chame `hobots.log.info(...)`.
332
107
 
333
- ### Anexos (screenshots)
108
+ ---
334
109
 
335
- Texto de log conta o que o robô fez; um print conta **como estava a tela**. Os métodos de captura (`Hobots.captureException` e `Hobots.captureMessage`) 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 — no app ela aparece como miniatura logo abaixo dessa linha, clicável para ampliar.
110
+ ## Cancelamento automático
336
111
 
337
- ```js
338
- Hobots.register('processar-nfe', async (params, ctx) => {
339
- await entrar();
340
- Hobots.captureMessage('portal após o login', 'info', {
341
- attachment: await page.screenshot(),
342
- });
343
-
344
- try {
345
- await emitir(params);
346
- } catch (erro) {
347
- Hobots.captureException(erro, {
348
- attachment: { image: await page.screenshot(), caption: 'tela no momento do erro' },
349
- });
350
- throw erro;
351
- }
352
- });
353
- ```
112
+ Uma execução cancelada no app **não mata** o handler — o SDK dispara `hobots.signal` e o próximo ponto de parada lança **`RunCanceled`**:
354
113
 
355
- **O SDK não tira o print** — ele recebe a imagem pronta, de onde você quiser (Playwright, Puppeteer, uma lib de captura de desktop, um arquivo em disco). O `attachment` aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`), um caminho de arquivo (só em Node) ou um objeto `{ image, caption?, contentType? }`.
114
+ - `hobots.items.*`, `hobots.progress.total/advance/set`, `hobots.transaction.startChild()`, `hobots.sleep()` e cada tentativa de `hobots.retry()`.
115
+ - **Nunca `hobots.log.*`** — um log dentro de um `finally` de limpeza não pode explodir.
356
116
 
357
- | Limite | Valor |
358
- | --- | --- |
359
- | formatos | PNG, JPEG, WEBP |
360
- | tamanho por imagem | 5 MB |
361
- | anexos por execução | 20 |
362
- | anexos por linha de log | 4 |
117
+ Num loop de itens, isso costuma bastar: registrar o item processado já é o ponto de parada. Para controle fino, `hobots.throwIfCanceled()` e `hobots.signal` continuam disponíveis. Quando o cancelamento veio do app, o `RunCanceled` é **suprimido** — a execução já está `cancelada` no servidor e nada é reportado por cima. Limpeza (fechar navegador, desfazer estado) vai em `try/finally`.
363
118
 
364
- A linha de log é registrada **na hora**; o upload roda em segundo plano e vai direto para o S3 por URL presignada, sem passar pela API — a captura **nunca bloqueia nem lança** por causa do anexo (falhas viram um aviso no log da run). Fora de uma execução de task não há onde pendurar a imagem: o `attachment` é descartado com um aviso. Detalhes em [Anexos](./docs/tasks.md#anexos-screenshots).
119
+ > **Atenção:** não use `if (hobots.signal.aborted) return;`. Retornar normalmente reporta a execução como **`succeeded`**. É preciso **lançar** — e é o que os pontos de parada fazem.
365
120
 
366
- > **Dados sensíveis:** o print é enviado **exatamente como está** — o SDK não borra nem recorta nada, e telas de portal costumam conter CPF, nome e valores. Capture só o necessário. Solicitantes só veem os anexos quando o projeto está com "mostrar logs para solicitantes" ligado, o mesmo gate dos logs.
121
+ ---
367
122
 
368
- ### Cancelamento cooperativo
123
+ ## Retry
369
124
 
370
- Uma run cancelada no app **não mata** o handler — o SDK apenas dispara `ctx.signal`. Chame **`ctx.throwIfCanceled()`** em loops longos e pontos de parada (e troque esperas longas por `ctx.sleep()`):
125
+ Quando o handler lança, o SDK **tenta de novo na mesma execução**: aviso no log (`tentativa 1/2 falhou: … — nova tentativa em 5000ms`), espera cancelável, re-invocação com os mesmos `params`. Default: **1 tentativa extra** com 5 s. Itens e progresso já enviados permanecem — escreva handlers **idempotentes** (use o `id` de negócio nos itens: repetido, o servidor ignora).
371
126
 
372
127
  ```js
373
- Hobots.register('processar-nfe', async (params, ctx) => {
374
- for (const nota of params.notas) {
375
- ctx.throwIfCanceled(); // lança RunCanceled se cancelada — o handler para aqui
376
- await processar(nota);
377
- }
378
- });
128
+ hobots.register('processar-nfe', handler, { retries: 3, retryDelayMs: attempt => attempt * 10_000 });
379
129
  ```
380
130
 
381
- Quando o cancelamento veio do app, o `RunCanceled` lançado é **suprimido** — a run já está `cancelada` no servidor e nada é reportado por cima. Para limpeza antes de parar (fechar navegador, desfazer estado), use `try/finally`.
382
-
383
- > **Atenção:** não use `if (ctx.signal.aborted) return;`. Retornar normalmente faz o SDK reportar a run como **`succeeded`** — uma run cancelada apareceria como sucesso no app. É preciso **lançar**, e é o que o `throwIfCanceled()` faz.
131
+ Nunca há nova tentativa em **cancelamento**, **timeout** (`TaskTimeout` é terminal) ou execução já cancelada no app. Para uma chamada instável dentro do handler, use `hobots.retry()`.
384
132
 
385
- ### Concorrência
386
-
387
- `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**: re-pesquisa imediatamente enquanto vem trabalho; quando não vem, espera `pollIntervalMs`.
388
-
389
- ### Timeout por run
390
-
391
- `taskTimeoutMs` é **client-side**. No estouro o SDK **não mata** o handler — dispara `ctx.signal`, libera o slot e reporta a run como falha (`TaskTimeout`). Respeite o `signal` para parar de verdade. Sem ele, o backstop é o servidor.
392
-
393
- ### Idempotência (at-least-once até `running`)
133
+ ---
394
134
 
395
- 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).
135
+ ## Opções
396
136
 
397
- 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).
137
+ ```ts
138
+ hobots.register(task, handler, options?: RegisterOptions);
139
+ hobots.start(task, handler, options?: StartOptions);
140
+ ```
141
+
142
+ | Opção | Default | Descrição |
143
+ | --- | --- | --- |
144
+ | `clientSecret` | env `HOBOTS_CLIENT_SECRET` | Client Secret do agente |
145
+ | `instanceId` | env `HOBOTS_INSTANCE_ID` → hostname | Identidade da instância |
146
+ | `environment` | `'production'` | `development` \| `testing` \| `staging` \| `production` — decide quem é alertado |
147
+ | `release` | — | Versão do programa, ex. `robo-nfe@1.4.2` |
148
+ | `tags` | — | Tags de todos os eventos |
149
+ | `debug` | `false` | Loga a atividade do SDK |
150
+ | `machineMetrics` | `true` | CPU/memória/disco no heartbeat e em cada evento |
151
+ | `captureUnhandled` | `true` | `uncaughtException`/`unhandledRejection` viram issues |
152
+ | `retries` | `1` | Tentativas extras do handler (0 desliga) |
153
+ | `retryDelayMs` | `5_000` | ms fixo ou `(tentativa) => ms` |
154
+ | `timeoutMs` | — (infinito) | Timeout por tentativa; terminal (`TaskTimeout`). O handler não é morto: o signal dispara e o slot é liberado |
155
+ | `concurrency` (register) | `1` | Execuções simultâneas desta task (1..5) |
156
+ | `pollIntervalMs` (register) | `2_000` | Intervalo entre polls sem trabalho (mín. 1_000) |
157
+ | avançadas: `installExitHandlers`, `logFlushIntervalMs`, `cancelCheckIntervalMs`, `cancelGraceMs` | `true`, `2_000`, `5_000`, `10_000` | Ver [Referência de API](./docs/api-reference.md) |
158
+
159
+ As opções de **processo** (`clientSecret`, `instanceId`, `environment`, `release`, `tags`, `debug`, `machineMetrics`, `captureUnhandled`, `installExitHandlers`, `pollIntervalMs`) valem as da **primeira** chamada; chamadas seguintes com valor diferente avisam e são ignoradas.
398
160
 
399
161
  ---
400
162
 
401
- ## Encerramento gracioso
163
+ ## Encerramento
402
164
 
403
- `close(timeoutMs?)` encerra tudo na ordem certa: para o polling de tasks (aguardando a run atual), para o heartbeat e faz o flush final do transport.
165
+ O heartbeat mantém o processo vivo por design. `hobots.close(timeoutMs = 30_000)` para o polling, aguarda as execuções em voo, para o heartbeat (libera o processo) e faz o flush final:
404
166
 
405
167
  ```js
406
- // só telemetria: não segura o processo, mas faça o flush final para não perder eventos
407
- await Hobots.close(2_000);
408
-
409
- // com tasks: segura o processo por design — aguarde a run atual
410
168
  process.on('SIGINT', () => {
411
- void Hobots.close(15_000).then(() => process.exit(0));
169
+ void hobots.close(15_000).then(() => process.exit(0));
412
170
  });
413
171
  ```
414
172
 
415
- Para parar de aceitar execuções **sem** derrubar a telemetria, use `stop(timeoutMs?)`.
416
-
417
- ---
418
-
419
- ## Entry points / exports
420
-
421
- | Import | Conteúdo |
422
- | --- | --- |
423
- | `hobots` | Tudo: `init`, telemetria (captura, transactions, heartbeat), tasks (`register`/`start`/`stop`), `close`, classes e tipos. |
424
- | `hobots/protocol` | Tipos do wire format (telemetria e tasks). |
173
+ Sem handler próprio de sinal, um `SIGINT`/`SIGTERM` (ou erro fatal) com execuções ativas reporta cada uma como `failed`/`AgentTerminated` antes do processo sair — o app mostra a falha na hora.
425
174
 
426
- Empacotado em **ESM e CommonJS** com **definições de tipos** (`.d.ts`) para todas as entradas.
175
+ > **Dados sensíveis:** logs, itens, mensagens de erro e prints são enviados ao servidor e ficam visíveis no app. O SDK redige padrões óbvios de segredo, mas não borra prints nem filtra dados pessoais. Capture só o necessário.
427
176
 
428
177
  ---
429
178
 
430
- ## Documentação completa
179
+ ## Documentação
431
180
 
432
- Guias detalhados acompanham o pacote em [`docs/`](./docs/README.md).
433
-
434
- Novo no vocabulário de observabilidade (transaction, step, breadcrumb, tag, context, extra, flush, close)? Comece por [Conceitos](./docs/concepts.md) — as outras páginas assumem esses termos.
435
-
436
- - [Conceitos](./docs/concepts.md) — o que cada coisa é, por que existe e quando escolher uma em vez da outra.
437
- - [Getting started](./docs/getting-started.md) — instalação, Client Secret, primeiro `init`, telemetria e tasks.
438
- - [Telemetria](./docs/telemetry.md) — erros, breadcrumbs, escopo, transactions/etapas, heartbeat.
439
- - [Tasks](./docs/tasks.md) — handlers, `RunContext`, cancelamento, concorrência, idempotência.
440
- - [Referência de API](./docs/api-reference.md) — todas as funções e tabelas de opções com defaults.
441
- - [Protocolo & transporte](./docs/protocol.md) — o Client Secret, endpoints, envelope, retries.
442
-
443
- ---
181
+ - [Getting started](./docs/getting-started.md) — instalação, envs, primeira task nos dois modos.
182
+ - [Tasks](./docs/tasks.md) — handler, `hobots.*`, itens, progresso, anexos, cancelamento, retry, concorrência, idempotência.
183
+ - [Conceitos](./docs/concepts.md) — o vocabulário: task, execução, evento, transaction, step, heartbeat, instância, claim…
184
+ - [Referência de API](./docs/api-reference.md) — assinaturas, tabelas de opções e tempos.
185
+ - [Protocolo & transporte](./docs/protocol.md) — endpoints, envelope, retries, claim.
186
+ - Exemplos runnable em [`packages/hobots-sdk-demos`](../hobots-sdk-demos).
444
187
 
445
188
  ## Licença
446
189