@hotmeshio/hotmesh 0.22.8 → 0.24.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.22.8",
3
+ "version": "0.24.1",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -285,6 +285,14 @@ class ExporterService {
285
285
  if (options.enrich_inputs) {
286
286
  await this.enrichExecutionInputs(execution, jobId);
287
287
  }
288
+ // Opt-in upward lineage pointer — one indexed lookup; surfaces the real
289
+ // spawning parent (not the collator $C job) and the root ancestor.
290
+ if (options.include_lineage && this.store.getJobLineage) {
291
+ const jobKey = `hmsh:${this.appId}:j:${jobId}`;
292
+ const lineage = await this.store.getJobLineage(jobKey);
293
+ execution.parent_workflow_id = lineage?.parent_id ?? null;
294
+ execution.origin_id = lineage?.origin_id ?? null;
295
+ }
288
296
  return execution;
289
297
  }
290
298
  /**
@@ -1,4 +1,4 @@
1
- declare const APP_VERSION = "16";
1
+ declare const APP_VERSION = "17";
2
2
  declare const APP_ID = "durable";
3
3
  /**
4
4
  * returns a new durable workflow schema
@@ -6,8 +6,9 @@ exports.APP_ID = exports.APP_VERSION = exports.getWorkflowYAML = void 0;
6
6
  // schema upgrade and hot-swap to it (see WorkerService.activateWorkflow and
7
7
  // ClientService.deployAndActivate). Changing the YAML without a bump leaves
8
8
  // every already-deployed database on the old schema forever. Numeric string —
9
- // compared with `Number()` for ordering. (v16: condition() escalation hook.)
10
- const APP_VERSION = '16';
9
+ // compared with `Number()` for ordering. (v17: collator_waiter escalation block —
10
+ // escalation-bearing condition() inside Promise.all writes its hmsh_escalations row.)
11
+ const APP_VERSION = '17';
11
12
  exports.APP_VERSION = APP_VERSION;
12
13
  const APP_ID = 'durable';
13
14
  exports.APP_ID = APP_ID;
@@ -1944,6 +1945,49 @@ const getWorkflowYAML = (app, version) => {
1944
1945
  - ['{collator_trigger.output.data.items}', '{collator_cycle_hook.output.data.cur_index}']
1945
1946
  - ['{@array.get}', duration]
1946
1947
  - ['{@object.get}']
1948
+ # Flatten the CURRENT item's escalation config into this activity's own
1949
+ # output so the escalation block below can address it with flat symbols.
1950
+ # mapOutputData runs before the escalation INSERT in Leg1, so these are
1951
+ # resolved in time. For a plain (non-escalation) collated condition these
1952
+ # resolve to undefined and the INSERT is skipped, just like the inline waiter.
1953
+ output:
1954
+ maps:
1955
+ queueConfig:
1956
+ '@pipe':
1957
+ - ['{collator_trigger.output.data.items}', '{collator_cycle_hook.output.data.cur_index}']
1958
+ - ['{@array.get}', queueConfig]
1959
+ - ['{@object.get}']
1960
+ taskQueue:
1961
+ '@pipe':
1962
+ - ['{collator_trigger.output.data.items}', '{collator_cycle_hook.output.data.cur_index}']
1963
+ - ['{@array.get}', taskQueue]
1964
+ - ['{@object.get}']
1965
+ workflowName:
1966
+ '@pipe':
1967
+ - ['{collator_trigger.output.data.items}', '{collator_cycle_hook.output.data.cur_index}']
1968
+ - ['{@array.get}', workflowName]
1969
+ - ['{@object.get}']
1970
+ # Identical in principle to the inline waiter escalation block, but sourced
1971
+ # from the collated item. The escalation signal_key is auto-derived from this
1972
+ # activity's reentry hook rule (wfs.signal, matching item.signalId), so resolve()
1973
+ # and signal() — which double-fire wfs.signal + wfs.wait — reach it correctly.
1974
+ escalation:
1975
+ type: '{$self.output.data.queueConfig.type}'
1976
+ subtype: '{$self.output.data.queueConfig.subtype}'
1977
+ entity: '{$self.output.data.queueConfig.entity}'
1978
+ description: '{$self.output.data.queueConfig.description}'
1979
+ role: '{$self.output.data.queueConfig.role}'
1980
+ priority: '{$self.output.data.queueConfig.priority}'
1981
+ metadata: '{$self.output.data.queueConfig.metadata}'
1982
+ envelope: '{$self.output.data.queueConfig.envelope}'
1983
+ originId: '{$self.output.data.queueConfig.originId}'
1984
+ parentId: '{$self.output.data.queueConfig.parentId}'
1985
+ initiatedBy: '{$self.output.data.queueConfig.initiatedBy}'
1986
+ traceId: '{$self.output.data.queueConfig.traceId}'
1987
+ spanId: '{$self.output.data.queueConfig.spanId}'
1988
+ expiresAt: '{$self.output.data.queueConfig.expiresAt}'
1989
+ taskQueue: '{$self.output.data.taskQueue}'
1990
+ workflowType: '{$self.output.data.workflowName}'
1947
1991
  hook:
1948
1992
  type: object
1949
1993
  properties:
@@ -885,7 +885,12 @@ class WorkerService {
885
885
  code: enums_1.HMSH_CODE_DURABLE_ALL,
886
886
  items: [...interruptionRegistry],
887
887
  size: interruptionRegistry.length,
888
- workflowDimension: workflowDimension || '',
888
+ // Use the items' execution dimension (all items in one Promise.all share it),
889
+ // not the raw input dimension. In a continued generation (post-continueAsNew)
890
+ // the effective dimension is the generation prefix `$N`, which the items carry
891
+ // but `workflowInput.workflowDimension` does not — mirrors the single-condition
892
+ // path above so the collator's harvested markers land where condition() replays.
893
+ workflowDimension: interruptionRegistry[0].workflowDimension || workflowDimension || '',
889
894
  index: execIndex,
890
895
  originJobId: originJobId || workflowId,
891
896
  parentWorkflowId: workflowId,
@@ -160,7 +160,16 @@ async function condition(signalId, timeoutOrConfig) {
160
160
  type: 'DurableWaitForError',
161
161
  code: common_1.HMSH_CODE_DURABLE_WAIT,
162
162
  ...(timeout ? { duration: (0, common_1.s)(timeout) } : {}),
163
- ...(queueConfig ? { queueConfig } : {}),
163
+ // taskQueue + workflowName ride along so a collated wait (Promise.all) can
164
+ // write a faithful hmsh_escalations row from inside the collator flow —
165
+ // matching the inline waiter, which sources these from its trigger.
166
+ ...(queueConfig
167
+ ? {
168
+ queueConfig,
169
+ taskQueue: store.get('taskQueue'),
170
+ workflowName: store.get('workflowName'),
171
+ }
172
+ : {}),
164
173
  };
165
174
  interruptionRegistry.push(interruptionMessage);
166
175
  await (0, common_1.sleepImmediate)();
@@ -143,11 +143,21 @@ export declare class EscalationClientService {
143
143
  * durable proof. Signal delivery is best-effort post-commit — the resolved
144
144
  * row is the recovery record for any missed delivery. Returns the updated
145
145
  * row as `entry` on success.
146
+ *
147
+ * Pass `params.metadata` to merge the resolution outcome ("what actually
148
+ * happened") into the row's GIN-indexed `metadata` in the same atomic UPDATE,
149
+ * making it `@>`-queryable alongside the creation metadata ("what was
150
+ * intended"). This is distinct from `resolverPayload`, which is delivered to
151
+ * the waiting workflow as `condition()`'s return value and is not GIN-indexed.
146
152
  */
