Four values complete the vocabulary, so the opaque body is reached only by
producers that genuinely promise no shape.
`snapshot` — current state a later snapshot supersedes. system-prompt now
exposes `renderContextSections()`, the named contributions
`renderContextSnapshot()` already joins for the model, so the body attributes
each part to the subsystem that produced it instead of re-splitting joined
prose. The runtime snapshot, time-context, and tmux-context declare it.
`notice` — a one-off account of what just happened, declared by tool-tasks,
goal state changes, tool-goal wrap-up, plan-mode switches, and
repeat-tool-guard. Its `summary` rides the COLLAPSED row: these five are the
majority of shipped producers and none of them needs expanding to be read.
The task summary bounds itself because its inputs are unbounded caller text.
`relay` — a message another agent addressed to this one; both subagent
sources declare it and the body names the sender above what it said.
`recall` — material lifted from another session's log. session-reference
needed no new field: its references already record retained and omitted
counts and the truncation flag, which the body shows first, because recalled
context is bounded on the way in.
`ContextFormed` is now discriminated by `form`, so a producer cannot declare
a shape without the facts that shape is presented from — a notice without its
summary, or a snapshot without its sections, fails to compile.
Only the two hook bridges stay opaque, by design: their content is whatever
an external program printed, so no shape can be promised for it. Unknown
kinds and unreadable records land there too.
- `catalogHistory` validated its durable read. `agent.session.events` is a
JSONL/SQLite seed on resume or fork, and seed validation guarantees only a
source object with a non-empty `kind`; a `skill-catalog` record with missing
or wrongly shaped `entries` threw inside the step listener, failing every
later turn of that session. It is now skipped as an unrecognizable record,
the posture the replaced content digest had, with a regression test over six
malformed shapes.
- The headless keyless smoke still filtered catalogs by the old plugin source,
so the `built-bin-smoke` gate would not have found the catalog message.
- Entries record the published description unescaped. The pseudo-XML escaping
belongs to the `<available_skills>` frame and is applied when rendering it,
so a description containing `<` no longer reaches the card as `<`.
`escapeText` is injective, so republish semantics and the model-facing text
are unchanged.
- Adjacent text blocks join with no separator, matching how provider adapters
flatten them; the body no longer shows a line break the model never saw.
- Provenance fields are bounded like the text: an unknown producer may record
an arbitrarily large value.
- Both readers are all-or-nothing, and the row's form marker reports what
rendered rather than what was declared, so a partly unreadable record cannot
present a confident but incomplete account.
- The catalog body consumes `update` as a replacement notice; the digest
canonicalizes per entry as JSON, since every separator character is itself
legal in a description.
- `core.md` documents the form axis with a `ContextForm` type-equiv block, and
both projections assert the wiring they duplicate.
Injected context reached the transcript as one anonymous shape whose
expanded body was the whole message serialized as inline JSON, so the
escaping collapsed the only readable part — the model-facing prose —
into a single line.
`MessageSource` gains an optional producer-declared `form`, a small
semantic vocabulary of information shapes independent of `kind`: kind
says who produced the context, form says what shape it is, so several
producers may share one presentation. Two values ship.
`instructions` (workspace-context) lists the reconciled files above
their text and keeps the `<system-reminder>` framing verbatim, because
the framing is part of what the model read. `catalog` moves dsh-tool-skill
off the shared plugin kind onto a `skill-catalog` source carrying the
published name/description entries, and the body lists those instead of
re-parsing `<available_skills>` out of the prose. Catalog identity moves
with it: the republish digest now covers the durable entries, deleting
the text-slicing that recovered them from a logged message.
Everything else renders the opaque body — the model-facing text with its
real line breaks, then the remaining provenance as fields. That is the
documented default, not a leftover: a resumed, forked, or foreign log
must render whether or not its producer is mounted here, which is why
the classification lives in the durable source rather than a client-side
table keyed by producer.
Review found five real defects in the configuration-source work, all confirmed
against the code rather than argued:
1. The note claimed --config outranks settings.yaml. It does not: the settings
seam registers a plugin's cordis entry config as the `base` layer and the
user section layers over it, and the seam cannot tell a shipped value from a
--config one. The note now states shipped reality and names --config-replace
as the lever for a deployment that must win. Separately, a literal `apiKey`
in settings outranked both the environment and .credentials.yaml — the field
is removed, so configuration carries a reference and nothing else.
2. DEEPSEEK_SEARCH_BASE_URL was functionally deleted: the shipped inline went
away without the provider learning to read it. It now resolves from the
environment snapshot, as the README always claimed.
3. The bootstrap deny list missed the interpreter start-up hooks. BASH_ENV is
the sharpest: `bash -c` sources it on every bash tool call, so a project
.env could run a file of its choosing before every command. The list now
covers BASH_ENV and its per-language siblings, the Git hook commands, and
the remaining preload and CA variables, organised by what a variable does
rather than which runtime owns it.
4. YAML parse errors quoted the offending source line — which in a credentials
document is the secret — into boot stderr and the watcher's logger. Only the
error code and position are reported now, in credentials-local and
settings-local alike, pinned by a test that asserts the secret is absent.
5. 0600 governed only files the harness wrote. A hand-created 0644 document was
read normally. POSIX now checks the mode before reading contents, at boot
and on every reload; Windows has no mode to inspect and is skipped rather
than faked.
The project a session is launched in is trusted by default, with no prompt and
no stored trust record: it may supply its own endpoint, ordinary variables, and
a key ranked below the managed store. Trust stops at the harness itself — a
discovered file still cannot set DSH_PERMISSION_MODE, PATH, BASH_ENV, or the
rest, because those take effect with no user action, before any turn, outside
the permission policy and the sandbox.
$DSH_HOME/.env carried two incompatible jobs. As credentials-local's writable
secret store it could not be hoisted into process.env — hoisting makes every
stored key read as a read-only launch override and blocks rotation from the
TUI and the web page. But its name and dotenv format promise an environment
file, so a DEEPSEEK_BASE_URL sitting beside a working DEEPSEEK_API_KEY in the
same file was silently ignored: only the credential provider read the
document, and it addresses credential references alone.
Split the two jobs into two files.
.credentials.yaml is the provider-managed store: a strict YAML mapping of
CredentialRef to non-empty string, no version field, no wrapper level. Because
it holds credentials and nothing else, a non-mapping root, a non-identifier
key, a non-string value, an empty string, a duplicate key, and malformed YAML
are all rejections rather than skipped entries — loud at boot and at a write,
warn-and-keep-last-good on a live reload. The dotenv physical-line editor
gives way to a patch of the parsed document, so comments and untouched entries
keep their formatting and any string value round-trips, multi-line included.
Writer lock, read-modify-write, atomic 0600 write under a 0700 directory,
watcher, self-write suppression, and quiescent disposal are unchanged.
$DSH_HOME/.env becomes the user's ordinary environment layer. app-boot's new
loadLayeredEnv loads the invoking directory's .env then the Harness home's,
giving user < project < inherited; the home resolves from the inherited
environment first, so a project .env cannot redirect it.
Credential precedence is unchanged: the live environment still wins read-only
over the file, and shadowed writes still reject. Whether a provider-managed
store should instead win over the environment is a separate decision.
No migration: a key already in $DSH_HOME/.env keeps resolving through the new
environment layer, as a read-only env source that shadows the stored one.
- Wire the control service and send_message tool into every shipped
composition with a resumable provider and background enabled
(headless-agent, tui-agent, and the SDK helper's subagent feature
base resources); jsonrpc-agent disables background and is unchanged.
- Resolve the send_message availability check in the CALLER's tool
scope so a restriction that removes the follow-up tool from one
agent also blocks that agent's continuable start.
- Control-service disposal now cancels live activations and awaits
producer settlement instead of stranding them: TaskService keeps
producer Tasks across a reload, so the disposing service aborts each
activation-owned controller, resolves its terminal gate (the
effect-scoped onTaskDone listener is already gone), and awaits done.
A new test kills a mid-start activation through HMR disposal.
Implement the continuable background subagents RFC: a durable child
session with a series of Task-backed activations, each disposing its
run before the Task settles.
- dsh-subagent: rename SubagentRun.sendMessage to strict steer, drop
run-level resume, add SubagentProvider.resume dispatch via
SubagentService.resume, the continuation start field, and the
versioned model-hidden subagent/descriptor session event.
- dsh-subagent-inprocess/-spawn/-fork: publish the control-allocated
child id, append the descriptor inside the initial turn, implement
cold resume from the child's own transcript under the live parent
scope, and strict running-only steer.
- dsh-subagent-control (new): SubagentControlService owning stable
child ids, descriptor snapshot/fold/authorization, Task-backed
activation with settle-then-dispose ordering, the process-local
active-run association, and steer-or-resume sendMessage routing.
- dsh-tool-subagent: background route branches on the provider's
resume capability (continuable via the control service; one-shot
task for ACP), returning both child and task ids.
- dsh-tool-subagent-control (new): the globally named send_message
tool rendering steered/started routes.
Keyless coverage spans Task ownership and disposal ordering, running
delivery, cold follow-up, descriptor rejection and rollback, known-id
reconstruction, kill during lookup, admission races, and a new
subagent-continuable ACP snapshot scenario.
ds-review-bot round 1 on the repository-plugin runtime:
- a manifest-declared skill root absent or non-directory in the installed
package now fails the plugin load (skill-local treats a missing root as
legitimately empty, which silently mounted a skill-less plugin)
- includeDefaultRoots: false no longer inherits $DSH_BUNDLED_SKILL_DIR, so
isolated repository providers see only their explicit roots
- prepared wrapper baseUrl schema requires the file: scheme, failing hostile
URLs at the declared validation boundary
- preparedPath reuses format.ts's isOutside; SERVER_NAME_PATTERN is exported
and pinned equal to dsh-mcp-client's, with the restatement justified (the
prepare bin keeps a zod-only module graph); the unexplained `as never`
cast now carries its schemastery rationale
- the import-free wrapper assertion also rejects dynamic import(
- the headless fixture wrapper is regenerated by the real prepareDshPlugin
and a drift test pins fixture == generator output
- prepareDshPlugin JSDoc states the non-atomic publish repair contract
Resolve the agent-loop import conflict by retaining both durable request context and runtime policy context. Refresh the combined session fixtures and regenerate documentation catalogs. Mark PDF artifacts as binary so staged whitespace checks do not parse PDF bytes as text.