@hotmeshio/hotmesh 0.22.1 → 0.22.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.
@@ -7,6 +7,7 @@ const utils_1 = require("../../modules/utils");
7
7
  const hotmesh_1 = require("../hotmesh");
8
8
  const key_1 = require("../../modules/key");
9
9
  const types_1 = require("../../types");
10
+ const client_1 = require("../escalations/client");
10
11
  const search_1 = require("./search");
11
12
  const handle_1 = require("./handle");
12
13
  const factory_1 = require("./schemas/factory");
@@ -284,351 +285,11 @@ class ClientService {
284
285
  }
285
286
  },
286
287
  };
287
- /**
288
- * Escalation queue operations over `public.hmsh_escalations` — a global
289
- * table that surfaces workflow signal pauses as role-based, claimable,
290
- * searchable queue items.
291
- *
292
- * When a YAML `hook` activity suspends with an `escalation:` block, or
293
- * `Durable.workflow.condition(signalId, config)` fires, **one row is
294
- * written atomically** with the workflow checkpoint — no enrichment step,
295
- * no secondary round-trip. Every connected app shares the same table;
296
- * rows are namespaced by `namespace` + `app_id`.
297
- *
298
- * **Status lifecycle:**
299
- * ```
300
- * pending → claimed → resolved
301
- * ↘ cancelled (any non-terminal state)
302
- * ↗ pending (via release or releaseExpired)
303
- * ```
304
- *
305
- * **Typical human-in-the-loop flow:**
306
- * ```typescript
307
- * // 1. Workflow pauses and writes the escalation row automatically
308
- * const decision = await Durable.workflow.condition('manager-approval', {
309
- * role: 'manager',
310
- * type: 'order-approval',
311
- * priority: 2,
312
- * metadata: { orderId },
313
- * });
314
- *
315
- * // 2. Dashboard lists pending approvals for this role
316
- * const [item] = await client.escalations.list({ role: 'manager', status: 'pending' });
317
- *
318
- * // 3. Reviewer claims it (sets assigned_to + expiry)
319
- * await client.escalations.claim({ id: item.id, assignee: 'alice@company.com' });
320
- *
321
- * // 4. Resolve atomically marks it resolved AND delivers the signal
322
- * await client.escalations.resolve({
323
- * id: item.id,
324
- * resolverPayload: { approved: true },
325
- * });
326
- * // workflow resumes with { approved: true }
327
- * ```
328
- */
329
- this.escalations = {
330
- /**
331
- * Returns all escalation rows matching the given filters.
332
- *
333
- * @example
334
- * ```typescript
335
- * // All pending approvals for the manager role
336
- * const items = await client.escalations.list({ role: 'manager', status: 'pending' });
337
- *
338
- * // By workflow ID
339
- * const items = await client.escalations.list({ workflowId: 'order-123' });
340
- * ```
341
- */
342
- list: async (params) => {
343
- const hotMeshClient = await this.getHotMeshClient(null, params?.namespace);
344
- return hotMeshClient.engine.store.listEscalations(params ?? {});
345
- },
346
- /**
347
- * Returns a single escalation row by its UUID primary key.
348
- * Returns `null` if not found.
349
- */
350
- get: async (id, namespace) => {
351
- const hotMeshClient = await this.getHotMeshClient(null, namespace);
352
- return hotMeshClient.engine.store.getEscalation(id, namespace);
353
- },
354
- /**
355
- * Looks up an escalation row by its `signal_key` — the value that was
356
- * passed to `condition()` or stored in the hook activity's collation rule.
357
- * This is the same key used to deliver the signal via `hotMesh.signal()`.
358
- *
359
- * @example
360
- * ```typescript
361
- * const item = await client.escalations.getBySignalKey('manager-approval');
362
- * ```
363
- */
364
- getBySignalKey: async (signalKey, namespace) => {
365
- const hotMeshClient = await this.getHotMeshClient(null, namespace);
366
- return hotMeshClient.engine.store.getEscalationBySignalKey(signalKey, namespace);
367
- },
368
- /**
369
- * Creates a standalone escalation row that is **not** backed by a signal.
370
- * `signal_key` is `null`. Useful for external task tracking that doesn't
371
- * need to resume a workflow (e.g., audit tasks, out-of-band approvals).
372
- *
373
- * @example
374
- * ```typescript
375
- * const entry = await client.escalations.create({
376
- * role: 'support',
377
- * type: 'data-correction',
378
- * description: 'Fix the customer address',
379
- * metadata: { customerId: 'cust-42' },
380
- * });
381
- * ```
382
- */
383
- create: async (params) => {
384
- const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
385
- return hotMeshClient.engine.store.createEscalation(params);
386
- },
387
- /**
388
- * Patches an existing escalation row. All fields are optional — only
389
- * provided fields are written. `metadata` is **merged**, not replaced.
390
- *
391
- * Signal routing fields (`signalKey`, `topic`, `workflowId`, …) can be
392
- * enriched after the row is created — useful when the row is created
393
- * before the workflow starts and routing context is not yet known.
394
- *
395
- * @example
396
- * ```typescript
397
- * await client.escalations.update({
398
- * id: item.id,
399
- * description: 'Updated description',
400
- * metadata: { extraKey: 'value' }, // merged into existing metadata
401
- * });
402
- * ```
403
- */
404
- update: async (params) => {
405
- const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
406
- return hotMeshClient.engine.store.updateEscalation(params);
407
- },
408
- /**
409
- * Appends one or more milestone entries to the escalation's
410
- * `milestones` audit trail array. Milestones are append-only; they
411
- * record events like state transitions, reviewer notes, or external
412
- * system callbacks.
413
- *
414
- * @example
415
- * ```typescript
416
- * await client.escalations.appendMilestones({
417
- * id: item.id,
418
- * milestones: [{ at: new Date().toISOString(), by: 'alice', note: 'Reviewed' }],
419
- * });
420
- * ```
421
- */
422
- appendMilestones: async (params) => {
423
- const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
424
- return hotMeshClient.engine.store.appendEscalationMilestones(params);
425
- },
426
- /**
427
- * Atomically claims an escalation row by UUID. Sets `assigned_to`,
428
- * `claimed_at`, and `claim_expires_at`. Returns `conflict` if another
429
- * actor already holds the claim.
430
- *
431
- * @example
432
- * ```typescript
433
- * const result = await client.escalations.claim({
434
- * id: item.id,
435
- * assignee: 'alice@company.com',
436
- * durationMinutes: 30,
437
- * });
438
- * if (!result.ok) console.warn('Already claimed by someone else');
439
- * ```
440
- */
441
- claim: async (params) => {
442
- const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
443
- return hotMeshClient.engine.store.claimEscalation(params);
444
- },
445
- /**
446
- * Atomically claims the highest-priority pending escalation whose
447
- * `metadata` contains the given key/value pair. Uses
448
- * `FOR UPDATE SKIP LOCKED` so concurrent callers never double-claim.
449
- *
450
- * Returns `candidatesExist` to distinguish two cases:
451
- * - `not-found, candidatesExist: 0` — no rows matched the metadata filter
452
- * - `conflict, candidatesExist: N` — matching rows exist but all are claimed
453
- *
454
- * @example
455
- * ```typescript
456
- * const result = await client.escalations.claimByMetadata({
457
- * key: 'region',
458
- * value: 'west',
459
- * assignee: 'bob@company.com',
460
- * roles: ['manager'],
461
- * });
462
- * if (result.ok) console.log('Claimed:', result.entry.id);
463
- * ```
464
- */
465
- claimByMetadata: async (params) => {
466
- const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
467
- return hotMeshClient.engine.store.claimEscalationByMetadata(params);
468
- },
469
- /**
470
- * Releases a claimed escalation, returning it to `pending` status and
471
- * clearing `assigned_to` and `claim_expires_at`. The row is immediately
472
- * available for other actors to claim.
473
- *
474
- * @example
475
- * ```typescript
476
- * await client.escalations.release({ id: item.id });
477
- * ```
478
- */
479
- release: async (params) => {
480
- const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
481
- return hotMeshClient.engine.store.releaseEscalation(params);
482
- },
483
- /**
484
- * Reassigns the escalation to a different role, clearing any current
485
- * claim and returning status to `pending`. Use when an escalation must
486
- * be handled by a different team or tier.
487
- *
488
- * @example
489
- * ```typescript
490
- * await client.escalations.escalateToRole({ id: item.id, role: 'senior-manager' });
491
- * ```
492
- */
493
- escalateToRole: async (params) => {
494
- const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
495
- return hotMeshClient.engine.store.escalateEscalationToRole(params);
496
- },
497
- /**
498
- * Terminates the escalation without delivering a signal. Rows in
499
- * `pending` or `claimed` state move to `cancelled`. Terminal rows
500
- * (`resolved`, `cancelled`) return `already-terminal`.
501
- *
502
- * @example
503
- * ```typescript
504
- * await client.escalations.cancel(item.id);
505
- * ```
506
- */
507
- cancel: async (id, namespace) => {
508
- const hotMeshClient = await this.getHotMeshClient(null, namespace);
509
- return hotMeshClient.engine.store.cancelEscalation(id, namespace);
510
- },
511
- /**
512
- * Atomically marks the escalation `resolved` **and** delivers the
513
- * signal to the waiting workflow — one round-trip, no separate
514
- * `signal()` call required. If `signal_key` is null (standalone
515
- * escalation), only the row is updated.
516
- *
517
- * @example
518
- * ```typescript
519
- * const result = await client.escalations.resolve({
520
- * id: item.id,
521
- * resolverPayload: { approved: true, note: 'LGTM' },
522
- * });
523
- * if (!result.ok) console.error(result.reason); // 'not-found' | 'already-resolved' | 'signal-failed'
524
- * // workflow resumes with { approved: true, note: 'LGTM' }
525
- * ```
526
- */
527
- resolve: async (params, namespace) => {
528
- const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
529
- const hotMeshClient = await this.getHotMeshClient(null, ns);
530
- const store = hotMeshClient.engine.store;
531
- // store.resolveEscalation() uses FOR UPDATE inside its CTE, serializing concurrent
532
- // callers at the DB level. The second caller blocks on the row lock, reads
533
- // 'already-resolved' after the first commits, and returns — no signal is queued.
534
- // This eliminates the double-signal race that the previous KVTransaction approach
535
- // had: two concurrent callers could both pass the unlocked getEscalation() read,
536
- // both queue signal INSERTs, and both commit them — even though only one UPDATE won.
537
- const dbResult = await store.resolveEscalation({
538
- id: params.id,
539
- resolverPayload: params.resolverPayload,
540
- // namespace intentionally omitted — UUID lookup; passing ns would miss rows
541
- // stored under namespace 'hmsh' (the engine default) when ns = APP_ID = 'durable'.
542
- });
543
- if (!dbResult.ok)
544
- return dbResult;
545
- if (dbResult.signalKey) {
546
- const signalPayload = { id: dbResult.signalKey, data: params.resolverPayload ?? {} };
547
- const delivered = await this._deliverEscalationSignal(ns, dbResult.topic, signalPayload);
548
- if (!delivered)
549
- return { ok: false, reason: 'signal-failed' };
550
- }
551
- return { ok: true };
552
- },
553
- /**
554
- * Resolves the highest-priority matching escalation by metadata filter,
555
- * then delivers its signal. Identical semantics to `resolve()` but
556
- * selects the target row by metadata key/value instead of UUID.
557
- *
558
- * @example
559
- * ```typescript
560
- * await client.escalations.resolveByMetadata({
561
- * key: 'orderId',
562
- * value: 'order-123',
563
- * resolverPayload: { approved: true },
564
- * });
565
- * ```
566
- */
567
- resolveByMetadata: async (params, namespace) => {
568
- const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
569
- const hotMeshClient = await this.getHotMeshClient(null, ns);
570
- const store = hotMeshClient.engine.store;
571
- // Same FOR UPDATE CTE serialization as resolve(). Metadata filter selects
572
- // the highest-priority matching row; the lock prevents concurrent callers
573
- // from both resolving it.
574
- const dbResult = await store.resolveEscalationByMetadata({
575
- key: params.key,
576
- value: params.value,
577
- resolverPayload: params.resolverPayload,
578
- roles: params.roles,
579
- });
580
- if (!dbResult.ok)
581
- return dbResult;
582
- if (dbResult.signalKey) {
583
- const signalPayload = { id: dbResult.signalKey, data: params.resolverPayload ?? {} };
584
- const delivered = await this._deliverEscalationSignal(ns, dbResult.topic, signalPayload);
585
- if (!delivered)
586
- return { ok: false, reason: 'signal-failed' };
587
- }
588
- return { ok: true };
589
- },
590
- /**
591
- * Full-fidelity migration: inserts an escalation row preserving the original
592
- * UUID and all lifecycle state. Returns the inserted row, or `null` if the
593
- * UUID already exists (idempotent — safe to call multiple times with the same
594
- * `params.id`). Use this to migrate rows from a legacy escalation table to
595
- * `hmsh_escalations` without losing original IDs or state.
596
- *
597
- * @example
598
- * ```typescript
599
- * const entry = await client.escalations.migrate({
600
- * id: 'original-uuid',
601
- * status: 'resolved',
602
- * resolvedAt: new Date('2025-01-01'),
603
- * type: 'order-approval',
604
- * role: 'approver',
605
- * });
606
- * // null on subsequent calls with the same id — idempotent
607
- * ```
608
- */
609
- migrate: async (params, namespace) => {
610
- const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
611
- const hotMeshClient = await this.getHotMeshClient(null, ns);
612
- return hotMeshClient.engine.store.createEscalationForMigration(params);
613
- },
614
- /**
615
- * Releases all claimed escalations whose `claim_expires_at` has lapsed,
616
- * returning them to `pending` so they can be claimed again. Returns the
617
- * number of rows released. Call periodically from a maintenance job or
618
- * cron to prevent stale claims from blocking the queue.
619
- *
620
- * @example
621
- * ```typescript
622
- * const released = await client.escalations.releaseExpired();
623
- * console.log(`Released ${released} expired claims`);
624
- * ```
625
- */
626
- releaseExpired: async (namespace) => {
627
- const hotMeshClient = await this.getHotMeshClient(null, namespace);
628
- return hotMeshClient.engine.store.releaseExpiredEscalations(namespace);
629
- },
630
- };
631
288
  this.connection = config.connection;
