@axiom-lattice/pg-stores 3.0.2 → 3.1.1

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.
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/index.ts
2
- import { Pool as Pool27 } from "pg";
2
+ import { Pool as Pool28 } from "pg";
3
3
 
4
4
  // src/createPgStoreConfig.ts
5
- import { Pool as Pool24 } from "pg";
5
+ import { Pool as Pool25 } from "pg";
6
6
 
7
7
  // src/migrations/migration.ts
8
8
  var MigrationManager = class {
@@ -2272,6 +2272,9 @@ var PostgreSQLWorkspaceStore = class {
2272
2272
 
2273
2273
  // src/stores/PostgreSQLProjectStore.ts
2274
2274
  import { Pool as Pool7 } from "pg";
2275
+ import {
2276
+ assertGenericProjectConfig
2277
+ } from "@axiom-lattice/protocols";
2275
2278
 
2276
2279
  // src/migrations/project_migrations.ts
2277
2280
  var createProjectsTable = {
@@ -2487,34 +2490,62 @@ var PostgreSQLProjectStore = class {
2487
2490
  * Create a new project
2488
2491
  */
2489
2492
  async createProject(tenantId, workspaceId, id, data) {
2493
+ assertGenericProjectConfig(data.config);
2490
2494
  await this.ensureInitialized();
2491
2495
  const now = /* @__PURE__ */ new Date();
2492
2496
  const kind = data.kind || "business";
2493
- await this.pool.query(
2494
- `
2495
- INSERT INTO lattice_projects (id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at)
2497
+ const client = await this.pool.connect();
2498
+ try {
2499
+ await client.query("BEGIN");
2500
+ await client.query(
2501
+ "SELECT pg_advisory_xact_lock(hashtextextended($1 || ':project-mutations', 0))",
2502
+ [tenantId]
2503
+ );
2504
+ await client.query(
2505
+ "SELECT pg_advisory_xact_lock(hashtextextended($2 || ':project:' || $1, 0))",
2506
+ [id, tenantId]
2507
+ );
2508
+ const existing = await client.query(
2509
+ "SELECT config FROM lattice_projects WHERE id = $1 AND tenant_id = $2 FOR UPDATE",
2510
+ [id, tenantId]
2511
+ );
2512
+ const ids = existing.rows[0]?.config?.capabilityBundleIds;
2513
+ const bundleIds = Array.isArray(ids) && ids.every((bundleId) => typeof bundleId === "string") ? [...ids].sort() : [];
2514
+ if (bundleIds.length > 0) {
2515
+ await client.query(
2516
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || ':' || bundle_id, 0))
2517
+ FROM unnest($2::text[]) AS bundle_id
2518
+ ORDER BY bundle_id`,
2519
+ [tenantId, bundleIds]
2520
+ );
2521
+ }
2522
+ const result = await client.query(
2523
+ `INSERT INTO lattice_projects (id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at)
2496
2524
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
2497
2525
  ON CONFLICT (id, tenant_id) DO UPDATE SET
2498
2526
  workspace_id = EXCLUDED.workspace_id,
2499
2527
  name = EXCLUDED.name,
2500
2528
  description = EXCLUDED.description,
2501
- config = EXCLUDED.config,
2529
+ config = CASE
2530
+ WHEN lattice_projects.config ? 'capabilityBundleIds'
2531
+ THEN COALESCE(EXCLUDED.config, '{}'::jsonb)
2532
+ || jsonb_build_object('capabilityBundleIds', lattice_projects.config->'capabilityBundleIds')
2533
+ ELSE EXCLUDED.config
2534
+ END,
2502
2535
  kind = EXCLUDED.kind,
2503
2536
  updated_at = EXCLUDED.updated_at
2537
+ RETURNING id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at
2504
2538
  `,
2505
- [id, tenantId, workspaceId, data.name, data.description || null, data.config || null, kind, now, now]
2506
- );
2507
- return {
2508
- id,
2509
- tenantId,
2510
- workspaceId,
2511
- name: data.name,
2512
- description: data.description,
2513
- config: data.config,
2514
- kind,
2515
- createdAt: now,
2516
- updatedAt: now
2517
- };
2539
+ [id, tenantId, workspaceId, data.name, data.description || null, data.config || null, kind, now, now]
2540
+ );
2541
+ await client.query("COMMIT");
2542
+ return this.mapRowToProject(result.rows[0]);
2543
+ } catch (error) {
2544
+ await client.query("ROLLBACK");
2545
+ throw error;
2546
+ } finally {
2547
+ client.release();
2548
+ }
2518
2549
  }
2519
2550
  /**
2520
2551
  * Update an existing project
@@ -2524,11 +2555,8 @@ var PostgreSQLProjectStore = class {
2524
2555
  * overwrites the existing config. To preserve the current config, omit this field.
2525
2556
  */
2526
2557
  async updateProject(tenantId, id, updates) {
2558
+ assertGenericProjectConfig(updates.config);
2527
2559
  await this.ensureInitialized();
2528
- const existing = await this.getProjectById(tenantId, id);
2529
- if (!existing) {
2530
- return null;
2531
- }
2532
2560
  const updateFields = [];
2533
2561
  const updateValues = [];
2534
2562
  let paramIndex = 1;
@@ -2541,43 +2569,190 @@ var PostgreSQLProjectStore = class {
2541
2569
  updateValues.push(updates.description || null);
2542
2570
  }
2543
2571
  if (updates.config !== void 0) {
2544
- updateFields.push(`config = $${paramIndex++}`);
2545
- updateValues.push(updates.config || null);
2572
+ const configParam = `$${paramIndex++}`;
2573
+ updateFields.push(`config = CASE
2574
+ WHEN config ? 'capabilityBundleIds'
2575
+ THEN COALESCE(${configParam}::jsonb, '{}'::jsonb)
2576
+ || jsonb_build_object('capabilityBundleIds', config->'capabilityBundleIds')
2577
+ ELSE ${configParam}::jsonb
2578
+ END`);
2579
+ updateValues.push(updates.config);
2546
2580
  }
2547
2581
  if (updates.kind !== void 0) {
2548
2582
  updateFields.push(`kind = $${paramIndex++}`);
2549
2583
  updateValues.push(updates.kind);
2550
2584
  }
2551
2585
  if (updateFields.length === 0) {
2552
- return existing;
2586
+ return await this.getProjectById(tenantId, id);
2553
2587
  }
2554
2588
  updateFields.push(`updated_at = $${paramIndex++}`);
2555
2589
  updateValues.push(/* @__PURE__ */ new Date());
2556
- updateValues.push(id);
2557
- updateValues.push(tenantId);
2558
- await this.pool.query(
2559
- `
2560
- UPDATE lattice_projects
2561
- SET ${updateFields.join(", ")}
2562
- WHERE id = $${paramIndex} AND tenant_id = $${paramIndex + 1}
2563
- `,
2564
- updateValues
2565
- );
2566
- return await this.getProjectById(tenantId, id);
2590
+ updateValues.push(id, tenantId);
2591
+ const client = await this.pool.connect();
2592
+ try {
2593
+ await client.query("BEGIN");
2594
+ await client.query(
2595
+ "SELECT pg_advisory_xact_lock(hashtextextended($1 || ':project-mutations', 0))",
2596
+ [tenantId]
2597
+ );
2598
+ await client.query(
2599
+ "SELECT pg_advisory_xact_lock(hashtextextended($2 || ':project:' || $1, 0))",
2600
+ [id, tenantId]
2601
+ );
2602
+ const existing = await client.query(
2603
+ "SELECT config FROM lattice_projects WHERE id = $1 AND tenant_id = $2 FOR UPDATE",
2604
+ [id, tenantId]
2605
+ );
2606
+ if (!existing.rows[0]) {
2607
+ await client.query("COMMIT");
2608
+ return null;
2609
+ }
2610
+ const ids = existing.rows[0].config?.capabilityBundleIds;
2611
+ const bundleIds = Array.isArray(ids) && ids.every((bundleId) => typeof bundleId === "string") ? [...ids].sort() : [];
2612
+ if (bundleIds.length > 0) {
2613
+ await client.query(
2614
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || ':' || bundle_id, 0))
2615
+ FROM unnest($2::text[]) AS bundle_id
2616
+ ORDER BY bundle_id`,
2617
+ [tenantId, bundleIds]
2618
+ );
2619
+ }
2620
+ const result = await client.query(
2621
+ `UPDATE lattice_projects SET ${updateFields.join(", ")}
2622
+ WHERE id = $${paramIndex} AND tenant_id = $${paramIndex + 1}
2623
+ RETURNING id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at`,
2624
+ updateValues
2625
+ );
2626
+ await client.query("COMMIT");
2627
+ return this.mapRowToProject(result.rows[0]);
2628
+ } catch (error) {
2629
+ await client.query("ROLLBACK");
2630
+ throw error;
2631
+ } finally {
2632
+ client.release();
2633
+ }
2567
2634
  }
2568
2635
  /**
2569
2636
  * Delete a project by ID
2570
2637
  */
2571
2638
  async deleteProject(tenantId, id) {
2639
+ await this.ensureInitialized();
2640
+ const client = await this.pool.connect();
2641
+ try {
2642
+ await client.query("BEGIN");
2643
+ await client.query(
2644
+ "SELECT pg_advisory_xact_lock(hashtextextended($1 || ':project-mutations', 0))",
2645
+ [tenantId]
2646
+ );
2647
+ await client.query(
2648
+ "SELECT pg_advisory_xact_lock(hashtextextended($2 || ':project:' || $1, 0))",
2649
+ [id, tenantId]
2650
+ );
2651
+ const selected = await client.query(
2652
+ "SELECT config FROM lattice_projects WHERE id = $1 AND tenant_id = $2 FOR UPDATE",
2653
+ [id, tenantId]
2654
+ );
2655
+ if (!selected.rows[0]) {
2656
+ await client.query("COMMIT");
2657
+ return false;
2658
+ }
2659
+ const ids = selected.rows[0].config?.capabilityBundleIds;
2660
+ const bundleIds = Array.isArray(ids) && ids.every((bundleId) => typeof bundleId === "string") ? [...ids].sort() : [];
2661
+ if (bundleIds.length > 0) {
2662
+ await client.query(
2663
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || ':' || bundle_id, 0))
2664
+ FROM unnest($2::text[]) AS bundle_id
2665
+ ORDER BY bundle_id`,
2666
+ [tenantId, bundleIds]
2667
+ );
2668
+ }
2669
+ const result = await client.query(
2670
+ "DELETE FROM lattice_projects WHERE id = $1 AND tenant_id = $2",
2671
+ [id, tenantId]
2672
+ );
2673
+ await client.query("COMMIT");
2674
+ return result.rowCount !== null && result.rowCount > 0;
2675
+ } catch (error) {
2676
+ await client.query("ROLLBACK");
2677
+ throw error;
2678
+ } finally {
2679
+ client.release();
2680
+ }
2681
+ }
2682
+ async updateCapabilityBundleIds(tenantId, projectId, bundleIds, expectedRevisions = {}) {
2683
+ await this.ensureInitialized();
2684
+ const client = await this.pool.connect();
2685
+ try {
2686
+ await client.query("BEGIN");
2687
+ await client.query(
2688
+ "SELECT pg_advisory_xact_lock(hashtextextended($1 || ':project-mutations', 0))",
2689
+ [tenantId]
2690
+ );
2691
+ await client.query(
2692
+ "SELECT pg_advisory_xact_lock(hashtextextended($2 || ':project:' || $1, 0))",
2693
+ [projectId, tenantId]
2694
+ );
2695
+ await client.query(
2696
+ "SELECT id FROM lattice_projects WHERE id = $1 AND tenant_id = $2 FOR UPDATE",
2697
+ [projectId, tenantId]
2698
+ );
2699
+ if (bundleIds.length > 0) {
2700
+ await client.query(
2701
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || ':' || bundle_id, 0))
2702
+ FROM unnest($2::text[]) AS bundle_id
2703
+ ORDER BY bundle_id`,
2704
+ [tenantId, [...bundleIds].sort()]
2705
+ );
2706
+ if (Object.keys(expectedRevisions).length > 0) {
2707
+ const revisions = await client.query("SELECT id, updated_at::text AS updated_at FROM lattice_capability_bundles WHERE tenant_id = $1 AND id = ANY($2::uuid[]) FOR UPDATE", [tenantId, bundleIds]);
2708
+ if (revisions.rows.some((bundle) => expectedRevisions[bundle.id] !== void 0 && expectedRevisions[bundle.id] !== bundle.updated_at)) {
2709
+ await client.query("ROLLBACK");
2710
+ return { status: "bundle_conflict" };
2711
+ }
2712
+ }
2713
+ }
2714
+ const result = await client.query(
2715
+ `UPDATE lattice_projects
2716
+ SET config = COALESCE(config, '{}'::jsonb) || jsonb_build_object('capabilityBundleIds', $3::jsonb), updated_at = NOW()
2717
+ WHERE id = $1 AND tenant_id = $2
2718
+ AND NOT EXISTS (
2719
+ SELECT 1 FROM unnest($4::uuid[]) AS bundle_id
2720
+ WHERE NOT EXISTS (
2721
+ SELECT 1 FROM lattice_capability_bundles
2722
+ WHERE tenant_id = $2 AND id = bundle_id
2723
+ )
2724
+ )
2725
+ RETURNING id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at`,
2726
+ [projectId, tenantId, JSON.stringify(bundleIds), bundleIds]
2727
+ );
2728
+ if (result.rows[0]) {
2729
+ await client.query("COMMIT");
2730
+ return { status: "updated", project: this.mapRowToProject(result.rows[0]) };
2731
+ }
2732
+ const project = await client.query(
2733
+ "SELECT id FROM lattice_projects WHERE id = $1 AND tenant_id = $2",
2734
+ [projectId, tenantId]
2735
+ );
2736
+ await client.query("COMMIT");
2737
+ return project.rows.length > 0 ? { status: "bundle_not_found" } : { status: "project_not_found" };
2738
+ } catch (error) {
2739
+ await client.query("ROLLBACK");
2740
+ throw error;
2741
+ } finally {
2742
+ client.release();
2743
+ }
2744
+ }
2745
+ async isCapabilityBundleReferenced(tenantId, bundleId) {
2572
2746
  await this.ensureInitialized();
2573
2747
  const result = await this.pool.query(
2574
- `
2575
- DELETE FROM lattice_projects
2576
- WHERE id = $1 AND tenant_id = $2
2577
- `,
2578
- [id, tenantId]
2748
+ `SELECT 1 AS exists FROM lattice_projects
2749
+ WHERE tenant_id = $1
2750
+ AND jsonb_typeof(config->'capabilityBundleIds') = 'array'
2751
+ AND COALESCE(config->'capabilityBundleIds', '[]'::jsonb) ? $2
2752
+ LIMIT 1`,
2753
+ [tenantId, bundleId]
2579
2754
  );
2580
- return result.rowCount !== null && result.rowCount > 0;
2755
+ return result.rows.length > 0;
2581
2756
  }
2582
2757
  };
2583
2758
 
@@ -5060,10 +5235,48 @@ var ThreadMessageQueueStore = class {
5060
5235
  (id, thread_id, tenant_id, assistant_id, workspace_id, project_id, message_content, message_type, sequence_order, priority, command, custom_run_config)
5061
5236
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
5062
5237
  RETURNING *`,
5063
- [id || crypto.randomUUID(), threadId, tenantId, assistantId, workspaceId || null, projectId || null, JSON.stringify(content), type, nextSeq, priority, command ? JSON.stringify(command) : null, custom_run_config ? JSON.stringify(custom_run_config) : null]
5238
+ [id || crypto.randomUUID(), threadId, tenantId, assistantId, workspaceId ?? null, projectId ?? null, JSON.stringify(content), type, nextSeq, priority, command ? JSON.stringify(command) : null, custom_run_config ? JSON.stringify(custom_run_config) : null]
5064
5239
  );
5065
5240
  return this.rowToMessage(result.rows[0]);
5066
5241
  }
