@hotmeshio/hotmesh 0.22.2 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.22.2",
3
+ "version": "0.22.3",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -88,6 +88,7 @@
88
88
  "test:unit": "vitest run tests/unit",
89
89
  "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",
90
90
  "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",
91
+ "prove:migrations": "docker compose exec hotmesh npx vitest run tests/durable/migrations/postgres.test.ts 2>&1 | tee /tmp/hmsh-migrations.txt && grep -E 'FAIL|Tests |Files ' /tmp/hmsh-migrations.txt | tail -5",
91
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",
92
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",
93
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"
@@ -1,13 +1,13 @@
1
1
  import { HotMesh } from '../hotmesh';
2
2
  import { Connection } from '../../types/durable';
3
- import { EscalationEntry, ClaimEscalationResult, ClaimByMetadataResult, ReleaseEscalationResult, ResolveEscalationResult, CancelEscalationResult, ListEscalationsParams, CreateEscalationParams, UpdateEscalationParams, AppendMilestonesParams, ClaimEscalationParams, ClaimByMetadataParams, ReleaseEscalationParams, ResolveEscalationParams, ResolveByMetadataParams, EscalateToRoleParams, MigrateEscalationParams } from '../../types/hmsh_escalations';
4
- type GetHotMeshFn = (topic: string | null, namespace?: string) => Promise<HotMesh>;
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
5
  export interface EscalationClientConfig {
6
6
  /** Postgres connection options — used when creating a standalone EscalationClient. */
7
7
  connection?: Connection;
8
8
  /**
9
9
  * Inject a pre-existing `getHotMeshClient` function (e.g. from Durable.Client).
10
- * When provided, the client reuses the caller's engine pool — no second connection.
10
+ * When provided, the client reuses the caller's engine pool — no extra connections.
11
11
  */
12
12
  getHotMeshClient?: GetHotMeshFn;
13
13
  }
