hobots 4.1.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,62 +2,9 @@ import {
2
2
  WIRE_LIMITS
3
3
  } from "./chunk-HKCDMUA3.js";
4
4
 
5
- // src/stacktrace.ts
6
- var MAX_FRAMES = 50;
7
- var MAX_FILENAME_CHARS = 1024;
8
- var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
9
- var LOCATION_RE = /^(.*?):(\d+):(\d+)$/;
10
- function isInApp(filename) {
11
- if (!filename) return false;
12
- if (filename.startsWith("node:")) return false;
13
- if (filename.startsWith("internal/")) return false;
14
- if (filename.includes("node_modules")) return false;
15
- if (filename === "<anonymous>") return false;
16
- return true;
17
- }
18
- function parseStacktrace(stack) {
19
- if (!stack) return [];
20
- const frames = [];
21
- for (const line of stack.split("\n")) {
22
- if (frames.length >= MAX_FRAMES) break;
23
- const match = FRAME_RE.exec(line);
24
- if (!match || !line.trimStart().startsWith("at ")) continue;
25
- const [, fn, location] = match;
26
- const loc = LOCATION_RE.exec(location ?? "");
27
- const filename = loc ? loc[1] : location;
28
- frames.push({
29
- function: fn || "<anonymous>",
30
- filename: filename ? filename.slice(0, MAX_FILENAME_CHARS) : void 0,
31
- lineno: loc?.[2] ? Number(loc[2]) : void 0,
32
- colno: loc?.[3] ? Number(loc[3]) : void 0,
33
- in_app: isInApp(filename)
34
- });
35
- }
36
- return frames;
37
- }
38
-
39
- // src/scrub.ts
40
- var SECRET_PATTERNS = [
41
- // client secret da Hobots
42
- [/hb_[a-f0-9]{16,}/gi, "[REDACTED]"],
43
- // tokens bearer em headers/mensagens
44
- [/\b(authorization:?\s*)?bearer\s+[a-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]"],
45
- // chaves no formato sk-… (OpenAI e afins)
46
- [/\bsk-[a-z0-9_-]{16,}\b/gi, "[REDACTED]"],
47
- // credenciais embutidas em URL (https://user:pass@host)
48
- [/\/\/[^/\s:@]+:[^/\s@]+@/g, "//[REDACTED]@"]
49
- ];
50
- function scrubSecrets(text) {
51
- let out = text;
52
- for (const [pattern, replacement] of SECRET_PATTERNS) {
53
- out = out.replace(pattern, replacement);
54
- }
55
- return out;
56
- }
57
-
58
5
  // src/utils.ts
59
6
  var SDK_NAME = "hobots";
60
- var SDK_VERSION = true ? "4.1.0" : "0.0.0-dev";
7
+ var SDK_VERSION = true ? "4.5.0" : "0.0.0-dev";
61
8
  function isNode() {
62
9
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
63
10
  }
@@ -139,13 +86,13 @@ async function fetchWithRetry(send, options) {
139
86
  } catch (error) {
140
87
  retriable = error;
141
88
  }
89
+ const failure = retriable instanceof Error ? retriable : new Error(String(retriable));
142
90
  const base = delays[attempt];
143
- if (base === void 0) {
144
- throw retriable instanceof Error ? retriable : new Error(String(retriable));
145
- }
91
+ if (base === void 0 || options.signal?.aborted) throw failure;
146
92
  const delay = serverDelay ?? jitter(base);
147
93
  options.logger.warn(`${options.label}: falha (${String(retriable)}), retry em ${delay}ms`);
148
- await sleep(delay);
94
+ await (options.signal ? abortableSleep(delay, options.signal) : sleep(delay));
95
+ if (options.signal?.aborted) throw failure;
149
96
  }
150
97
  }
151
98
  var MAX_RETRY_AFTER_MS = 6e4;
@@ -205,6 +152,104 @@ function colorize(text, color) {
205
152
  return `${ANSI[color]}${text}${ANSI.reset}`;
206
153
  }
207
154
 
