dsh-token-total 1.1.2 → 1.1.3

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/README.md CHANGED
@@ -43,7 +43,7 @@ DSH(DeepSeek Harness)Web 插件:**Token 用量统计看板** + **单会话
43
43
  - **Token 胶囊**显示的是**整棵会话树**(主智能体 + 全部子智能体)的合计,点开弹窗分为「主智能体 / 子智能体 / 会话合计」三行,并给出子智能体会话数。
44
44
  - 子智能体是独立持久化的会话,通过日志头的 `parentSession` 串成树;插件按需重放这些日志汇总(同一套 `(turn, step)` 去重规则,且跳过 fork 继承来的事件前缀,避免与父会话重复计数)。
45
45
  - **会话 Token 明细始终开启**:未命中缓存、缓存读 / 写、输出分别列出;底部胶囊的缓存命中率始终显示两位小数。
46
- - **流式吞吐速度始终开启**:始终显示当前的实时解码速度——输出过程中按已到达的流式增量实测(每步开始约 0.2 秒内出首个读数);一步结算后保持该步最后的实测值,工具执行、步间空档也不回落到会话均值,只有从未产生过输出的会话才显示会话累计的精确值。
46
+ - **流式吞吐速度始终开启**:输出过程中显示当前这一步的实时解码速度——按已到达的流式增量实测(每步开始约 0.2 秒内出首个读数);一旦没有步骤在流(一步刚结算、工具执行中、步间空档),就交回本轮会话的加权平均解码速度(Σ 输出 token ÷ Σ 解码耗时,与会话「用量」页里的平均速率同口径),而不是停在最后那次短窗口读数上——那正是底部 57 tok/s 与请求记录 156 token/秒看起来互相矛盾的原因。
47
47
  - 不再提供底部增强功能的开关;旧版本保存的关闭设置不再影响显示。
48
48
 
49
49
  ### 会话「用量」页
