ai_co_discuss 0.2.91 → 0.2.93

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,10 +1,10 @@
1
- # 开发协作(自由协作房间的主持人)
2
-
3
- 仅在你是自由协作房间的主持人、且用户请成员分工改代码(定计划、实现、评审、整理文档)时读本页;派活规则见 [自由协作主持](free.md)。以下是建议而非固定流程,按任务取舍,并在回答中说明取舍。
4
-
5
- - **先定计划再派活**:与用户谈清目标后写一份计划文档(目标、按目录或文件划分的分工、接口约定、验收方式),写入项目里用户指定的位置;用户未表示可以开始时不派活。
6
- - **按文件划分**:派活时写明每位成员负责的目录或文件和对应的计划章节;需要并行的部分事先约定接口,避免两人改同一文件。
7
- - **要接力就让它回到你**:只点名一位成员时,它的回答默认直接给用户;之后还要交给评审或安排下一步时,派活加 `--handoff`(API 为 `handoff: true`),它做完你会再被唤醒。点名多位时总会回到你。
8
- - **实现后评审**:实现成员结束后,按 `descriptions` 交给负责评审的成员,写明计划文档位置和本次改动的范围(`room view` 的执行报告列有改动文件),并加 `--handoff` 取回评审结论。
9
- - **返工回原实现者**:评审提出问题时交回原实现者修改,再请评审复核;同一问题反复不通过,或剩余次数(`free.max_auto_hops` 减去本链已用)不够完成下一轮时,停下向用户说明分歧与现状。
10
- - **最后整理文档**:评审通过后,再交给负责文档的成员同步说明;向用户汇报时列出各步结果和仍未验证的部分。
1
+ # 开发协作(自由协作房间的主持人)
2
+
3
+ 仅在你是自由协作房间的主持人、且用户请成员分工改代码(定计划、实现、评审、整理文档)时读本页;派活规则见 [自由协作主持](free.md)。以下是建议而非固定流程,按任务取舍,并在回答中说明取舍。
4
+
5
+ - **先定计划再派活**:与用户谈清目标后写一份计划文档(目标、按目录或文件划分的分工、接口约定、验收方式),写入项目里用户指定的位置;用户未表示可以开始时不派活。
6
+ - **按文件划分**:派活时写明每位成员负责的目录或文件和对应的计划章节;需要并行的部分事先约定接口,避免两人改同一文件。
7
+ - **要接力就让它回到你**:只点名一位成员时,它的回答默认直接给用户;之后还要交给评审或安排下一步时,派活加 `--handoff`(API 为 `handoff: true`),它做完你会再被唤醒。点名多位时总会回到你。
8
+ - **实现后评审**:实现成员结束后,按 `descriptions` 交给负责评审的成员,写明计划文档位置和本次改动的范围(`room view` 的执行报告列有改动文件),并加 `--handoff` 取回评审结论。
9
+ - **返工回原实现者**:评审提出问题时交回原实现者修改,再请评审复核;同一问题反复不通过,或本链剩余轮数(`free.chain_rounds` 里本链的 `allowed` 减 `used`)不够完成下一轮时,停下向用户说明分歧与现状。
10
+ - **最后整理文档**:评审通过后,再交给负责文档的成员同步说明;向用户汇报时列出各步结果和仍未验证的部分。
@@ -1,22 +1,22 @@
1
- # 自由协作主持
2
-
3
- 仅用于自由协作房间的主持人(任务 `role` 为 `host`)。成员的回答与提交方式见参与技能的自由协作一节。
4
-
5
- ## 请求类型
6
-
7
- 任务的 `request_purposes` 说明每条请求为何交给你:
8
-
9
- - `route`:用户未点名的消息,由你回答或分派。
10
- - `handoff`:你派出并要求交回的成员都已结束,据结果决定下一步。
11
- - `all`:用户请全体成员各自回应一次,其他成员已同时收到;你只回答自己的部分,不加 `--to`。
12
-
13
- 先 `room view` 看成员与状态,再决定由谁处理。
14
-
15
- ## 派活
16
-
17
- - 需要他人时在回答中加 `--to 成员名`(可重复,不能点名自己),写明各自负责的文件或模块;不需要他人时不加,本次即结束。
18
- - 只点名一位成员时,它的回答直接面向用户;还要按结果安排下一步(如再交评审)时加 `--handoff`,它做完你会再被唤醒。点名多位时,全部结束后总会回到你汇总。
19
- - 收到 `handoff` 后交给其他成员或汇总给用户;成员的回答已完整时只用一两句说明完成,不复述。
20
- - 同一条用户消息引出的 AI 请求(派活与回交)有上限(`room view` 的 `free.max_auto_hops`,用户可调),用完即停并等用户发新消息;派活前先想清楚需要几位、几步。(上限防止 AI 之间无人值守地来回转交。)
21
-
22
- 用户请成员分工改代码(定计划、实现、评审、整理文档)时,先读 [开发协作](dev-flow.md)。
1
+ # 自由协作主持
2
+
3
+ 仅用于自由协作房间的主持人(任务 `role` 为 `host`)。成员的回答与提交方式见参与技能的自由协作一节。
4
+
5
+ ## 请求类型
6
+
7
+ 任务的 `request_purposes` 说明每条请求为何交给你:
8
+
9
+ - `route`:用户未点名的消息,由你回答或分派。
10
+ - `handoff`:你派出并要求交回的成员都已结束,据结果决定下一步。
11
+ - `all`:用户请全体成员各自回应一次,其他成员已同时收到;你只回答自己的部分,不加 `--to`。
12
+
13
+ 先 `room view` 看成员与状态,再决定由谁处理。
14
+
15
+ ## 派活
16
+
17
+ - 需要他人时在回答中加 `--to 成员名`(可重复,不能点名自己),写明各自负责的文件或模块;不需要他人时不加,本次即结束。
18
+ - 只点名一位成员时,它的回答直接面向用户;还要按结果安排下一步(如再交评审)时加 `--handoff`,它做完你会再被唤醒。点名多位时,全部结束后总会回到你汇总。
19
+ - 收到 `handoff` 后交给其他成员或汇总给用户;成员的回答已完整时只用一两句说明完成,不复述。
20
+ - 同一条用户消息最多推进若干轮(`room view` 的 `free.max_rounds`,本链已用见 `free.chain_rounds`,用户可调):你派一次活算一轮,成员做完交回给你汇总不另计。到上限时你的下一次派活会被暂存、任务链停下,由用户决定继续或发新消息;你不能自己加轮。派活前先想清楚需要几位、几轮,最后一轮留给汇总。(上限防止 AI 之间无人值守地来回转交。)
21
+
22
+ 用户请成员分工改代码(定计划、实现、评审、整理文档)时,先读 [开发协作](dev-flow.md)。
@@ -25,6 +25,10 @@ RECENT_EXECUTIONS = 100
25
25
  # The timeline page reads this many recent executions of each kind; with `after` (?3) only those running or ended since.
