ai_co_discuss 0.2.87 → 0.2.89

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,808 +1,833 @@
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
- DROP TRIGGER IF EXISTS reject_inactive_opinion;
119
- CREATE TRIGGER reject_inactive_opinion BEFORE INSERT ON message_details
120
- WHEN NEW.kind='opinion'
121
- AND (SELECT author FROM messages WHERE id=NEW.message_id)<>'用户'
122
- AND NOT EXISTS (SELECT 1 FROM message_details WHERE message_id=NEW.message_id)
123
- AND EXISTS (SELECT 1 FROM messages JOIN topic_settings ON topic_settings.room_id=messages.room_id
124
- WHERE messages.id=NEW.message_id AND topic_settings.phase IN ('paused','concluded'))
125
- BEGIN
126
- SELECT RAISE(ABORT, 'topic_inactive');
127
- END;
128
- CREATE TRIGGER IF NOT EXISTS reject_inactive_turn BEFORE INSERT ON discussion_turns
129
- WHEN NOT EXISTS (SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND submission_id=NEW.submission_id)
130
- AND EXISTS (SELECT 1 FROM topic_settings WHERE room_id=NEW.room_id AND phase IN ('paused','concluded'))
131
- BEGIN
132
- SELECT RAISE(ABORT, 'topic_inactive');
133
- END;
134
- CREATE TRIGGER IF NOT EXISTS reject_conclusion_with_open_turn BEFORE UPDATE OF phase ON topic_settings
135
- WHEN NEW.phase='concluded'
136
- AND EXISTS (SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'))
137
- BEGIN
138
- SELECT RAISE(ABORT, 'open_turn');
139
- END;
140
-
141
- CREATE TABLE IF NOT EXISTS room_floor (
142
- room_id TEXT PRIMARY KEY REFERENCES rooms(id),
143
- holder TEXT,
144
- next_speaker TEXT NOT NULL,
145
- revision INTEGER NOT NULL DEFAULT 0
146
- );
147
- -- The speaking floor is retired: the table stays only so old databases and backups load, and nothing reads or writes it.
148
- DROP TRIGGER IF EXISTS initialize_room_floor;
149
- DROP TRIGGER IF EXISTS transfer_host_floor;
150
- -- Site administrator account (single user) and its login sessions. Raw tokens and passwords are never stored.
151
- CREATE TABLE IF NOT EXISTS site_admin (
152
- id INTEGER PRIMARY KEY CHECK(id=1),
153
- username TEXT NOT NULL,
154
- password_hash TEXT NOT NULL,
155
- avatar TEXT NOT NULL DEFAULT '',
156
- failed_attempts INTEGER NOT NULL DEFAULT 0,
157
- locked_until INTEGER NOT NULL DEFAULT 0,
158
- created_at INTEGER NOT NULL
159
- );
160
- CREATE TABLE IF NOT EXISTS admin_sessions (
161
- token_hash TEXT PRIMARY KEY,
162
- created_at INTEGER NOT NULL
163
- );
164
-
165
- -- Free collaboration rooms. Rooms without a row are brainstorm rooms and keep every rule below.
166
- CREATE TABLE IF NOT EXISTS room_modes (
167
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
168
- mode TEXT NOT NULL CHECK(mode IN ('free','brainstorm')),
169
- control_hash TEXT NOT NULL DEFAULT ''
170
- );
171
- -- Replaced on every start so existing databases drop every speaking-floor rule.
172
- DROP TRIGGER IF EXISTS require_message_floor;
173
- DROP TRIGGER IF EXISTS return_message_floor;
174
- DROP TRIGGER IF EXISTS require_host_floor;
175
- DROP TRIGGER IF EXISTS nominate_expert_floor;
176
- DROP TRIGGER IF EXISTS cancel_turn_floor;
177
- CREATE TRIGGER IF NOT EXISTS require_opinion_turn BEFORE INSERT ON message_details
178
- WHEN NEW.kind='opinion' AND NEW.turn_id IS NULL
179
- AND NOT EXISTS (SELECT 1 FROM message_details WHERE message_id=NEW.message_id)
180
- AND EXISTS (SELECT 1 FROM messages WHERE id=NEW.message_id AND author<>'用户')
181
- BEGIN
182
- SELECT RAISE(ABORT, 'floor_turn_required');
183
- END;
184
-
185
- -- Runtime control is opt-in. Legacy rooms have no runtime row and keep manual entry.
186
- CREATE TABLE IF NOT EXISTS discussion_limits (
187
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
188
- max_rounds INTEGER NOT NULL DEFAULT 0,
189
- max_minutes INTEGER NOT NULL DEFAULT 0,
190
- execution_minutes INTEGER NOT NULL DEFAULT 0,
191
- on_limit TEXT NOT NULL DEFAULT 'pause',
192
- summary_minutes INTEGER NOT NULL DEFAULT 5,
193
- started_at INTEGER NOT NULL DEFAULT 0,
194
- closing_at INTEGER NOT NULL DEFAULT 0,
195
- stop_reason TEXT NOT NULL DEFAULT '',
196
- revision INTEGER NOT NULL DEFAULT 0
197
- );
198
- INSERT OR IGNORE INTO discussion_limits(room_id,execution_minutes) SELECT id,0 FROM rooms;
199
- DROP TRIGGER IF EXISTS limits_room_created;
200
- CREATE TRIGGER limits_room_created AFTER INSERT ON rooms
201
- BEGIN
202
- INSERT INTO discussion_limits(room_id,execution_minutes) VALUES (NEW.id,0);
203
- END;
204
- CREATE TRIGGER IF NOT EXISTS limits_topic_started AFTER INSERT ON topic_settings
205
- WHEN NEW.phase IN ('initial','discussing')
206
- BEGIN
207
- UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
208
- END;
209
- CREATE TRIGGER IF NOT EXISTS limits_phase_started AFTER UPDATE OF phase ON topic_settings
210
- WHEN NEW.phase IN ('initial','discussing') AND NEW.phase<>OLD.phase
211
- BEGIN
212
- UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
213
- END;
214
- CREATE TRIGGER IF NOT EXISTS limits_turn_started AFTER INSERT ON discussion_turns
215
- BEGIN
216
- UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
217
- END;
218
- CREATE TABLE IF NOT EXISTS discussion_runtime (
219
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
220
- mode TEXT NOT NULL CHECK(mode IN ('manual','automatic')),
221
- run_id TEXT NOT NULL,
222
- generation INTEGER NOT NULL DEFAULT 1,
223
- status TEXT NOT NULL CHECK(status IN ('ready','running','paused','stopped','concluded')),
224
- control_hash TEXT NOT NULL,
225
- deadline INTEGER NOT NULL DEFAULT 0, -- 0 means no run duration limit; execution leases remain bounded.
226
- revision INTEGER NOT NULL DEFAULT 1,
227
- resume_phase TEXT NOT NULL DEFAULT 'initial',
228
- updated_at TEXT NOT NULL
229
- );
230
- CREATE TABLE IF NOT EXISTS discussion_events (
231
- id INTEGER PRIMARY KEY AUTOINCREMENT,
232
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
233
- run_id TEXT NOT NULL,
234
- generation INTEGER NOT NULL,
235
- cause TEXT NOT NULL,
236
- participant TEXT NOT NULL,
237
- role TEXT NOT NULL CHECK(role IN ('host','expert')),
238
- turn_id INTEGER,
239
- status TEXT NOT NULL DEFAULT 'pending'
240
- CHECK(status IN ('pending','dispatched','started','completed','cancelled','failed')),
241
- token_hash TEXT NOT NULL DEFAULT '',
242
- session_id TEXT NOT NULL DEFAULT '',
243
- lease_until INTEGER NOT NULL DEFAULT 0,
244
- started_at INTEGER NOT NULL DEFAULT 0,
245
- result TEXT NOT NULL DEFAULT '',
246
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
247
- UNIQUE(room_id,run_id,generation,cause)
248
- );
249
- CREATE INDEX IF NOT EXISTS events_room_cursor ON discussion_events(room_id,id);
250
- CREATE UNIQUE INDEX IF NOT EXISTS one_active_execution ON discussion_events(room_id)
251
- WHERE status IN ('dispatched','started');
252
- -- Stage timestamps for the progress panel; old events simply have no timing row.
253
- CREATE TABLE IF NOT EXISTS event_timings (
254
- event_id INTEGER PRIMARY KEY REFERENCES discussion_events(id) ON DELETE CASCADE,
255
- dispatched_at INTEGER NOT NULL DEFAULT 0,
256
- finished_at INTEGER NOT NULL DEFAULT 0
257
- );
258
- -- Freeze dispatch-to-publication time independently of the recent execution list.
259
- CREATE TABLE IF NOT EXISTS message_timings (
260
- message_id INTEGER PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
261
- elapsed_seconds INTEGER NOT NULL CHECK(elapsed_seconds>=0)
262
- );
263
- -- Only completed expert events identify a historical message unambiguously.
264
- INSERT INTO message_timings(message_id,elapsed_seconds)
265
- SELECT m.id,MAX(0,CAST(strftime('%s',m.created_at) AS INTEGER)-t.dispatched_at)
266
- FROM messages AS m JOIN discussion_events AS e
267
- ON e.room_id=m.room_id AND e.result='message:'||m.id AND e.participant=m.author
268
- JOIN event_timings AS t ON t.event_id=e.id
269
- WHERE m.source IN ('cli','api') AND e.role='expert' AND e.status='completed'
270
- AND e.started_at>0 AND t.dispatched_at>0 AND t.finished_at>0
271
- ON CONFLICT(message_id) DO NOTHING;
272
- CREATE TRIGGER IF NOT EXISTS record_event_timing AFTER UPDATE OF status ON discussion_events
273
- WHEN OLD.status<>NEW.status
274
- BEGIN
275
- -- Avoid OR IGNORE: an outer statement's conflict policy would override it.
276
- INSERT INTO event_timings(event_id) SELECT NEW.id
277
- WHERE NOT EXISTS(SELECT 1 FROM event_timings WHERE event_id=NEW.id);
278
- UPDATE event_timings SET dispatched_at=CAST(strftime('%s','now') AS INTEGER)
279
- WHERE event_id=NEW.id AND NEW.status='dispatched';
280
- UPDATE event_timings SET finished_at=CAST(strftime('%s','now') AS INTEGER)
281
- WHERE event_id=NEW.id AND NEW.status IN ('completed','cancelled','failed');
282
- END;
283
- -- A failed assertion rolls back the whole D1 batch, including concurrent writes.
284
- CREATE TABLE IF NOT EXISTS runtime_assertions (
285
- id TEXT PRIMARY KEY, allowed INTEGER NOT NULL CHECK(allowed=1)
286
- );
287
- CREATE TRIGGER IF NOT EXISTS runtime_generation AFTER UPDATE ON discussion_runtime
288
- WHEN OLD.generation<>NEW.generation OR OLD.status<>NEW.status
289
- BEGIN
290
- UPDATE discussion_events SET status='cancelled' WHERE room_id=NEW.room_id
291
- AND status IN ('pending','dispatched','started')
292
- AND (generation<>NEW.generation OR NEW.status<>'running');
293
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
294
- SELECT NEW.room_id,NEW.run_id,NEW.generation,'start:'||NEW.generation,
295
- COALESCE(turns.expert,settings.host),CASE WHEN turns.id IS NULL THEN 'host' ELSE 'expert' END,turns.id
296
- FROM topic_settings AS settings LEFT JOIN discussion_turns AS turns
297
- ON turns.room_id=settings.room_id AND turns.status IN ('pending','notified')
298
- WHERE settings.room_id=NEW.room_id AND NEW.status='running';
299
- END;
300
- DROP TRIGGER IF EXISTS runtime_turn_created;
301
- CREATE TRIGGER runtime_turn_created AFTER INSERT ON discussion_turns
302
- WHEN NEW.status<>'queued'
303
- BEGIN
304
- UPDATE discussion_events SET status='completed',result='turn:'||NEW.id
305
- WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started');
306
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
307
- SELECT NEW.room_id,run_id,generation,'turn:'||NEW.id,NEW.expert,'expert',NEW.id
308
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running';
309
- END;
310
- -- Round plans: queued turns wait in id order. held_since marks the first unprocessed user action
311
- -- (a message id, or the next id for a topic edit); while set, answers return to the host instead of relaying.
312
- CREATE TABLE IF NOT EXISTS turn_relay (
313
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
314
- held_since INTEGER NOT NULL DEFAULT 0
315
- );
316
- -- Both triggers below clear the hold only when no user message arrived after the running host execution began; the host cannot have read it.
317
- DROP TRIGGER IF EXISTS relay_turn_promoted;
318
- CREATE TRIGGER relay_turn_promoted AFTER UPDATE OF status ON discussion_turns
319
- WHEN OLD.status='queued' AND NEW.status='pending'
320
- BEGIN
321
- UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id
322
- 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'
323
- WHERE m.room_id=NEW.room_id AND m.author='用户' AND CAST(strftime('%s',m.created_at) AS INTEGER)>e.started_at);
324
- UPDATE discussion_events SET status='completed',result='turn:'||NEW.id
325
- WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started');
326
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
327
- SELECT NEW.room_id,run_id,generation,'turn:'||NEW.id,NEW.expert,'expert',NEW.id
328
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running';
329
- END;
330
- -- A new arrangement is the host's response: it replaces any older plan and clears the hold.
331
- DROP TRIGGER IF EXISTS relay_plan_replaced;
332
- CREATE TRIGGER relay_plan_replaced AFTER INSERT ON discussion_turns
333
- WHEN NEW.status<>'queued'
334
- BEGIN
335
- UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued' AND id<NEW.id;
336
- UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id
337
- 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'
338
- WHERE m.room_id=NEW.room_id AND m.author='用户' AND CAST(strftime('%s',m.created_at) AS INTEGER)>e.started_at);
339
- END;
340
- CREATE TRIGGER IF NOT EXISTS relay_hold_user_message AFTER INSERT ON message_details
341
- WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
342
- BEGIN
343
- INSERT INTO turn_relay(room_id) SELECT room_id FROM messages WHERE id=NEW.message_id
344
- AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE turn_relay.room_id=messages.room_id);
345
- UPDATE turn_relay SET held_since=NEW.message_id
346
- WHERE room_id=(SELECT room_id FROM messages WHERE id=NEW.message_id) AND held_since=0;
347
- END;
348
- CREATE TRIGGER IF NOT EXISTS relay_hold_goal AFTER UPDATE OF goal ON rooms
349
- WHEN NEW.goal<>OLD.goal
350
- BEGIN
351
- INSERT INTO turn_relay(room_id) SELECT NEW.id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.id);
352
- UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.id)
353
- WHERE room_id=NEW.id AND held_since=0;
354
- END;
355
- CREATE TRIGGER IF NOT EXISTS relay_hold_topic AFTER UPDATE OF background,boundaries,focus ON topic_settings
356
- WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries OR NEW.focus<>OLD.focus
357
- BEGIN
358
- INSERT INTO turn_relay(room_id) SELECT NEW.room_id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id);
359
- UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.room_id)
360
- WHERE room_id=NEW.room_id AND held_since=0;
361
- END;
362
- -- Concluding, replacing the host or removing/changing an expert voids plans that have not started.
363
- -- Adding experts preserves the current plan; the invitation transaction appends their opening turns.
364
- CREATE TRIGGER IF NOT EXISTS relay_plan_concluded AFTER UPDATE OF phase ON topic_settings
365
- WHEN NEW.phase='concluded'
366
- BEGIN
367
- UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
368
- END;
369
- DROP TRIGGER IF EXISTS relay_plan_participants;
370
- CREATE TRIGGER relay_plan_participants AFTER UPDATE OF host,experts ON topic_settings
371
- WHEN NEW.host<>OLD.host OR EXISTS (
372
- SELECT 1 FROM json_each(OLD.experts) AS previous
373
- WHERE NOT EXISTS (SELECT 1 FROM json_each(NEW.experts) AS current
374
- WHERE current.key=previous.key AND current.value=previous.value)
375
- )
376
- BEGIN
377
- UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
378
- END;
379
- DROP TRIGGER IF EXISTS runtime_turn_answered;
380
- -- An answer relays to the next queued turn unless the user has acted since the host last arranged.
381
- CREATE TRIGGER runtime_turn_answered AFTER UPDATE OF status ON discussion_turns
382
- WHEN NEW.status='answered' AND OLD.status<>'answered'
383
- BEGIN
384
- UPDATE discussion_events SET status='completed',result='message:'||NEW.answer_id
385
- WHERE room_id=NEW.room_id AND turn_id=NEW.id AND status IN ('pending','dispatched','started');
386
- UPDATE discussion_turns SET status='pending'
387
- WHERE id=(SELECT MIN(id) FROM discussion_turns WHERE room_id=NEW.room_id AND status='queued')
388
- AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id AND held_since>0)
389
- AND NOT EXISTS(SELECT 1 FROM topic_settings WHERE room_id=NEW.room_id AND phase IN ('paused','concluded'));
390
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
391
- SELECT NEW.room_id,run_id,generation,'answer:'||NEW.id,host,'host'
392
- FROM discussion_runtime JOIN topic_settings USING(room_id)
393
- WHERE room_id=NEW.room_id AND status='running'
394
- AND NOT EXISTS(SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'));
395
- END;
396
- DROP TRIGGER IF EXISTS runtime_topic_stop;
397
- CREATE TRIGGER runtime_topic_stop AFTER UPDATE OF phase ON topic_settings
398
- WHEN NEW.phase IN ('paused','concluded') AND OLD.phase<>NEW.phase
399
- BEGIN
400
- UPDATE discussion_events SET status='completed',result='concluded' WHERE room_id=NEW.room_id
401
- AND role='host' AND status='started' AND NEW.phase='concluded';
402
- UPDATE discussion_runtime SET status=NEW.phase,generation=generation+1,revision=revision+1,
403
- resume_phase=CASE WHEN OLD.phase IN ('initial','discussing') THEN OLD.phase ELSE resume_phase END,
404
- updated_at=strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE room_id=NEW.room_id;
405
- END;
406
- CREATE TRIGGER IF NOT EXISTS runtime_user_message AFTER INSERT ON message_details
407
- WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
408
- BEGIN
409
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
410
- SELECT messages.room_id,run_id,generation,'user:'||NEW.message_id,host,'host'
411
- FROM messages JOIN discussion_runtime ON discussion_runtime.room_id=messages.room_id
412
- JOIN topic_settings ON topic_settings.room_id=messages.room_id
413
- WHERE messages.id=NEW.message_id AND status='running';
414
- END;
415
- CREATE TRIGGER IF NOT EXISTS runtime_turn_cancelled AFTER UPDATE OF status ON discussion_turns
416
- WHEN NEW.status='cancelled' AND OLD.status<>'cancelled'
417
- BEGIN
418
- UPDATE discussion_events SET status='cancelled' WHERE room_id=NEW.room_id AND turn_id=NEW.id
419
- AND status IN ('pending','dispatched','started');
420
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
421
- SELECT NEW.room_id,run_id,generation,'cancel:'||NEW.id,host,'host'
422
- FROM discussion_runtime JOIN topic_settings USING(room_id)
423
- WHERE room_id=NEW.room_id AND status='running'
424
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
425
- END;
426
- CREATE TRIGGER IF NOT EXISTS runtime_archive AFTER UPDATE OF archived ON room_directory
427
- WHEN NEW.archived=1 AND OLD.archived<>NEW.archived
428
- BEGIN
429
- UPDATE discussion_runtime SET status='stopped',generation=generation+1,revision=revision+1
430
- WHERE room_id=NEW.room_id;
431
- END;
432
- DROP TRIGGER IF EXISTS runtime_participants_changed;
433
- -- A new host needs a new run. Experts joining or leaving keep the run going; the host is woken to re-plan.
434
- CREATE TRIGGER runtime_participants_changed AFTER UPDATE OF host ON topic_settings
435
- WHEN NEW.host<>OLD.host
436
- BEGIN
437
- UPDATE discussion_runtime SET status='paused',generation=generation+1,revision=revision+1
438
- WHERE room_id=NEW.room_id AND status='running';
439
- END;
440
- DROP TRIGGER IF EXISTS runtime_experts_changed;
441
- CREATE TRIGGER runtime_experts_changed AFTER UPDATE OF experts ON topic_settings
442
- WHEN NEW.host=OLD.host AND NEW.experts<>OLD.experts
443
- BEGIN
444
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
445
- SELECT NEW.room_id,run_id,generation,'members:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),NEW.host,'host'
446
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running'
447
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started'))
448
- AND NOT EXISTS(SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'));
449
- END;
450
- CREATE TRIGGER IF NOT EXISTS runtime_goal_changed AFTER UPDATE OF goal ON rooms
451
- WHEN NEW.goal<>OLD.goal
452
- BEGIN
453
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
454
- SELECT NEW.id,run_id,generation,'goal:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),host,'host'
455
- FROM discussion_runtime JOIN topic_settings USING(room_id)
456
- WHERE room_id=NEW.id AND status='running'
457
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.id AND role='host' AND status='started');
458
- END;
459
- CREATE TRIGGER IF NOT EXISTS runtime_constraints_changed AFTER UPDATE OF background,boundaries ON topic_settings
460
- WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries
461
- BEGIN
462
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
463
- SELECT NEW.room_id,run_id,generation,'constraints:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),NEW.host,'host'
464
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running'
465
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
466
- END;
467
- -- Paused, preparing or concluded time is idle: it is excluded from elapsed time and total-duration deadlines.
468
- CREATE TABLE IF NOT EXISTS discussion_pauses (
469
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
470
- paused_at INTEGER NOT NULL DEFAULT 0,
471
- paused_seconds INTEGER NOT NULL DEFAULT 0
472
- );
473
- INSERT INTO discussion_pauses(room_id,paused_at)
474
- SELECT room_id,CASE WHEN phase IN ('paused','concluded') THEN COALESCE((SELECT CAST(strftime('%s',updated_at) AS INTEGER)
475
- FROM discussion_runtime WHERE discussion_runtime.room_id=topic_settings.room_id AND status<>'running'),
476
- CAST(strftime('%s','now') AS INTEGER)) ELSE 0 END
477
- FROM topic_settings WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE discussion_pauses.room_id=topic_settings.room_id);
478
- CREATE TRIGGER IF NOT EXISTS pause_clock_stopped AFTER UPDATE OF phase ON topic_settings
479
- WHEN OLD.phase IN ('initial','discussing') AND NEW.phase NOT IN ('initial','discussing')
480
- BEGIN
481
- INSERT INTO discussion_pauses(room_id) SELECT NEW.room_id
482
- WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE room_id=NEW.room_id);
483
- UPDATE discussion_pauses SET paused_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND paused_at=0;
484
- END;
485
- CREATE TRIGGER IF NOT EXISTS pause_clock_resumed AFTER UPDATE OF phase ON topic_settings
486
- WHEN NEW.phase IN ('initial','discussing') AND OLD.phase NOT IN ('initial','discussing')
487
- BEGIN
488
- UPDATE discussion_pauses SET paused_seconds=paused_seconds+MAX(0,CAST(strftime('%s','now') AS INTEGER)-paused_at),paused_at=0
489
- WHERE room_id=NEW.room_id AND paused_at>0;
490
- END;
491
- -- Both room kinds register members; free rooms additionally persist requests and executions.
492
- CREATE TABLE IF NOT EXISTS room_members (
493
- id INTEGER PRIMARY KEY AUTOINCREMENT,
494
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
495
- name TEXT NOT NULL,
496
- adapter TEXT NOT NULL DEFAULT '',
497
- role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('member','host')),
498
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','away','removed')),
499
- token_hash TEXT NOT NULL,
500
- read_until INTEGER NOT NULL DEFAULT 0,
501
- created_at TEXT NOT NULL,
502
- access TEXT NOT NULL DEFAULT 'runner',
503
- session_mode TEXT NOT NULL DEFAULT 'new'
504
- );
505
- CREATE UNIQUE INDEX IF NOT EXISTS member_name ON room_members(room_id,name) WHERE status<>'removed';
506
- CREATE UNIQUE INDEX IF NOT EXISTS member_credential ON room_members(room_id,token_hash);
507
- -- A manual member's own software session, reported by its CLI on posting; the local service uses it to take the member over.
508
- CREATE TABLE IF NOT EXISTS member_sessions (
509
- member_id INTEGER PRIMARY KEY REFERENCES room_members(id) ON DELETE CASCADE,
510
- room_id TEXT NOT NULL,
511
- session_id TEXT NOT NULL,
512
- client TEXT NOT NULL
513
- );
514
- -- A manual member's own registration; it is bound to the credential it used, so a re-issued credential must register again.
515
- CREATE TABLE IF NOT EXISTS member_registrations (
516
- member_id INTEGER PRIMARY KEY REFERENCES room_members(id) ON DELETE CASCADE,
517
- room_id TEXT NOT NULL,
518
- token_hash TEXT NOT NULL,
519
- registered_at TEXT NOT NULL
520
- );
521
- CREATE TABLE IF NOT EXISTS member_executions (
522
- id INTEGER PRIMARY KEY AUTOINCREMENT,
523
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
524
- member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
525
- client_id TEXT NOT NULL,
526
- status TEXT NOT NULL DEFAULT 'dispatched'
527
- CHECK(status IN ('dispatched','started','completed','cancelled','failed')),
528
- session_id TEXT NOT NULL DEFAULT '',
529
- lease_until INTEGER NOT NULL DEFAULT 0,
530
- created_at INTEGER NOT NULL,
531
- finished_at INTEGER NOT NULL DEFAULT 0,
532
- result TEXT NOT NULL DEFAULT '',
533
- changes TEXT NOT NULL DEFAULT '',
534
- cwd TEXT NOT NULL DEFAULT '',
535
- activity TEXT NOT NULL DEFAULT '',
536
- activity_at INTEGER NOT NULL DEFAULT 0,
537
- UNIQUE(room_id,client_id)
538
- );
539
- -- What the owner wrote about each member (strengths, division of work), by name so it survives leaving and rejoining.
540
- CREATE TABLE IF NOT EXISTS member_descriptions (
541
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
542
- name TEXT NOT NULL,
543
- description TEXT NOT NULL,
544
- updated_at INTEGER NOT NULL,
545
- PRIMARY KEY(room_id,name)
546
- );
547
- -- Progress notes the runner saw a member's software write between tool calls; numbered per execution so a retried renewal adds nothing.
548
- CREATE TABLE IF NOT EXISTS execution_notes (
549
- execution_id INTEGER NOT NULL REFERENCES member_executions(id) ON DELETE CASCADE,
550
- seq INTEGER NOT NULL,
551
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
552
- text TEXT NOT NULL,
553
- created_at INTEGER NOT NULL,
554
- PRIMARY KEY(execution_id,seq)
555
- );
556
- -- One live execution per member: different members may run in parallel, one session never twice.
557
- CREATE UNIQUE INDEX IF NOT EXISTS one_member_execution ON member_executions(member_id)
558
- WHERE status IN ('dispatched','started');
559
- CREATE TABLE IF NOT EXISTS member_requests (
560
- id INTEGER PRIMARY KEY AUTOINCREMENT,
561
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
562
- member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
563
- message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
564
- status TEXT NOT NULL DEFAULT 'pending'
565
- CHECK(status IN ('pending','delivering','completed','cancelled','failed')),
566
- execution_id INTEGER REFERENCES member_executions(id),
567
- answer_id INTEGER REFERENCES messages(id),
568
- created_at INTEGER NOT NULL,
569
- chain_id INTEGER NOT NULL DEFAULT 0,
570
- batch_id INTEGER NOT NULL DEFAULT 0,
571
- purpose TEXT NOT NULL DEFAULT 'ask',
572
- created_by TEXT NOT NULL DEFAULT 'user',
573
- UNIQUE(message_id,member_id)
574
- );
575
- CREATE INDEX IF NOT EXISTS member_requests_open ON member_requests(member_id,status);
576
- -- Discussion options chosen at creation. A room without a row predates the option and keeps named follow-ups.
577
- CREATE TABLE IF NOT EXISTS discussion_options (
578
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
579
- named_followup INTEGER NOT NULL DEFAULT 0 CHECK(named_followup IN (0,1))
580
- );
581
- -- Program-controlled round budget: rounds are expert opinions per active expert counted from `base`, so a roster that skips
582
- -- someone cannot stall the limit. `cap` is the current segment's round limit (0 = discussion_limits.max_rounds); `extra` is the
583
- -- total rounds the user appended. A room without a row predates the budget and keeps the per-roster round count.
584
- CREATE TABLE IF NOT EXISTS round_budgets (
585
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
586
- base INTEGER NOT NULL DEFAULT 0,
587
- cap INTEGER NOT NULL DEFAULT 0,
588
- extra INTEGER NOT NULL DEFAULT 0
589
- );
590
- -- Visible record of each smart-scheduling decision in a discussion; it is not a message and moves no round or floor.
591
- CREATE TABLE IF NOT EXISTS scheduler_decisions (
592
- id INTEGER PRIMARY KEY AUTOINCREMENT,
593
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
594
- after_message_id INTEGER NOT NULL DEFAULT 0,
595
- kind TEXT NOT NULL CHECK(kind IN ('interjection','round')),
596
- label TEXT NOT NULL,
597
- confidence REAL,
598
- applied INTEGER NOT NULL DEFAULT 0 CHECK(applied IN (0,1)),
599
- provider TEXT NOT NULL,
600
- created_at TEXT NOT NULL
601
- );
602
- CREATE INDEX IF NOT EXISTS scheduler_decisions_room ON scheduler_decisions(room_id,id);
603
- -- What the decision model was asked and chose, shown folded under the decision; not part of backups.
604
- CREATE TABLE IF NOT EXISTS scheduler_decision_details (
605
- decision_id INTEGER PRIMARY KEY REFERENCES scheduler_decisions(id) ON DELETE CASCADE,
606
- detail TEXT NOT NULL
607
- );
608
- -- The owner's verdict on one decision; a 'bad' one may also say who should have answered and why.
609
- -- Kept apart from the decision so older databases need no column change.
610
- CREATE TABLE IF NOT EXISTS scheduler_feedback (
611
- decision_id INTEGER PRIMARY KEY REFERENCES scheduler_decisions(id) ON DELETE CASCADE,
612
- rating TEXT NOT NULL CHECK(rating IN ('good','bad')),
613
- should_be TEXT NOT NULL DEFAULT '',
614
- note TEXT NOT NULL DEFAULT '',
615
- created_at TEXT NOT NULL
616
- );
617
- -- Messages the local service is still asking the decision model about; a transient marker, not part of backups.
618
- CREATE TABLE IF NOT EXISTS scheduler_routing (
619
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
620
- message_id INTEGER NOT NULL,
621
- created_at INTEGER NOT NULL,
622
- PRIMARY KEY (room_id, message_id)
623
- );
624
- -- Why a request was cancelled; a side table so older databases need no column change. Requests cancelled before it existed have no row.
625
- CREATE TABLE IF NOT EXISTS request_cancel_reasons (
626
- request_id INTEGER PRIMARY KEY REFERENCES member_requests(id) ON DELETE CASCADE,
627
- reason TEXT NOT NULL CHECK(reason IN ('user_stop','member_away','member_removed','rerouted','no_reply','archived'))
628
- );
629
- -- What produced a decision (model, threshold, prompt and package versions), so ratings stay comparable across versions.
630
- CREATE TABLE IF NOT EXISTS scheduler_decision_context (
631
- decision_id INTEGER PRIMARY KEY REFERENCES scheduler_decisions(id) ON DELETE CASCADE,
632
- context TEXT NOT NULL
633
- );
634
- -- The exact input the decision model saw (state and full questions with option descriptions); not part of backups.
635
- CREATE TABLE IF NOT EXISTS scheduler_decision_inputs (
636
- decision_id INTEGER PRIMARY KEY REFERENCES scheduler_decisions(id) ON DELETE CASCADE,
637
- input TEXT NOT NULL
638
- );
639
- -- A self-contained copy of each decision the owner currently rates bad, kept when its room is deleted so prompts can be reviewed.
640
- CREATE TABLE IF NOT EXISTS scheduler_feedback_cases (
641
- decision_id INTEGER PRIMARY KEY,
642
- room_id TEXT NOT NULL,
643
- room_title TEXT NOT NULL,
644
- snapshot TEXT NOT NULL,
645
- should_be TEXT NOT NULL DEFAULT '',
646
- note TEXT NOT NULL DEFAULT '',
647
- rated_at TEXT NOT NULL
648
- );
649
- -- Smart scheduling's suggestion that another member answer an unnamed message. The host keeps the message;
650
- -- the user may adopt before expires_at (waiting until the host's request ends for mode 'after'), otherwise it lapses.
651
- CREATE TABLE IF NOT EXISTS member_recommendations (
652
- message_id INTEGER PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
653
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
654
- member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
655
- provider TEXT NOT NULL,
656
- confidence REAL NOT NULL,
657
- created_at INTEGER NOT NULL,
658
- expires_at INTEGER NOT NULL,
659
- status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','waiting','adopted')),
660
- mode TEXT NOT NULL DEFAULT ''
661
- );
662
- -- Ordinary members can suggest a response without creating work or spending a chain budget.
663
- CREATE TABLE IF NOT EXISTS member_suggestions (
664
- message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
665
- member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
666
- PRIMARY KEY (message_id,member_id)
667
- );
668
- -- A host's dispatch: its named requests form one batch that hands back to the host once.
669
- CREATE TABLE IF NOT EXISTS free_batches (
670
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
671
- batch_id INTEGER NOT NULL,
672
- chain_id INTEGER NOT NULL,
673
- host_id INTEGER NOT NULL,
674
- settled INTEGER NOT NULL DEFAULT 0,
675
- PRIMARY KEY (room_id,batch_id)
676
- );
677
- -- Members the user named to answer one after another, in the room's speaking order: each gets its request once the one before
678
- -- it ends. Asked rows stay so the speaking order can show who already answered; the owner may reorder rows not yet asked.
679
- CREATE TABLE IF NOT EXISTS free_queue (
680
- id INTEGER PRIMARY KEY AUTOINCREMENT,
681
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
682
- message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
683
- member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE
684
- );
685
- -- The owner's default speaking order (JSON list of member ids); members it lacks follow, the host first, then by joining order.
686
- CREATE TABLE IF NOT EXISTS speaking_orders (
687
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
688
- member_ids TEXT NOT NULL
689
- );
690
- -- Chains whose automatic progress stopped (budget reached or every dispatched member failed).
691
- CREATE TABLE IF NOT EXISTS free_chains (
692
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
693
- chain_id INTEGER NOT NULL,
694
- reason TEXT NOT NULL,
695
- PRIMARY KEY (room_id,chain_id)
696
- );
697
- -- A brainstorm room started from a free room keeps its origin for the way back.
698
- CREATE TABLE IF NOT EXISTS room_links (
699
- room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
700
- parent_id TEXT NOT NULL,
701
- created_at TEXT NOT NULL
702
- );
703
-
704
- -- A discussion is a switch on a room, not a room type: one thin session per started discussion. Its status mirrors the legacy
705
- -- topic phase until the phase machinery is retired. Free rooms have none.
706
- CREATE TABLE IF NOT EXISTS discussion_sessions (
707
- id INTEGER PRIMARY KEY AUTOINCREMENT,
708
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
709
- goal TEXT NOT NULL,
710
- status TEXT NOT NULL DEFAULT 'paused' CHECK(status IN ('active','paused','concluded')),
711
- started_message_id INTEGER NOT NULL DEFAULT 0,
712
- ended_message_id INTEGER NOT NULL DEFAULT 0,
713
- created_at TEXT NOT NULL
714
- );
715
- CREATE UNIQUE INDEX IF NOT EXISTS one_open_session ON discussion_sessions(room_id) WHERE status IN ('active','paused');
716
- -- Rooms that predate sessions: every non-free room with topic settings gets one, mapped from its phase. Safe to repeat.
717
- INSERT INTO discussion_sessions (room_id,goal,status,started_message_id,ended_message_id,created_at)
718
- SELECT rooms.id,rooms.goal,
719
- CASE WHEN topic_settings.phase='concluded' THEN 'concluded' WHEN topic_settings.phase IN ('preparing','paused') THEN 'paused' ELSE 'active' END,
720
- 0,CASE WHEN topic_settings.phase='concluded' THEN COALESCE((SELECT MAX(id) FROM messages WHERE room_id=rooms.id),0) ELSE 0 END,rooms.created_at
721
- FROM rooms JOIN topic_settings ON topic_settings.room_id=rooms.id
722
- WHERE NOT EXISTS (SELECT 1 FROM room_modes WHERE room_id=rooms.id AND mode='free')
723
- AND NOT EXISTS (SELECT 1 FROM discussion_sessions WHERE room_id=rooms.id);
724
- DROP TRIGGER IF EXISTS session_for_new_discussion;
725
- CREATE TRIGGER session_for_new_discussion AFTER INSERT ON room_modes
726
- WHEN NEW.mode='brainstorm' AND NOT EXISTS (SELECT 1 FROM discussion_sessions WHERE room_id=NEW.room_id)
727
- BEGIN
728
- INSERT INTO discussion_sessions (room_id,goal,status,created_at)
729
- SELECT rooms.id,rooms.goal,
730
- COALESCE((SELECT CASE WHEN phase='concluded' THEN 'concluded' WHEN phase IN ('preparing','paused') THEN 'paused' ELSE 'active' END
731
- FROM topic_settings WHERE room_id=rooms.id),'paused'),rooms.created_at
732
- FROM rooms WHERE rooms.id=NEW.room_id;
733
- END;
734
- DROP TRIGGER IF EXISTS session_follows_phase_insert;
735
- CREATE TRIGGER session_follows_phase_insert AFTER INSERT ON topic_settings
736
- BEGIN
737
- UPDATE discussion_sessions SET status=CASE WHEN NEW.phase='concluded' THEN 'concluded' WHEN NEW.phase IN ('preparing','paused') THEN 'paused' ELSE 'active' END
738
- WHERE id=(SELECT MAX(id) FROM discussion_sessions WHERE room_id=NEW.room_id);
739
- END;
740
- DROP TRIGGER IF EXISTS session_follows_phase;
741
- CREATE TRIGGER session_follows_phase AFTER UPDATE OF phase ON topic_settings
742
- BEGIN
743
- UPDATE discussion_sessions SET status=CASE WHEN NEW.phase='concluded' THEN 'concluded' WHEN NEW.phase IN ('preparing','paused') THEN 'paused' ELSE 'active' END,
744
- ended_message_id=CASE WHEN NEW.phase='concluded' THEN COALESCE((SELECT MAX(id) FROM messages WHERE room_id=NEW.room_id),0) ELSE 0 END
745
- WHERE id=(SELECT MAX(id) FROM discussion_sessions WHERE room_id=NEW.room_id);
746
- END;
747
- -- What a discussion concluded with: the room keeps one shared state and brief that the next discussion overwrites, so
748
- -- ending a discussion copies them, with why it ended, onto its session; a state or brief last written before it started
749
- -- belongs to an earlier discussion and is left out. Reopening and ending again replaces the copy.
750
- CREATE TABLE IF NOT EXISTS discussion_conclusions (
751
- session_id INTEGER PRIMARY KEY REFERENCES discussion_sessions(id) ON DELETE CASCADE,
752
- room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
753
- stop_reason TEXT NOT NULL DEFAULT '',
754
- max_rounds INTEGER NOT NULL DEFAULT 0,
755
- max_minutes INTEGER NOT NULL DEFAULT 0,
756
- brief TEXT NOT NULL DEFAULT '',
757
- state TEXT NOT NULL DEFAULT '',
758
- state_until INTEGER NOT NULL DEFAULT 0,
759
- concluded_at TEXT NOT NULL
760
- );
761
- DROP TRIGGER IF EXISTS conclusion_snapshot;
762
- CREATE TRIGGER conclusion_snapshot AFTER UPDATE OF phase ON topic_settings
763
- WHEN NEW.phase='concluded' AND OLD.phase<>'concluded'
764
- BEGIN
765
- INSERT OR REPLACE INTO discussion_conclusions (session_id,room_id,stop_reason,max_rounds,max_minutes,brief,state,state_until,concluded_at)
766
- SELECT s.id,s.room_id,COALESCE(l.stop_reason,''),COALESCE(l.max_rounds,0),COALESCE(l.max_minutes,0),CASE WHEN b.until>s.started_message_id THEN b.text ELSE '' END,
767
- CASE WHEN r.state_until>s.started_message_id THEN r.state_text ELSE '' END,CASE WHEN r.state_until>s.started_message_id THEN r.state_until ELSE 0 END,
768
- strftime('%Y-%m-%dT%H:%M:%SZ','now')
769
- FROM discussion_sessions AS s JOIN rooms AS r ON r.id=s.room_id
770
- LEFT JOIN discussion_limits AS l ON l.room_id=s.room_id LEFT JOIN room_briefs AS b ON b.room_id=s.room_id
771
- WHERE s.id=(SELECT MAX(id) FROM discussion_sessions WHERE room_id=NEW.room_id);
772
- END;
773
- -- Discussions that ended before snapshots: only the latest still matches the room's state, so only it gets one. Safe to repeat.
774
- INSERT OR IGNORE INTO discussion_conclusions (session_id,room_id,stop_reason,max_rounds,max_minutes,brief,state,state_until,concluded_at)
775
- SELECT s.id,s.room_id,COALESCE(l.stop_reason,''),COALESCE(l.max_rounds,0),COALESCE(l.max_minutes,0),CASE WHEN b.until>s.started_message_id THEN b.text ELSE '' END,
776
- CASE WHEN r.state_until>s.started_message_id THEN r.state_text ELSE '' END,CASE WHEN r.state_until>s.started_message_id THEN r.state_until ELSE 0 END,''
777
- FROM discussion_sessions AS s JOIN rooms AS r ON r.id=s.room_id
778
- LEFT JOIN discussion_limits AS l ON l.room_id=s.room_id LEFT JOIN room_briefs AS b ON b.room_id=s.room_id
779
- WHERE s.status='concluded' AND s.id=(SELECT MAX(id) FROM discussion_sessions WHERE room_id=s.room_id);
780
-
781
- -- The review phase behaved exactly like discussing and was retired; move rows left in it.
782
- UPDATE topic_settings SET phase='discussing' WHERE phase='reviewing';
783
- UPDATE discussion_runtime SET resume_phase='discussing' WHERE resume_phase='reviewing';
784
-
785
- -- A computer running the local service, paired by the site administrator. Only project names and ids and the
786
- -- configured client of each platform are published; directories and credentials stay on that computer.
787
- CREATE TABLE IF NOT EXISTS local_machines (
788
- id TEXT PRIMARY KEY,
789
- name TEXT NOT NULL,
790
- token_hash TEXT NOT NULL,
791
- projects TEXT NOT NULL DEFAULT '[]',
792
- clients TEXT NOT NULL DEFAULT '{}',
793
- seen_at INTEGER NOT NULL DEFAULT 0,
794
- created_at INTEGER NOT NULL
795
- );
796
- -- A room launch submitted from a device without the local service; the paired computer claims and runs it.
797
- -- The request (with the room's new credentials) is cleared once the launch finishes or fails.
798
- CREATE TABLE IF NOT EXISTS remote_launches (
799
- id TEXT PRIMARY KEY,
800
- machine_id TEXT NOT NULL,
801
- request TEXT NOT NULL DEFAULT '{}',
802
- status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','claimed','done','failed')),
803
- result TEXT NOT NULL DEFAULT '{}',
804
- error TEXT NOT NULL DEFAULT '',
805
- created_at INTEGER NOT NULL,
806
- updated_at INTEGER NOT NULL
807
- );
808
- CREATE INDEX IF NOT EXISTS remote_launches_machine ON remote_launches(machine_id, status);
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
+ DROP TRIGGER IF EXISTS reject_inactive_opinion;
119
+ CREATE TRIGGER reject_inactive_opinion BEFORE INSERT ON message_details
120
+ WHEN NEW.kind='opinion'
121
+ AND (SELECT author FROM messages WHERE id=NEW.message_id)<>'用户'
122
+ AND NOT EXISTS (SELECT 1 FROM message_details WHERE message_id=NEW.message_id)
123
+ AND EXISTS (SELECT 1 FROM messages JOIN topic_settings ON topic_settings.room_id=messages.room_id
124
+ WHERE messages.id=NEW.message_id AND topic_settings.phase IN ('paused','concluded'))
125
+ BEGIN
126
+ SELECT RAISE(ABORT, 'topic_inactive');
127
+ END;
128
+ CREATE TRIGGER IF NOT EXISTS reject_inactive_turn BEFORE INSERT ON discussion_turns
129
+ WHEN NOT EXISTS (SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND submission_id=NEW.submission_id)
130
+ AND EXISTS (SELECT 1 FROM topic_settings WHERE room_id=NEW.room_id AND phase IN ('paused','concluded'))
131
+ BEGIN
132
+ SELECT RAISE(ABORT, 'topic_inactive');
133
+ END;
134
+ CREATE TRIGGER IF NOT EXISTS reject_conclusion_with_open_turn BEFORE UPDATE OF phase ON topic_settings
135
+ WHEN NEW.phase='concluded'
136
+ AND EXISTS (SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'))
137
+ BEGIN
138
+ SELECT RAISE(ABORT, 'open_turn');
139
+ END;
140
+
141
+ CREATE TABLE IF NOT EXISTS room_floor (
142
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id),
143
+ holder TEXT,
144
+ next_speaker TEXT NOT NULL,
145
+ revision INTEGER NOT NULL DEFAULT 0
146
+ );
147
+ -- The speaking floor is retired: the table stays only so old databases and backups load, and nothing reads or writes it.
148
+ DROP TRIGGER IF EXISTS initialize_room_floor;
149
+ DROP TRIGGER IF EXISTS transfer_host_floor;
150
+ -- Site administrator account (single user) and its login sessions. Raw tokens and passwords are never stored.
151
+ CREATE TABLE IF NOT EXISTS site_admin (
152
+ id INTEGER PRIMARY KEY CHECK(id=1),
153
+ username TEXT NOT NULL,
154
+ password_hash TEXT NOT NULL,
155
+ avatar TEXT NOT NULL DEFAULT '',
156
+ failed_attempts INTEGER NOT NULL DEFAULT 0,
157
+ locked_until INTEGER NOT NULL DEFAULT 0,
158
+ created_at INTEGER NOT NULL
159
+ );
160
+ CREATE TABLE IF NOT EXISTS admin_sessions (
161
+ token_hash TEXT PRIMARY KEY,
162
+ created_at INTEGER NOT NULL
163
+ );
164
+
165
+ -- Free collaboration rooms. Rooms without a row are brainstorm rooms and keep every rule below.
166
+ CREATE TABLE IF NOT EXISTS room_modes (
167
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
168
+ mode TEXT NOT NULL CHECK(mode IN ('free','brainstorm')),
169
+ control_hash TEXT NOT NULL DEFAULT ''
170
+ );
171
+ -- Replaced on every start so existing databases drop every speaking-floor rule.
172
+ DROP TRIGGER IF EXISTS require_message_floor;
173
+ DROP TRIGGER IF EXISTS return_message_floor;
174
+ DROP TRIGGER IF EXISTS require_host_floor;
175
+ DROP TRIGGER IF EXISTS nominate_expert_floor;
176
+ DROP TRIGGER IF EXISTS cancel_turn_floor;
177
+ CREATE TRIGGER IF NOT EXISTS require_opinion_turn BEFORE INSERT ON message_details
178
+ WHEN NEW.kind='opinion' AND NEW.turn_id IS NULL
179
+ AND NOT EXISTS (SELECT 1 FROM message_details WHERE message_id=NEW.message_id)
180
+ AND EXISTS (SELECT 1 FROM messages WHERE id=NEW.message_id AND author<>'用户')
181
+ BEGIN
182
+ SELECT RAISE(ABORT, 'floor_turn_required');
183
+ END;
184
+
185
+ -- Runtime control is opt-in. Legacy rooms have no runtime row and keep manual entry.
186
+ CREATE TABLE IF NOT EXISTS discussion_limits (
187
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
188
+ max_rounds INTEGER NOT NULL DEFAULT 0,
189
+ max_minutes INTEGER NOT NULL DEFAULT 0,
190
+ execution_minutes INTEGER NOT NULL DEFAULT 0,
191
+ on_limit TEXT NOT NULL DEFAULT 'pause',
192
+ summary_minutes INTEGER NOT NULL DEFAULT 5,
193
+ started_at INTEGER NOT NULL DEFAULT 0,
194
+ closing_at INTEGER NOT NULL DEFAULT 0,
195
+ stop_reason TEXT NOT NULL DEFAULT '',
196
+ revision INTEGER NOT NULL DEFAULT 0
197
+ );
198
+ INSERT OR IGNORE INTO discussion_limits(room_id,execution_minutes) SELECT id,0 FROM rooms;
199
+ DROP TRIGGER IF EXISTS limits_room_created;
200
+ CREATE TRIGGER limits_room_created AFTER INSERT ON rooms
201
+ BEGIN
202
+ INSERT INTO discussion_limits(room_id,execution_minutes) VALUES (NEW.id,0);
203
+ END;
204
+ CREATE TRIGGER IF NOT EXISTS limits_topic_started AFTER INSERT ON topic_settings
205
+ WHEN NEW.phase IN ('initial','discussing')
206
+ BEGIN
207
+ UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
208
+ END;
209
+ CREATE TRIGGER IF NOT EXISTS limits_phase_started AFTER UPDATE OF phase ON topic_settings
210
+ WHEN NEW.phase IN ('initial','discussing') AND NEW.phase<>OLD.phase
211
+ BEGIN
212
+ UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
213
+ END;
214
+ CREATE TRIGGER IF NOT EXISTS limits_turn_started AFTER INSERT ON discussion_turns
215
+ BEGIN
216
+ UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
217
+ END;
218
+ CREATE TABLE IF NOT EXISTS discussion_runtime (
219
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
220
+ mode TEXT NOT NULL CHECK(mode IN ('manual','automatic')),
221
+ run_id TEXT NOT NULL,
222
+ generation INTEGER NOT NULL DEFAULT 1,
223
+ status TEXT NOT NULL CHECK(status IN ('ready','running','paused','stopped','concluded')),
224
+ control_hash TEXT NOT NULL,
225
+ deadline INTEGER NOT NULL DEFAULT 0, -- 0 means no run duration limit; execution leases remain bounded.
226
+ revision INTEGER NOT NULL DEFAULT 1,
227
+ resume_phase TEXT NOT NULL DEFAULT 'initial',
228
+ updated_at TEXT NOT NULL
229
+ );
230
+ CREATE TABLE IF NOT EXISTS discussion_events (
231
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
232
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
233
+ run_id TEXT NOT NULL,
234
+ generation INTEGER NOT NULL,
235
+ cause TEXT NOT NULL,
236
+ participant TEXT NOT NULL,
237
+ role TEXT NOT NULL CHECK(role IN ('host','expert')),
238
+ turn_id INTEGER,
239
+ status TEXT NOT NULL DEFAULT 'pending'
240
+ CHECK(status IN ('pending','dispatched','started','completed','cancelled','failed')),
241
+ token_hash TEXT NOT NULL DEFAULT '',
242
+ session_id TEXT NOT NULL DEFAULT '',
243
+ lease_until INTEGER NOT NULL DEFAULT 0,
244
+ started_at INTEGER NOT NULL DEFAULT 0,
245
+ result TEXT NOT NULL DEFAULT '',
246
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
247
+ UNIQUE(room_id,run_id,generation,cause)
248
+ );
249
+ CREATE INDEX IF NOT EXISTS events_room_cursor ON discussion_events(room_id,id);
250
+ CREATE UNIQUE INDEX IF NOT EXISTS one_active_execution ON discussion_events(room_id)
251
+ WHERE status IN ('dispatched','started');
252
+ -- Stage timestamps for the progress panel; old events simply have no timing row.
253
+ CREATE TABLE IF NOT EXISTS event_timings (
254
+ event_id INTEGER PRIMARY KEY REFERENCES discussion_events(id) ON DELETE CASCADE,
255
+ dispatched_at INTEGER NOT NULL DEFAULT 0,
256
+ finished_at INTEGER NOT NULL DEFAULT 0
257
+ );
258
+ -- Freeze dispatch-to-publication time independently of the recent execution list.
259
+ CREATE TABLE IF NOT EXISTS message_timings (
260
+ message_id INTEGER PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
261
+ elapsed_seconds INTEGER NOT NULL CHECK(elapsed_seconds>=0)
262
+ );
263
+ -- Only completed expert events identify a historical message unambiguously.
264
+ INSERT INTO message_timings(message_id,elapsed_seconds)
265
+ SELECT m.id,MAX(0,CAST(strftime('%s',m.created_at) AS INTEGER)-t.dispatched_at)
266
+ FROM messages AS m JOIN discussion_events AS e
267
+ ON e.room_id=m.room_id AND e.result='message:'||m.id AND e.participant=m.author
268
+ JOIN event_timings AS t ON t.event_id=e.id
269
+ WHERE m.source IN ('cli','api') AND e.role='expert' AND e.status='completed'
270
+ AND e.started_at>0 AND t.dispatched_at>0 AND t.finished_at>0
271
+ ON CONFLICT(message_id) DO NOTHING;
272
+ CREATE TRIGGER IF NOT EXISTS record_event_timing AFTER UPDATE OF status ON discussion_events
273
+ WHEN OLD.status<>NEW.status
274
+ BEGIN
275
+ -- Avoid OR IGNORE: an outer statement's conflict policy would override it.
276
+ INSERT INTO event_timings(event_id) SELECT NEW.id
277
+ WHERE NOT EXISTS(SELECT 1 FROM event_timings WHERE event_id=NEW.id);
278
+ UPDATE event_timings SET dispatched_at=CAST(strftime('%s','now') AS INTEGER)
279
+ WHERE event_id=NEW.id AND NEW.status='dispatched';
280
+ UPDATE event_timings SET finished_at=CAST(strftime('%s','now') AS INTEGER)
281
+ WHERE event_id=NEW.id AND NEW.status IN ('completed','cancelled','failed');
282
+ END;
283
+ -- A failed assertion rolls back the whole D1 batch, including concurrent writes.
284
+ CREATE TABLE IF NOT EXISTS runtime_assertions (
285
+ id TEXT PRIMARY KEY, allowed INTEGER NOT NULL CHECK(allowed=1)
286
+ );
287
+ CREATE TRIGGER IF NOT EXISTS runtime_generation AFTER UPDATE ON discussion_runtime
288
+ WHEN OLD.generation<>NEW.generation OR OLD.status<>NEW.status
289
+ BEGIN
290
+ UPDATE discussion_events SET status='cancelled' WHERE room_id=NEW.room_id
291
+ AND status IN ('pending','dispatched','started')
292
+ AND (generation<>NEW.generation OR NEW.status<>'running');
293
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
294
+ SELECT NEW.room_id,NEW.run_id,NEW.generation,'start:'||NEW.generation,
295
+ COALESCE(turns.expert,settings.host),CASE WHEN turns.id IS NULL THEN 'host' ELSE 'expert' END,turns.id
296
+ FROM topic_settings AS settings LEFT JOIN discussion_turns AS turns
297
+ ON turns.room_id=settings.room_id AND turns.status IN ('pending','notified')
298
+ WHERE settings.room_id=NEW.room_id AND NEW.status='running';
299
+ END;
300
+ DROP TRIGGER IF EXISTS runtime_turn_created;
301
+ CREATE TRIGGER runtime_turn_created AFTER INSERT ON discussion_turns
302
+ WHEN NEW.status<>'queued'
303
+ BEGIN
304
+ UPDATE discussion_events SET status='completed',result='turn:'||NEW.id
305
+ WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started');
306
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
307
+ SELECT NEW.room_id,run_id,generation,'turn:'||NEW.id,NEW.expert,'expert',NEW.id
308
+ FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running';
309
+ END;
310
+ -- Round plans: queued turns wait in id order. held_since marks the first unprocessed user action
311
+ -- (a message id, or the next id for a topic edit); while set, answers return to the host instead of relaying.
312
+ CREATE TABLE IF NOT EXISTS turn_relay (
313
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
314
+ held_since INTEGER NOT NULL DEFAULT 0
315
+ );
316
+ -- Both triggers below clear the hold only when no user message arrived after the running host execution began; the host cannot have read it.
317
+ DROP TRIGGER IF EXISTS relay_turn_promoted;
318
+ CREATE TRIGGER relay_turn_promoted AFTER UPDATE OF status ON discussion_turns
319
+ WHEN OLD.status='queued' AND NEW.status='pending'
320
+ BEGIN
321
+ UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id
322
+ 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'
323
+ WHERE m.room_id=NEW.room_id AND m.author='用户' AND CAST(strftime('%s',m.created_at) AS INTEGER)>e.started_at);
324
+ UPDATE discussion_events SET status='completed',result='turn:'||NEW.id
325
+ WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started');
326
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
327
+ SELECT NEW.room_id,run_id,generation,'turn:'||NEW.id,NEW.expert,'expert',NEW.id
328
+ FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running';
329
+ END;
330
+ -- A new arrangement is the host's response: it replaces any older plan and clears the hold.
331
+ DROP TRIGGER IF EXISTS relay_plan_replaced;
332
+ CREATE TRIGGER relay_plan_replaced AFTER INSERT ON discussion_turns
333
+ WHEN NEW.status<>'queued'
334
+ BEGIN
335
+ UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued' AND id<NEW.id;
336
+ UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id
337
+ 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'
338
+ WHERE m.room_id=NEW.room_id AND m.author='用户' AND CAST(strftime('%s',m.created_at) AS INTEGER)>e.started_at);
339
+ END;
340
+ CREATE TRIGGER IF NOT EXISTS relay_hold_user_message AFTER INSERT ON message_details
341
+ WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
342
+ BEGIN
343
+ INSERT INTO turn_relay(room_id) SELECT room_id FROM messages WHERE id=NEW.message_id
344
+ AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE turn_relay.room_id=messages.room_id);
345
+ UPDATE turn_relay SET held_since=NEW.message_id
346
+ WHERE room_id=(SELECT room_id FROM messages WHERE id=NEW.message_id) AND held_since=0;
347
+ END;
348
+ CREATE TRIGGER IF NOT EXISTS relay_hold_goal AFTER UPDATE OF goal ON rooms
349
+ WHEN NEW.goal<>OLD.goal
350
+ BEGIN
351
+ INSERT INTO turn_relay(room_id) SELECT NEW.id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.id);
352
+ UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.id)
353
+ WHERE room_id=NEW.id AND held_since=0;
354
+ END;
355
+ CREATE TRIGGER IF NOT EXISTS relay_hold_topic AFTER UPDATE OF background,boundaries,focus ON topic_settings
356
+ WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries OR NEW.focus<>OLD.focus
357
+ BEGIN
358
+ INSERT INTO turn_relay(room_id) SELECT NEW.room_id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id);
359
+ UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.room_id)
360
+ WHERE room_id=NEW.room_id AND held_since=0;
361
+ END;
362
+ -- Concluding, replacing the host or removing/changing an expert voids plans that have not started.
363
+ -- Adding experts preserves the current plan; the invitation transaction appends their opening turns.
364
+ CREATE TRIGGER IF NOT EXISTS relay_plan_concluded AFTER UPDATE OF phase ON topic_settings
365
+ WHEN NEW.phase='concluded'
366
+ BEGIN
367
+ UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
368
+ END;
369
+ DROP TRIGGER IF EXISTS relay_plan_participants;
370
+ CREATE TRIGGER relay_plan_participants AFTER UPDATE OF host,experts ON topic_settings
371
+ WHEN NEW.host<>OLD.host OR EXISTS (
372
+ SELECT 1 FROM json_each(OLD.experts) AS previous
373
+ WHERE NOT EXISTS (SELECT 1 FROM json_each(NEW.experts) AS current
374
+ WHERE current.key=previous.key AND current.value=previous.value)
375
+ )
376
+ BEGIN
377
+ UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
378
+ END;
379
+ DROP TRIGGER IF EXISTS runtime_turn_answered;
380
+ -- An answer relays to the next queued turn unless the user has acted since the host last arranged.
381
+ CREATE TRIGGER runtime_turn_answered AFTER UPDATE OF status ON discussion_turns
382
+ WHEN NEW.status='answered' AND OLD.status<>'answered'
383
+ BEGIN
384
+ UPDATE discussion_events SET status='completed',result='message:'||NEW.answer_id
385
+ WHERE room_id=NEW.room_id AND turn_id=NEW.id AND status IN ('pending','dispatched','started');
386
+ UPDATE discussion_turns SET status='pending'
387
+ WHERE id=(SELECT MIN(id) FROM discussion_turns WHERE room_id=NEW.room_id AND status='queued')
388
+ AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id AND held_since>0)
389
+ AND NOT EXISTS(SELECT 1 FROM topic_settings WHERE room_id=NEW.room_id AND phase IN ('paused','concluded'));
390
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
391
+ SELECT NEW.room_id,run_id,generation,'answer:'||NEW.id,host,'host'
392
+ FROM discussion_runtime JOIN topic_settings USING(room_id)
393
+ WHERE room_id=NEW.room_id AND status='running'
394
+ AND NOT EXISTS(SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'));
395
+ END;
396
+ DROP TRIGGER IF EXISTS runtime_topic_stop;
397
+ CREATE TRIGGER runtime_topic_stop AFTER UPDATE OF phase ON topic_settings
398
+ WHEN NEW.phase IN ('paused','concluded') AND OLD.phase<>NEW.phase
399
+ BEGIN
400
+ UPDATE discussion_events SET status='completed',result='concluded' WHERE room_id=NEW.room_id
401
+ AND role='host' AND status='started' AND NEW.phase='concluded';
402
+ UPDATE discussion_runtime SET status=NEW.phase,generation=generation+1,revision=revision+1,
403
+ resume_phase=CASE WHEN OLD.phase IN ('initial','discussing') THEN OLD.phase ELSE resume_phase END,
404
+ updated_at=strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE room_id=NEW.room_id;
405
+ END;
406
+ CREATE TRIGGER IF NOT EXISTS runtime_user_message AFTER INSERT ON message_details
407
+ WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
408
+ BEGIN
409
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
410
+ SELECT messages.room_id,run_id,generation,'user:'||NEW.message_id,host,'host'
411
+ FROM messages JOIN discussion_runtime ON discussion_runtime.room_id=messages.room_id
412
+ JOIN topic_settings ON topic_settings.room_id=messages.room_id
413
+ WHERE messages.id=NEW.message_id AND status='running';
414
+ END;
415
+ CREATE TRIGGER IF NOT EXISTS runtime_turn_cancelled AFTER UPDATE OF status ON discussion_turns
416
+ WHEN NEW.status='cancelled' AND OLD.status<>'cancelled'
417
+ BEGIN
418
+ UPDATE discussion_events SET status='cancelled' WHERE room_id=NEW.room_id AND turn_id=NEW.id
419
+ AND status IN ('pending','dispatched','started');
420
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
421
+ SELECT NEW.room_id,run_id,generation,'cancel:'||NEW.id,host,'host'
422
+ FROM discussion_runtime JOIN topic_settings USING(room_id)
423
+ WHERE room_id=NEW.room_id AND status='running'
424
+ AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
425
+ END;
426
+ CREATE TRIGGER IF NOT EXISTS runtime_archive AFTER UPDATE OF archived ON room_directory
427
+ WHEN NEW.archived=1 AND OLD.archived<>NEW.archived
428
+ BEGIN
429
+ UPDATE discussion_runtime SET status='stopped',generation=generation+1,revision=revision+1
430
+ WHERE room_id=NEW.room_id;
431
+ END;
432
+ DROP TRIGGER IF EXISTS runtime_participants_changed;
433
+ -- A new host needs a new run. Experts joining or leaving keep the run going; the host is woken to re-plan.
434
+ CREATE TRIGGER runtime_participants_changed AFTER UPDATE OF host ON topic_settings
435
+ WHEN NEW.host<>OLD.host
436
+ BEGIN
437
+ UPDATE discussion_runtime SET status='paused',generation=generation+1,revision=revision+1
438
+ WHERE room_id=NEW.room_id AND status='running';
439
+ END;
440
+ DROP TRIGGER IF EXISTS runtime_experts_changed;
441
+ CREATE TRIGGER runtime_experts_changed AFTER UPDATE OF experts ON topic_settings
442
+ WHEN NEW.host=OLD.host AND NEW.experts<>OLD.experts
443
+ BEGIN
444
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
445
+ SELECT NEW.room_id,run_id,generation,'members:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),NEW.host,'host'
446
+ FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running'
447
+ AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started'))
448
+ AND NOT EXISTS(SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'));
449
+ END;
450
+ CREATE TRIGGER IF NOT EXISTS runtime_goal_changed AFTER UPDATE OF goal ON rooms
451
+ WHEN NEW.goal<>OLD.goal
452
+ BEGIN
453
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
454
+ SELECT NEW.id,run_id,generation,'goal:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),host,'host'
455
+ FROM discussion_runtime JOIN topic_settings USING(room_id)
456
+ WHERE room_id=NEW.id AND status='running'
457
+ AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.id AND role='host' AND status='started');
458
+ END;
459
+ CREATE TRIGGER IF NOT EXISTS runtime_constraints_changed AFTER UPDATE OF background,boundaries ON topic_settings
460
+ WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries
461
+ BEGIN
462
+ INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
463
+ SELECT NEW.room_id,run_id,generation,'constraints:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),NEW.host,'host'
464
+ FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running'
465
+ AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
466
+ END;
467
+ -- Paused, preparing or concluded time is idle: it is excluded from elapsed time and total-duration deadlines.
468
+ CREATE TABLE IF NOT EXISTS discussion_pauses (
469
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
470
+ paused_at INTEGER NOT NULL DEFAULT 0,
471
+ paused_seconds INTEGER NOT NULL DEFAULT 0
472
+ );
473
+ INSERT INTO discussion_pauses(room_id,paused_at)
474
+ SELECT room_id,CASE WHEN phase IN ('paused','concluded') THEN COALESCE((SELECT CAST(strftime('%s',updated_at) AS INTEGER)
475
+ FROM discussion_runtime WHERE discussion_runtime.room_id=topic_settings.room_id AND status<>'running'),
476
+ CAST(strftime('%s','now') AS INTEGER)) ELSE 0 END
477
+ FROM topic_settings WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE discussion_pauses.room_id=topic_settings.room_id);
478
+ CREATE TRIGGER IF NOT EXISTS pause_clock_stopped AFTER UPDATE OF phase ON topic_settings
479
+ WHEN OLD.phase IN ('initial','discussing') AND NEW.phase NOT IN ('initial','discussing')
480
+ BEGIN
481
+ INSERT INTO discussion_pauses(room_id) SELECT NEW.room_id
482
+ WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE room_id=NEW.room_id);
483
+ UPDATE discussion_pauses SET paused_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND paused_at=0;
484
+ END;
485
+ CREATE TRIGGER IF NOT EXISTS pause_clock_resumed AFTER UPDATE OF phase ON topic_settings
486
+ WHEN NEW.phase IN ('initial','discussing') AND OLD.phase NOT IN ('initial','discussing')
487
+ BEGIN
488
+ UPDATE discussion_pauses SET paused_seconds=paused_seconds+MAX(0,CAST(strftime('%s','now') AS INTEGER)-paused_at),paused_at=0
489
+ WHERE room_id=NEW.room_id AND paused_at>0;
490
+ END;
491
+ -- Both room kinds register members; free rooms additionally persist requests and executions.
492
+ CREATE TABLE IF NOT EXISTS room_members (
493
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
494
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
495
+ name TEXT NOT NULL,
496
+ adapter TEXT NOT NULL DEFAULT '',
497
+ role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('member','host')),
498
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','away','removed')),
499
+ token_hash TEXT NOT NULL,
500
+ read_until INTEGER NOT NULL DEFAULT 0,
501
+ created_at TEXT NOT NULL,
502
+ access TEXT NOT NULL DEFAULT 'runner',
503
+ session_mode TEXT NOT NULL DEFAULT 'new'
504
+ );
505
+ CREATE UNIQUE INDEX IF NOT EXISTS member_name ON room_members(room_id,name) WHERE status<>'removed';
506
+ CREATE UNIQUE INDEX IF NOT EXISTS member_credential ON room_members(room_id,token_hash);
507
+ -- A manual member's own software session, reported by its CLI on posting; the local service uses it to take the member over.
508
+ CREATE TABLE IF NOT EXISTS member_sessions (
509
+ member_id INTEGER PRIMARY KEY REFERENCES room_members(id) ON DELETE CASCADE,
510
+ room_id TEXT NOT NULL,
511
+ session_id TEXT NOT NULL,
512
+ client TEXT NOT NULL
513
+ );
514
+ -- A manual member's own registration; it is bound to the credential it used, so a re-issued credential must register again.
515
+ CREATE TABLE IF NOT EXISTS member_registrations (
516
+ member_id INTEGER PRIMARY KEY REFERENCES room_members(id) ON DELETE CASCADE,
517
+ room_id TEXT NOT NULL,
518
+ token_hash TEXT NOT NULL,
519
+ registered_at TEXT NOT NULL
520
+ );
521
+ CREATE TABLE IF NOT EXISTS member_executions (
522
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
523
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
524
+ member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
525
+ client_id TEXT NOT NULL,
526
+ status TEXT NOT NULL DEFAULT 'dispatched'
527
+ CHECK(status IN ('dispatched','started','completed','cancelled','failed')),
528
+ session_id TEXT NOT NULL DEFAULT '',
529
+ lease_until INTEGER NOT NULL DEFAULT 0,
530
+ created_at INTEGER NOT NULL,
531
+ finished_at INTEGER NOT NULL DEFAULT 0,
532
+ result TEXT NOT NULL DEFAULT '',
533
+ changes TEXT NOT NULL DEFAULT '',
534
+ cwd TEXT NOT NULL DEFAULT '',
535
+ activity TEXT NOT NULL DEFAULT '',
536
+ activity_at INTEGER NOT NULL DEFAULT 0,
537
+ UNIQUE(room_id,client_id)
538
+ );
539
+ -- What the owner wrote about each member (strengths, division of work), by name so it survives leaving and rejoining.
540
+ CREATE TABLE IF NOT EXISTS member_descriptions (
541
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
542
+ name TEXT NOT NULL,
543
+ description TEXT NOT NULL,
544
+ updated_at INTEGER NOT NULL,
545
+ PRIMARY KEY(room_id,name)
546
+ );
547
+ -- Progress notes the runner saw a member's software write between tool calls; numbered per execution so a retried renewal adds nothing.
548
+ CREATE TABLE IF NOT EXISTS execution_notes (
549
+ execution_id INTEGER NOT NULL REFERENCES member_executions(id) ON DELETE CASCADE,
550
+ seq INTEGER NOT NULL,
551
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
552
+ text TEXT NOT NULL,
553
+ created_at INTEGER NOT NULL,
554
+ PRIMARY KEY(execution_id,seq)
555
+ );
556
+ -- Phases the runner saw a member's software go through in one execution (a free execution or a discussion event), as
557
+ -- millisecond offsets from its delivery; numbered per execution, and a resent segment keeps the later end. Not part of backups.
558
+ CREATE TABLE IF NOT EXISTS execution_segments (
559
+ source TEXT NOT NULL CHECK(source IN ('free','discussion')),
560
+ execution_id INTEGER NOT NULL,
561
+ seq INTEGER NOT NULL,
562
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
563
+ phase TEXT NOT NULL,
564
+ tool TEXT NOT NULL DEFAULT '',
565
+ start_ms INTEGER NOT NULL,
566
+ end_ms INTEGER NOT NULL,
567
+ PRIMARY KEY(source,execution_id,seq)
568
+ );
569
+ CREATE INDEX IF NOT EXISTS execution_segments_room ON execution_segments(room_id);
570
+ -- One live execution per member: different members may run in parallel, one session never twice.
571
+ CREATE UNIQUE INDEX IF NOT EXISTS one_member_execution ON member_executions(member_id)
572
+ WHERE status IN ('dispatched','started');
573
+ CREATE TABLE IF NOT EXISTS member_requests (
574
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
575
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
576
+ member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
577
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
578
+ status TEXT NOT NULL DEFAULT 'pending'
579
+ CHECK(status IN ('pending','delivering','completed','cancelled','failed')),
580
+ execution_id INTEGER REFERENCES member_executions(id),
581
+ answer_id INTEGER REFERENCES messages(id),
582
+ created_at INTEGER NOT NULL,
583
+ chain_id INTEGER NOT NULL DEFAULT 0,
584
+ batch_id INTEGER NOT NULL DEFAULT 0,
585
+ purpose TEXT NOT NULL DEFAULT 'ask',
586
+ created_by TEXT NOT NULL DEFAULT 'user',
587
+ UNIQUE(message_id,member_id)
588
+ );
589
+ CREATE INDEX IF NOT EXISTS member_requests_open ON member_requests(member_id,status);
590
+ -- Discussion options chosen at creation. A room without a row predates the option and keeps named follow-ups.
591
+ CREATE TABLE IF NOT EXISTS discussion_options (
592
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
593
+ named_followup INTEGER NOT NULL DEFAULT 0 CHECK(named_followup IN (0,1))
594
+ );
595
+ -- Program-controlled round budget: rounds are expert opinions per active expert counted from `base`, so a roster that skips
596
+ -- someone cannot stall the limit. `cap` is the current segment's round limit (0 = discussion_limits.max_rounds); `extra` is the
597
+ -- total rounds the user appended. A room without a row predates the budget and keeps the per-roster round count.
598
+ CREATE TABLE IF NOT EXISTS round_budgets (
599
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
600
+ base INTEGER NOT NULL DEFAULT 0,
601
+ cap INTEGER NOT NULL DEFAULT 0,
602
+ extra INTEGER NOT NULL DEFAULT 0
603
+ );
604
+ -- Visible record of each smart-scheduling decision in a discussion; it is not a message and moves no round or floor.
605
+ CREATE TABLE IF NOT EXISTS scheduler_decisions (
606
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
607
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
608
+ after_message_id INTEGER NOT NULL DEFAULT 0,
609
+ kind TEXT NOT NULL CHECK(kind IN ('interjection','round')),
610
+ label TEXT NOT NULL,
611
+ confidence REAL,
612
+ applied INTEGER NOT NULL DEFAULT 0 CHECK(applied IN (0,1)),
613
+ provider TEXT NOT NULL,
614
+ created_at TEXT NOT NULL
615
+ );
616
+ CREATE INDEX IF NOT EXISTS scheduler_decisions_room ON scheduler_decisions(room_id,id);
617
+ -- What the decision model was asked and chose, shown folded under the decision; not part of backups.
618
+ CREATE TABLE IF NOT EXISTS scheduler_decision_details (
619
+ decision_id INTEGER PRIMARY KEY REFERENCES scheduler_decisions(id) ON DELETE CASCADE,
620
+ detail TEXT NOT NULL
621
+ );
622
+ -- The owner's verdict on one decision; a 'bad' one may also say who should have answered and why.
623
+ -- Kept apart from the decision so older databases need no column change.
624
+ CREATE TABLE IF NOT EXISTS scheduler_feedback (
625
+ decision_id INTEGER PRIMARY KEY REFERENCES scheduler_decisions(id) ON DELETE CASCADE,
626
+ rating TEXT NOT NULL CHECK(rating IN ('good','bad')),
627
+ should_be TEXT NOT NULL DEFAULT '',
628
+ note TEXT NOT NULL DEFAULT '',
629
+ created_at TEXT NOT NULL
630
+ );
631
+ -- Messages the local service is still asking the decision model about; a transient marker, not part of backups.
632
+ CREATE TABLE IF NOT EXISTS scheduler_routing (
633
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
634
+ message_id INTEGER NOT NULL,
635
+ created_at INTEGER NOT NULL,
636
+ PRIMARY KEY (room_id, message_id)
637
+ );
638
+ -- Why a request was cancelled; a side table so older databases need no column change. Requests cancelled before it existed have no row.
639
+ CREATE TABLE IF NOT EXISTS request_cancel_reasons (
640
+ request_id INTEGER PRIMARY KEY REFERENCES member_requests(id) ON DELETE CASCADE,
641
+ reason TEXT NOT NULL CHECK(reason IN ('user_stop','member_away','member_removed','rerouted','no_reply','archived'))
642
+ );
643
+ -- What produced a decision (model, threshold, prompt and package versions), so ratings stay comparable across versions.
644
+ CREATE TABLE IF NOT EXISTS scheduler_decision_context (
645
+ decision_id INTEGER PRIMARY KEY REFERENCES scheduler_decisions(id) ON DELETE CASCADE,
646
+ context TEXT NOT NULL
647
+ );
648
+ -- The exact input the decision model saw (state and full questions with option descriptions); not part of backups.
649
+ CREATE TABLE IF NOT EXISTS scheduler_decision_inputs (
650
+ decision_id INTEGER PRIMARY KEY REFERENCES scheduler_decisions(id) ON DELETE CASCADE,
651
+ input TEXT NOT NULL
652
+ );
653
+ -- A self-contained copy of each decision the owner currently rates bad, kept when its room is deleted so prompts can be reviewed.
654
+ CREATE TABLE IF NOT EXISTS scheduler_feedback_cases (
655
+ decision_id INTEGER PRIMARY KEY,
656
+ room_id TEXT NOT NULL,
657
+ room_title TEXT NOT NULL,
658
+ snapshot TEXT NOT NULL,
659
+ should_be TEXT NOT NULL DEFAULT '',
660
+ note TEXT NOT NULL DEFAULT '',
661
+ rated_at TEXT NOT NULL
662
+ );
663
+ -- Smart scheduling's suggestion that another member answer an unnamed message. The host keeps the message;
664
+ -- the user may adopt before expires_at (waiting until the host's request ends for mode 'after'), otherwise it lapses.
665
+ CREATE TABLE IF NOT EXISTS member_recommendations (
666
+ message_id INTEGER PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
667
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
668
+ member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
669
+ provider TEXT NOT NULL,
670
+ confidence REAL NOT NULL,
671
+ created_at INTEGER NOT NULL,
672
+ expires_at INTEGER NOT NULL,
673
+ status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','waiting','adopted')),
674
+ mode TEXT NOT NULL DEFAULT ''
675
+ );
676
+ -- Ordinary members can suggest a response without creating work or spending a chain budget.
677
+ CREATE TABLE IF NOT EXISTS member_suggestions (
678
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
679
+ member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE,
680
+ PRIMARY KEY (message_id,member_id)
681
+ );
682
+ -- A host's dispatch: its named requests form one batch that hands back to the host once.
683
+ CREATE TABLE IF NOT EXISTS free_batches (
684
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
685
+ batch_id INTEGER NOT NULL,
686
+ chain_id INTEGER NOT NULL,
687
+ host_id INTEGER NOT NULL,
688
+ settled INTEGER NOT NULL DEFAULT 0,
689
+ PRIMARY KEY (room_id,batch_id)
690
+ );
691
+ -- Members the user named to answer one after another, in the room's speaking order: each gets its request once the one before
692
+ -- it ends. Asked rows stay so the speaking order can show who already answered; the owner may reorder rows not yet asked.
693
+ CREATE TABLE IF NOT EXISTS free_queue (
694
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
695
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
696
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
697
+ member_id INTEGER NOT NULL REFERENCES room_members(id) ON DELETE CASCADE
698
+ );
699
+ -- The owner's default speaking order (JSON list of member ids); members it lacks follow, the host first, then by joining order.
700
+ CREATE TABLE IF NOT EXISTS speaking_orders (
701
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
702
+ member_ids TEXT NOT NULL
703
+ );
704
+ -- Chains whose automatic progress stopped (budget reached or every dispatched member failed).
705
+ CREATE TABLE IF NOT EXISTS free_chains (
706
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
707
+ chain_id INTEGER NOT NULL,
708
+ reason TEXT NOT NULL,
709
+ PRIMARY KEY (room_id,chain_id)
710
+ );
711
+ -- A host's one-member dispatch that should come back to the host when it ends (several members always do).
712
+ CREATE TABLE IF NOT EXISTS free_handoffs (
713
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
714
+ batch_id INTEGER NOT NULL,
715
+ PRIMARY KEY (room_id,batch_id)
716
+ );
717
+ -- The owner's per-room chain budget; rooms without a row use the default.
718
+ CREATE TABLE IF NOT EXISTS free_settings (
719
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
720
+ max_auto_hops INTEGER NOT NULL
721
+ );
722
+ -- A brainstorm room started from a free room keeps its origin for the way back.
723
+ CREATE TABLE IF NOT EXISTS room_links (
724
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
725
+ parent_id TEXT NOT NULL,
726
+ created_at TEXT NOT NULL
727
+ );
728
+
729
+ -- A discussion is a switch on a room, not a room type: one thin session per started discussion. Its status mirrors the legacy
730
+ -- topic phase until the phase machinery is retired. Free rooms have none.
731
+ CREATE TABLE IF NOT EXISTS discussion_sessions (
732
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
733
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
734
+ goal TEXT NOT NULL,
735
+ status TEXT NOT NULL DEFAULT 'paused' CHECK(status IN ('active','paused','concluded')),
736
+ started_message_id INTEGER NOT NULL DEFAULT 0,
737
+ ended_message_id INTEGER NOT NULL DEFAULT 0,
738
+ created_at TEXT NOT NULL
739
+ );
740
+ CREATE UNIQUE INDEX IF NOT EXISTS one_open_session ON discussion_sessions(room_id) WHERE status IN ('active','paused');
741
+ -- Rooms that predate sessions: every non-free room with topic settings gets one, mapped from its phase. Safe to repeat.
742
+ INSERT INTO discussion_sessions (room_id,goal,status,started_message_id,ended_message_id,created_at)
743
+ SELECT rooms.id,rooms.goal,
744
+ CASE WHEN topic_settings.phase='concluded' THEN 'concluded' WHEN topic_settings.phase IN ('preparing','paused') THEN 'paused' ELSE 'active' END,
745
+ 0,CASE WHEN topic_settings.phase='concluded' THEN COALESCE((SELECT MAX(id) FROM messages WHERE room_id=rooms.id),0) ELSE 0 END,rooms.created_at
746
+ FROM rooms JOIN topic_settings ON topic_settings.room_id=rooms.id
747
+ WHERE NOT EXISTS (SELECT 1 FROM room_modes WHERE room_id=rooms.id AND mode='free')
748
+ AND NOT EXISTS (SELECT 1 FROM discussion_sessions WHERE room_id=rooms.id);
749
+ DROP TRIGGER IF EXISTS session_for_new_discussion;
750
+ CREATE TRIGGER session_for_new_discussion AFTER INSERT ON room_modes
751
+ WHEN NEW.mode='brainstorm' AND NOT EXISTS (SELECT 1 FROM discussion_sessions WHERE room_id=NEW.room_id)
752
+ BEGIN
753
+ INSERT INTO discussion_sessions (room_id,goal,status,created_at)
754
+ SELECT rooms.id,rooms.goal,
755
+ COALESCE((SELECT CASE WHEN phase='concluded' THEN 'concluded' WHEN phase IN ('preparing','paused') THEN 'paused' ELSE 'active' END
756
+ FROM topic_settings WHERE room_id=rooms.id),'paused'),rooms.created_at
757
+ FROM rooms WHERE rooms.id=NEW.room_id;
758
+ END;
759
+ DROP TRIGGER IF EXISTS session_follows_phase_insert;
760
+ CREATE TRIGGER session_follows_phase_insert AFTER INSERT ON topic_settings
761
+ BEGIN
762
+ UPDATE discussion_sessions SET status=CASE WHEN NEW.phase='concluded' THEN 'concluded' WHEN NEW.phase IN ('preparing','paused') THEN 'paused' ELSE 'active' END
763
+ WHERE id=(SELECT MAX(id) FROM discussion_sessions WHERE room_id=NEW.room_id);
764
+ END;
765
+ DROP TRIGGER IF EXISTS session_follows_phase;
766
+ CREATE TRIGGER session_follows_phase AFTER UPDATE OF phase ON topic_settings
767
+ BEGIN
768
+ UPDATE discussion_sessions SET status=CASE WHEN NEW.phase='concluded' THEN 'concluded' WHEN NEW.phase IN ('preparing','paused') THEN 'paused' ELSE 'active' END,
769
+ ended_message_id=CASE WHEN NEW.phase='concluded' THEN COALESCE((SELECT MAX(id) FROM messages WHERE room_id=NEW.room_id),0) ELSE 0 END
770
+ WHERE id=(SELECT MAX(id) FROM discussion_sessions WHERE room_id=NEW.room_id);
771
+ END;
772
+ -- What a discussion concluded with: the room keeps one shared state and brief that the next discussion overwrites, so
773
+ -- ending a discussion copies them, with why it ended, onto its session; a state or brief last written before it started
774
+ -- belongs to an earlier discussion and is left out. Reopening and ending again replaces the copy.
775
+ CREATE TABLE IF NOT EXISTS discussion_conclusions (
776
+ session_id INTEGER PRIMARY KEY REFERENCES discussion_sessions(id) ON DELETE CASCADE,
777
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
778
+ stop_reason TEXT NOT NULL DEFAULT '',
779
+ max_rounds INTEGER NOT NULL DEFAULT 0,
780
+ max_minutes INTEGER NOT NULL DEFAULT 0,
781
+ brief TEXT NOT NULL DEFAULT '',
782
+ state TEXT NOT NULL DEFAULT '',
783
+ state_until INTEGER NOT NULL DEFAULT 0,
784
+ concluded_at TEXT NOT NULL
785
+ );
786
+ DROP TRIGGER IF EXISTS conclusion_snapshot;
787
+ CREATE TRIGGER conclusion_snapshot AFTER UPDATE OF phase ON topic_settings
788
+ WHEN NEW.phase='concluded' AND OLD.phase<>'concluded'
789
+ BEGIN
790
+ INSERT OR REPLACE INTO discussion_conclusions (session_id,room_id,stop_reason,max_rounds,max_minutes,brief,state,state_until,concluded_at)
791
+ SELECT s.id,s.room_id,COALESCE(l.stop_reason,''),COALESCE(l.max_rounds,0),COALESCE(l.max_minutes,0),CASE WHEN b.until>s.started_message_id THEN b.text ELSE '' END,
792
+ CASE WHEN r.state_until>s.started_message_id THEN r.state_text ELSE '' END,CASE WHEN r.state_until>s.started_message_id THEN r.state_until ELSE 0 END,
793
+ strftime('%Y-%m-%dT%H:%M:%SZ','now')
794
+ FROM discussion_sessions AS s JOIN rooms AS r ON r.id=s.room_id
795
+ LEFT JOIN discussion_limits AS l ON l.room_id=s.room_id LEFT JOIN room_briefs AS b ON b.room_id=s.room_id
796
+ WHERE s.id=(SELECT MAX(id) FROM discussion_sessions WHERE room_id=NEW.room_id);
797
+ END;
798
+ -- Discussions that ended before snapshots: only the latest still matches the room's state, so only it gets one. Safe to repeat.
799
+ INSERT OR IGNORE INTO discussion_conclusions (session_id,room_id,stop_reason,max_rounds,max_minutes,brief,state,state_until,concluded_at)
800
+ SELECT s.id,s.room_id,COALESCE(l.stop_reason,''),COALESCE(l.max_rounds,0),COALESCE(l.max_minutes,0),CASE WHEN b.until>s.started_message_id THEN b.text ELSE '' END,
801
+ CASE WHEN r.state_until>s.started_message_id THEN r.state_text ELSE '' END,CASE WHEN r.state_until>s.started_message_id THEN r.state_until ELSE 0 END,''
802
+ FROM discussion_sessions AS s JOIN rooms AS r ON r.id=s.room_id
803
+ LEFT JOIN discussion_limits AS l ON l.room_id=s.room_id LEFT JOIN room_briefs AS b ON b.room_id=s.room_id
804
+ WHERE s.status='concluded' AND s.id=(SELECT MAX(id) FROM discussion_sessions WHERE room_id=s.room_id);
805
+
806
+ -- The review phase behaved exactly like discussing and was retired; move rows left in it.
807
+ UPDATE topic_settings SET phase='discussing' WHERE phase='reviewing';
808
+ UPDATE discussion_runtime SET resume_phase='discussing' WHERE resume_phase='reviewing';
809
+
810
+ -- A computer running the local service, paired by the site administrator. Only project names and ids and the
811
+ -- configured client of each platform are published; directories and credentials stay on that computer.
812
+ CREATE TABLE IF NOT EXISTS local_machines (
813
+ id TEXT PRIMARY KEY,
814
+ name TEXT NOT NULL,
815
+ token_hash TEXT NOT NULL,
816
+ projects TEXT NOT NULL DEFAULT '[]',
817
+ clients TEXT NOT NULL DEFAULT '{}',
818
+ seen_at INTEGER NOT NULL DEFAULT 0,
819
+ created_at INTEGER NOT NULL
820
+ );
821
+ -- A room launch submitted from a device without the local service; the paired computer claims and runs it.
822
+ -- The request (with the room's new credentials) is cleared once the launch finishes or fails.
823
+ CREATE TABLE IF NOT EXISTS remote_launches (
824
+ id TEXT PRIMARY KEY,
825
+ machine_id TEXT NOT NULL,
826
+ request TEXT NOT NULL DEFAULT '{}',
827
+ status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','claimed','done','failed')),
828
+ result TEXT NOT NULL DEFAULT '{}',
829
+ error TEXT NOT NULL DEFAULT '',
830
+ created_at INTEGER NOT NULL,
831
+ updated_at INTEGER NOT NULL
832
+ );
833
+ CREATE INDEX IF NOT EXISTS remote_launches_machine ON remote_launches(machine_id, status);