ai_co_discuss 0.2.99 → 0.3.0

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.
Files changed (39) hide show
  1. package/.agents/skills/discuss-host/SKILL.md +3 -3
  2. package/.agents/skills/discuss-host/references/discussion.md +16 -16
  3. package/.agents/skills/discuss-host/references/launch.md +1 -3
  4. package/.agents/skills/discuss-host/references/manual.md +14 -77
  5. package/.agents/skills/discuss-participant/SKILL.md +3 -3
  6. package/.agents/skills/discuss-participant/references/discussion.md +29 -27
  7. package/.agents/skills/discuss-participant/references/free.md +2 -2
  8. package/.agents/skills/discuss-participant/references/manual.md +8 -10
  9. package/.agents/skills/discuss-participant/references/writing.md +9 -9
  10. package/.github/SECURITY.md +1 -1
  11. package/README.md +3 -3
  12. package/README.zh-CN.md +3 -3
  13. package/backend/free_room.py +360 -68
  14. package/backend/room_import.py +75 -15
  15. package/backend/runtime.py +81 -169
  16. package/backend/schema.sql +136 -244
  17. package/backend/server.py +602 -166
  18. package/backend/session_room.py +919 -0
  19. package/docs/architecture.md +3 -2
  20. package/docs/en/architecture.md +4 -3
  21. package/docs/en/runner.md +3 -1
  22. package/docs/en/workflow.md +9 -6
  23. package/docs/runner.md +3 -1
  24. package/docs/workflow.md +11 -8
  25. package/package.json +2 -2
  26. package/runner/api_participant.mjs +47 -44
  27. package/runner/discuss.py +77 -58
  28. package/runner/discussion_adapters.mjs +6 -3
  29. package/runner/discussion_launch.mjs +98 -70
  30. package/runner/discussion_runner.mjs +38 -496
  31. package/runner/discussion_service.mjs +21 -74
  32. package/runner/discussion_sidecar.mjs +4 -13
  33. package/runner/expert_prompts.mjs +29 -21
  34. package/runner/free_runner.mjs +84 -19
  35. package/runner/session_driver.mjs +175 -0
  36. package/web/app.js +193 -128
  37. package/web/backup.js +1 -1
  38. package/web/modern.css +1 -1
  39. package/web/modern.js +5482 -6646
@@ -188,7 +188,7 @@ BEGIN
188
188
  SELECT RAISE(ABORT, 'floor_turn_required');
189
189
  END;
190
190
 