147
153
  resolve(params: ResolveEscalationParams, namespace?: string): Promise<ResolveEscalationResult>;
148
154
  /**
149
155
  * Resolves the highest-priority matching escalation by metadata filter,
150
156
  * then delivers its signal. Same transaction + WHERE guard semantics as `resolve()`.
157
+ *
158
+ * The `key`/`value` are the *selector* used to find the row; pass
159
+ * `params.metadata` to additionally merge a resolution patch into that row's
160
+ * GIN-indexed `metadata` in the same atomic UPDATE. See {@link resolve}.
151
161
  */
152
162
  resolveByMetadata(params: ResolveByMetadataParams, namespace?: string): Promise<ResolveEscalationResult>;
153
163
  /**
@@ -182,6 +192,10 @@ export declare class EscalationClientService {
182
192
  * Bulk-resolves pending escalations by id-set. No signal delivery — intended
183
193
  * for redirect-to-triage flows where no workflow is waiting. Returns the
184
194
  * resolved rows.
195
+ *
196
+ * Pass `params.metadata` to merge a resolution patch into every winning
197
+ * (still-pending) row's GIN-indexed `metadata` in the single atomic UPDATE.
198
+ * See {@link resolve}.
185
199
  */
186
200
  resolveMany(params: ResolveManyParams): Promise<EscalationEntry[]>;
