@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.
- package/build/package.json +1 -1
- package/build/services/durable/exporter.js +8 -0
- package/build/services/durable/schemas/factory.d.ts +1 -1
- package/build/services/durable/schemas/factory.js +46 -2
- package/build/services/durable/worker.js +6 -1
- package/build/services/durable/workflow/condition.js +10 -1
- package/build/services/escalations/client.d.ts +14 -0
- package/build/services/escalations/client.js +16 -2
- package/build/services/store/index.d.ts +13 -0
- package/build/services/store/providers/postgres/exporter-sql.d.ts +5 -0
- package/build/services/store/providers/postgres/exporter-sql.js +11 -1
- package/build/services/store/providers/postgres/postgres.d.ts +8 -0
- package/build/services/store/providers/postgres/postgres.js +40 -10
- package/build/types/exporter.d.ts +24 -0
- package/build/types/hmsh_escalations.d.ts +19 -0
- package/package.json +1 -1
package/build/package.json
CHANGED
|
@@ -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
|
/**
|
|
@@ -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. (
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
1779
|
-
|
|
1780
|
-
|
|
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,
|
|
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 = $
|
|
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:
|