Skip to content

feat(portal): live daemon status + recent-turns panel#59

Merged
Germey merged 1 commit into
mainfrom
feat/portal-status-panel
Jul 4, 2026
Merged

feat(portal): live daemon status + recent-turns panel#59
Germey merged 1 commit into
mainfrom
feat/portal-status-panel

Conversation

@acedatacloud-dev

Copy link
Copy Markdown
Member

What & why

Portal 之前是纯配置编辑器 —— 你从界面上看不出 channels start 到底跑没跑、跑了之后做了啥。现在守护进程会把心跳和一圈内容无关的 turn 指标写到 <config_dir>/status/,portal 顶部显示一张实时 「Daemon running / stopped + 最近对话」 卡(每 5s 轮询)。

  • channels/status.py:StatusStore —— 原子心跳 write/read + is_running()(60s 陈旧窗口判活)+ record_turn() 环(封顶 50 条)+ read_turns()(最新在前)。
  • observability.py:可选、故障隔离的 set_turn_sink(),在 log_turn 之后调用(不设 sink 时 turn 路径字节不变)。
  • channels start:写 run.json,把每个 TurnEvent 镜像进环,20s 心跳任务,干净退出时清 run.json
  • portal:status_overview() + GET /api/status;#status 卡(运行绿点、运行时长、最近 turn 显示为 adapter/instance · outcome · latency · chars —— 永远不含消息内容)。

Testing

  • ruff clean;全套件 498 pass / 1 skip(3 个 pre-existing Windows-only fail 无关)。
  • 8 个新测试:StatusStore round-trip / 环封顶 / 陈旧心跳、status_overview running/stopped、/api/status 路由、observability sink 注册+故障隔离、静态守卫。
  • Playwright:seed 一个「运行中」心跳 + 4 条 turn,卡正确显示 Daemon running — 2 channels · up 2m 56s + 4 条 turn(telegram/demo-tg · claude · ok · 2100ms · 120c 等)。

🤖 对抗评审

Reviewer: Claude general-purpose subagent(此机无 Codex),read-only,一轮。VERDICT: CLEAN:

  1. 心跳任务生命周期hb_tasktry 前定义,finally 里必被 cancel+await(无孤儿任务);write_run 异常被 suppress,循环存活。
  2. set_turn_sink 全局态 + 执行上下文log_turn 在 asyncio loop 内被调,record_turn 的 ≤50 行文件 IO <1ms,相对 provider 延迟可忽略;sink 异常在 log_turn 内捕获,绝不打断 turn;测试都 finally 清 sink。
  3. 原子写 + 单写者tempfile+os.replace 原子,读端不会读到撕裂文件;单 event loop 写者无交错;lines[-max_turns:] 封顶,无无限增长。
  4. 崩溃/陈旧 — 干净退出清 run.json;硬杀留下的心跳 >60s 判 stopped;心跳 20s vs 60s 窗口 3× 余量。
  5. 隐私 — 环 + 心跳只含指标 + endpoint(非密),无消息文本/回复体/token
  6. portal 健壮 + 前端 — 畸形 run.json/缺字段全类型守卫 → 默认 stopped;read_turns 跳坏行;status/ 不存在 → stopped;turn 字段经 el(...) 文本节点渲染,无 XSS。

RISK,无需 fix/rebut。

The portal was a pure config editor — you couldn't tell from it whether 'channels start' was actually running or what it had done. Now the daemon writes a heartbeat + a bounded ring of content-free turn metrics under <config_dir>/status/, and the portal shows a live 'Daemon running/stopped + recent turns' card (polls every 5s).

- channels/status.py: StatusStore (atomic heartbeat write/read + is_running via 60s-stale window; record_turn ring capped at 50; read_turns newest-first).

- observability.py: optional, failure-isolated set_turn_sink() called after log_turn (no sink = turn path unchanged).

- channels start: writes run.json, mirrors each TurnEvent into the ring, 20s heartbeat task, clears run.json on clean exit.

- portal: status_overview() + GET /api/status; #status card (running dot, uptime, recent turns as adapter/instance/outcome/latency/chars — NEVER message content).

8 new tests; full suite 498 pass/1 skip. Playwright-verified card renders running + 4 turns from a seeded heartbeat. Adversarial (Explore subagent): VERDICT CLEAN (heartbeat lifecycle, atomic writes, single-writer race, privacy content-free, sink failure-isolated).
@Germey Germey merged commit 5db6383 into main Jul 4, 2026
3 checks passed
@Germey Germey deleted the feat/portal-status-panel branch July 4, 2026 07:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants