ai_co_discuss 0.2.13 → 0.2.14

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,580 +1,580 @@
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='cli' 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
- CREATE TRIGGER IF NOT EXISTS relay_turn_promoted AFTER UPDATE OF status ON discussion_turns
351
- WHEN OLD.status='queued' AND NEW.status='pending'
352
- BEGIN
353
- UPDATE room_floor SET holder=NULL,next_speaker=NEW.expert,revision=revision+1 WHERE room_id=NEW.room_id;
354
- UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id;
355
- UPDATE discussion_events SET status='completed',result='turn:'||NEW.id
356
- WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started');
357
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
358
- SELECT NEW.room_id,run_id,generation,'turn:'||NEW.id,NEW.expert,'expert',NEW.id
359
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running';
360
- END;
361
- -- A new arrangement is the host's response: it replaces any older plan and clears the hold.
362
- CREATE TRIGGER IF NOT EXISTS relay_plan_replaced AFTER INSERT ON discussion_turns
363
- WHEN NEW.status<>'queued'
364
- BEGIN
365
- UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued' AND id<NEW.id;
366
- UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id;
367
- END;
368
- CREATE TRIGGER IF NOT EXISTS relay_hold_user_message AFTER INSERT ON message_details
369
- WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
370
- BEGIN
371
- INSERT INTO turn_relay(room_id) SELECT room_id FROM messages WHERE id=NEW.message_id
372
- AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE turn_relay.room_id=messages.room_id);
373
- UPDATE turn_relay SET held_since=NEW.message_id
374
- WHERE room_id=(SELECT room_id FROM messages WHERE id=NEW.message_id) AND held_since=0;
375
- END;
376
- CREATE TRIGGER IF NOT EXISTS relay_hold_goal AFTER UPDATE OF goal ON rooms
377
- WHEN NEW.goal<>OLD.goal
378
- BEGIN
379
- INSERT INTO turn_relay(room_id) SELECT NEW.id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.id);
380
- UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.id)
381
- WHERE room_id=NEW.id AND held_since=0;
382
- END;
383
- CREATE TRIGGER IF NOT EXISTS relay_hold_topic AFTER UPDATE OF background,boundaries,focus ON topic_settings
384
- WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries OR NEW.focus<>OLD.focus
385
- BEGIN
386
- INSERT INTO turn_relay(room_id) SELECT NEW.room_id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id);
387
- UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.room_id)
388
- WHERE room_id=NEW.room_id AND held_since=0;
389
- END;
390
- -- Concluding or changing participants voids plans that have not started.
391
- CREATE TRIGGER IF NOT EXISTS relay_plan_concluded AFTER UPDATE OF phase ON topic_settings
392
- WHEN NEW.phase='concluded'
393
- BEGIN
394
- UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
395
- END;
396
- CREATE TRIGGER IF NOT EXISTS relay_plan_participants AFTER UPDATE OF host,experts ON topic_settings
397
- WHEN NEW.host<>OLD.host OR NEW.experts<>OLD.experts
398
- BEGIN
399
- UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
400
- END;
401
- DROP TRIGGER IF EXISTS runtime_turn_answered;
402
- -- An answer relays to the next queued turn unless the user has acted since the host last arranged.
403
- CREATE TRIGGER runtime_turn_answered AFTER UPDATE OF status ON discussion_turns
404
- WHEN NEW.status='answered' AND OLD.status<>'answered'
405
- BEGIN
406
- UPDATE discussion_events SET status='completed',result='message:'||NEW.answer_id
407
- WHERE room_id=NEW.room_id AND turn_id=NEW.id AND status IN ('pending','dispatched','started');
408
- UPDATE discussion_turns SET status='pending'
409
- WHERE id=(SELECT MIN(id) FROM discussion_turns WHERE room_id=NEW.room_id AND status='queued')
410
- AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id AND held_since>0)
411
- AND NOT EXISTS(SELECT 1 FROM topic_settings WHERE room_id=NEW.room_id AND phase IN ('paused','concluded'));
412
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
413
- SELECT NEW.room_id,run_id,generation,'answer:'||NEW.id,host,'host'
414
- FROM discussion_runtime JOIN topic_settings USING(room_id)
415
- WHERE room_id=NEW.room_id AND status='running'
416
- AND NOT EXISTS(SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'));
417
- END;
418
- DROP TRIGGER IF EXISTS runtime_topic_stop;
419
- CREATE TRIGGER runtime_topic_stop AFTER UPDATE OF phase ON topic_settings
420
- WHEN NEW.phase IN ('paused','concluded') AND OLD.phase<>NEW.phase
421
- BEGIN
422
- UPDATE discussion_events SET status='completed',result='concluded' WHERE room_id=NEW.room_id
423
- AND role='host' AND status='started' AND NEW.phase='concluded';
424
- UPDATE discussion_runtime SET status=NEW.phase,generation=generation+1,revision=revision+1,
425
- resume_phase=CASE WHEN OLD.phase IN ('initial','discussing','reviewing') THEN OLD.phase ELSE resume_phase END,
426
- updated_at=strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE room_id=NEW.room_id;
427
- END;
428
- CREATE TRIGGER IF NOT EXISTS runtime_user_message AFTER INSERT ON message_details
429
- WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
430
- BEGIN
431
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
432
- SELECT messages.room_id,run_id,generation,'user:'||NEW.message_id,host,'host'
433
- FROM messages JOIN discussion_runtime ON discussion_runtime.room_id=messages.room_id
434
- JOIN topic_settings ON topic_settings.room_id=messages.room_id
435
- WHERE messages.id=NEW.message_id AND status='running';
436
- END;
437
- CREATE TRIGGER IF NOT EXISTS runtime_turn_cancelled AFTER UPDATE OF status ON discussion_turns
438
- WHEN NEW.status='cancelled' AND OLD.status<>'cancelled'
439
- BEGIN
440
- UPDATE discussion_events SET status='cancelled' WHERE room_id=NEW.room_id AND turn_id=NEW.id
441
- AND status IN ('pending','dispatched','started');
442
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
443
- SELECT NEW.room_id,run_id,generation,'cancel:'||NEW.id,host,'host'
444
- FROM discussion_runtime JOIN topic_settings USING(room_id)
445
- WHERE room_id=NEW.room_id AND status='running'
446
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
447
- END;
448
- CREATE TRIGGER IF NOT EXISTS runtime_archive AFTER UPDATE OF archived ON room_directory
449
- WHEN NEW.archived=1 AND OLD.archived<>NEW.archived
450
- BEGIN
451
- UPDATE discussion_runtime SET status='stopped',generation=generation+1,revision=revision+1
452
- WHERE room_id=NEW.room_id;
453
- END;
454
- CREATE TRIGGER IF NOT EXISTS runtime_participants_changed AFTER UPDATE OF host,experts ON topic_settings
455
- WHEN NEW.host<>OLD.host OR NEW.experts<>OLD.experts
456
- BEGIN
457
- UPDATE discussion_runtime SET status='paused',generation=generation+1,revision=revision+1
458
- WHERE room_id=NEW.room_id AND status='running';
459
- END;
460
- CREATE TRIGGER IF NOT EXISTS runtime_goal_changed AFTER UPDATE OF goal ON rooms
461
- WHEN NEW.goal<>OLD.goal
462
- BEGIN
463
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
464
- SELECT NEW.id,run_id,generation,'goal:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),host,'host'
465
- FROM discussion_runtime JOIN topic_settings USING(room_id)
466
- WHERE room_id=NEW.id AND status='running'
467
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.id AND role='host' AND status='started');
468
- END;
469
- CREATE TRIGGER IF NOT EXISTS runtime_constraints_changed AFTER UPDATE OF background,boundaries ON topic_settings
470
- WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries
471
- BEGIN
472
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
473
- SELECT NEW.room_id,run_id,generation,'constraints:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),NEW.host,'host'
474
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running'
475
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
476
- END;
477
- -- Paused, preparing or concluded time is idle: it is excluded from elapsed time and total-duration deadlines.
478
- CREATE TABLE IF NOT EXISTS discussion_pauses (
479
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
480
- paused_at INTEGER NOT NULL DEFAULT 0,
481
- paused_seconds INTEGER NOT NULL DEFAULT 0
482
- );
483
- INSERT INTO discussion_pauses(room_id,paused_at)
484
- SELECT room_id,CASE WHEN phase IN ('paused','concluded') THEN COALESCE((SELECT CAST(strftime('%s',updated_at) AS INTEGER)
485
- FROM discussion_runtime WHERE discussion_runtime.room_id=topic_settings.room_id AND status<>'running'),
486
- CAST(strftime('%s','now') AS INTEGER)) ELSE 0 END
487
- FROM topic_settings WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE discussion_pauses.room_id=topic_settings.room_id);
488
- CREATE TRIGGER IF NOT EXISTS pause_clock_stopped AFTER UPDATE OF phase ON topic_settings
489
- WHEN OLD.phase IN ('initial','discussing','reviewing') AND NEW.phase NOT IN ('initial','discussing','reviewing')
490
- BEGIN
491
- INSERT INTO discussion_pauses(room_id) SELECT NEW.room_id
492
- WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE room_id=NEW.room_id);
493
- UPDATE discussion_pauses SET paused_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND paused_at=0;
494
- END;
495
- CREATE TRIGGER IF NOT EXISTS pause_clock_resumed AFTER UPDATE OF phase ON topic_settings
496
- WHEN NEW.phase IN ('initial','discussing','reviewing') AND OLD.phase NOT IN ('initial','discussing','reviewing')
497
- BEGIN
498
- UPDATE discussion_pauses SET paused_seconds=paused_seconds+MAX(0,CAST(strftime('%s','now') AS INTEGER)-paused_at),paused_at=0
499
- WHERE room_id=NEW.room_id AND paused_at>0;
500
- END;
501
- -- Both room kinds register members; free rooms additionally persist requests and executions.
502
- CREATE TABLE IF NOT EXISTS room_members (
503
- id INTEGER PRIMARY KEY AUTOINCREMENT,
504
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
505
- name TEXT NOT NULL,
506
- adapter TEXT NOT NULL DEFAULT '',
507
- role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('member','host')),
508
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','away','removed')),
509
- token_hash TEXT NOT NULL,
510
- read_until INTEGER NOT NULL DEFAULT 0,
511
- created_at TEXT NOT NULL,
512
- access TEXT NOT NULL DEFAULT 'runner',
513
- session_mode TEXT NOT NULL DEFAULT 'new'
514
- );
515
- CREATE UNIQUE INDEX IF NOT EXISTS member_name ON room_members(room_id,name) WHERE status<>'removed';
516
- CREATE UNIQUE INDEX IF NOT EXISTS member_credential ON room_members(room_id,token_hash);
517
- CREATE TABLE IF NOT EXISTS member_executions (
518
- id INTEGER PRIMARY KEY AUTOINCREMENT,
519
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
520
- member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
521
- client_id TEXT NOT NULL,
522
- status TEXT NOT NULL DEFAULT 'dispatched'
523
- CHECK(status IN ('dispatched','started','completed','cancelled','failed')),
524
- session_id TEXT NOT NULL DEFAULT '',
525
- lease_until INTEGER NOT NULL DEFAULT 0,
526
- created_at INTEGER NOT NULL,
527
- finished_at INTEGER NOT NULL DEFAULT 0,
528
- result TEXT NOT NULL DEFAULT '',
529
- changes TEXT NOT NULL DEFAULT '',
530
- cwd TEXT NOT NULL DEFAULT '',
531
- UNIQUE(room_id,client_id)
532
- );
533
- -- One live execution per member: different members may run in parallel, one session never twice.
534
- CREATE UNIQUE INDEX IF NOT EXISTS one_member_execution ON member_executions(member_id)
535
- WHERE status IN ('dispatched','started');
536
- CREATE TABLE IF NOT EXISTS member_requests (
537
- id INTEGER PRIMARY KEY AUTOINCREMENT,
538
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
539
- member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
540
- message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
541
- status TEXT NOT NULL DEFAULT 'pending'
542
- CHECK(status IN ('pending','delivering','completed','cancelled','failed')),
543
- execution_id INTEGER REFERENCES member_executions(id),
544
- answer_id INTEGER REFERENCES messages(id),
545
- created_at INTEGER NOT NULL,
546
- chain_id INTEGER NOT NULL DEFAULT 0,
547
- batch_id INTEGER NOT NULL DEFAULT 0,
548
- purpose TEXT NOT NULL DEFAULT 'ask',
549
- created_by TEXT NOT NULL DEFAULT 'user',
550
- UNIQUE(message_id,member_id)
551
- );
552
- CREATE INDEX IF NOT EXISTS member_requests_open ON member_requests(member_id,status);
553
- -- Ordinary members can suggest a response without creating work or spending a chain budget.
554
- CREATE TABLE IF NOT EXISTS member_suggestions (
555
- message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
556
- member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
557
- PRIMARY KEY (message_id,member_id)
558
- );
559
- -- A host's dispatch: its named requests form one batch that hands back to the host once.
560
- CREATE TABLE IF NOT EXISTS free_batches (
561
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
562
- batch_id INTEGER NOT NULL,
563
- chain_id INTEGER NOT NULL,
564
- host_id INTEGER NOT NULL,
565
- settled INTEGER NOT NULL DEFAULT 0,
566
- PRIMARY KEY (room_id,batch_id)
567
- );
568
- -- Chains whose automatic progress stopped (budget reached or every dispatched member failed).
569
- CREATE TABLE IF NOT EXISTS free_chains (
570
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
571
- chain_id INTEGER NOT NULL,
572
- reason TEXT NOT NULL,
573
- PRIMARY KEY (room_id,chain_id)
574
- );
575
- -- A brainstorm room started from a free room keeps its origin for the way back.
576
- CREATE TABLE IF NOT EXISTS room_links (
577
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
578
- parent_id TEXT NOT NULL,
579
- created_at TEXT NOT NULL
580
- );
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
+ CREATE TRIGGER IF NOT EXISTS relay_turn_promoted AFTER UPDATE OF status ON discussion_turns
351
+ WHEN OLD.status='queued' AND NEW.status='pending'
352
+ BEGIN
353
+ UPDATE room_floor SET holder=NULL,next_speaker=NEW.expert,revision=revision+1 WHERE room_id=NEW.room_id;
354
+ UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id;
355
+ UPDATE discussion_events SET status='completed',result='turn:'||NEW.id
356
+ WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started');
357
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
358
+ SELECT NEW.room_id,run_id,generation,'turn:'||NEW.id,NEW.expert,'expert',NEW.id
359
+ FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running';
360
+ END;
361
+ -- A new arrangement is the host's response: it replaces any older plan and clears the hold.
362
+ CREATE TRIGGER IF NOT EXISTS relay_plan_replaced AFTER INSERT ON discussion_turns
363
+ WHEN NEW.status<>'queued'
364
+ BEGIN
365
+ UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued' AND id<NEW.id;
366
+ UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id;
367
+ END;
368
+ CREATE TRIGGER IF NOT EXISTS relay_hold_user_message AFTER INSERT ON message_details
369
+ WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
370
+ BEGIN
371
+ INSERT INTO turn_relay(room_id) SELECT room_id FROM messages WHERE id=NEW.message_id
372
+ AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE turn_relay.room_id=messages.room_id);
373
+ UPDATE turn_relay SET held_since=NEW.message_id
374
+ WHERE room_id=(SELECT room_id FROM messages WHERE id=NEW.message_id) AND held_since=0;
375
+ END;
376
+ CREATE TRIGGER IF NOT EXISTS relay_hold_goal AFTER UPDATE OF goal ON rooms
377
+ WHEN NEW.goal<>OLD.goal
378
+ BEGIN
379
+ INSERT INTO turn_relay(room_id) SELECT NEW.id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.id);
380
+ UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.id)
381
+ WHERE room_id=NEW.id AND held_since=0;
382
+ END;
383
+ CREATE TRIGGER IF NOT EXISTS relay_hold_topic AFTER UPDATE OF background,boundaries,focus ON topic_settings
384
+ WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries OR NEW.focus<>OLD.focus
385
+ BEGIN
386
+ INSERT INTO turn_relay(room_id) SELECT NEW.room_id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id);
387
+ UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.room_id)
388
+ WHERE room_id=NEW.room_id AND held_since=0;
389
+ END;
390
+ -- Concluding or changing participants voids plans that have not started.
391
+ CREATE TRIGGER IF NOT EXISTS relay_plan_concluded AFTER UPDATE OF phase ON topic_settings
392
+ WHEN NEW.phase='concluded'
393
+ BEGIN
394
+ UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
395
+ END;
396
+ CREATE TRIGGER IF NOT EXISTS relay_plan_participants AFTER UPDATE OF host,experts ON topic_settings
397
+ WHEN NEW.host<>OLD.host OR NEW.experts<>OLD.experts
398
+ BEGIN
399
+ UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
400
+ END;
401
+ DROP TRIGGER IF EXISTS runtime_turn_answered;
402
+ -- An answer relays to the next queued turn unless the user has acted since the host last arranged.
403
+ CREATE TRIGGER runtime_turn_answered AFTER UPDATE OF status ON discussion_turns
404
+ WHEN NEW.status='answered' AND OLD.status<>'answered'
405
+ BEGIN
406
+ UPDATE discussion_events SET status='completed',result='message:'||NEW.answer_id
407
+ WHERE room_id=NEW.room_id AND turn_id=NEW.id AND status IN ('pending','dispatched','started');
408
+ UPDATE discussion_turns SET status='pending'
409
+ WHERE id=(SELECT MIN(id) FROM discussion_turns WHERE room_id=NEW.room_id AND status='queued')
410
+ AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id AND held_since>0)
411
+ AND NOT EXISTS(SELECT 1 FROM topic_settings WHERE room_id=NEW.room_id AND phase IN ('paused','concluded'));
412
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
413
+ SELECT NEW.room_id,run_id,generation,'answer:'||NEW.id,host,'host'
414
+ FROM discussion_runtime JOIN topic_settings USING(room_id)
415
+ WHERE room_id=NEW.room_id AND status='running'
416
+ AND NOT EXISTS(SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'));
417
+ END;
418
+ DROP TRIGGER IF EXISTS runtime_topic_stop;
419
+ CREATE TRIGGER runtime_topic_stop AFTER UPDATE OF phase ON topic_settings
420
+ WHEN NEW.phase IN ('paused','concluded') AND OLD.phase<>NEW.phase
421
+ BEGIN
422
+ UPDATE discussion_events SET status='completed',result='concluded' WHERE room_id=NEW.room_id
423
+ AND role='host' AND status='started' AND NEW.phase='concluded';
424
+ UPDATE discussion_runtime SET status=NEW.phase,generation=generation+1,revision=revision+1,
425
+ resume_phase=CASE WHEN OLD.phase IN ('initial','discussing','reviewing') THEN OLD.phase ELSE resume_phase END,
426
+ updated_at=strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE room_id=NEW.room_id;
427
+ END;
428
+ CREATE TRIGGER IF NOT EXISTS runtime_user_message AFTER INSERT ON message_details
429
+ WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
430
+ BEGIN
431
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
432
+ SELECT messages.room_id,run_id,generation,'user:'||NEW.message_id,host,'host'
433
+ FROM messages JOIN discussion_runtime ON discussion_runtime.room_id=messages.room_id
434
+ JOIN topic_settings ON topic_settings.room_id=messages.room_id
435
+ WHERE messages.id=NEW.message_id AND status='running';
436
+ END;
437
+ CREATE TRIGGER IF NOT EXISTS runtime_turn_cancelled AFTER UPDATE OF status ON discussion_turns
438
+ WHEN NEW.status='cancelled' AND OLD.status<>'cancelled'
439
+ BEGIN
440
+ UPDATE discussion_events SET status='cancelled' WHERE room_id=NEW.room_id AND turn_id=NEW.id
441
+ AND status IN ('pending','dispatched','started');
442
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
443
+ SELECT NEW.room_id,run_id,generation,'cancel:'||NEW.id,host,'host'
444
+ FROM discussion_runtime JOIN topic_settings USING(room_id)
445
+ WHERE room_id=NEW.room_id AND status='running'
446
+ AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
447
+ END;
448
+ CREATE TRIGGER IF NOT EXISTS runtime_archive AFTER UPDATE OF archived ON room_directory
449
+ WHEN NEW.archived=1 AND OLD.archived<>NEW.archived
450
+ BEGIN
451
+ UPDATE discussion_runtime SET status='stopped',generation=generation+1,revision=revision+1
452
+ WHERE room_id=NEW.room_id;
453
+ END;
454
+ CREATE TRIGGER IF NOT EXISTS runtime_participants_changed AFTER UPDATE OF host,experts ON topic_settings
455
+ WHEN NEW.host<>OLD.host OR NEW.experts<>OLD.experts
456
+ BEGIN
457
+ UPDATE discussion_runtime SET status='paused',generation=generation+1,revision=revision+1
458
+ WHERE room_id=NEW.room_id AND status='running';
459
+ END;
460
+ CREATE TRIGGER IF NOT EXISTS runtime_goal_changed AFTER UPDATE OF goal ON rooms
461
+ WHEN NEW.goal<>OLD.goal
462
+ BEGIN
463
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
464
+ SELECT NEW.id,run_id,generation,'goal:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),host,'host'
465
+ FROM discussion_runtime JOIN topic_settings USING(room_id)
466
+ WHERE room_id=NEW.id AND status='running'
467
+ AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.id AND role='host' AND status='started');
468
+ END;
469
+ CREATE TRIGGER IF NOT EXISTS runtime_constraints_changed AFTER UPDATE OF background,boundaries ON topic_settings
470
+ WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries
471
+ BEGIN
472
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
473
+ SELECT NEW.room_id,run_id,generation,'constraints:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),NEW.host,'host'
474
+ FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running'
475
+ AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
476
+ END;
477
+ -- Paused, preparing or concluded time is idle: it is excluded from elapsed time and total-duration deadlines.
478
+ CREATE TABLE IF NOT EXISTS discussion_pauses (
479
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
480
+ paused_at INTEGER NOT NULL DEFAULT 0,
481
+ paused_seconds INTEGER NOT NULL DEFAULT 0
482
+ );
483
+ INSERT INTO discussion_pauses(room_id,paused_at)
484
+ SELECT room_id,CASE WHEN phase IN ('paused','concluded') THEN COALESCE((SELECT CAST(strftime('%s',updated_at) AS INTEGER)
485
+ FROM discussion_runtime WHERE discussion_runtime.room_id=topic_settings.room_id AND status<>'running'),
486
+ CAST(strftime('%s','now') AS INTEGER)) ELSE 0 END
487
+ FROM topic_settings WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE discussion_pauses.room_id=topic_settings.room_id);
488
+ CREATE TRIGGER IF NOT EXISTS pause_clock_stopped AFTER UPDATE OF phase ON topic_settings
489
+ WHEN OLD.phase IN ('initial','discussing','reviewing') AND NEW.phase NOT IN ('initial','discussing','reviewing')
490
+ BEGIN
491
+ INSERT INTO discussion_pauses(room_id) SELECT NEW.room_id
492
+ WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE room_id=NEW.room_id);
493
+ UPDATE discussion_pauses SET paused_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND paused_at=0;
494
+ END;
495
+ CREATE TRIGGER IF NOT EXISTS pause_clock_resumed AFTER UPDATE OF phase ON topic_settings
496
+ WHEN NEW.phase IN ('initial','discussing','reviewing') AND OLD.phase NOT IN ('initial','discussing','reviewing')
497
+ BEGIN
498
+ UPDATE discussion_pauses SET paused_seconds=paused_seconds+MAX(0,CAST(strftime('%s','now') AS INTEGER)-paused_at),paused_at=0
499
+ WHERE room_id=NEW.room_id AND paused_at>0;
500
+ END;
501
+ -- Both room kinds register members; free rooms additionally persist requests and executions.
502
+ CREATE TABLE IF NOT EXISTS room_members (
503
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
504
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
505
+ name TEXT NOT NULL,
506
+ adapter TEXT NOT NULL DEFAULT '',
507
+ role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('member','host')),
508
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','away','removed')),
509
+ token_hash TEXT NOT NULL,
510
+ read_until INTEGER NOT NULL DEFAULT 0,
511
+ created_at TEXT NOT NULL,
512
+ access TEXT NOT NULL DEFAULT 'runner',
513
+ session_mode TEXT NOT NULL DEFAULT 'new'
514
+ );
515
+ CREATE UNIQUE INDEX IF NOT EXISTS member_name ON room_members(room_id,name) WHERE status<>'removed';
516
+ CREATE UNIQUE INDEX IF NOT EXISTS member_credential ON room_members(room_id,token_hash);
517
+ CREATE TABLE IF NOT EXISTS member_executions (
518
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
519
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
520
+ member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
521
+ client_id TEXT NOT NULL,
522
+ status TEXT NOT NULL DEFAULT 'dispatched'
523
+ CHECK(status IN ('dispatched','started','completed','cancelled','failed')),
524
+ session_id TEXT NOT NULL DEFAULT '',
525
+ lease_until INTEGER NOT NULL DEFAULT 0,
526
+ created_at INTEGER NOT NULL,
527
+ finished_at INTEGER NOT NULL DEFAULT 0,
528
+ result TEXT NOT NULL DEFAULT '',
529
+ changes TEXT NOT NULL DEFAULT '',
530
+ cwd TEXT NOT NULL DEFAULT '',
531
+ UNIQUE(room_id,client_id)
532
+ );
533
+ -- One live execution per member: different members may run in parallel, one session never twice.
534
+ CREATE UNIQUE INDEX IF NOT EXISTS one_member_execution ON member_executions(member_id)
535
+ WHERE status IN ('dispatched','started');
536
+ CREATE TABLE IF NOT EXISTS member_requests (
537
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
538
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
539
+ member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
540
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
541
+ status TEXT NOT NULL DEFAULT 'pending'
542
+ CHECK(status IN ('pending','delivering','completed','cancelled','failed')),
543
+ execution_id INTEGER REFERENCES member_executions(id),
544
+ answer_id INTEGER REFERENCES messages(id),
545
+ created_at INTEGER NOT NULL,
546
+ chain_id INTEGER NOT NULL DEFAULT 0,
547
+ batch_id INTEGER NOT NULL DEFAULT 0,
548
+ purpose TEXT NOT NULL DEFAULT 'ask',
549
+ created_by TEXT NOT NULL DEFAULT 'user',
550
+ UNIQUE(message_id,member_id)
551
+ );
552
+ CREATE INDEX IF NOT EXISTS member_requests_open ON member_requests(member_id,status);
553
+ -- Ordinary members can suggest a response without creating work or spending a chain budget.
554
+ CREATE TABLE IF NOT EXISTS member_suggestions (
555
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
556
+ member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
557
+ PRIMARY KEY (message_id,member_id)
558
+ );
559
+ -- A host's dispatch: its named requests form one batch that hands back to the host once.
560
+ CREATE TABLE IF NOT EXISTS free_batches (
561
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
562
+ batch_id INTEGER NOT NULL,
563
+ chain_id INTEGER NOT NULL,
564
+ host_id INTEGER NOT NULL,
565
+ settled INTEGER NOT NULL DEFAULT 0,
566
+ PRIMARY KEY (room_id,batch_id)
567
+ );
568
+ -- Chains whose automatic progress stopped (budget reached or every dispatched member failed).
569
+ CREATE TABLE IF NOT EXISTS free_chains (
570
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
571
+ chain_id INTEGER NOT NULL,
572
+ reason TEXT NOT NULL,
573
+ PRIMARY KEY (room_id,chain_id)
574
+ );
575
+ -- A brainstorm room started from a free room keeps its origin for the way back.
576
+ CREATE TABLE IF NOT EXISTS room_links (
577
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
578
+ parent_id TEXT NOT NULL,
579
+ created_at TEXT NOT NULL
580
+ );