ai_co_discuss 0.3.38 → 0.3.39

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.
@@ -16,7 +16,7 @@
16
16
 
17
17
  ## 派活
18
18
 
19
- - 需要他人时在回答中加 `--to 成员名`(可重复,不能点名自己),写明各自负责的文件或模块;不需要他人时不加,本次即结束。
19
+ - 需要他人时在回答中加 `--to 显示名`(可重复,不能点名自己),写明各自负责的文件或模块;不需要他人时不加,本次即结束。
20
20
  - 只点名一位成员时,它的回答直接面向用户;还要按结果安排下一步(如再交评审)时加 `--handoff`,它做完你会再被唤醒。点名多位时,全部结束后总会回到你汇总。
21
21
  - 需要预先指定实现后的只读评审时,先读 `review roster ROOM`,再在派实现的 `message post` 中带 `--next-review NAME --review-scope 范围 --review-roster REVISION`;评审者不能是本批实现者。授权与实现任务同时保存,自动要求该批交回主持人。候选目前限程序启动的 Codex CLI;没有合格成员时读取结果给出原因。
22
22
  - 收到 `review_handoffs` 时,先用 `review ready ROOM --batch ID` 读取完整委派、交付和报告,确认可交既定评审后用 `review advance ROOM --batch ID --revision REV --submission-id UUID --as 本人` 推进。只处理该批交接,其他请求继续完成;状态变化须重读,不能替换评审者或扩大权限。能力失效、额度不足、交付不完整等原因保留在读取结果和任务中。实际投递前本机复核只读权限与能力版本,失败撤销该次评审并交回主持人。
@@ -37,6 +37,8 @@ description: "AI CoDiscuss 的通用参与技能,所有参与者(包括主
37
37
 
38
38
  每次 shell 调用都是一次模型往返,手动会话里还可能要用户逐条确认,所以互不依赖的只读命令放进同一条 shell 命令依次执行。正文想好后,先用当前软件的文件写入工具把正文(和需要的摘要)写成草稿文件,再用一条只含 `codiscuss` 的命令以 `--body-file`(`--summary-file`)提交;失败重试仍用这份草稿。不要用 heredoc、`echo` 重定向或命令行字面量传长文本,也不要在命令里调用函数生成编号(如 `[guid]::NewGuid()`、`$(uuidgen)`):自动运行时这类组合命令会被权限检查拒绝,文本中的引号(含中文弯引号)还会截断参数。其他写入命令需要编号时直接写一个新的 UUID 字面值。不同的网站写入各自单独执行并核对结果。
39
39
 
40
+ 称呼和点名成员一律用显示名:`room view` 中 `members[].label`(如 `Gemini`)就是它在房间里的名字,正文 `@`、`--to`、`--next-review`、`--expert` 都写它。`name` 是成员 ID,与 `descriptions` 同属成员的完整信息,需要核对时再看,不写进正文;旧消息、`author` 等字段里出现的 ID 指的是同一位成员。
41
+
40
42
  ## 阅读
41
43
 
42
44
  背景不足读 `room brief`,需要接续判断读 `state view`,核对具体论据读 `message view ROOM ID`,查找材料才用 `message list`。按争点阅读相关原文,不用摘要代替需要复核的论证,也不无目的拉取全部历史。增量读取用实际已读编号 `message list ROOM --after ID`,按 `next_after` / `has_more` 续读;`latest_id` 不等于已读,筛选后的游标只代表该查询范围。阶段和安排以最新服务端状态为准。
@@ -16,4 +16,4 @@
16
16
 
17
17
  ## 提到其他成员
18
18
 
19
- 普通成员可在回答中用 `--to 成员名` 或正文 `@成员` 提到他人,不能点名自己;这不会派发任务,也不会通知对方。需要别人补充时,在回答里写明缺什么,由用户或主持人决定是否请对方处理。
19
+ 普通成员可在回答中用 `--to 显示名` 或正文 `@显示名` 提到他人,不能点名自己;这不会派发任务,也不会通知对方。需要别人补充时,在回答里写明缺什么,由用户或主持人决定是否请对方处理。
@@ -15,7 +15,7 @@ import re
15
15
  import time
