Skip to content

Latest commit

 

History

History
60 lines (41 loc) · 13.3 KB

File metadata and controls

60 lines (41 loc) · 13.3 KB

@deepseek-ai/dsh-app-boot

English | 中文

Shared boot glue for the app bins (dsh and dsh-acp-demo): each bin is a thin self-executing composition over these helpers, parameterized by its diagnostic prefix, so loader-failure behavior has one owner 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)
loadLayeredEnv(binName, cwd?, warn?) Build the product CLI's frozen inherited > project .env > user .env snapshot, reject bootstrap-only file variables, and materialize accepted file values without replacing inherited ones
installFailLoud(binName, proc?, release?) Turn an unhandled boot or later Loader rejection into one labelled stderr line + exit(1); the optional release teardown is awaited between the two (bounded by FAIL_LOUD_RELEASE_TIMEOUT_MS) so a terminal-owning surface restores the terminal before exit; returns the uninstaller
FAIL_LOUD_RELEASE_TIMEOUT_MS How long installFailLoud waits for its release hook; a wedged disposer delays the fatal exit, never cancels it
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
assertEntriesActivated(ctx, binName) Include the assertEntriesLoaded check, then await every enabled entry after the Loader settles; throw with each failed plugin's original stack or each pending plugin's unresolved services
loadOptionalPatches(binName, file) Parse an optional patch-list file (a profile's cordis.patch.yml) — 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
loadOverlayPatches(binName, file) Parse a required top-level YAML array containing the same include PatchOptions entries described above; a missing file also throws because the caller named it
mountRootInclude(ctx, absoluteConfigPath, patches?, bareModuleBaseUrl?) Register the statically imported cordis:include and cordis:group builtins, mount the include, and retain the exact root entry used by user patch-layer HMR; an optional module base anchors bare package names to the installed host while relative names stay config-relative
watchUserPatches(ctx, options) Register the named patch file with the existing Cordis HMR service; each add/change/removal transactionally recomposes the full patch list through the caller's compose closure (app-owned layers around the current user layer) and returns an async disposer
resolveProfileDir / initProfile / loadProfile / readProfileManifest / writeProfileManifest / resolveBundleDir / composeEntries / healProfilesModuleFallback / PROFILE_TEMPLATES / DEFAULT_PROFILE_BUNDLES / PROFILES_DIR / PROFILE_PATCH_FILENAME Profile machinery (see Profiles)
boot(binName, absoluteConfigPath, patches?, prepare?, bareModuleBaseUrl?) Create the root context, expose dshHomePath(...segments) to Loader !!js config expressions, install Loader, run optional host preparation before config-tree entries mount (prepare may use Loader and provide launcher-owned context slots), then mount and await the include tree, assert entries loaded and activated, and return the root context — or dispose the partial context and reject a labelled error; the optional module base has the same resolution semantics as mountRootInclude
renderConfigDump(binName, absoluteConfigPath, layers, warn?) Compose the base config and labeled overlay layers offline with the include's own parser and patch algorithm (entryListSchema/applyEntryPatches), so the result equals what boot() mounts, and render YAML with !!js expressions verbatim; each run of rows that shares one source file and the same patch layers is preceded by a # == comment naming that file and those layers, keeping the output one loadable document; a patch matching no row goes to warn with its layer label (default: one stderr line), and read, parse, or field validation failures throw
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 the DSH implementation checkout while warning it not to infer the current working directory from that path and to use pwd instead; 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

Loader settlement rejects import and lifecycle failures with the failing entry and stage; boot() disposes the partial context and wraps that failure with the bin name. Entries settlement leaves behind are audited separately: assertEntriesLoaded turns an enabled fiber-less entry into a rejection naming every unresolved plugin, and assertEntriesActivated awaits each failed fiber to include its original stack in the startup rejection and names each pending entry's unresolved services. Before throwing, the audit marks those exact rejection reasons through one process checkpoint so installFailLoud coalesces Loader's duplicate notification while every unrelated unhandled rejection remains fatal.

The Loader mounts entries concurrently, so a surface can already own the terminal when something else fails: exiting without the tree's own teardown would leave raw mode, bracketed paste, and the keyboard protocol set on the user's shell, and an in-flight terminal query's reply would land as literal text at the next prompt. A config-tree failure settles through boot(), whose disposal of the partial context runs the surface's own shutdown before the labelled rejection. For the rejections boot() cannot see — a plugin's detached async work rejecting during or after mounting — a terminal-owning bin passes release to dispose the tree before the exit commits; dsh captures the root context in boot()'s prepare hook rather than from its return value so the hook covers the whole mounting window. While a release is in flight the handler stays installed and latched: the first rejection is the reported one, and later rejections (teardown's own included) are swallowed rather than becoming uncaught and killing the process mid-teardown.

cordis:group is registered beside cordis:include so a composition can give one isolate realm to a provider and its consumers together. Both load through the ambient module pipeline rather than the included tree's own specifier resolution, which is what lets a composition outside this workspace — an agent preset under the Harness home — use a group row at all.

Bare plugin specifiers in a config (@deepseek-ai/dsh-*, npm packages) resolve through the Cordis Loader's internal module loader. They resolve from the config directory by default; a closed runtime passes bareModuleBaseUrl to boot or mountRootInclude so its installed package tree remains authoritative even when the config lives inside another Node project. Relative specifiers always resolve against the config directory. 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. The built dsh-app-boot artifact embeds the statically mounted Include implementation while leaving Loader external, so the include tree and host bind to one Loader peer. The pnpm dsh source path additionally maps manifest-declared workspace packages to their TypeScript source; its configuration gate requires every shipped raw/Web bare plugin to appear in the resolver manifest's dependencies.

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.

Profiles

A profile is a directory under $DSH_HOME/profiles/<name> (the Harness home resolves through resolveDshHome: $DSH_HOME, else ~/.dsh) holding a package.json — out-of-tree plugin dependencies plus the profile manifest dsh.profile with its ordered bundles layer list — and the user's own cordis.patch.yml. A bundle is an npm package whose manifest declares "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }; loadProfile resolves each dsh.profile.bundles name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. composeEntries applies patch layers over an empty entry list through the include's own applyEntryPatches, so composition, flag derivation, and config dumps cannot drift from what boots. healProfilesModuleFallback maintains the flat $DSH_HOME/profiles/node_modules directory — one symlink per package the installation's app and bundles depend on — so bare plugin names in any profile resolve through Node's ordinary parent-walk without pnpm managing in-box packages. PROFILE_TEMPLATES (web, headless) auto-initialize on first use; other names fail loud until initProfile creates them (the dsh plugin path). loadProfile normalizes an exact installation-owned bundle tuple to its shipped template while preserving every other manifest field; any extra, missing, or reordered entry makes the list user-owned and leaves it unchanged.

User-level machine-local preferences also live in the Harness home:

  • .env — the product CLI's ordinary environment layers: the invoking directory's file outranks the Harness-home file, and both sit below the inherited environment. loadLayeredEnv snapshots each value's source, rejects bootstrap-only file variables case-insensitively, and materializes accepted values into process.env for Loader expressions and third-party libraries. Managed credentials live separately in .credentials.yaml; a credential left in either .env remains a lower-priority fallback.
  • cordis.patch.yml (home level) and profiles/<name>/cordis.patch.yml — the user patch layers, applied after every bundle layer (per-profile first, then the home-level file, which therefore outranks it): an id-targeted patch replaces the named entry's whole config (restate unchanged fields), insert adds entries, and !!js expressions interpolate at mount. A patch naming an entry id absent from the composed tree is a stderr warning. An empty or comments-only file throws (it parses to nothing, not to a list); disable the layer with [].

Every profile boot keeps cordis.patch.yml live through watchUserPatches (a one-shot surface disposes the watcher through its bounded shutdown). The watcher targets the exact path even when the file or immediate parent does not exist, serializes bursts, and recomposes the user patches inside the caller's layer order (bundle layers below, overlays above). A rejected read, parse, or Loader candidate leaves the last good tree running and the HMR service broadcasts hmr/config-update-failed(filename, Error) after logging it; observer failures are contained. Disposing the context closes the watcher and drains an active refresh.

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 discovery is launch-scopedloadLayeredEnv reads only the invocation directory and Harness home once; it does not search parents or follow a workspace selected later. loadEnv remains the one-directory helper for non-product bins.
  • A user patch replaces the whole matched config — an id-targeted patch does not deep-merge, so a profile override restates the bundle fields it keeps.