dsh-token-panorama 0.0.0-stage → 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jiuwu02
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,444 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # Token Panorama for DSH
2
+
3
+ A DeepSeek Harness bundle that shows where every session's tokens go: one panel with lifetime totals, a
4
+ calendar heatmap, a trend chart, a per-model breakdown, a per-session usage table, and CSV/JSON export. Two
5
+ tool groups come with it: context clear/compact buttons, and a prompt optimization button.
6
+
7
+ Everything is read from the platform's own session persistence service, so nothing is scraped from logs and no
8
+ third-party dependency is required.
9
+
10
+ ## Features
11
+
12
+ - **Lifetime totals**: cumulative tokens, plus the input / output / cache-read / cache-write split and call count.
13
+ - **Cache hit rate**: cache reads as a share of all input-side tokens, with absolute hit and miss counts.
14
+ - **Cost estimate**: priced against DeepSeek's peak / off-peak bands, each band's amount shown separately.
15
+ - **Calendar heatmap**: GitHub-style week columns or one mini calendar per month; switch with the `Week` /
16
+ `Month` control.
17
+ - **Trend chart**: daily / weekly / monthly / yearly granularity, with a hover readout per point.
18
+ - **Model breakdown**: tokens and calls per model, sorted by volume.
19
+ - **Workspace breakdown**: grouped by the working directory each session header records, so you can see which
20
+ project costs the most.
21
+ - **Session usage tree**: main sessions only by default; opening one reveals the subagent sessions it owns, so
22
+ the card does not grow with the subagent count. Sortable by column, archived sessions marked and hideable,
23
+ and row figures follow the selected date range.
24
+ - **Paging**: 40 session groups per page, so a large history never renders every row at once.
25
+ - **Shared time range**: the filter bar is the one control every card reads (All / Today / Last 24 hours /
26
+ This week / Last 7 days / This month / Last 30 days, plus a pair of date inputs). The heatmap, trend, cost,
27
+ peak, model, session and workspace cards all recompute from the same span.
28
+ - **Hourly axis**: narrowing to Today or Last 24 hours switches the trend card to hours, filling in the quiet
29
+ hours so the line cannot cut across an idle stretch.
30
+ - **Export**: daily buckets as CSV (UTF-8 BOM + CRLF) or JSON, plus a separate per-session CSV with the
31
+ workspace, parent session, and cost columns.
32
+ - **Incremental refresh**: a per-session revision token decides what to re-read, so a refresh only decodes
33
+ sessions that actually changed.
34
+ - **Holiday auto-update**: any year is fetched at runtime and cached, so a new year needs no new build.
35
+ - **Context actions**: Clear (orange) and Compact (green) buttons in the dock under the composer, plus the
36
+ `/clear-context`, `/compact-context` and `/compact` commands. Compact runs mid-conversation: it fixes a cut
37
+ before summarizing, so everything written while the summarizer works stays verbatim behind the summary, and
38
+ the checkpoint reuses the platform's own "context compacted" marker with the item and token counts. A
39
+ `/compact` run folds the command row and the marker into one merged card, like the platform's own compaction.
40
+ Clear replaces the whole surface and therefore needs an idle session.
41
+ - **Prompt optimization**: an Optimize button in the input box toolbar sends the draft to a configurable
42
+ optimizer model and pastes the rewrite back into the composer; attachments are untouched. The request can
43
+ also carry an excerpt of the session's context, so a prompt that says "this function" rewrites into one that
44
+ names it. Instruction, route and the context toggle live on the **Prompt optimization** settings tab.
45
+ - **Bilingual**: Chinese and English follow the platform locale.
46
+
47
+ ## Requirements
48
+
49
+ - DSH `>= 0.2.1-rc.3` (declared as `dsh.engines.dsh`).
50
+ - A profile whose client runs the Web UI; the bundle registers into `sidebar.panellist` and `main`.
51
+
52
+ ## Install
53
+
54
+ Install from a local directory through the Plugins page or the plugin manager, pointing at the package
55
+ directory (the folder containing `package.json`):
56
+
57
+ ```
58
+ plugin_manager install_bundle target="<absolute path to this directory>"
59
+ ```
60
+
61
+ After installation the sidebar shows a **Token Panorama** entry; selecting it opens the main panel.
62
+
63
+ To remove it, uninstall the `dsh-token-panorama` bundle from the Plugins page.
64
+
65
+ ## npm package
66
+
67
+ The plugin is also published to the public npm registry as `dsh-token-panorama`. Storefronts
68
+ (the awesome-dsh-plugin catalog and the in-app market) can then install it by package name
69
+ and show a download figure, instead of falling back to the GitHub Release tarball.
70
+
71
+ - Publish a new version with `npm publish --access public`. npm requires either two-factor
72
+ authentication or a granular access token with Bypass 2FA enabled.
73
+ - The catalog reads the `name` field of the repository `package.json`, confirms that package on
74
+ the registry, and requires the npm package `repository` field to point back at this GitHub
75
+ repo. The mapping is discovered automatically; no field needs to be added to any listing.
76
+ - The download figure is the npm download count over a rolling 30-day UTC window
77
+ (`api.npmjs.org/downloads/point`), refreshed by the catalog nightly probe. It is not a
78
+ page-view or visit count: neither the catalog nor the market exposes any view metric.
79
+ GitHub Release asset downloads are a separate number and do not feed this field.
80
+ - Every change bumps `version` in `package.json`; publish that version too, so the registry
81
+ `latest` tag and the catalog `version` stay in step.
82
+
83
+ ## Usage
84
+
85
+ 1. Select **Token Panorama** in the sidebar.
86
+ 2. Choose a range from the filter bar, or type a `From` / `To` pair.
87
+ 3. Switch the heatmap between `Week` and `Month`, and the trend between `Hour` / `Day` / `Week` /
88
+ `Month` / `Year` (`Hour` appears only on a sub-day range).
89
+ 4. Hover any trend point for its exact numbers.
90
+ 5. The **Session usage** tree lists main sessions by default. Select the `▶` at the start of a row to open it,
91
+ revealing every subagent session it owns. Select a column header (Session / Tokens / Calls / Created) to
92
+ sort, clear `Show archived` to see only sessions still in use (the choice is remembered across visits), and
93
+ use `Export sessions CSV` to save what the table currently shows.
94
+ 6. Page through large histories with `Previous` / `Next`; each page holds 40 session groups.
95
+ 7. Use **Refresh** to re-scan, or **Export** to save the daily buckets as CSV / JSON.
96
+ 8. Use **General settings** to open the settings window and change the zone, currency, peak windows, holidays,
97
+ and price sheet; saving re-prices immediately.
98
+
99
+ The panel refreshes automatically when it opens; large histories load in about a second because only changed
100
+ sessions are decoded after the first pass.
101
+
102
+ ## Billing configuration
103
+
104
+ Every billing parameter is a plugin config entry. Open the settings window from the `General settings` button
105
+ in the panel header to edit them online; saving re-prices immediately, with no DSH restart.
106
+
107
+ The window edits the config by structure, not by field name: model prices are a table, peak schedules are
108
+ reusable profiles, and holidays are a checklist, none of which a generic key/value editor can express. It has
109
+ six tabs:
110
+
111
+ | Tab | Contents |
112
+ | --- | --- |
113
+ | **Models & prices** | One row per model with four independent price inputs (miss input, cache-hit input, cache write, output), plus the peak toggle with its schedule picker and the delete button pinned to the right edge |
114
+ | **Peak schedules** | Reusable profiles: name, automatic mode, any number of start–end windows, off-peak ratio |
115
+ | **Holidays** | Calendars shipped with the plugin; tick to enable |
116
+ | **Context** | The compaction instruction, its model route, the live context readout, and the automatic and segmented compaction switches |
117
+ | **Prompt optimization** | The optimizer instruction, its model route, and whether the request carries an excerpt of the session's context |
118
+ | **General** | Billing time zone and currency |
119
+
120
+ The window can **export** the billing configuration to JSON and **import** a shared one; an import only
121
+ fills the form, so nothing changes until Save is pressed.
122
+
123
+ The model list defaults to the models the logs actually contain, ordered by usage, so opening the window
124
+ puts a row in front of you for every model currently being charged. A model with no price row yet shows
125
+ "not priced" and an *add price* button.
126
+
127
+ Every model is priced on its own. A model missing from the sheet falls back to `defaultPrice`, and the cost
128
+ card names it ("kimi-k3.1-pro is not priced; charging the default") instead of quietly billing zero.
129
+
130
+ Four rates, billed separately. A provider does not treat every input token alike, so the sheet has one
131
+ column per thing it charges for:
132
+
133
+ | Column | What it bills | When the provider does not charge for it |
134
+ | --- | --- | --- |
135
+ | Miss input | Input sent for the first time, with no prefix-cache hit | The listed rate |
136
+ | Cache-hit input | Input served from the prefix cache | 0 |
137
+ | Cache write | Input stored into the cache | 0 |
138
+ | Output | What the model generated | The listed rate |
139
+
140
+ Keeping the two cache columns apart matters: a write stores input and a read fetches it back, and their rates
141
+ commonly differ by more than ten times. Collapse them into one number and a session warming a cache prices
142
+ too cheaply while a session reusing one prices too expensively. A cache write is also excluded from the
143
+ hit-rate denominator: it is a write, not a read.
144
+
145
+ Peak pricing is a separate, reusable item. One schedule can back several models: switch the model's toggle on
146
+ and pick the schedule name. A model with the toggle off bills one rate all day, the full listed price, so it
147
+ counts in the peak column rather than the discounted one. Renaming a schedule renames every model's reference
148
+ with it; deleting one leaves those references behind, and a model pointing at a missing schedule bills flat
149
+ until it is re-pointed (the row says so and offers the picker).
150
+
151
+ Off-peak is a ratio, not a second price. The schedule's off-peak ratio is a share of the listed price (0.5 by
152
+ default), so there is one price sheet to maintain and changing the ratio re-prices both bands at once.
153
+
154
+ ### Automatic modes
155
+
156
+ The mode decides which days a schedule applies to, following the shape of an alarm's repeat rule:
157
+
158
+ | Mode | Meaning |
159
+ | --- | --- |
160
+ | Every day | No weekday or holiday filter |
161
+ | Statutory workdays (skips holidays) | Monday to Friday, excluding any enabled holiday |
162
+ | Statutory holidays (skips workdays) | Only on enabled holidays |
163
+ | Monday to Friday | Weekday only; ignore holidays |
164
+ | Custom | Any combination of the seven weekdays |
165
+
166
+ ### Time windows
167
+
168
+ A schedule holds any number of start–end windows, each entered on a 24-hour clock. **More than one window is
169
+ necessary**: DeepSeek's peak band is split around the lunch break (09:00-12:00 and 14:00-18:00), and collapsing
170
+ it into one 09:00-18:00 span bills the break at the peak rate: peak tokens would read ~450M instead of ~404M.
171
+
172
+ An `end` earlier than its `start` wraps midnight, so `22:00`-`02:00` is one entry.
173
+
174
+ ### Holidays
175
+
176
+ Holiday dates are fetched over the network at runtime and cached under the profile, so a new year works without
177
+ shipping a new plugin build. Only years that have no data yet are fetched: a published year never changes again,
178
+ while a year the sources have not published (for example the next one) is retried once a day. Three keyless
179
+ sources are tried in order, and a year that every source rejects keeps whatever was already cached.
180
+
181
+ The plugin still starts with the 2026 dates built in, so pricing is correct on the very first run, before any
182
+ request leaves the process. Settings shows how many years are loaded and has a refresh button that forces a
183
+ re-fetch. The statutory-holiday calendar is on by default.
184
+
185
+ The bundled list is seed data, not the source of truth: once a year is fetched its network copy wins.
186
+
187
+ ### Context actions
188
+
189
+ Two buttons sit in the dock under the composer, in the same row as the context-usage meter they act on:
190
+ **Clear context** (orange) on the left and **Compact context** (green) on the right. Both are also slash
191
+ commands (`/clear-context` and `/compact-context`), so they work without the mouse.
192
+
193
+ They differ in what they replace. Clear takes the whole surface and therefore needs an idle session: a step
194
+ appending during the rewrite would be dropped with the history it belongs to. Compact takes only the span it
195
+ selected *before* summarizing, up to the last cut that does not split a tool-call/result pair, so it works
196
+ mid-conversation, and everything written while the summarizer runs is replayed verbatim behind the checkpoint.
197
+ The session and its log are untouched either way: the events stay on disk, the surface only stops showing the
198
+ replaced part, and the context window is freed.
199
+
200
+ - **Clear**: writes a one-line placeholder. It is not a summary; nothing before the next message is retained.
201
+ - **Compact**: calls the model directly to write a summary, then replaces the cut with it. It runs on the
202
+ session's provider and model unless the Context tab overrides them. The span is replayed as the messages it
203
+ really holds (same system prompt, same tool definitions, instruction appended last), so the provider's
204
+ prefix cache covers the history and the call bills as a cache read instead of a second full send. No
205
+ subagent is started, so the summarizer has no session, no tools of its own, and nothing it could edit.
206
+
207
+ After either one, the transcript shows one command card, with no duplicate note under the buttons. The two
208
+ report different things: Clear gives the item and token counts before and after the swap, Compact adds the
209
+ character count of the summary it wrote. They ride the platform's own `command/run` / `command/done` events,
210
+ so the card is visible and expandable but never enters the surface: the model's context stays free of its own
211
+ bookkeeping.
212
+
213
+ The prompt the compaction model reads is editable on the **Context** settings tab, which also picks the
214
+ model's provider and model; leaving those empty inherits the session's. The default prompt asks for the goal,
215
+ decisions and their reasons, verbatim paths and signatures, error text, and open work, and forbids mentioning
216
+ the compaction itself.
217
+
218
+ The same tab carries three more things about how much window is left:
219
+
220
+ - **Context usage** is the live total for the current session, built from the real system prompt, tool
221
+ definitions, and conversation messages, next to the selected model's input window, the share in use, and
222
+ the three-part split.
223
+ - The **automatic compaction** switch and its **trigger threshold** (75% by default) decide when this plugin
224
+ compacts on its own, before the window is full. The trigger rides the same `agent/pre-step` event the platform
225
+ engine uses, but this plugin runs it and writes no platform config. A profile may disable that engine outright
226
+ (billion-context disables `dsh-compaction-basic` as a whole, and a preset's nested row is not reachable
227
+ through the config editor anyway), and then a saved switch would enforce nothing. The decision is made on the
228
+ three-part real total described above, not the usage-anchored figure the platform panel prints at the top: on
229
+ a long session that one reads 112K while the same request really carries 2.3M. Two compactions of one
230
+ session are at least a minute apart, so a history still over the threshold right after being summarized does
231
+ not pay for another summary on the very next step.
232
+ - **Segment when over the window** decides what happens once the history is larger than the summarizer's
233
+ window. Off: the oldest part is dropped, only the newest records that fit are summarized, and the excerpt
234
+ says how many were omitted. On: the span is cut into segments, each summarized in parallel, and a final
235
+ request merges them into the one summary that replaces the span. Nothing is dropped, and no single request
236
+ exceeds the window; a long conversation that used to hang or come back empty was running into exactly that
237
+ request.
238
+ - Compaction measures first. A history past two thirds of the summarizer's window falls back to a head-and-tail
239
+ excerpt when segmenting is off: the head keeps the goal and the constraints, the tail keeps whatever is in
240
+ progress, and the omitted middle is marked explicitly. A span that reaches into the running turn is no
241
+ different: the summarizer is handed the messages themselves, so the summary covers exactly what the
242
+ checkpoint will replace.
243
+ - The compaction lock is opened before the summarizer is called, not after. A second request then answers
244
+ "already in flight" instead of paying for the same summary twice, and the platform's own engine backs off
245
+ rather than racing this one.
246
+
247
+ Clearing an already-cleared session is a no-op that reports success: rewriting one placeholder with another
248
+ identical one is not an error.
249
+
250
+ ### Validation
251
+
252
+ The platform schema and the plugin each validate before a save lands: an unknown zone, a malformed `HH:MM`, a
253
+ negative price, an off-peak ratio outside (0, 1], or an unknown automatic mode is rejected with the offending
254
+ field named.
255
+
256
+ ### Upgrading
257
+
258
+ A 1.0 configuration (one flat `peakWindows` list, six-field prices named `hitPeak` / `missPeak` / `outPeak`,
259
+ holidays as a date array) is migrated on read: the peak half becomes the listed price, a model that had an
260
+ off-peak rate is attached to the `standard` schedule, and the date array folds back into calendar keys. The
261
+ migration is read-only, so the file on disk keeps its old shape until you save from the window.
262
+
263
+ Every string in the window lives in the `ZH` / `EN` dictionaries in `client.js`, 230 keys with identical key
264
+ sets across both languages.
265
+
266
+ Saving goes through the platform's `configEditor` service, which validates the schema, writes the whole config
267
+ into the profile's `cordis.patch.yml`, and triggers a hot reload. The plugin validates once more first, so a bad
268
+ value comes back as a message naming the offending field instead of a Loader-level failure. When the profile has
269
+ no such service or no matching row, the window degrades to read-only and says why.
270
+
271
+ - **Holidays win over weekdays**: a listed day bills off-peak all day whatever weekday it falls on.
272
+ - **Validation runs at load**: an unknown zone, a malformed `HH:MM`, an impossible date, or a negative price
273
+ is rejected with a message naming the offending field.
274
+
275
+ > DeepSeek's peak rule reads "Monday through Friday", so make-up working days (weekends worked for a holiday
276
+ > swap) still bill off-peak; the panel does not treat them as peak. Prices are per million tokens, matching the
277
+ > provider's published sheet.
278
+
279
+ ## How the data is collected
280
+
281
+ - **Source**: the Host `sessionPersistence` service lists every session with an opaque `revision`. Sessions
282
+ whose revision is unchanged are skipped. For the rest, the log's Zstandard frames are walked without
283
+ inflating them, so only the frames appended since the last pass are decoded — a pass over an
284
+ unchanged corpus decodes nothing at all, and one that has to re-read a session decodes its tail
285
+ rather than its whole history.
286
+ - **Counting**: each `assistant/message` event carrying `data.usage` contributes one call. The total prefers
287
+ `usage.totalTokens`, falling back to `inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens`.
288
+ - **Scope**: every session in the profile, including subagent sessions (`header.origin === 'subagent'`) and
289
+ archived sessions; the footer counts main, subagent, and archived separately.
290
+ - **Session titles**: taken from the last `session/title` event in each session's own log, never from mutable
291
+ metadata.
292
+ - **Archived flag**: read from `workspaceRegistry.archivedSessionIds`; when that service is absent the plugin
293
+ treats the profile as having no archived sessions and every other figure stays correct.
294
+ - **Workspace**: taken from `cwd` in each session's header event (`type: "session"`); a session without one is
295
+ grouped under "(no workspace)".
296
+ - **Lineage**: taken from `parentSession` in the same header event. A subagent whose parent is outside this
297
+ report is shown as a top-level entry rather than dropped.
298
+ - **Buckets**: usage is keyed by the local calendar day of the event timestamp; billing additionally splits by
299
+ hour, converted through the configured zone.
300
+ - **Cache**: the folded result is written to `~/.dsh/storages/dsh-token-panorama/usage-cache.json`
301
+ (`CACHE_VERSION = 5`), written atomically via a temp file and rename. Each record also carries the
302
+ byte offset and the highest sequence number it has folded, which is what lets the next pass resume.
303
+ - **Projection seed**: when the Host's `sessionProjections` service is present, the plugin registers its
304
+ own fold (`tokenPanoramaUsage`) so the Host keeps it current as events are committed. A session whose
305
+ projection row is already at the log's end is adopted without reading the log at all. The row is used
306
+ only after its state version, zone, watermark and log identity all agree; anything else falls back to
307
+ reading the log, so the seed can save work but never change an answer. Without the service the plugin
308
+ works as before. An unreadable log is remembered by revision for ten minutes instead of being decoded
309
+ again on every pass.
310
+ - **Accuracy**: totals are exactly the sum of the underlying `usage` records; the panel never estimates. Session
311
+ rows follow the selected range, their sum over any day equals that day's heatmap figure exactly, and the
312
+ hourly cost is rolled into the day, model, and session dimensions, whose sums all equal the grand total.
313
+
314
+ ## HTTP API
315
+
316
+ The Host registers a prefix route at `/token-stats/api`. Every request must pass the platform's own
317
+ Host/Origin fence (`ctx.connection.requestRejection`) and be `POST` with a JSON body.
318
+
319
+ | Method | Body | Purpose |
320
+ | ------------ | ------------------- | --------------------------------------------------------------- |
321
+ | `summary` | `{}` | Return the current snapshot, re-scanning only changed sessions. |
322
+ | `summary` | `{"refresh": true}` | Distrust this plugin's folds and rebuild them, still seeding from any Host projection row that proves itself. |
323
+ | `refresh` | `{}` | Alias for a forced refresh. |
324
+ | `config` | `{}` | Return the live config, its field list, and editability. `models` carries the Host's live model catalog (each entry with its context window). |
325
+ | `saveConfig` | `{"value": {...}}` | Validate and persist a config; returns it with the field list. Automatic compaction runs in this plugin and writes no platform config, so there is no "did the mirror land" field. |
326
+ | `holidays` | `{}` | Force a re-fetch; returns whether it changed and the per-year list. |
327
+ | `clearContext` | `{"sessionId": "..."}` | Replace that session's whole surface with a placeholder. |
328
+ | `compactContext` | `{"sessionId": "..."}` | Summarize that session with a direct model call and replace the surface. |
329
+ | `contextPressure` | `{"sessionId": "..."}` | Measure one session: total tokens, the system/tools/messages split, the routed model's context window, and the share in use. |
330
+
331
+ Response shape:
332
+
333
+ ```json
334
+ {
335
+ "ok": true,
336
+ "value": {
337
+ "generatedAt": 1793511000000,
338
+ "lastScanAt": 1793510999000,
339
+ "scan": null,
340
+ "sessions": { "total": 0, "subagent": 0, "main": 0, "archived": 0 },
341
+ "totals": {
342
+ "input": 0,
343
+ "output": 0,
344
+ "cacheRead": 0,
345
+ "cacheWrite": 0,
346
+ "total": 0,
347
+ "calls": 0
348
+ },
349
+ "days": [["2026-11-01", 0, 0, 0, 0, 0, 0, 0]],
350
+ "models": [["deepseek-v4.1-flash", 0, 0, 0]],
351
+ "cache": { "hits": 0, "misses": 0, "hitRate": 0 },
352
+ "billing": {
353
+ "currency": "CNY",
354
+ "timeZone": "Asia/Shanghai",
355
+ "cost": 0,
356
+ "peakCost": 0,
357
+ "offPeakCost": 0,
358
+ "peakTokens": 0,
359
+ "offPeakTokens": 0,
360
+ "peakCalls": 0,
361
+ "offPeakCalls": 0,
362
+ "hitRate": 0,
363
+ "hits": 0,
364
+ "misses": 0,
365
+ "priced": ["deepseek-v4.1-flash"],
366
+ "unpriced": []
367
+ },
368
+ "sessionRows": [],
369
+ "firstDay": "2026-10-30",
370
+ "lastDay": "2026-11-01"
371
+ }
372
+ }
373
+ ```
374
+
375
+ `days`, `hours` and `models` rows are `[key, input, output, cacheRead, cacheWrite, total, calls, cost,
376
+ peakInput, peakOutput, peakCacheRead, peakCacheWrite, peakTotal, peakCalls, peakCost]`. `sessionRows`
377
+ rows are `[sessionId, title, createdAt, isSubagent(0|1), isArchived(0|1), total, calls, parentId,
378
+ dayRows, cost, workspace, slotRows, models]`, where `dayRows` is `[day, …]` and `slotRows` is
379
+ `[hour, model, input, output, cacheRead, cacheWrite, total, calls, cost, isPeak(0|1)]`. Archive state
380
+ comes from `workspaceRegistry.archivedSessionIds`; archived sessions keep their logs on disk, so they
381
+ also appear in `sessionPersistence.list()`.
382
+
383
+ Slots are the finest fact the Host has, so the Client can narrow a session to a range and a model from the
384
+ row alone. The peak flag travels with them because only the Host knows the schedule and the holiday calendar,
385
+ and a second copy of those rules in the Client would eventually disagree with it.
386
+
387
+ Three extra per-model fields let the Client narrow the whole page to one model without a second request:
388
+
389
+ - `modelDays`: `[model, dayRows]`, used to rebuild that model's trend and heatmap over any range.
390
+ - `modelHours`: `[model, hourRows]`, the hour-grain twin of `modelDays`.
391
+ - `modelBilling`: `[model, {cost, peakCost, offPeakCost, peakTokens, offPeakTokens, peakCalls,
392
+ offPeakCalls, hits, misses}]`, used to rebuild its cost, peak/off-peak and hit-rate figures.
393
+
394
+ Each family sums back to the global `totals` and `billing` exactly.
395
+
396
+ ## Package layout
397
+
398
+ ```
399
+ package.json manifest: exports, dsh.bundle.patch, dsh.client, engines
400
+ cordis.patch.yml inserts this bundle into the profile patch
401
+ index.js Host entry: Config validation, HTTP route, plugin lifecycle
402
+ lib/time.js Zone math, hour bucketing, numeric normalisation
403
+ lib/scan.js Folding session events into hour × model slots
404
+ lib/schema.js Config schema, default price sheet, field descriptions
405
+ lib/billing.js Peak rules, pricing, legacy-config migrations
406
+ lib/holidays.js Network fetch, per-year cache, built-in seed
407
+ lib/contextops.js Surface replacement for clear / compact, the real three-part measurement, and segmentation
408
+ lib/store.js Revision-keyed cache, the frame-level scan, and the snapshot (per day / per model)
409
+ lib/frames.js Walking a stored log's Zstandard frames to decode only what is new
410
+ lib/projection.js The tokenPanoramaUsage projection unit the Host keeps current
411
+ lib/http.js Request body reading and JSON responses
412
+ client.js Client half: panel, heatmap, trend chart, session tree, workspaces, export
413
+ locale/en.json plugin metadata (English)
414
+ locale/zh.json plugin metadata (Chinese)
415
+ icon.svg plugin icon (reachable via the "./icon" export)
416
+ icon-prompt-optimize.svg Optimize button icon (the same graphic is inlined in client.js)
417
+ ```
418
+
419
+ UI strings live in `client.js` (`ZH` / `EN`) and register through `ctx.locale.register`; the `locale/*.json`
420
+ files only carry the plugin title and description shown by the Plugins page.
421
+
422
+ `schemastery` is resolved at runtime from the profile directory (see `schemaLibrary()`) and is deliberately
423
+ **absent from `package.json` `dependencies`**: this bundle installs as a `link:`, so its own dependencies are
424
+ never installed, and declaring one would only make the module fail to load.
425
+
426
+ ## Notes and limits
427
+
428
+ - The heatmap, trend, and billing use the configured `timeZone` (default `Asia/Shanghai`, matching how DeepSeek
429
+ bills), not the machine's local zone.
430
+ - A very long history renders one heatmap column per week; the strip scrolls horizontally rather than shrinking
431
+ below legibility.
432
+ - Usage is attributed to models exactly as recorded; sessions predating usage reporting contribute nothing.
433
+ - A model absent from the price sheet bills at `defaultPrice`, and the cost card names it rather than pricing it
434
+ silently.
435
+ - Make-up working days bill off-peak per the rule's literal wording; switch a schedule to Custom and tick those
436
+ weekdays to count them as peak.
437
+ - The bundle reads session persistence and the archive list, registers one HTTP prefix route, and exposes one
438
+ Config schema.
439
+ - The context actions rewrite the session surface (the `compaction/*` pair plus one surface replacement), so they
440
+ need four platform services: `commands`, `tokenMeter`, `agents`, and `llm`. On a profile missing any of them
441
+ those two buttons and commands are unavailable there; the statistics are unaffected. Replaced events are never
442
+ deleted from the log.
443
+ - A compaction cannot rewrite until the model has finished writing, so one run takes as long as the session
444
+ length and chosen model require. The session stays usable throughout: the rewrite waits for it to go idle.