@axiom-lattice/pg-stores 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +18 -0
  3. package/dist/index.d.mts +283 -23
  4. package/dist/index.d.ts +283 -23
  5. package/dist/index.js +1867 -224
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +1854 -217
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +3 -3
  10. package/src/__tests__/ChannelBindingStore.test.ts +122 -0
  11. package/src/__tests__/PostgreSQLCapabilityBundleStore.migrations.test.ts +67 -0
  12. package/src/__tests__/PostgreSQLCapabilityBundleStore.test.ts +383 -0
  13. package/src/__tests__/PostgreSQLChannelInstallationStore.test.ts +16 -0
  14. package/src/__tests__/PostgreSQLProjectBotMembershipStore.test.ts +117 -0
  15. package/src/__tests__/PostgreSQLProjectMembershipStore.test.ts +235 -0
  16. package/src/__tests__/PostgreSQLProjectRoomMessageStore.test.ts +103 -0
  17. package/src/__tests__/PostgreSQLProjectRoomStore.migrations.test.ts +114 -0
  18. package/src/__tests__/PostgreSQLProjectRoomStore.test.ts +48 -0
  19. package/src/__tests__/PostgreSQLTaskStore.integration.test.ts +27 -0
  20. package/src/__tests__/PostgreSQLTaskStore.test.ts +57 -0
  21. package/src/__tests__/PostgreSQLTaskWorkItemStore.integration.test.ts +107 -0
  22. package/src/__tests__/PostgreSQLTaskWorkItemStore.migrations.test.ts +62 -0
  23. package/src/__tests__/PostgreSQLTaskWorkItemStore.test.ts +162 -1
  24. package/src/__tests__/ThreadMessageQueueStore.migrations.test.ts +63 -0
  25. package/src/__tests__/ThreadMessageQueueStore.test.ts +209 -4
  26. package/src/__tests__/add_workspace_project_to_queue.test.ts +15 -0
  27. package/src/__tests__/migration-name-version-compatibility.test.ts +67 -0
  28. package/src/__tests__/task-files.test.ts +4 -3
  29. package/src/createPgStoreConfig.ts +25 -1
  30. package/src/index.ts +13 -0
  31. package/src/migrations/add_trusted_run_context_column.ts +18 -0
  32. package/src/migrations/capability_bundle_migration.ts +20 -0
  33. package/src/migrations/migration.ts +2 -1
  34. package/src/migrations/project_room_migration.ts +128 -0
  35. package/src/migrations/task_migration.ts +15 -0
  36. package/src/migrations/task_work_items_migration.ts +45 -1
  37. package/src/stores/ChannelBindingStore.ts +99 -59
  38. package/src/stores/PostgreSQLCapabilityBundleStore.ts +306 -0
  39. package/src/stores/PostgreSQLChannelInstallationStore.ts +12 -30
  40. package/src/stores/PostgreSQLProjectBotMembershipStore.ts +199 -0
  41. package/src/stores/PostgreSQLProjectMembershipStore.ts +140 -0
  42. package/src/stores/PostgreSQLProjectRoomMessageStore.ts +177 -0
  43. package/src/stores/PostgreSQLProjectRoomStore.ts +57 -0
  44. package/src/stores/PostgreSQLProjectStore.ts +230 -50
  45. package/src/stores/PostgreSQLTaskStore.ts +89 -3
  46. package/src/stores/PostgreSQLTaskWorkItemStore.ts +198 -8
  47. package/src/stores/ThreadMessageQueueStore.ts +130 -32
package/dist/index.js CHANGED
@@ -32,13 +32,16 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ChannelBindingStore: () => ChannelBindingStore,
34
34
  ChannelIdentityMappingStore: () => ChannelIdentityMappingStore,
35
+ DuplicateProjectMembershipError: () => DuplicateProjectMembershipError,
36
+ DuplicateProjectRoomMessageIdempotencyKeyError: () => DuplicateProjectRoomMessageIdempotencyKeyError,
35
37
  MenuStore: () => MenuStore,
36
38
  MigrationManager: () => MigrationManager,
37
39
  PGVectorStoreProvider: () => PGVectorStoreProvider,
38
- Pool: () => import_pg27.Pool,
40
+ Pool: () => import_pg29.Pool,
39
41
  PostgreSQLA2AApiKeyStore: () => PostgreSQLA2AApiKeyStore,
40
42
  PostgreSQLAgentWebAppStore: () => PostgreSQLAgentWebAppStore,
41
43
  PostgreSQLAssistantStore: () => PostgreSQLAssistantStore,
44
+ PostgreSQLCapabilityBundleStore: () => PostgreSQLCapabilityBundleStore,
42
45
  PostgreSQLChannelInstallationStore: () => PostgreSQLChannelInstallationStore,
43
46
  PostgreSQLCollectionStore: () => PostgreSQLCollectionStore,
44
47
  PostgreSQLConnectionStore: () => PostgreSQLConnectionStore,