26
26
  # Parameters: ?1 room, ?2 limit, ?3 after (0 reads all). Shared verbatim with the Worker.
27
27
  TIMELINE_EXECUTIONS = 100
28
+ # Rounds used and allowed in the room's recent chains, for "第 2/3 轮"; parameters: the room's limit, the room.
29
+ CHAIN_PROGRESS_SQL = """SELECT b.chain_id,COUNT(*) AS used,? + COALESCE((SELECT h.extra_rounds FROM free_chain_holds h
30
+ 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=?
31
+ GROUP BY b.chain_id ORDER BY b.chain_id DESC LIMIT 10"""
28
32
  TIMELINE_STATES = {"completed": "done", "failed": "failed", "cancelled": "cancelled"}
29
33
  TIMELINE_FREE_IDS = "SELECT id FROM member_executions WHERE room_id=?1 AND (?3=0 OR finished_at=0 OR finished_at>=?3) ORDER BY id DESC LIMIT ?2"
30
34
  TIMELINE_DISCUSSION_IDS = """SELECT e.id FROM discussion_events e LEFT JOIN event_timings t ON t.event_id=e.id
@@ -76,10 +80,11 @@ SCHEDULER_PROVIDERS = ("jev", "decisions")
76
80
  RECOMMEND_SECONDS = 60
77
81
  # A click sent just before the hint disappears still counts.
78
82
  RECOMMEND_GRACE_SECONDS = 5
79
- # AI-created requests (host dispatches and hand-offs) allowed per chain by default; each owner message starts a chain.
80
- MAX_AUTO_HOPS = 8
81
- # The most an owner may allow per chain.
82
- MAX_AUTO_HOPS_LIMIT = 50
83
+ # Rounds a host may run from one owner message by default (a round: one host dispatch, its members' answers and the
84
+ # hand-back); each owner message starts a chain. Counted in rounds, so a room with more members gets no fewer of them.
85
+ CHAIN_ROUNDS = 3
86
+ # The most rounds an owner may allow per chain, or add at once when continuing a stopped chain.
87
+ MAX_CHAIN_ROUNDS = 20
83
88
  # A host that left gives up the role; the first active runner member (else manual) then takes it.
84
89
  RELEASE_HOST = "UPDATE room_members SET role='member' WHERE room_id=? AND role='host' AND status<>'active'"
85
90
  ASSIGN_HOST = """UPDATE room_members SET role='host' WHERE id=(SELECT id FROM room_members WHERE room_id=? AND status='active'
@@ -371,9 +376,11 @@ def free_view(connection, room_id):
371
376
  WHERE room_id=? ORDER BY id DESC LIMIT ?""", (room_id, RECENT_REQUESTS))]
372
377
  chains = [dict(row) for row in connection.execute(
373
378
  "SELECT chain_id,reason FROM free_chains WHERE room_id=? ORDER BY chain_id DESC LIMIT 10", (room_id,))]
379
+ max_rounds = chain_rounds(connection, room_id)
380
+ progress = [dict(row) for row in connection.execute(CHAIN_PROGRESS_SQL, (max_rounds, room_id))]
374
381
  return {"members": members, "requests": requests[::-1], "executions": recent_executions(connection, room_id), "execution_minutes": execution_minutes(connection, room_id),
375
- "conflicts": possible_conflicts(connection, room_id, names), "halted_chains": chains, "max_auto_hops": auto_hops(connection, room_id),
376
- "recommendations": recent_recommendations(connection, room_id, now)[::-1]}
382
+ "conflicts": possible_conflicts(connection, room_id, names), "halted_chains": chains, "max_rounds": max_rounds,
383
+ "chain_rounds": progress, "recommendations": recent_recommendations(connection, room_id, now)[::-1]}
377
384
 
378
385
 
379
386
  def require_member(connection, room_id, token, name=None):
@@ -633,40 +640,74 @@ def halt_chain(connection, room_id, chain_id, reason):
633
640
  connection.execute("INSERT OR IGNORE INTO free_chains (room_id,chain_id,reason) VALUES (?,?,?)", (room_id, chain_id, reason))
634
641
 
635
642
 
636
- def chain_budget(connection, room_id, chain_id):
637
- """AI-created requests left in a chain, or zero once it is halted."""
643
+ def rounds_left(connection, room_id, chain_id):
644
+ """Rounds a chain may still start: the room's limit plus what the owner added, less the dispatches made; none once halted."""
638
645
  if connection.execute("SELECT 1 FROM free_chains WHERE room_id=? AND chain_id=?", (room_id, chain_id)).fetchone():
639
646
  return 0
640
- used = connection.execute("SELECT COUNT(*) FROM member_requests WHERE room_id=? AND chain_id=? AND created_by='host'",
641
- (room_id, chain_id)).fetchone()[0]
642
- return auto_hops(connection, room_id) - used
647
+ used = connection.execute("SELECT COUNT(*) FROM free_batches WHERE room_id=? AND chain_id=?", (room_id, chain_id)).fetchone()[0]
648
+ extra = connection.execute("SELECT extra_rounds FROM free_chain_holds WHERE room_id=? AND chain_id=?", (room_id, chain_id)).fetchone()
649
+ return chain_rounds(connection, room_id) + (extra[0] if extra else 0) - used
643
650
 
644
651
 
645
- def auto_hops(connection, room_id):
646
- """AI-created requests allowed per chain in this room."""
647
- row = connection.execute("SELECT max_auto_hops FROM free_settings WHERE room_id=?", (room_id,)).fetchone()
648
- return row[0] if row else MAX_AUTO_HOPS
652
+ def chain_rounds(connection, room_id):
653
+ """Rounds a host may run from one owner message in this room."""
654
+ row = connection.execute("SELECT max_rounds FROM free_round_limits WHERE room_id=?", (room_id,)).fetchone()
655
+ return row[0] if row else CHAIN_ROUNDS
649
656
 
650
657
 
651
- def set_auto_hops(connection, room_id, token, payload):
652
- """Owner sets the room's chain budget; it applies to chains still running and to later ones."""
658
+ def set_chain_rounds(connection, room_id, token, payload):
659
+ """Owner sets the room's round limit; it applies to chains still running and to later ones."""
653
660
  require_owner(connection, room_id, token)
654
- value = payload.get("max_auto_hops")
655
- if type(value) is not int or not 1 <= value <= MAX_AUTO_HOPS_LIMIT:
656
- raise ValueError(f"max_auto_hops 须为 1–{MAX_AUTO_HOPS_LIMIT} 的整数")
661
+ value = payload.get("max_rounds")
662
+ if type(value) is not int or not 1 <= value <= MAX_CHAIN_ROUNDS:
663
+ raise ValueError(f"max_rounds 须为 1–{MAX_CHAIN_ROUNDS} 的整数")
657
664
  begin_write(connection)
658
- connection.execute("""INSERT INTO free_settings (room_id,max_auto_hops) VALUES (?,?)
659
- ON CONFLICT(room_id) DO UPDATE SET max_auto_hops=excluded.max_auto_hops""", (room_id, value))
660
- return {"max_auto_hops": value}
665
+ connection.execute("""INSERT INTO free_round_limits (room_id,max_rounds) VALUES (?,?)
666
+ ON CONFLICT(room_id) DO UPDATE SET max_rounds=excluded.max_rounds""", (room_id, value))
667
+ return {"max_rounds": value}
668
+
669
+
670
+ def continue_chain(connection, room_id, token, chain_id, payload):
671
+ """Owner lets a chain the round limit stopped run on: adds rounds and sends the dispatch the limit held back.
672
+
673
+ Bound to one known chain, so it takes no revision; a chain that is not stopped by its limit is refused.
674
+ """
675
+ require_owner(connection, room_id, token)
676
+ rounds = payload.get("rounds")
677
+ if type(rounds) is not int or not 1 <= rounds <= MAX_CHAIN_ROUNDS:
678
+ raise ValueError(f"rounds 须为 1–{MAX_CHAIN_ROUNDS} 的整数")
679
+ begin_write(connection)
680
+ halted = connection.execute("SELECT reason FROM free_chains WHERE room_id=? AND chain_id=?", (room_id, chain_id)).fetchone()
681
+ if not halted or halted[0] != "budget":
682
+ raise FileExistsError("这条任务链没有因轮数上限停下,无需继续")
683
+ host = connection.execute("SELECT * FROM room_members WHERE room_id=? AND role='host' AND status='active'", (room_id,)).fetchone()
684
+ if not host:
685
+ raise ValueError("房间没有在场的主持人,无法继续")
686
+ connection.execute("""INSERT INTO free_chain_holds (room_id,chain_id,extra_rounds) VALUES (?,?,?)
687
+ ON CONFLICT(room_id,chain_id) DO UPDATE SET extra_rounds=extra_rounds+excluded.extra_rounds""", (room_id, chain_id, rounds))
688
+ connection.execute("DELETE FROM free_chains WHERE room_id=? AND chain_id=?", (room_id, chain_id))
689
+ held = connection.execute("SELECT message_id,names,handoff FROM free_chain_holds WHERE room_id=? AND chain_id=?", (room_id, chain_id)).fetchone()
690
+ connection.execute("UPDATE free_chain_holds SET message_id=0,host_id=0,names='[]',handoff=0 WHERE room_id=? AND chain_id=?", (room_id, chain_id))
691
+ if held["message_id"]:
692
+ dispatch(connection, room_id, host, held["message_id"], json.loads(held["names"]), chain_id, bool(held["handoff"]))
693
+ return free_view(connection, room_id)
694
+
695
+
696
+ def hold_dispatch(connection, room_id, host, message_id, names, chain_id, handoff):
697
+ """Stop a chain at its round limit, keeping the dispatch it held back so the owner can send it by continuing."""
698
+ halt_chain(connection, room_id, chain_id, "budget")
699
+ connection.execute("""INSERT INTO free_chain_holds (room_id,chain_id,message_id,host_id,names,handoff) VALUES (?,?,?,?,?,?)
700
+ ON CONFLICT(room_id,chain_id) DO UPDATE SET message_id=excluded.message_id,host_id=excluded.host_id,names=excluded.names,
701
+ handoff=excluded.handoff""", (room_id, chain_id, message_id, host["id"], json.dumps(list(names), ensure_ascii=False), int(handoff)))
661
702
 
662
703
 
663
704
  def dispatch(connection, room_id, host, message_id, names, chain_id, handoff=False):
664
- """Queue the host's named members as one batch; several members, or one the host asked back, hand back when it ends."""
705
+ """Queue the host's named members as one batch, one round; several members, or one the host asked back, hand back when it ends."""
665
706
  member_ids = named_members(active_members(connection, room_id), names, exclude=host["name"])
666
707
  if not member_ids or not chain_id:
667
708
  return
668
- if chain_budget(connection, room_id, chain_id) < len(member_ids):
669
- halt_chain(connection, room_id, chain_id, "budget")
709
+ if rounds_left(connection, room_id, chain_id) < 1:
710
+ hold_dispatch(connection, room_id, host, message_id, names, chain_id, handoff)
670
711
  return
671
712
  for member_id in member_ids:
672
713
  insert_request(connection, room_id, member_id, message_id, chain_id, "ask", "host", batch_id=message_id)
@@ -691,9 +732,8 @@ def settle_batches(connection, room_id):
691
732
  "SELECT 1 FROM free_handoffs WHERE room_id=? AND batch_id=?", (room_id, batch["batch_id"])).fetchone():
