hobots 3.0.0 → 3.2.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/README.md CHANGED
@@ -27,6 +27,7 @@ CommonJS: `const { hobots } = require('hobots')`.
27
27
  ```bash
28
28
  export HOBOTS_CLIENT_SECRET=hb_<64 hex> # Configurações → Agentes
29
29
  export HOBOTS_INSTANCE_ID=vm-cliente-01 # opcional — default: hostname da máquina
30
+ export HOBOTS_ENVIRONMENT=staging # opcional — default: production
30
31
  ```
31
32
 
32
33
  ```js
@@ -65,6 +66,7 @@ O objeto **`hobots`** é global: `hobots.log`, `hobots.items`, `hobots.progress`
65
66
  | --- | --- | --- |
66
67
  | `HOBOTS_CLIENT_SECRET` | `clientSecret` | O **Client Secret do agente** (`hb_<64 hex>`), copiado em **Configurações → Agentes**. Viaja só no header `Authorization: Bearer`, nunca na URL. Ausente ou inválido → `register()`/`start()` lança na hora, citando a env (sem ecoar o valor). |
67
68
  | `HOBOTS_INSTANCE_ID` | `instanceId` | Identidade **estável** desta instância do robô (`vm-cliente-01`). Sem ela, o hostname. Heartbeat e tasks convergem nela: **um agente só** no painel. |
69
+ | `HOBOTS_ENVIRONMENT` | `environment` | Ambiente lógico do robô (`development` \| `testing` \| `staging` \| `production`). Sem ela, `production`. |
68
70
  | `HOBOTS_API_URL` | — | Só em desenvolvimento: aponta o SDK para outro host (`http://localhost:3001`). Em produção é sempre `https://api2.hobots.app`. |
69
71
 
70
72
  A opção passada em `register()`/`start()` tem precedência sobre a env — útil para copiar e colar o snippet do app.
@@ -91,8 +93,8 @@ Os dois podem coexistir no mesmo processo: um monitor sempre ligado + tasks sob
91
93
 
92
94
  | Membro | O que faz |
93
95
  | --- | --- |
94
- | `hobots.log.debug/info/warn/error/success(msg)` | Log da execução, enviado incrementalmente ao app (e ecoado no terminal). **Nunca lança** — pode ir em `finally`. |
95
- | `hobots.items.succeeded(payload?, { id }?)` · `.failed(msg, payload?, …)` · `.occurrence(msg, payload?, …)` · `.add({...})` | **A entrega do robô**: cada item processado, com status e atributos (viram colunas no app). Bufferizado, em lotes. |
96
+ | `hobots.log.debug/info/warn/error/success(msg, ...rest)` | Log da execução, enviado incrementalmente ao app (e ecoado no terminal). Aceita vários argumentos como `console.log` — `hobots.log.info('nota', numero, 'ok')` — cada um vira texto, unidos por espaço. **Nunca lança** — pode ir em `finally`. |
97
+ | `hobots.items.succeeded(payload?, { id }?)` · `.failed(msg, payload?, …)` · `.occurrence(msg, payload?, …)` · `.add({...})` | **A entrega do robô**: cada item processado, com status e atributos (viram colunas no app — declare-as com a opção `items: { columns: [...] }` do `register`/`start`). Bufferizado, em lotes. |
96
98
  | `hobots.progress.total(n)` · `.advance(k = 1)` · `.set(atual, total?)` · `.current` · `.snapshot` | Barra, percentual e tempo restante na solicitação. Sem `total()` nada é enviado. |
97
99
  | `hobots.transaction.startChild(op, descrição)` | Uma **step** no waterfall de performance da execução. A transaction da execução já existe e é finalizada pelo SDK. |
98
100
  | `hobots.attach(imagem, { caption?, level? })` | Pendura um **print** (PNG/JPEG/WEBP, até 5 MB, 20 por execução) na linha de log. Nunca lança nem bloqueia. |
@@ -143,7 +145,7 @@ hobots.start(task, handler, options?: StartOptions);
143
145
  | --- | --- | --- |
144
146
  | `clientSecret` | env `HOBOTS_CLIENT_SECRET` | Client Secret do agente |
145
147
  | `instanceId` | env `HOBOTS_INSTANCE_ID` → hostname | Identidade da instância |