191
- -- Runtime control is opt-in. Legacy rooms have no runtime row and keep manual entry.
191
+ -- Legacy room-scoped limits remain for read-only history; new discussions use session-scoped limits below.
192
192
  CREATE TABLE IF NOT EXISTS discussion_limits (
193
193
  room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
194
194
  max_rounds INTEGER NOT NULL DEFAULT 0,
@@ -207,20 +207,10 @@ CREATE TRIGGER limits_room_created AFTER INSERT ON rooms
207
207
  BEGIN
208
208
  INSERT INTO discussion_limits(room_id,execution_minutes) VALUES (NEW.id,0);
209
209
  END;
210
- CREATE TRIGGER IF NOT EXISTS limits_topic_started AFTER INSERT ON topic_settings
211
- WHEN NEW.phase IN ('initial','discussing')
212
- BEGIN
213
- UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
214
- END;
215
- CREATE TRIGGER IF NOT EXISTS limits_phase_started AFTER UPDATE OF phase ON topic_settings
216
- WHEN NEW.phase IN ('initial','discussing') AND NEW.phase<>OLD.phase
217
- BEGIN
218
- UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
219
- END;
220
- CREATE TRIGGER IF NOT EXISTS limits_turn_started AFTER INSERT ON discussion_turns
221
- BEGIN
222
- UPDATE discussion_limits SET started_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND started_at=0;
223
- END;
210
+ DROP TRIGGER IF EXISTS limits_topic_started;
211
+ DROP TRIGGER IF EXISTS limits_phase_started;
212
+ DROP TRIGGER IF EXISTS limits_turn_started;
213
+ -- Legacy event-based orchestration tables remain for historical reads; the request-based session is authoritative.
224
214
  CREATE TABLE IF NOT EXISTS discussion_runtime (
225
215
  room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
226
216
  mode TEXT NOT NULL CHECK(mode IN ('manual','automatic')),
@@ -275,202 +265,34 @@ INSERT INTO message_timings(message_id,elapsed_seconds)
275
265
  WHERE m.source IN ('cli','api') AND e.role='expert' AND e.status='completed'
276
266
  AND e.started_at>0 AND t.dispatched_at>0 AND t.finished_at>0
277
267
  ON CONFLICT(message_id) DO NOTHING;
278
- CREATE TRIGGER IF NOT EXISTS record_event_timing AFTER UPDATE OF status ON discussion_events
279
- WHEN OLD.status<>NEW.status
280
- BEGIN
281
- -- Avoid OR IGNORE: an outer statement's conflict policy would override it.
282
- INSERT INTO event_timings(event_id) SELECT NEW.id
283
- WHERE NOT EXISTS(SELECT 1 FROM event_timings WHERE event_id=NEW.id);
284
- UPDATE event_timings SET dispatched_at=CAST(strftime('%s','now') AS INTEGER)
285
- WHERE event_id=NEW.id AND NEW.status='dispatched';
286
- UPDATE event_timings SET finished_at=CAST(strftime('%s','now') AS INTEGER)
287
- WHERE event_id=NEW.id AND NEW.status IN ('completed','cancelled','failed');
288
- END;
268
+ DROP TRIGGER IF EXISTS record_event_timing;
289
269
  -- A failed assertion rolls back the whole D1 batch, including concurrent writes.
290
270
  CREATE TABLE IF NOT EXISTS runtime_assertions (
291
271
  id TEXT PRIMARY KEY, allowed INTEGER NOT NULL CHECK(allowed=1)
292
272
  );
293
- CREATE TRIGGER IF NOT EXISTS runtime_generation AFTER UPDATE ON discussion_runtime
294
- WHEN OLD.generation<>NEW.generation OR OLD.status<>NEW.status
295
- BEGIN
296
- UPDATE discussion_events SET status='cancelled' WHERE room_id=NEW.room_id
297
- AND status IN ('pending','dispatched','started')
298
- AND (generation<>NEW.generation OR NEW.status<>'running');
299
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
300
- SELECT NEW.room_id,NEW.run_id,NEW.generation,'start:'||NEW.generation,
301
- COALESCE(turns.expert,settings.host),CASE WHEN turns.id IS NULL THEN 'host' ELSE 'expert' END,turns.id
302
- FROM topic_settings AS settings LEFT JOIN discussion_turns AS turns
303
- ON turns.room_id=settings.room_id AND turns.status IN ('pending','notified')
304
- WHERE settings.room_id=NEW.room_id AND NEW.status='running';
305
- END;
273
+ DROP TRIGGER IF EXISTS runtime_generation;
306
274
  DROP TRIGGER IF EXISTS runtime_turn_created;
307
- CREATE TRIGGER runtime_turn_created AFTER INSERT ON discussion_turns
308
- WHEN NEW.status<>'queued'
309
- BEGIN
310
- UPDATE discussion_events SET status='completed',result='turn:'||NEW.id
311
- WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started');
312
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
313
- SELECT NEW.room_id,run_id,generation,'turn:'||NEW.id,NEW.expert,'expert',NEW.id
314
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running';
315
- END;
316
- -- Round plans: queued turns wait in id order. held_since marks the first unprocessed user action
317
- -- (a message id, or the next id for a topic edit); while set, answers return to the host instead of relaying.
275
+ -- Retired relay state is preserved for old databases and historical reads only.
318
276
  CREATE TABLE IF NOT EXISTS turn_relay (
319
277
  room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
320
278
  held_since INTEGER NOT NULL DEFAULT 0
321
279
  );
322
- -- Both triggers below clear the hold only when no user message arrived after the running host execution began; the host cannot have read it.
323
280
  DROP TRIGGER IF EXISTS relay_turn_promoted;
324
- CREATE TRIGGER relay_turn_promoted AFTER UPDATE OF status ON discussion_turns
325
- WHEN OLD.status='queued' AND NEW.status='pending'
326
- BEGIN
327
- UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id
328
- 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'
329
- WHERE m.room_id=NEW.room_id AND m.author='用户' AND CAST(strftime('%s',m.created_at) AS INTEGER)>e.started_at);
330
- UPDATE discussion_events SET status='completed',result='turn:'||NEW.id
331
- WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started');
332
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role,turn_id)
333
- SELECT NEW.room_id,run_id,generation,'turn:'||NEW.id,NEW.expert,'expert',NEW.id
334
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running';
335
- END;
336
- -- A new arrangement is the host's response: it replaces any older plan and clears the hold.
337
281
  DROP TRIGGER IF EXISTS relay_plan_replaced;
