dsh-tokenlens 1.5.0 → 1.5.2

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
@@ -4,6 +4,26 @@
4
4
 
5
5
  **版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
6
6
 
7
+ ## 1.5.2 — 2026-11-02
8
+
9
+ (当日已发布版本:v1.3.5 / v1.3.6 / v1.3.7 / v1.3.8 / v1.3.9 / v1.4.0 / v1.5.0 / v1.5.1 / v1.5.2)
10
+
11
+ - **fix(open-design): 窗口重置后「已消耗」不归零**——Design Plan 的窗口是**锚定**的(从订阅时间起算,每 5 小时 / 7 天翻篇),但弹窗一直按「最近 5 小时 / 最近 7 天」从本机日志切片,两者根本不是同一区间。于是 5 小时窗口重置后仍把上一轮的量算进来,`已消耗` 居高不下、`预计可消耗` 被放大(实测 96.4% → 7.3% 时估算从 766M 暴涨到 10.1B)
12
+ - **fix(open-design): 7 天窗口存在同一问题**——已一并修复,不是只修 5 小时
13
+ - fix(wire): 窗口对象新增 `startedAt`(上游 `windowStart`),客户端据此按窗口真实 `[startedAt, resetsAt)` 区间累加本机每小时 Token,只统计完整落在区间内的小时;窗口边界落在整点内时不计该小时,宁可少算也不按比例猜
14
+ - fix(ui): 无法确定窗口区间时才回退到原来的滚动桶(旧 Host 没下发 `startedAt`),因此旧版本 Host 仍能工作,不会显示空白
15
+ - 测试:314 → 317(5h 重置后只统计新窗口的回归测试、7d 中途重置的锚点切片、Host 下发 `startedAt`)
16
+
17
+ ## 1.5.1 — 2026-11-02
18
+
19
+ (当日已发布版本:v1.3.5 / v1.3.6 / v1.3.7 / v1.3.8 / v1.3.9 / v1.4.0 / v1.5.0 / v1.5.1)
20
+
21
+ - **feat(open-design): chip 直接显示月用量%**——官方没有月窗口,但**月度额度是 4 × 7 天窗口**(定价表全档位一致:Go $120 vs $30、Plus $160 vs $40、Pro $360 vs $90、Max $600 vs $150)。Host 据此把周期估算填进 `windows.monthly`,chip 沿用现有「月 N%」槽位显示,无需第二套 UI
22
+ - feat(open-design): `designPlan` 增加 `periodLimitCredits`(= 周上限 × 4)与 `percent`;`limitPerWindow` 用账号**实测**上限而非公开价目表(本机 Go 实为 5h $5 / 7d $15,只有官方标价的一半),避免百分比整体偏大
23
+ - fix(open-design): **部分覆盖的估算不冒充完整值**——`complete: false` 时月窗口 `status` 置为 `unknown`,百分比仍显示但**永不触发黄色/红色告警**,避免把下限当满额报警
24
+ - feat(open-design): 月窗口的重置时间取账期结束(`subscriptionCurrentPeriodEnd`),与 5 小时 / 7 天窗口的倒计时并列显示
25
+ - 测试:307 → 314(月额度 = 4 × 周窗口、31 天账期 5 个窗口时封顶 100%、无周上限时不臆造百分比、部分覆盖状态为 unknown、Host 推导月度窗口与 designPlan 一致、chip 显示「月 8.8%」)
26
+
7
27
  ## 1.5.0 — 2026-11-02
8
28
 
9
29
  (当日已发布版本:v1.3.5 / v1.3.6 / v1.3.7 / v1.3.8 / v1.3.9 / v1.4.0 / v1.5.0)
