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.js CHANGED
@@ -2,62 +2,9 @@ import {
2
2
  WIRE_LIMITS
3
3
  } from "./chunk-HKCDMUA3.js";
4
4
 
5
- // src/stacktrace.ts
6
- var MAX_FRAMES = 50;
7
- var MAX_FILENAME_CHARS = 1024;
8
- var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
9
- var LOCATION_RE = /^(.*?):(\d+):(\d+)$/;
10
- function isInApp(filename) {
11
- if (!filename) return false;
12
- if (filename.startsWith("node:")) return false;
13
- if (filename.startsWith("internal/")) return false;
14
- if (filename.includes("node_modules")) return false;
15
- if (filename === "<anonymous>") return false;
16
- return true;
17
- }
18
- function parseStacktrace(stack) {
19
- if (!stack) return [];
20
- const frames = [];
21
- for (const line of stack.split("\n")) {
22
- if (frames.length >= MAX_FRAMES) break;
23
- const match = FRAME_RE.exec(line);
24
- if (!match || !line.trimStart().startsWith("at ")) continue;
25
- const [, fn, location] = match;
26
- const loc = LOCATION_RE.exec(location ?? "");
27
- const filename = loc ? loc[1] : location;
28
- frames.push({
29
- function: fn || "<anonymous>",
30
- filename: filename ? filename.slice(0, MAX_FILENAME_CHARS) : void 0,
31
- lineno: loc?.[2] ? Number(loc[2]) : void 0,
32
- colno: loc?.[3] ? Number(loc[3]) : void 0,
33
- in_app: isInApp(filename)
34
- });
35
- }
36
- return frames;
37
- }
38
-
39
- // src/scrub.ts
40
- var SECRET_PATTERNS = [
41
- // client secret da Hobots
42
- [/hb_[a-f0-9]{16,}/gi, "[REDACTED]"],
43
- // tokens bearer em headers/mensagens
44
- [/\b(authorization:?\s*)?bearer\s+[a-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]"],
45
- // chaves no formato sk-… (OpenAI e afins)
46
- [/\bsk-[a-z0-9_-]{16,}\b/gi, "[REDACTED]"],
47
- // credenciais embutidas em URL (https://user:pass@host)
48
- [/\/\/[^/\s:@]+:[^/\s@]+@/g, "//[REDACTED]@"]
49
- ];
50
- function scrubSecrets(text) {
51
- let out = text;
52
- for (const [pattern, replacement] of SECRET_PATTERNS) {
53
- out = out.replace(pattern, replacement);
54
- }
55
- return out;
56
- }
57
-
58
5
  // src/utils.ts
59
6
  var SDK_NAME = "hobots";
60
- var SDK_VERSION = true ? "4.2.0" : "0.0.0-dev";
7
+ var SDK_VERSION = true ? "4.5.0" : "0.0.0-dev";
61
8
  function isNode() {
62
9
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
63
10
  }
@@ -205,6 +152,104 @@ function colorize(text, color) {
205
152
  return `${ANSI[color]}${text}${ANSI.reset}`;
206
153
  }
207
154
 
155
+ // src/env.ts
156
+ var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
157
+ var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
158
+ var RUN_ID_ENV = "HOBOTS_RUN_ID";
159
+ var dotenvLoaded = false;
160
+ function loadDotenv() {
161
+ if (dotenvLoaded) return;
162
+ dotenvLoaded = true;
163
+ if (!isNode() || typeof process.loadEnvFile !== "function") return;
164
+ try {
165
+ process.loadEnvFile(".env");
166
+ } catch (err) {
167
+ const code = err?.code;
168
+ if (code === "ENOENT") return;
169
+ 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`);
170
+ }
171
+ }
172
+ function resolveInstanceId(option) {
173
+ const fromOption = option?.trim();
174
+ if (fromOption) return fromOption;
175
+ if (typeof process === "undefined" || !process.env) return void 0;
176
+ return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
177
+ }
178
+ function resolveRunId(option) {
179
+ const fromOption = option?.trim();
180
+ if (fromOption) return fromOption;
181
+ if (typeof process === "undefined" || !process.env) return void 0;
182
+ return process.env[RUN_ID_ENV]?.trim() || void 0;
183
+ }
184
+ function resolveEnvironmentOption(option) {
185
+ const fromOption = option?.trim();
186
+ if (fromOption) return fromOption;
187
+ if (typeof process === "undefined" || !process.env) return void 0;
188
+ return process.env[ENVIRONMENT_ENV]?.trim() || void 0;
189
+ }
190
+ async function fallbackInstanceId() {
191
+ if (!isNode()) return "browser";
192
+ try {
193
+ const os = await import("os");
194
+ return os.hostname() || "unknown-host";
195
+ } catch {
196
+ return "unknown-host";
197
+ }
198
+ }
199
+
200
+ // src/stacktrace.ts
201
+ var MAX_FRAMES = 50;
202
+ var MAX_FILENAME_CHARS = 1024;
203
+ var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
204
+ var LOCATION_RE = /^(.*?):(\d+):(\d+)$/;
205
+ function isInApp(filename) {
206
+ if (!filename) return false;
207
+ if (filename.startsWith("node:")) return false;
208
+ if (filename.startsWith("internal/")) return false;
209
+ if (filename.includes("node_modules")) return false;
210
+ if (filename === "<anonymous>") return false;
211
+ return true;
212
+ }
213
+ function parseStacktrace(stack) {
214
+ if (!stack) return [];
215
+ const frames = [];
216
+ for (const line of stack.split("\n")) {
217
+ if (frames.length >= MAX_FRAMES) break;
218
+ const match = FRAME_RE.exec(line);
219
+ if (!match || !line.trimStart().startsWith("at ")) continue;
220
+ const [, fn, location] = match;
221
+ const loc = LOCATION_RE.exec(location ?? "");
222
+ const filename = loc ? loc[1] : location;
223
+ frames.push({
224
+ function: fn || "<anonymous>",
225
+ filename: filename ? filename.slice(0, MAX_FILENAME_CHARS) : void 0,
226
+ lineno: loc?.[2] ? Number(loc[2]) : void 0,
227
+ colno: loc?.[3] ? Number(loc[3]) : void 0,
228
+ in_app: isInApp(filename)
229
+ });
230
+ }
231
+ return frames;
232
+ }
233
+
234
+ // src/scrub.ts
235
+ var SECRET_PATTERNS = [
236
+ // client secret da Hobots
237
+ [/hb_[a-f0-9]{16,}/gi, "[REDACTED]"],
238
+ // tokens bearer em headers/mensagens
239
+ [/\b(authorization:?\s*)?bearer\s+[a-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]"],
240
+ // chaves no formato sk-… (OpenAI e afins)
241
+ [/\bsk-[a-z0-9_-]{16,}\b/gi, "[REDACTED]"],
242
+ // credenciais embutidas em URL (https://user:pass@host)
243
+ [/\/\/[^/\s:@]+:[^/\s@]+@/g, "//[REDACTED]@"]
244
+ ];
245
+ function scrubSecrets(text) {
246
+ let out = text;
247
+ for (const [pattern, replacement] of SECRET_PATTERNS) {
248
+ out = out.replace(pattern, replacement);
249
+ }
250
+ return out;
251
+ }
252
+
208
253
  // src/eventbuilder.ts
209
254
  var MAX_EXCEPTION_VALUE_CHARS = 8192;
210
255
  function eventFromException(error, type = "error") {
@@ -356,6 +401,10 @@ function clipFingerprint(fingerprint) {
356
401
  if (fingerprint === void 0) return void 0;
357
402
  return fingerprint.slice(0, MAX_FINGERPRINT_ITEMS).map((item) => truncate(String(item), MAX_FINGERPRINT_ITEM_CHARS));
358
403
  }
404
+ var captured = /* @__PURE__ */ new WeakSet();
405
+ function wasCaptured(error) {
406
+ return typeof error === "object" && error !== null && captured.has(error);
407
+ }
359
408
  function runtimeContexts() {
360
409
  if (isNode()) {
361
410
  return {
@@ -407,6 +456,7 @@ var Client = class {
407
456
  return `Client { baseUrl: '${this.#config.baseUrl}' }`;
408
457
  }
409
458
  captureException(error, hint) {
459
+ if (typeof error === "object" && error !== null) captured.add(error);
410
460
  return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
411
461
  }
412
462
  captureEvent(event, hint) {
@@ -514,12 +564,12 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
514
564
  const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
515
565
  if (!key) {
516
566
  throw new Error(
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`
567
+ `${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`
518
568
  );
519
569
  }
520
570
  if (!SECRET_PATTERN.test(key)) {
521
571
  throw new Error(
522
- `${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`
572
+ `${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`
523
573
  );
524
574
  }
525
575
  const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
@@ -531,31 +581,6 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
531
581
  };
