hobots 1.3.3 → 1.3.5

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
@@ -58,7 +58,7 @@ function parseStacktrace(stack) {
58
58
 
59
59
  // src/utils.ts
60
60
  var SDK_NAME = "hobots";
61
- var SDK_VERSION = true ? "1.3.2" : "0.0.0-dev";
61
+ var SDK_VERSION = true ? "1.3.4" : "0.0.0-dev";
62
62
  function isNode() {
63
63
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
64
64
  }
@@ -380,28 +380,61 @@ var Heartbeat = class {
380
380
  }
381
381
  };
382
382
 
383
+ // src/internal-handlers.ts
384
+ var INTERNAL = /* @__PURE__ */ Symbol.for("hobots.sdk.internalListener");
385
+ function markInternal(listener) {
386
+ listener[INTERNAL] = true;
387
+ return listener;
388
+ }
389
+ function externalListenerCount(event) {
390
+ const listeners = process.listeners(event);
391
+ return listeners.filter((listener) => !listener[INTERNAL]).length;
392
+ }
393
+ var emergencyTasks = [];
394
+ function addEmergencyTask(task) {
395
+ emergencyTasks.push(task);
396
+ }
397
+ async function awaitEmergencyTasks(capMs) {
398
+ if (emergencyTasks.length === 0) return;
399
+ await Promise.race([Promise.allSettled(emergencyTasks), sleep(capMs)]);
400
+ }
401
+
383
402
  // src/instrument.ts
403
+ var EMERGENCY_REPORT_CAP_MS = 3e3;
384
404
  var installed = false;
405
+ function hasInstalledGlobalHandlers() {
406
+ return installed;
407
+ }
385
408
  function installGlobalHandlers(client2, getScope) {
386
409
  if (installed) return;
387
410
  installed = true;
388
411
  if (isNode()) {
389
- process.on("uncaughtException", (error) => {
390
- client2.captureException(error, { level: "fatal", tags: { mechanism: "uncaughtException" } }, getScope());
391
- void client2.flush(2e3).then(() => {
392
- if (process.listenerCount("uncaughtException") <= 1) {
393
- console.error(error);
394
- process.exit(1);
395
- }
396
- });
397
- });
398
- process.on("unhandledRejection", (reason) => {
399
- client2.captureException(reason, { level: "error", tags: { mechanism: "unhandledRejection" } }, getScope());
400
- void client2.flush(2e3);
401
- });
402
- process.on("beforeExit", () => {
403
- void client2.flush(2e3);
404
- });
412
+ process.on(
413
+ "uncaughtException",
414
+ markInternal((error) => {
415
+ client2.captureException(error, { level: "fatal", tags: { mechanism: "uncaughtException" } }, getScope());
416
+ void client2.flush(2e3).then(async () => {
417
+ if (externalListenerCount("uncaughtException") === 0) {
418
+ await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
419
+ console.error(error);
420
+ process.exit(1);
421
+ }
422
+ });
423
+ })
424
+ );
425
+ process.on(
426
+ "unhandledRejection",
427
+ markInternal((reason) => {
428
+ client2.captureException(reason, { level: "error", tags: { mechanism: "unhandledRejection" } }, getScope());
429
+ void client2.flush(2e3);
430
+ })
431
+ );
432
+ process.on(
433
+ "beforeExit",
434
+ markInternal(() => {
435
+ void client2.flush(2e3);
436
+ })
437
+ );
405
438
  return;
406
439
  }
407
440
  if (typeof window !== "undefined") {
@@ -697,12 +730,46 @@ var ManagerApi = class {
697
730
  await sleep(delay);
698
731
  }
699
732
  }
733
+ /**
734
+ * Um único PATCH com timeout curto — exit path (o processo está morrendo, o retry
735
+ * ladder do updateRun é proibido aqui). Status HTTP é ignorado: 409/404 significa
736
+ * que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
737
+ */
738
+ async updateRunOnce(runId, update, timeoutMs = 2e3) {
739
+ await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
740
+ method: "PATCH",
741
+ headers: this.headers(),
742
+ body: JSON.stringify(update),
743
+ signal: AbortSignal.timeout(timeoutMs)
744
+ });
745
+ }
700
746
  };
