Files
deepseek-harness/packages/settings/settings-file

description, kind
description kind
The file-backed settings provider for users and maintainers choosing, configuring, or debugging the YAML/JSON settings document and its hot reload. package-reference

@deepseek-ai/dsh-settings-file

English | 中文

Summary

dsh-settings-file keeps every namespace's user settings in one YAML or JSON document, by default settings.yaml under the harness home: users can edit the document directly — changes take effect live — or write through the service, which merges concurrent edits safely. YAML writes preserve comments, anchors, and formatting on every untouched node, and a section owned by a plugin that is not loaded is never dropped. Boot fails loud on an invalid document; a live reload that fails keeps the last good sections and warns rather than taking the process down.

Table of Contents


Use this package

Mount this provider when a composition wants one user-editable settings document. The common path is explicit: mount the provider, register namespaces through ctx.settings, and let users edit the document or a configuration UI write through the service.

When to choose it

Choose it as the default user-settings store: one human-readable document that users can open in any editor, with changes taking effect without a restart. Choose it when comments and formatting in that document matter, because writes preserve them. A non-file store, such as a remote settings backend, is not shipped here; that would need another provider.

Minimal configuration

- name: '@deepseek-ai/dsh-settings-file'
  config:
    path: /absolute/path/to/settings.yaml
Field Default Meaning
path <harness home>/settings.yaml Settings document path; the extension picks the format (.yaml, .yml, or .json)
dshHome $DSH_HOME or ~/.dsh Harness home used when path is omitted
watch true Watch the document and hot-publish external edits
debounceMs 100 Watcher write-settle window, in milliseconds

The generated configuration catalog is the exhaustive source for every accepted field and its JSDoc.

Editing the document

The document is a YAML or JSON mapping of namespace to user section. Users can edit it directly: any change takes effect automatically, and deleting the file resets every namespace to defaults and base. A document that exists but is invalid fails plugin load at boot — the provider never silently ignores or overwrites it. Once live, an unreadable or unparsable edit warns and keeps the last good sections, so a hand-edit mistake cannot take the process down.

Writing through the service

Writes through ctx.settings never lose concurrent changes: an external edit still in flight, a change the watcher missed, or another process's write is merged into the document before the write lands. YAML edits are leaf-level diffs: only changed values are set and only removed keys deleted, so comments, anchors, and formatting survive on every untouched node and on the key of every changed pair; a changed array or other non-map value replaces wholesale. JSON documents re-serialize without comments. If the on-disk document turned invalid, the write fails loud instead of overwriting the user's manual edit.

The lock has a 2-second acquisition deadline with exponential backoff; a contender that times out leaves the existing lock in place, because lock age cannot distinguish a crashed owner from a paused live writer — orphan lock recovery is an operator action. The document is created 0600 under an owner-only 0700 directory and replaced atomically through a random-suffix temp sibling that never follows a planted symlink.

Failures and recovery

  • An unsupported extension fails at load — the format comes from the extension (.yaml, .yml, .json).
  • A missing document is an empty store; deleting the file returns to that state.
  • An invalid on-disk document at runtime blocks nothing but keeps the last good sections; a write refuses to overwrite it.
  • prepareDocument() materializes an absent document as an empty owner-only file before a native editor opens it.

Understand the implementation

Implementation internals — click to expand

This section explains the design decisions behind the provider and points at the code that realizes them; the observable behavior is fully covered in Use this package.

Design philosophy

  • One explicit defaulting step. resolveSpec(config) resolves the filename, format, watch flag, and debounce window in one step, so programmatic construction that bypasses Schemastery normalization gets the same defaults.
  • Boot fails loud, reload keeps last good. An existing-but-invalid document fails plugin load; once live, an unreadable or unparsable edit warns and keeps the last good sections.
  • Every write is a read-modify-write. A persist first reconciles from disk and publishes any difference into the seam, then renders against that fresh text, so a write can never resurrect a stale document or drop an unobserved sibling section.
  • Writes hold a cross-process writer lock. The read-render-rename cycle runs under a wx-created <file>.lock sibling with exponential backoff and a 2-second acquisition deadline; readers never take the lock because the rename commit is atomic.
  • YAML edits are leaf-level diffs. Only changed values are set and only removed keys deleted, preserving comments, anchors, and formatting on untouched nodes.
  • Reloads and writes share one operation chain. Watcher refreshes and persists from every namespace queue run one at a time in queue order; each render sees the text the previous operation committed.
  • Self-write suppression by content. The provider caches the last good text; a watcher event whose content equals the cache — its own write included — is a no-op.

Source map

File Role
src/index.ts Provider: spec resolution, load/parse, read-modify-write under the writer lock, watcher lifecycle, YAML/JSON rendering
src/invariant.ts Invariant companion (no runtime invariant; file round-trip, watcher timing, and atomic-write behavior are proven by package tests, and the in-process commit relation is owned by dsh-settings)

Document lifecycle

The base service init loads and publishes the document before the service becomes injectable; the provider then starts the watcher and reconciles once at ready to close the startup gap in which a change written between the initial read and the watcher becoming active never fires an event. Every watcher event and every persist queues onto one exclusive operation chain. reconcileFromDisk compares on-disk text against the cache, publishes any difference (including absence as the empty document), and throws only on a parse failure so each caller picks its policy — a reload warns and keeps the last good document, a write fails loud. Disposal marks the provider closed, closes the watcher, and waits out every queued or in-flight operation so nothing publishes after teardown.

Render paths

YAML renders by parsing the cached text into a mutable comment-preserving tree and patching one namespace with leaf-level edits; JSON renders by replacing one namespace key and re-serializing with two-space indentation. Before Chokidar opens the target, the provider realpaths its deepest existing ancestor and restores any missing suffix, so Windows cannot mix an 8.3 alias with long-form event paths inside libuv.


Further Exploration

Read these pages when the provider-level contract is not enough. They move from the seam contract to the atomic-write primitive and the exhaustive configuration surface.


Model Experience

Indirectly, through the consumers of ctx.settings, which own any model-facing behavior fed by a stored value; the file provider only stores and publishes namespace sections and registers nothing model-facing itself.

KV Cache effect

No direct invalidation; the consuming plugin owns any request-prefix changes.

Known Limitations and Deferred Work

These limits define when the provider is a poor fit or needs special operational care. They are current package constraints, not a task backlog.

  • Same-namespace conflicts stay last-write-wins — the writer lock and read-modify-write keep concurrent writers from dropping each other's namespaces, but two writers editing one namespace still resolve to the later write; there is no per-value merge or revision check.
  • A missed watcher event stays unseen until the next signal — reads never re-stat the file, so a change the watcher fails to report is only folded in by the next event, the next write, or a restart.
  • Comment preservation is YAML-only and map-shaped — JSON documents re-serialize without comments, and comments inside a changed array, or attached inline to a changed scalar value, go with the value they described.
  • No value indirection — sections hold literal values; ${env:VAR}-style references for secrets are a deferred seam-level feature.

Dev Note

Working context for maintainers — click to expand

This Dev Note is working context for maintainers: deferred directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above and the package code. Deferred directions: ${env:VAR}-style value indirection is a seam-level feature — it belongs with the settings service contract when it lands, not with this provider. Orphan lock recovery remains an operator action by design, because lock age cannot distinguish a crashed owner from a paused live writer.