338
- CREATE TRIGGER relay_plan_replaced AFTER INSERT ON discussion_turns
339
- WHEN NEW.status<>'queued'
340
- BEGIN
341
- UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued' AND id<NEW.id;
342
- UPDATE turn_relay SET held_since=0 WHERE room_id=NEW.room_id
343
- 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'
344
- WHERE m.room_id=NEW.room_id AND m.author='用户' AND CAST(strftime('%s',m.created_at) AS INTEGER)>e.started_at);
345
- END;
346
- CREATE TRIGGER IF NOT EXISTS relay_hold_user_message AFTER INSERT ON message_details
347
- WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
348
- BEGIN
349
- INSERT INTO turn_relay(room_id) SELECT room_id FROM messages WHERE id=NEW.message_id
350
- AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE turn_relay.room_id=messages.room_id);
351
- UPDATE turn_relay SET held_since=NEW.message_id
352
- WHERE room_id=(SELECT room_id FROM messages WHERE id=NEW.message_id) AND held_since=0;
353
- END;
354
- CREATE TRIGGER IF NOT EXISTS relay_hold_goal AFTER UPDATE OF goal ON rooms
355
- WHEN NEW.goal<>OLD.goal
356
- BEGIN
357
- INSERT INTO turn_relay(room_id) SELECT NEW.id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.id);
358
- UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.id)
359
- WHERE room_id=NEW.id AND held_since=0;
360
- END;
361
- CREATE TRIGGER IF NOT EXISTS relay_hold_topic AFTER UPDATE OF background,boundaries,focus ON topic_settings
362
- WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries OR NEW.focus<>OLD.focus
363
- BEGIN
364
- INSERT INTO turn_relay(room_id) SELECT NEW.room_id WHERE NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id);
365
- UPDATE turn_relay SET held_since=(SELECT COALESCE(MAX(id),0)+1 FROM messages WHERE room_id=NEW.room_id)
366
- WHERE room_id=NEW.room_id AND held_since=0;
367
- END;
368
- -- Concluding, replacing the host or removing/changing an expert voids plans that have not started.
369
- -- Adding experts preserves the current plan; the invitation transaction appends their opening turns.
370
- CREATE TRIGGER IF NOT EXISTS relay_plan_concluded AFTER UPDATE OF phase ON topic_settings
371
- WHEN NEW.phase='concluded'
372
- BEGIN
373
- UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
374
- END;
282
+ DROP TRIGGER IF EXISTS relay_hold_user_message;
283
+ DROP TRIGGER IF EXISTS relay_hold_goal;
284
+ DROP TRIGGER IF EXISTS relay_hold_topic;
285
+ DROP TRIGGER IF EXISTS relay_plan_concluded;
375
286
  DROP TRIGGER IF EXISTS relay_plan_participants;
376
- CREATE TRIGGER relay_plan_participants AFTER UPDATE OF host,experts ON topic_settings
377
- WHEN NEW.host<>OLD.host OR EXISTS (
378
- SELECT 1 FROM json_each(OLD.experts) AS previous
379
- WHERE NOT EXISTS (SELECT 1 FROM json_each(NEW.experts) AS current
380
- WHERE current.key=previous.key AND current.value=previous.value)
381
- )
382
- BEGIN
383
- UPDATE discussion_turns SET status='skipped' WHERE room_id=NEW.room_id AND status='queued';
384
- END;
385
287
  DROP TRIGGER IF EXISTS runtime_turn_answered;