701
747
 
702
748
  // src/manager/context.ts
749
+ function runCanceledError() {
750
+ return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
751
+ }
752
+ function cancelableSleep(ms, signal) {
753
+ return new Promise((resolve, reject) => {
754
+ if (signal.aborted) {
755
+ reject(runCanceledError());
756
+ return;
757
+ }
758
+ const onAbort = () => {
759
+ clearTimeout(timer);
760
+ reject(runCanceledError());
761
+ };
762
+ const timer = setTimeout(() => {
763
+ signal.removeEventListener("abort", onAbort);
764
+ resolve();
765
+ }, ms);
766
+ signal.addEventListener("abort", onAbort, { once: true });
767
+ });
768
+ }
703
769
  var DEFAULT_LOG_FLUSH_INTERVAL_MS = 2e3;
704
770
  var DEFAULT_CANCEL_CHECK_INTERVAL_MS = 5e3;
705
771
  var MIN_CANCEL_CHECK_INTERVAL_MS = 1e3;
772
+ var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
706
773
  var MAX_LOG_MESSAGE_CHARS = 1e4;
707
774
  var MAX_LOGS_PER_RUN = 500;
708
775
  var MAX_LOGS_PER_PATCH = 100;
@@ -713,15 +780,26 @@ var RunReporter = class {
713
780
  this.runId = runId;
714
781
  this.logger = logger;
715
782
  this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
716
- this.cancelCheckIntervalMs = Math.max(
717
- MIN_CANCEL_CHECK_INTERVAL_MS,
718
- cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS
783
+ this.cancelCheckIntervalMs = Math.min(
784
+ MAX_CANCEL_CHECK_INTERVAL_MS,
785
+ Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
719
786
  );
787
+ const log = {
788
+ debug: (message) => this.log("debug", message),
789
+ info: (message) => this.log("info", message),
790
+ warn: (message) => this.log("warning", message),
791
+ error: (message) => this.log("error", message)
792
+ };
793
+ const signal = this.abortController.signal;
720
794
  this.context = {
721
795
  runId,
722
796
  task,
723
- log: (level, message) => this.log(level, message),
724
- signal: this.abortController.signal
797
+ log,
798
+ signal,
799
+ throwIfCanceled: () => {
800
+ if (signal.aborted) throw runCanceledError();
801
+ },
802
+ sleep: (ms) => cancelableSleep(ms, signal)
725
803
  };
726
804
  }
727
805
  context;
@@ -744,6 +822,17 @@ var RunReporter = class {
744
822
  this.stopCancelWatch();
745
823
  if (!this.abortController.signal.aborted) this.abortController.abort();
746
824
  }
825
+ /**
826
+ * Mata o reporter de vez: aborta o signal e suprime todo reporte futuro.
827
+ * Usado no exit path — depois que o agente reportou AgentTerminated por fora
828
+ * da fila, um handler zumbi não pode reenviar status por cima.
829
+ */
830
+ kill() {
831
+ this.abort();
832
+ this.dead = true;
833
+ this.clearFlushTimer();
834
+ this.buffer = [];
835
+ }
747
836
  log(level, message) {
748
837
  if (this.dead) return;
749
838
  if (this.loggedCount >= MAX_LOGS_PER_RUN) {
@@ -859,6 +948,50 @@ var RunReporter = class {
859
948
  }
860
949
  };
861
950
 
951
+ // src/manager/shutdown.ts
952
+ var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
953
+ function installShutdownHandlers(agent2) {
954
+ if (!isNode()) return () => {
955
+ };
956
+ let signaled = false;
957
+ const signalHandlers = /* @__PURE__ */ new Map();
958
+ for (const signal of WATCHED_SIGNALS) {
959
+ const handler = markInternal(() => {
960
+ if (externalListenerCount(signal) > 0) return;
961
+ if (signaled) return;
962
+ signaled = true;
963
+ const reraise = () => {
964
+ process.removeListener(signal, handler);
965
+ process.kill(process.pid, signal);
966
+ };
967
+ if (agent2.activeCount === 0) {
968
+ reraise();
969
+ return;
970
+ }
971
+ const report = agent2.reportAgentTerminated(`o processo do agente recebeu ${signal} durante a execu\xE7\xE3o`);
972
+ addEmergencyTask(report);
973
+ void report.finally(reraise);
974
+ });
975
+ process.on(signal, handler);
976
+ signalHandlers.set(signal, handler);
977
+ }
978
+ const onFatal = markInternal((error) => {
979
+ 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();
980
+ addEmergencyTask(report);
981
+ if (!hasInstalledGlobalHandlers() && externalListenerCount("uncaughtException") === 0) {
982
+ void report.finally(() => {
983
+ console.error(error);
984
+ process.exit(1);
985
+ });
986
+ }
987
+ });
988
+ process.on("uncaughtException", onFatal);
989
+ return () => {
990
+ for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
991
+ process.removeListener("uncaughtException", onFatal);
992
+ };
993
+ }
994
+
862
995
  // src/manager/agent.ts
863
996
  var DEFAULT_POLL_INTERVAL_MS = 5e3;
864
997
  var MIN_POLL_INTERVAL_MS = 1e3;
@@ -899,6 +1032,7 @@ var ManagerAgent = class {
899
1032
  consecutiveAuthFailures = 0;
900
1033
  activeRuns = /* @__PURE__ */ new Set();
901
1034
  reporters = /* @__PURE__ */ new Set();
1035
+ uninstallShutdown;
902
1036
  get activeCount() {
903
1037
  return this.activeRuns.size;
904
1038
  }
@@ -911,6 +1045,9 @@ var ManagerAgent = class {
911
1045
  throw new Error("[hobots-manager] nenhuma task registrada \u2014 chame register() antes de start()");
912
1046
  }
913
1047
  this.state = "started";
1048
+ if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
1049
+ this.uninstallShutdown = installShutdownHandlers(this);
1050
+ }
914
1051
  this.logger.debug(`polling iniciado (tasks: ${[...this.registry.keys()].join(", ")})`);
915
1052
  this.schedule(0);
916
1053
  }
@@ -927,11 +1064,32 @@ var ManagerAgent = class {
927
1064
  const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : false;
928
1065
  if (!finished && this.activeRuns.size > 0) {
929
1066
  this.logger.warn(`${this.activeRuns.size} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
930
- for (const reporter of this.reporters) reporter.abort();
1067
+ await this.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
931
1068
  }
1069
+ this.uninstallShutdown?.();
1070
+ this.uninstallShutdown = void 0;
932
1071
  this.state = "idle";
933
1072
  return finished;
934
1073
  }
1074
+ /**
1075
+ * Best-effort do exit path: reporta 'failed'/AgentTerminated para as runs ativas
1076
+ * antes do processo morrer, fora da fila do reporter e com timeout curto. Runs já
1077
+ * canceladas no app (reporter morto) são puladas; erros de rede são engolidos —
1078
+ * o sweep server-side é o backstop.
1079
+ */
1080
+ async reportAgentTerminated(message, timeoutMs = 2500) {
1081
+ const pending = [];
1082
+ for (const reporter of this.reporters) {
1083
+ if (reporter.isDead) continue;
1084
+ reporter.kill();
1085
+ pending.push(
1086
+ this.api.updateRunOnce(reporter.context.runId, { status: "failed", error: { type: "AgentTerminated", message } }, timeoutMs).catch(() => {
1087
+ })
1088
+ );
1089
+ }
1090
+ if (pending.length === 0) return;
1091
+ await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
1092
+ }
935
1093
  /** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
936
1094
  schedule(delayMs) {
937
1095
  if (this.state !== "started" || this.timer) return;