# Changelog

All notable changes to this project are documented per commit series. Since
0.37.6 the rc channel's releases (`master`) also ship to npm (`latest`); the
alpha channel ships GitHub Releases only, never npm.

## 0.40.1 — a degraded close queues nothing and stops showing a stuck fold row (2026-11-03)

A task closed *without folding* could leave a permanent `folding…` row in the
dock and a lifecycle counter that never went down. Reported live and
reproduced on 0.38.3/0.39.0 (`plugins/task-marks.mjs` is byte-identical between
them): when AUTO compaction shadowed a task's `task_begin` anchor while the task
was still open, the later `task_end` took the degraded path and rendered
"Mark no longer on the surface; task closed without folding." — the log ended
with `task stack — empty; 3 folding`.

Three layers disagreed about that close. The TOOL layer queues nothing by
design (the verdict *is* the result). The REDUCER keyed on the rendered prefix
`Task ended: ` alone, so it popped the mark **and** pushed a `pendingArchives`
row anyway. The DRAIN then verdicts that row `drop` — and the drop settled it
in memory only, while the dock renders `closing` rows straight from the
projection. Nothing could ever clean it up: the anchor was shadowed *before*
the row existed, so the compaction that would have witnessed it is already in
the log, and the compaction-witness closure the reducer does have never fires.
The same mechanism explained three older tasks that *did* disappear — only
because a later compaction happened to shadow their close results.

- **Reducer**: a close whose result ENDS WITH the degraded verdict sentence
  (`UNFOLDED_CLOSE_MARKER`) queues no archive — the mark is popped, the row is
  not, exactly like `BELOW_FLOOR_MARKER`. Matched by POSITION, never `indexOf`:
  the tool embeds this task's name AND every still-open sibling's in the same
  result, so a task named e.g. `… task closed without folding audit` would
  otherwise suppress a legitimate archive silently, with no fold attempt ever
  billed. Both degraded wordings are covered — the tool layer queues nothing in
  either case, and the design record (D-R2) has an engine-less close degrading
  to close-only, no fold.
- **Drain**: every TERMINAL settle now publishes its composite display key, not
  just the below-floor one — the `drop` verdict, a fold that committed nothing,
  and the give-up after two summary rejections all leave the row queued while
  the dock kept drawing it as folding. `markArchiveSettled` absorbs the
  publish, so the invariant is structural: settled ⇒ never displayed, or
  counted, as folding.
