@teamvibe/poller 0.2.3 → 0.2.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/dist/config.d.ts +4 -0
- package/dist/config.js +2 -1
- package/dist/poller.js +67 -4
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
16
16
|
IDLE_WATCHDOG_WARN_MS: z.ZodDefault<z.ZodNumber>;
|
|
17
17
|
IDLE_WATCHDOG_KILL_MS: z.ZodDefault<z.ZodNumber>;
|
|
18
18
|
NETWORK_ERROR_KILL_MS: z.ZodDefault<z.ZodNumber>;
|
|
19
|
+
MAX_CONSECUTIVE_NET_FAILURES: z.ZodDefault<z.ZodNumber>;
|
|
19
20
|
TEAMVIBE_DATA_DIR: z.ZodDefault<z.ZodString>;
|
|
20
21
|
BRAINS_PATH: z.ZodDefault<z.ZodString>;
|
|
21
22
|
DEFAULT_BRAIN_PATH: z.ZodDefault<z.ZodString>;
|
|
@@ -45,6 +46,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
45
46
|
IDLE_WATCHDOG_WARN_MS: number;
|
|
46
47
|
IDLE_WATCHDOG_KILL_MS: number;
|
|
47
48
|
NETWORK_ERROR_KILL_MS: number;
|
|
49
|
+
MAX_CONSECUTIVE_NET_FAILURES: number;
|
|
48
50
|
TEAMVIBE_DATA_DIR: string;
|
|
49
51
|
BRAINS_PATH: string;
|
|
50
52
|
DEFAULT_BRAIN_PATH: string;
|
|
@@ -80,6 +82,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
80
82
|
IDLE_WATCHDOG_WARN_MS?: number | undefined;
|
|
81
83
|
IDLE_WATCHDOG_KILL_MS?: number | undefined;
|
|
82
84
|
NETWORK_ERROR_KILL_MS?: number | undefined;
|
|
85
|
+
MAX_CONSECUTIVE_NET_FAILURES?: number | undefined;
|
|
83
86
|
TEAMVIBE_DATA_DIR?: string | undefined;
|
|
84
87
|
BRAINS_PATH?: string | undefined;
|
|
85
88
|
DEFAULT_BRAIN_PATH?: string | undefined;
|
|
@@ -112,6 +115,7 @@ export declare const config: {
|
|
|
112
115
|
IDLE_WATCHDOG_WARN_MS: number;
|
|
113
116
|
IDLE_WATCHDOG_KILL_MS: number;
|
|
114
117
|
NETWORK_ERROR_KILL_MS: number;
|
|
118
|
+
MAX_CONSECUTIVE_NET_FAILURES: number;
|
|
115
119
|
TEAMVIBE_DATA_DIR: string;
|
|
116
120
|
BRAINS_PATH: string;
|
|
117
121
|
DEFAULT_BRAIN_PATH: string;
|
package/dist/config.js
CHANGED
|
@@ -24,7 +24,8 @@ const configSchema = z.object({
|
|
|
24
24
|
LOCK_RETRY_INTERVAL_MS: z.coerce.number().default(30000), // 30 seconds
|
|
25
25
|
IDLE_WATCHDOG_WARN_MS: z.coerce.number().default(300000), // 5 minutes
|
|
26
26
|
IDLE_WATCHDOG_KILL_MS: z.coerce.number().default(600000), // 10 minutes
|
|
27
|
-
NETWORK_ERROR_KILL_MS: z.coerce.number().default(120000), // 2
|
|
27
|
+
NETWORK_ERROR_KILL_MS: z.coerce.number().default(120000), // Per-session: kill stuck session after 2 min of network errors
|
|
28
|
+
MAX_CONSECUTIVE_NET_FAILURES: z.coerce.number().default(5), // Poll loop: DNS flush at N, exit(1) at 2×N consecutive failures
|
|
28
29
|
// Paths
|
|
29
30
|
TEAMVIBE_DATA_DIR: z.string().default(DEFAULT_DATA_DIR),
|
|
30
31
|
BRAINS_PATH: z.string().default(''),
|
package/dist/poller.js
CHANGED
|
@@ -13,6 +13,10 @@ import { writeMessage, pickNextMessage, markProcessed, markFailed, hasUnprocesse
|
|
|
13
13
|
import { Cron } from 'croner';
|
|
14
14
|
import { isControlMessage, handleControlMessage } from './control-handler.js';
|
|
15
15
|
import { checkForUpdate } from './auto-update.js';
|
|
16
|
+
import { exec } from 'child_process';
|
|
17
|
+
import { promisify } from 'util';
|
|
18
|
+
import { platform } from 'os';
|
|
19
|
+
const execAsync = promisify(exec);
|
|
16
20
|
// Track active message processing
|
|
17
21
|
const processingMessages = new Set();
|
|
18
22
|
// Poller auto-update cron (separate from brain heartbeats)
|
|
@@ -49,6 +53,38 @@ function stopAutoUpdateCron() {
|
|
|
49
53
|
autoUpdateJob = null;
|
|
50
54
|
}
|
|
51
55
|
}
|
|
56
|
+
// --- Circuit breaker ---
|
|
57
|
+
let consecutiveNetFailures = 0;
|
|
58
|
+
const NETWORK_ERROR_CODES = ['ENOTFOUND', 'ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED'];
|
|
59
|
+
function isNetworkError(error) {
|
|
60
|
+
if (error instanceof Error) {
|
|
61
|
+
const code = error.code || '';
|
|
62
|
+
const message = error.message || '';
|
|
63
|
+
return (NETWORK_ERROR_CODES.includes(code) ||
|
|
64
|
+
message.includes('getaddrinfo') ||
|
|
65
|
+
message.includes('socket hang up'));
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
async function attemptDnsFlush() {
|
|
70
|
+
const cmd = platform() === 'darwin'
|
|
71
|
+
? 'dscacheutil -flushcache'
|
|
72
|
+
: platform() === 'linux'
|
|
73
|
+
? 'resolvectl flush-caches'
|
|
74
|
+
: null;
|
|
75
|
+
if (!cmd) {
|
|
76
|
+
logger.warn(`Circuit breaker: DNS flush not supported on platform "${platform()}"`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
logger.warn('Circuit breaker: Attempting DNS cache flush...');
|
|
80
|
+
try {
|
|
81
|
+
await execAsync(cmd, { timeout: 5000 });
|
|
82
|
+
logger.info('Circuit breaker: DNS cache flush completed');
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
logger.warn(`Circuit breaker: DNS cache flush failed: ${err}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
52
88
|
// Track which threads have an active processing loop
|
|
53
89
|
const activeLoops = new Set();
|
|
54
90
|
async function runInboxLoop(threadId, kbPath, queueMessage, sessionLog) {
|
|
@@ -314,6 +350,7 @@ async function pollLoop() {
|
|
|
314
350
|
const availableSlots = config.MAX_CONCURRENT_SESSIONS - getActiveProcessCount();
|
|
315
351
|
logger.debug(`Polling for up to ${availableSlots} messages...`);
|
|
316
352
|
const messages = await pollMessages(availableSlots);
|
|
353
|
+
consecutiveNetFailures = 0; // Successful poll — reset circuit breaker
|
|
317
354
|
if (messages.length === 0) {
|
|
318
355
|
continue;
|
|
319
356
|
}
|
|
@@ -347,8 +384,34 @@ async function pollLoop() {
|
|
|
347
384
|
Promise.all(processPromises);
|
|
348
385
|
}
|
|
349
386
|
catch (error) {
|
|
350
|
-
|
|
351
|
-
|
|
387
|
+
if (isNetworkError(error)) {
|
|
388
|
+
consecutiveNetFailures++;
|
|
389
|
+
logger.error(`Network error in poll loop (${consecutiveNetFailures}/${config.MAX_CONSECUTIVE_NET_FAILURES}):`, error);
|
|
390
|
+
if (consecutiveNetFailures === config.MAX_CONSECUTIVE_NET_FAILURES) {
|
|
391
|
+
await attemptDnsFlush();
|
|
392
|
+
}
|
|
393
|
+
if (consecutiveNetFailures >= config.MAX_CONSECUTIVE_NET_FAILURES * 2) {
|
|
394
|
+
logger.error(`Circuit breaker triggered: ${consecutiveNetFailures} consecutive network failures. Exiting for restart.`);
|
|
395
|
+
emitEvent('poller.circuit_breaker', '', {
|
|
396
|
+
reason: 'sustained_network_failure',
|
|
397
|
+
count: consecutiveNetFailures,
|
|
398
|
+
});
|
|
399
|
+
// Hard timeout on shutdown — network may be down, don't hang
|
|
400
|
+
await Promise.race([
|
|
401
|
+
shutdown('CIRCUIT_BREAKER', 1),
|
|
402
|
+
sleep(15000),
|
|
403
|
+
]);
|
|
404
|
+
process.exit(1); // Fallback if shutdown didn't call process.exit (e.g. timeout)
|
|
405
|
+
}
|
|
406
|
+
// Exponential backoff: 5s, 10s, 20s, 40s... capped at 60s
|
|
407
|
+
const backoffMs = Math.min(5000 * Math.pow(2, consecutiveNetFailures - 1), 60000);
|
|
408
|
+
await sleep(backoffMs);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
// Non-network errors don't affect the circuit breaker counter
|
|
412
|
+
logger.error('Error in poll loop:', error);
|
|
413
|
+
await sleep(5000);
|
|
414
|
+
}
|
|
352
415
|
}
|
|
353
416
|
}
|
|
354
417
|
}
|
|
@@ -357,7 +420,7 @@ function sleep(ms) {
|
|
|
357
420
|
}
|
|
358
421
|
// Graceful shutdown
|
|
359
422
|
let shuttingDown = false;
|
|
360
|
-
async function shutdown(signal) {
|
|
423
|
+
async function shutdown(signal, exitCode = 0) {
|
|
361
424
|
if (shuttingDown)
|
|
362
425
|
return;
|
|
363
426
|
shuttingDown = true;
|
|
@@ -382,7 +445,7 @@ async function shutdown(signal) {
|
|
|
382
445
|
// Final flush after all processes stopped — captures errors emitted during shutdown
|
|
383
446
|
await flushEventQueue();
|
|
384
447
|
logger.info('Shutdown complete');
|
|
385
|
-
process.exit(
|
|
448
|
+
process.exit(exitCode);
|
|
386
449
|
}
|
|
387
450
|
export async function startPoller() {
|
|
388
451
|
logger.info('TeamVibe Poller starting...');
|