@khoralabs/chat 0.1.0

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.
Files changed (85) hide show
  1. package/README.md +7 -0
  2. package/dist/domain.d.ts +10 -0
  3. package/dist/domain.d.ts.map +1 -0
  4. package/dist/errors.d.ts +20 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/events.d.ts +59 -0
  7. package/dist/events.d.ts.map +1 -0
  8. package/dist/hash.d.ts +47 -0
  9. package/dist/hash.d.ts.map +1 -0
  10. package/dist/http/client.d.ts +21 -0
  11. package/dist/http/client.d.ts.map +1 -0
  12. package/dist/http/client.js +367 -0
  13. package/dist/http/config.d.ts +5 -0
  14. package/dist/http/config.d.ts.map +1 -0
  15. package/dist/http/index.d.ts +6 -0
  16. package/dist/http/index.d.ts.map +1 -0
  17. package/dist/http/index.js +1084 -0
  18. package/dist/http/routes.d.ts +15 -0
  19. package/dist/http/routes.d.ts.map +1 -0
  20. package/dist/http/routes.js +645 -0
  21. package/dist/http/server.d.ts +19 -0
  22. package/dist/http/server.d.ts.map +1 -0
  23. package/dist/http/server.js +973 -0
  24. package/dist/http/service.d.ts +34 -0
  25. package/dist/http/service.d.ts.map +1 -0
  26. package/dist/http/service.js +501 -0
  27. package/dist/ids.d.ts +8 -0
  28. package/dist/ids.d.ts.map +1 -0
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +453 -0
  32. package/dist/lineage.d.ts +5 -0
  33. package/dist/lineage.d.ts.map +1 -0
  34. package/dist/persistence/core/index.d.ts +2 -0
  35. package/dist/persistence/core/index.d.ts.map +1 -0
  36. package/dist/persistence/core/persistence/base-persistence.d.ts +12 -0
  37. package/dist/persistence/core/persistence/base-persistence.d.ts.map +1 -0
  38. package/dist/persistence/core/persistence/helpers.d.ts +30 -0
  39. package/dist/persistence/core/persistence/helpers.d.ts.map +1 -0
  40. package/dist/persistence/core/persistence/index.d.ts +6 -0
  41. package/dist/persistence/core/persistence/index.d.ts.map +1 -0
  42. package/dist/persistence/core/persistence/index.js +1276 -0
  43. package/dist/persistence/core/persistence/memory-persistence.d.ts +55 -0
  44. package/dist/persistence/core/persistence/memory-persistence.d.ts.map +1 -0
  45. package/dist/persistence/core/persistence/signed-persistence.d.ts +18 -0
  46. package/dist/persistence/core/persistence/signed-persistence.d.ts.map +1 -0
  47. package/dist/persistence/core/persistence/types.d.ts +210 -0
  48. package/dist/persistence/core/persistence/types.d.ts.map +1 -0
  49. package/dist/persistence/sqlite/index.d.ts +3 -0
  50. package/dist/persistence/sqlite/index.d.ts.map +1 -0
  51. package/dist/persistence/sqlite/index.js +1544 -0
  52. package/dist/persistence/sqlite/persistence/persistence.d.ts +49 -0
  53. package/dist/persistence/sqlite/persistence/persistence.d.ts.map +1 -0
  54. package/dist/persistence/sqlite/persistence/schema.d.ts +4 -0
  55. package/dist/persistence/sqlite/persistence/schema.d.ts.map +1 -0
  56. package/dist/persistence/sqlite/persistence/streaming.d.ts +27 -0
  57. package/dist/persistence/sqlite/persistence/streaming.d.ts.map +1 -0
  58. package/dist/persistence/testing/index.d.ts +2 -0
  59. package/dist/persistence/testing/index.d.ts.map +1 -0
  60. package/dist/persistence/testing/index.js +689 -0
  61. package/dist/persistence/testing/persistence/contract.d.ts +3 -0
  62. package/dist/persistence/testing/persistence/contract.d.ts.map +1 -0
  63. package/dist/persistence/turso-serverless/index.d.ts +10 -0
  64. package/dist/persistence/turso-serverless/index.d.ts.map +1 -0
  65. package/dist/persistence/turso-serverless/index.js +1812 -0
  66. package/dist/persistence/turso-serverless/persistence/local-sqlite.d.ts +5 -0
  67. package/dist/persistence/turso-serverless/persistence/local-sqlite.d.ts.map +1 -0
  68. package/dist/persistence/turso-serverless/persistence/persistence.d.ts +49 -0
  69. package/dist/persistence/turso-serverless/persistence/persistence.d.ts.map +1 -0
  70. package/dist/persistence/turso-serverless/persistence/schema.d.ts +4 -0
  71. package/dist/persistence/turso-serverless/persistence/schema.d.ts.map +1 -0
  72. package/dist/persistence/turso-serverless/persistence/sql.d.ts +20 -0
  73. package/dist/persistence/turso-serverless/persistence/sql.d.ts.map +1 -0
  74. package/dist/persistence/turso-serverless/persistence/sql.js +53 -0
  75. package/dist/persistence/turso-serverless/persistence/streaming.d.ts +27 -0
  76. package/dist/persistence/turso-serverless/persistence/streaming.d.ts.map +1 -0
  77. package/dist/service.d.ts +49 -0
  78. package/dist/service.d.ts.map +1 -0
  79. package/dist/stream.d.ts +32 -0
  80. package/dist/stream.d.ts.map +1 -0
  81. package/dist/types.d.ts +201 -0
  82. package/dist/types.d.ts.map +1 -0
  83. package/dist/validation.d.ts +4 -0
  84. package/dist/validation.d.ts.map +1 -0
  85. package/package.json +104 -0
