Three per-event costs leave the fold: every state-changing event shallow-copied
all seven retained collections, every event re-scanned the retained requests to
count turn runs, and every tool call stringified its arguments even when the
tool can never row a file op. Cloning now follows the branch's own mutation
list, the run count rides an incremental ledger (recomputed only where a trim
replaces the array), and raw arguments are kept only where they are read.
Measured over 279 real sessions (282,783 events): the fold drops 16.8% corpus
wide, ~51% on V3-shaped logs and ~57% on sessions at the retention caps, with
state output field-identical apart from the new optional `turnRuns` field and
the intended `argsRaw` omission. `stateVersion` is unchanged, so cached rows
keep parsing and stay usable.
The Windows check rode along: `path.relative` returns an ABSOLUTE path across
drives, so the old `..`-prefix test read a foreign drive's module as a child of
the plugin's own tree and degraded version detection; `!isAbsolute(rel)` closes
it.
Review follow-up on top of the original six-file change:
- an absent `timing` slot stays absent. The selective clone materialized
`timing: undefined`, which is not losslessly JSON-serializable, so
`snapshotJsonValue` rejected the whole checkpoint: every projection-cache
write for that session was skipped, and a log ending on an unpaired
`tool/result` never got a cache row at all — every reopen then paid the full
cold fold this change set aimed to cut;
- the no-`undefined`-member rule is now pinned on every intermediate fold
state, not only the last one — the gap that let the above ride through a
green suite, since reading the slot cannot tell an absent key from an
`undefined`-valued one;
- the "which tool needs its raw arguments" gate moved beside the file-op
parser (`opBearingTool` / `rawArgsNeeded`), so the fold no longer restates
the tool list.
Verified: `pnpm run lint:fix && pnpm run test && pnpm run build` green (1763
tests, per-file coverage 100%), plus a field-level diff of the fold over
282,783 real events (no divergence beyond the intended deltas), the
incremental ledger cross-checked against a full scan on every event including
both trims, and a frozen-predecessor run proving no in-place write.
Co-authored-by: Nwflower <1679659@qq.com>
Add the V4 baseline (session format 4, cordis 4.0.3): the fold's switched
event families are unchanged, so its entry rides the V3 seam face with a
per-generation settings surface — the `register` face and the retired
`settings.plugin.item` slot are asserted BOTH ways (present through V3,
absent on V4+), and the modal dock probe now pins the sidebar-leading
grid track the measure actually reads instead of one generation's exact
template. New ICON_SEAMS probe: every icon the client renders must exist
in at least one spelling per tag, so a platform rename fails the matrix
instead of the browser. Declare the release in the manifest, fetch its
tag in the release workflow, and document the V4 generation with its
known deltas (deferred: the preferences card's Plugins-page migration,
image-offload accounting).
dsh 0.1.7 renamed every `Icon*Outline16`/`Icon*Outline14` to
`Icon*OutlineRegular`/`Icon*OutlineMedium`, so the plugin's named imports
arrived as `undefined` at runtime and the Context tab crashed with React
error #130. Each icon now resolves through BOTH spellings at module load —
modern first, legacy fallback — with a render-nothing component and a
bounded property-read catch so any future rename degrades to a missing
glyph instead of a crashed tab.
V4 lifted `isError` from the tool-result content wrapper onto the message
and made the event-level `data.error` identity optional, so a failing
result could lose its error flag. The fold now reads the mark from BOTH
spellings — surface nodes and file-op rows keep it on every generation.
dsh 0.1.7 (session format V4) removed `settings.register` — configuration
forms derive from each loader entry's own Config schema there. The inject
callback now probes the method and stays inert on a generation without it
instead of throwing inside the fiber.
The Plugin / GitHub / Settings labels open with siDeepseek, siGithub,
and the harness's IconSettingsOutline14 gear, riding currentColor at the
label's baseline. simple-icons is a dev-only dependency pinned to the
harness ui-primitives' own 16.31.0 and tree-shakes into lib/client.js as
exactly the two mark paths, so no runtime dependency is added and the
marks are identical to the harness's own SiteGlyph artwork.
The context stats card takes three flex parts and the plugin card one,
with the plugin card's floor lowered to 240px so the ratio shows at
common widths (its rows ellipsize and recover values on hover). Below
the 360 + 240px floors the pair wraps onto full-width lines as before.
A dashboard reopen landing inside the balance read's window joined the
in-flight route read but never registered its landing callback — the
read reported only to the open that started it, so the reopened
dashboard showed the stale remembered figure for its whole session
(issue #82, exactly as reported). The landing callbacks now collect in
a set and all fire when the read lands; the previous behavior was even
pinned by a test asserting the joiner stayed silent, which now asserts
the opposite. The store-level join test and a capsule-level
close-reopen repro pin the regression both ways.
The timing card gains a top-right tok/s chip, computed over the same
pairing as the harness's own session-stats fold: per assistant call,
the provider-reported output tokens paired with the first-token →
message window, summed only over calls carrying BOTH — so the plugin's
figure equals the harness pill's by construction (verified against the
running harness). The formatter mirrors the harness's too: whole tokens
from ten up, one decimal below.
The seat rides TimingTotals as additive-optional fields (speedTokens /
speedMs — a usage-gated subset of genMs), so cached rows written before
it simply render no chip, with no stateVersion bump. Unreadable or
absent pairing renders no chip; the tooltip names the pairing.
The range selector gains 近 24 小时 (Last 24 hours) ahead of 7d — the
finest window the dashboard offered was a week, too coarse for the
hour-scale sessions a working day produces. The window filters by the
rows' last activity exactly like the other ranges; the heatmap keeps
its own fixed 8-week view.
The tooltip repeated the pill's own total, and a zero side (granted or
topped-up) rode the bubble as a meaningless line. The pill keeps
carrying the total; the tooltip now lists only the non-zero parts, and
an all-zero account renders the pill bare — no bubble, nothing to break
down. The unused balance.tip.total key drops from both dictionaries.
The capsule's figure could come from two stale layers, and its total could
disagree with its own breakdown. Both are gone.
The host route remembered a successful read for five minutes, so opening the
console first pinned the capsule to that older snapshot — and the balance
moves with every billed request, so the two could not agree. Nothing outlives
a read now beyond the outbound fetch already in flight.
The client paints the remembered figure (memory, then storage) while it
revalidates in the background, so a reopening panel shows a number at once and
settles on the live one without ever trading it for a blank pill: a failed or
absent read notifies nobody and keeps serving what it remembered.
The total is now the granted and topped-up parts summed rather than the
platform's own `total_balance`, which rounds independently of them and can
land a cent away from what the breakdown beside it adds up to (the viewer's
arithmetic is the authority); an entry no longer needs `total_balance` to
prove its shape at all.
The specs cover the storage and transport failures that must keep the figure,
the per-open revalidation, and the disagreeing total.
Replace the native `title` on the balance pill — browser-throttled to about
a second — with the harness Tooltip primitive, the same zero-delay seam the
heatmap cells and the assistant jump action already use. The multi-line
breakdown keeps its shape through the bubble's `pre-line`, and an
`aria-label` carries the same text for assistive tech.
The spec drives the bubble through real hover events instead of reading
`title`, and covers the leave that drops it.
The Context Dashboard's balance pill becomes a link to
platform.deepseek.com/usage — the console page behind the figure it
shows — opened in a new tab with noreferrer, the same external-link
shape the Plugin Info rows use. Hover underlines the figure as the
click affordance; the read, degradation, and tooltip behavior are
unchanged.
The dashboard header gains a capsule with the DeepSeek open-platform
balance (issue #80). The host mounts a /api/dsh-context/balance route
through the same Connection exact-route registry as the detail and
backfill routes, resolves the connection facts per request the way
llm-deepseek serves its own requests (its llm-deepseek settings section
for the credential ref and endpoint override, the credentials service
for the key), and reads /user/balance behind a short TTL cache — the
key never leaves the host process.
Every face is optional and runtime-proved: a deployment without the
DeepSeek provider, without a resolvable credential, without the route
registry, or whose platform read fails (network, non-2xx, malformed
payload, hostile entries dropped whole) answers a typed null, and the
capsule renders nothing — no pill, no spinner, no error state. The
entry follows the display locale (zh → CNY) with the account's first
currency as fallback, and the tooltip carries the granted/topped-up
breakdown.
The persistence seam translates the format edge's
SessionFormatUnsupportedMigrationError into SessionFormatUnsupportedError
(with the "source vN artifact remains unchanged" suffix) before the error
escapes, so the exact-name check never matched on any supported dsh
baseline: the skip summary never fired and every refused legacy session
kept its own WARN (issue #75). Classify by the SessionFormatUnsupported
prefix instead, and cover both faces of the translation in the fixture.
The models.dev deepseek branch publishes the official OFF-PEAK rates
(deepseek-flash: 0.15/0.6/0.003 USD per 1M = api-docs.deepseek.com's
valley tier), but the estimator read the book as the peak list and then
halved the off bucket — every DeepSeek session under-estimated ~2x in
both periods. Flip the semantics: DeepSeek's peak bucket prices at
twice the book rate and the off bucket at book; the tooltip's peak
column doubles for DeepSeek rows with its structure untouched, so
deepseek-flash now reproduces the official peak|off-peak list exactly.
Fixes#78
The jump's landing used to reset the scrollport to the top, where the
stats cards hide the composition card and the pinned turn's trend chart
below the fold. The landing now reveals the Current Composition card
(`data-lc-current`) so the card and the trend chart share the
scrollport.
The reveal walks from the anchor to the nearest scrollable ancestor —
host-agnostic by need: the conversation tab scrolls the shared
`[data-conversation-scroll]` container while the right Sidebar scrolls
dockkit's pane body, a CSS-module class with no stable attribute of its
own. The rect delta lands the anchor flush regardless of the
container's current position, overriding the scroll ledger's restore;
repeat jumps re-reveal. No scrollable ancestor (or a hostile chain)
degrades to a quiet no-op.
The jump's seq resolution ran inside the click handler, but the
assistant-actions seat's `useChat` is a real React hook — calling it
off-render throws the dispatcher guard (React #321), which the seat
guard swallowed, so no focus was ever recorded and the landing view
stayed on its default selection. The button now resolves the reply's
seq at RENDER time (unconditional seat read, click consumes the
captured value), so the pin finally fires in production.
The relay also wakes mounted views now: the sidebar landing keeps the
Context panel mounted across jumps, so repeat clicks re-pin it instead
of recording a focus nothing consumes (the conversation-tab landing had
the same gap whenever the tab was already active).
The assistant-action jump now opens the Context tab on the right Sidebar
over the guarded `ctx.sidebarRight.openTab` optional seam (the column
expands in the same step, keeping the clicked reply in view) instead of
switching the conversation tab. Every harness or placement the seam
cannot serve — lines older than 0.1.5-rc.1, tab-only placement, no
mounted session surface, a hostile face — keeps the conversation-tab
activation as fallback. The tooltip is reworded neutrally ("Context
view") since the landing follows what the harness serves.
The contextActivity/contextTimeline warm-up used to cold-read the whole
session corpus at every startup, whether the dashboard was ever opened or
not — and a legacy log the running harness refuses to migrate re-attempted
and re-warned per session on every boot (one reporter measured 115 warn
lines, ~60 KB per start; #75). Under that sustained read pressure the host
also stalls its own client handshake, leaving the workspace sidebar to
mount without its session rows until a restart.
The pass now runs on demand, once per host process: the client POSTs the
plugin's /api/dsh-context/backfill fetch route (the same authenticated
/api fence as the detail route) the first time the Context Insights panel
opens, and every later hit is a no-op. The route and the cold-path faces
compose in either order; a deployment without the connection service or
the exact-route registry never arms the pass — such a client could not
open the dashboard to read the rows anyway.
Skips are accounted instead of spammed: a migration-refused log
(permanent, source-side) drops to a per-session debug line and joins one
summary info line per pass; unexpected per-session failures keep their
own warn, now proof against hostile error objects whose toString throws.
The inter-session yield widens to 100 ms so an on-demand pass never
squeezes the host's other services.
The green pill on the sidebar-foot entry read as a notification badge
(#74): a running-task count is not something a footer control should
surface as an attention marker. The entry returns to a plain icon +
label; the count's only consumer was the badge, so the tally helper and
the button's useSessions seat plumbing go with it.
Fixes#74.
The cost cell's sub-line now counts the sessions whose spend the price
book could estimate (costSessions), and the cache-hit cell's the
sessions whose usage feeds the rate (usageSessions) — a session with
usage but no book rates feeds the rate while pricing to nothing, and a
session without usage counts toward neither figure.
The cost and cache-hit subtitles now qualify with the session count they
aggregate over (a shared ov.kpi.sessionsSub key) instead of long
explanations that overflowed the narrow KPI cards; the heatmap card's
hint reads 'Filter by date'.
Each column is labeled when a month begins inside it: the month changes
between its Monday and Sunday, or its Monday is the 1st; the label names
the Sunday's month. Months that began before the window stay unlabeled.
New insightsEntry field ('show' | 'hide', default 'show') in the
dsh-context settings namespace, edited from the Plugin configuration
card. The sidebar entry subscribes to the store and renders null while
hidden; the seat stays registered so flipping back needs no remount.
Fails open everywhere: a schema-less host, an unserved namespace, a
section without the field, a stale persisted value (loose union), or a
write that never persists all resolve to the visible entry.
The sessions/KPI columns' own 8px bar rendered flush against the card
edges, riding on them while scrolling, and a column gaining or losing
the bar reflowed its content by the bar's width. Reserve the track
(scrollbar-gutter: stable) and pad the columns so the thumb stays 8px
clear of the cards; the folded single-column layout resets the padding.
- Page the session grid at 12 cards per page: a filter change re-anchors
the pager at the first page, and clamping keeps a shrunken list's view
in range instead of showing a blank page.
- Redesign the session cards: 3 columns, a project pill, a first-active-
day line under the relative time, wrap-clamped titles with uniform row
heights, a turns-and-steps lead figure, and the cost label shortened
to 费用/Cost.
- Restack the insight band as one folding row — the KPI 2x2 block beside
the activity heatmap — and drop the aggregate composition card with
its now-unused fold, keys, and styles.
useEscapeClose resubscribed on every render — callers pass a fresh
closure each time — and each resubscribe's cleanup re-ran the pre-open
focus restore, yanking focus out of the overlay's own inputs on every
keystroke. Hold the callback in a latest-ref so the subscription and
its restore live on the active transitions alone.
The donut center shows the total alone — the overflowing
"tokens (estimated)" moves to the card's subtitle as plain
"tokens" — and the sort buttons follow the workspace browser's
vocabulary: Last updated / Total Tokens / Current Context.
The card pins to the 252px its fixed grid needs — about half the
previous share — and the composition card takes the rest; wrapped
below the row's fold, the heat card stretches again.
The raw session list carries every session while the workspace browser
hides its archive set, so the dashboard surfaced archived ghosts and
read as duplicates and stale rows. rowsOfSnapshot now drops rows present
in the workspaces snapshot's archivedSessionIds, failing open — an
absent seat or a hostile shape archives nothing.
Default the window to the last 8 weeks, draw fixed-size cells centered
in the card (the block's height now follows the seven cell rows instead
of stretching to the card's width), floor data-less days with a visible
gray, and rebalance the chart row to heat:composition 2:3 so the
composition legend flows into multiple columns.
A cross-session insight surface: the sidebar-foot entry (stacked above
Settings by the harness's own foot layout, with a running-sessions badge)
opens a frame-wide overlay through the shared module store. The panel reads
the root-scope useSessions/useWorkspaces standard kit — every list row's
host-cached projection values — so no session log needs opening:
- KPI band over the picked range (7d / 30d / all): active sessions, billed
tokens, estimated cost (models.dev book), cache-hit rate.
- Activity heatmap (GitHub-style, 12 weeks, square cells filling the card):
a day click pins the session grid to that day.
- Aggregate composition donut over the range's sessions.
- Session cards with a mini composition ring, billed/turns/cost figures and
a workspace-group / project breadcrumb (deduped when both names match);
grid filters by range, pinned day, group chips (incl. Ungrouped), and
search; sorts by recency, tokens, or context size; a card click opens the
session through the harness's own sessions.open and closes the panel.
Subagent-origin rows are excluded, mirroring the workspace browser's own
predicate (plain open() cannot address them). Every row is sanitized in
isolation — a hostile row degrades to a metadata-only card, never an error.
The Context Dashboard's heatmap and session cards read the session list's
projection column, which only serves durable cache rows. The new
contextActivity unit folds each assistant settlement into a per-day ledger
(billed tokens + completed requests, keyed by local day, 400-day retention)
alongside the existing contextTimeline/contextHeaders units.
A session folded before a unit existed — or one that predates the plugin
entirely — has no row until it next goes live, so a one-pass warm-up
(backfill.ts) cold-reads every such session's durable log through the
projection cache's own cold-read ladder (coldSnapshot): each unit seeds
from its cached rows, folds the remainder, and the refreshed checkpoint is
written back. Live sessions are skipped (a throwing liveness probe skips
conservatively too), unreadable logs are logged and skipped per session,
and the whole pass rides a deferred inject across the
sessionQuery/sessionProjectionCache/sessionPersistence/sessions services.
The Context card's cost cell now prices the current agent TOGETHER with
every subagent session's cumulative billed totals, and a new subagent-cost
cell prices the subagents' share alone; both hover bubbles explain their
scope, the merged one listing the per-model rates of every billed side.
The subagents' usage folds out of the session-list snapshot's warm
contextTimeline rows (subagentCostFoldOf, whole descendant subtree,
cycles cut), with cold relatives' slim heads standing in. Those head
fetches leave the Agent network card's factory closure for a shared
page-scope cache (agentHeads.ts) the stats board's seat reads too, so a
relative is fetched once no matter how many cards consume it; the card's
list-snapshot subscription moves there as useSessionsSnapshot.