146
- | `environment` | `'production'` | `development` \| `testing` \| `staging` \| `production` — decide quem é alertado |
148
+ | `environment` | env `HOBOTS_ENVIRONMENT` → `'production'` | `development` \| `testing` \| `staging` \| `production` — decide quem é alertado |
147
149
  | `release` | — | Versão do programa, ex. `robo-nfe@1.4.2` |
148
150
  | `tags` | — | Tags de todos os eventos |
149
151
  | `debug` | `false` | Loga a atividade do SDK |
@@ -176,6 +178,10 @@ Sem handler próprio de sinal, um `SIGINT`/`SIGTERM` (ou erro fatal) com execuç
176
178
 
177
179
  ---
178
180
 
181
+ ## Changelog resumido
182
+
183
+ - **3.1.0** — `hobots.log.*` aceita vários argumentos (como `console.log`); `retries` inválido (`NaN`) cai no default em vez de tentar para sempre; 429 de itens e 429/501/503 de anexo são definitivos (sem esperar a ladder — `quota`/`unsupported` passam a funcionar de fato); timeout **por requisição** nos anexos; truncamento nos limites do servidor (tag 512, fingerprint 10×255, nome de transaction 500, 200 steps, `exception.value` 8192, `filename` 1024); PNG acima de 20 000 px não declara dimensões; PATCH terminal nunca passa de 100 logs.
184
+
179
185
  ## Documentação
180
186
 
181
187
  - [Getting started](./docs/getting-started.md) — instalação, envs, primeira task nos dois modos.
package/dist/index.cjs CHANGED
@@ -42,6 +42,7 @@ module.exports = __toCommonJS(src_exports);
42
42
 
43
43
  // src/stacktrace.ts
44
44
  var MAX_FRAMES = 50;
45
+ var MAX_FILENAME_CHARS = 1024;
45
46
  var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
46
47
  var LOCATION_RE = /^(.*?):(\d+):(\d+)$/;
