Files
deepseek-harness/packages/ui/app-boot
Turtle f290a8b851 refactor(cli)!: one shared base config with per-surface overlays
`dsh` shipped two config trees that were 43 rows the same: apps/cli/cordis.yml
composed web as 74 flat rows, while the TUI booted examples/tui-agent/cordis.yml
whose single `@deepseek-ai/dsh-tui-demo` row mounted twelve plugins behind a
twenty-key pass-through Config. Neither file was what its location claimed —
apps/cli hardcoded the "example" as the product default and the "demo" bundle
was the application — and every capability change had to be made twice.

- apps/cli/base.cordis.yml holds the 43 shared rows; tui.cordis.yml and
  web.cordis.yml are patch lists stating only what differs per surface
- overlays apply as SIBLING patch lists at one include level, because include
  patches never cross an include boundary. Precedence: base < surface <
  (--config | personal ~/.dsh/config.yaml) < launcher flag/profile patches
- `--config` now applies an overlay INSTEAD OF the personal one, so a demo or
  test tree never inherits the user's route; new `--config-replace` boots a file
  as the entire tree (the old `--config` behaviour). Both survive /resume
- vendor/include: index each `insert`ed row as it is added so a later patch can
  configure or disable it. Upstream built the id index once before the patch
  loop, leaving every surface-only row — the whole TUI front door — silently
  unpatchable from user config. Logged as local modification 8
- session identity moves to dsh-agent-loop's CONFIGURED_AGENT_IDENTITIES_KEY;
  dsh-tui's MAIN_SESSION_ID_KEY is deleted (only the bundle read it)
- delete examples/tui-agent, examples/cordis-agent, packages/examples/tui-demo;
  TUI tests → apps/cli/tests, cordis e2e → packages/cordis/tool-cordis/tests,
  examples/code-mode survives as an overlay leaf
- `dsh web` gains --config, threaded into AppCLIEntry as an extra overlay

Three latent defects surfaced and are fixed here: the TUI captured the optional
sessionQuery service once at construction and could permanently disable /resume
when it won the mount race; the session-store root silently reverted to a
project-local ./.sessions; --config-replace was dropped by the resume handoff.

Verified by booting each tree through the real Loader (TUI 55 entries, web 75,
zero unsettled) rather than reading YAML. All eight terminal snapshots replay
byte-identically; 14/14 PTY smoke, 112/112 snapshots, 25/25 doc-sync, hygiene
and lint clean.
2026-07-29 21:15:42 +08:00
..

@deepseek-ai/dsh-app-boot

English | 中文

Shared boot glue for the app bins (dsh, dsh-cli-demo, dsh-acp-demo): each bin is a thin self-executing composition over these helpers, parameterized by its diagnostic prefix, so the loader-failure lore lives once — under the per-file coverage gate — instead of drifting between published artifacts.

Export Role
resolveConfigPath(path, snapshotMode, cwd?) Absolute config path; snapshotMode === 'replay' swaps a cordis.yml/.yaml basename for its sibling cordis.snapshot.yml
loadEnv(binName, dir?, warn?) Load the gitignored .env (Node process.loadEnvFile); absent file is fine, an unloadable one warns a single labelled line (default: stderr)
installFailLoud(binName, proc?) Turn a post-boot() unhandled Loader rejection into one labelled stderr line + exit(1); returns the uninstaller (for tests)
assertEntriesLoaded(ctx, binName) Throw when a settled tree holds an enabled entry with no fiber, reporting every unresolved plugin name as a Cordis startup failure
loadPersonalPatches(binName, dir?) Parse the optional config.yaml in the Harness home (default resolveDshHome(): $DSH_HOME, else ~/.dsh) — a top-level YAML array of include PatchOptions (id-targeted config overrides, insert lists, !!js allowed); absent file → undefined, an unreadable/unparsable/non-array file throws
boot(binName, absoluteConfigPath, patches?, prepare?) Create the root context, run optional host preparation before plugins mount (prepare is where a bin provides launcher-owned context slots a mounted app reads, such as MAIN_SESSION_ID_KEY), then mount the Loader/include tree, await it, assert entries loaded, and return the root context
addHarnessSourceSection(ctx, sourceRoot) Add a global harness:source prompt section (ordered just after the harness identity, before the persona) telling the agent the on-disk path to its own source checkout; a no-op returning undefined when the booted tree has no systemPrompt service. The section is registered against that service's fiber, so a dev HMR reload of the system prompt drops it until the next boot
HARNESS_SOURCE_SECTION The 'harness:source' section name addHarnessSourceSection registers under

