@teamvibe/poller 0.2.0 → 0.2.1
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 +8 -8
- package/dist/config.js +3 -3
- package/dist/heartbeat-manager.d.ts +5 -3
- package/dist/heartbeat-manager.js +79 -68
- package/dist/poller.js +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -27,8 +27,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
27
27
|
BASE_BRAIN_PATH: z.ZodDefault<z.ZodString>;
|
|
28
28
|
BRAIN_AUTO_UPDATE: z.ZodEffects<z.ZodDefault<z.ZodString>, boolean, string | undefined>;
|
|
29
29
|
BRAIN_UPDATE_COOLDOWN_MS: z.ZodDefault<z.ZodNumber>;
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
HEARTBEAT_CRON: z.ZodDefault<z.ZodString>;
|
|
31
|
+
MAINTENANCE_CRON: z.ZodDefault<z.ZodString>;
|
|
32
32
|
MAINTENANCE_ENABLED: z.ZodEffects<z.ZodDefault<z.ZodString>, boolean, string | undefined>;
|
|
33
33
|
AUTO_UPDATE_POLICY: z.ZodDefault<z.ZodEnum<["patch", "minor", "off"]>>;
|
|
34
34
|
AUTO_UPDATE_SCHEDULE: z.ZodDefault<z.ZodString>;
|
|
@@ -56,8 +56,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
56
56
|
BASE_BRAIN_PATH: string;
|
|
57
57
|
BRAIN_AUTO_UPDATE: boolean;
|
|
58
58
|
BRAIN_UPDATE_COOLDOWN_MS: number;
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
HEARTBEAT_CRON: string;
|
|
60
|
+
MAINTENANCE_CRON: string;
|
|
61
61
|
MAINTENANCE_ENABLED: boolean;
|
|
62
62
|
AUTO_UPDATE_POLICY: "patch" | "minor" | "off";
|
|
63
63
|
AUTO_UPDATE_SCHEDULE: string;
|
|
@@ -91,8 +91,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
91
91
|
BASE_BRAIN_PATH?: string | undefined;
|
|
92
92
|
BRAIN_AUTO_UPDATE?: string | undefined;
|
|
93
93
|
BRAIN_UPDATE_COOLDOWN_MS?: number | undefined;
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
HEARTBEAT_CRON?: string | undefined;
|
|
95
|
+
MAINTENANCE_CRON?: string | undefined;
|
|
96
96
|
MAINTENANCE_ENABLED?: string | undefined;
|
|
97
97
|
AUTO_UPDATE_POLICY?: "patch" | "minor" | "off" | undefined;
|
|
98
98
|
AUTO_UPDATE_SCHEDULE?: string | undefined;
|
|
@@ -123,8 +123,8 @@ export declare const config: {
|
|
|
123
123
|
BASE_BRAIN_PATH: string;
|
|
124
124
|
BRAIN_AUTO_UPDATE: boolean;
|
|
125
125
|
BRAIN_UPDATE_COOLDOWN_MS: number;
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
HEARTBEAT_CRON: string;
|
|
127
|
+
MAINTENANCE_CRON: string;
|
|
128
128
|
MAINTENANCE_ENABLED: boolean;
|
|
129
129
|
AUTO_UPDATE_POLICY: "patch" | "minor" | "off";
|
|
130
130
|
AUTO_UPDATE_SCHEDULE: string;
|
package/dist/config.js
CHANGED
|
@@ -42,9 +42,9 @@ const configSchema = z.object({
|
|
|
42
42
|
.default('true')
|
|
43
43
|
.transform((v) => v.toLowerCase() === 'true'),
|
|
44
44
|
BRAIN_UPDATE_COOLDOWN_MS: z.coerce.number().default(300000), // 5 minutes
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
// Brain heartbeat & maintenance (cron-based)
|
|
46
|
+
HEARTBEAT_CRON: z.string().default('*/5 * * * *'), // Every 5 minutes
|
|
47
|
+
MAINTENANCE_CRON: z.string().default('30 3 * * *'), // Daily at 03:30 UTC
|
|
48
48
|
MAINTENANCE_ENABLED: z.string().default('true').transform((v) => v.toLowerCase() === 'true'),
|
|
49
49
|
// Auto-update settings
|
|
50
50
|
AUTO_UPDATE_POLICY: z.enum(['patch', 'minor', 'off']).default('patch'),
|
|
@@ -5,8 +5,10 @@ import type { TeamVibeQueueMessage } from './types.js';
|
|
|
5
5
|
*/
|
|
6
6
|
export declare function registerBrain(msg: TeamVibeQueueMessage): void;
|
|
7
7
|
/**
|
|
8
|
-
* Start the
|
|
9
|
-
*
|
|
8
|
+
* Start the cron-based heartbeat and maintenance schedules.
|
|
9
|
+
*
|
|
10
|
+
* - Heartbeat: lightweight, frequent (default: every 5 min)
|
|
11
|
+
* - Maintenance: heavy, daily (default: 03:30 UTC)
|
|
10
12
|
*/
|
|
11
13
|
export declare function startHeartbeatLoop(processMessageFn: (msg: {
|
|
12
14
|
queueMessage: TeamVibeQueueMessage;
|
|
@@ -14,6 +16,6 @@ export declare function startHeartbeatLoop(processMessageFn: (msg: {
|
|
|
14
16
|
messageId: string;
|
|
15
17
|
}) => Promise<void>): void;
|
|
16
18
|
/**
|
|
17
|
-
* Stop the heartbeat
|
|
19
|
+
* Stop the heartbeat and maintenance crons (for shutdown).
|
|
18
20
|
*/
|
|
19
21
|
export declare function stopHeartbeatLoop(): void;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { Cron } from 'croner';
|
|
1
2
|
import { config } from './config.js';
|
|
2
3
|
import { logger } from './logger.js';
|
|
3
4
|
import { isAtCapacity } from './claude-spawner.js';
|
|
4
5
|
import { emitError } from './event-queue.js';
|
|
5
6
|
const trackedBrains = new Map();
|
|
6
|
-
let
|
|
7
|
+
let heartbeatJob = null;
|
|
8
|
+
let maintenanceJob = null;
|
|
7
9
|
/**
|
|
8
10
|
* Register a brain from a real message so we know about it for heartbeats.
|
|
9
11
|
* Called from processMessage() after brain path is resolved.
|
|
@@ -12,11 +14,9 @@ export function registerBrain(msg) {
|
|
|
12
14
|
const brain = msg.teamvibe.brain;
|
|
13
15
|
if (!brain?.brainId)
|
|
14
16
|
return;
|
|
15
|
-
// Don't register from heartbeat messages (avoid self-referential loop)
|
|
16
|
-
if (msg.source === 'heartbeat')
|
|
17
|
+
// Don't register from heartbeat/maintenance messages (avoid self-referential loop)
|
|
18
|
+
if (msg.source === 'heartbeat' || msg.source === 'maintenance')
|
|
17
19
|
return;
|
|
18
|
-
const existing = trackedBrains.get(brain.brainId);
|
|
19
|
-
// Update brain info but preserve lastHeartbeat if it exists
|
|
20
20
|
trackedBrains.set(brain.brainId, {
|
|
21
21
|
brainId: brain.brainId,
|
|
22
22
|
gitRepoUrl: brain.gitRepoUrl,
|
|
@@ -25,82 +25,93 @@ export function registerBrain(msg) {
|
|
|
25
25
|
botId: msg.teamvibe.botId,
|
|
26
26
|
botToken: msg.teamvibe.botToken,
|
|
27
27
|
channelId: msg.teamvibe.channelId,
|
|
28
|
-
lastHeartbeat: existing?.lastHeartbeat ?? 0,
|
|
29
28
|
});
|
|
30
29
|
logger.debug(`[Heartbeat] Registered brain: ${brain.brainId}`);
|
|
31
30
|
}
|
|
32
31
|
/**
|
|
33
|
-
*
|
|
34
|
-
|
|
32
|
+
* Send a synthetic message to all tracked brains.
|
|
33
|
+
*/
|
|
34
|
+
async function triggerAllBrains(source, processMessageFn) {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
for (const [brainId, info] of trackedBrains.entries()) {
|
|
37
|
+
if (isAtCapacity()) {
|
|
38
|
+
logger.debug(`[${source}] At capacity, skipping ${brainId}`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
logger.info(`[${source}] Triggering ${source} for brain ${brainId}`);
|
|
42
|
+
const syntheticMessage = {
|
|
43
|
+
id: `${source}_${brainId}_${now}`,
|
|
44
|
+
source,
|
|
45
|
+
thread_id: `${source}_${brainId}_${now}`,
|
|
46
|
+
sender: {
|
|
47
|
+
id: 'system',
|
|
48
|
+
name: 'System',
|
|
49
|
+
},
|
|
50
|
+
text: source,
|
|
51
|
+
attachments: [],
|
|
52
|
+
response_context: {},
|
|
53
|
+
type: 'message',
|
|
54
|
+
teamvibe: {
|
|
55
|
+
workspaceId: info.workspaceId,
|
|
56
|
+
botId: info.botId,
|
|
57
|
+
botToken: info.botToken,
|
|
58
|
+
channelId: info.channelId,
|
|
59
|
+
brain: {
|
|
60
|
+
brainId: info.brainId,
|
|
61
|
+
gitRepoUrl: info.gitRepoUrl,
|
|
62
|
+
branch: info.branch,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
await processMessageFn({
|
|
68
|
+
queueMessage: syntheticMessage,
|
|
69
|
+
receiptHandle: '', // No SQS receipt for synthetic messages
|
|
70
|
+
messageId: syntheticMessage.id,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger.error(`[${source}] Failed for ${brainId}: ${error instanceof Error ? error.message : error}`);
|
|
75
|
+
emitError('session', `${source} failed for ${brainId}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
76
|
+
channelId: info.channelId,
|
|
77
|
+
context: { brainId },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Start the cron-based heartbeat and maintenance schedules.
|
|
84
|
+
*
|
|
85
|
+
* - Heartbeat: lightweight, frequent (default: every 5 min)
|
|
86
|
+
* - Maintenance: heavy, daily (default: 03:30 UTC)
|
|
35
87
|
*/
|
|
36
88
|
export function startHeartbeatLoop(processMessageFn) {
|
|
37
89
|
if (!config.MAINTENANCE_ENABLED) {
|
|
38
90
|
logger.info('[Heartbeat] Maintenance disabled via config');
|
|
39
91
|
return;
|
|
40
92
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
logger.info(`[Heartbeat] Triggering maintenance for brain ${brainId} (last: ${info.lastHeartbeat === 0 ? 'never' : `${Math.round(elapsed / 60000)}m ago`})`);
|
|
54
|
-
// Update lastHeartbeat immediately to prevent double-triggering
|
|
55
|
-
info.lastHeartbeat = now;
|
|
56
|
-
const syntheticMessage = {
|
|
57
|
-
id: `heartbeat_${brainId}_${now}`,
|
|
58
|
-
source: 'heartbeat',
|
|
59
|
-
thread_id: `heartbeat_${brainId}_${now}`,
|
|
60
|
-
sender: {
|
|
61
|
-
id: 'system',
|
|
62
|
-
name: 'System',
|
|
63
|
-
},
|
|
64
|
-
text: 'heartbeat',
|
|
65
|
-
attachments: [],
|
|
66
|
-
response_context: {},
|
|
67
|
-
type: 'message',
|
|
68
|
-
teamvibe: {
|
|
69
|
-
workspaceId: info.workspaceId,
|
|
70
|
-
botId: info.botId,
|
|
71
|
-
botToken: info.botToken,
|
|
72
|
-
channelId: info.channelId,
|
|
73
|
-
brain: {
|
|
74
|
-
brainId: info.brainId,
|
|
75
|
-
gitRepoUrl: info.gitRepoUrl,
|
|
76
|
-
branch: info.branch,
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
try {
|
|
81
|
-
await processMessageFn({
|
|
82
|
-
queueMessage: syntheticMessage,
|
|
83
|
-
receiptHandle: '', // No SQS receipt for synthetic messages
|
|
84
|
-
messageId: syntheticMessage.id,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
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
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}, config.MAINTENANCE_CHECK_INTERVAL_MS);
|
|
93
|
+
// Heartbeat cron — frequent, lightweight
|
|
94
|
+
logger.info(`[Heartbeat] Starting heartbeat cron: "${config.HEARTBEAT_CRON}"`);
|
|
95
|
+
heartbeatJob = new Cron(config.HEARTBEAT_CRON, { timezone: 'UTC', protect: true }, async () => {
|
|
96
|
+
await triggerAllBrains('heartbeat', processMessageFn);
|
|
97
|
+
});
|
|
98
|
+
// Maintenance cron — daily, heavy (consolidation, memory cleanup)
|
|
99
|
+
logger.info(`[Maintenance] Starting maintenance cron: "${config.MAINTENANCE_CRON}"`);
|
|
100
|
+
maintenanceJob = new Cron(config.MAINTENANCE_CRON, { timezone: 'UTC', protect: true }, async () => {
|
|
101
|
+
await triggerAllBrains('maintenance', processMessageFn);
|
|
102
|
+
});
|
|
96
103
|
}
|
|
97
104
|
/**
|
|
98
|
-
* Stop the heartbeat
|
|
105
|
+
* Stop the heartbeat and maintenance crons (for shutdown).
|
|
99
106
|
*/
|
|
100
107
|
export function stopHeartbeatLoop() {
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
if (heartbeatJob) {
|
|
109
|
+
heartbeatJob.stop();
|
|
110
|
+
heartbeatJob = null;
|
|
111
|
+
}
|
|
112
|
+
if (maintenanceJob) {
|
|
113
|
+
maintenanceJob.stop();
|
|
114
|
+
maintenanceJob = null;
|
|
105
115
|
}
|
|
116
|
+
logger.info('[Heartbeat] Stopped heartbeat & maintenance crons');
|
|
106
117
|
}
|
package/dist/poller.js
CHANGED
|
@@ -200,7 +200,7 @@ async function processMessage(received) {
|
|
|
200
200
|
const threadId = queueMessage.thread_id;
|
|
201
201
|
const sessionLog = logger.createSession(logSessionId);
|
|
202
202
|
// Fetch user info if we only have ID
|
|
203
|
-
if (queueMessage.source !== 'cron' && queueMessage.source !== 'heartbeat' &&
|
|
203
|
+
if (queueMessage.source !== 'cron' && queueMessage.source !== 'heartbeat' && queueMessage.source !== 'maintenance' &&
|
|
204
204
|
queueMessage.sender.name === 'Unknown User' &&
|
|
205
205
|
queueMessage.sender.id !== 'unknown') {
|
|
206
206
|
const userInfo = await getUserInfo(queueMessage.teamvibe.botToken, queueMessage.sender.id);
|
package/dist/types.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface ControlMessage {
|
|
|
7
7
|
export type QueueMessage = TeamVibeQueueMessage | ControlMessage;
|
|
8
8
|
export interface TeamVibeQueueMessage {
|
|
9
9
|
id: string;
|
|
10
|
-
source: 'slack' | 'email' | 'cron' | 'heartbeat' | 'webhook';
|
|
10
|
+
source: 'slack' | 'email' | 'cron' | 'heartbeat' | 'maintenance' | 'webhook';
|
|
11
11
|
thread_id: string;
|
|
12
12
|
sender: {
|
|
13
13
|
id: string;
|