187
201
  /**
@@ -281,12 +281,18 @@ class EscalationClientService {
281
281
  * durable proof. Signal delivery is best-effort post-commit — the resolved
282
282
  * row is the recovery record for any missed delivery. Returns the updated
283
283
  * row as `entry` on success.
284
+ *
285
+ * Pass `params.metadata` to merge the resolution outcome ("what actually
286
+ * happened") into the row's GIN-indexed `metadata` in the same atomic UPDATE,
287
+ * making it `@>`-queryable alongside the creation metadata ("what was
288
+ * intended"). This is distinct from `resolverPayload`, which is delivered to
289
+ * the waiting workflow as `condition()`'s return value and is not GIN-indexed.
284
290
  */
285
291
  async resolve(params, namespace) {
286
292
  const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
287
293
  const hm = await this._engine(null, ns);
288
294
  const store = hm.engine.store;
289
- const dbResult = await store.resolveEscalation({ id: params.id, resolverPayload: params.resolverPayload });
295
+ const dbResult = await store.resolveEscalation({ id: params.id, resolverPayload: params.resolverPayload, metadata: params.metadata });
290
296
  if (!dbResult.ok)
291
297
  return dbResult;
292
298
  if (dbResult.signalKey) {
@@ -301,12 +307,16 @@ class EscalationClientService {
301
307
  /**
302
308
  * Resolves the highest-priority matching escalation by metadata filter,
303
309
  * then delivers its signal. Same transaction + WHERE guard semantics as `resolve()`.
310
+ *
311
+ * The `key`/`value` are the *selector* used to find the row; pass
312
+ * `params.metadata` to additionally merge a resolution patch into that row's
313
+ * GIN-indexed `metadata` in the same atomic UPDATE. See {@link resolve}.
304
314
  */
305
315
  async resolveByMetadata(params, namespace) {
306
316
  const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
307
317
  const hm = await this._engine(null, ns);
308
318
  const store = hm.engine.store;
309
- const dbResult = await store.resolveEscalationByMetadata({ key: params.key, value: params.value, resolverPayload: params.resolverPayload, roles: params.roles });
319
+ const dbResult = await store.resolveEscalationByMetadata({ key: params.key, value: params.value, resolverPayload: params.resolverPayload, roles: params.roles, metadata: params.metadata });
310
320
  if (!dbResult.ok)
311
321
  return dbResult;
312
322
  if (dbResult.signalKey) {
@@ -366,6 +376,10 @@ class EscalationClientService {
366
376
  * Bulk-resolves pending escalations by id-set. No signal delivery — intended
367
377
  * for redirect-to-triage flows where no workflow is waiting. Returns the
368
378
  * resolved rows.
379
+ *
380
+ * Pass `params.metadata` to merge a resolution patch into every winning
381
+ * (still-pending) row's GIN-indexed `metadata` in the single atomic UPDATE.
382
+ * See {@link resolve}.
369
383
  */
370
384
  async resolveMany(params) {
371
385
  const hm = await this._engine(null, params.namespace);
@@ -157,5 +157,18 @@ declare abstract class StoreService<Provider extends ProviderClient, Transaction
157
157
  };
158
158
  attributes: Record<string, string>;
159
159
  }>;
160
+ /**
161
+ * Fetch just the lineage columns (`parent_id`, `origin_id`) for a job via a
162
+ * single indexed lookup on `key`. Powers the exporter's opt-in
163
+ * `include_lineage` pointer — `parent_id` is the real spawning workflow (never
164
+ * the synthetic collator `$C` job), `origin_id` is the root ancestor.
165
+ *
166
+ * @param jobKey - The job key (e.g., "hmsh:durable:j:workflowId")
167
+ * @returns parent/origin ids (null when absent — a null `parent_id` marks a root)
168
+ */
169
+ getJobLineage?(jobKey: string): Promise<{
170
+ parent_id: string | null;
171
+ origin_id: string | null;
172
+ } | null>;
160
173
  }
161
174
  export { StoreService };
@@ -6,6 +6,11 @@
6
6
  * Fetch job record by key.
7
7
  */
8
8
  export declare const GET_JOB_BY_KEY = "\n SELECT id, key, status, created_at, updated_at, expired_at, is_live\n FROM {schema}.jobs\n WHERE key = $1\n LIMIT 1\n";
9
+ /**
10
+ * Fetch just the lineage columns for a job — a single indexed lookup on `key`.
11
+ * Backs the exporter's opt-in `include_lineage` pointer.
12
+ */
13
+ export declare const GET_JOB_LINEAGE = "\n SELECT parent_id, origin_id\n FROM {schema}.jobs\n WHERE key = $1\n LIMIT 1\n";
9
14
  /**
10
15
  * Fetch all attributes for a job.
11
16
  */
@@ -4,7 +4,7 @@
4
4
  * These queries support the exporter's input enrichment and direct query features.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.buildChildWorkflowInputsQuery = exports.GET_STREAM_HISTORY_BY_JID_AND_AID = exports.GET_STREAM_HISTORY_BY_JID_AND_TYPE = exports.GET_STREAM_HISTORY_BY_JID = exports.GET_PROXYER_STREAM_INPUTS = exports.GET_ACTIVITY_INPUTS = exports.GET_JOB_ATTRIBUTES = exports.GET_JOB_BY_KEY = void 0;
7
+ exports.buildChildWorkflowInputsQuery = exports.GET_STREAM_HISTORY_BY_JID_AND_AID = exports.GET_STREAM_HISTORY_BY_JID_AND_TYPE = exports.GET_STREAM_HISTORY_BY_JID = exports.GET_PROXYER_STREAM_INPUTS = exports.GET_ACTIVITY_INPUTS = exports.GET_JOB_ATTRIBUTES = exports.GET_JOB_LINEAGE = exports.GET_JOB_BY_KEY = void 0;
8
8
  /**
9
9
  * Fetch job record by key.
10
10
  */
@@ -14,6 +14,16 @@ exports.GET_JOB_BY_KEY = `
14
14
  WHERE key = $1
15
15
  LIMIT 1
16
16
  `;
17
+ /**
18
+ * Fetch just the lineage columns for a job — a single indexed lookup on `key`.
19
+ * Backs the exporter's opt-in `include_lineage` pointer.
20
+ */
21
+ exports.GET_JOB_LINEAGE = `
22
+ SELECT parent_id, origin_id
23
+ FROM {schema}.jobs
24
+ WHERE key = $1
25
+ LIMIT 1
26
+ `;
17
27
  /**
18
28
  * Fetch all attributes for a job.
19
29
  */
@@ -216,6 +216,14 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
216
216
  };
217
217
  attributes: Record<string, string>;
218
218
  }>;
219
+ /**
220
+ * Single indexed lookup of the lineage columns for a job key. Returns the real
221
+ * spawning parent (never the synthetic collator `$C` job) and the root ancestor.
222
+ */
223
+ getJobLineage(jobKey: string): Promise<{
224
+ parent_id: string | null;
225
+ origin_id: string | null;
226
+ } | null>;
219
227
  /**
220
228
  * Fetch stream message history for a job from worker_streams.
221
229
  * Returns raw activity input/output data from soft-deleted messages.
@@ -1338,6 +1338,20 @@ class PostgresStoreService extends __1.StoreService {
1338
1338
  }
1339
1339
  return { job, attributes };
1340
1340
  }
1341
+ /**
1342
+ * Single indexed lookup of the lineage columns for a job key. Returns the real
1343
+ * spawning parent (never the synthetic collator `$C` job) and the root ancestor.
1344
+ */
1345
+ async getJobLineage(jobKey) {
1346
+ const { GET_JOB_LINEAGE } = await Promise.resolve().then(() => __importStar(require('./exporter-sql')));
1347
+ const schemaName = this.kvsql().safeName(this.appId);
1348
+ const sql = GET_JOB_LINEAGE.replace(/{schema}/g, schemaName);
1349
+ const result = await this.pgClient.query(sql, [jobKey]);
1350
+ if (result.rows.length === 0)
1351
+ return null;
1352
+ const { parent_id, origin_id } = result.rows[0];
1353
+ return { parent_id: parent_id ?? null, origin_id: origin_id ?? null };
1354
+ }
1341
1355
  /**
1342
1356
  * Fetch stream message history for a job from worker_streams.
1343
1357
  * Returns raw activity input/output data from soft-deleted messages.
@@ -1755,8 +1769,11 @@ class PostgresStoreService extends __1.StoreService {
1755
1769
  return { ok: true, entry: row.entry_json };
1756
1770
  }
1757
1771
  async resolveEscalation(params) {
1758
- const { id, namespace, resolverPayload } = params;
1772
+ const { id, namespace, resolverPayload, metadata } = params;
1759
1773
  const payloadJson = resolverPayload ? JSON.stringify(resolverPayload) : null;
1774
+ // metaJson is bound at a fixed index; the CASE no-ops when null so resolution
1775
+ // can merge into the GIN-indexed metadata without disturbing the param layout.
1776
+ const metaJson = metadata ? JSON.stringify(metadata) : null;
1760
1777
  // Explicit transaction: FOR UPDATE locks the row; WHERE guard on UPDATE is the TOCTOU
1761
1778
  // barrier — a concurrent caller whose UPDATE matches 0 rows sees 'already-resolved'.
1762
1779
  // On crash before COMMIT neither write lands; on crash after COMMIT both are durable.
@@ -1775,9 +1792,13 @@ class PostgresStoreService extends __1.StoreService {
1775
1792
  return { ok: false, reason: 'already-cancelled' };
1776
1793
  }
1777
1794
  const updateResult = await this.pgClient.query(`UPDATE public.hmsh_escalations
1778
- SET status = 'resolved', resolved_at = NOW(), resolver_payload = $2, updated_at = NOW()
1779
- WHERE id = $1 ${namespace ? 'AND namespace = $3' : ''} AND status = 'pending'
1780
- RETURNING *`, namespace ? [id, payloadJson, namespace] : [id, payloadJson]);
1795
+ SET status = 'resolved', resolved_at = NOW(), resolver_payload = $2,
1796
+ metadata = CASE WHEN $3::jsonb IS NOT NULL
1797
+ THEN COALESCE(metadata, '{}'::jsonb) || $3::jsonb
1798
+ ELSE metadata END,
1799
+ updated_at = NOW()
1800
+ WHERE id = $1 ${namespace ? 'AND namespace = $4' : ''} AND status = 'pending'
1801
+ RETURNING *`, namespace ? [id, payloadJson, metaJson, namespace] : [id, payloadJson, metaJson]);
1781
1802
  if (!updateResult.rows[0]) {
1782
1803
  await this.pgClient.query('ROLLBACK');
1783
1804
  return { ok: false, reason: 'already-resolved' };
@@ -1791,9 +1812,10 @@ class PostgresStoreService extends __1.StoreService {
1791
1812
  }
1792
1813
  }
1793
1814
  async resolveEscalationByMetadata(params) {
1794
- const { key, value, namespace, resolverPayload, roles } = params;
1815
+ const { key, value, namespace, resolverPayload, roles, metadata } = params;
1795
1816
  const filter = JSON.stringify({ [key]: value });
1796
1817
  const payloadJson = resolverPayload ? JSON.stringify(resolverPayload) : null;
1818
+ const metaJson = metadata ? JSON.stringify(metadata) : null;
1797
1819
  await this.pgClient.query('BEGIN');
1798
1820
  try {
1799
1821
  const lockResult = await this.pgClient.query(`SELECT id, signal_key, topic, status FROM public.hmsh_escalations
@@ -1813,9 +1835,13 @@ class PostgresStoreService extends __1.StoreService {
1813
1835
  return { ok: false, reason: 'already-cancelled' };
1814
1836
  }
1815
1837
  const updateResult = await this.pgClient.query(`UPDATE public.hmsh_escalations
1816
- SET status = 'resolved', resolved_at = NOW(), resolver_payload = $2, updated_at = NOW()
1838
+ SET status = 'resolved', resolved_at = NOW(), resolver_payload = $2,
1839
+ metadata = CASE WHEN $3::jsonb IS NOT NULL
1840
+ THEN COALESCE(metadata, '{}'::jsonb) || $3::jsonb
1841
+ ELSE metadata END,
1842
+ updated_at = NOW()
1817
1843
  WHERE id = $1 AND status = 'pending'
1818
- RETURNING *`, [id, payloadJson]);
1844
+ RETURNING *`, [id, payloadJson, metaJson]);
1819
1845
  if (!updateResult.rows[0]) {
1820
1846
  await this.pgClient.query('ROLLBACK');
1821
1847
  return { ok: false, reason: 'already-resolved' };
@@ -1993,17 +2019,21 @@ class PostgresStoreService extends __1.StoreService {
1993
2019
  return result.rowCount ?? 0;
1994
2020
  }
1995
2021
  async resolveManyEscalations(params) {
1996
- const { ids, namespace, resolverPayload } = params;
2022
+ const { ids, namespace, resolverPayload, metadata } = params;
1997
2023
  const payloadJson = resolverPayload ? JSON.stringify(resolverPayload) : null;
2024
+ const metaJson = metadata ? JSON.stringify(metadata) : null;
1998
2025
  const result = await this.pgClient.query(`UPDATE public.hmsh_escalations
1999
2026
  SET status = 'resolved',
2000
2027
  resolved_at = NOW(),
2001
2028
  resolver_payload = $1,
2029
+ metadata = CASE WHEN $3::jsonb IS NOT NULL
2030
+ THEN COALESCE(metadata, '{}'::jsonb) || $3::jsonb
2031
+ ELSE metadata END,
2002
2032
  updated_at = NOW()
2003
2033
  WHERE id = ANY($2::uuid[])
2004
- ${namespace ? 'AND namespace = $3' : ''}
2034
+ ${namespace ? 'AND namespace = $4' : ''}
2005
2035
  AND status = 'pending'
2006
- RETURNING *`, namespace ? [payloadJson, ids, namespace] : [payloadJson, ids]);
2036
+ RETURNING *`, namespace ? [payloadJson, ids, metaJson, namespace] : [payloadJson, ids, metaJson]);
2007
2037
  return result.rows;
2008
2038
  }
2009
2039
  async escalationStats(params = {}) {
@@ -265,6 +265,19 @@ export interface WorkflowExecution {
265
265
  summary: WorkflowExecutionSummary;
266
266
  children?: WorkflowExecution[];
267
267
  stream_history?: StreamHistoryEntry[];
268
+ /**
269
+ * Upward lineage pointer to the real spawning workflow — never the synthetic
270
+ * collator `$C` job. `null` marks a root. Only present when the export was
271
+ * requested with `include_lineage: true`.
272
+ */
273
+ parent_workflow_id?: string | null;
274
+ /**
275
+ * Root ancestor of this execution; `null` for a root itself (identify roots
276
+ * via a null `parent_workflow_id`). Every descendant carries the same
277
+ * `origin_id`, so a subtree is recoverable in one query. Only present when the
278
+ * export was requested with `include_lineage: true`.
279
+ */
280
+ origin_id?: string | null;
268
281
  }
269
282
  /**
270
283
  * Options for `handle.exportExecution()`. Controls event filtering, enrichment,
@@ -310,6 +323,17 @@ export interface ExecutionExportOptions {
310
323
  * @default false
311
324
  */
312
325
  allow_direct_query?: boolean;
326
+ /**
327
+ * When true, populates the export's `parent_workflow_id` and `origin_id` from
328
+ * the jobs table via a single indexed lookup. `parent_workflow_id` is the real
329
+ * spawning workflow (never the synthetic collator `$C` job); `origin_id` is the
330
+ * root ancestor (a null `parent_workflow_id` marks a root). Opt-in so the extra
331
+ * query is only paid when a UI needs upward lineage. Requires a provider that
332
+ * implements `getJobLineage` (e.g., Postgres); a no-op otherwise.
333
+ *
334
+ * @default false
335
+ */
336
+ include_lineage?: boolean;
313
337
  /**
314
338
  * When true, fetches the full stream message history for this workflow
315
339
  * from the worker_streams table and attaches it as `stream_history`.
@@ -256,6 +256,14 @@ export interface ResolveEscalationParams {
256
256
  id: string;
257
257
  namespace?: string;
258
258
  resolverPayload?: Record<string, unknown>;
259
+ /**
260
+ * Merged (not replaced) into the resolved row's `metadata` in the same atomic
261
+ * UPDATE — and only on the winning resolve. Records "what actually happened"
262
+ * into the GIN-indexed (`@>`-queryable) surface alongside the creation metadata.
263
+ * Distinct from `resolverPayload`, which is delivered to the waiting workflow as
264
+ * `condition()`'s return value and is not GIN-indexed.
265
+ */
266
+ metadata?: Record<string, unknown>;
259
267
  }
