ai_co_discuss 0.2.41 → 0.2.42

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.
@@ -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
- -- Free collaboration rooms. Rooms without a row are brainstorm rooms and keep every rule below.
167
- CREATE TABLE IF NOT EXISTS room_modes (
168
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
169
- mode TEXT NOT NULL CHECK(mode IN ('free','brainstorm')),
170
- control_hash TEXT NOT NULL DEFAULT ''
171
- );
172
- -- Replaced on every start so existing databases drop the floor rule for free rooms.
173
- DROP TRIGGER IF EXISTS require_message_floor;
174
- CREATE TRIGGER require_message_floor BEFORE INSERT ON messages
175
- WHEN NEW.author<>'用户'
176
- AND NOT EXISTS (SELECT 1 FROM messages WHERE room_id=NEW.room_id AND submission_id=NEW.submission_id)
177
- AND NOT EXISTS (SELECT 1 FROM room_floor WHERE room_id=NEW.room_id AND holder=NEW.author)
178
- AND NOT EXISTS (SELECT 1 FROM room_modes WHERE room_id=NEW.room_id AND mode='free')
179
- BEGIN
180
- SELECT RAISE(ABORT, 'floor_required');
181
- END;
182
- CREATE TRIGGER IF NOT EXISTS require_opinion_turn BEFORE INSERT ON message_details
183
- WHEN NEW.kind='opinion' AND NEW.turn_id IS NULL
184
- AND NOT EXISTS (SELECT 1 FROM message_details WHERE message_id=NEW.message_id)
185
- AND EXISTS (SELECT 1 FROM messages WHERE id=NEW.message_id AND author<>'用户')
186
- BEGIN
187
- SELECT RAISE(ABORT, 'floor_turn_required');
188
- END;
189
- CREATE TRIGGER IF NOT EXISTS return_message_floor AFTER INSERT ON message_details
190
- WHEN EXISTS (SELECT 1 FROM messages WHERE id=NEW.message_id AND author<>'用户')
191
- BEGIN
192
- UPDATE room_floor SET holder=NULL,
193
- next_speaker=COALESCE((SELECT host FROM topic_settings WHERE room_id=room_floor.room_id),'用户'),
194
- revision=revision+1 WHERE room_id=(SELECT room_id FROM messages WHERE id=NEW.message_id);
195
- END;
196
- DROP TRIGGER IF EXISTS require_host_floor;
197
- -- Queued turns of a round plan are inserted after the first one has already nominated an expert.
198
- CREATE TRIGGER require_host_floor BEFORE INSERT ON discussion_turns
199
- WHEN NEW.status<>'queued' AND NOT EXISTS (SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND submission_id=NEW.submission_id)
200
- AND NOT EXISTS (SELECT 1 FROM room_floor WHERE room_id=NEW.room_id
201
- AND holder=COALESCE((SELECT host FROM topic_settings WHERE room_id=NEW.room_id),'用户'))
202
- BEGIN
203
- SELECT RAISE(ABORT, 'floor_required');
204
- END;
205
- DROP TRIGGER IF EXISTS nominate_expert_floor;
206
- CREATE TRIGGER nominate_expert_floor AFTER INSERT ON discussion_turns
207
- WHEN NEW.status<>'queued'
208
- BEGIN
209
- UPDATE room_floor SET holder=NULL,next_speaker=NEW.expert,revision=revision+1 WHERE room_id=NEW.room_id;
210
- END;
211
- CREATE TRIGGER IF NOT EXISTS cancel_turn_floor AFTER UPDATE OF status ON discussion_turns
212
- WHEN NEW.status='cancelled' AND OLD.status<>NEW.status
213
- BEGIN
214
- UPDATE room_floor SET holder=NULL,
215
- next_speaker=COALESCE((SELECT host FROM topic_settings WHERE room_id=NEW.room_id),'用户'),
216
- revision=revision+1 WHERE room_id=NEW.room_id;
217
- END;
218
-
219
- -- Runtime control is opt-in. Legacy rooms have no runtime row and keep manual entry.
220
- CREATE TABLE IF NOT EXISTS discussion_limits (
221
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
222
- max_rounds INTEGER NOT NULL DEFAULT 0,
223
- max_minutes INTEGER NOT NULL DEFAULT 0,
224
- execution_minutes INTEGER NOT NULL DEFAULT 0,
225
- on_limit TEXT NOT NULL DEFAULT 'pause',
226
- summary_minutes INTEGER NOT NULL DEFAULT 5,
227
- started_at INTEGER NOT NULL DEFAULT 0,
228
- closing_at INTEGER NOT NULL DEFAULT 0,
229
- stop_reason TEXT NOT NULL DEFAULT '',
230
- revision INTEGER NOT NULL DEFAULT 0
231
- );
232
- INSERT OR IGNORE INTO discussion_limits(room_id,execution_minutes) SELECT id,0 FROM rooms;
233
- DROP TRIGGER IF EXISTS limits_room_created;
234
- CREATE TRIGGER limits_room_created AFTER INSERT ON rooms
235
- BEGIN
236
- INSERT INTO discussion_limits(room_id,execution_minutes) VALUES (NEW.id,0);
237
- END;
238
- CREATE TRIGGER IF NOT EXISTS limits_topic_started AFTER INSERT ON topic_settings
239
- WHEN NEW.phase IN ('initial','discussing','reviewing')
240
- BEGIN
241
- UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
242
- END;
243
- CREATE TRIGGER IF NOT EXISTS limits_phase_started AFTER UPDATE OF phase ON topic_settings
244
- WHEN NEW.phase IN ('initial','discussing','reviewing') AND NEW.phase<>OLD.phase
245
- BEGIN
246
- UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
247
- END;
248
- CREATE TRIGGER IF NOT EXISTS limits_turn_started AFTER INSERT ON discussion_turns
249
- BEGIN
250
- UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
251
- END;
252
- CREATE TABLE IF NOT EXISTS discussion_runtime (
253
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
254
- mode TEXT NOT NULL CHECK(mode IN ('manual','automatic')),
255
- run_id TEXT NOT NULL,
256
- generation INTEGER NOT NULL DEFAULT 1,
257
- status TEXT NOT NULL CHECK(status IN ('ready','running','paused','stopped','concluded')),
258
- control_hash TEXT NOT NULL,
259
- deadline INTEGER NOT NULL DEFAULT 0, -- 0 means no run duration limit; execution leases remain bounded.
260
- revision INTEGER NOT NULL DEFAULT 1,
261
- resume_phase TEXT NOT NULL DEFAULT 'initial',
262
- updated_at TEXT NOT NULL
263
- );
264
- CREATE TABLE IF NOT EXISTS discussion_events (
265
- id INTEGER PRIMARY KEY AUTOINCREMENT,
266
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
267
- run_id TEXT NOT NULL,
268
- generation INTEGER NOT NULL,
269
- cause TEXT NOT NULL,
270
- participant TEXT NOT NULL,
271
- role TEXT NOT NULL CHECK(role IN ('host','expert')),
272
- turn_id INTEGER,
273
- status TEXT NOT NULL DEFAULT 'pending'
274
- CHECK(status IN ('pending','dispatched','started','completed','cancelled','failed')),
275
- token_hash TEXT NOT NULL DEFAULT '',
276
- session_id TEXT NOT NULL DEFAULT '',
277
- lease_until INTEGER NOT NULL DEFAULT 0,
278
- started_at INTEGER NOT NULL DEFAULT 0,
279
- result TEXT NOT NULL DEFAULT '',
280
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
281
- UNIQUE(room_id,run_id,generation,cause)
282
- );
283
- CREATE INDEX IF NOT EXISTS events_room_cursor ON discussion_events(room_id,id);
284
- CREATE UNIQUE INDEX IF NOT EXISTS one_active_execution ON discussion_events(room_id)
285
- WHERE status IN ('dispatched','started');
286
- -- Stage timestamps for the progress panel; old events simply have no timing row.
287
- CREATE TABLE IF NOT EXISTS event_timings (
288
- event_id INTEGER PRIMARY KEY REFERENCES discussion_events(id) ON DELETE CASCADE,
289
- dispatched_at INTEGER NOT NULL DEFAULT 0,
290
- finished_at INTEGER NOT NULL DEFAULT 0
291
- );
292
- -- Freeze dispatch-to-publication time independently of the recent execution list.
293
- CREATE TABLE IF NOT EXISTS message_timings (
294
- message_id INTEGER PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
295
- elapsed_seconds INTEGER NOT NULL CHECK(elapsed_seconds>=0)
296
- );
297
- -- Only completed expert events identify a historical message unambiguously.
298
- INSERT INTO message_timings(message_id,elapsed_seconds)
299
- SELECT m.id,MAX(0,CAST(strftime('%s',m.created_at) AS INTEGER)-t.dispatched_at)
300
- FROM messages AS m JOIN discussion_events AS e
301
- ON e.room_id=m.room_id AND e.result='message:'||m.id AND e.participant=m.author
302
- JOIN event_timings AS t ON t.event_id=e.id
303
- WHERE m.source IN ('cli','api') AND e.role='expert' AND e.status='completed'
304
- AND e.started_at>0 AND t.dispatched_at>0 AND t.finished_at>0
305
- ON CONFLICT(message_id) DO NOTHING;
306
- CREATE TRIGGER IF NOT EXISTS record_event_timing AFTER UPDATE OF status ON discussion_events
307
- WHEN OLD.status<>NEW.status
308
- BEGIN
309
- -- Avoid OR IGNORE: an outer statement's conflict policy would override it.
310
- INSERT INTO event_timings(event_id) SELECT NEW.id
311
- WHERE NOT EXISTS(SELECT 1 FROM event_timings WHERE event_id=NEW.id);
312
- UPDATE event_timings SET dispatched_at=CAST(strftime('%s','now') AS INTEGER)
313
- WHERE event_id=NEW.id AND NEW.status='dispatched';
314
- UPDATE event_timings SET finished_at=CAST(strftime('%s','now') AS INTEGER)
315
- WHERE event_id=NEW.id AND NEW.status IN ('completed','cancelled','failed');
316
- END;
317
- -- A failed assertion rolls back the whole D1 batch, including concurrent writes.
318
- CREATE TABLE IF NOT EXISTS runtime_assertions (
319
- id TEXT PRIMARY KEY, allowed INTEGER NOT NULL CHECK(allowed=1)
320
- );
321
- CREATE TRIGGER IF NOT EXISTS runtime_generation AFTER UPDATE ON discussion_runtime
322
- WHEN OLD.generation<>NEW.generation OR OLD.status<>NEW.status
323
- BEGIN
324
- UPDATE discussion_events SET status='cancelled' WHERE room_id=NEW.room_id
325
- AND status IN ('pending','dispatched','started')
326
- AND (generation<>NEW.generation OR NEW.status<>'running');
327
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
328
- SELECT NEW.room_id,NEW.run_id,NEW.generation,'start:'||NEW.generation,
329
- COALESCE(turns.expert,settings.host),CASE WHEN turns.id IS NULL THEN 'host' ELSE 'expert' END,turns.id
330
- FROM topic_settings AS settings LEFT JOIN discussion_turns AS turns
331
- ON turns.room_id=settings.room_id AND turns.status IN ('pending','notified')
332
- WHERE settings.room_id=NEW.room_id AND NEW.status='running';
333
- END;
334
- DROP TRIGGER IF EXISTS runtime_turn_created;
335
- CREATE TRIGGER runtime_turn_created AFTER INSERT ON discussion_turns
336
- WHEN NEW.status<>'queued'
337
- BEGIN
338
- UPDATE discussion_events SET status='completed',result='turn:'||NEW.id
339
- WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started');
340
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
341
- SELECT NEW.room_id,run_id,generation,'turn:'||NEW.id,NEW.expert,'expert',NEW.id
342
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running';
343
- END;
344
- -- Round plans: queued turns wait in id order. held_since marks the first unprocessed user action
345
- -- (a message id, or the next id for a topic edit); while set, answers return to the host instead of relaying.
346
- CREATE TABLE IF NOT EXISTS turn_relay (
347
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
348
- held_since INTEGER NOT NULL DEFAULT 0
349
- );
350
- -- Both triggers below clear the hold only when no user message arrived after the running host execution began; the host cannot have read it.
351
- DROP TRIGGER IF EXISTS relay_turn_promoted;
352
- CREATE TRIGGER relay_turn_promoted AFTER UPDATE OF status ON discussion_turns
353
- WHEN OLD.status='queued' AND NEW.status='pending'
354
- BEGIN
355
- UPDATE room_floor SET holder=NULL,next_speaker=NEW.expert,revision=revision+1 WHERE room_id=NEW.room_id;
356
- UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id
357
- 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'
358
- WHERE m.room_id=NEW.room_id AND m.author='用户' AND CAST(strftime('%s',m.created_at) AS INTEGER)>e.started_at);
359
- UPDATE discussion_events SET status='completed',result='turn:'||NEW.id
360
- WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started');
361
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
362
- SELECT NEW.room_id,run_id,generation,'turn:'||NEW.id,NEW.expert,'expert',NEW.id
363
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running';
364
- END;
365
- -- A new arrangement is the host's response: it replaces any older plan and clears the hold.
366
- DROP TRIGGER IF EXISTS relay_plan_replaced;
367
- CREATE TRIGGER relay_plan_replaced AFTER INSERT ON discussion_turns
368
- WHEN NEW.status<>'queued'
369
- BEGIN
370
- UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued' AND id<NEW.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
- END;
375
- CREATE TRIGGER IF NOT EXISTS relay_hold_user_message AFTER INSERT ON message_details
376
- WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
377
- BEGIN
378
- INSERT INTO turn_relay(room_id) SELECT room_id FROM messages WHERE id=NEW.message_id
379
- AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE turn_relay.room_id=messages.room_id);
380
- UPDATE turn_relay SET held_since=NEW.message_id
381
- WHERE room_id=(SELECT room_id FROM messages WHERE id=NEW.message_id) AND held_since=0;
382
- END;
383
- CREATE TRIGGER IF NOT EXISTS relay_hold_goal AFTER UPDATE OF goal ON rooms
384
- WHEN NEW.goal<>OLD.goal
385
- BEGIN
386
- INSERT INTO turn_relay(room_id) SELECT NEW.id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.id);
387
- UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.id)
388
- WHERE room_id=NEW.id AND held_since=0;
389
- END;
390
- CREATE TRIGGER IF NOT EXISTS relay_hold_topic AFTER UPDATE OF background,boundaries,focus ON topic_settings
391
- WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries OR NEW.focus<>OLD.focus
392
- BEGIN
393
- INSERT INTO turn_relay(room_id) SELECT NEW.room_id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id);
394
- UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.room_id)
395
- WHERE room_id=NEW.room_id AND held_since=0;
396
- END;
397
- -- Concluding, replacing the host or removing/changing an expert voids plans that have not started.
398
- -- Adding experts preserves the current plan; the invitation transaction appends their opening turns.
399
- CREATE TRIGGER IF NOT EXISTS relay_plan_concluded AFTER UPDATE OF phase ON topic_settings
400
- WHEN NEW.phase='concluded'
401
- BEGIN
402
- UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
403
- END;
404
- DROP TRIGGER IF EXISTS relay_plan_participants;
405
- CREATE TRIGGER relay_plan_participants AFTER UPDATE OF host,experts ON topic_settings
406
- WHEN NEW.host<>OLD.host OR EXISTS (
407
- SELECT 1 FROM json_each(OLD.experts) AS previous
408
- WHERE NOT EXISTS (SELECT 1 FROM json_each(NEW.experts) AS current
409
- WHERE current.key=previous.key AND current.value=previous.value)
410
- )
411
- BEGIN
412
- UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
413
- END;
414
- DROP TRIGGER IF EXISTS runtime_turn_answered;
415
- -- An answer relays to the next queued turn unless the user has acted since the host last arranged.
416
- CREATE TRIGGER runtime_turn_answered AFTER UPDATE OF status ON discussion_turns
417
- WHEN NEW.status='answered' AND OLD.status<>'answered'
418
- BEGIN
419
- UPDATE discussion_events SET status='completed',result='message:'||NEW.answer_id
420
- WHERE room_id=NEW.room_id AND turn_id=NEW.id AND status IN ('pending','dispatched','started');
421
- UPDATE discussion_turns SET status='pending'
422
- WHERE id=(SELECT MIN(id) FROM discussion_turns WHERE room_id=NEW.room_id AND status='queued')
423
- AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id AND held_since>0)
424
- AND NOT EXISTS(SELECT 1 FROM topic_settings WHERE room_id=NEW.room_id AND phase IN ('paused','concluded'));
425
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
426
- SELECT NEW.room_id,run_id,generation,'answer:'||NEW.id,host,'host'
427
- FROM discussion_runtime JOIN topic_settings USING(room_id)
428
- WHERE room_id=NEW.room_id AND status='running'
429
- AND NOT EXISTS(SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'));
430
- END;
431
- DROP TRIGGER IF EXISTS runtime_topic_stop;
432
- CREATE TRIGGER runtime_topic_stop AFTER UPDATE OF phase ON topic_settings
433
- WHEN NEW.phase IN ('paused','concluded') AND OLD.phase<>NEW.phase
434
- BEGIN
435
- UPDATE discussion_events SET status='completed',result='concluded' WHERE room_id=NEW.room_id
436
- AND role='host' AND status='started' AND NEW.phase='concluded';
437
- UPDATE discussion_runtime SET status=NEW.phase,generation=generation+1,revision=revision+1,
438
- resume_phase=CASE WHEN OLD.phase IN ('initial','discussing','reviewing') THEN OLD.phase ELSE resume_phase END,
439
- updated_at=strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE room_id=NEW.room_id;
440
- END;
441
- CREATE TRIGGER IF NOT EXISTS runtime_user_message AFTER INSERT ON message_details
442
- WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
443
- BEGIN
444
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
445
- SELECT messages.room_id,run_id,generation,'user:'||NEW.message_id,host,'host'
446
- FROM messages JOIN discussion_runtime ON discussion_runtime.room_id=messages.room_id
447
- JOIN topic_settings ON topic_settings.room_id=messages.room_id
448
- WHERE messages.id=NEW.message_id AND status='running';
449
- END;
450
- CREATE TRIGGER IF NOT EXISTS runtime_turn_cancelled AFTER UPDATE OF status ON discussion_turns
451
- WHEN NEW.status='cancelled' AND OLD.status<>'cancelled'
452
- BEGIN
453
- UPDATE discussion_events SET status='cancelled' WHERE room_id=NEW.room_id AND turn_id=NEW.id
454
- AND status IN ('pending','dispatched','started');
455
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
456
- SELECT NEW.room_id,run_id,generation,'cancel:'||NEW.id,host,'host'
457
- FROM discussion_runtime JOIN topic_settings USING(room_id)
458
- WHERE room_id=NEW.room_id AND status='running'
459
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
460
- END;
461
- CREATE TRIGGER IF NOT EXISTS runtime_archive AFTER UPDATE OF archived ON room_directory
462
- WHEN NEW.archived=1 AND OLD.archived<>NEW.archived
463
- BEGIN
464
- UPDATE discussion_runtime SET status='stopped',generation=generation+1,revision=revision+1
465
- WHERE room_id=NEW.room_id;
466
- END;
467
- DROP TRIGGER IF EXISTS runtime_participants_changed;
468
- -- A new host needs a new run. Experts joining or leaving keep the run going; the host is woken to re-plan.
469
- CREATE TRIGGER runtime_participants_changed AFTER UPDATE OF host ON topic_settings
470
- WHEN NEW.host<>OLD.host
471
- BEGIN
472
- UPDATE discussion_runtime SET status='paused',generation=generation+1,revision=revision+1
473
- WHERE room_id=NEW.room_id AND status='running';
474
- END;
475
- DROP TRIGGER IF EXISTS runtime_experts_changed;
476
- CREATE TRIGGER runtime_experts_changed AFTER UPDATE OF experts ON topic_settings
477
- WHEN NEW.host=OLD.host AND NEW.experts<>OLD.experts
478
- BEGIN
479
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
480
- SELECT NEW.room_id,run_id,generation,'members:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),NEW.host,'host'
481
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running'
482
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started'))
483
- AND NOT EXISTS(SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'));
484
- END;
485
- CREATE TRIGGER IF NOT EXISTS runtime_goal_changed AFTER UPDATE OF goal ON rooms
486
- WHEN NEW.goal<>OLD.goal
487
- BEGIN
488
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
489
- SELECT NEW.id,run_id,generation,'goal:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),host,'host'
490
- FROM discussion_runtime JOIN topic_settings USING(room_id)
491
- WHERE room_id=NEW.id AND status='running'
492
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.id AND role='host' AND status='started');
493
- END;
494
- CREATE TRIGGER IF NOT EXISTS runtime_constraints_changed AFTER UPDATE OF background,boundaries ON topic_settings
495
- WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries
496
- BEGIN
497
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
498
- SELECT NEW.room_id,run_id,generation,'constraints:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),NEW.host,'host'
499
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running'
500
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
501
- END;
502
- -- Paused, preparing or concluded time is idle: it is excluded from elapsed time and total-duration deadlines.
503
- CREATE TABLE IF NOT EXISTS discussion_pauses (
504
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
505
- paused_at INTEGER NOT NULL DEFAULT 0,
506
- paused_seconds INTEGER NOT NULL DEFAULT 0
507
- );
508
- INSERT INTO discussion_pauses(room_id,paused_at)
509
- SELECT room_id,CASE WHEN phase IN ('paused','concluded') THEN COALESCE((SELECT CAST(strftime('%s',updated_at) AS INTEGER)
510
- FROM discussion_runtime WHERE discussion_runtime.room_id=topic_settings.room_id AND status<>'running'),
511
- CAST(strftime('%s','now') AS INTEGER)) ELSE 0 END
512
- FROM topic_settings WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE discussion_pauses.room_id=topic_settings.room_id);
513
- CREATE TRIGGER IF NOT EXISTS pause_clock_stopped AFTER UPDATE OF phase ON topic_settings
514
- WHEN OLD.phase IN ('initial','discussing','reviewing') AND NEW.phase NOT IN ('initial','discussing','reviewing')
515
- BEGIN
516
- INSERT INTO discussion_pauses(room_id) SELECT NEW.room_id
517
- WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE room_id=NEW.room_id);
518
- UPDATE discussion_pauses SET paused_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND paused_at=0;
519
- END;
520
- CREATE TRIGGER IF NOT EXISTS pause_clock_resumed AFTER UPDATE OF phase ON topic_settings
521
- WHEN NEW.phase IN ('initial','discussing','reviewing') AND OLD.phase NOT IN ('initial','discussing','reviewing')
522
- BEGIN
523
- UPDATE discussion_pauses SET paused_seconds=paused_seconds+MAX(0,CAST(strftime('%s','now') AS INTEGER)-paused_at),paused_at=0
524
- WHERE room_id=NEW.room_id AND paused_at>0;
525
- END;
526
- -- Both room kinds register members; free rooms additionally persist requests and executions.
527
- CREATE TABLE IF NOT EXISTS room_members (
528
- id INTEGER PRIMARY KEY AUTOINCREMENT,
529
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
530
- name TEXT NOT NULL,
531
- adapter TEXT NOT NULL DEFAULT '',
532
- role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('member','host')),
533
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','away','removed')),
534
- token_hash TEXT NOT NULL,
535
- read_until INTEGER NOT NULL DEFAULT 0,
536
- created_at TEXT NOT NULL,
537
- access TEXT NOT NULL DEFAULT 'runner',
538
- session_mode TEXT NOT NULL DEFAULT 'new'
539
- );
540
- CREATE UNIQUE INDEX IF NOT EXISTS member_name ON room_members(room_id,name) WHERE status<>'removed';
541
- CREATE UNIQUE INDEX IF NOT EXISTS member_credential ON room_members(room_id,token_hash);
542
- -- A manual member's own software session, reported by its CLI on posting; the local service uses it to take the member over.
543
- CREATE TABLE IF NOT EXISTS member_sessions (
544
- member_id INTEGER PRIMARY KEY REFERENCES room_members(id) ON DELETE CASCADE,
545
- room_id TEXT NOT NULL,
546
- session_id TEXT NOT NULL,
547
- client TEXT NOT NULL
548
- );
549
- -- A manual member's own registration; it is bound to the credential it used, so a re-issued credential must register again.
550
- CREATE TABLE IF NOT EXISTS member_registrations (
551
- member_id INTEGER PRIMARY KEY REFERENCES room_members(id) ON DELETE CASCADE,
552
- room_id TEXT NOT NULL,
553
- token_hash TEXT NOT NULL,
554
- registered_at TEXT NOT NULL
555
- );
556
- CREATE TABLE IF NOT EXISTS member_executions (
557
- id INTEGER PRIMARY KEY AUTOINCREMENT,
558
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
559
- member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
560
- client_id TEXT NOT NULL,
561
- status TEXT NOT NULL DEFAULT 'dispatched'
562
- CHECK(status IN ('dispatched','started','completed','cancelled','failed')),
563
- session_id TEXT NOT NULL DEFAULT '',
564
- lease_until INTEGER NOT NULL DEFAULT 0,
565
- created_at INTEGER NOT NULL,
566
- finished_at INTEGER NOT NULL DEFAULT 0,
567
- result TEXT NOT NULL DEFAULT '',
568
- changes TEXT NOT NULL DEFAULT '',
569
- cwd TEXT NOT NULL DEFAULT '',
570
- UNIQUE(room_id,client_id)
571
- );
572
- -- One live execution per member: different members may run in parallel, one session never twice.
573
- CREATE UNIQUE INDEX IF NOT EXISTS one_member_execution ON member_executions(member_id)
574
- WHERE status IN ('dispatched','started');
575
- CREATE TABLE IF NOT EXISTS member_requests (
576
- id INTEGER PRIMARY KEY AUTOINCREMENT,
577
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
578
- member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
579
- message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
580
- status TEXT NOT NULL DEFAULT 'pending'
581
- CHECK(status IN ('pending','delivering','completed','cancelled','failed')),
582
- execution_id INTEGER REFERENCES member_executions(id),
583
- answer_id INTEGER REFERENCES messages(id),
584
- created_at INTEGER NOT NULL,
585
- chain_id INTEGER NOT NULL DEFAULT 0,
586
- batch_id INTEGER NOT NULL DEFAULT 0,
587
- purpose TEXT NOT NULL DEFAULT 'ask',
588
- created_by TEXT NOT NULL DEFAULT 'user',
589
- UNIQUE(message_id,member_id)
590
- );
591
- CREATE INDEX IF NOT EXISTS member_requests_open ON member_requests(member_id,status);
592
- -- Discussion options chosen at creation. A room without a row predates the option and keeps named follow-ups.
593
- CREATE TABLE IF NOT EXISTS discussion_options (
594
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
595
- named_followup INTEGER NOT NULL DEFAULT 0 CHECK(named_followup IN (0,1))
596
- );
597
- -- Visible record of each smart-scheduling decision in a discussion; it is not a message and moves no round or floor.
598
- CREATE TABLE IF NOT EXISTS scheduler_decisions (
599
- id INTEGER PRIMARY KEY AUTOINCREMENT,
600
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
601
- after_message_id INTEGER NOT NULL DEFAULT 0,
602
- kind TEXT NOT NULL CHECK(kind IN ('interjection','round')),
603
- label TEXT NOT NULL,
604
- confidence REAL,
605
- applied INTEGER NOT NULL DEFAULT 0 CHECK(applied IN (0,1)),
606
- provider TEXT NOT NULL,
607
- created_at TEXT NOT NULL
608
- );
609
- CREATE INDEX IF NOT EXISTS scheduler_decisions_room ON scheduler_decisions(room_id,id);
610
- -- What the decision model was asked and chose, shown folded under the decision; not part of backups.
611
- CREATE TABLE IF NOT EXISTS scheduler_decision_details (
612
- decision_id INTEGER PRIMARY KEY REFERENCES scheduler_decisions(id) ON DELETE CASCADE,
613
- detail TEXT NOT NULL
614
- );
615
- -- Smart scheduling's suggestion that another member answer an unnamed message. The host keeps the message;
616
- -- the user may adopt before expires_at (waiting until the host's request ends for mode 'after'), otherwise it lapses.
617
- CREATE TABLE IF NOT EXISTS member_recommendations (
618
- message_id INTEGER PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
619
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
620
- member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
621
- provider TEXT NOT NULL,
622
- confidence REAL NOT NULL,
623
- created_at INTEGER NOT NULL,
624
- expires_at INTEGER NOT NULL,
625
- status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','waiting','adopted')),
626
- mode TEXT NOT NULL DEFAULT ''
627
- );
628
- -- Ordinary members can suggest a response without creating work or spending a chain budget.
629
- CREATE TABLE IF NOT EXISTS member_suggestions (
630
- message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
631
- member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
632
- PRIMARY KEY (message_id,member_id)
633
- );
634
- -- A host's dispatch: its named requests form one batch that hands back to the host once.
635
- CREATE TABLE IF NOT EXISTS free_batches (
636
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
637
- batch_id INTEGER NOT NULL,
638
- chain_id INTEGER NOT NULL,
639
- host_id INTEGER NOT NULL,
640
- settled INTEGER NOT NULL DEFAULT 0,
641
- PRIMARY KEY (room_id,batch_id)
642
- );
643
- -- Chains whose automatic progress stopped (budget reached or every dispatched member failed).
644
- CREATE TABLE IF NOT EXISTS free_chains (
645
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
646
- chain_id INTEGER NOT NULL,
647
- reason TEXT NOT NULL,
648
- PRIMARY KEY (room_id,chain_id)
649
- );
650
- -- A brainstorm room started from a free room keeps its origin for the way back.
651
- CREATE TABLE IF NOT EXISTS room_links (
652
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
653
- parent_id TEXT NOT NULL,
654
- created_at TEXT NOT NULL
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
+ );