@hotmeshio/hotmesh 0.22.0 → 0.22.2

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.
@@ -1440,6 +1440,68 @@ class PostgresStoreService extends __1.StoreService {
1440
1440
  addEscalationToTransaction(params, transaction) {
1441
1441
  transaction.addCommand(this._escalationInsertSql, this._escalationInsertParams(params), 'void');
1442
1442
  }
1443
+ /**
1444
+ * Full-fidelity INSERT for data migration. Preserves the original `id` (UUID),
1445
+ * lifecycle state, and timestamps from the source table. Uses
1446
+ * `ON CONFLICT (id) DO NOTHING` so re-running a migration batch is safe —
1447
+ * rows that already exist are skipped and `null` is returned for them.
1448
+ */
1449
+ async createEscalationForMigration(params) {
1450
+ const { id, namespace, appId, signalKey, topic, workflowId, taskQueue, workflowType, type, subtype, entity, description, role, priority, originId, parentId, initiatedBy, createdBy, traceId, spanId, escalationPayload, metadata, envelope, expiresAt, status, assignedTo, claimExpiresAt, claimedAt, resolvedAt, resolverPayload, milestones, createdAt, updatedAt, } = params;
1451
+ const result = await this.pgClient.query(`
1452
+ INSERT INTO public.hmsh_escalations
1453
+ (id, namespace, app_id, signal_key, topic, workflow_id, task_queue, workflow_type,
1454
+ type, subtype, entity, description, role, priority,
1455
+ origin_id, parent_id, initiated_by, created_by, trace_id, span_id,
1456
+ escalation_payload, metadata, envelope, expires_at,
1457
+ status, assigned_to, claim_expires_at, claimed_at, resolved_at,
1458
+ resolver_payload, milestones, created_at, updated_at)
1459
+ VALUES
1460
+ ($1, $2, $3, $4, $5, $6, $7, $8,
1461
+ $9, $10, $11, $12, $13, $14,
1462
+ $15, $16, $17, $18, $19, $20,
1463
+ $21, $22, $23, $24,
1464
+ $25, $26, $27, $28, $29,
1465
+ $30, $31, $32, $33)
1466
+ ON CONFLICT (id) DO NOTHING
1467
+ RETURNING *
1468
+ `, [
1469
+ id,
1470
+ namespace ?? 'hmsh',
1471
+ appId ?? 'hmsh',
1472
+ signalKey ?? null,
1473
+ topic ?? null,
1474
+ workflowId ?? null,
1475
+ taskQueue ?? null,
1476
+ workflowType ?? null,
1477
+ type ?? null,
1478
+ subtype ?? null,
1479
+ entity ?? null,
1480
+ description ?? null,
1481
+ role ?? null,
1482
+ priority ?? 5,
1483
+ originId ?? null,
1484
+ parentId ?? null,
1485
+ initiatedBy ?? null,
1486
+ createdBy ?? null,
1487
+ traceId ?? null,
1488
+ spanId ?? null,
1489
+ escalationPayload ? JSON.stringify(escalationPayload) : null,
1490
+ metadata ? JSON.stringify(metadata) : null,
1491
+ envelope ? JSON.stringify(envelope) : null,
1492
+ expiresAt ?? null,
1493
+ status ?? 'pending',
1494
+ assignedTo ?? null,
1495
+ claimExpiresAt ?? null,
1496
+ claimedAt ?? null,
1497
+ resolvedAt ?? null,
1498
+ resolverPayload ? JSON.stringify(resolverPayload) : null,
1499
+ milestones ? JSON.stringify(milestones) : '[]',
1500
+ createdAt ?? new Date(),
1501
+ updatedAt ?? new Date(),
1502
+ ]);
1503
+ return result.rows[0] ?? null;
1504
+ }
1443
1505
  async getEscalation(id, namespace) {
1444
1506
  const result = await this.pgClient.query(`SELECT * FROM public.hmsh_escalations WHERE id = $1${namespace ? ' AND namespace = $2' : ''}`, namespace ? [id, namespace] : [id]);
1445
1507
  return result.rows[0] ?? null;
@@ -1449,7 +1511,7 @@ class PostgresStoreService extends __1.StoreService {
1449
1511
  return result.rows[0] ?? null;
1450
1512
  }
1451
1513
  async listEscalations(params = {}) {
1452
- const { namespace, role, type, subtype, entity, status, assignedTo, workflowId, originId, limit = 50, offset = 0 } = params;
1514
+ const { namespace, role, roles, type, subtype, entity, status, assignedTo, workflowId, originId, available, sortBy, sortOrder, limit = 50, offset = 0 } = params;
1453
1515
  const conditions = [];
1454
1516
  const values = [];
1455
1517
  let idx = 1;
@@ -1457,7 +1519,12 @@ class PostgresStoreService extends __1.StoreService {
1457
1519
  conditions.push(`namespace = $${idx++}`);
1458
1520
  values.push(namespace);
1459
1521
  }
1460
- if (role) {
1522
+ // roles[] takes precedence over role when both provided
1523
+ if (roles?.length) {
1524
+ conditions.push(`role = ANY($${idx++}::text[])`);
1525
+ values.push(roles);
1526
+ }
1527
+ else if (role) {
1461
1528
  conditions.push(`role = $${idx++}`);
1462
1529
  values.push(role);
1463
1530
  }
@@ -1489,17 +1556,86 @@ class PostgresStoreService extends __1.StoreService {
1489
1556
  conditions.push(`origin_id = $${idx++}`);
1490
1557
  values.push(originId);
1491
1558
  }
1559
+ if (available === true) {
1560
+ conditions.push(`(assigned_to IS NULL OR (assigned_until IS NOT NULL AND assigned_until <= NOW()))`);
1561
+ }
1562
+ if (available === false) {
1563
+ conditions.push(`(assigned_to IS NOT NULL AND (assigned_until IS NULL OR assigned_until > NOW()))`);
1564
+ }
1492
1565
  const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
1566
+ const col = sortBy === 'updated_at' ? 'updated_at' : sortBy === 'created_at' ? 'created_at' : 'priority';
1567
+ const dir = sortOrder === 'desc' ? 'DESC' : 'ASC';
1568
+ // Secondary sort is always created_at DESC for deterministic page ordering
1569
+ const orderBy = col === 'priority'
1570
+ ? `ORDER BY priority ${dir}, created_at DESC`
1571
+ : `ORDER BY ${col} ${dir}`;
1493
1572
  values.push(limit, offset);
1494
- const result = await this.pgClient.query(`SELECT * FROM public.hmsh_escalations ${where} ORDER BY priority ASC, created_at ASC LIMIT $${idx++} OFFSET $${idx}`, values);
1573
+ const result = await this.pgClient.query(`SELECT *,
1574
+ (assigned_to IS NULL OR (assigned_until IS NOT NULL AND assigned_until <= NOW())) AS available
1575
+ FROM public.hmsh_escalations ${where} ${orderBy} LIMIT $${idx++} OFFSET $${idx}`, values);
1495
1576
  return result.rows;
1496
1577
  }
1578
+ async countEscalations(params = {}) {
1579
+ const { namespace, role, roles, type, subtype, entity, status, assignedTo, workflowId, originId, available } = params;
1580
+ const conditions = [];
1581
+ const values = [];
1582
+ let idx = 1;
1583
+ if (namespace) {
1584
+ conditions.push(`namespace = $${idx++}`);
1585
+ values.push(namespace);
1586
+ }
1587
+ if (roles?.length) {
1588
+ conditions.push(`role = ANY($${idx++}::text[])`);
1589
+ values.push(roles);
1590
+ }
1591
+ else if (role) {
1592
+ conditions.push(`role = $${idx++}`);
1593
+ values.push(role);
1594
+ }
1595
+ if (type) {
1596
+ conditions.push(`type = $${idx++}`);
1597
+ values.push(type);
1598
+ }
1599
+ if (subtype) {
1600
+ conditions.push(`subtype = $${idx++}`);
1601
+ values.push(subtype);
1602
+ }
1603
+ if (entity) {
1604
+ conditions.push(`entity = $${idx++}`);
1605
+ values.push(entity);
1606
+ }
1607
+ if (status) {
1608
+ conditions.push(`status = $${idx++}`);
1609
+ values.push(status);
1610
+ }
1611
+ if (assignedTo) {
1612
+ conditions.push(`assigned_to = $${idx++}`);
1613
+ values.push(assignedTo);
1614
+ }
1615
+ if (workflowId) {
1616
+ conditions.push(`workflow_id = $${idx++}`);
1617
+ values.push(workflowId);
1618
+ }
1619
+ if (originId) {
1620
+ conditions.push(`origin_id = $${idx++}`);
1621
+ values.push(originId);
1622
+ }
1623
+ if (available === true) {
1624
+ conditions.push(`(assigned_to IS NULL OR (assigned_until IS NOT NULL AND assigned_until <= NOW()))`);
1625
+ }
1626
+ if (available === false) {
1627
+ conditions.push(`(assigned_to IS NOT NULL AND (assigned_until IS NULL OR assigned_until > NOW()))`);
1628
+ }
1629
+ const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
1630
+ const result = await this.pgClient.query(`SELECT COUNT(*)::int AS total FROM public.hmsh_escalations ${where}`, values);
1631
+ return result.rows[0]?.total ?? 0;
1632
+ }
1497
1633
  async claimEscalation(params) {
1498
1634
  const { id, namespace, assignee = '', durationMinutes = 1 } = params;
1499
- // all_rows: reads the row without locking, so we can distinguish 'not-found' vs 'conflict'
1500
- // target: attempts the lock; SKIP LOCKED means a locked row is skipped without error
1501
- // claimed: performs the UPDATE only if target succeeded
1502
- // The final SELECT always returns exactly 1 row via the dummy left-join — no second round-trip.
1635
+ // Claims are implicit: status stays 'pending'; availability = assigned_to IS NULL OR assigned_until <= NOW().
1636
+ // all_rows: unlocked read to distinguish 'not-found' vs 'conflict'.
1637
+ // target: locks the row only when it is available (no active claim or same assignee re-claiming).
1638
+ // claimed: writes both assigned_until and claim_expires_at (same value) for service-layer compat.
1503
1639
  const result = await this.pgClient.query(`
1504
1640
  WITH all_rows AS MATERIALIZED (
1505
1641
  SELECT id FROM public.hmsh_escalations
@@ -1510,16 +1646,16 @@ class PostgresStoreService extends __1.StoreService {
1510
1646
  WHERE id = $1
1511
1647
  ${namespace ? 'AND namespace = $4' : ''}
1512
1648
  AND status = 'pending'
1513
- AND (assigned_to IS NULL OR claim_expires_at <= NOW() OR assigned_to = $2)
1649
+ AND (assigned_to IS NULL OR assigned_until IS NULL OR assigned_until <= NOW() OR assigned_to = $2)
1514
1650
  LIMIT 1
1515
1651
  FOR UPDATE SKIP LOCKED
1516
1652
  ),
1517
1653
  claimed AS (
1518
1654
  UPDATE public.hmsh_escalations
1519
- SET status = 'claimed',
1520
- assigned_to = $2,
1655
+ SET assigned_to = $2,
1521
1656
  claimed_at = NOW(),
1522
1657
  claim_expires_at = NOW() + ($3 * INTERVAL '1 minute'),
1658
+ assigned_until = NOW() + ($3 * INTERVAL '1 minute'),
1523
1659
  updated_at = NOW()
1524
1660
  FROM target WHERE public.hmsh_escalations.id = target.id
1525
1661
  RETURNING public.hmsh_escalations.*
@@ -1538,10 +1674,8 @@ class PostgresStoreService extends __1.StoreService {
1538
1674
  async claimEscalationByMetadata(params) {
1539
1675
  const { key, value, namespace, assignee = '', durationMinutes = 1, roles } = params;
1540
1676
  const filter = JSON.stringify({ [key]: value });
1541
- // all_candidates: counts ALL matching rows (any status) to distinguish 'not-found' vs 'conflict'
1542
- // target: attempts the lock on the highest-priority pending row; SKIP LOCKED skips already-claimed ones
1543
- // claimed: performs the UPDATE atomically from target
1544
- // The dummy left-join ensures exactly 1 row is always returned so candidatesExist is always visible.
1677
+ // target captures prior_assigned_to to detect re-claim (isExtension).
1678
+ // Implicit claim model: status stays 'pending'; availability = assigned_until IS NULL OR assigned_until <= NOW().
1545
1679
  const result = await this.pgClient.query(`
1546
1680
  WITH all_candidates AS MATERIALIZED (
1547
1681
  SELECT id FROM public.hmsh_escalations
@@ -1550,25 +1684,25 @@ class PostgresStoreService extends __1.StoreService {
1550
1684
  AND ($4::text[] IS NULL OR role = ANY($4::text[]))
1551
1685
  ),
1552
1686
  target AS MATERIALIZED (
1553
- SELECT id FROM public.hmsh_escalations
1687
+ SELECT id, assigned_to AS prior_assigned_to FROM public.hmsh_escalations
1554
1688
  WHERE ${namespace ? 'namespace = $5 AND' : ''}
1555
1689
  metadata @> $1::jsonb
1556
1690
  AND ($4::text[] IS NULL OR role = ANY($4::text[]))
1557
1691
  AND status = 'pending'
1558
- AND (assigned_to IS NULL OR claim_expires_at <= NOW() OR assigned_to = $2)
1692
+ AND (assigned_to IS NULL OR assigned_until IS NULL OR assigned_until <= NOW() OR assigned_to = $2)
1559
1693
  ORDER BY priority ASC, created_at ASC
1560
1694
  LIMIT 1
1561
1695
  FOR UPDATE SKIP LOCKED
1562
1696
  ),
1563
1697
  claimed AS (
1564
1698
  UPDATE public.hmsh_escalations
1565
- SET status = 'claimed',
1566
- assigned_to = $2,
1699
+ SET assigned_to = $2,
1567
1700
  claimed_at = NOW(),
1568
1701
  claim_expires_at = NOW() + ($3 * INTERVAL '1 minute'),
1702
+ assigned_until = NOW() + ($3 * INTERVAL '1 minute'),
1569
1703
  updated_at = NOW()
1570
1704
  FROM target WHERE public.hmsh_escalations.id = target.id
1571
- RETURNING public.hmsh_escalations.*
1705
+ RETURNING public.hmsh_escalations.*, target.prior_assigned_to
1572
1706
  )
1573
1707
  SELECT c.*, (SELECT COUNT(*)::int FROM all_candidates) AS candidates_exist
1574
1708
  FROM (VALUES(1)) AS dummy(v)
@@ -1578,8 +1712,10 @@ class PostgresStoreService extends __1.StoreService {
1578
1712
  : [filter, assignee, durationMinutes, roles ?? null]);
1579
1713
  const row = result.rows[0];
1580
1714
  const candidatesExist = row?.candidates_exist ?? 0;
1581
- if (row?.id)
1582
- return { ok: true, entry: row, candidatesExist };
1715
+ if (row?.id) {
1716
+ const isExtension = row.prior_assigned_to === assignee;
1717
+ return { ok: true, entry: row, candidatesExist, isExtension };
1718
+ }
1583
1719
  return {
1584
1720
  ok: false,
1585
1721
  reason: candidatesExist > 0 ? 'conflict' : 'not-found',
@@ -1595,7 +1731,7 @@ class PostgresStoreService extends __1.StoreService {
1595
1731
  WITH target AS MATERIALIZED (
1596
1732
  SELECT id, assigned_to FROM public.hmsh_escalations
1597
1733
  WHERE id = $1 ${namespace ? 'AND namespace = $3' : ''}
1598
- AND status = 'claimed'
1734
+ AND status = 'pending' AND assigned_to IS NOT NULL
1599
1735
  FOR UPDATE
1600
1736
  ),
1601
1737
  released AS (
@@ -1620,6 +1756,7 @@ class PostgresStoreService extends __1.StoreService {
1620
1756
  }
1621
1757
  async resolveEscalation(params) {
1622
1758
  const { id, namespace, resolverPayload } = params;
1759
+ const payloadJson = resolverPayload ? JSON.stringify(resolverPayload) : null;
1623
1760
  const result = await this.pgClient.query(`
1624
1761
  WITH target AS MATERIALIZED (
1625
1762
  SELECT id, signal_key, topic, status FROM public.hmsh_escalations
@@ -1634,7 +1771,7 @@ class PostgresStoreService extends __1.StoreService {
1634
1771
  updated_at = NOW()
1635
1772
  FROM target
1636
1773
  WHERE public.hmsh_escalations.id = target.id
1637
- AND target.status IN ('pending', 'claimed')
1774
+ AND target.status = 'pending'
1638
1775
  RETURNING public.hmsh_escalations.id
1639
1776
  )
1640
1777
  SELECT t.signal_key, t.topic, t.status AS prior_status,
@@ -1647,8 +1784,8 @@ class PostgresStoreService extends __1.StoreService {
1647
1784
  FROM (SELECT * FROM target) t
1648
1785
  FULL OUTER JOIN (SELECT id FROM resolved) r ON r.id = t.id
1649
1786
  `, namespace
1650
- ? [id, resolverPayload ? JSON.stringify(resolverPayload) : null, namespace]
1651
- : [id, resolverPayload ? JSON.stringify(resolverPayload) : null]);
1787
+ ? [id, payloadJson, namespace]
1788
+ : [id, payloadJson]);
1652
1789
  if (!result.rows[0] || result.rows[0].outcome === 'not-found')
1653
1790
  return { ok: false, reason: 'not-found' };
1654
1791
  if (result.rows[0].outcome === 'already-cancelled')
@@ -1660,6 +1797,7 @@ class PostgresStoreService extends __1.StoreService {
1660
1797
  async resolveEscalationByMetadata(params) {
1661
1798
  const { key, value, namespace, resolverPayload, roles } = params;
1662
1799
  const filter = JSON.stringify({ [key]: value });
1800
+ const payloadJson = resolverPayload ? JSON.stringify(resolverPayload) : null;
1663
1801
  const result = await this.pgClient.query(`
1664
1802
  WITH target AS MATERIALIZED (
1665
1803
  SELECT id, signal_key, topic, status FROM public.hmsh_escalations
@@ -1677,7 +1815,7 @@ class PostgresStoreService extends __1.StoreService {
1677
1815
  updated_at = NOW()
1678
1816
  FROM target
1679
1817
  WHERE public.hmsh_escalations.id = target.id
1680
- AND target.status IN ('pending', 'claimed')
1818
+ AND target.status = 'pending'
1681
1819
  RETURNING public.hmsh_escalations.id
1682
1820
  )
1683
1821
  SELECT t.signal_key, t.topic, t.status AS prior_status,
@@ -1690,8 +1828,8 @@ class PostgresStoreService extends __1.StoreService {
1690
1828
  FROM (SELECT * FROM target) t
1691
1829
  FULL OUTER JOIN (SELECT id FROM resolved) r ON r.id = t.id
1692
1830
  `, namespace
1693
- ? [filter, resolverPayload ? JSON.stringify(resolverPayload) : null, roles ?? null, namespace]
1694
- : [filter, resolverPayload ? JSON.stringify(resolverPayload) : null, roles ?? null]);
1831
+ ? [filter, payloadJson, roles ?? null, namespace]
1832
+ : [filter, payloadJson, roles ?? null]);
1695
1833
  if (!result.rows[0] || result.rows[0].outcome === 'not-found')
1696
1834
  return { ok: false, reason: 'not-found' };
1697
1835
  if (result.rows[0].outcome === 'already-cancelled')
@@ -1747,7 +1885,7 @@ class PostgresStoreService extends __1.StoreService {
1747
1885
  SET status = 'cancelled', updated_at = NOW()
1748
1886
  FROM target
1749
1887
  WHERE public.hmsh_escalations.id = target.id
1750
- AND target.status IN ('pending', 'claimed')
1888
+ AND target.status = 'pending'
1751
1889
  RETURNING public.hmsh_escalations.id
1752
1890
  )
1753
1891
  SELECT t.id, t.status AS prior_status,
@@ -1777,7 +1915,7 @@ class PostgresStoreService extends __1.StoreService {
1777
1915
  claim_expires_at = NULL,
1778
1916
  updated_at = NOW()
1779
1917
  WHERE id = $1 ${namespace ? 'AND namespace = $3' : ''}
1780
- AND status IN ('pending', 'claimed')
1918
+ AND status = 'pending'
1781
1919
  RETURNING *
1782
1920
  `, namespace ? [id, targetRole, namespace] : [id, targetRole]);
1783
1921
  return result.rows[0] ?? null;
@@ -1853,16 +1991,10 @@ class PostgresStoreService extends __1.StoreService {
1853
1991
  `, namespace ? [id, JSON.stringify(stamped), namespace] : [id, JSON.stringify(stamped)]);
1854
1992
  return result.rows[0] ?? null;
1855
1993
  }
1856
- async releaseExpiredEscalations(namespace) {
1857
- const result = await this.pgClient.query(`
1858
- UPDATE public.hmsh_escalations
1859
- SET status = 'pending', assigned_to = NULL, assigned_until = NULL,
1860
- claimed_at = NULL, claim_expires_at = NULL, updated_at = NOW()
1861
- WHERE status = 'claimed'
1862
- AND claim_expires_at <= NOW()
1863
- ${namespace ? 'AND namespace = $1' : ''}
1864
- `, namespace ? [namespace] : []);
1865
- return result.rowCount ?? 0;
1994
+ async releaseExpiredEscalations(_namespace) {
1995
+ // No-op in the implicit claim model: availability is computed at query time from
1996
+ // assigned_until, so no background sweep is needed to release expired claims.
1997
+ return 0;
1866
1998
  }
1867
1999
  }
1868
2000
  exports.PostgresStoreService = PostgresStoreService;
@@ -34,7 +34,8 @@ export interface EscalationEntry {
34
34
  entity: string | null;
35
35
  description: string | null;
36
36
  role: string | null;
37
- status: 'pending' | 'claimed' | 'resolved' | 'cancelled' | 'expired';
37
+ /** Lifecycle status. Claims are implicit: status='pending' + assigned_to IS NOT NULL + assigned_until > NOW(). */
38
+ status: 'pending' | 'resolved' | 'cancelled' | 'expired';
38
39
  priority: number;
39
40
  assigned_to: string | null;
40
41
  assigned_until: Date | null;
@@ -55,6 +56,8 @@ export interface EscalationEntry {
55
56
  expires_at: Date | null;
56
57
  created_at: Date;
57
58
  updated_at: Date;
59
+ /** Computed by list(): true when the row is claimable (no active assignee or expired claim). */
60
+ available?: boolean;
58
61
  }
59
62
  /**
60
63
  * Result of `claim()` — identifies whether failure was due to the row not
@@ -70,15 +73,15 @@ export type ClaimEscalationResult = {
70
73
  reason: 'not-found' | 'conflict';
71
74
  };
72
75
  /**
73
- * Result of `claimByMetadata()`. Includes `candidatesExist` the total
74
- * count of rows matching the metadata filter regardless of claimability — so
75
- * callers can distinguish "nothing matching at all" from "found candidates but
76
- * all are locked or in-progress".
76
+ * Result of `claimByMetadata()`. Includes `candidatesExist` and `isExtension`:
77
+ * - `candidatesExist` — total count of rows matching the filter regardless of claimability
78
+ * - `isExtension` true when the same assignee re-claims a row they already hold (extends the expiry)
77
79
  */
78
80
  export type ClaimByMetadataResult = {
79
81
  ok: true;
80
82
  entry: EscalationEntry;
81
83
  candidatesExist: number;
84
+ isExtension: boolean;
82
85
  } | {
83
86
  ok: false;
84
87
  reason: 'not-found' | 'conflict';
@@ -88,7 +91,7 @@ export type ResolveEscalationResult = {
88
91
  ok: true;
89
92
  } | {
90
93
  ok: false;
91
- reason: 'not-found' | 'already-resolved' | 'already-cancelled' | 'signal-failed';
94
+ reason: 'not-found' | 'already-resolved' | 'already-cancelled';
92
95
  };
93
96
  export type ReleaseEscalationResult = {
94
97
  ok: true;
@@ -105,6 +108,8 @@ export type CancelEscalationResult = {
105
108
  export interface ListEscalationsParams {
106
109
  namespace?: string;
107
110
  role?: string;
111
+ /** Filter by one or more roles (OR semantics; takes precedence over `role` when both set). */
112
+ roles?: string[];
108
113
  type?: string;
109
114
  subtype?: string;
110
115
  entity?: string;
@@ -112,6 +117,10 @@ export interface ListEscalationsParams {
112
117
  assignedTo?: string;
113
118
  workflowId?: string;
114
119
  originId?: string;
120
+ /** When true, returns only rows without an active claim. When false, returns only actively claimed rows. */
121
+ available?: boolean;
122
+ sortBy?: 'created_at' | 'priority' | 'updated_at';
123
+ sortOrder?: 'asc' | 'desc';
115
124
  limit?: number;
116
125
  offset?: number;
117
126
  }
@@ -210,3 +219,31 @@ export interface EscalateToRoleParams {
210
219
  targetRole: string;
211
220
  namespace?: string;
212
221
  }
222
+ /**
223
+ * Full-fidelity migration params. Extends `CreateEscalationParams` with:
224
+ * - `id` (required) — preserves the original UUID; no auto-generation
225
+ * - lifecycle state fields (`status`, `assignedTo`, `claimExpiresAt`, …) — carry over
226
+ * the exact state of the migrated row so in-flight escalations land correctly
227
+ * - `createdAt` / `updatedAt` — preserve original timestamps
228
+ *
229
+ * The underlying INSERT uses `ON CONFLICT (id) DO NOTHING`, so calling
230
+ * `migrate()` multiple times with the same ID is safe — subsequent calls
231
+ * return `null` without touching the existing row.
232
+ */
233
+ export interface MigrateEscalationParams extends CreateEscalationParams {
234
+ /** Required — preserve the original UUID from the source table. */
235
+ id: string;
236
+ status?: 'pending' | 'claimed' | 'resolved' | 'cancelled' | 'expired';
237
+ assignedTo?: string;
238
+ claimExpiresAt?: Date;
239
+ claimedAt?: Date;
240
+ resolvedAt?: Date;
241
+ resolverPayload?: Record<string, unknown>;
242
+ milestones?: Array<{
243
+ name: string;
244
+ value: unknown;
245
+ [key: string]: unknown;
246
+ }>;
247
+ createdAt?: Date;
248
+ updatedAt?: Date;
249
+ }
@@ -25,4 +25,4 @@ export { ReclaimedMessageType, RetryPolicy, RouterConfig, StreamCode, StreamConf
25
25
  export { context, Context, Counter, Meter, metrics, propagation, SpanContext, Span, SpanStatus, SpanStatusCode, SpanKind, trace, Tracer, ValueType, } from './telemetry';
26
26
  export { WorkListTaskType } from './task';
27
27
  export { TransitionMatch, TransitionRule, Transitions } from './transition';
28
- export { ConditionQueueConfig, EscalationEntry, ClaimEscalationResult, ClaimByMetadataResult, ReleaseEscalationResult, ResolveEscalationResult, CancelEscalationResult, ListEscalationsParams, CreateEscalationParams, UpdateEscalationParams, AppendMilestonesParams, ClaimEscalationParams, ClaimByMetadataParams, ReleaseEscalationParams, ResolveEscalationParams, ResolveByMetadataParams, EscalateToRoleParams, } from './hmsh_escalations';
28
+ export { ConditionQueueConfig, EscalationEntry, ClaimEscalationResult, ClaimByMetadataResult, ReleaseEscalationResult, ResolveEscalationResult, CancelEscalationResult, ListEscalationsParams, CreateEscalationParams, UpdateEscalationParams, AppendMilestonesParams, ClaimEscalationParams, ClaimByMetadataParams, ReleaseEscalationParams, ResolveEscalationParams, ResolveByMetadataParams, EscalateToRoleParams, MigrateEscalationParams, } from './hmsh_escalations';
package/index.ts CHANGED
@@ -18,6 +18,7 @@ import * as KeyStore from './modules/key';
18
18
  import { ConnectorService as Connector } from './services/connector/factory';
19
19
  import { PostgresConnection as ConnectorPostgres } from './services/connector/providers/postgres';
20
20
  import { NatsConnection as ConnectorNATS } from './services/connector/providers/nats';
21
+ import { Escalations } from './services/escalations';
21
22
 
22
23
  export {
23
24
  //Provider Connectors
@@ -30,6 +31,7 @@ export {
30
31
  HotMeshConfig,
31
32
  Virtual,
32
33
  Durable,
34
+ Escalations,
33
35
  DBA,
34
36
 
35
37
  //Durable Submodules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.22.0",
3
+ "version": "0.22.2",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -50,7 +50,7 @@
50
50
  "test:durable:signal": "vitest run tests/durable/signal/postgres.test.ts",
51
51
  "test:durable:readonly": "docker compose --profile readonly up -d --build && docker compose exec hotmesh-readonly npx vitest run --config tests/durable/readonly/vitest.config.mts",
52
52
  "test:durable:unknown": "vitest run tests/durable/unknown/postgres.test.ts",
53
- "test:durable:esclations": "HMSH_LOGLEVEL=info vitest run tests/durable/escalations",
53
+ "test:durable:escalations": "HMSH_LOGLEVEL=info vitest run tests/durable/escalations",
54
54
  "test:durable:exporter": "HMSH_LOGLEVEL=info vitest run tests/durable/exporter",
55
55
  "test:durable:exporter:debug": "EXPORT_DEBUG=1 HMSH_LOGLEVEL=error vitest run tests/durable/basic/postgres.test.ts",
56
56
  "test:durable:codec": "vitest run tests/durable/codec/postgres.test.ts",
@@ -85,7 +85,13 @@
85
85
  "test:sub:nats": "vitest run tests/functional/sub/providers/nats/nats.test.ts",
86
86
  "test:trigger": "vitest run tests/unit/services/activities/trigger.test.ts",
87
87
  "test:virtual": "vitest run tests/virtual",
88
- "test:unit": "vitest run tests/unit"
88
+ "test:unit": "vitest run tests/unit",
89
+
90
+ "prove": "docker compose exec hotmesh npx vitest run tests/durable 2>&1 | tee /tmp/hmsh-durable.txt && grep -E 'FAIL|Tests |Files ' /tmp/hmsh-durable.txt | tail -5",
91
+ "prove:escalations": "docker compose exec hotmesh npx vitest run tests/durable/escalations/postgres.test.ts 2>&1 | tee /tmp/hmsh-escalations.txt && grep -E 'FAIL|Tests |Files ' /tmp/hmsh-escalations.txt | tail -5",
92
+ "prove:functional": "docker compose exec hotmesh npx vitest run tests/functional 2>&1 | tee /tmp/hmsh-functional.txt && grep -E 'FAIL|Tests |Files ' /tmp/hmsh-functional.txt | tail -5",
93
+ "prove:all": "docker compose exec hotmesh npx vitest run tests/ 2>&1 | tee /tmp/hmsh-all.txt && grep -E 'FAIL|Tests |Files ' /tmp/hmsh-all.txt | tail -5",
94
+ "prove:file": "f() { docker compose exec hotmesh npx vitest run \"$@\" 2>&1 | tee /tmp/hmsh-file.txt && grep -E 'FAIL|Tests |Files ' /tmp/hmsh-file.txt | tail -5; }; f"
89
95
  },
90
96
  "keywords": [
91
97
  "Invisible Infrastructure",