hobots 1.3.3 → 1.3.4
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 +5 -5
- package/dist/index.cjs +181 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -5
- package/dist/index.d.ts +40 -5
- package/dist/index.js +181 -23
- package/dist/index.js.map +1 -1
- package/docs/README.md +1 -1
- package/docs/api-reference.md +4 -0
- package/docs/getting-started.md +1 -1
- package/docs/manager.md +23 -11
- package/docs/protocol.md +2 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -58,7 +58,7 @@ function parseStacktrace(stack) {
|
|
|
58
58
|
|
|
59
59
|
// src/utils.ts
|
|
60
60
|
var SDK_NAME = "hobots";
|
|
61
|
-
var SDK_VERSION = true ? "1.3.
|
|
61
|
+
var SDK_VERSION = true ? "1.3.3" : "0.0.0-dev";
|
|
62
62
|
function isNode() {
|
|
63
63
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
64
64
|
}
|
|
@@ -380,28 +380,61 @@ var Heartbeat = class {
|
|
|
380
380
|
}
|
|
381
381
|
};
|
|
382
382
|
|
|
383
|
+
// src/internal-handlers.ts
|
|
384
|
+
var INTERNAL = /* @__PURE__ */ Symbol.for("hobots.sdk.internalListener");
|
|
385
|
+
function markInternal(listener) {
|
|
386
|
+
listener[INTERNAL] = true;
|
|
387
|
+
return listener;
|
|
388
|
+
}
|
|
389
|
+
function externalListenerCount(event) {
|
|
390
|
+
const listeners = process.listeners(event);
|
|
391
|
+
return listeners.filter((listener) => !listener[INTERNAL]).length;
|
|
392
|
+
}
|
|
393
|
+
var emergencyTasks = [];
|
|
394
|
+
function addEmergencyTask(task) {
|
|
395
|
+
emergencyTasks.push(task);
|
|
396
|
+
}
|
|
397
|
+
async function awaitEmergencyTasks(capMs) {
|
|
398
|
+
if (emergencyTasks.length === 0) return;
|
|
399
|
+
await Promise.race([Promise.allSettled(emergencyTasks), sleep(capMs)]);
|
|
400
|
+
}
|
|
401
|
+
|
|
383
402
|
// src/instrument.ts
|
|
403
|
+
var EMERGENCY_REPORT_CAP_MS = 3e3;
|
|
384
404
|
var installed = false;
|
|
405
|
+
function hasInstalledGlobalHandlers() {
|
|
406
|
+
return installed;
|
|
407
|
+
}
|
|
385
408
|
function installGlobalHandlers(client2, getScope) {
|
|
386
409
|
if (installed) return;
|
|
387
410
|
installed = true;
|
|
388
411
|
if (isNode()) {
|
|
389
|
-
process.on(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
process.on(
|
|
403
|
-
|
|
404
|
-
|
|
412
|
+
process.on(
|
|
413
|
+
"uncaughtException",
|
|
414
|
+
markInternal((error) => {
|
|
415
|
+
client2.captureException(error, { level: "fatal", tags: { mechanism: "uncaughtException" } }, getScope());
|
|
416
|
+
void client2.flush(2e3).then(async () => {
|
|
417
|
+
if (externalListenerCount("uncaughtException") === 0) {
|
|
418
|
+
await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
|
|
419
|
+
console.error(error);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
})
|
|
424
|
+
);
|
|
425
|
+
process.on(
|
|
426
|
+
"unhandledRejection",
|
|
427
|
+
markInternal((reason) => {
|
|
428
|
+
client2.captureException(reason, { level: "error", tags: { mechanism: "unhandledRejection" } }, getScope());
|
|
429
|
+
void client2.flush(2e3);
|
|
430
|
+
})
|
|
431
|
+
);
|
|
432
|
+
process.on(
|
|
433
|
+
"beforeExit",
|
|
434
|
+
markInternal(() => {
|
|
435
|
+
void client2.flush(2e3);
|
|
436
|
+
})
|
|
437
|
+
);
|
|
405
438
|
return;
|
|
406
439
|
}
|
|
407
440
|
if (typeof window !== "undefined") {
|
|
@@ -697,12 +730,46 @@ var ManagerApi = class {
|
|
|
697
730
|
await sleep(delay);
|
|
698
731
|
}
|
|
699
732
|
}
|
|
733
|
+
/**
|
|
734
|
+
* Um único PATCH com timeout curto — exit path (o processo está morrendo, o retry
|
|
735
|
+
* ladder do updateRun é proibido aqui). Status HTTP é ignorado: 409/404 significa
|
|
736
|
+
* que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
|
|
737
|
+
*/
|
|
738
|
+
async updateRunOnce(runId, update, timeoutMs = 2e3) {
|
|
739
|
+
await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
740
|
+
method: "PATCH",
|
|
741
|
+
headers: this.headers(),
|
|
742
|
+
body: JSON.stringify(update),
|
|
743
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
744
|
+
});
|
|
745
|
+
}
|
|
700
746
|
};
|
|
701
747
|
|
|
702
748
|
// src/manager/context.ts
|
|
749
|
+
function runCanceledError() {
|
|
750
|
+
return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
|
|
751
|
+
}
|
|
752
|
+
function cancelableSleep(ms, signal) {
|
|
753
|
+
return new Promise((resolve, reject) => {
|
|
754
|
+
if (signal.aborted) {
|
|
755
|
+
reject(runCanceledError());
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const onAbort = () => {
|
|
759
|
+
clearTimeout(timer);
|
|
760
|
+
reject(runCanceledError());
|
|
761
|
+
};
|
|
762
|
+
const timer = setTimeout(() => {
|
|
763
|
+
signal.removeEventListener("abort", onAbort);
|
|
764
|
+
resolve();
|
|
765
|
+
}, ms);
|
|
766
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
767
|
+
});
|
|
768
|
+
}
|
|
703
769
|
var DEFAULT_LOG_FLUSH_INTERVAL_MS = 2e3;
|
|
704
770
|
var DEFAULT_CANCEL_CHECK_INTERVAL_MS = 5e3;
|
|
705
771
|
var MIN_CANCEL_CHECK_INTERVAL_MS = 1e3;
|
|
772
|
+
var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
|
|
706
773
|
var MAX_LOG_MESSAGE_CHARS = 1e4;
|
|
707
774
|
var MAX_LOGS_PER_RUN = 500;
|
|
708
775
|
var MAX_LOGS_PER_PATCH = 100;
|
|
@@ -713,15 +780,26 @@ var RunReporter = class {
|
|
|
713
780
|
this.runId = runId;
|
|
714
781
|
this.logger = logger;
|
|
715
782
|
this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
|
|
716
|
-
this.cancelCheckIntervalMs = Math.
|
|
717
|
-
|
|
718
|
-
cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS
|
|
783
|
+
this.cancelCheckIntervalMs = Math.min(
|
|
784
|
+
MAX_CANCEL_CHECK_INTERVAL_MS,
|
|
785
|
+
Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
|
|
719
786
|
);
|
|
787
|
+
const log = {
|
|
788
|
+
debug: (message) => this.log("debug", message),
|
|
789
|
+
info: (message) => this.log("info", message),
|
|
790
|
+
warn: (message) => this.log("warning", message),
|
|
791
|
+
error: (message) => this.log("error", message)
|
|
792
|
+
};
|
|
793
|
+
const signal = this.abortController.signal;
|
|
720
794
|
this.context = {
|
|
721
795
|
runId,
|
|
722
796
|
task,
|
|
723
|
-
log
|
|
724
|
-
signal
|
|
797
|
+
log,
|
|
798
|
+
signal,
|
|
799
|
+
throwIfCanceled: () => {
|
|
800
|
+
if (signal.aborted) throw runCanceledError();
|
|
801
|
+
},
|
|
802
|
+
sleep: (ms) => cancelableSleep(ms, signal)
|
|
725
803
|
};
|
|
726
804
|
}
|
|
727
805
|
context;
|
|
@@ -744,6 +822,17 @@ var RunReporter = class {
|
|
|
744
822
|
this.stopCancelWatch();
|
|
745
823
|
if (!this.abortController.signal.aborted) this.abortController.abort();
|
|
746
824
|
}
|
|
825
|
+
/**
|
|
826
|
+
* Mata o reporter de vez: aborta o signal e suprime todo reporte futuro.
|
|
827
|
+
* Usado no exit path — depois que o agente reportou AgentTerminated por fora
|
|
828
|
+
* da fila, um handler zumbi não pode reenviar status por cima.
|
|
829
|
+
*/
|
|
830
|
+
kill() {
|
|
831
|
+
this.abort();
|
|
832
|
+
this.dead = true;
|
|
833
|
+
this.clearFlushTimer();
|
|
834
|
+
this.buffer = [];
|
|
835
|
+
}
|
|
747
836
|
log(level, message) {
|
|
748
837
|
if (this.dead) return;
|
|
749
838
|
if (this.loggedCount >= MAX_LOGS_PER_RUN) {
|
|
@@ -859,6 +948,50 @@ var RunReporter = class {
|
|
|
859
948
|
}
|
|
860
949
|
};
|
|
861
950
|
|
|
951
|
+
// src/manager/shutdown.ts
|
|
952
|
+
var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
953
|
+
function installShutdownHandlers(agent2) {
|
|
954
|
+
if (!isNode()) return () => {
|
|
955
|
+
};
|
|
956
|
+
let signaled = false;
|
|
957
|
+
const signalHandlers = /* @__PURE__ */ new Map();
|
|
958
|
+
for (const signal of WATCHED_SIGNALS) {
|
|
959
|
+
const handler = markInternal(() => {
|
|
960
|
+
if (externalListenerCount(signal) > 0) return;
|
|
961
|
+
if (signaled) return;
|
|
962
|
+
signaled = true;
|
|
963
|
+
const reraise = () => {
|
|
964
|
+
process.removeListener(signal, handler);
|
|
965
|
+
process.kill(process.pid, signal);
|
|
966
|
+
};
|
|
967
|
+
if (agent2.activeCount === 0) {
|
|
968
|
+
reraise();
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const report = agent2.reportAgentTerminated(`o processo do agente recebeu ${signal} durante a execu\xE7\xE3o`);
|
|
972
|
+
addEmergencyTask(report);
|
|
973
|
+
void report.finally(reraise);
|
|
974
|
+
});
|
|
975
|
+
process.on(signal, handler);
|
|
976
|
+
signalHandlers.set(signal, handler);
|
|
977
|
+
}
|
|
978
|
+
const onFatal = markInternal((error) => {
|
|
979
|
+
const report = agent2.activeCount > 0 ? agent2.reportAgentTerminated("o processo do agente morreu com um erro n\xE3o tratado durante a execu\xE7\xE3o") : Promise.resolve();
|
|
980
|
+
addEmergencyTask(report);
|
|
981
|
+
if (!hasInstalledGlobalHandlers() && externalListenerCount("uncaughtException") === 0) {
|
|
982
|
+
void report.finally(() => {
|
|
983
|
+
console.error(error);
|
|
984
|
+
process.exit(1);
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
process.on("uncaughtException", onFatal);
|
|
989
|
+
return () => {
|
|
990
|
+
for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
|
|
991
|
+
process.removeListener("uncaughtException", onFatal);
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
862
995
|
// src/manager/agent.ts
|
|
863
996
|
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
864
997
|
var MIN_POLL_INTERVAL_MS = 1e3;
|
|
@@ -899,6 +1032,7 @@ var ManagerAgent = class {
|
|
|
899
1032
|
consecutiveAuthFailures = 0;
|
|
900
1033
|
activeRuns = /* @__PURE__ */ new Set();
|
|
901
1034
|
reporters = /* @__PURE__ */ new Set();
|
|
1035
|
+
uninstallShutdown;
|
|
902
1036
|
get activeCount() {
|
|
903
1037
|
return this.activeRuns.size;
|
|
904
1038
|
}
|
|
@@ -911,6 +1045,9 @@ var ManagerAgent = class {
|
|
|
911
1045
|
throw new Error("[hobots-manager] nenhuma task registrada \u2014 chame register() antes de start()");
|
|
912
1046
|
}
|
|
913
1047
|
this.state = "started";
|
|
1048
|
+
if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
|
|
1049
|
+
this.uninstallShutdown = installShutdownHandlers(this);
|
|
1050
|
+
}
|
|
914
1051
|
this.logger.debug(`polling iniciado (tasks: ${[...this.registry.keys()].join(", ")})`);
|
|
915
1052
|
this.schedule(0);
|
|
916
1053
|
}
|
|
@@ -927,11 +1064,32 @@ var ManagerAgent = class {
|
|
|
927
1064
|
const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : false;
|
|
928
1065
|
if (!finished && this.activeRuns.size > 0) {
|
|
929
1066
|
this.logger.warn(`${this.activeRuns.size} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
930
|
-
|
|
1067
|
+
await this.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
|
|
931
1068
|
}
|
|
1069
|
+
this.uninstallShutdown?.();
|
|
1070
|
+
this.uninstallShutdown = void 0;
|
|
932
1071
|
this.state = "idle";
|
|
933
1072
|
return finished;
|
|
934
1073
|
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Best-effort do exit path: reporta 'failed'/AgentTerminated para as runs ativas
|
|
1076
|
+
* antes do processo morrer, fora da fila do reporter e com timeout curto. Runs já
|
|
1077
|
+
* canceladas no app (reporter morto) são puladas; erros de rede são engolidos —
|
|
1078
|
+
* o sweep server-side é o backstop.
|
|
1079
|
+
*/
|
|
1080
|
+
async reportAgentTerminated(message, timeoutMs = 2500) {
|
|
1081
|
+
const pending = [];
|
|
1082
|
+
for (const reporter of this.reporters) {
|
|
1083
|
+
if (reporter.isDead) continue;
|
|
1084
|
+
reporter.kill();
|
|
1085
|
+
pending.push(
|
|
1086
|
+
this.api.updateRunOnce(reporter.context.runId, { status: "failed", error: { type: "AgentTerminated", message } }, timeoutMs).catch(() => {
|
|
1087
|
+
})
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
if (pending.length === 0) return;
|
|
1091
|
+
await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
|
|
1092
|
+
}
|
|
935
1093
|
/** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
|
|
936
1094
|
schedule(delayMs) {
|
|
937
1095
|
if (this.state !== "started" || this.timer) return;
|