@ouro.bot/cli 0.1.0-alpha.830 → 0.1.0-alpha.831

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 CHANGED
@@ -1,6 +1,12 @@
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.831",
6
+ "changes": [
7
+ "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."
8
+ ]
9
+ },
4
10
  {
5
11
  "version": "0.1.0-alpha.830",
6
12
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.830",
2
+ "runtimeVersion": "0.1.0-alpha.831",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -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.830</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.831</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -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(new Error("frontend socket closed"));
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
- this.failPending(error);
96
+ const transient = markTransientTransport(error);
97
+ this.failPending(transient);
88
98
  for (const listener of this.closeListeners)
89
- listener(error);
99
+ listener(transient);
90
100
  });
91
101
  socket.on("data", (chunk) => this.handleData(chunk.toString("utf8")));
92
102
  socket.on("close", () => {
93
- const error = new Error("frontend socket closed");
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, reject) => {
476
- const timer = setTimeout(resolve, 1_000);
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
- reject(signal.reason);
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
- if (!(error instanceof TelegramApiError))
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: { status: error.status, errorCode: error.errorCode },
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
  },
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.830",
3
+ "version": "0.1.0-alpha.831",
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.830",
9
+ "version": "0.1.0-alpha.831",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.830",
3
+ "version": "0.1.0-alpha.831",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },