@teamvibe/poller 0.3.0 → 0.4.0

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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Module-scoped state for poller-level cron-job telemetry.
3
+ *
4
+ * Per teamvibeai/teamvibe.ai#119, the metrics collector reports six measures:
5
+ * last{Heartbeat,Maintenance,AutoUpdate}Job{,Success}Ms
6
+ * each computed as `Date.now() - storedTs` at collection time, with the
7
+ * measure omitted entirely when the cron has never fired (storedTs null).
8
+ *
9
+ * Pulled out of heartbeat-manager.ts / poller.ts into a dedicated module to
10
+ * avoid a circular import — the metrics collector reads from these getters,
11
+ * and heartbeat-manager.ts already imports from the collector.
12
+ */
13
+ export declare function markHeartbeatJobFired(at?: number): void;
14
+ export declare function markHeartbeatJobSuccess(at?: number): void;
15
+ export declare function markMaintenanceJobFired(at?: number): void;
16
+ export declare function markMaintenanceJobSuccess(at?: number): void;
17
+ export declare function markAutoUpdateJobFired(at?: number): void;
18
+ export declare function markAutoUpdateJobSuccess(at?: number): void;
19
+ export interface CronJobTimestamps {
20
+ lastHeartbeatJobFiredAt: number | null;
21
+ lastHeartbeatJobSuccessAt: number | null;
22
+ lastMaintenanceJobFiredAt: number | null;
23
+ lastMaintenanceJobSuccessAt: number | null;
24
+ lastAutoUpdateJobFiredAt: number | null;
25
+ lastAutoUpdateJobSuccessAt: number | null;
26
+ }
27
+ export declare function getCronJobTimestamps(): CronJobTimestamps;
28
+ /**
29
+ * Test-only helper to clear telemetry between tests.
30
+ * @internal
31
+ */
32
+ export declare function __resetCronTelemetryForTest(): void;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Module-scoped state for poller-level cron-job telemetry.
3
+ *
4
+ * Per teamvibeai/teamvibe.ai#119, the metrics collector reports six measures:
5
+ * last{Heartbeat,Maintenance,AutoUpdate}Job{,Success}Ms
6
+ * each computed as `Date.now() - storedTs` at collection time, with the
7
+ * measure omitted entirely when the cron has never fired (storedTs null).
8
+ *
9
+ * Pulled out of heartbeat-manager.ts / poller.ts into a dedicated module to
10
+ * avoid a circular import — the metrics collector reads from these getters,
11
+ * and heartbeat-manager.ts already imports from the collector.
12
+ */
13
+ let lastHeartbeatJobFiredAt = null;
14
+ let lastHeartbeatJobSuccessAt = null;
15
+ let lastMaintenanceJobFiredAt = null;
16
+ let lastMaintenanceJobSuccessAt = null;
17
+ let lastAutoUpdateJobFiredAt = null;
18
+ let lastAutoUpdateJobSuccessAt = null;
19
+ export function markHeartbeatJobFired(at = Date.now()) {
20
+ lastHeartbeatJobFiredAt = at;
21
+ }
22
+ export function markHeartbeatJobSuccess(at = Date.now()) {
23
+ lastHeartbeatJobSuccessAt = at;
24
+ }
25
+ export function markMaintenanceJobFired(at = Date.now()) {
26
+ lastMaintenanceJobFiredAt = at;
27
+ }
28
+ export function markMaintenanceJobSuccess(at = Date.now()) {
29
+ lastMaintenanceJobSuccessAt = at;
30
+ }
31
+ export function markAutoUpdateJobFired(at = Date.now()) {
32
+ lastAutoUpdateJobFiredAt = at;
33
+ }
34
+ export function markAutoUpdateJobSuccess(at = Date.now()) {
35
+ lastAutoUpdateJobSuccessAt = at;
36
+ }
37
+ export function getCronJobTimestamps() {
38
+ return {
39
+ lastHeartbeatJobFiredAt,
40
+ lastHeartbeatJobSuccessAt,
41
+ lastMaintenanceJobFiredAt,
42
+ lastMaintenanceJobSuccessAt,
43
+ lastAutoUpdateJobFiredAt,
44
+ lastAutoUpdateJobSuccessAt,
45
+ };
46
+ }
47
+ /**
48
+ * Test-only helper to clear telemetry between tests.
49
+ * @internal
50
+ */
51
+ export function __resetCronTelemetryForTest() {
52
+ lastHeartbeatJobFiredAt = null;
53
+ lastHeartbeatJobSuccessAt = null;
54
+ lastMaintenanceJobFiredAt = null;
55
+ lastMaintenanceJobSuccessAt = null;
56
+ lastAutoUpdateJobFiredAt = null;
57
+ lastAutoUpdateJobSuccessAt = null;
58
+ }
@@ -15,6 +15,24 @@ export declare function startHeartbeatLoop(processMessageFn: (msg: {
15
15
  receiptHandle: string;
16
16
  messageId: string;
17
17
  }) => Promise<void>): void;
