dsh-all-usage 1.1.7 → 1.1.9

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,28 @@
2
2
 
3
3
  All notable changes to `dsh-all-usage` are documented here.
4
4
 
5
+ ## [1.1.9] - 2026-09-12
6
+
7
+ ### Fixed
8
+
9
+ - Deleting a workspace no longer erases its recorded usage. The registry probe used to call `removeSession()` for every session of a deregistered workspace and drop their ledger rows, and recovery skipped any row whose `sourceCwd` no longer mapped to a registered workspace; together they destroyed history the durable ledger had already recorded. (v1.1.6 fixed a different path — the DSH 0.1.5 `session.events` removal that wrote empty ledger records.)
10
+
11
+ ### Changed
12
+
13
+ - Usage from every removed workspace is now summed into a single **Deleted** row instead of vanishing, and one row per dead workspace is no longer created. Retiring a workspace re-keys the live aggregate in place through the new `aggregation.retargetSession()`, so usage folded from the live feed that had not reached the ledger yet survives too.
14
+ - Persisted ledger rows keep their original workspace id and cwd: retargeting happens on the in-memory copy only, the bucket is re-derived from the ledger after every restart, and a row with no usage never advertises the bucket.
15
+ - The strict registration boundary is unchanged — sessions in unregistered or deleted directories are still excluded from new statistics — and legacy `unregistered:` rows are still never resurrected. Re-registering a workspace keeps its old history in the Deleted row while new sessions count under the live workspace.
16
+
17
+ ## [1.1.8] - 2026-09-11
18
+
19
+ ### Fixed
20
+
21
+ - README.md still quoted the pre-0.1.5 compatibility declaration (`>=0.1.1-rc.1 <0.1.2`) and a verification matrix that stopped at 0.1.1, contradicting the shipped `package.json` after v1.1.6 and v1.1.7. Both language sections now record the corrected per-tuple range and list `0.1.5-rc.1` in the verified matrix.
22
+
23
+ ### Changed
24
+
25
+ - The CI runtime smoke matrix now covers `0.1.5-rc.1` alongside `0.1.1-rc.2` and `0.1.1-rc.1`, and selects the Cordis/loader/timer line per runtime version (0.1.5-rc.1 ships cordis 4.0.2 / loader 1.0.3 / timer 1.1.4) instead of pinning a single hard-coded set.
26
+
5
27
  ## [1.1.7] - 2026-09-11
6
28
 
7
29
  ### Fixed
package/README.md CHANGED
@@ -31,12 +31,13 @@ DeepSeek Harness 全量用量看板:按模型、供应商、工作区和时间
31
31
  ### 兼容性与已知限制
32
32
 
33
33
  - **运行环境**:需要 Node.js `>=22 <25`;CI 会在 Node 22 和 Node 24 上运行测试、语法检查和 npm 包内容检查。
34
- - **DSH 兼容**:`package.json` 声明 DSH runtime `>=0.1.1-rc.1 <0.1.2`,已使用 `0.1.1-rc.2` 和 `0.1.1-rc.1` 的真实 Cordis 服务链验证;`0.1.2-rc.1` 已通过本人实际使用验证兼容,但尚未纳入 CI smoke 矩阵。
34
+ - **DSH 兼容**:`package.json` 声明 DSH runtime `>=0.1.1-rc.1 <0.1.5-0 || >=0.1.5-rc.1 <0.1.6-0`,已使用 `0.1.5-rc.1`、`0.1.1-rc.2` 和 `0.1.1-rc.1` 的真实 Cordis 服务链验证;`0.1.2-rc.1` 已通过实际使用验证兼容,但未纳入 CI smoke 矩阵。声明按元组拆成两段而非写成单一区间,是因为 node-semver 只有在范围里存在与目标版本同 `major.minor.patch` 且自身带预发布标签的比较符时,才会放行该预发布版本。
35
35
  - **Web 服务依赖**:Host 将 `webServer` 声明为必需依赖,确保服务晚挂载时由 DSH 等待后再执行插件;该包面向 DSH Web profile,不提供无 WebServer 的 headless 路由。HTTP 守卫还会检查真实 socket peer,反向代理只有在连接本身来自 loopback 时才会被接受。
