The supported range is now two baselines - dsh-v0.1.5-rc.1 (session
format V3) and dsh-v0.1.7-rc.2 (V4) - enforced by the runtime gate at
BASELINE_DSH_VERSION = 0.1.5-rc.1; below it the fallback units and the
upgrade modal take over (0.1.2/0.1.3 hosts were previously served by the
real folds).
The pre-V3 log shapes are dead code on every supported harness: the
running harness migrates historical logs on every read path (or refuses
the session wholesale), and 0.1.5-rc.1 rejects a request/header carrying
header.system outright. The fold therefore drops the V0/V2-only paths -
the header.system envelope system prompt, assistant/chunk floods,
{start, end} replacement endpoints, tool/code-dispatch - and the state
fields only they wrote (systemsFromHeader, the stepStart decode
accumulators). stateVersion stays 20: no supported log folds
differently, and pre-V3-era cached rows are discarded by the projection
cache's identity gate (and the strict stepStart sub-schema) instead of
served. The headers unit folds metadata-only epochs (envelope
systemTokens write dropped; the v1 cached-row read-compat and its pinned
stateVersion 1 stay); replaceRangeOf speaks startSeq/endSeq only; the
browser's epoch fetch maps request/header to tools and system/message to
the prompt text.
tests/baselines.ts re-fits to the two entries (the right Sidebar seam is
required on both), the compat matrix drives both tags' REAL sources
(canonical logs re-shaped to V3, the hostile probe renumbered past them),
and the version fixtures pin the new floor. Manifest: dshReleases
{0.1.5-rc.1, 0.1.7-rc.2}, dsh peers >=0.1.5-rc.1 (satisfies the 0.1.7
admission check under includePrerelease), devDeps pinned to the
0.1.5-rc.1 surface plus the ui-primitives runtime closure the client
lane inlines (test-only), and the release workflow fetches the two tags.
docs/compatibility.md is rewritten around the two-line matrix; README,
AGENTS.md and the issue template follow.
Also riding this move, from the audit pass: the fold re-proves an inject
source's form before it books the event record (the harness validates
`kind` but not `form` - a hostile one previously failed the strict
wire/state schemas on every delivery, the #44 freeze class), the headers
unit guards a data-less envelope (it has no try/catch of its own), the
schemastery schema annotations drop the explicit z<T> wrappers the 3.18.4
Mode generics reject, and the version-history note records why
stateVersion is deliberately not bumped.
The weekday gutter drew 一/三/五 on rows 1/3/5 while the grid was
Monday-first, so every caption named the row below its day - today's
Thursday cell read as Wednesday. Rows now start on Sunday (row 0 =
Sunday): days.ts renames mondayOfWeek to sundayOfWeek and anchors on
getDay() directly, the grid builds off that anchor, and the labels key
by the row index they name so no row arithmetic can shift them again.
The month gutter is untouched - the label still marks the column
containing the month's 1st, now read off the column's Saturday. Specs
re-derive every pinned date from the Sunday-first calendar.
The generation split (thinking / answer / tool args) already tiles its
window off the stream's block-start markers; the same markers now count
into additive-optional per-bucket tallies (reasoningBlocks / textBlocks /
toolArgBlocks), so the card qualifies each decode slice with the number
of blocks actually decoded - the honest per-slice counterpart of TTFT's
call count and tools' run count (one model call emits zero to many
blocks, so a call count there would be a false tally). Overhead stays
unqualified: it is the residue, with nothing behind it to count.
Both log generations carry the markers: the V0 chunk flood counts them
into the step slot alongside the spans, and the V2+ embedded stream
yields them from the same single pass that prices the split
(decodeSpansOfStream -> decodeTallyOfStream). Counts ride the split's
own first-token gate, and rows cached before the fields existed render
the duration alone.
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>
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 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.
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.
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 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 split generation's detail transport rode connection.rpc.handle, whose
registration reads webServer off the connection service's own context —
cordis's strict-read guard rejects that for plugin callers, so the channel
never armed and every detail read fell through to the static fallback (405).
Mount the handler through connection.fetch.register instead (the same
exact-route seam the harness's own file-upload and media-reference routes
use), riding the authenticated /api fence; the client POSTs it same-origin.
The detail payload now also carries the slim wire head at the same fold cut
(the composition scalars the Agent network's cold-node ring fetch renders
from); a missing or malformed head drops out alone, never rejecting the
payload the detail cards consume.
The Context browser's Injected Context and Skill Injections rows previewed
the raw injected content, while the Context Events card named the producer
(plugin id, reconciled instruction files, durable kind). The fold now
stamps that identity on the surface node alongside the event, and the rows
prefer it for the collapsed preview - the content stays one expand away,
and the row filter still scans the folded text. Nodes folded before the
stamp (additive-optional on the wire and persisted state, no stateVersion
bump) degrade to the previous content preview.
The fold buckets all three skill-content shapes the harness injects — the
<available_skills> catalog digest, a user-explicit /name invocation's
instructions message, and the content a skill-tool load returns — into a
new 'skill' composition category instead of burying them in inject/tool
(closes#66). Skill-tool loads keep their tool identity stamped, so the
call tallies, hit counts, and failure marking are unchanged.
stateVersion 18 → 19: the per-category sums change shape, so cached rows
refold from the durable log on upgrade (docs/compatibility.md).
The trend chart, overview, Token donut, stats, and the Context browser all
gain the seventh bucket (orange-500); the browser's skill section pins its
rows like injects, so long sessions keep listing what the sums count.
New 'Tool Schemas - Default sort' row in Settings -> Plugins -> dsh-context
(ahead of the File Activity one): size, call count, or name, persisted per
user through the dsh-context settings namespace. The Context browser reads
it as the mount-time default for its tools toolbar; in-toolbar toggling
stays session-local and never writes back. The default default is now call
count. Degradation mirrors the existing preferences: the host schema is
loose (a stale persisted value falls back to the default), the client drops
values outside the vocabulary, and a host without the settings surface (or
a section missing the field) keeps the schema default, so the tools list
stays fully functional either way.
Replace the hardcoded DeepSeek rate table with a client-fetched price
book: @opencode-ai/models reads the models.dev registry once per page
load, every entry is re-proved at the boundary, and a failed fetch
degrades to a dash + note with backed-off automatic retries.
- cost totals rekey per (provider, model); only DeepSeek splits a
pricing period (peak windows at list price, all other hours half
price) — every other provider bills everything at list price
- provider ids resolve through a shared rename table (deepseek-official,
kimi-coding, minimax-cn, zai-coding-cn) with verbatim passthrough for
unmapped ids; model ids match exactly, case-insensitively, or by
suffix (k3 -> kimi-k3), ambiguous matches price nothing
- CNY display converts at the fixed 1 CNY = 0.15 USD; the tooltip lists
per-1M rates for the models the session billed, with peak|off-peak
pairs and the DeepSeek note shown on DeepSeek sessions only
- stateVersion 18: cached rows refold from the log, which rebuilds the
new cost keys and periods
The rates themselves check out against the official pricing pages (both
currencies, both families, and the Beijing peak windows), but the flash
row's label named deepseek-v4.1-flash — the model VERSION, not an API ID.
The page's current flash ID is deepseek-flash, with deepseek-v4-flash as
the accepted legacy ID, so the label and the tip's applies-to list now
cite those; the tip also drops the colloquial hardcoded phrasing for a
plain for-reference-only note.
AGENTS.md carried the full compatibility matrix — per-generation event
vocabulary, the moving V3 client seams, and the runtime gate's internals —
duplicating what docs/compatibility.md exists to document. Restate only the
requirements there and point at that page, keeping the agent-facing anchors
that have no user-facing home: the single shape-driven fold path
(`logShapes.ts`'s `firstTokenTimeOfStream` / `replaceRangeOf` / `isTokenChunk`,
`applyTimeline`), the upstream implementation mirrors (`dsh-token-meter`'s
`estimateSystemMessage`, `dsh-llm`'s `assistantStreamFirstTokenTime`), the
gated-inject rule for newer-only seams, and the baseline verification
mechanism.
docs/compatibility.md gains the previously undocumented baseline gate
(including the running-tree-first probe and fail-open behavior) and names the
V2 stream paths in full.
Closes#58: the Context panel hung off two registrations at once (the
conversation tab and the right-Sidebar guide entry), with no way to keep
just one. A new per-user `defaultPlacement` preference — the first row of
the Plugin configuration card — picks who carries the view: the
conversation tab, the right Sidebar, or both (the default, so existing
deployments see no change).
- shared: `DefaultPlacement` vocabulary (`all` | `tab` | `sidebar`) on the
settings namespace; the Host schema gains a loose `defaultPlacement`
field so a stale persisted value degrades to `all`
- client: `placement.ts` mounts each registration through its own factory
and, on a preference flip, disposes exactly what the new value drops
(the harness's own dynamic-mount idiom); `watchSidebarContextTab`
returns its deferred-inject disposer so the sidebar mount can be taken
down and put back; the settings card leads with the placement row
The family matcher required a `v4` marker, so `deepseek-flash` requests
went unpriced. Accept `deepseek` as an alternative marker — proxy
spellings like `deepseek/deepseek-flash` land too, while foreign
flash-named models (gemini-2.0-flash) still match no family. The stats
board's price-list label and both locales' cost tooltip name the new
model beside deepseek-v4.1-flash.
The timing totals priced a model call as TTFT + one opaque generation
window, so thinking, answer text, and tool-argument decoding were
indistinguishable. Fold the stream's `block-start` framing into three new
additive-optional buckets — `reasoningMs`, `textMs`, `toolArgMs` — that
tile the window's marker span.
Both log generations feed the same split: a V0 log's `assistant/chunk`
markers open and close spans in the step slot; a V2+ log carries no chunk
events, so `decodeSpansOfStream` reads them off the embedded settlement
stream. The split is priced only when the window was (an unstamped call's
model time stays unattributed wholesale), and a zero span stays absent so
pre-split-shaped state gains no dead properties.
The fields are additive-optional, so `stateVersion` stays at 15 and
cached rows keep parsing — a bump would discard them and orphan the key
for idle sessions (the #37 regression).
Measured over 305 local sessions: reasoning 31.3% of active time, tool
args 15.3%, answer text 3.5% — the answer is 1/9th of what the model
spends decoding.
DeepSeek reprices the Flash series effective 2026-09-10 12:00 BJT
(off-peak per 1M tokens: hit ¥0.02/$0.003, miss ¥1/$0.15, out ¥4/$0.6;
peak at 2x). Between the V4.1 Flash launch and the V4.1 Pro release
every Pro request is routed to V4.1 Flash and billed at the Flash
rates, so pro shares the flash triple until V4.1 Pro is priced — swap
that one reference when it ships. The stats-board tooltip names the
flash row deepseek-v4.1-flash, and both locales note the interim
routing. Fold logic is untouched: the substring family match already
lands v4.1 spellings (new fixture), and the peak windows are
unchanged. Logs spanning the cutover estimate entirely at the new
table — the cell is a reference estimate by contract.
The supported range (dsh 0.1.2-rc.1+) spans three durable-log generations, and the fold only read the V0 spellings — so on 0.1.5-alpha.1 sessions the system prompt priced 0, TTFT/generation stayed 0, and startSeq/endSeq replacements appended instead of replacing.
Read SHAPES, never a detected version (the probe can be wrong: a healed profile mirror may name another release than the running harness):
- system/message surface nodes feed systemTokens plus a bounded systems list (the harness's "last nonempty surviving system" rule), replacing the V0/V2 request/header.header.system envelope figure; the browser resolves the prompt in force per step and fetches its text from either event shape.
- The first token comes from the embedded assistant stream (assistant/message.data.stream / assistant/attempt.data.stream) when the log carries no V0 assistant/chunk deltas, mirroring dsh-llm's assistantStreamFirstTokenTime.
- Replacement endpoints accept both startSeq/endSeq and start/end.
- tool/ptc-dispatch books nested file ops alongside tool/code-dispatch.
Statistics were differentially verified against the harness's OWN projection values on real V0, V3-native, and V3-migrated logs: system/tools/message tokens, request and turn counts, TTFT, generation, tool time, and every billed cost bucket match exactly.
The new state fields are additive-optional, so stateVersion stays 15: a bump would orphan the key for idle sessions (the #37 regression).
A grep/glob with the complete matched-file meta rowed ONLY per hit file —
the searched path (or the workspace-wide pattern) vanished from the File
Activity card entirely. Both now book: the call's target row first (the
searched path, carrying the pattern(+include) detail; a pathless search
rows the pattern itself, marked as a pattern), then one row per matched
file with its hit count. The degenerate single-file search (the target IS
the matched file) keeps one row. Truncated or malformed metas keep the
honest fallback (the call target only).
The fold's op-log semantics change, so stateVersion bumps to 15 and cached
rows refold.
Verified: 1147 tests, per-file 100% coverage; live on a grep-heavy session
the card counts 80 search ops — the searched directories row alongside
their per-file hits (plus a deterministic fold over the real log: 6 target
rows + 107 hit rows).
The File Activity card derived its ops client-side from the served tool
nodes joined with the conversation WINDOW: a call whose arguments had aged
out of the loaded tail named no target and vanished from the card silently,
so the "up to latest" scope actually covered only the chat window. The join
was also the only meta/subCalls carrier, and the per-call op assembly was
written twice (top-level and Code-Mode nested paths).
The derivation moves to the host fold, which sees every lifecycle event with
its full payload: `tool/call` arms name + raw arguments (the pending-call
map's entries grow `argsRaw`), `tool/result` books the ops off the armed
arguments plus the result's presentation meta (grep's by-file match groups,
glob's path list, read's exact window), and `tool/code-dispatch` buffers
nested Code-Mode ops under their run_code root until the parent result
flushes them with `parent` + the program description. The log is bounded
(`maxFileOps`, default 400) with a `fileOpsFloor` coverage stamp, rides the
detail payload (and the inline value on channel-less hosts), and is plain
JSON end to end. stateVersion bumps to 14; cached rows refold from the log
to rebuild the op log.
The per-call parsing is ONE shared module (shared/fileOps.ts — opsOfCall
plus the kind/path/delta/read-window/search-attribution primitives),
consumed by the host fold and by the client's inline-generation fallback
(the legacy window derivation stays for old hosts, now through the same
parser, closing the duplicated assembly). The client aggregates + filters
(`activityOfOps` joins the archive's `gone` stamps at render time; nested
ops scope by their parent result's seq, matching the legacy node-level
rule); the card, its chips, rows, deltas, and locate bridge render
unchanged.
Verified: 1142 tests with per-file 100% coverage (new parser matrix,
fold-ops, aggregation, end-to-end through the real registry + detail
endpoint); the compat vocabulary probe gains tool/code-dispatch (present at
the 0.1.2-rc.1 tag). Live on the real deployment: the big session's card
grew from 4 window-bound files to 37 files / 135 ops spanning every turn,
and folding the real Code-Mode logs books nested ops with parent + program
stamps (e.g. 136 nested reads across one session, errors flagged).
Known upstream boundary (unchanged): `tool/code-dispatch` persists no
result meta, so nested reads keep the limit-estimate footprint and nested
searches the call-target form until the harness carries meta there.
The contextTimeline projection value rode every harness delivery channel
WHOLE: each session.list row, every control baseline, every follow
snapshot, and a full-value broadcast on every state change (with a
full-value schema parse per push). The heavy collections (request records,
context events, the served surface window, the removed-node archive) are
only ever rendered by an OPEN Context tab/modal, so they moved off the
wire value.
Wire head (buildTimelineHead, ~1KB): the composition breakdown, the stats
board's precomputed counters (turns/steps/injects/compactions/prunes), the
newest request's billing summary (the headline's derived anchor), the
timing/cost totals, and `detailRev` — a revision marker that bumps exactly
on detail-mutating folds, the open tab's refetch signal.
Detail channel (host/detail.ts): the pre-v0.9 generic Connection RPC shape
returns as one narrow endpoint, `/dsh-context` `detail`, serving the
collections off the registry's live fold state (`stateOf` — no second
fold, no private copy). A nested `ctx.inject(['connection', 'sessions'])`
arms it load-order-free; a deployment whose faces never compose keeps the
inline wire value unchanged. The baseline-gate fallback path is untouched.
Client (client/timelineSource.ts): one per-session detail store shared by
the tab and the /context modal — fetch on open, then debounced
trailing-edge refetches whenever the pushed detailRev outruns the served
detail (single-flight, latest-wins by revision, refold reset, failure
backoff, absence stops trailing). Detail-driven cards show a pending/retry
note instead of a misleading empty state; the counters and headline render
off the head immediately. Both wire generations are reconciled per value
(new client + old host renders the inline value untouched; old client + new
host degrades to empty charts in the stale-watch window, never a white
screen). The chat-jump relay holds its one-shot pin until the detail lands.
Fold state keeps the full shape: no stateVersion bump, no cached-row
invalidation (the slim view is recomputed per read for cold checkpoints).
Compat matrix gains the detail-channel seam probes (HostConnectionRpc,
the browser caller, the registry stateOf), verified against the
0.1.2-rc.1 tag.
Measured over 263 real session logs from a live deployment, plus live RPC:
- per-session wire value: 84–150KB (top sessions) -> 0.6–0.9KB (~100-165x)
- all sessions aggregate (session.list rows / control baselines):
4.4MB -> 0.2MB (~22x)
- per-client push traffic over one big session's lifetime:
157MB -> 1.8MB (~87x), and the per-push full-value zod parse shrinks
by the same factor
- detail payloads now flow only for the session being viewed, only while
its tab/modal is open (120KB measured live for the biggest session);
background sessions cost zero detail traffic
Verified visually against the running harness (big/small/cold sessions,
a live turn refreshing in place, past-step reconstruction, header epoch
and tool schema reads, the /context modal, the Agent network card) with
byte-identical card output. 1110 tests, per-file 100% coverage.
- The settings preference vocabulary (DefaultGranularity/DefaultTrendMode/
DefaultFileSort/PluginSettings/SettingsField) was declared in both
host/settings.ts and client/settings.ts — declared once in
shared/types.ts now, re-exported at both ends.
- The Context browser's two fetch-on-miss state machines (row content,
header epoch) were line-identical and had already drifted (the row
read's idle note said 'no content') — one useFetchOnMiss hook plus one
fetchMissNote helper (components/fetchOnMiss.tsx).
- The /context modal and the upgrade gate duplicated the capture-phase
Escape handling; useEscapeClose (components/escapeClose.ts) is the one
overlay contract, and the gate gains the modal's focus restore.
- The five/four identical useProjection reads in contextView/contextModal
collapse into services.ts's projectionOf; agentTree drops its private
asRecord copy for the exported one.
- agentTree's compact duration formatter is renamed fmtDurationCompact —
same name as format.ts's locale-aware fmtDuration with different
behavior was an import trap.
- Dead surface removed: Snapshot.occupancy (unfolded since 0.11 and
undeliverable under the strict wire schema), CAT_COLORS,
HistoryEventLike, and three exports nothing consumed (EVENT_ICONS,
ContextViewProps, the FileSort alias).
- Comment drift fixed: the conversation join has a single useChat seat
since the matrix narrowed to 0.1.2-rc.1+ (three sites), and
services.ts's header no longer claims the module is type-only.
- services.ts module doc: the client does call one RPC — the gateway
history page read (remote.session.page) that fetches a request-header
epoch's content on demand; drop the 'no longer calls any RPC' claim
left from the v0.9 data-path migration
- services.ts ImageRefLike: normalization on rc.1 is a
deployment-resolvable policy (defaults: 2048x2048 total-pixel budget,
8192px long edge), not the 0.1.1-line long-edge 2048px cap
- compat.ts: the below-baseline fallback units still emit the pre-0.1.1
{ schema, view } aliases (only the real units are modern-shape-only),
uncontradicting fallback.ts
- headers.ts / shared/types.ts: no supported-baseline harness path
writes the header entry plugin field (ToolSchema carries none); the
read stays defensive for foreign/newer producers
At apply time the host probes the running harness's version (the
dshHomePath-anchored profiles mirror first, the plugin's module anchor
second) and compares it against BASELINE_DSH_VERSION 0.1.2-rc.1 — with
release > rc > beta > alpha ordering at equal X.Y.Z. A harness below the
baseline gets fallback projection units (both registry contract
generations) that fold nothing and serve zeroed data plus an
`unsupported` wire record; the client keeps rendering the blank cards
and pops a per-session-dismissable modal naming both versions and urging
the update. A detection failure or an unparseable version fails open, so
a probe misfire can never blank a working deployment.
The step-start → assistant-message slice reads off the stream now: the
model wait (TTFT, step start → first token) and the generation (first
token → message), folded from assistant/chunk — only the open step's
first non-empty text/reasoning/tool-call delta stamps the slot (the
session-stats rule). Calls with no recorded token stay unattributed in
the overhead residue. `lmMs` leaves TimingTotals for `ttftMs` + `genMs`,
the stepStart slot gains the stamp — stateVersion 12, rows refold.
The context browser read the system row's item count from
`header.systemTokens`, a field only the post-#37 metadata wire shape
carries — but a host still running the pre-#37 view (a stale watch
build, an app not restarted since the upgrade) delivers content-bearing
epochs where the system TEXT rides the value instead. The row then read
0 项 while the ≈tokens figure still rendered, because the two come from
independent projections. Diagnosed against a real session: the cached
projection row was pre-#37 shaped, holding a 6261-char system string
(≈1570 tokens shown, count 0).
`headersOf` now normalizes both generations: unpriced legacy entries get
the meter's own heuristic, priced entries and new-shape values keep the
zero-copy pass-through. The heuristic (ceil(chars/4) + 4) moves to
shared/estimate.ts so the host fold and the client boundary price
identically. Regression tests cover the boundary matrix and the browser
row through the real call path; the #37 slimming itself is untouched.
The contextHeaders projection served full prompt/schema content through every harness channel (session.list, control baseline, pushes, checkpoints), so the initial payload grew with sessions x epochs x content size (issue #37). The projection now carries epoch metadata only; content lazy-loads per epoch via a seq-anchored history read; stateVersion stays pinned at 1 with a v1-tolerant state schema and a normalizing view so no cold session is ever orphaned. Headers wire -97.8%, list projections block -81.8%, checkpoint writes -82% (new sessions), host heap -92%, browser projection memory -98%, checkpoint CPU -90%, schema stringification 52x.
The host fold derives whole-session timing totals from the durable
step/tool lifecycle (step/start -> assistant/message -> step/end;
tool/call -> tool/result by callId): step wall time, the model-call
slice, and per-call tool durations with a bounded per-name tally
(stateVersion 11; cached rows refold from the log).
The head row becomes four equal cards: an eight-cell context grid
(shape stats, event tally, cost estimate with its rate tooltip), a
token ring around the cache-hit share, and a timing ring splitting
model calls vs tool runs. Legend rows pair each donut slice with
its label, quantity, and share - no bars; the ring center type
scales with the ring so it never touches the stroke. Wire guards,
i18n, styles, and hostile-input test fixtures included.
The session-projection contract drifted again in 0.1.1/0.1.2: the unit-key
constraint moved from SessionProjectionMap to SessionProjectionStateMap, the
pre-0.1.1 `schema`/`view` fields left the published definition, the wired
register overload now demands `wire` PRESENT, and 0.1.2 widened `init` to
`init(header: SessionHeader)`. The dual-contract definition is therefore
mirrored locally in full (ProjectionDefinitionV1 + V2) instead of Pick-ing
from the installed dts, the plugin keys merge into BOTH type maps, and the
zero-argument init satisfies every supported registry — one definition
object still installs unchanged from 0.1.0-rc.7 through 0.1.2-alpha.1.
Tool-schema rows in the Context browser carry a best-effort source tag
for the registering plugin, recovered without a harness change:
- a harness-logged `plugin` field passes through verbatim (future-proof);
- MCP-proxied tools resolve `mcp:<server>` from their public names;
- shipped first-party tools resolve from a pinned package map;
- a live `tools.register()` hook (cordis `internal/get` reader slot,
with a caller-package stack fallback for local-link installs) tags
third-party, agent-scoped, and dynamic registrations, and peels a
prior incarnation's wrapper on reload;
- tools already registered when the plugin mounts render a localized
"unknown plugin" tag explaining the boot-order gap.
Priority: `mcp:<server>` > live record > pinned map > unknown. The
chip rides the shared tag frame tinted with the brand badge tokens.
Rebased onto v0.35.0 with maintainer review fixes; the attribution
priority and reload-safety rework and the chip theming are described
in the PR review comment.
Co-authored-by: Nwflower <1679659@qq.com>
Co-authored-by: bowenliang123 <liang.bowen.123@qq.com>
The chip row lived in CurrentComposition, was fed the fold's per-tool
toolList summary, and the chips focused the Context browser on a tool
via a one-shot toolFocus callback. The browser already exposes the
tool schemas through its headers section, so the chip row was a
duplicate entry point that no longer pulled its weight.
Removes the lc-tools DOM block, the toolFocus/onToolFocusHandled
prop wiring across ContextView/ContextModal/Browser, the
'tools.top'/'tools.more' translations, the .lc-tools* CSS, and the
now-unused toolList summary across the wire + state schemas
(stateVersion 9 -> 10). estimateToolSchema stays because headers.ts
still prices individual tools for the browser's tool-schema cards.
Every annotation carries its evidence inline (guard dominance chains,
the 8:1 aspect clamp making the wide-solve arm unreachable, a
500k-dimension fuzz with zero hits). browser.tsx also drops two
redundant `?? null` in JSX render positions — undefined and null
render identically.
Aligns with the token-meter vocabulary of recent dsh releases (signed
token delta) and with the existing zh label 增量. UI labels, i18n keys,
settings schema, client logic and tests all switch from diff to delta.
The settings schema is now loose on defaultTrendMode so a stale
persisted value degrades to the default instead of throwing, and the
client ignores unrecognized values the same way.
Drop the heuristic estimate from the step header; show the provider-reported
cache-hit share of the billed prompt (cacheRead / prompt, truncated to two
decimals) instead. The host folds each request's cacheReadTokens into its
record (stateVersion 9); the stats board reuses the same formula via a shared
format helper.
Apply the full-repo comment audit to the host half and shared wire types:
delete restatements and decorative headers, compress rationale comments to
one line, and fix stale references (agent-loop buildRequest, live-surface
image cell, five->four priced categories). Kept only contract notes,
invariants (plain-JSON precondition, shadow-seqs protocol) and verified
dsh-mirror pins.
Plan mode includes a guidance section in every model request while
active, so its durable plan/mode event is a real context-composition
change and earns a neutral mode event row (own kind chip + filter).
A request header logged with reason 'resume' carrying a different model
is likewise a real switch made between sessions — record it alongside
the in-session 'change' ones.
A notice-form source already carries the producer's bounded one-line
account; record it as the event's detail and render the row as
title · producer · account, matching the dsh transcript row shape.
The image count moves off the persisted whole-session counter onto a
per-node imgs field (absent when zero), and the view sums it over the
live surface — matching the Tool Calls cell's semantics: images of a
message compacted or pruned out of the context stop counting. The
persisted-state shape changes, so stateVersion bumps to 8 and cached
rows are refolded.
The cell sits left of Images and counts the tool calls whose result is
live in the current context: one tool/result folds to exactly one tool
surface node, so the host derives the figure at view time from the live
surface (no persisted counter, no state-version bump). Calls still in
flight and results compacted or pruned out of the surface are excluded.
Adds a display-mode pair (全量/增量, Total/Diff) next to the Step/Turn granularity group in the Context History card. Diff mode plots each request's per-category |change| vs the previous request, with the hover tip reporting the signed net change (Δ +1.2k / Δ −535.5k); Total keeps the cumulative composition and stays the default.
Original PR by @xieshuaix (delta toggle); reworked before merge: the on/off delta switch became a two-way mode control (mode: 'total' | 'diff' end to end), the two toggle groups were wrapped so the title's auto-margin push happens once, trend.hint was shortened/localized, and the fiber-slot/button-count tests were updated.
Fold counts image blocks (user uploads + nested tool-result images)
cumulatively over the complete session log, rides the wire snapshot as
an optional additive field, and the stats board shows it as a new cell
between prunes and cache hit. stateVersion 7 refolds cached rows.
Follow DeepSeek's peak/off-peak rule adjustment: Saturdays and Sundays
(Beijing Time) no longer distinguish peak windows, so the whole weekend
prices at the half-price off-peak rate. Peak windows are now expressed in
Beijing Time (weekdays 09:00-12:00, 14:00-18:00), equivalent to the old
UTC windows on weekdays. List prices themselves are unchanged.
The composition legend and tooltips now show the exact system/tools/
messages counts of the chat composer ring's click-open panel (the
token-meter's contextBreakdown projection) instead of the fold's own
sums re-anchored to the provider total: parts carry the heuristic count
(raw) for display beside the provider-anchored bar width (value), and
the message bucket subdivides into the four surface categories by the
fold's per-category ratios, always summing exactly to the official
message figure. Absent the projection (older harness), the fold's own
sums serve unchanged.
The token-meter heuristic prices image blocks through its generic JSON
branch (~40 tokens for the durable ref), while DeepSeek's vision model
actually bills 117-384 tokens per image by pixel dimensions. Port the
official docs image-token calculator (patch grid 14px, downsample x3,
384-token cap, minPixels 384^2) into src/shared/imageTokens.ts and price
image blocks with it in the fold; unknown dimensions keep the JSON
fallback. stateVersion bumps to 6 so cached rows refold.
The differential fold check replays a parallel image-delta surface so
image-bearing logs still match the official contextBreakdown exactly,
and adapts to the 0.1.1 wire.view contract.
Stats board gains an 预估费用/Est. Cost cell pricing the complete
session's billed tokens at hardcoded DeepSeek V4 list prices: the host
folds per-request usage totals (peak/off-peak split, cache hit/miss
input, output incl. reasoning) from the session log, flash and pro on
any provider, displayed in the locale's currency. The cell carries a
"?" marker whose hover bubble explains the whole-session estimate and
lists the per-1M-token price table straight from the pricing table, so
the printed rates can never drift from the math.