Skip to content

Releases: yindf/taskfold

v0.34.6

Choose a tag to compare

@yindf yindf released this 18 Sep 09:03

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. summarizeCompaction now 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 no waterfall — so every
    scoped-fold summarize failure surfaced as a masking TypeError
    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. shimWaterfall now 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
    reasoningEffort into 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). foldRequestOptions now
    mirrors the host buildRequest()'s shape — the latest routed
    requestHeader config rides along, maxTokens stripped 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

Choose a tag to compare

@yindf yindf released this 15 Sep 03:13

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.

Prebuilt plugin bundle attached: dsh-taskfold-0.34.5.tgz (npm pack of this tag).

v0.34.4

Choose a tag to compare

@yindf yindf released this 12 Sep 04:04

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.

Prebuilt plugin bundle attached: dsh-taskfold-0.34.4.tgz (npm pack of this tag).

v0.34.3

Choose a tag to compare

@yindf yindf released this 12 Sep 02:40

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.


Prebuilt plugin bundle attached: dsh-taskfold-0.34.3.tgz (npm pack of this tag).

v0.34.2

Choose a tag to compare

@yindf yindf released this 12 Sep 02:09

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.


Prebuilt plugin bundle attached: dsh-taskfold-0.34.2.tgz (npm pack of this tag).

v0.34.1

Choose a tag to compare

@yindf yindf released this 11 Sep 02:01

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

  • Host upgrade 0.1.5-rc.1 → 0.1.5-rc.2 verified — no plugin change

    • a byte-level seam audit of all 240 first-party @deepseek-ai packages between
      the two install roots: 231 differ only in their package.json version fields,
      7 change implementation files (the message-feedback group —
      dsh-client-ui-message-feedback, dsh-message-feedback,
      dsh-command-feedback, dsh-client-ui-chat, dsh-client-ui-deliverables,
      dsh-client-ui-sidebar — plus the dsh-web-frontend shell), no package is
      added or removed, and no package changed content while keeping its version
    • the two packages this plugin imports — dsh-compaction-basic and dsh-llm,
      the only importHostPackage calls in fold-engine.mjs — are byte-identical in
      implementation, and a host-API probe against the real rc.2 packages confirms
      the engine class export, its summarize prototype, subclassing, and
      BlockAssembler with push/blocks
    • the client contract is unchanged: a sweep of conversation.input.dock,
      useProjection, __ModuleLoader__, and slots.inject across both builds is
      identical, and the dock's owner (dsh-client-ui-conversation), the reference
      consumer (dsh-client-ui-goal), and the bundle registry
      (dsh-client-modules) are byte-identical — three packages that never even
      enter the seven-package delta
    • live, on the running 0.1.5-rc.2 host: the taskMarks projection rebuilds
      after restart (ver: 10, pendingArchives: [], open tasks in stack order) and
      the first fold passes verify-cache --since-restart (98.6% prefix-cache hit);
      the mounted plugin copy is byte-identical to this repo's v0.34.0 tag blobs
      (12/12 plugin files)
  • Release publishes its own tarball. release now creates the GitHub Release
    and attaches dsh-taskfold-<version>.tgz; a new assets [--version X.Y.Z]
    subcommand publishes or repairs the asset for an existing tag. That subcommand
    exits non-zero on failure, while the in-release attempt only warns — by then the
    commit, tag, and push have already landed, and a missing attachment must not read
    as a failed release. Portability was probed rather than assumed: npm is spawned
    as process.execPath + npm-cli.js (a bare npm is ENOENT on Windows without
    a shell, npm.cmd is EINVAL, and shell: true triggers DEP0190), and gh is
    resolved from PATH first, then the standard install locations.

    • v0.34.0 was backfilled this way; it carries dsh-taskfold-0.34.0.tgz
      (106,183 B), verified by unpacking the asset and comparing every file against
      git cat-file blob v0.34.0:<path> (18 files; at the time 12 differed only by
      CRLF — see the next bullet)
  • Release assets are byte-reproducible. .gitattributes (* text=auto eol=lf,
    plus explicit binary marks) closes a gap that made every attachment differ from
    its tag blobs: the blobs were already LF, but core.autocrlf=true had written
    CRLF into the working tree, and npm pack packs the working tree rather than the
    blobs. Repairing an existing checkout needs the renormalize sequence
    (git add --renormalize . → commit → git rm --cached -r .git reset --hard); git checkout-index -a -f does not rewrite working-tree line
    endings. Verified: 18/18 packed files byte-identical to the committed blobs
    (previously 6/18) and zero w/crlf across the 46 tracked files.

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

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