36
36
 
37
37
  | DSH runtime | Node.js 支持 | 真实 Cordis smoke | 结论 |
38
38
  | --- | --- | --- | --- |
39
- | `0.1.2-rc.1` | `>=22 <25` | 通过(本人实际使用验证,未纳入 CI) | 已实际验证兼容 |
39
+ | `0.1.5-rc.1` | `>=22 <25`,CI 覆盖 22/24 | 通过(真实 Cordis 服务链,Node 24) | 已声明、已验证 |
40
+ | `0.1.2-rc.1` | `>=22 <25` | 通过(实际使用验证,未纳入 CI) | 已实际验证兼容 |
40
41
  | `0.1.1-rc.2` | `>=22 <25`,CI 覆盖 22/24 | 通过(当前 Node 24) | 已声明、已验证 |
41
42
  | `0.1.1-rc.1` | `>=22 <25`,CI 覆盖 22/24 | 通过(当前 Node 24) | 已声明、已验证 |
42
43
  | 其他版本 | `>=22 <25` | 未测试 | 不在已验证矩阵内 |
@@ -46,7 +47,7 @@ DeepSeek Harness 全量用量看板:按模型、供应商、工作区和时间
46
47
  - **中断请求**:上游请求被中断时可能只有 `assistant/chunk` 的 usage,没有最终 `assistant/message`;本插件会保留该 chunk 用量。同一 `turn / step` 后续出现最终 message 时,message 会替换 chunk。若上游完全没有 usage 事件,则无法从响应内容精确恢复 Token。
47
48
  - **估算成本**:成本是基于 models.dev 价格和 DSH usage 桶的估算,不是供应商账单;目录不可用或模型没有官方匹配时不会猜测价格,而是显示未计价。缓存读取、缓存写入和 reasoning 的口径取决于 DSH 上游事件。
48
49
  - **分层价格**:models.dev 的 tiered/context-dependent 价格按本次请求的输入上下文(fresh input + cache read + cache write)选择对应档位;阈值边界遵循目录定义,无法验证的异常 tier 仍显示为 unsupported。
49
- - **工作区边界**:只有 cwd 能映射到 DSH 已注册工作区的会话才进入统计;未注册 cwd(包括已存在但未在 registry 中登记的目录)会被忽略。工作区注册列表通过 DSH 的 `domain/changed` 探针自动同步:注册表一有改动就重读并只对新增/删除的工作区做增量处理,未变化的已有工作区直接复用已计算账本,不会全量重扫。会话尚未成功 flush 前删除或损坏的日志无法由独立账本恢复。
50
+ - **工作区边界**:只有 cwd 能映射到 DSH 已注册工作区的会话才进入统计;未注册 cwd(包括已存在但未在 registry 中登记的目录)会被忽略。工作区注册列表通过 DSH 的 `domain/changed` 探针自动同步:注册表一有改动就重读并只对新增/删除的工作区做增量处理,未变化的已有工作区直接复用已计算账本,不会全量重扫。会话尚未成功 flush 前删除或损坏的日志无法由独立账本恢复。工作区被删除时历史用量不会丢失:它会被保留并汇总为一行「已删除」。
50
51
 
51
52
  ### 本地统计与官方账单
52
53
 
@@ -55,7 +56,7 @@ DeepSeek Harness 全量用量看板:按模型、供应商、工作区和时间
55
56
  - 本地统计读取 DSH 的 `assistant/chunk`、最终 `assistant/message` 和其他会话事件,按同一 `turn / step` 去重和替换;官方账单可能按供应商自己的请求、分词器、舍入、折扣、免费额度和结算周期计算。
