@lazyingart/agent-web 0.1.40
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/LICENSE +22 -0
- package/README.md +438 -0
- package/docs/architecture.md +503 -0
- package/package.json +43 -0
- package/src/aginti-adapter.js +602 -0
- package/src/chat-context.js +1020 -0
- package/src/chat-migrations.js +947 -0
- package/src/chat-store.js +3308 -0
- package/src/cli.js +134 -0
- package/src/cloud-server.js +2043 -0
- package/src/contracts.js +103 -0
- package/src/deterministic-context-summarizer.js +254 -0
- package/src/direct-chat-capability-limits.js +66 -0
- package/src/direct-chat-contract.js +3 -0
- package/src/errors.js +50 -0
- package/src/http-contract.js +592 -0
- package/src/index.js +88 -0
- package/src/localllm-connector.js +667 -0
- package/src/migrations.js +231 -0
- package/src/operator-health.js +184 -0
- package/src/password-verifier.js +131 -0
- package/src/service-config.js +547 -0
- package/src/service.js +408 -0
- package/src/sqlite-health.js +83 -0
- package/src/storage-path.js +130 -0
- package/src/store.js +914 -0
- package/src/validation.js +181 -0
- package/src/vision-attachment.js +404 -0
- package/src/web/aginti-client.js +552 -0
- package/src/web/aginti-protocol.js +1146 -0
- package/src/web/asset-map.js +462 -0
- package/src/web/browser-app.js +6491 -0
- package/src/web/cloud-session-client.js +427 -0
- package/src/web/direct-chat-client.js +1482 -0
- package/src/web/index.js +10 -0
- package/src/web/presentation-state.js +107 -0
- package/src/web/pwa-assets.js +854 -0
- package/src/web/pwa-update-handoff-store.js +179 -0
- package/src/web/safe-rendering.js +836 -0
- package/src/web/vision-image-client.js +546 -0
- package/src/web/vision-image-sanitizer.js +168 -0
- package/src/web/web-release.js +28 -0
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { StorageCorruptionError, UnsupportedSchemaError } from './errors.js';
|
|
4
|
+
|
|
5
|
+
// "LADC" -- deliberately different from the Agent presentation-index database.
|
|
6
|
+
export const CHAT_SQLITE_APPLICATION_ID = 0x4c414443;
|
|
7
|
+
|
|
8
|
+
const INITIAL_CHAT_SCHEMA = `
|
|
9
|
+
CREATE TABLE chat_schema_migrations (
|
|
10
|
+
version INTEGER PRIMARY KEY,
|
|
11
|
+
name TEXT NOT NULL UNIQUE,
|
|
12
|
+
checksum TEXT NOT NULL CHECK (
|
|
13
|
+
length(checksum) = 64 AND checksum NOT GLOB '*[^0-9a-f]*'
|
|
14
|
+
),
|
|
15
|
+
applied_at TEXT NOT NULL
|
|
16
|
+
) STRICT;
|
|
17
|
+
|
|
18
|
+
CREATE TABLE direct_chat_threads (
|
|
19
|
+
account_id TEXT NOT NULL CHECK (length(account_id) BETWEEN 1 AND 128),
|
|
20
|
+
thread_id TEXT NOT NULL CHECK (length(thread_id) BETWEEN 1 AND 128),
|
|
21
|
+
title TEXT NOT NULL DEFAULT '',
|
|
22
|
+
title_bytes INTEGER NOT NULL DEFAULT 0 CHECK (
|
|
23
|
+
title_bytes BETWEEN 0 AND 512
|
|
24
|
+
AND length(CAST(title AS BLOB)) = title_bytes
|
|
25
|
+
),
|
|
26
|
+
model_alias TEXT NOT NULL CHECK (length(model_alias) BETWEEN 1 AND 64),
|
|
27
|
+
ledger_revision INTEGER NOT NULL DEFAULT 0 CHECK (ledger_revision >= 0),
|
|
28
|
+
ledger_hash TEXT,
|
|
29
|
+
message_count INTEGER NOT NULL DEFAULT 0 CHECK (message_count >= 0),
|
|
30
|
+
ledger_bytes INTEGER NOT NULL DEFAULT 0 CHECK (ledger_bytes >= 0),
|
|
31
|
+
generation_count INTEGER NOT NULL DEFAULT 0 CHECK (generation_count BETWEEN 0 AND 1024),
|
|
32
|
+
journal_delta_count INTEGER NOT NULL DEFAULT 0 CHECK (journal_delta_count BETWEEN 0 AND 32768),
|
|
33
|
+
journal_bytes INTEGER NOT NULL DEFAULT 0 CHECK (journal_bytes BETWEEN 0 AND 16777216),
|
|
34
|
+
current_generation_id TEXT CHECK (
|
|
35
|
+
current_generation_id IS NULL OR length(current_generation_id) BETWEEN 1 AND 128
|
|
36
|
+
),
|
|
37
|
+
created_at TEXT NOT NULL,
|
|
38
|
+
updated_at TEXT NOT NULL,
|
|
39
|
+
PRIMARY KEY (account_id, thread_id),
|
|
40
|
+
CHECK (message_count = ledger_revision),
|
|
41
|
+
CHECK (
|
|
42
|
+
(ledger_revision = 0 AND ledger_hash IS NULL)
|
|
43
|
+
OR (
|
|
44
|
+
ledger_revision > 0
|
|
45
|
+
AND length(ledger_hash) = 64
|
|
46
|
+
AND ledger_hash NOT GLOB '*[^0-9a-f]*'
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
) STRICT;
|
|
50
|
+
|
|
51
|
+
CREATE INDEX direct_chat_threads_owner_updated
|
|
52
|
+
ON direct_chat_threads(account_id, updated_at DESC, thread_id DESC);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE direct_chat_messages (
|
|
55
|
+
account_id TEXT NOT NULL,
|
|
56
|
+
thread_id TEXT NOT NULL,
|
|
57
|
+
message_id TEXT NOT NULL CHECK (length(message_id) BETWEEN 1 AND 128),
|
|
58
|
+
revision INTEGER NOT NULL CHECK (revision >= 1),
|
|
59
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
60
|
+
content TEXT NOT NULL,
|
|
61
|
+
content_bytes INTEGER NOT NULL CHECK (
|
|
62
|
+
content_bytes BETWEEN 1 AND 65536
|
|
63
|
+
AND length(CAST(content AS BLOB)) = content_bytes
|
|
64
|
+
),
|
|
65
|
+
previous_hash TEXT,
|
|
66
|
+
message_hash TEXT NOT NULL CHECK (
|
|
67
|
+
length(message_hash) = 64 AND message_hash NOT GLOB '*[^0-9a-f]*'
|
|
68
|
+
),
|
|
69
|
+
generation_id TEXT CHECK (
|
|
70
|
+
generation_id IS NULL OR length(generation_id) BETWEEN 1 AND 128
|
|
71
|
+
),
|
|
72
|
+
created_at TEXT NOT NULL,
|
|
73
|
+
PRIMARY KEY (account_id, thread_id, message_id),
|
|
74
|
+
UNIQUE (account_id, thread_id, revision),
|
|
75
|
+
FOREIGN KEY (account_id, thread_id)
|
|
76
|
+
REFERENCES direct_chat_threads(account_id, thread_id) ON DELETE RESTRICT,
|
|
77
|
+
CHECK (
|
|
78
|
+
(revision = 1 AND previous_hash IS NULL)
|
|
79
|
+
OR (
|
|
80
|
+
revision > 1
|
|
81
|
+
AND length(previous_hash) = 64
|
|
82
|
+
AND previous_hash NOT GLOB '*[^0-9a-f]*'
|
|
83
|
+
)
|
|
84
|
+
),
|
|
85
|
+
CHECK (
|
|
86
|
+
(role = 'user' AND generation_id IS NULL)
|
|
87
|
+
OR (role = 'assistant' AND generation_id IS NOT NULL)
|
|
88
|
+
)
|
|
89
|
+
) STRICT;
|
|
90
|
+
|
|
91
|
+
CREATE INDEX direct_chat_messages_owner_revision
|
|
92
|
+
ON direct_chat_messages(account_id, thread_id, revision);
|
|
93
|
+
|
|
94
|
+
CREATE TRIGGER direct_chat_messages_no_update
|
|
95
|
+
BEFORE UPDATE ON direct_chat_messages
|
|
96
|
+
BEGIN
|
|
97
|
+
SELECT RAISE(ABORT, 'direct chat messages are append-only');
|
|
98
|
+
END;
|
|
99
|
+
|
|
100
|
+
CREATE TRIGGER direct_chat_messages_no_delete
|
|
101
|
+
BEFORE DELETE ON direct_chat_messages
|
|
102
|
+
BEGIN
|
|
103
|
+
SELECT RAISE(ABORT, 'direct chat messages are append-only');
|
|
104
|
+
END;
|
|
105
|
+
|
|
106
|
+
CREATE TABLE direct_chat_generations (
|
|
107
|
+
account_id TEXT NOT NULL,
|
|
108
|
+
thread_id TEXT NOT NULL,
|
|
109
|
+
generation_id TEXT NOT NULL CHECK (length(generation_id) BETWEEN 1 AND 128),
|
|
110
|
+
assistant_message_id TEXT NOT NULL CHECK (length(assistant_message_id) BETWEEN 1 AND 128),
|
|
111
|
+
status TEXT NOT NULL CHECK (status IN ('in_progress', 'completed', 'cancelled', 'failed')),
|
|
112
|
+
model_alias TEXT NOT NULL CHECK (length(model_alias) BETWEEN 1 AND 64),
|
|
113
|
+
source_revision INTEGER NOT NULL CHECK (source_revision >= 1),
|
|
114
|
+
source_hash TEXT NOT NULL CHECK (
|
|
115
|
+
length(source_hash) = 64 AND source_hash NOT GLOB '*[^0-9a-f]*'
|
|
116
|
+
),
|
|
117
|
+
delta_count INTEGER NOT NULL DEFAULT 0 CHECK (delta_count BETWEEN 0 AND 8192),
|
|
118
|
+
delta_bytes INTEGER NOT NULL DEFAULT 0 CHECK (delta_bytes BETWEEN 0 AND 65536),
|
|
119
|
+
last_delta_hash TEXT,
|
|
120
|
+
final_revision INTEGER CHECK (final_revision IS NULL OR final_revision >= 2),
|
|
121
|
+
final_hash TEXT CHECK (
|
|
122
|
+
final_hash IS NULL OR (
|
|
123
|
+
length(final_hash) = 64 AND final_hash NOT GLOB '*[^0-9a-f]*'
|
|
124
|
+
)
|
|
125
|
+
),
|
|
126
|
+
failure_code TEXT CHECK (
|
|
127
|
+
failure_code IS NULL OR failure_code IN (
|
|
128
|
+
'provider_unavailable', 'timeout', 'internal_error',
|
|
129
|
+
'response_limit', 'content_rejected'
|
|
130
|
+
)
|
|
131
|
+
),
|
|
132
|
+
started_at TEXT NOT NULL,
|
|
133
|
+
updated_at TEXT NOT NULL,
|
|
134
|
+
terminal_at TEXT,
|
|
135
|
+
deltas_pruned INTEGER NOT NULL DEFAULT 0 CHECK (deltas_pruned IN (0, 1)),
|
|
136
|
+
pruned_at TEXT,
|
|
137
|
+
PRIMARY KEY (account_id, thread_id, generation_id),
|
|
138
|
+
UNIQUE (account_id, thread_id, assistant_message_id),
|
|
139
|
+
FOREIGN KEY (account_id, thread_id)
|
|
140
|
+
REFERENCES direct_chat_threads(account_id, thread_id) ON DELETE RESTRICT,
|
|
141
|
+
CHECK (
|
|
142
|
+
(delta_count = 0 AND last_delta_hash IS NULL)
|
|
143
|
+
OR (
|
|
144
|
+
delta_count > 0
|
|
145
|
+
AND length(last_delta_hash) = 64
|
|
146
|
+
AND last_delta_hash NOT GLOB '*[^0-9a-f]*'
|
|
147
|
+
)
|
|
148
|
+
),
|
|
149
|
+
CHECK (
|
|
150
|
+
(status = 'in_progress' AND terminal_at IS NULL AND final_revision IS NULL
|
|
151
|
+
AND final_hash IS NULL AND failure_code IS NULL
|
|
152
|
+
AND deltas_pruned = 0 AND pruned_at IS NULL)
|
|
153
|
+
OR (status = 'completed' AND terminal_at IS NOT NULL AND final_revision IS NOT NULL
|
|
154
|
+
AND final_hash IS NOT NULL AND failure_code IS NULL
|
|
155
|
+
AND ((deltas_pruned = 0 AND pruned_at IS NULL) OR (deltas_pruned = 1 AND pruned_at IS NOT NULL)))
|
|
156
|
+
OR (status = 'cancelled' AND terminal_at IS NOT NULL AND final_revision IS NULL
|
|
157
|
+
AND final_hash IS NULL AND failure_code IS NULL
|
|
158
|
+
AND deltas_pruned = 0 AND pruned_at IS NULL)
|
|
159
|
+
OR (status = 'failed' AND terminal_at IS NOT NULL AND final_revision IS NULL
|
|
160
|
+
AND final_hash IS NULL AND failure_code IS NOT NULL
|
|
161
|
+
AND deltas_pruned = 0 AND pruned_at IS NULL)
|
|
162
|
+
)
|
|
163
|
+
) STRICT;
|
|
164
|
+
|
|
165
|
+
CREATE UNIQUE INDEX direct_chat_one_active_generation
|
|
166
|
+
ON direct_chat_generations(account_id, thread_id)
|
|
167
|
+
WHERE status = 'in_progress';
|
|
168
|
+
|
|
169
|
+
CREATE INDEX direct_chat_generations_owner_started
|
|
170
|
+
ON direct_chat_generations(account_id, thread_id, started_at DESC, generation_id DESC);
|
|
171
|
+
|
|
172
|
+
CREATE TRIGGER direct_chat_generations_no_delete
|
|
173
|
+
BEFORE DELETE ON direct_chat_generations
|
|
174
|
+
BEGIN
|
|
175
|
+
SELECT RAISE(ABORT, 'direct chat generation records are durable');
|
|
176
|
+
END;
|
|
177
|
+
|
|
178
|
+
CREATE TRIGGER direct_chat_generations_prune_only_superseded
|
|
179
|
+
BEFORE UPDATE OF deltas_pruned, pruned_at ON direct_chat_generations
|
|
180
|
+
WHEN OLD.deltas_pruned = 0 AND NEW.deltas_pruned = 1 AND NOT EXISTS (
|
|
181
|
+
SELECT 1 FROM direct_chat_generations AS newer
|
|
182
|
+
WHERE newer.account_id = OLD.account_id
|
|
183
|
+
AND newer.thread_id = OLD.thread_id
|
|
184
|
+
AND newer.status = 'completed'
|
|
185
|
+
AND (
|
|
186
|
+
newer.terminal_at > OLD.terminal_at
|
|
187
|
+
OR (newer.terminal_at = OLD.terminal_at AND newer.generation_id > OLD.generation_id)
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
BEGIN
|
|
191
|
+
SELECT RAISE(ABORT, 'only superseded completed generation deltas may be pruned');
|
|
192
|
+
END;
|
|
193
|
+
|
|
194
|
+
CREATE TRIGGER direct_chat_generations_prune_irreversible
|
|
195
|
+
BEFORE UPDATE OF deltas_pruned, pruned_at ON direct_chat_generations
|
|
196
|
+
WHEN OLD.deltas_pruned = 1 AND (
|
|
197
|
+
NEW.deltas_pruned != OLD.deltas_pruned OR NEW.pruned_at IS NOT OLD.pruned_at
|
|
198
|
+
)
|
|
199
|
+
BEGIN
|
|
200
|
+
SELECT RAISE(ABORT, 'direct chat generation pruning is irreversible');
|
|
201
|
+
END;
|
|
202
|
+
|
|
203
|
+
CREATE TABLE direct_chat_deltas (
|
|
204
|
+
account_id TEXT NOT NULL,
|
|
205
|
+
thread_id TEXT NOT NULL,
|
|
206
|
+
generation_id TEXT NOT NULL,
|
|
207
|
+
sequence INTEGER NOT NULL CHECK (sequence >= 1),
|
|
208
|
+
content TEXT NOT NULL,
|
|
209
|
+
content_bytes INTEGER NOT NULL CHECK (
|
|
210
|
+
content_bytes BETWEEN 1 AND 16384
|
|
211
|
+
AND length(CAST(content AS BLOB)) = content_bytes
|
|
212
|
+
),
|
|
213
|
+
previous_hash TEXT,
|
|
214
|
+
delta_hash TEXT NOT NULL CHECK (
|
|
215
|
+
length(delta_hash) = 64 AND delta_hash NOT GLOB '*[^0-9a-f]*'
|
|
216
|
+
),
|
|
217
|
+
created_at TEXT NOT NULL,
|
|
218
|
+
PRIMARY KEY (account_id, thread_id, generation_id, sequence),
|
|
219
|
+
FOREIGN KEY (account_id, thread_id, generation_id)
|
|
220
|
+
REFERENCES direct_chat_generations(account_id, thread_id, generation_id) ON DELETE RESTRICT,
|
|
221
|
+
CHECK (
|
|
222
|
+
(sequence = 1 AND previous_hash IS NULL)
|
|
223
|
+
OR (
|
|
224
|
+
sequence > 1
|
|
225
|
+
AND length(previous_hash) = 64
|
|
226
|
+
AND previous_hash NOT GLOB '*[^0-9a-f]*'
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
) STRICT;
|
|
230
|
+
|
|
231
|
+
CREATE INDEX direct_chat_deltas_replay
|
|
232
|
+
ON direct_chat_deltas(account_id, thread_id, generation_id, sequence);
|
|
233
|
+
|
|
234
|
+
CREATE TRIGGER direct_chat_deltas_no_update
|
|
235
|
+
BEFORE UPDATE ON direct_chat_deltas
|
|
236
|
+
BEGIN
|
|
237
|
+
SELECT RAISE(ABORT, 'direct chat deltas are append-only');
|
|
238
|
+
END;
|
|
239
|
+
|
|
240
|
+
CREATE TRIGGER direct_chat_deltas_no_delete
|
|
241
|
+
BEFORE DELETE ON direct_chat_deltas
|
|
242
|
+
WHEN NOT EXISTS (
|
|
243
|
+
SELECT 1 FROM direct_chat_generations
|
|
244
|
+
WHERE account_id = OLD.account_id
|
|
245
|
+
AND thread_id = OLD.thread_id
|
|
246
|
+
AND generation_id = OLD.generation_id
|
|
247
|
+
AND status = 'completed'
|
|
248
|
+
AND deltas_pruned = 1
|
|
249
|
+
AND EXISTS (
|
|
250
|
+
SELECT 1 FROM direct_chat_generations AS newer
|
|
251
|
+
WHERE newer.account_id = OLD.account_id
|
|
252
|
+
AND newer.thread_id = OLD.thread_id
|
|
253
|
+
AND newer.status = 'completed'
|
|
254
|
+
AND (
|
|
255
|
+
newer.terminal_at > direct_chat_generations.terminal_at
|
|
256
|
+
OR (
|
|
257
|
+
newer.terminal_at = direct_chat_generations.terminal_at
|
|
258
|
+
AND newer.generation_id > direct_chat_generations.generation_id
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
BEGIN
|
|
264
|
+
SELECT RAISE(ABORT, 'only safely retained terminal direct chat deltas may be pruned');
|
|
265
|
+
END;
|
|
266
|
+
|
|
267
|
+
CREATE TABLE direct_chat_compactions (
|
|
268
|
+
account_id TEXT NOT NULL,
|
|
269
|
+
thread_id TEXT NOT NULL,
|
|
270
|
+
snapshot_id TEXT NOT NULL CHECK (length(snapshot_id) BETWEEN 1 AND 128),
|
|
271
|
+
source_start_revision INTEGER NOT NULL CHECK (source_start_revision >= 1),
|
|
272
|
+
source_start_hash TEXT NOT NULL CHECK (
|
|
273
|
+
length(source_start_hash) = 64 AND source_start_hash NOT GLOB '*[^0-9a-f]*'
|
|
274
|
+
),
|
|
275
|
+
source_end_revision INTEGER NOT NULL CHECK (source_end_revision >= source_start_revision),
|
|
276
|
+
source_end_hash TEXT NOT NULL CHECK (
|
|
277
|
+
length(source_end_hash) = 64 AND source_end_hash NOT GLOB '*[^0-9a-f]*'
|
|
278
|
+
),
|
|
279
|
+
summary_text TEXT NOT NULL,
|
|
280
|
+
summary_bytes INTEGER NOT NULL CHECK (
|
|
281
|
+
summary_bytes BETWEEN 1 AND 262144
|
|
282
|
+
AND length(CAST(summary_text AS BLOB)) = summary_bytes
|
|
283
|
+
),
|
|
284
|
+
summary_hash TEXT NOT NULL CHECK (
|
|
285
|
+
length(summary_hash) = 64 AND summary_hash NOT GLOB '*[^0-9a-f]*'
|
|
286
|
+
),
|
|
287
|
+
created_at TEXT NOT NULL,
|
|
288
|
+
PRIMARY KEY (account_id, thread_id, snapshot_id),
|
|
289
|
+
FOREIGN KEY (account_id, thread_id)
|
|
290
|
+
REFERENCES direct_chat_threads(account_id, thread_id) ON DELETE RESTRICT
|
|
291
|
+
) STRICT;
|
|
292
|
+
|
|
293
|
+
CREATE INDEX direct_chat_compactions_owner_range
|
|
294
|
+
ON direct_chat_compactions(
|
|
295
|
+
account_id, thread_id, source_end_revision DESC, created_at DESC, snapshot_id DESC
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
CREATE TRIGGER direct_chat_compactions_no_update
|
|
299
|
+
BEFORE UPDATE ON direct_chat_compactions
|
|
300
|
+
BEGIN
|
|
301
|
+
SELECT RAISE(ABORT, 'direct chat compaction snapshots are immutable');
|
|
302
|
+
END;
|
|
303
|
+
|
|
304
|
+
CREATE TRIGGER direct_chat_compactions_no_delete
|
|
305
|
+
BEFORE DELETE ON direct_chat_compactions
|
|
306
|
+
WHEN NOT EXISTS (
|
|
307
|
+
SELECT 1 FROM direct_chat_compactions AS newer
|
|
308
|
+
WHERE newer.account_id = OLD.account_id
|
|
309
|
+
AND newer.thread_id = OLD.thread_id
|
|
310
|
+
AND (
|
|
311
|
+
newer.source_end_revision > OLD.source_end_revision
|
|
312
|
+
OR (
|
|
313
|
+
newer.source_end_revision = OLD.source_end_revision
|
|
314
|
+
AND newer.created_at > OLD.created_at
|
|
315
|
+
)
|
|
316
|
+
OR (
|
|
317
|
+
newer.source_end_revision = OLD.source_end_revision
|
|
318
|
+
AND newer.created_at = OLD.created_at
|
|
319
|
+
AND newer.snapshot_id > OLD.snapshot_id
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
BEGIN
|
|
324
|
+
SELECT RAISE(ABORT, 'the current direct chat compaction snapshot cannot be pruned');
|
|
325
|
+
END;
|
|
326
|
+
|
|
327
|
+
CREATE TABLE direct_chat_idempotency (
|
|
328
|
+
account_id TEXT NOT NULL,
|
|
329
|
+
operation TEXT NOT NULL CHECK (operation IN (
|
|
330
|
+
'thread.create', 'message.user.append', 'generation.start',
|
|
331
|
+
'generation.finalize', 'generation.cancel',
|
|
332
|
+
'generation.fail', 'compaction.create'
|
|
333
|
+
)),
|
|
334
|
+
key_hash TEXT NOT NULL CHECK (
|
|
335
|
+
length(key_hash) = 64 AND key_hash NOT GLOB '*[^0-9a-f]*'
|
|
336
|
+
),
|
|
337
|
+
request_hash TEXT NOT NULL CHECK (
|
|
338
|
+
length(request_hash) = 64 AND request_hash NOT GLOB '*[^0-9a-f]*'
|
|
339
|
+
),
|
|
340
|
+
resource_kind TEXT NOT NULL CHECK (
|
|
341
|
+
resource_kind IN ('thread', 'message', 'generation', 'compaction')
|
|
342
|
+
),
|
|
343
|
+
thread_id TEXT NOT NULL CHECK (length(thread_id) BETWEEN 1 AND 128),
|
|
344
|
+
resource_id TEXT NOT NULL CHECK (length(resource_id) BETWEEN 1 AND 128),
|
|
345
|
+
resource_version INTEGER NOT NULL CHECK (resource_version >= 0),
|
|
346
|
+
result_digest TEXT NOT NULL CHECK (
|
|
347
|
+
length(result_digest) = 64 AND result_digest NOT GLOB '*[^0-9a-f]*'
|
|
348
|
+
),
|
|
349
|
+
created_at TEXT NOT NULL,
|
|
350
|
+
expires_at TEXT NOT NULL,
|
|
351
|
+
PRIMARY KEY (account_id, operation, key_hash),
|
|
352
|
+
FOREIGN KEY (account_id, thread_id)
|
|
353
|
+
REFERENCES direct_chat_threads(account_id, thread_id) ON DELETE RESTRICT,
|
|
354
|
+
CHECK (
|
|
355
|
+
(operation = 'thread.create' AND resource_kind = 'thread')
|
|
356
|
+
OR (operation = 'message.user.append' AND resource_kind = 'message')
|
|
357
|
+
OR (operation IN (
|
|
358
|
+
'generation.start', 'generation.finalize', 'generation.cancel', 'generation.fail'
|
|
359
|
+
) AND resource_kind = 'generation')
|
|
360
|
+
OR (operation = 'compaction.create' AND resource_kind = 'compaction')
|
|
361
|
+
)
|
|
362
|
+
) STRICT;
|
|
363
|
+
|
|
364
|
+
CREATE INDEX direct_chat_idempotency_expiry
|
|
365
|
+
ON direct_chat_idempotency(expires_at);
|
|
366
|
+
|
|
367
|
+
CREATE INDEX direct_chat_idempotency_owner_created
|
|
368
|
+
ON direct_chat_idempotency(account_id, created_at DESC);
|
|
369
|
+
`;
|
|
370
|
+
|
|
371
|
+
const GENERATION_DISPATCH_LEASE_SCHEMA = `
|
|
372
|
+
CREATE TABLE direct_chat_generation_leases (
|
|
373
|
+
account_id TEXT NOT NULL,
|
|
374
|
+
thread_id TEXT NOT NULL,
|
|
375
|
+
generation_id TEXT NOT NULL,
|
|
376
|
+
owner_hash TEXT NOT NULL CHECK (
|
|
377
|
+
length(owner_hash) = 64 AND owner_hash NOT GLOB '*[^0-9a-f]*'
|
|
378
|
+
),
|
|
379
|
+
fence INTEGER NOT NULL CHECK (fence BETWEEN 1 AND 9007199254740991),
|
|
380
|
+
phase TEXT NOT NULL CHECK (
|
|
381
|
+
phase IN ('claimed', 'dispatch_started', 'released', 'interrupted')
|
|
382
|
+
),
|
|
383
|
+
expires_at TEXT NOT NULL,
|
|
384
|
+
claimed_at TEXT NOT NULL,
|
|
385
|
+
dispatch_started_at TEXT,
|
|
386
|
+
updated_at TEXT NOT NULL,
|
|
387
|
+
released_at TEXT,
|
|
388
|
+
PRIMARY KEY (account_id, thread_id, generation_id),
|
|
389
|
+
FOREIGN KEY (account_id, thread_id, generation_id)
|
|
390
|
+
REFERENCES direct_chat_generations(account_id, thread_id, generation_id) ON DELETE RESTRICT,
|
|
391
|
+
CHECK (
|
|
392
|
+
(phase IN ('claimed', 'dispatch_started') AND released_at IS NULL)
|
|
393
|
+
OR (phase IN ('released', 'interrupted') AND released_at = expires_at)
|
|
394
|
+
),
|
|
395
|
+
CHECK (
|
|
396
|
+
(phase = 'claimed' AND dispatch_started_at IS NULL)
|
|
397
|
+
OR (phase = 'released')
|
|
398
|
+
OR (phase IN ('dispatch_started', 'interrupted') AND dispatch_started_at IS NOT NULL)
|
|
399
|
+
)
|
|
400
|
+
) STRICT;
|
|
401
|
+
|
|
402
|
+
-- LocalLLM inference is a workstation-wide scarce resource for this service.
|
|
403
|
+
-- A partial unique index on the fixed dispatch phase makes the one-active-
|
|
404
|
+
-- inference policy durable across overlapping BFF processes, not merely an
|
|
405
|
+
-- in-memory per-process convention.
|
|
406
|
+
CREATE UNIQUE INDEX direct_chat_one_active_dispatch
|
|
407
|
+
ON direct_chat_generation_leases(phase)
|
|
408
|
+
WHERE phase = 'dispatch_started' AND released_at IS NULL;
|
|
409
|
+
|
|
410
|
+
CREATE TRIGGER direct_chat_generation_leases_fence_monotonic
|
|
411
|
+
BEFORE UPDATE ON direct_chat_generation_leases
|
|
412
|
+
WHEN NEW.fence < OLD.fence
|
|
413
|
+
OR NEW.fence > OLD.fence + 1
|
|
414
|
+
OR (NEW.owner_hash != OLD.owner_hash AND NEW.fence != OLD.fence + 1)
|
|
415
|
+
OR (OLD.released_at IS NOT NULL AND NEW.released_at IS NULL AND NEW.fence != OLD.fence + 1)
|
|
416
|
+
OR (OLD.dispatch_started_at IS NOT NULL AND NEW.dispatch_started_at IS NULL)
|
|
417
|
+
BEGIN
|
|
418
|
+
SELECT RAISE(ABORT, 'direct chat generation lease fence must advance monotonically');
|
|
419
|
+
END;
|
|
420
|
+
|
|
421
|
+
CREATE TRIGGER direct_chat_generation_leases_phase_monotonic
|
|
422
|
+
BEFORE UPDATE ON direct_chat_generation_leases
|
|
423
|
+
WHEN NOT (
|
|
424
|
+
(OLD.phase = 'claimed' AND NEW.phase IN ('claimed', 'dispatch_started', 'released'))
|
|
425
|
+
OR (OLD.phase = 'dispatch_started' AND NEW.phase IN ('dispatch_started', 'released', 'interrupted'))
|
|
426
|
+
OR (
|
|
427
|
+
OLD.phase = 'released' AND NEW.phase = 'released'
|
|
428
|
+
)
|
|
429
|
+
OR (
|
|
430
|
+
OLD.phase = 'released' AND OLD.dispatch_started_at IS NULL
|
|
431
|
+
AND NEW.phase = 'claimed' AND NEW.fence = OLD.fence + 1
|
|
432
|
+
)
|
|
433
|
+
OR (OLD.phase = 'interrupted' AND NEW.phase = 'interrupted')
|
|
434
|
+
)
|
|
435
|
+
BEGIN
|
|
436
|
+
SELECT RAISE(ABORT, 'direct chat generation lease phase must advance monotonically');
|
|
437
|
+
END;
|
|
438
|
+
|
|
439
|
+
CREATE TRIGGER direct_chat_generation_leases_no_delete
|
|
440
|
+
BEFORE DELETE ON direct_chat_generation_leases
|
|
441
|
+
BEGIN
|
|
442
|
+
SELECT RAISE(ABORT, 'direct chat generation lease fences are durable');
|
|
443
|
+
END;
|
|
444
|
+
`;
|
|
445
|
+
|
|
446
|
+
const VISION_ATTACHMENT_SCHEMA = `
|
|
447
|
+
CREATE TABLE direct_chat_attachments (
|
|
448
|
+
account_id TEXT NOT NULL,
|
|
449
|
+
thread_id TEXT NOT NULL,
|
|
450
|
+
attachment_id TEXT NOT NULL CHECK (length(attachment_id) BETWEEN 1 AND 128),
|
|
451
|
+
message_id TEXT NOT NULL CHECK (length(message_id) BETWEEN 1 AND 128),
|
|
452
|
+
media_type TEXT NOT NULL CHECK (media_type IN ('image/jpeg', 'image/png')),
|
|
453
|
+
byte_length INTEGER NOT NULL CHECK (byte_length BETWEEN 1 AND 4194304),
|
|
454
|
+
width INTEGER NOT NULL CHECK (width BETWEEN 1 AND 4096),
|
|
455
|
+
height INTEGER NOT NULL CHECK (height BETWEEN 1 AND 4096),
|
|
456
|
+
content_sha256 TEXT NOT NULL CHECK (
|
|
457
|
+
length(content_sha256) = 64 AND content_sha256 NOT GLOB '*[^0-9a-f]*'
|
|
458
|
+
),
|
|
459
|
+
content BLOB NOT NULL CHECK (length(content) = byte_length),
|
|
460
|
+
created_at TEXT NOT NULL,
|
|
461
|
+
PRIMARY KEY (account_id, thread_id, attachment_id),
|
|
462
|
+
UNIQUE (account_id, thread_id, message_id),
|
|
463
|
+
FOREIGN KEY (account_id, thread_id, message_id)
|
|
464
|
+
REFERENCES direct_chat_messages(account_id, thread_id, message_id) ON DELETE RESTRICT,
|
|
465
|
+
CHECK (width * height <= 16777216)
|
|
466
|
+
) STRICT;
|
|
467
|
+
|
|
468
|
+
CREATE INDEX direct_chat_attachments_owner_latest
|
|
469
|
+
ON direct_chat_attachments(account_id, thread_id, created_at DESC, attachment_id DESC);
|
|
470
|
+
|
|
471
|
+
CREATE TRIGGER direct_chat_attachments_no_update
|
|
472
|
+
BEFORE UPDATE ON direct_chat_attachments
|
|
473
|
+
BEGIN
|
|
474
|
+
SELECT RAISE(ABORT, 'direct chat attachments are immutable');
|
|
475
|
+
END;
|
|
476
|
+
|
|
477
|
+
CREATE TRIGGER direct_chat_attachments_no_delete
|
|
478
|
+
BEFORE DELETE ON direct_chat_attachments
|
|
479
|
+
BEGIN
|
|
480
|
+
SELECT RAISE(ABORT, 'direct chat attachments are durable');
|
|
481
|
+
END;
|
|
482
|
+
|
|
483
|
+
CREATE TRIGGER direct_chat_generations_model_immutable
|
|
484
|
+
BEFORE UPDATE OF model_alias, source_revision, source_hash ON direct_chat_generations
|
|
485
|
+
WHEN NEW.model_alias != OLD.model_alias
|
|
486
|
+
OR NEW.source_revision != OLD.source_revision
|
|
487
|
+
OR NEW.source_hash != OLD.source_hash
|
|
488
|
+
BEGIN
|
|
489
|
+
SELECT RAISE(ABORT, 'direct chat generation inference authority is immutable');
|
|
490
|
+
END;
|
|
491
|
+
`;
|
|
492
|
+
|
|
493
|
+
const MULTI_VISION_ATTACHMENT_SCHEMA = `
|
|
494
|
+
DROP TRIGGER direct_chat_attachments_no_update;
|
|
495
|
+
DROP TRIGGER direct_chat_attachments_no_delete;
|
|
496
|
+
DROP INDEX direct_chat_attachments_owner_latest;
|
|
497
|
+
|
|
498
|
+
ALTER TABLE direct_chat_attachments RENAME TO direct_chat_attachments_v3;
|
|
499
|
+
|
|
500
|
+
CREATE TABLE direct_chat_attachments (
|
|
501
|
+
account_id TEXT NOT NULL,
|
|
502
|
+
thread_id TEXT NOT NULL,
|
|
503
|
+
attachment_id TEXT NOT NULL CHECK (length(attachment_id) BETWEEN 1 AND 128),
|
|
504
|
+
message_id TEXT NOT NULL CHECK (length(message_id) BETWEEN 1 AND 128),
|
|
505
|
+
position INTEGER NOT NULL CHECK (position BETWEEN 0 AND 3),
|
|
506
|
+
media_type TEXT NOT NULL CHECK (media_type IN ('image/jpeg', 'image/png')),
|
|
507
|
+
byte_length INTEGER NOT NULL CHECK (byte_length BETWEEN 1 AND 4194304),
|
|
508
|
+
width INTEGER NOT NULL CHECK (width BETWEEN 1 AND 4096),
|
|
509
|
+
height INTEGER NOT NULL CHECK (height BETWEEN 1 AND 4096),
|
|
510
|
+
content_sha256 TEXT NOT NULL CHECK (
|
|
511
|
+
length(content_sha256) = 64 AND content_sha256 NOT GLOB '*[^0-9a-f]*'
|
|
512
|
+
),
|
|
513
|
+
content BLOB NOT NULL CHECK (length(content) = byte_length),
|
|
514
|
+
created_at TEXT NOT NULL,
|
|
515
|
+
PRIMARY KEY (account_id, thread_id, attachment_id),
|
|
516
|
+
UNIQUE (account_id, thread_id, message_id, position),
|
|
517
|
+
FOREIGN KEY (account_id, thread_id, message_id)
|
|
518
|
+
REFERENCES direct_chat_messages(account_id, thread_id, message_id) ON DELETE RESTRICT,
|
|
519
|
+
CHECK (width * height <= 16777216)
|
|
520
|
+
) STRICT;
|
|
521
|
+
|
|
522
|
+
INSERT INTO direct_chat_attachments(
|
|
523
|
+
account_id, thread_id, attachment_id, message_id, position, media_type,
|
|
524
|
+
byte_length, width, height, content_sha256, content, created_at
|
|
525
|
+
)
|
|
526
|
+
SELECT account_id, thread_id, attachment_id, message_id, 0, media_type,
|
|
527
|
+
byte_length, width, height, content_sha256, content, created_at
|
|
528
|
+
FROM direct_chat_attachments_v3;
|
|
529
|
+
|
|
530
|
+
DROP TABLE direct_chat_attachments_v3;
|
|
531
|
+
|
|
532
|
+
CREATE INDEX direct_chat_attachments_owner_latest
|
|
533
|
+
ON direct_chat_attachments(account_id, thread_id, created_at DESC, attachment_id DESC);
|
|
534
|
+
|
|
535
|
+
CREATE TRIGGER direct_chat_attachments_no_update
|
|
536
|
+
BEFORE UPDATE ON direct_chat_attachments
|
|
537
|
+
BEGIN
|
|
538
|
+
SELECT RAISE(ABORT, 'direct chat attachments are immutable');
|
|
539
|
+
END;
|
|
540
|
+
|
|
541
|
+
CREATE TRIGGER direct_chat_attachments_no_delete
|
|
542
|
+
BEFORE DELETE ON direct_chat_attachments
|
|
543
|
+
BEGIN
|
|
544
|
+
SELECT RAISE(ABORT, 'direct chat attachments are durable');
|
|
545
|
+
END;
|
|
546
|
+
`;
|
|
547
|
+
|
|
548
|
+
const SAFE_THREAD_DELETION_SCHEMA = `
|
|
549
|
+
CREATE TABLE direct_chat_thread_deletions (
|
|
550
|
+
account_id TEXT NOT NULL CHECK (length(account_id) BETWEEN 1 AND 128),
|
|
551
|
+
thread_id TEXT NOT NULL CHECK (length(thread_id) BETWEEN 1 AND 128),
|
|
552
|
+
key_hash TEXT NOT NULL CHECK (
|
|
553
|
+
length(key_hash) = 64 AND key_hash NOT GLOB '*[^0-9a-f]*'
|
|
554
|
+
),
|
|
555
|
+
request_hash TEXT NOT NULL CHECK (
|
|
556
|
+
length(request_hash) = 64 AND request_hash NOT GLOB '*[^0-9a-f]*'
|
|
557
|
+
),
|
|
558
|
+
deleted_revision INTEGER NOT NULL CHECK (deleted_revision BETWEEN 0 AND 2000),
|
|
559
|
+
deleted_hash TEXT,
|
|
560
|
+
result_digest TEXT NOT NULL CHECK (
|
|
561
|
+
length(result_digest) = 64 AND result_digest NOT GLOB '*[^0-9a-f]*'
|
|
562
|
+
),
|
|
563
|
+
deleted_at TEXT NOT NULL,
|
|
564
|
+
PRIMARY KEY (account_id, key_hash),
|
|
565
|
+
UNIQUE (account_id, thread_id),
|
|
566
|
+
CHECK (
|
|
567
|
+
(deleted_revision = 0 AND deleted_hash IS NULL)
|
|
568
|
+
OR (
|
|
569
|
+
deleted_revision > 0
|
|
570
|
+
AND length(deleted_hash) = 64
|
|
571
|
+
AND deleted_hash NOT GLOB '*[^0-9a-f]*'
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
) STRICT;
|
|
575
|
+
|
|
576
|
+
CREATE TRIGGER direct_chat_thread_deletions_no_update
|
|
577
|
+
BEFORE UPDATE ON direct_chat_thread_deletions
|
|
578
|
+
BEGIN
|
|
579
|
+
SELECT RAISE(ABORT, 'direct chat deletion receipts are immutable');
|
|
580
|
+
END;
|
|
581
|
+
|
|
582
|
+
CREATE TRIGGER direct_chat_thread_deletions_no_delete
|
|
583
|
+
BEFORE DELETE ON direct_chat_thread_deletions
|
|
584
|
+
BEGIN
|
|
585
|
+
SELECT RAISE(ABORT, 'direct chat deletion receipts are durable');
|
|
586
|
+
END;
|
|
587
|
+
|
|
588
|
+
CREATE TRIGGER direct_chat_threads_authorized_delete
|
|
589
|
+
BEFORE DELETE ON direct_chat_threads
|
|
590
|
+
WHEN NOT EXISTS (
|
|
591
|
+
SELECT 1 FROM direct_chat_thread_deletions
|
|
592
|
+
WHERE account_id = OLD.account_id AND thread_id = OLD.thread_id
|
|
593
|
+
)
|
|
594
|
+
BEGIN
|
|
595
|
+
SELECT RAISE(ABORT, 'direct chat threads require durable deletion authority');
|
|
596
|
+
END;
|
|
597
|
+
|
|
598
|
+
DROP TRIGGER direct_chat_messages_no_delete;
|
|
599
|
+
CREATE TRIGGER direct_chat_messages_no_delete
|
|
600
|
+
BEFORE DELETE ON direct_chat_messages
|
|
601
|
+
WHEN NOT EXISTS (
|
|
602
|
+
SELECT 1 FROM direct_chat_thread_deletions
|
|
603
|
+
WHERE account_id = OLD.account_id AND thread_id = OLD.thread_id
|
|
604
|
+
)
|
|
605
|
+
BEGIN
|
|
606
|
+
SELECT RAISE(ABORT, 'direct chat messages are append-only');
|
|
607
|
+
END;
|
|
608
|
+
|
|
609
|
+
DROP TRIGGER direct_chat_generations_no_delete;
|
|
610
|
+
CREATE TRIGGER direct_chat_generations_no_delete
|
|
611
|
+
BEFORE DELETE ON direct_chat_generations
|
|
612
|
+
WHEN NOT EXISTS (
|
|
613
|
+
SELECT 1 FROM direct_chat_thread_deletions
|
|
614
|
+
WHERE account_id = OLD.account_id AND thread_id = OLD.thread_id
|
|
615
|
+
)
|
|
616
|
+
BEGIN
|
|
617
|
+
SELECT RAISE(ABORT, 'direct chat generation records are durable');
|
|
618
|
+
END;
|
|
619
|
+
|
|
620
|
+
DROP TRIGGER direct_chat_generation_leases_no_delete;
|
|
621
|
+
CREATE TRIGGER direct_chat_generation_leases_no_delete
|
|
622
|
+
BEFORE DELETE ON direct_chat_generation_leases
|
|
623
|
+
WHEN NOT EXISTS (
|
|
624
|
+
SELECT 1 FROM direct_chat_thread_deletions
|
|
625
|
+
WHERE account_id = OLD.account_id AND thread_id = OLD.thread_id
|
|
626
|
+
)
|
|
627
|
+
BEGIN
|
|
628
|
+
SELECT RAISE(ABORT, 'direct chat generation lease fences are durable');
|
|
629
|
+
END;
|
|
630
|
+
|
|
631
|
+
DROP TRIGGER direct_chat_attachments_no_delete;
|
|
632
|
+
CREATE TRIGGER direct_chat_attachments_no_delete
|
|
633
|
+
BEFORE DELETE ON direct_chat_attachments
|
|
634
|
+
WHEN NOT EXISTS (
|
|
635
|
+
SELECT 1 FROM direct_chat_thread_deletions
|
|
636
|
+
WHERE account_id = OLD.account_id AND thread_id = OLD.thread_id
|
|
637
|
+
)
|
|
638
|
+
BEGIN
|
|
639
|
+
SELECT RAISE(ABORT, 'direct chat attachments are durable');
|
|
640
|
+
END;
|
|
641
|
+
|
|
642
|
+
DROP TRIGGER direct_chat_deltas_no_delete;
|
|
643
|
+
CREATE TRIGGER direct_chat_deltas_no_delete
|
|
644
|
+
BEFORE DELETE ON direct_chat_deltas
|
|
645
|
+
WHEN NOT EXISTS (
|
|
646
|
+
SELECT 1 FROM direct_chat_thread_deletions
|
|
647
|
+
WHERE account_id = OLD.account_id AND thread_id = OLD.thread_id
|
|
648
|
+
)
|
|
649
|
+
AND NOT EXISTS (
|
|
650
|
+
SELECT 1 FROM direct_chat_generations
|
|
651
|
+
WHERE account_id = OLD.account_id
|
|
652
|
+
AND thread_id = OLD.thread_id
|
|
653
|
+
AND generation_id = OLD.generation_id
|
|
654
|
+
AND status = 'completed'
|
|
655
|
+
AND deltas_pruned = 1
|
|
656
|
+
AND EXISTS (
|
|
657
|
+
SELECT 1 FROM direct_chat_generations AS newer
|
|
658
|
+
WHERE newer.account_id = OLD.account_id
|
|
659
|
+
AND newer.thread_id = OLD.thread_id
|
|
660
|
+
AND newer.status = 'completed'
|
|
661
|
+
AND (
|
|
662
|
+
newer.terminal_at > direct_chat_generations.terminal_at
|
|
663
|
+
OR (
|
|
664
|
+
newer.terminal_at = direct_chat_generations.terminal_at
|
|
665
|
+
AND newer.generation_id > direct_chat_generations.generation_id
|
|
666
|
+
)
|
|
667
|
+
)
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
BEGIN
|
|
671
|
+
SELECT RAISE(ABORT, 'only safely retained terminal direct chat deltas may be pruned');
|
|
672
|
+
END;
|
|
673
|
+
|
|
674
|
+
DROP TRIGGER direct_chat_compactions_no_delete;
|
|
675
|
+
CREATE TRIGGER direct_chat_compactions_no_delete
|
|
676
|
+
BEFORE DELETE ON direct_chat_compactions
|
|
677
|
+
WHEN NOT EXISTS (
|
|
678
|
+
SELECT 1 FROM direct_chat_thread_deletions
|
|
679
|
+
WHERE account_id = OLD.account_id AND thread_id = OLD.thread_id
|
|
680
|
+
)
|
|
681
|
+
AND NOT EXISTS (
|
|
682
|
+
SELECT 1 FROM direct_chat_compactions AS newer
|
|
683
|
+
WHERE newer.account_id = OLD.account_id
|
|
684
|
+
AND newer.thread_id = OLD.thread_id
|
|
685
|
+
AND (
|
|
686
|
+
newer.source_end_revision > OLD.source_end_revision
|
|
687
|
+
OR (
|
|
688
|
+
newer.source_end_revision = OLD.source_end_revision
|
|
689
|
+
AND newer.created_at > OLD.created_at
|
|
690
|
+
)
|
|
691
|
+
OR (
|
|
692
|
+
newer.source_end_revision = OLD.source_end_revision
|
|
693
|
+
AND newer.created_at = OLD.created_at
|
|
694
|
+
AND newer.snapshot_id > OLD.snapshot_id
|
|
695
|
+
)
|
|
696
|
+
)
|
|
697
|
+
)
|
|
698
|
+
BEGIN
|
|
699
|
+
SELECT RAISE(ABORT, 'the current direct chat compaction snapshot cannot be pruned');
|
|
700
|
+
END;
|
|
701
|
+
`;
|
|
702
|
+
|
|
703
|
+
function checksum(sql) {
|
|
704
|
+
return createHash('sha256').update(sql, 'utf8').digest('hex');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export const CHAT_MIGRATIONS = Object.freeze([
|
|
708
|
+
Object.freeze({
|
|
709
|
+
version: 1,
|
|
710
|
+
name: 'standalone_direct_chat_ledger',
|
|
711
|
+
sql: INITIAL_CHAT_SCHEMA,
|
|
712
|
+
checksum: checksum(INITIAL_CHAT_SCHEMA)
|
|
713
|
+
}),
|
|
714
|
+
Object.freeze({
|
|
715
|
+
version: 2,
|
|
716
|
+
name: 'fenced_generation_dispatch_leases',
|
|
717
|
+
sql: GENERATION_DISPATCH_LEASE_SCHEMA,
|
|
718
|
+
checksum: checksum(GENERATION_DISPATCH_LEASE_SCHEMA)
|
|
719
|
+
}),
|
|
720
|
+
Object.freeze({
|
|
721
|
+
version: 3,
|
|
722
|
+
name: 'private_vision_attachments',
|
|
723
|
+
sql: VISION_ATTACHMENT_SCHEMA,
|
|
724
|
+
checksum: checksum(VISION_ATTACHMENT_SCHEMA)
|
|
725
|
+
}),
|
|
726
|
+
Object.freeze({
|
|
727
|
+
version: 4,
|
|
728
|
+
name: 'bounded_multi_vision_attachments',
|
|
729
|
+
sql: MULTI_VISION_ATTACHMENT_SCHEMA,
|
|
730
|
+
checksum: checksum(MULTI_VISION_ATTACHMENT_SCHEMA)
|
|
731
|
+
}),
|
|
732
|
+
Object.freeze({
|
|
733
|
+
version: 5,
|
|
734
|
+
name: 'safe_direct_chat_thread_deletion',
|
|
735
|
+
sql: SAFE_THREAD_DELETION_SCHEMA,
|
|
736
|
+
checksum: checksum(SAFE_THREAD_DELETION_SCHEMA)
|
|
737
|
+
})
|
|
738
|
+
]);
|
|
739
|
+
|
|
740
|
+
export const LATEST_CHAT_SCHEMA_VERSION = CHAT_MIGRATIONS[CHAT_MIGRATIONS.length - 1].version;
|
|
741
|
+
// Thread deletion needs the v5 authority/receipt schema in every deployment.
|
|
742
|
+
// The attachment tables remain feature-gated at the application boundary when
|
|
743
|
+
// vision is disabled, but are materialized so one ordered migration lineage is
|
|
744
|
+
// retained for both deployment modes.
|
|
745
|
+
export const DEFAULT_CHAT_SCHEMA_VERSION = LATEST_CHAT_SCHEMA_VERSION;
|
|
746
|
+
|
|
747
|
+
const EXPECTED_SCHEMA_OBJECTS_V2 = Object.freeze([
|
|
748
|
+
'index:direct_chat_compactions_owner_range:direct_chat_compactions',
|
|
749
|
+
'index:direct_chat_deltas_replay:direct_chat_deltas',
|
|
750
|
+
'index:direct_chat_generations_owner_started:direct_chat_generations',
|
|
751
|
+
'index:direct_chat_idempotency_expiry:direct_chat_idempotency',
|
|
752
|
+
'index:direct_chat_idempotency_owner_created:direct_chat_idempotency',
|
|
753
|
+
'index:direct_chat_messages_owner_revision:direct_chat_messages',
|
|
754
|
+
'index:direct_chat_one_active_dispatch:direct_chat_generation_leases',
|
|
755
|
+
'index:direct_chat_one_active_generation:direct_chat_generations',
|
|
756
|
+
'index:direct_chat_threads_owner_updated:direct_chat_threads',
|
|
757
|
+
'table:chat_schema_migrations:chat_schema_migrations',
|
|
758
|
+
'table:direct_chat_compactions:direct_chat_compactions',
|
|
759
|
+
'table:direct_chat_deltas:direct_chat_deltas',
|
|
760
|
+
'table:direct_chat_generation_leases:direct_chat_generation_leases',
|
|
761
|
+
'table:direct_chat_generations:direct_chat_generations',
|
|
762
|
+
'table:direct_chat_idempotency:direct_chat_idempotency',
|
|
763
|
+
'table:direct_chat_messages:direct_chat_messages',
|
|
764
|
+
'table:direct_chat_threads:direct_chat_threads',
|
|
765
|
+
'trigger:direct_chat_compactions_no_delete:direct_chat_compactions',
|
|
766
|
+
'trigger:direct_chat_compactions_no_update:direct_chat_compactions',
|
|
767
|
+
'trigger:direct_chat_deltas_no_delete:direct_chat_deltas',
|
|
768
|
+
'trigger:direct_chat_deltas_no_update:direct_chat_deltas',
|
|
769
|
+
'trigger:direct_chat_generation_leases_fence_monotonic:direct_chat_generation_leases',
|
|
770
|
+
'trigger:direct_chat_generation_leases_no_delete:direct_chat_generation_leases',
|
|
771
|
+
'trigger:direct_chat_generation_leases_phase_monotonic:direct_chat_generation_leases',
|
|
772
|
+
'trigger:direct_chat_generations_no_delete:direct_chat_generations',
|
|
773
|
+
'trigger:direct_chat_generations_prune_irreversible:direct_chat_generations',
|
|
774
|
+
'trigger:direct_chat_generations_prune_only_superseded:direct_chat_generations',
|
|
775
|
+
'trigger:direct_chat_messages_no_delete:direct_chat_messages',
|
|
776
|
+
'trigger:direct_chat_messages_no_update:direct_chat_messages'
|
|
777
|
+
]);
|
|
778
|
+
|
|
779
|
+
const EXPECTED_SCHEMA_OBJECTS_V3 = Object.freeze([
|
|
780
|
+
'index:direct_chat_attachments_owner_latest:direct_chat_attachments',
|
|
781
|
+
...EXPECTED_SCHEMA_OBJECTS_V2.slice(0, 9),
|
|
782
|
+
'table:chat_schema_migrations:chat_schema_migrations',
|
|
783
|
+
'table:direct_chat_attachments:direct_chat_attachments',
|
|
784
|
+
...EXPECTED_SCHEMA_OBJECTS_V2.slice(10, 17),
|
|
785
|
+
'trigger:direct_chat_attachments_no_delete:direct_chat_attachments',
|
|
786
|
+
'trigger:direct_chat_attachments_no_update:direct_chat_attachments',
|
|
787
|
+
...EXPECTED_SCHEMA_OBJECTS_V2.slice(17, 24),
|
|
788
|
+
'trigger:direct_chat_generations_model_immutable:direct_chat_generations',
|
|
789
|
+
...EXPECTED_SCHEMA_OBJECTS_V2.slice(24)
|
|
790
|
+
]);
|
|
791
|
+
|
|
792
|
+
const EXPECTED_SCHEMA_OBJECTS_V4 = EXPECTED_SCHEMA_OBJECTS_V3;
|
|
793
|
+
|
|
794
|
+
const EXPECTED_SCHEMA_OBJECTS_V5 = Object.freeze([
|
|
795
|
+
...EXPECTED_SCHEMA_OBJECTS_V4,
|
|
796
|
+
'table:direct_chat_thread_deletions:direct_chat_thread_deletions',
|
|
797
|
+
'trigger:direct_chat_thread_deletions_no_delete:direct_chat_thread_deletions',
|
|
798
|
+
'trigger:direct_chat_thread_deletions_no_update:direct_chat_thread_deletions',
|
|
799
|
+
'trigger:direct_chat_threads_authorized_delete:direct_chat_threads'
|
|
800
|
+
].sort());
|
|
801
|
+
|
|
802
|
+
const EXPECTED_SCHEMA_FINGERPRINTS = Object.freeze({
|
|
803
|
+
2: 'ad84514c28ac6762061439579862853249f6c87b613f81b79a20947d5c52d4d5',
|
|
804
|
+
3: '1707c8971cd41ed0fe3743af381528ff176dc91c2ed398b2e74ffb0d17b5d0bb',
|
|
805
|
+
4: '4c8475d7a9aa7052733d27f514adfb539266a58c629f7eccf6522bbd70d9a256',
|
|
806
|
+
5: 'd2a81bde2e0a58b31b50222652a924cb82f583d93592f49b1c0c33e1ac10d424'
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
function pragmaInteger(database, pragma) {
|
|
810
|
+
const row = database.prepare(`PRAGMA ${pragma}`).get();
|
|
811
|
+
const value = row?.[pragma];
|
|
812
|
+
if (!Number.isSafeInteger(value)) {
|
|
813
|
+
throw new StorageCorruptionError(`SQLite returned an invalid ${pragma} value.`);
|
|
814
|
+
}
|
|
815
|
+
return value;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function assertIntegrity(database) {
|
|
819
|
+
const integrity = database.prepare('PRAGMA integrity_check').get();
|
|
820
|
+
if (integrity?.integrity_check !== 'ok') {
|
|
821
|
+
throw new StorageCorruptionError('The direct-chat database failed SQLite integrity validation.');
|
|
822
|
+
}
|
|
823
|
+
const foreignKeyFailures = database.prepare('PRAGMA foreign_key_check').all();
|
|
824
|
+
if (foreignKeyFailures.length !== 0) {
|
|
825
|
+
throw new StorageCorruptionError('The direct-chat database contains invalid ownership references.');
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function existingUserTables(database) {
|
|
830
|
+
return database.prepare(`
|
|
831
|
+
SELECT name
|
|
832
|
+
FROM sqlite_schema
|
|
833
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
834
|
+
ORDER BY name
|
|
835
|
+
`).all();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function verifyMigrationLedger(database, currentVersion) {
|
|
839
|
+
let rows;
|
|
840
|
+
try {
|
|
841
|
+
rows = database.prepare(`
|
|
842
|
+
SELECT version, name, checksum
|
|
843
|
+
FROM chat_schema_migrations
|
|
844
|
+
ORDER BY version
|
|
845
|
+
`).all();
|
|
846
|
+
} catch (error) {
|
|
847
|
+
throw new StorageCorruptionError('The direct-chat migration ledger is missing or unreadable.', { cause: error });
|
|
848
|
+
}
|
|
849
|
+
if (rows.length !== currentVersion) {
|
|
850
|
+
throw new StorageCorruptionError('The direct-chat migration ledger does not match the schema version.');
|
|
851
|
+
}
|
|
852
|
+
for (let index = 0; index < currentVersion; index += 1) {
|
|
853
|
+
const expected = CHAT_MIGRATIONS[index];
|
|
854
|
+
const actual = rows[index];
|
|
855
|
+
if (
|
|
856
|
+
Number(actual?.version) !== expected.version ||
|
|
857
|
+
actual?.name !== expected.name ||
|
|
858
|
+
actual?.checksum !== expected.checksum
|
|
859
|
+
) {
|
|
860
|
+
throw new StorageCorruptionError(`Direct-chat migration ${expected.version} failed checksum validation.`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function verifySchemaObjects(database, currentVersion) {
|
|
866
|
+
const rows = database.prepare(`
|
|
867
|
+
SELECT type, name, tbl_name, sql
|
|
868
|
+
FROM sqlite_schema
|
|
869
|
+
WHERE name NOT LIKE 'sqlite_%'
|
|
870
|
+
ORDER BY type, name, tbl_name
|
|
871
|
+
`).all();
|
|
872
|
+
const actual = rows.map((row) => `${row.type}:${row.name}:${row.tbl_name}`);
|
|
873
|
+
const expectedObjects = currentVersion === 2
|
|
874
|
+
? EXPECTED_SCHEMA_OBJECTS_V2
|
|
875
|
+
: (currentVersion === 3
|
|
876
|
+
? EXPECTED_SCHEMA_OBJECTS_V3
|
|
877
|
+
: (currentVersion === 4
|
|
878
|
+
? EXPECTED_SCHEMA_OBJECTS_V4
|
|
879
|
+
: (currentVersion === 5 ? EXPECTED_SCHEMA_OBJECTS_V5 : null)));
|
|
880
|
+
if (expectedObjects === null || JSON.stringify(actual) !== JSON.stringify(expectedObjects)) {
|
|
881
|
+
throw new StorageCorruptionError('The direct-chat schema object set is missing, altered, or extended.');
|
|
882
|
+
}
|
|
883
|
+
if (checksum(JSON.stringify(rows)) !== EXPECTED_SCHEMA_FINGERPRINTS[currentVersion]) {
|
|
884
|
+
throw new StorageCorruptionError('The direct-chat schema definition fingerprint does not match.');
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export function applyChatMigrations(database, appliedAt, { enableVisionAttachments = false } = {}) {
|
|
889
|
+
assertIntegrity(database);
|
|
890
|
+
|
|
891
|
+
const currentVersion = pragmaInteger(database, 'user_version');
|
|
892
|
+
const applicationId = pragmaInteger(database, 'application_id');
|
|
893
|
+
if (currentVersion > LATEST_CHAT_SCHEMA_VERSION) throw new UnsupportedSchemaError();
|
|
894
|
+
if (currentVersion === 0 && applicationId === 0 && existingUserTables(database).length !== 0) {
|
|
895
|
+
throw new StorageCorruptionError('Refusing to claim a non-empty, unidentified direct-chat database.');
|
|
896
|
+
}
|
|
897
|
+
if (currentVersion > 0 && applicationId !== CHAT_SQLITE_APPLICATION_ID) {
|
|
898
|
+
throw new StorageCorruptionError('The database belongs to a different application.');
|
|
899
|
+
}
|
|
900
|
+
if (applicationId !== 0 && applicationId !== CHAT_SQLITE_APPLICATION_ID) {
|
|
901
|
+
throw new StorageCorruptionError('The direct-chat database application identifier is not recognized.');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (typeof enableVisionAttachments !== 'boolean') {
|
|
905
|
+
throw new TypeError('enableVisionAttachments must be boolean');
|
|
906
|
+
}
|
|
907
|
+
if (currentVersion > 0) {
|
|
908
|
+
verifyMigrationLedger(database, currentVersion);
|
|
909
|
+
if (currentVersion >= 2) verifySchemaObjects(database, currentVersion);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// V5 is common to every deployment because safe thread deletion relies on
|
|
913
|
+
// its durable authority receipts. Vision use remains application-gated even
|
|
914
|
+
// though the empty attachment tables now exist in a vision-disabled store.
|
|
915
|
+
const targetVersion = enableVisionAttachments || currentVersion >= 3
|
|
916
|
+
? LATEST_CHAT_SCHEMA_VERSION
|
|
917
|
+
: DEFAULT_CHAT_SCHEMA_VERSION;
|
|
918
|
+
|
|
919
|
+
for (const migration of CHAT_MIGRATIONS.slice(currentVersion, targetVersion)) {
|
|
920
|
+
database.exec('BEGIN IMMEDIATE');
|
|
921
|
+
try {
|
|
922
|
+
database.exec(migration.sql);
|
|
923
|
+
database.prepare(`
|
|
924
|
+
INSERT INTO chat_schema_migrations(version, name, checksum, applied_at)
|
|
925
|
+
VALUES (?, ?, ?, ?)
|
|
926
|
+
`).run(migration.version, migration.name, migration.checksum, appliedAt);
|
|
927
|
+
database.exec(`PRAGMA application_id = ${CHAT_SQLITE_APPLICATION_ID}`);
|
|
928
|
+
database.exec(`PRAGMA user_version = ${migration.version}`);
|
|
929
|
+
database.exec('COMMIT');
|
|
930
|
+
} catch (error) {
|
|
931
|
+
try {
|
|
932
|
+
database.exec('ROLLBACK');
|
|
933
|
+
} catch {
|
|
934
|
+
// Preserve the migration failure.
|
|
935
|
+
}
|
|
936
|
+
throw new StorageCorruptionError(
|
|
937
|
+
`Direct-chat migration ${migration.version} could not be applied.`,
|
|
938
|
+
{ cause: error }
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
verifyMigrationLedger(database, targetVersion);
|
|
944
|
+
verifySchemaObjects(database, targetVersion);
|
|
945
|
+
assertIntegrity(database);
|
|
946
|
+
return targetVersion;
|
|
947
|
+
}
|