hobots 1.0.0 → 1.1.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,14 +1,28 @@
1
- import {
2
- SDK_NAME,
3
- SDK_VERSION,
4
- isNode,
5
- makeLogger,
6
- nowISO,
7
- parseDsn,
8
- sleep,
9
- unrefTimer,
10
- uuid4
11
- } from "./chunk-FW32OR7V.js";
1
+ // src/dsn.ts
2
+ function parseDsnUrl(dsn, prefix) {
3
+ let url;
4
+ try {
5
+ url = new URL(dsn);
6
+ } catch {
7
+ throw new Error(`${prefix} DSN inv\xE1lido: "${dsn}"`);
8
+ }
9
+ const key = url.username;
10
+ const path = url.pathname.replace(/^\/+/, "");
11
+ if (!key || !path) {
12
+ throw new Error(`${prefix} DSN inv\xE1lido: "${dsn}" \u2014 esperado https://<key>@host/<projeto>`);
13
+ }
14
+ return { key, path, origin: `${url.protocol}//${url.host}` };
15
+ }
16
+ function parseDsn(dsn) {
17
+ const { key, path, origin } = parseDsnUrl(dsn, "[hobots]");
18
+ return {
19
+ key,
20
+ projectSlug: path,
21
+ origin,
22
+ ingestEndpoint: `${origin}/api/ingest`,
23
+ managerBaseUrl: `${origin}/api/manager`
24
+ };
25
+ }
12
26
 
13
27
  // src/stacktrace.ts
14
28
  var MAX_FRAMES = 50;
@@ -43,6 +57,44 @@ function parseStacktrace(stack) {
43
57
  return frames;
44
58
  }
45
59
 
60
+ // src/utils.ts
61
+ var SDK_NAME = "hobots";
62
+ var SDK_VERSION = "1.0.0";
63
+ function isNode() {
64
+ return typeof process !== "undefined" && typeof process.versions?.node === "string";
65
+ }
66
+ function uuid4() {
67
+ const cryptoRef = globalThis.crypto;
68
+ if (cryptoRef && typeof cryptoRef.randomUUID === "function") {
69
+ return cryptoRef.randomUUID().replace(/-/g, "");
70
+ }
71
+ let out = "";
72
+ for (let i = 0; i < 32; i++) {
73
+ out += Math.floor(Math.random() * 16).toString(16);
74
+ }
75
+ return out;
76
+ }
77
+ function nowISO() {
78
+ return (/* @__PURE__ */ new Date()).toISOString();
79
+ }
80
+ function unrefTimer(timer) {
81
+ timer?.unref?.();
82
+ }
83
+ function sleep(ms) {
84
+ return new Promise((resolve) => {
85
+ const timer = setTimeout(resolve, ms);
86
+ unrefTimer(timer);
87
+ });
88
+ }
89
+ var PREFIX = "[hobots-monitor]";
90
+ function makeLogger(enabled, prefix = PREFIX) {
91
+ return {
92
+ debug: (...args) => enabled && console.debug(prefix, ...args),
93
+ warn: (...args) => enabled && console.warn(prefix, ...args),
94
+ error: (...args) => console.error(prefix, ...args)
95
+ };
96
+ }
97
+
46
98
  // src/eventbuilder.ts
