The Events intro carried "The harness declares N events across M scopes."
and the Services intro "The N `ctx.<key>` services the harness provides."
Both embed counts the generator recomputes from source, so every branch
that adds an event or service rewrites that one line — a guaranteed merge
conflict against any sibling branch that also touched the catalog, for
prose that adds nothing a reader can't get by scanning the page.
Remove the count clauses from the generator's render() and regenerate the
catalog. The freshness gate (verify-cordis-catalog) stays green.
The `if (error instanceof WebError) throw error` guard is dead code in the
exa and perplexity providers: their mappers (mapExaResponse /
mapPerplexityResponse) never throw a WebError — a wrong-shape body throws a
TypeError, which the catch correctly translates to WEB_PROVIDER_ERROR. The
guard was added for symmetry with the deepseek provider, whose mapper DOES
throw a WebError in strict mode (no web_search_tool_result block), so it
keeps the rethrow. The unreachable lines tripped the per-file 100% coverage
gate.
- search providers (exa/perplexity/deepseek): map the parsed response
INSIDE the parse try, so a well-formed body of the wrong shape surfaces
as WEB_PROVIDER_ERROR instead of escaping as a raw TypeError; a WebError
the mapper throws on purpose is re-thrown untouched
- web-fetch-local: validate numeric limits at plugin construction (positive
finite caps; non-negative integer maxRedirects) rather than constructing a
provider with nonsensical values
- web-fetch-local: enforce the redirect budget BEFORE resolving each hop, so
maxRedirects:N follows exactly N redirects and an over-limit hop reports
"exceeded the maximum" rather than misdiagnosing a cross-origin block
- drop the stale dsh-tool-web/search and /fetch path aliases (the package no
longer declares those subpath exports)
- strip trailing EOF blank lines flagged by git diff --check
Each fix carries a regression test.
Use maxTokens as the provider generation cap and remove the confusing stored-summary max config.
Strip reasoning blocks before storing compaction summaries, reject non-shrinking summaries, and retry bounded re-compaction when the surface remains over threshold.
Add config validation for numeric and type-shaped knobs plus unit and real-API e2e coverage for reasoning-capable summarization.
Honor cancellation and disposal around async pre-step setup before the loop can open a step or call the model.
Route compaction summarization through agent/request so router agents can select the model, and remove the stale model argument from agent/pre-step.
Document serial events and the approximate convergence bound, regenerate the Cordis catalog, and add regression coverage for router compaction, HMR cleanup, and assembly/pre-step interruption.
Add a "Run the CI gates locally BEFORE marking a PR ready" subsection: the
CI-equivalent local command line, and the rule that `pnpm run test:coverage`
(per-file 100%, CI-enforced) — not `pnpm run test` — is the gating test command,
alongside hygiene/snapshot/doc-sync. A green `test` run can still fail CI on an
uncovered line, which is usually dead code the gate is correctly flagging.
Clarity-first revisions from review:
- keep English for ambiguous terms (Cordis, fork, harness, schema, spawn,
manifest, transcript, compaction, dispose)
- first-use glosses for CLI, Function Calling, HMR, fiber
- inference/reasoning carry the English in parens to disambiguate
- memory distinguishes 记忆 (agent memory) vs 内存 (resource usage)
- plugin -> 插件; add a separate mod -> 模组 row
- 中英搭配 note on translated terms (wire format, adapter contract)
The registry's validateArgs rejects a bad `status` enum before execute runs, so
the in-body re-check (`status !== 'pending' && …` → throw) was unreachable dead
code — line 70 was uncovered, failing the per-file 100% coverage gate. Narrow
the registry-guaranteed value with `status as TodoItem['status']` instead of
re-validating it, mirroring tool-bash (which only checks what the DSL can't
express). The malformed-status test still passes — it exercises the registry's
rejection, the actual path. Coverage back to 100%.
Add @deepseek-ai/dsh-web-search-deepseek: a WebSearchProvider that calls
DeepSeek's Anthropic-compatible Messages API with the native
web_search_20250305 server tool and parses the structured
web_search_tool_result blocks into the ctx.web seam's WebSearchResult.
- Namespace plugin (inject: ['web']), no default export — registers into
ctx.web like dsh-llm-deepseek registers into ctx.llm.
- Strict mode: a response with no web_search_tool_result block throws
WEB_PROVIDER_ERROR rather than scraping URLs from model prose.
- Reuses $DEEPSEEK_API_KEY; baseURL defaults to the Anthropic-compatible
base (api.deepseek.com/anthropic/v1) and does NOT reuse
$DEEPSEEK_BASE_URL, which belongs to the chat-completions LLM adapter.
- snippet joined from text-block citations; sources deduped by url.
- Two-stage build layout (outDir lib/types) matching the other web
packages; registered in tsconfig.json, tsconfig.build.json, knip.json,
and docs/module-graph.md.
Codex confirmation review: the trim-the-stored-content fix had no test that
would fail if it regressed (existing assertions use already-trimmed todos).
Add a focused test asserting " plan the work " appends content "plan the
work". Verified it fails red against the pre-fix code.
Record the `todo-plan` snapshot scenario: a real prompt drives the model to call
todo_write, and the golden captures the resulting `plan` sessionUpdate (three
entries, priority synthesized as medium, status 1:1) plus the persisted
todo/write event. Registered in SCENARIOS; replays deterministically keyless.
Add a with-key coding-agent e2e that verifies the WORLD — a real model call to
todo_write lands a todo/write event whose snapshot is a valid, one-in-progress
list — not the agent's self-report. Wire tool-todo into the e2e harness.
toTodoList dedupes and length-checks on the trimmed content but stored the raw
item.content, so a todo with leading/trailing whitespace was deduped by its
trimmed form yet persisted untrimmed — the stored value and the uniqueness key
could differ. Store the trimmed content so the persisted list matches what was
validated.
Two cohesion cleanups on the filesystem tool package:
- Fold window.ts + types.ts + formatReadOutput into one cordis-free
read-render.ts. Line windowing, the FileReadOutcome shape, and output
formatting are one concern (the read tool's rendering); splitting them across
three files added no value. read.ts is now just the tool (schema + I/O).
- Drop observe.ts and emit fs/observed with a plain ctx.emit in read/write/edit.
The event is contractually a synchronous, side-effect-only recorder
(file-context's listener is a WeakMap.set), so the per-call try/catch guarded
against a contract violation that cannot happen under the shipped listener —
defensive code for an impossible case. The event contract (dsh-fs JSDoc,
README, RFC) is updated to state the fire-and-forget semantics plainly.
Add @deepseek-ai/dsh-tool-todo (a new packages/todo/ group): a model-facing
todo_write(todos: [{content, status}]) tool with whole-list-replace semantics.
Each call appends the full list as a todo/write event to the calling agent's
session log; the current list is the most recent such event (last-write-wins).
Single-owner — a non-agent caller is rejected. Beyond the schema's
type/required/enum checks, execute rejects empty/duplicate content and more than
one in_progress task, narrowing the loosely-typed args into a real TodoItem[].
Both UIs render off the existing session/event: the stdio UI prints a glyphed
checklist; the ACP bridge maps the list to a `plan` sessionUpdate (todosToPlan
synthesizes the priority ACP requires; status maps 1:1). Wired into the
coding-agent, acp-agent, and snapshot example configs with a system-prompt nudge.
Tests: unit (schema, validation, append/replace, no-agent rejection, presentCall,
HMR-safety, Loader export-shape guard), full-loop integration through the agent
loop, the ACP todosToPlan mapping + stream-update arm, the stdio render arm, and
a session/load replay that re-emits the plan. New-group TS wiring added to
tsconfig.base/json/build. RFC + a doc-inventory sweep (architecture, packages
README, AGENTS layout, cookbook group list, example READMEs) ship with it.
The todo-plan ACP snapshot scenario is recorded separately (needs an API key).
Codex Phase 1 review: the event JSDoc described Phase 2 consumers (the
todo_write tool, stdio printing, ACP plan mapping) as current state, and put an
@mode tag on a SessionEventMap member. @mode is for first-class Cordis
`interface Events` entries the catalog generator reads — this event rides the
existing session/event emit and has no catalog row, so the tag was wrong.
Trim the JSDoc to the event's own contract (snapshot data shape,
last-write-wins, not-a-surface-event) and drop @mode; phrase TodoItem in terms
of its own purpose rather than a not-yet-present tool.
Add the TodoItem type and a todo/write SessionEventMap variant carrying the
whole todo list as a snapshot (last-write-wins on replay). It is NOT a
SurfaceEventType: it produces no LLM message and never reaches
deriveMessages(), so it carries no surfaceOp and stays off the surface — it is
durable, replayable UI state that rides the existing session/event emit.
Tests cover the snapshot-clone-on-append contract, last-write-wins, the
not-on-surface guarantee, and a seeded replay round-trip. Docs: session.md
gains the TodoItem type-equiv block + the event member; core.md's variant count
goes to twelve; the type-equiv manifest gains TodoItem.
Adapt the four fs packages to master's single-tsconfig build convention
(lib/types outDir + types path + files allowlist), brought in by the merge.
While doing so, drop dsh-tool-fs's /read//write//edit subpath plugins. They
were the only subpath-export package in the tree and forced bespoke tsdown,
tsconfig path, package.json files, and workspace-constraint handling that no
sibling tool package (e.g. dsh-tool-bash) carries, for a focused-deployment
use case no consumer needed. dsh-tool-fs is now a single root plugin that
registers read/write/edit, mirroring dsh-tool-bash; the per-tool registration
helpers stay internal modules the root composes. The file-context event-gate
RFC is amended to record the narrowed scope.
The web tool package exposed ./search and ./fetch as standalone subpath
plugins, but nothing consumed them, the RFC never called for them, and the
sibling dsh-tool-bash (also a multi-tool consumer) ships a single entry and
selects tools via config. The extra entries also tripped the workspace
constraints gate, whose expected `files` list covers single-entry and bin
packages but not a non-bin multi-entry one.
Collapse to a single `.` entry: drop the ./search|./fetch exports and their
lib/*.js from package.json files, delete the per-package tsdown override (the
root config's lib/types/index.js entry now suffices), and remove the
plugin-shaped name/inject exports from search.ts/fetch.ts (renaming each
apply to its applyWeb{Search,Fetch}Tool helper, still composed by the root
plugin and re-exported from the index). Selective enablement stays via the
existing { search?, fetch? } config. Docs updated to match.
- edit tool: add the read-before-edit requirement to the model-facing prompt
(with the just-created/edited-this-session exception), matching write's
guidance so the model doesn't only learn it via a failed FS_NOT_OBSERVED call.
- fsspec-style-fs-seam RFC: correct the acceptance criteria that still claimed
a ctx.fileContext service and a fileContext inject — the landed design is the
fs/* event gate with the tool injecting fs.
- filesystem-tool-schemas RFC: replace the stale "prior full file state"
edit requirement with version-freshness wording (any windowed read authorizes
a fresh edit; no partial-view flag).
Correct the filesystem-wiring claims flagged by codex: no default/example
config wires the fs tools yet (the demo agents do file ops through bash), so
the docs and RFC no longer assert that "the default product config loads
dsh-file-context". They now state the intended stance — a deployment that
loads the fs tools is expected to also load dsh-file-context for
read-before-write/edit.
Invert the tool↔policy control flow per the file-context event-gate RFC.
dsh-tool-fs becomes the executor — it reads/writes/edits through ctx.fs
directly, owns read windowing, and dispatches fs/write-expectation /
fs/edit-expectation (single-slot waterfalls) plus a contained fs/observed
emit. dsh-file-context drops its ctx.fileContext service and becomes a pure
event-gate plugin (observed-state + read-before-edit + version-guarded
write/edit, decided on those events). The provider's version guard becomes
optional so ctx.fs alone is a complete unconstrained text-storage seam:
removing the policy plugin gracefully loses the policy instead of breaking
the tool at a service-injection boundary.
The per-file 100% coverage gate flagged surface.ts line 46 — the
branch where a surface-eligible event type carries no surfaceOp marker
(isSurfaceEvent returns false). Exercise both guards directly: the
type-only eligibility check, the positive narrowing path, a
non-eligible type, and the markerless-but-eligible branch.
Type-aware ESLint loads every package tsconfig through the project
service and peaks at ~3.4GB RSS. The default V8 old-space ceiling
(~2GB) OOMs it (FATAL ERROR: Ineffective mark-compacts near heap
limit, exit 134) on both node 24 and 26. Set NODE_OPTIONS with an
8GB ceiling for the Lint step, comfortably above the peak.
The web packages were authored against the old single-stage layout where
tsc emitted directly to lib/. Master compiles declarations to lib/types/
via tsc -b, then bundles JS into lib/ via tsdown. Point every web package's
tsc outDir at lib/types, update package.json types/exports/files to the
lib/types declaration + lib/ bundle shape (matching dsh-bash/dsh-tool-bash),
and bundle tool-web's subpath entries from lib/types/*.js rather than src.
Register the five web packages in the root tsconfig.json project graph
(master's typecheck moved to `tsc -b tsconfig.json` and dropped the
separate tsconfig.typecheck.json), and regenerate the module graph and
cordis catalog so they reflect the web packages on master.
The function carried two consecutive JSDoc blocks; the first was an
outdated short version missing the timeout-recovery contract. Keep only
the accurate detailed block.
Resource-lifecycle and error-classification fixes in the local fetch provider:
- Classify a timeout that fires DURING the body read as WEB_FETCH_TIMEOUT, not
WEB_ABORTED: thread the controller signal into the body-read translate path
and recover the timeout WebError from signal.reason, honoring the public
WEB_FETCH_TIMEOUT contract for a stalled response body.
- Cancel the response body before every blocked-redirect throw path
(cross-origin, invalid target, missing Location), so a rejected redirect with
a large or streaming body does not leak the socket after the tool returns
WEB_REDIRECT_BLOCKED.
- Cancel the body when charset validation fails, matching the
unsupported-content-type and over-size paths (the round-1 charset check threw
before readCapped owned the stream).
- Preserve abort errors while parsing search responses: when the caller's
AbortSignal fires after headers but during response.json() (both the success
and HTTP-error body parses), surface WEB_ABORTED instead of wrapping it as
WEB_PROVIDER_ERROR, so agent cancel/dispose is not misreported as a provider
failure. Applied to both the Exa and Perplexity providers.
- Report a malformed baseURL as misconfigured in status() (URL.canParse), so
selection diagnostics and execution agree (configured-unavailable up front
rather than a late WEB_PROVIDER_ERROR). WebProviderStatus already had the
reason.
- Re-validate redirect targets through validateFetchUrl before following, so a
same-origin Location carrying credentials (or a non-http(s)/over-long URL)
cannot bypass the transport hygiene a direct request enforces.
- Treat only DROPPED bytes as truncation: a body exactly at maxResponseBytes is
no longer falsely flagged truncated (which emitted a spurious footer).
- Honor the declared response charset: parse the Content-Type charset and decode
with it (rejecting unsupported labels as WEB_UNSUPPORTED_CONTENT_TYPE) instead
of always assuming UTF-8 and returning replacement characters.
- Catalog the web seam vocabulary in docs/core-data-structures/web.md with
type-equiv blocks + manifest entries, per the core-data-structures rule.
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).