- **Hint**: the stack line's terminal split gains a `dropped` bucket
  (`terminalArchiveKeys` classifies by PLAN, fresh from the log, so it is right
  on the first render after a restart; `terminalSplit` unions in the rows this
  process's drain has already settled, which no plan can see). Closed-unfolded
  rows now read `N closed unfolded` instead of `N folding`, and the dock and the
  hint agree on every settle path.
- **Not taken**: the issue's alternative of recovering the shadowed region from
  the compaction chain and folding it anyway. After the reducer fix no
  degraded-close row is ever queued, so that branch would be unreachable — and
  it would contradict the close result the model already read while risking a
  region that swallows live surface nodes. The reported class (a row that can
  never fold must not display or count as folding) is closed for EVERY drop
  reason instead, not just the anchor-shadowed one.
- **Verified**: `npm test` 15 suites / 251 tests / 0 failures, plus a new
  end-to-end regression suite that drives the real seams (`applyTaskMarks`,
  `createArchiveDrain`, `pruneBelowFloorView`, `taskStackView`,
  `taskStackLine`) — it fails 6/6 against the pre-fix tree with the issue's own
  symptoms and passes 6/6 after. Independent adversarial review could not
  falsify "a drop verdict is permanent", the counter arithmetic, single-counting
  in `terminalSplit`, or no-regression for a normal deferred fold.

  - host: `UNFOLDED_CLOSE_MARKER` + the reducer guard; `terminalArchiveKeys`
    (`belowFloorArchiveKeys` reimplemented over it, semantics unchanged)
  - drain: `markArchiveSettled(session, entry)` settles *and* publishes; the
    four terminal call sites route through it
  - hint: `taskStackLine`'s 5th parameter (`dropped`) + `terminalSplit`; the
    pre-0.40.0 rendering without it stays byte-stable
  - docs: README records dsh 0.2.1-rc.5 as the verified rc-channel host
    (already on master before this fix)
  - tests: `issue-3-degraded-close.test.mjs` (new, wired into `npm test`) plus
    reducer/drain/wire-view/counter cases in the existing suites

Cut as **0.40.1**, not the next rc patch (0.39.2): the alpha channel's 0.40.0 is
npm's current `latest`, and npm refuses to move that tag DOWN, so the rc
channel's fix takes the next free number above it. Alpha releases ship as GitHub
Releases only and never publish to npm — so from here the bare `dsh-taskfold`
name follows the rc channel again, and both READMEs now say so (they claimed
"latest = whichever channel released last").

## 0.39.1 — dsh 0.2.1-rc.3: re-verified, no plugin change (2026-10-26)

A patch step this time, and the package set barely moved: the dsh monorepo went
0.2.1-rc.2 → 0.2.1-rc.3 and the `@deepseek-ai/*` closure grew from 306 to 307
packages — one added (`dsh-client-ui-entry-customization`), none removed — so
the host surface was re-probed rather than assumed. Per-file diffs of the 21
packages this plugin touches show that the server-side ones — `dsh-app-boot`,
`dsh-compaction-basic`, `dsh-llm`, `dsh-tools`, `dsh-system-prompt`,
`dsh-session-projection`, `dsh-token-meter`, `dsh-agent-loop`, `dsh-session`,
`dsh-session-persistence` — changed only their `package.json` version field,
while the real code deltas are client-side (`dsh-client-ui-primitives`,
`dsh-client-ui-conversation`, `dsh-client-modules`,
`dsh-client-ui-plugin-manager`) plus the rebuilt `dsh-web-frontend` shell
(`index-CN7NIWNw.js` → `index-BXV-oxvX.js`) and `dsh-web-app/cordis.patch.yml`.

The compatibility gate re-checked with the real
`evaluatePluginCompatibility` on 0.2.1-rc.3
(`@deepseek-ai/dsh-app-boot@0.2.1-rc.3`, `getDshRuntimeVersion()` →
`0.2.1-rc.3`): the existing `^0.2.0-rc.1` peer floor still passes — the gate's
own `satisfies()` runs semver with `includePrerelease: true`, so a 0.2.x
prerelease anywhere inside the caret's `<0.3.0-0` bound is admitted and no
same-minor floor is required — v0.39.0 loads as-is, no code or manifest change.
Three negative controls (peers forced to `^0.1.7-rc.1` together and singly; the
real manifest judged against a 0.1.7-rc.1 runtime) are rejected with
unmet-requirements objects, so the pass is not vacuous.

Verification chain against the real 0.2.1-rc.3 packages: host-API shape probes —
52/52 (`BasicCompactionEngine` default export with `compactRegion` arity 4 and
`summarize` arity 3 on the prototype, subclassed exactly as the fold engine does
it; `BlockAssembler` driven to a real `blocks()` result; the schemastery CJS
chain plus this plugin's own `Config` metadata — `minSpanTokens` 0..1000000,
default 2000, volatile, and `showTaskBar` default `false`, volatile, i.e.
v0.39.0's opt-in default exactly as the host serves it; all seven services,
three hooks (`agent/pre-step`, `agent/turn-stopping`,
`compaction/summary-error`) and six methods it uses present; all four
`@deepseek-ai` specifiers it references resolvable); the client-contract sweep —
19/19 present, 0 absent (`conversation.input.dock`, `plugins.bundle.config`,
configForms `whileServed`, `window.__ModuleLoader__`, `SettingsFormModel`,
`settingsNumberField`, plus `slots`/`configForms`/`locale`,
`inject(key, callback)` and `register(...)`, with the rebuilt shell's factory map
supplying both specifiers the bundle requires and the served bundle matching
`client/taskfold-client.mjs` for all 753 lines apart from the appended `;` and
sourcemap); an end-to-end probe host (real 0.2.1-rc.3 build over a hash-pinned
copy of this tree at 9e98e04, on an isolated `DSH_HOME`) with empty stderr and
zero plugin-disable lines — checked against BOTH wordings, because 0.2.1+
prints `disabling profile plugin ${label}` while 0.2.0 printed
`disabling profile plugin row "<id>"` — whose `settings/describe` serves the
`taskfold` namespace at `{"minSpanTokens":2000,"showTaskBar":false}` with
`applies: live` and the plugin's own schema verbatim, and whose `--dump-config`
lists both rows (`taskfold` → `dsh-taskfold/plugins`, `taskfold-client` →
`dsh-taskfold-client`) with no disabled marker; and the offline suite — 14
suites, 239 tests, 0 fail. The desktop payload this record was written in is
already `0.2.1-rc.3` (`desktopVersion` in its runtime manifest), running this
plugin with task marks, folds and `fold_recall` active. Not verified in the
0.2.1-rc.3 probe: a real fold/summarization round trip (the isolated home had no
model-provider credentials) — engine shape, subclassing, the resolver, the
shim's construction and a driven `BlockAssembler` stand in for it. Both READMEs'
"Supported dsh versions" records updated (the 0.2.1-rc.2 entry replaced per the
one-entry-per-channel convention). Not applicable this time:
`verify-cache.mjs --since-restart` — the fold envelope is untouched (no
summarization input, prefix or drain change), so there is no new live-cache
number to record.

  - record 0.2.1-rc.3 as the verified rc-channel host in README.md + README.zh.md

## 0.39.0 — the task-banner dock is hidden by default (2026-10-23)

The task-stack dock — the banner beside the conversation input that lists the
session's named tasks and their fold state — is now OPT-IN: `showTaskBar`'s
schema default flipped `true` → `false`, so a profile that never touched the
switch no longer gets the banner. Nothing else about the feature changed: the
Taskfold card on the Plugins page still stages the same volatile Switch, and the
client's rule is untouched — the dock is hidden on exactly a served `false`, so
an unserved namespace or a missing ui-settings/configForms service still renders
it instead of silently dropping the dock. Switching it back on restores the old
behaviour verbatim; a profile that already persisted `showTaskBar: true` keeps
its banner (the default only decides the profiles that never chose).

The default lives in the host schema (`plugins/taskfold.mjs` — the Config field
dsh-settings exposes, validates and persists through the active profile's
Cordis patch), and both halves moved together: the field description, the
comment in `plugins/fold-settings-ui.mjs`, the dock sections of both READMEs,
and the regenerated `client/taskfold-client.mjs`. `test/client-bundle.test.mjs`
now pins the pair rather than either half alone — a schema default of `true`
would render the banner for every untouched profile, while a client that hid on
anything but an explicit `false` would hide it where settings are unavailable —
and it also asserts the committed bundle carries no unsubstituted placeholder.

Regenerating that bundle surfaced a real defect in the published 0.38.3: the
template's two `__DOCK_ID__` slots went through `String.replace`, which
substitutes only the FIRST match, so the dock's registration id shipped as the
literal `"__DOCK_ID__"` (the list slot validates `id`, and the only valid value
is `task-stack`). The old default hid the bug — the mount-time registration is
the first slot — while the now-default path (hidden at boot, then switched on)
is exactly the one that reaches the second. `substitute()` now replaces every
occurrence (split/join, literal insertion, so replacement-pattern characters in
a value cannot reinterpret it), and `renderBundle` fails the build on any
leftover `__[A-Z_]+__` placeholder, so the class cannot ship silently again.
`test/client-bundle.test.mjs` locks both halves (two `id: "task-stack"`
occurrences, zero leftovers) and keeps the guard itself under test.

Verification: the default probed through the real `@deepseek-ai/schemastery`
from the 0.2.1-rc.2 host — `Config({}).showTaskBar.get()` → `false`,
`meta.default` → `false`, `meta.volatile` → `true`, with `minSpanTokens` still
`2000` — and a fresh render of the bundle is byte-identical to the committed one
(the release's own freshness gate, re-run here). Offline suite: 14 suites, 239
tests, 239 pass, 0 fail. Not applicable this time: `verify-cache.mjs
--since-restart` — the fold envelope is untouched (no summarization input,
prefix or drain change), so there is no new live-cache number to record.

  - the task-banner dock is hidden by default

## 0.38.3 — dsh 0.2.1-rc.2: re-verified, no plugin change (2026-10-19)

A minor step this time, and the package set moved with it: the dsh monorepo went
0.2.0-rc.2 → 0.2.1-rc.2 in lockstep, but the `@deepseek-ai/*` closure grew from
288 to 306 packages (28 added — `dsh-plugin-market`, `dsh-tool-terminal`,
`dsh-tool-working-directory`, `dsh-experimental-session-search`, …; 10 removed —
`dsh-hook-protocol`, `dsh-hooks-codex`/`dsh-hooks-claude-code`,
`dsh-webhook`/`dsh-webhook-github`, `dsh-client-ui-slots`, `dsh-client-store`,
`dsh-subagent-in-process-driver`, `dsh-invariants`,
`dsh-experimental-schedule-bundle`), so the host surface was re-probed rather
than assumed. The compatibility gate re-checked with the real
`evaluatePluginCompatibility` on 0.2.1-rc.2 (`@deepseek-ai/dsh-app-boot@0.2.1-rc.2`,
`getDshRuntimeVersion()` → `0.2.1-rc.2`): the existing `^0.2.0-rc.1` peer floor
still passes — the gate's own `satisfies()` runs semver with
`includePrerelease: true`, so a 0.2.x prerelease anywhere inside the caret's
`<0.3.0-0` bound is admitted and no same-minor floor is required — v0.38.2 loads
as-is, no code or manifest change. Three negative controls (peers forced to
`^0.1.7-rc.1` together and singly; the real manifest judged against a
0.1.7-rc.1 runtime) are rejected with unmet-requirements objects, so the pass
is not vacuous.

Verification chain against the real 0.2.1-rc.2 packages: host-API shape probes —
52/52 (`BasicCompactionEngine` default export with `compactRegion` arity 4 and
`summarize` arity 3 on the prototype, subclassed exactly as the fold engine does
it; `BlockAssembler` driven to a real `blocks()` result; the schemastery CJS
chain plus this plugin's own `Config` metadata; all seven services, three hooks
(`agent/pre-step`, `agent/turn-stopping`, `compaction/summary-error`) and six
methods it uses present; all four `@deepseek-ai` specifiers it references
resolvable); the client-contract sweep — 19/19 (`conversation.input.dock`,
`plugins.bundle.config`, configForms `whileServed`, `window.__ModuleLoader__`,
`SettingsFormModel`, `settingsNumberField`, plus `slots`/`configForms`/`locale`,
`inject(key, callback)` and `register(...)`, with the served bundle matching
`client/taskfold-client.mjs` for all 752 lines apart from the appended `;` and
sourcemap); an end-to-end probe host (real 0.2.1-rc.2 build over a hash-pinned
copy of this tree at 4c7985b, on an isolated `DSH_HOME`) with empty stderr and
zero plugin-disable lines — checked against BOTH wordings, because 0.2.1
rewords the diagnostic from `disabling profile plugin row` to `disabling profile
plugin` — whose `settings/describe` serves the `taskfold` namespace (the row id,
not the historical `cmpct-region`) at
`{"minSpanTokens":2000,"showTaskBar":true}` with `applies: live` and the
plugin's own schema verbatim, and whose `--dump-config` lists both rows
(`taskfold` → `dsh-taskfold/plugins`, `taskfold-client` → `dsh-taskfold-client`)
with no disabled marker; and the offline suite — 14 suites, 236 tests, 0 fail.
Not verified in the 0.2.1-rc.2 probe: a real fold/summarization round trip (the
isolated home had no model-provider credentials) — engine shape, subclassing,
the resolver, the shim's construction and a driven `BlockAssembler` stand in for
it. Both READMEs'
"Supported dsh versions" records updated (superseded 0.2.0-rc.2 entry replaced
per the one-entry-per-channel convention).

  - record 0.2.1-rc.2 as the verified rc-channel host in README.md + README.zh.md

## 0.38.2 — install instructions that survive npm's release-age policy (2026-10-08)

The install sections now lead with a **version-pinned** install
(`dsh-taskfold@0.38.1`) instead of the bare package name, because dsh Desktop
bundles pnpm 11, whose `minimumReleaseAge` supply-chain policy refuses a release
younger than a fixed window. A bare `pnpm add dsh-taskfold` therefore resolves to
the newest release *older* than that window — on 2026-10-08 that was 0.38.0,
which cannot be installed at all, while the dialog's own version chip showed
0.38.1. The failure surfaced as the pre-0.38.1 `file:./client` error and read
like a packaging bug; it was the policy falling back onto a broken release.

Pinning works because pnpm treats an explicit version as consent: it installs
what was asked for and records it in the profile's `minimumReleaseAgeExclude`.
The prebuilt tarball on the GitHub Release and a Git address both bypass the
policy too, so the README lists all three routes — and drops the `@0.38.0`
example that used to send readers straight into the broken version.

  - README (en/zh): pin the version in the dialog and the CLI examples, and explain the policy, the misleading version chip, and the tarball/Git routes
  - `dsh-taskfold@0.38.0` is deprecated on npm (`0.38.0 cannot be installed … use 0.38.1 or newer`)
  - the missing GitHub Release for 0.38.1 was published from the existing tag, so `releases/latest` no longer points at an uninstallable version

## 0.38.1 — publish the client half as its own npm package so consumers can install (2026-10-06)

0.38.0 was uninstallable. The root manifest declared the browser half as
`"dsh-taskfold-client": "file:./client"`, and pnpm resolves a path specifier
against the workspace root of whoever is INSTALLING the package — not against
the directory that declares it. So the nested `client/` directory was looked for
one level above the profile and every install stopped at

    ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND
    Could not install from "<profile>/client" as it does not exist.
    This error happened while installing the dependencies of dsh-taskfold@0.38.0

Both routes into a profile hit it, which is what made the release useless rather
than merely awkward: `pnpm install dsh-taskfold@0.38.0` from the registry and
`dsh plugin add github:yindf/taskfold` both died the same way, so no user could
reach 0.38.0 without hand-writing a `pnpm.overrides` entry in their profile.

The declaration itself is load-bearing and stays. The client row in
`cordis.patch.yml` mounts its OWN package name, and `client-modules` accepts a
row name only when it is path-like or an exact bare package name — a subpath of
the root package is dropped outright, and a relative name renders as a raw
`file:///` path with no title or introduction. A bare name therefore has to be
resolvable through the CONSUMER's `node_modules`, which no specifier pointing
inside a published package can be: `file:` and `link:` resolve against the
consumer's workspace root, and `workspace:*` is not a member of anyone else's
workspace. Only a registry version range is installable, so the browser half is
now a package of its own.

  - `dsh-taskfold-client` is published, with `"private": true` removed (npm
    refuses to publish a private package, which would have left the version the
    root depends on permanently absent); the root depends on it by range
    (`^0.38.1`), replacing `file:./client`
  - `scripts/release.mjs` publishes BOTH packages, CLIENT FIRST: the root's
    manifest names the client's version, so a root published first would 404 for
    every consumer until the client landed. Each package is probed and published
    individually, so a re-run finishes whichever half is missing, and the loop
    stops on any status but `published`/`already` — publishing the root over an
    unpublished client is the exact failure that order exists to prevent
  - new ship-guard `assertClientDependencyResolvable()`, run by `release` before
    either manifest is written (validating the version the release is ABOUT to
    write, not the tree as it stands) and by `npm` before it publishes: it
    refuses a missing declaration, any `file:`/`link:`/`portal:`/`workspace:`
    specifier, a non-range specifier, a private or misnamed client, a client
    without `dsh.client.platform === "web"` or without the `./client` export, a
    declared range that does not include the client's version, and a client
    version that is not in lockstep with the release
  - `spawnCaptured`/`npmSpawn` accept a working directory, so `npm publish` runs
    in `client/` as well as in the repo root
  - `test/release.test.mjs` is now 32 tests (was 25); `test/client-bundle.test.mjs`
    pins the dependency's SHAPE instead of its old `file:./client` text — that
    assertion was why the suite stayed green while the shipped package could not
    be installed, since inside this repo the nested directory is simply there and
    no unit test can see a consumer's resolver
  - `npm test`: 14 suites, 0 failures

Repair for anyone already on 0.38.0: drop the `pnpm.overrides` entry and install
0.38.1. Publishing order for future releases is a rule, not a preference —
`dsh-taskfold-client` first, then `dsh-taskfold`.

## 0.38.0 — the release flow gains an npm repair command (2026-09-30)

0.37.7 reached git, GitHub and npm only because its npm copy was published by
hand. `release` runs the npm step while it still is `release` or a PENDING
resume, and once the tag, the commit and the GitHub Release are pushed the state
is CLEAN — so a release whose npm step was skipped (no token visible to the
script at the time) or whose upload failed has no retry entry point at all.
`node scripts/release.mjs npm [--version X.Y.Z]` is that entry point: it does
nothing but publish to npm. It is idempotent through the existing registry probe
(a version already there prints `nothing to publish` and exits 0), and unlike
`release` — which must never let a failed upload look like a failed release —
it exits 1 whenever it did not publish, because publishing is the whole point of
running it.

Its guards all follow from one fact: `npm publish` ships the WORKING TREE, never
a tag. The target is `--version`, or else the CHANGELOG's top entry, which must
be a released one (a draft is not a version the registry can already hold, so a
draft top demands an explicit `--version`). It then refuses unless package.json's
version, a local `v<version>` tag, a clean tree — untracked files included,
since anything inside the `files` allowlist would be packed — and a tree
identical to that tag all say this checkout really is that release.
`assertClientBundleFresh()` runs before the publish, so a stale browser bundle
cannot go out either. The tag-identity guard is the one worth having: without
it, repairing a version npm missed from a branch that has moved on since would
put unreleased files on the registry under a released version number.

The token lookup gained a second source. `NPM_TOKEN` is still read from the
process environment, and on Windows now falls back to the user-level variable
(`reg query HKCU\Environment /v NPM_TOKEN`), because a host-launched shell
snapshots its environment at start-up: a token set after that is invisible to
the child while plainly set for the user — precisely what happened to 0.37.7,
whose npm step reported "no token" in a process that could see none.

  - `scripts/release.mjs`: new `npm [--version X.Y.Z]` subcommand — target version, the four guards, `assertClientBundleFresh()`, exit 1 on every outcome but `published`/`already`
  - `publishToNpm` returns `'published' | 'already' | 'no-token' | 'refused' | 'failed'`; `release` still ignores it (npm must never fail a durable release), `npm` does not
  - token fallback to the Windows user-level variable (`parseRegQueryToken`: `REG_SZ` and `REG_EXPAND_SZ`), so a stale shell environment no longer hides a token that exists
  - new pure exports with tests: `resolveNpmVersion` (`--version` wins; a draft top demands it), `checkNpmTarget` (guard order: version → tag → dirty tree → tree differs from the tag), `parseRegQueryToken`; `test/release.test.mjs` is now 25 tests, `npm test` 14 suites / 0 failures
  - verified on a clone checked out at the v0.37.7 tag with this script committed and the tag moved onto it: `release.mjs npm` hit the idempotent branch (`already on the registry — nothing to publish.`, exit 0) and the token fallback found the user-level variable; a clean tree carrying one commit the release does not was refused (exit 1) with the `git switch --detach v0.37.7` hint
  - README (en/zh) release notes and `docs/design/release-flow.md` (`### npm`, the CLI contract, and the 2025-06 "no npm publish" decision marked retired) document the repair path

## 0.37.7 — the Plugins page drops to two named rows, and the release flow publishes to npm (2026-09-30)

The bundle patch declared THREE Loader rows, so the Plugins page listed three
components for one package: `cmpct-region`, `cmpct-stats`, and
`taskfold-client`. That was never a duplicate registration — the page renders
one row per `insert` entry in `cordis.patch.yml` — but two of the three were
avoidable. The observation tools moved out of their own row: `compact-stats.mjs`
is now a PLAIN module exporting `registerStatsTools(ctx)`, and `taskfold.mjs`
(the renamed `compact-region.mjs`, the bundle's single host row) calls it from
`apply`. Same `ctx.tools` service, same plugin fiber, same two tools — one
fewer Loader row. The remaining two rows cannot be merged into one: a client
package is discovered by client-modules FROM a live Loader row, and two rows
resolving the same package make it throw "resolves from multiple active Loader
sources", which is why the browser half stays in the nested `client/` package
with a row of its own.

Both rows are named after the package. Row ids: `cmpct-region` → `taskfold`,
while `taskfold-client` keeps its name. The host row's specifier moves from the
relative `./plugins/compact-region.mjs` to the package SUBPATH
`dsh-taskfold/plugins`, because `readPluginMeta()` returns undefined for
anything that carries no package name and the boot rewrites a `./…` row name
into a `file:///` URL before the page reads it — the relative form is why the
page rendered a raw path with no title, description or icon. The subpath
resolves through the profile's own `node_modules` (the `dsh plugin add` link)
into this package's `exports` map, and the row then reads its own
`<specifier>/locale/*.json` plus `<specifier>/package.json`: `plugins/locale/*.json`
carries this row's title `Taskfold` and its one-line introduction, and
`plugins/package.json` — the description fallback — is deliberately absent and
unexported, so the row's copy can only come from that locale file while
`package.json` and the package-level `locale/*.json` keep the longer blurb for
the package page header and the npm listing.

The client row mounts its own package NAME, `dsh-taskfold-client`, which is how
the official bundles mount their halves (one component = one resolvable package,
each carrying its own `locale/*.json`). Two host rules leave no other option:
client-modules' `locatePkgJson()` accepts a path-like name or an exact bare
package name and drops everything else — a subpath of the root package,
`dsh-taskfold/client` included, never reaches the boot graph — while a path-like
name can never be titled, because boot rewrites a `./…` row name into a `file:///`
URL and `readPluginMeta()` returns undefined for any specifier that carries no
package name. That path is what the page printed as this row's title. The name
resolves without publishing a second npm package: `package.json` declares
`"dsh-taskfold-client": "file:./client"`, and dsh-app-boot publishes a profile
bundle's resolvable dependencies as `scope: 'profile'` resolution entries
(`collectProfileScopePackages` → `dependencyClosure`, walking the bundle
manifest's dependencies/peers). The nested `client/` package already ships inside
the tarball, so npm/pnpm recreate that link on install; a working tree needs
`pnpm install` (or the equivalent link) before the row resolves. `client/locale/*.json`
titles the row `Taskfold Client` (`Taskfold 客户端` in Chinese) and carries its
one-line introduction, while `client/package.json` still drops its own
`description` so the row's copy can only come from that locale file. Both rows
now render the same four lines — title, introduction, row id, module name —
because `client.js` prints the id and the module name only when the title
differs from them, and both titles are deliberately distinct from both, the way
the official bundles' rows are shaped.

The plugin's own `name` is `taskfold` (was `compact-region`).

BREAKING for installs that already saved settings: a row id IS the config key
of its Settings form (`dsh-settings` keys forms by entry id), so a previously
saved fold floor / `showTaskBar` under `cmpct-region` is no longer read — the
new `taskfold` row starts from the schema defaults (2000 / true). The old key
stays harmless in `cordis.patch.yml`; delete it or re-enter the value on the
Settings page. Anyone mounting this bundle beside a preset that still carries
the historical `cmpct-region` / `cmpct-stats` rows must drop those rows first —
tool names live in a shared registry and duplicate registration fails.

  - `compact-stats.mjs` exports `registerStatsTools(ctx)`; its Cordis plugin object and row are gone
  - `plugins/compact-region.mjs` → `plugins/taskfold.mjs`; exported plugin `name: 'taskfold'`
  - `cordis.patch.yml`: host row `name: 'dsh-taskfold/plugins'` (was the relative `./plugins/taskfold.mjs`); client row `name: 'dsh-taskfold-client'` (was the relative `./client/index.mjs`); legacy `cmpct-*` row ids retired
  - `package.json` `main`/`exports["."]`/`exports["./plugins"]` → `./plugins/taskfold.mjs`; new `plugins/locale/*.json` (title + one-line introduction, both languages) gives the host row its own copy
  - new `dependencies` entry `"dsh-taskfold-client": "file:./client"`, plus `client/locale/*.json` (title + one-line introduction) and a description-free `client/package.json`: the client row mounts a resolvable bare name and renders as `Taskfold Client`, not as a `file:///` path
  - test file renamed to `test/taskfold-stats.test.mjs`; `test/client-bundle.test.mjs` pins the host subpath, the declared component dependency, the client row's bare-name specifier, and both row locales — a title plus a short translated introduction, with the title distinct from the row id and the module name so both rows keep the same four-line shape
  - client bundle regenerated (source comments only); `npm test` green (14 files)
  - the package blurb (`package.json` plus `locale/{en,zh}.json`) and both row introductions rewritten rather than translated: the Chinese copy now reads as written Chinese instead of translated English, and each row says what its component actually does (the browser row names the task bar and the fold settings, not a calque of "browser half")
  - the fold-floor settings card binds the renamed row id `taskfold` (`FOLD_SETTINGS_PLUGIN_NS`): dsh-settings serves one config form per profile entry and the card registers `whileServed`, so a stale namespace would have left the card permanently absent with no error anywhere — `test/client-bundle.test.mjs` now pins the constant to the host row's `id:` in `cordis.patch.yml`
  - README (en/zh) localization + layout sections and `docs/design/task-stack-ui.md` describe the two named rows and the component-package shape; `docs/design-compact-stats.md` keeps the single-row shape

npm rejoins the release pipeline: `node scripts/release.mjs release` (and its
PENDING resume) now publishes the version to npm whenever the `NPM_TOKEN`
environment variable is set. npm itself reads no token from the environment
(`NODE_AUTH_TOKEN` is only honored through .npmrc templating), so the step
writes a project-level `.npmrc` carrying the token, publishes with
`--access public`, and removes the file in a finally; a pre-existing project
.npmrc aborts the step rather than clobber it. The token must be
**automation-type**: accounts with 2FA reject publish-time OTPs for granular
tokens (verified live this cycle — EOTP on the granular token, first-try
publish on the automation one). Idempotent: `npm view <pkg>@<version>` runs
first and a version already on the registry is a no-op, so resumed and
hand-rerun releases are safe. The npm step never fails the release — the tag,
commit, and GitHub Release are durable before it runs; failures print a
manual `npm publish` hint instead. npm caught up to 0.37.6 by hand this
cycle; from the next release the pipeline owns it.

  - release (and PENDING resume) gains the opt-in npm publish step (NPM_TOKEN)
  - pure helpers `npmrcAuthLine` + `manualNpmHint` covered by offline tests
  - README maintainers sections (en/zh) and the release-flow header comment document the step

## 0.37.6 — the below-floor verdict moves into the Task-ended result ("plan A") (2026-09-30)

The 0.37.5 sidecar ledger is gone. The plugin owns task_begin/task_end
(compact-region registers both tools and renders their results), so the
verdict is now made AT CLOSE TIME and recorded IN BAND: a solitary task_end
(no partner tool calls in the carrying message — partner results have not
landed yet and their unbounded size would make the estimate lie in the skip
direction) measures the span right there (`spanEstimateAtClose` mirrors
deferredArchivePlan's start walk; the surface tail is the end) via the same
CJK-aware estimator, and a below-floor close renders the marker phrase
"Closed below the fold floor" into the `Task ended` result. The reducer
keys on that marker: the mark pops and the row is NEVER queued into
pendingArchives — the verdict lives in the event log itself, permanent by
construction, so there is no ledger to read, no reopen path to disable, and
nothing for the dock to prune (below-floor closes never reach it; the
"closed below floor" lifecycle counter likewise no longer sees them).
Non-solitary closes and pre-0.37.6 rows fall back to the drain's in-memory
below-floor settle (kept, with the prune view and the settle-aware apply
push) — that fallback re-derives after a restart under the then-current
floor, the documented 0.37.4 semantics. This also fixes a small lie the
below-floor close used to tell the model: its result claimed "Archival
queued — the span folds automatically" for a span that never folds; the
below-floor result now says so and drops the report-gate instructions.
Rationale: the session log is the single durable record; a verdict written
into it needs no second store, and the only writer allowed to touch it —
the plugin's own tool result — is exactly where the verdict is born.

  - close-time fold-floor verdict recorded in the Task-ended result (BELOW_FLOOR_MARKER; reducer never queues marked closes)
  - delete the floor-skips.jsonl ledger and permanentSkips (the log is the record)
  - task_end result for below-floor closes stops promising an archival that never happens
  - fallback path (non-solitary closes, old logs) keeps the drain's in-memory settle + dock prune

## 0.37.5 — below-floor skips are permanent: persisted per session, never reopened (2026-09-30)

First-principles correction, from live use: the plugin exists to keep context
CHEAP, and retroactively folding spans a later, lower floor would admit
surface rewrites the prefix cache never planned — every such rewrite
invalidates the provider prefix cache behind it, a cost no small fold can
repay. So the reopen machinery (0.37.0–0.37.4: lowering the floor re-settled
previously skipped spans, both live and after every restart) is GONE. A
below-floor settle is now an append-only verdict:

- the drain writes each verdict to `floor-skips.jsonl` in the session's
  taskfold artifact directory — a restart re-settles straight from the
  ledger instead of re-measuring under whatever floor is current then
  (degrades to memory-only when no artifact directory resolves)
- no floor edit reopens a settled span: `reopenIfFloorLowered` and the
  settle-time floor stamps are removed; the display key is published once
  and never withdrawn
- the lifecycle stack line unions the ledger's keys into its below-floor
  count (intersected with the queued rows), so a permanently skipped row is
  never counted as 'folding' after the floor drops below its span

  - remove the floor-lowering reopen; below-floor settles are final for the
    life of the session log
  - persist below-floor verdicts to <artifact-dir>/taskfold/floor-skips.jsonl
  - tests: reopen expectations inverted (lowering to 1 never folds a settled
    span), restart simulation via a second drain over the same ledger,
    memory-only degrade keeps the re-derive-from-current-floor behavior

## 0.37.4 — below-floor settles vanish from the dock, and the prune now ships without waiting for the next task mark (2026-09-30)

Follow-up to 0.37.3, driven by live use: the terminal 'closed · below fold
floor' chip answered the honesty question but not the noise one — small
tasks kept a permanent dock row and a permanent '{n} closed below floor'
counter the user has no use for, and the labeled state itself only reached
the client when a task-mark event happened to push it (a settle changes
nothing in the event log, and the projection registry re-runs a wire view
only when apply produced a NEW state reference — so the chip could lag a
whole conversation). Two changes:

- The wire view now PRUNES settled below-floor rows from the wire value
  instead of flagging them: a span that will never fold has nothing worth a
  dock row. The raw projection state keeps the rows (the drain re-settles
  them after every restart; `archivesOf` reads the state, not the view), and
  the dock drops the belowFloor field, suffix label, and separate counter —
  closing rows are plain 'folding…' again, counted only as folding.
- `makeSettleAwareApply` wraps the taskMarks reducer: on the first event
  after the drain's settle registry mutates (settle or reopen bumps its
  version), the wrapper returns a content-identical shallow copy — exactly
  one extra state-reference move per mutation, which re-runs the pruned view
  and pushes it. A below-floor task now disappears from the dock at the
  next event after its settle, with no task-mark activity required; a
  floor-lowering reopen restores the row the same way. Replay-safe: the
  clone changes object identity only, never derived content.

Restart window unchanged (one boundary): the registry is in-memory, so
after a restart settled rows re-appear as 'folding…' until the drain's
first pass re-settles them and the next event ships the prune.

  - dock: below-floor rows/labels/counters removed (0.37.3 display work superseded)
  - host: pruneBelowFloorView replaces annotateBelowFloorView; makeSettleAwareApply wired as the projection's apply
  - tests: below-floor-view suite (prune/memo/reopen, wrapper clone-per-bump, null-state safety); task-stack-ui expectations updated

## 0.37.3 — below-floor settles stop masquerading as 'folding…' (2026-09-30)

Live regression from the 0.37.0 fold floor: a task closed below
`minSpanTokens` (observed at 616 estimated tokens against the 2000 default)
settles in the drain's MEMORY as closed-unfolded — zero LLM calls, by design
— but nothing ever shadows its close result, so the projection keeps the row
in `pendingArchives` forever and every consumer rendered it as a fold in
flight: the dock chip read '发布 0.37.1 折叠中…' indefinitely and the
lifecycle hint counted '1 folding' that would never be billed.

  - the drain now publishes each below-floor settle into a bounded
    display registry (composite `seq:name:foldResultSeq` key, withdrawn on
    a floor-lowering reopen), and the taskMarks wire view annotates exactly
    those rows `belowFloor: true` — reference-stable (the change feed's
    Object.is gate stays quiet on unrelated commits) and memoized on the
    state reference plus a registry version so settles between commits
    re-annotate on the next view computation
  - the dock renders flagged rows as terminal: '已关闭 · 低于折叠下限' /
    'closed · below fold floor', a neutral rail dot, and a separate
    '{n} closed below floor' count in the header tail — never inside the
    folding count
  - the lifecycle hint's stack line measures the queued archives freshly
    (`belowFloorArchiveKeys`: same plan, same estimator, same floor as the
    drain — restart-accurate immediately, unlike the in-memory registry)
    and reports 'N closed below floor' apart from 'N folding'; the default
    zero keeps the legacy byte-stable rendering
  - unannotated rows (registry empty right after a restart, before the
    drain's first pass) keep the legacy 'folding…' label — one boundary of
    old semantics at worst

## 0.37.2 — dsh 0.2.0-rc.2: re-verified, no plugin change (2026-09-30)

A same-minor lockstep bump: the whole dsh monorepo moved 0.2.0-rc.1 →
0.2.0-rc.2 with the package set unchanged (no additions, no removals), so this
release is a re-verification, not a migration. The compatibility gate re-checked
with the real `evaluatePluginCompatibility` on 0.2.0-rc.2: the existing
`^0.2.0-rc.1` peer floor already covers rc.1 through the 0.2.0 final — v0.37.1
loads as-is, no code or manifest change. Verification chain against the real
0.2.0-rc.2 packages: host-API shape probes (`BasicCompactionEngine` default
export with `compactRegion` on the prototype, `BlockAssembler` export,
schemastery CJS entry); the client-contract sweep
(`conversation.input.dock`, `plugins.bundle.config`, configForms
`whileServed`, `window.__ModuleLoader__`, `SettingsFormModel`,
`settingsNumberField` — all present); an end-to-end probe host whose
`settings/describe` serves `cmpct-region` at
`{"minSpanTokens":2000,"showTaskBar":true}` with `applies: live`; and the
offline suite — 14 suites, 208 tests, 0 fail. Both READMEs' "Supported dsh
versions" records updated (superseded rc.1 entry replaced; dist-tag note:
`0.2.0-rc.2` now under both `latest` and `next`).

  - record 0.2.0-rc.2 as the verified rc-channel host in README.md + README.zh.md

## 0.37.1 — dsh 0.2.0-rc.1: peer floor bump, verified (2026-09-30)

The dsh monorepo jumped 0.1.7-rc.2 → 0.2.0-rc.1 in lockstep (no package
removals; five new telemetry/log packages). The compatibility gate rejects the
0.37.0 manifest on the new host — `^0.1.7-rc.1`'s semver upper bound `<0.2.0`
excludes the new minor — so this release bumps all three peerDependencies
(`@deepseek-ai/dsh`, `@deepseek-ai/dsh-compaction-basic`, `@deepseek-ai/dsh-llm`)
to `^0.2.0-rc.1`. Hosts still on 0.1.7-rc.* keep using v0.37.0, whose floor
matches them. Verification on the real 0.2.0-rc.1 packages: the gate probe
(`evaluatePluginCompatibility`: old range rejected, bumped range passes);
host-API shape probes (`BasicCompactionEngine` default export with
`compactRegion` on the prototype, `BlockAssembler` export, schemastery CJS
entry); a client-contract sweep (`conversation.input.dock`,
`plugins.bundle.config`, configForms `whileServed`, `window.__ModuleLoader__`,
`SettingsFormModel`, `settingsNumberField` — all present); an end-to-end probe
host whose `settings/describe` serves `cmpct-region` at
`{"minSpanTokens":2000,"showTaskBar":true}` with `applies: live`; and the
offline suite — 14 suites, 208 tests, 0 fail. Both READMEs' "Supported dsh
versions" records updated (superseded 0.1.7-rc.2 entry replaced per the
one-entry-per-channel convention; dist-tag note: `0.2.0-rc.1` under `next`,
`latest` still 0.1.7-rc.2).

  - bump dsh peerDependencies floors to ^0.2.0-rc.1 (all three host-surface packages)
  - record 0.2.0-rc.1 as the verified rc-channel host in README.md + README.zh.md

## 0.37.0 — user-configurable fold floor on the Plugins page: `minSpanTokens` (2026-09-28)

Issue #2: folding a span too small to pay for itself wastes tokens — the
summarization call (plus its prompt overhead) can approach the span's own
size, and for tiny tasks the fold costs nearly what it saves. This release
adds the floor as a first-class plugin setting: the mounted row exports a
Config schema (attached to the plugin object itself — the loader's
`unwrapExports` keeps only `exports.default`, so a named `export { Config }`
never reaches `runtime.Config`) whose volatile `minSpanTokens` field the
dsh-settings service exposes, validates, and persists through the active
profile's Cordis patch. The shipped clients do NOT auto-render schemas
(`autoGenerate` has no schema-driven client yet), so the browser half now
ships its own "Taskfold" card — `plugins/fold-settings-ui.mjs`, inlined into
the client bundle — that binds the namespace through `ctx.configForms` and
registers into the Plugins page's `plugins.bundle.config` slot (keyed by the
bundle's package name; the official-group `plugins.item` slot stays reserved
for first-party companions) while the Host serves it. No environment
variables, no restart:

- The settings card also stages a second volatile field, `showTaskBar`
  (default true): a Switch on the same card hides the task-stack dock beside
  the conversation input. The dock registers unconditionally at mount and the
  card's namespace watch hides/restores it live — an unserved namespace or a
  missing settings service always means visible, so the dock keeps working on
  deployments without ui-settings.
- Every user-facing surface is now localized (en/zh): the settings card and
  the task-stack dock read their copy through the locale service (dictionaries
  `settings.taskfold` / `ui.taskfold`; the dock's slot registration declares
  its namespace so the runtime dispatches a bound reader, with the English
  dictionary as the built-in fallback for deployments without ui-settings),
  and the package ships `locale/en.json` + `locale/zh.json` so the Plugins
  page resolves the bundle's title/description per language instead of always
  falling back to the English `package.json` description.
- The browser bundle moved into its own nested package `client/`
  (`dsh-taskfold-client`, one dedicated `taskfold-client` Loader row in
  cordis.patch.yml): client-modules requires every package declaring
  `dsh.client` to have exactly ONE owning Loader row, and with the two
  legacy-stable host rows (cmpct-region/cmpct-stats) both resolving to the
  root package it threw "resolves from multiple active Loader sources" and
  dropped the whole browser half — dock included — from the boot graph. The
  root package no longer declares `dsh.client`; the bundle is generated to
  `client/taskfold-client.mjs`.

- `minSpanTokens` (default `2000` — the measured break-even, see
  `docs/fold-floor.md`; `0` = fold everything, the legacy behavior): the
  minimum number of ESTIMATED TOKENS a planned region must
  carry before a summarization call is billed. Below the floor the drain
  settles the archive BEFORE building the engine — zero LLM work, tooSmall
  semantics. The floor is re-read from the volatile config ref at every
  drain pass, so a Settings edit applies at the next step boundary — and
  LOWERING the floor reopens spans that already settled unfolded under
  the higher one (their original content is still on the surface); other
  settles remain final.
- New pure module `plugins/fold-settings.mjs` (`foldFloorFromConfig`,
  `estimateTokens`, `spanTokenEstimate`, `belowFoldFloor`): unwrapping is
  total — an undefined config, a missing field, a throwing ref, or an
  invalid value (non-integer, negative, absurdly large) all fall back to
  the default floor (`2000`), never throws, so a bad edit can never
  silently disable the floor (the drain resolves its per-pass floor
  through the same `foldFloorFromConfig`, one normalization everywhere).
  The estimate is POSITIONAL over the surface (which is not seq-ordered),
  CJK-aware (`0.75` token per CJK char — Han, kana, and Hangul syllables
  alike — and `4` chars per token otherwise; the rate constants already
  embed the calibration: against 194 measured `shadowedTokenCount` values
  from two weeks of live sessions the estimates read ~7% low, a
  conservative direction), and an unmeasurable span is never
  skipped: the floor protects against wasted calls, not against folds.
  (Revision on the same unreleased entry: an earlier draft counted
  SURFACE NODES — `minSpanNodes` — because span tokens were assumed
  unknowable before the call; the token heuristic plus the calibration
  data reversed that.)
- schemastery (the Config schema library) cannot be imported statically —
  the installed copy sits in a harness-owned node_modules tree plain ESM
  resolution never crosses (the same constraint fold-engine.mjs resolves
  for dsh-compaction-basic). The mounted row requires its CJS build
  synchronously from a harness anchor instead; when no anchor resolves,
  `Config` stays undefined and the plugin mounts exactly as before (the
  form is simply absent, the floor keeps its default).
- Review hardening (two-agent review of the branch before release):
  the dock's slot registration no longer declares `locale:` (scoped
  slots throw on locale-declaring entries when no locale face is
  installed — the dock must render on every deployment); its
  dictionaries register in a locale-only fiber (previously gated behind
  `configForms`, so a locale-without-settings host printed raw keys) and
  the reader reaches the dock through a lifetime-managed ref with
  per-call English fallback inside the model. The estimator's CJK
  classes gained Hangul syllables and dropped the per-character regex
  (char-code range checks); the settings-card's dead `summary` view and
  copy keys are gone; the release script bumps and stages the nested
  `client/package.json` in lockstep with the root; the published package
  now ships `docs/` (README/settings-card links to `docs/fold-floor.md`
  resolve inside the tarball) and drops the unused `./client-package`
  export.

Offline suite: 14 suites, 208 tests, 0 fail (fold-drain floor tests: config/
volatile-ref totality, the CJK-aware estimator incl. Hangul, positional
token estimation/unmeasurable-span pass-through, an end-to-end
below-floor settle with zero `compactRegion` attempts, a live
settings-getter edit applying at the next pass, idempotent
reopen/re-settle with no un-settle on raise, and drain-level malformed-
settings normalization; client-bundle tests additionally pin the
locale-free dock registration + locale-only dictionary fiber and the
en/zh key parity of both dictionaries).

- **Features**
  - user-configurable fold floor on the Settings page: `minSpanTokens` closes spans below the bound unfolded, before any summarization call is billed; edits apply live, and lowering the floor reopens previously settled spans

## 0.36.0 — declare dsh peerDependencies so the host compatibility gate has a range to check (^0.1.7-rc.1) (2026-09-26)

dsh 0.1.7 hosts check a bundle's `peerDependencies` against the running
harness version at install and at startup/recomposition; until now this
plugin declared nothing, so every host — compatible or not — loaded it
unconditionally. This release declares the whole host-coupling surface:
`@deepseek-ai/dsh`, `@deepseek-ai/dsh-compaction-basic` (the engine base
class), and `@deepseek-ai/dsh-llm` (BlockAssembler), all `^0.1.7-rc.1`.

The floor sits on the oldest verified rc build deliberately: semver ranks
a prerelease below its own release (`0.1.7-rc.1 < 0.1.7`), so a naive
`^0.1.7` would reject the `0.1.7-rc.*` hosts this plugin is verified on.
Checked with the real `evaluatePluginCompatibility` on 0.1.7-rc.2: this
manifest passes, while a `^0.1.7` variant is correctly rejected with the
allow-version exemption hint. Offline suite: 14 suites, 0 fail. The
READMEs' bounds note now records the enforced floor. (The alpha branch
carries the same declaration anchored at `^0.1.7-alpha.2` — its own
verified floor — released separately on that branch.)