289
+ // Inject our getHotMeshClient so the escalation client shares the same engine pool.
290
+ this.escalations = new client_1.EscalationClientService({
291
+ getHotMeshClient: this.getHotMeshClient.bind(this),
292
+ });
632
293
  }
633
294
  hashOptions() {
634
295
  if ('options' in this.connection) {
@@ -703,46 +364,6 @@ class ClientService {
703
364
  }
704
365
  }
705
366
  }
706
- /**
707
- * Delivers a signal to the registered escalation topic.
708
- *
709
- * When the topic is known (stored at condition() time), we deliver only to that
710
- * topic. This avoids writing a stale pending entry to the alternate stream, which
711
- * would interfere with concurrent consumers in other workflows or tests.
712
- *
713
- * When topic is null (legacy/standalone rows with no registered topic), we try
714
- * both wfs.signal and wfs.wait unconditionally — engine.signal() on the wrong
715
- * topic stores pending without throwing, so a sequential fallback would skip
716
- * the second topic and leave single-condition workflows permanently suspended.
717
- *
718
- * @private
719
- */
720
- async _deliverEscalationSignal(ns, topic, signalPayload) {
721
- if (topic) {
722
- // Registered topic known — deliver precisely, no stream pollution.
723
- try {
724
- const tc = await this.getHotMeshClient(topic, ns);
725
- await tc.engine.signal(topic, signalPayload, types_1.StreamStatus.SUCCESS, 200);
726
- return true;
727
- }
728
- catch { /* topic not currently registered — fall through */ }
729
- }
730
- // Topic unknown or primary delivery failed — try both topics unconditionally.
731
- let delivered = false;
732
- try {
733
- const sc = await this.getHotMeshClient(`${ns}.wfs.signal`, ns);
734
- await sc.engine.signal(`${ns}.wfs.signal`, signalPayload, types_1.StreamStatus.SUCCESS, 200);
735
- delivered = true;
736
- }
737
- catch { /* no collator hook rule for this workflow */ }
738
- try {
739
- const wc = await this.getHotMeshClient(`${ns}.wfs.wait`, ns);
740
- await wc.engine.signal(`${ns}.wfs.wait`, signalPayload, types_1.StreamStatus.SUCCESS, 200);
741
- delivered = true;
742
- }
743
- catch { /* no waiter hook rule for this workflow */ }
744
- return delivered;
745
- }
746
367
  /**
747
368
  * @private
748
369
  */