5242
+ async addMessageIfCapacity(params, maxSize) {
5243
+ if (maxSize === Infinity) {
5244
+ await this.addMessage(params);
5245
+ return true;
5246
+ }
5247
+ const client = await this.pool.connect();
5248
+ try {
5249
+ await client.query("BEGIN");
5250
+ const scope = { tenantId: params.tenantId, assistantId: params.assistantId, workspaceId: params.workspaceId, projectId: params.projectId };
5251
+ const lockKey = `${params.tenantId}:${params.assistantId}:${params.threadId}:${params.workspaceId ?? ""}:${params.projectId ?? ""}`;
5252
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [lockKey]);
5253
+ const filter = scopeClause(scope, 2);
5254
+ const count = await client.query(
5255
+ `SELECT COUNT(*) as count FROM lattice_thread_message_queue WHERE thread_id = $1 AND status = 'pending'${filter.sql}`,
5256
+ [params.threadId, ...filter.params]
5257
+ );
5258
+ if (parseInt(count.rows[0].count, 10) >= maxSize) {
5259
+ await client.query("ROLLBACK");
5260
+ return false;
5261
+ }
5262
+ const seq = await client.query(
5263
+ `SELECT COALESCE(MAX(sequence_order), 0) + 1 as next_seq FROM lattice_thread_message_queue WHERE thread_id = $1`,
5264
+ [params.threadId]
5265
+ );
5266
+ const result = await client.query(
5267
+ `INSERT INTO lattice_thread_message_queue (id, thread_id, tenant_id, assistant_id, workspace_id, project_id, message_content, message_type, sequence_order, priority, command, custom_run_config)
5268
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`,
5269
+ [params.id || crypto.randomUUID(), params.threadId, params.tenantId, params.assistantId, params.workspaceId ?? null, params.projectId ?? null, JSON.stringify(params.content), params.type || "human", seq.rows[0].next_seq, params.priority ?? 0, params.command ? JSON.stringify(params.command) : null, params.custom_run_config ? JSON.stringify(params.custom_run_config) : null]
5270
+ );
5271
+ await client.query("COMMIT");
5272
+ return Boolean(result.rows[0]);
5273
+ } catch (error) {
5274
+ await client.query("ROLLBACK");
5275
+ throw error;
5276
+ } finally {
5277
+ client.release();
5278
+ }
5279
+ }
5067
5280
  /**
5068
5281
  * Add message at head of queue (high priority, e.g., STEER/Command messages)
5069
5282
  * Uses priority=100 to ensure message is processed first
@@ -5084,42 +5297,45 @@ var ThreadMessageQueueStore = class {
5084
5297
  (id, thread_id, tenant_id, assistant_id, workspace_id, project_id, message_content, message_type, sequence_order, priority, command, custom_run_config)
5085
5298
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 100, $10, $11)
5086
5299
  RETURNING *`,
5087
- [id || crypto.randomUUID(), threadId, resolvedTenantId, resolvedAssistantId, workspaceId || null, projectId || null, JSON.stringify(content), type, nextSeq, command ? JSON.stringify(command) : null, custom_run_config ? JSON.stringify(custom_run_config) : null]
5300
+ [id || crypto.randomUUID(), threadId, resolvedTenantId, resolvedAssistantId, workspaceId ?? null, projectId ?? null, JSON.stringify(content), type, nextSeq, command ? JSON.stringify(command) : null, custom_run_config ? JSON.stringify(custom_run_config) : null]
5088
5301
  );
5089
5302
  return this.rowToMessage(result.rows[0]);
5090
5303
  }
5091
5304
  /**
5092
5305
  * Get pending messages for thread
5093
5306
  */