16
16
  import uuid
17
17
 
18
- from .identity import MAX_ROOM_AGENTS, is_ai
18
+ from .identity import MAX_ROOM_AGENTS, default_label, is_ai
19
19
  from .message_queue import pause as pause_queue, queue_view
20
20
  from .runtime import (LEASE_SECONDS, MAX_EXECUTION_SEGMENTS, begin_write, enforce_session_limits,
21
21
  require_owner, save_segments, token_hash)
@@ -43,6 +43,13 @@ RECENT_EXECUTIONS = 100
43
43
  # The timeline page reads this many recent executions of each kind; with `after` (?3) only those running or ended since.
44
44
  # Parameters: ?1 room, ?2 limit, ?3 after (0 reads all). Shared verbatim with the Worker.
45
45
  TIMELINE_EXECUTIONS = 100
46
+ # The event log page reads at most this many room events per call, newest first when paging back.
47
+ ROOM_EVENT_PAGE = 500
48
+ # The message a request's card sits under, for member_requests aliased `r`. A handoff request belongs to the dispatch
49
+ # message (`message_id`, which batch logic relies on) but happens because the batch's last answer arrived, so its card
50
+ # and answer sit under that answer; later messages (the user asking something meanwhile) then stay above it.
51
+ ANCHOR_SQL = """CASE WHEN r.purpose='handoff' THEN COALESCE((SELECT MAX(a.answer_id) FROM member_requests a
52
+ WHERE a.room_id=r.room_id AND a.batch_id=r.message_id AND a.purpose<>'handoff'), r.message_id) ELSE r.message_id END"""
46
53
  # Rounds used and allowed in the room's recent chains, for "第 2/3 轮"; parameters: the room's limit, the room.
47
54
  CHAIN_PROGRESS_SQL = """SELECT b.chain_id,COUNT(*) AS used,? + COALESCE((SELECT h.extra_rounds FROM free_chain_holds h
48
55
  WHERE h.room_id=b.room_id AND h.chain_id=b.chain_id),0) AS allowed FROM free_batches b WHERE b.room_id=?
@@ -526,6 +533,38 @@ def member_labels(connection, room_id):
526
533
  "SELECT name,label FROM member_labels WHERE room_id=? ORDER BY name", (room_id,))}
527
534
 
528
535
 
536
+ def member_display_names(connection, room_id):
537
+ """What each present member is called in text: the owner's label, else its platform nickname, else its ID."""
538
+ labels = member_labels(connection, room_id)
539
+ return {row["name"]: labels.get(row["name"]) or default_label(row["name"]) for row in connection.execute(
540
+ "SELECT name FROM room_members WHERE room_id=? AND status<>'removed' ORDER BY id", (room_id,))}
541
+
542
+
543
+ def member_handles(connection, room_id):
544
+ """Every way to name a present member, mapped to its ID: the ID, and its display name unless another member shares it."""
545
+ shown = member_display_names(connection, room_id)
546
+ handles = {name: name for name in shown}
547
+ taken = list(shown.values())
548
+ for name, label in shown.items():
549
+ if label not in handles and taken.count(label) == 1:
550
+ handles[label] = name
551
+ return handles
552
+
553
+
554
+ def resolve_member_fields(connection, room_id, payload):
555
+ """Turn display names in a write's member fields into IDs, so members are named as they are seen; IDs still work."""
556
+ handles = member_handles(connection, room_id)
557
+ resolve = lambda value: handles.get(value.strip(), value) if isinstance(value, str) else value
558
+ if isinstance(payload.get("to"), list):
559
+ payload["to"] = [resolve(item) for item in payload["to"]]
560
+ for key in ("next_review", "expert"):
561
+ if key in payload:
562
+ payload[key] = resolve(payload[key])
563
+ if isinstance(payload.get("turns"), list):
564
+ payload["turns"] = [{**item, "expert": resolve(item.get("expert"))} if isinstance(item, dict) else item for item in payload["turns"]]
565
+ return payload
566
+
567
+
529
568
  def label_members(connection, room_id, token, payload):
