hobots 4.0.0 → 4.1.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
@@ -91,7 +91,7 @@ A opção passada em `register()`/`start()` tem precedência sobre a env — út
91
91
 
92
92
  **Um processo = um agente = uma chamada.** A segunda chamada a `register()`/`start()` lança (`o agente já foi iniciado`). Um robô que precisa monitorar uma pasta *e* atender solicitações são dois agentes — dois processos, cada um com o seu Client Secret.
93
93
 
94
- > **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).
94
+ > **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 teto por execução (só o armazenamento do plano limita). As steps de uma run que não termina só chegam à Central com `hobots.flush()` — chame-o ao fim de cada rodada que processou itens.
95
95
 
96
96
  ---
97
97
 
@@ -103,6 +103,7 @@ A opção passada em `register()`/`start()` tem precedência sobre a env — út
103
103
  | `hobots.items.succeeded(payload?, { id, image }?)` · `.failed(msg, payload?, …)` · `.occurrence(msg, payload?, …)` · `.add({...})` | **A entrega do robô**: cada item processado, com status e atributos (viram colunas no app — declare-as com a opção `items: { columns: [...] }` do `register`/`start`). `image` pendura um print no item. Bufferizado, em lotes. |
104
104
  | `hobots.items.each(lista, async (el, item) => …, { id, payload }?)` | **Loop com o contrato garantido**: cada elemento vira step + item; exceção no handler = item `failed` com a mensagem e o loop **segue** até o fim; progresso automático. Só cancelamento/timeout interrompem. |
105
105
  | `hobots.progress.total(n)` · `.advance(k = 1)` · `.set(atual, total?)` · `.current` · `.snapshot` | Barra, percentual e tempo restante na execução. Sem `total()` nada é enviado. |
106
+ | `hobots.flush(timeoutMs?)` | **Entrega já** o que a execução fez: as steps raiz finalizadas viram transactions parciais (waterfall na Central sem esperar a run terminar) e itens, logs e progresso pendentes saem sem esperar o throttle. O bot decide quando — num agente `start()`, ao fim de cada rodada que processou itens. Aguarda com teto (15 s) e nunca lança. |
106
107
  | `hobots.transaction.startChild(op, descrição)` | Uma **step** no waterfall de performance da execução. A transaction da execução (`run`) já existe e é finalizada pelo SDK. `step.setStatus(status, msg?, { image })` / `setError(err, msg?, { image })` penduram um print na step. |
107
108
  | `hobots.image(bytes \| caminho, { caption?, contentType? })` | Embrulha um **print** (PNG/JPEG/WEBP, até 5 MB, 20 por execução) para ir junto de um log, item ou step. Puro: o upload roda em segundo plano quando o evento é registrado; nunca lança nem bloqueia. |
108
109
  | `hobots.captureException(erro, hint?)` | Abre uma **issue** (módulo Issues). `hint.attachment` anexa um print à linha de log correspondente. |
