mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Review round three, credentials half. dsh-atomic-write grows the cross-process writer-lock primitive (withFileLock: wx sentinel, bounded backoff, stale takeover via onStaleBreak, deadline failure) plus a dirMode option, and settings-local migrates its private copy to it; both providers now create harness-home directories 0700. credentials-local reuses the reviewed settings-local shape: watcher reloads and line edits share one settled operation chain; every write re-reads the document under the lock and publishes unobserved external entries before editing, so an edit inside the debounce window (or another process's write) can never be overwritten; the watcher's ready signal queues one reconcile closing the startup gap. The line editor is now physical-line aware: continuation lines of a quoted multi-line value are never mistaken for assignments, untouched lines keep their exact bytes (CRLF included), an edited line keeps its own terminator, and appends use the document's dominant ending. A multi-line entry reports writable: false, matching what set() would do. The Credentials base class owns a contained notifyUpdated fan-out: providers publish only after the commit, every listener runs, sync throws and async rejections are logged without failing the committed write, and INVARIANT-coded failures rethrow after the fan-out.
@deepseek-ai/dsh-settings-local
English | 中文
File-backed settings provider. One YAML or JSON document carries every namespace section; external edits hot-publish through ctx.settings, and update() re-reads the document under a writer lock before writing back atomically, preserving the user's YAML comments, any section owned by a plugin that is not currently loaded, and any on-disk change this process has not observed yet.
Config
| Field | Meaning | Default |
|---|---|---|
path |
Settings document path; extension picks the format (.yaml/.yml/.json) |
settings.yaml under the harness home |
dshHome |
Harness home used when path is omitted |
$DSH_HOME or ~/.dsh |
watch |
Watch the document and hot-publish external edits | true |
debounceMs |
Watcher write-settle window in milliseconds | 100 |
Defaulting is one explicit resolveSpec(config) step; an unsupported extension fails at load.
Behavior
- 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. A missing document resolves every namespace from defaults and
base; deleting it publishes the same empty state. - Every write is a read-modify-write. A persist first re-reads the document and publishes any difference into the seam — an external edit still inside the watcher debounce window, a change the watcher missed, or another process's write — then renders against that fresh text, so a write can never resurrect a stale document or drop an unobserved sibling section. If the on-disk document turned invalid, the write rejects loud instead of overwriting the user's manual edit.
- Writes hold a cross-process writer lock. The read-render-rename cycle runs under a
wx-created<file>.locksibling with exponential backoff, a 2 s acquisition deadline (the write rejects), and stale-lock takeover after 5 s (a crashed holder, broken with a warning). Readers never take the lock: the rename commit is atomic, so reloads are always consistent. - Write-back is atomic, owner-only, and symlink-proof. The render exclusive-creates a random-suffix temp sibling with mode
0600(wxrefuses to follow a planted symlink) and renames over the target, cleaning the temp up on failure. - YAML edits are leaf-level diffs. A write sets only the values that changed and deletes only the keys that were removed, 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, taking comments inside it along. JSON re-serializes without comments.
- 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.
- The watcher's ready signal reconciles once. The initial load races the watcher's own setup, so a change written in between never fires an event; the reconcile at ready closes that startup gap.
- Dispose quiesces. Teardown stops accepting watcher events, closes the watcher, then waits out any queued or in-flight operation, so nothing publishes after disposal.
- 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.
Model Experience
Indirectly, through consumers of ctx.settings: this provider only stores and publishes namespace sections, and each consumer's own surface documents any model effect.
KV Cache effect
No direct invalidation; the consuming plugin owns any request-prefix changes.
Known Limitations and Deferred Work
- 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 (JSON has none), 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.