@teamvibe/poller 0.1.69 → 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 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
- MAINTENANCE_INTERVAL_MS: z.ZodDefault<z.ZodNumber>;
31
- MAINTENANCE_CHECK_INTERVAL_MS: z.ZodDefault<z.ZodNumber>;
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
- MAINTENANCE_INTERVAL_MS: number;
60
- MAINTENANCE_CHECK_INTERVAL_MS: number;
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
- MAINTENANCE_INTERVAL_MS?: number | undefined;
95
- MAINTENANCE_CHECK_INTERVAL_MS?: number | undefined;
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
- MAINTENANCE_INTERVAL_MS: number;
127
- MAINTENANCE_CHECK_INTERVAL_MS: number;
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
- // Maintenance heartbeat
46
- MAINTENANCE_INTERVAL_MS: z.coerce.number().default(21600000), // 6 hours between maintenance per brain
47
- MAINTENANCE_CHECK_INTERVAL_MS: z.coerce.number().default(300000), // 5 min check loop
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 periodic heartbeat check loop.
9
- * Checks every MAINTENANCE_CHECK_INTERVAL_MS whether any brain needs maintenance.
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 loop (for shutdown).
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 heartbeatInterval = null;
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
- * Start the periodic heartbeat check loop.
34
- * Checks every MAINTENANCE_CHECK_INTERVAL_MS whether any brain needs maintenance.
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
- logger.info(`[Heartbeat] Starting heartbeat loop (check every ${config.MAINTENANCE_CHECK_INTERVAL_MS}ms, interval ${config.MAINTENANCE_INTERVAL_MS}ms)`);
42
- heartbeatInterval = setInterval(async () => {
43
- const now = Date.now();
44
- for (const [brainId, info] of trackedBrains.entries()) {
45
- const elapsed = now - info.lastHeartbeat;
46
- if (elapsed < config.MAINTENANCE_INTERVAL_MS) {
47
- continue;
48
- }
49
- if (isAtCapacity()) {
50
- logger.debug(`[Heartbeat] At capacity, skipping heartbeat for ${brainId}`);
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 loop (for shutdown).
105
+ * Stop the heartbeat and maintenance crons (for shutdown).
99
106
  */
100
107
  export function stopHeartbeatLoop() {
101
- if (heartbeatInterval) {
102
- clearInterval(heartbeatInterval);
103
- heartbeatInterval = null;
104
- logger.info('[Heartbeat] Stopped heartbeat loop');
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamvibe/poller",
3
- "version": "0.1.69",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {