@hotmeshio/hotmesh 0.10.2 → 0.12.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 (59) hide show
  1. package/README.md +1 -1
  2. package/build/modules/enums.d.ts +1 -0
  3. package/build/modules/enums.js +3 -1
  4. package/build/modules/errors.d.ts +2 -0
  5. package/build/modules/errors.js +2 -0
  6. package/build/modules/key.js +3 -2
  7. package/build/package.json +2 -2
  8. package/build/services/activities/worker.js +10 -0
  9. package/build/services/dba/index.d.ts +2 -1
  10. package/build/services/dba/index.js +11 -2
  11. package/build/services/durable/client.js +6 -1
  12. package/build/services/durable/exporter.d.ts +15 -0
  13. package/build/services/durable/exporter.js +384 -5
  14. package/build/services/durable/schemas/factory.d.ts +1 -1
  15. package/build/services/durable/schemas/factory.js +27 -4
  16. package/build/services/durable/worker.d.ts +2 -2
  17. package/build/services/durable/worker.js +15 -9
  18. package/build/services/durable/workflow/context.js +2 -0
  19. package/build/services/durable/workflow/execChild.js +5 -2
  20. package/build/services/durable/workflow/hook.js +6 -0
  21. package/build/services/durable/workflow/proxyActivities.js +3 -4
  22. package/build/services/engine/index.d.ts +2 -2
  23. package/build/services/engine/index.js +10 -5
  24. package/build/services/exporter/index.d.ts +16 -2
  25. package/build/services/exporter/index.js +76 -0
  26. package/build/services/hotmesh/index.d.ts +2 -2
  27. package/build/services/hotmesh/index.js +2 -2
  28. package/build/services/router/config/index.d.ts +2 -2
  29. package/build/services/router/config/index.js +2 -1
  30. package/build/services/router/consumption/index.js +80 -5
  31. package/build/services/store/index.d.ts +52 -0
  32. package/build/services/store/providers/postgres/exporter-sql.d.ts +40 -0
  33. package/build/services/store/providers/postgres/exporter-sql.js +92 -0
  34. package/build/services/store/providers/postgres/kvtables.js +6 -0
  35. package/build/services/store/providers/postgres/postgres.d.ts +42 -0
  36. package/build/services/store/providers/postgres/postgres.js +151 -0
  37. package/build/services/stream/index.d.ts +1 -0
  38. package/build/services/stream/providers/postgres/kvtables.d.ts +1 -1
  39. package/build/services/stream/providers/postgres/kvtables.js +235 -82
  40. package/build/services/stream/providers/postgres/lifecycle.d.ts +4 -3
  41. package/build/services/stream/providers/postgres/lifecycle.js +6 -5
  42. package/build/services/stream/providers/postgres/messages.d.ts +14 -6
  43. package/build/services/stream/providers/postgres/messages.js +153 -76
  44. package/build/services/stream/providers/postgres/notifications.d.ts +5 -2
  45. package/build/services/stream/providers/postgres/notifications.js +39 -35
  46. package/build/services/stream/providers/postgres/postgres.d.ts +21 -118
  47. package/build/services/stream/providers/postgres/postgres.js +87 -140
  48. package/build/services/stream/providers/postgres/scout.js +2 -2
  49. package/build/services/stream/providers/postgres/stats.js +3 -2
  50. package/build/services/stream/registry.d.ts +62 -0
  51. package/build/services/stream/registry.js +198 -0
  52. package/build/services/worker/index.js +20 -6
  53. package/build/types/durable.d.ts +6 -1
  54. package/build/types/error.d.ts +2 -0
  55. package/build/types/exporter.d.ts +84 -0
  56. package/build/types/hotmesh.d.ts +7 -1
  57. package/build/types/index.d.ts +1 -1
  58. package/build/types/stream.d.ts +2 -0
  59. 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
 
@@ -78,6 +78,7 @@ export declare const INITIAL_STREAM_BACKOFF: number;
78
78
  export declare const MAX_STREAM_RETRIES: number;
79
79
  export declare const MAX_DELAY = 2147483647;
80
80
  export declare const HMSH_MAX_RETRIES: number;
81
+ export declare const HMSH_POISON_MESSAGE_THRESHOLD: number;
81
82
  export declare const HMSH_MAX_TIMEOUT_MS: number;
82
83
  export declare const HMSH_GRADUATED_INTERVAL_MS: number;
