# Changelog All notable changes to this project are documented per commit series; versions here follow the preset/plugin generations (not npm releases yet). ## 0.34.5 — the handoff shape, stated so a literal reader cannot end the turn (2026-09-15) A live session (project `yxx`) went quiet right after a task closed. The model had closed its task, written the report as a plain text message, and planned to start the successor `task_begin` in the "next step" — a step that never came. An assistant message with no tool call IS the end of a turn (`if (toolCalls.length === 0) return { kind: 'completed' }` in the host's agent loop), so the work the model had just promised never started. Nothing errored: the turn closed as `completed`, and the fold activity running immediately after the report made the UI look busy while it was already over. The user's next message was "?". The trigger was our own wording. The section and both descriptions said a task-mark call must stand "alone in a step" / in "its own message", and a model reading that literally concluded the report message had to be text ONLY. It was obeying the contract we wrote. Two independent passes (a five-round implementation review, then a GLM-5.3 prompt audit) confirmed the direction of the fix and found the remaining inaccuracies below — and found that the shape the 0.34.3 guard rejects fails loudly, while the shape that caused this incident was perfectly legal and silently fatal. - **Fixes (prompts — `616e547`)** - "only" scopes correctly everywhere: it constrains task-mark CALLS. Text, reasoning, `read` and `present` may sit beside the single mark, and the section now says so in those words. Every "alone in a step" / "its own message" phrasing is gone. - the turn-ending rule is stated with BOTH exits: when the turn still has work, the report message must carry the tool call (normally the successor `task_begin`) — a text-only report is a STOP, not a pause; when you are done or waiting on the user, a text-only report is exactly right. The `task_end` description and its result text carry the same exit, and the shape example is now the supported one: the report shares its message with whatever mark comes next. - the rejected shape is described accurately: a second task-mark call in one message fails BOTH calls (the guard is symmetric — see `test/task-marks.test.mjs`), the set is `{task_begin, task_end, task_fold}` rather than begin/end, non-mark partners are unrestricted rather than the enumeration "reads and present", and the other failure reasons are named instead of dropped. - span and timing anchors: the fold span opens after the LAST result of the begin-carrying message (plus parallel partner results), the gate message is named, and folds fire at the step boundary after that message rather than "as soon as it lands". - the nudge channel is described as injected messages. "Runtime context carries lifecycle nudges" was false — nudges arrive through `agent/pre-step` — and a nudge can never arrive after a turn ends, which is exactly why none of the existing nudges fired in the incident above. - size, net against v0.34.4: the three always-visible prompt surfaces (section plus both task-mark descriptions) grew 5674 → 6549 chars (≈ +290 tokens per request). The turn-ending rule needs that space; the audit then trimmed the section from 4020 to 3732 (−288) while keeping every constraint, and rejected a proposed −21.6% rewrite because it paid for its savings with three known semantic errors. The remaining overlap — "mark exclusivity" and "text ends the turn" are each stated in the section AND in both descriptions — is where the next cut should come from. - **Verification** — 182 pass / 0 fail across 13 files (181 before; the 6 new pins in `test/prompt-contract.test.mjs` decode the prompt literals and assert both directions — new contract present, old phrasings absent). Every edit was checked against the byte-stable latch and the load-bearing `'Task ended: '` reducer prefix before landing. `npm run verify:cache` → 14 judged / 14 pass; the fold summarization envelope is untouched. ## 0.34.4 — accept the summaries models actually write; make every prompt layer tell the same truth (2026-09-12) Two five-round reviews — an implementation audit, then a prompt audit — converged from opposite sides on the same picture: the scheduling machinery (region planning, settle, backoff, replay) survived every adversarial pass, but the ACCEPTANCE face turned frequent, benign summary format deviations into deterministic retry loops, and the prompts still described the pre-0.34.2 semantics in four places while carrying an authority conflict between the section and the guard it describes. - **Fixes (fold acceptance — `dd8c00e`)** - bounded preamble tolerance in the structural receipt check: a summary whose first `## ` heading arrives after a short lead-in (≤3 lines, ≤400 chars) now passes with the preamble stripped. The audit session itself hit the old behavior twice — "summarization produced no text summary content" after a leading sentence, four full-price retries, then a budget settle; with the tolerance both folds would have committed on attempt 1. No heading at all, or an oversized preamble, still fails loud. - build-time fail-fast for the degraded Assembler: when `buildScopedEngine` can never produce content blocks (e.g. a host API surface where blocks() is permanently empty), it now throws immediately, routing through fold-drain's zero-LLM settle path instead of billing full-price summarization retries that are structurally incapable of succeeding. - second-chance settle for `'not smaller'`: one transient not-smaller comparison no longer permanently settles a row; two consecutive occurrences are required. - stale HOLD filter: `autoFoldFailures` entries are filtered against the live archives projection before rendering, so a row that settled through the reducer no longer prints a permanent false "folding" notice. - **Fixes (prompts — `297e5a2`)** - accuracy: the task_end description's "retry at every step boundary" now matches the classified backoff shipped in 0.34.2; FOLD_BOUNDARY_RULE gains the end-side PARALLEL-END extension and scopes "parallel partner results stay outside" to the BEGIN side; the section and fold_recall describe the span as closing with the last result of the task_end message; fold_recall's span sentence distinguishes task folds from auto-compaction folds. - authority conflict ended: the section's "alone in a step" (×2) becomes "as the only task-mark call in its message" — until now `task_begin` + `read` in one message was legal by tool description, illegal by section, and silently allowed by the guard; section, descriptions, and guard now state one contract. - standing token trim, net −347 chars (≈ −89 tokens per request): the task_end description drops its triple-nested archive-anatomy parenthetical and compresses the ONE-call tail; task_begin compresses its ONE-call parenthetical; the section drops the ONE-call mechanism parenthetical. Every deleted fact survives verbatim in a remaining layer or in the guard's error text. - polish: the shape example's outer close gains its `→ report` node; the Fold-archive anatomy sentence is compressed; "never track message positions yourself" becomes actionable ("never estimate message positions or line numbers from memory — copy them from a visible index or quote a fragment"); report/deliverable unified. - **Verification** — 171 pass / 0 fail / 5 noReact skips (163 before this cycle; the 8 new tests pin the four acceptance fixes). Every prompt edit was verified for zero test pinning and zero BYTE-STABLE exposure before landing; fold-envelope bytes are untouched, so verify-cache does not apply. ## 0.34.3 — one task-mark call per message: enforced at execute time, taught in every prompt layer (2026-09-12) 0.34.2 made the parallel end→begin shape SAFE (the close settles on its first fold) but could not make it cheap: a `task_begin(B)` riding the close message shares `task_end(A)`'s anchor message, the closing fold swallows it, and B ends closed-unfolded — no archive of its own, its content live until a later span sweeps it. The live logs also showed WHY models relay: the prompt's shape example drew `task_end → task_begin` as an unbroken arrow chain with no message boundaries, and the model implemented exactly that as parallel calls. Two commits close the shape itself. - **Fixes** - reject relayed task-mark calls at execute time. The host plugin API has no pre-execution interception hook (`agent/pre-step` gates user messages only, `agent/request` cannot mutate messages), but a tool handler executes with its carrying message as the last assistant message on the surface — so `task_begin`/`task_end` now inspect it via the pure helper `siblingTaskMarkCalls` (dual access paths, event-at-seq and snapshot, the same ones anchoring already uses) and REJECT the call when the message carries another task-mark call (`task_begin`/`task_end`/`task_fold`), with a corrective error naming the re-issue path (`task_end` through the lifecycle hint channel). Both directions reject, so `[task_end(A), task_begin(B)]` leaves nothing changed and the model re-issues each alone: end and start each land on their own message boundary, and every task keeps its own archive. Non-mark partners (`present`, reads) still pass — those shapes were already handled by 0.34.2's PARALLEL-END extension and touch no anchor. An unreadable carrier yields a null verdict and the guard degrades open: older hosts never see a false rejection. - state the standalone-call contract in every prompt layer. The `task-marker-compaction` system-prompt section now declares "ONE task-mark call per message, always" where the discipline begins, names the end→begin relay trap and its anchor-swallowing consequence, and demands the successor's `task_begin` as its own FOLLOWING message; the shape example now carries message boundaries (`task_end "review PR #98"` → report → `task_begin "review PR #99"`, a report after every close); both tool descriptions upgrade the advisory "Call alone in a step" to "This MUST be the only task-mark call in its message … rejected at execute time", each with the concrete consequence and the re-issue instruction. The prompt teaches the rule so a compliant model never pays the rejection round-trip; the guard enforces it for the rest, and 0.34.2's extension stays as the safety net for pre-guard logs and non-mark partners. Verification: offline suite 163 pass / 0 fail (5 skipped are the React/DOM `{ skip: noReact }` cases, environmental; 168 tests total, 12 new since 0.34.2); new tests lock the relay rejection in both directions, the single-call pass, the non-mark partner exemption, the null-verdict degradation, and that both access paths read the LAST assistant message. The summarization request envelope is untouched (guard and prompt text only), so the prefix-cache verification deltas of previous releases carry over unchanged. ## 0.34.2 — settle deferred archives on the first fold, never drop a starved drain (2026-09-12) Two compaction bugs found live on dsh 0.1.5 (the wxgame workspace, plugin v0.34.0), both stemming from the same operational shape: the host's task lifecycle asks the model to end a task and deliver the next step in ONE assistant message, so a `task_end(A)` frequently travels together with the successor's `task_begin(B)` — or a `present` — as parallel tool calls. - **Fixes** - extend the deferred archive END past parallel close-call results. When the close-carrying message also calls other tools, their results follow the close result on the surface, so a cut AT the close result splits those call/result pairs — an unbalanced END boundary. The shrink walk then committed regions ending BELOW the close result, `foldResultSeq` was never shadowed, the row never left `pendingArchives`, and every subsequent step boundary re-planned and re-summarized the previous summary node: one task was summarized 5 times, four tasks 3–5 times each — 13 redundant summarization calls in a single session, each with its own artifact file. `deferredArchivePlan` now resolves the close message's full call set (`closeMessageCallIds`, two-step pure resolution) and extends the END to the last partner result still on the surface — the mirror of the existing parallel-BEGIN guard — so the committed region shadows the close result and the row settles on the first fold. When the log lacks a close event the guard skips and the plan is byte-identical to the previous behavior. - back off unsettled commits instead of re-planning them free at every boundary. A fold that commits below the close result used to leave its row queued for the next pass to re-plan immediately — which is exactly the cascade above. Such a commit now joins the shared backoff schedule (`recordAttempt` + `backoffPasses`), ends the pass (`skipped.add`), and records a HOLD failure line naming the state; the sibling `task_begin`'s archive row still drops at plan time as it always did. - chain starved drain calls instead of dropping them. `drainRunning` is a process-global singleton (subagent sessions share the process), and the cascade above held it across a sibling session's `agent/turn-stopping` hook: that session's closed task never folded — zero compaction events, no artifacts, the archive queued until a resume. A drain call that finds the pass busy now queues its agent (bounded, `MAX_DRAIN_QUEUE = 8`) and the running pass chains one more pass for it in its `finally`, under a timeout-only signal — the starved call's own turn is over by then, so its hook signal cannot be reused. Beyond the queue cap the old semantics (retry at the session's own next boundary) keep applying. Verification: offline suite 151 pass / 0 fail (5 skipped are the React/DOM `{ skip: noReact }` cases, environmental); new tests lock the parallel-end plan (`task-marks`), the two-pass shrink-plus-backoff behavior, and the cross-session starvation chaining (`fold-drain`). The summarization request envelope is untouched (region boundaries and drain scheduling only), so the prefix-cache verification deltas of previous releases carry over unchanged. ## 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-.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:` (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` 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 `", attempt N: "`. 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 `# ` 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 `` / `` 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-`; 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-` 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-` 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-`, 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 | 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 `# ` 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\\-.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): 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): ]`. 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 `# ` 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.