From d7f9b5c1fdb7982f89a131bbdc72cf9de9895de2 Mon Sep 17 00:00:00 2001 From: Eugene Datsky Date: Fri, 26 Jun 2026 10:52:32 +1000 Subject: [PATCH 1/3] Fix heartbeat worker watch-mode messages --- .../heartbeat/heartbeat-controller.test.ts | 23 +++++++++++++++++++ .../heartbeat/heartbeat-controller.ts | 20 ++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 sdks/typescript/src/clients/dispatcher/heartbeat/heartbeat-controller.test.ts diff --git a/sdks/typescript/src/clients/dispatcher/heartbeat/heartbeat-controller.test.ts b/sdks/typescript/src/clients/dispatcher/heartbeat/heartbeat-controller.test.ts new file mode 100644 index 0000000000..b270520c29 --- /dev/null +++ b/sdks/typescript/src/clients/dispatcher/heartbeat/heartbeat-controller.test.ts @@ -0,0 +1,23 @@ +import { isHeartbeatMessage } from './heartbeat-controller'; + +describe('isHeartbeatMessage', () => { + it('accepts heartbeat log messages', () => { + expect(isHeartbeatMessage({ type: 'debug', message: 'Heartbeat sent' })).toBe(true); + expect(isHeartbeatMessage({ type: 'info', message: 'Heartbeat started' })).toBe(true); + expect(isHeartbeatMessage({ type: 'warn', message: 'Heartbeat delayed' })).toBe(true); + expect(isHeartbeatMessage({ type: 'error', message: 'Heartbeat failed' })).toBe(true); + }); + + it('ignores Node watch-mode worker dependency messages', () => { + expect(isHeartbeatMessage({ 'watch:require': ['/worker-dep.js'] })).toBe(false); + expect(isHeartbeatMessage({ 'watch:import': ['file:///worker-dep.mjs'] })).toBe(false); + }); + + it('rejects malformed heartbeat messages', () => { + expect(isHeartbeatMessage(undefined)).toBe(false); + expect(isHeartbeatMessage('debug')).toBe(false); + expect(isHeartbeatMessage({ type: 'debug' })).toBe(false); + expect(isHeartbeatMessage({ type: 'green', message: 'not a heartbeat level' })).toBe(false); + expect(isHeartbeatMessage({ type: 'error', message: new Error('failed') })).toBe(false); + }); +}); diff --git a/sdks/typescript/src/clients/dispatcher/heartbeat/heartbeat-controller.ts b/sdks/typescript/src/clients/dispatcher/heartbeat/heartbeat-controller.ts index 808d93927a..43278f180f 100644 --- a/sdks/typescript/src/clients/dispatcher/heartbeat/heartbeat-controller.ts +++ b/sdks/typescript/src/clients/dispatcher/heartbeat/heartbeat-controller.ts @@ -5,11 +5,17 @@ import path from 'path'; import { runThreaded } from '@hatchet/util/thread-helper'; import { ClientConfig } from '../../hatchet-client'; import { DispatcherClient } from '../dispatcher-client'; +import { z } from 'zod/v4'; -export interface HeartbeatMessage { - type: 'info' | 'warn' | 'error' | 'debug'; - message: string; -} +const HeartbeatMessageSchema = z.object({ + type: z.enum(['info', 'warn', 'error', 'debug']), + message: z.string(), +}); + +export type HeartbeatMessage = z.infer; + +export const isHeartbeatMessage = (message: unknown): message is HeartbeatMessage => + HeartbeatMessageSchema.safeParse(message).success; export const STOP_HEARTBEAT = 'stop'; export class Heartbeat { @@ -41,7 +47,11 @@ export class Heartbeat { }, }); - this.heartbeatWorker.on('message', (message: HeartbeatMessage) => { + this.heartbeatWorker.on('message', (message: unknown) => { + if (!isHeartbeatMessage(message)) { + return; + } + this.logger[message.type](message.message); }); } From be11b35f1b5927de19c4b24291171a627ca7f335 Mon Sep 17 00:00:00 2001 From: Eugene Datsky Date: Fri, 26 Jun 2026 11:01:32 +1000 Subject: [PATCH 2/3] Remove AI-generated tests lol --- .../heartbeat/heartbeat-controller.test.ts | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 sdks/typescript/src/clients/dispatcher/heartbeat/heartbeat-controller.test.ts diff --git a/sdks/typescript/src/clients/dispatcher/heartbeat/heartbeat-controller.test.ts b/sdks/typescript/src/clients/dispatcher/heartbeat/heartbeat-controller.test.ts deleted file mode 100644 index b270520c29..0000000000 --- a/sdks/typescript/src/clients/dispatcher/heartbeat/heartbeat-controller.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { isHeartbeatMessage } from './heartbeat-controller'; - -describe('isHeartbeatMessage', () => { - it('accepts heartbeat log messages', () => { - expect(isHeartbeatMessage({ type: 'debug', message: 'Heartbeat sent' })).toBe(true); - expect(isHeartbeatMessage({ type: 'info', message: 'Heartbeat started' })).toBe(true); - expect(isHeartbeatMessage({ type: 'warn', message: 'Heartbeat delayed' })).toBe(true); - expect(isHeartbeatMessage({ type: 'error', message: 'Heartbeat failed' })).toBe(true); - }); - - it('ignores Node watch-mode worker dependency messages', () => { - expect(isHeartbeatMessage({ 'watch:require': ['/worker-dep.js'] })).toBe(false); - expect(isHeartbeatMessage({ 'watch:import': ['file:///worker-dep.mjs'] })).toBe(false); - }); - - it('rejects malformed heartbeat messages', () => { - expect(isHeartbeatMessage(undefined)).toBe(false); - expect(isHeartbeatMessage('debug')).toBe(false); - expect(isHeartbeatMessage({ type: 'debug' })).toBe(false); - expect(isHeartbeatMessage({ type: 'green', message: 'not a heartbeat level' })).toBe(false); - expect(isHeartbeatMessage({ type: 'error', message: new Error('failed') })).toBe(false); - }); -}); From e2a3c71514af5b88884f0fa4e22d6dfe3b8525d4 Mon Sep 17 00:00:00 2001 From: Eugene Datsky Date: Tue, 21 Jul 2026 16:58:47 +1000 Subject: [PATCH 3/3] Update changelog --- sdks/typescript/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sdks/typescript/CHANGELOG.md b/sdks/typescript/CHANGELOG.md index 9b5922a70b..afbaf3edb2 100644 --- a/sdks/typescript/CHANGELOG.md +++ b/sdks/typescript/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Hatchet's TypeScript SDK will be documented in this chang The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- Fixed heartbeat worker logging to ignore Node watch-mode worker reload messages that don’t match the heartbeat message protocol. + ## [1.26.1] - 2026-07-20 ### Added