18
+ /**
19
+ * Run the heartbeat cron handler with telemetry instrumentation (#119).
20
+ * Records last-fire timestamp on entry and last-success timestamp on completion.
21
+ * Exported for use by the cron registration above and tests that fire it manually.
22
+ */
23
+ export declare function runHeartbeatJob(processMessageFn: (msg: {
24
+ queueMessage: TeamVibeQueueMessage;
25
+ receiptHandle: string;
26
+ messageId: string;
27
+ }) => Promise<void>): Promise<void>;
28
+ /**
29
+ * Run the maintenance cron handler with telemetry instrumentation (#119).
30
+ */
31
+ export declare function runMaintenanceJob(processMessageFn: (msg: {
32
+ queueMessage: TeamVibeQueueMessage;
33
+ receiptHandle: string;
34
+ messageId: string;
35
+ }) => Promise<void>): Promise<void>;
18
36
  /**
19
37
  * Stop the heartbeat and maintenance crons (for shutdown).
20
38
  */
@@ -6,6 +6,7 @@ import { emitError } from './event-queue.js';
6
6
  import { registerBrainForMetrics } from './metrics/scheduler.js';
7
7
  import { collectMetrics } from './metrics/collector.js';
8
8
  import { sendMetrics } from './metrics/sender.js';
9
+ import { markHeartbeatJobFired, markHeartbeatJobSuccess, markMaintenanceJobFired, markMaintenanceJobSuccess, } from './cron-telemetry.js';
9
10
  import { join } from 'path';
10
11
  const trackedBrains = new Map();
11
12
  let heartbeatJob = null;
@@ -115,14 +116,44 @@ export function startHeartbeatLoop(processMessageFn) {
115
116
  // Heartbeat cron — frequent, lightweight
116
117
  logger.info(`[Heartbeat] Starting heartbeat cron: "${config.HEARTBEAT_CRON}"`);
117
118
  heartbeatJob = new Cron(config.HEARTBEAT_CRON, { timezone: 'UTC', protect: true }, async () => {
118
- await triggerAllBrains('heartbeat', processMessageFn);
119
+ await runHeartbeatJob(processMessageFn);
119
120
  });
120
121
  // Maintenance cron — daily, heavy (consolidation, memory cleanup)
121
122
  logger.info(`[Maintenance] Starting maintenance cron: "${config.MAINTENANCE_CRON}"`);
122
123
  maintenanceJob = new Cron(config.MAINTENANCE_CRON, { timezone: 'UTC', protect: true }, async () => {
123
- await triggerAllBrains('maintenance', processMessageFn);
124
+ await runMaintenanceJob(processMessageFn);
124
125
  });
125
126
  }
