talon-agent 3.21.0 → 3.22.1

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.
@@ -129,10 +129,17 @@ export async function disconnectUserClient(): Promise<void> {
129
129
  const CHECK_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
130
130
 
131
131
  let reconnecting = false;
132
+ // Bumped by stopConnectionMonitor so a tick whose awaits straddle a shutdown
133
+ // can tell that its own client was torn down under it — the liveness probe
134
+ // rejecting because *we* disconnected must not trigger a reconnect, or
135
+ // shutdown resurrects the client it just closed.
136
+ let monitorGeneration = 0;
132
137
 
133
138
  function startConnectionMonitor(): void {
134
139
  if (reconnectTimer) return;
140
+ const gen = monitorGeneration;
135
141
  reconnectTimer = setInterval(async () => {
142
+ if (gen !== monitorGeneration) return;
136
143
  if (!client) return;
137
144
  if (reconnecting) return; // prevent overlapping reconnect attempts
138
145
 
@@ -146,10 +153,12 @@ function startConnectionMonitor(): void {
146
153
  await client.getMe();
147
154
  return; // genuinely alive
148
155
  } catch {
156
+ if (gen !== monitorGeneration) return; // monitor stopped mid-probe
149
157
  logWarn("userbot", "Liveness check failed — treating socket as dead.");
150
158
  }
151
159
  }
152
160
 
161
+ if (!client) return; // torn down while the probe was in flight
153
162
  reconnecting = true;
154
163
  logWarn("userbot", "Connection lost, attempting reconnect...");
155
164
  try {
@@ -160,6 +169,7 @@ function startConnectionMonitor(): void {
160
169
  logWarn("userbot", "Reconnected but not authorized.");
161
170
  }
162
171
  } catch (err) {
172
+ if (gen !== monitorGeneration) return; // stopped while reconnecting
163
173
  logError("userbot", "Reconnect failed", err);
164
174
  // Try a full re-init on next check
165
175
  if (storedApiId && storedApiHash) {
@@ -189,6 +199,7 @@ function startConnectionMonitor(): void {
189
199
  }
190
200
 
191
201
  function stopConnectionMonitor(): void {
202
+ monitorGeneration++;
192
203
  if (reconnectTimer) {
193
204
  clearInterval(reconnectTimer);
194
205
  reconnectTimer = null;
@@ -30,6 +30,18 @@ export function recordMessageProcessed(): void {
30
30
  resetStuckWarnBackoff();
31
31
  }
32
32
 
33
+ /**
34
+ * Record that a message's turn settled in failure. The loop is moving —
35
+ * the error was reported and the queue slot freed — so advance the stuck
36
+ * clock without counting a success. Without this, one failed turn leaves
37
+ * `lastProcessedAt` stale forever and the watchdog reports a wedged loop
38
+ * (and an unhealthy bridge) on a bot that is actually idle.
39
+ */
40
+ export function recordMessageSettled(): void {
41
+ lastProcessedAt = Date.now();
42
+ resetStuckWarnBackoff();
43
+ }
44
+
33
45
  /**
34
46
  * Test-only: reset the activity clocks to "just processed, nothing pending"
35
47
  * at the CURRENT Date.now(). Fake-timer suites need this because each test