@hotmeshio/hotmesh 0.10.2 → 0.11.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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/build/modules/errors.d.ts +2 -0
  3. package/build/modules/errors.js +2 -0
  4. package/build/modules/key.js +3 -2
  5. package/build/package.json +2 -2
  6. package/build/services/activities/worker.js +10 -0
  7. package/build/services/dba/index.d.ts +2 -1
  8. package/build/services/dba/index.js +11 -2
  9. package/build/services/durable/client.js +6 -1
  10. package/build/services/durable/exporter.d.ts +15 -0
  11. package/build/services/durable/exporter.js +343 -5
  12. package/build/services/durable/schemas/factory.d.ts +1 -1
  13. package/build/services/durable/schemas/factory.js +27 -4
  14. package/build/services/durable/worker.d.ts +2 -2
  15. package/build/services/durable/worker.js +15 -9
  16. package/build/services/durable/workflow/context.js +2 -0
  17. package/build/services/durable/workflow/execChild.js +5 -2
  18. package/build/services/durable/workflow/hook.js +6 -0
  19. package/build/services/durable/workflow/proxyActivities.js +3 -4
  20. package/build/services/engine/index.js +5 -3
  21. package/build/services/store/index.d.ts +40 -0
  22. package/build/services/store/providers/postgres/exporter-sql.d.ts +23 -0
  23. package/build/services/store/providers/postgres/exporter-sql.js +52 -0
  24. package/build/services/store/providers/postgres/kvtables.js +6 -0
  25. package/build/services/store/providers/postgres/postgres.d.ts +34 -0
  26. package/build/services/store/providers/postgres/postgres.js +99 -0
  27. package/build/services/stream/providers/postgres/kvtables.d.ts +1 -1
  28. package/build/services/stream/providers/postgres/kvtables.js +175 -82
  29. package/build/services/stream/providers/postgres/lifecycle.d.ts +4 -3
  30. package/build/services/stream/providers/postgres/lifecycle.js +6 -5
  31. package/build/services/stream/providers/postgres/messages.d.ts +9 -6
  32. package/build/services/stream/providers/postgres/messages.js +121 -75
  33. package/build/services/stream/providers/postgres/notifications.d.ts +5 -2
  34. package/build/services/stream/providers/postgres/notifications.js +39 -35
  35. package/build/services/stream/providers/postgres/postgres.d.ts +20 -118
  36. package/build/services/stream/providers/postgres/postgres.js +83 -140
  37. package/build/services/stream/registry.d.ts +62 -0
  38. package/build/services/stream/registry.js +198 -0
  39. package/build/services/worker/index.js +20 -6
  40. package/build/types/durable.d.ts +6 -1
  41. package/build/types/error.d.ts +2 -0
  42. package/build/types/exporter.d.ts +39 -0
  43. package/build/types/hotmesh.d.ts +7 -1
  44. package/build/types/stream.d.ts +2 -0
  45. package/package.json +2 -2
package/README.md CHANGED
@@ -11,7 +11,7 @@ npm install @hotmeshio/hotmesh
11
11
  ## Use HotMesh for
12
12
 
13
13
  - **Durable pipelines** — Orchestrate long-running, multi-step pipelines transactionally.