@@ -0,0 +1,168 @@
1
+ import { HotMesh } from '../hotmesh';
2
+ import { Connection } from '../../types/durable';
3
+ import { EscalationEntry, ClaimEscalationResult, ClaimByMetadataResult, ReleaseEscalationResult, ResolveEscalationResult, CancelEscalationResult, ListEscalationsParams, StatsEscalationsParams, EscalationStats, CreateEscalationParams, UpdateEscalationParams, AppendMilestonesParams, ClaimEscalationParams, ClaimByMetadataParams, ReleaseEscalationParams, ResolveEscalationParams, ResolveByMetadataParams, EscalateToRoleParams, MigrateEscalationParams, ClaimManyParams, EscalateManyToRoleParams, UpdateManyPriorityParams, ResolveManyParams } from '../../types/hmsh_escalations';
4
+ export type GetHotMeshFn = (topic: string | null, namespace?: string) => Promise<HotMesh>;
5
+ export interface EscalationClientConfig {
6
+ /** Postgres connection options — used when creating a standalone EscalationClient. */
7
+ connection?: Connection;
8
+ /**
9
+ * Inject a pre-existing `getHotMeshClient` function (e.g. from Durable.Client).
10
+ * When provided, the client reuses the caller's engine pool — no extra connections.
11
+ */
12
+ getHotMeshClient?: GetHotMeshFn;
13
+ }
14
+ /**
15
+ * Standalone client for the `public.hmsh_escalations` signal-pause surface.
16
+ *
17
+ * Requires NO dependency on `services/durable/`. Any HotMesh consumer — AI
18
+ * agent, YAML DAG worker, REST API — can interact with the escalation queue
19
+ * directly with just a Postgres connection.
20
+ *
21
+ * Signal delivery (for `resolve()` / `resolveByMetadata()`) uses HotMesh's
22
+ * `engine.signal()` internally. The engine is initialised lazily on first use
23
+ * and cached for the lifetime of the process.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * import { Escalations } from '@hotmeshio/hotmesh';
28
+ * import { Client as Postgres } from 'pg';
29
+ *
30
+ * const client = new Escalations.Client({
31
+ * connection: {
32
+ * class: Postgres,
33
+ * options: { connectionString: 'postgresql://usr:pwd@localhost:5432/db' },
34
+ * },
35
+ * });
36
+ *
37
+ * // Claim the next available approval for the 'manager' role
38
+ * const result = await client.claimByMetadata({
39
+ * key: 'orderId', value: 'order-123',
40
+ * assignee: 'alice@company.com',
41
+ * roles: ['manager'],
42
+ * });
43
+ *
44
+ * if (result.ok) {
45
+ * await client.resolve({ id: result.entry.id, resolverPayload: { approved: true } });
46
+ * }
47
+ * ```
48
+ */
49
+ export declare class EscalationClientService {
50
+ private readonly _engine;
51
+ static instances: Map<string, HotMesh | Promise<HotMesh>>;
52
+ constructor(config?: EscalationClientConfig);
53
+ private _makeEngineFactory;
54
+ private _hashConnection;
55
+ private _deliverEscalationSignal;
56
+ /**
57
+ * Returns all escalation rows matching the given filters. Each row includes
58
+ * a computed `available` field (true = claimable). Supports `sortBy`,
59
+ * `sortOrder`, `orderBy[]`, and multi-role `roles[]` filter.
60
+ */
61
+ list(params?: ListEscalationsParams): Promise<EscalationEntry[]>;
62
+ /**
63
+ * Returns the count of escalation rows matching the given filters.
64
+ * Uses the same filter parameters as `list()`.
65
+ */
66
+ count(params?: ListEscalationsParams): Promise<number>;
67
+ /** Returns a single escalation row by UUID. Returns `null` if not found. */
68
+ get(id: string, namespace?: string): Promise<EscalationEntry | null>;
69
+ /** Looks up an escalation by `signal_key` — the value passed to `condition()`. */
70
+ getBySignalKey(signalKey: string, namespace?: string): Promise<EscalationEntry | null>;
71
+ /**
72
+ * Creates a standalone escalation row with `signal_key = null`.
73
+ * Useful for external task tracking that doesn't need to resume a workflow.
74
+ */
75
+ create(params: CreateEscalationParams): Promise<EscalationEntry>;
76
+ /**
77
+ * Patches an existing escalation row. `metadata` is merged, not replaced.
78
+ * Signal routing fields can be enriched after creation.
79
+ */
80
+ update(params: UpdateEscalationParams): Promise<EscalationEntry | null>;
81
+ /** Appends milestone entries to the escalation's audit trail. */
82
+ appendMilestones(params: AppendMilestonesParams): Promise<EscalationEntry | null>;
83
+ /**
84
+ * Atomically claims an escalation by UUID. Implicit model: `status` stays
85
+ * `'pending'`; claim is expressed via `assigned_to` + `assigned_until`.
86
+ * Returns `isExtension: true` when the same assignee re-claims a row they already hold.
87
+ */
88
+ claim(params: ClaimEscalationParams): Promise<ClaimEscalationResult>;
89
+ /**
90
+ * Atomically claims the highest-priority pending escalation whose `metadata`
91
+ * contains the given key/value. Optionally merges `metadata` into the claimed row
92
+ * in the same atomic UPDATE. Returns `isExtension: true` when the same assignee
93
+ * re-claims a row they already hold (extends the expiry).
94
+ */
95
+ claimByMetadata(params: ClaimByMetadataParams): Promise<ClaimByMetadataResult>;
96
+ /** Releases a claimed escalation, returning it to available status. */
97
+ release(params: ReleaseEscalationParams): Promise<ReleaseEscalationResult>;
98
+ /**
99
+ * Reassigns the escalation to a different role, clearing any current claim
100
+ * and resetting status to `'pending'`.
101
+ */
102
+ escalateToRole(params: EscalateToRoleParams): Promise<EscalationEntry | null>;
103
+ /**
104
+ * Cancels a pending escalation without delivering a signal. Terminal rows
105
+ * return `already-terminal`.
106
+ */
107
+ cancel(id: string, namespace?: string): Promise<CancelEscalationResult>;
108
+ /**
109
+ * Resolves a pending escalation by UUID. Uses an explicit Postgres transaction
110
+ * with FOR UPDATE + WHERE guard: only one concurrent caller can commit the
111
+ * status change; the committed resolved row with its `signal_key` is the
112
+ * durable proof. Signal delivery is best-effort post-commit — the resolved
113
+ * row is the recovery record for any missed delivery. Returns the updated
114
+ * row as `entry` on success.
115
+ */
116
+ resolve(params: ResolveEscalationParams, namespace?: string): Promise<ResolveEscalationResult>;
117
+ /**
118
+ * Resolves the highest-priority matching escalation by metadata filter,
119
+ * then delivers its signal. Same transaction + WHERE guard semantics as `resolve()`.
120
+ */
121
+ resolveByMetadata(params: ResolveByMetadataParams, namespace?: string): Promise<ResolveEscalationResult>;
122
+ /**
123
+ * Full-fidelity migration: inserts an escalation row preserving the original
124
+ * UUID and lifecycle state. Returns `null` on duplicate (idempotent).
125
+ */
126
+ migrate(params: MigrateEscalationParams, namespace?: string): Promise<EscalationEntry | null>;
127
+ /**
128
+ * No-op in the implicit claim model — availability is computed at query time
129
+ * from `assigned_until`. Kept for API compatibility.
130
+ */
131
+ releaseExpired(namespace?: string): Promise<number>;
132
+ /**
133
+ * Bulk-claims up to `ids.length` pending escalations in one statement.
134
+ * Returns `{ claimed, skipped }` — skipped rows are either already claimed
135
+ * by another assignee or non-existent. Implicit-claim semantics apply.
136
+ */
137
+ claimMany(params: ClaimManyParams): Promise<{
138
+ claimed: number;
139
+ skipped: number;
140
+ }>;
141
+ /**
142
+ * Bulk-reassigns pending escalations to a new role, clearing any current claim.
143
+ * Returns the count of rows updated.
144
+ */
145
+ escalateManyToRole(params: EscalateManyToRoleParams): Promise<number>;
146
+ /**
147
+ * Bulk-updates priority for pending escalations. Returns the count of rows updated.
148
+ */
149
+ updateManyPriority(params: UpdateManyPriorityParams): Promise<number>;
150
+ /**
151
+ * Bulk-resolves pending escalations by id-set. No signal delivery — intended
152
+ * for redirect-to-triage flows where no workflow is waiting. Returns the
153
+ * resolved rows.
154
+ */
155
+ resolveMany(params: ResolveManyParams): Promise<EscalationEntry[]>;
156
+ /**
157
+ * Returns dashboard-ready escalation counts. `period` controls the window
158
+ * used for `created` and `resolved` counts (default `'24h'`). When `roles`
159
+ * is an empty array, all counts are zero (RBAC guard).
160
+ */
161
+ stats(params?: StatsEscalationsParams): Promise<EscalationStats>;
162
+ /**
163
+ * Returns the sorted list of distinct `type` values in the escalations table.
164
+ * Useful for populating filter dropdowns.
165
+ */
166
+ listDistinctTypes(namespace?: string): Promise<string[]>;
167
+ static shutdown(): Promise<void>;
168
+ }