530
569
  """Owner sets members' display labels by name; an empty text removes that member's label."""
531
570
  require_owner(connection, room_id, token)
@@ -593,12 +632,43 @@ def execution_timeline(connection, room_id, after=0):
593
632
  "segment_limit": MAX_EXECUTION_SEGMENTS}
594
633
 
595
634
 
635
+ def room_event_log(connection, room_id, token, after=0, before=0):
636
+ """Read the owner's append-only room event log written by the schema triggers.
637
+
638
+ Args:
639
+ after: return events with a larger id (live follow), oldest first.
640
+ before: with no `after`, return the page of events just below this id (0 = the newest page), oldest first.
641
+
642
+ Returns:
643
+ `events` (id, at in epoch ms, kind, data) ordered by `at` then id, `members` id → name for resolving
644
+ `member_id`/`host_id`, `first_id`/`last_id` as paging cursors, and `more` when older events remain.
645
+ A write request (`api.*`) is stored after it finishes, so its id follows the changes it caused; its `at` is
646
+ when it arrived, and ordering by `at` puts the input before its effects.
647
+ """
648
+ require_owner(connection, room_id, token)
649
+ after, before = max(0, int(after)), max(0, int(before))
650
+ if after:
651
+ rows = connection.execute("SELECT id,at,kind,data FROM room_events WHERE room_id=? AND id>? ORDER BY id LIMIT ?",
652
+ (room_id, after, ROOM_EVENT_PAGE)).fetchall()
653
+ else:
654
+ rows = connection.execute("""SELECT id,at,kind,data FROM room_events WHERE room_id=? AND (?=0 OR id<?)
655
+ ORDER BY id DESC LIMIT ?""", (room_id, before, before, ROOM_EVENT_PAGE)).fetchall()
656
+ first_id = min((row["id"] for row in rows), default=0)
657
+ more = bool(rows) and not after and connection.execute(
658
+ "SELECT 1 FROM room_events WHERE room_id=? AND id<? LIMIT 1", (room_id, first_id)).fetchone() is not None
659
+ members = {str(row["id"]): row["name"] for row in connection.execute("SELECT id,name FROM room_members WHERE room_id=?", (room_id,))}
660
+ events = sorted(({**dict(row), "data": json.loads(row["data"])} for row in rows), key=lambda event: (event["at"], event["id"]))
661
+ return {"events": events, "members": members, "more": more, "first_id": first_id,
662
+ "last_id": max((row["id"] for row in rows), default=after)}
663
+
664
+
596
665
  def recent_executions(connection, room_id):
597
666
  """Return bounded public reports, including requests no longer in the recent request list."""