127
+ /**
128
+ * Run the heartbeat cron handler with telemetry instrumentation (#119).
129
+ * Records last-fire timestamp on entry and last-success timestamp on completion.
130
+ * Exported for use by the cron registration above and tests that fire it manually.
131
+ */
132
+ export async function runHeartbeatJob(processMessageFn) {
133
+ markHeartbeatJobFired();
134
+ try {
135
+ await triggerAllBrains('heartbeat', processMessageFn);
136
+ markHeartbeatJobSuccess();
137
+ }
138
+ catch (e) {
139
+ logger.error(`[Heartbeat] heartbeatJob handler threw: ${e instanceof Error ? e.message : e}`);
140
+ throw e; // preserve existing behavior; croner protect handles overlap
141
+ }
142
+ }
143
+ /**
144
+ * Run the maintenance cron handler with telemetry instrumentation (#119).
145
+ */
146
+ export async function runMaintenanceJob(processMessageFn) {
147
+ markMaintenanceJobFired();
148
+ try {
149
+ await triggerAllBrains('maintenance', processMessageFn);
150
+ markMaintenanceJobSuccess();
151
+ }
152
+ catch (e) {
153
+ logger.error(`[Maintenance] maintenanceJob handler threw: ${e instanceof Error ? e.message : e}`);
154
+ throw e;
155
+ }
156
+ }
126
157
  /**
127
158
  * Stop the heartbeat and maintenance crons (for shutdown).
128
159
  */
@@ -19,6 +19,12 @@ export interface MetricsPayload {
19
19
  oldestInboxMessageAgeSeconds?: number;
20
20
  uncommittedChangeCount?: number;
21
21
  daysSinceMaintenance?: number;
22
+ lastHeartbeatJobMs?: number;
23
+ lastHeartbeatJobSuccessMs?: number;
24
+ lastMaintenanceJobMs?: number;
25
+ lastMaintenanceJobSuccessMs?: number;
26
+ lastAutoUpdateJobMs?: number;
27
+ lastAutoUpdateJobSuccessMs?: number;
22
28
  timestamp?: string;
23
29
  }
24
30
  /**
@@ -3,6 +3,7 @@ import { statfs } from 'fs';
3
3
  import { promisify } from 'util';
4
4
  import { execFile } from 'child_process';
5
5
  import { join } from 'path';
6
+ import { getCronJobTimestamps } from '../cron-telemetry.js';
6
7
  const statfsAsync = promisify(statfs);
7
8
  const execFileAsync = promisify(execFile);
8
9
  const MS_PER_DAY = 86_400_000;
@@ -210,5 +211,28 @@ export async function collectMetrics(brainPath, brainId) {
210
211
  catch {
211
212
  // Non-git brain, missing git binary, timeout — omit.
212
213
  }
214
+ // Poller-level cron-job telemetry (#119). Same six values for every brain on
215
+ // the poller — see issue body for storage cost rationale. Each measure is
216
+ // `now - storedTs`; if the cron has never fired (storedTs null), omit it.
217
+ const now = Date.now();
218
+ const cron = getCronJobTimestamps();
219
+ if (cron.lastHeartbeatJobFiredAt !== null) {
220
+ payload.lastHeartbeatJobMs = now - cron.lastHeartbeatJobFiredAt;
221
+ }
222
+ if (cron.lastHeartbeatJobSuccessAt !== null) {
223
+ payload.lastHeartbeatJobSuccessMs = now - cron.lastHeartbeatJobSuccessAt;
224
+ }
225
+ if (cron.lastMaintenanceJobFiredAt !== null) {
226
+ payload.lastMaintenanceJobMs = now - cron.lastMaintenanceJobFiredAt;
227
+ }
228
+ if (cron.lastMaintenanceJobSuccessAt !== null) {
229
+ payload.lastMaintenanceJobSuccessMs = now - cron.lastMaintenanceJobSuccessAt;
230
+ }
231
+ if (cron.lastAutoUpdateJobFiredAt !== null) {
232
+ payload.lastAutoUpdateJobMs = now - cron.lastAutoUpdateJobFiredAt;
233
+ }
234
+ if (cron.lastAutoUpdateJobSuccessAt !== null) {
235
+ payload.lastAutoUpdateJobSuccessMs = now - cron.lastAutoUpdateJobSuccessAt;
236
+ }
213
237
  return payload;
214
238
  }
package/dist/poller.d.ts CHANGED
@@ -1 +1,11 @@
1
+ /**
2
+ * Run the auto-update cron handler with telemetry instrumentation (#119).
3
+ * Exported for tests that fire the handler manually.
4
+ *
5
+ * `checkForUpdateFn` is injected (default = real checkForUpdate) so tests
6
+ * can lock in the "swallow but still mark fired" contract that
7
+ * distinguishes scenario (A) cron-doesn't-fire from (B/C) cron-fires-but-fails
8
+ * in morning-check telemetry. Same DI shape as runHeartbeatJob/runMaintenanceJob.
9
+ */
10
+ export declare function runAutoUpdateJob(checkForUpdateFn?: () => Promise<boolean>): Promise<void>;
1
11
  export declare function startPoller(): Promise<void>;
