hobots 4.2.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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 ? "5.0.0" : "0.0.0-dev";
101
48
  function isNode() {
102
49
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
103
50
  }
@@ -150,8 +97,13 @@ function abortableSleep(ms, signal) {
150
97
  function jitter(baseMs) {
151
98
  return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
152
99
  }
100
+ var TRUNCATION_SUFFIX = "\u2026 [truncado]";
153
101
  function truncate(value, max) {
154
- return value.length > max ? `${value.slice(0, max)}\u2026 [truncado]` : value;
102
+ return value.length > max ? `${value.slice(0, max)}${TRUNCATION_SUFFIX}` : value;
103
+ }
104
+ function clip(value, max) {
105
+ if (value.length <= max) return value;
106
+ return `${value.slice(0, Math.max(0, max - TRUNCATION_SUFFIX.length))}${TRUNCATION_SUFFIX}`;
155
107
  }
156
108
  function safeString(value) {
157
109
  if (typeof value === "string") return value;
@@ -245,6 +197,124 @@ function colorize(text, color) {
245
197
  return `${ANSI[color]}${text}${ANSI.reset}`;
246
198
  }
247
199
 
200
+ // src/env.ts
201
+ var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
202
+ var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
203
+ var RUN_ID_ENV = "HOBOTS_RUN_ID";
204
+ var LOCAL_RUN_ENV = "HOBOTS_LOCAL_RUN";
205
+ var RUN_PARAMS_ENV = "HOBOTS_RUN_PARAMS";
206
+ var dotenvLoaded = false;
207
+ function loadDotenv() {
208
+ if (dotenvLoaded) return;
209
+ dotenvLoaded = true;
210
+ if (!isNode() || typeof process.loadEnvFile !== "function") return;
211
+ try {
212
+ process.loadEnvFile(".env");
213
+ } catch (err) {
214
+ const code = err?.code;
215
+ if (code === "ENOENT") return;
216
+ 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`);
217
+ }
218
+ }
219
+ function resolveInstanceId(option) {
220
+ const fromOption = option?.trim();
221
+ if (fromOption) return fromOption;
222
+ if (typeof process === "undefined" || !process.env) return void 0;
223
+ return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
224
+ }
225
+ function resolveRunId(option) {
226
+ const fromOption = option?.trim();
227
+ if (fromOption) return fromOption;
228
+ if (typeof process === "undefined" || !process.env) return void 0;
229
+ return process.env[RUN_ID_ENV]?.trim() || void 0;
230
+ }
231
+ function isLocalRun() {
232
+ if (typeof process === "undefined" || !process.env) return false;
233
+ return process.env[LOCAL_RUN_ENV]?.trim() === "1";
234
+ }
235
+ function resolveLocalRunParams() {
236
+ const raw = process.env[RUN_PARAMS_ENV]?.trim();
237
+ if (!raw) return {};
238
+ let parsed;
239
+ try {
240
+ parsed = JSON.parse(raw);
241
+ } catch {
242
+ throw new Error(`${RUN_PARAMS_ENV} inv\xE1lido: n\xE3o \xE9 JSON v\xE1lido`);
243
+ }
244
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
245
+ throw new Error(`${RUN_PARAMS_ENV} inv\xE1lido: esperado um objeto JSON`);
246
+ }
247
+ return parsed;
248
+ }
249
+ function resolveEnvironmentOption(option) {
250
+ const fromOption = option?.trim();
251
+ if (fromOption) return fromOption;
252
+ if (typeof process === "undefined" || !process.env) return void 0;
253
+ return process.env[ENVIRONMENT_ENV]?.trim() || void 0;
254
+ }
255
+ async function fallbackInstanceId() {
256
+ if (!isNode()) return "browser";
257
+ try {
258
+ const os = await import("os");
259
+ return os.hostname() || "unknown-host";
260
+ } catch {
261
+ return "unknown-host";
262
+ }
263
+ }
264
+
265
+ // src/stacktrace.ts
266
+ var MAX_FRAMES = 50;
267
+ var MAX_FILENAME_CHARS = 1024;
268
+ var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
269
+ var LOCATION_RE = /^(.*?):(\d+):(\d+)$/;
270
+ function isInApp(filename) {
271
+ if (!filename) return false;
272
+ if (filename.startsWith("node:")) return false;
273
+ if (filename.startsWith("internal/")) return false;
274
+ if (filename.includes("node_modules")) return false;
275
+ if (filename === "<anonymous>") return false;
276
+ return true;
277
+ }
278
+ function parseStacktrace(stack) {
279
+ if (!stack) return [];
280
+ const frames = [];
281
+ for (const line of stack.split("\n")) {
282
+ if (frames.length >= MAX_FRAMES) break;
283
+ const match = FRAME_RE.exec(line);
284
+ if (!match || !line.trimStart().startsWith("at ")) continue;
285
+ const [, fn, location] = match;
286
+ const loc = LOCATION_RE.exec(location ?? "");
287
+ const filename = loc ? loc[1] : location;
288
+ frames.push({
289
+ function: fn || "<anonymous>",
290
+ filename: filename ? filename.slice(0, MAX_FILENAME_CHARS) : void 0,
291
+ lineno: loc?.[2] ? Number(loc[2]) : void 0,
292
+ colno: loc?.[3] ? Number(loc[3]) : void 0,
293
+ in_app: isInApp(filename)
294
+ });
295
+ }
296
+ return frames;
297
+ }
298
+
299
+ // src/scrub.ts
300
+ var SECRET_PATTERNS = [
301
+ // client secret da Hobots
302
+ [/hb_[a-f0-9]{16,}/gi, "[REDACTED]"],
303
+ // tokens bearer em headers/mensagens
304
+ [/\b(authorization:?\s*)?bearer\s+[a-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]"],
305
+ // chaves no formato sk-… (OpenAI e afins)
306
+ [/\bsk-[a-z0-9_-]{16,}\b/gi, "[REDACTED]"],
307
+ // credenciais embutidas em URL (https://user:pass@host)
308
+ [/\/\/[^/\s:@]+:[^/\s@]+@/g, "//[REDACTED]@"]
309
+ ];
310
+ function scrubSecrets(text) {
311
+ let out = text;
312
+ for (const [pattern, replacement] of SECRET_PATTERNS) {
313
+ out = out.replace(pattern, replacement);
314
+ }
315
+ return out;
316
+ }
317
+
248
318
  // src/eventbuilder.ts
249
319
  var MAX_EXCEPTION_VALUE_CHARS = 8192;
250
320
  function eventFromException(error, type = "error") {
@@ -415,13 +485,26 @@ var MAX_FINGERPRINT_ITEMS = 10;
415
485
  var MAX_FINGERPRINT_ITEM_CHARS = 255;
416
486
  var MAX_TRANSACTION_NAME_CHARS = 500;
417
487
  var MAX_STEPS = 200;
488
+ var MAX_STEP_DESCRIPTION_CHARS = 2048;
489
+ var MAX_STEP_OP_CHARS = 255;
418
490
  function clipTags(tags) {
419
491
  return Object.fromEntries(Object.entries(tags).map(([key, value]) => [key, truncate(String(value), MAX_TAG_VALUE_CHARS)]));
420
492
  }
493
+ function clipStep(step) {
494
+ return {
495
+ ...step,
496
+ ...step.op ? { op: clip(step.op, MAX_STEP_OP_CHARS) } : {},
497
+ ...step.description ? { description: clip(step.description, MAX_STEP_DESCRIPTION_CHARS) } : {}
498
+ };
499
+ }
421
500
  function clipFingerprint(fingerprint) {
422
501
  if (fingerprint === void 0) return void 0;
423
502
  return fingerprint.slice(0, MAX_FINGERPRINT_ITEMS).map((item) => truncate(String(item), MAX_FINGERPRINT_ITEM_CHARS));
424
503
  }
504
+ var captured = /* @__PURE__ */ new WeakSet();
505
+ function wasCaptured(error) {
506
+ return typeof error === "object" && error !== null && captured.has(error);
507
+ }
425
508
  function runtimeContexts() {
426
509
  if (isNode()) {
427
510
  return {
@@ -472,11 +555,13 @@ var Client = class {
472
555
  [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
473
556
  return `Client { baseUrl: '${this.#config.baseUrl}' }`;
474
557
  }
475
- captureException(error, hint) {
476
- return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
558
+ /** `contexts` é interno ao SDK (ex.: `contexts.run` com o print do captureException) — o evento ainda pode sobrescrever. */
559
+ captureException(error, hint, contexts) {
560
+ if (typeof error === "object" && error !== null) captured.add(error);
561
+ return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint, contexts);
477
562
  }
478
- captureEvent(event, hint) {
479
- const final = this.prepareEvent(event, hint);
563
+ captureEvent(event, hint, contexts) {
564
+ const final = this.prepareEvent(contexts ? { ...event, contexts: { ...contexts, ...event.contexts } } : event, hint);
480
565
  this.transport.enqueue({ type: "event", payload: final });
481
566
  return final.event_id;
482
567
  }
@@ -499,14 +584,15 @@ var Client = class {
499
584
  sendTransaction(payload) {
500
585
  let steps = payload.steps;
501
586
  if (steps.length > MAX_STEPS) {
502
- this.logger.warn(`transaction com ${steps.length} steps \u2014 enviando as primeiras ${MAX_STEPS}`);
587
+ this.logger.warn(`transaction com ${steps.length} etapas \u2014 enviando as primeiras ${MAX_STEPS}`);
503
588
  steps = steps.slice(0, MAX_STEPS);
504
589
  }
590
+ steps = steps.map(clipStep);
505
591
  this.transport.enqueue({
506
592
  type: "transaction",
507
593
  payload: {
508
594
  ...payload,
509
- name: truncate(payload.name || "(sem nome)", MAX_TRANSACTION_NAME_CHARS),
595
+ name: clip(payload.name || "(sem nome)", MAX_TRANSACTION_NAME_CHARS),
510
596
  steps,
511
597
  release: payload.release ?? this.options.release,
512
598
  environment: this.resolveEnvironment(payload.environment),
@@ -580,12 +666,12 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
580
666
  const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
581
667
  if (!key) {
582
668
  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`
669
+ `${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
670
  );
585
671
  }
586
672
  if (!SECRET_PATTERN.test(key)) {
587
673
  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`
674
+ `${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
675
  );
590
676
  }
591
677
  const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
@@ -597,31 +683,6 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
597
683
  };
598
684
  }
599
685
 
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
686
  // src/heartbeat.ts
626
687
  var DEFAULT_INTERVAL_MS = WIRE_LIMITS.HEARTBEAT_DEFAULT_INTERVAL_MS;
627
688
  var MIN_REPORTED_INTERVAL_MS = WIRE_LIMITS.HEARTBEAT_MIN_INTERVAL_MS;
@@ -983,6 +1044,42 @@ var TasksApi = class {
983
1044
  created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
984
1045
  };
985
1046
  }
1047
+ /**
1048
+ * Anexa o processo a uma run já reivindicada para esta instância (env HOBOTS_RUN_ID):
1049
+ * POST /v1/runs/:id/start. Um request só — quem decide o retry é o AttachedRunner.
1050
+ * 404 (não existe) e 409 (cancelada, já iniciada ou de outra instância) viram
1051
+ * TasksApiError com o status.
1052
+ */
1053
+ async startRun(runId, request, signal) {
1054
+ const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
1055
+ let response;
1056
+ try {
1057
+ response = await fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/start`, {
1058
+ redirect: "error",
1059
+ method: "POST",
1060
+ headers: this.headers(),
1061
+ body: JSON.stringify(request),
1062
+ signal: timeoutSignal
1063
+ });
1064
+ } finally {
1065
+ cleanup();
1066
+ }
1067
+ if (!response.ok) {
1068
+ throw new TasksApiError(`in\xEDcio da run falhou (HTTP ${response.status})`, response.status);
1069
+ }
1070
+ const body = await response.json().catch(() => null);
1071
+ if (typeof body?.id !== "string") {
1072
+ throw new TasksApiError("resposta inv\xE1lida do in\xEDcio da run (id ausente)", 0);
1073
+ }
1074
+ const form = body.form;
1075
+ return {
1076
+ id: body.id,
1077
+ params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
1078
+ form: typeof form?.id === "string" && typeof form.name === "string" ? { id: form.id, name: form.name } : null,
1079
+ source: typeof body.source === "string" ? body.source : "hobots",
1080
+ created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
1081
+ };
1082
+ }
986
1083
  /** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
987
1084
  parsePollResponse(body, max) {
988
1085
  const runs = body?.runs;
@@ -1240,7 +1337,7 @@ var Backoff = class {
1240
1337
  this.authFailures++;
1241
1338
  if (this.authFailures === 1 || this.authFailures % 10 === 0) {
1242
1339
  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"
1340
+ 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
1341
  );
1245
1342
  }
1246
1343
  return Math.max(this.floorMs, AUTH_FAILURE_INTERVAL_MS);
@@ -1266,6 +1363,9 @@ var TERMINAL_ERROR_NAMES = /* @__PURE__ */ new Set(["RunCanceled", "AbortError",
1266
1363
  function isTerminalError(error) {
1267
1364
  return error instanceof Error && TERMINAL_ERROR_NAMES.has(error.name);
1268
1365
  }
1366
+ function isCancellation(error) {
1367
+ return error instanceof Error && (error.name === "RunCanceled" || error.name === "AbortError");
1368
+ }
1269
1369
 
1270
1370
  // src/tasks/each.ts
1271
1371
  function thrownMessage(error) {
@@ -1292,11 +1392,11 @@ var ItemHandle = class {
1292
1392
  }
1293
1393
  failed(message, payload, options) {
1294
1394
  this.record("failed", message, payload, options?.image);
1295
- if (this.step && this.step.status !== "error") this.step.setStatus("error", message);
1395
+ if (this.step && this.step.status !== "error") this.step._setStatus("error", message);
1296
1396
  }
1297
1397
  occurrence(message, payload, options) {
1298
1398
  this.record("occurrence", message, payload, options?.image);
1299
- if (this.step && this.step.status === "ok") this.step.setStatus("warning", message);
1399
+ if (this.step && this.step.status === "ok") this.step._setStatus("warning", message);
1300
1400
  }
1301
1401
  record(status, message, payload, image3) {
1302
1402
  this.reported = true;
@@ -1327,24 +1427,24 @@ async function eachItem(list, handler, options, sink) {
1327
1427
  const step = sink.startStep(options.op ?? "item", description);
1328
1428
  const handle = new ItemHandle(index, id, step, options.payload?.(element, index), sink);
1329
1429
  try {
1330
- await handler(element, handle, index);
1430
+ await sink.stepScope(step, async () => handler(element, handle, index));
1331
1431
  if (!handle.reported) handle.succeeded();
1332
1432
  } catch (error) {
1333
1433
  if (isTerminalError(error) || sink.isAborted()) {
1334
- step?.setStatus("cancelled");
1335
- step?.finish();
1434
+ step?._setStatus("cancelled")._close();
1336
1435
  throw error;
1337
1436
  }
1338
1437
  const message = thrownMessage(error);
1339
1438
  if (!handle.reported) handle.failed(message);
1340
- step?.setError(error);
1439
+ step?._failed(error);
1341
1440
  sink.logError(`item ${description} falhou: ${message}`);
1342
1441
  }
1343
- step?.finish();
1442
+ step?._close();
1344
1443
  if (options.progress !== false) sink.advance();
1345
1444
  result.total += 1;
1346
1445
  result[handle.status ?? "succeeded"] += 1;
1347
1446
  index += 1;
1447
+ if (options.flush !== false) await sink.flush();
1348
1448
  }
1349
1449
  return result;
1350
1450
  }
@@ -1384,6 +1484,11 @@ function isInsideDir(root, candidate, sep) {
1384
1484
  function unavailableLocal(ref) {
1385
1485
  return new Error(`arquivo "${ref.name}" enviado pelo Studio de outro computador ou j\xE1 consumido`);
1386
1486
  }
1487
+ function localRunPath(ref) {
1488
+ if (!isLocalRun()) return void 0;
1489
+ const path = ref.path;
1490
+ return typeof path === "string" && path.trim() !== "" ? path : void 0;
1491
+ }
1387
1492
  function errorCode(error) {
1388
1493
  return error?.code;
1389
1494
  }
@@ -1551,8 +1656,16 @@ var RunFileStore = class {
1551
1656
  this.deps.warn(`arquivo "${ref.name}" baixado, mas a confirma\xE7\xE3o ao servidor falhou: ${String(error)}`);
1552
1657
  }
1553
1658
  }
1554
- /** storage 'local': copia o único arquivo de `$HOBOTS_LOCAL_FILES_DIR/<id>/` e apaga a pasta de origem. */
1659
+ /**
1660
+ * storage 'local': copia o único arquivo de `$HOBOTS_LOCAL_FILES_DIR/<id>/` e apaga a pasta de
1661
+ * origem. No modo local, um FileRef com `path` copia direto desse caminho, sem apagar a origem.
1662
+ */
1555
1663
  async copyLocal(ref, target) {
1664
+ const direct = localRunPath(ref);
1665
+ if (direct !== void 0) {
1666
+ await this.copyFromPath(ref, direct, target);
1667
+ return;
1668
+ }
1556
1669
  const base = process.env[LOCAL_FILES_DIR_ENV];
1557
1670
  if (!base) throw unavailableLocal(ref);
1558
1671
  const { copyFile, readdir, rename, rm, stat } = await import("fs/promises");
@@ -1586,6 +1699,26 @@ var RunFileStore = class {
1586
1699
  }
1587
1700
  await rm(folder, { recursive: true, force: true });
1588
1701
  }
1702
+ /**
1703
+ * Modo local: cópia do arquivo apontado por `path` (teste roda várias vezes — a fonte fica).
1704
+ * Sem conferência de tamanho: o `size` do FileRef é informativo aqui.
1705
+ */
1706
+ async copyFromPath(ref, path, target) {
1707
+ const { copyFile, rename, rm, stat } = await import("fs/promises");
1708
+ const { resolve: resolve2 } = await import("path");
1709
+ const source = resolve2(path);
1710
+ const info = await stat(source).catch(() => void 0);
1711
+ if (!info?.isFile()) throw new Error(`arquivo "${ref.name}" n\xE3o encontrado em ${path}`);
1712
+ this.checkCanceled();
1713
+ const part = `${target}.part`;
1714
+ try {
1715
+ await copyFile(source, part);
1716
+ await rename(part, target);
1717
+ } catch (error) {
1718
+ await rm(part, { force: true });
1719
+ throw error;
1720
+ }
1721
+ }
1589
1722
  translate(error, ref) {
1590
1723
  if (this.deps.signal.aborted) return runCanceledError();
1591
1724
  if (error instanceof TasksApiError && error.status === 410) {
@@ -1714,6 +1847,11 @@ function printRunLog(level, message) {
1714
1847
  const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
1715
1848
  console[method2](line, message);
1716
1849
  }
1850
+ var PROGRESS_LINE_PREFIX = "[hobots:progress]";
1851
+ var PROGRESS_PRINT_THROTTLE_MS = 250;
1852
+ function printRunProgress(current, total) {
1853
+ console.log(`${PROGRESS_LINE_PREFIX} ${current}/${total}`);
1854
+ }
1717
1855
  function cancelableSleep(ms, signal) {
1718
1856
  return new Promise((resolve2, reject) => {
1719
1857
  if (signal.aborted) {
@@ -1782,14 +1920,16 @@ var RunReporter = class {
1782
1920
  tx.setStepGuard(checkCanceled);
1783
1921
  const eachSink = {
1784
1922
  report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
1785
- startStep: (op, description) => tx.startChild(op, description),
1923
+ startStep: (op, description) => tx.start(op, description),
1924
+ stepScope: (step, fn) => tx.scope(step, fn),
1786
1925
  declareTotal: (total) => {
1787
1926
  if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
1788
1927
  },
1789
1928
  advance: () => this.setProgress(this.progressCurrent + 1, this.progressTotal),
1790
1929
  checkCanceled,
1791
1930
  isAborted: () => this.abortController.signal.aborted,
1792
- logError: (message) => this.log("error", message)
1931
+ logError: (message) => this.log("error", message),
1932
+ flush: () => this.flush()
1793
1933
  };
1794
1934
  const items = {
1795
1935
  add: (item) => {
@@ -1844,7 +1984,14 @@ var RunReporter = class {
1844
1984
  items,
1845
1985
  progress,
1846
1986
  files: this.fileStore.files,
1847
- transaction: tx,
1987
+ step: async (name, fn, options) => {
1988
+ const root = tx.currentStep() === void 0;
1989
+ try {
1990
+ return await tx.step(name, fn, { ...options, signal, wait: sleep2, warn: (message) => this.log("warning", message) });
1991
+ } finally {
1992
+ if (options?.flush !== false && root && !signal.aborted) await this.flush();
1993
+ }
1994
+ },
1848
1995
  flush: (timeoutMs) => this.flush(timeoutMs),
1849
1996
  signal,
1850
1997
  throwIfCanceled: checkCanceled,
@@ -1875,8 +2022,17 @@ var RunReporter = class {
1875
2022
  itemSeq = 0;
1876
2023
  /** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
1877
2024
  itemsUnsupported = false;
1878
- /** Cap de itens por run atingido (client-side ou 429) — degrada com um aviso só. */
2025
+ /**
2026
+ * Cota de itens por run do servidor (429, legado — hoje a api não tem teto de itens) —
2027
+ * degrada com um aviso só.
2028
+ */
1879
2029
  itemsQuotaExceeded = false;
2030
+ /**
2031
+ * Armazenamento do plano cheio (402 `storage_limit`): o lote é descartado e o aviso sai
2032
+ * uma vez por episódio. Os envios continuam — num agente `start()` o espaço pode ser
2033
+ * liberado (ou o plano trocado) sem reiniciar; um lote aceito rearma o aviso.
2034
+ */
2035
+ itemsStorageFull = false;
1880
2036
  /**
1881
2037
  * Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
1882
2038
  * servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
@@ -1898,6 +2054,8 @@ var RunReporter = class {
1898
2054
  progressDirty = false;
1899
2055
  progressTimer;
1900
2056
  progressWarned = false;
2057
+ /** Relógio do último eco `[hobots:progress]` no terminal (throttle do eco, não do wire). */
2058
+ progressLastPrint = 0;
1901
2059
  /** Arquivos do formulário desta run (`ctx.files`) — a pasta temporária é apagada no fim. */
1902
2060
  fileStore;
1903
2061
  /** Apaga as cópias de arquivo que ficaram na pasta temporária da run. Nunca lança. */
@@ -1948,15 +2106,17 @@ var RunReporter = class {
1948
2106
  /**
1949
2107
  * Linha de log da run com eco no terminal. `image` (opcional) vira o anexo da linha:
1950
2108
  * o id entra na entrada ANTES de qualquer I/O — é isso que mantém a ordem dos logs
1951
- * intacta e o handler livre da latência do upload.
2109
+ * intacta e o handler livre da latência do upload. Devolve o id do anexo (quando houve
2110
+ * imagem e ela não foi descartada) — o `captureException` o leva ao evento da issue.
1952
2111
  */
1953
2112
  log(level, message, image3) {
1954
- if (this.dead) return;
2113
+ if (this.dead) return void 0;
1955
2114
  const scrubbed = scrubSecrets(safeString(message));
1956
2115
  const ts = nowISO();
1957
2116
  const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
1958
2117
  printRunLog(level, attachmentId ? `${scrubbed} [imagem]` : scrubbed);
1959
2118
  this.push(level, scrubbed, true, attachmentId ? [attachmentId] : void 0, ts);
2119
+ return attachmentId;
1960
2120
  }
1961
2121
  /** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
1962
2122
  push(level, message, alreadyScrubbed = false, attachments, ts = nowISO()) {
@@ -2002,7 +2162,7 @@ var RunReporter = class {
2002
2162
  }
2003
2163
  }
2004
2164
  /**
2005
- * `hobots.flush()`: o bot decide quando entregar o que já fez — steps raiz finalizadas
2165
+ * `hobots.flush()`: o bot decide quando entregar o que já fez — etapas raiz finalizadas
2006
2166
  * viram transactions parciais da run, e itens, logs e progresso pendentes saem já, sem
2007
2167
  * esperar o throttle. Aguarda a entrega com teto. Nunca lança (pode ir num `finally`).
2008
2168
  */
@@ -2060,10 +2220,16 @@ var RunReporter = class {
2060
2220
  }
2061
2221
  const safeCurrent = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : this.progressCurrent;
2062
2222
  const next = total == null ? safeCurrent : Math.min(safeCurrent, total);
2063
- if (next === this.progressCurrent && total === this.progressTotal) return;
2223
+ const totalChanged = total !== this.progressTotal;
2224
+ if (next === this.progressCurrent && !totalChanged) return;
2064
2225
  this.progressCurrent = next;
2065
2226
  this.progressTotal = total;
2066
2227
  if (total == null) return;
2228
+ const now = Date.now();
2229
+ if (totalChanged || next >= total || now - this.progressLastPrint >= PROGRESS_PRINT_THROTTLE_MS) {
2230
+ this.progressLastPrint = now;
2231
+ printRunProgress(next, total);
2232
+ }
2067
2233
  this.progressDirty = true;
2068
2234
  if (this.progressTimer) return;
2069
2235
  this.progressTimer = setTimeout(() => {
@@ -2186,6 +2352,7 @@ var RunReporter = class {
2186
2352
  if (this.itemsUnsupported) return;
2187
2353
  try {
2188
2354
  await this.api.createItems(this.runId, { items: batch });
2355
+ this.itemsStorageFull = false;
2189
2356
  } catch (error) {
2190
2357
  if (error instanceof TasksApiError && error.status === 404) {
2191
2358
  this.itemsUnsupported = true;
@@ -2199,6 +2366,13 @@ var RunReporter = class {
2199
2366
  }
2200
2367
  return;
2201
2368
  }
2369
+ if (error instanceof TasksApiError && error.status === 402) {
2370
+ if (!this.itemsStorageFull) {
2371
+ this.itemsStorageFull = true;
2372
+ this.log("warning", "itens descartados: armazenamento do plano cheio \u2014 libere espa\xE7o ou fa\xE7a upgrade em Plano e cobran\xE7a");
2373
+ }
2374
+ return;
2375
+ }
2202
2376
  if (error instanceof TasksApiError) {
2203
2377
  this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
2204
2378
  this.log("warning", `${batch.length} item(ns) descartado(s): envio rejeitado pelo servidor (HTTP ${error.status})`);
@@ -2230,7 +2404,7 @@ var RunReporter = class {
2230
2404
  }
2231
2405
  // ── Anexos ───────────────────────────────────────────────────────────────────
2232
2406
  /**
2233
- * Pendura uma imagem num evento da run (log, item ou step): faz as checagens que não
2407
+ * Pendura uma imagem num evento da run (log, item ou etapa): faz as checagens que não
2234
2408
  * dependem de I/O (reporter morto, instalação sem anexos, cota), reserva o id e dispara
2235
2409
  * o upload em segundo plano. Devolve o id — já referenciável pelo evento — ou undefined
2236
2410
  * quando a imagem foi descartada (sempre com aviso no log da run). Nunca lança: todo
@@ -2403,11 +2577,13 @@ var RunReporter = class {
2403
2577
 
2404
2578
  // src/tasks/run-context-store.ts
2405
2579
  var storage;
2580
+ var stepStorage;
2406
2581
  var initPromise;
2407
2582
  async function ensureStorage() {
2408
2583
  if (storage || !isNode()) return;
2409
2584
  initPromise ??= import("async_hooks").then(({ AsyncLocalStorage: ALS }) => {
2410
2585
  storage = new ALS();
2586
+ stepStorage = new ALS();
2411
2587
  });
2412
2588
  await initPromise;
2413
2589
  }
@@ -2418,12 +2594,23 @@ async function runWithRunContext(store, fn) {
2418
2594
  function currentRunReporter() {
2419
2595
  return storage?.getStore()?.reporter;
2420
2596
  }
2597
+ async function runWithStepScope(scope, fn) {
2598
+ await ensureStorage();
2599
+ return stepStorage ? stepStorage.run(scope, fn) : fn();
2600
+ }
2601
+ function currentStep(transaction) {
2602
+ const scope = stepStorage?.getStore();
2603
+ return scope?.transaction === transaction ? scope.step : void 0;
2604
+ }
2421
2605
 
2422
2606
  // src/transaction.ts
2423
2607
  var FLUSH_TRANSACTION_OP = "hobots.run.flush";
2608
+ var DEFAULT_STEP_RETRIES = 1;
2609
+ var DEFAULT_STEP_RETRY_DELAY_MS = 5e3;
2424
2610
  var MAX_STEP_MESSAGE_CHARS = 2e3;
2611
+ var MAX_BACKLOG = 100;
2425
2612
  function stepMessage(text) {
2426
- return truncate(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
2613
+ return clip(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
2427
2614
  }
2428
2615
  function stepId() {
2429
2616
  return uuid4().slice(0, 16);
@@ -2431,6 +2618,13 @@ function stepId() {
2431
2618
  function stepLabel(op, description) {
2432
2619
  return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
2433
2620
  }
2621
+ function errorText(error) {
2622
+ if (error instanceof Error) return error.message || error.name;
2623
+ return safeString(error);
2624
+ }
2625
+ function errorReason(error) {
2626
+ return error instanceof Error ? `${error.name}: ${errorText(error)}` : errorText(error);
2627
+ }
2434
2628
  function levelForStatus(status) {
2435
2629
  switch (status) {
2436
2630
  case "warning":
@@ -2470,58 +2664,68 @@ var Step = class {
2470
2664
  startTimestamp = nowISO();
2471
2665
  status = "ok";
2472
2666
  endTimestamp;
2473
- data;
2474
- /**
2475
- * Desfecho da step. `message` (opcional) é o motivo legível — vai em `data.message`
2476
- * e aparece na Central (performance e banner da execução) e no log de FINISH.
2477
- * `options.image` pendura um print do estado da tela em `data.attachments`.
2478
- */
2479
- setStatus(status, message, options) {
2480
- this.status = status;
2481
- if (message) this.setData("message", stepMessage(message));
2667
+ _data;
2668
+ // ── API do bot ────────────────────────────────────────────────────────────
2669
+ warning(message, options) {
2670
+ if (this.status !== "error") this._setStatus("warning", message);
2482
2671
  if (options?.image !== void 0) this.attachImage(options.image);
2483
2672
  return this;
2484
2673
  }
2674
+ image(image3) {
2675
+ this.attachImage(image3);
2676
+ return this;
2677
+ }
2678
+ data(key, value) {
2679
+ if (key === "attachments") return this;
2680
+ this._setData(key, value);
2681
+ return this;
2682
+ }
2683
+ // ── Internos (chamados pelo SDK) ──────────────────────────────────────────
2684
+ /** `data` da etapa como vai no wire (undefined quando vazio). */
2685
+ get payload() {
2686
+ return this._data;
2687
+ }
2688
+ _setStatus(status, message) {
2689
+ this.status = status;
2690
+ if (message) this._setData("message", stepMessage(safeString(message)));
2691
+ return this;
2692
+ }
2485
2693
  /**
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.
2488
- * `options.image` pendura um print do estado da tela em `data.attachments`.
2694
+ * Marca `error` guardando `{ type, message }` em `data.error` e fecha a etapa. Aceita qualquer
2695
+ * valor lançado; `message` sobrescreve a mensagem do erro. Numa etapa já fechada não faz nada.
2489
2696
  */
2490
- setError(error, message, options) {
2697
+ _failed(error, message) {
2698
+ if (this.endTimestamp) return this;
2491
2699
  this.status = "error";
2492
2700
  const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
2493
- const raw = message ?? (error instanceof Error ? error.message || error.name : String(error));
2494
- this.setData("error", { ...type ? { type } : {}, message: stepMessage(raw) });
2495
- if (options?.image !== void 0) this.attachImage(options.image);
2701
+ const raw = message ?? errorText(error);
2702
+ this._setData("error", { ...type ? { type } : {}, message: stepMessage(safeString(raw)) });
2703
+ this._close();
2496
2704
  return this;
2497
2705
  }
2706
+ _setData(key, value) {
2707
+ this._data = { ...this._data, [key]: value };
2708
+ }
2498
2709
  /**
2499
2710
  * Dispara o upload pela run (sink do RunReporter) e referencia o anexo em
2500
2711
  * `data.attachments` (chave reservada; teto igual ao das linhas de log). Fora de uma
2501
2712
  * run não há onde pendurar a imagem — descarta com aviso único.
2502
2713
  */
2503
2714
  attachImage(image3) {
2504
- const current = Array.isArray(this.data?.attachments) ? this.data.attachments : [];
2715
+ const current = Array.isArray(this._data?.attachments) ? this._data.attachments : [];
2505
2716
  if (current.length >= MAX_ATTACHMENTS_PER_LOG_ENTRY) return;
2506
2717
  const id = this.transaction.attachImage(image3);
2507
2718
  if (!id) return;
2508
- this.setData("attachments", [...current, id]);
2719
+ this._setData("attachments", [...current, id]);
2509
2720
  }
2510
- /** Motivo registrado via setStatus(…, message) ou setError — usado no log de FINISH. */
2721
+ /** Motivo registrado (warning/erro) — usado no log de FINISH. */
2511
2722
  reason() {
2512
- const error = this.data?.error;
2513
- const text = typeof error?.message === "string" ? error.message : this.data?.message;
2723
+ const error = this._data?.error;
2724
+ const text = typeof error?.message === "string" ? error.message : this._data?.message;
2514
2725
  return typeof text === "string" && text ? text : void 0;
2515
2726
  }
2516
- setData(key, value) {
2517
- this.data = { ...this.data, [key]: value };
2518
- return this;
2519
- }
2520
- /** Step aninhada (filha desta step). */
2521
- startChild(op, description) {
2522
- return this.transaction.createStep(this.stepId, op, description);
2523
- }
2524
- finish() {
2727
+ /** Fecha a etapa mantendo o status que estiver. Idempotente. */
2728
+ _close() {
2525
2729
  if (this.endTimestamp) return;
2526
2730
  this.endTimestamp = nowISO();
2527
2731
  this.transaction.emit(
@@ -2537,22 +2741,23 @@ var Step = class {
2537
2741
  toJSON() {
2538
2742
  return {
2539
2743
  step_id: this.stepId,
2540
- parent_step_id: this.parentStepId,
2541
- op: this.op,
2542
- description: this.description,
2744
+ ...this.parentStepId ? { parent_step_id: this.parentStepId } : {},
2745
+ ...this.op ? { op: clip(this.op, MAX_STEP_OP_CHARS) } : {},
2746
+ ...this.description ? { description: clip(this.description, MAX_STEP_DESCRIPTION_CHARS) } : {},
2543
2747
  status: this.status,
2544
2748
  start_timestamp: this.startTimestamp,
2545
2749
  timestamp: this.endTimestamp ?? nowISO(),
2546
- data: this.data
2750
+ ...this._data ? { data: this._data } : {}
2547
2751
  };
2548
2752
  }
2549
2753
  };
2550
2754
  var Transaction = class {
2551
- constructor(client, name, op, runId) {
2755
+ constructor(client, name, op, runId, options = {}) {
2552
2756
  this.client = client;
2553
2757
  this.name = name;
2554
2758
  this.op = op;
2555
2759
  this.runId = runId;
2760
+ this.echo = options.echo ?? false;
2556
2761
  this.emit("transaction", "start", op ? `${name} (${op})` : name);
2557
2762
  }
2558
2763
  client;
@@ -2564,33 +2769,36 @@ var Transaction = class {
2564
2769
  startTimestamp = nowISO();
2565
2770
  status = "ok";
2566
2771
  steps = [];
2567
- tags = {};
2568
2772
  finished = false;
2569
2773
  lifecycleSink;
2570
2774
  attachmentSink;
2571
2775
  warnedNoAttachmentSink = false;
2572
- // instalado pelo RunReporter: abrir uma step é um ponto de parada cooperativo (lança RunCanceled)
2776
+ // instalado pelo RunReporter: abrir uma etapa é um ponto de parada cooperativo (lança RunCanceled)
2573
2777
  stepGuard;
2778
+ // modo local (sem client): o FINISH de cada etapa sai no terminal, como `[hobots:item]`
2779
+ echo;
2574
2780
  // eventos emitidos antes do sink existir (o START da transaction sai no construtor,
2575
2781
  // antes do RunReporter conectar) — replay no setLifecycleSink
2576
2782
  lifecycleBacklog = [];
2783
+ // ── Lifecycle / eco ───────────────────────────────────────────────────────
2577
2784
  /**
2578
- * Registra o início/fim de uma step ou transaction: espelha a linha no sink de
2579
- * lifecycle, se conectado (é assim que os eventos de performance aparecem nos
2580
- * logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
2581
- * escreve no stdout do processo do usuário sem permissão.
2785
+ * Registra o início/fim de uma etapa ou da transaction: espelha a linha no sink de
2786
+ * lifecycle, se conectado (é assim que os eventos de performance aparecem nos logs da
2787
+ * run no app), e ecoa no terminal com `debug: true` — ou, no modo local, só o FINISH das
2788
+ * etapas (`[hobots:step] FINISH …`), para o Studio e o dev verem.
2582
2789
  */
2583
2790
  emit(kind, phase, label, startTs, endTs, status, reason) {
2584
2791
  const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
2585
2792
  const suffix = (durationMs != null ? ` (${durationMs}ms)` : "") + (reason ? `: ${reason}` : "");
2586
- if (this.client?.debug) {
2793
+ const localFinish = this.echo && kind === "step" && phase === "finish";
2794
+ if (this.client?.debug || localFinish) {
2587
2795
  const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
2588
2796
  console.log(`${tag} ${label}${suffix}`);
2589
2797
  }
2590
2798
  const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
2591
2799
  const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
2592
2800
  if (this.lifecycleSink) this.lifecycleSink(level, message);
2593
- else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
2801
+ else if (this.lifecycleBacklog.length < MAX_BACKLOG) this.lifecycleBacklog.push([level, message]);
2594
2802
  }
2595
2803
  /** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
2596
2804
  setLifecycleSink(sink) {
@@ -2599,59 +2807,111 @@ var Transaction = class {
2599
2807
  this.lifecycleBacklog = [];
2600
2808
  for (const [level, message] of backlog) sink(level, message);
2601
2809
  }
2602
- /** Conecta o pipeline de anexos da run — é o que dá imagem às steps. */
2810
+ /** Conecta o pipeline de anexos da run — é o que dá imagem às etapas. */
2603
2811
  setAttachmentSink(sink) {
2604
2812
  this.attachmentSink = sink;
2605
2813
  }
2606
- /** Imagem de uma step → id do anexo pela run; sem run, descarta com aviso único. */
2814
+ /** Imagem de uma etapa → id do anexo pela run; sem run, descarta com aviso único. */
2607
2815
  attachImage(image3) {
2608
2816
  if (this.attachmentSink) return this.attachmentSink(image3, nowISO());
2609
2817
  if (!this.warnedNoAttachmentSink) {
2610
2818
  this.warnedNoAttachmentSink = true;
2611
- console.warn("[hobots] imagem de step descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o");
2819
+ console.warn("[hobots] imagem de etapa descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o");
2612
2820
  }
2613
2821
  return void 0;
2614
2822
  }
2823
+ // ── Etapas ────────────────────────────────────────────────────────────────
2615
2824
  /**
2616
- * Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
2617
- * `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
2825
+ * Conecta a checagem de cancelamento chamada antes de cada etapa nova (inclusive antes de
2826
+ * cada nova tentativa) — é assim que `hobots.step()` para uma run cancelada.
2618
2827
  */
2619
2828
  setStepGuard(guard) {
2620
2829
  this.stepGuard = guard;
2621
2830
  }
2622
- createStep(parentStepId, op, description) {
2831
+ /** A etapa aberta por `step()` (ou `items.each`) no contexto async atual, se houver. */
2832
+ currentStep() {
2833
+ return currentStep(this);
2834
+ }
2835
+ /** Faz de `step` a etapa corrente durante `fn` (filhas aninham nela). Sem etapa, só roda `fn`. */
2836
+ scope(step, fn) {
2837
+ if (!step) return fn();
2838
+ return runWithStepScope({ transaction: this, step }, fn);
2839
+ }
2840
+ /** Abre uma etapa sob a corrente (ou direta da transaction). Interno: o bot usa `step()`. */
2841
+ start(op, description) {
2623
2842
  this.stepGuard?.();
2624
- const step = new Step(this, parentStepId, op, description);
2843
+ const parent = this.currentStep();
2844
+ const step = new Step(this, parent ? parent.stepId : this.stepId, op, description);
2625
2845
  this.steps.push(step);
2626
2846
  this.emit("step", "start", stepLabel(op, description));
2627
2847
  return step;
2628
2848
  }
2629
- /** Step direta da transaction. */
2630
- startChild(op, description) {
2631
- return this.createStep(this.stepId, op, description);
2849
+ /**
2850
+ * Registra a etapa `name` executando `fn(etapa)` até dar certo ou esgotar `retries`
2851
+ * tentativas extras (default 1), esperando `retryDelayMs` entre elas (default 5 000).
2852
+ * Resolve com o retorno de `fn`.
2853
+ *
2854
+ * Uma etapa só no waterfall, cobrindo todas as tentativas: `data.attempts` quando houve
2855
+ * mais de uma; `warning` com o motivo quando a última deu certo; `error` (e a exceção
2856
+ * sobe) quando esgotou; `cancelled` em cancelamento/timeout, que nunca repetem.
2857
+ */
2858
+ async step(name, fn, options = {}) {
2859
+ const requested = options.retries ?? DEFAULT_STEP_RETRIES;
2860
+ const totalRetries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_STEP_RETRIES;
2861
+ const step = this.start(void 0, safeString(name));
2862
+ let attempts = 0;
2863
+ let lastFailure;
2864
+ try {
2865
+ const result = await retryWithBackoff(
2866
+ (attempt) => {
2867
+ attempts = attempt;
2868
+ return this.scope(step, async () => fn(step));
2869
+ },
2870
+ {
2871
+ retries: totalRetries,
2872
+ delayMs: (attempt) => resolveDelay(options.retryDelayMs, attempt, DEFAULT_STEP_RETRY_DELAY_MS),
2873
+ signal: options.signal,
2874
+ wait: options.wait,
2875
+ onRetry: (error, attempt, delayMs) => {
2876
+ lastFailure = { error };
2877
+ options.warn?.(
2878
+ `etapa "${step.description}": tentativa ${attempt}/${totalRetries + 1} falhou: ${truncate(errorReason(error), 500)} \u2014 nova tentativa em ${delayMs}ms`
2879
+ );
2880
+ }
2881
+ }
2882
+ );
2883
+ this.noteAttempts(step, attempts);
2884
+ if (lastFailure && step.status === "ok") {
2885
+ step._setStatus("warning", `tentativa ${attempts - 1}/${totalRetries + 1} falhou: ${errorReason(lastFailure.error)}`);
2886
+ }
2887
+ step._close();
2888
+ return result;
2889
+ } catch (error) {
2890
+ this.noteAttempts(step, attempts);
2891
+ if (isTerminalError(error)) step._setStatus("cancelled")._close();
2892
+ else step._failed(error);
2893
+ throw error;
2894
+ }
2632
2895
  }
2633
- setTag(key, value) {
2634
- this.tags[key] = value;
2635
- return this;
2896
+ noteAttempts(step, attempts) {
2897
+ if (attempts > 1) step._setData("attempts", attempts);
2636
2898
  }
2899
+ // ── Metadados ─────────────────────────────────────────────────────────────
2637
2900
  setStatus(status) {
2638
2901
  this.status = status;
2639
2902
  return this;
2640
2903
  }
2641
- setName(name) {
2642
- this.name = name;
2643
- return this;
2644
- }
2904
+ // ── Encerramento ──────────────────────────────────────────────────────────
2645
2905
  /**
2646
- * Envia já as steps raiz finalizadas (e todas as descendentes, fechando as que
2906
+ * Envia já as etapas raiz finalizadas (e todas as descendentes, fechando as que
2647
2907
  * ficaram abertas) como transactions parciais da mesma run (`op` hobots.run.flush,
2648
- * `event_id` novo, até 200 steps cada) e as tira da memória. Steps raiz ainda abertas
2908
+ * `event_id` novo, até 200 etapas cada) e as tira da memória. Etapas raiz ainda abertas
2649
2909
  * ficam para o próximo flush ou para o `finish()`. É assim que um agente sempre
2650
2910
  * ligado (`start()`), cuja run não termina, entrega as etapas enquanto processa.
2651
- * Devolve quantas steps foram enviadas.
2911
+ * Devolve quantas etapas foram enviadas (0 sem client, sem nada terminado ou depois do `finish()`).
2652
2912
  */
2653
2913
  flush() {
2654
- if (this.finished) return 0;
2914
+ if (!this.client || this.finished) return 0;
2655
2915
  const ready = /* @__PURE__ */ new Set();
2656
2916
  for (const step of this.steps) {
2657
2917
  if (step.parentStepId === this.stepId && step.endTimestamp) ready.add(step.stepId);
@@ -2667,15 +2927,13 @@ var Transaction = class {
2667
2927
  remaining.push(step);
2668
2928
  }
2669
2929
  }
2670
- for (const step of batch) step.finish();
2930
+ for (const step of batch) step._close();
2671
2931
  this.steps.splice(0, this.steps.length, ...remaining);
2672
- if (this.client) {
2673
- for (let i = 0; i < batch.length; i += MAX_STEPS) {
2674
- const chunk = batch.slice(i, i + MAX_STEPS);
2675
- const startTimestamp = chunk.reduce((min, step) => step.startTimestamp < min ? step.startTimestamp : min, chunk[0].startTimestamp);
2676
- const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
2677
- this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, startTimestamp, nowISO(), chunk));
2678
- }
2932
+ for (let i = 0; i < batch.length; i += MAX_STEPS) {
2933
+ const chunk = batch.slice(i, i + MAX_STEPS);
2934
+ const startTimestamp = chunk.reduce((min, step) => step.startTimestamp < min ? step.startTimestamp : min, chunk[0].startTimestamp);
2935
+ const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
2936
+ this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, startTimestamp, nowISO(), chunk));
2679
2937
  }
2680
2938
  return batch.length;
2681
2939
  }
@@ -2692,14 +2950,15 @@ var Transaction = class {
2692
2950
  start_timestamp: startTimestamp,
2693
2951
  timestamp,
2694
2952
  steps: steps.map((step) => step.toJSON()),
2695
- tags: Object.keys(this.tags).length > 0 ? { ...this.tags } : void 0,
2953
+ // sem tags próprias: o client mescla as do processo
2954
+ tags: void 0,
2696
2955
  run_id: this.runId
2697
2956
  };
2698
2957
  }
2699
2958
  finish() {
2700
2959
  if (this.finished) return;
2701
2960
  this.finished = true;
2702
- for (const step of this.steps) step.finish();
2961
+ for (const step of this.steps) step._close();
2703
2962
  const endTimestamp = nowISO();
2704
2963
  this.emit(
2705
2964
  "transaction",
@@ -2727,7 +2986,7 @@ var DEFAULT_RETRY_DELAY_MS = 5e3;
2727
2986
  async function executeRun(input) {
2728
2987
  const { run, handler, api, client, logger, tracker } = input;
2729
2988
  const options = handler.options;
2730
- const tx = new Transaction(client, "run", "hobots.run", run.id);
2989
+ const tx = new Transaction(client, "run", "hobots.run", run.id, { echo: !client });
2731
2990
  const reporter = new RunReporter(
2732
2991
  api,
2733
2992
  run.id,
@@ -2761,6 +3020,7 @@ async function executeRun(input) {
2761
3020
  await reporter.finalizeItems();
2762
3021
  tx.setStatus("error");
2763
3022
  tx.finish();
3023
+ captureRunFailure(client, error, reporter);
2764
3024
  reporter.reportFailure(toRunError(error));
2765
3025
  logger.debug(`run ${run.id} falhou: ${String(error)}`);
2766
3026
  }
@@ -2770,6 +3030,11 @@ async function executeRun(input) {
2770
3030
  tracker.remove(reporter);
2771
3031
  }
2772
3032
  }
3033
+ function captureRunFailure(client, error, reporter) {
3034
+ if (!client?.options.captureRunErrors) return;
3035
+ if (reporter.isDead || isCancellation(error) || wasCaptured(error)) return;
3036
+ client.captureException(error, { type: "error", tags: { mechanism: "run_handler", run_id: reporter.runId } });
3037
+ }
2773
3038
  function warnIncompleteProgress(reporter) {
2774
3039
  const snapshot = reporter.context.progress.snapshot;
2775
3040
  if (!snapshot || snapshot.current >= snapshot.total) return;
@@ -2961,6 +3226,96 @@ var TaskAgent = class {
2961
3226
  }
2962
3227
  };
2963
3228
 
3229
+ // src/tasks/single-run.ts
3230
+ var SingleRunRunner = class {
3231
+ constructor(handler, deps) {
3232
+ this.handler = handler;
3233
+ this.deps = deps;
3234
+ this.done = new Promise((resolve2) => {
3235
+ this.resolveDone = resolve2;
3236
+ });
3237
+ }
3238
+ handler;
3239
+ deps;
3240
+ abort = new AbortController();
3241
+ /** Resolve quando a run terminou (ou não pôde começar, ou o runner foi parado antes). */
3242
+ done;
3243
+ resolveDone;
3244
+ async run() {
3245
+ try {
3246
+ const run = await this.openRun();
3247
+ if (!run) return;
3248
+ this.deps.logger.debug(this.opened(run));
3249
+ await executeRun({
3250
+ run,
3251
+ handler: this.handler,
3252
+ api: this.deps.api,
3253
+ client: this.deps.client,
3254
+ logger: this.deps.logger,
3255
+ tracker: this.deps.tracker
3256
+ });
3257
+ } catch (error) {
3258
+ this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
3259
+ } finally {
3260
+ this.resolveDone();
3261
+ }
3262
+ }
3263
+ /** Desiste de obter a run (close() antes dela existir). Uma run já em execução segue pelo tracker. */
3264
+ stop() {
3265
+ this.abort.abort();
3266
+ }
3267
+ };
3268
+
3269
+ // src/tasks/attached.ts
3270
+ var START_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
3271
+ function isTransient(error) {
3272
+ if (!(error instanceof TasksApiError)) return true;
3273
+ return error.status >= 500 || error.status === 429;
3274
+ }
3275
+ function describeRejection(runId, error) {
3276
+ if (error instanceof TasksApiError && error.status === 404) {
3277
+ return `run ${runId} n\xE3o existe na plataforma (404) \u2014 nada a executar`;
3278
+ }
3279
+ if (error instanceof TasksApiError && error.status === 409) {
3280
+ return `run ${runId} n\xE3o pode ser iniciada (409: cancelada, j\xE1 iniciada ou de outra inst\xE2ncia) \u2014 nada a executar`;
3281
+ }
3282
+ return `n\xE3o foi poss\xEDvel iniciar a run ${runId} (${String(error)}) \u2014 nada a executar`;
3283
+ }
3284
+ var AttachedRunner = class extends SingleRunRunner {
3285
+ constructor(runId, handler, deps) {
3286
+ super(handler, deps);
3287
+ this.runId = runId;
3288
+ }
3289
+ runId;
3290
+ opened(run) {
3291
+ return `run ${run.id} iniciada (HOBOTS_RUN_ID)`;
3292
+ }
3293
+ async openRun() {
3294
+ for (let attempt = 0; ; attempt++) {
3295
+ if (this.abort.signal.aborted) return void 0;
3296
+ try {
3297
+ return await this.deps.api.startRun(this.runId, { instance: this.deps.instanceInfo() }, this.abort.signal);
3298
+ } catch (error) {
3299
+ if (this.abort.signal.aborted) return void 0;
3300
+ if (!isTransient(error)) {
3301
+ this.deps.logger.error(describeRejection(this.runId, error));
3302
+ return void 0;
3303
+ }
3304
+ const delay = START_RETRY_DELAYS_MS[attempt];
3305
+ if (delay === void 0) {
3306
+ this.deps.logger.error(
3307
+ `n\xE3o foi poss\xEDvel iniciar a run ${this.runId} ap\xF3s ${attempt + 1} tentativas (${String(error)}) \u2014 desistindo`
3308
+ );
3309
+ return void 0;
3310
+ }
3311
+ const wait = jitter(delay);
3312
+ this.deps.logger.warn(`in\xEDcio da run ${this.runId} falhou (${String(error)}), pr\xF3xima tentativa em ${wait}ms`);
3313
+ await abortableSleep(wait, this.abort.signal);
3314
+ }
3315
+ }
3316
+ }
3317
+ };
3318
+
2964
3319
  // src/tasks/item-columns.ts
2965
3320
  var KEY_RE = /^[a-zA-Z0-9_.-]{1,100}$/;
2966
3321
  var MAX_COLUMNS = WIRE_LIMITS.MAX_ITEM_COLUMNS;
@@ -2998,46 +3353,16 @@ function normalizeItemColumns(input, logger) {
2998
3353
  }
2999
3354
 
3000
3355
  // src/tasks/local.ts
3001
- var LocalRunner = class {
3356
+ var LocalRunner = class extends SingleRunRunner {
3357
+ backoff;
3002
3358
  constructor(handler, deps) {
3003
- this.handler = handler;
3004
- this.deps = deps;
3359
+ super(handler, deps);
3005
3360
  this.backoff = new Backoff(deps.logger, "cria\xE7\xE3o da run");
3006
- this.done = new Promise((resolve2) => {
3007
- this.resolveDone = resolve2;
3008
- });
3009
- }
3010
- handler;
3011
- deps;
3012
- abort = new AbortController();
3013
- backoff;
3014
- /** Resolve quando a run terminou (ou o runner foi parado antes de abri-la). */
3015
- done;
3016
- resolveDone;
3017
- async run() {
3018
- try {
3019
- const run = await this.createRun();
3020
- if (!run) return;
3021
- this.deps.logger.debug(`run ${run.id} aberta (modo start)`);
3022
- await executeRun({
3023
- run,
3024
- handler: this.handler,
3025
- api: this.deps.api,
3026
- client: this.deps.client,
3027
- logger: this.deps.logger,
3028
- tracker: this.deps.tracker
3029
- });
3030
- } catch (error) {
3031
- this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
3032
- } finally {
3033
- this.resolveDone();
3034
- }
3035
3361
  }
3036
- /** Desiste de abrir a run (close() antes da plataforma responder). Uma run já aberta segue pelo tracker. */
3037
- stop() {
3038
- this.abort.abort();
3362
+ opened(run) {
3363
+ return `run ${run.id} aberta (modo start)`;
3039
3364
  }
3040
- async createRun() {
3365
+ async openRun() {
3041
3366
  for (; ; ) {
3042
3367
  if (this.abort.signal.aborted) return void 0;
3043
3368
  try {
@@ -3050,6 +3375,129 @@ var LocalRunner = class {
3050
3375
  }
3051
3376
  };
3052
3377
 
3378
+ // src/tasks/local-transport.ts
3379
+ var LOCAL_RUN_ID = "local";
3380
+ var ITEM_LINE_PREFIX = "[hobots:item]";
3381
+ var SUMMARY_LINE_PREFIX = "[hobots:summary]";
3382
+ var ATTACHMENTS_DISCARDED_WARNING = "imagens e anexos n\xE3o s\xE3o enviados no modo local (descartados)";
3383
+ function unavailable(name) {
3384
+ return new Error(`arquivo "${name}" enviado pela Central n\xE3o est\xE1 dispon\xEDvel no modo local`);
3385
+ }
3386
+ function cell(value) {
3387
+ return (value ?? "").replace(/\r\n|[\t\r\n]/g, " ");
3388
+ }
3389
+ var LocalRunTransport = class {
3390
+ constructor(params = {}, out = (line) => void process.stdout.write(`${line}
3391
+ `)) {
3392
+ this.params = params;
3393
+ this.out = out;
3394
+ }
3395
+ params;
3396
+ out;
3397
+ counts = { succeeded: 0, occurrence: 0, failed: 0 };
3398
+ warnedAttachments = false;
3399
+ /** Status terminal que o pipeline reportou — `undefined` enquanto a run corre (ou se ela nem reportou). */
3400
+ terminalStatus;
3401
+ async updateRun(_runId, update) {
3402
+ if (update.status === "running") return;
3403
+ this.terminalStatus = update.status;
3404
+ if (update.status === "failed" && update.error && update.error.type !== "RunCanceled") {
3405
+ printRunLog("error", `execu\xE7\xE3o falhou \u2014 ${update.error.type ?? "Error"}: ${update.error.message}`);
3406
+ if (update.error.stack) console.error(update.error.stack);
3407
+ }
3408
+ }
3409
+ async updateRunOnce() {
3410
+ }
3411
+ async createItems(_runId, request) {
3412
+ for (const item of request.items) {
3413
+ this.counts[item.status]++;
3414
+ this.out(`${ITEM_LINE_PREFIX} ${item.status} ${cell(item.id)} ${cell(item.message)}`);
3415
+ }
3416
+ return { accepted: request.items.length, duplicates: 0 };
3417
+ }
3418
+ /** Totais por status — chame uma vez, no fim da run, antes da linha de conclusão. */
3419
+ printSummary() {
3420
+ const { succeeded, occurrence, failed } = this.counts;
3421
+ this.out(
3422
+ `${SUMMARY_LINE_PREFIX} total=${succeeded + occurrence + failed} ok=${succeeded} avisos=${occurrence} falhas=${failed} pulados=0`
3423
+ );
3424
+ }
3425
+ async createAttachment(_runId, request) {
3426
+ if (!this.warnedAttachments) {
3427
+ this.warnedAttachments = true;
3428
+ printRunLog("warning", ATTACHMENTS_DISCARDED_WARNING);
3429
+ }
3430
+ return { id: request.id, upload_url: "", headers: {}, expires_in: 0 };
3431
+ }
3432
+ async uploadAttachment() {
3433
+ }
3434
+ async confirmAttachment() {
3435
+ }
3436
+ /** Nome do arquivo pelo id (nos params); sem ele, o próprio id. */
3437
+ fileName(fileId) {
3438
+ return collectFileRefs(this.params).find((ref) => ref.id === fileId)?.name ?? fileId;
3439
+ }
3440
+ async requestFileDownload(_runId, fileId) {
3441
+ throw unavailable(this.fileName(fileId));
3442
+ }
3443
+ async fetchFile(url) {
3444
+ throw unavailable(url);
3445
+ }
3446
+ async confirmFileDownload(_runId, fileId) {
3447
+ throw unavailable(this.fileName(fileId));
3448
+ }
3449
+ };
3450
+
3451
+ // src/tasks/local-run.ts
3452
+ var LOCAL_RUN_EXIT_CODES = { succeeded: 0, failed: 1, canceled: 130 };
3453
+ var LOCAL_RUN_CANCEL_GRACE_MS = 4e3;
3454
+ var LOCAL_RUN_BANNER = "modo local \u2014 execu\xE7\xE3o sem a Central; params de HOBOTS_RUN_PARAMS";
3455
+ var LocalRunRunner = class extends SingleRunRunner {
3456
+ constructor(params, handler, deps) {
3457
+ super(handler, deps);
3458
+ this.params = params;
3459
+ }
3460
+ params;
3461
+ canceled = false;
3462
+ startedAt = Date.now();
3463
+ finished = false;
3464
+ run() {
3465
+ this.startedAt = Date.now();
3466
+ return super.run();
3467
+ }
3468
+ opened() {
3469
+ return "run local iniciada (HOBOTS_LOCAL_RUN)";
3470
+ }
3471
+ async openRun() {
3472
+ if (this.abort.signal.aborted) return void 0;
3473
+ return { id: LOCAL_RUN_ID, params: this.params, form: null, source: "local", created_at: nowISO() };
3474
+ }
3475
+ /** SIGINT/SIGTERM: dispara o `ctx.signal` da run em voo — o handler para no próximo ponto de parada. */
3476
+ cancel() {
3477
+ this.canceled = true;
3478
+ this.deps.tracker.singleActive()?.abort();
3479
+ }
3480
+ /** Desfecho da run: cancelada pelo sinal, senão o status terminal reportado (sem reporte = falha). */
3481
+ get outcome() {
3482
+ if (this.canceled) return "canceled";
3483
+ return this.deps.api.terminalStatus ?? "failed";
3484
+ }
3485
+ /**
3486
+ * Fecha o relato no terminal — `[hobots:summary]` e a linha de conclusão — e devolve o
3487
+ * desfecho. Idempotente: o runtime chama ao fim da run ou quando a graça do cancelamento
3488
+ * esgota com o handler ainda rodando.
3489
+ */
3490
+ finish() {
3491
+ const outcome = this.outcome;
3492
+ if (this.finished) return outcome;
3493
+ this.finished = true;
3494
+ this.deps.api.printSummary();
3495
+ const seconds = Math.round((Date.now() - this.startedAt) / 1e3);
3496
+ printRunLog("info", `execu\xE7\xE3o local conclu\xEDda (${outcome}) em ${seconds}s`);
3497
+ return outcome;
3498
+ }
3499
+ };
3500
+
3053
3501
  // src/tasks/registry.ts
3054
3502
  var RunTracker = class {
3055
3503
  constructor(api) {
@@ -3165,19 +3613,10 @@ function hostname() {
3165
3613
  if (!isNode()) return void 0;
3166
3614
  return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
3167
3615
  }
3168
- function createRuntime(options, handler) {
3169
- const config = resolveConfig({ clientSecret: options.clientSecret });
3170
- const logger = makeLogger(options.debug ?? false);
3171
- const environment = resolveEnvironmentOption(options.environment);
3172
- const client = new Client(
3173
- { environment, release: options.release, tags: options.tags, debug: options.debug },
3174
- config
3175
- );
3176
- const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
3177
- const rt = {
3616
+ function baseRuntime(options, handler, logger, api) {
3617
+ return {
3178
3618
  options: { ...options, clientSecret: void 0 },
3179
- config,
3180
- client,
3619
+ client: void 0,
3181
3620
  logger,
3182
3621
  api,
3183
3622
  handler,
@@ -3186,18 +3625,55 @@ function createRuntime(options, handler) {
3186
3625
  heartbeat: void 0,
3187
3626
  agent: void 0,
3188
3627
  local: void 0,
3628
+ attached: void 0,
3629
+ localRun: void 0,
3630
+ runId: void 0,
3189
3631
  instanceId: resolveInstanceId(options.instanceId),
3190
- environment,
3632
+ environment: void 0,
3191
3633
  pollIntervalMs: void 0,
3192
3634
  booted: false,
3193
3635
  bootScheduled: false,
3194
3636
  bootPromise: void 0,
3195
- uninstallGlobalHandlers: options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0,
3637
+ uninstallGlobalHandlers: void 0,
3196
3638
  uninstallShutdown: void 0
3197
3639
  };
3640
+ }
3641
+ function createRuntime(options, handler, logger) {
3642
+ const config = resolveConfig({ clientSecret: options.clientSecret });
3643
+ const environment = resolveEnvironmentOption(options.environment);
3644
+ const client = new Client(
3645
+ {
3646
+ environment,
3647
+ release: options.release,
3648
+ tags: options.tags,
3649
+ debug: options.debug,
3650
+ captureRunErrors: options.captureUnhandled !== false
3651
+ },
3652
+ config
3653
+ );
3654
+ const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
3655
+ const rt = baseRuntime(options, handler, logger, api);
3656
+ rt.client = client;
3657
+ rt.environment = environment;
3658
+ rt.uninstallGlobalHandlers = options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0;
3198
3659
  logger.debug("inicializado", { endpoint: config.ingestEndpoint });
3199
3660
  return rt;
3200
3661
  }
3662
+ function createLocalRunRuntime(options, handler, logger) {
3663
+ let params;
3664
+ try {
3665
+ params = resolveLocalRunParams();
3666
+ } catch (error) {
3667
+ printRunLog("error", error instanceof Error ? error.message : String(error));
3668
+ process.exit(2);
3669
+ throw error;
3670
+ }
3671
+ const transport = new LocalRunTransport(params);
3672
+ const rt = baseRuntime(options, handler, logger, transport);
3673
+ rt.localRun = new LocalRunRunner(params, handler, { api: transport, client: void 0, logger, tracker: rt.tracker });
3674
+ printRunLog("info", LOCAL_RUN_BANNER);
3675
+ return rt;
3676
+ }
3201
3677
  function validateHandler(handler, fn) {
3202
3678
  if (typeof handler !== "function") {
3203
3679
  throw new Error(`[hobots] ${fn}(): o handler precisa ser uma fun\xE7\xE3o \u2014 ${fn}(async (params) => { ... })`);
@@ -3240,9 +3716,12 @@ function instanceInfo(rt) {
3240
3716
  machine: rt.sampler?.sample()
3241
3717
  };
3242
3718
  }
3719
+ function platformApi(rt) {
3720
+ return rt.api;
3721
+ }
3243
3722
  function ensureAgent(rt) {
3244
3723
  rt.agent ??= new TaskAgent({
3245
- api: rt.api,
3724
+ api: platformApi(rt),
3246
3725
  client: rt.client,
3247
3726
  logger: rt.logger,
3248
3727
  handler: rt.handler,
@@ -3254,7 +3733,7 @@ function ensureAgent(rt) {
3254
3733
  }
3255
3734
  function launchLocal(rt) {
3256
3735
  const runner = new LocalRunner(rt.handler, {
3257
- api: rt.api,
3736
+ api: platformApi(rt),
3258
3737
  client: rt.client,
3259
3738
  logger: rt.logger,
3260
3739
  tracker: rt.tracker,
@@ -3263,7 +3742,57 @@ function launchLocal(rt) {
3263
3742
  rt.local = runner;
3264
3743
  void runner.run();
3265
3744
  }
3745
+ function launchAttached(rt, runId) {
3746
+ const runner = new AttachedRunner(runId, rt.handler, {
3747
+ api: platformApi(rt),
3748
+ client: rt.client,
3749
+ logger: rt.logger,
3750
+ tracker: rt.tracker,
3751
+ instanceInfo: () => instanceInfo(rt)
3752
+ });
3753
+ rt.attached = runner;
3754
+ void runner.run();
3755
+ void runner.done.then(() => {
3756
+ if (runtime === rt) void close();
3757
+ });
3758
+ }
3759
+ function launchLocalRun(rt, runner) {
3760
+ let finished = false;
3761
+ const finish = () => {
3762
+ if (finished) return;
3763
+ finished = true;
3764
+ rt.uninstallShutdown?.();
3765
+ rt.uninstallShutdown = void 0;
3766
+ const code = LOCAL_RUN_EXIT_CODES[runner.finish()];
3767
+ if (runtime === rt) void close(0);
3768
+ process.stdout.write("", () => process.exit(code));
3769
+ };
3770
+ if (rt.options.installExitHandlers !== false) {
3771
+ let signaled = false;
3772
+ const onSignal = (signal) => {
3773
+ if (externalListenerCount(signal) > 0 || signaled) return;
3774
+ signaled = true;
3775
+ runner.cancel();
3776
+ void Promise.race([runner.done, sleep(LOCAL_RUN_CANCEL_GRACE_MS)]).then(finish);
3777
+ };
3778
+ const onInt = markInternal(() => onSignal("SIGINT"));
3779
+ const onTerm = markInternal(() => onSignal("SIGTERM"));
3780
+ process.on("SIGINT", onInt);
3781
+ process.on("SIGTERM", onTerm);
3782
+ rt.uninstallShutdown = () => {
3783
+ process.off("SIGINT", onInt);
3784
+ process.off("SIGTERM", onTerm);
3785
+ };
3786
+ }
3787
+ void runner.run();
3788
+ void runner.done.then(finish);
3789
+ }
3266
3790
  async function boot(rt) {
3791
+ if (rt.localRun) {
3792
+ rt.booted = true;
3793
+ launchLocalRun(rt, rt.localRun);
3794
+ return;
3795
+ }
3267
3796
  if (!rt.instanceId) {
3268
3797
  const id = await fallbackInstanceId();
3269
3798
  if (runtime !== rt) return;
@@ -3282,8 +3811,9 @@ async function boot(rt) {
3282
3811
  if (rt.options.installExitHandlers !== false) {
3283
3812
  rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
3284
3813
  }
3285
- if (rt.handler.mode === "remote") ensureAgent(rt).start();
3286
- else launchLocal(rt);
3814
+ if (rt.handler.mode === "local") launchLocal(rt);
3815
+ else if (rt.runId) launchAttached(rt, rt.runId);
3816
+ else ensureAgent(rt).start();
3287
3817
  }
3288
3818
  function scheduleBoot(rt) {
3289
3819
  if (rt.bootScheduled) return;
@@ -3299,8 +3829,12 @@ function startAgent(fn, mode, handler, options) {
3299
3829
  }
3300
3830
  const logger = makeLogger(options.debug ?? false);
3301
3831
  const agentHandler = { mode, handler, options: resolveRunOptions(options, mode, logger) };
3302
- const rt = createRuntime(options, agentHandler);
3303
- if (mode === "remote" && options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
3832
+ loadDotenv();
3833
+ const rt = isLocalRun() ? createLocalRunRuntime(options, agentHandler, logger) : createRuntime(options, agentHandler, logger);
3834
+ if (!rt.localRun && mode === "remote") {
3835
+ if (options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
3836
+ rt.runId = resolveRunId(options.runId);
3837
+ }
3304
3838
  runtime = rt;
3305
3839
  scheduleBoot(rt);
3306
3840
  }
@@ -3323,10 +3857,10 @@ function forwardAttachment(rt, attachment, message, type) {
3323
3857
  const reporter = currentReporter();
3324
3858
  if (!reporter) {
3325
3859
  rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o");
3326
- return;
3860
+ return void 0;
3327
3861
  }
3328
3862
  const withMeta = typeof attachment === "object" && !isImageAttachment(attachment) && "image" in attachment ? image(attachment.image, { caption: attachment.caption, contentType: attachment.contentType }) : attachment;
3329
- reporter.log(toRunLogLevel(type), message, withMeta);
3863
+ return reporter.log(toRunLogLevel(type), message, withMeta);
3330
3864
  }
3331
3865
  function captureException(error, hint) {
3332
3866
  const rt = runtime;
@@ -3337,11 +3871,19 @@ function captureException(error, hint) {
3337
3871
  }
3338
3872
  return void 0;
3339
3873
  }
3874
+ let attachmentId;
3340
3875
  if (hint?.attachment) {
3341
3876
  const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
3342
- forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
3877
+ attachmentId = forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
3343
3878
  }
3344
- return rt.client.captureException(error, hint);
3879
+ if (!rt.client) {
3880
+ rt.logger.debug("captureException ignorado no modo local (sem Central)");
3881
+ return void 0;
3882
+ }
3883
+ const runId = currentReporter()?.runId;
3884
+ const withRun = runId ? { ...hint, tags: { run_id: runId, ...hint?.tags } } : hint;
3885
+ const contexts = runId && attachmentId ? { run: { id: runId, attachments: [attachmentId] } } : void 0;
3886
+ return rt.client.captureException(error, withRun, contexts);
3345
3887
  }
3346
3888
  async function close(timeoutMs = 3e4) {
3347
3889
  const rt = runtime;
@@ -3350,6 +3892,8 @@ async function close(timeoutMs = 3e4) {
3350
3892
  await rt.bootPromise;
3351
3893
  if (rt.agent) await rt.agent.stop(0);
3352
3894
  rt.local?.stop();
3895
+ rt.attached?.stop();
3896
+ rt.localRun?.stop();
3353
3897
  const idle = await rt.tracker.waitIdle(timeoutMs);
3354
3898
  if (!idle) {
3355
3899
  rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
@@ -3358,7 +3902,7 @@ async function close(timeoutMs = 3e4) {
3358
3902
  rt.heartbeat?.stop();
3359
3903
  rt.uninstallShutdown?.();
3360
3904
  rt.uninstallGlobalHandlers?.();
3361
- const flushed = await rt.client.close(timeoutMs);
3905
+ const flushed = rt.client ? await rt.client.close(timeoutMs) : true;
3362
3906
  return idle && flushed;
3363
3907
  }
3364
3908
 
@@ -3387,11 +3931,13 @@ var noop = () => void 0;
3387
3931
  var fallbackEachSink = {
3388
3932
  report: noop,
3389
3933
  startStep: () => void 0,
3934
+ stepScope: (_step, fn) => fn(),
3390
3935
  declareTotal: noop,
3391
3936
  advance: noop,
3392
3937
  checkCanceled: noop,
3393
3938
  isAborted: () => false,
3394
- logError: (message) => printRunLog("error", message)
3939
+ logError: (message) => printRunLog("error", message),
3940
+ flush: () => Promise.resolve()
3395
3941
  };
3396
3942
  var fallbackItems = {
3397
3943
  add: noop,
@@ -3410,7 +3956,6 @@ var fallbackFiles = {
3410
3956
  read: outsideRunFiles
3411
3957
  };
3412
3958
  var neverAborts = new AbortController().signal;
3413
- var detachedTransaction;
3414
3959
  function resolve(member) {
3415
3960
  const reporter = currentReporter();
3416
3961
  if (reporter) return reporter.context;
@@ -3431,8 +3976,12 @@ var hobots = Object.defineProperties({}, {
3431
3976
  items: getter("items", () => fallbackItems),
3432
3977
  progress: getter("progress", () => fallbackProgress),
3433
3978
  files: getter("files", () => fallbackFiles),
3434
- transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
3435
3979
  signal: getter("signal", () => neverAborts),
3980
+ step: method((name, fn, options) => {
3981
+ const ctx = resolve("step");
3982
+ if (ctx) return ctx.step(name, fn, options);
3983
+ return new Transaction(void 0, "hobots.detached").step(name, fn, options);
3984
+ }),
3436
3985
  throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
3437
3986
  sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
3438
3987
  flush: method((timeoutMs) => resolve("flush")?.flush(timeoutMs) ?? Promise.resolve()),
@@ -3467,5 +4016,6 @@ var image2 = hobots.image;
3467
4016
  var isFileRef2 = hobots.isFileRef;
3468
4017
 
3469
4018
  // src/index.ts
4019
+ loadDotenv();
3470
4020
  var src_default = hobots;
3471
4021
  //# sourceMappingURL=index.cjs.map