692
733
  # One member's answer reaches the user directly unless the host asked for it back, e.g. to send it to a reviewer.
693
734
  continue
694
- elif chain_budget(connection, room_id, batch["chain_id"]) < 1:
695
- halt_chain(connection, room_id, batch["chain_id"], "budget")
696
735
  else:
736
+ # The hand-back closes the round it belongs to, so it is never held back: the last round still gets its summary.
697
737
  insert_request(connection, room_id, host["id"], batch["batch_id"], batch["chain_id"], "handoff", "host")
698
738
  advance_queues(connection, room_id)
699
739
 
@@ -714,11 +714,29 @@ CREATE TABLE IF NOT EXISTS free_handoffs (
714
714
  batch_id INTEGER NOT NULL,
715
715
  PRIMARY KEY (room_id,batch_id)
716
716
  );
717
- -- The owner's per-room chain budget; rooms without a row use the default.
717
+ -- Superseded by free_round_limits: it counted AI-created requests, which rose with the number of members. Kept, unread.
718
718
  CREATE TABLE IF NOT EXISTS free_settings (
719
719
  room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
720
720
  max_auto_hops INTEGER NOT NULL
721
721
  );
722
+ -- The owner's per-room limit on rounds a host may run from one owner message (a round is one host dispatch, its members'
723
+ -- answers and the hand-back); rooms without a row use the default.
724
+ CREATE TABLE IF NOT EXISTS free_round_limits (
725
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
726
+ max_rounds INTEGER NOT NULL
727
+ );
728
+ -- A chain the owner let run on past its limit: the rounds added, and the dispatch the limit held back, which is sent
729
+ -- when the owner continues (message_id 0 when nothing is held).
730
+ CREATE TABLE IF NOT EXISTS free_chain_holds (
731
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
732
+ chain_id INTEGER NOT NULL,
733
+ extra_rounds INTEGER NOT NULL DEFAULT 0,
734
+ message_id INTEGER NOT NULL DEFAULT 0,
735
+ host_id INTEGER NOT NULL DEFAULT 0,
736
+ names TEXT NOT NULL DEFAULT '[]',
737
+ handoff INTEGER NOT NULL DEFAULT 0,
738
+ PRIMARY KEY (room_id,chain_id)
739
+ );
722
740
  -- A brainstorm room started from a free room keeps its origin for the way back.
723
741
  CREATE TABLE IF NOT EXISTS room_links (
724
742
  room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,