@hotmeshio/hotmesh 0.4.1 → 0.4.2

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/README.md CHANGED
@@ -1,15 +1,16 @@
1
- # HotMesh MemFlow
1
+ # HotMesh
2
2
 
3
3
  **Permanent-Memory Workflows & AI Agents**
4
4
 
5
5
  ![beta release](https://img.shields.io/badge/release-beta-blue.svg) ![made with typescript](https://img.shields.io/badge/built%20with-typescript-lightblue.svg)
6
6
 
7
- MemFlow is a drop-in Temporal-style engine that runs natively on Postgresbut with a twist:
8
- every workflow owns a *permanent*, JSON-backed context that lives beyond the main workflow.
9
- Any number of *hooks* (lightweight, thread-safe workers) can attach to that record at any
10
- time, read it, and safely write back incremental knowledge.
11
- Think **durable execution** + **shared, evolving memory** perfect for human-in-the-loop
12
- processes and AI agents that learn over time.
7
+ **HotMesh** is a Temporal-style workflow engine that runs natively on PostgreSQL — with a powerful twist: every workflow maintains a permanent, JSON-backed context that persists independently of the workflow itself.
8
+
9
+ This means:
10
+
11
+ * Any number of lightweight, thread-safe **hook workers** can attach to the same workflow record at any time.
12
+ * These hooks can safely **read and incrementally write** to shared state.
13
+ * The result is a **durable execution model** with **evolving memory**, ideal for **human-in-the-loop processes** and **AI agents that learn over time**.
13
14
 
14
15
  ---
15
16
 
@@ -61,10 +62,11 @@ async function main() {
61
62
  main().catch(console.error);
62
63
  ```
63
64
 
65
+ ---
64
66
 
65
67
  ## 🧠 How Permanent Memory Works
66
68
 
67
- * **Context = JSONB row** in `<yourappname>.jobs` table
69
+ * **Context = persistent JSON record** – each workflow's memory is stored as a JSONB row in your Postgres database
68
70
  * **Atomic operations** (`set`, `merge`, `append`, `increment`, `toggle`, `delete`, …)
69
71
  * **Transactional** – every update participates in the workflow/DB transaction
70
72
  * **Time-travel-safe** – full replay compatibility; side-effect detector guarantees determinism
@@ -135,14 +137,37 @@ export async function hook2(name: string, kind: string): Promise<void> {
135
137
  await ctx.merge({ user: { lastSeen: new Date().toISOString() } });
136
138
  await MemFlow.workflow.signal('hook2-complete', { ok: true });
137
139
  }
138
- ```
139
140
 
140
- **Highlights**
141
+ /* ------------ Worker/Hook Registration ------------ */
142
+ async function startWorker() {
143
+ const mf = await MemFlow.init({
144
+ appId: 'my-app',
145
+ engine: {
146
+ connection: {
147
+ class: Postgres,
148
+ options: { connectionString: process.env.DATABASE_URL }
149
+ }
150
+ }
151
+ });
152
+
153
+ const worker = await mf.worker.create({
154
+ taskQueue: 'contextual',
155
+ workflow: example
156
+ });
141
157
 
142
- * Hook functions are replay-safe.
143
- * Hook functions can safely read and write to the the *same* JSON context.
144
- * All context operations (`set`, `merge`, `append`, etc.) execute transactionally.
145
- * Context data is stored as JSONB; add partial indexes for improved query analysis.
158
+ await mf.worker.create({
159
+ taskQueue: 'contextual',
160
+ workflow: hook1
161
+ });
162
+
163
+ await mf.worker.create({
164
+ taskQueue: 'contextual',
165
+ workflow: hook2
166
+ });
167
+
168
+ console.log('Workers and hooks started and listening...');
169
+ }
170
+ ```
146
171
 
147
172
  ---
148
173
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Permanent-Memory Workflows & AI Agents",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -1,6 +1,28 @@
1
1
  /**
2
2
  * Sends a signal payload to any paused workflow thread awaiting this signal.
3
- * @param {string} signalId - Unique signal identifier.
3
+ * This method is commonly used to coordinate between workflows, hook functions,
4
+ * and external events.
5
+ *
6
+ * @example
7
+ * // Basic usage - send a simple signal with data
8
+ * await MemFlow.workflow.signal('signal-id', { name: 'WarmMash' });
9
+ *
10
+ * @example
11
+ * // Hook function signaling completion
12
+ * export async function exampleHook(name: string): Promise<void> {
13
+ * const result = await processData(name);
14
+ * await MemFlow.workflow.signal('hook-complete', { data: result });
15
+ * }
16
+ *
17
+ * @example
18
+ * // Signal with complex data structure
19
+ * await MemFlow.workflow.signal('process-complete', {
20
+ * status: 'success',
21
+ * data: { id: 123, name: 'test' },
22
+ * timestamp: new Date().toISOString()
23
+ * });
24
+ *
25
+ * @param {string} signalId - Unique signal identifier that matches a waitFor() call.
4
26
  * @param {Record<any, any>} data - The payload to send with the signal.
5
27
  * @returns {Promise<string>} The resulting hook/stream ID.
6
28
  */
@@ -5,7 +5,29 @@ const common_1 = require("./common");
5
5
  const isSideEffectAllowed_1 = require("./isSideEffectAllowed");
6
6
  /**
7
7
  * Sends a signal payload to any paused workflow thread awaiting this signal.
8
- * @param {string} signalId - Unique signal identifier.
8
+ * This method is commonly used to coordinate between workflows, hook functions,
9
+ * and external events.
10
+ *
11
+ * @example
12
+ * // Basic usage - send a simple signal with data
13
+ * await MemFlow.workflow.signal('signal-id', { name: 'WarmMash' });
14
+ *
15
+ * @example
16
+ * // Hook function signaling completion
17
+ * export async function exampleHook(name: string): Promise<void> {
18
+ * const result = await processData(name);
19
+ * await MemFlow.workflow.signal('hook-complete', { data: result });
20
+ * }
21
+ *
22
+ * @example
23
+ * // Signal with complex data structure
24
+ * await MemFlow.workflow.signal('process-complete', {
25
+ * status: 'success',
26
+ * data: { id: 123, name: 'test' },
27
+ * timestamp: new Date().toISOString()
28
+ * });
29
+ *
30
+ * @param {string} signalId - Unique signal identifier that matches a waitFor() call.
9
31
  * @param {Record<any, any>} data - The payload to send with the signal.
10
32
  * @returns {Promise<string>} The resulting hook/stream ID.
11
33
  */
@@ -2,7 +2,23 @@
2
2
  * Sleeps the workflow for a specified duration, deterministically.
3
3
  * On replay, it will not actually sleep again, but resume after sleep.
4
4
  *
5
- * @param {string} duration - A human-readable duration string (e.g., '1m', '2 hours').
5
+ * @example
6
+ * // Basic usage - sleep for a specific duration
7
+ * await MemFlow.workflow.sleepFor('2 seconds');
8
+ *
9
+ * @example
10
+ * // Using with Promise.all for parallel operations
11
+ * const [greeting, timeInSeconds] = await Promise.all([
12
+ * someActivity(name),
13
+ * MemFlow.workflow.sleepFor('1 second')
14
+ * ]);
15
+ *
16
+ * @example
17
+ * // Multiple sequential sleeps
18
+ * await MemFlow.workflow.sleepFor('1 seconds'); // First pause
19
+ * await MemFlow.workflow.sleepFor('2 seconds'); // Second pause
20
+ *
21
+ * @param {string} duration - A human-readable duration string (e.g., '1m', '2 hours', '30 seconds').
6
22
  * @returns {Promise<number>} The resolved duration in seconds.
7
23
  */
8
24
  export declare function sleepFor(duration: string): Promise<number>;
@@ -7,7 +7,23 @@ const didRun_1 = require("./didRun");
7
7
  * Sleeps the workflow for a specified duration, deterministically.
8
8
  * On replay, it will not actually sleep again, but resume after sleep.
9
9
  *
10
- * @param {string} duration - A human-readable duration string (e.g., '1m', '2 hours').
10
+ * @example
11
+ * // Basic usage - sleep for a specific duration
12
+ * await MemFlow.workflow.sleepFor('2 seconds');
13
+ *
14
+ * @example
15
+ * // Using with Promise.all for parallel operations
16
+ * const [greeting, timeInSeconds] = await Promise.all([
17
+ * someActivity(name),
18
+ * MemFlow.workflow.sleepFor('1 second')
19
+ * ]);
20
+ *
21
+ * @example
22
+ * // Multiple sequential sleeps
23
+ * await MemFlow.workflow.sleepFor('1 seconds'); // First pause
24
+ * await MemFlow.workflow.sleepFor('2 seconds'); // Second pause
25
+ *
26
+ * @param {string} duration - A human-readable duration string (e.g., '1m', '2 hours', '30 seconds').
11
27
  * @returns {Promise<number>} The resolved duration in seconds.
12
28
  */
13
29
  async function sleepFor(duration) {
@@ -1,7 +1,28 @@
1
1
  /**
2
2
  * Pauses the workflow until a signal with the given `signalId` is received.
3
+ * This method is commonly used to coordinate between the main workflow and hook functions,
4
+ * or to wait for external events.
3
5
  *
4
- * @template T
6
+ * @example
7
+ * // Basic usage - wait for a single signal
8
+ * const payload = await MemFlow.workflow.waitFor<PayloadType>('abcdefg');
9
+ *
10
+ * @example
11
+ * // Wait for multiple signals in parallel
12
+ * const [signal1, signal2] = await Promise.all([
13
+ * MemFlow.workflow.waitFor<Record<string, any>>('signal1'),
14
+ * MemFlow.workflow.waitFor<Record<string, any>>('signal2')
15
+ * ]);
16
+ *
17
+ * @example
18
+ * // Typical pattern with hook functions
19
+ * // In main workflow:
20
+ * await MemFlow.workflow.waitFor<ResponseType>('hook-complete');
21
+ *
22
+ * // In hook function:
23
+ * await MemFlow.workflow.signal('hook-complete', { data: result });
24
+ *
25
+ * @template T - The type of data expected in the signal payload
5
26
  * @param {string} signalId - A unique signal identifier shared by the sender and receiver.
6
27
  * @returns {Promise<T>} The data payload associated with the received signal.
7
28
  */
@@ -5,8 +5,29 @@ const common_1 = require("./common");
5
5
  const didRun_1 = require("./didRun");
6
6
  /**
7
7
  * Pauses the workflow until a signal with the given `signalId` is received.
8
+ * This method is commonly used to coordinate between the main workflow and hook functions,
9
+ * or to wait for external events.
8
10
  *
9
- * @template T
11
+ * @example
12
+ * // Basic usage - wait for a single signal
13
+ * const payload = await MemFlow.workflow.waitFor<PayloadType>('abcdefg');
14
+ *
15
+ * @example
16
+ * // Wait for multiple signals in parallel
17
+ * const [signal1, signal2] = await Promise.all([
18
+ * MemFlow.workflow.waitFor<Record<string, any>>('signal1'),
19
+ * MemFlow.workflow.waitFor<Record<string, any>>('signal2')
20
+ * ]);
21
+ *
22
+ * @example
23
+ * // Typical pattern with hook functions
24
+ * // In main workflow:
25
+ * await MemFlow.workflow.waitFor<ResponseType>('hook-complete');
26
+ *
27
+ * // In hook function:
28
+ * await MemFlow.workflow.signal('hook-complete', { data: result });
29
+ *
30
+ * @template T - The type of data expected in the signal payload
10
31
  * @param {string} signalId - A unique signal identifier shared by the sender and receiver.
11
32
  * @returns {Promise<T>} The data payload associated with the received signal.
12
33
  */
@@ -18,6 +18,7 @@ import { KVTables } from './kvtables';
18
18
  declare class PostgresStoreService extends StoreService<ProviderClient, ProviderTransaction> {
19
19
  pgClient: PostgresClientType;
20
20
  kvTables: ReturnType<typeof KVTables>;
21
+ isScout: boolean;
21
22
  transact(): ProviderTransaction;
22
23
  constructor(storeClient: ProviderClient);
23
24
  init(namespace: string, appId: string, logger: ILogger): Promise<HotMeshApps>;
@@ -141,5 +142,37 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
141
142
  setThrottleRate(options: ThrottleOptions): Promise<void>;
142
143
  getThrottleRates(): Promise<StringStringType>;
143
144
  getThrottleRate(topic: string): Promise<number>;
145
+ /**
146
+ * Deploy time-aware notification triggers and functions
147
+ */
148
+ private deployTimeNotificationTriggers;
149
+ /**
150
+ * Enhanced time scout that uses LISTEN/NOTIFY to reduce polling
151
+ */
152
+ startTimeScoutWithNotifications(timeEventCallback: (jobId: string, gId: string, activityId: string, type: WorkListTaskType) => Promise<void>): Promise<void>;
153
+ /**
154
+ * Handle time notifications from PostgreSQL
155
+ */
156
+ private handleTimeNotification;
157
+ /**
158
+ * Process time hooks that are ready to be awakened
159
+ */
160
+ private processReadyTimeHooks;
161
+ /**
162
+ * Enhanced time scout process that uses notifications
163
+ */
164
+ private processTimeHooksWithNotifications;
165
+ /**
166
+ * Get the next awakening time from the database
167
+ */
168
+ private getNextAwakeningTime;
169
+ /**
170
+ * Update the time scout's sleep timing based on schedule changes
171
+ */
172
+ private updateTimeScoutSleep;
173
+ /**
174
+ * Enhanced shouldScout that can handle notifications
175
+ */
176
+ shouldScout(): Promise<boolean>;
144
177
  }
145
178
  export { PostgresStoreService };
@@ -39,6 +39,7 @@ class PostgresStoreService extends __1.StoreService {
39
39
  }
40
40
  constructor(storeClient) {
41
41
  super(storeClient);
42
+ this.isScout = false;
42
43
  //Instead of directly referencing the 'pg' package and methods like 'query',
43
44
  // the PostgresStore wraps the 'pg' client in a class that implements
44
45
  // the Redis client interface. This allows the same methods to be called
@@ -58,6 +59,8 @@ class PostgresStoreService extends __1.StoreService {
58
59
  this.logger = logger;
59
60
  //confirm db tables exist
60
61
  await this.kvTables.deploy(appId);
62
+ // Deploy time notification triggers
63
+ await this.deployTimeNotificationTriggers(appId);
61
64
  //note: getSettings will contact db to confirm r/w access
62
65
  const settings = await this.getSettings(true);
63
66
  this.cache = new cache_1.Cache(appId, settings);
@@ -1032,5 +1035,210 @@ class PostgresStoreService extends __1.StoreService {
1032
1035
  }
1033
1036
  return resolveRate(response, topic);
1034
1037
  }
1038
+ /**
1039
+ * Deploy time-aware notification triggers and functions
1040
+ */
1041
+ async deployTimeNotificationTriggers(appId) {
1042
+ const schemaName = this.kvsql().safeName(appId);
1043
+ const client = this.pgClient;
1044
+ try {
1045
+ // Read the SQL template and replace schema placeholder
1046
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
1047
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
1048
+ const sqlTemplate = fs.readFileSync(path.join(__dirname, 'time-notify.sql'), 'utf8');
1049
+ const sql = sqlTemplate.replace(/{schema}/g, schemaName);
1050
+ // Execute the entire SQL as one statement (functions contain $$ blocks with semicolons)
1051
+ await client.query(sql);
1052
+ this.logger.info('postgres-time-notifications-deployed', {
1053
+ appId,
1054
+ schemaName,
1055
+ message: 'Time-aware notifications ENABLED - using LISTEN/NOTIFY instead of polling'
1056
+ });
1057
+ }
1058
+ catch (error) {
1059
+ this.logger.error('postgres-time-notifications-deploy-error', {
1060
+ appId,
1061
+ schemaName,
1062
+ error: error.message
1063
+ });
1064
+ // Don't throw - fall back to polling mode
1065
+ }
1066
+ }
1067
+ /**
1068
+ * Enhanced time scout that uses LISTEN/NOTIFY to reduce polling
1069
+ */
1070
+ async startTimeScoutWithNotifications(timeEventCallback) {
1071
+ const channelName = `time_hooks_${this.appId}`;
1072
+ try {
1073
+ // Set up LISTEN for time notifications
1074
+ await this.pgClient.query(`LISTEN "${channelName}"`);
1075
+ // Set up notification handler
1076
+ this.pgClient.on('notification', (notification) => {
1077
+ this.handleTimeNotification(notification, timeEventCallback);
1078
+ });
1079
+ this.logger.debug('postgres-time-scout-notifications-started', {
1080
+ appId: this.appId,
1081
+ channelName
1082
+ });
1083
+ // Start the enhanced time scout loop
1084
+ await this.processTimeHooksWithNotifications(timeEventCallback);
1085
+ }
1086
+ catch (error) {
1087
+ this.logger.error('postgres-time-scout-notifications-error', {
1088
+ appId: this.appId,
1089
+ error
1090
+ });
1091
+ // Fall back to regular polling mode
1092
+ throw error;
1093
+ }
1094
+ }
1095
+ /**
1096
+ * Handle time notifications from PostgreSQL
1097
+ */
1098
+ async handleTimeNotification(notification, timeEventCallback) {
1099
+ try {
1100
+ const payload = JSON.parse(notification.payload);
1101
+ const { type, app_id, next_awakening, ready_at } = payload;
1102
+ if (app_id !== this.appId) {
1103
+ return; // Not for this app
1104
+ }
1105
+ this.logger.debug('postgres-time-notification-received', {
1106
+ type,
1107
+ appId: app_id,
1108
+ nextAwakening: next_awakening,
1109
+ readyAt: ready_at
1110
+ });
1111
+ if (type === 'time_hooks_ready') {
1112
+ // Process any ready time hooks immediately
1113
+ await this.processReadyTimeHooks(timeEventCallback);
1114
+ }
1115
+ else if (type === 'time_schedule_updated') {
1116
+ // Update our sleep timing if we're the time scout
1117
+ await this.updateTimeScoutSleep(next_awakening);
1118
+ }
1119
+ }
1120
+ catch (error) {
1121
+ this.logger.error('postgres-time-notification-handle-error', {
1122
+ notification,
1123
+ error
1124
+ });
1125
+ }
1126
+ }
1127
+ /**
1128
+ * Process time hooks that are ready to be awakened
1129
+ */
1130
+ async processReadyTimeHooks(timeEventCallback) {
1131
+ let hasMoreTasks = true;
1132
+ while (hasMoreTasks) {
1133
+ const workListTask = await this.getNextTask();
1134
+ if (Array.isArray(workListTask)) {
1135
+ const [listKey, target, gId, activityId, type] = workListTask;
1136
+ if (type === 'child') {
1137
+ // Skip child tasks - they're handled by ancestors
1138
+ }
1139
+ else if (type === 'delist') {
1140
+ // Delist the signal key
1141
+ const key = this.mintKey(key_1.KeyType.SIGNALS, { appId: this.appId });
1142
+ await this.delistSignalKey(key, target);
1143
+ }
1144
+ else {
1145
+ // Process the task
1146
+ await timeEventCallback(target, gId, activityId, type);
1147
+ }
1148
+ }
1149
+ else if (workListTask === true) {
1150
+ // A worklist was emptied, continue processing
1151
+ continue;
1152
+ }
1153
+ else {
1154
+ // No more tasks ready
1155
+ hasMoreTasks = false;
1156
+ }
1157
+ }
1158
+ }
1159
+ /**
1160
+ * Enhanced time scout process that uses notifications
1161
+ */
1162
+ async processTimeHooksWithNotifications(timeEventCallback) {
1163
+ let currentSleepTimeout = null;
1164
+ // Function to calculate next sleep duration
1165
+ const calculateNextSleep = async () => {
1166
+ const nextAwakeningTime = await this.getNextAwakeningTime();
1167
+ if (nextAwakeningTime) {
1168
+ const sleepMs = nextAwakeningTime - Date.now();
1169
+ return Math.max(sleepMs, 0);
1170
+ }
1171
+ // Default sleep if no tasks scheduled
1172
+ return enums_1.HMSH_FIDELITY_SECONDS * 1000;
1173
+ };
1174
+ // Main loop
1175
+ while (true) {
1176
+ try {
1177
+ if (await this.shouldScout()) {
1178
+ // Process any ready tasks
1179
+ await this.processReadyTimeHooks(timeEventCallback);
1180
+ // Calculate next sleep
1181
+ const sleepMs = await calculateNextSleep();
1182
+ // Sleep with ability to be interrupted by notifications
1183
+ await new Promise((resolve) => {
1184
+ currentSleepTimeout = setTimeout(resolve, sleepMs);
1185
+ });
1186
+ }
1187
+ else {
1188
+ // Not the scout, sleep longer
1189
+ await (0, utils_1.sleepFor)(enums_1.HMSH_SCOUT_INTERVAL_SECONDS * 1000);
1190
+ }
1191
+ }
1192
+ catch (error) {
1193
+ this.logger.error('postgres-time-scout-loop-error', { error });
1194
+ await (0, utils_1.sleepFor)(1000);
1195
+ }
1196
+ }
1197
+ }
1198
+ /**
1199
+ * Get the next awakening time from the database
1200
+ */
1201
+ async getNextAwakeningTime() {
1202
+ const schemaName = this.kvsql().safeName(this.appId);
1203
+ const appKey = `${this.appId}:time_range`;
1204
+ try {
1205
+ const result = await this.pgClient.query(`SELECT ${schemaName}.get_next_awakening_time($1) as next_time`, [appKey]);
1206
+ if (result.rows[0]?.next_time) {
1207
+ return new Date(result.rows[0].next_time).getTime();
1208
+ }
1209
+ return null;
1210
+ }
1211
+ catch (error) {
1212
+ this.logger.error('postgres-get-next-awakening-error', { error });
1213
+ return null;
1214
+ }
1215
+ }
1216
+ /**
1217
+ * Update the time scout's sleep timing based on schedule changes
1218
+ */
1219
+ async updateTimeScoutSleep(nextAwakening) {
1220
+ // This could be used to interrupt current sleep and recalculate
1221
+ // For now, just log the schedule update
1222
+ this.logger.debug('postgres-time-schedule-updated', {
1223
+ nextAwakening,
1224
+ currentTime: Date.now()
1225
+ });
1226
+ }
1227
+ /**
1228
+ * Enhanced shouldScout that can handle notifications
1229
+ */
1230
+ async shouldScout() {
1231
+ const wasScout = this.isScout;
1232
+ const isScout = wasScout || (this.isScout = await this.reserveScoutRole('time'));
1233
+ if (isScout) {
1234
+ if (!wasScout) {
1235
+ setTimeout(() => {
1236
+ this.isScout = false;
1237
+ }, enums_1.HMSH_SCOUT_INTERVAL_SECONDS * 1000);
1238
+ }
1239
+ return true;
1240
+ }
1241
+ return false;
1242
+ }
1035
1243
  }
1036
1244
  exports.PostgresStoreService = PostgresStoreService;
@@ -32,5 +32,17 @@ declare class TaskService {
32
32
  registerWebHook(topic: string, context: JobState, dad: string, expire: number, transaction?: ProviderTransaction): Promise<string>;
33
33
  processWebHookSignal(topic: string, data: Record<string, unknown>): Promise<[string, string, string, string] | undefined>;
34
34
  deleteWebHookSignal(topic: string, data: Record<string, unknown>): Promise<number>;
35
+ /**
36
+ * Enhanced processTimeHooks that uses notifications for PostgreSQL stores
37
+ */
38
+ processTimeHooksWithNotifications(timeEventCallback: (jobId: string, gId: string, activityId: string, type: WorkListTaskType) => Promise<void>): Promise<void>;
39
+ /**
40
+ * Check if this is a PostgreSQL store
41
+ */
42
+ private isPostgresStore;
43
+ /**
44
+ * Check if the store supports notifications
45
+ */
46
+ private supportsNotifications;
35
47
  }
36
48
  export { TaskService };
@@ -202,5 +202,52 @@ class TaskService {
202
202
  throw new Error('signaler.process:error: hook rule not found');
203
203
  }
204
204
  }
205
+ /**
206
+ * Enhanced processTimeHooks that uses notifications for PostgreSQL stores
207
+ */
208
+ async processTimeHooksWithNotifications(timeEventCallback) {
209
+ // Check if the store supports notifications
210
+ if (this.isPostgresStore() && this.supportsNotifications()) {
211
+ try {
212
+ this.logger.info('task-using-notification-mode', {
213
+ appId: this.store.appId,
214
+ message: 'Time scout using PostgreSQL LISTEN/NOTIFY mode for efficient task processing'
215
+ });
216
+ // Use the PostgreSQL store's notification-based approach
217
+ await this.store.startTimeScoutWithNotifications(timeEventCallback);
218
+ }
219
+ catch (error) {
220
+ this.logger.warn('task-notifications-fallback', {
221
+ appId: this.store.appId,
222
+ error: error.message,
223
+ fallbackTo: 'polling',
224
+ message: 'Notification mode failed - falling back to traditional polling'
225
+ });
226
+ // Fall back to regular polling
227
+ await this.processTimeHooks(timeEventCallback);
228
+ }
229
+ }
230
+ else {
231
+ this.logger.info('task-using-polling-mode', {
232
+ appId: this.store.appId,
233
+ storeType: this.store.constructor.name,
234
+ message: 'Time scout using traditional polling mode (notifications not available)'
235
+ });
236
+ // Use regular polling for non-PostgreSQL stores
237
+ await this.processTimeHooks(timeEventCallback);
238
+ }
239
+ }
240
+ /**
241
+ * Check if this is a PostgreSQL store
242
+ */
243
+ isPostgresStore() {
244
+ return this.store.constructor.name === 'PostgresStoreService';
245
+ }
246
+ /**
247
+ * Check if the store supports notifications
248
+ */
249
+ supportsNotifications() {
250
+ return typeof this.store.startTimeScoutWithNotifications === 'function';
251
+ }
205
252
  }
206
253
  exports.TaskService = TaskService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Permanent-Memory Workflows & AI Agents",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",