Releases: yindf/taskfold
Release list
v0.34.6
The 0.1.5-alpha.2 → 0.1.6-alpha.2 host upgrade changed two seams this
plugin rides. All nine audited seam packages changed implementation files
(unlike the rc.1→rc.2 round, where only 7 changed and the two imported
ones were byte-identical), and two of the changes bit:
-
The recover loop.
summarizeCompactionnow wraps summarize in an
error-recovery loop: on failure it calls
ctx.waterfall('compaction/summary-error', …, () => false)before
rethrowing. The ScopedEngine's shim ctx had nowaterfall— so every
scoped-fold summarize failure surfaced as a maskingTypeError
instead of its cause. Seen live before the fix: one failed fold on the
running host (fold failed, attempt 1: this.ctx.waterfall is not a function), after which the host's own pressure compaction swallowed
the span — the documented degradation mode, in production, from a bug
that hid its own diagnosis.shimWaterfallnow delegates to the real
cordis Context when it has the method — host recovery listeners
(image offload) apply to scoped folds exactly as to the realm engine's
— and otherwise runs the caller's own default, preserving pre-0.1.6
semantics (rethrow → drain retries at the next boundary). Three
offline tests pin the contract (verbatim delegation, default-run
fallback, propagation on throw). -
The prefix-cache anchor. The zai/glm adapter now threads
reasoningEffortinto the request's token stream. Fold options
carried only provider/model — so the fold request diverged from every
main request right after the system+tools block and re-billed its
whole prefix: measured live, 48.0% hit with tail +8444 (a textbook
verify-cache regression signature, caught by exactly the check the
flow mandates after every host upgrade).foldRequestOptionsnow
mirrors the hostbuildRequest()'s shape — the latest routed
requestHeader config rides along,maxTokensstripped after the
spread (the no-summary-cap product ruling stands; generation params
do not affect the input-prefix cache). With the spread: 90.6% hit,
tail +712; after the maxTokens strip (final shape): 90.0%/89.7% hit,
negative tail.
Verified on 0.1.6-alpha.2: 188/188 offline (the 182 that shipped with
0.34.5 plus three shimWaterfall tests and three foldRequestOptions
tests), 12/12 host-API probe against the real engine+assembler, a source
audit of all nine runtime seam surfaces in the installed host, and live
folds on the running host with the mounted copy byte-identical to HEAD
passing verify-cache --since-restart.
Release channels split (user decision): from this release, master
carries only rc-channel releases and stays at the newest verified rc
(v0.34.1; reset with force-push); alpha-channel releases are committed
and tagged on the alpha branch — this is the first one. The release
script needed no change: it pushes the current branch and the tag.
README raw-asset URLs on the alpha branch now point at /alpha/.
- Fixes
- dsh 0.1.6-alpha.2 seams — recover-loop waterfall + routed-config spread
- Docs
- alpha channel verified through 0.1.6-alpha.2; channel-branch release policy
Prebuilt plugin bundle attached: dsh-taskfold-0.34.6.tgz (npm pack of this tag).
v0.34.5
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,readandpresentmay 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
successortask_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. Thetask_enddescription 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.
- "only" scopes correctly everywhere: it constrains task-mark CALLS.
- Verification — 182 pass / 0 fail across 13 files (181 before; the 6
new pins intest/prompt-contract.test.mjsdecode 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.
Prebuilt plugin bundle attached: dsh-taskfold-0.34.5.tgz (npm pack of this tag).
v0.34.4
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
buildScopedEnginecan 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:
autoFoldFailuresentries 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.
- bounded preamble tolerance in the structural receipt check: a
- 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+readin 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
→ reportnode;
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.
- accuracy: the task_end description's "retry at every step
- 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.
Prebuilt plugin bundle attached: dsh-taskfold-0.34.4.tgz (npm pack of this tag).
v0.34.3
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-stepgates user messages
only,agent/requestcannot mutate messages), but a tool handler executes
with its carrying message as the last assistant message on the surface —
sotask_begin/task_endnow 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_endthrough 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-compactionsystem-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'stask_beginas 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.
- reject relayed task-mark calls at execute time. The host plugin API has no
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.
Prebuilt plugin bundle attached: dsh-taskfold-0.34.3.tgz (npm pack of this tag).
v0.34.2
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,foldResultSeqwas never
shadowed, the row never leftpendingArchives, 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.
deferredArchivePlannow 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 siblingtask_begin's
archive row still drops at plan time as it always did. - chain starved drain calls instead of dropping them.
drainRunningis a
process-global singleton (subagent sessions share the process), and the
cascade above held it across a sibling session'sagent/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 itsfinally, 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.
- extend the deferred archive END past parallel close-call results. When the
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.
Prebuilt plugin bundle attached: dsh-taskfold-0.34.2.tgz (npm pack of this tag).
v0.34.1
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-aipackages between
the two install roots: 231 differ only in theirpackage.jsonversion 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 thedsh-web-frontendshell), no package is
added or removed, and no package changed content while keeping its version - the two packages this plugin imports —
dsh-compaction-basicanddsh-llm,
the onlyimportHostPackagecalls infold-engine.mjs— are byte-identical in
implementation, and a host-API probe against the real rc.2 packages confirms
the engine class export, itssummarizeprototype, subclassing, and
BlockAssemblerwithpush/blocks - the client contract is unchanged: a sweep of
conversation.input.dock,
useProjection,__ModuleLoader__, andslots.injectacross 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
taskMarksprojection rebuilds
after restart (ver: 10,pendingArchives: [], open tasks in stack order) and
the first fold passesverify-cache --since-restart(98.6% prefix-cache hit);
the mounted plugin copy is byte-identical to this repo'sv0.34.0tag blobs
(12/12 plugin files)
- a byte-level seam audit of all 240 first-party
-
Release publishes its own tarball.
releasenow creates the GitHub Release
and attachesdsh-taskfold-<version>.tgz; a newassets [--version X.Y.Z]
subcommand publishes or repairs the asset for an existing tag. That subcommand
exits non-zero on failure, while the in-releaseattempt 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:npmis spawned
asprocess.execPath+npm-cli.js(a barenpmisENOENTon Windows without
a shell,npm.cmdisEINVAL, andshell: truetriggers DEP0190), andghis
resolved fromPATHfirst, then the standard install locations.v0.34.0was backfilled this way; it carriesdsh-taskfold-0.34.0.tgz
(106,183 B), verified by unpacking the asset and comparing every file against
git cat-file blob v0.34.0:<path>(18 files; at the time 12 differed only by
CRLF — see the next bullet)
-
Release assets are byte-reproducible.
.gitattributes(* text=auto eol=lf,
plus explicitbinarymarks) closes a gap that made every attachment differ from
its tag blobs: the blobs were already LF, butcore.autocrlf=truehad written
CRLF into the working tree, andnpm packpacks 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 -fdoes not rewrite working-tree line
endings. Verified: 18/18 packed files byte-identical to the committed blobs
(previously 6/18) and zerow/crlfacross the 46 tracked files. -
Docs moved into the repo. The design notes and ADRs had never been
version-controlled — the workspacedocs/tree was not inside any git
repository. They now live indocs/README.md(index),docs/design/(13 notes),
anddocs/adr/(2 decisions), still outside the npmfileswhitelist. The move
repaired two referencesCHANGELOG.mdhad been making all along, to
docs/design/deferred-report-fold.mdanddocs/design/lazy-fold.md. The two
historical notes stay at their flatdocs/*.mdpaths 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.
Prebuilt plugin bundle attached: dsh-taskfold-0.34.1.tgz (npm pack of this tag).
v0.34.0
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 awireview (mirroring thetodosprojection) so the browser can read
it, andplugins/task-stack-ui.mjsrenders it: outermost task first, the
innermost highlighted, one row per closing task, pendingtask_begin/
task_endcalls 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. stateVersion9 → 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 theTask begunresult, so the close result is the witness
every successful fold leaves behind. Either witness present now drops the row
(rows persisted beforefoldResultSeqexisted keep the anchor-only rule), and
the version bump forces the full replay that converges sessions already
carrying ghost rows — observed live: sixfolding…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.(orempty). 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.mjsis the single source of truth;
scripts/build-client.mjsstrips its ESM keywords and splices it into
scripts/taskfold-client.template.mjs, emittingplugins/taskfold-client.mjs
as a loader-factory classic script — no bundler, no build step in the repo.
test/client-bundle.test.mjsbyte-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
buildClientcarried 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.pngand
assets/screenshot-tasks-collapsed.png, both 1280×720 — rendered from the
shipped component with the host's real theme tokens, listed in
screenshots.jsonand embedded in both READMEs. - Live verification on the running host (dsh 0.1.5-rc.1): the persisted
projection cache showstaskMarksatver: 10withpendingArchives: [];
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-restartjudges 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.
Prebuilt plugin bundle attached: dsh-taskfold-0.34.0.tgz (npm pack of this tag).
v0.33.0 —
v0.32.1 — fold regions follow surface POSITION, and one coordinate for index, artifact and recall
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.nodesis 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 asmin{ s : floor < s < close }andfoldRegionpicked the
shrink target asmax{ 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 owntask_begincall (with its opening reasoning), the
Task begunresult 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 -= 1withendPos/startPos
guards), and the drain's walk is index-driven end to end (the old
end >= startSeqNUMERIC 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 theTask begunresult.
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 begunresult"; 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 indocs/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-restartpasses 4/4. - Fix (high) — the span index, the artifact and the resident footer were
numbered in the REQUEST's coordinate whilefold_recallrebuilds the SPAN's.
The host prepends the surface-head system prompt intoinput.messages
(dsh >= 0.1.5-alpha.1), and all three were rendered frominput.messages,
so every artifact carried one extra leading message (measured 36/5/14 lines
against 35/4/13 shadowed seqs, first line alwaysrole: system). A model
copying a printedL<N>therefore pointed at a line that was neither in its
own span nor the same linefold_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
helperspanMessagesFor()recomputes the commit's own slice
(nodes[startIdx..endIdx]projected per event, exactly what the host
validates asshadowedSeqsand whatfold_recallrebuilds) 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.classifyCategorycollected 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), pastMAX_FOLD_ATTEMPTS(5) the entry
still retries — never abandoned silently — but only once per
GIVE_UP_PASSES(200) boundaries, and the failure line carries
"<bucket>, attempt N: <error message>". Acancelledfold (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;collectFoldsmerely 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 newautoFoldAttemptsandcompact-region's
lifecycleLatchare 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
readdirSynccalls 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 abcwasNumber('abc')→ NaN →slice(-NaN)→
slice(0), i.e. it judged EVERY fold while looking scoped. - Docs:
plugins/compact-stats.mjsanddocs/design-compact-stats.md
described the pre-0.15 in-flighttask_foldtitle correlation as if it were
the live path; it is unreachable for any current log (fold titles come from
the constructed# <name>heading, AUTO checkpoints stay untitled and list by
their preview), so both now say so and the tests pin the live path as well as
the legacy replay. Also corrected the stale## Primary Request and Intent
andtask_foldreferences in the stats module.CHANGELOG.mdjoins the
publishedfileslist (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,foldOftotality, the AUTO-vs-titled
listing, andparseArgsvalue validation.
npm: dsh-taskfold@0.32.1
v0.32.0 — message gate - fold at the first assistant message after the close, drop the unreachable successor-anchor defer
- 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, immediatelytask_begintask 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 thesuccessorAnchors
parameter ofdeferredArchivePlan. 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.
npm: dsh-taskfold@0.32.0