pi-multi-viewers 0.2.2 → 0.4.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.
- package/AGENTS.md +26 -3
- package/README.md +7 -5
- package/docs/design.md +182 -2
- package/docs/reviews/2026-09-12-e2e17-thinking-level-analysis.md +374 -0
- package/docs/reviews/2026-09-13-e2e19-scoped-config-review.md +126 -0
- package/docs/reviews/2026-09-13-e2e20-pure-run-review.md +159 -0
- package/docs/reviews/2026-09-13-e2e21-postfix-review.md +257 -0
- package/docs/reviews/2026-09-13-e2e23-time-breakdown-analysis.md +175 -0
- package/docs/reviews/README.md +7 -2
- package/docs/test-methodology.md +100 -0
- package/fake_agent.py +11 -3
- package/meeting_fs.py +49 -5
- package/meeting_loop.py +46 -24
- package/mv_cli.py +363 -0
- package/observability.py +351 -29
- package/package.json +1 -1
- package/prompts/multi-viewers.md +1 -1
- package/scripts/check-residue.sh +39 -17
- package/scripts/mv.sh +10 -357
- package/scripts/pi-probe.sh +134 -0
- package/spec_gen.py +20 -17
- package/start_discussion.py +37 -24
- package/templates/spec-readme.md.tpl +17 -7
package/observability.py
CHANGED
|
@@ -9,6 +9,7 @@ human_viewer(observability 是"读"侧,human_viewer.incremental 是它
|
|
|
9
9
|
的进展数据源);不准 import 主文件。
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import datetime
|
|
12
13
|
import glob
|
|
13
14
|
import json
|
|
14
15
|
import os
|
|
@@ -326,27 +327,94 @@ def build_report(base):
|
|
|
326
327
|
f"{_dur(d['max_ms'] // 1000)} | rc≠0 {d['fails']} 次")
|
|
327
328
|
|
|
328
329
|
# ---- LLM 运行事实(session 文档化字段;流式预过滤,不整文件解析) ----
|
|
330
|
+
# ---- 终止原因(只报事实与计数)----
|
|
331
|
+
_report_termination(base, agents, out)
|
|
332
|
+
|
|
333
|
+
# ---- 每次唤醒构成表(零插桩事后推导)----
|
|
334
|
+
_report_wake_table(base, agents, out)
|
|
335
|
+
|
|
329
336
|
out.append("LLM(session 文档化字段):")
|
|
330
337
|
any_usage = False
|
|
331
338
|
for a in agents:
|
|
332
|
-
|
|
333
|
-
if not
|
|
339
|
+
m = _report_session_metrics(base, a)
|
|
340
|
+
if not m:
|
|
334
341
|
out.append(f" {a}: n/a")
|
|
335
342
|
continue
|
|
336
343
|
any_usage = True
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
344
|
+
u = m["usage"]
|
|
345
|
+
# usage 合计:reasoning ⊂ output(**不可相加**)——缺席时省略该括注
|
|
346
|
+
# (缺席 ≠ 0),出现时即使为 0 也写出
|
|
347
|
+
rea = ("" if u["reasoning"] is None
|
|
348
|
+
else f"(reasoning {_num(u['reasoning'])})")
|
|
349
|
+
out.append(f" {a}: 响应 {m['responses']} 次 | input {_num(u['input'])}"
|
|
350
|
+
f" | cacheRead {_num(u['cacheRead'])}"
|
|
351
|
+
f" | output {_num(u['output'])}{rea}")
|
|
352
|
+
# 按 stopReason 原值分组:**键集固定,不随数据增长**——新同类数字
|
|
353
|
+
# 只多一个键。error 那笔账(计数 + 时长)就在这里,这是 provider
|
|
354
|
+
# 失败从"完全不可见"变为"一行可读"的落点。
|
|
355
|
+
# 键 = 原值(含 None):解释性命名会因"长消息也是 toolUse"立刻过期。
|
|
356
|
+
parts = []
|
|
357
|
+
for k in sorted(m["stop"], key=lambda x: (x is None, x or "")):
|
|
358
|
+
d = m["stop"][k]
|
|
359
|
+
parts.append(f"{k if k is not None else '(无)'} "
|
|
360
|
+
f"{d['n']}({_dur(d['sec'])})")
|
|
361
|
+
out.append(f" stopReason:{' | '.join(parts)}")
|
|
340
362
|
if not any_usage:
|
|
341
363
|
out.append(" n/a(session 缺失,或无本轮数据——边界条目自 2026-09-11 "
|
|
342
364
|
"起写入,此前的老分析不适用)")
|
|
365
|
+
# ---- 档位对照:声明值(spec/pi-agent.json)vs 生效值(session) ----
|
|
366
|
+
out.append(_report_levels_line(base, agents))
|
|
343
367
|
out.append("(口径:进程跨度=pi 进程生命周期;输出=prompt 分段合计;"
|
|
344
368
|
"墙钟=commit 时间差——三者不可互替;"
|
|
369
|
+
"stopReason 括注 = **相邻条目 Δ 合计**(含每次唤醒首条响应;"
|
|
370
|
+
"跨唤醒空闲不进入 Δ——唤醒 prompt 本身是一条 user 条目);"
|
|
345
371
|
"消息数含流程信号(freezing/pass/concluded)与 human,"
|
|
346
372
|
"配额只计 meeting 发言)")
|
|
347
373
|
return out
|
|
348
374
|
|
|
349
375
|
|
|
376
|
+
def _report_levels_line(base, agents):
|
|
377
|
+
"""档位对照行:**声明值 vs 生效值**(e2e17 评审 §1)。
|
|
378
|
+
|
|
379
|
+
为什么需要:声明值(`pi-agent.json.thinking`)从来没人跟生效值对照过——
|
|
380
|
+
探测失败 → 静默取 DEFAULT_THINKING,spec 文件表面完全正常。这一行是那个
|
|
381
|
+
缺口的最小可见性形态:**零新增字段**(两个值都已有家,只是从没被并列读出)、
|
|
382
|
+
**零新增分支**(集合比较)。
|
|
383
|
+
|
|
384
|
+
生效值来源 = session 的 `thinking_level_change` 条目(pi 在会话缺该条目时
|
|
385
|
+
写入,位于边界之后)——**本仓只读,不复刻 pi 的解析链**(那是 pi 的配置)。
|
|
386
|
+
fail-open:任一侧读不到 → 显式 n/a(不猜、不写 0)。
|
|
387
|
+
"""
|
|
388
|
+
declared, effective = {}, {}
|
|
389
|
+
for a in agents:
|
|
390
|
+
try:
|
|
391
|
+
with open(os.path.join(base, f"work-{a}", "pi-agent.json")) as f:
|
|
392
|
+
v = json.load(f).get("thinking") or ""
|
|
393
|
+
if v:
|
|
394
|
+
declared[a] = v
|
|
395
|
+
except (OSError, ValueError):
|
|
396
|
+
continue
|
|
397
|
+
levels = _report_session_levels(base, a)
|
|
398
|
+
effective[a] = levels
|
|
399
|
+
if not declared and not effective:
|
|
400
|
+
return "档位:n/a(无 pi-agent.json 且无 session 档位条目)"
|
|
401
|
+
d_set = set(declared.values())
|
|
402
|
+
e_set = {x for v in effective.values() for x in v}
|
|
403
|
+
d_txt = "、".join(sorted(d_set)) if d_set else "n/a"
|
|
404
|
+
e_txt = "、".join(sorted(e_set)) if e_set else "n/a"
|
|
405
|
+
if d_set and e_set:
|
|
406
|
+
same = (
|
|
407
|
+
f"✓ 一致" if d_set == e_set
|
|
408
|
+
# 不一致是两个方向的异常:声明了没生效(写错/被覆盖),或生效值
|
|
409
|
+
# 不在声明里(外部改档/会话遗留)——两种都要人看到
|
|
410
|
+
else f"⚠ 不一致(声明 {d_txt} / 生效 {e_txt})")
|
|
411
|
+
else:
|
|
412
|
+
same = "(一侧 n/a,无法对照)"
|
|
413
|
+
detail = "、".join(f"{a} {v or 'n/a'}"
|
|
414
|
+
for a, v in ((a, declared.get(a, "")) for a in agents))
|
|
415
|
+
return f"档位:声明 {d_txt}({detail})| 生效 {e_txt} | {same}"
|
|
416
|
+
|
|
417
|
+
|
|
350
418
|
def _dur(sec):
|
|
351
419
|
"""人类可读时长(口径由调用方在同一行标注——进程跨度/墙钟/间隔)。"""
|
|
352
420
|
sec = int(sec)
|
|
@@ -361,6 +429,195 @@ def _hhmm(ts):
|
|
|
361
429
|
return time.strftime("%H:%M:%S", time.localtime(ts))
|
|
362
430
|
|
|
363
431
|
|
|
432
|
+
def _parse_wake_windows(base, agent):
|
|
433
|
+
"""从一个 agent 的 loop log 解析每次唤醒的窗口(零判定输入——只读登记行)。
|
|
434
|
+
|
|
435
|
+
返回 [(spawn_epoch, exit_epoch, elapsed_ms, rc, retries)];日志时间戳是
|
|
436
|
+
**本地无时区**(`[2026-09-13T19:43:10.123]`)→ 转 epoch 再与 session 的
|
|
437
|
+
UTC 时间戳 / git 的 epoch 对齐。fail-open:文件缺失 → []。
|
|
438
|
+
"""
|
|
439
|
+
path = os.path.join(base, f"loop-{agent}.log")
|
|
440
|
+
wakes, cur = [], None
|
|
441
|
+
try:
|
|
442
|
+
lines = open(path, encoding="utf-8", errors="replace").read().splitlines()
|
|
443
|
+
except OSError:
|
|
444
|
+
return []
|
|
445
|
+
for line in lines:
|
|
446
|
+
m = re.match(r"\[([^\]]+)\]\s+\S+:\s+(.*)$", line)
|
|
447
|
+
if not m:
|
|
448
|
+
continue
|
|
449
|
+
try:
|
|
450
|
+
dt = datetime.datetime.fromisoformat(m.group(1))
|
|
451
|
+
except ValueError:
|
|
452
|
+
continue
|
|
453
|
+
epoch = time.mktime(dt.timetuple()) + dt.microsecond / 1e6
|
|
454
|
+
body = m.group(2)
|
|
455
|
+
if body.startswith("唤醒 pi"):
|
|
456
|
+
cur = [epoch, None, 0, 0, 0]
|
|
457
|
+
elif cur is not None and body.startswith("pi 完成"):
|
|
458
|
+
mm = re.search(r"elapsed_ms=(\d+) rc=(-?\d+)", body)
|
|
459
|
+
if mm:
|
|
460
|
+
cur[1] = epoch
|
|
461
|
+
cur[2] = int(mm.group(1))
|
|
462
|
+
cur[3] = int(mm.group(2))
|
|
463
|
+
wakes.append(tuple(cur))
|
|
464
|
+
cur = None
|
|
465
|
+
elif cur is not None and "无产出" in body and "重试" in body:
|
|
466
|
+
cur[4] += 1
|
|
467
|
+
return wakes
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _agent_event_roles(base, agent):
|
|
471
|
+
"""该 agent 本轮(边界之后)的事件流:[(epoch, role)]——顺序读取,fail-open。"""
|
|
472
|
+
fp = _agent_session_file(base, agent)
|
|
473
|
+
if not fp:
|
|
474
|
+
return []
|
|
475
|
+
out = []
|
|
476
|
+
for ev in meeting_fs.iter_after_boundary(fp):
|
|
477
|
+
m = ev.get("message") or {}
|
|
478
|
+
role, ts = m.get("role"), ev.get("timestamp")
|
|
479
|
+
if not role or not ts:
|
|
480
|
+
continue
|
|
481
|
+
try:
|
|
482
|
+
out.append((datetime.datetime.fromisoformat(
|
|
483
|
+
ts.replace("Z", "+00:00")).timestamp(), role))
|
|
484
|
+
except ValueError:
|
|
485
|
+
continue
|
|
486
|
+
return out
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _wake_rows(base, agent, commits):
|
|
490
|
+
"""每次唤醒的构成记录(零插桩事后推导;精度契约见 design.md 观测面契约)。
|
|
491
|
+
|
|
492
|
+
每条 = 四端点(spawn / 首事件 / 末事件 / exit)+ 往返数 + assistant Δ +
|
|
493
|
+
toolResult Δ + retry + 本唤醒内的 commit。**不承诺**单次调用内的
|
|
494
|
+
TTFT/生成拆分(session 只有完成时间戳)。
|
|
495
|
+
"""
|
|
496
|
+
wakes = _parse_wake_windows(base, agent)
|
|
497
|
+
if not wakes:
|
|
498
|
+
return []
|
|
499
|
+
events = _agent_event_roles(base, agent)
|
|
500
|
+
rows = []
|
|
501
|
+
for i, (t0, t1, ms, rc, retries) in enumerate(wakes, 1):
|
|
502
|
+
t1 = t1 or (t0 + ms / 1000)
|
|
503
|
+
row = {"n": i, "spawn": t0, "exit": t1, "ms": ms, "rc": rc,
|
|
504
|
+
"retries": retries, "rounds": 0, "d_assist": 0.0,
|
|
505
|
+
"d_tool": 0.0, "first": None, "last": None, "commit": None}
|
|
506
|
+
prev = None
|
|
507
|
+
for t, role in events:
|
|
508
|
+
if not (t0 - 2 <= t <= t1 + 2):
|
|
509
|
+
continue
|
|
510
|
+
if role == "assistant":
|
|
511
|
+
row["rounds"] += 1
|
|
512
|
+
if prev is not None and t >= prev:
|
|
513
|
+
row["d_assist"] += t - prev
|
|
514
|
+
elif role == "toolResult" and prev is not None and t >= prev:
|
|
515
|
+
row["d_tool"] += t - prev
|
|
516
|
+
if row["first"] is None:
|
|
517
|
+
row["first"] = t
|
|
518
|
+
row["last"] = t
|
|
519
|
+
prev = t
|
|
520
|
+
for c in commits:
|
|
521
|
+
if t0 <= c <= t1:
|
|
522
|
+
row["commit"] = c
|
|
523
|
+
break
|
|
524
|
+
rows.append(row)
|
|
525
|
+
return rows
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _report_wake_table(base, agents, out):
|
|
529
|
+
"""每次唤醒构成表 + 聚合(只读 loop log / session / bare 三处已有事实)。"""
|
|
530
|
+
r = meeting_fs.run_git(meeting_fs.bare_of_base(base), "log",
|
|
531
|
+
"--format=%ct", check=False)
|
|
532
|
+
commits = sorted(int(x) for x in r.stdout.split() if x.isdigit())
|
|
533
|
+
any_row = False
|
|
534
|
+
for agent in agents:
|
|
535
|
+
rows = _wake_rows(base, agent, commits)
|
|
536
|
+
if not rows:
|
|
537
|
+
continue
|
|
538
|
+
if not any_row:
|
|
539
|
+
out.append("唤醒构成(每次一行;Δ 助手/Δ 工具 = 相邻条目差,"
|
|
540
|
+
"不含跨唤醒空闲):")
|
|
541
|
+
any_row = True
|
|
542
|
+
start = tail = inner = 0.0
|
|
543
|
+
decomposed = no_ev = 0 # 无 session 事件的唤醒无法分解(不冒充 0)
|
|
544
|
+
for row in rows:
|
|
545
|
+
pre = (row["first"] - row["spawn"]) if row["first"] else 0.0
|
|
546
|
+
post = (row["exit"] - row["last"]) if row["last"] else 0.0
|
|
547
|
+
inner += (row["last"] - row["first"]) if row["first"] else 0.0
|
|
548
|
+
start += pre
|
|
549
|
+
tail += post
|
|
550
|
+
if row["first"]:
|
|
551
|
+
decomposed += row["ms"] / 1000
|
|
552
|
+
else:
|
|
553
|
+
no_ev += 1
|
|
554
|
+
retry = f" retry×{row['retries']}" if row["retries"] else ""
|
|
555
|
+
commit = _hhmm(row["commit"]) if row["commit"] else "—"
|
|
556
|
+
out.append(
|
|
557
|
+
f" {agent} #{row['n']:>2} {_hhmm(row['spawn'])}"
|
|
558
|
+
f" 启动{pre:4.1f}s"
|
|
559
|
+
f" 内{_dur(row['last'] - row['first']) if row['first'] else 'n/a':>7}"
|
|
560
|
+
f" 收尾{post:4.1f}s | 往返 {row['rounds']:>2}"
|
|
561
|
+
f" | Δ助手 {_dur(row['d_assist']):>6} Δ工具 {_dur(row['d_tool']):>6}"
|
|
562
|
+
f" | commit {commit}{retry}"
|
|
563
|
+
+ (" rc≠0" if row["rc"] else ""))
|
|
564
|
+
total = sum(x["ms"] for x in rows) / 1000
|
|
565
|
+
out.append(f" {agent} 合计:{len(rows)} 唤 | 跨度 {_dur(total)} = "
|
|
566
|
+
f"启动前 {_dur(start)} + 事件内 {_dur(inner)} + 收尾 {_dur(tail)}"
|
|
567
|
+
f" | 平均 {total / len(rows):.1f}s"
|
|
568
|
+
f" | 往返 {sum(x['rounds'] for x in rows)}"
|
|
569
|
+
f" | retry {sum(x['retries'] for x in rows)}"
|
|
570
|
+
+ (f" | 未分解 {no_ev} 次(无 session 事件)" if no_ev else ""))
|
|
571
|
+
# 对账恒等式(e2e23 分析产出):**仅有事件的那部分**应满足
|
|
572
|
+
# Σ(启动+事件内+收尾) = Σ进程跨度;差>2s 说明时间源没对齐
|
|
573
|
+
# (loop 日志=本地无时区 / session=UTC)。只报告事实,不进任何代码
|
|
574
|
+
# 分支(跨度阈值不得参与判定,观测面契约)。
|
|
575
|
+
diff = abs((start + inner + tail) - decomposed)
|
|
576
|
+
if diff > 2:
|
|
577
|
+
out.append(f" 恒等校验:Σ(启动+事件内+收尾) 与可分解跨度 差 "
|
|
578
|
+
f"{diff:.1f}s——时间源未对齐(日志=本地时区,"
|
|
579
|
+
f"session=UTC)")
|
|
580
|
+
if not any_row:
|
|
581
|
+
out.append("唤醒构成:n/a(无 loop log 或无登记行)")
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _report_termination(base, agents, out):
|
|
585
|
+
"""终止原因——**只报事实与计数**(不做评分/判定;观测面契约)。"""
|
|
586
|
+
bare = meeting_fs.bare_of_base(base)
|
|
587
|
+
msgs = meeting_engine.each_agent_messages(bare, agents)
|
|
588
|
+
types = {}
|
|
589
|
+
for a in agents:
|
|
590
|
+
for fm in msgs.get(a, []): # each_agent_messages → frontmatter dict 列表
|
|
591
|
+
t = (fm or {}).get("type")
|
|
592
|
+
types[t] = types.get(t, 0) + 1
|
|
593
|
+
has_result = bool(meeting_fs.run_git(bare, "show", "HEAD:result.md",
|
|
594
|
+
check=False).stdout.strip())
|
|
595
|
+
stalls = 0
|
|
596
|
+
for f in glob.glob(os.path.join(base, "loop-*.log")):
|
|
597
|
+
try:
|
|
598
|
+
txt = open(f, encoding="utf-8", errors="replace").read()
|
|
599
|
+
except OSError:
|
|
600
|
+
continue
|
|
601
|
+
stalls += txt.count("超时兜底") + txt.count("声明接管")
|
|
602
|
+
if not meeting_fs.read_protocol(bare).get("participants"):
|
|
603
|
+
out.append("终止:n/a(协议不可读)")
|
|
604
|
+
return
|
|
605
|
+
if stalls:
|
|
606
|
+
why = "stall 接管"
|
|
607
|
+
elif types.get("pass"):
|
|
608
|
+
why = "共识(RR 全体 pass)"
|
|
609
|
+
elif types.get("freezing") or types.get("all-freezing"):
|
|
610
|
+
why = "配额耗尽(冻结,无 pass)"
|
|
611
|
+
elif has_result:
|
|
612
|
+
why = "已收尾(无 pass/冻结记录)"
|
|
613
|
+
else:
|
|
614
|
+
why = "未完成"
|
|
615
|
+
out.append(f"终止:{why} | freezing {types.get('freezing', 0)}"
|
|
616
|
+
f" / all-freezing {types.get('all-freezing', 0)}"
|
|
617
|
+
f" / pass {types.get('pass', 0)} / stall 接管行 {stalls}"
|
|
618
|
+
+ (" | result.md 已提交" if has_result else ""))
|
|
619
|
+
|
|
620
|
+
|
|
364
621
|
def _report_wake_fields(base):
|
|
365
622
|
"""解析 loop-*.log 的登记字段(`elapsed_ms` / `rc`)。
|
|
366
623
|
|
|
@@ -390,48 +647,113 @@ def _report_wake_fields(base):
|
|
|
390
647
|
return out
|
|
391
648
|
|
|
392
649
|
|
|
393
|
-
def
|
|
394
|
-
"""
|
|
650
|
+
def _report_session_metrics(base, agent):
|
|
651
|
+
"""该 agent **本轮**(边界之后)的 session 运行事实——**单一适配器**。
|
|
395
652
|
|
|
396
653
|
字段来源 = pi 的**文档化** session schema(`docs/session-format.md`:
|
|
397
|
-
`usage` / `stopReason
|
|
398
|
-
|
|
654
|
+
`usage` / `stopReason` / `timestamp`)——**只读已有家,不新增记录**
|
|
655
|
+
(同一事实两处 = 双写;loop log 的不变量是零判定输入)。
|
|
656
|
+
|
|
657
|
+
返回:{responses, usage: {input, cacheRead, output, reasoning},
|
|
658
|
+
stop: {stopReason 原值: {"n": 次数, "sec": 响应跨度合计}}}
|
|
659
|
+
`usage["reasoning"]` = None 表示**从未出现**(缺席 ≠ 0)。
|
|
660
|
+
|
|
661
|
+
响应跨度口径:`Δt = ts(本条) − ts(紧邻前一条事件)`——单次遍历顺序读取,
|
|
662
|
+
不需要随机访问。**每次唤醒的首条响应也计入**:每次唤醒都会在 session 里
|
|
663
|
+
写入 user 条目(唤醒 prompt),跨唤醒空闲落在"上一唤醒末条 → 本次 user
|
|
664
|
+
条目"之间,**从不进入任何 Δ**(e2e20 评审批实测:本场 7 个唤醒 user 条目
|
|
665
|
+
每个后面直接跟 assistant;首响 Δ 与非首响同量级,无分钟级样本)。
|
|
666
|
+
**不做"首响不计时"**:那是基于错误归因的排除——当时以为 e2e19 的
|
|
667
|
+
"Δ 合计 33m09s > 进程跨度 13m09s"来自跨唤醒空闲,实际根因是边界子串
|
|
668
|
+
误判(`iter_after_boundary` 把历史条目算进本轮;修好后 ΣΔ ≤ 墙钟成立),
|
|
669
|
+
排除掉的是真数据里常最贵的那条。
|
|
670
|
+
error 类单列(usage 全零)——不并入也不丢弃:否则 provider 抖动会被算成
|
|
671
|
+
"生成变慢"(e2e14 评审)。
|
|
399
672
|
fail-open:文件缺失/字段变 → 返回 {}。
|
|
400
673
|
"""
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
sid = json.load(f).get("sessionID") or ""
|
|
404
|
-
except (OSError, ValueError):
|
|
405
|
-
sid = ""
|
|
406
|
-
if not sid:
|
|
407
|
-
return {}
|
|
408
|
-
fp = os.path.join(base, "pi-sessions", f"fork-src-{sid}.jsonl")
|
|
409
|
-
if not os.path.isfile(fp):
|
|
674
|
+
fp = _agent_session_file(base, agent)
|
|
675
|
+
if not fp:
|
|
410
676
|
return {}
|
|
411
677
|
# **只统计边界之后的条目**(本轮运行事实)——fork 携带的历史条目里也
|
|
412
678
|
# 有大量 assistant+usage,全文件统计会把主 pi 的历史算成本次分析的
|
|
413
679
|
# 消耗(2026-09-11 实测:717 条 fork 历史被算成"本轮 367 次响应 /
|
|
414
680
|
# input 1.2M")。边界由 append_handoff_turns 写入(显式登记,非推断)。
|
|
415
|
-
|
|
681
|
+
r = {"responses": 0,
|
|
682
|
+
"usage": {"input": 0, "cacheRead": 0, "output": 0,
|
|
683
|
+
"reasoning": None},
|
|
684
|
+
"stop": {}}
|
|
685
|
+
prev_ts = None
|
|
416
686
|
for ev in meeting_fs.iter_after_boundary(fp):
|
|
417
687
|
m = ev.get("message") or {}
|
|
418
688
|
if m.get("role") != "assistant":
|
|
689
|
+
prev_ts = ev.get("timestamp") or prev_ts
|
|
419
690
|
continue
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
691
|
+
r["responses"] += 1
|
|
692
|
+
reason = m.get("stopReason")
|
|
693
|
+
dur = _delta_seconds(prev_ts, ev.get("timestamp"))
|
|
694
|
+
d = r["stop"].setdefault(reason, {"n": 0, "sec": 0})
|
|
695
|
+
d["n"] += 1
|
|
696
|
+
if dur is not None:
|
|
697
|
+
d["sec"] += dur
|
|
423
698
|
usage = m.get("usage") or {}
|
|
424
|
-
for k, key in (("input", "input"), ("cacheRead", "
|
|
699
|
+
for k, key in (("input", "input"), ("cacheRead", "cacheRead"),
|
|
425
700
|
("output", "output")):
|
|
426
701
|
v = usage.get(k)
|
|
427
702
|
if isinstance(v, int):
|
|
428
|
-
|
|
429
|
-
|
|
703
|
+
r["usage"][key] += v
|
|
704
|
+
rv = usage.get("reasoning")
|
|
705
|
+
if isinstance(rv, int):
|
|
706
|
+
r["usage"]["reasoning"] = (r["usage"]["reasoning"] or 0) + rv
|
|
707
|
+
prev_ts = ev.get("timestamp") or prev_ts
|
|
708
|
+
if not r["responses"]:
|
|
430
709
|
return {}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
710
|
+
return r
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _delta_seconds(prev_iso, cur_iso):
|
|
714
|
+
"""两个 ISO 时间戳的秒差;任一缺失/不可解析 → None(缺席 ≠ 0)。"""
|
|
715
|
+
if not prev_iso or not cur_iso:
|
|
716
|
+
return None
|
|
717
|
+
try:
|
|
718
|
+
a = datetime.datetime.fromisoformat(prev_iso.replace("Z", "+00:00"))
|
|
719
|
+
b = datetime.datetime.fromisoformat(cur_iso.replace("Z", "+00:00"))
|
|
720
|
+
except ValueError:
|
|
721
|
+
return None
|
|
722
|
+
d = (b - a).total_seconds()
|
|
723
|
+
return d if d >= 0 else None
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _agent_session_file(base, agent):
|
|
727
|
+
"""该 agent 的 fork 源 session 文件路径(找不到 → "")。"""
|
|
728
|
+
try:
|
|
729
|
+
with open(os.path.join(base, f"status-{agent}.json")) as f:
|
|
730
|
+
sid = json.load(f).get("sessionID") or ""
|
|
731
|
+
except (OSError, ValueError):
|
|
732
|
+
sid = ""
|
|
733
|
+
if not sid:
|
|
734
|
+
return ""
|
|
735
|
+
fp = os.path.join(base, "pi-sessions", f"fork-src-{sid}.jsonl")
|
|
736
|
+
return fp if os.path.isfile(fp) else ""
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _report_session_levels(base, agent):
|
|
740
|
+
"""该 agent 本轮**生效的 thinking 档位**(去重、保序)——session 侧的家。
|
|
741
|
+
|
|
742
|
+
pi 在会话缺 `thinking_level_change` 条目时写入它(每次打开会话至多一次),
|
|
743
|
+
且位于我们的边界之后——所以边界过滤后读到的就是"本场生效值"。
|
|
744
|
+
**本仓只读,不复刻 pi 的解析链**(`modelThinkingLevels` →
|
|
745
|
+
`defaultThinkingLevel` 是 pi 的配置,复刻 = 两处实现/必漂移)。
|
|
746
|
+
"""
|
|
747
|
+
fp = _agent_session_file(base, agent)
|
|
748
|
+
if not fp:
|
|
749
|
+
return []
|
|
750
|
+
out = []
|
|
751
|
+
for ev in meeting_fs.iter_after_boundary(fp):
|
|
752
|
+
if ev.get("type") == "thinking_level_change":
|
|
753
|
+
lv = ev.get("thinkingLevel")
|
|
754
|
+
if lv and lv not in out:
|
|
755
|
+
out.append(lv)
|
|
756
|
+
return out
|
|
435
757
|
|
|
436
758
|
|
|
437
759
|
def _num(n):
|
package/package.json
CHANGED
package/prompts/multi-viewers.md
CHANGED
|
@@ -53,7 +53,7 @@ argument-hint: '"<主题>"'
|
|
|
53
53
|
视角之间**互补或对立都可以**——对立产生的分歧正是多视角分析的价值。
|
|
54
54
|
每个视角任务书必须包含:
|
|
55
55
|
|
|
56
|
-
1. **单一 lenses**:明确该 agent
|
|
56
|
+
1. **单一 lenses**:明确该 agent 用什么角度看(效率/简单化/安全/……),
|
|
57
57
|
要求"所有观点必须从该视角出发"
|
|
58
58
|
2. **不越界**:"其它视角由别的参与者负责,你不要越界展开"(**不要**列举
|
|
59
59
|
具体是哪几个视角——参与者会变,列举会过期)
|
package/scripts/check-residue.sh
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
# 用法: ./scripts/check-residue.sh [--verbose]
|
|
5
5
|
# 退出码: 0 = 干净;1 = 有残留(列出清单)
|
|
6
6
|
#
|
|
7
|
+
# **调用纪律**(2026-09-12):看退出码 + 末行 `[residue] ...` 汇总;
|
|
8
|
+
# 不要只 grep 某一类残留——那会把其它类整块滤掉,把"有残留"跑成"看起来
|
|
9
|
+
# 干净"(实测:grep 残留-2|残留-3 滤掉了 4 个 session 残留)。
|
|
10
|
+
#
|
|
7
11
|
# 检查三类测试残留形态(2026-09-09 建立——此前靠人工记忆逐项检查,
|
|
8
12
|
# 反复漏检:afk-e2e 引导 session 漏删即反例):
|
|
9
13
|
# 1. 主 pi 侧 session:~/.pi/agent/sessions/<编码目录>/*.jsonl 中
|
|
@@ -40,6 +44,9 @@ is_registered() {
|
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
RESIDUE=0
|
|
47
|
+
N_SESSION=0
|
|
48
|
+
N_PROC=0
|
|
49
|
+
N_DIR=0
|
|
43
50
|
|
|
44
51
|
say() { [ "$1" = "--verbose" ] || [ -n "$VERBOSE" ] && echo "$2"; }
|
|
45
52
|
|
|
@@ -64,16 +71,25 @@ for d in "$SESS_DIR"/*/; do
|
|
|
64
71
|
"$w"*|"$w") whitelisted=1 ;;
|
|
65
72
|
esac
|
|
66
73
|
done
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
# 归属判定(2026-09-09 起):登记日志回查——已登记=测试产物(可删),
|
|
75
|
+
# 未登记=非本流程创建(勿删,提示加白名单)。首条消息摘要仅作补充
|
|
76
|
+
# 信息(不作删除依据)。
|
|
77
|
+
# **登记优先于白名单**(2026-09-12 修):登记是**显式事实**(创建即
|
|
78
|
+
# 登记),白名单是**按 cwd 的猜测**。此前白名单在前 → 探针跑在白名单
|
|
79
|
+
# 项目目录(如 --print 冒烟在 /root/pi-agents-helper)时整个文件被跳过、
|
|
80
|
+
# 连"已登记"都不报——实测漏检(那次 4 个漏检 session 里有 1 个正是此形)。
|
|
81
|
+
if is_registered "$f"; then
|
|
82
|
+
# 已登记 = **本流程测试产物**(可删)——但"还存在"本身就是残留:
|
|
83
|
+
# 验收语义是"清理完毕"→ 计入 RESIDUE(归属标记只决定谁来删)
|
|
84
|
+
owner="[已登记-测试产物,可删]"
|
|
85
|
+
RESIDUE=1
|
|
86
|
+
elif [ "$whitelisted" = "0" ]; then
|
|
87
|
+
owner="[未登记-非本流程创建,勿删]"
|
|
88
|
+
RESIDUE=1
|
|
89
|
+
else
|
|
90
|
+
continue # 白名单 cwd 且未登记 = 用户自己的长期会话,不是残留
|
|
91
|
+
fi
|
|
92
|
+
N_SESSION=$((N_SESSION + 1))
|
|
77
93
|
first_user=$(python3 -c "
|
|
78
94
|
import json, sys
|
|
79
95
|
try:
|
|
@@ -87,10 +103,9 @@ try:
|
|
|
87
103
|
except Exception:
|
|
88
104
|
print('')
|
|
89
105
|
" 2>/dev/null)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
fi
|
|
106
|
+
echo "[残留-1] session(24h 内 cwd=$cwd,创建 $(stat -c %y "$f" | cut -d. -f1))$owner"
|
|
107
|
+
echo " 首条消息: ${first_user:-(无 user 消息)}"
|
|
108
|
+
echo " 路径: $f"
|
|
94
109
|
done
|
|
95
110
|
done
|
|
96
111
|
|
|
@@ -128,6 +143,7 @@ for d in /proc/[0-9]*; do
|
|
|
128
143
|
*/meeting_loop.py)
|
|
129
144
|
echo "[残留-2] meeting_loop 进程 PID=$pid: $(ps -p "$pid" -o cmd= 2>/dev/null)"
|
|
130
145
|
RESIDUE=1
|
|
146
|
+
N_PROC=$((N_PROC + 1))
|
|
131
147
|
LOOP_PIDS="$LOOP_PIDS $pid"
|
|
132
148
|
break
|
|
133
149
|
;;
|
|
@@ -143,12 +159,14 @@ for d in /proc/[0-9]*; do
|
|
|
143
159
|
# loop 已退出、pi 被 init 收养(PPID=1)——孤儿残留
|
|
144
160
|
echo "[残留-2] pi 进程 PID=$pid(孤儿:PPID=1,其 loop 已退出)"
|
|
145
161
|
RESIDUE=1
|
|
162
|
+
N_PROC=$((N_PROC + 1))
|
|
146
163
|
continue
|
|
147
164
|
fi
|
|
148
165
|
case " $LOOP_PIDS " in
|
|
149
166
|
*" $ppid "*)
|
|
150
167
|
echo "[残留-2] pi 进程 PID=$pid(父 meeting_loop PID=$ppid)"
|
|
151
168
|
RESIDUE=1
|
|
169
|
+
N_PROC=$((N_PROC + 1))
|
|
152
170
|
;;
|
|
153
171
|
# 其余 comm=pi 的进程不报:用户自己正在用的 pi 会话(PPID 是
|
|
154
172
|
# pi-web/shell 等)不是残留——"名称 + PPID"两个条件同时成立才判定
|
|
@@ -165,6 +183,7 @@ for d in mv-*/; do
|
|
|
165
183
|
esac
|
|
166
184
|
echo "[残留-3] 分析目录: $PWD/$d"
|
|
167
185
|
RESIDUE=1
|
|
186
|
+
N_DIR=$((N_DIR + 1))
|
|
168
187
|
done
|
|
169
188
|
|
|
170
189
|
# 3b. 结构形态:任何含 repo.git/ + pi-sessions/ 的目录 = 讨论环境
|
|
@@ -181,10 +200,13 @@ for base in "$PWD" "${TMPDIR:-/tmp}"; do
|
|
|
181
200
|
SEEN_ENVS="$SEEN_ENVS $d"
|
|
182
201
|
echo "[残留-3] 讨论环境: $d($(du -sh "$d" 2>/dev/null | cut -f1))"
|
|
183
202
|
RESIDUE=1
|
|
203
|
+
N_DIR=$((N_DIR + 1))
|
|
184
204
|
done < <(find "$base" -maxdepth 3 -type d -name repo.git 2>/dev/null)
|
|
185
205
|
done
|
|
186
206
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
207
|
+
# 汇总行:**结论只在这一行**——调用方看这一行 + 退出码即可,
|
|
208
|
+
# 不必(也不得)只 grep 某一类(2026-09-12:调用方 grep "残留-2|残留-3"
|
|
209
|
+
# 把 [残留-1] 整类滤掉,把有残留跑成"看起来干净")。
|
|
210
|
+
echo "[residue] session=$N_SESSION 进程=$N_PROC 目录=$N_DIR →" \
|
|
211
|
+
"$([ "$RESIDUE" = "0" ] && echo 干净 || echo 有残留)"
|
|
190
212
|
exit "$RESIDUE"
|