@@ -48,7 +48,6 @@ export interface EscalationClientConfig {
48
48
  */
49
49
  export declare class EscalationClientService {
50
50
  private readonly _engine;
51
- private readonly _connection?;
52
51
  static instances: Map<string, HotMesh | Promise<HotMesh>>;
53
52
  constructor(config?: EscalationClientConfig);
54
53
  private _makeEngineFactory;
@@ -57,7 +56,7 @@ export declare class EscalationClientService {
57
56
  /**
58
57
  * Returns all escalation rows matching the given filters. Each row includes
59
58
  * a computed `available` field (true = claimable). Supports `sortBy`,
60
- * `sortOrder`, and multi-role `roles[]` filter.
59
+ * `sortOrder`, `orderBy[]`, and multi-role `roles[]` filter.
61
60
  */
62
61
  list(params?: ListEscalationsParams): Promise<EscalationEntry[]>;
63
62
  /**
@@ -84,12 +83,14 @@ export declare class EscalationClientService {
84
83
  /**
85
84
  * Atomically claims an escalation by UUID. Implicit model: `status` stays
86
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
87
  */
88
88
  claim(params: ClaimEscalationParams): Promise<ClaimEscalationResult>;
89
89
  /**
90
90
  * Atomically claims the highest-priority pending escalation whose `metadata`
91
- * contains the given key/value. Returns `isExtension: true` when the same
92
- * assignee re-claims a row they already hold (extends the expiry).
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).
93
94
  */
94
95
  claimByMetadata(params: ClaimByMetadataParams): Promise<ClaimByMetadataResult>;
95
96
  /** Releases a claimed escalation, returning it to available status. */
@@ -105,14 +106,17 @@ export declare class EscalationClientService {
105
106
  */
106
107
  cancel(id: string, namespace?: string): Promise<CancelEscalationResult>;
107
108
  /**
108
- * Signal-first resolve: marks the escalation resolved **and** delivers the
109
- * signal to the waiting workflow in a single held transaction. If signal
110
- * delivery fails, the transaction rolls back `committed: false`.
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.
111
115
  */
112
116
  resolve(params: ResolveEscalationParams, namespace?: string): Promise<ResolveEscalationResult>;
113
117
  /**
114
118
  * Resolves the highest-priority matching escalation by metadata filter,
115
- * then delivers its signal.
119
+ * then delivers its signal. Same transaction + WHERE guard semantics as `resolve()`.
116
120
  */
117
121
  resolveByMetadata(params: ResolveByMetadataParams, namespace?: string): Promise<ResolveEscalationResult>;
118
122
  /**
@@ -125,6 +129,40 @@ export declare class EscalationClientService {
125
129
  * from `assigned_until`. Kept for API compatibility.
126
130
  */
127
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[]>;
128
167
  static shutdown(): Promise<void>;
129
168
  }
130
- export {};
@@ -48,7 +48,6 @@ class EscalationClientService {
48
48
  this._engine = config.getHotMeshClient;
49
49
  }
50
50
  else if (config.connection) {
51
- this._connection = config.connection;
52
51
  this._engine = this._makeEngineFactory(config.connection);
53
52
  }
54
53
  else {
@@ -114,7 +113,7 @@ class EscalationClientService {
114
113
  /**
115
114
  * Returns all escalation rows matching the given filters. Each row includes
116
115
  * a computed `available` field (true = claimable). Supports `sortBy`,
117
- * `sortOrder`, and multi-role `roles[]` filter.
116
+ * `sortOrder`, `orderBy[]`, and multi-role `roles[]` filter.
118
117
  */
119
118
  async list(params) {
120
119
  const hm = await this._engine(null, params?.namespace);
@@ -162,6 +161,7 @@ class EscalationClientService {
162
161
  /**
163
162
  * Atomically claims an escalation by UUID. Implicit model: `status` stays
164
163
  * `'pending'`; claim is expressed via `assigned_to` + `assigned_until`.
164
+ * Returns `isExtension: true` when the same assignee re-claims a row they already hold.
165
165
  */
166
166
  async claim(params) {
167
167
  const hm = await this._engine(null, params.namespace);
@@ -169,8 +169,9 @@ class EscalationClientService {
169
169
  }
170
170
  /**
171
171
  * Atomically claims the highest-priority pending escalation whose `metadata`
172
- * contains the given key/value. Returns `isExtension: true` when the same
173
- * assignee re-claims a row they already hold (extends the expiry).
172
+ * contains the given key/value. Optionally merges `metadata` into the claimed row
173
+ * in the same atomic UPDATE. Returns `isExtension: true` when the same assignee
174
+ * re-claims a row they already hold (extends the expiry).
174
175
  */
175
176
  async claimByMetadata(params) {
176
177
  const hm = await this._engine(null, params.namespace);
@@ -198,14 +199,18 @@ class EscalationClientService {
198
199
  return hm.engine.store.cancelEscalation(id, namespace);
199
200
  }
200
201
  /**
201
- * Signal-first resolve: marks the escalation resolved **and** delivers the
202
- * signal to the waiting workflow in a single held transaction. If signal
203
- * delivery fails, the transaction rolls back `committed: false`.
202
+ * Resolves a pending escalation by UUID. Uses an explicit Postgres transaction
203
+ * with FOR UPDATE + WHERE guard: only one concurrent caller can commit the
204
+ * status change; the committed resolved row with its `signal_key` is the
205
+ * durable proof. Signal delivery is best-effort post-commit — the resolved
206
+ * row is the recovery record for any missed delivery. Returns the updated
207
+ * row as `entry` on success.
204
208
  */
205
209
  async resolve(params, namespace) {
206
210
  const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
207
211
  const hm = await this._engine(null, ns);
208
- const dbResult = await hm.engine.store.resolveEscalation({ id: params.id, resolverPayload: params.resolverPayload });
212
+ const store = hm.engine.store;
213
+ const dbResult = await store.resolveEscalation({ id: params.id, resolverPayload: params.resolverPayload });
209
214
  if (!dbResult.ok)
210
215
  return dbResult;
211
216
  if (dbResult.signalKey) {
@@ -214,16 +219,17 @@ class EscalationClientService {
214
219
  data: params.resolverPayload ?? {},
215
220
  });
216
221
  }
217
- return { ok: true };
222
+ return { ok: true, entry: dbResult.entry };
218
223
  }
219
224
  /**
220
225
  * Resolves the highest-priority matching escalation by metadata filter,
221
- * then delivers its signal.
226
+ * then delivers its signal. Same transaction + WHERE guard semantics as `resolve()`.
222
227
  */
223
228
  async resolveByMetadata(params, namespace) {
224
229
  const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
225
230
  const hm = await this._engine(null, ns);
226
- const dbResult = await hm.engine.store.resolveEscalationByMetadata({ key: params.key, value: params.value, resolverPayload: params.resolverPayload, roles: params.roles });
231
+ const store = hm.engine.store;
232
+ const dbResult = await store.resolveEscalationByMetadata({ key: params.key, value: params.value, resolverPayload: params.resolverPayload, roles: params.roles });
227
233
  if (!dbResult.ok)
228
234
  return dbResult;
229
235
  if (dbResult.signalKey) {
@@ -232,7 +238,7 @@ class EscalationClientService {
232
238
  data: params.resolverPayload ?? {},
233
239
  });
234
240
  }
235
- return { ok: true };
241
+ return { ok: true, entry: dbResult.entry };
236
242
  }
237
243
  /**
238
244
  * Full-fidelity migration: inserts an escalation row preserving the original
@@ -251,6 +257,58 @@ class EscalationClientService {
251
257
  const hm = await this._engine(null, namespace);
252
258
  return hm.engine.store.releaseExpiredEscalations(namespace);
253
259
  }
260
+ // ─── Bulk operations ────────────────────────────────────────────────────────
261
+ /**
262
+ * Bulk-claims up to `ids.length` pending escalations in one statement.
263
+ * Returns `{ claimed, skipped }` — skipped rows are either already claimed
264
+ * by another assignee or non-existent. Implicit-claim semantics apply.
265
+ */
266
+ async claimMany(params) {
267
+ const hm = await this._engine(null, params.namespace);
268
+ return hm.engine.store.claimManyEscalations(params);
269
+ }
270
+ /**
271
+ * Bulk-reassigns pending escalations to a new role, clearing any current claim.
272
+ * Returns the count of rows updated.
273
+ */
274
+ async escalateManyToRole(params) {
275
+ const hm = await this._engine(null, params.namespace);
276
+ return hm.engine.store.escalateManyEscalationsToRole(params);
277
+ }
278
+ /**
279
+ * Bulk-updates priority for pending escalations. Returns the count of rows updated.
280
+ */
281
+ async updateManyPriority(params) {
282
+ const hm = await this._engine(null, params.namespace);
283
+ return hm.engine.store.updateManyEscalationsPriority(params);
284
+ }
285
+ /**
286
+ * Bulk-resolves pending escalations by id-set. No signal delivery — intended
287
+ * for redirect-to-triage flows where no workflow is waiting. Returns the
288
+ * resolved rows.
289
+ */
290
+ async resolveMany(params) {
291
+ const hm = await this._engine(null, params.namespace);
292
+ return hm.engine.store.resolveManyEscalations(params);
293
+ }
294
+ // ─── Aggregates ─────────────────────────────────────────────────────────────
295
+ /**
296
+ * Returns dashboard-ready escalation counts. `period` controls the window
297
+ * used for `created` and `resolved` counts (default `'24h'`). When `roles`
298
+ * is an empty array, all counts are zero (RBAC guard).
299
+ */
300
+ async stats(params) {
301
+ const hm = await this._engine(null, params?.namespace);
302
+ return hm.engine.store.escalationStats(params ?? {});
303
+ }
304
+ /**
305
+ * Returns the sorted list of distinct `type` values in the escalations table.
306
+ * Useful for populating filter dropdowns.
307
+ */
308
+ async listDistinctTypes(namespace) {
309
+ const hm = await this._engine(null, namespace);
310
+ return hm.engine.store.listDistinctEscalationTypes(namespace);
311
+ }
254
312
  static async shutdown() {
255
313
  for (const [_, instance] of EscalationClientService.instances) {
256
314
  (await instance).stop();
@@ -166,6 +166,145 @@ const KVTables = (context) => ({
166
166
  ON ${jobsTable} (key) WHERE is_live;
167
167
  `);
168
168
  }
169
+ // v0.22.0: origin_id, parent_id on per-app jobs table (workflow lineage).
170
+ // ADD COLUMN IF NOT EXISTS on a nullable column with no DEFAULT is catalog-only
171
+ // in PG11+ — no table rewrite, brief AccessExclusiveLock only.
172
+ await client.query(`
173
+ ALTER TABLE ${jobsTable} ADD COLUMN IF NOT EXISTS origin_id TEXT;
174
+ `);
175
+ await client.query(`
176
+ ALTER TABLE ${jobsTable} ADD COLUMN IF NOT EXISTS parent_id TEXT;
177
+ `);
178
+ // Partial indexes — only cover the opt-in rows so build is near-instant.
179
+ const { rows: originIdxRows } = await client.query(`SELECT 1 FROM pg_indexes WHERE indexname = $1 AND schemaname = $2 LIMIT 1`, [`idx_${schemaName}_jobs_origin`, schemaName]);
180
+ if (originIdxRows.length === 0) {
181
+ await client.query(`
182
+ CREATE INDEX IF NOT EXISTS idx_${schemaName}_jobs_origin
183
+ ON ${jobsTable}(origin_id) WHERE origin_id IS NOT NULL;
184
+ `);
185
+ }
186
+ const { rows: parentIdxRows } = await client.query(`SELECT 1 FROM pg_indexes WHERE indexname = $1 AND schemaname = $2 LIMIT 1`, [`idx_${schemaName}_jobs_parent`, schemaName]);
187
+ if (parentIdxRows.length === 0) {
188
+ await client.query(`
189
+ CREATE INDEX IF NOT EXISTS idx_${schemaName}_jobs_parent
190
+ ON ${jobsTable}(parent_id) WHERE parent_id IS NOT NULL;
191
+ `);
192
+ }
193
+ // ─── v0.22.0+: public.hmsh_escalations global table migrations ───────────
194
+ // Use the fixed advisory XACT lock so concurrent deployments of DIFFERENT
195
+ // appIds (each holding their own per-appId session lock) don't race on the
196
+ // shared public table DDL. Transaction-level lock auto-releases on commit/rollback.
197
+ await client.query('BEGIN');
198
+ await client.query('SELECT pg_advisory_xact_lock($1)', [0x484D5348]);
199
+ // v0.22.0: create hmsh_escalations if missing (upgrade from pre-0.22.x).
200
+ // Column list matches createTables() exactly; task_id is NOT here because
201
+ // the ADD COLUMN below is the canonical way to add it to both fresh and
202
+ // existing tables in a single idempotent statement.
203
+ await client.query(`
204
+ CREATE TABLE IF NOT EXISTS public.hmsh_escalations (
205
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
206
+ namespace TEXT NOT NULL,
207
+ app_id TEXT NOT NULL,
208
+ signal_key TEXT,
209
+ topic TEXT,
210
+ workflow_id TEXT,
211
+ task_queue TEXT,
212
+ workflow_type TEXT,
213
+ type TEXT,
214
+ subtype TEXT,
215
+ entity TEXT,
216
+ description TEXT,
217
+ role TEXT,
218
+ status TEXT NOT NULL DEFAULT 'pending',
219
+ priority INT NOT NULL DEFAULT 5,
220
+ assigned_to TEXT,
221
+ assigned_until TIMESTAMPTZ,
222
+ claimed_at TIMESTAMPTZ,
223
+ claim_expires_at TIMESTAMPTZ,
224
+ resolved_at TIMESTAMPTZ,
225
+ escalation_payload JSONB,
226
+ resolver_payload JSONB,
227
+ envelope JSONB,
228
+ metadata JSONB,
229
+ origin_id TEXT,
230
+ parent_id TEXT,
231
+ initiated_by TEXT,
232
+ created_by TEXT,
233
+ milestones JSONB NOT NULL DEFAULT '[]',
234
+ trace_id TEXT,
235
+ span_id TEXT,
236
+ expires_at TIMESTAMPTZ,
237
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
238
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
239
+ );
240
+ `);
241
+ // v0.22.3: task_id column — catalog-only ADD COLUMN (nullable TEXT, no DEFAULT,
242
+ // no table rewrite). IF NOT EXISTS makes this idempotent across all upgrade paths.
243
+ await client.query(`
244
+ ALTER TABLE public.hmsh_escalations ADD COLUMN IF NOT EXISTS task_id TEXT;
245
+ `);
246
+ // Ensure signal_key unique index exists.
247
+ await client.query(`
248
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_hmsh_esc_signal_key
249
+ ON public.hmsh_escalations(namespace, app_id, signal_key)
250
+ WHERE signal_key IS NOT NULL;
251
+ `);
252
+ // v0.22.0: idx_hmsh_esc_available — predicate must be status='pending'.
253
+ // Fresh 0.22.0 installs may have this with the correct predicate already;
254
+ // ensure it exists.
255
+ await client.query(`
256
+ CREATE INDEX IF NOT EXISTS idx_hmsh_esc_available
257
+ ON public.hmsh_escalations(namespace, app_id, role, priority ASC, created_at ASC)
258
+ WHERE status = 'pending';
259
+ `);
260
+ await client.query(`
261
+ CREATE INDEX IF NOT EXISTS idx_hmsh_esc_available_expiry
262
+ ON public.hmsh_escalations(namespace, app_id, role, assigned_until, created_at DESC);
263
+ `);
264
+ // v0.22.2: idx_hmsh_esc_assigned predicate changed from status='claimed' to
265
+ // status='pending' to match the implicit claim model. If the old predicate
266
+ // is present, drop and recreate so claimEscalation / list queries use it.
267
+ const { rows: assignedDefRows } = await client.query(`
268
+ SELECT indexdef FROM pg_indexes WHERE indexname = 'idx_hmsh_esc_assigned' LIMIT 1
269
+ `);
270
+ if (assignedDefRows.length > 0 &&
271
+ assignedDefRows[0].indexdef.includes("status = 'claimed'")) {
272
+ await client.query(`DROP INDEX IF EXISTS idx_hmsh_esc_assigned;`);
273
+ }
274
+ await client.query(`
275
+ CREATE INDEX IF NOT EXISTS idx_hmsh_esc_assigned
276
+ ON public.hmsh_escalations(assigned_to, assigned_until, created_at DESC)
277
+ WHERE status = 'pending' AND assigned_to IS NOT NULL;
278
+ `);
279
+ // v0.22.2: claim_expiry sweeper index is obsolete (releaseExpiredEscalations
280
+ // is a no-op in the implicit-claim model — availability is query-time computed).
281
+ await client.query(`DROP INDEX IF EXISTS idx_hmsh_esc_claim_expiry;`);
282
+ await client.query(`
283
+ CREATE INDEX IF NOT EXISTS idx_hmsh_esc_entity
284
+ ON public.hmsh_escalations(namespace, app_id, entity, created_at DESC)
285
+ WHERE entity IS NOT NULL;
286
+ `);
287
+ await client.query(`
288
+ CREATE INDEX IF NOT EXISTS idx_hmsh_esc_workflow
289
+ ON public.hmsh_escalations(workflow_id)
290
+ WHERE workflow_id IS NOT NULL;
291
+ `);
292
+ await client.query(`
293
+ CREATE INDEX IF NOT EXISTS idx_hmsh_esc_origin
294
+ ON public.hmsh_escalations(origin_id)
295
+ WHERE origin_id IS NOT NULL;
296
+ `);
297
+ await client.query(`
298
+ CREATE INDEX IF NOT EXISTS idx_hmsh_esc_metadata
299
+ ON public.hmsh_escalations USING GIN(metadata jsonb_path_ops);
300
+ `);
301
+ // v0.22.3: task_id partial index.
302
+ await client.query(`
303
+ CREATE INDEX IF NOT EXISTS idx_hmsh_esc_task
304
+ ON public.hmsh_escalations(task_id)
305
+ WHERE task_id IS NOT NULL;
306
+ `);
307
+ await client.query('COMMIT');
169
308
  },
170
309
  async createTables(client, appName) {
171
310
  try {
@@ -253,16 +392,15 @@ const KVTables = (context) => ({
253
392
  CREATE INDEX IF NOT EXISTS idx_hmsh_esc_available_expiry
254
393
  ON public.hmsh_escalations(namespace, app_id, role, assigned_until, created_at DESC);
255
394
  `);
395
+ // idx_hmsh_esc_assigned: implicit-claim model uses status='pending', not 'claimed'
396
+ await client.query(`DROP INDEX IF EXISTS idx_hmsh_esc_assigned;`);
256
397
  await client.query(`
257
398
  CREATE INDEX IF NOT EXISTS idx_hmsh_esc_assigned
258
399
  ON public.hmsh_escalations(assigned_to, assigned_until, created_at DESC)
259
- WHERE status = 'claimed' AND assigned_to IS NOT NULL;
260
- `);
261
- await client.query(`
262
- CREATE INDEX IF NOT EXISTS idx_hmsh_esc_claim_expiry
263
- ON public.hmsh_escalations(claim_expires_at)
264
- WHERE status = 'claimed';
400
+ WHERE status = 'pending' AND assigned_to IS NOT NULL;
265
401
  `);
402
+ // idx_hmsh_esc_claim_expiry: no longer used (releaseExpiredEscalations is a no-op)
403
+ await client.query(`DROP INDEX IF EXISTS idx_hmsh_esc_claim_expiry;`);
266
404
  await client.query(`
267
405
  CREATE INDEX IF NOT EXISTS idx_hmsh_esc_entity
268
406
  ON public.hmsh_escalations(namespace, app_id, entity, created_at DESC)
@@ -281,6 +419,15 @@ const KVTables = (context) => ({
281
419
  await client.query(`
282
420
  CREATE INDEX IF NOT EXISTS idx_hmsh_esc_metadata
283
421
  ON public.hmsh_escalations USING GIN(metadata jsonb_path_ops);
422
+ `);
423
+ // task_id: nullable passthrough column for downstream compat (no FK)
424
+ await client.query(`
425
+ ALTER TABLE public.hmsh_escalations ADD COLUMN IF NOT EXISTS task_id TEXT;
426
+ `);
427
+ await client.query(`
428
+ CREATE INDEX IF NOT EXISTS idx_hmsh_esc_task
429
+ ON public.hmsh_escalations(task_id)
430
+ WHERE task_id IS NOT NULL;
284
431
  `);
285
432
  break;
286
433
  case 'relational_connection':
@@ -246,6 +246,7 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
246
246
  createEscalationForMigration(params: import('../../../../types/hmsh_escalations').MigrateEscalationParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
247
247
  getEscalation(id: string, namespace?: string): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
248
248
  getEscalationBySignalKey(signalKey: string, namespace?: string): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
249
+ private _escalationFilterConditions;
249
250
  listEscalations(params?: import('../../../../types/hmsh_escalations').ListEscalationsParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry[]>;
250
251
  countEscalations(params?: import('../../../../types/hmsh_escalations').ListEscalationsParams): Promise<number>;
251
252
  claimEscalation(params: import('../../../../types/hmsh_escalations').ClaimEscalationParams): Promise<import('../../../../types/hmsh_escalations').ClaimEscalationResult>;
@@ -259,26 +260,19 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
259
260
  signalKey?: string | null;
260
261
  topic?: string | null;
261
262
  }>;
262
- /**
263
- * Queues the escalation UPDATE into an existing transaction without executing it.
264
- * Used by client.ts resolve() to commit the status change atomically with signal delivery.
265
- */
266
- queueResolveEscalation(params: {
267
- id: string;
268
- namespace?: string;
269
- resolverPayload?: Record<string, unknown>;
270
- }, transaction: {
271
- addCommand: (sql: string, params: any[], returnType: string) => any;
272
- }): void;
273
- /**
274
- * Finds the highest-priority pending or claimed escalation matching the given metadata filter.
275
- * Used by client.ts resolveByMetadata() to get routing info before building the atomic transaction.
276
- */
277
- findEscalationByMetadata(key: string, value: string, roles: string[] | null, namespace?: string): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
278
263
  cancelEscalation(id: string, namespace?: string): Promise<import('../../../../types/hmsh_escalations').CancelEscalationResult>;
279
264
  escalateEscalationToRole(params: import('../../../../types/hmsh_escalations').EscalateToRoleParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
280
265
  updateEscalation(params: import('../../../../types/hmsh_escalations').UpdateEscalationParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
281
266
  appendEscalationMilestones(params: import('../../../../types/hmsh_escalations').AppendMilestonesParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
267
+ claimManyEscalations(params: import('../../../../types/hmsh_escalations').ClaimManyParams): Promise<{
268
+ claimed: number;
269
+ skipped: number;
270
+ }>;
271
+ escalateManyEscalationsToRole(params: import('../../../../types/hmsh_escalations').EscalateManyToRoleParams): Promise<number>;
272
+ updateManyEscalationsPriority(params: import('../../../../types/hmsh_escalations').UpdateManyPriorityParams): Promise<number>;
273
+ resolveManyEscalations(params: import('../../../../types/hmsh_escalations').ResolveManyParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry[]>;
274
+ escalationStats(params?: import('../../../../types/hmsh_escalations').StatsEscalationsParams): Promise<import('../../../../types/hmsh_escalations').EscalationStats>;
275
+ listDistinctEscalationTypes(namespace?: string): Promise<string[]>;
282
276
  releaseExpiredEscalations(_namespace?: string): Promise<number>;
283
277
  }
284
278
  export { PostgresStoreService };