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 +12 -0
- package/README.md +8 -8
- package/lib/index.js +45 -5
- package/package.json +1 -1
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.
|
|
26
|
+
**v1.0.8**
|
|
26
27
|
|
|
27
|
-
-
|
|
28
|
-
-
|
|
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.
|
|
112
|
+
**v1.0.8**
|
|
112
113
|
|
|
113
|
-
-
|
|
114
|
-
-
|
|
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)
|