hobots 1.5.1 → 1.6.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 +2 -32
- package/dist/index.cjs +179 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +179 -40
- package/dist/index.js.map +1 -1
- package/dist/manager/protocol.cjs.map +1 -1
- package/dist/manager/protocol.d.cts +1 -1
- package/dist/manager/protocol.d.ts +1 -1
- package/docs/api-reference.md +0 -2
- package/docs/concepts.md +40 -40
- package/docs/getting-started.md +1 -1
- package/docs/protocol.md +2 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ Cada projeto tem **um único Client Secret** — a mesma credencial autentica o
|
|
|
65
65
|
hb_<64 hex>
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
A credencial é **secreta** e viaja sempre no header `Authorization: Bearer` — nunca na URL. Copie o Client Secret no app, em **Configurações → Projetos** — os snippets de integração já vêm prontos. O SDK fala com a API oficial `https://api2.hobots.app` automaticamente; em desenvolvimento, aponte para outro host com a env `HOBOTS_API_URL` (ex.: `HOBOTS_API_URL=http://localhost:3001`).
|
|
69
69
|
|
|
70
70
|
---
|
|
71
71
|
|
|
@@ -354,37 +354,7 @@ Para parar de aceitar execuções **sem** derrubar a telemetria, use `stop(timeo
|
|
|
354
354
|
| `hobots` | Tudo: `init`, Monitor (captura, transactions, crons, heartbeat), Manager (`register`/`start`/`stop`), `close`, classes e tipos. |
|
|
355
355
|
| `hobots/protocol` | Tipos do wire format (Monitor e Manager). |
|
|
356
356
|
|
|
357
|
-
Empacotado em **ESM e CommonJS** com **definições de tipos** (`.d.ts`) para todas as entradas.
|
|
358
|
-
|
|
359
|
-
---
|
|
360
|
-
|
|
361
|
-
## Migração da API antiga (`hobots/manager`)
|
|
362
|
-
|
|
363
|
-
O Manager foi unificado no import raiz — `hobots/manager` **foi removido**:
|
|
364
|
-
|
|
365
|
-
```diff
|
|
366
|
-
-import * as Monitor from 'hobots';
|
|
367
|
-
-import * as Manager from 'hobots/manager';
|
|
368
|
-
+import * as Hobots from 'hobots';
|
|
369
|
-
|
|
370
|
-
-Monitor.init({ dsn, serverName: 'vm-01', heartbeat: true });
|
|
371
|
-
-Manager.init({ dsn, agentName: 'vm-01' });
|
|
372
|
-
+Hobots.init({ clientSecret, instanceId: 'vm-01', heartbeat: true, manager: true });
|
|
373
|
-
|
|
374
|
-
-Manager.register('minha-task', handler);
|
|
375
|
-
-Manager.start();
|
|
376
|
-
+Hobots.register('minha-task', handler);
|
|
377
|
-
+Hobots.start();
|
|
378
|
-
|
|
379
|
-
-await Manager.stop(15_000);
|
|
380
|
-
-await Monitor.close(3_000);
|
|
381
|
-
+await Hobots.close(15_000);
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
- A `dsn` (mecanismo antigo, `https://<secretKey>@host/<slug>`) não existe mais — passe `clientSecret` (em desenvolvimento, aponte para outro host com a env `HOBOTS_API_URL`). Copie o Client Secret em **Configurações → Projetos**.
|
|
385
|
-
- `agentName` e `serverName` não existem mais — a identidade do agente é o `instanceId` (obrigatório, sem fallback).
|
|
386
|
-
- As opções do Manager (`pollIntervalMs`, `concurrency`, `taskTimeoutMs`, `logFlushIntervalMs`, `cancelCheckIntervalMs`, `installExitHandlers`) vão no campo `manager` do `init()`; `clientSecret` e `debug` não se repetem.
|
|
387
|
-
- Tipos (`ManagerTaskHandler`, `RunContext`, `ManagerOptions`) e classes (`ManagerAgent`, `ManagerApi`) agora vêm de `hobots`; o wire format vem de `hobots/protocol`.
|
|
357
|
+
Empacotado em **ESM e CommonJS** com **definições de tipos** (`.d.ts`) para todas as entradas.
|
|
388
358
|
|
|
389
359
|
---
|
|
390
360
|
|
package/dist/index.cjs
CHANGED
|
@@ -48,6 +48,7 @@ __export(src_exports, {
|
|
|
48
48
|
init: () => init,
|
|
49
49
|
register: () => register,
|
|
50
50
|
resolveConfig: () => resolveConfig,
|
|
51
|
+
scrubSecrets: () => scrubSecrets,
|
|
51
52
|
setContext: () => setContext,
|
|
52
53
|
setExtra: () => setExtra,
|
|
53
54
|
setTag: () => setTag,
|
|
@@ -63,21 +64,47 @@ module.exports = __toCommonJS(src_exports);
|
|
|
63
64
|
|
|
64
65
|
// src/config.ts
|
|
65
66
|
var DEFAULT_BASE_URL = "https://api2.hobots.app";
|
|
67
|
+
var SECRET_PATTERN = /^(hb_)?[a-f0-9]{64}$/i;
|
|
66
68
|
function envBaseUrl() {
|
|
67
69
|
if (typeof process === "undefined" || !process.env) return void 0;
|
|
68
70
|
return process.env.HOBOTS_API_URL?.trim() || void 0;
|
|
69
71
|
}
|
|
72
|
+
function isLoopbackHost(hostname2) {
|
|
73
|
+
return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
|
|
74
|
+
}
|
|
75
|
+
var warnedInsecureBaseUrl = false;
|
|
76
|
+
function validateBaseUrl(raw, prefix) {
|
|
77
|
+
let url;
|
|
78
|
+
try {
|
|
79
|
+
url = new URL(raw);
|
|
80
|
+
} catch {
|
|
81
|
+
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
|
|
82
|
+
}
|
|
83
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
84
|
+
throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
|
|
85
|
+
}
|
|
86
|
+
if (url.username || url.password) {
|
|
87
|
+
throw new Error(`${prefix} HOBOTS_API_URL n\xE3o pode conter credenciais na URL \u2014 a autentica\xE7\xE3o \xE9 s\xF3 pelo Client Secret`);
|
|
88
|
+
}
|
|
89
|
+
if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
|
|
90
|
+
warnedInsecureBaseUrl = true;
|
|
91
|
+
console.warn(
|
|
92
|
+
`${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return raw.replace(/\/+$/, "");
|
|
96
|
+
}
|
|
70
97
|
function resolveConfig(options, prefix = "[hobots]") {
|
|
71
98
|
const key = options.clientSecret?.trim();
|
|
72
99
|
if (!key) {
|
|
73
100
|
throw new Error(`${prefix} clientSecret ausente \u2014 copie o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos`);
|
|
74
101
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida: "${baseUrl}" \u2014 esperado ex.: http://localhost:3001`);
|
|
102
|
+
if (!SECRET_PATTERN.test(key)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`${prefix} clientSecret com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret do projeto em Configura\xE7\xF5es \u2192 Projetos`
|
|
105
|
+
);
|
|
80
106
|
}
|
|
107
|
+
const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
|
|
81
108
|
return {
|
|
82
109
|
key,
|
|
83
110
|
baseUrl,
|
|
@@ -119,9 +146,28 @@ function parseStacktrace(stack) {
|
|
|
119
146
|
return frames;
|
|
120
147
|
}
|
|
121
148
|
|
|
149
|
+
// src/scrub.ts
|
|
150
|
+
var SECRET_PATTERNS = [
|
|
151
|
+
// client secret da Hobots
|
|
152
|
+
[/hb_[a-f0-9]{16,}/gi, "[REDACTED]"],
|
|
153
|
+
// tokens bearer em headers/mensagens
|
|
154
|
+
[/\b(authorization:?\s*)?bearer\s+[a-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]"],
|
|
155
|
+
// chaves no formato sk-… (OpenAI e afins)
|
|
156
|
+
[/\bsk-[a-z0-9_-]{16,}\b/gi, "[REDACTED]"],
|
|
157
|
+
// credenciais embutidas em URL (https://user:pass@host)
|
|
158
|
+
[/\/\/[^/\s:@]+:[^/\s@]+@/g, "//[REDACTED]@"]
|
|
159
|
+
];
|
|
160
|
+
function scrubSecrets(text) {
|
|
161
|
+
let out = text;
|
|
162
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
163
|
+
out = out.replace(pattern, replacement);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
122
168
|
// src/utils.ts
|
|
123
169
|
var SDK_NAME = "hobots";
|
|
124
|
-
var SDK_VERSION = true ? "1.
|
|
170
|
+
var SDK_VERSION = true ? "1.6.0" : "0.0.0-dev";
|
|
125
171
|
function isNode() {
|
|
126
172
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
127
173
|
}
|
|
@@ -130,6 +176,12 @@ function uuid4() {
|
|
|
130
176
|
if (cryptoRef && typeof cryptoRef.randomUUID === "function") {
|
|
131
177
|
return cryptoRef.randomUUID().replace(/-/g, "");
|
|
132
178
|
}
|
|
179
|
+
if (cryptoRef && typeof cryptoRef.getRandomValues === "function") {
|
|
180
|
+
const bytes = cryptoRef.getRandomValues(new Uint8Array(16));
|
|
181
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
182
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
183
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
184
|
+
}
|
|
133
185
|
let out = "";
|
|
134
186
|
for (let i = 0; i < 32; i++) {
|
|
135
187
|
out += Math.floor(Math.random() * 16).toString(16);
|
|
@@ -148,7 +200,42 @@ function sleep(ms) {
|
|
|
148
200
|
unrefTimer(timer);
|
|
149
201
|
});
|
|
150
202
|
}
|
|
151
|
-
|
|
203
|
+
function jitter(baseMs) {
|
|
204
|
+
return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
|
|
205
|
+
}
|
|
206
|
+
var MAX_RETRY_AFTER_MS = 6e4;
|
|
207
|
+
function retryAfterMs(response) {
|
|
208
|
+
const header = response.headers.get("retry-after");
|
|
209
|
+
if (!header) return void 0;
|
|
210
|
+
const seconds = Number(header);
|
|
211
|
+
if (Number.isFinite(seconds)) {
|
|
212
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, seconds * 1e3));
|
|
213
|
+
}
|
|
214
|
+
const date = Date.parse(header);
|
|
215
|
+
if (Number.isNaN(date)) return void 0;
|
|
216
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, date - Date.now()));
|
|
217
|
+
}
|
|
218
|
+
function withTimeoutSignal(timeoutMs, external) {
|
|
219
|
+
const controller = new AbortController();
|
|
220
|
+
const onAbort = () => controller.abort(external?.reason);
|
|
221
|
+
if (external?.aborted) {
|
|
222
|
+
onAbort();
|
|
223
|
+
} else {
|
|
224
|
+
external?.addEventListener("abort", onAbort, { once: true });
|
|
225
|
+
}
|
|
226
|
+
const timer = setTimeout(() => {
|
|
227
|
+
controller.abort(Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms`), { name: "TimeoutError" }));
|
|
228
|
+
}, timeoutMs);
|
|
229
|
+
unrefTimer(timer);
|
|
230
|
+
return {
|
|
231
|
+
signal: controller.signal,
|
|
232
|
+
cleanup: () => {
|
|
233
|
+
clearTimeout(timer);
|
|
234
|
+
external?.removeEventListener("abort", onAbort);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
var PREFIX = "[hobots]";
|
|
152
239
|
function makeLogger(enabled, prefix = PREFIX) {
|
|
153
240
|
return {
|
|
154
241
|
debug: (...args) => enabled && console.debug(colorize(prefix, "gray"), ...args),
|
|
@@ -189,11 +276,11 @@ function eventFromException(error, level = "error") {
|
|
|
189
276
|
let frames = [];
|
|
190
277
|
if (error instanceof Error) {
|
|
191
278
|
type = error.name || "Error";
|
|
192
|
-
value = error.message;
|
|
279
|
+
value = scrubSecrets(error.message);
|
|
193
280
|
frames = parseStacktrace(error.stack);
|
|
194
281
|
} else {
|
|
195
282
|
type = "NonError";
|
|
196
|
-
value = safeString(error);
|
|
283
|
+
value = scrubSecrets(safeString(error));
|
|
197
284
|
}
|
|
198
285
|
return {
|
|
199
286
|
event_id: uuid4(),
|
|
@@ -211,12 +298,13 @@ function eventFromMessage(message, level = "info") {
|
|
|
211
298
|
event_id: uuid4(),
|
|
212
299
|
timestamp: nowISO(),
|
|
213
300
|
level,
|
|
214
|
-
message
|
|
301
|
+
message: scrubSecrets(message)
|
|
215
302
|
};
|
|
216
303
|
}
|
|
217
304
|
|
|
218
305
|
// src/transport.ts
|
|
219
306
|
var RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
307
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
220
308
|
var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
|
|
221
309
|
var DEFAULT_MAX_BATCH_SIZE = 10;
|
|
222
310
|
var DEFAULT_MAX_QUEUE_SIZE = 200;
|
|
@@ -283,11 +371,13 @@ var Transport = class {
|
|
|
283
371
|
}
|
|
284
372
|
async sendWithRetry(envelope) {
|
|
285
373
|
for (let attempt = 0; ; attempt++) {
|
|
374
|
+
let serverDelay;
|
|
286
375
|
try {
|
|
287
376
|
const response = await fetch(this.endpoint, {
|
|
288
377
|
method: "POST",
|
|
289
378
|
headers: { "content-type": "application/json", authorization: `Bearer ${this.key}` },
|
|
290
|
-
body: JSON.stringify(envelope)
|
|
379
|
+
body: JSON.stringify(envelope),
|
|
380
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
291
381
|
});
|
|
292
382
|
if (response.ok) {
|
|
293
383
|
this.logger.debug(`envelope enviado (${envelope.items.length} itens)`);
|
|
@@ -297,13 +387,15 @@ var Transport = class {
|
|
|
297
387
|
this.logger.error(`envelope rejeitado (HTTP ${response.status}), descartado`);
|
|
298
388
|
return;
|
|
299
389
|
}
|
|
390
|
+
if (response.status === 429) serverDelay = retryAfterMs(response);
|
|
300
391
|
throw new Error(`HTTP ${response.status}`);
|
|
301
392
|
} catch (error) {
|
|
302
|
-
const
|
|
303
|
-
if (
|
|
393
|
+
const base = RETRY_DELAYS_MS[attempt];
|
|
394
|
+
if (base === void 0) {
|
|
304
395
|
this.logger.error("envelope descartado ap\xF3s retries:", error);
|
|
305
396
|
return;
|
|
306
397
|
}
|
|
398
|
+
const delay = serverDelay ?? jitter(base);
|
|
307
399
|
this.logger.warn(`falha no envio (${String(error)}), retry em ${delay}ms`);
|
|
308
400
|
await sleep(delay);
|
|
309
401
|
}
|
|
@@ -333,22 +425,38 @@ function runtimeContexts() {
|
|
|
333
425
|
return { runtime: { name: "browser" } };
|
|
334
426
|
}
|
|
335
427
|
var Client = class {
|
|
336
|
-
options;
|
|
337
|
-
config;
|
|
338
428
|
logger;
|
|
429
|
+
// campos privados + getters de protótipo: o clientSecret não aparece em
|
|
430
|
+
// JSON.stringify/serialização enumerável do Client (ex.: um objeto capturado
|
|
431
|
+
// que referencie o client), mas client.options/config seguem funcionando
|
|
432
|
+
#options;
|
|
433
|
+
#config;
|
|
339
434
|
transport;
|
|
340
435
|
constructor(options) {
|
|
341
|
-
this
|
|
436
|
+
this.#options = options;
|
|
342
437
|
this.logger = makeLogger(!!options.debug);
|
|
343
|
-
this
|
|
438
|
+
this.#config = resolveConfig(options);
|
|
344
439
|
this.transport = new Transport({
|
|
345
|
-
endpoint: this
|
|
346
|
-
key: this
|
|
440
|
+
endpoint: this.#config.ingestEndpoint,
|
|
441
|
+
key: this.#config.key,
|
|
347
442
|
logger: this.logger,
|
|
348
443
|
flushIntervalMs: options.flushIntervalMs,
|
|
349
444
|
maxBatchSize: options.maxBatchSize
|
|
350
445
|
});
|
|
351
446
|
}
|
|
447
|
+
get options() {
|
|
448
|
+
return this.#options;
|
|
449
|
+
}
|
|
450
|
+
get config() {
|
|
451
|
+
return this.#config;
|
|
452
|
+
}
|
|
453
|
+
/** Serialização explícita nunca inclui o secret. */
|
|
454
|
+
toJSON() {
|
|
455
|
+
return { sdk: SDK_NAME, version: SDK_VERSION, baseUrl: this.#config.baseUrl };
|
|
456
|
+
}
|
|
457
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
458
|
+
return `Client { baseUrl: '${this.#config.baseUrl}' }`;
|
|
459
|
+
}
|
|
352
460
|
captureException(error, hint, scope2) {
|
|
353
461
|
return this.captureEvent(eventFromException(error, hint?.level ?? "error"), hint, scope2);
|
|
354
462
|
}
|
|
@@ -767,6 +875,8 @@ async function withMonitorImpl(client2, captureError, monitorSlug, fn, options)
|
|
|
767
875
|
|
|
768
876
|
// src/manager/api.ts
|
|
769
877
|
var RETRY_DELAYS_MS2 = [1e3, 4e3, 16e3];
|
|
878
|
+
var POLL_TIMEOUT_MS = 3e4;
|
|
879
|
+
var UPDATE_TIMEOUT_MS = 1e4;
|
|
770
880
|
var ManagerApiError = class extends Error {
|
|
771
881
|
constructor(message, status) {
|
|
772
882
|
super(message);
|
|
@@ -791,16 +901,39 @@ var ManagerApi = class {
|
|
|
791
901
|
}
|
|
792
902
|
/** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
|
|
793
903
|
async poll(request, signal) {
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
904
|
+
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
|
|
905
|
+
let response;
|
|
906
|
+
try {
|
|
907
|
+
response = await fetch(`${this.baseUrl}/poll`, {
|
|
908
|
+
method: "POST",
|
|
909
|
+
headers: this.headers(),
|
|
910
|
+
body: JSON.stringify(request),
|
|
911
|
+
signal: timeoutSignal
|
|
912
|
+
});
|
|
913
|
+
} finally {
|
|
914
|
+
cleanup();
|
|
915
|
+
}
|
|
800
916
|
if (!response.ok) {
|
|
801
917
|
throw new ManagerApiError(`poll falhou (HTTP ${response.status})`, response.status);
|
|
802
918
|
}
|
|
803
|
-
return await response.json();
|
|
919
|
+
return this.parsePollResponse(await response.json(), request.max ?? 1);
|
|
920
|
+
}
|
|
921
|
+
/** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
|
|
922
|
+
parsePollResponse(body, max) {
|
|
923
|
+
const runs = body?.runs;
|
|
924
|
+
if (!Array.isArray(runs)) {
|
|
925
|
+
throw new ManagerApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
|
|
926
|
+
}
|
|
927
|
+
const valid = [];
|
|
928
|
+
for (const run of runs.slice(0, Math.max(0, max))) {
|
|
929
|
+
if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
|
|
930
|
+
const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
|
|
931
|
+
valid.push({ ...run, id: run.id, task: run.task, params });
|
|
932
|
+
}
|
|
933
|
+
if (valid.length < runs.length) {
|
|
934
|
+
this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
|
|
935
|
+
}
|
|
936
|
+
return { runs: valid };
|
|
804
937
|
}
|
|
805
938
|
/**
|
|
806
939
|
* PATCH de status/logs com retry em falha de rede/5xx/429.
|
|
@@ -809,25 +942,29 @@ var ManagerApi = class {
|
|
|
809
942
|
async updateRun(runId, update) {
|
|
810
943
|
for (let attempt = 0; ; attempt++) {
|
|
811
944
|
let retriable;
|
|
945
|
+
let serverDelay;
|
|
812
946
|
try {
|
|
813
947
|
const response = await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
814
948
|
method: "PATCH",
|
|
815
949
|
headers: this.headers(),
|
|
816
|
-
body: JSON.stringify(update)
|
|
950
|
+
body: JSON.stringify(update),
|
|
951
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
817
952
|
});
|
|
818
953
|
if (response.ok) return;
|
|
819
954
|
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
|
820
955
|
throw new ManagerApiError(`update rejeitado (HTTP ${response.status})`, response.status);
|
|
821
956
|
}
|
|
957
|
+
if (response.status === 429) serverDelay = retryAfterMs(response);
|
|
822
958
|
retriable = new Error(`HTTP ${response.status}`);
|
|
823
959
|
} catch (error) {
|
|
824
960
|
if (error instanceof ManagerApiError) throw error;
|
|
825
961
|
retriable = error;
|
|
826
962
|
}
|
|
827
|
-
const
|
|
828
|
-
if (
|
|
963
|
+
const base = RETRY_DELAYS_MS2[attempt];
|
|
964
|
+
if (base === void 0) {
|
|
829
965
|
throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(retriable)}`);
|
|
830
966
|
}
|
|
967
|
+
const delay = serverDelay ?? jitter(base);
|
|
831
968
|
this.logger.warn(`falha no update da run ${runId} (${String(retriable)}), retry em ${delay}ms`);
|
|
832
969
|
await sleep(delay);
|
|
833
970
|
}
|
|
@@ -951,13 +1088,14 @@ var RunReporter = class {
|
|
|
951
1088
|
}
|
|
952
1089
|
log(level, message) {
|
|
953
1090
|
if (this.dead) return;
|
|
954
|
-
|
|
1091
|
+
const scrubbed = scrubSecrets(message);
|
|
1092
|
+
printRunLog(this.context.task, level, scrubbed);
|
|
955
1093
|
if (this.loggedCount >= MAX_LOGS_PER_RUN) {
|
|
956
1094
|
this.droppedCount++;
|
|
957
1095
|
return;
|
|
958
1096
|
}
|
|
959
1097
|
this.loggedCount++;
|
|
960
|
-
const truncated =
|
|
1098
|
+
const truncated = scrubbed.length > MAX_LOG_MESSAGE_CHARS ? `${scrubbed.slice(0, MAX_LOG_MESSAGE_CHARS)}\u2026 [truncado]` : scrubbed;
|
|
961
1099
|
this.buffer.push({ ts: nowISO(), level, message: truncated });
|
|
962
1100
|
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
963
1101
|
this.flushLogs();
|
|
@@ -1141,8 +1279,8 @@ var ManagerAgent = class {
|
|
|
1141
1279
|
constructor(options, registry2) {
|
|
1142
1280
|
this.options = options;
|
|
1143
1281
|
this.registry = registry2;
|
|
1144
|
-
this.logger = makeLogger(options.debug ?? false, "[hobots
|
|
1145
|
-
const config = resolveConfig(options, "[hobots
|
|
1282
|
+
this.logger = makeLogger(options.debug ?? false, "[hobots]");
|
|
1283
|
+
const config = resolveConfig(options, "[hobots]");
|
|
1146
1284
|
this.api = new ManagerApi({ baseUrl: config.managerBaseUrl, key: config.key, logger: this.logger });
|
|
1147
1285
|
this.client = options.client;
|
|
1148
1286
|
this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
@@ -1176,10 +1314,10 @@ var ManagerAgent = class {
|
|
|
1176
1314
|
start() {
|
|
1177
1315
|
if (this.state === "started") return;
|
|
1178
1316
|
if (this.state === "stopping") {
|
|
1179
|
-
throw new Error("[hobots
|
|
1317
|
+
throw new Error("[hobots] start() chamado durante stop() \u2014 aguarde o stop terminar");
|
|
1180
1318
|
}
|
|
1181
1319
|
if (this.registry.size === 0) {
|
|
1182
|
-
throw new Error("[hobots
|
|
1320
|
+
throw new Error("[hobots] nenhuma task registrada \u2014 chame register() antes de start()");
|
|
1183
1321
|
}
|
|
1184
1322
|
this.state = "started";
|
|
1185
1323
|
if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
|
|
@@ -1261,6 +1399,7 @@ var ManagerAgent = class {
|
|
|
1261
1399
|
this.consecutiveFailures = 0;
|
|
1262
1400
|
this.consecutiveAuthFailures = 0;
|
|
1263
1401
|
for (const run of response.runs) {
|
|
1402
|
+
if (this.activeRuns.size >= this.concurrency) break;
|
|
1264
1403
|
this.dispatch(run);
|
|
1265
1404
|
}
|
|
1266
1405
|
this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
|
|
@@ -1281,7 +1420,7 @@ var ManagerAgent = class {
|
|
|
1281
1420
|
}
|
|
1282
1421
|
this.consecutiveFailures++;
|
|
1283
1422
|
const ladder = POLL_RETRY_DELAYS_MS[Math.min(this.consecutiveFailures - 1, POLL_RETRY_DELAYS_MS.length - 1)];
|
|
1284
|
-
const delay = Math.min(Math.max(ladder, 0), Math.max(this.pollIntervalMs, 16e3));
|
|
1423
|
+
const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.pollIntervalMs, 16e3));
|
|
1285
1424
|
this.logger.warn(`poll falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
|
|
1286
1425
|
return delay;
|
|
1287
1426
|
}
|
|
@@ -1293,7 +1432,7 @@ var ManagerAgent = class {
|
|
|
1293
1432
|
this.activeRuns.add(promise);
|
|
1294
1433
|
}
|
|
1295
1434
|
async executeRun(run) {
|
|
1296
|
-
const tx = new Transaction(this.client, `run:${run.task}`, "
|
|
1435
|
+
const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", true, run.id);
|
|
1297
1436
|
const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx, this.logFlushIntervalMs, this.cancelCheckIntervalMs);
|
|
1298
1437
|
this.reporters.add(reporter);
|
|
1299
1438
|
try {
|
|
@@ -1359,11 +1498,11 @@ function toRunError(error) {
|
|
|
1359
1498
|
if (error instanceof Error) {
|
|
1360
1499
|
return {
|
|
1361
1500
|
type: error.name,
|
|
1362
|
-
message: truncate(error.message, MAX_ERROR_MESSAGE_CHARS),
|
|
1363
|
-
stack: error.stack ? truncate(error.stack, MAX_ERROR_STACK_CHARS) : void 0
|
|
1501
|
+
message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
|
|
1502
|
+
stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
|
|
1364
1503
|
};
|
|
1365
1504
|
}
|
|
1366
|
-
return { message: truncate(String(error), MAX_ERROR_MESSAGE_CHARS) };
|
|
1505
|
+
return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
|
|
1367
1506
|
}
|
|
1368
1507
|
|
|
1369
1508
|
// src/core.ts
|