hobots 3.3.0 → 4.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.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.3.0" : "0.0.0-dev";
99
+ var SDK_VERSION = true ? "4.0.0" : "0.0.0-dev";
99
100
  function isNode() {
100
101
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
101
102
  }
@@ -377,6 +378,7 @@ var Transport = class {
377
378
  try {
378
379
  response = await fetchWithRetry(
379
380
  () => fetch(this.endpoint, {
381
+ redirect: "error",
380
382
  method: "POST",
381
383
  headers: { "content-type": "application/json", authorization: `Bearer ${this.key}` },
382
384
  body: JSON.stringify(envelope),
@@ -554,7 +556,6 @@ function envBaseUrl() {
554
556
  function isLoopbackHost(hostname2) {
555
557
  return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
556
558
  }
557
- var warnedInsecureBaseUrl = false;
558
559
  function validateBaseUrl(raw, prefix) {
559
560
  let url;
560
561
  try {
@@ -568,11 +569,8 @@ function validateBaseUrl(raw, prefix) {
568
569
  if (url.username || url.password) {
569
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`);
570
571
  }
571
- if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
572
- warnedInsecureBaseUrl = true;
573
- console.warn(
574
- `${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
575
- );
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`);
576
574
  }
577
575
  return raw.replace(/\/+$/, "");
578
576
  }
@@ -582,7 +580,7 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
582
580
  const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
583
581
  if (!key) {
584
582
  throw new Error(
585
- `${prefix} Client Secret ausente \u2014 defina a env ${CLIENT_SECRET_ENV} (ou passe clientSecret nas op\xE7\xF5es da task); copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
583
+ `${prefix} Client Secret ausente \u2014 defina a env ${CLIENT_SECRET_ENV} (ou passe clientSecret nas op\xE7\xF5es do register/start); copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
586
584
  );
587
585
  }
588
586
  if (!SECRET_PATTERN.test(key)) {
@@ -595,7 +593,7 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
595
593
  key,
596
594
  baseUrl,
597
595
  ingestEndpoint: `${baseUrl}/v1/ingest`,
598
- tasksBaseUrl: `${baseUrl}/v1/tasks`
596
+ runsBaseUrl: `${baseUrl}/v1/runs`
599
597
  };
600
598
  }
601
599
 
@@ -936,6 +934,7 @@ var TasksApi = class {
936
934
  let response;
937
935
  try {
938
936
  response = await fetch(`${this.baseUrl}/poll`, {
937
+ redirect: "error",
939
938
  method: "POST",
940
939
  headers: this.headers(),
941
940
  body: JSON.stringify(request),
@@ -958,7 +957,8 @@ var TasksApi = class {
958
957
  const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
959
958
  let response;
960
959
  try {
961
- response = await fetch(`${this.baseUrl}/runs`, {
960
+ response = await fetch(`${this.baseUrl}`, {
961
+ redirect: "error",
962
962
  method: "POST",
963
963
  headers: this.headers(),
964
964
  body: JSON.stringify(request),
@@ -971,12 +971,11 @@ var TasksApi = class {
971
971
  throw new TasksApiError(`cria\xE7\xE3o da run falhou (HTTP ${response.status})`, response.status);
972
972
  }
973
973
  const body = await response.json().catch(() => null);
974
- if (typeof body?.id !== "string" || typeof body.task !== "string") {
975
- throw new TasksApiError("resposta inv\xE1lida da cria\xE7\xE3o de run (id/task ausentes)", 0);
974
+ if (typeof body?.id !== "string") {
975
+ throw new TasksApiError("resposta inv\xE1lida da cria\xE7\xE3o de run (id ausente)", 0);
976
976
  }
977
977
  return {
978
978
  id: body.id,
979
- task: body.task,
980
979
  params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
981
980
  form: null,
982
981
  source: typeof body.source === "string" ? body.source : "agent",
@@ -991,9 +990,9 @@ var TasksApi = class {
991
990
  }
992
991
  const valid = [];
993
992
  for (const run of runs.slice(0, Math.max(0, max))) {
994
- if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
993
+ if (typeof run?.id !== "string") continue;
995
994
  const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
996
- valid.push({ ...run, id: run.id, task: run.task, params });
995
+ valid.push({ ...run, id: run.id, params });
997
996
  }
998
997
  if (valid.length < runs.length) {
999
998
  this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
@@ -1008,7 +1007,8 @@ var TasksApi = class {
1008
1007
  let response;
1009
1008
  try {
1010
1009
  response = await fetchWithRetry(
1011
- () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
1010
+ () => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}`, {
1011
+ redirect: "error",
1012
1012
  method: "PATCH",
1013
1013
  headers: this.headers(),
1014
1014
  body: JSON.stringify(update),
@@ -1029,7 +1029,8 @@ var TasksApi = class {
1029
1029
  * que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
1030
1030
  */
1031
1031
  async updateRunOnce(runId, update, timeoutMs = 2e3) {
1032
- await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
1032
+ await fetch(`${this.baseUrl}/${encodeURIComponent(runId)}`, {
1033
+ redirect: "error",
1033
1034
  method: "PATCH",
1034
1035
  headers: this.headers(),
1035
1036
  body: JSON.stringify(update),
@@ -1045,7 +1046,8 @@ var TasksApi = class {
1045
1046
  let response;
1046
1047
  try {
1047
1048
  response = await fetchWithRetry(
1048
- () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/items`, {
1049
+ () => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/items`, {
1050
+ redirect: "error",
1049
1051
  method: "POST",
1050
1052
  headers: this.headers(),
1051
1053
  body: JSON.stringify(request),
@@ -1074,7 +1076,8 @@ var TasksApi = class {
1074
1076
  () => withRequestTimeout(
1075
1077
  UPDATE_TIMEOUT_MS,
1076
1078
  signal,
1077
- (timeoutSignal) => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments`, {
1079
+ (timeoutSignal) => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/attachments`, {
1080
+ redirect: "error",
1078
1081
  method: "POST",
1079
1082
  headers: this.headers(),
1080
1083
  body: JSON.stringify(request),
@@ -1107,6 +1110,7 @@ var TasksApi = class {
1107
1110
  UPLOAD_TIMEOUT_MS,
1108
1111
  signal,
1109
1112
  (timeoutSignal) => fetch(url, {
1113
+ redirect: "error",
1110
1114
  method: "PUT",
1111
1115
  headers,
1112
1116
  // BodyInit não aceita Uint8Array<ArrayBufferLike> em todos os lib.dom; o buffer é o mesmo
@@ -1123,8 +1127,9 @@ var TasksApi = class {
1123
1127
  /** Marca o anexo como enviado (ou falho). Best-effort — a leitura não depende disso. */
1124
1128
  async confirmAttachment(runId, attachmentId, request) {
1125
1129
  const response = await fetch(
1126
- `${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments/${encodeURIComponent(attachmentId)}`,
1130
+ `${this.baseUrl}/${encodeURIComponent(runId)}/attachments/${encodeURIComponent(attachmentId)}`,
1127
1131
  {
1132
+ redirect: "error",
1128
1133
  method: "PATCH",
1129
1134
  headers: this.headers(),
1130
1135
  body: JSON.stringify(request),
@@ -1207,22 +1212,22 @@ var ItemHandle = class {
1207
1212
  sink;
1208
1213
  reported = false;
1209
1214
  status;
1210
- succeeded(payload) {
1211
- this.record("succeeded", void 0, payload);
1215
+ succeeded(payload, options) {
1216
+ this.record("succeeded", void 0, payload, options?.image);
1212
1217
  }
1213
- failed(message, payload) {
1214
- this.record("failed", message, payload);
1218
+ failed(message, payload, options) {
1219
+ this.record("failed", message, payload, options?.image);
1215
1220
  if (this.step && this.step.status !== "error") this.step.setStatus("error", message);
1216
1221
  }
1217
- occurrence(message, payload) {
1218
- this.record("occurrence", message, payload);
1222
+ occurrence(message, payload, options) {
1223
+ this.record("occurrence", message, payload, options?.image);
1219
1224
  if (this.step && this.step.status === "ok") this.step.setStatus("warning", message);
1220
1225
  }
1221
- record(status, message, payload) {
1226
+ record(status, message, payload, image3) {
1222
1227
  this.reported = true;
1223
1228
  this.status = status;
1224
1229
  const merged = { ...this.basePayload ?? {}, ...payload ?? {} };
1225
- this.sink.report(status, message, Object.keys(merged).length > 0 ? merged : void 0, this.id);
1230
+ this.sink.report(status, message, Object.keys(merged).length > 0 ? merged : void 0, this.id, image3);
1226
1231
  }
1227
1232
  };
1228
1233
  function resolveId(options, element, index) {
@@ -1302,6 +1307,27 @@ var MAX_ATTACHMENTS_PER_LOG_ENTRY = WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY;
1302
1307
  var MAX_CONCURRENT_UPLOADS = 3;
1303
1308
  var MAX_CAPTION_CHARS = WIRE_LIMITS.MAX_CAPTION_CHARS;
1304
1309
  var ATTACHMENT_DRAIN_MS = 15e3;
1310
+ var IMAGE_BRAND = /* @__PURE__ */ Symbol.for("hobots.image");
1311
+ var ImageAttachment = class {
1312
+ constructor(input, caption, contentType) {
1313
+ this.input = input;
1314
+ this.caption = caption;
1315
+ this.contentType = contentType;
1316
+ }
1317
+ input;
1318
+ caption;
1319
+ contentType;
1320
+ [IMAGE_BRAND] = true;
1321
+ };
1322
+ function image(input, options = {}) {
1323
+ return new ImageAttachment(input, options.caption, options.contentType);
1324
+ }
1325
+ function isImageAttachment(value) {
1326
+ return typeof value === "object" && value !== null && value[IMAGE_BRAND] === true;
1327
+ }
1328
+ function toImageAttachment(value) {
1329
+ return isImageAttachment(value) ? value : new ImageAttachment(value);
1330
+ }
1305
1331
  async function readImageFile(path) {
1306
1332
  if (!isNode()) {
1307
1333
  throw new Error("caminho de arquivo s\xF3 \xE9 suportado em Node");
@@ -1351,8 +1377,16 @@ function joinLogArgs(message, rest) {
1351
1377
  if (rest.length === 0) return safeString(message);
1352
1378
  return [message, ...rest].map(safeString).join(" ");
1353
1379
  }
1354
- function printRunLog(task, level, message) {
1355
- const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
1380
+ function splitLogArgs(message, rest) {
1381
+ const args = [message, ...rest];
1382
+ const last = args[args.length - 1];
1383
+ if (!isImageAttachment(last)) return { text: joinLogArgs(message, rest) };
1384
+ const text = args.slice(0, -1);
1385
+ if (text.length === 0) return { text: last.caption !== void 0 ? safeString(last.caption) : "captura de tela", image: last };
1386
+ return { text: joinLogArgs(text[0], text.slice(1)), image: last };
1387
+ }
1388
+ function printRunLog(level, message) {
1389
+ const line = colorize(`[hobots] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
1356
1390
  const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
1357
1391
  console[method2](line, message);
1358
1392
  }
@@ -1398,7 +1432,7 @@ function scrubPayloadValue(value, depth) {
1398
1432
  return out;
1399
1433
  }
1400
1434
  var RunReporter = class {
1401
- constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
1435
+ constructor(api, runId, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
1402
1436
  this.api = api;
1403
1437
  this.runId = runId;
1404
1438
  this.logger = logger;
@@ -1407,18 +1441,23 @@ var RunReporter = class {
1407
1441
  MAX_CANCEL_CHECK_INTERVAL_MS,
1408
1442
  Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
1409
1443
  );
1444
+ const logAt = (level) => (message, ...rest) => {
1445
+ const { text, image: image3 } = splitLogArgs(message, rest);
1446
+ this.log(level, text, image3);
1447
+ };
1410
1448
  const log = {
1411
- debug: (message, ...rest) => this.log("debug", joinLogArgs(message, rest)),
1412
- info: (message, ...rest) => this.log("info", joinLogArgs(message, rest)),
1413
- warn: (message, ...rest) => this.log("warning", joinLogArgs(message, rest)),
1414
- error: (message, ...rest) => this.log("error", joinLogArgs(message, rest)),
1415
- success: (message, ...rest) => this.log("success", joinLogArgs(message, rest))
1449
+ debug: logAt("debug"),
1450
+ info: logAt("info"),
1451
+ warn: logAt("warning"),
1452
+ error: logAt("error"),
1453
+ success: logAt("success")
1416
1454
  };
1417
1455
  tx.setLifecycleSink((level, message) => this.push(level, message));
1456
+ tx.setAttachmentSink((image3, ts) => this.queueAttachment(image3, ts));
1418
1457
  const checkCanceled = () => this.checkCanceled();
1419
1458
  tx.setStepGuard(checkCanceled);
1420
1459
  const eachSink = {
1421
- report: (status, message, payload, id) => this.addItem(status, message, payload, id),
1460
+ report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
1422
1461
  startStep: (op, description) => tx.startChild(op, description),
1423
1462
  declareTotal: (total) => {
1424
1463
  if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
@@ -1431,19 +1470,19 @@ var RunReporter = class {
1431
1470
  const items = {
1432
1471
  add: (item) => {
1433
1472
  checkCanceled();
1434
- this.addItem(item.status, item.message, item.payload, item.id);
1473
+ this.addItem(item.status, item.message, item.payload, item.id, item.image);
1435
1474
  },
1436
1475
  succeeded: (payload, options) => {
1437
1476
  checkCanceled();
1438
- this.addItem("succeeded", void 0, payload, options?.id);
1477
+ this.addItem("succeeded", void 0, payload, options?.id, options?.image);
1439
1478
  },
1440
1479
  failed: (message, payload, options) => {
1441
1480
  checkCanceled();
1442
- this.addItem("failed", message, payload, options?.id);
1481
+ this.addItem("failed", message, payload, options?.id, options?.image);
1443
1482
  },
1444
1483
  occurrence: (message, payload, options) => {
1445
1484
  checkCanceled();
1446
- this.addItem("occurrence", message, payload, options?.id);
1485
+ this.addItem("occurrence", message, payload, options?.id, options?.image);
1447
1486
  },
1448
1487
  each: (list, handler, options = {}) => eachItem(list, handler, options, eachSink)
1449
1488
  };
@@ -1475,7 +1514,6 @@ var RunReporter = class {
1475
1514
  const sleep2 = (ms) => cancelableSleep(ms, signal);
1476
1515
  this.context = {
1477
1516
  runId,
1478
- task,
1479
1517
  params,
1480
1518
  log,
1481
1519
  items,
@@ -1484,7 +1522,6 @@ var RunReporter = class {
1484
1522
  signal,
1485
1523
  throwIfCanceled: checkCanceled,
1486
1524
  sleep: sleep2,
1487
- attach: (image, options) => this.attach(image, options),
1488
1525
  retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
1489
1526
  };
1490
1527
  }
@@ -1575,15 +1612,21 @@ var RunReporter = class {
1575
1612
  this.clearFlushTimer();
1576
1613
  return this.takeLogs().slice(-MAX_LOGS_PER_PATCH);
1577
1614
  }
1578
- log(level, message) {
1615
+ /**
1616
+ * Linha de log da run com eco no terminal. `image` (opcional) vira o anexo da linha:
1617
+ * o id entra na entrada ANTES de qualquer I/O — é isso que mantém a ordem dos logs
1618
+ * intacta e o handler livre da latência do upload.
1619
+ */
1620
+ log(level, message, image3) {
1579
1621
  if (this.dead) return;
1580
1622
  const scrubbed = scrubSecrets(safeString(message));
1581
- printRunLog(this.context.task, level, scrubbed);
1582
- this.push(level, scrubbed, true);
1623
+ const ts = nowISO();
1624
+ const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
1625
+ printRunLog(level, attachmentId ? `${scrubbed} [imagem]` : scrubbed);
1626
+ this.push(level, scrubbed, true, attachmentId ? [attachmentId] : void 0, ts);
1583
1627
  }
1584
1628
  /** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
1585
- push(level, message, alreadyScrubbed = false, attachments) {
1586
- const ts = nowISO();
1629
+ push(level, message, alreadyScrubbed = false, attachments, ts = nowISO()) {
1587
1630
  if (this.dead) return ts;
1588
1631
  const scrubbed = alreadyScrubbed ? message : scrubSecrets(message);
1589
1632
  this.buffer.push({
@@ -1733,7 +1776,7 @@ var RunReporter = class {
1733
1776
  * de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
1734
1777
  * então um reporter morto não descarta: só um kill() de exit path descarta.
1735
1778
  */
1736
- addItem(status, message, payload, id) {
1779
+ addItem(status, message, payload, id, image3) {
1737
1780
  if (this.itemsClosed || this.itemsUnsupported) return;
1738
1781
  if (this.itemCount >= MAX_ITEMS_PER_RUN) {
1739
1782
  if (!this.itemsQuotaExceeded) {
@@ -1758,13 +1801,16 @@ var RunReporter = class {
1758
1801
  scrubbedPayload = void 0;
1759
1802
  }
1760
1803
  }
1804
+ const ts = nowISO();
1805
+ const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
1761
1806
  this.itemsBuffer.push({
1762
1807
  id: id ? truncate(String(id), 255) : uuid4(),
1763
1808
  seq: this.itemSeq++,
1764
1809
  status,
1765
1810
  ...message ? { message: truncate(scrubSecrets(safeString(message)), MAX_ITEM_MESSAGE_CHARS) } : {},
1766
1811
  ...scrubbedPayload ? { payload: scrubbedPayload } : {},
1767
- ts: nowISO()
1812
+ ts,
1813
+ ...attachmentId ? { attachments: [attachmentId] } : {}
1768
1814
  });
1769
1815
  if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
1770
1816
  this.flushItems();
@@ -1844,59 +1890,51 @@ var RunReporter = class {
1844
1890
  }
1845
1891
  // ── Anexos ───────────────────────────────────────────────────────────────────
1846
1892
  /**
1847
- * Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
1848
- * o upload em segundo plano. Nunca lança: todo caminho de erro vira `{ ok: false }`
1849
- * com um aviso no log da run. É o `hobots.attach()`; o hint `attachment` de
1850
- * `hobots.captureException` também cai aqui via run-context-store.
1893
+ * Pendura uma imagem num evento da run (log, item ou step): faz as checagens que não
1894
+ * dependem de I/O (reporter morto, instalação sem anexos, cota), reserva o id e dispara
1895
+ * o upload em segundo plano. Devolve o id — já referenciável pelo evento — ou undefined
1896
+ * quando a imagem foi descartada (sempre com aviso no log da run). Nunca lança: todo
1897
+ * caminho de erro do upload vira aviso. O hint `attachment` de `hobots.captureException`
1898
+ * também cai aqui via `log()`.
1851
1899
  */
1852
- async attach(image, options = {}) {
1853
- const id = uuid4();
1854
- const level = options.level ?? "info";
1855
- const rawCaption = options.caption !== void 0 ? safeString(options.caption) : void 0;
1856
- const rawMessage = options.message !== void 0 ? safeString(options.message) : void 0;
1857
- const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
1858
- const message = rawMessage ?? rawCaption ?? "captura de tela";
1859
- if (this.dead) return { id, ok: false, reason: "canceled" };
1860
- if (this.attachmentsUnsupported) {
1861
- this.log(level, message);
1862
- return { id, ok: false, reason: "unsupported" };
1863
- }
1900
+ queueAttachment(imageIn, ts) {
1901
+ if (this.dead) return void 0;
1902
+ const image3 = toImageAttachment(imageIn);
1903
+ if (this.attachmentsUnsupported) return void 0;
1864
1904
  if (this.attachmentCount >= MAX_ATTACHMENTS_PER_RUN) {
1865
- this.log(level, message);
1866
- this.log("warning", `anexo descartado: limite de ${MAX_ATTACHMENTS_PER_RUN} por execu\xE7\xE3o atingido`);
1867
- return { id, ok: false, reason: "quota" };
1905
+ if (this.attachmentCount === MAX_ATTACHMENTS_PER_RUN) {
1906
+ this.attachmentCount++;
1907
+ this.log("warning", `imagem descartada: limite de ${MAX_ATTACHMENTS_PER_RUN} por execu\xE7\xE3o atingido`);
1908
+ }
1909
+ return void 0;
1868
1910
  }
1869
1911
  this.attachmentCount++;
1870
- const scrubbed = scrubSecrets(message);
1871
- printRunLog(this.context.task, level, `${scrubbed} [imagem]`);
1872
- const logTs = this.push(level, scrubbed, true, [id]);
1873
- let settle;
1874
- const result = new Promise((resolve2) => {
1875
- settle = resolve2;
1876
- });
1877
- 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" }));
1912
+ const id = uuid4();
1913
+ const rawCaption = image3.caption !== void 0 ? safeString(image3.caption) : void 0;
1914
+ const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
1915
+ const work = this.uploadAttachment(id, image3.input, { caption, logTs: ts, contentType: image3.contentType }).then(() => void 0).catch(() => void 0);
1878
1916
  this.pendingAttachments.add(work);
1879
1917
  void work.finally(() => this.pendingAttachments.delete(work));
1880
- return result;
1918
+ return id;
1881
1919
  }
1882
1920
  /** Retorna o motivo da falha, ou undefined em sucesso. */
1883
- async uploadAttachment(id, image, meta) {
1921
+ async uploadAttachment(id, image3, meta) {
1884
1922
  let bytes;
1885
1923
  try {
1886
- bytes = await toBytes(image);
1924
+ bytes = await toBytes(image3);
1887
1925
  } catch (error) {
1888
- this.log("warning", `anexo n\xE3o p\xF4de ser lido: ${String(error)}`);
1926
+ this.log("warning", `imagem n\xE3o p\xF4de ser lida: ${String(error)}`);
1889
1927
  return "read-failed";
1890
1928
  }
1891
1929
  const contentType = sniffContentType(bytes) ?? meta.contentType;
1892
1930
  if (!contentType) {
1893
- this.log("warning", "anexo descartado: formato n\xE3o reconhecido (aceitos: PNG, JPEG, WEBP)");
1931
+ this.log("warning", "imagem descartada: formato n\xE3o reconhecido (aceitos: PNG, JPEG, WEBP)");
1894
1932
  return "unsupported-format";
1895
1933
  }
1896
1934
  if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {
1897
1935
  this.log(
1898
1936
  "warning",
1899
- `anexo descartado: ${Math.round(bytes.byteLength / 1024)} KB excede o limite de ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB`
1937
+ `imagem descartada: ${Math.round(bytes.byteLength / 1024)} KB excede o limite de ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB`
1900
1938
  );
1901
1939
  return "too-large";
1902
1940
  }
@@ -1927,11 +1965,11 @@ var RunReporter = class {
1927
1965
  return "unsupported";
1928
1966
  }
1929
1967
  if (error instanceof TasksApiError && error.status === 429) {
1930
- this.log("warning", "anexo descartado: limite de anexos por execu\xE7\xE3o atingido no servidor");
1968
+ this.log("warning", "imagem descartada: limite de anexos por execu\xE7\xE3o atingido no servidor");
1931
1969
  return "quota";
1932
1970
  }
1933
1971
  if (this.abortController.signal.aborted) return "canceled";
1934
- this.log("warning", `falha ao enviar o anexo: ${String(error)}`);
1972
+ this.log("warning", `falha ao enviar a imagem: ${String(error)}`);
1935
1973
  void this.api.confirmAttachment(this.runId, id, { status: "failed" }).catch(() => {
1936
1974
  });
1937
1975
  return "upload-failed";
@@ -2095,23 +2133,39 @@ var Step = class {
2095
2133
  /**
2096
2134
  * Desfecho da step. `message` (opcional) é o motivo legível — vai em `data.message`
2097
2135
  * e aparece na Central (performance e banner da execução) e no log de FINISH.
2136
+ * `options.image` pendura um print do estado da tela em `data.attachments`.
2098
2137
  */
2099
- setStatus(status, message) {
2138
+ setStatus(status, message, options) {
2100
2139
  this.status = status;
2101
2140
  if (message) this.setData("message", stepMessage(message));
2141
+ if (options?.image !== void 0) this.attachImage(options.image);
2102
2142
  return this;
2103
2143
  }
2104
2144
  /**
2105
2145
  * Marca a step como `error` guardando o motivo em `data.error` ({ type, message }).
2106
2146
  * Aceita qualquer valor lançado; `message` sobrescreve a mensagem do erro.
2147
+ * `options.image` pendura um print do estado da tela em `data.attachments`.
2107
2148
  */
2108
- setError(error, message) {
2149
+ setError(error, message, options) {
2109
2150
  this.status = "error";
2110
2151
  const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
2111
2152
  const raw = message ?? (error instanceof Error ? error.message || error.name : String(error));
2112
2153
  this.setData("error", { ...type ? { type } : {}, message: stepMessage(raw) });
2154
+ if (options?.image !== void 0) this.attachImage(options.image);
2113
2155
  return this;
2114
2156
  }
2157
+ /**
2158
+ * Dispara o upload pela run (sink do RunReporter) e referencia o anexo em
2159
+ * `data.attachments` (chave reservada; teto igual ao das linhas de log). Fora de uma
2160
+ * run não há onde pendurar a imagem — descarta com aviso único.
2161
+ */
2162
+ attachImage(image3) {
2163
+ const current = Array.isArray(this.data?.attachments) ? this.data.attachments : [];
2164
+ if (current.length >= MAX_ATTACHMENTS_PER_LOG_ENTRY) return;
2165
+ const id = this.transaction.attachImage(image3);
2166
+ if (!id) return;
2167
+ this.setData("attachments", [...current, id]);
2168
+ }
2115
2169
  /** Motivo registrado via setStatus(…, message) ou setError — usado no log de FINISH. */
2116
2170
  reason() {
2117
2171
  const error = this.data?.error;
@@ -2172,6 +2226,8 @@ var Transaction = class {
2172
2226
  tags = {};
2173
2227
  finished = false;
2174
2228
  lifecycleSink;
2229
+ attachmentSink;
2230
+ warnedNoAttachmentSink = false;
2175
2231
  // instalado pelo RunReporter: abrir uma step é um ponto de parada cooperativo (lança RunCanceled)
2176
2232
  stepGuard;
2177
2233
  // eventos emitidos antes do sink existir (o START da transaction sai no construtor,
@@ -2202,6 +2258,19 @@ var Transaction = class {
2202
2258
  this.lifecycleBacklog = [];
2203
2259
  for (const [level, message] of backlog) sink(level, message);
2204
2260
  }
2261
+ /** Conecta o pipeline de anexos da run — é o que dá imagem às steps. */
2262
+ setAttachmentSink(sink) {
2263
+ this.attachmentSink = sink;
2264
+ }
2265
+ /** Imagem de uma step → id do anexo pela run; sem run, descarta com aviso único. */
2266
+ attachImage(image3) {
2267
+ if (this.attachmentSink) return this.attachmentSink(image3, nowISO());
2268
+ if (!this.warnedNoAttachmentSink) {
2269
+ this.warnedNoAttachmentSink = true;
2270
+ console.warn("[hobots] imagem de step descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o");
2271
+ }
2272
+ return void 0;
2273
+ }
2205
2274
  /**
2206
2275
  * Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
2207
2276
  * `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
@@ -2264,13 +2333,12 @@ var Transaction = class {
2264
2333
  // src/tasks/runner.ts
2265
2334
  var DEFAULT_RETRY_DELAY_MS = 5e3;
2266
2335
  async function executeRun(input) {
2267
- const { run, task, api, client, logger, tracker } = input;
2268
- const options = task.options;
2269
- const tx = new Transaction(client, `run:${run.task}`, "hobots.task", run.id);
2336
+ const { run, handler, api, client, logger, tracker } = input;
2337
+ const options = handler.options;
2338
+ const tx = new Transaction(client, "run", "hobots.run", run.id);
2270
2339
  const reporter = new RunReporter(
2271
2340
  api,
2272
2341
  run.id,
2273
- run.task,
2274
2342
  logger,
2275
2343
  tx,
2276
2344
  options.logFlushIntervalMs,
@@ -2286,9 +2354,9 @@ async function executeRun(input) {
2286
2354
  tx.finish();
2287
2355
  return;
2288
2356
  }
2289
- logger.debug(`executando run ${run.id} (${run.task})`);
2357
+ logger.debug(`executando run ${run.id}`);
2290
2358
  try {
2291
- await runWithRunContext({ reporter }, () => invokeWithRetry(task, run, reporter, logger));
2359
+ await runWithRunContext({ reporter }, () => invokeWithRetry(handler, run, reporter, logger));
2292
2360
  await reporter.finalizeAttachments();
2293
2361
  await reporter.finalizeItems();
2294
2362
  warnIncompleteProgress(reporter);
@@ -2316,11 +2384,11 @@ function warnIncompleteProgress(reporter) {
2316
2384
  `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`
2317
2385
  );
2318
2386
  }
2319
- function invokeWithRetry(task, run, reporter, logger) {
2320
- const { retries, retryDelayMs } = task.options;
2387
+ function invokeWithRetry(agent, run, reporter, logger) {
2388
+ const { retries, retryDelayMs } = agent.options;
2321
2389
  const total = retries + 1;
2322
2390
  const ctx = reporter.context;
2323
- return retryWithBackoff(() => invokeWithTimeout(task, run, reporter, logger), {
2391
+ return retryWithBackoff(() => invokeWithTimeout(agent, run, reporter, logger), {
2324
2392
  retries,
2325
2393
  delayMs: (attempt) => resolveDelay(retryDelayMs, attempt, DEFAULT_RETRY_DELAY_MS),
2326
2394
  shouldRetry: () => !reporter.isDead,
@@ -2334,9 +2402,9 @@ function invokeWithRetry(task, run, reporter, logger) {
2334
2402
  }
2335
2403
  var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
2336
2404
  var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
2337
- async function invokeWithTimeout(task, run, reporter, logger) {
2338
- const { timeoutMs, cancelGraceMs } = task.options;
2339
- const handler = task.handler;
2405
+ async function invokeWithTimeout(agent, run, reporter, logger) {
2406
+ const { timeoutMs, cancelGraceMs } = agent.options;
2407
+ const handler = agent.handler;
2340
2408
  const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
2341
2409
  const races = [];
2342
2410
  if (timeoutMs && timeoutMs > 0) {
@@ -2393,7 +2461,7 @@ var TaskAgent = class {
2393
2461
  this.logger = options.logger;
2394
2462
  this.api = options.api;
2395
2463
  this.client = options.client;
2396
- this.registry = options.registry;
2464
+ this.handler = options.handler;
2397
2465
  this.tracker = options.tracker;
2398
2466
  this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
2399
2467
  this.backoff = new Backoff(this.logger, "poll", this.pollIntervalMs);
@@ -2403,7 +2471,7 @@ var TaskAgent = class {
2403
2471
  pollIntervalMs;
2404
2472
  api;
2405
2473
  client;
2406
- registry;
2474
+ handler;
2407
2475
  tracker;
2408
2476
  backoff;
2409
2477
  state = "idle";
@@ -2419,10 +2487,10 @@ var TaskAgent = class {
2419
2487
  throw new Error("[hobots] start do agente durante o stop \u2014 aguarde o stop terminar");
2420
2488
  }
2421
2489
  this.state = "started";
2422
- this.logger.debug(`polling iniciado (tasks: ${this.registry.remoteTasks().map((task) => task.slug).join(", ")})`);
2490
+ this.logger.debug(`polling iniciado (concorr\xEAncia ${this.handler.options.concurrency})`);
2423
2491
  this.schedule(0);
2424
2492
  }
2425
- /** Uma task registrada depois do boot entra no próximo poll — que é antecipado para já. */
2493
+ /** Antecipa o próximo poll para já. */
2426
2494
  wake() {
2427
2495
  if (this.state !== "started") return;
2428
2496
  if (this.timer) {
@@ -2453,21 +2521,14 @@ var TaskAgent = class {
2453
2521
  void this.tick();
2454
2522
  }, delayMs);
2455
2523
  }
2456
- /** Slots livres por task remota — a soma vira o `max` do poll e as tasks com slot viram o filtro. */
2524
+ /** Slots livres do agente — vira o `max` do poll (nunca reivindica trabalho para deixar parado). */
2457
2525
  freeSlots() {
2458
- const tasks = [];
2459
- let max = 0;
2460
- for (const task of this.registry.remoteTasks()) {
2461
- const free = task.options.concurrency - this.tracker.activeFor(task.slug);
2462
- if (free <= 0) continue;
2463
- tasks.push(task.slug);
2464
- max += free;
2465
- }
2466
- return { tasks, max: Math.min(MAX_CLAIM_PER_POLL, max) };
2526
+ const free = this.handler.options.concurrency - this.tracker.activeCount;
2527
+ return Math.min(MAX_CLAIM_PER_POLL, Math.max(0, free));
2467
2528
  }
2468
2529
  async tick() {
2469
2530
  if (this.state !== "started") return;
2470
- const { tasks, max } = this.freeSlots();
2531
+ const max = this.freeSlots();
2471
2532
  if (max === 0) {
2472
2533
  this.schedule(this.pollIntervalMs);
2473
2534
  return;
@@ -2477,11 +2538,9 @@ var TaskAgent = class {
2477
2538
  const response = await this.api.poll(
2478
2539
  {
2479
2540
  // identidade canônica do agente no painel — o mesmo instanceId do heartbeat,
2480
- // então os dois canais convergem em um agente. `instance.tasks` lista TODAS as
2481
- // tasks (register + start); o filtro `tasks` abaixo só as que este poll pode rodar.
2541
+ // então os dois canais convergem em um agente
2482
2542
  instance: this.options.instanceInfo(),
2483
- max,
2484
- tasks
2543
+ max
2485
2544
  },
2486
2545
  this.pollAbort.signal
2487
2546
  );
@@ -2505,17 +2564,7 @@ var TaskAgent = class {
2505
2564
  this.activeRuns.add(promise);
2506
2565
  }
2507
2566
  async executeClaimed(run) {
2508
- const task = this.registry.get(run.task);
2509
- if (!task || task.mode !== "remote") {
2510
- const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", run.id);
2511
- const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx);
2512
- reporter.reportFailure(toRunError(Object.assign(new Error(`tarefa n\xE3o registrada no agente: ${run.task}`), { name: "HandlerNotFound" })));
2513
- await reporter.finalize();
2514
- tx.setStatus("error");
2515
- tx.finish();
2516
- return;
2517
- }
2518
- await executeRun({ run, task, api: this.api, client: this.client, logger: this.logger, tracker: this.tracker });
2567
+ await executeRun({ run, handler: this.handler, api: this.api, client: this.client, logger: this.logger, tracker: this.tracker });
2519
2568
  }
2520
2569
  };
2521
2570
 
@@ -2523,11 +2572,11 @@ var TaskAgent = class {
2523
2572
  var KEY_RE = /^[a-zA-Z0-9_.-]{1,100}$/;
2524
2573
  var MAX_COLUMNS = WIRE_LIMITS.MAX_ITEM_COLUMNS;
2525
2574
  var MAX_LABEL_CHARS = WIRE_LIMITS.MAX_ITEM_COLUMN_LABEL_CHARS;
2526
- function normalizeItemColumns(slug, input, logger) {
2575
+ function normalizeItemColumns(input, logger) {
2527
2576
  if (input === void 0 || input === null) return void 0;
2528
2577
  const columns = input.columns;
2529
2578
  if (typeof input !== "object" || !Array.isArray(columns)) {
2530
- logger.warn(`task "${slug}": items.columns precisa ser uma lista \u2014 declara\xE7\xE3o de colunas ignorada`);
2579
+ logger.warn(`items.columns precisa ser uma lista \u2014 declara\xE7\xE3o de colunas ignorada`);
2531
2580
  return void 0;
2532
2581
  }
2533
2582
  const result = [];
@@ -2537,15 +2586,15 @@ function normalizeItemColumns(slug, input, logger) {
2537
2586
  const rawLabel = typeof entry === "object" && entry !== null ? entry.label : void 0;
2538
2587
  const key = typeof rawKey === "string" ? rawKey.trim() : "";
2539
2588
  if (!KEY_RE.test(key)) {
2540
- logger.warn(`task "${slug}": coluna de item inv\xE1lida ignorada (${JSON.stringify(rawKey)}) \u2014 use letras, n\xFAmeros, ponto, h\xEDfen e underline`);
2589
+ logger.warn(`coluna de item inv\xE1lida ignorada (${JSON.stringify(rawKey)}) \u2014 use letras, n\xFAmeros, ponto, h\xEDfen e underline`);
2541
2590
  continue;
2542
2591
  }
2543
2592
  if (seen.has(key)) {
2544
- logger.warn(`task "${slug}": coluna de item "${key}" repetida ignorada`);
2593
+ logger.warn(`coluna de item "${key}" repetida ignorada`);
2545
2594
  continue;
2546
2595
  }
2547
2596
  if (result.length >= MAX_COLUMNS) {
2548
- logger.warn(`task "${slug}": no m\xE1ximo ${MAX_COLUMNS} colunas de item \u2014 "${key}" em diante ignoradas`);
2597
+ logger.warn(`no m\xE1ximo ${MAX_COLUMNS} colunas de item \u2014 "${key}" em diante ignoradas`);
2549
2598
  break;
2550
2599
  }
2551
2600
  seen.add(key);
@@ -2557,15 +2606,15 @@ function normalizeItemColumns(slug, input, logger) {
2557
2606
 
2558
2607
  // src/tasks/local.ts
2559
2608
  var LocalRunner = class {
2560
- constructor(task, deps) {
2561
- this.task = task;
2609
+ constructor(handler, deps) {
2610
+ this.handler = handler;
2562
2611
  this.deps = deps;
2563
- this.backoff = new Backoff(deps.logger, `cria\xE7\xE3o da run "${task.slug}"`);
2612
+ this.backoff = new Backoff(deps.logger, "cria\xE7\xE3o da run");
2564
2613
  this.done = new Promise((resolve2) => {
2565
2614
  this.resolveDone = resolve2;
2566
2615
  });
2567
2616
  }
2568
- task;
2617
+ handler;
2569
2618
  deps;
2570
2619
  abort = new AbortController();
2571
2620
  backoff;
@@ -2576,17 +2625,17 @@ var LocalRunner = class {
2576
2625
  try {
2577
2626
  const run = await this.createRun();
2578
2627
  if (!run) return;
2579
- this.deps.logger.debug(`run ${run.id} aberta para "${this.task.slug}" (modo start)`);
2628
+ this.deps.logger.debug(`run ${run.id} aberta (modo start)`);
2580
2629
  await executeRun({
2581
2630
  run,
2582
- task: this.task,
2631
+ handler: this.handler,
2583
2632
  api: this.deps.api,
2584
2633
  client: this.deps.client,
2585
2634
  logger: this.deps.logger,
2586
2635
  tracker: this.deps.tracker
2587
2636
  });
2588
2637
  } catch (error) {
2589
- this.deps.logger.error(`erro inesperado na task "${this.task.slug}":`, error);
2638
+ this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
2590
2639
  } finally {
2591
2640
  this.resolveDone();
2592
2641
  }
@@ -2599,7 +2648,7 @@ var LocalRunner = class {
2599
2648
  for (; ; ) {
2600
2649
  if (this.abort.signal.aborted) return void 0;
2601
2650
  try {
2602
- return await this.deps.api.createRun({ task: this.task.slug, instance: this.deps.instanceInfo() }, this.abort.signal);
2651
+ return await this.deps.api.createRun({ instance: this.deps.instanceInfo() }, this.abort.signal);
2603
2652
  } catch (error) {
2604
2653
  if (this.abort.signal.aborted) return void 0;
2605
2654
  await abortableSleep(this.backoff.next(error), this.abort.signal);
@@ -2609,45 +2658,6 @@ var LocalRunner = class {
2609
2658
  };
2610
2659
 
2611
2660
  // src/tasks/registry.ts
2612
- var TaskRegistry = class {
2613
- tasks = /* @__PURE__ */ new Map();
2614
- add(task) {
2615
- if (this.tasks.has(task.slug)) {
2616
- throw new Error(`[hobots] task "${task.slug}" j\xE1 registrada \u2014 cada slug s\xF3 pode ter um handler (register ou start)`);
2617
- }
2618
- this.tasks.set(task.slug, task);
2619
- }
2620
- get(slug) {
2621
- return this.tasks.get(slug);
2622
- }
2623
- get size() {
2624
- return this.tasks.size;
2625
- }
2626
- /** Tasks que chegam pelo poll (register). */
2627
- remoteTasks() {
2628
- return [...this.tasks.values()].filter((task) => task.mode === "remote");
2629
- }
2630
- /** Tasks abertas pelo próprio bot (start). */
2631
- localTasks() {
2632
- return [...this.tasks.values()].filter((task) => task.mode === "local");
2633
- }
2634
- /** Todos os slugs, para a presença da instância (`instance.tasks`). */
2635
- allSlugs() {
2636
- return [...this.tasks.keys()];
2637
- }
2638
- /** Colunas de itens declaradas no código, por slug (`instance.task_items`); undefined se nenhuma task declarou. */
2639
- itemDeclarations() {
2640
- let result;
2641
- for (const task of this.tasks.values()) {
2642
- if (!task.options.itemColumns) continue;
2643
- (result ??= {})[task.slug] = { attributes: task.options.itemColumns };
2644
- }
2645
- return result;
2646
- }
2647
- clear() {
2648
- this.tasks.clear();
2649
- }
2650
- };
2651
2661
  var RunTracker = class {
2652
2662
  constructor(api) {
2653
2663
  this.api = api;
@@ -2668,12 +2678,6 @@ var RunTracker = class {
2668
2678
  get activeCount() {
2669
2679
  return this.reporters.size;
2670
2680
  }
2671
- /** Runs ativas de uma task (concorrência por task no poll). */
2672
- activeFor(slug) {
2673
- let count = 0;
2674
- for (const reporter of this.reporters) if (reporter.context.task === slug) count++;
2675
- return count;
2676
- }
2677
2681
  /** A única run ativa, quando há exatamente uma — o que `hobots.*` usa fora de um contexto async. */
2678
2682
  singleActive() {
2679
2683
  if (this.reporters.size !== 1) return void 0;
@@ -2762,40 +2766,13 @@ var DEFAULT_RETRIES2 = 1;
2762
2766
  var DEFAULT_RETRY_DELAY_MS2 = 5e3;
2763
2767
  var DEFAULT_CANCEL_GRACE_MS = 1e4;
2764
2768
  var MAX_CONCURRENCY = 5;
2765
- var MAX_SLUG_CHARS = 255;
2766
- var PROCESS_LEVEL_KEYS = [
2767
- "clientSecret",
2768
- "instanceId",
2769
- "environment",
2770
- "release",
2771
- "tags",
2772
- "debug",
2773
- "machineMetrics",
2774
- "captureUnhandled",
2775
- "installExitHandlers"
2776
- ];
2777
2769
  var runtime;
2778
2770
  var warnedNoRuntime = false;
2779
2771
  function hostname() {
2780
2772
  if (!isNode()) return void 0;
2781
2773
  return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
2782
2774
  }
2783
- function sameValue(a, b) {
2784
- if (a === b) return true;
2785
- if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
2786
- return false;
2787
- }
2788
- function ensureRuntime(options) {
2789
- if (runtime) {
2790
- for (const key of PROCESS_LEVEL_KEYS) {
2791
- if (options[key] !== void 0 && !sameValue(options[key], runtime.options[key])) {
2792
- runtime.logger.warn(
2793
- `op\xE7\xE3o "${key}" divergente ignorada \u2014 o primeiro register()/start() define as op\xE7\xF5es do processo`
2794
- );
2795
- }
2796
- }
2797
- return runtime;
2798
- }
2775
+ function createRuntime(options, handler) {
2799
2776
  const config = resolveConfig({ clientSecret: options.clientSecret });
2800
2777
  const logger = makeLogger(options.debug ?? false);
2801
2778
  const environment = resolveEnvironmentOption(options.environment);
@@ -2803,19 +2780,19 @@ function ensureRuntime(options) {
2803
2780
  { environment, release: options.release, tags: options.tags, debug: options.debug },
2804
2781
  config
2805
2782
  );
2806
- const api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger });
2807
- runtime = {
2783
+ const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
2784
+ const rt = {
2808
2785
  options: { ...options, clientSecret: void 0 },
2809
2786
  config,
2810
2787
  client,
2811
2788
  logger,
2812
2789
  api,
2813
- registry: new TaskRegistry(),
2790
+ handler,
2814
2791
  tracker: new RunTracker(api),
2815
2792
  sampler: void 0,
2816
2793
  heartbeat: void 0,
2817
2794
  agent: void 0,
2818
- locals: /* @__PURE__ */ new Map(),
2795
+ local: void 0,
2819
2796
  instanceId: resolveInstanceId(options.instanceId),
2820
2797
  environment,
2821
2798
  pollIntervalMs: void 0,
@@ -2825,24 +2802,19 @@ function ensureRuntime(options) {
2825
2802
  uninstallGlobalHandlers: options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0,
2826
2803
  uninstallShutdown: void 0
2827
2804
  };
2828
- runtime.options.clientSecret = options.clientSecret;
2829
2805
  logger.debug("inicializado", { endpoint: config.ingestEndpoint });
2830
- return runtime;
2806
+ return rt;
2831
2807
  }
2832
- function validateTask(slug, handler, fn) {
2833
- if (typeof slug !== "string" || slug.trim().length === 0 || slug.length > MAX_SLUG_CHARS) {
2834
- throw new Error(`[hobots] ${fn}(): o slug da task precisa ser uma string de 1 a ${MAX_SLUG_CHARS} caracteres`);
2835
- }
2808
+ function validateHandler(handler, fn) {
2836
2809
  if (typeof handler !== "function") {
2837
- throw new Error(`[hobots] ${fn}('${slug}'): o handler precisa ser uma fun\xE7\xE3o`);
2810
+ throw new Error(`[hobots] ${fn}(): o handler precisa ser uma fun\xE7\xE3o \u2014 ${fn}(async (params) => { ... })`);
2838
2811
  }
2839
- return slug.trim();
2840
2812
  }
2841
2813
  function clampConcurrency(value) {
2842
2814
  if (value === void 0 || !Number.isFinite(value)) return 1;
2843
2815
  return Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(value)));
2844
2816
  }
2845
- function resolveTaskOptions(slug, options, mode, logger) {
2817
+ function resolveRunOptions(options, mode, logger) {
2846
2818
  const retries = options.retries === void 0 || !Number.isFinite(options.retries) ? DEFAULT_RETRIES2 : Math.max(0, Math.floor(options.retries));
2847
2819
  return {
2848
2820
  retries,
@@ -2852,7 +2824,7 @@ function resolveTaskOptions(slug, options, mode, logger) {
2852
2824
  logFlushIntervalMs: options.logFlushIntervalMs,
2853
2825
  cancelCheckIntervalMs: options.cancelCheckIntervalMs,
2854
2826
  cancelGraceMs: options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS,
2855
- itemColumns: normalizeItemColumns(slug, options.items, logger)
2827
+ itemColumns: normalizeItemColumns(options.items, logger)
2856
2828
  };
2857
2829
  }
2858
2830
  function instanceInfo(rt) {
@@ -2862,10 +2834,10 @@ function instanceInfo(rt) {
2862
2834
  host: hostname(),
2863
2835
  // ambiente da instância: junto do heartbeat, é como o servidor sabe onde o robô roda
2864
2836
  environment: rt.environment,
2865
- // TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
2866
- tasks: rt.registry.allSlugs(),
2837
+ // como esta instância trabalha — o painel mostra "sob demanda" / "sempre ligado"
2838
+ mode: rt.handler.mode === "remote" ? "on_demand" : "always_on",
2867
2839
  // colunas da aba "itens" declaradas no código — a plataforma usa enquanto o usuário não personalizar
2868
- task_items: rt.registry.itemDeclarations(),
2840
+ items: rt.handler.options.itemColumns ? { attributes: rt.handler.options.itemColumns } : void 0,
2869
2841
  // o servidor calcula a tolerância de offline (3×) a partir disto em agentes que fazem
2870
2842
  // poll; clampado à faixa aceita pelo schema (1s..15min)
2871
2843
  poll_interval_ms: rt.agent ? Math.min(Math.max(rt.agent.pollIntervalMs, 1e3), 9e5) : void 0,
@@ -2878,22 +2850,22 @@ function ensureAgent(rt) {
2878
2850
  api: rt.api,
2879
2851
  client: rt.client,
2880
2852
  logger: rt.logger,
2881
- registry: rt.registry,
2853
+ handler: rt.handler,
2882
2854
  tracker: rt.tracker,
2883
2855
  instanceInfo: () => instanceInfo(rt),
2884
2856
  pollIntervalMs: rt.pollIntervalMs
2885
2857
  });
2886
2858
  return rt.agent;
2887
2859
  }
2888
- function launchLocal(rt, task) {
2889
- const runner = new LocalRunner(task, {
2860
+ function launchLocal(rt) {
2861
+ const runner = new LocalRunner(rt.handler, {
2890
2862
  api: rt.api,
2891
2863
  client: rt.client,
2892
2864
  logger: rt.logger,
2893
2865
  tracker: rt.tracker,
2894
2866
  instanceInfo: () => instanceInfo(rt)
2895
2867
  });
2896
- rt.locals.set(task.slug, runner);
2868
+ rt.local = runner;
2897
2869
  void runner.run();
2898
2870
  }
2899
2871
  async function boot(rt) {
@@ -2915,8 +2887,8 @@ async function boot(rt) {
2915
2887
  if (rt.options.installExitHandlers !== false) {
2916
2888
  rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
2917
2889
  }
2918
- if (rt.registry.remoteTasks().length > 0) ensureAgent(rt).start();
2919
- for (const task of rt.registry.localTasks()) launchLocal(rt, task);
2890
+ if (rt.handler.mode === "remote") ensureAgent(rt).start();
2891
+ else launchLocal(rt);
2920
2892
  }
2921
2893
  function scheduleBoot(rt) {
2922
2894
  if (rt.bootScheduled) return;
@@ -2925,34 +2897,23 @@ function scheduleBoot(rt) {
2925
2897
  rt.bootPromise = boot(rt).catch((error) => rt.logger.error("falha ao iniciar o SDK:", error));
2926
2898
  });
2927
2899
  }
2928
- function addTask(fn, mode, task, handler, options) {
2929
- const slug = validateTask(task, handler, fn);
2930
- const rt = ensureRuntime(options);
2931
- if (mode === "remote" && options.pollIntervalMs !== void 0) {
2932
- if (rt.pollIntervalMs === void 0 && !rt.agent) rt.pollIntervalMs = options.pollIntervalMs;
2933
- else if (rt.pollIntervalMs !== options.pollIntervalMs) {
2934
- rt.logger.warn('op\xE7\xE3o "pollIntervalMs" divergente ignorada \u2014 o primeiro register() define o intervalo do poll');
2935
- }
2936
- }
2937
- const registered = { slug, mode, handler, options: resolveTaskOptions(slug, options, mode, rt.logger) };
2938
- rt.registry.add(registered);
2939
- if (!rt.booted) {
2940
- scheduleBoot(rt);
2941
- return;
2942
- }
2943
- if (mode === "remote") {
2944
- const agent = ensureAgent(rt);
2945
- agent.start();
2946
- agent.wake();
2947
- } else {
2948
- launchLocal(rt, registered);
2900
+ function startAgent(fn, mode, handler, options) {
2901
+ validateHandler(handler, fn);
2902
+ if (runtime) {
2903
+ throw new Error(`[hobots] ${fn}(): o agente j\xE1 foi iniciado \u2014 register()/start() s\xF3 pode ser chamado uma vez por processo (chame hobots.close() antes de iniciar de novo)`);
2949
2904
  }
2905
+ const logger = makeLogger(options.debug ?? false);
2906
+ const agentHandler = { mode, handler, options: resolveRunOptions(options, mode, logger) };
2907
+ const rt = createRuntime(options, agentHandler);
2908
+ if (mode === "remote" && options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
2909
+ runtime = rt;
2910
+ scheduleBoot(rt);
2950
2911
  }
2951
- function register(task, handler, options = {}) {
2952
- addTask("register", "remote", task, handler, options);
2912
+ function register(handler, options = {}) {
2913
+ startAgent("register", "remote", handler, options);
2953
2914
  }
2954
- function start(task, handler, options = {}) {
2955
- addTask("start", "local", task, handler, options);
2915
+ function start(handler, options = {}) {
2916
+ startAgent("start", "local", handler, options);
2956
2917
  }
2957
2918
  function currentReporter() {
2958
2919
  return currentRunReporter() ?? runtime?.tracker.singleActive();
@@ -2966,23 +2927,18 @@ function toRunLogLevel(type) {
2966
2927
  function forwardAttachment(rt, attachment, message, type) {
2967
2928
  const reporter = currentReporter();
2968
2929
  if (!reporter) {
2969
- rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task");
2930
+ rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o");
2970
2931
  return;
2971
2932
  }
2972
- const withMeta = typeof attachment === "object" && "image" in attachment ? attachment : { image: attachment };
2973
- void reporter.attach(withMeta.image, {
2974
- message,
2975
- level: toRunLogLevel(type),
2976
- caption: withMeta.caption,
2977
- contentType: withMeta.contentType
2978
- });
2933
+ const withMeta = typeof attachment === "object" && !isImageAttachment(attachment) && "image" in attachment ? image(attachment.image, { caption: attachment.caption, contentType: attachment.contentType }) : attachment;
2934
+ reporter.log(toRunLogLevel(type), message, withMeta);
2979
2935
  }
2980
2936
  function captureException(error, hint) {
2981
2937
  const rt = runtime;
2982
2938
  if (!rt) {
2983
2939
  if (!warnedNoRuntime) {
2984
2940
  warnedNoRuntime = true;
2985
- console.warn("[hobots] captureException antes de registrar uma task (register/start) \u2014 evento descartado");
2941
+ console.warn("[hobots] captureException antes de iniciar o agente (register/start) \u2014 evento descartado");
2986
2942
  }
2987
2943
  return void 0;
2988
2944
  }
@@ -2998,7 +2954,7 @@ async function close(timeoutMs = 3e4) {
2998
2954
  runtime = void 0;
2999
2955
  await rt.bootPromise;
3000
2956
  if (rt.agent) await rt.agent.stop(0);
3001
- for (const local of rt.locals.values()) local.stop();
2957
+ rt.local?.stop();
3002
2958
  const idle = await rt.tracker.waitIdle(timeoutMs);
3003
2959
  if (!idle) {
3004
2960
  rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
@@ -3016,18 +2972,21 @@ var warned = /* @__PURE__ */ new Set();
3016
2972
  function warnOutsideRun(member) {
3017
2973
  if (warned.has(member)) return;
3018
2974
  warned.add(member);
3019
- const message = `hobots.${member} usado fora de uma execu\xE7\xE3o de task \u2014 ignorado (registre a task com register()/start() e use dentro do handler)`;
2975
+ const message = `hobots.${member} usado fora de uma execu\xE7\xE3o \u2014 ignorado (inicie o agente com register()/start() e use dentro do handler)`;
3020
2976
  const logger = activeLogger();
3021
2977
  if (logger) logger.warn(message);
3022
2978
  else console.warn(`[hobots] ${message}`);
3023
2979
  }
3024
- var FALLBACK_TASK = "hobots";
2980
+ var fallbackLogAt = (level) => (message, ...rest) => {
2981
+ const { text, image: dropped } = splitLogArgs(message, rest);
2982
+ printRunLog(level, dropped ? `${text} [imagem descartada]` : text);
2983
+ };
3025
2984
  var fallbackLog = {
3026
- debug: (message, ...rest) => printRunLog(FALLBACK_TASK, "debug", joinLogArgs(message, rest)),
3027
- info: (message, ...rest) => printRunLog(FALLBACK_TASK, "info", joinLogArgs(message, rest)),
3028
- warn: (message, ...rest) => printRunLog(FALLBACK_TASK, "warning", joinLogArgs(message, rest)),
3029
- error: (message, ...rest) => printRunLog(FALLBACK_TASK, "error", joinLogArgs(message, rest)),
3030
- success: (message, ...rest) => printRunLog(FALLBACK_TASK, "success", joinLogArgs(message, rest))
2985
+ debug: fallbackLogAt("debug"),
2986
+ info: fallbackLogAt("info"),
2987
+ warn: fallbackLogAt("warning"),
2988
+ error: fallbackLogAt("error"),
2989
+ success: fallbackLogAt("success")
3031
2990
  };
3032
2991
  var noop = () => void 0;
3033
2992
  var fallbackEachSink = {
@@ -3037,7 +2996,7 @@ var fallbackEachSink = {
3037
2996
  advance: noop,
3038
2997
  checkCanceled: noop,
3039
2998
  isAborted: () => false,
3040
- logError: (message) => printRunLog(FALLBACK_TASK, "error", message)
2999
+ logError: (message) => printRunLog("error", message)
3041
3000
  };
3042
3001
  var fallbackItems = {
3043
3002
  add: noop,
@@ -3064,7 +3023,6 @@ function method(value) {
3064
3023
  var hobots = Object.defineProperties({}, {
3065
3024
  // ── contexto da run atual (getters) ─────────────────────────────────────────
3066
3025
  runId: getter("runId", () => ""),
3067
- task: getter("task", () => ""),
3068
3026
  params: getter("params", () => ({})),
3069
3027
  log: getter("log", () => fallbackLog),
3070
3028
  items: getter("items", () => fallbackItems),
@@ -3073,24 +3031,22 @@ var hobots = Object.defineProperties({}, {
3073
3031
  signal: getter("signal", () => neverAborts),
3074
3032
  throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
3075
3033
  sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
3076
- attach: method(
3077
- (image, options) => resolve("attach")?.attach(image, options) ?? Promise.resolve({ id: "", ok: false, reason: "canceled" })
3078
- ),
3079
3034
  retry: method((fn, options) => {
3080
3035
  const reporter = currentReporter();
3081
3036
  return reporter ? reporter.context.retry(fn, options) : retryWithBackoff(fn, options);
3082
3037
  }),
3083
3038
  // ── processo ────────────────────────────────────────────────────────────────
3084
- register: method((task, handler, options) => {
3085
- register(task, handler, options);
3039
+ register: method((handler, options) => {
3040
+ register(handler, options);
3086
3041
  installGlobalHobots();
3087
3042
  }),
3088
- start: method((task, handler, options) => {
3089
- start(task, handler, options);
3043
+ start: method((handler, options) => {
3044
+ start(handler, options);
3090
3045
  installGlobalHobots();
3091
3046
  }),
3092
3047
  close: method((timeoutMs) => close(timeoutMs)),
3093
- captureException: method((error, hint) => captureException(error, hint))
3048
+ captureException: method((error, hint) => captureException(error, hint)),
3049
+ image: method(image)
3094
3050
  });
3095
3051
  function installGlobalHobots() {
3096
3052
  if (globalThis.hobots === hobots) return;
@@ -3101,6 +3057,7 @@ var start2 = hobots.start;
3101
3057
  var close2 = hobots.close;
3102
3058
  var captureException2 = hobots.captureException;
3103
3059
  var retry = hobots.retry;
3060
+ var image2 = hobots.image;
3104
3061
 
3105
3062
  // src/index.ts
3106
3063
  var src_default = hobots;