dsh-all-usage 1.0.6 → 1.0.8

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/CHANGELOG.md ADDED
@@ -0,0 +1,69 @@
1
+ # Changelog
2
+
3
+ All notable changes to `dsh-all-usage` are documented here.
4
+
5
+ ## [1.0.8] - 2026-08-23
6
+
7
+ ### Added
8
+
9
+ - Added a restart read-avoidance cursor: on startup the plugin uses the persisted log revision (via `sessionPersistence.listSnapshots()`, a header-line + stat read) as a per-session change signal. Sessions whose log is unchanged since the last ledger write are applied straight from the durable ledger without reading their full event log; only changed or new sessions are read (incrementally).
10
+ - Stored the log revision in each durable ledger record (`lastRevision`); existing rows without it are re-read once and backfilled, so behavior is backward compatible.
11
+
12
+ ### Fixed
13
+
14
+ - Eliminated the per-session full-event read at restart for unchanged persisted sessions, so DSH restarts no longer re-read every session's history from scratch.
15
+
16
+ ## [1.0.7] - 2026-08-23
17
+
18
+ ### Added
19
+
20
+ - Added an incremental ledger cursor: baseline rescans reuse the durable ledger as a per-session cursor, so unchanged sessions seed from the ledger instead of re-folding every historical event, and changed sessions fold only the new tail.
21
+ - Added an all-zero usage guard: zero-token usage replays no longer overwrite already-recorded turn/step usage, while pure cache-read requests (billable cache hits with no input/output) are still counted.
22
+ - Added a structured, machine-readable token semantics declaration (`tokenSemantics.semantics`) to the `/api/all-usage` snapshot: input is fresh (excludes cache), cache and reasoning are separate buckets.
23
+
24
+ ### Fixed
25
+
26
+ - Reduced startup scan cost on long usage history: sessions whose events are unchanged since the last ledger write are no longer re-folded or rewritten.
27
+ - Prevented a live retry of an already-recorded step from double-counting after a fresh-instance seed.
28
+
29
+ ## [1.0.6] - 2026-08-23
30
+
31
+ ### Added
32
+
33
+ - Added a durable per-session usage ledger written at the awaited `session/flush` boundary.
34
+ - Preserved successfully flushed turns and token usage after conversation logs are deleted.
35
+ - Added automatic rebuilds from ledger records after session disposal or reconciliation detects missing logs.
36
+
37
+ ### Fixed
38
+
39
+ - Prevented stale baseline snapshots from overwriting newer flushed ledger records.
40
+ - Normalized duplicate ledger keys during hydration and guarded ledger writes during plugin disposal.
41
+ - Prevented inactive Cordis timer contexts from escalating into fatal DSH load failures.
42
+ - Replaced invalid hyphenated storage unit names with valid underscore names.
43
+
44
+ ## [1.0.5] - 2026-08-20
45
+
46
+ ### Added
47
+
48
+ - Added custom start/end date ranges across all available historical daily data.
49
+ - Kept the heatmap as a fixed latest-53-week view while allowing older range aggregation and CSV export.
50
+ - Added distinct session counts for bounded ranges.
51
+
52
+ ### Fixed
53
+
54
+ - Added baseline retry with backoff after transient session registry failures.
55
+ - Stopped in-flight scans and stale polling responses after plugin disposal or refresh races.
56
+
57
+ ## [1.0.4] - 2026-08-19
58
+
59
+ ### Fixed
60
+
61
+ - Hardened loopback, same-origin, method, and process-token checks for dashboard API routes.
62
+ - Allowed same-origin browser balance GET requests that omit `Origin` while retaining token protection.
63
+ - Standardized English date buckets, range filters, streaks, heatmap dates, and export timestamps on UTC.
64
+
65
+ [1.0.8]: https://github.com/ParticleLight/dsh-all-usage/releases/tag/v1.0.8
66
+ [1.0.7]: https://github.com/ParticleLight/dsh-all-usage/releases/tag/v1.0.7
67
+ [1.0.6]: https://github.com/ParticleLight/dsh-all-usage/releases/tag/v1.0.6
68
+ [1.0.5]: https://github.com/ParticleLight/dsh-all-usage/releases/tag/v1.0.5
69
+ [1.0.4]: https://github.com/ParticleLight/dsh-all-usage/releases/tag/v1.0.4
package/README.md CHANGED
@@ -17,6 +17,18 @@ DeepSeek Harness 全量用量看板:按模型、供应商、工作区和时间
17
17
  - **时间范围**:今日、近 30 天、近 90 天、全部,或在全部可扫描历史日数据中自定义起止日期;热力图始终展示最近 53 周
