@ouro.bot/cli 0.1.0-alpha.830 → 0.1.0-alpha.832
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/changelog.json +12 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/daemon/sanctuary-telegram-authority-service.js +38 -5
- package/dist/heart/frontend-socket-client.js +15 -5
- package/dist/senses/telegram-client.js +38 -6
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.832",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Sanctuary authority gateway: a getUpdates 409 conflict (and transient 5xx) after a gateway restart is retried internally until the previous Telegram long-poll session expires, so the tokenless resident never sees the reclaim window."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.831",
|
|
12
|
+
"changes": [
|
|
13
|
+
"Sanctuary: the Telegram long-poll survives transient transport failures — a dropped authority socket or a Telegram API error is retried with exponential backoff instead of exiting the sense process, so a gateway restart no longer crash-loops the resident. Non-transport (dispatch/durable-audit) failures still surface."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.1.0-alpha.830",
|
|
6
18
|
"changes": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.832</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -38,6 +38,7 @@ exports.createSanctuaryTelegramAuthorityServer = createSanctuaryTelegramAuthorit
|
|
|
38
38
|
const fs = __importStar(require("node:fs"));
|
|
39
39
|
const net = __importStar(require("node:net"));
|
|
40
40
|
const path = __importStar(require("node:path"));
|
|
41
|
+
const telegram_client_1 = require("../../senses/telegram-client");
|
|
41
42
|
const telegram_effect_adapter_1 = require("../../senses/telegram-effect-adapter");
|
|
42
43
|
const frontend_socket_client_1 = require("../frontend-socket-client");
|
|
43
44
|
const sanctuary_authority_codec_1 = require("./sanctuary-authority-codec");
|
|
@@ -68,8 +69,17 @@ function exactBody(value, required, optional = []) {
|
|
|
68
69
|
function boundedText(value, maxLength) {
|
|
69
70
|
return typeof value === "string" && value.length > 0 && value.length <= maxLength;
|
|
70
71
|
}
|
|
72
|
+
// After a gateway restart, Telegram keeps the previous getUpdates long-poll
|
|
73
|
+
// alive server-side and answers 409 Conflict to the new poller until that
|
|
74
|
+
// session expires (about the previous poll's timeout). Retry through that
|
|
75
|
+
// window so the tokenless resident never sees the reclaim error; a transient
|
|
76
|
+
// 5xx from Telegram is retried the same way.
|
|
77
|
+
const AUTHORITY_POLL_RECLAIM_ATTEMPTS = 20;
|
|
78
|
+
const AUTHORITY_POLL_RECLAIM_DELAY_MS = 4_000;
|
|
71
79
|
class SanctuaryTelegramAuthorityService {
|
|
72
80
|
#api;
|
|
81
|
+
#pollReclaimAttempts;
|
|
82
|
+
#pollReclaimDelayMs;
|
|
73
83
|
#gateway;
|
|
74
84
|
#downloadFile;
|
|
75
85
|
#hostAuthority;
|
|
@@ -86,10 +96,37 @@ class SanctuaryTelegramAuthorityService {
|
|
|
86
96
|
this.#hostAuthority = options.hostAuthority;
|
|
87
97
|
this.#hostExecutor = options.hostExecutor;
|
|
88
98
|
this.#downloadFile = options.downloadFile;
|
|
99
|
+
this.#pollReclaimAttempts = options.pollReclaimAttempts ?? AUTHORITY_POLL_RECLAIM_ATTEMPTS;
|
|
100
|
+
this.#pollReclaimDelayMs = options.pollReclaimDelayMs ?? AUTHORITY_POLL_RECLAIM_DELAY_MS;
|
|
89
101
|
}
|
|
90
102
|
async drainHostExecutions() {
|
|
91
103
|
await Promise.allSettled(this.#hostExecutions.values());
|
|
92
104
|
}
|
|
105
|
+
async #pollUpdates() {
|
|
106
|
+
for (let attempt = 1;; attempt += 1) {
|
|
107
|
+
try {
|
|
108
|
+
return await this.#api.request("getUpdates", {
|
|
109
|
+
offset: this.#gateway.cursor(),
|
|
110
|
+
timeout: 50,
|
|
111
|
+
allowed_updates: ["message", "callback_query"],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
const status = error instanceof telegram_client_1.TelegramApiError ? error.status : null;
|
|
116
|
+
const reclaimable = status === 409 || (status !== null && status >= 500);
|
|
117
|
+
if (!reclaimable || attempt >= this.#pollReclaimAttempts)
|
|
118
|
+
throw error;
|
|
119
|
+
(0, runtime_1.emitNervesEvent)({
|
|
120
|
+
level: "warn",
|
|
121
|
+
component: "daemon",
|
|
122
|
+
event: "daemon.sanctuary_authority_poll_reclaim",
|
|
123
|
+
message: "getUpdates conflicted while reclaiming the Telegram session; retrying",
|
|
124
|
+
meta: { status, attempt },
|
|
125
|
+
});
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, this.#pollReclaimDelayMs));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
93
130
|
#hostReady() {
|
|
94
131
|
return this.#hostExecutor !== undefined && (this.#hostExecutor.isHealthy?.() ?? true);
|
|
95
132
|
}
|
|
@@ -103,11 +140,7 @@ class SanctuaryTelegramAuthorityService {
|
|
|
103
140
|
if (method === "telegram.poll") {
|
|
104
141
|
if (!emptyParams(params))
|
|
105
142
|
throw new Error("Sanctuary Telegram poll params are invalid");
|
|
106
|
-
const updates = await this.#
|
|
107
|
-
offset: this.#gateway.cursor(),
|
|
108
|
-
timeout: 50,
|
|
109
|
-
allowed_updates: ["message", "callback_query"],
|
|
110
|
-
});
|
|
143
|
+
const updates = await this.#pollUpdates();
|
|
111
144
|
if (!Array.isArray(updates))
|
|
112
145
|
throw new Error("Sanctuary Telegram poll result must be an array");
|
|
113
146
|
this.#gateway.capture(updates);
|
|
@@ -36,6 +36,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.SocketFrontendClient = void 0;
|
|
37
37
|
const net = __importStar(require("node:net"));
|
|
38
38
|
const runtime_1 = require("../nerves/runtime");
|
|
39
|
+
// A dropped or refused frontend socket is a transient transport condition, not a
|
|
40
|
+
// logic failure: the caller (e.g. the Telegram long-poll loop) should reconnect
|
|
41
|
+
// and retry rather than exit. Flag such errors so that layer can classify them.
|
|
42
|
+
function markTransientTransport(error) {
|
|
43
|
+
return Object.assign(error, { isTransientTransportError: true });
|
|
44
|
+
}
|
|
45
|
+
function transientTransportError(message) {
|
|
46
|
+
return markTransientTransport(new Error(message));
|
|
47
|
+
}
|
|
39
48
|
class SocketFrontendClient {
|
|
40
49
|
socketPath;
|
|
41
50
|
pending = new Map();
|
|
@@ -65,7 +74,7 @@ class SocketFrontendClient {
|
|
|
65
74
|
return () => this.closeListeners.delete(listener);
|
|
66
75
|
}
|
|
67
76
|
close() {
|
|
68
|
-
this.failPending(
|
|
77
|
+
this.failPending(transientTransportError("frontend socket closed"));
|
|
69
78
|
this.socket?.destroy();
|
|
70
79
|
this.socket = null;
|
|
71
80
|
}
|
|
@@ -78,19 +87,20 @@ class SocketFrontendClient {
|
|
|
78
87
|
const socket = net.createConnection(this.socketPath);
|
|
79
88
|
const fail = (error) => {
|
|
80
89
|
this.connecting = null;
|
|
81
|
-
reject(error);
|
|
90
|
+
reject(markTransientTransport(error));
|
|
82
91
|
};
|
|
83
92
|
socket.once("error", fail);
|
|
84
93
|
socket.once("connect", () => {
|
|
85
94
|
socket.removeListener("error", fail);
|
|
86
95
|
socket.on("error", (error) => {
|
|
87
|
-
|
|
96
|
+
const transient = markTransientTransport(error);
|
|
97
|
+
this.failPending(transient);
|
|
88
98
|
for (const listener of this.closeListeners)
|
|
89
|
-
listener(
|
|
99
|
+
listener(transient);
|
|
90
100
|
});
|
|
91
101
|
socket.on("data", (chunk) => this.handleData(chunk.toString("utf8")));
|
|
92
102
|
socket.on("close", () => {
|
|
93
|
-
const error =
|
|
103
|
+
const error = transientTransportError("frontend socket closed");
|
|
94
104
|
this.failPending(error);
|
|
95
105
|
for (const listener of this.closeListeners)
|
|
96
106
|
listener(error);
|
|
@@ -80,6 +80,12 @@ class FileTelegramOffsetStore {
|
|
|
80
80
|
exports.FileTelegramOffsetStore = FileTelegramOffsetStore;
|
|
81
81
|
const DEFAULT_TELEGRAM_INDETERMINATE_RETENTION_MS = 24 * 60 * 60 * 1_000;
|
|
82
82
|
const DEFAULT_TELEGRAM_MAX_INDETERMINATE_RECEIPTS = 1_000;
|
|
83
|
+
// Long-poll retry backoff. A transient failure (a Telegram 5xx, a dropped
|
|
84
|
+
// authority socket in the resident, or a relayed gateway 409 during a
|
|
85
|
+
// getUpdates-session reclaim) must not exit the sense process; the loop waits
|
|
86
|
+
// and retries, doubling from the base up to the cap and resetting on success.
|
|
87
|
+
const TELEGRAM_POLL_RETRY_BASE_MS = 1_000;
|
|
88
|
+
const TELEGRAM_POLL_RETRY_MAX_MS = 30_000;
|
|
83
89
|
const TELEGRAM_UPDATE_DIGEST_DOMAIN = "ouroboros.telegram.update.v1";
|
|
84
90
|
const TELEGRAM_UPDATE_SEQUENCE_DOMAIN = "ouroboros.telegram.update-sequence.v1";
|
|
85
91
|
const TELEGRAM_UPDATE_DIGEST = /^tgu_[A-Za-z0-9_-]{43}$/u;
|
|
@@ -469,14 +475,26 @@ class FileTelegramUpdateInboxStore {
|
|
|
469
475
|
}
|
|
470
476
|
}
|
|
471
477
|
exports.FileTelegramUpdateInboxStore = FileTelegramUpdateInboxStore;
|
|
478
|
+
// A poll failure is retryable when it is a transient transport condition: a
|
|
479
|
+
// Telegram Bot API error (network/HTTP), or an authority-socket error flagged
|
|
480
|
+
// transient by the resident's socket client (a dropped or refused connection).
|
|
481
|
+
// Dispatch and durable-audit invariant failures are not transport conditions;
|
|
482
|
+
// they must surface rather than be spun on indefinitely.
|
|
483
|
+
function isRetryablePollError(error) {
|
|
484
|
+
if (error instanceof TelegramApiError)
|
|
485
|
+
return true;
|
|
486
|
+
return error instanceof Error
|
|
487
|
+
&& error.isTransientTransportError === true;
|
|
488
|
+
}
|
|
472
489
|
function createTelegramLongPoll(options) {
|
|
473
490
|
let nextUpdateId = options.offsetStore.load();
|
|
474
491
|
const shutdown = new AbortController();
|
|
475
|
-
const retryAfterPollError = (signal) => new Promise((resolve
|
|
476
|
-
const
|
|
492
|
+
const retryAfterPollError = (signal, attempt) => new Promise((resolve) => {
|
|
493
|
+
const delay = Math.min(TELEGRAM_POLL_RETRY_BASE_MS * 2 ** Math.max(0, attempt - 1), TELEGRAM_POLL_RETRY_MAX_MS);
|
|
494
|
+
const timer = setTimeout(resolve, delay);
|
|
477
495
|
signal.addEventListener("abort", () => {
|
|
478
496
|
clearTimeout(timer);
|
|
479
|
-
|
|
497
|
+
resolve();
|
|
480
498
|
}, { once: true });
|
|
481
499
|
});
|
|
482
500
|
const inboundAttachments = (message) => {
|
|
@@ -690,23 +708,37 @@ function createTelegramLongPoll(options) {
|
|
|
690
708
|
pollOnce,
|
|
691
709
|
async run(signal) {
|
|
692
710
|
const runSignal = signal ? AbortSignal.any([shutdown.signal, signal]) : shutdown.signal;
|
|
711
|
+
let consecutiveFailures = 0;
|
|
693
712
|
while (!runSignal.aborted) {
|
|
694
713
|
try {
|
|
695
714
|
await pollOnce(runSignal);
|
|
715
|
+
consecutiveFailures = 0;
|
|
696
716
|
}
|
|
697
717
|
catch (error) {
|
|
698
718
|
if (shutdown.signal.aborted || signal?.aborted)
|
|
699
719
|
return;
|
|
700
|
-
|
|
720
|
+
// Only transient transport failures are retried here: a Telegram API
|
|
721
|
+
// error, or a dropped/again-failing authority socket in the resident.
|
|
722
|
+
// Exiting the process on those (as an unhandled throw would) only
|
|
723
|
+
// trades an in-process retry for a heavier container/daemon restart
|
|
724
|
+
// cycle, which is what produced the observed ~10s crash-loops. A
|
|
725
|
+
// non-transport failure (a dispatch or durable-audit invariant) still
|
|
726
|
+
// surfaces so it is not silently spun on.
|
|
727
|
+
if (!isRetryablePollError(error))
|
|
701
728
|
throw error;
|
|
729
|
+
consecutiveFailures += 1;
|
|
702
730
|
(0, runtime_1.emitNervesEvent)({
|
|
703
731
|
level: "warn",
|
|
704
732
|
component: "senses",
|
|
705
733
|
event: "telegram.poll_retry",
|
|
706
734
|
message: "Telegram long poll request failed; retrying",
|
|
707
|
-
meta: {
|
|
735
|
+
meta: {
|
|
736
|
+
attempt: consecutiveFailures,
|
|
737
|
+
...(error instanceof TelegramApiError ? { status: error.status, errorCode: error.errorCode } : {}),
|
|
738
|
+
reason: error.message,
|
|
739
|
+
},
|
|
708
740
|
});
|
|
709
|
-
await retryAfterPollError(runSignal);
|
|
741
|
+
await retryAfterPollError(runSignal, consecutiveFailures);
|
|
710
742
|
}
|
|
711
743
|
}
|
|
712
744
|
},
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.832",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.832",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|