hobots 2.4.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,85 +30,19 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
- Client: () => Client,
34
- DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
35
- Scope: () => Scope,
36
- Step: () => Step,
37
- Transaction: () => Transaction,
38
- addBreadcrumb: () => addBreadcrumb,
39
- captureException: () => captureException,
40
- captureMessage: () => captureMessage,
41
- close: () => close,
42
- flush: () => flush,
43
- getClient: () => getClient,
44
- init: () => init,
45
- register: () => register,
46
- scrubSecrets: () => scrubSecrets,
47
- setContext: () => setContext,
48
- setExtra: () => setExtra,
49
- setTag: () => setTag,
50
- setTags: () => setTags,
51
- setUser: () => setUser,
52
- start: () => start,
53
- startTransaction: () => startTransaction,
54
- stop: () => stop,
55
- withScope: () => withScope
33
+ captureException: () => captureException2,
34
+ close: () => close2,
35
+ default: () => src_default,
36
+ hobots: () => hobots,
37
+ register: () => register2,
38
+ retry: () => retry,
39
+ start: () => start2
56
40
  });
57
41
  module.exports = __toCommonJS(src_exports);
58
42
 
59
- // src/config.ts
60
- var DEFAULT_BASE_URL = "https://api2.hobots.app";
61
- var SECRET_PATTERN = /^hb_[a-f0-9]{64}$/i;
62
- function envBaseUrl() {
63
- if (typeof process === "undefined" || !process.env) return void 0;
64
- return process.env.HOBOTS_API_URL?.trim() || void 0;
65
- }
66
- function isLoopbackHost(hostname2) {
67
- return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
68
- }
69
- var warnedInsecureBaseUrl = false;
70
- function validateBaseUrl(raw, prefix) {
71
- let url;
72
- try {
73
- url = new URL(raw);
74
- } catch {
75
- throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
76
- }
77
- if (url.protocol !== "http:" && url.protocol !== "https:") {
78
- throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
79
- }
80
- if (url.username || url.password) {
81
- throw new Error(`${prefix} HOBOTS_API_URL n\xE3o pode conter credenciais na URL \u2014 a autentica\xE7\xE3o \xE9 s\xF3 pelo Client Secret`);
82
- }
83
- if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
84
- warnedInsecureBaseUrl = true;
85
- console.warn(
86
- `${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
87
- );
88
- }
89
- return raw.replace(/\/+$/, "");
90
- }
91
- function resolveConfig(options, prefix = "[hobots]") {
92
- const key = options.clientSecret?.trim();
93
- if (!key) {
94
- throw new Error(`${prefix} clientSecret ausente \u2014 copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`);
95
- }
96
- if (!SECRET_PATTERN.test(key)) {
97
- throw new Error(
98
- `${prefix} clientSecret com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
99
- );
100
- }
101
- const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
102
- return {
103
- key,
104
- baseUrl,
105
- ingestEndpoint: `${baseUrl}/v1/ingest`,
106
- tasksBaseUrl: `${baseUrl}/v1/tasks`
107
- };
108
- }
109
-
110
43
  // src/stacktrace.ts
111
44
  var MAX_FRAMES = 50;
45
+ var MAX_FILENAME_CHARS = 1024;
112
46
  var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
113
47
  var LOCATION_RE = /^(.*?):(\d+):(\d+)$/;
