hobots 3.4.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  WIRE_LIMITS
3
- } from "./chunk-5WIIACWC.js";
3
+ } from "./chunk-HKCDMUA3.js";
4
4
 
5
5
  // src/stacktrace.ts
6
6
  var MAX_FRAMES = 50;
@@ -57,7 +57,7 @@ function scrubSecrets(text) {
57
57
 
58
58
  // src/utils.ts
59
59
  var SDK_NAME = "hobots";
60
- var SDK_VERSION = true ? "3.4.0" : "0.0.0-dev";
60
+ var SDK_VERSION = true ? "4.1.0" : "0.0.0-dev";
61
61
  function isNode() {
62
62
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
63
63
  }
@@ -514,7 +514,7 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
514
514
  const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
515
515
  if (!key) {
516
516
  throw new Error(
517
- `${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`
517
+ `${prefix} Client Secret ausente \u2014 defina a env ${CLIENT_SECRET_ENV} (ou passe clientSecret nas op\xE7\xF5es do register/start); copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
518
518
  );
519
519
  }
520
520
  if (!SECRET_PATTERN.test(key)) {
@@ -527,7 +527,7 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
527
527
  key,
528
528
  baseUrl,
529
529
  ingestEndpoint: `${baseUrl}/v1/ingest`,
530
- tasksBaseUrl: `${baseUrl}/v1/tasks`
530
+ runsBaseUrl: `${baseUrl}/v1/runs`
531
531
  };
532
532
  }
533
533
 
@@ -891,7 +891,7 @@ var TasksApi = class {
891
891
  const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
892
892
  let response;
893
893
  try {
894
- response = await fetch(`${this.baseUrl}/runs`, {
894
+ response = await fetch(`${this.baseUrl}`, {
895
895
  redirect: "error",
896
896
  method: "POST",
897
897
  headers: this.headers(),
@@ -905,12 +905,11 @@ var TasksApi = class {
905
905
  throw new TasksApiError(`cria\xE7\xE3o da run falhou (HTTP ${response.status})`, response.status);
906
906
  }
907
907
  const body = await response.json().catch(() => null);
908
- if (typeof body?.id !== "string" || typeof body.task !== "string") {
909
- throw new TasksApiError("resposta inv\xE1lida da cria\xE7\xE3o de run (id/task ausentes)", 0);
908
+ if (typeof body?.id !== "string") {
909
+ throw new TasksApiError("resposta inv\xE1lida da cria\xE7\xE3o de run (id ausente)", 0);
910
910
  }
911
911
  return {
912
912
  id: body.id,
913
- task: body.task,
914
913
  params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
915
914
  form: null,
916
915
  source: typeof body.source === "string" ? body.source : "agent",
@@ -925,9 +924,9 @@ var TasksApi = class {
925
924
  }
926
925
  const valid = [];
927
926
  for (const run of runs.slice(0, Math.max(0, max))) {
928
- if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
927
+ if (typeof run?.id !== "string") continue;
929
928
  const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
930
- valid.push({ ...run, id: run.id, task: run.task, params });
929
+ valid.push({ ...run, id: run.id, params });
931
930
  }
932
931
  if (valid.length < runs.length) {
933
932
  this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
@@ -942,7 +941,7 @@ var TasksApi = class {
942
941
  let response;
943
942
  try {
944
943
  response = await fetchWithRetry(
945
- () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
944
+ () => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}`, {
946
945
  redirect: "error",
947
946
  method: "PATCH",
948
947
  headers: this.headers(),
@@ -964,7 +963,7 @@ var TasksApi = class {
964
963
  * que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
965
964
  */
966
965
  async updateRunOnce(runId, update, timeoutMs = 2e3) {
967
- await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
966
+ await fetch(`${this.baseUrl}/${encodeURIComponent(runId)}`, {
968
967
  redirect: "error",
969
968
  method: "PATCH",
970
969
  headers: this.headers(),
@@ -981,7 +980,7 @@ var TasksApi = class {
981
980
  let response;
982
981
  try {
983
982
  response = await fetchWithRetry(
984
- () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/items`, {
983
+ () => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/items`, {
985
984
  redirect: "error",
986
985
  method: "POST",
987
986
  headers: this.headers(),
@@ -1011,7 +1010,7 @@ var TasksApi = class {
1011
1010
  () => withRequestTimeout(
1012
1011
  UPDATE_TIMEOUT_MS,
1013
1012
  signal,
1014
- (timeoutSignal) => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments`, {
1013
+ (timeoutSignal) => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/attachments`, {
1015
1014
  redirect: "error",
1016
1015
  method: "POST",
1017
1016
  headers: this.headers(),
@@ -1062,7 +1061,7 @@ var TasksApi = class {
1062
1061
  /** Marca o anexo como enviado (ou falho). Best-effort — a leitura não depende disso. */
1063
1062
  async confirmAttachment(runId, attachmentId, request) {
1064
1063
  const response = await fetch(
1065
- `${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments/${encodeURIComponent(attachmentId)}`,
1064
+ `${this.baseUrl}/${encodeURIComponent(runId)}/attachments/${encodeURIComponent(attachmentId)}`,
1066
1065
  {
1067
1066
  redirect: "error",
1068
1067
  method: "PATCH",
@@ -1320,8 +1319,8 @@ function splitLogArgs(message, rest) {
1320
1319
  if (text.length === 0) return { text: last.caption !== void 0 ? safeString(last.caption) : "captura de tela", image: last };
1321
1320
  return { text: joinLogArgs(text[0], text.slice(1)), image: last };
1322
1321
  }
1323
- function printRunLog(task, level, message) {
1324
- const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
1322
+ function printRunLog(level, message) {
1323
+ const line = colorize(`[hobots] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
1325
1324
  const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
1326
1325
  console[method2](line, message);
1327
1326
  }
@@ -1349,7 +1348,6 @@ var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
1349
1348
  var MAX_LOG_MESSAGE_CHARS = WIRE_LIMITS.MAX_LOG_MESSAGE_CHARS;
1350
1349
  var MAX_LOGS_PER_PATCH = WIRE_LIMITS.MAX_LOGS_PER_PATCH;
1351
1350
  var FLUSH_AT_BUFFERED = 20;
1352
- var MAX_ITEMS_PER_RUN = WIRE_LIMITS.MAX_ITEMS_PER_RUN;
1353
1351
  var MAX_ITEMS_PER_REQUEST = WIRE_LIMITS.MAX_ITEMS_PER_REQUEST;
1354
1352
  var MAX_ITEM_MESSAGE_CHARS = WIRE_LIMITS.MAX_ITEM_MESSAGE_CHARS;
1355
1353
  var MAX_ITEM_PAYLOAD_BYTES = WIRE_LIMITS.MAX_ITEM_PAYLOAD_BYTES;
@@ -1367,10 +1365,11 @@ function scrubPayloadValue(value, depth) {
1367
1365
  return out;
1368
1366
  }
1369
1367
  var RunReporter = class {
1370
- constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
1368
+ constructor(api, runId, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
1371
1369
  this.api = api;
1372
1370
  this.runId = runId;
1373
1371
  this.logger = logger;
1372
+ this.tx = tx;
1374
1373
  this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
1375
1374
  this.cancelCheckIntervalMs = Math.min(
1376
1375
  MAX_CANCEL_CHECK_INTERVAL_MS,
@@ -1449,12 +1448,12 @@ var RunReporter = class {
1449
1448
  const sleep2 = (ms) => cancelableSleep(ms, signal);
1450
1449
  this.context = {
1451
1450
  runId,
1452
- task,
1453
1451
  params,
1454
1452
  log,
1455
1453
  items,
1456
1454
  progress,
1457
1455
  transaction: tx,
1456
+ flush: (timeoutMs) => this.flush(timeoutMs),
1458
1457
  signal,
1459
1458
  throwIfCanceled: checkCanceled,
1460
1459
  sleep: sleep2,
@@ -1464,6 +1463,7 @@ var RunReporter = class {
1464
1463
  api;
1465
1464
  runId;
1466
1465
  logger;
1466
+ tx;
1467
1467
  context;
1468
1468
  buffer = [];
1469
1469
  dead = false;
@@ -1481,7 +1481,6 @@ var RunReporter = class {
1481
1481
  itemsChain = Promise.resolve();
1482
1482
  itemsTimer;
1483
1483
  itemSeq = 0;
1484
- itemCount = 0;
1485
1484
  /** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
1486
1485
  itemsUnsupported = false;
1487
1486
  /** Cap de itens por run atingido (client-side ou 429) — degrada com um aviso só. */
@@ -1558,7 +1557,7 @@ var RunReporter = class {
1558
1557
  const scrubbed = scrubSecrets(safeString(message));
1559
1558
  const ts = nowISO();
1560
1559
  const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
1561
- printRunLog(this.context.task, level, attachmentId ? `${scrubbed} [imagem]` : scrubbed);
1560
+ printRunLog(level, attachmentId ? `${scrubbed} [imagem]` : scrubbed);
1562
1561
  this.push(level, scrubbed, true, attachmentId ? [attachmentId] : void 0, ts);
1563
1562
  }
1564
1563
  /** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
@@ -1604,6 +1603,21 @@ var RunReporter = class {
1604
1603
  this.enqueue({ status: "running", logs: logs.slice(i, i + MAX_LOGS_PER_PATCH) });
1605
1604
  }
1606
1605
  }
1606
+ /**
1607
+ * `hobots.flush()`: o bot decide quando entregar o que já fez — steps raiz finalizadas
1608
+ * viram transactions parciais da run, e itens, logs e progresso pendentes saem já, sem
1609
+ * esperar o throttle. Aguarda a entrega com teto. Nunca lança (pode ir num `finally`).
1610
+ */
1611
+ async flush(timeoutMs = ITEMS_DRAIN_MS) {
1612
+ this.tx.flush();
1613
+ this.flushItems();
1614
+ if (this.progressDirty) this.flushProgress();
1615
+ else this.flushLogs();
1616
+ await Promise.race([
1617
+ Promise.all([this.itemsChain, this.chain, this.tx.drain(timeoutMs)]).catch(() => void 0),
1618
+ new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
1619
+ ]);
1620
+ }
1607
1621
  /** Primeiro PATCH (claimed → running). Retorna false se a run já morreu (cancelada). */
1608
1622
  async reportRunning() {
1609
1623
  this.enqueue({ status: "running" });
@@ -1714,14 +1728,6 @@ var RunReporter = class {
1714
1728
  */
1715
1729
  addItem(status, message, payload, id, image3) {
1716
1730
  if (this.itemsClosed || this.itemsUnsupported) return;
1717
- if (this.itemCount >= MAX_ITEMS_PER_RUN) {
1718
- if (!this.itemsQuotaExceeded) {
1719
- this.itemsQuotaExceeded = true;
1720
- this.log("warning", `itens descartados: limite de ${MAX_ITEMS_PER_RUN} por execu\xE7\xE3o atingido`);
1721
- }
1722
- return;
1723
- }
1724
- this.itemCount++;
1725
1731
  let scrubbedPayload;
1726
1732
  if (payload) {
1727
1733
  scrubbedPayload = scrubPayloadValue(payload, 0);
@@ -2016,6 +2022,7 @@ function currentRunReporter() {
2016
2022
  }
2017
2023
 
2018
2024
  // src/transaction.ts
2025
+ var FLUSH_TRANSACTION_OP = "hobots.run.flush";
2019
2026
  var MAX_STEP_MESSAGE_CHARS = 2e3;
2020
2027
  function stepMessage(text) {
2021
2028
  return truncate(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
@@ -2203,7 +2210,7 @@ var Transaction = class {
2203
2210
  if (this.attachmentSink) return this.attachmentSink(image3, nowISO());
2204
2211
  if (!this.warnedNoAttachmentSink) {
2205
2212
  this.warnedNoAttachmentSink = true;
2206
- console.warn("[hobots] imagem de step descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o de task");
2213
+ console.warn("[hobots] imagem de step descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o");
2207
2214
  }
2208
2215
  return void 0;
2209
2216
  }
@@ -2237,6 +2244,60 @@ var Transaction = class {
2237
2244
  this.name = name;
2238
2245
  return this;
2239
2246
  }
2247
+ /**
2248
+ * Envia já as steps raiz finalizadas (e todas as descendentes, fechando as que
2249
+ * ficaram abertas) como transactions parciais da mesma run (`op` hobots.run.flush,
2250
+ * `event_id` novo, até 200 steps cada) e as tira da memória. Steps raiz ainda abertas
2251
+ * ficam para o próximo flush ou para o `finish()`. É assim que um agente sempre
2252
+ * ligado (`start()`), cuja run não termina, entrega as etapas enquanto processa.
2253
+ * Devolve quantas steps foram enviadas.
2254
+ */
2255
+ flush() {
2256
+ if (this.finished) return 0;
2257
+ const ready = /* @__PURE__ */ new Set();
2258
+ for (const step of this.steps) {
2259
+ if (step.parentStepId === this.stepId && step.endTimestamp) ready.add(step.stepId);
2260
+ }
2261
+ if (ready.size === 0) return 0;
2262
+ const batch = [];
2263
+ const remaining = [];
2264
+ for (const step of this.steps) {
2265
+ if (ready.has(step.stepId) || ready.has(step.parentStepId)) {
2266
+ ready.add(step.stepId);
2267
+ batch.push(step);
2268
+ } else {
2269
+ remaining.push(step);
2270
+ }
2271
+ }
2272
+ for (const step of batch) step.finish();
2273
+ this.steps.splice(0, this.steps.length, ...remaining);
2274
+ if (this.client) {
2275
+ for (let i = 0; i < batch.length; i += MAX_STEPS) {
2276
+ const chunk = batch.slice(i, i + MAX_STEPS);
2277
+ const startTimestamp = chunk.reduce((min, step) => step.startTimestamp < min ? step.startTimestamp : min, chunk[0].startTimestamp);
2278
+ const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
2279
+ this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, startTimestamp, nowISO(), chunk));
2280
+ }
2281
+ }
2282
+ return batch.length;
2283
+ }
2284
+ /** Entrega já (com teto) o que está na fila do transport — as transactions parciais inclusive. */
2285
+ async drain(timeoutMs) {
2286
+ await this.client?.flush(timeoutMs);
2287
+ }
2288
+ payload(eventId, op, status, startTimestamp, timestamp, steps) {
2289
+ return {
2290
+ event_id: eventId,
2291
+ name: this.name,
2292
+ op,
2293
+ status,
2294
+ start_timestamp: startTimestamp,
2295
+ timestamp,
2296
+ steps: steps.map((step) => step.toJSON()),
2297
+ tags: Object.keys(this.tags).length > 0 ? { ...this.tags } : void 0,
2298
+ run_id: this.runId
2299
+ };
2300
+ }
2240
2301
  finish() {
2241
2302
  if (this.finished) return;
2242
2303
  this.finished = true;
@@ -2251,31 +2312,27 @@ var Transaction = class {
2251
2312
  this.status
2252
2313
  );
2253
2314
  if (!this.client) return;
2254
- const payload = {
2255
- event_id: this.eventId,
2256
- name: this.name,
2257
- op: this.op,
2258
- status: this.status,
2259
- start_timestamp: this.startTimestamp,
2260
- timestamp: endTimestamp,
2261
- steps: this.steps.map((step) => step.toJSON()),
2262
- tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
2263
- run_id: this.runId
2264
- };
2265
- this.client.sendTransaction(payload);
2315
+ const steps = this.steps;
2316
+ let first = 0;
2317
+ while (steps.length - first > MAX_STEPS) {
2318
+ const chunk = steps.slice(first, first + MAX_STEPS);
2319
+ const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
2320
+ this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, chunk[0].startTimestamp, endTimestamp, chunk));
2321
+ first += MAX_STEPS;
2322
+ }
2323
+ this.client.sendTransaction(this.payload(this.eventId, this.op, this.status, this.startTimestamp, endTimestamp, steps.slice(first)));
2266
2324
  }
2267
2325
  };
2268
2326
 
2269
2327
  // src/tasks/runner.ts
2270
2328
  var DEFAULT_RETRY_DELAY_MS = 5e3;
2271
2329
  async function executeRun(input) {
2272
- const { run, task, api, client, logger, tracker } = input;
2273
- const options = task.options;
2274
- const tx = new Transaction(client, `run:${run.task}`, "hobots.task", run.id);
2330
+ const { run, handler, api, client, logger, tracker } = input;
2331
+ const options = handler.options;
2332
+ const tx = new Transaction(client, "run", "hobots.run", run.id);
2275
2333
  const reporter = new RunReporter(
2276
2334
  api,
2277
2335
  run.id,
2278
- run.task,
2279
2336
  logger,
2280
2337
  tx,
2281
2338
  options.logFlushIntervalMs,
@@ -2291,9 +2348,9 @@ async function executeRun(input) {
2291
2348
  tx.finish();
2292
2349
  return;
2293
2350
  }
2294
- logger.debug(`executando run ${run.id} (${run.task})`);
2351
+ logger.debug(`executando run ${run.id}`);
2295
2352
  try {
2296
- await runWithRunContext({ reporter }, () => invokeWithRetry(task, run, reporter, logger));
2353
+ await runWithRunContext({ reporter }, () => invokeWithRetry(handler, run, reporter, logger));
2297
2354
  await reporter.finalizeAttachments();
2298
2355
  await reporter.finalizeItems();
2299
2356
  warnIncompleteProgress(reporter);
@@ -2321,11 +2378,11 @@ function warnIncompleteProgress(reporter) {
2321
2378
  `execu\xE7\xE3o conclu\xEDda com progresso incompleto: ${snapshot.current}/${snapshot.total} \u2014 o handler terminou sem processar (ou sem avan\xE7ar) todos os itens previstos`
2322
2379
  );
2323
2380
  }
2324
- function invokeWithRetry(task, run, reporter, logger) {
2325
- const { retries, retryDelayMs } = task.options;
2381
+ function invokeWithRetry(agent, run, reporter, logger) {
2382
+ const { retries, retryDelayMs } = agent.options;
2326
2383
  const total = retries + 1;
2327
2384
  const ctx = reporter.context;
2328
- return retryWithBackoff(() => invokeWithTimeout(task, run, reporter, logger), {
2385
+ return retryWithBackoff(() => invokeWithTimeout(agent, run, reporter, logger), {
2329
2386
  retries,
2330
2387
  delayMs: (attempt) => resolveDelay(retryDelayMs, attempt, DEFAULT_RETRY_DELAY_MS),
2331
2388
  shouldRetry: () => !reporter.isDead,
@@ -2339,9 +2396,9 @@ function invokeWithRetry(task, run, reporter, logger) {
2339
2396
  }
2340
2397
  var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
2341
2398
  var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
2342
- async function invokeWithTimeout(task, run, reporter, logger) {
2343
- const { timeoutMs, cancelGraceMs } = task.options;
2344
- const handler = task.handler;
2399
+ async function invokeWithTimeout(agent, run, reporter, logger) {
2400
+ const { timeoutMs, cancelGraceMs } = agent.options;
2401
+ const handler = agent.handler;
2345
2402
  const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
2346
2403
  const races = [];
2347
2404
  if (timeoutMs && timeoutMs > 0) {
@@ -2398,7 +2455,7 @@ var TaskAgent = class {
2398
2455
  this.logger = options.logger;
2399
2456
  this.api = options.api;
2400
2457
  this.client = options.client;
2401
- this.registry = options.registry;
2458
+ this.handler = options.handler;
2402
2459
  this.tracker = options.tracker;
2403
2460
  this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
2404
2461
  this.backoff = new Backoff(this.logger, "poll", this.pollIntervalMs);
@@ -2408,7 +2465,7 @@ var TaskAgent = class {
2408
2465
  pollIntervalMs;
2409
2466
  api;
2410
2467
  client;
2411
- registry;
2468
+ handler;
2412
2469
  tracker;
2413
2470
  backoff;
2414
2471
  state = "idle";
@@ -2424,10 +2481,10 @@ var TaskAgent = class {
2424
2481
  throw new Error("[hobots] start do agente durante o stop \u2014 aguarde o stop terminar");
2425
2482
  }
2426
2483
  this.state = "started";
2427
- this.logger.debug(`polling iniciado (tasks: ${this.registry.remoteTasks().map((task) => task.slug).join(", ")})`);
2484
+ this.logger.debug(`polling iniciado (concorr\xEAncia ${this.handler.options.concurrency})`);
2428
2485
  this.schedule(0);
2429
2486
  }
2430
- /** Uma task registrada depois do boot entra no próximo poll — que é antecipado para já. */
2487
+ /** Antecipa o próximo poll para já. */
2431
2488
  wake() {
2432
2489
  if (this.state !== "started") return;
2433
2490
  if (this.timer) {
@@ -2458,21 +2515,14 @@ var TaskAgent = class {
2458
2515
  void this.tick();
2459
2516
  }, delayMs);
2460
2517
  }
2461
- /** Slots livres por task remota — a soma vira o `max` do poll e as tasks com slot viram o filtro. */
2518
+ /** Slots livres do agente — vira o `max` do poll (nunca reivindica trabalho para deixar parado). */
2462
2519
  freeSlots() {
2463
- const tasks = [];
2464
- let max = 0;
2465
- for (const task of this.registry.remoteTasks()) {
2466
- const free = task.options.concurrency - this.tracker.activeFor(task.slug);
2467
- if (free <= 0) continue;
2468
- tasks.push(task.slug);
2469
- max += free;
2470
- }
2471
- return { tasks, max: Math.min(MAX_CLAIM_PER_POLL, max) };
2520
+ const free = this.handler.options.concurrency - this.tracker.activeCount;
2521
+ return Math.min(MAX_CLAIM_PER_POLL, Math.max(0, free));
2472
2522
  }
2473
2523
  async tick() {
2474
2524
  if (this.state !== "started") return;
2475
- const { tasks, max } = this.freeSlots();
2525
+ const max = this.freeSlots();
2476
2526
  if (max === 0) {
2477
2527
  this.schedule(this.pollIntervalMs);
2478
2528
  return;
@@ -2482,11 +2532,9 @@ var TaskAgent = class {
2482
2532
  const response = await this.api.poll(
2483
2533
  {
2484
2534
  // identidade canônica do agente no painel — o mesmo instanceId do heartbeat,
2485
- // então os dois canais convergem em um agente. `instance.tasks` lista TODAS as
2486
- // tasks (register + start); o filtro `tasks` abaixo só as que este poll pode rodar.
2535
+ // então os dois canais convergem em um agente
2487
2536
  instance: this.options.instanceInfo(),
2488
- max,
2489
- tasks
2537
+ max
2490
2538
  },
2491
2539
  this.pollAbort.signal
2492
2540
  );
@@ -2510,17 +2558,7 @@ var TaskAgent = class {
2510
2558
  this.activeRuns.add(promise);
2511
2559
  }
2512
2560
  async executeClaimed(run) {
2513
- const task = this.registry.get(run.task);
2514
- if (!task || task.mode !== "remote") {
2515
- const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", run.id);
2516
- const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx);
2517
- reporter.reportFailure(toRunError(Object.assign(new Error(`tarefa n\xE3o registrada no agente: ${run.task}`), { name: "HandlerNotFound" })));
2518
- await reporter.finalize();
2519
- tx.setStatus("error");
2520
- tx.finish();
2521
- return;
2522
- }
2523
- await executeRun({ run, task, api: this.api, client: this.client, logger: this.logger, tracker: this.tracker });
2561
+ await executeRun({ run, handler: this.handler, api: this.api, client: this.client, logger: this.logger, tracker: this.tracker });
2524
2562
  }
2525
2563
  };
2526
2564
 
@@ -2528,11 +2566,11 @@ var TaskAgent = class {
2528
2566
  var KEY_RE = /^[a-zA-Z0-9_.-]{1,100}$/;
2529
2567
  var MAX_COLUMNS = WIRE_LIMITS.MAX_ITEM_COLUMNS;
2530
2568
  var MAX_LABEL_CHARS = WIRE_LIMITS.MAX_ITEM_COLUMN_LABEL_CHARS;
2531
- function normalizeItemColumns(slug, input, logger) {
2569
+ function normalizeItemColumns(input, logger) {
2532
2570
  if (input === void 0 || input === null) return void 0;
2533
2571
  const columns = input.columns;
2534
2572
  if (typeof input !== "object" || !Array.isArray(columns)) {
2535
- logger.warn(`task "${slug}": items.columns precisa ser uma lista \u2014 declara\xE7\xE3o de colunas ignorada`);
2573
+ logger.warn(`items.columns precisa ser uma lista \u2014 declara\xE7\xE3o de colunas ignorada`);
2536
2574
  return void 0;
2537
2575
  }
2538
2576
  const result = [];
@@ -2542,15 +2580,15 @@ function normalizeItemColumns(slug, input, logger) {
2542
2580
  const rawLabel = typeof entry === "object" && entry !== null ? entry.label : void 0;
2543
2581
  const key = typeof rawKey === "string" ? rawKey.trim() : "";
2544
2582
  if (!KEY_RE.test(key)) {
2545
- logger.warn(`task "${slug}": coluna de item inv\xE1lida ignorada (${JSON.stringify(rawKey)}) \u2014 use letras, n\xFAmeros, ponto, h\xEDfen e underline`);
2583
+ logger.warn(`coluna de item inv\xE1lida ignorada (${JSON.stringify(rawKey)}) \u2014 use letras, n\xFAmeros, ponto, h\xEDfen e underline`);
2546
2584
  continue;
2547
2585
  }
2548
2586
  if (seen.has(key)) {
2549
- logger.warn(`task "${slug}": coluna de item "${key}" repetida ignorada`);
2587
+ logger.warn(`coluna de item "${key}" repetida ignorada`);
2550
2588
  continue;
2551
2589
  }
2552
2590
  if (result.length >= MAX_COLUMNS) {
2553
- logger.warn(`task "${slug}": no m\xE1ximo ${MAX_COLUMNS} colunas de item \u2014 "${key}" em diante ignoradas`);
2591
+ logger.warn(`no m\xE1ximo ${MAX_COLUMNS} colunas de item \u2014 "${key}" em diante ignoradas`);
2554
2592
  break;
2555
2593
  }
2556
2594
  seen.add(key);
@@ -2562,15 +2600,15 @@ function normalizeItemColumns(slug, input, logger) {
2562
2600
 
2563
2601
  // src/tasks/local.ts
2564
2602
  var LocalRunner = class {
2565
- constructor(task, deps) {
2566
- this.task = task;
2603
+ constructor(handler, deps) {
2604
+ this.handler = handler;
2567
2605
  this.deps = deps;
2568
- this.backoff = new Backoff(deps.logger, `cria\xE7\xE3o da run "${task.slug}"`);
2606
+ this.backoff = new Backoff(deps.logger, "cria\xE7\xE3o da run");
2569
2607
  this.done = new Promise((resolve2) => {
2570
2608
  this.resolveDone = resolve2;
2571
2609
  });
2572
2610
  }
2573
- task;
2611
+ handler;
2574
2612
  deps;
2575
2613
  abort = new AbortController();
2576
2614
  backoff;
@@ -2581,17 +2619,17 @@ var LocalRunner = class {
2581
2619
  try {
2582
2620
  const run = await this.createRun();
2583
2621
  if (!run) return;
2584
- this.deps.logger.debug(`run ${run.id} aberta para "${this.task.slug}" (modo start)`);
2622
+ this.deps.logger.debug(`run ${run.id} aberta (modo start)`);
2585
2623
  await executeRun({
2586
2624
  run,
2587
- task: this.task,
2625
+ handler: this.handler,
2588
2626
  api: this.deps.api,
2589
2627
  client: this.deps.client,
2590
2628
  logger: this.deps.logger,
2591
2629
  tracker: this.deps.tracker
2592
2630
  });
2593
2631
  } catch (error) {
2594
- this.deps.logger.error(`erro inesperado na task "${this.task.slug}":`, error);
2632
+ this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
2595
2633
  } finally {
2596
2634
  this.resolveDone();
2597
2635
  }
@@ -2604,7 +2642,7 @@ var LocalRunner = class {
2604
2642
  for (; ; ) {
2605
2643
  if (this.abort.signal.aborted) return void 0;
2606
2644
  try {
2607
- return await this.deps.api.createRun({ task: this.task.slug, instance: this.deps.instanceInfo() }, this.abort.signal);
2645
+ return await this.deps.api.createRun({ instance: this.deps.instanceInfo() }, this.abort.signal);
2608
2646
  } catch (error) {
2609
2647
  if (this.abort.signal.aborted) return void 0;
2610
2648
  await abortableSleep(this.backoff.next(error), this.abort.signal);
@@ -2614,45 +2652,6 @@ var LocalRunner = class {
2614
2652
  };
2615
2653
 
2616
2654
  // src/tasks/registry.ts
2617
- var TaskRegistry = class {
2618
- tasks = /* @__PURE__ */ new Map();
2619
- add(task) {
2620
- if (this.tasks.has(task.slug)) {
2621
- throw new Error(`[hobots] task "${task.slug}" j\xE1 registrada \u2014 cada slug s\xF3 pode ter um handler (register ou start)`);
2622
- }
2623
- this.tasks.set(task.slug, task);
2624
- }
2625
- get(slug) {
2626
- return this.tasks.get(slug);
2627
- }
2628
- get size() {
2629
- return this.tasks.size;
2630
- }
2631
- /** Tasks que chegam pelo poll (register). */
2632
- remoteTasks() {
2633
- return [...this.tasks.values()].filter((task) => task.mode === "remote");
2634
- }
2635
- /** Tasks abertas pelo próprio bot (start). */
2636
- localTasks() {
2637
- return [...this.tasks.values()].filter((task) => task.mode === "local");
2638
- }
2639
- /** Todos os slugs, para a presença da instância (`instance.tasks`). */
2640
- allSlugs() {
2641
- return [...this.tasks.keys()];
2642
- }
2643
- /** Colunas de itens declaradas no código, por slug (`instance.task_items`); undefined se nenhuma task declarou. */
2644
- itemDeclarations() {
2645
- let result;
2646
- for (const task of this.tasks.values()) {
2647
- if (!task.options.itemColumns) continue;
2648
- (result ??= {})[task.slug] = { attributes: task.options.itemColumns };
2649
- }
2650
- return result;
2651
- }
2652
- clear() {
2653
- this.tasks.clear();
2654
- }
2655
- };
2656
2655
  var RunTracker = class {
2657
2656
  constructor(api) {
2658
2657
  this.api = api;
@@ -2673,12 +2672,6 @@ var RunTracker = class {
2673
2672
  get activeCount() {
2674
2673
  return this.reporters.size;
2675
2674
  }
2676
- /** Runs ativas de uma task (concorrência por task no poll). */
2677
- activeFor(slug) {
2678
- let count = 0;
2679
- for (const reporter of this.reporters) if (reporter.context.task === slug) count++;
2680
- return count;
2681
- }
2682
2675
  /** A única run ativa, quando há exatamente uma — o que `hobots.*` usa fora de um contexto async. */
2683
2676
  singleActive() {
2684
2677
  if (this.reporters.size !== 1) return void 0;
@@ -2767,40 +2760,13 @@ var DEFAULT_RETRIES2 = 1;
2767
2760
  var DEFAULT_RETRY_DELAY_MS2 = 5e3;
2768
2761
  var DEFAULT_CANCEL_GRACE_MS = 1e4;
2769
2762
  var MAX_CONCURRENCY = 5;
2770
- var MAX_SLUG_CHARS = 255;
2771
- var PROCESS_LEVEL_KEYS = [
2772
- "clientSecret",
2773
- "instanceId",
2774
- "environment",
2775
- "release",
2776
- "tags",
2777
- "debug",
2778
- "machineMetrics",
2779
- "captureUnhandled",
2780
- "installExitHandlers"
2781
- ];
2782
2763
  var runtime;
2783
2764
  var warnedNoRuntime = false;
2784
2765
  function hostname() {
2785
2766
  if (!isNode()) return void 0;
2786
2767
  return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
2787
2768
  }
2788
- function sameValue(a, b) {
2789
- if (a === b) return true;
2790
- if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
2791
- return false;
2792
- }
2793
- function ensureRuntime(options) {
2794
- if (runtime) {
2795
- for (const key of PROCESS_LEVEL_KEYS) {
2796
- if (options[key] !== void 0 && !sameValue(options[key], runtime.options[key])) {
2797
- runtime.logger.warn(
2798
- `op\xE7\xE3o "${key}" divergente ignorada \u2014 o primeiro register()/start() define as op\xE7\xF5es do processo`
2799
- );
2800
- }
2801
- }
2802
- return runtime;
2803
- }
2769
+ function createRuntime(options, handler) {
2804
2770
  const config = resolveConfig({ clientSecret: options.clientSecret });
2805
2771
  const logger = makeLogger(options.debug ?? false);
2806
2772
  const environment = resolveEnvironmentOption(options.environment);
@@ -2808,19 +2774,19 @@ function ensureRuntime(options) {
2808
2774
  { environment, release: options.release, tags: options.tags, debug: options.debug },
2809
2775
  config
2810
2776
  );
2811
- const api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger });
2812
- runtime = {
2777
+ const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
2778
+ const rt = {
2813
2779
  options: { ...options, clientSecret: void 0 },
2814
2780
  config,
2815
2781
  client,
2816
2782
  logger,
2817
2783
  api,
2818
- registry: new TaskRegistry(),
2784
+ handler,
2819
2785
  tracker: new RunTracker(api),
2820
2786
  sampler: void 0,
2821
2787
  heartbeat: void 0,
2822
2788
  agent: void 0,
2823
- locals: /* @__PURE__ */ new Map(),
2789
+ local: void 0,
2824
2790
  instanceId: resolveInstanceId(options.instanceId),
2825
2791
  environment,
2826
2792
  pollIntervalMs: void 0,
@@ -2830,24 +2796,19 @@ function ensureRuntime(options) {
2830
2796
  uninstallGlobalHandlers: options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0,
2831
2797
  uninstallShutdown: void 0
2832
2798
  };
2833
- runtime.options.clientSecret = options.clientSecret;
2834
2799
  logger.debug("inicializado", { endpoint: config.ingestEndpoint });
2835
- return runtime;
2800
+ return rt;
2836
2801
  }
2837
- function validateTask(slug, handler, fn) {
2838
- if (typeof slug !== "string" || slug.trim().length === 0 || slug.length > MAX_SLUG_CHARS) {
2839
- throw new Error(`[hobots] ${fn}(): o slug da task precisa ser uma string de 1 a ${MAX_SLUG_CHARS} caracteres`);
2840
- }
2802
+ function validateHandler(handler, fn) {
2841
2803
  if (typeof handler !== "function") {
2842
- throw new Error(`[hobots] ${fn}('${slug}'): o handler precisa ser uma fun\xE7\xE3o`);
2804
+ throw new Error(`[hobots] ${fn}(): o handler precisa ser uma fun\xE7\xE3o \u2014 ${fn}(async (params) => { ... })`);
2843
2805
  }
2844
- return slug.trim();
2845
2806
  }
2846
2807
  function clampConcurrency(value) {
2847
2808
  if (value === void 0 || !Number.isFinite(value)) return 1;
2848
2809
  return Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(value)));
2849
2810
  }
2850
- function resolveTaskOptions(slug, options, mode, logger) {
2811
+ function resolveRunOptions(options, mode, logger) {
2851
2812
  const retries = options.retries === void 0 || !Number.isFinite(options.retries) ? DEFAULT_RETRIES2 : Math.max(0, Math.floor(options.retries));
2852
2813
  return {
2853
2814
  retries,
@@ -2857,7 +2818,7 @@ function resolveTaskOptions(slug, options, mode, logger) {
2857
2818
  logFlushIntervalMs: options.logFlushIntervalMs,
2858
2819
  cancelCheckIntervalMs: options.cancelCheckIntervalMs,
2859
2820
  cancelGraceMs: options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS,
2860
- itemColumns: normalizeItemColumns(slug, options.items, logger)
2821
+ itemColumns: normalizeItemColumns(options.items, logger)
2861
2822
  };
2862
2823
  }
2863
2824
  function instanceInfo(rt) {
@@ -2867,10 +2828,10 @@ function instanceInfo(rt) {
2867
2828
  host: hostname(),
2868
2829
  // ambiente da instância: junto do heartbeat, é como o servidor sabe onde o robô roda
2869
2830
  environment: rt.environment,
2870
- // TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
2871
- tasks: rt.registry.allSlugs(),
2831
+ // como esta instância trabalha — o painel mostra "sob demanda" / "sempre ligado"
2832
+ mode: rt.handler.mode === "remote" ? "on_demand" : "always_on",
2872
2833
  // colunas da aba "itens" declaradas no código — a plataforma usa enquanto o usuário não personalizar
2873
- task_items: rt.registry.itemDeclarations(),
2834
+ items: rt.handler.options.itemColumns ? { attributes: rt.handler.options.itemColumns } : void 0,
2874
2835
  // o servidor calcula a tolerância de offline (3×) a partir disto em agentes que fazem
2875
2836
  // poll; clampado à faixa aceita pelo schema (1s..15min)
2876
2837
  poll_interval_ms: rt.agent ? Math.min(Math.max(rt.agent.pollIntervalMs, 1e3), 9e5) : void 0,
@@ -2883,22 +2844,22 @@ function ensureAgent(rt) {
2883
2844
  api: rt.api,
2884
2845
  client: rt.client,
2885
2846
  logger: rt.logger,
2886
- registry: rt.registry,
2847
+ handler: rt.handler,
2887
2848
  tracker: rt.tracker,
2888
2849
  instanceInfo: () => instanceInfo(rt),
2889
2850
  pollIntervalMs: rt.pollIntervalMs
2890
2851
  });
2891
2852
  return rt.agent;
2892
2853
  }
2893
- function launchLocal(rt, task) {
2894
- const runner = new LocalRunner(task, {
2854
+ function launchLocal(rt) {
2855
+ const runner = new LocalRunner(rt.handler, {
2895
2856
  api: rt.api,
2896
2857
  client: rt.client,
2897
2858
  logger: rt.logger,
2898
2859
  tracker: rt.tracker,
2899
2860
  instanceInfo: () => instanceInfo(rt)
2900
2861
  });
2901
- rt.locals.set(task.slug, runner);
2862
+ rt.local = runner;
2902
2863
  void runner.run();
2903
2864
  }
2904
2865
  async function boot(rt) {
@@ -2920,8 +2881,8 @@ async function boot(rt) {
2920
2881
  if (rt.options.installExitHandlers !== false) {
2921
2882
  rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
2922
2883
  }
2923
- if (rt.registry.remoteTasks().length > 0) ensureAgent(rt).start();
2924
- for (const task of rt.registry.localTasks()) launchLocal(rt, task);
2884
+ if (rt.handler.mode === "remote") ensureAgent(rt).start();
2885
+ else launchLocal(rt);
2925
2886
  }
2926
2887
  function scheduleBoot(rt) {
2927
2888
  if (rt.bootScheduled) return;
@@ -2930,34 +2891,23 @@ function scheduleBoot(rt) {
2930
2891
  rt.bootPromise = boot(rt).catch((error) => rt.logger.error("falha ao iniciar o SDK:", error));
2931
2892
  });
2932
2893
  }
2933
- function addTask(fn, mode, task, handler, options) {
2934
- const slug = validateTask(task, handler, fn);
2935
- const rt = ensureRuntime(options);
2936
- if (mode === "remote" && options.pollIntervalMs !== void 0) {
2937
- if (rt.pollIntervalMs === void 0 && !rt.agent) rt.pollIntervalMs = options.pollIntervalMs;
2938
- else if (rt.pollIntervalMs !== options.pollIntervalMs) {
2939
- rt.logger.warn('op\xE7\xE3o "pollIntervalMs" divergente ignorada \u2014 o primeiro register() define o intervalo do poll');
2940
- }
2941
- }
2942
- const registered = { slug, mode, handler, options: resolveTaskOptions(slug, options, mode, rt.logger) };
2943
- rt.registry.add(registered);
2944
- if (!rt.booted) {
2945
- scheduleBoot(rt);
2946
- return;
2947
- }
2948
- if (mode === "remote") {
2949
- const agent = ensureAgent(rt);
2950
- agent.start();
2951
- agent.wake();
2952
- } else {
2953
- launchLocal(rt, registered);
2894
+ function startAgent(fn, mode, handler, options) {
2895
+ validateHandler(handler, fn);
2896
+ if (runtime) {
2897
+ throw new Error(`[hobots] ${fn}(): o agente j\xE1 foi iniciado \u2014 register()/start() s\xF3 pode ser chamado uma vez por processo (chame hobots.close() antes de iniciar de novo)`);
2954
2898
  }
2899
+ const logger = makeLogger(options.debug ?? false);
2900
+ const agentHandler = { mode, handler, options: resolveRunOptions(options, mode, logger) };
2901
+ const rt = createRuntime(options, agentHandler);
2902
+ if (mode === "remote" && options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
2903
+ runtime = rt;
2904
+ scheduleBoot(rt);
2955
2905
  }
2956
- function register(task, handler, options = {}) {
2957
- addTask("register", "remote", task, handler, options);
2906
+ function register(handler, options = {}) {
2907
+ startAgent("register", "remote", handler, options);
2958
2908
  }
2959
- function start(task, handler, options = {}) {
2960
- addTask("start", "local", task, handler, options);
2909
+ function start(handler, options = {}) {
2910
+ startAgent("start", "local", handler, options);
2961
2911
  }
2962
2912
  function currentReporter() {
2963
2913
  return currentRunReporter() ?? runtime?.tracker.singleActive();
@@ -2971,7 +2921,7 @@ function toRunLogLevel(type) {
2971
2921
  function forwardAttachment(rt, attachment, message, type) {
2972
2922
  const reporter = currentReporter();
2973
2923
  if (!reporter) {
2974
- rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task");
2924
+ rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o");
2975
2925
  return;
2976
2926
  }
2977
2927
  const withMeta = typeof attachment === "object" && !isImageAttachment(attachment) && "image" in attachment ? image(attachment.image, { caption: attachment.caption, contentType: attachment.contentType }) : attachment;
@@ -2982,7 +2932,7 @@ function captureException(error, hint) {
2982
2932
  if (!rt) {
2983
2933
  if (!warnedNoRuntime) {
2984
2934
  warnedNoRuntime = true;
2985
- console.warn("[hobots] captureException antes de registrar uma task (register/start) \u2014 evento descartado");
2935
+ console.warn("[hobots] captureException antes de iniciar o agente (register/start) \u2014 evento descartado");
2986
2936
  }
2987
2937
  return void 0;
2988
2938
  }
@@ -2998,7 +2948,7 @@ async function close(timeoutMs = 3e4) {
2998
2948
  runtime = void 0;
2999
2949
  await rt.bootPromise;
3000
2950
  if (rt.agent) await rt.agent.stop(0);
3001
- for (const local of rt.locals.values()) local.stop();
2951
+ rt.local?.stop();
3002
2952
  const idle = await rt.tracker.waitIdle(timeoutMs);
3003
2953
  if (!idle) {
3004
2954
  rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
@@ -3016,15 +2966,14 @@ var warned = /* @__PURE__ */ new Set();
3016
2966
  function warnOutsideRun(member) {
3017
2967
  if (warned.has(member)) return;
3018
2968
  warned.add(member);
3019
- 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)`;
2969
+ const message = `hobots.${member} usado fora de uma execu\xE7\xE3o \u2014 ignorado (inicie o agente com register()/start() e use dentro do handler)`;
3020
2970
  const logger = activeLogger();
3021
2971
  if (logger) logger.warn(message);
3022
2972
  else console.warn(`[hobots] ${message}`);
3023
2973
  }
3024
- var FALLBACK_TASK = "hobots";
3025
2974
  var fallbackLogAt = (level) => (message, ...rest) => {
3026
2975
  const { text, image: dropped } = splitLogArgs(message, rest);
3027
- printRunLog(FALLBACK_TASK, level, dropped ? `${text} [imagem descartada]` : text);
2976
+ printRunLog(level, dropped ? `${text} [imagem descartada]` : text);
3028
2977
  };
3029
2978
  var fallbackLog = {
3030
2979
  debug: fallbackLogAt("debug"),
@@ -3041,7 +2990,7 @@ var fallbackEachSink = {
3041
2990
  advance: noop,
3042
2991
  checkCanceled: noop,
3043
2992
  isAborted: () => false,
3044
- logError: (message) => printRunLog(FALLBACK_TASK, "error", message)
2993
+ logError: (message) => printRunLog("error", message)
3045
2994
  };
3046
2995
  var fallbackItems = {
3047
2996
  add: noop,
@@ -3068,7 +3017,6 @@ function method(value) {
3068
3017
  var hobots = Object.defineProperties({}, {
3069
3018
  // ── contexto da run atual (getters) ─────────────────────────────────────────
3070
3019
  runId: getter("runId", () => ""),
3071
- task: getter("task", () => ""),
3072
3020
  params: getter("params", () => ({})),
3073
3021
  log: getter("log", () => fallbackLog),
3074
3022
  items: getter("items", () => fallbackItems),
@@ -3077,17 +3025,18 @@ var hobots = Object.defineProperties({}, {
3077
3025
  signal: getter("signal", () => neverAborts),
3078
3026
  throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
3079
3027
  sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
3028
+ flush: method((timeoutMs) => resolve("flush")?.flush(timeoutMs) ?? Promise.resolve()),
3080
3029
  retry: method((fn, options) => {
3081
3030
  const reporter = currentReporter();
3082
3031
  return reporter ? reporter.context.retry(fn, options) : retryWithBackoff(fn, options);
3083
3032
  }),
3084
3033
  // ── processo ────────────────────────────────────────────────────────────────
3085
- register: method((task, handler, options) => {
3086
- register(task, handler, options);
3034
+ register: method((handler, options) => {
3035
+ register(handler, options);
3087
3036
  installGlobalHobots();
3088
3037
  }),
3089
- start: method((task, handler, options) => {
3090
- start(task, handler, options);
3038
+ start: method((handler, options) => {
3039
+ start(handler, options);
3091
3040
  installGlobalHobots();
3092
3041
  }),
3093
3042
  close: method((timeoutMs) => close(timeoutMs)),