Prebuilt plugin bundle attached: dsh-taskfold-0.34.1.tgz (npm pack of this tag).

v0.34.0

Choose a tag to compare

@yindf yindf released this 10 Sep 12:18

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.

Prebuilt plugin bundle attached: dsh-taskfold-0.34.0.tgz (npm pack of this tag).

v0.33.0 —

Choose a tag to compare

@yindf yindf released this 10 Sep 10:03

v0.32.1 — fold regions follow surface POSITION, and one coordinate for index, artifact and recall

Choose a tag to compare

@yindf yindf released this 10 Sep 06:22

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

  • Fix (high) — the fold region was resolved by seq MAGNITUDE instead of
    surface POSITION.
    surface.nodes is a position list, not a sorted one: a
    committed fold re-inserts its summary node AT the position of the region it
    shadowed while that node carries a seq from the log's END. deferredArchivePlan
    picked the start as min{ s : floor < s < close } and foldRegion picked the
    shrink target as max{ s : start <= s < end }, so on any post-fold surface
    both could land EARLIER on the surface than the intended node. Live evidence
    (this session, fold #3 at seq 240): its region opened at 183 — an EARLIER
    fold's resident summary node, three nodes ahead of where it belonged —
    swallowing the task's own task_begin call (with its opening reasoning), the
    Task begun result and that earlier fold's summary node: exactly the "the
    bookmark stays live" promise made by the README, both tool descriptions and
    docs/scoped-summary-acceptance.md. Both sites now resolve the region by
    INDEX (posOf, nodes[floorPos + 1], endPos -= 1 with endPos/startPos
    guards), and the drain's walk is index-driven end to end (the old
    end >= startSeq NUMERIC loop guard could stop a walk that still had room —
    silently closing a task unfolded through the tooSmall path). Seq comparisons
    remain only over the event log, which is seq-ordered by construction. Both
    halves were then re-checked against the session log's own record of that fold:
    replayed over the recorded region
    (shadowedSeqs = [183,177,179,187,…,220,214,216]) the OLD numeric scan
    returns startSeq 183 — bit-identical to the region the host actually
    committed, which is the bug reproduced from data — while the fixed positional
    scan returns 187, the node immediately after the Task begun result.
    Regression tests drive a real post-fold surface both in the plan and through
    the drain's shrink walk.
    CORRECTION (post-release re-audit, dsh 0.1.5-rc.1, 2026-09-10) — the first
    published version of this entry described the damage more broadly, listing a
    nested subtask's begin pair and two already-committed summary nodes as
    swallowed and calling the summary cross-task contaminated. The correct
    criterion is "is the region's first node positionally BEFORE this fold's own
    Task begun result"; replayed against all 15 folds of that session log,
    exactly ONE fold met it — this one — and its bug-caused prefix is the three
    nodes above. The nested subtask's begin pair and a later fold's resident
    summary node lie AFTER this fold's own begin result, so ANY
    position-contiguous region contains them: that is the by-design span sweep,
    now documented in docs/scoped-summary-acceptance.md, not fallout of this
    bug. The fix itself is unaffected and was re-verified live on 0.1.5-rc.1:
    0.32.1-produced folds carry no pre-begin prefix, and
    verify-cache --since-restart passes 4/4.
  • Fix (high) — the span index, the artifact and the resident footer were
    numbered in the REQUEST's coordinate while fold_recall rebuilds the SPAN's.

    The host prepends the surface-head system prompt into input.messages
    (dsh >= 0.1.5-alpha.1), and all three were rendered from input.messages,
    so every artifact carried one extra leading message (measured 36/5/14 lines
    against 35/4/13 shadowed seqs, first line always role: system). A model
    copying a printed L<N> therefore pointed at a line that was neither in its
    own span nor the same line fold_recall({ fold, line: N }) returns, and
    preview line N = artifact line N — the contract in
    docs/scoped-summary-acceptance.md — was false on the recall side. New pure
    helper spanMessagesFor() recomputes the commit's own slice
    (nodes[startIdx..endIdx] projected per event, exactly what the host
    validates as shadowedSeqs and what fold_recall rebuilds) and falls back
    to the deduped request span when there is no closing declaration. All four
    sites — instruction index, artifact, footer, message-count bullet — now use
    it, so fold-time output and recall-time regeneration are the same
    construction.
  • Fix — the retry loop was unbounded and undiagnosable. One fold attempt is
    a whole summarization call (30–70 s); a deterministic failure (structure
    receipt, missing host API, provider refusal) was re-attempted at EVERY step
    boundary forever, with no cap and no backoff. classifyCategory collected the
    error's own message and then threw it away, so the HOLD line named a bare
    category that appeared nowhere in the log. Now: consecutive failures back off
    geometrically (1, 2, 4, 8 boundaries), past MAX_FOLD_ATTEMPTS (5) the entry
    still retries — never abandoned silently — but only once per
    GIVE_UP_PASSES (200) boundaries, and the failure line carries
    "<bucket>, attempt N: <error message>". A cancelled fold (an interrupted
    or superseded turn — the normal shape of an Esc) stays quiet on its first
    occurrence and is surfaced only if cancellation repeats. Also guarded
    agent.session.requestHeader() in the engine: an unguarded throw there was a
    deterministic failure in the loop above.
  • Fix — small, each found by reading against a live log. foldOf(null)
    threw a TypeError while its doc promised "defensive on every field" (it is an
    exported pure helper; collectFolds merely hid it). Artifact filenames used
    Date.now().toString(36) alone, so two writers in one millisecond
    overwrote each other; they now carry a random tail. settledArchives,
    autoFoldFailures, the new autoFoldAttempts and compact-region's
    lifecycleLatch are keyed by session id and never shrank (a long-lived host
    serves one session per subagent) — all four are now evicted past
    MAX_TRACKED_SESSIONS / 200. scripts/verify-cache.mjs: resolveLog's
    readdirSync calls are guarded (a missing sessions root must be the clean
    "no session log found", exit 2, not an ENOENT stack) and every numeric flag is
    validated — --last abc was Number('abc') → NaN → slice(-NaN)
    slice(0), i.e. it judged EVERY fold while looking scoped.
  • Docs: plugins/compact-stats.mjs and docs/design-compact-stats.md
    described the pre-0.15 in-flight task_fold title correlation as if it were
    the live path; it is unreachable for any current log (fold titles come from
    the constructed # <name> heading, AUTO checkpoints stay untitled and list by
    their preview), so both now say so and the tests pin the live path as well as
    the legacy replay. Also corrected the stale ## Primary Request and Intent
    and task_fold references in the stats module. CHANGELOG.md joins the
    published files list (the README links it).
  • Tests: 131 offline assertions pass (was ~120), including new
    discriminating cases for the positional region start and END-shrink walk, the
    span coordinate (a numerically-below-start node that IS span content is
    included; out-of-position summary nodes are not), the retry budget/backoff and
    failure naming, cancellation quietness, foldOf totality, the AUTO-vs-titled
    listing, and parseArgs value validation.

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

Choose a tag to compare

@yindf yindf released this 10 Sep 04:11
  • 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.

npm: dsh-taskfold@0.32.0