@axiom-lattice/local-stores 3.0.2 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +21 -0
- package/dist/index.d.mts +53 -10
- package/dist/index.d.ts +53 -10
- package/dist/index.js +323 -45
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +322 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/LocalCapabilityBundleStore.test.ts +217 -0
- package/src/__tests__/LocalEvalStore.test.ts +41 -2
- package/src/__tests__/LocalProjectStore.test.ts +39 -2
- package/src/__tests__/LocalTaskWorkItemStore.test.ts +107 -0
- package/src/__tests__/LocalThreadMessageQueueStore.test.ts +78 -0
- package/src/createLocalStoreConfig.ts +2 -0
- package/src/index.ts +1 -0
- package/src/stores/LocalCapabilityBundleStore.ts +53 -0
- package/src/stores/LocalEvalStore.ts +28 -2
- package/src/stores/LocalProjectStore.ts +67 -14
- package/src/stores/LocalTaskWorkItemStore.ts +45 -0
- package/src/stores/LocalThreadMessageQueueStore.ts +86 -24
|
@@ -264,8 +264,34 @@ export class LocalEvalStore implements EvalStore {
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
async deleteProject(tenantId: string, id: string): Promise<boolean> {
|
|
267
|
-
const
|
|
268
|
-
|
|
267
|
+
const db = this.db.getRawDb();
|
|
268
|
+
db.run("BEGIN TRANSACTION");
|
|
269
|
+
try {
|
|
270
|
+
db.run(
|
|
271
|
+
`DELETE FROM lt_eval_run_results
|
|
272
|
+
WHERE run_id IN (
|
|
273
|
+
SELECT id FROM lt_eval_runs WHERE tenant_id = ? AND project_id = ?
|
|
274
|
+
)`,
|
|
275
|
+
[tenantId, id]
|
|
276
|
+
);
|
|
277
|
+
db.run(
|
|
278
|
+
`DELETE FROM lt_eval_cases
|
|
279
|
+
WHERE tenant_id = ? AND suite_id IN (
|
|
280
|
+
SELECT id FROM lt_eval_suites WHERE tenant_id = ? AND project_id = ?
|
|
281
|
+
)`,
|
|
282
|
+
[tenantId, tenantId, id]
|
|
283
|
+
);
|
|
284
|
+
db.run(`DELETE FROM lt_eval_runs WHERE tenant_id = ? AND project_id = ?`, [tenantId, id]);
|
|
285
|
+
db.run(`DELETE FROM lt_eval_suites WHERE tenant_id = ? AND project_id = ?`, [tenantId, id]);
|
|
286
|
+
db.run(`DELETE FROM lt_eval_projects WHERE tenant_id = ? AND id = ?`, [tenantId, id]);
|
|
287
|
+
const deleted = db.getRowsModified() > 0;
|
|
288
|
+
db.run("COMMIT");
|
|
289
|
+
this.db.save();
|
|
290
|
+
return deleted;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
db.run("ROLLBACK");
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
269
295
|
}
|
|
270
296
|
|
|
271
297
|
// -------------------------------------------------------------------------
|
|
@@ -9,7 +9,9 @@ import type {
|
|
|
9
9
|
ProjectFilter,
|
|
10
10
|
CreateProjectRequest,
|
|
11
11
|
UpdateProjectRequest,
|
|
12
|
+
UpdateProjectCapabilityBundlesResult,
|
|
12
13
|
} from "@axiom-lattice/protocols";
|
|
14
|
+
import { assertGenericProjectConfig } from "@axiom-lattice/protocols";
|
|
13
15
|
import { ensureTable, nowISO, parseISO } from "../database";
|
|
14
16
|
|
|
15
17
|
const DDL = `
|
|
@@ -75,6 +77,7 @@ export class LocalProjectStore implements ProjectStore {
|
|
|
75
77
|
id: string,
|
|
76
78
|
data: CreateProjectRequest,
|
|
77
79
|
): Promise<Project> {
|
|
80
|
+
assertGenericProjectConfig(data.config);
|
|
78
81
|
const now = nowISO();
|
|
79
82
|
const kind = data.kind || "business";
|
|
80
83
|
|
|
@@ -85,22 +88,16 @@ export class LocalProjectStore implements ProjectStore {
|
|
|
85
88
|
workspace_id = excluded.workspace_id,
|
|
86
89
|
name = excluded.name,
|
|
87
90
|
description = excluded.description,
|
|
88
|
-
config =
|
|
91
|
+
config = CASE
|
|
92
|
+
WHEN json_type(lt_projects.config, '$.capabilityBundleIds') IS NOT NULL
|
|
93
|
+
THEN json_set(COALESCE(excluded.config, '{}'), '$.capabilityBundleIds', json_extract(lt_projects.config, '$.capabilityBundleIds'))
|
|
94
|
+
ELSE excluded.config
|
|
95
|
+
END,
|
|
89
96
|
kind = excluded.kind,
|
|
90
97
|
updated_at = excluded.updated_at`,
|
|
91
98
|
).run(id, tenantId, workspaceId, data.name, data.description || null, data.config ? JSON.stringify(data.config) : null, kind, now, now);
|
|
92
99
|
|
|
93
|
-
return
|
|
94
|
-
id,
|
|
95
|
-
tenantId,
|
|
96
|
-
workspaceId,
|
|
97
|
-
name: data.name,
|
|
98
|
-
description: data.description,
|
|
99
|
-
config: data.config,
|
|
100
|
-
kind,
|
|
101
|
-
createdAt: parseISO(now),
|
|
102
|
-
updatedAt: parseISO(now),
|
|
103
|
-
};
|
|
100
|
+
return (await this.getProjectById(tenantId, id)) as Project;
|
|
104
101
|
}
|
|
105
102
|
|
|
106
103
|
async updateProject(
|
|
@@ -108,6 +105,7 @@ export class LocalProjectStore implements ProjectStore {
|
|
|
108
105
|
id: string,
|
|
109
106
|
updates: UpdateProjectRequest,
|
|
110
107
|
): Promise<Project | null> {
|
|
108
|
+
assertGenericProjectConfig(updates.config);
|
|
111
109
|
const existing = await this.getProjectById(tenantId, id);
|
|
112
110
|
if (!existing) return null;
|
|
113
111
|
|
|
@@ -123,8 +121,13 @@ export class LocalProjectStore implements ProjectStore {
|
|
|
123
121
|
values.push(updates.description || null);
|
|
124
122
|
}
|
|
125
123
|
if (updates.config !== undefined) {
|
|
126
|
-
setClauses.push(
|
|
127
|
-
|
|
124
|
+
setClauses.push(`config = CASE
|
|
125
|
+
WHEN json_type(config, '$.capabilityBundleIds') IS NOT NULL
|
|
126
|
+
THEN json_set(json(?), '$.capabilityBundleIds', json_extract(config, '$.capabilityBundleIds'))
|
|
127
|
+
ELSE ?
|
|
128
|
+
END`);
|
|
129
|
+
const config = JSON.stringify(updates.config ?? {});
|
|
130
|
+
values.push(config, config);
|
|
128
131
|
}
|
|
129
132
|
if (updates.kind !== undefined) {
|
|
130
133
|
setClauses.push("kind = ?");
|
|
@@ -152,6 +155,56 @@ export class LocalProjectStore implements ProjectStore {
|
|
|
152
155
|
return result.changes > 0;
|
|
153
156
|
}
|
|
154
157
|
|
|
158
|
+
async updateCapabilityBundleIds(tenantId: string, projectId: string, bundleIds: string[], expectedRevisions: Record<string, string> = {}): Promise<UpdateProjectCapabilityBundlesResult> {
|
|
159
|
+
const now = nowISO();
|
|
160
|
+
const result = this.db.prepare(
|
|
161
|
+
`UPDATE lt_projects
|
|
162
|
+
SET config = json_set(COALESCE(config, '{}'), '$.capabilityBundleIds', json(?)), updated_at = ?
|
|
163
|
+
WHERE tenant_id = ? AND id = ?
|
|
164
|
+
AND NOT EXISTS (
|
|
165
|
+
SELECT 1 FROM json_each(?) AS requested
|
|
166
|
+
WHERE NOT EXISTS (
|
|
167
|
+
SELECT 1 FROM lt_capability_bundles
|
|
168
|
+
WHERE tenant_id = ? AND id = requested.value
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
AND NOT EXISTS (
|
|
172
|
+
SELECT 1 FROM json_each(?) AS expected
|
|
173
|
+
WHERE EXISTS (
|
|
174
|
+
SELECT 1 FROM lt_capability_bundles
|
|
175
|
+
WHERE tenant_id = ? AND id = expected.key AND updated_at <> expected.value
|
|
176
|
+
)
|
|
177
|
+
)`,
|
|
178
|
+
).run(JSON.stringify(bundleIds), now, tenantId, projectId, JSON.stringify(bundleIds), tenantId, JSON.stringify(expectedRevisions), tenantId);
|
|
179
|
+
if (result.changes > 0) {
|
|
180
|
+
const project = await this.getProjectById(tenantId, projectId);
|
|
181
|
+
return project ? { status: "updated", project } : { status: "project_not_found" };
|
|
182
|
+
}
|
|
183
|
+
const revisionMismatch = this.db.prepare(
|
|
184
|
+
"SELECT 1 FROM lt_capability_bundles WHERE tenant_id = ? AND id IN (SELECT key FROM json_each(?)) AND updated_at <> (SELECT value FROM json_each(?) WHERE key = lt_capability_bundles.id) LIMIT 1",
|
|
185
|
+
).get(tenantId, JSON.stringify(expectedRevisions), JSON.stringify(expectedRevisions));
|
|
186
|
+
if (revisionMismatch) return { status: "bundle_conflict" };
|
|
187
|
+
return await this.getProjectById(tenantId, projectId)
|
|
188
|
+
? { status: "bundle_not_found" }
|
|
189
|
+
: { status: "project_not_found" };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async isCapabilityBundleReferenced(tenantId: string, bundleId: string): Promise<boolean> {
|
|
193
|
+
const rows = this.db.prepare("SELECT config FROM lt_projects WHERE tenant_id = ?").all(tenantId) as unknown as Array<{ config: string | null }>;
|
|
194
|
+
return rows.some((row) => {
|
|
195
|
+
if (!row.config) return false;
|
|
196
|
+
let config: Record<string, unknown>;
|
|
197
|
+
try {
|
|
198
|
+
config = JSON.parse(row.config) as Record<string, unknown>;
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
if (config === null || typeof config !== "object" || Array.isArray(config)) return false;
|
|
203
|
+
const ids = config.capabilityBundleIds;
|
|
204
|
+
return Array.isArray(ids) && ids.every((id) => typeof id === "string") && ids.includes(bundleId);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
155
208
|
/** Add a column if it does not exist (SQLite version compatible). */
|
|
156
209
|
private ensureColumn(table: string, column: string, ddl: string): void {
|
|
157
210
|
const cols = this.db.prepare(`PRAGMA table_info(${table})`).all() as unknown as Array<{ name: string }>;
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
CreateWorkItemIfAbsentRequest,
|
|
11
11
|
TaskWorkItemListFilter,
|
|
12
12
|
} from "@axiom-lattice/protocols";
|
|
13
|
+
import { MAX_PENDING_EXECUTION_RESULTS_LIMIT } from "@axiom-lattice/protocols";
|
|
13
14
|
import { ensureTable, nowISO, parseISO } from "../database";
|
|
14
15
|
import { v4 as uuid } from "uuid";
|
|
15
16
|
|
|
@@ -76,6 +77,11 @@ export class LocalTaskWorkItemStore implements TaskWorkItemStore {
|
|
|
76
77
|
this.ensureColumn("lt_task_work_items", "event_key", "event_key TEXT");
|
|
77
78
|
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_task_work_items_event_key
|
|
78
79
|
ON lt_task_work_items (tenant_id, task_id, event_key);`);
|
|
80
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_task_work_items_pending_order
|
|
81
|
+
ON lt_task_work_items (tenant_id, task_id, action, created_at DESC, id DESC);`);
|
|
82
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_task_work_items_reconciled_result
|
|
83
|
+
ON lt_task_work_items (tenant_id, task_id, json_extract(detail, '$.executionResultId'))
|
|
84
|
+
WHERE action = 'execution_reconciled';`);
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
/** Add a column if it does not exist (SQLite version compatible). */
|
|
@@ -154,4 +160,43 @@ export class LocalTaskWorkItemStore implements TaskWorkItemStore {
|
|
|
154
160
|
).all(...params, limit, offset) as unknown as WorkItemRow[];
|
|
155
161
|
return rows.map(mapRowToWorkItem);
|
|
156
162
|
}
|
|
163
|
+
|
|
164
|
+
/** List pending execution results using one bounded SQLite anti-join query. */
|
|
165
|
+
async listPendingExecutionResults(params: {
|
|
166
|
+
tenantId: string;
|
|
167
|
+
taskId: string;
|
|
168
|
+
limit: number;
|
|
169
|
+
}): Promise<TaskWorkItem[]> {
|
|
170
|
+
if (!Number.isSafeInteger(params.limit)
|
|
171
|
+
|| params.limit < 0
|
|
172
|
+
|| params.limit > MAX_PENDING_EXECUTION_RESULTS_LIMIT) {
|
|
173
|
+
const error = new RangeError(`limit must be a safe integer between 0 and ${MAX_PENDING_EXECUTION_RESULTS_LIMIT}`) as RangeError & {
|
|
174
|
+
code: "INVALID_LIMIT";
|
|
175
|
+
};
|
|
176
|
+
error.code = "INVALID_LIMIT";
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
if (params.limit === 0) return [];
|
|
180
|
+
const rows = this.db.prepare(
|
|
181
|
+
`SELECT result.*
|
|
182
|
+
FROM lt_task_work_items AS result
|
|
183
|
+
WHERE result.tenant_id = ?
|
|
184
|
+
AND result.task_id = ?
|
|
185
|
+
AND result.action = 'execution_result'
|
|
186
|
+
AND substr(result.event_key, 1, 17) = 'execution-result:'
|
|
187
|
+
AND length(result.event_key) > 17
|
|
188
|
+
AND result.event_key NOT GLOB '*[^A-Za-z0-9._:-]*'
|
|
189
|
+
AND NOT EXISTS (
|
|
190
|
+
SELECT 1
|
|
191
|
+
FROM lt_task_work_items AS reconciled
|
|
192
|
+
WHERE reconciled.tenant_id = result.tenant_id
|
|
193
|
+
AND reconciled.task_id = result.task_id
|
|
194
|
+
AND reconciled.action = 'execution_reconciled'
|
|
195
|
+
AND json_extract(reconciled.detail, '$.executionResultId') = result.event_key
|
|
196
|
+
)
|
|
197
|
+
ORDER BY result.created_at DESC, result.id DESC
|
|
198
|
+
LIMIT ?`,
|
|
199
|
+
).all(params.tenantId, params.taskId, params.limit) as unknown as WorkItemRow[];
|
|
200
|
+
return rows.map(mapRowToWorkItem);
|
|
201
|
+
}
|
|
157
202
|
}
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
PendingMessage,
|
|
9
9
|
AddMessageParams,
|
|
10
10
|
ThreadInfo,
|
|
11
|
+
QueueScope,
|
|
11
12
|
} from "@axiom-lattice/core";
|
|
12
13
|
import { ensureTable, nowISO, parseISO } from "../database";
|
|
13
14
|
import { randomUUID } from "crypto";
|
|
@@ -52,6 +53,7 @@ interface QueueRow {
|
|
|
52
53
|
|
|
53
54
|
export class LocalThreadMessageQueueStore implements IMessageQueueStore {
|
|
54
55
|
private db: DatabaseWrapper;
|
|
56
|
+
private capacityTail: Promise<void> = Promise.resolve();
|
|
55
57
|
|
|
56
58
|
constructor(db: DatabaseWrapper) {
|
|
57
59
|
this.db = db;
|
|
@@ -72,9 +74,9 @@ export class LocalThreadMessageQueueStore implements IMessageQueueStore {
|
|
|
72
74
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
73
75
|
).run(
|
|
74
76
|
id, params.threadId, params.tenantId, params.assistantId,
|
|
75
|
-
|
|
77
|
+
params.workspaceId ?? null, params.projectId ?? null,
|
|
76
78
|
JSON.stringify(params.content), params.type || "human", nextSeq,
|
|
77
|
-
|
|
79
|
+
params.priority ?? 0,
|
|
78
80
|
params.command ? JSON.stringify(params.command) : null,
|
|
79
81
|
params.custom_run_config ? JSON.stringify(params.custom_run_config) : null,
|
|
80
82
|
now,
|
|
@@ -83,6 +85,43 @@ export class LocalThreadMessageQueueStore implements IMessageQueueStore {
|
|
|
83
85
|
return this.getById(id);
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
async addMessageIfCapacity(params: AddMessageParams, maxSize: number): Promise<boolean> {
|
|
89
|
+
let release!: () => void;
|
|
90
|
+
const previous = this.capacityTail;
|
|
91
|
+
this.capacityTail = new Promise<void>((resolve) => { release = resolve; });
|
|
92
|
+
await previous;
|
|
93
|
+
try {
|
|
94
|
+
const scope = {
|
|
95
|
+
tenantId: params.tenantId,
|
|
96
|
+
assistantId: params.assistantId,
|
|
97
|
+
workspaceId: params.workspaceId,
|
|
98
|
+
projectId: params.projectId,
|
|
99
|
+
};
|
|
100
|
+
const filter = scopeClause(scope);
|
|
101
|
+
const id = params.id || randomUUID();
|
|
102
|
+
const limit = maxSize === Infinity ? -1 : maxSize;
|
|
103
|
+
const result = this.db.prepare(
|
|
104
|
+
`INSERT INTO lt_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, created_at)
|
|
105
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?,
|
|
106
|
+
(SELECT COALESCE(MAX(sequence_order), 0) + 1 FROM lt_thread_message_queue WHERE thread_id = ?),
|
|
107
|
+
?, ?, ?, ?
|
|
108
|
+
WHERE ? = -1 OR (SELECT COUNT(*) FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'pending'${filter.sql}) < ?`,
|
|
109
|
+
).run(
|
|
110
|
+
id, params.threadId, params.tenantId, params.assistantId,
|
|
111
|
+
params.workspaceId ?? null, params.projectId ?? null,
|
|
112
|
+
JSON.stringify(params.content), params.type || "human",
|
|
113
|
+
params.threadId,
|
|
114
|
+
params.priority ?? 0,
|
|
115
|
+
params.command ? JSON.stringify(params.command) : null,
|
|
116
|
+
params.custom_run_config ? JSON.stringify(params.custom_run_config) : null,
|
|
117
|
+
nowISO(), limit, params.threadId, ...filter.params, limit,
|
|
118
|
+
);
|
|
119
|
+
return result.changes > 0;
|
|
120
|
+
} finally {
|
|
121
|
+
release();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
86
125
|
async addMessageAtHead(params: AddMessageParams): Promise<PendingMessage> {
|
|
87
126
|
const now = nowISO();
|
|
88
127
|
const id = params.id || randomUUID();
|
|
@@ -96,7 +135,7 @@ export class LocalThreadMessageQueueStore implements IMessageQueueStore {
|
|
|
96
135
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 100, ?, ?, ?)`,
|
|
97
136
|
).run(
|
|
98
137
|
id, params.threadId, params.tenantId!, params.assistantId!,
|
|
99
|
-
params.workspaceId
|
|
138
|
+
params.workspaceId ?? null, params.projectId ?? null,
|
|
100
139
|
JSON.stringify(params.content), params.type || "human", seqRow.next_seq,
|
|
101
140
|
params.command ? JSON.stringify(params.command) : null,
|
|
102
141
|
params.custom_run_config ? JSON.stringify(params.custom_run_config) : null,
|
|
@@ -106,57 +145,70 @@ export class LocalThreadMessageQueueStore implements IMessageQueueStore {
|
|
|
106
145
|
return this.getById(id);
|
|
107
146
|
}
|
|
108
147
|
|
|
109
|
-
async getPendingMessages(threadId: string): Promise<PendingMessage[]> {
|
|
148
|
+
async getPendingMessages(threadId: string, scope?: QueueScope): Promise<PendingMessage[]> {
|
|
149
|
+
const filter = scopeClause(scope);
|
|
110
150
|
const rows = this.db.prepare(
|
|
111
|
-
`SELECT * FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'pending' ORDER BY priority DESC, sequence_order ASC`,
|
|
112
|
-
).all(threadId) as unknown as QueueRow[];
|
|
151
|
+
`SELECT * FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'pending'${filter.sql} ORDER BY priority DESC, sequence_order ASC`,
|
|
152
|
+
).all(threadId, ...filter.params) as unknown as QueueRow[];
|
|
113
153
|
return rows.map(rowToMessage);
|
|
114
154
|
}
|
|
115
155
|
|
|
116
|
-
async getProcessingMessages(threadId: string): Promise<PendingMessage[]> {
|
|
156
|
+
async getProcessingMessages(threadId: string, scope?: QueueScope): Promise<PendingMessage[]> {
|
|
157
|
+
const filter = scopeClause(scope);
|
|
117
158
|
const rows = this.db.prepare(
|
|
118
|
-
`SELECT * FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'processing' ORDER BY priority DESC, sequence_order ASC`,
|
|
119
|
-
).all(threadId) as unknown as QueueRow[];
|
|
159
|
+
`SELECT * FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'processing'${filter.sql} ORDER BY priority DESC, sequence_order ASC`,
|
|
160
|
+
).all(threadId, ...filter.params) as unknown as QueueRow[];
|
|
120
161
|
return rows.map(rowToMessage);
|
|
121
162
|
}
|
|
122
163
|
|
|
123
|
-
async getQueueSize(threadId: string): Promise<number> {
|
|
164
|
+
async getQueueSize(threadId: string, scope?: QueueScope): Promise<number> {
|
|
165
|
+
const filter = scopeClause(scope);
|
|
124
166
|
const row = this.db.prepare(
|
|
125
|
-
`SELECT COUNT(*) as count FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'pending'`,
|
|
126
|
-
).get(threadId) as { count: number };
|
|
167
|
+
`SELECT COUNT(*) as count FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'pending'${filter.sql}`,
|
|
168
|
+
).get(threadId, ...filter.params) as { count: number };
|
|
127
169
|
return row.count;
|
|
128
170
|
}
|
|
129
171
|
|
|
130
172
|
async getThreadsWithPendingMessages(): Promise<ThreadInfo[]> {
|
|
131
173
|
const rows = this.db.prepare(
|
|
132
|
-
`SELECT
|
|
174
|
+
`SELECT tenant_id, assistant_id, thread_id, workspace_id, project_id FROM lt_thread_message_queue WHERE status IN ('pending', 'processing') GROUP BY tenant_id, assistant_id, thread_id, workspace_id, project_id ORDER BY thread_id`,
|
|
133
175
|
).all() as { tenant_id: string; assistant_id: string; thread_id: string; workspace_id: string | null; project_id: string | null }[];
|
|
134
176
|
return rows.map((r) => ({
|
|
135
177
|
tenantId: r.tenant_id,
|
|
136
178
|
assistantId: r.assistant_id,
|
|
137
179
|
threadId: r.thread_id,
|
|
138
|
-
workspaceId: r.workspace_id
|
|
139
|
-
projectId: r.project_id
|
|
180
|
+
workspaceId: r.workspace_id,
|
|
181
|
+
projectId: r.project_id,
|
|
140
182
|
}));
|
|
141
183
|
}
|
|
142
184
|
|
|
143
|
-
async removeMessage(messageId: string): Promise<boolean> {
|
|
144
|
-
const
|
|
185
|
+
async removeMessage(messageId: string, scope?: QueueScope): Promise<boolean> {
|
|
186
|
+
const filter = scopeClause(scope);
|
|
187
|
+
const result = this.db.prepare(`DELETE FROM lt_thread_message_queue WHERE id = ?${filter.sql}`).run(messageId, ...filter.params);
|
|
145
188
|
return result.changes > 0;
|
|
146
189
|
}
|
|
147
190
|
|
|
148
|
-
async clearMessages(threadId: string): Promise<void> {
|
|
149
|
-
|
|
191
|
+
async clearMessages(threadId: string, scope?: QueueScope): Promise<void> {
|
|
192
|
+
const filter = scopeClause(scope);
|
|
193
|
+
this.db.prepare(`DELETE FROM lt_thread_message_queue WHERE thread_id = ?${filter.sql}`).run(threadId, ...filter.params);
|
|
150
194
|
}
|
|
151
195
|
|
|
152
|
-
async markProcessing(messageId: string): Promise<void> {
|
|
153
|
-
|
|
196
|
+
async markProcessing(messageId: string, customRunConfig?: unknown, scope?: QueueScope): Promise<void> {
|
|
197
|
+
const filter = scopeClause(scope);
|
|
198
|
+
if (customRunConfig === undefined) {
|
|
199
|
+
this.db.prepare(`UPDATE lt_thread_message_queue SET status = 'processing' WHERE id = ?${filter.sql}`).run(messageId, ...filter.params);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
this.db.prepare(
|
|
203
|
+
`UPDATE lt_thread_message_queue SET status = 'processing', custom_run_config = ? WHERE id = ?${filter.sql}`,
|
|
204
|
+
).run(JSON.stringify(customRunConfig), messageId, ...filter.params);
|
|
154
205
|
}
|
|
155
206
|
|
|
156
|
-
async resetProcessingToPending(threadId: string): Promise<number> {
|
|
207
|
+
async resetProcessingToPending(threadId: string, scope?: QueueScope): Promise<number> {
|
|
208
|
+
const filter = scopeClause(scope);
|
|
157
209
|
const result = this.db.prepare(
|
|
158
|
-
`UPDATE lt_thread_message_queue SET status = 'pending' WHERE thread_id = ? AND status = 'processing'`,
|
|
159
|
-
).run(threadId);
|
|
210
|
+
`UPDATE lt_thread_message_queue SET status = 'pending' WHERE thread_id = ? AND status = 'processing'${filter.sql}`,
|
|
211
|
+
).run(threadId, ...filter.params);
|
|
160
212
|
return result.changes;
|
|
161
213
|
}
|
|
162
214
|
|
|
@@ -166,6 +218,16 @@ export class LocalThreadMessageQueueStore implements IMessageQueueStore {
|
|
|
166
218
|
}
|
|
167
219
|
}
|
|
168
220
|
|
|
221
|
+
function scopeClause(scope?: QueueScope): { sql: string; params: unknown[] } {
|
|
222
|
+
if (!scope) return { sql: "", params: [] };
|
|
223
|
+
const entries = (["tenantId", "assistantId", "workspaceId", "projectId"] as const)
|
|
224
|
+
.map((key) => [key, scope[key]] as const);
|
|
225
|
+
return {
|
|
226
|
+
sql: entries.map(([key, value]) => ` AND ${key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)} ${value == null ? "IS NULL" : "= ?"}`).join(""),
|
|
227
|
+
params: entries.flatMap(([, value]) => value == null ? [] : [value]),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
169
231
|
function rowToMessage(row: QueueRow): PendingMessage {
|
|
170
232
|
return {
|
|
171
233
|
id: row.id,
|