hobots 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,54 +1,3 @@
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;
54
3
  var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
@@ -103,7 +52,7 @@ function scrubSecrets(text) {
103
52
 
104
53
  // src/utils.ts
105
54
  var SDK_NAME = "hobots";
106
- var SDK_VERSION = true ? "2.4.0" : "0.0.0-dev";
55
+ var SDK_VERSION = true ? "3.0.0" : "0.0.0-dev";
107
56
  function isNode() {
108
57
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
109
58
  }
@@ -131,11 +80,28 @@ function unrefTimer(timer) {
131
80
  timer?.unref?.();
132
81
  }
133
82
  function sleep(ms) {
134
- return new Promise((resolve) => {
135
- const timer = setTimeout(resolve, ms);
83
+ return new Promise((resolve2) => {
84
+ const timer = setTimeout(resolve2, ms);
136
85
  unrefTimer(timer);
137
86
  });
138
87
  }
88
+ function abortableSleep(ms, signal) {
89
+ return new Promise((resolve2) => {
90
+ if (signal?.aborted) {
91
+ resolve2();
92
+ return;
93
+ }
94
+ const onAbort = () => {
95
+ clearTimeout(timer);
96
+ resolve2();
97
+ };
98
+ const timer = setTimeout(() => {
99
+ signal?.removeEventListener("abort", onAbort);
100
+ resolve2();
101
+ }, ms);
102
+ signal?.addEventListener("abort", onAbort, { once: true });
103
+ });
104
+ }
139
105
  function jitter(baseMs) {
140
106
  return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
141
107
  }
@@ -256,14 +222,6 @@ function eventFromException(error, type = "error") {
256
222
  }
257
223
  };
258
224
  }
259
- function eventFromMessage(message, type = "warning") {
260
- return {
261
- event_id: uuid4(),
262
- timestamp: nowISO(),
263
- type,
264
- message: scrubSecrets(message)
265
- };
266
- }
267
225
 
268
226
  // src/transport.ts
269
227
  var REQUEST_TIMEOUT_MS = 1e4;
@@ -397,7 +355,7 @@ var Client = class {
397
355
  #config;
398
356
  transport;
399
357
  machine;
400
- constructor(options, config = resolveConfig(options)) {
358
+ constructor(options, config) {
401
359
  this.#options = options;
402
360
  this.debug = !!options.debug;
403
361
  this.logger = makeLogger(this.debug);
@@ -427,43 +385,28 @@ var Client = class {
427
385
  [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
428
386
  return `Client { baseUrl: '${this.#config.baseUrl}' }`;
429
387
  }
430
- captureException(error, hint, scope2) {
431
- return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint, scope2);
388
+ captureException(error, hint) {
389
+ return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
432
390
  }
433
- captureMessage(message, type, hint, scope2) {
434
- return this.captureEvent(eventFromMessage(message, type), hint, scope2);
435
- }
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
- }
391
+ captureEvent(event, hint) {
392
+ const final = this.prepareEvent(event, hint);
448
393
  this.transport.enqueue({ type: "event", payload: final });
449
394
  return final.event_id;
450
395
  }
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 };
396
+ prepareEvent(event, hint) {
397
+ const tags = { ...this.options.tags, ...event.tags, ...hint?.tags };
398
+ const extra = { ...event.extra, ...hint?.extra };
454
399
  const sampled = this.machine?.sample();
455
400
  const machine = sampled ? { machine: sampled } : {};
456
401
  return {
457
402
  ...event,
458
- type: hint?.type ?? event.type ?? scope2.type ?? "error",
403
+ type: hint?.type ?? event.type ?? "error",
459
404
  release: event.release ?? this.options.release,
460
405
  environment: this.resolveEnvironment(event.environment),
461
406
  tags: Object.keys(tags).length > 0 ? tags : void 0,
462
407
  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
408
+ contexts: { ...runtimeContexts(), ...machine, ...event.contexts },
409
+ fingerprint: hint?.fingerprint ?? event.fingerprint
467
410
  };
468
411
  }
469
412
  sendTransaction(payload) {
@@ -489,7 +432,7 @@ var Client = class {
489
432
  });
490
433
  }
491
434
  /**
492
- * Ambiente do item: o dele, senão o do init(), senão produção. Centralizado
435
+ * Ambiente do item: o dele, senão o das opções, senão produção. Centralizado
493
436
  * porque evento, transaction e heartbeat precisam responder igual — o servidor
494
437
  * normaliza apelidos legados, então aqui basta repassar.
495
438
  */
@@ -504,23 +447,103 @@ var Client = class {
504
447
  }
505
448
  };
506
449
 
450
+ // src/config.ts
451
+ var DEFAULT_BASE_URL = "https://api2.hobots.app";
452
+ var CLIENT_SECRET_ENV = "HOBOTS_CLIENT_SECRET";
453
+ function envClientSecret() {
454
+ if (typeof process === "undefined" || !process.env) return void 0;
455
+ return process.env[CLIENT_SECRET_ENV]?.trim() || void 0;
456
+ }
457
+ var SECRET_PATTERN = /^hb_[a-f0-9]{64}$/i;
458
+ function envBaseUrl() {
459
+ if (typeof process === "undefined" || !process.env) return void 0;
460
+ return process.env.HOBOTS_API_URL?.trim() || void 0;
461
+ }
462
+ function isLoopbackHost(hostname2) {
463
+ return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
464
+ }
465
+ var warnedInsecureBaseUrl = false;
466
+ function validateBaseUrl(raw, prefix) {
467
+ let url;
468
+ try {
469
+ url = new URL(raw);
470
+ } catch {
471
+ throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
472
+ }
473
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
474
+ throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
475
+ }
476
+ if (url.username || url.password) {
477
+ 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`);
478
+ }
479
+ if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
480
+ warnedInsecureBaseUrl = true;
481
+ console.warn(
482
+ `${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
483
+ );
484
+ }
485
+ return raw.replace(/\/+$/, "");
486
+ }
487
+ function resolveConfig(options = {}, prefix = "[hobots]") {
488
+ const fromOption = options.clientSecret?.trim();
489
+ const key = fromOption || envClientSecret();
490
+ const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
491
+ if (!key) {
492
+ throw new Error(
493
+ `${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`
494
+ );
495
+ }
496
+ if (!SECRET_PATTERN.test(key)) {
497
+ throw new Error(
498
+ `${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`
499
+ );
500
+ }
501
+ const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
502
+ return {
503
+ key,
504
+ baseUrl,
505
+ ingestEndpoint: `${baseUrl}/v1/ingest`,
506
+ tasksBaseUrl: `${baseUrl}/v1/tasks`
507
+ };
508
+ }
509
+
510
+ // src/env.ts
511
+ var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
512
+ function resolveInstanceId(option) {
513
+ const fromOption = option?.trim();
514
+ if (fromOption) return fromOption;
515
+ if (typeof process === "undefined" || !process.env) return void 0;
516
+ return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
517
+ }
518
+ async function fallbackInstanceId() {
519
+ if (!isNode()) return "browser";
520
+ try {
521
+ const os = await import("os");
522
+ return os.hostname() || "unknown-host";
523
+ } catch {
524
+ return "unknown-host";
525
+ }
526
+ }
527
+
507
528
  // src/heartbeat.ts
508
529
  var DEFAULT_INTERVAL_MS = 3e4;
509
530
  var MIN_REPORTED_INTERVAL_MS = 1e3;
510
531
  var MAX_REPORTED_INTERVAL_MS = 9e5;
511
532
  var Heartbeat = class {
512
- constructor(client2, options = {}, instanceId, sampler) {
513
- this.client = client2;
533
+ constructor(client, options = {}, instanceId, sampler) {
534
+ this.client = client;
514
535
  this.sampler = sampler;
515
536
  this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
516
537
  this.instanceId = instanceId;
517
538
  this.name = options.name ?? instanceId;
518
539
  this.metadata = options.metadata;
540
+ this.keepAlive = options.keepAlive ?? false;
519
541
  }
520
542
  intervalMs;
521
543
  instanceId;
522
544
  name;
523
545
  metadata;
546
+ keepAlive;
524
547
  timer;
525
548
  start() {
526
549
  if (this.timer) return;
@@ -536,7 +559,7 @@ var Heartbeat = class {
536
559
  },
537
560
  this.intervalMs * (0.9 + Math.random() * 0.2)
538
561
  );
539
- unrefTimer(this.timer);
562
+ if (!this.keepAlive) unrefTimer(this.timer);
540
563
  }
541
564
  stop() {
542
565
  if (this.timer) {
@@ -582,13 +605,13 @@ var installed = false;
582
605
  function hasInstalledGlobalHandlers() {
583
606
  return installed;
584
607
  }
585
- function installGlobalHandlers(client2, getScope) {
608
+ function installGlobalHandlers(client) {
586
609
  if (installed) return () => void 0;
587
610
  installed = true;
588
611
  if (isNode()) {
589
612
  const onUncaught = markInternal((error) => {
590
- client2.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } }, getScope());
591
- void client2.flush(2e3).then(async () => {
613
+ client.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } });
614
+ void client.flush(2e3).then(async () => {
592
615
  if (externalListenerCount("uncaughtException") === 0) {
593
616
  await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
594
617
  console.error(error);
@@ -597,11 +620,11 @@ function installGlobalHandlers(client2, getScope) {
597
620
  });
598
621
  });
599
622
  const onRejection = markInternal((reason) => {
600
- client2.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } }, getScope());
601
- void client2.flush(2e3);
623
+ client.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } });
624
+ void client.flush(2e3);
602
625
  });