56
57
  - 失败请求只要留下 usage chunk,就会进入本地统计;供应商是否对该失败请求收费,应以官方账单为准。
57
58
  - 价格来自 models.dev 的公开模型目录和本地显式覆盖;目录价格、供应商实际价格、区域费率和账单折扣可能不同。成本字段应理解为估算值。
58
- - 本地统计只包含已注册工作区;未注册 cwd 与已删除目录的旧 ledger 不进入统计,并可能因日志损坏、清理或上游没有发出 usage 而少于官方账单。
59
+ - 本地统计只包含已注册工作区;未注册 cwd 的旧 ledger 不进入统计。**已删除工作区**的历史用量会保留并汇总为一行「已删除」,删除工作区不会让历史统计变小。统计仍可能因日志损坏、清理或上游没有发出 usage 而少于官方账单。
59
60
 
60
61
  ### 可复现事件示例
61
62
 
@@ -106,8 +107,9 @@ node scripts/replay-fixture.mjs fixtures/usage-events.json
106
107
 
107
108
  **v1.1.5**
108
109
 
110
+ - **删除工作区不再丢数据**:工作区被删除后,已记录的用量不再从统计中移除,而是与其它已删工作区一起汇总为一行「已删除」(含尚未落账的实时用量);磁盘账本行保留原工作区 id 与 cwd,因此可逆、可在重启后重建同一个桶。
109
111
  - **工作区注册探针**:自动跟随 DSH 工作区注册表(`domain/changed` 事件);只对新增/删除的工作区增量重扫,未变化工作区直接复用账本,零全量重扫。移除了手动“刷新工作区”按钮与其 `POST /api/all-usage/workspaces/refresh` 路由。
110
- - **严格注册边界**:统计只包含 cwd 能映射到已注册工作区的会话;未登记目录(即使存在)与历史 `unregistered:` 账本行不再进入统计,已删除目录的旧账本行在恢复时跳过(`sourceCwd` 现在随每条账本持久化以校验归属)。
112
+ - **严格注册边界**:统计只包含 cwd 能映射到已注册工作区的会话;未登记目录(即使存在)与历史 `unregistered:` 账本行不再进入统计。已删除工作区的**历史**用量保留(汇总为「已删除」行),但其目录不会再接纳新会话。
111
113
 
112
114
  **v1.1.4**
113
115
 
@@ -182,7 +184,7 @@ dsh plugin --profile web add github:ParticleLight/dsh-all-usage
182
184
 
183
185
  - 使用次数与 Token 来自 DSH 会话日志;`session/flush` 只在存在新的相关事件时重建并将派生账本写入异步队列,同一 session 的 pending record 会合并,插件退出时 drain;插件激活时会回填日志与账本历史,插件卸载/重启后已成功持久化的数据不丢
184
186
  - 按日范围统计会保留全部可读取历史会话的有使用记录日期;热力图仅作为最近 53 周的固定视图窗口
185
- - 会话删除后,已成功 flush 的用量仍从独立账本恢复;会话销毁提示和周期对账只负责触发重建,不会删除账本记录
187
+ - 会话删除后,已成功 flush 的用量仍从独立账本恢复;工作区删除同样不会丢数据——其历史用量汇总为一行「已删除」(含未落账的实时用量)。会话销毁提示和周期对账只负责触发重建,不会删除账本记录
186
188
  - 同一会话的同一 `turn / step` 只保留一份最终 usage;重试或替换消息会替换旧贡献,不重复累计
187
189
  - 输入 Token 按「未含缓存命中」计(缓存命中 / 写入独立成桶);全 0 用量的重放事件不会覆盖已记录的真实用量,纯缓存命中的请求仍会计入
188
190
  - 轻量状态接口只公开 Host 实例、统计 revision、扫描进度与同步计数,不公开会话 ID、工作区路径、提示词或回复正文;完整快照仅在状态变化或手动刷新时获取