532
582
  }
533
583
 
534
- // src/env.ts
535
- var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
536
- var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
537
- function resolveInstanceId(option) {
538
- const fromOption = option?.trim();
539
- if (fromOption) return fromOption;
540
- if (typeof process === "undefined" || !process.env) return void 0;
541
- return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
542
- }
543
- function resolveEnvironmentOption(option) {
544
- const fromOption = option?.trim();
545
- if (fromOption) return fromOption;
546
- if (typeof process === "undefined" || !process.env) return void 0;
547
- return process.env[ENVIRONMENT_ENV]?.trim() || void 0;
548
- }
549
- async function fallbackInstanceId() {
550
- if (!isNode()) return "browser";
551
- try {
552
- const os = await import("os");
553
- return os.hostname() || "unknown-host";
554
- } catch {
555
- return "unknown-host";
556
- }
557
- }
558
-
559
584
  // src/heartbeat.ts
560
585
  var DEFAULT_INTERVAL_MS = WIRE_LIMITS.HEARTBEAT_DEFAULT_INTERVAL_MS;
561
586
  var MIN_REPORTED_INTERVAL_MS = WIRE_LIMITS.HEARTBEAT_MIN_INTERVAL_MS;
@@ -917,6 +942,42 @@ var TasksApi = class {
917
942
  created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
918
943
  };