386
- -- An answer relays to the next queued turn unless the user has acted since the host last arranged.
387
- CREATE TRIGGER runtime_turn_answered AFTER UPDATE OF status ON discussion_turns
388
- WHEN NEW.status='answered' AND OLD.status<>'answered'
389
- BEGIN
390
- UPDATE discussion_events SET status='completed',result='message:'||NEW.answer_id
391
- WHERE room_id=NEW.room_id AND turn_id=NEW.id AND status IN ('pending','dispatched','started');
392
- UPDATE discussion_turns SET status='pending'
393
- WHERE id=(SELECT MIN(id) FROM discussion_turns WHERE room_id=NEW.room_id AND status='queued')
394
- AND NOT EXISTS(SELECT 1 FROM turn_relay WHERE room_id=NEW.room_id AND held_since>0)
395
- AND NOT EXISTS(SELECT 1 FROM topic_settings WHERE room_id=NEW.room_id AND phase IN ('paused','concluded'));
396
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
397
- SELECT NEW.room_id,run_id,generation,'answer:'||NEW.id,host,'host'
398
- FROM discussion_runtime JOIN topic_settings USING(room_id)
399
- WHERE room_id=NEW.room_id AND status='running'
400
- AND NOT EXISTS(SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'));
401
- END;
402
288
  DROP TRIGGER IF EXISTS runtime_topic_stop;
403
- CREATE TRIGGER runtime_topic_stop AFTER UPDATE OF phase ON topic_settings
404
- WHEN NEW.phase IN ('paused','concluded') AND OLD.phase<>NEW.phase
405
- BEGIN
406
- UPDATE discussion_events SET status='completed',result='concluded' WHERE room_id=NEW.room_id
407
- AND role='host' AND status='started' AND NEW.phase='concluded';
408
- UPDATE discussion_runtime SET status=NEW.phase,generation=generation+1,revision=revision+1,
409
- resume_phase=CASE WHEN OLD.phase IN ('initial','discussing') THEN OLD.phase ELSE resume_phase END,
410
- updated_at=strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE room_id=NEW.room_id;
411
- END;
412
- CREATE TRIGGER IF NOT EXISTS runtime_user_message AFTER INSERT ON message_details
413
- WHEN EXISTS(SELECT 1 FROM messages WHERE id=NEW.message_id AND author='用户')
414
- BEGIN
415
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
416
- SELECT messages.room_id,run_id,generation,'user:'||NEW.message_id,host,'host'
417
- FROM messages JOIN discussion_runtime ON discussion_runtime.room_id=messages.room_id
418
- JOIN topic_settings ON topic_settings.room_id=messages.room_id
419
- WHERE messages.id=NEW.message_id AND status='running';
420
- END;
421
- CREATE TRIGGER IF NOT EXISTS runtime_turn_cancelled AFTER UPDATE OF status ON discussion_turns
422
- WHEN NEW.status='cancelled' AND OLD.status<>'cancelled'
423
- BEGIN
424
- UPDATE discussion_events SET status='cancelled' WHERE room_id=NEW.room_id AND turn_id=NEW.id
425
- AND status IN ('pending','dispatched','started');
426
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
427
- SELECT NEW.room_id,run_id,generation,'cancel:'||NEW.id,host,'host'
428
- FROM discussion_runtime JOIN topic_settings USING(room_id)
429
- WHERE room_id=NEW.room_id AND status='running'
430
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
431
- END;
432
- CREATE TRIGGER IF NOT EXISTS runtime_archive AFTER UPDATE OF archived ON room_directory
433
- WHEN NEW.archived=1 AND OLD.archived<>NEW.archived
434
- BEGIN
435
- UPDATE discussion_runtime SET status='stopped',generation=generation+1,revision=revision+1
436
- WHERE room_id=NEW.room_id;
437
- END;
289
+ DROP TRIGGER IF EXISTS runtime_user_message;
290
+ DROP TRIGGER IF EXISTS runtime_turn_cancelled;
291
+ DROP TRIGGER IF EXISTS runtime_archive;
438
292
  DROP TRIGGER IF EXISTS runtime_participants_changed;
439
- -- A new host needs a new run. Experts joining or leaving keep the run going; the host is woken to re-plan.
440
- CREATE TRIGGER runtime_participants_changed AFTER UPDATE OF host ON topic_settings
441
- WHEN NEW.host<>OLD.host
442
- BEGIN
443
- UPDATE discussion_runtime SET status='paused',generation=generation+1,revision=revision+1
444
- WHERE room_id=NEW.room_id AND status='running';
445
- END;
446
293
  DROP TRIGGER IF EXISTS runtime_experts_changed;
447
- CREATE TRIGGER runtime_experts_changed AFTER UPDATE OF experts ON topic_settings
448
- WHEN NEW.host=OLD.host AND NEW.experts<>OLD.experts
449
- BEGIN
450
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
451
- SELECT NEW.room_id,run_id,generation,'members:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),NEW.host,'host'
452
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running'
453
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status IN ('pending','dispatched','started'))
454
- AND NOT EXISTS(SELECT 1 FROM discussion_turns WHERE room_id=NEW.room_id AND status IN ('pending','notified'));
455
- END;
456
- CREATE TRIGGER IF NOT EXISTS runtime_goal_changed AFTER UPDATE OF goal ON rooms
457
- WHEN NEW.goal<>OLD.goal
458
- BEGIN
459
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
460
- SELECT NEW.id,run_id,generation,'goal:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),host,'host'
461
- FROM discussion_runtime JOIN topic_settings USING(room_id)
462
- WHERE room_id=NEW.id AND status='running'
463
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.id AND role='host' AND status='started');
464
- END;
465
- CREATE TRIGGER IF NOT EXISTS runtime_constraints_changed AFTER UPDATE OF background,boundaries ON topic_settings
466
- WHEN NEW.background<>OLD.background OR NEW.boundaries<>OLD.boundaries
467
- BEGIN
468
- INSERT OR IGNORE INTO discussion_events(room_id,run_id,generation,cause,participant,role)
469
- SELECT NEW.room_id,run_id,generation,'constraints:'||COALESCE((SELECT MAX(id) FROM discussion_events),0),NEW.host,'host'
470
- FROM discussion_runtime WHERE room_id=NEW.room_id AND status='running'
471
- AND NOT EXISTS(SELECT 1 FROM discussion_events WHERE room_id=NEW.room_id AND role='host' AND status='started');
472
- END;
473
- -- Paused, preparing or concluded time is idle: it is excluded from elapsed time and total-duration deadlines.
294
+ DROP TRIGGER IF EXISTS runtime_goal_changed;
295
+ DROP TRIGGER IF EXISTS runtime_constraints_changed;
474
296
  CREATE TABLE IF NOT EXISTS discussion_pauses (
475
297
  room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
476
298
  paused_at INTEGER NOT NULL DEFAULT 0,
@@ -481,19 +303,8 @@ INSERT INTO discussion_pauses(room_id,paused_at)
481
303
  FROM discussion_runtime WHERE discussion_runtime.room_id=topic_settings.room_id AND status<>'running'),