5094
- async getPendingMessages(threadId) {
5307
+ async getPendingMessages(threadId, scope) {
5308
+ const filter = scopeClause(scope, 2);
5095
5309
  const result = await this.pool.query(
5096
- `SELECT * FROM lattice_thread_message_queue
5097
- WHERE thread_id = $1 AND status = 'pending'
5310
+ `SELECT * FROM lattice_thread_message_queue
5311
+ WHERE thread_id = $1 AND status = 'pending'${filter.sql}
5098
5312
  ORDER BY priority DESC, sequence_order ASC`,
5099
- [threadId]
5313
+ [threadId, ...filter.params]
5100
5314
  );
5101
5315
  return result.rows.map((row) => this.rowToMessage(row));
5102
5316
  }
5103
5317
  /**
5104
5318
  * Get processing messages for a thread
5105
5319
  */
5106
- async getProcessingMessages(threadId) {
5320
+ async getProcessingMessages(threadId, scope) {
5321
+ const filter = scopeClause(scope, 2);
5107
5322
  const result = await this.pool.query(
5108
5323
  `SELECT * FROM lattice_thread_message_queue
5109
- WHERE thread_id = $1 AND status = 'processing'
5324
+ WHERE thread_id = $1 AND status = 'processing'${filter.sql}
5110
5325
  ORDER BY priority DESC, sequence_order ASC`,
5111
- [threadId]
5326
+ [threadId, ...filter.params]
5112
5327
  );
5113
5328
  return result.rows.map((row) => this.rowToMessage(row));
5114
5329
  }
5115
5330
  /**
5116
5331
  * Get queue size
5117
5332
  */
5118
- async getQueueSize(threadId) {
5333
+ async getQueueSize(threadId, scope) {
5334
+ const filter = scopeClause(scope, 2);
5119
5335
  const result = await this.pool.query(
5120
5336
  `SELECT COUNT(*) as count FROM lattice_thread_message_queue
5121
- WHERE thread_id = $1 AND status = 'pending'`,
5122
- [threadId]
5337
+ WHERE thread_id = $1 AND status = 'pending'${filter.sql}`,
5338
+ [threadId, ...filter.params]
5123
5339
  );
5124
5340
  return parseInt(result.rows[0].count, 10);
5125
5341
  }
@@ -5128,58 +5344,70 @@ var ThreadMessageQueueStore = class {
5128
5344
  */
5129
5345
  async getThreadsWithPendingMessages() {
5130
5346
  const result = await this.pool.query(
5131
- `SELECT DISTINCT ON (thread_id) tenant_id, assistant_id, thread_id, workspace_id, project_id
5347
+ `SELECT DISTINCT ON (tenant_id, assistant_id, workspace_id, project_id, thread_id) tenant_id, assistant_id, thread_id, workspace_id, project_id
5132
5348
  FROM lattice_thread_message_queue
5133
5349
  WHERE status IN ('pending', 'processing')
5134
- ORDER BY thread_id`
5350
+ ORDER BY tenant_id, assistant_id, workspace_id, project_id, thread_id`
5135
5351
  );
5136
5352
  return result.rows.map((row) => ({
5137
5353
  tenantId: row.tenant_id,
5138
5354
  assistantId: row.assistant_id,
5139
5355
  threadId: row.thread_id,
5140
- workspaceId: row.workspace_id || void 0,
5141
- projectId: row.project_id || void 0
5356
+ workspaceId: row.workspace_id,
5357
+ projectId: row.project_id
5142
5358
  }));
5143
5359
  }
5144
5360
  /**
5145
5361
  * Remove message
5146
5362
  */
5147
- async removeMessage(messageId) {
5363
+ async removeMessage(messageId, scope) {
5364
+ const filter = scopeClause(scope, 2);
5148
5365
  const result = await this.pool.query(
5149
- `DELETE FROM lattice_thread_message_queue WHERE id = $1 RETURNING id`,
5150
- [messageId]
5366
+ `DELETE FROM lattice_thread_message_queue WHERE id = $1${filter.sql} RETURNING id`,
5367
+ [messageId, ...filter.params]
5151
5368
  );
5152
5369
  return (result.rowCount ?? 0) > 0;
5153
5370
  }
5154
5371
  /**
5155
5372
  * Clear all messages for thread
5156
5373
  */
5157
- async clearMessages(threadId) {
5374
+ async clearMessages(threadId, scope) {
5375
+ const filter = scopeClause(scope, 2);
5158
5376
  await this.pool.query(
5159
- `DELETE FROM lattice_thread_message_queue WHERE thread_id = $1`,
5160
- [threadId]
5377
+ `DELETE FROM lattice_thread_message_queue WHERE thread_id = $1${filter.sql}`,
5378
+ [threadId, ...filter.params]
5161
5379
  );
5162
5380
  }
5163
5381
  /**
5164
5382
  * Mark message as processing
5165
5383
  */
5166
- async markProcessing(messageId) {
5384
+ async markProcessing(messageId, customRunConfig, scope) {
5385
+ if (customRunConfig !== void 0) {
5386
+ const filter2 = scopeClause(scope, 3);
5387
+ await this.pool.query(
5388
+ `UPDATE lattice_thread_message_queue SET status = 'processing', custom_run_config = $2 WHERE id = $1${filter2.sql}`,
5389
+ [messageId, JSON.stringify(customRunConfig), ...filter2.params]
5390
+ );
5391
+ return;
5392
+ }
5393
+ const filter = scopeClause(scope, 2);
5167
5394
  await this.pool.query(
5168
- `UPDATE lattice_thread_message_queue SET status = 'processing' WHERE id = $1`,
5169
- [messageId]
5395
+ `UPDATE lattice_thread_message_queue SET status = 'processing' WHERE id = $1${filter.sql}`,
5396
+ [messageId, ...filter.params]
5170
5397
  );
5171
5398
  }
5172
5399
  /**
5173
5400
  * Reset all processing messages to pending state for a thread
5174
5401
  * Returns the number of messages reset
5175
5402
  */
5176
- async resetProcessingToPending(threadId) {
5403
+ async resetProcessingToPending(threadId, scope) {
5404
+ const filter = scopeClause(scope, 2);
5177
5405
  const result = await this.pool.query(
5178
5406
  `UPDATE lattice_thread_message_queue
5179
5407
  SET status = 'pending'
5180
- WHERE thread_id = $1 AND status = 'processing'
5408
+ WHERE thread_id = $1 AND status = 'processing'${filter.sql}
5181
5409
  RETURNING id`,
5182
- [threadId]
5410
+ [threadId, ...filter.params]
5183
5411
  );
5184
5412
  return result.rowCount ?? 0;
5185
5413
  }
@@ -5196,6 +5424,18 @@ var ThreadMessageQueueStore = class {
5196
5424
  };
5197
5425
  }
5198
5426
  };
5427
+ function scopeClause(scope, start) {
5428
+ if (!scope) return { sql: "", params: [] };
5429
+ const params = [];
5430
+ const entries = ["tenantId", "assistantId", "workspaceId", "projectId"].map((key) => [key, scope[key]]);
5431
+ const sql = entries.map(([key, value]) => {
5432
+ const column = key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
5433
+ if (value == null) return ` AND ${column} IS NULL`;
5434
+ params.push(value);
5435
+ return ` AND ${column} = $${start + params.length - 1}`;
5436
+ }).join("");
5437
+ return { sql, params };
5438
+ }
5199
5439
 
5200
5440
  // src/stores/ChannelBindingStore.ts
5201
5441
  import { Pool as Pool14 } from "pg";
@@ -5946,17 +6186,17 @@ var PostgreSQLA2AApiKeyStore = class {
5946
6186
  const result = await this.pool.query(
5947
6187
  `SELECT * FROM lattice_a2a_api_keys WHERE enabled = true`
5948
6188
  );
5949
- const map = /* @__PURE__ */ new Map();
6189
+ const map2 = /* @__PURE__ */ new Map();
5950
6190
  for (const row of result.rows) {
5951
6191
  const key = decrypt5(row.key_value);
5952
- map.set(key, {
6192
+ map2.set(key, {
5953
6193
  key,
5954
6194
  tenantId: row.tenant_id,
5955
6195
  projectId: row.project_id,
5956
6196
  assistantIds: row.assistant_ids ?? void 0
5957
6197
  });
5958
6198
  }
5959
- return map;
6199
+ return map2;
5960
6200
  }
5961
6201
  };
5962
6202
 
@@ -7629,6 +7869,7 @@ var PostgreSQLTaskStore = class {
7629
7869
  };
7630
7870
 
7631
7871
  // src/stores/PostgreSQLTaskWorkItemStore.ts
7872
+ import { MAX_PENDING_EXECUTION_RESULTS_LIMIT } from "@axiom-lattice/protocols";
7632
7873
  import { v4 } from "uuid";
7633
7874
  var PostgreSQLTaskWorkItemStore = class {
7634
7875
  constructor(pool) {
@@ -7720,6 +7961,35 @@ var PostgreSQLTaskWorkItemStore = class {
7720
7961
  const result = await this.pool.query(query, params);
7721
7962
  return result.rows.map((row) => this.rowToItem(row));
7722
7963
  }
7964
+ /** List pending execution results using one bounded PostgreSQL anti-join query. */
7965
+ async listPendingExecutionResults(params) {
7966
+ if (!Number.isSafeInteger(params.limit) || params.limit < 0 || params.limit > MAX_PENDING_EXECUTION_RESULTS_LIMIT) {
7967
+ const error = new RangeError(`limit must be a safe integer between 0 and ${MAX_PENDING_EXECUTION_RESULTS_LIMIT}`);
7968
+ error.code = "INVALID_LIMIT";
7969
+ throw error;
7970
+ }
7971
+ if (params.limit === 0) return [];
7972
+ const result = await this.pool.query(
7973
+ `SELECT result.*
7974
+ FROM lattice_task_work_items AS result
7975
+ WHERE result.tenant_id = $1
7976
+ AND result.task_id = $2
7977
+ AND result.action = 'execution_result'
7978
+ AND result.event_key COLLATE "C" ~ '^execution-result:[A-Za-z0-9._:-]+$'
7979
+ AND NOT EXISTS (
7980
+ SELECT 1
7981
+ FROM lattice_task_work_items AS reconciled
7982
+ WHERE reconciled.tenant_id = result.tenant_id
7983
+ AND reconciled.task_id = result.task_id
7984
+ AND reconciled.action = 'execution_reconciled'
7985
+ AND reconciled.detail ->> 'executionResultId' = result.event_key
7986
+ )
7987
+ ORDER BY result.created_at DESC, result.id DESC
7988
+ LIMIT $3`,
7989
+ [params.tenantId, params.taskId, params.limit]
7990
+ );
7991
+ return result.rows.map((row) => this.rowToItem(row));
7992
+ }
7723
7993
  rowToItem(row) {
7724
7994
  return {
7725
7995
  id: row.id,
@@ -8747,10 +9017,268 @@ var addTaskWorkItemEventKeyMigration = {
8747
9017
  `);
8748
9018
  }
8749
9019
  };
9020
+ var addTaskWorkItemPendingIndexesMigration = {
9021
+ version: 171,
9022
+ name: "add_task_work_item_pending_indexes",
9023
+ up: async (client) => {
9024
+ await client.query(`
9025
+ CREATE INDEX IF NOT EXISTS idx_task_work_items_pending_order
9026
+ ON lattice_task_work_items (tenant_id, task_id, action, created_at DESC, id DESC)
9027
+ `);
9028
+ await client.query(`
9029
+ CREATE INDEX IF NOT EXISTS idx_task_work_items_reconciled_result
9030
+ ON lattice_task_work_items (tenant_id, task_id, (detail ->> 'executionResultId'))
9031
+ WHERE action = 'execution_reconciled'
9032
+ `);
9033
+ }
9034
+ };
9035
+
9036
+ // src/migrations/capability_bundle_migration.ts
9037
+ var createCapabilityBundlesTable = {
9038
+ version: 170,
9039
+ name: "create_capability_bundles_table",
9040
+ up: async (client) => {
9041
+ await client.query(`CREATE TABLE IF NOT EXISTS lattice_capability_bundles (
9042
+ id UUID PRIMARY KEY, tenant_id VARCHAR(255) NOT NULL, bundle_key VARCHAR(255) NOT NULL,
9043
+ name VARCHAR(255) NOT NULL, description TEXT, capabilities JSONB NOT NULL DEFAULT '[]'::jsonb,
9044
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
9045
+ UNIQUE (tenant_id, bundle_key)
9046
+ )`);
9047
+ await client.query("CREATE INDEX IF NOT EXISTS idx_lattice_capability_bundles_tenant ON lattice_capability_bundles(tenant_id)");
9048
+ },
9049
+ down: async (client) => {
9050
+ await client.query("DROP INDEX IF EXISTS idx_lattice_capability_bundles_tenant");
9051
+ await client.query("DROP TABLE IF EXISTS lattice_capability_bundles");
9052
+ }
9053
+ };
9054
+
9055
+ // src/stores/PostgreSQLCapabilityBundleStore.ts
9056
+ import { randomUUID as randomUUID3 } from "crypto";
9057
+ import { Pool as Pool24 } from "pg";
9058
+ var duplicateMessage = "Capability bundle key already exists for tenant";
9059
+ function map(row) {
9060
+ return {
9061
+ id: row.id,
9062
+ tenantId: row.tenant_id,
9063
+ key: row.bundle_key,
9064
+ name: row.name,
9065
+ description: row.description ?? void 0,
9066
+ capabilities: row.capabilities,
9067
+ createdAt: row.created_at.toISOString(),
9068
+ updatedAt: typeof row.updated_at === "string" ? row.updated_at : row.updated_at.toISOString()
9069
+ };
9070
+ }
9071
+ function isDuplicate(error) {
9072
+ return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
9073
+ }
9074
+ var PostgreSQLCapabilityBundleStore = class {
9075
+ /**
9076
+ * Creates a PostgreSQL capability bundle store.
9077
+ *
9078
+ * @param options - Pool ownership, connection, and migration options.
9079
+ */
9080
+ constructor(options) {
9081
+ this.initialized = false;
9082
+ this.ownsPool = true;
9083
+ this.initPromise = null;
9084
+ if (options.pool) {
9085
+ this.pool = options.pool;
9086
+ this.ownsPool = false;
9087
+ this.initialized = true;
9088
+ return;
9089
+ }
9090
+ this.pool = typeof options.poolConfig === "string" ? new Pool24({ connectionString: options.poolConfig }) : options.poolConfig ? new Pool24(options.poolConfig) : (() => {
9091
+ throw new Error("Either pool or poolConfig must be provided");
9092
+ })();
9093
+ this.migrationManager = new MigrationManager(this.pool);
9094
+ this.migrationManager.register(createCapabilityBundlesTable);
9095
+ if (options.autoMigrate !== false) {
9096
+ this.startInitialization();
9097
+ }
9098
+ }
9099
+ /**
9100
+ * Applies pending migrations for an internally managed pool.
9101
+ *
9102
+ * @returns A shared promise that resolves when initialization completes.
9103
+ */
9104
+ async initialize() {
9105
+ if (this.initialized) return;
9106
+ if (this.initPromise) return this.initPromise;
9107
+ return this.startInitialization();
9108
+ }
9109
+ startInitialization() {
9110
+ this.initPromise = this.migrationManager.migrate().then(() => {
9111
+ this.initialized = true;
9112
+ });
9113
+ void this.initPromise.catch(() => void 0);
9114
+ return this.initPromise;
9115
+ }
9116
+ /**
9117
+ * Closes the pool when it was created by this store.
9118
+ *
9119
+ * @returns A promise that resolves after owned resources are released.
9120
+ */
9121
+ async dispose() {
9122
+ if (this.ownsPool) await this.pool.end();
9123
+ }
9124
+ async ready() {
9125
+ if (!this.initialized) await this.initialize();
9126
+ }
9127
+ async listByTenant(tenantId) {
9128
+ await this.ready();
9129
+ const result = await this.pool.query(
9130
+ "SELECT id, tenant_id, bundle_key, name, description, capabilities, created_at, updated_at::text AS updated_at FROM lattice_capability_bundles WHERE tenant_id = $1 ORDER BY created_at",
9131
+ [tenantId]
9132
+ );
9133
+ return result.rows.map(map);
9134
+ }
9135
+ async getById(tenantId, id) {
9136
+ await this.ready();
9137
+ const result = await this.pool.query(
9138
+ "SELECT id, tenant_id, bundle_key, name, description, capabilities, created_at, updated_at::text AS updated_at FROM lattice_capability_bundles WHERE tenant_id = $1 AND id = $2",
9139
+ [tenantId, id]
9140
+ );
9141
+ return result.rows[0] ? map(result.rows[0]) : null;
9142
+ }
9143
+ async getManyByIds(tenantId, ids) {
9144
+ await this.ready();
9145
+ if (ids.length === 0) return [];
9146
+ const result = await this.pool.query(
9147
+ "SELECT id, tenant_id, bundle_key, name, description, capabilities, created_at, updated_at::text AS updated_at FROM lattice_capability_bundles WHERE tenant_id = $1 AND id = ANY($2::uuid[])",
9148
+ [tenantId, ids]
9149
+ );
9150
+ return result.rows.map(map);
9151
+ }
9152
+ async create(tenantId, input) {
9153
+ await this.ready();
9154
+ try {
9155
+ const result = await this.pool.query(
9156
+ `INSERT INTO lattice_capability_bundles
9157
+ (id, tenant_id, bundle_key, name, description, capabilities)
9158
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb)
9159
+ RETURNING id, tenant_id, bundle_key, name, description, capabilities, created_at, updated_at::text AS updated_at`,
9160
+ [
9161
+ randomUUID3(),
9162
+ tenantId,
9163
+ input.key,
9164
+ input.name,
9165
+ input.description ?? null,
9166
+ JSON.stringify(input.capabilities)
9167
+ ]
9168
+ );
9169
+ return map(result.rows[0]);
9170
+ } catch (error) {
9171
+ if (isDuplicate(error)) throw new Error(duplicateMessage);
9172
+ throw error;
9173
+ }
9174
+ }
9175
+ async update(tenantId, id, input) {
9176
+ await this.ready();
9177
+ const fields = [];
9178
+ const values = [];
9179
+ const add = (field, value) => {
9180
+ values.push(value);
9181
+ fields.push(`${field} = $${values.length}`);
9182
+ };
9183
+ if (input.name !== void 0) add("name", input.name);
9184
+ if (Object.prototype.hasOwnProperty.call(input, "description")) {
9185
+ add("description", input.description ?? null);
9186
+ }
9187
+ if (input.capabilities !== void 0) {
9188
+ add("capabilities", JSON.stringify(input.capabilities));
9189
+ }
9190
+ if (fields.length === 0) {
9191
+ const expectedUpdatedAt2 = input.expectedUpdatedAt;
9192
+ const result = await this.pool.query(
9193
+ `SELECT id, tenant_id, bundle_key, name, description, capabilities, created_at, updated_at::text AS updated_at
9194
+ FROM lattice_capability_bundles
9195
+ WHERE tenant_id = $1 AND id = $2${expectedUpdatedAt2 === void 0 ? "" : " AND updated_at::text = $3"}`,
9196
+ expectedUpdatedAt2 === void 0 ? [tenantId, id] : [tenantId, id, expectedUpdatedAt2]
9197
+ );
9198
+ if (result.rows[0]) return map(result.rows[0]);
9199
+ return expectedUpdatedAt2 === void 0 ? null : { status: "conflict" };
9200
+ }
9201
+ const expectedUpdatedAt = input.expectedUpdatedAt;
9202
+ values.push(tenantId, id);
9203
+ if (expectedUpdatedAt !== void 0) values.push(expectedUpdatedAt);
9204
+ const tenantParam = values.length - (expectedUpdatedAt === void 0 ? 1 : 2);
9205
+ const idParam = tenantParam + 1;
9206
+ const revisionPredicate = expectedUpdatedAt === void 0 ? "" : ` AND updated_at::text = $${values.length}`;
9207
+ try {
9208
+ const result = await this.pool.query(
9209
+ `UPDATE lattice_capability_bundles
9210
+ SET ${fields.join(", ")}, updated_at = GREATEST(updated_at + interval '1 microsecond', clock_timestamp())
9211
+ WHERE tenant_id = $${tenantParam} AND id = $${idParam}${revisionPredicate}
9212
+ RETURNING id, tenant_id, bundle_key, name, description, capabilities, created_at, updated_at::text AS updated_at`,
9213
+ values
9214
+ );
9215
+ if (result.rows[0]) return map(result.rows[0]);
9216
+ return expectedUpdatedAt === void 0 ? null : { status: "conflict" };
9217
+ } catch (error) {
9218
+ if (isDuplicate(error)) throw new Error(duplicateMessage);
9219
+ throw error;
9220
+ }
9221
+ }
9222
+ async deleteIfUnreferenced(tenantId, id) {
9223
+ await this.ready();
9224
+ const client = await this.pool.connect();
9225
+ try {
9226
+ await client.query("BEGIN");
9227
+ await client.query(
9228
+ "SELECT pg_advisory_xact_lock(hashtextextended($1 || ':project-mutations', 0))",
9229
+ [tenantId]
9230
+ );
9231
+ const projectIds = await client.query(
9232
+ `SELECT id FROM lattice_projects
9233
+ WHERE tenant_id = $1
9234
+ ORDER BY id`,
9235
+ [tenantId]
9236
+ );
9237
+ if (projectIds.rows.length > 0) {
9238
+ await client.query(
9239
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || ':project:' || project_id, 0))
9240
+ FROM unnest($2::text[]) AS project_id
9241
+ ORDER BY project_id`,
9242
+ [tenantId, projectIds.rows.map((project) => project.id)]
9243
+ );
9244
+ }
9245
+ await client.query(
9246
+ `SELECT id FROM lattice_projects
9247
+ WHERE tenant_id = $1
9248
+ FOR UPDATE`,
9249
+ [tenantId]
9250
+ );
9251
+ await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1 || ':' || $2, 0))", [tenantId, id]);
9252
+ const referenced = await client.query(
9253
+ `SELECT 1 FROM lattice_projects
9254
+ WHERE tenant_id = $1
9255
+ AND jsonb_typeof(config->'capabilityBundleIds') = 'array'
9256
+ AND COALESCE(config->'capabilityBundleIds', '[]'::jsonb) ? $2
9257
+ LIMIT 1`,
9258
+ [tenantId, id]
9259
+ );
9260
+ if (referenced.rows.length > 0) {
9261
+ await client.query("COMMIT");
9262
+ return "in_use";
9263
+ }
9264
+ const deleted = await client.query(
9265
+ "DELETE FROM lattice_capability_bundles WHERE tenant_id = $1 AND id = $2 RETURNING id",
9266
+ [tenantId, id]
9267
+ );
9268
+ await client.query("COMMIT");
9269
+ return deleted.rows.length > 0 ? "deleted" : "not_found";
9270
+ } catch (error) {
9271
+ await client.query("ROLLBACK");
9272
+ throw error;
9273
+ } finally {
9274
+ client.release();
9275
+ }
9276
+ }
9277
+ };
8750
9278
 
8751
9279
  // src/createPgStoreConfig.ts
8752
9280
  async function createPgStoreConfig(connectionString) {
8753
- const pool = new Pool24({ connectionString });
9281
+ const pool = new Pool25({ connectionString });
8754
9282
  const mm = new MigrationManager(pool);
8755
9283
  mm.register(createThreadsTable);
8756
9284
  mm.register(createScheduledTasksTable);
@@ -8805,6 +9333,8 @@ async function createPgStoreConfig(connectionString) {
8805
9333
  mm.register(addA2AKeyAssistantIds);
8806
9334
  mm.register(addTaskWorkItemEventKeyMigration);
8807
9335
  mm.register(createAgentWebAppsTable);
9336
+ mm.register(createCapabilityBundlesTable);
9337
+ mm.register(addTaskWorkItemPendingIndexesMigration);
8808
9338
  await mm.migrate();
8809
9339
  const checkpoint = PostgresSaver.fromConnString(connectionString);
8810
9340
  checkpoint.setup().catch((err) => {
@@ -8833,6 +9363,7 @@ async function createPgStoreConfig(connectionString) {
8833
9363
  taskWorkItem: taskWorkItemStore,
8834
9364
  a2aApiKey: new PostgreSQLA2AApiKeyStore(opts),
8835
9365
  agentWebApp: new PostgreSQLAgentWebAppStore(opts),
9366
+ capabilityBundle: new PostgreSQLCapabilityBundleStore(opts),
8836
9367
  schedule: new PostgreSQLScheduleStorage(opts),
8837
9368
  menu: new MenuStore(opts),
8838
9369
  sharedResource: new PostgresSharedResourceStore(opts),
@@ -8843,7 +9374,7 @@ async function createPgStoreConfig(connectionString) {
8843
9374
  }
8844
9375
 
8845
9376
  // src/stores/PostgreSQLSkillStore.ts
8846
- import { Pool as Pool25 } from "pg";
9377
+ import { Pool as Pool26 } from "pg";
8847
9378
  var PostgreSQLSkillStore = class {
8848
9379
  constructor(options) {
8849
9380
  this.initialized = false;
@@ -8856,9 +9387,9 @@ var PostgreSQLSkillStore = class {
8856
9387
  return;
8857
9388
  }
8858
9389
  if (typeof options.poolConfig === "string") {
8859
- this.pool = new Pool25({ connectionString: options.poolConfig });
9390
+ this.pool = new Pool26({ connectionString: options.poolConfig });
8860
9391
  } else if (options.poolConfig) {
8861
- this.pool = new Pool25(options.poolConfig);
9392
+ this.pool = new Pool26(options.poolConfig);
8862
9393
  } else {
8863
9394
  throw new Error("Either pool or poolConfig must be provided");
8864
9395
  }
@@ -9159,7 +9690,7 @@ var PostgreSQLSkillStore = class {
9159
9690
  };
9160
9691
 
9161
9692
  // src/stores/ChannelIdentityMappingStore.ts
9162
- import { Pool as Pool26 } from "pg";
9693
+ import { Pool as Pool27 } from "pg";
9163
9694
  var ChannelIdentityMappingStore = class {
9164
9695
  constructor(options) {
9165
9696
  this.initialized = false;
@@ -9171,7 +9702,7 @@ var ChannelIdentityMappingStore = class {
9171
9702
  this.initialized = true;
9172
9703
  return;
9173
9704
  }
9174
- this.pool = typeof options.poolConfig === "string" ? new Pool26({ connectionString: options.poolConfig }) : options.poolConfig ? new Pool26(options.poolConfig) : (() => {
9705
+ this.pool = typeof options.poolConfig === "string" ? new Pool27({ connectionString: options.poolConfig }) : options.poolConfig ? new Pool27(options.poolConfig) : (() => {
9175
9706
  throw new Error("Either pool or poolConfig must be provided");
9176
9707
  })();
9177
9708
  this.migrationManager = new MigrationManager(this.pool);
@@ -9395,10 +9926,11 @@ export {
9395
9926
  MenuStore,
9396
9927
  MigrationManager,
9397
9928
  PGVectorStoreProvider,
9398
- Pool27 as Pool,
9929
+ Pool28 as Pool,
9399
9930
  PostgreSQLA2AApiKeyStore,
9400
9931
  PostgreSQLAgentWebAppStore,
9401
9932
  PostgreSQLAssistantStore,
9933
+ PostgreSQLCapabilityBundleStore,
9402
9934
  PostgreSQLChannelInstallationStore,
9403
9935
  PostgreSQLCollectionStore,
9404
9936
  PostgreSQLConnectionStore,
@@ -9437,6 +9969,7 @@ export {
9437
9969
  createA2AApiKeysTable,
9438
9970
  createAgentWebAppsTable,
9439
9971
  createAssistantsTable,
9972
+ createCapabilityBundlesTable,
9440
9973
  createChannelBindingsTable,
9441
9974
  createChannelIdentityMappingTables,
9442
9975
  createChannelInstallationsTable,