hobots 1.9.2 → 2.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/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData, RunAttachmentContentType } from './protocol.cjs';
2
- export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.cjs';
1
+ import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData, RunItemStatus, RunAttachmentContentType } from './protocol.cjs';
2
+ export { Envelope, EnvelopeItem, ExceptionData, InstanceInfo, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunItemInput, RunItemsCreateRequest, RunItemsCreateResponse, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.cjs';
3
3
 
4
4
  /** Aceita tudo que o handler pode ter em mãos: bytes ou um caminho de arquivo (só Node). */
5
5
  type AttachInput = Uint8Array | ArrayBuffer | string;
@@ -50,6 +50,8 @@ interface Logger {
50
50
  declare class Client {
51
51
  #private;
52
52
  readonly logger: Logger;
53
+ /** Eco de lifecycle (Transaction.emit) e afins só imprimem no terminal com debug ligado. */
54
+ readonly debug: boolean;
53
55
  private readonly transport;
54
56
  constructor(options: InitOptions, config?: ResolvedConfig);
55
57
  get options(): InitOptions;
@@ -110,9 +112,10 @@ declare class Transaction {
110
112
  /** Correlaciona a uma execução de task — preenchido automaticamente em ctx.tx */
111
113
  runId?: string | undefined);
112
114
  /**
113
- * Ecoa no terminal o início/fim de uma step ou transaction (sempre, independente de
114
- * sampling ou do debug) e espelha a linha no sink de lifecycle, se conectado — é
115
- * assim que os eventos de performance também aparecem nos logs da run no app.
115
+ * Registra o início/fim de uma step ou transaction: espelha a linha no sink de
116
+ * lifecycle, se conectado (é assim que os eventos de performance aparecem nos
117
+ * logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
118
+ * escreve no stdout do processo do usuário sem permissão.
116
119
  */
117
120
  emit(kind: 'transaction' | 'step', phase: 'start' | 'finish', label: string, startTs?: string, endTs?: string, status?: StepStatus | string): void;
118
121
  /** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
@@ -167,11 +170,41 @@ interface RunLogger {
167
170
  error(message: string): void;
168
171
  success(message: string): void;
169
172
  }
173
+ interface RunItemOptions {
174
+ /**
175
+ * Identidade de negócio do item na run (ex.: nº da nota fiscal). Repetido na
176
+ * mesma run = ignorado pelo servidor (registro único, retry idempotente).
177
+ * Omitido: o SDK gera um id único e cada chamada vira um item novo.
178
+ */
179
+ id?: string;
180
+ }
181
+ /**
182
+ * Registro dos itens que a execução processou/gerou (a entrega do bot: notas
183
+ * fiscais baixadas, boletos emitidos...). Chamadas são síncronas — os itens são
184
+ * bufferizados e enviados em lotes; o agente drena o buffer antes do reporte final.
185
+ */
186
+ interface RunItems {
187
+ /** Forma genérica — os atalhos succeeded/failed/occurrence cobrem o dia a dia. */
188
+ add(item: {
189
+ status: RunItemStatus;
190
+ id?: string;
191
+ message?: string;
192
+ payload?: Record<string, unknown>;
193
+ }): void;
194
+ /** Item processado com sucesso. `payload` = atributos do item (viram colunas na plataforma). */
195
+ succeeded(payload?: Record<string, unknown>, options?: RunItemOptions): void;
196
+ /** Falha técnica ao processar o item — `message` é o motivo exibido na plataforma. */
197
+ failed(message: string, payload?: Record<string, unknown>, options?: RunItemOptions): void;
198
+ /** Ocorrência de negócio (ex.: "nota nº 10 não encontrada") — não é falha do bot. */
199
+ occurrence(message: string, payload?: Record<string, unknown>, options?: RunItemOptions): void;
200
+ }
170
201
  interface RunContext {
171
202
  runId: string;
172
203
  task: string;
173
204
  /** Log bufferizado — enviado ao servidor em PATCHes incrementais com throttle */
174
205
  log: RunLogger;
206
+ /** Itens processados/gerados pela execução — a entrega do bot, com status e atributos */
207
+ items: RunItems;
175
208
  /**
176
209
  * Transaction de performance desta execução — já correlacionada à run e finalizada
177
210
  * automaticamente ao fim do handler. Use `ctx.tx.startChild(op, description)` pra medir
@@ -303,4 +336,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
303
336
 
304
337
  declare function scrubSecrets(text: string): string;
305
338
 
306
- export { type AttachInput, Breadcrumb, type BreadcrumbInput, type CaptureAttachment, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
339
+ export { type AttachInput, Breadcrumb, type BreadcrumbInput, type CaptureAttachment, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, type RunItemOptions, RunItemStatus, type RunItems, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData, RunAttachmentContentType } from './protocol.js';
2
- export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.js';
1
+ import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData, RunItemStatus, RunAttachmentContentType } from './protocol.js';
2
+ export { Envelope, EnvelopeItem, ExceptionData, InstanceInfo, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunItemInput, RunItemsCreateRequest, RunItemsCreateResponse, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.js';
3
3
 
4
4
  /** Aceita tudo que o handler pode ter em mãos: bytes ou um caminho de arquivo (só Node). */
5
5
  type AttachInput = Uint8Array | ArrayBuffer | string;
@@ -50,6 +50,8 @@ interface Logger {
50
50
  declare class Client {
51
51
  #private;
52
52
  readonly logger: Logger;
53
+ /** Eco de lifecycle (Transaction.emit) e afins só imprimem no terminal com debug ligado. */
54
+ readonly debug: boolean;
53
55
  private readonly transport;
54
56
  constructor(options: InitOptions, config?: ResolvedConfig);
55
57
  get options(): InitOptions;
@@ -110,9 +112,10 @@ declare class Transaction {
110
112
  /** Correlaciona a uma execução de task — preenchido automaticamente em ctx.tx */
111
113
  runId?: string | undefined);
112
114
  /**
113
- * Ecoa no terminal o início/fim de uma step ou transaction (sempre, independente de
114
- * sampling ou do debug) e espelha a linha no sink de lifecycle, se conectado — é
115
- * assim que os eventos de performance também aparecem nos logs da run no app.
115
+ * Registra o início/fim de uma step ou transaction: espelha a linha no sink de
116
+ * lifecycle, se conectado (é assim que os eventos de performance aparecem nos
117
+ * logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
118
+ * escreve no stdout do processo do usuário sem permissão.
116
119
  */
117
120
  emit(kind: 'transaction' | 'step', phase: 'start' | 'finish', label: string, startTs?: string, endTs?: string, status?: StepStatus | string): void;
118
121
  /** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
@@ -167,11 +170,41 @@ interface RunLogger {
167
170
  error(message: string): void;
168
171
  success(message: string): void;
169
172
  }
173
+ interface RunItemOptions {
174
+ /**
175
+ * Identidade de negócio do item na run (ex.: nº da nota fiscal). Repetido na
176
+ * mesma run = ignorado pelo servidor (registro único, retry idempotente).
177
+ * Omitido: o SDK gera um id único e cada chamada vira um item novo.
178
+ */
179
+ id?: string;
180
+ }
181
+ /**
182
+ * Registro dos itens que a execução processou/gerou (a entrega do bot: notas
183
+ * fiscais baixadas, boletos emitidos...). Chamadas são síncronas — os itens são
184
+ * bufferizados e enviados em lotes; o agente drena o buffer antes do reporte final.
185
+ */
186
+ interface RunItems {
187
+ /** Forma genérica — os atalhos succeeded/failed/occurrence cobrem o dia a dia. */
188
+ add(item: {
189
+ status: RunItemStatus;
190
+ id?: string;
191
+ message?: string;
192
+ payload?: Record<string, unknown>;
193
+ }): void;
194
+ /** Item processado com sucesso. `payload` = atributos do item (viram colunas na plataforma). */
195
+ succeeded(payload?: Record<string, unknown>, options?: RunItemOptions): void;
196
+ /** Falha técnica ao processar o item — `message` é o motivo exibido na plataforma. */
197
+ failed(message: string, payload?: Record<string, unknown>, options?: RunItemOptions): void;
198
+ /** Ocorrência de negócio (ex.: "nota nº 10 não encontrada") — não é falha do bot. */
199
+ occurrence(message: string, payload?: Record<string, unknown>, options?: RunItemOptions): void;
200
+ }
170
201
  interface RunContext {
171
202
  runId: string;
172
203
  task: string;
173
204
  /** Log bufferizado — enviado ao servidor em PATCHes incrementais com throttle */
174
205
  log: RunLogger;
206
+ /** Itens processados/gerados pela execução — a entrega do bot, com status e atributos */
207
+ items: RunItems;
175
208
  /**
176
209
  * Transaction de performance desta execução — já correlacionada à run e finalizada
177
210
  * automaticamente ao fim do handler. Use `ctx.tx.startChild(op, description)` pra medir
@@ -303,4 +336,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
303
336
 
304
337
  declare function scrubSecrets(text: string): string;
305
338
 
306
- export { type AttachInput, Breadcrumb, type BreadcrumbInput, type CaptureAttachment, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
339
+ export { type AttachInput, Breadcrumb, type BreadcrumbInput, type CaptureAttachment, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, type RunItemOptions, RunItemStatus, type RunItems, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
package/dist/index.js CHANGED
@@ -103,7 +103,7 @@ function scrubSecrets(text) {
103
103
 
104
104
  // src/utils.ts
105
105
  var SDK_NAME = "hobots";
106
- var SDK_VERSION = true ? "1.9.1" : "0.0.0-dev";
106
+ var SDK_VERSION = true ? "2.0.0" : "0.0.0-dev";
107
107
  function isNode() {
108
108
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
109
109
  }
@@ -296,7 +296,8 @@ var Transport = class {
296
296
  return;
297
297
  }
298
298
  if (this.buffer.length >= this.maxQueueSize) {
299
- this.buffer.shift();
299
+ const dropIndex = this.buffer.findIndex((queued) => queued.type !== "heartbeat");
300
+ this.buffer.splice(dropIndex === -1 ? 0 : dropIndex, 1);
300
301
  this.logger.warn("fila cheia, item mais antigo descartado");
301
302
  }
302
303
  this.buffer.push(item);
@@ -387,6 +388,8 @@ function runtimeContexts() {
387
388
  }
388
389
  var Client = class {
389
390
  logger;
391
+ /** Eco de lifecycle (Transaction.emit) e afins só imprimem no terminal com debug ligado. */
392
+ debug;
390
393
  // campos privados + getters de protótipo: o clientSecret não aparece em
391
394
  // JSON.stringify/serialização enumerável do Client (ex.: um objeto capturado
392
395
  // que referencie o client), mas client.options/config seguem funcionando
@@ -395,7 +398,8 @@ var Client = class {
395
398
  transport;
396
399
  constructor(options, config = resolveConfig(options)) {
397
400
  this.#options = options;
398
- this.logger = makeLogger(!!options.debug);
401
+ this.debug = !!options.debug;
402
+ this.logger = makeLogger(this.debug);
399
403
  this.#config = config;
400
404
  this.transport = new Transport({
401
405
  endpoint: this.#config.ingestEndpoint,
@@ -482,6 +486,8 @@ var Client = class {
482
486
 
483
487
  // src/heartbeat.ts
484
488
  var DEFAULT_INTERVAL_MS = 3e4;
489
+ var MIN_REPORTED_INTERVAL_MS = 1e3;
490
+ var MAX_REPORTED_INTERVAL_MS = 9e5;
485
491
  var Heartbeat = class {
486
492
  constructor(client2, options = {}, instanceId) {
487
493
  this.client = client2;
@@ -498,12 +504,22 @@ var Heartbeat = class {
498
504
  start() {
499
505
  if (this.timer) return;
500
506
  this.beat();
501
- this.timer = setInterval(() => this.beat(), this.intervalMs);
507
+ this.schedule();
508
+ }
509
+ /** Jitter de ±10%: uma frota iniciada junto não sincroniza todos os beats no mesmo segundo. */
510
+ schedule() {
511
+ this.timer = setTimeout(
512
+ () => {
513
+ this.beat();
514
+ this.schedule();
515
+ },
516
+ this.intervalMs * (0.9 + Math.random() * 0.2)
517
+ );
502
518
  unrefTimer(this.timer);
503
519
  }
504
520
  stop() {
505
521
  if (this.timer) {
506
- clearInterval(this.timer);
522
+ clearTimeout(this.timer);
507
523
  this.timer = void 0;
508
524
  }
509
525
  }
@@ -511,7 +527,7 @@ var Heartbeat = class {
511
527
  this.client.sendHeartbeat({
512
528
  instance_id: this.instanceId,
513
529
  name: this.name,
514
- interval_ms: this.intervalMs,
530
+ interval_ms: Math.min(Math.max(this.intervalMs, MIN_REPORTED_INTERVAL_MS), MAX_REPORTED_INTERVAL_MS),
515
531
  timestamp: nowISO(),
516
532
  metadata: this.metadata
517
533
  });
@@ -758,15 +774,18 @@ var Transaction = class {
758
774
  // antes do RunReporter conectar) — replay no setLifecycleSink
759
775
  lifecycleBacklog = [];
760
776
  /**
761
- * Ecoa no terminal o início/fim de uma step ou transaction (sempre, independente de
762
- * sampling ou do debug) e espelha a linha no sink de lifecycle, se conectado — é
763
- * assim que os eventos de performance também aparecem nos logs da run no app.
777
+ * Registra o início/fim de uma step ou transaction: espelha a linha no sink de
778
+ * lifecycle, se conectado (é assim que os eventos de performance aparecem nos
779
+ * logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
780
+ * escreve no stdout do processo do usuário sem permissão.
764
781
  */
765
782
  emit(kind, phase, label, startTs, endTs, status) {
766
783
  const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
767
- const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
768
784
  const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
769
- console.log(`${tag} ${label}${suffix}`);
785
+ if (this.client?.debug) {
786
+ const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
787
+ console.log(`${tag} ${label}${suffix}`);
788
+ }
770
789
  const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
771
790
  const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
772
791
  if (this.lifecycleSink) this.lifecycleSink(level, message);
@@ -929,6 +948,38 @@ var TasksApi = class {
929
948
  signal: AbortSignal.timeout(timeoutMs)
930
949
  });
931
950
  }
951
+ /**
952
+ * Envia um lote de itens de execução. Retry em falha de rede/5xx/429-transporte;
953
+ * 4xx é definitivo (404 = servidor sem o recurso, 429 = cap de itens por run) —
954
+ * o RunReporter trata por status.
955
+ */
956
+ async createItems(runId, request) {
957
+ let response;
958
+ try {
959
+ response = await fetchWithRetry(
960
+ () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/items`, {
961
+ method: "POST",
962
+ headers: this.headers(),
963
+ body: JSON.stringify(request),
964
+ signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
965
+ }),
966
+ { logger: this.logger, label: `itens da run ${runId}` }
967
+ );
968
+ } catch (error) {
969
+ if (error instanceof Error && error.message === "HTTP 429") {
970
+ throw new TasksApiError("envio de itens rejeitado (HTTP 429)", 429);
971
+ }
972
+ throw new Error(`envio de itens da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
973
+ }
974
+ if (!response.ok) {
975
+ throw new TasksApiError(`envio de itens rejeitado (HTTP ${response.status})`, response.status);
976
+ }
977
+ const body = await response.json().catch(() => null);
978
+ return {
979
+ accepted: typeof body?.accepted === "number" ? body.accepted : request.items.length,
980
+ duplicates: typeof body?.duplicates === "number" ? body.duplicates : 0
981
+ };
982
+ }
932
983
  /**
933
984
  * Registra o anexo e obtém a URL presignada de upload. 404/501/503 significam
934
985
  * "esta instalação não tem anexos" — o RunReporter degrada em silêncio a partir daí.
@@ -1086,6 +1137,22 @@ var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
1086
1137
  var MAX_LOG_MESSAGE_CHARS = 1e4;
1087
1138
  var MAX_LOGS_PER_PATCH = 100;
1088
1139
  var FLUSH_AT_BUFFERED = 20;
1140
+ var MAX_ITEMS_PER_RUN = 1e4;
1141
+ var MAX_ITEMS_PER_REQUEST = 500;
1142
+ var MAX_ITEM_MESSAGE_CHARS = 2e3;
1143
+ var MAX_ITEM_PAYLOAD_BYTES = 16384;
1144
+ var ITEMS_FLUSH_AT_BUFFERED = 100;
1145
+ var ITEMS_DRAIN_MS = 15e3;
1146
+ function scrubPayloadValue(value, depth) {
1147
+ if (typeof value === "string") return scrubSecrets(value);
1148
+ if (depth >= 4 || value === null || typeof value !== "object") return value;
1149
+ if (Array.isArray(value)) return value.map((entry) => scrubPayloadValue(entry, depth + 1));
1150
+ const out = {};
1151
+ for (const [key, entry] of Object.entries(value)) {
1152
+ out[key] = scrubPayloadValue(entry, depth + 1);
1153
+ }
1154
+ return out;
1155
+ }
1089
1156
  var RunReporter = class {
1090
1157
  constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs) {
1091
1158
  this.api = api;
@@ -1104,11 +1171,18 @@ var RunReporter = class {
1104
1171
  success: (message) => this.log("success", message)
1105
1172
  };
1106
1173
  tx.setLifecycleSink((level, message) => this.push(level, message));
1174
+ const items = {
1175
+ add: (item) => this.addItem(item.status, item.message, item.payload, item.id),
1176
+ succeeded: (payload, options) => this.addItem("succeeded", void 0, payload, options?.id),
1177
+ failed: (message, payload, options) => this.addItem("failed", message, payload, options?.id),
1178
+ occurrence: (message, payload, options) => this.addItem("occurrence", message, payload, options?.id)
1179
+ };
1107
1180
  const signal = this.abortController.signal;
1108
1181
  this.context = {
1109
1182
  runId,
1110
1183
  task,
1111
1184
  log,
1185
+ items,
1112
1186
  tx,
1113
1187
  signal,
1114
1188
  throwIfCanceled: () => {
@@ -1127,6 +1201,23 @@ var RunReporter = class {
1127
1201
  abortController = new AbortController();
1128
1202
  flushIntervalMs;
1129
1203
  cancelCheckIntervalMs;
1204
+ // Itens de execução vivem FORA de `this.chain` (status/liveness não pode ficar
1205
+ // atrás de milhares de itens), mas têm cadeia própria serializada — ordem de
1206
+ // envio preservada e sem lotes sobrepostos.
1207
+ itemsBuffer = [];
1208
+ itemsChain = Promise.resolve();
1209
+ itemsTimer;
1210
+ itemSeq = 0;
1211
+ itemCount = 0;
1212
+ /** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
1213
+ itemsUnsupported = false;
1214
+ /** Cap de itens por run atingido (client-side ou 429) — degrada com um aviso só. */
1215
+ itemsQuotaExceeded = false;
1216
+ /**
1217
+ * Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
1218
+ * servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
1219
+ */
1220
+ itemsClosed = false;
1130
1221
  // Uploads de anexo vivem FORA de `this.chain`: a cadeia é um FIFO puro de
1131
1222
  // status/logs e não pode ficar bloqueada esperando um binário subir.
1132
1223
  pendingAttachments = /* @__PURE__ */ new Set();
@@ -1153,6 +1244,12 @@ var RunReporter = class {
1153
1244
  this.dead = true;
1154
1245
  this.clearFlushTimer();
1155
1246
  this.buffer = [];
1247
+ this.itemsClosed = true;
1248
+ this.clearItemsTimer();
1249
+ if (this.itemsBuffer.length > 0) {
1250
+ this.logger.warn(`${this.itemsBuffer.length} item(ns) de execu\xE7\xE3o descartados (processo encerrando)`);
1251
+ this.itemsBuffer = [];
1252
+ }
1156
1253
  const waiting = this.uploadQueue.splice(0, this.uploadQueue.length);
1157
1254
  for (const resume of waiting) resume();
1158
1255
  }
@@ -1255,6 +1352,120 @@ var RunReporter = class {
1255
1352
  finalize() {
1256
1353
  return this.chain;
1257
1354
  }
1355
+ // ── Itens de execução ────────────────────────────────────────────────────────
1356
+ /**
1357
+ * Bufferiza um item (síncrono — a chamada nunca espera rede). Diferente do PATCH
1358
+ * de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
1359
+ * então um reporter morto não descarta: só um kill() de exit path descarta.
1360
+ */
1361
+ addItem(status, message, payload, id) {
1362
+ if (this.itemsClosed || this.itemsUnsupported) return;
1363
+ if (this.itemCount >= MAX_ITEMS_PER_RUN) {
1364
+ if (!this.itemsQuotaExceeded) {
1365
+ this.itemsQuotaExceeded = true;
1366
+ this.log("warning", `itens descartados: limite de ${MAX_ITEMS_PER_RUN} por execu\xE7\xE3o atingido`);
1367
+ }
1368
+ return;
1369
+ }
1370
+ this.itemCount++;
1371
+ let scrubbedPayload;
1372
+ if (payload) {
1373
+ scrubbedPayload = scrubPayloadValue(payload, 0);
1374
+ let serializedLength;
1375
+ try {
1376
+ serializedLength = JSON.stringify(scrubbedPayload).length;
1377
+ } catch {
1378
+ }
1379
+ if (serializedLength === void 0 || serializedLength > MAX_ITEM_PAYLOAD_BYTES) {
1380
+ this.logger.warn(
1381
+ `payload do item descartado (${serializedLength === void 0 ? "n\xE3o serializ\xE1vel" : `${serializedLength} bytes > ${MAX_ITEM_PAYLOAD_BYTES}`}) \u2014 o item foi mantido`
1382
+ );
1383
+ scrubbedPayload = void 0;
1384
+ }
1385
+ }
1386
+ this.itemsBuffer.push({
1387
+ id: id ? truncate(String(id), 255) : uuid4(),
1388
+ seq: this.itemSeq++,
1389
+ status,
1390
+ ...message ? { message: truncate(scrubSecrets(message), MAX_ITEM_MESSAGE_CHARS) } : {},
1391
+ ...scrubbedPayload ? { payload: scrubbedPayload } : {},
1392
+ ts: nowISO()
1393
+ });
1394
+ if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
1395
+ this.flushItems();
1396
+ } else {
1397
+ this.scheduleItemsFlush();
1398
+ }
1399
+ }
1400
+ scheduleItemsFlush() {
1401
+ if (this.itemsTimer) return;
1402
+ this.itemsTimer = setTimeout(() => {
1403
+ this.itemsTimer = void 0;
1404
+ this.flushItems();
1405
+ }, this.flushIntervalMs);
1406
+ unrefTimer(this.itemsTimer);
1407
+ }
1408
+ clearItemsTimer() {
1409
+ if (this.itemsTimer) {
1410
+ clearTimeout(this.itemsTimer);
1411
+ this.itemsTimer = void 0;
1412
+ }
1413
+ }
1414
+ /** Envia o buffer de itens em lotes serializados na cadeia própria. */
1415
+ flushItems() {
1416
+ this.clearItemsTimer();
1417
+ if (this.itemsBuffer.length === 0 || this.itemsUnsupported) return;
1418
+ const items = this.itemsBuffer.splice(0, this.itemsBuffer.length);
1419
+ for (let i = 0; i < items.length; i += MAX_ITEMS_PER_REQUEST) {
1420
+ const batch = items.slice(i, i + MAX_ITEMS_PER_REQUEST);
1421
+ this.itemsChain = this.itemsChain.then(() => this.sendItems(batch));
1422
+ }
1423
+ }
1424
+ async sendItems(batch) {
1425
+ if (this.itemsUnsupported) return;
1426
+ try {
1427
+ await this.api.createItems(this.runId, { items: batch });
1428
+ } catch (error) {
1429
+ if (error instanceof TasksApiError && error.status === 404) {
1430
+ this.itemsUnsupported = true;
1431
+ this.logger.warn("itens de execu\xE7\xE3o n\xE3o s\xE3o suportados por este servidor Hobots \u2014 descartados");
1432
+ return;
1433
+ }
1434
+ if (error instanceof TasksApiError && error.status === 429) {
1435
+ if (!this.itemsQuotaExceeded) {
1436
+ this.itemsQuotaExceeded = true;
1437
+ this.log("warning", "itens descartados: limite de itens por execu\xE7\xE3o atingido no servidor");
1438
+ }
1439
+ return;
1440
+ }
1441
+ if (error instanceof TasksApiError) {
1442
+ this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
1443
+ return;
1444
+ }
1445
+ this.itemsBuffer.unshift(...batch);
1446
+ this.scheduleItemsFlush();
1447
+ this.logger.error(`lote de ${batch.length} item(ns) n\xE3o enviado (devolvido ao buffer):`, error);
1448
+ }
1449
+ }
1450
+ /**
1451
+ * Dreno final de itens antes do PATCH terminal (com teto): garante que os
1452
+ * totalizadores estejam completos quando a run vira succeeded/failed. Como o
1453
+ * servidor aceita itens tardios, perder a corrida aqui não perde dados.
1454
+ */
1455
+ async finalizeItems(timeoutMs = ITEMS_DRAIN_MS) {
1456
+ this.flushItems();
1457
+ await Promise.race([
1458
+ this.itemsChain,
1459
+ new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1460
+ ]);
1461
+ if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
1462
+ this.flushItems();
1463
+ await Promise.race([
1464
+ this.itemsChain,
1465
+ new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1466
+ ]);
1467
+ }
1468
+ }
1258
1469
  // ── Anexos ───────────────────────────────────────────────────────────────────
1259
1470
  /**
1260
1471
  * Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
@@ -1621,13 +1832,17 @@ var TaskAgent = class {
1621
1832
  try {
1622
1833
  const response = await this.api.poll(
1623
1834
  {
1624
- agent: {
1835
+ instance: {
1625
1836
  // identidade canônica do agente no painel — o mesmo instanceId do
1626
1837
  // heartbeat da Telemetria, então os dois canais convergem em um agente
1627
1838
  instance_id: this.instanceId,
1628
1839
  version: SDK_VERSION,
1629
1840
  host: hostname(),
1630
- tasks: [...this.registry.keys()]
1841
+ tasks: [...this.registry.keys()],
1842
+ // o servidor calcula a tolerância de offline (3×) a partir disto em
1843
+ // agentes poll-only — sem o campo, um poll lento flaparia offline/online.
1844
+ // Clampado à faixa aceita pelo schema do poll (1s..15min).
1845
+ poll_interval_ms: Math.min(Math.max(this.pollIntervalMs, 1e3), 9e5)
1631
1846
  },
1632
1847
  max: Math.min(MAX_CONCURRENCY, this.concurrency - this.activeRuns.size),
1633
1848
  tasks: [...this.registry.keys()]
@@ -1699,12 +1914,14 @@ var TaskAgent = class {
1699
1914
  () => this.invokeWithTimeout(handler, run, reporter)
1700
1915
  );
1701
1916
  await reporter.finalizeAttachments();
1917
+ await reporter.finalizeItems();
1702
1918
  tx.setStatus("ok");
1703
1919
  tx.finish();
1704
1920
  reporter.reportSuccess();
1705
1921
  this.logger.debug(`run ${run.id} conclu\xEDda`);
1706
1922
  } catch (error) {
1707
1923
  await reporter.finalizeAttachments();
1924
+ await reporter.finalizeItems();
1708
1925
  tx.setStatus("error");
1709
1926
  tx.finish();
1710
1927
  reporter.reportFailure(toRunError(error));