bowenliang123/dsh-context
bowenliang123/dsh-contextDSH 上下文洞察面板:Context 仪表盘 + /context命令 + Context 浏览器,查看 Context的分类组成、内容详情、演进趋势、压缩/注入事件、统计等一站式 Context 全生命周期管理。
统计所有已存 DSH 会话的 token 用量:Host 端把持久化会话日志折叠进检查点,并经一条只读 HTTP 路由提供;「用量」设置页先立即绘出已保存的聚合结果,再在后台更新。当日视图按小时分桶(带日期选择器),旁边是近 7/30 天窗口,含分模型明细、请求数与联网搜索次数;不含费用/定价模型。
English | 中文
Use this package to report how many tokens this device has spent, aggregated by day across every session rather than only the one on screen. It folds the durable session logs into per-day totals of input, cache-read, and output tokens plus web-search counts, and serves them to clients through one read-only HTTP route. Because the log is the source of truth, totals survive restarts and backfill history; a storage checkpoint makes a warm start fold only new events. Deleted sessions keep their contribution, and unreadable logs are skipped rather than failing the query.
Install into a profile; the plugin belongs to the profile rather than to a dsh checkout.
dsh plugin --profile web add https://github.com/moonlight2893267956/dsh-usage-stats/releases/download/v0.1.1/moonlight2893267956-dsh-usage-stats-0.1.1.tgz
dsh plugin --profile web add github:moonlight2893267956/dsh-usage-stats#path:packages/usage-stats
dsh plugin --profile web add /absolute/path/to/dsh-usage-stats/packages/usage-stats
The release tarball is prebuilt, so it installs in one step. The git spec builds the package through its prepare script, which pnpm blocks until you allow it: run that command once, copy the exact key it prints into allowBuilds in the profile's pnpm-workspace.yaml, then run it again; its trailing #path: selects the package inside this repository, because the repository root is a workspace rather than the plugin. Restart dsh afterwards; the Usage page appears under Settings.
Mount this plugin beside the session persistence it reads; the read route it registers then serves the Web GUI's Usage page.
- id: usage-stats
name: '@deepseek-ai/dsh-usage-stats'
A deployment that wants the SQLite medium for the checkpoint adds both rows below; without them the usage_stats domain stays on the composition's default backend, which is the shared json one.
- id: storage-sqlite
name: '@deepseek-ai/dsh-storage-sqlite'
config:
path: .../storages/usage-stats.db
- id: storage-domain
config:
backend: json
routes:
usage_stats: sqlite
refreshPollIntervalMs controls browser polling during reconciliation (default 1000 ms, integer range 250–10000 ms). It injects sessionPersistence and storageDomain: without sessionPersistence the fiber stays pending, and without storageDomain activation fails loud. The shipped Web composition leaves usage_stats on the json default, because storage-sqlite opens its database on activation and that composition also packs the browser-only Worker deployment, whose node:sqlite stub refuses the constructor; routing the domain to SQLite is a later patch layer's choice.
storage-sqlite is not part of every runtime: the bundled desktop application ships dsh-storage, dsh-storage-json, and dsh-storage-domain, but not this backend, so a profile there installs the package itself and pins it to the runtime's version, because the backend peers on @deepseek-ai/dsh-storage at exactly that version. A missing or mismatched install fails at activation: the backend does not import, and every plugin that injects storageDomain — this one included — stays pending.
| Field | Meaning |
|---|---|
input | Full prompt input: uncached input plus both cache compartments (cache read and cache write), so it already contains cacheRead |
cacheRead | Prompt-cache hits — the reused prefix |
output | Output (completion) tokens |
requests | Assistant messages carrying usage, one per model completion request; the card's subline reports the search count |
searches | web_search tool calls made that day |
The four token counts are disjoint, so input is the complete prompt side of a request and input + output is its whole billed total.
A single-day request also carries 24 hourly buckets; a trailing window carries per-day buckets only. The window length is clamped to [1, 370], and an unusable value defaults to 30.
The fold reads the durable log, not the model-visible surface, so tokens a later compaction hid from the model still count because they were consumed. The first query in a process reconciles the durable session catalog; later queries inspect only sessions that crossed a durability checkpoint, so reopening Usage does not enumerate every stored session. A per-lifecycle file revision still skips logs whose bytes did not advance.
Each column is one bucket — an hour for a single day, a date for a window. A column stacks input misses, cache hits, and output; a bucket with no usage keeps a mark on the baseline rail, so an untouched hour reads as a measured zero instead of missing. Hovering a column, or moving keyboard focus to it, shows that bucket's exact figures. The entrance cascade and hover motion are dropped under prefers-reduced-motion.
The Usage page reads GET /dsh-usage-stats/snapshot with the same days, date, and models query fields as the strict GET /dsh-usage-stats/stats route. A snapshot returns value, freshness, revision, error, and refreshPollIntervalMs. It displays the last complete saved aggregate immediately while freshness: pending labels it as updating. No checkpoint means value: null until the initial backfill completes; an unreconciled zero is not an empty-history result. While a first response is outstanding the four metric cards show a dash rather than 0, so a cold backfill never reads as a measured empty window; a window switch drops the previous window's buckets for the same reason. Polling stops after reconciliation or a failure and when the page closes. retry=1 explicitly retries a failed background scan.
Reconciliation shares one task and publishes the full totals and cursors only after their atomic save succeeds. A failed scan or save retains the previous committed view and its progress; the page keeps that chart visible with Retry. The checkpoint stays at schema version 1. Cached display removes reconciliation from first paint, not from the total background workload; old-generation revisions may still require expensive migration validation.
The settings shell draws each nav row's icon from its own list of section ids and falls back to the generic settings gear for every other one, and a settings.section registration has no icon field to pass — so the plugin claims its own row once the dialog is mounted and swaps the gear for a bar-chart mark matching the page's chart. Only the row whose visible text equals this plugin's current section label is marked, so no other section loses its icon; a locale switch re-claims it. Uninstalling the plugin or unloading its fiber removes both the mark and the stylesheet it injects.
icon.png is the mark the plugin manager shows in its list. It is a raster image rather than an SVG on purpose: the manager renders a manifest icon through an <img>, and an isolated image document gives currentColor no cascade to resolve against, so an SVG drawn that way renders black in both themes. The PNG carries its own colours, a transparent background, and sits well under the manifest's 256 KiB limit — about 41 KiB at 256px.
A query never fails because one session log is corrupt: the fold warns, skips that session, and records its revision in memory only, so a fresh process retries it. A lost or stale checkpoint costs a longer tail replay on the next cold read and never loses data, because the logs remain the authority. A session deleted from the device keeps its contribution.
UsageStatsService keeps one in-memory aggregate: per-day buckets keyed by local calendar day, each carrying 24 hourly buckets and a per-model map, plus a fold cursor and last-seen revision per session lifecycle. Its first fold walks sessionPersistence.list(); later folds inspect only sessions invalidated by session/flush. It skips unchanged revisions, opens each remaining log for reading, skips the fork-inherited prefix (inheritedEventCount) so a forked child never double-counts its parent, and folds every event after the cursor. foldEvent adds assistant/message usage to the day, hour, and model accumulators, and counts tool/call events named web_search. Concurrent queries share one lifecycle-owned task; strict callers also reconcile flushes that arrive while they wait. Cached readers see only the last committed state. After a mutated fold the service writes the whole checkpoint back through the usage_stats storage domain as one global value — accumulated totals plus per-session progress, with unreadable sessions excluded so a restart re-attempts them.
dsh-session-persistence — the durable log store whose listings, revisions, and reads the fold consumes.dsh-storage-domain — the checkpoint domain facility and its backend routes.dsh-client-ui-usage — the Usage settings page that renders these figures.Those three live in the DeepSeek Harness monorepo, not in this repository, so they are named rather than linked.
None, as the plugin only computes a client-facing read model of already-logged session events and touches no prompt, message, schema, stream, or tool result.
None; the plugin never assembles or sends provider requests.
These limits define what the figures cover and what the checkpoint can recover. They are current package constraints.
version, and both backends then reject the stale unit with version-mismatch at open — json included, which keeps the file rather than backing it up — leaving every plugin that injects storageDomain pending until the unit is cleared. The totals are fully re-derivable from the logs, so recovery is clearing the unit rather than migrating it.[1, 370] days does not delete anything. Days older than the clamp are unreachable through a trailing window but reachable through the date picker, whose prevMonth has no lower bound, so pruning would turn a real past day into a reading of zero. At the measured 2.4 KiB per day the checkpoint reaches roughly 900 KiB at the ceiling.assistant/message record does not carry the request's purpose; splitting further requires logging that field first.None.
Runtime invariant: No companion is published. The package only aggregates assistant/message usage records and tool/call names whose shapes and append-only ordering are owned and runtime-checked by dsh-session and dsh-agent-loop, and reads them through dsh-session-persistence, whose contiguity and durability are checked there; it owns no event relation of its own to assert.