package/PROTOCOL.md CHANGED
@@ -78,6 +78,7 @@ from `dsh-app://app/` and forwards non-asset paths to the Host).
78
78
  "windows": { // canonical ids, display order
79
79
  "rolling": { "id": "rolling", "percent": 8, "status": "ok",
80
80
  "resetsAt": "2026-10-25T11:31:25.000Z", "remainingMs": 123456,
81
+ "startedAt": "2026-10-25T06:31:25.000Z", // v1.5.2, when reported
81
82
  "used": 0.5, "cap": 2.5, "unit": "USD" } | null,
82
83
  "weekly": { … } | null,
83
84
  "monthly": { … } | null
@@ -155,10 +156,20 @@ by observing the weekly windows it sees:
155
156
  "windows": 1, // weekly windows observed
156
157
  "complete": true, // earliest window starts at periodStart
157
158
  "since": "2026-11-02T09:53:18.000Z", // first window actually observed
158
- "limitPerWindow": 150000 // the account's real weekly cap
159
+ "limitPerWindow": 150000, // the account's real weekly cap
160
+ "periodLimitCredits": 600000, // limitPerWindow * 4
161
+ "percent": 8.80 // usedCredits / periodLimitCredits
159
162
  }
160
163
  ```
161
164
 
165
+ `windows.monthly` is then filled from that estimate (v1.5.1), with `resetsAt` taken
166
+ from `periodEnd`, so the existing monthly row and the chip's `月 N%` piece carry it
167
+ without a second UI. The ×4 monthly-to-weekly ratio is the **one documented
168
+ derivation**: the published pricing table states the monthly cap as exactly four
169
+ weekly windows for every tier (Go $120/$30, Plus $160/$40, Pro $360/$90, Max
170
+ $600/$150). A partial estimate still reports a percentage but sets the window
171
+ `status` to `unknown`, so it can never paint a warning/critical dot.
172
+
162
173
  Rules the Host guarantees:
163
174
 
164
175
  - **Only the 7-day window is counted.** The 5-hour window measures the same spend
@@ -190,6 +201,13 @@ Rules:
190
201
  - The model picker emits `deepseek-official` / `deepseek-account`, never a bare
191
202
  `deepseek`: the Host aliases `deepseek-official` onto the `deepseek` adapter.
192
203
  A provider without an adapter answers `unsupported-provider`.
204
+ - **`startedAt` (v1.5.2) is the window instance's own start**, not "now minus the
205
+ window length". Design Plan windows are anchored to the subscription time
206
+ (`resetMode: "anchored_recurring"`), so a window can begin at 09:53 and reset at
207
+ 14:53 regardless of the current clock. A client that wants to describe a window
208
+ in local tokens MUST sum the interval `[startedAt, resetsAt)`; using a trailing
209
+ "last N hours" slice mixes in the previous window and keeps reporting consumed
210
+ tokens after a reset. Omitted when the upstream reports no `windowStart`.
193
211
  - **Plan capacity vs local usage (v3 split):** the snapshot above is the
194
212
  server-side **plan capacity** (provider-native units). `used` / `cap` / `unit`
195
213
  appear only for providers that report their allowance in a native unit
package/README.md CHANGED
@@ -327,7 +327,7 @@ Control Key 是 **Open Design 桌面端/CLI 登录时写入本机的会话凭据
327
327
  - OpenCode 的额度定义见[官方文档](https://opencode.ai/docs/go/#usage-limits)(5h = 月度额度的 20%、周 = 50%、月 = 100%);接口只返回百分比,插件也只画百分比。
328
328
  - Command Code 的 `/alpha/*` 是官方 CLI 自用的私有计费路由;免费 / 无方案账户返回空窗口(显示 `—`),插件不会用方案目录猜额度。
329
329
  - Open Design 的窗口按账户实际返回的时长渲染:Go 同时有 5 小时和 7 天窗口,Plus / Pro / Max 只有 7 天窗口;钱包余额是独立于窗口的 USD 额度,用尽后继续扣钱包。查询要求工作区权限,鉴权失败会明确显示错误而不是 0;工作区 ID 与 Control Key 缺任一项时,弹窗会指出缺哪一项并给出「去配置」入口(v1.3.5)。窗口、余额、套餐档位是**三层独立读取**(v1.4.0):任一接口不可用时只少那一块数据,不会整套报错。
330
- - **「本期 Design Plan 累计」是插件本机算的估算值**(v1.5.0)。Open Design 有月度额度(Go 为每月 $120 = 4 × 每周 $30),但**没有「本账期已用」接口**——控制台的 `/billing/usage`、`/billing/coding-plan-usage` 都靠浏览器 Cookie 鉴权,Control Key 一律 401。于是插件累加它观测到的 **7 天窗口**(只记 7 天,因为 5 小时窗口测量的是同一笔消耗,相加会重复计数),按 `creditsPerUsd` 折算成美元;弹窗会标注覆盖情况(「已覆盖本账期全部窗口」或「仅统计已观测的 N 个窗口」),插件没运行期间错过的窗口无法计入。
330
+ - **chip 的「月 N%」与「本期 Design Plan 累计」是插件本机算的估算值**(v1.5.0 / v1.5.1)。Open Design 有月度额度(Go 为每月 $120 = 4 × 每周 $30),但**没有「本账期已用」接口**——控制台的 `/billing/usage`、`/billing/coding-plan-usage` 都靠浏览器 Cookie 鉴权,Control Key 一律 401。于是插件累加它观测到的 **7 天窗口**(只记 7 天,因为 5 小时窗口测量的是同一笔消耗,相加会重复计数),再按官方「月度 = 4 × 周额度」的比例折算成百分比填进 chip 的「月」槽位,并按 `creditsPerUsd` 折算美元。弹窗会标注覆盖情况(「已覆盖本账期全部窗口」或「仅统计已观测的 N 个窗口」);覆盖不完整时百分比仍显示但**不会触发告警配色**,插件没运行期间错过的窗口无法计入。
331
331
  - 未配置凭据的供应商 chip 静默隐藏;`zai`、`minimax`、`deepinfra` 属 best-effort 适配(未公开接口,失败时按可重试错误优雅降级)。
332
332
  - 各供应商的元数据(`kind` / `label` / `shortLabel` / `baseURL` / `apiKeyRef` / `consoleUrl`)集中在 `lib/providers.mjs` 的 `PROVIDER_DEFAULTS` 与 `ADAPTERS` 中,新增适配器只需在该文件登记并补测试。
333
333
 
@@ -439,7 +439,7 @@ Host 半注册 `tokenlens_usage`,参数 `action`(默认 `snapshot`)与可
439
439
 
440
440
  ## 验证
441
441
 
442
- - `node --test`:**307 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、Design Plan 周期账本、插件集成与浏览器端渲染 / 交互。
442
+ - `node --test`:**317 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、Design Plan 周期账本、窗口锚点切片、插件集成与浏览器端渲染 / 交互。
443
443
  - 在真实环境中验证过 **5 个 harness** 的数据源;本机统计与独立的 SQLite 查询、独立的日志解析逐项对账(差额为 0),跨 harness 合并与供应商映射算术自洽。
444
444
  - 供应商接口按各官方文档与参考客户端行为验证;未配置凭据时 chip 静默隐藏,接口异常时按可重试错误降级。
445
445
 
package/client.js CHANGED
@@ -993,6 +993,10 @@ window.__ModuleLoader__.load({
993
993
  status: typeof raw.status === 'string' && raw.status !== '' ? raw.status : 'unknown',
994
994
  resetsAt: typeof raw.resetsAt === 'string' ? raw.resetsAt : null,
995
995
  remainingMs: Number.isFinite(raw.remainingMs) ? raw.remainingMs : null,
996
+ // v1.5.2: the window instance's own start (an anchored Design Plan window
997
+ // does not begin "N hours ago"), so the local token slice is aligned with
998
+ // the allowance the percentage describes.
999
+ startedAt: typeof raw.startedAt === 'string' ? raw.startedAt : null,
996
1000
  // Provider-native capacity (e.g. Command Code USD): shown only when all
997
1001
  // three fields are usable, so an unsized window never renders "$undefined".
998
1002
  used: numberOrNull(raw.used),
@@ -1680,7 +1684,72 @@ window.__ModuleLoader__.load({
1680
1684
  }
1681
1685
 
1682
1686
  /**
1683
- * The `{short, full}` labels of a provider (v0.10.0).
1687
+ * Local tokens spent inside one allowance window (v1.5.2).
1688
+ *
1689
+ * A Design Plan window is ANCHORED: it starts at the subscription anchor and
1690
+ * resets every N hours, so it is not "the last N hours" (the old behaviour,
1691
+ * which kept showing pre-reset consumption after a reset and inflated the
1692
+ * estimated quota ~13x). The window's own `startedAt` / `resetsAt` bound the
1693
+ * slice, folded from the per-day, per-provider hour histograms.
1694
+ *
1695
+ * @returns the token total, or `null` when the range cannot be resolved (an
1696
+ * old Host without `startedAt`, or no histogram), in which case the caller
1697
+ * falls back to the rolling bucket.
1698
+ */
1699
+ function localTokensInWindow(stats, statsRow, providerId, window) {
1700
+ if (stats === null || statsRow === null || window === null) return null
1701
+ const startMs = typeof window.startedAt === 'string' ? Date.parse(window.startedAt) : NaN
1702
+ const endMs = typeof window.resetsAt === 'string' ? Date.parse(window.resetsAt) : NaN
1703
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) return null
1704
+ const daily = stats.daily
1705
+ if (!Array.isArray(daily) || daily.length === 0) return null
1706
+ let total = null
1707
+ for (const entry of daily) {
1708
+ // A day bucket only contributes whole hours through `hours`; resolve the
1709
+ // day's local midnight, then walk its 24 slots.
1710
+ const dayStartMs = localDayStartMs(entry.day)
1711
+ if (!Number.isFinite(dayStartMs)) continue
1712
+ const providerHours = entry.providers?.[providerId]?.hours ?? null
1713
+ const hours = Array.isArray(providerHours) ? providerHours : null
1714
+ if (hours === null) continue
1715
+ for (let slot = 0; slot < hours.length && slot < 24; slot += 1) {
1716
+ const hourStart = dayStartMs + slot * 3600000
1717
+ const hourEnd = hourStart + 3600000
1718
+ // An hour counts only when the whole slot lies inside the window; a
1719
+ // window boundary mid-hour would otherwise need a proportional guess.
1720
+ if (hourStart < startMs || hourEnd > endMs) continue
1721
+ total = (total ?? 0) + (Number.isFinite(hours[slot]) ? hours[slot] : 0)
1722
+ }
1723
+ }
1724
+ return total
1725
+ }
1726
+
1727
+ /** Local midnight (epoch ms) of a `YYYY-MM-DD` day key, or NaN. */
1728
+ function localDayStartMs(day) {
1729
+ if (typeof day !== 'string') return Number.NaN
1730
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(day)
1731
+ if (match === null) return Number.NaN
1732
+ const ms = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])).getTime()
1733
+ return Number.isFinite(ms) ? ms : Number.NaN
1734
+ }
1735
+
1736
+ /**
1737
+ * The local tokens consumed inside one allowance window.
1738
+ *
1739
+ * Prefers the window-anchored slice; falls back to the rolling provider
1740
+ * bucket only when the window bounds are unavailable (an older Host).
1741
+ */
1742
+ function windowLocalUsed(stats, statsRow, providerId, window) {
1743
+ const anchored = localTokensInWindow(stats, statsRow, providerId, window)
1744
+ if (anchored !== null) return anchored
1745
+ const id = window?.id ?? ''
1746
+ if (id === 'rolling') return statsRow?.fiveHours?.total ?? 0
1747
+ if (id === 'weekly') return statsRow?.last7Days?.total ?? 0
1748
+ if (id === 'monthly') return statsRow?.last30Days?.total ?? 0
1749
+ return 0
1750
+ }
1751
+
1752
+ /** The `{short, full}` labels of a provider (v0.10.0).
1684
1753
  *
1685
1754
  * Wire labels cached from the providers payload win, then the local catalog,
1686
1755
  * then the snapshot / raw id. Both fields are always usable strings, so the
@@ -3829,7 +3898,7 @@ window.__ModuleLoader__.load({
3829
3898
  }
3830
3899
 
3831
3900
  /** One allowance-window row: label + percent, bar, reset countdown, status. */
