hobots 1.9.1 → 1.10.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 { AgentInfo, Envelope, EnvelopeItem, ExceptionData, 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;
@@ -146,6 +146,13 @@ interface TasksOptions {
146
146
  * uma run sem reporte por ~60s. Default: 5_000 (mínimo 1_000, máximo 15_000)
147
147
  */
148
148
  cancelCheckIntervalMs?: number;
149
+ /**
150
+ * Tempo de graça após ctx.signal disparar (run cancelada no app) antes do
151
+ * agente desistir de esperar o handler e liberar o slot de concorrência. O
152
+ * handler não é morto — se ele nunca checar ctx.signal, pode continuar
153
+ * rodando em segundo plano; só o agente para de esperar por ele. Default: 10_000
154
+ */
155
+ cancelGraceMs?: number;
149
156
  /**
150
157
  * Instala handlers de SIGINT/SIGTERM/uncaughtException que reportam as runs
151
158
  * ativas como falha (AgentTerminated) antes do processo morrer. Nunca interfere
@@ -160,11 +167,41 @@ interface RunLogger {
160
167
  error(message: string): void;
161
168
  success(message: string): void;
162
169
  }
170
+ interface RunItemOptions {
171
+ /**
172
+ * Identidade de negócio do item na run (ex.: nº da nota fiscal). Repetido na
173
+ * mesma run = ignorado pelo servidor (registro único, retry idempotente).
174
+ * Omitido: o SDK gera um id único e cada chamada vira um item novo.
175
+ */
176
+ id?: string;
177
+ }
178
+ /**
179
+ * Registro dos itens que a execução processou/gerou (a entrega do bot: notas
180
+ * fiscais baixadas, boletos emitidos...). Chamadas são síncronas — os itens são
181
+ * bufferizados e enviados em lotes; o agente drena o buffer antes do reporte final.
182
+ */
183
+ interface RunItems {
184
+ /** Forma genérica — os atalhos succeeded/failed/occurrence cobrem o dia a dia. */
185
+ add(item: {
186
+ status: RunItemStatus;
187
+ id?: string;
188
+ message?: string;
189
+ payload?: Record<string, unknown>;
190
+ }): void;
191
+ /** Item processado com sucesso. `payload` = atributos do item (viram colunas na plataforma). */
192
+ succeeded(payload?: Record<string, unknown>, options?: RunItemOptions): void;
193
+ /** Falha técnica ao processar o item — `message` é o motivo exibido na plataforma. */
194
+ failed(message: string, payload?: Record<string, unknown>, options?: RunItemOptions): void;
195
+ /** Ocorrência de negócio (ex.: "nota nº 10 não encontrada") — não é falha do bot. */
196
+ occurrence(message: string, payload?: Record<string, unknown>, options?: RunItemOptions): void;
197
+ }
163
198
  interface RunContext {
164
199
  runId: string;
165
200
  task: string;
166
201
  /** Log bufferizado — enviado ao servidor em PATCHes incrementais com throttle */
167
202
  log: RunLogger;
203
+ /** Itens processados/gerados pela execução — a entrega do bot, com status e atributos */
204
+ items: RunItems;
168
205
  /**
169
206
  * Transaction de performance desta execução — já correlacionada à run e finalizada
170
207
  * automaticamente ao fim do handler. Use `ctx.tx.startChild(op, description)` pra medir
@@ -296,4 +333,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
296
333
 
297
334
  declare function scrubSecrets(text: string): string;
298
335
 
299
- 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 };
336
+ 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 { AgentInfo, Envelope, EnvelopeItem, ExceptionData, 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;
@@ -146,6 +146,13 @@ interface TasksOptions {
146
146
  * uma run sem reporte por ~60s. Default: 5_000 (mínimo 1_000, máximo 15_000)
147
147
  */
148
148
  cancelCheckIntervalMs?: number;
149
+ /**
150
+ * Tempo de graça após ctx.signal disparar (run cancelada no app) antes do
151
+ * agente desistir de esperar o handler e liberar o slot de concorrência. O
152
+ * handler não é morto — se ele nunca checar ctx.signal, pode continuar
153
+ * rodando em segundo plano; só o agente para de esperar por ele. Default: 10_000
154
+ */
155
+ cancelGraceMs?: number;
149
156
  /**
150
157
  * Instala handlers de SIGINT/SIGTERM/uncaughtException que reportam as runs
151
158
  * ativas como falha (AgentTerminated) antes do processo morrer. Nunca interfere
@@ -160,11 +167,41 @@ interface RunLogger {
160
167
  error(message: string): void;
161
168
  success(message: string): void;
162
169
  }
170
+ interface RunItemOptions {
171
+ /**
172
+ * Identidade de negócio do item na run (ex.: nº da nota fiscal). Repetido na
173
+ * mesma run = ignorado pelo servidor (registro único, retry idempotente).
174
+ * Omitido: o SDK gera um id único e cada chamada vira um item novo.
175
+ */
176
+ id?: string;
177
+ }
178
+ /**
179
+ * Registro dos itens que a execução processou/gerou (a entrega do bot: notas
180
+ * fiscais baixadas, boletos emitidos...). Chamadas são síncronas — os itens são
181
+ * bufferizados e enviados em lotes; o agente drena o buffer antes do reporte final.
182
+ */
183
+ interface RunItems {
184
+ /** Forma genérica — os atalhos succeeded/failed/occurrence cobrem o dia a dia. */
185
+ add(item: {
186
+ status: RunItemStatus;
187
+ id?: string;
188
+ message?: string;
189
+ payload?: Record<string, unknown>;
190
+ }): void;
191
+ /** Item processado com sucesso. `payload` = atributos do item (viram colunas na plataforma). */
192
+ succeeded(payload?: Record<string, unknown>, options?: RunItemOptions): void;
193
+ /** Falha técnica ao processar o item — `message` é o motivo exibido na plataforma. */
194
+ failed(message: string, payload?: Record<string, unknown>, options?: RunItemOptions): void;
195
+ /** Ocorrência de negócio (ex.: "nota nº 10 não encontrada") — não é falha do bot. */
196
+ occurrence(message: string, payload?: Record<string, unknown>, options?: RunItemOptions): void;
197
+ }
163
198
  interface RunContext {
164
199
  runId: string;
165
200
  task: string;
166
201
  /** Log bufferizado — enviado ao servidor em PATCHes incrementais com throttle */
167
202
  log: RunLogger;
203
+ /** Itens processados/gerados pela execução — a entrega do bot, com status e atributos */
204
+ items: RunItems;
168
205
  /**
169
206
  * Transaction de performance desta execução — já correlacionada à run e finalizada
170
207
  * automaticamente ao fim do handler. Use `ctx.tx.startChild(op, description)` pra medir
@@ -296,4 +333,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
296
333
 
297
334
  declare function scrubSecrets(text: string): string;
298
335
 
299
- 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 };
336
+ 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.0" : "0.0.0-dev";
106
+ var SDK_VERSION = true ? "1.10.0" : "0.0.0-dev";
107
107
  function isNode() {
108
108
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
109
109
  }
@@ -929,6 +929,38 @@ var TasksApi = class {
929
929
  signal: AbortSignal.timeout(timeoutMs)
930
930
  });
931
931
  }
932
+ /**
933
+ * Envia um lote de itens de execução. Retry em falha de rede/5xx/429-transporte;
934
+ * 4xx é definitivo (404 = servidor sem o recurso, 429 = cap de itens por run) —
935
+ * o RunReporter trata por status.
936
+ */
937
+ async createItems(runId, request) {
938
+ let response;
939
+ try {
940
+ response = await fetchWithRetry(
941
+ () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/items`, {
942
+ method: "POST",
943
+ headers: this.headers(),
944
+ body: JSON.stringify(request),
945
+ signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
946
+ }),
947
+ { logger: this.logger, label: `itens da run ${runId}` }
948
+ );
949
+ } catch (error) {
950
+ if (error instanceof Error && error.message === "HTTP 429") {
951
+ throw new TasksApiError("envio de itens rejeitado (HTTP 429)", 429);
952
+ }
953
+ throw new Error(`envio de itens da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
954
+ }
955
+ if (!response.ok) {
956
+ throw new TasksApiError(`envio de itens rejeitado (HTTP ${response.status})`, response.status);
957
+ }
958
+ const body = await response.json().catch(() => null);
959
+ return {
960
+ accepted: typeof body?.accepted === "number" ? body.accepted : request.items.length,
961
+ duplicates: typeof body?.duplicates === "number" ? body.duplicates : 0
962
+ };
963
+ }
932
964
  /**
933
965
  * Registra o anexo e obtém a URL presignada de upload. 404/501/503 significam
934
966
  * "esta instalação não tem anexos" — o RunReporter degrada em silêncio a partir daí.
@@ -1086,6 +1118,22 @@ var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
1086
1118
  var MAX_LOG_MESSAGE_CHARS = 1e4;
1087
1119
  var MAX_LOGS_PER_PATCH = 100;
1088
1120
  var FLUSH_AT_BUFFERED = 20;
1121
+ var MAX_ITEMS_PER_RUN = 1e4;
1122
+ var MAX_ITEMS_PER_REQUEST = 500;
1123
+ var MAX_ITEM_MESSAGE_CHARS = 2e3;
1124
+ var MAX_ITEM_PAYLOAD_BYTES = 16384;
1125
+ var ITEMS_FLUSH_AT_BUFFERED = 100;
1126
+ var ITEMS_DRAIN_MS = 15e3;
1127
+ function scrubPayloadValue(value, depth) {
1128
+ if (typeof value === "string") return scrubSecrets(value);
1129
+ if (depth >= 4 || value === null || typeof value !== "object") return value;
1130
+ if (Array.isArray(value)) return value.map((entry) => scrubPayloadValue(entry, depth + 1));
1131
+ const out = {};
1132
+ for (const [key, entry] of Object.entries(value)) {
1133
+ out[key] = scrubPayloadValue(entry, depth + 1);
1134
+ }
1135
+ return out;
1136
+ }
1089
1137
  var RunReporter = class {
1090
1138
  constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs) {
1091
1139
  this.api = api;
@@ -1104,11 +1152,18 @@ var RunReporter = class {
1104
1152
  success: (message) => this.log("success", message)
1105
1153
  };
1106
1154
  tx.setLifecycleSink((level, message) => this.push(level, message));
1155
+ const items = {
1156
+ add: (item) => this.addItem(item.status, item.message, item.payload, item.id),
1157
+ succeeded: (payload, options) => this.addItem("succeeded", void 0, payload, options?.id),
1158
+ failed: (message, payload, options) => this.addItem("failed", message, payload, options?.id),
1159
+ occurrence: (message, payload, options) => this.addItem("occurrence", message, payload, options?.id)
1160
+ };
1107
1161
  const signal = this.abortController.signal;
1108
1162
  this.context = {
1109
1163
  runId,
1110
1164
  task,
1111
1165
  log,
1166
+ items,
1112
1167
  tx,
1113
1168
  signal,
1114
1169
  throwIfCanceled: () => {
@@ -1127,6 +1182,23 @@ var RunReporter = class {
1127
1182
  abortController = new AbortController();
1128
1183
  flushIntervalMs;
1129
1184
  cancelCheckIntervalMs;
1185
+ // Itens de execução vivem FORA de `this.chain` (status/liveness não pode ficar
1186
+ // atrás de milhares de itens), mas têm cadeia própria serializada — ordem de
1187
+ // envio preservada e sem lotes sobrepostos.
1188
+ itemsBuffer = [];
1189
+ itemsChain = Promise.resolve();
1190
+ itemsTimer;
1191
+ itemSeq = 0;
1192
+ itemCount = 0;
1193
+ /** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
1194
+ itemsUnsupported = false;
1195
+ /** Cap de itens por run atingido (client-side ou 429) — degrada com um aviso só. */
1196
+ itemsQuotaExceeded = false;
1197
+ /**
1198
+ * Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
1199
+ * servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
1200
+ */
1201
+ itemsClosed = false;
1130
1202
  // Uploads de anexo vivem FORA de `this.chain`: a cadeia é um FIFO puro de
1131
1203
  // status/logs e não pode ficar bloqueada esperando um binário subir.
1132
1204
  pendingAttachments = /* @__PURE__ */ new Set();
@@ -1153,6 +1225,12 @@ var RunReporter = class {
1153
1225
  this.dead = true;
1154
1226
  this.clearFlushTimer();
1155
1227
  this.buffer = [];
1228
+ this.itemsClosed = true;
1229
+ this.clearItemsTimer();
1230
+ if (this.itemsBuffer.length > 0) {
1231
+ this.logger.warn(`${this.itemsBuffer.length} item(ns) de execu\xE7\xE3o descartados (processo encerrando)`);
1232
+ this.itemsBuffer = [];
1233
+ }
1156
1234
  const waiting = this.uploadQueue.splice(0, this.uploadQueue.length);
1157
1235
  for (const resume of waiting) resume();
1158
1236
  }
@@ -1255,6 +1333,120 @@ var RunReporter = class {
1255
1333
  finalize() {
1256
1334
  return this.chain;
1257
1335
  }
1336
+ // ── Itens de execução ────────────────────────────────────────────────────────
1337
+ /**
1338
+ * Bufferiza um item (síncrono — a chamada nunca espera rede). Diferente do PATCH
1339
+ * de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
1340
+ * então um reporter morto não descarta: só um kill() de exit path descarta.
1341
+ */
1342
+ addItem(status, message, payload, id) {
1343
+ if (this.itemsClosed || this.itemsUnsupported) return;
1344
+ if (this.itemCount >= MAX_ITEMS_PER_RUN) {
1345
+ if (!this.itemsQuotaExceeded) {
1346
+ this.itemsQuotaExceeded = true;
1347
+ this.log("warning", `itens descartados: limite de ${MAX_ITEMS_PER_RUN} por execu\xE7\xE3o atingido`);
1348
+ }
1349
+ return;
1350
+ }
1351
+ this.itemCount++;
1352
+ let scrubbedPayload;
1353
+ if (payload) {
1354
+ scrubbedPayload = scrubPayloadValue(payload, 0);
1355
+ let serializedLength;
1356
+ try {
1357
+ serializedLength = JSON.stringify(scrubbedPayload).length;
1358
+ } catch {
1359
+ }
1360
+ if (serializedLength === void 0 || serializedLength > MAX_ITEM_PAYLOAD_BYTES) {
1361
+ this.logger.warn(
1362
+ `payload do item descartado (${serializedLength === void 0 ? "n\xE3o serializ\xE1vel" : `${serializedLength} bytes > ${MAX_ITEM_PAYLOAD_BYTES}`}) \u2014 o item foi mantido`
1363
+ );
1364
+ scrubbedPayload = void 0;
1365
+ }
1366
+ }
1367
+ this.itemsBuffer.push({
1368
+ id: id ? truncate(String(id), 255) : uuid4(),
1369
+ seq: this.itemSeq++,
1370
+ status,
1371
+ ...message ? { message: truncate(scrubSecrets(message), MAX_ITEM_MESSAGE_CHARS) } : {},
1372
+ ...scrubbedPayload ? { payload: scrubbedPayload } : {},
1373
+ ts: nowISO()
1374
+ });
1375
+ if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
1376
+ this.flushItems();
1377
+ } else {
1378
+ this.scheduleItemsFlush();
1379
+ }
1380
+ }
1381
+ scheduleItemsFlush() {
1382
+ if (this.itemsTimer) return;
1383
+ this.itemsTimer = setTimeout(() => {
1384
+ this.itemsTimer = void 0;
1385
+ this.flushItems();
1386
+ }, this.flushIntervalMs);
1387
+ unrefTimer(this.itemsTimer);
1388
+ }
1389
+ clearItemsTimer() {
1390
+ if (this.itemsTimer) {
1391
+ clearTimeout(this.itemsTimer);
1392
+ this.itemsTimer = void 0;
1393
+ }
1394
+ }
1395
+ /** Envia o buffer de itens em lotes serializados na cadeia própria. */
1396
+ flushItems() {
1397
+ this.clearItemsTimer();
1398
+ if (this.itemsBuffer.length === 0 || this.itemsUnsupported) return;
1399
+ const items = this.itemsBuffer.splice(0, this.itemsBuffer.length);
1400
+ for (let i = 0; i < items.length; i += MAX_ITEMS_PER_REQUEST) {
1401
+ const batch = items.slice(i, i + MAX_ITEMS_PER_REQUEST);
1402
+ this.itemsChain = this.itemsChain.then(() => this.sendItems(batch));
1403
+ }
1404
+ }
1405
+ async sendItems(batch) {
1406
+ if (this.itemsUnsupported) return;
1407
+ try {
1408
+ await this.api.createItems(this.runId, { items: batch });
1409
+ } catch (error) {
1410
+ if (error instanceof TasksApiError && error.status === 404) {
1411
+ this.itemsUnsupported = true;
1412
+ this.logger.warn("itens de execu\xE7\xE3o n\xE3o s\xE3o suportados por este servidor Hobots \u2014 descartados");
1413
+ return;
1414
+ }
1415
+ if (error instanceof TasksApiError && error.status === 429) {
1416
+ if (!this.itemsQuotaExceeded) {
1417
+ this.itemsQuotaExceeded = true;
1418
+ this.log("warning", "itens descartados: limite de itens por execu\xE7\xE3o atingido no servidor");
1419
+ }
1420
+ return;
1421
+ }
1422
+ if (error instanceof TasksApiError) {
1423
+ this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
1424
+ return;
1425
+ }
1426
+ this.itemsBuffer.unshift(...batch);
1427
+ this.scheduleItemsFlush();
1428
+ this.logger.error(`lote de ${batch.length} item(ns) n\xE3o enviado (devolvido ao buffer):`, error);
1429
+ }
1430
+ }
1431
+ /**
1432
+ * Dreno final de itens antes do PATCH terminal (com teto): garante que os
1433
+ * totalizadores estejam completos quando a run vira succeeded/failed. Como o
1434
+ * servidor aceita itens tardios, perder a corrida aqui não perde dados.
1435
+ */
1436
+ async finalizeItems(timeoutMs = ITEMS_DRAIN_MS) {
1437
+ this.flushItems();
1438
+ await Promise.race([
1439
+ this.itemsChain,
1440
+ new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1441
+ ]);
1442
+ if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
1443
+ this.flushItems();
1444
+ await Promise.race([
1445
+ this.itemsChain,
1446
+ new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1447
+ ]);
1448
+ }
1449
+ }
1258
1450
  // ── Anexos ───────────────────────────────────────────────────────────────────
1259
1451
  /**
1260
1452
  * Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
@@ -1501,6 +1693,7 @@ var MIN_POLL_INTERVAL_MS = 1e3;
1501
1693
  var MAX_CONCURRENCY = 5;
1502
1694
  var POLL_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
1503
1695
  var AUTH_FAILURE_INTERVAL_MS = 3e4;
1696
+ var DEFAULT_CANCEL_GRACE_MS = 1e4;
1504
1697
  function hostname() {
1505
1698
  if (!isNode()) return void 0;
1506
1699
  return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
@@ -1519,6 +1712,7 @@ var TaskAgent = class {
1519
1712
  this.taskTimeoutMs = options.taskTimeoutMs;
1520
1713
  this.logFlushIntervalMs = options.logFlushIntervalMs;
1521
1714
  this.cancelCheckIntervalMs = options.cancelCheckIntervalMs;
1715
+ this.cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
1522
1716
  this.logger.debug("agente configurado", { baseUrl: config.tasksBaseUrl });
1523
1717
  }
1524
1718
  logger;
@@ -1530,6 +1724,7 @@ var TaskAgent = class {
1530
1724
  taskTimeoutMs;
1531
1725
  logFlushIntervalMs;
1532
1726
  cancelCheckIntervalMs;
1727
+ cancelGraceMs;
1533
1728
  state = "idle";
1534
1729
  timer;
1535
1730
  pollAbort;
@@ -1696,12 +1891,14 @@ var TaskAgent = class {
1696
1891
  () => this.invokeWithTimeout(handler, run, reporter)
1697
1892
  );
1698
1893
  await reporter.finalizeAttachments();
1894
+ await reporter.finalizeItems();
1699
1895
  tx.setStatus("ok");
1700
1896
  tx.finish();
1701
1897
  reporter.reportSuccess();
1702
1898
  this.logger.debug(`run ${run.id} conclu\xEDda`);
1703
1899
  } catch (error) {
1704
1900
  await reporter.finalizeAttachments();
1901
+ await reporter.finalizeItems();
1705
1902
  tx.setStatus("error");
1706
1903
  tx.finish();
1707
1904
  reporter.reportFailure(toRunError(error));
@@ -1714,14 +1911,26 @@ var TaskAgent = class {
1714
1911
  }
1715
1912
  async invokeWithTimeout(handler, run, reporter) {
1716
1913
  const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
1717
- if (!this.taskTimeoutMs || this.taskTimeoutMs <= 0) {
1914
+ const races = [];
1915
+ if (this.taskTimeoutMs && this.taskTimeoutMs > 0) {
1916
+ races.push(sleep(this.taskTimeoutMs).then(() => TIMEOUT_SENTINEL));
1917
+ }
1918
+ if (this.cancelGraceMs > 0) {
1919
+ races.push(waitForCancelGrace(reporter.context.signal, this.cancelGraceMs).then(() => CANCEL_SENTINEL));
1920
+ }
1921
+ if (races.length === 0) {
1718
1922
  await invocation;
1719
1923
  return;
1720
1924
  }
1925
+ const outcome = await Promise.race([invocation, ...races]);
1926
+ if (outcome !== TIMEOUT_SENTINEL && outcome !== CANCEL_SENTINEL) return;
1927
+ if (outcome === CANCEL_SENTINEL) {
1928
+ this.logger.warn(
1929
+ `run ${run.id} cancelada \u2014 handler n\xE3o respondeu a ctx.signal em ${this.cancelGraceMs}ms, liberando o slot (o handler pode continuar rodando em segundo plano)`
1930
+ );
1931
+ return;
1932
+ }
1721
1933
  const timeoutMs = this.taskTimeoutMs;
1722
- const timeout = sleep(timeoutMs).then(() => TIMEOUT_SENTINEL);
1723
- const outcome = await Promise.race([invocation, timeout]);
1724
- if (outcome !== TIMEOUT_SENTINEL) return;
1725
1934
  reporter.abort();
1726
1935
  invocation.then(() => this.logger.warn(`run ${run.id} terminou depois do timeout de ${timeoutMs}ms`)).catch(() => this.logger.warn(`run ${run.id} falhou depois do timeout de ${timeoutMs}ms (erro descartado)`));
1727
1936
  throw Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
@@ -1730,6 +1939,16 @@ var TaskAgent = class {
1730
1939
  }
1731
1940
  };
1732
1941
  var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
1942
+ var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
1943
+ function waitForCancelGrace(signal, graceMs) {
1944
+ return new Promise((resolve) => {
1945
+ if (signal.aborted) {
1946
+ setTimeout(resolve, graceMs);
1947
+ return;
1948
+ }
1949
+ signal.addEventListener("abort", () => setTimeout(resolve, graceMs), { once: true });
1950
+ });
1951
+ }
1733
1952
  var MAX_ERROR_MESSAGE_CHARS = 1e4;
1734
1953
  var MAX_ERROR_STACK_CHARS = 2e4;
1735
1954
  function toRunError(error) {