hobots 3.2.2 → 3.4.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.cjs CHANGED
@@ -34,6 +34,7 @@ __export(src_exports, {
34
34
  close: () => close2,
35
35
  default: () => src_default,
36
36
  hobots: () => hobots,
37
+ image: () => image2,
37
38
  register: () => register2,
38
39
  retry: () => retry,
39
40
  start: () => start2
@@ -95,7 +96,7 @@ function scrubSecrets(text) {
95
96
 
96
97
  // src/utils.ts
97
98
  var SDK_NAME = "hobots";
98
- var SDK_VERSION = true ? "3.2.2" : "0.0.0-dev";
99
+ var SDK_VERSION = true ? "3.4.0" : "0.0.0-dev";
99
100
  function isNode() {
100
101
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
101
102
  }
@@ -269,12 +270,39 @@ function eventFromException(error, type = "error") {
269
270
  };
270
271
  }
271
272
 
273
+ // protocol-limits.json
274
+ var protocol_limits_default = {
275
+ MAX_ITEMS_PER_ENVELOPE: 50,
276
+ HEARTBEAT_DEFAULT_INTERVAL_MS: 3e4,
277
+ HEARTBEAT_MIN_INTERVAL_MS: 1e3,
278
+ HEARTBEAT_MAX_INTERVAL_MS: 9e5,
279
+ MAX_CLAIM_PER_POLL: 5,
280
+ MAX_LOG_MESSAGE_CHARS: 1e4,
281
+ MAX_LOGS_PER_PATCH: 100,
282
+ MAX_ERROR_MESSAGE_CHARS: 1e4,
283
+ MAX_ERROR_STACK_CHARS: 2e4,
284
+ MAX_ITEMS_PER_RUN: 1e4,
285
+ MAX_ITEMS_PER_REQUEST: 500,
286
+ MAX_ITEM_MESSAGE_CHARS: 2e3,
287
+ MAX_ITEM_PAYLOAD_BYTES: 16384,
288
+ MAX_ITEM_COLUMNS: 12,
289
+ MAX_ITEM_COLUMN_LABEL_CHARS: 60,
290
+ MAX_ATTACHMENT_BYTES: 5242880,
291
+ MAX_ATTACHMENTS_PER_RUN: 20,
292
+ MAX_ATTACHMENTS_PER_LOG_ENTRY: 4,
293
+ MAX_CAPTION_CHARS: 200,
294
+ MAX_IMAGE_DIMENSION_PX: 2e4
295
+ };
296
+
297
+ // src/wire-limits.ts
298
+ var WIRE_LIMITS = protocol_limits_default;
299
+
272
300
  // src/transport.ts
273
301
  var REQUEST_TIMEOUT_MS = 1e4;
274
302
  var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
275
303
  var DEFAULT_MAX_BATCH_SIZE = 10;
276
304
  var DEFAULT_MAX_QUEUE_SIZE = 200;
277
- var MAX_ITEMS_PER_ENVELOPE = 50;
305
+ var MAX_ITEMS_PER_ENVELOPE = WIRE_LIMITS.MAX_ITEMS_PER_ENVELOPE;
278
306
  var Transport = class {
279
307
  endpoint;
280
308
  key;
@@ -350,6 +378,7 @@ var Transport = class {
350
378
  try {
351
379
  response = await fetchWithRetry(
352
380
  () => fetch(this.endpoint, {
381
+ redirect: "error",
353
382
  method: "POST",
354
383
  headers: { "content-type": "application/json", authorization: `Bearer ${this.key}` },
355
384
  body: JSON.stringify(envelope),
@@ -527,7 +556,6 @@ function envBaseUrl() {
527
556
  function isLoopbackHost(hostname2) {
528
557
  return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
529
558
  }
530
- var warnedInsecureBaseUrl = false;
531
559
  function validateBaseUrl(raw, prefix) {
532
560
  let url;
533
561
  try {
@@ -541,11 +569,8 @@ function validateBaseUrl(raw, prefix) {
541
569
  if (url.username || url.password) {
542
570
  throw new Error(`${prefix} HOBOTS_API_URL n\xE3o pode conter credenciais na URL \u2014 a autentica\xE7\xE3o \xE9 s\xF3 pelo Client Secret`);
543
571
  }
544
- if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
545
- warnedInsecureBaseUrl = true;
546
- console.warn(
547
- `${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
548
- );
572
+ if (url.protocol === "http:" && !isLoopbackHost(url.hostname)) {
573
+ throw new Error(`${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafegaria sem TLS; use https`);
549
574
  }
550
575
  return raw.replace(/\/+$/, "");
551
576
  }
@@ -598,9 +623,9 @@ async function fallbackInstanceId() {
598
623
  }
599
624
 
600
625
  // src/heartbeat.ts
601
- var DEFAULT_INTERVAL_MS = 3e4;
602
- var MIN_REPORTED_INTERVAL_MS = 1e3;
603
- var MAX_REPORTED_INTERVAL_MS = 9e5;
626
+ var DEFAULT_INTERVAL_MS = WIRE_LIMITS.HEARTBEAT_DEFAULT_INTERVAL_MS;
627
+ var MIN_REPORTED_INTERVAL_MS = WIRE_LIMITS.HEARTBEAT_MIN_INTERVAL_MS;
628
+ var MAX_REPORTED_INTERVAL_MS = WIRE_LIMITS.HEARTBEAT_MAX_INTERVAL_MS;
604
629
  var Heartbeat = class {
605
630
  constructor(client, options = {}, instanceId, sampler) {
606
631
  this.client = client;
@@ -909,6 +934,7 @@ var TasksApi = class {
909
934
  let response;
910
935
  try {
911
936
  response = await fetch(`${this.baseUrl}/poll`, {
937
+ redirect: "error",
912
938
  method: "POST",
913
939
  headers: this.headers(),
914
940
  body: JSON.stringify(request),
@@ -932,6 +958,7 @@ var TasksApi = class {
932
958
  let response;
933
959
  try {
934
960
  response = await fetch(`${this.baseUrl}/runs`, {
961
+ redirect: "error",
935
962
  method: "POST",
936
963
  headers: this.headers(),
937
964
  body: JSON.stringify(request),
@@ -982,6 +1009,7 @@ var TasksApi = class {
982
1009
  try {
983
1010
  response = await fetchWithRetry(
984
1011
  () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
1012
+ redirect: "error",
985
1013
  method: "PATCH",
986
1014
  headers: this.headers(),
987
1015
  body: JSON.stringify(update),
@@ -1003,6 +1031,7 @@ var TasksApi = class {
1003
1031
  */
1004
1032
  async updateRunOnce(runId, update, timeoutMs = 2e3) {
1005
1033
  await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
1034
+ redirect: "error",
1006
1035
  method: "PATCH",
1007
1036
  headers: this.headers(),
1008
1037
  body: JSON.stringify(update),
@@ -1019,6 +1048,7 @@ var TasksApi = class {
1019
1048
  try {
1020
1049
  response = await fetchWithRetry(
1021
1050
  () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/items`, {
1051
+ redirect: "error",
1022
1052
  method: "POST",
1023
1053
  headers: this.headers(),
1024
1054
  body: JSON.stringify(request),
@@ -1048,6 +1078,7 @@ var TasksApi = class {
1048
1078
  UPDATE_TIMEOUT_MS,
1049
1079
  signal,
1050
1080
  (timeoutSignal) => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments`, {
1081
+ redirect: "error",
1051
1082
  method: "POST",
1052
1083
  headers: this.headers(),
1053
1084
  body: JSON.stringify(request),
@@ -1080,6 +1111,7 @@ var TasksApi = class {
1080
1111
  UPLOAD_TIMEOUT_MS,
1081
1112
  signal,
1082
1113
  (timeoutSignal) => fetch(url, {
1114
+ redirect: "error",
1083
1115
  method: "PUT",
1084
1116
  headers,
1085
1117
  // BodyInit não aceita Uint8Array<ArrayBufferLike> em todos os lib.dom; o buffer é o mesmo
@@ -1098,6 +1130,7 @@ var TasksApi = class {
1098
1130
  const response = await fetch(
1099
1131
  `${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments/${encodeURIComponent(attachmentId)}`,
1100
1132
  {
1133
+ redirect: "error",
1101
1134
  method: "PATCH",
1102
1135
  headers: this.headers(),
1103
1136
  body: JSON.stringify(request),
@@ -1160,6 +1193,88 @@ function isTerminalError(error) {
1160
1193
  return error instanceof Error && TERMINAL_ERROR_NAMES.has(error.name);
1161
1194
  }
1162
1195
 
1196
+ // src/tasks/each.ts
1197
+ function thrownMessage(error) {
1198
+ if (error instanceof Error) return error.message || error.name;
1199
+ return safeString(error);
1200
+ }
1201
+ var ItemHandle = class {
1202
+ constructor(index, id, step, basePayload, sink) {
1203
+ this.index = index;
1204
+ this.id = id;
1205
+ this.step = step;
1206
+ this.basePayload = basePayload;
1207
+ this.sink = sink;
1208
+ }
1209
+ index;
1210
+ id;
1211
+ step;
1212
+ basePayload;
1213
+ sink;
1214
+ reported = false;
1215
+ status;
1216
+ succeeded(payload, options) {
1217
+ this.record("succeeded", void 0, payload, options?.image);
1218
+ }
1219
+ failed(message, payload, options) {
1220
+ this.record("failed", message, payload, options?.image);
1221
+ if (this.step && this.step.status !== "error") this.step.setStatus("error", message);
1222
+ }
1223
+ occurrence(message, payload, options) {
1224
+ this.record("occurrence", message, payload, options?.image);
1225
+ if (this.step && this.step.status === "ok") this.step.setStatus("warning", message);
1226
+ }
1227
+ record(status, message, payload, image3) {
1228
+ this.reported = true;
1229
+ this.status = status;
1230
+ const merged = { ...this.basePayload ?? {}, ...payload ?? {} };
1231
+ this.sink.report(status, message, Object.keys(merged).length > 0 ? merged : void 0, this.id, image3);
1232
+ }
1233
+ };
1234
+ function resolveId(options, element, index) {
1235
+ const raw = options.id?.(element, index);
1236
+ if (raw === null || raw === void 0) return void 0;
1237
+ const text = safeString(raw);
1238
+ return text.length > 0 ? text : void 0;
1239
+ }
1240
+ function knownLength(list) {
1241
+ const length = list?.length;
1242
+ return typeof length === "number" && Number.isInteger(length) && length >= 1 ? length : void 0;
1243
+ }
1244
+ async function eachItem(list, handler, options, sink) {
1245
+ const result = { total: 0, succeeded: 0, failed: 0, occurrence: 0 };
1246
+ const length = options.progress === false ? void 0 : knownLength(list);
1247
+ if (length !== void 0) sink.declareTotal(length);
1248
+ let index = 0;
1249
+ for await (const element of list) {
1250
+ sink.checkCanceled();
1251
+ const id = resolveId(options, element, index);
1252
+ const description = options.description?.(element, index) ?? id ?? `#${index + 1}`;
1253
+ const step = sink.startStep(options.op ?? "item", description);
1254
+ const handle = new ItemHandle(index, id, step, options.payload?.(element, index), sink);
1255
+ try {
1256
+ await handler(element, handle, index);
1257
+ if (!handle.reported) handle.succeeded();
1258
+ } catch (error) {
1259
+ if (isTerminalError(error) || sink.isAborted()) {
1260
+ step?.setStatus("cancelled");
1261
+ step?.finish();
1262
+ throw error;
1263
+ }
1264
+ const message = thrownMessage(error);
1265
+ if (!handle.reported) handle.failed(message);
1266
+ step?.setError(error);
1267
+ sink.logError(`item ${description} falhou: ${message}`);
1268
+ }
1269
+ step?.finish();
1270
+ if (options.progress !== false) sink.advance();
1271
+ result.total += 1;
1272
+ result[handle.status ?? "succeeded"] += 1;
1273
+ index += 1;
1274
+ }
1275
+ return result;
1276
+ }
1277
+
1163
1278
  // src/retry.ts
1164
1279
  var DEFAULT_RETRIES = 3;
1165
1280
  var DEFAULT_DELAY_MS = 1e3;
@@ -1187,11 +1302,33 @@ async function retryWithBackoff(fn, options = {}) {
1187
1302
  }
1188
1303
 
1189
1304
  // src/tasks/attachments.ts
1190
- var MAX_ATTACHMENT_BYTES = 5242880;
1191
- var MAX_ATTACHMENTS_PER_RUN = 20;
1305
+ var MAX_ATTACHMENT_BYTES = WIRE_LIMITS.MAX_ATTACHMENT_BYTES;
1306
+ var MAX_ATTACHMENTS_PER_RUN = WIRE_LIMITS.MAX_ATTACHMENTS_PER_RUN;
1307
+ var MAX_ATTACHMENTS_PER_LOG_ENTRY = WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY;
1192
1308
  var MAX_CONCURRENT_UPLOADS = 3;
1193
- var MAX_CAPTION_CHARS = 200;
1309
+ var MAX_CAPTION_CHARS = WIRE_LIMITS.MAX_CAPTION_CHARS;
1194
1310
  var ATTACHMENT_DRAIN_MS = 15e3;
1311
+ var IMAGE_BRAND = /* @__PURE__ */ Symbol.for("hobots.image");
1312
+ var ImageAttachment = class {
1313
+ constructor(input, caption, contentType) {
1314
+ this.input = input;
1315
+ this.caption = caption;
1316
+ this.contentType = contentType;
1317
+ }
1318
+ input;
1319
+ caption;
1320
+ contentType;
1321
+ [IMAGE_BRAND] = true;
1322
+ };
1323
+ function image(input, options = {}) {
1324
+ return new ImageAttachment(input, options.caption, options.contentType);
1325
+ }
1326
+ function isImageAttachment(value) {
1327
+ return typeof value === "object" && value !== null && value[IMAGE_BRAND] === true;
1328
+ }
1329
+ function toImageAttachment(value) {
1330
+ return isImageAttachment(value) ? value : new ImageAttachment(value);
1331
+ }
1195
1332
  async function readImageFile(path) {
1196
1333
  if (!isNode()) {
1197
1334
  throw new Error("caminho de arquivo s\xF3 \xE9 suportado em Node");
@@ -1204,7 +1341,7 @@ function startsWith(bytes, signature, offset = 0) {
1204
1341
  return signature.every((byte, index) => bytes[offset + index] === byte);
1205
1342
  }
1206
1343
  var PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
1207
- var MAX_PNG_DIMENSION = 2e4;
1344
+ var MAX_PNG_DIMENSION = WIRE_LIMITS.MAX_IMAGE_DIMENSION_PX;
1208
1345
  var JPEG_MAGIC = [255, 216, 255];
1209
1346
  var RIFF_MAGIC = [82, 73, 70, 70];
1210
1347
  var WEBP_MAGIC = [87, 69, 66, 80];
@@ -1241,6 +1378,14 @@ function joinLogArgs(message, rest) {
1241
1378
  if (rest.length === 0) return safeString(message);
1242
1379
  return [message, ...rest].map(safeString).join(" ");
1243
1380
  }
1381
+ function splitLogArgs(message, rest) {
1382
+ const args = [message, ...rest];
1383
+ const last = args[args.length - 1];
1384
+ if (!isImageAttachment(last)) return { text: joinLogArgs(message, rest) };
1385
+ const text = args.slice(0, -1);
1386
+ if (text.length === 0) return { text: last.caption !== void 0 ? safeString(last.caption) : "captura de tela", image: last };
1387
+ return { text: joinLogArgs(text[0], text.slice(1)), image: last };
1388
+ }
1244
1389
  function printRunLog(task, level, message) {
1245
1390
  const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
1246
1391
  const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
@@ -1267,13 +1412,13 @@ var DEFAULT_LOG_FLUSH_INTERVAL_MS = 2e3;
1267
1412
  var DEFAULT_CANCEL_CHECK_INTERVAL_MS = 5e3;
1268
1413
  var MIN_CANCEL_CHECK_INTERVAL_MS = 1e3;
1269
1414
  var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
1270
- var MAX_LOG_MESSAGE_CHARS = 1e4;
1271
- var MAX_LOGS_PER_PATCH = 100;
1415
+ var MAX_LOG_MESSAGE_CHARS = WIRE_LIMITS.MAX_LOG_MESSAGE_CHARS;
1416
+ var MAX_LOGS_PER_PATCH = WIRE_LIMITS.MAX_LOGS_PER_PATCH;
1272
1417
  var FLUSH_AT_BUFFERED = 20;
1273
- var MAX_ITEMS_PER_RUN = 1e4;
1274
- var MAX_ITEMS_PER_REQUEST = 500;
1275
- var MAX_ITEM_MESSAGE_CHARS = 2e3;
1276
- var MAX_ITEM_PAYLOAD_BYTES = 16384;
1418
+ var MAX_ITEMS_PER_RUN = WIRE_LIMITS.MAX_ITEMS_PER_RUN;
1419
+ var MAX_ITEMS_PER_REQUEST = WIRE_LIMITS.MAX_ITEMS_PER_REQUEST;
1420
+ var MAX_ITEM_MESSAGE_CHARS = WIRE_LIMITS.MAX_ITEM_MESSAGE_CHARS;
1421
+ var MAX_ITEM_PAYLOAD_BYTES = WIRE_LIMITS.MAX_ITEM_PAYLOAD_BYTES;
1277
1422
  var ITEMS_FLUSH_AT_BUFFERED = 100;
1278
1423
  var ITEMS_DRAIN_MS = 15e3;
1279
1424
  var PROGRESS_FLUSH_MS = 1e3;
@@ -1297,33 +1442,50 @@ var RunReporter = class {
1297
1442
  MAX_CANCEL_CHECK_INTERVAL_MS,
1298
1443
  Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
1299
1444
  );
1445
+ const logAt = (level) => (message, ...rest) => {
1446
+ const { text, image: image3 } = splitLogArgs(message, rest);
1447
+ this.log(level, text, image3);
1448
+ };
1300
1449
  const log = {
1301
- debug: (message, ...rest) => this.log("debug", joinLogArgs(message, rest)),
1302
- info: (message, ...rest) => this.log("info", joinLogArgs(message, rest)),
1303
- warn: (message, ...rest) => this.log("warning", joinLogArgs(message, rest)),
1304
- error: (message, ...rest) => this.log("error", joinLogArgs(message, rest)),
1305
- success: (message, ...rest) => this.log("success", joinLogArgs(message, rest))
1450
+ debug: logAt("debug"),
1451
+ info: logAt("info"),
1452
+ warn: logAt("warning"),
1453
+ error: logAt("error"),
1454
+ success: logAt("success")
1306
1455
  };
1307
1456
  tx.setLifecycleSink((level, message) => this.push(level, message));
1457
+ tx.setAttachmentSink((image3, ts) => this.queueAttachment(image3, ts));
1308
1458
  const checkCanceled = () => this.checkCanceled();
1309
1459
  tx.setStepGuard(checkCanceled);
1460
+ const eachSink = {
1461
+ report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
1462
+ startStep: (op, description) => tx.startChild(op, description),
1463
+ declareTotal: (total) => {
1464
+ if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
1465
+ },
1466
+ advance: () => this.setProgress(this.progressCurrent + 1, this.progressTotal),
1467
+ checkCanceled,
1468
+ isAborted: () => this.abortController.signal.aborted,
1469
+ logError: (message) => this.log("error", message)
1470
+ };
1310
1471
  const items = {
1311
1472
  add: (item) => {
1312
1473
  checkCanceled();
1313
- this.addItem(item.status, item.message, item.payload, item.id);
1474
+ this.addItem(item.status, item.message, item.payload, item.id, item.image);
1314
1475
  },
1315
1476
  succeeded: (payload, options) => {
1316
1477
  checkCanceled();
1317
- this.addItem("succeeded", void 0, payload, options?.id);
1478
+ this.addItem("succeeded", void 0, payload, options?.id, options?.image);
1318
1479
  },
1319
1480
  failed: (message, payload, options) => {
1320
1481
  checkCanceled();
1321
- this.addItem("failed", message, payload, options?.id);
1482
+ this.addItem("failed", message, payload, options?.id, options?.image);
1322
1483
  },
1323
1484
  occurrence: (message, payload, options) => {
1324
1485
  checkCanceled();
1325
- this.addItem("occurrence", message, payload, options?.id);
1326
- }
1486
+ this.addItem("occurrence", message, payload, options?.id, options?.image);
1487
+ },
1488
+ each: (list, handler, options = {}) => eachItem(list, handler, options, eachSink)
1327
1489
  };
1328
1490
  const progress = Object.defineProperties(
1329
1491
  {
@@ -1362,7 +1524,6 @@ var RunReporter = class {
1362
1524
  signal,
1363
1525
  throwIfCanceled: checkCanceled,
1364
1526
  sleep: sleep2,
1365
- attach: (image, options) => this.attach(image, options),
1366
1527
  retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
1367
1528
  };
1368
1529
  }
@@ -1453,22 +1614,28 @@ var RunReporter = class {
1453
1614
  this.clearFlushTimer();
1454
1615
  return this.takeLogs().slice(-MAX_LOGS_PER_PATCH);
1455
1616
  }
1456
- log(level, message) {
1617
+ /**
1618
+ * Linha de log da run com eco no terminal. `image` (opcional) vira o anexo da linha:
1619
+ * o id entra na entrada ANTES de qualquer I/O — é isso que mantém a ordem dos logs
1620
+ * intacta e o handler livre da latência do upload.
1621
+ */
1622
+ log(level, message, image3) {
1457
1623
  if (this.dead) return;
1458
1624
  const scrubbed = scrubSecrets(safeString(message));
1459
- printRunLog(this.context.task, level, scrubbed);
1460
- this.push(level, scrubbed, true);
1625
+ const ts = nowISO();
1626
+ const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
1627
+ printRunLog(this.context.task, level, attachmentId ? `${scrubbed} [imagem]` : scrubbed);
1628
+ this.push(level, scrubbed, true, attachmentId ? [attachmentId] : void 0, ts);
1461
1629
  }
1462
1630
  /** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
1463
- push(level, message, alreadyScrubbed = false, attachments) {
1464
- const ts = nowISO();
1631
+ push(level, message, alreadyScrubbed = false, attachments, ts = nowISO()) {
1465
1632
  if (this.dead) return ts;
1466
1633
  const scrubbed = alreadyScrubbed ? message : scrubSecrets(message);
1467
1634
  this.buffer.push({
1468
1635
  ts,
1469
1636
  level,
1470
1637
  message: truncate(scrubbed, MAX_LOG_MESSAGE_CHARS),
1471
- ...attachments && attachments.length > 0 ? { attachments } : {}
1638
+ ...attachments && attachments.length > 0 ? { attachments: attachments.slice(0, WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY) } : {}
1472
1639
  });
1473
1640
  if (this.buffer.length >= FLUSH_AT_BUFFERED) {
1474
1641
  this.flushLogs();
@@ -1611,7 +1778,7 @@ var RunReporter = class {
1611
1778
  * de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
1612
1779
  * então um reporter morto não descarta: só um kill() de exit path descarta.
1613
1780
  */
1614
- addItem(status, message, payload, id) {
1781
+ addItem(status, message, payload, id, image3) {
1615
1782
  if (this.itemsClosed || this.itemsUnsupported) return;
1616
1783
  if (this.itemCount >= MAX_ITEMS_PER_RUN) {
1617
1784
  if (!this.itemsQuotaExceeded) {
@@ -1636,13 +1803,16 @@ var RunReporter = class {
1636
1803
  scrubbedPayload = void 0;
1637
1804
  }
1638
1805
  }
1806
+ const ts = nowISO();
1807
+ const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
1639
1808
  this.itemsBuffer.push({
1640
1809
  id: id ? truncate(String(id), 255) : uuid4(),
1641
1810
  seq: this.itemSeq++,
1642
1811
  status,
1643
1812
  ...message ? { message: truncate(scrubSecrets(safeString(message)), MAX_ITEM_MESSAGE_CHARS) } : {},
1644
1813
  ...scrubbedPayload ? { payload: scrubbedPayload } : {},
1645
- ts: nowISO()
1814
+ ts,
1815
+ ...attachmentId ? { attachments: [attachmentId] } : {}
1646
1816
  });
1647
1817
  if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
1648
1818
  this.flushItems();
@@ -1722,59 +1892,51 @@ var RunReporter = class {
1722
1892
  }
1723
1893
  // ── Anexos ───────────────────────────────────────────────────────────────────
1724
1894
  /**
1725
- * Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
1726
- * o upload em segundo plano. Nunca lança: todo caminho de erro vira `{ ok: false }`
1727
- * com um aviso no log da run. É o `hobots.attach()`; o hint `attachment` de
1728
- * `hobots.captureException` também cai aqui via run-context-store.
1895
+ * Pendura uma imagem num evento da run (log, item ou step): faz as checagens que não
1896
+ * dependem de I/O (reporter morto, instalação sem anexos, cota), reserva o id e dispara
1897
+ * o upload em segundo plano. Devolve o id — já referenciável pelo evento — ou undefined
1898
+ * quando a imagem foi descartada (sempre com aviso no log da run). Nunca lança: todo
1899
+ * caminho de erro do upload vira aviso. O hint `attachment` de `hobots.captureException`
1900
+ * também cai aqui via `log()`.
1729
1901
  */
1730
- async attach(image, options = {}) {
1731
- const id = uuid4();
1732
- const level = options.level ?? "info";
1733
- const rawCaption = options.caption !== void 0 ? safeString(options.caption) : void 0;
1734
- const rawMessage = options.message !== void 0 ? safeString(options.message) : void 0;
1735
- const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
1736
- const message = rawMessage ?? rawCaption ?? "captura de tela";
1737
- if (this.dead) return { id, ok: false, reason: "canceled" };
1738
- if (this.attachmentsUnsupported) {
1739
- this.log(level, message);
1740
- return { id, ok: false, reason: "unsupported" };
1741
- }
1902
+ queueAttachment(imageIn, ts) {
1903
+ if (this.dead) return void 0;
1904
+ const image3 = toImageAttachment(imageIn);
1905
+ if (this.attachmentsUnsupported) return void 0;
1742
1906
  if (this.attachmentCount >= MAX_ATTACHMENTS_PER_RUN) {
1743
- this.log(level, message);
1744
- this.log("warning", `anexo descartado: limite de ${MAX_ATTACHMENTS_PER_RUN} por execu\xE7\xE3o atingido`);
1745
- return { id, ok: false, reason: "quota" };
1907
+ if (this.attachmentCount === MAX_ATTACHMENTS_PER_RUN) {
1908
+ this.attachmentCount++;
1909
+ this.log("warning", `imagem descartada: limite de ${MAX_ATTACHMENTS_PER_RUN} por execu\xE7\xE3o atingido`);
1910
+ }
1911
+ return void 0;
1746
1912
  }
1747
1913
  this.attachmentCount++;
1748
- const scrubbed = scrubSecrets(message);
1749
- printRunLog(this.context.task, level, `${scrubbed} [imagem]`);
1750
- const logTs = this.push(level, scrubbed, true, [id]);
1751
- let settle;
1752
- const result = new Promise((resolve2) => {
1753
- settle = resolve2;
1754
- });
1755
- const work = this.uploadAttachment(id, image, { caption, logTs, contentType: options.contentType }).then((reason) => settle(reason ? { id, ok: false, reason } : { id, ok: true })).catch(() => settle({ id, ok: false, reason: "upload-failed" }));
1914
+ const id = uuid4();
1915
+ const rawCaption = image3.caption !== void 0 ? safeString(image3.caption) : void 0;
1916
+ const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
1917
+ const work = this.uploadAttachment(id, image3.input, { caption, logTs: ts, contentType: image3.contentType }).then(() => void 0).catch(() => void 0);
1756
1918
  this.pendingAttachments.add(work);
1757
1919
  void work.finally(() => this.pendingAttachments.delete(work));
1758
- return result;
1920
+ return id;
1759
1921
  }
1760
1922
  /** Retorna o motivo da falha, ou undefined em sucesso. */
1761
- async uploadAttachment(id, image, meta) {
1923
+ async uploadAttachment(id, image3, meta) {
1762
1924
  let bytes;
1763
1925
  try {
1764
- bytes = await toBytes(image);
1926
+ bytes = await toBytes(image3);
1765
1927
  } catch (error) {
1766
- this.log("warning", `anexo n\xE3o p\xF4de ser lido: ${String(error)}`);
1928
+ this.log("warning", `imagem n\xE3o p\xF4de ser lida: ${String(error)}`);
1767
1929
  return "read-failed";
1768
1930
  }
1769
1931
  const contentType = sniffContentType(bytes) ?? meta.contentType;
1770
1932
  if (!contentType) {
1771
- this.log("warning", "anexo descartado: formato n\xE3o reconhecido (aceitos: PNG, JPEG, WEBP)");
1933
+ this.log("warning", "imagem descartada: formato n\xE3o reconhecido (aceitos: PNG, JPEG, WEBP)");
1772
1934
  return "unsupported-format";
1773
1935
  }
1774
1936
  if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {
1775
1937
  this.log(
1776
1938
  "warning",
1777
- `anexo descartado: ${Math.round(bytes.byteLength / 1024)} KB excede o limite de ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB`
1939
+ `imagem descartada: ${Math.round(bytes.byteLength / 1024)} KB excede o limite de ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB`
1778
1940
  );
1779
1941
  return "too-large";
1780
1942
  }
@@ -1805,11 +1967,11 @@ var RunReporter = class {
1805
1967
  return "unsupported";
1806
1968
  }
1807
1969
  if (error instanceof TasksApiError && error.status === 429) {
1808
- this.log("warning", "anexo descartado: limite de anexos por execu\xE7\xE3o atingido no servidor");
1970
+ this.log("warning", "imagem descartada: limite de anexos por execu\xE7\xE3o atingido no servidor");
1809
1971
  return "quota";
1810
1972
  }
1811
1973
  if (this.abortController.signal.aborted) return "canceled";
1812
- this.log("warning", `falha ao enviar o anexo: ${String(error)}`);
1974
+ this.log("warning", `falha ao enviar a imagem: ${String(error)}`);
1813
1975
  void this.api.confirmAttachment(this.runId, id, { status: "failed" }).catch(() => {
1814
1976
  });
1815
1977
  return "upload-failed";
@@ -1973,23 +2135,39 @@ var Step = class {
1973
2135
  /**
1974
2136
  * Desfecho da step. `message` (opcional) é o motivo legível — vai em `data.message`
1975
2137
  * e aparece na Central (performance e banner da execução) e no log de FINISH.
2138
+ * `options.image` pendura um print do estado da tela em `data.attachments`.
1976
2139
  */
1977
- setStatus(status, message) {
2140
+ setStatus(status, message, options) {
1978
2141
  this.status = status;
1979
2142
  if (message) this.setData("message", stepMessage(message));
2143
+ if (options?.image !== void 0) this.attachImage(options.image);
1980
2144
  return this;
1981
2145
  }
1982
2146
  /**
1983
2147
  * Marca a step como `error` guardando o motivo em `data.error` ({ type, message }).
1984
2148
  * Aceita qualquer valor lançado; `message` sobrescreve a mensagem do erro.
2149
+ * `options.image` pendura um print do estado da tela em `data.attachments`.
1985
2150
  */
1986
- setError(error, message) {
2151
+ setError(error, message, options) {
1987
2152
  this.status = "error";
1988
2153
  const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
1989
2154
  const raw = message ?? (error instanceof Error ? error.message || error.name : String(error));
1990
2155
  this.setData("error", { ...type ? { type } : {}, message: stepMessage(raw) });
2156
+ if (options?.image !== void 0) this.attachImage(options.image);
1991
2157
  return this;
1992
2158
  }
2159
+ /**
2160
+ * Dispara o upload pela run (sink do RunReporter) e referencia o anexo em
2161
+ * `data.attachments` (chave reservada; teto igual ao das linhas de log). Fora de uma
2162
+ * run não há onde pendurar a imagem — descarta com aviso único.
2163
+ */
2164
+ attachImage(image3) {
2165
+ const current = Array.isArray(this.data?.attachments) ? this.data.attachments : [];
2166
+ if (current.length >= MAX_ATTACHMENTS_PER_LOG_ENTRY) return;
2167
+ const id = this.transaction.attachImage(image3);
2168
+ if (!id) return;
2169
+ this.setData("attachments", [...current, id]);
2170
+ }
1993
2171
  /** Motivo registrado via setStatus(…, message) ou setError — usado no log de FINISH. */
1994
2172
  reason() {
1995
2173
  const error = this.data?.error;
@@ -2050,6 +2228,8 @@ var Transaction = class {
2050
2228
  tags = {};
2051
2229
  finished = false;
2052
2230
  lifecycleSink;
2231
+ attachmentSink;
2232
+ warnedNoAttachmentSink = false;
2053
2233
  // instalado pelo RunReporter: abrir uma step é um ponto de parada cooperativo (lança RunCanceled)
2054
2234
  stepGuard;
2055
2235
  // eventos emitidos antes do sink existir (o START da transaction sai no construtor,
@@ -2080,6 +2260,19 @@ var Transaction = class {
2080
2260
  this.lifecycleBacklog = [];
2081
2261
  for (const [level, message] of backlog) sink(level, message);
2082
2262
  }
2263
+ /** Conecta o pipeline de anexos da run — é o que dá imagem às steps. */
2264
+ setAttachmentSink(sink) {
2265
+ this.attachmentSink = sink;
2266
+ }
2267
+ /** Imagem de uma step → id do anexo pela run; sem run, descarta com aviso único. */
2268
+ attachImage(image3) {
2269
+ if (this.attachmentSink) return this.attachmentSink(image3, nowISO());
2270
+ if (!this.warnedNoAttachmentSink) {
2271
+ this.warnedNoAttachmentSink = true;
2272
+ console.warn("[hobots] imagem de step descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o de task");
2273
+ }
2274
+ return void 0;
2275
+ }
2083
2276
  /**
2084
2277
  * Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
2085
2278
  * `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
@@ -2169,6 +2362,7 @@ async function executeRun(input) {
2169
2362
  await runWithRunContext({ reporter }, () => invokeWithRetry(task, run, reporter, logger));
2170
2363
  await reporter.finalizeAttachments();
2171
2364
  await reporter.finalizeItems();
2365
+ warnIncompleteProgress(reporter);
2172
2366
  tx.setStatus("ok");
2173
2367
  tx.finish();
2174
2368
  reporter.reportSuccess();
@@ -2186,6 +2380,13 @@ async function executeRun(input) {
2186
2380
  tracker.remove(reporter);
2187
2381
  }
2188
2382
  }
2383
+ function warnIncompleteProgress(reporter) {
2384
+ const snapshot = reporter.context.progress.snapshot;
2385
+ if (!snapshot || snapshot.current >= snapshot.total) return;
2386
+ reporter.context.log.warn(
2387
+ `execu\xE7\xE3o conclu\xEDda com progresso incompleto: ${snapshot.current}/${snapshot.total} \u2014 o handler terminou sem processar (ou sem avan\xE7ar) todos os itens previstos`
2388
+ );
2389
+ }
2189
2390
  function invokeWithRetry(task, run, reporter, logger) {
2190
2391
  const { retries, retryDelayMs } = task.options;
2191
2392
  const total = retries + 1;
@@ -2240,8 +2441,8 @@ function waitForCancelGrace(signal, graceMs) {
2240
2441
  signal.addEventListener("abort", () => setTimeout(resolve2, graceMs), { once: true });
2241
2442
  });
2242
2443
  }
2243
- var MAX_ERROR_MESSAGE_CHARS = 1e4;
2244
- var MAX_ERROR_STACK_CHARS = 2e4;
2444
+ var MAX_ERROR_MESSAGE_CHARS = WIRE_LIMITS.MAX_ERROR_MESSAGE_CHARS;
2445
+ var MAX_ERROR_STACK_CHARS = WIRE_LIMITS.MAX_ERROR_STACK_CHARS;
2245
2446
  function toRunError(error) {
2246
2447
  if (error instanceof Error) {
2247
2448
  return {
@@ -2256,7 +2457,7 @@ function toRunError(error) {
2256
2457
  // src/tasks/agent.ts
2257
2458
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
2258
2459
  var MIN_POLL_INTERVAL_MS = 1e3;
2259
- var MAX_CLAIM_PER_POLL = 5;
2460
+ var MAX_CLAIM_PER_POLL = WIRE_LIMITS.MAX_CLAIM_PER_POLL;
2260
2461
  var TaskAgent = class {
2261
2462
  constructor(options) {
2262
2463
  this.options = options;
@@ -2391,8 +2592,8 @@ var TaskAgent = class {
2391
2592
 
2392
2593
  // src/tasks/item-columns.ts
2393
2594
  var KEY_RE = /^[a-zA-Z0-9_.-]{1,100}$/;
2394
- var MAX_COLUMNS = 12;
2395
- var MAX_LABEL_CHARS = 60;
2595
+ var MAX_COLUMNS = WIRE_LIMITS.MAX_ITEM_COLUMNS;
2596
+ var MAX_LABEL_CHARS = WIRE_LIMITS.MAX_ITEM_COLUMN_LABEL_CHARS;
2396
2597
  function normalizeItemColumns(slug, input, logger) {
2397
2598
  if (input === void 0 || input === null) return void 0;
2398
2599
  const columns = input.columns;
@@ -2839,13 +3040,8 @@ function forwardAttachment(rt, attachment, message, type) {
2839
3040
  rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task");
2840
3041
  return;
2841
3042
  }
2842
- const withMeta = typeof attachment === "object" && "image" in attachment ? attachment : { image: attachment };
2843
- void reporter.attach(withMeta.image, {
2844
- message,
2845
- level: toRunLogLevel(type),
2846
- caption: withMeta.caption,
2847
- contentType: withMeta.contentType
2848
- });
3043
+ const withMeta = typeof attachment === "object" && !isImageAttachment(attachment) && "image" in attachment ? image(attachment.image, { caption: attachment.caption, contentType: attachment.contentType }) : attachment;
3044
+ reporter.log(toRunLogLevel(type), message, withMeta);
2849
3045
  }
2850
3046
  function captureException(error, hint) {
2851
3047
  const rt = runtime;
@@ -2892,15 +3088,34 @@ function warnOutsideRun(member) {
2892
3088
  else console.warn(`[hobots] ${message}`);
2893
3089
  }
2894
3090
  var FALLBACK_TASK = "hobots";
3091
+ var fallbackLogAt = (level) => (message, ...rest) => {
3092
+ const { text, image: dropped } = splitLogArgs(message, rest);
3093
+ printRunLog(FALLBACK_TASK, level, dropped ? `${text} [imagem descartada]` : text);
3094
+ };
2895
3095
  var fallbackLog = {
2896
- debug: (message, ...rest) => printRunLog(FALLBACK_TASK, "debug", joinLogArgs(message, rest)),
2897
- info: (message, ...rest) => printRunLog(FALLBACK_TASK, "info", joinLogArgs(message, rest)),
2898
- warn: (message, ...rest) => printRunLog(FALLBACK_TASK, "warning", joinLogArgs(message, rest)),
2899
- error: (message, ...rest) => printRunLog(FALLBACK_TASK, "error", joinLogArgs(message, rest)),
2900
- success: (message, ...rest) => printRunLog(FALLBACK_TASK, "success", joinLogArgs(message, rest))
3096
+ debug: fallbackLogAt("debug"),
3097
+ info: fallbackLogAt("info"),
3098
+ warn: fallbackLogAt("warning"),
3099
+ error: fallbackLogAt("error"),
3100
+ success: fallbackLogAt("success")
2901
3101
  };
2902
3102
  var noop = () => void 0;
2903
- var fallbackItems = { add: noop, succeeded: noop, failed: noop, occurrence: noop };
3103
+ var fallbackEachSink = {
3104
+ report: noop,
3105
+ startStep: () => void 0,
3106
+ declareTotal: noop,
3107
+ advance: noop,
3108
+ checkCanceled: noop,
3109
+ isAborted: () => false,
3110
+ logError: (message) => printRunLog(FALLBACK_TASK, "error", message)
3111
+ };
3112
+ var fallbackItems = {
3113
+ add: noop,
3114
+ succeeded: noop,
3115
+ failed: noop,
3116
+ occurrence: noop,
3117
+ each: (list, handler, options = {}) => eachItem(list, handler, options, fallbackEachSink)
3118
+ };
2904
3119
  var fallbackProgress = { total: noop, advance: noop, set: noop, current: 0, snapshot: null };
2905
3120
  var neverAborts = new AbortController().signal;
2906
3121
  var detachedTransaction;
@@ -2928,9 +3143,6 @@ var hobots = Object.defineProperties({}, {
2928
3143
  signal: getter("signal", () => neverAborts),
2929
3144
  throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
2930
3145
  sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
2931
- attach: method(
2932
- (image, options) => resolve("attach")?.attach(image, options) ?? Promise.resolve({ id: "", ok: false, reason: "canceled" })
2933
- ),
2934
3146
  retry: method((fn, options) => {
2935
3147
  const reporter = currentReporter();
2936
3148
  return reporter ? reporter.context.retry(fn, options) : retryWithBackoff(fn, options);
@@ -2945,7 +3157,8 @@ var hobots = Object.defineProperties({}, {
2945
3157
  installGlobalHobots();
2946
3158
  }),
2947
3159
  close: method((timeoutMs) => close(timeoutMs)),
2948
- captureException: method((error, hint) => captureException(error, hint))
3160
+ captureException: method((error, hint) => captureException(error, hint)),
3161
+ image: method(image)
2949
3162
  });
2950
3163
  function installGlobalHobots() {
2951
3164
  if (globalThis.hobots === hobots) return;
@@ -2956,6 +3169,7 @@ var start2 = hobots.start;
2956
3169
  var close2 = hobots.close;
2957
3170
  var captureException2 = hobots.captureException;
2958
3171
  var retry = hobots.retry;
3172
+ var image2 = hobots.image;
2959
3173
 
2960
3174
  // src/index.ts
2961
3175
  var src_default = hobots;