603
626
  const onBeforeExit = markInternal(() => {
604
- void client2.flush(2e3);
627
+ void client.flush(2e3);
605
628
  });
606
629
  process.on("uncaughtException", onUncaught);
607
630
  process.on("unhandledRejection", onRejection);
@@ -615,13 +638,13 @@ function installGlobalHandlers(client2, getScope) {
615
638
  }
616
639
  if (typeof window !== "undefined") {
617
640
  const onError = (event) => {
618
- client2.captureException(event.error ?? event.message, { tags: { mechanism: "onerror" } }, getScope());
641
+ client.captureException(event.error ?? event.message, { tags: { mechanism: "onerror" } });
619
642
  };
620
643
  const onRejection = (event) => {
621
- client2.captureException(event.reason, { tags: { mechanism: "onunhandledrejection" } }, getScope());
644
+ client.captureException(event.reason, { tags: { mechanism: "onunhandledrejection" } });
622
645
  };
623
646
  const onPageHide = () => {
624
- void client2.flush(1e3);
647
+ void client.flush(1e3);
625
648
  };
626
649
  window.addEventListener("error", onError);
627
650
  window.addEventListener("unhandledrejection", onRejection);
@@ -761,349 +784,138 @@ var MachineSampler = class {
761
784
  }
762
785
  };
763
786
 
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;
787
+ // src/tasks/api.ts
788
+ var POLL_TIMEOUT_MS = 3e4;
789
+ var UPDATE_TIMEOUT_MS = 1e4;
790
+ var UPLOAD_TIMEOUT_MS = 3e4;
791
+ var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
792
+ var TasksApiError = class extends Error {
793
+ constructor(message, status) {
794
+ super(message);
795
+ this.status = status;
796
+ this.name = "TasksApiError";
776
797
  }
777
- setTags(tags) {
778
- Object.assign(this.tags, tags);
779
- return this;
798
+ };
799
+ var TasksApi = class {
800
+ baseUrl;
801
+ key;
802
+ logger;
803
+ constructor(options) {
804
+ this.baseUrl = options.baseUrl;
805
+ this.key = options.key;
806
+ this.logger = options.logger;
780
807
  }
781
- setUser(user) {
782
- this.user = user ?? void 0;
783
- return this;
808
+ headers() {
809
+ return {
810
+ "content-type": "application/json",
811
+ authorization: `Bearer ${this.key}`
812
+ };
784
813
  }
785
- setContext(name, context) {
786
- if (context === null) {
787
- delete this.contexts[name];
788
- } else {
789
- this.contexts[name] = context;
814
+ /** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
815
+ async poll(request, signal) {
816
+ const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
817
+ let response;
818
+ try {
819
+ response = await fetch(`${this.baseUrl}/poll`, {
820
+ method: "POST",
821
+ headers: this.headers(),
822
+ body: JSON.stringify(request),
823
+ signal: timeoutSignal
824
+ });
825
+ } finally {
826
+ cleanup();
790
827
  }
791
- return this;
792
- }
793
- setExtra(key, value) {
794
- this.extra[key] = value;
795
- return this;
796
- }
797
- setExtras(extras) {
798
- Object.assign(this.extra, extras);
799
- return this;
800
- }
801
- setType(type) {
802
- this.type = type;
803
- return this;
828
+ if (!response.ok) {
829
+ throw new TasksApiError(`poll falhou (HTTP ${response.status})`, response.status);
830
+ }
831
+ return this.parsePollResponse(await response.json(), request.max ?? 1);
804
832
  }
805
- setFingerprint(fingerprint) {
806
- this.fingerprint = fingerprint;
807
- return this;
833
+ /**
834
+ * Abre uma run por conta própria (modo start). Um request só — como no poll, o
835
+ * ladder de espera entre tentativas é decisão do chamador (LocalRunner).
836
+ * 4xx (401 = chave rejeitada) vira TasksApiError com o status.
837
+ */
838
+ async createRun(request, signal) {
839
+ const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
840
+ let response;
841
+ try {
842
+ response = await fetch(`${this.baseUrl}/runs`, {
843
+ method: "POST",
844
+ headers: this.headers(),
845
+ body: JSON.stringify(request),
846
+ signal: timeoutSignal
847
+ });
848
+ } finally {
849
+ cleanup();
850
+ }
851
+ if (!response.ok) {
852
+ throw new TasksApiError(`cria\xE7\xE3o da run falhou (HTTP ${response.status})`, response.status);
853
+ }
854
+ const body = await response.json().catch(() => null);
855
+ if (typeof body?.id !== "string" || typeof body.task !== "string") {
856
+ throw new TasksApiError("resposta inv\xE1lida da cria\xE7\xE3o de run (id/task ausentes)", 0);
857
+ }
858
+ return {
859
+ id: body.id,
860
+ task: body.task,
861
+ params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
862
+ form: null,
863
+ source: typeof body.source === "string" ? body.source : "agent",
864
+ created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
865
+ };
808
866
  }
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);
867
+ /** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
868
+ parsePollResponse(body, max) {
869
+ const runs = body?.runs;
870
+ if (!Array.isArray(runs)) {
871
+ throw new TasksApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
813
872
  }
814
- return this;
873
+ const valid = [];
874
+ for (const run of runs.slice(0, Math.max(0, max))) {
875
+ if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
876
+ const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
877
+ valid.push({ ...run, id: run.id, task: run.task, params });
878
+ }
879
+ if (valid.length < runs.length) {
880
+ this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
881
+ }
882
+ return { runs: valid };
815
883
  }
816
- clearBreadcrumbs() {
817
- this.breadcrumbs = [];
818
- return this;
884
+ /**
885
+ * PATCH de status/logs com retry em falha de rede/5xx/429.
886
+ * 4xx é definitivo e vira TasksApiError imediato (409 = run cancelada/transição inválida).
887
+ */
888
+ async updateRun(runId, update) {
889
+ let response;
890
+ try {
891
+ response = await fetchWithRetry(
892
+ () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
893
+ method: "PATCH",
894
+ headers: this.headers(),
895
+ body: JSON.stringify(update),
896
+ signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
897
+ }),
898
+ { logger: this.logger, label: `update da run ${runId}` }
899
+ );
900
+ } catch (error) {
901
+ throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
902
+ }
903
+ if (!response.ok) {
904
+ throw new TasksApiError(`update rejeitado (HTTP ${response.status})`, response.status);
905
+ }
819
906
  }
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 };
1071
- }
1072
- /**
1073
- * PATCH de status/logs com retry em falha de rede/5xx/429.
1074
- * 4xx é definitivo e vira TasksApiError imediato (409 = run cancelada/transição inválida).
1075
- */
1076
- async updateRun(runId, update) {
1077
- let response;
1078
- try {
1079
- response = await fetchWithRetry(
1080
- () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
1081
- method: "PATCH",
1082
- headers: this.headers(),
1083
- body: JSON.stringify(update),
1084
- signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
1085
- }),
1086
- { logger: this.logger, label: `update da run ${runId}` }
1087
- );
1088
- } catch (error) {
1089
- throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
1090
- }
1091
- if (!response.ok) {
1092
- throw new TasksApiError(`update rejeitado (HTTP ${response.status})`, response.status);
1093
- }
1094
- }
1095
- /**
1096
- * Um único PATCH com timeout curto — exit path (o processo está morrendo, o retry
1097
- * ladder do updateRun é proibido aqui). Status HTTP é ignorado: 409/404 significa
1098
- * que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
1099
- */
1100
- async updateRunOnce(runId, update, timeoutMs = 2e3) {
1101
- await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
1102
- method: "PATCH",
1103
- headers: this.headers(),
1104
- body: JSON.stringify(update),
1105
- signal: AbortSignal.timeout(timeoutMs)
1106
- });
907
+ /**
908
+ * Um único PATCH com timeout curto — exit path (o processo está morrendo, o retry
909
+ * ladder do updateRun é proibido aqui). Status HTTP é ignorado: 409/404 significa
910
+ * que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
911
+ */
912
+ async updateRunOnce(runId, update, timeoutMs = 2e3) {
913
+ await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
914
+ method: "PATCH",
915
+ headers: this.headers(),
916
+ body: JSON.stringify(update),
917
+ signal: AbortSignal.timeout(timeoutMs)
918
+ });
1107
919
  }