83
84
  /**
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HMSH_ROUTER_POLL_FALLBACK_INTERVAL = exports.HMSH_NOTIFY_PAYLOAD_LIMIT = exports.DEFAULT_TASK_QUEUE = exports.HMSH_GUID_SIZE = exports.HMSH_ROUTER_SCOUT_INTERVAL_MS = exports.HMSH_ROUTER_SCOUT_INTERVAL_SECONDS = exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_DURABLE_EXP_BACKOFF = exports.HMSH_DURABLE_MAX_INTERVAL = exports.HMSH_DURABLE_MAX_ATTEMPTS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.MAX_DELAY = exports.MAX_STREAM_RETRIES = exports.INITIAL_STREAM_BACKOFF = exports.MAX_STREAM_BACKOFF = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_DEPLOYMENT_PAUSE = exports.HMSH_DEPLOYMENT_DELAY = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_QUORUM_ROLLCALL_CYCLES = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAIT = exports.HMSH_CODE_DURABLE_PROXY = exports.HMSH_CODE_DURABLE_CHILD = exports.HMSH_CODE_DURABLE_ALL = exports.HMSH_CODE_DURABLE_SLEEP = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_SIGNAL_EXPIRE = exports.HMSH_TELEMETRY = exports.HMSH_LOGLEVEL = void 0;
3
+ exports.HMSH_NOTIFY_PAYLOAD_LIMIT = exports.DEFAULT_TASK_QUEUE = exports.HMSH_GUID_SIZE = exports.HMSH_ROUTER_SCOUT_INTERVAL_MS = exports.HMSH_ROUTER_SCOUT_INTERVAL_SECONDS = exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_DURABLE_EXP_BACKOFF = exports.HMSH_DURABLE_MAX_INTERVAL = exports.HMSH_DURABLE_MAX_ATTEMPTS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_POISON_MESSAGE_THRESHOLD = exports.HMSH_MAX_RETRIES = exports.MAX_DELAY = exports.MAX_STREAM_RETRIES = exports.INITIAL_STREAM_BACKOFF = exports.MAX_STREAM_BACKOFF = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_DEPLOYMENT_PAUSE = exports.HMSH_DEPLOYMENT_DELAY = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_QUORUM_ROLLCALL_CYCLES = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAIT = exports.HMSH_CODE_DURABLE_PROXY = exports.HMSH_CODE_DURABLE_CHILD = exports.HMSH_CODE_DURABLE_ALL = exports.HMSH_CODE_DURABLE_SLEEP = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_SIGNAL_EXPIRE = exports.HMSH_TELEMETRY = exports.HMSH_LOGLEVEL = void 0;
4
+ exports.HMSH_ROUTER_POLL_FALLBACK_INTERVAL = void 0;
4
5
  /**
5
6
  * Determines the log level for the application. The default is 'info'.
6
7
  */
@@ -87,6 +88,7 @@ exports.INITIAL_STREAM_BACKOFF = parseInt(process.env.INITIAL_STREAM_BACKOFF, 10
87
88
  exports.MAX_STREAM_RETRIES = parseInt(process.env.MAX_STREAM_RETRIES, 10) || 2;
88
89
  exports.MAX_DELAY = 2147483647; // Maximum allowed delay in milliseconds for setTimeout
89
90
  exports.HMSH_MAX_RETRIES = parseInt(process.env.HMSH_MAX_RETRIES, 10) || 3;
91
+ exports.HMSH_POISON_MESSAGE_THRESHOLD = parseInt(process.env.HMSH_POISON_MESSAGE_THRESHOLD, 10) || 5;
90
92
  exports.HMSH_MAX_TIMEOUT_MS = parseInt(process.env.HMSH_MAX_TIMEOUT_MS, 10) || 60000;
91
93
  exports.HMSH_GRADUATED_INTERVAL_MS = parseInt(process.env.HMSH_GRADUATED_INTERVAL_MS, 10) || 5000;
92
94
  // DURABLE
@@ -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.12.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,393 @@ 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
+ // ── 3. Stream-based fallback for unenriched activity events ──
514
+ // When job attributes have been pruned, recover inputs from worker_streams
515
+ if (this.store.getStreamHistory) {
516
+ const unenrichedEvents = execution.events.filter((e) => (e.event_type === 'activity_task_scheduled' ||
517
+ e.event_type === 'activity_task_completed' ||
518
+ e.event_type === 'activity_task_failed') &&
519
+ e.attributes.input === undefined);
520
+ if (unenrichedEvents.length > 0) {
521
+ const streamHistory = await this.store.getStreamHistory(workflowId, {
522
+ types: ['worker'],
523
+ });
524
+ // Build a map of aid -> stream message data (the worker invocation inputs)
525
+ const streamInputsByAid = new Map();
526
+ for (const entry of streamHistory) {
527
+ if (entry.msg_type === 'worker' && entry.data) {
528
+ const key = `${entry.aid}:${entry.dad || ''}`;
529
+ if (!streamInputsByAid.has(key)) {
530
+ streamInputsByAid.set(key, entry.data);
531
+ }
532
+ }
533
+ }
534
+ for (const evt of unenrichedEvents) {
535
+ const attrs = evt.attributes;
536
+ // Try matching by activity_type + dimensional address
537
+ const key = `${attrs.activity_type}:${attrs.timeline_key || ''}`;
538
+ let input = streamInputsByAid.get(key);
539
+ if (input === undefined) {
540
+ // Fallback: match by activity name alone (first occurrence)
541
+ for (const [k, v] of streamInputsByAid) {
542
+ if (k.startsWith(`${attrs.activity_type}:`)) {
543
+ input = v;
544
+ break;
545
+ }
546
+ }
547
+ }
548
+ if (input !== undefined) {
549
+ attrs.input = input;
550
+ }
551
+ }
552
+ }
553
+ }
554
+ }
555
+ /**
556
+ * Resolve a symbol field from stable JSON path using the symbol registry.
557
+ */
558
+ resolveSymbolField(symbolSets, range, path) {
559
+ // Get the symbol map for this range
560
+ const symbolMap = symbolSets[range];
561
+ if (!symbolMap) {
562
+ return null;
563
+ }
564
+ // Look up the symbol code for this path
565
+ const symbolCode = symbolMap[path];
566
+ if (!symbolCode) {
567
+ return null;
568
+ }
569
+ // Return with dimension suffix
570
+ return `${symbolCode},0`;
571
+ }
193
572
  /**
194
573
  * Pure transformation: convert a raw DurableJobExport into a
195
574
  * 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