Resolutions: keep both sides where master's new storage/workspace group
registrations sit adjacent to the telemetry ones (gen-doc-graphs
SERVICE_ROLES/GROUP_ORDER, tsconfig.base.json wildcards,
packages/README.md rows); regenerate the graph docs; condense four
packages/README.md rows back under the 790-word ceiling.
The bare 'domain' name was too generic for a published package. The
directory moves to packages/storage/storage-domain, the package becomes
@deepseek-ai/dsh-storage-domain, and the plugin/invariant names follow;
the ctx surface (ctx.storage.domain), the domain/changed event, and all
runtime behavior are unchanged. References, catalogs, graphs, and the
bilingual design note move together.
The jscpd gate flags the deliberately mirrored open/stamp sequence
against session-persistence-sqlite and session-query-sqlite. The copy
is the settled this-phase choice — this group is the third user and the
shared medium helper is deferred to the log-facet migration so the
session packages stay untouched (reuse audit in the design note); mark
the span accordingly.
- domain/changed emission is isolated from the write path: an observer
throwing synchronously can no longer turn a committed (durable +
in-memory) write into a rejection; the failure is logged and later
writes proceed.
- Domain lifecycle belongs to the opening consumer: Domain gains an
idempotent close() (drain, unit close, reservation release), the
facility stops registering effects on its own context and instead
closes any still-open domains on unmount; WorkspaceRegistry holds its
domain through its own effect, so disposing and re-mounting the
consumer no longer wedges on already-open.
- defineDomain rejects a global schema accepting null at declaration
time: JSON null is the medium's absence sentinel, so a nullable global
could never round-trip; failing loud at the spec keeps set(null)
unrepresentable.
Regression tests cover all three (hostile listener, close/reopen and
consumer re-mount, nullable-global rejection).
Dropping async for the lint gate turned the closed-unit guard into a
synchronous throw, breaking the conformance clause that every post-close
operation rejects. Restore async with a justified require-await disable;
the guard's rejection semantics are what the shared suite pins.
eslint --fix formatting sweep plus the manual residue: sync method
bodies drop async behind Promise-returning signatures (the sqlite unit
routes primitives through a settle() guard preserving the never-throws-
synchronously contract), catch callbacks type their reason as unknown,
loadAll's global slot is plain unknown (null semantics stay in JSDoc),
a non-null assertion becomes a narrowing, and unsafe any assignments in
tests gain explicit types. One justified eslint-disable for
prefer-promise-reject-errors follows the core/session precedent —
wrapping would discard the original StorageError code.
- Per-file 100% coverage across the five new packages (invariant
companion suites, failure-injection negatives, lifecycle and
malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
storage/ and workspace/ group READMEs; packages/README.md rows (budget
ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.
doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
Review findings applied across the group:
- storage hub: stale disposers no longer remove a successor registration;
the package now default-exports the Storage service class per the
service-package export shape.
- json backend: failed publishes roll back the authoritative memory state
(a rejected write can no longer resurface via get() or ride the next
publish); close() drains in-flight writes and blocks in-flight opens;
double-open rejects as a plain caller error instead of malformed-medium.
- sqlite backend: loadAll builds records on a null prototype (__proto__
keys round-trip instead of polluting), user_version is stamped only
after the schema is fully created, and corrupt record JSON rejects as
malformed-medium instead of a bare SyntaxError.
- domain form: writes persist before mutating authoritative memory or
emitting; DomainChanged is a put/deleted discriminated union.
- workspace: attach/detach idempotence decided on the write chain (stale
snapshots no longer short-circuit), create() requires a directory, and
startup fails loud on duplicate stored paths.
Eleven regression tests pin the fixed behaviors.
ctx.workspace registry owns WorkspaceId-branded records: realpath-
normalized unique paths (create rejects collisions; resolveByPath shares
the normalization), ordered sessionIds as the single source of ownership
truth, attachSession gated on the session header cwd matching the
workspace path (double-booking structurally impossible), dead session
ids filtered on projection and pruned on the next mutate, status()
reporting missing directories. No delete surface this phase — deletion
ships together with the session-side primitives as future work.
ctx.storage.domain opens declared domains: zod value schemas parsed at
the durable boundary, one write chain per domain (update(fn) is the only
read-modify-write), domain/changed emitted per record after durability
(new snapshot + operation, no old value, per repo event convention).
Domain-to-backend routing is configuration (default backend + per-domain
overrides); unknown names and missing facets fail loud. Ships the
MemoryStorageBackend test helper and a runtime invariant asserting every
change event matches the in-memory state.
node:sqlite DatabaseSync with the session-persistence-sqlite open
sequence (0o700 dir, exclusive 0o600 create, foreign_keys, configurable
journal mode, user_version stamp-or-reject). STRICT tables throughout:
units/unit_globals meta tables plus one document-per-row table per
declared unit table, keeping per-key durable updates precise.
Atomic whole-file replacement (same-dir temp + fsync + rename + parent
fsync); the in-memory unit state is authoritative and the file is always
the current net state, pretty-printed. Missing files open as empty units
and materialize on first write; foreign or unparsable files reject with
malformed-medium, stored-version drift with version-mismatch.
ctx.storage is a pure registration hub: multiple named backends stay
mounted side by side, data forms (domain first) mount via the
merge-extensible StorageForms map. src/backend.ts is the normative
KV-facet contract; tests/contract.ts is the shared conformance suite
every backend runs. Backends expose data-shape facets (kv now, an
append-log facet reserved for the future session-backend migration).
Lint (bridge JSDoc params, service-class export shape, async invariant
listener form), regenerated doc catalogs/graphs with role classifications
for httpServer and clientModuleHost, catalog type-link exemptions for the
route/graph contracts, knip alignment (apps/cli composes via cordis.yml so
its yml-named deps are runtime edges knip cannot see; webserver's deleted
test dir), the zh side of the loading-model note brought along with its
pairing records, and coverage exclusions for the new web-transport halves
under the GUI test-lane TODO (real-composition harnesses land with that
lane).
The four free functions in boot.tsx become one kernel class holding what
must exist before cordis: the parsed BootManifest, the ClientModuleSystem
instance, and the loading-page handles. Context/Loader setup runs in
parallel with the immediately-tier prefetch, but entry creation awaits the
prefetch: materialization is tree.import's synchronous require, so
cross-package require edges (i18n -> runtime/client) need every
immediately-tier factory registered first — unbarriered creation raced
10-25% of boots. The kernel adopts the modules entry (writes the
__DSH_MODULES__ slot pre-cordis, creates the entry first, skips its graph
row), and provide('modules') now lives in the adoption apply. apps/web
drops its host-package edges (composition is apps/cli's job).
connection binds the web transport: it injects httpServer + apiProxy and
registers toFetchHandler(ctx.apiProxy) under the /api prefix (the node:http
to fetch bridge moves in from the webserver, keeping the res-close disconnect
detection and drain/close backpressure waits). hmr owns dev reload: a
stat-poll watch per graph row driven by clientModuleHost.onGraphChanged,
rebuilt(id) on content change, and the /plugins/events SSE route (GET/HEAD
guarded); frame types are single-sourced in events.ts shared by both halves.
The node half is ClientModuleHostService (ctx.clientModuleHost): it composes
the __DSH_BOOT__ graph by scanning loader entries for dshClient packages,
serves /plugins/<id>/client.js, taps the index render, and exposes
rebuilt/onRebuilt/onGraphChanged. Scanning is incremental per package — no
full-rescan path exists: internal/plugin marks the fiber's entry name dirty,
a flush reconciles each name against live entries, package metadata
(including negative verdicts) caches forever, and re-hashing is reachable
only through rebuilt(id). The browser half moves wholesale to the standard
./client export (ClientModuleSystem, parseBootManifest with the dual-view
BootManifest, and the adoption plugin face that reads the
window.__DSH_MODULES__ slot and provides ctx.modules).
HttpServerService provides ctx.httpServer: register(route) -> disposer
(duplicate patterns throw), tapIndex transforms in registration order, and
the bound port; matching is exact > longest prefix > static dist fallback
(403/405/SPA semantics preserved). The server listens on activation, answers
per-request failures with 400 + a log line instead of exiting the process,
and knows no harness concepts — the boot graph, bundle routes, SSE channel,
and /api prefix all moved to their owning plugins.
createApiProxy moves from dsh-host-runtime into dsh-host-apiproxy (the
dependency direction already pointed this way); the package now
default-exports ApiProxyService (config {provider, model}, provides
ctx.apiProxy) while staying transport-agnostic — it registers no routes.
Runtime keeps bootHost/startHost for the headless path with its import
re-anchored, and drops the mountWebPlugins roster mounting helper.
Second review round, both pinned red-first:
- Overlapping turn-boundary flush hints now JOIN the outstanding flush
promise (Promise.all) instead of displacing it: the SDK's
concurrent-flush guard resolves an overlapping forceFlush()
immediately, so retaining only the latest promise let shutdown()
proceed while the first export was still in flight — the same silent
drop the single-flush fix closed.
- Adoption replay contains failures per event, matching the firehose:
one rejected record is withheld fail-closed while the rest of the
historical log still hands off. Wrapping the whole loop let a single
failure silently skip the remainder on an already-adopted session.
Three review findings, each pinned by a red test first:
- The OTel backend retains the latest turn-boundary flush promise and
shutdown() awaits it before provider.shutdown(): the SDK's
concurrent-flush guard makes the shutdown-internal flush return early
while one is in flight, silently dropping everything enqueued after
the flush snapshot (including the coordinator's dispose-time shutdown
markers).
- The coordinator retires sessions from the adopted set on
session/disposed (mirroring the persistence coordinator): a long-lived
backend no longer retains closed sessions and their event logs, and
final unload no longer stamps shutdown markers for dead sessions.
- The exporter config passes through whole to OTLPLogExporter and its
type/JSDoc now advertise the full OTLPExporterNodeConfigBase shape:
rebuilding url/headers only silently dropped documented SDK options
(timeoutMillis, compression, keepAlive, ...).
tests/otel.e2e.ts self-skipped on $DSH_OTLP_E2E_ENDPOINT, which nothing in
the repo sets — it never ran; the mock-collector wire spec and the keyless
Loader-composition e2e already cover the pipeline both ways.
The severityOf compact/end probe parsed another package's merged event
shape by string comparison — an untyped cross-package contract that breaks
silently — and its only consumer was the test's own stand-in declaration.
Unknown event types now uniformly fall through as info; outcome semantics
stay with the owning package.
The seam keeps the telemetry/redact scrubbing interface but ships no rules
of its own: the innermost next() passes records through unchanged, and
deployments mount their rules as waterfall listeners. As an SDK we cannot
know which patterns are secrets in a given deployment; a shipped list
invites false confidence while catching only known shapes, and false
positives would corrupt exported bodies. Mechanism stays with the seam,
policy moves to the deployment; both READMEs and the Agent Note state the
raw-export default plainly.
The loader-composition e2e now mounts a deployment-style rule fixture and
pins the same wire behavior: secret absent, placeholder present, canonical
log untouched.
Revive the reviewed session-telemetry packages from the closed
session-telemetry-otlp-rfc branch (PR #222/#231) on current master, renamed
to @deepseek-ai/dsh-session-telemetry{,-otel} (the SDK component-telemetry
package holds the dsh-telemetry name).
Delta over the branch version: every record now passes a telemetry/redact
waterfall between projection and emit() — the innermost next() applies a
non-configurable conservative credential-shape rule set, listeners stack
stricter rules, a throwing rule withholds the record fail-closed, and the
canonical log is never rewritten. This answers the export-side concern that
closed PR #222; the boundary axiom (our aspect ends at emit(); delivery is
the reporting SDK's) is unchanged, and the runtime-telemetry RFC's outbox /
readCommitted lane is recorded as deferred in the Agent Note.
Covered by seam/redact/OTel-wire unit tiers (100% per-file) and a keyless
Loader-composition e2e that boots the examples fixture against a mock OTLP
collector and pins redaction on the wire plus the untouched canonical log.
The dev bundle watch missed rebuilds that landed while the registry was
constructing: fs.watchFile captures its comparison baseline with an
ASYNCHRONOUS first stat, so a write racing that window is absorbed into
the baseline and never reported. Standalone repro missed 24/400 same-tick
rewrites; the CI flake in web-plugins.spec.ts ('watch mode: a bundle
content change re-hashes the row...') was exactly this — the spec writes
immediately after createHostWebPluginRegistry returns.
The watch now polls from one registry-owned setInterval against a stat
baseline the scan itself captures synchronously, stat-before-read: a
write landing between stat and read leaves the hash newer than the
baseline (next tick re-hashes to the same rev, no spurious notify); a
write landing after the read leaves the baseline older (next tick
detects and notifies). No blind window. The poll iterates the live
table, so rescans retarget the watch for free and dispose clears one
timer. Stress: real-registry same-tick rewrite 0/600 missed (was 1/300);
spec watch tests 0/50.
New regression test pins the same-tick-as-construction write. Its
rewrite deliberately differs in size from the seed: a same-millisecond
same-size rewrite is invisible to any mtime+size poll (coarse fs
timestamps) — a stat-polling limit, not this regression. Loading-model
Agent Note updated in both languages (pair re-recorded).
Keep inherited child prompt markers bounded by the normal silence fallback. Stage web-plugin rescans atomically and retain missing watch state until a successful rebuild.
The automation-only rewrite edited many implemented Agent Notes; several
edits replaced still-live or historical rationale instead of reframing:
- llm-model-catalog: restore the prompt/request consistency section and
selection-ownership alternatives — installAgentLlmTarget and the TUI
/model selector still ship that design; only the ACP wire is gone.
- plan-specific-collaboration-state, acp-multi-session, todo-write,
ask-user-question: link the superseding automation-only note instead
of silently rewriting the original decision or motivation; drop a
paragraph duplicating the Web-provider facts stated two paragraphs up.
- sandbox: stop claiming unit coverage for turn-enclosed config writes
(that mechanism left with the bridge) and retitle the commit-boundary
paragraph accordingly.
- Fix the missing blank line before '## Consequences' in the
plugin-command-registration pair, the JSON-RPC/Web render-intent
consumer misattribution (the second consumer is the host/client
runtime), stale bash_output/bash_kill names, and 'optional goals' in
architecture.md.
- examples/acp-agent/README.md: point at the package contract instead
of restating it; packages/ui/permission and plan-mode READMEs record
the consumer-less preset service and the exit_plan_mode coverage gap
under Known Limitations.
- 2026-06-19-acp-snapshot-tests: the new note defers the corpus
migration rather than committing to it; say so.
Re-record the touched bilingual pairs.
ACP v1 requires every agent to accept text AND resource_link prompt
content; the automation rewrite dropped the resource_link half of that
baseline. Restore the old bracketed-reference flattening in the codec,
reject only beyond-baseline blocks, and update the package contract and
Agent Note.
Also release the per-session prompt slot when agent.send() throws
synchronously (an agent disposed outside the bridge would otherwise
wedge the session into permanent 'already in flight' rejections),
drop the tautological version-negotiation branch, prove the scenario
env layer reaches the snapshot subprocess, pin bridge-side fail-closed
permission errors, and correct two overpromising test names.
The fake terminal retains earlier autocomplete frames, so the directory follow-up assertion could pass before the scoped lookup completed. Wait for the quoted file mention to be applied before submitting.
The master merge introduced a tui goal-restore test and the guard parent
README that still used the removed context/message event. Point both at
the coalesced plugin-sourced user/message.