@teamvibe/poller 0.1.56 → 0.1.58
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/auth-provider.d.ts +1 -0
- package/dist/auth-provider.js +9 -0
- package/dist/brain-manager.js +5 -0
- package/dist/claude-spawner.js +26 -0
- package/dist/event-queue.d.ts +14 -0
- package/dist/event-queue.js +113 -0
- package/dist/heartbeat-manager.js +5 -0
- package/dist/maintenance-reporter.js +6 -0
- package/dist/poller.js +5 -0
- package/dist/session-store.js +5 -0
- package/dist/slack-client.js +4 -0
- package/dist/sqs-poller.js +4 -0
- package/dist/types.d.ts +6 -1
- package/package.json +1 -1
package/dist/auth-provider.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ interface AuthConfig {
|
|
|
12
12
|
export declare function initAuth(apiUrl: string, token: string): Promise<void>;
|
|
13
13
|
export declare function getCredentials(): AuthCredentials;
|
|
14
14
|
export declare function getAuthConfig(): AuthConfig;
|
|
15
|
+
export declare function getPollerId(): string | null;
|
|
15
16
|
export declare function isTokenMode(): boolean;
|
|
16
17
|
export declare function stopRefresh(): void;
|
|
17
18
|
export {};
|
package/dist/auth-provider.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { POLLER_VERSION } from './config.js';
|
|
2
2
|
import { logger } from './logger.js';
|
|
3
|
+
import { emitError } from './event-queue.js';
|
|
3
4
|
let currentCredentials = null;
|
|
4
5
|
let currentConfig = null;
|
|
6
|
+
let currentPollerId = null;
|
|
5
7
|
let refreshTimer = null;
|
|
6
8
|
let heartbeatTimer = null;
|
|
7
9
|
const REFRESH_INTERVAL_MS = 50 * 60 * 1000; // 50 minutes (before 1hr expiry)
|
|
@@ -18,10 +20,12 @@ async function sendHeartbeat(apiUrl, token, version) {
|
|
|
18
20
|
});
|
|
19
21
|
if (!response.ok) {
|
|
20
22
|
logger.warn(`Heartbeat failed (${response.status})`);
|
|
23
|
+
emitError('auth', `Heartbeat failed (${response.status})`, { severity: 'warn' });
|
|
21
24
|
}
|
|
22
25
|
}
|
|
23
26
|
catch (error) {
|
|
24
27
|
logger.warn(`Heartbeat error: ${error instanceof Error ? error.message : error}`);
|
|
28
|
+
emitError('auth', `Heartbeat error: ${error instanceof Error ? error.message : String(error)}`, { severity: 'warn' });
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
31
|
async function fetchCredentials(apiUrl, token) {
|
|
@@ -43,6 +47,7 @@ export async function initAuth(apiUrl, token) {
|
|
|
43
47
|
const auth = await fetchCredentials(apiUrl, token);
|
|
44
48
|
currentCredentials = auth.credentials;
|
|
45
49
|
currentConfig = auth.config;
|
|
50
|
+
currentPollerId = auth.pollerId;
|
|
46
51
|
logger.info(`Authenticated successfully. Region: ${auth.config.region}`);
|
|
47
52
|
logger.info(` SQS Queue: ${auth.config.sqsQueueUrl}`);
|
|
48
53
|
logger.info(` Sessions Table: ${auth.config.sessionsTable}`);
|
|
@@ -60,6 +65,7 @@ export async function initAuth(apiUrl, token) {
|
|
|
60
65
|
}
|
|
61
66
|
catch (error) {
|
|
62
67
|
logger.error(`Credential refresh failed: ${error instanceof Error ? error.message : error}`);
|
|
68
|
+
emitError('auth', `Credential refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
63
69
|
}
|
|
64
70
|
}, REFRESH_INTERVAL_MS);
|
|
65
71
|
}
|
|
@@ -75,6 +81,9 @@ export function getAuthConfig() {
|
|
|
75
81
|
}
|
|
76
82
|
return currentConfig;
|
|
77
83
|
}
|
|
84
|
+
export function getPollerId() {
|
|
85
|
+
return currentPollerId;
|
|
86
|
+
}
|
|
78
87
|
export function isTokenMode() {
|
|
79
88
|
return currentCredentials !== null;
|
|
80
89
|
}
|
package/dist/brain-manager.js
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync } from 'fs';
|
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { config } from './config.js';
|
|
6
6
|
import { logger } from './logger.js';
|
|
7
|
+
import { emitError } from './event-queue.js';
|
|
7
8
|
const execAsync = promisify(exec);
|
|
8
9
|
// Clean env for git commands:
|
|
9
10
|
// - npm/npx sets GIT_ASKPASS which prevents credential helpers from working
|
|
@@ -42,10 +43,12 @@ async function reportBrainSync(brainId, workspaceId, event) {
|
|
|
42
43
|
});
|
|
43
44
|
if (!response.ok) {
|
|
44
45
|
logger.warn(`Brain sync report failed (${response.status})`);
|
|
46
|
+
emitError('brain', `Brain sync report failed (${response.status})`, { severity: 'warn', context: { brainId, event } });
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
catch (error) {
|
|
48
50
|
logger.warn(`Brain sync report error: ${error instanceof Error ? error.message : error}`);
|
|
51
|
+
emitError('brain', `Brain sync report error: ${error instanceof Error ? error.message : String(error)}`, { severity: 'warn', context: { brainId } });
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
export async function getBrainPath(brain, channelId, workspaceId) {
|
|
@@ -107,6 +110,7 @@ async function updateBrain(brainDir, brainId, branch) {
|
|
|
107
110
|
catch (error) {
|
|
108
111
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
109
112
|
logger.error(`Failed to update brain ${brainId}: ${errorMessage}`);
|
|
113
|
+
emitError('brain', `Failed to update brain ${brainId}: ${errorMessage}`, { context: { brainId, brainDir } });
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
/**
|
|
@@ -170,6 +174,7 @@ export async function pushBrainChanges(brainDir, brainId, workspaceId) {
|
|
|
170
174
|
catch (error) {
|
|
171
175
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
172
176
|
logger.warn(`Failed to push brain ${brainId} changes: ${errorMessage}`);
|
|
177
|
+
emitError('brain', `Failed to push brain ${brainId} changes: ${errorMessage}`, { severity: 'warn', context: { brainId } });
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
/**
|
package/dist/claude-spawner.js
CHANGED
|
@@ -5,6 +5,8 @@ import { WebClient } from '@slack/web-api';
|
|
|
5
5
|
import { config } from './config.js';
|
|
6
6
|
import { logger } from './logger.js';
|
|
7
7
|
import { getBaseBrainPath } from './brain-manager.js';
|
|
8
|
+
import { getPollerId } from './auth-provider.js';
|
|
9
|
+
import { emitError } from './event-queue.js';
|
|
8
10
|
/**
|
|
9
11
|
* Maps tool names (or prefixes) to human-readable status messages.
|
|
10
12
|
* More specific patterns are checked first.
|
|
@@ -79,6 +81,7 @@ async function countNewThreadMessages(botToken, channel, threadTs, sinceTs) {
|
|
|
79
81
|
}
|
|
80
82
|
catch (error) {
|
|
81
83
|
logger.warn(`Failed to count new thread messages: ${error}`);
|
|
84
|
+
emitError('slack', `Failed to count new thread messages: ${error instanceof Error ? error.message : String(error)}`, { severity: 'warn' });
|
|
82
85
|
return 0;
|
|
83
86
|
}
|
|
84
87
|
}
|
|
@@ -96,6 +99,7 @@ function readBrainSettings(cwd) {
|
|
|
96
99
|
}
|
|
97
100
|
catch (error) {
|
|
98
101
|
logger.warn(`Failed to read brain settings at ${settingsPath}: ${error}`);
|
|
102
|
+
emitError('brain', `Failed to read brain settings at ${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, { severity: 'warn' });
|
|
99
103
|
return {};
|
|
100
104
|
}
|
|
101
105
|
}
|
|
@@ -136,6 +140,17 @@ function buildPrompt(msg, resumeHint) {
|
|
|
136
140
|
if (msg.type === 'button_click' && msg.button) {
|
|
137
141
|
prompt += `\n\n**Button Click:** User clicked button \`${msg.button.action_id}\` with value \`${msg.button.value}\``;
|
|
138
142
|
}
|
|
143
|
+
if (msg.type === 'modal_submission' && msg.modal) {
|
|
144
|
+
prompt += `\n\n**Modal Submitted:** User filled out "${msg.modal.callbackId}"\n`;
|
|
145
|
+
if (msg.modal.values) {
|
|
146
|
+
for (const [key, value] of Object.entries(msg.modal.values)) {
|
|
147
|
+
prompt += `- ${key}: ${value}\n`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (msg.type === 'view_closed' && msg.modal) {
|
|
152
|
+
prompt += `\n\n**Modal Dismissed:** User closed "${msg.modal.callbackId}" without submitting.`;
|
|
153
|
+
}
|
|
139
154
|
if (msg.teamvibe.emailChannelPrompt) {
|
|
140
155
|
prompt += '\n\n**Channel Instructions:**\n' + msg.teamvibe.emailChannelPrompt + '\n';
|
|
141
156
|
}
|
|
@@ -295,6 +310,8 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
295
310
|
TEAMVIBE_POLLER_TOKEN: config.TEAMVIBE_POLLER_TOKEN || '',
|
|
296
311
|
TEAMVIBE_WORKSPACE_ID: msg.teamvibe.workspaceId,
|
|
297
312
|
TEAMVIBE_CHANNEL_ID: msg.teamvibe.channelId,
|
|
313
|
+
TEAMVIBE_BOT_ID: msg.teamvibe.botId,
|
|
314
|
+
TEAMVIBE_POLLER_ID: getPollerId() || '',
|
|
298
315
|
},
|
|
299
316
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
300
317
|
});
|
|
@@ -359,6 +376,10 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
359
376
|
const timeout = setTimeout(() => {
|
|
360
377
|
timedOut = true;
|
|
361
378
|
logger.error(`Claude Code process timed out after ${timeoutMinutes} minutes`);
|
|
379
|
+
emitError('session', `Claude Code process timed out after ${timeoutMinutes} minutes`, {
|
|
380
|
+
context: { channelId: msg.teamvibe.channelId, threadId: msg.thread_id },
|
|
381
|
+
channelId: msg.teamvibe.channelId,
|
|
382
|
+
});
|
|
362
383
|
proc.kill('SIGTERM');
|
|
363
384
|
setTimeout(() => proc.kill('SIGKILL'), 5000);
|
|
364
385
|
}, config.CLAUDE_TIMEOUT_MS);
|
|
@@ -381,6 +402,10 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
381
402
|
proc.on('error', (error) => {
|
|
382
403
|
clearTimeout(timeout);
|
|
383
404
|
activeProcesses.delete(msg.id);
|
|
405
|
+
emitError('session', `Claude Code process spawn error: ${error.message}`, {
|
|
406
|
+
channelId: msg.teamvibe.channelId,
|
|
407
|
+
context: { threadId: msg.thread_id },
|
|
408
|
+
});
|
|
384
409
|
resolve({
|
|
385
410
|
success: false,
|
|
386
411
|
output: stdout,
|
|
@@ -426,6 +451,7 @@ export function killAllActiveProcesses() {
|
|
|
426
451
|
}
|
|
427
452
|
const forceKillTimer = setTimeout(() => {
|
|
428
453
|
logger.warn(`Process ${id} did not exit after SIGTERM, sending SIGKILL`);
|
|
454
|
+
emitError('session', `Process ${id} did not exit after SIGTERM, sending SIGKILL`, { severity: 'warn' });
|
|
429
455
|
proc.kill('SIGKILL');
|
|
430
456
|
}, 5000);
|
|
431
457
|
proc.once('close', () => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type ErrorCategory = 'sqs' | 'brain' | 'auth' | 'session' | 'slack' | 'maintenance';
|
|
2
|
+
export type ErrorSeverity = 'warn' | 'error';
|
|
3
|
+
export declare function flush(): Promise<void>;
|
|
4
|
+
export declare function startEventQueue(): void;
|
|
5
|
+
export declare function stopEventQueue(): void;
|
|
6
|
+
export declare function emitError(category: ErrorCategory, message: string, options?: {
|
|
7
|
+
severity?: ErrorSeverity;
|
|
8
|
+
channelId?: string;
|
|
9
|
+
context?: Record<string, unknown>;
|
|
10
|
+
}): void;
|
|
11
|
+
/** For testing: get current queue length */
|
|
12
|
+
export declare function getQueueLength(): number;
|
|
13
|
+
/** For testing: clear the queue */
|
|
14
|
+
export declare function clearQueue(): void;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// packages/poller/src/event-queue.ts
|
|
2
|
+
import { config } from './config.js';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
const MAX_QUEUE_SIZE = 200;
|
|
5
|
+
const FLUSH_INTERVAL_MS = 30_000;
|
|
6
|
+
const DEDUP_WINDOW_MS = 60_000;
|
|
7
|
+
let queue = [];
|
|
8
|
+
let flushTimer = null;
|
|
9
|
+
let isFlushing = false;
|
|
10
|
+
function dedupKey(channelId, category, message) {
|
|
11
|
+
return `${channelId}:${category}:${message}`;
|
|
12
|
+
}
|
|
13
|
+
function enqueue(event) {
|
|
14
|
+
// Dedup: collapse identical category+message within window
|
|
15
|
+
const key = dedupKey(event.channelId, event.metadata.category, event.metadata.message);
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
const windowStart = new Date(now - DEDUP_WINDOW_MS).toISOString();
|
|
18
|
+
const existing = queue.find((e) => dedupKey(e.channelId, e.metadata.category, e.metadata.message) === key &&
|
|
19
|
+
e.timestamp >= windowStart);
|
|
20
|
+
if (existing) {
|
|
21
|
+
existing.metadata.count += 1;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (queue.length >= MAX_QUEUE_SIZE) {
|
|
25
|
+
queue.shift(); // Drop oldest
|
|
26
|
+
}
|
|
27
|
+
queue.push(event);
|
|
28
|
+
}
|
|
29
|
+
async function flushOne(event) {
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch(`${config.TEAMVIBE_API_URL}/events`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
Authorization: `Bearer ${config.TEAMVIBE_POLLER_TOKEN}`,
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
eventType: event.eventType,
|
|
39
|
+
channelId: event.channelId,
|
|
40
|
+
metadata: event.metadata,
|
|
41
|
+
error: event.error,
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
return response.ok;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function flush() {
|
|
51
|
+
if (queue.length === 0 || isFlushing)
|
|
52
|
+
return;
|
|
53
|
+
isFlushing = true;
|
|
54
|
+
try {
|
|
55
|
+
const batch = [...queue];
|
|
56
|
+
const failed = [];
|
|
57
|
+
for (const event of batch) {
|
|
58
|
+
const ok = await flushOne(event);
|
|
59
|
+
if (!ok) {
|
|
60
|
+
failed.push(event);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
queue = failed;
|
|
64
|
+
if (failed.length > 0) {
|
|
65
|
+
logger.debug(`[EventQueue] ${failed.length} event(s) failed to flush, will retry`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
isFlushing = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function startEventQueue() {
|
|
73
|
+
if (!config.TEAMVIBE_API_URL || !config.TEAMVIBE_POLLER_TOKEN) {
|
|
74
|
+
return; // No-op in direct mode
|
|
75
|
+
}
|
|
76
|
+
flushTimer = setInterval(() => {
|
|
77
|
+
flush().catch(() => { });
|
|
78
|
+
}, FLUSH_INTERVAL_MS);
|
|
79
|
+
}
|
|
80
|
+
export function stopEventQueue() {
|
|
81
|
+
if (flushTimer) {
|
|
82
|
+
clearInterval(flushTimer);
|
|
83
|
+
flushTimer = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function emitError(category, message, options) {
|
|
87
|
+
if (!config.TEAMVIBE_API_URL || !config.TEAMVIBE_POLLER_TOKEN) {
|
|
88
|
+
return; // No-op in direct mode
|
|
89
|
+
}
|
|
90
|
+
const severity = options?.severity ?? 'error';
|
|
91
|
+
const channelId = options?.channelId ?? '_system';
|
|
92
|
+
enqueue({
|
|
93
|
+
eventType: 'poller.error',
|
|
94
|
+
channelId,
|
|
95
|
+
metadata: {
|
|
96
|
+
category,
|
|
97
|
+
severity,
|
|
98
|
+
message,
|
|
99
|
+
count: 1,
|
|
100
|
+
context: options?.context,
|
|
101
|
+
},
|
|
102
|
+
error: { code: category, message },
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** For testing: get current queue length */
|
|
107
|
+
export function getQueueLength() {
|
|
108
|
+
return queue.length;
|
|
109
|
+
}
|
|
110
|
+
/** For testing: clear the queue */
|
|
111
|
+
export function clearQueue() {
|
|
112
|
+
queue = [];
|
|
113
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { config } from './config.js';
|
|
2
2
|
import { logger } from './logger.js';
|
|
3
3
|
import { isAtCapacity } from './claude-spawner.js';
|
|
4
|
+
import { emitError } from './event-queue.js';
|
|
4
5
|
const trackedBrains = new Map();
|
|
5
6
|
let heartbeatInterval = null;
|
|
6
7
|
/**
|
|
@@ -85,6 +86,10 @@ export function startHeartbeatLoop(processMessageFn) {
|
|
|
85
86
|
}
|
|
86
87
|
catch (error) {
|
|
87
88
|
logger.error(`[Heartbeat] Failed maintenance for ${brainId}: ${error instanceof Error ? error.message : error}`);
|
|
89
|
+
emitError('session', `Heartbeat maintenance failed for ${brainId}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
90
|
+
channelId: info.channelId,
|
|
91
|
+
context: { brainId },
|
|
92
|
+
});
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
}, config.MAINTENANCE_CHECK_INTERVAL_MS);
|
|
@@ -4,6 +4,7 @@ import { execFile } from 'child_process';
|
|
|
4
4
|
import { promisify } from 'util';
|
|
5
5
|
import { config } from './config.js';
|
|
6
6
|
import { logger } from './logger.js';
|
|
7
|
+
import { emitError } from './event-queue.js';
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
8
9
|
const OPERATION_TO_EVENT_TYPE = {
|
|
9
10
|
consolidation: 'maintenance.consolidation',
|
|
@@ -32,11 +33,13 @@ export async function postMaintenanceReports(brainPath, channelId) {
|
|
|
32
33
|
const report = JSON.parse(content);
|
|
33
34
|
if (!report.operationType) {
|
|
34
35
|
logger.warn(`[MaintenanceReporter] Skipping ${file}: missing operationType`);
|
|
36
|
+
emitError('maintenance', `Skipping report ${file}: missing operationType`, { severity: 'warn' });
|
|
35
37
|
continue;
|
|
36
38
|
}
|
|
37
39
|
const eventType = OPERATION_TO_EVENT_TYPE[report.operationType];
|
|
38
40
|
if (!eventType) {
|
|
39
41
|
logger.warn(`[MaintenanceReporter] Skipping ${file}: unknown operationType "${report.operationType}"`);
|
|
42
|
+
emitError('maintenance', `Skipping report ${file}: unknown operationType "${report.operationType}"`, { severity: 'warn' });
|
|
40
43
|
continue;
|
|
41
44
|
}
|
|
42
45
|
const payload = {
|
|
@@ -60,10 +63,12 @@ export async function postMaintenanceReports(brainPath, channelId) {
|
|
|
60
63
|
else {
|
|
61
64
|
const errorBody = await response.text().catch(() => 'unknown');
|
|
62
65
|
logger.warn(`[MaintenanceReporter] Failed to POST ${file}: ${response.status} ${errorBody}`);
|
|
66
|
+
emitError('maintenance', `Failed to POST report ${file}: ${response.status}`, { severity: 'warn', channelId });
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
catch (err) {
|
|
66
70
|
logger.warn(`[MaintenanceReporter] Error processing ${file}: ${err instanceof Error ? err.message : err}`);
|
|
71
|
+
emitError('maintenance', `Error processing report ${file}: ${err instanceof Error ? err.message : String(err)}`, { severity: 'warn', channelId });
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
// Remove posted JSON reports from git so they don't resurrect on next pull
|
|
@@ -80,5 +85,6 @@ export async function postMaintenanceReports(brainPath, channelId) {
|
|
|
80
85
|
}
|
|
81
86
|
catch (err) {
|
|
82
87
|
logger.warn(`[MaintenanceReporter] Error scanning reports: ${err instanceof Error ? err.message : err}`);
|
|
88
|
+
emitError('maintenance', `Error scanning reports: ${err instanceof Error ? err.message : String(err)}`, { severity: 'warn' });
|
|
83
89
|
}
|
|
84
90
|
}
|
package/dist/poller.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getBrainPath, ensureDirectories, ensureBaseBrain, pushBrainChanges } fr
|
|
|
8
8
|
import { postMaintenanceReports } from './maintenance-reporter.js';
|
|
9
9
|
import { initAuth, stopRefresh } from './auth-provider.js';
|
|
10
10
|
import { registerBrain, startHeartbeatLoop, stopHeartbeatLoop } from './heartbeat-manager.js';
|
|
11
|
+
import { startEventQueue, stopEventQueue, flush as flushEventQueue } from './event-queue.js';
|
|
11
12
|
// Track active message processing
|
|
12
13
|
const processingMessages = new Set();
|
|
13
14
|
// Per-thread completion signals
|
|
@@ -312,6 +313,7 @@ async function shutdown(signal) {
|
|
|
312
313
|
logger.info(`${signal} received, shutting down gracefully...`);
|
|
313
314
|
stopRefresh();
|
|
314
315
|
stopHeartbeatLoop();
|
|
316
|
+
stopEventQueue();
|
|
315
317
|
// Unblock all messages waiting on thread completion so they can exit
|
|
316
318
|
for (const [threadId, signals] of threadCompletionSignals.entries()) {
|
|
317
319
|
logger.info(`Unblocking ${signals.length} waiting message(s) on thread ${threadId}`);
|
|
@@ -332,6 +334,8 @@ async function shutdown(signal) {
|
|
|
332
334
|
logger.info(`Waiting for ${processingMessages.size} message(s) to complete...`);
|
|
333
335
|
await sleep(1000);
|
|
334
336
|
}
|
|
337
|
+
// Final flush after all processes stopped — captures errors emitted during shutdown
|
|
338
|
+
await flushEventQueue();
|
|
335
339
|
logger.info('Shutdown complete');
|
|
336
340
|
process.exit(0);
|
|
337
341
|
}
|
|
@@ -349,6 +353,7 @@ export async function startPoller() {
|
|
|
349
353
|
logger.info(` Queue: ${config.SQS_QUEUE_URL}`);
|
|
350
354
|
logger.info(` Sessions table: ${config.SESSIONS_TABLE}`);
|
|
351
355
|
}
|
|
356
|
+
startEventQueue();
|
|
352
357
|
await ensureDirectories();
|
|
353
358
|
await ensureBaseBrain();
|
|
354
359
|
startHeartbeatLoop(processMessage);
|
package/dist/session-store.js
CHANGED
|
@@ -4,6 +4,7 @@ import { randomUUID } from 'crypto';
|
|
|
4
4
|
import { config } from './config.js';
|
|
5
5
|
import { logger } from './logger.js';
|
|
6
6
|
import { isTokenMode, getCredentials, getAuthConfig } from './auth-provider.js';
|
|
7
|
+
import { emitError } from './event-queue.js';
|
|
7
8
|
function createDocClient() {
|
|
8
9
|
if (isTokenMode()) {
|
|
9
10
|
const creds = getCredentials();
|
|
@@ -94,6 +95,7 @@ export async function acquireSessionLock(threadId, workspacePath) {
|
|
|
94
95
|
const staleLockTimeout = config.STALE_LOCK_TIMEOUT_MS;
|
|
95
96
|
if (lockAge > staleLockTimeout) {
|
|
96
97
|
logger.warn(`Session ${threadId} has stale lock (age: ${Math.round(lockAge / 1000)}s), attempting recovery...`);
|
|
98
|
+
emitError('session', `Stale lock detected on session ${threadId} (age: ${Math.round(lockAge / 1000)}s)`, { severity: 'warn' });
|
|
97
99
|
const released = await forceReleaseStalelock(threadId, staleLockTimeout);
|
|
98
100
|
if (released) {
|
|
99
101
|
logger.info(`Stale lock released for ${threadId}, retrying acquisition`);
|
|
@@ -164,6 +166,7 @@ export async function updateSessionId(threadId, lockToken, newSessionId) {
|
|
|
164
166
|
catch (error) {
|
|
165
167
|
if (error.name === 'ConditionalCheckFailedException') {
|
|
166
168
|
logger.warn(`Lock token mismatch when updating session_id for ${threadId}`);
|
|
169
|
+
emitError('session', `Lock token mismatch when updating session_id for ${threadId}`, { severity: 'warn' });
|
|
167
170
|
return;
|
|
168
171
|
}
|
|
169
172
|
throw error;
|
|
@@ -201,6 +204,7 @@ export async function releaseSessionLock(threadId, lockToken, newStatus = 'idle'
|
|
|
201
204
|
catch (error) {
|
|
202
205
|
if (error.name === 'ConditionalCheckFailedException') {
|
|
203
206
|
logger.warn(`Lock token mismatch when releasing session ${threadId}`);
|
|
207
|
+
emitError('session', `Lock token mismatch when releasing session ${threadId}`, { severity: 'warn' });
|
|
204
208
|
return;
|
|
205
209
|
}
|
|
206
210
|
throw error;
|
|
@@ -232,6 +236,7 @@ export async function forceReleaseStalelock(threadId, maxAgeMs) {
|
|
|
232
236
|
},
|
|
233
237
|
}));
|
|
234
238
|
logger.warn(`Force released stale lock on session ${threadId} (age: ${lockAge}ms)`);
|
|
239
|
+
emitError('session', `Force released stale lock on session ${threadId}`, { severity: 'warn' });
|
|
235
240
|
return true;
|
|
236
241
|
}
|
|
237
242
|
catch (error) {
|
package/dist/slack-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { WebClient } from '@slack/web-api';
|
|
2
2
|
import { logger } from './logger.js';
|
|
3
|
+
import { emitError } from './event-queue.js';
|
|
3
4
|
// Cache user info to avoid repeated API calls
|
|
4
5
|
const userCache = new Map();
|
|
5
6
|
// Per-token Slack client cache
|
|
@@ -34,6 +35,7 @@ export async function getUserInfo(botToken, userId) {
|
|
|
34
35
|
}
|
|
35
36
|
else {
|
|
36
37
|
logger.warn(`Failed to fetch user info for ${userId}:`, error?.data?.error || error);
|
|
38
|
+
emitError('slack', `Failed to fetch user info for ${userId}`, { severity: 'warn' });
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
const fallback = { name: userId, realName: userId };
|
|
@@ -84,6 +86,7 @@ export async function addReaction(msg, emoji) {
|
|
|
84
86
|
}
|
|
85
87
|
catch (error) {
|
|
86
88
|
logger.warn(`Failed to add reaction ${emoji}:`, error);
|
|
89
|
+
emitError('slack', `Failed to add reaction ${emoji}`, { severity: 'warn' });
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
export async function setThreadStatus(msg, status) {
|
|
@@ -100,6 +103,7 @@ export async function setThreadStatus(msg, status) {
|
|
|
100
103
|
}
|
|
101
104
|
catch (error) {
|
|
102
105
|
logger.warn(`Failed to set thread status: ${error}`);
|
|
106
|
+
emitError('slack', `Failed to set thread status`, { severity: 'warn' });
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
export function startTypingIndicator(msg, statusText = 'is thinking...') {
|
package/dist/sqs-poller.js
CHANGED
|
@@ -2,6 +2,7 @@ import { SQSClient, ReceiveMessageCommand, DeleteMessageCommand, ChangeMessageVi
|
|
|
2
2
|
import { config } from './config.js';
|
|
3
3
|
import { logger } from './logger.js';
|
|
4
4
|
import { isTokenMode, getCredentials, getAuthConfig } from './auth-provider.js';
|
|
5
|
+
import { emitError } from './event-queue.js';
|
|
5
6
|
function createSqsClient() {
|
|
6
7
|
if (isTokenMode()) {
|
|
7
8
|
const creds = getCredentials();
|
|
@@ -42,12 +43,14 @@ export async function pollMessages(maxMessages = 10) {
|
|
|
42
43
|
}
|
|
43
44
|
catch (error) {
|
|
44
45
|
logger.error('Error polling SQS:', error);
|
|
46
|
+
emitError('sqs', `Error polling SQS: ${error instanceof Error ? error.message : String(error)}`);
|
|
45
47
|
throw error;
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
function parseMessage(msg) {
|
|
49
51
|
if (!msg.Body || !msg.ReceiptHandle || !msg.MessageId) {
|
|
50
52
|
logger.error('Invalid SQS message - missing required fields');
|
|
53
|
+
emitError('sqs', 'Invalid SQS message - missing required fields');
|
|
51
54
|
return null;
|
|
52
55
|
}
|
|
53
56
|
try {
|
|
@@ -60,6 +63,7 @@ function parseMessage(msg) {
|
|
|
60
63
|
}
|
|
61
64
|
catch (error) {
|
|
62
65
|
logger.error('Failed to parse message body:', error);
|
|
66
|
+
emitError('sqs', `Failed to parse message body: ${error instanceof Error ? error.message : String(error)}`);
|
|
63
67
|
return null;
|
|
64
68
|
}
|
|
65
69
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface TeamVibeQueueMessage {
|
|
|
26
26
|
inReplyTo?: string;
|
|
27
27
|
};
|
|
28
28
|
};
|
|
29
|
-
type: 'message' | 'approval_response' | 'button_click';
|
|
29
|
+
type: 'message' | 'approval_response' | 'button_click' | 'modal_submission' | 'view_closed';
|
|
30
30
|
approval?: {
|
|
31
31
|
action_id: string;
|
|
32
32
|
approved: boolean;
|
|
@@ -35,6 +35,11 @@ export interface TeamVibeQueueMessage {
|
|
|
35
35
|
action_id: string;
|
|
36
36
|
value: string;
|
|
37
37
|
};
|
|
38
|
+
modal?: {
|
|
39
|
+
callbackId: string;
|
|
40
|
+
modalDefId: string;
|
|
41
|
+
values?: Record<string, string>;
|
|
42
|
+
};
|
|
38
43
|
teamvibe: {
|
|
39
44
|
workspaceId: string;
|
|
40
45
|
botId: string;
|