@teamvibe/poller 0.1.46 → 0.1.48
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.js +26 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +2 -0
- package/dist/poller.js +88 -9
- package/package.json +1 -1
package/dist/claude-spawner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
4
|
import { WebClient } from '@slack/web-api';
|
|
4
5
|
import { config } from './config.js';
|
|
5
6
|
import { logger } from './logger.js';
|
|
@@ -81,6 +82,23 @@ async function countNewThreadMessages(botToken, channel, threadTs, sinceTs) {
|
|
|
81
82
|
return 0;
|
|
82
83
|
}
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Read brain settings.json from the brain working directory.
|
|
87
|
+
* Returns parsed settings or empty object if not found.
|
|
88
|
+
*/
|
|
89
|
+
function readBrainSettings(cwd) {
|
|
90
|
+
const settingsPath = join(cwd, 'settings.json');
|
|
91
|
+
if (!existsSync(settingsPath))
|
|
92
|
+
return {};
|
|
93
|
+
try {
|
|
94
|
+
const raw = readFileSync(settingsPath, 'utf-8');
|
|
95
|
+
return JSON.parse(raw);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
logger.warn(`Failed to read brain settings at ${settingsPath}: ${error}`);
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
84
102
|
function buildPrompt(msg, resumeHint) {
|
|
85
103
|
// Heartbeat messages get a minimal prompt — CLAUDE.md/MAINTENANCE.md handle the rest
|
|
86
104
|
if (msg.source === 'heartbeat') {
|
|
@@ -203,6 +221,8 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
203
221
|
'teamvibe-api': { command: 'node', args: [teamvibeApiServerPath] },
|
|
204
222
|
},
|
|
205
223
|
});
|
|
224
|
+
// Read per-brain settings (e.g. model override)
|
|
225
|
+
const brainSettings = readBrainSettings(cwd);
|
|
206
226
|
const args = [
|
|
207
227
|
'--print',
|
|
208
228
|
'--verbose',
|
|
@@ -215,6 +235,12 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
215
235
|
'--disallowedTools',
|
|
216
236
|
'NotebookEdit',
|
|
217
237
|
];
|
|
238
|
+
// Model override: brain settings.json takes priority, then CLAUDE_MODEL env var
|
|
239
|
+
const model = (typeof brainSettings['model'] === 'string' && brainSettings['model']) || config.CLAUDE_MODEL;
|
|
240
|
+
if (model) {
|
|
241
|
+
args.push('--model', model);
|
|
242
|
+
sessionLog.info(`Using model: ${model}`);
|
|
243
|
+
}
|
|
218
244
|
// Handle session continuity
|
|
219
245
|
if (sessionId) {
|
|
220
246
|
if (isFirstMessage) {
|
package/dist/config.d.ts
CHANGED
|
@@ -12,10 +12,12 @@ declare const configSchema: z.ZodObject<{
|
|
|
12
12
|
HEARTBEAT_INTERVAL_MS: z.ZodDefault<z.ZodNumber>;
|
|
13
13
|
CLAUDE_TIMEOUT_MS: z.ZodDefault<z.ZodNumber>;
|
|
14
14
|
STALE_LOCK_TIMEOUT_MS: z.ZodDefault<z.ZodNumber>;
|
|
15
|
+
LOCK_RETRY_INTERVAL_MS: z.ZodDefault<z.ZodNumber>;
|
|
15
16
|
TEAMVIBE_DATA_DIR: z.ZodDefault<z.ZodString>;
|
|
16
17
|
BRAINS_PATH: z.ZodDefault<z.ZodString>;
|
|
17
18
|
DEFAULT_BRAIN_PATH: z.ZodDefault<z.ZodString>;
|
|
18
19
|
CLAUDE_CLI_PATH: z.ZodDefault<z.ZodString>;
|
|
20
|
+
CLAUDE_MODEL: z.ZodDefault<z.ZodString>;
|
|
19
21
|
PERSISTENT_STORAGE_PATH: z.ZodDefault<z.ZodString>;
|
|
20
22
|
BASE_BRAIN_REPO: z.ZodDefault<z.ZodString>;
|
|
21
23
|
BASE_BRAIN_BRANCH: z.ZodDefault<z.ZodString>;
|
|
@@ -34,10 +36,12 @@ declare const configSchema: z.ZodObject<{
|
|
|
34
36
|
HEARTBEAT_INTERVAL_MS: number;
|
|
35
37
|
CLAUDE_TIMEOUT_MS: number;
|
|
36
38
|
STALE_LOCK_TIMEOUT_MS: number;
|
|
39
|
+
LOCK_RETRY_INTERVAL_MS: number;
|
|
37
40
|
TEAMVIBE_DATA_DIR: string;
|
|
38
41
|
BRAINS_PATH: string;
|
|
39
42
|
DEFAULT_BRAIN_PATH: string;
|
|
40
43
|
CLAUDE_CLI_PATH: string;
|
|
44
|
+
CLAUDE_MODEL: string;
|
|
41
45
|
PERSISTENT_STORAGE_PATH: string;
|
|
42
46
|
BASE_BRAIN_REPO: string;
|
|
43
47
|
BASE_BRAIN_BRANCH: string;
|
|
@@ -62,10 +66,12 @@ declare const configSchema: z.ZodObject<{
|
|
|
62
66
|
HEARTBEAT_INTERVAL_MS?: number | undefined;
|
|
63
67
|
CLAUDE_TIMEOUT_MS?: number | undefined;
|
|
64
68
|
STALE_LOCK_TIMEOUT_MS?: number | undefined;
|
|
69
|
+
LOCK_RETRY_INTERVAL_MS?: number | undefined;
|
|
65
70
|
TEAMVIBE_DATA_DIR?: string | undefined;
|
|
66
71
|
BRAINS_PATH?: string | undefined;
|
|
67
72
|
DEFAULT_BRAIN_PATH?: string | undefined;
|
|
68
73
|
CLAUDE_CLI_PATH?: string | undefined;
|
|
74
|
+
CLAUDE_MODEL?: string | undefined;
|
|
69
75
|
PERSISTENT_STORAGE_PATH?: string | undefined;
|
|
70
76
|
BASE_BRAIN_REPO?: string | undefined;
|
|
71
77
|
BASE_BRAIN_BRANCH?: string | undefined;
|
|
@@ -87,10 +93,12 @@ export declare const config: {
|
|
|
87
93
|
HEARTBEAT_INTERVAL_MS: number;
|
|
88
94
|
CLAUDE_TIMEOUT_MS: number;
|
|
89
95
|
STALE_LOCK_TIMEOUT_MS: number;
|
|
96
|
+
LOCK_RETRY_INTERVAL_MS: number;
|
|
90
97
|
TEAMVIBE_DATA_DIR: string;
|
|
91
98
|
BRAINS_PATH: string;
|
|
92
99
|
DEFAULT_BRAIN_PATH: string;
|
|
93
100
|
CLAUDE_CLI_PATH: string;
|
|
101
|
+
CLAUDE_MODEL: string;
|
|
94
102
|
PERSISTENT_STORAGE_PATH: string;
|
|
95
103
|
BASE_BRAIN_REPO: string;
|
|
96
104
|
BASE_BRAIN_BRANCH: string;
|
package/dist/config.js
CHANGED
|
@@ -21,11 +21,13 @@ const configSchema = z.object({
|
|
|
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
23
|
STALE_LOCK_TIMEOUT_MS: z.coerce.number().default(3900000), // 65 minutes
|
|
24
|
+
LOCK_RETRY_INTERVAL_MS: z.coerce.number().default(30000), // 30 seconds
|
|
24
25
|
// Paths
|
|
25
26
|
TEAMVIBE_DATA_DIR: z.string().default(DEFAULT_DATA_DIR),
|
|
26
27
|
BRAINS_PATH: z.string().default(''),
|
|
27
28
|
DEFAULT_BRAIN_PATH: z.string().default(''),
|
|
28
29
|
CLAUDE_CLI_PATH: z.string().default('claude'),
|
|
30
|
+
CLAUDE_MODEL: z.string().default('claude-opus-4-6'),
|
|
29
31
|
PERSISTENT_STORAGE_PATH: z.string().default(''),
|
|
30
32
|
// Base brain repo (user-scope config for Claude Code)
|
|
31
33
|
BASE_BRAIN_REPO: z.string().default('https://github.com/teamvibeai/poller-brain.git'),
|
package/dist/poller.js
CHANGED
|
@@ -28,7 +28,9 @@ function logQueueState(context) {
|
|
|
28
28
|
logger.info(`[Queue] ${context} | Processing: ${stats.processing}, Threads with waiting: ${stats.threadsWithWaiting}, Total waiting: ${stats.totalWaiting}`);
|
|
29
29
|
}
|
|
30
30
|
function waitForThreadCompletion(threadId) {
|
|
31
|
-
|
|
31
|
+
let resolveFn;
|
|
32
|
+
const promise = new Promise((resolve) => {
|
|
33
|
+
resolveFn = resolve;
|
|
32
34
|
const signals = threadCompletionSignals.get(threadId) || [];
|
|
33
35
|
signals.push(resolve);
|
|
34
36
|
threadCompletionSignals.set(threadId, signals);
|
|
@@ -37,6 +39,25 @@ function waitForThreadCompletion(threadId) {
|
|
|
37
39
|
logger.info(`[Queue] Thread ${threadId}: ${currentCount + 1} message(s) now waiting`);
|
|
38
40
|
logQueueState('After enqueue');
|
|
39
41
|
});
|
|
42
|
+
const cancel = () => {
|
|
43
|
+
// Remove this specific resolve from the signals array
|
|
44
|
+
const signals = threadCompletionSignals.get(threadId);
|
|
45
|
+
if (signals) {
|
|
46
|
+
const idx = signals.indexOf(resolveFn);
|
|
47
|
+
if (idx !== -1)
|
|
48
|
+
signals.splice(idx, 1);
|
|
49
|
+
if (signals.length === 0)
|
|
50
|
+
threadCompletionSignals.delete(threadId);
|
|
51
|
+
}
|
|
52
|
+
const count = waitingCountByThread.get(threadId) || 0;
|
|
53
|
+
if (count <= 1) {
|
|
54
|
+
waitingCountByThread.delete(threadId);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
waitingCountByThread.set(threadId, count - 1);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return { promise, cancel };
|
|
40
61
|
}
|
|
41
62
|
function signalThreadCompletion(threadId) {
|
|
42
63
|
const signals = threadCompletionSignals.get(threadId);
|
|
@@ -110,13 +131,27 @@ async function processMessage(received) {
|
|
|
110
131
|
sessionLog.info(`Session ${threadId} is processing, waiting for completion...`);
|
|
111
132
|
const waitHeartbeat = startHeartbeat(receiptHandle, sessionLog);
|
|
112
133
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
134
|
+
const maxWaitMs = config.STALE_LOCK_TIMEOUT_MS;
|
|
135
|
+
const retryIntervalMs = config.LOCK_RETRY_INTERVAL_MS;
|
|
136
|
+
const waitStart = Date.now();
|
|
137
|
+
while (Date.now() - waitStart < maxWaitMs) {
|
|
138
|
+
const waiter = waitForThreadCompletion(threadId);
|
|
139
|
+
// Wait for either: completion signal OR retry interval timeout
|
|
140
|
+
await Promise.race([
|
|
141
|
+
waiter.promise,
|
|
142
|
+
sleep(retryIntervalMs),
|
|
143
|
+
]);
|
|
144
|
+
// Clean up the waiter (handles the timeout case where signal didn't fire)
|
|
145
|
+
waiter.cancel();
|
|
146
|
+
lockResult = await acquireSessionLock(threadId, kbPath);
|
|
147
|
+
if (lockResult.success)
|
|
148
|
+
break;
|
|
149
|
+
sessionLog.info(`Lock still held for ${threadId}, retrying in ${retryIntervalMs / 1000}s...`);
|
|
150
|
+
}
|
|
116
151
|
if (!lockResult.success) {
|
|
117
|
-
sessionLog.
|
|
118
|
-
|
|
119
|
-
return
|
|
152
|
+
sessionLog.error(`Gave up waiting for lock on ${threadId} after ${maxWaitMs / 1000}s`);
|
|
153
|
+
await deleteMessage(receiptHandle);
|
|
154
|
+
return;
|
|
120
155
|
}
|
|
121
156
|
}
|
|
122
157
|
finally {
|
|
@@ -205,6 +240,25 @@ async function processMessage(received) {
|
|
|
205
240
|
signalThreadCompletion(threadId);
|
|
206
241
|
}
|
|
207
242
|
}
|
|
243
|
+
function deduplicateMessages(messages) {
|
|
244
|
+
const byThread = new Map();
|
|
245
|
+
for (const msg of messages) {
|
|
246
|
+
const threadId = msg.queueMessage.thread_id;
|
|
247
|
+
const existing = byThread.get(threadId) || [];
|
|
248
|
+
existing.push(msg);
|
|
249
|
+
byThread.set(threadId, existing);
|
|
250
|
+
}
|
|
251
|
+
const unique = [];
|
|
252
|
+
const duplicates = [];
|
|
253
|
+
for (const [, msgs] of byThread) {
|
|
254
|
+
// Keep the last message (most recent), mark rest as duplicates
|
|
255
|
+
unique.push(msgs[msgs.length - 1]);
|
|
256
|
+
for (let i = 0; i < msgs.length - 1; i++) {
|
|
257
|
+
duplicates.push(msgs[i]);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { unique, duplicates };
|
|
261
|
+
}
|
|
208
262
|
async function pollLoop() {
|
|
209
263
|
logger.info('Poll loop started');
|
|
210
264
|
while (!shuttingDown) {
|
|
@@ -222,7 +276,16 @@ async function pollLoop() {
|
|
|
222
276
|
}
|
|
223
277
|
logger.info(`Received ${messages.length} message(s) from SQS`);
|
|
224
278
|
logQueueState('After SQS poll');
|
|
225
|
-
|
|
279
|
+
// Deduplicate: if multiple messages for the same thread, keep only the latest
|
|
280
|
+
const { unique, duplicates } = deduplicateMessages(messages);
|
|
281
|
+
for (const dup of duplicates) {
|
|
282
|
+
logger.info(`Deleting duplicate message for thread ${dup.queueMessage.thread_id} (messageId: ${dup.messageId})`);
|
|
283
|
+
await deleteMessage(dup.receiptHandle);
|
|
284
|
+
}
|
|
285
|
+
if (duplicates.length > 0) {
|
|
286
|
+
logger.info(`Deduplicated: ${messages.length} -> ${unique.length} message(s)`);
|
|
287
|
+
}
|
|
288
|
+
const processPromises = unique.map((msg) => processMessage(msg).catch((error) => {
|
|
226
289
|
logger.error(`Failed to process message ${msg.messageId}:`, error);
|
|
227
290
|
}));
|
|
228
291
|
// Don't await - let them run in parallel
|
|
@@ -286,5 +349,21 @@ export async function startPoller() {
|
|
|
286
349
|
await ensureDirectories();
|
|
287
350
|
await ensureBaseBrain();
|
|
288
351
|
startHeartbeatLoop(processMessage);
|
|
289
|
-
|
|
352
|
+
// Periodic queue health check — surfaces stuck messages in logs
|
|
353
|
+
const healthCheckInterval = setInterval(() => {
|
|
354
|
+
const stats = getQueueStats();
|
|
355
|
+
if (stats.totalWaiting > 0) {
|
|
356
|
+
logger.warn(`[Queue Health] ${stats.totalWaiting} message(s) waiting across ${stats.threadsWithWaiting} thread(s), ` +
|
|
357
|
+
`${stats.processing} actively processing`);
|
|
358
|
+
for (const [threadId, count] of waitingCountByThread.entries()) {
|
|
359
|
+
logger.warn(`[Queue Health] Thread ${threadId}: ${count} waiting`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}, 60000);
|
|
363
|
+
try {
|
|
364
|
+
await pollLoop();
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
clearInterval(healthCheckInterval);
|
|
368
|
+
}
|
|
290
369
|
}
|