114
48
  function isInApp(filename) {
@@ -131,7 +65,7 @@ function parseStacktrace(stack) {
131
65
  const filename = loc ? loc[1] : location;
132
66
  frames.push({
133
67
  function: fn || "<anonymous>",
134
- filename: filename || void 0,
68
+ filename: filename ? filename.slice(0, MAX_FILENAME_CHARS) : void 0,
135
69
  lineno: loc?.[2] ? Number(loc[2]) : void 0,
136
70
  colno: loc?.[3] ? Number(loc[3]) : void 0,
137
71
  in_app: isInApp(filename)
@@ -161,7 +95,7 @@ function scrubSecrets(text) {
161
95
 
162
96
  // src/utils.ts
163
97
  var SDK_NAME = "hobots";
164
- var SDK_VERSION = true ? "2.4.0" : "0.0.0-dev";
98
+ var SDK_VERSION = true ? "3.2.0" : "0.0.0-dev";
165
99
  function isNode() {
166
100
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
167
101
  }
@@ -189,20 +123,47 @@ function unrefTimer(timer) {
189
123
  timer?.unref?.();
190
124
  }
191
125
  function sleep(ms) {
192
- return new Promise((resolve) => {
193
- const timer = setTimeout(resolve, ms);
126
+ return new Promise((resolve2) => {
127
+ const timer = setTimeout(resolve2, ms);
194
128
  unrefTimer(timer);
195
129
  });
196
130
  }
131
+ function abortableSleep(ms, signal) {
132
+ return new Promise((resolve2) => {
133
+ if (signal?.aborted) {
134
+ resolve2();
135
+ return;
136
+ }
137
+ const onAbort = () => {
138
+ clearTimeout(timer);
139
+ resolve2();
140
+ };
141
+ const timer = setTimeout(() => {
142
+ signal?.removeEventListener("abort", onAbort);
143
+ resolve2();
144
+ }, ms);
145
+ signal?.addEventListener("abort", onAbort, { once: true });
146
+ });
147
+ }
197
148
  function jitter(baseMs) {
198
149
  return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
199
150
  }
200
151
  function truncate(value, max) {
201
152
  return value.length > max ? `${value.slice(0, max)}\u2026 [truncado]` : value;
202
153
  }
154
+ function safeString(value) {
155
+ if (typeof value === "string") return value;
156
+ try {
157
+ const json = JSON.stringify(value);
158
+ if (json && json !== "{}") return json;
159
+ } catch {
160
+ }
161
+ return String(value);
162
+ }
203
163
  var DEFAULT_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
204
164
  async function fetchWithRetry(send, options) {
205
165
  const delays = options.delays ?? DEFAULT_RETRY_DELAYS_MS;
166
+ const noRetry = options.noRetryStatuses ?? [];
206
167
  for (let attempt = 0; ; attempt++) {
207
168
  let retriable;
208
169
  let serverDelay;
@@ -210,6 +171,7 @@ async function fetchWithRetry(send, options) {
210
171
  const response = await send();
211
172
  if (response.ok) return response;
212
173
  if (response.status >= 400 && response.status < 500 && response.status !== 429) return response;
174
+ if (noRetry.includes(response.status)) return response;
213
175
  if (response.status === 429) serverDelay = retryAfterMs(response);
214
176
  retriable = new Error(`HTTP ${response.status}`);
215
177
  } catch (error) {
@@ -282,26 +244,18 @@ function colorize(text, color) {
282
244
  }
283
245
 
284
246
  // src/eventbuilder.ts
285
- function safeString(value) {
286
- if (typeof value === "string") return value;
287
- try {
288
- const json = JSON.stringify(value);
289
- if (json && json !== "{}") return json;
290
- } catch {
291
- }
292
- return String(value);
293
- }
247
+ var MAX_EXCEPTION_VALUE_CHARS = 8192;
294
248
  function eventFromException(error, type = "error") {
295
249
  let exceptionType;
296
250
  let value;
297
251
  let frames = [];
298
252
  if (error instanceof Error) {
299
253
  exceptionType = error.name || "Error";
300
- value = scrubSecrets(error.message);
254
+ value = truncate(scrubSecrets(error.message), MAX_EXCEPTION_VALUE_CHARS);
301
255
  frames = parseStacktrace(error.stack);
302
256
  } else {
303
257
  exceptionType = "NonError";
304
- value = scrubSecrets(safeString(error));
258
+ value = truncate(scrubSecrets(safeString(error)), MAX_EXCEPTION_VALUE_CHARS);
305
259
  }
306
260
  return {
307
261
  event_id: uuid4(),
@@ -314,14 +268,6 @@ function eventFromException(error, type = "error") {
314
268
  }
315
269
  };
316
270
  }
317
- function eventFromMessage(message, type = "warning") {
318
- return {
319
- event_id: uuid4(),
320
- timestamp: nowISO(),
321
- type,
322
- message: scrubSecrets(message)
323
- };
324
- }
325
271
 
326
272
  // src/transport.ts
327
273
  var REQUEST_TIMEOUT_MS = 1e4;
@@ -435,6 +381,18 @@ var Transport = class {
435
381
  };
436
382
 
437
383
  // src/client.ts
384
+ var MAX_TAG_VALUE_CHARS = 512;
385
+ var MAX_FINGERPRINT_ITEMS = 10;
386
+ var MAX_FINGERPRINT_ITEM_CHARS = 255;
387
+ var MAX_TRANSACTION_NAME_CHARS = 500;
388
+ var MAX_STEPS = 200;
389
+ function clipTags(tags) {
390
+ return Object.fromEntries(Object.entries(tags).map(([key, value]) => [key, truncate(String(value), MAX_TAG_VALUE_CHARS)]));
391
+ }
392
+ function clipFingerprint(fingerprint) {
393
+ if (fingerprint === void 0) return void 0;
394
+ return fingerprint.slice(0, MAX_FINGERPRINT_ITEMS).map((item) => truncate(String(item), MAX_FINGERPRINT_ITEM_CHARS));
395
+ }
438
396
  function runtimeContexts() {
439
397
  if (isNode()) {
440
398
  return {
@@ -455,7 +413,7 @@ var Client = class {
455
413
  #config;
456
414
  transport;
457
415
  machine;
458
- constructor(options, config = resolveConfig(options)) {
416
+ constructor(options, config) {
459
417
  this.#options = options;
460
418
  this.debug = !!options.debug;
461
419
  this.logger = makeLogger(this.debug);
@@ -485,53 +443,45 @@ var Client = class {
485
443
  [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
486
444
  return `Client { baseUrl: '${this.#config.baseUrl}' }`;
487
445
  }
488
- captureException(error, hint, scope2) {
489
- return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint, scope2);
490
- }
491
- captureMessage(message, type, hint, scope2) {
492
- return this.captureEvent(eventFromMessage(message, type), hint, scope2);
446
+ captureException(error, hint) {
447
+ return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
493
448
  }
494
- captureEvent(event, hint, scope2) {
495
- const sampleRate = this.options.sampleRate ?? 1;
496
- if (sampleRate < 1 && Math.random() >= sampleRate) {
497
- this.logger.debug("evento descartado por sampleRate");
498
- return event.event_id;
499
- }
500
- const prepared = this.prepareEvent(event, hint, scope2);
501
- const final = this.options.beforeSend ? this.options.beforeSend(prepared) : prepared;
502
- if (!final) {
503
- this.logger.debug("evento descartado por beforeSend");
504
- return event.event_id;
505
- }
449
+ captureEvent(event, hint) {
450
+ const final = this.prepareEvent(event, hint);
506
451
  this.transport.enqueue({ type: "event", payload: final });
507
452
  return final.event_id;
508
453
  }
509
- prepareEvent(event, hint, scope2) {
510
- const tags = { ...this.options.tags, ...scope2.tags, ...event.tags, ...hint?.tags };
511
- const extra = { ...scope2.extra, ...event.extra, ...hint?.extra };
454
+ prepareEvent(event, hint) {
455
+ const tags = { ...this.options.tags, ...event.tags, ...hint?.tags };
456
+ const extra = { ...event.extra, ...hint?.extra };
512
457
  const sampled = this.machine?.sample();
513
458
  const machine = sampled ? { machine: sampled } : {};
514
459
  return {
515
460
  ...event,
516
- type: hint?.type ?? event.type ?? scope2.type ?? "error",
461
+ type: hint?.type ?? event.type ?? "error",
517
462
  release: event.release ?? this.options.release,
518
463
  environment: this.resolveEnvironment(event.environment),
519
- tags: Object.keys(tags).length > 0 ? tags : void 0,
464
+ tags: Object.keys(tags).length > 0 ? clipTags(tags) : void 0,
520
465
  extra: Object.keys(extra).length > 0 ? extra : void 0,
521
- user: event.user ?? scope2.user,
522
- contexts: { ...runtimeContexts(), ...machine, ...scope2.contexts, ...event.contexts },
523
- breadcrumbs: scope2.breadcrumbs.length > 0 ? [...scope2.breadcrumbs] : event.breadcrumbs,
524
- fingerprint: hint?.fingerprint ?? event.fingerprint ?? scope2.fingerprint
466
+ contexts: { ...runtimeContexts(), ...machine, ...event.contexts },
467
+ fingerprint: clipFingerprint(hint?.fingerprint ?? event.fingerprint)
525
468
  };
526
469
  }
527
470
  sendTransaction(payload) {
471
+ let steps = payload.steps;
472
+ if (steps.length > MAX_STEPS) {
473
+ this.logger.warn(`transaction com ${steps.length} steps \u2014 enviando as primeiras ${MAX_STEPS}`);
474
+ steps = steps.slice(0, MAX_STEPS);
475
+ }
528
476
  this.transport.enqueue({
529
477
  type: "transaction",
530
478
  payload: {
531
479
  ...payload,
480
+ name: truncate(payload.name || "(sem nome)", MAX_TRANSACTION_NAME_CHARS),
481
+ steps,
532
482
  release: payload.release ?? this.options.release,
533
483
  environment: this.resolveEnvironment(payload.environment),
534
- tags: { ...this.options.tags, ...payload.tags }
484
+ tags: clipTags({ ...this.options.tags, ...payload.tags })
535
485
  }
536
486
  });
537
487
  }
@@ -547,7 +497,7 @@ var Client = class {
547
497
  });
548
498
  }
549
499
  /**
550
- * Ambiente do item: o dele, senão o do init(), senão produção. Centralizado
500
+ * Ambiente do item: o dele, senão o das opções, senão produção. Centralizado
551
501
  * porque evento, transaction e heartbeat precisam responder igual — o servidor
552
502
  * normaliza apelidos legados, então aqui basta repassar.
553
503
  */
@@ -562,23 +512,110 @@ var Client = class {
562
512
  }
563
513
  };
564
514
 
515
+ // src/config.ts
516
+ var DEFAULT_BASE_URL = "https://api2.hobots.app";
517
+ var CLIENT_SECRET_ENV = "HOBOTS_CLIENT_SECRET";
518
+ function envClientSecret() {
519
+ if (typeof process === "undefined" || !process.env) return void 0;
520
+ return process.env[CLIENT_SECRET_ENV]?.trim() || void 0;
521
+ }
522
+ var SECRET_PATTERN = /^hb_[a-f0-9]{64}$/i;
523
+ function envBaseUrl() {
524
+ if (typeof process === "undefined" || !process.env) return void 0;
525
+ return process.env.HOBOTS_API_URL?.trim() || void 0;
526
+ }
527
+ function isLoopbackHost(hostname2) {
528
+ return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
529
+ }
530
+ var warnedInsecureBaseUrl = false;
531
+ function validateBaseUrl(raw, prefix) {
532
+ let url;
533
+ try {
534
+ url = new URL(raw);
535
+ } catch {
536
+ throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
537
+ }
538
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
539
+ throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
540
+ }
541
+ if (url.username || url.password) {
542
+ throw new Error(`${prefix} HOBOTS_API_URL n\xE3o pode conter credenciais na URL \u2014 a autentica\xE7\xE3o \xE9 s\xF3 pelo Client Secret`);
543
+ }
544
+ if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
545
+ warnedInsecureBaseUrl = true;
546
+ console.warn(
547
+ `${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
548
+ );
549
+ }
550
+ return raw.replace(/\/+$/, "");
551
+ }
552
+ function resolveConfig(options = {}, prefix = "[hobots]") {
553
+ const fromOption = options.clientSecret?.trim();
554
+ const key = fromOption || envClientSecret();
555
+ const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
556
+ if (!key) {
557
+ throw new Error(
558
+ `${prefix} Client Secret ausente \u2014 defina a env ${CLIENT_SECRET_ENV} (ou passe clientSecret nas op\xE7\xF5es da task); copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
559
+ );
560
+ }
561
+ if (!SECRET_PATTERN.test(key)) {
562
+ throw new Error(
563
+ `${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`
564
+ );
565
+ }
566
+ const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
567
+ return {
568
+ key,
569
+ baseUrl,
570
+ ingestEndpoint: `${baseUrl}/v1/ingest`,
571
+ tasksBaseUrl: `${baseUrl}/v1/tasks`
572
+ };
573
+ }
574
+
575
+ // src/env.ts
576
+ var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
577
+ var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
578
+ function resolveInstanceId(option) {
579
+ const fromOption = option?.trim();
580
+ if (fromOption) return fromOption;
581
+ if (typeof process === "undefined" || !process.env) return void 0;
582
+ return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
583
+ }
584
+ function resolveEnvironmentOption(option) {
585
+ const fromOption = option?.trim();
586
+ if (fromOption) return fromOption;
587
+ if (typeof process === "undefined" || !process.env) return void 0;
588
+ return process.env[ENVIRONMENT_ENV]?.trim() || void 0;
589
+ }
590
+ async function fallbackInstanceId() {
591
+ if (!isNode()) return "browser";
592
+ try {
593
+ const os = await import("os");
594
+ return os.hostname() || "unknown-host";
595
+ } catch {
596
+ return "unknown-host";
597
+ }
598
+ }
599
+
565
600
  // src/heartbeat.ts
566
601
  var DEFAULT_INTERVAL_MS = 3e4;
567
602
  var MIN_REPORTED_INTERVAL_MS = 1e3;
568
603
  var MAX_REPORTED_INTERVAL_MS = 9e5;
569
604
  var Heartbeat = class {
570
- constructor(client2, options = {}, instanceId, sampler) {
571
- this.client = client2;
605
+ constructor(client, options = {}, instanceId, sampler) {
606
+ this.client = client;
572
607
  this.sampler = sampler;
573
608
  this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
574
609
  this.instanceId = instanceId;
575
610
  this.name = options.name ?? instanceId;
576
611
  this.metadata = options.metadata;
612
+ this.keepAlive = options.keepAlive ?? false;
577
613
  }
578
614
  intervalMs;
579
615
  instanceId;
580
616
  name;
581
617
  metadata;
618
+ keepAlive;
582
619
  timer;
583
620
  start() {
584
621
  if (this.timer) return;
@@ -594,7 +631,7 @@ var Heartbeat = class {
594
631
  },
595
632
  this.intervalMs * (0.9 + Math.random() * 0.2)
596
633
  );
597
- unrefTimer(this.timer);
634
+ if (!this.keepAlive) unrefTimer(this.timer);
598
635
  }
599
636
  stop() {
600
637
  if (this.timer) {
@@ -628,6 +665,11 @@ function externalListenerCount(event) {
628
665
  var emergencyTasks = [];
629
666
  function addEmergencyTask(task) {
630
667
  emergencyTasks.push(task);
668
+ const forget = () => {
669
+ const index = emergencyTasks.indexOf(task);
670
+ if (index !== -1) emergencyTasks.splice(index, 1);
671
+ };
672
+ task.then(forget, forget);
631
673
  }
632
674
  async function awaitEmergencyTasks(capMs) {
633
675
  if (emergencyTasks.length === 0) return;
@@ -640,13 +682,13 @@ var installed = false;
640
682
  function hasInstalledGlobalHandlers() {
641
683
  return installed;
642
684
  }
643
- function installGlobalHandlers(client2, getScope) {
685
+ function installGlobalHandlers(client) {
644
686
  if (installed) return () => void 0;
645
687
  installed = true;
646
688
  if (isNode()) {
647
689
  const onUncaught = markInternal((error) => {
648
- client2.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } }, getScope());
649
- void client2.flush(2e3).then(async () => {
690
+ client.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } });
691
+ void client.flush(2e3).then(async () => {
650
692
  if (externalListenerCount("uncaughtException") === 0) {
651
693
  await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
652
694
  console.error(error);
@@ -655,11 +697,11 @@ function installGlobalHandlers(client2, getScope) {
655
697
  });
656
698
  });
657
699
  const onRejection = markInternal((reason) => {
658
- client2.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } }, getScope());
659
- void client2.flush(2e3);
700
+ client.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } });
701
+ void client.flush(2e3);
660
702
  });
661
703
  const onBeforeExit = markInternal(() => {
662
- void client2.flush(2e3);
704
+ void client.flush(2e3);
663
705
  });
664
706
  process.on("uncaughtException", onUncaught);
665
707
  process.on("unhandledRejection", onRejection);
@@ -673,13 +715,13 @@ function installGlobalHandlers(client2, getScope) {
673
715
  }
674
716
  if (typeof window !== "undefined") {
675
717
  const onError = (event) => {
676
- client2.captureException(event.error ?? event.message, { tags: { mechanism: "onerror" } }, getScope());
718
+ client.captureException(event.error ?? event.message, { tags: { mechanism: "onerror" } });
677
719
  };
678
720
  const onRejection = (event) => {
679
- client2.captureException(event.reason, { tags: { mechanism: "onunhandledrejection" } }, getScope());
721
+ client.captureException(event.reason, { tags: { mechanism: "onunhandledrejection" } });
680
722
  };
681
723
  const onPageHide = () => {
682
- void client2.flush(1e3);
724
+ void client.flush(1e3);
683
725
  };
684
726
  window.addEventListener("error", onError);
685
727
  window.addEventListener("unhandledrejection", onRejection);
@@ -819,313 +861,112 @@ var MachineSampler = class {
819
861
  }
820
862
  };
821
863
 
822
- // src/scope.ts
823
- var Scope = class _Scope {
824
- tags = {};
825
- user;
826
- contexts = {};
827
- extra = {};
828
- type;
829
- fingerprint;
830
- breadcrumbs = [];
831
- setTag(key, value) {
832
- this.tags[key] = value;
833
- return this;
834
- }
835
- setTags(tags) {
836
- Object.assign(this.tags, tags);
837
- return this;
838
- }
839
- setUser(user) {
840
- this.user = user ?? void 0;
841
- return this;
842
- }
843
- setContext(name, context) {
844
- if (context === null) {
845
- delete this.contexts[name];
846
- } else {
847
- this.contexts[name] = context;
848
- }
849
- return this;
850
- }
851
- setExtra(key, value) {
852
- this.extra[key] = value;
853
- return this;
864
+ // src/tasks/api.ts
865
+ var POLL_TIMEOUT_MS = 3e4;
866
+ var UPDATE_TIMEOUT_MS = 1e4;
867
+ var ATTACHMENT_TERMINAL_STATUSES = [429, 501, 503];
868
+ var ITEMS_TERMINAL_STATUSES = [429];
869
+ var UPLOAD_TIMEOUT_MS = 3e4;
870
+ var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
871
+ var TasksApiError = class extends Error {
872
+ constructor(message, status) {
873
+ super(message);
874
+ this.status = status;
875
+ this.name = "TasksApiError";
854
876
  }
855
- setExtras(extras) {
856
- Object.assign(this.extra, extras);
857
- return this;
877
+ };
878
+ async function withRequestTimeout(timeoutMs, external, send) {
879
+ const { signal, cleanup } = withTimeoutSignal(timeoutMs, external);
880
+ try {
881
+ return await send(signal);
882
+ } finally {
883
+ cleanup();
858
884
  }
859
- setType(type) {
860
- this.type = type;
861
- return this;
885
+ }
886
+ var TasksApi = class {
887
+ baseUrl;
888
+ key;
889
+ logger;
890
+ constructor(options) {
891
+ this.baseUrl = options.baseUrl;
892
+ this.key = options.key;
893
+ this.logger = options.logger;
862
894
  }
863
- setFingerprint(fingerprint) {
864
- this.fingerprint = fingerprint;
865
- return this;
895
+ headers() {
896
+ return {
897
+ "content-type": "application/json",
898
+ authorization: `Bearer ${this.key}`
899
+ };
866
900
  }
867
- addBreadcrumb(crumb, maxBreadcrumbs) {
868
- this.breadcrumbs.push({ timestamp: nowISO(), ...crumb });
869
- if (this.breadcrumbs.length > maxBreadcrumbs) {
870
- this.breadcrumbs.splice(0, this.breadcrumbs.length - maxBreadcrumbs);
901
+ /** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
902
+ async poll(request, signal) {
903
+ const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
904
+ let response;
905
+ try {
906
+ response = await fetch(`${this.baseUrl}/poll`, {
907
+ method: "POST",
908
+ headers: this.headers(),
909
+ body: JSON.stringify(request),
910
+ signal: timeoutSignal
911
+ });
912
+ } finally {
913
+ cleanup();
871
914
  }
872
- return this;
915
+ if (!response.ok) {
916
+ throw new TasksApiError(`poll falhou (HTTP ${response.status})`, response.status);
917
+ }
918
+ return this.parsePollResponse(await response.json(), request.max ?? 1);
873
919
  }
874
- clearBreadcrumbs() {
875
- this.breadcrumbs = [];
876
- return this;
920
+ /**
921
+ * Abre uma run por conta própria (modo start). Um request só — como no poll, o
922
+ * ladder de espera entre tentativas é decisão do chamador (LocalRunner).
923
+ * 4xx (401 = chave rejeitada) vira TasksApiError com o status.
924
+ */
925
+ async createRun(request, signal) {
926
+ const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
927
+ let response;
928
+ try {
929
+ response = await fetch(`${this.baseUrl}/runs`, {
930
+ method: "POST",
931
+ headers: this.headers(),
932
+ body: JSON.stringify(request),
933
+ signal: timeoutSignal
934
+ });
935
+ } finally {
936
+ cleanup();
937
+ }
938
+ if (!response.ok) {
939
+ throw new TasksApiError(`cria\xE7\xE3o da run falhou (HTTP ${response.status})`, response.status);
940
+ }
941
+ const body = await response.json().catch(() => null);
942
+ if (typeof body?.id !== "string" || typeof body.task !== "string") {
943
+ throw new TasksApiError("resposta inv\xE1lida da cria\xE7\xE3o de run (id/task ausentes)", 0);
944
+ }
945
+ return {
946
+ id: body.id,
947
+ task: body.task,
948
+ params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
949
+ form: null,
950
+ source: typeof body.source === "string" ? body.source : "agent",
951
+ created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
952
+ };
877
953
  }
878
- clone() {
879
- const next = new _Scope();
880
- next.tags = { ...this.tags };
881
- next.user = this.user ? { ...this.user } : void 0;
882
- next.contexts = Object.fromEntries(Object.entries(this.contexts).map(([k, v]) => [k, { ...v }]));
883
- next.extra = { ...this.extra };
884
- next.type = this.type;
885
- next.fingerprint = this.fingerprint ? [...this.fingerprint] : void 0;
886
- next.breadcrumbs = [...this.breadcrumbs];
887
- return next;
888
- }
889
- };
890
-
891
- // src/transaction.ts
892
- function stepId() {
893
- return uuid4().slice(0, 16);
894
- }
895
- function stepLabel(op, description) {
896
- return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
897
- }
898
- function levelForStatus(status) {
899
- switch (status) {
900
- case "warning":
901
- return "warning";
902
- case "error":
903
- return "error";
904
- case "cancelled":
905
- return "warning";
906
- default:
907
- return "success";
908
- }
909
- }
910
- function colorForStatus(status) {
911
- switch (status) {
912
- case "warning":
913
- return "yellow";
914
- case "error":
915
- return "red";
916
- case "cancelled":
917
- return "gray";
918
- default:
919
- return "green";
920
- }
921
- }
922
- var Step = class {
923
- constructor(transaction, parentStepId, op, description) {
924
- this.transaction = transaction;
925
- this.parentStepId = parentStepId;
926
- this.op = op;
927
- this.description = description;
928
- }
929
- stepId = stepId();
930
- startTimestamp = nowISO();
931
- status = "ok";
932
- endTimestamp;
933
- data;
934
- setStatus(status) {
935
- this.status = status;
936
- return this;
937
- }
938
- setData(key, value) {
939
- this.data = { ...this.data, [key]: value };
940
- return this;
941
- }
942
- /** Step aninhada (filha desta step). */
943
- startChild(op, description) {
944
- return this.transaction.createStep(this.stepId, op, description);
945
- }
946
- finish() {
947
- if (this.endTimestamp) return;
948
- this.endTimestamp = nowISO();
949
- this.transaction.emit(
950
- "step",
951
- "finish",
952
- stepLabel(this.op, this.description),
953
- this.startTimestamp,
954
- this.endTimestamp,
955
- this.status
956
- );
957
- }
958
- toJSON() {
959
- return {
960
- step_id: this.stepId,
961
- parent_step_id: this.parentStepId,
962
- op: this.op,
963
- description: this.description,
964
- status: this.status,
965
- start_timestamp: this.startTimestamp,
966
- timestamp: this.endTimestamp ?? nowISO(),
967
- data: this.data
968
- };
969
- }
970
- };
971
- var Transaction = class {
972
- constructor(client2, name, op, sampled = true, runId) {
973
- this.client = client2;
974
- this.name = name;
975
- this.op = op;
976
- this.sampled = sampled;
977
- this.runId = runId;
978
- this.emit("transaction", "start", op ? `${name} (${op})` : name);
979
- }
980
- eventId = uuid4();
981
- stepId = stepId();
982
- startTimestamp = nowISO();
983
- status = "ok";
984
- steps = [];
985
- tags = {};
986
- finished = false;
987
- lifecycleSink;
988
- // eventos emitidos antes do sink existir (o START da transaction sai no construtor,
989
- // antes do RunReporter conectar) — replay no setLifecycleSink
990
- lifecycleBacklog = [];
991
- /**
992
- * Registra o início/fim de uma step ou transaction: espelha a linha no sink de
993
- * lifecycle, se conectado (é assim que os eventos de performance aparecem nos
994
- * logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
995
- * escreve no stdout do processo do usuário sem permissão.
996
- */
997
- emit(kind, phase, label, startTs, endTs, status) {
998
- const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
999
- const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
1000
- if (this.client?.debug) {
1001
- const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
1002
- console.log(`${tag} ${label}${suffix}`);
1003
- }
1004
- const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
1005
- const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
1006
- if (this.lifecycleSink) this.lifecycleSink(level, message);
1007
- else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
1008
- }
1009
- /** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
1010
- setLifecycleSink(sink) {
1011
- this.lifecycleSink = sink;
1012
- const backlog = this.lifecycleBacklog;
1013
- this.lifecycleBacklog = [];
1014
- for (const [level, message] of backlog) sink(level, message);
1015
- }
1016
- createStep(parentStepId, op, description) {
1017
- const step = new Step(this, parentStepId, op, description);
1018
- this.steps.push(step);
1019
- this.emit("step", "start", stepLabel(op, description));
1020
- return step;
1021
- }
1022
- /** Step direta da transaction. */
1023
- startChild(op, description) {
1024
- return this.createStep(this.stepId, op, description);
1025
- }
1026
- setTag(key, value) {
1027
- this.tags[key] = value;
1028
- return this;
1029
- }
1030
- setStatus(status) {
1031
- this.status = status;
1032
- return this;
1033
- }
1034
- setName(name) {
1035
- this.name = name;
1036
- return this;
1037
- }
1038
- finish() {
1039
- if (this.finished) return;
1040
- this.finished = true;
1041
- for (const step of this.steps) step.finish();
1042
- const endTimestamp = nowISO();
1043
- this.emit(
1044
- "transaction",
1045
- "finish",
1046
- this.op ? `${this.name} (${this.op})` : this.name,
1047
- this.startTimestamp,
1048
- endTimestamp,
1049
- this.status
1050
- );
1051
- if (!this.sampled || !this.client) return;
1052
- const payload = {
1053
- event_id: this.eventId,
1054
- name: this.name,
1055
- op: this.op,
1056
- status: this.status,
1057
- start_timestamp: this.startTimestamp,
1058
- timestamp: endTimestamp,
1059
- steps: this.steps.map((step) => step.toJSON()),
1060
- tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
1061
- run_id: this.runId
1062
- };
1063
- this.client.sendTransaction(payload);
1064
- }
1065
- };
1066
-
1067
- // src/tasks/api.ts
1068
- var POLL_TIMEOUT_MS = 3e4;
1069
- var UPDATE_TIMEOUT_MS = 1e4;
1070
- var UPLOAD_TIMEOUT_MS = 3e4;
1071
- var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
1072
- var TasksApiError = class extends Error {
1073
- constructor(message, status) {
1074
- super(message);
1075
- this.status = status;
1076
- this.name = "TasksApiError";
1077
- }
1078
- };
1079
- var TasksApi = class {
1080
- baseUrl;
1081
- key;
1082
- logger;
1083
- constructor(options) {
1084
- this.baseUrl = options.baseUrl;
1085
- this.key = options.key;
1086
- this.logger = options.logger;
1087
- }
1088
- headers() {
1089
- return {
1090
- "content-type": "application/json",
1091
- authorization: `Bearer ${this.key}`
1092
- };
1093
- }
1094
- /** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
1095
- async poll(request, signal) {
1096
- const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
1097
- let response;
1098
- try {
1099
- response = await fetch(`${this.baseUrl}/poll`, {
1100
- method: "POST",
1101
- headers: this.headers(),
1102
- body: JSON.stringify(request),
1103
- signal: timeoutSignal
1104
- });
1105
- } finally {
1106
- cleanup();
1107
- }
1108
- if (!response.ok) {
1109
- throw new TasksApiError(`poll falhou (HTTP ${response.status})`, response.status);
1110
- }
1111
- return this.parsePollResponse(await response.json(), request.max ?? 1);
1112
- }
1113
- /** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
1114
- parsePollResponse(body, max) {
1115
- const runs = body?.runs;
1116
- if (!Array.isArray(runs)) {
1117
- throw new TasksApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
1118
- }
1119
- const valid = [];
1120
- for (const run of runs.slice(0, Math.max(0, max))) {
1121
- if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
1122
- const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
1123
- valid.push({ ...run, id: run.id, task: run.task, params });
1124
- }
1125
- if (valid.length < runs.length) {
1126
- this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
1127
- }
1128
- return { runs: valid };
954
+ /** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
955
+ parsePollResponse(body, max) {
956
+ const runs = body?.runs;
957
+ if (!Array.isArray(runs)) {
958
+ throw new TasksApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
959
+ }
960
+ const valid = [];
961
+ for (const run of runs.slice(0, Math.max(0, max))) {
962
+ if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
963
+ const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
964
+ valid.push({ ...run, id: run.id, task: run.task, params });
965
+ }
966
+ if (valid.length < runs.length) {
967
+ this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
968
+ }
969
+ return { runs: valid };
1129
970
  }
1130
971
  /**
1131
972
  * PATCH de status/logs com retry em falha de rede/5xx/429.
@@ -1178,12 +1019,9 @@ var TasksApi = class {
1178
1019
  body: JSON.stringify(request),
1179
1020
  signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
1180
1021
  }),
1181
- { logger: this.logger, label: `itens da run ${runId}` }
1022
+ { logger: this.logger, label: `itens da run ${runId}`, noRetryStatuses: ITEMS_TERMINAL_STATUSES }
1182
1023
  );
1183
1024
  } catch (error) {
1184
- if (error instanceof Error && error.message === "HTTP 429") {
1185
- throw new TasksApiError("envio de itens rejeitado (HTTP 429)", 429);
1186
- }
1187
1025
  throw new Error(`envio de itens da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
1188
1026
  }
1189
1027
  if (!response.ok) {
@@ -1200,21 +1038,19 @@ var TasksApi = class {
1200
1038
  * "esta instalação não tem anexos" — o RunReporter degrada em silêncio a partir daí.
1201
1039
  */
1202
1040
  async createAttachment(runId, request, signal) {
1203
- const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
1204
- let response;
1205
- try {
1206
- response = await fetchWithRetry(
1207
- () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments`, {
1041
+ const response = await fetchWithRetry(
1042
+ () => withRequestTimeout(
1043
+ UPDATE_TIMEOUT_MS,
1044
+ signal,
1045
+ (timeoutSignal) => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments`, {
1208
1046
  method: "POST",
1209
1047
  headers: this.headers(),
1210
1048
  body: JSON.stringify(request),
1211
1049
  signal: timeoutSignal
1212
- }),
1213
- { logger: this.logger, label: `anexo da run ${runId}` }
1214
- );
1215
- } finally {
1216
- cleanup();
1217
- }
1050
+ })
1051
+ ),
1052
+ { logger: this.logger, label: `anexo da run ${runId}`, noRetryStatuses: ATTACHMENT_TERMINAL_STATUSES }
1053
+ );
1218
1054
  if (!response.ok) {
1219
1055
  throw new TasksApiError(`cria\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
1220
1056
  }
@@ -1234,22 +1070,20 @@ var TasksApi = class {
1234
1070
  * está na assinatura da URL, e mandar o client secret para fora da API seria vazamento.
1235
1071
  */
1236
1072
  async uploadAttachment(url, headers, body, signal) {
1237
- const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPLOAD_TIMEOUT_MS, signal);
1238
- let response;
1239
- try {
1240
- response = await fetchWithRetry(
1241
- () => fetch(url, {
1073
+ const response = await fetchWithRetry(
1074
+ () => withRequestTimeout(
1075
+ UPLOAD_TIMEOUT_MS,
1076
+ signal,
1077
+ (timeoutSignal) => fetch(url, {
1242
1078
  method: "PUT",
1243
1079
  headers,
1244
1080
  // BodyInit não aceita Uint8Array<ArrayBufferLike> em todos os lib.dom; o buffer é o mesmo
1245
1081
  body,
1246
1082
  signal: timeoutSignal
1247
- }),
1248
- { delays: UPLOAD_RETRY_DELAYS_MS, logger: this.logger, label: "upload de anexo" }
1249
- );
1250
- } finally {
1251
- cleanup();
1252
- }
1083
+ })
1084
+ ),
1085
+ { delays: UPLOAD_RETRY_DELAYS_MS, logger: this.logger, label: "upload de anexo" }
1086
+ );
1253
1087
  if (!response.ok) {
1254
1088
  throw new TasksApiError(`upload de anexo rejeitado (HTTP ${response.status})`, response.status);
1255
1089
  }
@@ -1271,14 +1105,87 @@ var TasksApi = class {
1271
1105
  }
1272
1106
  };
1273
1107
 
1274
- // src/tasks/attachments.ts
1275
- var MAX_ATTACHMENT_BYTES = 5242880;
1276
- var MAX_ATTACHMENTS_PER_RUN = 20;
1277
- var MAX_CONCURRENT_UPLOADS = 3;
1278
- var MAX_CAPTION_CHARS = 200;
1279
- var ATTACHMENT_DRAIN_MS = 15e3;
1280
- async function readImageFile(path) {
1281
- if (!isNode()) {
1108
+ // src/tasks/backoff.ts
1109
+ var RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
1110
+ var AUTH_FAILURE_INTERVAL_MS = 3e4;
1111
+ var Backoff = class {
1112
+ constructor(logger, label, floorMs = 0) {
1113
+ this.logger = logger;
1114
+ this.label = label;
1115
+ this.floorMs = floorMs;
1116
+ }
1117
+ failures = 0;
1118
+ authFailures = 0;
1119
+ reset() {
1120
+ this.failures = 0;
1121
+ this.authFailures = 0;
1122
+ }
1123
+ next(error) {
1124
+ if (error instanceof TasksApiError && (error.status === 401 || error.status === 402)) {
1125
+ this.authFailures++;
1126
+ if (this.authFailures === 1 || this.authFailures % 10 === 0) {
1127
+ this.logger.error(
1128
+ 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"
1129
+ );
1130
+ }
1131
+ return Math.max(this.floorMs, AUTH_FAILURE_INTERVAL_MS);
1132
+ }
1133
+ this.failures++;
1134
+ const ladder = RETRY_DELAYS_MS[Math.min(this.failures - 1, RETRY_DELAYS_MS.length - 1)];
1135
+ const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.floorMs, 16e3));
1136
+ this.logger.warn(`${this.label} falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
1137
+ return delay;
1138
+ }
1139
+ };
1140
+
1141
+ // src/tasks/errors.ts
1142
+ function runCanceledError() {
1143
+ return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
1144
+ }
1145
+ function taskTimeoutError(timeoutMs) {
1146
+ return Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
1147
+ name: "TaskTimeout"
1148
+ });
1149
+ }
1150
+ var TERMINAL_ERROR_NAMES = /* @__PURE__ */ new Set(["RunCanceled", "AbortError", "TaskTimeout"]);
1151
+ function isTerminalError(error) {
1152
+ return error instanceof Error && TERMINAL_ERROR_NAMES.has(error.name);
1153
+ }
1154
+
1155
+ // src/retry.ts
1156
+ var DEFAULT_RETRIES = 3;
1157
+ var DEFAULT_DELAY_MS = 1e3;
1158
+ function resolveDelay(delay, attempt, fallbackMs) {
1159
+ const value = typeof delay === "function" ? delay(attempt) : delay ?? fallbackMs;
1160
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1161
+ }
1162
+ async function retryWithBackoff(fn, options = {}) {
1163
+ const requested = options.retries ?? DEFAULT_RETRIES;
1164
+ const retries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_RETRIES;
1165
+ const wait = options.wait ?? sleep;
1166
+ for (let attempt = 1; ; attempt++) {
1167
+ if (options.signal?.aborted) throw runCanceledError();
1168
+ try {
1169
+ return await fn(attempt);
1170
+ } catch (error) {
1171
+ const exhausted = attempt > retries;
1172
+ const retriable = !isTerminalError(error) && !options.signal?.aborted && (options.shouldRetry?.(error) ?? true);
1173
+ if (exhausted || !retriable) throw error;
1174
+ const delay = resolveDelay(options.delayMs, attempt, DEFAULT_DELAY_MS);
1175
+ options.onRetry?.(error, attempt, delay);
1176
+ await wait(delay);
1177
+ }
1178
+ }
1179
+ }
1180
+
1181
+ // src/tasks/attachments.ts
1182
+ var MAX_ATTACHMENT_BYTES = 5242880;
1183
+ var MAX_ATTACHMENTS_PER_RUN = 20;
1184
+ var MAX_CONCURRENT_UPLOADS = 3;
1185
+ var MAX_CAPTION_CHARS = 200;
1186
+ var ATTACHMENT_DRAIN_MS = 15e3;
1187
+ async function readImageFile(path) {
1188
+ if (!isNode()) {
1282
1189
  throw new Error("caminho de arquivo s\xF3 \xE9 suportado em Node");
1283
1190
  }
1284
1191
  const { readFile } = await import("fs/promises");
@@ -1289,6 +1196,7 @@ function startsWith(bytes, signature, offset = 0) {
1289
1196
  return signature.every((byte, index) => bytes[offset + index] === byte);
1290
1197
  }
1291
1198
  var PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
1199
+ var MAX_PNG_DIMENSION = 2e4;
1292
1200
  var JPEG_MAGIC = [255, 216, 255];
1293
1201
  var RIFF_MAGIC = [82, 73, 70, 70];
1294
1202
  var WEBP_MAGIC = [87, 69, 66, 80];
@@ -1304,6 +1212,7 @@ function readPngDimensions(bytes) {
1304
1212
  const width = view.getUint32(16);
1305
1213
  const height = view.getUint32(20);
1306
1214
  if (width === 0 || height === 0) return void 0;
1215
+ if (width > MAX_PNG_DIMENSION || height > MAX_PNG_DIMENSION) return void 0;
1307
1216
  return { width, height };
1308
1217
  }
1309
1218
  async function toBytes(input) {
@@ -1320,16 +1229,17 @@ var LOG_LEVEL_COLOR = {
1320
1229
  error: "red",
1321
1230
  success: "green"
1322
1231
  };
1232
+ function joinLogArgs(message, rest) {
1233
+ if (rest.length === 0) return safeString(message);
1234
+ return [message, ...rest].map(safeString).join(" ");
1235
+ }
1323
1236
  function printRunLog(task, level, message) {
1324
1237
  const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
1325
- const method = level === "warning" ? "warn" : level === "success" ? "info" : level;
1326
- console[method](line, message);
1327
- }
1328
- function runCanceledError() {
1329
- return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
1238
+ const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
1239
+ console[method2](line, message);
1330
1240
  }
1331
1241
  function cancelableSleep(ms, signal) {
1332
- return new Promise((resolve, reject) => {
1242
+ return new Promise((resolve2, reject) => {
1333
1243
  if (signal.aborted) {
1334
1244
  reject(runCanceledError());
1335
1245
  return;
@@ -1340,7 +1250,7 @@ function cancelableSleep(ms, signal) {
1340
1250
  };
1341
1251
  const timer = setTimeout(() => {
1342
1252
  signal.removeEventListener("abort", onAbort);
1343
- resolve();
1253
+ resolve2();
1344
1254
  }, ms);
1345
1255
  signal.addEventListener("abort", onAbort, { once: true });
1346
1256
  });
@@ -1370,7 +1280,7 @@ function scrubPayloadValue(value, depth) {
1370
1280
  return out;
1371
1281
  }
1372
1282
  var RunReporter = class {
1373
- constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs) {
1283
+ constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
1374
1284
  this.api = api;
1375
1285
  this.runId = runId;
1376
1286
  this.logger = logger;
@@ -1380,24 +1290,47 @@ var RunReporter = class {
1380
1290
  Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
1381
1291
  );
1382
1292
  const log = {
1383
- debug: (message) => this.log("debug", message),
1384
- info: (message) => this.log("info", message),
1385
- warn: (message) => this.log("warning", message),
1386
- error: (message) => this.log("error", message),
1387
- success: (message) => this.log("success", message)
1293
+ debug: (message, ...rest) => this.log("debug", joinLogArgs(message, rest)),
1294
+ info: (message, ...rest) => this.log("info", joinLogArgs(message, rest)),
1295
+ warn: (message, ...rest) => this.log("warning", joinLogArgs(message, rest)),
1296
+ error: (message, ...rest) => this.log("error", joinLogArgs(message, rest)),
1297
+ success: (message, ...rest) => this.log("success", joinLogArgs(message, rest))
1388
1298
  };
1389
1299
  tx.setLifecycleSink((level, message) => this.push(level, message));
1300
+ const checkCanceled = () => this.checkCanceled();
1301
+ tx.setStepGuard(checkCanceled);
1390
1302
  const items = {
1391
- add: (item) => this.addItem(item.status, item.message, item.payload, item.id),
1392
- succeeded: (payload, options) => this.addItem("succeeded", void 0, payload, options?.id),
1393
- failed: (message, payload, options) => this.addItem("failed", message, payload, options?.id),
1394
- occurrence: (message, payload, options) => this.addItem("occurrence", message, payload, options?.id)
1303
+ add: (item) => {
1304
+ checkCanceled();
1305
+ this.addItem(item.status, item.message, item.payload, item.id);
1306
+ },
1307
+ succeeded: (payload, options) => {
1308
+ checkCanceled();
1309
+ this.addItem("succeeded", void 0, payload, options?.id);
1310
+ },
1311
+ failed: (message, payload, options) => {
1312
+ checkCanceled();
1313
+ this.addItem("failed", message, payload, options?.id);
1314
+ },
1315
+ occurrence: (message, payload, options) => {
1316
+ checkCanceled();
1317
+ this.addItem("occurrence", message, payload, options?.id);
1318
+ }
1395
1319
  };
1396
1320
  const progress = Object.defineProperties(
1397
1321
  {
1398
- total: (total) => this.setProgress(this.progressCurrent, total),
1399
- advance: (count = 1) => this.setProgress(this.progressCurrent + count, this.progressTotal),
1400
- set: (current, total) => this.setProgress(current, total ?? this.progressTotal)
1322
+ total: (total) => {
1323
+ checkCanceled();
1324
+ this.setProgress(this.progressCurrent, total);
1325
+ },
1326
+ advance: (count = 1) => {
1327
+ checkCanceled();
1328
+ this.setProgress(this.progressCurrent + count, this.progressTotal);
1329
+ },
1330
+ set: (current, total) => {
1331
+ checkCanceled();
1332
+ this.setProgress(current, total ?? this.progressTotal);
1333
+ }
1401
1334
  },
1402
1335
  {
1403
1336
  current: { enumerable: true, get: () => this.progressCurrent },
@@ -1409,18 +1342,20 @@ var RunReporter = class {
1409
1342
  // defineProperties não tipa os getters adicionados — a interface é a fonte de verdade
1410
1343
  );
1411
1344
  const signal = this.abortController.signal;
1345
+ const sleep2 = (ms) => cancelableSleep(ms, signal);
1412
1346
  this.context = {
1413
1347
  runId,
1414
1348
  task,
1349
+ params,
1415
1350
  log,
1416
1351
  items,
1417
1352
  progress,
1418
- tx,
1353
+ transaction: tx,
1419
1354
  signal,
1420
- throwIfCanceled: () => {
1421
- if (signal.aborted) throw runCanceledError();
1422
- },
1423
- sleep: (ms) => cancelableSleep(ms, signal)
1355
+ throwIfCanceled: checkCanceled,
1356
+ sleep: sleep2,
1357
+ attach: (image, options) => this.attach(image, options),
1358
+ retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
1424
1359
  };
1425
1360
  }
1426
1361
  context;
@@ -1469,6 +1404,10 @@ var RunReporter = class {
1469
1404
  get isDead() {
1470
1405
  return this.dead;
1471
1406
  }
1407
+ /** Lança `RunCanceled` se o signal desta run já disparou. */
1408
+ checkCanceled() {
1409
+ if (this.abortController.signal.aborted) throw runCanceledError();
1410
+ }
1472
1411
  /** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
1473
1412
  abort() {
1474
1413
  this.stopCancelWatch();
@@ -1505,7 +1444,7 @@ var RunReporter = class {
1505
1444
  }
1506
1445
  log(level, message) {
1507
1446
  if (this.dead) return;
1508
- const scrubbed = scrubSecrets(message);
1447
+ const scrubbed = scrubSecrets(safeString(message));
1509
1448
  printRunLog(this.context.task, level, scrubbed);
1510
1449
  this.push(level, scrubbed, true);
1511
1450
  }
@@ -1564,13 +1503,25 @@ var RunReporter = class {
1564
1503
  this.stopCancelWatch();
1565
1504
  this.clearFlushTimer();
1566
1505
  this.forceFinalProgress();
1567
- this.enqueue({ status: "succeeded", logs: this.takeLogs() });
1506
+ this.enqueue({ status: "succeeded", logs: this.takeLogsForTerminal() });
1568
1507
  }
1569
1508
  reportFailure(error) {
1570
1509
  this.stopCancelWatch();
1571
1510
  this.clearFlushTimer();
1572
1511
  this.forceFinalProgress();
1573
- this.enqueue({ status: "failed", error, logs: this.takeLogs() });
1512
+ this.enqueue({ status: "failed", error, logs: this.takeLogsForTerminal() });
1513
+ }
1514
+ /**
1515
+ * Esvazia o buffer para o PATCH terminal respeitando o teto de 100 logs por PATCH:
1516
+ * o excedente sai antes, em PATCHes `running` na mesma cadeia (a ordem se mantém).
1517
+ */
1518
+ takeLogsForTerminal() {
1519
+ let logs = this.takeLogs();
1520
+ while (logs.length > MAX_LOGS_PER_PATCH) {
1521
+ this.enqueue({ status: "running", logs: logs.slice(0, MAX_LOGS_PER_PATCH) });
1522
+ logs = logs.slice(MAX_LOGS_PER_PATCH);
1523
+ }
1524
+ return logs;
1574
1525
  }
1575
1526
  // ── Progresso ────────────────────────────────────────────────────────────────
1576
1527
  /** Valida, clampa e marca o snapshot como pendente; agenda flush só se nada mais vai sair. */
@@ -1678,7 +1629,7 @@ var RunReporter = class {
1678
1629
  id: id ? truncate(String(id), 255) : uuid4(),
1679
1630
  seq: this.itemSeq++,
1680
1631
  status,
1681
- ...message ? { message: truncate(scrubSecrets(message), MAX_ITEM_MESSAGE_CHARS) } : {},
1632
+ ...message ? { message: truncate(scrubSecrets(safeString(message)), MAX_ITEM_MESSAGE_CHARS) } : {},
1682
1633
  ...scrubbedPayload ? { payload: scrubbedPayload } : {},
1683
1634
  ts: nowISO()
1684
1635
  });
@@ -1747,13 +1698,13 @@ var RunReporter = class {
1747
1698
  this.flushItems();
1748
1699
  await Promise.race([
1749
1700
  this.itemsChain,
1750
- new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1701
+ new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
1751
1702
  ]);
1752
1703
  if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
1753
1704
  this.flushItems();
1754
1705
  await Promise.race([
1755
1706
  this.itemsChain,
1756
- new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1707
+ new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
1757
1708
  ]);
1758
1709
  }
1759
1710
  }
@@ -1761,14 +1712,16 @@ var RunReporter = class {
1761
1712
  /**
1762
1713
  * Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
1763
1714
  * o upload em segundo plano. Nunca lança: todo caminho de erro vira `{ ok: false }`
1764
- * com um aviso no log da run. Chega ao handler pelos capture* do SDK — o hint
1765
- * `attachment` de Hobots.captureException/captureMessage cai aqui via run-context-store.
1715
+ * com um aviso no log da run. É o `hobots.attach()`; o hint `attachment` de
1716
+ * `hobots.captureException` também cai aqui via run-context-store.
1766
1717
  */
1767
1718
  async attach(image, options = {}) {
1768
1719
  const id = uuid4();
1769
1720
  const level = options.level ?? "info";
1770
- const caption = options.caption ? truncate(scrubSecrets(options.caption), MAX_CAPTION_CHARS) : void 0;
1771
- const message = options.message ?? options.caption ?? "captura de tela";
1721
+ const rawCaption = options.caption !== void 0 ? safeString(options.caption) : void 0;
1722
+ const rawMessage = options.message !== void 0 ? safeString(options.message) : void 0;
1723
+ const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
1724
+ const message = rawMessage ?? rawCaption ?? "captura de tela";
1772
1725
  if (this.dead) return { id, ok: false, reason: "canceled" };
1773
1726
  if (this.attachmentsUnsupported) {
1774
1727
  this.log(level, message);
@@ -1784,8 +1737,8 @@ var RunReporter = class {
1784
1737
  printRunLog(this.context.task, level, `${scrubbed} [imagem]`);
1785
1738
  const logTs = this.push(level, scrubbed, true, [id]);
1786
1739
  let settle;
1787
- const result = new Promise((resolve) => {
1788
- settle = resolve;
1740
+ const result = new Promise((resolve2) => {
1741
+ settle = resolve2;
1789
1742
  });
1790
1743
  const work = this.uploadAttachment(id, image, { caption, logTs, contentType: options.contentType }).then((reason) => settle(reason ? { id, ok: false, reason } : { id, ok: true })).catch(() => settle({ id, ok: false, reason: "upload-failed" }));
1791
1744
  this.pendingAttachments.add(work);
@@ -1867,8 +1820,8 @@ var RunReporter = class {
1867
1820
  this.uploadSlots--;
1868
1821
  return Promise.resolve(release);
1869
1822
  }
1870
- return new Promise((resolve) => {
1871
- this.uploadQueue.push(() => resolve(release));
1823
+ return new Promise((resolve2) => {
1824
+ this.uploadQueue.push(() => resolve2(release));
1872
1825
  });
1873
1826
  }
1874
1827
  /**
@@ -1879,7 +1832,7 @@ var RunReporter = class {
1879
1832
  if (this.pendingAttachments.size === 0) return;
1880
1833
  await Promise.race([
1881
1834
  Promise.allSettled([...this.pendingAttachments]),
1882
- new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1835
+ new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
1883
1836
  ]);
1884
1837
  }
1885
1838
  enqueue(updateIn) {
@@ -1950,166 +1903,371 @@ async function runWithRunContext(store, fn) {
1950
1903
  await ensureStorage();
1951
1904
  return storage ? storage.run(store, fn) : fn();
1952
1905
  }
1953
- function currentRunTransaction() {
1954
- return storage?.getStore()?.tx;
1955
- }
1956
- function currentRunAttach() {
1957
- return storage?.getStore()?.attach;
1906
+ function currentRunReporter() {
1907
+ return storage?.getStore()?.reporter;
1958
1908
  }
1959
1909
 
1960
- // src/tasks/shutdown.ts
1961
- var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
1962
- function installShutdownHandlers(agent2) {
1963
- if (!isNode()) return () => {
1964
- };
1965
- let signaled = false;
1966
- const signalHandlers = /* @__PURE__ */ new Map();
1967
- for (const signal of WATCHED_SIGNALS) {
1968
- const handler = markInternal(() => {
1969
- if (externalListenerCount(signal) > 0) return;
1970
- if (signaled) return;
1971
- signaled = true;
1972
- const reraise = () => {
1973
- process.removeListener(signal, handler);
1974
- process.kill(process.pid, signal);
1975
- };
1976
- if (agent2.activeCount === 0) {
1977
- reraise();
1978
- return;
1979
- }
1980
- const report = agent2.reportAgentTerminated(`o processo do agente recebeu ${signal} durante a execu\xE7\xE3o`);
1981
- addEmergencyTask(report);
1982
- void report.finally(reraise);
1983
- });
1984
- process.on(signal, handler);
1985
- signalHandlers.set(signal, handler);
1910
+ // src/transaction.ts
1911
+ function stepId() {
1912
+ return uuid4().slice(0, 16);
1913
+ }
1914
+ function stepLabel(op, description) {
1915
+ return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
1916
+ }
1917
+ function levelForStatus(status) {
1918
+ switch (status) {
1919
+ case "warning":
1920
+ return "warning";
1921
+ case "error":
1922
+ return "error";
1923
+ case "cancelled":
1924
+ return "warning";
1925
+ default:
1926
+ return "success";
1986
1927
  }
1987
- const onFatal = markInternal((error) => {
1988
- const report = agent2.activeCount > 0 ? agent2.reportAgentTerminated("o processo do agente morreu com um erro n\xE3o tratado durante a execu\xE7\xE3o") : Promise.resolve();
1989
- addEmergencyTask(report);
1990
- if (!hasInstalledGlobalHandlers() && externalListenerCount("uncaughtException") === 0) {
1991
- void report.finally(() => {
1992
- console.error(error);
1993
- process.exit(1);
1994
- });
1995
- }
1996
- });
1997
- process.on("uncaughtException", onFatal);
1998
- return () => {
1999
- for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
2000
- process.removeListener("uncaughtException", onFatal);
2001
- };
2002
1928
  }
2003
-
2004
- // src/tasks/agent.ts
2005
- var DEFAULT_POLL_INTERVAL_MS = 2e3;
2006
- var MIN_POLL_INTERVAL_MS = 1e3;
2007
- var MAX_CONCURRENCY = 5;
2008
- var POLL_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
2009
- var AUTH_FAILURE_INTERVAL_MS = 3e4;
2010
- var DEFAULT_CANCEL_GRACE_MS = 1e4;
2011
- function hostname() {
2012
- if (!isNode()) return void 0;
2013
- return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
1929
+ function colorForStatus(status) {
1930
+ switch (status) {
1931
+ case "warning":
1932
+ return "yellow";
1933
+ case "error":
1934
+ return "red";
1935
+ case "cancelled":
1936
+ return "gray";
1937
+ default:
1938
+ return "green";
1939
+ }
2014
1940
  }
2015
- var TaskAgent = class {
2016
- constructor(options, registry2) {
2017
- this.options = options;
2018
- this.registry = registry2;
2019
- this.logger = makeLogger(options.debug ?? false);
2020
- const config = options.config;
2021
- this.api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger: this.logger });
2022
- this.client = options.client;
2023
- this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
2024
- this.concurrency = Math.min(MAX_CONCURRENCY, Math.max(1, options.concurrency ?? 1));
2025
- this.instanceId = options.instanceId;
2026
- this.taskTimeoutMs = options.taskTimeoutMs;
2027
- this.logFlushIntervalMs = options.logFlushIntervalMs;
2028
- this.cancelCheckIntervalMs = options.cancelCheckIntervalMs;
2029
- this.cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
2030
- this.logger.debug("agente configurado", { baseUrl: config.tasksBaseUrl });
1941
+ var Step = class {
1942
+ constructor(transaction, parentStepId, op, description) {
1943
+ this.transaction = transaction;
1944
+ this.parentStepId = parentStepId;
1945
+ this.op = op;
1946
+ this.description = description;
2031
1947
  }
2032
- logger;
2033
- api;
2034
- client;
2035
- pollIntervalMs;
2036
- concurrency;
2037
- instanceId;
2038
- taskTimeoutMs;
2039
- logFlushIntervalMs;
2040
- cancelCheckIntervalMs;
2041
- cancelGraceMs;
2042
- state = "idle";
2043
- timer;
2044
- pollAbort;
2045
- consecutiveFailures = 0;
2046
- consecutiveAuthFailures = 0;
2047
- activeRuns = /* @__PURE__ */ new Set();
2048
- reporters = /* @__PURE__ */ new Set();
2049
- uninstallShutdown;
2050
- get activeCount() {
2051
- return this.activeRuns.size;
1948
+ stepId = stepId();
1949
+ startTimestamp = nowISO();
1950
+ status = "ok";
1951
+ endTimestamp;
1952
+ data;
1953
+ setStatus(status) {
1954
+ this.status = status;
1955
+ return this;
2052
1956
  }
2053
- start() {
2054
- if (this.state === "started") return;
2055
- if (this.state === "stopping") {
2056
- throw new Error("[hobots] start() chamado durante stop() \u2014 aguarde o stop terminar");
2057
- }
2058
- if (this.registry.size === 0) {
2059
- throw new Error("[hobots] nenhuma task registrada \u2014 chame register() antes de start()");
2060
- }
2061
- this.state = "started";
2062
- if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
2063
- this.uninstallShutdown = installShutdownHandlers(this);
2064
- }
2065
- this.logger.debug(`polling iniciado (tasks: ${[...this.registry.keys()].join(", ")})`);
2066
- this.schedule(0);
1957
+ setData(key, value) {
1958
+ this.data = { ...this.data, [key]: value };
1959
+ return this;
2067
1960
  }
2068
- /** Para o polling e aguarda as runs ativas (até timeoutMs). Retorna true se tudo terminou a tempo. */
2069
- async stop(timeoutMs = 3e4) {
2070
- if (this.state === "idle") return true;
2071
- this.state = "stopping";
2072
- if (this.timer) {
2073
- clearTimeout(this.timer);
2074
- this.timer = void 0;
2075
- }
2076
- this.pollAbort?.abort();
2077
- const all = Promise.allSettled([...this.activeRuns]).then(() => true);
2078
- const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : false;
2079
- if (!finished && this.activeRuns.size > 0) {
2080
- this.logger.warn(`${this.activeRuns.size} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
2081
- await this.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
2082
- }
2083
- this.uninstallShutdown?.();
2084
- this.uninstallShutdown = void 0;
2085
- this.state = "idle";
2086
- return finished;
1961
+ /** Step aninhada (filha desta step). */
1962
+ startChild(op, description) {
1963
+ return this.transaction.createStep(this.stepId, op, description);
1964
+ }
1965
+ finish() {
1966
+ if (this.endTimestamp) return;
1967
+ this.endTimestamp = nowISO();
1968
+ this.transaction.emit(
1969
+ "step",
1970
+ "finish",
1971
+ stepLabel(this.op, this.description),
1972
+ this.startTimestamp,
1973
+ this.endTimestamp,
1974
+ this.status
1975
+ );
1976
+ }
1977
+ toJSON() {
1978
+ return {
1979
+ step_id: this.stepId,
1980
+ parent_step_id: this.parentStepId,
1981
+ op: this.op,
1982
+ description: this.description,
1983
+ status: this.status,
1984
+ start_timestamp: this.startTimestamp,
1985
+ timestamp: this.endTimestamp ?? nowISO(),
1986
+ data: this.data
1987
+ };
1988
+ }
1989
+ };
1990
+ var Transaction = class {
1991
+ constructor(client, name, op, runId) {
1992
+ this.client = client;
1993
+ this.name = name;
1994
+ this.op = op;
1995
+ this.runId = runId;
1996
+ this.emit("transaction", "start", op ? `${name} (${op})` : name);
2087
1997
  }
1998
+ eventId = uuid4();
1999
+ stepId = stepId();
2000
+ startTimestamp = nowISO();
2001
+ status = "ok";
2002
+ steps = [];
2003
+ tags = {};
2004
+ finished = false;
2005
+ lifecycleSink;
2006
+ // instalado pelo RunReporter: abrir uma step é um ponto de parada cooperativo (lança RunCanceled)
2007
+ stepGuard;
2008
+ // eventos emitidos antes do sink existir (o START da transaction sai no construtor,
2009
+ // antes do RunReporter conectar) — replay no setLifecycleSink
2010
+ lifecycleBacklog = [];
2088
2011
  /**
2089
- * Best-effort do exit path: reporta 'failed'/AgentTerminated para as runs ativas
2090
- * antes do processo morrer, fora da fila do reporter e com timeout curto. Runs já
2091
- * canceladas no app (reporter morto) são puladas; erros de rede são engolidos —
2092
- * o sweep server-side é o backstop.
2012
+ * Registra o início/fim de uma step ou transaction: espelha a linha no sink de
2013
+ * lifecycle, se conectado (é assim que os eventos de performance aparecem nos
2014
+ * logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
2015
+ * escreve no stdout do processo do usuário sem permissão.
2093
2016
  */
2094
- async reportAgentTerminated(message, timeoutMs = 2500) {
2095
- const pending = [];
2096
- for (const reporter of this.reporters) {
2097
- if (reporter.isDead) continue;
2098
- const logs = reporter.drainLogs();
2099
- reporter.kill();
2100
- pending.push(
2101
- this.api.updateRunOnce(
2102
- reporter.context.runId,
2103
- { status: "failed", error: { type: "AgentTerminated", message }, logs: logs.length > 0 ? logs : void 0 },
2104
- timeoutMs
2105
- ).catch(() => {
2106
- })
2107
- );
2017
+ emit(kind, phase, label, startTs, endTs, status) {
2018
+ const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
2019
+ const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
2020
+ if (this.client?.debug) {
2021
+ const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
2022
+ console.log(`${tag} ${label}${suffix}`);
2108
2023
  }
2109
- if (pending.length === 0) return;
2110
- await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
2111
- }
2112
- /** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
2024
+ const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
2025
+ const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
2026
+ if (this.lifecycleSink) this.lifecycleSink(level, message);
2027
+ else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
2028
+ }
2029
+ /** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
2030
+ setLifecycleSink(sink) {
2031
+ this.lifecycleSink = sink;
2032
+ const backlog = this.lifecycleBacklog;
2033
+ this.lifecycleBacklog = [];
2034
+ for (const [level, message] of backlog) sink(level, message);
2035
+ }
2036
+ /**
2037
+ * Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
2038
+ * `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
2039
+ */
2040
+ setStepGuard(guard) {
2041
+ this.stepGuard = guard;
2042
+ }
2043
+ createStep(parentStepId, op, description) {
2044
+ this.stepGuard?.();
2045
+ const step = new Step(this, parentStepId, op, description);
2046
+ this.steps.push(step);
2047
+ this.emit("step", "start", stepLabel(op, description));
2048
+ return step;
2049
+ }
2050
+ /** Step direta da transaction. */
2051
+ startChild(op, description) {
2052
+ return this.createStep(this.stepId, op, description);
2053
+ }
2054
+ setTag(key, value) {
2055
+ this.tags[key] = value;
2056
+ return this;
2057
+ }
2058
+ setStatus(status) {
2059
+ this.status = status;
2060
+ return this;
2061
+ }
2062
+ setName(name) {
2063
+ this.name = name;
2064
+ return this;
2065
+ }
2066
+ finish() {
2067
+ if (this.finished) return;
2068
+ this.finished = true;
2069
+ for (const step of this.steps) step.finish();
2070
+ const endTimestamp = nowISO();
2071
+ this.emit(
2072
+ "transaction",
2073
+ "finish",
2074
+ this.op ? `${this.name} (${this.op})` : this.name,
2075
+ this.startTimestamp,
2076
+ endTimestamp,
2077
+ this.status
2078
+ );
2079
+ if (!this.client) return;
2080
+ const payload = {
2081
+ event_id: this.eventId,
2082
+ name: this.name,
2083
+ op: this.op,
2084
+ status: this.status,
2085
+ start_timestamp: this.startTimestamp,
2086
+ timestamp: endTimestamp,
2087
+ steps: this.steps.map((step) => step.toJSON()),
2088
+ tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
2089
+ run_id: this.runId
2090
+ };
2091
+ this.client.sendTransaction(payload);
2092
+ }
2093
+ };
2094
+
2095
+ // src/tasks/runner.ts
2096
+ var DEFAULT_RETRY_DELAY_MS = 5e3;
2097
+ async function executeRun(input) {
2098
+ const { run, task, api, client, logger, tracker } = input;
2099
+ const options = task.options;
2100
+ const tx = new Transaction(client, `run:${run.task}`, "hobots.task", run.id);
2101
+ const reporter = new RunReporter(
2102
+ api,
2103
+ run.id,
2104
+ run.task,
2105
+ logger,
2106
+ tx,
2107
+ options.logFlushIntervalMs,
2108
+ options.cancelCheckIntervalMs,
2109
+ run.params
2110
+ );
2111
+ tracker.add(reporter);
2112
+ try {
2113
+ const ok = await reporter.reportRunning();
2114
+ if (!ok) {
2115
+ logger.debug(`run ${run.id} cancelada antes de iniciar \u2014 handler n\xE3o executado`);
2116
+ tx.setStatus("cancelled");
2117
+ tx.finish();
2118
+ return;
2119
+ }
2120
+ logger.debug(`executando run ${run.id} (${run.task})`);
2121
+ try {
2122
+ await runWithRunContext({ reporter }, () => invokeWithRetry(task, run, reporter, logger));
2123
+ await reporter.finalizeAttachments();
2124
+ await reporter.finalizeItems();
2125
+ tx.setStatus("ok");
2126
+ tx.finish();
2127
+ reporter.reportSuccess();
2128
+ logger.debug(`run ${run.id} conclu\xEDda`);
2129
+ } catch (error) {
2130
+ await reporter.finalizeAttachments();
2131
+ await reporter.finalizeItems();
2132
+ tx.setStatus("error");
2133
+ tx.finish();
2134
+ reporter.reportFailure(toRunError(error));
2135
+ logger.debug(`run ${run.id} falhou: ${String(error)}`);
2136
+ }
2137
+ await reporter.finalize();
2138
+ } finally {
2139
+ tracker.remove(reporter);
2140
+ }
2141
+ }
2142
+ function invokeWithRetry(task, run, reporter, logger) {
2143
+ const { retries, retryDelayMs } = task.options;
2144
+ const total = retries + 1;
2145
+ const ctx = reporter.context;
2146
+ return retryWithBackoff(() => invokeWithTimeout(task, run, reporter, logger), {
2147
+ retries,
2148
+ delayMs: (attempt) => resolveDelay(retryDelayMs, attempt, DEFAULT_RETRY_DELAY_MS),
2149
+ shouldRetry: () => !reporter.isDead,
2150
+ signal: ctx.signal,
2151
+ wait: (ms) => ctx.sleep(ms),
2152
+ onRetry: (error, attempt, delayMs) => {
2153
+ const reason = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2154
+ ctx.log.warn(`tentativa ${attempt}/${total} falhou: ${truncate(reason, 500)} \u2014 nova tentativa em ${delayMs}ms`);
2155
+ }
2156
+ });
2157
+ }
2158
+ var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
2159
+ var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
2160
+ async function invokeWithTimeout(task, run, reporter, logger) {
2161
+ const { timeoutMs, cancelGraceMs } = task.options;
2162
+ const handler = task.handler;
2163
+ const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
2164
+ const races = [];
2165
+ if (timeoutMs && timeoutMs > 0) {
2166
+ races.push(sleep(timeoutMs).then(() => TIMEOUT_SENTINEL));
2167
+ }
2168
+ if (cancelGraceMs > 0) {
2169
+ races.push(waitForCancelGrace(reporter.context.signal, cancelGraceMs).then(() => CANCEL_SENTINEL));
2170
+ }
2171
+ if (races.length === 0) {
2172
+ await invocation;
2173
+ return;
2174
+ }
2175
+ const outcome = await Promise.race([invocation, ...races]);
2176
+ if (outcome !== TIMEOUT_SENTINEL && outcome !== CANCEL_SENTINEL) return;
2177
+ if (outcome === CANCEL_SENTINEL) {
2178
+ logger.warn(
2179
+ `run ${run.id} cancelada \u2014 handler n\xE3o respondeu a hobots.signal em ${cancelGraceMs}ms, liberando o slot (o handler pode continuar rodando em segundo plano)`
2180
+ );
2181
+ return;
2182
+ }
2183
+ reporter.abort();
2184
+ invocation.then(() => logger.warn(`run ${run.id} terminou depois do timeout de ${timeoutMs}ms`)).catch(() => logger.warn(`run ${run.id} falhou depois do timeout de ${timeoutMs}ms (erro descartado)`));
2185
+ throw taskTimeoutError(timeoutMs);
2186
+ }
2187
+ function waitForCancelGrace(signal, graceMs) {
2188
+ return new Promise((resolve2) => {
2189
+ if (signal.aborted) {
2190
+ setTimeout(resolve2, graceMs);
2191
+ return;
2192
+ }
2193
+ signal.addEventListener("abort", () => setTimeout(resolve2, graceMs), { once: true });
2194
+ });
2195
+ }
2196
+ var MAX_ERROR_MESSAGE_CHARS = 1e4;
2197
+ var MAX_ERROR_STACK_CHARS = 2e4;
2198
+ function toRunError(error) {
2199
+ if (error instanceof Error) {
2200
+ return {
2201
+ type: error.name,
2202
+ message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
2203
+ stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
2204
+ };
2205
+ }
2206
+ return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
2207
+ }
2208
+
2209
+ // src/tasks/agent.ts
2210
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
2211
+ var MIN_POLL_INTERVAL_MS = 1e3;
2212
+ var MAX_CLAIM_PER_POLL = 5;
2213
+ var TaskAgent = class {
2214
+ constructor(options) {
2215
+ this.options = options;
2216
+ this.logger = options.logger;
2217
+ this.api = options.api;
2218
+ this.client = options.client;
2219
+ this.registry = options.registry;
2220
+ this.tracker = options.tracker;
2221
+ this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
2222
+ this.backoff = new Backoff(this.logger, "poll", this.pollIntervalMs);
2223
+ }
2224
+ logger;
2225
+ pollIntervalMs;
2226
+ api;
2227
+ client;
2228
+ registry;
2229
+ tracker;
2230
+ backoff;
2231
+ state = "idle";
2232
+ timer;
2233
+ pollAbort;
2234
+ activeRuns = /* @__PURE__ */ new Set();
2235
+ get activeCount() {
2236
+ return this.activeRuns.size;
2237
+ }
2238
+ start() {
2239
+ if (this.state === "started") return;
2240
+ if (this.state === "stopping") {
2241
+ throw new Error("[hobots] start do agente durante o stop \u2014 aguarde o stop terminar");
2242
+ }
2243
+ this.state = "started";
2244
+ this.logger.debug(`polling iniciado (tasks: ${this.registry.remoteTasks().map((task) => task.slug).join(", ")})`);
2245
+ this.schedule(0);
2246
+ }
2247
+ /** Uma task registrada depois do boot entra no próximo poll — que é antecipado para já. */
2248
+ wake() {
2249
+ if (this.state !== "started") return;
2250
+ if (this.timer) {
2251
+ clearTimeout(this.timer);
2252
+ this.timer = void 0;
2253
+ }
2254
+ this.schedule(0);
2255
+ }
2256
+ /** Para o polling e aguarda as runs ativas (até timeoutMs). Retorna true se tudo terminou a tempo. */
2257
+ async stop(timeoutMs = 3e4) {
2258
+ if (this.state === "idle") return true;
2259
+ this.state = "stopping";
2260
+ if (this.timer) {
2261
+ clearTimeout(this.timer);
2262
+ this.timer = void 0;
2263
+ }
2264
+ this.pollAbort?.abort();
2265
+ const all = Promise.allSettled([...this.activeRuns]).then(() => true);
2266
+ const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : this.activeRuns.size === 0;
2267
+ this.state = "idle";
2268
+ return finished;
2269
+ }
2270
+ /** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
2113
2271
  schedule(delayMs) {
2114
2272
  if (this.state !== "started" || this.timer) return;
2115
2273
  this.timer = setTimeout(() => {
@@ -2117,9 +2275,22 @@ var TaskAgent = class {
2117
2275
  void this.tick();
2118
2276
  }, delayMs);
2119
2277
  }
2278
+ /** Slots livres por task remota — a soma vira o `max` do poll e as tasks com slot viram o filtro. */
2279
+ freeSlots() {
2280
+ const tasks = [];
2281
+ let max = 0;
2282
+ for (const task of this.registry.remoteTasks()) {
2283
+ const free = task.options.concurrency - this.tracker.activeFor(task.slug);
2284
+ if (free <= 0) continue;
2285
+ tasks.push(task.slug);
2286
+ max += free;
2287
+ }
2288
+ return { tasks, max: Math.min(MAX_CLAIM_PER_POLL, max) };
2289
+ }
2120
2290
  async tick() {
2121
2291
  if (this.state !== "started") return;
2122
- if (this.activeRuns.size >= this.concurrency) {
2292
+ const { tasks, max } = this.freeSlots();
2293
+ if (max === 0) {
2123
2294
  this.schedule(this.pollIntervalMs);
2124
2295
  return;
2125
2296
  }
@@ -2127,224 +2298,498 @@ var TaskAgent = class {
2127
2298
  try {
2128
2299
  const response = await this.api.poll(
2129
2300
  {
2130
- instance: {
2131
- // identidade canônica do agente no painel — o mesmo instanceId do
2132
- // heartbeat da Telemetria, então os dois canais convergem em um agente
2133
- instance_id: this.instanceId,
2134
- version: SDK_VERSION,
2135
- host: hostname(),
2136
- // ambiente da instância: em agentes poll-only (sem heartbeat) este é o
2137
- // único canal por onde o servidor descobre onde o robô está rodando
2138
- environment: this.options.environment,
2139
- tasks: [...this.registry.keys()],
2140
- // o servidor calcula a tolerância de offline (3×) a partir disto em
2141
- // agentes poll-only — sem o campo, um poll lento flaparia offline/online.
2142
- // Clampado à faixa aceita pelo schema do poll (1s..15min).
2143
- poll_interval_ms: Math.min(Math.max(this.pollIntervalMs, 1e3), 9e5),
2144
- // recursos da máquina — alimenta a série da instância em agentes sem heartbeat
2145
- // (o servidor grava no máx. 1 ponto/30s por instância)
2146
- machine: this.options.machineSampler?.sample()
2147
- },
2148
- max: Math.min(MAX_CONCURRENCY, this.concurrency - this.activeRuns.size),
2149
- tasks: [...this.registry.keys()]
2301
+ // identidade canônica do agente no painel — o mesmo instanceId do heartbeat,
2302
+ // então os dois canais convergem em um agente. `instance.tasks` lista TODAS as
2303
+ // tasks (register + start); o filtro `tasks` abaixo só as que este poll pode rodar.
2304
+ instance: this.options.instanceInfo(),
2305
+ max,
2306
+ tasks
2150
2307
  },
2151
2308
  this.pollAbort.signal
2152
2309
  );
2153
- this.consecutiveFailures = 0;
2154
- this.consecutiveAuthFailures = 0;
2310
+ this.backoff.reset();
2155
2311
  for (const run of response.runs) {
2156
- if (this.activeRuns.size >= this.concurrency) break;
2157
2312
  this.dispatch(run);
2158
2313
  }
2159
2314
  this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
2160
2315
  } catch (error) {
2161
2316
  if (this.state !== "started") return;
2162
- this.schedule(this.delayAfterPollError(error));
2317
+ this.schedule(this.backoff.next(error));
2163
2318
  } finally {
2164
2319
  this.pollAbort = void 0;
2165
2320
  }
2166
2321
  }
2167
- delayAfterPollError(error) {
2168
- if (error instanceof TasksApiError && error.status === 401) {
2169
- this.consecutiveAuthFailures++;
2170
- if (this.consecutiveAuthFailures === 1 || this.consecutiveAuthFailures % 10 === 0) {
2171
- this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes");
2172
- }
2173
- return Math.max(this.pollIntervalMs, AUTH_FAILURE_INTERVAL_MS);
2174
- }
2175
- this.consecutiveFailures++;
2176
- const ladder = POLL_RETRY_DELAYS_MS[Math.min(this.consecutiveFailures - 1, POLL_RETRY_DELAYS_MS.length - 1)];
2177
- const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.pollIntervalMs, 16e3));
2178
- this.logger.warn(`poll falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
2179
- return delay;
2180
- }
2181
2322
  dispatch(run) {
2182
- const promise = this.executeRun(run).catch((error) => this.logger.error(`erro inesperado na run ${run.id}:`, error)).finally(() => {
2323
+ const promise = this.executeClaimed(run).catch((error) => this.logger.error(`erro inesperado na run ${run.id}:`, error)).finally(() => {
2183
2324
  this.activeRuns.delete(promise);
2184
2325
  if (this.state === "started") this.schedule(0);
2185
2326
  });
2186
2327
  this.activeRuns.add(promise);
2187
2328
  }
2188
- async executeRun(run) {
2189
- const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", true, run.id);
2190
- const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx, this.logFlushIntervalMs, this.cancelCheckIntervalMs);
2191
- this.reporters.add(reporter);
2329
+ async executeClaimed(run) {
2330
+ const task = this.registry.get(run.task);
2331
+ if (!task || task.mode !== "remote") {
2332
+ const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", run.id);
2333
+ const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx);
2334
+ reporter.reportFailure(toRunError(Object.assign(new Error(`tarefa n\xE3o registrada no agente: ${run.task}`), { name: "HandlerNotFound" })));
2335
+ await reporter.finalize();
2336
+ tx.setStatus("error");
2337
+ tx.finish();
2338
+ return;
2339
+ }
2340
+ await executeRun({ run, task, api: this.api, client: this.client, logger: this.logger, tracker: this.tracker });
2341
+ }
2342
+ };
2343
+
2344
+ // src/tasks/item-columns.ts
2345
+ var KEY_RE = /^[a-zA-Z0-9_.-]{1,100}$/;
2346
+ var MAX_COLUMNS = 12;
2347
+ var MAX_LABEL_CHARS = 60;
2348
+ function normalizeItemColumns(slug, input, logger) {
2349
+ if (input === void 0 || input === null) return void 0;
2350
+ const columns = input.columns;
2351
+ if (typeof input !== "object" || !Array.isArray(columns)) {
2352
+ logger.warn(`task "${slug}": items.columns precisa ser uma lista \u2014 declara\xE7\xE3o de colunas ignorada`);
2353
+ return void 0;
2354
+ }
2355
+ const result = [];
2356
+ const seen = /* @__PURE__ */ new Set();
2357
+ for (const entry of columns) {
2358
+ const rawKey = typeof entry === "string" ? entry : entry?.key;
2359
+ const rawLabel = typeof entry === "object" && entry !== null ? entry.label : void 0;
2360
+ const key = typeof rawKey === "string" ? rawKey.trim() : "";
2361
+ if (!KEY_RE.test(key)) {
2362
+ logger.warn(`task "${slug}": coluna de item inv\xE1lida ignorada (${JSON.stringify(rawKey)}) \u2014 use letras, n\xFAmeros, ponto, h\xEDfen e underline`);
2363
+ continue;
2364
+ }
2365
+ if (seen.has(key)) {
2366
+ logger.warn(`task "${slug}": coluna de item "${key}" repetida ignorada`);
2367
+ continue;
2368
+ }
2369
+ if (result.length >= MAX_COLUMNS) {
2370
+ logger.warn(`task "${slug}": no m\xE1ximo ${MAX_COLUMNS} colunas de item \u2014 "${key}" em diante ignoradas`);
2371
+ break;
2372
+ }
2373
+ seen.add(key);
2374
+ const label = typeof rawLabel === "string" ? rawLabel.trim().slice(0, MAX_LABEL_CHARS) : "";
2375
+ result.push(label ? { key, label } : { key });
2376
+ }
2377
+ return result.length > 0 ? result : void 0;
2378
+ }
2379
+
2380
+ // src/tasks/local.ts
2381
+ var LocalRunner = class {
2382
+ constructor(task, deps) {
2383
+ this.task = task;
2384
+ this.deps = deps;
2385
+ this.backoff = new Backoff(deps.logger, `cria\xE7\xE3o da run "${task.slug}"`);
2386
+ this.done = new Promise((resolve2) => {
2387
+ this.resolveDone = resolve2;
2388
+ });
2389
+ }
2390
+ abort = new AbortController();
2391
+ backoff;
2392
+ /** Resolve quando a run terminou (ou o runner foi parado antes de abri-la). */
2393
+ done;
2394
+ resolveDone;
2395
+ async run() {
2192
2396
  try {
2193
- const handler = this.registry.get(run.task);
2194
- if (!handler) {
2195
- reporter.reportFailure({
2196
- type: "HandlerNotFound",
2197
- message: `tarefa n\xE3o registrada no agente: ${run.task}`
2198
- });
2199
- await reporter.finalize();
2200
- tx.setStatus("error");
2201
- tx.finish();
2202
- return;
2203
- }
2204
- const ok = await reporter.reportRunning();
2205
- if (!ok) {
2206
- this.logger.debug(`run ${run.id} cancelada antes de iniciar \u2014 handler n\xE3o executado`);
2207
- tx.setStatus("cancelled");
2208
- tx.finish();
2209
- return;
2210
- }
2211
- this.logger.debug(`executando run ${run.id} (${run.task})`);
2397
+ const run = await this.createRun();
2398
+ if (!run) return;
2399
+ this.deps.logger.debug(`run ${run.id} aberta para "${this.task.slug}" (modo start)`);
2400
+ await executeRun({
2401
+ run,
2402
+ task: this.task,
2403
+ api: this.deps.api,
2404
+ client: this.deps.client,
2405
+ logger: this.deps.logger,
2406
+ tracker: this.deps.tracker
2407
+ });
2408
+ } catch (error) {
2409
+ this.deps.logger.error(`erro inesperado na task "${this.task.slug}":`, error);
2410
+ } finally {
2411
+ this.resolveDone();
2412
+ }
2413
+ }
2414
+ /** Desiste de abrir a run (close() antes da plataforma responder). Uma run já aberta segue pelo tracker. */
2415
+ stop() {
2416
+ this.abort.abort();
2417
+ }
2418
+ async createRun() {
2419
+ for (; ; ) {
2420
+ if (this.abort.signal.aborted) return void 0;
2212
2421
  try {
2213
- await runWithRunContext(
2214
- { runId: run.id, tx, attach: (image, options) => reporter.attach(image, options) },
2215
- () => this.invokeWithTimeout(handler, run, reporter)
2216
- );
2217
- await reporter.finalizeAttachments();
2218
- await reporter.finalizeItems();
2219
- tx.setStatus("ok");
2220
- tx.finish();
2221
- reporter.reportSuccess();
2222
- this.logger.debug(`run ${run.id} conclu\xEDda`);
2422
+ return await this.deps.api.createRun({ task: this.task.slug, instance: this.deps.instanceInfo() }, this.abort.signal);
2223
2423
  } catch (error) {
2224
- await reporter.finalizeAttachments();
2225
- await reporter.finalizeItems();
2226
- tx.setStatus("error");
2227
- tx.finish();
2228
- reporter.reportFailure(toRunError(error));
2229
- this.logger.debug(`run ${run.id} falhou: ${String(error)}`);
2424
+ if (this.abort.signal.aborted) return void 0;
2425
+ await abortableSleep(this.backoff.next(error), this.abort.signal);
2230
2426
  }
2231
- await reporter.finalize();
2232
- } finally {
2233
- this.reporters.delete(reporter);
2234
2427
  }
2235
2428
  }
2236
- async invokeWithTimeout(handler, run, reporter) {
2237
- const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
2238
- const races = [];
2239
- if (this.taskTimeoutMs && this.taskTimeoutMs > 0) {
2240
- races.push(sleep(this.taskTimeoutMs).then(() => TIMEOUT_SENTINEL));
2429
+ };
2430
+
2431
+ // src/tasks/registry.ts
2432
+ var TaskRegistry = class {
2433
+ tasks = /* @__PURE__ */ new Map();
2434
+ add(task) {
2435
+ if (this.tasks.has(task.slug)) {
2436
+ throw new Error(`[hobots] task "${task.slug}" j\xE1 registrada \u2014 cada slug s\xF3 pode ter um handler (register ou start)`);
2241
2437
  }
2242
- if (this.cancelGraceMs > 0) {
2243
- races.push(waitForCancelGrace(reporter.context.signal, this.cancelGraceMs).then(() => CANCEL_SENTINEL));
2438
+ this.tasks.set(task.slug, task);
2439
+ }
2440
+ get(slug) {
2441
+ return this.tasks.get(slug);
2442
+ }
2443
+ get size() {
2444
+ return this.tasks.size;
2445
+ }
2446
+ /** Tasks que chegam pelo poll (register). */
2447
+ remoteTasks() {
2448
+ return [...this.tasks.values()].filter((task) => task.mode === "remote");
2449
+ }
2450
+ /** Tasks abertas pelo próprio bot (start). */
2451
+ localTasks() {
2452
+ return [...this.tasks.values()].filter((task) => task.mode === "local");
2453
+ }
2454
+ /** Todos os slugs, para a presença da instância (`instance.tasks`). */
2455
+ allSlugs() {
2456
+ return [...this.tasks.keys()];
2457
+ }
2458
+ /** Colunas de itens declaradas no código, por slug (`instance.task_items`); undefined se nenhuma task declarou. */
2459
+ itemDeclarations() {
2460
+ let result;
2461
+ for (const task of this.tasks.values()) {
2462
+ if (!task.options.itemColumns) continue;
2463
+ (result ??= {})[task.slug] = { attributes: task.options.itemColumns };
2244
2464
  }
2245
- if (races.length === 0) {
2246
- await invocation;
2247
- return;
2465
+ return result;
2466
+ }
2467
+ clear() {
2468
+ this.tasks.clear();
2469
+ }
2470
+ };
2471
+ var RunTracker = class {
2472
+ constructor(api) {
2473
+ this.api = api;
2474
+ }
2475
+ reporters = /* @__PURE__ */ new Set();
2476
+ waiters = /* @__PURE__ */ new Set();
2477
+ add(reporter) {
2478
+ this.reporters.add(reporter);
2479
+ }
2480
+ remove(reporter) {
2481
+ this.reporters.delete(reporter);
2482
+ if (this.reporters.size === 0) {
2483
+ for (const resolve2 of this.waiters) resolve2();
2484
+ this.waiters.clear();
2248
2485
  }
2249
- const outcome = await Promise.race([invocation, ...races]);
2250
- if (outcome !== TIMEOUT_SENTINEL && outcome !== CANCEL_SENTINEL) return;
2251
- if (outcome === CANCEL_SENTINEL) {
2252
- this.logger.warn(
2253
- `run ${run.id} cancelada \u2014 handler n\xE3o respondeu a ctx.signal em ${this.cancelGraceMs}ms, liberando o slot (o handler pode continuar rodando em segundo plano)`
2486
+ }
2487
+ get activeCount() {
2488
+ return this.reporters.size;
2489
+ }
2490
+ /** Runs ativas de uma task (concorrência por task no poll). */
2491
+ activeFor(slug) {
2492
+ let count = 0;
2493
+ for (const reporter of this.reporters) if (reporter.context.task === slug) count++;
2494
+ return count;
2495
+ }
2496
+ /** A única run ativa, quando há exatamente uma — o que `hobots.*` usa fora de um contexto async. */
2497
+ singleActive() {
2498
+ if (this.reporters.size !== 1) return void 0;
2499
+ return this.reporters.values().next().value;
2500
+ }
2501
+ /** Resolve quando não houver mais runs ativas (ou após timeoutMs, devolvendo false). */
2502
+ async waitIdle(timeoutMs) {
2503
+ if (this.reporters.size === 0) return true;
2504
+ const idle = new Promise((resolve2) => this.waiters.add(() => resolve2(true)));
2505
+ if (timeoutMs <= 0) return false;
2506
+ return Promise.race([idle, sleep(timeoutMs).then(() => false)]);
2507
+ }
2508
+ /**
2509
+ * Best-effort do exit path: reporta 'failed'/AgentTerminated para as runs ativas
2510
+ * antes do processo morrer, fora da fila do reporter e com timeout curto. Runs já
2511
+ * canceladas no app (reporter morto) são puladas; erros de rede são engolidos —
2512
+ * o sweep server-side é o backstop.
2513
+ */
2514
+ async reportAgentTerminated(message, timeoutMs = 2500) {
2515
+ const pending = [];
2516
+ for (const reporter of this.reporters) {
2517
+ if (reporter.isDead) continue;
2518
+ const logs = reporter.drainLogs();
2519
+ reporter.kill();
2520
+ pending.push(
2521
+ this.api.updateRunOnce(
2522
+ reporter.context.runId,
2523
+ { status: "failed", error: { type: "AgentTerminated", message }, logs: logs.length > 0 ? logs : void 0 },
2524
+ timeoutMs
2525
+ ).catch(() => {
2526
+ })
2254
2527
  );
2255
- return;
2256
2528
  }
2257
- const timeoutMs = this.taskTimeoutMs;
2258
- reporter.abort();
2259
- invocation.then(() => this.logger.warn(`run ${run.id} terminou depois do timeout de ${timeoutMs}ms`)).catch(() => this.logger.warn(`run ${run.id} falhou depois do timeout de ${timeoutMs}ms (erro descartado)`));
2260
- throw Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
2261
- name: "TaskTimeout"
2262
- });
2529
+ if (pending.length === 0) return;
2530
+ await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
2263
2531
  }
2264
2532
  };
2265
- var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
2266
- var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
2267
- function waitForCancelGrace(signal, graceMs) {
2268
- return new Promise((resolve) => {
2269
- if (signal.aborted) {
2270
- setTimeout(resolve, graceMs);
2271
- return;
2533
+
2534
+ // src/tasks/shutdown.ts
2535
+ var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
2536
+ function installShutdownHandlers(agent) {
2537
+ if (!isNode()) return () => {
2538
+ };
2539
+ let signaled = false;
2540
+ const signalHandlers = /* @__PURE__ */ new Map();
2541
+ for (const signal of WATCHED_SIGNALS) {
2542
+ const handler = markInternal(() => {
2543
+ if (externalListenerCount(signal) > 0) return;
2544
+ if (signaled) return;
2545
+ signaled = true;
2546
+ const reraise = () => {
2547
+ process.removeListener(signal, handler);
2548
+ process.kill(process.pid, signal);
2549
+ };
2550
+ if (agent.activeCount === 0) {
2551
+ reraise();
2552
+ return;
2553
+ }
2554
+ const report = agent.reportAgentTerminated(`o processo do agente recebeu ${signal} durante a execu\xE7\xE3o`);
2555
+ addEmergencyTask(report);
2556
+ void report.finally(reraise);
2557
+ });
2558
+ process.on(signal, handler);
2559
+ signalHandlers.set(signal, handler);
2560
+ }
2561
+ const onFatal = markInternal((error) => {
2562
+ const report = agent.activeCount > 0 ? agent.reportAgentTerminated("o processo do agente morreu com um erro n\xE3o tratado durante a execu\xE7\xE3o") : Promise.resolve();
2563
+ addEmergencyTask(report);
2564
+ if (!hasInstalledGlobalHandlers() && externalListenerCount("uncaughtException") === 0) {
2565
+ void report.finally(() => {
2566
+ console.error(error);
2567
+ process.exit(1);
2568
+ });
2272
2569
  }
2273
- signal.addEventListener("abort", () => setTimeout(resolve, graceMs), { once: true });
2274
2570
  });
2571
+ process.on("uncaughtException", onFatal);
2572
+ return () => {
2573
+ for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
2574
+ process.removeListener("uncaughtException", onFatal);
2575
+ };
2275
2576
  }
2276
- var MAX_ERROR_MESSAGE_CHARS = 1e4;
2277
- var MAX_ERROR_STACK_CHARS = 2e4;
2278
- function toRunError(error) {
2279
- if (error instanceof Error) {
2280
- return {
2281
- type: error.name,
2282
- message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
2283
- stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
2284
- };
2577
+
2578
+ // src/runtime.ts
2579
+ var HEARTBEAT_INTERVAL_MS = 3e4;
2580
+ var DEFAULT_RETRIES2 = 1;
2581
+ var DEFAULT_RETRY_DELAY_MS2 = 5e3;
2582
+ var DEFAULT_CANCEL_GRACE_MS = 1e4;
2583
+ var MAX_CONCURRENCY = 5;
2584
+ var MAX_SLUG_CHARS = 255;
2585
+ var PROCESS_LEVEL_KEYS = [
2586
+ "clientSecret",
2587
+ "instanceId",
2588
+ "environment",
2589
+ "release",
2590
+ "tags",
2591
+ "debug",
2592
+ "machineMetrics",
2593
+ "captureUnhandled",
2594
+ "installExitHandlers"
2595
+ ];
2596
+ var runtime;
2597
+ var warnedNoRuntime = false;
2598
+ function hostname() {
2599
+ if (!isNode()) return void 0;
2600
+ return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
2601
+ }
2602
+ function sameValue(a, b) {
2603
+ if (a === b) return true;
2604
+ if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
2605
+ return false;
2606
+ }
2607
+ function ensureRuntime(options) {
2608
+ if (runtime) {
2609
+ for (const key of PROCESS_LEVEL_KEYS) {
2610
+ if (options[key] !== void 0 && !sameValue(options[key], runtime.options[key])) {
2611
+ runtime.logger.warn(
2612
+ `op\xE7\xE3o "${key}" divergente ignorada \u2014 o primeiro register()/start() define as op\xE7\xF5es do processo`
2613
+ );
2614
+ }
2615
+ }
2616
+ return runtime;
2617
+ }
2618
+ const config = resolveConfig({ clientSecret: options.clientSecret });
2619
+ const logger = makeLogger(options.debug ?? false);
2620
+ const environment = resolveEnvironmentOption(options.environment);
2621
+ const client = new Client(
2622
+ { environment, release: options.release, tags: options.tags, debug: options.debug },
2623
+ config
2624
+ );
2625
+ const api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger });
2626
+ runtime = {
2627
+ options: { ...options, clientSecret: void 0 },
2628
+ config,
2629
+ client,
2630
+ logger,
2631
+ api,
2632
+ registry: new TaskRegistry(),
2633
+ tracker: new RunTracker(api),
2634
+ sampler: void 0,
2635
+ heartbeat: void 0,
2636
+ agent: void 0,
2637
+ locals: /* @__PURE__ */ new Map(),
2638
+ instanceId: resolveInstanceId(options.instanceId),
2639
+ environment,
2640
+ pollIntervalMs: void 0,
2641
+ booted: false,
2642
+ bootScheduled: false,
2643
+ bootPromise: void 0,
2644
+ uninstallGlobalHandlers: options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0,
2645
+ uninstallShutdown: void 0
2646
+ };
2647
+ runtime.options.clientSecret = options.clientSecret;
2648
+ logger.debug("inicializado", { endpoint: config.ingestEndpoint });
2649
+ return runtime;
2650
+ }
2651
+ function validateTask(slug, handler, fn) {
2652
+ if (typeof slug !== "string" || slug.trim().length === 0 || slug.length > MAX_SLUG_CHARS) {
2653
+ throw new Error(`[hobots] ${fn}(): o slug da task precisa ser uma string de 1 a ${MAX_SLUG_CHARS} caracteres`);
2285
2654
  }
2286
- return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
2655
+ if (typeof handler !== "function") {
2656
+ throw new Error(`[hobots] ${fn}('${slug}'): o handler precisa ser uma fun\xE7\xE3o`);
2657
+ }
2658
+ return slug.trim();
2287
2659
  }
2288
-
2289
- // src/core.ts
2290
- var DEFAULT_MAX_BREADCRUMBS = 100;
2291
- var client;
2292
- var scope = new Scope();
2293
- var heartbeat;
2294
- var machineSampler;
2295
- var agent;
2296
- var uninstallGlobalHandlers;
2297
- var registry = /* @__PURE__ */ new Map();
2298
- var warnedTasksDisabled = false;
2299
- function init(options) {
2300
- if (client) {
2301
- client.logger.warn("init() chamado mais de uma vez \u2014 substituindo o client anterior");
2302
- heartbeat?.stop();
2303
- heartbeat = void 0;
2304
- }
2305
- if (agent) {
2306
- void agent.stop(0);
2307
- agent = void 0;
2308
- }
2309
- uninstallGlobalHandlers?.();
2310
- uninstallGlobalHandlers = void 0;
2311
- warnedTasksDisabled = false;
2312
- const config = resolveConfig(options);
2313
- client = new Client(options, config);
2314
- machineSampler = options.machineMetrics === false ? void 0 : new MachineSampler(options.instanceId, client.logger);
2315
- void machineSampler?.start();
2316
- client.attachMachineSampler(machineSampler);
2317
- if (options.captureUnhandled !== false) {
2318
- uninstallGlobalHandlers = installGlobalHandlers(client, () => scope);
2319
- }
2320
- if (options.heartbeat) {
2321
- const heartbeatOptions = options.heartbeat === true ? {} : options.heartbeat;
2322
- heartbeat = new Heartbeat(client, heartbeatOptions, options.instanceId, machineSampler);
2323
- heartbeat.start();
2324
- }
2325
- if (options.tasks) {
2326
- const tasksOptions = options.tasks === true ? {} : options.tasks;
2327
- agent = new TaskAgent(
2328
- { ...tasksOptions, config, instanceId: options.instanceId, environment: options.environment, debug: options.debug, client, machineSampler },
2329
- registry
2330
- );
2660
+ function clampConcurrency(value) {
2661
+ if (value === void 0 || !Number.isFinite(value)) return 1;
2662
+ return Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(value)));
2663
+ }
2664
+ function resolveTaskOptions(slug, options, mode, logger) {
2665
+ const retries = options.retries === void 0 || !Number.isFinite(options.retries) ? DEFAULT_RETRIES2 : Math.max(0, Math.floor(options.retries));
2666
+ return {
2667
+ retries,
2668
+ retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS2,
2669
+ timeoutMs: options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : void 0,
2670
+ concurrency: mode === "remote" ? clampConcurrency(options.concurrency) : 1,
2671
+ logFlushIntervalMs: options.logFlushIntervalMs,
2672
+ cancelCheckIntervalMs: options.cancelCheckIntervalMs,
2673
+ cancelGraceMs: options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS,
2674
+ itemColumns: normalizeItemColumns(slug, options.items, logger)
2675
+ };
2676
+ }
2677
+ function instanceInfo(rt) {
2678
+ return {
2679
+ instance_id: rt.instanceId ?? "unknown",
2680
+ version: SDK_VERSION,
2681
+ host: hostname(),
2682
+ // ambiente da instância: junto do heartbeat, é como o servidor sabe onde o robô roda
2683
+ environment: rt.environment,
2684
+ // TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
2685
+ tasks: rt.registry.allSlugs(),
2686
+ // colunas da aba "itens" declaradas no código — a plataforma usa enquanto o usuário não personalizar
2687
+ task_items: rt.registry.itemDeclarations(),
2688
+ // o servidor calcula a tolerância de offline (3×) a partir disto em agentes que fazem
2689
+ // poll; clampado à faixa aceita pelo schema (1s..15min)
2690
+ poll_interval_ms: rt.agent ? Math.min(Math.max(rt.agent.pollIntervalMs, 1e3), 9e5) : void 0,
2691
+ // recursos da máquina — alimenta a série da instância (o servidor grava no máx. 1 ponto/30s)
2692
+ machine: rt.sampler?.sample()
2693
+ };
2694
+ }
2695
+ function ensureAgent(rt) {
2696
+ rt.agent ??= new TaskAgent({
2697
+ api: rt.api,
2698
+ client: rt.client,
2699
+ logger: rt.logger,
2700
+ registry: rt.registry,
2701
+ tracker: rt.tracker,
2702
+ instanceInfo: () => instanceInfo(rt),
2703
+ pollIntervalMs: rt.pollIntervalMs
2704
+ });
2705
+ return rt.agent;
2706
+ }
2707
+ function launchLocal(rt, task) {
2708
+ const runner = new LocalRunner(task, {
2709
+ api: rt.api,
2710
+ client: rt.client,
2711
+ logger: rt.logger,
2712
+ tracker: rt.tracker,
2713
+ instanceInfo: () => instanceInfo(rt)
2714
+ });
2715
+ rt.locals.set(task.slug, runner);
2716
+ void runner.run();
2717
+ }
2718
+ async function boot(rt) {
2719
+ if (!rt.instanceId) {
2720
+ const id = await fallbackInstanceId();
2721
+ if (runtime !== rt) return;
2722
+ rt.instanceId = id;
2723
+ rt.logger.debug(`instanceId n\xE3o informado (env ${INSTANCE_ID_ENV}) \u2014 usando o hostname "${id}"`);
2724
+ }
2725
+ if (runtime !== rt) return;
2726
+ rt.booted = true;
2727
+ if (rt.options.machineMetrics !== false) {
2728
+ rt.sampler = new MachineSampler(rt.instanceId, rt.logger);
2729
+ void rt.sampler.start();
2730
+ rt.client.attachMachineSampler(rt.sampler);
2731
+ }
2732
+ rt.heartbeat = new Heartbeat(rt.client, { intervalMs: HEARTBEAT_INTERVAL_MS, keepAlive: true }, rt.instanceId, rt.sampler);
2733
+ rt.heartbeat.start();
2734
+ if (rt.options.installExitHandlers !== false) {
2735
+ rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
2736
+ }
2737
+ if (rt.registry.remoteTasks().length > 0) ensureAgent(rt).start();
2738
+ for (const task of rt.registry.localTasks()) launchLocal(rt, task);
2739
+ }
2740
+ function scheduleBoot(rt) {
2741
+ if (rt.bootScheduled) return;
2742
+ rt.bootScheduled = true;
2743
+ queueMicrotask(() => {
2744
+ rt.bootPromise = boot(rt).catch((error) => rt.logger.error("falha ao iniciar o SDK:", error));
2745
+ });
2746
+ }
2747
+ function addTask(fn, mode, task, handler, options) {
2748
+ const slug = validateTask(task, handler, fn);
2749
+ const rt = ensureRuntime(options);
2750
+ if (mode === "remote" && options.pollIntervalMs !== void 0) {
2751
+ if (rt.pollIntervalMs === void 0 && !rt.agent) rt.pollIntervalMs = options.pollIntervalMs;
2752
+ else if (rt.pollIntervalMs !== options.pollIntervalMs) {
2753
+ rt.logger.warn('op\xE7\xE3o "pollIntervalMs" divergente ignorada \u2014 o primeiro register() define o intervalo do poll');
2754
+ }
2755
+ }
2756
+ const registered = { slug, mode, handler, options: resolveTaskOptions(slug, options, mode, rt.logger) };
2757
+ rt.registry.add(registered);
2758
+ if (!rt.booted) {
2759
+ scheduleBoot(rt);
2760
+ return;
2331
2761
  }
2332
- client.logger.debug("inicializado", { endpoint: client.config.ingestEndpoint });
2762
+ if (mode === "remote") {
2763
+ const agent = ensureAgent(rt);
2764
+ agent.start();
2765
+ agent.wake();
2766
+ } else {
2767
+ launchLocal(rt, registered);
2768
+ }
2769
+ }
2770
+ function register(task, handler, options = {}) {
2771
+ addTask("register", "remote", task, handler, options);
2772
+ }
2773
+ function start(task, handler, options = {}) {
2774
+ addTask("start", "local", task, handler, options);
2775
+ }
2776
+ function currentReporter() {
2777
+ return currentRunReporter() ?? runtime?.tracker.singleActive();
2333
2778
  }
2334
- function getClient() {
2335
- return client;
2779
+ function activeLogger() {
2780
+ return runtime?.logger;
2336
2781
  }
2337
2782
  function toRunLogLevel(type) {
2338
2783
  return type === "fatal" ? "error" : type;
2339
2784
  }
2340
- function forwardAttachment(attachment, message, type) {
2341
- const attach = currentRunAttach();
2342
- if (!attach) {
2343
- client?.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task (register/start)");
2785
+ function forwardAttachment(rt, attachment, message, type) {
2786
+ const reporter = currentReporter();
2787
+ if (!reporter) {
2788
+ rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task");
2344
2789
  return;
2345
2790
  }
2346
2791
  const withMeta = typeof attachment === "object" && "image" in attachment ? attachment : { image: attachment };
2347
- void attach(withMeta.image, {
2792
+ void reporter.attach(withMeta.image, {
2348
2793
  message,
2349
2794
  level: toRunLogLevel(type),
2350
2795
  caption: withMeta.caption,
@@ -2352,84 +2797,115 @@ function forwardAttachment(attachment, message, type) {
2352
2797
  });
2353
2798
  }
2354
2799
  function captureException(error, hint) {
2355
- if (hint?.attachment) {
2356
- const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2357
- forwardAttachment(hint.attachment, message, hint.type ?? "error");
2800
+ const rt = runtime;
2801
+ if (!rt) {
2802
+ if (!warnedNoRuntime) {
2803
+ warnedNoRuntime = true;
2804
+ console.warn("[hobots] captureException antes de registrar uma task (register/start) \u2014 evento descartado");
2805
+ }
2806
+ return void 0;
2358
2807
  }
2359
- return client?.captureException(error, hint, scope);
2360
- }
2361
- function captureMessage(message, type = "warning", hint) {
2362
2808
  if (hint?.attachment) {
2363
- forwardAttachment(hint.attachment, message, hint.type ?? type);
2364
- }
2365
- return client?.captureMessage(message, type, hint, scope);
2366
- }
2367
- function addBreadcrumb(crumb) {
2368
- scope.addBreadcrumb(crumb, client?.options.maxBreadcrumbs ?? DEFAULT_MAX_BREADCRUMBS);
2369
- }
2370
- function setTag(key, value) {
2371
- scope.setTag(key, value);
2372
- }
2373
- function setTags(tags) {
2374
- scope.setTags(tags);
2375
- }
2376
- function setUser(user) {
2377
- scope.setUser(user);
2378
- }
2379
- function setContext(name, context) {
2380
- scope.setContext(name, context);
2381
- }
2382
- function setExtra(key, value) {
2383
- scope.setExtra(key, value);
2384
- }
2385
- function withScope(callback) {
2386
- const previous = scope;
2387
- scope = previous.clone();
2388
- try {
2389
- return callback(scope);
2390
- } finally {
2391
- scope = previous;
2392
- }
2393
- }
2394
- function startTransaction(name, op) {
2395
- const active = currentRunTransaction();
2396
- if (active) {
2397
- client?.logger.debug(`startTransaction('${name}') dentro de uma execu\xE7\xE3o \u2014 reutilizando a transaction da run`);
2398
- return active;
2399
- }
2400
- const rate = client?.options.tracesSampleRate ?? 1;
2401
- const sampled = rate >= 1 || Math.random() < rate;
2402
- return new Transaction(client, name, op, sampled);
2403
- }
2404
- function flush(timeoutMs) {
2405
- return client ? client.flush(timeoutMs) : Promise.resolve(true);
2406
- }
2407
- function register(task, handler) {
2408
- if (registry.has(task)) {
2409
- (agent?.logger ?? client?.logger)?.warn(`register('${task}') sobrescreveu um handler existente`);
2410
- }
2411
- if (client && !agent && !warnedTasksDisabled) {
2412
- warnedTasksDisabled = true;
2413
- client.logger.warn(
2414
- `register('${task}') com tasks desabilitadas \u2014 passe tasks: true (ou op\xE7\xF5es) no init()`
2415
- );
2416
- }
2417
- registry.set(task, handler);
2418
- }
2419
- function start() {
2420
- if (!agent) {
2421
- throw new Error("[hobots] tasks n\xE3o configuradas \u2014 passe tasks: true (ou op\xE7\xF5es) no init() antes de start()");
2422
- }
2423
- agent.start();
2424
- }
2425
- function stop(timeoutMs) {
2426
- return agent ? agent.stop(timeoutMs) : Promise.resolve(true);
2809
+ const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2810
+ forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
2811
+ }
2812
+ return rt.client.captureException(error, hint);
2813
+ }
2814
+ async function close(timeoutMs = 3e4) {
2815
+ const rt = runtime;
2816
+ if (!rt) return true;
2817
+ runtime = void 0;
2818
+ await rt.bootPromise;
2819
+ if (rt.agent) await rt.agent.stop(0);
2820
+ for (const local of rt.locals.values()) local.stop();
2821
+ const idle = await rt.tracker.waitIdle(timeoutMs);
2822
+ if (!idle) {
2823
+ rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
2824
+ await rt.tracker.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
2825
+ }
2826
+ rt.heartbeat?.stop();
2827
+ rt.uninstallShutdown?.();
2828
+ rt.uninstallGlobalHandlers?.();
2829
+ const flushed = await rt.client.close(timeoutMs);
2830
+ return idle && flushed;
2427
2831
  }
2428
- async function close(timeoutMs) {
2429
- const agentStopped = agent ? await agent.stop(timeoutMs) : true;
2430
- heartbeat?.stop();
2431
- heartbeat = void 0;
2432
- const clientClosed = client ? await client.close(timeoutMs) : true;
2433
- return agentStopped && clientClosed;
2832
+
2833
+ // src/hobots.ts
2834
+ var warned = /* @__PURE__ */ new Set();
2835
+ function warnOutsideRun(member) {
2836
+ if (warned.has(member)) return;
2837
+ warned.add(member);
2838
+ const message = `hobots.${member} usado fora de uma execu\xE7\xE3o de task \u2014 ignorado (registre a task com register()/start() e use dentro do handler)`;
2839
+ const logger = activeLogger();
2840
+ if (logger) logger.warn(message);
2841
+ else console.warn(`[hobots] ${message}`);
2842
+ }
2843
+ var FALLBACK_TASK = "hobots";
2844
+ var fallbackLog = {
2845
+ debug: (message, ...rest) => printRunLog(FALLBACK_TASK, "debug", joinLogArgs(message, rest)),
2846
+ info: (message, ...rest) => printRunLog(FALLBACK_TASK, "info", joinLogArgs(message, rest)),
2847
+ warn: (message, ...rest) => printRunLog(FALLBACK_TASK, "warning", joinLogArgs(message, rest)),
2848
+ error: (message, ...rest) => printRunLog(FALLBACK_TASK, "error", joinLogArgs(message, rest)),
2849
+ success: (message, ...rest) => printRunLog(FALLBACK_TASK, "success", joinLogArgs(message, rest))
2850
+ };
2851
+ var noop = () => void 0;
2852
+ var fallbackItems = { add: noop, succeeded: noop, failed: noop, occurrence: noop };
2853
+ var fallbackProgress = { total: noop, advance: noop, set: noop, current: 0, snapshot: null };
2854
+ var neverAborts = new AbortController().signal;
2855
+ var detachedTransaction;
2856
+ function resolve(member) {
2857
+ const reporter = currentReporter();
2858
+ if (reporter) return reporter.context;
2859
+ warnOutsideRun(member);
2860
+ return void 0;
2434
2861
  }
2862
+ function getter(member, fallback) {
2863
+ return { enumerable: true, get: () => resolve(member)?.[member] ?? fallback() };
2864
+ }
2865
+ function method(value) {
2866
+ return { enumerable: true, writable: false, value };
2867
+ }
2868
+ var hobots = Object.defineProperties({}, {
2869
+ // ── contexto da run atual (getters) ─────────────────────────────────────────
2870
+ runId: getter("runId", () => ""),
2871
+ task: getter("task", () => ""),
2872
+ params: getter("params", () => ({})),
2873
+ log: getter("log", () => fallbackLog),
2874
+ items: getter("items", () => fallbackItems),
2875
+ progress: getter("progress", () => fallbackProgress),
2876
+ transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
2877
+ signal: getter("signal", () => neverAborts),
2878
+ throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
2879
+ sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
2880
+ attach: method(
2881
+ (image, options) => resolve("attach")?.attach(image, options) ?? Promise.resolve({ id: "", ok: false, reason: "canceled" })
2882
+ ),
2883
+ retry: method((fn, options) => {
2884
+ const reporter = currentReporter();
2885
+ return reporter ? reporter.context.retry(fn, options) : retryWithBackoff(fn, options);
2886
+ }),
2887
+ // ── processo ────────────────────────────────────────────────────────────────
2888
+ register: method((task, handler, options) => {
2889
+ register(task, handler, options);
2890
+ installGlobalHobots();
2891
+ }),
2892
+ start: method((task, handler, options) => {
2893
+ start(task, handler, options);
2894
+ installGlobalHobots();
2895
+ }),
2896
+ close: method((timeoutMs) => close(timeoutMs)),
2897
+ captureException: method((error, hint) => captureException(error, hint))
2898
+ });
2899
+ function installGlobalHobots() {
2900
+ if (globalThis.hobots === hobots) return;
2901
+ Object.defineProperty(globalThis, "hobots", { value: hobots, configurable: true, writable: true, enumerable: false });
2902
+ }
2903
+ var register2 = hobots.register;
2904
+ var start2 = hobots.start;
2905
+ var close2 = hobots.close;
2906
+ var captureException2 = hobots.captureException;
2907
+ var retry = hobots.retry;
2908
+
2909
+ // src/index.ts
2910
+ var src_default = hobots;
2435
2911
  //# sourceMappingURL=index.cjs.map