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.js CHANGED
@@ -57,7 +57,7 @@ function scrubSecrets(text) {
57
57
 
58
58
  // src/utils.ts
59
59
  var SDK_NAME = "hobots";
60
- var SDK_VERSION = true ? "3.3.0" : "0.0.0-dev";
60
+ var SDK_VERSION = true ? "4.0.0" : "0.0.0-dev";
61
61
  function isNode() {
62
62
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
63
63
  }
@@ -312,6 +312,7 @@ var Transport = class {
312
312
  try {
313
313
  response = await fetchWithRetry(
314
314
  () => fetch(this.endpoint, {
315
+ redirect: "error",
315
316
  method: "POST",
316
317
  headers: { "content-type": "application/json", authorization: `Bearer ${this.key}` },
317
318
  body: JSON.stringify(envelope),
@@ -489,7 +490,6 @@ function envBaseUrl() {
489
490
  function isLoopbackHost(hostname2) {
490
491
  return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
491
492
  }
492
- var warnedInsecureBaseUrl = false;
493
493
  function validateBaseUrl(raw, prefix) {
494
494
  let url;
495
495
  try {
@@ -503,11 +503,8 @@ function validateBaseUrl(raw, prefix) {
503
503
  if (url.username || url.password) {
504
504
  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`);
505
505
  }
506
- if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
507
- warnedInsecureBaseUrl = true;
508
- console.warn(
509
- `${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
510
- );
506
+ if (url.protocol === "http:" && !isLoopbackHost(url.hostname)) {
507
+ throw new Error(`${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafegaria sem TLS; use https`);
511
508
  }
512
509
  return raw.replace(/\/+$/, "");
513
510
  }
@@ -517,7 +514,7 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
517
514
  const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
518
515
  if (!key) {
519
516
  throw new Error(
520
- `${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`
517
+ `${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`
521
518
  );
522
519
  }
523
520
  if (!SECRET_PATTERN.test(key)) {
@@ -530,7 +527,7 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
530
527
  key,
531
528
  baseUrl,
532
529
  ingestEndpoint: `${baseUrl}/v1/ingest`,
533
- tasksBaseUrl: `${baseUrl}/v1/tasks`
530
+ runsBaseUrl: `${baseUrl}/v1/runs`
534
531
  };
535
532
  }
536
533
 
@@ -871,6 +868,7 @@ var TasksApi = class {
871
868
  let response;
872
869
  try {
873
870
  response = await fetch(`${this.baseUrl}/poll`, {
871
+ redirect: "error",
874
872
  method: "POST",
875
873
  headers: this.headers(),
876
874
  body: JSON.stringify(request),
@@ -893,7 +891,8 @@ var TasksApi = class {
893
891
  const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
894
892
  let response;
895
893
  try {
896
- response = await fetch(`${this.baseUrl}/runs`, {
894
+ response = await fetch(`${this.baseUrl}`, {
895
+ redirect: "error",
897
896
  method: "POST",
898
897
  headers: this.headers(),
899
898
  body: JSON.stringify(request),
@@ -906,12 +905,11 @@ var TasksApi = class {
906
905
  throw new TasksApiError(`cria\xE7\xE3o da run falhou (HTTP ${response.status})`, response.status);
907
906
  }
908
907
  const body = await response.json().catch(() => null);
909
- if (typeof body?.id !== "string" || typeof body.task !== "string") {
910
- throw new TasksApiError("resposta inv\xE1lida da cria\xE7\xE3o de run (id/task ausentes)", 0);
908
+ if (typeof body?.id !== "string") {
909
+ throw new TasksApiError("resposta inv\xE1lida da cria\xE7\xE3o de run (id ausente)", 0);
911
910
  }
912
911
  return {
913
912
  id: body.id,
914
- task: body.task,
915
913
  params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
916
914
  form: null,
917
915
  source: typeof body.source === "string" ? body.source : "agent",
@@ -926,9 +924,9 @@ var TasksApi = class {
926
924
  }
927
925
  const valid = [];
928
926
  for (const run of runs.slice(0, Math.max(0, max))) {
929
- if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
927
+ if (typeof run?.id !== "string") continue;
930
928
  const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
931
- valid.push({ ...run, id: run.id, task: run.task, params });
929
+ valid.push({ ...run, id: run.id, params });
932
930
  }
933
931
  if (valid.length < runs.length) {
934
932
  this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
@@ -943,7 +941,8 @@ var TasksApi = class {
943
941
  let response;
944
942
  try {
945
943
  response = await fetchWithRetry(
946
- () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
944
+ () => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}`, {
945
+ redirect: "error",
947
946
  method: "PATCH",
948
947
  headers: this.headers(),
949
948
  body: JSON.stringify(update),
@@ -964,7 +963,8 @@ var TasksApi = class {
964
963
  * que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
965
964
  */
966
965
  async updateRunOnce(runId, update, timeoutMs = 2e3) {
967
- await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
966
+ await fetch(`${this.baseUrl}/${encodeURIComponent(runId)}`, {
967
+ redirect: "error",
968
968
  method: "PATCH",
969
969
  headers: this.headers(),
970
970
  body: JSON.stringify(update),
@@ -980,7 +980,8 @@ var TasksApi = class {
980
980
  let response;
981
981
  try {
982
982
  response = await fetchWithRetry(
983
- () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/items`, {
983
+ () => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/items`, {
984
+ redirect: "error",
984
985
  method: "POST",
985
986
  headers: this.headers(),
986
987
  body: JSON.stringify(request),
@@ -1009,7 +1010,8 @@ var TasksApi = class {
1009
1010
  () => withRequestTimeout(
1010
1011
  UPDATE_TIMEOUT_MS,
1011
1012
  signal,
1012
- (timeoutSignal) => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments`, {
1013
+ (timeoutSignal) => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/attachments`, {
1014
+ redirect: "error",
1013
1015
  method: "POST",
1014
1016
  headers: this.headers(),
1015
1017
  body: JSON.stringify(request),
@@ -1042,6 +1044,7 @@ var TasksApi = class {
1042
1044
  UPLOAD_TIMEOUT_MS,
1043
1045
  signal,
1044
1046
  (timeoutSignal) => fetch(url, {
1047
+ redirect: "error",
1045
1048
  method: "PUT",
1046
1049
  headers,
1047
1050
  // BodyInit não aceita Uint8Array<ArrayBufferLike> em todos os lib.dom; o buffer é o mesmo
@@ -1058,8 +1061,9 @@ var TasksApi = class {
1058
1061
  /** Marca o anexo como enviado (ou falho). Best-effort — a leitura não depende disso. */
1059
1062
  async confirmAttachment(runId, attachmentId, request) {
1060
1063
  const response = await fetch(
1061
- `${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments/${encodeURIComponent(attachmentId)}`,
1064
+ `${this.baseUrl}/${encodeURIComponent(runId)}/attachments/${encodeURIComponent(attachmentId)}`,
1062
1065
  {
1066
+ redirect: "error",
1063
1067
  method: "PATCH",
1064
1068
  headers: this.headers(),
1065
1069
  body: JSON.stringify(request),
@@ -1142,22 +1146,22 @@ var ItemHandle = class {
1142
1146
  sink;
1143
1147
  reported = false;
1144
1148
  status;
1145
- succeeded(payload) {
1146
- this.record("succeeded", void 0, payload);
1149
+ succeeded(payload, options) {
1150
+ this.record("succeeded", void 0, payload, options?.image);
1147
1151
  }
1148
- failed(message, payload) {
1149
- this.record("failed", message, payload);
1152
+ failed(message, payload, options) {
1153
+ this.record("failed", message, payload, options?.image);
1150
1154
  if (this.step && this.step.status !== "error") this.step.setStatus("error", message);
1151
1155
  }
1152
- occurrence(message, payload) {
1153
- this.record("occurrence", message, payload);
1156
+ occurrence(message, payload, options) {
1157
+ this.record("occurrence", message, payload, options?.image);
1154
1158
  if (this.step && this.step.status === "ok") this.step.setStatus("warning", message);
1155
1159
  }
1156
- record(status, message, payload) {
1160
+ record(status, message, payload, image3) {
1157
1161
  this.reported = true;
1158
1162
  this.status = status;
1159
1163
  const merged = { ...this.basePayload ?? {}, ...payload ?? {} };
1160
- this.sink.report(status, message, Object.keys(merged).length > 0 ? merged : void 0, this.id);
1164
+ this.sink.report(status, message, Object.keys(merged).length > 0 ? merged : void 0, this.id, image3);
1161
1165
  }
1162
1166
  };
1163
1167
  function resolveId(options, element, index) {
@@ -1237,6 +1241,27 @@ var MAX_ATTACHMENTS_PER_LOG_ENTRY = WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY;
1237
1241
  var MAX_CONCURRENT_UPLOADS = 3;
1238
1242
  var MAX_CAPTION_CHARS = WIRE_LIMITS.MAX_CAPTION_CHARS;
1239
1243
  var ATTACHMENT_DRAIN_MS = 15e3;
1244
+ var IMAGE_BRAND = /* @__PURE__ */ Symbol.for("hobots.image");
1245
+ var ImageAttachment = class {
1246
+ constructor(input, caption, contentType) {
1247
+ this.input = input;
1248
+ this.caption = caption;
1249
+ this.contentType = contentType;
1250
+ }
1251
+ input;
1252
+ caption;
1253
+ contentType;
1254
+ [IMAGE_BRAND] = true;
1255
+ };
1256
+ function image(input, options = {}) {
1257
+ return new ImageAttachment(input, options.caption, options.contentType);
1258
+ }
1259
+ function isImageAttachment(value) {
1260
+ return typeof value === "object" && value !== null && value[IMAGE_BRAND] === true;
1261
+ }
1262
+ function toImageAttachment(value) {
1263
+ return isImageAttachment(value) ? value : new ImageAttachment(value);
1264
+ }
1240
1265
  async function readImageFile(path) {
1241
1266
  if (!isNode()) {
1242
1267
  throw new Error("caminho de arquivo s\xF3 \xE9 suportado em Node");
@@ -1286,8 +1311,16 @@ function joinLogArgs(message, rest) {
1286
1311
  if (rest.length === 0) return safeString(message);
1287
1312
  return [message, ...rest].map(safeString).join(" ");
1288
1313
  }
1289
- function printRunLog(task, level, message) {
1290
- const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
1314
+ function splitLogArgs(message, rest) {
1315
+ const args = [message, ...rest];
1316
+ const last = args[args.length - 1];
1317
+ if (!isImageAttachment(last)) return { text: joinLogArgs(message, rest) };
1318
+ const text = args.slice(0, -1);
1319
+ if (text.length === 0) return { text: last.caption !== void 0 ? safeString(last.caption) : "captura de tela", image: last };
1320
+ return { text: joinLogArgs(text[0], text.slice(1)), image: last };
1321
+ }
1322
+ function printRunLog(level, message) {
1323
+ const line = colorize(`[hobots] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
1291
1324
  const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
1292
1325
  console[method2](line, message);
1293
1326
  }
@@ -1333,7 +1366,7 @@ function scrubPayloadValue(value, depth) {
1333
1366
  return out;
1334
1367
  }
1335
1368
  var RunReporter = class {
1336
- constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
1369
+ constructor(api, runId, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
1337
1370
  this.api = api;
1338
1371
  this.runId = runId;
1339
1372
  this.logger = logger;
@@ -1342,18 +1375,23 @@ var RunReporter = class {
1342
1375
  MAX_CANCEL_CHECK_INTERVAL_MS,
1343
1376
  Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
1344
1377
  );
1378
+ const logAt = (level) => (message, ...rest) => {
1379
+ const { text, image: image3 } = splitLogArgs(message, rest);
1380
+ this.log(level, text, image3);
1381
+ };
1345
1382
  const log = {
1346
- debug: (message, ...rest) => this.log("debug", joinLogArgs(message, rest)),
1347
- info: (message, ...rest) => this.log("info", joinLogArgs(message, rest)),
1348
- warn: (message, ...rest) => this.log("warning", joinLogArgs(message, rest)),
1349
- error: (message, ...rest) => this.log("error", joinLogArgs(message, rest)),
1350
- success: (message, ...rest) => this.log("success", joinLogArgs(message, rest))
1383
+ debug: logAt("debug"),
1384
+ info: logAt("info"),
1385
+ warn: logAt("warning"),
1386
+ error: logAt("error"),
1387
+ success: logAt("success")
1351
1388
  };
1352
1389
  tx.setLifecycleSink((level, message) => this.push(level, message));
1390
+ tx.setAttachmentSink((image3, ts) => this.queueAttachment(image3, ts));
1353
1391
  const checkCanceled = () => this.checkCanceled();
1354
1392
  tx.setStepGuard(checkCanceled);
1355
1393
  const eachSink = {
1356
- report: (status, message, payload, id) => this.addItem(status, message, payload, id),
1394
+ report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
1357
1395
  startStep: (op, description) => tx.startChild(op, description),
1358
1396
  declareTotal: (total) => {
1359
1397
  if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
@@ -1366,19 +1404,19 @@ var RunReporter = class {
1366
1404
  const items = {
1367
1405
  add: (item) => {
1368
1406
  checkCanceled();
1369
- this.addItem(item.status, item.message, item.payload, item.id);
1407
+ this.addItem(item.status, item.message, item.payload, item.id, item.image);
1370
1408
  },
1371
1409
  succeeded: (payload, options) => {
1372
1410
  checkCanceled();
1373
- this.addItem("succeeded", void 0, payload, options?.id);
1411
+ this.addItem("succeeded", void 0, payload, options?.id, options?.image);
1374
1412
  },
1375
1413
  failed: (message, payload, options) => {
1376
1414
  checkCanceled();
1377
- this.addItem("failed", message, payload, options?.id);
1415
+ this.addItem("failed", message, payload, options?.id, options?.image);
1378
1416
  },
1379
1417
  occurrence: (message, payload, options) => {
1380
1418
  checkCanceled();
1381
- this.addItem("occurrence", message, payload, options?.id);
1419
+ this.addItem("occurrence", message, payload, options?.id, options?.image);
1382
1420
  },
1383
1421
  each: (list, handler, options = {}) => eachItem(list, handler, options, eachSink)
1384
1422
  };
@@ -1410,7 +1448,6 @@ var RunReporter = class {
1410
1448
  const sleep2 = (ms) => cancelableSleep(ms, signal);
1411
1449
  this.context = {
1412
1450
  runId,
1413
- task,
1414
1451
  params,
1415
1452
  log,
1416
1453
  items,
@@ -1419,7 +1456,6 @@ var RunReporter = class {
1419
1456
  signal,
1420
1457
  throwIfCanceled: checkCanceled,
1421
1458
  sleep: sleep2,
1422
- attach: (image, options) => this.attach(image, options),
1423
1459
  retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
1424
1460
  };
1425
1461
  }
@@ -1510,15 +1546,21 @@ var RunReporter = class {
1510
1546
  this.clearFlushTimer();
1511
1547
  return this.takeLogs().slice(-MAX_LOGS_PER_PATCH);
1512
1548
  }
1513
- log(level, message) {
1549
+ /**
1550
+ * Linha de log da run com eco no terminal. `image` (opcional) vira o anexo da linha:
1551
+ * o id entra na entrada ANTES de qualquer I/O — é isso que mantém a ordem dos logs
1552
+ * intacta e o handler livre da latência do upload.
1553
+ */
1554
+ log(level, message, image3) {
1514
1555
  if (this.dead) return;
1515
1556
  const scrubbed = scrubSecrets(safeString(message));
1516
- printRunLog(this.context.task, level, scrubbed);
1517
- this.push(level, scrubbed, true);
1557
+ const ts = nowISO();
1558
+ const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
1559
+ printRunLog(level, attachmentId ? `${scrubbed} [imagem]` : scrubbed);
1560
+ this.push(level, scrubbed, true, attachmentId ? [attachmentId] : void 0, ts);
1518
1561
  }
1519
1562
  /** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
1520
- push(level, message, alreadyScrubbed = false, attachments) {
1521
- const ts = nowISO();
1563
+ push(level, message, alreadyScrubbed = false, attachments, ts = nowISO()) {
1522
1564
  if (this.dead) return ts;
1523
1565
  const scrubbed = alreadyScrubbed ? message : scrubSecrets(message);
1524
1566
  this.buffer.push({
@@ -1668,7 +1710,7 @@ var RunReporter = class {
1668
1710
  * de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
1669
1711
  * então um reporter morto não descarta: só um kill() de exit path descarta.
1670
1712
  */
1671
- addItem(status, message, payload, id) {
1713
+ addItem(status, message, payload, id, image3) {
1672
1714
  if (this.itemsClosed || this.itemsUnsupported) return;
1673
1715
  if (this.itemCount >= MAX_ITEMS_PER_RUN) {
1674
1716
  if (!this.itemsQuotaExceeded) {
@@ -1693,13 +1735,16 @@ var RunReporter = class {
1693
1735
  scrubbedPayload = void 0;
1694
1736
  }
1695
1737
  }
1738
+ const ts = nowISO();
1739
+ const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
1696
1740
  this.itemsBuffer.push({
1697
1741
  id: id ? truncate(String(id), 255) : uuid4(),
1698
1742
  seq: this.itemSeq++,
1699
1743
  status,
1700
1744
  ...message ? { message: truncate(scrubSecrets(safeString(message)), MAX_ITEM_MESSAGE_CHARS) } : {},
1701
1745
  ...scrubbedPayload ? { payload: scrubbedPayload } : {},
1702
- ts: nowISO()
1746
+ ts,
1747
+ ...attachmentId ? { attachments: [attachmentId] } : {}
1703
1748
  });
1704
1749
  if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
1705
1750
  this.flushItems();
@@ -1779,59 +1824,51 @@ var RunReporter = class {
1779
1824
  }
1780
1825
  // ── Anexos ───────────────────────────────────────────────────────────────────
1781
1826
  /**
1782
- * Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
1783
- * o upload em segundo plano. Nunca lança: todo caminho de erro vira `{ ok: false }`
1784
- * com um aviso no log da run. É o `hobots.attach()`; o hint `attachment` de
1785
- * `hobots.captureException` também cai aqui via run-context-store.
1827
+ * Pendura uma imagem num evento da run (log, item ou step): faz as checagens que não
1828
+ * dependem de I/O (reporter morto, instalação sem anexos, cota), reserva o id e dispara
1829
+ * o upload em segundo plano. Devolve o id — já referenciável pelo evento — ou undefined
1830
+ * quando a imagem foi descartada (sempre com aviso no log da run). Nunca lança: todo
1831
+ * caminho de erro do upload vira aviso. O hint `attachment` de `hobots.captureException`
1832
+ * também cai aqui via `log()`.
1786
1833
  */
1787
- async attach(image, options = {}) {
1788
- const id = uuid4();
1789
- const level = options.level ?? "info";
1790
- const rawCaption = options.caption !== void 0 ? safeString(options.caption) : void 0;
1791
- const rawMessage = options.message !== void 0 ? safeString(options.message) : void 0;
1792
- const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
1793
- const message = rawMessage ?? rawCaption ?? "captura de tela";
1794
- if (this.dead) return { id, ok: false, reason: "canceled" };
1795
- if (this.attachmentsUnsupported) {
1796
- this.log(level, message);
1797
- return { id, ok: false, reason: "unsupported" };
1798
- }
1834
+ queueAttachment(imageIn, ts) {
1835
+ if (this.dead) return void 0;
1836
+ const image3 = toImageAttachment(imageIn);
1837
+ if (this.attachmentsUnsupported) return void 0;
1799
1838
  if (this.attachmentCount >= MAX_ATTACHMENTS_PER_RUN) {
1800
- this.log(level, message);
1801
- this.log("warning", `anexo descartado: limite de ${MAX_ATTACHMENTS_PER_RUN} por execu\xE7\xE3o atingido`);
1802
- return { id, ok: false, reason: "quota" };
1839
+ if (this.attachmentCount === MAX_ATTACHMENTS_PER_RUN) {
1840
+ this.attachmentCount++;
1841
+ this.log("warning", `imagem descartada: limite de ${MAX_ATTACHMENTS_PER_RUN} por execu\xE7\xE3o atingido`);
1842
+ }
1843
+ return void 0;
1803
1844
  }
1804
1845
  this.attachmentCount++;
1805
- const scrubbed = scrubSecrets(message);
1806
- printRunLog(this.context.task, level, `${scrubbed} [imagem]`);
1807
- const logTs = this.push(level, scrubbed, true, [id]);
1808
- let settle;
1809
- const result = new Promise((resolve2) => {
1810
- settle = resolve2;
1811
- });
1812
- 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" }));
1846
+ const id = uuid4();
1847
+ const rawCaption = image3.caption !== void 0 ? safeString(image3.caption) : void 0;
1848
+ const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
1849
+ const work = this.uploadAttachment(id, image3.input, { caption, logTs: ts, contentType: image3.contentType }).then(() => void 0).catch(() => void 0);
1813
1850
  this.pendingAttachments.add(work);
1814
1851
  void work.finally(() => this.pendingAttachments.delete(work));
1815
- return result;
1852
+ return id;
1816
1853
  }
1817
1854
  /** Retorna o motivo da falha, ou undefined em sucesso. */
1818
- async uploadAttachment(id, image, meta) {
1855
+ async uploadAttachment(id, image3, meta) {
1819
1856
  let bytes;
1820
1857
  try {
1821
- bytes = await toBytes(image);
1858
+ bytes = await toBytes(image3);
1822
1859
  } catch (error) {
1823
- this.log("warning", `anexo n\xE3o p\xF4de ser lido: ${String(error)}`);
1860
+ this.log("warning", `imagem n\xE3o p\xF4de ser lida: ${String(error)}`);
1824
1861
  return "read-failed";
1825
1862
  }
1826
1863
  const contentType = sniffContentType(bytes) ?? meta.contentType;
1827
1864
  if (!contentType) {
1828
- this.log("warning", "anexo descartado: formato n\xE3o reconhecido (aceitos: PNG, JPEG, WEBP)");
1865
+ this.log("warning", "imagem descartada: formato n\xE3o reconhecido (aceitos: PNG, JPEG, WEBP)");
1829
1866
  return "unsupported-format";
1830
1867
  }
1831
1868
  if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {
1832
1869
  this.log(
1833
1870
  "warning",
1834
- `anexo descartado: ${Math.round(bytes.byteLength / 1024)} KB excede o limite de ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB`
1871
+ `imagem descartada: ${Math.round(bytes.byteLength / 1024)} KB excede o limite de ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB`
1835
1872
  );
1836
1873
  return "too-large";
1837
1874
  }
@@ -1862,11 +1899,11 @@ var RunReporter = class {
1862
1899
  return "unsupported";
1863
1900
  }
1864
1901
  if (error instanceof TasksApiError && error.status === 429) {
1865
- this.log("warning", "anexo descartado: limite de anexos por execu\xE7\xE3o atingido no servidor");
1902
+ this.log("warning", "imagem descartada: limite de anexos por execu\xE7\xE3o atingido no servidor");
1866
1903
  return "quota";
1867
1904
  }
1868
1905
  if (this.abortController.signal.aborted) return "canceled";
1869
- this.log("warning", `falha ao enviar o anexo: ${String(error)}`);
1906
+ this.log("warning", `falha ao enviar a imagem: ${String(error)}`);
1870
1907
  void this.api.confirmAttachment(this.runId, id, { status: "failed" }).catch(() => {
1871
1908
  });
1872
1909
  return "upload-failed";
@@ -2030,23 +2067,39 @@ var Step = class {
2030
2067
  /**
2031
2068
  * Desfecho da step. `message` (opcional) é o motivo legível — vai em `data.message`
2032
2069
  * e aparece na Central (performance e banner da execução) e no log de FINISH.
2070
+ * `options.image` pendura um print do estado da tela em `data.attachments`.
2033
2071
  */
2034
- setStatus(status, message) {
2072
+ setStatus(status, message, options) {
2035
2073
  this.status = status;
2036
2074
  if (message) this.setData("message", stepMessage(message));
2075
+ if (options?.image !== void 0) this.attachImage(options.image);
2037
2076
  return this;
2038
2077
  }
2039
2078
  /**
2040
2079
  * Marca a step como `error` guardando o motivo em `data.error` ({ type, message }).
2041
2080
  * Aceita qualquer valor lançado; `message` sobrescreve a mensagem do erro.
2081
+ * `options.image` pendura um print do estado da tela em `data.attachments`.
2042
2082
  */
2043
- setError(error, message) {
2083
+ setError(error, message, options) {
2044
2084
  this.status = "error";
2045
2085
  const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
2046
2086
  const raw = message ?? (error instanceof Error ? error.message || error.name : String(error));
2047
2087
  this.setData("error", { ...type ? { type } : {}, message: stepMessage(raw) });
2088
+ if (options?.image !== void 0) this.attachImage(options.image);
2048
2089
  return this;
2049
2090
  }
2091
+ /**
2092
+ * Dispara o upload pela run (sink do RunReporter) e referencia o anexo em
2093
+ * `data.attachments` (chave reservada; teto igual ao das linhas de log). Fora de uma
2094
+ * run não há onde pendurar a imagem — descarta com aviso único.
2095
+ */
2096
+ attachImage(image3) {
2097
+ const current = Array.isArray(this.data?.attachments) ? this.data.attachments : [];
2098
+ if (current.length >= MAX_ATTACHMENTS_PER_LOG_ENTRY) return;
2099
+ const id = this.transaction.attachImage(image3);
2100
+ if (!id) return;
2101
+ this.setData("attachments", [...current, id]);
2102
+ }
2050
2103
  /** Motivo registrado via setStatus(…, message) ou setError — usado no log de FINISH. */
2051
2104
  reason() {
2052
2105
  const error = this.data?.error;
@@ -2107,6 +2160,8 @@ var Transaction = class {
2107
2160
  tags = {};
2108
2161
  finished = false;
2109
2162
  lifecycleSink;
2163
+ attachmentSink;
2164
+ warnedNoAttachmentSink = false;
2110
2165
  // instalado pelo RunReporter: abrir uma step é um ponto de parada cooperativo (lança RunCanceled)
2111
2166
  stepGuard;
2112
2167
  // eventos emitidos antes do sink existir (o START da transaction sai no construtor,
@@ -2137,6 +2192,19 @@ var Transaction = class {
2137
2192
  this.lifecycleBacklog = [];
2138
2193
  for (const [level, message] of backlog) sink(level, message);
2139
2194
  }
2195
+ /** Conecta o pipeline de anexos da run — é o que dá imagem às steps. */
2196
+ setAttachmentSink(sink) {
2197
+ this.attachmentSink = sink;
2198
+ }
2199
+ /** Imagem de uma step → id do anexo pela run; sem run, descarta com aviso único. */
2200
+ attachImage(image3) {
2201
+ if (this.attachmentSink) return this.attachmentSink(image3, nowISO());
2202
+ if (!this.warnedNoAttachmentSink) {
2203
+ this.warnedNoAttachmentSink = true;
2204
+ console.warn("[hobots] imagem de step descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o");
2205
+ }
2206
+ return void 0;
2207
+ }
2140
2208
  /**
2141
2209
  * Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
2142
2210
  * `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
@@ -2199,13 +2267,12 @@ var Transaction = class {
2199
2267
  // src/tasks/runner.ts
2200
2268
  var DEFAULT_RETRY_DELAY_MS = 5e3;
2201
2269
  async function executeRun(input) {
2202
- const { run, task, api, client, logger, tracker } = input;
2203
- const options = task.options;
2204
- const tx = new Transaction(client, `run:${run.task}`, "hobots.task", run.id);
2270
+ const { run, handler, api, client, logger, tracker } = input;
2271
+ const options = handler.options;
2272
+ const tx = new Transaction(client, "run", "hobots.run", run.id);
2205
2273
  const reporter = new RunReporter(
2206
2274
  api,
2207
2275
  run.id,
2208
- run.task,
2209
2276
  logger,
2210
2277
  tx,
2211
2278
  options.logFlushIntervalMs,
@@ -2221,9 +2288,9 @@ async function executeRun(input) {
2221
2288
  tx.finish();
2222
2289
  return;
2223
2290
  }
2224
- logger.debug(`executando run ${run.id} (${run.task})`);
2291
+ logger.debug(`executando run ${run.id}`);
2225
2292
  try {
2226
- await runWithRunContext({ reporter }, () => invokeWithRetry(task, run, reporter, logger));
2293
+ await runWithRunContext({ reporter }, () => invokeWithRetry(handler, run, reporter, logger));
2227
2294
  await reporter.finalizeAttachments();
2228
2295
  await reporter.finalizeItems();
2229
2296
  warnIncompleteProgress(reporter);
@@ -2251,11 +2318,11 @@ function warnIncompleteProgress(reporter) {
2251
2318
  `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`
2252
2319
  );
2253
2320
  }
2254
- function invokeWithRetry(task, run, reporter, logger) {
2255
- const { retries, retryDelayMs } = task.options;
2321
+ function invokeWithRetry(agent, run, reporter, logger) {
2322
+ const { retries, retryDelayMs } = agent.options;
2256
2323
  const total = retries + 1;
2257
2324
  const ctx = reporter.context;
2258
- return retryWithBackoff(() => invokeWithTimeout(task, run, reporter, logger), {
2325
+ return retryWithBackoff(() => invokeWithTimeout(agent, run, reporter, logger), {
2259
2326
  retries,
2260
2327
  delayMs: (attempt) => resolveDelay(retryDelayMs, attempt, DEFAULT_RETRY_DELAY_MS),
2261
2328
  shouldRetry: () => !reporter.isDead,
@@ -2269,9 +2336,9 @@ function invokeWithRetry(task, run, reporter, logger) {
2269
2336
  }
2270
2337
  var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
2271
2338
  var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
2272
- async function invokeWithTimeout(task, run, reporter, logger) {
2273
- const { timeoutMs, cancelGraceMs } = task.options;
2274
- const handler = task.handler;
2339
+ async function invokeWithTimeout(agent, run, reporter, logger) {
2340
+ const { timeoutMs, cancelGraceMs } = agent.options;
2341
+ const handler = agent.handler;
2275
2342
  const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
2276
2343
  const races = [];
2277
2344
  if (timeoutMs && timeoutMs > 0) {
@@ -2328,7 +2395,7 @@ var TaskAgent = class {
2328
2395
  this.logger = options.logger;
2329
2396
  this.api = options.api;
2330
2397
  this.client = options.client;
2331
- this.registry = options.registry;
2398
+ this.handler = options.handler;
2332
2399
  this.tracker = options.tracker;
2333
2400
  this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
2334
2401
  this.backoff = new Backoff(this.logger, "poll", this.pollIntervalMs);
@@ -2338,7 +2405,7 @@ var TaskAgent = class {
2338
2405
  pollIntervalMs;
2339
2406
  api;
2340
2407
  client;
2341
- registry;
2408
+ handler;
2342
2409
  tracker;
2343
2410
  backoff;
2344
2411
  state = "idle";
@@ -2354,10 +2421,10 @@ var TaskAgent = class {
2354
2421
  throw new Error("[hobots] start do agente durante o stop \u2014 aguarde o stop terminar");
2355
2422
  }
2356
2423
  this.state = "started";
2357
- this.logger.debug(`polling iniciado (tasks: ${this.registry.remoteTasks().map((task) => task.slug).join(", ")})`);
2424
+ this.logger.debug(`polling iniciado (concorr\xEAncia ${this.handler.options.concurrency})`);
2358
2425
  this.schedule(0);
2359
2426
  }
2360
- /** Uma task registrada depois do boot entra no próximo poll — que é antecipado para já. */
2427
+ /** Antecipa o próximo poll para já. */
2361
2428
  wake() {
2362
2429
  if (this.state !== "started") return;
2363
2430
  if (this.timer) {
@@ -2388,21 +2455,14 @@ var TaskAgent = class {
2388
2455
  void this.tick();
2389
2456
  }, delayMs);
2390
2457
  }
2391
- /** Slots livres por task remota — a soma vira o `max` do poll e as tasks com slot viram o filtro. */
2458
+ /** Slots livres do agente — vira o `max` do poll (nunca reivindica trabalho para deixar parado). */
2392
2459
  freeSlots() {
2393
- const tasks = [];
2394
- let max = 0;
2395
- for (const task of this.registry.remoteTasks()) {
2396
- const free = task.options.concurrency - this.tracker.activeFor(task.slug);
2397
- if (free <= 0) continue;
2398
- tasks.push(task.slug);
2399
- max += free;
2400
- }
2401
- return { tasks, max: Math.min(MAX_CLAIM_PER_POLL, max) };
2460
+ const free = this.handler.options.concurrency - this.tracker.activeCount;
2461
+ return Math.min(MAX_CLAIM_PER_POLL, Math.max(0, free));
2402
2462
  }
2403
2463
  async tick() {
2404
2464
  if (this.state !== "started") return;
2405
- const { tasks, max } = this.freeSlots();
2465
+ const max = this.freeSlots();
2406
2466
  if (max === 0) {
2407
2467
  this.schedule(this.pollIntervalMs);
2408
2468
  return;
@@ -2412,11 +2472,9 @@ var TaskAgent = class {
2412
2472
  const response = await this.api.poll(
2413
2473
  {
2414
2474
  // identidade canônica do agente no painel — o mesmo instanceId do heartbeat,
2415
- // então os dois canais convergem em um agente. `instance.tasks` lista TODAS as
2416
- // tasks (register + start); o filtro `tasks` abaixo só as que este poll pode rodar.
2475
+ // então os dois canais convergem em um agente
2417
2476
  instance: this.options.instanceInfo(),
2418
- max,
2419
- tasks
2477
+ max
2420
2478
  },
2421
2479
  this.pollAbort.signal
2422
2480
  );
@@ -2440,17 +2498,7 @@ var TaskAgent = class {
2440
2498
  this.activeRuns.add(promise);
2441
2499
  }
2442
2500
  async executeClaimed(run) {
2443
- const task = this.registry.get(run.task);
2444
- if (!task || task.mode !== "remote") {
2445
- const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", run.id);
2446
- const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx);
2447
- reporter.reportFailure(toRunError(Object.assign(new Error(`tarefa n\xE3o registrada no agente: ${run.task}`), { name: "HandlerNotFound" })));
2448
- await reporter.finalize();
2449
- tx.setStatus("error");
2450
- tx.finish();
2451
- return;
2452
- }
2453
- await executeRun({ run, task, api: this.api, client: this.client, logger: this.logger, tracker: this.tracker });
2501
+ await executeRun({ run, handler: this.handler, api: this.api, client: this.client, logger: this.logger, tracker: this.tracker });
2454
2502
  }
2455
2503
  };
2456
2504
 
@@ -2458,11 +2506,11 @@ var TaskAgent = class {
2458
2506
  var KEY_RE = /^[a-zA-Z0-9_.-]{1,100}$/;
2459
2507
  var MAX_COLUMNS = WIRE_LIMITS.MAX_ITEM_COLUMNS;
2460
2508
  var MAX_LABEL_CHARS = WIRE_LIMITS.MAX_ITEM_COLUMN_LABEL_CHARS;
2461
- function normalizeItemColumns(slug, input, logger) {
2509
+ function normalizeItemColumns(input, logger) {
2462
2510
  if (input === void 0 || input === null) return void 0;
2463
2511
  const columns = input.columns;
2464
2512
  if (typeof input !== "object" || !Array.isArray(columns)) {
2465
- logger.warn(`task "${slug}": items.columns precisa ser uma lista \u2014 declara\xE7\xE3o de colunas ignorada`);
2513
+ logger.warn(`items.columns precisa ser uma lista \u2014 declara\xE7\xE3o de colunas ignorada`);
2466
2514
  return void 0;
2467
2515
  }
2468
2516
  const result = [];
@@ -2472,15 +2520,15 @@ function normalizeItemColumns(slug, input, logger) {
2472
2520
  const rawLabel = typeof entry === "object" && entry !== null ? entry.label : void 0;
2473
2521
  const key = typeof rawKey === "string" ? rawKey.trim() : "";
2474
2522
  if (!KEY_RE.test(key)) {
2475
- logger.warn(`task "${slug}": coluna de item inv\xE1lida ignorada (${JSON.stringify(rawKey)}) \u2014 use letras, n\xFAmeros, ponto, h\xEDfen e underline`);
2523
+ logger.warn(`coluna de item inv\xE1lida ignorada (${JSON.stringify(rawKey)}) \u2014 use letras, n\xFAmeros, ponto, h\xEDfen e underline`);
2476
2524
  continue;
2477
2525
  }
2478
2526
  if (seen.has(key)) {
2479
- logger.warn(`task "${slug}": coluna de item "${key}" repetida ignorada`);
2527
+ logger.warn(`coluna de item "${key}" repetida ignorada`);
2480
2528
  continue;
2481
2529
  }
2482
2530
  if (result.length >= MAX_COLUMNS) {
2483
- logger.warn(`task "${slug}": no m\xE1ximo ${MAX_COLUMNS} colunas de item \u2014 "${key}" em diante ignoradas`);
2531
+ logger.warn(`no m\xE1ximo ${MAX_COLUMNS} colunas de item \u2014 "${key}" em diante ignoradas`);
2484
2532
  break;
2485
2533
  }
2486
2534
  seen.add(key);
@@ -2492,15 +2540,15 @@ function normalizeItemColumns(slug, input, logger) {
2492
2540
 
2493
2541
  // src/tasks/local.ts
2494
2542
  var LocalRunner = class {
2495
- constructor(task, deps) {
2496
- this.task = task;
2543
+ constructor(handler, deps) {
2544
+ this.handler = handler;
2497
2545
  this.deps = deps;
2498
- this.backoff = new Backoff(deps.logger, `cria\xE7\xE3o da run "${task.slug}"`);
2546
+ this.backoff = new Backoff(deps.logger, "cria\xE7\xE3o da run");
2499
2547
  this.done = new Promise((resolve2) => {
2500
2548
  this.resolveDone = resolve2;
2501
2549
  });
2502
2550
  }
2503
- task;
2551
+ handler;
2504
2552
  deps;
2505
2553
  abort = new AbortController();
2506
2554
  backoff;
@@ -2511,17 +2559,17 @@ var LocalRunner = class {
2511
2559
  try {
2512
2560
  const run = await this.createRun();
2513
2561
  if (!run) return;
2514
- this.deps.logger.debug(`run ${run.id} aberta para "${this.task.slug}" (modo start)`);
2562
+ this.deps.logger.debug(`run ${run.id} aberta (modo start)`);
2515
2563
  await executeRun({
2516
2564
  run,
2517
- task: this.task,
2565
+ handler: this.handler,
2518
2566
  api: this.deps.api,
2519
2567
  client: this.deps.client,
2520
2568
  logger: this.deps.logger,
2521
2569
  tracker: this.deps.tracker
2522
2570
  });
2523
2571
  } catch (error) {
2524
- this.deps.logger.error(`erro inesperado na task "${this.task.slug}":`, error);
2572
+ this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
2525
2573
  } finally {
2526
2574
  this.resolveDone();
2527
2575
  }
@@ -2534,7 +2582,7 @@ var LocalRunner = class {
2534
2582
  for (; ; ) {
2535
2583
  if (this.abort.signal.aborted) return void 0;
2536
2584
  try {
2537
- return await this.deps.api.createRun({ task: this.task.slug, instance: this.deps.instanceInfo() }, this.abort.signal);
2585
+ return await this.deps.api.createRun({ instance: this.deps.instanceInfo() }, this.abort.signal);
2538
2586
  } catch (error) {
2539
2587
  if (this.abort.signal.aborted) return void 0;
2540
2588
  await abortableSleep(this.backoff.next(error), this.abort.signal);
@@ -2544,45 +2592,6 @@ var LocalRunner = class {
2544
2592
  };
2545
2593
 
2546
2594
  // src/tasks/registry.ts
2547
- var TaskRegistry = class {
2548
- tasks = /* @__PURE__ */ new Map();
2549
- add(task) {
2550
- if (this.tasks.has(task.slug)) {
2551
- throw new Error(`[hobots] task "${task.slug}" j\xE1 registrada \u2014 cada slug s\xF3 pode ter um handler (register ou start)`);
2552
- }
2553
- this.tasks.set(task.slug, task);
2554
- }
2555
- get(slug) {
2556
- return this.tasks.get(slug);
2557
- }
2558
- get size() {
2559
- return this.tasks.size;
2560
- }
2561
- /** Tasks que chegam pelo poll (register). */
2562
- remoteTasks() {
2563
- return [...this.tasks.values()].filter((task) => task.mode === "remote");
2564
- }
2565
- /** Tasks abertas pelo próprio bot (start). */
2566
- localTasks() {
2567
- return [...this.tasks.values()].filter((task) => task.mode === "local");
2568
- }
2569
- /** Todos os slugs, para a presença da instância (`instance.tasks`). */
2570
- allSlugs() {
2571
- return [...this.tasks.keys()];
2572
- }
2573
- /** Colunas de itens declaradas no código, por slug (`instance.task_items`); undefined se nenhuma task declarou. */
2574
- itemDeclarations() {
2575
- let result;
2576
- for (const task of this.tasks.values()) {
2577
- if (!task.options.itemColumns) continue;
2578
- (result ??= {})[task.slug] = { attributes: task.options.itemColumns };
2579
- }
2580
- return result;
2581
- }
2582
- clear() {
2583
- this.tasks.clear();
2584
- }
2585
- };
2586
2595
  var RunTracker = class {
2587
2596
  constructor(api) {
2588
2597
  this.api = api;
@@ -2603,12 +2612,6 @@ var RunTracker = class {
2603
2612
  get activeCount() {
2604
2613
  return this.reporters.size;
2605
2614
  }
2606
- /** Runs ativas de uma task (concorrência por task no poll). */
2607
- activeFor(slug) {
2608
- let count = 0;
2609
- for (const reporter of this.reporters) if (reporter.context.task === slug) count++;
2610
- return count;
2611
- }
2612
2615
  /** A única run ativa, quando há exatamente uma — o que `hobots.*` usa fora de um contexto async. */
2613
2616
  singleActive() {
2614
2617
  if (this.reporters.size !== 1) return void 0;
@@ -2697,40 +2700,13 @@ var DEFAULT_RETRIES2 = 1;
2697
2700
  var DEFAULT_RETRY_DELAY_MS2 = 5e3;
2698
2701
  var DEFAULT_CANCEL_GRACE_MS = 1e4;
2699
2702
  var MAX_CONCURRENCY = 5;
2700
- var MAX_SLUG_CHARS = 255;
2701
- var PROCESS_LEVEL_KEYS = [
2702
- "clientSecret",
2703
- "instanceId",
2704
- "environment",
2705
- "release",
2706
- "tags",
2707
- "debug",
2708
- "machineMetrics",
2709
- "captureUnhandled",
2710
- "installExitHandlers"
2711
- ];
2712
2703
  var runtime;
2713
2704
  var warnedNoRuntime = false;
2714
2705
  function hostname() {
2715
2706
  if (!isNode()) return void 0;
2716
2707
  return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
2717
2708
  }
2718
- function sameValue(a, b) {
2719
- if (a === b) return true;
2720
- if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
2721
- return false;
2722
- }
2723
- function ensureRuntime(options) {
2724
- if (runtime) {
2725
- for (const key of PROCESS_LEVEL_KEYS) {
2726
- if (options[key] !== void 0 && !sameValue(options[key], runtime.options[key])) {
2727
- runtime.logger.warn(
2728
- `op\xE7\xE3o "${key}" divergente ignorada \u2014 o primeiro register()/start() define as op\xE7\xF5es do processo`
2729
- );
2730
- }
2731
- }
2732
- return runtime;
2733
- }
2709
+ function createRuntime(options, handler) {
2734
2710
  const config = resolveConfig({ clientSecret: options.clientSecret });
2735
2711
  const logger = makeLogger(options.debug ?? false);
2736
2712
  const environment = resolveEnvironmentOption(options.environment);
@@ -2738,19 +2714,19 @@ function ensureRuntime(options) {
2738
2714
  { environment, release: options.release, tags: options.tags, debug: options.debug },
2739
2715
  config
2740
2716
  );
2741
- const api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger });
2742
- runtime = {
2717
+ const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
2718
+ const rt = {
2743
2719
  options: { ...options, clientSecret: void 0 },
2744
2720
  config,
2745
2721
  client,
2746
2722
  logger,
2747
2723
  api,
2748
- registry: new TaskRegistry(),
2724
+ handler,
2749
2725
  tracker: new RunTracker(api),
2750
2726
  sampler: void 0,
2751
2727
  heartbeat: void 0,
2752
2728
  agent: void 0,
2753
- locals: /* @__PURE__ */ new Map(),
2729
+ local: void 0,
2754
2730
  instanceId: resolveInstanceId(options.instanceId),
2755
2731
  environment,
2756
2732
  pollIntervalMs: void 0,
@@ -2760,24 +2736,19 @@ function ensureRuntime(options) {
2760
2736
  uninstallGlobalHandlers: options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0,
2761
2737
  uninstallShutdown: void 0
2762
2738
  };
2763
- runtime.options.clientSecret = options.clientSecret;
2764
2739
  logger.debug("inicializado", { endpoint: config.ingestEndpoint });
2765
- return runtime;
2740
+ return rt;
2766
2741
  }
2767
- function validateTask(slug, handler, fn) {
2768
- if (typeof slug !== "string" || slug.trim().length === 0 || slug.length > MAX_SLUG_CHARS) {
2769
- throw new Error(`[hobots] ${fn}(): o slug da task precisa ser uma string de 1 a ${MAX_SLUG_CHARS} caracteres`);
2770
- }
2742
+ function validateHandler(handler, fn) {
2771
2743
  if (typeof handler !== "function") {
2772
- throw new Error(`[hobots] ${fn}('${slug}'): o handler precisa ser uma fun\xE7\xE3o`);
2744
+ throw new Error(`[hobots] ${fn}(): o handler precisa ser uma fun\xE7\xE3o \u2014 ${fn}(async (params) => { ... })`);
2773
2745
  }
2774
- return slug.trim();
2775
2746
  }
2776
2747
  function clampConcurrency(value) {
2777
2748
  if (value === void 0 || !Number.isFinite(value)) return 1;
2778
2749
  return Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(value)));
2779
2750
  }
2780
- function resolveTaskOptions(slug, options, mode, logger) {
2751
+ function resolveRunOptions(options, mode, logger) {
2781
2752
  const retries = options.retries === void 0 || !Number.isFinite(options.retries) ? DEFAULT_RETRIES2 : Math.max(0, Math.floor(options.retries));
2782
2753
  return {
2783
2754
  retries,
@@ -2787,7 +2758,7 @@ function resolveTaskOptions(slug, options, mode, logger) {
2787
2758
  logFlushIntervalMs: options.logFlushIntervalMs,
2788
2759
  cancelCheckIntervalMs: options.cancelCheckIntervalMs,
2789
2760
  cancelGraceMs: options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS,
2790
- itemColumns: normalizeItemColumns(slug, options.items, logger)
2761
+ itemColumns: normalizeItemColumns(options.items, logger)
2791
2762
  };
2792
2763
  }
2793
2764
  function instanceInfo(rt) {
@@ -2797,10 +2768,10 @@ function instanceInfo(rt) {
2797
2768
  host: hostname(),
2798
2769
  // ambiente da instância: junto do heartbeat, é como o servidor sabe onde o robô roda
2799
2770
  environment: rt.environment,
2800
- // TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
2801
- tasks: rt.registry.allSlugs(),
2771
+ // como esta instância trabalha — o painel mostra "sob demanda" / "sempre ligado"
2772
+ mode: rt.handler.mode === "remote" ? "on_demand" : "always_on",
2802
2773
  // colunas da aba "itens" declaradas no código — a plataforma usa enquanto o usuário não personalizar
2803
- task_items: rt.registry.itemDeclarations(),
2774
+ items: rt.handler.options.itemColumns ? { attributes: rt.handler.options.itemColumns } : void 0,
2804
2775
  // o servidor calcula a tolerância de offline (3×) a partir disto em agentes que fazem
2805
2776
  // poll; clampado à faixa aceita pelo schema (1s..15min)
2806
2777
  poll_interval_ms: rt.agent ? Math.min(Math.max(rt.agent.pollIntervalMs, 1e3), 9e5) : void 0,
@@ -2813,22 +2784,22 @@ function ensureAgent(rt) {
2813
2784
  api: rt.api,
2814
2785
  client: rt.client,
2815
2786
  logger: rt.logger,
2816
- registry: rt.registry,
2787
+ handler: rt.handler,
2817
2788
  tracker: rt.tracker,
2818
2789
  instanceInfo: () => instanceInfo(rt),
2819
2790
  pollIntervalMs: rt.pollIntervalMs
2820
2791
  });
2821
2792
  return rt.agent;
2822
2793
  }
2823
- function launchLocal(rt, task) {
2824
- const runner = new LocalRunner(task, {
2794
+ function launchLocal(rt) {
2795
+ const runner = new LocalRunner(rt.handler, {
2825
2796
  api: rt.api,
2826
2797
  client: rt.client,
2827
2798
  logger: rt.logger,
2828
2799
  tracker: rt.tracker,
2829
2800
  instanceInfo: () => instanceInfo(rt)
2830
2801
  });
2831
- rt.locals.set(task.slug, runner);
2802
+ rt.local = runner;
2832
2803
  void runner.run();
2833
2804
  }
2834
2805
  async function boot(rt) {
@@ -2850,8 +2821,8 @@ async function boot(rt) {
2850
2821
  if (rt.options.installExitHandlers !== false) {
2851
2822
  rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
2852
2823
  }
2853
- if (rt.registry.remoteTasks().length > 0) ensureAgent(rt).start();
2854
- for (const task of rt.registry.localTasks()) launchLocal(rt, task);
2824
+ if (rt.handler.mode === "remote") ensureAgent(rt).start();
2825
+ else launchLocal(rt);
2855
2826
  }
2856
2827
  function scheduleBoot(rt) {
2857
2828
  if (rt.bootScheduled) return;
@@ -2860,34 +2831,23 @@ function scheduleBoot(rt) {
2860
2831
  rt.bootPromise = boot(rt).catch((error) => rt.logger.error("falha ao iniciar o SDK:", error));
2861
2832
  });
2862
2833
  }
2863
- function addTask(fn, mode, task, handler, options) {
2864
- const slug = validateTask(task, handler, fn);
2865
- const rt = ensureRuntime(options);
2866
- if (mode === "remote" && options.pollIntervalMs !== void 0) {
2867
- if (rt.pollIntervalMs === void 0 && !rt.agent) rt.pollIntervalMs = options.pollIntervalMs;
2868
- else if (rt.pollIntervalMs !== options.pollIntervalMs) {
2869
- rt.logger.warn('op\xE7\xE3o "pollIntervalMs" divergente ignorada \u2014 o primeiro register() define o intervalo do poll');
2870
- }
2871
- }
2872
- const registered = { slug, mode, handler, options: resolveTaskOptions(slug, options, mode, rt.logger) };
2873
- rt.registry.add(registered);
2874
- if (!rt.booted) {
2875
- scheduleBoot(rt);
2876
- return;
2877
- }
2878
- if (mode === "remote") {
2879
- const agent = ensureAgent(rt);
2880
- agent.start();
2881
- agent.wake();
2882
- } else {
2883
- launchLocal(rt, registered);
2834
+ function startAgent(fn, mode, handler, options) {
2835
+ validateHandler(handler, fn);
2836
+ if (runtime) {
2837
+ 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)`);
2884
2838
  }
2839
+ const logger = makeLogger(options.debug ?? false);
2840
+ const agentHandler = { mode, handler, options: resolveRunOptions(options, mode, logger) };
2841
+ const rt = createRuntime(options, agentHandler);
2842
+ if (mode === "remote" && options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
2843
+ runtime = rt;
2844
+ scheduleBoot(rt);
2885
2845
  }
2886
- function register(task, handler, options = {}) {
2887
- addTask("register", "remote", task, handler, options);
2846
+ function register(handler, options = {}) {
2847
+ startAgent("register", "remote", handler, options);
2888
2848
  }
2889
- function start(task, handler, options = {}) {
2890
- addTask("start", "local", task, handler, options);
2849
+ function start(handler, options = {}) {
2850
+ startAgent("start", "local", handler, options);
2891
2851
  }
2892
2852
  function currentReporter() {
2893
2853
  return currentRunReporter() ?? runtime?.tracker.singleActive();
@@ -2901,23 +2861,18 @@ function toRunLogLevel(type) {
2901
2861
  function forwardAttachment(rt, attachment, message, type) {
2902
2862
  const reporter = currentReporter();
2903
2863
  if (!reporter) {
2904
- rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task");
2864
+ rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o");
2905
2865
  return;
2906
2866
  }
2907
- const withMeta = typeof attachment === "object" && "image" in attachment ? attachment : { image: attachment };
2908
- void reporter.attach(withMeta.image, {
2909
- message,
2910
- level: toRunLogLevel(type),
2911
- caption: withMeta.caption,
2912
- contentType: withMeta.contentType
2913
- });
2867
+ const withMeta = typeof attachment === "object" && !isImageAttachment(attachment) && "image" in attachment ? image(attachment.image, { caption: attachment.caption, contentType: attachment.contentType }) : attachment;
2868
+ reporter.log(toRunLogLevel(type), message, withMeta);
2914
2869
  }
2915
2870
  function captureException(error, hint) {
2916
2871
  const rt = runtime;
2917
2872
  if (!rt) {
2918
2873
  if (!warnedNoRuntime) {
2919
2874
  warnedNoRuntime = true;
2920
- console.warn("[hobots] captureException antes de registrar uma task (register/start) \u2014 evento descartado");
2875
+ console.warn("[hobots] captureException antes de iniciar o agente (register/start) \u2014 evento descartado");
2921
2876
  }
2922
2877
  return void 0;
2923
2878
  }
@@ -2933,7 +2888,7 @@ async function close(timeoutMs = 3e4) {
2933
2888
  runtime = void 0;
2934
2889
  await rt.bootPromise;
2935
2890
  if (rt.agent) await rt.agent.stop(0);
2936
- for (const local of rt.locals.values()) local.stop();
2891
+ rt.local?.stop();
2937
2892
  const idle = await rt.tracker.waitIdle(timeoutMs);
2938
2893
  if (!idle) {
2939
2894
  rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
@@ -2951,18 +2906,21 @@ var warned = /* @__PURE__ */ new Set();
2951
2906
  function warnOutsideRun(member) {
2952
2907
  if (warned.has(member)) return;
2953
2908
  warned.add(member);
2954
- 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)`;
2909
+ const message = `hobots.${member} usado fora de uma execu\xE7\xE3o \u2014 ignorado (inicie o agente com register()/start() e use dentro do handler)`;
2955
2910
  const logger = activeLogger();
2956
2911
  if (logger) logger.warn(message);
2957
2912
  else console.warn(`[hobots] ${message}`);
2958
2913
  }
2959
- var FALLBACK_TASK = "hobots";
2914
+ var fallbackLogAt = (level) => (message, ...rest) => {
2915
+ const { text, image: dropped } = splitLogArgs(message, rest);
2916
+ printRunLog(level, dropped ? `${text} [imagem descartada]` : text);
2917
+ };
2960
2918
  var fallbackLog = {
2961
- debug: (message, ...rest) => printRunLog(FALLBACK_TASK, "debug", joinLogArgs(message, rest)),
2962
- info: (message, ...rest) => printRunLog(FALLBACK_TASK, "info", joinLogArgs(message, rest)),
2963
- warn: (message, ...rest) => printRunLog(FALLBACK_TASK, "warning", joinLogArgs(message, rest)),
2964
- error: (message, ...rest) => printRunLog(FALLBACK_TASK, "error", joinLogArgs(message, rest)),
2965
- success: (message, ...rest) => printRunLog(FALLBACK_TASK, "success", joinLogArgs(message, rest))
2919
+ debug: fallbackLogAt("debug"),
2920
+ info: fallbackLogAt("info"),
2921
+ warn: fallbackLogAt("warning"),
2922
+ error: fallbackLogAt("error"),
2923
+ success: fallbackLogAt("success")
2966
2924
  };
2967
2925
  var noop = () => void 0;
2968
2926
  var fallbackEachSink = {
@@ -2972,7 +2930,7 @@ var fallbackEachSink = {
2972
2930
  advance: noop,
2973
2931
  checkCanceled: noop,
2974
2932
  isAborted: () => false,
2975
- logError: (message) => printRunLog(FALLBACK_TASK, "error", message)
2933
+ logError: (message) => printRunLog("error", message)
2976
2934
  };
2977
2935
  var fallbackItems = {
2978
2936
  add: noop,
@@ -2999,7 +2957,6 @@ function method(value) {
2999
2957
  var hobots = Object.defineProperties({}, {
3000
2958
  // ── contexto da run atual (getters) ─────────────────────────────────────────
3001
2959
  runId: getter("runId", () => ""),
3002
- task: getter("task", () => ""),
3003
2960
  params: getter("params", () => ({})),
3004
2961
  log: getter("log", () => fallbackLog),
3005
2962
  items: getter("items", () => fallbackItems),
@@ -3008,24 +2965,22 @@ var hobots = Object.defineProperties({}, {
3008
2965
  signal: getter("signal", () => neverAborts),
3009
2966
  throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
3010
2967
  sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
3011
- attach: method(
3012
- (image, options) => resolve("attach")?.attach(image, options) ?? Promise.resolve({ id: "", ok: false, reason: "canceled" })
3013
- ),
3014
2968
  retry: method((fn, options) => {
3015
2969
  const reporter = currentReporter();
3016
2970
  return reporter ? reporter.context.retry(fn, options) : retryWithBackoff(fn, options);
3017
2971
  }),
3018
2972
  // ── processo ────────────────────────────────────────────────────────────────
3019
- register: method((task, handler, options) => {
3020
- register(task, handler, options);
2973
+ register: method((handler, options) => {
2974
+ register(handler, options);
3021
2975
  installGlobalHobots();
3022
2976
  }),
3023
- start: method((task, handler, options) => {
3024
- start(task, handler, options);
2977
+ start: method((handler, options) => {
2978
+ start(handler, options);
3025
2979
  installGlobalHobots();
3026
2980
  }),
3027
2981
  close: method((timeoutMs) => close(timeoutMs)),
3028
- captureException: method((error, hint) => captureException(error, hint))
2982
+ captureException: method((error, hint) => captureException(error, hint)),
2983
+ image: method(image)
3029
2984
  });
3030
2985
  function installGlobalHobots() {
3031
2986
  if (globalThis.hobots === hobots) return;
@@ -3036,6 +2991,7 @@ var start2 = hobots.start;
3036
2991
  var close2 = hobots.close;
3037
2992
  var captureException2 = hobots.captureException;
3038
2993
  var retry = hobots.retry;
2994
+ var image2 = hobots.image;
3039
2995
 
3040
2996
  // src/index.ts
3041
2997
  var src_default = hobots;
@@ -3044,6 +3000,7 @@ export {
3044
3000
  close2 as close,
3045
3001
  src_default as default,
3046
3002
  hobots,
3003
+ image2 as image,
3047
3004
  register2 as register,
3048
3005
  retry,
3049
3006
  start2 as start