Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions external_plugins/session-stats/.zcode-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "session-stats",
"version": "0.3.0",
"description": "Session statistics for ZCode: turns/steps, model vs tool time, TTFT, TPS (average + latest request), token usage and cache hit rate. Zero third-party dependencies.",
"description_i18n": {
"en": "Session statistics for ZCode: turns/steps, model vs tool time, TTFT, TPS (average + latest request), token usage and cache hit rate. Zero third-party dependencies.",
"zh-CN": "会话统计:轮次/步数、模型/工具耗时、TTFT、TPS(会话平均+最近请求)、token 用量与缓存命中率。零第三方依赖。"
},
"author": {
"name": "gary.liu"
},
"keywords": [
"stats",
"usage",
"tokens",
"cache",
"panel",
"mcp-apps"
],
"license": "MIT",
"mcpServers": {
"session-stats": {
"type": "stdio",
"command": "python",
"args": [
"${ZCODE_PLUGIN_ROOT}/server/stats_server.py"
],
"env": {
"PYTHONUTF8": "1",
"PYTHONIOENCODING": "utf-8"
}
}
},
"ui": {
"surfaces": [
{
"id": "stats-panel",
"title": {
"en": "Session Stats",
"zh-CN": "会话统计"
},
"server": "session-stats",
"resourceUri": "ui://session-stats/panel.html",
"availability": "session"
}
]
},
"commands": "./commands"
}
66 changes: 66 additions & 0 deletions external_plugins/session-stats/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# session-stats

Session statistics for ZCode: turns/steps, model vs tool time, TTFT, TPS (average +
latest request), token usage and cache hit rate. Zero third-party dependencies.

## Components

- **MCP tool `get_stats`** — statistics for the current session (including subagents,
aggregated via `session.parent_id`) or today's aggregate across sessions.
- **MCP App side panel** (`ui.surfaces`) — stat pills with a details popup
(timings, token buckets, cache hit, per-model breakdown), auto-refresh every 5 s,
follows session switching and the live theme.
- **`/stats` command** — text card rendering of the same numbers.
- **SessionStart hook** — writes the current session id into the plugin data dir so
the server can resolve "current session" without extra round-trips.

## Data source and metric definitions

Everything is read **read-only** from the local session database
`~/.zcode/cli/db/db.sqlite` (SQLite URI `mode=ro`, `busy_timeout=5000`, WAL-safe).
Measured against ZCode 3.17.1:

| Metric | Definition |
| --- | --- |
| Turns | distinct `turn_id` of the main session |
| Steps | distinct `logical_request_id` (retries excluded; failed/cancelled counted) |
| Model time | Σ `duration_ms` |
| Tool time | Σ `tool_usage.duration_ms` |
| TTFT avg | AVG(`time_to_first_token_ms`) |
| TPS | Σ output ÷ Σ(completed_at − first_token_at); "live" TPS = latest completed request |
| Tokens | Σ input + Σ output (`input_tokens` already includes cache reads — measured) |
| Cache hit | cache_read ÷ input |

The database schema is an implementation detail of the host (not a stable API) and
usage rows are retained for 30 days; queries degrade gracefully when columns are
missing.

## Side effects

