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