155
+ // src/env.ts
156
+ var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
157
+ var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
158
+ var RUN_ID_ENV = "HOBOTS_RUN_ID";
159
+ var dotenvLoaded = false;
160
+ function loadDotenv() {
161
+ if (dotenvLoaded) return;
162
+ dotenvLoaded = true;
163
+ if (!isNode() || typeof process.loadEnvFile !== "function") return;
164
+ try {
165
+ process.loadEnvFile(".env");
166
+ } catch (err) {
167
+ const code = err?.code;
168
+ if (code === "ENOENT") return;
169
+ console.warn(`[hobots] n\xE3o foi poss\xEDvel ler o .env da pasta de trabalho (${typeof code === "string" ? code : "erro desconhecido"}) \u2014 seguindo s\xF3 com as vari\xE1veis do ambiente`);
170
+ }
171
+ }
172
+ function resolveInstanceId(option) {
173
+ const fromOption = option?.trim();
174
+ if (fromOption) return fromOption;
175
+ if (typeof process === "undefined" || !process.env) return void 0;
176
+ return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
177
+ }
178
+ function resolveRunId(option) {
179
+ const fromOption = option?.trim();
180
+ if (fromOption) return fromOption;
181
+ if (typeof process === "undefined" || !process.env) return void 0;
182
+ return process.env[RUN_ID_ENV]?.trim() || void 0;
183
+ }
184
+ function resolveEnvironmentOption(option) {
185
+ const fromOption = option?.trim();
186
+ if (fromOption) return fromOption;
187
+ if (typeof process === "undefined" || !process.env) return void 0;
188
+ return process.env[ENVIRONMENT_ENV]?.trim() || void 0;
189
+ }
190
+ async function fallbackInstanceId() {
191
+ if (!isNode()) return "browser";
192
+ try {
193
+ const os = await import("os");
194
+ return os.hostname() || "unknown-host";
195
+ } catch {
196
+ return "unknown-host";
197
+ }
198
+ }
199
+
200
+ // src/stacktrace.ts
201
+ var MAX_FRAMES = 50;
202
+ var MAX_FILENAME_CHARS = 1024;
203
+ var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
204
+ var LOCATION_RE = /^(.*?):(\d+):(\d+)$/;
205
+ function isInApp(filename) {
206
+ if (!filename) return false;
207
+ if (filename.startsWith("node:")) return false;
208
+ if (filename.startsWith("internal/")) return false;
209
+ if (filename.includes("node_modules")) return false;
210
+ if (filename === "<anonymous>") return false;
211
+ return true;
212
+ }
213
+ function parseStacktrace(stack) {
214
+ if (!stack) return [];
215
+ const frames = [];
216
+ for (const line of stack.split("\n")) {
217
+ if (frames.length >= MAX_FRAMES) break;
218
+ const match = FRAME_RE.exec(line);
219
+ if (!match || !line.trimStart().startsWith("at ")) continue;
220
+ const [, fn, location] = match;
221
+ const loc = LOCATION_RE.exec(location ?? "");
222
+ const filename = loc ? loc[1] : location;
223
+ frames.push({
224
+ function: fn || "<anonymous>",
225
+ filename: filename ? filename.slice(0, MAX_FILENAME_CHARS) : void 0,
226
+ lineno: loc?.[2] ? Number(loc[2]) : void 0,
227
+ colno: loc?.[3] ? Number(loc[3]) : void 0,
228
+ in_app: isInApp(filename)
229
+ });
230
+ }
231
+ return frames;
232
+ }
233
+
234
+ // src/scrub.ts
235
+ var SECRET_PATTERNS = [
236
+ // client secret da Hobots
237
+ [/hb_[a-f0-9]{16,}/gi, "[REDACTED]"],
238
+ // tokens bearer em headers/mensagens
239
+ [/\b(authorization:?\s*)?bearer\s+[a-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]"],
240
+ // chaves no formato sk-… (OpenAI e afins)
241
+ [/\bsk-[a-z0-9_-]{16,}\b/gi, "[REDACTED]"],
242
+ // credenciais embutidas em URL (https://user:pass@host)
243
+ [/\/\/[^/\s:@]+:[^/\s@]+@/g, "//[REDACTED]@"]
244
+ ];
245
+ function scrubSecrets(text) {
246
+ let out = text;
247
+ for (const [pattern, replacement] of SECRET_PATTERNS) {
248
+ out = out.replace(pattern, replacement);
249
+ }
250
+ return out;
251
+ }
252
+
208
253
  // src/eventbuilder.ts
209
254
  var MAX_EXCEPTION_VALUE_CHARS = 8192;
210
255
  function eventFromException(error, type = "error") {
@@ -356,6 +401,10 @@ function clipFingerprint(fingerprint) {
356
401
  if (fingerprint === void 0) return void 0;
357
402
  return fingerprint.slice(0, MAX_FINGERPRINT_ITEMS).map((item) => truncate(String(item), MAX_FINGERPRINT_ITEM_CHARS));
358
403
  }
404
+ var captured = /* @__PURE__ */ new WeakSet();
405
+ function wasCaptured(error) {
406
+ return typeof error === "object" && error !== null && captured.has(error);
407
+ }
359
408
  function runtimeContexts() {
360
409
  if (isNode()) {
361
410
  return {
@@ -407,6 +456,7 @@ var Client = class {
407
456
  return `Client { baseUrl: '${this.#config.baseUrl}' }`;
408
457
  }
409
458
  captureException(error, hint) {
459
+ if (typeof error === "object" && error !== null) captured.add(error);
410
460
  return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
411
461
  }
412
462
  captureEvent(event, hint) {
@@ -514,12 +564,12 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
514
564
  const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
515
565
  if (!key) {
516
566
  throw new Error(
517
- `${prefix} Client Secret ausente \u2014 defina a env ${CLIENT_SECRET_ENV} (ou passe clientSecret nas op\xE7\xF5es do register/start); copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
567
+ `${prefix} Client Secret ausente \u2014 defina a env ${CLIENT_SECRET_ENV} (ou passe clientSecret nas op\xE7\xF5es do register/start); copie o Client Secret na Central: abra o agente \u2192 Configura\xE7\xF5es \u2192 agente \u2192 aba credenciais`
518
568
  );
519
569
  }
520
570
  if (!SECRET_PATTERN.test(key)) {
521
571
  throw new Error(
522
- `${prefix} ${origin} com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
572
+ `${prefix} ${origin} com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret na Central: abra o agente \u2192 Configura\xE7\xF5es \u2192 agente \u2192 aba credenciais`
523
573
  );
524
574
  }
525
575
  const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
@@ -531,31 +581,6 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
531
581
  };
532
582
  }
533
583
 
534
- // src/env.ts
535
- var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
536
- var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
537
- function resolveInstanceId(option) {
538
- const fromOption = option?.trim();
539
- if (fromOption) return fromOption;
540
- if (typeof process === "undefined" || !process.env) return void 0;
541
- return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
542
- }
543
- function resolveEnvironmentOption(option) {
544
- const fromOption = option?.trim();
545
- if (fromOption) return fromOption;
546
- if (typeof process === "undefined" || !process.env) return void 0;
547
- return process.env[ENVIRONMENT_ENV]?.trim() || void 0;
548
- }
549
- async function fallbackInstanceId() {
550
- if (!isNode()) return "browser";
551
- try {
552
- const os = await import("os");
553
- return os.hostname() || "unknown-host";
554
- } catch {
555
- return "unknown-host";
556
- }
557
- }
558
-
559
584
  // src/heartbeat.ts
560
585
  var DEFAULT_INTERVAL_MS = WIRE_LIMITS.HEARTBEAT_DEFAULT_INTERVAL_MS;
561
586
  var MIN_REPORTED_INTERVAL_MS = WIRE_LIMITS.HEARTBEAT_MIN_INTERVAL_MS;
@@ -831,6 +856,7 @@ var ATTACHMENT_TERMINAL_STATUSES = [429, 501, 503];
831
856
  var ITEMS_TERMINAL_STATUSES = [429];
832
857
  var UPLOAD_TIMEOUT_MS = 3e4;
833
858
  var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
859
+ var FILE_RETRY_DELAYS_MS = [1e3, 4e3];
834
860
  var TasksApiError = class extends Error {
835
861
  constructor(message, status) {
836
862
  super(message);
@@ -916,6 +942,42 @@ var TasksApi = class {
916
942
  created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
917
943
  };
918
944
  }
945
+ /**
946
+ * Anexa o processo a uma run já reivindicada para esta instância (env HOBOTS_RUN_ID):
947
+ * POST /v1/runs/:id/start. Um request só — quem decide o retry é o AttachedRunner.
948
+ * 404 (não existe) e 409 (cancelada, já iniciada ou de outra instância) viram
949
+ * TasksApiError com o status.
950
+ */
951
+ async startRun(runId, request, signal) {
952
+ const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
953
+ let response;
954
+ try {
955
+ response = await fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/start`, {
956
+ redirect: "error",
957
+ method: "POST",
958
+ headers: this.headers(),
959
+ body: JSON.stringify(request),
960
+ signal: timeoutSignal
961
+ });
962
+ } finally {
963
+ cleanup();
964
+ }
965
+ if (!response.ok) {
966
+ throw new TasksApiError(`in\xEDcio da run falhou (HTTP ${response.status})`, response.status);
967
+ }
968
+ const body = await response.json().catch(() => null);
969
+ if (typeof body?.id !== "string") {
970
+ throw new TasksApiError("resposta inv\xE1lida do in\xEDcio da run (id ausente)", 0);
971
+ }
972
+ const form = body.form;
973
+ return {
974
+ id: body.id,
975
+ params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
976
+ form: typeof form?.id === "string" && typeof form.name === "string" ? { id: form.id, name: form.name } : null,
977
+ source: typeof body.source === "string" ? body.source : "hobots",
978
+ created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
979
+ };
980
+ }
919
981
  /** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
920
982
  parsePollResponse(body, max) {
921
983
  const runs = body?.runs;
@@ -1018,7 +1080,13 @@ var TasksApi = class {
1018
1080
  signal: timeoutSignal
1019
1081
  })
1020
1082
  ),
1021
- { logger: this.logger, label: `anexo da run ${runId}`, noRetryStatuses: ATTACHMENT_TERMINAL_STATUSES }
1083
+ {
1084
+ logger: this.logger,
1085
+ label: `anexo da run ${runId}`,
1086
+ noRetryStatuses: ATTACHMENT_TERMINAL_STATUSES,
1087
+ // run abortada (timeout, 409, kill): o create em voo rejeita por abort e não entra na ladder
1088
+ signal
1089
+ }
1022
1090
  );
1023
1091
  if (!response.ok) {
1024
1092
  throw new TasksApiError(`cria\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
@@ -1052,7 +1120,7 @@ var TasksApi = class {
1052
1120
  signal: timeoutSignal
1053
1121
  })
1054
1122
  ),
1055
- { delays: UPLOAD_RETRY_DELAYS_MS, logger: this.logger, label: "upload de anexo" }
1123
+ { delays: UPLOAD_RETRY_DELAYS_MS, logger: this.logger, label: "upload de anexo", signal }
1056
1124
  );
1057
1125
  if (!response.ok) {
1058
1126
  throw new TasksApiError(`upload de anexo rejeitado (HTTP ${response.status})`, response.status);
@@ -1074,6 +1142,74 @@ var TasksApi = class {
1074
1142
  throw new TasksApiError(`confirma\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
1075
1143
  }
1076
1144
  }
1145
+ /**
1146
+ * Pede a URL pré-assinada de GET de um arquivo do formulário. 404 (não existe nesta run)
1147
+ * e 410 (já baixado/expirado) viram `TasksApiError` com o status — quem chama traduz.
1148
+ */
1149
+ async requestFileDownload(runId, fileId, signal) {
1150
+ const response = await fetchWithRetry(
1151
+ () => withRequestTimeout(
1152
+ UPDATE_TIMEOUT_MS,
1153
+ signal,
1154
+ (timeoutSignal) => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/files/${encodeURIComponent(fileId)}/download`, {
1155
+ redirect: "error",
1156
+ method: "POST",
1157
+ headers: this.headers(),
1158
+ signal: timeoutSignal
1159
+ })
1160
+ ),
1161
+ { delays: FILE_RETRY_DELAYS_MS, logger: this.logger, label: `download do arquivo ${fileId}`, signal }
1162
+ );
1163
+ if (!response.ok) {
1164
+ throw new TasksApiError(`download de arquivo rejeitado (HTTP ${response.status})`, response.status);
1165
+ }
1166
+ const body = await response.json().catch(() => null);
1167
+ if (typeof body?.url !== "string") {
1168
+ throw new TasksApiError("resposta inv\xE1lida do download de arquivo (url ausente)", 0);
1169
+ }
1170
+ return {
1171
+ url: body.url,
1172
+ expires_in: typeof body.expires_in === "number" ? body.expires_in : 0,
1173
+ name: typeof body.name === "string" ? body.name : "",
1174
+ size: typeof body.size === "number" ? body.size : -1,
1175
+ content_type: typeof body.content_type === "string" ? body.content_type : ""
1176
+ };
1177
+ }
1178
+ /**
1179
+ * GET do binário direto no S3 — deliberadamente sem `authorization` (a autorização
1180
+ * está na assinatura da URL; mandar o client secret para fora da API seria vazamento).
1181
+ * Devolve a Response com o corpo ainda não lido: quem chama faz o streaming para o disco.
1182
+ */
1183
+ async fetchFile(url, signal) {
1184
+ const response = await fetchWithRetry(() => fetch(url, { redirect: "error", method: "GET", signal }), {
1185
+ delays: FILE_RETRY_DELAYS_MS,
1186
+ logger: this.logger,
1187
+ label: "download de arquivo",
1188
+ signal
1189
+ });
1190
+ if (!response.ok) {
1191
+ throw new TasksApiError(`download de arquivo falhou (HTTP ${response.status})`, response.status);
1192
+ }
1193
+ return response;
1194
+ }
1195
+ /**
1196
+ * Confirma o download: o servidor apaga o objeto do S3 na hora. Idempotente (204);
1197
+ * 404/410 também significam "não há mais nada a apagar" e contam como confirmado.
1198
+ */
1199
+ async confirmFileDownload(runId, fileId) {
1200
+ const response = await fetchWithRetry(
1201
+ () => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/files/${encodeURIComponent(fileId)}`, {
1202
+ redirect: "error",
1203
+ method: "DELETE",
1204
+ headers: this.headers(),
1205
+ signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
1206
+ }),
1207
+ { delays: FILE_RETRY_DELAYS_MS, logger: this.logger, label: `confirma\xE7\xE3o do arquivo ${fileId}` }
1208
+ );
1209
+ if (!response.ok && response.status !== 404 && response.status !== 410) {
1210
+ throw new TasksApiError(`confirma\xE7\xE3o de download rejeitada (HTTP ${response.status})`, response.status);
1211
+ }
1212
+ }
1077
1213
  };
1078
1214
 
1079
1215
  // src/tasks/backoff.ts
@@ -1099,7 +1235,7 @@ var Backoff = class {
1099
1235
  this.authFailures++;
1100
1236
  if (this.authFailures === 1 || this.authFailures % 10 === 0) {
1101
1237
  this.logger.error(
1102
- error.status === 402 ? "agente fora do limite do plano (402) \u2014 fa\xE7a upgrade em Plano e cobran\xE7a para reativ\xE1-lo" : "chave rejeitada (401) \u2014 verifique o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes"
1238
+ error.status === 402 ? "agente fora do limite do plano (402) \u2014 fa\xE7a upgrade em Plano e cobran\xE7a para reativ\xE1-lo" : "chave rejeitada (401) \u2014 verifique o Client Secret na Central: abra o agente \u2192 Configura\xE7\xF5es \u2192 agente \u2192 aba credenciais"
1103
1239
  );
1104
1240
  }
1105
1241
  return Math.max(this.floorMs, AUTH_FAILURE_INTERVAL_MS);
@@ -1125,6 +1261,9 @@ var TERMINAL_ERROR_NAMES = /* @__PURE__ */ new Set(["RunCanceled", "AbortError",
1125
1261
  function isTerminalError(error) {
1126
1262
  return error instanceof Error && TERMINAL_ERROR_NAMES.has(error.name);
1127
1263
  }
1264
+ function isCancellation(error) {
1265
+ return error instanceof Error && (error.name === "RunCanceled" || error.name === "AbortError");
1266
+ }
1128
1267
 
1129
1268
  // src/tasks/each.ts
1130
1269
  function thrownMessage(error) {
@@ -1190,16 +1329,15 @@ async function eachItem(list, handler, options, sink) {
1190
1329
  if (!handle.reported) handle.succeeded();
1191
1330
  } catch (error) {
1192
1331
  if (isTerminalError(error) || sink.isAborted()) {
1193
- step?.setStatus("cancelled");
1194
- step?.finish();
1332
+ step?.setStatus("cancelled").succeeded();
1195
1333
  throw error;
1196
1334
  }
1197
1335
  const message = thrownMessage(error);
1198
1336
  if (!handle.reported) handle.failed(message);
1199
- step?.setError(error);
1337
+ step?.failed(error);
1200
1338
  sink.logError(`item ${description} falhou: ${message}`);
1201
1339
  }
1202
- step?.finish();
1340
+ step?.succeeded();
1203
1341
  if (options.progress !== false) sink.advance();
1204
1342
  result.total += 1;
1205
1343
  result[handle.status ?? "succeeded"] += 1;
@@ -1208,6 +1346,255 @@ async function eachItem(list, handler, options, sink) {
1208
1346
  return result;
1209
1347
  }
1210
1348
 
1349
+ // src/tasks/files.ts
1350
+ var LOCAL_FILES_DIR_ENV = "HOBOTS_LOCAL_FILES_DIR";
1351
+ var FILE_DOWNLOAD_IDLE_TIMEOUT_MS = 6e4;
1352
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1353
+ function isFileRef(value) {
1354
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1355
+ const ref = value;
1356
+ return typeof ref.id === "string" && typeof ref.name === "string" && typeof ref.size === "number" && typeof ref.contentType === "string" && (ref.storage === "s3" || ref.storage === "local");
1357
+ }
1358
+ function fileField(params, field) {
1359
+ if (!Object.prototype.hasOwnProperty.call(params, field)) return void 0;
1360
+ const value = params[field];
1361
+ if (isFileRef(value)) return value;
1362
+ if (Array.isArray(value) && value.every(isFileRef)) return value;
1363
+ return void 0;
1364
+ }
1365
+ function collectFileRefs(params) {
1366
+ const refs = [];
1367
+ for (const field of Object.keys(params)) {
1368
+ const value = fileField(params, field);
1369
+ if (value !== void 0) refs.push(...Array.isArray(value) ? value : [value]);
1370
+ }
1371
+ return refs;
1372
+ }
1373
+ function safeFileName(name, fallback) {
1374
+ const cleaned = name.replace(/^.*[\\/]/, "").replace(/[\u0000-\u001f<>:"|?*]/g, "_").trim();
1375
+ return cleaned === "" || cleaned === "." || cleaned === ".." ? fallback : cleaned;
1376
+ }
1377
+ function isInsideDir(root, candidate, sep) {
1378
+ const base = root.endsWith(sep) ? root : root + sep;
1379
+ return candidate.startsWith(base) && candidate.length > base.length;
1380
+ }
1381
+ function unavailableLocal(ref) {
1382
+ return new Error(`arquivo "${ref.name}" enviado pelo Studio de outro computador ou j\xE1 consumido`);
1383
+ }
1384
+ function errorCode(error) {
1385
+ return error?.code;
1386
+ }
1387
+ var RunFileStore = class {
1388
+ constructor(deps) {
1389
+ this.deps = deps;
1390
+ this.files = {
1391
+ list: () => collectFileRefs(deps.params),
1392
+ get: (field) => fileField(deps.params, field),
1393
+ // async: erro de uso (campo sem arquivo, valor inválido) chega como Promise rejeitada, nunca síncrono
1394
+ download: async (refOrField, options) => this.download(this.single(refOrField), options?.dest),
1395
+ downloadAll: async (field, options) => this.downloadAll(field, options?.dest),
1396
+ read: async (refOrField) => this.read(this.single(refOrField))
1397
+ };
1398
+ }
1399
+ deps;
1400
+ files;
1401
+ tempDir;
1402
+ /** Apaga a pasta temporária da execução (cópias que o bot não levou para `dest`). Nunca lança. */
1403
+ async cleanup() {
1404
+ const pending = this.tempDir;
1405
+ this.tempDir = void 0;
1406
+ if (!pending) return;
1407
+ try {
1408
+ const { rm } = await import("fs/promises");
1409
+ await rm(await pending, { recursive: true, force: true });
1410
+ } catch {
1411
+ }
1412
+ }
1413
+ field(field) {
1414
+ const value = fileField(this.deps.params, field);
1415
+ if (value === void 0) throw new Error(`campo "${field}" n\xE3o tem arquivo nos params`);
1416
+ return value;
1417
+ }
1418
+ single(refOrField) {
1419
+ if (typeof refOrField === "string") {
1420
+ const value = this.field(refOrField);
1421
+ if (Array.isArray(value)) {
1422
+ throw new Error(`campo "${refOrField}" tem v\xE1rios arquivos \u2014 use downloadAll("${refOrField}") ou passe um item da lista`);
1423
+ }
1424
+ return value;
1425
+ }
1426
+ if (!isFileRef(refOrField)) throw new TypeError("esperado um FileRef ou o nome de um campo de arquivo");
1427
+ return refOrField;
1428
+ }
1429
+ async downloadAll(field, dest) {
1430
+ const value = this.field(field);
1431
+ const refs = Array.isArray(value) ? value : [value];
1432
+ const paths = [];
1433
+ for (const ref of refs) paths.push(await this.download(ref, dest, true));
1434
+ return paths;
1435
+ }
1436
+ async read(ref) {
1437
+ const { readFile, rm } = await import("fs/promises");
1438
+ const { dirname } = await import("path");
1439
+ const path = await this.download(ref, void 0);
1440
+ try {
1441
+ return await readFile(path);
1442
+ } finally {
1443
+ await rm(dirname(path), { recursive: true, force: true });
1444
+ }
1445
+ }
1446
+ async download(ref, dest, destIsDir = false) {
1447
+ this.checkCanceled();
1448
+ const target = await this.resolveTarget(ref, dest, destIsDir);
1449
+ if (ref.storage === "local") await this.copyLocal(ref, target);
1450
+ else await this.downloadS3(ref, target);
1451
+ return target;
1452
+ }
1453
+ checkCanceled() {
1454
+ if (this.deps.signal.aborted) throw runCanceledError();
1455
+ }
1456
+ runTempDir() {
1457
+ this.tempDir ??= (async () => {
1458
+ const { mkdtemp } = await import("fs/promises");
1459
+ const { tmpdir } = await import("os");
1460
+ const { join } = await import("path");
1461
+ return mkdtemp(join(tmpdir(), "hobots-files-"));
1462
+ })();
1463
+ return this.tempDir;
1464
+ }
1465
+ async resolveTarget(ref, dest, destIsDir) {
1466
+ const { mkdir, mkdtemp, stat } = await import("fs/promises");
1467
+ const { dirname, join, resolve: resolve2 } = await import("path");
1468
+ const name = safeFileName(ref.name, "arquivo");
1469
+ if (dest === void 0) {
1470
+ return join(await mkdtemp(join(await this.runTempDir(), "f-")), name);
1471
+ }
1472
+ const isDir = destIsDir || dest.endsWith("/") || dest.endsWith("\\") || (await stat(dest).catch(() => void 0))?.isDirectory() === true;
1473
+ if (isDir) {
1474
+ await mkdir(dest, { recursive: true });
1475
+ return resolve2(dest, name);
1476
+ }
1477
+ await mkdir(dirname(resolve2(dest)), { recursive: true });
1478
+ return resolve2(dest);
1479
+ }
1480
+ /** storage 's3': URL pré-assinada → GET em streaming → confere o tamanho → só então DELETE. */
1481
+ async downloadS3(ref, target) {
1482
+ const { api, runId, signal } = this.deps;
1483
+ let expected;
1484
+ let url;
1485
+ try {
1486
+ const info = await api.requestFileDownload(runId, ref.id, signal);
1487
+ expected = info.size >= 0 ? info.size : ref.size;
1488
+ url = info.url;
1489
+ } catch (error) {
1490
+ throw this.translate(error, ref);
1491
+ }
1492
+ const { open, rename, rm } = await import("fs/promises");
1493
+ const part = `${target}.part`;
1494
+ const idleTimeoutMs = this.deps.idleTimeoutMs ?? FILE_DOWNLOAD_IDLE_TIMEOUT_MS;
1495
+ const controller = new AbortController();
1496
+ let timedOut = false;
1497
+ let timer;
1498
+ const arm = () => {
1499
+ clearTimeout(timer);
1500
+ timer = setTimeout(() => {
1501
+ timedOut = true;
1502
+ controller.abort();
1503
+ }, idleTimeoutMs);
1504
+ };
1505
+ const onCancel = () => controller.abort();
1506
+ signal.addEventListener("abort", onCancel, { once: true });
1507
+ try {
1508
+ arm();
1509
+ const response = await api.fetchFile(url, controller.signal);
1510
+ const handle = await open(part, "w");
1511
+ let written = 0;
1512
+ try {
1513
+ const reader = response.body?.getReader();
1514
+ const stopReading = () => void reader?.cancel().catch(() => void 0);
1515
+ controller.signal.addEventListener("abort", stopReading, { once: true });
1516
+ for (; ; ) {
1517
+ const chunk = reader ? await reader.read() : { done: true, value: void 0 };
1518
+ if (chunk.done) break;
1519
+ arm();
1520
+ written += chunk.value.byteLength;
1521
+ if (written > expected) {
1522
+ stopReading();
1523
+ break;
1524
+ }
1525
+ await handle.write(chunk.value);
1526
+ }
1527
+ } finally {
1528
+ await handle.close();
1529
+ }
1530
+ if (written !== expected) {
1531
+ throw new Error(`tamanho divergente no download de "${ref.name}": recebidos ${written} bytes, esperados ${expected}`);
1532
+ }
1533
+ await rename(part, target);
1534
+ } catch (error) {
1535
+ await rm(part, { force: true });
1536
+ if (signal.aborted) throw runCanceledError();
1537
+ if (timedOut) {
1538
+ throw new Error(`download de "${ref.name}" parado h\xE1 ${idleTimeoutMs / 1e3}s \u2014 tente de novo`);
1539
+ }
1540
+ throw this.translate(error, ref);
1541
+ } finally {
1542
+ clearTimeout(timer);
1543
+ signal.removeEventListener("abort", onCancel);
1544
+ }
1545
+ try {
1546
+ await api.confirmFileDownload(runId, ref.id);
1547
+ } catch (error) {
1548
+ this.deps.warn(`arquivo "${ref.name}" baixado, mas a confirma\xE7\xE3o ao servidor falhou: ${String(error)}`);
1549
+ }
1550
+ }
1551
+ /** storage 'local': copia o único arquivo de `$HOBOTS_LOCAL_FILES_DIR/<id>/` e apaga a pasta de origem. */
1552
+ async copyLocal(ref, target) {
1553
+ const base = process.env[LOCAL_FILES_DIR_ENV];
1554
+ if (!base) throw unavailableLocal(ref);
1555
+ const { copyFile, readdir, rename, rm, stat } = await import("fs/promises");
1556
+ const { join, resolve: resolve2, sep } = await import("path");
1557
+ const root = resolve2(base);
1558
+ const folder = resolve2(root, ref.id);
1559
+ if (!isInsideDir(root, folder, sep) || !UUID_RE.test(ref.id)) {
1560
+ throw new Error(`id de arquivo local inv\xE1lido: ${JSON.stringify(ref.id)}`);
1561
+ }
1562
+ let names;
1563
+ try {
1564
+ names = (await readdir(folder, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name);
1565
+ } catch (error) {
1566
+ if (errorCode(error) === "ENOENT" || errorCode(error) === "ENOTDIR") throw unavailableLocal(ref);
1567
+ throw error;
1568
+ }
1569
+ if (names.length === 0) throw unavailableLocal(ref);
1570
+ if (names.length > 1) throw new Error(`pasta do arquivo "${ref.name}" tem ${names.length} arquivos \u2014 esperado 1`);
1571
+ this.checkCanceled();
1572
+ const part = `${target}.part`;
1573
+ try {
1574
+ await copyFile(join(folder, names[0]), part);
1575
+ const { size } = await stat(part);
1576
+ if (size !== ref.size) {
1577
+ throw new Error(`tamanho divergente na c\xF3pia de "${ref.name}": copiados ${size} bytes, esperados ${ref.size}`);
1578
+ }
1579
+ await rename(part, target);
1580
+ } catch (error) {
1581
+ await rm(part, { force: true });
1582
+ throw error;
1583
+ }
1584
+ await rm(folder, { recursive: true, force: true });
1585
+ }
1586
+ translate(error, ref) {
1587
+ if (this.deps.signal.aborted) return runCanceledError();
1588
+ if (error instanceof TasksApiError && error.status === 410) {
1589
+ return new TasksApiError(`arquivo "${ref.name}" j\xE1 baixado ou expirado`, 410);
1590
+ }
1591
+ if (error instanceof TasksApiError && error.status === 404) {
1592
+ return new TasksApiError(`arquivo "${ref.name}" n\xE3o encontrado nesta execu\xE7\xE3o`, 404);
1593
+ }
1594
+ return error;
1595
+ }
1596
+ };
1597
+
1211
1598
  // src/retry.ts
1212
1599
  var DEFAULT_RETRIES = 3;
1213
1600
  var DEFAULT_DELAY_MS = 1e3;
@@ -1392,7 +1779,7 @@ var RunReporter = class {
1392
1779
  tx.setStepGuard(checkCanceled);
1393
1780
  const eachSink = {
1394
1781
  report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
1395
- startStep: (op, description) => tx.startChild(op, description),
1782
+ startStep: (op, description) => tx.start(op, description),
1396
1783
  declareTotal: (total) => {
1397
1784
  if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
1398
1785
  },
@@ -1446,12 +1833,14 @@ var RunReporter = class {
1446
1833
  );
1447
1834
  const signal = this.abortController.signal;
1448
1835
  const sleep2 = (ms) => cancelableSleep(ms, signal);
1836
+ this.fileStore = new RunFileStore({ api, runId, params, signal, warn: (message) => this.log("warning", message) });
1449
1837
  this.context = {
1450
1838
  runId,
1451
1839
  params,
1452
1840
  log,
1453
1841
  items,
1454
1842
  progress,
1843
+ files: this.fileStore.files,
1455
1844
  transaction: tx,
1456
1845
  flush: (timeoutMs) => this.flush(timeoutMs),
1457
1846
  signal,
@@ -1483,8 +1872,17 @@ var RunReporter = class {
1483
1872
  itemSeq = 0;
1484
1873
  /** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
1485
1874
  itemsUnsupported = false;
1486
- /** Cap de itens por run atingido (client-side ou 429) — degrada com um aviso só. */
1875
+ /**
1876
+ * Cota de itens por run do servidor (429, legado — hoje a api não tem teto de itens) —
1877
+ * degrada com um aviso só.
1878
+ */
1487
1879
  itemsQuotaExceeded = false;
1880
+ /**
1881
+ * Armazenamento do plano cheio (402 `storage_limit`): o lote é descartado e o aviso sai
1882
+ * uma vez por episódio. Os envios continuam — num agente `start()` o espaço pode ser
1883
+ * liberado (ou o plano trocado) sem reiniciar; um lote aceito rearma o aviso.
1884
+ */
1885
+ itemsStorageFull = false;
1488
1886
  /**
1489
1887
  * Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
1490
1888
  * servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
@@ -1506,6 +1904,12 @@ var RunReporter = class {
1506
1904
  progressDirty = false;
1507
1905
  progressTimer;
1508
1906
  progressWarned = false;
1907
+ /** Arquivos do formulário desta run (`ctx.files`) — a pasta temporária é apagada no fim. */
1908
+ fileStore;
1909
+ /** Apaga as cópias de arquivo que ficaram na pasta temporária da run. Nunca lança. */
1910
+ cleanupFiles() {
1911
+ return this.fileStore.cleanup();
1912
+ }
1509
1913
  get isDead() {
1510
1914
  return this.dead;
1511
1915
  }
@@ -1788,6 +2192,7 @@ var RunReporter = class {
1788
2192
  if (this.itemsUnsupported) return;
1789
2193
  try {
1790
2194
  await this.api.createItems(this.runId, { items: batch });
2195
+ this.itemsStorageFull = false;
1791
2196
  } catch (error) {
1792
2197
  if (error instanceof TasksApiError && error.status === 404) {
1793
2198
  this.itemsUnsupported = true;
@@ -1801,6 +2206,13 @@ var RunReporter = class {
1801
2206
  }
1802
2207
  return;
1803
2208
  }
2209
+ if (error instanceof TasksApiError && error.status === 402) {
2210
+ if (!this.itemsStorageFull) {
2211
+ this.itemsStorageFull = true;
2212
+ this.log("warning", "itens descartados: armazenamento do plano cheio \u2014 libere espa\xE7o ou fa\xE7a upgrade em Plano e cobran\xE7a");
2213
+ }
2214
+ return;
2215
+ }
1804
2216
  if (error instanceof TasksApiError) {
1805
2217
  this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
1806
2218
  this.log("warning", `${batch.length} item(ns) descartado(s): envio rejeitado pelo servidor (HTTP ${error.status})`);
@@ -2085,16 +2497,19 @@ var Step = class {
2085
2497
  return this;
2086
2498
  }
2087
2499
  /**
2088
- * Marca a step como `error` guardando o motivo em `data.error` ({ type, message }).
2089
- * Aceita qualquer valor lançado; `message` sobrescreve a mensagem do erro.
2500
+ * Marca a step como `error` guardando o motivo em `data.error` ({ type, message }) e a
2501
+ * fecha. Aceita qualquer valor lançado; `message` sobrescreve a mensagem do erro.
2090
2502
  * `options.image` pendura um print do estado da tela em `data.attachments`.
2503
+ * Numa step já fechada não faz nada.
2091
2504
  */
2092
- setError(error, message, options) {
2505
+ failed(error, message, options) {
2506
+ if (this.endTimestamp) return this;
2093
2507
  this.status = "error";
2094
2508
  const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
2095
2509
  const raw = message ?? (error instanceof Error ? error.message || error.name : String(error));
2096
2510
  this.setData("error", { ...type ? { type } : {}, message: stepMessage(raw) });
2097
2511
  if (options?.image !== void 0) this.attachImage(options.image);
2512
+ this.close();
2098
2513
  return this;
2099
2514
  }
2100
2515
  /**
@@ -2109,7 +2524,7 @@ var Step = class {
2109
2524
  if (!id) return;
2110
2525
  this.setData("attachments", [...current, id]);
2111
2526
  }
2112
- /** Motivo registrado via setStatus(…, message) ou setError — usado no log de FINISH. */
2527
+ /** Motivo registrado via setStatus(…, message) ou failed — usado no log de FINISH. */
2113
2528
  reason() {
2114
2529
  const error = this.data?.error;
2115
2530
  const text = typeof error?.message === "string" ? error.message : this.data?.message;
@@ -2120,10 +2535,18 @@ var Step = class {
2120
2535
  return this;
2121
2536
  }
2122
2537
  /** Step aninhada (filha desta step). */
2123
- startChild(op, description) {
2538
+ start(op, description) {
2124
2539
  return this.transaction.createStep(this.stepId, op, description);
2125
2540
  }
2126
- finish() {
2541
+ /**
2542
+ * Fecha a step. O status fica o que estiver (`ok` por padrão, ou o de `setStatus`).
2543
+ * Idempotente; a transaction fecha no fim da run o que ficou aberto.
2544
+ */
2545
+ succeeded() {
2546
+ this.close();
2547
+ return this;
2548
+ }
2549
+ close() {
2127
2550
  if (this.endTimestamp) return;
2128
2551
  this.endTimestamp = nowISO();
2129
2552
  this.transaction.emit(
@@ -2216,7 +2639,7 @@ var Transaction = class {
2216
2639
  }
2217
2640
  /**
2218
2641
  * Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
2219
- * `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
2642
+ * `hobots.transaction.start()` (e `step.start()`) param uma run cancelada.
2220
2643
  */
2221
2644
  setStepGuard(guard) {
2222
2645
  this.stepGuard = guard;
@@ -2228,10 +2651,32 @@ var Transaction = class {
2228
2651
  this.emit("step", "start", stepLabel(op, description));
2229
2652
  return step;
2230
2653
  }
2231
- /** Step direta da transaction. */
2232
- startChild(op, description) {
2654
+ /** Step direta da transaction. `op` é o nome usado por `succeeded(name)`/`failed(error, name)`. */
2655
+ start(op, description) {
2233
2656
  return this.createStep(this.stepId, op, description);
2234
2657
  }
2658
+ /**
2659
+ * Fecha a step aberta mais recente — com `name`, a mais recente aberta com esse `op`.
2660
+ * Sem step aberta (ou sem nenhuma com esse nome), não faz nada e devolve undefined.
2661
+ */
2662
+ succeeded(name) {
2663
+ return this.findOpenStep(name)?.succeeded();
2664
+ }
2665
+ /**
2666
+ * Marca como `error` e fecha a step aberta mais recente — com `name`, a mais recente
2667
+ * aberta com esse `op`. `options.message` sobrescreve a mensagem do erro e
2668
+ * `options.image` pendura um print. Sem step aberta, não faz nada e devolve undefined.
2669
+ */
2670
+ failed(error, name, options) {
2671
+ return this.findOpenStep(name)?.failed(error, options?.message, options);
2672
+ }
2673
+ findOpenStep(name) {
2674
+ for (let i = this.steps.length - 1; i >= 0; i--) {
2675
+ const step = this.steps[i];
2676
+ if (!step.endTimestamp && (name === void 0 || step.op === name)) return step;
2677
+ }
2678
+ return void 0;
2679
+ }
2235
2680
  setTag(key, value) {
2236
2681
  this.tags[key] = value;
2237
2682
  return this;
@@ -2269,7 +2714,7 @@ var Transaction = class {
2269
2714
  remaining.push(step);
2270
2715
  }
2271
2716
  }
2272
- for (const step of batch) step.finish();
2717
+ for (const step of batch) step.succeeded();
2273
2718
  this.steps.splice(0, this.steps.length, ...remaining);
2274
2719
  if (this.client) {
2275
2720
  for (let i = 0; i < batch.length; i += MAX_STEPS) {
@@ -2301,7 +2746,7 @@ var Transaction = class {
2301
2746
  finish() {
2302
2747
  if (this.finished) return;
2303
2748
  this.finished = true;
2304
- for (const step of this.steps) step.finish();
2749
+ for (const step of this.steps) step.succeeded();
2305
2750
  const endTimestamp = nowISO();
2306
2751
  this.emit(
2307
2752
  "transaction",
@@ -2363,14 +2808,21 @@ async function executeRun(input) {
2363
2808
  await reporter.finalizeItems();
2364
2809
  tx.setStatus("error");
2365
2810
  tx.finish();
2811
+ captureRunFailure(client, error, reporter);
2366
2812
  reporter.reportFailure(toRunError(error));
2367
2813
  logger.debug(`run ${run.id} falhou: ${String(error)}`);
2368
2814
  }
2369
2815
  await reporter.finalize();
2370
2816
  } finally {
2817
+ await reporter.cleanupFiles();
2371
2818
  tracker.remove(reporter);
2372
2819
  }
2373
2820
  }
2821
+ function captureRunFailure(client, error, reporter) {
2822
+ if (!client?.options.captureRunErrors) return;
2823
+ if (reporter.isDead || isCancellation(error) || wasCaptured(error)) return;
2824
+ client.captureException(error, { type: "error", tags: { mechanism: "run_handler", run_id: reporter.runId } });
2825
+ }
2374
2826
  function warnIncompleteProgress(reporter) {
2375
2827
  const snapshot = reporter.context.progress.snapshot;
2376
2828
  if (!snapshot || snapshot.current >= snapshot.total) return;
@@ -2562,6 +3014,86 @@ var TaskAgent = class {
2562
3014
  }
2563
3015
  };
2564
3016
 
3017
+ // src/tasks/attached.ts
3018
+ var START_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
3019
+ function isTransient(error) {
3020
+ if (!(error instanceof TasksApiError)) return true;
3021
+ return error.status >= 500 || error.status === 429;
3022
+ }
3023
+ function describeRejection(runId, error) {
3024
+ if (error instanceof TasksApiError && error.status === 404) {
3025
+ return `run ${runId} n\xE3o existe na plataforma (404) \u2014 nada a executar`;
3026
+ }
3027
+ if (error instanceof TasksApiError && error.status === 409) {
3028
+ return `run ${runId} n\xE3o pode ser iniciada (409: cancelada, j\xE1 iniciada ou de outra inst\xE2ncia) \u2014 nada a executar`;
3029
+ }
3030
+ return `n\xE3o foi poss\xEDvel iniciar a run ${runId} (${String(error)}) \u2014 nada a executar`;
3031
+ }
3032
+ var AttachedRunner = class {
3033
+ constructor(runId, handler, deps) {
3034
+ this.runId = runId;
3035
+ this.handler = handler;
3036
+ this.deps = deps;
3037
+ this.done = new Promise((resolve2) => {
3038
+ this.resolveDone = resolve2;
3039
+ });
3040
+ }
3041
+ runId;
3042
+ handler;
3043
+ deps;
3044
+ abort = new AbortController();
3045
+ /** Resolve quando a run terminou (ou não pôde ser iniciada, ou o runner foi parado antes). */
3046
+ done;
3047
+ resolveDone;
3048
+ async run() {
3049
+ try {
3050
+ const run = await this.startRun();
3051
+ if (!run) return;
3052
+ this.deps.logger.debug(`run ${run.id} iniciada (HOBOTS_RUN_ID)`);
3053
+ await executeRun({
3054
+ run,
3055
+ handler: this.handler,
3056
+ api: this.deps.api,
3057
+ client: this.deps.client,
3058
+ logger: this.deps.logger,
3059
+ tracker: this.deps.tracker
3060
+ });
3061
+ } catch (error) {
3062
+ this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
3063
+ } finally {
3064
+ this.resolveDone();
3065
+ }
3066
+ }
3067
+ /** Desiste de iniciar a run (close() antes da plataforma responder). Uma run já iniciada segue pelo tracker. */
3068
+ stop() {
3069
+ this.abort.abort();
3070
+ }
3071
+ async startRun() {
3072
+ for (let attempt = 0; ; attempt++) {
3073
+ if (this.abort.signal.aborted) return void 0;
3074
+ try {
3075
+ return await this.deps.api.startRun(this.runId, { instance: this.deps.instanceInfo() }, this.abort.signal);
3076
+ } catch (error) {
3077
+ if (this.abort.signal.aborted) return void 0;
3078
+ if (!isTransient(error)) {
3079
+ this.deps.logger.error(describeRejection(this.runId, error));
3080
+ return void 0;
3081
+ }
3082
+ const delay = START_RETRY_DELAYS_MS[attempt];
3083
+ if (delay === void 0) {
3084
+ this.deps.logger.error(
3085
+ `n\xE3o foi poss\xEDvel iniciar a run ${this.runId} ap\xF3s ${attempt + 1} tentativas (${String(error)}) \u2014 desistindo`
3086
+ );
3087
+ return void 0;
3088
+ }
3089
+ const wait = jitter(delay);
3090
+ this.deps.logger.warn(`in\xEDcio da run ${this.runId} falhou (${String(error)}), pr\xF3xima tentativa em ${wait}ms`);
3091
+ await abortableSleep(wait, this.abort.signal);
3092
+ }
3093
+ }
3094
+ }
3095
+ };
3096
+
2565
3097
  // src/tasks/item-columns.ts
2566
3098
  var KEY_RE = /^[a-zA-Z0-9_.-]{1,100}$/;
2567
3099
  var MAX_COLUMNS = WIRE_LIMITS.MAX_ITEM_COLUMNS;
@@ -2767,11 +3299,18 @@ function hostname() {
2767
3299
  return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
2768
3300
  }
2769
3301
  function createRuntime(options, handler) {
3302
+ loadDotenv();
2770
3303
  const config = resolveConfig({ clientSecret: options.clientSecret });
2771
3304
  const logger = makeLogger(options.debug ?? false);
2772
3305
  const environment = resolveEnvironmentOption(options.environment);
2773
3306
  const client = new Client(
2774
- { environment, release: options.release, tags: options.tags, debug: options.debug },
3307
+ {
3308
+ environment,
3309
+ release: options.release,
3310
+ tags: options.tags,
3311
+ debug: options.debug,
3312
+ captureRunErrors: options.captureUnhandled !== false
3313
+ },
2775
3314
  config
2776
3315
  );
2777
3316
  const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
@@ -2787,6 +3326,8 @@ function createRuntime(options, handler) {
2787
3326
  heartbeat: void 0,
2788
3327
  agent: void 0,
2789
3328
  local: void 0,
3329
+ attached: void 0,
3330
+ runId: void 0,
2790
3331
  instanceId: resolveInstanceId(options.instanceId),
2791
3332
  environment,
2792
3333
  pollIntervalMs: void 0,
@@ -2823,7 +3364,9 @@ function resolveRunOptions(options, mode, logger) {
2823
3364
  }
2824
3365
  function instanceInfo(rt) {
2825
3366
  return {
3367
+ /* v8 ignore start -- defensivo: o boot define o instanceId antes de qualquer poll/criação de run */
2826
3368
  instance_id: rt.instanceId ?? "unknown",
3369
+ /* v8 ignore stop */
2827
3370
  version: SDK_VERSION,
2828
3371
  host: hostname(),
2829
3372
  // ambiente da instância: junto do heartbeat, é como o servidor sabe onde o robô roda
@@ -2862,6 +3405,20 @@ function launchLocal(rt) {
2862
3405
  rt.local = runner;
2863
3406
  void runner.run();
2864
3407
  }
3408
+ function launchAttached(rt, runId) {
3409
+ const runner = new AttachedRunner(runId, rt.handler, {
3410
+ api: rt.api,
3411
+ client: rt.client,
3412
+ logger: rt.logger,
3413
+ tracker: rt.tracker,
3414
+ instanceInfo: () => instanceInfo(rt)
3415
+ });
3416
+ rt.attached = runner;
3417
+ void runner.run();
3418
+ void runner.done.then(() => {
3419
+ if (runtime === rt) void close();
3420
+ });
3421
+ }
2865
3422
  async function boot(rt) {
2866
3423
  if (!rt.instanceId) {
2867
3424
  const id = await fallbackInstanceId();
@@ -2881,8 +3438,9 @@ async function boot(rt) {
2881
3438
  if (rt.options.installExitHandlers !== false) {
2882
3439
  rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
2883
3440
  }
2884
- if (rt.handler.mode === "remote") ensureAgent(rt).start();
2885
- else launchLocal(rt);
3441
+ if (rt.handler.mode === "local") launchLocal(rt);
3442
+ else if (rt.runId) launchAttached(rt, rt.runId);
3443
+ else ensureAgent(rt).start();
2886
3444
  }
2887
3445
  function scheduleBoot(rt) {
2888
3446
  if (rt.bootScheduled) return;
@@ -2900,6 +3458,7 @@ function startAgent(fn, mode, handler, options) {
2900
3458
  const agentHandler = { mode, handler, options: resolveRunOptions(options, mode, logger) };
2901
3459
  const rt = createRuntime(options, agentHandler);
2902
3460
  if (mode === "remote" && options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
3461
+ if (mode === "remote") rt.runId = resolveRunId(options.runId);
2903
3462
  runtime = rt;
2904
3463
  scheduleBoot(rt);
2905
3464
  }
@@ -2940,7 +3499,9 @@ function captureException(error, hint) {
2940
3499
  const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2941
3500
  forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
2942
3501
  }
2943
- return rt.client.captureException(error, hint);
3502
+ const runId = currentReporter()?.runId;
3503
+ const withRun = runId ? { ...hint, tags: { run_id: runId, ...hint?.tags } } : hint;
3504
+ return rt.client.captureException(error, withRun);
2944
3505
  }
2945
3506
  async function close(timeoutMs = 3e4) {
2946
3507
  const rt = runtime;
@@ -2949,6 +3510,7 @@ async function close(timeoutMs = 3e4) {
2949
3510
  await rt.bootPromise;
2950
3511
  if (rt.agent) await rt.agent.stop(0);
2951
3512
  rt.local?.stop();
3513
+ rt.attached?.stop();
2952
3514
  const idle = await rt.tracker.waitIdle(timeoutMs);
2953
3515
  if (!idle) {
2954
3516
  rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
@@ -3000,6 +3562,14 @@ var fallbackItems = {
3000
3562
  each: (list, handler, options = {}) => eachItem(list, handler, options, fallbackEachSink)
3001
3563
  };
3002
3564
  var fallbackProgress = { total: noop, advance: noop, set: noop, current: 0, snapshot: null };
3565
+ var outsideRunFiles = () => Promise.reject(new Error("hobots.files s\xF3 baixa arquivos dentro de uma execu\xE7\xE3o (handler do register()/start())"));
3566
+ var fallbackFiles = {
3567
+ list: () => [],
3568
+ get: () => void 0,
3569
+ download: outsideRunFiles,
3570
+ downloadAll: outsideRunFiles,
3571
+ read: outsideRunFiles
3572
+ };
3003
3573
  var neverAborts = new AbortController().signal;
3004
3574
  var detachedTransaction;
3005
3575
  function resolve(member) {
@@ -3021,6 +3591,7 @@ var hobots = Object.defineProperties({}, {
3021
3591
  log: getter("log", () => fallbackLog),
3022
3592
  items: getter("items", () => fallbackItems),
3023
3593
  progress: getter("progress", () => fallbackProgress),
3594
+ files: getter("files", () => fallbackFiles),
3024
3595
  transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
3025
3596
  signal: getter("signal", () => neverAborts),
3026
3597
  throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
@@ -3041,7 +3612,8 @@ var hobots = Object.defineProperties({}, {
3041
3612
  }),
3042
3613
  close: method((timeoutMs) => close(timeoutMs)),
3043
3614
  captureException: method((error, hint) => captureException(error, hint)),
3044
- image: method(image)
3615
+ image: method(image),
3616
+ isFileRef: method(isFileRef)
3045
3617
  });
3046
3618
  function installGlobalHobots() {
3047
3619
  if (globalThis.hobots === hobots) return;
@@ -3053,8 +3625,10 @@ var close2 = hobots.close;
3053
3625
  var captureException2 = hobots.captureException;
3054
3626
  var retry = hobots.retry;
3055
3627
  var image2 = hobots.image;
3628
+ var isFileRef2 = hobots.isFileRef;
3056
3629
 
3057
3630
  // src/index.ts
3631
+ loadDotenv();
3058
3632
  var src_default = hobots;
3059
3633
  export {
3060
3634
  captureException2 as captureException,
@@ -3062,6 +3636,7 @@ export {
3062
3636
  src_default as default,
3063
3637
  hobots,
3064
3638
  image2 as image,
3639
+ isFileRef2 as isFileRef,
3065
3640
  register2 as register,
3066
3641
  retry,
3067
3642
  start2 as start