919
944
  }
945
+ /**
946
+ * Anexa o processo a uma run já reivindicada para esta instância (env HOBOTS_RUN_ID):
947
+ * POST /v1/runs/:id/start. Um request só — quem decide o retry é o AttachedRunner.
948
+ * 404 (não existe) e 409 (cancelada, já iniciada ou de outra instância) viram
949
+ * TasksApiError com o status.
950
+ */
951
+ async startRun(runId, request, signal) {
952
+ const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
953
+ let response;
954
+ try {
955
+ response = await fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/start`, {
956
+ redirect: "error",
957
+ method: "POST",
958
+ headers: this.headers(),
959
+ body: JSON.stringify(request),
960
+ signal: timeoutSignal
961
+ });
962
+ } finally {
963
+ cleanup();
964
+ }
965
+ if (!response.ok) {
966
+ throw new TasksApiError(`in\xEDcio da run falhou (HTTP ${response.status})`, response.status);
967
+ }
968
+ const body = await response.json().catch(() => null);
969
+ if (typeof body?.id !== "string") {
970
+ throw new TasksApiError("resposta inv\xE1lida do in\xEDcio da run (id ausente)", 0);
971
+ }
972
+ const form = body.form;
973
+ return {
974
+ id: body.id,
975
+ params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
976
+ form: typeof form?.id === "string" && typeof form.name === "string" ? { id: form.id, name: form.name } : null,
977
+ source: typeof body.source === "string" ? body.source : "hobots",
978
+ created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
979
+ };
980
+ }
920
981
  /** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
921
982
  parsePollResponse(body, max) {
922
983
  const runs = body?.runs;
@@ -1174,7 +1235,7 @@ var Backoff = class {
1174
1235
  this.authFailures++;
1175
1236
  if (this.authFailures === 1 || this.authFailures % 10 === 0) {
1176
1237
  this.logger.error(
1177
- 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"
1238
+ 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"
1178
1239
  );
1179
1240
  }
1180
1241
  return Math.max(this.floorMs, AUTH_FAILURE_INTERVAL_MS);
@@ -1200,6 +1261,9 @@ var TERMINAL_ERROR_NAMES = /* @__PURE__ */ new Set(["RunCanceled", "AbortError",
1200
1261
  function isTerminalError(error) {
1201
1262
  return error instanceof Error && TERMINAL_ERROR_NAMES.has(error.name);
1202
1263
  }
1264
+ function isCancellation(error) {
1265
+ return error instanceof Error && (error.name === "RunCanceled" || error.name === "AbortError");
1266
+ }
1203
1267
 
1204
1268
  // src/tasks/each.ts
1205
1269
  function thrownMessage(error) {
@@ -1265,16 +1329,15 @@ async function eachItem(list, handler, options, sink) {
1265
1329
  if (!handle.reported) handle.succeeded();
1266
1330
  } catch (error) {
1267
1331
  if (isTerminalError(error) || sink.isAborted()) {
1268
- step?.setStatus("cancelled");
1269
- step?.finish();
1332
+ step?.setStatus("cancelled").succeeded();
1270
1333
  throw error;
1271
1334
  }
1272
1335
  const message = thrownMessage(error);
1273
1336
  if (!handle.reported) handle.failed(message);
1274
- step?.setError(error);
1337
+ step?.failed(error);
1275
1338
  sink.logError(`item ${description} falhou: ${message}`);
1276
1339
  }
1277
- step?.finish();
1340
+ step?.succeeded();
1278
1341
  if (options.progress !== false) sink.advance();
1279
1342
  result.total += 1;
1280
1343
  result[handle.status ?? "succeeded"] += 1;
@@ -1716,7 +1779,7 @@ var RunReporter = class {
1716
1779
  tx.setStepGuard(checkCanceled);
1717
1780
  const eachSink = {
1718
1781
  report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
1719
- startStep: (op, description) => tx.startChild(op, description),
1782
+ startStep: (op, description) => tx.start(op, description),
1720
1783
  declareTotal: (total) => {
1721
1784
  if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
1722
1785
  },
@@ -1809,8 +1872,17 @@ var RunReporter = class {
1809
1872
  itemSeq = 0;
1810
1873
  /** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
1811
1874
  itemsUnsupported = false;
1812
- /** Cap de itens por run atingido (client-side ou 429) — degrada com um aviso só. */
1875
+ /**
1876
+ * Cota de itens por run do servidor (429, legado — hoje a api não tem teto de itens) —
1877
+ * degrada com um aviso só.
1878
+ */
1813
1879
  itemsQuotaExceeded = false;
1880
+ /**
1881
+ * Armazenamento do plano cheio (402 `storage_limit`): o lote é descartado e o aviso sai
1882
+ * uma vez por episódio. Os envios continuam — num agente `start()` o espaço pode ser
1883
+ * liberado (ou o plano trocado) sem reiniciar; um lote aceito rearma o aviso.
1884
+ */
1885
+ itemsStorageFull = false;
1814
1886
  /**
1815
1887
  * Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
1816
1888
  * servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
@@ -2120,6 +2192,7 @@ var RunReporter = class {
2120
2192
  if (this.itemsUnsupported) return;
2121
2193
  try {
2122
2194
  await this.api.createItems(this.runId, { items: batch });
2195
+ this.itemsStorageFull = false;
2123
2196
  } catch (error) {
2124
2197
  if (error instanceof TasksApiError && error.status === 404) {
2125
2198
  this.itemsUnsupported = true;
@@ -2133,6 +2206,13 @@ var RunReporter = class {
2133
2206
  }
2134
2207
  return;
2135
2208
  }
2209
+ if (error instanceof TasksApiError && error.status === 402) {
2210
+ if (!this.itemsStorageFull) {
2211
+ this.itemsStorageFull = true;
2212
+ this.log("warning", "itens descartados: armazenamento do plano cheio \u2014 libere espa\xE7o ou fa\xE7a upgrade em Plano e cobran\xE7a");
2213
+ }
2214
+ return;
2215
+ }
2136
2216
  if (error instanceof TasksApiError) {
2137
2217
  this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
2138
2218
  this.log("warning", `${batch.length} item(ns) descartado(s): envio rejeitado pelo servidor (HTTP ${error.status})`);
@@ -2417,16 +2497,19 @@ var Step = class {
2417
2497
  return this;
2418
2498
  }
2419
2499
  /**
2420
- * Marca a step como `error` guardando o motivo em `data.error` ({ type, message }).
2421
- * Aceita qualquer valor lançado; `message` sobrescreve a mensagem do erro.
2500
+ * Marca a step como `error` guardando o motivo em `data.error` ({ type, message }) e a
2501
+ * fecha. Aceita qualquer valor lançado; `message` sobrescreve a mensagem do erro.
2422
2502
  * `options.image` pendura um print do estado da tela em `data.attachments`.
2503
+ * Numa step já fechada não faz nada.
2423
2504
  */
2424
- setError(error, message, options) {
2505
+ failed(error, message, options) {
2506
+ if (this.endTimestamp) return this;
2425
2507
  this.status = "error";
2426
2508
  const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
2427
2509
  const raw = message ?? (error instanceof Error ? error.message || error.name : String(error));
2428
2510
  this.setData("error", { ...type ? { type } : {}, message: stepMessage(raw) });
2429
2511
  if (options?.image !== void 0) this.attachImage(options.image);
2512
+ this.close();
2430
2513
  return this;
2431
2514
  }
2432
2515
  /**
@@ -2441,7 +2524,7 @@ var Step = class {
2441
2524
  if (!id) return;
2442
2525
  this.setData("attachments", [...current, id]);
2443
2526
  }
2444
- /** Motivo registrado via setStatus(…, message) ou setError — usado no log de FINISH. */
2527
+ /** Motivo registrado via setStatus(…, message) ou failed — usado no log de FINISH. */
2445
2528
  reason() {
2446
2529
  const error = this.data?.error;
2447
2530
  const text = typeof error?.message === "string" ? error.message : this.data?.message;
@@ -2452,10 +2535,18 @@ var Step = class {
2452
2535
  return this;
2453
2536
  }
2454
2537
  /** Step aninhada (filha desta step). */
2455
- startChild(op, description) {
2538
+ start(op, description) {
2456
2539
  return this.transaction.createStep(this.stepId, op, description);
2457
2540
  }
2458
- finish() {
2541
+ /**
2542
+ * Fecha a step. O status fica o que estiver (`ok` por padrão, ou o de `setStatus`).
2543
+ * Idempotente; a transaction fecha no fim da run o que ficou aberto.
2544
+ */
2545
+ succeeded() {
2546
+ this.close();
2547
+ return this;
2548
+ }
2549
+ close() {
2459
2550
  if (this.endTimestamp) return;
2460
2551
  this.endTimestamp = nowISO();
2461
2552
  this.transaction.emit(
@@ -2548,7 +2639,7 @@ var Transaction = class {
2548
2639
  }
2549
2640
  /**
2550
2641
  * Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
2551
- * `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
2642
+ * `hobots.transaction.start()` (e `step.start()`) param uma run cancelada.
2552
2643
  */
2553
2644
  setStepGuard(guard) {
2554
2645
  this.stepGuard = guard;
@@ -2560,10 +2651,32 @@ var Transaction = class {
2560
2651
  this.emit("step", "start", stepLabel(op, description));
2561
2652
  return step;
2562
2653
  }
2563
- /** Step direta da transaction. */
2564
- startChild(op, description) {
2654
+ /** Step direta da transaction. `op` é o nome usado por `succeeded(name)`/`failed(error, name)`. */
2655
+ start(op, description) {
2565
2656
  return this.createStep(this.stepId, op, description);
2566
2657
  }
2658
+ /**
2659
+ * Fecha a step aberta mais recente — com `name`, a mais recente aberta com esse `op`.
2660
+ * Sem step aberta (ou sem nenhuma com esse nome), não faz nada e devolve undefined.
2661
+ */
2662
+ succeeded(name) {
2663
+ return this.findOpenStep(name)?.succeeded();
2664
+ }
2665
+ /**
2666
+ * Marca como `error` e fecha a step aberta mais recente — com `name`, a mais recente
2667
+ * aberta com esse `op`. `options.message` sobrescreve a mensagem do erro e
2668
+ * `options.image` pendura um print. Sem step aberta, não faz nada e devolve undefined.
2669
+ */
2670
+ failed(error, name, options) {
2671
+ return this.findOpenStep(name)?.failed(error, options?.message, options);
2672
+ }
2673
+ findOpenStep(name) {
2674
+ for (let i = this.steps.length - 1; i >= 0; i--) {
2675
+ const step = this.steps[i];
2676
+ if (!step.endTimestamp && (name === void 0 || step.op === name)) return step;
2677
+ }
2678
+ return void 0;
2679
+ }
2567
2680
  setTag(key, value) {
2568
2681
  this.tags[key] = value;
2569
2682
  return this;
@@ -2601,7 +2714,7 @@ var Transaction = class {
2601
2714
  remaining.push(step);
2602
2715
  }
2603
2716
  }
2604
- for (const step of batch) step.finish();
2717
+ for (const step of batch) step.succeeded();
2605
2718
  this.steps.splice(0, this.steps.length, ...remaining);
2606
2719
  if (this.client) {
2607
2720
  for (let i = 0; i < batch.length; i += MAX_STEPS) {
@@ -2633,7 +2746,7 @@ var Transaction = class {
2633
2746
  finish() {
2634
2747
  if (this.finished) return;
2635
2748
  this.finished = true;
2636
- for (const step of this.steps) step.finish();
2749
+ for (const step of this.steps) step.succeeded();
2637
2750
  const endTimestamp = nowISO();
2638
2751
  this.emit(
2639
2752
  "transaction",
@@ -2695,6 +2808,7 @@ async function executeRun(input) {
2695
2808
  await reporter.finalizeItems();
2696
2809
  tx.setStatus("error");
2697
2810
  tx.finish();
2811
+ captureRunFailure(client, error, reporter);
2698
2812
  reporter.reportFailure(toRunError(error));
2699
2813
  logger.debug(`run ${run.id} falhou: ${String(error)}`);
2700
2814
  }
@@ -2704,6 +2818,11 @@ async function executeRun(input) {
2704
2818
  tracker.remove(reporter);
2705
2819
  }
2706
2820
  }
2821
+ function captureRunFailure(client, error, reporter) {
2822
+ if (!client?.options.captureRunErrors) return;
2823
+ if (reporter.isDead || isCancellation(error) || wasCaptured(error)) return;
2824
+ client.captureException(error, { type: "error", tags: { mechanism: "run_handler", run_id: reporter.runId } });
2825
+ }
2707
2826
  function warnIncompleteProgress(reporter) {
2708
2827
  const snapshot = reporter.context.progress.snapshot;
2709
2828
  if (!snapshot || snapshot.current >= snapshot.total) return;
@@ -2895,6 +3014,86 @@ var TaskAgent = class {
2895
3014
  }
2896
3015
  };
2897
3016
 
3017
+ // src/tasks/attached.ts
3018
+ var START_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
3019
+ function isTransient(error) {
3020
+ if (!(error instanceof TasksApiError)) return true;
3021
+ return error.status >= 500 || error.status === 429;
3022
+ }
3023
+ function describeRejection(runId, error) {
3024
+ if (error instanceof TasksApiError && error.status === 404) {
3025
+ return `run ${runId} n\xE3o existe na plataforma (404) \u2014 nada a executar`;
3026
+ }
3027
+ if (error instanceof TasksApiError && error.status === 409) {
3028
+ return `run ${runId} n\xE3o pode ser iniciada (409: cancelada, j\xE1 iniciada ou de outra inst\xE2ncia) \u2014 nada a executar`;
3029
+ }
3030
+ return `n\xE3o foi poss\xEDvel iniciar a run ${runId} (${String(error)}) \u2014 nada a executar`;
3031
+ }
3032
+ var AttachedRunner = class {
3033
+ constructor(runId, handler, deps) {
3034
+ this.runId = runId;
3035
+ this.handler = handler;
3036
+ this.deps = deps;
3037
+ this.done = new Promise((resolve2) => {
3038
+ this.resolveDone = resolve2;
3039
+ });
3040
+ }
3041
+ runId;
3042
+ handler;
3043
+ deps;
3044
+ abort = new AbortController();
3045
+ /** Resolve quando a run terminou (ou não pôde ser iniciada, ou o runner foi parado antes). */
3046
+ done;
3047
+ resolveDone;
3048
+ async run() {
3049
+ try {
3050
+ const run = await this.startRun();
3051
+ if (!run) return;
3052
+ this.deps.logger.debug(`run ${run.id} iniciada (HOBOTS_RUN_ID)`);
3053
+ await executeRun({
3054
+ run,
3055
+ handler: this.handler,
3056
+ api: this.deps.api,
3057
+ client: this.deps.client,
3058
+ logger: this.deps.logger,
3059
+ tracker: this.deps.tracker
3060
+ });
3061
+ } catch (error) {
3062
+ this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
3063
+ } finally {
3064
+ this.resolveDone();
3065
+ }
3066
+ }
3067
+ /** Desiste de iniciar a run (close() antes da plataforma responder). Uma run já iniciada segue pelo tracker. */
3068
+ stop() {
3069
+ this.abort.abort();
3070
+ }
3071
+ async startRun() {
3072
+ for (let attempt = 0; ; attempt++) {
3073
+ if (this.abort.signal.aborted) return void 0;
3074
+ try {
3075
+ return await this.deps.api.startRun(this.runId, { instance: this.deps.instanceInfo() }, this.abort.signal);
3076
+ } catch (error) {
3077
+ if (this.abort.signal.aborted) return void 0;
3078
+ if (!isTransient(error)) {
3079
+ this.deps.logger.error(describeRejection(this.runId, error));
3080
+ return void 0;
3081
+ }
3082
+ const delay = START_RETRY_DELAYS_MS[attempt];
3083
+ if (delay === void 0) {
3084
+ this.deps.logger.error(
3085
+ `n\xE3o foi poss\xEDvel iniciar a run ${this.runId} ap\xF3s ${attempt + 1} tentativas (${String(error)}) \u2014 desistindo`
3086
+ );
3087
+ return void 0;
3088
+ }
3089
+ const wait = jitter(delay);
3090
+ this.deps.logger.warn(`in\xEDcio da run ${this.runId} falhou (${String(error)}), pr\xF3xima tentativa em ${wait}ms`);
3091
+ await abortableSleep(wait, this.abort.signal);
3092
+ }
3093
+ }
3094
+ }
3095
+ };
3096
+
2898
3097
  // src/tasks/item-columns.ts
2899
3098
  var KEY_RE = /^[a-zA-Z0-9_.-]{1,100}$/;
2900
3099
  var MAX_COLUMNS = WIRE_LIMITS.MAX_ITEM_COLUMNS;
@@ -3100,11 +3299,18 @@ function hostname() {
3100
3299
  return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
3101
3300
  }
3102
3301
  function createRuntime(options, handler) {
3302
+ loadDotenv();
3103
3303
  const config = resolveConfig({ clientSecret: options.clientSecret });
3104
3304
  const logger = makeLogger(options.debug ?? false);
3105
3305
  const environment = resolveEnvironmentOption(options.environment);
3106
3306
  const client = new Client(
3107
- { environment, release: options.release, tags: options.tags, debug: options.debug },
3307
+ {
3308
+ environment,
3309
+ release: options.release,
3310
+ tags: options.tags,
3311
+ debug: options.debug,
3312
+ captureRunErrors: options.captureUnhandled !== false
3313
+ },
3108
3314
  config
3109
3315
  );
3110
3316
  const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
@@ -3120,6 +3326,8 @@ function createRuntime(options, handler) {
3120
3326
  heartbeat: void 0,
3121
3327
  agent: void 0,
3122
3328
  local: void 0,
3329
+ attached: void 0,
3330
+ runId: void 0,
3123
3331
  instanceId: resolveInstanceId(options.instanceId),
3124
3332
  environment,
3125
3333
  pollIntervalMs: void 0,
@@ -3197,6 +3405,20 @@ function launchLocal(rt) {
3197
3405
  rt.local = runner;
3198
3406
  void runner.run();
3199
3407
  }
3408
+ function launchAttached(rt, runId) {
3409
+ const runner = new AttachedRunner(runId, rt.handler, {
3410
+ api: rt.api,
3411
+ client: rt.client,
3412
+ logger: rt.logger,
3413
+ tracker: rt.tracker,
3414
+ instanceInfo: () => instanceInfo(rt)
3415
+ });
3416
+ rt.attached = runner;
3417
+ void runner.run();
3418
+ void runner.done.then(() => {
3419
+ if (runtime === rt) void close();
3420
+ });
3421
+ }
3200
3422
  async function boot(rt) {
3201
3423
  if (!rt.instanceId) {
3202
3424
  const id = await fallbackInstanceId();
@@ -3216,8 +3438,9 @@ async function boot(rt) {
3216
3438
  if (rt.options.installExitHandlers !== false) {
3217
3439
  rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
3218
3440
  }
3219
- if (rt.handler.mode === "remote") ensureAgent(rt).start();
3220
- else launchLocal(rt);
3441
+ if (rt.handler.mode === "local") launchLocal(rt);
3442
+ else if (rt.runId) launchAttached(rt, rt.runId);
3443
+ else ensureAgent(rt).start();
3221
3444
  }
3222
3445
  function scheduleBoot(rt) {
3223
3446
  if (rt.bootScheduled) return;
@@ -3235,6 +3458,7 @@ function startAgent(fn, mode, handler, options) {
3235
3458
  const agentHandler = { mode, handler, options: resolveRunOptions(options, mode, logger) };
3236
3459
  const rt = createRuntime(options, agentHandler);
3237
3460
  if (mode === "remote" && options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
3461
+ if (mode === "remote") rt.runId = resolveRunId(options.runId);
3238
3462
  runtime = rt;
3239
3463
  scheduleBoot(rt);
3240
3464
  }
@@ -3275,7 +3499,9 @@ function captureException(error, hint) {
3275
3499
  const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
3276
3500
  forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
3277
3501
  }
3278
- return rt.client.captureException(error, hint);
3502
+ const runId = currentReporter()?.runId;
3503
+ const withRun = runId ? { ...hint, tags: { run_id: runId, ...hint?.tags } } : hint;
3504
+ return rt.client.captureException(error, withRun);
3279
3505
  }
3280
3506
  async function close(timeoutMs = 3e4) {
3281
3507
  const rt = runtime;
@@ -3284,6 +3510,7 @@ async function close(timeoutMs = 3e4) {
3284
3510
  await rt.bootPromise;
3285
3511
  if (rt.agent) await rt.agent.stop(0);
3286
3512
  rt.local?.stop();
3513
+ rt.attached?.stop();
3287
3514
  const idle = await rt.tracker.waitIdle(timeoutMs);
3288
3515
  if (!idle) {
3289
3516
  rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
@@ -3401,6 +3628,7 @@ var image2 = hobots.image;
3401
3628
  var isFileRef2 = hobots.isFileRef;
3402
3629
 
3403
3630
  // src/index.ts
3631
+ loadDotenv();
3404
3632
  var src_default = hobots;
3405
3633
  export {
3406
3634
  captureException2 as captureException,