@@ -230,12 +232,13 @@ A full usage dashboard for DeepSeek Harness. Analyze tokens, cache behavior, est
230
232
  ### Compatibility and Known Limitations
231
233
 
232
234
  - **Runtime**: Node.js `>=22 <25` is required. CI runs the test suite, syntax checks, and package-content checks on Node 22 and Node 24.
233
- - **DSH compatibility**: `package.json` declares DSH runtime `>=0.1.1-rc.1 <0.1.2`; the real Cordis service chain is verified on `0.1.1-rc.2` and `0.1.1-rc.1`. `0.1.2-rc.1` has also been verified compatible through the maintainer's real-world use, but is not yet covered by the CI smoke matrix.
235
+ - **DSH compatibility**: `package.json` declares DSH runtime `>=0.1.1-rc.1 <0.1.5-0 || >=0.1.5-rc.1 <0.1.6-0`; the real Cordis service chain is verified on `0.1.5-rc.1`, `0.1.1-rc.2` and `0.1.1-rc.1`. `0.1.2-rc.1` has also been verified compatible through real-world use, but is not covered by the CI smoke matrix. The declaration is split per tuple rather than written as one interval because node-semver only admits a prerelease version when some comparator shares its exact `major.minor.patch` tuple and itself carries a prerelease tag.
234
236
  - **Web service dependency**: the Host declares `webServer` as a required dependency, so DSH waits for a late-mounted service before applying the plugin; this package targets the DSH Web profile and does not expose routes without WebServer. The HTTP guard also checks the actual socket peer, so a reverse proxy is accepted only when the connection itself is loopback.
235
237
 
236
238
  | DSH runtime | Node.js support | Real Cordis smoke | Conclusion |
237
239
  | --- | --- | --- | --- |
238
- | `0.1.2-rc.1` | `>=22 <25` | Passed through maintainer use (not in CI) | Verified compatible in real-world use |
240
+ | `0.1.5-rc.1` | `>=22 <25`, CI covers 22/24 | Passed (real Cordis service chain, Node 24) | Declared and verified |
241
+ | `0.1.2-rc.1` | `>=22 <25` | Passed through real-world use (not in CI) | Verified compatible in real-world use |
239
242
  | `0.1.1-rc.2` | `>=22 <25`, CI covers 22/24 | Passed (current Node 24) | Declared and verified |
240
243
  | `0.1.1-rc.1` | `>=22 <25`, CI covers 22/24 | Passed (current Node 24) | Declared and verified |
241
244
  | Other versions | `>=22 <25` | Not tested | Outside the verified matrix |
@@ -254,7 +257,7 @@ This plugin reports replayable statistics from local DSH event logs; it is not a
254
257
  - Local statistics read DSH `assistant/chunk`, final `assistant/message`, and related session events, then deduplicate and replace samples by logical `turn / step`. Official billing may use a provider tokenizer, rounding rules, discounts, free quotas, and billing periods.
255
258
  - A failed request is included locally whenever it leaves a usage chunk; whether the provider charged for that failed request must be checked against the official bill.
256
259
  - Prices come from the public models.dev catalog and local explicit overrides. Catalog prices can differ from provider prices, regional rates, and invoice discounts, so the cost field is an estimate.
257
- - Local statistics include registered workspaces only; unregistered cwds and old ledger rows for deleted directories are excluded, and totals can still be lower than the official bill when logs are damaged, cleaned up, or the upstream emits no usage event.
260
+ - Local statistics include registered workspaces only; old ledger rows for unregistered cwds are excluded. Usage recorded for a **deleted workspace** is kept and summed into one "Deleted" row, so removing a workspace never shrinks historical totals. Totals can still be lower than the official bill when logs are damaged, cleaned up, or the upstream emits no usage event.
258
261
 
259
262
  ### Reproducible Event Examples
260
263
 
@@ -305,8 +308,9 @@ The command loads the real plugin Host, calls its compatible APIs, checks the do
305
308
 