- **Features**
  - declare dsh peerDependencies so the host compatibility gate has a range to check (^0.1.7-rc.1)
- **Docs**
  - the peer floor is enforced on 0.1.7 hosts - rewrite the bounds note for the peerDependencies declaration

## 0.35.2 — the alpha channel's functional line, merged into the rc channel (2026-09-24)

Since the channel split, every functional fix had landed only on alpha
(0.34.3–0.35.1) while the rc channel kept verifying and releasing on the
0.34.2-era code. This release ports alpha's whole functional line onto master
— 14 commits picked with `-x` provenance, channel docs resolved to master's
own — so the rc build now carries everything the alpha build does.

- **What came over**
  - task-mark execute-time enforcement: one task-mark call per message, and
    the standalone-call contract explicit in every prompt layer
  - fold-drain hardening: the deferred archive END extended past parallel
    close-call results; unsettled commits back off geometrically and chain
    starved drain calls; the summarization guard raised 120s → 300s
  - fold fixes: bounded preamble tolerance, build-time assembler fail-fast,
    second-chance summary settle, stale HOLD filter
  - prompt corrections: stale semantics, end-authority conflicts, standing
    tokens trimmed; the handoff shape no longer ends the turn
  - dsh 0.1.6-alpha.2 seam adaptations in fold-engine: recover-loop waterfall
    + routed-config spread
  - session format v4: shape-independent tool-result extraction, reducer and
    archive-plan guards on v4 result linkage, v4 tool-role previews with
    retiring fold titles
  - the master-v0.34.2 twin content alpha's v0.35.1 release had absorbed
    (task-stack-ui, taskfold-client)
  - tests: both grammars pinned — v4 round trips, plan guards, previews,
    legacy titles
- **The merge is byte-exact.** `git diff 5755cc7 HEAD -- plugins test` is
  empty — this branch's functional tree is identical to alpha's HEAD.
- **Verified on dsh 0.1.7-rc.1** (README record updated):
  - offline: 14 suites, 196 tests, 0 fail (up from 13/167 — the
    prompt-contract suite and the grammar pins came with the merge)
  - a host-API probe against the real rc.1 packages: 12/12
  - live on the running rc.1 host: 6/6 folds pass `verify-cache
    --since-restart` (93.9–96.3% prefix-cache hits), produced by the mounted
    copy whose plugins files hash-match alpha's v0.34.7 release commit — the
    same functional code this release carries, exercised live
- **Version 0.35.2** sorts above alpha's 0.35.1 (whose tag never landed) and
  keeps the channels' shared tag namespace collision-free; the host-jump
  audit behind `0.1.7-rc.1` stands as recorded under 0.34.8.

## 0.34.8 — dsh 0.1.7-rc.1 verified, and channel-scoped release baselines (2026-09-24)

dsh's rc line jumped two hosts at once — from `0.1.5-rc.2` (the rc channel's
last recorded host) to `0.1.7-rc.1` — so this verification round audited the
larger step and found no plugin change needed. The same round fixed the release
flow for a problem the channel split had left behind: master could not draft at
all, because the script's "latest tag" was branch-global while the tag
namespace is shared by both channels.

- **Host upgrade verified — no plugin change.**
  - the offline suite — 13 suites, 167 tests, 0 fail
  - a byte-level audit of all 277 first-party `@deepseek-ai` packages common to
    the 0.1.7-alpha.2 and 0.1.7-rc.1 install trees (alpha.2 being the last host
    either channel of this plugin had live-verified): no package added or
    removed, 221 differ only in their `package.json` version fields, 56 change
    implementation files
  - of the packages this plugin couples to, `dsh-compaction-basic`,
    `dsh-session`, and `dsh-agent-loop` are byte-identical in implementation,
    and `dsh-llm`'s only change is `lib/typert.host.js` (TYPERT type-registry
    declarations, the `Team*` typing entries) while `lib/index.js` —
    `BlockAssembler`, the chunk grammar, the export line — is byte-identical
  - a host-API probe against the real rc.1 packages with this branch's own
    `fold-engine` copy: engine class export, its `summarize` prototype,
    ScopedEngine subclassing the actual rc.1 base class, `BlockAssembler` with
    `push`/`blocks`, and the end-to-end fold path (prefix-anchored envelope
    with host-head dedup, exactly one system message, the constructed heading
    with its fenced line-numbered span index, usage passthrough) — 12/12
  - a client-contract sweep: `conversation.input.dock`, `useProjection`,
    `__ModuleLoader__`, and `slots.inject` occurrence-identical across both
    trees (8 / 14 / 11 / 10), with the dock-registration lines themselves
    character-identical inside the changed owner `dsh-client-ui-conversation`
    and the bundle registry `dsh-client-modules` byte-identical
  - live, on the running 0.1.7-rc.1 host: 2/2 folds pass
    `verify-cache --since-restart` (96.3% and 96.2% prefix-cache hits) — folded
    by the mounted alpha-channel copy (0.35.1); this branch's own code is
    covered by the probe and the suite above
  - one dist-tag note: `0.1.7-rc.1` ships under `next` while `latest` still
    resolves to `0.1.5-rc.3`, so a bare `npx @deepseek-ai/dsh web` keeps
    running rc.3 — only `@0.1.7-rc.1` (or `@next`) reaches the new build