3832
- function buildWindowRow(id, window, state, now, t, statsRow = null) {
3901
+ function buildWindowRow(id, window, state, now, t, statsRow = null, stats = null, snapshotProviderId = '') {
3833
3902
  const label = rowLabel(id, t)
3834
3903
  const level = window === null ? 'unknown' : levelOf(window.percent, window.status)
3835
3904
  const percentText = formatPercent(window?.percent ?? null, t)
@@ -3838,14 +3907,10 @@ window.__ModuleLoader__.load({
3838
3907
  // v0.30.13: local-token consumption + estimated consumable quota. The used
3839
3908
  // number comes from the selected provider's local stats buckets; the quota
3840
3909
  // is only an estimate derived from the provider-reported percentage.
3841
- const localUsed =
3842
- id === 'rolling'
3843
- ? statsRow?.fiveHours?.total ?? 0
3844
- : id === 'weekly'
3845
- ? statsRow?.last7Days?.total ?? 0
3846
- : id === 'monthly'
3847
- ? statsRow?.last30Days?.total ?? 0
3848
- : 0
3910
+ // v1.5.2: slice from the window's OWN bounds, so a reset window starts from
3911
+ // zero instead of replaying the previous window's consumption.
3912
+ const providerId = statsRow?.providerId ?? snapshotProviderId ?? ''
3913
+ const localUsed = windowLocalUsed(stats, statsRow, providerId, window)
3849
3914
  const percent = window !== null && Number.isFinite(window.percent) ? window.percent : null
3850
3915
  const quota = percent !== null && percent > 0 && localUsed > 0 ? localUsed / (percent / 100) : null
3851
3916
  // v0.30.13 follow-up: the estimate is INLINE on the existing reset line
@@ -5372,7 +5437,9 @@ window.__ModuleLoader__.load({
5372
5437
  const balanceRow = buildCapacityBalanceRow(snapshot.balance, t)
5373
5438
  const designPlanRow = buildDesignPlanRow(snapshot.designPlan, t)
5374
5439
  if (snapshot.windows !== null) {
5375
- const rows = WINDOW_ORDER.map((id) => buildWindowRow(id, snapshot.windows[id], state, now, t, statsRow))
5440
+ const rows = WINDOW_ORDER.map((id) =>
5441
+ buildWindowRow(id, snapshot.windows[id], state, now, t, statsRow, stats, snapshot.provider.id),
5442
+ )
5376
5443
  if (balanceRow !== null) rows.push(balanceRow)
5377
5444
  if (designPlanRow !== null) rows.push(designPlanRow)
5378
5445
  section.push(h('div', { key: 'rows', className: 'dsh-tokenlens-rows' }, rows))
package/index.js CHANGED
@@ -83,7 +83,7 @@ export const inject = []
83
83
  export const PLUGIN_NAME = 'dsh-tokenlens'
84
84
 
85
85
  /** Plugin version; mirrors package.json (kept here so diagnostics need no fs read). */
86
- export const PLUGIN_VERSION = '1.5.0'
86
+ export const PLUGIN_VERSION = '1.5.2'
87
87
 
88
88
  /** Documented defaults; every field of the raw row config is optional. */
89
89
  const DEFAULTS = {
@@ -439,6 +439,40 @@ export function apply(ctx, rawConfig) {
439
439
  return summarizeDesignPlan(designPlanLedger, periodStart)
440
440
  }
441
441
 
442
+ /**
443
+ * Fill the `monthly` window from the period estimate (v1.5.1).
444
+ *
445
+ * Open Design reports only rolling 5h/7d windows, so `windows.monthly` is
446
+ * always null upstream — yet the dialog already knows how to render a monthly
447
+ * row (label, bar, reset countdown) and the chip already knows how to show a
448
+ * monthly percentage. Rather than inventing a parallel UI, place the estimate
449
+ * in the slot it belongs to. The `designPlan` field stays on the wire as the
450
+ * machine-readable source, including its `complete` coverage flag; the client
451
+ * uses it to qualify the number as an estimate.
452
+ *
453
+ * The percentage is `usedCredits / (weeklyLimit * 4)`; the ×4 is the published
454
+ * monthly-to-weekly ratio (see design-plan-ledger.mjs) and the only derived
455
+ * step. Without a usable weekly limit there is no honest denominator, so the
456
+ * monthly slot stays empty instead of showing a made-up percentage.
457
+ */
458
+ function withMonthlyWindow(windows, designPlan) {
459
+ if (designPlan === null || designPlan.percent === null) return windows
460
+ const base = windows !== null && typeof windows === 'object' ? windows : { rolling: null, weekly: null }
461
+ return {
462
+ ...base,
463
+ monthly: {
464
+ id: 'monthly',
465
+ percent: designPlan.percent,
466
+ // A partial estimate must not raise an alarm: only a complete period is
467
+ // allowed to reach the warning/critical thresholds downstream.
468
+ status: designPlan.complete === true ? 'ok' : 'unknown',
469
+ resetsAt: designPlan.periodEnd === '' ? null : designPlan.periodEnd,
470
+ remainingMs:
471
+ designPlan.periodEnd === '' ? null : Math.max(0, Date.parse(designPlan.periodEnd) - Date.now()),
472
+ },
473
+ }
474
+ }
475
+
442
476
  /** Built-in defaults + profile config + management-page overrides. */
443
477
  function effectiveProviderConfig(providerId) {
444
478
  const id = canonicalProviderId(providerId)
@@ -1174,7 +1208,7 @@ export function apply(ctx, rawConfig) {
1174
1208
  return buildSnapshot({
1175
1209
  provider: providerInfo(providerId),
1176
1210
  plan: read.result.plan ?? null,
1177
- windows: read.result.windows ?? null,
1211
+ windows: withMonthlyWindow(read.result.windows ?? null, designPlan),
1178
1212
  balance: read.result.balance ?? null,
1179
1213
  credits: read.result.credits ?? null,
1180
1214
  account: read.result.account ?? null,
@@ -221,14 +221,28 @@ export function observeDesignPlan(ledger, input) {
221
221
  }
222
222
  }
223
223
 
224
+ /**
225
+ * How many weekly windows make up one monthly allowance.
226
+ *
227
+ * The published pricing table states it for every tier — Go $120/mo vs $30
228
+ * weekly, Plus $160 vs $40, Pro $360 vs $90, Max $600 vs $150 — so the monthly
229
+ * cap is always exactly four weekly windows. This is the ONLY step in the
230
+ * estimate that is a documented ratio rather than an observed number.
231
+ */
232
+ export const WEEKS_PER_PERIOD = 4
233
+
224
234
  /**
225
235
  * Summarize one period: the estimated Design Plan usage the Host has observed.
226
236
  *
227
237
  * @returns `null` when the period has no observations at all, else
228
- * `{ usedCredits, estimatedUsd, windows, complete, since, periodStart, periodEnd, limitPerWindow }`.
229
- * `complete` is true only when the earliest observed window starts at the
230
- * period start — otherwise earlier windows of this period were never seen and
231
- * the number is a lower bound.
238
+ * `{ usedCredits, estimatedUsd, windows, complete, since, periodStart,
239
+ * periodEnd, limitPerWindow, periodLimitCredits, percent }`.
240
+ * `percent` is `usedCredits / periodLimitCredits`, where the period limit is
241
+ * `limitPerWindow * WEEKS_PER_PERIOD` (the published monthly cap); it is null
242
+ * when the upstream reported no usable weekly limit. `complete` is true only
243
+ * when the earliest observed window starts at the period start — otherwise
244
+ * earlier windows of this period were never seen and both numbers are lower
245
+ * bounds.
232
246
  */
233
247
  export function summarizeDesignPlan(ledger, periodStart) {
234
248
  const key = isoOf(periodStart)
@@ -248,6 +262,9 @@ export function summarizeDesignPlan(ledger, periodStart) {
248
262
  Number.isSafeInteger(creditsPerUsd) && creditsPerUsd > 0 ? usedCredits / creditsPerUsd : null
249
263
  const since = entries[0][0]
250
264
  const limitPerWindow = entries[entries.length - 1][1].limitCredits
265
+ const usableWindowLimit = Number.isSafeInteger(limitPerWindow) && limitPerWindow > 0 ? limitPerWindow : null
266
+ const periodLimitCredits = usableWindowLimit === null ? null : usableWindowLimit * WEEKS_PER_PERIOD
267
+ const percent = periodLimitCredits === null ? null : (usedCredits / periodLimitCredits) * 100
251
268
  return {
252
269
  periodStart: key,
253
270
  periodEnd: period.periodEnd || '',
@@ -256,7 +273,9 @@ export function summarizeDesignPlan(ledger, periodStart) {
256
273
  windows,
257
274
  complete: since === key,
258
275
  since,
259
- limitPerWindow: Number.isSafeInteger(limitPerWindow) && limitPerWindow > 0 ? limitPerWindow : null,
276
+ limitPerWindow: usableWindowLimit,
277
+ periodLimitCredits,
278
+ percent: percent === null ? null : Math.min(100, Math.max(0, percent)),
260
279
  }
261
280
  }
262
281
 
package/lib/providers.mjs CHANGED
@@ -344,12 +344,17 @@ export function makeWindow(id, input, now) {
344
344
  const used = toNumber(input?.used)
345
345
  const cap = toNumber(input?.cap)
346
346
  const unit = typeof input?.unit === 'string' && input.unit !== '' ? input.unit : undefined
347
+ // v1.5.2: the upstream window's own start instant, when the provider reports
348
+ // one. The client slices its local token counters from here so the numbers it
349
+ // shows describe THIS window rather than an arbitrary trailing period.
350
+ const startMs = parseTimestamp(input?.windowStart)
347
351
  return {
348
352
  id,
349
353
  percent,
350
354
  status: typeof input?.status === 'string' && input.status !== '' ? input.status : 'unknown',
351
355
  resetsAt: resetsMs === null ? null : new Date(resetsMs).toISOString(),
352
356
  remainingMs: resetsMs === null ? null : Math.max(0, resetsMs - now),
357
+ ...(startMs === null ? {} : { startedAt: new Date(startMs).toISOString() }),
353
358
  ...(used === null ? {} : { used }),
354
359
  ...(cap === null ? {} : { cap }),
355
360
  ...(used === null || cap === null || unit === undefined ? {} : { unit }),
@@ -843,6 +848,10 @@ export function parseOpenDesignUsage(payload, workspaceId, now = Date.now()) {
843
848
  status: 'ok',
844
849
  resetsAt:
845
850
  typeof row.resetsAt === 'string' || typeof row.resetsAt === 'number' ? row.resetsAt : undefined,
851
+ // v1.5.2: the ACTUAL start of this window instance. Design Plan windows are
852
+ // anchored to the subscription time and reset every N hours, so the local
853
+ // token counters must slice from here — not from a naive "last N hours".
854
+ windowStart: typeof row.windowStart === 'string' ? row.windowStart : undefined,
846
855
  }
847
856
  }
848
857
  const windows = windowSet(inputs, now)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tokenlens",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "type": "module",
5
5
  "description": "dsh-TokenLens (词元透镜) — provider-aware plan capacity plus local token statistics (today/7d/30d/all-time, by harness/provider/model/workspace) for DSH.",
6
6
  "license": "MIT",