306
309
  **v1.1.5**
307
310
 
311
+ - **Deleting a workspace no longer loses data**: usage already recorded for a removed workspace is no longer subtracted; it is summed with every other removed workspace into one "Deleted" row (including usage folded from the live feed that had not been persisted yet). Persisted ledger rows keep their original workspace id and cwd, so the mapping stays reversible and the same bucket is rebuilt after a restart.
308
312
  - **Workspace registry probe**: all-usage follows DSH's workspace registry automatically through the `domain/changed` event; only added/removed workspaces are reprocessed incrementally while unchanged workspaces reuse their ledger with zero rescan. The manual Refresh workspaces control and its `POST /api/all-usage/workspaces/refresh` route were removed.
309
- - **Strict registration boundary**: only sessions whose cwd maps to a registered workspace are counted; unregistered directories (even existing ones) and legacy `unregistered:` ledger rows never re-enter statistics, and ledger rows for deleted directories are skipped on recovery (`sourceCwd` is now persisted with every ledger record so ownership can be validated after a restart).
313
+ - **Strict registration boundary**: only sessions whose cwd maps to a registered workspace are counted; unregistered directories (even existing ones) and legacy `unregistered:` ledger rows never re-enter statistics. Usage already recorded for a deleted workspace is kept in the "Deleted" row, but its directory never accepts new sessions again.
310
314
 
311
315
  **v1.1.4**
312
316
 
@@ -371,7 +375,7 @@ The profile patch layer hot-reloads; save the file and refresh the page.
371
375
 
372
376
  ### Data semantics
373
377
 
374
- - Calls and tokens come from DSH session logs; `session/flush` rebuilds and queues the derived ledger only when related events are dirty, coalescing the latest pending record per session and draining on plugin disposal. Readable logs and ledger history are backfilled when the plugin activates, so successfully persisted data survives reloads or session deletion
378
+ - Calls and tokens come from DSH session logs; `session/flush` rebuilds and queues the derived ledger only when related events are dirty, coalescing the latest pending record per session and draining on plugin disposal. Readable logs and ledger history are backfilled when the plugin activates, so successfully persisted data survives reloads or session deletion; deleting a workspace likewise keeps its history, summed into one "Deleted" row together with usage that had not reached the ledger yet
375
379
  - Day-level range data retains every readable historical session date with tracked usage; the heatmap is only a fixed latest-53-week view
376
380
  - 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
377
381
  - For each session and logical `turn / step`, only the final usage contribution is kept; retries or replaced messages do not double-count
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto'
2
+ import { RETIRED_WORKSPACE_ID } from './ledger.js'
2
3
  import { COST_SCHEMA_VERSION, addCostAccumulator, addCostAggregate, calculateCost, createCostAccumulator, decimalSubtract, isCostAccumulator, normalizeCostSnapshot, resolvePricing, serializeCostAggregate, temporalPlanFor } from './pricing.js'
3
4
  import { extractUsageEvent, normalizeEventSeq, normalizeUsageValues as usageValues, upsertUsageSample as upsertUsageSampleState, usageStepKey, validEventTime as validUsageEventTime } from './usage-core.js'
4
5
 