- **Release flow: the tag baseline is now per-branch, and taken versions are
  refused at draft.** `draft` on master died with "latest tag v0.34.7 is not
  an ancestor of HEAD": `latestTag()` was branch-global, and the channel split
  shares one tag namespace — alpha owns `v0.34.3`–`v0.34.7`, and, from before
  the split, the `v0.34.2` tag itself, while master's own 0.34.2 release commit
  is untagged (a version twin). Now `latestTag()` lists only tags reachable
  from HEAD (`--merged`), and `baselineTag()` lets the other branch's tag stand
  in as this branch's baseline exactly when the branch's own newest tag is
  below `package.json` and the missing version is tagged somewhere — globally
  unique, exactly one tag, just not here. A reachable tag above package.json,
  or a version below it with no tag anywhere, stays a rejection; the ancestry
  tripwire now guards only the branch's own tag. `draft` also refuses a target
  version whose tag already exists ("pass --version with a free number")
  instead of dying at `git tag` deep inside `release` — which is why this
  release is 0.34.8: 0.34.3–0.34.7 are alpha's, and 0.34.8 is master's next
  free number below alpha's 0.35.x line.

**Verification** — offline suite 13 suites / 167 tests / 0 fail; host-API
probe 12/12 against the real rc.1 packages; live `verify-cache
--since-restart` 2/2 folds pass (96.3% / 96.2% prefix-cache hits) on the
running 0.1.7-rc.1 host; the taken-version guard probed both ways (0.34.3
refused with exit 1, 0.34.8 accepted).

## 0.34.2 — agent-level fold single flight + resume-linkage hardening, backported from the alpha channel (2026-09-23)

Two live-verified fixes backported from the alpha channel's 0.35.0/0.35.1
releases, adapted to this rc baseline (the alpha incident data is quoted
verbatim for the record):

- **fold drain: agent-level single flight.** The drain's running guard is now
  keyed by session id instead of a process-wide boolean, and the backoff
  clock (`drainPass`) became per-session — one session's fold no longer makes
  any other session's drain wait, and one session's step boundaries no longer
  advance another session's backoff schedule. Origin (alpha, dsh
  0.1.7-alpha.2): a subagent session whose only two drain opportunities (the
  pre-step and turn-stopping hooks of its final step) both fell inside a
  sibling session's 37.5 s fold window; the starved retry needed a step
  boundary that a settled subagent never produces, so the archive row stayed
  queued forever. Sessions in one process now fold concurrently; every path
  they touch is per-session by construction (closingTasks is keyed by session
  id, artifacts are written per session, the durable event-log lock is per
  session). Same-session reentry — theoretical, the host loop serializes one
  session's dispatch — merges into one chained pass instead of being dropped.
- **taskMarks: shape-independent tool-result linkage + stateVersion 11.**
  `toolResultEntries` (new in events.mjs) reads every event grammar: v3
  block-level `tool-result` blocks, v4 message-level `toolCallId` on the
  tool-role message, and the resume-derived shape where the message keeps
  only `source.callId`. The reducer and the archive-plan guards pair through
  it. Origin (live on dsh 0.1.7-alpha.2): sessions re-opened after a restart
  fed the projection resume-derived events; every mark call/result pair
  failed to pair and the dock showed one stuck `opening…`/`closing…` row per
  call ever made (19 rows on one session, 40 on another), with the poisoned
  state checkpointing itself back into the projection cache. stateVersion 11
  discards those persisted v10 rows; `source.callId` is accepted only for
  `source.kind === 'tool'`, so a source-bearing non-tool message can never
  masquerade as a result.
- **task banner: the spinner glyph is gone.** The rotating glyph said
  "animation" where the rail dot, the bold name, and the `folding…` suffix
  already carry the state; static, it read as a broken spinner. Status
  expression is unchanged otherwise.

## 0.34.1 — dsh 0.1.5-rc.2 verified, and the release flow publishes its own tarball (2026-09-11)

dsh moved from `0.1.5-rc.1` to `0.1.5-rc.2`, and this round verified the plugin
against the new host: no code change was needed. The same round taught the release
flow to publish the asset it documents, and taught the repository to keep its
working tree byte-equal to its committed blobs — so the tarball it attaches is
reproducible. One dist-tag gotcha is worth recording: `0.1.5-rc.2` shipped under
`next` while `latest` still resolves to `0.1.5-rc.1`, so a bare
`npx @deepseek-ai/dsh web` keeps running rc.1 — only an explicit `@0.1.5-rc.2`
(or `@next`) reaches the new build.

- **Host upgrade 0.1.5-rc.1 → 0.1.5-rc.2 verified — no plugin change**
  - a byte-level seam audit of all 240 first-party `@deepseek-ai` packages between
    the two install roots: 231 differ only in their `package.json` version fields,
    7 change implementation files (the message-feedback group —
    `dsh-client-ui-message-feedback`, `dsh-message-feedback`,
    `dsh-command-feedback`, `dsh-client-ui-chat`, `dsh-client-ui-deliverables`,
    `dsh-client-ui-sidebar` — plus the `dsh-web-frontend` shell), no package is
    added or removed, and no package changed content while keeping its version
  - the two packages this plugin imports — `dsh-compaction-basic` and `dsh-llm`,
    the only `importHostPackage` calls in `fold-engine.mjs` — are byte-identical in
    implementation, and a host-API probe against the real rc.2 packages confirms
    the engine class export, its `summarize` prototype, subclassing, and
    `BlockAssembler` with `push`/`blocks`
  - the client contract is unchanged: a sweep of `conversation.input.dock`,
    `useProjection`, `__ModuleLoader__`, and `slots.inject` across both builds is
    identical, and the dock's owner (`dsh-client-ui-conversation`), the reference
    consumer (`dsh-client-ui-goal`), and the bundle registry
    (`dsh-client-modules`) are byte-identical — three packages that never even
    enter the seven-package delta
  - live, on the running 0.1.5-rc.2 host: the `taskMarks` projection rebuilds
    after restart (`ver: 10`, `pendingArchives: []`, open tasks in stack order) and
    the first fold passes `verify-cache --since-restart` (98.6% prefix-cache hit);
    the mounted plugin copy is byte-identical to this repo's `v0.34.0` tag blobs
    (12/12 plugin files)

- **Release publishes its own tarball.** `release` now creates the GitHub Release
  and attaches `dsh-taskfold-<version>.tgz`; a new `assets [--version X.Y.Z]`
  subcommand publishes or repairs the asset for an existing tag. That subcommand
  exits non-zero on failure, while the in-`release` attempt only warns — by then the
  commit, tag, and push have already landed, and a missing attachment must not read
  as a failed release. Portability was probed rather than assumed: `npm` is spawned
  as `process.execPath` + `npm-cli.js` (a bare `npm` is `ENOENT` on Windows without
  a shell, `npm.cmd` is `EINVAL`, and `shell: true` triggers DEP0190), and `gh` is
  resolved from `PATH` first, then the standard install locations.
  - `v0.34.0` was backfilled this way; it carries `dsh-taskfold-0.34.0.tgz`
    (106,183 B), verified by unpacking the asset and comparing every file against
    `git cat-file blob v0.34.0:<path>` (18 files; at the time 12 differed only by
    CRLF — see the next bullet)

- **Release assets are byte-reproducible.** `.gitattributes` (`* text=auto eol=lf`,
  plus explicit `binary` marks) closes a gap that made every attachment differ from
  its tag blobs: the blobs were already LF, but `core.autocrlf=true` had written
  CRLF into the working tree, and `npm pack` packs the working tree rather than the
  blobs. Repairing an existing checkout needs the renormalize sequence
  (`git add --renormalize .` → commit → `git rm --cached -r .` → `git reset
  --hard`); `git checkout-index -a -f` does **not** rewrite working-tree line
  endings. Verified: 18/18 packed files byte-identical to the committed blobs
  (previously 6/18) and zero `w/crlf` across the 46 tracked files.

- **Docs moved into the repo.** The design notes and ADRs had never been
  version-controlled — the workspace `docs/` tree was not inside any git
  repository. They now live in `docs/README.md` (index), `docs/design/` (13 notes),
  and `docs/adr/` (2 decisions), still outside the npm `files` whitelist. The move
  repaired two references `CHANGELOG.md` had been making all along, to
  `docs/design/deferred-report-fold.md` and `docs/design/lazy-fold.md`. The two
  historical notes stay at their flat `docs/*.md` paths because the changelog cites
  them there.

**Verification** — offline suite 12 suites / 162 tests / 0 fail; the seam audit and
client-contract sweep above; live `verify-cache --since-restart` 1/1 pass at 98.6%
prefix-cache hit on the running 0.1.5-rc.2 host; mounted plugin copy byte-identical
to the `v0.34.0` tag blobs.

## 0.34.0 — the live task stack in a Web GUI dock, and archive closure on the close-result witness (2026-09-10)

An open-task stack was only ever visible as prose: every `task_begin`/`task_end`
result names the depth, and the lifecycle hints name the task they nag about —
but nothing showed the STACK itself. Meanwhile a real session was carrying six
permanent `folding…` rows for tasks that had ended long before: the reducer
closed a queued archive only when a committed fold shadowed its BEGIN anchor,
and a fold region deliberately STARTS AFTER the `Task begun` result so that
anchor always survives — the witness could never fire, and a restart replayed to
the same verdict. Both halves are fixed here, and the dock is what made the
second one visible at all.

- **The open-task stack is now a live dock above the composer.** `taskMarks`
  gains a `wire` view (mirroring the `todos` projection) so the browser can read
  it, and `plugins/task-stack-ui.mjs` renders it: outermost task first, the
  innermost highlighted, one row per closing task, pending `task_begin` /
  `task_end` calls as counts, and NOTHING at all when the stack is empty. Every
  number comes from the session's own projection — the dock appends no events
  and costs the model no context. Layout is the host's composer-card recipe
  (aligned width, hairline border, 13px rows) with the stylesheet injected by
  the bundle, and the panel collapses to its one-line summary on click.
- **`stateVersion` 9 → 10: archives close on the close RESULT, not only the
  begin anchor.** A committed fold shadows a range that ENDS AT the close result
  and starts after the `Task begun` result, so the close result is the witness
  every successful fold leaves behind. Either witness present now drops the row
  (rows persisted before `foldResultSeq` existed keep the anchor-only rule), and
  the version bump forces the full replay that converges sessions already
  carrying ghost rows — observed live: six `folding…` rows before, `[]` after,
  with the one genuinely open task untouched.
- **The drain settles in the pass that removed the row — and only then.** A fold
  whose region had to shrink BELOW the close result leaves the row queued AND
  unsettled on purpose: unconditional settling would have eaten the "shrink at
  an END boundary needs a re-plan" semantics and stopped the walk early.
- **The lifecycle hint now carries the whole stack on one line:**
  `Task lifecycle: task stack — 3 open, outermost first: "a" > "b" > "c";
  2 folding, 1 end pending.` (or `empty`). Shape only — names in stack order
  plus the counts, no round ages and no seqs — because the hint channel compares
  published text verbatim: a number that drifted per round would re-inject
  context every round. It is appended to a live hint and never emitted as a
  standing state line, so it re-publishes exactly when the stack moves.
- **The browser bundle stays a COMMITTED artifact with a freshness gate.**
  `plugins/task-stack-ui.mjs` is the single source of truth;
  `scripts/build-client.mjs` strips its ESM keywords and splices it into
  `scripts/taskfold-client.template.mjs`, emitting `plugins/taskfold-client.mjs`
  as a loader-factory classic script — no bundler, no build step in the repo.
  `test/client-bundle.test.mjs` byte-compares the committed file against a fresh
  render, and the release flow refuses to draft or publish a stale one.
- **Fix — the release freshness guard never actually ran.** It called
  `clientBundlePath()` / `renderBundle()` with no arguments while only
  `buildClient` carried the repo-root default, so it threw a TypeError instead
  of comparing anything. Both sides now default to this repo through
  `repoBundleText(root)`, and two tests cover the exact default-root call the
  release makes plus a stale artifact in a temp tree, which must fail with
  "is stale" rather than crash.
- **Manifest:** `exports["./client"]` + `dsh.client.platform = "web"` make the
  bundle discoverable, while `"."` and `"./plugins/*"` keep the existing
  path-mounted rows and legacy subpath imports working.
- **Docs:** two store screenshots — `assets/screenshot-tasks.png` and
  `assets/screenshot-tasks-collapsed.png`, both 1280×720 — rendered from the
  shipped component with the host's real theme tokens, listed in
  `screenshots.json` and embedded in both READMEs.
- Live verification on the running host (dsh 0.1.5-rc.1): the persisted
  projection cache shows `taskMarks` at `ver: 10` with `pendingArchives: []`;
  the lifecycle event renders the new stack line (byte-stable across two
  consecutive injections); the served client bundle is byte-identical to the
  committed artifact. `verify-cache --since-restart` judges the folds committed
  after the session's restart: **2/2 pass**, prefix-cache hit 96.5% and 96.2%
  (uncached 2,309 / 1,918 tokens against spans of 20,254 / 3,390). Offline
  suite: **156 tests, 12 suites, 0 fail**.

## 0.33.0 — escalate close-pressure nudges in 20+/50+/100+ buckets (2026-09-10)

Live evidence (a real session): a mark stayed innermost for 200+ rounds;
the byte-stable "20+" line fired ONCE at round 20 and the publish latch
then suppressed every later render for 74 minutes — the nudge system went
silent exactly while the situation kept worsening, and the task was finally
closed by the user, not by a hint. The design says "a hint is an EVENT, not
a state display", but the only event detector is the text transition — and
past round 21 the text never changed again. Exact-age wording would fix the
philosophy and spam one injection per round; buckets fix it with one event
per meaningful milestone.

- **Close pressure escalates in buckets.** `closePressureLine(name, age)`
  renders "20+", "50+" or "100+ rounds" — byte-stable WITHIN each bucket
  (one event per milestone, never per-round spam) and text-distinct ACROSS
  them, so the existing publish latch naturally re-fires at every bucket
  crossing. An omitted/garbage `age` falls back to the lowest bucket, keeping
  old callers byte-compatible.
- **`taskAgeRounds` cap 21 → 101.** The cap bounds the backward scan AND
  stabilized the old wording; 101 keeps both properties while feeding the
  "100+" bucket the only precision it needs.
- **`CLOSE_PRESSURE_MIN_ROUNDS` (20) exported** and used by the renderer's
  threshold check, next to the exported decompose-window constants.
- Tests: bucket boundaries (49 vs 50, 99 vs 100) are text transitions,
  in-bucket byte-stability, undefined-age fallback, no drifting numbers,
  and the 8–19 decompose window still hands off cleanly at 20.

Riding along since 0.32.1:

- **Docs**
  - correct the 0.32.1 fold-region attribution and document the span sweep

## 0.32.1 — fold regions follow surface POSITION, and one coordinate for index, artifact and recall (2026-09-10)

Review pass over every plugin module, script and test against a live session
log; five real defects and a retry-budget hole. No envelope change: the
summarization REQUEST is byte-identical, so the cache-verification numbers are
unaffected.

- **Fix (high) — the fold region was resolved by seq MAGNITUDE instead of
  surface POSITION.** `surface.nodes` is a position list, not a sorted one: a
  committed fold re-inserts its summary node AT the position of the region it
  shadowed while that node carries a seq from the log's END. `deferredArchivePlan`
  picked the start as `min{ s : floor < s < close }` and `foldRegion` picked the
  shrink target as `max{ s : start <= s < end }`, so on any post-fold surface
  both could land EARLIER on the surface than the intended node. Live evidence
  (this session, fold #3 at seq 240): its region opened at 183 — an EARLIER
  fold's resident summary node, three nodes ahead of where it belonged —
  swallowing the task's own `task_begin` call (with its opening reasoning), the
  `Task begun` result and that earlier fold's summary node: exactly the "the
  bookmark stays live" promise made by the README, both tool descriptions and
  `docs/scoped-summary-acceptance.md`. Both sites now resolve the region by
  INDEX (`posOf`, `nodes[floorPos + 1]`, `endPos -= 1` with `endPos`/`startPos`
  guards), and the drain's walk is index-driven end to end (the old
  `end >= startSeq` NUMERIC loop guard could stop a walk that still had room —
  silently closing a task unfolded through the tooSmall path). Seq comparisons
  remain only over the event log, which is seq-ordered by construction. Both
  halves were then re-checked against the session log's own record of that fold:
  replayed over the recorded region
  (`shadowedSeqs = [183,177,179,187,…,220,214,216]`) the OLD numeric scan
  returns startSeq 183 — bit-identical to the region the host actually
  committed, which is the bug reproduced from data — while the fixed positional
  scan returns 187, the node immediately after the `Task begun` result.
  Regression tests drive a real post-fold surface both in the plan and through
  the drain's shrink walk.
  **CORRECTION (post-release re-audit, dsh 0.1.5-rc.1, 2026-09-10)** — the first
  published version of this entry described the damage more broadly, listing a
  nested subtask's begin pair and two already-committed summary nodes as
  swallowed and calling the summary cross-task contaminated. The correct
  criterion is "is the region's first node positionally BEFORE this fold's own
  `Task begun` result"; replayed against all 15 folds of that session log,
  exactly ONE fold met it — this one — and its bug-caused prefix is the three
  nodes above. The nested subtask's begin pair and a later fold's resident
  summary node lie AFTER this fold's own begin result, so ANY
  position-contiguous region contains them: that is the by-design span sweep,
  now documented in `docs/scoped-summary-acceptance.md`, not fallout of this
  bug. The fix itself is unaffected and was re-verified live on 0.1.5-rc.1:
  0.32.1-produced folds carry no pre-begin prefix, and
  `verify-cache --since-restart` passes 4/4.