18
18
  - **工作区别名**:在侧栏入口打开看板后管理,持久化保存到 $DSH_HOME/storages 的 KV 单元 `all_usage_aliases`
19
19
  - **界面语言**:在看板顶部切换中文与 English;选择会保存到浏览器本地
20
+ - **完整历史与增量重建**:基线扫描全部可读历史会话;独立用量账本同时作为每会话游标——未变化的会话直接复用账本,新增事件只增量回填,长历史重启不再全量重建
21
+ - **重启免读**:用持久化日志的 revision 作为每会话的变更信号(只读头部行 + stat,不读全量)——日志未变的会话重启时连事件都不读,直接从账本复用;仅日志变化(新增/修改)的会话才做增量读取
22
+ - **Token 口径**:输入按「未含缓存命中」计,缓存命中 / 写入与推理独立成桶;全 0 用量的重放事件不会覆盖已记录的真实用量,仅缓存命中的请求也会计入
23
+
24
+ ### 最近更新
25
+
26
+ **v1.0.8**
27
+
28
+ - 重启免读:用持久化日志的 revision 做每会话变更信号,未变化会话重启时完全不读事件,直接复用账本;仅在日志变化时增量读取
29
+ - 账本记录持久化每会话的日志 revision(旧记录首次重读后自动回填)
30
+
31
+ 完整版本记录见 [CHANGELOG.md](CHANGELOG.md)。
20
32
 
21
33
  ### 截图 / Screenshots
22
34
 
@@ -26,7 +38,7 @@ DeepSeek Harness 全量用量看板:按模型、供应商、工作区和时间
26
38
 
27
39
  ### 安装
28
40
 
29
- 本插件是标准的 DSH 社区插件包(声明 `dsh.bundle` manifest 与 Web Client),数据全部来自持久化会话日志,安装后自动回填历史。
41
+ 本插件是标准的 DSH 社区插件包(声明 `dsh.bundle` manifest 与 Web Client),数据来自持久化会话日志和独立用量账本,安装后自动回填历史。
30
42
 
31
43
  #### 官方插件命令(推荐)
32
44
 
@@ -68,6 +80,7 @@ dsh plugin --profile web add github:ParticleLight/dsh-all-usage
68
80
  - 按日范围统计会保留全部可读取历史会话的有使用记录日期;热力图仅作为最近 53 周的固定视图窗口
69
81
  - 会话删除后,已成功 flush 的用量仍从独立账本恢复;会话销毁提示和周期对账只负责触发重建,不会删除账本记录
70
82
  - 同一会话的同一 `turn / step` 只保留一份最终 usage;重试或替换消息会替换旧贡献,不重复累计
83
+ - 输入 Token 按「未含缓存命中」计(缓存命中 / 写入独立成桶);全 0 用量的重放事件不会覆盖已记录的真实用量,纯缓存命中的请求仍会计入
71
84
  - 看板中的总处理量 = 输入 + 输出 + 缓存读写 + 推理;缓存命中表示复用的上下文 Token,不等于新生成 Token 或实际费用
72
85
  - 余额查询走 DeepSeek 官方 `/user/balance` 接口;未配置 API Key 时卡片显示引导文案
73
86
  - 仅统计能归属到已注册工作区(按会话 cwd 匹配)的会话
