hobots 1.3.2 → 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 +218 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -4
- package/dist/index.d.ts +45 -4
- package/dist/index.js +218 -22
- 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,25 +730,76 @@ 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;
|
|
770
|
+
var DEFAULT_CANCEL_CHECK_INTERVAL_MS = 5e3;
|
|
771
|
+
var MIN_CANCEL_CHECK_INTERVAL_MS = 1e3;
|
|
772
|
+
var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
|
|
704
773
|
var MAX_LOG_MESSAGE_CHARS = 1e4;
|
|
705
774
|
var MAX_LOGS_PER_RUN = 500;
|
|
706
775
|
var MAX_LOGS_PER_PATCH = 100;
|
|
707
776
|
var FLUSH_AT_BUFFERED = 20;
|
|
708
777
|
var RunReporter = class {
|
|
709
|
-
constructor(api, runId, task, logger, flushIntervalMs) {
|
|
778
|
+
constructor(api, runId, task, logger, flushIntervalMs, cancelCheckIntervalMs) {
|
|
710
779
|
this.api = api;
|
|
711
780
|
this.runId = runId;
|
|
712
781
|
this.logger = logger;
|
|
713
782
|
this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_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)
|
|
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;
|
|
714
794
|
this.context = {
|
|
715
795
|
runId,
|
|
716
796
|
task,
|
|
717
|
-
log
|
|
718
|
-
signal
|
|
797
|
+
log,
|
|
798
|
+
signal,
|
|
799
|
+
throwIfCanceled: () => {
|
|
800
|
+
if (signal.aborted) throw runCanceledError();
|
|
801
|
+
},
|
|
802
|
+
sleep: (ms) => cancelableSleep(ms, signal)
|
|
719
803
|
};
|
|
720
804
|
}
|
|
721
805
|
context;
|
|
@@ -724,16 +808,31 @@ var RunReporter = class {
|
|
|
724
808
|
droppedCount = 0;
|
|
725
809
|
dead = false;
|
|
726
810
|
chain = Promise.resolve();
|
|
811
|
+
pendingUpdates = 0;
|
|
727
812
|
timer;
|
|
813
|
+
watchTimer;
|
|
728
814
|
abortController = new AbortController();
|
|
729
815
|
flushIntervalMs;
|
|
816
|
+
cancelCheckIntervalMs;
|
|
730
817
|
get isDead() {
|
|
731
818
|
return this.dead;
|
|
732
819
|
}
|
|
733
820
|
/** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
|
|
734
821
|
abort() {
|
|
822
|
+
this.stopCancelWatch();
|
|
735
823
|
if (!this.abortController.signal.aborted) this.abortController.abort();
|
|
736
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
|
+
}
|
|
737
836
|
log(level, message) {
|
|
738
837
|
if (this.dead) return;
|
|
739
838
|
if (this.loggedCount >= MAX_LOGS_PER_RUN) {
|
|
@@ -788,22 +887,45 @@ var RunReporter = class {
|
|
|
788
887
|
async reportRunning() {
|
|
789
888
|
this.enqueue({ status: "running" });
|
|
790
889
|
await this.chain;
|
|
890
|
+
if (!this.dead) this.startCancelWatch();
|
|
791
891
|
return !this.dead;
|
|
792
892
|
}
|
|
793
893
|
reportSuccess() {
|
|
894
|
+
this.stopCancelWatch();
|
|
794
895
|
this.clearFlushTimer();
|
|
795
896
|
this.enqueue({ status: "succeeded", logs: this.takeLogs() });
|
|
796
897
|
}
|
|
797
898
|
reportFailure(error) {
|
|
899
|
+
this.stopCancelWatch();
|
|
798
900
|
this.clearFlushTimer();
|
|
799
901
|
this.enqueue({ status: "failed", error, logs: this.takeLogs() });
|
|
800
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* Heartbeat de cancelamento: enquanto o handler roda, um PATCH 'running' vazio
|
|
905
|
+
* periódico faz o servidor responder 409 se a run foi cancelada no app — o que
|
|
906
|
+
* mata o reporter e dispara ctx.signal, mesmo em handlers que nunca logam.
|
|
907
|
+
*/
|
|
908
|
+
startCancelWatch() {
|
|
909
|
+
if (this.watchTimer) return;
|
|
910
|
+
this.watchTimer = setInterval(() => {
|
|
911
|
+
if (this.dead || this.pendingUpdates > 0 || this.buffer.length > 0) return;
|
|
912
|
+
this.enqueue({ status: "running" });
|
|
913
|
+
}, this.cancelCheckIntervalMs);
|
|
914
|
+
unrefTimer(this.watchTimer);
|
|
915
|
+
}
|
|
916
|
+
stopCancelWatch() {
|
|
917
|
+
if (this.watchTimer) {
|
|
918
|
+
clearInterval(this.watchTimer);
|
|
919
|
+
this.watchTimer = void 0;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
801
922
|
/** Aguarda a fila de PATCHes esvaziar (chame depois do terminal). */
|
|
802
923
|
finalize() {
|
|
803
924
|
return this.chain;
|
|
804
925
|
}
|
|
805
926
|
enqueue(update) {
|
|
806
927
|
if (this.dead) return;
|
|
928
|
+
this.pendingUpdates++;
|
|
807
929
|
this.chain = this.chain.then(async () => {
|
|
808
930
|
if (this.dead) return;
|
|
809
931
|
const payload = update.logs && update.logs.length === 0 ? { ...update, logs: void 0 } : update;
|
|
@@ -812,6 +934,7 @@ var RunReporter = class {
|
|
|
812
934
|
} catch (error) {
|
|
813
935
|
if (error instanceof ManagerApiError && (error.status === 409 || error.status === 404)) {
|
|
814
936
|
this.dead = true;
|
|
937
|
+
this.stopCancelWatch();
|
|
815
938
|
this.abort();
|
|
816
939
|
this.buffer = [];
|
|
817
940
|
this.logger.warn(`run ${this.runId} cancelada/removida no servidor \u2014 reporte suprimido`);
|
|
@@ -819,10 +942,56 @@ var RunReporter = class {
|
|
|
819
942
|
}
|
|
820
943
|
this.logger.error(`update da run ${this.runId} perdido:`, error);
|
|
821
944
|
}
|
|
945
|
+
}).finally(() => {
|
|
946
|
+
this.pendingUpdates--;
|
|
822
947
|
});
|
|
823
948
|
}
|
|
824
949
|
};
|
|
825
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
|
+
|
|
826
995
|
// src/manager/agent.ts
|
|
827
996
|
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
828
997
|
var MIN_POLL_INTERVAL_MS = 1e3;
|
|
@@ -845,6 +1014,7 @@ var ManagerAgent = class {
|
|
|
845
1014
|
this.instanceId = options.instanceId;
|
|
846
1015
|
this.taskTimeoutMs = options.taskTimeoutMs;
|
|
847
1016
|
this.logFlushIntervalMs = options.logFlushIntervalMs;
|
|
1017
|
+
this.cancelCheckIntervalMs = options.cancelCheckIntervalMs;
|
|
848
1018
|
this.logger.debug("agente configurado", { baseUrl: config.managerBaseUrl });
|
|
849
1019
|
}
|
|
850
1020
|
logger;
|
|
@@ -854,6 +1024,7 @@ var ManagerAgent = class {
|
|
|
854
1024
|
instanceId;
|
|
855
1025
|
taskTimeoutMs;
|
|
856
1026
|
logFlushIntervalMs;
|
|
1027
|
+
cancelCheckIntervalMs;
|
|
857
1028
|
state = "idle";
|
|
858
1029
|
timer;
|
|
859
1030
|
pollAbort;
|
|
@@ -861,6 +1032,7 @@ var ManagerAgent = class {
|
|
|
861
1032
|
consecutiveAuthFailures = 0;
|
|
862
1033
|
activeRuns = /* @__PURE__ */ new Set();
|
|
863
1034
|
reporters = /* @__PURE__ */ new Set();
|
|
1035
|
+
uninstallShutdown;
|
|
864
1036
|
get activeCount() {
|
|
865
1037
|
return this.activeRuns.size;
|
|
866
1038
|
}
|
|
@@ -873,6 +1045,9 @@ var ManagerAgent = class {
|
|
|
873
1045
|
throw new Error("[hobots-manager] nenhuma task registrada \u2014 chame register() antes de start()");
|
|
874
1046
|
}
|
|
875
1047
|
this.state = "started";
|
|
1048
|
+
if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
|
|
1049
|
+
this.uninstallShutdown = installShutdownHandlers(this);
|
|
1050
|
+
}
|
|
876
1051
|
this.logger.debug(`polling iniciado (tasks: ${[...this.registry.keys()].join(", ")})`);
|
|
877
1052
|
this.schedule(0);
|
|
878
1053
|
}
|
|
@@ -889,11 +1064,32 @@ var ManagerAgent = class {
|
|
|
889
1064
|
const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : false;
|
|
890
1065
|
if (!finished && this.activeRuns.size > 0) {
|
|
891
1066
|
this.logger.warn(`${this.activeRuns.size} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
892
|
-
|
|
1067
|
+
await this.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
|
|
893
1068
|
}
|
|
1069
|
+
this.uninstallShutdown?.();
|
|
1070
|
+
this.uninstallShutdown = void 0;
|
|
894
1071
|
this.state = "idle";
|
|
895
1072
|
return finished;
|
|
896
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
|
+
}
|
|
897
1093
|
/** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
|
|
898
1094
|
schedule(delayMs) {
|
|
899
1095
|
if (this.state !== "started" || this.timer) return;
|
|
@@ -960,7 +1156,7 @@ var ManagerAgent = class {
|
|
|
960
1156
|
this.activeRuns.add(promise);
|
|
961
1157
|
}
|
|
962
1158
|
async executeRun(run) {
|
|
963
|
-
const reporter = new RunReporter(this.api, run.id, run.task, this.logger, this.logFlushIntervalMs);
|
|
1159
|
+
const reporter = new RunReporter(this.api, run.id, run.task, this.logger, this.logFlushIntervalMs, this.cancelCheckIntervalMs);
|
|
964
1160
|
this.reporters.add(reporter);
|
|
965
1161
|
try {
|
|
966
1162
|
const handler = this.registry.get(run.task);
|