talon-agent 1.42.1 → 1.43.0
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/package.json
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { classify, friendlyMessage } from "../../../core/errors.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
recordMessageProcessed,
|
|
10
|
+
recordMessageReceived,
|
|
11
|
+
recordError,
|
|
12
|
+
} from "../../../util/watchdog.js";
|
|
9
13
|
import { appendDailyLog } from "../../../storage/daily-log.js";
|
|
10
14
|
import { log, logError } from "../../../util/log.js";
|
|
11
15
|
import { processAndReply } from "./delivery.js";
|
|
@@ -24,6 +28,9 @@ export function enqueueMessage(
|
|
|
24
28
|
numericChatId: number,
|
|
25
29
|
msg: QueuedMessage,
|
|
26
30
|
): void {
|
|
31
|
+
// Tell the watchdog work has arrived — stuck detection compares this
|
|
32
|
+
// against processing completions (idle chats must never look wedged).
|
|
33
|
+
recordMessageReceived();
|
|
27
34
|
const existing = messageQueues.get(chatId);
|
|
28
35
|
if (existing) {
|
|
29
36
|
if (existing.messages.length >= MAX_QUEUED_PER_CHAT) return;
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
getRecentHistory,
|
|
15
15
|
type HistoryMessage,
|
|
16
16
|
} from "../../../storage/history.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
recordMessageProcessed,
|
|
19
|
+
recordMessageReceived,
|
|
20
|
+
recordError,
|
|
21
|
+
} from "../../../util/watchdog.js";
|
|
18
22
|
import { log, logError, logWarn, logDebug } from "../../../util/log.js";
|
|
19
23
|
import { appendDailyLog } from "../../../storage/daily-log.js";
|
|
20
24
|
import { processAndReply, sendHtml } from "./delivery.js";
|
|
@@ -75,6 +79,9 @@ export function enqueueMessage(
|
|
|
75
79
|
numericChatId: number,
|
|
76
80
|
msg: QueuedMessage,
|
|
77
81
|
): void {
|
|
82
|
+
// Tell the watchdog work has arrived — stuck detection compares this
|
|
83
|
+
// against processing completions (idle chats must never look wedged).
|
|
84
|
+
recordMessageReceived();
|
|
78
85
|
const existing = messageQueues.get(chatId);
|
|
79
86
|
if (existing) {
|
|
80
87
|
if (existing.messages.length >= MAX_QUEUED_PER_CHAT) return; // drop excess
|
package/src/util/watchdog.ts
CHANGED
|
@@ -9,13 +9,46 @@ import { logWarn } from "./log.js";
|
|
|
9
9
|
// ── Message processing tracking ──────────────────────────────────────────────
|
|
10
10
|
|
|
11
11
|
let lastProcessedAt = Date.now();
|
|
12
|
+
let lastReceivedAt = 0;
|
|
12
13
|
let totalMessagesProcessed = 0;
|
|
13
14
|
const startTime = Date.now();
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Record that a message ARRIVED (enqueued for processing). Paired with
|
|
18
|
+
* `recordMessageProcessed`, this is what lets the watchdog tell a wedged
|
|
19
|
+
* message loop (work arrived, nothing finishing) apart from a bot that's
|
|
20
|
+
* simply idle because nobody is talking to it. Idle is not a fault.
|
|
21
|
+
*/
|
|
22
|
+
export function recordMessageReceived(): void {
|
|
23
|
+
lastReceivedAt = Date.now();
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
/** Record that a message was successfully processed. */
|
|
16
27
|
export function recordMessageProcessed(): void {
|
|
17
28
|
lastProcessedAt = Date.now();
|
|
18
29
|
totalMessagesProcessed++;
|
|
30
|
+
resetStuckWarnBackoff();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Test-only: reset the activity clocks to "just processed, nothing pending"
|
|
35
|
+
* at the CURRENT Date.now(). Fake-timer suites need this because each test
|
|
36
|
+
* restarts its fake clock at the real now, while timestamps recorded by an
|
|
37
|
+
* earlier test may sit hours into that test's own fake future.
|
|
38
|
+
*/
|
|
39
|
+
export function resetWatchdogActivityForTests(): void {
|
|
40
|
+
lastProcessedAt = Date.now();
|
|
41
|
+
lastReceivedAt = 0;
|
|
42
|
+
resetStuckWarnBackoff();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* How long the newest RECEIVED message has been waiting with no processing
|
|
47
|
+
* completed after it. Zero when idle or keeping up.
|
|
48
|
+
*/
|
|
49
|
+
function stuckMs(now: number): number {
|
|
50
|
+
if (lastReceivedAt === 0 || lastReceivedAt <= lastProcessedAt) return 0;
|
|
51
|
+
return now - lastReceivedAt;
|
|
19
52
|
}
|
|
20
53
|
|
|
21
54
|
/** Get total messages processed since startup. */
|
|
@@ -53,17 +86,38 @@ export function getRecentErrors(limit = 5): ErrorRecord[] {
|
|
|
53
86
|
|
|
54
87
|
// ── Inactivity monitoring ────────────────────────────────────────────────────
|
|
55
88
|
|
|
56
|
-
const
|
|
89
|
+
const STUCK_WARN_MS = 10 * 60 * 1000; // 10 minutes with unprocessed work
|
|
90
|
+
const STUCK_WARN_MAX_BACKOFF_MS = 60 * 60 * 1000;
|
|
57
91
|
let watchdogTimer: ReturnType<typeof setInterval> | null = null;
|
|
92
|
+
let stuckWarnBackoffMs = STUCK_WARN_MS;
|
|
93
|
+
let nextStuckWarnAt = 0;
|
|
94
|
+
|
|
95
|
+
function resetStuckWarnBackoff(): void {
|
|
96
|
+
stuckWarnBackoffMs = STUCK_WARN_MS;
|
|
97
|
+
nextStuckWarnAt = 0;
|
|
98
|
+
}
|
|
58
99
|
|
|
59
100
|
export function startWatchdog(workspaceDir?: string): void {
|
|
60
101
|
if (watchdogTimer) return;
|
|
61
102
|
|
|
62
103
|
watchdogTimer = setInterval(() => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
104
|
+
// Warn only when work is actually stuck — a message arrived and nothing
|
|
105
|
+
// has finished processing since. A quiet chat used to trip this every
|
|
106
|
+
// minute all night ("No messages processed for N minutes"), burying real
|
|
107
|
+
// warnings in noise. Repeat warnings back off exponentially.
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const stuck = stuckMs(now);
|
|
110
|
+
if (stuck > STUCK_WARN_MS && now >= nextStuckWarnAt) {
|
|
111
|
+
const mins = Math.round(stuck / 60000);
|
|
112
|
+
logWarn(
|
|
113
|
+
"watchdog",
|
|
114
|
+
`Message received ${mins} minutes ago is still unprocessed — the message loop may be wedged`,
|
|
115
|
+
);
|
|
116
|
+
nextStuckWarnAt = now + stuckWarnBackoffMs;
|
|
117
|
+
stuckWarnBackoffMs = Math.min(
|
|
118
|
+
stuckWarnBackoffMs * 2,
|
|
119
|
+
STUCK_WARN_MAX_BACKOFF_MS,
|
|
120
|
+
);
|
|
67
121
|
}
|
|
68
122
|
|
|
69
123
|
// Ensure workspace still exists (might have been deleted externally)
|
|
@@ -101,8 +155,11 @@ export function getHealthStatus(): HealthStatus {
|
|
|
101
155
|
const now = Date.now();
|
|
102
156
|
const msSinceLastMessage = now - lastProcessedAt;
|
|
103
157
|
return {
|
|
104
|
-
// Unhealthy
|
|
105
|
-
|
|
158
|
+
// Unhealthy only when the message loop is actually stuck: a received
|
|
159
|
+
// message has gone 30+ minutes with no processing completing after it.
|
|
160
|
+
// Mere idleness (nobody texting the bot) used to flip this false and
|
|
161
|
+
// show a red "unhealthy" row in the companion diagnostics overnight.
|
|
162
|
+
healthy: stuckMs(now) < 30 * 60_000,
|
|
106
163
|
uptimeMs: now - startTime,
|
|
107
164
|
totalMessagesProcessed,
|
|
108
165
|
lastProcessedAt,
|