47
48
  function isInApp(filename) {
@@ -64,7 +65,7 @@ function parseStacktrace(stack) {
64
65
  const filename = loc ? loc[1] : location;
65
66
  frames.push({
66
67
  function: fn || "<anonymous>",
67
- filename: filename || void 0,
68
+ filename: filename ? filename.slice(0, MAX_FILENAME_CHARS) : void 0,
68
69
  lineno: loc?.[2] ? Number(loc[2]) : void 0,
69
70
  colno: loc?.[3] ? Number(loc[3]) : void 0,
70
71
  in_app: isInApp(filename)
@@ -94,7 +95,7 @@ function scrubSecrets(text) {
94
95
 
95
96
  // src/utils.ts
96
97
  var SDK_NAME = "hobots";
97
- var SDK_VERSION = true ? "3.0.0" : "0.0.0-dev";
98
+ var SDK_VERSION = true ? "3.2.0" : "0.0.0-dev";
98
99
  function isNode() {
99
100
  return typeof process !== "undefined" && typeof process.versions?.node === "string";
100
101
  }
@@ -150,9 +151,19 @@ function jitter(baseMs) {
150
151
  function truncate(value, max) {
151
152
  return value.length > max ? `${value.slice(0, max)}\u2026 [truncado]` : value;
152
153
  }
154
+ function safeString(value) {
155
+ if (typeof value === "string") return value;
156
+ try {
157
+ const json = JSON.stringify(value);
158
+ if (json && json !== "{}") return json;
159
+ } catch {
160
+ }
161
+ return String(value);
162
+ }
153
163
  var DEFAULT_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
154
164
  async function fetchWithRetry(send, options) {
155
165
  const delays = options.delays ?? DEFAULT_RETRY_DELAYS_MS;
166
+ const noRetry = options.noRetryStatuses ?? [];
156
167
  for (let attempt = 0; ; attempt++) {
157
168
  let retriable;
158
169
  let serverDelay;
@@ -160,6 +171,7 @@ async function fetchWithRetry(send, options) {
160
171
  const response = await send();
161
172
  if (response.ok) return response;
162
173
  if (response.status >= 400 && response.status < 500 && response.status !== 429) return response;
174
+ if (noRetry.includes(response.status)) return response;
163
175
  if (response.status === 429) serverDelay = retryAfterMs(response);
164
176
  retriable = new Error(`HTTP ${response.status}`);
165
177
  } catch (error) {
@@ -232,26 +244,18 @@ function colorize(text, color) {
232
244
  }
233
245
 
234
246
  // src/eventbuilder.ts
235
- function safeString(value) {
236
- if (typeof value === "string") return value;
237
- try {
238
- const json = JSON.stringify(value);
239
- if (json && json !== "{}") return json;
240
- } catch {
241
- }
242
- return String(value);
243
- }
247
+ var MAX_EXCEPTION_VALUE_CHARS = 8192;
244
248
  function eventFromException(error, type = "error") {
245
249
  let exceptionType;
246
250
  let value;
247
251
  let frames = [];
248
252
  if (error instanceof Error) {
249
253
  exceptionType = error.name || "Error";
250
- value = scrubSecrets(error.message);
254
+ value = truncate(scrubSecrets(error.message), MAX_EXCEPTION_VALUE_CHARS);
251
255
  frames = parseStacktrace(error.stack);
252
256
  } else {
253
257
  exceptionType = "NonError";
254
- value = scrubSecrets(safeString(error));
258
+ value = truncate(scrubSecrets(safeString(error)), MAX_EXCEPTION_VALUE_CHARS);
255
259
  }
256
260
  return {
257
261
  event_id: uuid4(),
@@ -377,6 +381,18 @@ var Transport = class {
377
381
  };
378
382
 
379
383
  // src/client.ts
384
+ var MAX_TAG_VALUE_CHARS = 512;
385
+ var MAX_FINGERPRINT_ITEMS = 10;
386
+ var MAX_FINGERPRINT_ITEM_CHARS = 255;
387
+ var MAX_TRANSACTION_NAME_CHARS = 500;
388
+ var MAX_STEPS = 200;
389
+ function clipTags(tags) {
390
+ return Object.fromEntries(Object.entries(tags).map(([key, value]) => [key, truncate(String(value), MAX_TAG_VALUE_CHARS)]));
391
+ }
392
+ function clipFingerprint(fingerprint) {
393
+ if (fingerprint === void 0) return void 0;
394
+ return fingerprint.slice(0, MAX_FINGERPRINT_ITEMS).map((item) => truncate(String(item), MAX_FINGERPRINT_ITEM_CHARS));
395
+ }
380
396
  function runtimeContexts() {
381
397
  if (isNode()) {
382
398
  return {
@@ -445,20 +461,27 @@ var Client = class {
445
461
  type: hint?.type ?? event.type ?? "error",
446
462
  release: event.release ?? this.options.release,
447
463
  environment: this.resolveEnvironment(event.environment),
448
- tags: Object.keys(tags).length > 0 ? tags : void 0,
464
+ tags: Object.keys(tags).length > 0 ? clipTags(tags) : void 0,
449
465
  extra: Object.keys(extra).length > 0 ? extra : void 0,
450
466
  contexts: { ...runtimeContexts(), ...machine, ...event.contexts },
451
- fingerprint: hint?.fingerprint ?? event.fingerprint
467
+ fingerprint: clipFingerprint(hint?.fingerprint ?? event.fingerprint)
452
468
  };
453
469
  }
454
470
  sendTransaction(payload) {
471
+ let steps = payload.steps;
472
+ if (steps.length > MAX_STEPS) {
473
+ this.logger.warn(`transaction com ${steps.length} steps \u2014 enviando as primeiras ${MAX_STEPS}`);
474
+ steps = steps.slice(0, MAX_STEPS);
475
+ }
455
476
  this.transport.enqueue({
456
477
  type: "transaction",
457
478
  payload: {
458
479
  ...payload,
480
+ name: truncate(payload.name || "(sem nome)", MAX_TRANSACTION_NAME_CHARS),
481
+ steps,
459
482
  release: payload.release ?? this.options.release,
460
483
  environment: this.resolveEnvironment(payload.environment),
461
- tags: { ...this.options.tags, ...payload.tags }
484
+ tags: clipTags({ ...this.options.tags, ...payload.tags })
462
485
  }
463
486
  });
464
487
  }
@@ -551,12 +574,19 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
551
574
 
552
575
  // src/env.ts
553
576
  var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
577
+ var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
554
578
  function resolveInstanceId(option) {
555
579
  const fromOption = option?.trim();
556
580
  if (fromOption) return fromOption;
557
581
  if (typeof process === "undefined" || !process.env) return void 0;
558
582
  return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
559
583
  }
584
+ function resolveEnvironmentOption(option) {
585
+ const fromOption = option?.trim();
586
+ if (fromOption) return fromOption;
587
+ if (typeof process === "undefined" || !process.env) return void 0;
588
+ return process.env[ENVIRONMENT_ENV]?.trim() || void 0;
589
+ }
560
590
  async function fallbackInstanceId() {
561
591
  if (!isNode()) return "browser";
562
592
  try {
@@ -635,6 +665,11 @@ function externalListenerCount(event) {
635
665
  var emergencyTasks = [];
636
666
  function addEmergencyTask(task) {
637
667
  emergencyTasks.push(task);
668
+ const forget = () => {
669
+ const index = emergencyTasks.indexOf(task);
670
+ if (index !== -1) emergencyTasks.splice(index, 1);
671
+ };
672
+ task.then(forget, forget);
638
673
  }
639
674
  async function awaitEmergencyTasks(capMs) {
640
675
  if (emergencyTasks.length === 0) return;
@@ -829,6 +864,8 @@ var MachineSampler = class {
829
864
  // src/tasks/api.ts
830
865
  var POLL_TIMEOUT_MS = 3e4;
831
866
  var UPDATE_TIMEOUT_MS = 1e4;
867
+ var ATTACHMENT_TERMINAL_STATUSES = [429, 501, 503];
868
+ var ITEMS_TERMINAL_STATUSES = [429];
832
869
  var UPLOAD_TIMEOUT_MS = 3e4;
833
870
  var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
834
871
  var TasksApiError = class extends Error {
@@ -838,6 +875,14 @@ var TasksApiError = class extends Error {
838
875
  this.name = "TasksApiError";
839
876
  }
840
877
  };
878
+ async function withRequestTimeout(timeoutMs, external, send) {
879
+ const { signal, cleanup } = withTimeoutSignal(timeoutMs, external);
880
+ try {
881
+ return await send(signal);
882
+ } finally {
883
+ cleanup();
884
+ }
885
+ }
841
886
  var TasksApi = class {
842
887
  baseUrl;
843
888
  key;
@@ -974,12 +1019,9 @@ var TasksApi = class {
974
1019
  body: JSON.stringify(request),
975
1020
  signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
976
1021
  }),
977
- { logger: this.logger, label: `itens da run ${runId}` }
1022
+ { logger: this.logger, label: `itens da run ${runId}`, noRetryStatuses: ITEMS_TERMINAL_STATUSES }
978
1023
  );
979
1024
  } catch (error) {
980
- if (error instanceof Error && error.message === "HTTP 429") {
981
- throw new TasksApiError("envio de itens rejeitado (HTTP 429)", 429);
982
- }
983
1025
  throw new Error(`envio de itens da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
984
1026
  }
985
1027
  if (!response.ok) {
@@ -996,21 +1038,19 @@ var TasksApi = class {
996
1038
  * "esta instalação não tem anexos" — o RunReporter degrada em silêncio a partir daí.
997
1039
  */
998
1040
  async createAttachment(runId, request, signal) {
999
- const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
1000
- let response;
1001
- try {
1002
- response = await fetchWithRetry(
1003
- () => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments`, {
1041
+ const response = await fetchWithRetry(
1042
+ () => withRequestTimeout(
1043
+ UPDATE_TIMEOUT_MS,
1044
+ signal,
1045
+ (timeoutSignal) => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments`, {
1004
1046
  method: "POST",
1005
1047
  headers: this.headers(),
1006
1048
  body: JSON.stringify(request),
1007
1049
  signal: timeoutSignal
1008
- }),
1009
- { logger: this.logger, label: `anexo da run ${runId}` }
1010
- );
1011
- } finally {
1012
- cleanup();
1013
- }
1050
+ })
1051
+ ),
1052
+ { logger: this.logger, label: `anexo da run ${runId}`, noRetryStatuses: ATTACHMENT_TERMINAL_STATUSES }
1053
+ );
1014
1054
  if (!response.ok) {
1015
1055
  throw new TasksApiError(`cria\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
1016
1056
  }
@@ -1030,22 +1070,20 @@ var TasksApi = class {
1030
1070
  * está na assinatura da URL, e mandar o client secret para fora da API seria vazamento.
1031
1071
  */
1032
1072
  async uploadAttachment(url, headers, body, signal) {
1033
- const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPLOAD_TIMEOUT_MS, signal);
1034
- let response;
1035
- try {
1036
- response = await fetchWithRetry(
1037
- () => fetch(url, {
1073
+ const response = await fetchWithRetry(
1074
+ () => withRequestTimeout(
1075
+ UPLOAD_TIMEOUT_MS,
1076
+ signal,
1077
+ (timeoutSignal) => fetch(url, {
1038
1078
  method: "PUT",
1039
1079
  headers,
1040
1080
  // BodyInit não aceita Uint8Array<ArrayBufferLike> em todos os lib.dom; o buffer é o mesmo
1041
1081
  body,
1042
1082
  signal: timeoutSignal
1043
- }),
1044
- { delays: UPLOAD_RETRY_DELAYS_MS, logger: this.logger, label: "upload de anexo" }
1045
- );
1046
- } finally {
1047
- cleanup();
1048
- }
1083
+ })
1084
+ ),
1085
+ { delays: UPLOAD_RETRY_DELAYS_MS, logger: this.logger, label: "upload de anexo" }
1086
+ );
1049
1087
  if (!response.ok) {
1050
1088
  throw new TasksApiError(`upload de anexo rejeitado (HTTP ${response.status})`, response.status);
1051
1089
  }
@@ -1083,10 +1121,12 @@ var Backoff = class {
1083
1121
  this.authFailures = 0;
1084
1122
  }
1085
1123
  next(error) {
1086
- if (error instanceof TasksApiError && error.status === 401) {
1124
+ if (error instanceof TasksApiError && (error.status === 401 || error.status === 402)) {
1087
1125
  this.authFailures++;
1088
1126
  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");
1127
+ this.logger.error(
1128
+ error.status === 402 ? "agente fora do limite do plano (402) \u2014 fa\xE7a upgrade em Plano e cobran\xE7a para reativ\xE1-lo" : "chave rejeitada (401) \u2014 verifique o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes"
1129
+ );
1090
1130
  }
1091
1131
  return Math.max(this.floorMs, AUTH_FAILURE_INTERVAL_MS);
1092
1132
  }
@@ -1120,7 +1160,8 @@ function resolveDelay(delay, attempt, fallbackMs) {
1120
1160
  return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1121
1161
  }
1122
1162
  async function retryWithBackoff(fn, options = {}) {
1123
- const retries = Math.max(0, Math.floor(options.retries ?? DEFAULT_RETRIES));
1163
+ const requested = options.retries ?? DEFAULT_RETRIES;
1164
+ const retries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_RETRIES;
1124
1165
  const wait = options.wait ?? sleep;
1125
1166
  for (let attempt = 1; ; attempt++) {
1126
1167
  if (options.signal?.aborted) throw runCanceledError();
@@ -1155,6 +1196,7 @@ function startsWith(bytes, signature, offset = 0) {
1155
1196
  return signature.every((byte, index) => bytes[offset + index] === byte);
1156
1197
  }
1157
1198
  var PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
1199
+ var MAX_PNG_DIMENSION = 2e4;
1158
1200
  var JPEG_MAGIC = [255, 216, 255];
1159
1201
  var RIFF_MAGIC = [82, 73, 70, 70];
1160
1202
  var WEBP_MAGIC = [87, 69, 66, 80];
@@ -1170,6 +1212,7 @@ function readPngDimensions(bytes) {
1170
1212
  const width = view.getUint32(16);
1171
1213
  const height = view.getUint32(20);
1172
1214
  if (width === 0 || height === 0) return void 0;
1215
+ if (width > MAX_PNG_DIMENSION || height > MAX_PNG_DIMENSION) return void 0;
1173
1216
  return { width, height };
1174
1217
  }
1175
1218
  async function toBytes(input) {
@@ -1186,6 +1229,10 @@ var LOG_LEVEL_COLOR = {
1186
1229
  error: "red",
1187
1230
  success: "green"
1188
1231
  };
1232
+ function joinLogArgs(message, rest) {
1233
+ if (rest.length === 0) return safeString(message);
1234
+ return [message, ...rest].map(safeString).join(" ");
1235
+ }
1189
1236
  function printRunLog(task, level, message) {
1190
1237
  const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
1191
1238
  const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
@@ -1243,11 +1290,11 @@ var RunReporter = class {
1243
1290
  Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
1244
1291
  );
1245
1292
  const log = {
1246
- debug: (message) => this.log("debug", message),
1247
- info: (message) => this.log("info", message),
1248
- warn: (message) => this.log("warning", message),
1249
- error: (message) => this.log("error", message),
1250
- success: (message) => this.log("success", message)
1293
+ debug: (message, ...rest) => this.log("debug", joinLogArgs(message, rest)),
1294
+ info: (message, ...rest) => this.log("info", joinLogArgs(message, rest)),
1295
+ warn: (message, ...rest) => this.log("warning", joinLogArgs(message, rest)),
1296
+ error: (message, ...rest) => this.log("error", joinLogArgs(message, rest)),
1297
+ success: (message, ...rest) => this.log("success", joinLogArgs(message, rest))
1251
1298
  };
1252
1299
  tx.setLifecycleSink((level, message) => this.push(level, message));
1253
1300
  const checkCanceled = () => this.checkCanceled();
@@ -1397,7 +1444,7 @@ var RunReporter = class {
1397
1444
  }
1398
1445
  log(level, message) {
1399
1446
  if (this.dead) return;
1400
- const scrubbed = scrubSecrets(message);
1447
+ const scrubbed = scrubSecrets(safeString(message));
1401
1448
  printRunLog(this.context.task, level, scrubbed);
1402
1449
  this.push(level, scrubbed, true);
1403
1450
  }
@@ -1456,13 +1503,25 @@ var RunReporter = class {
1456
1503
  this.stopCancelWatch();
1457
1504
  this.clearFlushTimer();
1458
1505
  this.forceFinalProgress();
1459
- this.enqueue({ status: "succeeded", logs: this.takeLogs() });
1506
+ this.enqueue({ status: "succeeded", logs: this.takeLogsForTerminal() });
1460
1507
  }
1461
1508
  reportFailure(error) {
1462
1509
  this.stopCancelWatch();
1463
1510
  this.clearFlushTimer();
1464
1511
  this.forceFinalProgress();
1465
- this.enqueue({ status: "failed", error, logs: this.takeLogs() });
1512
+ this.enqueue({ status: "failed", error, logs: this.takeLogsForTerminal() });
1513
+ }
1514
+ /**
1515
+ * Esvazia o buffer para o PATCH terminal respeitando o teto de 100 logs por PATCH:
1516
+ * o excedente sai antes, em PATCHes `running` na mesma cadeia (a ordem se mantém).
1517
+ */
1518
+ takeLogsForTerminal() {
1519
+ let logs = this.takeLogs();
1520
+ while (logs.length > MAX_LOGS_PER_PATCH) {
1521
+ this.enqueue({ status: "running", logs: logs.slice(0, MAX_LOGS_PER_PATCH) });
1522
+ logs = logs.slice(MAX_LOGS_PER_PATCH);
1523
+ }
1524
+ return logs;
1466
1525
  }
1467
1526
  // ── Progresso ────────────────────────────────────────────────────────────────
1468
1527
  /** Valida, clampa e marca o snapshot como pendente; agenda flush só se nada mais vai sair. */
@@ -1570,7 +1629,7 @@ var RunReporter = class {
1570
1629
  id: id ? truncate(String(id), 255) : uuid4(),
1571
1630
  seq: this.itemSeq++,
1572
1631
  status,
1573
- ...message ? { message: truncate(scrubSecrets(message), MAX_ITEM_MESSAGE_CHARS) } : {},
1632
+ ...message ? { message: truncate(scrubSecrets(safeString(message)), MAX_ITEM_MESSAGE_CHARS) } : {},
1574
1633
  ...scrubbedPayload ? { payload: scrubbedPayload } : {},
1575
1634
  ts: nowISO()
1576
1635
  });
@@ -1659,8 +1718,10 @@ var RunReporter = class {
1659
1718
  async attach(image, options = {}) {
1660
1719
  const id = uuid4();
1661
1720
  const level = options.level ?? "info";
1662
- const caption = options.caption ? truncate(scrubSecrets(options.caption), MAX_CAPTION_CHARS) : void 0;
1663
- const message = options.message ?? options.caption ?? "captura de tela";
1721
+ const rawCaption = options.caption !== void 0 ? safeString(options.caption) : void 0;
1722
+ const rawMessage = options.message !== void 0 ? safeString(options.message) : void 0;
1723
+ const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
1724
+ const message = rawMessage ?? rawCaption ?? "captura de tela";
1664
1725
  if (this.dead) return { id, ok: false, reason: "canceled" };
1665
1726
  if (this.attachmentsUnsupported) {
1666
1727
  this.log(level, message);
@@ -2280,6 +2341,42 @@ var TaskAgent = class {
2280
2341
  }
2281
2342
  };
2282
2343
 
2344
+ // src/tasks/item-columns.ts
2345
+ var KEY_RE = /^[a-zA-Z0-9_.-]{1,100}$/;
2346
+ var MAX_COLUMNS = 12;
2347
+ var MAX_LABEL_CHARS = 60;
2348
+ function normalizeItemColumns(slug, input, logger) {
2349
+ if (input === void 0 || input === null) return void 0;
2350
+ const columns = input.columns;
2351
+ if (typeof input !== "object" || !Array.isArray(columns)) {
2352
+ logger.warn(`task "${slug}": items.columns precisa ser uma lista \u2014 declara\xE7\xE3o de colunas ignorada`);
2353
+ return void 0;
2354
+ }
2355
+ const result = [];
2356
+ const seen = /* @__PURE__ */ new Set();
2357
+ for (const entry of columns) {
2358
+ const rawKey = typeof entry === "string" ? entry : entry?.key;
2359
+ const rawLabel = typeof entry === "object" && entry !== null ? entry.label : void 0;
2360
+ const key = typeof rawKey === "string" ? rawKey.trim() : "";
2361
+ if (!KEY_RE.test(key)) {
2362
+ logger.warn(`task "${slug}": coluna de item inv\xE1lida ignorada (${JSON.stringify(rawKey)}) \u2014 use letras, n\xFAmeros, ponto, h\xEDfen e underline`);
2363
+ continue;
2364
+ }
2365
+ if (seen.has(key)) {
2366
+ logger.warn(`task "${slug}": coluna de item "${key}" repetida ignorada`);
2367
+ continue;
2368
+ }
2369
+ if (result.length >= MAX_COLUMNS) {
2370
+ logger.warn(`task "${slug}": no m\xE1ximo ${MAX_COLUMNS} colunas de item \u2014 "${key}" em diante ignoradas`);
2371
+ break;
2372
+ }
2373
+ seen.add(key);
2374
+ const label = typeof rawLabel === "string" ? rawLabel.trim().slice(0, MAX_LABEL_CHARS) : "";
2375
+ result.push(label ? { key, label } : { key });
2376
+ }
2377
+ return result.length > 0 ? result : void 0;
2378
+ }
2379
+
2283
2380
  // src/tasks/local.ts
2284
2381
  var LocalRunner = class {
2285
2382
  constructor(task, deps) {
@@ -2358,6 +2455,15 @@ var TaskRegistry = class {
2358
2455
  allSlugs() {
2359
2456
  return [...this.tasks.keys()];
2360
2457
  }
2458
+ /** Colunas de itens declaradas no código, por slug (`instance.task_items`); undefined se nenhuma task declarou. */
2459
+ itemDeclarations() {
2460
+ let result;
2461
+ for (const task of this.tasks.values()) {
2462
+ if (!task.options.itemColumns) continue;
2463
+ (result ??= {})[task.slug] = { attributes: task.options.itemColumns };
2464
+ }
2465
+ return result;
2466
+ }
2361
2467
  clear() {
2362
2468
  this.tasks.clear();
2363
2469
  }
@@ -2511,8 +2617,9 @@ function ensureRuntime(options) {
2511
2617
  }
2512
2618
  const config = resolveConfig({ clientSecret: options.clientSecret });
2513
2619
  const logger = makeLogger(options.debug ?? false);
2620
+ const environment = resolveEnvironmentOption(options.environment);
2514
2621
  const client = new Client(
2515
- { environment: options.environment, release: options.release, tags: options.tags, debug: options.debug },
2622
+ { environment, release: options.release, tags: options.tags, debug: options.debug },
2516
2623
  config
2517
2624
  );
2518
2625
  const api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger });
@@ -2529,6 +2636,7 @@ function ensureRuntime(options) {
2529
2636
  agent: void 0,
2530
2637
  locals: /* @__PURE__ */ new Map(),
2531
2638
  instanceId: resolveInstanceId(options.instanceId),
2639
+ environment,
2532
2640
  pollIntervalMs: void 0,
2533
2641
  booted: false,
2534
2642
  bootScheduled: false,
@@ -2553,7 +2661,7 @@ function clampConcurrency(value) {
2553
2661
  if (value === void 0 || !Number.isFinite(value)) return 1;
2554
2662
  return Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(value)));
2555
2663
  }
2556
- function resolveTaskOptions(options, mode) {
2664
+ function resolveTaskOptions(slug, options, mode, logger) {
2557
2665
  const retries = options.retries === void 0 || !Number.isFinite(options.retries) ? DEFAULT_RETRIES2 : Math.max(0, Math.floor(options.retries));
2558
2666
  return {
2559
2667
  retries,
@@ -2562,7 +2670,8 @@ function resolveTaskOptions(options, mode) {
2562
2670
  concurrency: mode === "remote" ? clampConcurrency(options.concurrency) : 1,
2563
2671
  logFlushIntervalMs: options.logFlushIntervalMs,
2564
2672
  cancelCheckIntervalMs: options.cancelCheckIntervalMs,
2565
- cancelGraceMs: options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS
2673
+ cancelGraceMs: options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS,
2674
+ itemColumns: normalizeItemColumns(slug, options.items, logger)
2566
2675
  };
2567
2676
  }
2568
2677
  function instanceInfo(rt) {
@@ -2571,9 +2680,11 @@ function instanceInfo(rt) {
2571
2680
  version: SDK_VERSION,
2572
2681
  host: hostname(),
2573
2682
  // ambiente da instância: junto do heartbeat, é como o servidor sabe onde o robô roda
2574
- environment: rt.options.environment,
2683
+ environment: rt.environment,
2575
2684
  // TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
2576
2685
  tasks: rt.registry.allSlugs(),
2686
+ // colunas da aba "itens" declaradas no código — a plataforma usa enquanto o usuário não personalizar
2687
+ task_items: rt.registry.itemDeclarations(),
2577
2688
  // o servidor calcula a tolerância de offline (3×) a partir disto em agentes que fazem
2578
2689
  // poll; clampado à faixa aceita pelo schema (1s..15min)
2579
2690
  poll_interval_ms: rt.agent ? Math.min(Math.max(rt.agent.pollIntervalMs, 1e3), 9e5) : void 0,
@@ -2642,7 +2753,7 @@ function addTask(fn, mode, task, handler, options) {
2642
2753
  rt.logger.warn('op\xE7\xE3o "pollIntervalMs" divergente ignorada \u2014 o primeiro register() define o intervalo do poll');
2643
2754
  }
2644
2755
  }
2645
- const registered = { slug, mode, handler, options: resolveTaskOptions(options, mode) };
2756
+ const registered = { slug, mode, handler, options: resolveTaskOptions(slug, options, mode, rt.logger) };
2646
2757
  rt.registry.add(registered);
2647
2758
  if (!rt.booted) {
2648
2759
  scheduleBoot(rt);
@@ -2731,11 +2842,11 @@ function warnOutsideRun(member) {
2731
2842
  }
2732
2843
  var FALLBACK_TASK = "hobots";
2733
2844
  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)
2845
+ debug: (message, ...rest) => printRunLog(FALLBACK_TASK, "debug", joinLogArgs(message, rest)),
2846
+ info: (message, ...rest) => printRunLog(FALLBACK_TASK, "info", joinLogArgs(message, rest)),
2847
+ warn: (message, ...rest) => printRunLog(FALLBACK_TASK, "warning", joinLogArgs(message, rest)),
2848
+ error: (message, ...rest) => printRunLog(FALLBACK_TASK, "error", joinLogArgs(message, rest)),
2849
+ success: (message, ...rest) => printRunLog(FALLBACK_TASK, "success", joinLogArgs(message, rest))
2739
2850
  };
2740
2851
  var noop = () => void 0;
2741
2852
  var fallbackItems = { add: noop, succeeded: noop, failed: noop, occurrence: noop };