@hotmeshio/long-tail 0.5.1 → 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 (32) hide show
  1. package/build/adapters/express.d.ts +1 -0
  2. package/build/adapters/express.js +4 -0
  3. package/build/lib/events/system-events.d.ts +19 -0
  4. package/build/lib/events/system-events.js +62 -0
  5. package/build/modules/ltconfig.d.ts +8 -0
  6. package/build/modules/ltconfig.js +10 -0
  7. package/build/services/escalation/bulk.d.ts +2 -1
  8. package/build/services/escalation/bulk.js +20 -19
  9. package/build/services/escalation/client.d.ts +22 -0
  10. package/build/services/escalation/client.js +141 -0
  11. package/build/services/escalation/crud.d.ts +29 -21
  12. package/build/services/escalation/crud.js +175 -140
  13. package/build/services/escalation/index.d.ts +1 -0
  14. package/build/services/escalation/index.js +3 -0
  15. package/build/services/escalation/map.d.ts +15 -0
  16. package/build/services/escalation/map.js +63 -0
  17. package/build/services/escalation/queries.js +64 -149
  18. package/build/services/escalation/sql.d.ts +13 -32
  19. package/build/services/escalation/sql.js +36 -176
  20. package/build/services/export/post-process.js +23 -4
  21. package/build/services/interceptor/activities/config.js +5 -1
  22. package/build/services/interceptor/index.d.ts +3 -0
  23. package/build/services/interceptor/index.js +7 -21
  24. package/build/services/mcp/db-server/schemas.d.ts +1 -1
  25. package/build/services/yaml-workflow/deployer.js +4 -0
  26. package/build/services/yaml-workflow/workers/register.js +3 -0
  27. package/build/start/index.js +2 -1
  28. package/build/start/workers.js +12 -0
  29. package/build/system/mcp-servers/admin/schemas.d.ts +1 -1
  30. package/build/system/mcp-servers/db-query/schemas.d.ts +1 -1
  31. package/build/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +2 -2