package/dist/poller.js CHANGED
@@ -14,6 +14,7 @@ import { writeMessage, pickNextMessage, markProcessed, markFailed, hasUnprocesse
14
14
  import { Cron } from 'croner';
15
15
  import { isControlMessage, handleControlMessage } from './control-handler.js';
16
16
  import { checkForUpdate } from './auto-update.js';
17
+ import { markAutoUpdateJobFired, markAutoUpdateJobSuccess } from './cron-telemetry.js';
17
18
  import { exec } from 'child_process';
18
19
  import { promisify } from 'util';
19
20
  import { platform } from 'os';
@@ -22,6 +23,30 @@ const execAsync = promisify(exec);
22
23
  const processingMessages = new Set();
23
24
  // Poller auto-update cron (separate from brain heartbeats)
24
25
  let autoUpdateJob = null;
26
+ /**
27
+ * Run the auto-update cron handler with telemetry instrumentation (#119).
28
+ * Exported for tests that fire the handler manually.
29
+ *
30
+ * `checkForUpdateFn` is injected (default = real checkForUpdate) so tests
31
+ * can lock in the "swallow but still mark fired" contract that
32
+ * distinguishes scenario (A) cron-doesn't-fire from (B/C) cron-fires-but-fails
33
+ * in morning-check telemetry. Same DI shape as runHeartbeatJob/runMaintenanceJob.
34
+ */
35
+ export async function runAutoUpdateJob(checkForUpdateFn = checkForUpdate) {
36
+ markAutoUpdateJobFired();
37
+ try {
38
+ const shouldRestart = await checkForUpdateFn();
39
+ markAutoUpdateJobSuccess();
40
+ if (shouldRestart) {
41
+ logger.info('[AutoUpdate] Update applied, restarting...');
42
+ await flushEventQueue();
43
+ process.exit(0);
44
+ }
45
+ }
46
+ catch (error) {
47
+ logger.error(`[AutoUpdate] Check error: ${error instanceof Error ? error.message : error}`);
48
+ }
49
+ }
25
50
  function startAutoUpdateCron() {
26
51
  const schedule = config.AUTO_UPDATE_SCHEDULE;
27
52
  const match = schedule.match(/^(\d{2}):(\d{2})$/);
@@ -35,17 +60,7 @@ function startAutoUpdateCron() {
35
60
  const cronExpr = `${match[2]} ${match[1]} * * *`;
36
61
  logger.info(`[AutoUpdate] Scheduled daily at ${schedule} UTC (cron: ${cronExpr}), policy: ${config.AUTO_UPDATE_POLICY}`);
37
62
  autoUpdateJob = new Cron(cronExpr, { timezone: 'UTC' }, async () => {
38
- try {
39
- const shouldRestart = await checkForUpdate();
40
- if (shouldRestart) {
41
- logger.info('[AutoUpdate] Update applied, restarting...');
42
- await flushEventQueue();
43
- process.exit(0);
44
- }
45
- }
46
- catch (error) {
47
- logger.error(`[AutoUpdate] Check error: ${error instanceof Error ? error.message : error}`);
48
- }
63
+ await runAutoUpdateJob();
49
64
  });
50
65
  }
51
66
  function stopAutoUpdateCron() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamvibe/poller",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {