Преглед на файлове

feat(boot): add profile resolution modes

imccyu преди 5 дни
родител
ревизия
6aa2e4633c
променени са 63 файла, в които са добавени 2565 реда и са изтрити 199 реда
  1. 2 2
      .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml
  2. 2 2
      .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
  3. 2 2
      .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md
  7. 6 0
      .agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.i18n.yaml
  8. 115 0
      .agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md
  9. 115 0
      .agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md
  10. 2 2
      apps/cli/reference/README.i18n.yaml
  11. 1 1
      apps/cli/reference/README.md
  12. 1 1
      apps/cli/reference/README.zh.md
  13. 23 4
      apps/cli/src/profile-boot.ts
  14. 1 0
      apps/cli/tests/built-bin.e2e.ts
  15. 20 8
      apps/cli/tests/web-agent-presets.e2e.ts
  16. 22 15
      apps/web/tests/scaffold.ts
  17. 5 3
      apps/web/tests/shipped-composition.e2e.ts
  18. 2 2
      docs/config-catalog.i18n.yaml
  19. 2 2
      docs/config-catalog.md
  20. 2 2
      docs/config-catalog.zh.md
  21. 2 2
      packages/boot/app-boot/README.i18n.yaml
  22. 10 5
      packages/boot/app-boot/README.md
  23. 10 5
      packages/boot/app-boot/README.zh.md
  24. 6 0
      packages/boot/app-boot/package.json
  25. 9 0
      packages/boot/app-boot/src/index.ts
  26. 73 0
      packages/boot/app-boot/src/profile-resolution/legacy-links.ts
  27. 612 0
      packages/boot/app-boot/src/profile-resolution/resolver.ts
  28. 139 0
      packages/boot/app-boot/src/profile-resolution/service.ts
  29. 23 0
      packages/boot/app-boot/src/profile-resolution/worker-bootstrap.ts
  30. 148 56
      packages/boot/app-boot/src/profile.ts
  31. 187 0
      packages/boot/app-boot/tests/profile-resolution-service.spec.ts
  32. 70 0
      packages/boot/app-boot/tests/profile-resolution-worker-bootstrap.spec.ts
  33. 701 0
      packages/boot/app-boot/tests/profile-resolution.spec.ts
  34. 24 12
      packages/boot/app-boot/tsdown.config.ts
  35. 1 1
      packages/client/AGENTS.md
  36. 1 0
      packages/experimental/inspector/package.json
  37. 5 1
      packages/experimental/inspector/tsdown.config.ts
  38. 1 0
      packages/llm/plugin-package-inventory-deepseek/package.json
  39. 14 8
      packages/llm/plugin-package-inventory-deepseek/src/index.ts
  40. 18 2
      packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts
  41. 3 0
      packages/llm/plugin-package-inventory-deepseek/tsconfig.json
  42. 1 0
      packages/preset/agent-presets/package.json
  43. 24 24
      packages/preset/agent-presets/src/discovery.ts
  44. 9 2
      packages/preset/agent-presets/src/index.ts
  45. 23 0
      packages/preset/agent-presets/tests/mount.spec.ts
  46. 3 0
      packages/preset/agent-presets/tsconfig.json
  47. 2 2
      packages/test-support/loader-smoke/README.i18n.yaml
  48. 1 1
      packages/test-support/loader-smoke/README.md
  49. 1 1
      packages/test-support/loader-smoke/README.zh.md
  50. 10 7
      packages/test-support/session-snapshot/src/launcher.ts
  51. 3 3
      packages/test-support/session-snapshot/tests/harness.spec.ts
  52. 13 0
      packages/tsdown.worker.ts
  53. 1 0
      packages/typert/loader/package.json
  54. 30 10
      packages/typert/loader/src/index.ts
  55. 28 1
      packages/typert/loader/tests/loader.spec.ts
  56. 3 0
      packages/typert/loader/tsconfig.json
  57. 1 0
      packages/workflow/workflow-ptc/package.json
  58. 21 3
      pnpm-lock.yaml
  59. 1 0
      python/sdk-runtime/package.json
  60. 2 0
      scripts/check-workspace-constraints.ts
  61. 1 0
      scripts/gen-cordis-catalog.ts
  62. 2 2
      snapshots/sdk/sdk.snapshot.ts
  63. 1 1
      snapshots/session/headless.snapshot.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
-2026-08-05-profile-plugin-bundles.md: 7e51345e7eba8a58db63807e31d4a11481e3ffea
-2026-08-05-profile-plugin-bundles.zh.md: b2631603737ea9412eb97029ff01d751d8084cec
+2026-08-05-profile-plugin-bundles.md: 48786a9c1ccaceb5f16c9eefed01e556cf759e6d
+2026-08-05-profile-plugin-bundles.zh.md: c88cbf98e4276549a6fae6a5b1253d3d838a64ca

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md

@@ -14,7 +14,7 @@ Everything becomes a **profile**: a directory `$DSH_HOME/profiles/<name>` with a
 
 The default Profile templates use `@deepseek-ai/dsh-base` as the shared core for `web`, `headless`, `sdk`, and `acp`, with one mode bundle above it. The [standalone `sdk-minimal` profile](../../../../packages/bundle/sdk-minimal/README.md) instead lists one bundle that owns its complete explicit tree. Generic `dsh --profile <name>` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, headless owns its task positional, and the protocol profiles accept no app options. Patch overlays use launcher-owned `--patch`. A new, non-shipped target can use `--from-default-profile <template>` to copy one default template's bundle list and patch-reload policy before boot or config dump. This creates an independent profile with empty dependencies and an empty user patch: it neither reads a local profile named by the template nor records an inheritance relationship. The launcher claims the complete target directory exclusively, so existing state and concurrent creators fail without modification. `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes a base-backed profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](../../archived/architecture/2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract.
 
-Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them — while bare plugin names in patch rows resolve through the profile directory's Node parent-walk into the maintained flat fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch).
+Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory, so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them. Bare plugin names in patch rows use the [immutable profile resolution generation](2026-09-09-profile-resolution-generations.md), which applies the same installation-first and ordered-bundle rules in memory; retained link and dual modes can materialize the same result.
 
 Two supporting refactors: the webserver's built-in static dist serving became the single-owner **fallback seat** (`registerFallback`/`applyIndexTaps`), with the SPA server extracted to `@deepseek-ai/dsh-host-frontend-static` so the web bundle owns its dist as composition, not launcher code; and the personal-overlay machinery of the [dsh CLI personal-config decision](../../archived/feature/2026-07-20-dsh-cli-personal-config.md) (`loadPersonalPatches`, `$DSH_HOME/config.yaml`) was retargeted to the per-profile and home-level `cordis.patch.yml` layers (`loadOptionalPatches`, `watchUserPatches` taking a filename), superseding that note's entry modes and file location while keeping its Harness-home root, patch semantics, and fail-loud parsing.
 
@@ -31,5 +31,5 @@ Two supporting refactors: the webserver's built-in static dist serving became th
 - New composition surfaces (a TUI, provider packs) ship as ordinary npm packages installable per profile, without a repository row for every deployment shape.
 - Users can start an independent custom profile from any shipped application template without copying machine-local profile state.
 - `apps/cli` shrank to argv parsing, profile machinery consumption, and the pnpm forwarder; `AppCLIEntry` and the per-surface boot paths are gone.
-- The keyless web e2e scaffold boots the same bundle layers over the same empty-root shape as production, including the profiles module fallback, so composition drift between test and product fails loudly.
+- The keyless web e2e scaffold boots the same bundle layers over the same empty-root shape as production and exercises the same profile package-selection rules, so composition drift between test and product fails loudly.
 - Under the pre-release stance, backends carry no compatibility behavior for old on-disk configuration; `$DSH_HOME/config.yaml` is ignored.

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md

@@ -14,7 +14,7 @@ Status: implemented
 
 默认 Profile 模板为 `web`、`headless`、`sdk` 与 `acp` 使用 `@deepseek-ai/dsh-base` 作为共享核心,并在其上叠加一个模式组合包。[独立 `sdk-minimal` profile](../../../../packages/bundle/sdk-minimal/README.zh.md)则只列出一个拥有完整显式配置树的组合包。通用的 `dsh --profile <name>` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 持有任务位置参数,协议 profile 不接受应用选项。patch overlay 使用启动器持有的 `--patch`。新的非内置目标可以使用 `--from-default-profile <template>`,在启动或配置 dump 之前复制一个默认模板的 bundle 列表与 patch 重载策略。这会创建依赖为空、用户 patch 为空的独立 profile:它既不读取与模板同名的本地 profile,也不记录继承关系。launcher 会以独占方式领取完整的目标目录,因此既有状态和并发创建者都会在不作修改的情况下失败。`dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化一个以 base 为基础的 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](../../archived/architecture/2026-08-09-headless-direct-core-entry-point.md)负责 headless 组合约定。
 
-解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 `$DSH_HOME/profiles/node_modules`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)
+解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析,因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们。patch 行中的裸插件名称使用[不可变 profile resolution generation](2026-09-09-profile-resolution-generations.zh.md),在内存中应用相同的安装优先与有序 bundle 规则;保留的 link 与 dual 模式可以物化同一结果
 
 两项配套重构:webserver 内置的静态 dist 服务改为单一所有者的**回退席位**(`registerFallback`/`applyIndexTaps`),SPA 服务器提取到 `@deepseek-ai/dsh-host-frontend-static`,使 web 组合包以组合的方式持有自己的 dist,而不是靠启动器代码;[dsh CLI 个人配置决策](../../archived/feature/2026-07-20-dsh-cli-personal-config.md)的个人 overlay 机制(`loadPersonalPatches`、`$DSH_HOME/config.yaml`)改为面向逐 profile 与 home 级的 `cordis.patch.yml` 层(`loadOptionalPatches`、接受文件名的 `watchUserPatches`),取代该笔记的各入口模式与文件位置,同时保留其 Harness home 根目录、patch 语义与响亮失败的解析。
 
@@ -31,5 +31,5 @@ Status: implemented
 - 新的组合表层(TUI、提供方扩展包)以普通 npm 包形式交付,可按 profile 安装,无需在仓库中为每种部署形态各留一行。
 - 用户可以从任意随附应用模板启动一个独立的自定义 profile,而不会复制机器本地的 profile 状态。
 - `apps/cli` 收缩为 argv 解析、profile 机制的消费方和 pnpm 转发器;`AppCLIEntry` 与各表层专属的启动路径全部移除。
-- 无密钥 web e2e 脚手架以与生产相同的空根形态启动相同的组合包层,包括 profiles 模块回退,因此测试与产品之间的组合漂移会响亮失败。
+- 无密钥 web e2e 脚手架以与生产相同的空根形态启动相同的组合包层,并执行相同的 profile 包选择规则,因此测试与产品之间的组合漂移会响亮失败。
 - 按发布前姿态,后端不携带旧磁盘配置的兼容行为;`$DSH_HOME/config.yaml` 会被忽略。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md
-2026-08-18-experimental-agent-teams-packages.md: 2a00b651e0073434a5a68df13b9716adcab5fccf
-2026-08-18-experimental-agent-teams-packages.zh.md: df73ee5c05fd3a25bf843bee10b06535a1512783
+2026-08-18-experimental-agent-teams-packages.md: 4a78a60c2ad7463c06b670e6ec664579ffd767b9
+2026-08-18-experimental-agent-teams-packages.zh.md: 8056cd8194695e8ce41acb359e51c040d4ed3aec

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md

@@ -20,7 +20,7 @@ The generic caller-reserved continuable child identity and selective direct-chil
 
 The published Host-side Agent Teams profile bundle depends on the Team packages and applies after `dsh-base`. It inserts the Team rows and disables the global continuable-child controls whose model-visible names overlap the Team tools. The separate published Web profile applies after `dsh-web-app` and the Host profile; it inserts the Team UI, which mounts the Remote contribution generated by the Team package. Both layers remain opt-in and leave the shipped base, CLI, Web, and Python runtime dependency graphs unchanged.
 
-Profile installation resolves each published bundle and its dependencies through the profile's package manager. The generic profile launcher then applies the selected layers without adding them to any shipped profile or changing another profile's resolution.
+Profile startup resolves selected bundles before computing the [immutable profile resolution generation](2026-09-09-profile-resolution-generations.md). The generation retains installation-first precedence, traverses each explicit bundle root completely in profile order, and keeps pnpm-managed profile packages authoritative. Runtime mode enforces the result in memory; retained link and dual modes materialize the same result as shared and profile-owned projections. A private profile layer can therefore carry experimental plugin rows without adding those plugins to a release app, requiring profile users to install transitive packages directly, weakening packaged-runtime module identity, or changing another profile's resolution.
 
 Experimental status changes compatibility and support expectations, not publication for these five packages. They retain the repository's ordinary documentation, invariant, lifecycle, security, unit, real-composition, and snapshot requirements. Promotion still requires review of the public contracts, limitations, test evidence, runtime dependents, and a named owner accepting stable-package obligations.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md

@@ -20,7 +20,7 @@ dsh 打包与发布集合以及本地基线发布器包含这五个 Agent Teams
 
 公开发布的 Host 侧 Agent Teams profile bundle 依赖 Team 包,并在 `dsh-base` 之后应用。它会插入 Team 配置行,并禁用模型可见名称与 Team 工具重叠的全局 continuable-child control。独立公开发布的 Web profile 在 `dsh-web-app` 与 Host profile 之后应用;它会插入 Team UI,后者挂载 Team package 生成的 Remote contribution。两个层都保持显式启用,不改变随附 base、CLI、Web 与 Python runtime 的依赖图。
 
-profile 安装通过自身 package manager 解析每个公开 bundle 及其依赖。通用 profile launcher 随后应用所选层,不会把它们加入任何随附 profile,也不会改变其他 profile 的解析结果。
+profile 启动会先解析所选 bundle,再计算[不可变 profile resolution generation](2026-09-09-profile-resolution-generations.zh.md)。generation 保留安装优先顺序,按 profile 顺序完整遍历每个显式 bundle 根,并让 pnpm 管理的 profile 包保持优先。runtime 模式在内存中强制该结果;保留的 link 与 dual 模式把同一结果物化为共享和 profile 自有投影。因此,私有 profile 层可以携带实验性 plugin 配置行,而无需把这些 plugin 加入发布 app、要求 profile 用户直接安装传递依赖、破坏 packaged-runtime 的模块身份,或改变其他 profile 的解析结果。
 
 对这五个包而言,实验性状态改变兼容性与支持预期,而不阻止发布。这些包仍须满足仓库的一般文档、不变式、生命周期、安全、单元测试、真实组合测试和快照要求。promotion 前仍须评审公开约定、限制、测试证据、运行时依赖方,并由一名具名 owner 接受稳定包义务。
 

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md
+2026-09-09-profile-resolution-generations.md: 3785f62a77c8b5a7f467153a2fe5cc046c4f7965
+2026-09-09-profile-resolution-generations.zh.md: 3998eda50e4a05691af86a4f7e78a8ce86bc80ab

+ 115 - 0
.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md

@@ -0,0 +1,115 @@
+# Agent Note: Add immutable profile resolution generations
+
+Status: implemented
+
+English | [中文](2026-09-09-profile-resolution-generations.zh.md)
+
+## Problem
+
+A profile loads plugin rows from its own package project, while Harness packages and packages carried by selected bundles can live outside that project's ordinary dependency tree. The current launcher bridges the trees by calculating package precedence at startup and materializing that result as shared symlinks, profile-owned links, or packaged-executable proxy packages. The files persist across processes and installations, require reconciliation and locking, expose generated proxy manifests to metadata readers, and cannot represent a process-local change atomically.
+
+The runtime design preserves the existing selection rules rather than introducing a second package policy. It covers imports performed by plugin modules as well as Loader row imports, works in the main thread and Harness-owned Workers, and keeps hot per-resolution overhead within 15% of Node without hooks. Generation replacement accepts only additive package sets and never mutates a live table entry by entry.
+
+## Decision
+
+Profile startup computes one immutable `ResolutionGeneration` from the same dependency traversal that supplies the disk module fallback. The launcher defaults to link mode and preserves the existing materialized lookup behavior. Internal callers and tests can select runtime mode, which installs the generation into Node's ESM and CommonJS resolvers, or dual mode, which materializes and verifies the same generation. `PluginPackages.replace()` publishes a complete additive successor with one reference replacement.
+
+### One selection algorithm
+
+The package traversal remains in `@deepseek-ai/dsh-app-boot` beside profile loading. The disk materializer and the runtime resolver consume one pure plan; neither owns a copy of the precedence algorithm. Direct callers can select link, dual, or runtime mode, while an omitted mode selects link. Runtime and dual remain internal migration and verification paths rather than user-facing launcher behavior.
+
+The installation manifest is the first root. Its graph traverses `dependencies` followed by `peerDependencies` breadth-first, resolving each edge from the manifest that declares it. The first installed package reached under a name owns that name. Selected bundle roots then run in profile order, with each earlier root's complete graph taking precedence over every later root. Names supplied by the installation are reserved, and bundle package roots themselves do not become plugin fallbacks. Missing declared packages are skipped as before.
+
+Profile-local and plugin-private `node_modules` entries stay outside the fallback entries, and Node checks them before the virtual fallback position. The generation records only installed direct profile package names for a no-I/O native fast path. Each fallback entry records the package name, version, selected lookup directory, declaring manifest anchor, and scope needed to rerun Node's native resolution from the selected package and validate that a successor preserves existing mappings.
+
+The existing `healProfilesModuleFallback()` remains as the disk materializer for the same computed result, which permits direct comparison without rewriting the selection rules. The launcher uses it by default. Runtime mode computes the generation without materializing it, while dual mode materializes and installs that generation for comparison.
+
+### Immutable generations
+
+A resolver registration holds one `current` generation. Each synchronous resolution captures that reference once. Generation construction reads every required manifest before publication; an error leaves the current generation unchanged. Successful publication replaces one reference, and in-flight calls may finish against the generation they captured.
+
+Selection and package-metadata caches belong to a generation. Publishing a successor invalidates them by making the old generation unreachable after its callers finish; update code does not mutate or clear individual entries. Calls with explicit CommonJS paths or non-default conditions never reuse a default-resolution cache entry.
+
+The launcher constructs one startup generation. The service accepts an additive successor, but no package-manager transaction invokes replacement in this implementation.
+
+### Shared ESM and CommonJS rule
+
+The resolver uses `node-addon-require-builtin` to read `internal/modules/esm/loader` and `internal/modules/cjs/loader`. The ESM adapter wraps the per-thread singleton `CascadedLoader` resolve methods. The CommonJS adapter wraps the internal builtin's `Module._resolveFilename`; that `Module` is the same object exported by `node:module`.
+
+Both adapters call one routing function. It ignores builtins, relative or absolute paths, URLs, `#imports`, parents outside the profile scope, and explicit calls outside the supported default lookup. For a scoped bare request, it keeps the original parent when Node's lookup order contains a profile-local or plugin-private package before the virtual shared-fallback position. Otherwise it routes a generation hit through that entry's declaring anchor and leaves a miss at the original parent.
+
+The adapters call the captured native resolver after routing. Node remains responsible for exports, import and require conditions, main files, subpaths, extensions, native caches, and final errors. A selected package's invalid export or missing target does not trigger another same-name candidate. CommonJS does not replace `_findPath` or reproduce `_resolveFilename`.
+
+The guarantee covers Node's default `import`, `import()`, `import.meta.resolve`, `require`, and `require.resolve` after installation in that thread. It does not cover already linked modules, custom `vm` linkers, opaque non-Node importers, or third-party Workers.
+
+### Active plugin list and package metadata
+
+The resolution generation lists available fallback packages; Loader entries form the active plugin list. Consumers keep using Loader's existing entry lifecycle and filter the entries relevant to their own scope. Consumers that need package metadata pass a specifier and owning tree base URL to a lightweight `app-boot` service without requiring a `./package.json` export. An installed generation is authoritative, including a miss; a service created without a generation retains native lookup for low-level embedders.
+
+The resolver does not expose `imported(entry)` and does not observe ModuleJobs, wrap Entry methods, associate fibers with import calls, replace registry or tree methods, or adapt HMR transactions. A repeated query uses the same generation and therefore cannot drift from the route used for the import. Non-Node importers that need package metadata must explicitly implement the same deterministic resolver interface.
+
+The implementation lives under `app-boot/src/profile-resolution/`. `service.ts` provides the long-lived `ctx.pluginPackages` and owns the main-thread resolver and Worker-generation lifetimes; `resolver.ts` implements generation lookup and the Node Internal adapters; `worker-bootstrap.ts` installs an inherited generation in one thread. Existing profile selection and disk materialization remain in `profile.ts`. Workers reference the bootstrap only through the public `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap` export.
+
+### Workers and generation updates
+
+The main thread publishes a structured-clone representation of the current generation through Worker environment data. Each built Harness-owned Worker uses its build banner to obtain its own ESM and CommonJS internal objects and install the same adapters without traversing manifests. The bootstrap bundle has no static package imports; on Windows it temporarily exposes a service-private native cache directory and restores the environment before business code starts. Source Worker entries retain their existing self-contained dependencies. Third-party Workers remain unchanged.
+
+New Workers inherit the latest published generation. Existing Workers keep the generation they inherited, so a caller that publishes a successor must restart them. The ESM bootstrap cannot affect static dependencies linked before its execution, so Worker bundles keep pre-bootstrap static imports natively resolvable and start code needing the profile resolver through a later dynamic import.
+
+### Additive package changes
+
+A caller adding a package completes its pnpm transaction before constructing a successor generation. Replacement rejects any generation that changes the directory or version of an existing package. The caller publishes an additive successor before mounting the new Loader row; this implementation does not provide that package transaction. A mount failure may leave the package installed but inactive.
+
+Replacing, upgrading, or removing an already loaded package requires process restart because Node's ESM Module Map, CommonJS cache, existing object references, and running Workers can retain the old module identity. Generation replacement does not claim to unload modules.
+
+### Disk migration
+
+Runtime-only launch paths do not create, update, or retire symlinks and proxy packages. The resolver treats the legacy shared fallback and `.dsh-module-fallback` projections as virtual insertion positions: a generation hit uses the table target, while a miss skips those old positions before continuing native ancestor lookup. During a dual phase, the launcher materializes and installs the same generation; tests disable each backend in turn and compare their targets.
+
+Legacy disk state remains available to link-only launches, old processes, and rollback without participating in runtime-only selection. Removing that state is a separate maintenance operation outside this change.
+
+### Mode behavior
+
+Link, dual, and runtime modes use the same generation schema and dependency-selection policy. Link mode persists the computed result, runtime mode installs it only in the process, and dual mode requires Node's materialized result to equal the generation route.
+
+The `dsh` launcher selects link mode when its caller omits `resolutionMode`, so supported profile startup keeps its existing filesystem behavior. Tests and low-level embedders select runtime or dual explicitly before any profile row mounts.
+
+Runtime mode requires a supported Node Internal loader interface and does not create, update, or retire fallback links. Dual mode retains link writes and fails when Node's disk result differs from the generation. Writable profile state and package-manager transactions remain outside the resolver.
+
+Packaged-carrier selection, virtual-filesystem adaptation, and Electron ASAR launch behavior are separate decisions layered on this mode-neutral architecture.
+
+### Performance and verification
+
+Generation construction is startup or update work, not resolve work, and its absolute latency is reported separately. The hot path consists of scope classification, bare-name extraction, local-before-fallback selection, a Map lookup, and at most one native resolution; a cache hit returns the generation-owned result directly. Out-of-scope calls do not read manifests and cache only whether each parent belongs to the profile scope.
+
+Performance measurements run built JavaScript under plain Node in fresh processes and compare against a process that installs no hook. Seven alternating rounds cover outside, profile-local, and fallback imports through dynamic import, `import.meta.resolve`, require, and `require.resolve`. Across Node 22.19, 24.18, and 26.8, the largest positive hot-path median is 4.5%. On Node 24.18, a 256-package cold workload regresses by at most 11.2% and generation construction takes 16.027 ms median; the 32-package local `require.resolve` case adds 1.033 ms across the batch (+34.7%) from fixed startup cost.
+
+Behavior tests compare the runtime generation with the disk materializer over the same package trees, then exercise root order, transitive and peer dependencies, local and external precedence, exports and subpath errors, conditions, explicit CommonJS options, source and built Workers, and supported Node versions. Generation tests prove failed construction does not publish partial state and successful replacement is atomic.
+
+## Alternatives considered
+
+**Keep disk projections permanently.** This preserves native lookup without process hooks, but retains cross-process mutation, stale generations, proxy manifests, writer locks, and packaged-runtime divergence. A bounded dual migration remains useful because both backends consume the same generation.
+
+**Expand the dependency graph lazily during resolve.** This spreads manifest reads and errors across first-use calls, changes timing from the disk implementation, complicates Worker startup, and makes the hot path depend on graph size. Complete generation construction is easier to compare and replace atomically.
+
+**Use `module.registerHooks`.** The public API puts every relevant resolution through Node's global hook dispatch before profile scope can reject it. Direct access to the existing internal ESM and CommonJS resolver objects permits a smaller fast path while retaining Node as the final resolver.
+
+**Record each Entry's actual import through Loader and HMR adapters.** Actual import records support stateful resolvers that return different targets for identical inputs. This design instead makes the generation authoritative and deterministic, so those records duplicate the resolver's answer while adding Entry, fiber, registry, ModuleJob, and HMR lifecycle state.
+
+**Mutate one long-lived table after each package operation.** Incremental mutation exposes partial graphs and requires targeted cache invalidation. Building a complete successor makes failure atomic and keeps all caches generation-owned.
+
+**Hot-replace already loaded package versions.** A resolution-table swap cannot invalidate every live module instance or object reference. Restart preserves one package identity per process.
+
+## Verification
+
+- One eager computation supplies the retained disk materializer and runtime generation.
+- Link-only, dual, and runtime-only tests consume the same generation; runtime startup neither writes nor retires module-resolution data.
+- ESM and CommonJS adapters share one router and delegate final resolution to Node without `module.registerHooks` or `_findPath` replacement.
+- Production metadata lookup does not record Loader import results or wrap Entry, registry, tree, or HMR methods.
+- Main-thread and built owned-Worker tests cover supported Node versions.
+- Built plain-Node measurements record the hot and cold results above against no-hook Node.
+- Package READMEs, architecture references, generated catalogs, and the bilingual pair describe the shipped implementation.
+
+## Consequences
+
+Runtime startup avoids disk mutation and proxy manifests while preserving the existing package-selection algorithm. It accepts the maintenance cost of Node Internal compatibility tests and an early, self-contained bootstrap in each owned Worker. Link remains the launcher default, and dual keeps a migration comparison path. Generation replacement remains additive until the product owns module-cache invalidation and Worker restart. Carrier-specific selection and virtual-filesystem launch integration remain separate work.

+ 115 - 0
.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md