1108
920
  /**
1109
921
  * Envia um lote de itens de execução. Retry em falha de rede/5xx/429-transporte;
@@ -1206,13 +1018,83 @@ var TasksApi = class {
1206
1018
  body: JSON.stringify(request),
1207
1019
  signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
1208
1020
  }
1209
- );
1210
- if (!response.ok) {
1211
- throw new TasksApiError(`confirma\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
1021
+ );
1022
+ if (!response.ok) {
1023
+ throw new TasksApiError(`confirma\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
1024
+ }
1025
+ }
1026
+ };
1027
+
1028
+ // src/tasks/backoff.ts
1029
+ var RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
1030
+ var AUTH_FAILURE_INTERVAL_MS = 3e4;
1031
+ var Backoff = class {
1032
+ constructor(logger, label, floorMs = 0) {
1033
+ this.logger = logger;
1034
+ this.label = label;
1035
+ this.floorMs = floorMs;
1036
+ }
1037
+ failures = 0;
1038
+ authFailures = 0;
1039
+ reset() {
1040
+ this.failures = 0;
1041
+ this.authFailures = 0;
1042
+ }
1043
+ next(error) {
1044
+ if (error instanceof TasksApiError && error.status === 401) {
1045
+ this.authFailures++;
1046
+ if (this.authFailures === 1 || this.authFailures % 10 === 0) {
1047
+ this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes");
1048
+ }
1049
+ return Math.max(this.floorMs, AUTH_FAILURE_INTERVAL_MS);
1212
1050
  }
1051
+ this.failures++;
1052
+ const ladder = RETRY_DELAYS_MS[Math.min(this.failures - 1, RETRY_DELAYS_MS.length - 1)];
1053
+ const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.floorMs, 16e3));
1054
+ this.logger.warn(`${this.label} falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
1055
+ return delay;
1213
1056
  }
1214
1057
  };
1215
1058
 
1059
+ // src/tasks/errors.ts
1060
+ function runCanceledError() {
1061
+ return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
1062
+ }
1063
+ function taskTimeoutError(timeoutMs) {
1064
+ return Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
1065
+ name: "TaskTimeout"
1066
+ });
1067
+ }
1068
+ var TERMINAL_ERROR_NAMES = /* @__PURE__ */ new Set(["RunCanceled", "AbortError", "TaskTimeout"]);
1069
+ function isTerminalError(error) {
1070
+ return error instanceof Error && TERMINAL_ERROR_NAMES.has(error.name);
1071
+ }
1072
+
1073
+ // src/retry.ts
1074
+ var DEFAULT_RETRIES = 3;
1075
+ var DEFAULT_DELAY_MS = 1e3;
1076
+ function resolveDelay(delay, attempt, fallbackMs) {
1077
+ const value = typeof delay === "function" ? delay(attempt) : delay ?? fallbackMs;
1078
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1079
+ }
1080
+ async function retryWithBackoff(fn, options = {}) {
1081
+ const retries = Math.max(0, Math.floor(options.retries ?? DEFAULT_RETRIES));
1082
+ const wait = options.wait ?? sleep;
1083
+ for (let attempt = 1; ; attempt++) {
1084
+ if (options.signal?.aborted) throw runCanceledError();
1085
+ try {
1086
+ return await fn(attempt);
1087
+ } catch (error) {
1088
+ const exhausted = attempt > retries;
1089
+ const retriable = !isTerminalError(error) && !options.signal?.aborted && (options.shouldRetry?.(error) ?? true);
1090
+ if (exhausted || !retriable) throw error;
1091
+ const delay = resolveDelay(options.delayMs, attempt, DEFAULT_DELAY_MS);
1092
+ options.onRetry?.(error, attempt, delay);
1093
+ await wait(delay);
1094
+ }
1095
+ }
1096
+ }
1097
+
1216
1098
  // src/tasks/attachments.ts
1217
1099
  var MAX_ATTACHMENT_BYTES = 5242880;
1218
1100
  var MAX_ATTACHMENTS_PER_RUN = 20;
@@ -1264,14 +1146,11 @@ var LOG_LEVEL_COLOR = {
1264
1146
  };
1265
1147
  function printRunLog(task, level, message) {
1266
1148
  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" });
1149
+ const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
1150
+ console[method2](line, message);
1272
1151
  }
