effectively infinite context for your coding agent
Your agent compacts its own context, as it works. It wraps each piece of work in a named task, closes it when the work is done, and the closed span becomes one short titled summary — written while that span is still in context, so it is accurate by construction. There is no command to run, and nothing you must manage.
Nothing is thrown away: every fold is archived in full, and fold_recall brings any of it back exactly as it was.
For DeepSeek Harness (DSH).
Latest supported dsh:
0.2.2-rc.1. dsh rc builds are supported on this branch; dsh alpha builds are supported on thealphabranch.
Long sessions drown in their own history: every request re-sends hours of finished work — old tool outputs, debug logs, abandoned attempts. Costs climb, the model gets distracted, and the window eventually fills.
taskfold takes finished work off the surface at the moment it finishes:
task_begin("fix the login bug") … the work … task_end("fix the login bug")
→ "fix the login bug" — one titled summary: what was tried, what failed and why,
what changed, what the user decided
The agent opens a task before starting and closes it when done. Closing is the fold: the span collapses into that summary, and the original messages are archived untouched.
The agent compacts on its own initiative, at task boundaries — not when a token counter decides the window is nearly full. That is the difference from dsh's built-in compaction, and the two compose:
| dsh built-in compaction | taskfold | |
|---|---|---|
| Trigger | token pressure | the agent, at each task boundary |
| Fires when | the window is nearly full | the work is finished |
| Summary written | from history that has already gone stale | while the span is still in context |
| Scope | a token-sized range | one named task, with a title |
| Reading the original | re-summarized | fold_recall({ fold: N }) returns the exact messages |
| Context window | fills until it fires | rarely fills, so it fires later or not at all |
- Accurate by construction. The summary is written once, while the span it describes is still in context — never a summary of a summary.
- Navigable. Every fold carries a title, so a long session stays scannable.
- Cache-friendly. Folding rewrites a middle chunk of history; the stable prefix (system prompt, tools, earlier context) stays cache-friendly.
- Guardrails, not noise. If the agent forgets the discipline, a short hint appears in its context. Hints are events, not state: one is published when the condition appears or its wording changes, nothing is published once it clears, and there is no wrapper, supersession header or expiry notice.
One real session — 411 model steps, 26 folds — read from the harness's own usage records:
| Without folding | With taskfold | |
|---|---|---|
| Total prompt tokens | 142,654,308 | 52,127,098 (−63.5%) |
| Largest single request | 594,909 | 206,896 (−65%) |
| Context window used at peak | 59.5% | 20.7% |
- Folding moved 441,100 tokens of finished work off the surface; because that history would otherwise be re-sent on every later request, it added up to 90,527,210 tokens never sent.
- Producing the 26 summaries cost 3,550,270 tokens — 3.9% of the saving, and most of it cache reads.
- The biggest single fold took 40,422 tokens out of the conversation in one call.
Most of those tokens would have been cache reads rather than fresh input — cheaper, but still billed and still occupying the window. In longer sessions that is the difference between staying inside the context window and not.
One session, one task shape — your numbers will differ. The mechanism is the point: finished work leaves the surface, the stable prefix stays cached, and the model keeps the lessons instead of the transcript.
- Open dsh Desktop and go to the Plugins page in the sidebar.
- Click Add plugin in the top-right corner.
- In Package name or address, enter
dsh-taskfold. The dialog runs pnpm for you:- want the newest release the moment it ships? pin it —
dsh-taskfold@<version>from the versions list. pnpm 11 (bundled with dsh Desktop) leaves a release younger than 24 h out of the bare name's reach, and an explicit version overrides that; pnpm records it in the profile'sminimumReleaseAgeExcludetoo. Never install 0.37.7 or 0.38.0 — uninstallable (ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND) and deprecated on npm; - skip npm entirely: paste the prebuilt
.tgzfrom the newest Release — a tarball is not subject to the release-age policy; - follow a branch:
github:yindf/taskfold#master(rc channel) orgithub:yindf/taskfold#alpha(alpha channel) — needs direct GitHub access from this machine; - a local path must point at a copy, never at your working checkout: the plugin manager treats whatever it installs as its own (it leaves
dsh-taskfold.frozen-*.baksnapshots beside it and replaces the directory on upgrade).
- want the newest release the moment it ships? pin it —
- Leave Registry on Default registry; mainland-China networks can pick Mainland China mirror and retry.
- Click Install, wait for "Installed; it loads at the next start.", then click Enable now.
- Restart dsh Desktop — plugin composition is evaluated only at startup. Back on the Plugins page you should see a Taskfold card reading Components · 2 total · 2 running (
taskfoldandtaskfold-client); the fold floor and the task-bar switch live on that card (see Settings). - To upgrade, uninstall and install the new version — the dialog states that automatic updates are not supported yet, and uninstalling takes its features away.
The dialog writes the dependency and the bundle entry into the active profile (desktop for dsh Desktop); nothing else needs configuring by hand.
dsh plugin --profile <name> add is the shell equivalent of the dialog. desktop is dsh Desktop's profile and web is the DSH Web GUI's — use whichever one you actually run:
# the newest npm release
dsh plugin --profile desktop add dsh-taskfold
# a pinned version — required while the release is younger than 24 h; 0.37.7/0.38.0 cannot be installed
dsh plugin --profile desktop add dsh-taskfold@<version>
# a channel branch (keep the quotes: # starts a comment in sh)
dsh plugin --profile web add "github:yindf/taskfold#master"
dsh plugin --profile web add "github:yindf/taskfold#alpha"Whichever way you install, restart dsh afterwards: every session on that profile then gets the tools.
dsh-taskfoldon npm is a prebuilt package — it skips dsh'sallowBuildsbuild approval.- Since 0.37.6 the release pipeline publishes to npm for the rc channel (
master) only: alpha-channel releases ship as GitHub Releases and never reach npm. The bare package name therefore follows the rc channel's newest release — pin@versionfor determinism, or enter the branch's Git address. - Channel branches:
mastercarries the rc channel,alphacarries the alpha channel. This branch records the rc channel's support range (see Supported dsh versions); the alpha channel's record lives on the alpha branch. - Every release attaches a prebuilt
dsh-taskfold-<version>.tgz. Plugin storefronts offer that asset — or the npm package — instead of the build-from-source command, which also skips dsh'sallowBuildsapproval step. See the latest release.
Four agent tools (plus the hints above):
| Tool | One-liner |
|---|---|
task_begin({ name }) |
Open a named task. |
task_end({ name }) |
Close it and fold its whole span into one titled summary. |
list_folds |
List all folds (number, size, title). |
fold_recall({ fold }) |
Bring back any fold's original content on demand. |
- Open tasks survive restarts; closing is well-ordered (innermost first), and a failed close never corrupts anything — just retry.
- Summaries are instructed to preserve user decisions and feedback (verbatim where wording matters), pitfalls and why things failed, what changed, and the outcome.
In the Web GUI, the current open-task stack can also show as a live dock above the composer (like the todo panel) — hidden by default, switched on with showTaskBar on the Plugins page: outermost first, the innermost task highlighted, with the closing/pending counts — read straight from the session's taskMarks projection, no extra events.
Two settings, on the host's Plugins page as a "Taskfold" card (the browser half registers it while the host serves the taskfold namespace — no environment variables, no restart):
| Setting | Default | Accepted | What it does |
|---|---|---|---|
minSpanTokens |
2000 |
integer 0–1000000 |
Minimum estimated tokens a closed task's span must carry before it is worth a summarization call. |
showTaskBar |
false |
on / off | Shows the task-stack dock beside the conversation input. |
minSpanTokens counts a CJK-aware character heuristic over the span's message text before any model call — its rate constants are calibrated against measured shadowedTokenCount values, and estimates read ~7% low, a conservative direction. Edits are validated by the form's schema, persisted in the active profile's config, and applied live: the next close (or fallback settle) judges under the new floor.
- Below the floor a span closes unfolded — permanently. The verdict is made at close time and recorded in the
Task endedresult itself (0.37.6): the span never enters the archival queue, so no reopen path exists. Retro-folding settled spans would rewrite the surface and invalidate the provider prefix cache behind them, a cost no small fold can repay. - Edge cases.
0folds everything; an unmeasurable span always folds; a value that is not a usable number (non-integer, negative) falls back to the default instead of throwing, and the form blocks a save outside0–1000000. - Closes that carry other tool calls — their partner results have not landed when
task_endexecutes — fall back to drain-time, in-memory settling, re-measured after a restart. - Why 2000: measured over two weeks of session logs, a summary call's fixed overhead makes spans under ~1000 tokens a net loss, and 2000 keeps a 2× margin. Derivation in docs/fold-floor.md.
showTaskBar is hidden by default, so a fresh profile carries no dock until it is switched on; showing or hiding it does not affect folding. The one visible fallback is deliberate: an unserved namespace or a missing settings service still means visible, because the dock's registration never depends on settings to render.
Re-verified against @deepseek-ai/dsh-app-boot@0.2.2-rc.1 by reading the host's own sources — this was a minor step with real host-surface movement, so the surface was re-read rather than assumed:
| What was checked | Result |
|---|---|
| Compatibility gate | semver.satisfies(runtimeVersion, range, { includePrerelease: true }) — lib/index.js:1095, real semver 7.8.6. That option is what admits this host: a 0.2.x prerelease is allowed anywhere inside the caret's <0.3.0-0 bound; without it node-semver's same-tuple prerelease rule would reject it. |
peerDependencies |
@deepseek-ai/dsh, @deepseek-ai/dsh-compaction-basic, @deepseek-ai/dsh-llm — all ^0.2.0-rc.1, all unmet-free, gate returns undefined. The manifest declares no engines. |
| Host APIs the plugin binds | 27/27 present, 0 absent — 7 services, 3 hooks, 6 methods, the BasicCompactionEngine subclass contract, BlockAssembler, schemastery's CJS chain, both gate exports. |
| Client contracts | 16/16 present — conversation.input.dock, plugins.bundle.config, slots.inject/register, configForms/whileServed, window.__ModuleLoader__, ctx.provide("locale", …). @deepseek-ai/dsh-client-ui-primitives is shell-inlined, so a filesystem check for it is a false negative. |
| The live host | It boots the published dsh-taskfold@0.40.1 — the same version as this checkout, whose installed client bundle matched it at all 28 anchors across the same 753 lines. A rejected row would be dropped with disabling profile plugin …, so the running row is the gate's own verdict; no compatibility.json exemption exists anywhere under ~/.dsh. |
| Non-lockstep neighbours | schemastery 3.18.5 → 3.19.0; @deepseek-ai/cordis 4.0.5 → 4.1.0. |
| Drift found | Deprecation, not breakage: sessionProjections.register() is now a legacy wrapper over acquire(), which requires an integer stateVersion (0.40.1 declares stateVersion: 11); stateOf() logs one deprecation warning per key from three call sites. |
| Not re-run | The rc.5 probe battery — shape battery, gate script, e2e probe host, 239-test offline suite. This environment had no usable shell (no node/npm/git), so the record rests on source reads plus the live host. |
- rc channel — supported through
0.2.2-rc.1(verified 2026-11-07). Desktop payload:@deepseek-ai/dsh-desktop-runtime@0.2.2-rc.1(desktopVersionin its runtime manifest). - alpha channel — use the
#alphainstall (dsh plugin --profile web add "github:yindf/taskfold#alpha", the alpha branch); the alpha build's supported-version record lives in the alpha branch's README. - Bounds: enforced floor, untested ceiling. The plugin declares its host surface in
peerDependencies(^0.2.0-rc.1): dsh 0.2.0+ hosts check those ranges at install and at startup/recomposition, so an incompatible host now gets the bundle skipped with anincompatible-versionverdict instead of loading it — 0.1.7-rc.* hosts should install v0.37.0, whose^0.1.7-rc.1floor matches them. Hosts older than the check itself negotiate nothing — on those, folds degrade (tasks still close, unfolded) rather than corrupt. After each dsh upgrade, re-check this section and update it with test results. - Optional hook:
agent/turn-stopping— since 0.26.0 the archive drain also runs at turn end, folding turn-final deliverables while the provider prefix cache is still hot. Hosts without the hook simply keep the previous pre-step-only semantics (folds still happen, one turn later); the registration is wrapped so its absence never breaksapply().
Layout
plugins/— ONE mounted host row,taskfold.mjs, mounted as the package subpathdsh-taskfold/plugins. The bundle patch declares one host component, so the Plugins page shows two rows: this one and the client row, which is mounted by its own component package namedsh-taskfold-client.compact-stats.mjsis a plain module imported by the row, not a row.plugins/shared plain modules —events.mjs,task-marks.mjs,fold-instruction.mjs,fold-engine.mjs,fold-drain.mjs,fold-settings.mjs,lifecycle-nudges.mjs,lifecycle-injection.mjs,span-preview.mjs; browser half:task-stack-ui.mjs(the dock's source of truth) andfold-settings-ui.mjs(the Plugins-page settings card), both generated intoclient/taskfold-client.mjsbyscripts/build-client.mjs.client/— the nesteddsh-taskfold-clientpackage, owning the browser bundle. A client package may have exactly ONE owning Loader row, and the host half already owns the root package row. It is published as its own npm package, and the root declares it underdependenciesas a registry version range, so dsh-app-boot's dependency closure publishes the name, the row mounts the bare name instead of afile:///path, and a consumer's pnpm can install it — afile:/link:specifier is resolved against the INSTALLER's workspace root, never against this package, which is why 0.38.0 could not be installed at all.scripts/—release.mjs,verify-cache.mjs,build-client.mjs;test/(npm test);assets/(README banner, the social-preview image to upload in repo settings, and the store screenshots listed inscreenshots.json);docs/(docs/README.mdindex plus thedocs/design/notes anddocs/adr/decisions — deliberately kept out of the npm package);CHANGELOG.md.
Localization
- Every user-facing surface ships English and Simplified Chinese. The settings card resolves its copy through the host's locale service (dictionary
settings.taskfold). - The dock's registration deliberately carries no locale requirement — the bundle wires a locale-bound reader for dictionary
ui.taskfoldwhile the locale service is available, and prints its built-in English copy otherwise, so it renders on every deployment. - The package exports
locale/en.json+locale/zh.json(the package page header and the npm listing) plus one row locale per mounted component:plugins/locale/*.jsonfor the host row, reached through the subpath it mounts (dsh-taskfold/plugins), andclient/locale/*.jsonfor the client row, reached through its own package name. The Plugins page therefore shows each row's title and one-line introduction in the active language instead of the Englishpackage.jsonfallback. - Both row titles deliberately differ from their row id and their module name:
client.jsprints the id only when the title differs from it and the module name only when the title differs from that, so each row renders the same four lines the official bundles' rows use — title, introduction, row id, module name. The client half'spackage.jsonstill carries no description, so a row introduction can only ever come from its locale file.
Releasing
node scripts/release.mjs draft→ review the CHANGELOG entry →node scripts/release.mjs release. CHANGELOG is the single source of truth for versions.release(and its PENDING resume) also publishes to npm when theNPM_TOKENenvironment variable is set — an automation-type token, because 2FA accounts reject publish-time OTPs for granular tokens. The step is idempotent (a version already on the registry is a no-op) and its failure never rolls back the git-side release; the token comes from the process environment or, on Windows, the user-level variable.- If that step was skipped or failed,
node scripts/release.mjs npm [--version X.Y.Z](re)publishes an already-released version on its own — it refuses unlesspackage.json, the localv<version>tag, a clean tree and a tree identical to that tag all say this checkout really is that release. - Two packages publish, and the client goes FIRST: the root's manifest names
dsh-taskfold-client's version, so a root published ahead of its client would 404 for every consumer until the client landed (each package is probed and published on its own, so a re-run finishes whichever half is missing). Before publishing,assertClientDependencyResolvablerefuses a client dependency that is not a registry version range — afile:/link:/workspace:specifier is resolved against the CONSUMER's workspace root and makes every install fail, which is exactly what 0.38.0 shipped. - Channel branches (since 0.34.6):
mastercarries only rc-channel releases — it stays at the newest verified-rc version; alpha-channel releases happen on thealphabranch, whose commits and tags carry the alpha-verified work (run draft/release ON thealphabranch; the script pushes the current branch and the tag). - If a release changes which dsh versions are supported, update the "Supported dsh versions" section in both READMEs (README.md + README.zh.md) before releasing — the release script reminds you. Each branch records only what was verified on that branch: one newest verified version for its own channel (superseded entries are deleted), and a link to the other branch's README for the other channel — never a copied version number.
Fold cache verification is part of the flow. After every dsh upgrade — and before any release that touches the fold envelope — run node scripts/verify-cache.mjs --since-restart against a live session log, and record the numbers in the CHANGELOG entry. It exits non-zero when a fold's summarizer call re-pays its span — the test is uncached − span > --tail-budget, i.e. a positive tail, the signature of the prefix envelope no longer matching the host's summarization input. The offline suite pins the structural precondition (one system message, strict prefix); only a live log can show the actual cache read.
Design decisions and history live in CHANGELOG.md and in docs/ (docs/README.md is the index; the docs/design/ notes and docs/adr/ decisions ship with the repo, not with the npm package).
A star helps other dsh users find it — in this ecosystem, that is how a plugin gets discovered. Numbers from your own sessions are welcome in Discussions.
MIT. Developed against the DeepSeek Harness (@deepseek-ai/*, MIT) public packages.


