ai_co_discuss 0.2.41 → 0.2.43
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/.agents/skills/discuss-host/SKILL.md +1 -0
- package/backend/admin.py +186 -0
- package/backend/schema.sql +670 -655
- package/backend/server.py +57 -1
- package/docs/architecture.md +4 -1
- package/docs/deployment.md +4 -0
- package/docs/runner.md +1 -1
- package/docs/workflow.md +1 -1
- package/package.json +1 -1
- package/runner/discussion_runner.mjs +5 -2
- package/runner/scheduler.mjs +50 -26
- package/web/app.js +171 -9
- package/web/modern.css +1 -1
- package/web/modern.js +1441 -1127
package/backend/schema.sql
CHANGED
|
@@ -1,655 +1,670 @@
|
|
|
1
|
-
CREATE TABLE IF NOT EXISTS rooms (
|
|
2
|
-
id TEXT PRIMARY KEY, title TEXT NOT NULL, goal TEXT NOT NULL,
|
|
3
|
-
state_text TEXT NOT NULL DEFAULT '', state_until INTEGER NOT NULL DEFAULT 0,
|
|
4
|
-
created_at TEXT NOT NULL
|
|
5
|
-
);
|
|
6
|
-
CREATE TABLE IF NOT EXISTS room_directory (
|
|
7
|
-
room_id TEXT PRIMARY KEY REFERENCES rooms(id),
|
|
8
|
-
position INTEGER NOT NULL DEFAULT 0,
|
|
9
|
-
archived INTEGER NOT NULL DEFAULT 0 CHECK (archived IN (0,1))
|
|
10
|
-
);
|
|
11
|
-
INSERT OR IGNORE INTO room_directory (room_id,position)
|
|
12
|
-
SELECT id,ROW_NUMBER() OVER (ORDER BY created_at,id) FROM rooms;
|
|
13
|
-
CREATE TRIGGER IF NOT EXISTS add_room_to_directory AFTER INSERT ON rooms
|
|
14
|
-
BEGIN
|
|
15
|
-
INSERT INTO room_directory (room_id,position)
|
|
16
|
-
VALUES (NEW.id,(SELECT COALESCE(MAX(position),0)+1 FROM room_directory));
|
|
17
|
-
END;
|
|
18
|
-
CREATE TABLE IF NOT EXISTS topic_folders (
|
|
19
|
-
id TEXT PRIMARY KEY, name TEXT NOT NULL, position INTEGER NOT NULL DEFAULT 0
|
|
20
|
-
);
|
|
21
|
-
CREATE TABLE IF NOT EXISTS directory_settings (
|
|
22
|
-
id INTEGER PRIMARY KEY CHECK (id=1),
|
|
23
|
-
uncategorized_position INTEGER CHECK (uncategorized_position>=0)
|
|
24
|
-
);
|
|
25
|
-
CREATE TABLE IF NOT EXISTS room_folder_assignments (
|
|
26
|
-
room_id TEXT PRIMARY KEY REFERENCES rooms(id),
|
|
27
|
-
folder_id TEXT REFERENCES topic_folders(id) ON DELETE SET NULL
|
|
28
|
-
);
|
|
29
|
-
INSERT OR IGNORE INTO room_folder_assignments (room_id,folder_id)
|
|
30
|
-
SELECT id,NULL FROM rooms;
|
|
31
|
-
CREATE TRIGGER IF NOT EXISTS add_room_to_folder_assignments AFTER INSERT ON rooms
|
|
32
|
-
BEGIN
|
|
33
|
-
INSERT INTO room_folder_assignments (room_id,folder_id) VALUES (NEW.id,NULL);
|
|
34
|
-
END;
|
|
35
|
-
CREATE TABLE IF NOT EXISTS room_creations (
|
|
36
|
-
submission_id TEXT PRIMARY KEY,
|
|
37
|
-
room_id TEXT NOT NULL UNIQUE REFERENCES rooms(id),
|
|
38
|
-
request_text TEXT NOT NULL
|
|
39
|
-
);
|
|
40
|
-
CREATE TABLE IF NOT EXISTS room_user_requests (
|
|
41
|
-
room_id TEXT PRIMARY KEY REFERENCES rooms(id),
|
|
42
|
-
text TEXT NOT NULL DEFAULT ''
|
|
43
|
-
);
|
|
44
|
-
CREATE TABLE IF NOT EXISTS room_briefs (
|
|
45
|
-
room_id TEXT PRIMARY KEY REFERENCES rooms(id),
|
|
46
|
-
text TEXT NOT NULL, until INTEGER NOT NULL, updated_at TEXT NOT NULL
|
|
47
|
-
);
|
|
48
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
49
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, room_id TEXT NOT NULL REFERENCES rooms(id),
|
|
50
|
-
author TEXT NOT NULL, text TEXT NOT NULL, reply_to INTEGER REFERENCES messages(id),
|
|
51
|
-
source TEXT NOT NULL, submission_id TEXT NOT NULL, created_at TEXT NOT NULL,
|
|
52
|
-
UNIQUE(room_id, submission_id)
|
|
53
|
-
);
|
|
54
|
-
CREATE INDEX IF NOT EXISTS messages_room ON messages(room_id, id);
|
|
55
|
-
CREATE TABLE IF NOT EXISTS attachments (
|
|
56
|
-
id TEXT PRIMARY KEY, room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
57
|
-
message_id INTEGER REFERENCES messages(id) ON DELETE CASCADE,
|
|
58
|
-
position INTEGER NOT NULL DEFAULT 0,
|
|
59
|
-
file_name TEXT NOT NULL, content_type TEXT NOT NULL,
|
|
60
|
-
size INTEGER NOT NULL, sha256 TEXT NOT NULL,
|
|
61
|
-
actor TEXT NOT NULL, submission_id TEXT NOT NULL, created_at TEXT NOT NULL,
|
|
62
|
-
UNIQUE(room_id, submission_id)
|
|
63
|
-
);
|
|
64
|
-
CREATE INDEX IF NOT EXISTS attachments_message ON attachments(message_id, position);
|
|
65
|
-
CREATE TRIGGER IF NOT EXISTS validate_attachment_link BEFORE UPDATE OF message_id ON attachments
|
|
66
|
-
WHEN NEW.message_id IS NOT NULL AND (
|
|
67
|
-
(OLD.message_id IS NOT NULL AND OLD.message_id != NEW.message_id)
|
|
68
|
-
OR NOT EXISTS (
|
|
69
|
-
SELECT 1 FROM messages m JOIN message_details d ON d.message_id=m.id
|
|
70
|
-
WHERE m.id=NEW.message_id AND m.room_id=NEW.room_id AND d.submitted_by=NEW.actor
|
|
71
|
-
)
|
|
72
|
-
)
|
|
73
|
-
BEGIN
|
|
74
|
-
SELECT RAISE(ABORT, 'attachment_unavailable');
|
|
75
|
-
END;
|
|
76
|
-
CREATE TABLE IF NOT EXISTS message_summaries (
|
|
77
|
-
message_id INTEGER PRIMARY KEY REFERENCES messages(id),
|
|
78
|
-
text TEXT NOT NULL DEFAULT '', author TEXT NOT NULL DEFAULT ''
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
CREATE TABLE IF NOT EXISTS topic_settings (
|
|
82
|
-
room_id TEXT PRIMARY KEY REFERENCES rooms(id),
|
|
83
|
-
host TEXT NOT NULL, background TEXT NOT NULL DEFAULT '', boundaries TEXT NOT NULL DEFAULT '',
|
|
84
|
-
experts TEXT NOT NULL, phase TEXT NOT NULL DEFAULT 'preparing', focus TEXT NOT NULL DEFAULT ''
|
|
85
|
-
);
|
|
86
|
-
CREATE TRIGGER IF NOT EXISTS reject_archived_resume BEFORE UPDATE OF phase ON topic_settings
|
|
87
|
-
WHEN NEW.phase NOT IN ('paused','concluded')
|
|
88
|
-
AND EXISTS (SELECT 1 FROM room_directory WHERE room_id=NEW.room_id AND archived=1)
|
|
89
|
-
BEGIN
|
|
90
|
-
SELECT RAISE(ABORT, 'topic_archived');
|
|
91
|
-
END;
|
|
92
|
-
CREATE TABLE IF NOT EXISTS discussion_turns (
|
|
93
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, room_id TEXT NOT NULL REFERENCES rooms(id),
|
|
94
|
-
expert TEXT NOT NULL, question TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
|
|
95
|
-
submission_id TEXT NOT NULL, created_at TEXT NOT NULL, notified_at TEXT NOT NULL DEFAULT '',
|
|
96
|
-
answer_id INTEGER REFERENCES messages(id), UNIQUE(room_id, submission_id)
|
|
97
|
-
);
|
|
98
|
-
CREATE UNIQUE INDEX IF NOT EXISTS one_open_turn ON discussion_turns(room_id)
|
|
99
|
-
WHERE status IN ('pending', 'notified');
|
|
100
|
-
CREATE TABLE IF NOT EXISTS message_details (
|
|
101
|
-
message_id INTEGER PRIMARY KEY REFERENCES messages(id),
|
|
102
|
-
kind TEXT NOT NULL DEFAULT 'opinion', submitted_by TEXT NOT NULL,
|
|
103
|
-
turn_id INTEGER UNIQUE REFERENCES discussion_turns(id)
|
|
104
|
-
);
|
|
105
|
-
CREATE TRIGGER IF NOT EXISTS validate_turn_answer BEFORE INSERT ON message_details
|
|
106
|
-
WHEN NEW.turn_id IS NOT NULL
|
|
107
|
-
AND NOT EXISTS (SELECT 1 FROM message_details WHERE message_id=NEW.message_id)
|
|
108
|
-
AND NOT EXISTS (
|
|
109
|
-
SELECT 1 FROM discussion_turns JOIN messages ON messages.id=NEW.message_id
|
|
110
|
-
WHERE discussion_turns.id=NEW.turn_id AND discussion_turns.room_id=messages.room_id
|
|
111
|
-
AND discussion_turns.expert=messages.author AND discussion_turns.status IN ('pending','notified')
|
|
112
|
-
AND NEW.kind='opinion'
|
|
113
|
-
)
|
|
114
|
-
BEGIN
|
|
115
|
-
SELECT RAISE(ABORT, 'turn_unavailable');
|
|
116
|
-
END;
|
|
117
|
-
|
|
118
|
-
CREATE TRIGGER IF NOT EXISTS reject_inactive_opinion BEFORE INSERT ON message_details
|
|
119
|
-
WHEN NEW.kind='opinion'
|
|
120
|
-
AND NOT EXISTS (SELECT 1 FROM message_details WHERE message_id=NEW.message_id)
|
|
121
|
-
AND EXISTS (SELECT 1 FROM messages JOIN topic_settings ON topic_settings.room_id=messages.room_id
|
|
122
|
-
WHERE messages.id=NEW.message_id AND topic_settings.phase IN ('paused','concluded'))
|
|
123
|
-
BEGIN
|
|
124
|
-
SELECT RAISE(ABORT, 'topic_inactive');
|
|
125
|
-
END;
|
|
126
|
-
CREATE TRIGGER IF NOT EXISTS reject_inactive_turn BEFORE INSERT ON discussion_turns
|
|
127
|
-
WHEN NOT EXISTS (SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND submission_id=NEW.submission_id)
|
|
128
|
-
AND EXISTS (SELECT 1 FROM topic_settings WHERE room_id=NEW.room_id AND phase IN ('paused','concluded'))
|
|
129
|
-
BEGIN
|
|
130
|
-
SELECT RAISE(ABORT, 'topic_inactive');
|
|
131
|
-
END;
|
|
132
|
-
CREATE TRIGGER IF NOT EXISTS reject_conclusion_with_open_turn BEFORE UPDATE OF phase ON topic_settings
|
|
133
|
-
WHEN NEW.phase='concluded'
|
|
134
|
-
AND EXISTS (SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'))
|
|
135
|
-
BEGIN
|
|
136
|
-
SELECT RAISE(ABORT, 'open_turn');
|
|
137
|
-
END;
|
|
138
|
-
|
|
139
|
-
CREATE TABLE IF NOT EXISTS room_floor (
|
|
140
|
-
room_id TEXT PRIMARY KEY REFERENCES rooms(id),
|
|
141
|
-
holder TEXT,
|
|
142
|
-
next_speaker TEXT NOT NULL,
|
|
143
|
-
revision INTEGER NOT NULL DEFAULT 0
|
|
144
|
-
);
|
|
145
|
-
INSERT OR IGNORE INTO room_floor (room_id,holder,next_speaker)
|
|
146
|
-
SELECT rooms.id,CASE WHEN turns.id IS NULL THEN COALESCE(settings.host,'用户') END,
|
|
147
|
-
COALESCE(turns.expert,settings.host,'用户')
|
|
148
|
-
FROM rooms LEFT JOIN topic_settings AS settings ON settings.room_id=rooms.id
|
|
149
|
-
LEFT JOIN discussion_turns AS turns ON turns.room_id=rooms.id AND turns.status IN ('pending','notified');
|
|
150
|
-
CREATE TRIGGER IF NOT EXISTS initialize_room_floor AFTER INSERT ON topic_settings
|
|
151
|
-
BEGIN
|
|
152
|
-
INSERT INTO room_floor (room_id,holder,next_speaker) VALUES (NEW.room_id,NEW.host,NEW.host)
|
|
153
|
-
ON CONFLICT(room_id) DO UPDATE SET
|
|
154
|
-
holder=CASE WHEN holder='用户' THEN NEW.host ELSE holder END,
|
|
155
|
-
next_speaker=CASE WHEN next_speaker='用户' THEN NEW.host ELSE next_speaker END,
|
|
156
|
-
revision=revision+1;
|
|
157
|
-
END;
|
|
158
|
-
CREATE TRIGGER IF NOT EXISTS transfer_host_floor AFTER UPDATE OF host ON topic_settings
|
|
159
|
-
WHEN OLD.host<>NEW.host
|
|
160
|
-
BEGIN
|
|
161
|
-
UPDATE room_floor SET holder=CASE WHEN holder=OLD.host THEN NEW.host ELSE holder END,
|
|
162
|
-
next_speaker=CASE WHEN next_speaker=OLD.host THEN NEW.host ELSE next_speaker END,
|
|
163
|
-
revision=revision+1 WHERE room_id=NEW.room_id
|
|
164
|
-
AND NOT EXISTS (SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified') AND expert=OLD.host);
|
|
165
|
-
END;
|
|
166
|
-
--
|
|
167
|
-
CREATE TABLE IF NOT EXISTS
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
CREATE
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
CREATE TRIGGER
|
|
190
|
-
WHEN
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
AND
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
END;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
);
|
|
321
|
-
CREATE TRIGGER IF NOT EXISTS
|
|
322
|
-
WHEN OLD.
|
|
323
|
-
BEGIN
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
--
|
|
366
|
-
DROP TRIGGER IF EXISTS
|
|
367
|
-
CREATE TRIGGER
|
|
368
|
-
WHEN
|
|
369
|
-
BEGIN
|
|
370
|
-
UPDATE
|
|
371
|
-
UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id
|
|
372
|
-
AND NOT EXISTS(SELECT 1 FROM messages m JOIN discussion_events e ON e.room_id=m.room_id AND e.role='host' AND e.status='started'
|
|
373
|
-
WHERE m.room_id=NEW.room_id AND m.author='用户' AND CAST(strftime('%s',m.created_at) AS INTEGER)>e.started_at);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
WHERE room_id=NEW.
|
|
389
|
-
END;
|
|
390
|
-
CREATE TRIGGER IF NOT EXISTS
|
|
391
|
-
WHEN
|
|
392
|
-
BEGIN
|
|
393
|
-
INSERT INTO turn_relay(room_id) SELECT
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
UPDATE
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
CREATE TRIGGER
|
|
406
|
-
WHEN NEW.
|
|
407
|
-
|
|
408
|
-
WHERE NOT EXISTS
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
WHERE room_id=NEW.room_id AND status='running'
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
CREATE TRIGGER
|
|
477
|
-
WHEN NEW.
|
|
478
|
-
BEGIN
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
);
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
);
|
|
556
|
-
CREATE
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
member_id INTEGER
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
)
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
provider TEXT NOT NULL,
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
)
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
)
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
);
|
|
643
|
-
--
|
|
644
|
-
CREATE TABLE IF NOT EXISTS
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1
|
+
CREATE TABLE IF NOT EXISTS rooms (
|
|
2
|
+
id TEXT PRIMARY KEY, title TEXT NOT NULL, goal TEXT NOT NULL,
|
|
3
|
+
state_text TEXT NOT NULL DEFAULT '', state_until INTEGER NOT NULL DEFAULT 0,
|
|
4
|
+
created_at TEXT NOT NULL
|
|
5
|
+
);
|
|
6
|
+
CREATE TABLE IF NOT EXISTS room_directory (
|
|
7
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id),
|
|
8
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
9
|
+
archived INTEGER NOT NULL DEFAULT 0 CHECK (archived IN (0,1))
|
|
10
|
+
);
|
|
11
|
+
INSERT OR IGNORE INTO room_directory (room_id,position)
|
|
12
|
+
SELECT id,ROW_NUMBER() OVER (ORDER BY created_at,id) FROM rooms;
|
|
13
|
+
CREATE TRIGGER IF NOT EXISTS add_room_to_directory AFTER INSERT ON rooms
|
|
14
|
+
BEGIN
|
|
15
|
+
INSERT INTO room_directory (room_id,position)
|
|
16
|
+
VALUES (NEW.id,(SELECT COALESCE(MAX(position),0)+1 FROM room_directory));
|
|
17
|
+
END;
|
|
18
|
+
CREATE TABLE IF NOT EXISTS topic_folders (
|
|
19
|
+
id TEXT PRIMARY KEY, name TEXT NOT NULL, position INTEGER NOT NULL DEFAULT 0
|
|
20
|
+
);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS directory_settings (
|
|
22
|
+
id INTEGER PRIMARY KEY CHECK (id=1),
|
|
23
|
+
uncategorized_position INTEGER CHECK (uncategorized_position>=0)
|
|
24
|
+
);
|
|
25
|
+
CREATE TABLE IF NOT EXISTS room_folder_assignments (
|
|
26
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id),
|
|
27
|
+
folder_id TEXT REFERENCES topic_folders(id) ON DELETE SET NULL
|
|
28
|
+
);
|
|
29
|
+
INSERT OR IGNORE INTO room_folder_assignments (room_id,folder_id)
|
|
30
|
+
SELECT id,NULL FROM rooms;
|
|
31
|
+
CREATE TRIGGER IF NOT EXISTS add_room_to_folder_assignments AFTER INSERT ON rooms
|
|
32
|
+
BEGIN
|
|
33
|
+
INSERT INTO room_folder_assignments (room_id,folder_id) VALUES (NEW.id,NULL);
|
|
34
|
+
END;
|
|
35
|
+
CREATE TABLE IF NOT EXISTS room_creations (
|
|
36
|
+
submission_id TEXT PRIMARY KEY,
|
|
37
|
+
room_id TEXT NOT NULL UNIQUE REFERENCES rooms(id),
|
|
38
|
+
request_text TEXT NOT NULL
|
|
39
|
+
);
|
|
40
|
+
CREATE TABLE IF NOT EXISTS room_user_requests (
|
|
41
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id),
|
|
42
|
+
text TEXT NOT NULL DEFAULT ''
|
|
43
|
+
);
|
|
44
|
+
CREATE TABLE IF NOT EXISTS room_briefs (
|
|
45
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id),
|
|
46
|
+
text TEXT NOT NULL, until INTEGER NOT NULL, updated_at TEXT NOT NULL
|
|
47
|
+
);
|
|
48
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
49
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, room_id TEXT NOT NULL REFERENCES rooms(id),
|
|
50
|
+
author TEXT NOT NULL, text TEXT NOT NULL, reply_to INTEGER REFERENCES messages(id),
|
|
51
|
+
source TEXT NOT NULL, submission_id TEXT NOT NULL, created_at TEXT NOT NULL,
|
|
52
|
+
UNIQUE(room_id, submission_id)
|
|
53
|
+
);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS messages_room ON messages(room_id, id);
|
|
55
|
+
CREATE TABLE IF NOT EXISTS attachments (
|
|
56
|
+
id TEXT PRIMARY KEY, room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
57
|
+
message_id INTEGER REFERENCES messages(id) ON DELETE CASCADE,
|
|
58
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
59
|
+
file_name TEXT NOT NULL, content_type TEXT NOT NULL,
|
|
60
|
+
size INTEGER NOT NULL, sha256 TEXT NOT NULL,
|
|
61
|
+
actor TEXT NOT NULL, submission_id TEXT NOT NULL, created_at TEXT NOT NULL,
|
|
62
|
+
UNIQUE(room_id, submission_id)
|
|
63
|
+
);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS attachments_message ON attachments(message_id, position);
|
|
65
|
+
CREATE TRIGGER IF NOT EXISTS validate_attachment_link BEFORE UPDATE OF message_id ON attachments
|
|
66
|
+
WHEN NEW.message_id IS NOT NULL AND (
|
|
67
|
+
(OLD.message_id IS NOT NULL AND OLD.message_id != NEW.message_id)
|
|
68
|
+
OR NOT EXISTS (
|
|
69
|
+
SELECT 1 FROM messages m JOIN message_details d ON d.message_id=m.id
|
|
70
|
+
WHERE m.id=NEW.message_id AND m.room_id=NEW.room_id AND d.submitted_by=NEW.actor
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
BEGIN
|
|
74
|
+
SELECT RAISE(ABORT, 'attachment_unavailable');
|
|
75
|
+
END;
|
|
76
|
+
CREATE TABLE IF NOT EXISTS message_summaries (
|
|
77
|
+
message_id INTEGER PRIMARY KEY REFERENCES messages(id),
|
|
78
|
+
text TEXT NOT NULL DEFAULT '', author TEXT NOT NULL DEFAULT ''
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS topic_settings (
|
|
82
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id),
|
|
83
|
+
host TEXT NOT NULL, background TEXT NOT NULL DEFAULT '', boundaries TEXT NOT NULL DEFAULT '',
|
|
84
|
+
experts TEXT NOT NULL, phase TEXT NOT NULL DEFAULT 'preparing', focus TEXT NOT NULL DEFAULT ''
|
|
85
|
+
);
|
|
86
|
+
CREATE TRIGGER IF NOT EXISTS reject_archived_resume BEFORE UPDATE OF phase ON topic_settings
|
|
87
|
+
WHEN NEW.phase NOT IN ('paused','concluded')
|
|
88
|
+
AND EXISTS (SELECT 1 FROM room_directory WHERE room_id=NEW.room_id AND archived=1)
|
|
89
|
+
BEGIN
|
|
90
|
+
SELECT RAISE(ABORT, 'topic_archived');
|
|
91
|
+
END;
|
|
92
|
+
CREATE TABLE IF NOT EXISTS discussion_turns (
|
|
93
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, room_id TEXT NOT NULL REFERENCES rooms(id),
|
|
94
|
+
expert TEXT NOT NULL, question TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
|
|
95
|
+
submission_id TEXT NOT NULL, created_at TEXT NOT NULL, notified_at TEXT NOT NULL DEFAULT '',
|
|
96
|
+
answer_id INTEGER REFERENCES messages(id), UNIQUE(room_id, submission_id)
|
|
97
|
+
);
|
|
98
|
+
CREATE UNIQUE INDEX IF NOT EXISTS one_open_turn ON discussion_turns(room_id)
|
|
99
|
+
WHERE status IN ('pending', 'notified');
|
|
100
|
+
CREATE TABLE IF NOT EXISTS message_details (
|
|
101
|
+
message_id INTEGER PRIMARY KEY REFERENCES messages(id),
|
|
102
|
+
kind TEXT NOT NULL DEFAULT 'opinion', submitted_by TEXT NOT NULL,
|
|
103
|
+
turn_id INTEGER UNIQUE REFERENCES discussion_turns(id)
|
|
104
|
+
);
|
|
105
|
+
CREATE TRIGGER IF NOT EXISTS validate_turn_answer BEFORE INSERT ON message_details
|
|
106
|
+
WHEN NEW.turn_id IS NOT NULL
|
|
107
|
+
AND NOT EXISTS (SELECT 1 FROM message_details WHERE message_id=NEW.message_id)
|
|
108
|
+
AND NOT EXISTS (
|
|
109
|
+
SELECT 1 FROM discussion_turns JOIN messages ON messages.id=NEW.message_id
|
|
110
|
+
WHERE discussion_turns.id=NEW.turn_id AND discussion_turns.room_id=messages.room_id
|
|
111
|
+
AND discussion_turns.expert=messages.author AND discussion_turns.status IN ('pending','notified')
|
|
112
|
+
AND NEW.kind='opinion'
|
|
113
|
+
)
|
|
114
|
+
BEGIN
|
|
115
|
+
SELECT RAISE(ABORT, 'turn_unavailable');
|
|
116
|
+
END;
|
|
117
|
+
|
|
118
|
+
CREATE TRIGGER IF NOT EXISTS reject_inactive_opinion BEFORE INSERT ON message_details
|
|
119
|
+
WHEN NEW.kind='opinion'
|
|
120
|
+
AND NOT EXISTS (SELECT 1 FROM message_details WHERE message_id=NEW.message_id)
|
|
121
|
+
AND EXISTS (SELECT 1 FROM messages JOIN topic_settings ON topic_settings.room_id=messages.room_id
|
|
122
|
+
WHERE messages.id=NEW.message_id AND topic_settings.phase IN ('paused','concluded'))
|
|
123
|
+
BEGIN
|
|
124
|
+
SELECT RAISE(ABORT, 'topic_inactive');
|
|
125
|
+
END;
|
|
126
|
+
CREATE TRIGGER IF NOT EXISTS reject_inactive_turn BEFORE INSERT ON discussion_turns
|
|
127
|
+
WHEN NOT EXISTS (SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND submission_id=NEW.submission_id)
|
|
128
|
+
AND EXISTS (SELECT 1 FROM topic_settings WHERE room_id=NEW.room_id AND phase IN ('paused','concluded'))
|
|
129
|
+
BEGIN
|
|
130
|
+
SELECT RAISE(ABORT, 'topic_inactive');
|
|
131
|
+
END;
|
|
132
|
+
CREATE TRIGGER IF NOT EXISTS reject_conclusion_with_open_turn BEFORE UPDATE OF phase ON topic_settings
|
|
133
|
+
WHEN NEW.phase='concluded'
|
|
134
|
+
AND EXISTS (SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'))
|
|
135
|
+
BEGIN
|
|
136
|
+
SELECT RAISE(ABORT, 'open_turn');
|
|
137
|
+
END;
|
|
138
|
+
|
|
139
|
+
CREATE TABLE IF NOT EXISTS room_floor (
|
|
140
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id),
|
|
141
|
+
holder TEXT,
|
|
142
|
+
next_speaker TEXT NOT NULL,
|
|
143
|
+
revision INTEGER NOT NULL DEFAULT 0
|
|
144
|
+
);
|
|
145
|
+
INSERT OR IGNORE INTO room_floor (room_id,holder,next_speaker)
|
|
146
|
+
SELECT rooms.id,CASE WHEN turns.id IS NULL THEN COALESCE(settings.host,'用户') END,
|
|
147
|
+
COALESCE(turns.expert,settings.host,'用户')
|
|
148
|
+
FROM rooms LEFT JOIN topic_settings AS settings ON settings.room_id=rooms.id
|
|
149
|
+
LEFT JOIN discussion_turns AS turns ON turns.room_id=rooms.id AND turns.status IN ('pending','notified');
|
|
150
|
+
CREATE TRIGGER IF NOT EXISTS initialize_room_floor AFTER INSERT ON topic_settings
|
|
151
|
+
BEGIN
|
|
152
|
+
INSERT INTO room_floor (room_id,holder,next_speaker) VALUES (NEW.room_id,NEW.host,NEW.host)
|
|
153
|
+
ON CONFLICT(room_id) DO UPDATE SET
|
|
154
|
+
holder=CASE WHEN holder='用户' THEN NEW.host ELSE holder END,
|
|
155
|
+
next_speaker=CASE WHEN next_speaker='用户' THEN NEW.host ELSE next_speaker END,
|
|
156
|
+
revision=revision+1;
|
|
157
|
+
END;
|
|
158
|
+
CREATE TRIGGER IF NOT EXISTS transfer_host_floor AFTER UPDATE OF host ON topic_settings
|
|
159
|
+
WHEN OLD.host<>NEW.host
|
|
160
|
+
BEGIN
|
|
161
|
+
UPDATE room_floor SET holder=CASE WHEN holder=OLD.host THEN NEW.host ELSE holder END,
|
|
162
|
+
next_speaker=CASE WHEN next_speaker=OLD.host THEN NEW.host ELSE next_speaker END,
|
|
163
|
+
revision=revision+1 WHERE room_id=NEW.room_id
|
|
164
|
+
AND NOT EXISTS (SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified') AND expert=OLD.host);
|
|
165
|
+
END;
|
|
166
|
+
-- Site administrator account (single user) and its login sessions. Raw tokens and passwords are never stored.
|
|
167
|
+
CREATE TABLE IF NOT EXISTS site_admin (
|
|
168
|
+
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
169
|
+
username TEXT NOT NULL,
|
|
170
|
+
password_hash TEXT NOT NULL,
|
|
171
|
+
avatar TEXT NOT NULL DEFAULT '',
|
|
172
|
+
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
|
173
|
+
locked_until INTEGER NOT NULL DEFAULT 0,
|
|
174
|
+
created_at INTEGER NOT NULL
|
|
175
|
+
);
|
|
176
|
+
CREATE TABLE IF NOT EXISTS admin_sessions (
|
|
177
|
+
token_hash TEXT PRIMARY KEY,
|
|
178
|
+
created_at INTEGER NOT NULL
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
-- Free collaboration rooms. Rooms without a row are brainstorm rooms and keep every rule below.
|
|
182
|
+
CREATE TABLE IF NOT EXISTS room_modes (
|
|
183
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
|
|
184
|
+
mode TEXT NOT NULL CHECK(mode IN ('free','brainstorm')),
|
|
185
|
+
control_hash TEXT NOT NULL DEFAULT ''
|
|
186
|
+
);
|
|
187
|
+
-- Replaced on every start so existing databases drop the floor rule for free rooms.
|
|
188
|
+
DROP TRIGGER IF EXISTS require_message_floor;
|
|
189
|
+
CREATE TRIGGER require_message_floor BEFORE INSERT ON messages
|
|
190
|
+
WHEN NEW.author<>'用户'
|
|
191
|
+
AND NOT EXISTS (SELECT 1 FROM messages WHERE room_id=NEW.room_id AND submission_id=NEW.submission_id)
|
|
192
|
+
AND NOT EXISTS (SELECT 1 FROM room_floor WHERE room_id=NEW.room_id AND holder=NEW.author)
|
|
193
|
+
AND NOT EXISTS (SELECT 1 FROM room_modes WHERE room_id=NEW.room_id AND mode='free')
|
|
194
|
+
BEGIN
|
|
195
|
+
SELECT RAISE(ABORT, 'floor_required');
|
|
196
|
+
END;
|
|
197
|
+
CREATE TRIGGER IF NOT EXISTS require_opinion_turn BEFORE INSERT ON message_details
|
|
198
|
+
WHEN NEW.kind='opinion' AND NEW.turn_id IS NULL
|
|
199
|
+
AND NOT EXISTS (SELECT 1 FROM message_details WHERE message_id=NEW.message_id)
|
|
200
|
+
AND EXISTS (SELECT 1 FROM messages WHERE id=NEW.message_id AND author<>'用户')
|
|
201
|
+
BEGIN
|
|
202
|
+
SELECT RAISE(ABORT, 'floor_turn_required');
|
|
203
|
+
END;
|
|
204
|
+
CREATE TRIGGER IF NOT EXISTS return_message_floor AFTER INSERT ON message_details
|
|
205
|
+
WHEN EXISTS (SELECT 1 FROM messages WHERE id=NEW.message_id AND author<>'用户')
|
|
206
|
+
BEGIN
|
|
207
|
+
UPDATE room_floor SET holder=NULL,
|
|
208
|
+
next_speaker=COALESCE((SELECT host FROM topic_settings WHERE room_id=room_floor.room_id),'用户'),
|
|
209
|
+
revision=revision+1 WHERE room_id=(SELECT room_id FROM messages WHERE id=NEW.message_id);
|
|
210
|
+
END;
|
|
211
|
+
DROP TRIGGER IF EXISTS require_host_floor;
|
|
212
|
+
-- Queued turns of a round plan are inserted after the first one has already nominated an expert.
|
|
213
|
+
CREATE TRIGGER require_host_floor BEFORE INSERT ON discussion_turns
|
|
214
|
+
WHEN NEW.status<>'queued' AND NOT EXISTS (SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND submission_id=NEW.submission_id)
|
|
215
|
+
AND NOT EXISTS (SELECT 1 FROM room_floor WHERE room_id=NEW.room_id
|
|
216
|
+
AND holder=COALESCE((SELECT host FROM topic_settings WHERE room_id=NEW.room_id),'用户'))
|
|
217
|
+
BEGIN
|
|
218
|
+
SELECT RAISE(ABORT, 'floor_required');
|
|
219
|
+
END;
|
|
220
|
+
DROP TRIGGER IF EXISTS nominate_expert_floor;
|
|
221
|
+
CREATE TRIGGER nominate_expert_floor AFTER INSERT ON discussion_turns
|
|
222
|
+
WHEN NEW.status<>'queued'
|
|
223
|
+
BEGIN
|
|
224
|
+
UPDATE room_floor SET holder=NULL,next_speaker=NEW.expert,revision=revision+1 WHERE room_id=NEW.room_id;
|
|
225
|
+
END;
|
|
226
|
+
CREATE TRIGGER IF NOT EXISTS cancel_turn_floor AFTER UPDATE OF status ON discussion_turns
|
|
227
|
+
WHEN NEW.status='cancelled' AND OLD.status<>NEW.status
|
|
228
|
+
BEGIN
|
|
229
|
+
UPDATE room_floor SET holder=NULL,
|
|
230
|
+
next_speaker=COALESCE((SELECT host FROM topic_settings WHERE room_id=NEW.room_id),'用户'),
|
|
231
|
+
revision=revision+1 WHERE room_id=NEW.room_id;
|
|
232
|
+
END;
|
|
233
|
+
|
|
234
|
+
-- Runtime control is opt-in. Legacy rooms have no runtime row and keep manual entry.
|
|
235
|
+
CREATE TABLE IF NOT EXISTS discussion_limits (
|
|
236
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
|
|
237
|
+
max_rounds INTEGER NOT NULL DEFAULT 0,
|
|
238
|
+
max_minutes INTEGER NOT NULL DEFAULT 0,
|
|
239
|
+
execution_minutes INTEGER NOT NULL DEFAULT 0,
|
|
240
|
+
on_limit TEXT NOT NULL DEFAULT 'pause',
|
|
241
|
+
summary_minutes INTEGER NOT NULL DEFAULT 5,
|
|
242
|
+
started_at INTEGER NOT NULL DEFAULT 0,
|
|
243
|
+
closing_at INTEGER NOT NULL DEFAULT 0,
|
|
244
|
+
stop_reason TEXT NOT NULL DEFAULT '',
|
|
245
|
+
revision INTEGER NOT NULL DEFAULT 0
|
|
246
|
+
);
|
|
247
|
+
INSERT OR IGNORE INTO discussion_limits(room_id,execution_minutes) SELECT id,0 FROM rooms;
|
|
248
|
+
DROP TRIGGER IF EXISTS limits_room_created;
|
|
249
|
+
CREATE TRIGGER limits_room_created AFTER INSERT ON rooms
|
|
250
|
+
BEGIN
|
|
251
|
+
INSERT INTO discussion_limits(room_id,execution_minutes) VALUES (NEW.id,0);
|
|
252
|
+
END;
|
|
253
|
+
CREATE TRIGGER IF NOT EXISTS limits_topic_started AFTER INSERT ON topic_settings
|
|
254
|
+
WHEN NEW.phase IN ('initial','discussing','reviewing')
|
|
255
|
+
BEGIN
|
|
256
|
+
UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
|
|
257
|
+
END;
|
|
258
|
+
CREATE TRIGGER IF NOT EXISTS limits_phase_started AFTER UPDATE OF phase ON topic_settings
|
|
259
|
+
WHEN NEW.phase IN ('initial','discussing','reviewing') AND NEW.phase<>OLD.phase
|
|
260
|
+
BEGIN
|
|
261
|
+
UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
|
|
262
|
+
END;
|
|
263
|
+
CREATE TRIGGER IF NOT EXISTS limits_turn_started AFTER INSERT ON discussion_turns
|
|
264
|
+
BEGIN
|
|
265
|
+
UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
|
|
266
|
+
END;
|
|
267
|
+
CREATE TABLE IF NOT EXISTS discussion_runtime (
|
|
268
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
|
|
269
|
+
mode TEXT NOT NULL CHECK(mode IN ('manual','automatic')),
|
|
270
|
+
run_id TEXT NOT NULL,
|
|
271
|
+
generation INTEGER NOT NULL DEFAULT 1,
|
|
272
|
+
status TEXT NOT NULL CHECK(status IN ('ready','running','paused','stopped','concluded')),
|
|
273
|
+
control_hash TEXT NOT NULL,
|
|
274
|
+
deadline INTEGER NOT NULL DEFAULT 0, -- 0 means no run duration limit; execution leases remain bounded.
|
|
275
|
+
revision INTEGER NOT NULL DEFAULT 1,
|
|
276
|
+
resume_phase TEXT NOT NULL DEFAULT 'initial',
|
|
277
|
+
updated_at TEXT NOT NULL
|
|
278
|
+
);
|
|
279
|
+
CREATE TABLE IF NOT EXISTS discussion_events (
|
|
280
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
281
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
282
|
+
run_id TEXT NOT NULL,
|
|
283
|
+
generation INTEGER NOT NULL,
|
|
284
|
+
cause TEXT NOT NULL,
|
|
285
|
+
participant TEXT NOT NULL,
|
|
286
|
+
role TEXT NOT NULL CHECK(role IN ('host','expert')),
|
|
287
|
+
turn_id INTEGER,
|
|
288
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
289
|
+
CHECK(status IN ('pending','dispatched','started','completed','cancelled','failed')),
|
|
290
|
+
token_hash TEXT NOT NULL DEFAULT '',
|
|
291
|
+
session_id TEXT NOT NULL DEFAULT '',
|
|
292
|
+
lease_until INTEGER NOT NULL DEFAULT 0,
|
|
293
|
+
started_at INTEGER NOT NULL DEFAULT 0,
|
|
294
|
+
result TEXT NOT NULL DEFAULT '',
|
|
295
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
|
296
|
+
UNIQUE(room_id,run_id,generation,cause)
|
|
297
|
+
);
|
|
298
|
+
CREATE INDEX IF NOT EXISTS events_room_cursor ON discussion_events(room_id,id);
|
|
299
|
+
CREATE UNIQUE INDEX IF NOT EXISTS one_active_execution ON discussion_events(room_id)
|
|
300
|
+
WHERE status IN ('dispatched','started');
|
|
301
|
+
-- Stage timestamps for the progress panel; old events simply have no timing row.
|
|
302
|
+
CREATE TABLE IF NOT EXISTS event_timings (
|
|
303
|
+
event_id INTEGER PRIMARY KEY REFERENCES discussion_events(id) ON DELETE CASCADE,
|
|
304
|
+
dispatched_at INTEGER NOT NULL DEFAULT 0,
|
|
305
|
+
finished_at INTEGER NOT NULL DEFAULT 0
|
|
306
|
+
);
|
|
307
|
+
-- Freeze dispatch-to-publication time independently of the recent execution list.
|
|
308
|
+
CREATE TABLE IF NOT EXISTS message_timings (
|
|
309
|
+
message_id INTEGER PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
|
|
310
|
+
elapsed_seconds INTEGER NOT NULL CHECK(elapsed_seconds>=0)
|
|
311
|
+
);
|
|
312
|
+
-- Only completed expert events identify a historical message unambiguously.
|
|
313
|
+
INSERT INTO message_timings(message_id,elapsed_seconds)
|
|
314
|
+
SELECT m.id,MAX(0,CAST(strftime('%s',m.created_at) AS INTEGER)-t.dispatched_at)
|
|
315
|
+
FROM messages AS m JOIN discussion_events AS e
|
|
316
|
+
ON e.room_id=m.room_id AND e.result='message:'||m.id AND e.participant=m.author
|
|
317
|
+
JOIN event_timings AS t ON t.event_id=e.id
|
|
318
|
+
WHERE m.source IN ('cli','api') AND e.role='expert' AND e.status='completed'
|
|
319
|
+
AND e.started_at>0 AND t.dispatched_at>0 AND t.finished_at>0
|
|
320
|
+
ON CONFLICT(message_id) DO NOTHING;
|
|
321
|
+
CREATE TRIGGER IF NOT EXISTS record_event_timing AFTER UPDATE OF status ON discussion_events
|
|
322
|
+
WHEN OLD.status<>NEW.status
|
|
323
|
+
BEGIN
|
|
324
|
+
-- Avoid OR IGNORE: an outer statement's conflict policy would override it.
|
|
325
|
+
INSERT INTO event_timings(event_id) SELECT NEW.id
|
|
326
|
+
WHERE NOT EXISTS(SELECT 1 FROM event_timings WHERE event_id=NEW.id);
|
|
327
|
+
UPDATE event_timings SET dispatched_at=CAST(strftime('%s','now') AS INTEGER)
|
|
328
|
+
WHERE event_id=NEW.id AND NEW.status='dispatched';
|
|
329
|
+
UPDATE event_timings SET finished_at=CAST(strftime('%s','now') AS INTEGER)
|
|
330
|
+
WHERE event_id=NEW.id AND NEW.status IN ('completed','cancelled','failed');
|
|
331
|
+
END;
|
|
332
|
+
-- A failed assertion rolls back the whole D1 batch, including concurrent writes.
|
|
333
|
+
CREATE TABLE IF NOT EXISTS runtime_assertions (
|
|
334
|
+
id TEXT PRIMARY KEY, allowed INTEGER NOT NULL CHECK(allowed=1)
|
|
335
|
+
);
|
|
336
|
+
CREATE TRIGGER IF NOT EXISTS runtime_generation AFTER UPDATE ON discussion_runtime
|
|
337
|
+
WHEN OLD.generation<>NEW.generation OR OLD.status<>NEW.status
|
|
338
|
+
BEGIN
|
|
339
|
+
UPDATE discussion_events SET status='cancelled' WHERE room_id=NEW.room_id
|
|
340
|
+
AND status IN ('pending','dispatched','started')
|
|
341
|
+
AND (generation<>NEW.generation OR NEW.status<>'running');
|
|
342
|
+
INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
|
|
343
|
+
SELECT NEW.room_id,NEW.run_id,NEW.generation,'start:'||NEW.generation,
|
|
344
|
+
COALESCE(turns.expert,settings.host),CASE WHEN turns.id IS NULL THEN 'host' ELSE 'expert' END,turns.id
|
|
345
|
+
FROM topic_settings AS settings LEFT JOIN discussion_turns AS turns
|
|
346
|
+
ON turns.room_id=settings.room_id AND turns.status IN ('pending','notified')
|
|
347
|
+
WHERE settings.room_id=NEW.room_id AND NEW.status='running';
|
|
348
|
+
END;
|
|
349
|
+
DROP TRIGGER IF EXISTS runtime_turn_created;
|
|
350
|
+
CREATE TRIGGER runtime_turn_created AFTER INSERT ON discussion_turns
|
|
351
|
+
WHEN NEW.status<>'queued'
|
|
352
|
+
BEGIN
|
|
353
|
+
UPDATE discussion_events SET status='completed',result='turn:'||NEW.id
|
|
354
|
+
WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started');
|
|
355
|
+
INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
|
|
356
|
+
SELECT NEW.room_id,run_id,generation,'turn:'||NEW.id,NEW.expert,'expert',NEW.id
|
|
357
|
+
FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running';
|
|
358
|
+
END;
|
|
359
|
+
-- Round plans: queued turns wait in id order. held_since marks the first unprocessed user action
|
|
360
|
+
-- (a message id, or the next id for a topic edit); while set, answers return to the host instead of relaying.
|
|
361
|
+
CREATE TABLE IF NOT EXISTS turn_relay (
|
|
362
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
|
|
363
|
+
held_since INTEGER NOT NULL DEFAULT 0
|
|
364
|
+
);
|
|
365
|
+
-- Both triggers below clear the hold only when no user message arrived after the running host execution began; the host cannot have read it.
|
|
366
|
+
DROP TRIGGER IF EXISTS relay_turn_promoted;
|
|
367
|
+
CREATE TRIGGER relay_turn_promoted AFTER UPDATE OF status ON discussion_turns
|
|
368
|
+
WHEN OLD.status='queued' AND NEW.status='pending'
|
|
369
|
+
BEGIN
|
|
370
|
+
UPDATE room_floor SET holder=NULL,next_speaker=NEW.expert,revision=revision+1 WHERE room_id=NEW.room_id;
|
|
371
|
+
UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id
|
|
372
|
+
AND NOT EXISTS(SELECT 1 FROM messages m JOIN discussion_events e ON e.room_id=m.room_id AND e.role='host' AND e.status='started'
|
|
373
|
+
WHERE m.room_id=NEW.room_id AND m.author='用户' AND CAST(strftime('%s',m.created_at) AS INTEGER)>e.started_at);
|
|
374
|
+
UPDATE discussion_events SET status='completed',result='turn:'||NEW.id
|
|
375
|
+
WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started');
|
|
376
|
+
INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
|
|
377
|
+
SELECT NEW.room_id,run_id,generation,'turn:'||NEW.id,NEW.expert,'expert',NEW.id
|
|
378
|
+
FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running';
|
|
379
|
+
END;
|
|
380
|
+
-- A new arrangement is the host's response: it replaces any older plan and clears the hold.
|
|
381
|
+
DROP TRIGGER IF EXISTS relay_plan_replaced;
|
|
382
|
+
CREATE TRIGGER relay_plan_replaced AFTER INSERT ON discussion_turns
|
|
383
|
+
WHEN NEW.status<>'queued'
|
|
384
|
+
BEGIN
|
|
385
|
+
UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued' AND id<NEW.id;
|
|
386
|
+
UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id
|
|
387
|
+
AND NOT EXISTS(SELECT 1 FROM messages m JOIN discussion_events e ON e.room_id=m.room_id AND e.role='host' AND e.status='started'
|
|
388
|
+
WHERE m.room_id=NEW.room_id AND m.author='用户' AND CAST(strftime('%s',m.created_at) AS INTEGER)>e.started_at);
|
|
389
|
+
END;
|
|
390
|
+
CREATE TRIGGER IF NOT EXISTS relay_hold_user_message AFTER INSERT ON message_details
|
|
391
|
+
WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
|
|
392
|
+
BEGIN
|
|
393
|
+
INSERT INTO turn_relay(room_id) SELECT room_id FROM messages WHERE id=NEW.message_id
|
|
394
|
+
AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE turn_relay.room_id=messages.room_id);
|
|
395
|
+
UPDATE turn_relay SET held_since=NEW.message_id
|
|
396
|
+
WHERE room_id=(SELECT room_id FROM messages WHERE id=NEW.message_id) AND held_since=0;
|
|
397
|
+
END;
|
|
398
|
+
CREATE TRIGGER IF NOT EXISTS relay_hold_goal AFTER UPDATE OF goal ON rooms
|
|
399
|
+
WHEN NEW.goal<>OLD.goal
|
|
400
|
+
BEGIN
|
|
401
|
+
INSERT INTO turn_relay(room_id) SELECT NEW.id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.id);
|
|
402
|
+
UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.id)
|
|
403
|
+
WHERE room_id=NEW.id AND held_since=0;
|
|
404
|
+
END;
|
|
405
|
+
CREATE TRIGGER IF NOT EXISTS relay_hold_topic AFTER UPDATE OF background,boundaries,focus ON topic_settings
|
|
406
|
+
WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries OR NEW.focus<>OLD.focus
|
|
407
|
+
BEGIN
|
|
408
|
+
INSERT INTO turn_relay(room_id) SELECT NEW.room_id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id);
|
|
409
|
+
UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.room_id)
|
|
410
|
+
WHERE room_id=NEW.room_id AND held_since=0;
|
|
411
|
+
END;
|
|
412
|
+
-- Concluding, replacing the host or removing/changing an expert voids plans that have not started.
|
|
413
|
+
-- Adding experts preserves the current plan; the invitation transaction appends their opening turns.
|
|
414
|
+
CREATE TRIGGER IF NOT EXISTS relay_plan_concluded AFTER UPDATE OF phase ON topic_settings
|
|
415
|
+
WHEN NEW.phase='concluded'
|
|
416
|
+
BEGIN
|
|
417
|
+
UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
|
|
418
|
+
END;
|
|
419
|
+
DROP TRIGGER IF EXISTS relay_plan_participants;
|
|
420
|
+
CREATE TRIGGER relay_plan_participants AFTER UPDATE OF host,experts ON topic_settings
|
|
421
|
+
WHEN NEW.host<>OLD.host OR EXISTS (
|
|
422
|
+
SELECT 1 FROM json_each(OLD.experts) AS previous
|
|
423
|
+
WHERE NOT EXISTS (SELECT 1 FROM json_each(NEW.experts) AS current
|
|
424
|
+
WHERE current.key=previous.key AND current.value=previous.value)
|
|
425
|
+
)
|
|
426
|
+
BEGIN
|
|
427
|
+
UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
|
|
428
|
+
END;
|
|
429
|
+
DROP TRIGGER IF EXISTS runtime_turn_answered;
|
|
430
|
+
-- An answer relays to the next queued turn unless the user has acted since the host last arranged.
|
|
431
|
+
CREATE TRIGGER runtime_turn_answered AFTER UPDATE OF status ON discussion_turns
|
|
432
|
+
WHEN NEW.status='answered' AND OLD.status<>'answered'
|
|
433
|
+
BEGIN
|
|
434
|
+
UPDATE discussion_events SET status='completed',result='message:'||NEW.answer_id
|
|
435
|
+
WHERE room_id=NEW.room_id AND turn_id=NEW.id AND status IN ('pending','dispatched','started');
|
|
436
|
+
UPDATE discussion_turns SET status='pending'
|
|
437
|
+
WHERE id=(SELECT MIN(id) FROM discussion_turns WHERE room_id=NEW.room_id AND status='queued')
|
|
438
|
+
AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id AND held_since>0)
|
|
439
|
+
AND NOT EXISTS(SELECT 1 FROM topic_settings WHERE room_id=NEW.room_id AND phase IN ('paused','concluded'));
|
|
440
|
+
INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
|
|
441
|
+
SELECT NEW.room_id,run_id,generation,'answer:'||NEW.id,host,'host'
|
|
442
|
+
FROM discussion_runtime JOIN topic_settings USING(room_id)
|
|
443
|
+
WHERE room_id=NEW.room_id AND status='running'
|
|
444
|
+
AND NOT EXISTS(SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'));
|
|
445
|
+
END;
|
|
446
|
+
DROP TRIGGER IF EXISTS runtime_topic_stop;
|
|
447
|
+
CREATE TRIGGER runtime_topic_stop AFTER UPDATE OF phase ON topic_settings
|
|
448
|
+
WHEN NEW.phase IN ('paused','concluded') AND OLD.phase<>NEW.phase
|
|
449
|
+
BEGIN
|
|
450
|
+
UPDATE discussion_events SET status='completed',result='concluded' WHERE room_id=NEW.room_id
|
|
451
|
+
AND role='host' AND status='started' AND NEW.phase='concluded';
|
|
452
|
+
UPDATE discussion_runtime SET status=NEW.phase,generation=generation+1,revision=revision+1,
|
|
453
|
+
resume_phase=CASE WHEN OLD.phase IN ('initial','discussing','reviewing') THEN OLD.phase ELSE resume_phase END,
|
|
454
|
+
updated_at=strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE room_id=NEW.room_id;
|
|
455
|
+
END;
|
|
456
|
+
CREATE TRIGGER IF NOT EXISTS runtime_user_message AFTER INSERT ON message_details
|
|
457
|
+
WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
|
|
458
|
+
BEGIN
|
|
459
|
+
INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
|
|
460
|
+
SELECT messages.room_id,run_id,generation,'user:'||NEW.message_id,host,'host'
|
|
461
|
+
FROM messages JOIN discussion_runtime ON discussion_runtime.room_id=messages.room_id
|
|
462
|
+
JOIN topic_settings ON topic_settings.room_id=messages.room_id
|
|
463
|
+
WHERE messages.id=NEW.message_id AND status='running';
|
|
464
|
+
END;
|
|
465
|
+
CREATE TRIGGER IF NOT EXISTS runtime_turn_cancelled AFTER UPDATE OF status ON discussion_turns
|
|
466
|
+
WHEN NEW.status='cancelled' AND OLD.status<>'cancelled'
|
|
467
|
+
BEGIN
|
|
468
|
+
UPDATE discussion_events SET status='cancelled' WHERE room_id=NEW.room_id AND turn_id=NEW.id
|
|
469
|
+
AND status IN ('pending','dispatched','started');
|
|
470
|
+
INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
|
|
471
|
+
SELECT NEW.room_id,run_id,generation,'cancel:'||NEW.id,host,'host'
|
|
472
|
+
FROM discussion_runtime JOIN topic_settings USING(room_id)
|
|
473
|
+
WHERE room_id=NEW.room_id AND status='running'
|
|
474
|
+
AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
|
|
475
|
+
END;
|
|
476
|
+
CREATE TRIGGER IF NOT EXISTS runtime_archive AFTER UPDATE OF archived ON room_directory
|
|
477
|
+
WHEN NEW.archived=1 AND OLD.archived<>NEW.archived
|
|
478
|
+
BEGIN
|
|
479
|
+
UPDATE discussion_runtime SET status='stopped',generation=generation+1,revision=revision+1
|
|
480
|
+
WHERE room_id=NEW.room_id;
|
|
481
|
+
END;
|
|
482
|
+
DROP TRIGGER IF EXISTS runtime_participants_changed;
|
|
483
|
+
-- A new host needs a new run. Experts joining or leaving keep the run going; the host is woken to re-plan.
|
|
484
|
+
CREATE TRIGGER runtime_participants_changed AFTER UPDATE OF host ON topic_settings
|
|
485
|
+
WHEN NEW.host<>OLD.host
|
|
486
|
+
BEGIN
|
|
487
|
+
UPDATE discussion_runtime SET status='paused',generation=generation+1,revision=revision+1
|
|
488
|
+
WHERE room_id=NEW.room_id AND status='running';
|
|
489
|
+
END;
|
|
490
|
+
DROP TRIGGER IF EXISTS runtime_experts_changed;
|
|
491
|
+
CREATE TRIGGER runtime_experts_changed AFTER UPDATE OF experts ON topic_settings
|
|
492
|
+
WHEN NEW.host=OLD.host AND NEW.experts<>OLD.experts
|
|
493
|
+
BEGIN
|
|
494
|
+
INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
|
|
495
|
+
SELECT NEW.room_id,run_id,generation,'members:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),NEW.host,'host'
|
|
496
|
+
FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running'
|
|
497
|
+
AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started'))
|
|
498
|
+
AND NOT EXISTS(SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'));
|
|
499
|
+
END;
|
|
500
|
+
CREATE TRIGGER IF NOT EXISTS runtime_goal_changed AFTER UPDATE OF goal ON rooms
|
|
501
|
+
WHEN NEW.goal<>OLD.goal
|
|
502
|
+
BEGIN
|
|
503
|
+
INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
|
|
504
|
+
SELECT NEW.id,run_id,generation,'goal:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),host,'host'
|
|
505
|
+
FROM discussion_runtime JOIN topic_settings USING(room_id)
|
|
506
|
+
WHERE room_id=NEW.id AND status='running'
|
|
507
|
+
AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.id AND role='host' AND status='started');
|
|
508
|
+
END;
|
|
509
|
+
CREATE TRIGGER IF NOT EXISTS runtime_constraints_changed AFTER UPDATE OF background,boundaries ON topic_settings
|
|
510
|
+
WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries
|
|
511
|
+
BEGIN
|
|
512
|
+
INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
|
|
513
|
+
SELECT NEW.room_id,run_id,generation,'constraints:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),NEW.host,'host'
|
|
514
|
+
FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running'
|
|
515
|
+
AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
|
|
516
|
+
END;
|
|
517
|
+
-- Paused, preparing or concluded time is idle: it is excluded from elapsed time and total-duration deadlines.
|
|
518
|
+
CREATE TABLE IF NOT EXISTS discussion_pauses (
|
|
519
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
|
|
520
|
+
paused_at INTEGER NOT NULL DEFAULT 0,
|
|
521
|
+
paused_seconds INTEGER NOT NULL DEFAULT 0
|
|
522
|
+
);
|
|
523
|
+
INSERT INTO discussion_pauses(room_id,paused_at)
|
|
524
|
+
SELECT room_id,CASE WHEN phase IN ('paused','concluded') THEN COALESCE((SELECT CAST(strftime('%s',updated_at) AS INTEGER)
|
|
525
|
+
FROM discussion_runtime WHERE discussion_runtime.room_id=topic_settings.room_id AND status<>'running'),
|
|
526
|
+
CAST(strftime('%s','now') AS INTEGER)) ELSE 0 END
|
|
527
|
+
FROM topic_settings WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE discussion_pauses.room_id=topic_settings.room_id);
|
|
528
|
+
CREATE TRIGGER IF NOT EXISTS pause_clock_stopped AFTER UPDATE OF phase ON topic_settings
|
|
529
|
+
WHEN OLD.phase IN ('initial','discussing','reviewing') AND NEW.phase NOT IN ('initial','discussing','reviewing')
|
|
530
|
+
BEGIN
|
|
531
|
+
INSERT INTO discussion_pauses(room_id) SELECT NEW.room_id
|
|
532
|
+
WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE room_id=NEW.room_id);
|
|
533
|
+
UPDATE discussion_pauses SET paused_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND paused_at=0;
|
|
534
|
+
END;
|
|
535
|
+
CREATE TRIGGER IF NOT EXISTS pause_clock_resumed AFTER UPDATE OF phase ON topic_settings
|
|
536
|
+
WHEN NEW.phase IN ('initial','discussing','reviewing') AND OLD.phase NOT IN ('initial','discussing','reviewing')
|
|
537
|
+
BEGIN
|
|
538
|
+
UPDATE discussion_pauses SET paused_seconds=paused_seconds+MAX(0,CAST(strftime('%s','now') AS INTEGER)-paused_at),paused_at=0
|
|
539
|
+
WHERE room_id=NEW.room_id AND paused_at>0;
|
|
540
|
+
END;
|
|
541
|
+
-- Both room kinds register members; free rooms additionally persist requests and executions.
|
|
542
|
+
CREATE TABLE IF NOT EXISTS room_members (
|
|
543
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
544
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
545
|
+
name TEXT NOT NULL,
|
|
546
|
+
adapter TEXT NOT NULL DEFAULT '',
|
|
547
|
+
role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('member','host')),
|
|
548
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','away','removed')),
|
|
549
|
+
token_hash TEXT NOT NULL,
|
|
550
|
+
read_until INTEGER NOT NULL DEFAULT 0,
|
|
551
|
+
created_at TEXT NOT NULL,
|
|
552
|
+
access TEXT NOT NULL DEFAULT 'runner',
|
|
553
|
+
session_mode TEXT NOT NULL DEFAULT 'new'
|
|
554
|
+
);
|
|
555
|
+
CREATE UNIQUE INDEX IF NOT EXISTS member_name ON room_members(room_id,name) WHERE status<>'removed';
|
|
556
|
+
CREATE UNIQUE INDEX IF NOT EXISTS member_credential ON room_members(room_id,token_hash);
|
|
557
|
+
-- A manual member's own software session, reported by its CLI on posting; the local service uses it to take the member over.
|
|
558
|
+
CREATE TABLE IF NOT EXISTS member_sessions (
|
|
559
|
+
member_id INTEGER PRIMARY KEY REFERENCES room_members(id) ON DELETE CASCADE,
|
|
560
|
+
room_id TEXT NOT NULL,
|
|
561
|
+
session_id TEXT NOT NULL,
|
|
562
|
+
client TEXT NOT NULL
|
|
563
|
+
);
|
|
564
|
+
-- A manual member's own registration; it is bound to the credential it used, so a re-issued credential must register again.
|
|
565
|
+
CREATE TABLE IF NOT EXISTS member_registrations (
|
|
566
|
+
member_id INTEGER PRIMARY KEY REFERENCES room_members(id) ON DELETE CASCADE,
|
|
567
|
+
room_id TEXT NOT NULL,
|
|
568
|
+
token_hash TEXT NOT NULL,
|
|
569
|
+
registered_at TEXT NOT NULL
|
|
570
|
+
);
|
|
571
|
+
CREATE TABLE IF NOT EXISTS member_executions (
|
|
572
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
573
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
574
|
+
member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
|
|
575
|
+
client_id TEXT NOT NULL,
|
|
576
|
+
status TEXT NOT NULL DEFAULT 'dispatched'
|
|
577
|
+
CHECK(status IN ('dispatched','started','completed','cancelled','failed')),
|
|
578
|
+
session_id TEXT NOT NULL DEFAULT '',
|
|
579
|
+
lease_until INTEGER NOT NULL DEFAULT 0,
|
|
580
|
+
created_at INTEGER NOT NULL,
|
|
581
|
+
finished_at INTEGER NOT NULL DEFAULT 0,
|
|
582
|
+
result TEXT NOT NULL DEFAULT '',
|
|
583
|
+
changes TEXT NOT NULL DEFAULT '',
|
|
584
|
+
cwd TEXT NOT NULL DEFAULT '',
|
|
585
|
+
UNIQUE(room_id,client_id)
|
|
586
|
+
);
|
|
587
|
+
-- One live execution per member: different members may run in parallel, one session never twice.
|
|
588
|
+
CREATE UNIQUE INDEX IF NOT EXISTS one_member_execution ON member_executions(member_id)
|
|
589
|
+
WHERE status IN ('dispatched','started');
|
|
590
|
+
CREATE TABLE IF NOT EXISTS member_requests (
|
|
591
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
592
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
593
|
+
member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
|
|
594
|
+
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
595
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
596
|
+
CHECK(status IN ('pending','delivering','completed','cancelled','failed')),
|
|
597
|
+
execution_id INTEGER REFERENCES member_executions(id),
|
|
598
|
+
answer_id INTEGER REFERENCES messages(id),
|
|
599
|
+
created_at INTEGER NOT NULL,
|
|
600
|
+
chain_id INTEGER NOT NULL DEFAULT 0,
|
|
601
|
+
batch_id INTEGER NOT NULL DEFAULT 0,
|
|
602
|
+
purpose TEXT NOT NULL DEFAULT 'ask',
|
|
603
|
+
created_by TEXT NOT NULL DEFAULT 'user',
|
|
604
|
+
UNIQUE(message_id,member_id)
|
|
605
|
+
);
|
|
606
|
+
CREATE INDEX IF NOT EXISTS member_requests_open ON member_requests(member_id,status);
|
|
607
|
+
-- Discussion options chosen at creation. A room without a row predates the option and keeps named follow-ups.
|
|
608
|
+
CREATE TABLE IF NOT EXISTS discussion_options (
|
|
609
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
|
|
610
|
+
named_followup INTEGER NOT NULL DEFAULT 0 CHECK(named_followup IN (0,1))
|
|
611
|
+
);
|
|
612
|
+
-- Visible record of each smart-scheduling decision in a discussion; it is not a message and moves no round or floor.
|
|
613
|
+
CREATE TABLE IF NOT EXISTS scheduler_decisions (
|
|
614
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
615
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
616
|
+
after_message_id INTEGER NOT NULL DEFAULT 0,
|
|
617
|
+
kind TEXT NOT NULL CHECK(kind IN ('interjection','round')),
|
|
618
|
+
label TEXT NOT NULL,
|
|
619
|
+
confidence REAL,
|
|
620
|
+
applied INTEGER NOT NULL DEFAULT 0 CHECK(applied IN (0,1)),
|
|
621
|
+
provider TEXT NOT NULL,
|
|
622
|
+
created_at TEXT NOT NULL
|
|
623
|
+
);
|
|
624
|
+
CREATE INDEX IF NOT EXISTS scheduler_decisions_room ON scheduler_decisions(room_id,id);
|
|
625
|
+
-- What the decision model was asked and chose, shown folded under the decision; not part of backups.
|
|
626
|
+
CREATE TABLE IF NOT EXISTS scheduler_decision_details (
|
|
627
|
+
decision_id INTEGER PRIMARY KEY REFERENCES scheduler_decisions(id) ON DELETE CASCADE,
|
|
628
|
+
detail TEXT NOT NULL
|
|
629
|
+
);
|
|
630
|
+
-- Smart scheduling's suggestion that another member answer an unnamed message. The host keeps the message;
|
|
631
|
+
-- the user may adopt before expires_at (waiting until the host's request ends for mode 'after'), otherwise it lapses.
|
|
632
|
+
CREATE TABLE IF NOT EXISTS member_recommendations (
|
|
633
|
+
message_id INTEGER PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
|
|
634
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
635
|
+
member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
|
|
636
|
+
provider TEXT NOT NULL,
|
|
637
|
+
confidence REAL NOT NULL,
|
|
638
|
+
created_at INTEGER NOT NULL,
|
|
639
|
+
expires_at INTEGER NOT NULL,
|
|
640
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','waiting','adopted')),
|
|
641
|
+
mode TEXT NOT NULL DEFAULT ''
|
|
642
|
+
);
|
|
643
|
+
-- Ordinary members can suggest a response without creating work or spending a chain budget.
|
|
644
|
+
CREATE TABLE IF NOT EXISTS member_suggestions (
|
|
645
|
+
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
646
|
+
member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
|
|
647
|
+
PRIMARY KEY (message_id,member_id)
|
|
648
|
+
);
|
|
649
|
+
-- A host's dispatch: its named requests form one batch that hands back to the host once.
|
|
650
|
+
CREATE TABLE IF NOT EXISTS free_batches (
|
|
651
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
652
|
+
batch_id INTEGER NOT NULL,
|
|
653
|
+
chain_id INTEGER NOT NULL,
|
|
654
|
+
host_id INTEGER NOT NULL,
|
|
655
|
+
settled INTEGER NOT NULL DEFAULT 0,
|
|
656
|
+
PRIMARY KEY (room_id,batch_id)
|
|
657
|
+
);
|
|
658
|
+
-- Chains whose automatic progress stopped (budget reached or every dispatched member failed).
|
|
659
|
+
CREATE TABLE IF NOT EXISTS free_chains (
|
|
660
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
661
|
+
chain_id INTEGER NOT NULL,
|
|
662
|
+
reason TEXT NOT NULL,
|
|
663
|
+
PRIMARY KEY (room_id,chain_id)
|
|
664
|
+
);
|
|
665
|
+
-- A brainstorm room started from a free room keeps its origin for the way back.
|
|
666
|
+
CREATE TABLE IF NOT EXISTS room_links (
|
|
667
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
|
|
668
|
+
parent_id TEXT NOT NULL,
|
|
669
|
+
created_at TEXT NOT NULL
|
|
670
|
+
);
|