vellum 0.2.7 → 0.2.9
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/bun.lock +4 -4
- package/package.json +4 -3
- package/src/__tests__/asset-materialize-tool.test.ts +2 -2
- package/src/__tests__/checker.test.ts +104 -0
- package/src/__tests__/config-schema.test.ts +0 -6
- package/src/__tests__/forbidden-legacy-symbols.test.ts +69 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +538 -0
- package/src/__tests__/ingress-url-consistency.test.ts +214 -0
- package/src/__tests__/ipc-snapshot.test.ts +17 -5
- package/src/__tests__/oauth-callback-registry.test.ts +85 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +304 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +51 -12
- package/src/__tests__/public-ingress-urls.test.ts +222 -0
- package/src/__tests__/runtime-events-sse-parity.test.ts +343 -0
- package/src/__tests__/runtime-events-sse.test.ts +162 -0
- package/src/__tests__/tool-executor.test.ts +88 -0
- package/src/__tests__/turn-commit.test.ts +64 -0
- package/src/__tests__/twilio-provider.test.ts +1 -1
- package/src/__tests__/twilio-routes.test.ts +4 -4
- package/src/__tests__/twitter-auth-handler.test.ts +87 -2
- package/src/calls/call-domain.ts +8 -6
- package/src/calls/twilio-config.ts +18 -3
- package/src/calls/twilio-routes.ts +10 -2
- package/src/config/bundled-skills/tasks/TOOLS.json +25 -0
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +9 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +25 -0
- package/src/config/bundled-skills/transcribe/TOOLS.json +32 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +370 -0
- package/src/config/defaults.ts +4 -1
- package/src/config/schema.ts +30 -6
- package/src/config/system-prompt.ts +1 -1
- package/src/config/types.ts +1 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +5 -4
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -2
- package/src/config/vellum-skills/telegram-setup/SKILL.md +3 -3
- package/src/daemon/computer-use-session.ts +2 -1
- package/src/daemon/handlers/config.ts +49 -17
- package/src/daemon/handlers/sessions.ts +2 -2
- package/src/daemon/handlers/shared.ts +1 -0
- package/src/daemon/handlers/subagents.ts +85 -2
- package/src/daemon/handlers/twitter-auth.ts +31 -2
- package/src/daemon/handlers/work-items.ts +1 -1
- package/src/daemon/ipc-contract-inventory.json +8 -4
- package/src/daemon/ipc-contract.ts +34 -15
- package/src/daemon/lifecycle.ts +9 -4
- package/src/daemon/server.ts +7 -0
- package/src/daemon/session-tool-setup.ts +8 -1
- package/src/inbound/public-ingress-urls.ts +112 -0
- package/src/memory/attachments-store.ts +0 -1
- package/src/memory/channel-delivery-store.ts +0 -1
- package/src/memory/conversation-key-store.ts +0 -1
- package/src/memory/db.ts +472 -148
- package/src/memory/llm-usage-store.ts +0 -1
- package/src/memory/runs-store.ts +51 -6
- package/src/memory/schema.ts +2 -6
- package/src/runtime/gateway-client.ts +7 -1
- package/src/runtime/http-server.ts +174 -7
- package/src/runtime/routes/channel-routes.ts +7 -2
- package/src/runtime/routes/events-routes.ts +79 -0
- package/src/runtime/routes/run-routes.ts +43 -0
- package/src/runtime/run-orchestrator.ts +64 -7
- package/src/security/oauth-callback-registry.ts +66 -0
- package/src/security/oauth2.ts +208 -58
- package/src/subagent/manager.ts +3 -1
- package/src/swarm/backend-claude-code.ts +1 -1
- package/src/tools/assets/search.ts +1 -36
- package/src/tools/claude-code/claude-code.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +16 -2
- package/src/tools/tasks/work-item-run.ts +78 -0
- package/src/util/platform.ts +1 -1
- package/src/work-items/work-item-runner.ts +171 -0
- package/src/workspace/provider-commit-message-generator.ts +39 -23
- package/src/workspace/turn-commit.ts +6 -2
- package/src/__tests__/handlers-twilio-config.test.ts +0 -221
- package/src/calls/__tests__/twilio-webhook-urls.test.ts +0 -162
- package/src/calls/twilio-webhook-urls.ts +0 -50
package/src/memory/db.ts
CHANGED
|
@@ -184,7 +184,6 @@ export function initializeDb(): void {
|
|
|
184
184
|
database.run(/*sql*/ `
|
|
185
185
|
CREATE TABLE IF NOT EXISTS attachments (
|
|
186
186
|
id TEXT PRIMARY KEY,
|
|
187
|
-
assistant_id TEXT NOT NULL,
|
|
188
187
|
original_filename TEXT NOT NULL,
|
|
189
188
|
mime_type TEXT NOT NULL,
|
|
190
189
|
size_bytes INTEGER NOT NULL,
|
|
@@ -223,7 +222,6 @@ export function initializeDb(): void {
|
|
|
223
222
|
database.run(/*sql*/ `
|
|
224
223
|
CREATE TABLE IF NOT EXISTS channel_inbound_events (
|
|
225
224
|
id TEXT PRIMARY KEY,
|
|
226
|
-
assistant_id TEXT NOT NULL,
|
|
227
225
|
source_channel TEXT NOT NULL,
|
|
228
226
|
external_chat_id TEXT NOT NULL,
|
|
229
227
|
external_message_id TEXT NOT NULL,
|
|
@@ -232,18 +230,18 @@ export function initializeDb(): void {
|
|
|
232
230
|
delivery_status TEXT NOT NULL DEFAULT 'pending',
|
|
233
231
|
created_at INTEGER NOT NULL,
|
|
234
232
|
updated_at INTEGER NOT NULL,
|
|
235
|
-
UNIQUE (
|
|
233
|
+
UNIQUE (source_channel, external_chat_id, external_message_id)
|
|
236
234
|
)
|
|
237
235
|
`);
|
|
238
236
|
|
|
239
237
|
database.run(/*sql*/ `
|
|
240
238
|
CREATE TABLE IF NOT EXISTS message_runs (
|
|
241
239
|
id TEXT PRIMARY KEY,
|
|
242
|
-
assistant_id TEXT NOT NULL,
|
|
243
240
|
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
244
241
|
message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
|
|
245
242
|
status TEXT NOT NULL DEFAULT 'running',
|
|
246
243
|
pending_confirmation TEXT,
|
|
244
|
+
pending_secret TEXT,
|
|
247
245
|
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
248
246
|
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
249
247
|
estimated_cost REAL NOT NULL DEFAULT 0,
|
|
@@ -253,6 +251,8 @@ export function initializeDb(): void {
|
|
|
253
251
|
)
|
|
254
252
|
`);
|
|
255
253
|
|
|
254
|
+
try { database.run(/*sql*/ `ALTER TABLE message_runs ADD COLUMN pending_secret TEXT`); } catch (e) { log.debug({ err: e }, 'ALTER TABLE message_runs ADD COLUMN pending_secret (likely already exists)'); }
|
|
255
|
+
|
|
256
256
|
database.run(/*sql*/ `
|
|
257
257
|
CREATE TABLE IF NOT EXISTS reminders (
|
|
258
258
|
id TEXT PRIMARY KEY,
|
|
@@ -421,7 +421,6 @@ export function initializeDb(): void {
|
|
|
421
421
|
CREATE TABLE IF NOT EXISTS llm_usage_events (
|
|
422
422
|
id TEXT PRIMARY KEY,
|
|
423
423
|
created_at INTEGER NOT NULL,
|
|
424
|
-
assistant_id TEXT,
|
|
425
424
|
conversation_id TEXT,
|
|
426
425
|
run_id TEXT,
|
|
427
426
|
request_id TEXT,
|
|
@@ -507,11 +506,9 @@ export function initializeDb(): void {
|
|
|
507
506
|
database.run(/*sql*/ `
|
|
508
507
|
CREATE TABLE IF NOT EXISTS conversation_keys (
|
|
509
508
|
id TEXT PRIMARY KEY,
|
|
510
|
-
|
|
511
|
-
conversation_key TEXT NOT NULL,
|
|
509
|
+
conversation_key TEXT NOT NULL UNIQUE,
|
|
512
510
|
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
513
|
-
created_at INTEGER NOT NULL
|
|
514
|
-
UNIQUE (assistant_id, conversation_key)
|
|
511
|
+
created_at INTEGER NOT NULL
|
|
515
512
|
)
|
|
516
513
|
`);
|
|
517
514
|
|
|
@@ -552,6 +549,8 @@ export function initializeDb(): void {
|
|
|
552
549
|
migrateMemoryItemsFingerprintScopeUnique(database);
|
|
553
550
|
migrateMemoryItemsScopeSaltedFingerprints(database);
|
|
554
551
|
migrateAssistantIdToSelf(database);
|
|
552
|
+
migrateRemoveAssistantIdColumns(database);
|
|
553
|
+
migrateLlmUsageEventsDropAssistantId(database);
|
|
555
554
|
|
|
556
555
|
// Indexes for query performance on large datasets
|
|
557
556
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_request_logs_conv_created ON llm_request_logs(conversation_id, created_at)`);
|
|
@@ -592,17 +591,16 @@ export function initializeDb(): void {
|
|
|
592
591
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_segments_scope_id ON memory_segments(scope_id)`);
|
|
593
592
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_items_scope_id ON memory_items(scope_id)`);
|
|
594
593
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_summaries_scope_id ON memory_summaries(scope_id)`);
|
|
595
|
-
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS
|
|
596
|
-
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS
|
|
597
|
-
database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_attachments_content_dedup ON attachments(assistant_id, content_hash) WHERE content_hash IS NOT NULL`);
|
|
594
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_conversation_keys_key ON conversation_keys(conversation_key)`);
|
|
595
|
+
database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_attachments_content_dedup ON attachments(content_hash) WHERE content_hash IS NOT NULL`);
|
|
598
596
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_message_attachments_message_id ON message_attachments(message_id)`);
|
|
599
597
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_message_attachments_attachment_id ON message_attachments(attachment_id)`);
|
|
600
|
-
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_inbound_events_lookup ON channel_inbound_events(
|
|
598
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_inbound_events_lookup ON channel_inbound_events(source_channel, external_chat_id, external_message_id)`);
|
|
601
599
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_inbound_events_conversation ON channel_inbound_events(conversation_id)`);
|
|
602
|
-
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_inbound_events_source_msg ON channel_inbound_events(
|
|
600
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_inbound_events_source_msg ON channel_inbound_events(source_channel, external_chat_id, source_message_id)`);
|
|
603
601
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_inbound_events_processing_retry ON channel_inbound_events(processing_status, retry_after)`);
|
|
604
602
|
|
|
605
|
-
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS
|
|
603
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_message_runs_status ON message_runs(status)`);
|
|
606
604
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_message_runs_conversation ON message_runs(conversation_id)`);
|
|
607
605
|
|
|
608
606
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_reminders_status_fire_at ON reminders(status, fire_at)`);
|
|
@@ -615,7 +613,6 @@ export function initializeDb(): void {
|
|
|
615
613
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_accounts_status ON accounts(status)`);
|
|
616
614
|
|
|
617
615
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_usage_events_created_at ON llm_usage_events(created_at)`);
|
|
618
|
-
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_usage_events_assistant_id ON llm_usage_events(assistant_id)`);
|
|
619
616
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_usage_events_provider ON llm_usage_events(provider)`);
|
|
620
617
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_usage_events_model ON llm_usage_events(model)`);
|
|
621
618
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_usage_events_actor ON llm_usage_events(actor)`);
|
|
@@ -1224,119 +1221,162 @@ function migrateAssistantIdToSelf(database: ReturnType<typeof drizzle<typeof sch
|
|
|
1224
1221
|
).get(checkpointKey);
|
|
1225
1222
|
if (checkpoint) return;
|
|
1226
1223
|
|
|
1224
|
+
// On fresh installs the tables are created without assistant_id (PR 7+). Skip the
|
|
1225
|
+
// migration if NONE of the four affected tables have the column — pre-seed the
|
|
1226
|
+
// checkpoint so subsequent startups are also skipped. Checking all four (not just
|
|
1227
|
+
// conversation_keys) avoids a false negative on very old installs where
|
|
1228
|
+
// conversation_keys may not exist yet but other tables still carry assistant_id data.
|
|
1229
|
+
const affectedTables = ['conversation_keys', 'attachments', 'channel_inbound_events', 'message_runs'];
|
|
1230
|
+
const anyHasAssistantId = affectedTables.some((tbl) => {
|
|
1231
|
+
const ddl = raw.query(
|
|
1232
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?`,
|
|
1233
|
+
).get(tbl) as { sql: string } | null;
|
|
1234
|
+
return ddl?.sql.includes('assistant_id') ?? false;
|
|
1235
|
+
});
|
|
1236
|
+
if (!anyHasAssistantId) {
|
|
1237
|
+
raw.query(
|
|
1238
|
+
`INSERT OR IGNORE INTO memory_checkpoints (key, value, updated_at) VALUES (?, '1', ?)`,
|
|
1239
|
+
).run(checkpointKey, Date.now());
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Helper: returns true if the given table's current DDL contains 'assistant_id'.
|
|
1244
|
+
const tableHasAssistantId = (tbl: string): boolean => {
|
|
1245
|
+
const ddl = raw.query(
|
|
1246
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?`,
|
|
1247
|
+
).get(tbl) as { sql: string } | null;
|
|
1248
|
+
return ddl?.sql.includes('assistant_id') ?? false;
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1227
1251
|
try {
|
|
1228
1252
|
raw.exec('BEGIN');
|
|
1229
1253
|
|
|
1254
|
+
// Each section is guarded so that SQL referencing assistant_id is only executed
|
|
1255
|
+
// when the column still exists in that table. This handles mixed-schema states
|
|
1256
|
+
// (e.g., very old installs where some tables may already lack the column).
|
|
1257
|
+
|
|
1230
1258
|
// conversation_keys: UNIQUE (assistant_id, conversation_key)
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
WHERE ck_ns.assistant_id != 'self'
|
|
1254
|
-
AND ck_ns.conversation_key = conversation_keys.conversation_key
|
|
1255
|
-
ORDER BY ck_ns.rowid
|
|
1256
|
-
LIMIT 1
|
|
1257
|
-
)
|
|
1258
|
-
WHERE assistant_id = 'self'
|
|
1259
|
-
AND EXISTS (
|
|
1260
|
-
SELECT 1 FROM conversation_keys ck_ns
|
|
1259
|
+
if (tableHasAssistantId('conversation_keys')) {
|
|
1260
|
+
// Step 1: Among non-self rows, keep only one per conversation_key so the
|
|
1261
|
+
// bulk UPDATE cannot hit a (non-self-A, key) + (non-self-B, key) collision.
|
|
1262
|
+
raw.exec(/*sql*/ `
|
|
1263
|
+
DELETE FROM conversation_keys
|
|
1264
|
+
WHERE assistant_id != 'self'
|
|
1265
|
+
AND rowid NOT IN (
|
|
1266
|
+
SELECT MIN(rowid) FROM conversation_keys
|
|
1267
|
+
WHERE assistant_id != 'self'
|
|
1268
|
+
GROUP BY conversation_key
|
|
1269
|
+
)
|
|
1270
|
+
`);
|
|
1271
|
+
// Step 2: For 'self' rows that have a non-self counterpart with the same
|
|
1272
|
+
// conversation_key, update the 'self' row to use the non-self row's
|
|
1273
|
+
// conversation_id. This preserves the historical conversation (which
|
|
1274
|
+
// has the message history from before the route change) rather than
|
|
1275
|
+
// discarding it in favour of a potentially-empty 'self' conversation.
|
|
1276
|
+
raw.exec(/*sql*/ `
|
|
1277
|
+
UPDATE conversation_keys
|
|
1278
|
+
SET conversation_id = (
|
|
1279
|
+
SELECT ck_ns.conversation_id
|
|
1280
|
+
FROM conversation_keys ck_ns
|
|
1261
1281
|
WHERE ck_ns.assistant_id != 'self'
|
|
1262
1282
|
AND ck_ns.conversation_key = conversation_keys.conversation_key
|
|
1283
|
+
ORDER BY ck_ns.rowid
|
|
1284
|
+
LIMIT 1
|
|
1263
1285
|
)
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1286
|
+
WHERE assistant_id = 'self'
|
|
1287
|
+
AND EXISTS (
|
|
1288
|
+
SELECT 1 FROM conversation_keys ck_ns
|
|
1289
|
+
WHERE ck_ns.assistant_id != 'self'
|
|
1290
|
+
AND ck_ns.conversation_key = conversation_keys.conversation_key
|
|
1291
|
+
)
|
|
1292
|
+
`);
|
|
1293
|
+
// Step 3: Delete the now-redundant non-self rows (their conversation_ids
|
|
1294
|
+
// have been preserved in the 'self' rows above).
|
|
1295
|
+
raw.exec(/*sql*/ `
|
|
1296
|
+
DELETE FROM conversation_keys
|
|
1297
|
+
WHERE assistant_id != 'self'
|
|
1298
|
+
AND EXISTS (
|
|
1299
|
+
SELECT 1 FROM conversation_keys ck2
|
|
1300
|
+
WHERE ck2.assistant_id = 'self'
|
|
1301
|
+
AND ck2.conversation_key = conversation_keys.conversation_key
|
|
1302
|
+
)
|
|
1303
|
+
`);
|
|
1304
|
+
// Step 4: Remaining non-self rows have no 'self' counterpart — safe to bulk-update.
|
|
1305
|
+
raw.exec(/*sql*/ `
|
|
1306
|
+
UPDATE conversation_keys SET assistant_id = 'self' WHERE assistant_id != 'self'
|
|
1307
|
+
`);
|
|
1308
|
+
}
|
|
1280
1309
|
|
|
1281
1310
|
// attachments: UNIQUE (assistant_id, content_hash) WHERE content_hash IS NOT NULL
|
|
1282
1311
|
//
|
|
1283
1312
|
// message_attachments rows reference attachment IDs with ON DELETE CASCADE, so we
|
|
1284
1313
|
// must remap links to the surviving row BEFORE deleting duplicates to avoid
|
|
1285
1314
|
// silently dropping attachment metadata from messages.
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1315
|
+
if (tableHasAssistantId('attachments')) {
|
|
1316
|
+
// Step 1: Remap message_attachments from non-self duplicates to their survivor
|
|
1317
|
+
// (MIN rowid per content_hash group), then delete the duplicates.
|
|
1318
|
+
raw.exec(/*sql*/ `
|
|
1319
|
+
UPDATE message_attachments
|
|
1320
|
+
SET attachment_id = (
|
|
1321
|
+
SELECT a_survivor.id
|
|
1322
|
+
FROM attachments a_survivor
|
|
1323
|
+
WHERE a_survivor.assistant_id != 'self'
|
|
1324
|
+
AND a_survivor.content_hash = (
|
|
1325
|
+
SELECT a_dup.content_hash FROM attachments a_dup
|
|
1326
|
+
WHERE a_dup.id = message_attachments.attachment_id
|
|
1327
|
+
)
|
|
1328
|
+
ORDER BY a_survivor.rowid
|
|
1329
|
+
LIMIT 1
|
|
1330
|
+
)
|
|
1331
|
+
WHERE attachment_id IN (
|
|
1332
|
+
SELECT id FROM attachments
|
|
1333
|
+
WHERE assistant_id != 'self'
|
|
1334
|
+
AND content_hash IS NOT NULL
|
|
1335
|
+
AND rowid NOT IN (
|
|
1336
|
+
SELECT MIN(rowid) FROM attachments
|
|
1337
|
+
WHERE assistant_id != 'self' AND content_hash IS NOT NULL
|
|
1338
|
+
GROUP BY content_hash
|
|
1339
|
+
)
|
|
1340
|
+
)
|
|
1341
|
+
`);
|
|
1342
|
+
raw.exec(/*sql*/ `
|
|
1343
|
+
DELETE FROM attachments
|
|
1304
1344
|
WHERE assistant_id != 'self'
|
|
1305
1345
|
AND content_hash IS NOT NULL
|
|
1306
1346
|
AND rowid NOT IN (
|
|
1307
1347
|
SELECT MIN(rowid) FROM attachments
|
|
1308
|
-
WHERE assistant_id != 'self'
|
|
1348
|
+
WHERE assistant_id != 'self'
|
|
1349
|
+
AND content_hash IS NOT NULL
|
|
1309
1350
|
GROUP BY content_hash
|
|
1310
1351
|
)
|
|
1311
|
-
)
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1352
|
+
`);
|
|
1353
|
+
// Step 2: Remap message_attachments from non-self rows conflicting with a 'self'
|
|
1354
|
+
// row to the 'self' row, then delete the now-unlinked non-self rows.
|
|
1355
|
+
raw.exec(/*sql*/ `
|
|
1356
|
+
UPDATE message_attachments
|
|
1357
|
+
SET attachment_id = (
|
|
1358
|
+
SELECT a_self.id
|
|
1359
|
+
FROM attachments a_self
|
|
1360
|
+
WHERE a_self.assistant_id = 'self'
|
|
1361
|
+
AND a_self.content_hash = (
|
|
1362
|
+
SELECT a_ns.content_hash FROM attachments a_ns
|
|
1363
|
+
WHERE a_ns.id = message_attachments.attachment_id
|
|
1364
|
+
)
|
|
1365
|
+
LIMIT 1
|
|
1366
|
+
)
|
|
1367
|
+
WHERE attachment_id IN (
|
|
1368
|
+
SELECT id FROM attachments
|
|
1319
1369
|
WHERE assistant_id != 'self'
|
|
1320
1370
|
AND content_hash IS NOT NULL
|
|
1321
|
-
|
|
1371
|
+
AND EXISTS (
|
|
1372
|
+
SELECT 1 FROM attachments a2
|
|
1373
|
+
WHERE a2.assistant_id = 'self'
|
|
1374
|
+
AND a2.content_hash = attachments.content_hash
|
|
1375
|
+
)
|
|
1322
1376
|
)
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
raw.exec(/*sql*/ `
|
|
1327
|
-
UPDATE message_attachments
|
|
1328
|
-
SET attachment_id = (
|
|
1329
|
-
SELECT a_self.id
|
|
1330
|
-
FROM attachments a_self
|
|
1331
|
-
WHERE a_self.assistant_id = 'self'
|
|
1332
|
-
AND a_self.content_hash = (
|
|
1333
|
-
SELECT a_ns.content_hash FROM attachments a_ns
|
|
1334
|
-
WHERE a_ns.id = message_attachments.attachment_id
|
|
1335
|
-
)
|
|
1336
|
-
LIMIT 1
|
|
1337
|
-
)
|
|
1338
|
-
WHERE attachment_id IN (
|
|
1339
|
-
SELECT id FROM attachments
|
|
1377
|
+
`);
|
|
1378
|
+
raw.exec(/*sql*/ `
|
|
1379
|
+
DELETE FROM attachments
|
|
1340
1380
|
WHERE assistant_id != 'self'
|
|
1341
1381
|
AND content_hash IS NOT NULL
|
|
1342
1382
|
AND EXISTS (
|
|
@@ -1344,55 +1384,336 @@ function migrateAssistantIdToSelf(database: ReturnType<typeof drizzle<typeof sch
|
|
|
1344
1384
|
WHERE a2.assistant_id = 'self'
|
|
1345
1385
|
AND a2.content_hash = attachments.content_hash
|
|
1346
1386
|
)
|
|
1347
|
-
)
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
AND EXISTS (
|
|
1354
|
-
SELECT 1 FROM attachments a2
|
|
1355
|
-
WHERE a2.assistant_id = 'self'
|
|
1356
|
-
AND a2.content_hash = attachments.content_hash
|
|
1357
|
-
)
|
|
1358
|
-
`);
|
|
1359
|
-
// Step 3: Bulk-update remaining non-self rows.
|
|
1360
|
-
raw.exec(/*sql*/ `
|
|
1361
|
-
UPDATE attachments SET assistant_id = 'self' WHERE assistant_id != 'self'
|
|
1362
|
-
`);
|
|
1387
|
+
`);
|
|
1388
|
+
// Step 3: Bulk-update remaining non-self rows.
|
|
1389
|
+
raw.exec(/*sql*/ `
|
|
1390
|
+
UPDATE attachments SET assistant_id = 'self' WHERE assistant_id != 'self'
|
|
1391
|
+
`);
|
|
1392
|
+
}
|
|
1363
1393
|
|
|
1364
1394
|
// channel_inbound_events: UNIQUE (assistant_id, source_channel, external_chat_id, external_message_id)
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1395
|
+
if (tableHasAssistantId('channel_inbound_events')) {
|
|
1396
|
+
// Step 1: Dedup non-self rows sharing the same (source_channel, external_chat_id, external_message_id).
|
|
1397
|
+
raw.exec(/*sql*/ `
|
|
1398
|
+
DELETE FROM channel_inbound_events
|
|
1399
|
+
WHERE assistant_id != 'self'
|
|
1400
|
+
AND rowid NOT IN (
|
|
1401
|
+
SELECT MIN(rowid) FROM channel_inbound_events
|
|
1402
|
+
WHERE assistant_id != 'self'
|
|
1403
|
+
GROUP BY source_channel, external_chat_id, external_message_id
|
|
1404
|
+
)
|
|
1405
|
+
`);
|
|
1406
|
+
// Step 2: Delete non-self rows conflicting with existing 'self' rows.
|
|
1407
|
+
raw.exec(/*sql*/ `
|
|
1408
|
+
DELETE FROM channel_inbound_events
|
|
1409
|
+
WHERE assistant_id != 'self'
|
|
1410
|
+
AND EXISTS (
|
|
1411
|
+
SELECT 1 FROM channel_inbound_events e2
|
|
1412
|
+
WHERE e2.assistant_id = 'self'
|
|
1413
|
+
AND e2.source_channel = channel_inbound_events.source_channel
|
|
1414
|
+
AND e2.external_chat_id = channel_inbound_events.external_chat_id
|
|
1415
|
+
AND e2.external_message_id = channel_inbound_events.external_message_id
|
|
1416
|
+
)
|
|
1417
|
+
`);
|
|
1418
|
+
// Step 3: Bulk-update remaining non-self rows.
|
|
1419
|
+
raw.exec(/*sql*/ `
|
|
1420
|
+
UPDATE channel_inbound_events SET assistant_id = 'self' WHERE assistant_id != 'self'
|
|
1421
|
+
`);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// message_runs: no unique constraint on assistant_id — simple bulk update
|
|
1425
|
+
if (tableHasAssistantId('message_runs')) {
|
|
1426
|
+
raw.exec(/*sql*/ `
|
|
1427
|
+
UPDATE message_runs SET assistant_id = 'self' WHERE assistant_id != 'self'
|
|
1428
|
+
`);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
raw.query(
|
|
1432
|
+
`INSERT OR IGNORE INTO memory_checkpoints (key, value, updated_at) VALUES (?, '1', ?)`,
|
|
1433
|
+
).run(checkpointKey, Date.now());
|
|
1434
|
+
|
|
1435
|
+
raw.exec('COMMIT');
|
|
1436
|
+
} catch (e) {
|
|
1437
|
+
try { raw.exec('ROLLBACK'); } catch { /* no active transaction */ }
|
|
1438
|
+
throw e;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* One-shot migration: rebuild tables that previously stored assistant_id to remove
|
|
1444
|
+
* that column now that all rows are keyed to the implicit single-tenant identity ("self").
|
|
1445
|
+
*
|
|
1446
|
+
* Must run AFTER migrateAssistantIdToSelf (which normalises all values to "self")
|
|
1447
|
+
* so there are no constraint violations when recreating the tables without the
|
|
1448
|
+
* assistant_id dimension.
|
|
1449
|
+
*
|
|
1450
|
+
* Each table section is guarded by a DDL check so this is safe on fresh installs
|
|
1451
|
+
* where the column was never created in the first place.
|
|
1452
|
+
*
|
|
1453
|
+
* Tables rebuilt:
|
|
1454
|
+
* - conversation_keys UNIQUE (conversation_key)
|
|
1455
|
+
* - attachments no structural unique; content-dedup index updated
|
|
1456
|
+
* - channel_inbound_events UNIQUE (source_channel, external_chat_id, external_message_id)
|
|
1457
|
+
* - message_runs no unique constraint on assistant_id
|
|
1458
|
+
* - llm_usage_events nullable column with no constraint
|
|
1459
|
+
*/
|
|
1460
|
+
function migrateRemoveAssistantIdColumns(database: ReturnType<typeof drizzle<typeof schema>>): void {
|
|
1461
|
+
const raw = (database as unknown as { $client: Database }).$client;
|
|
1462
|
+
const checkpointKey = 'migration_remove_assistant_id_columns_v1';
|
|
1463
|
+
const checkpoint = raw.query(
|
|
1464
|
+
`SELECT 1 FROM memory_checkpoints WHERE key = ?`,
|
|
1465
|
+
).get(checkpointKey);
|
|
1466
|
+
if (checkpoint) return;
|
|
1467
|
+
|
|
1468
|
+
raw.exec('PRAGMA foreign_keys = OFF');
|
|
1469
|
+
try {
|
|
1470
|
+
raw.exec('BEGIN');
|
|
1471
|
+
|
|
1472
|
+
// --- conversation_keys ---
|
|
1473
|
+
const ckDdl = raw.query(
|
|
1474
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'conversation_keys'`,
|
|
1475
|
+
).get() as { sql: string } | null;
|
|
1476
|
+
if (ckDdl?.sql.includes('assistant_id')) {
|
|
1477
|
+
raw.exec(/*sql*/ `
|
|
1478
|
+
CREATE TABLE conversation_keys_new (
|
|
1479
|
+
id TEXT PRIMARY KEY,
|
|
1480
|
+
conversation_key TEXT NOT NULL UNIQUE,
|
|
1481
|
+
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
1482
|
+
created_at INTEGER NOT NULL
|
|
1373
1483
|
)
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1484
|
+
`);
|
|
1485
|
+
raw.exec(/*sql*/ `
|
|
1486
|
+
INSERT INTO conversation_keys_new (id, conversation_key, conversation_id, created_at)
|
|
1487
|
+
SELECT id, conversation_key, conversation_id, created_at FROM conversation_keys
|
|
1488
|
+
`);
|
|
1489
|
+
raw.exec(/*sql*/ `DROP TABLE conversation_keys`);
|
|
1490
|
+
raw.exec(/*sql*/ `ALTER TABLE conversation_keys_new RENAME TO conversation_keys`);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// --- attachments ---
|
|
1494
|
+
const attDdl = raw.query(
|
|
1495
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'attachments'`,
|
|
1496
|
+
).get() as { sql: string } | null;
|
|
1497
|
+
if (attDdl?.sql.includes('assistant_id')) {
|
|
1498
|
+
raw.exec(/*sql*/ `
|
|
1499
|
+
CREATE TABLE attachments_new (
|
|
1500
|
+
id TEXT PRIMARY KEY,
|
|
1501
|
+
original_filename TEXT NOT NULL,
|
|
1502
|
+
mime_type TEXT NOT NULL,
|
|
1503
|
+
size_bytes INTEGER NOT NULL,
|
|
1504
|
+
kind TEXT NOT NULL,
|
|
1505
|
+
data_base64 TEXT NOT NULL,
|
|
1506
|
+
content_hash TEXT,
|
|
1507
|
+
thumbnail_base64 TEXT,
|
|
1508
|
+
created_at INTEGER NOT NULL
|
|
1385
1509
|
)
|
|
1386
|
-
|
|
1387
|
-
|
|
1510
|
+
`);
|
|
1511
|
+
raw.exec(/*sql*/ `
|
|
1512
|
+
INSERT INTO attachments_new (id, original_filename, mime_type, size_bytes, kind, data_base64, content_hash, thumbnail_base64, created_at)
|
|
1513
|
+
SELECT id, original_filename, mime_type, size_bytes, kind, data_base64, content_hash, thumbnail_base64, created_at FROM attachments
|
|
1514
|
+
`);
|
|
1515
|
+
raw.exec(/*sql*/ `DROP TABLE attachments`);
|
|
1516
|
+
raw.exec(/*sql*/ `ALTER TABLE attachments_new RENAME TO attachments`);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// --- channel_inbound_events ---
|
|
1520
|
+
const cieDdl = raw.query(
|
|
1521
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'channel_inbound_events'`,
|
|
1522
|
+
).get() as { sql: string } | null;
|
|
1523
|
+
if (cieDdl?.sql.includes('assistant_id')) {
|
|
1524
|
+
raw.exec(/*sql*/ `
|
|
1525
|
+
CREATE TABLE channel_inbound_events_new (
|
|
1526
|
+
id TEXT PRIMARY KEY,
|
|
1527
|
+
source_channel TEXT NOT NULL,
|
|
1528
|
+
external_chat_id TEXT NOT NULL,
|
|
1529
|
+
external_message_id TEXT NOT NULL,
|
|
1530
|
+
source_message_id TEXT,
|
|
1531
|
+
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
1532
|
+
message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
|
|
1533
|
+
delivery_status TEXT NOT NULL DEFAULT 'pending',
|
|
1534
|
+
processing_status TEXT NOT NULL DEFAULT 'pending',
|
|
1535
|
+
processing_attempts INTEGER NOT NULL DEFAULT 0,
|
|
1536
|
+
last_processing_error TEXT,
|
|
1537
|
+
retry_after INTEGER,
|
|
1538
|
+
raw_payload TEXT,
|
|
1539
|
+
created_at INTEGER NOT NULL,
|
|
1540
|
+
updated_at INTEGER NOT NULL,
|
|
1541
|
+
UNIQUE (source_channel, external_chat_id, external_message_id)
|
|
1542
|
+
)
|
|
1543
|
+
`);
|
|
1544
|
+
raw.exec(/*sql*/ `
|
|
1545
|
+
INSERT INTO channel_inbound_events_new (
|
|
1546
|
+
id, source_channel, external_chat_id, external_message_id, source_message_id,
|
|
1547
|
+
conversation_id, message_id, delivery_status, processing_status,
|
|
1548
|
+
processing_attempts, last_processing_error, retry_after, raw_payload,
|
|
1549
|
+
created_at, updated_at
|
|
1550
|
+
)
|
|
1551
|
+
SELECT
|
|
1552
|
+
id, source_channel, external_chat_id, external_message_id, source_message_id,
|
|
1553
|
+
conversation_id, message_id, delivery_status, processing_status,
|
|
1554
|
+
processing_attempts, last_processing_error, retry_after, raw_payload,
|
|
1555
|
+
created_at, updated_at
|
|
1556
|
+
FROM channel_inbound_events
|
|
1557
|
+
`);
|
|
1558
|
+
raw.exec(/*sql*/ `DROP TABLE channel_inbound_events`);
|
|
1559
|
+
raw.exec(/*sql*/ `ALTER TABLE channel_inbound_events_new RENAME TO channel_inbound_events`);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// --- message_runs ---
|
|
1563
|
+
const mrDdl = raw.query(
|
|
1564
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'message_runs'`,
|
|
1565
|
+
).get() as { sql: string } | null;
|
|
1566
|
+
if (mrDdl?.sql.includes('assistant_id')) {
|
|
1567
|
+
raw.exec(/*sql*/ `
|
|
1568
|
+
CREATE TABLE message_runs_new (
|
|
1569
|
+
id TEXT PRIMARY KEY,
|
|
1570
|
+
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
1571
|
+
message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
|
|
1572
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
1573
|
+
pending_confirmation TEXT,
|
|
1574
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
1575
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
1576
|
+
estimated_cost REAL NOT NULL DEFAULT 0,
|
|
1577
|
+
error TEXT,
|
|
1578
|
+
created_at INTEGER NOT NULL,
|
|
1579
|
+
updated_at INTEGER NOT NULL
|
|
1580
|
+
)
|
|
1581
|
+
`);
|
|
1582
|
+
raw.exec(/*sql*/ `
|
|
1583
|
+
INSERT INTO message_runs_new (
|
|
1584
|
+
id, conversation_id, message_id, status, pending_confirmation,
|
|
1585
|
+
input_tokens, output_tokens, estimated_cost, error, created_at, updated_at
|
|
1586
|
+
)
|
|
1587
|
+
SELECT
|
|
1588
|
+
id, conversation_id, message_id, status, pending_confirmation,
|
|
1589
|
+
input_tokens, output_tokens, estimated_cost, error, created_at, updated_at
|
|
1590
|
+
FROM message_runs
|
|
1591
|
+
`);
|
|
1592
|
+
raw.exec(/*sql*/ `DROP TABLE message_runs`);
|
|
1593
|
+
raw.exec(/*sql*/ `ALTER TABLE message_runs_new RENAME TO message_runs`);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// --- llm_usage_events ---
|
|
1597
|
+
const lueDdl = raw.query(
|
|
1598
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'llm_usage_events'`,
|
|
1599
|
+
).get() as { sql: string } | null;
|
|
1600
|
+
if (lueDdl?.sql.includes('assistant_id')) {
|
|
1601
|
+
raw.exec(/*sql*/ `
|
|
1602
|
+
CREATE TABLE llm_usage_events_new (
|
|
1603
|
+
id TEXT PRIMARY KEY,
|
|
1604
|
+
created_at INTEGER NOT NULL,
|
|
1605
|
+
conversation_id TEXT,
|
|
1606
|
+
run_id TEXT,
|
|
1607
|
+
request_id TEXT,
|
|
1608
|
+
actor TEXT NOT NULL,
|
|
1609
|
+
provider TEXT NOT NULL,
|
|
1610
|
+
model TEXT NOT NULL,
|
|
1611
|
+
input_tokens INTEGER NOT NULL,
|
|
1612
|
+
output_tokens INTEGER NOT NULL,
|
|
1613
|
+
cache_creation_input_tokens INTEGER,
|
|
1614
|
+
cache_read_input_tokens INTEGER,
|
|
1615
|
+
estimated_cost_usd REAL,
|
|
1616
|
+
pricing_status TEXT NOT NULL,
|
|
1617
|
+
metadata_json TEXT
|
|
1618
|
+
)
|
|
1619
|
+
`);
|
|
1620
|
+
raw.exec(/*sql*/ `
|
|
1621
|
+
INSERT INTO llm_usage_events_new (
|
|
1622
|
+
id, created_at, conversation_id, run_id, request_id, actor, provider, model,
|
|
1623
|
+
input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens,
|
|
1624
|
+
estimated_cost_usd, pricing_status, metadata_json
|
|
1625
|
+
)
|
|
1626
|
+
SELECT
|
|
1627
|
+
id, created_at, conversation_id, run_id, request_id, actor, provider, model,
|
|
1628
|
+
input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens,
|
|
1629
|
+
estimated_cost_usd, pricing_status, metadata_json
|
|
1630
|
+
FROM llm_usage_events
|
|
1631
|
+
`);
|
|
1632
|
+
raw.exec(/*sql*/ `DROP TABLE llm_usage_events`);
|
|
1633
|
+
raw.exec(/*sql*/ `ALTER TABLE llm_usage_events_new RENAME TO llm_usage_events`);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
raw.query(
|
|
1637
|
+
`INSERT OR IGNORE INTO memory_checkpoints (key, value, updated_at) VALUES (?, '1', ?)`,
|
|
1638
|
+
).run(checkpointKey, Date.now());
|
|
1639
|
+
|
|
1640
|
+
raw.exec('COMMIT');
|
|
1641
|
+
} catch (e) {
|
|
1642
|
+
try { raw.exec('ROLLBACK'); } catch { /* no active transaction */ }
|
|
1643
|
+
throw e;
|
|
1644
|
+
} finally {
|
|
1645
|
+
raw.exec('PRAGMA foreign_keys = ON');
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* One-shot migration: rebuild llm_usage_events to drop the assistant_id column.
|
|
1651
|
+
*
|
|
1652
|
+
* This is a SEPARATE migration from migrateRemoveAssistantIdColumns so that installs
|
|
1653
|
+
* where the 4-table version of that migration already ran (checkpoint already set)
|
|
1654
|
+
* still get the llm_usage_events column removed. Without a separate checkpoint key,
|
|
1655
|
+
* those installs would skip the llm_usage_events rebuild entirely.
|
|
1656
|
+
*
|
|
1657
|
+
* Safe on fresh installs (DDL guard exits early) and idempotent via checkpoint.
|
|
1658
|
+
*/
|
|
1659
|
+
function migrateLlmUsageEventsDropAssistantId(database: ReturnType<typeof drizzle<typeof schema>>): void {
|
|
1660
|
+
const raw = (database as unknown as { $client: Database }).$client;
|
|
1661
|
+
const checkpointKey = 'migration_remove_assistant_id_lue_v1';
|
|
1662
|
+
const checkpoint = raw.query(
|
|
1663
|
+
`SELECT 1 FROM memory_checkpoints WHERE key = ?`,
|
|
1664
|
+
).get(checkpointKey);
|
|
1665
|
+
if (checkpoint) return;
|
|
1666
|
+
|
|
1667
|
+
// DDL guard: if the column was already removed (fresh install or migrateRemoveAssistantIdColumns
|
|
1668
|
+
// ran with the llm_usage_events block), just record the checkpoint and exit.
|
|
1669
|
+
const lueDdl = raw.query(
|
|
1670
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'llm_usage_events'`,
|
|
1671
|
+
).get() as { sql: string } | null;
|
|
1672
|
+
|
|
1673
|
+
if (!lueDdl?.sql.includes('assistant_id')) {
|
|
1674
|
+
raw.query(
|
|
1675
|
+
`INSERT OR IGNORE INTO memory_checkpoints (key, value, updated_at) VALUES (?, '1', ?)`,
|
|
1676
|
+
).run(checkpointKey, Date.now());
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
raw.exec('PRAGMA foreign_keys = OFF');
|
|
1681
|
+
try {
|
|
1682
|
+
raw.exec('BEGIN');
|
|
1683
|
+
|
|
1388
1684
|
raw.exec(/*sql*/ `
|
|
1389
|
-
|
|
1685
|
+
CREATE TABLE llm_usage_events_new (
|
|
1686
|
+
id TEXT PRIMARY KEY,
|
|
1687
|
+
created_at INTEGER NOT NULL,
|
|
1688
|
+
conversation_id TEXT,
|
|
1689
|
+
run_id TEXT,
|
|
1690
|
+
request_id TEXT,
|
|
1691
|
+
actor TEXT NOT NULL,
|
|
1692
|
+
provider TEXT NOT NULL,
|
|
1693
|
+
model TEXT NOT NULL,
|
|
1694
|
+
input_tokens INTEGER NOT NULL,
|
|
1695
|
+
output_tokens INTEGER NOT NULL,
|
|
1696
|
+
cache_creation_input_tokens INTEGER,
|
|
1697
|
+
cache_read_input_tokens INTEGER,
|
|
1698
|
+
estimated_cost_usd REAL,
|
|
1699
|
+
pricing_status TEXT NOT NULL,
|
|
1700
|
+
metadata_json TEXT
|
|
1701
|
+
)
|
|
1390
1702
|
`);
|
|
1391
|
-
|
|
1392
|
-
// message_runs: no unique constraint on assistant_id — simple bulk update
|
|
1393
1703
|
raw.exec(/*sql*/ `
|
|
1394
|
-
|
|
1704
|
+
INSERT INTO llm_usage_events_new (
|
|
1705
|
+
id, created_at, conversation_id, run_id, request_id, actor, provider, model,
|
|
1706
|
+
input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens,
|
|
1707
|
+
estimated_cost_usd, pricing_status, metadata_json
|
|
1708
|
+
)
|
|
1709
|
+
SELECT
|
|
1710
|
+
id, created_at, conversation_id, run_id, request_id, actor, provider, model,
|
|
1711
|
+
input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens,
|
|
1712
|
+
estimated_cost_usd, pricing_status, metadata_json
|
|
1713
|
+
FROM llm_usage_events
|
|
1395
1714
|
`);
|
|
1715
|
+
raw.exec(/*sql*/ `DROP TABLE llm_usage_events`);
|
|
1716
|
+
raw.exec(/*sql*/ `ALTER TABLE llm_usage_events_new RENAME TO llm_usage_events`);
|
|
1396
1717
|
|
|
1397
1718
|
raw.query(
|
|
1398
1719
|
`INSERT OR IGNORE INTO memory_checkpoints (key, value, updated_at) VALUES (?, '1', ?)`,
|
|
@@ -1402,6 +1723,8 @@ function migrateAssistantIdToSelf(database: ReturnType<typeof drizzle<typeof sch
|
|
|
1402
1723
|
} catch (e) {
|
|
1403
1724
|
try { raw.exec('ROLLBACK'); } catch { /* no active transaction */ }
|
|
1404
1725
|
throw e;
|
|
1726
|
+
} finally {
|
|
1727
|
+
raw.exec('PRAGMA foreign_keys = ON');
|
|
1405
1728
|
}
|
|
1406
1729
|
}
|
|
1407
1730
|
|
|
@@ -1463,3 +1786,4 @@ function migrateCallSessionsProviderSidDedup(database: ReturnType<typeof drizzle
|
|
|
1463
1786
|
throw e;
|
|
1464
1787
|
}
|
|
1465
1788
|
}
|
|
1789
|
+
|