482
304
  CAST(strftime('%s','now') AS INTEGER)) ELSE 0 END
483
305
  FROM topic_settings WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE discussion_pauses.room_id=topic_settings.room_id);
484
- CREATE TRIGGER IF NOT EXISTS pause_clock_stopped AFTER UPDATE OF phase ON topic_settings
485
- WHEN OLD.phase IN ('initial','discussing') AND NEW.phase NOT IN ('initial','discussing')
486
- BEGIN
487
- INSERT INTO discussion_pauses(room_id) SELECT NEW.room_id
488
- WHERE NOT EXISTS(SELECT 1 FROM discussion_pauses WHERE room_id=NEW.room_id);
489
- UPDATE discussion_pauses SET paused_at=CAST(strftime('%s','now') AS INTEGER) WHERE room_id=NEW.room_id AND paused_at=0;
490
- END;
491
- CREATE TRIGGER IF NOT EXISTS pause_clock_resumed AFTER UPDATE OF phase ON topic_settings
492
- WHEN NEW.phase IN ('initial','discussing') AND OLD.phase NOT IN ('initial','discussing')
493
- BEGIN
494
- UPDATE discussion_pauses SET paused_seconds=paused_seconds+MAX(0,CAST(strftime('%s','now') AS INTEGER)-paused_at),paused_at=0
495
- WHERE room_id=NEW.room_id AND paused_at>0;
496
- END;
306
+ DROP TRIGGER IF EXISTS pause_clock_stopped;
307
+ DROP TRIGGER IF EXISTS pause_clock_resumed;
497
308
  -- Both room kinds register members; free rooms additionally persist requests and executions.