- **Fix (high) — the span index, the artifact and the resident footer were
  numbered in the REQUEST's coordinate while `fold_recall` rebuilds the SPAN's.**
  The host prepends the surface-head system prompt into `input.messages`
  (dsh >= 0.1.5-alpha.1), and all three were rendered from `input.messages`,
  so every artifact carried one extra leading message (measured 36/5/14 lines
  against 35/4/13 shadowed seqs, first line always `role: system`). A model
  copying a printed `L<N>` therefore pointed at a line that was neither in its
  own span nor the same line `fold_recall({ fold, line: N })` returns, and
  `preview line N = artifact line N` — the contract in
  `docs/scoped-summary-acceptance.md` — was false on the recall side. New pure
  helper `spanMessagesFor()` recomputes the commit's own slice
  (`nodes[startIdx..endIdx]` projected per event, exactly what the host
  validates as `shadowedSeqs` and what `fold_recall` rebuilds) and falls back
  to the deduped request span when there is no closing declaration. All four
  sites — instruction index, artifact, footer, message-count bullet — now use
  it, so fold-time output and recall-time regeneration are the same
  construction.
- **Fix — the retry loop was unbounded and undiagnosable.** One fold attempt is
  a whole summarization call (30–70 s); a deterministic failure (structure
  receipt, missing host API, provider refusal) was re-attempted at EVERY step
  boundary forever, with no cap and no backoff. `classifyCategory` collected the
  error's own message and then threw it away, so the HOLD line named a bare
  category that appeared nowhere in the log. Now: consecutive failures back off
  geometrically (1, 2, 4, 8 boundaries), past `MAX_FOLD_ATTEMPTS` (5) the entry
  still retries — never abandoned silently — but only once per
  `GIVE_UP_PASSES` (200) boundaries, and the failure line carries
  `"<bucket>, attempt N: <error message>"`. A `cancelled` fold (an interrupted
  or superseded turn — the normal shape of an Esc) stays quiet on its first
  occurrence and is surfaced only if cancellation repeats. Also guarded
  `agent.session.requestHeader()` in the engine: an unguarded throw there was a
  deterministic failure in the loop above.
- **Fix — small, each found by reading against a live log.** `foldOf(null)`
  threw a TypeError while its doc promised "defensive on every field" (it is an
  exported pure helper; `collectFolds` merely hid it). Artifact filenames used
  `Date.now().toString(36)` alone, so two writers in one millisecond
  overwrote each other; they now carry a random tail. `settledArchives`,
  `autoFoldFailures`, the new `autoFoldAttempts` and `compact-region`'s
  `lifecycleLatch` are keyed by session id and never shrank (a long-lived host
  serves one session per subagent) — all four are now evicted past
  `MAX_TRACKED_SESSIONS` / 200. `scripts/verify-cache.mjs`: `resolveLog`'s
  `readdirSync` calls are guarded (a missing sessions root must be the clean
  "no session log found", exit 2, not an ENOENT stack) and every numeric flag is
  validated — `--last abc` was `Number('abc')` → NaN → `slice(-NaN)` →
  `slice(0)`, i.e. it judged EVERY fold while looking scoped.
- **Docs**: `plugins/compact-stats.mjs` and `docs/design-compact-stats.md`
  described the pre-0.15 in-flight `task_fold` title correlation as if it were
  the live path; it is unreachable for any current log (fold titles come from
  the constructed `# <name>` heading, AUTO checkpoints stay untitled and list by
  their preview), so both now say so and the tests pin the live path as well as
  the legacy replay. Also corrected the stale `## Primary Request and Intent`
  and `task_fold` references in the stats module. `CHANGELOG.md` joins the
  published `files` list (the README links it).
- **Tests**: 131 offline assertions pass (was ~120), including new
  discriminating cases for the positional region start and END-shrink walk, the
  span coordinate (a numerically-below-start node that IS span content is
  included; out-of-position summary nodes are not), the retry budget/backoff and
  failure naming, cancellation quietness, `foldOf` totality, the AUTO-vs-titled
  listing, and `parseArgs` value validation.

## 0.32.0 — message gate - fold at the first assistant message after the close, drop the unreachable successor-anchor defer (2026-09-10)