- Reads the local session database (read-only).
- Starts a loopback-only HTTP service (`127.0.0.1:31710`) that serves the statistics
JSON and the panel's dynamic script to ZCode's renderer. It answers only requests
without a cross-origin `Origin` header (i.e. the app's own `file://` pages); other
origins get 403.
- The SessionStart hook writes one small file (`current_session`) into the
plugin-owned data directory.
- No network access, no credentials, no telemetry.

## Install

ZCode Desktop → **Plugin Marketplace** → add the marketplace that carries this plugin
(the official `zcode-plugins` marketplace after review, or a local directory) →
**Install session-stats** → restart or open a new session → run `/stats` or open the
"Session Stats" side panel.

## Optional composer pills (host patch, experimental)

A stats strip centered under the composer input can be enabled with a patch to the
host `app.asar` (see the upstream project's `patches/` directory). The patch is
idempotent, backs up the host file per version and supports restore; it must be
re-applied after ZCode updates. This is **not** part of the marketplace plugin and
is purely optional.

## License

MIT
51 changes: 51 additions & 0 deletions external_plugins/session-stats/README_CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# session-stats — ZCode 会话统计插件

ZCode 会话统计插件:轮次/步数、模型/工具耗时、
TTFT、TPS(会话平均 + 最近请求)、token 用量与缓存命中率。零第三方依赖。

## 组件

- **MCP 工具 `get_stats`**:当前会话(含子代理,经 `session.parent_id` 聚合)
或今日汇总的完整统计。
- **MCP App 侧栏面板**(`ui.surfaces`):统计卡片 + 明细弹窗(耗时、token 分桶、
缓存命中、模型分布),5 秒轮询自动刷新,跟随会话切换与主题。
- **`/stats` 命令**:同样数字的文字卡片输出。
- **SessionStart hook**:把当前会话 id 写入插件数据目录,供服务端解析「当前会话」。

## 数据源与指标口径

全部**只读**读取本地会话数据库 `~/.zcode/cli/db/db.sqlite`(SQLite URI `mode=ro` +
`busy_timeout=5000`,WAL 下并发安全)。口径对 ZCode 3.17.1 实测定案:

| 指标 | 定义 |
|---|---|
| 轮次 | 主会话 `model_usage.turn_id` 去重数 |
| 步数 | `logical_request_id` 去重数(重试不计步;失败/取消计步) |
| 模型用时 | Σ `duration_ms` |
| 工具用时 | Σ `tool_usage.duration_ms` |
| TTFT 均值 | AVG(`time_to_first_token_ms`) |
| TPS | Σ output ÷ Σ(completed_at − first_token_at);「实时」= 最近一次完成请求 |
| token | Σ input + Σ output(实测 `input_tokens` 已含缓存读) |
| 缓存命中率 | cache_read ÷ input |

数据库 schema 属宿主实现细节(非稳定 API),usage 行保留 30 天;查询对缺列做了
优雅降级。

## 副作用说明

- 只读读取本地会话数据库。
- 启动仅绑定回环地址的本地服务(`127.0.0.1:31710`),向 ZCode 渲染层提供统计
JSON 与面板动态脚本;只响应无跨域 `Origin` 头的请求(即应用自身 `file://` 页面),
其余 Origin 一律 403。
- SessionStart hook 在插件数据目录写一个小文件(`current_session`)。
- 无网络访问、无凭证、无遥测。

## 安装

ZCode Desktop → **插件市场** → 添加收录本插件的市场(审核通过后为官方
`zcode-plugins` 市场,审核前可用本地目录)→ **安装 session-stats** → 重启或新开会话
→ 输入 `/stats` 或打开「会话统计」侧栏面板。

## 许可

MIT
26 changes: 26 additions & 0 deletions external_plugins/session-stats/commands/stats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
description: 显示会话统计(轮次/步数/耗时/TTFT/TPS/token/缓存命中)
---

请调用 session-stats MCP 服务器的 get_stats 工具查询会话统计。scope 取值:当参数含「今日」「today」时用 `today`,否则用 `current`。

拿到 structuredContent 后,**严格按数据输出**下面的中文统计卡片(不要编造/估算任何数据里没有的数值,缺失显示「—」):

```
⏱ {turns} 轮 {steps} 步 · {tps} tok/s
Σ {tokens.total} tok · 缓存命中 {cache_hit_pct}%
────────────────────────
模型用时 {model_ms 格式化为 时/分/秒}
工具调用用时 {tool_ms}
首 token 平均 {ttft_ms_avg}
输出速度(TPS) {tps} tok/s
输入(含缓存) {tokens.input}(缓存读 {tokens.cache_read} / 缓存写 {tokens.cache_write})
输出 {tokens.output}(含推理 {tokens.reasoning})
总计 {tokens.total}
```

- `today` 口径:开头注明「今日 {sessions} 个会话汇总」。
- `current` 口径:结尾注明「会话 {session_id} · 含子代理 {subagent_count} 个」。
- 若返回 isError 或 error 字段,如实转述错误并提示检查插件 server 日志。

参数:$ARGUMENTS
18 changes: 18 additions & 0 deletions external_plugins/session-stats/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear|compact",
"hooks": [
{
"type": "process",
"command": "python",
"args": ["${ZCODE_PLUGIN_ROOT}/hooks/write_session.py"],
"timeoutMs": 3000,
"statusMessage": "session-stats: 记录当前会话 id"
}
]
}
]
}
}
32 changes: 32 additions & 0 deletions external_plugins/session-stats/hooks/write_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""SessionStart hook:把当前 session_id 写进插件数据目录,供 MCP 服务器解析「当前会话」。

输入:stdin 一行 JSON(含 session_id);输出:无(保持 stdout 干净,退出码恒 0,
绝不阻塞会话)。
"""

import json
import os
import sys
from pathlib import Path


def main() -> int:
try:
req = json.loads(sys.stdin.read() or "{}")
except Exception: # noqa: BLE001
req = {}
sid = req.get("session_id") or os.environ.get("ZCODE_SESSION_ID") or ""
data_dir = os.environ.get("ZCODE_PLUGIN_DATA", "")
if not sid or not data_dir:
return 0 # 静默跳过,由 MCP 服务器走 env/db 兜底
try:
p = Path(data_dir)
p.mkdir(parents=True, exist_ok=True)
(p / "current_session").write_text(sid, encoding="utf-8")
except OSError:
pass
return 0


if __name__ == "__main__":
sys.exit(main())
Loading