pi-multi-viewers 0.1.0 → 0.2.1

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.
package/meeting_engine.py CHANGED
@@ -35,6 +35,7 @@ from meeting_fs import (
35
35
  git_show, is_message_file, parse_log_nameonly,
36
36
  read_protocol, cat_batch, remove_message, write_text, file_size,
37
37
  bare_of_base, bare_of_workdir, log,
38
+ RESULT_MD, RESULT_MD_MIN_BYTES, DEFAULT_STALL_TIMEOUT,
38
39
  )
39
40
  from meeting_core import (
40
41
  meeting_speak_count as core_meeting_speak_count,
@@ -42,6 +43,8 @@ from meeting_core import (
42
43
  should_write_af, can_start_rr, validate_and_fix, is_all_last_in,
43
44
  aggregate_mode as core_aggregate_mode,
44
45
  has_new_messages_for_me,
46
+ T_MESSAGE, T_FREEZING, T_ALL_FREEZING, T_PASS, T_CONCLUDED,
47
+ M_MEETING, M_ROUND_ROBIN, M_ALL_FREEZING, M_CONCLUDED,
45
48
  )
46
49
  POLL_INTERVAL = 2.0
47
50
  JITTER = 0.3
@@ -209,7 +212,7 @@ def rr_active_count(messages, agents):
209
212
  """
210
213
  starter = agents[0]
211
214
  return sum(1 for fm in messages.get(starter, [])
212
- if fm.get("mode") == "round-robin")
215
+ if fm.get("mode") == M_ROUND_ROBIN)
213
216
 
214
217
 
215
218
  def human_msg_count(bare):
@@ -255,11 +258,12 @@ def write_af_if_no_rr(bare, workdir, agent, agents):
255
258
  head = git_head(workdir)
256
259
  # 重检:bare 是否有 RR 已启动(任一 agent 最后一条 mode==round-robin)
257
260
  lasts = each_agent_last(bare, agents)
258
- if any(v is not None and v.get("mode") == "round-robin" for v in lasts.values()):
261
+ if any(v is not None and v.get("mode") == M_ROUND_ROBIN
262
+ for v in lasts.values()):
259
263
  log(agent, "bare 已有 RR 启动——放弃补写 af")
260
264
  return
261
265
  # 无 RR → 正常写 af
262
- write_protocol_signal(workdir, agent, "all-freezing", "all-freezing")
266
+ write_protocol_signal(workdir, agent, T_ALL_FREEZING, M_ALL_FREEZING)
263
267
 
264
268
 
265
269
  def write_protocol_signal(workdir, agent, type_, mode, next_=None):
@@ -331,11 +335,11 @@ def respond_with_fallback(workdir, agent, responder, head, meta, is_first,
331
335
  if rr_turn:
332
336
  nxt_a = core_next(agents, agent)
333
337
  log(agent, f"responder 无法产出——loop 代写 pass(无静默铁律最后执行)")
334
- write_protocol_signal(workdir, agent, "pass", "round-robin",
338
+ write_protocol_signal(workdir, agent, T_PASS, M_ROUND_ROBIN,
335
339
  nxt_a)
336
340
  else:
337
341
  log(agent, f"responder 无法产出——loop 代写 freezing")
338
- write_protocol_signal(workdir, agent, "freezing", "meeting")
342
+ write_protocol_signal(workdir, agent, T_FREEZING, M_MEETING)
339
343
  # 代写 = 无产出(设计 11.1:只数 responder 成功产出的轮,
340
344
  # 代写不耗配额)——返回 False(审核#1:原先 return True 让 C' 修复失效)
341
345
  return False
@@ -391,7 +395,7 @@ def commit_new_files(workdir, agent, head, mode):
391
395
  # review5 A6:next 无条件覆盖为顺序下一位(与 mode 无条件覆盖对称)。
392
396
  # 只补缺省会让 LLM 写错的 next 保留 → 轮转顺序混乱。单向流下
393
397
  # RR 只有 pass(无异议),next 是轮转顺序(协议状态),LLM 不知道。
394
- if mode == "round-robin" and fixed.get("type") in ("pass", "message"):
398
+ if mode == M_ROUND_ROBIN and fixed.get("type") in (T_PASS, T_MESSAGE):
395
399
  fixed["next"] = core_next(
396
400
  participants(bare_of_workdir(workdir)), agent)
397
401
  new_content = serialize_message(fixed, content)
@@ -447,7 +451,7 @@ def _result_md_valid(workdir):
447
451
  LLM 可能写空文件/仅 frontmatter——存在性检查退化为空提交(审核 A2)。
448
452
  存在性与大小经由 fs.file_size 一次判定(-1 = 不存在/不可读)。
449
453
  """
450
- return file_size(workdir, "result.md") > 50
454
+ return file_size(workdir, RESULT_MD) > RESULT_MD_MIN_BYTES
451
455
 
452
456
 
453
457
  def finalize_discussion(workdir, agent, responder, head, reason="consensus"):
@@ -462,7 +466,7 @@ def finalize_discussion(workdir, agent, responder, head, reason="consensus"):
462
466
  responder(workdir, agent, head, [], False, False, False,
463
467
  finalizing=True, finalize_reason=reason)
464
468
  # ② 校验 result.md 有效(存在 + 非空);无则重试(无静默铁律的扩展)
465
- result_path = os.path.join(workdir, "result.md")
469
+ result_path = os.path.join(workdir, RESULT_MD)
466
470
  for _ in range(MAX_RETRY):
467
471
  if _result_md_valid(workdir):
468
472
  break
@@ -475,11 +479,11 @@ def finalize_discussion(workdir, agent, responder, head, reason="consensus"):
475
479
  _commit_result_md(workdir, agent, "discuss: result.md")
476
480
  else:
477
481
  log(agent, "result.md 重试后仍无效——loop 兜底代写")
478
- write_text(workdir, "result.md",
482
+ write_text(workdir, RESULT_MD,
479
483
  "# 讨论结论\n\n(resultWriter 未能生成有效 result.md,"
480
484
  f"由本地循环兜底代写。收尾原因:{reason})\n")
481
485
  _commit_result_md(workdir, agent, "discuss: result.md (loop fallback)")
482
- write_protocol_signal(workdir, agent, "concluded", "concluded")
486
+ write_protocol_signal(workdir, agent, T_CONCLUDED, M_CONCLUDED)
483
487
  return True
484
488
 
485
489
 
@@ -491,16 +495,17 @@ def _commit_result_md(workdir, agent, subject):
491
495
  用 git status --porcelain(不用 git diff --quiet——diff 看不到
492
496
  未跟踪新文件,会误判"已提交"导致 result.md 永不 commit)。
493
497
  """
494
- r = run_git(workdir, "status", "--porcelain", "--", "result.md",
498
+ r = run_git(workdir, "status", "--porcelain", "--", RESULT_MD,
495
499
  check=False)
496
500
  if r.stdout.strip():
497
- git_commit(workdir, ["result.md"], subject)
501
+ git_commit(workdir, [RESULT_MD], subject)
498
502
  else:
499
503
  log(agent, "result.md 无改动——跳过 commit(幂等)")
500
504
 
501
505
 
502
506
  def agent_loop(workdir, agent, responder, max_meeting=10, max_rr=7,
503
- poll_interval=POLL_INTERVAL, stall_timeout=600):
507
+ poll_interval=POLL_INTERVAL,
508
+ stall_timeout=DEFAULT_STALL_TIMEOUT):
504
509
  """主状态机(v2)。responder 注入:响应一轮并返回是否产出。
505
510
 
506
511
  stall_timeout: 无进展超时兜底(秒,默认 600=10 分钟)。任何 agent
@@ -542,7 +547,7 @@ def agent_loop(workdir, agent, responder, max_meeting=10, max_rr=7,
542
547
  mode = core_aggregate_mode(lasts)
543
548
 
544
549
  # ===== ① concluded → 退出 =====
545
- if mode == "concluded":
550
+ if mode == M_CONCLUDED:
546
551
  log(agent, "讨论已收尾,退出")
547
552
  return
548
553
 
@@ -559,16 +564,29 @@ def agent_loop(workdir, agent, responder, max_meeting=10, max_rr=7,
559
564
  finalize_discussion(workdir, agent, responder, head,
560
565
  reason="stall")
561
566
  continue
562
- # 非 rw:rw 可能离线(崩溃/未启动)——接管收尾(审核#2,
563
- # 主协议 7.6"任一 agent 可接管"meeting 收紧成仅 rw 是缺口)。
564
- # 接管仲裁(review5 A3):两个非 rw 同时接管 → 各自 finalize
565
- # 双写 result.md → rebase 冲突真死锁。仲裁:
566
- # ① 先 pull 重检 bare 是否已收尾(result.md 已提交 → 让位)
567
- # ② 写接管声明消息(推进 HEAD → 其他 agent 的 stall 判定被
568
- # 重置 天然唯一接管者;push 冲突由现有容错重试仲裁,
569
- # 先到者赢)③ finalize。
567
+ # 非 rw:rw 可能离线(崩溃/未启动)——接管收尾(主协议
568
+ # 7.6"任一 agent 可接管"meeting 收紧成仅 rw 是缺口)。
569
+ #
570
+ # 仲裁 = **心跳式软仲裁**(不是互斥锁):
571
+ # ① 先 pull + 重检两个"是否已收尾"的共享事实(concluded /
572
+ # result.md 已提交)——已收尾则让位;
573
+ # 写一条接管声明 commit,作用是**推进 HEAD**:其他 agent
574
+ # 下一轮 `_stall_elapsed` 看到 HEAD 变化 → 累计归零 →
575
+ # 退出接管分支(先到者成为事实上的唯一收尾者);
576
+ # ③ finalize。
577
+ #
578
+ # **残余窗口(诚实边界)**:声明落地前,另一方若已走完本轮
579
+ # 的 git_head + stall 判定,则同样进入本分支(窗口≈毫秒级,
580
+ # 同一轮竞态)。此时双方各自唤醒 LLM 写 result.md——结果
581
+ # **不是死锁**:先 push 者成功;后者 push 走 git_push 的
582
+ # pull --rebase 重试,内容冲突则耗尽重试抛错 → loop 顶层
583
+ # 异常边界接住 → 下一轮看到 concluded 退出。
584
+ # 代价 = 偶尔多一次 LLM 调用(约 30–60s);产物始终唯一
585
+ # (bare 只有一个 result.md 版本)。
586
+ # **真正的兜底是 push 容错 + 下一轮 concluded 退出这两层**;
587
+ # 声明只降低并发概率,不构成互斥保证(勿读成"天然唯一")。
570
588
  git_pull(workdir)
571
- if aggregate_mode(bare, agents) == "concluded":
589
+ if aggregate_mode(bare, agents) == M_CONCLUDED:
572
590
  log(agent, "收尾已完成(他人接管)——让位")
573
591
  continue
574
592
  r = run_git(bare, "show", "HEAD:result.md", check=False)
@@ -577,8 +595,9 @@ def agent_loop(workdir, agent, responder, max_meeting=10, max_rr=7,
577
595
  continue
578
596
  # 写接管声明(freezing——推进 HEAD + 不破坏流程)
579
597
  log(agent, "rw 未收尾——声明接管(推进 HEAD 天然唯一接管者)")
580
- write_protocol_signal(workdir, agent, "freezing",
581
- aggregate_mode(bare, agents) or "meeting")
598
+ write_protocol_signal(workdir, agent, T_FREEZING,
599
+ aggregate_mode(bare, agents)
600
+ or M_MEETING)
582
601
  finalize_discussion(workdir, agent, responder, head,
583
602
  reason="stall")
584
603
  continue
@@ -591,7 +610,7 @@ def agent_loop(workdir, agent, responder, max_meeting=10, max_rr=7,
591
610
  continue
592
611
 
593
612
  # ===== ③ all-freezing 阶段:starter 启动 RR =====
594
- if mode == "all-freezing":
613
+ if mode == M_ALL_FREEZING:
595
614
  # starter 严格条件:全员 af → 启动 RR(用户 10084/10089:
596
615
  # 唤醒目的 = 有新的文件可提交给 LLM 处理。starter 冻结期间
597
616
  # 别人可能发了 message(读取点后),必须唤醒读完再 pass;
@@ -609,14 +628,14 @@ def agent_loop(workdir, agent, responder, max_meeting=10, max_rr=7,
609
628
  else:
610
629
  # 无新消息 → 确定性写 pass(启动 RR,带 next 轮转链)
611
630
  nxt_a = core_next(agents, agent)
612
- write_protocol_signal(workdir, agent, "pass",
613
- "round-robin", nxt_a)
631
+ write_protocol_signal(workdir, agent, T_PASS,
632
+ M_ROUND_ROBIN, nxt_a)
614
633
  continue
615
634
  time.sleep(poll_interval)
616
635
  continue
617
636
 
618
637
  # ===== ④ round-robin 阶段(发言锁已解)=====
619
- if mode == "round-robin":
638
+ if mode == M_ROUND_ROBIN:
620
639
  # 全员 pass → resultWriter 收尾(写 result.md + concluded)
621
640
  # 判定收敛到 core(审核 D:判定只此一份——core 吃完整消息列表,
622
641
  # 引擎只做 bare 组装)
@@ -649,8 +668,8 @@ def agent_loop(workdir, agent, responder, max_meeting=10, max_rr=7,
649
668
  meta = new_messages_with_meta(workdir, rp, agent)
650
669
  if not has_new_messages_for_me(meta, agent):
651
670
  nxt_a = core_next(agents, agent)
652
- write_protocol_signal(workdir, agent, "pass",
653
- "round-robin", nxt_a)
671
+ write_protocol_signal(workdir, agent, T_PASS,
672
+ M_ROUND_ROBIN, nxt_a)
654
673
  continue
655
674
  before = len(list_my_messages(workdir, agent))
656
675
  respond_with_fallback(workdir, agent, responder, head, meta,
@@ -691,7 +710,7 @@ def agent_loop(workdir, agent, responder, max_meeting=10, max_rr=7,
691
710
  and all_last.get(agent) != "freezing"):
692
711
  log(agent, f"meeting 配额耗尽({quota} 轮,含 human 增量"
693
712
  f"{quota - max_meeting})——确定性 freezing")
694
- write_protocol_signal(workdir, agent, "freezing", "meeting")
713
+ write_protocol_signal(workdir, agent, T_FREEZING, M_MEETING)
695
714
  continue
696
715
 
697
716
  # ⑤.3 触发判断
@@ -705,7 +724,7 @@ def agent_loop(workdir, agent, responder, max_meeting=10, max_rr=7,
705
724
  # 唯一未冻结者(无新消息)→ 确定性 freezing(不经 responder)
706
725
  if others_frozen and not triggered and all_last.get(agent) != "freezing":
707
726
  log(agent, "其他人都已冻结且无新消息——确定性 freezing")
708
- write_protocol_signal(workdir, agent, "freezing", "meeting")
727
+ write_protocol_signal(workdir, agent, T_FREEZING, M_MEETING)
709
728
  continue
710
729
  # 无新消息(非唯一未冻结者)→ 等待(L2:化简,先 freezing 后 sleep)
711
730
  if not triggered:
@@ -713,7 +732,7 @@ def agent_loop(workdir, agent, responder, max_meeting=10, max_rr=7,
713
732
  continue
714
733
 
715
734
  # ⑤.4 发言锁:我最后是 freezing → 锁(不发 message,等 RR)
716
- if all_last.get(agent) == "freezing":
735
+ if all_last.get(agent) == T_FREEZING:
717
736
  time.sleep(poll_interval)
718
737
  continue
719
738
 
package/meeting_fs.py CHANGED
@@ -18,6 +18,23 @@ import subprocess
18
18
  import time
19
19
  from datetime import datetime, timezone
20
20
 
21
+ # ---------------------------------------------------------------
22
+ # 产品级常量(跨模块的单一约定)
23
+ # ---------------------------------------------------------------
24
+
25
+ # 收尾产物的文件名。**单一来源**:engine(写/校验/提交)、loop(保存副本)、
26
+ # viewer(done 判据)、start_discussion(状态检查)、--report(提示)都引用它
27
+ # ——曾是 8 处字面量,产品级核心约定却没有家(e2e15 自审 S1)。
28
+ RESULT_MD = "result.md"
29
+
30
+ # result.md 的**有效性阈值**(字节):存在但小于它 → 视为未生成(LLM 可能
31
+ # 写空文件/仅 frontmatter——只查存在性会退化为空提交,审核 A2)。
32
+ RESULT_MD_MIN_BYTES = 50
33
+
34
+ # 无进展超时兜底(秒)——协议参数的默认值(gen_protocol 固化进
35
+ # protocol.json;engine/fake_agent 的签名默认与 CLI default 同源于此)。
36
+ DEFAULT_STALL_TIMEOUT = 600
37
+
21
38
  # ---------------------------------------------------------------
22
39
  # git 基础操作
23
40
  # ---------------------------------------------------------------
@@ -314,6 +331,9 @@ def cat_batch(bare, paths):
314
331
  finally:
315
332
  proc.stdin.close()
316
333
  proc.wait()
334
+ # 关输出管道:Popen 的 pipe 是文件对象,不关会留 ResourceWarning
335
+ # (wait() 之后 close 安全——进程已退出,无未读数据风险)
336
+ proc.stdout.close()
317
337
  return result
318
338
 
319
339
 
package/meeting_loop.py CHANGED
@@ -505,7 +505,7 @@ def make_responder(pure, fork_source=None, fork_cwd=None,
505
505
  else:
506
506
  reason_txt = "无进展超时(stall),未完全共识"
507
507
  # fork 模式(cwd=主项目)下“工作区根目录”有歧义——路径必须绝对
508
- result_path = os.path.join(workdir, "result.md")
508
+ result_path = os.path.join(workdir, meeting_fs.RESULT_MD)
509
509
  prompt = (f"讨论已收敛({reason_txt})。"
510
510
  f"请写 result.md 到 {result_path},总结讨论结论。")
511
511
  if retry: