dsh-token-panorama 0.0.0-stage → 1.0.0

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