@@ -59,6 +59,7 @@ export declare class LTExpressAdapter {
59
59
  /**
60
60
  * Return a self-contained Express Router that serves:
61
61
  * - `/api/*` — Long Tail API routes (auth, tasks, escalations, etc.)
62
+ * - `/mcp` — MCP streamable-HTTP transport (Claude Desktop, Cursor, agents)
62
63
  * - `/health` — health check
63
64
  * - Static dashboard assets
64
65
  * - SPA fallback with injected `<base href>` and `window.__LT_BASE__`
@@ -41,6 +41,7 @@ const fs_1 = require("fs");
41
41
  const path_1 = __importDefault(require("path"));
42
42
  const express_1 = __importStar(require("express"));
43
43
  const routes_1 = __importDefault(require("../routes"));
44
+ const mcp_endpoint_1 = __importDefault(require("../routes/mcp-endpoint"));
44
45
  const events_1 = require("../lib/events");
45
46
  const socketio_1 = require("../lib/events/socketio");
46
47
  const nats_1 = require("../lib/events/nats");
@@ -134,6 +135,7 @@ class LTExpressAdapter {
134
135
  /**
135
136
  * Return a self-contained Express Router that serves:
136
137
  * - `/api/*` — Long Tail API routes (auth, tasks, escalations, etc.)
138
+ * - `/mcp` — MCP streamable-HTTP transport (Claude Desktop, Cursor, agents)
137
139
  * - `/health` — health check
138
140
  * - Static dashboard assets
139
141
  * - SPA fallback with injected `<base href>` and `window.__LT_BASE__`
@@ -147,6 +149,8 @@ class LTExpressAdapter {
147
149
  });
148
150
  // API routes — internal routes handle their own JWT auth
149
151
  router.use('/api', routes_1.default);
152
+ // MCP streamable-HTTP transport — same endpoint as standalone server
153
+ router.use('/mcp', mcp_endpoint_1.default);
150
154
  // Dashboard static assets
151
155
  const dashboardDist = this.resolveDashboardDist();
152
156
  if (dashboardDist) {
@@ -0,0 +1,19 @@
1
+ import type { Types } from '@hotmeshio/hotmesh';
2
+ import type { LTEvent } from '../../types';
3
+ type SystemEvent = Types.SystemEvent;
4
+ /**
5
+ * Translate a HotMesh `SystemEvent` into long-tail's `LTEvent`. Escalation
6
+ * events carry the full committed row in `data`, from which we lift the routing
7
+ * fields; engine/worker lifecycle events pass through with their canonical type.
8
+ */
9
+ export declare function mapSystemEvent(event: SystemEvent): LTEvent;
10
+ /**
11
+ * The `EventsConfig.publish` hook long-tail wires into every worker/engine it
12
+ * constructs. Fire-and-forget — never throws back into the SDK's committed call.
13
+ */
14
+ export declare function onSystemEvent(event: SystemEvent): void;
15
+ /** The EventsConfig long-tail passes to Durable.Client / Worker.create / HotMesh.init. */
16
+ export declare const systemEventsConfig: {
17
+ publish: typeof onSystemEvent;
18
+ };
19
+ export {};
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.systemEventsConfig = void 0;
4
+ exports.mapSystemEvent = mapSystemEvent;
5
+ exports.onSystemEvent = onSystemEvent;
6
+ const index_1 = require("./index");
7
+ /** Verb → long-tail event status, matching the manual publishEscalationEvent convention. */
8
+ const ESCALATION_STATUS_BY_VERB = {
9
+ created: 'pending',
10
+ claimed: 'claimed',
11
+ released: 'released',
12
+ reassigned: 'pending',
13
+ resolved: 'resolved',
14
+ cancelled: 'cancelled',
15
+ };
16
+ /**
17
+ * Translate a HotMesh `SystemEvent` into long-tail's `LTEvent`. Escalation
18
+ * events carry the full committed row in `data`, from which we lift the routing
19
+ * fields; engine/worker lifecycle events pass through with their canonical type.
20
+ */
21
+ function mapSystemEvent(event) {
22
+ const segments = event.type.split('.');
23
+ const domain = segments[1];
24
+ if (domain === 'escalation') {
25
+ const row = (event.data ?? {});
26
+ const verb = segments[3] ?? '';
27
+ return {
28
+ type: event.type,
29
+ source: 'sdk',
30
+ workflowId: row.workflow_id || event.workflow_id || '',
31
+ workflowName: row.workflow_type || '',
32
+ taskQueue: row.task_queue || '',
33
+ escalationId: row.id || segments[2],
34
+ originId: row.origin_id || event.origin_id || undefined,
35
+ status: ESCALATION_STATUS_BY_VERB[verb] ?? verb,
36
+ data: row,
37
+ timestamp: event.ts,
38
+ };
39
+ }
40
+ // Engine / worker lifecycle (system.engine.*, system.worker.*) — additive
41
+ // observability; pass through with the canonical type and metadata payload.
42
+ return {
43
+ type: event.type,
44
+ source: 'sdk',
45
+ workflowId: event.workflow_id || '',
46
+ workflowName: '',
47
+ taskQueue: event.data?.taskQueue || '',
48
+ data: event.data,
49
+ timestamp: event.ts,
50
+ };
51
+ }
52
+ /**
53
+ * The `EventsConfig.publish` hook long-tail wires into every worker/engine it
54
+ * constructs. Fire-and-forget — never throws back into the SDK's committed call.
55
+ */
56
+ function onSystemEvent(event) {
57
+ if (!index_1.eventRegistry.hasAdapters)
58
+ return;
59
+ void index_1.eventRegistry.publish(mapSystemEvent(event)).catch(() => { });
60
+ }
61
+ /** The EventsConfig long-tail passes to Durable.Client / Worker.create / HotMesh.init. */
62
+ exports.systemEventsConfig = { publish: onSystemEvent };
@@ -23,6 +23,14 @@ declare class LTConfigCache {
23
23
  * return null so the interceptor skips them.
24
24
  */
25
25
  getResolvedConfig(name: string): Promise<LTResolvedConfig | null>;
26
+ /**
27
+ * Get the config for any workflow REGISTERED in lt_config_workflows (a row
28
+ * exists), regardless of certification (roles/consumes). The interceptor
29
+ * uses this to decide whether to apply task tracking, escalation handling,
30
+ * and orchestrator context — every registered workflow gets the full
31
+ * treatment; only unregistered ad-hoc durable workflows are skipped.
32
+ */
33
+ getRegisteredConfig(name: string): Promise<LTResolvedConfig | null>;
26
34
  }
27
35
  export declare const ltConfig: LTConfigCache;
28
36
  export {};
@@ -116,5 +116,15 @@ class LTConfigCache {
116
116
  const isCertified = (config.roles?.length ?? 0) > 0 || (config.consumes?.length ?? 0) > 0;
117
117
  return isCertified ? config : null;
118
118
  }
119
+ /**
120
+ * Get the config for any workflow REGISTERED in lt_config_workflows (a row
121
+ * exists), regardless of certification (roles/consumes). The interceptor
122
+ * uses this to decide whether to apply task tracking, escalation handling,
123
+ * and orchestrator context — every registered workflow gets the full
124
+ * treatment; only unregistered ad-hoc durable workflows are skipped.
125
+ */
126
+ async getRegisteredConfig(name) {
127
+ return (await this.get(name)) ?? null;
128
+ }
119
129
  }
