@teamvibe/poller 0.1.29 → 0.1.31
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 +2 -1
- package/dist/claude-spawner.js +66 -7
- package/dist/config.js +2 -2
- package/dist/poller.js +8 -6
- package/dist/slack-client.d.ts +7 -1
- package/dist/slack-client.js +19 -7
- package/package.json +1 -1
package/dist/claude-spawner.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { TeamVibeQueueMessage } from './types.js';
|
|
2
2
|
import { type SessionLogger } from './logger.js';
|
|
3
|
+
import type { TypingIndicator } from './slack-client.js';
|
|
3
4
|
export interface SpawnResult {
|
|
4
5
|
success: boolean;
|
|
5
6
|
output: string;
|
|
6
7
|
exitCode: number | null;
|
|
7
8
|
error?: string;
|
|
8
9
|
}
|
|
9
|
-
export declare function spawnClaudeCode(msg: TeamVibeQueueMessage, sessionLog: SessionLogger, cwd: string, sessionId?: string, isFirstMessage?: boolean, lastMessageTs?: string, onMessageSent?: () => void): Promise<SpawnResult & {
|
|
10
|
+
export declare function spawnClaudeCode(msg: TeamVibeQueueMessage, sessionLog: SessionLogger, cwd: string, sessionId?: string, isFirstMessage?: boolean, lastMessageTs?: string, onMessageSent?: () => void, typingIndicator?: TypingIndicator): Promise<SpawnResult & {
|
|
10
11
|
newSessionId?: string;
|
|
11
12
|
}>;
|
|
12
13
|
export declare function getActiveProcessCount(): number;
|
package/dist/claude-spawner.js
CHANGED
|
@@ -4,6 +4,45 @@ import { WebClient } from '@slack/web-api';
|
|
|
4
4
|
import { config } from './config.js';
|
|
5
5
|
import { logger } from './logger.js';
|
|
6
6
|
import { getBaseBrainPath } from './brain-manager.js';
|
|
7
|
+
/**
|
|
8
|
+
* Maps tool names (or prefixes) to human-readable status messages.
|
|
9
|
+
* More specific patterns are checked first.
|
|
10
|
+
*/
|
|
11
|
+
const TOOL_STATUS_MAP = [
|
|
12
|
+
// Slack MCP tools
|
|
13
|
+
{ pattern: 'mcp__slack__search_messages', status: 'Searching Slack messages...' },
|
|
14
|
+
{ pattern: 'mcp__slack__read_thread', status: 'Reading thread...' },
|
|
15
|
+
{ pattern: 'mcp__slack__send_message', status: 'Sending message...' },
|
|
16
|
+
{ pattern: 'mcp__slack__set_status', status: '' }, // Don't override Claude's own status
|
|
17
|
+
{ pattern: 'mcp__slack__upload_snippet', status: 'Uploading snippet...' },
|
|
18
|
+
{ pattern: /^mcp__slack__/, status: 'Working with Slack...' },
|
|
19
|
+
// Web tools
|
|
20
|
+
{ pattern: 'WebSearch', status: 'Searching the web...' },
|
|
21
|
+
{ pattern: 'WebFetch', status: 'Reading a webpage...' },
|
|
22
|
+
// File/code tools
|
|
23
|
+
{ pattern: 'Read', status: 'Reading files...' },
|
|
24
|
+
{ pattern: 'Grep', status: 'Searching code...' },
|
|
25
|
+
{ pattern: 'Glob', status: 'Finding files...' },
|
|
26
|
+
{ pattern: 'Edit', status: 'Editing code...' },
|
|
27
|
+
{ pattern: 'Write', status: 'Writing code...' },
|
|
28
|
+
{ pattern: 'Bash', status: 'Running a command...' },
|
|
29
|
+
{ pattern: 'Agent', status: 'Delegating to a sub-agent...' },
|
|
30
|
+
// Generic MCP fallback
|
|
31
|
+
{ pattern: /^mcp__/, status: 'Using an integration...' },
|
|
32
|
+
];
|
|
33
|
+
function getStatusForTool(toolName) {
|
|
34
|
+
for (const { pattern, status } of TOOL_STATUS_MAP) {
|
|
35
|
+
if (typeof pattern === 'string') {
|
|
36
|
+
if (toolName === pattern)
|
|
37
|
+
return status;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
if (pattern.test(toolName))
|
|
41
|
+
return status;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
7
46
|
// Per-token Slack client cache for thread context
|
|
8
47
|
const slackClients = new Map();
|
|
9
48
|
function getSlackClient(botToken) {
|
|
@@ -126,7 +165,7 @@ function formatStreamEvent(event) {
|
|
|
126
165
|
return null;
|
|
127
166
|
}
|
|
128
167
|
}
|
|
129
|
-
async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent) {
|
|
168
|
+
async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent, typingIndicator) {
|
|
130
169
|
const slackContext = msg.response_context.slack;
|
|
131
170
|
const isCronMessage = msg.source === 'cron';
|
|
132
171
|
if (!slackContext && !isCronMessage) {
|
|
@@ -231,6 +270,20 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
231
270
|
event.name === 'mcp__slack__send_message') {
|
|
232
271
|
onMessageSent();
|
|
233
272
|
}
|
|
273
|
+
// Auto-update typing indicator status based on tool usage
|
|
274
|
+
if (typingIndicator) {
|
|
275
|
+
if (event.type === 'tool_use' && event.name) {
|
|
276
|
+
const status = getStatusForTool(event.name);
|
|
277
|
+
if (status !== null && status !== '') {
|
|
278
|
+
typingIndicator.updateStatus(status);
|
|
279
|
+
}
|
|
280
|
+
// When status is '' (e.g. set_status tool), don't override — Claude is managing it
|
|
281
|
+
}
|
|
282
|
+
else if (event.type === 'assistant') {
|
|
283
|
+
// Reset to "Thinking..." when Claude is reasoning between tool calls
|
|
284
|
+
typingIndicator.updateStatus('Thinking...');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
234
287
|
}
|
|
235
288
|
catch {
|
|
236
289
|
const truncated = line.length > 500 ? line.slice(0, 500) + '...' : line;
|
|
@@ -243,8 +296,11 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
243
296
|
stderr += chunk;
|
|
244
297
|
sessionLog.claude('stderr', chunk.trimEnd());
|
|
245
298
|
});
|
|
299
|
+
let timedOut = false;
|
|
300
|
+
const timeoutMinutes = Math.round(config.CLAUDE_TIMEOUT_MS / 60000);
|
|
246
301
|
const timeout = setTimeout(() => {
|
|
247
|
-
|
|
302
|
+
timedOut = true;
|
|
303
|
+
logger.error(`Claude Code process timed out after ${timeoutMinutes} minutes`);
|
|
248
304
|
proc.kill('SIGTERM');
|
|
249
305
|
setTimeout(() => proc.kill('SIGKILL'), 5000);
|
|
250
306
|
}, config.CLAUDE_TIMEOUT_MS);
|
|
@@ -256,7 +312,10 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
256
312
|
output: stdout,
|
|
257
313
|
exitCode: code,
|
|
258
314
|
};
|
|
259
|
-
if (
|
|
315
|
+
if (timedOut) {
|
|
316
|
+
result.error = `Session timed out after ${timeoutMinutes} minutes. The task was still running when the time limit was reached. Consider breaking complex tasks into smaller steps.`;
|
|
317
|
+
}
|
|
318
|
+
else if (code !== 0) {
|
|
260
319
|
result.error = stderr || `Process exited with code ${code}`;
|
|
261
320
|
}
|
|
262
321
|
resolve(result);
|
|
@@ -273,20 +332,20 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
273
332
|
});
|
|
274
333
|
});
|
|
275
334
|
}
|
|
276
|
-
export async function spawnClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent) {
|
|
277
|
-
const result = await runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage, lastMessageTs, onMessageSent);
|
|
335
|
+
export async function spawnClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent, typingIndicator) {
|
|
336
|
+
const result = await runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage, lastMessageTs, onMessageSent, typingIndicator);
|
|
278
337
|
// If --resume failed, retry as a fresh session (session files may have been lost on container restart)
|
|
279
338
|
let retryResult = result;
|
|
280
339
|
if (!result.success && sessionId && !isFirstMessage) {
|
|
281
340
|
sessionLog.info('Resume failed, retrying as fresh session (session files may have been lost)');
|
|
282
|
-
retryResult = await runClaudeCode(msg, sessionLog, cwd, sessionId, true, lastMessageTs, onMessageSent);
|
|
341
|
+
retryResult = await runClaudeCode(msg, sessionLog, cwd, sessionId, true, lastMessageTs, onMessageSent, typingIndicator);
|
|
283
342
|
}
|
|
284
343
|
// If session ID is "already in use" (stale lock file), retry with a fresh session ID
|
|
285
344
|
if (!retryResult.success && sessionId && retryResult.error?.includes('already in use')) {
|
|
286
345
|
sessionLog.info('Session ID already in use (stale lock), retrying with fresh session ID');
|
|
287
346
|
const { randomUUID } = await import('crypto');
|
|
288
347
|
const freshId = randomUUID();
|
|
289
|
-
const freshResult = await runClaudeCode(msg, sessionLog, cwd, freshId, true, lastMessageTs, onMessageSent);
|
|
348
|
+
const freshResult = await runClaudeCode(msg, sessionLog, cwd, freshId, true, lastMessageTs, onMessageSent, typingIndicator);
|
|
290
349
|
return { ...freshResult, newSessionId: freshId };
|
|
291
350
|
}
|
|
292
351
|
return retryResult;
|
package/dist/config.js
CHANGED
|
@@ -19,8 +19,8 @@ const configSchema = z.object({
|
|
|
19
19
|
POLL_WAIT_TIME_SECONDS: z.coerce.number().default(20),
|
|
20
20
|
VISIBILITY_TIMEOUT_SECONDS: z.coerce.number().default(60),
|
|
21
21
|
HEARTBEAT_INTERVAL_MS: z.coerce.number().default(40000), // 40 seconds
|
|
22
|
-
CLAUDE_TIMEOUT_MS: z.coerce.number().default(
|
|
23
|
-
STALE_LOCK_TIMEOUT_MS: z.coerce.number().default(
|
|
22
|
+
CLAUDE_TIMEOUT_MS: z.coerce.number().default(3600000), // 60 minutes
|
|
23
|
+
STALE_LOCK_TIMEOUT_MS: z.coerce.number().default(3900000), // 65 minutes
|
|
24
24
|
// Paths
|
|
25
25
|
TEAMVIBE_DATA_DIR: z.string().default(DEFAULT_DATA_DIR),
|
|
26
26
|
BRAINS_PATH: z.string().default(''),
|
package/dist/poller.js
CHANGED
|
@@ -125,14 +125,14 @@ async function processMessage(received) {
|
|
|
125
125
|
processingMessages.add(messageId);
|
|
126
126
|
const heartbeat = startHeartbeat(receiptHandle, sessionLog);
|
|
127
127
|
// Start typing indicator
|
|
128
|
-
const
|
|
128
|
+
const typing = hasSlackContext
|
|
129
129
|
? startTypingIndicator(queueMessage)
|
|
130
130
|
: undefined;
|
|
131
131
|
try {
|
|
132
132
|
// Refresh base brain if cooldown has elapsed (5 min default)
|
|
133
133
|
await ensureBaseBrain();
|
|
134
|
-
const result = await spawnClaudeCode(queueMessage, sessionLog, kbPath, session.session_id || undefined, isFirstMessage, session.last_message_ts, () =>
|
|
135
|
-
|
|
134
|
+
const result = await spawnClaudeCode(queueMessage, sessionLog, kbPath, session.session_id || undefined, isFirstMessage, session.last_message_ts, () => typing?.stop(), typing);
|
|
135
|
+
typing?.stop();
|
|
136
136
|
// If Claude generated a new session ID (stale lock recovery), persist it
|
|
137
137
|
if (result.newSessionId && lockToken) {
|
|
138
138
|
await updateSessionId(threadId, lockToken, result.newSessionId);
|
|
@@ -151,8 +151,10 @@ async function processMessage(received) {
|
|
|
151
151
|
else {
|
|
152
152
|
sessionLog.error(`Claude Code failed: ${result.error}`);
|
|
153
153
|
if (hasSlackContext) {
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
const errorMessage = result.error || `Process exited with code ${result.exitCode}`;
|
|
155
|
+
const isTimeout = errorMessage.includes('timed out');
|
|
156
|
+
await sendSlackError(queueMessage, errorMessage);
|
|
157
|
+
await addReaction(queueMessage, isTimeout ? 'hourglass' : 'x');
|
|
156
158
|
}
|
|
157
159
|
if (lockToken) {
|
|
158
160
|
await releaseSessionLock(threadId, lockToken, 'idle');
|
|
@@ -162,7 +164,7 @@ async function processMessage(received) {
|
|
|
162
164
|
sessionLog.info('Message processed and deleted');
|
|
163
165
|
}
|
|
164
166
|
catch (error) {
|
|
165
|
-
|
|
167
|
+
typing?.stop();
|
|
166
168
|
sessionLog.error(`Error processing message: ${error instanceof Error ? error.message : error}`);
|
|
167
169
|
if (lockToken) {
|
|
168
170
|
try {
|
package/dist/slack-client.d.ts
CHANGED
|
@@ -7,5 +7,11 @@ export declare function sendSlackMessage(msg: TeamVibeQueueMessage, text: string
|
|
|
7
7
|
export declare function sendSlackError(msg: TeamVibeQueueMessage, error: string): Promise<void>;
|
|
8
8
|
export declare function addReaction(msg: TeamVibeQueueMessage, emoji: string): Promise<void>;
|
|
9
9
|
export declare function setThreadStatus(msg: TeamVibeQueueMessage, status: string): Promise<void>;
|
|
10
|
-
export
|
|
10
|
+
export interface TypingIndicator {
|
|
11
|
+
/** Update the displayed status text */
|
|
12
|
+
updateStatus: (newStatus: string) => void;
|
|
13
|
+
/** Stop the typing indicator and clear the status */
|
|
14
|
+
stop: () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function startTypingIndicator(msg: TeamVibeQueueMessage, statusText?: string): TypingIndicator;
|
|
11
17
|
export declare function removeReaction(msg: TeamVibeQueueMessage, emoji: string): Promise<void>;
|
package/dist/slack-client.js
CHANGED
|
@@ -60,10 +60,14 @@ export async function sendSlackError(msg, error) {
|
|
|
60
60
|
return;
|
|
61
61
|
const { channel, thread_ts } = msg.response_context.slack;
|
|
62
62
|
const slack = getSlackClient(msg.teamvibe.botToken);
|
|
63
|
+
const isTimeout = error.includes('timed out');
|
|
64
|
+
const text = isTimeout
|
|
65
|
+
? `:warning: ${error}`
|
|
66
|
+
: `Error processing your request:\n\`\`\`\n${error}\n\`\`\``;
|
|
63
67
|
await slack.chat.postMessage({
|
|
64
68
|
channel,
|
|
65
69
|
thread_ts,
|
|
66
|
-
text
|
|
70
|
+
text,
|
|
67
71
|
});
|
|
68
72
|
}
|
|
69
73
|
export async function addReaction(msg, emoji) {
|
|
@@ -99,13 +103,14 @@ export async function setThreadStatus(msg, status) {
|
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
export function startTypingIndicator(msg, statusText = 'is thinking...') {
|
|
106
|
+
let currentStatus = statusText;
|
|
102
107
|
// Set initial status
|
|
103
|
-
setThreadStatus(msg,
|
|
108
|
+
setThreadStatus(msg, currentStatus);
|
|
104
109
|
// Keepalive every 3 seconds (Slack clears after ~2 min timeout)
|
|
105
110
|
let failures = 0;
|
|
106
111
|
const interval = setInterval(async () => {
|
|
107
112
|
try {
|
|
108
|
-
await setThreadStatus(msg,
|
|
113
|
+
await setThreadStatus(msg, currentStatus);
|
|
109
114
|
failures = 0;
|
|
110
115
|
}
|
|
111
116
|
catch {
|
|
@@ -115,10 +120,17 @@ export function startTypingIndicator(msg, statusText = 'is thinking...') {
|
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
}, 3000);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
return {
|
|
124
|
+
updateStatus: (newStatus) => {
|
|
125
|
+
if (newStatus !== currentStatus) {
|
|
126
|
+
currentStatus = newStatus;
|
|
127
|
+
setThreadStatus(msg, currentStatus);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
stop: () => {
|
|
131
|
+
clearInterval(interval);
|
|
132
|
+
setThreadStatus(msg, '');
|
|
133
|
+
},
|
|
122
134
|
};
|
|
123
135
|
}
|
|
124
136
|
export async function removeReaction(msg, emoji) {
|