14
- - **Temporal alternative** — The `Durable` module provides a Temporal-compatible API (`Client`, `Worker`, `proxyActivities`, `sleepFor`, `startChild`, signals) that runs directly on Postgres. No app server required.
14
+ - **Familiar Temporal syntax** — The `Durable` module provides a Temporal-compatible API (`Client`, `Worker`, `proxyActivities`, `sleepFor`, `startChild`, signals) that runs directly on Postgres. No app server required.
15
15
  - **Distributed state machines** — Build stateful applications where every component can [fail and recover](https://github.com/hotmeshio/sdk-typescript/blob/main/services/collator/README.md).
16
16
  - **AI and training pipelines** — Multi-step AI workloads where each stage is expensive and must not be repeated on failure. A crashed pipeline resumes from the last committed step, not from the beginning.
17
17
 
@@ -52,6 +52,8 @@ declare class DurableChildError extends Error {
52
52
  parentWorkflowId: string;
53
53
  workflowId: string;
54
54
  workflowTopic: string;
55
+ taskQueue: string;
56
+ workflowName: string;
55
57
  type: string;
56
58
  constructor(params: DurableChildErrorType);
57
59
  }
@@ -54,6 +54,8 @@ class DurableChildError extends Error {
54
54
  this.arguments = params.arguments;
55
55
  this.workflowId = params.workflowId;
56
56
  this.workflowTopic = params.workflowTopic;
57
+ this.taskQueue = params.taskQueue;
58
+ this.workflowName = params.workflowName;
57
59
  this.parentWorkflowId = params.parentWorkflowId;
58
60
  this.expire = params.expire;
59
61
  this.persistent = params.persistent;
@@ -143,7 +143,7 @@ class KeyService {
143
143
  case 'v':
144
144
  return 'versions';
145
145
  case 'x':
146
- return id === '' ? 'streams' : 'stream_topics';
146
+ return id === '' ? 'engine_streams' : 'worker_streams';
147
147
  case 'hooks':
148
148
  return 'signal_patterns';
149
149
  case 'signals':
@@ -174,7 +174,8 @@ class KeyService {
174
174
  return 's';
175
175
  case 'versions':
176
176
  return 'v';
177
- case 'streams':
177
+ case 'engine_streams':
178
+ case 'worker_streams':
178
179
  return 'x';
179
180
  case 'signal_patterns':
180
181
  return 'hooks';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "description": "Permanent-Memory Workflows & AI Agents",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -46,7 +46,7 @@
46
46
  "test:durable:sleep": "vitest run tests/durable/sleep/postgres.test.ts",
47
47
  "test:durable:signal": "vitest run tests/durable/signal/postgres.test.ts",
48
48
  "test:durable:unknown": "vitest run tests/durable/unknown/postgres.test.ts",
49
- "test:durable:exporter": "vitest run tests/durable/exporter/exporter.test.ts",
49
+ "test:durable:exporter": "HMSH_LOGLEVEL=info vitest run tests/durable/exporter",
50
50
  "test:durable:exporter:debug": "EXPORT_DEBUG=1 HMSH_LOGLEVEL=error vitest run tests/durable/basic/postgres.test.ts",
51
51
  "test:dba": "vitest run tests/dba",
52
52
  "test:cycle": "vitest run tests/functional/cycle",
@@ -189,6 +189,15 @@ class Worker extends activity_1.Activity {
189
189
  }
190
190
  async execActivity(transaction) {
191
191
  const topic = pipe_1.Pipe.resolve(this.config.subtype, this.context);
192
+ // Extract workflow name from job data (set by durable client) or derive from subscribes
193
+ const jobData = this.context.data;
194
+ let wfn = jobData?.workflowName || '';
195
+ if (!wfn && this.config.subscribes) {
196
+ // Fallback: derive from subscribes by removing topic prefix
197
+ wfn = this.config.subscribes.startsWith(`${topic}-`)
198
+ ? this.config.subscribes.substring(topic.length + 1)
199
+ : this.config.subscribes;
200
+ }
192
201
  const streamData = {
193
202
  metadata: {
194
203
  guid: (0, utils_1.guid)(),
@@ -197,6 +206,7 @@ class Worker extends activity_1.Activity {
197
206
  dad: this.metadata.dad,
198
207
  aid: this.metadata.aid,
199
208
  topic,
209
+ wfn,
200
210
  spn: this.context['$self'].output.metadata.l1s,
201
211
  trc: this.context.metadata.trc,
202
212
  },
@@ -12,7 +12,8 @@ import { PostgresClientType } from '../../types/postgres';
12
12
  * |---|---|
13
13
  * | `{appId}.jobs` | Completed/expired jobs with `expired_at` set |
14
14
  * | `{appId}.jobs_attributes` | Execution artifacts (`adata`, `hmark`, `status`, `other`) that are only needed during workflow execution |
15
- * | `{appId}.streams` | Processed stream messages with `expired_at` set |
15
+ * | `{appId}.engine_streams` | Processed engine stream messages with `expired_at` set |
16
+ * | `{appId}.worker_streams` | Processed worker stream messages with `expired_at` set |
16
17
  *
17
18
  * The `DBA` service addresses this with two methods:
18
19
  *
@@ -15,7 +15,8 @@ const postgres_1 = require("../connector/providers/postgres");
15
15
  * |---|---|
16
16
  * | `{appId}.jobs` | Completed/expired jobs with `expired_at` set |
17
17
  * | `{appId}.jobs_attributes` | Execution artifacts (`adata`, `hmark`, `status`, `other`) that are only needed during workflow execution |
18
- * | `{appId}.streams` | Processed stream messages with `expired_at` set |
18
+ * | `{appId}.engine_streams` | Processed engine stream messages with `expired_at` set |
19
+ * | `{appId}.worker_streams` | Processed worker stream messages with `expired_at` set |
19
20
  *
20
21
  * The `DBA` service addresses this with two methods:
21
22
  *
@@ -188,6 +189,7 @@ class DBA {
188
189
  v_stripped_attributes BIGINT := 0;
189
190
  v_deleted_transient BIGINT := 0;
190
191
  v_marked_pruned BIGINT := 0;
192
+ v_temp_count BIGINT := 0;
191
193
  BEGIN
192
194
  -- 1. Hard-delete expired jobs older than the retention window.
193
195
  -- FK CASCADE on jobs_attributes handles attribute cleanup.
@@ -210,11 +212,18 @@ class DBA {
210
212
  END IF;
211
213
 
212
214
  -- 3. Hard-delete expired stream messages older than the retention window.
215
+ -- Deletes from both engine_streams and worker_streams tables.
213
216
  IF prune_streams THEN
214
- DELETE FROM ${schema}.streams
217
+ DELETE FROM ${schema}.engine_streams
215
218
  WHERE expired_at IS NOT NULL
216
219
  AND expired_at < NOW() - retention;
217
220
  GET DIAGNOSTICS v_deleted_streams = ROW_COUNT;
221
+
222
+ DELETE FROM ${schema}.worker_streams
223
+ WHERE expired_at IS NOT NULL
224
+ AND expired_at < NOW() - retention;
225
+ GET DIAGNOSTICS v_temp_count = ROW_COUNT;
226
+ v_deleted_streams := v_deleted_streams + v_temp_count;
218
227
  END IF;
219
228
 
220
229
  -- 4. Strip execution artifacts from completed, live, un-pruned jobs.
@@ -145,6 +145,8 @@ class ClientService {
145
145
  parentWorkflowId: options.parentWorkflowId,
146
146
  workflowId: options.workflowId || hotmesh_1.HotMesh.guid(),
147
147
  workflowTopic: workflowTopic,
148
+ taskQueue: taskQueueName,
149
+ workflowName: workflowName,
148
150
  backoffCoefficient: options.config?.backoffCoefficient || enums_1.HMSH_DURABLE_EXP_BACKOFF,
149
151
  maximumAttempts: options.config?.maximumAttempts || enums_1.HMSH_DURABLE_MAX_ATTEMPTS,
150
152
  maximumInterval: (0, utils_1.s)(options.config?.maximumInterval || enums_1.HMSH_DURABLE_MAX_INTERVAL),
@@ -186,11 +188,14 @@ class ClientService {
186
188
  */
187
189
  hook: async (options) => {
188
190
  const taskQueue = options.taskQueue ?? options.entity;
189
- const workflowTopic = `${taskQueue}-${options.entity ?? options.workflowName}`;
191
+ const hookWorkflowName = options.entity ?? options.workflowName;
192
+ const workflowTopic = `${taskQueue}-${hookWorkflowName}`;
190
193
  const payload = {
191
194
  arguments: [...options.args],
192
195
  id: options.workflowId,
193
196
  workflowTopic,
197
+ taskQueue,
198
+ workflowName: hookWorkflowName,
194
199
  backoffCoefficient: options.config?.backoffCoefficient || enums_1.HMSH_DURABLE_EXP_BACKOFF,
195
200
  maximumAttempts: options.config?.maximumAttempts || enums_1.HMSH_DURABLE_MAX_ATTEMPTS,
196
201
  maximumInterval: (0, utils_1.s)(options.config?.maximumInterval || enums_1.HMSH_DURABLE_MAX_INTERVAL),
@@ -63,6 +63,21 @@ declare class ExporterService {
63
63
  * their executions as nested `children`.
64
64
  */
65
65
  exportExecution(jobId: string, workflowTopic: string, options?: ExecutionExportOptions): Promise<WorkflowExecution>;
66
+ /**
67
+ * Reconstruct a WorkflowExecution from raw database rows when the job
68
+ * handle has expired or been pruned. Only available if the store provider
69
+ * implements getJobByKeyDirect.
70
+ */
71
+ private exportExecutionDirect;
72
+ /**
73
+ * Enrich execution events with activity and child workflow inputs.
74
+ * Queries the store for activity arguments and child workflow arguments.
75
+ */
76
+ private enrichExecutionInputs;
77
+ /**
78
+ * Resolve a symbol field from stable JSON path using the symbol registry.
79
+ */
80
+ private resolveSymbolField;
66
81
  /**
67
82
  * Pure transformation: convert a raw DurableJobExport into a
68
83
  * Temporal-compatible WorkflowExecution event history.
@@ -182,14 +182,352 @@ class ExporterService {
182
182
  * their executions as nested `children`.
183
183
  */
184
184
  async exportExecution(jobId, workflowTopic, options = {}) {
185
- const raw = await this.export(jobId);
186
- const execution = this.transformToExecution(raw, jobId, workflowTopic, options);
187
- if (options.mode === 'verbose') {
188
- const maxDepth = options.max_depth ?? 5;
189
- execution.children = await this.fetchChildren(raw, workflowTopic, options, 1, maxDepth);
185
+ let execution;
186
+ try {
187
+ const raw = await this.export(jobId);
188
+ execution = this.transformToExecution(raw, jobId, workflowTopic, options);
189
+ if (options.mode === 'verbose') {
190
+ const maxDepth = options.max_depth ?? 5;
191
+ execution.children = await this.fetchChildren(raw, workflowTopic, options, 1, maxDepth);
192
+ }
193
+ }
194
+ catch (error) {
195
+ // Fallback to direct query for expired/pruned jobs
196
+ if (options.allow_direct_query && this.store.getJobByKeyDirect) {
197
+ this.logger.debug('Job export failed, attempting direct query', { jobId, error: error.message });
198
+ execution = await this.exportExecutionDirect(jobId, workflowTopic, options);
199
+ }
200
+ else {
201
+ throw error;
202
+ }
203
+ }
204
+ // Enrich with activity/child workflow inputs if requested
205
+ if (options.enrich_inputs) {
206
+ await this.enrichExecutionInputs(execution, jobId);
190
207
  }
191
208
  return execution;
192
209
  }
210
+ /**
211
+ * Reconstruct a WorkflowExecution from raw database rows when the job
212
+ * handle has expired or been pruned. Only available if the store provider
213
+ * implements getJobByKeyDirect.
214
+ */
215
+ async exportExecutionDirect(workflowId, workflowTopic, options) {
216
+ if (!this.store.getJobByKeyDirect) {
217
+ throw new Error('Direct query not supported by this store provider');
218
+ }
219
+ const jobKey = `hmsh:${this.appId}:j:${workflowId}`;
220
+ const { job, attributes } = await this.store.getJobByKeyDirect(jobKey);
221
+ // Parse metadata for timing
222
+ const startTime = attributes['aoa'] ? parseTimestamp(attributes['aoa']) : job.created_at?.toISOString();
223
+ const closeTime = attributes['apa'] ? parseTimestamp(attributes['apa']) : job.updated_at?.toISOString();
224
+ // Parse workflow result
225
+ let workflowResult;
226
+ if (attributes['aBa']) {
227
+ const raw = attributes['aBa'].startsWith('/s') ? attributes['aBa'].slice(2) : attributes['aBa'];
228
+ try {
229
+ workflowResult = JSON.parse(raw);
230
+ }
231
+ catch { /* ignore */ }
232
+ }
233
+ // Build events from timeline operations
234
+ const events = [];
235
+ let nextId = 1;
236
+ // Helper to extract operation entries from attributes
237
+ const getOperationKeys = (prefix) => {
238
+ return Object.keys(attributes)
239
+ .filter((k) => k.startsWith(prefix))
240
+ .sort((a, b) => {
241
+ const numA = parseInt(a.replace(new RegExp(`${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|-`, 'g'), ''));
242
+ const numB = parseInt(b.replace(new RegExp(`${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|-`, 'g'), ''));
243
+ return numA - numB;
244
+ })
245
+ .map((key) => {
246
+ const raw = attributes[key].startsWith('/s') ? attributes[key].slice(2) : attributes[key];
247
+ try {
248
+ return { key, index: parseInt(key.replace(/[^0-9]/g, '')), val: JSON.parse(raw) };
249
+ }
250
+ catch {
251
+ return null;
252
+ }
253
+ })
254
+ .filter(Boolean);
255
+ };
256
+ let systemCount = 0;
257
+ let userCount = 0;
258
+ let activityCompleted = 0;
259
+ let activityFailed = 0;
260
+ let childTotal = 0;
261
+ let childCompleted = 0;
262
+ let childFailed = 0;
263
+ let timerCount = 0;
264
+ let signalCount = 0;
265
+ // Process proxy (activities)
266
+ for (const { key, index, val } of getOperationKeys('-proxy-')) {
267
+ const activityName = extractActivityName(val);
268
+ const isSystem = isSystemActivity(activityName);
269
+ const ac = val.ac;
270
+ const au = val.au;
271
+ const dur = computeDuration(ac, au);
272
+ const hasError = '$error' in val;
273
+ if (isSystem)
274
+ systemCount++;
275
+ else
276
+ userCount++;
277
+ if (options.exclude_system && isSystem)
278
+ continue;
279
+ if (ac) {
280
+ events.push(makeEvent(nextId++, 'activity_task_scheduled', 'activity', parseTimestamp(ac), null, isSystem, {
281
+ kind: 'activity_task_scheduled',
282
+ activity_type: activityName,
283
+ timeline_key: val.job_id || key,
284
+ execution_index: index,
285
+ }));
286
+ }
287
+ if (au) {
288
+ if (hasError) {
289
+ activityFailed++;
290
+ events.push(makeEvent(nextId++, 'activity_task_failed', 'activity', parseTimestamp(au), dur, isSystem, {
291
+ kind: 'activity_task_failed',
292
+ activity_type: activityName,
293
+ failure: val.$error,
294
+ timeline_key: val.job_id || key,
295
+ execution_index: index,
296
+ }));
297
+ }
298
+ else {
299
+ activityCompleted++;
300
+ events.push(makeEvent(nextId++, 'activity_task_completed', 'activity', parseTimestamp(au), dur, isSystem, {
301
+ kind: 'activity_task_completed',
302
+ activity_type: activityName,
303
+ result: options.omit_results ? undefined : val.data,
304
+ timeline_key: val.job_id || key,
305
+ execution_index: index,
306
+ }));
307
+ }
308
+ }
309
+ }
310
+ // Process wait (signals)
311
+ for (const { key, index, val } of getOperationKeys('-wait-')) {
312
+ const signalName = val.id || val.data?.id || val.data?.data?.id || `signal-${index}`;
313
+ const ac = val.ac;
314
+ const au = val.au;
315
+ const dur = computeDuration(ac, au);
316
+ signalCount++;
317
+ const ts = au ? parseTimestamp(au) : ac ? parseTimestamp(ac) : startTime;
318
+ events.push(makeEvent(nextId++, 'workflow_execution_signaled', 'signal', ts, dur, false, {
319
+ kind: 'workflow_execution_signaled',
320
+ signal_name: signalName,
321
+ input: options.omit_results ? undefined : val.data?.data,
322
+ timeline_key: val.job_id || key,
323
+ execution_index: index,
324
+ }));
325
+ }
326
+ // Process sleep (timers)
327
+ for (const { key, index, val } of getOperationKeys('-sleep-')) {
328
+ const ac = val.ac;
329
+ const au = val.au;
330
+ const dur = computeDuration(ac, au);
331
+ timerCount++;
332
+ if (ac) {
333
+ events.push(makeEvent(nextId++, 'timer_started', 'timer', parseTimestamp(ac), null, false, {
334
+ kind: 'timer_started',
335
+ duration_ms: dur ?? undefined,
336
+ timeline_key: val.job_id || key,
337
+ execution_index: index,
338
+ }));
339
+ }
340
+ if (au) {
341
+ events.push(makeEvent(nextId++, 'timer_fired', 'timer', parseTimestamp(au), dur, false, {
342
+ kind: 'timer_fired',
343
+ timeline_key: val.job_id || key,
344
+ execution_index: index,
345
+ }));
346
+ }
347
+ }
348
+ // Process child (awaited child workflows)
349
+ for (const { key, index, val } of getOperationKeys('-child-')) {
350
+ const childId = val.job_id || key;
351
+ const ac = val.ac;
352
+ const au = val.au;
353
+ const dur = computeDuration(ac, au);
354
+ const hasError = '$error' in val;
355
+ childTotal++;
356
+ if (ac) {
357
+ events.push(makeEvent(nextId++, 'child_workflow_execution_started', 'child_workflow', parseTimestamp(ac), null, false, {
358
+ kind: 'child_workflow_execution_started',
359
+ child_workflow_id: childId,
360
+ awaited: true,
361
+ timeline_key: childId,
362
+ execution_index: index,
363
+ }));
364
+ }
365
+ if (au) {
366
+ if (hasError) {
367
+ childFailed++;
368
+ events.push(makeEvent(nextId++, 'child_workflow_execution_failed', 'child_workflow', parseTimestamp(au), dur, false, {
369
+ kind: 'child_workflow_execution_failed',
370
+ child_workflow_id: childId,
371
+ failure: val.$error,
372
+ timeline_key: childId,
373
+ execution_index: index,
374
+ }));
375
+ }
376
+ else {
377
+ childCompleted++;
378
+ events.push(makeEvent(nextId++, 'child_workflow_execution_completed', 'child_workflow', parseTimestamp(au), dur, false, {
379
+ kind: 'child_workflow_execution_completed',
380
+ child_workflow_id: childId,
381
+ result: options.omit_results ? undefined : val.data,
382
+ timeline_key: childId,
383
+ execution_index: index,
384
+ }));
385
+ }
386
+ }
387
+ }
388
+ // Process start (fire-and-forget child workflows)
389
+ for (const { key, index, val } of getOperationKeys('-start-')) {
390
+ const childId = val.job_id || key;
391
+ const ac = val.ac;
392
+ const au = val.au;
393
+ const ts = ac ? parseTimestamp(ac) : au ? parseTimestamp(au) : startTime;
394
+ childTotal++;
395
+ events.push(makeEvent(nextId++, 'child_workflow_execution_started', 'child_workflow', ts, null, false, {
396
+ kind: 'child_workflow_execution_started',
397
+ child_workflow_id: childId,
398
+ awaited: false,
399
+ timeline_key: childId,
400
+ execution_index: index,
401
+ }));
402
+ }
403
+ // Sort chronologically and re-number
404
+ events.sort((a, b) => {
405
+ const cmp = a.event_time.localeCompare(b.event_time);
406
+ return cmp !== 0 ? cmp : a.event_id - b.event_id;
407
+ });
408
+ for (let i = 0; i < events.length; i++) {
409
+ events[i].event_id = i + 1;
410
+ }
411
+ // Back-references
412
+ const scheduledMap = new Map();
413
+ const initiatedMap = new Map();
414
+ for (const e of events) {
415
+ const attrs = e.attributes;
416
+ if (e.event_type === 'activity_task_scheduled' && attrs.timeline_key) {
417
+ scheduledMap.set(attrs.timeline_key, e.event_id);
418
+ }
419
+ if (e.event_type === 'child_workflow_execution_started' && attrs.timeline_key) {
420
+ initiatedMap.set(attrs.timeline_key, e.event_id);
421
+ }
422
+ if ((e.event_type === 'activity_task_completed' || e.event_type === 'activity_task_failed') && attrs.timeline_key) {
423
+ attrs.scheduled_event_id = scheduledMap.get(attrs.timeline_key) ?? null;
424
+ }
425
+ if ((e.event_type === 'child_workflow_execution_completed' || e.event_type === 'child_workflow_execution_failed') && attrs.timeline_key) {
426
+ attrs.initiated_event_id = initiatedMap.get(attrs.timeline_key) ?? null;
427
+ }
428
+ }
429
+ // Compute total duration
430
+ let totalDurationMs = null;
431
+ if (startTime && closeTime) {
432
+ const diffMs = new Date(closeTime).getTime() - new Date(startTime).getTime();
433
+ if (diffMs >= 0)
434
+ totalDurationMs = diffMs;
435
+ }
436
+ const proxyTotal = systemCount + userCount;
437
+ return {
438
+ workflow_id: workflowId,
439
+ workflow_type: workflowTopic,
440
+ task_queue: workflowTopic,
441
+ status: 'completed',
442
+ start_time: startTime || null,
443
+ close_time: closeTime || null,
444
+ duration_ms: totalDurationMs,
445
+ result: workflowResult,
446
+ events,
447
+ summary: {
448
+ total_events: events.length,
449
+ activities: {
450
+ total: proxyTotal,
451
+ completed: activityCompleted,
452
+ failed: activityFailed,
453
+ system: systemCount,
454
+ user: userCount,
455
+ },
456
+ child_workflows: { total: childTotal, completed: childCompleted, failed: childFailed },
457
+ timers: timerCount,
458
+ signals: signalCount,
459
+ },
460
+ };
461
+ }
462
+ /**
463
+ * Enrich execution events with activity and child workflow inputs.
464
+ * Queries the store for activity arguments and child workflow arguments.
465
+ */
466
+ async enrichExecutionInputs(execution, workflowId) {
467
+ // Check if store supports exporter queries
468
+ if (!this.store.getActivityInputs || !this.store.getChildWorkflowInputs) {
469
+ this.logger.warn('Store does not support input enrichment (provider may not implement getActivityInputs/getChildWorkflowInputs)');
470
+ return;
471
+ }
472
+ // Resolve symbol fields for activity and workflow arguments using symbol keys
473
+ const symbolSets = await this.store.getSymbolKeys(['activity_trigger', 'trigger']);
474
+ const activityArgsField = this.resolveSymbolField(symbolSets, 'activity_trigger', 'activity_trigger/output/data/arguments');
475
+ const workflowArgsField = this.resolveSymbolField(symbolSets, 'trigger', 'trigger/output/data/arguments');
476
+ // ── 1. Enrich activity inputs ──
477
+ if (activityArgsField) {
478
+ const activityEvents = execution.events.filter((e) => e.event_type === 'activity_task_scheduled' || e.event_type === 'activity_task_completed' || e.event_type === 'activity_task_failed');
479
+ if (activityEvents.length > 0) {
480
+ const { byJobId, byNameIndex } = await this.store.getActivityInputs(workflowId, activityArgsField);
481
+ for (const evt of activityEvents) {
482
+ const attrs = evt.attributes;
483
+ let input = attrs.timeline_key ? byJobId.get(attrs.timeline_key) : undefined;
484
+ if (input === undefined && attrs.activity_type && attrs.execution_index !== undefined) {
485
+ input = byNameIndex.get(`${attrs.activity_type}:${attrs.execution_index}`);
486
+ }
487
+ if (input !== undefined) {
488
+ attrs.input = input;
489
+ }
490
+ }
491
+ }
492
+ }
493
+ // ── 2. Enrich child workflow inputs ──
494
+ if (workflowArgsField) {
495
+ const childEvents = execution.events.filter((e) => e.event_type === 'child_workflow_execution_started');
496
+ if (childEvents.length > 0) {
497
+ const childIds = [...new Set(childEvents
498
+ .map((e) => e.attributes.child_workflow_id)
499
+ .filter(Boolean))];
500
+ if (childIds.length > 0) {
501
+ const childJobKeys = childIds.map((id) => `hmsh:${this.appId}:j:${id}`);
502
+ const childInputMap = await this.store.getChildWorkflowInputs(childJobKeys, workflowArgsField);
503
+ for (const evt of childEvents) {
504
+ const attrs = evt.attributes;
505
+ const input = childInputMap.get(attrs.child_workflow_id);
506
+ if (input !== undefined) {
507
+ attrs.input = input;
508
+ }
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+ /**
515
+ * Resolve a symbol field from stable JSON path using the symbol registry.
516
+ */
517
+ resolveSymbolField(symbolSets, range, path) {
518
+ // Get the symbol map for this range
519
+ const symbolMap = symbolSets[range];
520
+ if (!symbolMap) {
521
+ return null;
522
+ }
523
+ // Look up the symbol code for this path
524
+ const symbolCode = symbolMap[path];
525
+ if (!symbolCode) {
526
+ return null;
527
+ }
528
+ // Return with dimension suffix
529
+ return `${symbolCode},0`;
530
+ }
193
531
  /**
194
532
  * Pure transformation: convert a raw DurableJobExport into a
195
533
  * Temporal-compatible WorkflowExecution event history.
@@ -17,7 +17,7 @@
17
17
  * * Service Meshes
18
18
  * * Master Data Management systems
19
19
  */
20
- declare const APP_VERSION = "5";
20
+ declare const APP_VERSION = "8";
21
21
  declare const APP_ID = "durable";
22
22
  /**
23
23
  * returns a new durable workflow schema
@@ -20,7 +20,7 @@
20
20
  */
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
22
  exports.APP_ID = exports.APP_VERSION = exports.getWorkflowYAML = void 0;
23
- const APP_VERSION = '5';
23
+ const APP_VERSION = '8';
24
24
  exports.APP_VERSION = APP_VERSION;
25
25
  const APP_ID = 'durable';
26
26
  exports.APP_ID = APP_ID;
@@ -66,7 +66,13 @@ const getWorkflowYAML = (app, version) => {
66
66
  description: the arguments to pass to the flow
67
67
  type: array
68
68
  workflowTopic:
69
- description: the stream topic the worker is listening on
69
+ description: concatenated taskQueue-workflowName for engine-internal routing
70
+ type: string
71
+ taskQueue:
72
+ description: the task queue name (stream_name in worker_streams)
73
+ type: string
74
+ workflowName:
75
+ description: the workflow function name (workflow_name in worker_streams)
70
76
  type: string
71
77
  backoffCoefficient:
72
78
  description: the time multiple in seconds to backoff before retrying
@@ -155,7 +161,7 @@ const getWorkflowYAML = (app, version) => {
155
161
  worker:
156
162
  title: Main Worker - Calls linked Workflow functions
157
163
  type: worker
158
- topic: '{trigger.output.data.workflowTopic}'
164
+ topic: '{trigger.output.data.taskQueue}'
159
165
  emit: '{$job.data.done}'
160
166
  input:
161
167
  schema:
@@ -169,6 +175,8 @@ const getWorkflowYAML = (app, version) => {
169
175
  type: array
170
176
  workflowTopic:
171
177
  type: string
178
+ workflowName:
179
+ type: string
172
180
  canRetry:
173
181
  type: boolean
174
182
  expire:
@@ -178,6 +186,7 @@ const getWorkflowYAML = (app, version) => {
178
186
  workflowId: '{trigger.output.data.workflowId}'
179
187
  arguments: '{trigger.output.data.arguments}'
180
188
  workflowTopic: '{trigger.output.data.workflowTopic}'
189
+ workflowName: '{trigger.output.data.workflowName}'
181
190
  expire: '{trigger.output.data.expire}'
182
191
  canRetry:
183
192
  '@pipe':
@@ -348,6 +357,10 @@ const getWorkflowYAML = (app, version) => {
348
357
  properties:
349
358
  workflowTopic:
350
359
  type: string
360
+ taskQueue:
361
+ type: string
362
+ workflowName:
363
+ type: string
351
364
  backoffCoefficient:
352
365
  type: number
353
366
  maximumAttempts:
@@ -387,6 +400,7 @@ const getWorkflowYAML = (app, version) => {
387
400
  persistent: '{worker.output.data.persistent}'
388
401
  signalIn: '{worker.output.data.signalIn}'
389
402
  workflowId: '{worker.output.data.workflowId}'
403
+ taskQueue: '{worker.output.data.taskQueue}'
390
404
  workflowName: '{worker.output.data.workflowName}'
391
405
  workflowTopic: '{worker.output.data.workflowTopic}'
392
406
  entity: '{worker.output.data.entity}'
@@ -893,7 +907,7 @@ const getWorkflowYAML = (app, version) => {
893
907
  signaler_worker:
894
908
  title: Signal In - Worker
895
909
  type: worker
896
- topic: '{signaler.hook.data.workflowTopic}'
910
+ topic: '{signaler.hook.data.taskQueue}'
897
911
  input:
898
912
  schema:
899
913
  type: object
@@ -906,6 +920,8 @@ const getWorkflowYAML = (app, version) => {
906
920
  type: string
907
921
  arguments:
908
922
  type: array
923
+ workflowName:
924
+ type: string
909
925
  canRetry:
910
926
  type: boolean
911
927
  expire:
@@ -915,6 +931,7 @@ const getWorkflowYAML = (app, version) => {
915
931
  originJobId: '{trigger.output.data.originJobId}'
916
932
  workflowDimension: '{signaler.output.metadata.dad}'
917
933
  arguments: '{signaler.hook.data.arguments}'
934
+ workflowName: '{signaler.hook.data.workflowName}'
918
935
  expire: '{trigger.output.data.expire}'
919
936
  canRetry:
920
937
  '@pipe':
@@ -1136,6 +1153,7 @@ const getWorkflowYAML = (app, version) => {
1136
1153
  persistent: '{signaler_worker.output.data.persistent}'
1137
1154
  signalIn: '{signaler_worker.output.data.signalIn}'
1138
1155
  workflowId: '{signaler_worker.output.data.workflowId}'
1156
+ taskQueue: '{signaler_worker.output.data.taskQueue}'
1139
1157
  workflowName: '{signaler_worker.output.data.workflowName}'
1140
1158
  workflowTopic: '{signaler_worker.output.data.workflowTopic}'
1141
1159
  entity: '{signaler_worker.output.data.entity}'
@@ -1942,6 +1960,11 @@ const getWorkflowYAML = (app, version) => {
1942
1960
  - ['{collator_trigger.output.data.items}', '{collator_cycle_hook.output.data.cur_index}']
1943
1961
  - ['{@array.get}', workflowId]
1944
1962
  - ['{@object.get}']
1963
+ taskQueue:
1964
+ '@pipe':
1965
+ - ['{collator_trigger.output.data.items}', '{collator_cycle_hook.output.data.cur_index}']
1966
+ - ['{@array.get}', taskQueue]
1967
+ - ['{@object.get}']
1945
1968
  workflowName:
1946
1969
  '@pipe':
1947
1970
  - ['{collator_trigger.output.data.items}', '{collator_cycle_hook.output.data.cur_index}']