120
130
  exports.ltConfig = new LTConfigCache();
@@ -22,6 +22,7 @@ export declare function bulkAssignEscalations(ids: string[], targetUserId: strin
22
22
  export declare function bulkEscalateToRole(ids: string[], targetRole: string): Promise<number>;
23
23
  /**
24
24
  * Bulk resolve escalations for AI triage.
25
- * Returns full records so the caller can start triage workflows.
25
+ * Returns full records so the caller can start triage workflows. No signal is
26
+ * delivered — the triage workflow takes over handling.
26
27
  */
27
28
  export declare function bulkResolveForTriage(ids: string[], hint?: string): Promise<LTEscalationRecord[]>;
@@ -4,8 +4,8 @@ exports.bulkClaimEscalations = bulkClaimEscalations;
4
4
  exports.bulkAssignEscalations = bulkAssignEscalations;
5
5
  exports.bulkEscalateToRole = bulkEscalateToRole;
6
6
  exports.bulkResolveForTriage = bulkResolveForTriage;
7
- const db_1 = require("../../lib/db");
8
- const sql_1 = require("./sql");
7
+ const client_1 = require("./client");
8
+ const map_1 = require("./map");
9
9
  /**
10
10
  * Bulk claim escalations for a user.
11
11
  * Items already claimed by another active user are skipped.
@@ -13,10 +13,8 @@ const sql_1 = require("./sql");
13
13
  async function bulkClaimEscalations(ids, userId, durationMinutes = 30) {
14
14
  if (ids.length === 0)
15
15
  return { claimed: 0, skipped: 0 };
16
- const pool = (0, db_1.getPool)();
17
- const { rowCount } = await pool.query(sql_1.BULK_CLAIM, [userId, durationMinutes, ids]);
18
- const claimed = rowCount ?? 0;
19
- return { claimed, skipped: ids.length - claimed };
16
+ const client = await (0, client_1.escalations)();
17
+ return client.claimMany({ ids, assignee: userId, durationMinutes });
20
18
  }
21
19
  /**
22
20
  * Bulk assign escalations to a specific user (admin action).
@@ -25,10 +23,13 @@ async function bulkClaimEscalations(ids, userId, durationMinutes = 30) {
25
23
  async function bulkAssignEscalations(ids, targetUserId, durationMinutes = 30) {
26
24
  if (ids.length === 0)
27
25
  return { assigned: 0, skipped: 0 };
28
- const pool = (0, db_1.getPool)();
29
- const { rowCount } = await pool.query(sql_1.BULK_ASSIGN, [targetUserId, durationMinutes, ids]);
30
- const assigned = rowCount ?? 0;
31
- return { assigned, skipped: ids.length - assigned };
26
+ const client = await (0, client_1.escalations)();
27
+ const { claimed, skipped } = await client.claimMany({
28
+ ids,
29
+ assignee: targetUserId,
30
+ durationMinutes,
31
+ });
32
+ return { assigned: claimed, skipped };
32
33
  }
33
34
  /**
34
35
  * Bulk reassign escalations to a different role.
@@ -37,21 +38,21 @@ async function bulkAssignEscalations(ids, targetUserId, durationMinutes = 30) {
37
38
  async function bulkEscalateToRole(ids, targetRole) {
38
39
  if (ids.length === 0)
39
40
  return 0;
40
- const pool = (0, db_1.getPool)();
41
- const { rowCount } = await pool.query(sql_1.BULK_ESCALATE_TO_ROLE, [targetRole, ids]);
42
- return rowCount ?? 0;
41
+ const client = await (0, client_1.escalations)();
42
+ return client.escalateManyToRole({ ids, targetRole });
43
43
  }
44
44
  /**
45
45
  * Bulk resolve escalations for AI triage.
46
- * Returns full records so the caller can start triage workflows.
46
+ * Returns full records so the caller can start triage workflows. No signal is
47
+ * delivered — the triage workflow takes over handling.
47
48
  */
48
49
  async function bulkResolveForTriage(ids, hint) {
49
50
  if (ids.length === 0)
50
51
  return [];
51
- const pool = (0, db_1.getPool)();
52
- const resolverPayload = JSON.stringify({
53
- _lt: { needsTriage: true, ...(hint ? { hint } : {}) },
52
+ const client = await (0, client_1.escalations)();
53
+ const resolved = await client.resolveMany({
54
+ ids,
55
+ resolverPayload: { _lt: { needsTriage: true, ...(hint ? { hint } : {}) } },
54
56
  });
55
- const { rows } = await pool.query(sql_1.BULK_RESOLVE_FOR_TRIAGE, [resolverPayload, ids]);
56
- return rows;
57
+ return (0, map_1.toEscalationRecords)(resolved);
57
58
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The escalation client, with the `lt_escalations` compatibility view ensured
3
+ * exactly once per process. Every service function awaits this so the view is
4
+ * present before the first read/write in any context (app, route test, or the
5
+ * service-only test that runs `migrate()` without starting workers).
6
+ */
7
+ export declare function escalations(): Promise<import("@hotmeshio/hotmesh/build/services/escalations").EscalationClientService>;
8
+ /**
9
+ * Replace the legacy `lt_escalations` table with a view over
10
+ * `public.hmsh_escalations`. Idempotent and memoized per process.
11
+ *
12
+ * - Migrates any legacy rows into `hmsh_escalations` (no-op on a fresh DB), then
13
+ * RENAMES the legacy table to `lt_escalations_legacy` (a recoverable backup —
14
+ * never dropped here) so the view can take the `lt_escalations` name.
15
+ * - Read-path consumers (role, agent, mcp, overview) and frozen test cleanup
16
+ * (`DELETE FROM lt_escalations`) continue to work unchanged against the view.
17
+ * - The one-time conversion is serialized across concurrent containers with a
18
+ * dedicated Postgres advisory lock, so a multi-container deploy is safe.
19
+ *
20
+ * Safe to call eagerly at startup and lazily on first escalation use.
21
+ */
22
+ export declare function ensureEscalationCompatView(): Promise<void>;
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.escalations = escalations;
4
+ exports.ensureEscalationCompatView = ensureEscalationCompatView;
5
+ const hotmesh_1 = require("@hotmeshio/hotmesh");
6
+ const db_1 = require("../../lib/db");
7
+ const logger_1 = require("../../lib/logger");
8
+ // ---------------------------------------------------------------------------
9
+ // Escalation client — long-tail's service layer talks to the shared
10
+ // `public.hmsh_escalations` table exclusively through `client.escalations.*`
11
+ // (HotMesh 0.22.3). The escalation client is created off a `Durable.Client`,
12
+ // which injects `getHotMeshClient` so the escalation engine pool is shared
13
+ // with the rest of the app and torn down by `Durable.shutdown()`.
14
+ // ---------------------------------------------------------------------------
15
+ let durableClient = null;
16
+ /** The raw `client.escalations` surface over `public.hmsh_escalations`. */
17
+ function rawEscalations() {
18
+ if (!durableClient) {
19
+ durableClient = new hotmesh_1.Durable.Client({ connection: (0, db_1.getConnection)() });
20
+ }
21
+ return durableClient.escalations;
22
+ }
23
+ /**
24
+ * The escalation client, with the `lt_escalations` compatibility view ensured
25
+ * exactly once per process. Every service function awaits this so the view is
26
+ * present before the first read/write in any context (app, route test, or the
27
+ * service-only test that runs `migrate()` without starting workers).
28
+ */
29
+ async function escalations() {
30
+ await ensureEscalationCompatView();
31
+ return rawEscalations();
32
+ }
33
+ let viewReady = null;
34
+ /**
35
+ * Replace the legacy `lt_escalations` table with a view over
36
+ * `public.hmsh_escalations`. Idempotent and memoized per process.
37
+ *
38
+ * - Migrates any legacy rows into `hmsh_escalations` (no-op on a fresh DB), then
39
+ * RENAMES the legacy table to `lt_escalations_legacy` (a recoverable backup —
40
+ * never dropped here) so the view can take the `lt_escalations` name.
41
+ * - Read-path consumers (role, agent, mcp, overview) and frozen test cleanup
42
+ * (`DELETE FROM lt_escalations`) continue to work unchanged against the view.
43
+ * - The one-time conversion is serialized across concurrent containers with a
44
+ * dedicated Postgres advisory lock, so a multi-container deploy is safe.
45
+ *
46
+ * Safe to call eagerly at startup and lazily on first escalation use.
47
+ */
48
+ function ensureEscalationCompatView() {
49
+ if (!viewReady)
50
+ viewReady = installEscalationCompatView();
51
+ return viewReady;
52
+ }
53
+ // Dedicated advisory-lock id for the compat-view conversion. Distinct from
54
+ // migrate()'s lock (8675309) because this step runs after HotMesh engine init,
55
+ // outside the migrate() sequence.
56
+ const COMPAT_VIEW_LOCK_ID = 8675310;
57
+ async function installEscalationCompatView() {
58
+ // Force HotMesh engine init so `public.hmsh_escalations` exists before the
59
+ // view binds to it (kvtables are deployed on first engine use).
60
+ await rawEscalations().get('00000000-0000-0000-0000-000000000000');
61
+ // Serialize the conversion across concurrent containers on a dedicated
62
+ // connection. Only one process performs the migrate+rename; the rest acquire
63
+ // the lock afterward, see the view already in place, and no-op (the DO block
64
+ // is guarded and CREATE OR REPLACE VIEW is idempotent).
65
+ const pool = (0, db_1.getPool)();
66
+ const client = await pool.connect();
67
+ try {
68
+ await client.query('SELECT pg_advisory_lock($1)', [COMPAT_VIEW_LOCK_ID]);
69
+ await client.query(MIGRATE_AND_RENAME_LEGACY_TABLE);
70
+ await client.query(CREATE_COMPAT_VIEW);
71
+ logger_1.loggerRegistry.info('[escalation] lt_escalations compatibility view ensured');
72
+ }
73
+ finally {
74
+ await client.query('SELECT pg_advisory_unlock($1)', [COMPAT_VIEW_LOCK_ID]).catch(() => { });
75
+ client.release();
76
+ }
77
+ }
78
+ // Migrate legacy `lt_escalations` rows into `hmsh_escalations` (idempotent),
79
+ // then preserve the original table as `lt_escalations_legacy` rather than
80
+ // dropping it — the rows survive untouched for verification and rollback; a
81
+ // later explicit migration can drop the backup once the cut is confirmed. Runs
82
+ // only while `lt_escalations` is still a real table; once it is a view this
83
+ // block is skipped. Payload/envelope TEXT columns are cast to JSONB defensively
84
+ // so a malformed value can never abort the upgrade.
85
+ const MIGRATE_AND_RENAME_LEGACY_TABLE = `
86
+ DO $$
87
+ BEGIN
88
+ IF EXISTS (
89
+ SELECT 1 FROM pg_class c
90
+ JOIN pg_namespace n ON n.oid = c.relnamespace
91
+ WHERE c.relname = 'lt_escalations' AND c.relkind = 'r' AND n.nspname = 'public'
92
+ ) THEN
93
+ CREATE OR REPLACE FUNCTION pg_temp.lt_try_jsonb(t text) RETURNS jsonb AS $fn$
94
+ BEGIN
95
+ IF t IS NULL OR t = '' THEN RETURN NULL; END IF;
96
+ RETURN t::jsonb;
97
+ EXCEPTION WHEN others THEN
98
+ RETURN to_jsonb(t);
99
+ END;
100
+ $fn$ LANGUAGE plpgsql IMMUTABLE;
101
+
102
+ INSERT INTO public.hmsh_escalations
103
+ (id, namespace, app_id, type, subtype, description, status, priority,
104
+ task_id, origin_id, parent_id, workflow_id, task_queue, workflow_type,
105
+ role, assigned_to, assigned_until, claim_expires_at, resolved_at, claimed_at,
106
+ created_by, envelope, metadata, escalation_payload, resolver_payload,
107
+ trace_id, span_id, created_at, updated_at)
108
+ SELECT
109
+ id, 'hmsh', 'hmsh', type, subtype, description, status, priority,
110
+ task_id::text, origin_id, parent_id, workflow_id, task_queue, workflow_type,
111
+ role, assigned_to, assigned_until, assigned_until, resolved_at, claimed_at,
112
+ created_by::text,
113
+ pg_temp.lt_try_jsonb(envelope),
114
+ metadata,
115
+ pg_temp.lt_try_jsonb(escalation_payload),
116
+ pg_temp.lt_try_jsonb(resolver_payload),
117
+ trace_id, span_id, created_at, updated_at
118
+ FROM public.lt_escalations
119
+ ON CONFLICT (id) DO NOTHING;
120
+
121
+ -- Preserve the originals as a recoverable backup (rows already migrated).
122
+ -- If a backup already exists from a prior conversion, the current table is
123
+ -- redundant and is dropped instead of clobbering the backup.
124
+ IF EXISTS (
125
+ SELECT 1 FROM pg_class c
126
+ JOIN pg_namespace n ON n.oid = c.relnamespace
127
+ WHERE c.relname = 'lt_escalations_legacy' AND n.nspname = 'public'
128
+ ) THEN
129
+ DROP TABLE public.lt_escalations CASCADE;
130
+ ELSE
131
+ ALTER TABLE public.lt_escalations RENAME TO lt_escalations_legacy;
132
+ END IF;
133
+ END IF;
134
+ END $$;`;
135
+ // `available` mirrors the legacy isEffectivelyClaimed/isAvailable heuristic so
136
+ // existing `SELECT *` consumers are unaffected; the column is additive.
137
+ const CREATE_COMPAT_VIEW = `
138
+ CREATE OR REPLACE VIEW public.lt_escalations AS
139
+ SELECT *,
140
+ (assigned_to IS NULL OR assigned_until IS NULL OR assigned_until <= NOW()) AS available
141
+ FROM public.hmsh_escalations;`;
@@ -2,20 +2,21 @@ import type { LTEscalationRecord } from '../../types';
2
2
  import type { CreateEscalationInput, ClaimResult } from './types';
3
3
  export declare function createEscalation(input: CreateEscalationInput): Promise<LTEscalationRecord>;
4
4
  /**
5
- * Atomic claim operation. Does NOT change status "claimed" is implicit
6
- * via assigned_to + assigned_until > NOW().
7
- *
8
- * Conditions:
9
- * - status = 'pending' (not resolved/cancelled)
10
- * - Either: unassigned, expired claim, or same user (extension)
11
- *
12
- * Uses a CTE to capture the previous state so callers can detect extensions.
5
+ * Atomic claim. Implicit model status stays 'pending'; "claimed" is
6
+ * assigned_to + assigned_until > NOW(). `isExtension` is true when the same
7
+ * user re-claims (extends expiry). Returns null when the row is not claimable.
13
8
  */
14
9
  export declare function claimEscalation(id: string, userId: string, durationMinutes?: number): Promise<ClaimResult | null>;
10
+ /**
11
+ * Mark an escalation resolved. Signal delivery is owned by the resolution
12
+ * orchestrator (api/escalations/resolve.ts); service-created rows have no
13
+ * signal_key, so this never delivers a signal itself. Returns null when the
14
+ * row is missing or already terminal.
15
+ */
15
16
  export declare function resolveEscalation(id: string, resolverPayload: Record<string, any>): Promise<LTEscalationRecord | null>;
16
17
  /**
17
- * Bulk update priority for a set of escalations.
18
- * Only updates pending escalations.
18
+ * Bulk update priority for a set of escalations. Only pending escalations are
19
+ * updated.
19
20
  */
20
21
  export declare function updateEscalationsPriority(ids: string[], priority: 1 | 2 | 3 | 4): Promise<number>;
21
22
  /**
@@ -28,6 +29,12 @@ export declare function getEscalationRoles(ids: string[]): Promise<string[]>;
28
29
  * Only the assigned user (or superadmin via route) may release.
29
30
  */
30
31
  export declare function releaseEscalation(id: string, userId: string): Promise<LTEscalationRecord | null>;
32
+ /**
33
+ * Sweep expired claims back to the available pool, returning the count cleared.
34
+ * Availability is already query-time in the implicit model, but long-tail's
35
+ * public contract clears `assigned_to` and returns a count, so this runs as a
36
+ * single direct UPDATE on the shared table (the SDK's releaseExpired is a no-op).
37
+ */
31
38
  export declare function releaseExpiredClaims(): Promise<number>;
32
39
  /**
33
40
  * Reassign an escalation to a different role.
@@ -50,13 +57,12 @@ export declare function findByMetadata(key: string, value: string, status?: stri
50
57
  total: number;
51
58
  }>;
52
59
  /**
53
- * Atomic claim by metadata with inline RBAC.
54
- * The SQL WHERE clause enforces role membershipif the caller
55
- * doesn't have an allowed role, zero rows match and the claim
56
- * never happens. No pre-flight find, no TOCTOU.
60
+ * Atomic claim by metadata with inline RBAC and optional metadata merge.
61
+ * The SDK enforces the role filter in SQL callers without an allowed role
62
+ * match zero rows. Returns `{ escalation, isExtension, candidatesExist }` or
63
+ * null when nothing was claimed.
57
64
  *
58
- * @param allowedRoles — roles the caller can claim (null = no filter / global access)
59
- * @returns `{ escalation, isExtension, candidatesExist }` or null
65
+ * @param allowedRoles — roles the caller can claim (null = no filter / global)
60
66
  */
61
67
  export declare function claimByMetadata(key: string, value: string, userId: string, durationMinutes?: number, metadata?: Record<string, any>, allowedRoles?: string[] | null): Promise<(ClaimResult & {
62
68
  candidatesExist: number;
@@ -74,11 +80,13 @@ export interface ResolveByMetadataResult {
74
80
  taskQueue?: string;
75
81
  }
76
82
  /**
77
- * Atomic resolve by metadata with signal guard.
83
+ * Atomic resolve by metadata with signal guard, in a single CTE.
78
84
  *
79
- * Single query, two outcomes:
80
- * 1. No signal_id claim + resolve atomically. Returns { outcome: 'resolved', escalation }.
81
- * 2. signal_id present resolve skipped. Returns { outcome: 'signal_required', signalId, escalationId, ... }
82
- * so the caller can signal the workflow. conditionLT handles the rest.
85
+ * Signal-backed rows (those carrying `metadata.signal_id`) are NOT resolved
86
+ * here long-tail signals the paused workflow and the workflow interceptor
87
+ * resolves durably. If the workflow is gone the signal fails and the row stays
88
+ * pending, which is the contract the route suite pins. This guard is long-tail
89
+ * business logic over the shared table, so it runs as one atomic statement on
90
+ * `hmsh_escalations` rather than through the generic SDK resolve.
83
91
  */
84
92
  export declare function resolveByMetadataAtomic(key: string, value: string, userId: string, resolverPayload: Record<string, any>, metadata?: Record<string, any>, allowedRoles?: string[] | null): Promise<ResolveByMetadataResult>;