Two failure classes the guards handle: loader.await() swallows init rejections (Promise.allSettled) — Node still exits non-zero on the resulting unhandled rejection, and installFailLoud replaces the noisy dump with one labelled line and a guaranteed exit(1); a failed plugin import is only logged by the Loader (the process would otherwise exit 0 on a usable config typo), leaving a fiber-less entry that assertEntriesLoaded turns into a boot() rejection naming every failed plugin.

Bare plugin specifiers in a config (@deepseek-ai/dsh-*, npm packages) resolve through the Cordis Loader's internal module loader. Repository bins install Loader's optional node-addon-require-builtin peer; external callers must supply it or install plugins where plain Node import resolution can find them. Relative specifiers resolve against the config directory without the native helper. The dsh source launcher additionally maps manifest-declared workspace packages to their TypeScript source; its configuration gate requires every TUI/Web bare plugin to appear in the resolver manifest's dependencies. The bins' subprocess smokes exercise the internal-loader path, while this package's unit suite drives boot() in-process against configs with relative specifiers.

This package carries no loader hooks and no dev-mode surface. The dsh app owns its Node source-launch hook and consumes these helpers for the boot sequence; built consumers continue to use plain Node package resolution.

Personal config

A developer's machine-local preferences live outside every repository in the Harness home (default ~/.dsh, overridable via $DSH_HOME; the single root resolveDshHome resolves), consumed by the dsh CLI's TUI surface (apps/cli); the demo bins boot their committed trees verbatim. Two optional files:

  • .env — loaded after the invoking directory's .env; process.loadEnvFile never overrides, so precedence is ambient environment > project .env > personal .env.
  • config.yaml — loader overlay patches applied over the shipped default config, with the same semantics as an include entry's patches (the committed Code Mode overlay is the template): an id-targeted patch replaces the named entry's whole config (restate unchanged fields), insert adds entries, and !!js expressions interpolate at mount — so a personal apiKey can reference the personal .env. A patch naming an entry id absent from the booted tree is skipped with a loader warning. An empty or comments-only file throws (it parses to nothing, not to a list); disable the overlay with [] or by deleting the file.

Subprocess test launchers point DSH_HOME at an isolated per-test directory so a developer's personal overlay can never leak into fixtures.

Model Experience

Indirectly, through the plugin tree it loads, which determines the prompts, schemas, messages, and model adapter in the resulting application; the one export that contributes model-visible text, addHarnessSourceSection, does so only when a consumer calls it after boot.

KV Cache effect

No direct invalidation from boot(); a consumer that calls addHarnessSourceSection places one short line near the system prompt's head, before per-request content, so it does not invalidate the cache across turns, and any other request-prefix change is owned by the named consumer.

Known Limitations and Deferred Work

  • Bare package specifiers depend on Loader internals — production bins need Loader's optional native helper; an in-process caller without it must use resolvable relative/file specifiers or provide its own module-resolution hook.
  • Snapshot replay swapping is basename-specific — only a config ending in cordis.yml or cordis.yaml maps to the sibling cordis.snapshot.yml; custom config names require caller-managed selection.
  • Environment loading is cwd-scoped and optional — the helper loads one .env file and warns on failure; it does not search parents, merge profiles, or validate required variables.
  • Personal config is patch-shaped — an id-targeted patch replaces the entry's whole config rather than deep-merging, so a personal override restates the base fields it keeps.
  • Personal patches see only the booted file's own entries — an overlay leaf that reaches its base through a nested include entry (the Code Mode configs) resolves personal patch ids against the overlay's top-level entries, not the included subtree.