@@ -90,10 +103,22 @@ A full usage dashboard for DeepSeek Harness. Analyze tokens, cache behavior, acc
90
103
  - **Time ranges**: today, last 30 days, last 90 days, all time, or a custom start/end date across all available historical daily data; the heatmap always shows the latest 53 weeks
91
104
  - **Workspace aliases**: manage aliases from the sidebar dashboard; values persist in the $DSH_HOME/storages KV cell `all_usage_aliases`
92
105
  - **Interface language**: switch between Chinese and English from the dashboard header; your choice persists locally in the browser
106
+ - **Full history & incremental rebuild**: the baseline scans every readable historical session; the durable usage ledger doubles as a per-session cursor, so unchanged sessions are reused straight from the ledger and only newly appended events are folded — long histories restart without a full rebuild
107
+ - **Restart with no re-read**: the persisted log revision (a header-line + stat via `sessionPersistence.listSnapshots()`) acts as a per-session change signal — sessions whose log is unchanged are applied from the ledger on restart without reading their events at all; only changed/new sessions are read incrementally
108
+ - **Token accounting semantics**: input tokens are fresh (exclude cache hits/writes, which sit in separate buckets along with reasoning); all-zero usage replays never overwrite recorded usage, while cache-only requests still count
109
+
110
+ ### Latest Update
111
+
112
+ **v1.0.8**
113
+
114
+ - Restart with no re-read: the persisted log revision acts as a per-session change signal, so unchanged sessions are applied from the ledger on restart without reading any events; only changed logs are read incrementally
115
+ - The ledger now stores each session's log revision; existing rows are re-read once and backfilled automatically
116
+
117
+ See [CHANGELOG.md](CHANGELOG.md) for the complete version history.
93
118
 
94
119
  ### Installation
95
120
 
96
- This is a standard DSH community bundle. It declares a `dsh.bundle` manifest and a web client, and backfills its data from persisted session logs after installation.
121
+ This is a standard DSH community bundle. It declares a `dsh.bundle` manifest and a web client, and backfills its data from persisted session logs and the durable usage ledger after installation.
97
122
 
98
123
  #### Official plugin command (recommended)
99
124
 
@@ -135,6 +160,7 @@ The profile patch layer hot-reloads; save the file and refresh the page.
135
160
  - Day-level range data retains every readable historical session date with tracked usage; the heatmap is only a fixed latest-53-week view
136
161
  - After a session is deleted, successfully flushed usage is restored from the separate ledger; disposal hints and periodic reconciliation trigger rebuilds without deleting ledger rows
137
162
  - For each session and logical `turn / step`, only the final usage contribution is kept; retries or replaced messages do not double-count
163
+ - Input tokens are fresh (exclude cache hits/writes, which sit in their own buckets); all-zero usage replays do not overwrite recorded usage and pure cache-read requests still count
138
164
  - Processed tokens = input + output + cache read/write + reasoning; a cache hit means reused context, not newly generated tokens or actual cost
139
165
  - Balance data comes from DeepSeek’s official `/user/balance` endpoint; the card shows guidance when no API key is configured
140
166
  - English mode uses UTC for date buckets, range filters, streaks, heatmap dates, and export timestamps; Chinese mode uses local time
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { randomBytes, timingSafeEqual } from 'node:crypto'
4
4
 
5
5
  const name = 'dsh-all-usage'
6
- const inject = ['sessionQuery', 'workspaceRegistry', 'timer', 'storage']
6
+ const inject = ['sessionQuery', 'workspaceRegistry', 'timer', 'sessionPersistence', 'storage']
7
7
 
8
8
  // webServer route handlers do not inherit the connection API fence; keep this plugin
9
9
  // local and require a browser-originated capability for state-changing reads/writes.
