@axiom-lattice/pg-stores 3.1.1 → 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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +9 -0
- package/dist/index.d.mts +195 -15
- package/dist/index.d.ts +195 -15
- package/dist/index.js +1309 -199
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1295 -191
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/ChannelBindingStore.test.ts +122 -0
- package/src/__tests__/PostgreSQLChannelInstallationStore.test.ts +16 -0
- package/src/__tests__/PostgreSQLProjectBotMembershipStore.test.ts +117 -0
- package/src/__tests__/PostgreSQLProjectMembershipStore.test.ts +235 -0
- package/src/__tests__/PostgreSQLProjectRoomMessageStore.test.ts +103 -0
- package/src/__tests__/PostgreSQLProjectRoomStore.migrations.test.ts +114 -0
- package/src/__tests__/PostgreSQLProjectRoomStore.test.ts +48 -0
- package/src/__tests__/PostgreSQLTaskStore.integration.test.ts +27 -0
- package/src/__tests__/PostgreSQLTaskStore.test.ts +57 -0
- package/src/__tests__/PostgreSQLTaskWorkItemStore.integration.test.ts +74 -0
- package/src/__tests__/PostgreSQLTaskWorkItemStore.migrations.test.ts +62 -0
- package/src/__tests__/PostgreSQLTaskWorkItemStore.test.ts +96 -0
- package/src/__tests__/ThreadMessageQueueStore.migrations.test.ts +63 -0
- package/src/__tests__/ThreadMessageQueueStore.test.ts +113 -0
- package/src/__tests__/migration-name-version-compatibility.test.ts +67 -0
- package/src/__tests__/task-files.test.ts +4 -3
- package/src/createPgStoreConfig.ts +14 -0
- package/src/index.ts +7 -0
- package/src/migrations/add_trusted_run_context_column.ts +18 -0
- package/src/migrations/migration.ts +2 -1
- package/src/migrations/project_room_migration.ts +128 -0
- package/src/migrations/task_migration.ts +15 -0
- package/src/migrations/task_work_items_migration.ts +29 -1
- package/src/stores/ChannelBindingStore.ts +99 -59
- package/src/stores/PostgreSQLChannelInstallationStore.ts +12 -30
- package/src/stores/PostgreSQLProjectBotMembershipStore.ts +199 -0
- package/src/stores/PostgreSQLProjectMembershipStore.ts +140 -0
- package/src/stores/PostgreSQLProjectRoomMessageStore.ts +177 -0
- package/src/stores/PostgreSQLProjectRoomStore.ts +57 -0
- package/src/stores/PostgreSQLTaskStore.ts +89 -3
- package/src/stores/PostgreSQLTaskWorkItemStore.ts +159 -8
- package/src/stores/ThreadMessageQueueStore.ts +38 -9
|
@@ -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,75 @@ 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
|
+
);
|
|
37
114
|
it('atomically rejects enqueue after the scoped pending capacity is reached', async () => {
|
|
38
115
|
const client = {
|
|
39
116
|
query: jest.fn()
|
|
@@ -285,6 +362,32 @@ describe('ThreadMessageQueueStore', () => {
|
|
|
285
362
|
expect((result[0].content as { command?: unknown }).command).toBeUndefined();
|
|
286
363
|
});
|
|
287
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
|
+
|
|
288
391
|
it('matches nullish dimensions as NULL when a scope is supplied', async () => {
|
|
289
392
|
mockQuery.mockResolvedValueOnce({ rows: [] });
|
|
290
393
|
|
|
@@ -419,6 +522,16 @@ describe('ThreadMessageQueueStore', () => {
|
|
|
419
522
|
['msg-1']
|
|
420
523
|
);
|
|
421
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
|
+
});
|
|
422
535
|
});
|
|
423
536
|
|
|
424
537
|
describe('clearMessages', () => {
|
|
@@ -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
|
|
17
|
-
const
|
|
18
|
-
|
|
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";
|
|
@@ -76,10 +77,16 @@ import {
|
|
|
76
77
|
addWorkItemProjectFieldsMigration,
|
|
77
78
|
addTaskWorkItemEventKeyMigration,
|
|
78
79
|
addTaskWorkItemPendingIndexesMigration,
|
|
80
|
+
addProjectLifecycleEventIndex,
|
|
79
81
|
} from "./migrations/task_work_items_migration";
|
|
80
82
|
import { createAgentWebAppsTable } from "./migrations/agent_web_apps_migration";
|
|
81
83
|
import { createCapabilityBundlesTable } from "./migrations/capability_bundle_migration";
|
|
84
|
+
import { createProjectRoomTables } from "./migrations/project_room_migration";
|
|
82
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";
|
|
83
90
|
|
|
84
91
|
export async function createPgStoreConfig(connectionString: string) {
|
|
85
92
|
const pool = new Pool({ connectionString });
|
|
@@ -143,6 +150,9 @@ export async function createPgStoreConfig(connectionString: string) {
|
|
|
143
150
|
mm.register(createAgentWebAppsTable); // v169
|
|
144
151
|
mm.register(createCapabilityBundlesTable); // v170
|
|
145
152
|
mm.register(addTaskWorkItemPendingIndexesMigration); // v171
|
|
153
|
+
mm.register(createProjectRoomTables); // v172
|
|
154
|
+
mm.register(addTrustedRunContextColumn); // v173
|
|
155
|
+
mm.register(addProjectLifecycleEventIndex); // v175
|
|
146
156
|
|
|
147
157
|
await mm.migrate();
|
|
148
158
|
|
|
@@ -181,6 +191,10 @@ export async function createPgStoreConfig(connectionString: string) {
|
|
|
181
191
|
menu: new MenuStore(opts),
|
|
182
192
|
sharedResource: new PostgresSharedResourceStore(opts),
|
|
183
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),
|
|
184
198
|
vectorStoreProvider: new PGVectorStoreProvider(pool, connectionString),
|
|
185
199
|
checkpoint,
|
|
186
200
|
};
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,7 @@ 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";
|
|
61
62
|
export * from "./stores/PostgreSQLCapabilityBundleStore";
|
|
62
63
|
export * from "./migrations/capability_bundle_migration";
|
|
63
64
|
export * from "./stores/PostgreSQLWorkflowTrackingStore";
|
|
@@ -70,6 +71,11 @@ export * from "./stores/MenuStore";
|
|
|
70
71
|
export * from "./stores/SharedResourceStore";
|
|
71
72
|
export * from "./stores/PostgresSharedResourceStore";
|
|
72
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";
|
|
73
79
|
|
|
74
80
|
export * from "./PGVectorStoreProvider";
|
|
75
81
|
|
|
@@ -127,6 +133,7 @@ export type {
|
|
|
127
133
|
CreateMenuItemInput,
|
|
128
134
|
ChannelInstallationStore,
|
|
129
135
|
ChannelInstallation,
|
|
136
|
+
CreateChannelInstallationInput,
|
|
130
137
|
CreateChannelInstallationRequest,
|
|
131
138
|
UpdateChannelInstallationRequest,
|
|
132
139
|
LarkChannelInstallationConfig,
|
|
@@ -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
|
+
};
|
|
@@ -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
|
|
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
|
+
};
|
|
@@ -98,10 +98,25 @@ export const addFilesToTasks: Migration = {
|
|
|
98
98
|
},
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
+
/** Add JSONB containment support for dependency recovery queries. */
|
|
102
|
+
export const addTaskDependenciesGinIndex: Migration = {
|
|
103
|
+
version: 174,
|
|
104
|
+
name: "add_task_dependencies_gin_index",
|
|
105
|
+
up: async (client) => {
|
|
106
|
+
await client.query(`CREATE INDEX IF NOT EXISTS idx_lattice_tasks_dependencies_gin
|
|
107
|
+
ON lattice_tasks USING GIN (dependencies jsonb_path_ops)
|
|
108
|
+
WHERE dependencies IS NOT NULL`);
|
|
109
|
+
},
|
|
110
|
+
down: async (client) => {
|
|
111
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_tasks_dependencies_gin");
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
101
115
|
/** All task migrations in version order */
|
|
102
116
|
export const taskMigrations: Migration[] = [
|
|
103
117
|
createTasksTable,
|
|
104
118
|
addTaskFieldsMigration,
|
|
105
119
|
addTaskProjectFieldsMigration,
|
|
106
120
|
addFilesToTasks,
|
|
121
|
+
addTaskDependenciesGinIndex,
|
|
107
122
|
];
|
|
@@ -16,7 +16,7 @@ export const createTaskWorkItemsMigration: Migration = {
|
|
|
16
16
|
summary TEXT,
|
|
17
17
|
detail JSONB,
|
|
18
18
|
attempt INTEGER,
|
|
19
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT
|
|
19
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT date_trunc('milliseconds', clock_timestamp()),
|
|
20
20
|
PRIMARY KEY (tenant_id, id)
|
|
21
21
|
)
|
|
22
22
|
`);
|
|
@@ -80,3 +80,31 @@ export const addTaskWorkItemPendingIndexesMigration: Migration = {
|
|
|
80
80
|
`);
|
|
81
81
|
},
|
|
82
82
|
};
|
|
83
|
+
|
|
84
|
+
/** Add deterministic project lifecycle event scan support. */
|
|
85
|
+
export const addProjectLifecycleEventIndex: Migration = {
|
|
86
|
+
version: 175,
|
|
87
|
+
name: "add_project_lifecycle_event_index",
|
|
88
|
+
up: async (client) => {
|
|
89
|
+
await client.query(`UPDATE lattice_task_work_items
|
|
90
|
+
SET created_at = date_trunc('milliseconds', created_at)
|
|
91
|
+
WHERE created_at <> date_trunc('milliseconds', created_at)`);
|
|
92
|
+
await client.query(`ALTER TABLE lattice_task_work_items
|
|
93
|
+
ALTER COLUMN created_at SET DEFAULT date_trunc('milliseconds', clock_timestamp())`);
|
|
94
|
+
await client.query(`CREATE INDEX IF NOT EXISTS idx_task_work_items_project_lifecycle
|
|
95
|
+
ON lattice_task_work_items (tenant_id, workspace_id, project_id, created_at DESC, id DESC)
|
|
96
|
+
WHERE event_key IS NOT NULL`);
|
|
97
|
+
},
|
|
98
|
+
down: async (client) => {
|
|
99
|
+
await client.query("DROP INDEX IF EXISTS idx_task_work_items_project_lifecycle");
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Complete standalone TaskWorkItem migration chain in dependency order. */
|
|
104
|
+
export const taskWorkItemMigrations: Migration[] = [
|
|
105
|
+
createTaskWorkItemsMigration,
|
|
106
|
+
addWorkItemProjectFieldsMigration,
|
|
107
|
+
addTaskWorkItemEventKeyMigration,
|
|
108
|
+
addTaskWorkItemPendingIndexesMigration,
|
|
109
|
+
addProjectLifecycleEventIndex,
|
|
110
|
+
];
|