@hotmeshio/long-tail 0.5.2 → 0.5.3

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 (30) hide show
  1. package/build/lib/events/system-events.d.ts +19 -0
  2. package/build/lib/events/system-events.js +62 -0
  3. package/build/modules/ltconfig.d.ts +8 -0
  4. package/build/modules/ltconfig.js +10 -0
  5. package/build/services/escalation/bulk.d.ts +2 -1
  6. package/build/services/escalation/bulk.js +20 -19
  7. package/build/services/escalation/client.d.ts +22 -0
  8. package/build/services/escalation/client.js +141 -0
  9. package/build/services/escalation/crud.d.ts +29 -21
  10. package/build/services/escalation/crud.js +175 -140
  11. package/build/services/escalation/index.d.ts +1 -0
  12. package/build/services/escalation/index.js +3 -0
  13. package/build/services/escalation/map.d.ts +15 -0
  14. package/build/services/escalation/map.js +63 -0
  15. package/build/services/escalation/queries.js +64 -149
  16. package/build/services/escalation/sql.d.ts +13 -32
  17. package/build/services/escalation/sql.js +36 -176
  18. package/build/services/export/post-process.js +23 -4
  19. package/build/services/interceptor/activities/config.js +5 -1
  20. package/build/services/interceptor/index.d.ts +3 -0
  21. package/build/services/interceptor/index.js +7 -21
  22. package/build/services/mcp/db-server/schemas.d.ts +1 -1
  23. package/build/services/yaml-workflow/deployer.js +4 -0
  24. package/build/services/yaml-workflow/workers/register.js +3 -0
  25. package/build/start/index.js +2 -1
  26. package/build/start/workers.js +12 -0
  27. package/build/system/mcp-servers/admin/schemas.d.ts +1 -1
  28. package/build/system/mcp-servers/db-query/schemas.d.ts +1 -1
  29. package/build/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +2 -2
@@ -19,32 +19,37 @@ exports.claimByMetadata = claimByMetadata;
19
19
  exports.resolveByMetadataAtomic = resolveByMetadataAtomic;
20
20
  const db_1 = require("../../lib/db");
21
21
  const publish_1 = require("../../lib/events/publish");
22
- const logger_1 = require("../../lib/logger");
22
+ const client_1 = require("./client");
23
+ const map_1 = require("./map");
23
24
  const sql_1 = require("./sql");