@@ -0,0 +1,115 @@
+# Agent Note: 增加不可变 profile 解析代际
+
+Status: implemented
+
+[English](2026-09-09-profile-resolution-generations.md) | 中文
+
+## Problem
+
+profile 从自己的包项目加载插件配置项,而 Harness 包和所选 bundle 携带的包可能位于该项目普通依赖树之外。当前启动器在启动时计算包优先级,再将结果物化为共享 symlink、profile 自有链接或打包可执行文件的代理包。文件跨进程和安装版本持续存在,需要协调和锁来维护,并向元数据读取方暴露生成的代理 manifest,也无法原子表示进程内变更。
+
+运行时设计保留现有选包规则,不另建一套包策略。它覆盖插件模块内部的 import 以及 Loader 配置项的 import,在主线程和 Harness 自有 Worker 中工作,并将热 resolve 相对无 hook Node 的开销控制在 15% 以内。generation 替换只接受新增包的集合,不会逐项修改正在使用的表。
+
+## Decision
+
+profile 启动从磁盘 module fallback 使用的同一套依赖遍历生成一个不可变 `ResolutionGeneration`。launcher 默认使用 link 模式,保留现有的物化查找行为。内部调用方和测试可以选择 runtime 模式,把 generation 安装到 Node 的 ESM 与 CommonJS resolver;也可以选择 dual 模式,同时物化并校验同一份 generation。`PluginPackages.replace()` 通过一次引用替换发布完整的新增型后继 generation。
+
+### 唯一选包算法
+
+包遍历继续放在 `@deepseek-ai/dsh-app-boot` 的 profile 加载代码旁。磁盘 materializer 和运行时解析器消费同一个纯计划;两者都不持有另一份优先级算法。直接调用方可以选择 link、dual 或 runtime 模式,省略模式时使用 link。runtime 与 dual 仍是内部迁移和验证路径,不改变面向用户的 launcher 行为。
+
+安装 manifest 是第一个根。它按 BFS 依次遍历 `dependencies` 和 `peerDependencies`,每条边从声明它的 manifest 解析,同名包由第一次找到的已安装包占有。所选 bundle 随后按 profile 顺序逐根遍历;每个较早根的完整依赖图优先于所有较晚根。安装闭包中的名称被保留,bundle 包根本身不成为插件 fallback。与旧行为相同,已声明但未安装的包会被跳过。
+
+profile 本地和插件私有 `node_modules` 不进入 fallback entries,由 Node 在虚拟 fallback 位置之前选择。generation 只记录已安装的 profile 直接包名用于 native 快速分流;每个 fallback 记录包名、版本、旧规则选中的查找目录、声明该边的 manifest 锚点和作用域,足以从选定包重新进入 Node 原生解析并验证换代保持既有映射。
+
+旧 `healProfilesModuleFallback()` 保留为相同纯计算结果的磁盘 materializer,便于直接比较并避免重写旧规则。launcher 默认调用它。runtime 模式只计算 generation 而不物化,dual 模式会物化并安装该 generation 进行比较。
+
+### 不可变 generation
+
+一个解析器 registration 持有一个 `current` generation。每个同步 resolve 在入口只捕获一次该引用,完整调用只读该引用。generation 构造在发布前读取所有必需 manifest;失败时当前 generation 不变。发布成功只替换一个引用,执行中的调用可以继续使用它已捕获的 generation。
+
+选包缓存和包元数据缓存归 generation 所有。发布下一代后,旧 generation 在调用方退出后自然不可达,不逐项清理缓存。显式 CommonJS paths 或非默认 conditions 不得复用默认解析缓存。
+
+launcher 只构造启动 generation。服务接受新增型后继 generation,但本实现没有包管理器事务调用替换操作。
+
+### ESM 与 CommonJS 共用规则
+
+resolver 使用 `node-addon-require-builtin` 读取 `internal/modules/esm/loader` 和 `internal/modules/cjs/loader`。ESM 适配器包装每线程单例 `CascadedLoader` 的 resolve 方法。CommonJS 适配器包装内部 builtin 导出的 `Module._resolveFilename`;该 `Module` 与 `node:module` 导出的对象相同。
+
+两个适配器调用同一个路由函数。builtin、相对或绝对路径、URL、`#imports`、profile 作用域外 parent 和支持的默认查找以外的显式调用都直接委托原生实现。对于作用域内的 bare request,如果 Node 查找顺序在虚拟共享 fallback 之前存在 profile 本地包或插件私有包,则保留原 parent;否则 generation 命中时返回该条目的声明锚点作为 routed parent,未命中时保留原 parent。
+
+适配器完成路由后调用捕获的原生 resolver。exports、import/require conditions、main、subpath、扩展名、原生缓存和最终错误仍归 Node 处理。选中包的无效 export 或缺失目标不会触发另一个同名候选。CommonJS 不替换 `_findPath`,也不复制 `_resolveFilename`。
+
+保证范围是当前线程安装后发生的 Node 默认 `import`、`import()`、`import.meta.resolve`、`require` 和 `require.resolve`。已经链接的模块、自定义 `vm` linker、不透明的非 Node importer 和第三方 Worker 不在透明保证范围。
+
+### 活动插件列表与包元数据
+
+resolution generation 列出可用 fallback 包;Loader entries 组成活动插件列表,两者不能合并。消费方继续使用 Loader 原有 entry 生命周期,并按自身 scope 过滤相关 entries。需要 package metadata 的消费方将 specifier 和所属树的 base URL 交给 app-boot 中的轻量服务,无需 package 导出 `./package.json`。安装 generation 后,即使查询未命中也以 generation 为准;底层嵌入方只安装服务而不提供 generation 时,服务保留 Node 原生查找。
+
+解析器不提供 `imported(entry)`,不观察 ModuleJob,不包装 Entry 方法,不把 fiber 与 import 调用关联,也不替换 registry、tree 或 HMR 方法。重复查询读取同一个 generation,因此不会偏离 import 使用的路线。需要包元数据的非 Node importer 必须显式实现同一个确定性 resolver 接口,不能把调用来源推断重新引入 Node 主路径。
+
+实现集中在 `app-boot/src/profile-resolution/`。`service.ts` 提供长期存在的 `ctx.pluginPackages`,并拥有主线程 resolver 与 Worker generation 的生命周期;`resolver.ts` 实现 generation 查询和 Node Internal 适配器;`worker-bootstrap.ts` 在线程内安装继承的 generation。旧 profile 选包和磁盘 materialize 逻辑留在 `profile.ts`。Worker 只通过 `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap` 公开入口引用 bootstrap。
+
+### Worker 与 generation 更新
+
+主线程通过 Worker environment data 发布当前 generation 的可结构化克隆表示和 profile scope。每个 Harness 自有 Worker 构建产物通过构建 banner 获取自己的 ESM/CJS Internal 并安装同一适配器,不重新遍历 manifest。bootstrap bundle 不静态导入任何包;在 Windows 上,它会临时暴露一个服务私有的 native cache 目录,并在业务代码启动前恢复环境。源码 Worker 入口保持原有自包含依赖;第三方 Worker 保持不变。
+
+新 Worker 继承最新发布的 generation。已运行的 Worker 保留启动时继承的 generation,因此发布后继 generation 的调用方必须重启它们。ESM bootstrap 无法影响其执行前已链接的静态依赖,因此 Worker bundle 必须保证 bootstrap 之前的静态 import 可由原生 Node 解析,需要 profile resolver 的业务入口在 bootstrap 后通过 dynamic import 启动。
+
+### 只增加包的变更
+
+添加包的调用方先完成 pnpm 事务,再构造下一代。替换操作会拒绝改变任何既有 package name 的目录或版本。调用方先发布只增加映射的后继 generation,再挂载新的 Loader 配置项;本实现不提供该包事务。挂载失败可以留下已安装但未启用的包。
+
+替换、升级或删除已加载包需要重启,因为 Node 的 ESM Module Map、CommonJS cache、现存对象引用和运行中的 Worker 都可能保留旧模块 identity。generation 换代不声称卸载模块。
+
+### 磁盘迁移
+
+runtime-only 启动流程不创建、更新或退休 symlink 和代理包。resolver 把旧共享 fallback 和 `.dsh-module-fallback` 投影视为虚拟插入位置:generation 命中时使用表中目标,未命中时越过旧位置继续原生祖先查找。dual 阶段物化并安装同一个 generation;测试分别禁用一个后端并比较目标。
+
+旧磁盘状态继续供 link-only 启动、旧进程和回滚使用,但不参与 runtime-only 选择。清理旧链接是本次变更之外的独立维护操作。
+
+### 模式行为
+
+link、dual 与 runtime 模式使用同一种 generation schema 和依赖选择策略。link 模式持久化计算结果,runtime 模式只在进程内安装,dual 模式要求 Node 的磁盘结果与 generation 路由一致。
+
+`dsh` launcher 在调用方省略 `resolutionMode` 时选择 link 模式,因此受支持的 profile 启动保留现有文件系统行为。测试与底层嵌入方会在挂载任何 profile 条目前显式选择 runtime 或 dual。
+
+runtime 模式要求受支持的 Node Internal loader 接口,并且不会创建、更新或退休 fallback 链接。dual 模式保留链接写入,并在 Node 的磁盘结果与 generation 不同时失败。可写 profile 状态和包管理器事务不属于 resolver。
+
+打包载体选择、虚拟文件系统适配和 Electron ASAR 启动行为属于叠加在这套模式无关架构上的独立决策。
+
+### 性能与验证
+
+generation 构造发生在启动或显式更新阶段,不属于单次 resolve,但需要单独报告绝对延迟。热路径只包括 scope 分类、bare name 提取、本地优先判断、Map 查询和至多一次原生解析;缓存命中直接返回 generation 级结果。作用域外调用不读取 manifest,只缓存 parent 是否位于 profile scope。
+
+性能测量用 plain Node 在全新进程中执行构建后的 JavaScript,并以完全没有安装 hook 的进程为基线。七轮交替顺序覆盖 outside、profile-local 和 fallback 的 dynamic import、`import.meta.resolve`、require、`require.resolve`。Node 22.19、24.18 和 26.8 的热路径中位数最大正向回退为 4.5%。Node 24.18 的 256 包 cold workload 最大回退为 11.2%,generation 构造中位数为 16.027 ms;32 包本地 `require.resolve` 因固定启动成本在整批增加 1.033 ms(+34.7%)。
+
+行为测试在同一包树上比较运行时 generation 与磁盘 materializer,再覆盖根顺序、传递依赖和 peer、本地与外层优先级、exports 与 subpath 错误、conditions、显式 CommonJS options、源码和构建 Worker及支持的 Node 版本。generation 测试证明构造失败不发布部分状态,成功换代只做原子引用替换。
+
+## Alternatives considered
+
+**永久保留磁盘投影。** 这能在没有进程 hook 时沿用原生查找,但仍有跨进程写入、陈旧 generation、代理 manifest、写锁和打包运行时差异。迁移期 dual 模式仍有价值,因为两个后端消费同一个 generation。
+
+**在 resolve 时惰性扩展依赖图。** 这会把 manifest 读取和错误分散到首次使用,改变磁盘实现的时机,使 Worker 启动更复杂,并让热路径成本随依赖图变化。完整构造 generation 更容易比较和原子替换。
+
+**使用 `module.registerHooks`。** 公共 API 会在 profile scope 拒绝请求之前让相关解析进入 Node 的全局 hook 分发。直接访问已有 ESM/CJS 内部解析器可以保留更小的快速路径,并继续让 Node 完成最终解析。
+
+**通过 Loader 和 HMR 适配器记录每个 Entry 的实际 import。** 实际 import 记录能支持相同输入返回不同目标的有状态 resolver。本设计改为以 generation 作为确定性权威,因此这些记录只会复制 resolver 的答案,同时增加 Entry、fiber、registry、ModuleJob 和 HMR 生命周期状态。
+
+**每次包操作增量修改一张长期表。** 增量修改会暴露半成品依赖图,并要求定点失效缓存。完整构造下一代使失败保持原子,并让所有缓存随 generation 生命周期存在。
+
+**热替换已经加载的包版本。** 解析表换代无法使所有存活模块实例和对象引用失效。重启可以保证每个进程只使用一个 package identity。
+
+## Verification
+
+- 一次 eager 计算同时供应保留的磁盘 materializer 和运行时 generation。
+- link-only、dual 和 runtime-only 测试消费同一个 generation;runtime 启动既不写入也不退休模块解析数据。
+- ESM 与 CommonJS 适配器共享同一个路由器,并把最终解析委托给 Node,不使用 `module.registerHooks` 或替换 `_findPath`。
+- 生产 package metadata 查询不记录 Loader import 结果,也不包装 Entry、registry、tree 或 HMR 方法。
+- 主线程和构建后的自有 Worker 测试覆盖受支持的 Node 版本。
+- plain Node 构建产物测量记录上述相对无 hook Node 的热路径和 cold 结果。
+- package README、架构引用、生成目录和双语文档对描述已交付实现。
+
+## Consequences
+
+runtime 启动避免磁盘修改和代理 manifest,同时保留既有选包算法。代价是持续维护 Node Internal 兼容测试,并在每个自有 Worker 中最早执行自包含 bootstrap。link 保持 launcher 默认值,dual 保留迁移比较路径。在产品拥有模块缓存失效和 Worker 重启前,generation 替换只能新增映射。载体专用选择和虚拟文件系统启动集成仍属于独立工作。

+ 2 - 2
apps/cli/reference/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/cli/reference/README.md
-README.md: ce8dcc40297cb8ae7eff4f90ada61f97588677d4
-README.zh.md: 32bc15a3be5cd28866c4db0dbe593a4b1e119101
+README.md: b2931e05c1c10c6de13427b2cdaf38a0e78db904
+README.zh.md: 2a9014a5d338c3d81d9976d8cb47474a95c45cb5

+ 1 - 1
apps/cli/reference/README.md

@@ -8,7 +8,7 @@ This reference defines the profile, web-alias, plugin-management, and config-dum
 
 `dsh --profile <name>` boots the profile at `$DSH_HOME/profiles/<name>`. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's `dsh.profile.bundles` list, the profile's own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml` (machine-local preferences shared by every profile, so it outranks the per-profile layer), and each `--patch <path>` overlay in argv order. Later layers win per row; a patch replaces the targeted row's complete `config` value rather than deep-merging keys, and may insert new rows. `dsh.profile.patchReload` selects `live` patch-file watching or `startup` one-time loading; omission defaults a custom profile to `live`. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit.
 
-Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-sdk-minimal`, `@deepseek-ai/dsh-acp-app`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules`. Plain Node installations place one healed symlink there per dependency-closure package. A pkg executable instead places a real ESM proxy that mirrors explicit exports and re-exports the virtual package URL, because operating-system symlinks cannot enter pkg's `/snapshot` filesystem. Every launch also links packages carried only by selected external bundles through a dsh-owned directory into the current profile's `node_modules`; existing pnpm entries win, and each profile owns its links independently.
+Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-sdk-minimal`, `@deepseek-ai/dsh-acp-app`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. Before mounting rows, the launcher traverses the installation and selected bundles in that order and materializes the resulting fallback links. The internal runtime and dual modes consume the same immutable generation in tests without changing the CLI's link-mode behavior. Profile-installed packages keep native priority in every mode.
 
 The `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` profiles auto-initialize from shipped templates on first use (`web`: base + web-app with live patches; `headless`: base + headless with startup-only patches; `sdk`: base + sdk-app with startup-only patches; `sdk-minimal`: its standalone bundle with startup-only patches; `acp`: base + acp-app with startup-only patches). Any other missing profile fails loudly with a hint to run `dsh plugin --profile <name> add <package>`.
 

+ 1 - 1
apps/cli/reference/README.zh.md

@@ -10,7 +10,7 @@
 
 `dsh --profile <name>` 启动位于 `$DSH_HOME/profiles/<name>` 的 profile。生效配置树以空根节点为起点,依次叠加 profile manifest(元数据清单)的 `dsh.profile.bundles` 列表中指定的各组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(这是各 profile 共享的机器本地偏好,因此优先于逐 profile 配置层),以及按 argv 顺序指定的各个 `--patch <path>` 覆盖层。对同一配置行,后应用的层优先。patch 会替换目标行的整个 `config` 值,而不是深度合并其中的键;patch 也可以插入新行。`dsh.profile.patchReload` 可选择 `live` patch 文件监视或 `startup` 单次加载;自定义 profile 省略该值时默认使用 `live`。配置解析、schema 校验、模块解析或插件启动失败时,系统会报告错误并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。
 
-组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-sdk-minimal`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。普通 Node 安装会为依赖闭包中的每个包放置并修复一个符号链接。pkg 可执行程序则放置真实 ESM 代理,镜像显式 exports 并重新导出虚拟包 URL,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统。每次启动还会把仅由所选外部组合包携带的包经 dsh 自有目录链接到当前 profile 的 `node_modules`;已有 pnpm 条目优先,且每个 profile 独立拥有自己的链接
+组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-sdk-minimal`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包来自 profile 中由 pnpm 管理的 `node_modules`。挂载配置行前,launcher 会按此顺序遍历安装与所选 bundle,并物化计算出的 fallback 链接。内部 runtime 与 dual 模式会在测试中消费同一份不可变 generation,但不改变 CLI 的 link 模式行为。所有模式都保留 profile 已安装包的原生优先级
 
 `web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app,实时应用 patch;`headless`:base + headless,只在启动时应用 patch;`sdk`:base + sdk-app,只在启动时应用 patch;`sdk-minimal`:独立组合包,只在启动时应用 patch;`acp`:base + acp-app,只在启动时应用 patch)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile <name> add <package>`。
 

+ 23 - 4
apps/cli/src/profile-boot.ts

@@ -20,17 +20,21 @@ import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
 import {
   boot,
   composeEntries,
+  createProfileResolutionGeneration,
   healProfilesModuleFallback,
   initProfile,
   installFailLoud,
   loadOptionalPatches,
   loadOverlayPatches,
   loadProfile,
+  PluginPackages,
   PROFILE_PATCH_FILENAME,
   PROFILE_TEMPLATES,
   resolveProfileDir,
   watchUserPatches,
   type Profile,
+  type ProfileResolutionGeneration,
+  type ProfileResolutionMode,
 } from '@deepseek-ai/dsh-app-boot'
 import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
 import { installProxyFromEnvironment } from '@deepseek-ai/dsh-http-proxy'
@@ -194,6 +198,8 @@ export function prepareProfile(name: string, userLayer = true, fromDefaultProfil
 /** One profile's patch layers, in application order. */
 interface ComposedProfile {
   profile: Profile
+  /** Immutable package fallback selected before any plugin imports. */
+  resolution: ProfileResolutionGeneration
   /** Bundle layers concatenated — the part below the user layers on a live reload. */
   bundlePatches: PatchOptions[]
   /** The home-level user layer (`$DSH_HOME/cordis.patch.yml`), applied after the profile's own. */
@@ -226,10 +232,14 @@ function allPatches(composed: ComposedProfile): PatchOptions[] {
 async function composeProfile(
   name: string,
   patchFiles: readonly string[],
+  resolutionMode: ProfileResolutionMode,
   fromDefaultProfile?: string,
 ): Promise<ComposedProfile> {
   const profile = prepareProfile(name, true, fromDefaultProfile)
-  await healProfilesModuleFallback({ installAnchor: INSTALL_ANCHOR, profile })
+  const resolutionOptions = { installAnchor: INSTALL_ANCHOR, profile }
+  const resolution = resolutionMode === 'runtime'
+    ? await createProfileResolutionGeneration(resolutionOptions)
+    : await healProfilesModuleFallback(resolutionOptions)
   const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
   const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
   const bundlePatches = profile.layers.flatMap(layer => layer.patches)
@@ -240,7 +250,7 @@ async function composeProfile(
   const composedOverlays = [...overlays]
   const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
   if (telemetryPatch !== undefined) composedOverlays.push(telemetryPatch)
-  return { profile, bundlePatches, homePatches, overlays: composedOverlays }
+  return { profile, resolution, bundlePatches, homePatches, overlays: composedOverlays }
 }
 
 /** Options for {@link runProfile}. */
@@ -255,6 +265,8 @@ export interface RunProfileOptions {
   patchFiles: readonly string[]
   /** The invocation's inner arguments, handed to the tree through `ctx.cmdlineArgs`. */
   args: readonly string[]
+  /** Module fallback backend; defaults to retained link materialization. */
+  resolutionMode?: ProfileResolutionMode
 }
 
 /**
@@ -289,7 +301,10 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
     (message) => { process.stderr.write(`${NAME}: ${message}\n`) },
   )
 
-  const composed = await composeProfile(options.profile, options.patchFiles, options.fromDefaultProfile)
+  const resolutionMode = options.resolutionMode ?? 'link'
+  const composed = await composeProfile(
+    options.profile, options.patchFiles, resolutionMode, options.fromDefaultProfile,
+  )
   const app: { current?: Context } = {}
   const appReady = createAppReady()
   const shutdown = createProcessShutdown(async () => {
@@ -333,11 +348,15 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
   ])
   // Cloned for the same insert-aliasing reason as composeLive: the boot
   // application must not mutate the objects later reloads recompose from.
-  const ctx = await boot(NAME, rootConfig, structuredClone(allPatches(composed)), (hostCtx) => {
+  const ctx = await boot(NAME, rootConfig, structuredClone(allPatches(composed)), async (hostCtx) => {
     app.current = hostCtx
     // Before any config-tree entry mounts, so plugins resolve all launch-time
     // environment values from the same immutable launch snapshot.
     hostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, options.environment)
+    await hostCtx.plugin(PluginPackages, resolutionMode === 'link' ? {} : {
+      generation: composed.resolution,
+      behavior: resolutionMode === 'dual' ? 'verify' : 'enforce',
+    })
     // The command line and bounded exit request are launcher facts available
     // to every app plugin that injects the argument snapshot.
     provideCmdline(hostCtx, {

+ 1 - 0
apps/cli/tests/built-bin.e2e.ts

@@ -868,6 +868,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
     try {
       await waitForFile(fixture.ready)
       expect(readFileSync(fixture.echo, 'utf8')).toBe('bundle-default')
+      expect(existsSync(join(fixture.home, 'profiles', 'node_modules'))).toBe(true)
       requestProfileShutdown(child, fixture)
       expect((await child).exitCode).toBe(0)
     } finally {

+ 20 - 8
apps/cli/tests/web-agent-presets.e2e.ts

@@ -4,7 +4,14 @@ import { tmpdir } from 'node:os'
 import { fileURLToPath } from 'node:url'
 import { dirname, join } from 'node:path'
 import { Context } from '@deepseek-ai/cordis'
-import { boot, healProfilesModuleFallback, loadOverlayPatches, loadProfile } from '@deepseek-ai/dsh-app-boot'
+import {
+  boot,
+  createProfileResolutionGeneration,
+  loadOverlayPatches,
+  loadProfile,
+  PluginPackages,
+  type Profile,
+} from '@deepseek-ai/dsh-app-boot'
 import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
 import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
 import type { Agent } from '@deepseek-ai/dsh-agent'
@@ -112,12 +119,7 @@ async function bootWeb(
     { id: 'agent-presets', config: { default: 'standard', includeUserRoot: false } },
     ...extra,
   ]
-  // The surface is patch layers over an empty preset root, so the root sits
-  // outside this workspace and bare plugin names cannot resolve by Node's
-  // upward walk. The flat fallback the preset boot maintains is what makes
-  // them resolvable — the same mechanism, not a test-only shim.
   const home = dirname(settingsFile)
-  await healProfilesModuleFallback({ installAnchor: INSTALL_ANCHOR, home })
   const profileDir = join(home, 'profiles', 'spec')
   await mkdir(profileDir, { recursive: true })
   // Product Bundles are installed into the Profile, not the dsh app. Model
@@ -130,6 +132,14 @@ async function bootWeb(
     await mkdir(dirname(link), { recursive: true })
     await symlink(packageDir, link, 'junction')
   }
+  let profile: Profile = {
+    name: 'spec',
+    dir: profileDir,
+    layers: [],
+    patchPath: join(profileDir, 'cordis.patch.yml'),
+    patches: [],
+    patchReload: 'startup',
+  }
   let bundlePatches: PatchOptions[] = [
     ...loadOverlayPatches('dsh-test', BASE_PATCH),
     ...loadOverlayPatches('dsh-test', WEB_PATCH),
@@ -140,12 +150,14 @@ async function bootWeb(
       dependencies: Object.fromEntries(profileBundles.map(name => [name, 'workspace:*'])),
       dsh: { profile: { bundles: profileBundles } },
     }, null, 2) + '\n')
-    const profile = loadProfile('dsh-test', 'spec', INSTALL_ANCHOR, home, { userLayer: false })
+    profile = loadProfile('dsh-test', 'spec', INSTALL_ANCHOR, home, { userLayer: false })
     bundlePatches = profile.layers.flatMap(layer => layer.patches)
   }
+  const resolution = await createProfileResolutionGeneration({ installAnchor: INSTALL_ANCHOR, home, profile })
   const rootConfig = join(profileDir, 'cordis.yml')
   await writeFile(rootConfig, '[]\n')
-  return await boot('dsh-test', rootConfig, [...bundlePatches, ...overrides], (bootCtx) => {
+  return await boot('dsh-test', rootConfig, [...bundlePatches, ...overrides], async (bootCtx) => {
+    await bootCtx.plugin(PluginPackages, { generation: resolution })
     bootCtx.provide('connection', {
       fetch: { register: () => () => {} },
       rpc: { intercept: () => () => {} },

+ 22 - 15
apps/web/tests/scaffold.ts

@@ -59,9 +59,12 @@ import {
 import {
   auditStartupEntries,
   composeEntries,
+  createProfileResolutionGeneration,
   healProfilesModuleFallback,
   loadOverlayPatches,
+  PluginPackages,
   type Profile,
+  type ProfileResolutionMode,
 } from '@deepseek-ai/dsh-app-boot'
 import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
 import { LlmAdapter } from '@deepseek-ai/dsh-llm'
@@ -288,6 +291,8 @@ export interface WebScaffold {
 
 /** Options for {@link launchWebScaffold}. */
 export interface LaunchOptions {
+  /** Profile resolver backend used by this test Host; defaults to runtime coverage. */
+  profileResolutionMode?: Extract<ProfileResolutionMode, 'dual' | 'runtime'>
   /** Enable the real Open In rows with deterministic launch-environment facts. */
   openInAppEnvironment?: LaunchEnvironmentSnapshot
   /** Compare the replayed root session with `replayFixture`; defaults on for a manifest-owned canonical recording. */
@@ -684,21 +689,19 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
         patches: [],
       }
     }))
-    // Mirror the production launcher: the shared installation closure keeps
-    // its carrier-specific fallback, while private bundle dependencies stay
-    // isolated to this synthetic scaffold profile.
-    await healProfilesModuleFallback({
-      installAnchor: INSTALL_ANCHOR,
-      home: harnessHome,
-      profile: {
-        name: 'scaffold',
-        dir: profileDir,
-        layers: extraLayers,
-        patchPath: join(profileDir, 'cordis.patch.yml'),
-        patches: [],
-        patchReload: 'startup',
-      },
-    })
+    const profile: Profile = {
+      name: 'scaffold',
+      dir: profileDir,
+      layers: extraLayers,
+      patchPath: join(profileDir, 'cordis.patch.yml'),
+      patches: [],
+      patchReload: 'startup',
+    }
+    const profileResolutionMode = options.profileResolutionMode ?? 'runtime'
+    const resolutionOptions = { installAnchor: INSTALL_ANCHOR, home: harnessHome, profile }
+    const resolution = profileResolutionMode === 'runtime'
+      ? await createProfileResolutionGeneration(resolutionOptions)
+      : await healProfilesModuleFallback(resolutionOptions)
     await mkdir(profileDir, { recursive: true })
     const rootConfig = join(profileDir, 'cordis.yml')
     await writeFile(rootConfig, '[]\n')
@@ -715,6 +718,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
         throw new Error(`web e2e scaffold: the web app requested exit ${String(code)} with no arguments to reject`)
       },
     })
+    await ctx.plugin(PluginPackages, {
+      generation: resolution,
+      behavior: profileResolutionMode === 'dual' ? 'verify' : 'enforce',
+    })
     await ctx.plugin(Loader)
     ctx.loader.builtins.include = Include
     // `cordis:group` beside it, exactly as `boot()` registers it: a group row is