@@ -0,0 +1,1812 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/persistence/turso-serverless/persistence/sql.ts
5
+ import { connect } from "@tursodatabase/serverless";
6
+ function createTursoDatabase(config) {
7
+ return connect({ url: config.url, authToken: config.authToken });
8
+ }
9
+ async function prepare(db, sql) {
10
+ return await Promise.resolve(db.prepare(sql));
11
+ }
12
+ async function run(db, sql, args = []) {
13
+ const stmt = await prepare(db, sql);
14
+ await stmt.run(args);
15
+ }
16
+ async function all(db, sql, args = []) {
17
+ const stmt = await prepare(db, sql);
18
+ return await stmt.all(args);
19
+ }
20
+ async function get(db, sql, args = []) {
21
+ const stmt = await prepare(db, sql);
22
+ return await stmt.get(args) ?? null;
23
+ }
24
+ async function exec(db, sql) {
25
+ if (db.exec !== undefined) {
26
+ await Promise.resolve(db.exec(sql));
27
+ return;
28
+ }
29
+ for (const statement of sql.split(";").map((part) => part.trim()).filter((part) => part.length > 0)) {
30
+ await run(db, statement);
31
+ }
32
+ }
33
+ async function transaction(db, fn) {
34
+ await run(db, "BEGIN IMMEDIATE");
35
+ try {
36
+ const result = await fn();
37
+ await run(db, "COMMIT");
38
+ return result;
39
+ } catch (error) {
40
+ try {
41
+ await run(db, "ROLLBACK");
42
+ } catch {}
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ // src/persistence/turso-serverless/persistence/local-sqlite.ts
48
+ import { Database } from "bun:sqlite";
49
+ import { mkdirSync } from "node:fs";
50
+ import path from "node:path";
51
+
52
+ // src/persistence/turso-serverless/persistence/schema.ts
53
+ var CHAT_SCHEMA = `
54
+ CREATE TABLE IF NOT EXISTS chat_channels (
55
+ id TEXT PRIMARY KEY,
56
+ metadata TEXT,
57
+ created_at_ms INTEGER NOT NULL
58
+ );
59
+
60
+ CREATE TABLE IF NOT EXISTS chat_threads (
61
+ id TEXT PRIMARY KEY,
62
+ root_type TEXT NOT NULL,
63
+ root_id TEXT NOT NULL,
64
+ root_version_id TEXT,
65
+ default_head_id TEXT,
66
+ metadata TEXT,
67
+ created_at_ms INTEGER NOT NULL,
68
+ archived_at_ms INTEGER
69
+ );
70
+
71
+ CREATE TABLE IF NOT EXISTS chat_posts (
72
+ id TEXT PRIMARY KEY,
73
+ thread_id TEXT NOT NULL,
74
+ post_index INTEGER NOT NULL,
75
+ status TEXT NOT NULL DEFAULT 'complete',
76
+ stream_message TEXT,
77
+ stream_mentions TEXT,
78
+ stream_model TEXT,
79
+ stream_usage TEXT,
80
+ stream_author_scope_type TEXT,
81
+ stream_author_scope_id TEXT,
82
+ stream_revision INTEGER NOT NULL DEFAULT 0,
83
+ completed_version_id TEXT,
84
+ created_at_ms INTEGER NOT NULL,
85
+ updated_at_ms INTEGER,
86
+ deleted_at_ms INTEGER,
87
+ UNIQUE(thread_id, post_index)
88
+ );
89
+
90
+ CREATE TABLE IF NOT EXISTS chat_post_stream_events (
91
+ id TEXT PRIMARY KEY,
92
+ post_id TEXT NOT NULL,
93
+ thread_id TEXT NOT NULL,
94
+ event_type TEXT NOT NULL,
95
+ revision INTEGER NOT NULL,
96
+ message TEXT,
97
+ delta TEXT,
98
+ mentions TEXT,
99
+ model TEXT,
100
+ usage TEXT,
101
+ idempotency_key TEXT UNIQUE,
102
+ created_at_ms INTEGER NOT NULL
103
+ );
104
+
105
+ CREATE TABLE IF NOT EXISTS chat_post_versions (
106
+ id TEXT PRIMARY KEY,
107
+ post_id TEXT NOT NULL,
108
+ thread_id TEXT NOT NULL,
109
+ parent_version_id TEXT,
110
+ previous_post_version_id TEXT,
111
+ author_scope_type TEXT NOT NULL,
112
+ author_scope_id TEXT NOT NULL,
113
+ message TEXT NOT NULL,
114
+ mentions TEXT,
115
+ model TEXT,
116
+ usage TEXT,
117
+ content_hash TEXT NOT NULL,
118
+ lineage_hash TEXT NOT NULL,
119
+ signature TEXT,
120
+ idempotency_key TEXT UNIQUE,
121
+ created_at_ms INTEGER NOT NULL
122
+ );
123
+
124
+ CREATE TABLE IF NOT EXISTS chat_thread_heads (
125
+ id TEXT PRIMARY KEY,
126
+ thread_id TEXT NOT NULL,
127
+ name TEXT NOT NULL,
128
+ head_post_version_id TEXT NOT NULL,
129
+ created_at_ms INTEGER NOT NULL
130
+ );
131
+
132
+ CREATE TABLE IF NOT EXISTS chat_acl_events (
133
+ id TEXT PRIMARY KEY,
134
+ target_type TEXT NOT NULL,
135
+ target_id TEXT NOT NULL,
136
+ event_type TEXT NOT NULL,
137
+ actor_scope_type TEXT NOT NULL,
138
+ actor_scope_id TEXT NOT NULL,
139
+ subject_scope_type TEXT,
140
+ subject_scope_id TEXT,
141
+ role TEXT,
142
+ previous_acl_event_id TEXT,
143
+ content_hash TEXT NOT NULL,
144
+ signature TEXT,
145
+ created_at_ms INTEGER NOT NULL
146
+ );
147
+
148
+ CREATE TABLE IF NOT EXISTS chat_channel_members (
149
+ channel_id TEXT NOT NULL,
150
+ scope_type TEXT NOT NULL,
151
+ scope_id TEXT NOT NULL,
152
+ role TEXT NOT NULL,
153
+ created_at_ms INTEGER NOT NULL,
154
+ PRIMARY KEY (channel_id, scope_type, scope_id)
155
+ );
156
+
157
+ CREATE TABLE IF NOT EXISTS chat_thread_participants (
158
+ thread_id TEXT NOT NULL,
159
+ scope_type TEXT NOT NULL,
160
+ scope_id TEXT NOT NULL,
161
+ role TEXT NOT NULL,
162
+ created_at_ms INTEGER NOT NULL,
163
+ PRIMARY KEY (thread_id, scope_type, scope_id)
164
+ );
165
+
166
+ CREATE INDEX IF NOT EXISTS idx_chat_threads_root ON chat_threads(root_type, root_id);
167
+ CREATE INDEX IF NOT EXISTS idx_chat_post_versions_thread ON chat_post_versions(thread_id);
168
+ CREATE INDEX IF NOT EXISTS idx_chat_post_versions_post ON chat_post_versions(post_id);
169
+ CREATE INDEX IF NOT EXISTS idx_chat_posts_thread ON chat_posts(thread_id, post_index);
170
+ CREATE INDEX IF NOT EXISTS idx_chat_post_stream_events_post ON chat_post_stream_events(post_id, revision);
171
+ `;
172
+ async function ensureChatSchema(db) {
173
+ await exec(db, CHAT_SCHEMA);
174
+ for (const statement of [
175
+ "ALTER TABLE chat_posts ADD COLUMN stream_model TEXT",
176
+ "ALTER TABLE chat_posts ADD COLUMN stream_usage TEXT",
177
+ "ALTER TABLE chat_post_stream_events ADD COLUMN model TEXT",
178
+ "ALTER TABLE chat_post_stream_events ADD COLUMN usage TEXT",
179
+ "ALTER TABLE chat_post_versions ADD COLUMN model TEXT",
180
+ "ALTER TABLE chat_post_versions ADD COLUMN usage TEXT"
181
+ ]) {
182
+ try {
183
+ await run(db, statement);
184
+ } catch (error) {
185
+ if (!(error instanceof Error) || !error.message.includes("duplicate column name")) {
186
+ throw error;
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ // src/persistence/turso-serverless/persistence/local-sqlite.ts
193
+ class BunSqliteDatabase {
194
+ db;
195
+ constructor(db) {
196
+ this.db = db;
197
+ }
198
+ prepare(sql) {
199
+ const stmt = this.db.prepare(sql);
200
+ return {
201
+ run: async (args = []) => {
202
+ stmt.run(...args);
203
+ },
204
+ all: async (args = []) => stmt.all(...args),
205
+ get: async (args = []) => stmt.get(...args) ?? null
206
+ };
207
+ }
208
+ exec(sql) {
209
+ this.db.exec(sql);
210
+ }
211
+ close() {
212
+ this.db.close();
213
+ }
214
+ }
215
+ function applyMigrationsSync(db) {
216
+ for (const statement of [
217
+ "ALTER TABLE chat_posts ADD COLUMN stream_model TEXT",
218
+ "ALTER TABLE chat_posts ADD COLUMN stream_usage TEXT",
219
+ "ALTER TABLE chat_post_stream_events ADD COLUMN model TEXT",
220
+ "ALTER TABLE chat_post_stream_events ADD COLUMN usage TEXT",
221
+ "ALTER TABLE chat_post_versions ADD COLUMN model TEXT",
222
+ "ALTER TABLE chat_post_versions ADD COLUMN usage TEXT"
223
+ ]) {
224
+ try {
225
+ db.run(statement);
226
+ } catch (error) {
227
+ if (!(error instanceof Error) || !error.message.includes("duplicate column name")) {
228
+ throw error;
229
+ }
230
+ }
231
+ }
232
+ }
233
+ function createLocalSqliteDatabase(dbPath) {
234
+ const resolved = path.resolve(dbPath);
235
+ mkdirSync(path.dirname(resolved), { recursive: true });
236
+ const db = new Database(resolved, { create: true });
237
+ db.exec(CHAT_SCHEMA);
238
+ applyMigrationsSync(db);
239
+ return new BunSqliteDatabase(db);
240
+ }
241
+ function createMemorySqliteDatabase() {
242
+ const db = new Database(":memory:");
243
+ db.exec(CHAT_SCHEMA);
244
+ applyMigrationsSync(db);
245
+ return new BunSqliteDatabase(db);
246
+ }
247
+ function closeLocalSqliteDatabase(db) {
248
+ if (db instanceof BunSqliteDatabase) {
249
+ db.close();
250
+ }
251
+ }
252
+ // src/persistence/turso-serverless/persistence/persistence.ts
253
+ import {
254
+ BaseChatPersistence,
255
+ buildAclEventContentHash,
256
+ prepareAppendPost as prepareAppendPost2,
257
+ prepareEditPost
258
+ } from "@khoralabs/chat/persistence";
259
+
260
+ // src/errors.ts
261
+ class ChatError extends Error {
262
+ code;
263
+ constructor(code, message) {
264
+ super(message);
265
+ this.name = "ChatError";
266
+ this.code = code;
267
+ }
268
+ }
269
+
270
+ class ChatNotFoundError extends ChatError {
271
+ resource;
272
+ resourceId;
273
+ constructor(resource, resourceId) {
274
+ super("not_found", `${resource} not found: ${resourceId}`);
275
+ this.name = "ChatNotFoundError";
276
+ this.resource = resource;
277
+ this.resourceId = resourceId;
278
+ }
279
+ }
280
+
281
+ class ChatConflictError extends ChatError {
282
+ reason;
283
+ constructor(reason, message) {
284
+ super("conflict", message);
285
+ this.name = "ChatConflictError";
286
+ this.reason = reason;
287
+ }
288
+ }
289
+
290
+ class ChatValidationError extends ChatError {
291
+ constructor(message) {
292
+ super("validation", message);
293
+ this.name = "ChatValidationError";
294
+ }
295
+ }
296
+
297
+ class ChatPermissionError extends ChatError {
298
+ constructor(message) {
299
+ super("permission_denied", message);
300
+ this.name = "ChatPermissionError";
301
+ }
302
+ }
303
+ // src/hash.ts
304
+ function sha256Hex(input) {
305
+ const hasher = new Bun.CryptoHasher("sha256");
306
+ hasher.update(input);
307
+ return hasher.digest("hex");
308
+ }
309
+ function stableStringify(value) {
310
+ if (value === undefined) {
311
+ return "null";
312
+ }
313
+ if (value === null || typeof value !== "object") {
314
+ return JSON.stringify(value);
315
+ }
316
+ if (Array.isArray(value)) {
317
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
318
+ }
319
+ const record = value;
320
+ const keys = Object.keys(record).sort();
321
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
322
+ }
323
+ function canonicalPostVersionPayload(input) {
324
+ return stableStringify({
325
+ postId: input.postId,
326
+ versionId: input.versionId,
327
+ threadId: input.threadId,
328
+ author: input.author,
329
+ role: input.role,
330
+ parts: input.parts,
331
+ metadata: input.metadata ?? null,
332
+ mentions: input.mentions ?? null,
333
+ model: input.model ?? null,
334
+ usage: input.usage ?? null,
335
+ parentVersionId: input.parentVersionId ?? null,
336
+ previousPostVersionId: input.previousPostVersionId ?? null
337
+ });
338
+ }
339
+ function computeContentHash(payload) {
340
+ return sha256Hex(canonicalPostVersionPayload(payload));
341
+ }
342
+ function computeLineageHash(input) {
343
+ return sha256Hex(stableStringify({
344
+ previousLineageHash: input.previousLineageHash,
345
+ contentHash: input.contentHash,
346
+ postId: input.postId,
347
+ versionId: input.versionId
348
+ }));
349
+ }
350
+ function canonicalAclEventPayload(input) {
351
+ return stableStringify({
352
+ type: input.type,
353
+ targetType: input.targetType,
354
+ targetId: input.targetId,
355
+ scope: input.scope ?? null,
356
+ role: input.role ?? null,
357
+ actor: input.actor,
358
+ previousAclEventId: input.previousAclEventId ?? null
359
+ });
360
+ }
361
+ function computeAclEventContentHash(payload) {
362
+ return sha256Hex(canonicalAclEventPayload(payload));
363
+ }
364
+ function signedPayloadBytes(payload) {
365
+ return new TextEncoder().encode(payload);
366
+ }
367
+ function canonicalSignedPostVersionPayload(input) {
368
+ return stableStringify({
369
+ contentHash: input.contentHash,
370
+ lineageHash: input.lineageHash,
371
+ payload: JSON.parse(canonicalPostVersionPayload(input))
372
+ });
373
+ }
374
+ function canonicalSignedAclEventPayload(input) {
375
+ return stableStringify({
376
+ contentHash: input.contentHash,
377
+ payload: JSON.parse(canonicalAclEventPayload(input.payload))
378
+ });
379
+ }
380
+ async function maybeSignPayload(payload, signer, sign) {
381
+ return sign(signedPayloadBytes(payload), signer);
382
+ }
383
+ // src/ids.ts
384
+ function createId() {
385
+ return crypto.randomUUID();
386
+ }
387
+ function scopeKey(scope) {
388
+ return `${scope.type}:${scope.id}`;
389
+ }
390
+ function scopeRefFromKey(key) {
391
+ const separatorIndex = key.indexOf(":");
392
+ if (separatorIndex === -1) {
393
+ throw new Error(`Invalid scope key: ${key}`);
394
+ }
395
+ return {
396
+ type: key.slice(0, separatorIndex),
397
+ id: key.slice(separatorIndex + 1)
398
+ };
399
+ }
400
+ // src/lineage.ts
401
+ function walkLineageFromHead(headVersionId, versionsById) {
402
+ const lineage = [];
403
+ let currentId = headVersionId;
404
+ const seenPostIds = new Set;
405
+ while (currentId) {
406
+ const version = versionsById.get(currentId);
407
+ if (!version)
408
+ break;
409
+ if (!seenPostIds.has(version.postId)) {
410
+ lineage.push(version);
411
+ seenPostIds.add(version.postId);
412
+ }
413
+ currentId = version.previousPostVersionId;
414
+ }
415
+ return lineage.reverse();
416
+ }
417
+ function lineageBetween(headVersionId, ancestorVersionId, versionsById) {
418
+ const lineage = walkLineageFromHead(headVersionId, versionsById);
419
+ const startIndex = lineage.findIndex((version) => version.id === ancestorVersionId);
420
+ if (startIndex === -1)
421
+ return null;
422
+ return lineage.slice(startIndex);
423
+ }
424
+ function postFromVersion(version, index, deletedAtMs) {
425
+ return {
426
+ ...version,
427
+ id: version.postId,
428
+ status: "complete",
429
+ versionId: version.id,
430
+ previousVersionId: version.parentVersionId ?? null,
431
+ index,
432
+ updatedAtMs: version.parentVersionId ? version.createdAtMs : null,
433
+ deletedAtMs: deletedAtMs ?? null
434
+ };
435
+ }
436
+ // src/stream.ts
437
+ function rebuildStreamCacheFromEvents(events) {
438
+ const sorted = [...events].sort((a, b) => a.revision - b.revision);
439
+ let message = null;
440
+ let mentions;
441
+ let model;
442
+ let usage;
443
+ let revision = 0;
444
+ for (const event of sorted) {
445
+ if (event.eventType === "stream.started") {
446
+ if (!event.message) {
447
+ throw new Error(`stream.started event ${event.id} missing message`);
448
+ }
449
+ message = event.message;
450
+ mentions = event.mentions;
451
+ model = event.model;
452
+ usage = event.usage;
453
+ revision = event.revision;
454
+ continue;
455
+ }
456
+ if (event.eventType === "stream.delta") {
457
+ if (!event.message) {
458
+ throw new Error(`stream.delta event ${event.id} missing message`);
459
+ }
460
+ message = event.message;
461
+ if (event.mentions !== undefined)
462
+ mentions = event.mentions;
463
+ if (event.model !== undefined)
464
+ model = event.model;
465
+ if (event.usage !== undefined)
466
+ usage = event.usage;
467
+ revision = event.revision;
468
+ continue;
469
+ }
470
+ if (event.eventType === "stream.completed" || event.eventType === "stream.aborted") {
471
+ revision = event.revision;
472
+ }
473
+ }
474
+ if (!message) {
475
+ throw new Error("stream events did not produce a message");
476
+ }
477
+ return { message, mentions, model, usage, revision };
478
+ }
479
+ function streamingPostFromCache(input) {
480
+ return {
481
+ ...input.message,
482
+ id: input.postId,
483
+ status: "streaming",
484
+ threadId: input.threadId,
485
+ author: input.author,
486
+ mentions: input.mentions,
487
+ model: input.model,
488
+ usage: input.usage,
489
+ index: input.index,
490
+ streamRevision: input.streamRevision,
491
+ createdAtMs: input.createdAtMs,
492
+ updatedAtMs: input.updatedAtMs ?? null,
493
+ deletedAtMs: input.deletedAtMs ?? null
494
+ };
495
+ }
496
+ function mergeThreadPostsForList(lineagePosts, activePosts) {
497
+ const byIndex = new Map;
498
+ for (const item of lineagePosts) {
499
+ byIndex.set(item.index, item.post);
500
+ }
501
+ for (const post of activePosts) {
502
+ if (post.status === "streaming" || post.status === "aborted") {
503
+ byIndex.set(post.index, post);
504
+ }
505
+ }
506
+ return [...byIndex.entries()].sort(([a], [b]) => a - b).map(([, post]) => post);
507
+ }
508
+ // src/validation.ts
509
+ function assertThreadRoot(root) {
510
+ if (root.type === "channel") {
511
+ if (!root.channelId.trim()) {
512
+ throw new ChatValidationError("channel root requires channelId");
513
+ }
514
+ return;
515
+ }
516
+ if (!root.postId.trim()) {
517
+ throw new ChatValidationError("post root requires postId");
518
+ }
519
+ }
520
+ function assertNonEmptyString(value, label) {
521
+ if (!value.trim()) {
522
+ throw new ChatValidationError(`${label} is required`);
523
+ }
524
+ }
525
+ // src/persistence/turso-serverless/persistence/streaming.ts
526
+ import { prepareAppendPost } from "@khoralabs/chat/persistence";
527
+ var DEFAULT_HEAD_NAME = "default";
528
+ function parseJson(value) {
529
+ if (!value)
530
+ return;
531
+ return JSON.parse(value);
532
+ }
533
+ function scopeFromRow(type, id) {
534
+ return { type, id };
535
+ }
536
+ function requireStreamAuthor(row) {
537
+ if (!row.stream_author_scope_type || !row.stream_author_scope_id) {
538
+ throw new ChatValidationError(`post ${row.id} is missing stream author`);
539
+ }
540
+ return scopeFromRow(row.stream_author_scope_type, row.stream_author_scope_id);
541
+ }
542
+ function postFromRow(row, getVersion) {
543
+ if (row.status === "streaming") {
544
+ const message = parseJson(row.stream_message);
545
+ if (!message || !row.stream_author_scope_type || !row.stream_author_scope_id)
546
+ return null;
547
+ return streamingPostFromCache({
548
+ postId: row.id,
549
+ threadId: row.thread_id,
550
+ author: scopeFromRow(row.stream_author_scope_type, row.stream_author_scope_id),
551
+ message,
552
+ mentions: parseJson(row.stream_mentions),
553
+ model: parseJson(row.stream_model),
554
+ usage: parseJson(row.stream_usage),
555
+ index: row.post_index,
556
+ streamRevision: row.stream_revision,
557
+ createdAtMs: row.created_at_ms,
558
+ updatedAtMs: row.updated_at_ms,
559
+ deletedAtMs: row.deleted_at_ms
560
+ });
561
+ }
562
+ if (row.status === "aborted") {
563
+ const message = parseJson(row.stream_message);
564
+ if (!message || !row.stream_author_scope_type || !row.stream_author_scope_id)
565
+ return null;
566
+ return {
567
+ ...message,
568
+ id: row.id,
569
+ status: "aborted",
570
+ threadId: row.thread_id,
571
+ author: scopeFromRow(row.stream_author_scope_type, row.stream_author_scope_id),
572
+ mentions: parseJson(row.stream_mentions),
573
+ model: parseJson(row.stream_model),
574
+ usage: parseJson(row.stream_usage),
575
+ index: row.post_index,
576
+ streamRevision: row.stream_revision,
577
+ createdAtMs: row.created_at_ms,
578
+ updatedAtMs: row.updated_at_ms,
579
+ deletedAtMs: row.deleted_at_ms
580
+ };
581
+ }
582
+ const versionId = row.completed_version_id;
583
+ if (!versionId)
584
+ return null;
585
+ const version = getVersion(versionId);
586
+ if (!version)
587
+ return null;
588
+ return postFromVersion(version, row.post_index, row.deleted_at_ms);
589
+ }
590
+ async function getPostRow(db, postId) {
591
+ return get(db, `SELECT id, thread_id, post_index, status, stream_message, stream_mentions, stream_model, stream_usage,
592
+ stream_author_scope_type, stream_author_scope_id, stream_revision,
593
+ completed_version_id, created_at_ms, updated_at_ms, deleted_at_ms
594
+ FROM chat_posts WHERE id = ?`, [postId]);
595
+ }
596
+ async function insertStreamEvent(db, event) {
597
+ await run(db, `INSERT INTO chat_post_stream_events
598
+ (id, post_id, thread_id, event_type, revision, message, delta, mentions, model, usage, idempotency_key, created_at_ms)
599
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
600
+ event.id,
601
+ event.postId,
602
+ event.threadId,
603
+ event.eventType,
604
+ event.revision,
605
+ event.message ? JSON.stringify(event.message) : null,
606
+ event.delta ? JSON.stringify(event.delta) : null,
607
+ event.mentions ? JSON.stringify(event.mentions) : null,
608
+ event.model ? JSON.stringify(event.model) : null,
609
+ event.usage ? JSON.stringify(event.usage) : null,
610
+ event.idempotencyKey ?? null,
611
+ event.createdAtMs
612
+ ]);
613
+ }
614
+ async function getStreamIdempotency(db, key) {
615
+ return get(db, `SELECT post_id, revision FROM chat_post_stream_events
616
+ WHERE idempotency_key = ?`, [key]);
617
+ }
618
+ async function listPostStreamEvents(db, postId) {
619
+ const rows = await all(db, `SELECT id, post_id, thread_id, event_type, revision, message, delta, mentions, model, usage, idempotency_key, created_at_ms
620
+ FROM chat_post_stream_events WHERE post_id = ? ORDER BY revision ASC`, [postId]);
621
+ return rows.map((row) => ({
622
+ id: row.id,
623
+ postId: row.post_id,
624
+ threadId: row.thread_id,
625
+ eventType: row.event_type,
626
+ revision: row.revision,
627
+ message: parseJson(row.message),
628
+ delta: parseJson(row.delta),
629
+ mentions: parseJson(row.mentions),
630
+ model: parseJson(row.model),
631
+ usage: parseJson(row.usage),
632
+ idempotencyKey: row.idempotency_key ?? undefined,
633
+ createdAtMs: row.created_at_ms
634
+ }));
635
+ }
636
+ async function startStreamedPost(db, input, getVersion) {
637
+ if (input.idempotencyKey) {
638
+ const existing = await getStreamIdempotency(db, input.idempotencyKey);
639
+ if (existing) {
640
+ const row = await getPostRow(db, existing.post_id);
641
+ if (!row)
642
+ throw new ChatNotFoundError("post", existing.post_id);
643
+ const post = postFromRow(row, getVersion);
644
+ if (post?.status !== "streaming") {
645
+ throw new ChatConflictError("idempotency_mismatch", "idempotency key reused");
646
+ }
647
+ return { post, revision: existing.revision };
648
+ }
649
+ }
650
+ return transaction(db, async () => {
651
+ const thread = await get(db, "SELECT id FROM chat_threads WHERE id = ?", [
652
+ input.threadId
653
+ ]);
654
+ if (!thread)
655
+ throw new ChatNotFoundError("thread", input.threadId);
656
+ const maxIndexRow = await get(db, "SELECT COALESCE(MAX(post_index), 0) AS max_index FROM chat_posts WHERE thread_id = ?", [input.threadId]);
657
+ const nextIndex = (maxIndexRow?.max_index ?? 0) + 1;
658
+ const postId = input.message.id || createId();
659
+ const createdAtMs = Date.now();
660
+ const message = { ...input.message, id: postId };
661
+ const revision = 1;
662
+ await run(db, `INSERT INTO chat_posts
663
+ (id, thread_id, post_index, status, stream_message, stream_mentions, stream_model, stream_usage,
664
+ stream_author_scope_type, stream_author_scope_id, stream_revision,
665
+ completed_version_id, created_at_ms, updated_at_ms, deleted_at_ms)
666
+ VALUES (?, ?, ?, 'streaming', ?, ?, ?, ?, ?, ?, ?, NULL, ?, NULL, NULL)`, [
667
+ postId,
668
+ input.threadId,
669
+ nextIndex,
670
+ JSON.stringify(message),
671
+ input.mentions ? JSON.stringify(input.mentions) : null,
672
+ input.model ? JSON.stringify(input.model) : null,
673
+ input.usage ? JSON.stringify(input.usage) : null,
674
+ input.author.type,
675
+ input.author.id,
676
+ revision,
677
+ createdAtMs
678
+ ]);
679
+ await insertStreamEvent(db, {
680
+ id: createId(),
681
+ postId,
682
+ threadId: input.threadId,
683
+ eventType: "stream.started",
684
+ revision,
685
+ message,
686
+ mentions: input.mentions,
687
+ model: input.model,
688
+ usage: input.usage,
689
+ idempotencyKey: input.idempotencyKey,
690
+ createdAtMs
691
+ });
692
+ const post = streamingPostFromCache({
693
+ postId,
694
+ threadId: input.threadId,
695
+ author: input.author,
696
+ message,
697
+ mentions: input.mentions,
698
+ model: input.model,
699
+ usage: input.usage,
700
+ index: nextIndex,
701
+ streamRevision: revision,
702
+ createdAtMs
703
+ });
704
+ return { post, revision };
705
+ });
706
+ }
707
+ async function applyPostDelta(db, input, getVersion) {
708
+ if (input.idempotencyKey) {
709
+ const existing = await getStreamIdempotency(db, input.idempotencyKey);
710
+ if (existing && existing.post_id === input.postId) {
711
+ const row = await getPostRow(db, input.postId);
712
+ if (!row)
713
+ throw new ChatNotFoundError("post", input.postId);
714
+ const post = postFromRow(row, getVersion);
715
+ if (post?.status !== "streaming") {
716
+ throw new ChatConflictError("idempotency_mismatch", "idempotency key reused");
717
+ }
718
+ return { post, revision: existing.revision };
719
+ }
720
+ }
721
+ return transaction(db, async () => {
722
+ const row = await getPostRow(db, input.postId);
723
+ if (!row)
724
+ throw new ChatNotFoundError("post", input.postId);
725
+ if (row.status !== "streaming") {
726
+ throw new ChatValidationError(`post ${input.postId} is not streaming`);
727
+ }
728
+ if (input.expectedRevision !== undefined && row.stream_revision !== input.expectedRevision) {
729
+ throw new ChatConflictError("stream_revision_conflict", `expected revision ${input.expectedRevision}, current ${row.stream_revision}`);
730
+ }
731
+ const revision = row.stream_revision + 1;
732
+ const updatedAtMs = Date.now();
733
+ const message = { ...input.message, id: input.postId };
734
+ const mentions = input.mentions ?? parseJson(row.stream_mentions);
735
+ const model = input.model ?? parseJson(row.stream_model);
736
+ const usage = input.usage ?? parseJson(row.stream_usage);
737
+ await run(db, `UPDATE chat_posts
738
+ SET stream_message = ?, stream_mentions = ?, stream_model = ?, stream_usage = ?, stream_revision = ?, updated_at_ms = ?
739
+ WHERE id = ?`, [
740
+ JSON.stringify(message),
741
+ mentions ? JSON.stringify(mentions) : null,
742
+ model ? JSON.stringify(model) : null,
743
+ usage ? JSON.stringify(usage) : null,
744
+ revision,
745
+ updatedAtMs,
746
+ input.postId
747
+ ]);
748
+ await insertStreamEvent(db, {
749
+ id: createId(),
750
+ postId: input.postId,
751
+ threadId: row.thread_id,
752
+ eventType: "stream.delta",
753
+ revision,
754
+ message,
755
+ delta: input.delta,
756
+ mentions,
757
+ model,
758
+ usage,
759
+ idempotencyKey: input.idempotencyKey,
760
+ createdAtMs: updatedAtMs
761
+ });
762
+ const post = streamingPostFromCache({
763
+ postId: input.postId,
764
+ threadId: row.thread_id,
765
+ author: requireStreamAuthor(row),
766
+ message,
767
+ mentions,
768
+ model,
769
+ usage,
770
+ index: row.post_index,
771
+ streamRevision: revision,
772
+ createdAtMs: row.created_at_ms,
773
+ updatedAtMs,
774
+ deletedAtMs: row.deleted_at_ms
775
+ });
776
+ return { post, revision };
777
+ });
778
+ }
779
+ async function completeStreamedPost(db, input) {
780
+ return transaction(db, async () => {
781
+ const row = await getPostRow(db, input.postId);
782
+ if (!row)
783
+ throw new ChatNotFoundError("post", input.postId);
784
+ if (row.status !== "streaming") {
785
+ throw new ChatValidationError(`post ${input.postId} is not streaming`);
786
+ }
787
+ if (input.expectedRevision !== undefined && row.stream_revision !== input.expectedRevision) {
788
+ throw new ChatConflictError("stream_revision_conflict", `expected revision ${input.expectedRevision}, current ${row.stream_revision}`);
789
+ }
790
+ const message = parseJson(row.stream_message);
791
+ if (!message)
792
+ throw new ChatValidationError(`post ${input.postId} has no streamed message`);
793
+ const model = parseJson(row.stream_model);
794
+ const usage = parseJson(row.stream_usage);
795
+ const thread = await get(db, "SELECT id, default_head_id FROM chat_threads WHERE id = ?", [row.thread_id]);
796
+ if (!thread)
797
+ throw new ChatNotFoundError("thread", row.thread_id);
798
+ const currentHead = thread.default_head_id ? await get(db, "SELECT * FROM chat_thread_heads WHERE id = ?", [thread.default_head_id]) : null;
799
+ if (input.expectedHeadPostVersionId !== undefined && (currentHead?.head_post_version_id ?? null) !== (input.expectedHeadPostVersionId ?? null)) {
800
+ if (!currentHead)
801
+ throw new ChatConflictError("head_conflict", "expected head but thread is empty");
802
+ return {
803
+ ok: false,
804
+ reason: "head_conflict",
805
+ currentHead: {
806
+ id: currentHead.id,
807
+ threadId: currentHead.thread_id,
808
+ name: currentHead.name,
809
+ headPostVersionId: currentHead.head_post_version_id,
810
+ createdAtMs: currentHead.created_at_ms
811
+ }
812
+ };
813
+ }
814
+ const previousVersion = currentHead ? await get(db, "SELECT * FROM chat_post_versions WHERE id = ?", [currentHead.head_post_version_id]) : null;
815
+ const prepared = prepareAppendPost({
816
+ threadId: row.thread_id,
817
+ author: requireStreamAuthor(row),
818
+ message,
819
+ mentions: parseJson(row.stream_mentions),
820
+ model,
821
+ usage,
822
+ previousPostVersionId: previousVersion?.id ?? null,
823
+ previousLineageHash: previousVersion?.lineage_hash ?? null
824
+ });
825
+ await run(db, `INSERT INTO chat_post_versions
826
+ (id, post_id, thread_id, parent_version_id, previous_post_version_id,
827
+ author_scope_type, author_scope_id, message, mentions, model, usage, content_hash, lineage_hash,
828
+ signature, idempotency_key, created_at_ms)
829
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
830
+ prepared.versionId,
831
+ prepared.postId,
832
+ prepared.threadId,
833
+ null,
834
+ prepared.previousPostVersionId,
835
+ prepared.author.type,
836
+ prepared.author.id,
837
+ JSON.stringify(prepared.message),
838
+ prepared.mentions ? JSON.stringify(prepared.mentions) : null,
839
+ prepared.model ? JSON.stringify(prepared.model) : null,
840
+ prepared.usage ? JSON.stringify(prepared.usage) : null,
841
+ prepared.contentHash,
842
+ prepared.lineageHash,
843
+ null,
844
+ input.idempotencyKey ?? null,
845
+ prepared.createdAtMs
846
+ ]);
847
+ const head = currentHead ? {
848
+ id: currentHead.id,
849
+ threadId: row.thread_id,
850
+ name: currentHead.name,
851
+ headPostVersionId: prepared.versionId,
852
+ createdAtMs: currentHead.created_at_ms
853
+ } : {
854
+ id: createId(),
855
+ threadId: row.thread_id,
856
+ name: DEFAULT_HEAD_NAME,
857
+ headPostVersionId: prepared.versionId,
858
+ createdAtMs: prepared.createdAtMs
859
+ };
860
+ if (currentHead) {
861
+ await run(db, "UPDATE chat_thread_heads SET head_post_version_id = ? WHERE id = ?", [
862
+ prepared.versionId,
863
+ head.id
864
+ ]);
865
+ } else {
866
+ await run(db, `INSERT INTO chat_thread_heads (id, thread_id, name, head_post_version_id, created_at_ms)
867
+ VALUES (?, ?, ?, ?, ?)`, [head.id, head.threadId, head.name, head.headPostVersionId, head.createdAtMs]);
868
+ await run(db, "UPDATE chat_threads SET default_head_id = ? WHERE id = ?", [
869
+ head.id,
870
+ row.thread_id
871
+ ]);
872
+ }
873
+ const completedRevision = row.stream_revision + 1;
874
+ const completedAtMs = Date.now();
875
+ await insertStreamEvent(db, {
876
+ id: createId(),
877
+ postId: input.postId,
878
+ threadId: row.thread_id,
879
+ eventType: "stream.completed",
880
+ revision: completedRevision,
881
+ message,
882
+ mentions: parseJson(row.stream_mentions),
883
+ model,
884
+ usage,
885
+ createdAtMs: completedAtMs
886
+ });
887
+ await run(db, `UPDATE chat_posts
888
+ SET status = 'complete', stream_message = NULL, stream_mentions = NULL,
889
+ stream_model = NULL, stream_usage = NULL,
890
+ stream_author_scope_type = NULL, stream_author_scope_id = NULL,
891
+ stream_revision = ?, completed_version_id = ?, updated_at_ms = ?
892
+ WHERE id = ?`, [completedRevision, prepared.versionId, completedAtMs, input.postId]);
893
+ const version = {
894
+ ...prepared.message,
895
+ id: prepared.versionId,
896
+ postId: prepared.postId,
897
+ threadId: prepared.threadId,
898
+ parentVersionId: null,
899
+ previousPostVersionId: prepared.previousPostVersionId,
900
+ author: prepared.author,
901
+ contentHash: prepared.contentHash,
902
+ lineageHash: prepared.lineageHash,
903
+ mentions: prepared.mentions,
904
+ model: prepared.model,
905
+ usage: prepared.usage,
906
+ createdAtMs: prepared.createdAtMs
907
+ };
908
+ return {
909
+ ok: true,
910
+ post: postFromVersion(version, row.post_index, row.deleted_at_ms),
911
+ head
912
+ };
913
+ });
914
+ }
915
+ async function abortStreamedPost(db, input, getVersion) {
916
+ return transaction(db, async () => {
917
+ const row = await getPostRow(db, input.postId);
918
+ if (!row)
919
+ throw new ChatNotFoundError("post", input.postId);
920
+ if (row.status !== "streaming") {
921
+ throw new ChatValidationError(`post ${input.postId} is not streaming`);
922
+ }
923
+ const deletedAtMs = input.deletedAtMs ?? Date.now();
924
+ const revision = row.stream_revision + 1;
925
+ await insertStreamEvent(db, {
926
+ id: createId(),
927
+ postId: input.postId,
928
+ threadId: row.thread_id,
929
+ eventType: "stream.aborted",
930
+ revision,
931
+ message: parseJson(row.stream_message),
932
+ mentions: parseJson(row.stream_mentions),
933
+ model: parseJson(row.stream_model),
934
+ usage: parseJson(row.stream_usage),
935
+ createdAtMs: deletedAtMs
936
+ });
937
+ await run(db, `UPDATE chat_posts SET status = 'aborted', stream_revision = ?, deleted_at_ms = ?, updated_at_ms = ? WHERE id = ?`, [revision, deletedAtMs, deletedAtMs, input.postId]);
938
+ const updated = await getPostRow(db, input.postId);
939
+ if (!updated)
940
+ throw new ChatNotFoundError("post", input.postId);
941
+ const post = postFromRow(updated, getVersion);
942
+ if (post?.status !== "aborted")
943
+ throw new ChatNotFoundError("post", input.postId);
944
+ return { post };
945
+ });
946
+ }
947
+ async function rebuildStreamedPostCache(db, postId, _getVersion) {
948
+ const row = await getPostRow(db, postId);
949
+ if (!row)
950
+ throw new ChatNotFoundError("post", postId);
951
+ if (row.status !== "streaming") {
952
+ throw new ChatValidationError(`post ${postId} is not streaming`);
953
+ }
954
+ const events2 = await listPostStreamEvents(db, postId);
955
+ const rebuilt = rebuildStreamCacheFromEvents(events2);
956
+ const updatedAtMs = Date.now();
957
+ await run(db, `UPDATE chat_posts SET stream_message = ?, stream_mentions = ?, stream_model = ?, stream_usage = ?, stream_revision = ?, updated_at_ms = ? WHERE id = ?`, [
958
+ JSON.stringify(rebuilt.message),
959
+ rebuilt.mentions ? JSON.stringify(rebuilt.mentions) : null,
960
+ rebuilt.model ? JSON.stringify(rebuilt.model) : null,
961
+ rebuilt.usage ? JSON.stringify(rebuilt.usage) : null,
962
+ rebuilt.revision,
963
+ updatedAtMs,
964
+ postId
965
+ ]);
966
+ return streamingPostFromCache({
967
+ postId,
968
+ threadId: row.thread_id,
969
+ author: requireStreamAuthor(row),
970
+ message: rebuilt.message,
971
+ mentions: rebuilt.mentions,
972
+ model: rebuilt.model,
973
+ usage: rebuilt.usage,
974
+ index: row.post_index,
975
+ streamRevision: rebuilt.revision,
976
+ createdAtMs: row.created_at_ms,
977
+ updatedAtMs,
978
+ deletedAtMs: row.deleted_at_ms
979
+ });
980
+ }
981
+
982
+ // src/persistence/turso-serverless/persistence/persistence.ts
983
+ var DEFAULT_HEAD_NAME2 = "default";
984
+ function parseJson2(value) {
985
+ if (!value)
986
+ return;
987
+ return JSON.parse(value);
988
+ }
989
+ function parseSignature(value) {
990
+ return parseJson2(value);
991
+ }
992
+ function requireAclSubjectScope(scope, eventId) {
993
+ if (!scope) {
994
+ throw new ChatValidationError(`ACL event ${eventId} is missing subject scope`);
995
+ }
996
+ return scope;
997
+ }
998
+ function requireAclRole(role, eventId) {
999
+ if (!role) {
1000
+ throw new ChatValidationError(`ACL event ${eventId} is missing role`);
1001
+ }
1002
+ return role;
1003
+ }
1004
+ function aclEventFromRow(row) {
1005
+ const actor = scopeFromRow2({
1006
+ scope_type: row.actor_scope_type,
1007
+ scope_id: row.actor_scope_id
1008
+ });
1009
+ const scope = row.subject_scope_type && row.subject_scope_id ? scopeFromRow2({
1010
+ scope_type: row.subject_scope_type,
1011
+ scope_id: row.subject_scope_id
1012
+ }) : undefined;
1013
+ const base = {
1014
+ id: row.id,
1015
+ previousAclEventId: row.previous_acl_event_id,
1016
+ contentHash: row.content_hash,
1017
+ signature: parseSignature(row.signature),
1018
+ createdAtMs: row.created_at_ms,
1019
+ actor
1020
+ };
1021
+ if (row.event_type === "channel.member.added") {
1022
+ return {
1023
+ ...base,
1024
+ type: "channel.member.added",
1025
+ channelId: row.target_id,
1026
+ scope: requireAclSubjectScope(scope, row.id),
1027
+ role: requireAclRole(row.role, row.id)
1028
+ };
1029
+ }
1030
+ if (row.event_type === "channel.member.removed") {
1031
+ return {
1032
+ ...base,
1033
+ type: "channel.member.removed",
1034
+ channelId: row.target_id,
1035
+ scope: requireAclSubjectScope(scope, row.id)
1036
+ };
1037
+ }
1038
+ if (row.event_type === "thread.participant.added") {
1039
+ return {
1040
+ ...base,
1041
+ type: "thread.participant.added",
1042
+ threadId: row.target_id,
1043
+ scope: requireAclSubjectScope(scope, row.id),
1044
+ role: requireAclRole(row.role, row.id)
1045
+ };
1046
+ }
1047
+ return {
1048
+ ...base,
1049
+ type: "thread.participant.removed",
1050
+ threadId: row.target_id,
1051
+ scope: requireAclSubjectScope(scope, row.id)
1052
+ };
1053
+ }
1054
+ function scopeFromRow2(row) {
1055
+ return { type: row.scope_type, id: row.scope_id };
1056
+ }
1057
+ function encodeThreadRoot(root) {
1058
+ if (root.type === "channel") {
1059
+ return {
1060
+ root_type: "channel",
1061
+ root_id: root.channelId,
1062
+ root_version_id: null
1063
+ };
1064
+ }
1065
+ return {
1066
+ root_type: "post",
1067
+ root_id: root.postId,
1068
+ root_version_id: root.versionId ?? null
1069
+ };
1070
+ }
1071
+ function decodeThreadRoot(row) {
1072
+ if (row.root_type === "channel") {
1073
+ return { type: "channel", channelId: row.root_id };
1074
+ }
1075
+ return {
1076
+ type: "post",
1077
+ postId: row.root_id,
1078
+ versionId: row.root_version_id ?? undefined
1079
+ };
1080
+ }
1081
+ function versionFromRow(row) {
1082
+ const message = JSON.parse(row.message);
1083
+ return {
1084
+ ...message,
1085
+ id: row.id,
1086
+ postId: row.post_id,
1087
+ threadId: row.thread_id,
1088
+ parentVersionId: row.parent_version_id,
1089
+ previousPostVersionId: row.previous_post_version_id,
1090
+ author: scopeFromRow2({
1091
+ scope_type: row.author_scope_type,
1092
+ scope_id: row.author_scope_id
1093
+ }),
1094
+ contentHash: row.content_hash,
1095
+ lineageHash: row.lineage_hash,
1096
+ signature: parseSignature(row.signature),
1097
+ mentions: parseJson2(row.mentions),
1098
+ model: parseJson2(row.model),
1099
+ usage: parseJson2(row.usage),
1100
+ createdAtMs: row.created_at_ms
1101
+ };
1102
+ }
1103
+
1104
+ class TursoChatPersistence extends BaseChatPersistence {
1105
+ db;
1106
+ constructor(db) {
1107
+ super();
1108
+ this.db = db;
1109
+ }
1110
+ async getPostVersionInternal(id) {
1111
+ const row = await get(this.db, "SELECT * FROM chat_post_versions WHERE id = ?", [id]);
1112
+ return row ? versionFromRow(row) : null;
1113
+ }
1114
+ async createChannel(input) {
1115
+ const channel = {
1116
+ id: input.id ?? createId(),
1117
+ metadata: input.metadata,
1118
+ createdAtMs: Date.now()
1119
+ };
1120
+ await run(this.db, "INSERT INTO chat_channels (id, metadata, created_at_ms) VALUES (?, ?, ?)", [
1121
+ channel.id,
1122
+ channel.metadata ? JSON.stringify(channel.metadata) : null,
1123
+ channel.createdAtMs
1124
+ ]);
1125
+ return channel;
1126
+ }
1127
+ async createThread(input) {
1128
+ this.validateCreateThreadInput(input);
1129
+ if (input.root.type === "channel") {
1130
+ await this.requireChannel(input.root.channelId);
1131
+ } else {
1132
+ await this.requirePost(input.root.postId);
1133
+ }
1134
+ const thread = {
1135
+ id: input.id ?? createId(),
1136
+ root: input.root,
1137
+ defaultHeadId: null,
1138
+ metadata: input.metadata,
1139
+ createdAtMs: Date.now()
1140
+ };
1141
+ const encoded = encodeThreadRoot(thread.root);
1142
+ await run(this.db, `INSERT INTO chat_threads
1143
+ (id, root_type, root_id, root_version_id, default_head_id, metadata, created_at_ms, archived_at_ms)
1144
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
1145
+ thread.id,
1146
+ encoded.root_type,
1147
+ encoded.root_id,
1148
+ encoded.root_version_id,
1149
+ null,
1150
+ thread.metadata ? JSON.stringify(thread.metadata) : null,
1151
+ thread.createdAtMs,
1152
+ null
1153
+ ]);
1154
+ return thread;
1155
+ }
1156
+ async getChannel(id) {
1157
+ const row = await get(this.db, "SELECT id, metadata, created_at_ms FROM chat_channels WHERE id = ?", [id]);
1158
+ if (!row)
1159
+ return null;
1160
+ return {
1161
+ id: row.id,
1162
+ metadata: parseJson2(row.metadata),
1163
+ createdAtMs: row.created_at_ms
1164
+ };
1165
+ }
1166
+ async getThread(id) {
1167
+ const row = await get(this.db, `SELECT id, root_type, root_id, root_version_id, default_head_id, metadata, created_at_ms, archived_at_ms
1168
+ FROM chat_threads WHERE id = ?`, [id]);
1169
+ if (!row)
1170
+ return null;
1171
+ return {
1172
+ id: row.id,
1173
+ root: decodeThreadRoot(row),
1174
+ defaultHeadId: row.default_head_id,
1175
+ metadata: parseJson2(row.metadata),
1176
+ createdAtMs: row.created_at_ms,
1177
+ archivedAtMs: row.archived_at_ms ?? undefined
1178
+ };
1179
+ }
1180
+ async getPost(id) {
1181
+ const postRow = await get(this.db, `SELECT id, thread_id, post_index, status, stream_message, stream_mentions, stream_model, stream_usage,
1182
+ stream_author_scope_type, stream_author_scope_id, stream_revision,
1183
+ completed_version_id, created_at_ms, updated_at_ms, deleted_at_ms
1184
+ FROM chat_posts WHERE id = ?`, [id]);
1185
+ if (!postRow)
1186
+ return null;
1187
+ const versionMap = new Map;
1188
+ if (postRow.completed_version_id) {
1189
+ const version = await this.getPostVersionInternal(postRow.completed_version_id);
1190
+ if (version) {
1191
+ versionMap.set(postRow.completed_version_id, version);
1192
+ }
1193
+ }
1194
+ return postFromRow(postRow, (versionId) => versionMap.get(versionId) ?? null);
1195
+ }
1196
+ async getPostVersion(id) {
1197
+ return this.getPostVersionInternal(id);
1198
+ }
1199
+ async getThreadHead(threadId, headId) {
1200
+ const thread = await this.requireThread(threadId);
1201
+ const resolvedHeadId = headId ?? thread.defaultHeadId;
1202
+ if (!resolvedHeadId)
1203
+ return null;
1204
+ const row = await get(this.db, "SELECT * FROM chat_thread_heads WHERE id = ?", [resolvedHeadId]);
1205
+ if (!row || row.thread_id !== threadId)
1206
+ return null;
1207
+ return {
1208
+ id: row.id,
1209
+ threadId: row.thread_id,
1210
+ name: row.name,
1211
+ headPostVersionId: row.head_post_version_id,
1212
+ createdAtMs: row.created_at_ms
1213
+ };
1214
+ }
1215
+ async getThreadTip(threadId) {
1216
+ const head = await this.getThreadHead(threadId);
1217
+ if (!head)
1218
+ return null;
1219
+ const version = await this.getPostVersion(head.headPostVersionId);
1220
+ if (!version)
1221
+ return null;
1222
+ return { id: version.id, lineageHash: version.lineageHash };
1223
+ }
1224
+ async listThreads(input) {
1225
+ const limit = input.limit ?? 50;
1226
+ const start = input.cursor ? Number.parseInt(input.cursor, 10) : 0;
1227
+ const rows = await this.listThreadRows(input);
1228
+ const page = rows.slice(start, start + limit);
1229
+ return {
1230
+ items: page.map((row) => ({
1231
+ id: row.id,
1232
+ root: decodeThreadRoot(row),
1233
+ defaultHeadId: row.default_head_id,
1234
+ metadata: parseJson2(row.metadata),
1235
+ createdAtMs: row.created_at_ms,
1236
+ archivedAtMs: row.archived_at_ms ?? undefined
1237
+ })),
1238
+ nextCursor: start + limit < rows.length ? String(start + limit) : null
1239
+ };
1240
+ }
1241
+ async listThreadRows(input) {
1242
+ const participant = input.participant;
1243
+ if (participant && input.channelId) {
1244
+ return all(this.db, `SELECT t.*
1245
+ FROM chat_threads t
1246
+ INNER JOIN chat_thread_participants p ON p.thread_id = t.id
1247
+ WHERE p.scope_type = ? AND p.scope_id = ?
1248
+ AND t.root_type = 'channel' AND t.root_id = ?
1249
+ ORDER BY t.created_at_ms ASC`, [participant.type, participant.id, input.channelId]);
1250
+ }
1251
+ if (participant && input.postId) {
1252
+ return all(this.db, `SELECT t.*
1253
+ FROM chat_threads t
1254
+ INNER JOIN chat_thread_participants p ON p.thread_id = t.id
1255
+ WHERE p.scope_type = ? AND p.scope_id = ?
1256
+ AND t.root_type = 'post' AND t.root_id = ?
1257
+ ORDER BY t.created_at_ms ASC`, [participant.type, participant.id, input.postId]);
1258
+ }
1259
+ if (participant) {
1260
+ return all(this.db, `SELECT t.*
1261
+ FROM chat_threads t
1262
+ INNER JOIN chat_thread_participants p ON p.thread_id = t.id
1263
+ WHERE p.scope_type = ? AND p.scope_id = ?
1264
+ ORDER BY t.created_at_ms ASC`, [participant.type, participant.id]);
1265
+ }
1266
+ if (input.channelId) {
1267
+ return all(this.db, `SELECT * FROM chat_threads
1268
+ WHERE root_type = 'channel' AND root_id = ?
1269
+ ORDER BY created_at_ms ASC`, [input.channelId]);
1270
+ }
1271
+ if (input.postId) {
1272
+ return all(this.db, `SELECT * FROM chat_threads
1273
+ WHERE root_type = 'post' AND root_id = ?
1274
+ ORDER BY created_at_ms ASC`, [input.postId]);
1275
+ }
1276
+ return all(this.db, "SELECT * FROM chat_threads ORDER BY created_at_ms ASC");
1277
+ }
1278
+ async listPosts(input) {
1279
+ const thread = await this.requireThread(input.threadId);
1280
+ const head = input.headPostVersionId ? {
1281
+ id: "inline",
1282
+ threadId: input.threadId,
1283
+ name: "inline",
1284
+ headPostVersionId: input.headPostVersionId,
1285
+ createdAtMs: 0
1286
+ } : await this.getThreadHead(input.threadId, input.headId ?? thread.defaultHeadId ?? undefined);
1287
+ const versionRows = await all(this.db, "SELECT * FROM chat_post_versions WHERE thread_id = ?", [input.threadId]);
1288
+ const versionsById = new Map(versionRows.map((row) => [row.id, versionFromRow(row)]));
1289
+ const postMetaRows = await all(this.db, "SELECT id, post_index, deleted_at_ms FROM chat_posts WHERE thread_id = ?", [
1290
+ input.threadId
1291
+ ]);
1292
+ const postsById = new Map(postMetaRows.map((row) => [row.id, row]));
1293
+ const lineagePosts = head ? walkLineageFromHead(head.headPostVersionId, versionsById).flatMap((version) => {
1294
+ const postRow = postsById.get(version.postId);
1295
+ if (!postRow)
1296
+ return [];
1297
+ return [
1298
+ {
1299
+ index: postRow.post_index,
1300
+ post: postFromVersion(version, postRow.post_index, postRow.deleted_at_ms)
1301
+ }
1302
+ ];
1303
+ }) : [];
1304
+ const activeRows = await all(this.db, `SELECT id, thread_id, post_index, status, stream_message, stream_mentions, stream_model, stream_usage,
1305
+ stream_author_scope_type, stream_author_scope_id, stream_revision,
1306
+ completed_version_id, created_at_ms, updated_at_ms, deleted_at_ms
1307
+ FROM chat_posts
1308
+ WHERE thread_id = ? AND status != 'complete'
1309
+ ORDER BY post_index ASC`, [input.threadId]);
1310
+ const activePosts = activeRows.map((row) => postFromRow(row, (versionId) => versionsById.get(versionId) ?? null)).filter((post) => post !== null && (post.status === "streaming" || post.status === "aborted"));
1311
+ const merged = mergeThreadPostsForList(lineagePosts, activePosts);
1312
+ const limit = input.limit ?? 50;
1313
+ const start = input.cursor ? Number.parseInt(input.cursor, 10) : 0;
1314
+ const items = merged.slice(start, start + limit);
1315
+ return {
1316
+ items,
1317
+ nextCursor: start + limit < merged.length ? String(start + limit) : null
1318
+ };
1319
+ }
1320
+ async appendPost(input) {
1321
+ if (input.idempotencyKey) {
1322
+ const existing = await get(this.db, `SELECT pv.*, cp.post_index, cp.deleted_at_ms
1323
+ FROM chat_post_versions pv
1324
+ JOIN chat_posts cp ON cp.id = pv.post_id
1325
+ WHERE pv.idempotency_key = ?`, [input.idempotencyKey]);
1326
+ if (existing) {
1327
+ const version = versionFromRow(existing);
1328
+ const head = await this.getThreadHead(input.threadId);
1329
+ if (!head) {
1330
+ throw new ChatNotFoundError("thread_head", input.threadId);
1331
+ }
1332
+ return {
1333
+ ok: true,
1334
+ post: postFromVersion(version, existing.post_index, existing.deleted_at_ms),
1335
+ head
1336
+ };
1337
+ }
1338
+ }
1339
+ return transaction(this.db, async () => {
1340
+ const thread = await get(this.db, "SELECT * FROM chat_threads WHERE id = ?", [input.threadId]);
1341
+ if (!thread)
1342
+ throw new ChatNotFoundError("thread", input.threadId);
1343
+ const currentHead = thread.default_head_id ? await get(this.db, "SELECT * FROM chat_thread_heads WHERE id = ?", [thread.default_head_id]) : null;
1344
+ if (input.expectedHeadPostVersionId !== undefined && (currentHead?.head_post_version_id ?? null) !== (input.expectedHeadPostVersionId ?? null)) {
1345
+ if (!currentHead) {
1346
+ throw new ChatConflictError("head_conflict", "expected head but thread is empty");
1347
+ }
1348
+ return {
1349
+ ok: false,
1350
+ reason: "head_conflict",
1351
+ currentHead: {
1352
+ id: currentHead.id,
1353
+ threadId: currentHead.thread_id,
1354
+ name: currentHead.name,
1355
+ headPostVersionId: currentHead.head_post_version_id,
1356
+ createdAtMs: currentHead.created_at_ms
1357
+ }
1358
+ };
1359
+ }
1360
+ const previousVersionRow = currentHead ? await get(this.db, "SELECT * FROM chat_post_versions WHERE id = ?", [currentHead.head_post_version_id]) : null;
1361
+ const previousVersion = previousVersionRow ? versionFromRow(previousVersionRow) : null;
1362
+ const prepared = prepareAppendPost2({
1363
+ ...input,
1364
+ previousPostVersionId: previousVersion?.id ?? null,
1365
+ previousLineageHash: previousVersion?.lineageHash ?? null
1366
+ });
1367
+ const maxIndexRow = await get(this.db, "SELECT COALESCE(MAX(post_index), 0) AS max_index FROM chat_posts WHERE thread_id = ?", [input.threadId]);
1368
+ const nextIndex = (maxIndexRow?.max_index ?? 0) + 1;
1369
+ await run(this.db, `INSERT INTO chat_posts
1370
+ (id, thread_id, post_index, status, stream_revision, completed_version_id,
1371
+ created_at_ms, updated_at_ms, deleted_at_ms)
1372
+ VALUES (?, ?, ?, 'complete', 0, ?, ?, NULL, NULL)`, [prepared.postId, prepared.threadId, nextIndex, prepared.versionId, prepared.createdAtMs]);
1373
+ await run(this.db, `INSERT INTO chat_post_versions
1374
+ (id, post_id, thread_id, parent_version_id, previous_post_version_id,
1375
+ author_scope_type, author_scope_id, message, mentions, model, usage, content_hash, lineage_hash,
1376
+ signature, idempotency_key, created_at_ms)
1377
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1378
+ prepared.versionId,
1379
+ prepared.postId,
1380
+ prepared.threadId,
1381
+ null,
1382
+ prepared.previousPostVersionId,
1383
+ prepared.author.type,
1384
+ prepared.author.id,
1385
+ JSON.stringify(prepared.message),
1386
+ prepared.mentions ? JSON.stringify(prepared.mentions) : null,
1387
+ prepared.model ? JSON.stringify(prepared.model) : null,
1388
+ prepared.usage ? JSON.stringify(prepared.usage) : null,
1389
+ prepared.contentHash,
1390
+ prepared.lineageHash,
1391
+ input.signature ? JSON.stringify(input.signature) : null,
1392
+ input.idempotencyKey ?? null,
1393
+ prepared.createdAtMs
1394
+ ]);
1395
+ const head = currentHead ? {
1396
+ id: currentHead.id,
1397
+ threadId: input.threadId,
1398
+ name: currentHead.name,
1399
+ headPostVersionId: prepared.versionId,
1400
+ createdAtMs: currentHead.created_at_ms
1401
+ } : {
1402
+ id: createId(),
1403
+ threadId: input.threadId,
1404
+ name: DEFAULT_HEAD_NAME2,
1405
+ headPostVersionId: prepared.versionId,
1406
+ createdAtMs: prepared.createdAtMs
1407
+ };
1408
+ if (currentHead) {
1409
+ await run(this.db, "UPDATE chat_thread_heads SET head_post_version_id = ? WHERE id = ?", [
1410
+ prepared.versionId,
1411
+ head.id
1412
+ ]);
1413
+ } else {
1414
+ await run(this.db, `INSERT INTO chat_thread_heads
1415
+ (id, thread_id, name, head_post_version_id, created_at_ms)
1416
+ VALUES (?, ?, ?, ?, ?)`, [head.id, head.threadId, head.name, head.headPostVersionId, head.createdAtMs]);
1417
+ await run(this.db, "UPDATE chat_threads SET default_head_id = ? WHERE id = ?", [
1418
+ head.id,
1419
+ input.threadId
1420
+ ]);
1421
+ }
1422
+ const version = {
1423
+ ...prepared.message,
1424
+ id: prepared.versionId,
1425
+ postId: prepared.postId,
1426
+ threadId: prepared.threadId,
1427
+ parentVersionId: null,
1428
+ previousPostVersionId: prepared.previousPostVersionId,
1429
+ author: prepared.author,
1430
+ contentHash: prepared.contentHash,
1431
+ lineageHash: prepared.lineageHash,
1432
+ mentions: prepared.mentions,
1433
+ createdAtMs: prepared.createdAtMs
1434
+ };
1435
+ return {
1436
+ ok: true,
1437
+ post: postFromVersion(version, nextIndex),
1438
+ head
1439
+ };
1440
+ });
1441
+ }
1442
+ async editPost(input) {
1443
+ if (input.idempotencyKey) {
1444
+ const existing = await get(this.db, `SELECT pv.*, cp.post_index, cp.deleted_at_ms
1445
+ FROM chat_post_versions pv
1446
+ JOIN chat_posts cp ON cp.id = pv.post_id
1447
+ WHERE pv.idempotency_key = ?`, [input.idempotencyKey]);
1448
+ if (existing) {
1449
+ const version = versionFromRow(existing);
1450
+ const head = await this.getThreadHead(version.threadId);
1451
+ if (!head) {
1452
+ throw new ChatNotFoundError("thread_head", version.threadId);
1453
+ }
1454
+ return {
1455
+ ok: true,
1456
+ post: postFromVersion(version, existing.post_index, existing.deleted_at_ms),
1457
+ head
1458
+ };
1459
+ }
1460
+ }
1461
+ return transaction(this.db, async () => {
1462
+ const parentRow = await get(this.db, "SELECT * FROM chat_post_versions WHERE id = ?", [input.parentVersionId]);
1463
+ if (!parentRow) {
1464
+ throw new ChatNotFoundError("post_version", input.parentVersionId);
1465
+ }
1466
+ const parentVersion = versionFromRow(parentRow);
1467
+ const thread = await get(this.db, "SELECT * FROM chat_threads WHERE id = ?", [parentVersion.threadId]);
1468
+ if (!thread)
1469
+ throw new ChatNotFoundError("thread", parentVersion.threadId);
1470
+ const currentHead = thread.default_head_id ? await get(this.db, "SELECT * FROM chat_thread_heads WHERE id = ?", [thread.default_head_id]) : null;
1471
+ if (input.expectedHeadPostVersionId !== undefined && (currentHead?.head_post_version_id ?? null) !== (input.expectedHeadPostVersionId ?? null)) {
1472
+ if (!currentHead) {
1473
+ throw new ChatConflictError("head_conflict", "expected head but thread is empty");
1474
+ }
1475
+ return {
1476
+ ok: false,
1477
+ reason: "head_conflict",
1478
+ currentHead: {
1479
+ id: currentHead.id,
1480
+ threadId: currentHead.thread_id,
1481
+ name: currentHead.name,
1482
+ headPostVersionId: currentHead.head_post_version_id,
1483
+ createdAtMs: currentHead.created_at_ms
1484
+ }
1485
+ };
1486
+ }
1487
+ const previousInChainRow = parentVersion.previousPostVersionId ? await get(this.db, "SELECT * FROM chat_post_versions WHERE id = ?", [parentVersion.previousPostVersionId]) : null;
1488
+ const previousInChain = previousInChainRow ? versionFromRow(previousInChainRow) : null;
1489
+ const prepared = prepareEditPost({
1490
+ ...input,
1491
+ threadId: parentVersion.threadId,
1492
+ previousPostVersionId: parentVersion.previousPostVersionId ?? null,
1493
+ previousLineageHash: previousInChain?.lineageHash ?? null
1494
+ });
1495
+ const postRow = await get(this.db, "SELECT post_index, deleted_at_ms FROM chat_posts WHERE id = ?", [input.postId]);
1496
+ if (!postRow)
1497
+ throw new ChatNotFoundError("post", input.postId);
1498
+ await run(this.db, `INSERT INTO chat_post_versions
1499
+ (id, post_id, thread_id, parent_version_id, previous_post_version_id,
1500
+ author_scope_type, author_scope_id, message, mentions, model, usage, content_hash, lineage_hash,
1501
+ signature, idempotency_key, created_at_ms)
1502
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1503
+ prepared.versionId,
1504
+ prepared.postId,
1505
+ prepared.threadId,
1506
+ prepared.parentVersionId,
1507
+ prepared.previousPostVersionId,
1508
+ prepared.author.type,
1509
+ prepared.author.id,
1510
+ JSON.stringify(prepared.message),
1511
+ prepared.mentions ? JSON.stringify(prepared.mentions) : null,
1512
+ prepared.model ? JSON.stringify(prepared.model) : null,
1513
+ prepared.usage ? JSON.stringify(prepared.usage) : null,
1514
+ prepared.contentHash,
1515
+ prepared.lineageHash,
1516
+ null,
1517
+ input.idempotencyKey ?? null,
1518
+ prepared.createdAtMs
1519
+ ]);
1520
+ let head;
1521
+ if (currentHead?.head_post_version_id === input.parentVersionId) {
1522
+ await run(this.db, "UPDATE chat_thread_heads SET head_post_version_id = ? WHERE id = ?", [
1523
+ prepared.versionId,
1524
+ currentHead.id
1525
+ ]);
1526
+ head = {
1527
+ id: currentHead.id,
1528
+ threadId: currentHead.thread_id,
1529
+ name: currentHead.name,
1530
+ headPostVersionId: prepared.versionId,
1531
+ createdAtMs: currentHead.created_at_ms
1532
+ };
1533
+ } else if (currentHead) {
1534
+ head = {
1535
+ id: currentHead.id,
1536
+ threadId: currentHead.thread_id,
1537
+ name: currentHead.name,
1538
+ headPostVersionId: currentHead.head_post_version_id,
1539
+ createdAtMs: currentHead.created_at_ms
1540
+ };
1541
+ } else {
1542
+ head = {
1543
+ id: createId(),
1544
+ threadId: parentVersion.threadId,
1545
+ name: DEFAULT_HEAD_NAME2,
1546
+ headPostVersionId: prepared.versionId,
1547
+ createdAtMs: prepared.createdAtMs
1548
+ };
1549
+ await run(this.db, `INSERT INTO chat_thread_heads
1550
+ (id, thread_id, name, head_post_version_id, created_at_ms)
1551
+ VALUES (?, ?, ?, ?, ?)`, [head.id, head.threadId, head.name, head.headPostVersionId, head.createdAtMs]);
1552
+ await run(this.db, "UPDATE chat_threads SET default_head_id = ? WHERE id = ?", [
1553
+ head.id,
1554
+ parentVersion.threadId
1555
+ ]);
1556
+ }
1557
+ const version = {
1558
+ ...prepared.message,
1559
+ id: prepared.versionId,
1560
+ postId: prepared.postId,
1561
+ threadId: prepared.threadId,
1562
+ parentVersionId: prepared.parentVersionId,
1563
+ previousPostVersionId: prepared.previousPostVersionId,
1564
+ author: prepared.author,
1565
+ contentHash: prepared.contentHash,
1566
+ lineageHash: prepared.lineageHash,
1567
+ mentions: prepared.mentions,
1568
+ createdAtMs: prepared.createdAtMs
1569
+ };
1570
+ return {
1571
+ ok: true,
1572
+ post: postFromVersion(version, postRow.post_index, postRow.deleted_at_ms),
1573
+ head
1574
+ };
1575
+ });
1576
+ }
1577
+ async deletePost(input) {
1578
+ const post = await this.requirePost(input.postId);
1579
+ const deletedAtMs = input.deletedAtMs ?? Date.now();
1580
+ await run(this.db, "UPDATE chat_posts SET deleted_at_ms = ? WHERE id = ?", [
1581
+ deletedAtMs,
1582
+ input.postId
1583
+ ]);
1584
+ return { ...post, deletedAtMs };
1585
+ }
1586
+ async addChannelMember(input) {
1587
+ await this.requireChannel(input.channelId);
1588
+ const createdAtMs = Date.now();
1589
+ await run(this.db, `INSERT OR REPLACE INTO chat_channel_members
1590
+ (channel_id, scope_type, scope_id, role, created_at_ms)
1591
+ VALUES (?, ?, ?, ?, ?)`, [input.channelId, input.scope.type, input.scope.id, input.role, createdAtMs]);
1592
+ const previousAclEventId = await this.latestAclEventId();
1593
+ const event = {
1594
+ id: createId(),
1595
+ type: "channel.member.added",
1596
+ channelId: input.channelId,
1597
+ scope: input.scope,
1598
+ role: input.role,
1599
+ actor: input.actor,
1600
+ previousAclEventId,
1601
+ contentHash: buildAclEventContentHash({
1602
+ type: "channel.member.added",
1603
+ targetType: "channel",
1604
+ targetId: input.channelId,
1605
+ scope: input.scope,
1606
+ role: input.role,
1607
+ actor: input.actor,
1608
+ previousAclEventId
1609
+ }),
1610
+ signature: input.signature,
1611
+ createdAtMs
1612
+ };
1613
+ await this.insertAclEvent(event, input.scope, input.role);
1614
+ return event;
1615
+ }
1616
+ async removeChannelMember(input) {
1617
+ await this.requireChannel(input.channelId);
1618
+ await run(this.db, "DELETE FROM chat_channel_members WHERE channel_id = ? AND scope_type = ? AND scope_id = ?", [input.channelId, input.scope.type, input.scope.id]);
1619
+ const previousAclEventId = await this.latestAclEventId();
1620
+ const createdAtMs = Date.now();
1621
+ const event = {
1622
+ id: createId(),
1623
+ type: "channel.member.removed",
1624
+ channelId: input.channelId,
1625
+ scope: input.scope,
1626
+ actor: input.actor,
1627
+ previousAclEventId,
1628
+ contentHash: buildAclEventContentHash({
1629
+ type: "channel.member.removed",
1630
+ targetType: "channel",
1631
+ targetId: input.channelId,
1632
+ scope: input.scope,
1633
+ actor: input.actor,
1634
+ previousAclEventId
1635
+ }),
1636
+ signature: input.signature,
1637
+ createdAtMs
1638
+ };
1639
+ await this.insertAclEvent(event, input.scope);
1640
+ return event;
1641
+ }
1642
+ async addThreadParticipant(input) {
1643
+ await this.requireThread(input.threadId);
1644
+ const createdAtMs = Date.now();
1645
+ await run(this.db, `INSERT OR REPLACE INTO chat_thread_participants
1646
+ (thread_id, scope_type, scope_id, role, created_at_ms)
1647
+ VALUES (?, ?, ?, ?, ?)`, [input.threadId, input.scope.type, input.scope.id, input.role, createdAtMs]);
1648
+ const previousAclEventId = await this.latestAclEventId();
1649
+ const event = {
1650
+ id: createId(),
1651
+ type: "thread.participant.added",
1652
+ threadId: input.threadId,
1653
+ scope: input.scope,
1654
+ role: input.role,
1655
+ actor: input.actor,
1656
+ previousAclEventId,
1657
+ contentHash: buildAclEventContentHash({
1658
+ type: "thread.participant.added",
1659
+ targetType: "thread",
1660
+ targetId: input.threadId,
1661
+ scope: input.scope,
1662
+ role: input.role,
1663
+ actor: input.actor,
1664
+ previousAclEventId
1665
+ }),
1666
+ signature: input.signature,
1667
+ createdAtMs
1668
+ };
1669
+ await this.insertAclEvent(event, input.scope, input.role, input.threadId);
1670
+ return event;
1671
+ }
1672
+ async removeThreadParticipant(input) {
1673
+ await this.requireThread(input.threadId);
1674
+ await run(this.db, "DELETE FROM chat_thread_participants WHERE thread_id = ? AND scope_type = ? AND scope_id = ?", [input.threadId, input.scope.type, input.scope.id]);
1675
+ const previousAclEventId = await this.latestAclEventId();
1676
+ const createdAtMs = Date.now();
1677
+ const event = {
1678
+ id: createId(),
1679
+ type: "thread.participant.removed",
1680
+ threadId: input.threadId,
1681
+ scope: input.scope,
1682
+ actor: input.actor,
1683
+ previousAclEventId,
1684
+ contentHash: buildAclEventContentHash({
1685
+ type: "thread.participant.removed",
1686
+ targetType: "thread",
1687
+ targetId: input.threadId,
1688
+ scope: input.scope,
1689
+ actor: input.actor,
1690
+ previousAclEventId
1691
+ }),
1692
+ signature: input.signature,
1693
+ createdAtMs
1694
+ };
1695
+ await this.insertAclEvent(event, input.scope, undefined, input.threadId);
1696
+ return event;
1697
+ }
1698
+ async createThreadHead(input) {
1699
+ await this.requireThread(input.threadId);
1700
+ const version = await this.getPostVersion(input.headPostVersionId);
1701
+ if (!version) {
1702
+ throw new ChatNotFoundError("post_version", input.headPostVersionId);
1703
+ }
1704
+ const head = {
1705
+ id: createId(),
1706
+ threadId: input.threadId,
1707
+ name: input.name,
1708
+ headPostVersionId: input.headPostVersionId,
1709
+ createdAtMs: Date.now()
1710
+ };
1711
+ await run(this.db, `INSERT INTO chat_thread_heads
1712
+ (id, thread_id, name, head_post_version_id, created_at_ms)
1713
+ VALUES (?, ?, ?, ?, ?)`, [head.id, head.threadId, head.name, head.headPostVersionId, head.createdAtMs]);
1714
+ return head;
1715
+ }
1716
+ async listChannelMembers(channelId) {
1717
+ const rows = await all(this.db, "SELECT scope_type, scope_id FROM chat_channel_members WHERE channel_id = ?", [channelId]);
1718
+ return rows.map(scopeFromRow2);
1719
+ }
1720
+ async listThreadParticipants(threadId) {
1721
+ const rows = await all(this.db, "SELECT scope_type, scope_id FROM chat_thread_participants WHERE thread_id = ?", [threadId]);
1722
+ return rows.map(scopeFromRow2);
1723
+ }
1724
+ async listAclEvents(input) {
1725
+ let query = "SELECT * FROM chat_acl_events";
1726
+ const params = [];
1727
+ if (input.channelId) {
1728
+ query += " WHERE target_type = 'channel' AND target_id = ?";
1729
+ params.push(input.channelId);
1730
+ } else if (input.threadId) {
1731
+ query += " WHERE target_type = 'thread' AND target_id = ?";
1732
+ params.push(input.threadId);
1733
+ }
1734
+ query += " ORDER BY created_at_ms ASC";
1735
+ const rows = await all(this.db, query, params);
1736
+ const events2 = rows.map(aclEventFromRow);
1737
+ return events2.slice(0, input.limit ?? events2.length);
1738
+ }
1739
+ async latestAclEventId() {
1740
+ const row = await get(this.db, "SELECT id FROM chat_acl_events ORDER BY created_at_ms DESC LIMIT 1");
1741
+ return row?.id ?? null;
1742
+ }
1743
+ async insertAclEvent(event, subject, role, threadTargetId) {
1744
+ const targetType = event.type.startsWith("channel.") ? "channel" : "thread";
1745
+ const targetId = targetType === "channel" ? event.channelId : threadTargetId ?? event.threadId;
1746
+ await run(this.db, `INSERT INTO chat_acl_events
1747
+ (id, target_type, target_id, event_type, actor_scope_type, actor_scope_id,
1748
+ subject_scope_type, subject_scope_id, role, previous_acl_event_id, content_hash, signature, created_at_ms)
1749
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1750
+ event.id,
1751
+ targetType,
1752
+ targetId,
1753
+ event.type,
1754
+ event.actor.type,
1755
+ event.actor.id,
1756
+ subject?.type ?? null,
1757
+ subject?.id ?? null,
1758
+ role ?? ("role" in event ? event.role : null),
1759
+ event.previousAclEventId ?? null,
1760
+ event.contentHash,
1761
+ event.signature ? JSON.stringify(event.signature) : null,
1762
+ event.createdAtMs
1763
+ ]);
1764
+ }
1765
+ async listPostStreamEvents(postId) {
1766
+ return listPostStreamEvents(this.db, postId);
1767
+ }
1768
+ async startStreamedPost(input) {
1769
+ return startStreamedPost(this.db, input, () => null);
1770
+ }
1771
+ async applyPostDelta(input) {
1772
+ return applyPostDelta(this.db, input, () => null);
1773
+ }
1774
+ async completeStreamedPost(input) {
1775
+ return completeStreamedPost(this.db, input);
1776
+ }
1777
+ async abortStreamedPost(input) {
1778
+ return abortStreamedPost(this.db, input, () => null);
1779
+ }
1780
+ async rebuildStreamedPostCache(postId) {
1781
+ return rebuildStreamedPostCache(this.db, postId, () => null);
1782
+ }
1783
+ async setPostVersionSignature(versionId, signature) {
1784
+ const existing = await get(this.db, "SELECT id FROM chat_post_versions WHERE id = ?", [versionId]);
1785
+ if (!existing) {
1786
+ throw new ChatNotFoundError("post_version", versionId);
1787
+ }
1788
+ await run(this.db, "UPDATE chat_post_versions SET signature = ? WHERE id = ?", [
1789
+ JSON.stringify(signature),
1790
+ versionId
1791
+ ]);
1792
+ }
1793
+ }
1794
+ function createTursoChatPersistence(db) {
1795
+ return new TursoChatPersistence(db);
1796
+ }
1797
+ // src/persistence/turso-serverless/index.ts
1798
+ async function createTestChatDatabase() {
1799
+ const db = createMemorySqliteDatabase();
1800
+ await ensureChatSchema(db);
1801
+ return { db, persistence: createTursoChatPersistence(db) };
1802
+ }
1803
+ export {
1804
+ ensureChatSchema,
1805
+ createTursoDatabase,
1806
+ createTursoChatPersistence,
1807
+ createTestChatDatabase,
1808
+ createMemorySqliteDatabase,
1809
+ createLocalSqliteDatabase,
1810
+ closeLocalSqliteDatabase,
1811
+ TursoChatPersistence
1812
+ };