25
+ // All escalation state lives in `public.hmsh_escalations` (HotMesh 0.22.3),
26
+ // reached through `client.escalations.*`. The function signatures and return
27
+ // shapes below are the frozen public surface — only the storage path changed.
28
+ // Generous upper bound for the "all escalations for X" lookups, which the
29
+ // legacy SQL returned without a LIMIT. Escalations per workflow/origin/task are
30
+ // few; this avoids a silent page cap without an unbounded scan.
31
+ const LOOKUP_LIMIT = 1000;
24
32
  async function createEscalation(input) {
25
- logger_1.loggerRegistry.info(`[escalation-crud] createEscalation called for wf=${input.workflow_id} type=${input.type} caller=${new Error().stack?.split('\n')[2]?.trim()}`);
26
- const pool = (0, db_1.getPool)();
27
- // Ensure the role exists in lt_roles (FK constraint)
28
- await pool.query(sql_1.ENSURE_ROLE_EXISTS, [input.role]);
29
- const { rows } = await pool.query(sql_1.CREATE_ESCALATION, [
30
- input.type,
31
- input.subtype,
32
- input.description || null,
33
- input.priority || 2,
34
- input.task_id || null,
35
- input.origin_id || null,
36
- input.parent_id || null,
37
- input.role,
38
- input.envelope,
39
- input.metadata ? JSON.stringify(input.metadata) : null,
40
- input.escalation_payload || null,
41
- input.workflow_id || null,
42
- input.task_queue || null,
43
- input.workflow_type || null,
44
- input.trace_id || null,
45
- input.span_id || null,
46
- ]);
47
- const escalation = rows[0];
33
+ const client = await (0, client_1.escalations)();
34
+ const entry = await client.create({
35
+ type: input.type,
36
+ subtype: input.subtype,
37
+ description: input.description,
38
+ priority: input.priority ?? 2,
39
+ role: input.role,
40
+ taskId: input.task_id,
41
+ originId: input.origin_id,
42
+ parentId: input.parent_id,
43
+ workflowId: input.workflow_id,
44
+ taskQueue: input.task_queue,
45
+ workflowType: input.workflow_type,
46
+ traceId: input.trace_id,
47
+ spanId: input.span_id,
48
+ envelope: (0, map_1.toEnvelopeObject)(input.envelope),
49
+ metadata: input.metadata,
50
+ escalationPayload: (0, map_1.toJsonObject)(input.escalation_payload),
51
+ });
52
+ const escalation = (0, map_1.toEscalationRecord)(entry);
48
53
  (0, publish_1.publishEscalationEvent)({
49
54
  type: 'escalation.created',
50
55
  source: 'service',
@@ -58,22 +63,16 @@ async function createEscalation(input) {
58
63
  return escalation;
59
64
  }
60
65
  /**
61
- * Atomic claim operation. Does NOT change status "claimed" is implicit
62
- * via assigned_to + assigned_until > NOW().
63
- *
64
- * Conditions:
65
- * - status = 'pending' (not resolved/cancelled)
66
- * - Either: unassigned, expired claim, or same user (extension)
67
- *
68
- * Uses a CTE to capture the previous state so callers can detect extensions.
66
+ * Atomic claim. Implicit model status stays 'pending'; "claimed" is
67
+ * assigned_to + assigned_until > NOW(). `isExtension` is true when the same
68
+ * user re-claims (extends expiry). Returns null when the row is not claimable.
69
69
  */
70
70
  async function claimEscalation(id, userId, durationMinutes = 30) {
71
- const pool = (0, db_1.getPool)();
72
- const { rows } = await pool.query(sql_1.CLAIM_ESCALATION, [id, userId, durationMinutes]);
73
- if (rows.length === 0)
71
+ const client = await (0, client_1.escalations)();
72
+ const result = await client.claim({ id, assignee: userId, durationMinutes });
73
+ if (!result.ok)
74
74
  return null;
75
- const row = rows[0];
76
- const escalation = row;
75
+ const escalation = (0, map_1.toEscalationRecord)(result.entry);
77
76
  (0, publish_1.publishEscalationEvent)({
78
77
  type: 'escalation.claimed',
79
78
  source: 'service',
@@ -84,39 +83,41 @@ async function claimEscalation(id, userId, durationMinutes = 30) {
84
83
  status: 'claimed',
85
84
  data: { assigned_to: userId },
86
85
  });
87
- return {
88
- escalation,
89
- isExtension: row.prev_assigned_to === userId,
90
- };
86
+ return { escalation, isExtension: result.isExtension };
91
87
  }
88
+ /**
89
+ * Mark an escalation resolved. Signal delivery is owned by the resolution
90
+ * orchestrator (api/escalations/resolve.ts); service-created rows have no
91
+ * signal_key, so this never delivers a signal itself. Returns null when the
92
+ * row is missing or already terminal.
93
+ */
92
94
  async function resolveEscalation(id, resolverPayload) {
93
- const pool = (0, db_1.getPool)();
94
- const { rows } = await pool.query(sql_1.RESOLVE_ESCALATION, [id, JSON.stringify(resolverPayload)]);
95
- const escalation = rows[0] || null;
96
- if (escalation) {
97
- (0, publish_1.publishEscalationEvent)({
98
- type: 'escalation.resolved',
99
- source: 'service',
100
- workflowId: escalation.workflow_id || '',
101
- workflowName: escalation.workflow_type || '',
102
- taskQueue: escalation.task_queue || '',
103
- escalationId: escalation.id,
104
- status: 'resolved',
105
- data: {},
106
- });
107
- }
95
+ const client = await (0, client_1.escalations)();
96
+ const result = await client.resolve({ id, resolverPayload });
97
+ if (!result.ok)
98
+ return null;
99
+ const escalation = (0, map_1.toEscalationRecord)(result.entry);
100
+ (0, publish_1.publishEscalationEvent)({
101
+ type: 'escalation.resolved',
102
+ source: 'service',
103
+ workflowId: escalation.workflow_id || '',
104
+ workflowName: escalation.workflow_type || '',
105
+ taskQueue: escalation.task_queue || '',
106
+ escalationId: escalation.id,
107
+ status: 'resolved',
108
+ data: {},
109
+ });
108
110
  return escalation;
109
111
  }
110
112
  /**
111
- * Bulk update priority for a set of escalations.
112
- * Only updates pending escalations.
113
+ * Bulk update priority for a set of escalations. Only pending escalations are
114
+ * updated.
113
115
  */
114
116
  async function updateEscalationsPriority(ids, priority) {
115
117
  if (ids.length === 0)
116
118
  return 0;
117
- const pool = (0, db_1.getPool)();
118
- const { rowCount } = await pool.query(sql_1.UPDATE_ESCALATIONS_PRIORITY, [priority, ids]);
119
- return rowCount ?? 0;
119
+ const client = await (0, client_1.escalations)();
120
+ return client.updateManyPriority({ ids, priority });
120
121
  }
121
122
  /**
122
123
  * Get the distinct roles for a set of escalation IDs.
@@ -125,113 +126,145 @@ async function updateEscalationsPriority(ids, priority) {
125
126
  async function getEscalationRoles(ids) {
126
127
  if (ids.length === 0)
127
128
  return [];
128
- const pool = (0, db_1.getPool)();
129
- const { rows } = await pool.query(sql_1.GET_ESCALATION_ROLES, [ids]);
130
- return rows.map((r) => r.role);
129
+ const client = await (0, client_1.escalations)();
130
+ const rows = await client.list({ ids, limit: LOOKUP_LIMIT });
131
+ return [...new Set(rows.map(r => r.role).filter((r) => !!r))];
131
132
  }
132
133
  /**
133
134
  * Release a single escalation claim back to the available pool.
134
135
  * Only the assigned user (or superadmin via route) may release.
135
136
  */
136
137
  async function releaseEscalation(id, userId) {
137
- const pool = (0, db_1.getPool)();
138
- const { rows } = await pool.query(sql_1.RELEASE_ESCALATION, [id, userId]);
139
- const released = rows[0];
140
- if (released) {
141
- (0, publish_1.publishEscalationEvent)({
142
- type: 'escalation.released',
143
- source: 'service',
144
- workflowId: released.workflow_id || '',
145
- workflowName: released.workflow_type || '',
146
- taskQueue: released.task_queue || '',
147
- escalationId: released.id,
148
- status: 'released',
149
- data: { released_by: userId },
150
- });
151
- }
152
- return released || null;
138
+ const client = await (0, client_1.escalations)();
139
+ const result = await client.release({ id, assignee: userId });
140
+ if (!result.ok)
141
+ return null;
142
+ const released = (0, map_1.toEscalationRecord)(result.entry);
143
+ (0, publish_1.publishEscalationEvent)({
144
+ type: 'escalation.released',
145
+ source: 'service',
146
+ workflowId: released.workflow_id || '',
147
+ workflowName: released.workflow_type || '',
148
+ taskQueue: released.task_queue || '',
149
+ escalationId: released.id,
150
+ status: 'released',
151
+ data: { released_by: userId },
152
+ });
153
+ return released;
153
154
  }
155
+ /**
156
+ * Sweep expired claims back to the available pool, returning the count cleared.
157
+ * Availability is already query-time in the implicit model, but long-tail's
158
+ * public contract clears `assigned_to` and returns a count, so this runs as a
159
+ * single direct UPDATE on the shared table (the SDK's releaseExpired is a no-op).
160
+ */
154
161
  async function releaseExpiredClaims() {
162
+ await (0, client_1.ensureEscalationCompatView)();
155
163
  const pool = (0, db_1.getPool)();
156
164
  const { rowCount } = await pool.query(sql_1.RELEASE_EXPIRED_CLAIMS);
157
- return rowCount || 0;
165
+ return rowCount ?? 0;
158
166
  }
159
167
  /**
160
168
  * Reassign an escalation to a different role.
161
169
  * Clears the current assignment so it becomes available to the new role.
162
170
  */
163
171
  async function escalateToRole(id, targetRole) {
164
- const pool = (0, db_1.getPool)();
165
- const { rows } = await pool.query(sql_1.ESCALATE_TO_ROLE, [id, targetRole]);
166
- return rows[0] || null;
172
+ const client = await (0, client_1.escalations)();
173
+ const entry = await client.escalateToRole({ id, targetRole });
174
+ return entry ? (0, map_1.toEscalationRecord)(entry) : null;
167
175
  }
168
176
  async function getEscalation(id) {
169
- const pool = (0, db_1.getPool)();
170
- const { rows } = await pool.query(sql_1.GET_ESCALATION, [id]);
171
- return rows[0] || null;
177
+ const client = await (0, client_1.escalations)();
178
+ const entry = await client.get(id);
179
+ return entry ? (0, map_1.toEscalationRecord)(entry) : null;
172
180
  }
173
181
  async function getEscalationsByTaskId(taskId) {
174
- const pool = (0, db_1.getPool)();
175
- const { rows } = await pool.query(sql_1.GET_ESCALATIONS_BY_TASK_ID, [taskId]);
176
- return rows;
182
+ const client = await (0, client_1.escalations)();
183
+ const rows = await client.list({
184
+ taskId,
185
+ sortBy: 'created_at',
186
+ sortOrder: 'desc',
187
+ limit: LOOKUP_LIMIT,
188
+ });
189
+ return (0, map_1.toEscalationRecords)(rows);
177
190
  }
178
191
  async function getEscalationsByWorkflowId(workflowId) {
179
- const pool = (0, db_1.getPool)();
180
- const { rows } = await pool.query(sql_1.GET_ESCALATIONS_BY_WORKFLOW_ID, [workflowId]);
181
- return rows;
192
+ const client = await (0, client_1.escalations)();
193
+ const rows = await client.list({
194
+ workflowId,
195
+ sortBy: 'created_at',
196
+ sortOrder: 'desc',
197
+ limit: LOOKUP_LIMIT,
198
+ });
199
+ return (0, map_1.toEscalationRecords)(rows);
182
200
  }
183
201
  async function updateEscalationMetadata(id, patch) {
184
- const pool = (0, db_1.getPool)();
185
- const { rows } = await pool.query(sql_1.UPDATE_ESCALATION_METADATA, [id, JSON.stringify(patch)]);
186
- return rows[0] || null;
202
+ const client = await (0, client_1.escalations)();
203
+ const entry = await client.update({ id, metadata: patch });
204
+ return entry ? (0, map_1.toEscalationRecord)(entry) : null;
187
205
  }
188
206
  async function enrichEscalationRouting(id, metadataPatch, workflowFields) {
189
- const pool = (0, db_1.getPool)();
190
- const { rows } = await pool.query(sql_1.ENRICH_ESCALATION_ROUTING, [
207
+ const client = await (0, client_1.escalations)();
208
+ const entry = await client.update({
191
209
  id,
192
- JSON.stringify(metadataPatch),
193
- workflowFields.workflowType || null,
194
- workflowFields.workflowId || null,
195
- workflowFields.taskQueue || null,
196
- workflowFields.taskId || null,
197
- ]);
198
- return rows[0] || null;
210
+ metadata: metadataPatch,
211
+ workflowType: workflowFields.workflowType,
212
+ workflowId: workflowFields.workflowId,
213
+ taskQueue: workflowFields.taskQueue,
214
+ taskId: workflowFields.taskId,
215
+ });
216
+ return entry ? (0, map_1.toEscalationRecord)(entry) : null;
199
217
  }
200
218
  async function getEscalationsByOriginId(originId) {
201
- const pool = (0, db_1.getPool)();
202
- const { rows } = await pool.query(sql_1.GET_ESCALATIONS_BY_ORIGIN_ID, [originId]);
203
- return rows;
219
+ const client = await (0, client_1.escalations)();
220
+ const rows = await client.list({
221
+ originId,
222
+ sortBy: 'created_at',
223
+ sortOrder: 'desc',
224
+ limit: LOOKUP_LIMIT,
225
+ });
226
+ return (0, map_1.toEscalationRecords)(rows);
204
227
  }
205
228
  // --- Metadata candidate key lookups -----------------------------------------
206
229
  async function findByMetadata(key, value, status, limit = 50, offset = 0) {
207
- const pool = (0, db_1.getPool)();
208
- const filter = JSON.stringify({ [key]: value });
209
- const { rows } = await pool.query(sql_1.FIND_BY_METADATA, [filter, status || null, limit, offset]);
210
- const total = rows.length > 0 ? parseInt(rows[0]._total, 10) : 0;
211
- // Strip the window function column from results
212
- const escalations = rows.map(({ _total, ...rest }) => rest);
213
- return { escalations, total };
230
+ const client = await (0, client_1.escalations)();
231
+ const metadata = { [key]: value };
232
+ const [rows, total] = await Promise.all([
233
+ client.list({
234
+ metadata,
235
+ status,
236
+ orderBy: [
237
+ { column: 'priority', direction: 'asc' },
238
+ { column: 'created_at', direction: 'asc' },
239
+ ],
240
+ limit,
241
+ offset,
242
+ }),
243
+ client.count({ metadata, status }),
244
+ ]);
245
+ return { escalations: (0, map_1.toEscalationRecords)(rows), total };
214
246
  }
215
247
  /**
216
- * Atomic claim by metadata with inline RBAC.
217
- * The SQL WHERE clause enforces role membershipif the caller
218
- * doesn't have an allowed role, zero rows match and the claim
219
- * never happens. No pre-flight find, no TOCTOU.
248
+ * Atomic claim by metadata with inline RBAC and optional metadata merge.
249
+ * The SDK enforces the role filter in SQL callers without an allowed role
250
+ * match zero rows. Returns `{ escalation, isExtension, candidatesExist }` or
251
+ * null when nothing was claimed.
220
252
  *
221
- * @param allowedRoles — roles the caller can claim (null = no filter / global access)
222
- * @returns `{ escalation, isExtension, candidatesExist }` or null
253
+ * @param allowedRoles — roles the caller can claim (null = no filter / global)
223
254
  */
224
255
  async function claimByMetadata(key, value, userId, durationMinutes = 30, metadata, allowedRoles) {
225
- const pool = (0, db_1.getPool)();
226
- const filter = JSON.stringify({ [key]: value });
227
- const metaPatch = metadata ? JSON.stringify(metadata) : null;
228
- const roles = allowedRoles ?? null;
229
- const { rows } = await pool.query(sql_1.CLAIM_BY_METADATA_GUARDED, [filter, userId, durationMinutes, metaPatch, roles]);
230
- if (rows.length === 0)
256
+ const client = await (0, client_1.escalations)();
257
+ const result = await client.claimByMetadata({
258
+ key,
259
+ value,
260
+ assignee: userId,
261
+ durationMinutes,
262
+ roles: allowedRoles === null ? undefined : allowedRoles,
263
+ metadata,
264
+ });
265
+ if (!result.ok)
231
266
  return null;
232
- const row = rows[0];
233
- const { candidates_exist, prev_assigned_to, _total, ...rest } = row;
234
- const escalation = rest;
267
+ const escalation = (0, map_1.toEscalationRecord)(result.entry);
235
268
  (0, publish_1.publishEscalationEvent)({
236
269
  type: 'escalation.claimed',
237
270
  source: 'service',
@@ -244,19 +277,22 @@ async function claimByMetadata(key, value, userId, durationMinutes = 30, metadat
244
277
  });
245
278
  return {
246
279
  escalation,
247
- isExtension: prev_assigned_to === userId,
248
- candidatesExist: parseInt(candidates_exist, 10),
280
+ isExtension: result.isExtension,
281
+ candidatesExist: result.candidatesExist,
249
282
  };
250
283
  }
251
284
  /**
252
- * Atomic resolve by metadata with signal guard.
285
+ * Atomic resolve by metadata with signal guard, in a single CTE.
253
286
  *
254
- * Single query, two outcomes:
255
- * 1. No signal_id claim + resolve atomically. Returns { outcome: 'resolved', escalation }.
256
- * 2. signal_id present resolve skipped. Returns { outcome: 'signal_required', signalId, escalationId, ... }
257
- * so the caller can signal the workflow. conditionLT handles the rest.
287
+ * Signal-backed rows (those carrying `metadata.signal_id`) are NOT resolved
288
+ * here long-tail signals the paused workflow and the workflow interceptor
289
+ * resolves durably. If the workflow is gone the signal fails and the row stays
290
+ * pending, which is the contract the route suite pins. This guard is long-tail
291
+ * business logic over the shared table, so it runs as one atomic statement on
292
+ * `hmsh_escalations` rather than through the generic SDK resolve.
258
293
  */
259
294
  async function resolveByMetadataAtomic(key, value, userId, resolverPayload, metadata, allowedRoles) {
295
+ await (0, client_1.ensureEscalationCompatView)();
260
296
  const pool = (0, db_1.getPool)();
261
297
  const filter = JSON.stringify({ [key]: value });
262
298
  const payloadJson = JSON.stringify(resolverPayload);
@@ -267,8 +303,7 @@ async function resolveByMetadataAtomic(key, value, userId, resolverPayload, meta
267
303
  return { outcome: 'not_found' };
268
304
  const row = rows[0];
269
305
  if (row.outcome === 'resolved') {
270
- const { target_id, signal_id, target_workflow_id, target_workflow_type, target_task_queue, outcome, ...rest } = row;
271
- const escalation = rest;
306
+ const escalation = (0, map_1.toEscalationRecord)(row);
272
307
  (0, publish_1.publishEscalationEvent)({
273
308
  type: 'escalation.resolved',
274
309
  source: 'service',
@@ -281,7 +316,7 @@ async function resolveByMetadataAtomic(key, value, userId, resolverPayload, meta
281
316
  });
282
317
  return { outcome: 'resolved', escalation };
283
318
  }
284
- // Signal-backed escalation — return the signal info for the caller
319
+ // Signal-backed escalation — return the signal info for the caller to deliver.
285
320
  return {
286
321
  outcome: 'signal_required',
287
322
  signalId: row.signal_id,
@@ -2,3 +2,4 @@ export * from './types';
2
2
  export * from './crud';
3
3
  export * from './bulk';
4
4
  export * from './queries';
5
+ export { ensureEscalationCompatView } from './client';
@@ -14,7 +14,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.ensureEscalationCompatView = void 0;
17
18
  __exportStar(require("./types"), exports);
18
19
  __exportStar(require("./crud"), exports);
19
20
  __exportStar(require("./bulk"), exports);
20
21
  __exportStar(require("./queries"), exports);
22
+ var client_1 = require("./client");
23
+ Object.defineProperty(exports, "ensureEscalationCompatView", { enumerable: true, get: function () { return client_1.ensureEscalationCompatView; } });
@@ -0,0 +1,15 @@
1
+ import type { Types } from '@hotmeshio/hotmesh';
2
+ import type { LTEscalationRecord } from '../../types';
3
+ type EscalationEntry = Types.EscalationEntry;
4
+ /** SDK row → long-tail public record. */
5
+ export declare function toEscalationRecord(entry: EscalationEntry): LTEscalationRecord;
6
+ export declare function toEscalationRecords(entries: EscalationEntry[]): LTEscalationRecord[];
7
+ /**
8
+ * Parse a TEXT/JSON-string field from the public input shape into the JSONB
9
+ * object the SDK expects. Returns `undefined` (field omitted) for empty/invalid
10
+ * input so the column stays NULL rather than storing garbage.
11
+ */
12
+ export declare function toJsonObject(value: string | null | undefined): Record<string, unknown> | undefined;
13
+ /** Parse an envelope string, always yielding an object (defaults to `{}`). */
14
+ export declare function toEnvelopeObject(value: string | null | undefined): Record<string, unknown>;
15
+ export {};
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toEscalationRecord = toEscalationRecord;
4
+ exports.toEscalationRecords = toEscalationRecords;
5
+ exports.toJsonObject = toJsonObject;
6
+ exports.toEnvelopeObject = toEnvelopeObject;
7
+ /** Serialize a JSONB object column back to the TEXT form the public shape uses. */
8
+ function toJsonText(value) {
9
+ return value == null ? null : JSON.stringify(value);
10
+ }
11
+ /** SDK row → long-tail public record. */
12
+ function toEscalationRecord(entry) {
13
+ return {
14
+ id: entry.id,
15
+ type: entry.type ?? '',
16
+ subtype: entry.subtype ?? '',
17
+ description: entry.description,
18
+ status: entry.status,
19
+ priority: entry.priority,
20
+ task_id: entry.task_id,
21
+ origin_id: entry.origin_id,
22
+ parent_id: entry.parent_id,
23
+ workflow_id: entry.workflow_id,
24
+ task_queue: entry.task_queue,
25
+ workflow_type: entry.workflow_type,
26
+ role: entry.role ?? '',
27
+ assigned_to: entry.assigned_to,
28
+ assigned_until: entry.assigned_until,
29
+ resolved_at: entry.resolved_at,
30
+ claimed_at: entry.claimed_at,
31
+ envelope: entry.envelope == null ? '{}' : JSON.stringify(entry.envelope),
32
+ metadata: entry.metadata,
33
+ escalation_payload: toJsonText(entry.escalation_payload),
34
+ resolver_payload: toJsonText(entry.resolver_payload),
35
+ trace_id: entry.trace_id,
36
+ span_id: entry.span_id,
37
+ created_at: entry.created_at,
38
+ updated_at: entry.updated_at,
39
+ };
40
+ }
41
+ function toEscalationRecords(entries) {
42
+ return entries.map(toEscalationRecord);
43
+ }
44
+ /**
45
+ * Parse a TEXT/JSON-string field from the public input shape into the JSONB
46
+ * object the SDK expects. Returns `undefined` (field omitted) for empty/invalid
47
+ * input so the column stays NULL rather than storing garbage.
48
+ */
49
+ function toJsonObject(value) {
50
+ if (value == null || value === '')
51
+ return undefined;
52
+ try {
53
+ const parsed = JSON.parse(value);
54
+ return parsed && typeof parsed === 'object' ? parsed : undefined;
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ /** Parse an envelope string, always yielding an object (defaults to `{}`). */
61
+ function toEnvelopeObject(value) {
62
+ return toJsonObject(value) ?? {};
63
+ }