1273
1152
  function cancelableSleep(ms, signal) {
1274
- return new Promise((resolve, reject) => {
1153
+ return new Promise((resolve2, reject) => {
1275
1154
  if (signal.aborted) {
1276
1155
  reject(runCanceledError());
1277
1156
  return;
@@ -1282,7 +1161,7 @@ function cancelableSleep(ms, signal) {
1282
1161
  };
1283
1162
  const timer = setTimeout(() => {
1284
1163
  signal.removeEventListener("abort", onAbort);
1285
- resolve();
1164
+ resolve2();
1286
1165
  }, ms);
1287
1166
  signal.addEventListener("abort", onAbort, { once: true });
1288
1167
  });
@@ -1312,7 +1191,7 @@ function scrubPayloadValue(value, depth) {
1312
1191
  return out;
1313
1192
  }
1314
1193
  var RunReporter = class {
1315
- constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs) {
1194
+ constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
1316
1195
  this.api = api;
1317
1196
  this.runId = runId;
1318
1197
  this.logger = logger;
@@ -1329,17 +1208,40 @@ var RunReporter = class {
1329
1208
  success: (message) => this.log("success", message)
1330
1209
  };
1331
1210
  tx.setLifecycleSink((level, message) => this.push(level, message));
1211
+ const checkCanceled = () => this.checkCanceled();
1212
+ tx.setStepGuard(checkCanceled);
1332
1213
  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)
1214
+ add: (item) => {
1215
+ checkCanceled();
1216
+ this.addItem(item.status, item.message, item.payload, item.id);
1217
+ },
1218
+ succeeded: (payload, options) => {
1219
+ checkCanceled();
1220
+ this.addItem("succeeded", void 0, payload, options?.id);
1221
+ },
1222
+ failed: (message, payload, options) => {
1223
+ checkCanceled();
1224
+ this.addItem("failed", message, payload, options?.id);
1225
+ },
1226
+ occurrence: (message, payload, options) => {
1227
+ checkCanceled();
1228
+ this.addItem("occurrence", message, payload, options?.id);
1229
+ }
1337
1230
  };
1338
1231
  const progress = Object.defineProperties(
1339
1232
  {
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)
1233
+ total: (total) => {
1234
+ checkCanceled();
1235
+ this.setProgress(this.progressCurrent, total);
1236
+ },
1237
+ advance: (count = 1) => {
1238
+ checkCanceled();
1239
+ this.setProgress(this.progressCurrent + count, this.progressTotal);
1240
+ },
1241
+ set: (current, total) => {
1242
+ checkCanceled();
1243
+ this.setProgress(current, total ?? this.progressTotal);
1244
+ }
1343
1245
  },
1344
1246
  {
1345
1247
  current: { enumerable: true, get: () => this.progressCurrent },
@@ -1351,18 +1253,20 @@ var RunReporter = class {
1351
1253
  // defineProperties não tipa os getters adicionados — a interface é a fonte de verdade
1352
1254
  );
1353
1255
  const signal = this.abortController.signal;
1256
+ const sleep2 = (ms) => cancelableSleep(ms, signal);
1354
1257
  this.context = {
1355
1258
  runId,
1356
1259
  task,
1260
+ params,
1357
1261
  log,
1358
1262
  items,
1359
1263
  progress,
1360
- tx,
1264
+ transaction: tx,
1361
1265
  signal,
1362
- throwIfCanceled: () => {
1363
- if (signal.aborted) throw runCanceledError();
1364
- },
1365
- sleep: (ms) => cancelableSleep(ms, signal)
1266
+ throwIfCanceled: checkCanceled,
1267
+ sleep: sleep2,
1268
+ attach: (image, options) => this.attach(image, options),
1269
+ retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
1366
1270
  };
1367
1271
  }
1368
1272
  context;
@@ -1411,6 +1315,10 @@ var RunReporter = class {
1411
1315
  get isDead() {
1412
1316
  return this.dead;
1413
1317
  }
1318
+ /** Lança `RunCanceled` se o signal desta run já disparou. */
1319
+ checkCanceled() {
1320
+ if (this.abortController.signal.aborted) throw runCanceledError();
1321
+ }
1414
1322
  /** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
1415
1323
  abort() {
1416
1324
  this.stopCancelWatch();
@@ -1689,13 +1597,13 @@ var RunReporter = class {
1689
1597
  this.flushItems();
1690
1598
  await Promise.race([
1691
1599
  this.itemsChain,
1692
- new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1600
+ new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
1693
1601
  ]);
1694
1602
  if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
1695
1603
  this.flushItems();
1696
1604
  await Promise.race([
1697
1605
  this.itemsChain,
1698
- new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1606
+ new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
1699
1607
  ]);
1700
1608
  }
1701
1609
  }
@@ -1703,8 +1611,8 @@ var RunReporter = class {
1703
1611
  /**
1704
1612
  * Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
1705
1613
  * 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.
1614
+ * com um aviso no log da run. É o `hobots.attach()`; o hint `attachment` de
1615
+ * `hobots.captureException` também cai aqui via run-context-store.
1708
1616
  */
1709
1617
  async attach(image, options = {}) {
1710
1618
  const id = uuid4();
@@ -1726,8 +1634,8 @@ var RunReporter = class {
1726
1634
  printRunLog(this.context.task, level, `${scrubbed} [imagem]`);
1727
1635
  const logTs = this.push(level, scrubbed, true, [id]);
1728
1636
  let settle;
1729
- const result = new Promise((resolve) => {
1730
- settle = resolve;
1637
+ const result = new Promise((resolve2) => {
1638
+ settle = resolve2;
1731
1639
  });
1732
1640
  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
1641
  this.pendingAttachments.add(work);
@@ -1809,8 +1717,8 @@ var RunReporter = class {
1809
1717
  this.uploadSlots--;
1810
1718
  return Promise.resolve(release);
1811
1719
  }
1812
- return new Promise((resolve) => {
1813
- this.uploadQueue.push(() => resolve(release));
1720
+ return new Promise((resolve2) => {
1721
+ this.uploadQueue.push(() => resolve2(release));
1814
1722
  });
1815
1723
  }
1816
1724
  /**
@@ -1821,7 +1729,7 @@ var RunReporter = class {
1821
1729
  if (this.pendingAttachments.size === 0) return;
1822
1730
  await Promise.race([
1823
1731
  Promise.allSettled([...this.pendingAttachments]),
1824
- new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1732
+ new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
1825
1733
  ]);
1826
1734
  }
1827
1735
  enqueue(updateIn) {
@@ -1892,119 +1800,354 @@ async function runWithRunContext(store, fn) {
1892
1800
  await ensureStorage();
1893
1801
  return storage ? storage.run(store, fn) : fn();
1894
1802
  }
1895
- function currentRunTransaction() {
1896
- return storage?.getStore()?.tx;
1803
+ function currentRunReporter() {
1804
+ return storage?.getStore()?.reporter;
1805
+ }
1806
+
1807
+ // src/transaction.ts
1808
+ function stepId() {
1809
+ return uuid4().slice(0, 16);
1810
+ }
1811
+ function stepLabel(op, description) {
1812
+ return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
1813
+ }
1814
+ function levelForStatus(status) {
1815
+ switch (status) {
1816
+ case "warning":
1817
+ return "warning";
1818
+ case "error":
1819
+ return "error";
1820
+ case "cancelled":
1821
+ return "warning";
1822
+ default:
1823
+ return "success";
1824
+ }
1897
1825
  }
1898
- function currentRunAttach() {
1899
- return storage?.getStore()?.attach;
1826
+ function colorForStatus(status) {
1827
+ switch (status) {
1828
+ case "warning":
1829
+ return "yellow";
1830
+ case "error":
1831
+ return "red";
1832
+ case "cancelled":
1833
+ return "gray";
1834
+ default:
1835
+ return "green";
1836
+ }
1900
1837
  }
1838
+ var Step = class {
1839
+ constructor(transaction, parentStepId, op, description) {
1840
+ this.transaction = transaction;
1841
+ this.parentStepId = parentStepId;
1842
+ this.op = op;
1843
+ this.description = description;
1844
+ }
1845
+ stepId = stepId();
1846
+ startTimestamp = nowISO();
1847
+ status = "ok";
1848
+ endTimestamp;
1849
+ data;
1850
+ setStatus(status) {
1851
+ this.status = status;
1852
+ return this;
1853
+ }
1854
+ setData(key, value) {
1855
+ this.data = { ...this.data, [key]: value };
1856
+ return this;
1857
+ }
1858
+ /** Step aninhada (filha desta step). */
1859
+ startChild(op, description) {
1860
+ return this.transaction.createStep(this.stepId, op, description);
1861
+ }
1862
+ finish() {
1863
+ if (this.endTimestamp) return;
1864
+ this.endTimestamp = nowISO();
1865
+ this.transaction.emit(
1866
+ "step",
1867
+ "finish",
1868
+ stepLabel(this.op, this.description),
1869
+ this.startTimestamp,
1870
+ this.endTimestamp,
1871
+ this.status
1872
+ );
1873
+ }
1874
+ toJSON() {
1875
+ return {
1876
+ step_id: this.stepId,
1877
+ parent_step_id: this.parentStepId,
1878
+ op: this.op,
1879
+ description: this.description,
1880
+ status: this.status,
1881
+ start_timestamp: this.startTimestamp,
1882
+ timestamp: this.endTimestamp ?? nowISO(),
1883
+ data: this.data
1884
+ };
1885
+ }
1886
+ };
1887
+ var Transaction = class {
1888
+ constructor(client, name, op, runId) {
1889
+ this.client = client;
1890
+ this.name = name;
1891
+ this.op = op;
1892
+ this.runId = runId;
1893
+ this.emit("transaction", "start", op ? `${name} (${op})` : name);
1894
+ }
1895
+ eventId = uuid4();
1896
+ stepId = stepId();
1897
+ startTimestamp = nowISO();
1898
+ status = "ok";
1899
+ steps = [];
1900
+ tags = {};
1901
+ finished = false;
1902
+ lifecycleSink;
1903
+ // instalado pelo RunReporter: abrir uma step é um ponto de parada cooperativo (lança RunCanceled)
1904
+ stepGuard;
1905
+ // eventos emitidos antes do sink existir (o START da transaction sai no construtor,
1906
+ // antes do RunReporter conectar) — replay no setLifecycleSink
1907
+ lifecycleBacklog = [];
1908
+ /**
1909
+ * Registra o início/fim de uma step ou transaction: espelha a linha no sink de
1910
+ * lifecycle, se conectado (é assim que os eventos de performance aparecem nos
1911
+ * logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
1912
+ * escreve no stdout do processo do usuário sem permissão.
1913
+ */
1914
+ emit(kind, phase, label, startTs, endTs, status) {
1915
+ const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
1916
+ const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
1917
+ if (this.client?.debug) {
1918
+ const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
1919
+ console.log(`${tag} ${label}${suffix}`);
1920
+ }
1921
+ const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
1922
+ const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
1923
+ if (this.lifecycleSink) this.lifecycleSink(level, message);
1924
+ else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
1925
+ }
1926
+ /** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
1927
+ setLifecycleSink(sink) {
1928
+ this.lifecycleSink = sink;
1929
+ const backlog = this.lifecycleBacklog;
1930
+ this.lifecycleBacklog = [];
1931
+ for (const [level, message] of backlog) sink(level, message);
1932
+ }
1933
+ /**
1934
+ * Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
1935
+ * `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
1936
+ */
1937
+ setStepGuard(guard) {
1938
+ this.stepGuard = guard;
1939
+ }
1940
+ createStep(parentStepId, op, description) {
1941
+ this.stepGuard?.();
1942
+ const step = new Step(this, parentStepId, op, description);
1943
+ this.steps.push(step);
1944
+ this.emit("step", "start", stepLabel(op, description));
1945
+ return step;
1946
+ }
1947
+ /** Step direta da transaction. */
1948
+ startChild(op, description) {
1949
+ return this.createStep(this.stepId, op, description);
1950
+ }
1951
+ setTag(key, value) {
1952
+ this.tags[key] = value;
1953
+ return this;
1954
+ }
1955
+ setStatus(status) {
1956
+ this.status = status;
1957
+ return this;
1958
+ }
1959
+ setName(name) {
1960
+ this.name = name;
1961
+ return this;
1962
+ }
1963
+ finish() {
1964
+ if (this.finished) return;
1965
+ this.finished = true;
1966
+ for (const step of this.steps) step.finish();
1967
+ const endTimestamp = nowISO();
1968
+ this.emit(
1969
+ "transaction",
1970
+ "finish",
1971
+ this.op ? `${this.name} (${this.op})` : this.name,
1972
+ this.startTimestamp,
1973
+ endTimestamp,
1974
+ this.status
1975
+ );
1976
+ if (!this.client) return;
1977
+ const payload = {
1978
+ event_id: this.eventId,
1979
+ name: this.name,
1980
+ op: this.op,
1981
+ status: this.status,
1982
+ start_timestamp: this.startTimestamp,
1983
+ timestamp: endTimestamp,
1984
+ steps: this.steps.map((step) => step.toJSON()),
1985
+ tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
1986
+ run_id: this.runId
1987
+ };
1988
+ this.client.sendTransaction(payload);
1989
+ }
1990
+ };
1901
1991
 
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);
1992
+ // src/tasks/runner.ts
1993
+ var DEFAULT_RETRY_DELAY_MS = 5e3;
1994
+ async function executeRun(input) {
1995
+ const { run, task, api, client, logger, tracker } = input;
1996
+ const options = task.options;
1997
+ const tx = new Transaction(client, `run:${run.task}`, "hobots.task", run.id);
1998
+ const reporter = new RunReporter(
1999
+ api,
2000
+ run.id,
2001
+ run.task,
2002
+ logger,
2003
+ tx,
2004
+ options.logFlushIntervalMs,
2005
+ options.cancelCheckIntervalMs,
2006
+ run.params
2007
+ );
2008
+ tracker.add(reporter);
2009
+ try {
2010
+ const ok = await reporter.reportRunning();
2011
+ if (!ok) {
2012
+ logger.debug(`run ${run.id} cancelada antes de iniciar \u2014 handler n\xE3o executado`);
2013
+ tx.setStatus("cancelled");
2014
+ tx.finish();
2015
+ return;
2016
+ }
2017
+ logger.debug(`executando run ${run.id} (${run.task})`);
2018
+ try {
2019
+ await runWithRunContext({ reporter }, () => invokeWithRetry(task, run, reporter, logger));
2020
+ await reporter.finalizeAttachments();
2021
+ await reporter.finalizeItems();
2022
+ tx.setStatus("ok");
2023
+ tx.finish();
2024
+ reporter.reportSuccess();
2025
+ logger.debug(`run ${run.id} conclu\xEDda`);
2026
+ } catch (error) {
2027
+ await reporter.finalizeAttachments();
2028
+ await reporter.finalizeItems();
2029
+ tx.setStatus("error");
2030
+ tx.finish();
2031
+ reporter.reportFailure(toRunError(error));
2032
+ logger.debug(`run ${run.id} falhou: ${String(error)}`);
2033
+ }
2034
+ await reporter.finalize();
2035
+ } finally {
2036
+ tracker.remove(reporter);
2037
+ }
2038
+ }
2039
+ function invokeWithRetry(task, run, reporter, logger) {
2040
+ const { retries, retryDelayMs } = task.options;
2041
+ const total = retries + 1;
2042
+ const ctx = reporter.context;
2043
+ return retryWithBackoff(() => invokeWithTimeout(task, run, reporter, logger), {
2044
+ retries,
2045
+ delayMs: (attempt) => resolveDelay(retryDelayMs, attempt, DEFAULT_RETRY_DELAY_MS),
2046
+ shouldRetry: () => !reporter.isDead,
2047
+ signal: ctx.signal,
2048
+ wait: (ms) => ctx.sleep(ms),
2049
+ onRetry: (error, attempt, delayMs) => {
2050
+ const reason = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2051
+ ctx.log.warn(`tentativa ${attempt}/${total} falhou: ${truncate(reason, 500)} \u2014 nova tentativa em ${delayMs}ms`);
2052
+ }
2053
+ });
2054
+ }
2055
+ var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
2056
+ var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
2057
+ async function invokeWithTimeout(task, run, reporter, logger) {
2058
+ const { timeoutMs, cancelGraceMs } = task.options;
2059
+ const handler = task.handler;
2060
+ const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
2061
+ const races = [];
2062
+ if (timeoutMs && timeoutMs > 0) {
2063
+ races.push(sleep(timeoutMs).then(() => TIMEOUT_SENTINEL));
2064
+ }
2065
+ if (cancelGraceMs > 0) {
2066
+ races.push(waitForCancelGrace(reporter.context.signal, cancelGraceMs).then(() => CANCEL_SENTINEL));
2067
+ }
2068
+ if (races.length === 0) {
2069
+ await invocation;
2070
+ return;
1928
2071
  }
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
- });
2072
+ const outcome = await Promise.race([invocation, ...races]);
2073
+ if (outcome !== TIMEOUT_SENTINEL && outcome !== CANCEL_SENTINEL) return;
2074
+ if (outcome === CANCEL_SENTINEL) {
2075
+ logger.warn(
2076
+ `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)`
2077
+ );
2078
+ return;
2079
+ }
2080
+ reporter.abort();
2081
+ 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)`));
2082
+ throw taskTimeoutError(timeoutMs);
2083
+ }
2084
+ function waitForCancelGrace(signal, graceMs) {
2085
+ return new Promise((resolve2) => {
2086
+ if (signal.aborted) {
2087
+ setTimeout(resolve2, graceMs);
2088
+ return;
1937
2089
  }
2090
+ signal.addEventListener("abort", () => setTimeout(resolve2, graceMs), { once: true });
1938
2091
  });
1939
- process.on("uncaughtException", onFatal);
1940
- return () => {
1941
- for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
1942
- process.removeListener("uncaughtException", onFatal);
1943
- };
2092
+ }
2093
+ var MAX_ERROR_MESSAGE_CHARS = 1e4;
2094
+ var MAX_ERROR_STACK_CHARS = 2e4;
2095
+ function toRunError(error) {
2096
+ if (error instanceof Error) {
2097
+ return {
2098
+ type: error.name,
2099
+ message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
2100
+ stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
2101
+ };
2102
+ }
2103
+ return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
1944
2104
  }
1945
2105
 
1946
2106
  // src/tasks/agent.ts
1947
2107
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
1948
2108
  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;
1956
- }
2109
+ var MAX_CLAIM_PER_POLL = 5;
1957
2110
  var TaskAgent = class {
1958
- constructor(options, registry2) {
2111
+ constructor(options) {
1959
2112
  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 });
2113
+ this.logger = options.logger;
2114
+ this.api = options.api;
1964
2115
  this.client = options.client;
2116
+ this.registry = options.registry;
2117
+ this.tracker = options.tracker;
1965
2118
  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 });
2119
+ this.backoff = new Backoff(this.logger, "poll", this.pollIntervalMs);
1973
2120
  }
1974
2121
  logger;
2122
+ pollIntervalMs;
1975
2123
  api;
1976
2124
  client;
1977
- pollIntervalMs;
1978
- concurrency;
1979
- instanceId;
1980
- taskTimeoutMs;
1981
- logFlushIntervalMs;
1982
- cancelCheckIntervalMs;
1983
- cancelGraceMs;
2125
+ registry;
2126
+ tracker;
2127
+ backoff;
1984
2128
  state = "idle";
1985
2129
  timer;
1986
2130
  pollAbort;
1987
- consecutiveFailures = 0;
1988
- consecutiveAuthFailures = 0;
1989
2131
  activeRuns = /* @__PURE__ */ new Set();
1990
- reporters = /* @__PURE__ */ new Set();
1991
- uninstallShutdown;
1992
2132
  get activeCount() {
1993
2133
  return this.activeRuns.size;
1994
2134
  }
1995
2135
  start() {
1996
2136
  if (this.state === "started") return;
1997
2137
  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()");
2138
+ throw new Error("[hobots] start do agente durante o stop \u2014 aguarde o stop terminar");
2002
2139
  }
2003
2140
  this.state = "started";
2004
- if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
2005
- this.uninstallShutdown = installShutdownHandlers(this);
2141
+ this.logger.debug(`polling iniciado (tasks: ${this.registry.remoteTasks().map((task) => task.slug).join(", ")})`);
2142
+ this.schedule(0);
2143
+ }
2144
+ /** Uma task registrada depois do boot entra no próximo poll — que é antecipado para já. */
2145
+ wake() {
2146
+ if (this.state !== "started") return;
2147
+ if (this.timer) {
2148
+ clearTimeout(this.timer);
2149
+ this.timer = void 0;
2006
2150
  }
2007
- this.logger.debug(`polling iniciado (tasks: ${[...this.registry.keys()].join(", ")})`);
2008
2151
  this.schedule(0);
2009
2152
  }
2010
2153
  /** Para o polling e aguarda as runs ativas (até timeoutMs). Retorna true se tudo terminou a tempo. */
@@ -2017,40 +2160,10 @@ var TaskAgent = class {
2017
2160
  }
2018
2161
  this.pollAbort?.abort();
2019
2162
  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;
2163
+ const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : this.activeRuns.size === 0;
2027
2164
  this.state = "idle";
2028
2165
  return finished;
2029
2166
  }
2030
- /**
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.
2035
- */
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
- );
2050
- }
2051
- if (pending.length === 0) return;
2052
- await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
2053
- }
2054
2167
  /** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
2055
2168
  schedule(delayMs) {
2056
2169
  if (this.state !== "started" || this.timer) return;
@@ -2059,9 +2172,22 @@ var TaskAgent = class {
2059
2172
  void this.tick();
2060
2173
  }, delayMs);
2061
2174
  }
2175
+ /** Slots livres por task remota — a soma vira o `max` do poll e as tasks com slot viram o filtro. */
2176
+ freeSlots() {
2177
+ const tasks = [];
2178
+ let max = 0;
2179
+ for (const task of this.registry.remoteTasks()) {
2180
+ const free = task.options.concurrency - this.tracker.activeFor(task.slug);
2181
+ if (free <= 0) continue;
2182
+ tasks.push(task.slug);
2183
+ max += free;
2184
+ }
2185
+ return { tasks, max: Math.min(MAX_CLAIM_PER_POLL, max) };
2186
+ }
2062
2187
  async tick() {
2063
2188
  if (this.state !== "started") return;
2064
- if (this.activeRuns.size >= this.concurrency) {
2189
+ const { tasks, max } = this.freeSlots();
2190
+ if (max === 0) {
2065
2191
  this.schedule(this.pollIntervalMs);
2066
2192
  return;
2067
2193
  }
@@ -2069,224 +2195,448 @@ var TaskAgent = class {
2069
2195
  try {
2070
2196
  const response = await this.api.poll(
2071
2197
  {
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()]
2198
+ // identidade canônica do agente no painel — o mesmo instanceId do heartbeat,
2199
+ // então os dois canais convergem em um agente. `instance.tasks` lista TODAS as
2200
+ // tasks (register + start); o filtro `tasks` abaixo só as que este poll pode rodar.
2201
+ instance: this.options.instanceInfo(),
2202
+ max,
2203
+ tasks
2092
2204
  },
2093
2205
  this.pollAbort.signal
2094
2206
  );
2095
- this.consecutiveFailures = 0;
2096
- this.consecutiveAuthFailures = 0;
2207
+ this.backoff.reset();
2097
2208
  for (const run of response.runs) {
2098
- if (this.activeRuns.size >= this.concurrency) break;
2099
2209
  this.dispatch(run);
2100
2210
  }
2101
2211
  this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
2102
2212
  } catch (error) {
2103
2213
  if (this.state !== "started") return;
2104
- this.schedule(this.delayAfterPollError(error));
2214
+ this.schedule(this.backoff.next(error));
2105
2215
  } finally {
2106
2216
  this.pollAbort = void 0;
2107
2217
  }
2108
2218
  }
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
2219
  dispatch(run) {
2124
- const promise = this.executeRun(run).catch((error) => this.logger.error(`erro inesperado na run ${run.id}:`, error)).finally(() => {
2220
+ const promise = this.executeClaimed(run).catch((error) => this.logger.error(`erro inesperado na run ${run.id}:`, error)).finally(() => {
2125
2221
  this.activeRuns.delete(promise);
2126
2222
  if (this.state === "started") this.schedule(0);
2127
2223
  });
2128
2224
  this.activeRuns.add(promise);
2129
2225
  }
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);
2226
+ async executeClaimed(run) {
2227
+ const task = this.registry.get(run.task);
2228
+ if (!task || task.mode !== "remote") {
2229
+ const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", run.id);
2230
+ const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx);
2231
+ reporter.reportFailure(toRunError(Object.assign(new Error(`tarefa n\xE3o registrada no agente: ${run.task}`), { name: "HandlerNotFound" })));
2232
+ await reporter.finalize();
2233
+ tx.setStatus("error");
2234
+ tx.finish();
2235
+ return;
2236
+ }
2237
+ await executeRun({ run, task, api: this.api, client: this.client, logger: this.logger, tracker: this.tracker });
2238
+ }
2239
+ };
2240
+
2241
+ // src/tasks/local.ts
2242
+ var LocalRunner = class {
2243
+ constructor(task, deps) {
2244
+ this.task = task;
2245
+ this.deps = deps;
2246
+ this.backoff = new Backoff(deps.logger, `cria\xE7\xE3o da run "${task.slug}"`);
2247
+ this.done = new Promise((resolve2) => {
2248
+ this.resolveDone = resolve2;
2249
+ });
2250
+ }
2251
+ abort = new AbortController();
2252
+ backoff;
2253
+ /** Resolve quando a run terminou (ou o runner foi parado antes de abri-la). */
2254
+ done;
2255
+ resolveDone;
2256
+ async run() {
2134
2257
  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})`);
2258
+ const run = await this.createRun();
2259
+ if (!run) return;
2260
+ this.deps.logger.debug(`run ${run.id} aberta para "${this.task.slug}" (modo start)`);
2261
+ await executeRun({
2262
+ run,
2263
+ task: this.task,
2264
+ api: this.deps.api,
2265
+ client: this.deps.client,
2266
+ logger: this.deps.logger,
2267
+ tracker: this.deps.tracker
2268
+ });
2269
+ } catch (error) {
2270
+ this.deps.logger.error(`erro inesperado na task "${this.task.slug}":`, error);
2271
+ } finally {
2272
+ this.resolveDone();
2273
+ }
2274
+ }
2275
+ /** Desiste de abrir a run (close() antes da plataforma responder). Uma run já aberta segue pelo tracker. */
2276
+ stop() {
2277
+ this.abort.abort();
2278
+ }
2279
+ async createRun() {
2280
+ for (; ; ) {
2281
+ if (this.abort.signal.aborted) return void 0;
2154
2282
  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`);
2283
+ return await this.deps.api.createRun({ task: this.task.slug, instance: this.deps.instanceInfo() }, this.abort.signal);
2165
2284
  } 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)}`);
2285
+ if (this.abort.signal.aborted) return void 0;
2286
+ await abortableSleep(this.backoff.next(error), this.abort.signal);
2172
2287
  }
2173
- await reporter.finalize();
2174
- } finally {
2175
- this.reporters.delete(reporter);
2176
2288
  }
2177
2289
  }
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));
2183
- }
2184
- if (this.cancelGraceMs > 0) {
2185
- races.push(waitForCancelGrace(reporter.context.signal, this.cancelGraceMs).then(() => CANCEL_SENTINEL));
2290
+ };
2291
+
2292
+ // src/tasks/registry.ts
2293
+ var TaskRegistry = class {
2294
+ tasks = /* @__PURE__ */ new Map();
2295
+ add(task) {
2296
+ if (this.tasks.has(task.slug)) {
2297
+ throw new Error(`[hobots] task "${task.slug}" j\xE1 registrada \u2014 cada slug s\xF3 pode ter um handler (register ou start)`);
2186
2298
  }
2187
- if (races.length === 0) {
2188
- await invocation;
2189
- return;
2299
+ this.tasks.set(task.slug, task);
2300
+ }
2301
+ get(slug) {
2302
+ return this.tasks.get(slug);
2303
+ }
2304
+ get size() {
2305
+ return this.tasks.size;
2306
+ }
2307
+ /** Tasks que chegam pelo poll (register). */
2308
+ remoteTasks() {
2309
+ return [...this.tasks.values()].filter((task) => task.mode === "remote");
2310
+ }
2311
+ /** Tasks abertas pelo próprio bot (start). */
2312
+ localTasks() {
2313
+ return [...this.tasks.values()].filter((task) => task.mode === "local");
2314
+ }
2315
+ /** Todos os slugs, para a presença da instância (`instance.tasks`). */
2316
+ allSlugs() {
2317
+ return [...this.tasks.keys()];
2318
+ }
2319
+ clear() {
2320
+ this.tasks.clear();
2321
+ }
2322
+ };
2323
+ var RunTracker = class {
2324
+ constructor(api) {
2325
+ this.api = api;
2326
+ }
2327
+ reporters = /* @__PURE__ */ new Set();
2328
+ waiters = /* @__PURE__ */ new Set();
2329
+ add(reporter) {
2330
+ this.reporters.add(reporter);
2331
+ }
2332
+ remove(reporter) {
2333
+ this.reporters.delete(reporter);
2334
+ if (this.reporters.size === 0) {
2335
+ for (const resolve2 of this.waiters) resolve2();
2336
+ this.waiters.clear();
2190
2337
  }
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)`
2338
+ }
2339
+ get activeCount() {
2340
+ return this.reporters.size;
2341
+ }
2342
+ /** Runs ativas de uma task (concorrência por task no poll). */
2343
+ activeFor(slug) {
2344
+ let count = 0;
2345
+ for (const reporter of this.reporters) if (reporter.context.task === slug) count++;
2346
+ return count;
2347
+ }
2348
+ /** A única run ativa, quando há exatamente uma — o que `hobots.*` usa fora de um contexto async. */
2349
+ singleActive() {
2350
+ if (this.reporters.size !== 1) return void 0;
2351
+ return this.reporters.values().next().value;
2352
+ }
2353
+ /** Resolve quando não houver mais runs ativas (ou após timeoutMs, devolvendo false). */
2354
+ async waitIdle(timeoutMs) {
2355
+ if (this.reporters.size === 0) return true;
2356
+ const idle = new Promise((resolve2) => this.waiters.add(() => resolve2(true)));
2357
+ if (timeoutMs <= 0) return false;
2358
+ return Promise.race([idle, sleep(timeoutMs).then(() => false)]);
2359
+ }
2360
+ /**
2361
+ * Best-effort do exit path: reporta 'failed'/AgentTerminated para as runs ativas
2362
+ * antes do processo morrer, fora da fila do reporter e com timeout curto. Runs já
2363
+ * canceladas no app (reporter morto) são puladas; erros de rede são engolidos —
2364
+ * o sweep server-side é o backstop.
2365
+ */
2366
+ async reportAgentTerminated(message, timeoutMs = 2500) {
2367
+ const pending = [];
2368
+ for (const reporter of this.reporters) {
2369
+ if (reporter.isDead) continue;
2370
+ const logs = reporter.drainLogs();
2371
+ reporter.kill();
2372
+ pending.push(
2373
+ this.api.updateRunOnce(
2374
+ reporter.context.runId,
2375
+ { status: "failed", error: { type: "AgentTerminated", message }, logs: logs.length > 0 ? logs : void 0 },
2376
+ timeoutMs
2377
+ ).catch(() => {
2378
+ })
2196
2379
  );
2197
- return;
2198
2380
  }
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
- });
2381
+ if (pending.length === 0) return;
2382
+ await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
2205
2383
  }
2206
2384
  };
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;
2385
+
2386
+ // src/tasks/shutdown.ts
2387
+ var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
2388
+ function installShutdownHandlers(agent) {
2389
+ if (!isNode()) return () => {
2390
+ };
2391
+ let signaled = false;
2392
+ const signalHandlers = /* @__PURE__ */ new Map();
2393
+ for (const signal of WATCHED_SIGNALS) {
2394
+ const handler = markInternal(() => {
2395
+ if (externalListenerCount(signal) > 0) return;
2396
+ if (signaled) return;
2397
+ signaled = true;
2398
+ const reraise = () => {
2399
+ process.removeListener(signal, handler);
2400
+ process.kill(process.pid, signal);
2401
+ };
2402
+ if (agent.activeCount === 0) {
2403
+ reraise();
2404
+ return;
2405
+ }
2406
+ const report = agent.reportAgentTerminated(`o processo do agente recebeu ${signal} durante a execu\xE7\xE3o`);
2407
+ addEmergencyTask(report);
2408
+ void report.finally(reraise);
2409
+ });
2410
+ process.on(signal, handler);
2411
+ signalHandlers.set(signal, handler);
2412
+ }
2413
+ const onFatal = markInternal((error) => {
2414
+ 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();
2415
+ addEmergencyTask(report);
2416
+ if (!hasInstalledGlobalHandlers() && externalListenerCount("uncaughtException") === 0) {
2417
+ void report.finally(() => {
2418
+ console.error(error);
2419
+ process.exit(1);
2420
+ });
2214
2421
  }
2215
- signal.addEventListener("abort", () => setTimeout(resolve, graceMs), { once: true });
2216
2422
  });
2423
+ process.on("uncaughtException", onFatal);
2424
+ return () => {
2425
+ for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
2426
+ process.removeListener("uncaughtException", onFatal);
2427
+ };
2217
2428
  }
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
- };
2429
+
2430
+ // src/runtime.ts
2431
+ var HEARTBEAT_INTERVAL_MS = 3e4;
2432
+ var DEFAULT_RETRIES2 = 1;
2433
+ var DEFAULT_RETRY_DELAY_MS2 = 5e3;
2434
+ var DEFAULT_CANCEL_GRACE_MS = 1e4;
2435
+ var MAX_CONCURRENCY = 5;
2436
+ var MAX_SLUG_CHARS = 255;
2437
+ var PROCESS_LEVEL_KEYS = [
2438
+ "clientSecret",
2439
+ "instanceId",
2440
+ "environment",
2441
+ "release",
2442
+ "tags",
2443
+ "debug",
2444
+ "machineMetrics",
2445
+ "captureUnhandled",
2446
+ "installExitHandlers"
2447
+ ];
2448
+ var runtime;
2449
+ var warnedNoRuntime = false;
2450
+ function hostname() {
2451
+ if (!isNode()) return void 0;
2452
+ return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
2453
+ }
2454
+ function sameValue(a, b) {
2455
+ if (a === b) return true;
2456
+ if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
2457
+ return false;
2458
+ }
2459
+ function ensureRuntime(options) {
2460
+ if (runtime) {
2461
+ for (const key of PROCESS_LEVEL_KEYS) {
2462
+ if (options[key] !== void 0 && !sameValue(options[key], runtime.options[key])) {
2463
+ runtime.logger.warn(
2464
+ `op\xE7\xE3o "${key}" divergente ignorada \u2014 o primeiro register()/start() define as op\xE7\xF5es do processo`
2465
+ );
2466
+ }
2467
+ }
2468
+ return runtime;
2469
+ }
2470
+ const config = resolveConfig({ clientSecret: options.clientSecret });
2471
+ const logger = makeLogger(options.debug ?? false);
2472
+ const client = new Client(
2473
+ { environment: options.environment, release: options.release, tags: options.tags, debug: options.debug },
2474
+ config
2475
+ );
2476
+ const api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger });
2477
+ runtime = {
2478
+ options: { ...options, clientSecret: void 0 },
2479
+ config,
2480
+ client,
2481
+ logger,
2482
+ api,
2483
+ registry: new TaskRegistry(),
2484
+ tracker: new RunTracker(api),
2485
+ sampler: void 0,
2486
+ heartbeat: void 0,
2487
+ agent: void 0,
2488
+ locals: /* @__PURE__ */ new Map(),
2489
+ instanceId: resolveInstanceId(options.instanceId),
2490
+ pollIntervalMs: void 0,
2491
+ booted: false,
2492
+ bootScheduled: false,
2493
+ bootPromise: void 0,
2494
+ uninstallGlobalHandlers: options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0,
2495
+ uninstallShutdown: void 0
2496
+ };
2497
+ runtime.options.clientSecret = options.clientSecret;
2498
+ logger.debug("inicializado", { endpoint: config.ingestEndpoint });
2499
+ return runtime;
2500
+ }
2501
+ function validateTask(slug, handler, fn) {
2502
+ if (typeof slug !== "string" || slug.trim().length === 0 || slug.length > MAX_SLUG_CHARS) {
2503
+ throw new Error(`[hobots] ${fn}(): o slug da task precisa ser uma string de 1 a ${MAX_SLUG_CHARS} caracteres`);
2227
2504
  }
2228
- return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
2505
+ if (typeof handler !== "function") {
2506
+ throw new Error(`[hobots] ${fn}('${slug}'): o handler precisa ser uma fun\xE7\xE3o`);
2507
+ }
2508
+ return slug.trim();
2229
2509
  }
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
- );
2510
+ function clampConcurrency(value) {
2511
+ if (value === void 0 || !Number.isFinite(value)) return 1;
2512
+ return Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(value)));
2513
+ }
2514
+ function resolveTaskOptions(options, mode) {
2515
+ const retries = options.retries === void 0 || !Number.isFinite(options.retries) ? DEFAULT_RETRIES2 : Math.max(0, Math.floor(options.retries));
2516
+ return {
2517
+ retries,
2518
+ retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS2,
2519
+ timeoutMs: options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : void 0,
2520
+ concurrency: mode === "remote" ? clampConcurrency(options.concurrency) : 1,
2521
+ logFlushIntervalMs: options.logFlushIntervalMs,
2522
+ cancelCheckIntervalMs: options.cancelCheckIntervalMs,
2523
+ cancelGraceMs: options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS
2524
+ };
2525
+ }
2526
+ function instanceInfo(rt) {
2527
+ return {
2528
+ instance_id: rt.instanceId ?? "unknown",
2529
+ version: SDK_VERSION,
2530
+ host: hostname(),
2531
+ // ambiente da instância: junto do heartbeat, é como o servidor sabe onde o robô roda
2532
+ environment: rt.options.environment,
2533
+ // TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
2534
+ tasks: rt.registry.allSlugs(),
2535
+ // o servidor calcula a tolerância de offline (3×) a partir disto em agentes que fazem
2536
+ // poll; clampado à faixa aceita pelo schema (1s..15min)
2537
+ poll_interval_ms: rt.agent ? Math.min(Math.max(rt.agent.pollIntervalMs, 1e3), 9e5) : void 0,
2538
+ // recursos da máquina — alimenta a série da instância (o servidor grava no máx. 1 ponto/30s)
2539
+ machine: rt.sampler?.sample()
2540
+ };
2541
+ }
2542
+ function ensureAgent(rt) {
2543
+ rt.agent ??= new TaskAgent({
2544
+ api: rt.api,
2545
+ client: rt.client,
2546
+ logger: rt.logger,
2547
+ registry: rt.registry,
2548
+ tracker: rt.tracker,
2549
+ instanceInfo: () => instanceInfo(rt),
2550
+ pollIntervalMs: rt.pollIntervalMs
2551
+ });
2552
+ return rt.agent;
2553
+ }
2554
+ function launchLocal(rt, task) {
2555
+ const runner = new LocalRunner(task, {
2556
+ api: rt.api,
2557
+ client: rt.client,
2558
+ logger: rt.logger,
2559
+ tracker: rt.tracker,
2560
+ instanceInfo: () => instanceInfo(rt)
2561
+ });
2562
+ rt.locals.set(task.slug, runner);
2563
+ void runner.run();
2564
+ }
2565
+ async function boot(rt) {
2566
+ if (!rt.instanceId) {
2567
+ const id = await fallbackInstanceId();
2568
+ if (runtime !== rt) return;
2569
+ rt.instanceId = id;
2570
+ rt.logger.debug(`instanceId n\xE3o informado (env ${INSTANCE_ID_ENV}) \u2014 usando o hostname "${id}"`);
2571
+ }
2572
+ if (runtime !== rt) return;
2573
+ rt.booted = true;
2574
+ if (rt.options.machineMetrics !== false) {
2575
+ rt.sampler = new MachineSampler(rt.instanceId, rt.logger);
2576
+ void rt.sampler.start();
2577
+ rt.client.attachMachineSampler(rt.sampler);
2578
+ }
2579
+ rt.heartbeat = new Heartbeat(rt.client, { intervalMs: HEARTBEAT_INTERVAL_MS, keepAlive: true }, rt.instanceId, rt.sampler);
2580
+ rt.heartbeat.start();
2581
+ if (rt.options.installExitHandlers !== false) {
2582
+ rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
2583
+ }
2584
+ if (rt.registry.remoteTasks().length > 0) ensureAgent(rt).start();
2585
+ for (const task of rt.registry.localTasks()) launchLocal(rt, task);
2586
+ }
2587
+ function scheduleBoot(rt) {
2588
+ if (rt.bootScheduled) return;
2589
+ rt.bootScheduled = true;
2590
+ queueMicrotask(() => {
2591
+ rt.bootPromise = boot(rt).catch((error) => rt.logger.error("falha ao iniciar o SDK:", error));
2592
+ });
2593
+ }
2594
+ function addTask(fn, mode, task, handler, options) {
2595
+ const slug = validateTask(task, handler, fn);
2596
+ const rt = ensureRuntime(options);
2597
+ if (mode === "remote" && options.pollIntervalMs !== void 0) {
2598
+ if (rt.pollIntervalMs === void 0 && !rt.agent) rt.pollIntervalMs = options.pollIntervalMs;
2599
+ else if (rt.pollIntervalMs !== options.pollIntervalMs) {
2600
+ rt.logger.warn('op\xE7\xE3o "pollIntervalMs" divergente ignorada \u2014 o primeiro register() define o intervalo do poll');
2601
+ }
2602
+ }
2603
+ const registered = { slug, mode, handler, options: resolveTaskOptions(options, mode) };
2604
+ rt.registry.add(registered);
2605
+ if (!rt.booted) {
2606
+ scheduleBoot(rt);
2607
+ return;
2608
+ }
2609
+ if (mode === "remote") {
2610
+ const agent = ensureAgent(rt);
2611
+ agent.start();
2612
+ agent.wake();
2613
+ } else {
2614
+ launchLocal(rt, registered);
2273
2615
  }
2274
- client.logger.debug("inicializado", { endpoint: client.config.ingestEndpoint });
2275
2616
  }
2276
- function getClient() {
2277
- return client;
2617
+ function register(task, handler, options = {}) {
2618
+ addTask("register", "remote", task, handler, options);
2619
+ }
2620
+ function start(task, handler, options = {}) {
2621
+ addTask("start", "local", task, handler, options);
2622
+ }
2623
+ function currentReporter() {
2624
+ return currentRunReporter() ?? runtime?.tracker.singleActive();
2625
+ }
2626
+ function activeLogger() {
2627
+ return runtime?.logger;
2278
2628
  }
2279
2629
  function toRunLogLevel(type) {
2280
2630
  return type === "fatal" ? "error" : type;
2281
2631
  }
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)");
2632
+ function forwardAttachment(rt, attachment, message, type) {
2633
+ const reporter = currentReporter();
2634
+ if (!reporter) {
2635
+ rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task");
2286
2636
  return;
2287
2637
  }
2288
2638
  const withMeta = typeof attachment === "object" && "image" in attachment ? attachment : { image: attachment };
2289
- void attach(withMeta.image, {
2639
+ void reporter.attach(withMeta.image, {
2290
2640
  message,
2291
2641
  level: toRunLogLevel(type),
2292
2642
  caption: withMeta.caption,
@@ -2294,109 +2644,124 @@ function forwardAttachment(attachment, message, type) {
2294
2644
  });
2295
2645
  }
2296
2646
  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");
2647
+ const rt = runtime;
2648
+ if (!rt) {
2649
+ if (!warnedNoRuntime) {
2650
+ warnedNoRuntime = true;
2651
+ console.warn("[hobots] captureException antes de registrar uma task (register/start) \u2014 evento descartado");
2652
+ }
2653
+ return void 0;
2300
2654
  }
2301
- return client?.captureException(error, hint, scope);
2302
- }
2303
- function captureMessage(message, type = "warning", hint) {
2304
2655
  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);
2656
+ const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2657
+ forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
2658
+ }
2659
+ return rt.client.captureException(error, hint);
2660
+ }
2661
+ async function close(timeoutMs = 3e4) {
2662
+ const rt = runtime;
2663
+ if (!rt) return true;
2664
+ runtime = void 0;
2665
+ await rt.bootPromise;
2666
+ if (rt.agent) await rt.agent.stop(0);
2667
+ for (const local of rt.locals.values()) local.stop();
2668
+ const idle = await rt.tracker.waitIdle(timeoutMs);
2669
+ if (!idle) {
2670
+ rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
2671
+ await rt.tracker.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
2672
+ }
2673
+ rt.heartbeat?.stop();
2674
+ rt.uninstallShutdown?.();
2675
+ rt.uninstallGlobalHandlers?.();
2676
+ const flushed = await rt.client.close(timeoutMs);
2677
+ return idle && flushed;
2369
2678
  }
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;
2679
+
2680
+ // src/hobots.ts
2681
+ var warned = /* @__PURE__ */ new Set();
2682
+ function warnOutsideRun(member) {
2683
+ if (warned.has(member)) return;
2684
+ warned.add(member);
2685
+ 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)`;
2686
+ const logger = activeLogger();
2687
+ if (logger) logger.warn(message);
2688
+ else console.warn(`[hobots] ${message}`);
2689
+ }
2690
+ var FALLBACK_TASK = "hobots";
2691
+ var fallbackLog = {
2692
+ debug: (message) => printRunLog(FALLBACK_TASK, "debug", message),
2693
+ info: (message) => printRunLog(FALLBACK_TASK, "info", message),
2694
+ warn: (message) => printRunLog(FALLBACK_TASK, "warning", message),
2695
+ error: (message) => printRunLog(FALLBACK_TASK, "error", message),
2696
+ success: (message) => printRunLog(FALLBACK_TASK, "success", message)
2697
+ };
2698
+ var noop = () => void 0;
2699
+ var fallbackItems = { add: noop, succeeded: noop, failed: noop, occurrence: noop };
2700
+ var fallbackProgress = { total: noop, advance: noop, set: noop, current: 0, snapshot: null };
2701
+ var neverAborts = new AbortController().signal;
2702
+ var detachedTransaction;
2703
+ function resolve(member) {
2704
+ const reporter = currentReporter();
2705
+ if (reporter) return reporter.context;
2706
+ warnOutsideRun(member);
2707
+ return void 0;
2376
2708
  }
2709
+ function getter(member, fallback) {
2710
+ return { enumerable: true, get: () => resolve(member)?.[member] ?? fallback() };
2711
+ }
2712
+ function method(value) {
2713
+ return { enumerable: true, writable: false, value };
2714
+ }
2715
+ var hobots = Object.defineProperties({}, {
2716
+ // ── contexto da run atual (getters) ─────────────────────────────────────────
2717
+ runId: getter("runId", () => ""),
2718
+ task: getter("task", () => ""),
2719
+ params: getter("params", () => ({})),
2720
+ log: getter("log", () => fallbackLog),
2721
+ items: getter("items", () => fallbackItems),
2722
+ progress: getter("progress", () => fallbackProgress),
2723
+ transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
2724
+ signal: getter("signal", () => neverAborts),
2725
+ throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
2726
+ sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
2727
+ attach: method(
2728
+ (image, options) => resolve("attach")?.attach(image, options) ?? Promise.resolve({ id: "", ok: false, reason: "canceled" })
2729
+ ),
2730
+ retry: method((fn, options) => {
2731
+ const reporter = currentReporter();
2732
+ return reporter ? reporter.context.retry(fn, options) : retryWithBackoff(fn, options);
2733
+ }),
2734
+ // ── processo ────────────────────────────────────────────────────────────────
2735
+ register: method((task, handler, options) => {
2736
+ register(task, handler, options);
2737
+ installGlobalHobots();
2738
+ }),
2739
+ start: method((task, handler, options) => {
2740
+ start(task, handler, options);
2741
+ installGlobalHobots();
2742
+ }),
2743
+ close: method((timeoutMs) => close(timeoutMs)),
2744
+ captureException: method((error, hint) => captureException(error, hint))
2745
+ });
2746
+ function installGlobalHobots() {
2747
+ if (globalThis.hobots === hobots) return;
2748
+ Object.defineProperty(globalThis, "hobots", { value: hobots, configurable: true, writable: true, enumerable: false });
2749
+ }
2750
+ var register2 = hobots.register;
2751
+ var start2 = hobots.start;
2752
+ var close2 = hobots.close;
2753
+ var captureException2 = hobots.captureException;
2754
+ var retry = hobots.retry;
2755
+
2756
+ // src/index.ts
2757
+ var src_default = hobots;
2377
2758
  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
2759
+ captureException2 as captureException,
2760
+ close2 as close,
2761
+ src_default as default,
2762
+ hobots,
2763
+ register2 as register,
2764
+ retry,
2765
+ start2 as start
2401
2766
  };
2402
2767
  //# sourceMappingURL=index.js.map