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.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 ? "5.0.0" : "0.0.0-dev";
61
8
  function isNode() {
62
9
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
63
10
  }
@@ -110,8 +57,13 @@ function abortableSleep(ms, signal) {
110
57
  function jitter(baseMs) {
111
58
  return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
112
59
  }
60
+ var TRUNCATION_SUFFIX = "\u2026 [truncado]";
113
61
  function truncate(value, max) {
114
- return value.length > max ? `${value.slice(0, max)}\u2026 [truncado]` : value;
62
+ return value.length > max ? `${value.slice(0, max)}${TRUNCATION_SUFFIX}` : value;
63
+ }
64
+ function clip(value, max) {
65
+ if (value.length <= max) return value;
66
+ return `${value.slice(0, Math.max(0, max - TRUNCATION_SUFFIX.length))}${TRUNCATION_SUFFIX}`;
115
67
  }
116
68
  function safeString(value) {
117
69
  if (typeof value === "string") return value;
@@ -205,6 +157,124 @@ function colorize(text, color) {
205
157
  return `${ANSI[color]}${text}${ANSI.reset}`;
206
158
  }
207
159
 
160
+ // src/env.ts
161
+ var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
162
+ var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
163
+ var RUN_ID_ENV = "HOBOTS_RUN_ID";
164
+ var LOCAL_RUN_ENV = "HOBOTS_LOCAL_RUN";
165
+ var RUN_PARAMS_ENV = "HOBOTS_RUN_PARAMS";
166
+ var dotenvLoaded = false;
167
+ function loadDotenv() {
168
+ if (dotenvLoaded) return;
169
+ dotenvLoaded = true;
170
+ if (!isNode() || typeof process.loadEnvFile !== "function") return;
171
+ try {
172
+ process.loadEnvFile(".env");
173
+ } catch (err) {
174
+ const code = err?.code;
175
+ if (code === "ENOENT") return;
176
+ 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`);
177
+ }
178
+ }
179
+ function resolveInstanceId(option) {
180
+ const fromOption = option?.trim();
181
+ if (fromOption) return fromOption;
182
+ if (typeof process === "undefined" || !process.env) return void 0;
183
+ return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
184
+ }
185
+ function resolveRunId(option) {
186
+ const fromOption = option?.trim();
187
+ if (fromOption) return fromOption;
188
+ if (typeof process === "undefined" || !process.env) return void 0;
189
+ return process.env[RUN_ID_ENV]?.trim() || void 0;
190
+ }
191
+ function isLocalRun() {
192
+ if (typeof process === "undefined" || !process.env) return false;
193
+ return process.env[LOCAL_RUN_ENV]?.trim() === "1";
194
+ }
195
+ function resolveLocalRunParams() {
196
+ const raw = process.env[RUN_PARAMS_ENV]?.trim();
197
+ if (!raw) return {};
198
+ let parsed;
199
+ try {
200
+ parsed = JSON.parse(raw);
201
+ } catch {
202
+ throw new Error(`${RUN_PARAMS_ENV} inv\xE1lido: n\xE3o \xE9 JSON v\xE1lido`);
203
+ }
204
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
205
+ throw new Error(`${RUN_PARAMS_ENV} inv\xE1lido: esperado um objeto JSON`);
206
+ }
207
+ return parsed;
208
+ }
209
+ function resolveEnvironmentOption(option) {
210
+ const fromOption = option?.trim();
211
+ if (fromOption) return fromOption;
212
+ if (typeof process === "undefined" || !process.env) return void 0;
213
+ return process.env[ENVIRONMENT_ENV]?.trim() || void 0;
214
+ }
215
+ async function fallbackInstanceId() {
216
+ if (!isNode()) return "browser";
217
+ try {
218
+ const os = await import("os");
219
+ return os.hostname() || "unknown-host";
220
+ } catch {
221
+ return "unknown-host";
222
+ }
223
+ }
224
+
225
+ // src/stacktrace.ts
226
+ var MAX_FRAMES = 50;
227
+ var MAX_FILENAME_CHARS = 1024;
228
+ var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
229
+ var LOCATION_RE = /^(.*?):(\d+):(\d+)$/;
230
+ function isInApp(filename) {
231
+ if (!filename) return false;
232
+ if (filename.startsWith("node:")) return false;
233
+ if (filename.startsWith("internal/")) return false;
234
+ if (filename.includes("node_modules")) return false;
235
+ if (filename === "<anonymous>") return false;
236
+ return true;
237
+ }
238
+ function parseStacktrace(stack) {
239
+ if (!stack) return [];
240
+ const frames = [];
241
+ for (const line of stack.split("\n")) {
242
+ if (frames.length >= MAX_FRAMES) break;
243
+ const match = FRAME_RE.exec(line);
244
+ if (!match || !line.trimStart().startsWith("at ")) continue;
245
+ const [, fn, location] = match;
246
+ const loc = LOCATION_RE.exec(location ?? "");
247
+ const filename = loc ? loc[1] : location;
248
+ frames.push({
249
+ function: fn || "<anonymous>",
250
+ filename: filename ? filename.slice(0, MAX_FILENAME_CHARS) : void 0,
251
+ lineno: loc?.[2] ? Number(loc[2]) : void 0,
252
+ colno: loc?.[3] ? Number(loc[3]) : void 0,
253
+ in_app: isInApp(filename)
254
+ });
255
+ }
256
+ return frames;
257
+ }
258
+
259
+ // src/scrub.ts
260
+ var SECRET_PATTERNS = [
261
+ // client secret da Hobots
262
+ [/hb_[a-f0-9]{16,}/gi, "[REDACTED]"],
263
+ // tokens bearer em headers/mensagens
264
+ [/\b(authorization:?\s*)?bearer\s+[a-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]"],
265
+ // chaves no formato sk-… (OpenAI e afins)
266
+ [/\bsk-[a-z0-9_-]{16,}\b/gi, "[REDACTED]"],
267
+ // credenciais embutidas em URL (https://user:pass@host)
268
+ [/\/\/[^/\s:@]+:[^/\s@]+@/g, "//[REDACTED]@"]
269
+ ];
270
+ function scrubSecrets(text) {
271
+ let out = text;
272
+ for (const [pattern, replacement] of SECRET_PATTERNS) {
273
+ out = out.replace(pattern, replacement);
274
+ }
275
+ return out;
276
+ }
277
+
208
278
  // src/eventbuilder.ts
209
279
  var MAX_EXCEPTION_VALUE_CHARS = 8192;
210
280
  function eventFromException(error, type = "error") {
@@ -349,13 +419,26 @@ var MAX_FINGERPRINT_ITEMS = 10;
349
419
  var MAX_FINGERPRINT_ITEM_CHARS = 255;
350
420
  var MAX_TRANSACTION_NAME_CHARS = 500;
351
421
  var MAX_STEPS = 200;
422
+ var MAX_STEP_DESCRIPTION_CHARS = 2048;
423
+ var MAX_STEP_OP_CHARS = 255;
352
424
  function clipTags(tags) {
353
425
  return Object.fromEntries(Object.entries(tags).map(([key, value]) => [key, truncate(String(value), MAX_TAG_VALUE_CHARS)]));
354
426
  }
427
+ function clipStep(step) {
428
+ return {
429
+ ...step,
430
+ ...step.op ? { op: clip(step.op, MAX_STEP_OP_CHARS) } : {},
431
+ ...step.description ? { description: clip(step.description, MAX_STEP_DESCRIPTION_CHARS) } : {}
432
+ };
433
+ }
355
434
  function clipFingerprint(fingerprint) {
356
435
  if (fingerprint === void 0) return void 0;
357
436
  return fingerprint.slice(0, MAX_FINGERPRINT_ITEMS).map((item) => truncate(String(item), MAX_FINGERPRINT_ITEM_CHARS));
358
437
  }
438
+ var captured = /* @__PURE__ */ new WeakSet();
439
+ function wasCaptured(error) {
440
+ return typeof error === "object" && error !== null && captured.has(error);
441
+ }
359
442
  function runtimeContexts() {
360
443
  if (isNode()) {
361
444
  return {
@@ -406,11 +489,13 @@ var Client = class {
406
489
  [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
407
490
  return `Client { baseUrl: '${this.#config.baseUrl}' }`;
408
491
  }
409
- captureException(error, hint) {
410
- return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
492
+ /** `contexts` é interno ao SDK (ex.: `contexts.run` com o print do captureException) — o evento ainda pode sobrescrever. */
493
+ captureException(error, hint, contexts) {
494
+ if (typeof error === "object" && error !== null) captured.add(error);
495
+ return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint, contexts);
411
496
  }
412
- captureEvent(event, hint) {
413
- const final = this.prepareEvent(event, hint);
497
+ captureEvent(event, hint, contexts) {
498
+ const final = this.prepareEvent(contexts ? { ...event, contexts: { ...contexts, ...event.contexts } } : event, hint);
414
499
  this.transport.enqueue({ type: "event", payload: final });
415
500
  return final.event_id;
416
501
  }
@@ -433,14 +518,15 @@ var Client = class {
433
518
  sendTransaction(payload) {
434
519
  let steps = payload.steps;
435
520
  if (steps.length > MAX_STEPS) {
436
- this.logger.warn(`transaction com ${steps.length} steps \u2014 enviando as primeiras ${MAX_STEPS}`);
521
+ this.logger.warn(`transaction com ${steps.length} etapas \u2014 enviando as primeiras ${MAX_STEPS}`);
437
522
  steps = steps.slice(0, MAX_STEPS);
438
523
  }
524
+ steps = steps.map(clipStep);
439
525
  this.transport.enqueue({
440
526
  type: "transaction",
441
527
  payload: {
442
528
  ...payload,
443
- name: truncate(payload.name || "(sem nome)", MAX_TRANSACTION_NAME_CHARS),
529
+ name: clip(payload.name || "(sem nome)", MAX_TRANSACTION_NAME_CHARS),
444
530
  steps,
445
531
  release: payload.release ?? this.options.release,
446
532
  environment: this.resolveEnvironment(payload.environment),
@@ -514,12 +600,12 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
514
600
  const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
515
601
  if (!key) {
516
602
  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`
603
+ `${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
604
  );
519
605
  }
520
606
  if (!SECRET_PATTERN.test(key)) {
521
607
  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`
608
+ `${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
609
  );
524
610
  }
525
611
  const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
@@ -531,31 +617,6 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
531
617
  };
532
618
  }
533
619
 
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
620
  // src/heartbeat.ts
560
621
  var DEFAULT_INTERVAL_MS = WIRE_LIMITS.HEARTBEAT_DEFAULT_INTERVAL_MS;
561
622
  var MIN_REPORTED_INTERVAL_MS = WIRE_LIMITS.HEARTBEAT_MIN_INTERVAL_MS;
@@ -917,6 +978,42 @@ var TasksApi = class {
917
978
  created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
918
979
  };
919
980
  }
981
+ /**
982
+ * Anexa o processo a uma run já reivindicada para esta instância (env HOBOTS_RUN_ID):
983
+ * POST /v1/runs/:id/start. Um request só — quem decide o retry é o AttachedRunner.
984
+ * 404 (não existe) e 409 (cancelada, já iniciada ou de outra instância) viram
985
+ * TasksApiError com o status.
986
+ */
987
+ async startRun(runId, request, signal) {
988
+ const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
989
+ let response;
990
+ try {
991
+ response = await fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/start`, {
992
+ redirect: "error",
993
+ method: "POST",
994
+ headers: this.headers(),
995
+ body: JSON.stringify(request),
996
+ signal: timeoutSignal
997
+ });
998
+ } finally {
999
+ cleanup();
1000
+ }
1001
+ if (!response.ok) {
1002
+ throw new TasksApiError(`in\xEDcio da run falhou (HTTP ${response.status})`, response.status);
1003
+ }
1004
+ const body = await response.json().catch(() => null);
1005
+ if (typeof body?.id !== "string") {
1006
+ throw new TasksApiError("resposta inv\xE1lida do in\xEDcio da run (id ausente)", 0);
1007
+ }
1008
+ const form = body.form;
1009
+ return {
1010
+ id: body.id,
1011
+ params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
1012
+ form: typeof form?.id === "string" && typeof form.name === "string" ? { id: form.id, name: form.name } : null,
1013
+ source: typeof body.source === "string" ? body.source : "hobots",
1014
+ created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
1015
+ };
1016
+ }
920
1017
  /** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
921
1018
  parsePollResponse(body, max) {
922
1019
  const runs = body?.runs;
@@ -1174,7 +1271,7 @@ var Backoff = class {
1174
1271
  this.authFailures++;
1175
1272
  if (this.authFailures === 1 || this.authFailures % 10 === 0) {
1176
1273
  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"
1274
+ 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
1275
  );
1179
1276
  }
1180
1277
  return Math.max(this.floorMs, AUTH_FAILURE_INTERVAL_MS);
@@ -1200,6 +1297,9 @@ var TERMINAL_ERROR_NAMES = /* @__PURE__ */ new Set(["RunCanceled", "AbortError",
1200
1297
  function isTerminalError(error) {
1201
1298
  return error instanceof Error && TERMINAL_ERROR_NAMES.has(error.name);
1202
1299
  }
1300
+ function isCancellation(error) {
1301
+ return error instanceof Error && (error.name === "RunCanceled" || error.name === "AbortError");
1302
+ }
1203
1303
 
1204
1304
  // src/tasks/each.ts
1205
1305
  function thrownMessage(error) {
@@ -1226,11 +1326,11 @@ var ItemHandle = class {
1226
1326
  }
1227
1327
  failed(message, payload, options) {
1228
1328
  this.record("failed", message, payload, options?.image);
1229
- if (this.step && this.step.status !== "error") this.step.setStatus("error", message);
1329
+ if (this.step && this.step.status !== "error") this.step._setStatus("error", message);
1230
1330
  }
1231
1331
  occurrence(message, payload, options) {
1232
1332
  this.record("occurrence", message, payload, options?.image);
1233
- if (this.step && this.step.status === "ok") this.step.setStatus("warning", message);
1333
+ if (this.step && this.step.status === "ok") this.step._setStatus("warning", message);
1234
1334
  }
1235
1335
  record(status, message, payload, image3) {
1236
1336
  this.reported = true;
@@ -1261,24 +1361,24 @@ async function eachItem(list, handler, options, sink) {
1261
1361
  const step = sink.startStep(options.op ?? "item", description);
1262
1362
  const handle = new ItemHandle(index, id, step, options.payload?.(element, index), sink);
1263
1363
  try {
1264
- await handler(element, handle, index);
1364
+ await sink.stepScope(step, async () => handler(element, handle, index));
1265
1365
  if (!handle.reported) handle.succeeded();
1266
1366
  } catch (error) {
1267
1367
  if (isTerminalError(error) || sink.isAborted()) {
1268
- step?.setStatus("cancelled");
1269
- step?.finish();
1368
+ step?._setStatus("cancelled")._close();
1270
1369
  throw error;
1271
1370
  }
1272
1371
  const message = thrownMessage(error);
1273
1372
  if (!handle.reported) handle.failed(message);
1274
- step?.setError(error);
1373
+ step?._failed(error);
1275
1374
  sink.logError(`item ${description} falhou: ${message}`);
1276
1375
  }
1277
- step?.finish();
1376
+ step?._close();
1278
1377
  if (options.progress !== false) sink.advance();
1279
1378
  result.total += 1;
1280
1379
  result[handle.status ?? "succeeded"] += 1;
1281
1380
  index += 1;
1381
+ if (options.flush !== false) await sink.flush();
1282
1382
  }
1283
1383
  return result;
1284
1384
  }
@@ -1318,6 +1418,11 @@ function isInsideDir(root, candidate, sep) {
1318
1418
  function unavailableLocal(ref) {
1319
1419
  return new Error(`arquivo "${ref.name}" enviado pelo Studio de outro computador ou j\xE1 consumido`);
1320
1420
  }
1421
+ function localRunPath(ref) {
1422
+ if (!isLocalRun()) return void 0;
1423
+ const path = ref.path;
1424
+ return typeof path === "string" && path.trim() !== "" ? path : void 0;
1425
+ }
1321
1426
  function errorCode(error) {
1322
1427
  return error?.code;
1323
1428
  }
@@ -1485,8 +1590,16 @@ var RunFileStore = class {
1485
1590
  this.deps.warn(`arquivo "${ref.name}" baixado, mas a confirma\xE7\xE3o ao servidor falhou: ${String(error)}`);
1486
1591
  }
1487
1592
  }
1488
- /** storage 'local': copia o único arquivo de `$HOBOTS_LOCAL_FILES_DIR/<id>/` e apaga a pasta de origem. */
1593
+ /**
1594
+ * storage 'local': copia o único arquivo de `$HOBOTS_LOCAL_FILES_DIR/<id>/` e apaga a pasta de
1595
+ * origem. No modo local, um FileRef com `path` copia direto desse caminho, sem apagar a origem.
1596
+ */
1489
1597
  async copyLocal(ref, target) {
1598
+ const direct = localRunPath(ref);
1599
+ if (direct !== void 0) {
1600
+ await this.copyFromPath(ref, direct, target);
1601
+ return;
1602
+ }
1490
1603
  const base = process.env[LOCAL_FILES_DIR_ENV];
1491
1604
  if (!base) throw unavailableLocal(ref);
1492
1605
  const { copyFile, readdir, rename, rm, stat } = await import("fs/promises");
@@ -1520,6 +1633,26 @@ var RunFileStore = class {
1520
1633
  }
1521
1634
  await rm(folder, { recursive: true, force: true });
1522
1635
  }
1636
+ /**
1637
+ * Modo local: cópia do arquivo apontado por `path` (teste roda várias vezes — a fonte fica).
1638
+ * Sem conferência de tamanho: o `size` do FileRef é informativo aqui.
1639
+ */
1640
+ async copyFromPath(ref, path, target) {
1641
+ const { copyFile, rename, rm, stat } = await import("fs/promises");
1642
+ const { resolve: resolve2 } = await import("path");
1643
+ const source = resolve2(path);
1644
+ const info = await stat(source).catch(() => void 0);
1645
+ if (!info?.isFile()) throw new Error(`arquivo "${ref.name}" n\xE3o encontrado em ${path}`);
1646
+ this.checkCanceled();
1647
+ const part = `${target}.part`;
1648
+ try {
1649
+ await copyFile(source, part);
1650
+ await rename(part, target);
1651
+ } catch (error) {
1652
+ await rm(part, { force: true });
1653
+ throw error;
1654
+ }
1655
+ }
1523
1656
  translate(error, ref) {
1524
1657
  if (this.deps.signal.aborted) return runCanceledError();
1525
1658
  if (error instanceof TasksApiError && error.status === 410) {
@@ -1648,6 +1781,11 @@ function printRunLog(level, message) {
1648
1781
  const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
1649
1782
  console[method2](line, message);
1650
1783
  }
1784
+ var PROGRESS_LINE_PREFIX = "[hobots:progress]";
1785
+ var PROGRESS_PRINT_THROTTLE_MS = 250;
1786
+ function printRunProgress(current, total) {
1787
+ console.log(`${PROGRESS_LINE_PREFIX} ${current}/${total}`);
1788
+ }
1651
1789
  function cancelableSleep(ms, signal) {
1652
1790
  return new Promise((resolve2, reject) => {
1653
1791
  if (signal.aborted) {
@@ -1716,14 +1854,16 @@ var RunReporter = class {
1716
1854
  tx.setStepGuard(checkCanceled);
1717
1855
  const eachSink = {
1718
1856
  report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
1719
- startStep: (op, description) => tx.startChild(op, description),
1857
+ startStep: (op, description) => tx.start(op, description),
1858
+ stepScope: (step, fn) => tx.scope(step, fn),
1720
1859
  declareTotal: (total) => {
1721
1860
  if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
1722
1861
  },
1723
1862
  advance: () => this.setProgress(this.progressCurrent + 1, this.progressTotal),
1724
1863
  checkCanceled,
1725
1864
  isAborted: () => this.abortController.signal.aborted,
1726
- logError: (message) => this.log("error", message)
1865
+ logError: (message) => this.log("error", message),
1866
+ flush: () => this.flush()
1727
1867
  };
1728
1868
  const items = {
1729
1869
  add: (item) => {
@@ -1778,7 +1918,14 @@ var RunReporter = class {
1778
1918
  items,
1779
1919
  progress,
1780
1920
  files: this.fileStore.files,
1781
- transaction: tx,
1921
+ step: async (name, fn, options) => {
1922
+ const root = tx.currentStep() === void 0;
1923
+ try {
1924
+ return await tx.step(name, fn, { ...options, signal, wait: sleep2, warn: (message) => this.log("warning", message) });
1925
+ } finally {
1926
+ if (options?.flush !== false && root && !signal.aborted) await this.flush();
1927
+ }
1928
+ },
1782
1929
  flush: (timeoutMs) => this.flush(timeoutMs),
1783
1930
  signal,
1784
1931
  throwIfCanceled: checkCanceled,
@@ -1809,8 +1956,17 @@ var RunReporter = class {
1809
1956
  itemSeq = 0;
1810
1957
  /** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
1811
1958
  itemsUnsupported = false;
1812
- /** Cap de itens por run atingido (client-side ou 429) — degrada com um aviso só. */
1959
+ /**
1960
+ * Cota de itens por run do servidor (429, legado — hoje a api não tem teto de itens) —
1961
+ * degrada com um aviso só.
1962
+ */
1813
1963
  itemsQuotaExceeded = false;
1964
+ /**
1965
+ * Armazenamento do plano cheio (402 `storage_limit`): o lote é descartado e o aviso sai
1966
+ * uma vez por episódio. Os envios continuam — num agente `start()` o espaço pode ser
1967
+ * liberado (ou o plano trocado) sem reiniciar; um lote aceito rearma o aviso.
1968
+ */
1969
+ itemsStorageFull = false;
1814
1970
  /**
1815
1971
  * Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
1816
1972
  * servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
@@ -1832,6 +1988,8 @@ var RunReporter = class {
1832
1988
  progressDirty = false;
1833
1989
  progressTimer;
1834
1990
  progressWarned = false;
1991
+ /** Relógio do último eco `[hobots:progress]` no terminal (throttle do eco, não do wire). */
1992
+ progressLastPrint = 0;
1835
1993
  /** Arquivos do formulário desta run (`ctx.files`) — a pasta temporária é apagada no fim. */
1836
1994
  fileStore;
1837
1995
  /** Apaga as cópias de arquivo que ficaram na pasta temporária da run. Nunca lança. */
@@ -1882,15 +2040,17 @@ var RunReporter = class {
1882
2040
  /**
1883
2041
  * Linha de log da run com eco no terminal. `image` (opcional) vira o anexo da linha:
1884
2042
  * o id entra na entrada ANTES de qualquer I/O — é isso que mantém a ordem dos logs
1885
- * intacta e o handler livre da latência do upload.
2043
+ * intacta e o handler livre da latência do upload. Devolve o id do anexo (quando houve
2044
+ * imagem e ela não foi descartada) — o `captureException` o leva ao evento da issue.
1886
2045
  */
1887
2046
  log(level, message, image3) {
1888
- if (this.dead) return;
2047
+ if (this.dead) return void 0;
1889
2048
  const scrubbed = scrubSecrets(safeString(message));
1890
2049
  const ts = nowISO();
1891
2050
  const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
1892
2051
  printRunLog(level, attachmentId ? `${scrubbed} [imagem]` : scrubbed);
1893
2052
  this.push(level, scrubbed, true, attachmentId ? [attachmentId] : void 0, ts);
2053
+ return attachmentId;
1894
2054
  }
1895
2055
  /** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
1896
2056
  push(level, message, alreadyScrubbed = false, attachments, ts = nowISO()) {
@@ -1936,7 +2096,7 @@ var RunReporter = class {
1936
2096
  }
1937
2097
  }
1938
2098
  /**
1939
- * `hobots.flush()`: o bot decide quando entregar o que já fez — steps raiz finalizadas
2099
+ * `hobots.flush()`: o bot decide quando entregar o que já fez — etapas raiz finalizadas
1940
2100
  * viram transactions parciais da run, e itens, logs e progresso pendentes saem já, sem
1941
2101
  * esperar o throttle. Aguarda a entrega com teto. Nunca lança (pode ir num `finally`).
1942
2102
  */
@@ -1994,10 +2154,16 @@ var RunReporter = class {
1994
2154
  }
1995
2155
  const safeCurrent = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : this.progressCurrent;
1996
2156
  const next = total == null ? safeCurrent : Math.min(safeCurrent, total);
1997
- if (next === this.progressCurrent && total === this.progressTotal) return;
2157
+ const totalChanged = total !== this.progressTotal;
2158
+ if (next === this.progressCurrent && !totalChanged) return;
1998
2159
  this.progressCurrent = next;
1999
2160
  this.progressTotal = total;
2000
2161
  if (total == null) return;
2162
+ const now = Date.now();
2163
+ if (totalChanged || next >= total || now - this.progressLastPrint >= PROGRESS_PRINT_THROTTLE_MS) {
2164
+ this.progressLastPrint = now;
2165
+ printRunProgress(next, total);
2166
+ }
2001
2167
  this.progressDirty = true;
2002
2168
  if (this.progressTimer) return;
2003
2169
  this.progressTimer = setTimeout(() => {
@@ -2120,6 +2286,7 @@ var RunReporter = class {
2120
2286
  if (this.itemsUnsupported) return;
2121
2287
  try {
2122
2288
  await this.api.createItems(this.runId, { items: batch });
2289
+ this.itemsStorageFull = false;
2123
2290
  } catch (error) {
2124
2291
  if (error instanceof TasksApiError && error.status === 404) {
2125
2292
  this.itemsUnsupported = true;
@@ -2133,6 +2300,13 @@ var RunReporter = class {
2133
2300
  }
2134
2301
  return;
2135
2302
  }
2303
+ if (error instanceof TasksApiError && error.status === 402) {
2304
+ if (!this.itemsStorageFull) {
2305
+ this.itemsStorageFull = true;
2306
+ this.log("warning", "itens descartados: armazenamento do plano cheio \u2014 libere espa\xE7o ou fa\xE7a upgrade em Plano e cobran\xE7a");
2307
+ }
2308
+ return;
2309
+ }
2136
2310
  if (error instanceof TasksApiError) {
2137
2311
  this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
2138
2312
  this.log("warning", `${batch.length} item(ns) descartado(s): envio rejeitado pelo servidor (HTTP ${error.status})`);
@@ -2164,7 +2338,7 @@ var RunReporter = class {
2164
2338
  }
2165
2339
  // ── Anexos ───────────────────────────────────────────────────────────────────
2166
2340
  /**
2167
- * Pendura uma imagem num evento da run (log, item ou step): faz as checagens que não
2341
+ * Pendura uma imagem num evento da run (log, item ou etapa): faz as checagens que não
2168
2342
  * dependem de I/O (reporter morto, instalação sem anexos, cota), reserva o id e dispara
2169
2343
  * o upload em segundo plano. Devolve o id — já referenciável pelo evento — ou undefined
2170
2344
  * quando a imagem foi descartada (sempre com aviso no log da run). Nunca lança: todo
@@ -2337,11 +2511,13 @@ var RunReporter = class {
2337
2511
 
2338
2512
  // src/tasks/run-context-store.ts
2339
2513
  var storage;
2514
+ var stepStorage;
2340
2515
  var initPromise;
2341
2516
  async function ensureStorage() {
2342
2517
  if (storage || !isNode()) return;
2343
2518
  initPromise ??= import("async_hooks").then(({ AsyncLocalStorage: ALS }) => {
2344
2519
  storage = new ALS();
2520
+ stepStorage = new ALS();
2345
2521
  });
2346
2522
  await initPromise;
2347
2523
  }
@@ -2352,12 +2528,23 @@ async function runWithRunContext(store, fn) {
2352
2528
  function currentRunReporter() {
2353
2529
  return storage?.getStore()?.reporter;
2354
2530
  }
2531
+ async function runWithStepScope(scope, fn) {
2532
+ await ensureStorage();
2533
+ return stepStorage ? stepStorage.run(scope, fn) : fn();
2534
+ }
2535
+ function currentStep(transaction) {
2536
+ const scope = stepStorage?.getStore();
2537
+ return scope?.transaction === transaction ? scope.step : void 0;
2538
+ }
2355
2539
 
2356
2540
  // src/transaction.ts
2357
2541
  var FLUSH_TRANSACTION_OP = "hobots.run.flush";
2542
+ var DEFAULT_STEP_RETRIES = 1;
2543
+ var DEFAULT_STEP_RETRY_DELAY_MS = 5e3;
2358
2544
  var MAX_STEP_MESSAGE_CHARS = 2e3;
2545
+ var MAX_BACKLOG = 100;
2359
2546
  function stepMessage(text) {
2360
- return truncate(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
2547
+ return clip(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
2361
2548
  }
2362
2549
  function stepId() {
2363
2550
  return uuid4().slice(0, 16);
@@ -2365,6 +2552,13 @@ function stepId() {
2365
2552
  function stepLabel(op, description) {
2366
2553
  return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
2367
2554
  }
2555
+ function errorText(error) {
2556
+ if (error instanceof Error) return error.message || error.name;
2557
+ return safeString(error);
2558
+ }
2559
+ function errorReason(error) {
2560
+ return error instanceof Error ? `${error.name}: ${errorText(error)}` : errorText(error);
2561
+ }
2368
2562
  function levelForStatus(status) {
2369
2563
  switch (status) {
2370
2564
  case "warning":
@@ -2404,58 +2598,68 @@ var Step = class {
2404
2598
  startTimestamp = nowISO();
2405
2599
  status = "ok";
2406
2600
  endTimestamp;
2407
- data;
2408
- /**
2409
- * Desfecho da step. `message` (opcional) é o motivo legível — vai em `data.message`
2410
- * e aparece na Central (performance e banner da execução) e no log de FINISH.
2411
- * `options.image` pendura um print do estado da tela em `data.attachments`.
2412
- */
2413
- setStatus(status, message, options) {
2414
- this.status = status;
2415
- if (message) this.setData("message", stepMessage(message));
2601
+ _data;
2602
+ // ── API do bot ────────────────────────────────────────────────────────────
2603
+ warning(message, options) {
2604
+ if (this.status !== "error") this._setStatus("warning", message);
2416
2605
  if (options?.image !== void 0) this.attachImage(options.image);
2417
2606
  return this;
2418
2607
  }
2608
+ image(image3) {
2609
+ this.attachImage(image3);
2610
+ return this;
2611
+ }
2612
+ data(key, value) {
2613
+ if (key === "attachments") return this;
2614
+ this._setData(key, value);
2615
+ return this;
2616
+ }
2617
+ // ── Internos (chamados pelo SDK) ──────────────────────────────────────────
2618
+ /** `data` da etapa como vai no wire (undefined quando vazio). */
2619
+ get payload() {
2620
+ return this._data;
2621
+ }
2622
+ _setStatus(status, message) {
2623
+ this.status = status;
2624
+ if (message) this._setData("message", stepMessage(safeString(message)));
2625
+ return this;
2626
+ }
2419
2627
  /**
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.
2422
- * `options.image` pendura um print do estado da tela em `data.attachments`.
2628
+ * Marca `error` guardando `{ type, message }` em `data.error` e fecha a etapa. Aceita qualquer
2629
+ * valor lançado; `message` sobrescreve a mensagem do erro. Numa etapa já fechada não faz nada.
2423
2630
  */
2424
- setError(error, message, options) {
2631
+ _failed(error, message) {
2632
+ if (this.endTimestamp) return this;
2425
2633
  this.status = "error";
2426
2634
  const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
2427
- const raw = message ?? (error instanceof Error ? error.message || error.name : String(error));
2428
- this.setData("error", { ...type ? { type } : {}, message: stepMessage(raw) });
2429
- if (options?.image !== void 0) this.attachImage(options.image);
2635
+ const raw = message ?? errorText(error);
2636
+ this._setData("error", { ...type ? { type } : {}, message: stepMessage(safeString(raw)) });
2637
+ this._close();
2430
2638
  return this;
2431
2639
  }
2640
+ _setData(key, value) {
2641
+ this._data = { ...this._data, [key]: value };
2642
+ }
2432
2643
  /**
2433
2644
  * Dispara o upload pela run (sink do RunReporter) e referencia o anexo em
2434
2645
  * `data.attachments` (chave reservada; teto igual ao das linhas de log). Fora de uma
2435
2646
  * run não há onde pendurar a imagem — descarta com aviso único.
2436
2647
  */
2437
2648
  attachImage(image3) {
2438
- const current = Array.isArray(this.data?.attachments) ? this.data.attachments : [];
2649
+ const current = Array.isArray(this._data?.attachments) ? this._data.attachments : [];
2439
2650
  if (current.length >= MAX_ATTACHMENTS_PER_LOG_ENTRY) return;
2440
2651
  const id = this.transaction.attachImage(image3);
2441
2652
  if (!id) return;
2442
- this.setData("attachments", [...current, id]);
2653
+ this._setData("attachments", [...current, id]);
2443
2654
  }
2444
- /** Motivo registrado via setStatus(…, message) ou setError — usado no log de FINISH. */
2655
+ /** Motivo registrado (warning/erro) — usado no log de FINISH. */
2445
2656
  reason() {
2446
- const error = this.data?.error;
2447
- const text = typeof error?.message === "string" ? error.message : this.data?.message;
2657
+ const error = this._data?.error;
2658
+ const text = typeof error?.message === "string" ? error.message : this._data?.message;
2448
2659
  return typeof text === "string" && text ? text : void 0;
2449
2660
  }
2450
- setData(key, value) {
2451
- this.data = { ...this.data, [key]: value };
2452
- return this;
2453
- }
2454
- /** Step aninhada (filha desta step). */
2455
- startChild(op, description) {
2456
- return this.transaction.createStep(this.stepId, op, description);
2457
- }
2458
- finish() {
2661
+ /** Fecha a etapa mantendo o status que estiver. Idempotente. */
2662
+ _close() {
2459
2663
  if (this.endTimestamp) return;
2460
2664
  this.endTimestamp = nowISO();
2461
2665
  this.transaction.emit(
@@ -2471,22 +2675,23 @@ var Step = class {
2471
2675
  toJSON() {
2472
2676
  return {
2473
2677
  step_id: this.stepId,
2474
- parent_step_id: this.parentStepId,
2475
- op: this.op,
2476
- description: this.description,
2678
+ ...this.parentStepId ? { parent_step_id: this.parentStepId } : {},
2679
+ ...this.op ? { op: clip(this.op, MAX_STEP_OP_CHARS) } : {},
2680
+ ...this.description ? { description: clip(this.description, MAX_STEP_DESCRIPTION_CHARS) } : {},
2477
2681
  status: this.status,
2478
2682
  start_timestamp: this.startTimestamp,
2479
2683
  timestamp: this.endTimestamp ?? nowISO(),
2480
- data: this.data
2684
+ ...this._data ? { data: this._data } : {}
2481
2685
  };
2482
2686
  }
2483
2687
  };
2484
2688
  var Transaction = class {
2485
- constructor(client, name, op, runId) {
2689
+ constructor(client, name, op, runId, options = {}) {
2486
2690
  this.client = client;
2487
2691
  this.name = name;
2488
2692
  this.op = op;
2489
2693
  this.runId = runId;
2694
+ this.echo = options.echo ?? false;
2490
2695
  this.emit("transaction", "start", op ? `${name} (${op})` : name);
2491
2696
  }
2492
2697
  client;
@@ -2498,33 +2703,36 @@ var Transaction = class {
2498
2703
  startTimestamp = nowISO();
2499
2704
  status = "ok";
2500
2705
  steps = [];
2501
- tags = {};
2502
2706
  finished = false;
2503
2707
  lifecycleSink;
2504
2708
  attachmentSink;
2505
2709
  warnedNoAttachmentSink = false;
2506
- // instalado pelo RunReporter: abrir uma step é um ponto de parada cooperativo (lança RunCanceled)
2710
+ // instalado pelo RunReporter: abrir uma etapa é um ponto de parada cooperativo (lança RunCanceled)
2507
2711
  stepGuard;
2712
+ // modo local (sem client): o FINISH de cada etapa sai no terminal, como `[hobots:item]`
2713
+ echo;
2508
2714
  // eventos emitidos antes do sink existir (o START da transaction sai no construtor,
2509
2715
  // antes do RunReporter conectar) — replay no setLifecycleSink
2510
2716
  lifecycleBacklog = [];
2717
+ // ── Lifecycle / eco ───────────────────────────────────────────────────────
2511
2718
  /**
2512
- * Registra o início/fim de uma step ou transaction: espelha a linha no sink de
2513
- * lifecycle, se conectado (é assim que os eventos de performance aparecem nos
2514
- * logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
2515
- * escreve no stdout do processo do usuário sem permissão.
2719
+ * Registra o início/fim de uma etapa ou da transaction: espelha a linha no sink de
2720
+ * lifecycle, se conectado (é assim que os eventos de performance aparecem nos logs da
2721
+ * run no app), e ecoa no terminal com `debug: true` — ou, no modo local, só o FINISH das
2722
+ * etapas (`[hobots:step] FINISH …`), para o Studio e o dev verem.
2516
2723
  */
2517
2724
  emit(kind, phase, label, startTs, endTs, status, reason) {
2518
2725
  const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
2519
2726
  const suffix = (durationMs != null ? ` (${durationMs}ms)` : "") + (reason ? `: ${reason}` : "");
2520
- if (this.client?.debug) {
2727
+ const localFinish = this.echo && kind === "step" && phase === "finish";
2728
+ if (this.client?.debug || localFinish) {
2521
2729
  const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
2522
2730
  console.log(`${tag} ${label}${suffix}`);
2523
2731
  }
2524
2732
  const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
2525
2733
  const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
2526
2734
  if (this.lifecycleSink) this.lifecycleSink(level, message);
2527
- else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
2735
+ else if (this.lifecycleBacklog.length < MAX_BACKLOG) this.lifecycleBacklog.push([level, message]);
2528
2736
  }
2529
2737
  /** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
2530
2738
  setLifecycleSink(sink) {
@@ -2533,59 +2741,111 @@ var Transaction = class {
2533
2741
  this.lifecycleBacklog = [];
2534
2742
  for (const [level, message] of backlog) sink(level, message);
2535
2743
  }
2536
- /** Conecta o pipeline de anexos da run — é o que dá imagem às steps. */
2744
+ /** Conecta o pipeline de anexos da run — é o que dá imagem às etapas. */
2537
2745
  setAttachmentSink(sink) {
2538
2746
  this.attachmentSink = sink;
2539
2747
  }
2540
- /** Imagem de uma step → id do anexo pela run; sem run, descarta com aviso único. */
2748
+ /** Imagem de uma etapa → id do anexo pela run; sem run, descarta com aviso único. */
2541
2749
  attachImage(image3) {
2542
2750
  if (this.attachmentSink) return this.attachmentSink(image3, nowISO());
2543
2751
  if (!this.warnedNoAttachmentSink) {
2544
2752
  this.warnedNoAttachmentSink = true;
2545
- console.warn("[hobots] imagem de step descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o");
2753
+ console.warn("[hobots] imagem de etapa descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o");
2546
2754
  }
2547
2755
  return void 0;
2548
2756
  }
2757
+ // ── Etapas ────────────────────────────────────────────────────────────────
2549
2758
  /**
2550
- * Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
2551
- * `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
2759
+ * Conecta a checagem de cancelamento chamada antes de cada etapa nova (inclusive antes de
2760
+ * cada nova tentativa) — é assim que `hobots.step()` para uma run cancelada.
2552
2761
  */
2553
2762
  setStepGuard(guard) {
2554
2763
  this.stepGuard = guard;
2555
2764
  }
2556
- createStep(parentStepId, op, description) {
2765
+ /** A etapa aberta por `step()` (ou `items.each`) no contexto async atual, se houver. */
2766
+ currentStep() {
2767
+ return currentStep(this);
2768
+ }
2769
+ /** Faz de `step` a etapa corrente durante `fn` (filhas aninham nela). Sem etapa, só roda `fn`. */
2770
+ scope(step, fn) {
2771
+ if (!step) return fn();
2772
+ return runWithStepScope({ transaction: this, step }, fn);
2773
+ }
2774
+ /** Abre uma etapa sob a corrente (ou direta da transaction). Interno: o bot usa `step()`. */
2775
+ start(op, description) {
2557
2776
  this.stepGuard?.();
2558
- const step = new Step(this, parentStepId, op, description);
2777
+ const parent = this.currentStep();
2778
+ const step = new Step(this, parent ? parent.stepId : this.stepId, op, description);
2559
2779
  this.steps.push(step);
2560
2780
  this.emit("step", "start", stepLabel(op, description));
2561
2781
  return step;
2562
2782
  }
2563
- /** Step direta da transaction. */
2564
- startChild(op, description) {
2565
- return this.createStep(this.stepId, op, description);
2783
+ /**
2784
+ * Registra a etapa `name` executando `fn(etapa)` até dar certo ou esgotar `retries`
2785
+ * tentativas extras (default 1), esperando `retryDelayMs` entre elas (default 5 000).
2786
+ * Resolve com o retorno de `fn`.
2787
+ *
2788
+ * Uma etapa só no waterfall, cobrindo todas as tentativas: `data.attempts` quando houve
2789
+ * mais de uma; `warning` com o motivo quando a última deu certo; `error` (e a exceção
2790
+ * sobe) quando esgotou; `cancelled` em cancelamento/timeout, que nunca repetem.
2791
+ */
2792
+ async step(name, fn, options = {}) {
2793
+ const requested = options.retries ?? DEFAULT_STEP_RETRIES;
2794
+ const totalRetries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_STEP_RETRIES;
2795
+ const step = this.start(void 0, safeString(name));
2796
+ let attempts = 0;
2797
+ let lastFailure;
2798
+ try {
2799
+ const result = await retryWithBackoff(
2800
+ (attempt) => {
2801
+ attempts = attempt;
2802
+ return this.scope(step, async () => fn(step));
2803
+ },
2804
+ {
2805
+ retries: totalRetries,
2806
+ delayMs: (attempt) => resolveDelay(options.retryDelayMs, attempt, DEFAULT_STEP_RETRY_DELAY_MS),
2807
+ signal: options.signal,
2808
+ wait: options.wait,
2809
+ onRetry: (error, attempt, delayMs) => {
2810
+ lastFailure = { error };
2811
+ options.warn?.(
2812
+ `etapa "${step.description}": tentativa ${attempt}/${totalRetries + 1} falhou: ${truncate(errorReason(error), 500)} \u2014 nova tentativa em ${delayMs}ms`
2813
+ );
2814
+ }
2815
+ }
2816
+ );
2817
+ this.noteAttempts(step, attempts);
2818
+ if (lastFailure && step.status === "ok") {
2819
+ step._setStatus("warning", `tentativa ${attempts - 1}/${totalRetries + 1} falhou: ${errorReason(lastFailure.error)}`);
2820
+ }
2821
+ step._close();
2822
+ return result;
2823
+ } catch (error) {
2824
+ this.noteAttempts(step, attempts);
2825
+ if (isTerminalError(error)) step._setStatus("cancelled")._close();
2826
+ else step._failed(error);
2827
+ throw error;
2828
+ }
2566
2829
  }
2567
- setTag(key, value) {
2568
- this.tags[key] = value;
2569
- return this;
2830
+ noteAttempts(step, attempts) {
2831
+ if (attempts > 1) step._setData("attempts", attempts);
2570
2832
  }
2833
+ // ── Metadados ─────────────────────────────────────────────────────────────
2571
2834
  setStatus(status) {
2572
2835
  this.status = status;
2573
2836
  return this;
2574
2837
  }
2575
- setName(name) {
2576
- this.name = name;
2577
- return this;
2578
- }
2838
+ // ── Encerramento ──────────────────────────────────────────────────────────
2579
2839
  /**
2580
- * Envia já as steps raiz finalizadas (e todas as descendentes, fechando as que
2840
+ * Envia já as etapas raiz finalizadas (e todas as descendentes, fechando as que
2581
2841
  * ficaram abertas) como transactions parciais da mesma run (`op` hobots.run.flush,
2582
- * `event_id` novo, até 200 steps cada) e as tira da memória. Steps raiz ainda abertas
2842
+ * `event_id` novo, até 200 etapas cada) e as tira da memória. Etapas raiz ainda abertas
2583
2843
  * ficam para o próximo flush ou para o `finish()`. É assim que um agente sempre
2584
2844
  * ligado (`start()`), cuja run não termina, entrega as etapas enquanto processa.
2585
- * Devolve quantas steps foram enviadas.
2845
+ * Devolve quantas etapas foram enviadas (0 sem client, sem nada terminado ou depois do `finish()`).
2586
2846
  */
2587
2847
  flush() {
2588
- if (this.finished) return 0;
2848
+ if (!this.client || this.finished) return 0;
2589
2849
  const ready = /* @__PURE__ */ new Set();
2590
2850
  for (const step of this.steps) {
2591
2851
  if (step.parentStepId === this.stepId && step.endTimestamp) ready.add(step.stepId);
@@ -2601,15 +2861,13 @@ var Transaction = class {
2601
2861
  remaining.push(step);
2602
2862
  }
2603
2863
  }
2604
- for (const step of batch) step.finish();
2864
+ for (const step of batch) step._close();
2605
2865
  this.steps.splice(0, this.steps.length, ...remaining);
2606
- if (this.client) {
2607
- for (let i = 0; i < batch.length; i += MAX_STEPS) {
2608
- const chunk = batch.slice(i, i + MAX_STEPS);
2609
- const startTimestamp = chunk.reduce((min, step) => step.startTimestamp < min ? step.startTimestamp : min, chunk[0].startTimestamp);
2610
- const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
2611
- this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, startTimestamp, nowISO(), chunk));
2612
- }
2866
+ for (let i = 0; i < batch.length; i += MAX_STEPS) {
2867
+ const chunk = batch.slice(i, i + MAX_STEPS);
2868
+ const startTimestamp = chunk.reduce((min, step) => step.startTimestamp < min ? step.startTimestamp : min, chunk[0].startTimestamp);
2869
+ const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
2870
+ this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, startTimestamp, nowISO(), chunk));
2613
2871
  }
2614
2872
  return batch.length;
2615
2873
  }
@@ -2626,14 +2884,15 @@ var Transaction = class {
2626
2884
  start_timestamp: startTimestamp,
2627
2885
  timestamp,
2628
2886
  steps: steps.map((step) => step.toJSON()),
2629
- tags: Object.keys(this.tags).length > 0 ? { ...this.tags } : void 0,
2887
+ // sem tags próprias: o client mescla as do processo
2888
+ tags: void 0,
2630
2889
  run_id: this.runId
2631
2890
  };
2632
2891
  }
2633
2892
  finish() {
2634
2893
  if (this.finished) return;
2635
2894
  this.finished = true;
2636
- for (const step of this.steps) step.finish();
2895
+ for (const step of this.steps) step._close();
2637
2896
  const endTimestamp = nowISO();
2638
2897
  this.emit(
2639
2898
  "transaction",
@@ -2661,7 +2920,7 @@ var DEFAULT_RETRY_DELAY_MS = 5e3;
2661
2920
  async function executeRun(input) {
2662
2921
  const { run, handler, api, client, logger, tracker } = input;
2663
2922
  const options = handler.options;
2664
- const tx = new Transaction(client, "run", "hobots.run", run.id);
2923
+ const tx = new Transaction(client, "run", "hobots.run", run.id, { echo: !client });
2665
2924
  const reporter = new RunReporter(
2666
2925
  api,
2667
2926
  run.id,
@@ -2695,6 +2954,7 @@ async function executeRun(input) {
2695
2954
  await reporter.finalizeItems();
2696
2955
  tx.setStatus("error");
2697
2956
  tx.finish();
2957
+ captureRunFailure(client, error, reporter);
2698
2958
  reporter.reportFailure(toRunError(error));
2699
2959
  logger.debug(`run ${run.id} falhou: ${String(error)}`);
2700
2960
  }
@@ -2704,6 +2964,11 @@ async function executeRun(input) {
2704
2964
  tracker.remove(reporter);
2705
2965
  }
2706
2966
  }
2967
+ function captureRunFailure(client, error, reporter) {
2968
+ if (!client?.options.captureRunErrors) return;
2969
+ if (reporter.isDead || isCancellation(error) || wasCaptured(error)) return;
2970
+ client.captureException(error, { type: "error", tags: { mechanism: "run_handler", run_id: reporter.runId } });
2971
+ }
2707
2972
  function warnIncompleteProgress(reporter) {
2708
2973
  const snapshot = reporter.context.progress.snapshot;
2709
2974
  if (!snapshot || snapshot.current >= snapshot.total) return;
@@ -2895,6 +3160,96 @@ var TaskAgent = class {
2895
3160
  }
2896
3161
  };
2897
3162
 
3163
+ // src/tasks/single-run.ts
3164
+ var SingleRunRunner = class {
3165
+ constructor(handler, deps) {
3166
+ this.handler = handler;
3167
+ this.deps = deps;
3168
+ this.done = new Promise((resolve2) => {
3169
+ this.resolveDone = resolve2;
3170
+ });
3171
+ }
3172
+ handler;
3173
+ deps;
3174
+ abort = new AbortController();
3175
+ /** Resolve quando a run terminou (ou não pôde começar, ou o runner foi parado antes). */
3176
+ done;
3177
+ resolveDone;
3178
+ async run() {
3179
+ try {
3180
+ const run = await this.openRun();
3181
+ if (!run) return;
3182
+ this.deps.logger.debug(this.opened(run));
3183
+ await executeRun({
3184
+ run,
3185
+ handler: this.handler,
3186
+ api: this.deps.api,
3187
+ client: this.deps.client,
3188
+ logger: this.deps.logger,
3189
+ tracker: this.deps.tracker
3190
+ });
3191
+ } catch (error) {
3192
+ this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
3193
+ } finally {
3194
+ this.resolveDone();
3195
+ }
3196
+ }
3197
+ /** Desiste de obter a run (close() antes dela existir). Uma run já em execução segue pelo tracker. */
3198
+ stop() {
3199
+ this.abort.abort();
3200
+ }
3201
+ };
3202
+
3203
+ // src/tasks/attached.ts
3204
+ var START_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
3205
+ function isTransient(error) {
3206
+ if (!(error instanceof TasksApiError)) return true;
3207
+ return error.status >= 500 || error.status === 429;
3208
+ }
3209
+ function describeRejection(runId, error) {
3210
+ if (error instanceof TasksApiError && error.status === 404) {
3211
+ return `run ${runId} n\xE3o existe na plataforma (404) \u2014 nada a executar`;
3212
+ }
3213
+ if (error instanceof TasksApiError && error.status === 409) {
3214
+ return `run ${runId} n\xE3o pode ser iniciada (409: cancelada, j\xE1 iniciada ou de outra inst\xE2ncia) \u2014 nada a executar`;
3215
+ }
3216
+ return `n\xE3o foi poss\xEDvel iniciar a run ${runId} (${String(error)}) \u2014 nada a executar`;
3217
+ }
3218
+ var AttachedRunner = class extends SingleRunRunner {
3219
+ constructor(runId, handler, deps) {
3220
+ super(handler, deps);
3221
+ this.runId = runId;
3222
+ }
3223
+ runId;
3224
+ opened(run) {
3225
+ return `run ${run.id} iniciada (HOBOTS_RUN_ID)`;
3226
+ }
3227
+ async openRun() {
3228
+ for (let attempt = 0; ; attempt++) {
3229
+ if (this.abort.signal.aborted) return void 0;
3230
+ try {
3231
+ return await this.deps.api.startRun(this.runId, { instance: this.deps.instanceInfo() }, this.abort.signal);
3232
+ } catch (error) {
3233
+ if (this.abort.signal.aborted) return void 0;
3234
+ if (!isTransient(error)) {
3235
+ this.deps.logger.error(describeRejection(this.runId, error));
3236
+ return void 0;
3237
+ }
3238
+ const delay = START_RETRY_DELAYS_MS[attempt];
3239
+ if (delay === void 0) {
3240
+ this.deps.logger.error(
3241
+ `n\xE3o foi poss\xEDvel iniciar a run ${this.runId} ap\xF3s ${attempt + 1} tentativas (${String(error)}) \u2014 desistindo`
3242
+ );
3243
+ return void 0;
3244
+ }
3245
+ const wait = jitter(delay);
3246
+ this.deps.logger.warn(`in\xEDcio da run ${this.runId} falhou (${String(error)}), pr\xF3xima tentativa em ${wait}ms`);
3247
+ await abortableSleep(wait, this.abort.signal);
3248
+ }
3249
+ }
3250
+ }
3251
+ };
3252
+
2898
3253
  // src/tasks/item-columns.ts
2899
3254
  var KEY_RE = /^[a-zA-Z0-9_.-]{1,100}$/;
2900
3255
  var MAX_COLUMNS = WIRE_LIMITS.MAX_ITEM_COLUMNS;
@@ -2932,46 +3287,16 @@ function normalizeItemColumns(input, logger) {
2932
3287
  }
2933
3288
 
2934
3289
  // src/tasks/local.ts
2935
- var LocalRunner = class {
3290
+ var LocalRunner = class extends SingleRunRunner {
3291
+ backoff;
2936
3292
  constructor(handler, deps) {
2937
- this.handler = handler;
2938
- this.deps = deps;
3293
+ super(handler, deps);
2939
3294
  this.backoff = new Backoff(deps.logger, "cria\xE7\xE3o da run");
2940
- this.done = new Promise((resolve2) => {
2941
- this.resolveDone = resolve2;
2942
- });
2943
- }
2944
- handler;
2945
- deps;
2946
- abort = new AbortController();
2947
- backoff;
2948
- /** Resolve quando a run terminou (ou o runner foi parado antes de abri-la). */
2949
- done;
2950
- resolveDone;
2951
- async run() {
2952
- try {
2953
- const run = await this.createRun();
2954
- if (!run) return;
2955
- this.deps.logger.debug(`run ${run.id} aberta (modo start)`);
2956
- await executeRun({
2957
- run,
2958
- handler: this.handler,
2959
- api: this.deps.api,
2960
- client: this.deps.client,
2961
- logger: this.deps.logger,
2962
- tracker: this.deps.tracker
2963
- });
2964
- } catch (error) {
2965
- this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
2966
- } finally {
2967
- this.resolveDone();
2968
- }
2969
3295
  }
2970
- /** Desiste de abrir a run (close() antes da plataforma responder). Uma run já aberta segue pelo tracker. */
2971
- stop() {
2972
- this.abort.abort();
3296
+ opened(run) {
3297
+ return `run ${run.id} aberta (modo start)`;
2973
3298
  }
2974
- async createRun() {
3299
+ async openRun() {
2975
3300
  for (; ; ) {
2976
3301
  if (this.abort.signal.aborted) return void 0;
2977
3302
  try {
@@ -2984,6 +3309,129 @@ var LocalRunner = class {
2984
3309
  }
2985
3310
  };
2986
3311
 
3312
+ // src/tasks/local-transport.ts
3313
+ var LOCAL_RUN_ID = "local";
3314
+ var ITEM_LINE_PREFIX = "[hobots:item]";
3315
+ var SUMMARY_LINE_PREFIX = "[hobots:summary]";
3316
+ var ATTACHMENTS_DISCARDED_WARNING = "imagens e anexos n\xE3o s\xE3o enviados no modo local (descartados)";
3317
+ function unavailable(name) {
3318
+ return new Error(`arquivo "${name}" enviado pela Central n\xE3o est\xE1 dispon\xEDvel no modo local`);
3319
+ }
3320
+ function cell(value) {
3321
+ return (value ?? "").replace(/\r\n|[\t\r\n]/g, " ");
3322
+ }
3323
+ var LocalRunTransport = class {
3324
+ constructor(params = {}, out = (line) => void process.stdout.write(`${line}
3325
+ `)) {
3326
+ this.params = params;
3327
+ this.out = out;
3328
+ }
3329
+ params;
3330
+ out;
3331
+ counts = { succeeded: 0, occurrence: 0, failed: 0 };
3332
+ warnedAttachments = false;
3333
+ /** Status terminal que o pipeline reportou — `undefined` enquanto a run corre (ou se ela nem reportou). */
3334
+ terminalStatus;
3335
+ async updateRun(_runId, update) {
3336
+ if (update.status === "running") return;
3337
+ this.terminalStatus = update.status;
3338
+ if (update.status === "failed" && update.error && update.error.type !== "RunCanceled") {
3339
+ printRunLog("error", `execu\xE7\xE3o falhou \u2014 ${update.error.type ?? "Error"}: ${update.error.message}`);
3340
+ if (update.error.stack) console.error(update.error.stack);
3341
+ }
3342
+ }
3343
+ async updateRunOnce() {
3344
+ }
3345
+ async createItems(_runId, request) {
3346
+ for (const item of request.items) {
3347
+ this.counts[item.status]++;
3348
+ this.out(`${ITEM_LINE_PREFIX} ${item.status} ${cell(item.id)} ${cell(item.message)}`);
3349
+ }
3350
+ return { accepted: request.items.length, duplicates: 0 };
3351
+ }
3352
+ /** Totais por status — chame uma vez, no fim da run, antes da linha de conclusão. */
3353
+ printSummary() {
3354
+ const { succeeded, occurrence, failed } = this.counts;
3355
+ this.out(
3356
+ `${SUMMARY_LINE_PREFIX} total=${succeeded + occurrence + failed} ok=${succeeded} avisos=${occurrence} falhas=${failed} pulados=0`
3357
+ );
3358
+ }
3359
+ async createAttachment(_runId, request) {
3360
+ if (!this.warnedAttachments) {
3361
+ this.warnedAttachments = true;
3362
+ printRunLog("warning", ATTACHMENTS_DISCARDED_WARNING);
3363
+ }
3364
+ return { id: request.id, upload_url: "", headers: {}, expires_in: 0 };
3365
+ }
3366
+ async uploadAttachment() {
3367
+ }
3368
+ async confirmAttachment() {
3369
+ }
3370
+ /** Nome do arquivo pelo id (nos params); sem ele, o próprio id. */
3371
+ fileName(fileId) {
3372
+ return collectFileRefs(this.params).find((ref) => ref.id === fileId)?.name ?? fileId;
3373
+ }
3374
+ async requestFileDownload(_runId, fileId) {
3375
+ throw unavailable(this.fileName(fileId));
3376
+ }
3377
+ async fetchFile(url) {
3378
+ throw unavailable(url);
3379
+ }
3380
+ async confirmFileDownload(_runId, fileId) {
3381
+ throw unavailable(this.fileName(fileId));
3382
+ }
3383
+ };
3384
+
3385
+ // src/tasks/local-run.ts
3386
+ var LOCAL_RUN_EXIT_CODES = { succeeded: 0, failed: 1, canceled: 130 };
3387
+ var LOCAL_RUN_CANCEL_GRACE_MS = 4e3;
3388
+ var LOCAL_RUN_BANNER = "modo local \u2014 execu\xE7\xE3o sem a Central; params de HOBOTS_RUN_PARAMS";
3389
+ var LocalRunRunner = class extends SingleRunRunner {
3390
+ constructor(params, handler, deps) {
3391
+ super(handler, deps);
3392
+ this.params = params;
3393
+ }
3394
+ params;
3395
+ canceled = false;
3396
+ startedAt = Date.now();
3397
+ finished = false;
3398
+ run() {
3399
+ this.startedAt = Date.now();
3400
+ return super.run();
3401
+ }
3402
+ opened() {
3403
+ return "run local iniciada (HOBOTS_LOCAL_RUN)";
3404
+ }
3405
+ async openRun() {
3406
+ if (this.abort.signal.aborted) return void 0;
3407
+ return { id: LOCAL_RUN_ID, params: this.params, form: null, source: "local", created_at: nowISO() };
3408
+ }
3409
+ /** SIGINT/SIGTERM: dispara o `ctx.signal` da run em voo — o handler para no próximo ponto de parada. */
3410
+ cancel() {
3411
+ this.canceled = true;
3412
+ this.deps.tracker.singleActive()?.abort();
3413
+ }
3414
+ /** Desfecho da run: cancelada pelo sinal, senão o status terminal reportado (sem reporte = falha). */
3415
+ get outcome() {
3416
+ if (this.canceled) return "canceled";
3417
+ return this.deps.api.terminalStatus ?? "failed";
3418
+ }
3419
+ /**
3420
+ * Fecha o relato no terminal — `[hobots:summary]` e a linha de conclusão — e devolve o
3421
+ * desfecho. Idempotente: o runtime chama ao fim da run ou quando a graça do cancelamento
3422
+ * esgota com o handler ainda rodando.
3423
+ */
3424
+ finish() {
3425
+ const outcome = this.outcome;
3426
+ if (this.finished) return outcome;
3427
+ this.finished = true;
3428
+ this.deps.api.printSummary();
3429
+ const seconds = Math.round((Date.now() - this.startedAt) / 1e3);
3430
+ printRunLog("info", `execu\xE7\xE3o local conclu\xEDda (${outcome}) em ${seconds}s`);
3431
+ return outcome;
3432
+ }
3433
+ };
3434
+
2987
3435
  // src/tasks/registry.ts
2988
3436
  var RunTracker = class {
2989
3437
  constructor(api) {
@@ -3099,19 +3547,10 @@ function hostname() {
3099
3547
  if (!isNode()) return void 0;
3100
3548
  return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
3101
3549
  }
3102
- function createRuntime(options, handler) {
3103
- const config = resolveConfig({ clientSecret: options.clientSecret });
3104
- const logger = makeLogger(options.debug ?? false);
3105
- const environment = resolveEnvironmentOption(options.environment);
3106
- const client = new Client(
3107
- { environment, release: options.release, tags: options.tags, debug: options.debug },
3108
- config
3109
- );
3110
- const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
3111
- const rt = {
3550
+ function baseRuntime(options, handler, logger, api) {
3551
+ return {
3112
3552
  options: { ...options, clientSecret: void 0 },
3113
- config,
3114
- client,
3553
+ client: void 0,
3115
3554
  logger,
3116
3555
  api,
3117
3556
  handler,
@@ -3120,18 +3559,55 @@ function createRuntime(options, handler) {
3120
3559
  heartbeat: void 0,
3121
3560
  agent: void 0,
3122
3561
  local: void 0,
3562
+ attached: void 0,
3563
+ localRun: void 0,
3564
+ runId: void 0,
3123
3565
  instanceId: resolveInstanceId(options.instanceId),
3124
- environment,
3566
+ environment: void 0,
3125
3567
  pollIntervalMs: void 0,
3126
3568
  booted: false,
3127
3569
  bootScheduled: false,
3128
3570
  bootPromise: void 0,
3129
- uninstallGlobalHandlers: options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0,
3571
+ uninstallGlobalHandlers: void 0,
3130
3572
  uninstallShutdown: void 0
3131
3573
  };
3574
+ }
3575
+ function createRuntime(options, handler, logger) {
3576
+ const config = resolveConfig({ clientSecret: options.clientSecret });
3577
+ const environment = resolveEnvironmentOption(options.environment);
3578
+ const client = new Client(
3579
+ {
3580
+ environment,
3581
+ release: options.release,
3582
+ tags: options.tags,
3583
+ debug: options.debug,
3584
+ captureRunErrors: options.captureUnhandled !== false
3585
+ },
3586
+ config
3587
+ );
3588
+ const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
3589
+ const rt = baseRuntime(options, handler, logger, api);
3590
+ rt.client = client;
3591
+ rt.environment = environment;
3592
+ rt.uninstallGlobalHandlers = options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0;
3132
3593
  logger.debug("inicializado", { endpoint: config.ingestEndpoint });
3133
3594
  return rt;
3134
3595
  }
3596
+ function createLocalRunRuntime(options, handler, logger) {
3597
+ let params;
3598
+ try {
3599
+ params = resolveLocalRunParams();
3600
+ } catch (error) {
3601
+ printRunLog("error", error instanceof Error ? error.message : String(error));
3602
+ process.exit(2);
3603
+ throw error;
3604
+ }
3605
+ const transport = new LocalRunTransport(params);
3606
+ const rt = baseRuntime(options, handler, logger, transport);
3607
+ rt.localRun = new LocalRunRunner(params, handler, { api: transport, client: void 0, logger, tracker: rt.tracker });
3608
+ printRunLog("info", LOCAL_RUN_BANNER);
3609
+ return rt;
3610
+ }
3135
3611
  function validateHandler(handler, fn) {
3136
3612
  if (typeof handler !== "function") {
3137
3613
  throw new Error(`[hobots] ${fn}(): o handler precisa ser uma fun\xE7\xE3o \u2014 ${fn}(async (params) => { ... })`);
@@ -3174,9 +3650,12 @@ function instanceInfo(rt) {
3174
3650
  machine: rt.sampler?.sample()
3175
3651
  };
3176
3652
  }
3653
+ function platformApi(rt) {
3654
+ return rt.api;
3655
+ }
3177
3656
  function ensureAgent(rt) {
3178
3657
  rt.agent ??= new TaskAgent({
3179
- api: rt.api,
3658
+ api: platformApi(rt),
3180
3659
  client: rt.client,
3181
3660
  logger: rt.logger,
3182
3661
  handler: rt.handler,
@@ -3188,7 +3667,7 @@ function ensureAgent(rt) {
3188
3667
  }
3189
3668
  function launchLocal(rt) {
3190
3669
  const runner = new LocalRunner(rt.handler, {
3191
- api: rt.api,
3670
+ api: platformApi(rt),
3192
3671
  client: rt.client,
3193
3672
  logger: rt.logger,
3194
3673
  tracker: rt.tracker,
@@ -3197,7 +3676,57 @@ function launchLocal(rt) {
3197
3676
  rt.local = runner;
3198
3677
  void runner.run();
3199
3678
  }
3679
+ function launchAttached(rt, runId) {
3680
+ const runner = new AttachedRunner(runId, rt.handler, {
3681
+ api: platformApi(rt),
3682
+ client: rt.client,
3683
+ logger: rt.logger,
3684
+ tracker: rt.tracker,
3685
+ instanceInfo: () => instanceInfo(rt)
3686
+ });
3687
+ rt.attached = runner;
3688
+ void runner.run();
3689
+ void runner.done.then(() => {
3690
+ if (runtime === rt) void close();
3691
+ });
3692
+ }
3693
+ function launchLocalRun(rt, runner) {
3694
+ let finished = false;
3695
+ const finish = () => {
3696
+ if (finished) return;
3697
+ finished = true;
3698
+ rt.uninstallShutdown?.();
3699
+ rt.uninstallShutdown = void 0;
3700
+ const code = LOCAL_RUN_EXIT_CODES[runner.finish()];
3701
+ if (runtime === rt) void close(0);
3702
+ process.stdout.write("", () => process.exit(code));
3703
+ };
3704
+ if (rt.options.installExitHandlers !== false) {
3705
+ let signaled = false;
3706
+ const onSignal = (signal) => {
3707
+ if (externalListenerCount(signal) > 0 || signaled) return;
3708
+ signaled = true;
3709
+ runner.cancel();
3710
+ void Promise.race([runner.done, sleep(LOCAL_RUN_CANCEL_GRACE_MS)]).then(finish);
3711
+ };
3712
+ const onInt = markInternal(() => onSignal("SIGINT"));
3713
+ const onTerm = markInternal(() => onSignal("SIGTERM"));
3714
+ process.on("SIGINT", onInt);
3715
+ process.on("SIGTERM", onTerm);
3716
+ rt.uninstallShutdown = () => {
3717
+ process.off("SIGINT", onInt);
3718
+ process.off("SIGTERM", onTerm);
3719
+ };
3720
+ }
3721
+ void runner.run();
3722
+ void runner.done.then(finish);
3723
+ }
3200
3724
  async function boot(rt) {
3725
+ if (rt.localRun) {
3726
+ rt.booted = true;
3727
+ launchLocalRun(rt, rt.localRun);
3728
+ return;
3729
+ }
3201
3730
  if (!rt.instanceId) {
3202
3731
  const id = await fallbackInstanceId();
3203
3732
  if (runtime !== rt) return;
@@ -3216,8 +3745,9 @@ async function boot(rt) {
3216
3745
  if (rt.options.installExitHandlers !== false) {
3217
3746
  rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
3218
3747
  }
3219
- if (rt.handler.mode === "remote") ensureAgent(rt).start();
3220
- else launchLocal(rt);
3748
+ if (rt.handler.mode === "local") launchLocal(rt);
3749
+ else if (rt.runId) launchAttached(rt, rt.runId);
3750
+ else ensureAgent(rt).start();
3221
3751
  }
3222
3752
  function scheduleBoot(rt) {
3223
3753
  if (rt.bootScheduled) return;
@@ -3233,8 +3763,12 @@ function startAgent(fn, mode, handler, options) {
3233
3763
  }
3234
3764
  const logger = makeLogger(options.debug ?? false);
3235
3765
  const agentHandler = { mode, handler, options: resolveRunOptions(options, mode, logger) };
3236
- const rt = createRuntime(options, agentHandler);
3237
- if (mode === "remote" && options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
3766
+ loadDotenv();
3767
+ const rt = isLocalRun() ? createLocalRunRuntime(options, agentHandler, logger) : createRuntime(options, agentHandler, logger);
3768
+ if (!rt.localRun && mode === "remote") {
3769
+ if (options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
3770
+ rt.runId = resolveRunId(options.runId);
3771
+ }
3238
3772
  runtime = rt;
3239
3773
  scheduleBoot(rt);
3240
3774
  }
@@ -3257,10 +3791,10 @@ function forwardAttachment(rt, attachment, message, type) {
3257
3791
  const reporter = currentReporter();
3258
3792
  if (!reporter) {
3259
3793
  rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o");
3260
- return;
3794
+ return void 0;
3261
3795
  }
3262
3796
  const withMeta = typeof attachment === "object" && !isImageAttachment(attachment) && "image" in attachment ? image(attachment.image, { caption: attachment.caption, contentType: attachment.contentType }) : attachment;
3263
- reporter.log(toRunLogLevel(type), message, withMeta);
3797
+ return reporter.log(toRunLogLevel(type), message, withMeta);
3264
3798
  }
3265
3799
  function captureException(error, hint) {
3266
3800
  const rt = runtime;
@@ -3271,11 +3805,19 @@ function captureException(error, hint) {
3271
3805
  }
3272
3806
  return void 0;
3273
3807
  }
3808
+ let attachmentId;
3274
3809
  if (hint?.attachment) {
3275
3810
  const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
3276
- forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
3811
+ attachmentId = forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
3277
3812
  }
3278
- return rt.client.captureException(error, hint);
3813
+ if (!rt.client) {
3814
+ rt.logger.debug("captureException ignorado no modo local (sem Central)");
3815
+ return void 0;
3816
+ }
3817
+ const runId = currentReporter()?.runId;
3818
+ const withRun = runId ? { ...hint, tags: { run_id: runId, ...hint?.tags } } : hint;
3819
+ const contexts = runId && attachmentId ? { run: { id: runId, attachments: [attachmentId] } } : void 0;
3820
+ return rt.client.captureException(error, withRun, contexts);
3279
3821
  }
3280
3822
  async function close(timeoutMs = 3e4) {
3281
3823
  const rt = runtime;
@@ -3284,6 +3826,8 @@ async function close(timeoutMs = 3e4) {
3284
3826
  await rt.bootPromise;
3285
3827
  if (rt.agent) await rt.agent.stop(0);
3286
3828
  rt.local?.stop();
3829
+ rt.attached?.stop();
3830
+ rt.localRun?.stop();
3287
3831
  const idle = await rt.tracker.waitIdle(timeoutMs);
3288
3832
  if (!idle) {
3289
3833
  rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
@@ -3292,7 +3836,7 @@ async function close(timeoutMs = 3e4) {
3292
3836
  rt.heartbeat?.stop();
3293
3837
  rt.uninstallShutdown?.();
3294
3838
  rt.uninstallGlobalHandlers?.();
3295
- const flushed = await rt.client.close(timeoutMs);
3839
+ const flushed = rt.client ? await rt.client.close(timeoutMs) : true;
3296
3840
  return idle && flushed;
3297
3841
  }
3298
3842
 
@@ -3321,11 +3865,13 @@ var noop = () => void 0;
3321
3865
  var fallbackEachSink = {
3322
3866
  report: noop,
3323
3867
  startStep: () => void 0,
3868
+ stepScope: (_step, fn) => fn(),
3324
3869
  declareTotal: noop,
3325
3870
  advance: noop,
3326
3871
  checkCanceled: noop,
3327
3872
  isAborted: () => false,
3328
- logError: (message) => printRunLog("error", message)
3873
+ logError: (message) => printRunLog("error", message),
3874
+ flush: () => Promise.resolve()
3329
3875
  };
3330
3876
  var fallbackItems = {
3331
3877
  add: noop,
@@ -3344,7 +3890,6 @@ var fallbackFiles = {
3344
3890
  read: outsideRunFiles
3345
3891
  };
3346
3892
  var neverAborts = new AbortController().signal;
3347
- var detachedTransaction;
3348
3893
  function resolve(member) {
3349
3894
  const reporter = currentReporter();
3350
3895
  if (reporter) return reporter.context;
@@ -3365,8 +3910,12 @@ var hobots = Object.defineProperties({}, {
3365
3910
  items: getter("items", () => fallbackItems),
3366
3911
  progress: getter("progress", () => fallbackProgress),
3367
3912
  files: getter("files", () => fallbackFiles),
3368
- transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
3369
3913
  signal: getter("signal", () => neverAborts),
3914
+ step: method((name, fn, options) => {
3915
+ const ctx = resolve("step");
3916
+ if (ctx) return ctx.step(name, fn, options);
3917
+ return new Transaction(void 0, "hobots.detached").step(name, fn, options);
3918
+ }),
3370
3919
  throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
3371
3920
  sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
3372
3921
  flush: method((timeoutMs) => resolve("flush")?.flush(timeoutMs) ?? Promise.resolve()),
@@ -3401,6 +3950,7 @@ var image2 = hobots.image;
3401
3950
  var isFileRef2 = hobots.isFileRef;
3402
3951
 
3403
3952
  // src/index.ts
3953
+ loadDotenv();
3404
3954
  var src_default = hobots;
3405
3955
  export {
3406
3956
  captureException2 as captureException,