@@ -75,6 +75,10 @@ function apply(ctx) {
75
75
  const settings = ctx.get('settings')
76
76
  const storage = ctx.get('storage')
77
77
  const webServer = ctx.get('webServer')
78
+ // v1.0.8: optional sessionPersistence exposes listSnapshots() — a cheap per-session
79
+ // revision (header line + stat, no full-log read) that lets the baseline skip
80
+ // re-reading unchanged sessions after a DSH restart.
81
+ const sessionPersistence = ctx.get('sessionPersistence')
78
82
 
79
83
  // ---------- owned aggregation state ----------
80
84
  const wsMeta = new Map()
@@ -259,6 +263,10 @@ function apply(ctx) {
259
263
  const values = usageValues(usage)
260
264
  const modelId = typeof model === 'string' && model !== '' ? model : '未知模型(历史记录缺少路由)'
261
265
  const eventSeq = typeof seq === 'number' ? seq : -1
266
+ // v1.0.7: a usage event carrying no billable token in any bucket must not add a
267
+ // meaningless row nor wipe previously recorded real usage (cc-switch
268
+ // has_billable_tokens parity). Pure cache-read requests are billable and pass.
269
+ if (noValues(values)) return
262
270
  const key = usageStepKey(sid, data, seq)
263
271
  const previous = usageByStep.get(key)
264
272
  // A late replay of an older raw event cannot replace the canonical later step.
@@ -297,7 +305,7 @@ function apply(ctx) {
297
305
  function ledgerEventKey(event, index) {
298
306
  return typeof event.seq === 'number' ? String(event.seq) : 'event:' + index
299
307
  }
300
- function buildLedgerRecord(session, workspaceId, source = 'scan') {
308
+ function buildLedgerRecord(session, workspaceId, source = 'scan', revision) {
301
309
  const sid = session && typeof session.id === 'string' ? session.id : ''
302
310
  const events = session && Array.isArray(session.events) ? session.events : []
303
311
  if (sid === '' || workspaceId === undefined) return null
@@ -320,6 +328,9 @@ function apply(ctx) {
320
328
  }
321
329
  if (event.type !== 'assistant/message' || data === null || typeof data !== 'object' || data.usage === undefined) continue
322
330
  const values = usageValues(data.usage)
331
+ // v1.0.7: all-zero usage rows carry no billable tokens and stay out of the
332
+ // durable ledger (cc-switch has_billable_tokens parity).
333
+ if (noValues(values)) continue
323
334
  const modelId = typeof modelFromMessage(data, currentModel) === 'string' && modelFromMessage(data, currentModel) !== ''
324
335
  ? modelFromMessage(data, currentModel) : '未知模型(历史记录缺少路由)'
325
336
  const eventSeq = typeof event.seq === 'number' ? event.seq : -1
@@ -328,7 +339,7 @@ function apply(ctx) {
328
339
  if (previous !== undefined && eventSeq >= 0 && previous.seq > eventSeq) continue
329
340
  usage.set(key, { key, seq: eventSeq, time: event.time, workspaceId, modelId, values })
330
341
  }
331
- return { version: LEDGER_VERSION, sessionId: sid, workspaceId, lastSeq: lastSeqOf(events), source, updatedAt: nextLedgerRevision(), turns: Array.from(turns.values()), usage: Array.from(usage.values()) }
342
+ return { version: LEDGER_VERSION, sessionId: sid, workspaceId, lastSeq: lastSeqOf(events), source, updatedAt: nextLedgerRevision(), lastRevision: typeof revision === 'string' ? revision : undefined, turns: Array.from(turns.values()), usage: Array.from(usage.values()) }
332
343
  }
333
344
  function normalizeLedgerRecord(raw, key) {
334
345
  if (raw === null || typeof raw !== 'object' || raw.version !== LEDGER_VERSION || typeof raw.sessionId !== 'string' || raw.sessionId !== key) return null
@@ -346,7 +357,7 @@ function apply(ctx) {
346
357
  }
347
358
  const updatedAt = typeof raw.updatedAt === 'number' ? raw.updatedAt : 0
348
359
  ledgerRevision = Math.max(ledgerRevision, updatedAt)
349
- return { version: LEDGER_VERSION, sessionId: raw.sessionId, workspaceId: raw.workspaceId, lastSeq: typeof raw.lastSeq === 'number' ? raw.lastSeq : -1, source: raw.source === 'flush' ? 'flush' : 'scan', updatedAt, turns: Array.from(turnMap.values()), usage: Array.from(usageMap.values()) }
360
+ return { version: LEDGER_VERSION, sessionId: raw.sessionId, workspaceId: raw.workspaceId, lastSeq: typeof raw.lastSeq === 'number' ? raw.lastSeq : -1, source: raw.source === 'flush' ? 'flush' : 'scan', updatedAt, lastRevision: typeof raw.lastRevision === 'string' ? raw.lastRevision : undefined, turns: Array.from(turnMap.values()), usage: Array.from(usageMap.values()) }
350
361
  }
351
362
  function applyLedgerRecord(record) {
352
363
  if (record === null || record === undefined) return
@@ -523,6 +534,22 @@ function apply(ctx) {
523
534
  } catch (err) {
524
535
  console.error('[all-usage] session list failed:', err)
525
536
  }
537
+ // v1.0.8: cheap per-session change signal (header line + stat, no full-log read)
538
+ let snapshots = null
539
+ if (sessionPersistence !== undefined && typeof sessionPersistence.listSnapshots === 'function') {
540
+ try {
541
+ const rows = await sessionPersistence.listSnapshots()
542
+ if (Array.isArray(rows)) {
543
+ snapshots = new Map()
544
+ for (const row of rows) {
545
+ const rid = row && row.header && typeof row.header.id === 'string' ? row.header.id : undefined
546
+ if (rid !== undefined && row && typeof row.revision === 'string') snapshots.set(rid, row.revision)
547
+ }
548
+ }
549
+ } catch (err) {
550
+ console.error('[all-usage] session persistence snapshots unavailable:', err)
551
+ }
552
+ }
526
553
  if (disposed || generation !== aggregationGeneration) return
527
554
  if (setupFailed || !Array.isArray(records)) {
528
555
  // A transient registry failure must not be reported as a completed empty scan.
@@ -560,18 +587,60 @@ function apply(ctx) {
560
587
  if (disposed || generation !== aggregationGeneration) return
561
588
  try {
562
589
  if (sessionSeq.has(sid)) return
590
+ // v1.0.8: when the persisted log revision is unchanged since the last ledger
591
+ // write, the whole readSession (full event transfer) is skipped — the ledger
592
+ // record is applied directly and the live feed keeps catching new events.
593
+ const previousRecord = ledgerRecords.get(sid)
594
+ const revision = snapshots === null ? undefined : snapshots.get(sid)
595
+ if (previousRecord !== undefined && typeof previousRecord.lastRevision === 'string' && typeof revision === 'string' && revision === previousRecord.lastRevision) {
596
+ applyLedgerRecord(previousRecord)
597
+ for (const item of previousRecord.usage) {
598
+ usageByStep.set(item.key, { seq: item.seq, wsId: item.workspaceId, time: item.time, values: item.values, modelId: item.modelId, sid })
599
+ }
600
+ sessionSeq.set(sid, previousRecord.lastSeq)
601
+ sessionCount.add(sid)
602
+ return
603
+ }
563
604
  const snap = await ctx.sessionQuery.readSession(sid)
564
605
  if (disposed || generation !== aggregationGeneration) return
565
606
  if (snap && Array.isArray(snap.events)) {
566
- const ledger = buildLedgerRecord({ id: sid, header: record.header, events: snap.events }, wsId)
607
+ // v1.0.7: incremental seed the durable ledger doubles as a per-session
608
+ // cursor (cc-switch session_log_sync mtime+offset parity). An unchanged
609
+ // session applies its canonical record directly and never re-folds;
610
+ // a changed session seeds the previous record once, then folds only the
611
+ // new tail (previously every listed session was re-read and fully rebuilt).
612
+ const currentLastSeq = lastSeqOf(snap.events)
613
+ const previous = ledgerRecords.get(sid)
614
+ if (previous !== undefined) {
615
+ applyLedgerRecord(previous)
616
+ // Seed usageByStep so a later live retry of an already-recorded step
617
+ // reverses the ledger contribution instead of double-counting it.
618
+ for (const item of previous.usage) {
619
+ usageByStep.set(item.key, { seq: item.seq, wsId: item.workspaceId, time: item.time, values: item.values, modelId: item.modelId, sid })
620
+ }
621
+ if (previous.lastSeq >= currentLastSeq) {
622
+ // Content unchanged despite a revision change (rare: ctime-only churn):
623
+ // refresh the stored revision so future restarts can skip the read.
624
+ if (revision !== undefined && previous.lastRevision !== revision) {
625
+ previous.lastRevision = revision
626
+ void persistLedgerRecord(previous)
627
+ }
628
+ sessionSeq.set(sid, previous.lastSeq)
629
+ sessionCount.add(sid)
630
+ return
631
+ }
632
+ foldEvents(wsId, snap.events, previous.lastSeq, sid)
633
+ } else {
634
+ foldEvents(wsId, snap.events, undefined, sid)
635
+ }
636
+ const ledger = buildLedgerRecord({ id: sid, header: record.header, events: snap.events }, wsId, 'scan', revision)
567
637
  const canonical = ledger === null ? ledgerRecords.get(sid) : storeLedgerRecord(ledger)
568
638
  if (canonical === ledger) {
569
639
  void persistLedgerRecord(ledger)
570
- foldEvents(wsId, snap.events, undefined, sid)
571
640
  } else if (canonical !== undefined) {
572
641
  applyLedgerRecord(canonical)
573
642
  }
574
- sessionSeq.set(sid, lastSeqOf(snap.events))
643
+ sessionSeq.set(sid, currentLastSeq)
575
644
  sessionCount.add(sid)
576
645
  }
577
646
  } catch (err) {
@@ -786,6 +855,18 @@ function apply(ctx) {
786
855
  processedTotal: 'input + output + cacheRead + cacheWrite + reasoning',
787
856
  cacheRead: 'reused context tokens; not newly generated output',
788
857
  cacheWrite: 'tokens written into a provider cache',
858
+ // v1.0.7: structured, machine-readable accounting semantics (cc-switch
859
+ // input_token_semantics parity). DSH reports input as fresh (cache read /
860
+ // cache write sit in their own buckets) — verified against real ledger data;
861
+ // reasoning is bucketed separately from output and assumed non-overlapping.
862
+ semantics: {
863
+ input: 'fresh (excludes cache-read and cache-write tokens, bucketed separately)',
864
+ buckets: ['input', 'output', 'cacheRead', 'cacheWrite', 'reasoning'],
865
+ inputIncludesCache: false,
866
+ cacheBucketed: true,
867
+ reasoningSeparate: true,
868
+ gate: 'all-zero usage rows are ignored; pure cache-read requests still count',
869
+ },
789
870
  },
790
871
  totals: { turns: totals.turns, sessions: sessionCount.size, input: totals.input, output: totals.output, cacheRead: totals.cacheRead, cacheWrite: totals.cacheWrite, reasoning: totals.reasoning },
791
872
  perWorkspace: Array.from(perWorkspace, (p) => ({ workspaceId: p[0], turns: p[1].turns, input: p[1].input, output: p[1].output, cacheRead: p[1].cacheRead, cacheWrite: p[1].cacheWrite, reasoning: p[1].reasoning })),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-all-usage",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "DeepSeek Harness usage dashboard with model, provider, workspace, cache, balance, and CSV insights",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,6 +37,7 @@
37
37
  "files": [
38
38
  "lib/index.js",
39
39
  "lib/client.js",
40
+ "CHANGELOG.md",
40
41
  "cordis.patch.yml",
41
42
  "assets"
42
43
  ],