@teamvibe/poller 0.1.66 → 0.1.68
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/claude-spawner.d.ts +5 -0
- package/dist/claude-spawner.js +70 -4
- package/dist/config.d.ts +4 -0
- package/dist/config.js +2 -1
- package/dist/poller.js +12 -3
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/dist/claude-spawner.d.ts
CHANGED
|
@@ -13,5 +13,10 @@ export declare function spawnClaudeCode(msg: TeamVibeQueueMessage, sessionLog: S
|
|
|
13
13
|
newSessionId?: string;
|
|
14
14
|
}>;
|
|
15
15
|
export declare function getActiveProcessCount(): number;
|
|
16
|
+
/**
|
|
17
|
+
* Kill a zombie process for a given thread ID.
|
|
18
|
+
* Returns true if a process was found and killed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function killZombieForThread(threadId: string): boolean;
|
|
16
21
|
export declare function isAtCapacity(): boolean;
|
|
17
22
|
export declare function killAllActiveProcesses(): Promise<void>;
|
package/dist/claude-spawner.js
CHANGED
|
@@ -128,6 +128,14 @@ export function buildPrompt(msg, resumeHint) {
|
|
|
128
128
|
: `Channel ${slack.channel}`;
|
|
129
129
|
prompt += `**Channel:** ${channelType}\n`;
|
|
130
130
|
}
|
|
131
|
+
// Inject human-readable timestamp from Slack message ts
|
|
132
|
+
if (msg.response_context.slack?.message_ts) {
|
|
133
|
+
const msgDate = new Date(parseFloat(msg.response_context.slack.message_ts) * 1000);
|
|
134
|
+
const dayOfWeek = msgDate.toLocaleDateString('en-US', { weekday: 'long', timeZone: 'UTC' });
|
|
135
|
+
const dateStr = msgDate.toISOString().slice(0, 10);
|
|
136
|
+
const timeStr = msgDate.toISOString().slice(11, 16);
|
|
137
|
+
prompt += `**Sent at:** ${dayOfWeek}, ${dateStr} ${timeStr} UTC\n`;
|
|
138
|
+
}
|
|
131
139
|
prompt += `\n**Message:**\n${msg.text}`;
|
|
132
140
|
if (msg.attachments.length > 0) {
|
|
133
141
|
prompt += '\n\n**Attachments:**\n';
|
|
@@ -152,8 +160,8 @@ export function buildPrompt(msg, resumeHint) {
|
|
|
152
160
|
if (msg.type === 'view_closed' && msg.modal) {
|
|
153
161
|
prompt += `\n\n**Modal Dismissed:** User closed "${msg.modal.callbackId}" without submitting.`;
|
|
154
162
|
}
|
|
155
|
-
if (msg.teamvibe.
|
|
156
|
-
prompt += '\n\n**Channel Instructions:**\n' + msg.teamvibe.
|
|
163
|
+
if (msg.teamvibe.emailHookPrompt) {
|
|
164
|
+
prompt += '\n\n**Channel Instructions:**\n' + msg.teamvibe.emailHookPrompt + '\n';
|
|
157
165
|
}
|
|
158
166
|
if (resumeHint) {
|
|
159
167
|
prompt += `\n\n${resumeHint}`;
|
|
@@ -333,8 +341,9 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
333
341
|
},
|
|
334
342
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
335
343
|
});
|
|
336
|
-
// Track active process
|
|
344
|
+
// Track active process by message ID and thread ID
|
|
337
345
|
activeProcesses.set(msg.id, proc);
|
|
346
|
+
activeProcessesByThread.set(msg.thread_id, proc);
|
|
338
347
|
proc.stdin?.write(prompt);
|
|
339
348
|
proc.stdin?.end();
|
|
340
349
|
let stdout = '';
|
|
@@ -342,6 +351,7 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
342
351
|
let stdoutBuffer = '';
|
|
343
352
|
let metrics;
|
|
344
353
|
let lastActivityTime = Date.now();
|
|
354
|
+
let networkErrorStart = null; // timestamp when continuous network errors began
|
|
345
355
|
proc.stdout.on('data', (data) => {
|
|
346
356
|
const chunk = data.toString();
|
|
347
357
|
stdout += chunk;
|
|
@@ -406,10 +416,21 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
406
416
|
}
|
|
407
417
|
}
|
|
408
418
|
});
|
|
419
|
+
const NETWORK_ERROR_PATTERNS = /ETIMEDOUT|ECONNRESET|ENOTFOUND|ECONNREFUSED|fetch failed|getaddrinfo/i;
|
|
409
420
|
proc.stderr.on('data', (data) => {
|
|
410
421
|
const chunk = data.toString();
|
|
411
422
|
stderr += chunk;
|
|
412
423
|
sessionLog.claude('stderr', chunk.trimEnd());
|
|
424
|
+
// Track continuous network errors for early kill
|
|
425
|
+
if (NETWORK_ERROR_PATTERNS.test(chunk)) {
|
|
426
|
+
if (networkErrorStart === null) {
|
|
427
|
+
networkErrorStart = Date.now();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
// Non-network stderr output resets the counter
|
|
432
|
+
networkErrorStart = null;
|
|
433
|
+
}
|
|
413
434
|
});
|
|
414
435
|
let timedOut = false;
|
|
415
436
|
const timeoutMinutes = Math.round(config.CLAUDE_TIMEOUT_MS / 60000);
|
|
@@ -427,6 +448,21 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
427
448
|
let idleWarned = false;
|
|
428
449
|
const idleWatchdog = setInterval(() => {
|
|
429
450
|
const idleMs = Date.now() - lastActivityTime;
|
|
451
|
+
// Network error watchdog: kill faster if seeing sustained network errors with no productive stdout
|
|
452
|
+
if (networkErrorStart !== null) {
|
|
453
|
+
const networkErrorDuration = Date.now() - networkErrorStart;
|
|
454
|
+
if (networkErrorDuration > config.NETWORK_ERROR_KILL_MS) {
|
|
455
|
+
const errorSec = Math.round(networkErrorDuration / 1000);
|
|
456
|
+
logger.error(`Claude Code has sustained network errors for ${errorSec}s, killing process`);
|
|
457
|
+
emitError('session', `Network errors for ${errorSec}s — killing process (likely network outage)`, {
|
|
458
|
+
context: { channelId: msg.teamvibe.channelId, threadId: msg.thread_id },
|
|
459
|
+
channelId: msg.teamvibe.channelId,
|
|
460
|
+
});
|
|
461
|
+
proc.kill('SIGTERM');
|
|
462
|
+
setTimeout(() => proc.kill('SIGKILL'), 5000);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
430
466
|
if (idleMs > config.IDLE_WATCHDOG_KILL_MS) {
|
|
431
467
|
const idleMin = Math.round(idleMs / 60000);
|
|
432
468
|
logger.error(`Claude Code idle for ${idleMin} minutes, killing process`);
|
|
@@ -452,6 +488,7 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
452
488
|
clearTimeout(timeout);
|
|
453
489
|
clearInterval(idleWatchdog);
|
|
454
490
|
activeProcesses.delete(msg.id);
|
|
491
|
+
activeProcessesByThread.delete(msg.thread_id);
|
|
455
492
|
const result = {
|
|
456
493
|
success: code === 0,
|
|
457
494
|
output: stdout,
|
|
@@ -470,6 +507,7 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
470
507
|
clearTimeout(timeout);
|
|
471
508
|
clearInterval(idleWatchdog);
|
|
472
509
|
activeProcesses.delete(msg.id);
|
|
510
|
+
activeProcessesByThread.delete(msg.thread_id);
|
|
473
511
|
emitError('session', `Claude Code process spawn error: ${error.message}`, {
|
|
474
512
|
channelId: msg.teamvibe.channelId,
|
|
475
513
|
context: { threadId: msg.thread_id },
|
|
@@ -491,8 +529,15 @@ export async function spawnClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMe
|
|
|
491
529
|
sessionLog.info('Resume failed, retrying as fresh session (session files may have been lost)');
|
|
492
530
|
retryResult = await runClaudeCode(msg, sessionLog, cwd, sessionId, true, lastMessageTs, onMessageSent, typingIndicator);
|
|
493
531
|
}
|
|
494
|
-
// If session ID is "already in use" (stale lock file), retry with
|
|
532
|
+
// If session ID is "already in use" (stale lock file), kill any zombie and retry with fresh ID
|
|
495
533
|
if (!retryResult.success && sessionId && retryResult.error?.includes('already in use')) {
|
|
534
|
+
// Kill zombie process for this thread if still alive
|
|
535
|
+
const killed = killZombieForThread(msg.thread_id);
|
|
536
|
+
if (killed) {
|
|
537
|
+
sessionLog.info('Killed zombie process before fresh session retry');
|
|
538
|
+
// Give the process a moment to fully exit and release lock files
|
|
539
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
540
|
+
}
|
|
496
541
|
sessionLog.info('Session ID already in use (stale lock), retrying with fresh session ID');
|
|
497
542
|
const { randomUUID } = await import('crypto');
|
|
498
543
|
const freshId = randomUUID();
|
|
@@ -502,9 +547,30 @@ export async function spawnClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMe
|
|
|
502
547
|
return retryResult;
|
|
503
548
|
}
|
|
504
549
|
const activeProcesses = new Map();
|
|
550
|
+
const activeProcessesByThread = new Map();
|
|
505
551
|
export function getActiveProcessCount() {
|
|
506
552
|
return activeProcesses.size;
|
|
507
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Kill a zombie process for a given thread ID.
|
|
556
|
+
* Returns true if a process was found and killed.
|
|
557
|
+
*/
|
|
558
|
+
export function killZombieForThread(threadId) {
|
|
559
|
+
const proc = activeProcessesByThread.get(threadId);
|
|
560
|
+
if (!proc || proc.exitCode !== null) {
|
|
561
|
+
activeProcessesByThread.delete(threadId);
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
logger.warn(`Killing zombie process for thread ${threadId} (pid: ${proc.pid})`);
|
|
565
|
+
emitError('session', `Killing zombie process for thread ${threadId}`, { severity: 'warn' });
|
|
566
|
+
proc.kill('SIGTERM');
|
|
567
|
+
setTimeout(() => {
|
|
568
|
+
if (proc.exitCode === null) {
|
|
569
|
+
proc.kill('SIGKILL');
|
|
570
|
+
}
|
|
571
|
+
}, 5000);
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
508
574
|
export function isAtCapacity() {
|
|
509
575
|
return activeProcesses.size >= config.MAX_CONCURRENT_SESSIONS;
|
|
510
576
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
15
15
|
LOCK_RETRY_INTERVAL_MS: z.ZodDefault<z.ZodNumber>;
|
|
16
16
|
IDLE_WATCHDOG_WARN_MS: z.ZodDefault<z.ZodNumber>;
|
|
17
17
|
IDLE_WATCHDOG_KILL_MS: z.ZodDefault<z.ZodNumber>;
|
|
18
|
+
NETWORK_ERROR_KILL_MS: z.ZodDefault<z.ZodNumber>;
|
|
18
19
|
TEAMVIBE_DATA_DIR: z.ZodDefault<z.ZodString>;
|
|
19
20
|
BRAINS_PATH: z.ZodDefault<z.ZodString>;
|
|
20
21
|
DEFAULT_BRAIN_PATH: z.ZodDefault<z.ZodString>;
|
|
@@ -41,6 +42,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
41
42
|
LOCK_RETRY_INTERVAL_MS: number;
|
|
42
43
|
IDLE_WATCHDOG_WARN_MS: number;
|
|
43
44
|
IDLE_WATCHDOG_KILL_MS: number;
|
|
45
|
+
NETWORK_ERROR_KILL_MS: number;
|
|
44
46
|
TEAMVIBE_DATA_DIR: string;
|
|
45
47
|
BRAINS_PATH: string;
|
|
46
48
|
DEFAULT_BRAIN_PATH: string;
|
|
@@ -73,6 +75,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
73
75
|
LOCK_RETRY_INTERVAL_MS?: number | undefined;
|
|
74
76
|
IDLE_WATCHDOG_WARN_MS?: number | undefined;
|
|
75
77
|
IDLE_WATCHDOG_KILL_MS?: number | undefined;
|
|
78
|
+
NETWORK_ERROR_KILL_MS?: number | undefined;
|
|
76
79
|
TEAMVIBE_DATA_DIR?: string | undefined;
|
|
77
80
|
BRAINS_PATH?: string | undefined;
|
|
78
81
|
DEFAULT_BRAIN_PATH?: string | undefined;
|
|
@@ -102,6 +105,7 @@ export declare const config: {
|
|
|
102
105
|
LOCK_RETRY_INTERVAL_MS: number;
|
|
103
106
|
IDLE_WATCHDOG_WARN_MS: number;
|
|
104
107
|
IDLE_WATCHDOG_KILL_MS: number;
|
|
108
|
+
NETWORK_ERROR_KILL_MS: number;
|
|
105
109
|
TEAMVIBE_DATA_DIR: string;
|
|
106
110
|
BRAINS_PATH: string;
|
|
107
111
|
DEFAULT_BRAIN_PATH: string;
|
package/dist/config.js
CHANGED
|
@@ -20,10 +20,11 @@ const configSchema = z.object({
|
|
|
20
20
|
VISIBILITY_TIMEOUT_SECONDS: z.coerce.number().default(60),
|
|
21
21
|
HEARTBEAT_INTERVAL_MS: z.coerce.number().default(40000), // 40 seconds
|
|
22
22
|
CLAUDE_TIMEOUT_MS: z.coerce.number().default(3600000), // 60 minutes
|
|
23
|
-
STALE_LOCK_TIMEOUT_MS: z.coerce.number().default(
|
|
23
|
+
STALE_LOCK_TIMEOUT_MS: z.coerce.number().default(900000), // 15 minutes (above idle watchdog kill time of 10 min)
|
|
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 minutes of continuous network errors → kill
|
|
27
28
|
// Paths
|
|
28
29
|
TEAMVIBE_DATA_DIR: z.string().default(DEFAULT_DATA_DIR),
|
|
29
30
|
BRAINS_PATH: z.string().default(''),
|
package/dist/poller.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { config } from './config.js';
|
|
2
2
|
import { logger } from './logger.js';
|
|
3
3
|
import { pollMessages, deleteMessage, } from './sqs-poller.js';
|
|
4
|
-
import { spawnClaudeCode, isAtCapacity, getActiveProcessCount, killAllActiveProcesses } from './claude-spawner.js';
|
|
4
|
+
import { spawnClaudeCode, isAtCapacity, getActiveProcessCount, killAllActiveProcesses, killZombieForThread } from './claude-spawner.js';
|
|
5
5
|
import { sendSlackError, addReaction, getUserInfo, startTypingIndicator } from './slack-client.js';
|
|
6
6
|
import { acquireSessionLock, releaseSessionLock, updateSessionId } from './session-store.js';
|
|
7
7
|
import { getBrainPath, ensureDirectories, ensureBaseBrain, pushBrainChanges } from './brain-manager.js';
|
|
@@ -25,8 +25,17 @@ async function runInboxLoop(threadId, kbPath, queueMessage, sessionLog) {
|
|
|
25
25
|
try {
|
|
26
26
|
let lockResult = await acquireSessionLock(threadId, kbPath);
|
|
27
27
|
if (!lockResult.success) {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// If lock acquisition failed, check for zombie processes holding the lock
|
|
29
|
+
const killed = killZombieForThread(threadId);
|
|
30
|
+
if (killed) {
|
|
31
|
+
sessionLog.info(`Killed zombie for thread ${threadId}, retrying lock acquisition`);
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
33
|
+
lockResult = await acquireSessionLock(threadId, kbPath);
|
|
34
|
+
}
|
|
35
|
+
if (!lockResult.success) {
|
|
36
|
+
sessionLog.info(`Session ${threadId} is locked, inbox message will be picked up by active session`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
30
39
|
}
|
|
31
40
|
const { session, lockToken } = lockResult;
|
|
32
41
|
let currentSessionId = session.session_id || undefined;
|
package/dist/types.d.ts
CHANGED