47
99
  function safeString(value) {
48
100
  if (typeof value === "string") return value;
@@ -92,6 +144,7 @@ var DEFAULT_MAX_BATCH_SIZE = 10;
92
144
  var DEFAULT_MAX_QUEUE_SIZE = 200;
93
145
  var Transport = class {
94
146
  endpoint;
147
+ key;
95
148
  logger;
96
149
  flushIntervalMs;
97
150
  maxBatchSize;
@@ -102,6 +155,7 @@ var Transport = class {
102
155
  closed = false;
103
156
  constructor(options) {
104
157
  this.endpoint = options.endpoint;
158
+ this.key = options.key;
105
159
  this.logger = options.logger;
106
160
  this.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
107
161
  this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
@@ -154,7 +208,7 @@ var Transport = class {
154
208
  try {
155
209
  const response = await fetch(this.endpoint, {
156
210
  method: "POST",
157
- headers: { "content-type": "application/json" },
211
+ headers: { "content-type": "application/json", authorization: `Bearer ${this.key}` },
158
212
  body: JSON.stringify(envelope)
159
213
  });
160
214
  if (response.ok) {
@@ -210,7 +264,8 @@ var Client = class {
210
264
  this.logger = makeLogger(!!options.debug);
211
265
  this.dsn = parseDsn(options.dsn);
212
266
  this.transport = new Transport({
213
- endpoint: this.dsn.endpoint,
267
+ endpoint: this.dsn.ingestEndpoint,
268
+ key: this.dsn.key,
214
269
  logger: this.logger,
215
270
  flushIntervalMs: options.flushIntervalMs,
216
271
  maxBatchSize: options.maxBatchSize
@@ -245,7 +300,6 @@ var Client = class {
245
300
  level: hint?.level ?? event.level ?? scope2.level ?? "error",
246
301
  release: event.release ?? this.options.release,
247
302
  environment: event.environment ?? this.options.environment ?? "production",
248
- server_name: event.server_name ?? this.options.serverName,
249
303
  tags: Object.keys(tags).length > 0 ? tags : void 0,
250
304
  extra: Object.keys(extra).length > 0 ? extra : void 0,
251
305
  user: event.user ?? scope2.user,
@@ -261,7 +315,6 @@ var Client = class {
261
315
  ...payload,
262
316
  release: payload.release ?? this.options.release,
263
317
  environment: payload.environment ?? this.options.environment ?? "production",
264
- server_name: payload.server_name ?? this.options.serverName,
265
318
  tags: { ...this.options.tags, ...payload.tags }
266
319
  }
267
320
  });
@@ -271,12 +324,16 @@ var Client = class {
271
324
  type: "check_in",
272
325
  payload: {
273
326
  ...payload,
327
+ release: payload.release ?? this.options.release,
274
328
  environment: payload.environment ?? this.options.environment ?? "production"
275
329
  }
276
330
  });
277
331
  }
278
332
  sendHeartbeat(payload) {
279
- this.transport.enqueue({ type: "heartbeat", payload });
333
+ this.transport.enqueue({
334
+ type: "heartbeat",
335
+ payload: { ...payload, release: payload.release ?? this.options.release }
336
+ });
280
337
  }
281
338
  flush(timeoutMs) {
282
339
  return this.transport.flush(timeoutMs);
@@ -288,19 +345,12 @@ var Client = class {
288
345
 
289
346
  // src/heartbeat.ts
290
347
  var DEFAULT_INTERVAL_MS = 3e4;
291
- function defaultInstanceId() {
292
- if (isNode()) {
293
- const host = process.env.HOSTNAME || process.env.COMPUTERNAME;
294
- if (host) return host;
295
- }
296
- return `instance-${uuid4().slice(0, 8)}`;
297
- }
298
348
  var Heartbeat = class {
299
- constructor(client2, options = {}, serverName) {
349
+ constructor(client2, options = {}, instanceId) {
300
350
  this.client = client2;
301
351
  this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
302
- this.instanceId = options.instanceId ?? serverName ?? defaultInstanceId();
303
- this.name = options.name ?? this.instanceId;
352
+ this.instanceId = instanceId;
353
+ this.name = options.name ?? instanceId;
304
354
  this.metadata = options.metadata;
305
355
  }
306
356
  intervalMs;
@@ -581,27 +631,437 @@ async function withMonitorImpl(client2, captureError, monitorSlug, fn, options)
581
631
  }
582
632
  }
583
633
 
634
+ // src/manager/api.ts
635
+ var RETRY_DELAYS_MS2 = [1e3, 4e3, 16e3];
636
+ var ManagerApiError = class extends Error {
637
+ constructor(message, status) {
638
+ super(message);
639
+ this.status = status;
640
+ this.name = "ManagerApiError";
641
+ }
642
+ };
643
+ var ManagerApi = class {
644
+ baseUrl;
645
+ key;
646
+ logger;
647
+ constructor(options) {
648
+ this.baseUrl = options.baseUrl;
649
+ this.key = options.key;
650
+ this.logger = options.logger;
651
+ }
652
+ headers() {
653
+ return {
654
+ "content-type": "application/json",
655
+ authorization: `Bearer ${this.key}`
656
+ };
657
+ }
658
+ /** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
659
+ async poll(request, signal) {
660
+ const response = await fetch(`${this.baseUrl}/poll`, {
661
+ method: "POST",
662
+ headers: this.headers(),
663
+ body: JSON.stringify(request),
664
+ signal
665
+ });
666
+ if (!response.ok) {
667
+ throw new ManagerApiError(`poll falhou (HTTP ${response.status})`, response.status);
668
+ }
669
+ return await response.json();
670
+ }
671
+ /**
672
+ * PATCH de status/logs com retry em falha de rede/5xx/429.
673
+ * 4xx é definitivo e vira ManagerApiError imediato (409 = run cancelada/transição inválida).
674
+ */
675
+ async updateRun(runId, update) {
676
+ for (let attempt = 0; ; attempt++) {
677
+ let retriable;
678
+ try {
679
+ const response = await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
680
+ method: "PATCH",
681
+ headers: this.headers(),
682
+ body: JSON.stringify(update)
683
+ });
684
+ if (response.ok) return;
685
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
686
+ throw new ManagerApiError(`update rejeitado (HTTP ${response.status})`, response.status);
687
+ }
688
+ retriable = new Error(`HTTP ${response.status}`);
689
+ } catch (error) {
690
+ if (error instanceof ManagerApiError) throw error;
691
+ retriable = error;
692
+ }
693
+ const delay = RETRY_DELAYS_MS2[attempt];
694
+ if (delay === void 0) {
695
+ throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(retriable)}`);
696
+ }
697
+ this.logger.warn(`falha no update da run ${runId} (${String(retriable)}), retry em ${delay}ms`);
698
+ await sleep(delay);
699
+ }
700
+ }
701
+ };
702
+
703
+ // src/manager/context.ts
704
+ var DEFAULT_LOG_FLUSH_INTERVAL_MS = 2e3;
705
+ var MAX_LOG_MESSAGE_CHARS = 1e4;
706
+ var MAX_LOGS_PER_RUN = 500;
707
+ var MAX_LOGS_PER_PATCH = 100;
708
+ var FLUSH_AT_BUFFERED = 20;
709
+ var RunReporter = class {
710
+ constructor(api, runId, task, logger, flushIntervalMs) {
711
+ this.api = api;
712
+ this.runId = runId;
713
+ this.logger = logger;
714
+ this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
715
+ this.context = {
716
+ runId,
717
+ task,
718
+ log: (level, message) => this.log(level, message),
719
+ signal: this.abortController.signal
720
+ };
721
+ }
722
+ context;
723
+ buffer = [];
724
+ loggedCount = 0;
725
+ droppedCount = 0;
726
+ dead = false;
727
+ chain = Promise.resolve();
728
+ timer;
729
+ abortController = new AbortController();
730
+ flushIntervalMs;
731
+ get isDead() {
732
+ return this.dead;
733
+ }
734
+ /** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
735
+ abort() {
736
+ if (!this.abortController.signal.aborted) this.abortController.abort();
737
+ }
738
+ log(level, message) {
739
+ if (this.dead) return;
740
+ if (this.loggedCount >= MAX_LOGS_PER_RUN) {
741
+ this.droppedCount++;
742
+ return;
743
+ }
744
+ this.loggedCount++;
745
+ const truncated = message.length > MAX_LOG_MESSAGE_CHARS ? `${message.slice(0, MAX_LOG_MESSAGE_CHARS)}\u2026 [truncado]` : message;
746
+ this.buffer.push({ ts: nowISO(), level, message: truncated });
747
+ if (this.buffer.length >= FLUSH_AT_BUFFERED) {
748
+ this.flushLogs();
749
+ } else {
750
+ this.scheduleFlush();
751
+ }
752
+ }
753
+ scheduleFlush() {
754
+ if (this.timer) return;
755
+ this.timer = setTimeout(() => {
756
+ this.timer = void 0;
757
+ this.flushLogs();
758
+ }, this.flushIntervalMs);
759
+ unrefTimer(this.timer);
760
+ }
761
+ clearFlushTimer() {
762
+ if (this.timer) {
763
+ clearTimeout(this.timer);
764
+ this.timer = void 0;
765
+ }
766
+ }
767
+ takeLogs() {
768
+ const logs = this.buffer.splice(0, this.buffer.length);
769
+ if (this.droppedCount > 0) {
770
+ logs.push({
771
+ ts: nowISO(),
772
+ level: "warning",
773
+ message: `${this.droppedCount} logs descartados (limite de ${MAX_LOGS_PER_RUN} por execu\xE7\xE3o)`
774
+ });
775
+ this.droppedCount = 0;
776
+ }
777
+ return logs;
778
+ }
779
+ /** Flush incremental: envia o buffer como status 'running' (em chunks). */
780
+ flushLogs() {
781
+ this.clearFlushTimer();
782
+ if (this.dead || this.buffer.length === 0) return;
783
+ const logs = this.buffer.splice(0, this.buffer.length);
784
+ for (let i = 0; i < logs.length; i += MAX_LOGS_PER_PATCH) {
785
+ this.enqueue({ status: "running", logs: logs.slice(i, i + MAX_LOGS_PER_PATCH) });
786
+ }
787
+ }
788
+ /** Primeiro PATCH (claimed → running). Retorna false se a run já morreu (cancelada). */
789
+ async reportRunning() {
790
+ this.enqueue({ status: "running" });
791
+ await this.chain;
792
+ return !this.dead;
793
+ }
794
+ reportSuccess() {
795
+ this.clearFlushTimer();
796
+ this.enqueue({ status: "succeeded", logs: this.takeLogs() });
797
+ }
798
+ reportFailure(error) {
799
+ this.clearFlushTimer();
800
+ this.enqueue({ status: "failed", error, logs: this.takeLogs() });
801
+ }
802
+ /** Aguarda a fila de PATCHes esvaziar (chame depois do terminal). */
803
+ finalize() {
804
+ return this.chain;
805
+ }
806
+ enqueue(update) {
807
+ if (this.dead) return;
808
+ this.chain = this.chain.then(async () => {
809
+ if (this.dead) return;
810
+ const payload = update.logs && update.logs.length === 0 ? { ...update, logs: void 0 } : update;
811
+ try {
812
+ await this.api.updateRun(this.runId, payload);
813
+ } catch (error) {
814
+ if (error instanceof ManagerApiError && (error.status === 409 || error.status === 404)) {
815
+ this.dead = true;
816
+ this.abort();
817
+ this.buffer = [];
818
+ this.logger.warn(`run ${this.runId} cancelada/removida no servidor \u2014 reporte suprimido`);
819
+ return;
820
+ }
821
+ this.logger.error(`update da run ${this.runId} perdido:`, error);
822
+ }
823
+ });
824
+ }
825
+ };
826
+
827
+ // src/manager/agent.ts
828
+ var DEFAULT_POLL_INTERVAL_MS = 5e3;
829
+ var MIN_POLL_INTERVAL_MS = 1e3;
830
+ var MAX_CONCURRENCY = 5;
831
+ var POLL_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
832
+ var AUTH_FAILURE_INTERVAL_MS = 3e4;
833
+ function hostname() {
834
+ if (!isNode()) return void 0;
835
+ return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
836
+ }
837
+ var ManagerAgent = class {
838
+ constructor(options, registry2) {
839
+ this.options = options;
840
+ this.registry = registry2;
841
+ this.logger = makeLogger(options.debug ?? false, "[hobots-manager]");
842
+ const dsn = parseDsn(options.dsn);
843
+ this.api = new ManagerApi({ baseUrl: dsn.managerBaseUrl, key: dsn.key, logger: this.logger });
844
+ this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
845
+ this.concurrency = Math.min(MAX_CONCURRENCY, Math.max(1, options.concurrency ?? 1));
846
+ this.instanceId = options.instanceId;
847
+ this.taskTimeoutMs = options.taskTimeoutMs;
848
+ this.logFlushIntervalMs = options.logFlushIntervalMs;
849
+ this.logger.debug("agente configurado", { baseUrl: dsn.managerBaseUrl, projectSlug: dsn.projectSlug });
850
+ }
851
+ logger;
852
+ api;
853
+ pollIntervalMs;
854
+ concurrency;
855
+ instanceId;
856
+ taskTimeoutMs;
857
+ logFlushIntervalMs;
858
+ state = "idle";
859
+ timer;
860
+ pollAbort;
861
+ consecutiveFailures = 0;
862
+ consecutiveAuthFailures = 0;
863
+ activeRuns = /* @__PURE__ */ new Set();
864
+ reporters = /* @__PURE__ */ new Set();
865
+ get activeCount() {
866
+ return this.activeRuns.size;
867
+ }
868
+ start() {
869
+ if (this.state === "started") return;
870
+ if (this.state === "stopping") {
871
+ throw new Error("[hobots-manager] start() chamado durante stop() \u2014 aguarde o stop terminar");
872
+ }
873
+ if (this.registry.size === 0) {
874
+ throw new Error("[hobots-manager] nenhuma task registrada \u2014 chame register() antes de start()");
875
+ }
876
+ this.state = "started";
877
+ this.logger.debug(`polling iniciado (tasks: ${[...this.registry.keys()].join(", ")})`);
878
+ this.schedule(0);
879
+ }
880
+ /** Para o polling e aguarda as runs ativas (até timeoutMs). Retorna true se tudo terminou a tempo. */
881
+ async stop(timeoutMs = 3e4) {
882
+ if (this.state === "idle") return true;
883
+ this.state = "stopping";
884
+ if (this.timer) {
885
+ clearTimeout(this.timer);
886
+ this.timer = void 0;
887
+ }
888
+ this.pollAbort?.abort();
889
+ const all = Promise.allSettled([...this.activeRuns]).then(() => true);
890
+ const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : false;
891
+ if (!finished && this.activeRuns.size > 0) {
892
+ this.logger.warn(`${this.activeRuns.size} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
893
+ for (const reporter of this.reporters) reporter.abort();
894
+ }
895
+ this.state = "idle";
896
+ return finished;
897
+ }
898
+ /** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
899
+ schedule(delayMs) {
900
+ if (this.state !== "started" || this.timer) return;
901
+ this.timer = setTimeout(() => {
902
+ this.timer = void 0;
903
+ void this.tick();
904
+ }, delayMs);
905
+ }
906
+ async tick() {
907
+ if (this.state !== "started") return;
908
+ if (this.activeRuns.size >= this.concurrency) {
909
+ this.schedule(this.pollIntervalMs);
910
+ return;
911
+ }
912
+ this.pollAbort = new AbortController();
913
+ try {
914
+ const response = await this.api.poll(
915
+ {
916
+ agent: {
917
+ // identidade canônica do agente no painel — o mesmo instanceId do
918
+ // heartbeat do Monitor, então os dois canais convergem em um agente
919
+ instance_id: this.instanceId,
920
+ version: SDK_VERSION,
921
+ host: hostname(),
922
+ tasks: [...this.registry.keys()]
923
+ },
924
+ max: Math.min(MAX_CONCURRENCY, this.concurrency - this.activeRuns.size),
925
+ tasks: [...this.registry.keys()]
926
+ },
927
+ this.pollAbort.signal
928
+ );
929
+ this.consecutiveFailures = 0;
930
+ this.consecutiveAuthFailures = 0;
931
+ for (const run of response.runs) {
932
+ this.dispatch(run);
933
+ }
934
+ this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
935
+ } catch (error) {
936
+ if (this.state !== "started") return;
937
+ this.schedule(this.delayAfterPollError(error));
938
+ } finally {
939
+ this.pollAbort = void 0;
940
+ }
941
+ }
942
+ delayAfterPollError(error) {
943
+ if (error instanceof ManagerApiError && error.status === 401) {
944
+ this.consecutiveAuthFailures++;
945
+ if (this.consecutiveAuthFailures === 1 || this.consecutiveAuthFailures % 10 === 0) {
946
+ this.logger.error("chave rejeitada (401) \u2014 verifique a DSN do projeto em Configura\xE7\xF5es \u2192 Agentes");
947
+ }
948
+ return Math.max(this.pollIntervalMs, AUTH_FAILURE_INTERVAL_MS);
949
+ }
950
+ this.consecutiveFailures++;
951
+ const ladder = POLL_RETRY_DELAYS_MS[Math.min(this.consecutiveFailures - 1, POLL_RETRY_DELAYS_MS.length - 1)];
952
+ const delay = Math.min(Math.max(ladder, 0), Math.max(this.pollIntervalMs, 16e3));
953
+ this.logger.warn(`poll falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
954
+ return delay;
955
+ }
956
+ dispatch(run) {
957
+ const promise = this.executeRun(run).catch((error) => this.logger.error(`erro inesperado na run ${run.id}:`, error)).finally(() => {
958
+ this.activeRuns.delete(promise);
959
+ if (this.state === "started") this.schedule(0);
960
+ });
961
+ this.activeRuns.add(promise);
962
+ }
963
+ async executeRun(run) {
964
+ const reporter = new RunReporter(this.api, run.id, run.task, this.logger, this.logFlushIntervalMs);
965
+ this.reporters.add(reporter);
966
+ try {
967
+ const handler = this.registry.get(run.task);
968
+ if (!handler) {
969
+ reporter.reportFailure({
970
+ type: "HandlerNotFound",
971
+ message: `tarefa n\xE3o registrada no agente: ${run.task}`
972
+ });
973
+ await reporter.finalize();
974
+ return;
975
+ }
976
+ const ok = await reporter.reportRunning();
977
+ if (!ok) {
978
+ this.logger.debug(`run ${run.id} cancelada antes de iniciar \u2014 handler n\xE3o executado`);
979
+ return;
980
+ }
981
+ this.logger.debug(`executando run ${run.id} (${run.task})`);
982
+ try {
983
+ await this.invokeWithTimeout(handler, run, reporter);
984
+ reporter.reportSuccess();
985
+ this.logger.debug(`run ${run.id} conclu\xEDda`);
986
+ } catch (error) {
987
+ reporter.reportFailure(toRunError(error));
988
+ this.logger.debug(`run ${run.id} falhou: ${String(error)}`);
989
+ }
990
+ await reporter.finalize();
991
+ } finally {
992
+ this.reporters.delete(reporter);
993
+ }
994
+ }
995
+ async invokeWithTimeout(handler, run, reporter) {
996
+ const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
997
+ if (!this.taskTimeoutMs || this.taskTimeoutMs <= 0) {
998
+ await invocation;
999
+ return;
1000
+ }
1001
+ const timeoutMs = this.taskTimeoutMs;
1002
+ const timeout = sleep(timeoutMs).then(() => TIMEOUT_SENTINEL);
1003
+ const outcome = await Promise.race([invocation, timeout]);
1004
+ if (outcome !== TIMEOUT_SENTINEL) return;
1005
+ reporter.abort();
1006
+ 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)`));
1007
+ throw Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
1008
+ name: "TaskTimeout"
1009
+ });
1010
+ }
1011
+ };
1012
+ var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
1013
+ var MAX_ERROR_MESSAGE_CHARS = 1e4;
1014
+ var MAX_ERROR_STACK_CHARS = 2e4;
1015
+ function truncate(value, max) {
1016
+ return value.length > max ? `${value.slice(0, max)}\u2026 [truncado]` : value;
1017
+ }
1018
+ function toRunError(error) {
1019
+ if (error instanceof Error) {
1020
+ return {
1021
+ type: error.name,
1022
+ message: truncate(error.message, MAX_ERROR_MESSAGE_CHARS),
1023
+ stack: error.stack ? truncate(error.stack, MAX_ERROR_STACK_CHARS) : void 0
1024
+ };
1025
+ }
1026
+ return { message: truncate(String(error), MAX_ERROR_MESSAGE_CHARS) };
1027
+ }
1028
+
584
1029
  // src/core.ts
585
1030
  var DEFAULT_MAX_BREADCRUMBS = 100;
586
1031
  var client;
587
1032
  var scope = new Scope();
588
1033
  var heartbeat;
1034
+ var agent;
1035
+ var registry = /* @__PURE__ */ new Map();
1036
+ var warnedManagerDisabled = false;
589
1037
  function init(options) {
590
1038
  if (client) {
591
1039
  client.logger.warn("init() chamado mais de uma vez \u2014 substituindo o client anterior");
592
1040
  heartbeat?.stop();
593
1041
  heartbeat = void 0;
594
1042
  }
1043
+ if (agent) {
1044
+ void agent.stop(0);
1045
+ agent = void 0;
1046
+ }
1047
+ warnedManagerDisabled = false;
595
1048
  client = new Client(options);
596
1049
  if (options.captureUnhandled !== false) {
597
1050
  installGlobalHandlers(client, () => scope);
598
1051
  }
599
1052
  if (options.heartbeat) {
600
1053
  const heartbeatOptions = options.heartbeat === true ? {} : options.heartbeat;
601
- heartbeat = new Heartbeat(client, heartbeatOptions, options.serverName);
1054
+ heartbeat = new Heartbeat(client, heartbeatOptions, options.instanceId);
602
1055
  heartbeat.start();
603
1056
  }
604
- client.logger.debug("inicializado", { endpoint: client.dsn.endpoint });
1057
+ if (options.manager) {
1058
+ const managerOptions = options.manager === true ? {} : options.manager;
1059
+ agent = new ManagerAgent(
1060
+ { ...managerOptions, dsn: options.dsn, instanceId: options.instanceId, debug: options.debug },
1061
+ registry
1062
+ );
1063
+ }
1064
+ client.logger.debug("inicializado", { endpoint: client.dsn.ingestEndpoint });
605
1065
  }
606
1066
  function getClient() {
607
1067
  return client;
@@ -660,13 +1120,39 @@ async function withMonitor(monitorSlug, fn, options) {
660
1120
  function flush(timeoutMs) {
661
1121
  return client ? client.flush(timeoutMs) : Promise.resolve(true);
662
1122
  }
663
- function close(timeoutMs) {
1123
+ function register(task, handler) {
1124
+ if (registry.has(task)) {
1125
+ (agent?.logger ?? client?.logger)?.warn(`register('${task}') sobrescreveu um handler existente`);
1126
+ }
1127
+ if (client && !agent && !warnedManagerDisabled) {
1128
+ warnedManagerDisabled = true;
1129
+ client.logger.warn(
1130
+ `register('${task}') com o Manager desabilitado \u2014 passe manager: true (ou op\xE7\xF5es) no init()`
1131
+ );
1132
+ }
1133
+ registry.set(task, handler);
1134
+ }
1135
+ function start() {
1136
+ if (!agent) {
1137
+ throw new Error("[hobots] Manager n\xE3o configurado \u2014 passe manager: true (ou op\xE7\xF5es) no init() antes de start()");
1138
+ }
1139
+ agent.start();
1140
+ }
1141
+ function stop(timeoutMs) {
1142
+ return agent ? agent.stop(timeoutMs) : Promise.resolve(true);
1143
+ }
1144
+ async function close(timeoutMs) {
1145
+ const agentStopped = agent ? await agent.stop(timeoutMs) : true;
664
1146
  heartbeat?.stop();
665
1147
  heartbeat = void 0;
666
- return client ? client.close(timeoutMs) : Promise.resolve(true);
1148
+ const clientClosed = client ? await client.close(timeoutMs) : true;
1149
+ return agentStopped && clientClosed;
667
1150
  }
668
1151
  export {
669
1152
  Client,
1153
+ ManagerAgent,
1154
+ ManagerApi,
1155
+ ManagerApiError,
670
1156
  Scope,
671
1157
  Span,
672
1158
  Transaction,
@@ -679,12 +1165,15 @@ export {
679
1165
  getClient,
680
1166
  init,
681
1167
  parseDsn,
1168
+ register,
682
1169
  setContext,
683
1170
  setExtra,
684
1171
  setTag,
685
1172
  setTags,
686
1173
  setUser,
1174
+ start,
687
1175
  startTransaction,
1176
+ stop,
688
1177
  withMonitor,
689
1178
  withScope
690
1179
  };