hobots 4.2.0 → 4.5.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
@@ -42,62 +42,9 @@ __export(src_exports, {
42
42
  });
43
43
  module.exports = __toCommonJS(src_exports);
44
44
 
45
- // src/stacktrace.ts
46
- var MAX_FRAMES = 50;
47
- var MAX_FILENAME_CHARS = 1024;
48
- var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
49
- var LOCATION_RE = /^(.*?):(\d+):(\d+)$/;
50
- function isInApp(filename) {
51
- if (!filename) return false;
52
- if (filename.startsWith("node:")) return false;
53
- if (filename.startsWith("internal/")) return false;
54
- if (filename.includes("node_modules")) return false;
55
- if (filename === "<anonymous>") return false;
56
- return true;
57
- }
58
- function parseStacktrace(stack) {
59
- if (!stack) return [];
60
- const frames = [];
61
- for (const line of stack.split("\n")) {
62
- if (frames.length >= MAX_FRAMES) break;
63
- const match = FRAME_RE.exec(line);
64
- if (!match || !line.trimStart().startsWith("at ")) continue;
65
- const [, fn, location] = match;
66
- const loc = LOCATION_RE.exec(location ?? "");
67
- const filename = loc ? loc[1] : location;
68
- frames.push({
69
- function: fn || "<anonymous>",
70
- filename: filename ? filename.slice(0, MAX_FILENAME_CHARS) : void 0,
71
- lineno: loc?.[2] ? Number(loc[2]) : void 0,
72
- colno: loc?.[3] ? Number(loc[3]) : void 0,
73
- in_app: isInApp(filename)
74
- });
75
- }
76
- return frames;
77
- }
78
-
79
- // src/scrub.ts
80
- var SECRET_PATTERNS = [
81
- // client secret da Hobots
82
- [/hb_[a-f0-9]{16,}/gi, "[REDACTED]"],
83
- // tokens bearer em headers/mensagens
84
- [/\b(authorization:?\s*)?bearer\s+[a-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]"],
85
- // chaves no formato sk-… (OpenAI e afins)
86
- [/\bsk-[a-z0-9_-]{16,}\b/gi, "[REDACTED]"],
87
- // credenciais embutidas em URL (https://user:pass@host)
88
- [/\/\/[^/\s:@]+:[^/\s@]+@/g, "//[REDACTED]@"]
89
- ];
90
- function scrubSecrets(text) {
91
- let out = text;
92
- for (const [pattern, replacement] of SECRET_PATTERNS) {
93
- out = out.replace(pattern, replacement);
94
- }
95
- return out;
96
- }
97
-
98
45
  // src/utils.ts
99
46
  var SDK_NAME = "hobots";
100
- var SDK_VERSION = true ? "4.2.0" : "0.0.0-dev";
47
+ var SDK_VERSION = true ? "4.5.0" : "0.0.0-dev";
101
48
  function isNode() {
102
49
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
103
50
  }
@@ -245,6 +192,104 @@ function colorize(text, color) {
245
192
  return `${ANSI[color]}${text}${ANSI.reset}`;
246
193
  }
247
194
 
195
+ // src/env.ts
196
+ var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
197
+ var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
198
+ var RUN_ID_ENV = "HOBOTS_RUN_ID";
199
+ var dotenvLoaded = false;
200
+ function loadDotenv() {
201
+ if (dotenvLoaded) return;
202
+ dotenvLoaded = true;
203
+ if (!isNode() || typeof process.loadEnvFile !== "function") return;
204
+ try {
205
+ process.loadEnvFile(".env");
206
+ } catch (err) {
207
+ const code = err?.code;
208
+ if (code === "ENOENT") return;
209
+ console.warn(`[hobots] n\xE3o foi poss\xEDvel ler o .env da pasta de trabalho (${typeof code === "string" ? code : "erro desconhecido"}) \u2014 seguindo s\xF3 com as vari\xE1veis do ambiente`);
210
+ }
211
+ }
212
+ function resolveInstanceId(option) {
213
+ const fromOption = option?.trim();
214
+ if (fromOption) return fromOption;
215
+ if (typeof process === "undefined" || !process.env) return void 0;
216
+ return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
217
+ }
218
+ function resolveRunId(option) {
219
+ const fromOption = option?.trim();
220
+ if (fromOption) return fromOption;
221
+ if (typeof process === "undefined" || !process.env) return void 0;
222
+ return process.env[RUN_ID_ENV]?.trim() || void 0;
223
+ }
224
+ function resolveEnvironmentOption(option) {
225
+ const fromOption = option?.trim();
226
+ if (fromOption) return fromOption;
227
+ if (typeof process === "undefined" || !process.env) return void 0;
228
+ return process.env[ENVIRONMENT_ENV]?.trim() || void 0;
229
+ }
230
+ async function fallbackInstanceId() {
231
+ if (!isNode()) return "browser";
232
+ try {
233
+ const os = await import("os");
234
+ return os.hostname() || "unknown-host";
235
+ } catch {
236
+ return "unknown-host";
237
+ }
238
+ }
239
+
240
+ // src/stacktrace.ts
241
+ var MAX_FRAMES = 50;
242
+ var MAX_FILENAME_CHARS = 1024;
243
+ var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
244
+ var LOCATION_RE = /^(.*?):(\d+):(\d+)$/;
245
+ function isInApp(filename) {
246
+ if (!filename) return false;
247
+ if (filename.startsWith("node:")) return false;
248
+ if (filename.startsWith("internal/")) return false;
249
+ if (filename.includes("node_modules")) return false;
250
+ if (filename === "<anonymous>") return false;
251
+ return true;
252
+ }
253
+ function parseStacktrace(stack) {
254
+ if (!stack) return [];
255
+ const frames = [];
256
+ for (const line of stack.split("\n")) {
257
+ if (frames.length >= MAX_FRAMES) break;
258
+ const match = FRAME_RE.exec(line);
259
+ if (!match || !line.trimStart().startsWith("at ")) continue;
260
+ const [, fn, location] = match;
261
+ const loc = LOCATION_RE.exec(location ?? "");
262
+ const filename = loc ? loc[1] : location;
263
+ frames.push({
264
+ function: fn || "<anonymous>",
265
+ filename: filename ? filename.slice(0, MAX_FILENAME_CHARS) : void 0,
266
+ lineno: loc?.[2] ? Number(loc[2]) : void 0,
267
+ colno: loc?.[3] ? Number(loc[3]) : void 0,
268
+ in_app: isInApp(filename)
269
+ });
270
+ }
271
+ return frames;
272
+ }
273
+
274
+ // src/scrub.ts
275
+ var SECRET_PATTERNS = [
276
+ // client secret da Hobots
277
+ [/hb_[a-f0-9]{16,}/gi, "[REDACTED]"],
278
+ // tokens bearer em headers/mensagens
279
+ [/\b(authorization:?\s*)?bearer\s+[a-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]"],
280
+ // chaves no formato sk-… (OpenAI e afins)
281
+ [/\bsk-[a-z0-9_-]{16,}\b/gi, "[REDACTED]"],
282
+ // credenciais embutidas em URL (https://user:pass@host)
283
+ [/\/\/[^/\s:@]+:[^/\s@]+@/g, "//[REDACTED]@"]
284
+ ];
285
+ function scrubSecrets(text) {
286
+ let out = text;
287
+ for (const [pattern, replacement] of SECRET_PATTERNS) {
288
+ out = out.replace(pattern, replacement);
289
+ }
290
+ return out;
291
+ }
292
+
248
293
  // src/eventbuilder.ts
249
294
  var MAX_EXCEPTION_VALUE_CHARS = 8192;
250
295
  function eventFromException(error, type = "error") {
@@ -422,6 +467,10 @@ function clipFingerprint(fingerprint) {
422
467
  if (fingerprint === void 0) return void 0;
423
468
  return fingerprint.slice(0, MAX_FINGERPRINT_ITEMS).map((item) => truncate(String(item), MAX_FINGERPRINT_ITEM_CHARS));
424
469
  }
470
+ var captured = /* @__PURE__ */ new WeakSet();
471
+ function wasCaptured(error) {
472
+ return typeof error === "object" && error !== null && captured.has(error);
473
+ }
425
474
  function runtimeContexts() {
426
475
  if (isNode()) {
427
476
  return {
@@ -473,6 +522,7 @@ var Client = class {
473
522
  return `Client { baseUrl: '${this.#config.baseUrl}' }`;
474
523
  }
475
524
  captureException(error, hint) {
525
+ if (typeof error === "object" && error !== null) captured.add(error);
476
526
  return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
477
527
  }
478
528
  captureEvent(event, hint) {
@@ -580,12 +630,12 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
580
630
  const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
581
631
  if (!key) {
582
632
  throw new Error(
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`
633
+ `${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 na Central: abra o agente \u2192 Configura\xE7\xF5es \u2192 agente \u2192 aba credenciais`
584
634
  );
585
635
  }
586
636
  if (!SECRET_PATTERN.test(key)) {
587
637
  throw new Error(
588
- `${prefix} ${origin} com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
638
+ `${prefix} ${origin} com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret na Central: abra o agente \u2192 Configura\xE7\xF5es \u2192 agente \u2192 aba credenciais`
589
639
  );
590
640
  }
591
641
  const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
@@ -597,31 +647,6 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
597
647
  };
598
648
  }
599
649
 
600
- // src/env.ts
601
- var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
602
- var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
603
- function resolveInstanceId(option) {
604
- const fromOption = option?.trim();
605
- if (fromOption) return fromOption;
606
- if (typeof process === "undefined" || !process.env) return void 0;
607
- return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
608
- }
609
- function resolveEnvironmentOption(option) {
610
- const fromOption = option?.trim();
611
- if (fromOption) return fromOption;
612
- if (typeof process === "undefined" || !process.env) return void 0;
613
- return process.env[ENVIRONMENT_ENV]?.trim() || void 0;
614
- }
615
- async function fallbackInstanceId() {
616
- if (!isNode()) return "browser";
617
- try {
618
- const os = await import("os");
619
- return os.hostname() || "unknown-host";
620
- } catch {
621
- return "unknown-host";
622
- }
623
- }
624
-
625
650
  // src/heartbeat.ts
626
651
  var DEFAULT_INTERVAL_MS = WIRE_LIMITS.HEARTBEAT_DEFAULT_INTERVAL_MS;
627
652
  var MIN_REPORTED_INTERVAL_MS = WIRE_LIMITS.HEARTBEAT_MIN_INTERVAL_MS;
@@ -983,6 +1008,42 @@ var TasksApi = class {
983
1008
  created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
984
1009
  };
985
1010
  }
1011
+ /**
1012
+ * Anexa o processo a uma run já reivindicada para esta instância (env HOBOTS_RUN_ID):
1013
+ * POST /v1/runs/:id/start. Um request só — quem decide o retry é o AttachedRunner.
1014
+ * 404 (não existe) e 409 (cancelada, já iniciada ou de outra instância) viram
1015
+ * TasksApiError com o status.
1016
+ */
1017
+ async startRun(runId, request, signal) {
1018
+ const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
1019
+ let response;
1020
+ try {
1021
+ response = await fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/start`, {
1022
+ redirect: "error",
1023
+ method: "POST",
1024
+ headers: this.headers(),
1025
+ body: JSON.stringify(request),
1026
+ signal: timeoutSignal
1027
+ });
1028
+ } finally {
1029
+ cleanup();
1030
+ }
1031
+ if (!response.ok) {
1032
+ throw new TasksApiError(`in\xEDcio da run falhou (HTTP ${response.status})`, response.status);
1033
+ }
1034
+ const body = await response.json().catch(() => null);
1035
+ if (typeof body?.id !== "string") {
1036
+ throw new TasksApiError("resposta inv\xE1lida do in\xEDcio da run (id ausente)", 0);
1037
+ }
1038
+ const form = body.form;
1039
+ return {
1040
+ id: body.id,
1041
+ params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
1042
+ form: typeof form?.id === "string" && typeof form.name === "string" ? { id: form.id, name: form.name } : null,
1043
+ source: typeof body.source === "string" ? body.source : "hobots",
1044
+ created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
1045
+ };
1046
+ }
986
1047
  /** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
987
1048
  parsePollResponse(body, max) {
988
1049
  const runs = body?.runs;
@@ -1240,7 +1301,7 @@ var Backoff = class {
1240
1301
  this.authFailures++;
1241
1302
  if (this.authFailures === 1 || this.authFailures % 10 === 0) {
1242
1303
  this.logger.error(
1243
- error.status === 402 ? "agente fora do limite do plano (402) \u2014 fa\xE7a upgrade em Plano e cobran\xE7a para reativ\xE1-lo" : "chave rejeitada (401) \u2014 verifique o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes"
1304
+ error.status === 402 ? "agente fora do limite do plano (402) \u2014 fa\xE7a upgrade em Plano e cobran\xE7a para reativ\xE1-lo" : "chave rejeitada (401) \u2014 verifique o Client Secret na Central: abra o agente \u2192 Configura\xE7\xF5es \u2192 agente \u2192 aba credenciais"
1244
1305
  );
1245
1306
  }
1246
1307
  return Math.max(this.floorMs, AUTH_FAILURE_INTERVAL_MS);
@@ -1266,6 +1327,9 @@ var TERMINAL_ERROR_NAMES = /* @__PURE__ */ new Set(["RunCanceled", "AbortError",
1266
1327
  function isTerminalError(error) {
1267
1328
  return error instanceof Error && TERMINAL_ERROR_NAMES.has(error.name);
1268
1329
  }
1330
+ function isCancellation(error) {
1331
+ return error instanceof Error && (error.name === "RunCanceled" || error.name === "AbortError");
1332
+ }
1269
1333
 
1270
1334
  // src/tasks/each.ts
1271
1335
  function thrownMessage(error) {
@@ -1331,16 +1395,15 @@ async function eachItem(list, handler, options, sink) {
1331
1395
  if (!handle.reported) handle.succeeded();
1332
1396
  } catch (error) {
1333
1397
  if (isTerminalError(error) || sink.isAborted()) {
1334
- step?.setStatus("cancelled");
1335
- step?.finish();
1398
+ step?.setStatus("cancelled").succeeded();
1336
1399
  throw error;
1337
1400
  }
1338
1401
  const message = thrownMessage(error);
1339
1402
  if (!handle.reported) handle.failed(message);
1340
- step?.setError(error);
1403
+ step?.failed(error);
1341
1404
  sink.logError(`item ${description} falhou: ${message}`);
1342
1405
  }
1343
- step?.finish();
1406
+ step?.succeeded();
1344
1407
  if (options.progress !== false) sink.advance();
1345
1408
  result.total += 1;
1346
1409
  result[handle.status ?? "succeeded"] += 1;
@@ -1782,7 +1845,7 @@ var RunReporter = class {
1782
1845
  tx.setStepGuard(checkCanceled);
1783
1846
  const eachSink = {
1784
1847
  report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
1785
- startStep: (op, description) => tx.startChild(op, description),
1848
+ startStep: (op, description) => tx.start(op, description),
1786
1849
  declareTotal: (total) => {
1787
1850
  if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
1788
1851
  },
@@ -1875,8 +1938,17 @@ var RunReporter = class {
1875
1938
  itemSeq = 0;
1876
1939
  /** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
1877
1940
  itemsUnsupported = false;
1878
- /** Cap de itens por run atingido (client-side ou 429) — degrada com um aviso só. */
1941
+ /**
1942
+ * Cota de itens por run do servidor (429, legado — hoje a api não tem teto de itens) —
1943
+ * degrada com um aviso só.
1944
+ */
1879
1945
  itemsQuotaExceeded = false;
1946
+ /**
1947
+ * Armazenamento do plano cheio (402 `storage_limit`): o lote é descartado e o aviso sai
1948
+ * uma vez por episódio. Os envios continuam — num agente `start()` o espaço pode ser
1949
+ * liberado (ou o plano trocado) sem reiniciar; um lote aceito rearma o aviso.
1950
+ */
1951
+ itemsStorageFull = false;
1880
1952
  /**
1881
1953
  * Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
1882
1954
  * servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
@@ -2186,6 +2258,7 @@ var RunReporter = class {
2186
2258
  if (this.itemsUnsupported) return;
2187
2259
  try {
2188
2260
  await this.api.createItems(this.runId, { items: batch });
2261
+ this.itemsStorageFull = false;
2189
2262
  } catch (error) {
2190
2263
  if (error instanceof TasksApiError && error.status === 404) {
2191
2264
  this.itemsUnsupported = true;
@@ -2199,6 +2272,13 @@ var RunReporter = class {
2199
2272
  }
2200
2273
  return;
2201
2274
  }
2275
+ if (error instanceof TasksApiError && error.status === 402) {
2276
+ if (!this.itemsStorageFull) {
2277
+ this.itemsStorageFull = true;
2278
+ this.log("warning", "itens descartados: armazenamento do plano cheio \u2014 libere espa\xE7o ou fa\xE7a upgrade em Plano e cobran\xE7a");
2279
+ }
2280
+ return;
2281
+ }
2202
2282
  if (error instanceof TasksApiError) {
2203
2283
  this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
2204
2284
  this.log("warning", `${batch.length} item(ns) descartado(s): envio rejeitado pelo servidor (HTTP ${error.status})`);
@@ -2483,16 +2563,19 @@ var Step = class {
2483
2563
  return this;
2484
2564
  }
2485
2565
  /**
2486
- * Marca a step como `error` guardando o motivo em `data.error` ({ type, message }).
2487
- * Aceita qualquer valor lançado; `message` sobrescreve a mensagem do erro.
2566
+ * Marca a step como `error` guardando o motivo em `data.error` ({ type, message }) e a
2567
+ * fecha. Aceita qualquer valor lançado; `message` sobrescreve a mensagem do erro.
2488
2568
  * `options.image` pendura um print do estado da tela em `data.attachments`.
2569
+ * Numa step já fechada não faz nada.
2489
2570
  */
2490
- setError(error, message, options) {
2571
+ failed(error, message, options) {
2572
+ if (this.endTimestamp) return this;
2491
2573
  this.status = "error";
2492
2574
  const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
2493
2575
  const raw = message ?? (error instanceof Error ? error.message || error.name : String(error));
2494
2576
  this.setData("error", { ...type ? { type } : {}, message: stepMessage(raw) });
2495
2577
  if (options?.image !== void 0) this.attachImage(options.image);
2578
+ this.close();
2496
2579
  return this;
2497
2580
  }
2498
2581
  /**
@@ -2507,7 +2590,7 @@ var Step = class {
2507
2590
  if (!id) return;
2508
2591
  this.setData("attachments", [...current, id]);
2509
2592
  }
2510
- /** Motivo registrado via setStatus(…, message) ou setError — usado no log de FINISH. */
2593
+ /** Motivo registrado via setStatus(…, message) ou failed — usado no log de FINISH. */
2511
2594
  reason() {
2512
2595
  const error = this.data?.error;
2513
2596
  const text = typeof error?.message === "string" ? error.message : this.data?.message;
@@ -2518,10 +2601,18 @@ var Step = class {
2518
2601
  return this;
2519
2602
  }
2520
2603
  /** Step aninhada (filha desta step). */
2521
- startChild(op, description) {
2604
+ start(op, description) {
2522
2605
  return this.transaction.createStep(this.stepId, op, description);
2523
2606
  }
2524
- finish() {
2607
+ /**
2608
+ * Fecha a step. O status fica o que estiver (`ok` por padrão, ou o de `setStatus`).
2609
+ * Idempotente; a transaction fecha no fim da run o que ficou aberto.
2610
+ */
2611
+ succeeded() {
2612
+ this.close();
2613
+ return this;
2614
+ }
2615
+ close() {
2525
2616
  if (this.endTimestamp) return;
2526
2617
  this.endTimestamp = nowISO();
2527
2618
  this.transaction.emit(
@@ -2614,7 +2705,7 @@ var Transaction = class {
2614
2705
  }
2615
2706
  /**
2616
2707
  * Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
2617
- * `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
2708
+ * `hobots.transaction.start()` (e `step.start()`) param uma run cancelada.
2618
2709
  */
2619
2710
  setStepGuard(guard) {
2620
2711
  this.stepGuard = guard;
@@ -2626,10 +2717,32 @@ var Transaction = class {
2626
2717
  this.emit("step", "start", stepLabel(op, description));
2627
2718
  return step;
2628
2719
  }
2629
- /** Step direta da transaction. */
2630
- startChild(op, description) {
2720
+ /** Step direta da transaction. `op` é o nome usado por `succeeded(name)`/`failed(error, name)`. */
2721
+ start(op, description) {
2631
2722
  return this.createStep(this.stepId, op, description);
2632
2723
  }
2724
+ /**
2725
+ * Fecha a step aberta mais recente — com `name`, a mais recente aberta com esse `op`.
2726
+ * Sem step aberta (ou sem nenhuma com esse nome), não faz nada e devolve undefined.
2727
+ */
2728
+ succeeded(name) {
2729
+ return this.findOpenStep(name)?.succeeded();
2730
+ }
2731
+ /**
2732
+ * Marca como `error` e fecha a step aberta mais recente — com `name`, a mais recente
2733
+ * aberta com esse `op`. `options.message` sobrescreve a mensagem do erro e
2734
+ * `options.image` pendura um print. Sem step aberta, não faz nada e devolve undefined.
2735
+ */
2736
+ failed(error, name, options) {
2737
+ return this.findOpenStep(name)?.failed(error, options?.message, options);
2738
+ }
2739
+ findOpenStep(name) {
2740
+ for (let i = this.steps.length - 1; i >= 0; i--) {
2741
+ const step = this.steps[i];
2742
+ if (!step.endTimestamp && (name === void 0 || step.op === name)) return step;
2743
+ }
2744
+ return void 0;
2745
+ }
2633
2746
  setTag(key, value) {
2634
2747
  this.tags[key] = value;
2635
2748
  return this;
@@ -2667,7 +2780,7 @@ var Transaction = class {
2667
2780
  remaining.push(step);
2668
2781
  }
2669
2782
  }
2670
- for (const step of batch) step.finish();
2783
+ for (const step of batch) step.succeeded();
2671
2784
  this.steps.splice(0, this.steps.length, ...remaining);
2672
2785
  if (this.client) {
2673
2786
  for (let i = 0; i < batch.length; i += MAX_STEPS) {
@@ -2699,7 +2812,7 @@ var Transaction = class {
2699
2812
  finish() {
2700
2813
  if (this.finished) return;
2701
2814
  this.finished = true;
2702
- for (const step of this.steps) step.finish();
2815
+ for (const step of this.steps) step.succeeded();
2703
2816
  const endTimestamp = nowISO();
2704
2817
  this.emit(
2705
2818
  "transaction",
@@ -2761,6 +2874,7 @@ async function executeRun(input) {
2761
2874
  await reporter.finalizeItems();
2762
2875
  tx.setStatus("error");
2763
2876
  tx.finish();
2877
+ captureRunFailure(client, error, reporter);
2764
2878
  reporter.reportFailure(toRunError(error));
2765
2879
  logger.debug(`run ${run.id} falhou: ${String(error)}`);
2766
2880
  }
@@ -2770,6 +2884,11 @@ async function executeRun(input) {
2770
2884
  tracker.remove(reporter);
2771
2885
  }
2772
2886
  }
2887
+ function captureRunFailure(client, error, reporter) {
2888
+ if (!client?.options.captureRunErrors) return;
2889
+ if (reporter.isDead || isCancellation(error) || wasCaptured(error)) return;
2890
+ client.captureException(error, { type: "error", tags: { mechanism: "run_handler", run_id: reporter.runId } });
2891
+ }
2773
2892
  function warnIncompleteProgress(reporter) {
2774
2893
  const snapshot = reporter.context.progress.snapshot;
2775
2894
  if (!snapshot || snapshot.current >= snapshot.total) return;
@@ -2961,6 +3080,86 @@ var TaskAgent = class {
2961
3080
  }
2962
3081
  };
2963
3082
 
3083
+ // src/tasks/attached.ts
3084
+ var START_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
3085
+ function isTransient(error) {
3086
+ if (!(error instanceof TasksApiError)) return true;
3087
+ return error.status >= 500 || error.status === 429;
3088
+ }
3089
+ function describeRejection(runId, error) {
3090
+ if (error instanceof TasksApiError && error.status === 404) {
3091
+ return `run ${runId} n\xE3o existe na plataforma (404) \u2014 nada a executar`;
3092
+ }
3093
+ if (error instanceof TasksApiError && error.status === 409) {
3094
+ return `run ${runId} n\xE3o pode ser iniciada (409: cancelada, j\xE1 iniciada ou de outra inst\xE2ncia) \u2014 nada a executar`;
3095
+ }
3096
+ return `n\xE3o foi poss\xEDvel iniciar a run ${runId} (${String(error)}) \u2014 nada a executar`;
3097
+ }
3098
+ var AttachedRunner = class {
3099
+ constructor(runId, handler, deps) {
3100
+ this.runId = runId;
3101
+ this.handler = handler;
3102
+ this.deps = deps;
3103
+ this.done = new Promise((resolve2) => {
3104
+ this.resolveDone = resolve2;
3105
+ });
3106
+ }
3107
+ runId;
3108
+ handler;
3109
+ deps;
3110
+ abort = new AbortController();
3111
+ /** Resolve quando a run terminou (ou não pôde ser iniciada, ou o runner foi parado antes). */
3112
+ done;
3113
+ resolveDone;
3114
+ async run() {
3115
+ try {
3116
+ const run = await this.startRun();
3117
+ if (!run) return;
3118
+ this.deps.logger.debug(`run ${run.id} iniciada (HOBOTS_RUN_ID)`);
3119
+ await executeRun({
3120
+ run,
3121
+ handler: this.handler,
3122
+ api: this.deps.api,
3123
+ client: this.deps.client,
3124
+ logger: this.deps.logger,
3125
+ tracker: this.deps.tracker
3126
+ });
3127
+ } catch (error) {
3128
+ this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
3129
+ } finally {
3130
+ this.resolveDone();
3131
+ }
3132
+ }
3133
+ /** Desiste de iniciar a run (close() antes da plataforma responder). Uma run já iniciada segue pelo tracker. */
3134
+ stop() {
3135
+ this.abort.abort();
3136
+ }
3137
+ async startRun() {
3138
+ for (let attempt = 0; ; attempt++) {
3139
+ if (this.abort.signal.aborted) return void 0;
3140
+ try {
3141
+ return await this.deps.api.startRun(this.runId, { instance: this.deps.instanceInfo() }, this.abort.signal);
3142
+ } catch (error) {
3143
+ if (this.abort.signal.aborted) return void 0;
3144
+ if (!isTransient(error)) {
3145
+ this.deps.logger.error(describeRejection(this.runId, error));
3146
+ return void 0;
3147
+ }
3148
+ const delay = START_RETRY_DELAYS_MS[attempt];
3149
+ if (delay === void 0) {
3150
+ this.deps.logger.error(
3151
+ `n\xE3o foi poss\xEDvel iniciar a run ${this.runId} ap\xF3s ${attempt + 1} tentativas (${String(error)}) \u2014 desistindo`
3152
+ );
3153
+ return void 0;
3154
+ }
3155
+ const wait = jitter(delay);
3156
+ this.deps.logger.warn(`in\xEDcio da run ${this.runId} falhou (${String(error)}), pr\xF3xima tentativa em ${wait}ms`);
3157
+ await abortableSleep(wait, this.abort.signal);
3158
+ }
3159
+ }
3160
+ }
3161
+ };
3162
+
2964
3163
  // src/tasks/item-columns.ts
2965
3164
  var KEY_RE = /^[a-zA-Z0-9_.-]{1,100}$/;
2966
3165
  var MAX_COLUMNS = WIRE_LIMITS.MAX_ITEM_COLUMNS;
@@ -3166,11 +3365,18 @@ function hostname() {
3166
3365
  return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
3167
3366
  }
3168
3367
  function createRuntime(options, handler) {
3368
+ loadDotenv();
3169
3369
  const config = resolveConfig({ clientSecret: options.clientSecret });
3170
3370
  const logger = makeLogger(options.debug ?? false);
3171
3371
  const environment = resolveEnvironmentOption(options.environment);
3172
3372
  const client = new Client(
3173
- { environment, release: options.release, tags: options.tags, debug: options.debug },
3373
+ {
3374
+ environment,
3375
+ release: options.release,
3376
+ tags: options.tags,
3377
+ debug: options.debug,
3378
+ captureRunErrors: options.captureUnhandled !== false
3379
+ },
3174
3380
  config
3175
3381
  );
3176
3382
  const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
@@ -3186,6 +3392,8 @@ function createRuntime(options, handler) {
3186
3392
  heartbeat: void 0,
3187
3393
  agent: void 0,
3188
3394
  local: void 0,
3395
+ attached: void 0,
3396
+ runId: void 0,
3189
3397
  instanceId: resolveInstanceId(options.instanceId),
3190
3398
  environment,
3191
3399
  pollIntervalMs: void 0,
@@ -3263,6 +3471,20 @@ function launchLocal(rt) {
3263
3471
  rt.local = runner;
3264
3472
  void runner.run();
3265
3473
  }
3474
+ function launchAttached(rt, runId) {
3475
+ const runner = new AttachedRunner(runId, rt.handler, {
3476
+ api: rt.api,
3477
+ client: rt.client,
3478
+ logger: rt.logger,
3479
+ tracker: rt.tracker,
3480
+ instanceInfo: () => instanceInfo(rt)
3481
+ });
3482
+ rt.attached = runner;
3483
+ void runner.run();
3484
+ void runner.done.then(() => {
3485
+ if (runtime === rt) void close();
3486
+ });
3487
+ }
3266
3488
  async function boot(rt) {
3267
3489
  if (!rt.instanceId) {
3268
3490
  const id = await fallbackInstanceId();
@@ -3282,8 +3504,9 @@ async function boot(rt) {
3282
3504
  if (rt.options.installExitHandlers !== false) {
3283
3505
  rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
3284
3506
  }
3285
- if (rt.handler.mode === "remote") ensureAgent(rt).start();
3286
- else launchLocal(rt);
3507
+ if (rt.handler.mode === "local") launchLocal(rt);
3508
+ else if (rt.runId) launchAttached(rt, rt.runId);
3509
+ else ensureAgent(rt).start();
3287
3510
  }
3288
3511
  function scheduleBoot(rt) {
3289
3512
  if (rt.bootScheduled) return;
@@ -3301,6 +3524,7 @@ function startAgent(fn, mode, handler, options) {
3301
3524
  const agentHandler = { mode, handler, options: resolveRunOptions(options, mode, logger) };
3302
3525
  const rt = createRuntime(options, agentHandler);
3303
3526
  if (mode === "remote" && options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
3527
+ if (mode === "remote") rt.runId = resolveRunId(options.runId);
3304
3528
  runtime = rt;
3305
3529
  scheduleBoot(rt);
3306
3530
  }
@@ -3341,7 +3565,9 @@ function captureException(error, hint) {
3341
3565
  const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
3342
3566
  forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
3343
3567
  }
3344
- return rt.client.captureException(error, hint);
3568
+ const runId = currentReporter()?.runId;
3569
+ const withRun = runId ? { ...hint, tags: { run_id: runId, ...hint?.tags } } : hint;
3570
+ return rt.client.captureException(error, withRun);
3345
3571
  }
3346
3572
  async function close(timeoutMs = 3e4) {
3347
3573
  const rt = runtime;
@@ -3350,6 +3576,7 @@ async function close(timeoutMs = 3e4) {
3350
3576
  await rt.bootPromise;
3351
3577
  if (rt.agent) await rt.agent.stop(0);
3352
3578
  rt.local?.stop();
3579
+ rt.attached?.stop();
3353
3580
  const idle = await rt.tracker.waitIdle(timeoutMs);
3354
3581
  if (!idle) {
3355
3582
  rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
@@ -3467,5 +3694,6 @@ var image2 = hobots.image;
3467
3694
  var isFileRef2 = hobots.isFileRef;
3468
3695
 
3469
3696
  // src/index.ts
3697
+ loadDotenv();
3470
3698
  var src_default = hobots;
3471
3699
  //# sourceMappingURL=index.cjs.map