@@ -97,6 +98,9 @@ export function createAggregation(host) {
97
98
  state.byDay.clear()
98
99
  state.byDayUtc.clear()
99
100
  state.perWorkspace.clear()
101
+ // The deleted bucket is re-derived from the ledger during the rebuild that
102
+ // follows, so a stale flag cannot advertise an empty row.
103
+ state.retiredBucketUsed = false
100
104
  state.perModel.clear()
101
105
  state.usageByStep.clear()
102
106
  state.turnRecords.clear()
@@ -377,6 +381,47 @@ export function createAggregation(host) {
377
381
  if (removed > 0) markStatsChanged('data')
378
382
  return removed
379
383
  }
384
+ // Re-key every aggregate entry owned by one session to another workspace id.
385
+ //
386
+ // Retiring a workspace cannot rely on the ledger alone: usage folded from the
387
+ // live feed in the last few milliseconds has no persisted row yet, so it would
388
+ // keep a workspace id the dashboard no longer advertises. Moving the in-memory
389
+ // records keeps the totals, the per-workspace split and the query buckets
390
+ // consistent no matter when the workspace disappears.
391
+ function retargetSession(sid, workspaceId) {
392
+ if (typeof sid !== 'string' || sid === '' || typeof workspaceId !== 'string' || workspaceId === '') return 0
393
+ let moved = 0
394
+ for (const item of state.usageByStep.values()) {
395
+ if (item.sid !== sid || item.wsId === workspaceId) continue
396
+ const previousWs = item.wsId
397
+ const identity = item.identity || item.modelId
398
+ const dates = { local: item.date, utc: item.dateUtc }
399
+ // Decrement under the old workspace, then increment under the new one: the
400
+ // session, model and global totals cancel out exactly.
401
+ adjustUsage(previousWs, item.time, item.values, identity, -1, sid, dates, item.cost)
402
+ adjustQueryUsage(state.byDay.get(item.date), item, -1, false)
403
+ adjustQueryUsage(state.byDayUtc.get(item.dateUtc), item, -1, true)
404
+ item.wsId = workspaceId
405
+ adjustUsage(workspaceId, item.time, item.values, identity, 1, sid, dates, item.cost)
406
+ adjustQueryUsage(state.byDay.get(item.date), item, 1, false)
407
+ adjustQueryUsage(state.byDayUtc.get(item.dateUtc), item, 1, true)
408
+ moved += 1
409
+ }
410
+ for (const turn of state.turnRecords.values()) {
411
+ if (turn.sid !== sid || turn.wsId === workspaceId) continue
412
+ removeTurnRecord(turn)
413
+ turn.wsId = workspaceId
414
+ ensureWs(workspaceId).turns += 1
415
+ state.totals.turns += 1
416
+ addDayTurn(state.byDay, turn.date, workspaceId, turn.sid, 1)
417
+ addDayTurn(state.byDayUtc, turn.dateUtc, workspaceId, turn.sid, 1)
418
+ adjustQueryTurn(state.byDay.get(turn.date), turn, 1, false)
419
+ adjustQueryTurn(state.byDayUtc.get(turn.dateUtc), turn, 1, true)
420
+ moved += 1
421
+ }
422
+ if (moved > 0) markStatsChanged('data')
423
+ return moved
424
+ }
380
425
  function upsertUsageSample(target, sample, options = {}) {
381
426
  const previous = target.get(sample.key)
382
427
  const candidate = sample.cost === undefined
@@ -595,7 +640,10 @@ export function createAggregation(host) {
595
640
  usageSchemaVersion: 3,
596
641
  costSchemaVersion: COST_SCHEMA_VERSION,
597
642
  requestToken: state.requestToken,
598
- workspaces: Array.from(state.wsMeta.values(), (w) => ({ id: w.id, title: w.title, path: w.path })),
643
+ workspaces: [
644
+ ...Array.from(state.wsMeta.values(), (w) => ({ id: w.id, title: w.title, path: w.path })),
645
+ ...(state.retiredBucketUsed ? [{ id: RETIRED_WORKSPACE_ID, title: '', path: '', deleted: true, retiredBucket: true }] : []),
646
+ ],
599
647
  aliases: Object.assign({}, state.aliases),
600
648
  pricing: pricingSnapshot({ detailed: false }),
601
649
  tokenSemantics: {
@@ -983,6 +1031,7 @@ export function createAggregation(host) {
983
1031
  resolveCurrentPricing,
984
1032
  costForUsage,
985
1033
  removeSession,
1034
+ retargetSession,
986
1035
  resetAggregationState,
987
1036
  ensureDay,
988
1037
  ensureWs,