- **Behavior (product owner ruling, superseding v0.14's G2)**: the fold
  gate now opens at the FIRST assistant message that follows the close
  result — any content counts (report text, a tool-call-only step,
  reasoning-only). The old text-only requirement held folds open through
  the common handoff shape — end task A, immediately `task_begin` task B,
  deliver A's report later — where A's gate stayed shut waiting for text
  that only landed deep inside B's span (found live on the MasterGoUI
  session: the 插件侧源码审查 → up 仓库与测试脚本审查 handoff folded two
  steps late, only after B's own fold). The lifecycle discipline still
  directs the model to deliver the report in that message; the gate only
  verifies the message exists. Tool copy and the system-prompt section
  updated to the new mechanics ("make the next message the report — the
  fold fires as soon as it lands").
- **Removed**: the successor-anchor defer (gate ②) and the drain's
  successor-anchor computation, together with the `successorAnchors`
  parameter of `deferredArchivePlan`. History: under v0.14's design the
  region ran to the last surface node trimmed AT the first still-open/
  pending successor anchor, so the defer protected the deliverable from
  being stranded outside the span. v0.16.0 pinned the region to
  begin..close exactly (deliverable stays on the surface; a later task's
  region sweeps it), voiding that rationale — and since anchors are
  begin-message seqs, the first post-close assistant message can never
  sit after the earliest successor anchor, making the defer branch
  unreachable. This also subsumes the 0.31.2 ghost-row filter's only
  consumer; the settled-row skip when picking entries remains.
- **Tests**: gate cases rewritten for the message semantics (wait only
  when NO assistant message follows the close; tool-call-only and
  whitespace-only messages open it); drain tests reshaped — the headline
  case is exactly the handoff shape above (elder folds in the same pass
  as the successor, no defer), plus wait-skip and next-pass retry.
  120/120 offline tests pass.

## 0.31.2 — exclude settled ghost rows from successor anchors so the drain cannot starve (2026-09-10)

- **Five closed tasks on one real session never folded; the drain now
  unblocks them all.** Root cause, confirmed by replaying the session log
  through the plugin's own pure functions: a committed fold never shadows
  its OWN begin anchor (the archive opens after the "Task begun" result,
  which stays live as the bookmark), so the queue row survives its own
  commit until a later, wider fold shadows the anchor. The drain filtered
  those settled rows when PICKING the next entry but not when computing
  successor anchors — the ghost still counted as a pending successor, and
  every older entry whose deliverable sat after it deferred forever. Worse,
  the defer was the whole pass's exit: one blocked newest entry starved
  every older foldable entry behind it, including the outer tasks whose
  wide folds were the only things that could have retired the ghosts.
  Nothing else ever did (the session never triggered AUTO compaction), so
  the queue deadlocked. The fix is symmetric filtering plus skip-not-exit:
  successor anchors exclude settled rows, and a 'wait'/'defer' verdict
  skips that entry for the rest of the pass instead of aborting the drain.
- **Verified three ways.** New `test/fold-drain.test.mjs` (wired into
  `npm test`) drives the real `createArchiveDrain` against a mock harness
  and reproduces the exact live shape — an older sibling closing before a
  younger one begins, one shared deliverable — plus the starvation and
  retry-on-next-boundary cases; all three tests fail on the pre-fix drain.
  Full suite: 120 tests green. And the original session's log, replayed
  with the fixed decisions and clean commits simulated, folds 13 tasks
  where the live run managed 6: the stuck five all fold at their turn
  boundaries, turn 1's whole queue drains in a single boundary, and zero
  rows remain waiting.
- **Restart behavior is unchanged and still self-healing.** `settledArchives`
  stays process-local on purpose: after a restart the ghost rows re-plan,
  hit the shadowed-close 'drop' path, and settle again within one drain
  round — no persisted state needed (now asserted by test too).
- **Riding along since 0.31.1** (docs/chores already on master): npm
  storefront material — hero banner, badges, quickstart, compaction
  comparison, screenshots, and a star/discussion call-to-action in both
  READMEs — plus the publish metadata and the dsh 0.1.5-alpha.2
  verification note.

## 0.31.1 — README states what folding saves against not folding at all (2026-09-09)

- **The savings section is a counterfactual now, not a log of one fix.** Both
  READMEs gained "What it saves (measured)" / "省了多少（实测）", computed from
  one real session's own usage records (411 model steps, 26 folds) by replaying
  every main request with the folded spans added back:

  | | without folding | with taskfold |
  | --- | --- | --- |
  | total prompt tokens | 142,654,308 | 52,127,098 (**−63.5%**) |
  | largest single request | 594,909 | 206,896 |
  | peak context-window use | 59.5% | 20.7% |

  441,100 tokens of finished work left the surface, and because that history
  would otherwise have been re-sent on every later request the cumulative
  effect is **90,527,210 tokens never sent**. Producing the 26 summaries cost
  3,550,270 tokens (3.9% of the saving, most of it cache reads); the largest
  single fold removed 40,422 tokens in one call. The session stayed inside the
  1M window either way, so the figures are arithmetic rather than a truncation
  artifact — and both READMEs say so, alongside the honest caveat that most of
  the saved tokens would have been cache reads: cheaper, but still billed and
  still occupying the window.

## 0.31.0 — lifecycle hints get their own channel, and target the innermost task (2026-09-09)

- **Guardrail hints no longer ride the host's runtime-context snapshot.** The
  `todo-bridge` system-prompt context is gone; hints are now published as their
  own `user/message` from an `agent/pre-step` handler, exactly the way
  `dsh-tool-skill` publishes its catalog (`source.kind =
  'task-marks:lifecycle'`, `form = 'hint'`). The snapshot is assembled by the
  host from every registered contribution, so one hint change re-sent the whole
  snapshot — including `sandbox:policy` and `approval:policy`, ~1.2k tokens of
  unchanged text. Live check after the switch: exactly one lifecycle message
  after the restart, zero new snapshots, zero consecutive duplicates.
- **Nested tasks are now nagged about the right task (reported live).** The
  close-pressure nudge scanned every open mark and held the OLDEST, so opening
  a nested subtask still produced "task 'parent' is 20+ rounds old — close it"
  while the parent was structurally blocked by its own child, and the parent's
  clock never moved. It now targets the innermost open mark, and a mark's age
  anchor advances on any nested `Task begun:` / `Task ended:` / `Task folded:`
  result, so a fresh subtask resets the parent's counter. The wording also
  offers both exits now — wrap the remaining distinct parts as nested subtasks,
  or close the task — instead of only pushing to close. The regression is
  pinned offline by a test named after the live bug (`a fresh nested begin
  resets the parent clock`); both this nudge and the 8–19 round decomposition
  hint then fired live against the innermost task.
- **A hint is an event, not a state.** No empty "nothing applies right now"
  notice, no expiry notice, no `<system-reminder>` / `<task_lifecycle>` wrapper
  and no supersession preamble: the message body is the hint line itself. That
  removes the mechanism that used to retract a satisfied hint — the model
  knows it opened a task when it opened one. The dedupe guard is therefore a
  per-session latch rather than a history scan: `renderLifecycleBody` returns
  `null` when nothing applies, which clears the latch, so the same hint can
  legitimately fire again the next time its condition appears, while an
  unchanged held hint stays silent.
- **Verification.** 117 assertions across 9 test files, all green, including a
  simulated session that publishes exactly the three hints it should and a
  negative assertion that the message carries no framing tags. The cache gate
  (`scripts/verify-cache.mjs --since-restart`) passes 3/3 on the live log
  (hit 98.8 / 99.0 / 98.2%, tails −10,173 / −1,730 / −15,413).

## 0.30.0 — gate fold cache reuse with verify-cache.mjs (2026-09-09)

- **The fold cache regression that 0.29.3 fixed can no longer come back
  unnoticed: `scripts/verify-cache.mjs` reads a live session log and fails when
  a fold re-pays its span.** Offline tests can only pin the structural
  precondition (one system message, the request is a strict prefix of the main
  conversation) — `cacheReadTokens` exists only in a real provider's usage, so
  the empirical check has to read a real session. The script decodes the
  multi-frame zstd log, takes each `compaction/summary`'s usage and
  `shadowedTokenCount`, and classifies the difference: a healthy fold has
  `uncached − span < 0` (the span came from cache, only the trailing
  instruction — plus any mid-span rewrite — is billed again), the regression
  has a positive tail of at least instruction + system head. `--since-restart`
  scopes judging to folds after the newest `reason=resume` request header,
  which is the right window right after a dsh upgrade; `--tail-budget`,
  `--min-span`, `--last`, `--require` and `--json` cover the rest, and a
  regression exits 1.
- **The rule was calibrated by its own first live run.** The first draft passed
  a fold when `uncached <= 3500`; on fold 1303 that misfired — `uncached` 5,655
  looked over budget, but the span was 19,796 (tail −14,141) and fully cached,
  the extra few thousand tokens coming from a nested fold that had rewritten
  the middle of the span. Switching the test to the sign of `uncached − span`
  fixes it: the same log then passes 3/3 after the restart (hit 97.7 / 98.3 /
  95.9%, tails −4,806 / −5,445 / −14,141) while all eleven historical broken
  folds still fail (+3,974 … +18,587). Assertions were added for both
  directions, including the mid-span-rewrite pass and the partially re-paid
  span fail.
- **Wired into the flow, not just the toolbox.** `npm run verify:cache` runs
  the script; `test/verify-cache.test.mjs` (15 assertions, suite 89 → 104)
  covers decoding, both usage paths, and the classifier, so `npm test` guards
  the gate itself; the maintainer notes in both READMEs, the `release.mjs`
  draft reminder, and the release-flow design note all require the live run
  after a dsh upgrade or before any release that touches the fold envelope.

## 0.29.3 — fold summary calls stop re-billing the span (2026-09-09)

- **Cache regression fixed — dsh `0.1.5-alpha.1` moved the system prompt into
  `messages[0]`, the prefix-anchored envelope already replayed it, and every
  fold therefore paid full price for its own span.** The host's
  `buildSummarizationInput()` used to return `{ system, tools, messages:
  regionMessages }`; `0.1.5-alpha.1` returns `{ tools, messages: [system,
  ...regionMessages] }`. taskfold's envelope prepends every surface node before
  the span — including surface node 0, the derived `system/message` — so the
  summarization request became `[system][pre-span][system][span][instruction]`.
  The duplicate system lands exactly at the span start, which is where the
  request stops being a prefix of the main conversation request, so the
  provider's warm cache broke there and the whole span was charged again. Live
  measurement across one session (8 folds) pinned the signature: `cacheRead`
  always equalled "last main request prompt − span" (residual = the post-span
  tail), never "last main request prompt", re-billing 201,196 tokens in total
  (111,831 over the first five folds, 89,365 over the next three). The fix is
  `dropDuplicateLeadingSystem(prefixMessages, regionMessages)`: a structural,
  byte-equality probe that drops a leading system message only when an
  identical one is already in the prefix. It is fail-open — an old host
  (separate `system` field, so `messages[0]` is not a system message) is a
  no-op, and a DIFFERENT leading system message (an in-history prompt update
  the envelope never replayed) is kept rather than silently dropped. Only the
  request envelope is deduped; `input.messages` still feeds the span artifact,
  the preview, and the archive footer, so fold files keep their system line.
  Six assertions were added (suite 83 → 89), including "the deduped request is
  a strict prefix of the main conversation". The gain is not yet re-measured
  live: the mounted profile copy was still the pre-fix build when the last
  session restarted, so a fold on the fixed copy is outstanding.
- **Docs**
  - keep supported-dsh section to one line per alpha/rc channel

## 0.29.2 — dsh 0.1.5-alpha.1 verified, no plugin change (2026-09-09)

- **Host upgrade verified — `0.1.5-alpha.1` is now the known-good dsh, and no
  plugin change was required.** The host moved from the previously verified
  `0.1.2-rc.1` to `0.1.5-alpha.1` (`dsh`, `dsh-compaction-basic`, and
  `dsh-llm` ship version-locked, so one number covers the whole coupled
  surface). Verification ran the full offline suite (83 assertions green), a
  12-check host-API probe against the real `dsh-compaction-basic` /
  `dsh-llm` / `dsh-session` packages, an end-to-end fold driven through the
  real engine plus assembler (prefix-anchored envelope, fenced span index
  carrying true line numbers, span bytes untouched, archive footer), a seam
  audit of every host surface this plugin touches (`agent/pre-step`,
  `agent/turn-stopping`, `sessionProjections.register` / `stateOf`,
  `systemPrompt.section` / `context`, `tools.register`, and the
  `dsh-compaction-basic` compaction discriminator), and live folding on the
  running 0.1.5-alpha.1 host with the mounted copy byte-identical to HEAD.
  One host-side drift was found and is harmless: `dsh-session` dropped
  `decodeStorageRecord` / `packChunkRuns`, neither of which this plugin
  uses; the offline region-transaction replay was therefore not re-run, and
  the live fold covers that path instead. Both READMEs now declare
  `0.1.5-alpha.1` verified (2026-09-09) and keep `0.1.2-rc.1` as previously
  verified; the minimum stays `0.1.2-alpha.5`, and the upper bound remains
  untested and unenforced.
- **Docs**
  - record dsh 0.1.5-alpha.1 as the verified host in both READMEs, demoting 0.1.2-rc.1 to previously verified

## 0.29.1 — intent-based bullets and structurally forced why: clauses (2026-09-07)

- **Instruction change — Granularity regroups by intent; Reasoning gets a
  structural forcing shape.** Live verification (0.29.0 subagent run) showed
  the numeric granularity targets met (20 messages → 4 bullets) but two
  wording-level defects: (a) the "3-5 steps per bullet" typical size and the
  anti-packing clause literally conflicted — one bullet covering several
  steps must contain several actions, so the model packed distinct actions
  with separators to satisfy both; (b) the Reasoning rule's "state WHY" was
  satisfiable by a trailing because-clause, and when the span already
  contained a polished report the summarizer echoed it instead of mining the
  thinking blocks (fold #22: rationale clauses present but sparse). Fixes:
  a bullet now covers ONE intent — a coherent purpose or phase of work;
  3-5 steps is the TYPICAL size, a smaller intent still gets its own bullet,
  10 steps remains the hard ceiling, and "never join distinct intents into
  one bullet — not even with separators" replaces the action-level clause.
  The Reasoning rule now forces structure: every What-happened bullet ENDS
  with an explicit `why:` clause naming the deciding consideration plus the
  strongest rejected alternative; action-only bullets are failures; an
  in-span polished report must not be echoed in place of the deliberation
  that produced it (hypotheses discarded mid-work, uncertainty resolved,
  ordering rationale). Tests pin the intent grouping, the small-intent
  guarantee, the why-clause forcing, the echo prohibition, and the continued
  absence of any budget language.
- **Features**
  - What happened bullets regroup by intent (typical 3-5 steps, ceiling 10)
  - every What happened bullet ends with an explicit why: clause

## 0.29.0 — word budget and maxTokens cap removed from fold summaries (2026-09-07)

- **Instruction + engine change — the mechanism no longer limits summary
  length.** The per-fold word budget is gone entirely: the engine no longer
  computes estTokens/wordBudget or injects a budget line, the assembler
  signature drops `budgetLine`, and the instruction's Budget rule is replaced
  by a Sections rule that keeps only the coverage duties (fidelity over
  padding, "(none)" as soon as true, Changes exhaustive, Pitfalls keeps every
  failure cause, compress narrative first — never anchors, decisions, or
  failure causes). The fold LLM call also no longer sets `maxTokens` — no
  mechanism-level length limit remains: accuracy governs length, bounded only
  by the provider's default and the host's not-smaller-than-span rejection.
  What-happened coverage is governed by structure alone (Granularity: 3-5
  steps per bullet, ceiling 10, split-don't-pack; Reasoning: WHAT + WHY per
  bullet, rejected alternatives kept). Field motivation: budget framing kept
  licensing coarse summaries even after the granularity rules landed; the
  ruling is that coverage accuracy is the only goal.
- **Features**
  - remove the per-fold word budget (instruction rule, engine computation, assembler parameter)
  - drop the fold call's maxTokens cap (no mechanism-level limit on summary length)

## 0.28.0 — What happened granularity + reasoning capture (2026-09-07)

- **Instruction change — coverage accuracy outranks the word budget in What
  happened.** Field observation on 0.26.0/0.27.0 summaries: with clustering
  merely *permitted* ("MAY cluster into one phase bullet") and no ceiling, the
  summarizer packed up to ~37 messages into a single separator-joined bullet —
  facts survived, but scanning granularity and per-action recall anchors
  degraded (controlled experiments had shown the natural density is 4-9
  messages per bullet; the license, not the capability, was the problem). The
  instruction now pins a numeric rule in both the section line and a dedicated
  Granularity rule: a step = one assistant action with its tool results
  (typically 2-4 span lines); one bullet covers 3-5 steps, 10 is the hard
  ceiling — an over-ceiling bullet MUST split into consecutive bullets, each
  keeping its own `L<N>-<M>`; distinct actions are never joined with
  separators inside one bullet; "Coverage is a correctness requirement that
  outweighs the word budget: when words run short, write terser bullets —
  never fewer, never merged, never dropped." The Budget rule now disclaims
  coverage authority explicitly ("The budget shapes prose economy, never
  coverage") and defers What happened to the Granularity rule. Self-limiting
  by construction: the ceiling bounds bullet count (~span_lines/30 minimum,
  in practice 5-30 bullets), so the section stays well inside the summarizer's
  maxTokens even on giant spans. Offline tests pin the numeric anchors, the
  ceiling, the anti-packing clause, and the budget disclaimer.
- **Instruction change — the summary now captures the reasoning, not just the
  actions.** Field observation: 0.27.0 fold summaries recorded WHAT happened
  (calls, results, files) but none of the WHY — the decision process lives
  only in the span's thinking blocks and was lost to every recall. The
  instruction adds a dedicated Reasoning rule and extends the What-happened
  section line: thinking blocks are "the primary source of decision rationale
  — hypotheses weighed, options compared, why one path was chosen over
  another, what a result confirmed or refuted"; each bullet states "not only
  WHAT was done but WHY, anchored to the deciding consideration"; keeps
  "rejected alternatives and the reason they lost"; and distinguishes settled
  conclusions from passing guesses. Division of labor stays explicit: failure
  causes → Pitfalls & gotchas, choice rationale → What happened. Rationale is
  the one class of span knowledge that exists nowhere else — actions are
  re-derivable from artifacts and code, reasoning is not.
- **Features**
  - What happened granularity rule (3-5 steps per bullet, ceiling 10, budget never trims coverage)
  - Reasoning capture in What happened (why + rejected alternatives, mined from thinking blocks)

## 0.27.0 — fold_recall range overload (from/to) (2026-09-07)

- **New recall grammar matching the citations summaries emit.** Since 0.26.0
  the walkthrough summaries cite span messages as `L<N>-<M>` ranges, but
  fold_recall only spoke single-line or full-index: recovering a cited phase
  meant either the whole preview (~15 tokens per message, plus an
  unconditional artifact rewrite) or N sequential single-line calls. The new
  `from`/`to` parameters (inclusive 1-based line numbers, mutually exclusive
  with `line`, capped at 10 lines) return the exact original messages of the
  cited slice in ONE call — `{role, content}` fidelity identical to the line
  overload, true line numbers preserved, no file written (full mode's write
  exists to make the artifact greppable; a slice lookup doesn't need it).
  Validation errors carry the fold context prefix and name the valid domain;
  the cap failure tells the model to narrow the range or use the full
  regeneration. The tool description grows by two sentences (~25 resident
  tokens) — wording deliberately mirrors the `L<N>-<M>` citation grammar so a
  summary citation maps to a recall call without translation.
- **Features**
  - fold_recall `from`/`to` range overload: exact originals, ≤10 lines, no file write

## 0.26.0 — turn-stopping drain, instruction-tail span index, archive footer (2026-09-07)

- **Folding economics: top-level folds now run while the provider prefix
  cache is hot.** The archive drain is additionally wired to the host's
  `agent/turn-stopping` hook (serial contract; the whole registration sits
  in a try/catch, so older hosts degrade to the previous pre-step-only
  semantics and `apply()` never breaks). Measured motivation: in a live
  session's seven folds, both all-miss folds billed ~82% fresh input because
  a turn-final deliverable's fold waited for the next turn's pre-step — after
  the user's idle gap had expired the cache; the same boundary drained
  seconds after the last main request instead hits ~97%. Aborted or errored
  turns never dispatch the hook, so pre-step remains the fallback; the
  drain's running guard was renamed `drainRunning` to reflect that it now
  covers both hooks (its real job is cross-session reentry — the host loop
  serializes same-session dispatch).
- **Citations grounded by the span message index.** The fold instruction's
  tail now carries a fenced, numbered, one-line-per-message index of the
  span, rendered by the same `renderSpanPreview` that serves `fold_recall`
  (preview line N = artifact line N), and four citation rules make the
  summarizer copy line numbers from that index (never count messages), cite
  source files only with file names and tool-visible line numbers (never
  estimated from memory), fall back to a short verbatim quote when no anchor
  resolves, and never mirror the index into the summary. Grounding: three
  fresh-context controlled arms on a 52-message artifact — index arm 100%
  correct message-level citations, control arm (no index, counting) 100% at
  that size, perturbed arm (+1 index labels) followed the printed labels in
  37/37 citations, proving index-copy over counting. The index rides the
  instruction message (always fresh input); the span bytes and the envelope's
  cached prefix are untouched.
- **Compact archive footer instead of the full inline preview.** The
  `## Fold archive` section embedded in every summary node now carries the
  fold number, the message count, the artifact path, and a head-3 + elision
  + tail-8 window of the span preview with TRUE line numbers — lines sliced
  from `renderSpanPreview` output, so footer and index can never drift
  apart; the complete index stays one `fold_recall({ fold })` away.
  Measured motivation: the full inline preview was 64% of all fold-summary
  tokens, and 49% of its characters were `[think]` fragments. What-happened
  bullets may now cluster consecutive steps into phase bullets carrying
  `L<N>-<M>`, and the Budget rule's old anti-clustering clause was rewritten
  to agree. Model-facing texts (task_end description, system-prompt section)
  no longer promise a no-elision complete preview; `list_folds` and
  `fold_recall` behavior is unchanged.
- **Features**
  - turn-stopping drain for cache-hot top-level folds
  - span message index + citation rules in the fold instruction
  - compact archive footer (head 3 + elision + tail 8, true line numbers)

## 0.25.0 — slim the list_folds listing to number, tokens, and title (2026-09-07)

- **Breaking-ish output change — `list_folds` now lists only what the model
  consumes.** The old per-fold line carried three internals no caller uses:
  the archive seq (`(seq N)` — an id `fold_recall` never takes; the tool
  description even had to warn against confusing it with the fold number),
  the shadowed event `range A..B` (raw seqs nothing else in the model's
  context references), and the total event count in the header. All three
  are gone; the durable event log and each artifact's footer still record
  them verbatim, so offline audits lose nothing. What remains per fold is
  the fold number (the exact `fold_recall` domain), the shadowed token
  count (the size signal for picking a recall target), and the title; the
  header keeps surface nodes and the shadowed total (`Surface: 137 nodes;
  folds: 7, ~114107 tokens shadowed.`). The resident tool description was
  halved too (~65 → ~33 words) — it sits in every session's tool list, a
  larger recurring cost than any single output. Tests now pin the shape
  (`#N <tokens> tokens | title`) and assert the internal ids never leak
  back into the render.
- **Features**
  - slim the list_folds listing to number, tokens, and title

## 0.24.2 — live-verified subtask folding on dsh 0.1.2-rc.1 (2026-09-07)

- **Docs — the dsh `0.1.2-rc.1` verification is now live, not offline-only.**
  After the host restarted onto taskfold 0.24.1, the full drain ran in a
  real session: every archive queued from the broken era folded on the
  first boundaries (three commits, ranges matching the offline replay's
  prediction seq-for-seq), a plain subtask folded end-to-end, and — the
  regression case — a subtask whose `task_begin` shares an assistant
  message with a parallel partner call opened its region through the
  v0.24.1 guard and committed (fold #7: 1,610 shadowed tokens, artifact
  on disk, `fold_recall` line round-trip exact). A deliberately tiny
  parallel span (215 tokens) was rejected by the shrink check
  (`summary is not smaller than the shadowed content`) and settled
  silently — the designed degradation, confirming the guard does not
  force-fold spans a summary cannot shrink. Both READMEs' "Supported dsh
  versions" now record the live in-process verification alongside the
  offline suite, host-API probe, and region-transaction replay.
- **Docs**
  - live-verified subtask folding on dsh 0.1.2-rc.1

## 0.24.1 — skip parallel partner results when picking the fold span start (2026-09-07)

- **Fix — parallel `task_begin` wedged every fold with an unbalanced START
  boundary** (found live while verifying dsh 0.1.2-rc.1): when the
  begin-carrying assistant message also calls other tools in parallel,
  their `tool/result` events follow the "Task begun" result on the surface,
  and `deferredArchivePlan` opened the span at the first of them — a cut
  that splits the partner call/result pair. The engine's
  `validateSurfaceRegion` rejects unbalanced START boundaries, and the
  drain cannot self-heal those (its node-by-node fallback shrinks the END;
  shrinking the end can never fix the start), so every retry failed into
  the `'fold failed'` bucket and the task never folded. Symptom in the
  running host: `auto-fold for "…" is failing (fold failed)` HOLD warnings,
  with zero `compaction/start` events in the session log — the transaction
  dies before its first durable write. The plan now advances the start
  floor past the LAST result of every call in the begin-carrying message
  (a bounded skip; partner calls/results stay live on the surface beside
  the begin pair), which is byte-identical to the old choice for a
  single-call begin; a missing partner result (interrupted step) degrades
  to the single-call choice. The fold instruction's boundary wording
  (`FOLD_BOUNDARY_RULE` and the prefix envelope's CONTEXT ONLY clause) is
  aligned to name the partner results. Regression-pinned in
  test/task-marks.test.mjs (five cases: skip, order-independence,
  interrupted partner, single-call identity, foreign-result exclusion).
- **Verification — dsh 0.1.2-rc.1** (drove this release): offline suite
  75/75; a runtime probe of every host seam the plugin touches (engine
  class export and `summarize(input, agent, signal)` hook, `BlockAssembler`,
  `Session.prototype.snapshotEvents/eventAt/deriveEventMessage/requestHeader`
  and the `surface` getter, the `compaction/summary` commit shape with
  `compactionId`/`shadowedRange`, the detailed-checkpoint
  `source: { kind: 'plugin', plugin: 'dsh-compaction-basic' }`
  discriminator) all intact; and an offline replay of the live session's
  decoded log (32,946 events rebuilt into a real `Session`) running the
  real rc.1 region transaction end-to-end — with the fix, the fold that
  failed live commits (span 8,672..13,556, 16,029 shadowed tokens), and
  the drain's END-shrink fallback demonstrably absorbs the
  `task_end`-with-parallel-partner imbalance. Both READMEs' "Supported
  dsh versions" now record `0.1.2-rc.1` as known-good (minimum unchanged
  at `0.1.2-alpha.5`).

## 0.24.0 — engine-built fold headings, nested-task-mark incentives (2026-09-05)

- **Breaking-ish fix — heading construction replaces heading compliance**: the
  fold engine now PREPENDS the exact `# <task name>` title line to the
  summarizer's output itself (`prependFoldHeading`), and the closing
  instruction tells the model to write NO title — open directly with the
  `## What happened` section (a structural receipt: `opensWithSectionHeading`
  rejects anything else). Live 0.23.0 data proved the similarity guard's core
  assumption wrong: in Chinese conversations the summarizer translated the
  heading 9+ times STRAIGHT THROUGH the printed verbatim-copy instruction
  (each rejection re-summarizing the whole span for 30–70 s), because no cheap
  string metric can rate a translation similar to its source. Construction
  makes the heading byte-exact by definition and deletes the entire failure
  class; fold titles in list_folds are now always the exact task names.
  test/fold-engine.test.mjs rewritten for the construction contract.
- **Enhancement — nested task marks get runtime incentives** (field finding:
  even large jobs run as ONE flat mark — a 14-minute 4-PR review folded as a
  single blob, because after the first task_begin the only live lifecycle
  signal was the 20-round close-nag): (1) Nudge 3 `shouldSuggestDecomposition`
  — while an open mark is 8–19 rounds old with ≥3 recent work calls, a HOLD
  hint suggests wrapping the remaining distinct parts as nested subtasks
  (innermost closes first, each part folds at its own close); it hands off
  cleanly to the 20+ close-nag and never co-renders with it. (2) task_begin's
  result text gains a nested-mark hint when the begin makes depth ≥ 2.
  (3) The system prompt's nesting rule is promoted to its own passage with a
  concrete review-shaped example. (4) The todo-bridge line now maps todo items
  to nested marks explicitly. Tests for the new predicates in
  task-marks.test.mjs.

## 0.23.0 — lenient scope guard, labeled span previews, slim fold_recall lines (2026-09-05)

- **Fixes**: the scope-adherence guard now scores the summary's first
  heading against the closing task name with a lenient similarity
  (NFKC + typographic quote/dash folding, case/whitespace-insensitive,
  best of sequence ratio and order-insensitive token overlap, threshold
  0.6) instead of demanding a byte-exact `# NAME` prefix. The byte-exact
  form turned cosmetic model behaviors — curly quotes for ASCII ones, a
  translated heading in a non-English conversation — into scope failures,
  and every scope failure re-summarizes the whole span on the next step
  boundary (one live session burned 12 consecutive full-span fold calls
  before an attempt happened to comply). The closing instruction now also
  prints the required first line verbatim (`# NAME`, on its own line) with a
  one-line copy rule — copy it, do not translate or reformat it, the summary
  body may use any language — so the residual failures the retry exists for
  are genuinely foreign headings — real drift into the earlier conversation.
  New offline tests pin the comparison contract (test/fold-engine.test.mjs).
- **Enhancement**: span-preview call fragments now prefer a human-authored
  label carried in ANY tool call's arguments — `description` first (pwsh
  already did this; subagent delegations get it too), then `name` (task
  marks read `→task_begin(Fix fold guard)`, skills read `→skill(x)`) —
  before falling back to clipped raw JSON. Reasoning blocks carry no label
  field (`{type, text}` only), so `[think] …` excerpts stay as they are.
- **Fixes**: fold_recall's line overload now slims the returned message to
  `{role, content}` — the exact form the JSONL artifact writer uses — and
  renders it as single-line JSON. The line result used to be the raw
  derived event message, dragging host provenance (`source.provider`,
  `source.model`, and a full `replayState.response` replay envelope) plus
  pretty-print inflation into the tool result, even though the artifact
  export path had been slimmed deliberately long ago. The file overload was
  never affected; only the in-result line lookup bled metadata.

## 0.22.1 — code-review remediation: schema hardening, module split, dedup (2026-09-05)

- **Fixes**: the taskMarks state schema now validates
  `pendingArchives[].foldResultSeq` for real (positive integer, like every
  other seq). The error message always claimed it; a corrupted row missing
  the field used to pass load and then wedge the deferred drain in a
  permanent 'wait' (its default 0 always sorts before the entry's own
  anchor). Corrupted rows now fail loudly at parse, and deferredArchivePlan
  drops — never waits on — entries whose close seq cannot sit after their
  begin anchor, so the drain settles them instead of stalling forever.
- **Fixes**: grep result-brief file counting now understands POSIX absolute,
  workspace-relative, and drive-absolute paths with any extension — the old
  Windows-drive-.js-only regex under-counted everything else as "1 file" —
  and the singular form "Found 1 match" now parses at all (`matches?` only
  matched "matche(s)", so singular count lines silently degraded to raw
  excerpts).
- **Fixes**: pushRelease reconciles against HEAD's actual upstream (falling
  back to origin/master, then origin/main) instead of hardcoding
  origin/master, so releases from non-master branches no longer diverge-check
  against the wrong ref.
- **Performance**: the todo-bridge context callback takes ONE sessionEvents
  snapshot per render and feeds every nudge predicate from it (was up to four
  full-log snapshots per render — on exactly the long sessions taskfold
  targets); task_begin's anchor check walks the surface from the end via
  eventAt instead of materializing a full event map; the dead `head` rev-parse
  in pushRelease is gone.
- **Refactoring**: compact-region.mjs (1428 lines, 7 concerns in one apply)
  is now a thin composition root over focused plain modules — events.mjs
  (shared defensive event extractions: sessionEvents/messageOf/
  toolResultText), task-marks.mjs (the taskMarks projection + pure
  close/fold decisions), fold-instruction.mjs (the two swapped-in
  instructions), fold-engine.mjs (self-hosted ScopedEngine + lazy
  resolution), fold-drain.mjs (the deliverable-gated pre-step auto-folder),
  lifecycle-nudges.mjs (pure nudge predicates over an events snapshot).
  Duplicates removed: sessionEvents existed verbatim in two plugins; the
  `event.data.message.content` defensive extraction appeared 8×; tool-result
  text joining had three variants; renderArchivePreview re-implemented
  renderSpanPreview's loop (now composes it). Dead code removed:
  lastSurfaceNode, the unused markSeq/void in task_begin.
- **Docs**: both design notes refreshed to the as-built reality (the
  compact_stats draft described a removed `unknownEventTypes` field; the
  scoped-summary note referenced long-gone `task_commit`/`compact` tools);
  cordis.patch.yml's header no longer calls the bundle "dsh-cmpct" (row ids
  stay legacy-stable by design, now documented as such); READMEs describe
  the real plugin layout and `npm test`; a `test` script was added to
  package.json.
- **Chores**: the ctx.llm.stream instruction swap logs one line on its
  first matching call per process, so a host-side rename of the
  dsh-compaction-basic discriminator becomes visible drift instead of a
  silent fail-open; the drain's settled-entry comment now describes the
  actual cleanup path (next-round 'drop' + in-memory settled set, not a
  reducer drop).

## 0.22.0 — prefix-anchored summarizer envelope — explicit begin→end scope, ~5x cheaper fold calls (2026-09-05)

- **Features**: the fold summarizer now sends the whole surface up to the
  span — prefix + span + a scoping instruction — instead of the span alone,
  making the request a strict prefix of the main conversation request. The
  A/B experiment (real session spans, deepseek-chat, three interleaved
  trials) measured ~97% prefix-cache hit against a structural 0% for the
  span-only envelope, cutting fold input cost to roughly a fifth at cache
  pricing, and zero path fabrications across trials where span-only
  occasionally invented plausible relative paths — the prefix supplies the
  correct full forms. The scoping instruction names the region by its exact
  lifecycle markers: the span begins immediately after the
  'Task begun: NAME' result and ends with the 'Task ended: NAME' result,
  and the earlier conversation is declared CONTEXT ONLY — usable to resolve
  references, never summarized or restated.
- **Safety rails**: a scope-adherence guard rejects any summary whose
  heading is not '# NAME' — mis-scoped summaries fail loud into the failure
  bucket and retry instead of ever committing. The prefix is
  all-or-nothing: a missing declaration, a span off-surface, a derivation
  failure, or a pathological size (>4M chars) falls back to the
  byte-identical span-only envelope (FOLD_SUMMARY_INSTRUCTION is now
  generated by buildFoldInstruction({})). The 10% word budget still derives
  from the span alone. LIFO drain order stays cache-optimal under this
  envelope — later-processed older entries keep untouched prefixes — so the
  drain is unchanged.

## 0.21.0 — proportional summaries, complete previews, line-level recall (2026-09-05)

- **Fix**: the 0.19.0 anchor regression. Fold regions started AT the
  "Task begun" result seq, but a tool result's leading cut always splits
  its own call/result pair, so the engine rejected every region as an
  unbalanced boundary within milliseconds — and the drain settled every
  queued fold SILENTLY: no compaction events, no failure warnings, just
  entries shadowed by an in-memory settled set. Regions now start at the
  first surface node strictly AFTER that result (falling back to the
  0.18 call-anchored region when the result is missing, shadowed, or
  has nothing after it), and foldRegion rethrows start-boundary errors
  loudly instead of grinding the end-shrink loop into a silent settle.
  Diagnosed by instrumenting the installed copy after offline replay of
  the real session log proved the gate, queue, and engine were all fine.
- **Features**: fold summaries are now proportional and exhaustive. The
  word budget is ~10% of the span's estimated tokens, injected as a
  concrete per-fold number (floor 150 words, cap 4000 to stay under the
  summarizer's maxTokens); the absolute 300-word cap and the 6/6/5/4
  bullet caps are retired. Sections get differentiated treatment: What
  happened keeps one bullet per meaningful step (compress phrasing, not
  facts), Changes is exhaustive — every path written and every key value,
  no selection — Pitfalls keep every failure and its cause, and triage
  drops narrative connective tissue first while anchors, decisions, and
  failure causes are never dropped. Span previews list EVERY message
  line — the head+tail window is gone, with a defensive degrade for
  degenerate near-empty spans that would approach the span's own size.
  fold_recall gains a line overload: fold_recall({fold, line}) returns
  that exact artifact line — the full original message content — without
  writing a file, making preview line numbers retrieval coordinates.
- **Features**: stock checkpoints get the same treatment. A ctx.llm.stream
  wrapper swaps the host's compaction instruction (terse bullets) for an
  uncapped detail-first variant, matched by purpose='compaction' plus the
  dsh-compaction-basic instruction source so the fold path is immune;
  the wrap is idempotent and fails open. Prompt surfaces across the
  board (system prompt, task tool descriptions, fold_recall, closing
  block, metadata lines) state the new boundaries and budgets, and the
  legacy task_fold mention is dropped from the closing block.

## 0.20.0 — fold artifacts live in the session's own directory (2026-09-05)

- **Features**: span artifacts are written into the session's own durable
  directory — resolved backend-neutrally as the dirname of
  sessionPersistence.locate(session.header), the per-session directory the
  persistence backend documents as "available for future session-local
  artifacts" — under a taskfold/ subfolder clear of backend-owned files.
  Placement now has a three-level precedence: session directory, then the
  OS tmp root scoped by a slugified session key, then the flat legacy tmp
  root. Session-local placement binds artifact lifetime to the session's
  durable log (a resumed session's old fold artifacts stay reachable;
  deleting the session cleans them up) and makes OS temp sweeps and
  cross-session mixing impossible by construction. The backend is resolved
  lazily through ctx.get rather than a hard inject, and any missing link
  (no backend, no per-session artifact, no header, locate() throwing)
  degrades to the tmp fallback instead of failing the fold. fold_recall's
  description and failure message state the new location.
- **Fix**: slugPart no longer coerces non-string session keys —
  String(undefined) is "undefined" and String(12345) is "12345", and both
  silently became literal directory names instead of the documented
  flat-root fallback. Only real strings scope now; anything else degrades
  to the empty slug. Tests pin the override precedence, the four bad-key
  fallbacks, and sessionArtifactDir's resolution plus defensive paths.

## 0.19.0 — symmetric fold bookends, per-session artifacts, head+tail previews (2026-09-05)

- **Features**: a fold's archive now spans the "Task begun" result through
  the "Task ended" result — result bookends on both sides. The begin call
  and its opening reasoning stay live on the surface as the task's
  bookmark, so a folded task shows its begin call with no visible result:
  the fold's summary node stands in for it. If the begin result is missing
  or already shadowed by auto-compaction, the plan falls back to the 0.18
  call-anchored span instead of dropping the fold. Every model-facing
  surface states the new boundary — fold-summary instruction, task_begin /
  task_end / fold_recall descriptions, the system-prompt section — and the
  Fold archive metadata line records it explicitly (span: "Task begun"
  result ... "Task ended" result).
- **Behavior**: span artifacts are written into a per-session subdirectory
  (%TEMP%\taskfold-artifacts\<session>\<name>-<stamp>.jsonl), so
  concurrent sessions never share a directory or leak task names to each
  other; a missing session key falls back to the flat 0.18 layout.
- **Previews**: over-cap span previews keep a head+tail window — head 25,
  an elision pointer, tail 4 with true line numbers — and the budgeted
  inline preview reserves its tail share first, so a span's close is
  visible whenever any preview is.

## 0.18.3 — prompt-text corrections across all four model-facing surfaces (2026-09-04)

- **Fixes**: a full prompt audit tightened every surface the model sees.
  The fold_recall description pointed at the runtime-context fold notices
  removed in 0.17.0 — it now points at the Fold archive section of the
  committed summary, and its "read-only" wording no longer contradicts
  the tmp write it performs. The fold-summary instruction no longer
  contradicts itself: the "# NAME" heading explicitly prefixes the
  five-section structure, the word caps bind only the four action
  sections (User inputs & decisions is exempt by design), Changes keeps
  only grep-able durable artifacts while commands move to What happened,
  and the closing rule separates closed from succeeded so genuine
  failures report honestly in Outcomes. The lifecycle section makes fold
  recall strictly on-demand (a dependent new task counts as demand) and
  states that deliverable text must land after the task_end result, not
  in the same message as the call. Name-ban, todo-bridge, and relay
  wording tightened; tests pin the new clauses.
- **Docs**: both READMEs still said task_fold in the fold workflow; they
  now say task_end (the name retired in 0.18.0 survives only in this
  changelog).

## 0.18.2 — Fold archive preview renders as a fenced code block (2026-09-04)

- **Fix**: the archive section used single newlines between preview lines;
  markdown renderers collapse single line breaks into one paragraph, so the
  whole preview read as a mashed blob. The metadata bullet now sits after a
  blank line and the span preview lives in a fenced code block, which
  preserves the one-message-per-line layout everywhere (GUI, model context,
  plain text).

## 0.18.1 — budget-aware Fold archive preview (small spans fold again) (2026-09-04)

- **Fix**: 0.18.0's unconditional 30-line preview made small spans' framed
  summary exceed the shadowed content — the engine's "summary is not smaller"
  check rejected the fold, which the plugin classified as a summary failure
  and silently settled (observed live: a 497-token task never folded, no
  warning). The archive preview now scales with the span: metadata +
  preview stay under ~15% of the span's estimated size (30-line cap
  unchanged); tiny spans get the metadata bullet plus a pointer to the
  artifact instead of inline lines, and fold normally.

## 0.18.0 — Fold archive: a proper summary section with the span preview (2026-09-04)

- **Features**: the metadata appended inside each committed summary node is
  now a section formatted like the summary's own five, restoring the
  per-message preview that 0.17.0 had dropped:

      ## Fold archive
      - fold #N · originals (JSONL, one message per line): <path>
      Span preview (N messages, one per line — …):
        1 assistant: …
        2 tool: …

  Preview line N maps to artifact line N. The bare footer line form from
  0.17.0 is gone; prompts (task_end description, lifecycle section)
  describe the section accordingly.

## 0.17.0 — fold metadata lives inside the summary node; no notice messages (2026-09-04)

- **Behavior**: the scoped summarize override — the last stop before the
  engine commits — now computes the fold number (per-session summary count
  + 1) and writes the JSONL artifact from `input.messages` (the engine's own
  span derivation), then appends a footer line to the summary text:
  `[fold #N · originals (JSONL, one message per line): <path>]`. The
  committed summary node carries its own recall handles.
- **Removed**: the separate one-shot fold-notice message and all its
  machinery (pre-step message append, context-callback drain queue, plugin
  source/id shaping) — the ordering, injection-timing, and message-shape
  problems disappear with it. `foldRegion` reports tokens only; the
  spanMessages helper is gone (the artifact is written inside summarize).
- Prompts (task_end description, lifecycle section) updated to describe the
  embedded footer. If the engine rejects a commit after summarize, the
  pre-written artifact becomes an orphan temp file — harmless.

## 0.16.1 — fix corrupt session: notice messages need an id (2026-09-04)

- **Fix**: the host commits every pre-step decision message as a
  `user/message` session event VERBATIM, and load-time validation rejects a
  message event without a string `id` ("session event at seq N lacks an
  identified message") — marking the WHOLE stored session corrupt and making
  its history unloadable. The 0.15.2 notice shape `{role, content, source}`
  still lacked that `id`, so every session that received a same-request fold
  notice under 0.15.2+ became unloadable after restart (one such session
  bricked at seq 379458). Notice messages now carry `id: randomUUID()`,
  matching the host's own `createUserMessage` contract.

## 0.16.0 — fold region is exactly begin..end, deliverable stays on the surface (2026-09-04)

- **Behavior**: the deferred fold's region is now [begin anchor .. the
  task_end result's own seq] INCLUSIVE — previously it ran to the last
  surface node at fold time, sweeping in the deliverable and any post-end
  steps. Everything written after the end (the report, probes, later turns)
  stays on the surface untouched; a later task's own [begin..end] region
  swallows those leftovers in turn. The deliverable gate is unchanged (fold
  still fires only at the first step boundary after the deliverable lands);
  if the close result itself was shadowed by AUTO compaction the entry drops.

## 0.15.2 — fix crash: appended notice messages need a source (2026-09-04)

- **Fix**: v0.15.1's same-request notice appended bare `{role, content}`
  messages to the pre-step decision. The host commits every decision message
  as a user/message event, and its runtime-context projection then reads
  `message.source.kind` on each — the missing `source` crashed every step
  with "Cannot read properties of undefined (reading 'kind')". Notice
  messages now carry `source: {kind: 'plugin', plugin: 'dsh-taskfold'}`: not
  the system-prompt snapshots' identity (never suppressed), and the notice —
  including its artifact path — survives durably in history.

## 0.15.1 — fold notices arrive in the same request as the fold (2026-09-04)

- **Fix**: the host assembles a step's context BEFORE dispatching the
  pre-step waterfall, so a fold committed inside the hook could only surface
  its notice at the NEXT assembly — one step late. The listener now appends
  the notice (fold number, tokens, artifact path, per-message preview) as a
  user message to the waterfall decision's messages (defensively copied), so
  the model sees it in the very request the fold committed in. The
  context-callback queue remains as a fallback when the decision cannot be
  augmented (e.g., rejected decisions), with no double delivery.

## 0.15.0 — task_end for the model, system-only folds with old-style output (2026-09-04)

- **Behavior**: the model-facing close tool is renamed `task_fold` →
  `task_end` (closes the task, queues archival; success text now reads
  "Task ended: …"). Folding is executed ONLY by the system at the next step
  boundary — the manual "call task_fold again to force the fold" escape
  hatch is removed, and failed auto-folds just retry (nudge wording updated).
- **Behavior**: the system-executed fold now produces the old-style output
  the model used to get from task_fold: the JSONL artifact is written, and a
  ONE-SHOT runtime-context notice reports the fold — fold number, token
  count, artifact path, and the per-message preview — retracting on the next
  render.
- **Compat**: legacy logs with 'Task folded: ' results replay identically
  (reducer, grace scan, and intent registration accept both prefixes and
  both tool names); prompts (section, descriptions, todo bridge, nudges)
  updated to the task_end vocabulary.

## 0.14.1 — fix host crash: pre-step listener must follow the waterfall contract (2026-09-04)

- **Fix**: v0.14.0's `agent/pre-step` listener returned undefined and never
  called `next()`, so the host's waterfall decision was undefined and every
  step crashed with "Cannot read properties of undefined (reading 'kind')".
  The listener now awaits deferred folds inside the hook (as the engine's own
  AUTO compaction does) and always returns `next()`.

## 0.14.0 — full-deferred folds: queue on close, auto-fold after the deliverable (2026-09-04)

- **Behavior (design `docs/design/deferred-report-fold.md` v5)**: `task_fold`
  now closes the task and QUEUES archival; the span folds AUTOMATICALLY at
  the next agent step boundary after the task's deliverable text lands
  (possibly mid-turn). Every deliverable — user report or subagent report —
  is written with full uncompressed context; folding never precedes it.
- **Mechanism**: `taskMarks` state v9 adds `pendingArchives` (registered on
  close, dropped when a compaction event shadows the anchor); the
  `deferredArchivePlan` pure function implements the deliverable gate
  (reasoning/tool-calls are not deliverables; out-of-order deliverables defer
  behind the successor anchor; region ends before any still-open/pending
  successor); an `agent/pre-step` handler folds gated entries innermost
  first, re-reading state before each, with a 120s summarization signal guard.
- **Escape hatch & safety**: calling task_fold again for a queued task forces
  the fold immediately; a HOLD runtime-context warning appears while an
  auto-fold keeps failing (engine busy etc.); too-small spans settle without
  folding. `list_folds` titles deferred folds via the summary's `# <name>`
  heading (in-flight-call correlation kept as the primary path).
- **Cleanup**: `foldDecision` (superseded by closeTarget + deferredArchivePlan)
  removed with its tests; tool and system-prompt copy rewritten for the new
  contract. State v8 → v9 (mismatch = full replay, old logs byte-stable).

## 0.13.0 — deliver-then-fold contract (2026-09-04)

- **Behavior**: the closing order is inverted and unified. When a task's work
  is done, the report or deliverable (to the user, or a subagent's report to
  its parent) is written FIRST, with full context, in its own step — then
  task_fold is called immediately, alone in a step, folding the span with the
  deliverable included. This removes the fold-first/relay ordering that made
  deliverables derive from compressed summaries (the subagent quality issue),
  keeps done-state dwell at zero (no context interleaving, minimal KV-cache
  invalidation), and absorbs the v0.12.0 delegation exemption into one rule.
  Deliver and fold must be SEPARATE steps (a deliverable sharing the
  task_fold message falls outside the fold).
- `task_fold`'s result now carries a deviation check instead of a relay
  instruction: only if the deliverable was never sent in an earlier step (and
  no tasks remain open) does it direct writing one from the fold summaries.
- Fold summaries: delivered reports inside the span are cited, not restated;
  the relay rule stays as fallback for never-sent deliverables.
- Mechanism unchanged (foldDecision/execute/boundary fallback identical to
  0.12.0); design converged over a four-round adversarial review
  (docs/design/lazy-fold.md).

## 0.12.0 — five-round prompt overhaul + delegation deliverable carve-out (2026-09-04)

- **Prompt (five-round adversarial review, fully landed)**: granularity rule
  (a task produces a verifiable outcome; single reads/greps are steps; when in
  doubt it is a task), bookmark-not-deadline suspension semantics, detour
  wrapping folded into the multi-part MUST, dedup of LIFO/exception detail
  between section and descriptions (~40% fixed overhead cut), summary budgets
  (80–150 words typical / 250 large / 300 cap; User inputs uncapped by
  design), What-happened vs Changes boundary (grep-able), on-demand recall
  recipe with dependency line ("when a new task depends on a folded task,
  recall it before starting"), nudge-2 three-way branch (done / blocked /
  genuinely waiting), todo bridge granularity fix, relay text synced.
- **Prompt (timing fix)**: a delegation's final deliverable (a subagent's
  report to its parent) is composed and sent with FULL context BEFORE
  folding — fold afterwards as the archival last action. Evidence: the
  five-round reviewer's own artifacts showed every deliverable was written
  post-fold from the summary alone (round 1: 47 KB of reading compressed
  before its review was composed). Fold-first now applies only to interactive
  closing reports to the human user.
  - five-round prompt review — granularity rule, bookmark semantics, dedup, summary budget, dependency recall

## 0.11.0 — runtime-context snapshots read harness: in the preview (2026-09-04)

- **Features**: plugin-injected messages (runtime-context snapshots, sandbox
  and approval policy lines, lifecycle nudges) arrive as user-role messages;
  the preview now labels them `NN harness: …` via their `source.kind ===
  'plugin'` provenance. Three-way distinction at a glance: `user:` genuine
  human input, `tool:` tool results, `harness:` environment snapshots.

## 0.10.0 — preview distinguishes tool results from user input (2026-09-04)

- **Features**: harness tool results arrive as user-role messages, which made
  them indistinguishable from genuine user input in the span preview. A
  message whose blocks are all tool-results now reads `NN tool: ←…`; real
  user text (and runtime-context snapshots) keeps `NN user: …`. Mixed-block
  messages keep their original role label.

## 0.9.0 — teach the model the recall path (2026-09-04)

- **Prompt**: the lifecycle section now carries the full recall recipe —
  `list_folds` (title + fold number index) → `fold_recall({ fold: N })`
  (regenerates the span's original messages as JSONL) → read/grep it; when a
  fold summary lacks a needed detail, recall instead of guessing or asking
  the user.
- **Prompt**: fold summaries keep their anchors (file paths, commands, error
  strings) precise — paths verbatim — so they answer most questions alone and
  double as grep keywords when recall is needed.

## 0.8.0 — common preview glyphs: ← for results, ↵ for line breaks (2026-09-04)

- **Fix**: the preview used rare glyphs that read oddly (hollow double-stroke
  `⇐`, and `⏎`). Tool results now use the common left arrow `←`, and original
  line breaks render as `↵` (the return-key symbol). Tool calls keep `→`.

## 0.7.0 — JSONL artifacts slim to role + content (2026-09-04)

- **Breaking (artifact shape only)**: each JSONL line is now the message
  reduced to `{role, content}` — full original content blocks, no host
  provenance metadata (`source`, `replayState` with provider/model/responseId,
  message ids). Recall serves content recovery; audit metadata stays in the
  durable event log. Lines get shorter and easier to read back before the read
  tool's truncation. Preview/line-number contract unchanged; existing `.jsonl`
  artifacts with metadata remain valid JSONL and still parse.

## 0.6.1 — pwsh brief falls back to the command when description is missing (2026-09-04)

- **Fix**: `description` is model-provided and can be absent or empty; the
  pwsh call brief now falls back to a slice of the command itself instead of
  rendering `→pwsh ‹›`.

## 0.6.0 — web-UI-style briefs for common tools in the span preview (2026-09-04)

- **Features**: preview fragments are now tool-aware (one line per message is
  unchanged — the JSONL line mapping contract holds):
  - `read`/`write`: full file path on the call side; the result side shows a
    content excerpt instead of repeating the path header.
  - `edit`: path plus a line delta (`→edit p.mjs +4 -2`) computed from the
    old/new strings.
  - `grep`: call shows the pattern (and include filter); result collapses to
    match stats (`⇐5 matches · 2 files`).
  - `pwsh`: call shows the command's description (`→pwsh ‹Run live probe›`).
  - Unknown tools keep the generic `→name(args)` / `⇐excerpt` form; results
  correlate with their call via toolCallId, degrading safely without the map.

## 0.5.2 — relay instruction moved to its own line (2026-09-04)

- **Fix**: in `task_fold`'s success output, the relay instruction was appended
  to the last span-preview line with only a space, reading as if it were part
  of the folded message. It now starts on its own line after the preview.

## 0.5.1 — span preview marks original line breaks with ⏎ (2026-09-04)

- **Fix**: preview lines flattened ALL whitespace, so multi-line tool results
  read as if a line break were missing. Newlines inside a block now render as a
  visible `⏎` marker — the preview stays one line per message, but the model
  can see where the original line breaks were.

## 0.5.0 — span preview in fold output, JSONL artifacts (2026-09-04)

- **Behavior**: `task_fold`'s result now carries a one-line-per-message
  preview of the folded span — every folded message summarized as a single
  numbered line (`NN role: [think]… →tool(args) ⇐result`, whitespace
  flattened, clipped), capped at 30 lines with an overflow pointer.
- **Artifact format change**: span artifacts are now **JSONL** — one message
  per line, in the exact order and numbering of the preview lines, so the
  model can map a preview line straight to its full original by line number.
  `fold_recall` regenerates the same JSONL format and also prints the
  preview. Old `.json` artifacts already on disk remain readable.
- New shared module `plugins/span-preview.mjs` (single source for both
  plugins, keeps preview and file numbering in lockstep); 4 new tests
  (51 total).

## 0.4.3 — supported-dsh-versions declaration + release-flow reminder (2026-09-03)

- **Docs**: both READMEs gain a "Supported dsh versions" section — known-good
  `0.1.2-alpha.5`, minimum `0.1.2-alpha.5` (older alphas differ in the
  compaction-engine internals), upper bound untested/unenforced (dsh has no
  host-version negotiation yet; incompatible hosts degrade, never corrupt).
- **Tooling**: `release` flow now shepherds that section — `draft` prints a
  reminder to update it when compatibility changes, and `release` warns if
  either README is missing the section entirely.

## 0.4.2 — README rewritten around benefits, "effectively infinite context" tagline (2026-09-03)

- **Docs**: both READMEs rewritten — lead with a one-line value proposition and
  the tagline "taskfold — effectively infinite context for your coding agent",
  explain the mechanism in plain words (fold finished tasks into summaries,
  recall originals on demand), and trim implementation detail (the old deep
  dive remains in git history).

## 0.4.1 — closing report becomes conditional (2026-09-03)

- **Behavior**: the closing report is now conditional, not mandatory. After a
  successful fold, a report is written only when the user is still owed one;
  skip it when the outcome was already reported during the work, and never
  write one for an inner subtask fold — continue the surrounding work instead.
- The relay instruction in `task_fold`'s success text now carries the same
  condition, and the lifecycle section spells out the skip rules.

## 0.4.0 — fold-first closing contract (2026-09-03)

- **Behavior**: the task lifecycle section now orders the closing sequence —
  when a task's work is done, `task_fold` is the FIRST closing action, and the
  user-facing closing report is written AFTER the fold, based on the fold
  summary node then present in context. This resolves the double-summary
  conflict: no more "summary of a summary" (fold-then-report from memory) and
  no more reports swallowed into their own fold (report-then-fold).
- `task_fold` success text ends with an explicit relay instruction (summary
  node in context — adapt wording for the user, no second summary layer);
  degraded paths (tooSmall/unfolded) stay unchanged.
- `FOLD_SUMMARY_INSTRUCTION` gains a rule that the summary doubles as the
  user-facing closing report basis and must stay human-readable; test pins it.

## 0.3.0 — release flow script (2026-09-03)

- **Release tooling** (`scripts/release.mjs`, offline-tested in
  `test/release.test.mjs`): CHANGELOG.md is the single source of truth for the
  version; `package.json` is synced by the script, never by hand — the 0.1.0
  desync cannot recur.
  - `draft`: groups commits since the last version tag (Conventional Commit
    prefixes) into a dated draft entry prepended to CHANGELOG.md for human
    review.
  - `release`: reads the top CHANGELOG version, syncs package.json, commits,
    tags `vX.Y.Z`, pushes master and the tag; a push blocked by the
    environment enters a PENDING state and re-running resumes it.
  - `status`: reports current version, CHANGELOG/package.json agreement, and
    unpushed commits/tags.
- README (en/zh): "Release flow" usage section.
- Fix: the package.json version rewrite regex missed the key's closing quote
  (`"(version)\s*:` never matched `"version":`), which aborted the first real
  `release` run mid-way; corrected and verified against the live file.

## 0.2.3 — mandatory-tone lifecycle section

- **System prompt section rewritten from descriptive to mandatory**: opens with
  "MANDATORY task lifecycle discipline: every discrete task MUST be wrapped in
  task marks"; unmarked tool work on a discrete task and leaving a finished
  task open are both named protocol violations; multi-module work MUST be
  split into nested subtasks; runtime-context nudges are "binding, not
  advisory". Dropped the `## Task lifecycle compaction` heading (the section
  now reads as directives, not documentation).

## 0.2.2 — fold summaries keep user inputs and pitfalls

- **Five-section fold instruction** (`FOLD_SUMMARY_INSTRUCTION`, exported for
  offline tests): fold summaries now carry `## User inputs & decisions` (the
  user's requests, corrections, answers, approvals from the span, verbatim
  where wording matters) and `## Pitfalls & gotchas` (failed attempts and why,
  workarounds, environment traps, "don't do X again" lessons) as first-class
  sections alongside What happened / Changes / Outcomes.
- Borrowed rule from the stock checkpoint instruction, reworded for folds:
  capture user feedback faithfully, especially corrections; never drop why
  something failed. Continuity-checkpoint sections (Pending Jobs / Current
  Work / Next Step) stay banned — they contradict the fold's CLOSED-task
  contract.

## 0.2.1 — nested folds own their full span

- **Region runs to the last surface node**: a task's final body message now
  folds into its OWN fold instead of leaking into the parent's span artifact;
  nested folds each recall their complete original context. Explicit task
  folds need no live-edge margin (auto compaction keeps its own).
- **Self-fold defense**: if a host commits the in-flight step before tool
  execution, the assistant message carrying the `task_fold` call itself is
  excluded from the region (`foldDecision` takes an optional `events`
  argument).
- **Nesting guidance in prompts**: the system-prompt section and the
  `task_begin` description now tell the model tasks nest hierarchically
  (innermost folds first; every fold keeps its own recallable context).

## 0.2.0 — review-hardening generation

External code review triage: no deterministic data-loss bugs, but semantic
edges, comment drift, and dead code — all addressed.

- **LIFO closing at the tool layer** (`closeTarget`/`foldDecision` pure
  exports): only the innermost open task can close; the projection stays
  name-keyed so pre-LIFO logs replay byte-identically.
- **Graceful unfold degradation**: when the engine cannot be resolved or the
  begin mark was shadowed by another fold, `task_fold` still closes the task
  (`unfolded: 'engine' | 'anchor'`) — no more permanent retry loops.
- **Fold numbering contract**: `list_folds` renders chronological `#N (seq X)`
  — the exact domain `fold_recall` validates; `task_fold` reports the same
  chronological number.
- **Name hygiene**: `validTaskName` rejects empty names and those containing
  the ` — ` separator; `task_begin` also rejects reopening an already-open
  name; legacy logs with separator-bearing names match by exact equality.
- **Per-session closing declarations**: the `__closingTask` engine field
  became an apply-scoped map keyed by session id (no cross-session title
  pollution in single-process multi-session hosts).
- **Todo bridge v2**: replaced the two counting nudges with a transient,
  stateless status line rendered the round after any `todo_write` —
  `Todo bridge: todos changed; open tasks: …` — reporting the open-task
  roster; the model decides begin/fold.
- **Hygiene**: ~70 lines of dead surface-indexing removed (readSurface/
  classify/indexEvents), stale header/NOTE comments rewritten (engine is
  always the plugin-built ScopedEngine), version annotations unified;
  nudge-2 now scans all open marks for the oldest.
- Tests 21 + 11 offline suites; no reducer or stateVersion change.

## 0.1.0 — plugin bundle generation

The preset grew into an installable dsh plugin bundle while keeping the
agent-preset install path.

- **Bundle packaging** (`30b8b1d`): `package.json` with `dsh.bundle.patch`
  declaration plus a host-plane `cordis.patch.yml`; `dsh plugin add` installs
  the tool family globally for every preset's sessions.
- **Self-hosted engine tier** (`ee67f56`, `65a00f6`, `c28d1a9`): `compaction`
  dropped from `inject`; `engineFor()` resolves a realm engine via
  `ctx.get('compaction')`, else lazily constructs
  `BasicCompactionEngine(ctx, { auto: false })` with a file-URL resolution
  fallback that walks up from host anchors. Validated live on a
  minimal-derived preset with no compaction rows at all.
- **Hold-semantics nudges** (`353935b`, `6cc390d`, `63d0633`): lifecycle
  nudges render while their condition holds and retract when it clears;
  cooldown machinery removed. Byte-stable wording ("20+ rounds") so a held
  line emits zero snapshots. The uncommitted backstop appears immediately
  once the step after `task_end` misses `task_commit`; the begin-nudge is
  suppressed while a fold question is open (pending `lastEnded` or a 3-round
  post-outcome grace).
- **Too-small abandonment** (`0f28656`, `bd30f3e`): a
  `task_commit failed (summary)` verdict durably abandons the ended record
  (the span never grows after end), so the backstop nudge cannot hold forever.
  Landed with a `stateVersion` bump after learning that BEHAVIOR changes —
  not just shape changes — require the bump for persisted rows to re-fold.
- **dsh 0.1.2-alpha.4 compatibility** (`3fa52fd`): `Session.events` replaced
  by on-demand APIs; both plugins read the log through a cross-version
  `sessionEvents()` accessor (`snapshotEvents()` on alpha.4+, array before).

## Named tasks generation

- **Named task lifecycle** (`102d260`): `task_begin({ name })` /
  `task_end({ name })` — identity by name, LIFO only within a name; a
  mismatched close cannot corrupt other tasks. `task_commit` folds the full
  span (begin pair + body + end pair) and labels the fold with the task name.
- **Two-tool end/commit split** (`c70ca87`): `task_end` is a pure state
  transition whose output carries the whole status (zero context injection);
  `task_commit` performs the explicit inline fold. The async follow-up fold
  (listener/timer/maintenance) line of designs was explored and abandoned —
  terminal blockers: append reentrancy, whole-surface stability assertion,
  and the agent active-work paradox.
- **Phantom self-heal** (`89713eb`): projection v6 drops unclosable nameless
  legacy marks at load; legacy `task/mark` snapshot coercion no longer
  creates them.
- **Output-carried state** (`cc4cb49`): the standing "open task marks"
  runtime-context line and the immediate lastEnded nudge removed — tool
  outputs are the sole lifecycle surface; runtime context keeps only the
  todo bridge.

## Region compaction generation

- Manual `compact(start, end)` / `compact_inspect` with pairing-balanced
  edges and corrupt-edge re-baselining; `compact_stats` / `compact_recall`
  observability over the append-only log (fold manifests, seq-addressed
  archive recall, title extraction from task names).
- Task marks as a **derived** session projection folding harness-native
  events only — no custom event types; marks survive restarts, resume, and
  compaction. The v1 whole-value `task/mark` events survive only as legacy
  reset points for pre-v2 ghosts.