@@ -46,9 +49,14 @@ __export(index_exports, {
46
49
  PostgreSQLEvalStore: () => PostgreSQLEvalStore,
47
50
  PostgreSQLMcpServerConfigStore: () => PostgreSQLMcpServerConfigStore,
48
51
  PostgreSQLMetricsServerConfigStore: () => PostgreSQLMetricsServerConfigStore,
52
+ PostgreSQLProjectBotMembershipStore: () => PostgreSQLProjectBotMembershipStore,
53
+ PostgreSQLProjectMembershipStore: () => PostgreSQLProjectMembershipStore,
54
+ PostgreSQLProjectRoomMessageStore: () => PostgreSQLProjectRoomMessageStore,
55
+ PostgreSQLProjectRoomStore: () => PostgreSQLProjectRoomStore,
49
56
  PostgreSQLProjectStore: () => PostgreSQLProjectStore,
50
57
  PostgreSQLScheduleStorage: () => PostgreSQLScheduleStorage,
51
58
  PostgreSQLSkillStore: () => PostgreSQLSkillStore,
59
+ PostgreSQLTaskWorkItemStore: () => PostgreSQLTaskWorkItemStore,
52
60
  PostgreSQLTenantStore: () => PostgreSQLTenantStore,
53
61
  PostgreSQLThreadStore: () => PostgreSQLThreadStore,
54
62
  PostgreSQLUserStore: () => PostgreSQLUserStore,
@@ -56,6 +64,9 @@ __export(index_exports, {
56
64
  PostgreSQLWorkflowTrackingStore: () => PostgreSQLWorkflowTrackingStore,
57
65
  PostgreSQLWorkspaceStore: () => PostgreSQLWorkspaceStore,
58
66
  PostgresSharedResourceStore: () => PostgresSharedResourceStore,
67
+ ProjectBotMembershipIdConflictError: () => ProjectBotMembershipIdConflictError,
68
+ ProjectMembershipIdConflictError: () => ProjectMembershipIdConflictError,
69
+ ProjectRoomMessageIdConflictError: () => ProjectRoomMessageIdConflictError,
59
70
  ThreadMessageQueueStore: () => ThreadMessageQueueStore,
60
71
  addAssistantTenantId: () => addAssistantTenantId,
61
72
  addEnvToEvalRuns: () => addEnvToEvalRuns,
@@ -77,6 +88,7 @@ __export(index_exports, {
77
88
  createA2AApiKeysTable: () => createA2AApiKeysTable,
78
89
  createAgentWebAppsTable: () => createAgentWebAppsTable,
79
90
  createAssistantsTable: () => createAssistantsTable,
91
+ createCapabilityBundlesTable: () => createCapabilityBundlesTable,
80
92
  createChannelBindingsTable: () => createChannelBindingsTable,
81
93
  createChannelIdentityMappingTables: () => createChannelIdentityMappingTables,
82
94
  createChannelInstallationsTable: () => createChannelInstallationsTable,
@@ -92,6 +104,7 @@ __export(index_exports, {
92
104
  createMetricsConfigsTable: () => createMetricsConfigsTable,
93
105
  createPGVectorStoreProvider: () => createPGVectorStoreProvider,
94
106
  createPgStoreConfig: () => createPgStoreConfig,
107
+ createProjectRoomTables: () => createProjectRoomTables,
95
108
  createProjectsTable: () => createProjectsTable,
96
109
  createScheduledTasksTable: () => createScheduledTasksTable,
97
110
  createSharedResourcesTable: () => createSharedResourcesTable,
@@ -109,10 +122,10 @@ __export(index_exports, {
109
122
  safeParse: () => safeParse
110
123
  });
111
124
  module.exports = __toCommonJS(index_exports);
112
- var import_pg27 = require("pg");
125
+ var import_pg29 = require("pg");
113
126
 
114
127
  // src/createPgStoreConfig.ts
115
- var import_pg24 = require("pg");
128
+ var import_pg26 = require("pg");
116
129
 
117
130
  // src/migrations/migration.ts
118
131
  var MigrationManager = class {
@@ -143,7 +156,8 @@ var MigrationManager = class {
143
156
  const tableExists = await client.query(`
144
157
  SELECT EXISTS (
145
158
  SELECT FROM information_schema.tables
146
- WHERE table_name = 'lattice_schema_migrations'
159
+ WHERE table_schema = current_schema()
160
+ AND table_name = 'lattice_schema_migrations'
147
161
  )
148
162
  `);
149
163
  if (!tableExists.rows[0].exists) {
@@ -2382,6 +2396,7 @@ var PostgreSQLWorkspaceStore = class {
2382
2396
 
2383
2397
  // src/stores/PostgreSQLProjectStore.ts
2384
2398
  var import_pg7 = require("pg");
2399
+ var import_protocols = require("@axiom-lattice/protocols");
2385
2400
 
2386
2401
  // src/migrations/project_migrations.ts
2387
2402
  var createProjectsTable = {
@@ -2597,34 +2612,62 @@ var PostgreSQLProjectStore = class {
2597
2612
  * Create a new project
2598
2613
  */
2599
2614
  async createProject(tenantId, workspaceId, id, data) {
2615
+ (0, import_protocols.assertGenericProjectConfig)(data.config);
2600
2616
  await this.ensureInitialized();
2601
2617
  const now = /* @__PURE__ */ new Date();
2602
2618
  const kind = data.kind || "business";
2603
- await this.pool.query(
2604
- `
2605
- INSERT INTO lattice_projects (id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at)
2619
+ const client = await this.pool.connect();
2620
+ try {
2621
+ await client.query("BEGIN");
2622
+ await client.query(
2623
+ "SELECT pg_advisory_xact_lock(hashtextextended($1 || ':project-mutations', 0))",
2624
+ [tenantId]
2625
+ );
2626
+ await client.query(
2627
+ "SELECT pg_advisory_xact_lock(hashtextextended($2 || ':project:' || $1, 0))",
2628
+ [id, tenantId]
2629
+ );
2630
+ const existing = await client.query(
2631
+ "SELECT config FROM lattice_projects WHERE id = $1 AND tenant_id = $2 FOR UPDATE",
2632
+ [id, tenantId]
2633
+ );
2634
+ const ids = existing.rows[0]?.config?.capabilityBundleIds;
2635
+ const bundleIds = Array.isArray(ids) && ids.every((bundleId) => typeof bundleId === "string") ? [...ids].sort() : [];
2636
+ if (bundleIds.length > 0) {
2637
+ await client.query(
2638
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || ':' || bundle_id, 0))
2639
+ FROM unnest($2::text[]) AS bundle_id
2640
+ ORDER BY bundle_id`,
2641
+ [tenantId, bundleIds]
2642
+ );
2643
+ }
2644
+ const result = await client.query(
2645
+ `INSERT INTO lattice_projects (id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at)
2606
2646
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
2607
2647
  ON CONFLICT (id, tenant_id) DO UPDATE SET
2608
2648
  workspace_id = EXCLUDED.workspace_id,
2609
2649
  name = EXCLUDED.name,
2610
2650
  description = EXCLUDED.description,
2611
- config = EXCLUDED.config,
2651
+ config = CASE
2652
+ WHEN lattice_projects.config ? 'capabilityBundleIds'
2653
+ THEN COALESCE(EXCLUDED.config, '{}'::jsonb)
2654
+ || jsonb_build_object('capabilityBundleIds', lattice_projects.config->'capabilityBundleIds')
2655
+ ELSE EXCLUDED.config
2656
+ END,
2612
2657
  kind = EXCLUDED.kind,
2613
2658
  updated_at = EXCLUDED.updated_at
2659
+ RETURNING id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at
2614
2660
  `,
2615
- [id, tenantId, workspaceId, data.name, data.description || null, data.config || null, kind, now, now]
2616
- );
2617
- return {
2618
- id,
2619
- tenantId,
2620
- workspaceId,
2621
- name: data.name,
2622
- description: data.description,
2623
- config: data.config,
2624
- kind,
2625
- createdAt: now,
2626
- updatedAt: now
2627
- };
2661
+ [id, tenantId, workspaceId, data.name, data.description || null, data.config || null, kind, now, now]
2662
+ );
2663
+ await client.query("COMMIT");
2664
+ return this.mapRowToProject(result.rows[0]);
2665
+ } catch (error) {
2666
+ await client.query("ROLLBACK");
2667
+ throw error;
2668
+ } finally {
2669
+ client.release();
2670
+ }
2628
2671
  }
2629
2672
  /**
2630
2673
  * Update an existing project
@@ -2634,11 +2677,8 @@ var PostgreSQLProjectStore = class {
2634
2677
  * overwrites the existing config. To preserve the current config, omit this field.
2635
2678
  */
2636
2679
  async updateProject(tenantId, id, updates) {
2680
+ (0, import_protocols.assertGenericProjectConfig)(updates.config);
2637
2681
  await this.ensureInitialized();
2638
- const existing = await this.getProjectById(tenantId, id);
2639
- if (!existing) {
2640
- return null;
2641
- }
2642
2682
  const updateFields = [];
2643
2683
  const updateValues = [];
2644
2684
  let paramIndex = 1;
@@ -2651,43 +2691,190 @@ var PostgreSQLProjectStore = class {
2651
2691
  updateValues.push(updates.description || null);
2652
2692
  }
2653
2693
  if (updates.config !== void 0) {
2654
- updateFields.push(`config = $${paramIndex++}`);
2655
- updateValues.push(updates.config || null);
2694
+ const configParam = `$${paramIndex++}`;
2695
+ updateFields.push(`config = CASE
2696
+ WHEN config ? 'capabilityBundleIds'
2697
+ THEN COALESCE(${configParam}::jsonb, '{}'::jsonb)
2698
+ || jsonb_build_object('capabilityBundleIds', config->'capabilityBundleIds')
2699
+ ELSE ${configParam}::jsonb
2700
+ END`);
2701
+ updateValues.push(updates.config);
2656
2702
  }
2657
2703
  if (updates.kind !== void 0) {
2658
2704
  updateFields.push(`kind = $${paramIndex++}`);
2659
2705
  updateValues.push(updates.kind);
2660
2706
  }
2661
2707
  if (updateFields.length === 0) {
2662
- return existing;
2708
+ return await this.getProjectById(tenantId, id);
2663
2709
  }
2664
2710
  updateFields.push(`updated_at = $${paramIndex++}`);
2665
2711
  updateValues.push(/* @__PURE__ */ new Date());
2666
- updateValues.push(id);
2667
- updateValues.push(tenantId);
2668
- await this.pool.query(
2669
- `
2670
- UPDATE lattice_projects
2671
- SET ${updateFields.join(", ")}
2672
- WHERE id = $${paramIndex} AND tenant_id = $${paramIndex + 1}
2673
- `,
2674
- updateValues
2675
- );
2676
- return await this.getProjectById(tenantId, id);
2712
+ updateValues.push(id, tenantId);
2713
+ const client = await this.pool.connect();
2714
+ try {
2715
+ await client.query("BEGIN");
2716
+ await client.query(
2717
+ "SELECT pg_advisory_xact_lock(hashtextextended($1 || ':project-mutations', 0))",
2718
+ [tenantId]
2719
+ );
2720
+ await client.query(
2721
+ "SELECT pg_advisory_xact_lock(hashtextextended($2 || ':project:' || $1, 0))",
2722
+ [id, tenantId]
2723
+ );
2724
+ const existing = await client.query(
2725
+ "SELECT config FROM lattice_projects WHERE id = $1 AND tenant_id = $2 FOR UPDATE",
2726
+ [id, tenantId]
2727
+ );
2728
+ if (!existing.rows[0]) {
2729
+ await client.query("COMMIT");
2730
+ return null;
2731
+ }
2732
+ const ids = existing.rows[0].config?.capabilityBundleIds;
2733
+ const bundleIds = Array.isArray(ids) && ids.every((bundleId) => typeof bundleId === "string") ? [...ids].sort() : [];
2734
+ if (bundleIds.length > 0) {
2735
+ await client.query(
2736
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || ':' || bundle_id, 0))
2737
+ FROM unnest($2::text[]) AS bundle_id
2738
+ ORDER BY bundle_id`,
2739
+ [tenantId, bundleIds]
2740
+ );
2741
+ }
2742
+ const result = await client.query(
2743
+ `UPDATE lattice_projects SET ${updateFields.join(", ")}
2744
+ WHERE id = $${paramIndex} AND tenant_id = $${paramIndex + 1}
2745
+ RETURNING id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at`,
2746
+ updateValues
2747
+ );
2748
+ await client.query("COMMIT");
2749
+ return this.mapRowToProject(result.rows[0]);
2750
+ } catch (error) {
2751
+ await client.query("ROLLBACK");
2752
+ throw error;
2753
+ } finally {
2754
+ client.release();
2755
+ }
2677
2756
  }
2678
2757
  /**
2679
2758
  * Delete a project by ID
2680
2759
  */
2681
2760
  async deleteProject(tenantId, id) {
2761
+ await this.ensureInitialized();
2762
+ const client = await this.pool.connect();
2763
+ try {
2764
+ await client.query("BEGIN");
2765
+ await client.query(
2766
+ "SELECT pg_advisory_xact_lock(hashtextextended($1 || ':project-mutations', 0))",
2767
+ [tenantId]
2768
+ );
2769
+ await client.query(
2770
+ "SELECT pg_advisory_xact_lock(hashtextextended($2 || ':project:' || $1, 0))",
2771
+ [id, tenantId]
2772
+ );
2773
+ const selected = await client.query(
2774
+ "SELECT config FROM lattice_projects WHERE id = $1 AND tenant_id = $2 FOR UPDATE",
2775
+ [id, tenantId]
2776
+ );
2777
+ if (!selected.rows[0]) {
2778
+ await client.query("COMMIT");
2779
+ return false;
2780
+ }
2781
+ const ids = selected.rows[0].config?.capabilityBundleIds;
2782
+ const bundleIds = Array.isArray(ids) && ids.every((bundleId) => typeof bundleId === "string") ? [...ids].sort() : [];
2783
+ if (bundleIds.length > 0) {
2784
+ await client.query(
2785
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || ':' || bundle_id, 0))
2786
+ FROM unnest($2::text[]) AS bundle_id
2787
+ ORDER BY bundle_id`,
2788
+ [tenantId, bundleIds]
2789
+ );
2790
+ }
2791
+ const result = await client.query(
2792
+ "DELETE FROM lattice_projects WHERE id = $1 AND tenant_id = $2",
2793
+ [id, tenantId]
2794
+ );
2795
+ await client.query("COMMIT");
2796
+ return result.rowCount !== null && result.rowCount > 0;
2797
+ } catch (error) {
2798
+ await client.query("ROLLBACK");
2799
+ throw error;
2800
+ } finally {
2801
+ client.release();
2802
+ }
2803
+ }
2804
+ async updateCapabilityBundleIds(tenantId, projectId, bundleIds, expectedRevisions = {}) {
2805
+ await this.ensureInitialized();
2806
+ const client = await this.pool.connect();
2807
+ try {
2808
+ await client.query("BEGIN");
2809
+ await client.query(
2810
+ "SELECT pg_advisory_xact_lock(hashtextextended($1 || ':project-mutations', 0))",
2811
+ [tenantId]
2812
+ );
2813
+ await client.query(
2814
+ "SELECT pg_advisory_xact_lock(hashtextextended($2 || ':project:' || $1, 0))",
2815
+ [projectId, tenantId]
2816
+ );
2817
+ await client.query(
2818
+ "SELECT id FROM lattice_projects WHERE id = $1 AND tenant_id = $2 FOR UPDATE",
2819
+ [projectId, tenantId]
2820
+ );
2821
+ if (bundleIds.length > 0) {
2822
+ await client.query(
2823
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || ':' || bundle_id, 0))
2824
+ FROM unnest($2::text[]) AS bundle_id
2825
+ ORDER BY bundle_id`,
2826
+ [tenantId, [...bundleIds].sort()]
2827
+ );
2828
+ if (Object.keys(expectedRevisions).length > 0) {
2829
+ 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]);
2830
+ if (revisions.rows.some((bundle) => expectedRevisions[bundle.id] !== void 0 && expectedRevisions[bundle.id] !== bundle.updated_at)) {
2831
+ await client.query("ROLLBACK");
2832
+ return { status: "bundle_conflict" };
2833
+ }
2834
+ }
2835
+ }
2836
+ const result = await client.query(
2837
+ `UPDATE lattice_projects
2838
+ SET config = COALESCE(config, '{}'::jsonb) || jsonb_build_object('capabilityBundleIds', $3::jsonb), updated_at = NOW()
2839
+ WHERE id = $1 AND tenant_id = $2
2840
+ AND NOT EXISTS (
2841
+ SELECT 1 FROM unnest($4::uuid[]) AS bundle_id
2842
+ WHERE NOT EXISTS (
2843
+ SELECT 1 FROM lattice_capability_bundles
2844
+ WHERE tenant_id = $2 AND id = bundle_id
2845
+ )
2846
+ )
2847
+ RETURNING id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at`,
2848
+ [projectId, tenantId, JSON.stringify(bundleIds), bundleIds]
2849
+ );
2850
+ if (result.rows[0]) {
2851
+ await client.query("COMMIT");
2852
+ return { status: "updated", project: this.mapRowToProject(result.rows[0]) };
2853
+ }
2854
+ const project = await client.query(
2855
+ "SELECT id FROM lattice_projects WHERE id = $1 AND tenant_id = $2",
2856
+ [projectId, tenantId]
2857
+ );
2858
+ await client.query("COMMIT");
2859
+ return project.rows.length > 0 ? { status: "bundle_not_found" } : { status: "project_not_found" };
2860
+ } catch (error) {
2861
+ await client.query("ROLLBACK");
2862
+ throw error;
2863
+ } finally {
2864
+ client.release();
2865
+ }
2866
+ }
2867
+ async isCapabilityBundleReferenced(tenantId, bundleId) {
2682
2868
  await this.ensureInitialized();
2683
2869
  const result = await this.pool.query(
2684
- `
2685
- DELETE FROM lattice_projects
2686
- WHERE id = $1 AND tenant_id = $2
2687
- `,
2688
- [id, tenantId]
2870
+ `SELECT 1 AS exists FROM lattice_projects
2871
+ WHERE tenant_id = $1
2872
+ AND jsonb_typeof(config->'capabilityBundleIds') = 'array'
2873
+ AND COALESCE(config->'capabilityBundleIds', '[]'::jsonb) ? $2
2874
+ LIMIT 1`,
2875
+ [tenantId, bundleId]
2689
2876
  );
2690
- return result.rowCount !== null && result.rowCount > 0;
2877
+ return result.rows.length > 0;
2691
2878
  }
2692
2879
  };
2693
2880
 
@@ -4967,6 +5154,7 @@ var PostgreSQLEvalStore = class {
4967
5154
  // src/stores/ThreadMessageQueueStore.ts
4968
5155
  var import_pg13 = require("pg");
4969
5156
  var import_crypto = __toESM(require("crypto"));
5157
+ var import_protocols2 = require("@axiom-lattice/protocols");
4970
5158
 
4971
5159
  // src/migrations/thread_message_queue_migrations.ts
4972
5160
  var createThreadMessageQueueTable = {
@@ -5103,6 +5291,23 @@ var addWorkspaceProjectToQueue = {
5103
5291
  }
5104
5292
  };
5105
5293
 
5294
+ // src/migrations/add_trusted_run_context_column.ts
5295
+ var addTrustedRunContextColumn = {
5296
+ version: 173,
5297
+ name: "add_thread_queue_trusted_run_context",
5298
+ up: async (client) => {
5299
+ await client.query(`ALTER TABLE lattice_thread_message_queue
5300
+ ADD COLUMN IF NOT EXISTS trusted_run_context JSONB,
5301
+ ADD COLUMN IF NOT EXISTS execution_mode VARCHAR(20)
5302
+ CHECK (execution_mode IS NULL OR execution_mode = 'followup')`);
5303
+ },
5304
+ down: async (client) => {
5305
+ await client.query(`ALTER TABLE lattice_thread_message_queue
5306
+ DROP COLUMN IF EXISTS execution_mode,
5307
+ DROP COLUMN IF EXISTS trusted_run_context`);
5308
+ }
5309
+ };
5310
+
5106
5311
  // src/stores/ThreadMessageQueueStore.ts
5107
5312
  var ThreadMessageQueueStore = class {
5108
5313
  constructor(options) {
@@ -5128,6 +5333,7 @@ var ThreadMessageQueueStore = class {
5128
5333
  this.migrationManager.register(addCustomRunConfigColumn);
5129
5334
  this.migrationManager.register(alterMessageQueueIdColumn);
5130
5335
  this.migrationManager.register(addWorkspaceProjectToQueue);
5336
+ this.migrationManager.register(addTrustedRunContextColumn);
5131
5337
  if (options.autoMigrate !== false) {
5132
5338
  this.initialize().catch((error) => {
5133
5339
  console.error("Failed to initialize ThreadMessageQueueStore:", error);
@@ -5157,6 +5363,7 @@ var ThreadMessageQueueStore = class {
5157
5363
  * Add message to queue
5158
5364
  */
5159
5365
  async addMessage(params) {
5366
+ const trusted = validateQueueTrust(params);
5160
5367
  const { threadId, tenantId, assistantId, workspaceId, projectId, content, type = "human", priority = 0, command, custom_run_config, id } = params;
5161
5368
  const seqResult = await this.pool.query(
5162
5369
  `SELECT COALESCE(MAX(sequence_order), 0) + 1 as next_seq
@@ -5167,18 +5374,58 @@ var ThreadMessageQueueStore = class {
5167
5374
  const nextSeq = seqResult.rows[0].next_seq;
5168
5375
  const result = await this.pool.query(
5169
5376
  `INSERT INTO lattice_thread_message_queue
5170
- (id, thread_id, tenant_id, assistant_id, workspace_id, project_id, message_content, message_type, sequence_order, priority, command, custom_run_config)
5171
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
5377
+ (id, thread_id, tenant_id, assistant_id, workspace_id, project_id, message_content, message_type, sequence_order, priority, command, custom_run_config, trusted_run_context, execution_mode)
5378
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
5172
5379
  RETURNING *`,
5173
- [id || import_crypto.default.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]
5380
+ [id || import_crypto.default.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, trusted.trustedRunContext ? JSON.stringify(trusted.trustedRunContext) : null, trusted.executionMode ?? null]
5174
5381
  );
5175
5382
  return this.rowToMessage(result.rows[0]);
5176
5383
  }
5384
+ async addMessageIfCapacity(params, maxSize) {
5385
+ const trusted = validateQueueTrust(params);
5386
+ if (maxSize === Infinity) {
5387
+ await this.addMessage(params);
5388
+ return true;
5389
+ }
5390
+ const client = await this.pool.connect();
5391
+ try {
5392
+ await client.query("BEGIN");
5393
+ const scope = { tenantId: params.tenantId, assistantId: params.assistantId, workspaceId: params.workspaceId, projectId: params.projectId };
5394
+ const lockKey = `${params.tenantId}:${params.assistantId}:${params.threadId}:${params.workspaceId ?? ""}:${params.projectId ?? ""}`;
5395
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [lockKey]);
5396
+ const filter = scopeClause(scope, 2);
5397
+ const count = await client.query(
5398
+ `SELECT COUNT(*) as count FROM lattice_thread_message_queue WHERE thread_id = $1 AND status = 'pending'${filter.sql}`,
5399
+ [params.threadId, ...filter.params]
5400
+ );
5401
+ if (parseInt(count.rows[0].count, 10) >= maxSize) {
5402
+ await client.query("ROLLBACK");
5403
+ return false;
5404
+ }
5405
+ const seq = await client.query(
5406
+ `SELECT COALESCE(MAX(sequence_order), 0) + 1 as next_seq FROM lattice_thread_message_queue WHERE thread_id = $1`,
5407
+ [params.threadId]
5408
+ );
5409
+ const result = await client.query(
5410
+ `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, trusted_run_context, execution_mode)
5411
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *`,
5412
+ [params.id || import_crypto.default.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, trusted.trustedRunContext ? JSON.stringify(trusted.trustedRunContext) : null, trusted.executionMode ?? null]
5413
+ );
5414
+ await client.query("COMMIT");
5415
+ return Boolean(result.rows[0]);
5416
+ } catch (error) {
5417
+ await client.query("ROLLBACK");
5418
+ throw error;
5419
+ } finally {
5420
+ client.release();
5421
+ }
5422
+ }
5177
5423
  /**
5178
5424
  * Add message at head of queue (high priority, e.g., STEER/Command messages)
5179
5425
  * Uses priority=100 to ensure message is processed first
5180
5426
  */
5181
5427
  async addMessageAtHead(params) {
5428
+ const trusted = validateQueueTrust(params);
5182
5429
  const { threadId, tenantId, assistantId, workspaceId, projectId, content, type = "human", command, custom_run_config, id } = params;
5183
5430
  const resolvedTenantId = tenantId;
5184
5431
  const resolvedAssistantId = assistantId;
@@ -5191,45 +5438,48 @@ var ThreadMessageQueueStore = class {
5191
5438
  const nextSeq = seqResult.rows[0].next_seq;
5192
5439
  const result = await this.pool.query(
5193
5440
  `INSERT INTO lattice_thread_message_queue
5194
- (id, thread_id, tenant_id, assistant_id, workspace_id, project_id, message_content, message_type, sequence_order, priority, command, custom_run_config)
5195
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 100, $10, $11)
5441
+ (id, thread_id, tenant_id, assistant_id, workspace_id, project_id, message_content, message_type, sequence_order, priority, command, custom_run_config, trusted_run_context, execution_mode)
5442
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 100, $10, $11, $12, $13)
5196
5443
  RETURNING *`,
5197
- [id || import_crypto.default.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]
5444
+ [id || import_crypto.default.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, trusted.trustedRunContext ? JSON.stringify(trusted.trustedRunContext) : null, trusted.executionMode ?? null]
5198
5445
  );
5199
5446
  return this.rowToMessage(result.rows[0]);
5200
5447
  }
5201
5448
  /**
5202
5449
  * Get pending messages for thread
5203
5450
  */
5204
- async getPendingMessages(threadId) {
5451
+ async getPendingMessages(threadId, scope) {
5452
+ const filter = scopeClause(scope, 2);
5205
5453
  const result = await this.pool.query(
5206
- `SELECT * FROM lattice_thread_message_queue
5207
- WHERE thread_id = $1 AND status = 'pending'
5454
+ `SELECT * FROM lattice_thread_message_queue
5455
+ WHERE thread_id = $1 AND status = 'pending'${filter.sql}
5208
5456
  ORDER BY priority DESC, sequence_order ASC`,
5209
- [threadId]
5457
+ [threadId, ...filter.params]
5210
5458
  );
5211
5459
  return result.rows.map((row) => this.rowToMessage(row));
5212
5460
  }
5213
5461
  /**
5214
5462
  * Get processing messages for a thread
5215
5463
  */
5216
- async getProcessingMessages(threadId) {
5464
+ async getProcessingMessages(threadId, scope) {
5465
+ const filter = scopeClause(scope, 2);
5217
5466
  const result = await this.pool.query(
5218
5467
  `SELECT * FROM lattice_thread_message_queue
5219
- WHERE thread_id = $1 AND status = 'processing'
5468
+ WHERE thread_id = $1 AND status = 'processing'${filter.sql}
5220
5469
  ORDER BY priority DESC, sequence_order ASC`,
5221
- [threadId]
5470
+ [threadId, ...filter.params]
5222
5471
  );
5223
5472
  return result.rows.map((row) => this.rowToMessage(row));
5224
5473
  }
5225
5474
  /**
5226
5475
  * Get queue size
5227
5476
  */
5228
- async getQueueSize(threadId) {
5477
+ async getQueueSize(threadId, scope) {
5478
+ const filter = scopeClause(scope, 2);
5229
5479
  const result = await this.pool.query(
5230
5480
  `SELECT COUNT(*) as count FROM lattice_thread_message_queue
5231
- WHERE thread_id = $1 AND status = 'pending'`,
5232
- [threadId]
5481
+ WHERE thread_id = $1 AND status = 'pending'${filter.sql}`,
5482
+ [threadId, ...filter.params]
5233
5483
  );
5234
5484
  return parseInt(result.rows[0].count, 10);
5235
5485
  }
@@ -5238,58 +5488,70 @@ var ThreadMessageQueueStore = class {
5238
5488
  */
5239
5489
  async getThreadsWithPendingMessages() {
5240
5490
  const result = await this.pool.query(
5241
- `SELECT DISTINCT ON (thread_id) tenant_id, assistant_id, thread_id, workspace_id, project_id
5491
+ `SELECT DISTINCT ON (tenant_id, assistant_id, workspace_id, project_id, thread_id) tenant_id, assistant_id, thread_id, workspace_id, project_id
5242
5492
  FROM lattice_thread_message_queue
5243
5493
  WHERE status IN ('pending', 'processing')
5244
- ORDER BY thread_id`
5494
+ ORDER BY tenant_id, assistant_id, workspace_id, project_id, thread_id`
5245
5495
  );
5246
5496
  return result.rows.map((row) => ({
5247
5497
  tenantId: row.tenant_id,
5248
5498
  assistantId: row.assistant_id,
5249
5499
  threadId: row.thread_id,
5250
- workspaceId: row.workspace_id || void 0,
5251
- projectId: row.project_id || void 0
5500
+ workspaceId: row.workspace_id,
5501
+ projectId: row.project_id
5252
5502
  }));
5253
5503
  }
5254
5504
  /**
5255
5505
  * Remove message
5256
5506
  */
5257
- async removeMessage(messageId) {
5507
+ async removeMessage(messageId, scope) {
5508
+ const filter = scopeClause(scope, 2);
5258
5509
  const result = await this.pool.query(
5259
- `DELETE FROM lattice_thread_message_queue WHERE id = $1 RETURNING id`,
5260
- [messageId]
5510
+ `DELETE FROM lattice_thread_message_queue WHERE id = $1${filter.sql} RETURNING id`,
5511
+ [messageId, ...filter.params]
5261
5512
  );
5262
5513
  return (result.rowCount ?? 0) > 0;
5263
5514
  }
5264
5515
  /**
5265
5516
  * Clear all messages for thread
5266
5517
  */
5267
- async clearMessages(threadId) {
5518
+ async clearMessages(threadId, scope) {
5519
+ const filter = scopeClause(scope, 2);
5268
5520
  await this.pool.query(
5269
- `DELETE FROM lattice_thread_message_queue WHERE thread_id = $1`,
5270
- [threadId]
5521
+ `DELETE FROM lattice_thread_message_queue WHERE thread_id = $1${filter.sql}`,
5522
+ [threadId, ...filter.params]
5271
5523
  );
5272
5524
  }
5273
5525
  /**
5274
5526
  * Mark message as processing
5275
5527
  */
5276
- async markProcessing(messageId) {
5528
+ async markProcessing(messageId, customRunConfig, scope) {
5529
+ if (customRunConfig !== void 0) {
5530
+ const filter2 = scopeClause(scope, 3);
5531
+ await this.pool.query(
5532
+ `UPDATE lattice_thread_message_queue SET status = 'processing', custom_run_config = $2 WHERE id = $1${filter2.sql}`,
5533
+ [messageId, JSON.stringify(customRunConfig), ...filter2.params]
5534
+ );
5535
+ return;
5536
+ }
5537
+ const filter = scopeClause(scope, 2);
5277
5538
  await this.pool.query(
5278
- `UPDATE lattice_thread_message_queue SET status = 'processing' WHERE id = $1`,
5279
- [messageId]
5539
+ `UPDATE lattice_thread_message_queue SET status = 'processing' WHERE id = $1${filter.sql}`,
5540
+ [messageId, ...filter.params]
5280
5541
  );
5281
5542
  }
5282
5543
  /**
5283
5544
  * Reset all processing messages to pending state for a thread
5284
5545
  * Returns the number of messages reset
5285
5546
  */
5286
- async resetProcessingToPending(threadId) {
5547
+ async resetProcessingToPending(threadId, scope) {
5548
+ const filter = scopeClause(scope, 2);
5287
5549
  const result = await this.pool.query(
5288
5550
  `UPDATE lattice_thread_message_queue
5289
5551
  SET status = 'pending'
5290
- WHERE thread_id = $1 AND status = 'processing'
5552
+ WHERE thread_id = $1 AND status = 'processing'${filter.sql}
5291
5553
  RETURNING id`,
5292
- [threadId]
5554
+ [threadId, ...filter.params]
5293
5555
  );
5294
5556
  return result.rowCount ?? 0;
5295
5557
  }
@@ -5302,13 +5564,34 @@ var ThreadMessageQueueStore = class {
5302
5564
  createdAt: new Date(row.created_at),
5303
5565
  priority: row.priority || 0,
5304
5566
  command: row.command ? typeof row.command === "string" ? JSON.parse(row.command) : row.command : void 0,
5305
- custom_run_config: row.custom_run_config ? typeof row.custom_run_config === "string" ? JSON.parse(row.custom_run_config) : row.custom_run_config : void 0
5567
+ custom_run_config: row.custom_run_config ? typeof row.custom_run_config === "string" ? JSON.parse(row.custom_run_config) : row.custom_run_config : void 0,
5568
+ trusted_run_context: row.trusted_run_context ? (0, import_protocols2.parseTrustedRunContext)(typeof row.trusted_run_context === "string" ? JSON.parse(row.trusted_run_context) : row.trusted_run_context) : void 0,
5569
+ execution_mode: row.execution_mode == null ? void 0 : (0, import_protocols2.parseQueuedExecutionMode)(row.execution_mode)
5306
5570
  };
5307
5571
  }
5308
5572
  };
5573
+ function validateQueueTrust(params) {
5574
+ return {
5575
+ trustedRunContext: params.trusted_run_context === void 0 ? void 0 : (0, import_protocols2.parseTrustedRunContext)(params.trusted_run_context),
5576
+ executionMode: params.execution_mode === void 0 ? void 0 : (0, import_protocols2.parseQueuedExecutionMode)(params.execution_mode)
5577
+ };
5578
+ }
5579
+ function scopeClause(scope, start) {
5580
+ if (!scope) return { sql: "", params: [] };
5581
+ const params = [];
5582
+ const entries = ["tenantId", "assistantId", "workspaceId", "projectId"].map((key) => [key, scope[key]]);
5583
+ const sql = entries.map(([key, value]) => {
5584
+ const column = key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
5585
+ if (value == null) return ` AND ${column} IS NULL`;
5586
+ params.push(value);
5587
+ return ` AND ${column} = $${start + params.length - 1}`;
5588
+ }).join("");
5589
+ return { sql, params };
5590
+ }
5309
5591
 
5310
5592
  // src/stores/ChannelBindingStore.ts
5311
5593
  var import_pg14 = require("pg");
5594
+ var import_protocols3 = require("@axiom-lattice/protocols");
5312
5595
 
5313
5596
  // src/migrations/channel_bindings_migration.ts
5314
5597
  var createChannelBindingsTable = {
@@ -5356,6 +5639,7 @@ var createChannelBindingsTable = {
5356
5639
  };
5357
5640
 
5358
5641
  // src/stores/ChannelBindingStore.ts
5642
+ var BINDING_SUBJECT_CONSTRAINT = "lattice_channel_bindings_channel_channel_installation_id_te_key";
5359
5643
  var ChannelBindingStore = class {
5360
5644
  constructor(options) {
5361
5645
  this.initialized = false;
@@ -5412,80 +5696,91 @@ var ChannelBindingStore = class {
5412
5696
  if (result.rows.length === 0) return null;
5413
5697
  return this.mapRowToBinding(result.rows[0]);
5414
5698
  }
5415
- async create(input) {
5699
+ async findById(tenantId, id) {
5416
5700
  await this.ensureInitialized();
5417
5701
  const result = await this.pool.query(
5418
- `INSERT INTO lattice_channel_bindings
5419
- (channel, channel_installation_id, tenant_id, sender_id, agent_id,
5420
- thread_mode, sender_display_name, sender_metadata, workspace_id, project_id)
5421
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
5422
- RETURNING *`,
5423
- [
5424
- input.channel,
5425
- input.channelInstallationId,
5426
- input.tenantId,
5427
- input.senderId,
5428
- input.agentId,
5429
- input.threadMode || "fixed",
5430
- input.senderDisplayName || null,
5431
- input.senderMetadata ? JSON.stringify(input.senderMetadata) : null,
5432
- input.workspaceId || null,
5433
- input.projectId || null
5434
- ]
5702
+ "SELECT * FROM lattice_channel_bindings WHERE id = $1 AND tenant_id = $2",
5703
+ [id, tenantId]
5435
5704
  );
5436
- return this.mapRowToBinding(result.rows[0]);
5705
+ return result.rows[0] ? this.mapRowToBinding(result.rows[0]) : null;
5437
5706
  }
5438
- async update(id, patch) {
5707
+ async findBySubject(params) {
5439
5708
  await this.ensureInitialized();
5440
- const existing = await this.pool.query(
5441
- `SELECT * FROM lattice_channel_bindings WHERE id = $1`,
5442
- [id]
5709
+ const result = await this.pool.query(
5710
+ `SELECT * FROM lattice_channel_bindings
5711
+ WHERE tenant_id = $1 AND channel = $2 AND channel_installation_id = $3 AND sender_id = $4 LIMIT 1`,
5712
+ [params.tenantId, params.channel, params.channelInstallationId, params.senderId]
5443
5713
  );
5444
- if (existing.rows.length === 0) {
5445
- throw new Error(`Binding ${id} not found`);
5714
+ return result.rows[0] ? this.mapRowToBinding(result.rows[0]) : null;
5715
+ }
5716
+ async create(input) {
5717
+ await this.ensureInitialized();
5718
+ try {
5719
+ const result = await this.pool.query(
5720
+ `INSERT INTO lattice_channel_bindings
5721
+ (channel, channel_installation_id, tenant_id, sender_id, agent_id,
5722
+ thread_id, thread_mode, sender_display_name, sender_metadata, workspace_id, project_id, enabled)
5723
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
5724
+ RETURNING *`,
5725
+ [
5726
+ input.channel,
5727
+ input.channelInstallationId,
5728
+ input.tenantId,
5729
+ input.senderId,
5730
+ input.agentId,
5731
+ input.threadId || null,
5732
+ input.threadMode || "fixed",
5733
+ input.senderDisplayName || null,
5734
+ input.senderMetadata ? JSON.stringify(input.senderMetadata) : null,
5735
+ input.workspaceId || null,
5736
+ input.projectId || null,
5737
+ input.enabled ?? true
5738
+ ]
5739
+ );
5740
+ return this.mapRowToBinding(result.rows[0]);
5741
+ } catch (error) {
5742
+ if (isBindingSubjectConflict(error)) {
5743
+ throw new import_protocols3.DuplicateChannelBindingSubjectError();
5744
+ }
5745
+ throw error;
5446
5746
  }
5447
- const row = existing.rows[0];
5448
- const updated = {
5449
- channel: patch.channel ?? row.channel,
5450
- channel_installation_id: patch.channelInstallationId ?? row.channel_installation_id,
5451
- sender_id: patch.senderId ?? row.sender_id,
5452
- agent_id: patch.agentId ?? row.agent_id,
5453
- thread_id: patch.threadId !== void 0 ? patch.threadId : row.thread_id,
5454
- workspace_id: patch.workspaceId !== void 0 ? patch.workspaceId : row.workspace_id,
5455
- project_id: patch.projectId !== void 0 ? patch.projectId : row.project_id,
5456
- thread_mode: patch.threadMode ?? row.thread_mode,
5457
- sender_display_name: patch.senderDisplayName !== void 0 ? patch.senderDisplayName : row.sender_display_name,
5458
- sender_metadata: patch.senderMetadata !== void 0 ? patch.senderMetadata : row.sender_metadata,
5459
- enabled: patch.enabled ?? row.enabled
5460
- };
5747
+ }
5748
+ async update(tenantId, id, patch) {
5749
+ await this.ensureInitialized();
5461
5750
  const result = await this.pool.query(
5462
5751
  `UPDATE lattice_channel_bindings SET
5463
- channel = $1, channel_installation_id = $2, sender_id = $3,
5464
- agent_id = $4, thread_id = $5, workspace_id = $6, project_id = $7,
5465
- thread_mode = $8, sender_display_name = $9, sender_metadata = $10,
5466
- enabled = $11, updated_at = NOW()
5467
- WHERE id = $12
5752
+ agent_id = COALESCE($1, agent_id),
5753
+ thread_id = COALESCE($2, thread_id),
5754
+ workspace_id = COALESCE($3, workspace_id),
5755
+ project_id = COALESCE($4, project_id),
5756
+ thread_mode = COALESCE($5, thread_mode),
5757
+ sender_display_name = COALESCE($6, sender_display_name),
5758
+ sender_metadata = COALESCE($7, sender_metadata),
5759
+ enabled = COALESCE($8, enabled), updated_at = NOW()
5760
+ WHERE id = $9 AND tenant_id = $10
5468
5761
  RETURNING *`,
5469
5762
  [
5470
- updated.channel,
5471
- updated.channel_installation_id,
5472
- updated.sender_id,
5473
- updated.agent_id,
5474
- updated.thread_id,
5475
- updated.workspace_id,
5476
- updated.project_id,
5477
- updated.thread_mode,
5478
- updated.sender_display_name,
5479
- updated.sender_metadata ? JSON.stringify(updated.sender_metadata) : null,
5480
- updated.enabled,
5481
- id
5763
+ patch.agentId ?? null,
5764
+ patch.threadId ?? null,
5765
+ patch.workspaceId ?? null,
5766
+ patch.projectId ?? null,
5767
+ patch.threadMode ?? null,
5768
+ patch.senderDisplayName ?? null,
5769
+ patch.senderMetadata ? JSON.stringify(patch.senderMetadata) : null,
5770
+ patch.enabled ?? null,
5771
+ id,
5772
+ tenantId
5482
5773
  ]
5483
5774
  );
5775
+ if (!result.rows[0]) throw new Error(`Binding ${id} not found`);
5484
5776
  return this.mapRowToBinding(result.rows[0]);
5485
5777
  }
5486
- async delete(id) {
5778
+ async delete(tenantId, id) {
5487
5779
  await this.ensureInitialized();
5488
- await this.pool.query(`DELETE FROM lattice_channel_bindings WHERE id = $1`, [id]);
5780
+ await this.pool.query(
5781
+ `DELETE FROM lattice_channel_bindings WHERE id = $1 AND tenant_id = $2`,
5782
+ [id, tenantId]
5783
+ );
5489
5784
  }
5490
5785
  async list(params) {
5491
5786
  await this.ensureInitialized();
@@ -5504,6 +5799,14 @@ var ChannelBindingStore = class {
5504
5799
  conditions.push(`channel_installation_id = $${idx++}`);
5505
5800
  values.push(params.channelInstallationId);
5506
5801
  }
5802
+ if (params.excludeChannels?.length) {
5803
+ conditions.push(`channel <> ALL($${idx++}::text[])`);
5804
+ values.push(params.excludeChannels);
5805
+ }
5806
+ for (const prefix of params.excludeInstallationIdPrefixes ?? []) {
5807
+ conditions.push(`channel_installation_id NOT LIKE $${idx++} ESCAPE '\\'`);
5808
+ values.push(`${escapeLikePattern(prefix)}%`);
5809
+ }
5507
5810
  const limit = params.limit ?? 50;
5508
5811
  const offset = params.offset ?? 0;
5509
5812
  values.push(limit, offset);
@@ -5516,7 +5819,16 @@ var ChannelBindingStore = class {
5516
5819
  );
5517
5820
  return result.rows.map((r) => this.mapRowToBinding(r));
5518
5821
  }
5519
- async import(bindings) {
5822
+ async import(tenantId, bindings) {
5823
+ if (bindings.some((binding) => binding.channel === "room")) {
5824
+ throw new Error("Room bindings cannot be imported through the public store API");
5825
+ }
5826
+ if (bindings.some((binding) => binding.channelInstallationId.startsWith("room-internal:"))) {
5827
+ throw new Error("Internal bindings cannot be imported through the public store API");
5828
+ }
5829
+ if (bindings.some((binding) => binding.tenantId !== tenantId)) {
5830
+ throw new Error("Binding import tenant mismatch");
5831
+ }
5520
5832
  const result = [];
5521
5833
  for (const input of bindings) {
5522
5834
  result.push(await this.create(input));
@@ -5524,7 +5836,13 @@ var ChannelBindingStore = class {
5524
5836
  return result;
5525
5837
  }
5526
5838
  async export(params) {
5527
- return this.list({ tenantId: params.tenantId, limit: 1e4, offset: 0 });
5839
+ return this.list({
5840
+ tenantId: params.tenantId,
5841
+ excludeChannels: ["room"],
5842
+ excludeInstallationIdPrefixes: ["room-internal:"],
5843
+ limit: 1e4,
5844
+ offset: 0
5845
+ });
5528
5846
  }
5529
5847
  async ensureInitialized() {
5530
5848
  if (!this.initialized) {
@@ -5551,6 +5869,14 @@ var ChannelBindingStore = class {
5551
5869
  };
5552
5870
  }
5553
5871
  };
5872
+ function isBindingSubjectConflict(error) {
5873
+ if (typeof error !== "object" || error === null) return false;
5874
+ const pgError = error;
5875
+ return pgError.code === "23505" && pgError.constraint === BINDING_SUBJECT_CONSTRAINT;
5876
+ }
5877
+ function escapeLikePattern(value) {
5878
+ return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
5879
+ }
5554
5880
 
5555
5881
  // src/stores/PostgreSQLChannelInstallationStore.ts
5556
5882
  var import_pg15 = require("pg");
@@ -5725,7 +6051,8 @@ var PostgreSQLChannelInstallationStore = class {
5725
6051
  async createInstallation(tenantId, installationId, data) {
5726
6052
  await this.ensureInitialized();
5727
6053
  const now = /* @__PURE__ */ new Date();
5728
- const encryptedConfig = this.encryptSecrets(data.config);
6054
+ const config = { ...data.config };
6055
+ const encryptedConfig = this.encryptSecrets(config);
5729
6056
  await this.pool.query(
5730
6057
  `
5731
6058
  INSERT INTO lattice_channel_installations (
@@ -5750,7 +6077,7 @@ var PostgreSQLChannelInstallationStore = class {
5750
6077
  tenantId,
5751
6078
  channel: data.channel,
5752
6079
  name: data.name,
5753
- config: data.config,
6080
+ config,
5754
6081
  enabled: data.enabled ?? true,
5755
6082
  fallbackAgentId: data.fallbackAgentId,
5756
6083
  rejectWhenNoBinding: data.rejectWhenNoBinding ?? true,
@@ -5837,17 +6164,17 @@ var PostgreSQLChannelInstallationStore = class {
5837
6164
  encryptSecrets(config) {
5838
6165
  return {
5839
6166
  ...config,
5840
- appSecret: typeof config.appSecret === "string" ? (0, import_core4.encrypt)(config.appSecret) : config.appSecret,
5841
- verificationToken: typeof config.verificationToken === "string" ? (0, import_core4.encrypt)(config.verificationToken) : config.verificationToken,
5842
- encryptKey: typeof config.encryptKey === "string" ? (0, import_core4.encrypt)(config.encryptKey) : config.encryptKey
6167
+ ...typeof config.appSecret === "string" ? { appSecret: (0, import_core4.encrypt)(config.appSecret) } : {},
6168
+ ...typeof config.verificationToken === "string" ? { verificationToken: (0, import_core4.encrypt)(config.verificationToken) } : {},
6169
+ ...typeof config.encryptKey === "string" ? { encryptKey: (0, import_core4.encrypt)(config.encryptKey) } : {}
5843
6170
  };
5844
6171
  }
5845
6172
  decryptSecrets(config) {
5846
6173
  return {
5847
6174
  ...config,
5848
- appSecret: typeof config.appSecret === "string" ? (0, import_core4.decrypt)(config.appSecret) : config.appSecret,
5849
- verificationToken: typeof config.verificationToken === "string" ? (0, import_core4.decrypt)(config.verificationToken) : config.verificationToken,
5850
- encryptKey: typeof config.encryptKey === "string" ? (0, import_core4.decrypt)(config.encryptKey) : config.encryptKey
6175
+ ...typeof config.appSecret === "string" ? { appSecret: (0, import_core4.decrypt)(config.appSecret) } : {},
6176
+ ...typeof config.verificationToken === "string" ? { verificationToken: (0, import_core4.decrypt)(config.verificationToken) } : {},
6177
+ ...typeof config.encryptKey === "string" ? { encryptKey: (0, import_core4.decrypt)(config.encryptKey) } : {}
5851
6178
  };
5852
6179
  }
5853
6180
  };
@@ -6056,17 +6383,17 @@ var PostgreSQLA2AApiKeyStore = class {
6056
6383
  const result = await this.pool.query(
6057
6384
  `SELECT * FROM lattice_a2a_api_keys WHERE enabled = true`
6058
6385
  );
6059
- const map = /* @__PURE__ */ new Map();
6386
+ const map2 = /* @__PURE__ */ new Map();
6060
6387
  for (const row of result.rows) {
6061
6388
  const key = (0, import_core5.decrypt)(row.key_value);
6062
- map.set(key, {
6389
+ map2.set(key, {
6063
6390
  key,
6064
6391
  tenantId: row.tenant_id,
6065
6392
  projectId: row.project_id,
6066
6393
  assistantIds: row.assistant_ids ?? void 0
6067
6394
  });
6068
6395
  }
6069
- return map;
6396
+ return map2;
6070
6397
  }
6071
6398
  };
6072
6399
 
@@ -6296,7 +6623,7 @@ var PostgreSQLAgentWebAppStore = class {
6296
6623
 
6297
6624
  // src/stores/PostgreSQLScheduleStorage.ts
6298
6625
  var import_pg18 = require("pg");
6299
- var import_protocols = require("@axiom-lattice/protocols");
6626
+ var import_protocols4 = require("@axiom-lattice/protocols");
6300
6627
 
6301
6628
  // src/migrations/schedule_migrations.ts
6302
6629
  var createScheduledTasksTable = {
@@ -6710,7 +7037,7 @@ var PostgreSQLScheduleStorage = class {
6710
7037
  await this.ensureInitialized();
6711
7038
  const result = await this.pool.query(
6712
7039
  `SELECT * FROM lattice_scheduled_tasks WHERE status IN ($1, $2) ORDER BY created_at ASC`,
6713
- [import_protocols.ScheduledTaskStatus.PENDING, import_protocols.ScheduledTaskStatus.PAUSED]
7040
+ [import_protocols4.ScheduledTaskStatus.PENDING, import_protocols4.ScheduledTaskStatus.PAUSED]
6714
7041
  );
6715
7042
  return result.rows.map((row) => this.mapRowToTask(row));
6716
7043
  }
@@ -6869,9 +7196,9 @@ var PostgreSQLScheduleStorage = class {
6869
7196
  AND updated_at < $4
6870
7197
  `,
6871
7198
  [
6872
- import_protocols.ScheduledTaskStatus.COMPLETED,
6873
- import_protocols.ScheduledTaskStatus.CANCELLED,
6874
- import_protocols.ScheduledTaskStatus.FAILED,
7199
+ import_protocols4.ScheduledTaskStatus.COMPLETED,
7200
+ import_protocols4.ScheduledTaskStatus.CANCELLED,
7201
+ import_protocols4.ScheduledTaskStatus.FAILED,
6875
7202
  cutoff
6876
7203
  ]
6877
7204
  );
@@ -7000,15 +7327,37 @@ var addFilesToTasks = {
7000
7327
  `);
7001
7328
  }
7002
7329
  };
7330
+ var addTaskDependenciesGinIndex = {
7331
+ version: 174,
7332
+ name: "add_task_dependencies_gin_index",
7333
+ up: async (client) => {
7334
+ await client.query(`CREATE INDEX IF NOT EXISTS idx_lattice_tasks_dependencies_gin
7335
+ ON lattice_tasks USING GIN (dependencies jsonb_path_ops)
7336
+ WHERE dependencies IS NOT NULL`);
7337
+ },
7338
+ down: async (client) => {
7339
+ await client.query("DROP INDEX IF EXISTS idx_lattice_tasks_dependencies_gin");
7340
+ }
7341
+ };
7003
7342
  var taskMigrations = [
7004
7343
  createTasksTable,
7005
7344
  addTaskFieldsMigration,
7006
7345
  addTaskProjectFieldsMigration,
7007
- addFilesToTasks
7346
+ addFilesToTasks,
7347
+ addTaskDependenciesGinIndex
7008
7348
  ];
7009
7349
 
7010
7350
  // src/stores/PostgreSQLTaskStore.ts
7011
7351
  var import_uuid3 = require("uuid");
7352
+ var TASK_STATUSES = /* @__PURE__ */ new Set([
7353
+ "pending",
7354
+ "in_progress",
7355
+ "review",
7356
+ "failed",
7357
+ "interrupted",
7358
+ "completed",
7359
+ "cancelled"
7360
+ ]);
7012
7361
  function nextUpdatedAtSql(column = "updated_at") {
7013
7362
  return `to_char(
7014
7363
  date_trunc('milliseconds', GREATEST(
@@ -7088,6 +7437,9 @@ function mapRowToTask(row) {
7088
7437
  updatedAt: new Date(row.updated_at)
7089
7438
  };
7090
7439
  }
7440
+ function assertPage(limit, offset = 0) {
7441
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !Number.isSafeInteger(offset) || offset < 0) throw new RangeError("Invalid task page");
7442
+ }
7091
7443
  var PostgreSQLTaskStore = class {
7092
7444
  constructor(options) {
7093
7445
  this.initialized = false;
@@ -7111,6 +7463,7 @@ var PostgreSQLTaskStore = class {
7111
7463
  this.migrationManager.register(addTaskFieldsMigration);
7112
7464
  this.migrationManager.register(addTaskProjectFieldsMigration);
7113
7465
  this.migrationManager.register(addFilesToTasks);
7466
+ this.migrationManager.register(addTaskDependenciesGinIndex);
7114
7467
  if (options.autoMigrate !== false) {
7115
7468
  this.initialize().catch((error) => {
7116
7469
  console.error("Failed to initialize PostgreSQLTaskStore:", error);
@@ -7206,7 +7559,9 @@ var PostgreSQLTaskStore = class {
7206
7559
  conditions.push(`workspace_id = $${paramIndex++}`);
7207
7560
  params.push(filter.workspaceId);
7208
7561
  }
7209
- if (filter.projectId) {
7562
+ if (filter.projectId === null) {
7563
+ conditions.push("(project_id IS NULL OR project_id = '' OR project_id = 'default')");
7564
+ } else if (filter.projectId !== void 0) {
7210
7565
  conditions.push(`project_id = $${paramIndex++}`);
7211
7566
  params.push(filter.projectId);
7212
7567
  }
@@ -7231,11 +7586,35 @@ var PostgreSQLTaskStore = class {
7231
7586
  const limit = filter.limit || 100;
7232
7587
  const offset = filter.offset || 0;
7233
7588
  const result = await this.pool.query(
7234
- `SELECT * FROM lattice_tasks WHERE ${where} ORDER BY created_at DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
7589
+ `SELECT * FROM lattice_tasks WHERE ${where} ORDER BY created_at DESC, id DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
7235
7590
  [...params, limit, offset]
7236
7591
  );
7237
7592
  return result.rows.map((r) => mapRowToTask(r));
7238
7593
  }
7594
+ /** Lists exact project tasks containing a JSON string dependency. */
7595
+ async listDependents(query) {
7596
+ assertPage(query.limit, query.offset);
7597
+ if (query.statuses.length === 0 || query.statuses.some((status) => !TASK_STATUSES.has(status))) {
7598
+ throw new RangeError("Invalid task statuses");
7599
+ }
7600
+ await this.ensureInitialized();
7601
+ const result = await this.pool.query(
7602
+ `SELECT * FROM lattice_tasks
7603
+ WHERE tenant_id=$1 AND workspace_id=$2 AND project_id=$3
7604
+ AND dependencies @> $4::jsonb AND status = ANY($5::text[])
7605
+ ORDER BY created_at DESC, id DESC LIMIT $6 OFFSET $7`,
7606
+ [
7607
+ query.tenantId,
7608
+ query.workspaceId,
7609
+ query.projectId,
7610
+ JSON.stringify([query.dependencyTaskId]),
7611
+ query.statuses,
7612
+ query.limit,
7613
+ query.offset
7614
+ ]
7615
+ );
7616
+ return result.rows.map(mapRowToTask);
7617
+ }
7239
7618
  async update(tenantId, id, updates) {
7240
7619
  await this.ensureInitialized();
7241
7620
  const existing = await this.getById(tenantId, id);
@@ -7606,6 +7985,63 @@ var PostgreSQLTaskStore = class {
7606
7985
  );
7607
7986
  return result.rows[0] ? mapRowToTask(result.rows[0]) : null;
7608
7987
  }
7988
+ /** Atomically update only while status, timestamp, owner, and Project scope match. */
7989
+ async updateIfSnapshot(tenantId, id, updates, snapshot) {
7990
+ await this.ensureInitialized();
7991
+ const setClauses = [];
7992
+ const params = [];
7993
+ let index = 1;
7994
+ const fields = [
7995
+ ["title", "title"],
7996
+ ["description", "description"],
7997
+ ["status", "status"],
7998
+ ["priority", "priority"],
7999
+ ["dueDate", "due_date"],
8000
+ ["metadata", "metadata", true],
8001
+ ["files", "files", true],
8002
+ ["parentId", "parent_id"],
8003
+ ["sourceId", "source_id"],
8004
+ ["context", "context", true],
8005
+ ["ownerType", "owner_type"],
8006
+ ["ownerId", "owner_id"],
8007
+ ["requireReview", "require_review"],
8008
+ ["dependencies", "dependencies", true],
8009
+ ["result", "result"],
8010
+ ["failureReason", "failure_reason"],
8011
+ ["workspaceId", "workspace_id"],
8012
+ ["projectId", "project_id"]
8013
+ ];
8014
+ for (const [field, column, json] of fields) {
8015
+ const value = updates[field];
8016
+ if (value === void 0) continue;
8017
+ setClauses.push(`${column} = $${index++}`);
8018
+ params.push(json && value !== null ? JSON.stringify(value) : value);
8019
+ }
8020
+ if (setClauses.length === 0) return this.getById(tenantId, id);
8021
+ setClauses.push(`updated_at = ${nextUpdatedAtSql()}`);
8022
+ const predicates = [
8023
+ tenantId,
8024
+ id,
8025
+ snapshot.status,
8026
+ new Date(snapshot.updatedAt).toISOString(),
8027
+ snapshot.ownerType,
8028
+ snapshot.ownerId,
8029
+ snapshot.workspaceId,
8030
+ snapshot.projectId
8031
+ ];
8032
+ const placeholders = predicates.map(() => `$${index++}`);
8033
+ params.push(...predicates);
8034
+ const result = await this.pool.query(
8035
+ `UPDATE lattice_tasks SET ${setClauses.join(", ")}
8036
+ WHERE tenant_id = ${placeholders[0]} AND id = ${placeholders[1]} AND status = ${placeholders[2]}
8037
+ AND ${canonicalTimestampSnapshotSql("updated_at", placeholders[3])}
8038
+ AND owner_type = ${placeholders[4]} AND owner_id = ${placeholders[5]}
8039
+ AND workspace_id IS NOT DISTINCT FROM ${placeholders[6]}
8040
+ AND project_id IS NOT DISTINCT FROM ${placeholders[7]} RETURNING *`,
8041
+ params
8042
+ );
8043
+ return result.rows[0] ? mapRowToTask(result.rows[0]) : null;
8044
+ }
7609
8045
  /** Atomically update a child only when both child and parent snapshots match. */
7610
8046
  async updateIfStatusUpdatedAtAndParentUpdatedAt(tenantId, id, updates, expectedStatuses, expectedUpdatedAt, parentId, expectedParentUpdatedAt) {
7611
8047
  await this.ensureInitialized();
@@ -7724,6 +8160,27 @@ var PostgreSQLTaskStore = class {
7724
8160
  );
7725
8161
  return (result.rowCount ?? 0) > 0;
7726
8162
  }
8163
+ /** Atomically delete only while status, timestamp, owner, and Project scope match. */
8164
+ async deleteIfSnapshot(tenantId, id, snapshot) {
8165
+ await this.ensureInitialized();
8166
+ const result = await this.pool.query(
8167
+ `DELETE FROM lattice_tasks WHERE tenant_id = $1 AND id = $2 AND status = $3
8168
+ AND ${canonicalTimestampSnapshotSql("updated_at", "$4")}
8169
+ AND owner_type = $5 AND owner_id = $6
8170
+ AND workspace_id IS NOT DISTINCT FROM $7 AND project_id IS NOT DISTINCT FROM $8`,
8171
+ [
8172
+ tenantId,
8173
+ id,
8174
+ snapshot.status,
8175
+ new Date(snapshot.updatedAt).toISOString(),
8176
+ snapshot.ownerType,
8177
+ snapshot.ownerId,
8178
+ snapshot.workspaceId,
8179
+ snapshot.projectId
8180
+ ]
8181
+ );
8182
+ return (result.rowCount ?? 0) > 0;
8183
+ }
7727
8184
  async dispose() {
7728
8185
  if (this.ownsPool && this.pool) {
7729
8186
  await this.pool.end();
@@ -7737,17 +8194,175 @@ var PostgreSQLTaskStore = class {
7737
8194
  };
7738
8195
 
7739
8196
  // src/stores/PostgreSQLTaskWorkItemStore.ts
8197
+ var import_pg20 = require("pg");
8198
+ var import_protocols5 = require("@axiom-lattice/protocols");
7740
8199
  var import_uuid4 = require("uuid");
7741
- var PostgreSQLTaskWorkItemStore = class {
7742
- constructor(pool) {
7743
- this.pool = pool;
7744
- }
7745
- async create(params) {
8200
+
8201
+ // src/migrations/task_work_items_migration.ts
8202
+ var createTaskWorkItemsMigration = {
8203
+ version: 138,
8204
+ name: "create_task_work_items_table",
8205
+ up: async (client) => {
8206
+ await client.query(`
8207
+ CREATE TABLE IF NOT EXISTS lattice_task_work_items (
8208
+ id TEXT NOT NULL,
8209
+ tenant_id TEXT NOT NULL,
8210
+ task_id TEXT NOT NULL,
8211
+ action TEXT NOT NULL,
8212
+ actor TEXT NOT NULL,
8213
+ thread_id TEXT,
8214
+ summary TEXT,
8215
+ detail JSONB,
8216
+ attempt INTEGER,
8217
+ created_at TIMESTAMPTZ NOT NULL DEFAULT date_trunc('milliseconds', clock_timestamp()),
8218
+ PRIMARY KEY (tenant_id, id)
8219
+ )
8220
+ `);
8221
+ await client.query(`
8222
+ CREATE INDEX IF NOT EXISTS idx_task_work_items_task_id
8223
+ ON lattice_task_work_items (tenant_id, task_id)
8224
+ `);
8225
+ await client.query(`
8226
+ CREATE INDEX IF NOT EXISTS idx_task_work_items_action
8227
+ ON lattice_task_work_items (tenant_id, task_id, action)
8228
+ `);
8229
+ }
8230
+ };
8231
+ var addWorkItemProjectFieldsMigration = {
8232
+ version: 140,
8233
+ name: "add_task_work_item_project_fields",
8234
+ up: async (client) => {
8235
+ await client.query(`
8236
+ ALTER TABLE lattice_task_work_items
8237
+ ADD COLUMN IF NOT EXISTS workspace_id TEXT,
8238
+ ADD COLUMN IF NOT EXISTS project_id TEXT
8239
+ `);
8240
+ await client.query(`
8241
+ CREATE INDEX IF NOT EXISTS idx_task_work_items_project
8242
+ ON lattice_task_work_items (tenant_id, project_id)
8243
+ `);
8244
+ }
8245
+ };
8246
+ var addTaskWorkItemEventKeyMigration = {
8247
+ version: 168,
8248
+ name: "add_task_work_item_event_key",
8249
+ up: async (client) => {
8250
+ await client.query(`
8251
+ ALTER TABLE lattice_task_work_items
8252
+ ADD COLUMN IF NOT EXISTS event_key TEXT
8253
+ `);
8254
+ await client.query(`
8255
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_task_work_items_event_key
8256
+ ON lattice_task_work_items (tenant_id, task_id, event_key) WHERE event_key IS NOT NULL
8257
+ `);
8258
+ }
8259
+ };
8260
+ var addTaskWorkItemPendingIndexesMigration = {
8261
+ version: 171,
8262
+ name: "add_task_work_item_pending_indexes",
8263
+ up: async (client) => {
8264
+ await client.query(`
8265
+ CREATE INDEX IF NOT EXISTS idx_task_work_items_pending_order
8266
+ ON lattice_task_work_items (tenant_id, task_id, action, created_at DESC, id DESC)
8267
+ `);
8268
+ await client.query(`
8269
+ CREATE INDEX IF NOT EXISTS idx_task_work_items_reconciled_result
8270
+ ON lattice_task_work_items (tenant_id, task_id, (detail ->> 'executionResultId'))
8271
+ WHERE action = 'execution_reconciled'
8272
+ `);
8273
+ }
8274
+ };
8275
+ var addProjectLifecycleEventIndex = {
8276
+ version: 175,
8277
+ name: "add_project_lifecycle_event_index",
8278
+ up: async (client) => {
8279
+ await client.query(`UPDATE lattice_task_work_items
8280
+ SET created_at = date_trunc('milliseconds', created_at)
8281
+ WHERE created_at <> date_trunc('milliseconds', created_at)`);
8282
+ await client.query(`ALTER TABLE lattice_task_work_items
8283
+ ALTER COLUMN created_at SET DEFAULT date_trunc('milliseconds', clock_timestamp())`);
8284
+ await client.query(`CREATE INDEX IF NOT EXISTS idx_task_work_items_project_lifecycle
8285
+ ON lattice_task_work_items (tenant_id, workspace_id, project_id, created_at DESC, id DESC)
8286
+ WHERE event_key IS NOT NULL`);
8287
+ },
8288
+ down: async (client) => {
8289
+ await client.query("DROP INDEX IF EXISTS idx_task_work_items_project_lifecycle");
8290
+ }
8291
+ };
8292
+ var taskWorkItemMigrations = [
8293
+ createTaskWorkItemsMigration,
8294
+ addWorkItemProjectFieldsMigration,
8295
+ addTaskWorkItemEventKeyMigration,
8296
+ addTaskWorkItemPendingIndexesMigration,
8297
+ addProjectLifecycleEventIndex
8298
+ ];
8299
+
8300
+ // src/stores/PostgreSQLTaskWorkItemStore.ts
8301
+ var PROJECT_LIFECYCLE_ACTIONS = /* @__PURE__ */ new Set([
8302
+ "in_progress",
8303
+ "interrupted",
8304
+ "failed",
8305
+ "completed",
8306
+ "cancelled",
8307
+ "reassigned"
8308
+ ]);
8309
+ function isRecord2(value) {
8310
+ return value !== null && typeof value === "object" && !Array.isArray(value);
8311
+ }
8312
+ function isPool(value) {
8313
+ return typeof value.query === "function";
8314
+ }
8315
+ var PostgreSQLTaskWorkItemStore = class {
8316
+ constructor(poolOrOptions) {
8317
+ this.initialized = false;
8318
+ this.ownsPool = false;
8319
+ this.initPromise = null;
8320
+ if (isPool(poolOrOptions)) {
8321
+ this.pool = poolOrOptions;
8322
+ this.initialized = true;
8323
+ return;
8324
+ }
8325
+ const options = poolOrOptions;
8326
+ if (options.pool) {
8327
+ this.pool = options.pool;
8328
+ this.initialized = true;
8329
+ return;
8330
+ }
8331
+ this.pool = typeof options.poolConfig === "string" ? new import_pg20.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg20.Pool(options.poolConfig) : (() => {
8332
+ throw new Error("Either pool or poolConfig must be provided");
8333
+ })();
8334
+ this.ownsPool = true;
8335
+ this.migrationManager = new MigrationManager(this.pool);
8336
+ for (const migration of taskWorkItemMigrations) this.migrationManager.register(migration);
8337
+ if (options.autoMigrate !== false) this.startInitialization();
8338
+ }
8339
+ /** Applies the complete standalone TaskWorkItem migration chain once. */
8340
+ async initialize() {
8341
+ if (this.initialized) return;
8342
+ if (this.initPromise) return this.initPromise;
8343
+ return this.startInitialization();
8344
+ }
8345
+ /** Closes the pool only when this store created it from connection configuration. */
8346
+ async dispose() {
8347
+ if (this.ownsPool) await this.pool.end();
8348
+ }
8349
+ startInitialization() {
8350
+ this.initPromise = this.migrationManager.migrate().then(() => {
8351
+ this.initialized = true;
8352
+ });
8353
+ void this.initPromise.catch(() => void 0);
8354
+ return this.initPromise;
8355
+ }
8356
+ async ensureInitialized() {
8357
+ if (!this.initialized) await this.initialize();
8358
+ }
8359
+ async create(params) {
8360
+ await this.ensureInitialized();
7746
8361
  const id = (0, import_uuid4.v4)();
7747
8362
  const result = await this.pool.query(
7748
8363
  `INSERT INTO lattice_task_work_items
7749
- (id, tenant_id, task_id, action, actor, thread_id, summary, detail, attempt, workspace_id, project_id)
7750
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
8364
+ (id, tenant_id, task_id, action, actor, thread_id, summary, detail, attempt, workspace_id, project_id, created_at)
8365
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, date_trunc('milliseconds', clock_timestamp()))
7751
8366
  RETURNING *`,
7752
8367
  [
7753
8368
  id,
@@ -7765,8 +8380,46 @@ var PostgreSQLTaskWorkItemStore = class {
7765
8380
  );
7766
8381
  return this.rowToItem(result.rows[0]);
7767
8382
  }
8383
+ /** Atomically inserts a work item by selecting one exact task snapshot. */
8384
+ async createIfTaskSnapshot(params, snapshot) {
8385
+ await this.ensureInitialized();
8386
+ const result = await this.pool.query(
8387
+ `INSERT INTO lattice_task_work_items
8388
+ (id, tenant_id, task_id, action, actor, thread_id, summary, detail, attempt, workspace_id, project_id, created_at)
8389
+ SELECT $1, task.tenant_id, task.id, $2, $3, $4, $5, $6, $7, task.workspace_id, task.project_id,
8390
+ date_trunc('milliseconds', clock_timestamp())
8391
+ FROM lattice_tasks AS task
8392
+ WHERE task.tenant_id = $8 AND task.id = $9 AND task.status = $10
8393
+ AND date_trunc('milliseconds', task.updated_at::timestamptz) = $11::timestamptz
8394
+ AND task.owner_type = $12 AND task.owner_id = $13
8395
+ AND task.workspace_id IS NOT DISTINCT FROM $14 AND task.project_id IS NOT DISTINCT FROM $15
8396
+ AND $14 IS NOT DISTINCT FROM $16 AND $15 IS NOT DISTINCT FROM $17
8397
+ RETURNING *`,
8398
+ [
8399
+ (0, import_uuid4.v4)(),
8400
+ params.action,
8401
+ params.actor,
8402
+ params.threadId || null,
8403
+ params.summary || null,
8404
+ params.detail ? JSON.stringify(params.detail) : null,
8405
+ params.attempt ?? null,
8406
+ params.tenantId,
8407
+ params.taskId,
8408
+ snapshot.status,
8409
+ new Date(snapshot.updatedAt).toISOString(),
8410
+ snapshot.ownerType,
8411
+ snapshot.ownerId,
8412
+ snapshot.workspaceId,
8413
+ snapshot.projectId,
8414
+ params.workspaceId,
8415
+ params.projectId
8416
+ ]
8417
+ );
8418
+ return result.rows[0] ? this.rowToItem(result.rows[0]) : null;
8419
+ }
7768
8420
  /** Find an event by its tenant- and task-scoped key without pagination. */
7769
8421
  async findByEventKey(tenantId, taskId, eventKey) {
8422
+ await this.ensureInitialized();
7770
8423
  const result = await this.pool.query(
7771
8424
  `SELECT * FROM lattice_task_work_items
7772
8425
  WHERE tenant_id = $1 AND task_id = $2 AND event_key = $3`,
@@ -7776,10 +8429,11 @@ var PostgreSQLTaskWorkItemStore = class {
7776
8429
  }
7777
8430
  /** Atomically return an existing event or create it once. */
7778
8431
  async createIfAbsentByEventKey(params) {
8432
+ await this.ensureInitialized();
7779
8433
  const result = await this.pool.query(
7780
8434
  `INSERT INTO lattice_task_work_items
7781
- (id, tenant_id, task_id, action, actor, thread_id, summary, detail, attempt, workspace_id, project_id, event_key)
7782
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
8435
+ (id, tenant_id, task_id, action, actor, thread_id, summary, detail, attempt, workspace_id, project_id, event_key, created_at)
8436
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, date_trunc('milliseconds', clock_timestamp()))
7783
8437
  ON CONFLICT (tenant_id, task_id, event_key) WHERE event_key IS NOT NULL
7784
8438
  DO UPDATE SET event_key = EXCLUDED.event_key
7785
8439
  RETURNING *`,
@@ -7801,6 +8455,7 @@ var PostgreSQLTaskWorkItemStore = class {
7801
8455
  return this.rowToItem(result.rows[0]);
7802
8456
  }
7803
8457
  async list(filter) {
8458
+ await this.ensureInitialized();
7804
8459
  let query = `SELECT * FROM lattice_task_work_items WHERE tenant_id = $1 AND task_id = $2`;
7805
8460
  const params = [filter.tenantId, filter.taskId];
7806
8461
  if (filter.action) {
@@ -7828,6 +8483,84 @@ var PostgreSQLTaskWorkItemStore = class {
7828
8483
  const result = await this.pool.query(query, params);
7829
8484
  return result.rows.map((row) => this.rowToItem(row));
7830
8485
  }
8486
+ /** List pending execution results using one bounded PostgreSQL anti-join query. */
8487
+ async listPendingExecutionResults(params) {
8488
+ if (!Number.isSafeInteger(params.limit) || params.limit < 0 || params.limit > import_protocols5.MAX_PENDING_EXECUTION_RESULTS_LIMIT) {
8489
+ const error = new RangeError(`limit must be a safe integer between 0 and ${import_protocols5.MAX_PENDING_EXECUTION_RESULTS_LIMIT}`);
8490
+ error.code = "INVALID_LIMIT";
8491
+ throw error;
8492
+ }
8493
+ if (params.limit === 0) return [];
8494
+ const result = await this.pool.query(
8495
+ `SELECT result.*
8496
+ FROM lattice_task_work_items AS result
8497
+ WHERE result.tenant_id = $1
8498
+ AND result.task_id = $2
8499
+ AND result.action = 'execution_result'
8500
+ AND result.event_key COLLATE "C" ~ '^execution-result:[A-Za-z0-9._:-]+$'
8501
+ AND NOT EXISTS (
8502
+ SELECT 1
8503
+ FROM lattice_task_work_items AS reconciled
8504
+ WHERE reconciled.tenant_id = result.tenant_id
8505
+ AND reconciled.task_id = result.task_id
8506
+ AND reconciled.action = 'execution_reconciled'
8507
+ AND reconciled.detail ->> 'executionResultId' = result.event_key
8508
+ )
8509
+ ORDER BY result.created_at DESC, result.id DESC
8510
+ LIMIT $3`,
8511
+ [params.tenantId, params.taskId, params.limit]
8512
+ );
8513
+ return result.rows.map((row) => this.rowToItem(row));
8514
+ }
8515
+ /** Lists canonical project lifecycle events with an exclusive cursor. */
8516
+ async listProjectLifecycleEvents(query) {
8517
+ let cursorMilliseconds = null;
8518
+ if (!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > 100 || query.actions.length === 0 || query.actions.some((action) => !PROJECT_LIFECYCLE_ACTIONS.has(action))) {
8519
+ throw new RangeError("Invalid project lifecycle event page");
8520
+ }
8521
+ if (query.before) {
8522
+ try {
8523
+ cursorMilliseconds = Date.prototype.getTime.call(query.before.createdAt);
8524
+ } catch {
8525
+ throw new RangeError("Invalid project lifecycle event cursor");
8526
+ }
8527
+ if (!Number.isFinite(cursorMilliseconds) || typeof query.before.id !== "string" || query.before.id.length === 0) {
8528
+ throw new RangeError("Invalid project lifecycle event cursor");
8529
+ }
8530
+ }
8531
+ const cursor = cursorMilliseconds === null ? null : new Date(cursorMilliseconds);
8532
+ await this.ensureInitialized();
8533
+ const result = await this.pool.query(
8534
+ `SELECT * FROM lattice_task_work_items
8535
+ WHERE tenant_id=$1 AND workspace_id=$2 AND project_id=$3
8536
+ AND action = ANY($4::text[]) AND event_key IS NOT NULL AND event_key <> ''
8537
+ AND ($5::timestamptz IS NULL OR created_at < $5::timestamptz
8538
+ OR (created_at = $5::timestamptz AND id < $6))
8539
+ ORDER BY created_at DESC, id DESC LIMIT $7`,
8540
+ [
8541
+ query.tenantId,
8542
+ query.workspaceId,
8543
+ query.projectId,
8544
+ query.actions,
8545
+ cursor,
8546
+ query.before?.id ?? null,
8547
+ query.limit
8548
+ ]
8549
+ );
8550
+ return result.rows.map((row) => this.assertProjectLifecycleEvent(this.rowToItem(row)));
8551
+ }
8552
+ assertProjectLifecycleEvent(item) {
8553
+ let milliseconds;
8554
+ try {
8555
+ milliseconds = Date.prototype.getTime.call(item.createdAt);
8556
+ } catch {
8557
+ throw new Error("Invalid project lifecycle event row");
8558
+ }
8559
+ if (!Number.isFinite(milliseconds) || typeof item.id !== "string" || item.id.length === 0 || typeof item.eventKey !== "string" || item.eventKey.length === 0) {
8560
+ throw new Error("Invalid project lifecycle event row");
8561
+ }
8562
+ return { ...item, createdAt: new Date(milliseconds) };
8563
+ }
7831
8564
  rowToItem(row) {
7832
8565
  return {
7833
8566
  id: row.id,
@@ -7837,7 +8570,7 @@ var PostgreSQLTaskWorkItemStore = class {
7837
8570
  actor: row.actor,
7838
8571
  threadId: row.thread_id,
7839
8572
  summary: row.summary,
7840
- detail: row.detail,
8573
+ detail: isRecord2(row.detail) ? row.detail : void 0,
7841
8574
  attempt: row.attempt,
7842
8575
  workspaceId: row.workspace_id,
7843
8576
  projectId: row.project_id,
@@ -7848,7 +8581,7 @@ var PostgreSQLTaskWorkItemStore = class {
7848
8581
  };
7849
8582
 
7850
8583
  // src/stores/MenuStore.ts
7851
- var import_pg20 = require("pg");
8584
+ var import_pg21 = require("pg");
7852
8585
 
7853
8586
  // src/migrations/menu_items_migration.ts
7854
8587
  var createMenuItemsTable = {
@@ -7925,7 +8658,7 @@ var MenuStore = class {
7925
8658
  this.initialized = true;
7926
8659
  return;
7927
8660
  }
7928
- this.pool = typeof options.poolConfig === "string" ? new import_pg20.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg20.Pool(options.poolConfig) : (() => {
8661
+ this.pool = typeof options.poolConfig === "string" ? new import_pg21.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg21.Pool(options.poolConfig) : (() => {
7929
8662
  throw new Error("Either pool or poolConfig must be provided");
7930
8663
  })();
7931
8664
  this.migrationManager = new MigrationManager(this.pool);
@@ -8080,7 +8813,7 @@ var MenuStore = class {
8080
8813
  };
8081
8814
 
8082
8815
  // src/stores/PostgresSharedResourceStore.ts
8083
- var import_pg21 = require("pg");
8816
+ var import_pg22 = require("pg");
8084
8817
 
8085
8818
  // src/migrations/shared_resources_migration.ts
8086
8819
  var createSharedResourcesTable = {
@@ -8134,7 +8867,7 @@ var PostgresSharedResourceStore = class {
8134
8867
  this.initialized = true;
8135
8868
  return;
8136
8869
  }
8137
- this.pool = typeof options.poolConfig === "string" ? new import_pg21.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg21.Pool(options.poolConfig) : (() => {
8870
+ this.pool = typeof options.poolConfig === "string" ? new import_pg22.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg22.Pool(options.poolConfig) : (() => {
8138
8871
  throw new Error("Either pool or poolConfig must be provided");
8139
8872
  })();
8140
8873
  this.migrationManager = new MigrationManager(this.pool);
@@ -8303,7 +9036,7 @@ var PostgresSharedResourceStore = class {
8303
9036
  };
8304
9037
 
8305
9038
  // src/stores/PostgreSQLCollectionStore.ts
8306
- var import_pg22 = require("pg");
9039
+ var import_pg23 = require("pg");
8307
9040
  var PostgreSQLCollectionStore = class {
8308
9041
  constructor(options) {
8309
9042
  this.initialized = false;
@@ -8316,9 +9049,9 @@ var PostgreSQLCollectionStore = class {
8316
9049
  return;
8317
9050
  }
8318
9051
  if (typeof options.poolConfig === "string") {
8319
- this.pool = new import_pg22.Pool({ connectionString: options.poolConfig });
9052
+ this.pool = new import_pg23.Pool({ connectionString: options.poolConfig });
8320
9053
  } else if (options.poolConfig) {
8321
- this.pool = new import_pg22.Pool(options.poolConfig);
9054
+ this.pool = new import_pg23.Pool(options.poolConfig);
8322
9055
  } else {
8323
9056
  throw new Error("Either pool or poolConfig must be provided");
8324
9057
  }
@@ -8433,7 +9166,7 @@ var PostgreSQLCollectionStore = class {
8433
9166
  var import_langgraph_checkpoint_postgres = require("@langchain/langgraph-checkpoint-postgres");
8434
9167
 
8435
9168
  // src/PGVectorStoreProvider.ts
8436
- var import_pg23 = require("pg");
9169
+ var import_pg24 = require("pg");
8437
9170
  var import_pgvector = require("@langchain/community/vectorstores/pgvector");
8438
9171
  var import_documents = require("@langchain/core/documents");
8439
9172
  var import_core6 = require("@axiom-lattice/core");
@@ -8507,7 +9240,7 @@ var PGVectorStoreProvider = class {
8507
9240
  }
8508
9241
  };
8509
9242
  function createPGVectorStoreProvider(connectionString) {
8510
- const pool = new import_pg23.Pool({ connectionString });
9243
+ const pool = new import_pg24.Pool({ connectionString });
8511
9244
  return {
8512
9245
  provider: new PGVectorStoreProvider(pool, connectionString),
8513
9246
  pool
@@ -8796,69 +9529,956 @@ var createCollectionsTable = {
8796
9529
  }
8797
9530
  };
8798
9531
 
8799
- // src/migrations/task_work_items_migration.ts
8800
- var createTaskWorkItemsMigration = {
8801
- version: 138,
8802
- name: "create_task_work_items_table",
9532
+ // src/migrations/capability_bundle_migration.ts
9533
+ var createCapabilityBundlesTable = {
9534
+ version: 170,
9535
+ name: "create_capability_bundles_table",
9536
+ up: async (client) => {
9537
+ await client.query(`CREATE TABLE IF NOT EXISTS lattice_capability_bundles (
9538
+ id UUID PRIMARY KEY, tenant_id VARCHAR(255) NOT NULL, bundle_key VARCHAR(255) NOT NULL,
9539
+ name VARCHAR(255) NOT NULL, description TEXT, capabilities JSONB NOT NULL DEFAULT '[]'::jsonb,
9540
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
9541
+ UNIQUE (tenant_id, bundle_key)
9542
+ )`);
9543
+ await client.query("CREATE INDEX IF NOT EXISTS idx_lattice_capability_bundles_tenant ON lattice_capability_bundles(tenant_id)");
9544
+ },
9545
+ down: async (client) => {
9546
+ await client.query("DROP INDEX IF EXISTS idx_lattice_capability_bundles_tenant");
9547
+ await client.query("DROP TABLE IF EXISTS lattice_capability_bundles");
9548
+ }
9549
+ };
9550
+
9551
+ // src/migrations/project_room_migration.ts
9552
+ var createProjectRoomTables = {
9553
+ version: 172,
9554
+ name: "create_project_room_tables",
8803
9555
  up: async (client) => {
8804
9556
  await client.query(`
8805
- CREATE TABLE IF NOT EXISTS lattice_task_work_items (
8806
- id TEXT NOT NULL,
8807
- tenant_id TEXT NOT NULL,
8808
- task_id TEXT NOT NULL,
8809
- action TEXT NOT NULL,
8810
- actor TEXT NOT NULL,
8811
- thread_id TEXT,
8812
- summary TEXT,
8813
- detail JSONB,
8814
- attempt INTEGER,
9557
+ CREATE TABLE IF NOT EXISTS lattice_project_rooms (
9558
+ id VARCHAR(255) NOT NULL,
9559
+ tenant_id VARCHAR(255) NOT NULL,
9560
+ workspace_id VARCHAR(255) NOT NULL,
9561
+ project_id VARCHAR(255) NOT NULL,
9562
+ name VARCHAR(255) NOT NULL,
9563
+ type VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_rooms_type
9564
+ CHECK (type IN ('main')),
8815
9565
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
8816
- PRIMARY KEY (tenant_id, id)
9566
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
9567
+ PRIMARY KEY (tenant_id, id),
9568
+ UNIQUE (tenant_id, project_id, type)
8817
9569
  )
8818
9570
  `);
8819
9571
  await client.query(`
8820
- CREATE INDEX IF NOT EXISTS idx_task_work_items_task_id
8821
- ON lattice_task_work_items (tenant_id, task_id)
9572
+ CREATE TABLE IF NOT EXISTS lattice_project_memberships (
9573
+ id VARCHAR(255) NOT NULL,
9574
+ tenant_id VARCHAR(255) NOT NULL,
9575
+ project_id VARCHAR(255) NOT NULL,
9576
+ user_id VARCHAR(255) NOT NULL,
9577
+ role VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_memberships_role
9578
+ CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
9579
+ status VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_memberships_status
9580
+ CHECK (status IN ('active', 'removed')),
9581
+ joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
9582
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
9583
+ PRIMARY KEY (tenant_id, id),
9584
+ UNIQUE (tenant_id, project_id, user_id)
9585
+ )
8822
9586
  `);
8823
9587
  await client.query(`
8824
- CREATE INDEX IF NOT EXISTS idx_task_work_items_action
8825
- ON lattice_task_work_items (tenant_id, task_id, action)
9588
+ CREATE INDEX IF NOT EXISTS idx_lattice_project_memberships_project
9589
+ ON lattice_project_memberships (tenant_id, project_id)
8826
9590
  `);
8827
- }
8828
- };
8829
- var addWorkItemProjectFieldsMigration = {
8830
- version: 140,
8831
- name: "add_task_work_item_project_fields",
8832
- up: async (client) => {
8833
9591
  await client.query(`
8834
- ALTER TABLE lattice_task_work_items
8835
- ADD COLUMN IF NOT EXISTS workspace_id TEXT,
8836
- ADD COLUMN IF NOT EXISTS project_id TEXT
9592
+ CREATE TABLE IF NOT EXISTS lattice_project_bot_memberships (
9593
+ id VARCHAR(255) NOT NULL,
9594
+ tenant_id VARCHAR(255) NOT NULL,
9595
+ workspace_id VARCHAR(255) NOT NULL,
9596
+ project_id VARCHAR(255) NOT NULL,
9597
+ room_id VARCHAR(255) NOT NULL,
9598
+ assistant_id VARCHAR(255) NOT NULL,
9599
+ role VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_bot_memberships_role
9600
+ CHECK (role IN ('coordinator', 'specialist')),
9601
+ title VARCHAR(255) NOT NULL,
9602
+ responsibility TEXT,
9603
+ mention_name VARCHAR(255) NOT NULL,
9604
+ status VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_bot_memberships_status
9605
+ CHECK (status IN ('active', 'paused', 'removed')),
9606
+ room_thread_id VARCHAR(255) NOT NULL,
9607
+ joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
9608
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
9609
+ PRIMARY KEY (tenant_id, id),
9610
+ UNIQUE (tenant_id, project_id, assistant_id)
9611
+ )
8837
9612
  `);
8838
9613
  await client.query(`
8839
- CREATE INDEX IF NOT EXISTS idx_task_work_items_project
8840
- ON lattice_task_work_items (tenant_id, project_id)
9614
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_project_bot_memberships_coordinator
9615
+ ON lattice_project_bot_memberships (tenant_id, project_id)
9616
+ WHERE role = 'coordinator' AND status IN ('active', 'paused')
8841
9617
  `);
8842
- }
8843
- };
8844
- var addTaskWorkItemEventKeyMigration = {
8845
- version: 168,
8846
- name: "add_task_work_item_event_key",
8847
- up: async (client) => {
8848
9618
  await client.query(`
8849
- ALTER TABLE lattice_task_work_items
8850
- ADD COLUMN IF NOT EXISTS event_key TEXT
9619
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_project_bot_memberships_mention
9620
+ ON lattice_project_bot_memberships (tenant_id, room_id, mention_name)
9621
+ WHERE status IN ('active', 'paused')
8851
9622
  `);
8852
9623
  await client.query(`
8853
- CREATE UNIQUE INDEX IF NOT EXISTS idx_task_work_items_event_key
8854
- ON lattice_task_work_items (tenant_id, task_id, event_key) WHERE event_key IS NOT NULL
9624
+ CREATE INDEX IF NOT EXISTS idx_lattice_project_bot_memberships_project
9625
+ ON lattice_project_bot_memberships (tenant_id, project_id)
9626
+ `);
9627
+ await client.query(`
9628
+ CREATE INDEX IF NOT EXISTS idx_lattice_project_bot_memberships_room
9629
+ ON lattice_project_bot_memberships (tenant_id, room_id)
9630
+ `);
9631
+ await client.query(`
9632
+ CREATE TABLE IF NOT EXISTS lattice_project_room_messages (
9633
+ id VARCHAR(255) NOT NULL,
9634
+ tenant_id VARCHAR(255) NOT NULL,
9635
+ workspace_id VARCHAR(255) NOT NULL,
9636
+ project_id VARCHAR(255) NOT NULL,
9637
+ room_id VARCHAR(255) NOT NULL,
9638
+ author JSONB NOT NULL,
9639
+ content JSONB NOT NULL,
9640
+ mentions JSONB NOT NULL DEFAULT '[]'::jsonb,
9641
+ reply_to_message_id VARCHAR(255),
9642
+ source VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_room_messages_source
9643
+ CHECK (source IN ('user', 'agent', 'task', 'routine', 'system')),
9644
+ source_id VARCHAR(255),
9645
+ idempotency_key VARCHAR(255),
9646
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
9647
+ PRIMARY KEY (tenant_id, id)
9648
+ )
9649
+ `);
9650
+ await client.query(`
9651
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_project_room_messages_idempotency
9652
+ ON lattice_project_room_messages (tenant_id, room_id, idempotency_key)
9653
+ WHERE idempotency_key IS NOT NULL
9654
+ `);
9655
+ await client.query(`
9656
+ CREATE INDEX IF NOT EXISTS idx_lattice_project_room_messages_room_created
9657
+ ON lattice_project_room_messages (tenant_id, room_id, created_at DESC, id DESC)
8855
9658
  `);
9659
+ },
9660
+ down: async (client) => {
9661
+ await client.query("DROP INDEX IF EXISTS idx_lattice_project_room_messages_room_created");
9662
+ await client.query("DROP INDEX IF EXISTS uq_lattice_project_room_messages_idempotency");
9663
+ await client.query("DROP TABLE IF EXISTS lattice_project_room_messages");
9664
+ await client.query("DROP INDEX IF EXISTS idx_lattice_project_bot_memberships_room");
9665
+ await client.query("DROP INDEX IF EXISTS idx_lattice_project_bot_memberships_project");
9666
+ await client.query("DROP INDEX IF EXISTS uq_lattice_project_bot_memberships_mention");
9667
+ await client.query("DROP INDEX IF EXISTS uq_lattice_project_bot_memberships_coordinator");
9668
+ await client.query("DROP TABLE IF EXISTS lattice_project_bot_memberships");
9669
+ await client.query("DROP INDEX IF EXISTS idx_lattice_project_memberships_project");
9670
+ await client.query("DROP TABLE IF EXISTS lattice_project_memberships");
9671
+ await client.query("DROP TABLE IF EXISTS lattice_project_rooms");
8856
9672
  }
8857
9673
  };
8858
9674
 
8859
- // src/createPgStoreConfig.ts
8860
- async function createPgStoreConfig(connectionString) {
8861
- const pool = new import_pg24.Pool({ connectionString });
9675
+ // src/stores/PostgreSQLCapabilityBundleStore.ts
9676
+ var import_crypto4 = require("crypto");
9677
+ var import_pg25 = require("pg");
9678
+ var duplicateMessage = "Capability bundle key already exists for tenant";
9679
+ function map(row) {
9680
+ return {
9681
+ id: row.id,
9682
+ tenantId: row.tenant_id,
9683
+ key: row.bundle_key,
9684
+ name: row.name,
9685
+ description: row.description ?? void 0,
9686
+ capabilities: row.capabilities,
9687
+ createdAt: row.created_at.toISOString(),
9688
+ updatedAt: typeof row.updated_at === "string" ? row.updated_at : row.updated_at.toISOString()
9689
+ };
9690
+ }
9691
+ function isDuplicate(error) {
9692
+ return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
9693
+ }
9694
+ var PostgreSQLCapabilityBundleStore = class {
9695
+ /**
9696
+ * Creates a PostgreSQL capability bundle store.
9697
+ *
9698
+ * @param options - Pool ownership, connection, and migration options.
9699
+ */
9700
+ constructor(options) {
9701
+ this.initialized = false;
9702
+ this.ownsPool = true;
9703
+ this.initPromise = null;
9704
+ if (options.pool) {
9705
+ this.pool = options.pool;
9706
+ this.ownsPool = false;
9707
+ this.initialized = true;
9708
+ return;
9709
+ }
9710
+ this.pool = typeof options.poolConfig === "string" ? new import_pg25.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg25.Pool(options.poolConfig) : (() => {
9711
+ throw new Error("Either pool or poolConfig must be provided");
9712
+ })();
9713
+ this.migrationManager = new MigrationManager(this.pool);
9714
+ this.migrationManager.register(createCapabilityBundlesTable);
9715
+ if (options.autoMigrate !== false) {
9716
+ this.startInitialization();
9717
+ }
9718
+ }
9719
+ /**
9720
+ * Applies pending migrations for an internally managed pool.
9721
+ *
9722
+ * @returns A shared promise that resolves when initialization completes.
9723
+ */
9724
+ async initialize() {
9725
+ if (this.initialized) return;
9726
+ if (this.initPromise) return this.initPromise;
9727
+ return this.startInitialization();
9728
+ }
9729
+ startInitialization() {
9730
+ this.initPromise = this.migrationManager.migrate().then(() => {
9731
+ this.initialized = true;
9732
+ });
9733
+ void this.initPromise.catch(() => void 0);
9734
+ return this.initPromise;
9735
+ }
9736
+ /**
9737
+ * Closes the pool when it was created by this store.
9738
+ *
9739
+ * @returns A promise that resolves after owned resources are released.
9740
+ */
9741
+ async dispose() {
9742
+ if (this.ownsPool) await this.pool.end();
9743
+ }
9744
+ async ready() {
9745
+ if (!this.initialized) await this.initialize();
9746
+ }
9747
+ async listByTenant(tenantId) {
9748
+ await this.ready();
9749
+ const result = await this.pool.query(
9750
+ "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",
9751
+ [tenantId]
9752
+ );
9753
+ return result.rows.map(map);
9754
+ }
9755
+ async getById(tenantId, id) {
9756
+ await this.ready();
9757
+ const result = await this.pool.query(
9758
+ "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",
9759
+ [tenantId, id]
9760
+ );
9761
+ return result.rows[0] ? map(result.rows[0]) : null;
9762
+ }
9763
+ async getManyByIds(tenantId, ids) {
9764
+ await this.ready();
9765
+ if (ids.length === 0) return [];
9766
+ const result = await this.pool.query(
9767
+ "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[])",
9768
+ [tenantId, ids]
9769
+ );
9770
+ return result.rows.map(map);
9771
+ }
9772
+ async create(tenantId, input) {
9773
+ await this.ready();
9774
+ try {
9775
+ const result = await this.pool.query(
9776
+ `INSERT INTO lattice_capability_bundles
9777
+ (id, tenant_id, bundle_key, name, description, capabilities)
9778
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb)
9779
+ RETURNING id, tenant_id, bundle_key, name, description, capabilities, created_at, updated_at::text AS updated_at`,
9780
+ [
9781
+ (0, import_crypto4.randomUUID)(),
9782
+ tenantId,
9783
+ input.key,
9784
+ input.name,
9785
+ input.description ?? null,
9786
+ JSON.stringify(input.capabilities)
9787
+ ]
9788
+ );
9789
+ return map(result.rows[0]);
9790
+ } catch (error) {
9791
+ if (isDuplicate(error)) throw new Error(duplicateMessage);
9792
+ throw error;
9793
+ }
9794
+ }
9795
+ async update(tenantId, id, input) {
9796
+ await this.ready();
9797
+ const fields = [];
9798
+ const values = [];
9799
+ const add = (field, value) => {
9800
+ values.push(value);
9801
+ fields.push(`${field} = $${values.length}`);
9802
+ };
9803
+ if (input.name !== void 0) add("name", input.name);
9804
+ if (Object.prototype.hasOwnProperty.call(input, "description")) {
9805
+ add("description", input.description ?? null);
9806
+ }
9807
+ if (input.capabilities !== void 0) {
9808
+ add("capabilities", JSON.stringify(input.capabilities));
9809
+ }
9810
+ if (fields.length === 0) {
9811
+ const expectedUpdatedAt2 = input.expectedUpdatedAt;
9812
+ const result = await this.pool.query(
9813
+ `SELECT id, tenant_id, bundle_key, name, description, capabilities, created_at, updated_at::text AS updated_at
9814
+ FROM lattice_capability_bundles
9815
+ WHERE tenant_id = $1 AND id = $2${expectedUpdatedAt2 === void 0 ? "" : " AND updated_at::text = $3"}`,
9816
+ expectedUpdatedAt2 === void 0 ? [tenantId, id] : [tenantId, id, expectedUpdatedAt2]
9817
+ );
9818
+ if (result.rows[0]) return map(result.rows[0]);
9819
+ return expectedUpdatedAt2 === void 0 ? null : { status: "conflict" };
9820
+ }
9821
+ const expectedUpdatedAt = input.expectedUpdatedAt;
9822
+ values.push(tenantId, id);
9823
+ if (expectedUpdatedAt !== void 0) values.push(expectedUpdatedAt);
9824
+ const tenantParam = values.length - (expectedUpdatedAt === void 0 ? 1 : 2);
9825
+ const idParam = tenantParam + 1;
9826
+ const revisionPredicate = expectedUpdatedAt === void 0 ? "" : ` AND updated_at::text = $${values.length}`;
9827
+ try {
9828
+ const result = await this.pool.query(
9829
+ `UPDATE lattice_capability_bundles
9830
+ SET ${fields.join(", ")}, updated_at = GREATEST(updated_at + interval '1 microsecond', clock_timestamp())
9831
+ WHERE tenant_id = $${tenantParam} AND id = $${idParam}${revisionPredicate}
9832
+ RETURNING id, tenant_id, bundle_key, name, description, capabilities, created_at, updated_at::text AS updated_at`,
9833
+ values
9834
+ );
9835
+ if (result.rows[0]) return map(result.rows[0]);
9836
+ return expectedUpdatedAt === void 0 ? null : { status: "conflict" };
9837
+ } catch (error) {
9838
+ if (isDuplicate(error)) throw new Error(duplicateMessage);
9839
+ throw error;
9840
+ }
9841
+ }
9842
+ async deleteIfUnreferenced(tenantId, id) {
9843
+ await this.ready();
9844
+ const client = await this.pool.connect();
9845
+ try {
9846
+ await client.query("BEGIN");
9847
+ await client.query(
9848
+ "SELECT pg_advisory_xact_lock(hashtextextended($1 || ':project-mutations', 0))",
9849
+ [tenantId]
9850
+ );
9851
+ const projectIds = await client.query(
9852
+ `SELECT id FROM lattice_projects
9853
+ WHERE tenant_id = $1
9854
+ ORDER BY id`,
9855
+ [tenantId]
9856
+ );
9857
+ if (projectIds.rows.length > 0) {
9858
+ await client.query(
9859
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || ':project:' || project_id, 0))
9860
+ FROM unnest($2::text[]) AS project_id
9861
+ ORDER BY project_id`,
9862
+ [tenantId, projectIds.rows.map((project) => project.id)]
9863
+ );
9864
+ }
9865
+ await client.query(
9866
+ `SELECT id FROM lattice_projects
9867
+ WHERE tenant_id = $1
9868
+ FOR UPDATE`,
9869
+ [tenantId]
9870
+ );
9871
+ await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1 || ':' || $2, 0))", [tenantId, id]);
9872
+ const referenced = await client.query(
9873
+ `SELECT 1 FROM lattice_projects
9874
+ WHERE tenant_id = $1
9875
+ AND jsonb_typeof(config->'capabilityBundleIds') = 'array'
9876
+ AND COALESCE(config->'capabilityBundleIds', '[]'::jsonb) ? $2
9877
+ LIMIT 1`,
9878
+ [tenantId, id]
9879
+ );
9880
+ if (referenced.rows.length > 0) {
9881
+ await client.query("COMMIT");
9882
+ return "in_use";
9883
+ }
9884
+ const deleted = await client.query(
9885
+ "DELETE FROM lattice_capability_bundles WHERE tenant_id = $1 AND id = $2 RETURNING id",
9886
+ [tenantId, id]
9887
+ );
9888
+ await client.query("COMMIT");
9889
+ return deleted.rows.length > 0 ? "deleted" : "not_found";
9890
+ } catch (error) {
9891
+ await client.query("ROLLBACK");
9892
+ throw error;
9893
+ } finally {
9894
+ client.release();
9895
+ }
9896
+ }
9897
+ };
9898
+
9899
+ // src/stores/PostgreSQLProjectRoomStore.ts
9900
+ function isValidDate(value) {
9901
+ return value instanceof Date && !Number.isNaN(value.getTime());
9902
+ }
9903
+ function mapRow2(row) {
9904
+ if (typeof row.id !== "string" || typeof row.tenant_id !== "string" || typeof row.workspace_id !== "string" || typeof row.project_id !== "string" || row.type !== "main" || typeof row.name !== "string" || !isValidDate(row.created_at) || !isValidDate(row.updated_at)) {
9905
+ throw new Error("Invalid project room row");
9906
+ }
9907
+ return {
9908
+ id: row.id,
9909
+ tenantId: row.tenant_id,
9910
+ workspaceId: row.workspace_id,
9911
+ projectId: row.project_id,
9912
+ type: "main",
9913
+ name: row.name,
9914
+ createdAt: row.created_at,
9915
+ updatedAt: row.updated_at
9916
+ };
9917
+ }
9918
+ var PostgreSQLProjectRoomStore = class {
9919
+ /** Creates a store using an externally managed pool; the pool is not migrated or closed. */
9920
+ constructor(options) {
9921
+ this.pool = options.pool;
9922
+ }
9923
+ /** Creates or returns the single persisted main room for a project. */
9924
+ async ensureMainRoom(input) {
9925
+ const result = await this.pool.query(
9926
+ `INSERT INTO lattice_project_rooms
9927
+ (id, tenant_id, workspace_id, project_id, type, name)
9928
+ VALUES ($1, $2, $3, $4, $5, $6)
9929
+ ON CONFLICT (tenant_id, project_id, type)
9930
+ DO UPDATE SET updated_at = lattice_project_rooms.updated_at
9931
+ RETURNING id, tenant_id, workspace_id, project_id, type, name, created_at, updated_at`,
9932
+ [input.id, input.tenantId, input.workspaceId, input.projectId, "main", input.name]
9933
+ );
9934
+ return mapRow2(result.rows[0]);
9935
+ }
9936
+ /** Finds a tenant-scoped project's main room, or returns null. */
9937
+ async getMainRoom(tenantId, projectId) {
9938
+ const result = await this.pool.query(
9939
+ "SELECT id, tenant_id, workspace_id, project_id, type, name, created_at, updated_at FROM lattice_project_rooms WHERE tenant_id = $1 AND project_id = $2 AND type = 'main'",
9940
+ [tenantId, projectId]
9941
+ );
9942
+ return result.rows[0] ? mapRow2(result.rows[0]) : null;
9943
+ }
9944
+ };
9945
+
9946
+ // src/stores/PostgreSQLProjectMembershipStore.ts
9947
+ var DuplicateProjectMembershipError = class extends Error {
9948
+ constructor() {
9949
+ super("Project membership already exists for tenant, project, and user");
9950
+ this.name = "DuplicateProjectMembershipError";
9951
+ }
9952
+ };
9953
+ var ProjectMembershipIdConflictError = class extends Error {
9954
+ constructor() {
9955
+ super("Project membership ID already exists for tenant");
9956
+ this.name = "ProjectMembershipIdConflictError";
9957
+ }
9958
+ };
9959
+ function isDate(value) {
9960
+ return value instanceof Date && !Number.isNaN(value.getTime());
9961
+ }
9962
+ function isRole(value) {
9963
+ return value === "owner" || value === "admin" || value === "member" || value === "viewer";
9964
+ }
9965
+ function isStatus2(value) {
9966
+ return value === "active" || value === "removed";
9967
+ }
9968
+ function mapRow3(row) {
9969
+ if (typeof row.id !== "string" || typeof row.tenant_id !== "string" || typeof row.project_id !== "string" || typeof row.user_id !== "string" || !isRole(row.role) || !isStatus2(row.status) || !isDate(row.joined_at) || !isDate(row.updated_at)) throw new Error("Invalid project membership row");
9970
+ return {
9971
+ id: row.id,
9972
+ tenantId: row.tenant_id,
9973
+ projectId: row.project_id,
9974
+ userId: row.user_id,
9975
+ role: row.role,
9976
+ status: row.status,
9977
+ joinedAt: row.joined_at,
9978
+ updatedAt: row.updated_at
9979
+ };
9980
+ }
9981
+ function isDuplicate2(error) {
9982
+ return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
9983
+ }
9984
+ function mapDuplicate(error) {
9985
+ if (isDuplicate2(error) && typeof error.constraint === "string" && error.constraint.includes("project_id_user_id")) throw new DuplicateProjectMembershipError();
9986
+ if (isDuplicate2(error)) throw new ProjectMembershipIdConflictError();
9987
+ throw error;
9988
+ }
9989
+ var columns = "id, tenant_id, project_id, user_id, role, status, joined_at, updated_at";
9990
+ var PostgreSQLProjectMembershipStore = class {
9991
+ /** Creates a store using an externally managed pool; the pool is not migrated or closed. */
9992
+ constructor(options) {
9993
+ this.pool = options.pool;
9994
+ }
9995
+ /** Lists memberships in stable joined-time and ID order. */
9996
+ async list(tenantId, projectId) {
9997
+ const result = await this.pool.query(`SELECT ${columns} FROM lattice_project_memberships WHERE tenant_id = $1 AND project_id = $2 ORDER BY joined_at ASC, id ASC`, [tenantId, projectId]);
9998
+ return result.rows.map(mapRow3);
9999
+ }
10000
+ /** Finds a membership by exact tenant, project, and user identity. */
10001
+ async findByUser(tenantId, projectId, userId) {
10002
+ const result = await this.pool.query(`SELECT ${columns} FROM lattice_project_memberships WHERE tenant_id = $1 AND project_id = $2 AND user_id = $3`, [tenantId, projectId, userId]);
10003
+ return result.rows[0] ? mapRow3(result.rows[0]) : null;
10004
+ }
10005
+ /** Inserts a membership and maps database uniqueness errors to stable typed errors. */
10006
+ async create(input) {
10007
+ const client = await this.pool.connect();
10008
+ try {
10009
+ await client.query("BEGIN");
10010
+ await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${input.tenantId}:${input.projectId}`]);
10011
+ const result = await client.query(`INSERT INTO lattice_project_memberships (id, tenant_id, project_id, user_id, role, status, joined_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, date_trunc('milliseconds', clock_timestamp()), date_trunc('milliseconds', clock_timestamp())) RETURNING ${columns}`, [input.id, input.tenantId, input.projectId, input.userId, input.role, input.status]);
10012
+ const membership = mapRow3(result.rows[0]);
10013
+ await client.query("COMMIT");
10014
+ return membership;
10015
+ } catch (error) {
10016
+ try {
10017
+ await client.query("ROLLBACK");
10018
+ } catch {
10019
+ }
10020
+ return mapDuplicate(error);
10021
+ } finally {
10022
+ client.release();
10023
+ }
10024
+ }
10025
+ /** Atomically creates the first active owner while serializing the project scope. */
10026
+ async createInitialOwner(input) {
10027
+ const client = await this.pool.connect();
10028
+ try {
10029
+ await client.query("BEGIN");
10030
+ await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${input.tenantId}:${input.projectId}`]);
10031
+ const existingResult = await client.query(`SELECT ${columns} FROM lattice_project_memberships WHERE tenant_id = $1 AND project_id = $2 FOR UPDATE`, [input.tenantId, input.projectId]);
10032
+ if (existingResult.rows.length > 0) {
10033
+ const existing = existingResult.rows.map(mapRow3).find((item) => item.userId === input.userId && item.role === "owner" && item.status === "active");
10034
+ await client.query("COMMIT");
10035
+ return existing ? { kind: "existing", membership: existing } : { kind: "already_initialized" };
10036
+ }
10037
+ const idResult = await client.query("SELECT id FROM lattice_project_memberships WHERE tenant_id = $1 AND id = $2", [input.tenantId, input.id]);
10038
+ if (idResult.rows.length > 0) throw new ProjectMembershipIdConflictError();
10039
+ const inserted = await client.query(`INSERT INTO lattice_project_memberships (id, tenant_id, project_id, user_id, role, status, joined_at, updated_at) VALUES ($1, $2, $3, $4, 'owner', 'active', date_trunc('milliseconds', clock_timestamp()), date_trunc('milliseconds', clock_timestamp())) RETURNING ${columns}`, [input.id, input.tenantId, input.projectId, input.userId]);
10040
+ const membership = mapRow3(inserted.rows[0]);
10041
+ await client.query("COMMIT");
10042
+ return { kind: "created", membership };
10043
+ } catch (error) {
10044
+ try {
10045
+ await client.query("ROLLBACK");
10046
+ } catch {
10047
+ }
10048
+ if (isDuplicate2(error)) return mapDuplicate(error);
10049
+ throw error;
10050
+ } finally {
10051
+ client.release();
10052
+ }
10053
+ }
10054
+ /** Updates a role under a project lock with exact timestamp and owner safeguards. */
10055
+ async updateRole(input) {
10056
+ return this.mutate(input.tenantId, input.id, input.expectedUpdatedAt, input.role, false);
10057
+ }
10058
+ /** Marks a membership removed under a project lock with exact timestamp and owner safeguards. */
10059
+ async remove(input) {
10060
+ return this.mutate(input.tenantId, input.id, input.expectedUpdatedAt, void 0, true);
10061
+ }
10062
+ async mutate(tenantId, id, expected, role, remove) {
10063
+ const client = await this.pool.connect();
10064
+ try {
10065
+ await client.query("BEGIN");
10066
+ const identity = await client.query("SELECT project_id FROM lattice_project_memberships WHERE tenant_id = $1 AND id = $2", [tenantId, id]);
10067
+ if (!identity.rows[0] || typeof identity.rows[0].project_id !== "string") {
10068
+ await client.query("COMMIT");
10069
+ return { kind: "not_found" };
10070
+ }
10071
+ await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${tenantId}:${identity.rows[0].project_id}`]);
10072
+ const activeRows = await client.query(`SELECT ${columns} FROM lattice_project_memberships WHERE tenant_id = $1 AND project_id = $2 AND status = 'active' FOR UPDATE`, [tenantId, identity.rows[0].project_id]);
10073
+ const lockedMemberships = activeRows.rows.map(mapRow3);
10074
+ let current = lockedMemberships.find((membership) => membership.id === id);
10075
+ if (!current) {
10076
+ const target = await client.query(`SELECT ${columns} FROM lattice_project_memberships WHERE tenant_id = $1 AND id = $2 FOR UPDATE`, [tenantId, id]);
10077
+ if (!target.rows[0]) {
10078
+ await client.query("COMMIT");
10079
+ return { kind: "not_found" };
10080
+ }
10081
+ current = mapRow3(target.rows[0]);
10082
+ }
10083
+ if (current.updatedAt.getTime() !== expected.getTime()) {
10084
+ await client.query("COMMIT");
10085
+ return { kind: "conflict" };
10086
+ }
10087
+ if (current.role === "owner" && current.status === "active" && (remove || role !== "owner")) {
10088
+ const owners = lockedMemberships.filter((membership) => membership.id !== id && membership.role === "owner");
10089
+ if (owners.length === 0) {
10090
+ await client.query("COMMIT");
10091
+ return { kind: "last_owner" };
10092
+ }
10093
+ }
10094
+ const result = await client.query(remove ? `UPDATE lattice_project_memberships SET status = 'removed', updated_at = GREATEST(date_trunc('milliseconds', updated_at) + interval '1 millisecond', date_trunc('milliseconds', clock_timestamp())) WHERE tenant_id = $1 AND id = $2 AND date_trunc('milliseconds', updated_at) = $3 RETURNING ${columns}` : `UPDATE lattice_project_memberships SET role = $4, updated_at = GREATEST(date_trunc('milliseconds', updated_at) + interval '1 millisecond', date_trunc('milliseconds', clock_timestamp())) WHERE tenant_id = $1 AND id = $2 AND date_trunc('milliseconds', updated_at) = $3 RETURNING ${columns}`, remove ? [tenantId, id, expected] : [tenantId, id, expected, role]);
10095
+ if (!result.rows[0]) {
10096
+ await client.query("COMMIT");
10097
+ return { kind: "conflict" };
10098
+ }
10099
+ await client.query("COMMIT");
10100
+ return { kind: remove ? "removed" : "updated", membership: mapRow3(result.rows[0]) };
10101
+ } catch (error) {
10102
+ try {
10103
+ await client.query("ROLLBACK");
10104
+ } catch {
10105
+ }
10106
+ throw error;
10107
+ } finally {
10108
+ client.release();
10109
+ }
10110
+ }
10111
+ };
10112
+
10113
+ // src/stores/PostgreSQLProjectBotMembershipStore.ts
10114
+ var columns2 = "id, tenant_id, workspace_id, project_id, room_id, assistant_id, role, title, responsibility, mention_name, status, room_thread_id, joined_at, updated_at";
10115
+ var ProjectBotMembershipIdConflictError = class extends Error {
10116
+ /** Creates an accurate tenant-scoped membership ID collision error. */
10117
+ constructor(tenantId, id) {
10118
+ super(`Project bot membership ID '${id}' already exists in tenant '${tenantId}'`);
10119
+ this.name = "ProjectBotMembershipIdConflictError";
10120
+ }
10121
+ };
10122
+ function isDate2(value) {
10123
+ return value instanceof Date && !Number.isNaN(value.getTime());
10124
+ }
10125
+ function isRole2(value) {
10126
+ return value === "coordinator" || value === "specialist";
10127
+ }
10128
+ function isStatus3(value) {
10129
+ return value === "active" || value === "paused" || value === "removed";
10130
+ }
10131
+ function mapRow4(row) {
10132
+ if (typeof row.id !== "string" || typeof row.tenant_id !== "string" || typeof row.workspace_id !== "string" || typeof row.project_id !== "string" || typeof row.room_id !== "string" || typeof row.assistant_id !== "string" || !isRole2(row.role) || typeof row.title !== "string" || row.responsibility !== null && typeof row.responsibility !== "string" || typeof row.mention_name !== "string" || !isStatus3(row.status) || typeof row.room_thread_id !== "string" || !isDate2(row.joined_at) || !isDate2(row.updated_at)) {
10133
+ throw new Error("Invalid project bot membership row");
10134
+ }
10135
+ return {
10136
+ id: row.id,
10137
+ tenantId: row.tenant_id,
10138
+ workspaceId: row.workspace_id,
10139
+ projectId: row.project_id,
10140
+ roomId: row.room_id,
10141
+ assistantId: row.assistant_id,
10142
+ role: row.role,
10143
+ title: row.title,
10144
+ ...row.responsibility === null ? {} : { responsibility: row.responsibility },
10145
+ mentionName: row.mention_name,
10146
+ status: row.status,
10147
+ roomThreadId: row.room_thread_id,
10148
+ joinedAt: row.joined_at,
10149
+ updatedAt: row.updated_at
10150
+ };
10151
+ }
10152
+ function isUniqueViolation(error) {
10153
+ return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
10154
+ }
10155
+ function constraintConflict(error) {
10156
+ if (!isUniqueViolation(error) || typeof error.constraint !== "string") return void 0;
10157
+ if (error.constraint === "uq_lattice_project_bot_memberships_coordinator") return "coordinator_conflict";
10158
+ if (error.constraint === "uq_lattice_project_bot_memberships_mention") return "mention_conflict";
10159
+ return void 0;
10160
+ }
10161
+ async function rollback(client) {
10162
+ try {
10163
+ await client.query("ROLLBACK");
10164
+ } catch {
10165
+ }
10166
+ }
10167
+ var PostgreSQLProjectBotMembershipStore = class {
10168
+ /** Creates a store using an externally managed shared pool. */
10169
+ constructor(options) {
10170
+ this.pool = options.pool;
10171
+ }
10172
+ /** Lists retained memberships in stable join order. */
10173
+ async list(tenantId, projectId) {
10174
+ const result = await this.pool.query(
10175
+ `SELECT ${columns2} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND project_id = $2 ORDER BY joined_at ASC, id ASC`,
10176
+ [tenantId, projectId]
10177
+ );
10178
+ return result.rows.map(mapRow4);
10179
+ }
10180
+ /** Finds a membership by tenant-scoped ID. */
10181
+ async findById(tenantId, id) {
10182
+ const result = await this.pool.query(
10183
+ `SELECT ${columns2} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND id = $2`,
10184
+ [tenantId, id]
10185
+ );
10186
+ return result.rows[0] ? mapRow4(result.rows[0]) : null;
10187
+ }
10188
+ /** Finds an assistant's durable membership in a tenant-scoped project. */
10189
+ async findByAssistant(tenantId, projectId, assistantId) {
10190
+ const result = await this.pool.query(
10191
+ `SELECT ${columns2} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND project_id = $2 AND assistant_id = $3`,
10192
+ [tenantId, projectId, assistantId]
10193
+ );
10194
+ return result.rows[0] ? mapRow4(result.rows[0]) : null;
10195
+ }
10196
+ /** Inserts a new membership or reactivates the assistant's durable membership atomically. */
10197
+ async save(input) {
10198
+ const client = await this.pool.connect();
10199
+ try {
10200
+ await client.query("BEGIN");
10201
+ await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${input.tenantId}:${input.projectId}`]);
10202
+ const existingResult = await client.query(
10203
+ `SELECT ${columns2} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND project_id = $2 AND assistant_id = $3 FOR UPDATE`,
10204
+ [input.tenantId, input.projectId, input.assistantId]
10205
+ );
10206
+ const existing = existingResult.rows[0] ? mapRow4(existingResult.rows[0]) : void 0;
10207
+ let result;
10208
+ if (existing) {
10209
+ result = await client.query(
10210
+ `UPDATE lattice_project_bot_memberships
10211
+ SET role = $3, title = $4, responsibility = $5, mention_name = $6, status = 'active',
10212
+ updated_at = GREATEST(date_trunc('milliseconds', updated_at) + interval '1 millisecond', date_trunc('milliseconds', clock_timestamp()))
10213
+ WHERE tenant_id = $1 AND id = $2 RETURNING ${columns2}`,
10214
+ [input.tenantId, existing.id, input.role, input.title, input.responsibility ?? null, input.mentionName]
10215
+ );
10216
+ } else {
10217
+ const idResult = await client.query(
10218
+ "SELECT id FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND id = $2",
10219
+ [input.tenantId, input.id]
10220
+ );
10221
+ if (idResult.rows.length > 0) throw new ProjectBotMembershipIdConflictError(input.tenantId, input.id);
10222
+ result = await client.query(
10223
+ `INSERT INTO lattice_project_bot_memberships
10224
+ (id, tenant_id, workspace_id, project_id, room_id, assistant_id, role, title, responsibility, mention_name, status, room_thread_id, joined_at, updated_at)
10225
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'active', $11, date_trunc('milliseconds', clock_timestamp()), date_trunc('milliseconds', clock_timestamp()))
10226
+ RETURNING ${columns2}`,
10227
+ [
10228
+ input.id,
10229
+ input.tenantId,
10230
+ input.workspaceId,
10231
+ input.projectId,
10232
+ input.roomId,
10233
+ input.assistantId,
10234
+ input.role,
10235
+ input.title,
10236
+ input.responsibility ?? null,
10237
+ input.mentionName,
10238
+ input.roomThreadId
10239
+ ]
10240
+ );
10241
+ }
10242
+ const membership = mapRow4(result.rows[0]);
10243
+ await client.query("COMMIT");
10244
+ return { kind: existing === void 0 ? "created" : existing.status === "removed" ? "reactivated" : "updated", membership };
10245
+ } catch (error) {
10246
+ await rollback(client);
10247
+ const conflict = constraintConflict(error);
10248
+ if (conflict) return { kind: conflict };
10249
+ if (isUniqueViolation(error) && error.constraint === "lattice_project_bot_memberships_pkey") {
10250
+ throw new ProjectBotMembershipIdConflictError(input.tenantId, input.id);
10251
+ }
10252
+ throw error;
10253
+ } finally {
10254
+ client.release();
10255
+ }
10256
+ }
10257
+ /** Applies a mutable-field patch with project serialization and millisecond-safe optimistic concurrency. */
10258
+ async update(input) {
10259
+ const client = await this.pool.connect();
10260
+ try {
10261
+ await client.query("BEGIN");
10262
+ const identity = await client.query(
10263
+ "SELECT project_id FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND id = $2",
10264
+ [input.tenantId, input.id]
10265
+ );
10266
+ if (!identity.rows[0]) {
10267
+ await client.query("COMMIT");
10268
+ return { kind: "not_found" };
10269
+ }
10270
+ if (typeof identity.rows[0].project_id !== "string") throw new Error("Invalid project bot membership row");
10271
+ await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${input.tenantId}:${identity.rows[0].project_id}`]);
10272
+ const locked = await client.query(
10273
+ `SELECT ${columns2} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND id = $2 FOR UPDATE`,
10274
+ [input.tenantId, input.id]
10275
+ );
10276
+ if (!locked.rows[0]) {
10277
+ await client.query("COMMIT");
10278
+ return { kind: "not_found" };
10279
+ }
10280
+ const existing = mapRow4(locked.rows[0]);
10281
+ if (existing.updatedAt.getTime() !== input.expectedUpdatedAt.getTime()) {
10282
+ await client.query("COMMIT");
10283
+ return { kind: "conflict" };
10284
+ }
10285
+ const candidate = { ...existing, ...input.patch };
10286
+ const updated = await client.query(
10287
+ `UPDATE lattice_project_bot_memberships
10288
+ SET role = $4, title = $5, responsibility = $6, mention_name = $7, status = $8,
10289
+ updated_at = GREATEST(date_trunc('milliseconds', updated_at) + interval '1 millisecond', date_trunc('milliseconds', clock_timestamp()))
10290
+ WHERE tenant_id = $1 AND id = $2 AND date_trunc('milliseconds', updated_at) = $3
10291
+ RETURNING ${columns2}`,
10292
+ [
10293
+ input.tenantId,
10294
+ input.id,
10295
+ input.expectedUpdatedAt,
10296
+ candidate.role,
10297
+ candidate.title,
10298
+ candidate.responsibility ?? null,
10299
+ candidate.mentionName,
10300
+ candidate.status
10301
+ ]
10302
+ );
10303
+ if (!updated.rows[0]) {
10304
+ await client.query("COMMIT");
10305
+ return { kind: "conflict" };
10306
+ }
10307
+ const membership = mapRow4(updated.rows[0]);
10308
+ await client.query("COMMIT");
10309
+ return { kind: "updated", membership };
10310
+ } catch (error) {
10311
+ await rollback(client);
10312
+ const conflict = constraintConflict(error);
10313
+ if (conflict) return { kind: conflict };
10314
+ throw error;
10315
+ } finally {
10316
+ client.release();
10317
+ }
10318
+ }
10319
+ };
10320
+
10321
+ // src/stores/PostgreSQLProjectRoomMessageStore.ts
10322
+ var columns3 = "id, tenant_id, workspace_id, project_id, room_id, author, content, mentions, reply_to_message_id, source, source_id, idempotency_key, created_at";
10323
+ var ProjectRoomMessageIdConflictError = class extends Error {
10324
+ /** Creates an accurate tenant-scoped message ID collision error. */
10325
+ constructor(tenantId, id) {
10326
+ super(`Project room message ID '${id}' already exists in tenant '${tenantId}'`);
10327
+ this.name = "ProjectRoomMessageIdConflictError";
10328
+ }
10329
+ };
10330
+ var DuplicateProjectRoomMessageIdempotencyKeyError = class extends Error {
10331
+ /** Creates an accurate tenant and room-scoped idempotency collision error. */
10332
+ constructor(tenantId, roomId, idempotencyKey) {
10333
+ super(`Project room message idempotency key '${idempotencyKey}' already exists in tenant '${tenantId}' room '${roomId}'`);
10334
+ this.name = "DuplicateProjectRoomMessageIdempotencyKeyError";
10335
+ }
10336
+ };
10337
+ function isRecord3(value) {
10338
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10339
+ }
10340
+ function hasOnlyKeys(value, keys) {
10341
+ return Object.keys(value).every((key) => keys.includes(key));
10342
+ }
10343
+ function mapAuthor(value) {
10344
+ if (!isRecord3(value) || typeof value.type !== "string") return void 0;
10345
+ if (value.type === "human" && hasOnlyKeys(value, ["type", "userId"]) && typeof value.userId === "string") return { type: "human", userId: value.userId };
10346
+ if (value.type === "bot" && hasOnlyKeys(value, ["type", "membershipId", "assistantId"]) && typeof value.membershipId === "string" && typeof value.assistantId === "string") {
10347
+ return { type: "bot", membershipId: value.membershipId, assistantId: value.assistantId };
10348
+ }
10349
+ if (value.type === "system" && hasOnlyKeys(value, ["type"])) return { type: "system" };
10350
+ return void 0;
10351
+ }
10352
+ function mapMention(value) {
10353
+ if (!isRecord3(value) || typeof value.type !== "string") return void 0;
10354
+ if (value.type === "bot" && hasOnlyKeys(value, ["type", "membershipId"]) && typeof value.membershipId === "string") {
10355
+ return { type: "bot", membershipId: value.membershipId };
10356
+ }
10357
+ if (value.type === "team" && hasOnlyKeys(value, ["type"])) return { type: "team" };
10358
+ return void 0;
10359
+ }
10360
+ function isSource(value) {
10361
+ return value === "user" || value === "agent" || value === "task" || value === "routine" || value === "system";
10362
+ }
10363
+ function mapRow5(row) {
10364
+ const author = mapAuthor(row.author);
10365
+ const content = isRecord3(row.content) && row.content.type === "text" && typeof row.content.text === "string" && hasOnlyKeys(row.content, ["type", "text"]) ? { type: "text", text: row.content.text } : void 0;
10366
+ const mentions = Array.isArray(row.mentions) ? row.mentions.map(mapMention) : void 0;
10367
+ if (typeof row.id !== "string" || typeof row.tenant_id !== "string" || typeof row.workspace_id !== "string" || typeof row.project_id !== "string" || typeof row.room_id !== "string" || !author || !content || !mentions || mentions.some((mention) => mention === void 0) || row.reply_to_message_id !== null && typeof row.reply_to_message_id !== "string" || !isSource(row.source) || row.source_id !== null && typeof row.source_id !== "string" || row.idempotency_key !== null && typeof row.idempotency_key !== "string" || !(row.created_at instanceof Date) || Number.isNaN(row.created_at.getTime())) {
10368
+ throw new Error("Invalid project room message row");
10369
+ }
10370
+ return {
10371
+ id: row.id,
10372
+ tenantId: row.tenant_id,
10373
+ workspaceId: row.workspace_id,
10374
+ projectId: row.project_id,
10375
+ roomId: row.room_id,
10376
+ author,
10377
+ content,
10378
+ mentions,
10379
+ ...row.reply_to_message_id === null ? {} : { replyToMessageId: row.reply_to_message_id },
10380
+ source: row.source,
10381
+ ...row.source_id === null ? {} : { sourceId: row.source_id },
10382
+ ...row.idempotency_key === null ? {} : { idempotencyKey: row.idempotency_key },
10383
+ createdAt: row.created_at
10384
+ };
10385
+ }
10386
+ function isUniqueViolation2(error) {
10387
+ return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
10388
+ }
10389
+ var PostgreSQLProjectRoomMessageStore = class {
10390
+ /** Creates a store using an externally managed shared pool. */
10391
+ constructor(options) {
10392
+ this.pool = options.pool;
10393
+ }
10394
+ /** Creates a room message and maps known uniqueness failures to typed errors. */
10395
+ async create(input) {
10396
+ try {
10397
+ const result = await this.pool.query(
10398
+ `INSERT INTO lattice_project_room_messages
10399
+ (id, tenant_id, workspace_id, project_id, room_id, author, content, mentions, reply_to_message_id, source, source_id, idempotency_key, created_at)
10400
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, date_trunc('milliseconds', clock_timestamp()))
10401
+ RETURNING ${columns3}`,
10402
+ this.parameters(input)
10403
+ );
10404
+ return mapRow5(result.rows[0]);
10405
+ } catch (error) {
10406
+ if (isUniqueViolation2(error) && error.constraint === "lattice_project_room_messages_pkey") {
10407
+ throw new ProjectRoomMessageIdConflictError(input.tenantId, input.id);
10408
+ }
10409
+ if (isUniqueViolation2(error) && error.constraint === "uq_lattice_project_room_messages_idempotency") {
10410
+ throw new DuplicateProjectRoomMessageIdempotencyKeyError(input.tenantId, input.roomId, input.idempotencyKey ?? "");
10411
+ }
10412
+ throw error;
10413
+ }
10414
+ }
10415
+ /** Atomically creates or returns the canonical message for a room-scoped idempotency key. */
10416
+ async createIdempotent(input) {
10417
+ try {
10418
+ const result = await this.pool.query(
10419
+ `INSERT INTO lattice_project_room_messages
10420
+ (id, tenant_id, workspace_id, project_id, room_id, author, content, mentions, reply_to_message_id, source, source_id, idempotency_key, created_at)
10421
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, date_trunc('milliseconds', clock_timestamp()))
10422
+ ON CONFLICT (tenant_id, room_id, idempotency_key) WHERE idempotency_key IS NOT NULL
10423
+ DO UPDATE SET idempotency_key = lattice_project_room_messages.idempotency_key
10424
+ RETURNING ${columns3}`,
10425
+ this.parameters(input)
10426
+ );
10427
+ return mapRow5(result.rows[0]);
10428
+ } catch (error) {
10429
+ if (isUniqueViolation2(error) && error.constraint === "lattice_project_room_messages_pkey") {
10430
+ throw new ProjectRoomMessageIdConflictError(input.tenantId, input.id);
10431
+ }
10432
+ throw error;
10433
+ }
10434
+ }
10435
+ /** Lists messages newest first using an exclusive stable cursor and a clamped limit. */
10436
+ async list(input) {
10437
+ if (input.before && (!(input.before.createdAt instanceof Date) || Number.isNaN(input.before.createdAt.getTime()))) {
10438
+ throw new RangeError("Project room message cursor date is invalid");
10439
+ }
10440
+ const limit = Math.min(100, Math.max(1, Math.trunc(Number.isFinite(input.limit) ? input.limit : 1)));
10441
+ const result = input.before ? await this.pool.query(
10442
+ `SELECT ${columns3} FROM lattice_project_room_messages
10443
+ WHERE tenant_id = $1 AND room_id = $2 AND (created_at < $3 OR (created_at = $3 AND id < $4))
10444
+ ORDER BY created_at DESC, id DESC LIMIT $5`,
10445
+ [input.tenantId, input.roomId, input.before.createdAt, input.before.id, limit]
10446
+ ) : await this.pool.query(
10447
+ `SELECT ${columns3} FROM lattice_project_room_messages
10448
+ WHERE tenant_id = $1 AND room_id = $2 ORDER BY created_at DESC, id DESC LIMIT $3`,
10449
+ [input.tenantId, input.roomId, limit]
10450
+ );
10451
+ return result.rows.map(mapRow5);
10452
+ }
10453
+ /** Finds a message by tenant-scoped ID. */
10454
+ async findById(tenantId, id) {
10455
+ const result = await this.pool.query(
10456
+ `SELECT ${columns3} FROM lattice_project_room_messages WHERE tenant_id = $1 AND id = $2`,
10457
+ [tenantId, id]
10458
+ );
10459
+ return result.rows[0] ? mapRow5(result.rows[0]) : null;
10460
+ }
10461
+ parameters(input) {
10462
+ return [
10463
+ input.id,
10464
+ input.tenantId,
10465
+ input.workspaceId,
10466
+ input.projectId,
10467
+ input.roomId,
10468
+ JSON.stringify(input.author),
10469
+ JSON.stringify(input.content),
10470
+ JSON.stringify(input.mentions),
10471
+ input.replyToMessageId ?? null,
10472
+ input.source,
10473
+ input.sourceId ?? null,
10474
+ input.idempotencyKey ?? null
10475
+ ];
10476
+ }
10477
+ };
10478
+
10479
+ // src/createPgStoreConfig.ts
10480
+ async function createPgStoreConfig(connectionString) {
10481
+ const pool = new import_pg26.Pool({ connectionString });
8862
10482
  const mm = new MigrationManager(pool);
8863
10483
  mm.register(createThreadsTable);
8864
10484
  mm.register(createScheduledTasksTable);
@@ -8913,6 +10533,11 @@ async function createPgStoreConfig(connectionString) {
8913
10533
  mm.register(addA2AKeyAssistantIds);
8914
10534
  mm.register(addTaskWorkItemEventKeyMigration);
8915
10535
  mm.register(createAgentWebAppsTable);
10536
+ mm.register(createCapabilityBundlesTable);
10537
+ mm.register(addTaskWorkItemPendingIndexesMigration);
10538
+ mm.register(createProjectRoomTables);
10539
+ mm.register(addTrustedRunContextColumn);
10540
+ mm.register(addProjectLifecycleEventIndex);
8916
10541
  await mm.migrate();
8917
10542
  const checkpoint = import_langgraph_checkpoint_postgres.PostgresSaver.fromConnString(connectionString);
8918
10543
  checkpoint.setup().catch((err) => {
@@ -8941,17 +10566,22 @@ async function createPgStoreConfig(connectionString) {
8941
10566
  taskWorkItem: taskWorkItemStore,
8942
10567
  a2aApiKey: new PostgreSQLA2AApiKeyStore(opts),
8943
10568
  agentWebApp: new PostgreSQLAgentWebAppStore(opts),
10569
+ capabilityBundle: new PostgreSQLCapabilityBundleStore(opts),
8944
10570
  schedule: new PostgreSQLScheduleStorage(opts),
8945
10571
  menu: new MenuStore(opts),
8946
10572
  sharedResource: new PostgresSharedResourceStore(opts),
8947
10573
  collection: new PostgreSQLCollectionStore(opts),
10574
+ projectRoom: new PostgreSQLProjectRoomStore(opts),
10575
+ projectMembership: new PostgreSQLProjectMembershipStore(opts),
10576
+ projectBotMembership: new PostgreSQLProjectBotMembershipStore(opts),
10577
+ projectRoomMessage: new PostgreSQLProjectRoomMessageStore(opts),
8948
10578
  vectorStoreProvider: new PGVectorStoreProvider(pool, connectionString),
8949
10579
  checkpoint
8950
10580
  };
8951
10581
  }
8952
10582
 
8953
10583
  // src/stores/PostgreSQLSkillStore.ts
8954
- var import_pg25 = require("pg");
10584
+ var import_pg27 = require("pg");
8955
10585
  var PostgreSQLSkillStore = class {
8956
10586
  constructor(options) {
8957
10587
  this.initialized = false;
@@ -8964,9 +10594,9 @@ var PostgreSQLSkillStore = class {
8964
10594
  return;
8965
10595
  }
8966
10596
  if (typeof options.poolConfig === "string") {
8967
- this.pool = new import_pg25.Pool({ connectionString: options.poolConfig });
10597
+ this.pool = new import_pg27.Pool({ connectionString: options.poolConfig });
8968
10598
  } else if (options.poolConfig) {
8969
- this.pool = new import_pg25.Pool(options.poolConfig);
10599
+ this.pool = new import_pg27.Pool(options.poolConfig);
8970
10600
  } else {
8971
10601
  throw new Error("Either pool or poolConfig must be provided");
8972
10602
  }
@@ -9267,7 +10897,7 @@ var PostgreSQLSkillStore = class {
9267
10897
  };
9268
10898
 
9269
10899
  // src/stores/ChannelIdentityMappingStore.ts
9270
- var import_pg26 = require("pg");
10900
+ var import_pg28 = require("pg");
9271
10901
  var ChannelIdentityMappingStore = class {
9272
10902
  constructor(options) {
9273
10903
  this.initialized = false;
@@ -9279,7 +10909,7 @@ var ChannelIdentityMappingStore = class {
9279
10909
  this.initialized = true;
9280
10910
  return;
9281
10911
  }
9282
- this.pool = typeof options.poolConfig === "string" ? new import_pg26.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg26.Pool(options.poolConfig) : (() => {
10912
+ this.pool = typeof options.poolConfig === "string" ? new import_pg28.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg28.Pool(options.poolConfig) : (() => {
9283
10913
  throw new Error("Either pool or poolConfig must be provided");
9284
10914
  })();
9285
10915
  this.migrationManager = new MigrationManager(this.pool);
@@ -9501,6 +11131,8 @@ function mapRowToChannelIdentityMapping(row) {
9501
11131
  0 && (module.exports = {
9502
11132
  ChannelBindingStore,
9503
11133
  ChannelIdentityMappingStore,
11134
+ DuplicateProjectMembershipError,
11135
+ DuplicateProjectRoomMessageIdempotencyKeyError,
9504
11136
  MenuStore,
9505
11137
  MigrationManager,
9506
11138
  PGVectorStoreProvider,
@@ -9508,6 +11140,7 @@ function mapRowToChannelIdentityMapping(row) {
9508
11140
  PostgreSQLA2AApiKeyStore,
9509
11141
  PostgreSQLAgentWebAppStore,
9510
11142
  PostgreSQLAssistantStore,
11143
+ PostgreSQLCapabilityBundleStore,
9511
11144
  PostgreSQLChannelInstallationStore,
9512
11145
  PostgreSQLCollectionStore,
9513
11146
  PostgreSQLConnectionStore,
@@ -9515,9 +11148,14 @@ function mapRowToChannelIdentityMapping(row) {
9515
11148
  PostgreSQLEvalStore,
9516
11149
  PostgreSQLMcpServerConfigStore,
9517
11150
  PostgreSQLMetricsServerConfigStore,
11151
+ PostgreSQLProjectBotMembershipStore,
11152
+ PostgreSQLProjectMembershipStore,
11153
+ PostgreSQLProjectRoomMessageStore,
11154
+ PostgreSQLProjectRoomStore,
9518
11155
  PostgreSQLProjectStore,
9519
11156
  PostgreSQLScheduleStorage,
9520
11157
  PostgreSQLSkillStore,
11158
+ PostgreSQLTaskWorkItemStore,
9521
11159
  PostgreSQLTenantStore,
9522
11160
  PostgreSQLThreadStore,
9523
11161
  PostgreSQLUserStore,
@@ -9525,6 +11163,9 @@ function mapRowToChannelIdentityMapping(row) {
9525
11163
  PostgreSQLWorkflowTrackingStore,
9526
11164
  PostgreSQLWorkspaceStore,
9527
11165
  PostgresSharedResourceStore,
11166
+ ProjectBotMembershipIdConflictError,
11167
+ ProjectMembershipIdConflictError,
11168
+ ProjectRoomMessageIdConflictError,
9528
11169
  ThreadMessageQueueStore,
9529
11170
  addAssistantTenantId,
9530
11171
  addEnvToEvalRuns,
@@ -9546,6 +11187,7 @@ function mapRowToChannelIdentityMapping(row) {
9546
11187
  createA2AApiKeysTable,
9547
11188
  createAgentWebAppsTable,
9548
11189
  createAssistantsTable,
11190
+ createCapabilityBundlesTable,
9549
11191
  createChannelBindingsTable,
9550
11192
  createChannelIdentityMappingTables,
9551
11193
  createChannelInstallationsTable,
@@ -9561,6 +11203,7 @@ function mapRowToChannelIdentityMapping(row) {
9561
11203
  createMetricsConfigsTable,
9562
11204
  createPGVectorStoreProvider,
9563
11205
  createPgStoreConfig,
11206
+ createProjectRoomTables,
9564
11207
  createProjectsTable,
9565
11208
  createScheduledTasksTable,
9566
11209
  createSharedResourcesTable,