@axiom-lattice/pg-stores 1.0.71 → 1.0.72

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.
@@ -132,6 +132,32 @@ export class PostgreSQLChannelInstallationStore
132
132
  return result.rows.map((row) => this.mapRowToInstallation(row));
133
133
  }
134
134
 
135
+ async getAllInstallations(
136
+ channel?: ChannelInstallationType,
137
+ ): Promise<ChannelInstallation[]> {
138
+ await this.ensureInitialized();
139
+
140
+ const result = channel
141
+ ? await this.pool.query<ChannelInstallationRow>(
142
+ `
143
+ SELECT id, tenant_id, channel, name, config, enabled, fallback_agent_id, reject_when_no_binding, created_at, updated_at
144
+ FROM lattice_channel_installations
145
+ WHERE channel = $1
146
+ ORDER BY created_at DESC
147
+ `,
148
+ [channel],
149
+ )
150
+ : await this.pool.query<ChannelInstallationRow>(
151
+ `
152
+ SELECT id, tenant_id, channel, name, config, enabled, fallback_agent_id, reject_when_no_binding, created_at, updated_at
153
+ FROM lattice_channel_installations
154
+ ORDER BY created_at DESC
155
+ `,
156
+ );
157
+
158
+ return result.rows.map((row) => this.mapRowToInstallation(row));
159
+ }
160
+
135
161
  async createInstallation(
136
162
  tenantId: string,
137
163
  installationId: string,
@@ -0,0 +1,294 @@
1
+ /**
2
+ * PostgreSQL implementation of TaskStore
3
+ */
4
+
5
+ import { Pool } from "pg";
6
+ import type { PoolConfig } from "pg";
7
+ import type {
8
+ TaskStore,
9
+ TaskItem,
10
+ CreateTaskRequest,
11
+ UpdateTaskRequest,
12
+ TaskListFilter,
13
+ } from "@axiom-lattice/protocols";
14
+ import { MigrationManager } from "../migrations/migration";
15
+ import { createTasksTable } from "../migrations/task_migration";
16
+ import { v4 as uuidv4 } from "uuid";
17
+
18
+ interface TaskRow {
19
+ id: string;
20
+ tenant_id: string;
21
+ owner_type: string;
22
+ owner_id: string;
23
+ title: string;
24
+ description: string | null;
25
+ status: string;
26
+ priority: string;
27
+ due_date: string | null;
28
+ metadata: Record<string, unknown> | null;
29
+ parent_id: string | null;
30
+ source_id: string | null;
31
+ context: Record<string, unknown> | null;
32
+ created_at: string;
33
+ updated_at: string;
34
+ }
35
+
36
+ function mapRowToTask(row: TaskRow): TaskItem {
37
+ return {
38
+ id: row.id,
39
+ tenantId: row.tenant_id,
40
+ ownerType: row.owner_type as "user" | "agent",
41
+ ownerId: row.owner_id,
42
+ title: row.title,
43
+ description: row.description ?? undefined,
44
+ status: row.status as TaskItem["status"],
45
+ priority: row.priority as TaskItem["priority"],
46
+ dueDate: row.due_date ?? undefined,
47
+ metadata: row.metadata ?? undefined,
48
+ parentId: row.parent_id ?? undefined,
49
+ sourceId: row.source_id ?? undefined,
50
+ context: row.context ?? undefined,
51
+ createdAt: new Date(row.created_at),
52
+ updatedAt: new Date(row.updated_at),
53
+ };
54
+ }
55
+
56
+ export interface PostgreSQLTaskStoreOptions {
57
+ poolConfig: string | PoolConfig;
58
+ autoMigrate?: boolean;
59
+ }
60
+
61
+ export class PostgreSQLTaskStore implements TaskStore {
62
+ private pool: Pool;
63
+ private migrationManager: MigrationManager;
64
+ private initialized: boolean = false;
65
+ private ownsPool: boolean = true;
66
+ private initPromise: Promise<void> | null = null;
67
+
68
+ constructor(options: PostgreSQLTaskStoreOptions) {
69
+ if (typeof options.poolConfig === "string") {
70
+ this.pool = new Pool({ connectionString: options.poolConfig });
71
+ } else {
72
+ this.pool = new Pool(options.poolConfig);
73
+ }
74
+
75
+ this.migrationManager = new MigrationManager(this.pool);
76
+ this.migrationManager.register(createTasksTable);
77
+
78
+ if (options.autoMigrate !== false) {
79
+ this.initialize().catch((error) => {
80
+ console.error("Failed to initialize PostgreSQLTaskStore:", error);
81
+ throw error;
82
+ });
83
+ }
84
+ }
85
+
86
+ async initialize(): Promise<void> {
87
+ if (this.initialized) {
88
+ return;
89
+ }
90
+
91
+ if (this.initPromise) {
92
+ return this.initPromise;
93
+ }
94
+
95
+ this.initPromise = (async () => {
96
+ try {
97
+ await this.migrationManager.migrate();
98
+ this.initialized = true;
99
+ } finally {
100
+ this.initPromise = null;
101
+ }
102
+ })();
103
+
104
+ return this.initPromise;
105
+ }
106
+
107
+ async create(
108
+ params: CreateTaskRequest & { tenantId: string; ownerType: string; ownerId: string },
109
+ ): Promise<TaskItem> {
110
+ await this.ensureInitialized();
111
+ const id = uuidv4();
112
+ const now = new Date().toISOString();
113
+
114
+ await this.pool.query(
115
+ `INSERT INTO lattice_tasks (id, tenant_id, owner_type, owner_id, title, description, status, priority, due_date, metadata, parent_id, source_id, context, created_at, updated_at)
116
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)`,
117
+ [
118
+ id,
119
+ params.tenantId,
120
+ params.ownerType,
121
+ params.ownerId,
122
+ params.title,
123
+ params.description || null,
124
+ params.status || "pending",
125
+ params.priority || "medium",
126
+ params.dueDate || null,
127
+ params.metadata ? JSON.stringify(params.metadata) : null,
128
+ params.parentId || null,
129
+ params.sourceId || null,
130
+ params.context ? JSON.stringify(params.context) : null,
131
+ now,
132
+ now,
133
+ ],
134
+ );
135
+
136
+ return (await this.getById(params.tenantId, id))!;
137
+ }
138
+
139
+ async getById(tenantId: string, id: string): Promise<TaskItem | null> {
140
+ await this.ensureInitialized();
141
+ const result = await this.pool.query<TaskRow>(
142
+ `SELECT * FROM lattice_tasks WHERE tenant_id = $1 AND id = $2`,
143
+ [tenantId, id],
144
+ );
145
+ if (result.rows.length === 0) return null;
146
+ return mapRowToTask(result.rows[0]);
147
+ }
148
+
149
+ async list(filter: TaskListFilter): Promise<TaskItem[]> {
150
+ await this.ensureInitialized();
151
+ const conditions: string[] = [`tenant_id = $1`];
152
+ const params: unknown[] = [filter.tenantId];
153
+ let paramIndex = 2;
154
+
155
+ if (filter.ownerType) {
156
+ conditions.push(`owner_type = $${paramIndex++}`);
157
+ params.push(filter.ownerType);
158
+ }
159
+ if (filter.ownerId) {
160
+ conditions.push(`owner_id = $${paramIndex++}`);
161
+ params.push(filter.ownerId);
162
+ }
163
+ if (filter.status) {
164
+ conditions.push(`status = $${paramIndex++}`);
165
+ params.push(filter.status);
166
+ }
167
+ if (filter.priority) {
168
+ conditions.push(`priority = $${paramIndex++}`);
169
+ params.push(filter.priority);
170
+ }
171
+ if (filter.parentId) {
172
+ conditions.push(`parent_id = $${paramIndex++}`);
173
+ params.push(filter.parentId);
174
+ }
175
+ if (filter.sourceId) {
176
+ conditions.push(`source_id = $${paramIndex++}`);
177
+ params.push(filter.sourceId);
178
+ }
179
+ if (filter.metadata) {
180
+ for (const [key, value] of Object.entries(filter.metadata)) {
181
+ const safeKey = key.replace(/[^a-zA-Z0-9_]/g, '');
182
+ if (safeKey) {
183
+ conditions.push(`metadata->>'${safeKey}' = $${paramIndex++}`);
184
+ params.push(String(value));
185
+ }
186
+ }
187
+ }
188
+
189
+ const where = conditions.join(" AND ");
190
+ const limit = filter.limit || 100;
191
+ const offset = filter.offset || 0;
192
+
193
+ const result = await this.pool.query<TaskRow>(
194
+ `SELECT * FROM lattice_tasks WHERE ${where} ORDER BY created_at DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
195
+ [...params, limit, offset],
196
+ );
197
+ return result.rows.map((r) => mapRowToTask(r));
198
+ }
199
+
200
+ async update(
201
+ tenantId: string,
202
+ id: string,
203
+ updates: UpdateTaskRequest,
204
+ ): Promise<TaskItem | null> {
205
+ await this.ensureInitialized();
206
+ const existing = await this.getById(tenantId, id);
207
+ if (!existing) return null;
208
+
209
+ const setClauses: string[] = [];
210
+ const params: unknown[] = [];
211
+ let paramIndex = 1;
212
+
213
+ if (updates.title !== undefined) {
214
+ setClauses.push(`title = $${paramIndex++}`);
215
+ params.push(updates.title);
216
+ }
217
+ if (updates.description !== undefined) {
218
+ setClauses.push(`description = $${paramIndex++}`);
219
+ params.push(updates.description);
220
+ }
221
+ if (updates.status !== undefined) {
222
+ setClauses.push(`status = $${paramIndex++}`);
223
+ params.push(updates.status);
224
+ }
225
+ if (updates.priority !== undefined) {
226
+ setClauses.push(`priority = $${paramIndex++}`);
227
+ params.push(updates.priority);
228
+ }
229
+ if (updates.dueDate !== undefined) {
230
+ setClauses.push(`due_date = $${paramIndex++}`);
231
+ params.push(updates.dueDate);
232
+ }
233
+ if (updates.metadata !== undefined) {
234
+ setClauses.push(`metadata = $${paramIndex++}`);
235
+ params.push(JSON.stringify(updates.metadata));
236
+ }
237
+ if (updates.parentId !== undefined) {
238
+ setClauses.push(`parent_id = $${paramIndex++}`);
239
+ params.push(updates.parentId);
240
+ }
241
+ if (updates.sourceId !== undefined) {
242
+ setClauses.push(`source_id = $${paramIndex++}`);
243
+ params.push(updates.sourceId);
244
+ }
245
+ if (updates.context !== undefined) {
246
+ setClauses.push(`context = $${paramIndex++}`);
247
+ params.push(JSON.stringify(updates.context));
248
+ }
249
+ if (updates.ownerType !== undefined) {
250
+ setClauses.push(`owner_type = $${paramIndex++}`);
251
+ params.push(updates.ownerType);
252
+ }
253
+ if (updates.ownerId !== undefined) {
254
+ setClauses.push(`owner_id = $${paramIndex++}`);
255
+ params.push(updates.ownerId);
256
+ }
257
+
258
+ if (setClauses.length === 0) {
259
+ return existing;
260
+ }
261
+
262
+ setClauses.push(`updated_at = $${paramIndex++}`);
263
+ params.push(new Date().toISOString());
264
+
265
+ params.push(tenantId, id);
266
+ await this.pool.query(
267
+ `UPDATE lattice_tasks SET ${setClauses.join(", ")} WHERE tenant_id = $${paramIndex++} AND id = $${paramIndex++}`,
268
+ params,
269
+ );
270
+
271
+ return this.getById(tenantId, id);
272
+ }
273
+
274
+ async delete(tenantId: string, id: string): Promise<boolean> {
275
+ await this.ensureInitialized();
276
+ const result = await this.pool.query(
277
+ `DELETE FROM lattice_tasks WHERE tenant_id = $1 AND id = $2`,
278
+ [tenantId, id],
279
+ );
280
+ return (result.rowCount ?? 0) > 0;
281
+ }
282
+
283
+ async dispose(): Promise<void> {
284
+ if (this.ownsPool && this.pool) {
285
+ await this.pool.end();
286
+ }
287
+ }
288
+
289
+ private async ensureInitialized(): Promise<void> {
290
+ if (!this.initialized) {
291
+ await this.initialize();
292
+ }
293
+ }
294
+ }
@@ -16,7 +16,7 @@ import {
16
16
  WorkflowRunStatus,
17
17
  } from "@axiom-lattice/protocols";
18
18
  import { MigrationManager } from "../migrations/migration";
19
- import { createWorkflowTrackingTables } from "../migrations/workflow_tracking_migrations";
19
+ import { createWorkflowTrackingTables, addStepThreadId } from "../migrations/workflow_tracking_migrations";
20
20
 
21
21
  export interface PostgreSQLWorkflowTrackingStoreOptions {
22
22
  poolConfig: string | PoolConfig;
@@ -38,6 +38,7 @@ export class PostgreSQLWorkflowTrackingStore implements WorkflowTrackingStore {
38
38
 
39
39
  this.migrationManager = new MigrationManager(this.pool);
40
40
  this.migrationManager.register(createWorkflowTrackingTables);
41
+ this.migrationManager.register(addStepThreadId);
41
42
 
42
43
  if (options.autoMigrate !== false) {
43
44
  this.initialize().catch((error) => {
@@ -171,17 +172,19 @@ export class PostgreSQLWorkflowTrackingStore implements WorkflowTrackingStore {
171
172
  const id = `step_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
172
173
 
173
174
  await this.pool.query(
174
- `INSERT INTO lattice_workflow_steps (id, run_id, tenant_id, step_type, step_name, edge_from, edge_to, edge_purpose, input, status, started_at, created_at, updated_at)
175
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
175
+ `INSERT INTO lattice_workflow_steps (id, run_id, tenant_id, step_type, step_name, edge_from, edge_to, edge_purpose, input, thread_id, status, started_at, created_at, updated_at)
176
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`,
176
177
  [id, request.runId, request.tenantId, request.stepType, request.stepName,
177
178
  request.edgeFrom || null, request.edgeTo || null, request.edgePurpose || null,
178
179
  request.input ? JSON.stringify(request.input) : null,
180
+ request.threadId || null,
179
181
  'running', now, now, now]
180
182
  );
181
183
 
182
184
  return {
183
185
  id, runId: request.runId, tenantId: request.tenantId,
184
186
  stepType: request.stepType, stepName: request.stepName,
187
+ threadId: request.threadId,
185
188
  edgeFrom: request.edgeFrom, edgeTo: request.edgeTo, edgePurpose: request.edgePurpose,
186
189
  input: request.input, status: 'running', startedAt: now,
187
190
  createdAt: now, updatedAt: now,
@@ -196,7 +199,16 @@ export class PostgreSQLWorkflowTrackingStore implements WorkflowTrackingStore {
196
199
  [request.runId, request.stepType, request.stepName]
197
200
  );
198
201
  if (result.rows.length > 0) {
199
- return this.mapRowToRunStep(result.rows[0]);
202
+ const existing = this.mapRowToRunStep(result.rows[0]);
203
+ // Update threadId if not already set and request has one
204
+ if (!existing.threadId && request.threadId) {
205
+ await this.pool.query(
206
+ `UPDATE lattice_workflow_steps SET thread_id = $1, updated_at = $2 WHERE run_id = $3 AND id = $4`,
207
+ [request.threadId, new Date(), request.runId, existing.id]
208
+ );
209
+ existing.threadId = request.threadId;
210
+ }
211
+ return existing;
200
212
  }
201
213
  return this.createRunStep(request);
202
214
  }
@@ -294,6 +306,7 @@ export class PostgreSQLWorkflowTrackingStore implements WorkflowTrackingStore {
294
306
  tenantId: row.tenant_id,
295
307
  stepType: row.step_type as StepType,
296
308
  stepName: row.step_name,
309
+ threadId: row.thread_id,
297
310
  edgeFrom: row.edge_from,
298
311
  edgeTo: row.edge_to,
299
312
  edgePurpose: row.edge_purpose,
@@ -17,6 +17,7 @@ import { createThreadMessageQueueTable } from "../migrations/thread_message_queu
17
17
  import { addPriorityAndCommandColumns } from "../migrations/add_priority_command_columns";
18
18
  import { addCustomRunConfigColumn } from "../migrations/add_custom_run_config_column";
19
19
  import { alterMessageQueueIdColumn } from "../migrations/alter_message_queue_id_column";
20
+ import { addWorkspaceProjectToQueue } from "../migrations/add_workspace_project_to_queue";
20
21
 
21
22
  export type { PendingMessage, AddMessageParams, ThreadInfo };
22
23
 
@@ -45,6 +46,7 @@ export class ThreadMessageQueueStore implements IMessageQueueStore {
45
46
  this.migrationManager.register(addPriorityAndCommandColumns);
46
47
  this.migrationManager.register(addCustomRunConfigColumn);
47
48
  this.migrationManager.register(alterMessageQueueIdColumn);
49
+ this.migrationManager.register(addWorkspaceProjectToQueue);
48
50
 
49
51
  if (options.autoMigrate !== false) {
50
52
  this.initialize().catch((error) => {
@@ -79,7 +81,7 @@ export class ThreadMessageQueueStore implements IMessageQueueStore {
79
81
  * Add message to queue
80
82
  */
81
83
  async addMessage(params: AddMessageParams): Promise<PendingMessage> {
82
- const { threadId, tenantId, assistantId, content, type = "human", priority = 0, command, custom_run_config, id } = params;
84
+ const { threadId, tenantId, assistantId, workspaceId, projectId, content, type = "human", priority = 0, command, custom_run_config, id } = params;
83
85
 
84
86
  // Get current max sequence (no lock needed without unique constraint)
85
87
  const seqResult = await this.pool.query(
@@ -92,10 +94,10 @@ export class ThreadMessageQueueStore implements IMessageQueueStore {
92
94
 
93
95
  const result = await this.pool.query(
94
96
  `INSERT INTO lattice_thread_message_queue
95
- (id, thread_id, tenant_id, assistant_id, message_content, message_type, sequence_order, priority, command, custom_run_config)
96
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
97
+ (id, thread_id, tenant_id, assistant_id, workspace_id, project_id, message_content, message_type, sequence_order, priority, command, custom_run_config)
98
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
97
99
  RETURNING *`,
98
- [id || crypto.randomUUID(), threadId, tenantId, assistantId, JSON.stringify(content), type, nextSeq, priority, command ? JSON.stringify(command) : null, custom_run_config ? JSON.stringify(custom_run_config) : null]
100
+ [id || crypto.randomUUID(), threadId, tenantId, assistantId, workspaceId || null, projectId || null, JSON.stringify(content), type, nextSeq, priority, command ? JSON.stringify(command) : null, custom_run_config ? JSON.stringify(custom_run_config) : null]
99
101
  );
100
102
 
101
103
  return this.rowToMessage(result.rows[0]);
@@ -106,7 +108,7 @@ export class ThreadMessageQueueStore implements IMessageQueueStore {
106
108
  * Uses priority=100 to ensure message is processed first
107
109
  */
108
110
  async addMessageAtHead(params: AddMessageParams): Promise<PendingMessage> {
109
- const { threadId, tenantId, assistantId, content, type = "human", command, custom_run_config, id } = params;
111
+ const { threadId, tenantId, assistantId, workspaceId, projectId, content, type = "human", command, custom_run_config, id } = params;
110
112
 
111
113
  const resolvedTenantId = tenantId!;
112
114
  const resolvedAssistantId = assistantId!;
@@ -123,10 +125,10 @@ export class ThreadMessageQueueStore implements IMessageQueueStore {
123
125
  // Insert with high priority (100)
124
126
  const result = await this.pool.query(
125
127
  `INSERT INTO lattice_thread_message_queue
126
- (id, thread_id, tenant_id, assistant_id, message_content, message_type, sequence_order, priority, command, custom_run_config)
127
- VALUES ($1, $2, $3, $4, $5, $6, $7, 100, $8, $9)
128
+ (id, thread_id, tenant_id, assistant_id, workspace_id, project_id, message_content, message_type, sequence_order, priority, command, custom_run_config)
129
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 100, $10, $11)
128
130
  RETURNING *`,
129
- [id || crypto.randomUUID(), threadId, resolvedTenantId, resolvedAssistantId, JSON.stringify(content), type, nextSeq, command ? JSON.stringify(command) : null, custom_run_config ? JSON.stringify(custom_run_config) : null]
131
+ [id || crypto.randomUUID(), threadId, resolvedTenantId, resolvedAssistantId, workspaceId || null, projectId || null, JSON.stringify(content), type, nextSeq, command ? JSON.stringify(command) : null, custom_run_config ? JSON.stringify(custom_run_config) : null]
130
132
  );
131
133
 
132
134
  return this.rowToMessage(result.rows[0]);
@@ -178,7 +180,7 @@ export class ThreadMessageQueueStore implements IMessageQueueStore {
178
180
  */
179
181
  async getThreadsWithPendingMessages(): Promise<ThreadInfo[]> {
180
182
  const result = await this.pool.query(
181
- `SELECT DISTINCT tenant_id, assistant_id, thread_id
183
+ `SELECT DISTINCT ON (thread_id) tenant_id, assistant_id, thread_id, workspace_id, project_id
182
184
  FROM lattice_thread_message_queue
183
185
  WHERE status IN ('pending', 'processing')
184
186
  ORDER BY thread_id`
@@ -188,6 +190,8 @@ export class ThreadMessageQueueStore implements IMessageQueueStore {
188
190
  tenantId: row.tenant_id,
189
191
  assistantId: row.assistant_id,
190
192
  threadId: row.thread_id,
193
+ workspaceId: row.workspace_id || undefined,
194
+ projectId: row.project_id || undefined,
191
195
  }));
192
196
  }
193
197