package/lib/client.js CHANGED
@@ -31661,14 +31661,14 @@ window.__ModuleLoader__.load({
31661
31661
  * cannot see.
31662
31662
  *
31663
31663
  * Streaming throughput replaces the gauge pill's speed
31664
- * figure with a live estimate of what is happening now. The figure is the
31665
- * streaming step's measured rate while one streams and the LAST measured rate
31666
- * once it settles — a session average describes the whole session's past, and
31667
- * it cannot match the pace of the answer on screen, which is what users read
31668
- * as the pill disagreeing with the stream in front of them. The official
31669
- * session figure only shows on a session that has never streamed. That
31670
- * subscription lives in
31671
- * its own child component so stream deltas never re-render the pills. The live
31664
+ * figure with a live estimate of what is happening now, and hands the slot back
31665
+ * to the session's decode average when nothing is streaming. While a step
31666
+ * streams the live rate is the only reading that can match the pace of the
31667
+ * answer on screen; once the step settles it becomes a figure from a window
31668
+ * nobody is watching any more, so the pill returns to the same quotient the
31669
+ * usage view quotes (ΣoutputTokens / ΣdecodeMs) instead of freezing on the last
31670
+ * measurement. That subscription lives in its own child component, so stream
31671
+ * deltas never re-render the pills. The live
31672
31672
  * figure itself is measured by a bounded sliding window over observed token
31673
31673
  * growth (stats-line-core's StreamingRateSampler): the earlier cumulative
31674
31674
  * quotient charged a whole step's text to a window that had just opened, so a
@@ -31754,14 +31754,23 @@ window.__ModuleLoader__.load({
31754
31754
  * The gauge pill's always-on streaming speed slot.
31755
31755
  *
31756
31756
  * It subscribes to `legacy.partial` itself so per-frame deltas re-render only
31757
- * this child, not the surrounding pills. The figure it carries is the CURRENT
31758
- * stream's measured rate, and it stays that rate once the step settles:
31759
- * tool execution and the gap between steps happen inside the same answer, and
31760
- * the session average this slot used to fall back to there is a figure from
31761
- * the whole session's past — a number that cannot match the pace of the answer
31762
- * on screen, which is what reads as "not the real-time speed". The official
31763
- * session figure is only a session's opening state, shown until the first
31764
- * window measures anything.
31757
+ * this child, not the surrounding pills. Two figures share the slot, chosen by
31758
+ * whether a step is streaming right now:
31759
+ *
31760
+ * - while one is (`partial != null`), the measured rate of THAT stream — a
31761
+ * bounded sliding window over observed token growth (stats-line-core's
31762
+ * StreamingRateSampler), the only reading that can track the pace of the
31763
+ * answer currently on screen;
31764
+ * - otherwise the session's own decode average, ΣoutputTokens / ΣdecodeMs —
31765
+ * the same quotient `decodeRateOf` quotes in the usage view. An idle composer
31766
+ * must not freeze on whatever the last window happened to measure: a settled
31767
+ * thirty-step turn kept showing its final second-long reading (57 tok/s)
31768
+ * beside the usage view's 156 tok/s for the same work, and the two read as a
31769
+ * contradiction rather than as two scopes of one number.
31770
+ *
31771
+ * The measured reading carries the step it was taken in, so a new step cannot
31772
+ * inherit the previous one's figure before its own window publishes; nothing is
31773
+ * held across the gap — the gap is exactly when the average applies.
31765
31774
  *
31766
31775
  * One window covers a whole step, across every output kind the accumulator
31767
31776
  * carries — reasoning, answer text, and Tool-call arguments — because the
@@ -31772,7 +31781,8 @@ window.__ModuleLoader__.load({
31772
31781
  * state, so no render reads a clock or mutates the window: a render React starts
31773
31782
  * and then discards must not decide what the next committed one displays. The
31774
31783
  * rate itself comes from stats-line-core's StreamingRateSampler — a bounded
31775
- * window over observed token growth, smoothed, and held while nothing arrives.
31784
+ * window over observed token growth, smoothed, and held while nothing arrives
31785
+ * inside the step it belongs to.
31776
31786
  */
31777
31787
  function LiveSpeed({ useChat, scale, fallback, t }) {
31778
31788
  const partial = useChat((s) => s.legacy.partial);
@@ -31788,13 +31798,17 @@ window.__ModuleLoader__.load({
31788
31798
  const scaleRef = (0, react.useRef)(scale);
31789
31799
  const [beat, setBeat] = (0, react.useState)(0);
31790
31800
  (0, react.useLayoutEffect)(() => {
31801
+ if (key === null) return;
31791
31802
  if (scaleRef.current !== scale) {
31792
31803
  scaleRef.current = scale;
31793
31804
  rate.reset();
31794
31805
  }
31795
31806
  rate.observe(tokens, Date.now());
31796
31807
  const next = rate.ratePerSecond();
31797
- if (next !== null) setReading((previous) => previous === next ? previous : next);
31808
+ if (next !== null) setReading((previous) => previous !== null && previous.key === key && previous.value === next ? previous : {
31809
+ key,
31810
+ value: next
31811
+ });
31798
31812
  }, [
31799
31813
  tokens,
31800
31814
  key,
@@ -31812,7 +31826,8 @@ window.__ModuleLoader__.load({
31812
31826
  clearInterval(handle);
31813
31827
  };
31814
31828
  }, [streaming]);
31815
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpeedFigure, { text: reading === null ? fallback : t("stats.tokensPerSecond", { throughput: formatTokensPerSecond(reading) }) });
31829
+ const measured = reading !== null && reading.key === key ? reading.value : null;
31830
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpeedFigure, { text: measured === null ? fallback : t("stats.tokensPerSecond", { throughput: formatTokensPerSecond(measured) }) });
31816
31831
  }
31817
31832
  function TimePill({ stats, t, dialog, useChat, scale, dock }) {
31818
31833
  const { open, setOpen, rootRef, panelRef, pos } = useStatDialog(dialog);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-token-total",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "DSH web plugin: token 用量统计看板 + 单会话(主智能体 + 子智能体)用量统计。提供今日/近3天/近7天/近1月/近3月/自定义筛选、六个汇总卡片、活跃热力图、缓存命中率折线与分模型柱状图、按模型/按供应商 Top5 列表。",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",
@@ -31,14 +31,14 @@
31
31
  * cannot see.
32
32
  *
33
33
  * Streaming throughput replaces the gauge pill's speed
34
- * figure with a live estimate of what is happening now. The figure is the
35
- * streaming step's measured rate while one streams and the LAST measured rate
36
- * once it settles — a session average describes the whole session's past, and
37
- * it cannot match the pace of the answer on screen, which is what users read
38
- * as the pill disagreeing with the stream in front of them. The official
39
- * session figure only shows on a session that has never streamed. That
40
- * subscription lives in
41
- * its own child component so stream deltas never re-render the pills. The live
34
+ * figure with a live estimate of what is happening now, and hands the slot back
35
+ * to the session's decode average when nothing is streaming. While a step
36
+ * streams the live rate is the only reading that can match the pace of the
37
+ * answer on screen; once the step settles it becomes a figure from a window
38
+ * nobody is watching any more, so the pill returns to the same quotient the
39
+ * usage view quotes (ΣoutputTokens / ΣdecodeMs) instead of freezing on the last
40
+ * measurement. That subscription lives in its own child component, so stream
41
+ * deltas never re-render the pills. The live
42
42
  * figure itself is measured by a bounded sliding window over observed token
43
43
  * growth (stats-line-core's StreamingRateSampler): the earlier cumulative
44
44
  * quotient charged a whole step's text to a window that had just opened, so a
@@ -181,14 +181,23 @@ const LIVE_SPEED_REFRESH_MS = 1_000
181
181
  * The gauge pill's always-on streaming speed slot.
182
182
  *
183
183
  * It subscribes to `legacy.partial` itself so per-frame deltas re-render only
184
- * this child, not the surrounding pills. The figure it carries is the CURRENT
185
- * stream's measured rate, and it stays that rate once the step settles:
186
- * tool execution and the gap between steps happen inside the same answer, and
187
- * the session average this slot used to fall back to there is a figure from
188
- * the whole session's past — a number that cannot match the pace of the answer
189
- * on screen, which is what reads as "not the real-time speed". The official
190
- * session figure is only a session's opening state, shown until the first
191
- * window measures anything.
184
+ * this child, not the surrounding pills. Two figures share the slot, chosen by
185
+ * whether a step is streaming right now:
186
+ *
187
+ * - while one is (`partial != null`), the measured rate of THAT stream — a
188
+ * bounded sliding window over observed token growth (stats-line-core's
189
+ * StreamingRateSampler), the only reading that can track the pace of the
190
+ * answer currently on screen;
191
+ * - otherwise the session's own decode average, ΣoutputTokens / ΣdecodeMs —
192
+ * the same quotient `decodeRateOf` quotes in the usage view. An idle composer
193
+ * must not freeze on whatever the last window happened to measure: a settled
194
+ * thirty-step turn kept showing its final second-long reading (57 tok/s)
195
+ * beside the usage view's 156 tok/s for the same work, and the two read as a
196
+ * contradiction rather than as two scopes of one number.
197
+ *
198
+ * The measured reading carries the step it was taken in, so a new step cannot
199
+ * inherit the previous one's figure before its own window publishes; nothing is
200
+ * held across the gap — the gap is exactly when the average applies.
192
201
  *
193
202
  * One window covers a whole step, across every output kind the accumulator
194
203
  * carries — reasoning, answer text, and Tool-call arguments — because the
@@ -199,13 +208,16 @@ const LIVE_SPEED_REFRESH_MS = 1_000
199
208
  * state, so no render reads a clock or mutates the window: a render React starts
200
209
  * and then discards must not decide what the next committed one displays. The
201
210
  * rate itself comes from stats-line-core's StreamingRateSampler — a bounded
202
- * window over observed token growth, smoothed, and held while nothing arrives.
211
+ * window over observed token growth, smoothed, and held while nothing arrives
212
+ * inside the step it belongs to.
203
213
  */
204
214
  function LiveSpeed({ useChat, scale, fallback, t }: {
205
215
  useChat: ChatSnapshotSelectorHook
206
216
  /** Correction measured from this session's settled steps (1 = raw prior). */
207
217
  scale: number
208
- /** The official session figure, shown until the slot has measured its own. */
218
+ /** The session's own decode average (ΣoutputTokens / ΣdecodeMs): what the
219
+ * slot shows whenever no step is streaming, and before the streaming step's
220
+ * window has published its first reading. */
209
221
  fallback: string | null
210
222
  t: T
211
223
  }) {
@@ -223,18 +235,22 @@ function LiveSpeed({ useChat, scale, fallback, t }: {
223
235
  sampler.current = { key, rate: new StreamingRateSampler() }
224
236
  }
225
237
  const rate = sampler.current.rate
226
- // The latest figure this slot has measured, held across step boundaries. A
227
- // settled step's rate is still the conversation's current speed until the
228
- // next step measures its own, so the display keeps it instead of switching to
229
- // a session average the moment the stream goes quiet — holding it is the
230
- // point, not a stale value to guard against, which is why no step tag rides
231
- // along any more.
232
- const [reading, setReading] = useState<number | null>(null)
238
+ // The figure last measured, tagged with the step it came from. The tag is
239
+ // what retires it: a reading taken in 3:4 never shows during 3:5, and with
240
+ // no step streaming (`key === null`) no tag can match at all, so the slot
241
+ // falls through to the session average on its own — no timer, no clearing
242
+ // effect, and no stale figure left standing after a turn settles.
243
+ const [reading, setReading] = useState<{ key: string; value: number } | null>(null)
233
244
  const scaleRef = useRef(scale)
234
245
  // Every delta re-runs this (its dependencies move), and the beat re-runs it once
235
246
  // a second so a step that has gone quiet still advances its window.
236
247
  const [beat, setBeat] = useState(0)
237
248
  useLayoutEffect(() => {
249
+ // Nothing is streaming: the slot shows the session average, and there is no
250
+ // window to advance. Skipping the observation also keeps a settled step's
251
+ // sampler from being fed a zero estimate, which would re-anchor it against
252
+ // text that is already counted.
253
+ if (key === null) return
238
254
  // A correction that moved re-prices the whole estimate, so growth already
239
255
  // counted under the old one must not be measured against the new one: that
240
256
  // would read as a burst the stream never produced.
@@ -244,11 +260,13 @@ function LiveSpeed({ useChat, scale, fallback, t }: {
244
260
  }
245
261
  rate.observe(tokens, Date.now())
246
262
  // A null here means the fresh window has not measured anything yet (a step
247
- // that just opened, or one that re-anchored); the figure already on screen
248
- // is still the best current reading, so it is left alone.
263
+ // that just opened, or one that re-anchored); the average stays up until
264
+ // this step's own window publishes something.
249
265
  const next = rate.ratePerSecond()
250
266
  if (next !== null) {
251
- setReading(previous => (previous === next ? previous : next))
267
+ setReading(previous => (previous !== null && previous.key === key && previous.value === next
268
+ ? previous
269
+ : { key, value: next }))
252
270
  }
253
271
  }, [tokens, key, beat, rate, scale])
254
272
  // Keyed on "a step is streaming", not on the step identity: switching steps
@@ -259,9 +277,12 @@ function LiveSpeed({ useChat, scale, fallback, t }: {
259
277
  const handle = setInterval(() => { setBeat(Date.now()) }, LIVE_SPEED_REFRESH_MS)
260
278
  return () => { clearInterval(handle) }
261
279
  }, [streaming])
262
- return <SpeedFigure text={reading === null
280
+ // The tag test is the whole selection rule: a reading only shows while the
281
+ // step that produced it is the step streaming now.
282
+ const measured = reading !== null && reading.key === key ? reading.value : null
283
+ return <SpeedFigure text={measured === null
263
284
  ? fallback
264
- : t('stats.tokensPerSecond', { throughput: formatTokensPerSecond(reading) })} />
285
+ : t('stats.tokensPerSecond', { throughput: formatTokensPerSecond(measured) })} />
265
286
  }
266
287
 
267
288
  function TimePill({ stats, t, dialog, useChat, scale, dock }: {
@@ -278,9 +299,10 @@ function TimePill({ stats, t, dialog, useChat, scale, dock }: {
278
299
  }) {
279
300
  const { open, setOpen, rootRef, panelRef, pos } = useStatDialog(dialog)
280
301
  const counts = t('stats.counts', { turns: stats.turns, steps: stats.steps })
281
- // The pill's accessible name stays on the official figure: a screen reader
282
- // gets the durable session reading rather than a value that changes every
283
- // second while a step streams.
302
+ // The session figure now serves both jobs: the pill's accessible name (a
303
+ // screen reader gets the durable session reading rather than a value that
304
+ // changes every second while a step streams) and the visible figure whenever
305
+ // no step is streaming.
284
306
  const officialTps = stats.decodeMs > 0
285
307
  ? t('stats.tokensPerSecond', {
286
308
  throughput: formatTokensPerSecond(stats.decodeTokens / (stats.decodeMs / 1_000)),