hobots 2.3.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.cjs CHANGED
@@ -30,83 +30,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
- Client: () => Client,
34
- DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
35
- Scope: () => Scope,
36
- Step: () => Step,
37
- Transaction: () => Transaction,
38
- addBreadcrumb: () => addBreadcrumb,
39
- captureException: () => captureException,
40
- captureMessage: () => captureMessage,
41
- close: () => close,
42
- flush: () => flush,
43
- getClient: () => getClient,
44
- init: () => init,
45
- register: () => register,
46
- scrubSecrets: () => scrubSecrets,
47
- setContext: () => setContext,
48
- setExtra: () => setExtra,
49
- setTag: () => setTag,
50
- setTags: () => setTags,
51
- setUser: () => setUser,
52
- start: () => start,
53
- startTransaction: () => startTransaction,
54
- stop: () => stop,
55
- withScope: () => withScope
33
+ captureException: () => captureException2,
34
+ close: () => close2,
35
+ default: () => src_default,
36
+ hobots: () => hobots,
37
+ register: () => register2,
38
+ retry: () => retry,
39
+ start: () => start2
56
40
  });
57
41
  module.exports = __toCommonJS(src_exports);
58
42
 
59
- // src/config.ts
60
- var DEFAULT_BASE_URL = "https://api2.hobots.app";
61
- var SECRET_PATTERN = /^hb_[a-f0-9]{64}$/i;
62
- function envBaseUrl() {
63
- if (typeof process === "undefined" || !process.env) return void 0;
64
- return process.env.HOBOTS_API_URL?.trim() || void 0;
65
- }
66
- function isLoopbackHost(hostname2) {
67
- return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
68
- }
69
- var warnedInsecureBaseUrl = false;
70
- function validateBaseUrl(raw, prefix) {
71
- let url;
72
- try {
73
- url = new URL(raw);
74
- } catch {
75
- throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
76
- }
77
- if (url.protocol !== "http:" && url.protocol !== "https:") {
78
- throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
79
- }
80
- if (url.username || url.password) {
81
- throw new Error(`${prefix} HOBOTS_API_URL n\xE3o pode conter credenciais na URL \u2014 a autentica\xE7\xE3o \xE9 s\xF3 pelo Client Secret`);
82
- }
83
- if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
84
- warnedInsecureBaseUrl = true;
85
- console.warn(
86
- `${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
87
- );
88
- }
89
- return raw.replace(/\/+$/, "");
90
- }
91
- function resolveConfig(options, prefix = "[hobots]") {
92
- const key = options.clientSecret?.trim();
93
- if (!key) {
94
- throw new Error(`${prefix} clientSecret ausente \u2014 copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`);
95
- }
96
- if (!SECRET_PATTERN.test(key)) {
97
- throw new Error(
98
- `${prefix} clientSecret com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
99
- );
100
- }
101
- const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
102
- return {
103
- key,
104
- baseUrl,
105
- ingestEndpoint: `${baseUrl}/v1/ingest`,
106
- tasksBaseUrl: `${baseUrl}/v1/tasks`
107
- };
108
- }
109
-
110
43
  // src/stacktrace.ts
111
44
  var MAX_FRAMES = 50;
112
45
  var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
