@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
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { ThreadMessageQueueStore } from '../stores/ThreadMessageQueueStore';
8
+ import type { AddMessageParams } from '@axiom-lattice/core';
8
9
 
9
10
  // Mock pg Pool
10
11
  const mockQuery = jest.fn();
@@ -23,6 +24,13 @@ describe('ThreadMessageQueueStore', () => {
23
24
  const mockThreadId = 'test-thread';
24
25
  const mockTenantId = 'test-tenant';
25
26
  const mockAssistantId = 'test-assistant';
27
+ const trusted = {
28
+ projectRoom: {
29
+ tenantId: mockTenantId, workspaceId: 'workspace', projectId: 'project', roomId: 'room',
30
+ sourceRoomMessageId: 'room-message', membershipId: 'membership', assistantId: mockAssistantId,
31
+ inputMessageId: 'input-1', role: 'coordinator' as const, title: 'Coordinator',
32
+ },
33
+ };
26
34
 
27
35
  beforeEach(() => {
28
36
  store = new ThreadMessageQueueStore({
@@ -34,6 +42,102 @@ describe('ThreadMessageQueueStore', () => {
34
42
  });
35
43
 
36
44
  describe('addMessage', () => {
45
+ it.each(['addMessage', 'addMessageIfCapacity', 'addMessageAtHead'] as const)(
46
+ 'persists trusted metadata through %s',
47
+ async (method) => {
48
+ const row = {
49
+ id: 'input-1', message_content: JSON.stringify('message'), message_type: 'human',
50
+ sequence_order: 1, created_at: new Date(), trusted_run_context: trusted,
51
+ execution_mode: 'followup',
52
+ };
53
+ const params = {
54
+ threadId: mockThreadId, tenantId: mockTenantId, assistantId: mockAssistantId,
55
+ workspaceId: 'workspace', projectId: 'project', id: 'input-1', content: 'message',
56
+ trusted_run_context: trusted, execution_mode: 'followup' as const,
57
+ };
58
+
59
+ if (method === 'addMessageIfCapacity') {
60
+ const client = {
61
+ query: jest.fn()
62
+ .mockResolvedValueOnce({}).mockResolvedValueOnce({})
63
+ .mockResolvedValueOnce({ rows: [{ count: '0' }] })
64
+ .mockResolvedValueOnce({ rows: [{ next_seq: 1 }] })
65
+ .mockResolvedValueOnce({ rows: [row] }).mockResolvedValueOnce({}),
66
+ release: jest.fn(),
67
+ };
68
+ mockConnect.mockResolvedValueOnce(client);
69
+ await store.addMessageIfCapacity(params, 10);
70
+ expect(client.query.mock.calls[4][0]).toEqual(expect.stringContaining('trusted_run_context'));
71
+ expect(client.query.mock.calls[4][1]).toEqual(expect.arrayContaining([JSON.stringify(trusted), 'followup']));
72
+ return;
73
+ }
74
+
75
+ mockQuery.mockResolvedValueOnce({ rows: [{ next_seq: 1 }] }).mockResolvedValueOnce({ rows: [row] });
76
+ await store[method](params);
77
+ expect(mockQuery).toHaveBeenNthCalledWith(2, expect.stringContaining('trusted_run_context'),
78
+ expect.arrayContaining([JSON.stringify(trusted), 'followup']));
79
+ },
80
+ );
81
+ it.each(['addMessage', 'addMessageIfCapacity', 'addMessageAtHead'] as const)(
82
+ 'rejects malformed trusted context before querying through %s',
83
+ async (method) => {
84
+ const params = {
85
+ threadId: mockThreadId, tenantId: mockTenantId, assistantId: mockAssistantId, content: 'message',
86
+ trusted_run_context: { projectRoom: { tenantId: mockTenantId } },
87
+ } as unknown as AddMessageParams;
88
+
89
+ const operation = method === 'addMessageIfCapacity'
90
+ ? store.addMessageIfCapacity(params, 10)
91
+ : store[method](params);
92
+ await expect(operation).rejects.toThrow('Invalid trusted agent run context');
93
+ expect(mockQuery).not.toHaveBeenCalled();
94
+ expect(mockConnect).not.toHaveBeenCalled();
95
+ },
96
+ );
97
+
98
+ it.each(['addMessage', 'addMessageIfCapacity', 'addMessageAtHead'] as const)(
99
+ 'rejects invalid execution mode before querying through %s',
100
+ async (method) => {
101
+ const params: AddMessageParams = {
102
+ threadId: mockThreadId, tenantId: mockTenantId, assistantId: mockAssistantId, content: 'message',
103
+ execution_mode: 'steer' as unknown as AddMessageParams['execution_mode'],
104
+ };
105
+
106
+ const operation = method === 'addMessageIfCapacity'
107
+ ? store.addMessageIfCapacity(params, 10)
108
+ : store[method](params);
109
+ await expect(operation).rejects.toThrow('Invalid queued execution mode');
110
+ expect(mockQuery).not.toHaveBeenCalled();
111
+ expect(mockConnect).not.toHaveBeenCalled();
112
+ },
113
+ );
114
+ it('atomically rejects enqueue after the scoped pending capacity is reached', async () => {
115
+ const client = {
116
+ query: jest.fn()
117
+ .mockResolvedValueOnce({})
118
+ .mockResolvedValueOnce({})
119
+ .mockResolvedValueOnce({ rows: [{ count: '2' }] })
120
+ .mockResolvedValueOnce({}),
121
+ release: jest.fn(),
122
+ };
123
+ mockConnect.mockResolvedValueOnce(client);
124
+
125
+ await expect(store.addMessageIfCapacity({
126
+ threadId: mockThreadId,
127
+ tenantId: mockTenantId,
128
+ assistantId: mockAssistantId,
129
+ workspaceId: null,
130
+ projectId: null,
131
+ content: 'full',
132
+ }, 2)).resolves.toBe(false);
133
+
134
+ expect(client.query).toHaveBeenCalledWith(
135
+ 'SELECT pg_advisory_xact_lock(hashtext($1))',
136
+ [`${mockTenantId}:${mockAssistantId}:${mockThreadId}::`],
137
+ );
138
+ expect(client.release).toHaveBeenCalled();
139
+ });
140
+
37
141
  it('should add message to queue', async () => {
38
142
  const mockMessage = {
39
143
  id: 'msg-1',
@@ -131,7 +235,6 @@ describe('ThreadMessageQueueStore', () => {
131
235
  it('should insert message with high priority', async () => {
132
236
  // Mock thread info query
133
237
  mockQuery
134
- .mockResolvedValueOnce({ rows: [{ tenant_id: mockTenantId, assistant_id: mockAssistantId }] }) // Get thread info
135
238
  .mockResolvedValueOnce({ rows: [{ next_seq: 1 }] }) // Get next sequence
136
239
  .mockResolvedValueOnce({
137
240
  rows: [{
@@ -158,7 +261,6 @@ describe('ThreadMessageQueueStore', () => {
158
261
 
159
262
  it('should use default tenant/assistant when no existing messages', async () => {
160
263
  mockQuery
161
- .mockResolvedValueOnce({ rows: [] }) // No existing messages
162
264
  .mockResolvedValueOnce({ rows: [{ next_seq: 0 }] }) // Get next sequence
163
265
  .mockResolvedValueOnce({
164
266
  rows: [{
@@ -259,6 +361,79 @@ describe('ThreadMessageQueueStore', () => {
259
361
  expect(result[0].custom_run_config).toEqual(customRunConfig);
260
362
  expect((result[0].content as { command?: unknown }).command).toBeUndefined();
261
363
  });
364
+
365
+ it('hydrates trusted metadata for pending and processing rows', async () => {
366
+ const row = {
367
+ id: 'input-1', message_content: JSON.stringify('message'), message_type: 'human',
368
+ sequence_order: 1, created_at: new Date(), trusted_run_context: JSON.stringify(trusted),
369
+ execution_mode: 'followup',
370
+ };
371
+ mockQuery.mockResolvedValueOnce({ rows: [row] }).mockResolvedValueOnce({ rows: [row] });
372
+
373
+ expect((await store.getPendingMessages(mockThreadId))[0]).toMatchObject({
374
+ trusted_run_context: trusted, execution_mode: 'followup',
375
+ });
376
+ expect((await store.getProcessingMessages(mockThreadId))[0]).toMatchObject({
377
+ trusted_run_context: trusted, execution_mode: 'followup',
378
+ });
379
+ });
380
+
381
+ it('rejects hostile trusted JSON read from PostgreSQL', async () => {
382
+ mockQuery.mockResolvedValueOnce({ rows: [{
383
+ id: 'hostile', message_content: JSON.stringify('message'), message_type: 'human',
384
+ sequence_order: 1, created_at: new Date(),
385
+ trusted_run_context: { projectRoom: { tenantId: mockTenantId } }, execution_mode: 'followup',
386
+ }] });
387
+
388
+ await expect(store.getPendingMessages(mockThreadId)).rejects.toThrow('Invalid trusted agent run context');
389
+ });
390
+
391
+ it('matches nullish dimensions as NULL when a scope is supplied', async () => {
392
+ mockQuery.mockResolvedValueOnce({ rows: [] });
393
+
394
+ await store.getPendingMessages(mockThreadId, {
395
+ tenantId: mockTenantId,
396
+ assistantId: mockAssistantId,
397
+ workspaceId: undefined,
398
+ projectId: undefined,
399
+ });
400
+
401
+ expect(mockQuery).toHaveBeenCalledWith(
402
+ expect.stringMatching(/tenant_id = \$2.*assistant_id = \$3.*workspace_id IS NULL.*project_id IS NULL/s),
403
+ [mockThreadId, mockTenantId, mockAssistantId],
404
+ );
405
+ });
406
+
407
+ it('applies null-sensitive full scope predicates to every scoped operation', async () => {
408
+ const scope = {
409
+ tenantId: mockTenantId,
410
+ assistantId: mockAssistantId,
411
+ workspaceId: undefined,
412
+ projectId: undefined,
413
+ };
414
+ mockQuery
415
+ .mockResolvedValueOnce({ rows: [] })
416
+ .mockResolvedValueOnce({ rows: [] })
417
+ .mockResolvedValueOnce({ rows: [{ count: '0' }] })
418
+ .mockResolvedValueOnce({ rowCount: 0 })
419
+ .mockResolvedValueOnce({ rowCount: 0 })
420
+ .mockResolvedValueOnce({ rowCount: 0 })
421
+ .mockResolvedValueOnce({ rowCount: 0 });
422
+
423
+ await store.getProcessingMessages(mockThreadId, scope);
424
+ await store.getPendingMessages(mockThreadId, scope);
425
+ await store.getQueueSize(mockThreadId, scope);
426
+ await store.markProcessing('msg-1', undefined, scope);
427
+ await store.resetProcessingToPending(mockThreadId, scope);
428
+ await store.removeMessage('msg-1', scope);
429
+ await store.clearMessages(mockThreadId, scope);
430
+
431
+ expect(mockQuery).toHaveBeenCalledTimes(7);
432
+ for (const [sql, params] of mockQuery.mock.calls as Array<[string, unknown[]]>) {
433
+ expect(sql).toMatch(/tenant_id = \$2.*assistant_id = \$3.*workspace_id IS NULL.*project_id IS NULL/s);
434
+ expect(params).toEqual(expect.arrayContaining([mockTenantId, mockAssistantId]));
435
+ }
436
+ });
262
437
  });
263
438
 
264
439
  describe('getQueueSize', () => {
@@ -296,6 +471,26 @@ describe('ThreadMessageQueueStore', () => {
296
471
  expect(result[1].threadId).toBe('thread-2');
297
472
  expect(result[2].threadId).toBe('thread-3');
298
473
  });
474
+
475
+ it('preserves NULL workspace and project identity for legacy rows', async () => {
476
+ mockQuery.mockResolvedValueOnce({
477
+ rows: [{
478
+ tenant_id: mockTenantId,
479
+ assistant_id: mockAssistantId,
480
+ thread_id: mockThreadId,
481
+ workspace_id: null,
482
+ project_id: null,
483
+ }],
484
+ });
485
+
486
+ await expect(store.getThreadsWithPendingMessages()).resolves.toEqual([{
487
+ tenantId: mockTenantId,
488
+ assistantId: mockAssistantId,
489
+ threadId: mockThreadId,
490
+ workspaceId: null,
491
+ projectId: null,
492
+ }]);
493
+ });
299
494
  });
300
495
 
301
496
  describe('removeMessage', () => {
@@ -323,10 +518,20 @@ describe('ThreadMessageQueueStore', () => {
323
518
  await store.markProcessing('msg-1');
324
519
 
325
520
  expect(mockQuery).toHaveBeenCalledWith(
326
- expect.stringContaining("UPDATE thread_message_queue"),
521
+ expect.stringContaining("UPDATE lattice_thread_message_queue"),
327
522
  ['msg-1']
328
523
  );
329
524
  });
525
+
526
+ it('updates only status and custom config, preserving trusted columns', async () => {
527
+ mockQuery.mockResolvedValueOnce({ rowCount: 1 });
528
+ await store.markProcessing('input-1', { model: 'updated' });
529
+
530
+ const sql = mockQuery.mock.calls[0][0] as string;
531
+ expect(sql).toContain("status = 'processing', custom_run_config = $2");
532
+ expect(sql).not.toContain('trusted_run_context =');
533
+ expect(sql).not.toContain('execution_mode =');
534
+ });
330
535
  });
331
536
 
332
537
  describe('clearMessages', () => {
@@ -336,7 +541,7 @@ describe('ThreadMessageQueueStore', () => {
336
541
  await store.clearMessages(mockThreadId);
337
542
 
338
543
  expect(mockQuery).toHaveBeenCalledWith(
339
- expect.stringContaining("DELETE FROM thread_message_queue"),
544
+ expect.stringContaining("DELETE FROM lattice_thread_message_queue"),
340
545
  [mockThreadId]
341
546
  );
342
547
  });
@@ -0,0 +1,15 @@
1
+ import type { PoolClient } from "pg";
2
+ import { addWorkspaceProjectToQueue } from "../migrations/add_workspace_project_to_queue";
3
+
4
+ describe("addWorkspaceProjectToQueue", () => {
5
+ it("adds nullable scope columns so legacy rows retain NULL identity", async () => {
6
+ const query = jest.fn().mockResolvedValue({ rows: [] });
7
+
8
+ await addWorkspaceProjectToQueue.up({ query } as unknown as PoolClient);
9
+
10
+ const sql = query.mock.calls.map(([statement]) => statement as string).join("\n");
11
+ expect(sql).toContain("ADD COLUMN IF NOT EXISTS workspace_id VARCHAR(255)");
12
+ expect(sql).toContain("ADD COLUMN IF NOT EXISTS project_id VARCHAR(255)");
13
+ expect(sql).not.toMatch(/SET\s+DEFAULT|NOT\s+NULL|UPDATE\s+lattice_thread_message_queue/i);
14
+ });
15
+ });
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it, jest } from "@jest/globals";
2
+ import type { Pool, PoolClient } from "pg";
3
+ import { addTrustedRunContextColumn } from "../migrations/add_trusted_run_context_column";
4
+ import { MigrationManager, type Migration } from "../migrations/migration";
5
+ import { createProjectRoomTables } from "../migrations/project_room_migration";
6
+ import { addTaskDependenciesGinIndex } from "../migrations/task_migration";
7
+ import {
8
+ addProjectLifecycleEventIndex,
9
+ addTaskWorkItemPendingIndexesMigration,
10
+ } from "../migrations/task_work_items_migration";
11
+
12
+ describe("migration name and version compatibility", () => {
13
+ it("skips renumbered feature migrations already applied under their stable names", async () => {
14
+ const previouslyApplied = [
15
+ { name: "create_project_room_tables", version: 171, applied_at: new Date() },
16
+ { name: "add_thread_queue_trusted_run_context", version: 172, applied_at: new Date() },
17
+ { name: "add_task_dependencies_gin_index", version: 173, applied_at: new Date() },
18
+ { name: "add_project_lifecycle_event_index", version: 174, applied_at: new Date() },
19
+ ];
20
+ const executedUpNames: string[] = [];
21
+ const insertedNames: string[] = [];
22
+ const release = jest.fn();
23
+ const query = jest.fn(async (sql: string, params?: unknown[]) => {
24
+ if (sql.includes("information_schema.tables")) return { rows: [{ exists: true }] };
25
+ if (sql.includes("pg_index")) {
26
+ return { rows: [{ column_name: "name", constraint_name: "lattice_schema_migrations_pkey" }] };
27
+ }
28
+ if (sql === "SELECT name, version, applied_at FROM lattice_schema_migrations ORDER BY version") {
29
+ return { rows: previouslyApplied };
30
+ }
31
+ if (sql.includes("INSERT INTO lattice_schema_migrations")) {
32
+ insertedNames.push(params?.[0] as string);
33
+ }
34
+ return { rows: [] };
35
+ });
36
+ const client = { query, release } as unknown as PoolClient;
37
+ const pool = { connect: jest.fn(async () => client) } as unknown as Pool;
38
+ const manager = new MigrationManager(pool);
39
+ const migrations = [
40
+ addTaskWorkItemPendingIndexesMigration,
41
+ createProjectRoomTables,
42
+ addTrustedRunContextColumn,
43
+ addTaskDependenciesGinIndex,
44
+ addProjectLifecycleEventIndex,
45
+ ];
46
+ for (const migration of migrations) {
47
+ const wrapped: Migration = {
48
+ ...migration,
49
+ up: async (migrationClient) => {
50
+ executedUpNames.push(migration.name);
51
+ await migration.up(migrationClient);
52
+ },
53
+ };
54
+ manager.register(wrapped);
55
+ }
56
+
57
+ await manager.migrate();
58
+
59
+ expect(migrations.map(({ version }) => version).every(
60
+ (version, index, versions) => index === 0 || version > versions[index - 1],
61
+ )).toBe(true);
62
+ expect(executedUpNames).toEqual(["add_task_work_item_pending_indexes"]);
63
+ expect(insertedNames).toEqual(["add_task_work_item_pending_indexes"]);
64
+ expect(insertedNames).not.toEqual(expect.arrayContaining(previouslyApplied.map(({ name }) => name)));
65
+ expect(release).toHaveBeenCalledTimes(1);
66
+ });
67
+ });
@@ -13,9 +13,10 @@ describe("add_files_to_tasks migration", () => {
13
13
  expect(taskMigrations.map((m) => m.name)).toContain("add_files_to_tasks");
14
14
  });
15
15
 
16
- it("has a version greater than all existing task migrations", () => {
17
- const versions = taskMigrations.map((m) => m.version);
18
- expect(addFilesToTasks.version).toBe(Math.max(...versions));
16
+ it("has a version greater than the task migrations registered before it", () => {
17
+ const migrationIndex = taskMigrations.indexOf(addFilesToTasks);
18
+ const precedingVersions = taskMigrations.slice(0, migrationIndex).map((m) => m.version);
19
+ expect(addFilesToTasks.version).toBeGreaterThan(Math.max(...precedingVersions));
19
20
  });
20
21
 
21
22
  it("emits ADD COLUMN files JSONB in up()", async () => {
@@ -62,6 +62,7 @@ import { addPriorityAndCommandColumns } from "./migrations/add_priority_command_
62
62
  import { addCustomRunConfigColumn } from "./migrations/add_custom_run_config_column";
63
63
  import { alterMessageQueueIdColumn } from "./migrations/alter_message_queue_id_column";
64
64
  import { addWorkspaceProjectToQueue } from "./migrations/add_workspace_project_to_queue";
65
+ import { addTrustedRunContextColumn } from "./migrations/add_trusted_run_context_column";
65
66
  import { createWorkflowTrackingTables, addStepThreadId, addWorkflowRunsTenantStatusUpdatedIndex } from "./migrations/workflow_tracking_migrations";
66
67
  import { evalMigrations } from "./migrations/eval_migrations";
67
68
  import { createA2AApiKeysTable } from "./migrations/a2a_api_key_migration";
@@ -71,8 +72,21 @@ import { createMenuItemsTable } from "./migrations/menu_items_migration";
71
72
  import { addFileContentType } from "./migrations/menu_items_add_file_type";
72
73
  import { createSharedResourcesTable } from "./migrations/shared_resources_migration";
73
74
  import { createCollectionsTable } from "./migrations/collection_migrations";
74
- import { createTaskWorkItemsMigration, addWorkItemProjectFieldsMigration, addTaskWorkItemEventKeyMigration } from "./migrations/task_work_items_migration";
75
+ import {
76
+ createTaskWorkItemsMigration,
77
+ addWorkItemProjectFieldsMigration,
78
+ addTaskWorkItemEventKeyMigration,
79
+ addTaskWorkItemPendingIndexesMigration,
80
+ addProjectLifecycleEventIndex,
81
+ } from "./migrations/task_work_items_migration";
75
82
  import { createAgentWebAppsTable } from "./migrations/agent_web_apps_migration";
83
+ import { createCapabilityBundlesTable } from "./migrations/capability_bundle_migration";
84
+ import { createProjectRoomTables } from "./migrations/project_room_migration";
85
+ import { PostgreSQLCapabilityBundleStore } from "./stores/PostgreSQLCapabilityBundleStore";
86
+ import { PostgreSQLProjectRoomStore } from "./stores/PostgreSQLProjectRoomStore";
87
+ import { PostgreSQLProjectMembershipStore } from "./stores/PostgreSQLProjectMembershipStore";
88
+ import { PostgreSQLProjectBotMembershipStore } from "./stores/PostgreSQLProjectBotMembershipStore";
89
+ import { PostgreSQLProjectRoomMessageStore } from "./stores/PostgreSQLProjectRoomMessageStore";
76
90
 
77
91
  export async function createPgStoreConfig(connectionString: string) {
78
92
  const pool = new Pool({ connectionString });
@@ -134,6 +148,11 @@ export async function createPgStoreConfig(connectionString: string) {
134
148
  mm.register(addA2AKeyAssistantIds); // v167
135
149
  mm.register(addTaskWorkItemEventKeyMigration); // v168
136
150
  mm.register(createAgentWebAppsTable); // v169
151
+ mm.register(createCapabilityBundlesTable); // v170
152
+ mm.register(addTaskWorkItemPendingIndexesMigration); // v171
153
+ mm.register(createProjectRoomTables); // v172
154
+ mm.register(addTrustedRunContextColumn); // v173
155
+ mm.register(addProjectLifecycleEventIndex); // v175
137
156
 
138
157
  await mm.migrate();
139
158
 
@@ -167,10 +186,15 @@ export async function createPgStoreConfig(connectionString: string) {
167
186
  taskWorkItem: taskWorkItemStore,
168
187
  a2aApiKey: new PostgreSQLA2AApiKeyStore(opts),
169
188
  agentWebApp: new PostgreSQLAgentWebAppStore(opts),
189
+ capabilityBundle: new PostgreSQLCapabilityBundleStore(opts),
170
190
  schedule: new PostgreSQLScheduleStorage(opts),
171
191
  menu: new MenuStore(opts),
172
192
  sharedResource: new PostgresSharedResourceStore(opts),
173
193
  collection: new PostgreSQLCollectionStore(opts),
194
+ projectRoom: new PostgreSQLProjectRoomStore(opts),
195
+ projectMembership: new PostgreSQLProjectMembershipStore(opts),
196
+ projectBotMembership: new PostgreSQLProjectBotMembershipStore(opts),
197
+ projectRoomMessage: new PostgreSQLProjectRoomMessageStore(opts),
174
198
  vectorStoreProvider: new PGVectorStoreProvider(pool, connectionString),
175
199
  checkpoint,
176
200
  };
package/src/index.ts CHANGED
@@ -58,6 +58,9 @@ export * from "./stores/ChannelIdentityMappingStore";
58
58
  export * from "./stores/PostgreSQLChannelInstallationStore";
59
59
  export * from "./stores/PostgreSQLA2AApiKeyStore";
60
60
  export * from "./stores/PostgreSQLAgentWebAppStore";
61
+ export * from "./stores/PostgreSQLTaskWorkItemStore";
62
+ export * from "./stores/PostgreSQLCapabilityBundleStore";
63
+ export * from "./migrations/capability_bundle_migration";
61
64
  export * from "./stores/PostgreSQLWorkflowTrackingStore";
62
65
  export * from "./stores/PostgreSQLEvalStore";
63
66
  export * from "./migrations/eval_migrations";
@@ -68,6 +71,11 @@ export * from "./stores/MenuStore";
68
71
  export * from "./stores/SharedResourceStore";
69
72
  export * from "./stores/PostgresSharedResourceStore";
70
73
  export * from "./stores/PostgreSQLCollectionStore";
74
+ export * from "./stores/PostgreSQLProjectRoomStore";
75
+ export * from "./stores/PostgreSQLProjectMembershipStore";
76
+ export * from "./stores/PostgreSQLProjectBotMembershipStore";
77
+ export * from "./stores/PostgreSQLProjectRoomMessageStore";
78
+ export * from "./migrations/project_room_migration";
71
79
 
72
80
  export * from "./PGVectorStoreProvider";
73
81
 
@@ -125,6 +133,7 @@ export type {
125
133
  CreateMenuItemInput,
126
134
  ChannelInstallationStore,
127
135
  ChannelInstallation,
136
+ CreateChannelInstallationInput,
128
137
  CreateChannelInstallationRequest,
129
138
  UpdateChannelInstallationRequest,
130
139
  LarkChannelInstallationConfig,
@@ -191,4 +200,8 @@ export type {
191
200
  AgentWebAppAppearance,
192
201
  CreateAgentWebAppInput,
193
202
  UpdateAgentWebAppInput,
203
+ CapabilityBundleStore,
204
+ CapabilityBundle,
205
+ CreateCapabilityBundleInput,
206
+ UpdateCapabilityBundleInput,
194
207
  } from "@axiom-lattice/protocols";
@@ -0,0 +1,18 @@
1
+ import type { Migration } from "./migration";
2
+
3
+ /** Adds durable privileged context and execution mode to queued thread messages. */
4
+ export const addTrustedRunContextColumn: Migration = {
5
+ version: 173,
6
+ name: "add_thread_queue_trusted_run_context",
7
+ up: async (client): Promise<void> => {
8
+ await client.query(`ALTER TABLE lattice_thread_message_queue
9
+ ADD COLUMN IF NOT EXISTS trusted_run_context JSONB,
10
+ ADD COLUMN IF NOT EXISTS execution_mode VARCHAR(20)
11
+ CHECK (execution_mode IS NULL OR execution_mode = 'followup')`);
12
+ },
13
+ down: async (client): Promise<void> => {
14
+ await client.query(`ALTER TABLE lattice_thread_message_queue
15
+ DROP COLUMN IF EXISTS execution_mode,
16
+ DROP COLUMN IF EXISTS trusted_run_context`);
17
+ },
18
+ };
@@ -0,0 +1,20 @@
1
+ import type { Migration } from "./migration";
2
+
3
+ /** Creates the tenant-scoped capability bundle table. */
4
+ export const createCapabilityBundlesTable: Migration = {
5
+ version: 170,
6
+ name: "create_capability_bundles_table",
7
+ up: async (client) => {
8
+ await client.query(`CREATE TABLE IF NOT EXISTS lattice_capability_bundles (
9
+ id UUID PRIMARY KEY, tenant_id VARCHAR(255) NOT NULL, bundle_key VARCHAR(255) NOT NULL,
10
+ name VARCHAR(255) NOT NULL, description TEXT, capabilities JSONB NOT NULL DEFAULT '[]'::jsonb,
11
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
12
+ UNIQUE (tenant_id, bundle_key)
13
+ )`);
14
+ await client.query("CREATE INDEX IF NOT EXISTS idx_lattice_capability_bundles_tenant ON lattice_capability_bundles(tenant_id)");
15
+ },
16
+ down: async (client) => {
17
+ await client.query("DROP INDEX IF EXISTS idx_lattice_capability_bundles_tenant");
18
+ await client.query("DROP TABLE IF EXISTS lattice_capability_bundles");
19
+ },
20
+ };
@@ -83,7 +83,8 @@ export class MigrationManager {
83
83
  const tableExists = await client.query(`
84
84
  SELECT EXISTS (
85
85
  SELECT FROM information_schema.tables
86
- WHERE table_name = 'lattice_schema_migrations'
86
+ WHERE table_schema = current_schema()
87
+ AND table_name = 'lattice_schema_migrations'
87
88
  )
88
89
  `);
89
90
 
@@ -0,0 +1,128 @@
1
+ import type { Migration } from "./migration";
2
+
3
+ /** Creates the tenant-scoped project room, roster, and message tables. */
4
+ export const createProjectRoomTables: Migration = {
5
+ version: 172,
6
+ name: "create_project_room_tables",
7
+ up: async (client) => {
8
+ await client.query(`
9
+ CREATE TABLE IF NOT EXISTS lattice_project_rooms (
10
+ id VARCHAR(255) NOT NULL,
11
+ tenant_id VARCHAR(255) NOT NULL,
12
+ workspace_id VARCHAR(255) NOT NULL,
13
+ project_id VARCHAR(255) NOT NULL,
14
+ name VARCHAR(255) NOT NULL,
15
+ type VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_rooms_type
16
+ CHECK (type IN ('main')),
17
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
18
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
19
+ PRIMARY KEY (tenant_id, id),
20
+ UNIQUE (tenant_id, project_id, type)
21
+ )
22
+ `);
23
+
24
+ await client.query(`
25
+ CREATE TABLE IF NOT EXISTS lattice_project_memberships (
26
+ id VARCHAR(255) NOT NULL,
27
+ tenant_id VARCHAR(255) NOT NULL,
28
+ project_id VARCHAR(255) NOT NULL,
29
+ user_id VARCHAR(255) NOT NULL,
30
+ role VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_memberships_role
31
+ CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
32
+ status VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_memberships_status
33
+ CHECK (status IN ('active', 'removed')),
34
+ joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
35
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
36
+ PRIMARY KEY (tenant_id, id),
37
+ UNIQUE (tenant_id, project_id, user_id)
38
+ )
39
+ `);
40
+ await client.query(`
41
+ CREATE INDEX IF NOT EXISTS idx_lattice_project_memberships_project
42
+ ON lattice_project_memberships (tenant_id, project_id)
43
+ `);
44
+
45
+ await client.query(`
46
+ CREATE TABLE IF NOT EXISTS lattice_project_bot_memberships (
47
+ id VARCHAR(255) NOT NULL,
48
+ tenant_id VARCHAR(255) NOT NULL,
49
+ workspace_id VARCHAR(255) NOT NULL,
50
+ project_id VARCHAR(255) NOT NULL,
51
+ room_id VARCHAR(255) NOT NULL,
52
+ assistant_id VARCHAR(255) NOT NULL,
53
+ role VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_bot_memberships_role
54
+ CHECK (role IN ('coordinator', 'specialist')),
55
+ title VARCHAR(255) NOT NULL,
56
+ responsibility TEXT,
57
+ mention_name VARCHAR(255) NOT NULL,
58
+ status VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_bot_memberships_status
59
+ CHECK (status IN ('active', 'paused', 'removed')),
60
+ room_thread_id VARCHAR(255) NOT NULL,
61
+ joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
62
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
63
+ PRIMARY KEY (tenant_id, id),
64
+ UNIQUE (tenant_id, project_id, assistant_id)
65
+ )
66
+ `);
67
+ await client.query(`
68
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_project_bot_memberships_coordinator
69
+ ON lattice_project_bot_memberships (tenant_id, project_id)
70
+ WHERE role = 'coordinator' AND status IN ('active', 'paused')
71
+ `);
72
+ await client.query(`
73
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_project_bot_memberships_mention
74
+ ON lattice_project_bot_memberships (tenant_id, room_id, mention_name)
75
+ WHERE status IN ('active', 'paused')
76
+ `);
77
+ await client.query(`
78
+ CREATE INDEX IF NOT EXISTS idx_lattice_project_bot_memberships_project
79
+ ON lattice_project_bot_memberships (tenant_id, project_id)
80
+ `);
81
+ await client.query(`
82
+ CREATE INDEX IF NOT EXISTS idx_lattice_project_bot_memberships_room
83
+ ON lattice_project_bot_memberships (tenant_id, room_id)
84
+ `);
85
+
86
+ await client.query(`
87
+ CREATE TABLE IF NOT EXISTS lattice_project_room_messages (
88
+ id VARCHAR(255) NOT NULL,
89
+ tenant_id VARCHAR(255) NOT NULL,
90
+ workspace_id VARCHAR(255) NOT NULL,
91
+ project_id VARCHAR(255) NOT NULL,
92
+ room_id VARCHAR(255) NOT NULL,
93
+ author JSONB NOT NULL,
94
+ content JSONB NOT NULL,
95
+ mentions JSONB NOT NULL DEFAULT '[]'::jsonb,
96
+ reply_to_message_id VARCHAR(255),
97
+ source VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_room_messages_source
98
+ CHECK (source IN ('user', 'agent', 'task', 'routine', 'system')),
99
+ source_id VARCHAR(255),
100
+ idempotency_key VARCHAR(255),
101
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
102
+ PRIMARY KEY (tenant_id, id)
103
+ )
104
+ `);
105
+ await client.query(`
106
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_project_room_messages_idempotency
107
+ ON lattice_project_room_messages (tenant_id, room_id, idempotency_key)
108
+ WHERE idempotency_key IS NOT NULL
109
+ `);
110
+ await client.query(`
111
+ CREATE INDEX IF NOT EXISTS idx_lattice_project_room_messages_room_created
112
+ ON lattice_project_room_messages (tenant_id, room_id, created_at DESC, id DESC)
113
+ `);
114
+ },
115
+ down: async (client) => {
116
+ await client.query("DROP INDEX IF EXISTS idx_lattice_project_room_messages_room_created");
117
+ await client.query("DROP INDEX IF EXISTS uq_lattice_project_room_messages_idempotency");
118
+ await client.query("DROP TABLE IF EXISTS lattice_project_room_messages");
119
+ await client.query("DROP INDEX IF EXISTS idx_lattice_project_bot_memberships_room");
120
+ await client.query("DROP INDEX IF EXISTS idx_lattice_project_bot_memberships_project");
121
+ await client.query("DROP INDEX IF EXISTS uq_lattice_project_bot_memberships_mention");
122
+ await client.query("DROP INDEX IF EXISTS uq_lattice_project_bot_memberships_coordinator");
123
+ await client.query("DROP TABLE IF EXISTS lattice_project_bot_memberships");
124
+ await client.query("DROP INDEX IF EXISTS idx_lattice_project_memberships_project");
125
+ await client.query("DROP TABLE IF EXISTS lattice_project_memberships");
126
+ await client.query("DROP TABLE IF EXISTS lattice_project_rooms");
127
+ },
128
+ };