498
309
  CREATE TABLE IF NOT EXISTS room_members (
499
310
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -524,6 +335,11 @@ CREATE TABLE IF NOT EXISTS member_registrations (
524
335
  token_hash TEXT NOT NULL,
525
336
  registered_at TEXT NOT NULL
526
337
  );
338
+ -- Web-relay members use the manual access path while retaining owner-only proxy semantics.
339
+ CREATE TABLE IF NOT EXISTS member_manual_sources (
340
+ member_id INTEGER PRIMARY KEY REFERENCES room_members(id) ON DELETE CASCADE,
341
+ source TEXT NOT NULL CHECK(source='web')
342
+ );
527
343
  CREATE TABLE IF NOT EXISTS member_executions (
528
344
  id INTEGER PRIMARY KEY AUTOINCREMENT,
529
345
  room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
@@ -574,6 +390,16 @@ CREATE TABLE IF NOT EXISTS execution_denials (
574
390
  room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
575
391
  denials TEXT NOT NULL
576
392
  );
393
+ -- A manual member's session refused a delivery: the runner keeps the execution and delivers again at retry_at
394
+ -- (0 while delivering). The owner may ask for it now or stop it, and the runner then fails it. Not part of backups.
395
+ CREATE TABLE IF NOT EXISTS execution_retries (
396
+ execution_id INTEGER PRIMARY KEY REFERENCES member_executions(id) ON DELETE CASCADE,
397
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
398
+ attempt INTEGER NOT NULL,
399
+ retry_at INTEGER NOT NULL DEFAULT 0,
400
+ reason TEXT NOT NULL DEFAULT '',
401
+ stopped INTEGER NOT NULL DEFAULT 0
402
+ );
577
403
  -- Phases the runner saw a member's software go through in one execution (a free execution or a discussion event), as
578
404
  -- millisecond offsets from its delivery; numbered per execution, and a resent segment keeps the later end. Not part of backups.
579
405
  CREATE TABLE IF NOT EXISTS execution_segments (
@@ -781,8 +607,8 @@ CREATE TABLE IF NOT EXISTS room_links (
781
607
  created_at TEXT NOT NULL
782
608
  );
783
609
 
784
- -- A discussion is a switch on a room, not a room type: one thin session per started discussion. Its status mirrors the legacy
785
- -- topic phase until the phase machinery is retired. Free rooms have none.
610
+ -- A discussion is a switch on a room, not a room type: one thin record per started discussion.
611
+ -- Session status is written by the application; legacy topic phase remains for old history only.
786
612
  CREATE TABLE IF NOT EXISTS discussion_sessions (
787
613
  id INTEGER PRIMARY KEY AUTOINCREMENT,
788
614
  room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
@@ -802,31 +628,9 @@ FROM rooms JOIN topic_settings ON topic_settings.room_id=rooms.id
802
628
  WHERE NOT EXISTS (SELECT 1 FROM room_modes WHERE room_id=rooms.id AND mode='free')
803
629
  AND NOT EXISTS (SELECT 1 FROM discussion_sessions WHERE room_id=rooms.id);
804
630
  DROP TRIGGER IF EXISTS session_for_new_discussion;
805
- CREATE TRIGGER session_for_new_discussion AFTER INSERT ON room_modes
806
- WHEN NEW.mode='brainstorm' AND NOT EXISTS (SELECT 1 FROM discussion_sessions WHERE room_id=NEW.room_id)
807
- BEGIN
808
- INSERT INTO discussion_sessions (room_id,goal,status,created_at)
809
- SELECT rooms.id,rooms.goal,
810
- COALESCE((SELECT CASE WHEN phase='concluded' THEN 'concluded' WHEN phase IN ('preparing','paused') THEN 'paused' ELSE 'active' END
811
- FROM topic_settings WHERE room_id=rooms.id),'paused'),rooms.created_at
812
- FROM rooms WHERE rooms.id=NEW.room_id;
813
- END;
814
631
  DROP TRIGGER IF EXISTS session_follows_phase_insert;
815
- CREATE TRIGGER session_follows_phase_insert AFTER INSERT ON topic_settings
816
- BEGIN
817
- UPDATE discussion_sessions SET status=CASE WHEN NEW.phase='concluded' THEN 'concluded' WHEN NEW.phase IN ('preparing','paused') THEN 'paused' ELSE 'active' END
818
- WHERE id=(SELECT MAX(id) FROM discussion_sessions WHERE room_id=NEW.room_id);
819
- END;
820
632
  DROP TRIGGER IF EXISTS session_follows_phase;
821
- CREATE TRIGGER session_follows_phase AFTER UPDATE OF phase ON topic_settings
822
- BEGIN
823
- UPDATE discussion_sessions SET status=CASE WHEN NEW.phase='concluded' THEN 'concluded' WHEN NEW.phase IN ('preparing','paused') THEN 'paused' ELSE 'active' END,
824
- 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
825
- WHERE id=(SELECT MAX(id) FROM discussion_sessions WHERE room_id=NEW.room_id);
826
- END;
827
- -- What a discussion concluded with: the room keeps one shared state and brief that the next discussion overwrites, so
828
- -- ending a discussion copies them, with why it ended, onto its session; a state or brief last written before it started
829
- -- belongs to an earlier discussion and is left out. Reopening and ending again replaces the copy.
633
+ -- Historical conclusion snapshots are written by the application and remain readable across later discussions.
830
634
  CREATE TABLE IF NOT EXISTS discussion_conclusions (
831
635
  session_id INTEGER PRIMARY KEY REFERENCES discussion_sessions(id) ON DELETE CASCADE,
832
636
  room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
@@ -839,17 +643,6 @@ CREATE TABLE IF NOT EXISTS discussion_conclusions (
839
643
  concluded_at TEXT NOT NULL
840
644
  );
841
645
  DROP TRIGGER IF EXISTS conclusion_snapshot;
842
- CREATE TRIGGER conclusion_snapshot AFTER UPDATE OF phase ON topic_settings
843
- WHEN NEW.phase='concluded' AND OLD.phase<>'concluded'
844
- BEGIN
845
- INSERT OR REPLACE INTO discussion_conclusions (session_id,room_id,stop_reason,max_rounds,max_minutes,brief,state,state_until,concluded_at)
846
- 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,
847
- 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,
848
- strftime('%Y-%m-%dT%H:%M:%SZ','now')
849
- FROM discussion_sessions AS s JOIN rooms AS r ON r.id=s.room_id
850
- 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
851
- WHERE s.id=(SELECT MAX(id) FROM discussion_sessions WHERE room_id=NEW.room_id);
852
- END;
853
646
  -- Discussions that ended before snapshots: only the latest still matches the room's state, so only it gets one. Safe to repeat.
854
647
  INSERT OR IGNORE INTO discussion_conclusions (session_id,room_id,stop_reason,max_rounds,max_minutes,brief,state,state_until,concluded_at)
855
648
  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,
@@ -862,6 +655,99 @@ WHERE s.status='concluded' AND s.id=(SELECT MAX(id) FROM discussion_sessions WHE
862
655
  UPDATE topic_settings SET phase='discussing' WHERE phase='reviewing';
863
656
  UPDATE discussion_runtime SET resume_phase='discussing' WHERE resume_phase='reviewing';
864
657
 
658
+ -- Request-based sessions use session-scoped settings. The room-scoped tables above remain read-only legacy history.
659
+ CREATE TABLE IF NOT EXISTS session_start_submissions (
660
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
661
+ submission_id TEXT NOT NULL,
662
+ session_id INTEGER NOT NULL UNIQUE REFERENCES discussion_sessions(id) ON DELETE CASCADE,
663
+ request_json TEXT NOT NULL,
664
+ PRIMARY KEY (room_id,submission_id)
665
+ );
666
+ CREATE TABLE IF NOT EXISTS session_state (
667
+ session_id INTEGER PRIMARY KEY REFERENCES discussion_sessions(id) ON DELETE CASCADE,
668
+ phase TEXT NOT NULL CHECK(phase IN ('initial','discussing')),
669
+ revision INTEGER NOT NULL DEFAULT 0,
670
+ read_until INTEGER NOT NULL DEFAULT 0,
671
+ held_since INTEGER NOT NULL DEFAULT 0
672
+ );
673
+ CREATE TABLE IF NOT EXISTS session_limits (
674
+ session_id INTEGER PRIMARY KEY REFERENCES discussion_sessions(id) ON DELETE CASCADE,
675
+ max_rounds INTEGER NOT NULL DEFAULT 0,
676
+ max_minutes INTEGER NOT NULL DEFAULT 0,
677
+ execution_minutes INTEGER NOT NULL DEFAULT 0,
678
+ started_at INTEGER NOT NULL DEFAULT 0,
679
+ stop_reason TEXT NOT NULL DEFAULT '',
680
+ revision INTEGER NOT NULL DEFAULT 0
681
+ );
682
+ CREATE TABLE IF NOT EXISTS session_round_budgets (
683
+ session_id INTEGER PRIMARY KEY REFERENCES discussion_sessions(id) ON DELETE CASCADE,
684
+ base INTEGER NOT NULL DEFAULT 0,
685
+ cap INTEGER NOT NULL DEFAULT 0,
686
+ extra INTEGER NOT NULL DEFAULT 0
687
+ );
688
+ CREATE TABLE IF NOT EXISTS session_pauses (
689
+ session_id INTEGER PRIMARY KEY REFERENCES discussion_sessions(id) ON DELETE CASCADE,
690
+ paused_at INTEGER NOT NULL DEFAULT 0,
691
+ paused_seconds INTEGER NOT NULL DEFAULT 0
692
+ );
693
+ CREATE TABLE IF NOT EXISTS session_options (
694
+ session_id INTEGER PRIMARY KEY REFERENCES discussion_sessions(id) ON DELETE CASCADE,
695
+ named_followup INTEGER NOT NULL DEFAULT 0 CHECK(named_followup IN (0,1))
696
+ );
697
+ CREATE TABLE IF NOT EXISTS session_requests (
698
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
699
+ session_id INTEGER NOT NULL REFERENCES discussion_sessions(id) ON DELETE CASCADE,
700
+ round INTEGER NOT NULL DEFAULT 0,
701
+ position INTEGER NOT NULL DEFAULT 0,
702
+ member_id INTEGER NOT NULL REFERENCES room_members(id),
703
+ purpose TEXT NOT NULL CHECK(purpose IN ('discuss','discuss_host')),
704
+ question TEXT NOT NULL DEFAULT '',
705
+ status TEXT NOT NULL DEFAULT 'queued'
706
+ CHECK(status IN ('queued','open','answered','failed','skipped','cancelled')),
707
+ member_request_id INTEGER UNIQUE REFERENCES member_requests(id),
708
+ submission_id TEXT NOT NULL,
709
+ read_until INTEGER DEFAULT NULL,
710
+ created_at TEXT NOT NULL,
711
+ UNIQUE(session_id,submission_id)
712
+ );
713
+ CREATE INDEX IF NOT EXISTS session_requests_queue ON session_requests(session_id,status,round,position,id);
714
+ CREATE UNIQUE INDEX IF NOT EXISTS one_open_session_request ON session_requests(session_id)
715
+ WHERE status='open' AND purpose='discuss';
716
+ CREATE TABLE IF NOT EXISTS task_message_sources (
717
+ message_id INTEGER PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
718
+ session_id INTEGER NOT NULL REFERENCES discussion_sessions(id) ON DELETE CASCADE,
719
+ session_request_id INTEGER NOT NULL UNIQUE REFERENCES session_requests(id) ON DELETE CASCADE,
720
+ cause_kind TEXT NOT NULL CHECK(cause_kind IN ('session_started','scheduled_speaker','speaker_answered','round_complete','user_message','member_change','request_failed')),
721
+ cause_message_id INTEGER REFERENCES messages(id),
722
+ cause_request_id INTEGER REFERENCES member_requests(id)
723
+ );
724
+ CREATE INDEX IF NOT EXISTS task_message_sources_session ON task_message_sources(session_id,message_id);
725
+
726
+ -- Stop old engine sessions once during upgrade. Their original phase and rows remain readable, while new sessions can open.
727
+ CREATE TABLE IF NOT EXISTS schema_migrations (
728
+ name TEXT PRIMARY KEY,
729
+ applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
730
+ );
731
+ INSERT OR IGNORE INTO discussion_conclusions (session_id,room_id,stop_reason,max_rounds,max_minutes,brief,state,state_until,concluded_at)
732
+ SELECT s.id,s.room_id,'engine_upgrade',COALESCE(l.max_rounds,0),COALESCE(l.max_minutes,0),
733
+ CASE WHEN b.until>s.started_message_id THEN b.text ELSE '' END,
734
+ CASE WHEN r.state_until>s.started_message_id THEN r.state_text ELSE '' END,
735
+ CASE WHEN r.state_until>s.started_message_id THEN r.state_until ELSE 0 END,
736
+ strftime('%Y-%m-%dT%H:%M:%SZ','now')
737
+ FROM discussion_sessions AS s JOIN rooms AS r ON r.id=s.room_id
738
+ 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
739
+ WHERE s.status IN ('active','paused')
740
+ AND NOT EXISTS (SELECT 1 FROM schema_migrations WHERE name='discussion_session_requests_v1');
741
+ UPDATE discussion_runtime SET status='stopped',generation=generation+1,updated_at=strftime('%Y-%m-%dT%H:%M:%SZ','now')
742
+ WHERE status IN ('ready','running','paused')
743
+ AND room_id IN (SELECT room_id FROM discussion_sessions WHERE status IN ('active','paused'))
744
+ AND NOT EXISTS (SELECT 1 FROM schema_migrations WHERE name='discussion_session_requests_v1');
745
+ UPDATE discussion_sessions SET status='concluded',
746
+ ended_message_id=COALESCE((SELECT MAX(id) FROM messages WHERE messages.room_id=discussion_sessions.room_id),0)
747
+ WHERE status IN ('active','paused')
748
+ AND NOT EXISTS (SELECT 1 FROM schema_migrations WHERE name='discussion_session_requests_v1');
749
+ INSERT OR IGNORE INTO schema_migrations(name) VALUES ('discussion_session_requests_v1');
750
+
865
751
  -- A computer running the local service, paired by the site administrator. Only project names and ids and the
866
752
  -- configured client of each platform are published; directories and credentials stay on that computer.
867
753
  CREATE TABLE IF NOT EXISTS local_machines (
@@ -886,3 +772,9 @@ CREATE TABLE IF NOT EXISTS remote_launches (
886
772
  updated_at INTEGER NOT NULL
887
773
  );
888
774
  CREATE INDEX IF NOT EXISTS remote_launches_machine ON remote_launches(machine_id, status);
775
+
776
+ -- Normalize legacy online relay members and retired brainstorm rooms for the shared free-room model.
777
+ INSERT OR IGNORE INTO member_manual_sources(member_id,source)
778
+ SELECT id,'web' FROM room_members WHERE access='online';
779
+ UPDATE room_members SET access='manual' WHERE access='online';
780
+ UPDATE room_modes SET mode='free' WHERE mode='brainstorm';