598
667
  rows = connection.execute(f"""SELECT e.*,m.name AS member,
599
668
  (SELECT json_group_array(id) FROM member_requests WHERE execution_id=e.id) AS request_ids,
600
669
  (SELECT MAX(answer_id) FROM member_requests WHERE execution_id=e.id) AS answer_id,
601
670
  (SELECT MIN(message_id) FROM member_requests WHERE execution_id=e.id) AS request_message_id,
671
+ (SELECT MIN({ANCHOR_SQL}) FROM member_requests r WHERE r.execution_id=e.id) AS anchor_message_id,
602
672
  (SELECT COUNT(*) FROM execution_notes WHERE execution_id=e.id) AS note_count,
603
673
  (SELECT denials FROM execution_denials WHERE execution_id=e.id) AS denials,{CAPABILITY_COLUMNS},
604
674
  (SELECT json_object('created_at',created_at) FROM late_answers WHERE execution_id=e.id) AS late_answer
@@ -610,7 +680,8 @@ def recent_executions(connection, room_id):
610
680
  requests.append({**dict(request), **session_request_source(connection, request["id"], request["message_id"])})
611
681
  executions.append({**public_execution(row), "member": row["member"], "cwd": row["cwd"], "changes": json.loads(row["changes"] or "[]"),
612
682
  "changes_recorded": bool(row["changes"]), "request_ids": json.loads(row["request_ids"]),
613
- "answer_id": row["answer_id"], "request_message_id": row["request_message_id"], "requests": requests,
683
+ "answer_id": row["answer_id"], "request_message_id": row["request_message_id"],
684
+ "anchor_message_id": row["anchor_message_id"], "requests": requests,
614
685
  "notes": row["note_count"], "denials": json.loads(row["denials"] or "[]"),
615
686
  "capabilities": reported_capabilities(row), "stop_result": row["stop_result"] or "",
616
687
  "late_answer": json.loads(row["late_answer"]) if row["late_answer"] else None,
@@ -898,9 +969,10 @@ def save_suggestions(connection, room_id, message_id, member, payload):
898
969
  active = active_members(connection, room_id)
899
970
  targets = set(named_members(active, payload["to"] if payload.get("to") is not None else [], exclude=member["name"]))
900
971
  prose = mention_prose(str(payload.get("text", "")))
901
- for name, target in active.items():
902
- if name != member["name"] and re.search(r"(^|[\s((,,。;;::])@" + re.escape(name) + r"(?![A-Za-z0-9_\-\u4e00-\u9fff])", prose):
903
- targets.add(target["id"])
972
+ for handle, name in member_handles(connection, room_id).items():
973
+ if name in active and name != member["name"] and re.search(
974
+ r"(^|[\s((,,。;;::])@" + re.escape(handle) + r"(?![A-Za-z0-9_\-\u4e00-\u9fff])", prose):
975
+ targets.add(active[name]["id"])
904
976
  for target_id in targets:
905
977
  connection.execute("INSERT INTO member_suggestions (message_id,member_id) VALUES (?,?)", (message_id, target_id))
906
978
 
@@ -1286,6 +1358,7 @@ def inbox(connection, room_id, token):
1286
1358
  release_recommendations(connection, room_id)
1287
1359
  archived = connection.execute("SELECT archived FROM room_directory WHERE room_id=?", (room_id,)).fetchone()[0]
1288
1360
  members, observers = [], []
1361
+ shown = member_display_names(connection, room_id)
1289
1362
  # Manual members are never delivered to; the runner only reads them so smart scheduling can route to them too.
1290
1363
  for row in connection.execute(f"SELECT * FROM room_members WHERE room_id=? AND status='active' AND {REGISTERED_MEMBER} ORDER BY id", (room_id,)):
1291
1364
  pending = [dict(item) for item in connection.execute("""SELECT r.id,r.message_id,r.purpose,
@@ -1296,6 +1369,7 @@ def inbox(connection, room_id, token):
1296
1369
  LEFT JOIN task_message_sources t ON t.session_request_id=sr.id
1297
1370
  WHERE r.member_id=? AND r.status='pending' ORDER BY r.id""", (row["id"],))]
1298
1371
  member_view = {key: row[key] for key in MEMBER_FIELDS}
1372
+ member_view["label"] = shown.get(row["name"], row["name"])
1299
1373
  member_view["review_capability"] = review_capability(connection, room_id, row["id"], token)
1300
1374
  member_view["held_until"] = held_until(connection, row["id"])
1301
1375
  # The runner compares this with its adapter's effect and reports a change.
@@ -1,31 +1,40 @@
1
- """Participant identities: an AI platform is a type, a room participant is one named instance of it.
2
-
3
- `Claude` and `Claude-review` are two instances of the Claude platform in the same room; each keeps its
4
- own session, model and thinking effort. Platform names alone keep their historical meaning.
5
- """
6
-
7
- import re
8
-
9
- USER = "用户"
10
- AI_PLATFORMS = ("Codex", "Claude", "Antigravity", "DeepSeek", "Qwen", "Grok")
11
- PARTICIPANTS = [USER, *AI_PLATFORMS]
12
- MAX_INSTANCE_SUFFIX = 16
13
- # AI instances one room may hold, across platforms.
14
- MAX_ROOM_AGENTS = 12
15
- INSTANCE_PATTERN = re.compile(rf"({'|'.join(AI_PLATFORMS)})(?:-[0-9A-Za-z_]{{1,{MAX_INSTANCE_SUFFIX}}})?")
16
-
17
-
18
- def is_ai(name):
19
- """Whether a name is an AI platform or a named instance of one."""
20
- return isinstance(name, str) and INSTANCE_PATTERN.fullmatch(name) is not None
21
-
22
-
23
- def is_participant(name):
24
- """Whether a name may author, submit or be scheduled: the user or any AI instance."""
25
- return name == USER or is_ai(name)
26
-
27
-
28
- def platform_of(name):
29
- """Return the platform an AI instance belongs to, or None for the user and invalid names."""
30
- match = INSTANCE_PATTERN.fullmatch(name) if isinstance(name, str) else None
31
- return match.group(1) if match else None
1
+ """Participant identities: an AI platform is a type, a room participant is one named instance of it.
2
+
3
+ `Claude` and `Claude-review` are two instances of the Claude platform in the same room; each keeps its
4
+ own session, model and thinking effort. Platform names alone keep their historical meaning.
5
+ """
6
+
7
+ import re
8
+
9
+ USER = "用户"
10
+ AI_PLATFORMS = ("Codex", "Claude", "Antigravity", "DeepSeek", "Qwen", "Grok")
11
+ PARTICIPANTS = [USER, *AI_PLATFORMS]
12
+ MAX_INSTANCE_SUFFIX = 16
13
+ # AI instances one room may hold, across platforms.
14
+ MAX_ROOM_AGENTS = 12
15
+ INSTANCE_PATTERN = re.compile(rf"({'|'.join(AI_PLATFORMS)})(?:-[0-9A-Za-z_]{{1,{MAX_INSTANCE_SUFFIX}}})?")
16
+ # The name people and AIs call a platform when it differs from its ID; web/app.js PLATFORM_IDENTITIES shows the same.
17
+ PLATFORM_NICKNAMES = {"Antigravity": "Gemini"}
18
+
19
+
20
+ def is_ai(name):
21
+ """Whether a name is an AI platform or a named instance of one."""
22
+ return isinstance(name, str) and INSTANCE_PATTERN.fullmatch(name) is not None
23
+
24
+
25
+ def is_participant(name):
26
+ """Whether a name may author, submit or be scheduled: the user or any AI instance."""
27
+ return name == USER or is_ai(name)
28
+
29
+
30
+ def platform_of(name):
31
+ """Return the platform an AI instance belongs to, or None for the user and invalid names."""
32
+ match = INSTANCE_PATTERN.fullmatch(name) if isinstance(name, str) else None
33
+ return match.group(1) if match else None
34
+
35
+
36
+ def default_label(name):
37
+ """The name an instance goes by without an owner's label: its platform's nickname plus its own suffix (Antigravity-2 → Gemini-2)."""
38
+ platform = platform_of(name)
39
+ nickname = PLATFORM_NICKNAMES.get(platform)
40
+ return nickname + name[len(platform):] if nickname else name
@@ -1328,3 +1328,169 @@ UPDATE free_chain_holds SET names=replace(replace(names,'"Gemini"','"Antigravity
1328
1328
  UPDATE room_creations SET request_text=replace(replace(request_text,'"Gemini"','"Antigravity"'),'"Gemini-','"Antigravity-') WHERE request_text LIKE '%"Gemini%';
1329
1329
  UPDATE session_start_submissions SET request_json=replace(replace(request_json,'"Gemini"','"Antigravity"'),'"Gemini-','"Antigravity-') WHERE request_json LIKE '%"Gemini%';
1330
1330
  UPDATE scheduler_decision_inputs SET input=replace(replace(input,'"Gemini"','"Antigravity"'),'"Gemini-','"Antigravity-') WHERE input LIKE '%"Gemini%';
1331
+ -- Room event log: an append-only record of each room state change, written by the triggers below so every code path
1332
+ -- is covered and later edits cannot forget it. State tables are overwritten in place; this log keeps the order of
1333
+ -- what happened (which request was created under which message, why it was cancelled, which batch handed back) so the
1334
+ -- timeline page can list and replay it. `at` is epoch milliseconds; `data` holds ids, names resolve at read time.
1335
+ CREATE TABLE IF NOT EXISTS room_events (
1336
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1337
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
1338
+ at INTEGER NOT NULL,
1339
+ kind TEXT NOT NULL,
1340
+ data TEXT NOT NULL DEFAULT '{}'
1341
+ );
1342
+ CREATE INDEX IF NOT EXISTS room_events_room ON room_events(room_id,id);
1343
+ DROP TRIGGER IF EXISTS event_message_posted;
1344
+ CREATE TRIGGER event_message_posted AFTER INSERT ON messages
1345
+ BEGIN
1346
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'message.posted',
1347
+ json_object('message_id',NEW.id,'author',NEW.author,'source',NEW.source,'reply_to',NEW.reply_to,'text',substr(NEW.text,1,80)));
1348
+ END;
1349
+ DROP TRIGGER IF EXISTS event_message_withdrawn;
1350
+ CREATE TRIGGER event_message_withdrawn AFTER INSERT ON message_withdrawals
1351
+ BEGIN
1352
+ INSERT INTO room_events (room_id,at,kind,data) SELECT room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'message.withdrawn',
1353
+ json_object('message_id',NEW.message_id,'kind',NEW.kind) FROM messages WHERE id=NEW.message_id;
1354
+ END;
1355
+ DROP TRIGGER IF EXISTS event_room_rewound;
1356
+ CREATE TRIGGER event_room_rewound AFTER INSERT ON room_rewinds
1357
+ BEGIN
1358
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'room.rewound',
1359
+ json_object('rewind_id',NEW.id,'message_id',NEW.message_id));
1360
+ END;
1361
+ DROP TRIGGER IF EXISTS event_member_joined;
1362
+ CREATE TRIGGER event_member_joined AFTER INSERT ON room_members
1363
+ BEGIN
1364
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'member.joined',
1365
+ json_object('member_id',NEW.id,'role',NEW.role,'status',NEW.status,'adapter',NEW.adapter));
1366
+ END;
1367
+ DROP TRIGGER IF EXISTS event_member_changed;
1368
+ CREATE TRIGGER event_member_changed AFTER UPDATE OF role,status ON room_members
1369
+ WHEN OLD.role IS NOT NEW.role OR OLD.status IS NOT NEW.status
1370
+ BEGIN
1371
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'member.changed',
1372
+ json_object('member_id',NEW.id,'role',NEW.role,'status',NEW.status,'from_role',OLD.role,'from_status',OLD.status));
1373
+ END;
1374
+ DROP TRIGGER IF EXISTS event_request_created;
1375
+ CREATE TRIGGER event_request_created AFTER INSERT ON member_requests
1376
+ BEGIN
1377
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'request.created',
1378
+ json_object('request_id',NEW.id,'member_id',NEW.member_id,'message_id',NEW.message_id,'purpose',NEW.purpose,
1379
+ 'created_by',NEW.created_by,'chain_id',NEW.chain_id,'batch_id',NEW.batch_id,'status',NEW.status));
1380
+ END;
1381
+ DROP TRIGGER IF EXISTS event_request_changed;
1382
+ CREATE TRIGGER event_request_changed AFTER UPDATE OF status,execution_id,answer_id ON member_requests
1383
+ WHEN OLD.status IS NOT NEW.status OR OLD.execution_id IS NOT NEW.execution_id OR OLD.answer_id IS NOT NEW.answer_id
1384
+ BEGIN
1385
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'request.changed',
1386
+ json_object('request_id',NEW.id,'member_id',NEW.member_id,'message_id',NEW.message_id,'status',NEW.status,
1387
+ 'from_status',OLD.status,'execution_id',NEW.execution_id,'answer_id',NEW.answer_id));
1388
+ END;
1389
+ DROP TRIGGER IF EXISTS event_request_cancel_reason;
1390
+ CREATE TRIGGER event_request_cancel_reason AFTER INSERT ON request_cancel_reasons
1391
+ BEGIN
1392
+ INSERT INTO room_events (room_id,at,kind,data) SELECT room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'request.cancel_reason',
1393
+ json_object('request_id',NEW.request_id,'member_id',member_id,'reason',NEW.reason) FROM member_requests WHERE id=NEW.request_id;
1394
+ END;
1395
+ DROP TRIGGER IF EXISTS event_execution_created;
1396
+ CREATE TRIGGER event_execution_created AFTER INSERT ON member_executions
1397
+ BEGIN
1398
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'execution.created',
1399
+ json_object('execution_id',NEW.id,'member_id',NEW.member_id,'status',NEW.status));
1400
+ END;
1401
+ DROP TRIGGER IF EXISTS event_execution_changed;
1402
+ CREATE TRIGGER event_execution_changed AFTER UPDATE OF status ON member_executions
1403
+ WHEN OLD.status IS NOT NEW.status
1404
+ BEGIN
1405
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'execution.changed',
1406
+ json_object('execution_id',NEW.id,'member_id',NEW.member_id,'status',NEW.status,'from_status',OLD.status,'result',substr(NEW.result,1,200)));
1407
+ END;
1408
+ DROP TRIGGER IF EXISTS event_execution_retry;
1409
+ CREATE TRIGGER event_execution_retry AFTER INSERT ON execution_retries
1410
+ BEGIN
1411
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'execution.retry',
1412
+ json_object('execution_id',NEW.execution_id,'attempt',NEW.attempt,'retry_at',NEW.retry_at,'reason',substr(NEW.reason,1,200),'stopped',NEW.stopped));
1413
+ END;
1414
+ DROP TRIGGER IF EXISTS event_execution_retry_changed;
1415
+ CREATE TRIGGER event_execution_retry_changed AFTER UPDATE ON execution_retries
1416
+ WHEN OLD.attempt IS NOT NEW.attempt OR OLD.stopped IS NOT NEW.stopped
1417
+ BEGIN
1418
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'execution.retry',
1419
+ json_object('execution_id',NEW.execution_id,'attempt',NEW.attempt,'retry_at',NEW.retry_at,'reason',substr(NEW.reason,1,200),'stopped',NEW.stopped));
1420
+ END;
1421
+ DROP TRIGGER IF EXISTS event_execution_late_answer;
1422
+ CREATE TRIGGER event_execution_late_answer AFTER INSERT ON late_answers
1423
+ BEGIN
1424
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'execution.late_answer',
1425
+ json_object('execution_id',NEW.execution_id,'text',substr(NEW.text,1,80)));
1426
+ END;
1427
+ DROP TRIGGER IF EXISTS event_batch_opened;
1428
+ CREATE TRIGGER event_batch_opened AFTER INSERT ON free_batches
1429
+ BEGIN
1430
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'batch.opened',
1431
+ json_object('batch_id',NEW.batch_id,'chain_id',NEW.chain_id,'host_id',NEW.host_id));
1432
+ END;
1433
+ DROP TRIGGER IF EXISTS event_batch_settled;
1434
+ CREATE TRIGGER event_batch_settled AFTER UPDATE OF settled ON free_batches
1435
+ WHEN OLD.settled IS NOT NEW.settled
1436
+ BEGIN
1437
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'batch.settled',
1438
+ json_object('batch_id',NEW.batch_id,'chain_id',NEW.chain_id,'settled',NEW.settled));
1439
+ END;
1440
+ DROP TRIGGER IF EXISTS event_batch_handoff;
1441
+ CREATE TRIGGER event_batch_handoff AFTER INSERT ON free_handoffs
1442
+ BEGIN
1443
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'batch.handoff',
1444
+ json_object('batch_id',NEW.batch_id));
1445
+ END;
1446
+ DROP TRIGGER IF EXISTS event_queue_added;
1447
+ CREATE TRIGGER event_queue_added AFTER INSERT ON free_queue
1448
+ BEGIN
1449
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'queue.added',
1450
+ json_object('queue_id',NEW.id,'message_id',NEW.message_id,'member_id',NEW.member_id));
1451
+ END;
1452
+ DROP TRIGGER IF EXISTS event_queue_removed;
1453
+ CREATE TRIGGER event_queue_removed AFTER DELETE ON free_queue
1454
+ BEGIN
1455
+ INSERT INTO room_events (room_id,at,kind,data) SELECT OLD.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'queue.removed',
1456
+ json_object('queue_id',OLD.id,'message_id',OLD.message_id,'member_id',OLD.member_id) WHERE EXISTS (SELECT 1 FROM rooms WHERE id=OLD.room_id);
1457
+ END;
1458
+ DROP TRIGGER IF EXISTS event_chain_held;
1459
+ CREATE TRIGGER event_chain_held AFTER INSERT ON free_chain_holds
1460
+ BEGIN
1461
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'chain.held',
1462
+ json_object('chain_id',NEW.chain_id,'message_id',NEW.message_id,'names',NEW.names,'handoff',NEW.handoff,'extra_rounds',NEW.extra_rounds));
1463
+ END;
1464
+ DROP TRIGGER IF EXISTS event_chain_released;
1465
+ CREATE TRIGGER event_chain_released AFTER DELETE ON free_chain_holds
1466
+ BEGIN
1467
+ INSERT INTO room_events (room_id,at,kind,data) SELECT OLD.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'chain.released',
1468
+ json_object('chain_id',OLD.chain_id,'message_id',OLD.message_id) WHERE EXISTS (SELECT 1 FROM rooms WHERE id=OLD.room_id);
1469
+ END;
1470
+ DROP TRIGGER IF EXISTS event_scheduler_decided;
1471
+ CREATE TRIGGER event_scheduler_decided AFTER INSERT ON scheduler_decisions
1472
+ BEGIN
1473
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'scheduler.decided',
1474
+ json_object('decision_id',NEW.id,'after_message_id',NEW.after_message_id,'kind',NEW.kind,'label',NEW.label,
1475
+ 'confidence',NEW.confidence,'applied',NEW.applied,'provider',NEW.provider));
1476
+ END;
1477
+ DROP TRIGGER IF EXISTS event_scheduler_applied;
1478
+ CREATE TRIGGER event_scheduler_applied AFTER UPDATE OF applied ON scheduler_decisions
1479
+ WHEN OLD.applied IS NOT NEW.applied
1480
+ BEGIN
1481
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'scheduler.applied',
1482
+ json_object('decision_id',NEW.id,'label',NEW.label,'applied',NEW.applied));
1483
+ END;
1484
+ DROP TRIGGER IF EXISTS event_discussion_created;
1485
+ CREATE TRIGGER event_discussion_created AFTER INSERT ON discussion_sessions
1486
+ BEGIN
1487
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'discussion.created',
1488
+ json_object('session_id',NEW.id,'status',NEW.status,'goal',substr(NEW.goal,1,80)));
1489
+ END;
1490
+ DROP TRIGGER IF EXISTS event_discussion_changed;
1491
+ CREATE TRIGGER event_discussion_changed AFTER UPDATE OF status ON discussion_sessions
1492
+ WHEN OLD.status IS NOT NEW.status
1493
+ BEGIN
1494
+ INSERT INTO room_events (room_id,at,kind,data) VALUES (NEW.room_id,CAST((julianday('now')-2440587.5)*86400000 AS INTEGER),'discussion.changed',
1495
+ json_object('session_id',NEW.id,'status',NEW.status,'from_status',OLD.status));
1496
+ END;