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 en/zh title and description are the canonical Plugins-page copy, not
a restatement of the package.json fields - the manifest values only
back the en fallback when a dictionary entry is absent. The en/manifest
equality assertions drop; the zh spec now pins that it is a real
translation (same fields, different copy) instead of only matching
en's shape.
The Host's package-meta reader serves each plugin's localized
title/description from the dictionaries exported under locale/ - the
same surface the official plugins use, so the Plugins page and the
settings inventory present dsh-context in the active locale instead of
the raw manifest fields. en carries the canonical copy (matching the
manifest name/description it overlays), zh the translation; the
exports pattern and the files list ship both dictionaries. A spec pins
the dictionaries' shape, the exports/files alignment, and the en/manifest
agreement.
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 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.
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 old suite exercised the built lib/ bundle, so v8 remapped almost
nothing (8% src coverage) and every run needed a build first. The new
suite imports src/ directly under two vitest projects (node for
host/shared, jsdom for client), rendering real React 18 trees against
the real dictionaries and ui-primitives, with faithful in-memory
harness faces (cordis inject/effect, locale chain, slots, sessions
provide) instead of plugin mocks.
- 43 fine-grained, stateless, single-module spec files (606 tests)
forked across workers: ~2.5s wall, no build step
- v8 thresholds at 100/100/100/100 per file over src/**
- host specs drive the real projection definitions (init/apply/view)
plus one full cordis + SessionStore + SessionProjectionRegistry
integration; the framework's snapshotJsonValue pins the
projection-cache precondition on every intermediate fold state
- oxlint's src ruleset now covers tests/** as well
- typecheck covers the tests via tsconfig.tests.json