dsh-all-usage 1.0.7 → 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 CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to `dsh-all-usage` are documented here.
4
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
+
5
16
  ## [1.0.7] - 2026-08-23
6
17
 
7
18
  ### Added
@@ -51,6 +62,7 @@ All notable changes to `dsh-all-usage` are documented here.
51
62
  - Allowed same-origin browser balance GET requests that omit `Origin` while retaining token protection.
52
63
  - Standardized English date buckets, range filters, streaks, heatmap dates, and export timestamps on UTC.
53
64
 
65
+ [1.0.8]: https://github.com/ParticleLight/dsh-all-usage/releases/tag/v1.0.8
54
66
  [1.0.7]: https://github.com/ParticleLight/dsh-all-usage/releases/tag/v1.0.7
55
67
  [1.0.6]: https://github.com/ParticleLight/dsh-all-usage/releases/tag/v1.0.6
56
68
  [1.0.5]: https://github.com/ParticleLight/dsh-all-usage/releases/tag/v1.0.5
package/README.md CHANGED
@@ -18,15 +18,15 @@ DeepSeek Harness 全量用量看板:按模型、供应商、工作区和时间
18
18
  - **工作区别名**:在侧栏入口打开看板后管理,持久化保存到 $DSH_HOME/storages 的 KV 单元 `all_usage_aliases`
19
19
  - **界面语言**:在看板顶部切换中文与 English;选择会保存到浏览器本地
20
20
  - **完整历史与增量重建**:基线扫描全部可读历史会话;独立用量账本同时作为每会话游标——未变化的会话直接复用账本,新增事件只增量回填,长历史重启不再全量重建
21
+ - **重启免读**:用持久化日志的 revision 作为每会话的变更信号(只读头部行 + stat,不读全量)——日志未变的会话重启时连事件都不读,直接从账本复用;仅日志变化(新增/修改)的会话才做增量读取
21
22
  - **Token 口径**:输入按「未含缓存命中」计,缓存命中 / 写入与推理独立成桶;全 0 用量的重放事件不会覆盖已记录的真实用量,仅缓存命中的请求也会计入
22
23
 
23
24
  ### 最近更新
24
25
 
25
- **v1.0.7**
26
+ **v1.0.8**
26
27
 
27
- - 增量游标:启动重扫只增量回填新增事件,未变化的会话直接从独立账本复用,不再全量重建
28
- - 0 用量防护:全 0 用量的重放事件不会覆盖已记录的真实用量;纯缓存命中的请求仍会计入
29
- - 结构化语义声明:API 返回机读的 token 口径(输入不含缓存命中,缓存与推理独立成桶)
28
+ - 重启免读:用持久化日志的 revision 做每会话变更信号,未变化会话重启时完全不读事件,直接复用账本;仅在日志变化时增量读取
29
+ - 账本记录持久化每会话的日志 revision(旧记录首次重读后自动回填)
30
30
 
31
31
  完整版本记录见 [CHANGELOG.md](CHANGELOG.md)。
32
32
 
@@ -104,15 +104,15 @@ A full usage dashboard for DeepSeek Harness. Analyze tokens, cache behavior, acc
104
104
  - **Workspace aliases**: manage aliases from the sidebar dashboard; values persist in the $DSH_HOME/storages KV cell `all_usage_aliases`
105
105
  - **Interface language**: switch between Chinese and English from the dashboard header; your choice persists locally in the browser
106
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
107
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
108
109
 
109
110
  ### Latest Update
110
111
 
111
- **v1.0.7**
112
+ **v1.0.8**
112
113
 
113
- - Incremental scan cursor: restarts re-fold only newly appended events; unchanged sessions are seeded straight from the durable ledger instead of a full rebuild
114
- - All-zero usage guard: zero-usage replays no longer overwrite recorded usage, while pure cache-read requests still count
115
- - Structured token semantics: the API now declares a machine-readable accounting contract (input excludes cache; cache and reasoning are separate buckets)
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
116
 
117
117
  See [CHANGELOG.md](CHANGELOG.md) for the complete version history.
118
118
 
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()
@@ -301,7 +305,7 @@ function apply(ctx) {
301
305
  function ledgerEventKey(event, index) {
302
306
  return typeof event.seq === 'number' ? String(event.seq) : 'event:' + index
303
307
  }
304
- function buildLedgerRecord(session, workspaceId, source = 'scan') {
308
+ function buildLedgerRecord(session, workspaceId, source = 'scan', revision) {
305
309
  const sid = session && typeof session.id === 'string' ? session.id : ''
306
310
  const events = session && Array.isArray(session.events) ? session.events : []
307
311
  if (sid === '' || workspaceId === undefined) return null
@@ -335,7 +339,7 @@ function apply(ctx) {
335
339
  if (previous !== undefined && eventSeq >= 0 && previous.seq > eventSeq) continue
336
340
  usage.set(key, { key, seq: eventSeq, time: event.time, workspaceId, modelId, values })
337
341
  }
338
- 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()) }
339
343
  }
340
344
  function normalizeLedgerRecord(raw, key) {
341
345
  if (raw === null || typeof raw !== 'object' || raw.version !== LEDGER_VERSION || typeof raw.sessionId !== 'string' || raw.sessionId !== key) return null
@@ -353,7 +357,7 @@ function apply(ctx) {
353
357
  }
354
358
  const updatedAt = typeof raw.updatedAt === 'number' ? raw.updatedAt : 0
355
359
  ledgerRevision = Math.max(ledgerRevision, updatedAt)
356
- 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()) }
357
361
  }
358
362
  function applyLedgerRecord(record) {
359
363
  if (record === null || record === undefined) return
@@ -530,6 +534,22 @@ function apply(ctx) {
530
534
  } catch (err) {
531
535
  console.error('[all-usage] session list failed:', err)
532
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
+ }
533
553
  if (disposed || generation !== aggregationGeneration) return
534
554
  if (setupFailed || !Array.isArray(records)) {
535
555
  // A transient registry failure must not be reported as a completed empty scan.
@@ -567,6 +587,20 @@ function apply(ctx) {
567
587
  if (disposed || generation !== aggregationGeneration) return
568
588
  try {
569
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
+ }
570
604
  const snap = await ctx.sessionQuery.readSession(sid)
571
605
  if (disposed || generation !== aggregationGeneration) return
572
606
  if (snap && Array.isArray(snap.events)) {
@@ -585,6 +619,12 @@ function apply(ctx) {
585
619
  usageByStep.set(item.key, { seq: item.seq, wsId: item.workspaceId, time: item.time, values: item.values, modelId: item.modelId, sid })
586
620
  }
587
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
+ }
588
628
  sessionSeq.set(sid, previous.lastSeq)
589
629
  sessionCount.add(sid)
590
630
  return
@@ -593,7 +633,7 @@ function apply(ctx) {
593
633
  } else {
594
634
  foldEvents(wsId, snap.events, undefined, sid)
595
635
  }
596
- const ledger = buildLedgerRecord({ id: sid, header: record.header, events: snap.events }, wsId)
636
+ const ledger = buildLedgerRecord({ id: sid, header: record.header, events: snap.events }, wsId, 'scan', revision)
597
637
  const canonical = ledger === null ? ledgerRecords.get(sid) : storeLedgerRecord(ledger)
598
638
  if (canonical === ledger) {
599
639
  void persistLedgerRecord(ledger)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-all-usage",
3
- "version": "1.0.7",
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",