@@ -9,7 +9,6 @@ var protocol_limits_default = {
9
9
  MAX_LOGS_PER_PATCH: 100,
10
10
  MAX_ERROR_MESSAGE_CHARS: 1e4,
11
11
  MAX_ERROR_STACK_CHARS: 2e4,
12
- MAX_ITEMS_PER_RUN: 1e4,
13
12
  MAX_ITEMS_PER_REQUEST: 500,
14
13
  MAX_ITEM_MESSAGE_CHARS: 2e3,
15
14
  MAX_ITEM_PAYLOAD_BYTES: 16384,
@@ -28,4 +27,4 @@ var WIRE_LIMITS = protocol_limits_default;
28
27
  export {
29
28
  WIRE_LIMITS
30
29
  };
31
- //# sourceMappingURL=chunk-5WIIACWC.js.map
30
+ //# sourceMappingURL=chunk-HKCDMUA3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../protocol-limits.json","../src/wire-limits.ts"],"sourcesContent":["{\n \"MAX_ITEMS_PER_ENVELOPE\": 50,\n \"HEARTBEAT_DEFAULT_INTERVAL_MS\": 30000,\n \"HEARTBEAT_MIN_INTERVAL_MS\": 1000,\n \"HEARTBEAT_MAX_INTERVAL_MS\": 900000,\n \"MAX_CLAIM_PER_POLL\": 5,\n \"MAX_LOG_MESSAGE_CHARS\": 10000,\n \"MAX_LOGS_PER_PATCH\": 100,\n \"MAX_ERROR_MESSAGE_CHARS\": 10000,\n \"MAX_ERROR_STACK_CHARS\": 20000,\n \"MAX_ITEMS_PER_REQUEST\": 500,\n \"MAX_ITEM_MESSAGE_CHARS\": 2000,\n \"MAX_ITEM_PAYLOAD_BYTES\": 16384,\n \"MAX_ITEM_COLUMNS\": 12,\n \"MAX_ITEM_COLUMN_LABEL_CHARS\": 60,\n \"MAX_ATTACHMENT_BYTES\": 5242880,\n \"MAX_ATTACHMENTS_PER_RUN\": 20,\n \"MAX_ATTACHMENTS_PER_LOG_ENTRY\": 4,\n \"MAX_CAPTION_CHARS\": 200,\n \"MAX_IMAGE_DIMENSION_PX\": 20000\n}\n","import limits from '../protocol-limits.json';\n\n/**\n * Limites do wire SDK ↔ servidor — fonte ÚNICA (protocol-limits.json): o SDK\n * Node lê daqui, a @hobots/api importa de `hobots/protocol` e o SDK Python\n * confere a paridade num teste que lê o mesmo JSON. Mudar um limite = mudar o\n * JSON (e, no Python, a constante espelhada — o teste acusa).\n */\nexport const WIRE_LIMITS = limits as Readonly<typeof limits>;\nexport type WireLimits = typeof WIRE_LIMITS;\n"],"mappings":";AAAA;AAAA,EACE,wBAA0B;AAAA,EAC1B,+BAAiC;AAAA,EACjC,2BAA6B;AAAA,EAC7B,2BAA6B;AAAA,EAC7B,oBAAsB;AAAA,EACtB,uBAAyB;AAAA,EACzB,oBAAsB;AAAA,EACtB,yBAA2B;AAAA,EAC3B,uBAAyB;AAAA,EACzB,uBAAyB;AAAA,EACzB,wBAA0B;AAAA,EAC1B,wBAA0B;AAAA,EAC1B,kBAAoB;AAAA,EACpB,6BAA+B;AAAA,EAC/B,sBAAwB;AAAA,EACxB,yBAA2B;AAAA,EAC3B,+BAAiC;AAAA,EACjC,mBAAqB;AAAA,EACrB,wBAA0B;AAC5B;;;ACZO,IAAM,cAAc;","names":[]}
package/dist/index.cjs CHANGED
@@ -96,7 +96,7 @@ function scrubSecrets(text) {
96
96
 
97
97
  // src/utils.ts
98
98
  var SDK_NAME = "hobots";
99
- var SDK_VERSION = true ? "4.0.0" : "0.0.0-dev";
99
+ var SDK_VERSION = true ? "4.1.0" : "0.0.0-dev";
100
100
  function isNode() {
101
101
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
102
102
  }
@@ -281,7 +281,6 @@ var protocol_limits_default = {
281
281
  MAX_LOGS_PER_PATCH: 100,
282
282
  MAX_ERROR_MESSAGE_CHARS: 1e4,
283
283
  MAX_ERROR_STACK_CHARS: 2e4,
284
- MAX_ITEMS_PER_RUN: 1e4,
285
284
  MAX_ITEMS_PER_REQUEST: 500,
286
285
  MAX_ITEM_MESSAGE_CHARS: 2e3,
287
286
  MAX_ITEM_PAYLOAD_BYTES: 16384,
@@ -1414,7 +1413,6 @@ var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
1414
1413
  var MAX_LOG_MESSAGE_CHARS = WIRE_LIMITS.MAX_LOG_MESSAGE_CHARS;
1415
1414
  var MAX_LOGS_PER_PATCH = WIRE_LIMITS.MAX_LOGS_PER_PATCH;
1416
1415
  var FLUSH_AT_BUFFERED = 20;
1417
- var MAX_ITEMS_PER_RUN = WIRE_LIMITS.MAX_ITEMS_PER_RUN;
1418
1416
  var MAX_ITEMS_PER_REQUEST = WIRE_LIMITS.MAX_ITEMS_PER_REQUEST;
1419
1417
  var MAX_ITEM_MESSAGE_CHARS = WIRE_LIMITS.MAX_ITEM_MESSAGE_CHARS;
1420
1418
  var MAX_ITEM_PAYLOAD_BYTES = WIRE_LIMITS.MAX_ITEM_PAYLOAD_BYTES;
@@ -1436,6 +1434,7 @@ var RunReporter = class {
1436
1434
  this.api = api;
1437
1435
  this.runId = runId;
1438
1436
  this.logger = logger;
1437
+ this.tx = tx;
1439
1438
  this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
1440
1439
  this.cancelCheckIntervalMs = Math.min(
1441
1440
  MAX_CANCEL_CHECK_INTERVAL_MS,
@@ -1519,6 +1518,7 @@ var RunReporter = class {
1519
1518
  items,
1520
1519
  progress,
1521
1520
  transaction: tx,
1521
+ flush: (timeoutMs) => this.flush(timeoutMs),
1522
1522
  signal,
1523
1523
  throwIfCanceled: checkCanceled,
1524
1524
  sleep: sleep2,
@@ -1528,6 +1528,7 @@ var RunReporter = class {
1528
1528
  api;
1529
1529
  runId;
1530
1530
  logger;
1531
+ tx;
1531
1532
  context;
1532
1533
  buffer = [];
1533
1534
  dead = false;
@@ -1545,7 +1546,6 @@ var RunReporter = class {
1545
1546
  itemsChain = Promise.resolve();
1546
1547
  itemsTimer;
1547
1548
  itemSeq = 0;
1548
- itemCount = 0;
1549
1549
  /** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
1550
1550
  itemsUnsupported = false;
1551
1551
  /** Cap de itens por run atingido (client-side ou 429) — degrada com um aviso só. */
@@ -1668,6 +1668,21 @@ var RunReporter = class {
1668
1668
  this.enqueue({ status: "running", logs: logs.slice(i, i + MAX_LOGS_PER_PATCH) });
1669
1669
  }
1670
1670
  }
1671
+ /**
1672
+ * `hobots.flush()`: o bot decide quando entregar o que já fez — steps raiz finalizadas
1673
+ * viram transactions parciais da run, e itens, logs e progresso pendentes saem já, sem
1674
+ * esperar o throttle. Aguarda a entrega com teto. Nunca lança (pode ir num `finally`).
1675
+ */
1676
+ async flush(timeoutMs = ITEMS_DRAIN_MS) {
1677
+ this.tx.flush();
1678
+ this.flushItems();
1679
+ if (this.progressDirty) this.flushProgress();
1680
+ else this.flushLogs();
1681
+ await Promise.race([
1682
+ Promise.all([this.itemsChain, this.chain, this.tx.drain(timeoutMs)]).catch(() => void 0),
1683
+ new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
1684
+ ]);
1685
+ }
1671
1686
  /** Primeiro PATCH (claimed → running). Retorna false se a run já morreu (cancelada). */
1672
1687
  async reportRunning() {
1673
1688
  this.enqueue({ status: "running" });
@@ -1778,14 +1793,6 @@ var RunReporter = class {
1778
1793
  */
1779
1794
  addItem(status, message, payload, id, image3) {
1780
1795
  if (this.itemsClosed || this.itemsUnsupported) return;
1781
- if (this.itemCount >= MAX_ITEMS_PER_RUN) {
1782
- if (!this.itemsQuotaExceeded) {
1783
- this.itemsQuotaExceeded = true;
1784
- this.log("warning", `itens descartados: limite de ${MAX_ITEMS_PER_RUN} por execu\xE7\xE3o atingido`);
1785
- }
1786
- return;
1787
- }
1788
- this.itemCount++;
1789
1796
  let scrubbedPayload;
1790
1797
  if (payload) {
1791
1798
  scrubbedPayload = scrubPayloadValue(payload, 0);
@@ -2080,6 +2087,7 @@ function currentRunReporter() {
2080
2087
  }
2081
2088
 
2082
2089
  // src/transaction.ts
2090
+ var FLUSH_TRANSACTION_OP = "hobots.run.flush";
2083
2091
  var MAX_STEP_MESSAGE_CHARS = 2e3;
2084
2092
  function stepMessage(text) {
2085
2093
  return truncate(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
@@ -2301,6 +2309,60 @@ var Transaction = class {
2301
2309
  this.name = name;
2302
2310
  return this;
2303
2311
  }
2312
+ /**
2313
+ * Envia já as steps raiz finalizadas (e todas as descendentes, fechando as que
2314
+ * ficaram abertas) como transactions parciais da mesma run (`op` hobots.run.flush,
2315
+ * `event_id` novo, até 200 steps cada) e as tira da memória. Steps raiz ainda abertas
2316
+ * ficam para o próximo flush ou para o `finish()`. É assim que um agente sempre
2317
+ * ligado (`start()`), cuja run não termina, entrega as etapas enquanto processa.
2318
+ * Devolve quantas steps foram enviadas.
2319
+ */
2320
+ flush() {
2321
+ if (this.finished) return 0;
2322
+ const ready = /* @__PURE__ */ new Set();
2323
+ for (const step of this.steps) {
2324
+ if (step.parentStepId === this.stepId && step.endTimestamp) ready.add(step.stepId);
2325
+ }
2326
+ if (ready.size === 0) return 0;
2327
+ const batch = [];
2328
+ const remaining = [];
2329
+ for (const step of this.steps) {
2330
+ if (ready.has(step.stepId) || ready.has(step.parentStepId)) {
2331
+ ready.add(step.stepId);
2332
+ batch.push(step);
2333
+ } else {
2334
+ remaining.push(step);
2335
+ }
2336
+ }
2337
+ for (const step of batch) step.finish();
2338
+ this.steps.splice(0, this.steps.length, ...remaining);
2339
+ if (this.client) {
2340
+ for (let i = 0; i < batch.length; i += MAX_STEPS) {
2341
+ const chunk = batch.slice(i, i + MAX_STEPS);
2342
+ const startTimestamp = chunk.reduce((min, step) => step.startTimestamp < min ? step.startTimestamp : min, chunk[0].startTimestamp);
2343
+ const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
2344
+ this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, startTimestamp, nowISO(), chunk));
2345
+ }
2346
+ }
2347
+ return batch.length;
2348
+ }
2349
+ /** Entrega já (com teto) o que está na fila do transport — as transactions parciais inclusive. */
2350
+ async drain(timeoutMs) {
2351
+ await this.client?.flush(timeoutMs);
2352
+ }
2353
+ payload(eventId, op, status, startTimestamp, timestamp, steps) {
2354
+ return {
2355
+ event_id: eventId,
2356
+ name: this.name,
2357
+ op,
2358
+ status,
2359
+ start_timestamp: startTimestamp,
2360
+ timestamp,
2361
+ steps: steps.map((step) => step.toJSON()),
2362
+ tags: Object.keys(this.tags).length > 0 ? { ...this.tags } : void 0,
2363
+ run_id: this.runId
2364
+ };
2365
+ }
2304
2366
  finish() {
2305
2367
  if (this.finished) return;
2306
2368
  this.finished = true;
@@ -2315,18 +2377,15 @@ var Transaction = class {
2315
2377
  this.status
2316
2378
  );
2317
2379
  if (!this.client) return;
2318
- const payload = {
2319
- event_id: this.eventId,
2320
- name: this.name,
2321
- op: this.op,
2322
- status: this.status,
2323
- start_timestamp: this.startTimestamp,
2324
- timestamp: endTimestamp,
2325
- steps: this.steps.map((step) => step.toJSON()),
2326
- tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
2327
- run_id: this.runId
2328
- };
2329
- this.client.sendTransaction(payload);
2380
+ const steps = this.steps;
2381
+ let first = 0;
2382
+ while (steps.length - first > MAX_STEPS) {
2383
+ const chunk = steps.slice(first, first + MAX_STEPS);
2384
+ const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
2385
+ this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, chunk[0].startTimestamp, endTimestamp, chunk));
2386
+ first += MAX_STEPS;
2387
+ }
2388
+ this.client.sendTransaction(this.payload(this.eventId, this.op, this.status, this.startTimestamp, endTimestamp, steps.slice(first)));
2330
2389
  }
2331
2390
  };
2332
2391
 
@@ -3031,6 +3090,7 @@ var hobots = Object.defineProperties({}, {
3031
3090
  signal: getter("signal", () => neverAborts),
3032
3091
  throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
3033
3092
  sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
3093
+ flush: method((timeoutMs) => resolve("flush")?.flush(timeoutMs) ?? Promise.resolve()),
3034
3094
  retry: method((fn, options) => {
3035
3095
  const reporter = currentReporter();
3036
3096
  return reporter ? reporter.context.retry(fn, options) : retryWithBackoff(fn, options);