+ 5 - 3
apps/web/tests/shipped-composition.e2e.ts

@@ -2,7 +2,7 @@
 // and asserts its catalog, defaults, Loader lifecycle, and one complete Auto
 // producer-to-tool path. Browser scenarios in this lane own visual behavior.
 import { randomUUID } from 'node:crypto'
-import { readFileSync } from 'node:fs'
+import { existsSync, readFileSync } from 'node:fs'
 import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
 import { tmpdir } from 'node:os'
 import { join } from 'node:path'
@@ -513,6 +513,7 @@ afterEach(async () => {
 
 it('assembles the shipped Web transport, catalog, guidance, and defaults', async () => {
   scaffold = await launchWebScaffold({ deepSeekMissingCredential: true })
+  expect(existsSync(join(scaffold.harnessHome, 'profiles', 'node_modules'))).toBe(false)
   const ctx = scaffold.ctx
   expect(ctx.llm.listProviders().some(provider => provider.id === 'deepseek-messages')).toBe(false)
   expect(ctx.agentDefaultModel.currentSelection()).toEqual({ provider: 'deepseek-official', model: 'deepseek-flash' })
@@ -640,8 +641,9 @@ it('assembles the shipped Web transport, catalog, guidance, and defaults', async
   }
 }, 120_000)
 
-it('ships PTC with run_code but without the general workflow SDK binding', async () => {
-  scaffold = await launchWebScaffold({ deepSeekMissingCredential: true })
+it('ships PTC with run_code but without the general workflow SDK binding under dual resolution', async () => {
+  scaffold = await launchWebScaffold({ deepSeekMissingCredential: true, profileResolutionMode: 'dual' })
+  expect(existsSync(join(scaffold.harnessHome, 'profiles', 'node_modules'))).toBe(true)
   const ctx = scaffold.ctx
   const handle = await ctx.agents.create({
     sessionId: SessionId('shipped-ptc-composition'),

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: 351aa11257708e4f7af06d621d38a3d7088ee857
-config-catalog.zh.md: 6da38435c9da889a0e4166eb95f6b04f0bb68ef0
+config-catalog.md: ca95f08dc7dd405422edeac13d716f2842d4388f
+config-catalog.zh.md: 91dc6b4e5d47776672f5f89a500460b7ef325b8a

+ 2 - 2
docs/config-catalog.md

@@ -1789,7 +1789,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/llm/plugin-package-inventory-deepseek/src/index.ts:31`](../packages/llm/plugin-package-inventory-deepseek/src/index.ts)
+Source: [`packages/llm/plugin-package-inventory-deepseek/src/index.ts:32`](../packages/llm/plugin-package-inventory-deepseek/src/index.ts)
 
 <a id="deepseek-aidsh-ptc-runtime-node"></a>
 
@@ -3356,7 +3356,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/typert/loader/src/index.ts:47`](../packages/typert/loader/src/index.ts)
+Source: [`packages/typert/loader/src/index.ts:48`](../packages/typert/loader/src/index.ts)
 
 <a id="deepseek-aidsh-user-approval"></a>
 

+ 2 - 2
docs/config-catalog.zh.md

@@ -1791,7 +1791,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/llm/plugin-package-inventory-deepseek/src/index.ts:31`](../packages/llm/plugin-package-inventory-deepseek/src/index.ts)
+来源:[`packages/llm/plugin-package-inventory-deepseek/src/index.ts:32`](../packages/llm/plugin-package-inventory-deepseek/src/index.ts)
 
 <a id="deepseek-aidsh-ptc-runtime-node"></a>
 
@@ -3358,7 +3358,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/typert/loader/src/index.ts:47`](../packages/typert/loader/src/index.ts)
+来源:[`packages/typert/loader/src/index.ts:48`](../packages/typert/loader/src/index.ts)
 
 <a id="deepseek-aidsh-user-approval"></a>
 

+ 2 - 2
packages/boot/app-boot/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
-README.md: 91ad6b67357172a0f6dea18c03deee73de391c0b
-README.zh.md: c8bb61e28e8ad758c757fe11a5b55135e1daf319
+README.md: d803b48ceb93104660eaac96f02554617f1e85b8
+README.zh.md: ad1c39bb8d5571e23d59c318980e57c5ef6f24d9

+ 10 - 5
packages/boot/app-boot/README.md

@@ -58,6 +58,8 @@ Profiles with `patchReload: live` watch both user patch files and apply the [rel
 
 Inserted plugin names may be absolute filesystem paths, file URLs, or package specifiers. Patch loading converts absolute paths and patch-relative `./` or `../` paths to file URLs within `insert` rows and their nested groups; existing-entry name assertions and replacement `config` values remain literal.
 
+Before mounting profile rows, the `dsh` launcher computes one immutable package-resolution generation from the installation and ordered bundle dependency graphs. The default link mode materializes the existing shared and profile-owned fallback links, so supported launch behavior stays unchanged. Internal callers and test harnesses can instead install the generation through Node's ESM and CommonJS resolvers in runtime mode, or materialize and verify the same generation in dual mode.
+
 ### Previewing the effective configuration
 
 Before you boot, you can print the exact configuration the app will mount: the dump shows the composed entry list with `!!js` expressions verbatim, grouped under comments naming each source file and the patch layers that changed it, as one loadable YAML document. Patches that match no row are reported with their layer label; a missing, unparsable, or invalid config fails the dump.
@@ -103,12 +105,14 @@ This section explains how the outcomes above are realized and points at the code
 
 ### Design notes
 
-- **Channel-neutral library.** The package carries no loader hooks and no dev-mode surface; the [`dsh` app](../../../apps/cli/README.md) owns its Node source-launch hook and consumes these helpers for the boot sequence, and built consumers use plain Node package resolution.
+- **Process-local module resolution.** The package installs one generation on Node's internal ESM and CommonJS resolvers before profile rows mount. Node still owns exports, conditions, subpaths, module caches, and final errors. `ctx.pluginPackages` exposes package metadata from the same generation without recording Entry imports; an installed generation is authoritative even for a miss, while low-level embedders that install the service without one retain native lookup.
 - **Two Loader builtins.** `mountRootInclude` registers `cordis:include` and `cordis:group` as Loader builtins: a group row gives one `isolate` realm to a provider and its consumers together, and an agent preset outside this workspace cannot resolve `@deepseek-ai/cordis-plugin-group` by name. Both load through the ambient module pipeline rather than the included tree's own specifier resolution.
 - **Consumer-owned strictness.** Ordinary Loader groups keep successful siblings. App-boot applies the global required-entry policy after initial settlement; agent presets and dynamic multi-entry compositions own and dispose their separate generation when they require all-or-nothing setup. App-boot reads failed fibers to report their recorded errors and coalesces duplicate Loader rejection notifications through one process checkpoint.
-- **Profile module fallback.** Bare plugin specifiers resolve through the Loader from the config directory. Plain Node maintains one symlink per package in the installation dependency closure. A packaged executable instead reads each installed export map with Node ESM conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Missing exports stay unavailable, malformed maps fail startup, and a cross-process writer lock replaces stale entries without exposing partial proxies. A selected external bundle absent from the installation closure receives a profile-local `.dsh-module-fallback` link; existing pnpm entries win, projected links are excluded from later closure discovery, and cleanup removes only dsh-owned links.
+- **One fallback generation.** The installation-first and ordered-bundle breadth-first traversal produces both the runtime table and the retained disk materializer. Runtime mode creates no resolution links and ignores stale projections at their former lookup positions. Link mode materializes the same table; dual mode also compares Node's disk result with the table. A complete successor may add package names atomically, while changing or removing an existing mapping requires restart.
+- **Owned Workers.** Worker build banners import `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap` before bundled business code. Each Worker installs the structured-cloned generation in its own isolate. The bootstrap bundle has no static package imports; on Windows it temporarily supplies the native loader with a service-private cache directory and restores the Worker environment before business code starts. Source Worker entries retain their self-contained dependency closure, and third-party Workers receive no injection.
 - **Update completion.** App boot observes restart failures through the `internal/update` waterfall. Live patch reloads wait for the tree's fibers before auditing activation; `Fiber.update()` and `Entry.update()` alone do not establish restart success.
-- **Two-stage failure labels.** `boot()` distinguishes `host preparation failed` — `prepare` threw before any config-tree entry mounted — from `plugin tree failed to load`. Plugin diagnostics include original stacks, nested causes, and aggregate member failures. Cyclic causes terminate diagnostic traversal without replacing the original cause.
+- **One rejection checkpoint.** `assertEntriesActivated` keeps the exact reasons it folds into the boot diagnostic visible through the next process rejection checkpoint, so `installFailLoud` coalesces Loader's duplicate notification while unrelated unhandled rejections remain fatal.
+- **Two-stage failure labels.** `boot()` distinguishes `host preparation failed` — `prepare` threw before any config-tree entry mounted — from `plugin tree failed to load`, and appends the deepest plugin error's stack. Plugin diagnostics retain nested causes and aggregate member failures; cyclic causes stop traversal without replacing the original error.
 
 ### Helper behavior
 
@@ -120,7 +124,8 @@ The exports each own one stage of the boot: config resolution and snapshot repla
 |---|---|
 | [`src/index.ts`](src/index.ts) | Boot helpers: config resolution, environment loading, fail-loud guard, activation audit, patch parsing, config dump, harness-source section |
 | [`src/profile.ts`](src/profile.ts) | Profile discovery, initialization, bundle resolution, module fallback |
-| — | No runtime invariant companion is published; this presentation adapter owns no durable package-local event stream; boundary and replay tests cover its protocol mapping. |
+| [`src/profile-resolution/`](src/profile-resolution/) | Runtime resolver, package-metadata service, and built Worker bootstrap |
+| — | No runtime invariant companion is published; one registration owns each resolver generation, and dual mode compares the independently materialized result at resolution time. |
 
 </details>
 
@@ -158,7 +163,7 @@ Boot itself changes no request prefix. `addHarnessSourceSection` places its sour
 
 These limits describe when this boot library is a poor fit or needs special care. They are current package constraints, not a task backlog.
 
-- **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.
+- **Runtime resolution depends on Node internals** — supported Node versions require the native builtin-access addon and executable compatibility coverage. Only built Harness-owned Workers receive the generation bootstrap; third-party Workers and custom `vm` linkers keep native resolution.
 - **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-scoped** — `loadLayeredEnv` 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.

+ 10 - 5
packages/boot/app-boot/README.zh.md

@@ -58,6 +58,8 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
 
 插入条目的插件名可以是绝对文件系统路径、文件 URL 或包标识符。patch 加载会把 `insert` 条目及其嵌套分组中的绝对路径以及相对于 patch 文件的 `./` 或 `../` 路径转换为文件 URL;对已有条目名称的断言及替换用的 `config` 值保持原样。
 
+挂载 profile 条目前,`dsh` launcher 会从安装依赖图与有序 bundle 依赖图计算一份不可变的 package resolution generation。默认 link 模式会物化现有的共享 fallback 链接与 profile 自有 fallback 链接,因此受支持的启动行为保持不变。内部调用方和测试工具可以改用 runtime 模式,把 generation 安装到 Node 的 ESM 与 CommonJS resolver;也可以使用 dual 模式,同时物化并校验同一份 generation。
+
 ### 预览生效配置
 
 启动前,你可以打印应用将挂载的确切配置:dump 会以 `!!js` 表达式原样展示组合后的条目列表,并按注释分组标明每个源文件及其 patch 层,输出是一份可加载的 YAML 文档。未匹配到任何行的 patch 会连同其层标签一起报告;配置缺失、无法解析或字段无效都会使 dump 失败。
@@ -103,12 +105,14 @@ Loader 结算后,app-boot 将 optional 失败报告为警告;若已启用的
 
 ### 设计说明
 
-- **与渠道无关的库。** 此包不包含 loader 钩子,也不提供开发模式接口;[`dsh` 应用](../../../apps/cli/README.zh.md) 持有自己的 Node 源码启动钩子,并在启动序列中使用这些 helper,构建后的消费方则使用普通 Node 包解析
+- **进程内模块解析。** 此包会在挂载 profile 条目前,将一份 generation 安装到 Node 的 ESM 与 CommonJS 内部 resolver。exports、conditions、subpath、模块缓存和最终错误仍由 Node 负责。`ctx.pluginPackages` 从同一 generation 提供 package metadata,不记录 Entry import;安装 generation 后,即使查询未命中也以 generation 为准,仅安装服务而未提供 generation 的底层嵌入方仍使用 Node 原生查找
 - **两个 Loader builtin。** `mountRootInclude` 把 `cordis:include` 与 `cordis:group` 注册为 Loader builtin:group 行能把一个提供方与它的消费方放进同一个 `isolate` realm,而位于本工作区之外的 agent preset 无法按名称解析 `@deepseek-ai/cordis-plugin-group`。两者都通过宿主的模块管线加载,而非被包含树自身的说明符解析。
 - **由 consumer 持有严格语义。** 普通 Loader group 保留成功 sibling。App-boot 在首次结算后应用全局 required-entry policy;agent preset 与动态多 entry 组合在需要 all-or-nothing setup 时,持有并拆卸各自的独立 generation。App-boot 读取 failed fiber 来报告已记录的错误,并在一个进程检查点内合并 Loader 重复的 rejection 通知。
-- **Profile 模块后备机制。** 裸插件 specifier 由 Loader 从配置目录解析。普通 Node 会为安装依赖闭包中的每个包维护一个符号链接。打包可执行文件无法让操作系统符号链接进入 pkg 的 `/snapshot` 树,因此会按 Node ESM 条件读取已安装包的 export map,并写入重新导出虚拟模块 URL 的真实代理包。缺失 export 保持不可用,错误 export map 会让启动失败,跨进程 writer lock 则会在不暴露部分代理的情况下替换陈旧条目。所选外部组合包若不在安装闭包中,则会获得 profile 本地的 `.dsh-module-fallback` 链接;已有 pnpm 条目优先,后续闭包发现会排除投影链接,清理也只删除 dsh 自有链接。
+- **唯一 fallback generation。** 安装优先、有序 bundle 逐根 breadth-first 遍历同时生成运行时表和保留的磁盘 materializer。runtime 模式不创建解析链接,并在旧链接原来的查找位置忽略陈旧投影。link 模式物化同一张表;dual 模式还会比较 Node 的磁盘结果与表。完整后继 generation 可以原子增加 package name,修改或删除既有映射则要求重启。
+- **自有 Worker。** Worker 构建 banner 会在业务 bundle 前导入 `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap`。每个 Worker 在自己的 isolate 中安装结构化克隆的 generation。bootstrap bundle 不静态导入任何包;在 Windows 上,它会临时向 native loader 提供服务私有缓存目录,并在业务代码启动前恢复 Worker 环境。源码 Worker 入口保留自包含依赖,第三方 Worker 不接受注入。
 - **更新完成。** App boot 通过 `internal/update` waterfall 观察重启失败。实时 patch 重载在检查激活状态前等待配置树中的 fiber;单独调用 `Fiber.update()` 或 `Entry.update()` 不能确定重启成功。
-- **两阶段失败标签。** `boot()` 区分 `host preparation failed`(`prepare` 在任何配置树条目挂载前抛出)与 `plugin tree failed to load`。插件诊断包含原始堆栈、嵌套原因和聚合错误中的各项失败。原因链出现循环时,诊断遍历会终止,不会替换原始原因。
+- **单一 rejection 检查点。** `assertEntriesActivated` 把折入启动诊断的确切原因保持到下一个进程级 rejection 检查点可见,使 `installFailLoud` 能合并 Loader 的重复通知,而所有无关的未处理 rejection 仍然致命。
+- **两阶段失败标签。** `boot()` 区分 `host preparation failed`(`prepare` 在任何配置树条目挂载前抛出)与 `plugin tree failed to load`,并追加最深层插件错误的堆栈。插件诊断保留嵌套原因和聚合错误中的各项失败;原因链出现循环时会停止遍历,但不会替换原始错误。
 
 ### Helper 行为
 
@@ -120,7 +124,8 @@ Loader 结算后,app-boot 将 optional 失败报告为警告;若已启用的
 |---|---|
 | [`src/index.ts`](src/index.ts) | 启动 helper:配置解析、环境加载、会明确报错的保护机制、激活审计、patch 解析、配置 dump、harness 源码段落 |
 | [`src/profile.ts`](src/profile.ts) | profile 发现、初始化、组合包解析、模块后备机制 |
-| — | 不发布运行时不变式伴生入口;边界与回放测试覆盖其协议映射。 |
+| [`src/profile-resolution/`](src/profile-resolution/) | 运行时 resolver、package metadata 服务与构建后 Worker bootstrap |
+| — | 不发布运行时不变式伴生入口;每个 resolver generation 只有一个 registration 所有,dual 模式在解析时比较独立物化的结果。 |
 
 </details>
 
@@ -158,7 +163,7 @@ Loader 结算后,app-boot 将 optional 失败报告为警告;若已启用的
 
 这些限制说明此启动库在何时不合适,或何时需要特别注意。它们是当前包约束,不是任务积压。
 
-- **裸包 specifier 依赖 Loader 内部机制**——生产 bin 需要 Loader 的可选原生辅助组件;没有该辅助组件的进程内调用方必须使用可解析的相对/file specifier,或提供自己的模块解析钩子
+- **运行时解析依赖 Node 内部机制**——受支持的 Node 版本需要 native builtin access addon 和可执行兼容验证。只有构建后的 Harness 自有 Worker 接收 generation bootstrap;第三方 Worker 与自定义 `vm` linker 保持原生解析
 - **快照回放替换仅识别特定 basename**——只有以 `cordis.yml` 或 `cordis.yaml` 结尾的配置会映射到同级 `cordis.snapshot.yml`;自定义配置名称需要调用方自行选择。
 - **环境发现以启动为界**——`loadLayeredEnv` 只读取一次调用目录与 harness home 中的 `.env`;它不搜索父目录,也不跟随之后选择的 workspace。`loadEnv` 仍是非产品 bin 使用的单目录 helper。
 - **用户 patch 会替换匹配到的整个配置**——按 id 定位的 patch 不做深度合并,因此 profile 覆盖必须重述需要保留的组合包字段。

+ 6 - 0
packages/boot/app-boot/package.json

@@ -18,11 +18,16 @@
       "types": "./lib/types/index.d.ts",
       "default": "./lib/index.js"
     },
+    "./worker/profile-resolution-bootstrap": {
+      "types": "./lib/types/profile-resolution/worker-bootstrap.d.ts",
+      "default": "./lib/worker/profile-resolution-bootstrap.js"
+    },
     "./src/*": "./src/*",
     "./package.json": "./package.json"
   },
   "files": [
     "lib/index.js",
+    "lib/worker/profile-resolution-bootstrap.js",
     "lib/types/**/*.d.ts"
   ],
   "license": "MIT",
@@ -31,6 +36,7 @@
     "@deepseek-ai/dsh-package-manifest": "workspace:^",
     "chokidar": "4.0.3",
     "js-yaml": "^4.2.0",
+    "node-addon-require-builtin": "^0.1.4",
     "resolve.exports": "^2.0.3"
   },
   "peerDependencies": {

+ 9 - 0
packages/boot/app-boot/src/index.ts

@@ -30,6 +30,7 @@ declare module '@deepseek-ai/cordis' {
 
 export {
   composeEntries,
+  createProfileResolutionGeneration,
   DEFAULT_PROFILE_BUNDLES,
   DEFAULT_PROFILE_PATCH_RELOAD,
   healProfilesModuleFallback,
@@ -47,8 +48,16 @@ export {
   type ProfileLayer,
   type ProfileManifest,
   type ProfileModuleFallbackOptions,
+  type ProfileResolutionEntry,
+  type ProfileResolutionGeneration,
+  type ProfileResolutionMode,
   type ProfileTemplate,
 } from './profile.ts'
+export {
+  PluginPackages,
+  type PluginPackage,
+  type PluginPackagesConfig,
+} from './profile-resolution/service.ts'
 
 /**
  * Resolve the config to boot. Replay swaps a `cordis.yml` basename for

+ 73 - 0
packages/boot/app-boot/src/profile-resolution/legacy-links.ts

@@ -0,0 +1,73 @@
+/** Legacy profile-link inspection shared by the disk materializer and runtime resolver. */
+
+import { lstatSync, readlinkSync, realpathSync } from 'node:fs'
+import { basename, dirname, join, resolve } from 'node:path'
+
+/** Profile-private package links projected into its pnpm-managed node_modules. */
+export const PROFILE_MODULE_FALLBACK_DIR = '.dsh-module-fallback'
+
+/**
+ * Return whether the process reads application modules from pkg's virtual filesystem.
+ * @returns whether pkg owns the module filesystem.
+ */
+export function isPackagedExecutable(): boolean {
+  return (process as NodeJS.Process & { pkg?: unknown }).pkg !== undefined
+}
+
+/**
+ * Resolve a directory through the active carrier's filesystem implementation.
+ * @param path - directory path to canonicalize.
+ * @returns the canonical directory path.
+ */
+export function realModuleDirectory(path: string): string {
+  return isPackagedExecutable() ? realpathSync(path) : realpathSync.native(path)
+}
+
+/**
+ * Resolve a link target without following the final path component.
+ * @param path - candidate path whose parent is canonicalized.
+ * @returns the canonical candidate, or undefined when its parent is absent.
+ */
+export function canonicalLinkPath(path: string): string | undefined {
+  try {
+    return join(realModuleDirectory(dirname(path)), basename(path))
+  } catch (error) {
+    // A missing parent means the candidate cannot identify an existing owned link.
+    /* v8 ignore next 2 -- a non-ENOENT realpath failure requires a host filesystem fault */
+    if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
+    /* v8 ignore next -- see the host-filesystem exception above */
+    throw error
+  }
+}
+
+/**
+ * Return whether a symlink or junction points at the same path as `target`.
+ * @param link - symlink or junction to inspect.
+ * @param target - expected target path.
+ * @returns whether both paths identify the same entry.
+ */
+export function symlinkPointsTo(link: string, target: string): boolean {
+  const actual = resolve(dirname(link), readlinkSync(link))
+  const canonicalActual = canonicalLinkPath(actual)
+  const canonicalTarget = canonicalLinkPath(resolve(target))
+  return canonicalActual !== undefined && canonicalActual === canonicalTarget
+}
+
+/**
+ * Return whether an observed profile package must not claim local precedence.
+ * @param profileDir - profile directory containing the package projection.
+ * @param packageName - bare package name to inspect.
+ * @returns whether the entry is a managed fallback link or disappeared during inspection.
+ */
+export function isProfileModuleFallbackLink(profileDir: string, packageName: string): boolean {
+  const link = join(profileDir, 'node_modules', packageName)
+  const target = join(profileDir, PROFILE_MODULE_FALLBACK_DIR, 'node_modules', packageName)
+  try {
+    return lstatSync(link).isSymbolicLink() && symlinkPointsTo(link, target)
+  } catch (error) {
+    /* v8 ignore next 2 -- a vanished candidate cannot claim local precedence */
+    if ((error as NodeJS.ErrnoException).code === 'ENOENT') return true
+    /* v8 ignore next -- non-ENOENT lstat/readlink failures require a host filesystem fault */
+    throw error
+  }
+}

+ 612 - 0
packages/boot/app-boot/src/profile-resolution/resolver.ts

@@ -0,0 +1,612 @@
+/** In-memory profile package routing for Node's default ESM and CommonJS loaders. */
+
+import { existsSync, realpathSync, statSync } from 'node:fs'
+import { createRequire, isBuiltin } from 'node:module'
+import { dirname, join, resolve, sep } from 'node:path'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+import { getEnvironmentData, setEnvironmentData } from 'node:worker_threads'
+import type { ModuleLoaderV1, ModuleLoaderV2, ResolveResult } from '@deepseek-ai/cordis-plugin-loader'
+import { isProfileModuleFallbackLink } from './legacy-links.ts'
+import type { ProfileResolutionEntry, ProfileResolutionGeneration } from '../profile.ts'
+
+const WORKER_RESOLUTION_KEY = '@deepseek-ai/dsh-app-boot/profile-resolution'
+const EMPTY_ATTRIBUTES: ImportAttributes = Object.freeze({})
+
+interface CommonJsParent {
+  filename?: string | null
+  paths?: string[]
+}
+
+interface CommonJsOptions {
+  paths?: string[]
+  conditions?: ReadonlySet<string>
+}
+
+interface CommonJsModule {
+  new(id?: string, parent?: CommonJsParent): CommonJsParent
+  _nodeModulePaths(path: string): string[]
+  _resolveFilename(
+    request: string, parent: CommonJsParent | null | undefined, isMain: boolean, options?: CommonJsOptions,
+  ): string
+}
+
+interface InternalModules {
+  esm: ModuleLoaderV1 | ModuleLoaderV2
+  cjs: CommonJsModule
+  modern: boolean
+}
+
+type EsmResolve = (
+  request: string, parent: string | undefined, attributes: ImportAttributes,
+) => ResolveResult | Promise<ResolveResult>
+
+type ResolutionRoute =
+  | { readonly kind: 'fallback'; readonly entry: ProfileResolutionEntry }
+  | { readonly kind: 'after-fallback'; readonly parent: string }
+  | { readonly kind: 'native'; readonly packageDir?: string }
+
+interface ResolutionRouteState {
+  readonly route: ResolutionRoute
+  packageDir?: string
+  esm?: ResolveResult
+  cjs?: string
+}
+
+interface ParentRoutes {
+  readonly parent: string
+  readonly profilesDir: string
+  readonly activeProfile: boolean
+  readonly requests: Map<string, ResolutionRouteState>
+}
+
+type ResolutionRoutes = Map<string, ParentRoutes | false>
+
+interface CompiledGeneration {
+  readonly entries: ReadonlyMap<string, ProfileResolutionEntry>
+  readonly profilesDir: string
+  readonly profileDir: string | undefined
+  readonly profilePaths: readonly string[]
+  readonly profileUrls: readonly string[]
+  readonly profile: readonly string[]
+  readonly activeProfileUrls: readonly string[]
+  readonly localPackageNames: ReadonlySet<string>
+  readonly shared: ReadonlySet<string>
+  readonly esmRoutes: ResolutionRoutes
+  readonly cjsRoutes: ResolutionRoutes
+}
+
+/** Whether runtime resolution redirects requests or verifies the materialized backend. */
+export type ProfileResolutionBehavior = 'enforce' | 'verify'
+
+/** Active resolver registration in one Node isolate. */
+export interface ProfileResolutionRegistration {
+  /**
+   * Locate a bare package without requiring one of its exports.
+   * @param specifier - bare package or package-subpath specifier.
+   * @param parentURL - file URL whose lookup order applies.
+   * @returns selected package directory, or undefined when it is absent.
+   */
+  packageDir(specifier: string, parentURL: string): string | undefined
+  /**
+   * Atomically publish an additive package table and fresh generation-owned caches.
+   * @param generation - fully constructed successor generation.
+   * @throws when the profile scope or an existing package mapping changes.
+   */
+  replace(generation: ProfileResolutionGeneration): void
+  /** Restore the native resolver methods. Registrations dispose in reverse order. */
+  dispose(): void
+}
+
+/**
+ * Split a bare request into its package name without allocating path segments.
+ * @param request - module specifier to classify.
+ * @returns the bare package name, or undefined for non-package requests.
+ */
+export function barePackageName(request: string): string | undefined {
+  if (!request || request[0] === '.' || request[0] === '/' || request[0] === '\\'
+    || request[0] === '#' || request.includes(':') || isBuiltin(request)) return
+  const first = request.indexOf('/')
+  if (request[0] !== '@') return first < 0 ? request : request.slice(0, first)
+  if (first < 0) return
+  const second = request.indexOf('/', first + 1)
+  return second < 0 ? request : request.slice(0, second)
+}
+
+function canonicalPath(path: string): string {
+  try {
+    return realpathSync(path)
+  } catch {
+    // A generation may name a profile scope before that directory is materialized.
+    return resolve(path)
+  }
+}
+
+function prefixes(path: string): readonly string[] {
+  const configured = resolve(path) + sep
+  const canonical = canonicalPath(path) + sep
+  return canonical === configured ? [configured] : [configured, canonical]
+}
+
+function compileGeneration(generation: ProfileResolutionGeneration): CompiledGeneration {
+  const profilePaths = prefixes(generation.profilesDir)
+  const profile = generation.profileDir === undefined ? [] : prefixes(generation.profileDir)
+  return {
+    entries: new Map(generation.entries.map(entry => [entry.name, entry])),
+    profilesDir: generation.profilesDir,
+    profileDir: generation.profileDir,
+    profilePaths,
+    profileUrls: profilePaths.map(path => pathToFileURL(path).href),
+    profile,
+    activeProfileUrls: profile.map(path => pathToFileURL(path).href),
+    localPackageNames: new Set(generation.localPackageNames),
+    shared: new Set(profilePaths.map(prefix => join(prefix, 'node_modules'))),
+    esmRoutes: new Map(),
+    cjsRoutes: new Map(),
+  }
+}
+
+function startsWithin(path: string, roots: readonly string[]): boolean {
+  for (const root of roots) {
+    if (path.startsWith(root)) return true
+  }
+  return false
+}
+
+function nativePackageDir(parent: string, name: string): string | undefined {
+  for (const searchPath of createRequire(parent).resolve.paths(name) as string[]) {
+    const candidate = join(searchPath, name)
+    if (existsSync(join(candidate, 'package.json'))) return candidate
+  }
+  return undefined
+}
+
+function localPackageCandidate(
+  searchPath: string, name: string, flavor: 'esm' | 'cjs',
+): { packageDir: string; canBeManagedLink: boolean } | undefined {
+  const candidate = join(searchPath, name)
+  const stat = statSync(candidate, { throwIfNoEntry: false })
+  const found = flavor === 'esm'
+    ? stat?.isDirectory() === true
+    : stat?.isFile() === true
+      || existsSync(join(candidate, 'package.json'))
+      || ['.js', '.json', '.node'].some(extension => existsSync(candidate + extension))
+      || ['index.js', 'index.json', 'index.node'].some(entry => existsSync(join(candidate, entry)))
+  return found ? { packageDir: candidate, canBeManagedLink: stat !== undefined } : undefined
+}
+
+function sameResolution(left: string, right: string): boolean {
+  if (left === right) return true
+  const leftPath = left.startsWith('file:') ? fileURLToPath(left) : left
+  const rightPath = right.startsWith('file:') ? fileURLToPath(right) : right
+  return canonicalPath(leftPath) === canonicalPath(rightPath)
+}
+
+/** One mutable pointer to immutable generation data. */
+class ResolutionRouter {
+  private current: CompiledGeneration
+
+  constructor(generation: ProfileResolutionGeneration) {
+    this.current = compileGeneration(generation)
+  }
+
+  replace(generation: ProfileResolutionGeneration): void {
+    const entries = new Map(generation.entries.map(entry => [entry.name, entry]))
+    if (generation.profilesDir !== this.current.profilesDir
+      || generation.profileDir !== this.current.profileDir) {
+      throw new Error('profile resolution: a generation cannot change its profile scope')
+    }
+    for (const [name, current] of this.current.entries) {
+      const next = entries.get(name)
+      if (next === undefined
+        || !sameResolution(current.packageDir, next.packageDir)
+        || !sameResolution(current.declarer, next.declarer)
+        || current.version !== next.version
+        || current.scope !== next.scope) {
+        throw new Error(`profile resolution: replacing ${JSON.stringify(name)} requires a process restart`)
+      }
+    }
+    const localPackageNames = new Set(generation.localPackageNames)
+    for (const name of this.current.localPackageNames) {
+      if (!localPackageNames.has(name)) {
+        throw new Error(`profile resolution: removing local package ${JSON.stringify(name)} requires a process restart`)
+      }
+    }
+    for (const name of localPackageNames) {
+      if (!this.current.localPackageNames.has(name) && this.current.entries.has(name)) {
+        throw new Error(`profile resolution: overriding ${JSON.stringify(name)} locally requires a process restart`)
+      }
+    }
+    this.current = compileGeneration(generation)
+  }
+
+  private routeScoped(
+    request: string,
+    parentRoutes: ParentRoutes,
+    generation: CompiledGeneration,
+    flavor: 'esm' | 'cjs',
+  ): ResolutionRouteState | undefined {
+    const { parent, profilesDir, requests } = parentRoutes
+    const name = barePackageName(request)
+    if (name === undefined) return undefined
+
+    const target = generation.entries.get(name)
+    for (const searchPath of createRequire(parent).resolve.paths(name) as string[]) {
+      if (generation.shared.has(resolve(searchPath))) break
+      const candidate = localPackageCandidate(searchPath, name, flavor)
+      if (candidate !== undefined) {
+        const legacy = candidate.canBeManagedLink && generation.profile.some(prefix => (
+          candidate.packageDir === join(prefix, 'node_modules', name)
+          && isProfileModuleFallbackLink(prefix.slice(0, -1), name)
+        ))
+        if (!legacy) {
+          const state = {
+            route: { kind: 'native' as const, packageDir: candidate.packageDir },
+            packageDir: candidate.packageDir,
+          }
+          requests.set(request, state)
+          return state
+        }
+      }
+    }
+
+    const eligible = target?.scope === 'installation'
+      || (target?.scope === 'profile' && parentRoutes.activeProfile)
+    const route: ResolutionRoute = eligible
+      ? { kind: 'fallback', entry: target }
+      : { kind: 'after-fallback', parent: join(dirname(profilesDir), 'package.json') }
+    const state: ResolutionRouteState = { route }
+    requests.set(request, state)
+    return state
+  }
+
+  private routeLocalPackage(
+    request: string, parentRoutes: ParentRoutes, generation: CompiledGeneration,
+  ): ResolutionRouteState | undefined {
+    const name = barePackageName(request)
+    if (name === undefined || !parentRoutes.activeProfile || !generation.localPackageNames.has(name)) return undefined
+    const state = { route: { kind: 'native' as const } }
+    parentRoutes.requests.set(request, state)
+    return state
+  }
+
+  routeUrl(request: string, parentURL: string): ResolutionRouteState | undefined {
+    const generation = this.current
+    let parentRoutes = generation.esmRoutes.get(parentURL)
+    if (parentRoutes === false) return undefined
+    const cached = parentRoutes?.requests.get(request)
+    if (cached !== undefined) return cached
+    if (parentRoutes === undefined) {
+      const profileIndex = generation.profileUrls.findIndex(prefix => parentURL.startsWith(prefix))
+      const activeProfileIndex = generation.activeProfileUrls.findIndex(prefix => parentURL.startsWith(prefix))
+      if (profileIndex < 0 && activeProfileIndex < 0) {
+        generation.esmRoutes.set(parentURL, false)
+        return undefined
+      }
+      let parent: string
+      try {
+        parent = fileURLToPath(parentURL)
+      } catch {
+        generation.esmRoutes.set(parentURL, false)
+        return undefined
+      }
+      const profileRoot = profileIndex >= 0
+        ? generation.profilePaths[profileIndex]
+        : generation.profile[activeProfileIndex]
+      /* v8 ignore next -- one matching index was established above */
+      if (profileRoot === undefined) return undefined
+      parentRoutes = {
+        parent,
+        profilesDir: profileRoot.slice(0, -1),
+        activeProfile: activeProfileIndex >= 0,
+        requests: new Map(),
+      }
+      generation.esmRoutes.set(parentURL, parentRoutes)
+    }
+    const local = this.routeLocalPackage(request, parentRoutes, generation)
+    if (local !== undefined) return local
+    return this.routeScoped(request, parentRoutes, generation, 'esm')
+  }
+
+  routePath(request: string, parent: string): ResolutionRouteState | undefined {
+    const generation = this.current
+    let parentRoutes = generation.cjsRoutes.get(parent)
+    if (parentRoutes === false) return undefined
+    const cached = parentRoutes?.requests.get(request)
+    if (cached !== undefined) return cached
+    if (parentRoutes === undefined) {
+      const profilesDir = generation.profilePaths.find(prefix => parent.startsWith(prefix))
+      const activeProfile = generation.profile.find(prefix => parent.startsWith(prefix))
+      if (profilesDir !== undefined || activeProfile !== undefined) {
+        const profileRoot = profilesDir ?? activeProfile
+        /* v8 ignore next -- one matching root was established above */
+        if (profileRoot === undefined) return undefined
+        parentRoutes = {
+          parent,
+          profilesDir: profileRoot.slice(0, -1),
+          activeProfile: activeProfile !== undefined,
+          requests: new Map(),
+        }
+        generation.cjsRoutes.set(parent, parentRoutes)
+      }
+    }
+    if (parentRoutes === undefined) {
+      generation.cjsRoutes.set(parent, false)
+      return undefined
+    }
+    const local = this.routeLocalPackage(request, parentRoutes, generation)
+    if (local !== undefined) return local
+    return this.routeScoped(request, parentRoutes, generation, 'cjs')
+  }
+
+  explicitRoute(
+    request: string, paths: readonly string[],
+  ): { index: number; state: ResolutionRouteState } | undefined {
+    for (const [index, path] of paths.entries()) {
+      const parent = join(resolve(path), '.dsh-profile-resolution.cjs')
+      const state = this.routePath(request, parent)
+      if (state !== undefined) return { index, state }
+    }
+    return undefined
+  }
+
+  packageDir(specifier: string, parentURL: string): string | undefined {
+    const name = barePackageName(specifier)
+    if (name === undefined) return undefined
+    const state = this.routeUrl(specifier, parentURL)
+    if (state?.route.kind === 'fallback') return state.route.entry.packageDir
+    if (state?.packageDir !== undefined) return state.packageDir
+    let parent: string
+    try {
+      parent = state?.route.kind === 'after-fallback' ? state.route.parent : fileURLToPath(parentURL)
+    } catch {
+      return undefined
+    }
+    const found = nativePackageDir(parent, name)
+    if (state !== undefined && found !== undefined) state.packageDir = found
+    return found
+  }
+}
+
+function internalModules(): InternalModules {
+  const require = createRequire(import.meta.url)
+  const addon = require('node-addon-require-builtin') as { requireBuiltin(moduleId: string): unknown }
+  const esmModule = addon.requireBuiltin('internal/modules/esm/loader') as {
+    getOrInitializeCascadedLoader(): ModuleLoaderV1 | ModuleLoaderV2
+  }
+  const cjsModule = addon.requireBuiltin('internal/modules/cjs/loader') as { Module: CommonJsModule }
+  const esm = esmModule.getOrInitializeCascadedLoader()
+  const modern = 'getOrCreateModuleJob' in esm
+  /* v8 ignore start -- the supported Node 22/24/26 matrix validates each available Internal interface */
+  if (typeof esm.resolveSync !== 'function'
+    || typeof Reflect.get(esm, modern ? 'getOrCreateModuleJob' : 'getModuleJobForImport') !== 'function'
+    || (!modern && typeof Reflect.get(esm, 'resolve') !== 'function')
+    || typeof cjsModule.Module._resolveFilename !== 'function') {
+    throw new Error('profile resolution: unsupported Node module loader')
+  }
+  /* v8 ignore stop */
+  return { esm, cjs: cjsModule.Module, modern }
+}
+
+function assertEquivalent(actual: string, expected: string, request: string, parent: string): void {
+  if (sameResolution(actual, expected)) return
+  throw new Error(
+    `profile resolution mismatch for ${JSON.stringify(request)} from ${parent}: disk resolved ${actual}, generation resolved ${expected}`,
+  )
+}
+
+/**
+ * Install one profile generation on Node's default ESM and CommonJS resolvers.
+ * @param generation - complete package table and profile scope.
+ * @param behavior - enforce the generation, or verify a materialized generation.
+ * @returns a registration that replaces the generation or restores the native methods.
+ */
+export function installProfileResolution(
+  generation: ProfileResolutionGeneration,
+  behavior: ProfileResolutionBehavior = 'enforce',
+): ProfileResolutionRegistration {
+  const router = new ResolutionRouter(generation)
+  const { esm, cjs, modern } = internalModules()
+  const esmScope = new Map<string, boolean>()
+  const profileUrls = [
+    ...prefixes(generation.profilesDir),
+    ...(generation.profileDir === undefined ? [] : prefixes(generation.profileDir)),
+  ].map(path => pathToFileURL(path).href)
+  let recentEsmParent: string | undefined
+  let recentEsmScoped = false
+  let delegatedEsm: { parent: string | undefined; request: string } | undefined
+
+  const adaptEsm = (native: EsmResolve): EsmResolve => (request, parent, attributes) => {
+    const delegated = delegatedEsm
+    /* v8 ignore next -- reentry requires a separate synchronous Node hook; supported launches install none */
+    if (delegated !== undefined && delegated.parent === parent && delegated.request === request) {
+      return native(request, parent, attributes)
+    }
+    if (parent === undefined) return native(request, parent, attributes)
+    let scoped = recentEsmParent === parent ? recentEsmScoped : esmScope.get(parent)
+    if (scoped === undefined) {
+      scoped = startsWithin(parent, profileUrls)
+      esmScope.set(parent, scoped)
+    }
+    if (recentEsmParent !== parent) {
+      recentEsmParent = parent
+      recentEsmScoped = scoped
+    }
+    if (!scoped) return native(request, parent, attributes)
+    const state = router.routeUrl(request, parent)
+    if (state === undefined) return native(request, parent, attributes)
+    const cacheable = attributes === EMPTY_ATTRIBUTES || Object.keys(attributes).length === 0
+    if (cacheable && state.esm !== undefined) return state.esm
+    const route = state.route
+    if (route.kind === 'native') {
+      const result = native(request, parent, attributes)
+      if (cacheable && !(result instanceof Promise)) state.esm = result
+      return result
+    }
+    const routedParent = pathToFileURL(route.kind === 'fallback' ? route.entry.declarer : route.parent).href
+    if (behavior === 'enforce') {
+      const previous = delegatedEsm
+      delegatedEsm = { parent: routedParent, request }
+      try {
+        const result = native(request, routedParent, attributes)
+        /* v8 ignore next -- Node 24+ resolves synchronously; the Node 22 matrix covers its Promise result */
+        if (cacheable && !(result instanceof Promise)) state.esm = result
+        return result
+      } finally {
+        delegatedEsm = previous
+      }
+    }
+    const actual = native(request, parent, attributes)
+    const previous = delegatedEsm
+    delegatedEsm = { parent: routedParent, request }
+    try {
+      const expected = native(request, routedParent, attributes)
+      /* v8 ignore start -- Node 22 is the asynchronous adapter and is covered by the external version matrix */
+      if (expected instanceof Promise || actual instanceof Promise) {
+        return Promise.all([actual, expected]).then(([resolved, wanted]) => {
+          assertEquivalent(resolved.url, wanted.url, request, parent)
+          if (cacheable) state.esm = resolved
+          return resolved
+        })
+      }
+      /* v8 ignore stop */
+      assertEquivalent(actual.url, expected.url, request, parent)
+      if (cacheable) state.esm = actual
+      return actual
+    } finally {
+      delegatedEsm = previous
+    }
+  }
+
+  let restoreEsm: () => void
+  /* v8 ignore else -- CI coverage runs Node 24 v2; the Node 22 matrix exercises the v1 adapter */
+  if (modern) {
+    const loader = esm as ModuleLoaderV2
+    const original = Reflect.get(loader, 'resolveSync')
+    const resolveRequest = adaptEsm((request, parent, attributes) => original.call(
+      loader, parent as string, { specifier: request, attributes },
+    ))
+    const wrapped = (parent: string, request: { specifier: string; attributes?: ImportAttributes }, ...rest: unknown[]): ResolveResult => (
+      rest.length
+        ? Reflect.apply(original, loader, [parent, request, ...rest]) as ResolveResult
+        : resolveRequest(request.specifier, parent, request.attributes ?? EMPTY_ATTRIBUTES) as ResolveResult
+    )
+    loader.resolveSync = wrapped
+    restoreEsm = () => {
+      /* v8 ignore else -- registrations are disposed in reverse installation order */
+      if (loader.resolveSync === wrapped) loader.resolveSync = original
+    }
+  } else {
+    const loader = esm as ModuleLoaderV1
+    const original = Reflect.get(loader, 'resolve')
+    const originalSync = Reflect.get(loader, 'resolveSync')
+    const resolveRequest = adaptEsm((request, parent, attributes) => original.call(loader, request, parent as string, attributes))
+    const resolveRequestSync = adaptEsm((request, parent, attributes) => originalSync.call(loader, request, parent as string, attributes))
+    const wrapped = (request: string, parent: string, attributes: ImportAttributes = EMPTY_ATTRIBUTES): Promise<ResolveResult> => (
+      resolveRequest(request, parent, attributes) as Promise<ResolveResult>
+    )
+    const wrappedSync = (request: string, parent: string, attributes: ImportAttributes = EMPTY_ATTRIBUTES): ResolveResult => (
+      resolveRequestSync(request, parent, attributes) as ResolveResult
+    )
+    loader.resolve = wrapped
+    loader.resolveSync = wrappedSync
+    restoreEsm = () => {
+      if (loader.resolve === wrapped) loader.resolve = original
+      if (loader.resolveSync === wrappedSync) loader.resolveSync = originalSync
+    }
+  }
+
+  const originalFilename = Reflect.get(cjs, '_resolveFilename')
+  let delegatedCjs = 0
+  const resolveRoutedCjs = (
+    request: string, routed: Exclude<ResolutionRoute, { kind: 'native' }>,
+    parent: CommonJsParent, main: boolean, options?: CommonJsOptions,
+  ): string => {
+    const anchor = routed.kind === 'fallback' ? routed.entry.declarer : routed.parent
+    const synthetic = new cjs(anchor, parent)
+    synthetic.filename = anchor
+    synthetic.paths = cjs._nodeModulePaths(dirname(anchor))
+    return originalFilename.call(cjs, request, synthetic, main, options)
+  }
+  const wrappedFilename: CommonJsModule['_resolveFilename'] = (request, parent, main, options) => {
+    if (delegatedCjs || !parent?.filename) {
+      return originalFilename.call(cjs, request, parent, main, options)
+    }
+    const explicitPaths = Array.isArray(options?.paths) ? options.paths : undefined
+    const explicit = explicitPaths === undefined ? undefined : router.explicitRoute(request, explicitPaths)
+    if (options?.paths !== undefined && explicit === undefined) {
+      return originalFilename.call(cjs, request, parent, main, options)
+    }
+    if (explicit !== undefined && explicitPaths !== undefined && explicit.index > 0) {
+      try {
+        return originalFilename.call(cjs, request, parent, main, {
+          ...options,
+          paths: explicitPaths.slice(0, explicit.index),
+        })
+      } catch (error) {
+        if ((error as NodeJS.ErrnoException).code !== 'MODULE_NOT_FOUND') throw error
+      }
+    }
+    const state = explicit?.state ?? router.routePath(request, parent.filename)
+    if (state === undefined) return originalFilename.call(cjs, request, parent, main, options)
+    const cacheable = options?.paths === undefined && options?.conditions === undefined
+    if (cacheable && state.cjs !== undefined) return state.cjs
+    const route = state.route
+    if (route.kind === 'native') {
+      const result = originalFilename.call(cjs, request, parent, main, options)
+      if (cacheable) state.cjs = result
+      return result
+    }
+    if (route.kind === 'after-fallback' && explicit !== undefined && explicitPaths !== undefined) {
+      const paths = [...explicitPaths]
+      paths[explicit.index] = dirname(route.parent)
+      return originalFilename.call(cjs, request, parent, main, { ...options, paths })
+    }
+    delegatedCjs++
+    try {
+      const routedOptions = options?.conditions === undefined ? undefined : { conditions: options.conditions }
+      const expected = resolveRoutedCjs(request, route, parent, main, routedOptions)
+      if (behavior === 'enforce') {
+        if (cacheable) state.cjs = expected
+        return expected
+      }
+      const actual = originalFilename.call(cjs, request, parent, main, options)
+      assertEquivalent(actual, expected, request, parent.filename)
+      if (cacheable) state.cjs = actual
+      return actual
+    } finally {
+      delegatedCjs--
+    }
+  }
+  cjs._resolveFilename = wrappedFilename
+
+  return {
+    packageDir(specifier, parentURL) { return router.packageDir(specifier, parentURL) },
+    replace(next) { router.replace(next) },
+    dispose() {
+      /* v8 ignore else -- registrations are disposed in reverse installation order */
+      if (cjs._resolveFilename === wrappedFilename) cjs._resolveFilename = originalFilename
+      restoreEsm()
+    },
+  }
+}
+
+/**
+ * Publish one generation for Harness-owned Workers.
+ * @param generation - complete package table and profile scope.
+ * @param behavior - enforce or verify the generation in newly created Workers.
+ * @param nativeCacheDir - private physical directory used to load the native adapter in a Worker.
+ * @returns a disposer restoring the previous thread environment data.
+ */
+export function registerWorkerResolution(
+  generation: ProfileResolutionGeneration,
+  behavior: ProfileResolutionBehavior = 'enforce',
+  nativeCacheDir?: string,
+): () => void {
+  const previous = getEnvironmentData(WORKER_RESOLUTION_KEY)
+  setEnvironmentData(WORKER_RESOLUTION_KEY, {
+    generation,
+    behavior,
+    ...(nativeCacheDir === undefined ? {} : { nativeCacheDir }),
+  })
+  return () => { setEnvironmentData(WORKER_RESOLUTION_KEY, previous) }
+}

+ 139 - 0
packages/boot/app-boot/src/profile-resolution/service.ts

@@ -0,0 +1,139 @@
+/** Package metadata resolved through one profile resolution registration. */
+
+import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { Service, type Context } from '@deepseek-ai/cordis'
+import {
+  barePackageName,
+  installProfileResolution,
+  registerWorkerResolution,
+  type ProfileResolutionBehavior,
+  type ProfileResolutionRegistration,
+} from './resolver.ts'
+import type { ProfileResolutionGeneration } from '../profile.ts'
+
+declare module '@deepseek-ai/cordis' {
+  interface Context {
+    /** Deterministic package lookup for configured plugin specifiers. */
+    pluginPackages: PluginPackages
+  }
+}
+
+/** The package that owns a resolved module. */
+export interface PluginPackage {
+  /** Manifest package name. */
+  name: string
+  /** Manifest version when declared. */
+  version: string | undefined
+  /** Absolute package directory. */
+  dir: string
+  /** Absolute package.json path. */
+  manifestPath: string
+  /** Parsed manifest shared by metadata readers. */
+  manifest: Record<string, unknown>
+}
+
+/** Optional runtime resolver installed and owned by {@link PluginPackages}. */
+export interface PluginPackagesConfig {
+  /** Complete package table; omit it to expose native package lookup only. */
+  generation?: ProfileResolutionGeneration
+  /** Enforce the table or compare it with a materialized fallback. */
+  behavior?: ProfileResolutionBehavior
+}
+
+function readPackage(dir: string, fallbackName: string): PluginPackage | undefined {
+  const manifestPath = join(dir, 'package.json')
+  if (!existsSync(manifestPath)) return undefined
+  const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record<string, unknown>
+  const name = manifest.name
+  const version = manifest.version
+  return {
+    name: typeof name === 'string' ? name : fallbackName,
+    version: typeof version === 'string' ? version : undefined,
+    dir,
+    manifestPath,
+    manifest,
+  }
+}
+
+/** Package lookup shared by metadata consumers in one profile process. */
+export class PluginPackages extends Service {
+  private packages = new Map<string, PluginPackage | undefined>()
+  private readonly resolver: ProfileResolutionRegistration | undefined
+  private readonly behavior: ProfileResolutionBehavior
+  private readonly nativeCacheDir: string | undefined
+  private disposeWorkerResolution: (() => void) | undefined
+
+  constructor(ctx: Context, config: PluginPackagesConfig = {}) {
+    super(ctx, 'pluginPackages')
+    this.behavior = config.behavior ?? 'enforce'
+    /* v8 ignore start -- Linux coverage cannot enter the Windows native-cache lifecycle. */
+    this.nativeCacheDir = config.generation !== undefined && process.platform === 'win32'
+      ? mkdtempSync(join(tmpdir(), 'dsh-profile-resolution-native-'))
+      : undefined
+    /* v8 ignore stop */
+    if (config.generation === undefined) return
+    let resolver: ProfileResolutionRegistration
+    try {
+      resolver = installProfileResolution(config.generation, this.behavior)
+    } catch (error) {
+      /* v8 ignore next -- only an unsupported Node Internal can fail before service publication. */
+      if (this.nativeCacheDir !== undefined) rmSync(this.nativeCacheDir, { recursive: true, force: true })
+      /* v8 ignore next -- the unsupported-Node failure is covered by the external version matrix. */
+      throw error
+    }
+    this.disposeWorkerResolution = registerWorkerResolution(
+      config.generation, this.behavior, this.nativeCacheDir,
+    )
+    this.resolver = resolver
+    ctx.effect(() => () => {
+      this.disposeWorkerResolution?.()
+      resolver.dispose()
+      /* v8 ignore start -- Linux coverage cannot enter the Windows native-cache lifecycle. */
+      if (this.nativeCacheDir !== undefined) {
+        rmSync(this.nativeCacheDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
+      }
+      /* v8 ignore stop */
+    }, 'profile package resolution')
+  }
+
+  /**
+   * Publish an additive generation for this process and subsequently created Workers.
+   * @param generation - fully constructed successor generation.
+   */
+  replace(generation: ProfileResolutionGeneration): void {
+    if (this.resolver === undefined) throw new Error('plugin-packages: runtime resolution is not installed')
+    this.resolver.replace(generation)
+    this.packages = new Map()
+    this.disposeWorkerResolution?.()
+    this.disposeWorkerResolution = registerWorkerResolution(generation, this.behavior, this.nativeCacheDir)
+  }
+
+  /**
+   * Locate the package named by a specifier without requiring a package export.
+   * @param specifier - module specifier whose package owns the requested module.
+   * @param parentURL - URL whose Node lookup order applies.
+   * @returns the parsed package, or undefined when no package owns the request.
+   */
+  packageOf(specifier: string, parentURL: string): PluginPackage | undefined {
+    const name = barePackageName(specifier)
+    if (name === undefined) return undefined
+    const dir = this.resolver === undefined
+      ? packageDirFromParent(name, parentURL)
+      : this.resolver.packageDir(name, parentURL)
+    if (dir === undefined) return undefined
+    const key = JSON.stringify({ dir, name })
+    if (!this.packages.has(key)) this.packages.set(key, readPackage(dir, name))
+    return this.packages.get(key)
+  }
+}
+
+function packageDirFromParent(name: string, parentURL: string): string | undefined {
+  for (const searchPath of createRequire(parentURL).resolve.paths(name) as string[]) {
+    const candidate = join(searchPath, name)
+    if (existsSync(join(candidate, 'package.json'))) return candidate
+  }
+  return undefined
+}

+ 23 - 0
packages/boot/app-boot/src/profile-resolution/worker-bootstrap.ts

@@ -0,0 +1,23 @@
+/** Install an inherited profile resolution generation in one Harness-owned Worker. */
+
+import { getEnvironmentData } from 'node:worker_threads'
+import { installProfileResolution, type ProfileResolutionBehavior } from './resolver.ts'
+import type { ProfileResolutionGeneration } from '../profile.ts'
+
+const registration = getEnvironmentData(
+  '@deepseek-ai/dsh-app-boot/profile-resolution',
+) as {
+  generation: ProfileResolutionGeneration
+  behavior: ProfileResolutionBehavior
+  nativeCacheDir?: string
+} | undefined
+if (registration !== undefined) {
+  const previous = process.env.NARB_NATIVE_CACHE_DIR
+  if (registration.nativeCacheDir !== undefined) process.env.NARB_NATIVE_CACHE_DIR = registration.nativeCacheDir
+  try {
+    installProfileResolution(registration.generation, registration.behavior)
+  } finally {
+    if (previous === undefined) delete process.env.NARB_NATIVE_CACHE_DIR
+    else process.env.NARB_NATIVE_CACHE_DIR = previous
+  }
+}

+ 148 - 56
packages/boot/app-boot/src/profile.ts

@@ -25,7 +25,7 @@
 
 import { createRequire } from 'node:module'
 import {
-  existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync,
+  existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync,
   symlinkSync, unlinkSync, writeFileSync,
 } from 'node:fs'
 import { basename, dirname, join, relative, resolve } from 'node:path'
@@ -37,6 +37,14 @@ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
 import type { DshPackageManifest, ProfilePatchReload } from '@deepseek-ai/dsh-package-manifest'
 import { resolve as resolvePackage, type Package as ResolvePackageManifest } from 'resolve.exports'
 import { loadOverlayPatches } from './index.ts'
+import {
+  canonicalLinkPath,
+  isPackagedExecutable,
+  isProfileModuleFallbackLink,
+  PROFILE_MODULE_FALLBACK_DIR,
+  realModuleDirectory,
+  symlinkPointsTo,
+} from './profile-resolution/legacy-links.ts'
 
 /** Directory under the Harness home holding every profile. */
 export const PROFILES_DIR = 'profiles'
@@ -44,9 +52,6 @@ export const PROFILES_DIR = 'profiles'
 /** The user patch layer inside a profile directory (hot-reloaded on long-lived surfaces). */
 export const PROFILE_PATCH_FILENAME = 'cordis.patch.yml'
 
-/** Profile-private package links projected into its pnpm-managed node_modules. */
-const PROFILE_MODULE_FALLBACK_DIR = '.dsh-module-fallback'
-
 /** Installation-owned defaults used when a shipped profile is first opened. */
 export interface ProfileTemplate {
   /** Ordered bundle layer list. */
@@ -86,6 +91,35 @@ export interface Profile {
   patchReload: ProfilePatchReload
 }
 
+/** One package selected by the profile module-fallback rules. */
+export interface ProfileResolutionEntry {
+  /** Bare package name. */
+  readonly name: string
+  /** Package directory selected by the existing dependency traversal. */
+  readonly packageDir: string
+  /** Selected package version when its manifest declares one. */
+  readonly version: string | undefined
+  /** Manifest whose dependency edge selected this package. */
+  readonly declarer: string
+  /** Whether every profile or only the active profile receives this fallback. */
+  readonly scope: 'installation' | 'profile'
+}
+
+/** Complete immutable fallback table for one profile launch. */
+export interface ProfileResolutionGeneration {
+  /** Directory containing every profile and the shared fallback position. */
+  readonly profilesDir: string
+  /** Active profile directory, when bundle-only fallbacks were included. */
+  readonly profileDir: string | undefined
+  /** Profile-declared packages already installed before the fallback position. */
+  readonly localPackageNames: readonly string[]
+  /** Installation entries followed by bundle-only entries in precedence order. */
+  readonly entries: readonly ProfileResolutionEntry[]
+}
+
+/** Startup backend selection for one computed profile resolution generation. */
+export type ProfileResolutionMode = 'link' | 'dual' | 'runtime'
+
 /**
  * Resolve a profile's directory under the Harness home.
  * @param name - the profile name (`dsh --profile <name>`).
@@ -234,27 +268,6 @@ function ensureSymlink(link: string, target: string): void {
   }
 }
 
-/** Resolve a link target without following the final path component. */
-function canonicalLinkPath(path: string): string | undefined {
-  try {
-    return join(realpathSync.native(dirname(path)), basename(path))
-  } catch (error) {
-    // A missing parent means the candidate cannot identify an existing owned link.
-    /* v8 ignore next 2 -- a non-ENOENT realpath failure requires a host filesystem fault */
-    if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
-    /* v8 ignore next -- see the host-filesystem exception above */
-    throw error
-  }
-}
-
-/** Return whether a symlink or junction points at the same path as `target`. */
-function symlinkPointsTo(link: string, target: string): boolean {
-  const actual = resolve(dirname(link), readlinkSync(link))
-  const canonicalActual = canonicalLinkPath(actual)
-  const canonicalTarget = canonicalLinkPath(resolve(target))
-  return canonicalActual !== undefined && canonicalActual === canonicalTarget
-}
-
 /** Add one profile-owned fallback link without replacing a pnpm-managed entry. */
 function ensureProfileSymlink(link: string, target: string): void {
   try {
@@ -311,11 +324,6 @@ interface ModuleProxyRecord {
   dsh?: { moduleFallback?: { targets?: unknown } }
 }
 
-/** Return whether the process reads application modules from pkg's virtual filesystem. */
-function isPackagedExecutable(): boolean {
-  return (process as NodeJS.Process & { pkg?: unknown }).pkg !== undefined
-}
-
 /** Resolve one available explicit package export under Node ESM import conditions. */
 function packageEntryFromPackage(
   packageName: string,
@@ -463,12 +471,24 @@ function profileDependencyNames(manifest: ProfileManifest): string[] {
 
 /** Resolve the installation generation that every profile must find through the fallback directory. */
 function resolveModuleFallbackEntries(
-  installAnchor: string,
-): { entries: ModuleFallbackEntry[]; packageNames: ReadonlySet<string> } {
+  installAnchor: string, materialize = true,
+): {
+  entries: ModuleFallbackEntry[]
+  packageNames: ReadonlySet<string>
+  packageDirs: ReadonlyMap<string, string>
+  declarers: ReadonlyMap<string, string>
+  versions: ReadonlyMap<string, string | undefined>
+} {
   const appManifest = readModuleFallbackManifest(installAnchor)
   const links = new Map<string, string>()
+  const declarers = new Map<string, string>()
+  const versions = new Map<string, string | undefined>()
   /* v8 ignore next -- a real app manifest always declares its name */
-  if (appManifest.name !== undefined) links.set(appManifest.name, dirname(installAnchor))
+  if (appManifest.name !== undefined) {
+    links.set(appManifest.name, dirname(installAnchor))
+    declarers.set(appManifest.name, installAnchor)
+    versions.set(appManifest.name, appManifest.version)
+  }
   // BFS over the resolvable dependency graph; the visited set is the link
   // map itself (first resolution wins, matching Node's own nearest-wins).
   const queue: { anchor: string; manifest: ProfileManifest }[] = [{ anchor: installAnchor, manifest: appManifest }]
@@ -484,19 +504,24 @@ function resolveModuleFallbackEntries(
       // plugin; skip it rather than fail the whole boot.
       if (dir === undefined) continue
       links.set(dep, dir)
+      declarers.set(dep, next.anchor)
       const manifestPath = join(dir, 'package.json')
-      queue.push({ anchor: manifestPath, manifest: readModuleFallbackManifest(manifestPath) })
+      const manifest = readModuleFallbackManifest(manifestPath)
+      versions.set(dep, manifest.version)
+      queue.push({ anchor: manifestPath, manifest })
     }
   }
-  const entries = !isPackagedExecutable()
-    ? [...links].map(([packageName, packageDir]) => ({ kind: 'symlink' as const, packageName, packageDir }))
-    : [...links].flatMap(([packageName, packageDir]) => {
-      const source = packageProxySource(packageName, packageDir)
-      return Object.keys(source.targets).length === 0
-        ? []
-        : [{ kind: 'proxy' as const, packageName, version: source.version, targets: source.targets }]
-    })
-  return { entries, packageNames: new Set(links.keys()) }
+  const entries = !materialize
+    ? []
+    : !isPackagedExecutable()
+      ? [...links].map(([packageName, packageDir]) => ({ kind: 'symlink' as const, packageName, packageDir }))
+      : [...links].flatMap(([packageName, packageDir]) => {
+        const source = packageProxySource(packageName, packageDir)
+        return Object.keys(source.targets).length === 0
+          ? []
+          : [{ kind: 'proxy' as const, packageName, version: source.version, targets: source.targets }]
+      })
+  return { entries, packageNames: new Set(links.keys()), packageDirs: links, declarers, versions }
 }
 
 /** Return whether one existing fallback entry already matches its resolved installation generation. */
@@ -530,6 +555,8 @@ export interface ProfileModuleFallbackOptions {
   profile?: Profile
   /** Harness home; defaults to {@link resolveDshHome}. */
   home?: string
+  /** Whether to materialize the computed generation; defaults to true. */
+  materialize?: boolean
 }
 
 /**
@@ -542,21 +569,71 @@ export interface ProfileModuleFallbackOptions {
  * `node_modules`; pnpm-managed entries remain authoritative, and another
  * profile's links cannot change its resolution.
  * @param options - installation anchor, optional loaded profile, and Harness home.
- * @returns settlement after the shared fallback and profile-local links are current.
+ * @returns the computed fallback generation after optional materialization.
  */
-export async function healProfilesModuleFallback(options: ProfileModuleFallbackOptions): Promise<void> {
-  const { installAnchor, profile, home = resolveDshHome() } = options
+export async function healProfilesModuleFallback(
+  options: ProfileModuleFallbackOptions,
+): Promise<ProfileResolutionGeneration> {
+  const { installAnchor, profile, home = resolveDshHome(), materialize = true } = options
   const profilesDir = join(home, PROFILES_DIR)
   const modulesDir = join(profilesDir, 'node_modules')
-  mkdirSync(modulesDir, { recursive: true })
-  const { entries, packageNames } = resolveModuleFallbackEntries(installAnchor)
-  if (!moduleFallbackCurrent(modulesDir, entries)) {
+  if (materialize) mkdirSync(modulesDir, { recursive: true })
+  const { entries, packageNames, packageDirs, declarers, versions } = resolveModuleFallbackEntries(installAnchor, materialize)
+  if (materialize && !moduleFallbackCurrent(modulesDir, entries)) {
     await withFileLock(modulesDir, () => {
       if (!moduleFallbackCurrent(modulesDir, entries)) healProfilesModuleFallbackLocked(entries, modulesDir)
       return Promise.resolve()
     })
   }
-  if (profile !== undefined) healProfileModuleFallback(profile, packageNames)
+  const profileDeclarers = new Map<string, string>()
+  const profileVersions = new Map<string, string | undefined>()
+  const localPackageNames = profile === undefined ? [] : installedProfilePackageNames(profile)
+  const profilePackages: ReadonlyMap<string, string> = profile === undefined
+    ? new Map<string, string>()
+    : healProfileModuleFallback(profile, packageNames, materialize, profileDeclarers, profileVersions)
+  return Object.freeze({
+    profilesDir,
+    profileDir: profile?.dir,
+    localPackageNames: Object.freeze(localPackageNames),
+    entries: Object.freeze([
+      ...[...packageDirs].map(([name, packageDir]) => Object.freeze({
+        name, packageDir, version: versions.get(name),
+        declarer: declarers.get(name) as string, scope: 'installation' as const,
+      })),
+      ...[...profilePackages].map(([name, packageDir]) => Object.freeze({
+        name, packageDir, version: profileVersions.get(name),
+        declarer: profileDeclarers.get(name) as string, scope: 'profile' as const,
+      })),
+    ]),
+  })
+}
+
+/** Return installed direct dependencies that Node resolves before profile fallback. */
+function installedProfilePackageNames(profile: Profile): string[] {
+  let manifest: ProfileManifest
+  try {
+    manifest = readModuleFallbackManifest(join(profile.dir, 'package.json'))
+  } catch (error) {
+    // Direct helper callers can supply a synthetic Profile without its on-disk manifest.
+    if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []
+    throw error
+  }
+  return profileDependencyNames(manifest).filter((name) => {
+    const candidate = join(profile.dir, 'node_modules', name)
+    if (!existsSync(join(candidate, 'package.json'))) return false
+    return !isProfileModuleFallbackLink(profile.dir, name)
+  })
+}
+
+/**
+ * Compute a profile resolution generation without materializing links or proxies.
+ * @param options - installation anchor, profile, and optional Harness home.
+ * @returns the complete immutable generation.
+ */
+export function createProfileResolutionGeneration(
+  options: Omit<ProfileModuleFallbackOptions, 'materialize'>,
+): Promise<ProfileResolutionGeneration> {
+  return healProfilesModuleFallback({ ...options, materialize: false })
 }
 
 /** Heal one module-fallback generation while the cross-process writer lock is held. */
@@ -576,17 +653,21 @@ function healProfilesModuleFallbackLocked(entries: readonly ModuleFallbackEntry[
 function dependencyClosure(
   anchors: readonly string[], reserved: ReadonlySet<string>,
   exclude: (candidate: string, packageName: string) => boolean,
+  declarers?: Map<string, string>,
+  versions?: Map<string, string | undefined>,
 ): Map<string, string> {
   const links = new Map<string, string>()
   const visited = new Set(reserved)
   for (const anchor of anchors) {
-    const canonicalAnchor = realpathSync.native(anchor)
+    const canonicalAnchor = join(realModuleDirectory(dirname(anchor)), basename(anchor))
     const manifest = readModuleFallbackManifest(canonicalAnchor)
     /* v8 ignore next -- an installable package manifest always declares its name */
     if (manifest.name === undefined) continue
     if (!visited.has(manifest.name)) {
       visited.add(manifest.name)
       links.set(manifest.name, dirname(canonicalAnchor))
+      declarers?.set(manifest.name, canonicalAnchor)
+      versions?.set(manifest.name, manifest.version)
     }
     const queue: { anchor: string; manifest: ProfileManifest }[] = [{ anchor: canonicalAnchor, manifest }]
     for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
@@ -599,8 +680,11 @@ function dependencyClosure(
         if (dir === undefined) continue
         visited.add(dep)
         links.set(dep, dir)
+        declarers?.set(dep, next.anchor)
         const manifestPath = join(dir, 'package.json')
-        queue.push({ anchor: manifestPath, manifest: readModuleFallbackManifest(manifestPath) })
+        const dependencyManifest = readModuleFallbackManifest(manifestPath)
+        versions?.set(dep, dependencyManifest.version)
+        queue.push({ anchor: manifestPath, manifest: dependencyManifest })
       }
     }
   }
@@ -608,11 +692,17 @@ function dependencyClosure(
 }
 
 /** Reconcile packages carried only by selected bundles into one profile. */
-function healProfileModuleFallback(profile: Profile, installationPackageNames: ReadonlySet<string>): void {
+function healProfileModuleFallback(
+  profile: Profile, installationPackageNames: ReadonlySet<string>, materialize = true,
+  declarers?: Map<string, string>,
+  versions?: Map<string, string | undefined>,
+): Map<string, string> {
   const profileModulesDir = join(profile.dir, 'node_modules')
   const ownedModulesDir = join(profile.dir, PROFILE_MODULE_FALLBACK_DIR, 'node_modules')
-  mkdirSync(profileModulesDir, { recursive: true })
-  mkdirSync(ownedModulesDir, { recursive: true })
+  if (materialize) {
+    mkdirSync(profileModulesDir, { recursive: true })
+    mkdirSync(ownedModulesDir, { recursive: true })
+  }
   const bundleAnchors = profile.layers
     .filter(layer => !installationPackageNames.has(layer.packageName))
     .map(layer => join(layer.packageDir, 'package.json'))
@@ -629,8 +719,9 @@ function healProfileModuleFallback(profile: Profile, installationPackageNames: R
       /* v8 ignore next -- see the host-filesystem exception above */
       throw error
     }
-  })
+  }, declarers, versions)
   for (const layer of profile.layers) bundleLinks.delete(layer.packageName)
+  if (!materialize) return bundleLinks
   for (const packageName of ownedPackageNames(ownedModulesDir)) {
     if (!bundleLinks.has(packageName)) removeProfileSymlink(profileModulesDir, ownedModulesDir, packageName)
   }
@@ -642,6 +733,7 @@ function healProfileModuleFallback(profile: Profile, installationPackageNames: R
     mkdirSync(dirname(profileLink), { recursive: true })
     ensureProfileSymlink(profileLink, ownedLink)
   }
+  return bundleLinks
 }
 
 /**

+ 187 - 0
packages/boot/app-boot/tests/profile-resolution-service.spec.ts

@@ -0,0 +1,187 @@
+/** Package metadata queries share the active profile resolution generation. */
+
+import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { tmpdir } from 'node:os'
+import { dirname, join } from 'node:path'
+import { pathToFileURL } from 'node:url'
+import { getEnvironmentData } from 'node:worker_threads'
+import { Context } from '@deepseek-ai/cordis'
+import { afterEach, describe, expect, it } from 'vitest'
+import { PluginPackages } from '../src/profile-resolution/service.ts'
+import type { ProfileResolutionGeneration } from '../src/profile.ts'
+
+const roots: string[] = []
+const contexts: Context[] = []
+
+afterEach(async () => {
+  for (const context of contexts.splice(0).reverse()) await context.fiber.dispose()
+  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
+})
+
+function file(path: string, text: string): void {
+  mkdirSync(dirname(path), { recursive: true })
+  writeFileSync(path, text)
+}
+
+function pkg(dir: string, version: string, name = 'metadata-lib'): string {
+  file(join(dir, 'package.json'), JSON.stringify({
+    name,
+    version,
+    type: 'module',
+    exports: { import: './index.js', require: './index.cjs' },
+  }))
+  file(join(dir, 'index.js'), `export const version = ${JSON.stringify(version)}\n`)
+  file(join(dir, 'index.cjs'), `exports.version = ${JSON.stringify(version)}\n`)
+  return join(dir, 'package.json')
+}
+
+function generation(
+  profilesDir: string, profileDir: string, packageDir: string, declarer: string, version: string,
+): ProfileResolutionGeneration {
+  return {
+    profilesDir,
+    profileDir,
+    localPackageNames: [],
+    entries: [{ name: 'metadata-lib', packageDir, version, declarer, scope: 'installation' }],
+  }
+}
+
+describe('profile package metadata service', () => {
+  it('resolves module URLs and package metadata through the current generation', async () => {
+    const root = mkdtempSync(join(tmpdir(), 'dsh-profile-package-service-'))
+    roots.push(root)
+    const profilesDir = join(root, 'profiles')
+    const profileDir = join(profilesDir, 'test')
+    const first = join(root, 'first')
+    const firstAnchor = pkg(first, '1.0.0')
+    file(join(profileDir, 'entry.mjs'), '')
+    const parentURL = pathToFileURL(join(profileDir, 'entry.mjs')).href
+
+    const ctx = new Context()
+    contexts.push(ctx)
+    ctx.baseUrl = pathToFileURL(profileDir).href + '/'
+    await ctx.plugin(PluginPackages, {
+      generation: generation(profilesDir, profileDir, first, firstAnchor, '1.0.0'),
+    })
+
+    expect(ctx.pluginPackages.packageOf('metadata-lib/private', parentURL)).toMatchObject({
+      name: 'metadata-lib',
+      version: '1.0.0',
+      dir: first,
+    })
+    const require = createRequire(join(profileDir, 'entry.cjs'))
+    expect(require.resolve('metadata-lib')).toBe(realpathSync(join(first, 'index.cjs')))
+    expect(ctx.pluginPackages.packageOf('node:fs', parentURL)).toBeUndefined()
+    expect(ctx.pluginPackages.packageOf('./local.js', parentURL)).toBeUndefined()
+
+    await ctx.fiber.dispose()
+    contexts.pop()
+    expect(() => { require.resolve('metadata-lib') }).toThrow(/Cannot find module/u)
+  })
+
+  it('uses native package lookup without a registration and caches parsed metadata', async () => {
+    const root = mkdtempSync(join(tmpdir(), 'dsh-native-package-service-'))
+    roots.push(root)
+    const packageDir = join(root, 'node_modules', '@scope', 'metadata')
+    file(join(packageDir, 'package.json'), JSON.stringify({ name: '@scope/metadata' }))
+    const parentURL = pathToFileURL(join(root, 'entry.mjs')).href
+    const ctx = new Context()
+    contexts.push(ctx)
+    await ctx.plugin(PluginPackages)
+
+    const first = ctx.pluginPackages.packageOf('@scope/metadata/subpath', parentURL)
+    expect(first).toMatchObject({ name: '@scope/metadata', version: undefined, dir: packageDir })
+    expect(ctx.pluginPackages.packageOf('@scope/metadata', parentURL)).toBe(first)
+    expect(ctx.pluginPackages.packageOf('missing-package', parentURL)).toBeUndefined()
+    expect(ctx.pluginPackages.packageOf('@scope', parentURL)).toBeUndefined()
+    expect(() => { ctx.pluginPackages.replace({
+      profilesDir: join(root, 'profiles'), profileDir: undefined, localPackageNames: [], entries: [],
+    }) }).toThrow(/runtime resolution is not installed/u)
+  })
+
+  it('rejects malformed package metadata selected by the resolver', async () => {
+    const root = mkdtempSync(join(tmpdir(), 'dsh-invalid-package-service-'))
+    roots.push(root)
+    const anonymous = join(root, 'node_modules', 'anonymous')
+    file(join(anonymous, 'package.json'), '{}')
+    const ctx = new Context()
+    contexts.push(ctx)
+    await ctx.plugin(PluginPackages)
+    const parentURL = pathToFileURL(join(root, 'entry.mjs')).href
+    expect(ctx.pluginPackages.packageOf('missing', parentURL)).toBeUndefined()
+    expect(ctx.pluginPackages.packageOf('anonymous', parentURL)).toMatchObject({
+      name: 'anonymous',
+      version: undefined,
+      dir: anonymous,
+    })
+  })
+
+  it('returns undefined when a selected package directory has no manifest', async () => {
+    const root = mkdtempSync(join(tmpdir(), 'dsh-missing-package-service-'))
+    roots.push(root)
+    const profilesDir = join(root, 'profiles')
+    const profileDir = join(profilesDir, 'test')
+    const missing = join(root, 'missing')
+    const ctx = new Context()
+    contexts.push(ctx)
+    await ctx.plugin(PluginPackages, {
+      generation: generation(profilesDir, profileDir, missing, join(root, 'owner.json'), '1.0.0'),
+    })
+    expect(ctx.pluginPackages.packageOf(
+      'metadata-lib', pathToFileURL(join(profileDir, 'entry.mjs')).href,
+    )).toBeUndefined()
+  })
+
+  it('does not revive a stale disk fallback after the runtime generation misses', async () => {
+    const root = mkdtempSync(join(tmpdir(), 'dsh-stale-package-service-'))
+    roots.push(root)
+    const profilesDir = join(root, 'profiles')
+    const profileDir = join(profilesDir, 'test')
+    pkg(join(profilesDir, 'node_modules', 'stale-metadata'), '0.9.0', 'stale-metadata')
+    const ctx = new Context()
+    contexts.push(ctx)
+    await ctx.plugin(PluginPackages, {
+      generation: { profilesDir, profileDir, localPackageNames: [], entries: [] },
+    })
+
+    expect(ctx.pluginPackages.packageOf(
+      'stale-metadata', pathToFileURL(join(profileDir, 'entry.mjs')).href,
+    )).toBeUndefined()
+  })
+
+  it('publishes additive generations to the process and future Workers', async () => {
+    const root = mkdtempSync(join(tmpdir(), 'dsh-package-service-generation-'))
+    roots.push(root)
+    const profilesDir = join(root, 'profiles')
+    const profileDir = join(profilesDir, 'test')
+    const first = join(root, 'first')
+    const firstAnchor = pkg(first, '1.0.0')
+    const initial = generation(profilesDir, profileDir, first, firstAnchor, '1.0.0')
+    const key = '@deepseek-ai/dsh-app-boot/profile-resolution'
+    const previous = getEnvironmentData(key)
+    const ctx = new Context()
+    contexts.push(ctx)
+    await ctx.plugin(PluginPackages, { generation: initial, behavior: 'verify' })
+    expect(getEnvironmentData(key)).toEqual({ generation: initial, behavior: 'verify' })
+
+    const added = join(root, 'added')
+    const addedAnchor = pkg(added, '2.0.0', 'added-metadata')
+    const next = {
+      ...initial,
+      entries: [...initial.entries, {
+        name: 'added-metadata', packageDir: added, version: '2.0.0',
+        declarer: addedAnchor, scope: 'installation' as const,
+      }],
+    }
+    ctx.pluginPackages.replace(next)
+    expect(getEnvironmentData(key)).toEqual({ generation: next, behavior: 'verify' })
+    expect(ctx.pluginPackages.packageOf(
+      'added-metadata', pathToFileURL(join(profileDir, 'entry.mjs')).href,
+    )).toMatchObject({ name: 'added-metadata', version: '2.0.0', dir: added })
+
+    await ctx.fiber.dispose()
+    contexts.pop()
+    expect(getEnvironmentData(key)).toBe(previous)
+  })
+})

+ 70 - 0
packages/boot/app-boot/tests/profile-resolution-worker-bootstrap.spec.ts

@@ -0,0 +1,70 @@
+/** Worker bootstrap installs only the generation inherited from its parent. */
+
+import { afterEach, beforeEach, expect, it, vi } from 'vitest'
+import type { ProfileResolutionGeneration } from '../src/profile.ts'
+
+const harness = vi.hoisted(() => ({
+  data: undefined as {
+    generation: ProfileResolutionGeneration
+    behavior: 'enforce' | 'verify'
+    nativeCacheDir?: string
+  } | undefined,
+  install: vi.fn(),
+}))
+
+let previousNativeCacheDir: string | undefined
+
+vi.mock('node:worker_threads', () => ({
+  getEnvironmentData: () => harness.data,
+}))
+
+vi.mock('../src/profile-resolution/resolver.ts', () => ({
+  installProfileResolution: harness.install,
+}))
+
+beforeEach(() => {
+  previousNativeCacheDir = process.env.NARB_NATIVE_CACHE_DIR
+  delete process.env.NARB_NATIVE_CACHE_DIR
+  harness.data = undefined
+  harness.install.mockReset()
+  vi.resetModules()
+})
+
+afterEach(() => {
+  if (previousNativeCacheDir === undefined) delete process.env.NARB_NATIVE_CACHE_DIR
+  else process.env.NARB_NATIVE_CACHE_DIR = previousNativeCacheDir
+})
+
+it('does nothing without inherited profile resolution data', async () => {
+  await import('../src/profile-resolution/worker-bootstrap.ts')
+  expect(harness.install).not.toHaveBeenCalled()
+})
+
+it('installs the inherited generation and behavior', async () => {
+  const generation: ProfileResolutionGeneration = {
+    profilesDir: '/profiles',
+    profileDir: '/profiles/test',
+    localPackageNames: [],
+    entries: [],
+  }
+  harness.data = { generation, behavior: 'verify', nativeCacheDir: '/private/native-cache' }
+  harness.install.mockImplementation(() => {
+    expect(process.env.NARB_NATIVE_CACHE_DIR).toBe('/private/native-cache')
+  })
+  await import('../src/profile-resolution/worker-bootstrap.ts')
+  expect(harness.install).toHaveBeenCalledWith(generation, 'verify')
+  expect(process.env.NARB_NATIVE_CACHE_DIR).toBeUndefined()
+})
+
+it('restores an existing native-cache environment value after bootstrap', async () => {
+  const generation: ProfileResolutionGeneration = {
+    profilesDir: '/profiles',
+    profileDir: '/profiles/test',
+    localPackageNames: [],
+    entries: [],
+  }
+  process.env.NARB_NATIVE_CACHE_DIR = '/existing/native-cache'
+  harness.data = { generation, behavior: 'enforce' }
+  await import('../src/profile-resolution/worker-bootstrap.ts')
+  expect(process.env.NARB_NATIVE_CACHE_DIR).toBe('/existing/native-cache')
+})

+ 701 - 0
packages/boot/app-boot/tests/profile-resolution.spec.ts

@@ -0,0 +1,701 @@
+/** Runtime profile resolution uses one eager generation for ESM and CommonJS. */
+
+import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { tmpdir } from 'node:os'
+import { dirname, join } from 'node:path'
+import { pathToFileURL } from 'node:url'
+import { getEnvironmentData } from 'node:worker_threads'
+import { afterEach, describe, expect, it } from 'vitest'
+import {
+  installProfileResolution,
+  registerWorkerResolution,
+  type ProfileResolutionRegistration,
+} from '../src/profile-resolution/resolver.ts'
+import {
+  createProfileResolutionGeneration,
+  healProfilesModuleFallback,
+  type Profile,
+  type ProfileResolutionGeneration,
+} from '../src/profile.ts'
+
+const roots: string[] = []
+const registrations: ProfileResolutionRegistration[] = []
+
+afterEach(() => {
+  for (const registration of registrations.splice(0).reverse()) registration.dispose()
+  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
+})
+
+function file(path: string, text: string): void {
+  mkdirSync(dirname(path), { recursive: true })
+  writeFileSync(path, text)
+}
+
+function pkg(
+  dir: string,
+  name: string,
+  marker: number,
+  dependencies: Record<string, string> = {},
+  peerDependencies: Record<string, string> = {},
+): string {
+  file(join(dir, 'package.json'), JSON.stringify({
+    name,
+    version: `${String(marker)}.0.0`,
+    type: 'module',
+    exports: { import: './index.js', require: './index.cjs' },
+    dependencies,
+    peerDependencies,
+  }))
+  file(join(dir, 'index.js'), `export const marker = ${String(marker)}\n`)
+  file(join(dir, 'index.cjs'), `module.exports = { marker: ${String(marker)} }\n`)
+  return join(dir, 'package.json')
+}
+
+function conditionalPkg(dir: string, name: string, importMarker: number, requireMarker: number): string {
+  file(join(dir, 'package.json'), JSON.stringify({
+    name,
+    version: '1.0.0',
+    type: 'module',
+    exports: { custom: './custom.cjs', import: './import.js', require: './require.cjs' },
+  }))
+  file(join(dir, 'import.js'), `export const marker = ${String(importMarker)}\n`)
+  file(join(dir, 'require.cjs'), `module.exports = { marker: ${String(requireMarker)} }\n`)
+  file(join(dir, 'custom.cjs'), 'module.exports = { marker: 13 }\n')
+  return join(dir, 'package.json')
+}
+
+async function importFrom(specifier: string, parent: string): Promise<Record<string, unknown>> {
+  const addon = createRequire(import.meta.url)('node-addon-require-builtin') as {
+    requireBuiltin(id: string): unknown
+  }
+  const loader = addon.requireBuiltin('internal/modules/esm/loader') as {
+    getOrInitializeCascadedLoader(): {
+      import(specifier: string, parent: string, attributes: ImportAttributes): Promise<Record<string, unknown>>
+    }
+  }
+  return await loader.getOrInitializeCascadedLoader().import(specifier, parent, {})
+}
+
+function resolveFrom(
+  specifier: string, parent: string | undefined, attributes: ImportAttributes = {}, skipSyncHooks = false,
+): string {
+  const addon = createRequire(import.meta.url)('node-addon-require-builtin') as {
+    requireBuiltin(id: string): unknown
+  }
+  const loader = addon.requireBuiltin('internal/modules/esm/loader') as {
+    getOrInitializeCascadedLoader(): {
+      getOrCreateModuleJob?: unknown
+      resolveSync(
+        first: string | undefined,
+        second: string | undefined | { specifier: string; attributes: ImportAttributes },
+        third?: ImportAttributes | boolean,
+      ): { url: string }
+    }
+  }
+  const internal = loader.getOrInitializeCascadedLoader()
+  if (!('getOrCreateModuleJob' in internal)) return internal.resolveSync(specifier, parent, attributes).url
+  return skipSyncHooks
+    ? internal.resolveSync(parent, { specifier, attributes }, true).url
+    : internal.resolveSync(parent, { specifier, attributes }).url
+}
+
+function fixture(name = 'resolution-lib'): {
+  root: string
+  installAnchor: string
+  installed: string
+  profile: Profile
+} {
+  // macOS exposes tmpdir through /var while Node returns resolved module paths through /private/var.
+  const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-profile-generation-')))
+  roots.push(root)
+  const installDir = join(root, 'install')
+  const installed = join(installDir, 'node_modules', name)
+  const installAnchor = pkg(installDir, 'test-app', 0, { [name]: '*' })
+  pkg(installed, name, 1)
+  const profileDir = join(root, 'profiles', 'test')
+  file(join(profileDir, 'package.json'), JSON.stringify({
+    name: 'test-profile', private: true, dependencies: { 'missing-local': '*' },
+  }))
+  return {
+    root,
+    installAnchor,
+    installed,
+    profile: {
+      name: 'test',
+      dir: profileDir,
+      layers: [],
+      patchPath: join(profileDir, 'cordis.patch.yml'),
+      patches: [],
+      patchReload: 'startup',
+    },
+  }
+}
+
+async function generationOf(f: ReturnType<typeof fixture>): Promise<ProfileResolutionGeneration> {
+  return await healProfilesModuleFallback({
+    installAnchor: f.installAnchor,
+    profile: f.profile,
+    home: f.root,
+    materialize: false,
+  })
+}
+
+describe('profile resolution generation', { concurrent: false }, () => {
+  it('computes the old fallback graph without materializing it', async () => {
+    const f = fixture()
+    const generation = await generationOf(f)
+    expect(generation.entries.find(entry => entry.name === 'resolution-lib')).toMatchObject({
+      packageDir: f.installed,
+      version: '1.0.0',
+      declarer: f.installAnchor,
+      scope: 'installation',
+    })
+    expect(existsSync(join(generation.profilesDir, 'node_modules'))).toBe(false)
+    expect(existsSync(join(f.profile.dir, '.dsh-module-fallback'))).toBe(false)
+    expect(Object.isFrozen(generation)).toBe(true)
+    expect(Object.isFrozen(generation.entries)).toBe(true)
+    expect(generation.entries.every(Object.isFrozen)).toBe(true)
+
+    const installationOnly = await createProfileResolutionGeneration({
+      installAnchor: f.installAnchor,
+      home: join(f.root, 'installation-only-home'),
+    })
+    expect(installationOnly.profileDir).toBeUndefined()
+    expect(installationOnly.localPackageNames).toEqual([])
+    const registration = installProfileResolution(installationOnly)
+    registrations.push(registration)
+    expect(createRequire(join(installationOnly.profilesDir, 'entry.cjs'))('resolution-lib'))
+      .toEqual({ marker: 1 })
+  })
+
+  it('fails generation construction before writing when the profile manifest is malformed', async () => {
+    const f = fixture()
+    file(join(f.profile.dir, 'package.json'), '{')
+    await expect(generationOf(f)).rejects.toThrow(SyntaxError)
+    expect(existsSync(join(f.root, 'profiles', 'node_modules'))).toBe(false)
+  })
+
+  it('materializes exactly the package targets in the computed generation', async () => {
+    const f = fixture()
+    const computed = await generationOf(f)
+    const materialized = await healProfilesModuleFallback({
+      installAnchor: f.installAnchor,
+      profile: f.profile,
+      home: f.root,
+    })
+    expect(materialized).toEqual(computed)
+    for (const entry of computed.entries) {
+      const projected = entry.scope === 'installation'
+        ? join(computed.profilesDir, 'node_modules', entry.name)
+        : join(f.profile.dir, '.dsh-module-fallback', 'node_modules', entry.name)
+      expect(realpathSync(projected)).toBe(realpathSync(entry.packageDir))
+    }
+  })
+
+  it('keeps each earlier root complete before considering a later root', async () => {
+    const f = fixture('installation-bridge')
+    const installedBridge = f.installed
+    const installationChoice = join(installedBridge, 'node_modules', 'ordered-choice')
+    pkg(installedBridge, 'installation-bridge', 1, { 'ordered-choice': '*' }, { 'peer-choice': '*' })
+    pkg(installationChoice, 'ordered-choice', 1)
+    const peerChoice = join(installedBridge, 'node_modules', 'peer-choice')
+    pkg(peerChoice, 'peer-choice', 3)
+    const bundleDir = join(f.root, 'bundle')
+    pkg(bundleDir, 'test-bundle', 0, { 'ordered-choice': '*', 'bundle-bridge': '*' })
+    pkg(join(bundleDir, 'node_modules', 'ordered-choice'), 'ordered-choice', 2)
+    const bundleBridge = join(bundleDir, 'node_modules', 'bundle-bridge')
+    pkg(bundleBridge, 'bundle-bridge', 0, { 'bundle-choice': '*' })
+    const firstBundleChoice = join(bundleBridge, 'node_modules', 'bundle-choice')
+    pkg(firstBundleChoice, 'bundle-choice', 1)
+    const laterBundle = join(f.root, 'later-bundle')
+    pkg(laterBundle, 'later-bundle', 0, { 'bundle-choice': '*' })
+    pkg(join(laterBundle, 'node_modules', 'bundle-choice'), 'bundle-choice', 2)
+    f.profile.layers.push({
+      packageName: 'test-bundle',
+      packageDir: bundleDir,
+      patchPath: join(bundleDir, 'cordis.patch.yml'),
+      patches: [],
+    }, {
+      packageName: 'later-bundle',
+      packageDir: laterBundle,
+      patchPath: join(laterBundle, 'cordis.patch.yml'),
+      patches: [],
+    })
+
+    const generation = await generationOf(f)
+    expect(generation.entries.find(entry => entry.name === 'ordered-choice')).toMatchObject({
+      packageDir: installationChoice,
+      scope: 'installation',
+    })
+    expect(generation.entries.find(entry => entry.name === 'bundle-choice')).toMatchObject({
+      packageDir: firstBundleChoice,
+      scope: 'profile',
+    })
+    expect(generation.entries.find(entry => entry.name === 'peer-choice')).toMatchObject({
+      packageDir: peerChoice,
+      scope: 'installation',
+    })
+  })
+
+  it('routes ESM and CommonJS through the same installation entry', async () => {
+    const f = fixture()
+    const registration = installProfileResolution(await generationOf(f))
+    registrations.push(registration)
+    const require = createRequire(join(f.profile.dir, 'entry.cjs'))
+    expect(require('resolution-lib')).toEqual({ marker: 1 })
+    expect(require.resolve('resolution-lib')).toBe(join(f.installed, 'index.cjs'))
+    const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
+    expect(resolveFrom('resolution-lib', parent)).toBe(pathToFileURL(join(f.installed, 'index.js')).href)
+    expect(resolveFrom('resolution-lib', parent)).toBe(pathToFileURL(join(f.installed, 'index.js')).href)
+    expect(import.meta.resolve('resolution-lib', parent)).toBe(pathToFileURL(join(f.installed, 'index.js')).href)
+    expect(await importFrom('resolution-lib', parent)).toMatchObject({ marker: 1 })
+  })
+
+  it('routes an application-owned profile outside the shared profiles directory', async () => {
+    const f = fixture()
+    const profileDir = join(f.root, 'application-profile')
+    file(join(profileDir, 'package.json'), JSON.stringify({ name: 'application-profile', private: true }))
+    const profile = {
+      ...f.profile,
+      dir: profileDir,
+      patchPath: join(profileDir, 'cordis.patch.yml'),
+    }
+    const generation = await healProfilesModuleFallback({
+      installAnchor: f.installAnchor,
+      profile,
+      home: f.root,
+      materialize: false,
+    })
+    const registration = installProfileResolution(generation)
+    registrations.push(registration)
+    const require = createRequire(join(profileDir, 'entry.cjs'))
+    expect(require('resolution-lib')).toEqual({ marker: 1 })
+    const parent = pathToFileURL(join(profileDir, 'entry.mjs')).href
+    expect(resolveFrom('resolution-lib', parent)).toBe(pathToFileURL(join(f.installed, 'index.js')).href)
+    expect(await importFrom('resolution-lib', parent)).toMatchObject({ marker: 1 })
+    expect(() => { createRequire(join(f.root, 'outside.cjs'))('resolution-lib') }).toThrow(/Cannot find module/u)
+  })
+
+  it('does not reuse a default route for explicit CommonJS paths', async () => {
+    const f = fixture()
+    const alternative = join(f.root, 'alternative')
+    const alternativePackage = join(alternative, 'node_modules', 'resolution-lib')
+    pkg(alternativePackage, 'resolution-lib', 2)
+    const registration = installProfileResolution(await generationOf(f))
+    registrations.push(registration)
+    const require = createRequire(join(f.profile.dir, 'entry.cjs'))
+    expect(require.resolve('resolution-lib')).toBe(join(f.installed, 'index.cjs'))
+    expect(require.resolve('resolution-lib', { paths: [alternative] })).toBe(join(alternativePackage, 'index.cjs'))
+    expect(require.resolve('resolution-lib', { paths: [f.profile.dir] })).toBe(join(f.installed, 'index.cjs'))
+    expect(require.resolve('resolution-lib', { paths: [join(f.root, 'missing'), f.profile.dir] }))
+      .toBe(join(f.installed, 'index.cjs'))
+    const invalid = join(f.root, 'invalid')
+    file(join(invalid, 'node_modules', 'resolution-lib', 'package.json'), '{')
+    expect(() => { require.resolve('resolution-lib', { paths: [invalid, f.profile.dir] }) })
+      .toThrow(/Invalid package config/u)
+  })
+
+  it('keeps a profile-local package ahead of the generation', async () => {
+    const f = fixture()
+    file(join(f.profile.dir, 'package.json'), JSON.stringify({
+      name: 'test-profile',
+      private: true,
+      dependencies: { 'resolution-lib': '*', 'linked-local': '*' },
+    }))
+    pkg(join(f.profile.dir, 'node_modules', 'resolution-lib'), 'resolution-lib', 2)
+    const linkedLocal = join(f.root, 'linked-local')
+    pkg(linkedLocal, 'linked-local', 3)
+    symlinkSync(
+      linkedLocal,
+      join(f.profile.dir, 'node_modules', 'linked-local'),
+      process.platform === 'win32' ? 'junction' : 'dir',
+    )
+    const generation = await generationOf(f)
+    expect(generation.localPackageNames).toEqual(['resolution-lib', 'linked-local'])
+    const registration = installProfileResolution(generation)
+    registrations.push(registration)
+    const require = createRequire(join(f.profile.dir, 'entry.cjs'))
+    expect(require('resolution-lib')).toEqual({ marker: 2 })
+    const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
+    expect(resolveFrom('resolution-lib', parent)).toBe(
+      pathToFileURL(join(f.profile.dir, 'node_modules', 'resolution-lib', 'index.js')).href,
+    )
+  })
+
+  it('delegates undeclared local packages and non-package specifiers to Node', async () => {
+    const f = fixture()
+    pkg(join(f.profile.dir, 'node_modules', 'undeclared-local'), 'undeclared-local', 6)
+    file(join(f.profile.dir, 'relative.cjs'), 'module.exports = 7\n')
+    const registration = installProfileResolution(await generationOf(f))
+    registrations.push(registration)
+    const require = createRequire(join(f.profile.dir, 'entry.cjs'))
+    expect(require('undeclared-local')).toEqual({ marker: 6 })
+    expect(require('./relative.cjs')).toBe(7)
+    expect(require('node:path')).toHaveProperty('join')
+    expect(require('path')).toHaveProperty('join')
+    const parent = pathToFileURL(join(f.profile.dir, 'undeclared-entry.mjs')).href
+    expect(registration.packageDir('undeclared-local', parent))
+      .toBe(join(f.profile.dir, 'node_modules', 'undeclared-local'))
+    expect(registration.packageDir('undeclared-local', parent))
+      .toBe(join(f.profile.dir, 'node_modules', 'undeclared-local'))
+    expect(resolveFrom('undeclared-local', parent)).toBe(
+      pathToFileURL(join(f.profile.dir, 'node_modules', 'undeclared-local', 'index.js')).href,
+    )
+    expect(await importFrom('undeclared-local', parent)).toMatchObject({ marker: 6 })
+    expect(resolveFrom('node:path', undefined)).toBe('node:path')
+    expect(resolveFrom('fs', parent)).toBe('node:fs')
+    expect(resolveFrom('node:path', pathToFileURL(join(f.root, 'outside.mjs')).href, {}, true)).toBe('node:path')
+
+    const addon = createRequire(import.meta.url)('node-addon-require-builtin') as { requireBuiltin(id: string): unknown }
+    const internal = addon.requireBuiltin('internal/modules/cjs/loader') as {
+      Module: {
+        _resolveFilename(
+          request: string, parent: { filename?: string } | undefined, isMain: boolean,
+        ): string
+      }
+    }
+    expect(internal.Module._resolveFilename('node:path', undefined, false)).toBe('node:path')
+  })
+
+  it('keeps a legacy CommonJS package without a manifest ahead of the generation', async () => {
+    const f = fixture()
+    file(join(f.profile.dir, 'node_modules', 'resolution-lib', 'index.js'), 'module.exports = { marker: 2 }\n')
+    const registration = installProfileResolution(await generationOf(f))
+    registrations.push(registration)
+    expect(createRequire(join(f.profile.dir, 'entry.cjs'))('resolution-lib')).toEqual({ marker: 2 })
+  })
+
+  it('keeps a profile-local CommonJS package file ahead of the generation', async () => {
+    const f = fixture()
+    file(join(f.profile.dir, 'node_modules', 'resolution-lib.js'), 'module.exports = { marker: 2 }\n')
+    const registration = installProfileResolution(await generationOf(f))
+    registrations.push(registration)
+    expect(createRequire(join(f.profile.dir, 'entry.cjs'))('resolution-lib')).toEqual({ marker: 2 })
+    expect(resolveFrom('resolution-lib', pathToFileURL(join(f.profile.dir, 'entry.mjs')).href)).toBe(
+      pathToFileURL(join(f.installed, 'index.js')).href,
+    )
+  })
+
+  it('preserves the ESM and CommonJS behavior of an empty local package directory', async () => {
+    const f = fixture()
+    mkdirSync(join(f.profile.dir, 'node_modules', 'resolution-lib'), { recursive: true })
+    const registration = installProfileResolution(await generationOf(f))
+    registrations.push(registration)
+    expect(createRequire(join(f.profile.dir, 'entry.cjs'))('resolution-lib')).toEqual({ marker: 1 })
+    expect(() => resolveFrom(
+      'resolution-lib', pathToFileURL(join(f.profile.dir, 'entry.mjs')).href,
+    )).toThrow(/Cannot find/u)
+  })
+
+  it('limits bundle-only entries to the active profile', async () => {
+    const f = fixture()
+    const bundleDir = join(f.root, 'bundle')
+    pkg(bundleDir, 'test-bundle', 0, { 'bundle-only': '*' })
+    const bundleOnly = join(bundleDir, 'node_modules', 'bundle-only')
+    pkg(bundleOnly, 'bundle-only', 4)
+    f.profile.layers.push({
+      packageName: 'test-bundle',
+      packageDir: bundleDir,
+      patchPath: join(bundleDir, 'cordis.patch.yml'),
+      patches: [],
+    })
+    const registration = installProfileResolution(await generationOf(f))
+    registrations.push(registration)
+    expect(createRequire(join(f.profile.dir, 'entry.cjs'))('bundle-only')).toEqual({ marker: 4 })
+    const other = join(f.root, 'profiles', 'other', 'entry.cjs')
+    expect(() => { createRequire(other)('bundle-only') }).toThrow(/Cannot find module/u)
+  })
+
+  it('keeps the generation ahead of packages above the shared fallback position', async () => {
+    const f = fixture()
+    pkg(join(f.root, 'node_modules', 'resolution-lib'), 'resolution-lib', 2)
+    const registration = installProfileResolution(await generationOf(f))
+    registrations.push(registration)
+    const require = createRequire(join(f.profile.dir, 'entry.cjs'))
+    expect(require('resolution-lib')).toEqual({ marker: 1 })
+    expect(await importFrom('resolution-lib', pathToFileURL(join(f.profile.dir, 'entry.mjs')).href))
+      .toMatchObject({ marker: 1 })
+  })
+
+  it('skips stale shared and profile-owned fallback entries when the generation misses', async () => {
+    const f = fixture()
+    pkg(join(f.root, 'profiles', 'node_modules', 'stale-shared'), 'stale-shared', 9)
+    pkg(join(f.root, 'node_modules', 'stale-shared'), 'stale-shared', 3)
+    const target = join(f.root, 'stale-private-target')
+    const owned = join(f.profile.dir, '.dsh-module-fallback', 'node_modules', 'stale-private')
+    const projected = join(f.profile.dir, 'node_modules', 'stale-private')
+    pkg(target, 'stale-private', 9)
+    mkdirSync(dirname(owned), { recursive: true })
+    symlinkSync(target, owned, process.platform === 'win32' ? 'junction' : 'dir')
+    mkdirSync(dirname(projected), { recursive: true })
+    symlinkSync(owned, projected, process.platform === 'win32' ? 'junction' : 'dir')
+    pkg(join(f.root, 'node_modules', 'stale-private'), 'stale-private', 3)
+    const registration = installProfileResolution(await generationOf(f))
+    registrations.push(registration)
+    const require = createRequire(join(f.profile.dir, 'entry.cjs'))
+    for (const name of ['stale-shared', 'stale-private']) {
+      expect(require(name)).toEqual({ marker: 3 })
+      expect(require.resolve(name, { paths: [f.profile.dir] })).toBe(join(f.root, 'node_modules', name, 'index.cjs'))
+      expect(await importFrom(name, pathToFileURL(join(f.profile.dir, `${name}.mjs`)).href))
+        .toMatchObject({ marker: 3 })
+    }
+  })
+
+  it('continues after a canonicalized profiles directory from the matching parent tree', async () => {
+    const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-profile-generation-symlink-')))
+    roots.push(root)
+    const carrier = join(root, 'carrier')
+    const profilesDir = join(root, 'home', 'profiles')
+    const realProfilesDir = join(carrier, 'profiles')
+    const realProfileDir = join(realProfilesDir, 'test')
+    mkdirSync(realProfileDir, { recursive: true })
+    mkdirSync(dirname(profilesDir), { recursive: true })
+    symlinkSync(realProfilesDir, profilesDir, process.platform === 'win32' ? 'junction' : 'dir')
+    pkg(join(realProfilesDir, 'node_modules', 'stale-only'), 'stale-only', 9)
+    pkg(join(carrier, 'node_modules', 'stale-only'), 'stale-only', 3)
+    pkg(join(root, 'home', 'node_modules', 'stale-only'), 'stale-only', 4)
+    const registration = installProfileResolution({
+      profilesDir,
+      profileDir: join(profilesDir, 'test'),
+      localPackageNames: [],
+      entries: [],
+    })
+    registrations.push(registration)
+    expect(createRequire(join(realProfileDir, 'entry.cjs'))('stale-only')).toEqual({ marker: 3 })
+  })
+
+  it('leaves conditional exports to Node', async () => {
+    const f = fixture('conditional-lib')
+    conditionalPkg(f.installed, 'conditional-lib', 11, 12)
+    const registration = installProfileResolution(await generationOf(f))
+    registrations.push(registration)
+    const require = createRequire(join(f.profile.dir, 'entry.cjs'))
+    expect(require('conditional-lib')).toEqual({ marker: 12 })
+    expect(await importFrom('conditional-lib', pathToFileURL(join(f.profile.dir, 'entry.mjs')).href))
+      .toMatchObject({ marker: 11 })
+    const addon = createRequire(import.meta.url)('node-addon-require-builtin') as { requireBuiltin(id: string): unknown }
+    const internal = addon.requireBuiltin('internal/modules/cjs/loader') as {
+      Module: {
+        new(id?: string): { filename?: string; paths?: string[] }
+        _nodeModulePaths(path: string): string[]
+        _resolveFilename(
+          request: string,
+          parent: { filename?: string; paths?: string[] },
+          isMain: boolean,
+          options: { conditions: Set<string> },
+        ): string
+      }
+    }
+    const parentFile = join(f.profile.dir, 'conditional-entry.cjs')
+    const parent = new internal.Module(parentFile)
+    parent.filename = parentFile
+    parent.paths = internal.Module._nodeModulePaths(f.profile.dir)
+    expect(internal.Module._resolveFilename(
+      'conditional-lib', parent, false, { conditions: new Set(['node', 'require', 'custom']) },
+    )).toBe(join(f.installed, 'custom.cjs'))
+  })
+
+  it('does not fall back after Node selects a broken profile-local package', async () => {
+    const f = fixture('broken-lib')
+    file(join(f.profile.dir, 'node_modules', 'broken-lib', 'package.json'), JSON.stringify({
+      name: 'broken-lib',
+      exports: './missing.js',
+    }))
+    const registration = installProfileResolution(await generationOf(f))
+    registrations.push(registration)
+    const require = createRequire(join(f.profile.dir, 'entry.cjs'))
+    expect(() => { require('broken-lib') }).toThrow(/Cannot find module|could not find/u)
+    await expect(importFrom(
+      'broken-lib', pathToFileURL(join(f.profile.dir, 'broken-entry.mjs')).href,
+    )).rejects.toThrow(/Cannot find module|Cannot find package/u)
+  })
+
+  it('detects a dual-mode mismatch instead of accepting another package', async () => {
+    const f = fixture()
+    const disk = await healProfilesModuleFallback({
+      installAnchor: f.installAnchor,
+      profile: f.profile,
+      home: f.root,
+    })
+    const second = join(f.root, 'second')
+    pkg(second, 'resolution-lib', 2)
+    const mismatched = {
+      ...disk,
+      entries: disk.entries.map(entry => entry.name === 'resolution-lib'
+        ? { ...entry, packageDir: second, declarer: join(second, 'package.json') }
+        : entry),
+    }
+    const registration = installProfileResolution(mismatched, 'verify')
+    registrations.push(registration)
+    const require = createRequire(join(f.profile.dir, 'entry.cjs'))
+    expect(() => { require.resolve('resolution-lib') }).toThrow(/profile resolution mismatch/u)
+    await expect(importFrom(
+      'resolution-lib', pathToFileURL(join(f.profile.dir, 'entry.mjs')).href,
+    )).rejects.toThrow(/profile resolution mismatch/u)
+  })
+
+  it('accepts matching disk and generation targets in dual mode', async () => {
+    const f = fixture()
+    const generation = await healProfilesModuleFallback({
+      installAnchor: f.installAnchor,
+      profile: f.profile,
+      home: f.root,
+    })
+    const registration = installProfileResolution(generation, 'verify')
+    registrations.push(registration)
+    const require = createRequire(join(f.profile.dir, 'entry.cjs'))
+    expect(require.resolve('resolution-lib')).toBe(join(f.installed, 'index.cjs'))
+    expect(await importFrom('resolution-lib', pathToFileURL(join(f.profile.dir, 'entry.mjs')).href))
+      .toMatchObject({ marker: 1 })
+  })
+
+  it('publishes an additive generation and replaces its miss cache atomically', async () => {
+    const f = fixture()
+    const added = join(f.root, 'added')
+    pkg(added, 'added-lib', 2)
+    const first = await generationOf(f)
+    const registration = installProfileResolution(first)
+    registrations.push(registration)
+    const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
+    expect(registration.packageDir('added-lib', parent)).toBeUndefined()
+    const require = createRequire(join(f.profile.dir, 'entry.cjs'))
+    registration.replace({
+      ...first,
+      entries: [...first.entries, {
+        name: 'added-lib', packageDir: added, version: '2.0.0',
+        declarer: join(added, 'package.json'), scope: 'installation',
+      }],
+    })
+    expect(registration.packageDir('added-lib', parent)).toBe(added)
+    expect(require.resolve('added-lib')).toBe(join(added, 'index.cjs'))
+    expect(resolveFrom('added-lib', parent)).toBe(pathToFileURL(join(added, 'index.js')).href)
+  })
+
+  it('rejects changing an existing package mapping without publishing it', async () => {
+    const f = fixture()
+    const second = join(f.root, 'second')
+    pkg(second, 'resolution-lib', 2)
+    const first = await generationOf(f)
+    const registration = installProfileResolution(first)
+    registrations.push(registration)
+    const alias = join(f.root, 'resolution-lib-alias')
+    symlinkSync(f.installed, alias, process.platform === 'win32' ? 'junction' : 'dir')
+    registration.replace({
+      ...first,
+      entries: first.entries.map(entry => entry.name === 'resolution-lib'
+        ? { ...entry, packageDir: alias }
+        : entry),
+    })
+    const changed = {
+      ...first,
+      entries: first.entries.map(entry => entry.name === 'resolution-lib'
+        ? { ...entry, packageDir: second, version: '2.0.0', declarer: join(second, 'package.json') }
+        : entry),
+    }
+    expect(() => { registration.replace(changed) }).toThrow(/requires a process restart/u)
+    expect(() => {
+      registration.replace({
+        ...first,
+        entries: first.entries.map(entry => entry.name === 'resolution-lib'
+          ? { ...entry, version: '9.0.0' }
+          : entry),
+      })
+    }).toThrow(/requires a process restart/u)
+    expect(() => {
+      registration.replace({ ...first, localPackageNames: ['resolution-lib'] })
+    }).toThrow(/requires a process restart/u)
+    expect(() => {
+      registration.replace({ ...first, entries: first.entries.filter(entry => entry.name !== 'resolution-lib') })
+    }).toThrow(/requires a process restart/u)
+    expect(() => {
+      registration.replace({ ...first, profilesDir: join(f.root, 'other-profiles') })
+    }).toThrow(/cannot change its profile scope/u)
+    registration.replace({ ...first, localPackageNames: ['new-local'] })
+    registration.replace({ ...first, localPackageNames: ['new-local'] })
+    expect(() => { registration.replace(first) }).toThrow(/removing local package/u)
+    expect(registration.packageDir(
+      'resolution-lib', pathToFileURL(join(f.profile.dir, 'entry.mjs')).href,
+    )).toBe(f.installed)
+  })
+
+  it('leaves non-package and out-of-scope metadata lookups to native resolution', async () => {
+    const f = fixture()
+    const outside = join(f.root, 'outside')
+    const outsidePackage = join(outside, 'node_modules', 'outside-lib')
+    pkg(outsidePackage, 'outside-lib', 5)
+    const scopedPackage = join(outside, 'node_modules', '@scope', 'outside')
+    pkg(scopedPackage, '@scope/outside', 6)
+    const ancestorPackage = join(f.root, 'node_modules', 'ancestor-lib')
+    pkg(ancestorPackage, 'ancestor-lib', 7)
+    const registration = installProfileResolution(await generationOf(f))
+    registrations.push(registration)
+    const profileParent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
+    const outsideParent = pathToFileURL(join(outside, 'entry.mjs')).href
+    expect(registration.packageDir('', profileParent)).toBeUndefined()
+    expect(registration.packageDir('./local.js', profileParent)).toBeUndefined()
+    expect(registration.packageDir('/absolute.js', profileParent)).toBeUndefined()
+    expect(registration.packageDir('\\server\\share', profileParent)).toBeUndefined()
+    expect(registration.packageDir('#internal', profileParent)).toBeUndefined()
+    expect(registration.packageDir('@scope', profileParent)).toBeUndefined()
+    expect(registration.packageDir('node:fs', profileParent)).toBeUndefined()
+    expect(registration.packageDir('resolution-lib/private', profileParent)).toBe(f.installed)
+    expect(registration.packageDir('outside-lib', outsideParent)).toBe(outsidePackage)
+    expect(registration.packageDir('outside-lib', outsideParent)).toBe(outsidePackage)
+    expect(registration.packageDir('@scope/outside', outsideParent)).toBe(scopedPackage)
+    expect(registration.packageDir('@scope/outside/private', outsideParent)).toBe(scopedPackage)
+    expect(registration.packageDir('ancestor-lib', profileParent)).toBe(ancestorPackage)
+    expect(registration.packageDir('ancestor-lib', profileParent)).toBe(ancestorPackage)
+    expect(resolveFrom('outside-lib', outsideParent)).toBe(pathToFileURL(join(outsidePackage, 'index.js')).href)
+    expect(resolveFrom('outside-lib', outsideParent)).toBe(pathToFileURL(join(outsidePackage, 'index.js')).href)
+    expect(registration.packageDir('missing', `${pathToFileURL(f.profile.dir).href}/%ZZ`)).toBeUndefined()
+  })
+
+  it('leaves the published generation intact when successor construction fails', async () => {
+    const f = fixture()
+    const first = await generationOf(f)
+    const registration = installProfileResolution(first)
+    registrations.push(registration)
+    await expect(createProfileResolutionGeneration({
+      installAnchor: join(f.root, 'missing', 'package.json'),
+      profile: f.profile,
+      home: f.root,
+    })).rejects.toThrow()
+    expect(registration.packageDir(
+      'resolution-lib', pathToFileURL(join(f.profile.dir, 'entry.mjs')).href,
+    )).toBe(f.installed)
+  })
+
+  it('publishes and restores the generation inherited by owned Workers', async () => {
+    const f = fixture()
+    const generation = await generationOf(f)
+    const key = '@deepseek-ai/dsh-app-boot/profile-resolution'
+    const previous = getEnvironmentData(key)
+    const dispose = registerWorkerResolution(generation, 'verify')
+    try {
+      expect(getEnvironmentData(key)).toEqual({ generation, behavior: 'verify' })
+    } finally {
+      dispose()
+    }
+    expect(getEnvironmentData(key)).toBe(previous)
+
+    const disposeWithCache = registerWorkerResolution(generation, 'enforce', '/private/native-cache')
+    try {
+      expect(getEnvironmentData(key)).toEqual({
+        generation, behavior: 'enforce', nativeCacheDir: '/private/native-cache',
+      })
+    } finally {
+      disposeWithCache()
+    }
+    expect(getEnvironmentData(key)).toBe(previous)
+  })
+
+  it('restores CommonJS resolution when the registration is disposed', async () => {
+    const f = fixture()
+    const registration = installProfileResolution(await generationOf(f))
+    const require = createRequire(join(f.profile.dir, 'entry.cjs'))
+    expect(require.resolve('resolution-lib')).toBe(join(f.installed, 'index.cjs'))
+    registration.dispose()
+    expect(() => { require.resolve('resolution-lib') }).toThrow(/Cannot find module/u)
+  })
+})

+ 24 - 12
packages/boot/app-boot/tsdown.config.ts

@@ -4,16 +4,28 @@ import { defineConfig } from 'tsdown'
  * Embed Include while keeping Loader external so the built include tree and
  * app host bind to one Loader peer.
  */
-export default defineConfig({
-  entry: ['lib/types/index.js'],
-  outDir: 'lib',
-  format: ['esm'],
-  platform: 'node',
-  target: 'es2024',
-  fixedExtension: false,
-  dts: false,
-  clean: false,
-  deps: {
-    alwaysBundle: ['@deepseek-ai/cordis-plugin-include'],
+export default defineConfig([
+  {
+    entry: ['lib/types/index.js'],
+    outDir: 'lib',
+    format: ['esm'],
+    platform: 'node',
+    target: 'es2024',
+    fixedExtension: false,
+    dts: false,
+    clean: false,
+    deps: {
+      alwaysBundle: ['@deepseek-ai/cordis-plugin-include'],
+    },
   },
-})
+  {
+    entry: { 'worker/profile-resolution-bootstrap': 'lib/types/profile-resolution/worker-bootstrap.js' },
+    outDir: 'lib',
+    format: ['esm'],
+    platform: 'node',
+    target: 'es2024',
+    fixedExtension: false,
+    dts: false,
+    clean: false,
+  },
+])

+ 1 - 1
packages/client/AGENTS.md

@@ -136,7 +136,7 @@ If `test:gui` is red on code you did not touch, neither silently fix nor ignore
 Bringing up a new `packages/client/<name>` plugin package (ui-workspace is a complete example; ui-sidebar/ui-user-questions are minimal skeletons):
 
 1. **Package skeleton**: `package.json` (`@deepseek-ai/dsh-client-<name>`, exports `.`/`./client`/`./src/*`/`./package.json`, optional `./invariant` only for an independent runtime relationship, `dsh.client` manifest, `files` list), `tsconfig.json` (extends `tsconfig.base.client.json`, one `references` entry per workspace dependency), `tsdown.config.ts` (`clientBundle(id, ['lib/types/index.js'])`, plus `lib/types/invariant.js` only when published), `src/index.ts` (empty node-half apply), optional `src/invariant.ts`, `src/css-modules.d.ts` when using CSS Modules, and `README.md` with the Model Experience section and the reason when no invariant is published.
-2. **Three registration surfaces, all required** (missing any one fails at a different, later point): the `tsconfig.client.json` aggregate `references` entry; a `dsh.client` row in `packages/bundle/web-app/cordis.patch.yml`; a `packages/bundle/web-app/package.json` dependency (profile boots resolve bare row names through the healed `$DSH_HOME/profiles/node_modules` fallback, which mirrors the app's and each bundle's declared dependencies — a row whose package no manifest declares fails to import). `pnpm-workspace.yaml` already globs `packages/*/*`.
+2. **Three registration surfaces, all required** (missing any one fails at a different, later point): the `tsconfig.client.json` aggregate `references` entry; a `dsh.client` row in `packages/bundle/web-app/cordis.patch.yml`; a `packages/bundle/web-app/package.json` dependency (profile boots resolve bare row names through the runtime generation computed from the app's and each bundle's declared dependencies — a row whose package no manifest declares fails to import). `pnpm-workspace.yaml` already globs `packages/*/*`.
 3. **dsh.client manifest semantics**: `platform: 'web'` always, and the declaration requires a `./client` export (the scan throws without one); `immediately: true` only for stage-one-prefetch infrastructure rows. `inject` lists package-name dependency edges — they are **informational only** (preflight display, HMR diffing); they do not sequence entry activation or apply order. Activation order is Cordis fiber inject waiting on *services*, nothing else. A non-baseline `external` request sequences its dynamic supplier ahead of the consumer — see [shared modules](#shared-modules-and-the-module-graph).
 4. **Registering into another package's slot**: apply order is unconstrained, and a business service is not a declaration barrier. Use `ctx.slots.inject(name, () => ctx.slots.register(...))`; it waits on the actual declaration, removes the contribution when that declaration collapses, reruns after redeclaration, and leaves with the caller's plugin fiber. Return a generator yielding each registration when several contributions must install and roll back atomically. A bare `slots.register` into an undeclared slot remains an error; keep service edges only for services the contribution actually reads.
 5. Rebuild the bundle (`pnpm --filter <pkg> bundle`) before probing a live `dsh web` server — the registry serves `lib/client.js`, not sources.

+ 1 - 0
packages/experimental/inspector/package.json

@@ -40,6 +40,7 @@
   ],
   "license": "MIT",
   "dependencies": {
+    "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/dsh-brand": "workspace:^",
     "@deepseek-ai/dsh-util-crypto": "workspace:^",
     "@deepseek-ai/schemastery": "workspace:^",

+ 5 - 1
packages/experimental/inspector/tsdown.config.ts

@@ -1,17 +1,21 @@
 import type { UserConfig } from 'tsdown'
 import { clientBundle } from '../../client/tsdown.client.ts'
+import { profileWorkerBanner } from '../../tsdown.worker.ts'
 
 const worker: UserConfig = {
   entry: { worker: 'lib/types/worker/entry.js' },
   outDir: 'lib',
   format: ['esm'],
+  banner: profileWorkerBanner('esm'),
   platform: 'node',
   target: 'es2024',
   fixedExtension: false,
   dts: false,
   clean: false,
   outputOptions: { inlineDynamicImports: true },
-  deps: { neverBundle: specifier => specifier === 'ws' },
+  deps: { neverBundle: specifier => (
+    specifier === 'ws' || specifier === '@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap'
+  ) },
 }
 
 /** Build the Host plugin and Worker during the Host pass, and the dynamic Client plugin during the Client pass. */

+ 1 - 0
packages/llm/plugin-package-inventory-deepseek/package.json

@@ -49,6 +49,7 @@
     }
   },
   "devDependencies": {
+    "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/cordis-plugin-include": "workspace:^",
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",

+ 14 - 8
packages/llm/plugin-package-inventory-deepseek/src/index.ts

@@ -17,6 +17,7 @@ import type {} from '@deepseek-ai/dsh-agent'
 import type {} from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
 import type { SessionId } from '@deepseek-ai/dsh-session'
 import type {} from '@deepseek-ai/dsh-agent-presets'
+import type {} from '@deepseek-ai/dsh-app-boot'
 import type { DeepSeekPluginPackageIdentity, DeepSeekPluginPackageInventoryExtension } from './types.ts'
 import type {} from './types.ts'
 
@@ -69,12 +70,14 @@ function identityFromManifest(path: string, allowAnonymous: boolean): DeepSeekPl
 }
 
 /** Resolve a bare package without requiring it to export `./package.json`. */
-function barePackageManifest(packageName: string, anchors: readonly string[]): string | undefined {
+function barePackageManifest(
+  packageName: string, anchors: readonly string[], packages: Context['pluginPackages'] | undefined,
+): string | undefined {
   for (const anchor of anchors) {
-    const searchPaths = createRequire(anchor).resolve.paths(packageName)
-    /* v8 ignore next -- active non-builtin package entries always have Node package search paths */
-    if (searchPaths === null) continue
-    for (const searchPath of searchPaths) {
+    const pkg = packages?.packageOf(packageName, anchor)
+    if (pkg !== undefined) return pkg.manifestPath
+    if (packages !== undefined) continue
+    for (const searchPath of createRequire(anchor).resolve.paths(packageName) as string[]) {
       const manifest = join(searchPath, packageName, 'package.json')
       if (existsSync(manifest)) return manifest
     }
@@ -99,7 +102,10 @@ class PackageIdentityResolver {
   // TODO: Invalidate manifest identities if in-process package-version replacement becomes a supported upgrade path.
   private readonly cache = new Map<string, DeepSeekPluginPackageIdentity | undefined>()
 
-  constructor(private readonly hostBaseUrl: string) {}
+  constructor(
+    private readonly hostBaseUrl: string,
+    private readonly packages: Context['pluginPackages'] | undefined,
+  ) {}
 
   /** Resolve one Loader entry's owning package, or absence for a non-package loose module. */
   resolve({ entry, bareBaseUrl }: ActiveEntry): DeepSeekPluginPackageIdentity | undefined {
@@ -112,7 +118,7 @@ class PackageIdentityResolver {
     const packageName = barePackageName(entry.options.name)
     let manifest: string | undefined
     if (packageName !== undefined) {
-      manifest = barePackageManifest(packageName, anchors)
+      manifest = barePackageManifest(packageName, anchors, this.packages)
       if (manifest === undefined) {
         throw new Error(`plugin-package-inventory-deepseek: cannot resolve active package ${JSON.stringify(packageName)}`)
       }
@@ -186,7 +192,7 @@ async function collectActivePluginPackages(
 export function apply(ctx: Context, config: Config): void {
   if (config.enabled === false) return
   const hostBaseUrl = ctx.baseUrl ?? import.meta.url
-  const resolver = new PackageIdentityResolver(hostBaseUrl)
+  const resolver = new PackageIdentityResolver(hostBaseUrl, ctx.get('pluginPackages'))
   ctx.deepseekLlmApiExtensions.register('dsh_plugin_packages', {
     prepare: async (request) => {
       const value: DeepSeekPluginPackageInventoryExtension = {

+ 18 - 2
packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts

@@ -11,6 +11,7 @@ import { SessionId } from '@deepseek-ai/dsh-session'
 import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { createScope } from '@deepseek-ai/dsh-scope'
 import AgentPresets, { mountPreset } from '@deepseek-ai/dsh-agent-presets'
+import { PluginPackages } from '@deepseek-ai/dsh-app-boot'
 import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
 import * as PluginInventory from '../src/index.ts'
 
@@ -36,13 +37,16 @@ async function packagePlugin(
   return `./${dir}/plugin.mjs`
 }
 
-async function harness(enabled?: boolean): Promise<{ ctx: Context; root: string; disposeInventory: () => Promise<void> }> {
+async function harness(
+  enabled?: boolean, packageService = false,
+): Promise<{ ctx: Context; root: string; disposeInventory: () => Promise<void> }> {
   const root = await mkdtemp(join(tmpdir(), 'dsh-plugin-packages-'))
   roots.push(root)
   const ctx = new Context()
   contexts.push(ctx)
   ctx.baseUrl = pathToFileURL(join(root, 'cordis.yml')).href
   await ctx.plugin(Loader)
+  if (packageService) await ctx.plugin(PluginPackages)
   ctx.loader.builtins.include = Include
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(SessionProjectionRegistry)
@@ -125,7 +129,7 @@ describe('DeepSeek plugin package inventory', () => {
   })
 
   it('resolves scoped and unscoped bare subpaths, absolute/file modules, and skips URL or Cordis modules', async () => {
-    const { ctx, root } = await harness()
+    const { ctx, root } = await harness(undefined, true)
     await packagePlugin(root, 'node_modules/plain-package', { name: 'plain-package', version: '1.0.0' })
     await packagePlugin(root, 'node_modules/@scope/scoped-package', { name: '@scope/scoped-package', version: '2.0.0' })
     await packagePlugin(root, 'absolute-package', { name: 'absolute-package', version: '3.0.0' })
@@ -169,6 +173,18 @@ describe('DeepSeek plugin package inventory', () => {
       .rejects.toThrow(/cannot resolve active package/)
   })
 
+  it('does not bypass the profile package service for a missing bare package', async () => {
+    const { ctx } = await harness(undefined, true)
+    ctx.loader.internal = {
+      version: 'v2',
+      import: async () => ({ default: () => {} }),
+    } as unknown as NonNullable<typeof ctx.loader.internal>
+    await ctx.loader.create({ name: 'missing-profile-package' })
+
+    await expect(ctx.deepseekLlmApiExtensions.prepare({ body: { messages: [] }, signal: SIGNAL }))
+      .rejects.toThrow(/cannot resolve active package/)
+  })
+
   it('supports a direct embedding whose context has no base URL', async () => {
     const ctx = new Context()
     contexts.push(ctx)

+ 3 - 0
packages/llm/plugin-package-inventory-deepseek/tsconfig.json

@@ -8,6 +8,9 @@
     "src"
   ],
   "references": [
+    {
+      "path": "../../boot/app-boot"
+    },
     {
       "path": "../../../vendor/cosmokit"
     },

+ 1 - 0
packages/preset/agent-presets/package.json

@@ -75,6 +75,7 @@
     "zod": "^4.4.3"
   },
   "devDependencies": {
+    "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/cordis-plugin-group": "workspace:^",
     "@deepseek-ai/cordis-plugin-include": "workspace:^",
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",

+ 24 - 24
packages/preset/agent-presets/src/discovery.ts

@@ -98,24 +98,14 @@ export function entryListProblem(rows: unknown, at = ''): string | undefined {
 }
 
 /**
- * Whether a package name is installed anywhere above `base`.
- *
- * Node's own upward `node_modules` walk, stopping at the package directory:
- * the question is whether the package is there at all, which is what a row
- * naming a package a rename or an uninstall took away gets wrong. A pnpm
- * store link answers through the symlink, and a link left dangling by a
- * deleted checkout answers false — the shape a stale profile install leaves.
- *
- * `existsSync` rather than the async `stat`: the walk is a handful of lookups
- * per package and runs on every roster read, where 150 promise round-trips
- * cost more than the lookups they wrap.
- * @param name - the package specifier, possibly carrying a subpath.
+ * Whether a package specifier resolves from `base` without importing it.
+ * @param specifier - the package specifier, possibly carrying a subpath.
  * @param base - the URL to walk up from.
- * @returns true when the package directory is installed above `base`.
+ * @returns true when the package is installed, including an unexported subpath.
  */
+type PackageResolves = (specifier: string, base: string) => boolean
+
 function packageInstalled(name: string, base: string): boolean {
-  // A scoped name spends two segments on the package; anything after either
-  // form is a subpath export, which lives inside the package directory.
   const pkg = name.split('/').slice(0, name.startsWith('@') ? 2 : 1).join('/')
   let dir = fileURLToPath(base)
   for (;;) {
@@ -155,9 +145,11 @@ function packageInstalled(name: string, base: string): boolean {
  * @param harnessBase - base URL a package name resolves against.
  * @returns true when the row names something that can be imported.
  */
-async function rowResolves(row: RowSpecifier, presetBase: string, harnessBase: string): Promise<boolean> {
+async function rowResolves(
+  row: RowSpecifier, presetBase: string, harnessBase: string, resolves: PackageResolves,
+): Promise<boolean> {
   if (row.kind === 'builtin') return true
-  if (row.kind === 'package') return isBuiltin(row.specifier) || packageInstalled(row.specifier, harnessBase)
+  if (row.kind === 'package') return isBuiltin(row.specifier) || resolves(row.specifier, harnessBase)
   const url = row.kind === 'file' ? new URL(row.specifier) : new URL(row.specifier, presetBase)
   return await isFile(fileURLToPath(url))
 }
@@ -195,6 +187,7 @@ async function unresolvableRows(
   rows: readonly unknown[],
   presetBase: string,
   harnessBase: string,
+  resolves: PackageResolves,
   at = '',
 ): Promise<UnresolvableRow[]> {
   const found: UnresolvableRow[] = []
@@ -203,10 +196,10 @@ async function unresolvableRows(
     if (Boolean(row.disabled)) continue
     const positional = at === '' ? `row ${String(index + 1)}` : `${at} row ${String(index + 1)}`
     if (row.group === true) {
-      found.push(...await unresolvableRows(row.config as readonly unknown[], presetBase, harnessBase, positional))
+      found.push(...await unresolvableRows(row.config as readonly unknown[], presetBase, harnessBase, resolves, positional))
       continue
     }
-    if (await rowResolves(classifyRowSpecifier(row.name), presetBase, harnessBase)) continue
+    if (await rowResolves(classifyRowSpecifier(row.name), presetBase, harnessBase, resolves)) continue
     const label = typeof row.id === 'string' && row.id !== '' ? `row "${row.id}"` : positional
     found.push({ label, name: row.name })
   }
@@ -222,7 +215,9 @@ async function unresolvableRows(
  * @param harnessBase - base URL a row's package name resolves against.
  * @returns one human-readable reason, or undefined when the file is loadable.
  */
-async function compositionProblem(path: string, harnessBase: string): Promise<string | undefined> {
+async function compositionProblem(
+  path: string, harnessBase: string, resolves: PackageResolves,
+): Promise<string | undefined> {
   let content: string
   try {
     content = await readFile(path, 'utf8')
@@ -246,7 +241,7 @@ async function compositionProblem(path: string, harnessBase: string): Promise<st
   // The composition's own directory, exactly as `Include` derives it, so a
   // row naming a file the preset ships resolves the way the mount will.
   const presetBase = new URL('.', pathToFileURL(path)).href
-  const unresolvable = await unresolvableRows(rows as readonly unknown[], presetBase, harnessBase)
+  const unresolvable = await unresolvableRows(rows as readonly unknown[], presetBase, harnessBase, resolves)
   const [first] = unresolvable
   if (first === undefined) return undefined
   if (unresolvable.length === 1) {
@@ -287,9 +282,12 @@ async function isFile(path: string): Promise<boolean> {
  * @param root - the directory and the trust its presets inherit.
  * @param harnessBase - base URL a row's package name resolves against; the
  * caller's own `ctx.baseUrl`, which is where the installed harness lives.
+ * @param resolves - package-presence lookup for the active runtime.
  * @returns the root's presets ordered by id.
  */
-export async function scanRoot(root: PresetRoot, harnessBase: string): Promise<AgentPreset[]> {
+export async function scanRoot(
+  root: PresetRoot, harnessBase: string, resolves: PackageResolves = packageInstalled,
+): Promise<AgentPreset[]> {
   const dir = resolve(expandHomePath(root.path))
   let children
   try {
@@ -304,7 +302,7 @@ export async function scanRoot(root: PresetRoot, harnessBase: string): Promise<A
     const directory = join(dir, child.name)
     const path = join(directory, COMPOSITION_FILE)
     const broken = await isFile(path)
-      ? await compositionProblem(path, harnessBase)
+      ? await compositionProblem(path, harnessBase, resolves)
       : `the composition file ${COMPOSITION_FILE} is missing — the directory still occupies the id; delete it or restore the file`
     // Display text only, and never fatal: a preset with unreadable metadata
     // still mounts, it just shows its id.
@@ -326,15 +324,17 @@ export async function scanRoot(root: PresetRoot, harnessBase: string): Promise<A
  * Scan every root in precedence order.
  * @param roots - roots in precedence order; an earlier root wins a duplicate id.
  * @param harnessBase - base URL a row's package name resolves against.
+ * @param resolves - package-presence lookup for the active runtime.
  * @returns every discovered preset, first-root-wins per id.
  */
 export async function discoverPresets(
   roots: readonly PresetRoot[],
   harnessBase: string,
+  resolves: PackageResolves = packageInstalled,
 ): Promise<AgentPreset[]> {
   const byId = new Map<string, AgentPreset>()
   for (const root of roots) {
-    for (const preset of await scanRoot(root, harnessBase)) {
+    for (const preset of await scanRoot(root, harnessBase, resolves)) {
       if (byId.has(preset.id)) continue
       byId.set(preset.id, preset)
     }

+ 9 - 2
packages/preset/agent-presets/src/index.ts

@@ -29,6 +29,7 @@ import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typer
 import { bindScopeParent, createScope, scopeOf, type Scope, type ScopeKey, type ScopeParentBinding } from '@deepseek-ai/dsh-scope'
 // Type-only: resolves the `agent/created` lifecycle event this service watches.
 import type {} from '@deepseek-ai/dsh-agent'
+import type {} from '@deepseek-ai/dsh-app-boot'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import type { AgentPresetDocument, AgentPresetRoster } from './types.ts'
 import type {} from '@deepseek-ai/dsh-session-projection'
@@ -139,7 +140,6 @@ export class AgentPresets extends TypertRemoteService {
    * base here is what lets health answer the question before a session does.
    */
   private readonly harnessBase: string
-
   /**
    * The user layer over `config.default`, present only while a settings
    * provider is composed. Held rather than snapshotted so a hot-reloaded
@@ -263,7 +263,14 @@ export class AgentPresets extends TypertRemoteService {
    * @returns the presets, first-root-wins per id.
    */
   async list(): Promise<AgentPreset[]> {
-    return await discoverPresets(this.resolvedRoots, this.harnessBase)
+    const packages = this.ctx.get('pluginPackages')
+    return packages === undefined
+      ? await discoverPresets(this.resolvedRoots, this.harnessBase)
+      : await discoverPresets(
+        this.resolvedRoots,
+        this.harnessBase,
+        (specifier, base) => packages.packageOf(specifier, base) !== undefined,
+      )
   }
 
   /**

+ 23 - 0
packages/preset/agent-presets/tests/mount.spec.ts

@@ -6,6 +6,7 @@ import { Context } from '@deepseek-ai/cordis'
 import Loader from '@deepseek-ai/cordis-plugin-loader'
 import Include from '@deepseek-ai/cordis-plugin-include'
 import Group from '@deepseek-ai/cordis-plugin-group'
+import { PluginPackages } from '@deepseek-ai/dsh-app-boot'
 import LlmRuntime from '@deepseek-ai/dsh-llm'
 import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
 import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
@@ -44,6 +45,7 @@ async function harness(roster: Config = { default: 'standard', roots: ROOTS, inc
   const ctx = new Context()
   ctx.baseUrl = pathToFileURL(FIXTURES).href + '/'
   await ctx.plugin(Loader)
+  await ctx.plugin(PluginPackages)
   ctx.loader.builtins.include = Include
   // A preset outside this workspace cannot resolve `cordis-plugin-group` by
   // name, so the app registers it as a builtin; the fixtures compose the same
@@ -373,6 +375,27 @@ describe('the preset roster', () => {
     expect(listed.find(preset => preset.id === 'not-a-preset')?.broken).toMatch(/is missing/)
   })
 
+  it('uses the profile package service when checking bare package rows', async () => {
+    const root = await mkdtemp(join(tmpdir(), 'dsh-preset-profile-package-'))
+    roots.push(root)
+    const presetDir = join(root, 'profile-package')
+    await mkdir(presetDir)
+    await writeFile(join(presetDir, COMPOSITION_FILE), '- id: package\n  name: profile-package/plugin.js\n')
+    const scoped = await harness({
+      default: 'profile-package', roots: [{ path: root, trust: 'user' }],
+      includeShippedRoot: false, includeUserRoot: false,
+    })
+    const packageOf = vi.spyOn(scoped.pluginPackages, 'packageOf').mockReturnValue({
+      name: 'profile-package', version: '1.0.0', dir: presetDir,
+      manifestPath: join(presetDir, 'package.json'), manifest: {},
+    })
+
+    const [listed] = await scoped.agentPresets.list()
+    expect(listed).toMatchObject({ id: 'profile-package', trust: 'user' })
+    expect(listed?.broken).toBeUndefined()
+    expect(packageOf).toHaveBeenCalledWith('profile-package/plugin.js', pathToFileURL(FIXTURES).href + '/')
+  })
+
   it('exposes the configured default id', () => {
     expect(ctx.agentPresets.defaultId).toBe('standard')
   })

+ 3 - 0
packages/preset/agent-presets/tsconfig.json

@@ -6,6 +6,9 @@
   },
   "include": ["src"],
   "references": [
+    {
+      "path": "../../boot/app-boot"
+    },
     {
       "path": "../../../vendor/cosmokit"
     },

+ 2 - 2
packages/test-support/loader-smoke/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/test-support/loader-smoke/README.md
-README.md: 4365cc16beadc517390dd4ce8616cdfb6ec9e7be
-README.zh.md: 3d9659acf7b845bab11be3a5f6c0ff37e15b8988
+README.md: e7096aadda968c0724233457eade06d7391d82cd
+README.zh.md: cff7fc96a884664941e2a583b8f7a6de8e69667b

+ 1 - 1
packages/test-support/loader-smoke/README.md

@@ -45,7 +45,7 @@ Set `expectedExitCode` when the scenario pins a designed failure surface — a o
 
 ### Testing a shipped profile
 
-Profile integration drivers use the repository-only `tests/fixtures/production-profile.ts` helper. It loads the named shipped profile and its bundle patches through `loadProfile`, reconciles the profile's module fallback, and passes the bundle patches followed by the test's `*.patch.yml` files to the root `cordis:include` mounted by `boot`. Those patches should contain only the test provider or model, isolated persistence paths, and subject-specific changes. Package-level unit tests that need an agent loop without profile integration mount `dsh-agent-loop-testkit` locally instead.
+Profile integration drivers use the repository-only `tests/fixtures/production-profile.ts` helper. It loads the named shipped profile and its bundle patches through `loadProfile`, materializes the retained link-mode fallback, and passes the bundle patches followed by the test's `*.patch.yml` files to the root `cordis:include` mounted by `boot`. Those patches should contain only the test provider or model, isolated persistence paths, and subject-specific changes. Package-level unit tests that need an agent loop without profile integration mount `dsh-agent-loop-testkit` locally instead.
 
 ### Driving a fixture turn
 

+ 1 - 1
packages/test-support/loader-smoke/README.zh.md

@@ -45,7 +45,7 @@ const result = await runLoaderSmoke({
 
 ### 测试交付 profile
 
-Profile 集成 driver 使用仅限仓库内部的 `tests/fixtures/production-profile.ts` helper。它通过 `loadProfile` 加载指定的已交付 profile 及其组合包 patch,协调处理 profile 的模块回退,然后把组合包 patch 与测试 `*.patch.yml` 文件依次交给 `boot` 挂载的根 `cordis:include`。这些 patch 应只包含测试提供方或模型、隔离持久化路径及被测对象专用变更。只需要 agent loop(智能体循环)而不测试 profile 集成的包级单元测试改为在本地挂载 `dsh-agent-loop-testkit`。
+Profile 集成 driver 使用仅限仓库内部的 `tests/fixtures/production-profile.ts` helper。它通过 `loadProfile` 加载指定的已交付 profile 及其组合包 patch,物化保留的 link-mode fallback,然后把组合包 patch 与测试 `*.patch.yml` 文件依次交给 `boot` 挂载的根 `cordis:include`。这些 patch 应只包含测试提供方或模型、隔离持久化路径及被测对象专用变更。只需要 agent loop 而不测试 profile 集成的包级单元测试改为在本地挂载 `dsh-agent-loop-testkit`。
 
 ### 驱动 fixture 轮次
 

+ 10 - 7
packages/test-support/session-snapshot/src/launcher.ts

@@ -358,7 +358,7 @@ function profileArgs(
   const materializedRoot = join(cwd, '.dsh-profile-patches')
   mkdirSync(materializedRoot, { recursive: true })
   const materializedDir = mkdtempSync(join(materializedRoot, 'launch-'))
-  const materialized = patches.map((file, index) => materializeProfilePatch(file, cwd, materializedDir, index))
+  const materialized = patches.map((file, index) => materializeProfilePatch(file, cwd, profile, materializedDir, index))
   return ['--profile', profile, ...materialized.flatMap(file => ['--patch', file])]
 }
 
@@ -386,14 +386,14 @@ function packageDirFromPatch(source: string, packageName: string): string | unde
 
 /**
  * Install an authored patch's resolvable bare package into the temporary
- * profile fallback. This mirrors `dsh plugin` while retaining the bare entry
+ * profile. This mirrors `dsh plugin` while retaining the bare entry
  * name and package identity used by request metadata.
  */
-function linkProfilePackage(source: string, cwd: string, packageName: string): void {
+function linkProfilePackage(source: string, cwd: string, profile: string, packageName: string): void {
   const packageDir = packageDirFromPatch(source, packageName)
   // The package may instead belong to the dsh installation; profile boot heals those links.
   if (packageDir === undefined) return
-  const link = join(cwd, '.dsh', 'profiles', 'node_modules', packageName)
+  const link = join(cwd, '.dsh', 'profiles', profile, 'node_modules', packageName)
   mkdirSync(dirname(link), { recursive: true })
   if (existsSync(link)) {
     if (realpathSync(link) !== packageDir) {
@@ -408,19 +408,22 @@ function linkProfilePackage(source: string, cwd: string, packageName: string): v
 /**
  * Copy one authored patch into the launch cwd with relative plugin names made absolute.
  * @param source - authored profile patch path.
- * @param cwd - isolated process cwd whose profile fallback receives package links.
+ * @param cwd - isolated process cwd whose profile receives package links.
+ * @param profile - profile whose local package lookup receives the test links.
  * @param targetDir - existing directory that owns the materialized patch.
  * @param index - stable patch ordinal used in the output filename.
  * @returns absolute materialized patch path.
  */
-export function materializeProfilePatch(source: string, cwd: string, targetDir: string, index: number): string {
+export function materializeProfilePatch(
+  source: string, cwd: string, profile: string, targetDir: string, index: number,
+): string {
   const parsed = yaml.load(readFileSync(source, 'utf8'), { schema: entryListSchema })
   if (!Array.isArray(parsed)) throw new Error(`snapshot profile patch must be a top-level array: ${source}`)
   const patches = parsed as PatchOptions[]
   const baseDir = dirname(source)
   const resolveName = (value: string): string => {
     const packageName = barePackageName(value)
-    if (packageName !== undefined) linkProfilePackage(source, cwd, packageName)
+    if (packageName !== undefined) linkProfilePackage(source, cwd, profile, packageName)
     return value.startsWith('./') || value.startsWith('../')
       ? pathToFileURL(resolve(baseDir, value)).href
       : value

+ 3 - 3
packages/test-support/session-snapshot/tests/harness.spec.ts

@@ -233,9 +233,9 @@ describe('runScenario', () => {
     expect(materialized).toContain(pathToFileURL(join(patchDir, 'plugin.mjs')).href)
     expect(materialized).toContain(pathToFileURL(join(dir, 'nested.mjs')).href)
     expect(materialized).toContain('example-package')
-    expect(await realpath(join(dir, '.dsh', 'profiles', 'node_modules', 'example-package')))
+    expect(await realpath(join(dir, '.dsh', 'profiles', 'acp', 'node_modules', 'example-package')))
       .toBe(await realpath(packageDir))
-    expect(await realpath(join(dir, '.dsh', 'profiles', 'node_modules', '@fixture', 'example-package')))
+    expect(await realpath(join(dir, '.dsh', 'profiles', 'acp', 'node_modules', '@fixture', 'example-package')))
       .toBe(await realpath(scopedPackageDir))
     expect(await readFile(await materializedPatch(materializedRoot, '1-selected.cordis.yml'), 'utf8')).toContain('[]')
 
@@ -255,7 +255,7 @@ describe('runScenario', () => {
     const conflictPatch = join(dir, 'conflict.cordis.yml')
     const conflictPackage = join(dir, 'node_modules', 'conflict-package')
     const otherPackage = join(dir, 'other-conflict-package')
-    const conflictLink = join(dir, '.dsh', 'profiles', 'node_modules', 'conflict-package')
+    const conflictLink = join(dir, '.dsh', 'profiles', 'acp', 'node_modules', 'conflict-package')
     await Promise.all([
       mkdir(conflictPackage, { recursive: true }),
       mkdir(otherPackage, { recursive: true }),

+ 13 - 0
packages/tsdown.worker.ts

@@ -0,0 +1,13 @@
+/** Fixed startup code for Node Worker bundles, after the executable's VFS bootstrap. */
+
+/**
+ * Load the parent's profile resolver before Worker business code executes.
+ * ESM static dependencies must resolve without the profile resolver.
+ * @param format - emitted Worker module format.
+ * @returns a tsdown banner that loads the shared native Worker bootstrap.
+ */
+export function profileWorkerBanner(format: 'cjs' | 'esm'): string {
+  return format === 'cjs'
+    ? '"use strict";\nrequire("@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap");'
+    : 'import "@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap";'
+}

+ 1 - 0
packages/typert/loader/package.json

@@ -35,6 +35,7 @@
     "@deepseek-ai/schemastery": "workspace:^"
   },
   "devDependencies": {
+    "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
     "@deepseek-ai/dsh-typert-registry": "workspace:^",
     "@deepseek-ai/cordis": "workspace:^",

+ 30 - 10
packages/typert/loader/src/index.ts

@@ -32,6 +32,7 @@ import { pathToFileURL } from 'node:url'
 import type { Context } from '@deepseek-ai/cordis'
 import z from '@deepseek-ai/schemastery'
 import type {} from '@deepseek-ai/cordis-plugin-loader'
+import type {} from '@deepseek-ai/dsh-app-boot'
 import type {} from '@deepseek-ai/dsh-typert-registry'
 import type { TypertContribution } from '@deepseek-ai/dsh-typert-registry/types'
 
@@ -56,6 +57,11 @@ export const Config: z<Config> = z.object({
 
 type ResolvedConfig = Required<Config>
 
+interface TypertArtifact {
+  packageName: string
+  path: string
+}
+
 const MEMBER_KINDS = new Set(['property', 'method', 'getter', 'setter', 'call', 'construct', 'index'])
 
 /** Resolve the `./typert` export to a relative path, accepting the string and one-level conditional forms. */
@@ -289,7 +295,8 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
   if (ctx.baseUrl === undefined) {
     throw new Error('typert-loader: ctx.baseUrl is unset — the loader needs the config-tree anchor to resolve plugin packages')
   }
-  const require = createRequire(ctx.baseUrl)
+  const baseUrl = ctx.baseUrl
+  const require = createRequire(baseUrl)
   const configured = new Set((config as ResolvedConfig).packages)
 
   // Registered contributions by entry name; the disposer withdraws the entry's registration.
@@ -299,7 +306,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
   // Artifact paths by package name. Negative verdicts (unresolvable specifier —
   // loader builtins, subpath rows — or no typert export) are cached as null and
   // never expire: plugin-set changes take effect on restart.
-  const artifactPath = new Map<string, string | null>()
+  const artifactPath = new Map<string, TypertArtifact | null>()
   // Imported+validated manifests by package name (one import per package per process).
   const manifests = new Map<string, Promise<TypertContribution>>()
   const dirty = new Set<string>()
@@ -312,12 +319,24 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
     }
   }, 'typert loader lifetime')
 
-  const resolveArtifact = (pkgName: string): string | null => {
+  const resolveArtifact = (pkgName: string): TypertArtifact | null => {
     const cached = artifactPath.get(pkgName)
     if (cached !== undefined) return cached
+    const firstSlash = pkgName.indexOf('/')
+    if (firstSlash >= 0 && (pkgName[0] !== '@' || pkgName.indexOf('/', firstSlash + 1) >= 0)) {
+      artifactPath.set(pkgName, null)
+      return null
+    }
     let pkgPath: string
+    let pkg: Record<string, unknown> | undefined
     try {
-      pkgPath = require.resolve(`${pkgName}/package.json`)
+      const packages = ctx.get('pluginPackages')
+      const resolvedPackage = packages?.packageOf(pkgName, baseUrl)
+      if (packages !== undefined && resolvedPackage === undefined) throw new Error('package is absent from the active resolver')
+      pkgPath = resolvedPackage === undefined
+        ? require.resolve(`${pkgName}/package.json`)
+        : resolvedPackage.manifestPath
+      pkg = resolvedPackage?.manifest
     } catch (cause) {
       if (configured.has(pkgName)) {
         throw new Error(
@@ -330,12 +349,13 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
       artifactPath.set(pkgName, null)
       return null
     }
-    const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as Record<string, unknown>
-    const rel = typertExportOf(pkgName, pkg.exports)
+    pkg ??= JSON.parse(readFileSync(pkgPath, 'utf8')) as Record<string, unknown>
+    const manifestName = typeof pkg.name === 'string' ? pkg.name : pkgName
+    const rel = typertExportOf(manifestName, pkg.exports)
     if (rel === undefined && configured.has(pkgName)) {
       throw new Error(`typert-loader: configured package "${pkgName}" does not export "${TYPERT_HOST_EXPORT}"`)
     }
-    const resolved = rel === undefined ? null : join(dirname(pkgPath), rel)
+    const resolved = rel === undefined ? null : { packageName: manifestName, path: join(dirname(pkgPath), rel) }
     artifactPath.set(pkgName, resolved)
     return resolved
   }
@@ -375,9 +395,9 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
       return undefined
     }
     if (registered.has(entryName) || pending.has(entryName)) return undefined
-    const path = resolveArtifact(entryName)
-    if (path === null) return undefined
-    const task = loadManifest(entryName, path).then((manifest) => {
+    const artifact = resolveArtifact(entryName)
+    if (artifact === null) return undefined
+    const task = loadManifest(artifact.packageName, artifact.path).then((manifest) => {
       // The entry may have unmounted (or already re-registered) while the import was in flight.
       if (!active || !qualifies(entryName) || registered.has(entryName)) return
       registered.set(entryName, ctx.typert.register(manifest))

+ 28 - 1
packages/typert/loader/tests/loader.spec.ts

@@ -6,6 +6,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import Loader from '@deepseek-ai/cordis-plugin-loader'
+import { PluginPackages } from '@deepseek-ai/dsh-app-boot'
 import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
 import * as typertLoader from '@deepseek-ai/dsh-typert-loader'
 import { validateTypertManifest } from '@deepseek-ai/dsh-typert-loader'
@@ -27,11 +28,13 @@ async function writePackage(
   base: string,
   pkgName: string,
   options: {
+    manifestName?: string
     typertExport?: boolean
     typertTarget?: unknown
     typertSource?: string
     pluginSource?: string
     omitExports?: boolean
+    pluginSubpath?: string
   } = {},
 ): Promise<void> {
   const dir = join(base, 'node_modules', ...pkgName.split('/'))
@@ -40,8 +43,9 @@ async function writePackage(
   if (options.typertExport !== false && options.typertSource !== undefined) {
     exportsField['./typert'] = options.typertTarget ?? './typert.host.js'
   }
+  if (options.pluginSubpath !== undefined) exportsField[options.pluginSubpath] = './index.js'
   await writeFile(join(dir, 'package.json'), JSON.stringify({
-    name: pkgName,
+    name: options.manifestName ?? pkgName,
     type: 'module',
     ...(options.omitExports ? { main: './index.js' } : { exports: exportsField }),
   }))
@@ -106,6 +110,7 @@ async function boot(): Promise<Context> {
       return module
     },
   } as unknown as NonNullable<typeof context.loader.internal>
+  await context.plugin(PluginPackages)
   // zod must be resolvable from the fixture packages; link the workspace copy.
   await mkdir(join(root as string, 'node_modules'), { recursive: true })
   return context
@@ -227,6 +232,28 @@ describe('typert loader', () => {
     expect(ctx.typert.get('@fixture/with-typert#Thing')).toBeDefined()
   })
 
+  it('skips package-subpath rows and validates npm aliases against the manifest owner', LOADER_TEST_TIMEOUT, async () => {
+    root = await mkdtemp(join(tmpdir(), 'dsh-typert-loader-'))
+    await linkZod(root)
+    await writePackage(root, '@fixture/subpath', {
+      pluginSubpath: './plugin',
+      typertSource: typertSource('@fixture/subpath', 'Subpath'),
+    })
+    await writePackage(root, 'fixture-alias', {
+      manifestName: '@fixture/actual',
+      typertSource: typertSource('@fixture/actual', 'Aliased'),
+    })
+    const ctx = await boot()
+    await ctx.loader.create({ name: '@fixture/subpath/plugin' })
+    await ctx.loader.create({ name: 'fixture-alias' })
+    await ctx.loader.await()
+
+    await mountTypertLoader(ctx)
+
+    expect(ctx.typert.getPackage('@fixture/subpath')).toBeUndefined()
+    expect(ctx.typert.get('@fixture/actual#Aliased')).toBeDefined()
+  })
+
   it('follows entries mounted after activation', LOADER_TEST_TIMEOUT, async () => {
     root = await mkdtemp(join(tmpdir(), 'dsh-typert-loader-'))
     await linkZod(root)

+ 3 - 0
packages/typert/loader/tsconfig.json

@@ -8,6 +8,9 @@
     "src"
   ],
   "references": [
+    {
+      "path": "../../boot/app-boot"
+    },
     {
       "path": "../../../vendor/cosmokit"
     },

+ 1 - 0
packages/workflow/workflow-ptc/package.json

@@ -39,6 +39,7 @@
     "@deepseek-ai/dsh-sandbox-policy": "workspace:^"
   },
   "dependencies": {
+    "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/dsh-brand": "workspace:^",
     "@deepseek-ai/dsh-util-values": "workspace:^",
     "@deepseek-ai/schemastery": "workspace:^"

+ 21 - 3
pnpm-lock.yaml

@@ -374,9 +374,6 @@ importers:
       js-yaml:
         specifier: ^4.2.0
         version: 4.2.0
-      node-addon-require-builtin:
-        specifier: ^0.1.4
-        version: 0.1.4
     devDependencies:
       '@agentclientprotocol/sdk':
         specifier: 1.4.0
@@ -1389,6 +1386,9 @@ importers:
       js-yaml:
         specifier: ^4.2.0
         version: 4.2.0
+      node-addon-require-builtin:
+        specifier: ^0.1.4
+        version: 0.1.4
       resolve.exports:
         specifier: ^2.0.3
         version: 2.0.3
@@ -6169,6 +6169,9 @@ importers:
 
   packages/experimental/inspector:
     dependencies:
+      '@deepseek-ai/dsh-app-boot':
+        specifier: workspace:^
+        version: link:../../boot/app-boot
       '@deepseek-ai/dsh-brand':
         specifier: workspace:^
         version: link:../../util/brand
@@ -7899,6 +7902,9 @@ importers:
       '@deepseek-ai/dsh-agent-presets':
         specifier: workspace:^
         version: link:../../preset/agent-presets
+      '@deepseek-ai/dsh-app-boot':
+        specifier: workspace:^
+        version: link:../../boot/app-boot
       '@deepseek-ai/dsh-deepseek-llm-api-extensions':
         specifier: workspace:^
         version: link:../deepseek-llm-api-extensions
@@ -8212,6 +8218,9 @@ importers:
       '@deepseek-ai/dsh-agent-loop':
         specifier: workspace:^
         version: link:../../core/agent-loop
+      '@deepseek-ai/dsh-app-boot':
+        specifier: workspace:^
+        version: link:../../boot/app-boot
       '@deepseek-ai/dsh-atomic-write':
         specifier: workspace:^
         version: link:../../util/atomic-write
@@ -11268,6 +11277,9 @@ importers:
       '@deepseek-ai/cordis-plugin-loader':
         specifier: workspace:^
         version: link:../../../vendor/loader
+      '@deepseek-ai/dsh-app-boot':
+        specifier: workspace:^
+        version: link:../../boot/app-boot
       '@deepseek-ai/dsh-typert-registry':
         specifier: workspace:^
         version: link:../registry
@@ -11758,6 +11770,9 @@ importers:
 
   packages/workflow/workflow-ptc:
     dependencies:
+      '@deepseek-ai/dsh-app-boot':
+        specifier: workspace:^
+        version: link:../../boot/app-boot
       '@deepseek-ai/dsh-brand':
         specifier: workspace:^
         version: link:../../util/brand
@@ -12096,6 +12111,9 @@ importers:
       '@deepseek-ai/dsh-session-title':
         specifier: workspace:^
         version: link:../../packages/session/session-title
+      '@deepseek-ai/dsh-session-title-llm':
+        specifier: workspace:^
+        version: link:../../packages/session/session-title-llm
       '@deepseek-ai/dsh-settings':
         specifier: workspace:^
         version: link:../../packages/settings/settings

+ 1 - 0
python/sdk-runtime/package.json

@@ -80,6 +80,7 @@
     "@deepseek-ai/dsh-session-reference": "workspace:^",
     "@deepseek-ai/dsh-session-telemetry": "workspace:^",
     "@deepseek-ai/dsh-session-title": "workspace:^",
+    "@deepseek-ai/dsh-session-title-llm": "workspace:^",
     "@deepseek-ai/dsh-settings": "workspace:^",
     "@deepseek-ai/dsh-shell": "workspace:^",
     "@deepseek-ai/dsh-shell-env": "workspace:^",

+ 2 - 0
scripts/check-workspace-constraints.ts

@@ -148,6 +148,8 @@ function workspaceManifests(): WorkspaceManifest[] {
 }
 
 const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
+  // Owned Worker bundles import this public bootstrap before their business entry.
+  '@deepseek-ai/dsh-app-boot': ['lib/worker/profile-resolution-bootstrap.js'],
   // Statically linked client libraries keep their stylesheets next to the emitted
   // JavaScript, which imports them by relative path: the compile shell runs
   // them through its own CSS pipeline, so the sheets are published artifacts.

+ 1 - 0
scripts/gen-cordis-catalog.ts

@@ -159,6 +159,7 @@ export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
   launcherSessionQueryPath: 'not a service: launcher-provided boot-context value (string | undefined) — packages/session-query/session-query-sqlite/README.md owns this launcher contract',
   dshHomePath: 'not a service: boot-provided root accessor function (typeof dshHomePath | undefined) for Loader !!js config expressions — packages/boot/app-boot/README.md owns the boot contract',
   launchEnvironment: 'not a service: launcher-provided root accessor value (LaunchEnvironmentSnapshot | undefined) — packages/util/launch-environment/README.md owns this launcher contract',
+  pluginPackages: 'profile-boot-owned package resolver service used by optional consumers — packages/boot/app-boot/README.md owns this internal API',
   connection: 'interface-typed (HostConnectionHandle); implementing class HostConnectionService is declared in rpc-host.ts — packages/client/connection/README.md owns the API',
   fileUpload: 'client-side browser upload service — packages/client/file-upload/README.md owns the API',
   uiRenderer: 'client-side interface-typed browser service — packages/client/ui-renderer/README.md owns the API',

+ 2 - 2
snapshots/sdk/sdk.snapshot.ts

@@ -543,12 +543,12 @@ async function runScenario(scenario: CorpusScenario): Promise<{
   await mkdir(patchRoot, { recursive: true })
   const assertions = SDK_ASSERTIONS[scenario.name] ?? {}
   const patches = [...authoredPatches(scenario, !recording), ...assertions.patches ?? []]
-    .map((patch, index) => materializeProfilePatch(patch, cwd, patchRoot, index))
+    .map((patch, index) => materializeProfilePatch(patch, cwd, 'sdk', patchRoot, index))
   let childSessionsRoot: string | undefined
   let childEnvironment: Record<string, string> = {}
   if (assertions.dshSdkChild !== undefined) {
     const childHome = join(cwd, '.child-dsh')
-    const childPatch = materializeProfilePatch(assertions.dshSdkChild.config, cwd, patchRoot, patches.length)
+    const childPatch = materializeProfilePatch(assertions.dshSdkChild.config, cwd, 'sdk', patchRoot, patches.length)
     await mkdir(childHome, { recursive: true })
     childSessionsRoot = join(childHome, 'sessions')
     childEnvironment = {

+ 1 - 1
snapshots/session/headless.snapshot.ts

@@ -1085,7 +1085,7 @@ describe('headless recorded-session snapshots', () => {
             await mkdir(join(cwd, patchRoot), { recursive: true })
             patchSources.forEach((source, index) => {
               if (source.endsWith('.snapshot.yml')) {
-                materializeProfilePatch(source, cwd, join(cwd, patchRoot), index)
+                materializeProfilePatch(source, cwd, 'headless', join(cwd, patchRoot), index)
               }
             })
             await seedWorkspace(scenario, cwd)