@ash-ai/server 0.0.2 → 0.0.4
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/dist/__tests__/attachments.test.d.ts +2 -0
- package/dist/__tests__/attachments.test.d.ts.map +1 -0
- package/dist/__tests__/attachments.test.js +57 -0
- package/dist/__tests__/attachments.test.js.map +1 -0
- package/dist/__tests__/bundle.test.d.ts +2 -0
- package/dist/__tests__/bundle.test.d.ts.map +1 -0
- package/dist/__tests__/bundle.test.js +55 -0
- package/dist/__tests__/bundle.test.js.map +1 -0
- package/dist/__tests__/coordinator.test.d.ts +2 -0
- package/dist/__tests__/coordinator.test.d.ts.map +1 -0
- package/dist/__tests__/coordinator.test.js +283 -0
- package/dist/__tests__/coordinator.test.js.map +1 -0
- package/dist/__tests__/crypto.test.d.ts +2 -0
- package/dist/__tests__/crypto.test.d.ts.map +1 -0
- package/dist/__tests__/crypto.test.js +45 -0
- package/dist/__tests__/crypto.test.js.map +1 -0
- package/dist/__tests__/file-store.test.d.ts +2 -0
- package/dist/__tests__/file-store.test.d.ts.map +1 -0
- package/dist/__tests__/file-store.test.js +105 -0
- package/dist/__tests__/file-store.test.js.map +1 -0
- package/dist/__tests__/files.test.js +3 -3
- package/dist/__tests__/files.test.js.map +1 -1
- package/dist/__tests__/openapi.test.js +6 -3
- package/dist/__tests__/openapi.test.js.map +1 -1
- package/dist/__tests__/queue.test.d.ts +2 -0
- package/dist/__tests__/queue.test.d.ts.map +1 -0
- package/dist/__tests__/queue.test.js +151 -0
- package/dist/__tests__/queue.test.js.map +1 -0
- package/dist/__tests__/usage.test.d.ts +2 -0
- package/dist/__tests__/usage.test.d.ts.map +1 -0
- package/dist/__tests__/usage.test.js +74 -0
- package/dist/__tests__/usage.test.js.map +1 -0
- package/dist/crypto.d.ts +8 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +29 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db/drizzle-db.d.ts +128 -0
- package/dist/db/drizzle-db.d.ts.map +1 -0
- package/dist/db/drizzle-db.js +789 -0
- package/dist/db/drizzle-db.js.map +1 -0
- package/dist/db/index.d.ts +161 -3
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +164 -8
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema.pg.d.ts +1625 -0
- package/dist/db/schema.pg.d.ts.map +1 -0
- package/dist/db/schema.pg.js +150 -0
- package/dist/db/schema.pg.js.map +1 -0
- package/dist/db/schema.sqlite.d.ts +1781 -0
- package/dist/db/schema.sqlite.d.ts.map +1 -0
- package/dist/db/schema.sqlite.js +150 -0
- package/dist/db/schema.sqlite.js.map +1 -0
- package/dist/index.js +18 -1
- package/dist/index.js.map +1 -1
- package/dist/queue/processor.d.ts +51 -0
- package/dist/queue/processor.d.ts.map +1 -0
- package/dist/queue/processor.js +98 -0
- package/dist/queue/processor.js.map +1 -0
- package/dist/routes/attachments.d.ts +3 -0
- package/dist/routes/attachments.d.ts.map +1 -0
- package/dist/routes/attachments.js +168 -0
- package/dist/routes/attachments.js.map +1 -0
- package/dist/routes/credentials.d.ts +11 -0
- package/dist/routes/credentials.d.ts.map +1 -0
- package/dist/routes/credentials.js +120 -0
- package/dist/routes/credentials.js.map +1 -0
- package/dist/routes/files.js +5 -5
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +9 -1
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/queue.d.ts +3 -0
- package/dist/routes/queue.d.ts.map +1 -0
- package/dist/routes/queue.js +144 -0
- package/dist/routes/queue.js.map +1 -0
- package/dist/routes/runners.d.ts +5 -0
- package/dist/routes/runners.d.ts.map +1 -1
- package/dist/routes/runners.js +42 -5
- package/dist/routes/runners.js.map +1 -1
- package/dist/routes/sessions.d.ts +2 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +236 -11
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/usage.d.ts +3 -0
- package/dist/routes/usage.d.ts.map +1 -0
- package/dist/routes/usage.js +64 -0
- package/dist/routes/usage.js.map +1 -0
- package/dist/routes/workspace.d.ts +4 -0
- package/dist/routes/workspace.d.ts.map +1 -0
- package/dist/routes/workspace.js +123 -0
- package/dist/routes/workspace.js.map +1 -0
- package/dist/runner/coordinator.d.ts +77 -9
- package/dist/runner/coordinator.d.ts.map +1 -1
- package/dist/runner/coordinator.js +163 -89
- package/dist/runner/coordinator.js.map +1 -1
- package/dist/runner/local-backend.d.ts +1 -0
- package/dist/runner/local-backend.d.ts.map +1 -1
- package/dist/runner/local-backend.js +7 -0
- package/dist/runner/local-backend.js.map +1 -1
- package/dist/runner/remote-backend.d.ts +2 -0
- package/dist/runner/remote-backend.d.ts.map +1 -1
- package/dist/runner/remote-backend.js +7 -0
- package/dist/runner/remote-backend.js.map +1 -1
- package/dist/runner/runner-client.d.ts +4 -0
- package/dist/runner/runner-client.d.ts.map +1 -1
- package/dist/runner/runner-client.js +12 -0
- package/dist/runner/runner-client.js.map +1 -1
- package/dist/runner/types.d.ts +4 -0
- package/dist/runner/types.d.ts.map +1 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +115 -1
- package/dist/schemas.js.map +1 -1
- package/dist/telemetry/exporter.d.ts +16 -0
- package/dist/telemetry/exporter.d.ts.map +1 -0
- package/dist/telemetry/exporter.js +89 -0
- package/dist/telemetry/exporter.js.map +1 -0
- package/dist/usage/extractor.d.ts +18 -0
- package/dist/usage/extractor.d.ts.map +1 -0
- package/dist/usage/extractor.js +48 -0
- package/dist/usage/extractor.js.map +1 -0
- package/drizzle/pg/0000_thick_loners.sql +75 -0
- package/drizzle/pg/0001_rare_lester.sql +13 -0
- package/drizzle/pg/0002_short_shinko_yamashiro.sql +1 -0
- package/drizzle/pg/0003_remarkable_mastermind.sql +14 -0
- package/drizzle/pg/0004_warm_reaper.sql +18 -0
- package/drizzle/pg/0005_overconfident_mole_man.sql +14 -0
- package/drizzle/pg/0006_third_shiva.sql +13 -0
- package/drizzle/pg/0007_keen_shockwave.sql +2 -0
- package/drizzle/pg/meta/0000_snapshot.json +648 -0
- package/drizzle/pg/meta/0001_snapshot.json +743 -0
- package/drizzle/pg/meta/0002_snapshot.json +749 -0
- package/drizzle/pg/meta/0003_snapshot.json +841 -0
- package/drizzle/pg/meta/0004_snapshot.json +974 -0
- package/drizzle/pg/meta/0005_snapshot.json +1079 -0
- package/drizzle/pg/meta/0006_snapshot.json +1193 -0
- package/drizzle/pg/meta/0007_snapshot.json +1199 -0
- package/drizzle/pg/meta/_journal.json +62 -0
- package/drizzle/sqlite/0000_massive_kinsey_walden.sql +75 -0
- package/drizzle/sqlite/0001_quiet_phantom_reporter.sql +13 -0
- package/drizzle/sqlite/0002_broad_sheva_callister.sql +1 -0
- package/drizzle/sqlite/0003_thankful_agent_brand.sql +14 -0
- package/drizzle/sqlite/0004_productive_wolverine.sql +18 -0
- package/drizzle/sqlite/0005_chilly_carlie_cooper.sql +14 -0
- package/drizzle/sqlite/0006_workable_starfox.sql +13 -0
- package/drizzle/sqlite/0007_quick_hemingway.sql +19 -0
- package/drizzle/sqlite/meta/0000_snapshot.json +503 -0
- package/drizzle/sqlite/meta/0001_snapshot.json +587 -0
- package/drizzle/sqlite/meta/0002_snapshot.json +594 -0
- package/drizzle/sqlite/meta/0003_snapshot.json +685 -0
- package/drizzle/sqlite/meta/0004_snapshot.json +807 -0
- package/drizzle/sqlite/meta/0005_snapshot.json +897 -0
- package/drizzle/sqlite/meta/0006_snapshot.json +981 -0
- package/drizzle/sqlite/meta/0007_snapshot.json +988 -0
- package/drizzle/sqlite/meta/_journal.json +62 -0
- package/package.json +10 -5
- package/dist/__tests__/schema.test.d.ts +0 -2
- package/dist/__tests__/schema.test.d.ts.map +0 -1
- package/dist/__tests__/schema.test.js +0 -31
- package/dist/__tests__/schema.test.js.map +0 -1
- package/dist/db/dump-schema.d.ts +0 -10
- package/dist/db/dump-schema.d.ts.map +0 -1
- package/dist/db/dump-schema.js +0 -64
- package/dist/db/dump-schema.js.map +0 -1
- package/dist/db/pg.d.ts +0 -35
- package/dist/db/pg.d.ts.map +0 -1
- package/dist/db/pg.js +0 -272
- package/dist/db/pg.js.map +0 -1
- package/dist/db/sqlite.d.ts +0 -34
- package/dist/db/sqlite.d.ts.map +0 -1
- package/dist/db/sqlite.js +0 -296
- package/dist/db/sqlite.js.map +0 -1
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { eq, and, sql, gt, lt, ne, asc, desc, inArray } from 'drizzle-orm';
|
|
3
|
+
/**
|
|
4
|
+
* Unified Db implementation backed by Drizzle ORM.
|
|
5
|
+
* Works with both SQLite (better-sqlite3) and PostgreSQL (pg) drivers.
|
|
6
|
+
*/
|
|
7
|
+
export class DrizzleDb {
|
|
8
|
+
drizzle;
|
|
9
|
+
schema;
|
|
10
|
+
dialect;
|
|
11
|
+
closeFn;
|
|
12
|
+
constructor(drizzle, // DrizzleSQLiteDatabase | DrizzleNodePgDatabase
|
|
13
|
+
schema, dialect, closeFn) {
|
|
14
|
+
this.drizzle = drizzle;
|
|
15
|
+
this.schema = schema;
|
|
16
|
+
this.dialect = dialect;
|
|
17
|
+
this.closeFn = closeFn;
|
|
18
|
+
}
|
|
19
|
+
// -- Runners ----------------------------------------------------------------
|
|
20
|
+
async upsertRunner(id, host, port, maxSandboxes) {
|
|
21
|
+
const { runners } = this.schema;
|
|
22
|
+
const now = new Date().toISOString();
|
|
23
|
+
await this.drizzle
|
|
24
|
+
.insert(runners)
|
|
25
|
+
.values({ id, host, port, maxSandboxes, activeCount: 0, warmingCount: 0, lastHeartbeatAt: now, registeredAt: now })
|
|
26
|
+
.onConflictDoUpdate({
|
|
27
|
+
target: runners.id,
|
|
28
|
+
set: { host, port, maxSandboxes, lastHeartbeatAt: now },
|
|
29
|
+
});
|
|
30
|
+
return { id, host, port, maxSandboxes, activeCount: 0, warmingCount: 0, lastHeartbeatAt: now, registeredAt: now };
|
|
31
|
+
}
|
|
32
|
+
async heartbeatRunner(id, activeCount, warmingCount) {
|
|
33
|
+
const { runners } = this.schema;
|
|
34
|
+
const now = new Date().toISOString();
|
|
35
|
+
await this.drizzle
|
|
36
|
+
.update(runners)
|
|
37
|
+
.set({ activeCount, warmingCount, lastHeartbeatAt: now })
|
|
38
|
+
.where(eq(runners.id, id));
|
|
39
|
+
}
|
|
40
|
+
async getRunner(id) {
|
|
41
|
+
const { runners } = this.schema;
|
|
42
|
+
const rows = await this.drizzle
|
|
43
|
+
.select()
|
|
44
|
+
.from(runners)
|
|
45
|
+
.where(eq(runners.id, id))
|
|
46
|
+
.limit(1);
|
|
47
|
+
if (rows.length === 0)
|
|
48
|
+
return null;
|
|
49
|
+
const r = rows[0];
|
|
50
|
+
return { id: r.id, host: r.host, port: r.port, maxSandboxes: r.maxSandboxes, activeCount: r.activeCount, warmingCount: r.warmingCount, lastHeartbeatAt: r.lastHeartbeatAt, registeredAt: r.registeredAt };
|
|
51
|
+
}
|
|
52
|
+
async listHealthyRunners(cutoffIso) {
|
|
53
|
+
const { runners } = this.schema;
|
|
54
|
+
const rows = await this.drizzle
|
|
55
|
+
.select()
|
|
56
|
+
.from(runners)
|
|
57
|
+
.where(gt(runners.lastHeartbeatAt, cutoffIso));
|
|
58
|
+
return rows.map((r) => ({
|
|
59
|
+
id: r.id, host: r.host, port: r.port, maxSandboxes: r.maxSandboxes,
|
|
60
|
+
activeCount: r.activeCount, warmingCount: r.warmingCount,
|
|
61
|
+
lastHeartbeatAt: r.lastHeartbeatAt, registeredAt: r.registeredAt,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
async listDeadRunners(cutoffIso) {
|
|
65
|
+
const { runners } = this.schema;
|
|
66
|
+
const rows = await this.drizzle
|
|
67
|
+
.select()
|
|
68
|
+
.from(runners)
|
|
69
|
+
.where(sql `${runners.lastHeartbeatAt} <= ${cutoffIso}`);
|
|
70
|
+
return rows.map((r) => ({
|
|
71
|
+
id: r.id, host: r.host, port: r.port, maxSandboxes: r.maxSandboxes,
|
|
72
|
+
activeCount: r.activeCount, warmingCount: r.warmingCount,
|
|
73
|
+
lastHeartbeatAt: r.lastHeartbeatAt, registeredAt: r.registeredAt,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
async selectBestRunner(cutoffIso) {
|
|
77
|
+
const { runners } = this.schema;
|
|
78
|
+
const available = sql `${runners.maxSandboxes} - ${runners.activeCount} - ${runners.warmingCount}`;
|
|
79
|
+
const rows = await this.drizzle
|
|
80
|
+
.select()
|
|
81
|
+
.from(runners)
|
|
82
|
+
.where(and(gt(runners.lastHeartbeatAt, cutoffIso), gt(available, 0)))
|
|
83
|
+
.orderBy(desc(available))
|
|
84
|
+
.limit(1);
|
|
85
|
+
if (rows.length === 0)
|
|
86
|
+
return null;
|
|
87
|
+
const r = rows[0];
|
|
88
|
+
return { id: r.id, host: r.host, port: r.port, maxSandboxes: r.maxSandboxes, activeCount: r.activeCount, warmingCount: r.warmingCount, lastHeartbeatAt: r.lastHeartbeatAt, registeredAt: r.registeredAt };
|
|
89
|
+
}
|
|
90
|
+
async deleteRunner(id) {
|
|
91
|
+
const { runners } = this.schema;
|
|
92
|
+
await this.drizzle
|
|
93
|
+
.delete(runners)
|
|
94
|
+
.where(eq(runners.id, id));
|
|
95
|
+
}
|
|
96
|
+
async listAllRunners() {
|
|
97
|
+
const { runners } = this.schema;
|
|
98
|
+
const rows = await this.drizzle
|
|
99
|
+
.select()
|
|
100
|
+
.from(runners)
|
|
101
|
+
.orderBy(desc(sql `${runners.maxSandboxes} - ${runners.activeCount} - ${runners.warmingCount}`));
|
|
102
|
+
return rows.map((r) => ({
|
|
103
|
+
id: r.id, host: r.host, port: r.port, maxSandboxes: r.maxSandboxes,
|
|
104
|
+
activeCount: r.activeCount, warmingCount: r.warmingCount,
|
|
105
|
+
lastHeartbeatAt: r.lastHeartbeatAt, registeredAt: r.registeredAt,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
// -- Agents -----------------------------------------------------------------
|
|
109
|
+
async upsertAgent(name, path, tenantId = 'default') {
|
|
110
|
+
const { agents } = this.schema;
|
|
111
|
+
const now = new Date().toISOString();
|
|
112
|
+
// Check for existing agent to get id and version
|
|
113
|
+
const existing = await this.drizzle
|
|
114
|
+
.select({ id: agents.id, version: agents.version })
|
|
115
|
+
.from(agents)
|
|
116
|
+
.where(and(eq(agents.tenantId, tenantId), eq(agents.name, name)))
|
|
117
|
+
.limit(1);
|
|
118
|
+
const version = existing.length > 0 ? existing[0].version + 1 : 1;
|
|
119
|
+
const id = existing.length > 0 ? existing[0].id : randomUUID();
|
|
120
|
+
await this.drizzle
|
|
121
|
+
.insert(agents)
|
|
122
|
+
.values({ id, tenantId, name, version, path, createdAt: now, updatedAt: now })
|
|
123
|
+
.onConflictDoUpdate({
|
|
124
|
+
target: [agents.tenantId, agents.name],
|
|
125
|
+
set: { version, path, updatedAt: now },
|
|
126
|
+
});
|
|
127
|
+
return { id, name, tenantId, version, path, createdAt: now, updatedAt: now };
|
|
128
|
+
}
|
|
129
|
+
async getAgent(name, tenantId = 'default') {
|
|
130
|
+
const { agents } = this.schema;
|
|
131
|
+
const rows = await this.drizzle
|
|
132
|
+
.select()
|
|
133
|
+
.from(agents)
|
|
134
|
+
.where(and(eq(agents.tenantId, tenantId), eq(agents.name, name)))
|
|
135
|
+
.limit(1);
|
|
136
|
+
if (rows.length === 0)
|
|
137
|
+
return null;
|
|
138
|
+
const r = rows[0];
|
|
139
|
+
return { id: r.id, name: r.name, tenantId: r.tenantId, version: r.version, path: r.path, createdAt: r.createdAt, updatedAt: r.updatedAt };
|
|
140
|
+
}
|
|
141
|
+
async listAgents(tenantId = 'default') {
|
|
142
|
+
const { agents } = this.schema;
|
|
143
|
+
const rows = await this.drizzle
|
|
144
|
+
.select()
|
|
145
|
+
.from(agents)
|
|
146
|
+
.where(eq(agents.tenantId, tenantId))
|
|
147
|
+
.orderBy(asc(agents.name));
|
|
148
|
+
return rows.map((r) => ({ id: r.id, name: r.name, tenantId: r.tenantId, version: r.version, path: r.path, createdAt: r.createdAt, updatedAt: r.updatedAt }));
|
|
149
|
+
}
|
|
150
|
+
async deleteAgent(name, tenantId = 'default') {
|
|
151
|
+
const { agents, sessions } = this.schema;
|
|
152
|
+
// Delete related sessions first
|
|
153
|
+
await this.drizzle
|
|
154
|
+
.delete(sessions)
|
|
155
|
+
.where(and(eq(sessions.agentName, name), eq(sessions.tenantId, tenantId)));
|
|
156
|
+
const result = await this.drizzle
|
|
157
|
+
.delete(agents)
|
|
158
|
+
.where(and(eq(agents.name, name), eq(agents.tenantId, tenantId)));
|
|
159
|
+
// Drizzle returns { changes } for sqlite and { rowCount } for pg
|
|
160
|
+
return (result.changes ?? result.rowCount ?? 0) > 0;
|
|
161
|
+
}
|
|
162
|
+
// -- Sessions ---------------------------------------------------------------
|
|
163
|
+
async insertSession(id, agentName, sandboxId, tenantId = 'default', parentSessionId) {
|
|
164
|
+
const { sessions } = this.schema;
|
|
165
|
+
const now = new Date().toISOString();
|
|
166
|
+
await this.drizzle
|
|
167
|
+
.insert(sessions)
|
|
168
|
+
.values({ id, tenantId, agentName, sandboxId, status: 'starting', parentSessionId: parentSessionId ?? null, createdAt: now, lastActiveAt: now });
|
|
169
|
+
return { id, tenantId, agentName, sandboxId, status: 'starting', parentSessionId: parentSessionId ?? null, createdAt: now, lastActiveAt: now };
|
|
170
|
+
}
|
|
171
|
+
async insertForkedSession(id, parentSession, sandboxId) {
|
|
172
|
+
const { sessions, messages } = this.schema;
|
|
173
|
+
const now = new Date().toISOString();
|
|
174
|
+
const tenantId = parentSession.tenantId ?? 'default';
|
|
175
|
+
// 1. Create new session linked to parent
|
|
176
|
+
await this.drizzle
|
|
177
|
+
.insert(sessions)
|
|
178
|
+
.values({ id, tenantId, agentName: parentSession.agentName, sandboxId, status: 'paused', parentSessionId: parentSession.id, createdAt: now, lastActiveAt: now });
|
|
179
|
+
// 2. Copy all messages from parent session with new IDs and session reference
|
|
180
|
+
const parentMessages = await this.drizzle
|
|
181
|
+
.select()
|
|
182
|
+
.from(messages)
|
|
183
|
+
.where(and(eq(messages.tenantId, tenantId), eq(messages.sessionId, parentSession.id)))
|
|
184
|
+
.orderBy(asc(messages.sequence));
|
|
185
|
+
if (parentMessages.length > 0) {
|
|
186
|
+
const copied = parentMessages.map((m) => ({
|
|
187
|
+
id: randomUUID(),
|
|
188
|
+
tenantId,
|
|
189
|
+
sessionId: id,
|
|
190
|
+
role: m.role,
|
|
191
|
+
content: m.content,
|
|
192
|
+
sequence: m.sequence,
|
|
193
|
+
createdAt: m.createdAt,
|
|
194
|
+
}));
|
|
195
|
+
await this.drizzle.insert(messages).values(copied);
|
|
196
|
+
}
|
|
197
|
+
return { id, tenantId, agentName: parentSession.agentName, sandboxId, status: 'paused', parentSessionId: parentSession.id, createdAt: now, lastActiveAt: now };
|
|
198
|
+
}
|
|
199
|
+
async updateSessionStatus(id, status) {
|
|
200
|
+
const { sessions } = this.schema;
|
|
201
|
+
const now = new Date().toISOString();
|
|
202
|
+
await this.drizzle
|
|
203
|
+
.update(sessions)
|
|
204
|
+
.set({ status, lastActiveAt: now })
|
|
205
|
+
.where(eq(sessions.id, id));
|
|
206
|
+
}
|
|
207
|
+
async updateSessionSandbox(id, sandboxId) {
|
|
208
|
+
const { sessions } = this.schema;
|
|
209
|
+
const now = new Date().toISOString();
|
|
210
|
+
await this.drizzle
|
|
211
|
+
.update(sessions)
|
|
212
|
+
.set({ sandboxId, lastActiveAt: now })
|
|
213
|
+
.where(eq(sessions.id, id));
|
|
214
|
+
}
|
|
215
|
+
async updateSessionRunner(id, runnerId) {
|
|
216
|
+
const { sessions } = this.schema;
|
|
217
|
+
const now = new Date().toISOString();
|
|
218
|
+
await this.drizzle
|
|
219
|
+
.update(sessions)
|
|
220
|
+
.set({ runnerId, lastActiveAt: now })
|
|
221
|
+
.where(eq(sessions.id, id));
|
|
222
|
+
}
|
|
223
|
+
async getSession(id) {
|
|
224
|
+
const { sessions } = this.schema;
|
|
225
|
+
const rows = await this.drizzle
|
|
226
|
+
.select()
|
|
227
|
+
.from(sessions)
|
|
228
|
+
.where(eq(sessions.id, id))
|
|
229
|
+
.limit(1);
|
|
230
|
+
if (rows.length === 0)
|
|
231
|
+
return null;
|
|
232
|
+
const r = rows[0];
|
|
233
|
+
return { id: r.id, tenantId: r.tenantId, agentName: r.agentName, sandboxId: r.sandboxId, status: r.status, runnerId: r.runnerId ?? null, parentSessionId: r.parentSessionId ?? null, createdAt: r.createdAt, lastActiveAt: r.lastActiveAt };
|
|
234
|
+
}
|
|
235
|
+
async listSessions(tenantId = 'default', agent) {
|
|
236
|
+
const { sessions } = this.schema;
|
|
237
|
+
const condition = agent
|
|
238
|
+
? and(eq(sessions.tenantId, tenantId), eq(sessions.agentName, agent))
|
|
239
|
+
: eq(sessions.tenantId, tenantId);
|
|
240
|
+
const rows = await this.drizzle
|
|
241
|
+
.select()
|
|
242
|
+
.from(sessions)
|
|
243
|
+
.where(condition)
|
|
244
|
+
.orderBy(desc(sessions.createdAt));
|
|
245
|
+
return rows.map((r) => ({
|
|
246
|
+
id: r.id, tenantId: r.tenantId, agentName: r.agentName, sandboxId: r.sandboxId,
|
|
247
|
+
status: r.status, runnerId: r.runnerId ?? null, parentSessionId: r.parentSessionId ?? null, createdAt: r.createdAt, lastActiveAt: r.lastActiveAt,
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
async listSessionsByRunner(runnerId) {
|
|
251
|
+
const { sessions } = this.schema;
|
|
252
|
+
const rows = await this.drizzle
|
|
253
|
+
.select()
|
|
254
|
+
.from(sessions)
|
|
255
|
+
.where(eq(sessions.runnerId, runnerId))
|
|
256
|
+
.orderBy(desc(sessions.createdAt));
|
|
257
|
+
return rows.map((r) => ({
|
|
258
|
+
id: r.id, tenantId: r.tenantId, agentName: r.agentName, sandboxId: r.sandboxId,
|
|
259
|
+
status: r.status, runnerId: r.runnerId ?? null, parentSessionId: r.parentSessionId ?? null, createdAt: r.createdAt, lastActiveAt: r.lastActiveAt,
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
async bulkPauseSessionsByRunner(runnerId) {
|
|
263
|
+
const { sessions } = this.schema;
|
|
264
|
+
const now = new Date().toISOString();
|
|
265
|
+
const result = await this.drizzle
|
|
266
|
+
.update(sessions)
|
|
267
|
+
.set({ status: 'paused', lastActiveAt: now })
|
|
268
|
+
.where(and(eq(sessions.runnerId, runnerId), inArray(sessions.status, ['active', 'starting'])));
|
|
269
|
+
return result.changes ?? result.rowCount ?? 0;
|
|
270
|
+
}
|
|
271
|
+
async touchSession(id) {
|
|
272
|
+
const { sessions } = this.schema;
|
|
273
|
+
const now = new Date().toISOString();
|
|
274
|
+
await this.drizzle
|
|
275
|
+
.update(sessions)
|
|
276
|
+
.set({ lastActiveAt: now })
|
|
277
|
+
.where(eq(sessions.id, id));
|
|
278
|
+
}
|
|
279
|
+
// -- Sandboxes --------------------------------------------------------------
|
|
280
|
+
async insertSandbox(id, agentName, workspaceDir, sessionId, tenantId = 'default') {
|
|
281
|
+
const { sandboxes } = this.schema;
|
|
282
|
+
const now = new Date().toISOString();
|
|
283
|
+
await this.drizzle
|
|
284
|
+
.insert(sandboxes)
|
|
285
|
+
.values({ id, tenantId, agentName, workspaceDir, sessionId: sessionId ?? null, state: 'warming', createdAt: now, lastUsedAt: now });
|
|
286
|
+
}
|
|
287
|
+
async updateSandboxState(id, state) {
|
|
288
|
+
const { sandboxes } = this.schema;
|
|
289
|
+
await this.drizzle
|
|
290
|
+
.update(sandboxes)
|
|
291
|
+
.set({ state })
|
|
292
|
+
.where(eq(sandboxes.id, id));
|
|
293
|
+
}
|
|
294
|
+
async updateSandboxSession(id, sessionId) {
|
|
295
|
+
const { sandboxes } = this.schema;
|
|
296
|
+
await this.drizzle
|
|
297
|
+
.update(sandboxes)
|
|
298
|
+
.set({ sessionId })
|
|
299
|
+
.where(eq(sandboxes.id, id));
|
|
300
|
+
}
|
|
301
|
+
async touchSandbox(id) {
|
|
302
|
+
const { sandboxes } = this.schema;
|
|
303
|
+
const now = new Date().toISOString();
|
|
304
|
+
await this.drizzle
|
|
305
|
+
.update(sandboxes)
|
|
306
|
+
.set({ lastUsedAt: now })
|
|
307
|
+
.where(eq(sandboxes.id, id));
|
|
308
|
+
}
|
|
309
|
+
async getSandbox(id) {
|
|
310
|
+
const { sandboxes } = this.schema;
|
|
311
|
+
const rows = await this.drizzle
|
|
312
|
+
.select()
|
|
313
|
+
.from(sandboxes)
|
|
314
|
+
.where(eq(sandboxes.id, id))
|
|
315
|
+
.limit(1);
|
|
316
|
+
if (rows.length === 0)
|
|
317
|
+
return null;
|
|
318
|
+
const r = rows[0];
|
|
319
|
+
return { id: r.id, sessionId: r.sessionId, agentName: r.agentName, state: r.state, workspaceDir: r.workspaceDir, createdAt: r.createdAt, lastUsedAt: r.lastUsedAt };
|
|
320
|
+
}
|
|
321
|
+
async countSandboxes() {
|
|
322
|
+
const { sandboxes } = this.schema;
|
|
323
|
+
const rows = await this.drizzle
|
|
324
|
+
.select({ count: sql `count(*)` })
|
|
325
|
+
.from(sandboxes);
|
|
326
|
+
// SQLite returns number directly, PG returns string
|
|
327
|
+
return typeof rows[0].count === 'string' ? parseInt(rows[0].count, 10) : rows[0].count;
|
|
328
|
+
}
|
|
329
|
+
async getBestEvictionCandidate() {
|
|
330
|
+
const { sandboxes } = this.schema;
|
|
331
|
+
const rows = await this.drizzle
|
|
332
|
+
.select()
|
|
333
|
+
.from(sandboxes)
|
|
334
|
+
.where(inArray(sandboxes.state, ['cold', 'warm', 'waiting']))
|
|
335
|
+
.orderBy(sql `CASE ${sandboxes.state} WHEN 'cold' THEN 0 WHEN 'warm' THEN 1 WHEN 'waiting' THEN 2 END`, asc(sandboxes.lastUsedAt))
|
|
336
|
+
.limit(1);
|
|
337
|
+
if (rows.length === 0)
|
|
338
|
+
return null;
|
|
339
|
+
const r = rows[0];
|
|
340
|
+
return { id: r.id, sessionId: r.sessionId, agentName: r.agentName, state: r.state, workspaceDir: r.workspaceDir, createdAt: r.createdAt, lastUsedAt: r.lastUsedAt };
|
|
341
|
+
}
|
|
342
|
+
async getIdleSandboxes(olderThan) {
|
|
343
|
+
const { sandboxes } = this.schema;
|
|
344
|
+
const rows = await this.drizzle
|
|
345
|
+
.select()
|
|
346
|
+
.from(sandboxes)
|
|
347
|
+
.where(and(eq(sandboxes.state, 'waiting'), lt(sandboxes.lastUsedAt, olderThan)))
|
|
348
|
+
.orderBy(asc(sandboxes.lastUsedAt));
|
|
349
|
+
return rows.map((r) => ({
|
|
350
|
+
id: r.id, sessionId: r.sessionId, agentName: r.agentName,
|
|
351
|
+
state: r.state, workspaceDir: r.workspaceDir, createdAt: r.createdAt, lastUsedAt: r.lastUsedAt,
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
async deleteSandbox(id) {
|
|
355
|
+
const { sandboxes } = this.schema;
|
|
356
|
+
await this.drizzle
|
|
357
|
+
.delete(sandboxes)
|
|
358
|
+
.where(eq(sandboxes.id, id));
|
|
359
|
+
}
|
|
360
|
+
async markAllSandboxesCold() {
|
|
361
|
+
const { sandboxes } = this.schema;
|
|
362
|
+
const result = await this.drizzle
|
|
363
|
+
.update(sandboxes)
|
|
364
|
+
.set({ state: 'cold' })
|
|
365
|
+
.where(ne(sandboxes.state, 'cold'));
|
|
366
|
+
return result.changes ?? result.rowCount ?? 0;
|
|
367
|
+
}
|
|
368
|
+
// -- Messages ---------------------------------------------------------------
|
|
369
|
+
async insertMessage(sessionId, role, content, tenantId = 'default') {
|
|
370
|
+
const { messages } = this.schema;
|
|
371
|
+
const id = randomUUID();
|
|
372
|
+
const now = new Date().toISOString();
|
|
373
|
+
if (this.dialect === 'pg') {
|
|
374
|
+
// PG: atomic subquery for sequence assignment (no TOCTOU race)
|
|
375
|
+
const rows = await this.drizzle.execute(sql `INSERT INTO messages (id, tenant_id, session_id, role, content, sequence, created_at)
|
|
376
|
+
VALUES (${id}, ${tenantId}, ${sessionId}, ${role}, ${content},
|
|
377
|
+
COALESCE((SELECT MAX(sequence) FROM messages WHERE tenant_id = ${tenantId} AND session_id = ${sessionId}), 0) + 1,
|
|
378
|
+
${now})
|
|
379
|
+
RETURNING sequence`);
|
|
380
|
+
return { id, sessionId, tenantId, role, content, sequence: rows.rows[0].sequence, createdAt: now };
|
|
381
|
+
}
|
|
382
|
+
// SQLite: synchronous driver, no race condition risk
|
|
383
|
+
const last = await this.drizzle
|
|
384
|
+
.select({ maxSeq: sql `MAX(${messages.sequence})` })
|
|
385
|
+
.from(messages)
|
|
386
|
+
.where(and(eq(messages.tenantId, tenantId), eq(messages.sessionId, sessionId)));
|
|
387
|
+
const sequence = (last[0]?.maxSeq ?? 0) + 1;
|
|
388
|
+
await this.drizzle
|
|
389
|
+
.insert(messages)
|
|
390
|
+
.values({ id, tenantId, sessionId, role, content, sequence, createdAt: now });
|
|
391
|
+
return { id, sessionId, tenantId, role, content, sequence, createdAt: now };
|
|
392
|
+
}
|
|
393
|
+
async listMessages(sessionId, tenantId = 'default', opts) {
|
|
394
|
+
const { messages } = this.schema;
|
|
395
|
+
const limit = opts?.limit ?? 100;
|
|
396
|
+
const afterSeq = opts?.afterSequence ?? 0;
|
|
397
|
+
const rows = await this.drizzle
|
|
398
|
+
.select()
|
|
399
|
+
.from(messages)
|
|
400
|
+
.where(and(eq(messages.tenantId, tenantId), eq(messages.sessionId, sessionId), gt(messages.sequence, afterSeq)))
|
|
401
|
+
.orderBy(asc(messages.sequence))
|
|
402
|
+
.limit(limit);
|
|
403
|
+
return rows.map((r) => ({
|
|
404
|
+
id: r.id, sessionId: r.sessionId, tenantId: r.tenantId,
|
|
405
|
+
role: r.role, content: r.content, sequence: r.sequence, createdAt: r.createdAt,
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
// -- Session Events ---------------------------------------------------------
|
|
409
|
+
async insertSessionEvent(sessionId, type, data, tenantId = 'default') {
|
|
410
|
+
const { sessionEvents } = this.schema;
|
|
411
|
+
const id = randomUUID();
|
|
412
|
+
const now = new Date().toISOString();
|
|
413
|
+
if (this.dialect === 'pg') {
|
|
414
|
+
const rows = await this.drizzle.execute(sql `INSERT INTO session_events (id, tenant_id, session_id, type, data, sequence, created_at)
|
|
415
|
+
VALUES (${id}, ${tenantId}, ${sessionId}, ${type}, ${data},
|
|
416
|
+
COALESCE((SELECT MAX(sequence) FROM session_events WHERE tenant_id = ${tenantId} AND session_id = ${sessionId}), 0) + 1,
|
|
417
|
+
${now})
|
|
418
|
+
RETURNING sequence`);
|
|
419
|
+
return { id, sessionId, tenantId, type, data, sequence: rows.rows[0].sequence, createdAt: now };
|
|
420
|
+
}
|
|
421
|
+
// SQLite
|
|
422
|
+
const last = await this.drizzle
|
|
423
|
+
.select({ maxSeq: sql `MAX(${sessionEvents.sequence})` })
|
|
424
|
+
.from(sessionEvents)
|
|
425
|
+
.where(and(eq(sessionEvents.tenantId, tenantId), eq(sessionEvents.sessionId, sessionId)));
|
|
426
|
+
const sequence = (last[0]?.maxSeq ?? 0) + 1;
|
|
427
|
+
await this.drizzle
|
|
428
|
+
.insert(sessionEvents)
|
|
429
|
+
.values({ id, tenantId, sessionId, type, data, sequence, createdAt: now });
|
|
430
|
+
return { id, sessionId, tenantId, type, data, sequence, createdAt: now };
|
|
431
|
+
}
|
|
432
|
+
async insertSessionEvents(events) {
|
|
433
|
+
if (events.length === 0)
|
|
434
|
+
return [];
|
|
435
|
+
if (events.length === 1)
|
|
436
|
+
return [await this.insertSessionEvent(events[0].sessionId, events[0].type, events[0].data, events[0].tenantId)];
|
|
437
|
+
const { sessionEvents } = this.schema;
|
|
438
|
+
const results = [];
|
|
439
|
+
if (this.dialect === 'pg') {
|
|
440
|
+
// PG: use a transaction with raw SQL for atomic sequence assignment
|
|
441
|
+
await this.drizzle.transaction(async (tx) => {
|
|
442
|
+
for (const ev of events) {
|
|
443
|
+
const id = randomUUID();
|
|
444
|
+
const now = new Date().toISOString();
|
|
445
|
+
const tenantId = ev.tenantId ?? 'default';
|
|
446
|
+
const rows = await tx.execute(sql `INSERT INTO session_events (id, tenant_id, session_id, type, data, sequence, created_at)
|
|
447
|
+
VALUES (${id}, ${tenantId}, ${ev.sessionId}, ${ev.type}, ${ev.data},
|
|
448
|
+
COALESCE((SELECT MAX(sequence) FROM session_events WHERE tenant_id = ${tenantId} AND session_id = ${ev.sessionId}), 0) + 1,
|
|
449
|
+
${now})
|
|
450
|
+
RETURNING sequence`);
|
|
451
|
+
results.push({ id, sessionId: ev.sessionId, tenantId, type: ev.type, data: ev.data, sequence: rows.rows[0].sequence, createdAt: now });
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
return results;
|
|
455
|
+
}
|
|
456
|
+
// SQLite: transaction with sequential sequence computation
|
|
457
|
+
return this.drizzle.transaction((tx) => {
|
|
458
|
+
const groups = new Map();
|
|
459
|
+
for (const ev of events) {
|
|
460
|
+
const tenantId = ev.tenantId ?? 'default';
|
|
461
|
+
const key = `${tenantId}:${ev.sessionId}`;
|
|
462
|
+
if (!groups.has(key)) {
|
|
463
|
+
const last = tx
|
|
464
|
+
.select({ maxSeq: sql `MAX(${sessionEvents.sequence})` })
|
|
465
|
+
.from(sessionEvents)
|
|
466
|
+
.where(and(eq(sessionEvents.tenantId, tenantId), eq(sessionEvents.sessionId, ev.sessionId)))
|
|
467
|
+
.get();
|
|
468
|
+
groups.set(key, last?.maxSeq ?? 0);
|
|
469
|
+
}
|
|
470
|
+
const sequence = groups.get(key) + 1;
|
|
471
|
+
groups.set(key, sequence);
|
|
472
|
+
const id = randomUUID();
|
|
473
|
+
const now = new Date().toISOString();
|
|
474
|
+
tx.insert(sessionEvents)
|
|
475
|
+
.values({ id, tenantId, sessionId: ev.sessionId, type: ev.type, data: ev.data, sequence, createdAt: now })
|
|
476
|
+
.run();
|
|
477
|
+
results.push({ id, sessionId: ev.sessionId, tenantId, type: ev.type, data: ev.data, sequence, createdAt: now });
|
|
478
|
+
}
|
|
479
|
+
return results;
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
async listSessionEvents(sessionId, tenantId = 'default', opts) {
|
|
483
|
+
const { sessionEvents } = this.schema;
|
|
484
|
+
const limit = opts?.limit ?? 200;
|
|
485
|
+
const afterSeq = opts?.afterSequence ?? 0;
|
|
486
|
+
const conditions = [
|
|
487
|
+
eq(sessionEvents.tenantId, tenantId),
|
|
488
|
+
eq(sessionEvents.sessionId, sessionId),
|
|
489
|
+
gt(sessionEvents.sequence, afterSeq),
|
|
490
|
+
];
|
|
491
|
+
if (opts?.type) {
|
|
492
|
+
conditions.push(eq(sessionEvents.type, opts.type));
|
|
493
|
+
}
|
|
494
|
+
const rows = await this.drizzle
|
|
495
|
+
.select()
|
|
496
|
+
.from(sessionEvents)
|
|
497
|
+
.where(and(...conditions))
|
|
498
|
+
.orderBy(asc(sessionEvents.sequence))
|
|
499
|
+
.limit(limit);
|
|
500
|
+
return rows.map((r) => ({
|
|
501
|
+
id: r.id, sessionId: r.sessionId, tenantId: r.tenantId,
|
|
502
|
+
type: r.type, data: r.data, sequence: r.sequence, createdAt: r.createdAt,
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
505
|
+
// -- API Keys ---------------------------------------------------------------
|
|
506
|
+
async getApiKeyByHash(keyHash) {
|
|
507
|
+
const { apiKeys } = this.schema;
|
|
508
|
+
const rows = await this.drizzle
|
|
509
|
+
.select()
|
|
510
|
+
.from(apiKeys)
|
|
511
|
+
.where(eq(apiKeys.keyHash, keyHash))
|
|
512
|
+
.limit(1);
|
|
513
|
+
if (rows.length === 0)
|
|
514
|
+
return null;
|
|
515
|
+
const r = rows[0];
|
|
516
|
+
return { id: r.id, tenantId: r.tenantId, keyHash: r.keyHash, label: r.label, createdAt: r.createdAt };
|
|
517
|
+
}
|
|
518
|
+
async insertApiKey(id, tenantId, keyHash, label) {
|
|
519
|
+
const { apiKeys } = this.schema;
|
|
520
|
+
const now = new Date().toISOString();
|
|
521
|
+
await this.drizzle
|
|
522
|
+
.insert(apiKeys)
|
|
523
|
+
.values({ id, tenantId, keyHash, label, createdAt: now });
|
|
524
|
+
return { id, tenantId, keyHash, label, createdAt: now };
|
|
525
|
+
}
|
|
526
|
+
async listApiKeysByTenant(tenantId) {
|
|
527
|
+
const { apiKeys } = this.schema;
|
|
528
|
+
const rows = await this.drizzle
|
|
529
|
+
.select()
|
|
530
|
+
.from(apiKeys)
|
|
531
|
+
.where(eq(apiKeys.tenantId, tenantId))
|
|
532
|
+
.orderBy(desc(apiKeys.createdAt));
|
|
533
|
+
return rows.map((r) => ({ id: r.id, tenantId: r.tenantId, keyHash: r.keyHash, label: r.label, createdAt: r.createdAt }));
|
|
534
|
+
}
|
|
535
|
+
async deleteApiKey(id) {
|
|
536
|
+
const { apiKeys } = this.schema;
|
|
537
|
+
const result = await this.drizzle
|
|
538
|
+
.delete(apiKeys)
|
|
539
|
+
.where(eq(apiKeys.id, id));
|
|
540
|
+
return (result.changes ?? result.rowCount ?? 0) > 0;
|
|
541
|
+
}
|
|
542
|
+
// -- Credentials ------------------------------------------------------------
|
|
543
|
+
async insertCredential(id, tenantId, type, encryptedKey, iv, authTag, label) {
|
|
544
|
+
const { credentials } = this.schema;
|
|
545
|
+
const now = new Date().toISOString();
|
|
546
|
+
await this.drizzle
|
|
547
|
+
.insert(credentials)
|
|
548
|
+
.values({ id, tenantId, type, encryptedKey, iv, authTag, label, active: 1, createdAt: now, lastUsedAt: null });
|
|
549
|
+
return { id, tenantId, type: type, label, active: true, createdAt: now, lastUsedAt: null };
|
|
550
|
+
}
|
|
551
|
+
async getCredential(id) {
|
|
552
|
+
const { credentials } = this.schema;
|
|
553
|
+
const rows = await this.drizzle
|
|
554
|
+
.select()
|
|
555
|
+
.from(credentials)
|
|
556
|
+
.where(eq(credentials.id, id))
|
|
557
|
+
.limit(1);
|
|
558
|
+
if (rows.length === 0)
|
|
559
|
+
return null;
|
|
560
|
+
const r = rows[0];
|
|
561
|
+
return { id: r.id, tenantId: r.tenantId, type: r.type, encryptedKey: r.encryptedKey, iv: r.iv, authTag: r.authTag, label: r.label, active: r.active === 1, createdAt: r.createdAt, lastUsedAt: r.lastUsedAt };
|
|
562
|
+
}
|
|
563
|
+
async listCredentials(tenantId) {
|
|
564
|
+
const { credentials } = this.schema;
|
|
565
|
+
const rows = await this.drizzle
|
|
566
|
+
.select()
|
|
567
|
+
.from(credentials)
|
|
568
|
+
.where(eq(credentials.tenantId, tenantId))
|
|
569
|
+
.orderBy(desc(credentials.createdAt));
|
|
570
|
+
return rows.map((r) => ({
|
|
571
|
+
id: r.id, tenantId: r.tenantId, type: r.type, label: r.label, active: r.active === 1, createdAt: r.createdAt, lastUsedAt: r.lastUsedAt,
|
|
572
|
+
}));
|
|
573
|
+
}
|
|
574
|
+
async deleteCredential(id) {
|
|
575
|
+
const { credentials } = this.schema;
|
|
576
|
+
const result = await this.drizzle
|
|
577
|
+
.delete(credentials)
|
|
578
|
+
.where(eq(credentials.id, id));
|
|
579
|
+
return (result.changes ?? result.rowCount ?? 0) > 0;
|
|
580
|
+
}
|
|
581
|
+
async touchCredentialUsed(id) {
|
|
582
|
+
const { credentials } = this.schema;
|
|
583
|
+
const now = new Date().toISOString();
|
|
584
|
+
await this.drizzle
|
|
585
|
+
.update(credentials)
|
|
586
|
+
.set({ lastUsedAt: now })
|
|
587
|
+
.where(eq(credentials.id, id));
|
|
588
|
+
}
|
|
589
|
+
// -- Queue ------------------------------------------------------------------
|
|
590
|
+
async insertQueueItem(id, tenantId, agentName, prompt, sessionId, priority, maxRetries) {
|
|
591
|
+
const { queueItems } = this.schema;
|
|
592
|
+
const now = new Date().toISOString();
|
|
593
|
+
const item = { id, tenantId, sessionId: sessionId ?? null, agentName, prompt, status: 'pending', priority: priority ?? 0, retryCount: 0, maxRetries: maxRetries ?? 3, error: null, createdAt: now, startedAt: null, completedAt: null };
|
|
594
|
+
await this.drizzle.insert(queueItems).values(item);
|
|
595
|
+
return item;
|
|
596
|
+
}
|
|
597
|
+
async getQueueItem(id) {
|
|
598
|
+
const { queueItems } = this.schema;
|
|
599
|
+
const rows = await this.drizzle.select().from(queueItems).where(eq(queueItems.id, id)).limit(1);
|
|
600
|
+
if (rows.length === 0)
|
|
601
|
+
return null;
|
|
602
|
+
return rows[0];
|
|
603
|
+
}
|
|
604
|
+
async getNextPendingQueueItem(tenantId) {
|
|
605
|
+
const { queueItems } = this.schema;
|
|
606
|
+
const now = new Date().toISOString();
|
|
607
|
+
const conditions = [
|
|
608
|
+
eq(queueItems.status, 'pending'),
|
|
609
|
+
// Only return items that are eligible (no retryAfter or retryAfter has passed)
|
|
610
|
+
sql `(${queueItems.retryAfter} IS NULL OR ${queueItems.retryAfter} <= ${now})`,
|
|
611
|
+
];
|
|
612
|
+
if (tenantId)
|
|
613
|
+
conditions.push(eq(queueItems.tenantId, tenantId));
|
|
614
|
+
const rows = await this.drizzle.select().from(queueItems).where(and(...conditions)).orderBy(desc(queueItems.priority), asc(queueItems.createdAt)).limit(1);
|
|
615
|
+
if (rows.length === 0)
|
|
616
|
+
return null;
|
|
617
|
+
return rows[0];
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Atomically claim a queue item by setting status to 'processing'
|
|
621
|
+
* only if it is still 'pending'. Returns true if the claim succeeded.
|
|
622
|
+
*/
|
|
623
|
+
async claimQueueItem(id) {
|
|
624
|
+
const { queueItems } = this.schema;
|
|
625
|
+
const now = new Date().toISOString();
|
|
626
|
+
const result = await this.drizzle
|
|
627
|
+
.update(queueItems)
|
|
628
|
+
.set({ status: 'processing', startedAt: now })
|
|
629
|
+
.where(and(eq(queueItems.id, id), eq(queueItems.status, 'pending')));
|
|
630
|
+
return (result.changes ?? result.rowCount ?? 0) > 0;
|
|
631
|
+
}
|
|
632
|
+
async updateQueueItemStatus(id, status, error) {
|
|
633
|
+
const { queueItems } = this.schema;
|
|
634
|
+
const now = new Date().toISOString();
|
|
635
|
+
const set = { status };
|
|
636
|
+
if (status === 'processing')
|
|
637
|
+
set.startedAt = now;
|
|
638
|
+
if (status === 'completed' || status === 'failed')
|
|
639
|
+
set.completedAt = now;
|
|
640
|
+
if (error !== undefined)
|
|
641
|
+
set.error = error;
|
|
642
|
+
await this.drizzle.update(queueItems).set(set).where(eq(queueItems.id, id));
|
|
643
|
+
}
|
|
644
|
+
async incrementQueueItemRetry(id, retryAfter) {
|
|
645
|
+
const { queueItems } = this.schema;
|
|
646
|
+
const set = { retryCount: sql `${queueItems.retryCount} + 1` };
|
|
647
|
+
if (retryAfter)
|
|
648
|
+
set.retryAfter = retryAfter;
|
|
649
|
+
await this.drizzle
|
|
650
|
+
.update(queueItems)
|
|
651
|
+
.set(set)
|
|
652
|
+
.where(eq(queueItems.id, id));
|
|
653
|
+
}
|
|
654
|
+
async listQueueItems(tenantId, status, limit = 50) {
|
|
655
|
+
const { queueItems } = this.schema;
|
|
656
|
+
const conditions = [eq(queueItems.tenantId, tenantId)];
|
|
657
|
+
if (status)
|
|
658
|
+
conditions.push(eq(queueItems.status, status));
|
|
659
|
+
const rows = await this.drizzle.select().from(queueItems).where(and(...conditions)).orderBy(desc(queueItems.createdAt)).limit(limit);
|
|
660
|
+
return rows;
|
|
661
|
+
}
|
|
662
|
+
async getQueueStats(tenantId) {
|
|
663
|
+
const { queueItems } = this.schema;
|
|
664
|
+
const rows = await this.drizzle
|
|
665
|
+
.select({ status: queueItems.status, count: sql `count(*)` })
|
|
666
|
+
.from(queueItems)
|
|
667
|
+
.where(eq(queueItems.tenantId, tenantId))
|
|
668
|
+
.groupBy(queueItems.status);
|
|
669
|
+
const stats = { pending: 0, processing: 0, completed: 0, failed: 0, cancelled: 0 };
|
|
670
|
+
for (const r of rows) {
|
|
671
|
+
const s = r.status;
|
|
672
|
+
if (s in stats)
|
|
673
|
+
stats[s] = Number(r.count);
|
|
674
|
+
}
|
|
675
|
+
return stats;
|
|
676
|
+
}
|
|
677
|
+
// -- Attachments -------------------------------------------------------------
|
|
678
|
+
async insertAttachment(id, tenantId, messageId, sessionId, filename, mimeType, size, storagePath) {
|
|
679
|
+
const { attachments } = this.schema;
|
|
680
|
+
const now = new Date().toISOString();
|
|
681
|
+
const row = { id, tenantId, messageId, sessionId, filename, mimeType, size, storagePath, createdAt: now };
|
|
682
|
+
await this.drizzle.insert(attachments).values(row);
|
|
683
|
+
return row;
|
|
684
|
+
}
|
|
685
|
+
async getAttachment(id) {
|
|
686
|
+
const { attachments } = this.schema;
|
|
687
|
+
const rows = await this.drizzle.select().from(attachments).where(eq(attachments.id, id)).limit(1);
|
|
688
|
+
if (rows.length === 0)
|
|
689
|
+
return null;
|
|
690
|
+
return rows[0];
|
|
691
|
+
}
|
|
692
|
+
async listAttachmentsByMessage(messageId, tenantId) {
|
|
693
|
+
const { attachments } = this.schema;
|
|
694
|
+
const conditions = [eq(attachments.messageId, messageId)];
|
|
695
|
+
if (tenantId)
|
|
696
|
+
conditions.push(eq(attachments.tenantId, tenantId));
|
|
697
|
+
const rows = await this.drizzle.select().from(attachments).where(and(...conditions)).orderBy(asc(attachments.createdAt));
|
|
698
|
+
return rows;
|
|
699
|
+
}
|
|
700
|
+
async listAttachmentsBySession(sessionId, tenantId) {
|
|
701
|
+
const { attachments } = this.schema;
|
|
702
|
+
const conditions = [eq(attachments.sessionId, sessionId)];
|
|
703
|
+
if (tenantId)
|
|
704
|
+
conditions.push(eq(attachments.tenantId, tenantId));
|
|
705
|
+
const rows = await this.drizzle.select().from(attachments).where(and(...conditions)).orderBy(asc(attachments.createdAt));
|
|
706
|
+
return rows;
|
|
707
|
+
}
|
|
708
|
+
async deleteAttachment(id) {
|
|
709
|
+
const { attachments } = this.schema;
|
|
710
|
+
const result = await this.drizzle.delete(attachments).where(eq(attachments.id, id));
|
|
711
|
+
return (result?.rowsAffected ?? result?.changes ?? 0) > 0;
|
|
712
|
+
}
|
|
713
|
+
// -- Usage ------------------------------------------------------------------
|
|
714
|
+
async insertUsageEvent(id, tenantId, sessionId, agentName, eventType, value) {
|
|
715
|
+
const { usageEvents } = this.schema;
|
|
716
|
+
const now = new Date().toISOString();
|
|
717
|
+
const row = { id, tenantId, sessionId, agentName, eventType, value, createdAt: now };
|
|
718
|
+
await this.drizzle.insert(usageEvents).values(row);
|
|
719
|
+
return row;
|
|
720
|
+
}
|
|
721
|
+
async insertUsageEvents(events) {
|
|
722
|
+
if (events.length === 0)
|
|
723
|
+
return;
|
|
724
|
+
const { usageEvents } = this.schema;
|
|
725
|
+
const now = new Date().toISOString();
|
|
726
|
+
await this.drizzle.insert(usageEvents).values(events.map(e => ({ ...e, createdAt: now })));
|
|
727
|
+
}
|
|
728
|
+
async listUsageEvents(tenantId, opts) {
|
|
729
|
+
const { usageEvents } = this.schema;
|
|
730
|
+
const conditions = [eq(usageEvents.tenantId, tenantId)];
|
|
731
|
+
if (opts?.sessionId)
|
|
732
|
+
conditions.push(eq(usageEvents.sessionId, opts.sessionId));
|
|
733
|
+
if (opts?.agentName)
|
|
734
|
+
conditions.push(eq(usageEvents.agentName, opts.agentName));
|
|
735
|
+
if (opts?.after)
|
|
736
|
+
conditions.push(sql `${usageEvents.createdAt} >= ${opts.after}`);
|
|
737
|
+
if (opts?.before)
|
|
738
|
+
conditions.push(sql `${usageEvents.createdAt} <= ${opts.before}`);
|
|
739
|
+
const limit = opts?.limit ?? 100;
|
|
740
|
+
const rows = await this.drizzle.select().from(usageEvents).where(and(...conditions)).orderBy(desc(usageEvents.createdAt)).limit(limit);
|
|
741
|
+
return rows;
|
|
742
|
+
}
|
|
743
|
+
async getUsageStats(tenantId, opts) {
|
|
744
|
+
const { usageEvents } = this.schema;
|
|
745
|
+
const conditions = [eq(usageEvents.tenantId, tenantId)];
|
|
746
|
+
if (opts?.sessionId)
|
|
747
|
+
conditions.push(eq(usageEvents.sessionId, opts.sessionId));
|
|
748
|
+
if (opts?.agentName)
|
|
749
|
+
conditions.push(eq(usageEvents.agentName, opts.agentName));
|
|
750
|
+
if (opts?.after)
|
|
751
|
+
conditions.push(sql `${usageEvents.createdAt} >= ${opts.after}`);
|
|
752
|
+
if (opts?.before)
|
|
753
|
+
conditions.push(sql `${usageEvents.createdAt} <= ${opts.before}`);
|
|
754
|
+
const rows = await this.drizzle
|
|
755
|
+
.select({ eventType: usageEvents.eventType, total: sql `sum(${usageEvents.value})` })
|
|
756
|
+
.from(usageEvents)
|
|
757
|
+
.where(and(...conditions))
|
|
758
|
+
.groupBy(usageEvents.eventType);
|
|
759
|
+
const stats = {
|
|
760
|
+
totalInputTokens: 0,
|
|
761
|
+
totalOutputTokens: 0,
|
|
762
|
+
totalCacheCreationTokens: 0,
|
|
763
|
+
totalCacheReadTokens: 0,
|
|
764
|
+
totalToolCalls: 0,
|
|
765
|
+
totalMessages: 0,
|
|
766
|
+
totalComputeSeconds: 0,
|
|
767
|
+
};
|
|
768
|
+
const map = {
|
|
769
|
+
input_tokens: 'totalInputTokens',
|
|
770
|
+
output_tokens: 'totalOutputTokens',
|
|
771
|
+
cache_creation_tokens: 'totalCacheCreationTokens',
|
|
772
|
+
cache_read_tokens: 'totalCacheReadTokens',
|
|
773
|
+
tool_call: 'totalToolCalls',
|
|
774
|
+
message: 'totalMessages',
|
|
775
|
+
compute_seconds: 'totalComputeSeconds',
|
|
776
|
+
};
|
|
777
|
+
for (const r of rows) {
|
|
778
|
+
const key = map[r.eventType];
|
|
779
|
+
if (key)
|
|
780
|
+
stats[key] = Number(r.total);
|
|
781
|
+
}
|
|
782
|
+
return stats;
|
|
783
|
+
}
|
|
784
|
+
// -- Lifecycle --------------------------------------------------------------
|
|
785
|
+
async close() {
|
|
786
|
+
await this.closeFn();
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
//# sourceMappingURL=drizzle-db.js.map
|