260
268
  export interface ResolveByMetadataParams {
261
269
  key: string;
@@ -263,6 +271,12 @@ export interface ResolveByMetadataParams {
263
271
  namespace?: string;
264
272
  resolverPayload?: Record<string, unknown>;
265
273
  roles?: string[];
274
+ /**
275
+ * Merge patch applied to the matched row's `metadata` (shallow, not replaced) in
276
+ * the same atomic UPDATE. Note this is the resolution patch — distinct from the
277
+ * `key`/`value` selector used to find the row. See {@link ResolveEscalationParams.metadata}.
278
+ */
279
+ metadata?: Record<string, unknown>;
266
280
  }
267
281
  export interface EscalateToRoleParams {
268
282
  id: string;
@@ -289,6 +303,11 @@ export interface ResolveManyParams {
289
303
  ids: string[];
290
304
  namespace?: string;
291
305
  resolverPayload?: Record<string, unknown>;
306
+ /**
307
+ * Merged (not replaced) into every winning (still-pending) row's `metadata` in
308
+ * the single bulk UPDATE. See {@link ResolveEscalationParams.metadata}.
309
+ */
310
+ metadata?: Record<string, unknown>;
292
311
  }
293
312
  /**
294
313
  * Full-fidelity migration params. Extends `CreateEscalationParams` with:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.22.8",
3
+ "version": "0.24.1",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",