@@ -161,7 +94,7 @@ function scrubSecrets(text) {
161
94
 
162
95
  // src/utils.ts
163
96
  var SDK_NAME = "hobots";
164
- var SDK_VERSION = true ? "2.3.0" : "0.0.0-dev";
97
+ var SDK_VERSION = true ? "3.0.0" : "0.0.0-dev";
165
98
  function isNode() {
166
99
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
167
100
  }
@@ -189,11 +122,28 @@ function unrefTimer(timer) {
189
122
  timer?.unref?.();
190
123
  }
191
124
  function sleep(ms) {
192
- return new Promise((resolve) => {
193
- const timer = setTimeout(resolve, ms);
125
+ return new Promise((resolve2) => {
126
+ const timer = setTimeout(resolve2, ms);
194
127
  unrefTimer(timer);
195
128
  });
196
129
  }
130
+ function abortableSleep(ms, signal) {
131
+ return new Promise((resolve2) => {
132
+ if (signal?.aborted) {
133
+ resolve2();
134
+ return;
135
+ }
136
+ const onAbort = () => {
137
+ clearTimeout(timer);
138
+ resolve2();
139
+ };
140
+ const timer = setTimeout(() => {
141
+ signal?.removeEventListener("abort", onAbort);
142
+ resolve2();
143
+ }, ms);
144
+ signal?.addEventListener("abort", onAbort, { once: true });
145
+ });
146
+ }
197
147
  function jitter(baseMs) {
198
148
  return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
199
149
  }
@@ -314,14 +264,6 @@ function eventFromException(error, type = "error") {
314
264
  }
315
265
  };
316
266
  }
317
- function eventFromMessage(message, type = "warning") {
318
- return {
319
- event_id: uuid4(),
320
- timestamp: nowISO(),
321
- type,
322
- message: scrubSecrets(message)
323
- };
324
- }
325
267
 
326
268
  // src/transport.ts
327
269
  var REQUEST_TIMEOUT_MS = 1e4;
@@ -455,7 +397,7 @@ var Client = class {
455
397
  #config;
456
398
  transport;
457
399
  machine;
458
- constructor(options, config = resolveConfig(options)) {
400
+ constructor(options, config) {
459
401
  this.#options = options;
460
402
  this.debug = !!options.debug;
461
403
  this.logger = makeLogger(this.debug);
@@ -485,43 +427,28 @@ var Client = class {
485
427
  [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
486
428
  return `Client { baseUrl: '${this.#config.baseUrl}' }`;
487
429
  }
488
- captureException(error, hint, scope2) {
489
- return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint, scope2);
430
+ captureException(error, hint) {
431
+ return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
490
432
  }
491
- captureMessage(message, type, hint, scope2) {
492
- return this.captureEvent(eventFromMessage(message, type), hint, scope2);
493
- }
494
- captureEvent(event, hint, scope2) {
495
- const sampleRate = this.options.sampleRate ?? 1;
496
- if (sampleRate < 1 && Math.random() >= sampleRate) {
497
- this.logger.debug("evento descartado por sampleRate");
498
- return event.event_id;
499
- }
500
- const prepared = this.prepareEvent(event, hint, scope2);
501
- const final = this.options.beforeSend ? this.options.beforeSend(prepared) : prepared;
502
- if (!final) {
503
- this.logger.debug("evento descartado por beforeSend");
504
- return event.event_id;
505
- }
433
+ captureEvent(event, hint) {
434
+ const final = this.prepareEvent(event, hint);
506
435
  this.transport.enqueue({ type: "event", payload: final });
507
436
  return final.event_id;
508
437
  }
509
- prepareEvent(event, hint, scope2) {
510
- const tags = { ...this.options.tags, ...scope2.tags, ...event.tags, ...hint?.tags };
511
- const extra = { ...scope2.extra, ...event.extra, ...hint?.extra };
438
+ prepareEvent(event, hint) {
439
+ const tags = { ...this.options.tags, ...event.tags, ...hint?.tags };
440
+ const extra = { ...event.extra, ...hint?.extra };
512
441
  const sampled = this.machine?.sample();
513
442
  const machine = sampled ? { machine: sampled } : {};
514
443
  return {
515
444
  ...event,
516
- type: hint?.type ?? event.type ?? scope2.type ?? "error",
445
+ type: hint?.type ?? event.type ?? "error",
517
446
  release: event.release ?? this.options.release,
518
- environment: event.environment ?? this.options.environment ?? "production",
447
+ environment: this.resolveEnvironment(event.environment),
519
448
  tags: Object.keys(tags).length > 0 ? tags : void 0,
520
449
  extra: Object.keys(extra).length > 0 ? extra : void 0,
521
- user: event.user ?? scope2.user,
522
- contexts: { ...runtimeContexts(), ...machine, ...scope2.contexts, ...event.contexts },
523
- breadcrumbs: scope2.breadcrumbs.length > 0 ? [...scope2.breadcrumbs] : event.breadcrumbs,
524
- fingerprint: hint?.fingerprint ?? event.fingerprint ?? scope2.fingerprint
450
+ contexts: { ...runtimeContexts(), ...machine, ...event.contexts },
451
+ fingerprint: hint?.fingerprint ?? event.fingerprint
525
452
  };
526
453
  }
527
454
  sendTransaction(payload) {
@@ -530,7 +457,7 @@ var Client = class {
530
457
  payload: {
531
458
  ...payload,
532
459
  release: payload.release ?? this.options.release,
533
- environment: payload.environment ?? this.options.environment ?? "production",
460
+ environment: this.resolveEnvironment(payload.environment),
534
461
  tags: { ...this.options.tags, ...payload.tags }
535
462
  }
536
463
  });
@@ -538,9 +465,22 @@ var Client = class {
538
465
  sendHeartbeat(payload) {
539
466
  this.transport.enqueue({
540
467
  type: "heartbeat",
541
- payload: { ...payload, release: payload.release ?? this.options.release }
468
+ payload: {
469
+ ...payload,
470
+ release: payload.release ?? this.options.release,
471
+ // o ambiente viaja no beat: é assim que a instância se classifica no app
472
+ environment: this.resolveEnvironment(payload.environment)
473
+ }
542
474
  });
543
475
  }
476
+ /**
477
+ * Ambiente do item: o dele, senão o das opções, senão produção. Centralizado
478
+ * porque evento, transaction e heartbeat precisam responder igual — o servidor
479
+ * normaliza apelidos legados, então aqui basta repassar.
480
+ */
481
+ resolveEnvironment(value) {
482
+ return value ?? this.options.environment ?? "production";
483
+ }
544
484
  flush(timeoutMs) {
545
485
  return this.transport.flush(timeoutMs);
546
486
  }
@@ -549,23 +489,103 @@ var Client = class {
549
489
  }
550
490
  };
551
491
 
492
+ // src/config.ts
493
+ var DEFAULT_BASE_URL = "https://api2.hobots.app";
494
+ var CLIENT_SECRET_ENV = "HOBOTS_CLIENT_SECRET";
495
+ function envClientSecret() {
496
+ if (typeof process === "undefined" || !process.env) return void 0;
497
+ return process.env[CLIENT_SECRET_ENV]?.trim() || void 0;
498
+ }
499
+ var SECRET_PATTERN = /^hb_[a-f0-9]{64}$/i;
500
+ function envBaseUrl() {
501
+ if (typeof process === "undefined" || !process.env) return void 0;
502
+ return process.env.HOBOTS_API_URL?.trim() || void 0;
503
+ }
504
+ function isLoopbackHost(hostname2) {
505
+ return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
506
+ }
507
+ var warnedInsecureBaseUrl = false;
508
+ function validateBaseUrl(raw, prefix) {
509
+ let url;
510
+ try {
511
+ url = new URL(raw);
512
+ } catch {
513
+ throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
514
+ }
515
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
516
+ throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
517
+ }
518
+ if (url.username || url.password) {
519
+ 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`);
520
+ }
521
+ if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
522
+ warnedInsecureBaseUrl = true;
523
+ console.warn(
524
+ `${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
525
+ );
526
+ }
527
+ return raw.replace(/\/+$/, "");
528
+ }
529
+ function resolveConfig(options = {}, prefix = "[hobots]") {
530
+ const fromOption = options.clientSecret?.trim();
531
+ const key = fromOption || envClientSecret();
532
+ const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
533
+ if (!key) {
534
+ throw new Error(
535
+ `${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`
536
+ );
537
+ }
538
+ if (!SECRET_PATTERN.test(key)) {
539
+ throw new Error(
540
+ `${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`
541
+ );
542
+ }
543
+ const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
544
+ return {
545
+ key,
546
+ baseUrl,
547
+ ingestEndpoint: `${baseUrl}/v1/ingest`,
548
+ tasksBaseUrl: `${baseUrl}/v1/tasks`
549
+ };
550
+ }
551
+
552
+ // src/env.ts
553
+ var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
554
+ function resolveInstanceId(option) {
555
+ const fromOption = option?.trim();
556
+ if (fromOption) return fromOption;
557
+ if (typeof process === "undefined" || !process.env) return void 0;
558
+ return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
559
+ }
560
+ async function fallbackInstanceId() {
561
+ if (!isNode()) return "browser";
562
+ try {
563
+ const os = await import("os");
564
+ return os.hostname() || "unknown-host";
565
+ } catch {
566
+ return "unknown-host";
567
+ }
568
+ }
569
+
552
570
  // src/heartbeat.ts
553
571
  var DEFAULT_INTERVAL_MS = 3e4;
554
572
  var MIN_REPORTED_INTERVAL_MS = 1e3;
555
573
  var MAX_REPORTED_INTERVAL_MS = 9e5;
556
574
  var Heartbeat = class {
557
- constructor(client2, options = {}, instanceId, sampler) {
558
- this.client = client2;
575
+ constructor(client, options = {}, instanceId, sampler) {
576
+ this.client = client;
559
577
  this.sampler = sampler;
560
578
  this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
561
579
  this.instanceId = instanceId;
562
580
  this.name = options.name ?? instanceId;
563
581
  this.metadata = options.metadata;
582
+ this.keepAlive = options.keepAlive ?? false;
564
583
  }
565
584
  intervalMs;
566
585
  instanceId;
567
586
  name;
568
587
  metadata;
588
+ keepAlive;
569
589
  timer;
570
590
  start() {
571
591
  if (this.timer) return;
@@ -581,7 +601,7 @@ var Heartbeat = class {
581
601
  },
582
602
  this.intervalMs * (0.9 + Math.random() * 0.2)
583
603
  );
584
- unrefTimer(this.timer);
604
+ if (!this.keepAlive) unrefTimer(this.timer);
585
605
  }
586
606
  stop() {
587
607
  if (this.timer) {
@@ -627,13 +647,13 @@ var installed = false;
627
647
  function hasInstalledGlobalHandlers() {
628
648
  return installed;
629
649
  }
630
- function installGlobalHandlers(client2, getScope) {
650
+ function installGlobalHandlers(client) {
631
651
  if (installed) return () => void 0;
632
652
  installed = true;
633
653
  if (isNode()) {
634
654
  const onUncaught = markInternal((error) => {
635
- client2.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } }, getScope());
636
- void client2.flush(2e3).then(async () => {
655
+ client.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } });
656
+ void client.flush(2e3).then(async () => {
637
657
  if (externalListenerCount("uncaughtException") === 0) {
638
658
  await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
639
659
  console.error(error);
@@ -642,11 +662,11 @@ function installGlobalHandlers(client2, getScope) {
642
662
  });
643
663
  });
644
664
  const onRejection = markInternal((reason) => {
645
- client2.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } }, getScope());
646
- void client2.flush(2e3);
665
+ client.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } });
666
+ void client.flush(2e3);
647
667
  });
648
668
  const onBeforeExit = markInternal(() => {
649
- void client2.flush(2e3);
669
+ void client.flush(2e3);
650
670
  });
651
671
  process.on("uncaughtException", onUncaught);
652
672
  process.on("unhandledRejection", onRejection);
@@ -660,13 +680,13 @@ function installGlobalHandlers(client2, getScope) {
660
680
  }
661
681
  if (typeof window !== "undefined") {
662
682
  const onError = (event) => {
663
- client2.captureException(event.error ?? event.message, { tags: { mechanism: "onerror" } }, getScope());
683
+ client.captureException(event.error ?? event.message, { tags: { mechanism: "onerror" } });
664
684
  };
665
685
  const onRejection = (event) => {
666
- client2.captureException(event.reason, { tags: { mechanism: "onunhandledrejection" } }, getScope());
686
+ client.captureException(event.reason, { tags: { mechanism: "onunhandledrejection" } });
667
687
  };
668
688
  const onPageHide = () => {
669
- void client2.flush(1e3);
689
+ void client.flush(1e3);
670
690
  };
671
691
  window.addEventListener("error", onError);
672
692
  window.addEventListener("unhandledrejection", onRejection);
@@ -806,349 +826,138 @@ var MachineSampler = class {
806
826
  }
807
827
  };
808
828
 
809
- // src/scope.ts
810
- var Scope = class _Scope {
811
- tags = {};
812
- user;
813
- contexts = {};
814
- extra = {};
815
- type;
816
- fingerprint;
817
- breadcrumbs = [];
818
- setTag(key, value) {
819
- this.tags[key] = value;
820
- return this;
829
+ // src/tasks/api.ts
830
+ var POLL_TIMEOUT_MS = 3e4;
831
+ var UPDATE_TIMEOUT_MS = 1e4;
832
+ var UPLOAD_TIMEOUT_MS = 3e4;
833
+ var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
834
+ var TasksApiError = class extends Error {
835
+ constructor(message, status) {
836
+ super(message);
837
+ this.status = status;
838
+ this.name = "TasksApiError";
821
839
  }
822
- setTags(tags) {
823
- Object.assign(this.tags, tags);
824
- return this;
840
+ };
841
+ var TasksApi = class {
842
+ baseUrl;
843
+ key;
844
+ logger;
845
+ constructor(options) {
846
+ this.baseUrl = options.baseUrl;
847
+ this.key = options.key;
848
+ this.logger = options.logger;
825
849
  }
826
- setUser(user) {
827
- this.user = user ?? void 0;
828
- return this;
850
+ headers() {
851
+ return {
852
+ "content-type": "application/json",
853
+ authorization: `Bearer ${this.key}`
854
+ };
829
855
  }
830
- setContext(name, context) {
831
- if (context === null) {
832
- delete this.contexts[name];
833
- } else {
834
- this.contexts[name] = context;
856
+ /** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
857
+ async poll(request, signal) {
858
+ const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
859
+ let response;
860
+ try {
861
+ response = await fetch(`${this.baseUrl}/poll`, {
862
+ method: "POST",
863
+ headers: this.headers(),
864
+ body: JSON.stringify(request),
865
+ signal: timeoutSignal
866
+ });
867
+ } finally {
868
+ cleanup();
835
869
  }
836
- return this;
837
- }
838
- setExtra(key, value) {
839
- this.extra[key] = value;
840
- return this;
841
- }
842
- setExtras(extras) {
843
- Object.assign(this.extra, extras);
844
- return this;
870
+ if (!response.ok) {
871
+ throw new TasksApiError(`poll falhou (HTTP ${response.status})`, response.status);
872
+ }
873
+ return this.parsePollResponse(await response.json(), request.max ?? 1);
845
874
  }
846
- setType(type) {
847
- this.type = type;
848
- return this;
875
+ /**
876
+ * Abre uma run por conta própria (modo start). Um request só — como no poll, o
877
+ * ladder de espera entre tentativas é decisão do chamador (LocalRunner).
878
+ * 4xx (401 = chave rejeitada) vira TasksApiError com o status.
879
+ */
880
+ async createRun(request, signal) {
881
+ const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
882
+ let response;
883
+ try {
884
+ response = await fetch(`${this.baseUrl}/runs`, {
885
+ method: "POST",
886
+ headers: this.headers(),
887
+ body: JSON.stringify(request),
888
+ signal: timeoutSignal
889
+ });
890
+ } finally {
891
+ cleanup();
892
+ }
893
+ if (!response.ok) {
894
+ throw new TasksApiError(`cria\xE7\xE3o da run falhou (HTTP ${response.status})`, response.status);
895
+ }
896
+ const body = await response.json().catch(() => null);
897
+ if (typeof body?.id !== "string" || typeof body.task !== "string") {
898
+ throw new TasksApiError("resposta inv\xE1lida da cria\xE7\xE3o de run (id/task ausentes)", 0);
899
+ }
900
+ return {
901
+ id: body.id,
902
+ task: body.task,
903
+ params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
904
+ form: null,
905
+ source: typeof body.source === "string" ? body.source : "agent",
906
+ created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
907
+ };
849
908
  }
850
- setFingerprint(fingerprint) {
851
- this.fingerprint = fingerprint;
852
- return this;
909
+ /** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
910
+ parsePollResponse(body, max) {
911
+ const runs = body?.runs;
912
+ if (!Array.isArray(runs)) {
913
+ throw new TasksApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
914
+ }
915
+ const valid = [];
916
+ for (const run of runs.slice(0, Math.max(0, max))) {
917
+ if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
918
+ const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
919
+ valid.push({ ...run, id: run.id, task: run.task, params });
920
+ }
921
+ if (valid.length < runs.length) {
922
+ this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
923
+ }
924
+ return { runs: valid };
853
925
  }
854
- addBreadcrumb(crumb, maxBreadcrumbs) {
855
- this.breadcrumbs.push({ timestamp: nowISO(), ...crumb });
856
- if (this.breadcrumbs.length > maxBreadcrumbs) {
857
- this.breadcrumbs.splice(0, this.breadcrumbs.length - maxBreadcrumbs);
926
+ /**
927
+ * PATCH de status/logs com retry em falha de rede/5xx/429.
928
+ * 4xx é definitivo e vira TasksApiError imediato (409 = run cancelada/transição inválida).
929
+ */
930
+ async updateRun(runId, update) {
931
+ let response;
932
+ try {
933
+ response = await fetchWithRetry(
934
+ () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
935
+ method: "PATCH",
936
+ headers: this.headers(),
937
+ body: JSON.stringify(update),
938
+ signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
939
+ }),
940
+ { logger: this.logger, label: `update da run ${runId}` }
941
+ );
942
+ } catch (error) {
943
+ throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
944
+ }
945
+ if (!response.ok) {
946
+ throw new TasksApiError(`update rejeitado (HTTP ${response.status})`, response.status);
858
947
  }
859
- return this;
860
948
  }
861
- clearBreadcrumbs() {
862
- this.breadcrumbs = [];
863
- return this;
864
- }
865
- clone() {
866
- const next = new _Scope();
867
- next.tags = { ...this.tags };
868
- next.user = this.user ? { ...this.user } : void 0;
869
- next.contexts = Object.fromEntries(Object.entries(this.contexts).map(([k, v]) => [k, { ...v }]));
870
- next.extra = { ...this.extra };
871
- next.type = this.type;
872
- next.fingerprint = this.fingerprint ? [...this.fingerprint] : void 0;
873
- next.breadcrumbs = [...this.breadcrumbs];
874
- return next;
875
- }
876
- };
877
-
878
- // src/transaction.ts
879
- function stepId() {
880
- return uuid4().slice(0, 16);
881
- }
882
- function stepLabel(op, description) {
883
- return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
884
- }
885
- function levelForStatus(status) {
886
- switch (status) {
887
- case "warning":
888
- return "warning";
889
- case "error":
890
- return "error";
891
- case "cancelled":
892
- return "warning";
893
- default:
894
- return "success";
895
- }
896
- }
897
- function colorForStatus(status) {
898
- switch (status) {
899
- case "warning":
900
- return "yellow";
901
- case "error":
902
- return "red";
903
- case "cancelled":
904
- return "gray";
905
- default:
906
- return "green";
907
- }
908
- }
909
- var Step = class {
910
- constructor(transaction, parentStepId, op, description) {
911
- this.transaction = transaction;
912
- this.parentStepId = parentStepId;
913
- this.op = op;
914
- this.description = description;
915
- }
916
- stepId = stepId();
917
- startTimestamp = nowISO();
918
- status = "ok";
919
- endTimestamp;
920
- data;
921
- setStatus(status) {
922
- this.status = status;
923
- return this;
924
- }
925
- setData(key, value) {
926
- this.data = { ...this.data, [key]: value };
927
- return this;
928
- }
929
- /** Step aninhada (filha desta step). */
930
- startChild(op, description) {
931
- return this.transaction.createStep(this.stepId, op, description);
932
- }
933
- finish() {
934
- if (this.endTimestamp) return;
935
- this.endTimestamp = nowISO();
936
- this.transaction.emit(
937
- "step",
938
- "finish",
939
- stepLabel(this.op, this.description),
940
- this.startTimestamp,
941
- this.endTimestamp,
942
- this.status
943
- );
944
- }
945
- toJSON() {
946
- return {
947
- step_id: this.stepId,
948
- parent_step_id: this.parentStepId,
949
- op: this.op,
950
- description: this.description,
951
- status: this.status,
952
- start_timestamp: this.startTimestamp,
953
- timestamp: this.endTimestamp ?? nowISO(),
954
- data: this.data
955
- };
956
- }
957
- };
958
- var Transaction = class {
959
- constructor(client2, name, op, sampled = true, runId) {
960
- this.client = client2;
961
- this.name = name;
962
- this.op = op;
963
- this.sampled = sampled;
964
- this.runId = runId;
965
- this.emit("transaction", "start", op ? `${name} (${op})` : name);
966
- }
967
- eventId = uuid4();
968
- stepId = stepId();
969
- startTimestamp = nowISO();
970
- status = "ok";
971
- steps = [];
972
- tags = {};
973
- finished = false;
974
- lifecycleSink;
975
- // eventos emitidos antes do sink existir (o START da transaction sai no construtor,
976
- // antes do RunReporter conectar) — replay no setLifecycleSink
977
- lifecycleBacklog = [];
978
- /**
979
- * Registra o início/fim de uma step ou transaction: espelha a linha no sink de
980
- * lifecycle, se conectado (é assim que os eventos de performance aparecem nos
981
- * logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
982
- * escreve no stdout do processo do usuário sem permissão.
983
- */
984
- emit(kind, phase, label, startTs, endTs, status) {
985
- const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
986
- const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
987
- if (this.client?.debug) {
988
- const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
989
- console.log(`${tag} ${label}${suffix}`);
990
- }
991
- const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
992
- const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
993
- if (this.lifecycleSink) this.lifecycleSink(level, message);
994
- else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
995
- }
996
- /** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
997
- setLifecycleSink(sink) {
998
- this.lifecycleSink = sink;
999
- const backlog = this.lifecycleBacklog;
1000
- this.lifecycleBacklog = [];
1001
- for (const [level, message] of backlog) sink(level, message);
1002
- }
1003
- createStep(parentStepId, op, description) {
1004
- const step = new Step(this, parentStepId, op, description);
1005
- this.steps.push(step);
1006
- this.emit("step", "start", stepLabel(op, description));
1007
- return step;
1008
- }
1009
- /** Step direta da transaction. */
1010
- startChild(op, description) {
1011
- return this.createStep(this.stepId, op, description);
1012
- }
1013
- setTag(key, value) {
1014
- this.tags[key] = value;
1015
- return this;
1016
- }
1017
- setStatus(status) {
1018
- this.status = status;
1019
- return this;
1020
- }
1021
- setName(name) {
1022
- this.name = name;
1023
- return this;
1024
- }
1025
- finish() {
1026
- if (this.finished) return;
1027
- this.finished = true;
1028
- for (const step of this.steps) step.finish();
1029
- const endTimestamp = nowISO();
1030
- this.emit(
1031
- "transaction",
1032
- "finish",
1033
- this.op ? `${this.name} (${this.op})` : this.name,
1034
- this.startTimestamp,
1035
- endTimestamp,
1036
- this.status
1037
- );
1038
- if (!this.sampled || !this.client) return;
1039
- const payload = {
1040
- event_id: this.eventId,
1041
- name: this.name,
1042
- op: this.op,
1043
- status: this.status,
1044
- start_timestamp: this.startTimestamp,
1045
- timestamp: endTimestamp,
1046
- steps: this.steps.map((step) => step.toJSON()),
1047
- tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
1048
- run_id: this.runId
1049
- };
1050
- this.client.sendTransaction(payload);
1051
- }
1052
- };
1053
-
1054
- // src/tasks/api.ts
1055
- var POLL_TIMEOUT_MS = 3e4;
1056
- var UPDATE_TIMEOUT_MS = 1e4;
1057
- var UPLOAD_TIMEOUT_MS = 3e4;
1058
- var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
1059
- var TasksApiError = class extends Error {
1060
- constructor(message, status) {
1061
- super(message);
1062
- this.status = status;
1063
- this.name = "TasksApiError";
1064
- }
1065
- };
1066
- var TasksApi = class {
1067
- baseUrl;
1068
- key;
1069
- logger;
1070
- constructor(options) {
1071
- this.baseUrl = options.baseUrl;
1072
- this.key = options.key;
1073
- this.logger = options.logger;
1074
- }
1075
- headers() {
1076
- return {
1077
- "content-type": "application/json",
1078
- authorization: `Bearer ${this.key}`
1079
- };
1080
- }
1081
- /** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
1082
- async poll(request, signal) {
1083
- const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
1084
- let response;
1085
- try {
1086
- response = await fetch(`${this.baseUrl}/poll`, {
1087
- method: "POST",
1088
- headers: this.headers(),
1089
- body: JSON.stringify(request),
1090
- signal: timeoutSignal
1091
- });
1092
- } finally {
1093
- cleanup();
1094
- }
1095
- if (!response.ok) {
1096
- throw new TasksApiError(`poll falhou (HTTP ${response.status})`, response.status);
1097
- }
1098
- return this.parsePollResponse(await response.json(), request.max ?? 1);
1099
- }
1100
- /** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
1101
- parsePollResponse(body, max) {
1102
- const runs = body?.runs;
1103
- if (!Array.isArray(runs)) {
1104
- throw new TasksApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
1105
- }
1106
- const valid = [];
1107
- for (const run of runs.slice(0, Math.max(0, max))) {
1108
- if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
1109
- const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
1110
- valid.push({ ...run, id: run.id, task: run.task, params });
1111
- }
1112
- if (valid.length < runs.length) {
1113
- this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
1114
- }
1115
- return { runs: valid };
1116
- }
1117
- /**
1118
- * PATCH de status/logs com retry em falha de rede/5xx/429.
1119
- * 4xx é definitivo e vira TasksApiError imediato (409 = run cancelada/transição inválida).
1120
- */
1121
- async updateRun(runId, update) {
1122
- let response;
1123
- try {
1124
- response = await fetchWithRetry(
1125
- () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
1126
- method: "PATCH",
1127
- headers: this.headers(),
1128
- body: JSON.stringify(update),
1129
- signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
1130
- }),
1131
- { logger: this.logger, label: `update da run ${runId}` }
1132
- );
1133
- } catch (error) {
1134
- throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
1135
- }
1136
- if (!response.ok) {
1137
- throw new TasksApiError(`update rejeitado (HTTP ${response.status})`, response.status);
1138
- }
1139
- }
1140
- /**
1141
- * Um único PATCH com timeout curto — exit path (o processo está morrendo, o retry
1142
- * ladder do updateRun é proibido aqui). Status HTTP é ignorado: 409/404 significa
1143
- * que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
1144
- */
1145
- async updateRunOnce(runId, update, timeoutMs = 2e3) {
1146
- await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
1147
- method: "PATCH",
1148
- headers: this.headers(),
1149
- body: JSON.stringify(update),
1150
- signal: AbortSignal.timeout(timeoutMs)
1151
- });
949
+ /**
950
+ * Um único PATCH com timeout curto — exit path (o processo está morrendo, o retry
951
+ * ladder do updateRun é proibido aqui). Status HTTP é ignorado: 409/404 significa
952
+ * que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
953
+ */
954
+ async updateRunOnce(runId, update, timeoutMs = 2e3) {
955
+ await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
956
+ method: "PATCH",
957
+ headers: this.headers(),
958
+ body: JSON.stringify(update),
959
+ signal: AbortSignal.timeout(timeoutMs)
960
+ });
1152
961
  }
1153
962
  /**
1154
963
  * Envia um lote de itens de execução. Retry em falha de rede/5xx/429-transporte;
@@ -1251,13 +1060,83 @@ var TasksApi = class {
1251
1060
  body: JSON.stringify(request),
1252
1061
  signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
1253
1062
  }
1254
- );
1255
- if (!response.ok) {
1256
- throw new TasksApiError(`confirma\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
1063
+ );
1064
+ if (!response.ok) {
1065
+ throw new TasksApiError(`confirma\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
1066
+ }
1067
+ }
1068
+ };
1069
+
1070
+ // src/tasks/backoff.ts
1071
+ var RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
1072
+ var AUTH_FAILURE_INTERVAL_MS = 3e4;
1073
+ var Backoff = class {
1074
+ constructor(logger, label, floorMs = 0) {
1075
+ this.logger = logger;
1076
+ this.label = label;
1077
+ this.floorMs = floorMs;
1078
+ }
1079
+ failures = 0;
1080
+ authFailures = 0;
1081
+ reset() {
1082
+ this.failures = 0;
1083
+ this.authFailures = 0;
1084
+ }
1085
+ next(error) {
1086
+ if (error instanceof TasksApiError && error.status === 401) {
1087
+ this.authFailures++;
1088
+ if (this.authFailures === 1 || this.authFailures % 10 === 0) {
1089
+ this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes");
1090
+ }
1091
+ return Math.max(this.floorMs, AUTH_FAILURE_INTERVAL_MS);
1257
1092
  }
1093
+ this.failures++;
1094
+ const ladder = RETRY_DELAYS_MS[Math.min(this.failures - 1, RETRY_DELAYS_MS.length - 1)];
1095
+ const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.floorMs, 16e3));
1096
+ this.logger.warn(`${this.label} falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
1097
+ return delay;
1258
1098
  }
1259
1099
  };
1260
1100
 
1101
+ // src/tasks/errors.ts
1102
+ function runCanceledError() {
1103
+ return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
1104
+ }
1105
+ function taskTimeoutError(timeoutMs) {
1106
+ return Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
1107
+ name: "TaskTimeout"
1108
+ });
1109
+ }
1110
+ var TERMINAL_ERROR_NAMES = /* @__PURE__ */ new Set(["RunCanceled", "AbortError", "TaskTimeout"]);
1111
+ function isTerminalError(error) {
1112
+ return error instanceof Error && TERMINAL_ERROR_NAMES.has(error.name);
1113
+ }
1114
+
1115
+ // src/retry.ts
1116
+ var DEFAULT_RETRIES = 3;
1117
+ var DEFAULT_DELAY_MS = 1e3;
1118
+ function resolveDelay(delay, attempt, fallbackMs) {
1119
+ const value = typeof delay === "function" ? delay(attempt) : delay ?? fallbackMs;
1120
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1121
+ }
1122
+ async function retryWithBackoff(fn, options = {}) {
1123
+ const retries = Math.max(0, Math.floor(options.retries ?? DEFAULT_RETRIES));
1124
+ const wait = options.wait ?? sleep;
1125
+ for (let attempt = 1; ; attempt++) {
1126
+ if (options.signal?.aborted) throw runCanceledError();
1127
+ try {
1128
+ return await fn(attempt);
1129
+ } catch (error) {
1130
+ const exhausted = attempt > retries;
1131
+ const retriable = !isTerminalError(error) && !options.signal?.aborted && (options.shouldRetry?.(error) ?? true);
1132
+ if (exhausted || !retriable) throw error;
1133
+ const delay = resolveDelay(options.delayMs, attempt, DEFAULT_DELAY_MS);
1134
+ options.onRetry?.(error, attempt, delay);
1135
+ await wait(delay);
1136
+ }
1137
+ }
1138
+ }
1139
+
1261
1140
  // src/tasks/attachments.ts
1262
1141
  var MAX_ATTACHMENT_BYTES = 5242880;
1263
1142
  var MAX_ATTACHMENTS_PER_RUN = 20;
@@ -1309,14 +1188,11 @@ var LOG_LEVEL_COLOR = {
1309
1188
  };
1310
1189
  function printRunLog(task, level, message) {
1311
1190
  const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
1312
- const method = level === "warning" ? "warn" : level === "success" ? "info" : level;
1313
- console[method](line, message);
1314
- }
1315
- function runCanceledError() {
1316
- return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
1191
+ const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
1192
+ console[method2](line, message);
1317
1193
  }
1318
1194
  function cancelableSleep(ms, signal) {
1319
- return new Promise((resolve, reject) => {
1195
+ return new Promise((resolve2, reject) => {
1320
1196
  if (signal.aborted) {
1321
1197
  reject(runCanceledError());
1322
1198
  return;
@@ -1327,7 +1203,7 @@ function cancelableSleep(ms, signal) {
1327
1203
  };
1328
1204
  const timer = setTimeout(() => {
1329
1205
  signal.removeEventListener("abort", onAbort);
1330
- resolve();
1206
+ resolve2();
1331
1207
  }, ms);
1332
1208
  signal.addEventListener("abort", onAbort, { once: true });
1333
1209
  });
@@ -1357,7 +1233,7 @@ function scrubPayloadValue(value, depth) {
1357
1233
  return out;
1358
1234
  }
1359
1235
  var RunReporter = class {
1360
- constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs) {
1236
+ constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
1361
1237
  this.api = api;
1362
1238
  this.runId = runId;
1363
1239
  this.logger = logger;
@@ -1374,17 +1250,40 @@ var RunReporter = class {
1374
1250
  success: (message) => this.log("success", message)
1375
1251
  };
1376
1252
  tx.setLifecycleSink((level, message) => this.push(level, message));
1253
+ const checkCanceled = () => this.checkCanceled();
1254
+ tx.setStepGuard(checkCanceled);
1377
1255
  const items = {
1378
- add: (item) => this.addItem(item.status, item.message, item.payload, item.id),
1379
- succeeded: (payload, options) => this.addItem("succeeded", void 0, payload, options?.id),
1380
- failed: (message, payload, options) => this.addItem("failed", message, payload, options?.id),
1381
- occurrence: (message, payload, options) => this.addItem("occurrence", message, payload, options?.id)
1256
+ add: (item) => {
1257
+ checkCanceled();
1258
+ this.addItem(item.status, item.message, item.payload, item.id);
1259
+ },
1260
+ succeeded: (payload, options) => {
1261
+ checkCanceled();
1262
+ this.addItem("succeeded", void 0, payload, options?.id);
1263
+ },
1264
+ failed: (message, payload, options) => {
1265
+ checkCanceled();
1266
+ this.addItem("failed", message, payload, options?.id);
1267
+ },
1268
+ occurrence: (message, payload, options) => {
1269
+ checkCanceled();
1270
+ this.addItem("occurrence", message, payload, options?.id);
1271
+ }
1382
1272
  };
1383
1273
  const progress = Object.defineProperties(
1384
1274
  {
1385
- total: (total) => this.setProgress(this.progressCurrent, total),
1386
- advance: (count = 1) => this.setProgress(this.progressCurrent + count, this.progressTotal),
1387
- set: (current, total) => this.setProgress(current, total ?? this.progressTotal)
1275
+ total: (total) => {
1276
+ checkCanceled();
1277
+ this.setProgress(this.progressCurrent, total);
1278
+ },
1279
+ advance: (count = 1) => {
1280
+ checkCanceled();
1281
+ this.setProgress(this.progressCurrent + count, this.progressTotal);
1282
+ },
1283
+ set: (current, total) => {
1284
+ checkCanceled();
1285
+ this.setProgress(current, total ?? this.progressTotal);
1286
+ }
1388
1287
  },
1389
1288
  {
1390
1289
  current: { enumerable: true, get: () => this.progressCurrent },
@@ -1396,18 +1295,20 @@ var RunReporter = class {
1396
1295
  // defineProperties não tipa os getters adicionados — a interface é a fonte de verdade
1397
1296
  );
1398
1297
  const signal = this.abortController.signal;
1298
+ const sleep2 = (ms) => cancelableSleep(ms, signal);
1399
1299
  this.context = {
1400
1300
  runId,
1401
1301
  task,
1302
+ params,
1402
1303
  log,
1403
1304
  items,
1404
1305
  progress,
1405
- tx,
1306
+ transaction: tx,
1406
1307
  signal,
1407
- throwIfCanceled: () => {
1408
- if (signal.aborted) throw runCanceledError();
1409
- },
1410
- sleep: (ms) => cancelableSleep(ms, signal)
1308
+ throwIfCanceled: checkCanceled,
1309
+ sleep: sleep2,
1310
+ attach: (image, options) => this.attach(image, options),
1311
+ retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
1411
1312
  };
1412
1313
  }
1413
1314
  context;
@@ -1456,6 +1357,10 @@ var RunReporter = class {
1456
1357
  get isDead() {
1457
1358
  return this.dead;
1458
1359
  }
1360
+ /** Lança `RunCanceled` se o signal desta run já disparou. */
1361
+ checkCanceled() {
1362
+ if (this.abortController.signal.aborted) throw runCanceledError();
1363
+ }
1459
1364
  /** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
1460
1365
  abort() {
1461
1366
  this.stopCancelWatch();
@@ -1734,13 +1639,13 @@ var RunReporter = class {
1734
1639
  this.flushItems();
1735
1640
  await Promise.race([
1736
1641
  this.itemsChain,
1737
- new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1642
+ new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
1738
1643
  ]);
1739
1644
  if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
1740
1645
  this.flushItems();
1741
1646
  await Promise.race([
1742
1647
  this.itemsChain,
1743
- new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1648
+ new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
1744
1649
  ]);
1745
1650
  }
1746
1651
  }
@@ -1748,8 +1653,8 @@ var RunReporter = class {
1748
1653
  /**
1749
1654
  * Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
1750
1655
  * o upload em segundo plano. Nunca lança: todo caminho de erro vira `{ ok: false }`
1751
- * com um aviso no log da run. Chega ao handler pelos capture* do SDK — o hint
1752
- * `attachment` de Hobots.captureException/captureMessage cai aqui via run-context-store.
1656
+ * com um aviso no log da run. É o `hobots.attach()`; o hint `attachment` de
1657
+ * `hobots.captureException` também cai aqui via run-context-store.
1753
1658
  */
1754
1659
  async attach(image, options = {}) {
1755
1660
  const id = uuid4();
@@ -1771,8 +1676,8 @@ var RunReporter = class {
1771
1676
  printRunLog(this.context.task, level, `${scrubbed} [imagem]`);
1772
1677
  const logTs = this.push(level, scrubbed, true, [id]);
1773
1678
  let settle;
1774
- const result = new Promise((resolve) => {
1775
- settle = resolve;
1679
+ const result = new Promise((resolve2) => {
1680
+ settle = resolve2;
1776
1681
  });
1777
1682
  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" }));
1778
1683
  this.pendingAttachments.add(work);
@@ -1854,8 +1759,8 @@ var RunReporter = class {
1854
1759
  this.uploadSlots--;
1855
1760
  return Promise.resolve(release);
1856
1761
  }
1857
- return new Promise((resolve) => {
1858
- this.uploadQueue.push(() => resolve(release));
1762
+ return new Promise((resolve2) => {
1763
+ this.uploadQueue.push(() => resolve2(release));
1859
1764
  });
1860
1765
  }
1861
1766
  /**
@@ -1866,7 +1771,7 @@ var RunReporter = class {
1866
1771
  if (this.pendingAttachments.size === 0) return;
1867
1772
  await Promise.race([
1868
1773
  Promise.allSettled([...this.pendingAttachments]),
1869
- new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
1774
+ new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
1870
1775
  ]);
1871
1776
  }
1872
1777
  enqueue(updateIn) {
@@ -1937,119 +1842,354 @@ async function runWithRunContext(store, fn) {
1937
1842
  await ensureStorage();
1938
1843
  return storage ? storage.run(store, fn) : fn();
1939
1844
  }
1940
- function currentRunTransaction() {
1941
- return storage?.getStore()?.tx;
1845
+ function currentRunReporter() {
1846
+ return storage?.getStore()?.reporter;
1847
+ }
1848
+
1849
+ // src/transaction.ts
1850
+ function stepId() {
1851
+ return uuid4().slice(0, 16);
1852
+ }
1853
+ function stepLabel(op, description) {
1854
+ return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
1855
+ }
1856
+ function levelForStatus(status) {
1857
+ switch (status) {
1858
+ case "warning":
1859
+ return "warning";
1860
+ case "error":
1861
+ return "error";
1862
+ case "cancelled":
1863
+ return "warning";
1864
+ default:
1865
+ return "success";
1866
+ }
1942
1867
  }
1943
- function currentRunAttach() {
1944
- return storage?.getStore()?.attach;
1868
+ function colorForStatus(status) {
1869
+ switch (status) {
1870
+ case "warning":
1871
+ return "yellow";
1872
+ case "error":
1873
+ return "red";
1874
+ case "cancelled":
1875
+ return "gray";
1876
+ default:
1877
+ return "green";
1878
+ }
1945
1879
  }
1880
+ var Step = class {
1881
+ constructor(transaction, parentStepId, op, description) {
1882
+ this.transaction = transaction;
1883
+ this.parentStepId = parentStepId;
1884
+ this.op = op;
1885
+ this.description = description;
1886
+ }
1887
+ stepId = stepId();
1888
+ startTimestamp = nowISO();
1889
+ status = "ok";
1890
+ endTimestamp;
1891
+ data;
1892
+ setStatus(status) {
1893
+ this.status = status;
1894
+ return this;
1895
+ }
1896
+ setData(key, value) {
1897
+ this.data = { ...this.data, [key]: value };
1898
+ return this;
1899
+ }
1900
+ /** Step aninhada (filha desta step). */
1901
+ startChild(op, description) {
1902
+ return this.transaction.createStep(this.stepId, op, description);
1903
+ }
1904
+ finish() {
1905
+ if (this.endTimestamp) return;
1906
+ this.endTimestamp = nowISO();
1907
+ this.transaction.emit(
1908
+ "step",
1909
+ "finish",
1910
+ stepLabel(this.op, this.description),
1911
+ this.startTimestamp,
1912
+ this.endTimestamp,
1913
+ this.status
1914
+ );
1915
+ }
1916
+ toJSON() {
1917
+ return {
1918
+ step_id: this.stepId,
1919
+ parent_step_id: this.parentStepId,
1920
+ op: this.op,
1921
+ description: this.description,
1922
+ status: this.status,
1923
+ start_timestamp: this.startTimestamp,
1924
+ timestamp: this.endTimestamp ?? nowISO(),
1925
+ data: this.data
1926
+ };
1927
+ }
1928
+ };
1929
+ var Transaction = class {
1930
+ constructor(client, name, op, runId) {
1931
+ this.client = client;
1932
+ this.name = name;
1933
+ this.op = op;
1934
+ this.runId = runId;
1935
+ this.emit("transaction", "start", op ? `${name} (${op})` : name);
1936
+ }
1937
+ eventId = uuid4();
1938
+ stepId = stepId();
1939
+ startTimestamp = nowISO();
1940
+ status = "ok";
1941
+ steps = [];
1942
+ tags = {};
1943
+ finished = false;
1944
+ lifecycleSink;
1945
+ // instalado pelo RunReporter: abrir uma step é um ponto de parada cooperativo (lança RunCanceled)
1946
+ stepGuard;
1947
+ // eventos emitidos antes do sink existir (o START da transaction sai no construtor,
1948
+ // antes do RunReporter conectar) — replay no setLifecycleSink
1949
+ lifecycleBacklog = [];
1950
+ /**
1951
+ * Registra o início/fim de uma step ou transaction: espelha a linha no sink de
1952
+ * lifecycle, se conectado (é assim que os eventos de performance aparecem nos
1953
+ * logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
1954
+ * escreve no stdout do processo do usuário sem permissão.
1955
+ */
1956
+ emit(kind, phase, label, startTs, endTs, status) {
1957
+ const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
1958
+ const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
1959
+ if (this.client?.debug) {
1960
+ const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
1961
+ console.log(`${tag} ${label}${suffix}`);
1962
+ }
1963
+ const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
1964
+ const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
1965
+ if (this.lifecycleSink) this.lifecycleSink(level, message);
1966
+ else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
1967
+ }
1968
+ /** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
1969
+ setLifecycleSink(sink) {
1970
+ this.lifecycleSink = sink;
1971
+ const backlog = this.lifecycleBacklog;
1972
+ this.lifecycleBacklog = [];
1973
+ for (const [level, message] of backlog) sink(level, message);
1974
+ }
1975
+ /**
1976
+ * Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
1977
+ * `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
1978
+ */
1979
+ setStepGuard(guard) {
1980
+ this.stepGuard = guard;
1981
+ }
1982
+ createStep(parentStepId, op, description) {
1983
+ this.stepGuard?.();
1984
+ const step = new Step(this, parentStepId, op, description);
1985
+ this.steps.push(step);
1986
+ this.emit("step", "start", stepLabel(op, description));
1987
+ return step;
1988
+ }
1989
+ /** Step direta da transaction. */
1990
+ startChild(op, description) {
1991
+ return this.createStep(this.stepId, op, description);
1992
+ }
1993
+ setTag(key, value) {
1994
+ this.tags[key] = value;
1995
+ return this;
1996
+ }
1997
+ setStatus(status) {
1998
+ this.status = status;
1999
+ return this;
2000
+ }
2001
+ setName(name) {
2002
+ this.name = name;
2003
+ return this;
2004
+ }
2005
+ finish() {
2006
+ if (this.finished) return;
2007
+ this.finished = true;
2008
+ for (const step of this.steps) step.finish();
2009
+ const endTimestamp = nowISO();
2010
+ this.emit(
2011
+ "transaction",
2012
+ "finish",
2013
+ this.op ? `${this.name} (${this.op})` : this.name,
2014
+ this.startTimestamp,
2015
+ endTimestamp,
2016
+ this.status
2017
+ );
2018
+ if (!this.client) return;
2019
+ const payload = {
2020
+ event_id: this.eventId,
2021
+ name: this.name,
2022
+ op: this.op,
2023
+ status: this.status,
2024
+ start_timestamp: this.startTimestamp,
2025
+ timestamp: endTimestamp,
2026
+ steps: this.steps.map((step) => step.toJSON()),
2027
+ tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
2028
+ run_id: this.runId
2029
+ };
2030
+ this.client.sendTransaction(payload);
2031
+ }
2032
+ };
1946
2033
 
1947
- // src/tasks/shutdown.ts
1948
- var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
1949
- function installShutdownHandlers(agent2) {
1950
- if (!isNode()) return () => {
1951
- };
1952
- let signaled = false;
1953
- const signalHandlers = /* @__PURE__ */ new Map();
1954
- for (const signal of WATCHED_SIGNALS) {
1955
- const handler = markInternal(() => {
1956
- if (externalListenerCount(signal) > 0) return;
1957
- if (signaled) return;
1958
- signaled = true;
1959
- const reraise = () => {
1960
- process.removeListener(signal, handler);
1961
- process.kill(process.pid, signal);
1962
- };
1963
- if (agent2.activeCount === 0) {
1964
- reraise();
1965
- return;
1966
- }
1967
- const report = agent2.reportAgentTerminated(`o processo do agente recebeu ${signal} durante a execu\xE7\xE3o`);
1968
- addEmergencyTask(report);
1969
- void report.finally(reraise);
1970
- });
1971
- process.on(signal, handler);
1972
- signalHandlers.set(signal, handler);
2034
+ // src/tasks/runner.ts
2035
+ var DEFAULT_RETRY_DELAY_MS = 5e3;
2036
+ async function executeRun(input) {
2037
+ const { run, task, api, client, logger, tracker } = input;
2038
+ const options = task.options;
2039
+ const tx = new Transaction(client, `run:${run.task}`, "hobots.task", run.id);
2040
+ const reporter = new RunReporter(
2041
+ api,
2042
+ run.id,
2043
+ run.task,
2044
+ logger,
2045
+ tx,
2046
+ options.logFlushIntervalMs,
2047
+ options.cancelCheckIntervalMs,
2048
+ run.params
2049
+ );
2050
+ tracker.add(reporter);
2051
+ try {
2052
+ const ok = await reporter.reportRunning();
2053
+ if (!ok) {
2054
+ logger.debug(`run ${run.id} cancelada antes de iniciar \u2014 handler n\xE3o executado`);
2055
+ tx.setStatus("cancelled");
2056
+ tx.finish();
2057
+ return;
2058
+ }
2059
+ logger.debug(`executando run ${run.id} (${run.task})`);
2060
+ try {
2061
+ await runWithRunContext({ reporter }, () => invokeWithRetry(task, run, reporter, logger));
2062
+ await reporter.finalizeAttachments();
2063
+ await reporter.finalizeItems();
2064
+ tx.setStatus("ok");
2065
+ tx.finish();
2066
+ reporter.reportSuccess();
2067
+ logger.debug(`run ${run.id} conclu\xEDda`);
2068
+ } catch (error) {
2069
+ await reporter.finalizeAttachments();
2070
+ await reporter.finalizeItems();
2071
+ tx.setStatus("error");
2072
+ tx.finish();
2073
+ reporter.reportFailure(toRunError(error));
2074
+ logger.debug(`run ${run.id} falhou: ${String(error)}`);
2075
+ }
2076
+ await reporter.finalize();
2077
+ } finally {
2078
+ tracker.remove(reporter);
2079
+ }
2080
+ }
2081
+ function invokeWithRetry(task, run, reporter, logger) {
2082
+ const { retries, retryDelayMs } = task.options;
2083
+ const total = retries + 1;
2084
+ const ctx = reporter.context;
2085
+ return retryWithBackoff(() => invokeWithTimeout(task, run, reporter, logger), {
2086
+ retries,
2087
+ delayMs: (attempt) => resolveDelay(retryDelayMs, attempt, DEFAULT_RETRY_DELAY_MS),
2088
+ shouldRetry: () => !reporter.isDead,
2089
+ signal: ctx.signal,
2090
+ wait: (ms) => ctx.sleep(ms),
2091
+ onRetry: (error, attempt, delayMs) => {
2092
+ const reason = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2093
+ ctx.log.warn(`tentativa ${attempt}/${total} falhou: ${truncate(reason, 500)} \u2014 nova tentativa em ${delayMs}ms`);
2094
+ }
2095
+ });
2096
+ }
2097
+ var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
2098
+ var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
2099
+ async function invokeWithTimeout(task, run, reporter, logger) {
2100
+ const { timeoutMs, cancelGraceMs } = task.options;
2101
+ const handler = task.handler;
2102
+ const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
2103
+ const races = [];
2104
+ if (timeoutMs && timeoutMs > 0) {
2105
+ races.push(sleep(timeoutMs).then(() => TIMEOUT_SENTINEL));
2106
+ }
2107
+ if (cancelGraceMs > 0) {
2108
+ races.push(waitForCancelGrace(reporter.context.signal, cancelGraceMs).then(() => CANCEL_SENTINEL));
2109
+ }
2110
+ if (races.length === 0) {
2111
+ await invocation;
2112
+ return;
1973
2113
  }
1974
- const onFatal = markInternal((error) => {
1975
- 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();
1976
- addEmergencyTask(report);
1977
- if (!hasInstalledGlobalHandlers() && externalListenerCount("uncaughtException") === 0) {
1978
- void report.finally(() => {
1979
- console.error(error);
1980
- process.exit(1);
1981
- });
2114
+ const outcome = await Promise.race([invocation, ...races]);
2115
+ if (outcome !== TIMEOUT_SENTINEL && outcome !== CANCEL_SENTINEL) return;
2116
+ if (outcome === CANCEL_SENTINEL) {
2117
+ logger.warn(
2118
+ `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)`
2119
+ );
2120
+ return;
2121
+ }
2122
+ reporter.abort();
2123
+ 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)`));
2124
+ throw taskTimeoutError(timeoutMs);
2125
+ }
2126
+ function waitForCancelGrace(signal, graceMs) {
2127
+ return new Promise((resolve2) => {
2128
+ if (signal.aborted) {
2129
+ setTimeout(resolve2, graceMs);
2130
+ return;
1982
2131
  }
2132
+ signal.addEventListener("abort", () => setTimeout(resolve2, graceMs), { once: true });
1983
2133
  });
1984
- process.on("uncaughtException", onFatal);
1985
- return () => {
1986
- for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
1987
- process.removeListener("uncaughtException", onFatal);
1988
- };
2134
+ }
2135
+ var MAX_ERROR_MESSAGE_CHARS = 1e4;
2136
+ var MAX_ERROR_STACK_CHARS = 2e4;
2137
+ function toRunError(error) {
2138
+ if (error instanceof Error) {
2139
+ return {
2140
+ type: error.name,
2141
+ message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
2142
+ stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
2143
+ };
2144
+ }
2145
+ return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
1989
2146
  }
1990
2147
 
1991
2148
  // src/tasks/agent.ts
1992
- var DEFAULT_POLL_INTERVAL_MS = 5e3;
2149
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
1993
2150
  var MIN_POLL_INTERVAL_MS = 1e3;
1994
- var MAX_CONCURRENCY = 5;
1995
- var POLL_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
1996
- var AUTH_FAILURE_INTERVAL_MS = 3e4;
1997
- var DEFAULT_CANCEL_GRACE_MS = 1e4;
1998
- function hostname() {
1999
- if (!isNode()) return void 0;
2000
- return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
2001
- }
2151
+ var MAX_CLAIM_PER_POLL = 5;
2002
2152
  var TaskAgent = class {
2003
- constructor(options, registry2) {
2153
+ constructor(options) {
2004
2154
  this.options = options;
2005
- this.registry = registry2;
2006
- this.logger = makeLogger(options.debug ?? false);
2007
- const config = options.config;
2008
- this.api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger: this.logger });
2155
+ this.logger = options.logger;
2156
+ this.api = options.api;
2009
2157
  this.client = options.client;
2158
+ this.registry = options.registry;
2159
+ this.tracker = options.tracker;
2010
2160
  this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
2011
- this.concurrency = Math.min(MAX_CONCURRENCY, Math.max(1, options.concurrency ?? 1));
2012
- this.instanceId = options.instanceId;
2013
- this.taskTimeoutMs = options.taskTimeoutMs;
2014
- this.logFlushIntervalMs = options.logFlushIntervalMs;
2015
- this.cancelCheckIntervalMs = options.cancelCheckIntervalMs;
2016
- this.cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
2017
- this.logger.debug("agente configurado", { baseUrl: config.tasksBaseUrl });
2161
+ this.backoff = new Backoff(this.logger, "poll", this.pollIntervalMs);
2018
2162
  }
2019
2163
  logger;
2164
+ pollIntervalMs;
2020
2165
  api;
2021
2166
  client;
2022
- pollIntervalMs;
2023
- concurrency;
2024
- instanceId;
2025
- taskTimeoutMs;
2026
- logFlushIntervalMs;
2027
- cancelCheckIntervalMs;
2028
- cancelGraceMs;
2167
+ registry;
2168
+ tracker;
2169
+ backoff;
2029
2170
  state = "idle";
2030
2171
  timer;
2031
2172
  pollAbort;
2032
- consecutiveFailures = 0;
2033
- consecutiveAuthFailures = 0;
2034
2173
  activeRuns = /* @__PURE__ */ new Set();
2035
- reporters = /* @__PURE__ */ new Set();
2036
- uninstallShutdown;
2037
2174
  get activeCount() {
2038
2175
  return this.activeRuns.size;
2039
2176
  }
2040
2177
  start() {
2041
2178
  if (this.state === "started") return;
2042
2179
  if (this.state === "stopping") {
2043
- throw new Error("[hobots] start() chamado durante stop() \u2014 aguarde o stop terminar");
2044
- }
2045
- if (this.registry.size === 0) {
2046
- throw new Error("[hobots] nenhuma task registrada \u2014 chame register() antes de start()");
2180
+ throw new Error("[hobots] start do agente durante o stop \u2014 aguarde o stop terminar");
2047
2181
  }
2048
2182
  this.state = "started";
2049
- if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
2050
- this.uninstallShutdown = installShutdownHandlers(this);
2183
+ this.logger.debug(`polling iniciado (tasks: ${this.registry.remoteTasks().map((task) => task.slug).join(", ")})`);
2184
+ this.schedule(0);
2185
+ }
2186
+ /** Uma task registrada depois do boot entra no próximo poll — que é antecipado para já. */
2187
+ wake() {
2188
+ if (this.state !== "started") return;
2189
+ if (this.timer) {
2190
+ clearTimeout(this.timer);
2191
+ this.timer = void 0;
2051
2192
  }
2052
- this.logger.debug(`polling iniciado (tasks: ${[...this.registry.keys()].join(", ")})`);
2053
2193
  this.schedule(0);
2054
2194
  }
2055
2195
  /** Para o polling e aguarda as runs ativas (até timeoutMs). Retorna true se tudo terminou a tempo. */
@@ -2062,40 +2202,10 @@ var TaskAgent = class {
2062
2202
  }
2063
2203
  this.pollAbort?.abort();
2064
2204
  const all = Promise.allSettled([...this.activeRuns]).then(() => true);
2065
- const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : false;
2066
- if (!finished && this.activeRuns.size > 0) {
2067
- this.logger.warn(`${this.activeRuns.size} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
2068
- await this.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
2069
- }
2070
- this.uninstallShutdown?.();
2071
- this.uninstallShutdown = void 0;
2205
+ const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : this.activeRuns.size === 0;
2072
2206
  this.state = "idle";
2073
2207
  return finished;
2074
2208
  }
2075
- /**
2076
- * Best-effort do exit path: reporta 'failed'/AgentTerminated para as runs ativas
2077
- * antes do processo morrer, fora da fila do reporter e com timeout curto. Runs já
2078
- * canceladas no app (reporter morto) são puladas; erros de rede são engolidos —
2079
- * o sweep server-side é o backstop.
2080
- */
2081
- async reportAgentTerminated(message, timeoutMs = 2500) {
2082
- const pending = [];
2083
- for (const reporter of this.reporters) {
2084
- if (reporter.isDead) continue;
2085
- const logs = reporter.drainLogs();
2086
- reporter.kill();
2087
- pending.push(
2088
- this.api.updateRunOnce(
2089
- reporter.context.runId,
2090
- { status: "failed", error: { type: "AgentTerminated", message }, logs: logs.length > 0 ? logs : void 0 },
2091
- timeoutMs
2092
- ).catch(() => {
2093
- })
2094
- );
2095
- }
2096
- if (pending.length === 0) return;
2097
- await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
2098
- }
2099
2209
  /** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
2100
2210
  schedule(delayMs) {
2101
2211
  if (this.state !== "started" || this.timer) return;
@@ -2104,9 +2214,22 @@ var TaskAgent = class {
2104
2214
  void this.tick();
2105
2215
  }, delayMs);
2106
2216
  }
2217
+ /** Slots livres por task remota — a soma vira o `max` do poll e as tasks com slot viram o filtro. */
2218
+ freeSlots() {
2219
+ const tasks = [];
2220
+ let max = 0;
2221
+ for (const task of this.registry.remoteTasks()) {
2222
+ const free = task.options.concurrency - this.tracker.activeFor(task.slug);
2223
+ if (free <= 0) continue;
2224
+ tasks.push(task.slug);
2225
+ max += free;
2226
+ }
2227
+ return { tasks, max: Math.min(MAX_CLAIM_PER_POLL, max) };
2228
+ }
2107
2229
  async tick() {
2108
2230
  if (this.state !== "started") return;
2109
- if (this.activeRuns.size >= this.concurrency) {
2231
+ const { tasks, max } = this.freeSlots();
2232
+ if (max === 0) {
2110
2233
  this.schedule(this.pollIntervalMs);
2111
2234
  return;
2112
2235
  }
@@ -2114,221 +2237,448 @@ var TaskAgent = class {
2114
2237
  try {
2115
2238
  const response = await this.api.poll(
2116
2239
  {
2117
- instance: {
2118
- // identidade canônica do agente no painel — o mesmo instanceId do
2119
- // heartbeat da Telemetria, então os dois canais convergem em um agente
2120
- instance_id: this.instanceId,
2121
- version: SDK_VERSION,
2122
- host: hostname(),
2123
- tasks: [...this.registry.keys()],
2124
- // o servidor calcula a tolerância de offline (3×) a partir disto em
2125
- // agentes poll-only — sem o campo, um poll lento flaparia offline/online.
2126
- // Clampado à faixa aceita pelo schema do poll (1s..15min).
2127
- poll_interval_ms: Math.min(Math.max(this.pollIntervalMs, 1e3), 9e5),
2128
- // recursos da máquina — alimenta a série da instância em agentes sem heartbeat
2129
- // (o servidor grava no máx. 1 ponto/30s por instância)
2130
- machine: this.options.machineSampler?.sample()
2131
- },
2132
- max: Math.min(MAX_CONCURRENCY, this.concurrency - this.activeRuns.size),
2133
- tasks: [...this.registry.keys()]
2240
+ // identidade canônica do agente no painel — o mesmo instanceId do heartbeat,
2241
+ // então os dois canais convergem em um agente. `instance.tasks` lista TODAS as
2242
+ // tasks (register + start); o filtro `tasks` abaixo só as que este poll pode rodar.
2243
+ instance: this.options.instanceInfo(),
2244
+ max,
2245
+ tasks
2134
2246
  },
2135
2247
  this.pollAbort.signal
2136
2248
  );
2137
- this.consecutiveFailures = 0;
2138
- this.consecutiveAuthFailures = 0;
2249
+ this.backoff.reset();
2139
2250
  for (const run of response.runs) {
2140
- if (this.activeRuns.size >= this.concurrency) break;
2141
2251
  this.dispatch(run);
2142
2252
  }
2143
2253
  this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
2144
2254
  } catch (error) {
2145
2255
  if (this.state !== "started") return;
2146
- this.schedule(this.delayAfterPollError(error));
2256
+ this.schedule(this.backoff.next(error));
2147
2257
  } finally {
2148
2258
  this.pollAbort = void 0;
2149
2259
  }
2150
2260
  }
2151
- delayAfterPollError(error) {
2152
- if (error instanceof TasksApiError && error.status === 401) {
2153
- this.consecutiveAuthFailures++;
2154
- if (this.consecutiveAuthFailures === 1 || this.consecutiveAuthFailures % 10 === 0) {
2155
- this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes");
2156
- }
2157
- return Math.max(this.pollIntervalMs, AUTH_FAILURE_INTERVAL_MS);
2158
- }
2159
- this.consecutiveFailures++;
2160
- const ladder = POLL_RETRY_DELAYS_MS[Math.min(this.consecutiveFailures - 1, POLL_RETRY_DELAYS_MS.length - 1)];
2161
- const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.pollIntervalMs, 16e3));
2162
- this.logger.warn(`poll falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
2163
- return delay;
2164
- }
2165
2261
  dispatch(run) {
2166
- const promise = this.executeRun(run).catch((error) => this.logger.error(`erro inesperado na run ${run.id}:`, error)).finally(() => {
2262
+ const promise = this.executeClaimed(run).catch((error) => this.logger.error(`erro inesperado na run ${run.id}:`, error)).finally(() => {
2167
2263
  this.activeRuns.delete(promise);
2168
2264
  if (this.state === "started") this.schedule(0);
2169
2265
  });
2170
2266
  this.activeRuns.add(promise);
2171
2267
  }
2172
- async executeRun(run) {
2173
- const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", true, run.id);
2174
- const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx, this.logFlushIntervalMs, this.cancelCheckIntervalMs);
2175
- this.reporters.add(reporter);
2268
+ async executeClaimed(run) {
2269
+ const task = this.registry.get(run.task);
2270
+ if (!task || task.mode !== "remote") {
2271
+ const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", run.id);
2272
+ const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx);
2273
+ reporter.reportFailure(toRunError(Object.assign(new Error(`tarefa n\xE3o registrada no agente: ${run.task}`), { name: "HandlerNotFound" })));
2274
+ await reporter.finalize();
2275
+ tx.setStatus("error");
2276
+ tx.finish();
2277
+ return;
2278
+ }
2279
+ await executeRun({ run, task, api: this.api, client: this.client, logger: this.logger, tracker: this.tracker });
2280
+ }
2281
+ };
2282
+
2283
+ // src/tasks/local.ts
2284
+ var LocalRunner = class {
2285
+ constructor(task, deps) {
2286
+ this.task = task;
2287
+ this.deps = deps;
2288
+ this.backoff = new Backoff(deps.logger, `cria\xE7\xE3o da run "${task.slug}"`);
2289
+ this.done = new Promise((resolve2) => {
2290
+ this.resolveDone = resolve2;
2291
+ });
2292
+ }
2293
+ abort = new AbortController();
2294
+ backoff;
2295
+ /** Resolve quando a run terminou (ou o runner foi parado antes de abri-la). */
2296
+ done;
2297
+ resolveDone;
2298
+ async run() {
2176
2299
  try {
2177
- const handler = this.registry.get(run.task);
2178
- if (!handler) {
2179
- reporter.reportFailure({
2180
- type: "HandlerNotFound",
2181
- message: `tarefa n\xE3o registrada no agente: ${run.task}`
2182
- });
2183
- await reporter.finalize();
2184
- tx.setStatus("error");
2185
- tx.finish();
2186
- return;
2187
- }
2188
- const ok = await reporter.reportRunning();
2189
- if (!ok) {
2190
- this.logger.debug(`run ${run.id} cancelada antes de iniciar \u2014 handler n\xE3o executado`);
2191
- tx.setStatus("cancelled");
2192
- tx.finish();
2193
- return;
2194
- }
2195
- this.logger.debug(`executando run ${run.id} (${run.task})`);
2300
+ const run = await this.createRun();
2301
+ if (!run) return;
2302
+ this.deps.logger.debug(`run ${run.id} aberta para "${this.task.slug}" (modo start)`);
2303
+ await executeRun({
2304
+ run,
2305
+ task: this.task,
2306
+ api: this.deps.api,
2307
+ client: this.deps.client,
2308
+ logger: this.deps.logger,
2309
+ tracker: this.deps.tracker
2310
+ });
2311
+ } catch (error) {
2312
+ this.deps.logger.error(`erro inesperado na task "${this.task.slug}":`, error);
2313
+ } finally {
2314
+ this.resolveDone();
2315
+ }
2316
+ }
2317
+ /** Desiste de abrir a run (close() antes da plataforma responder). Uma run já aberta segue pelo tracker. */
2318
+ stop() {
2319
+ this.abort.abort();
2320
+ }
2321
+ async createRun() {
2322
+ for (; ; ) {
2323
+ if (this.abort.signal.aborted) return void 0;
2196
2324
  try {
2197
- await runWithRunContext(
2198
- { runId: run.id, tx, attach: (image, options) => reporter.attach(image, options) },
2199
- () => this.invokeWithTimeout(handler, run, reporter)
2200
- );
2201
- await reporter.finalizeAttachments();
2202
- await reporter.finalizeItems();
2203
- tx.setStatus("ok");
2204
- tx.finish();
2205
- reporter.reportSuccess();
2206
- this.logger.debug(`run ${run.id} conclu\xEDda`);
2325
+ return await this.deps.api.createRun({ task: this.task.slug, instance: this.deps.instanceInfo() }, this.abort.signal);
2207
2326
  } catch (error) {
2208
- await reporter.finalizeAttachments();
2209
- await reporter.finalizeItems();
2210
- tx.setStatus("error");
2211
- tx.finish();
2212
- reporter.reportFailure(toRunError(error));
2213
- this.logger.debug(`run ${run.id} falhou: ${String(error)}`);
2327
+ if (this.abort.signal.aborted) return void 0;
2328
+ await abortableSleep(this.backoff.next(error), this.abort.signal);
2214
2329
  }
2215
- await reporter.finalize();
2216
- } finally {
2217
- this.reporters.delete(reporter);
2218
2330
  }
2219
2331
  }
2220
- async invokeWithTimeout(handler, run, reporter) {
2221
- const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
2222
- const races = [];
2223
- if (this.taskTimeoutMs && this.taskTimeoutMs > 0) {
2224
- races.push(sleep(this.taskTimeoutMs).then(() => TIMEOUT_SENTINEL));
2225
- }
2226
- if (this.cancelGraceMs > 0) {
2227
- races.push(waitForCancelGrace(reporter.context.signal, this.cancelGraceMs).then(() => CANCEL_SENTINEL));
2332
+ };
2333
+
2334
+ // src/tasks/registry.ts
2335
+ var TaskRegistry = class {
2336
+ tasks = /* @__PURE__ */ new Map();
2337
+ add(task) {
2338
+ if (this.tasks.has(task.slug)) {
2339
+ throw new Error(`[hobots] task "${task.slug}" j\xE1 registrada \u2014 cada slug s\xF3 pode ter um handler (register ou start)`);
2228
2340
  }
2229
- if (races.length === 0) {
2230
- await invocation;
2231
- return;
2341
+ this.tasks.set(task.slug, task);
2342
+ }
2343
+ get(slug) {
2344
+ return this.tasks.get(slug);
2345
+ }
2346
+ get size() {
2347
+ return this.tasks.size;
2348
+ }
2349
+ /** Tasks que chegam pelo poll (register). */
2350
+ remoteTasks() {
2351
+ return [...this.tasks.values()].filter((task) => task.mode === "remote");
2352
+ }
2353
+ /** Tasks abertas pelo próprio bot (start). */
2354
+ localTasks() {
2355
+ return [...this.tasks.values()].filter((task) => task.mode === "local");
2356
+ }
2357
+ /** Todos os slugs, para a presença da instância (`instance.tasks`). */
2358
+ allSlugs() {
2359
+ return [...this.tasks.keys()];
2360
+ }
2361
+ clear() {
2362
+ this.tasks.clear();
2363
+ }
2364
+ };
2365
+ var RunTracker = class {
2366
+ constructor(api) {
2367
+ this.api = api;
2368
+ }
2369
+ reporters = /* @__PURE__ */ new Set();
2370
+ waiters = /* @__PURE__ */ new Set();
2371
+ add(reporter) {
2372
+ this.reporters.add(reporter);
2373
+ }
2374
+ remove(reporter) {
2375
+ this.reporters.delete(reporter);
2376
+ if (this.reporters.size === 0) {
2377
+ for (const resolve2 of this.waiters) resolve2();
2378
+ this.waiters.clear();
2232
2379
  }
2233
- const outcome = await Promise.race([invocation, ...races]);
2234
- if (outcome !== TIMEOUT_SENTINEL && outcome !== CANCEL_SENTINEL) return;
2235
- if (outcome === CANCEL_SENTINEL) {
2236
- this.logger.warn(
2237
- `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)`
2380
+ }
2381
+ get activeCount() {
2382
+ return this.reporters.size;
2383
+ }
2384
+ /** Runs ativas de uma task (concorrência por task no poll). */
2385
+ activeFor(slug) {
2386
+ let count = 0;
2387
+ for (const reporter of this.reporters) if (reporter.context.task === slug) count++;
2388
+ return count;
2389
+ }
2390
+ /** A única run ativa, quando há exatamente uma — o que `hobots.*` usa fora de um contexto async. */
2391
+ singleActive() {
2392
+ if (this.reporters.size !== 1) return void 0;
2393
+ return this.reporters.values().next().value;
2394
+ }
2395
+ /** Resolve quando não houver mais runs ativas (ou após timeoutMs, devolvendo false). */
2396
+ async waitIdle(timeoutMs) {
2397
+ if (this.reporters.size === 0) return true;
2398
+ const idle = new Promise((resolve2) => this.waiters.add(() => resolve2(true)));
2399
+ if (timeoutMs <= 0) return false;
2400
+ return Promise.race([idle, sleep(timeoutMs).then(() => false)]);
2401
+ }
2402
+ /**
2403
+ * Best-effort do exit path: reporta 'failed'/AgentTerminated para as runs ativas
2404
+ * antes do processo morrer, fora da fila do reporter e com timeout curto. Runs já
2405
+ * canceladas no app (reporter morto) são puladas; erros de rede são engolidos —
2406
+ * o sweep server-side é o backstop.
2407
+ */
2408
+ async reportAgentTerminated(message, timeoutMs = 2500) {
2409
+ const pending = [];
2410
+ for (const reporter of this.reporters) {
2411
+ if (reporter.isDead) continue;
2412
+ const logs = reporter.drainLogs();
2413
+ reporter.kill();
2414
+ pending.push(
2415
+ this.api.updateRunOnce(
2416
+ reporter.context.runId,
2417
+ { status: "failed", error: { type: "AgentTerminated", message }, logs: logs.length > 0 ? logs : void 0 },
2418
+ timeoutMs
2419
+ ).catch(() => {
2420
+ })
2238
2421
  );
2239
- return;
2240
2422
  }
2241
- const timeoutMs = this.taskTimeoutMs;
2242
- reporter.abort();
2243
- 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)`));
2244
- throw Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
2245
- name: "TaskTimeout"
2246
- });
2423
+ if (pending.length === 0) return;
2424
+ await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
2247
2425
  }
2248
2426
  };
2249
- var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
2250
- var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
2251
- function waitForCancelGrace(signal, graceMs) {
2252
- return new Promise((resolve) => {
2253
- if (signal.aborted) {
2254
- setTimeout(resolve, graceMs);
2255
- return;
2427
+
2428
+ // src/tasks/shutdown.ts
2429
+ var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
2430
+ function installShutdownHandlers(agent) {
2431
+ if (!isNode()) return () => {
2432
+ };
2433
+ let signaled = false;
2434
+ const signalHandlers = /* @__PURE__ */ new Map();
2435
+ for (const signal of WATCHED_SIGNALS) {
2436
+ const handler = markInternal(() => {
2437
+ if (externalListenerCount(signal) > 0) return;
2438
+ if (signaled) return;
2439
+ signaled = true;
2440
+ const reraise = () => {
2441
+ process.removeListener(signal, handler);
2442
+ process.kill(process.pid, signal);
2443
+ };
2444
+ if (agent.activeCount === 0) {
2445
+ reraise();
2446
+ return;
2447
+ }
2448
+ const report = agent.reportAgentTerminated(`o processo do agente recebeu ${signal} durante a execu\xE7\xE3o`);
2449
+ addEmergencyTask(report);
2450
+ void report.finally(reraise);
2451
+ });
2452
+ process.on(signal, handler);
2453
+ signalHandlers.set(signal, handler);
2454
+ }
2455
+ const onFatal = markInternal((error) => {
2456
+ 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();
2457
+ addEmergencyTask(report);
2458
+ if (!hasInstalledGlobalHandlers() && externalListenerCount("uncaughtException") === 0) {
2459
+ void report.finally(() => {
2460
+ console.error(error);
2461
+ process.exit(1);
2462
+ });
2256
2463
  }
2257
- signal.addEventListener("abort", () => setTimeout(resolve, graceMs), { once: true });
2258
2464
  });
2465
+ process.on("uncaughtException", onFatal);
2466
+ return () => {
2467
+ for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
2468
+ process.removeListener("uncaughtException", onFatal);
2469
+ };
2259
2470
  }
2260
- var MAX_ERROR_MESSAGE_CHARS = 1e4;
2261
- var MAX_ERROR_STACK_CHARS = 2e4;
2262
- function toRunError(error) {
2263
- if (error instanceof Error) {
2264
- return {
2265
- type: error.name,
2266
- message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
2267
- stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
2268
- };
2471
+
2472
+ // src/runtime.ts
2473
+ var HEARTBEAT_INTERVAL_MS = 3e4;
2474
+ var DEFAULT_RETRIES2 = 1;
2475
+ var DEFAULT_RETRY_DELAY_MS2 = 5e3;
2476
+ var DEFAULT_CANCEL_GRACE_MS = 1e4;
2477
+ var MAX_CONCURRENCY = 5;
2478
+ var MAX_SLUG_CHARS = 255;
2479
+ var PROCESS_LEVEL_KEYS = [
2480
+ "clientSecret",
2481
+ "instanceId",
2482
+ "environment",
2483
+ "release",
2484
+ "tags",
2485
+ "debug",
2486
+ "machineMetrics",
2487
+ "captureUnhandled",
2488
+ "installExitHandlers"
2489
+ ];
2490
+ var runtime;
2491
+ var warnedNoRuntime = false;
2492
+ function hostname() {
2493
+ if (!isNode()) return void 0;
2494
+ return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
2495
+ }
2496
+ function sameValue(a, b) {
2497
+ if (a === b) return true;
2498
+ if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
2499
+ return false;
2500
+ }
2501
+ function ensureRuntime(options) {
2502
+ if (runtime) {
2503
+ for (const key of PROCESS_LEVEL_KEYS) {
2504
+ if (options[key] !== void 0 && !sameValue(options[key], runtime.options[key])) {
2505
+ runtime.logger.warn(
2506
+ `op\xE7\xE3o "${key}" divergente ignorada \u2014 o primeiro register()/start() define as op\xE7\xF5es do processo`
2507
+ );
2508
+ }
2509
+ }
2510
+ return runtime;
2511
+ }
2512
+ const config = resolveConfig({ clientSecret: options.clientSecret });
2513
+ const logger = makeLogger(options.debug ?? false);
2514
+ const client = new Client(
2515
+ { environment: options.environment, release: options.release, tags: options.tags, debug: options.debug },
2516
+ config
2517
+ );
2518
+ const api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger });
2519
+ runtime = {
2520
+ options: { ...options, clientSecret: void 0 },
2521
+ config,
2522
+ client,
2523
+ logger,
2524
+ api,
2525
+ registry: new TaskRegistry(),
2526
+ tracker: new RunTracker(api),
2527
+ sampler: void 0,
2528
+ heartbeat: void 0,
2529
+ agent: void 0,
2530
+ locals: /* @__PURE__ */ new Map(),
2531
+ instanceId: resolveInstanceId(options.instanceId),
2532
+ pollIntervalMs: void 0,
2533
+ booted: false,
2534
+ bootScheduled: false,
2535
+ bootPromise: void 0,
2536
+ uninstallGlobalHandlers: options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0,
2537
+ uninstallShutdown: void 0
2538
+ };
2539
+ runtime.options.clientSecret = options.clientSecret;
2540
+ logger.debug("inicializado", { endpoint: config.ingestEndpoint });
2541
+ return runtime;
2542
+ }
2543
+ function validateTask(slug, handler, fn) {
2544
+ if (typeof slug !== "string" || slug.trim().length === 0 || slug.length > MAX_SLUG_CHARS) {
2545
+ throw new Error(`[hobots] ${fn}(): o slug da task precisa ser uma string de 1 a ${MAX_SLUG_CHARS} caracteres`);
2269
2546
  }
2270
- return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
2547
+ if (typeof handler !== "function") {
2548
+ throw new Error(`[hobots] ${fn}('${slug}'): o handler precisa ser uma fun\xE7\xE3o`);
2549
+ }
2550
+ return slug.trim();
2271
2551
  }
2272
-
2273
- // src/core.ts
2274
- var DEFAULT_MAX_BREADCRUMBS = 100;
2275
- var client;
2276
- var scope = new Scope();
2277
- var heartbeat;
2278
- var machineSampler;
2279
- var agent;
2280
- var uninstallGlobalHandlers;
2281
- var registry = /* @__PURE__ */ new Map();
2282
- var warnedTasksDisabled = false;
2283
- function init(options) {
2284
- if (client) {
2285
- client.logger.warn("init() chamado mais de uma vez \u2014 substituindo o client anterior");
2286
- heartbeat?.stop();
2287
- heartbeat = void 0;
2288
- }
2289
- if (agent) {
2290
- void agent.stop(0);
2291
- agent = void 0;
2292
- }
2293
- uninstallGlobalHandlers?.();
2294
- uninstallGlobalHandlers = void 0;
2295
- warnedTasksDisabled = false;
2296
- const config = resolveConfig(options);
2297
- client = new Client(options, config);
2298
- machineSampler = options.machineMetrics === false ? void 0 : new MachineSampler(options.instanceId, client.logger);
2299
- void machineSampler?.start();
2300
- client.attachMachineSampler(machineSampler);
2301
- if (options.captureUnhandled !== false) {
2302
- uninstallGlobalHandlers = installGlobalHandlers(client, () => scope);
2303
- }
2304
- if (options.heartbeat) {
2305
- const heartbeatOptions = options.heartbeat === true ? {} : options.heartbeat;
2306
- heartbeat = new Heartbeat(client, heartbeatOptions, options.instanceId, machineSampler);
2307
- heartbeat.start();
2308
- }
2309
- if (options.tasks) {
2310
- const tasksOptions = options.tasks === true ? {} : options.tasks;
2311
- agent = new TaskAgent(
2312
- { ...tasksOptions, config, instanceId: options.instanceId, debug: options.debug, client, machineSampler },
2313
- registry
2314
- );
2552
+ function clampConcurrency(value) {
2553
+ if (value === void 0 || !Number.isFinite(value)) return 1;
2554
+ return Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(value)));
2555
+ }
2556
+ function resolveTaskOptions(options, mode) {
2557
+ const retries = options.retries === void 0 || !Number.isFinite(options.retries) ? DEFAULT_RETRIES2 : Math.max(0, Math.floor(options.retries));
2558
+ return {
2559
+ retries,
2560
+ retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS2,
2561
+ timeoutMs: options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : void 0,
2562
+ concurrency: mode === "remote" ? clampConcurrency(options.concurrency) : 1,
2563
+ logFlushIntervalMs: options.logFlushIntervalMs,
2564
+ cancelCheckIntervalMs: options.cancelCheckIntervalMs,
2565
+ cancelGraceMs: options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS
2566
+ };
2567
+ }
2568
+ function instanceInfo(rt) {
2569
+ return {
2570
+ instance_id: rt.instanceId ?? "unknown",
2571
+ version: SDK_VERSION,
2572
+ host: hostname(),
2573
+ // ambiente da instância: junto do heartbeat, é como o servidor sabe onde o robô roda
2574
+ environment: rt.options.environment,
2575
+ // TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
2576
+ tasks: rt.registry.allSlugs(),
2577
+ // o servidor calcula a tolerância de offline (3×) a partir disto em agentes que fazem
2578
+ // poll; clampado à faixa aceita pelo schema (1s..15min)
2579
+ poll_interval_ms: rt.agent ? Math.min(Math.max(rt.agent.pollIntervalMs, 1e3), 9e5) : void 0,
2580
+ // recursos da máquina — alimenta a série da instância (o servidor grava no máx. 1 ponto/30s)
2581
+ machine: rt.sampler?.sample()
2582
+ };
2583
+ }
2584
+ function ensureAgent(rt) {
2585
+ rt.agent ??= new TaskAgent({
2586
+ api: rt.api,
2587
+ client: rt.client,
2588
+ logger: rt.logger,
2589
+ registry: rt.registry,
2590
+ tracker: rt.tracker,
2591
+ instanceInfo: () => instanceInfo(rt),
2592
+ pollIntervalMs: rt.pollIntervalMs
2593
+ });
2594
+ return rt.agent;
2595
+ }
2596
+ function launchLocal(rt, task) {
2597
+ const runner = new LocalRunner(task, {
2598
+ api: rt.api,
2599
+ client: rt.client,
2600
+ logger: rt.logger,
2601
+ tracker: rt.tracker,
2602
+ instanceInfo: () => instanceInfo(rt)
2603
+ });
2604
+ rt.locals.set(task.slug, runner);
2605
+ void runner.run();
2606
+ }
2607
+ async function boot(rt) {
2608
+ if (!rt.instanceId) {
2609
+ const id = await fallbackInstanceId();
2610
+ if (runtime !== rt) return;
2611
+ rt.instanceId = id;
2612
+ rt.logger.debug(`instanceId n\xE3o informado (env ${INSTANCE_ID_ENV}) \u2014 usando o hostname "${id}"`);
2613
+ }
2614
+ if (runtime !== rt) return;
2615
+ rt.booted = true;
2616
+ if (rt.options.machineMetrics !== false) {
2617
+ rt.sampler = new MachineSampler(rt.instanceId, rt.logger);
2618
+ void rt.sampler.start();
2619
+ rt.client.attachMachineSampler(rt.sampler);
2620
+ }
2621
+ rt.heartbeat = new Heartbeat(rt.client, { intervalMs: HEARTBEAT_INTERVAL_MS, keepAlive: true }, rt.instanceId, rt.sampler);
2622
+ rt.heartbeat.start();
2623
+ if (rt.options.installExitHandlers !== false) {
2624
+ rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
2625
+ }
2626
+ if (rt.registry.remoteTasks().length > 0) ensureAgent(rt).start();
2627
+ for (const task of rt.registry.localTasks()) launchLocal(rt, task);
2628
+ }
2629
+ function scheduleBoot(rt) {
2630
+ if (rt.bootScheduled) return;
2631
+ rt.bootScheduled = true;
2632
+ queueMicrotask(() => {
2633
+ rt.bootPromise = boot(rt).catch((error) => rt.logger.error("falha ao iniciar o SDK:", error));
2634
+ });
2635
+ }
2636
+ function addTask(fn, mode, task, handler, options) {
2637
+ const slug = validateTask(task, handler, fn);
2638
+ const rt = ensureRuntime(options);
2639
+ if (mode === "remote" && options.pollIntervalMs !== void 0) {
2640
+ if (rt.pollIntervalMs === void 0 && !rt.agent) rt.pollIntervalMs = options.pollIntervalMs;
2641
+ else if (rt.pollIntervalMs !== options.pollIntervalMs) {
2642
+ rt.logger.warn('op\xE7\xE3o "pollIntervalMs" divergente ignorada \u2014 o primeiro register() define o intervalo do poll');
2643
+ }
2644
+ }
2645
+ const registered = { slug, mode, handler, options: resolveTaskOptions(options, mode) };
2646
+ rt.registry.add(registered);
2647
+ if (!rt.booted) {
2648
+ scheduleBoot(rt);
2649
+ return;
2650
+ }
2651
+ if (mode === "remote") {
2652
+ const agent = ensureAgent(rt);
2653
+ agent.start();
2654
+ agent.wake();
2655
+ } else {
2656
+ launchLocal(rt, registered);
2315
2657
  }
2316
- client.logger.debug("inicializado", { endpoint: client.config.ingestEndpoint });
2317
2658
  }
2318
- function getClient() {
2319
- return client;
2659
+ function register(task, handler, options = {}) {
2660
+ addTask("register", "remote", task, handler, options);
2661
+ }
2662
+ function start(task, handler, options = {}) {
2663
+ addTask("start", "local", task, handler, options);
2664
+ }
2665
+ function currentReporter() {
2666
+ return currentRunReporter() ?? runtime?.tracker.singleActive();
2667
+ }
2668
+ function activeLogger() {
2669
+ return runtime?.logger;
2320
2670
  }
2321
2671
  function toRunLogLevel(type) {
2322
2672
  return type === "fatal" ? "error" : type;
2323
2673
  }
2324
- function forwardAttachment(attachment, message, type) {
2325
- const attach = currentRunAttach();
2326
- if (!attach) {
2327
- client?.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task (register/start)");
2674
+ function forwardAttachment(rt, attachment, message, type) {
2675
+ const reporter = currentReporter();
2676
+ if (!reporter) {
2677
+ rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task");
2328
2678
  return;
2329
2679
  }
2330
2680
  const withMeta = typeof attachment === "object" && "image" in attachment ? attachment : { image: attachment };
2331
- void attach(withMeta.image, {
2681
+ void reporter.attach(withMeta.image, {
2332
2682
  message,
2333
2683
  level: toRunLogLevel(type),
2334
2684
  caption: withMeta.caption,
@@ -2336,84 +2686,115 @@ function forwardAttachment(attachment, message, type) {
2336
2686
  });
2337
2687
  }
2338
2688
  function captureException(error, hint) {
2339
- if (hint?.attachment) {
2340
- const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2341
- forwardAttachment(hint.attachment, message, hint.type ?? "error");
2689
+ const rt = runtime;
2690
+ if (!rt) {
2691
+ if (!warnedNoRuntime) {
2692
+ warnedNoRuntime = true;
2693
+ console.warn("[hobots] captureException antes de registrar uma task (register/start) \u2014 evento descartado");
2694
+ }
2695
+ return void 0;
2342
2696
  }
2343
- return client?.captureException(error, hint, scope);
2344
- }
2345
- function captureMessage(message, type = "warning", hint) {
2346
2697
  if (hint?.attachment) {
2347
- forwardAttachment(hint.attachment, message, hint.type ?? type);
2348
- }
2349
- return client?.captureMessage(message, type, hint, scope);
2350
- }
2351
- function addBreadcrumb(crumb) {
2352
- scope.addBreadcrumb(crumb, client?.options.maxBreadcrumbs ?? DEFAULT_MAX_BREADCRUMBS);
2353
- }
2354
- function setTag(key, value) {
2355
- scope.setTag(key, value);
2356
- }
2357
- function setTags(tags) {
2358
- scope.setTags(tags);
2359
- }
2360
- function setUser(user) {
2361
- scope.setUser(user);
2362
- }
2363
- function setContext(name, context) {
2364
- scope.setContext(name, context);
2365
- }
2366
- function setExtra(key, value) {
2367
- scope.setExtra(key, value);
2368
- }
2369
- function withScope(callback) {
2370
- const previous = scope;
2371
- scope = previous.clone();
2372
- try {
2373
- return callback(scope);
2374
- } finally {
2375
- scope = previous;
2376
- }
2377
- }
2378
- function startTransaction(name, op) {
2379
- const active = currentRunTransaction();
2380
- if (active) {
2381
- client?.logger.debug(`startTransaction('${name}') dentro de uma execu\xE7\xE3o \u2014 reutilizando a transaction da run`);
2382
- return active;
2383
- }
2384
- const rate = client?.options.tracesSampleRate ?? 1;
2385
- const sampled = rate >= 1 || Math.random() < rate;
2386
- return new Transaction(client, name, op, sampled);
2387
- }
2388
- function flush(timeoutMs) {
2389
- return client ? client.flush(timeoutMs) : Promise.resolve(true);
2390
- }
2391
- function register(task, handler) {
2392
- if (registry.has(task)) {
2393
- (agent?.logger ?? client?.logger)?.warn(`register('${task}') sobrescreveu um handler existente`);
2394
- }
2395
- if (client && !agent && !warnedTasksDisabled) {
2396
- warnedTasksDisabled = true;
2397
- client.logger.warn(
2398
- `register('${task}') com tasks desabilitadas \u2014 passe tasks: true (ou op\xE7\xF5es) no init()`
2399
- );
2400
- }
2401
- registry.set(task, handler);
2402
- }
2403
- function start() {
2404
- if (!agent) {
2405
- throw new Error("[hobots] tasks n\xE3o configuradas \u2014 passe tasks: true (ou op\xE7\xF5es) no init() antes de start()");
2406
- }
2407
- agent.start();
2408
- }
2409
- function stop(timeoutMs) {
2410
- return agent ? agent.stop(timeoutMs) : Promise.resolve(true);
2698
+ const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2699
+ forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
2700
+ }
2701
+ return rt.client.captureException(error, hint);
2702
+ }
2703
+ async function close(timeoutMs = 3e4) {
2704
+ const rt = runtime;
2705
+ if (!rt) return true;
2706
+ runtime = void 0;
2707
+ await rt.bootPromise;
2708
+ if (rt.agent) await rt.agent.stop(0);
2709
+ for (const local of rt.locals.values()) local.stop();
2710
+ const idle = await rt.tracker.waitIdle(timeoutMs);
2711
+ if (!idle) {
2712
+ rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
2713
+ await rt.tracker.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
2714
+ }
2715
+ rt.heartbeat?.stop();
2716
+ rt.uninstallShutdown?.();
2717
+ rt.uninstallGlobalHandlers?.();
2718
+ const flushed = await rt.client.close(timeoutMs);
2719
+ return idle && flushed;
2411
2720
  }
2412
- async function close(timeoutMs) {
2413
- const agentStopped = agent ? await agent.stop(timeoutMs) : true;
2414
- heartbeat?.stop();
2415
- heartbeat = void 0;
2416
- const clientClosed = client ? await client.close(timeoutMs) : true;
2417
- return agentStopped && clientClosed;
2721
+
2722
+ // src/hobots.ts
2723
+ var warned = /* @__PURE__ */ new Set();
2724
+ function warnOutsideRun(member) {
2725
+ if (warned.has(member)) return;
2726
+ warned.add(member);
2727
+ 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)`;
2728
+ const logger = activeLogger();
2729
+ if (logger) logger.warn(message);
2730
+ else console.warn(`[hobots] ${message}`);
2731
+ }
2732
+ var FALLBACK_TASK = "hobots";
2733
+ var fallbackLog = {
2734
+ debug: (message) => printRunLog(FALLBACK_TASK, "debug", message),
2735
+ info: (message) => printRunLog(FALLBACK_TASK, "info", message),
2736
+ warn: (message) => printRunLog(FALLBACK_TASK, "warning", message),
2737
+ error: (message) => printRunLog(FALLBACK_TASK, "error", message),
2738
+ success: (message) => printRunLog(FALLBACK_TASK, "success", message)
2739
+ };
2740
+ var noop = () => void 0;
2741
+ var fallbackItems = { add: noop, succeeded: noop, failed: noop, occurrence: noop };
2742
+ var fallbackProgress = { total: noop, advance: noop, set: noop, current: 0, snapshot: null };
2743
+ var neverAborts = new AbortController().signal;
2744
+ var detachedTransaction;
2745
+ function resolve(member) {
2746
+ const reporter = currentReporter();
2747
+ if (reporter) return reporter.context;
2748
+ warnOutsideRun(member);
2749
+ return void 0;
2418
2750
  }
2751
+ function getter(member, fallback) {
2752
+ return { enumerable: true, get: () => resolve(member)?.[member] ?? fallback() };
2753
+ }
2754
+ function method(value) {
2755
+ return { enumerable: true, writable: false, value };
2756
+ }
2757
+ var hobots = Object.defineProperties({}, {
2758
+ // ── contexto da run atual (getters) ─────────────────────────────────────────
2759
+ runId: getter("runId", () => ""),
2760
+ task: getter("task", () => ""),
2761
+ params: getter("params", () => ({})),
2762
+ log: getter("log", () => fallbackLog),
2763
+ items: getter("items", () => fallbackItems),
2764
+ progress: getter("progress", () => fallbackProgress),
2765
+ transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
2766
+ signal: getter("signal", () => neverAborts),
2767
+ throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
2768
+ sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
2769
+ attach: method(
2770
+ (image, options) => resolve("attach")?.attach(image, options) ?? Promise.resolve({ id: "", ok: false, reason: "canceled" })
2771
+ ),
2772
+ retry: method((fn, options) => {
2773
+ const reporter = currentReporter();
2774
+ return reporter ? reporter.context.retry(fn, options) : retryWithBackoff(fn, options);
2775
+ }),
2776
+ // ── processo ────────────────────────────────────────────────────────────────
2777
+ register: method((task, handler, options) => {
2778
+ register(task, handler, options);
2779
+ installGlobalHobots();
2780
+ }),
2781
+ start: method((task, handler, options) => {
2782
+ start(task, handler, options);
2783
+ installGlobalHobots();
2784
+ }),
2785
+ close: method((timeoutMs) => close(timeoutMs)),
2786
+ captureException: method((error, hint) => captureException(error, hint))
2787
+ });
2788
+ function installGlobalHobots() {
2789
+ if (globalThis.hobots === hobots) return;
2790
+ Object.defineProperty(globalThis, "hobots", { value: hobots, configurable: true, writable: true, enumerable: false });
2791
+ }
2792
+ var register2 = hobots.register;
2793
+ var start2 = hobots.start;
2794
+ var close2 = hobots.close;
2795
+ var captureException2 = hobots.captureException;
2796
+ var retry = hobots.retry;
2797
+
2798
+ // src/index.ts
2799
+ var src_default = hobots;
2419
2800
  //# sourceMappingURL=index.cjs.map