hobots 1.9.0 → 1.9.2

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.d.cts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData, RunAttachmentContentType } from './protocol.cjs';
2
2
  export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.cjs';
3
3
 
4
+ /** Aceita tudo que o handler pode ter em mãos: bytes ou um caminho de arquivo (só Node). */
5
+ type AttachInput = Uint8Array | ArrayBuffer | string;
6
+
4
7
  /** API oficial da Hobots — o SDK sempre fala com ela em produção. */
5
8
  declare const DEFAULT_BASE_URL = "https://api2.hobots.app";
6
9
  interface ResolvedConfig {
@@ -123,31 +126,6 @@ declare class Transaction {
123
126
  finish(): void;
124
127
  }
125
128
 
126
- interface AttachOptions {
127
- /** Legenda curta, exibida sob a miniatura no app. Passa pelo scrub de segredos. */
128
- caption?: string;
129
- /** Nível da linha de log que carrega o anexo. Default: 'info' */
130
- level?: RunLogLevel;
131
- /** Texto da linha de log. Default: a legenda, ou 'captura de tela' */
132
- message?: string;
133
- /** Informe quando os magic bytes não bastarem (o SDK sempre prefere o que detectar). */
134
- contentType?: RunAttachmentContentType;
135
- }
136
- type AttachFailureReason =
137
- /** A instalação do Hobots não tem anexos habilitados (ou a API é anterior ao recurso) */
138
- 'unsupported' | 'too-large' | 'quota' | 'read-failed'
139
- /** Formato não reconhecido ou fora de PNG/JPEG/WEBP */
140
- | 'unsupported-format' | 'upload-failed' | 'canceled';
141
- interface AttachResult {
142
- /** Id do anexo, o mesmo referenciado na linha de log */
143
- id: string;
144
- ok: boolean;
145
- /** Motivo do insucesso — para diagnóstico; `attach` nunca lança */
146
- reason?: AttachFailureReason;
147
- }
148
- /** Aceita tudo que o handler pode ter em mãos: bytes ou um caminho de arquivo (só Node). */
149
- type AttachInput = Uint8Array | ArrayBuffer | string;
150
-
151
129
  /** Opções de tasks dentro de `init({ tasks: ... })`. Credencial (clientSecret), identidade (instanceId) e debug vêm do init. */
152
130
  interface TasksOptions {
153
131
  /** Intervalo entre polls sem trabalho. Default: 5_000 (mínimo 1_000) */
@@ -168,6 +146,13 @@ interface TasksOptions {
168
146
  * uma run sem reporte por ~60s. Default: 5_000 (mínimo 1_000, máximo 15_000)
169
147
  */
170
148
  cancelCheckIntervalMs?: number;
149
+ /**
150
+ * Tempo de graça após ctx.signal disparar (run cancelada no app) antes do
151
+ * agente desistir de esperar o handler e liberar o slot de concorrência. O
152
+ * handler não é morto — se ele nunca checar ctx.signal, pode continuar
153
+ * rodando em segundo plano; só o agente para de esperar por ele. Default: 10_000
154
+ */
155
+ cancelGraceMs?: number;
171
156
  /**
172
157
  * Instala handlers de SIGINT/SIGTERM/uncaughtException que reportam as runs
173
158
  * ativas como falha (AgentTerminated) antes do processo morrer. Nunca interfere
@@ -202,22 +187,6 @@ interface RunContext {
202
187
  throwIfCanceled(): void;
203
188
  /** Sleep cancelável: rejeita com `RunCanceled` se ctx.signal disparar antes de ms. */
204
189
  sleep(ms: number): Promise<void>;
205
- /**
206
- * Anexa uma imagem (print da tela) a uma linha de log da execução — a evidência
207
- * visual aparece como miniatura junto do log, na aba "logs do agente".
208
- *
209
- * Aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo
210
- * (só em Node). A linha de log é registrada imediatamente; o upload roda em
211
- * segundo plano e **nunca bloqueia nem lança** — falhas viram um aviso no log da
212
- * run e um `{ ok: false, reason }` no retorno, que pode ser ignorado com `void`.
213
- *
214
- * PNG, JPEG ou WEBP, até 5 MB, no máximo 20 anexos por execução.
215
- *
216
- * ```ts
217
- * await ctx.attach(await page.screenshot(), { caption: 'portal após o login' });
218
- * ```
219
- */
220
- attach(image: AttachInput, options?: AttachOptions): Promise<AttachResult>;
221
190
  }
222
191
  /** O retorno do handler é ignorado — nada além de status/logs/erro é enviado ao servidor. */
223
192
  type TaskHandler<P = Record<string, unknown>> = (params: P, ctx: RunContext) => void | Promise<void>;
@@ -265,17 +234,43 @@ interface InitOptions {
265
234
  /** Itens por envelope antes de flush imediato. Default: 10 */
266
235
  maxBatchSize?: number;
267
236
  }
237
+ /** Imagem enviada junto de um capture* — com legenda e content-type opcionais. */
238
+ interface CaptureAttachment {
239
+ /** Bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo (só em Node) */
240
+ image: AttachInput;
241
+ /** Legenda curta, exibida sob a miniatura no app. Passa pelo scrub de segredos. */
242
+ caption?: string;
243
+ /** Informe quando os magic bytes não bastarem (o SDK sempre prefere o que detectar). */
244
+ contentType?: RunAttachmentContentType;
245
+ }
268
246
  interface CaptureHint {
269
247
  level?: Severity;
270
248
  tags?: Record<string, string>;
271
249
  extra?: Record<string, unknown>;
272
250
  fingerprint?: string[];
251
+ /**
252
+ * Print da tela enviado junto da captura. Dentro de uma execução de task, a imagem
253
+ * vira anexo da linha de log da execução que espelha o evento — miniatura na aba
254
+ * "logs do agente". Fora de uma execução não há onde pendurar a imagem: o SDK
255
+ * descarta com um aviso. O upload roda em segundo plano e **nunca bloqueia nem lança**.
256
+ *
257
+ * PNG, JPEG ou WEBP, até 5 MB, no máximo 20 anexos por execução.
258
+ *
259
+ * ```ts
260
+ * Hobots.captureException(error, { attachment: { image: await page.screenshot(), caption: 'tela no momento do erro' } });
261
+ * ```
262
+ */
263
+ attachment?: AttachInput | CaptureAttachment;
273
264
  }
274
265
 
275
266
  /** Inicializa o SDK. Chame uma vez, o mais cedo possível no programa. */
276
267
  declare function init(options: InitOptions): void;
277
268
  declare function getClient(): Client | undefined;
278
- /** Captura uma exceção e retorna o event_id (undefined se o SDK não foi inicializado). */
269
+ /**
270
+ * Captura uma exceção e retorna o event_id (undefined se o SDK não foi inicializado).
271
+ * `hint.attachment` envia um print junto — dentro de uma execução de task, ele aparece
272
+ * como miniatura na linha de log da run correspondente ao erro.
273
+ */
279
274
  declare function captureException(error: unknown, hint?: CaptureHint): string | undefined;
280
275
  declare function captureMessage(message: string, level?: Severity, hint?: CaptureHint): string | undefined;
281
276
  /** Registra um breadcrumb — anexado aos próximos eventos como trilha do que aconteceu antes. */
@@ -308,4 +303,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
308
303
 
309
304
  declare function scrubSecrets(text: string): string;
310
305
 
311
- export { type AttachFailureReason, type AttachOptions, type AttachResult, Breadcrumb, type BreadcrumbInput, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
306
+ export { type AttachInput, Breadcrumb, type BreadcrumbInput, type CaptureAttachment, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData, RunAttachmentContentType } from './protocol.js';
2
2
  export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.js';
3
3
 
4
+ /** Aceita tudo que o handler pode ter em mãos: bytes ou um caminho de arquivo (só Node). */
5
+ type AttachInput = Uint8Array | ArrayBuffer | string;
6
+
4
7
  /** API oficial da Hobots — o SDK sempre fala com ela em produção. */
5
8
  declare const DEFAULT_BASE_URL = "https://api2.hobots.app";
6
9
  interface ResolvedConfig {
@@ -123,31 +126,6 @@ declare class Transaction {
123
126
  finish(): void;
124
127
  }
125
128
 
126
- interface AttachOptions {
127
- /** Legenda curta, exibida sob a miniatura no app. Passa pelo scrub de segredos. */
128
- caption?: string;
129
- /** Nível da linha de log que carrega o anexo. Default: 'info' */
130
- level?: RunLogLevel;
131
- /** Texto da linha de log. Default: a legenda, ou 'captura de tela' */
132
- message?: string;
133
- /** Informe quando os magic bytes não bastarem (o SDK sempre prefere o que detectar). */
134
- contentType?: RunAttachmentContentType;
135
- }
136
- type AttachFailureReason =
137
- /** A instalação do Hobots não tem anexos habilitados (ou a API é anterior ao recurso) */
138
- 'unsupported' | 'too-large' | 'quota' | 'read-failed'
139
- /** Formato não reconhecido ou fora de PNG/JPEG/WEBP */
140
- | 'unsupported-format' | 'upload-failed' | 'canceled';
141
- interface AttachResult {
142
- /** Id do anexo, o mesmo referenciado na linha de log */
143
- id: string;
144
- ok: boolean;
145
- /** Motivo do insucesso — para diagnóstico; `attach` nunca lança */
146
- reason?: AttachFailureReason;
147
- }
148
- /** Aceita tudo que o handler pode ter em mãos: bytes ou um caminho de arquivo (só Node). */
149
- type AttachInput = Uint8Array | ArrayBuffer | string;
150
-
151
129
  /** Opções de tasks dentro de `init({ tasks: ... })`. Credencial (clientSecret), identidade (instanceId) e debug vêm do init. */
152
130
  interface TasksOptions {
153
131
  /** Intervalo entre polls sem trabalho. Default: 5_000 (mínimo 1_000) */
@@ -168,6 +146,13 @@ interface TasksOptions {
168
146
  * uma run sem reporte por ~60s. Default: 5_000 (mínimo 1_000, máximo 15_000)
169
147
  */
170
148
  cancelCheckIntervalMs?: number;
149
+ /**
150
+ * Tempo de graça após ctx.signal disparar (run cancelada no app) antes do
151
+ * agente desistir de esperar o handler e liberar o slot de concorrência. O
152
+ * handler não é morto — se ele nunca checar ctx.signal, pode continuar
153
+ * rodando em segundo plano; só o agente para de esperar por ele. Default: 10_000
154
+ */
155
+ cancelGraceMs?: number;
171
156
  /**
172
157
  * Instala handlers de SIGINT/SIGTERM/uncaughtException que reportam as runs
173
158
  * ativas como falha (AgentTerminated) antes do processo morrer. Nunca interfere
@@ -202,22 +187,6 @@ interface RunContext {
202
187
  throwIfCanceled(): void;
203
188
  /** Sleep cancelável: rejeita com `RunCanceled` se ctx.signal disparar antes de ms. */
204
189
  sleep(ms: number): Promise<void>;
205
- /**
206
- * Anexa uma imagem (print da tela) a uma linha de log da execução — a evidência
207
- * visual aparece como miniatura junto do log, na aba "logs do agente".
208
- *
209
- * Aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo
210
- * (só em Node). A linha de log é registrada imediatamente; o upload roda em
211
- * segundo plano e **nunca bloqueia nem lança** — falhas viram um aviso no log da
212
- * run e um `{ ok: false, reason }` no retorno, que pode ser ignorado com `void`.
213
- *
214
- * PNG, JPEG ou WEBP, até 5 MB, no máximo 20 anexos por execução.
215
- *
216
- * ```ts
217
- * await ctx.attach(await page.screenshot(), { caption: 'portal após o login' });
218
- * ```
219
- */
220
- attach(image: AttachInput, options?: AttachOptions): Promise<AttachResult>;
221
190
  }
222
191
  /** O retorno do handler é ignorado — nada além de status/logs/erro é enviado ao servidor. */
223
192
  type TaskHandler<P = Record<string, unknown>> = (params: P, ctx: RunContext) => void | Promise<void>;
@@ -265,17 +234,43 @@ interface InitOptions {
265
234
  /** Itens por envelope antes de flush imediato. Default: 10 */
266
235
  maxBatchSize?: number;
267
236
  }
237
+ /** Imagem enviada junto de um capture* — com legenda e content-type opcionais. */
238
+ interface CaptureAttachment {
239
+ /** Bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo (só em Node) */
240
+ image: AttachInput;
241
+ /** Legenda curta, exibida sob a miniatura no app. Passa pelo scrub de segredos. */
242
+ caption?: string;
243
+ /** Informe quando os magic bytes não bastarem (o SDK sempre prefere o que detectar). */
244
+ contentType?: RunAttachmentContentType;
245
+ }
268
246
  interface CaptureHint {
269
247
  level?: Severity;
270
248
  tags?: Record<string, string>;
271
249
  extra?: Record<string, unknown>;
272
250
  fingerprint?: string[];
251
+ /**
252
+ * Print da tela enviado junto da captura. Dentro de uma execução de task, a imagem
253
+ * vira anexo da linha de log da execução que espelha o evento — miniatura na aba
254
+ * "logs do agente". Fora de uma execução não há onde pendurar a imagem: o SDK
255
+ * descarta com um aviso. O upload roda em segundo plano e **nunca bloqueia nem lança**.
256
+ *
257
+ * PNG, JPEG ou WEBP, até 5 MB, no máximo 20 anexos por execução.
258
+ *
259
+ * ```ts
260
+ * Hobots.captureException(error, { attachment: { image: await page.screenshot(), caption: 'tela no momento do erro' } });
261
+ * ```
262
+ */
263
+ attachment?: AttachInput | CaptureAttachment;
273
264
  }
274
265
 
275
266
  /** Inicializa o SDK. Chame uma vez, o mais cedo possível no programa. */
276
267
  declare function init(options: InitOptions): void;
277
268
  declare function getClient(): Client | undefined;
278
- /** Captura uma exceção e retorna o event_id (undefined se o SDK não foi inicializado). */
269
+ /**
270
+ * Captura uma exceção e retorna o event_id (undefined se o SDK não foi inicializado).
271
+ * `hint.attachment` envia um print junto — dentro de uma execução de task, ele aparece
272
+ * como miniatura na linha de log da run correspondente ao erro.
273
+ */
279
274
  declare function captureException(error: unknown, hint?: CaptureHint): string | undefined;
280
275
  declare function captureMessage(message: string, level?: Severity, hint?: CaptureHint): string | undefined;
281
276
  /** Registra um breadcrumb — anexado aos próximos eventos como trilha do que aconteceu antes. */
@@ -308,4 +303,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
308
303
 
309
304
  declare function scrubSecrets(text: string): string;
310
305
 
311
- export { type AttachFailureReason, type AttachOptions, type AttachResult, Breadcrumb, type BreadcrumbInput, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
306
+ export { type AttachInput, Breadcrumb, type BreadcrumbInput, type CaptureAttachment, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
package/dist/index.js CHANGED
@@ -103,7 +103,7 @@ function scrubSecrets(text) {
103
103
 
104
104
  // src/utils.ts
105
105
  var SDK_NAME = "hobots";
106
- var SDK_VERSION = true ? "1.9.0" : "0.0.0-dev";
106
+ var SDK_VERSION = true ? "1.9.1" : "0.0.0-dev";
107
107
  function isNode() {
108
108
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
109
109
  }
@@ -1114,8 +1114,7 @@ var RunReporter = class {
1114
1114
  throwIfCanceled: () => {
1115
1115
  if (signal.aborted) throw runCanceledError();
1116
1116
  },
1117
- sleep: (ms) => cancelableSleep(ms, signal),
1118
- attach: (image, options) => this.attach(image, options)
1117
+ sleep: (ms) => cancelableSleep(ms, signal)
1119
1118
  };
1120
1119
  }
1121
1120
  context;
@@ -1260,7 +1259,8 @@ var RunReporter = class {
1260
1259
  /**
1261
1260
  * Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
1262
1261
  * o upload em segundo plano. Nunca lança: todo caminho de erro vira `{ ok: false }`
1263
- * com um aviso no log da run.
1262
+ * com um aviso no log da run. Chega ao handler pelos capture* do SDK — o hint
1263
+ * `attachment` de Hobots.captureException/captureMessage cai aqui via run-context-store.
1264
1264
  */
1265
1265
  async attach(image, options = {}) {
1266
1266
  const id = uuid4();
@@ -1447,6 +1447,9 @@ async function runWithRunContext(store, fn) {
1447
1447
  function currentRunTransaction() {
1448
1448
  return storage?.getStore()?.tx;
1449
1449
  }
1450
+ function currentRunAttach() {
1451
+ return storage?.getStore()?.attach;
1452
+ }
1450
1453
 
1451
1454
  // src/tasks/shutdown.ts
1452
1455
  var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
@@ -1498,6 +1501,7 @@ var MIN_POLL_INTERVAL_MS = 1e3;
1498
1501
  var MAX_CONCURRENCY = 5;
1499
1502
  var POLL_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
1500
1503
  var AUTH_FAILURE_INTERVAL_MS = 3e4;
1504
+ var DEFAULT_CANCEL_GRACE_MS = 1e4;
1501
1505
  function hostname() {
1502
1506
  if (!isNode()) return void 0;
1503
1507
  return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
@@ -1516,6 +1520,7 @@ var TaskAgent = class {
1516
1520
  this.taskTimeoutMs = options.taskTimeoutMs;
1517
1521
  this.logFlushIntervalMs = options.logFlushIntervalMs;
1518
1522
  this.cancelCheckIntervalMs = options.cancelCheckIntervalMs;
1523
+ this.cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
1519
1524
  this.logger.debug("agente configurado", { baseUrl: config.tasksBaseUrl });
1520
1525
  }
1521
1526
  logger;
@@ -1527,6 +1532,7 @@ var TaskAgent = class {
1527
1532
  taskTimeoutMs;
1528
1533
  logFlushIntervalMs;
1529
1534
  cancelCheckIntervalMs;
1535
+ cancelGraceMs;
1530
1536
  state = "idle";
1531
1537
  timer;
1532
1538
  pollAbort;
@@ -1688,7 +1694,10 @@ var TaskAgent = class {
1688
1694
  }
1689
1695
  this.logger.debug(`executando run ${run.id} (${run.task})`);
1690
1696
  try {
1691
- await runWithRunContext({ runId: run.id, tx }, () => this.invokeWithTimeout(handler, run, reporter));
1697
+ await runWithRunContext(
1698
+ { runId: run.id, tx, attach: (image, options) => reporter.attach(image, options) },
1699
+ () => this.invokeWithTimeout(handler, run, reporter)
1700
+ );
1692
1701
  await reporter.finalizeAttachments();
1693
1702
  tx.setStatus("ok");
1694
1703
  tx.finish();
@@ -1708,14 +1717,26 @@ var TaskAgent = class {
1708
1717
  }
1709
1718
  async invokeWithTimeout(handler, run, reporter) {
1710
1719
  const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
1711
- if (!this.taskTimeoutMs || this.taskTimeoutMs <= 0) {
1720
+ const races = [];
1721
+ if (this.taskTimeoutMs && this.taskTimeoutMs > 0) {
1722
+ races.push(sleep(this.taskTimeoutMs).then(() => TIMEOUT_SENTINEL));
1723
+ }
1724
+ if (this.cancelGraceMs > 0) {
1725
+ races.push(waitForCancelGrace(reporter.context.signal, this.cancelGraceMs).then(() => CANCEL_SENTINEL));
1726
+ }
1727
+ if (races.length === 0) {
1712
1728
  await invocation;
1713
1729
  return;
1714
1730
  }
1731
+ const outcome = await Promise.race([invocation, ...races]);
1732
+ if (outcome !== TIMEOUT_SENTINEL && outcome !== CANCEL_SENTINEL) return;
1733
+ if (outcome === CANCEL_SENTINEL) {
1734
+ this.logger.warn(
1735
+ `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)`
1736
+ );
1737
+ return;
1738
+ }
1715
1739
  const timeoutMs = this.taskTimeoutMs;
1716
- const timeout = sleep(timeoutMs).then(() => TIMEOUT_SENTINEL);
1717
- const outcome = await Promise.race([invocation, timeout]);
1718
- if (outcome !== TIMEOUT_SENTINEL) return;
1719
1740
  reporter.abort();
1720
1741
  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)`));
1721
1742
  throw Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
@@ -1724,6 +1745,16 @@ var TaskAgent = class {
1724
1745
  }
1725
1746
  };
1726
1747
  var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
1748
+ var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
1749
+ function waitForCancelGrace(signal, graceMs) {
1750
+ return new Promise((resolve) => {
1751
+ if (signal.aborted) {
1752
+ setTimeout(resolve, graceMs);
1753
+ return;
1754
+ }
1755
+ signal.addEventListener("abort", () => setTimeout(resolve, graceMs), { once: true });
1756
+ });
1757
+ }
1727
1758
  var MAX_ERROR_MESSAGE_CHARS = 1e4;
1728
1759
  var MAX_ERROR_STACK_CHARS = 2e4;
1729
1760
  function toRunError(error) {
@@ -1778,10 +1809,34 @@ function init(options) {
1778
1809
  function getClient() {
1779
1810
  return client;
1780
1811
  }
1812
+ function toRunLogLevel(level) {
1813
+ return level === "fatal" ? "error" : level;
1814
+ }
1815
+ function forwardAttachment(attachment, message, level) {
1816
+ const attach = currentRunAttach();
1817
+ if (!attach) {
1818
+ client?.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task (register/start)");
1819
+ return;
1820
+ }
1821
+ const withMeta = typeof attachment === "object" && "image" in attachment ? attachment : { image: attachment };
1822
+ void attach(withMeta.image, {
1823
+ message,
1824
+ level: toRunLogLevel(level),
1825
+ caption: withMeta.caption,
1826
+ contentType: withMeta.contentType
1827
+ });
1828
+ }
1781
1829
  function captureException(error, hint) {
1830
+ if (hint?.attachment) {
1831
+ const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
1832
+ forwardAttachment(hint.attachment, message, hint.level ?? "error");
1833
+ }
1782
1834
  return client?.captureException(error, hint, scope);
1783
1835
  }
1784
1836
  function captureMessage(message, level = "info", hint) {
1837
+ if (hint?.attachment) {
1838
+ forwardAttachment(hint.attachment, message, hint.level ?? level);
1839
+ }
1785
1840
  return client?.captureMessage(message, level, hint, scope);
1786
1841
  }
1787
1842
  function addBreadcrumb(crumb) {