@hotmeshio/long-tail 0.5.2 → 0.5.4

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